@hello-terrain/react 0.0.0-alpha.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +85 -0
- package/dist/index.cjs +603 -0
- package/dist/index.d.cts +63 -0
- package/dist/index.d.mts +63 -0
- package/dist/index.d.ts +63 -0
- package/dist/index.mjs +598 -0
- package/package.json +57 -0
package/README.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# @hello-terrain/react
|
|
2
|
+
|
|
3
|
+
React bindings for Hello Terrain, built for [react-three/fiber](https://r3f.docs.pmnd.rs/) and WebGPU.
|
|
4
|
+
|
|
5
|
+
`@hello-terrain/react` wraps the core terrain graph from `@hello-terrain/three` with a React-first API:
|
|
6
|
+
|
|
7
|
+
- `Terrain` renders the terrain mesh and connects node materials
|
|
8
|
+
- `useTerrain()` creates and owns a `TerrainHandle`
|
|
9
|
+
- `TerrainProvider` and `useTerrainContext()` share terrain state with sibling systems
|
|
10
|
+
- `terrain.ready` and `terrain.runtime` make it easier to coordinate rendering, queries, and raycasts
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pnpm add @hello-terrain/react @react-three/fiber three react react-dom
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`@hello-terrain/react` depends on WebGPU and expects a `three/webgpu` renderer.
|
|
19
|
+
|
|
20
|
+
## Basic Example
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
import { Terrain } from "@hello-terrain/react";
|
|
24
|
+
import { Canvas } from "@react-three/fiber";
|
|
25
|
+
import type { WebGPURendererParameters } from "three/src/renderers/webgpu/WebGPURenderer.js";
|
|
26
|
+
import { float } from "three/tsl";
|
|
27
|
+
import * as THREE from "three/webgpu";
|
|
28
|
+
|
|
29
|
+
const elevation = () => float(0);
|
|
30
|
+
|
|
31
|
+
export function App() {
|
|
32
|
+
return (
|
|
33
|
+
<Canvas
|
|
34
|
+
gl={async (props) => {
|
|
35
|
+
const renderer = new THREE.WebGPURenderer(
|
|
36
|
+
props as WebGPURendererParameters,
|
|
37
|
+
);
|
|
38
|
+
await renderer.init();
|
|
39
|
+
return renderer;
|
|
40
|
+
}}
|
|
41
|
+
camera={{ position: [0, 30, 60] }}
|
|
42
|
+
>
|
|
43
|
+
<ambientLight intensity={0.15} />
|
|
44
|
+
<directionalLight intensity={1} position={[1, 1, 1]} />
|
|
45
|
+
|
|
46
|
+
<Terrain rootSize={1024} maxLevel={6} elevation={elevation}>
|
|
47
|
+
{({ positionNode }) => (
|
|
48
|
+
<meshStandardNodeMaterial positionNode={positionNode} />
|
|
49
|
+
)}
|
|
50
|
+
</Terrain>
|
|
51
|
+
</Canvas>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## When To Use `useTerrain()`
|
|
57
|
+
|
|
58
|
+
Use `Terrain` by itself for the smallest setup. Reach for `useTerrain()` when you want to:
|
|
59
|
+
|
|
60
|
+
- reuse the same terrain handle across multiple components
|
|
61
|
+
- access `terrain.runtime.query` or `terrain.runtime.raycast`
|
|
62
|
+
- share terrain state with sibling systems like controllers or cameras
|
|
63
|
+
- inspect the underlying graph or provide custom graph tasks
|
|
64
|
+
|
|
65
|
+
## Public API
|
|
66
|
+
|
|
67
|
+
- `Terrain`
|
|
68
|
+
- `useTerrain`
|
|
69
|
+
- `TerrainProvider`
|
|
70
|
+
- `useTerrainContext`
|
|
71
|
+
- `TerrainHandle`
|
|
72
|
+
- `TerrainOptions`
|
|
73
|
+
- `TerrainRuntime`
|
|
74
|
+
- `TerrainTask`
|
|
75
|
+
|
|
76
|
+
## Docs
|
|
77
|
+
|
|
78
|
+
- [Introduction](http://hello-terrain.kenny.wtf/docs)
|
|
79
|
+
- [Installation](http://hello-terrain.kenny.wtf/docs/installation)
|
|
80
|
+
- [React Overview](http://hello-terrain.kenny.wtf/docs/react)
|
|
81
|
+
- [Examples](http://hello-terrain.kenny.wtf/examples)
|
|
82
|
+
|
|
83
|
+
## Support
|
|
84
|
+
|
|
85
|
+
For support, join the [Discord server](https://discord.gg/HgTd2B828n).
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const jsxRuntime = require('react/jsx-runtime');
|
|
4
|
+
const three = require('@hello-terrain/three');
|
|
5
|
+
const fiber = require('@react-three/fiber');
|
|
6
|
+
const react = require('react');
|
|
7
|
+
const work = require('@hello-terrain/work');
|
|
8
|
+
const three$1 = require('three');
|
|
9
|
+
|
|
10
|
+
const TerrainContext = react.createContext(null);
|
|
11
|
+
function TerrainProvider({ value, children }) {
|
|
12
|
+
return /* @__PURE__ */ jsxRuntime.jsx(TerrainContext.Provider, { value, children });
|
|
13
|
+
}
|
|
14
|
+
function useTerrainContext() {
|
|
15
|
+
const value = react.useContext(TerrainContext);
|
|
16
|
+
if (!value) {
|
|
17
|
+
throw new Error("useTerrainContext must be used within a TerrainProvider");
|
|
18
|
+
}
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function createSyncTerrainRuntimeTask(runtime) {
|
|
23
|
+
return work.task((get, work) => {
|
|
24
|
+
const query = get(three.terrainTasks.terrainQuery).query;
|
|
25
|
+
const raycast = get(three.terrainTasks.terrainRaycast);
|
|
26
|
+
return work(() => {
|
|
27
|
+
runtime.query = query;
|
|
28
|
+
runtime.raycast = raycast;
|
|
29
|
+
return runtime;
|
|
30
|
+
});
|
|
31
|
+
}).displayName("syncTerrainRuntimeTask").cache("none");
|
|
32
|
+
}
|
|
33
|
+
function createSyncTerrainNodesTask(getTerrainNodes, setTerrainNodes, getReady, setReady) {
|
|
34
|
+
return work.task((get, work) => {
|
|
35
|
+
const positionNode = get(three.terrainTasks.positionNode);
|
|
36
|
+
return work(() => {
|
|
37
|
+
const nextReady = positionNode != null;
|
|
38
|
+
const terrainNodes = getTerrainNodes();
|
|
39
|
+
if (terrainNodes.positionNode !== positionNode) {
|
|
40
|
+
setTerrainNodes({
|
|
41
|
+
positionNode
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
if (getReady() !== nextReady) {
|
|
45
|
+
setReady(nextReady);
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
positionNode,
|
|
49
|
+
ready: nextReady
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
}).displayName("syncTerrainNodesTask").cache("none");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function resetOrSet(graph, ownedParamIds, ref, value, setValue) {
|
|
56
|
+
if (value === void 0) {
|
|
57
|
+
if (ownedParamIds.has(ref.id)) {
|
|
58
|
+
graph.reset(ref);
|
|
59
|
+
ownedParamIds.delete(ref.id);
|
|
60
|
+
}
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
graph.set(ref, setValue);
|
|
64
|
+
ownedParamIds.add(ref.id);
|
|
65
|
+
}
|
|
66
|
+
function useTerrainParams(graph, options) {
|
|
67
|
+
const ownedParamIdsRef = react.useRef(/* @__PURE__ */ new Set());
|
|
68
|
+
const {
|
|
69
|
+
rootSize: nextRootSize,
|
|
70
|
+
origin: nextOrigin,
|
|
71
|
+
maxLevel: nextMaxLevel,
|
|
72
|
+
maxNodes: nextMaxNodes,
|
|
73
|
+
innerTileSegments: nextInnerTileSegments,
|
|
74
|
+
skirtScale: nextSkirtScale,
|
|
75
|
+
elevationScale: nextElevationScale,
|
|
76
|
+
elevation,
|
|
77
|
+
surface: nextSurface,
|
|
78
|
+
terrainFieldFilter: nextTerrainFieldFilter
|
|
79
|
+
} = options;
|
|
80
|
+
react.useLayoutEffect(() => {
|
|
81
|
+
ownedParamIdsRef.current.clear();
|
|
82
|
+
}, [graph]);
|
|
83
|
+
react.useLayoutEffect(() => {
|
|
84
|
+
const ownedParamIds = ownedParamIdsRef.current;
|
|
85
|
+
resetOrSet(
|
|
86
|
+
graph,
|
|
87
|
+
ownedParamIds,
|
|
88
|
+
three.rootSize,
|
|
89
|
+
nextRootSize,
|
|
90
|
+
() => nextRootSize
|
|
91
|
+
);
|
|
92
|
+
resetOrSet(graph, ownedParamIds, three.origin, nextOrigin, () => ({
|
|
93
|
+
x: nextOrigin?.x ?? 0,
|
|
94
|
+
y: nextOrigin?.y ?? 0,
|
|
95
|
+
z: nextOrigin?.z ?? 0
|
|
96
|
+
}));
|
|
97
|
+
resetOrSet(
|
|
98
|
+
graph,
|
|
99
|
+
ownedParamIds,
|
|
100
|
+
three.maxLevel,
|
|
101
|
+
nextMaxLevel,
|
|
102
|
+
() => nextMaxLevel
|
|
103
|
+
);
|
|
104
|
+
resetOrSet(
|
|
105
|
+
graph,
|
|
106
|
+
ownedParamIds,
|
|
107
|
+
three.maxNodes,
|
|
108
|
+
nextMaxNodes,
|
|
109
|
+
() => nextMaxNodes
|
|
110
|
+
);
|
|
111
|
+
resetOrSet(
|
|
112
|
+
graph,
|
|
113
|
+
ownedParamIds,
|
|
114
|
+
three.innerTileSegments,
|
|
115
|
+
nextInnerTileSegments,
|
|
116
|
+
() => nextInnerTileSegments
|
|
117
|
+
);
|
|
118
|
+
resetOrSet(
|
|
119
|
+
graph,
|
|
120
|
+
ownedParamIds,
|
|
121
|
+
three.skirtScale,
|
|
122
|
+
nextSkirtScale,
|
|
123
|
+
() => nextSkirtScale
|
|
124
|
+
);
|
|
125
|
+
resetOrSet(
|
|
126
|
+
graph,
|
|
127
|
+
ownedParamIds,
|
|
128
|
+
three.elevationScale,
|
|
129
|
+
nextElevationScale,
|
|
130
|
+
() => nextElevationScale
|
|
131
|
+
);
|
|
132
|
+
resetOrSet(
|
|
133
|
+
graph,
|
|
134
|
+
ownedParamIds,
|
|
135
|
+
three.surface,
|
|
136
|
+
nextSurface,
|
|
137
|
+
() => nextSurface
|
|
138
|
+
);
|
|
139
|
+
resetOrSet(
|
|
140
|
+
graph,
|
|
141
|
+
ownedParamIds,
|
|
142
|
+
three.terrainFieldFilter,
|
|
143
|
+
nextTerrainFieldFilter,
|
|
144
|
+
() => nextTerrainFieldFilter
|
|
145
|
+
);
|
|
146
|
+
if (elevation === void 0) {
|
|
147
|
+
if (ownedParamIds.has(three.elevationFn.id)) {
|
|
148
|
+
graph.reset(three.elevationFn);
|
|
149
|
+
ownedParamIds.delete(three.elevationFn.id);
|
|
150
|
+
}
|
|
151
|
+
} else {
|
|
152
|
+
graph.set(three.elevationFn, () => elevation);
|
|
153
|
+
ownedParamIds.add(three.elevationFn.id);
|
|
154
|
+
}
|
|
155
|
+
}, [
|
|
156
|
+
graph,
|
|
157
|
+
nextRootSize,
|
|
158
|
+
nextOrigin,
|
|
159
|
+
nextMaxLevel,
|
|
160
|
+
nextMaxNodes,
|
|
161
|
+
nextInnerTileSegments,
|
|
162
|
+
nextSkirtScale,
|
|
163
|
+
nextElevationScale,
|
|
164
|
+
elevation,
|
|
165
|
+
nextSurface,
|
|
166
|
+
nextTerrainFieldFilter
|
|
167
|
+
]);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const WEBGPU_RENDERER_ERROR = "@hello-terrain/react requires a WebGPURenderer on <Canvas gl={...}>.";
|
|
171
|
+
const GRAPH_RUN_ERROR = "@hello-terrain/react terrain graph run failed.";
|
|
172
|
+
function toVector3Like(state, getCameraOrigin) {
|
|
173
|
+
return getCameraOrigin?.(state) ?? state.camera.position;
|
|
174
|
+
}
|
|
175
|
+
function getTerrainRunnerErrorKey(error) {
|
|
176
|
+
if (error instanceof Error) {
|
|
177
|
+
return `${error.name}:${error.message}`;
|
|
178
|
+
}
|
|
179
|
+
return String(error);
|
|
180
|
+
}
|
|
181
|
+
function isWebGpuRenderer(renderer) {
|
|
182
|
+
return typeof renderer === "object" && renderer !== null && "backend" in renderer;
|
|
183
|
+
}
|
|
184
|
+
function useTerrainRunner({
|
|
185
|
+
graph,
|
|
186
|
+
targets,
|
|
187
|
+
getCameraOrigin,
|
|
188
|
+
cameraHysteresis = 0.05
|
|
189
|
+
}) {
|
|
190
|
+
const graphRef = react.useRef(graph);
|
|
191
|
+
const targetsRef = react.useRef(targets);
|
|
192
|
+
const getCameraOriginRef = react.useRef(getCameraOrigin);
|
|
193
|
+
const lastCameraOriginRef = react.useRef(null);
|
|
194
|
+
const runningRef = react.useRef(false);
|
|
195
|
+
const generationRef = react.useRef(0);
|
|
196
|
+
const runAbortControllerRef = react.useRef(null);
|
|
197
|
+
const runPromiseRef = react.useRef(null);
|
|
198
|
+
const lastErrorKeyRef = react.useRef(null);
|
|
199
|
+
const reportError = react.useCallback(
|
|
200
|
+
(error, errorKey) => {
|
|
201
|
+
const nextErrorKey = errorKey ?? getTerrainRunnerErrorKey(error);
|
|
202
|
+
if (lastErrorKeyRef.current === nextErrorKey) return;
|
|
203
|
+
lastErrorKeyRef.current = nextErrorKey;
|
|
204
|
+
console.error(error);
|
|
205
|
+
},
|
|
206
|
+
[]
|
|
207
|
+
);
|
|
208
|
+
const clearError = react.useCallback(() => {
|
|
209
|
+
lastErrorKeyRef.current = null;
|
|
210
|
+
}, []);
|
|
211
|
+
const stopCurrentRun = react.useCallback(async () => {
|
|
212
|
+
const activeController = runAbortControllerRef.current;
|
|
213
|
+
if (activeController && !activeController.signal.aborted) {
|
|
214
|
+
activeController.abort(new Error("Terrain runner stopped"));
|
|
215
|
+
}
|
|
216
|
+
const activeRun = runPromiseRef.current;
|
|
217
|
+
if (activeRun) {
|
|
218
|
+
await activeRun.catch(() => {
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}, []);
|
|
222
|
+
const updateCameraOrigin = react.useCallback(
|
|
223
|
+
(state, activeGraph) => {
|
|
224
|
+
const activeGetCameraOrigin = getCameraOriginRef.current;
|
|
225
|
+
const cameraOrigin = toVector3Like(state, activeGetCameraOrigin);
|
|
226
|
+
const nextOrigin = new three$1.Vector3(
|
|
227
|
+
cameraOrigin.x,
|
|
228
|
+
cameraOrigin.y,
|
|
229
|
+
cameraOrigin.z
|
|
230
|
+
);
|
|
231
|
+
const lastOrigin = lastCameraOriginRef.current;
|
|
232
|
+
const hysteresisSq = cameraHysteresis * cameraHysteresis;
|
|
233
|
+
if (!lastOrigin || lastOrigin.distanceToSquared(nextOrigin) >= hysteresisSq) {
|
|
234
|
+
activeGraph.set(three.quadtreeUpdate, (prev) => {
|
|
235
|
+
prev.cameraOrigin.x = nextOrigin.x;
|
|
236
|
+
prev.cameraOrigin.y = nextOrigin.y;
|
|
237
|
+
prev.cameraOrigin.z = nextOrigin.z;
|
|
238
|
+
return prev;
|
|
239
|
+
});
|
|
240
|
+
lastCameraOriginRef.current = nextOrigin;
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
[cameraHysteresis]
|
|
244
|
+
);
|
|
245
|
+
const finishRun = react.useCallback(
|
|
246
|
+
(activeRunController, activeGeneration) => {
|
|
247
|
+
if (runAbortControllerRef.current === activeRunController) {
|
|
248
|
+
runAbortControllerRef.current = null;
|
|
249
|
+
runPromiseRef.current = null;
|
|
250
|
+
}
|
|
251
|
+
if (generationRef.current === activeGeneration) {
|
|
252
|
+
runningRef.current = false;
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
[]
|
|
256
|
+
);
|
|
257
|
+
react.useLayoutEffect(() => {
|
|
258
|
+
void stopCurrentRun();
|
|
259
|
+
graphRef.current = graph;
|
|
260
|
+
targetsRef.current = targets;
|
|
261
|
+
getCameraOriginRef.current = getCameraOrigin;
|
|
262
|
+
lastCameraOriginRef.current = null;
|
|
263
|
+
runningRef.current = false;
|
|
264
|
+
generationRef.current += 1;
|
|
265
|
+
clearError();
|
|
266
|
+
return () => {
|
|
267
|
+
generationRef.current += 1;
|
|
268
|
+
runningRef.current = false;
|
|
269
|
+
void stopCurrentRun();
|
|
270
|
+
};
|
|
271
|
+
}, [clearError, getCameraOrigin, graph, stopCurrentRun, targets]);
|
|
272
|
+
fiber.useFrame((state) => {
|
|
273
|
+
if (runningRef.current) return;
|
|
274
|
+
const renderer = state.gl;
|
|
275
|
+
if (!isWebGpuRenderer(renderer)) {
|
|
276
|
+
reportError(
|
|
277
|
+
new Error(WEBGPU_RENDERER_ERROR),
|
|
278
|
+
`renderer:${WEBGPU_RENDERER_ERROR}`
|
|
279
|
+
);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
const activeGeneration = generationRef.current;
|
|
283
|
+
const activeRunController = new AbortController();
|
|
284
|
+
runningRef.current = true;
|
|
285
|
+
runAbortControllerRef.current = activeRunController;
|
|
286
|
+
const runPromise = (async () => {
|
|
287
|
+
try {
|
|
288
|
+
const activeGraph = graphRef.current;
|
|
289
|
+
const activeTargets = targetsRef.current;
|
|
290
|
+
updateCameraOrigin(state, activeGraph);
|
|
291
|
+
const report = await activeGraph.run({
|
|
292
|
+
targets: activeTargets,
|
|
293
|
+
signal: activeRunController.signal,
|
|
294
|
+
resources: {
|
|
295
|
+
renderer
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
if (report.status === "error") {
|
|
299
|
+
reportError(new Error(GRAPH_RUN_ERROR), `graph:${GRAPH_RUN_ERROR}`);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
clearError();
|
|
303
|
+
} catch (error) {
|
|
304
|
+
if (activeRunController.signal.aborted) return;
|
|
305
|
+
reportError(error);
|
|
306
|
+
} finally {
|
|
307
|
+
finishRun(activeRunController, activeGeneration);
|
|
308
|
+
}
|
|
309
|
+
})();
|
|
310
|
+
runPromiseRef.current = runPromise;
|
|
311
|
+
});
|
|
312
|
+
return stopCurrentRun;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function useTerrain(options = {}) {
|
|
316
|
+
const runtime = react.useMemo(
|
|
317
|
+
() => ({
|
|
318
|
+
query: null,
|
|
319
|
+
raycast: null
|
|
320
|
+
}),
|
|
321
|
+
[]
|
|
322
|
+
);
|
|
323
|
+
const isMountedRef = react.useRef(true);
|
|
324
|
+
const graphLifecycleRef = react.useRef(0);
|
|
325
|
+
const terrainNodesRef = react.useRef({
|
|
326
|
+
positionNode: null
|
|
327
|
+
});
|
|
328
|
+
const [terrainNodes, setTerrainNodes] = react.useState(terrainNodesRef.current);
|
|
329
|
+
const readyRef = react.useRef(false);
|
|
330
|
+
const [ready, setReady] = react.useState(false);
|
|
331
|
+
const emitTerrainNodes = react.useCallback(
|
|
332
|
+
(nextTerrainNodes) => {
|
|
333
|
+
if (!isMountedRef.current) return;
|
|
334
|
+
setTerrainNodes((prevTerrainNodes) => {
|
|
335
|
+
if (prevTerrainNodes.positionNode === nextTerrainNodes.positionNode) {
|
|
336
|
+
terrainNodesRef.current = prevTerrainNodes;
|
|
337
|
+
return prevTerrainNodes;
|
|
338
|
+
}
|
|
339
|
+
terrainNodesRef.current = nextTerrainNodes;
|
|
340
|
+
return nextTerrainNodes;
|
|
341
|
+
});
|
|
342
|
+
},
|
|
343
|
+
[]
|
|
344
|
+
);
|
|
345
|
+
const getTerrainNodes = react.useCallback(
|
|
346
|
+
() => terrainNodesRef.current,
|
|
347
|
+
[]
|
|
348
|
+
);
|
|
349
|
+
const emitReady = react.useCallback((nextReady) => {
|
|
350
|
+
if (!isMountedRef.current) return;
|
|
351
|
+
readyRef.current = nextReady;
|
|
352
|
+
setReady((prevReady) => prevReady === nextReady ? prevReady : nextReady);
|
|
353
|
+
}, []);
|
|
354
|
+
const getReady = react.useCallback(
|
|
355
|
+
() => readyRef.current,
|
|
356
|
+
[]
|
|
357
|
+
);
|
|
358
|
+
const syncTerrainRuntimeTask = react.useMemo(
|
|
359
|
+
() => createSyncTerrainRuntimeTask(runtime),
|
|
360
|
+
[runtime]
|
|
361
|
+
);
|
|
362
|
+
const syncTerrainNodesTask = react.useMemo(
|
|
363
|
+
() => createSyncTerrainNodesTask(
|
|
364
|
+
getTerrainNodes,
|
|
365
|
+
emitTerrainNodes,
|
|
366
|
+
getReady,
|
|
367
|
+
emitReady
|
|
368
|
+
),
|
|
369
|
+
[emitReady, emitTerrainNodes, getReady, getTerrainNodes]
|
|
370
|
+
);
|
|
371
|
+
const graph = react.useMemo(() => {
|
|
372
|
+
const nextGraph = three.terrainGraph();
|
|
373
|
+
for (const task of options.tasks ?? []) {
|
|
374
|
+
nextGraph.add(task);
|
|
375
|
+
}
|
|
376
|
+
nextGraph.add(syncTerrainRuntimeTask);
|
|
377
|
+
nextGraph.add(syncTerrainNodesTask);
|
|
378
|
+
return nextGraph;
|
|
379
|
+
}, [options.tasks, syncTerrainNodesTask, syncTerrainRuntimeTask]);
|
|
380
|
+
const runnerTargets = react.useMemo(
|
|
381
|
+
() => {
|
|
382
|
+
const userTasks = options.tasks ?? [];
|
|
383
|
+
return [
|
|
384
|
+
...userTasks,
|
|
385
|
+
three.terrainTasks.executeCompute,
|
|
386
|
+
three.terrainTasks.terrainReadback,
|
|
387
|
+
syncTerrainRuntimeTask,
|
|
388
|
+
syncTerrainNodesTask
|
|
389
|
+
];
|
|
390
|
+
},
|
|
391
|
+
[options.tasks, syncTerrainNodesTask, syncTerrainRuntimeTask]
|
|
392
|
+
);
|
|
393
|
+
const stopTerrainRunner = useTerrainRunner({
|
|
394
|
+
graph,
|
|
395
|
+
targets: runnerTargets,
|
|
396
|
+
getCameraOrigin: options.getCameraOrigin,
|
|
397
|
+
cameraHysteresis: options.cameraHysteresis
|
|
398
|
+
});
|
|
399
|
+
react.useEffect(() => {
|
|
400
|
+
graphLifecycleRef.current += 1;
|
|
401
|
+
const lifecycle = graphLifecycleRef.current;
|
|
402
|
+
isMountedRef.current = true;
|
|
403
|
+
return () => {
|
|
404
|
+
isMountedRef.current = false;
|
|
405
|
+
queueMicrotask(() => {
|
|
406
|
+
if (graphLifecycleRef.current !== lifecycle) return;
|
|
407
|
+
void stopTerrainRunner().finally(() => {
|
|
408
|
+
if (graphLifecycleRef.current !== lifecycle) return;
|
|
409
|
+
graph.dispose();
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
};
|
|
413
|
+
}, [graph, stopTerrainRunner]);
|
|
414
|
+
react.useLayoutEffect(() => {
|
|
415
|
+
runtime.query = null;
|
|
416
|
+
runtime.raycast = null;
|
|
417
|
+
const nextTerrainNodes = {
|
|
418
|
+
positionNode: null
|
|
419
|
+
};
|
|
420
|
+
terrainNodesRef.current = nextTerrainNodes;
|
|
421
|
+
readyRef.current = false;
|
|
422
|
+
setTerrainNodes(nextTerrainNodes);
|
|
423
|
+
setReady(false);
|
|
424
|
+
}, [graph, runtime]);
|
|
425
|
+
useTerrainParams(graph, options);
|
|
426
|
+
return react.useMemo(
|
|
427
|
+
() => ({
|
|
428
|
+
graph,
|
|
429
|
+
tasks: three.terrainTasks,
|
|
430
|
+
runtime,
|
|
431
|
+
ready,
|
|
432
|
+
...terrainNodes
|
|
433
|
+
}),
|
|
434
|
+
[graph, ready, runtime, terrainNodes]
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function useTerrainMesh(innerTileSegments, maxNodes) {
|
|
439
|
+
const [mesh] = react.useState(
|
|
440
|
+
() => new three.TerrainMesh({
|
|
441
|
+
innerTileSegments: innerTileSegments ?? 13,
|
|
442
|
+
maxNodes: maxNodes ?? 1024
|
|
443
|
+
})
|
|
444
|
+
);
|
|
445
|
+
react.useEffect(() => {
|
|
446
|
+
mesh.innerTileSegments = innerTileSegments ?? 13;
|
|
447
|
+
}, [mesh, innerTileSegments]);
|
|
448
|
+
react.useEffect(() => {
|
|
449
|
+
mesh.maxNodes = maxNodes ?? 1024;
|
|
450
|
+
}, [mesh, maxNodes]);
|
|
451
|
+
react.useEffect(() => {
|
|
452
|
+
return () => {
|
|
453
|
+
mesh.geometry.dispose();
|
|
454
|
+
};
|
|
455
|
+
}, [mesh]);
|
|
456
|
+
return mesh;
|
|
457
|
+
}
|
|
458
|
+
function syncTerrainMesh(mesh, terrain) {
|
|
459
|
+
const leaves = terrain.graph.peek(three.terrainTasks.quadtreeUpdate);
|
|
460
|
+
if (leaves && mesh.count !== leaves.count) {
|
|
461
|
+
mesh.count = leaves.count;
|
|
462
|
+
mesh.instanceMatrix.needsUpdate = true;
|
|
463
|
+
}
|
|
464
|
+
const raycast = terrain.runtime.raycast;
|
|
465
|
+
if (mesh.terrainRaycast !== raycast) {
|
|
466
|
+
mesh.terrainRaycast = raycast;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
function attachTerrainMaterial(node, terrainNodes) {
|
|
470
|
+
if (!react.isValidElement(node)) return node;
|
|
471
|
+
const nextKey = `terrain-material-${terrainNodes.positionNode?.id ?? "null"}`;
|
|
472
|
+
if ("attach" in node.props && node.props.attach != null) {
|
|
473
|
+
return react.cloneElement(node, { key: nextKey });
|
|
474
|
+
}
|
|
475
|
+
return react.cloneElement(node, { attach: "material", key: nextKey });
|
|
476
|
+
}
|
|
477
|
+
function TerrainWithHandle({
|
|
478
|
+
terrain,
|
|
479
|
+
children,
|
|
480
|
+
innerTileSegments,
|
|
481
|
+
maxNodes,
|
|
482
|
+
...primitiveProps
|
|
483
|
+
}) {
|
|
484
|
+
const mesh = useTerrainMesh(innerTileSegments, maxNodes);
|
|
485
|
+
const { visible: primitiveVisible = true, ...restPrimitiveProps } = primitiveProps;
|
|
486
|
+
fiber.useFrame(() => {
|
|
487
|
+
syncTerrainMesh(mesh, terrain);
|
|
488
|
+
});
|
|
489
|
+
return /* @__PURE__ */ jsxRuntime.jsx(TerrainProvider, { value: terrain, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
490
|
+
"primitive",
|
|
491
|
+
{
|
|
492
|
+
object: mesh,
|
|
493
|
+
visible: terrain.ready && primitiveVisible,
|
|
494
|
+
...restPrimitiveProps,
|
|
495
|
+
children: terrain.ready ? attachTerrainMaterial(
|
|
496
|
+
children({
|
|
497
|
+
positionNode: terrain.positionNode
|
|
498
|
+
}),
|
|
499
|
+
terrain
|
|
500
|
+
) : null
|
|
501
|
+
}
|
|
502
|
+
) });
|
|
503
|
+
}
|
|
504
|
+
function InternalTerrain(props) {
|
|
505
|
+
const {
|
|
506
|
+
children,
|
|
507
|
+
rootSize,
|
|
508
|
+
origin,
|
|
509
|
+
maxLevel,
|
|
510
|
+
innerTileSegments,
|
|
511
|
+
skirtScale,
|
|
512
|
+
elevationScale,
|
|
513
|
+
elevation,
|
|
514
|
+
surface,
|
|
515
|
+
terrainFieldFilter,
|
|
516
|
+
getCameraOrigin,
|
|
517
|
+
cameraHysteresis,
|
|
518
|
+
tasks,
|
|
519
|
+
maxNodes,
|
|
520
|
+
...primitiveProps
|
|
521
|
+
} = props;
|
|
522
|
+
const terrain = useTerrain({
|
|
523
|
+
rootSize,
|
|
524
|
+
origin,
|
|
525
|
+
maxLevel,
|
|
526
|
+
innerTileSegments,
|
|
527
|
+
skirtScale,
|
|
528
|
+
elevationScale,
|
|
529
|
+
elevation,
|
|
530
|
+
surface,
|
|
531
|
+
terrainFieldFilter,
|
|
532
|
+
getCameraOrigin,
|
|
533
|
+
cameraHysteresis,
|
|
534
|
+
tasks,
|
|
535
|
+
maxNodes
|
|
536
|
+
});
|
|
537
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
538
|
+
TerrainWithHandle,
|
|
539
|
+
{
|
|
540
|
+
terrain,
|
|
541
|
+
innerTileSegments,
|
|
542
|
+
maxNodes,
|
|
543
|
+
...primitiveProps,
|
|
544
|
+
children
|
|
545
|
+
}
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
function Terrain({
|
|
549
|
+
terrain: providedTerrain,
|
|
550
|
+
children,
|
|
551
|
+
rootSize,
|
|
552
|
+
origin,
|
|
553
|
+
maxLevel,
|
|
554
|
+
innerTileSegments,
|
|
555
|
+
skirtScale,
|
|
556
|
+
elevationScale,
|
|
557
|
+
elevation,
|
|
558
|
+
surface,
|
|
559
|
+
terrainFieldFilter,
|
|
560
|
+
getCameraOrigin,
|
|
561
|
+
cameraHysteresis,
|
|
562
|
+
tasks,
|
|
563
|
+
maxNodes,
|
|
564
|
+
...primitiveProps
|
|
565
|
+
}) {
|
|
566
|
+
if (providedTerrain) {
|
|
567
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
568
|
+
TerrainWithHandle,
|
|
569
|
+
{
|
|
570
|
+
terrain: providedTerrain,
|
|
571
|
+
innerTileSegments,
|
|
572
|
+
maxNodes,
|
|
573
|
+
...primitiveProps,
|
|
574
|
+
children
|
|
575
|
+
}
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
579
|
+
InternalTerrain,
|
|
580
|
+
{
|
|
581
|
+
rootSize,
|
|
582
|
+
origin,
|
|
583
|
+
maxLevel,
|
|
584
|
+
innerTileSegments,
|
|
585
|
+
skirtScale,
|
|
586
|
+
elevationScale,
|
|
587
|
+
elevation,
|
|
588
|
+
surface,
|
|
589
|
+
terrainFieldFilter,
|
|
590
|
+
getCameraOrigin,
|
|
591
|
+
cameraHysteresis,
|
|
592
|
+
tasks,
|
|
593
|
+
maxNodes,
|
|
594
|
+
...primitiveProps,
|
|
595
|
+
children
|
|
596
|
+
}
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
exports.Terrain = Terrain;
|
|
601
|
+
exports.TerrainProvider = TerrainProvider;
|
|
602
|
+
exports.useTerrain = useTerrain;
|
|
603
|
+
exports.useTerrainContext = useTerrainContext;
|