Squeeze to Scale (Object or World) in VR
The following code enables you to use both controllers in VR (tested on Quest) and scale the player's perspective (XRRig) by squeezing the grab triggers and moving the controllers closer (pinch out) or further apart (pinch in). The boolean allowWorldScaling has to be ticked in unity for that to work.
Upon selecting a draggable object (Drag controls script), the player can scale up or down that object, while keeping the finger on the trigger and squeezing both grab buttons and moving the hands closer or apart.
The current script enables you to visually see the scale. Create a world canvas with a text component as a child. Assign the world canvas to scaleTextObject and the text to scaleText. scaleTextObject will then spawn in front of the player and follow the head movement whenever scaling.
At the moment the position of the hands (controllers) is done by finding the avatar's hands. I couldn't make it work otherwise. If you find a better way please share.
import { Behaviour, WebXR,serializeable, WebXREvent,WebXRAvatar,GameObject, AvatarMarker,Text} from "@needle-tools/engine";
import { Object3D, Vector3,Quaternion,PerspectiveCamera} from "three";
export class SqueezeScale extends Behaviour {
private webXR?: WebXR;
private selectedObj: Object3D| null = null;
@serializeable(Object3D)
scaleTextObject: Object3D| null = null;
@serializeable(Text)
scaleText?: Text;
public allowWorldScaling: boolean =false;
private leftSqueeze:boolean=false;
private rightSqueeze:boolean=false;
private bothSqueezeStarted=false;
private rigScaleUpdated=false;
private initialDistance:number=1;
private initialScale:number=1;
private newScale:number | null=null;
private leftHand?:Object3D;
private rightHand?:Object3D;
private head?:Object3D;
start(): void {
let _webxr=GameObject.findObjectOfType(WebXR);
if(_webxr)
{
this.webXR=_webxr;
console.log("webxr found");
}
//Wait for XR Session
WebXR.addEventListener(WebXREvent.XRStarted, () => {
//listen to squeeze events
this.context.xrSession?.addEventListener("squeezestart", (event) => { this.onSqueezeEvent(event,true); });
this.context.xrSession?.addEventListener("squeezeend", (event) => { this.onSqueezeEvent(event,false); });
});
}
onSqueezeEvent(event: XRInputSourceEvent, status:boolean) {
if(event.inputSource.handedness==="right")
{
this.rightSqueeze=status;
}
if(event.inputSource.handedness==="left")
{
this.leftSqueeze=status;
}
}
update(){
if(this.context.isInVR)
{
//cache object selected if any
this.objectGrab();
//if both grips are squeezed
if(this.leftSqueeze && this.rightSqueeze)
{
//if object is selected either in the left or right controller (only one)
if(this.selectedObj!=null)
{
//after initial distance value has been set
if(this.bothSqueezeStarted)
{
//get current distance between controllers
const scaleValue=this.calculateDistance();
//get distance change since beginning of squeeze to get a "pinch in/out" effect
this.newScale=this.initialScale+scaleValue-this.initialDistance;
//avoid 0 and negative scales
if(this.newScale<0.001){this.newScale=0.001;}
// scale object according to new distance since initial distance
this.selectedObj.scale.x=this.newScale;
this.selectedObj.scale.y=this.newScale;
this.selectedObj.scale.z=this.newScale;
this.showVisual(this.newScale,"Object :");
}
else
{
//get initial distance value (only once at a new squeeze both hands event)
this.bothSqueezeStarted=true;
this.initialDistance=this.calculateDistance();
//cache object's initial scale
this.initialScale=this.selectedObj.scale.x;
}
}
else
{
//scale world ?
if(this.webXR?.Rig && this.allowWorldScaling)
{
//after initial distance value has been set
if(this.bothSqueezeStarted)
{
//get current distance between controllers
const scaleValue=this.calculateDistance();
//get distance change since beginning of squeeze to get a "pinch in/out" effect
this.newScale=this.initialScale+scaleValue-this.initialDistance;
//avoid 0 and negative scales
if(this.newScale<0.001){this.newScale=0.001;}
this.showVisual(this.newScale, "World :");
this.rigScaleUpdated=true;
}
else
{
//get initial distance value (only once at a new squeeze both hands event)
this.bothSqueezeStarted=true;
this.initialDistance=this.calculateDistance();
//cache object's initial scale
this.initialScale=this.webXR.Rig.scale.x;
}
}
}
}
else
{
//reset values
this.bothSqueezeStarted=false;
//if world has been scaled, scale rig accordingly at the end of squeezing and once only
if(this.webXR?.Rig && this.rigScaleUpdated && this.newScale)
{
//change rig scale
this.webXR.Rig.scale.set(this.newScale, this.newScale, this.newScale);
this.webXR.Rig.updateMatrixWorld();
const cam = this.context.mainCamera as PerspectiveCamera;
cam.near=this.newScale>2?0.0001:0.2;
cam.updateProjectionMatrix();
//reset
this.rigScaleUpdated=false;
}
if(this.scaleTextObject)
{
this.scaleTextObject.visible=false;
}
this.newScale=null;
}
}
}
private calculateDistance():number
{
let distance=1;
if(this.leftHand && this.rightHand)
{
const left=this.leftHand.position;
const right=this.rightHand.position;
// Calculate the difference between the positions
const dx = left.x - right.x;
const dy = left.y - right.y;
const dz = left.z - right.z;
// Calculate the distance using the Euclidean distance formula
distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
}
else
{
//set positions of controllers from your avatar (only once)
let allAvatars = AvatarMarker.instances;
if(allAvatars.length>0)
{
for (let i=0;i<allAvatars.length;i++)
{
if(allAvatars[i].isLocalAvatar())
{
const av=allAvatars[i].avatar as WebXRAvatar;
if(av!=null)
{
this.leftHand=av.handLeft as Object3D;
this.rightHand=av.handRight as Object3D;
this.head = av.head as Object3D;
}
}
}
}
}
return distance;
}
showVisual(scale:number, mesg:string):void
{
if(this.scaleTextObject && this.head && this.scaleText)
{
this.scaleTextObject.visible=true;
const offset = new Vector3(0,0,7);
offset.applyQuaternion(this.head.quaternion);
this.scaleTextObject.position.copy(this.head.position.add(offset));
const roundedNum= +scale.toFixed(3);
this.scaleText.text=mesg+" "+roundedNum;
}
}
objectGrab():void
{
if(this.webXR?.RightController?.grabbed?.selected)
{
this.selectedObj = this.webXR.RightController.grabbed.selected;
}
else if(this.webXR?.LeftController?.grabbed?.selected)
{
this.selectedObj = this.webXR.LeftController.grabbed.selected;
}
else
{
this.selectedObj=null;
}
}
}