playcanvas
Version:
PlayCanvas WebGL game engine
167 lines (130 loc) • 5.04 kB
JavaScript
import { Color, Quat, Script, Vec3, LAYERID_SKYBOX } from 'playcanvas';
export class XrSession extends Script {
static scriptName = 'xrSession';
/**
* Event name to start the WebXR AR session.
* @type {string}
* @attribute
*/
startArEvent = 'ar:start';
/**
* Event name to start the WebXR VR session.
* @type {string}
* @attribute
*/
startVrEvent = 'vr:start';
/**
* Event name to end the WebXR VR session.
* @type {string}
* @attribute
*/
endEvent = 'xr:end';
cameraEntity = null;
cameraRootEntity = null;
clearColor = new Color();
positionRoot = new Vec3();
rotationRoot = new Quat();
positionCamera = new Vec3();
rotationCamera = new Quat();
onKeyDownHandler = null;
initialize() {
this.cameraEntity = this.entity.findComponent('camera')?.entity || null;
this.cameraRootEntity = this.entity || null;
// Listen to global XR lifecycle to mirror example.mjs behavior
this.app.xr?.on('start', this.onXrStart, this);
this.app.xr?.on('end', this.onXrEnd, this);
// Listen for external events to control session
this.app.on(this.startArEvent, this.onStartArEvent, this);
this.app.on(this.startVrEvent, this.onStartVrEvent, this);
this.app.on(this.endEvent, this.onEndEvent, this);
// ESC to exit
this.onKeyDownHandler = (event) => {
if (event.key === 'Escape' && this.app.xr?.active) {
this.endSession();
}
};
window.addEventListener('keydown', this.onKeyDownHandler);
this.on('destroy', () => {
this.onDestroy();
});
}
onDestroy() {
this.app.xr?.off('start', this.onXrStart, this);
this.app.xr?.off('end', this.onXrEnd, this);
this.app.off(this.startVrEvent, this.onStartVrEvent, this);
this.app.off(this.startArEvent, this.onStartArEvent, this);
this.app.off(this.endEvent, this.onEndEvent, this);
if (this.onKeyDownHandler) {
window.removeEventListener('keydown', this.onKeyDownHandler);
this.onKeyDownHandler = null;
}
}
onStartArEvent(space = 'local-floor') {
this.startSession('immersive-ar', space);
}
onStartVrEvent(space = 'local-floor') {
this.startSession('immersive-vr', space);
}
onEndEvent() {
this.endSession();
}
startSession(type = 'immersive-vr', space = 'local-floor') {
if (!this.cameraEntity.camera) {
console.error('XrSession: No cameraEntity.camera found on the entity.');
return;
}
// Start XR on the camera component
this.cameraEntity.camera.startXr(type, space, {
callback: (err) => {
if (err) console.error(`WebXR ${type} failed to start: ${err.message}`);
}
});
}
endSession() {
if (!this.cameraEntity.camera) return;
this.cameraEntity.camera.endXr();
}
onXrStart() {
if (!this.cameraEntity || !this.cameraRootEntity) return;
// Cache original camera rig transforms
this.positionRoot.copy(this.cameraRootEntity.getPosition());
this.rotationRoot.copy(this.cameraRootEntity.getRotation());
this.positionCamera.copy(this.cameraEntity.getPosition());
this.rotationCamera.copy(this.cameraEntity.getRotation());
// Place root at camera position, but reset orientation to horizontal
this.cameraRootEntity.setPosition(this.positionCamera.x, 0, this.positionCamera.z);
// Only preserve Y-axis rotation (yaw), reset pitch and roll for VR
const eulerAngles = this.rotationCamera.getEulerAngles();
this.cameraRootEntity.setEulerAngles(0, eulerAngles.y, 0);
if (this.app.xr.type === 'immersive-ar') {
// Make camera background transparent and hide the sky
this.clearColor.copy(this.cameraEntity.camera.clearColor);
this.cameraEntity.camera.clearColor = new Color(0, 0, 0, 0);
this.disableSky();
}
}
onXrEnd() {
if (!this.cameraEntity || !this.cameraRootEntity) return;
// Restore original transforms
this.cameraRootEntity.setPosition(this.positionRoot);
this.cameraRootEntity.setRotation(this.rotationRoot);
this.cameraEntity.setPosition(this.positionCamera);
this.cameraEntity.setRotation(this.rotationCamera);
if (this.app.xr.type === 'immersive-ar') {
this.cameraEntity.camera.clearColor = this.clearColor;
this.restoreSky();
}
}
disableSky() {
const layer = this.app.scene.layers.getLayerById(LAYERID_SKYBOX);
if (layer) {
layer.enabled = false;
}
}
restoreSky() {
const layer = this.app.scene.layers.getLayerById(LAYERID_SKYBOX);
if (layer) {
layer.enabled = true;
}
}
}