playcanvas
Version:
Open-source WebGL/WebGPU 3D engine for the web
265 lines (264 loc) • 8.92 kB
JavaScript
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
};