Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 | 1x 10x 10x 10x 10x 10x 10x 10x 9x 10x 9x 10x 1x 10x 10x 10x 10x 10x 10x 19x 10x 8x 8x 7x 1x 13x 5x 8x 8x 8x 8x 8x 8x 8x 8x 8x | import { Script, XRTYPE_AR, XRTYPE_VR } from 'playcanvas';
import { XrStartBounds, xrStartRigPose, yawFromBasis } from './xr-start-pose';
type XrStartPoseNavigation = { boundsCenter: { x: number; z: number }; boundsRadius: number };
/**
* Starts an immersive session at the scene's camera position rather than at the world origin.
*
* PlayCanvas overwrites the camera entity's local transform with the head pose every frame, so the
* head lands wherever the headset's reference space puts it — near (0, 0) for a rig that has never
* been moved. This script moves the rig on the first frame of a session so the head instead starts
* at `targetX`/`targetZ` facing `focusX`/`focusZ`.
*
* Only XZ is touched: the user stands on the rig floor, which the boundary wall is also built
* against, so lifting the rig onto the splat's ground plane would put them through it.
*
* Attach to the rig entity — the camera's parent, alongside TfXrNavigation, whose bounds clamp
* pulls the start point back inside the scene bounds when the camera position sits outside them.
*/
export class XrStartPose extends Script {
static scriptName = 'xrStartPose';
/** World-space X the head should start at, i.e. the viewer's `cameraPosition`. */
targetX = 0;
/** World-space Z the head should start at. */
targetZ = 0;
/** World-space X the head should face from there, i.e. the viewer's `origin`. */
focusX = 0;
/** World-space Z the head should face from there. */
focusZ = 0;
/** Set when a session starts, cleared once the rig has been placed. */
private _pending = false;
/**
* Whether the engine has written a head pose for this session yet.
*/
private _posed = false;
private _onXrStart = () => {
this._pending = true;
};
/** Fired from XrManager.update only after the head pose has been written to the camera. */
private _onXrUpdate = () => {
this._posed = true;
};
private _onXrEnd = () => {
this._posed = false;
};
initialize() {
// Covers mounting into a session that is already running as well as the usual mount before one.
this._pending = this._isXrActive();
this.app.xr?.on('start', this._onXrStart);
this.app.xr?.on('update', this._onXrUpdate);
this.app.xr?.on('end', this._onXrEnd);
// Script removal fires a 'destroy' event rather than calling a destroy() method, so the
// teardown has to be registered as a listener or these handlers outlive the script.
this.once('destroy', () => this._teardown());
}
private _isXrActive = () =>
this.app.xr?.active === true && (this.app.xr?.type === XRTYPE_VR || this.app.xr?.type === XRTYPE_AR);
/**
* The bounds circle to place the start point in, or undefined when nothing is clamping the head.
* Resolved per call rather than cached: the React wrapper assigns the bounds to the navigation
* script imperatively, so they can land after this script initializes.
*/
private _bounds = (): XrStartBounds | undefined => {
// @ts-expect-error - scripts are added dynamically to the entity
const navigation = this.entity.script?.tfXrNavigation as XrStartPoseNavigation | undefined;
if (!navigation || navigation.boundsRadius <= 0) {
return undefined;
}
return { x: navigation.boundsCenter.x, z: navigation.boundsCenter.z, radius: navigation.boundsRadius };
};
/**
* Runs in update rather than on the 'start' event: the engine writes the head pose during
* xr.update, which precedes the script update in the same tick, so a frame that has posed is the
* first point at which there is a real head to solve the rig pose from.
*/
update() {
if (!this._pending || !this._posed || !this._isXrActive()) {
return;
}
const camera = this.entity.findComponent('camera')?.entity;
Iif (!camera) {
return;
}
const head = camera.getPosition();
const rig = this.entity.getPosition();
const rigY = rig.y;
const pose = xrStartRigPose({
head: { x: head.x, z: head.z },
headYaw: yawFromBasis(camera.forward, camera.right),
rig: { x: rig.x, z: rig.z },
target: { x: this.targetX, z: this.targetZ },
focus: { x: this.focusX, z: this.focusZ },
bounds: this._bounds(),
});
this.entity.rotate(0, pose.yawDelta, 0);
this.entity.setPosition(pose.x, rigY, pose.z);
this._pending = false;
}
private _teardown() {
this.app.xr?.off('start', this._onXrStart);
this.app.xr?.off('update', this._onXrUpdate);
this.app.xr?.off('end', this._onXrEnd);
}
}
|