Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 | 1x 1x 1x 2x 2x 2x 2x 1x 1x 1x 1x 1x 2x 1x 1x 1x 1x 1x 1x 1x 1x 2x 2x 2x 2x 2x | import { useCallback, useEffect, useMemo, useState } from 'react';
import { useApp } from '@playcanvas/react/hooks';
import { CameraComponent, XRSPACE_LOCALFLOOR, XRTYPE_AR, XRTYPE_VR } from 'playcanvas';
export type XrType = 'VR' | 'AR';
interface UseXrOptions {
onError?: (error: Error) => void;
}
const XR_TYPES: Record<XrType, string> = {
VR: XRTYPE_VR,
AR: XRTYPE_AR,
};
/**
* Factor PlayCanvas applies on top of the device pixel ratio to compute the headset's
* framebuffer resolution: `app.graphicsDevice.maxPixelRatio / window.devicePixelRatio *
* XR_FRAMEBUFFER_SCALE_FACTOR`. Splat rendering in stereo is fill-rate bound, so this (and
* `maxPixelRatio`, which also multiplies XR resolution) is the largest lever on XR framerate.
* Read once when the session starts and fixed for its lifetime.
*/
const XR_FRAMEBUFFER_SCALE_FACTOR = 1;
export const useXr = ({ onError }: UseXrOptions = {}) => {
const app = useApp();
// Seeded from the manager rather than defaulting to unavailable, so a caller acting in a mount
// effect isn't told VR is unsupported while the effect below is still waiting to run.
const [available, setAvailable] = useState<Record<XrType, boolean>>(() => ({
VR: app.xr?.isAvailable(XRTYPE_VR) ?? false,
AR: app.xr?.isAvailable(XRTYPE_AR) ?? false,
}));
// `type` is set as soon as a session is requested, so gate on `active` to ignore pending sessions.
const [currentXrType, setCurrentXrType] = useState<string | null>(app.xr?.active ? app.xr.type : null);
useEffect(() => {
// We subscribe to the manager directly because `app` doesn't update when XR's availability changes.
const handleAvailable = (type: string, isAvailable: boolean) => {
if (type === XRTYPE_VR) {
setAvailable((prev) => ({ ...prev, VR: isAvailable }));
} else if (type === XRTYPE_AR) {
setAvailable((prev) => ({ ...prev, AR: isAvailable }));
}
};
setAvailable({
VR: app.xr?.isAvailable(XRTYPE_VR) ?? false,
AR: app.xr?.isAvailable(XRTYPE_AR) ?? false,
});
app.xr?.on('available', handleAvailable);
return () => {
app.xr?.off('available', handleAvailable);
};
}, [app]);
useEffect(() => {
const handleStart = () => setCurrentXrType(app.xr?.type ?? null);
// `app.xr.type` isn't cleared until after the `end` event fires, so don't read it here.
const handleEnd = () => setCurrentXrType(null);
setCurrentXrType(app.xr?.active ? app.xr.type : null);
app.xr?.on('start', handleStart);
app.xr?.on('end', handleEnd);
return () => {
app.xr?.off('start', handleStart);
app.xr?.off('end', handleEnd);
};
}, [app]);
const isXrAvailable = useCallback((type: XrType) => available[type], [available]);
const startXr = useCallback(
(type: XrType) => {
const camera = app.root.findComponent('camera') as CameraComponent;
app.xr?.start(camera, XR_TYPES[type], XRSPACE_LOCALFLOOR, {
framebufferScaleFactor: XR_FRAMEBUFFER_SCALE_FACTOR,
callback: (err: Error | null) => {
if (err) {
onError?.(err);
app.xr?.end();
}
},
});
},
[app, onError]
);
const endXr = useCallback(() => {
app.xr?.end();
}, [app]);
return useMemo(
() => ({
isXrAvailable,
startXr,
endXr,
isCurrentlyInXr: currentXrType !== null,
isCurrentlyInAr: currentXrType === XRTYPE_AR,
isCurrentlyInVr: currentXrType === XRTYPE_VR,
}),
[isXrAvailable, startXr, endXr, currentXrType]
);
};
export default useXr;
|