playcanvas
Version:
PlayCanvas WebGL game engine
827 lines (824 loc) • 32.9 kB
JavaScript
import { Debug } from '../../core/debug.js';
import { EventHandler } from '../../core/event-handler.js';
import { platform } from '../../core/platform.js';
import { Mat4 } from '../../core/math/mat4.js';
import { Quat } from '../../core/math/quat.js';
import { Vec3 } from '../../core/math/vec3.js';
import { XRTYPE_INLINE, XRTYPE_VR, XRTYPE_AR, XRDEPTHSENSINGUSAGE_GPU, XRDEPTHSENSINGUSAGE_CPU, XRDEPTHSENSINGFORMAT_F32, XRDEPTHSENSINGFORMAT_L8A8, XRDEPTHSENSINGFORMAT_R16U } from './constants.js';
import { XrDomOverlay } from './xr-dom-overlay.js';
import { XrHitTest } from './xr-hit-test.js';
import { XrImageTracking } from './xr-image-tracking.js';
import { XrInput } from './xr-input.js';
import { XrLightEstimation } from './xr-light-estimation.js';
import { XrPlaneDetection } from './xr-plane-detection.js';
import { XrAnchors } from './xr-anchors.js';
import { XrMeshDetection } from './xr-mesh-detection.js';
import { XrViews } from './xr-views.js';
/**
* @import { AppBase } from '../app-base.js'
* @import { CameraComponent } from '../components/camera/component.js'
* @import { Entity } from '../entity.js'
*/ /**
* @callback XrErrorCallback
* Callback used by {@link XrManager#start} and {@link XrManager#end}.
* @param {Error|null} err - The Error object or null if operation was successful.
* @returns {void}
*/ /**
* @callback XrRoomCaptureCallback
* Callback used by {@link XrManager#initiateRoomCapture}.
* @param {Error|null} err - The Error object or null if manual room capture was successful.
* @returns {void}
*/ /**
* XrManager provides a comprehensive interface for WebXR integration in PlayCanvas applications.
* It manages the full lifecycle of XR sessions (VR/AR), handles device capabilities, and provides
* access to various XR features through specialized subsystems.
*
* In order for XR to be available, ensure that your application is served over HTTPS or localhost.
*
* The {@link AppBase} class automatically creates an instance of this class and makes it available
* as {@link AppBase#xr}.
*
* @category XR
*/ class XrManager extends EventHandler {
static{
/**
* Fired when availability of the XR type is changed. This event is available in two
* forms. They are as follows:
*
* 1. `available` - Fired when availability of any XR type is changed. The handler is passed
* the session type that has changed availability and a boolean representing the availability.
* 2. `available:[type]` - Fired when availability of specific XR type is changed. The handler
* is passed a boolean representing the availability.
*
* @event
* @example
* app.xr.on('available', (type, available) => {
* console.log(`XR type ${type} is now ${available ? 'available' : 'unavailable'}`);
* });
* @example
* app.xr.on(`available:${pc.XRTYPE_VR}`, (available) => {
* console.log(`XR type VR is now ${available ? 'available' : 'unavailable'}`);
* });
*/ this.EVENT_AVAILABLE = 'available';
}
static{
/**
* Fired when XR session is started.
*
* @event
* @example
* app.xr.on('start', () => {
* // XR session has started
* });
*/ this.EVENT_START = 'start';
}
static{
/**
* Fired when XR session is ended.
*
* @event
* @example
* app.xr.on('end', () => {
* // XR session has ended
* });
*/ this.EVENT_END = 'end';
}
static{
/**
* Fired when XR session is updated, providing relevant XRFrame object. The handler is passed
* [XRFrame](https://developer.mozilla.org/en-US/docs/Web/API/XRFrame) object that can be used
* for interfacing directly with WebXR APIs.
*
* @event
* @example
* app.xr.on('update', (frame) => {
* console.log('XR frame updated');
* });
*/ this.EVENT_UPDATE = 'update';
}
static{
/**
* Fired when XR session is failed to start or failed to check for session type support. The handler
* is passed the [Error](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error)
* object related to failure of session start or check of session type support.
*
* @event
* @example
* app.xr.on('error', (error) => {
* console.error(error.message);
* });
*/ this.EVENT_ERROR = 'error';
}
/**
* Create a new XrManager instance.
*
* @param {AppBase} app - The main application.
* @ignore
*/ constructor(app){
super(), /**
* @type {boolean}
* @private
*/ this._supported = platform.browser && !!navigator.xr, /**
* @type {Object<string, boolean>}
* @private
*/ this._available = {}, /**
* @type {string|null}
* @private
*/ this._type = null, /**
* @type {string|null}
* @private
*/ this._spaceType = null, /**
* @type {XRSession|null}
* @private
*/ this._session = null, /**
* @type {XRWebGLLayer|null}
* @private
*/ this._baseLayer = null, /**
* @type {XRWebGLBinding|null}
* @ignore
*/ this.webglBinding = null, /**
* @type {XRReferenceSpace|null}
* @ignore
*/ this._referenceSpace = null, /**
* @type {CameraComponent|null}
* @private
*/ this._camera = null, /**
* @type {Vec3}
* @private
*/ this._localPosition = new Vec3(), /**
* @type {Quat}
* @private
*/ this._localRotation = new Quat(), /**
* @type {number}
* @private
*/ this._depthNear = 0.1, /**
* @type {number}
* @private
*/ this._depthFar = 1000, /**
* @type {number[]|null}
* @private
*/ this._supportedFrameRates = null, /**
* @type {number}
* @private
*/ this._width = 0, /**
* @type {number}
* @private
*/ this._height = 0, /**
* @type {number}
* @private
*/ this._framebufferScaleFactor = 1.0;
this.app = app;
// Add all the supported session types
this._available[XRTYPE_INLINE] = false;
this._available[XRTYPE_VR] = false;
this._available[XRTYPE_AR] = false;
this.views = new XrViews(this);
this.domOverlay = new XrDomOverlay(this);
this.hitTest = new XrHitTest(this);
this.imageTracking = new XrImageTracking(this);
this.planeDetection = new XrPlaneDetection(this);
this.meshDetection = new XrMeshDetection(this);
this.input = new XrInput(this);
this.lightEstimation = new XrLightEstimation(this);
this.anchors = new XrAnchors(this);
this.views = new XrViews(this);
// TODO
// 1. HMD class with its params
// 2. Space class
// 3. Controllers class
if (this._supported) {
navigator.xr.addEventListener('devicechange', ()=>{
this._deviceAvailabilityCheck();
});
this._deviceAvailabilityCheck();
this.app.graphicsDevice.on('devicelost', this._onDeviceLost, this);
this.app.graphicsDevice.on('devicerestored', this._onDeviceRestored, this);
}
}
/**
* Destroys the XrManager instance.
*
* @ignore
*/ destroy() {}
/**
* Attempts to start XR session for provided {@link CameraComponent} and optionally fires
* callback when session is created or failed to create. Integrated XR APIs need to be enabled
* by providing relevant options.
*
* Note that the start method needs to be called in response to user action, such as a button
* click. It will not work if called in response to a timer or other event.
*
* @param {CameraComponent} camera - It will be used to render XR session and manipulated based
* on pose tracking.
* @param {string} type - Session type. Can be one of the following:
*
* - {@link XRTYPE_INLINE}: Inline - always available type of session. It has limited features
* availability and is rendered into HTML element.
* - {@link XRTYPE_VR}: Immersive VR - session that provides exclusive access to VR device with
* best available tracking features.
* - {@link XRTYPE_AR}: Immersive AR - session that provides exclusive access to VR/AR device
* that is intended to be blended with real-world environment.
*
* @param {string} spaceType - Reference space type. Can be one of the following:
*
* - {@link XRSPACE_VIEWER}: Viewer - always supported space with some basic tracking
* capabilities.
* - {@link XRSPACE_LOCAL}: Local - represents a tracking space with a native origin near the
* viewer at the time of creation. It is meant for seated or basic local XR sessions.
* - {@link XRSPACE_LOCALFLOOR}: Local Floor - represents a tracking space with a native origin
* at the floor in a safe position for the user to stand. The y axis equals 0 at floor level.
* Floor level value might be estimated by the underlying platform. It is meant for seated or
* basic local XR sessions.
* - {@link XRSPACE_BOUNDEDFLOOR}: Bounded Floor - represents a tracking space with its native
* origin at the floor, where the user is expected to move within a pre-established boundary.
* - {@link XRSPACE_UNBOUNDED}: Unbounded - represents a tracking space where the user is
* expected to move freely around their environment, potentially long distances from their
* starting point.
*
* @param {object} [options] - Object with additional options for XR session initialization.
* @param {number} [options.framebufferScaleFactor] - Framebuffer scale factor should
* be higher than 0.0, by default 1.0 (no scaling). A value of 0.5 will reduce the resolution
* of an XR session in half, and a value of 2.0 will double the resolution.
* @param {string[]} [options.optionalFeatures] - Optional features for XRSession start. It is
* used for getting access to additional WebXR spec extensions.
* @param {boolean} [options.anchors] - Set to true to attempt to enable
* {@link XrAnchors}.
* @param {boolean} [options.imageTracking] - Set to true to attempt to enable
* {@link XrImageTracking}.
* @param {boolean} [options.planeDetection] - Set to true to attempt to enable
* {@link XrPlaneDetection}.
* @param {boolean} [options.meshDetection] - Set to true to attempt to enable
* {@link XrMeshDetection}.
* @param {XrErrorCallback} [options.callback] - Optional callback function called once session
* is started. The callback has one argument Error - it is null if successfully started XR
* session.
* @param {object} [options.depthSensing] - Optional object with parameters to attempt to enable
* depth sensing.
* @param {string} [options.depthSensing.usagePreference] - Optional usage preference for depth
* sensing, can be 'cpu-optimized' or 'gpu-optimized' (XRDEPTHSENSINGUSAGE_*), defaults to
* 'cpu-optimized'. Most preferred and supported will be chosen by the underlying depth sensing
* system.
* @param {string} [options.depthSensing.dataFormatPreference] - Optional data format
* preference for depth sensing, can be 'luminance-alpha' or 'float32'
* (XRDEPTHSENSINGFORMAT_*), defaults to 'luminance-alpha'. Most preferred and supported will
* be chosen by the underlying depth sensing system.
* @example
* button.on('click', () => {
* app.xr.start(camera, pc.XRTYPE_VR, pc.XRSPACE_LOCALFLOOR);
* });
* @example
* button.on('click', () => {
* app.xr.start(camera, pc.XRTYPE_AR, pc.XRSPACE_LOCALFLOOR, {
* anchors: true,
* imageTracking: true,
* depthSensing: { }
* });
* });
*/ start(camera, type, spaceType, options) {
let callback = options;
if (typeof options === 'object') {
callback = options.callback;
}
if (!this._available[type]) {
if (callback) callback(new Error('XR is not available'));
return;
}
if (this._session) {
if (callback) callback(new Error('XR session is already started'));
return;
}
this._camera = camera;
this._camera.camera.xr = this;
this._type = type;
this._spaceType = spaceType;
this._framebufferScaleFactor = options?.framebufferScaleFactor ?? 1.0;
this._setClipPlanes(camera.nearClip, camera.farClip);
// TODO
// makeXRCompatible
// scenario to test:
// 1. app is running on integrated GPU
// 2. XR device is connected, to another GPU
// 3. probably immersive-vr will fail to be created
// 4. call makeXRCompatible, very likely will lead to context loss
const opts = {
requiredFeatures: [
spaceType
],
optionalFeatures: []
};
const device = this.app.graphicsDevice;
if (device?.isWebGPU) {
opts.requiredFeatures.push('webgpu');
}
const webgl = device?.isWebGL2;
if (type === XRTYPE_AR) {
opts.optionalFeatures.push('light-estimation');
opts.optionalFeatures.push('hit-test');
if (options) {
if (options.imageTracking && this.imageTracking.supported) {
opts.optionalFeatures.push('image-tracking');
}
if (options.planeDetection) {
opts.optionalFeatures.push('plane-detection');
}
if (options.meshDetection) {
opts.optionalFeatures.push('mesh-detection');
}
}
if (this.domOverlay.supported && this.domOverlay.root) {
opts.optionalFeatures.push('dom-overlay');
opts.domOverlay = {
root: this.domOverlay.root
};
}
if (options && options.anchors && this.anchors.supported) {
opts.optionalFeatures.push('anchors');
}
if (options && options.depthSensing && this.views.supportedDepth) {
opts.optionalFeatures.push('depth-sensing');
const usagePreference = [];
const dataFormatPreference = [];
usagePreference.push(XRDEPTHSENSINGUSAGE_GPU, XRDEPTHSENSINGUSAGE_CPU);
dataFormatPreference.push(XRDEPTHSENSINGFORMAT_F32, XRDEPTHSENSINGFORMAT_L8A8, XRDEPTHSENSINGFORMAT_R16U);
if (options.depthSensing.usagePreference) {
const ind = usagePreference.indexOf(options.depthSensing.usagePreference);
if (ind !== -1) usagePreference.splice(ind, 1);
usagePreference.unshift(options.depthSensing.usagePreference);
}
if (options.depthSensing.dataFormatPreference) {
const ind = dataFormatPreference.indexOf(options.depthSensing.dataFormatPreference);
if (ind !== -1) dataFormatPreference.splice(ind, 1);
dataFormatPreference.unshift(options.depthSensing.dataFormatPreference);
}
opts.depthSensing = {
usagePreference: usagePreference,
dataFormatPreference: dataFormatPreference
};
}
if (webgl && options && options.cameraColor && this.views.supportedColor) {
opts.optionalFeatures.push('camera-access');
}
}
opts.optionalFeatures.push('hand-tracking');
if (options && options.optionalFeatures) {
opts.optionalFeatures = opts.optionalFeatures.concat(options.optionalFeatures);
}
if (this.imageTracking.supported && this.imageTracking.images.length) {
this.imageTracking.prepareImages((err, trackedImages)=>{
if (err) {
if (callback) callback(err);
this.fire('error', err);
return;
}
if (trackedImages !== null) {
opts.trackedImages = trackedImages;
}
this._onStartOptionsReady(type, spaceType, opts, callback);
});
} else {
this._onStartOptionsReady(type, spaceType, opts, callback);
}
}
/**
* @param {string} type - Session type.
* @param {string} spaceType - Reference space type.
* @param {*} options - Session options.
* @param {XrErrorCallback} callback - Error callback.
* @private
*/ _onStartOptionsReady(type, spaceType, options, callback) {
navigator.xr.requestSession(type, options).then((session)=>{
this._onSessionStart(session, spaceType, callback);
}).catch((ex)=>{
this._camera.camera.xr = null;
this._camera = null;
this._type = null;
this._spaceType = null;
if (callback) callback(ex);
this.fire('error', ex);
});
}
/**
* Attempts to end XR session and optionally fires callback when session is ended or failed to
* end.
*
* @param {XrErrorCallback} [callback] - Optional callback function called once session is
* started. The callback has one argument Error - it is null if successfully started XR
* session.
* @example
* app.keyboard.on('keydown', (evt) => {
* if (evt.key === pc.KEY_ESCAPE && app.xr.active) {
* app.xr.end();
* }
* });
*/ end(callback) {
if (!this._session) {
if (callback) callback(new Error('XR Session is not initialized'));
return;
}
this.webglBinding = null;
if (callback) this.once('end', callback);
this._session.end();
}
/**
* Check if the specified type of session is available.
*
* @param {string} type - Session type. Can be one of the following:
*
* - {@link XRTYPE_INLINE}: Inline - always available type of session. It has limited features
* availability and is rendered into HTML element.
* - {@link XRTYPE_VR}: Immersive VR - session that provides exclusive access to VR device with
* best available tracking features.
* - {@link XRTYPE_AR}: Immersive AR - session that provides exclusive access to VR/AR device
* that is intended to be blended with real-world environment.
*
* @example
* if (app.xr.isAvailable(pc.XRTYPE_VR)) {
* // VR is available
* }
* @returns {boolean} True if the specified session type is available.
*/ isAvailable(type) {
return this._available[type];
}
/** @private */ _deviceAvailabilityCheck() {
for(const key in this._available){
this._sessionSupportCheck(key);
}
}
/**
* Initiate manual room capture. If the underlying XR system supports manual capture of the
* room, it will start the capturing process, which can affect plane and mesh detection,
* and improve hit-test quality against real-world geometry.
*
* @param {XrRoomCaptureCallback} callback - Callback that will be fired once capture is complete
* or failed.
*
* @example
* this.app.xr.initiateRoomCapture((err) => {
* if (err) {
* // capture failed
* return;
* }
* // capture was successful
* });
*/ initiateRoomCapture(callback) {
if (!this._session) {
callback(new Error('Session is not active'));
return;
}
if (!this._session.initiateRoomCapture) {
callback(new Error('Session does not support manual room capture'));
return;
}
this._session.initiateRoomCapture().then(()=>{
if (callback) callback(null);
}).catch((err)=>{
if (callback) callback(err);
});
}
/**
* Update target frame rate of an XR session to one of supported value provided by
* supportedFrameRates list.
*
* @param {number} frameRate - Target frame rate. It should be any value from the list
* of supportedFrameRates.
* @param {Function} [callback] - Callback that will be called when frameRate has been
* updated or failed to update with error provided.
*/ updateTargetFrameRate(frameRate, callback) {
if (!this._session?.updateTargetFrameRate) {
callback?.(new Error('unable to update frameRate'));
return;
}
this._session.updateTargetFrameRate(frameRate).then(()=>{
callback?.();
}).catch((err)=>{
callback?.(err);
});
}
/**
* @param {string} type - Session type.
* @private
*/ _sessionSupportCheck(type) {
navigator.xr.isSessionSupported(type).then((available)=>{
if (this._available[type] === available) {
return;
}
this._available[type] = available;
this.fire('available', type, available);
this.fire(`available:${type}`, available);
}).catch((ex)=>{
this.fire('error', ex);
});
}
/**
* @param {XRSession} session - XR session.
* @param {string} spaceType - Space type to request for the session.
* @param {Function} callback - Callback to call when session is started.
* @private
*/ _onSessionStart(session, spaceType, callback) {
let failed = false;
this._session = session;
const onVisibilityChange = ()=>{
this.fire('visibility:change', session.visibilityState);
};
const onClipPlanesChange = ()=>{
this._setClipPlanes(this._camera.nearClip, this._camera.farClip);
};
// clean up once session is ended
const onEnd = ()=>{
if (this._camera) {
this._camera.off('set_nearClip', onClipPlanesChange);
this._camera.off('set_farClip', onClipPlanesChange);
this._camera.camera.xr = null;
this._camera = null;
}
session.removeEventListener('end', onEnd);
session.removeEventListener('visibilitychange', onVisibilityChange);
if (!failed) this.fire('end');
this._session = null;
this._referenceSpace = null;
this._width = 0;
this._height = 0;
this._type = null;
this._spaceType = null;
// old requestAnimationFrame will never be triggered,
// so queue up new tick
if (this.app.systems) {
this.app.requestAnimationFrame();
}
};
session.addEventListener('end', onEnd);
session.addEventListener('visibilitychange', onVisibilityChange);
this._camera.on('set_nearClip', onClipPlanesChange);
this._camera.on('set_farClip', onClipPlanesChange);
// A framebufferScaleFactor scale of 1 is the full resolution of the display
// so we need to calculate this based on devicePixelRatio of the display and what
// we've set this in the graphics device
Debug.assert(window, 'window is needed to scale the XR framebuffer. Are you running XR headless?');
this._createBaseLayer();
if (this.session.supportedFrameRates) {
this._supportedFrameRates = Array.from(this.session.supportedFrameRates);
} else {
this._supportedFrameRates = null;
}
this._session.addEventListener('frameratechange', ()=>{
this.fire('frameratechange', this._session?.frameRate);
});
// request reference space
session.requestReferenceSpace(spaceType).then((referenceSpace)=>{
this._referenceSpace = referenceSpace;
// old requestAnimationFrame will never be triggered,
// so queue up new tick
this.app.requestAnimationFrame();
if (callback) callback(null);
this.fire('start');
}).catch((ex)=>{
failed = true;
session.end();
if (callback) callback(ex);
this.fire('error', ex);
});
}
/**
* @param {number} near - Near plane distance.
* @param {number} far - Far plane distance.
* @private
*/ _setClipPlanes(near, far) {
if (this._depthNear === near && this._depthFar === far) {
return;
}
this._depthNear = near;
this._depthFar = far;
if (!this._session) {
return;
}
// if session is available,
// queue up render state update
this._session.updateRenderState({
depthNear: this._depthNear,
depthFar: this._depthFar
});
}
_createBaseLayer() {
const device = this.app.graphicsDevice;
const framebufferScaleFactor = device.maxPixelRatio / window.devicePixelRatio * this._framebufferScaleFactor;
this._baseLayer = new XRWebGLLayer(this._session, device.gl, {
alpha: true,
depth: true,
stencil: true,
framebufferScaleFactor: framebufferScaleFactor,
antialias: false
});
if (device?.isWebGL2 && window.XRWebGLBinding) {
try {
this.webglBinding = new XRWebGLBinding(this._session, device.gl);
} catch (ex) {
this.fire('error', ex);
}
}
this._session.updateRenderState({
baseLayer: this._baseLayer,
depthNear: this._depthNear,
depthFar: this._depthFar
});
}
/** @private */ _onDeviceLost() {
if (!this._session) {
return;
}
if (this.webglBinding) {
this.webglBinding = null;
}
this._baseLayer = null;
this._session.updateRenderState({
baseLayer: this._baseLayer,
depthNear: this._depthNear,
depthFar: this._depthFar
});
}
/** @private */ _onDeviceRestored() {
if (!this._session) {
return;
}
setTimeout(()=>{
this.app.graphicsDevice.gl.makeXRCompatible().then(()=>{
this._createBaseLayer();
}).catch((ex)=>{
this.fire('error', ex);
});
}, 0);
}
/**
* @param {XRFrame} frame - XRFrame from requestAnimationFrame callback.
* @returns {boolean} True if update was successful, false otherwise.
* @ignore
*/ update(frame) {
if (!this._session) return false;
// canvas resolution should be set on first frame availability or resolution changes
const width = frame.session.renderState.baseLayer.framebufferWidth;
const height = frame.session.renderState.baseLayer.framebufferHeight;
if (this._width !== width || this._height !== height) {
this._width = width;
this._height = height;
this.app.graphicsDevice.setResolution(width, height);
}
const pose = frame.getViewerPose(this._referenceSpace);
if (!pose) return false;
const lengthOld = this.views.list.length;
// add views
this.views.update(frame, pose.views);
// reset position
const posePosition = pose.transform.position;
const poseOrientation = pose.transform.orientation;
this._localPosition.set(posePosition.x, posePosition.y, posePosition.z);
this._localRotation.set(poseOrientation.x, poseOrientation.y, poseOrientation.z, poseOrientation.w);
// update the camera fov properties only when we had 0 views
if (lengthOld === 0 && this.views.list.length > 0) {
const viewProjMat = new Mat4();
const view = this.views.list[0];
viewProjMat.copy(view.projMat);
const data = viewProjMat.data;
const fov = 2.0 * Math.atan(1.0 / data[5]) * 180.0 / Math.PI;
const aspectRatio = data[5] / data[0];
const farClip = data[14] / (data[10] + 1);
const nearClip = data[14] / (data[10] - 1);
const horizontalFov = false;
const camera = this._camera.camera;
camera.setXrProperties({
aspectRatio,
farClip,
fov,
horizontalFov,
nearClip
});
}
// position and rotate camera based on calculated vectors
this._camera.camera._node.setLocalPosition(this._localPosition);
this._camera.camera._node.setLocalRotation(this._localRotation);
this.input.update(frame);
if (this._type === XRTYPE_AR) {
if (this.hitTest.supported) {
this.hitTest.update(frame);
}
if (this.lightEstimation.supported) {
this.lightEstimation.update(frame);
}
if (this.imageTracking.supported) {
this.imageTracking.update(frame);
}
if (this.anchors.supported) {
this.anchors.update(frame);
}
if (this.planeDetection.supported) {
this.planeDetection.update(frame);
}
if (this.meshDetection.supported) {
this.meshDetection.update(frame);
}
}
this.fire('update', frame);
return true;
}
/**
* True if XR is supported.
*
* @type {boolean}
*/ get supported() {
return this._supported;
}
/**
* True if XR session is running.
*
* @type {boolean}
*/ get active() {
return !!this._session;
}
/**
* Returns type of currently running XR session or null if no session is running. Can be any of
* XRTYPE_*.
*
* @type {string|null}
*/ get type() {
return this._type;
}
/**
* Returns reference space type of currently running XR session or null if no session is
* running. Can be any of XRSPACE_*.
*
* @type {string|null}
*/ get spaceType() {
return this._spaceType;
}
/**
* Provides access to XRSession of WebXR.
*
* @type {XRSession|null}
*/ get session() {
return this._session;
}
/**
* XR session frameRate or null if this information is not available. This value can change
* during an active XR session.
*
* @type {number|null}
*/ get frameRate() {
return this._session?.frameRate ?? null;
}
/**
* List of supported frame rates, or null if this data is not available.
*
* @type {number[]|null}
*/ get supportedFrameRates() {
return this._supportedFrameRates;
}
/**
* Framebuffer scale factor. This value is read-only and can only be set when starting a new
* XR session.
*
* @type {number}
*/ get framebufferScaleFactor() {
return this._framebufferScaleFactor;
}
/**
* Set fixed foveation to the value between 0 and 1. Where 0 is no foveation and 1 is highest
* foveation. It only can be set during an active XR session. Fixed foveation will reduce the
* resolution of the back buffer at the edges of the screen, which can improve rendering
* performance.
*
* @type {number}
*/ set fixedFoveation(value) {
if ((this._baseLayer?.fixedFoveation ?? null) !== null) {
if (this.app.graphicsDevice.samples > 1) {
Debug.warn('Fixed Foveation is ignored. Disable anti-aliasing for it to be effective.');
}
this._baseLayer.fixedFoveation = value;
}
}
/**
* Gets the current fixed foveation level, which is between 0 and 1. 0 is no forveation and 1
* is highest foveation. If fixed foveation is not supported, this value returns null.
*
* @type {number|null}
*/ get fixedFoveation() {
return this._baseLayer?.fixedFoveation ?? null;
}
/**
* Active camera for which XR session is running or null.
*
* @type {Entity|null}
*/ get camera() {
return this._camera ? this._camera.entity : null;
}
/**
* Indicates whether WebXR content is currently visible to the user, and if it is, whether it's
* the primary focus. Can be 'hidden', 'visible' or 'visible-blurred'.
*
* @type {"hidden"|"visible"|"visible-blurred"|null}
* @ignore
*/ get visibilityState() {
if (!this._session) {
return null;
}
return this._session.visibilityState;
}
}
export { XrManager };