Skip to content

Commit 662a7b0

Browse files
committed
wip render target layer
1 parent 0c2591a commit 662a7b0

File tree

1 file changed

+334
-0
lines changed

1 file changed

+334
-0
lines changed
Lines changed: 334 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,334 @@
1+
import {
2+
context,
3+
InjectState,
4+
MeshProps,
5+
reconciler,
6+
RootState,
7+
useFrame,
8+
useStore,
9+
useThree,
10+
} from '@react-three/fiber'
11+
import { forwardRef, ReactNode, RefObject, useCallback, useEffect, useImperativeHandle, useMemo, useRef } from 'react'
12+
import {
13+
Mesh,
14+
MeshBasicMaterial,
15+
Object3D,
16+
PerspectiveCamera,
17+
Raycaster,
18+
Scene,
19+
Vector2,
20+
Vector3,
21+
WebGLRenderTarget,
22+
} from 'three'
23+
import { useXRLayer } from './layer.js'
24+
import { createXRLayer, XRLayerEntry, XRLayerOptions, XRLayerProperties, XRLayerShape } from '@pmndrs/xr'
25+
import { XRStore } from '../xr.js'
26+
import { isOrthographicCamera } from '@react-three/fiber/dist/declarations/src/core/utils.js'
27+
import { create, StoreApi, UseBoundStore } from 'zustand'
28+
import { forwardObjectEvents } from '@pmndrs/pointer-events'
29+
30+
declare module 'three' {
31+
export interface WebGLRenderer {
32+
setRenderTargetTextures(
33+
renderTarget: WebGLRenderTarget,
34+
colorTexture: WebGLTexture,
35+
depthTexture?: WebGLTexture,
36+
): void
37+
}
38+
}
39+
40+
export type XRRenderTargetLayerProperties = {
41+
children?: ReactNode
42+
shape?: XRLayerShape
43+
renderPriority?: number
44+
pixelWidth: number
45+
pixelHeight: number
46+
} & Omit<XRLayerOptions, 'textureType' | 'isStatic' | 'layout'> &
47+
XRLayerProperties &
48+
MeshProps
49+
50+
export function NonXRRenderTargetLayer({
51+
renderOrder,
52+
pixelWidth,
53+
pixelHeight,
54+
children,
55+
renderPriority = 0,
56+
...props
57+
}: XRRenderTargetLayerProperties) {
58+
const renderTargetRef = useRef<WebGLRenderTarget>(null)
59+
const ref = useRef<Mesh>(null)
60+
const materialRef = useRef<MeshBasicMaterial>(null)
61+
useEffect(() => {
62+
if (materialRef.current == null || renderTargetRef.current == null) {
63+
return
64+
}
65+
materialRef.current.map = renderTargetRef.current.texture
66+
}, [])
67+
68+
const layerStore = useLayerStore(pixelWidth, pixelHeight)
69+
useForwardEvents(layerStore, ref)
70+
return (
71+
<>
72+
{reconciler.createPortal(
73+
<context.Provider value={layerStore}>
74+
<ChildrenToRenderTarget ref={renderTargetRef} renderPriority={renderPriority}>
75+
{children}
76+
</ChildrenToRenderTarget>
77+
</context.Provider>,
78+
layerStore,
79+
null,
80+
)}
81+
<mesh {...props}>
82+
<meshBasicMaterial toneMapped={false} />
83+
</mesh>
84+
</>
85+
)
86+
}
87+
88+
export function XRRenderTargetLayer({
89+
pixelWidth,
90+
pixelHeight,
91+
renderOrder = 0,
92+
shape,
93+
centralAngle,
94+
centralHorizontalAngle,
95+
upperVerticalAngle,
96+
lowerVerticalAngle,
97+
colorFormat,
98+
depthFormat,
99+
mipLevels,
100+
blendTextureSourceAlpha,
101+
chromaticAberrationCorrection,
102+
quality,
103+
renderPriority = 0,
104+
children,
105+
...props
106+
}: XRRenderTargetLayerProperties) {
107+
const ref = useRef<Mesh>(null)
108+
const renderer = useThree((s) => s.gl)
109+
const createLayer = useCallback(
110+
async (store: XRStore, object: Object3D, properties: XRLayerProperties) =>
111+
createXRLayer(
112+
store.getState(),
113+
renderer.xr,
114+
shape ?? 'quad',
115+
object,
116+
pixelWidth,
117+
pixelHeight,
118+
{ colorFormat, depthFormat, isStatic: false, mipLevels, textureType: 'texture' },
119+
properties,
120+
),
121+
[colorFormat, depthFormat, mipLevels, pixelHeight, pixelWidth, renderer.xr, shape],
122+
)
123+
const layerEntryRef = useXRLayer(ref, renderOrder, createLayer, {
124+
blendTextureSourceAlpha,
125+
centralAngle,
126+
centralHorizontalAngle,
127+
chromaticAberrationCorrection,
128+
lowerVerticalAngle,
129+
quality,
130+
upperVerticalAngle,
131+
})
132+
const layerStore = useLayerStore(pixelWidth, pixelHeight)
133+
useForwardEvents(layerStore, ref)
134+
return (
135+
<>
136+
{reconciler.createPortal(
137+
<context.Provider value={layerStore}>
138+
<ChildrenToRenderTarget renderPriority={renderPriority} layerEntryRef={layerEntryRef}>
139+
{children}
140+
</ChildrenToRenderTarget>
141+
</context.Provider>,
142+
layerStore,
143+
null,
144+
)}
145+
<mesh {...props} renderOrder={-Infinity} ref={ref}>
146+
<meshBasicMaterial colorWrite={false} />
147+
</mesh>
148+
</>
149+
)
150+
}
151+
152+
function useForwardEvents(layerStore: UseBoundStore<StoreApi<RootState>>, ref: RefObject<Mesh>) {
153+
useEffect(() => {
154+
const { current } = ref
155+
if (current == null) {
156+
return
157+
}
158+
let cleanup: (() => void) | undefined
159+
const update = (state: RootState, prevState?: RootState) => {
160+
if (state.camera === prevState?.camera && state.scene === prevState.scene) {
161+
return
162+
}
163+
cleanup?.()
164+
cleanup = forwardObjectEvents(current, state.camera, state.scene)
165+
}
166+
update(layerStore.getState())
167+
const unsubscribe = layerStore.subscribe(update)
168+
return () => {
169+
unsubscribe()
170+
cleanup?.()
171+
}
172+
}, [layerStore, ref])
173+
}
174+
175+
// Keys that shouldn't be copied between R3F stores
176+
export const privateKeys = [
177+
'set',
178+
'get',
179+
'setSize',
180+
'setFrameloop',
181+
'setDpr',
182+
'events',
183+
'invalidate',
184+
'advance',
185+
'size',
186+
'viewport',
187+
]
188+
189+
export function useLayerStore(width: number, height: number) {
190+
const previousRoot = useStore()
191+
const layerStore = useMemo(() => {
192+
let previousState = previousRoot.getState()
193+
// We have our own camera in here, separate from the main scene.
194+
const camera = new PerspectiveCamera(50, 1, 0.1, 1000)
195+
camera.position.set(0, 0, 5)
196+
const pointer = new Vector2()
197+
let ownState: Partial<RootState> = {
198+
events: { enabled: false, priority: 0 },
199+
size: { width: 1, height: 1, left: 0, top: 0 },
200+
camera,
201+
scene: new Scene(),
202+
raycaster: new Raycaster(),
203+
pointer: pointer,
204+
mouse: pointer,
205+
previousRoot,
206+
}
207+
//we now merge in order previousState, injectState, ownState
208+
const store = create<RootState & { setPreviousState: (prevState: RootState) => void }>((innerSet, get) => {
209+
const merge = () => {
210+
const result = {} as any
211+
for (const key in previousState) {
212+
if (privateKeys.includes(key)) {
213+
continue
214+
}
215+
result[key as keyof RootState] = previousState[key as keyof RootState] as never
216+
}
217+
return Object.assign(result, ownState, {
218+
events: { ...previousState.events, ...ownState.events },
219+
viewport: Object.assign(
220+
{},
221+
previousState.viewport,
222+
previousState.viewport.getCurrentViewport(camera, new Vector3(), ownState.size),
223+
),
224+
})
225+
}
226+
const update = () => innerSet(merge())
227+
return {
228+
...previousState,
229+
// Set and get refer to this root-state
230+
set(newOwnState: Partial<InjectState> | ((s: InjectState) => Partial<InjectState>)) {
231+
if (typeof newOwnState === 'function') {
232+
newOwnState = newOwnState(get())
233+
}
234+
Object.assign(ownState, newOwnState)
235+
update()
236+
},
237+
setPreviousState(prevState: RootState) {
238+
previousState = prevState
239+
update()
240+
},
241+
get,
242+
setEvents() {},
243+
...merge(),
244+
}
245+
})
246+
return Object.assign(store, {
247+
setState(state: Partial<RootState>) {
248+
store.getState().set(state as any)
249+
},
250+
})
251+
}, [previousRoot])
252+
//syncing up previous store with the current store
253+
useEffect(() => previousRoot.subscribe(layerStore.getState().setPreviousState), [previousRoot, layerStore])
254+
useEffect(
255+
() =>
256+
layerStore.setState({
257+
size: { width, height, top: 0, left: 0 },
258+
viewport: { ...previousRoot.getState().viewport, width, height, aspect: width / height },
259+
}),
260+
[width, height, layerStore, previousRoot],
261+
)
262+
return layerStore
263+
}
264+
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+
275+
const renderTargetRef = useRef<WebGLRenderTarget | undefined>(undefined)
276+
useEffect(() => {
277+
const renderTarget = (renderTargetRef.current = new WebGLRenderTarget(1, 1, {}))
278+
return () => renderTarget.dispose()
279+
}, [])
280+
281+
useEffect(() => {
282+
const update = (state: RootState, prevState?: RootState) => {
283+
const { size, camera } = state
284+
renderTargetRef.current?.setSize(size.width, size.height)
285+
if (isOrthographicCamera(camera)) {
286+
camera.left = size.width / -2
287+
camera.right = size.width / 2
288+
camera.top = size.height / 2
289+
camera.bottom = size.height / -2
290+
} else {
291+
camera.aspect = size.width / size.height
292+
}
293+
if (size !== prevState?.size || camera !== prevState.camera) {
294+
camera.updateProjectionMatrix()
295+
// https://github.com/pmndrs/react-three-fiber/issues/178
296+
// Update matrix world since the renderer is a frame late
297+
camera.updateMatrixWorld()
298+
}
299+
}
300+
update(store.getState())
301+
return store.subscribe(update)
302+
}, [store])
303+
304+
useImperativeHandle(ref, () => renderTargetRef.current!, [])
305+
306+
let oldAutoClear
307+
let oldXrEnabled
308+
let oldIsPresenting
309+
let oldRenderTarget
310+
useFrame(({ gl, scene, camera }, _delta, frame: XRFrame | undefined) => {
311+
if (renderTargetRef.current == null || (layerEntryRef != null && layerEntryRef.current == null) || frame == null) {
312+
return
313+
}
314+
oldAutoClear = gl.autoClear
315+
oldXrEnabled = gl.xr.enabled
316+
oldIsPresenting = gl.xr.isPresenting
317+
oldRenderTarget = gl.getRenderTarget()
318+
gl.autoClear = true
319+
gl.xr.enabled = false
320+
gl.xr.isPresenting = false
321+
const renderTarget = renderTargetRef.current
322+
if (layerEntryRef?.current != null) {
323+
const subImage = gl.xr.getBinding().getSubImage(layerEntryRef.current.layer, frame)
324+
gl.setRenderTargetTextures(renderTarget, subImage.colorTexture)
325+
}
326+
gl.setRenderTarget(renderTarget)
327+
gl.render(scene, camera)
328+
gl.setRenderTarget(oldRenderTarget)
329+
gl.autoClear = oldAutoClear
330+
gl.xr.enabled = oldXrEnabled
331+
gl.xr.isPresenting = oldIsPresenting
332+
}, renderPriority)
333+
return <>{children}</>
334+
})

0 commit comments

Comments
 (0)