Add WebGPU XR rendering pipeline via XRGPUBinding
All checks were successful
Build / build (push) Successful in 1m42s
All checks were successful
Build / build (push) Successful in 1m42s
Create a parallel WebGPU XR path that uses XRGPUBinding and XRProjectionLayer instead of the WebGL-only XRWebGLLayer, while preserving the existing WebGL XR path as the default fallback. New files in src/core/xr-webgpu/: - xrGpuTypes.ts: TypeScript declarations for XRGPUBinding spec types - xrGpuSessionSetup.ts: GPUDevice access and session init helpers - xrGpuTextureProvider.ts: Per-frame GPUTexture swap via hwTex.set() - xrGpuLayerWrapper.ts: WebXRLayerWrapper subclass for projection layers - xrGpuRenderTarget.ts: WebXRRenderTarget using XRGPUBinding - xrGpuEntryPoint.ts: Public API for availability check and creation Modified xrEntryHandler.ts to conditionally route through WebGPU or WebGL XR entry based on XRGPUBinding availability. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
25286c56b0
commit
0bb27691fe
@ -1,6 +1,7 @@
|
||||
import { AbstractEngine, FreeCamera, Vector3 } from "@babylonjs/core";
|
||||
import { AbstractEngine, FreeCamera, Vector3, WebGPUEngine } from "@babylonjs/core";
|
||||
import { DefaultScene } from "../defaultScene";
|
||||
import { LevelConfig } from "../../levels/config/levelConfig";
|
||||
import { isWebGPUXRAvailable, createWebGPURenderTarget, getWebGPUSessionInit } from "../xr-webgpu/xrGpuEntryPoint";
|
||||
import log from '../logger';
|
||||
|
||||
/**
|
||||
@ -17,10 +18,9 @@ export async function enterXRMode(
|
||||
|
||||
try {
|
||||
prePositionCamera(config);
|
||||
const session = await DefaultScene.XR.baseExperience.enterXRAsync(
|
||||
'immersive-vr',
|
||||
'local-floor'
|
||||
);
|
||||
const session = isWebGPUXRAvailable(engine)
|
||||
? await enterWebGPUXR(engine as WebGPUEngine)
|
||||
: await enterWebGLXR();
|
||||
log.debug('XR session started successfully');
|
||||
return session;
|
||||
} catch (error) {
|
||||
@ -30,6 +30,16 @@ export async function enterXRMode(
|
||||
}
|
||||
}
|
||||
|
||||
async function enterWebGPUXR(engine: WebGPUEngine): Promise<any> {
|
||||
const base = DefaultScene.XR!.baseExperience;
|
||||
const gpuTarget = createWebGPURenderTarget(base.sessionManager, engine, DefaultScene.MainScene);
|
||||
return base.enterXRAsync('immersive-vr', 'local-floor', gpuTarget, getWebGPUSessionInit());
|
||||
}
|
||||
|
||||
async function enterWebGLXR(): Promise<any> {
|
||||
return DefaultScene.XR!.baseExperience.enterXRAsync('immersive-vr', 'local-floor');
|
||||
}
|
||||
|
||||
function prePositionCamera(config: LevelConfig): void {
|
||||
const spawnPos = config.ship?.position || [0, 0, 0];
|
||||
const cockpitPosition = new Vector3(spawnPos[0], spawnPos[1] + 1.2, spawnPos[2]);
|
||||
|
||||
@ -49,18 +49,40 @@ export async function setupScene(
|
||||
}
|
||||
|
||||
async function createEngine(canvas: HTMLCanvasElement): Promise<AbstractEngine> {
|
||||
let engine: AbstractEngine;
|
||||
if (useWebGPU) {
|
||||
log.info('[Engine] Creating WebGPU engine');
|
||||
log.warn('[Engine] WebXR/VR is still experimental with WebGPU engine');
|
||||
engine = await WebGPUEngine.CreateAsync(canvas, { antialias: true });
|
||||
} else {
|
||||
log.info('[Engine] Creating WebGL engine');
|
||||
engine = new Engine(canvas, true);
|
||||
const engine = useWebGPU
|
||||
? await tryCreateWebGPUEngine(canvas)
|
||||
: null;
|
||||
const finalEngine = engine ?? createWebGLEngine(canvas);
|
||||
finalEngine.setHardwareScalingLevel(1 / window.devicePixelRatio);
|
||||
window.onresize = () => finalEngine.resize();
|
||||
return finalEngine;
|
||||
}
|
||||
|
||||
async function tryCreateWebGPUEngine(canvas: HTMLCanvasElement): Promise<AbstractEngine | null> {
|
||||
if (!navigator.gpu) {
|
||||
log.warn('[Engine] WebGPU requested but navigator.gpu not available');
|
||||
return null;
|
||||
}
|
||||
engine.setHardwareScalingLevel(1 / window.devicePixelRatio);
|
||||
window.onresize = () => engine.resize();
|
||||
return engine;
|
||||
const adapter = await navigator.gpu.requestAdapter();
|
||||
if (!adapter) {
|
||||
log.warn('[Engine] No WebGPU adapter found');
|
||||
return null;
|
||||
}
|
||||
log.info(`[Engine] WebGPU adapter: ${adapter.info?.vendor ?? 'unknown'}`);
|
||||
try {
|
||||
const gpuEngine = new WebGPUEngine(canvas, { antialias: true });
|
||||
await gpuEngine.initAsync();
|
||||
log.info('[Engine] WebGPU engine ready — WebXR will use XRGPUBinding if available');
|
||||
return gpuEngine;
|
||||
} catch (e) {
|
||||
log.error('[Engine] WebGPU initialization failed', e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function createWebGLEngine(canvas: HTMLCanvasElement): AbstractEngine {
|
||||
log.info('[Engine] Creating WebGL engine');
|
||||
return new Engine(canvas, true);
|
||||
}
|
||||
|
||||
function createMainScene(engine: AbstractEngine): void {
|
||||
|
||||
28
src/core/xr-webgpu/xrGpuEntryPoint.ts
Normal file
28
src/core/xr-webgpu/xrGpuEntryPoint.ts
Normal file
@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Public API for WebGPU XR support.
|
||||
* Consumed by xrEntryHandler.ts to conditionally use the WebGPU XR path.
|
||||
*/
|
||||
import { WebGPUEngine } from "@babylonjs/core";
|
||||
import type { AbstractEngine, Scene, WebXRSessionManager } from "@babylonjs/core";
|
||||
import { XRGPURenderTarget } from "./xrGpuRenderTarget";
|
||||
import { buildWebGPUSessionInit } from "./xrGpuSessionSetup";
|
||||
import "./xrGpuTypes"; // ensure global XRGPUBinding augmentation is loaded
|
||||
|
||||
export function isWebGPUXRAvailable(engine: AbstractEngine): boolean {
|
||||
return (
|
||||
engine instanceof WebGPUEngine &&
|
||||
typeof globalThis.XRGPUBinding !== 'undefined'
|
||||
);
|
||||
}
|
||||
|
||||
export function createWebGPURenderTarget(
|
||||
sessionManager: WebXRSessionManager,
|
||||
engine: WebGPUEngine,
|
||||
scene: Scene
|
||||
): XRGPURenderTarget {
|
||||
return new XRGPURenderTarget(sessionManager, engine, scene);
|
||||
}
|
||||
|
||||
export function getWebGPUSessionInit(): XRSessionInit {
|
||||
return buildWebGPUSessionInit();
|
||||
}
|
||||
32
src/core/xr-webgpu/xrGpuLayerWrapper.ts
Normal file
32
src/core/xr-webgpu/xrGpuLayerWrapper.ts
Normal file
@ -0,0 +1,32 @@
|
||||
/**
|
||||
* WebXRLayerWrapper subclass for XRProjectionLayer created via XRGPUBinding.
|
||||
*/
|
||||
import { WebGPUEngine } from "@babylonjs/core";
|
||||
import { WebXRLayerWrapper } from "@babylonjs/core/XR/webXRLayerWrapper";
|
||||
import type { Scene } from "@babylonjs/core";
|
||||
import { XRGPUTextureProvider } from "./xrGpuTextureProvider";
|
||||
import type { XRGPUBinding } from "./xrGpuTypes";
|
||||
|
||||
export class XRGPUProjectionLayerWrapper extends WebXRLayerWrapper {
|
||||
constructor(
|
||||
public override readonly layer: XRProjectionLayer,
|
||||
private readonly _gpuBinding: XRGPUBinding,
|
||||
private readonly _gpuEngine: WebGPUEngine,
|
||||
private readonly _scene: Scene
|
||||
) {
|
||||
super(
|
||||
() => layer.textureWidth,
|
||||
() => layer.textureHeight,
|
||||
layer,
|
||||
"XRProjectionLayer",
|
||||
(sessionManager) =>
|
||||
new XRGPUTextureProvider(
|
||||
sessionManager.scene,
|
||||
this,
|
||||
_gpuBinding,
|
||||
layer,
|
||||
_gpuEngine
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
54
src/core/xr-webgpu/xrGpuRenderTarget.ts
Normal file
54
src/core/xr-webgpu/xrGpuRenderTarget.ts
Normal file
@ -0,0 +1,54 @@
|
||||
/**
|
||||
* WebXRRenderTarget implementation for WebGPU.
|
||||
* Creates XRGPUBinding + projection layer instead of XRWebGLLayer.
|
||||
*/
|
||||
import { WebGPUEngine } from "@babylonjs/core";
|
||||
import type { Scene, Nullable, WebXRSessionManager, WebXRRenderTarget } from "@babylonjs/core";
|
||||
import { XRGPUProjectionLayerWrapper } from "./xrGpuLayerWrapper";
|
||||
import { getGpuDeviceFromEngine } from "./xrGpuSessionSetup";
|
||||
import type { XRGPUBinding, XRGPUBindingConstructor } from "./xrGpuTypes";
|
||||
import log from '../logger';
|
||||
|
||||
export class XRGPURenderTarget implements WebXRRenderTarget {
|
||||
/** Unused in WebGPU path — required by interface */
|
||||
public canvasContext: WebGLRenderingContext = null as any;
|
||||
/** Unused in WebGPU path — required by interface */
|
||||
public xrLayer: Nullable<XRWebGLLayer> = null;
|
||||
|
||||
private _sessionManager: WebXRSessionManager;
|
||||
private _engine: WebGPUEngine;
|
||||
private _scene: Scene;
|
||||
|
||||
constructor(sessionManager: WebXRSessionManager, engine: WebGPUEngine, scene: Scene) {
|
||||
this._sessionManager = sessionManager;
|
||||
this._engine = engine;
|
||||
this._scene = scene;
|
||||
}
|
||||
|
||||
public async initializeXRLayerAsync(xrSession: XRSession): Promise<XRWebGLLayer> {
|
||||
const device = getGpuDeviceFromEngine(this._engine);
|
||||
const Binding = globalThis.XRGPUBinding as XRGPUBindingConstructor;
|
||||
const gpuBinding: XRGPUBinding = new Binding(xrSession, device);
|
||||
|
||||
const projectionLayer = gpuBinding.createProjectionLayer({
|
||||
textureFormat: 'rgba8unorm',
|
||||
depthStencilFormat: 'depth24plus-stencil8',
|
||||
});
|
||||
|
||||
log.info('[XR-WebGPU] Projection layer created:', projectionLayer.textureWidth, 'x', projectionLayer.textureHeight);
|
||||
|
||||
xrSession.updateRenderState({ layers: [projectionLayer] } as any);
|
||||
|
||||
const wrapper = new XRGPUProjectionLayerWrapper(
|
||||
projectionLayer, gpuBinding, this._engine, this._scene
|
||||
);
|
||||
this._sessionManager._setBaseLayerWrapper(wrapper);
|
||||
|
||||
log.info('[XR-WebGPU] Layer wrapper set on session manager');
|
||||
return null as any;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
/* nothing to clean up — layer lifetime is tied to XR session */
|
||||
}
|
||||
}
|
||||
30
src/core/xr-webgpu/xrGpuSessionSetup.ts
Normal file
30
src/core/xr-webgpu/xrGpuSessionSetup.ts
Normal file
@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Helpers for accessing the GPUDevice from WebGPUEngine
|
||||
* and building XR session init options for WebGPU.
|
||||
*/
|
||||
import { WebGPUEngine } from "@babylonjs/core";
|
||||
|
||||
/**
|
||||
* Extract the GPUDevice from a WebGPUEngine instance.
|
||||
* WebGPUEngine stores _device privately; this accessor is stable across versions.
|
||||
*/
|
||||
export function getGpuDeviceFromEngine(engine: WebGPUEngine): GPUDevice {
|
||||
const device = (engine as any)._device as GPUDevice | undefined;
|
||||
if (!device) {
|
||||
throw new Error('[XR-WebGPU] Could not access GPUDevice from engine');
|
||||
}
|
||||
return device;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build XRSessionInit that adds 'webgpu' to requiredFeatures.
|
||||
*/
|
||||
export function buildWebGPUSessionInit(
|
||||
baseInit: XRSessionInit = {}
|
||||
): XRSessionInit {
|
||||
const existing = baseInit.requiredFeatures ?? [];
|
||||
return {
|
||||
...baseInit,
|
||||
requiredFeatures: [...existing, 'webgpu'],
|
||||
};
|
||||
}
|
||||
85
src/core/xr-webgpu/xrGpuTextureProvider.ts
Normal file
85
src/core/xr-webgpu/xrGpuTextureProvider.ts
Normal file
@ -0,0 +1,85 @@
|
||||
/**
|
||||
* WebGPU XR texture provider — gets GPUTexture per-view from XRGPUBinding
|
||||
* and swaps it into the cached RenderTargetTexture each frame.
|
||||
*/
|
||||
import { RenderTargetTexture, WebGPUEngine, WebXRLayerRenderTargetTextureProvider } from "@babylonjs/core";
|
||||
import type { Viewport, Scene, Nullable } from "@babylonjs/core";
|
||||
import type { WebXRLayerWrapper } from "@babylonjs/core/XR/webXRLayerWrapper";
|
||||
import type { XRGPUBinding, XRGPUSubImage } from "./xrGpuTypes";
|
||||
import log from '../logger';
|
||||
|
||||
export class XRGPUTextureProvider extends WebXRLayerRenderTargetTextureProvider {
|
||||
private _gpuBinding: XRGPUBinding;
|
||||
private _projLayer: XRProjectionLayer;
|
||||
private _gpuEngine: WebGPUEngine;
|
||||
private _xrScene: Scene;
|
||||
|
||||
constructor(
|
||||
scene: Scene,
|
||||
layerWrapper: WebXRLayerWrapper,
|
||||
gpuBinding: XRGPUBinding,
|
||||
projectionLayer: XRProjectionLayer,
|
||||
gpuEngine: WebGPUEngine
|
||||
) {
|
||||
super(scene, layerWrapper);
|
||||
this._gpuBinding = gpuBinding;
|
||||
this._projLayer = projectionLayer;
|
||||
this._gpuEngine = gpuEngine;
|
||||
this._xrScene = scene;
|
||||
this._framebufferDimensions = {
|
||||
framebufferWidth: projectionLayer.textureWidth,
|
||||
framebufferHeight: projectionLayer.textureHeight,
|
||||
};
|
||||
}
|
||||
|
||||
public getRenderTargetTextureForView(view: XRView): Nullable<RenderTargetTexture> {
|
||||
const subImage = this._gpuBinding.getViewSubImage(this._projLayer, view);
|
||||
const idx = view.eye === "right" ? 1 : 0;
|
||||
if (!this._renderTargetTextures[idx]) {
|
||||
this._renderTargetTextures[idx] = this._createGpuRTT(subImage);
|
||||
} else {
|
||||
this._swapGpuTexture(this._renderTargetTextures[idx], subImage);
|
||||
}
|
||||
return this._renderTargetTextures[idx];
|
||||
}
|
||||
|
||||
public getRenderTargetTextureForEye(eye: XREye): Nullable<RenderTargetTexture> {
|
||||
return this._renderTargetTextures[eye === "right" ? 1 : 0] ?? null;
|
||||
}
|
||||
|
||||
public trySetViewportForView(viewport: Viewport, view: XRView): boolean {
|
||||
const sub = this._gpuBinding.getViewSubImage(this._projLayer, view);
|
||||
if (!sub) return false;
|
||||
const w = this._projLayer.textureWidth;
|
||||
const h = this._projLayer.textureHeight;
|
||||
viewport.x = sub.viewport.x / w;
|
||||
viewport.y = sub.viewport.y / h;
|
||||
viewport.width = sub.viewport.width / w;
|
||||
viewport.height = sub.viewport.height / h;
|
||||
return true;
|
||||
}
|
||||
|
||||
private _createGpuRTT(subImage: XRGPUSubImage): RenderTargetTexture {
|
||||
const w = this._projLayer.textureWidth;
|
||||
const h = this._projLayer.textureHeight;
|
||||
const internalTex = this._gpuEngine.wrapWebGPUTexture(subImage.colorTexture);
|
||||
internalTex.width = w;
|
||||
internalTex.height = h;
|
||||
const rtt = new RenderTargetTexture("xrGpuRTT", { width: w, height: h }, this._xrScene);
|
||||
const origTex = rtt._texture;
|
||||
rtt._texture = internalTex;
|
||||
rtt.renderTarget!.setTexture(internalTex, 0);
|
||||
origTex?.dispose();
|
||||
rtt.disableRescaling();
|
||||
log.debug(`[XR-WebGPU] Created RTT ${w}x${h}`);
|
||||
return rtt;
|
||||
}
|
||||
|
||||
private _swapGpuTexture(rtt: RenderTargetTexture, subImage: XRGPUSubImage): void {
|
||||
const hwTex = rtt._texture?._hardwareTexture as any;
|
||||
if (!hwTex?.set) return;
|
||||
hwTex.set(subImage.colorTexture);
|
||||
hwTex.view = null;
|
||||
hwTex.viewForWriting = null;
|
||||
}
|
||||
}
|
||||
33
src/core/xr-webgpu/xrGpuTypes.ts
Normal file
33
src/core/xr-webgpu/xrGpuTypes.ts
Normal file
@ -0,0 +1,33 @@
|
||||
/**
|
||||
* TypeScript declarations for WebXR-WebGPU Binding spec types.
|
||||
* These are not yet in lib.dom or BabylonJS type definitions.
|
||||
* @see https://github.com/immersive-web/WebXR-WebGPU-Binding/blob/main/explainer.md
|
||||
*/
|
||||
|
||||
export interface XRGPUProjectionLayerInit {
|
||||
textureFormat?: GPUTextureFormat;
|
||||
depthStencilFormat?: GPUTextureFormat;
|
||||
scaleFactor?: number;
|
||||
}
|
||||
|
||||
export interface XRGPUSubImage {
|
||||
colorTexture: GPUTexture;
|
||||
depthStencilTexture?: GPUTexture;
|
||||
imageIndex: number;
|
||||
viewport: { x: number; y: number; width: number; height: number };
|
||||
}
|
||||
|
||||
export interface XRGPUBinding {
|
||||
createProjectionLayer(init?: XRGPUProjectionLayerInit): XRProjectionLayer;
|
||||
getViewSubImage(layer: XRProjectionLayer, view: XRView): XRGPUSubImage;
|
||||
}
|
||||
|
||||
export interface XRGPUBindingConstructor {
|
||||
new (session: XRSession, device: GPUDevice): XRGPUBinding;
|
||||
}
|
||||
|
||||
/** Global augmentation for XRGPUBinding constructor */
|
||||
declare global {
|
||||
// eslint-disable-next-line no-var
|
||||
var XRGPUBinding: XRGPUBindingConstructor | undefined;
|
||||
}
|
||||
@ -26,21 +26,21 @@ export default defineConfig({
|
||||
// Shaders must be explicitly included to avoid dynamic import failures through CloudFlare proxy
|
||||
include: [
|
||||
'@babylonjs/core',
|
||||
// Core shaders
|
||||
// Core shaders (WebGL)
|
||||
'@babylonjs/core/Shaders/default.vertex',
|
||||
'@babylonjs/core/Shaders/default.fragment',
|
||||
'@babylonjs/core/Shaders/rgbdDecode.fragment',
|
||||
'@babylonjs/core/Shaders/procedural.vertex',
|
||||
// PBR shaders
|
||||
// PBR shaders (WebGL)
|
||||
'@babylonjs/core/Shaders/pbr.vertex',
|
||||
'@babylonjs/core/Shaders/pbr.fragment',
|
||||
'@babylonjs/core/Shaders/pbrDebug.fragment',
|
||||
// Particle shaders
|
||||
// Particle shaders (WebGL)
|
||||
'@babylonjs/core/Shaders/particles.vertex',
|
||||
'@babylonjs/core/Shaders/particles.fragment',
|
||||
'@babylonjs/core/Shaders/gpuRenderParticles.vertex',
|
||||
'@babylonjs/core/Shaders/gpuRenderParticles.fragment',
|
||||
// Other common shaders
|
||||
// Other common shaders (WebGL)
|
||||
'@babylonjs/core/Shaders/standard.fragment',
|
||||
'@babylonjs/core/Shaders/postprocess.vertex',
|
||||
'@babylonjs/core/Shaders/pass.fragment',
|
||||
@ -48,6 +48,21 @@ export default defineConfig({
|
||||
'@babylonjs/core/Shaders/shadowMap.fragment',
|
||||
'@babylonjs/core/Shaders/depth.vertex',
|
||||
'@babylonjs/core/Shaders/depth.fragment',
|
||||
// WGSL shaders (WebGPU equivalents)
|
||||
'@babylonjs/core/ShadersWGSL/default.vertex',
|
||||
'@babylonjs/core/ShadersWGSL/default.fragment',
|
||||
'@babylonjs/core/ShadersWGSL/rgbdDecode.fragment',
|
||||
'@babylonjs/core/ShadersWGSL/procedural.vertex',
|
||||
'@babylonjs/core/ShadersWGSL/pbr.vertex',
|
||||
'@babylonjs/core/ShadersWGSL/pbr.fragment',
|
||||
'@babylonjs/core/ShadersWGSL/particles.vertex',
|
||||
'@babylonjs/core/ShadersWGSL/particles.fragment',
|
||||
'@babylonjs/core/ShadersWGSL/postprocess.vertex',
|
||||
'@babylonjs/core/ShadersWGSL/pass.fragment',
|
||||
'@babylonjs/core/ShadersWGSL/shadowMap.vertex',
|
||||
'@babylonjs/core/ShadersWGSL/shadowMap.fragment',
|
||||
'@babylonjs/core/ShadersWGSL/depth.vertex',
|
||||
'@babylonjs/core/ShadersWGSL/depth.fragment',
|
||||
'@babylonjs/loaders',
|
||||
'@babylonjs/havok',
|
||||
'@babylonjs/materials',
|
||||
|
||||
Loading…
Reference in New Issue
Block a user