Skip to content

Commit 55c1b79

Browse files
committed
latest
1 parent 83a2cdf commit 55c1b79

3 files changed

Lines changed: 41 additions & 25 deletions

File tree

src/index.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,8 @@ const createApp = async (canvas: HTMLCanvasElement, config: Config) => {
9494
const useWebGPU = config.renderer === 'webgpu';
9595

9696
// Create the graphics device. The engine auto-appends WebGL2/null fallbacks
97-
// when WebGPU isn't supported, so request xrCompatible so the WebGL fallback
98-
// is also usable for AR/VR.
97+
// when WebGPU isn't supported. Request xrCompatible so the device — WebGPU
98+
// (via XRGPUBinding) or the WebGL fallback — is usable for AR/VR.
9999
const device = await createGraphicsDevice(canvas, {
100100
deviceTypes: useWebGPU ? ['webgpu'] : [],
101101
antialias: false,
@@ -262,8 +262,8 @@ const main = async (canvas: HTMLCanvasElement, settingsJson: any, config: Config
262262

263263
camera.addComponent('camera');
264264

265-
// Initialize XR support (availability detection always runs so the UI can offer
266-
// a reload into WebGL when the user requests AR/VR under WebGPU)
265+
// Initialize XR support (any backend; when the current device can't host a
266+
// session the UI can offer a reload into WebGL instead)
267267
initXr(global);
268268

269269
// Initialize user interface

src/ui.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -393,10 +393,11 @@ const initUI = (global: Global) => {
393393
const arChanged = () => dom.arMode.classList[state.hasAR ? 'remove' : 'add']('hidden');
394394
const vrChanged = () => dom.vrMode.classList[state.hasVR ? 'remove' : 'add']('hidden');
395395

396-
// XR sessions require a WebGL device. Under WebGPU, prompt the user to reload
397-
// the viewer with the WebGL renderer before starting AR/VR. Use replace() so
398-
// the renderer-switch reload doesn't add a back-button entry — important
399-
// because the viewer often runs inside an iframe (e.g. superspl.at /scene).
396+
// When a session can't start on the current (WebGPU) device but would work on
397+
// WebGL, prompt the user to reload the viewer with the WebGL renderer before
398+
// starting AR/VR. Use replace() so the renderer-switch reload doesn't add a
399+
// back-button entry — important because the viewer often runs inside an
400+
// iframe (e.g. superspl.at /scene).
400401
const reloadWithWebgl = () => {
401402
const reloadUrl = new URL(location.href);
402403
reloadUrl.searchParams.set('webgl', '');
@@ -411,10 +412,13 @@ const initUI = (global: Global) => {
411412
dom.xrModal.addEventListener('pointerdown', hideXrModal);
412413

413414
const handleXrClick = (type: 'AR' | 'VR') => {
414-
if (global.renderer !== 'webgl') {
415-
showXrModal();
416-
} else {
415+
// Availability is backend-aware: when the session can start on the current
416+
// device (WebGPU included), start it directly. Otherwise the button is only
417+
// visible because the session would work on WebGL, so offer the reload.
418+
if (global.app.xr.isAvailable(type === 'AR' ? 'immersive-ar' : 'immersive-vr')) {
417419
events.fire(type === 'AR' ? 'startAR' : 'startVR');
420+
} else {
421+
showXrModal();
418422
}
419423
};
420424

src/xr.ts

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import {
22
Color,
3+
DEVICETYPE_WEBGL2,
34
Entity,
45
Quat,
56
Vec3,
7+
XrManager,
68
type CameraComponent
79
} from 'playcanvas';
810
import { XrControllers } from 'playcanvas/scripts/esm/xr-controllers.mjs';
@@ -14,21 +16,31 @@ import { Global } from './types';
1416
const initXr = (global: Global) => {
1517
const { app, events, state, camera, renderer } = global;
1618

17-
state.hasAR = app.xr.isAvailable('immersive-ar');
18-
state.hasVR = app.xr.isAvailable('immersive-vr');
19-
20-
// initialize ar/vr
21-
app.xr.on('available:immersive-ar', (available) => {
22-
state.hasAR = available;
23-
});
24-
app.xr.on('available:immersive-vr', (available) => {
25-
state.hasVR = available;
26-
});
19+
// Engine availability is backend-aware (2.20+): under WebGPU a session is only
20+
// reported available when it can start on the current device (browser exposes
21+
// XRGPUBinding, e.g. Safari on Apple Vision Pro). A session the WebGPU device
22+
// can't host may still run after reloading into WebGL, so keep the buttons
23+
// visible then — the UI offers that reload when the session can't start directly.
24+
let webglAR = false;
25+
let webglVR = false;
26+
27+
const updateAvailable = () => {
28+
state.hasAR = app.xr.isAvailable('immersive-ar') || webglAR;
29+
state.hasVR = app.xr.isAvailable('immersive-vr') || webglVR;
30+
};
2731

28-
// XR sessions require a WebGL device; under WebGPU we only expose availability so
29-
// the UI can offer to reload the viewer in WebGL mode.
30-
if (renderer !== 'webgl') {
31-
return;
32+
updateAvailable();
33+
app.xr.on('available', updateAvailable);
34+
35+
if (renderer === 'webgpu') {
36+
Promise.all([
37+
XrManager.isDeviceSupported(DEVICETYPE_WEBGL2, 'immersive-ar'),
38+
XrManager.isDeviceSupported(DEVICETYPE_WEBGL2, 'immersive-vr')
39+
]).then(([ar, vr]) => {
40+
webglAR = ar;
41+
webglVR = vr;
42+
updateAvailable();
43+
});
3244
}
3345

3446
const parent = camera.parent as Entity;

0 commit comments

Comments
 (0)