UNPKG

playcanvas

Version:

Open-source WebGL/WebGPU 3D engine for the web

441 lines (440 loc) 13.7 kB
import { EventHandler } from "../../core/event-handler.js"; import { platform } from "../../core/platform.js"; import { warnInsecureContext } from "../../core/secure-context-warning.js"; import { Mat4 } from "../../core/math/mat4.js"; import { Quat } from "../../core/math/quat.js"; import { Vec2 } from "../../core/math/vec2.js"; import { Vec3 } from "../../core/math/vec3.js"; import { XRTYPE_INLINE, XRTYPE_VR, XRTYPE_AR, XRDEPTHSENSINGUSAGE_CPU, XRDEPTHSENSINGUSAGE_GPU, XRDEPTHSENSINGFORMAT_L8A8, XRDEPTHSENSINGFORMAT_R16U, XRDEPTHSENSINGFORMAT_F32 } 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 { XrBridge } from "../../platform/graphics/xr-bridge.js"; class XrManager extends EventHandler { static EVENT_AVAILABLE = "available"; static EVENT_START = "start"; static EVENT_END = "end"; static EVENT_UPDATE = "update"; static EVENT_ERROR = "error"; app; _supported = platform.browser && !!navigator.xr; _available = {}; _type = null; _spaceType = null; _session = null; xrBridge = null; get graphicsBinding() { return this.xrBridge?.graphicsBinding ?? null; } _referenceSpace = null; domOverlay; hitTest; imageTracking; planeDetection; meshDetection; input; lightEstimation; views; anchors; _camera = null; _localPosition = new Vec3(); _localRotation = new Quat(); _depthNear = 0.1; _depthFar = 1e3; _supportedFrameRates = null; _width = 0; _height = 0; _framebufferSize = new Vec2(); _framebufferScaleFactor = 1; constructor(app) { super(); this.app = app; this._available[XRTYPE_INLINE] = false; this._available[XRTYPE_VR] = false; this._available[XRTYPE_AR] = false; 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); if (this._supported) { navigator.xr.addEventListener("devicechange", () => { this._deviceAvailabilityCheck(); }); this._deviceAvailabilityCheck(); } } destroy() { if (this.xrBridge) { this.xrBridge.destroy(); this.xrBridge = null; } } start(camera, type, spaceType, options) { let callback = options; if (typeof options === "object") { callback = options.callback; } if (!this._available[type]) { warnInsecureContext("WebXR"); 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; this._setClipPlanes(camera.nearClip, camera.farClip); const opts = { requiredFeatures: [spaceType], optionalFeatures: [] }; const device = this.app.graphicsDevice; if (device?.isWebGPU) { opts.requiredFeatures.push("webgpu"); } 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, dataFormatPreference }; } if (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); } } _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); }); } end(callback) { if (!this._session) { if (callback) callback(new Error("XR Session is not initialized")); return; } this.xrBridge?.releasePresentation(); if (callback) this.once("end", callback); this._session.end(); } isAvailable(type) { return this._available[type]; } _deviceAvailabilityCheck() { for (const key in this._available) { this._sessionSupportCheck(key); } } 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); }); } 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); }); } _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); }); } _onSessionStart(session, spaceType, callback) { let failed = false; this._session = session; this.xrBridge = new XrBridge(this.app.graphicsDevice, this); const onVisibilityChange = () => { this.fire("visibility:change", session.visibilityState); }; const onClipPlanesChange = () => { this._setClipPlanes(this._camera.nearClip, this._camera.farClip); }; const onFrameRateChange = () => { this.fire("frameratechange", this._session?.frameRate); }; 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); session.removeEventListener("frameratechange", onFrameRateChange); if (!failed) this.fire("end"); if (this.xrBridge) { this.xrBridge.destroy(); this.xrBridge = null; } this._session = null; this._referenceSpace = null; this._width = 0; this._height = 0; this._type = null; this._spaceType = null; 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); const gd = this.app.graphicsDevice; const framebufferScaleFactor = gd.maxPixelRatio / window.devicePixelRatio * this._framebufferScaleFactor; this.xrBridge.attachPresentation(this._session, { framebufferScaleFactor, depthNear: this._depthNear, depthFar: this._depthFar, onBindingError: (ex) => { this.fire("error", ex); } }); if (this.session.supportedFrameRates) { this._supportedFrameRates = Array.from(this.session.supportedFrameRates); } else { this._supportedFrameRates = null; } this._session.addEventListener("frameratechange", onFrameRateChange); session.requestReferenceSpace(spaceType).then((referenceSpace) => { this._referenceSpace = referenceSpace; this.app.requestAnimationFrame(); if (callback) callback(null); this.fire("start"); }).catch((ex) => { failed = true; session.end(); if (callback) callback(ex); this.fire("error", ex); }); } _setClipPlanes(near, far) { if (this._depthNear === near && this._depthFar === far) { return; } this._depthNear = near; this._depthFar = far; if (!this._session) { return; } this._session.updateRenderState({ depthNear: this._depthNear, depthFar: this._depthFar }); } update(frame) { if (!this._session) return false; this.xrBridge.getFramebufferSize(frame, this._framebufferSize); const width = this._framebufferSize.x; const height = this._framebufferSize.y; 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; this.views.update(frame, pose.views); 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); 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 * Math.atan(1 / data[5]) * 180 / 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 }); } 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); this.xrBridge.beginFrame(frame, this._referenceSpace); return true; } get supported() { return this._supported; } get active() { return !!this._session; } get type() { return this._type; } get spaceType() { return this._spaceType; } get session() { return this._session; } get frameRate() { return this._session?.frameRate ?? null; } get supportedFrameRates() { return this._supportedFrameRates; } get framebufferScaleFactor() { return this._framebufferScaleFactor; } set fixedFoveation(value) { const layer = this.xrBridge?.presentationLayer; if ((layer?.fixedFoveation ?? null) !== null) { if (this.app.graphicsDevice.samples > 1) { } layer.fixedFoveation = value; } } get fixedFoveation() { const layer = this.xrBridge?.presentationLayer; return layer?.fixedFoveation ?? null; } get camera() { return this._camera ? this._camera.entity : null; } get visibilityState() { if (!this._session) { return null; } return this._session.visibilityState; } } export { XrManager };