Skip to content

Commit 49ceb48

Browse files
authored
Merge pull request #24 from pmndrs/offscreen-hook
Carry out offscreen workflow into separate hook
2 parents 04eeaa8 + f96bbb9 commit 49ceb48

File tree

2 files changed

+218
-186
lines changed

2 files changed

+218
-186
lines changed

src/core/Lightmap.tsx

+49-186
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,17 @@
44
*/
55

66
import React, { useState, useLayoutEffect, useRef } from 'react';
7-
import { useThree, createRoot } from '@react-three/fiber';
87
import * as THREE from 'three';
98

10-
import { withLightScene, materialIsSupported } from './lightScene';
9+
import { materialIsSupported } from './lightScene';
1110
import {
12-
initializeWorkbench,
1311
traverseSceneItems,
14-
Workbench,
1512
WorkbenchSettings,
1613
LIGHTMAP_READONLY_FLAG,
1714
LIGHTMAP_IGNORE_FLAG
1815
} from './workbench';
19-
import { runBakingPasses } from './bake';
2016
import { computeAutoUV2Layout } from './AutoUV2';
21-
import { createWorkManager } from './WorkManager';
17+
import { useOffscreenWorkflow, Debug } from './offscreenWorkflow';
2218

2319
// prevent lightmap and UV2 generation for content
2420
// (but still allow contribution to lightmap, for e.g. emissive objects, large occluders, etc)
@@ -97,106 +93,6 @@ function updateFinalSceneMaterials(
9793
}
9894
}
9995

100-
const WorkSceneWrapper: React.FC<{
101-
onReady: (gl: THREE.WebGLRenderer, scene: THREE.Scene) => void;
102-
children: React.ReactNode;
103-
}> = (props) => {
104-
const { gl } = useThree(); // @todo use state selector
105-
106-
// track latest reference to onReady callback
107-
const onReadyRef = useRef(props.onReady);
108-
onReadyRef.current = props.onReady;
109-
110-
const sceneRef = useRef<THREE.Scene>(null);
111-
useLayoutEffect(() => {
112-
// kick off the asynchronous workflow process in the parent
113-
// (this runs when scene content is loaded and suspensions are finished)
114-
const scene = sceneRef.current;
115-
if (!scene) {
116-
throw new Error('expecting lightmap scene');
117-
}
118-
119-
onReadyRef.current(gl, scene);
120-
}, [gl]);
121-
122-
// main baking scene container
123-
return (
124-
<scene name="Lightmap Baking Scene" ref={sceneRef}>
125-
{props.children}
126-
</scene>
127-
);
128-
};
129-
130-
type OffscreenSettings = WorkbenchSettings & {
131-
workPerFrame?: number; // @todo allow fractions, dynamic value
132-
};
133-
134-
// main async workflow, allows early cancellation via abortPromise
135-
async function runOffscreenWorkflow(
136-
content: React.ReactNode,
137-
settings: OffscreenSettings,
138-
abortPromise: Promise<void>,
139-
debugListeners: {
140-
onAtlasMap: (atlasMap: Workbench['atlasMap']) => void;
141-
onPassComplete: (data: Float32Array, width: number, height: number) => void;
142-
}
143-
) {
144-
// render hidden canvas with the given content, wait for suspense to finish loading inside it
145-
const scenePromise = await new Promise<{
146-
gl: THREE.WebGLRenderer;
147-
scene: THREE.Scene;
148-
}>((resolve) => {
149-
// just sensible small canvas, not actually used for direct output
150-
const canvas = document.createElement('canvas');
151-
canvas.width = 64;
152-
canvas.height = 64;
153-
154-
const root = createRoot(canvas).configure({
155-
frameloop: 'never', // framebuffer target rendering is inside own RAF loop
156-
shadows: true // @todo use the original GL context settings
157-
});
158-
159-
root.render(
160-
<React.Suspense fallback={null}>
161-
<WorkSceneWrapper
162-
onReady={(gl, scene) => {
163-
resolve({ gl, scene });
164-
}}
165-
>
166-
{content}
167-
</WorkSceneWrapper>
168-
</React.Suspense>
169-
);
170-
});
171-
172-
// preempt any further logic if already aborted
173-
const { gl, scene } = await Promise.race([
174-
scenePromise,
175-
abortPromise.then(() => {
176-
throw new Error('aborted before scene is complete');
177-
})
178-
]);
179-
180-
// our own work manager (which is aware of the abort signal promise)
181-
const requestWork = createWorkManager(
182-
gl,
183-
abortPromise,
184-
settings.workPerFrame
185-
);
186-
187-
const workbench = await initializeWorkbench(scene, settings, requestWork);
188-
debugListeners.onAtlasMap(workbench.atlasMap); // expose atlas map for debugging
189-
190-
await withLightScene(workbench, async () => {
191-
await runBakingPasses(workbench, requestWork, (data, width, height) => {
192-
// expose current pass output for debugging
193-
debugListeners.onPassComplete(data, width, height);
194-
});
195-
});
196-
197-
return workbench;
198-
}
199-
20096
export type LightmapProps = WorkbenchSettings & {
20197
workPerFrame?: number; // @todo allow fractions, dynamic value
20298
disabled?: boolean;
@@ -206,93 +102,64 @@ export type LightmapProps = WorkbenchSettings & {
206102
const Lightmap: React.FC<React.PropsWithChildren<LightmapProps>> = ({
207103
disabled,
208104
onComplete,
209-
...props
105+
children,
106+
...settings
210107
}) => {
211-
const initialPropsRef = useRef(props);
212-
213108
// track latest reference to onComplete callback
214109
const onCompleteRef = useRef(onComplete);
215110
onCompleteRef.current = onComplete;
216111

217-
// track one-time flip from disabled to non-disabled
218-
// (i.e. once allowStart is true, keep it true)
219-
const disabledStartRef = useRef(true);
220-
disabledStartRef.current = disabledStartRef.current && !!disabled;
221-
const allowStart = !disabledStartRef.current;
222-
223-
const [result, setResult] = useState<Workbench | null>(null);
112+
// debug helper
224113
const [debugInfo, setDebugInfo] = useState<DebugInfo | null>(null);
114+
const debug: Debug = {
115+
onAtlasMap(atlasMap) {
116+
// initialize debug display of atlas texture as well as blank placeholder for output
117+
const atlasTexture = new THREE.DataTexture(
118+
atlasMap.data,
119+
atlasMap.width,
120+
atlasMap.height,
121+
THREE.RGBAFormat,
122+
THREE.FloatType
123+
);
225124

226-
useLayoutEffect(() => {
227-
// @todo check if this runs multiple times on some React versions???
228-
const { children, ...settings } = initialPropsRef.current;
229-
230-
// set up abort signal promise
231-
let abortResolver = () => undefined as void;
232-
const abortPromise = new Promise<void>((resolve) => {
233-
abortResolver = resolve;
234-
});
235-
236-
// run main logic with the abort signal promise
237-
if (allowStart) {
238-
const workflowResult = runOffscreenWorkflow(
239-
children,
240-
settings,
241-
abortPromise,
242-
{
243-
onAtlasMap(atlasMap) {
244-
// initialize debug display of atlas texture as well as blank placeholder for output
245-
const atlasTexture = new THREE.DataTexture(
246-
atlasMap.data,
247-
atlasMap.width,
248-
atlasMap.height,
249-
THREE.RGBAFormat,
250-
THREE.FloatType
251-
);
125+
const outputTexture = new THREE.DataTexture(
126+
new Float32Array(atlasMap.width * atlasMap.height * 4),
127+
atlasMap.width,
128+
atlasMap.height,
129+
THREE.RGBAFormat,
130+
THREE.FloatType
131+
);
252132

253-
const outputTexture = new THREE.DataTexture(
254-
new Float32Array(atlasMap.width * atlasMap.height * 4),
255-
atlasMap.width,
256-
atlasMap.height,
133+
setDebugInfo({
134+
atlasTexture,
135+
outputTexture
136+
});
137+
},
138+
onPassComplete(data, width, height) {
139+
setDebugInfo(
140+
(prev) =>
141+
prev && {
142+
...prev,
143+
144+
// replace with a new texture with copied source buffer data
145+
outputTexture: new THREE.DataTexture(
146+
new Float32Array(data),
147+
width,
148+
height,
257149
THREE.RGBAFormat,
258150
THREE.FloatType
259-
);
260-
261-
setDebugInfo({
262-
atlasTexture,
263-
outputTexture
264-
});
265-
},
266-
onPassComplete(data, width, height) {
267-
setDebugInfo(
268-
(prev) =>
269-
prev && {
270-
...prev,
271-
272-
// replace with a new texture with copied source buffer data
273-
outputTexture: new THREE.DataTexture(
274-
new Float32Array(data),
275-
width,
276-
height,
277-
THREE.RGBAFormat,
278-
THREE.FloatType
279-
)
280-
}
281-
);
151+
)
282152
}
283-
}
284153
);
285-
286-
workflowResult.then((result) => {
287-
setResult(result);
288-
});
289154
}
155+
};
290156

291-
// on early unmount, resolve the abort signal promise
292-
return () => {
293-
abortResolver();
294-
};
295-
}, [allowStart]);
157+
// main offscreen workflow state
158+
const result = useOffscreenWorkflow(
159+
disabled ? null : children,
160+
settings,
161+
debug
162+
);
296163

297164
const sceneRef = useRef<THREE.Scene>(null);
298165

@@ -304,7 +171,7 @@ const Lightmap: React.FC<React.PropsWithChildren<LightmapProps>> = ({
304171
// create UV2 coordinates for the final scene meshes
305172
// @todo somehow reuse ones from the baker?
306173
computeAutoUV2Layout(
307-
initialPropsRef.current.lightMapSize,
174+
[result.atlasMap.width, result.atlasMap.height],
308175
traverseSceneItems(sceneRef.current, true),
309176
{
310177
texelsPerUnit: result.texelsPerUnit
@@ -314,11 +181,7 @@ const Lightmap: React.FC<React.PropsWithChildren<LightmapProps>> = ({
314181
// copy texture data since this is coming from a foreign canvas
315182
const texture = result.createOutputTexture();
316183

317-
updateFinalSceneMaterials(
318-
sceneRef.current,
319-
texture,
320-
!!initialPropsRef.current.ao
321-
);
184+
updateFinalSceneMaterials(sceneRef.current, texture, result.aoMode);
322185

323186
// notify listener and pass the texture instance intended for parent GL context
324187
if (onCompleteRef.current) {
@@ -332,7 +195,7 @@ const Lightmap: React.FC<React.PropsWithChildren<LightmapProps>> = ({
332195
<DebugContext.Provider value={debugInfo}>
333196
{result ? (
334197
<scene name="Lightmap Result Scene" ref={sceneRef}>
335-
{props.children}
198+
{children}
336199
</scene>
337200
) : null}
338201
</DebugContext.Provider>

0 commit comments

Comments
 (0)