UNPKG

playcanvas

Version:

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

265 lines (264 loc) 8.92 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 { PIXELFORMAT_DEPTH, PIXELFORMAT_R32F } from "../constants.js"; class WebglXrBridge { /** * @param {XrBridge} xrBridge - The XR bridge. */ constructor(xrBridge) { /** * @type {XRWebGLLayer|null} * @private */ __publicField(this, "_presentationLayer", null); /** * @type {XRWebGLBinding|null} * @private */ __publicField(this, "_graphicsBinding", null); /** * Read framebuffer used to blit the XR camera image into the engine texture. * * @type {WebGLFramebuffer|null} * @private */ __publicField(this, "_cameraFbSource", null); /** * Draw framebuffer used to blit the XR camera image into the engine texture. * * @type {WebGLFramebuffer|null} * @private */ __publicField(this, "_cameraFbDest", null); this.xrBridge = xrBridge; } /** * @param {GraphicsDevice} device - The graphics device. */ destroy(device) { this._graphicsBinding = null; this._presentationLayer = null; this._deleteCameraFramebuffers(device); } /** * @param {GraphicsDevice} device - The graphics device. * @private */ _deleteCameraFramebuffers(device) { if (this._cameraFbSource) { const gl = device.gl; gl.deleteFramebuffer(this._cameraFbSource); this._cameraFbSource = null; gl.deleteFramebuffer(this._cameraFbDest); this._cameraFbDest = null; } } /** * Sets the WebGL default framebuffer to the XR session's base layer framebuffer. * When there is no base layer (for example after GPU device loss), falls back to the * canvas framebuffer by assigning null. * * @param {XRFrame} frame - Current XR frame. * @param {XRReferenceSpace|null} _referenceSpace - Active XR reference space. */ beginFrame(frame, _referenceSpace) { const baseLayer = frame.session.renderState.baseLayer; this.xrBridge.device.defaultFramebuffer = baseLayer ? baseLayer.framebuffer : null; } /** * Resets the WebGL default framebuffer to the canvas (null). */ endFrame() { this.xrBridge.device.defaultFramebuffer = null; } /** * @returns {XRWebGLLayer|null} The active XR output layer, if any. */ get presentationLayer() { return this._presentationLayer; } /** * @returns {XRWebGLBinding|null} The WebXR GL binding for GPU camera/depth paths, if any. */ get graphicsBinding() { return this._graphicsBinding; } /** * @param {XRFrame} frame - Current XR frame. * @param {Vec2} out - Width in {@link Vec2#x}, height in {@link Vec2#y}. */ getFramebufferSize(frame, out) { const baseLayer = frame.session.renderState.baseLayer; if (!baseLayer) { out.set(0, 0); return; } out.set(baseLayer.framebufferWidth, baseLayer.framebufferHeight); } /** * @param {XRFrame} frame - Current XR frame. * @param {XRView} xrView - WebXR view. * @returns {XRViewport} Viewport from the session base layer, or zeros if the base layer is unavailable. */ getViewport(frame, xrView) { const baseLayer = frame.session.renderState.baseLayer; if (!baseLayer) { return ( /** @type {XRViewport} */ { x: 0, y: 0, width: 0, height: 0 } ); } return baseLayer.getViewport(xrView); } /** * @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 XRWebGLBinding construction fails. */ attachPresentation(session, options) { const device = this.xrBridge.device; this._presentationLayer = new XRWebGLLayer(session, device.gl, { alpha: true, depth: true, stencil: true, framebufferScaleFactor: options.framebufferScaleFactor, antialias: false }); if (window.XRWebGLBinding) { try { this._graphicsBinding = new XRWebGLBinding(session, device.gl); } catch (ex) { this.xrBridge._onBindingError?.(ex); } } session.updateRenderState({ baseLayer: this._presentationLayer, depthNear: options.depthNear, depthFar: options.depthFar }); } /** * Matches {@link XrManager#end} clearing {@link XrManager#graphicsBinding} only. */ releasePresentation() { this._graphicsBinding = null; } /** * Copies the XR passthrough camera image for the given XRCamera into a PlayCanvas * {@link Texture}, with a Y-flip to match engine UV conventions. No-ops if the graphics * binding is unavailable or the camera image is not ready this frame. * * @param {any} xrCamera - The XR camera whose image should be copied (XRCamera from WebXR API). * @param {Texture} texture - Destination engine texture (must be GPU-uploaded). */ syncCameraColorTexture(xrCamera, texture) { if (!this._graphicsBinding) { return; } const device = this.xrBridge.device; const gl = device.gl; const src = this._graphicsBinding.getCameraImage(xrCamera); if (!src) { return; } if (!this._cameraFbSource) { this._cameraFbSource = gl.createFramebuffer(); this._cameraFbDest = gl.createFramebuffer(); } const width = xrCamera.width; const height = xrCamera.height; device.setFramebuffer(this._cameraFbSource); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, src, 0); device.setFramebuffer(this._cameraFbDest); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture.impl._glTexture, 0); gl.bindFramebuffer(gl.READ_FRAMEBUFFER, this._cameraFbSource); gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, this._cameraFbDest); gl.blitFramebuffer(0, height, width, 0, 0, 0, width, height, gl.COLOR_BUFFER_BIT, gl.NEAREST); device.setFramebuffer(device.defaultFramebuffer); } /** * Aliases the XR runtime depth GL texture into the engine {@link Texture} implementation. * * @param {any} depthInfo - Depth information from WebXR (`getDepthInformation`). * @param {Texture} texture - Destination engine texture. * @param {number} depthPixelFormat - Resolved depth pixel format (`PIXELFORMAT_R32F` or `PIXELFORMAT_DEPTH`). */ syncCameraDepthTexture(depthInfo, texture, depthPixelFormat) { if (!depthInfo?.texture) { return; } const gl = this.xrBridge.device.gl; texture.impl._glTexture = depthInfo.texture; if (depthInfo.textureType === "texture-array") { texture.impl._glTarget = gl.TEXTURE_2D_ARRAY; } else { texture.impl._glTarget = gl.TEXTURE_2D; } switch (depthPixelFormat) { case PIXELFORMAT_R32F: texture.impl._glInternalFormat = gl.R32F; texture.impl._glPixelType = gl.FLOAT; texture.impl._glFormat = gl.RED; break; case PIXELFORMAT_DEPTH: texture.impl._glInternalFormat = gl.DEPTH_COMPONENT16; texture.impl._glPixelType = gl.UNSIGNED_SHORT; texture.impl._glFormat = gl.DEPTH_COMPONENT; break; } texture.impl._glCreated = true; } onGraphicsDeviceLost() { const session = this.xrBridge._session; if (!session) { return; } const rs = session.renderState; this._graphicsBinding = null; this._presentationLayer = null; this._cameraFbSource = null; this._cameraFbDest = null; session.updateRenderState({ baseLayer: this._presentationLayer, depthNear: rs.depthNear, depthFar: rs.depthFar }); } /** * Recreates presentation after GPU restore; fires `"error"` on the bridge {@link XrBridge#eventHandler} if restore fails. */ onGraphicsDeviceRestored() { const bridge = this.xrBridge; if (!bridge._session) { return; } const device = bridge.device; const eventHandler = bridge.eventHandler; setTimeout(() => { if (!bridge._session) { return; } device.gl.makeXRCompatible().then(() => { if (!bridge._session) { return; } 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 { WebglXrBridge };