UNPKG

@react-three/xr

Version:

VR/AR for react-three-fiber

352 lines (351 loc) 15.7 kB
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; import { forwardObjectEvents } from '@pmndrs/pointer-events'; import { createXRLayer, createXRLayerGeometry, createXRLayerRenderTarget, getXRLayerSrcTexture, setupXRImageLayer, setXRLayerRenderTarget, updateXRLayerProperties, updateXRLayerTransform, waitForXRLayerSrcSize, } from '@pmndrs/xr'; import { addEffect, context, reconciler, useFrame, useStore, useThree, } from '@react-three/fiber'; import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState, } from 'react'; import { OrthographicCamera, PerspectiveCamera, Raycaster, Scene, Vector2, Vector3, Vector4, WebGLRenderTarget, } from 'three'; import { create } from 'zustand'; import { useXRSessionFeatureEnabled } from './hooks.js'; import { useXR, useXRStore } from './xr.js'; /** * Component for rendering high quality quad, cylinder, or equirectangular layers inside supported sessions. Also includes a fallback for non-supported sessions. * * @param props * #### `src` - Property for displaying images and videos onto the layer. For rendering dynamic content to the layer, leave the `src` empty and put the dynamic (3D) content into the children, so that the layer acts as a render target. * #### `shape` - Property to configure the shape of the layer ("quad", "cylinder", "equirectangular"). * #### `layout` - Property to configure the layout of the display content for stereo content ("default", "mono", "stereo-left-right", "stereo-top-bottom"). * #### `centralAngle` - Property to configure the central angle in case the layer shape is a "cylinder". * #### `centralHorizontalAngle` - Property to configure the central horizontal angle in case the layer shape is "equirectangular". * #### `upperVerticalAngle` - Property to configure the upper vertical angle in case the layer shape is "equirectangular". * #### `lowerVerticalAngle` - Property to configure the lower verical angle in case the layer shape is "equirectangular". * #### `chromaticAberrationCorrection` - Property to configure whether chromatic abberration should be corrected by the layer. * #### `quality` - Property to configure for what type of content the layer should be optimized ("default", "text-optimized", "graphics-optimized"). */ export function XRLayer({ src, pixelWidth = 1024, pixelHeight = 1024, dpr = 1, renderPriority = 0, children, customRender, ...props }) { const [hasSize, setHasSize] = useState(false); const ref = useRef(null); const renderTargetRef = useRef(undefined); const layerEntryRef = useRef(undefined); useEffect(() => { setHasSize(false); let aborted = false; waitForXRLayerSrcSize(src).then(() => !aborted && setHasSize(true)); return () => void (aborted = true); }, [src]); const layersEnabled = useXRSessionFeatureEnabled('layers'); const geometry = useMemo(() => createXRLayerGeometry(props.shape ?? 'quad', { centralAngle: props.centralAngle, centralHorizontalAngle: props.centralHorizontalAngle, lowerVerticalAngle: props.lowerVerticalAngle, upperVerticalAngle: props.upperVerticalAngle, }), [props.centralAngle, props.centralHorizontalAngle, props.lowerVerticalAngle, props.shape, props.upperVerticalAngle]); const store = useLayerStore(pixelWidth, pixelHeight, dpr); useForwardEvents(store, ref, [hasSize, layersEnabled]); if (!hasSize) { return null; } return (_jsxs(_Fragment, { children: [src == null && (_jsx(ChildrenToRenderTarget, { customRender: customRender, store: store, renderPriority: renderPriority, renderTargetRef: renderTargetRef, layerEntryRef: layersEnabled ? layerEntryRef : undefined, children: children })), layersEnabled ? (_jsx(XRLayerImplementation, { renderTargetRef: renderTargetRef, layerEntryRef: layerEntryRef, pixelWidth: pixelWidth, pixelHeight: pixelHeight, dpr: dpr, ref: ref, ...props, src: src, geometry: geometry })) : (_jsx(FallbackXRLayerImplementation, { renderTargetRef: renderTargetRef, ref: ref, ...props, src: src, pixelWidth: pixelWidth, pixelHeight: pixelHeight, dpr: dpr, geometry: geometry }))] })); } export const XRLayerImplementation = forwardRef(({ src, shape, colorFormat, depthFormat, layout, mipLevels, renderOrder = 0, blendTextureSourceAlpha, centralAngle, centralHorizontalAngle, chromaticAberrationCorrection, lowerVerticalAngle, quality, upperVerticalAngle, invertStereo, pixelWidth, pixelHeight, dpr, renderTargetRef, layerEntryRef, ...props }, ref) => { const internalRef = useRef(null); const renderer = useThree((state) => state.gl); const store = useXRStore(); const layerProperties = { blendTextureSourceAlpha, centralAngle, centralHorizontalAngle, chromaticAberrationCorrection, lowerVerticalAngle, quality, upperVerticalAngle, }; const layerPropertiesRef = useRef(layerProperties); layerPropertiesRef.current = layerProperties; const renderOrderRef = useRef(renderOrder); renderOrderRef.current = renderOrder; const originReferenceSpace = useXR((s) => s.originReferenceSpace); //create layer useEffect(() => { if (internalRef.current == null || originReferenceSpace == null) { return; } const resolvedSrc = src ?? (renderTargetRef.current = createXRLayerRenderTarget(pixelWidth, pixelHeight, dpr)); const layer = createXRLayer(resolvedSrc, store.getState(), originReferenceSpace, renderer.xr, internalRef.current, { colorFormat, depthFormat, invertStereo, layout, mipLevels, shape, }, layerPropertiesRef.current); if (layer == null) { return; } const layerEntry = (layerEntryRef.current = { layer, renderOrder: renderOrderRef.current }); store.addLayerEntry(layerEntry); if (resolvedSrc instanceof HTMLVideoElement || resolvedSrc instanceof WebGLRenderTarget) { return () => { store.removeLayerEntry(layerEntry); layer.destroy(); }; } const cleanupXRImageLayer = setupXRImageLayer(renderer, store, layer, resolvedSrc); return () => { store.removeLayerEntry(layerEntry); cleanupXRImageLayer(); layer.destroy(); }; }, [ originReferenceSpace, colorFormat, depthFormat, invertStereo, layerEntryRef, layout, mipLevels, pixelHeight, pixelWidth, dpr, renderTargetRef, renderer, shape, src, store, ]); //update render order if (layerEntryRef.current != null) { layerEntryRef.current.renderOrder = renderOrder; } //update layer properties if (layerEntryRef.current != null) { updateXRLayerProperties(layerEntryRef.current.layer, layerPropertiesRef.current); } //update layer transform useFrame(() => { if (layerEntryRef.current == null || internalRef.current == null) { return; } updateXRLayerTransform(store.getState(), layerEntryRef.current.layer, layerPropertiesRef.current.centralAngle, internalRef.current); }); useImperativeHandle(ref, () => internalRef.current, []); return (_jsx("mesh", { ...props, renderOrder: -Infinity, ref: internalRef, children: _jsx("meshBasicMaterial", { colorWrite: false }) })); }); export const FallbackXRLayerImplementation = forwardRef(({ src, renderTargetRef, dpr, renderOrder, pixelWidth, pixelHeight, ...props }, ref) => { const materialRef = useRef(null); useEffect(() => { if (materialRef.current == null) { return; } const resolvedSrc = src ?? (renderTargetRef.current = createXRLayerRenderTarget(pixelWidth, pixelHeight, dpr)); const texture = getXRLayerSrcTexture(resolvedSrc); materialRef.current.map = texture; materialRef.current.needsUpdate = true; return () => { if (resolvedSrc instanceof WebGLRenderTarget) { resolvedSrc.dispose(); return; } texture.dispose(); }; }, [src, pixelWidth, pixelHeight, dpr, renderTargetRef]); return (_jsx("mesh", { ref: ref, ...props, children: _jsx("meshBasicMaterial", { ref: materialRef, toneMapped: false }) })); }); function useForwardEvents(store, ref, deps) { useEffect(() => { const { current } = ref; if (current == null) { return; } let cleanup; const update = (state, prevState) => { if (state.camera === prevState?.camera && state.scene === prevState.scene) { return; } cleanup?.(); const { destroy, update } = forwardObjectEvents(current, () => state.camera, state.scene); const cleanupUpdate = addEffect(update); cleanup = () => { destroy(); cleanupUpdate(); }; }; update(store.getState()); const unsubscribe = store.subscribe(update); return () => { unsubscribe(); cleanup?.(); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [store, ref, ...deps]); } // Keys that shouldn't be copied between R3F stores export const privateKeys = [ 'set', 'get', 'setSize', 'setFrameloop', 'setDpr', 'events', 'invalidate', 'advance', 'size', 'viewport', ]; export function useLayerStore(width, height, dpr) { const previousRoot = useStore(); const layerStore = useMemo(() => { let previousState = previousRoot.getState(); // We have our own camera in here, separate from the main scene. const camera = new PerspectiveCamera(50, 1, 0.1, 1000); camera.position.set(0, 0, 5); const pointer = new Vector2(); let ownState = { events: { enabled: false, priority: 0 }, size: { width: 1, height: 1, left: 0, top: 0 }, camera, scene: new Scene(), raycaster: new Raycaster(), pointer: pointer, mouse: pointer, previousRoot, }; //we now merge in order previousState, injectState, ownState const store = create((innerSet, get) => { const merge = () => { const result = {}; for (const key in previousState) { if (privateKeys.includes(key)) { continue; } result[key] = previousState[key]; } return Object.assign(result, ownState, { events: { ...previousState.events, ...ownState.events }, viewport: Object.assign({}, previousState.viewport, previousState.viewport.getCurrentViewport(camera, new Vector3(), ownState.size)), }); }; const update = () => innerSet(merge()); return { ...previousState, // Set and get refer to this root-state set(newOwnState) { if (typeof newOwnState === 'function') { newOwnState = newOwnState(get()); } Object.assign(ownState, newOwnState); update(); }, setPreviousState(prevState) { previousState = prevState; update(); }, get, setEvents() { }, ...merge(), }; }); return Object.assign(store, { setState(state) { store.getState().set(state); }, }); }, [previousRoot]); //syncing up previous store with the current store useEffect(() => previousRoot.subscribe(layerStore.getState().setPreviousState), [previousRoot, layerStore]); useEffect(() => { const viewport = { factor: 1, distance: 0, dpr, initialDpr: dpr, left: 0, top: 0, getCurrentViewport: () => viewport, width, height, aspect: width / height, }; layerStore.setState({ size: { width, height, top: 0, left: 0 }, viewport, }); }, [width, height, dpr, layerStore, previousRoot]); return layerStore; } const v4Helper = new Vector4(); //required hack to support pmndrs/postprocessing function getSize(target) { this.getViewport(v4Helper); target.x = v4Helper.z - v4Helper.x; target.y = v4Helper.w - v4Helper.y; return target; } const viewportHelper = new Vector4(); function ChildrenToRenderTarget({ renderPriority, children, layerEntryRef, renderTargetRef, store, customRender, }) { useEffect(() => { const update = (state, prevState) => { const { size, camera } = state; if (camera instanceof OrthographicCamera) { camera.left = size.width / -2; camera.right = size.width / 2; camera.top = size.height / 2; camera.bottom = size.height / -2; } else { camera.aspect = size.width / size.height; } if (size !== prevState?.size || camera !== prevState.camera) { camera.updateProjectionMatrix(); // https://github.com/pmndrs/react-three-fiber/issues/178 // Update matrix world since the renderer is a frame late camera.updateMatrixWorld(); } }; update(store.getState()); return store.subscribe(update); }, [store]); let oldAutoClear; let oldXrEnabled; let oldIsPresenting; let oldRenderTarget; let oldGetDrawingBufferSize; let oldGetSize; //TODO: support frameloop="demand" useFrame((_, delta, frame) => { if (renderTargetRef.current == null || (layerEntryRef != null && (layerEntryRef.current == null || frame == null))) { return; } const state = store.getState(); const { gl, scene, camera } = state; oldAutoClear = gl.autoClear; oldXrEnabled = gl.xr.enabled; oldIsPresenting = gl.xr.isPresenting; oldRenderTarget = gl.getRenderTarget(); oldGetSize = gl.getSize; oldGetDrawingBufferSize = gl.getDrawingBufferSize; gl.getViewport(viewportHelper); gl.autoClear = true; gl.xr.enabled = false; gl.xr.isPresenting = false; const renderTarget = renderTargetRef.current; gl.setViewport(0, 0, renderTarget.width, renderTarget.height); gl.getSize = getSize; gl.getDrawingBufferSize = getSize; setXRLayerRenderTarget(gl, renderTarget, layerEntryRef?.current, frame); if (customRender != null) { customRender(renderTarget, state, delta, frame); } else { gl.render(scene, camera); } gl.setRenderTarget(oldRenderTarget); gl.setViewport(viewportHelper); gl.autoClear = oldAutoClear; gl.xr.enabled = oldXrEnabled; gl.xr.isPresenting = oldIsPresenting; gl.getSize = oldGetSize; gl.getDrawingBufferSize = oldGetDrawingBufferSize; }, renderPriority); return _jsx(_Fragment, { children: reconciler.createPortal(_jsx(context.Provider, { value: store, children: children }), store, null) }); }