Add optional WebGPU engine support via ?webGPU=true query parameter

Allows launching the game with WebGPU rendering by passing ?webGPU=true in the URL.
Defaults to WebGL engine when the parameter is absent. Logs a warning that WebXR/VR
is not yet supported with WebGPU.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Michael Mainguy 2026-03-03 06:39:24 -06:00
parent 208d735ef4
commit 53388c8193
6 changed files with 33 additions and 18 deletions

View File

@ -1,11 +1,11 @@
import { Engine } from "@babylonjs/core"; import { AbstractEngine } from "@babylonjs/core";
import { DefaultScene } from "./defaultScene"; import { DefaultScene } from "./defaultScene";
import { RockFactory } from "../environment/asteroids/rockFactory"; import { RockFactory } from "../environment/asteroids/rockFactory";
import log from './logger'; import log from './logger';
import Level from "../levels/level"; import Level from "../levels/level";
export interface CleanupContext { export interface CleanupContext {
getEngine(): Engine; getEngine(): AbstractEngine;
getCurrentLevel(): Level | null; getCurrentLevel(): Level | null;
setCurrentLevel(level: Level | null): void; setCurrentLevel(level: Level | null): void;
resetState(): void; resetState(): void;

View File

@ -1,4 +1,4 @@
import { AudioEngineV2, Engine, ParticleHelper } from "@babylonjs/core"; import { AbstractEngine, AudioEngineV2, ParticleHelper } from "@babylonjs/core";
import { DefaultScene } from "../defaultScene"; import { DefaultScene } from "../defaultScene";
import { Level1 } from "../../levels/level1"; import { Level1 } from "../../levels/level1";
import Level from "../../levels/level"; import Level from "../../levels/level";
@ -20,7 +20,7 @@ export interface LevelSelectedContext {
initializeEngine(): Promise<void>; initializeEngine(): Promise<void>;
initializeXR(): Promise<void>; initializeXR(): Promise<void>;
getAudioEngine(): AudioEngineV2; getAudioEngine(): AudioEngineV2;
getEngine(): Engine; getEngine(): AbstractEngine;
setCurrentLevel(level: Level): void; setCurrentLevel(level: Level): void;
setProgressCallback(callback: (percent: number, message: string) => void): void; setProgressCallback(callback: (percent: number, message: string) => void): void;
play(): Promise<void>; play(): Promise<void>;
@ -169,7 +169,7 @@ function attachAudioListener(audioEngine: AudioEngineV2): void {
async function finalizeLevelStart( async function finalizeLevelStart(
level: Level1, level: Level1,
engine: Engine, engine: AbstractEngine,
preloader: Preloader, preloader: Preloader,
context: LevelSelectedContext context: LevelSelectedContext
): Promise<void> { ): Promise<void> {
@ -188,7 +188,7 @@ async function finalizeLevelStart(
await context.play(); await context.play();
} }
function showCanvasForFlatMode(engine: Engine): void { function showCanvasForFlatMode(engine: AbstractEngine): void {
const canvas = document.getElementById('gameCanvas'); const canvas = document.getElementById('gameCanvas');
if (canvas) canvas.style.display = 'block'; if (canvas) canvas.style.display = 'block';
engine.stopRenderLoop(); engine.stopRenderLoop();

View File

@ -1,4 +1,4 @@
import { Engine, FreeCamera, Vector3 } from "@babylonjs/core"; import { AbstractEngine, FreeCamera, Vector3 } from "@babylonjs/core";
import { DefaultScene } from "../defaultScene"; import { DefaultScene } from "../defaultScene";
import { LevelConfig } from "../../levels/config/levelConfig"; import { LevelConfig } from "../../levels/config/levelConfig";
import log from '../logger'; import log from '../logger';
@ -9,7 +9,7 @@ import log from '../logger';
*/ */
export async function enterXRMode( export async function enterXRMode(
config: LevelConfig, config: LevelConfig,
engine: Engine engine: AbstractEngine
): Promise<any> { ): Promise<any> {
if (!DefaultScene.XR) { if (!DefaultScene.XR) {
return startFlatMode(engine); return startFlatMode(engine);
@ -39,7 +39,7 @@ function prePositionCamera(config: LevelConfig): void {
log.debug('[XR] Camera pre-positioned at cockpit:', cockpitPosition.toString()); log.debug('[XR] Camera pre-positioned at cockpit:', cockpitPosition.toString());
} }
function startFlatMode(engine: Engine): null { function startFlatMode(engine: AbstractEngine): null {
const canvas = document.getElementById('gameCanvas'); const canvas = document.getElementById('gameCanvas');
if (canvas) canvas.style.display = 'block'; if (canvas) canvas.style.display = 'block';
engine.stopRenderLoop(); engine.stopRenderLoop();

3
src/core/queryParams.ts Normal file
View File

@ -0,0 +1,3 @@
const urlParams = new URLSearchParams(window.location.search);
export const useWebGPU = urlParams.get('webGPU') === 'true';

View File

@ -1,18 +1,22 @@
import { import {
AbstractEngine,
AudioEngineV2, AudioEngineV2,
Color3, Color3,
CreateAudioEngineAsync, CreateAudioEngineAsync,
Engine, Engine,
HavokPlugin, HavokPlugin,
Scene, Scene,
Vector3 Vector3,
WebGPUEngine,
} from "@babylonjs/core"; } from "@babylonjs/core";
import HavokPhysics from "@babylonjs/havok"; import HavokPhysics from "@babylonjs/havok";
import { DefaultScene } from "./defaultScene"; import { DefaultScene } from "./defaultScene";
import { ProgressReporter } from "./xrSetup"; import { ProgressReporter } from "./xrSetup";
import { useWebGPU } from "./queryParams";
import log from './logger';
export interface SceneSetupResult { export interface SceneSetupResult {
engine: Engine; engine: AbstractEngine;
audioEngine: AudioEngineV2; audioEngine: AudioEngineV2;
} }
@ -24,7 +28,7 @@ export async function setupScene(
reporter: ProgressReporter reporter: ProgressReporter
): Promise<SceneSetupResult> { ): Promise<SceneSetupResult> {
reporter.reportProgress(5, 'Creating rendering engine...'); reporter.reportProgress(5, 'Creating rendering engine...');
const engine = createEngine(canvas); const engine = await createEngine(canvas);
reporter.reportProgress(10, 'Creating scene...'); reporter.reportProgress(10, 'Creating scene...');
createMainScene(engine); createMainScene(engine);
@ -44,14 +48,22 @@ export async function setupScene(
return { engine, audioEngine }; return { engine, audioEngine };
} }
function createEngine(canvas: HTMLCanvasElement): Engine { async function createEngine(canvas: HTMLCanvasElement): Promise<AbstractEngine> {
const engine = new Engine(canvas, true); let engine: AbstractEngine;
if (useWebGPU) {
log.info('[Engine] Creating WebGPU engine');
log.warn('[Engine] WebXR/VR is still experimental');
engine = await WebGPUEngine.CreateAsync(canvas, { antialias: true });
} else {
log.info('[Engine] Creating WebGL engine');
engine = new Engine(canvas, true);
}
engine.setHardwareScalingLevel(1 / window.devicePixelRatio); engine.setHardwareScalingLevel(1 / window.devicePixelRatio);
window.onresize = () => engine.resize(); window.onresize = () => engine.resize();
return engine; return engine;
} }
function createMainScene(engine: Engine): void { function createMainScene(engine: AbstractEngine): void {
// Dispose old scene if it exists (prevents doubling on reload) // Dispose old scene if it exists (prevents doubling on reload)
if (DefaultScene.MainScene && !DefaultScene.MainScene.isDisposed) { if (DefaultScene.MainScene && !DefaultScene.MainScene.isDisposed) {
DefaultScene.MainScene.dispose(); DefaultScene.MainScene.dispose();

View File

@ -1,4 +1,4 @@
import { AudioEngineV2, Engine } from "@babylonjs/core"; import { AbstractEngine, AudioEngineV2 } from "@babylonjs/core";
import '@babylonjs/loaders'; import '@babylonjs/loaders';
import { DefaultScene } from "./core/defaultScene"; import { DefaultScene } from "./core/defaultScene";
@ -20,7 +20,7 @@ const canvas = document.querySelector('#gameCanvas') as HTMLCanvasElement;
export class Main implements LevelSelectedContext, CleanupContext { export class Main implements LevelSelectedContext, CleanupContext {
private _currentLevel: Level | null = null; private _currentLevel: Level | null = null;
private _engine: Engine; private _engine: AbstractEngine;
private _audioEngine: AudioEngineV2; private _audioEngine: AudioEngineV2;
private _initialized: boolean = false; private _initialized: boolean = false;
private _assetsLoaded: boolean = false; private _assetsLoaded: boolean = false;
@ -43,7 +43,7 @@ export class Main implements LevelSelectedContext, CleanupContext {
areAssetsLoaded(): boolean { return this._assetsLoaded; } areAssetsLoaded(): boolean { return this._assetsLoaded; }
setAssetsLoaded(value: boolean): void { this._assetsLoaded = value; } setAssetsLoaded(value: boolean): void { this._assetsLoaded = value; }
getAudioEngine(): AudioEngineV2 { return this._audioEngine; } getAudioEngine(): AudioEngineV2 { return this._audioEngine; }
getEngine(): Engine { return this._engine; } getEngine(): AbstractEngine { return this._engine; }
setCurrentLevel(level: Level): void { this._currentLevel = level; } setCurrentLevel(level: Level): void { this._currentLevel = level; }
setProgressCallback(cb: (percent: number, message: string) => void): void { setProgressCallback(cb: (percent: number, message: string) => void): void {
this._progressCallback = cb; this._progressCallback = cb;