UNPKG

playcanvas

Version:

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

289 lines (288 loc) 9.07 kB
var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); import { Debug } from "../../../core/debug.js"; import { Vec2 } from "../../../core/math/vec2.js"; class WebgpuXrBridge { /** * @param {XrBridge} xrBridge - The XR bridge. */ constructor(xrBridge) { /** * @type {XRGPUBinding|null} * @private */ __publicField(this, "_binding", null); /** * @type {XRProjectionLayer|null} * @private */ __publicField(this, "_layer", null); /** * Last known immersive color buffer size in pixels (updated in {@link WebgpuXrBridge#beginFrame}). * * @type {Vec2} * @private */ __publicField(this, "_cachedFramebufferSize", new Vec2()); this.xrBridge = xrBridge; } /** * @param {GraphicsDevice} device - The graphics device. */ destroy(device) { this._binding = null; this._layer = null; this._cachedFramebufferSize.set(0, 0); device._clearXrState(); } /** * @param {XRFrame} frame - Current XR frame. * @param {XRReferenceSpace|null} referenceSpace - Active reference space for the XR session. */ beginFrame(frame, referenceSpace) { const device = this.xrBridge.device; device._clearXrState(); if (!this._binding || !this._layer || !referenceSpace) { return; } const pose = frame.getViewerPose(referenceSpace); if (!pose || !pose.views?.length) { return; } const subImages = device.xrSubImages; for (let i = 0; i < pose.views.length; i++) { let sub; try { sub = this._binding.getViewSubImage(this._layer, pose.views[i]); } catch (e) { this.xrBridge._onBindingError?.(e); return; } const colorTexture = sub?.colorTexture; if (!colorTexture) continue; let viewDescriptor = null; if (typeof sub.getViewDescriptor === "function") { try { const desc = sub.getViewDescriptor(); if (desc) { viewDescriptor = { ...desc }; } } catch (e) { } } subImages.push({ colorTexture, viewDescriptor, viewport: sub.viewport, viewFormat: viewDescriptor?.format ?? colorTexture.format }); } const first = subImages[0]; if (first) { device.xrColorTexture = first.colorTexture; device.xrColorTextureViewFormat = first.viewFormat; this._cachedFramebufferSize.set(first.colorTexture.width, first.colorTexture.height); } } endFrame() { const device = this.xrBridge.device; device?._clearXrState(); } /** * @returns {any} // `XRProjectionLayer | null`; using `any` to avoid exporting WebXR GPU types in published typings. */ get presentationLayer() { return this._layer; } /** * @returns {any} // `XRGPUBinding | null`; using `any` to avoid exporting WebXR GPU types in published typings. */ get graphicsBinding() { return this._binding; } /** * @param {XRFrame} frame - Current XR frame. * @param {Vec2} out - Width in {@link Vec2#x}, height in {@link Vec2#y}. */ getFramebufferSize(_frame, out) { const layer = this._layer; if (layer) { const lw = layer.textureWidth ?? layer.width; const lh = layer.textureHeight ?? layer.height; if (lw > 0 && lh > 0) { out.set(lw, lh); return; } } if (this._cachedFramebufferSize.x > 0 && this._cachedFramebufferSize.y > 0) { out.copy(this._cachedFramebufferSize); return; } out.set(0, 0); } /** * @param {XRFrame} frame - Current XR frame. * @param {XRView} xrView - WebXR view. * @returns {XRViewport} Viewport for this view, or zeros if unavailable. */ getViewport(_frame, xrView) { if (this._binding && this._layer) { try { const sub = this._binding.getViewSubImage(this._layer, xrView); if (sub?.viewport) { return sub.viewport; } } catch { } } return ( /** @type {XRViewport} */ { x: 0, y: 0, width: 0, height: 0 } ); } /** * @param {XRSession} session - XR session. * @param {object} options - Presentation options. * @param {number} options.framebufferScaleFactor - Resolved framebuffer scale factor. * @param {number} options.depthNear - Depth near plane. * @param {number} options.depthFar - Depth far plane. * @param {Function} [options.onBindingError] - Called if XRGPUBinding construction fails. */ attachPresentation(session, options) { const XRGPUBindingCtor = globalThis.XRGPUBinding; if (typeof XRGPUBindingCtor === "undefined") { this.xrBridge._onBindingError?.(new Error("XRGPUBinding is not available in this browser.")); return; } const device = this.xrBridge.device; const wgpu = device.wgpu; try { this._binding = new XRGPUBindingCtor(session, wgpu); } catch (ex) { this.xrBridge._onBindingError?.(ex); return; } const colorFormat = this._binding.getPreferredColorFormat(); const layerOpts = { colorFormat, scaleFactor: options.framebufferScaleFactor }; try { this._layer = this._binding.createProjectionLayer({ ...layerOpts, textureType: "texture-array" }); } catch { this._layer = this._binding.createProjectionLayer({ ...layerOpts, textureType: "texture" }); } session.updateRenderState({ layers: [this._layer], depthNear: options.depthNear, depthFar: options.depthFar }); } /** * Copies the XR passthrough camera image for the given XRCamera into a PlayCanvas * {@link Texture} using `copyTextureToTexture`. No-ops if `XRGPUBinding.getCameraImage` is * unavailable or returns nothing this frame. * * @param {any} xrCamera - The XR camera whose image should be copied (XRCamera from WebXR API). * @param {Texture} texture - Destination engine texture. */ syncCameraColorTexture(xrCamera, texture) { if (!this._binding?.getCameraImage) { return; } const src = this._binding.getCameraImage(xrCamera); if (!src) { return; } const dst = texture.impl?.gpuTexture; if (!dst) { return; } const device = this.xrBridge.device; const encoder = device.getCommandEncoder(); const width = xrCamera.width; const height = xrCamera.height; encoder.copyTextureToTexture( { texture: src }, { texture: dst }, [width, height, 1] ); } /** * GPU XR depth texture binding is not implemented for WebGPU yet (`XRGPUBinding` has no depth API). * * @param {any} depthInfo - Depth information from WebXR (`getDepthInformation`); when `texture` is set, GPU depth was negotiated. * @param {Texture} _texture - Unused until WebGPU depth is implemented. * @param {number} _depthPixelFormat - Unused until WebGPU depth is implemented. */ syncCameraDepthTexture(depthInfo, _texture, _depthPixelFormat) { if (depthInfo?.texture) { Debug.warnOnce("WebXR GPU depth textures are not supported on WebGPU in this engine build; use CPU depth or WebGL until XRGPUBinding exposes depth."); } } /** * Clears the WebXR WebGPU binding reference (projection layer remains until session end). */ releasePresentation() { this._binding = null; } /** * Clears WebXR WebGPU binding and projection layer references and removes immersive layers from * the session (mirrors {@link WebglXrBridge#onGraphicsDeviceLost} clearing the base layer). */ onGraphicsDeviceLost() { const bridge = this.xrBridge; const session = bridge._session; if (!session) { return; } const rs = session.renderState; const device = bridge.device; this._binding = null; this._layer = null; this._cachedFramebufferSize.set(0, 0); device._clearXrState(); session.updateRenderState({ layers: [], depthNear: rs.depthNear, depthFar: rs.depthFar }); } /** * Recreates WebXR WebGPU presentation after GPU restore; fires `"error"` on the bridge * {@link XrBridge#eventHandler} if re-attachment fails. */ onGraphicsDeviceRestored() { const bridge = this.xrBridge; if (!bridge._session) { return; } const eventHandler = bridge.eventHandler; setTimeout(() => { if (!bridge._session) { return; } try { const rs = bridge._session.renderState; bridge.attachPresentation(bridge._session, { framebufferScaleFactor: bridge._framebufferScaleFactor, depthNear: rs.depthNear, depthFar: rs.depthFar, onBindingError: bridge._onBindingError }); } catch (ex) { eventHandler.fire("error", ex); } }, 0); } } export { WebgpuXrBridge };