Skip to content

Commit f96bbb9

Browse files
committed
Set up dedicated hook for offscreen baking workflow
1 parent dba18af commit f96bbb9

File tree

2 files changed

+104
-84
lines changed

2 files changed

+104
-84
lines changed

src/core/Lightmap.tsx

Lines changed: 48 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,12 @@ import * as THREE from 'three';
99
import { materialIsSupported } from './lightScene';
1010
import {
1111
traverseSceneItems,
12-
Workbench,
1312
WorkbenchSettings,
1413
LIGHTMAP_READONLY_FLAG,
1514
LIGHTMAP_IGNORE_FLAG
1615
} from './workbench';
1716
import { computeAutoUV2Layout } from './AutoUV2';
18-
import { runOffscreenWorkflow } from './offscreenWorkflow';
17+
import { useOffscreenWorkflow, Debug } from './offscreenWorkflow';
1918

2019
// prevent lightmap and UV2 generation for content
2120
// (but still allow contribution to lightmap, for e.g. emissive objects, large occluders, etc)
@@ -103,93 +102,64 @@ export type LightmapProps = WorkbenchSettings & {
103102
const Lightmap: React.FC<React.PropsWithChildren<LightmapProps>> = ({
104103
disabled,
105104
onComplete,
106-
...props
105+
children,
106+
...settings
107107
}) => {
108-
const initialPropsRef = useRef(props);
109-
110108
// track latest reference to onComplete callback
111109
const onCompleteRef = useRef(onComplete);
112110
onCompleteRef.current = onComplete;
113111

114-
// track one-time flip from disabled to non-disabled
115-
// (i.e. once allowStart is true, keep it true)
116-
const disabledStartRef = useRef(true);
117-
disabledStartRef.current = disabledStartRef.current && !!disabled;
118-
const allowStart = !disabledStartRef.current;
119-
120-
const [result, setResult] = useState<Workbench | null>(null);
112+
// debug helper
121113
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+
);
122124

123-
useLayoutEffect(() => {
124-
// @todo check if this runs multiple times on some React versions???
125-
const { children, ...settings } = initialPropsRef.current;
126-
127-
// set up abort signal promise
128-
let abortResolver = () => undefined as void;
129-
const abortPromise = new Promise<void>((resolve) => {
130-
abortResolver = resolve;
131-
});
132-
133-
// run main logic with the abort signal promise
134-
if (allowStart) {
135-
const workflowResult = runOffscreenWorkflow(
136-
children,
137-
settings,
138-
abortPromise,
139-
{
140-
onAtlasMap(atlasMap) {
141-
// initialize debug display of atlas texture as well as blank placeholder for output
142-
const atlasTexture = new THREE.DataTexture(
143-
atlasMap.data,
144-
atlasMap.width,
145-
atlasMap.height,
146-
THREE.RGBAFormat,
147-
THREE.FloatType
148-
);
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+
);
149132

150-
const outputTexture = new THREE.DataTexture(
151-
new Float32Array(atlasMap.width * atlasMap.height * 4),
152-
atlasMap.width,
153-
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,
154149
THREE.RGBAFormat,
155150
THREE.FloatType
156-
);
157-
158-
setDebugInfo({
159-
atlasTexture,
160-
outputTexture
161-
});
162-
},
163-
onPassComplete(data, width, height) {
164-
setDebugInfo(
165-
(prev) =>
166-
prev && {
167-
...prev,
168-
169-
// replace with a new texture with copied source buffer data
170-
outputTexture: new THREE.DataTexture(
171-
new Float32Array(data),
172-
width,
173-
height,
174-
THREE.RGBAFormat,
175-
THREE.FloatType
176-
)
177-
}
178-
);
151+
)
179152
}
180-
}
181153
);
182-
183-
workflowResult.then((result) => {
184-
setResult(result);
185-
});
186154
}
155+
};
187156

188-
// on early unmount, resolve the abort signal promise
189-
return () => {
190-
abortResolver();
191-
};
192-
}, [allowStart]);
157+
// main offscreen workflow state
158+
const result = useOffscreenWorkflow(
159+
disabled ? null : children,
160+
settings,
161+
debug
162+
);
193163

194164
const sceneRef = useRef<THREE.Scene>(null);
195165

@@ -201,7 +171,7 @@ const Lightmap: React.FC<React.PropsWithChildren<LightmapProps>> = ({
201171
// create UV2 coordinates for the final scene meshes
202172
// @todo somehow reuse ones from the baker?
203173
computeAutoUV2Layout(
204-
initialPropsRef.current.lightMapSize,
174+
[result.atlasMap.width, result.atlasMap.height],
205175
traverseSceneItems(sceneRef.current, true),
206176
{
207177
texelsPerUnit: result.texelsPerUnit
@@ -211,11 +181,7 @@ const Lightmap: React.FC<React.PropsWithChildren<LightmapProps>> = ({
211181
// copy texture data since this is coming from a foreign canvas
212182
const texture = result.createOutputTexture();
213183

214-
updateFinalSceneMaterials(
215-
sceneRef.current,
216-
texture,
217-
!!initialPropsRef.current.ao
218-
);
184+
updateFinalSceneMaterials(sceneRef.current, texture, result.aoMode);
219185

220186
// notify listener and pass the texture instance intended for parent GL context
221187
if (onCompleteRef.current) {
@@ -229,7 +195,7 @@ const Lightmap: React.FC<React.PropsWithChildren<LightmapProps>> = ({
229195
<DebugContext.Provider value={debugInfo}>
230196
{result ? (
231197
<scene name="Lightmap Result Scene" ref={sceneRef}>
232-
{props.children}
198+
{children}
233199
</scene>
234200
) : null}
235201
</DebugContext.Provider>

src/core/offscreenWorkflow.tsx

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Licensed under the MIT license
44
*/
55

6-
import React, { useLayoutEffect, useRef } from 'react';
6+
import React, { useState, useLayoutEffect, useRef } from 'react';
77
import { useThree, createRoot } from '@react-three/fiber';
88
import * as THREE from 'three';
99

@@ -52,7 +52,7 @@ export interface Debug {
5252
}
5353

5454
// main async workflow, allows early cancellation via abortPromise
55-
export async function runOffscreenWorkflow(
55+
async function runOffscreenWorkflow(
5656
content: React.ReactNode,
5757
settings: OffscreenSettings,
5858
abortPromise: Promise<void>,
@@ -113,3 +113,57 @@ export async function runOffscreenWorkflow(
113113

114114
return workbench;
115115
}
116+
117+
// hook lifecycle for offscreen workflow
118+
export function useOffscreenWorkflow(
119+
content: React.ReactNode | null | undefined,
120+
settings?: OffscreenSettings,
121+
debugListeners?: Debug
122+
) {
123+
// track the first reference to non-empty content
124+
const initialUsefulContentRef = useRef(content);
125+
initialUsefulContentRef.current = initialUsefulContentRef.current || content;
126+
127+
// wrap latest value in ref to avoid triggering effect
128+
const settingsRef = useRef(settings);
129+
settingsRef.current = settings;
130+
131+
const debugRef = useRef(debugListeners);
132+
debugRef.current = debugListeners;
133+
134+
const [result, setResult] = useState<Workbench | null>(null);
135+
136+
useLayoutEffect(() => {
137+
// @todo check if this runs multiple times on some React versions???
138+
const children = initialUsefulContentRef.current;
139+
const settings = settingsRef.current;
140+
141+
// set up abort signal promise
142+
let abortResolver = () => undefined as void;
143+
const abortPromise = new Promise<void>((resolve) => {
144+
abortResolver = resolve;
145+
});
146+
147+
// run main logic with the abort signal promise
148+
if (children) {
149+
const workflowResult = runOffscreenWorkflow(
150+
children,
151+
settings ?? {},
152+
abortPromise,
153+
debugRef.current
154+
);
155+
156+
workflowResult.then((result) => {
157+
setResult(result);
158+
});
159+
}
160+
161+
// on early unmount, resolve the abort signal promise
162+
return () => {
163+
abortResolver();
164+
};
165+
}, [initialUsefulContentRef.current]);
166+
167+
// @todo clean up for direct consumption
168+
return result;
169+
}

0 commit comments

Comments
 (0)