Skip to content

Commit 38f9ab7

Browse files
mvaligurskyMartin Valigursky
andauthored
feat(xr): add smooth turn mode to XrNavigation (#8771)
Adds a continuous-turn option to XrNavigation alongside the existing snap turn, and updates the vr-lod example to use it. - New turnMode attribute on XrNavigation: 'snap' (default — existing behaviour), 'smooth' (continuous angular rate), or 'none'. - New smoothTurnSpeed (default 90 deg/s) and smoothTurnThreshold (default 0.15 deadzone) attributes drive the smooth path. - handleSmoothTurning rotates the rig around the camera's local position so the view pivots in place (same math as handleSnapTurning, just continuous). - Backward compatible: default turnMode 'snap' keeps every existing caller on the current path. rotateSpeed / rotateThreshold / rotateResetThreshold are unchanged and remain snap-specific. vr-lod example: - Uses XrNavigation with turnMode: 'smooth'. - Initial splat budget dropped from 1.5M to 1M. - Switched the VR reference space from XRSPACE_LOCAL to XRSPACE_LOCALFLOOR so the viewer starts at proper standing height instead of with the head at floor level. Co-authored-by: Martin Valigursky <mvaligursky@snapchat.com>
1 parent 83a5840 commit 38f9ab7

2 files changed

Lines changed: 72 additions & 11 deletions

File tree

examples/src/examples/gaussian-splatting-xr/vr-lod.example.mjs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ assetListLoader.load(() => {
171171
});
172172

173173
data.set('renderer', pc.GSPLAT_RENDERER_AUTO);
174-
data.set('splatBudget', 1.5);
174+
data.set('splatBudget', 1);
175175
data.set('data.stats.gsplats', '—');
176176
data.set('data.stats.resolution', '—');
177177

@@ -217,7 +217,9 @@ assetListLoader.load(() => {
217217
properties: {
218218
enableTeleport: false,
219219
enableSnapVertical: false,
220-
movementThreshold: 0
220+
movementThreshold: 0,
221+
turnMode: 'smooth',
222+
smoothTurnSpeed: 90
221223
}
222224
});
223225

@@ -306,7 +308,10 @@ assetListLoader.load(() => {
306308
return;
307309
}
308310
if (app.xr.isAvailable(pc.XRTYPE_VR)) {
309-
app.fire('vr:start', pc.XRSPACE_LOCAL);
311+
// local-floor: WebXR puts the local-space origin at the floor below the viewer at
312+
// session start, so the camera (child of the rig) ends up at rig + ~1.6 m on Y.
313+
// `local` would put the head at rig.y, sinking the viewpoint into the scene floor.
314+
app.fire('vr:start', pc.XRSPACE_LOCALFLOOR);
310315
} else {
311316
setMessage('Immersive VR is not available');
312317
}
@@ -335,7 +340,7 @@ assetListLoader.load(() => {
335340
app.xr.on('start', () => {
336341
setCameraControlsForXr();
337342
updateEnterVrButton();
338-
setMessage('VR active — left thumbstick: move, right: snap turn; tap to exit');
343+
setMessage('VR active — left thumbstick: move, right: turn; tap to exit');
339344
});
340345
app.xr.on('end', () => {
341346
setMessage('VR ended — click Enter VR to re-enter');

scripts/esm/xr-navigation.mjs

Lines changed: 63 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@ import { Color, Script, Vec2, Vec3 } from 'playcanvas';
33
/** @import { XrInputSource } from 'playcanvas' */
44

55
/**
6-
* Handles VR navigation with support for teleportation, smooth locomotion, and snap vertical movement.
7-
* All methods can be enabled simultaneously, allowing users to choose their preferred
8-
* navigation method on the fly.
6+
* Handles VR navigation with support for teleportation, smooth locomotion, snap or smooth
7+
* turning, and snap vertical movement. All methods can be enabled simultaneously, allowing
8+
* users to choose their preferred navigation method on the fly.
99
*
1010
* Teleportation: Point and teleport using trigger/pinch gestures
1111
* Smooth Locomotion: Use left thumbstick for XZ movement
12-
* Snap Turn: Use right thumbstick X-axis for snap turning
12+
* Turning: Right thumbstick X-axis — snap turn (default) or continuous smooth turn, selected
13+
* via {@link XrNavigation#turnMode}
1314
* Snap Vertical: Use right thumbstick Y-axis to snap up/down (right grip for larger jumps)
1415
*
1516
* This script should be attached to a parent entity of the camera entity used for the XR
@@ -40,7 +41,19 @@ class XrNavigation extends Script {
4041
movementSpeed = 1.5;
4142

4243
/**
43-
* Angle in degrees for each snap turn.
44+
* Selects the right-thumbstick turn behaviour. One of:
45+
* - `'snap'`: discrete rotation of {@link XrNavigation#rotateSpeed} degrees per gesture
46+
* (default; existing behaviour).
47+
* - `'smooth'`: continuous rotation at {@link XrNavigation#smoothTurnSpeed} degrees/second
48+
* while the thumbstick is past {@link XrNavigation#smoothTurnThreshold}.
49+
* - `'none'`: thumbstick X is ignored.
50+
* @attribute
51+
* @enabledif {enableMove}
52+
*/
53+
turnMode = 'snap';
54+
55+
/**
56+
* Angle in degrees for each snap turn. Used when {@link XrNavigation#turnMode} is `'snap'`.
4457
* @attribute
4558
* @range [15, 180]
4659
* @enabledif {enableMove}
@@ -74,6 +87,24 @@ class XrNavigation extends Script {
7487
*/
7588
rotateResetThreshold = 0.25;
7689

90+
/**
91+
* Rotation speed in degrees per second when {@link XrNavigation#turnMode} is `'smooth'`.
92+
* @attribute
93+
* @range [30, 360]
94+
* @enabledif {enableMove}
95+
*/
96+
smoothTurnSpeed = 90;
97+
98+
/**
99+
* Deadzone for the right-thumbstick X-axis when {@link XrNavigation#turnMode} is `'smooth'`.
100+
* Below this magnitude the stick is treated as centred.
101+
* @attribute
102+
* @range [0, 0.5]
103+
* @precision 0.01
104+
* @enabledif {enableMove}
105+
*/
106+
smoothTurnThreshold = 0.15;
107+
77108
/**
78109
* Maximum distance for teleportation in meters.
79110
* @attribute
@@ -366,8 +397,13 @@ class XrNavigation extends Script {
366397
// Apply movement to camera parent (this entity)
367398
this.entity.translate(this.tmpVec2A.x, 0, this.tmpVec2A.y);
368399
}
369-
} else if (inputSource.handedness === 'right') { // Right controller - snap turning
370-
this.handleSnapTurning(inputSource);
400+
} else if (inputSource.handedness === 'right') { // Right controller - turning
401+
if (this.turnMode === 'smooth') {
402+
this.handleSmoothTurning(inputSource, dt);
403+
} else if (this.turnMode === 'snap') {
404+
this.handleSnapTurning(inputSource);
405+
}
406+
// 'none' → thumbstick X is ignored
371407
}
372408
}
373409
}
@@ -397,6 +433,26 @@ class XrNavigation extends Script {
397433
}
398434
}
399435

436+
/**
437+
* Continuous turn at {@link XrNavigation#smoothTurnSpeed} degrees per second while the
438+
* right thumbstick X-axis is held past {@link XrNavigation#smoothTurnThreshold}. Rotates
439+
* around the camera's local position so the view pivots in place rather than orbiting
440+
* the rig origin.
441+
*
442+
* @param {XrInputSource} inputSource - The right-hand input source.
443+
* @param {number} dt - Frame delta time in seconds.
444+
*/
445+
handleSmoothTurning(inputSource, dt) {
446+
const turn = -inputSource.gamepad.axes[2];
447+
if (Math.abs(turn) <= this.smoothTurnThreshold) return;
448+
if (!this.cameraEntity) return;
449+
450+
this.tmpVec3A.copy(this.cameraEntity.getLocalPosition());
451+
this.entity.translateLocal(this.tmpVec3A);
452+
this.entity.rotateLocal(0, turn * this.smoothTurnSpeed * dt, 0);
453+
this.entity.translateLocal(this.tmpVec3A.mulScalar(-1));
454+
}
455+
400456
/**
401457
* Handles snap vertical movement using right thumbstick Y.
402458
* Uses hysteresis to prevent multiple snaps from a single gesture.

0 commit comments

Comments
 (0)