Skip to content

Commit f878ec8

Browse files
committed
feat: xr render target layer
1 parent 662a7b0 commit f878ec8

File tree

5 files changed

+3583
-2834
lines changed

5 files changed

+3583
-2834
lines changed

examples/layers/app.tsx

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
import { Canvas } from '@react-three/fiber'
2-
import { createXRStore, XR, XRLayer } from '@react-three/xr'
3-
import { useMemo } from 'react'
1+
import { Canvas, useThree } from '@react-three/fiber'
2+
import { createXRStore, useHover, XR, XRLayer } from '@react-three/xr'
3+
import { useEffect, useMemo, useRef } from 'react'
4+
import { Mesh } from 'three'
5+
import { forwardHtmlEvents } from '@pmndrs/pointer-events'
46

57
const store = createXRStore()
68

@@ -14,23 +16,50 @@ export function App() {
1416
return (
1517
<>
1618
<button onClick={() => store.enterAR()}>Enter AR</button>
17-
<Canvas style={{ width: '100%', flexGrow: 1 }} camera={{ position: [0, 1.5, 0], rotation: [0, 0, 0] }}>
19+
<Canvas
20+
events={() => ({ enabled: false, priority: 0 })}
21+
style={{ width: '100%', flexGrow: 1 }}
22+
camera={{ position: [0, 1.5, 0], rotation: [0, 0, 0] }}
23+
>
24+
<SwitchToXRPointerEvents />
1825
<XR store={store}>
1926
{image != null && (
2027
<XRLayer
21-
onClick={() => {}}
2228
position={[0, 1.5, -0.5]}
23-
scale={0.2}
24-
shape="equirect"
29+
scale={1}
30+
shape="quad"
31+
pixelHeight={1024}
32+
pixelWidth={1024}
33+
centralAngle={Math.PI}
34+
blendTextureSourceAlpha
2535
centralHorizontalAngle={Math.PI}
2636
lowerVerticalAngle={-Math.PI / 2}
2737
upperVerticalAngle={Math.PI / 2}
28-
src={image}
29-
quality="graphics-optimized"
30-
/>
38+
>
39+
<Inner />
40+
</XRLayer>
3141
)}
3242
</XR>
3343
</Canvas>
3444
</>
3545
)
3646
}
47+
48+
function Inner() {
49+
const ref = useRef<Mesh>(null)
50+
const hover = useHover(ref)
51+
return (
52+
<mesh ref={ref}>
53+
<boxGeometry />
54+
<meshBasicMaterial color={hover ? 'red' : 'blue'} />
55+
</mesh>
56+
)
57+
}
58+
59+
export function SwitchToXRPointerEvents() {
60+
const domElement = useThree((s) => s.gl.domElement)
61+
const camera = useThree((s) => s.camera)
62+
const scene = useThree((s) => s.scene)
63+
useEffect(() => forwardHtmlEvents(domElement, camera, scene), [domElement, camera, scene])
64+
return null
65+
}

examples/layers/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"dependencies": {
3-
"@react-three/xr": "workspace:^"
3+
"@react-three/xr": "workspace:^",
4+
"@pmndrs/pointer-events": "workspace:^"
45
},
56
"scripts": {
67
"dev": "vite --host"

packages/react/xr/src/layer/index.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,24 @@
11
import { useSessionFeatureEnabled } from '../hooks.js'
22
import { useXRLayerGeometry } from './geometry.js'
33
import { NonXRImageLayer, XRImageLayer, XRImageLayerProperties } from './image.js'
4+
import { NonXRRenderTargetLayer, XRRenderTargetLayer, XRRenderTargetLayerProperties } from './render-target.js'
45
import { NonXRVideoLayer, XRVideoLayer, XRVideoLayerProperties } from './video.js'
56

6-
export function XRLayer(props: Omit<XRImageLayerProperties | XRVideoLayerProperties, 'geometry'>) {
7+
export function XRLayer(
8+
props:
9+
| Omit<XRImageLayerProperties, 'geometry'>
10+
| Omit<XRVideoLayerProperties, 'geometry'>
11+
| Omit<XRRenderTargetLayerProperties, 'geometry'>,
12+
) {
713
const layersEnabled = useSessionFeatureEnabled('layers')
814
const geometry = useXRLayerGeometry(props)
15+
if (props.src == null) {
16+
return layersEnabled ? (
17+
<XRRenderTargetLayer {...props} geometry={geometry} />
18+
) : (
19+
<NonXRRenderTargetLayer {...props} geometry={geometry} />
20+
)
21+
}
922
if (props.src instanceof HTMLVideoElement) {
1023
return layersEnabled ? (
1124
<XRVideoLayer {...(props as XRVideoLayerProperties)} geometry={geometry} />

packages/react/xr/src/layer/render-target.tsx

Lines changed: 52 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,15 @@ import {
88
useStore,
99
useThree,
1010
} from '@react-three/fiber'
11-
import { forwardRef, ReactNode, RefObject, useCallback, useEffect, useImperativeHandle, useMemo, useRef } from 'react'
11+
import { ReactNode, RefObject, useCallback, useEffect, useMemo, useRef } from 'react'
1212
import {
13+
DepthTexture,
14+
HalfFloatType,
15+
LinearFilter,
1316
Mesh,
1417
MeshBasicMaterial,
1518
Object3D,
19+
OrthographicCamera,
1620
PerspectiveCamera,
1721
Raycaster,
1822
Scene,
@@ -23,7 +27,6 @@ import {
2327
import { useXRLayer } from './layer.js'
2428
import { createXRLayer, XRLayerEntry, XRLayerOptions, XRLayerProperties, XRLayerShape } from '@pmndrs/xr'
2529
import { XRStore } from '../xr.js'
26-
import { isOrthographicCamera } from '@react-three/fiber/dist/declarations/src/core/utils.js'
2730
import { create, StoreApi, UseBoundStore } from 'zustand'
2831
import { forwardObjectEvents } from '@pmndrs/pointer-events'
2932

@@ -43,10 +46,13 @@ export type XRRenderTargetLayerProperties = {
4346
renderPriority?: number
4447
pixelWidth: number
4548
pixelHeight: number
49+
src?: undefined
4650
} & Omit<XRLayerOptions, 'textureType' | 'isStatic' | 'layout'> &
4751
XRLayerProperties &
4852
MeshProps
4953

54+
//TODO: custom geometry and border radius for cut outs
55+
5056
export function NonXRRenderTargetLayer({
5157
renderOrder,
5258
pixelWidth,
@@ -55,31 +61,32 @@ export function NonXRRenderTargetLayer({
5561
renderPriority = 0,
5662
...props
5763
}: XRRenderTargetLayerProperties) {
58-
const renderTargetRef = useRef<WebGLRenderTarget>(null)
5964
const ref = useRef<Mesh>(null)
65+
const layerStore = useLayerStore(pixelWidth, pixelHeight)
66+
const renderTargetRef = useRenderTargetRef(pixelWidth, pixelHeight)
6067
const materialRef = useRef<MeshBasicMaterial>(null)
6168
useEffect(() => {
6269
if (materialRef.current == null || renderTargetRef.current == null) {
6370
return
6471
}
6572
materialRef.current.map = renderTargetRef.current.texture
66-
}, [])
73+
materialRef.current.needsUpdate = true
74+
}, [renderTargetRef])
6775

68-
const layerStore = useLayerStore(pixelWidth, pixelHeight)
6976
useForwardEvents(layerStore, ref)
7077
return (
7178
<>
7279
{reconciler.createPortal(
7380
<context.Provider value={layerStore}>
74-
<ChildrenToRenderTarget ref={renderTargetRef} renderPriority={renderPriority}>
81+
<ChildrenToRenderTarget renderTargetRef={renderTargetRef} renderPriority={renderPriority}>
7582
{children}
7683
</ChildrenToRenderTarget>
7784
</context.Provider>,
7885
layerStore,
7986
null,
8087
)}
81-
<mesh {...props}>
82-
<meshBasicMaterial toneMapped={false} />
88+
<mesh ref={ref} {...props}>
89+
<meshBasicMaterial ref={materialRef} toneMapped={false} />
8390
</mesh>
8491
</>
8592
)
@@ -131,19 +138,24 @@ export function XRRenderTargetLayer({
131138
})
132139
const layerStore = useLayerStore(pixelWidth, pixelHeight)
133140
useForwardEvents(layerStore, ref)
141+
const renderTargetRef = useRenderTargetRef(pixelWidth, pixelHeight)
134142
return (
135143
<>
136144
{reconciler.createPortal(
137145
<context.Provider value={layerStore}>
138-
<ChildrenToRenderTarget renderPriority={renderPriority} layerEntryRef={layerEntryRef}>
146+
<ChildrenToRenderTarget
147+
renderTargetRef={renderTargetRef}
148+
renderPriority={renderPriority}
149+
layerEntryRef={layerEntryRef}
150+
>
139151
{children}
140152
</ChildrenToRenderTarget>
141153
</context.Provider>,
142154
layerStore,
143155
null,
144156
)}
145157
<mesh {...props} renderOrder={-Infinity} ref={ref}>
146-
<meshBasicMaterial colorWrite={false} />
158+
<meshBasicMaterial />
147159
</mesh>
148160
</>
149161
)
@@ -262,27 +274,37 @@ export function useLayerStore(width: number, height: number) {
262274
return layerStore
263275
}
264276

265-
const ChildrenToRenderTarget = forwardRef<
266-
WebGLRenderTarget,
267-
{
268-
renderPriority: number
269-
children: ReactNode
270-
layerEntryRef?: RefObject<XRLayerEntry | undefined>
271-
}
272-
>(({ renderPriority, children, layerEntryRef }, ref) => {
273-
const store = useStore()
274-
277+
export function useRenderTargetRef(width: number, height: number) {
275278
const renderTargetRef = useRef<WebGLRenderTarget | undefined>(undefined)
276279
useEffect(() => {
277-
const renderTarget = (renderTargetRef.current = new WebGLRenderTarget(1, 1, {}))
280+
const renderTarget = (renderTargetRef.current = new WebGLRenderTarget(width, height, {
281+
minFilter: LinearFilter,
282+
magFilter: LinearFilter,
283+
type: HalfFloatType,
284+
depthTexture: new DepthTexture(width, height),
285+
}))
278286
return () => renderTarget.dispose()
279-
}, [])
287+
}, [width, height])
288+
return renderTargetRef
289+
}
290+
291+
function ChildrenToRenderTarget({
292+
renderPriority,
293+
children,
294+
layerEntryRef,
295+
renderTargetRef,
296+
}: {
297+
renderPriority: number
298+
children: ReactNode
299+
layerEntryRef?: RefObject<XRLayerEntry | undefined>
300+
renderTargetRef: RefObject<WebGLRenderTarget | undefined>
301+
}) {
302+
const store = useStore()
280303

281304
useEffect(() => {
282305
const update = (state: RootState, prevState?: RootState) => {
283306
const { size, camera } = state
284-
renderTargetRef.current?.setSize(size.width, size.height)
285-
if (isOrthographicCamera(camera)) {
307+
if (camera instanceof OrthographicCamera) {
286308
camera.left = size.width / -2
287309
camera.right = size.width / 2
288310
camera.top = size.height / 2
@@ -301,14 +323,15 @@ const ChildrenToRenderTarget = forwardRef<
301323
return store.subscribe(update)
302324
}, [store])
303325

304-
useImperativeHandle(ref, () => renderTargetRef.current!, [])
305-
306326
let oldAutoClear
307327
let oldXrEnabled
308328
let oldIsPresenting
309329
let oldRenderTarget
310330
useFrame(({ gl, scene, camera }, _delta, frame: XRFrame | undefined) => {
311-
if (renderTargetRef.current == null || (layerEntryRef != null && layerEntryRef.current == null) || frame == null) {
331+
if (
332+
renderTargetRef.current == null ||
333+
(layerEntryRef != null && (layerEntryRef.current == null || frame == null))
334+
) {
312335
return
313336
}
314337
oldAutoClear = gl.autoClear
@@ -319,7 +342,7 @@ const ChildrenToRenderTarget = forwardRef<
319342
gl.xr.enabled = false
320343
gl.xr.isPresenting = false
321344
const renderTarget = renderTargetRef.current
322-
if (layerEntryRef?.current != null) {
345+
if (layerEntryRef?.current != null && frame != null) {
323346
const subImage = gl.xr.getBinding().getSubImage(layerEntryRef.current.layer, frame)
324347
gl.setRenderTargetTextures(renderTarget, subImage.colorTexture)
325348
}
@@ -331,4 +354,4 @@ const ChildrenToRenderTarget = forwardRef<
331354
gl.xr.isPresenting = oldIsPresenting
332355
}, renderPriority)
333356
return <>{children}</>
334-
})
357+
}

0 commit comments

Comments
 (0)