@hello-terrain/react 0.0.0-alpha.10 → 0.0.0-alpha.12
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/dist/index.cjs +81 -77
- package/dist/index.d.cts +11 -4
- package/dist/index.d.mts +11 -4
- package/dist/index.d.ts +11 -4
- package/dist/index.mjs +82 -78
- package/package.json +3 -2
package/dist/index.cjs
CHANGED
|
@@ -21,10 +21,15 @@ function useTerrainContext() {
|
|
|
21
21
|
|
|
22
22
|
function createSyncTerrainRuntimeTask(runtime) {
|
|
23
23
|
return work.task((get, work) => {
|
|
24
|
-
const
|
|
24
|
+
const queryContext = get(three.terrainTasks.terrainQuery);
|
|
25
|
+
const query = queryContext.query;
|
|
26
|
+
const surfaceQuery = queryContext.surfaceQuery;
|
|
27
|
+
const sphereQuery = queryContext.sphereQuery;
|
|
25
28
|
const raycast = get(three.terrainTasks.terrainRaycast);
|
|
26
29
|
return work(() => {
|
|
27
30
|
runtime.query = query;
|
|
31
|
+
runtime.surfaceQuery = surfaceQuery;
|
|
32
|
+
runtime.sphereQuery = sphereQuery;
|
|
28
33
|
runtime.raycast = raycast;
|
|
29
34
|
return runtime;
|
|
30
35
|
});
|
|
@@ -73,8 +78,9 @@ function useTerrainParams(graph, options) {
|
|
|
73
78
|
innerTileSegments: nextInnerTileSegments,
|
|
74
79
|
skirtScale: nextSkirtScale,
|
|
75
80
|
elevationScale: nextElevationScale,
|
|
81
|
+
radius: nextRadius,
|
|
76
82
|
elevation,
|
|
77
|
-
|
|
83
|
+
topology: nextTopology,
|
|
78
84
|
terrainFieldFilter: nextTerrainFieldFilter
|
|
79
85
|
} = options;
|
|
80
86
|
react.useLayoutEffect(() => {
|
|
@@ -132,9 +138,16 @@ function useTerrainParams(graph, options) {
|
|
|
132
138
|
resetOrSet(
|
|
133
139
|
graph,
|
|
134
140
|
ownedParamIds,
|
|
135
|
-
three.
|
|
136
|
-
|
|
137
|
-
() =>
|
|
141
|
+
three.radius,
|
|
142
|
+
nextRadius,
|
|
143
|
+
() => nextRadius
|
|
144
|
+
);
|
|
145
|
+
resetOrSet(
|
|
146
|
+
graph,
|
|
147
|
+
ownedParamIds,
|
|
148
|
+
three.topology,
|
|
149
|
+
nextTopology,
|
|
150
|
+
() => nextTopology
|
|
138
151
|
);
|
|
139
152
|
resetOrSet(
|
|
140
153
|
graph,
|
|
@@ -161,8 +174,9 @@ function useTerrainParams(graph, options) {
|
|
|
161
174
|
nextInnerTileSegments,
|
|
162
175
|
nextSkirtScale,
|
|
163
176
|
nextElevationScale,
|
|
177
|
+
nextRadius,
|
|
164
178
|
elevation,
|
|
165
|
-
|
|
179
|
+
nextTopology,
|
|
166
180
|
nextTerrainFieldFilter
|
|
167
181
|
]);
|
|
168
182
|
}
|
|
@@ -316,6 +330,8 @@ function useTerrain(options = {}) {
|
|
|
316
330
|
const runtime = react.useMemo(
|
|
317
331
|
() => ({
|
|
318
332
|
query: null,
|
|
333
|
+
surfaceQuery: null,
|
|
334
|
+
sphereQuery: null,
|
|
319
335
|
raycast: null
|
|
320
336
|
}),
|
|
321
337
|
[]
|
|
@@ -328,44 +344,27 @@ function useTerrain(options = {}) {
|
|
|
328
344
|
const [terrainNodes, setTerrainNodes] = react.useState(terrainNodesRef.current);
|
|
329
345
|
const readyRef = react.useRef(false);
|
|
330
346
|
const [ready, setReady] = react.useState(false);
|
|
331
|
-
const emitTerrainNodes = react.useCallback(
|
|
332
|
-
(
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
[]
|
|
344
|
-
);
|
|
345
|
-
const getTerrainNodes = react.useCallback(
|
|
346
|
-
() => terrainNodesRef.current,
|
|
347
|
-
[]
|
|
348
|
-
);
|
|
347
|
+
const emitTerrainNodes = react.useCallback((nextTerrainNodes) => {
|
|
348
|
+
if (!isMountedRef.current) return;
|
|
349
|
+
setTerrainNodes((prevTerrainNodes) => {
|
|
350
|
+
if (prevTerrainNodes.positionNode === nextTerrainNodes.positionNode) {
|
|
351
|
+
terrainNodesRef.current = prevTerrainNodes;
|
|
352
|
+
return prevTerrainNodes;
|
|
353
|
+
}
|
|
354
|
+
terrainNodesRef.current = nextTerrainNodes;
|
|
355
|
+
return nextTerrainNodes;
|
|
356
|
+
});
|
|
357
|
+
}, []);
|
|
358
|
+
const getTerrainNodes = react.useCallback(() => terrainNodesRef.current, []);
|
|
349
359
|
const emitReady = react.useCallback((nextReady) => {
|
|
350
360
|
if (!isMountedRef.current) return;
|
|
351
361
|
readyRef.current = nextReady;
|
|
352
362
|
setReady((prevReady) => prevReady === nextReady ? prevReady : nextReady);
|
|
353
363
|
}, []);
|
|
354
|
-
const getReady = react.useCallback(
|
|
355
|
-
|
|
356
|
-
[]
|
|
357
|
-
);
|
|
358
|
-
const syncTerrainRuntimeTask = react.useMemo(
|
|
359
|
-
() => createSyncTerrainRuntimeTask(runtime),
|
|
360
|
-
[runtime]
|
|
361
|
-
);
|
|
364
|
+
const getReady = react.useCallback(() => readyRef.current, []);
|
|
365
|
+
const syncTerrainRuntimeTask = react.useMemo(() => createSyncTerrainRuntimeTask(runtime), [runtime]);
|
|
362
366
|
const syncTerrainNodesTask = react.useMemo(
|
|
363
|
-
() => createSyncTerrainNodesTask(
|
|
364
|
-
getTerrainNodes,
|
|
365
|
-
emitTerrainNodes,
|
|
366
|
-
getReady,
|
|
367
|
-
emitReady
|
|
368
|
-
),
|
|
367
|
+
() => createSyncTerrainNodesTask(getTerrainNodes, emitTerrainNodes, getReady, emitReady),
|
|
369
368
|
[emitReady, emitTerrainNodes, getReady, getTerrainNodes]
|
|
370
369
|
);
|
|
371
370
|
const graph = react.useMemo(() => {
|
|
@@ -377,19 +376,17 @@ function useTerrain(options = {}) {
|
|
|
377
376
|
nextGraph.add(syncTerrainNodesTask);
|
|
378
377
|
return nextGraph;
|
|
379
378
|
}, [options.tasks, syncTerrainNodesTask, syncTerrainRuntimeTask]);
|
|
380
|
-
const runnerTargets = react.useMemo(
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
[options.tasks, syncTerrainNodesTask, syncTerrainRuntimeTask]
|
|
392
|
-
);
|
|
379
|
+
const runnerTargets = react.useMemo(() => {
|
|
380
|
+
const userTasks = options.tasks ?? [];
|
|
381
|
+
return [
|
|
382
|
+
...userTasks,
|
|
383
|
+
three.terrainTasks.executeCompute,
|
|
384
|
+
three.terrainTasks.terrainReadback,
|
|
385
|
+
three.terrainTasks.gpuSpatialIndexUpload,
|
|
386
|
+
syncTerrainRuntimeTask,
|
|
387
|
+
syncTerrainNodesTask
|
|
388
|
+
];
|
|
389
|
+
}, [options.tasks, syncTerrainNodesTask, syncTerrainRuntimeTask]);
|
|
393
390
|
const stopTerrainRunner = useTerrainRunner({
|
|
394
391
|
graph,
|
|
395
392
|
targets: runnerTargets,
|
|
@@ -423,31 +420,34 @@ function useTerrain(options = {}) {
|
|
|
423
420
|
setReady(false);
|
|
424
421
|
}, [graph, runtime]);
|
|
425
422
|
useTerrainParams(graph, options);
|
|
423
|
+
const topology = options.topology ?? null;
|
|
426
424
|
return react.useMemo(
|
|
427
425
|
() => ({
|
|
428
426
|
graph,
|
|
429
427
|
tasks: three.terrainTasks,
|
|
430
428
|
runtime,
|
|
431
429
|
ready,
|
|
430
|
+
topology,
|
|
432
431
|
...terrainNodes
|
|
433
432
|
}),
|
|
434
|
-
[graph, ready, runtime, terrainNodes]
|
|
433
|
+
[graph, ready, runtime, topology, terrainNodes]
|
|
435
434
|
);
|
|
436
435
|
}
|
|
437
436
|
|
|
438
|
-
function useTerrainMesh(innerTileSegments, maxNodes) {
|
|
437
|
+
function useTerrainMesh(innerTileSegments, maxNodes, flipWinding) {
|
|
439
438
|
const [mesh] = react.useState(
|
|
440
439
|
() => new three.TerrainMesh({
|
|
441
|
-
innerTileSegments
|
|
442
|
-
maxNodes: maxNodes ?? 1024
|
|
440
|
+
...innerTileSegments !== void 0 ? { innerTileSegments } : {},
|
|
441
|
+
maxNodes: maxNodes ?? 1024,
|
|
442
|
+
flipWinding
|
|
443
443
|
})
|
|
444
444
|
);
|
|
445
|
-
react.useEffect(() => {
|
|
446
|
-
mesh.innerTileSegments = innerTileSegments ?? 13;
|
|
447
|
-
}, [mesh, innerTileSegments]);
|
|
448
445
|
react.useEffect(() => {
|
|
449
446
|
mesh.maxNodes = maxNodes ?? 1024;
|
|
450
447
|
}, [mesh, maxNodes]);
|
|
448
|
+
react.useEffect(() => {
|
|
449
|
+
mesh.flipWinding = flipWinding;
|
|
450
|
+
}, [mesh, flipWinding]);
|
|
451
451
|
react.useEffect(() => {
|
|
452
452
|
return () => {
|
|
453
453
|
mesh.geometry.dispose();
|
|
@@ -461,6 +461,13 @@ function syncTerrainMesh(mesh, terrain) {
|
|
|
461
461
|
mesh.count = leaves.count;
|
|
462
462
|
mesh.instanceMatrix.needsUpdate = true;
|
|
463
463
|
}
|
|
464
|
+
const uniforms = terrain.graph.peek(three.terrainTasks.updateUniforms);
|
|
465
|
+
if (uniforms) {
|
|
466
|
+
const segments = uniforms.uInnerTileSegments.value;
|
|
467
|
+
if (typeof segments === "number") {
|
|
468
|
+
mesh.innerTileSegments = segments;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
464
471
|
const raycast = terrain.runtime.raycast;
|
|
465
472
|
if (mesh.terrainRaycast !== raycast) {
|
|
466
473
|
mesh.terrainRaycast = raycast;
|
|
@@ -481,25 +488,18 @@ function TerrainWithHandle({
|
|
|
481
488
|
maxNodes,
|
|
482
489
|
...primitiveProps
|
|
483
490
|
}) {
|
|
484
|
-
const
|
|
491
|
+
const flipWinding = terrain.topology?.projection?.faceOutward ?? false;
|
|
492
|
+
const mesh = useTerrainMesh(innerTileSegments, maxNodes, flipWinding);
|
|
485
493
|
const { visible: primitiveVisible = true, ...restPrimitiveProps } = primitiveProps;
|
|
486
494
|
fiber.useFrame(() => {
|
|
487
495
|
syncTerrainMesh(mesh, terrain);
|
|
488
496
|
});
|
|
489
|
-
return /* @__PURE__ */ jsxRuntime.jsx(TerrainProvider, { value: terrain, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
children: terrain.ready ? attachTerrainMaterial(
|
|
496
|
-
children({
|
|
497
|
-
positionNode: terrain.positionNode
|
|
498
|
-
}),
|
|
499
|
-
terrain
|
|
500
|
-
) : null
|
|
501
|
-
}
|
|
502
|
-
) });
|
|
497
|
+
return /* @__PURE__ */ jsxRuntime.jsx(TerrainProvider, { value: terrain, children: /* @__PURE__ */ jsxRuntime.jsx("primitive", { object: mesh, visible: terrain.ready && primitiveVisible, ...restPrimitiveProps, children: terrain.ready ? attachTerrainMaterial(
|
|
498
|
+
children({
|
|
499
|
+
positionNode: terrain.positionNode
|
|
500
|
+
}),
|
|
501
|
+
terrain
|
|
502
|
+
) : null }) });
|
|
503
503
|
}
|
|
504
504
|
function InternalTerrain(props) {
|
|
505
505
|
const {
|
|
@@ -510,8 +510,9 @@ function InternalTerrain(props) {
|
|
|
510
510
|
innerTileSegments,
|
|
511
511
|
skirtScale,
|
|
512
512
|
elevationScale,
|
|
513
|
+
radius,
|
|
513
514
|
elevation,
|
|
514
|
-
|
|
515
|
+
topology,
|
|
515
516
|
terrainFieldFilter,
|
|
516
517
|
getCameraOrigin,
|
|
517
518
|
cameraHysteresis,
|
|
@@ -526,8 +527,9 @@ function InternalTerrain(props) {
|
|
|
526
527
|
innerTileSegments,
|
|
527
528
|
skirtScale,
|
|
528
529
|
elevationScale,
|
|
530
|
+
radius,
|
|
529
531
|
elevation,
|
|
530
|
-
|
|
532
|
+
topology,
|
|
531
533
|
terrainFieldFilter,
|
|
532
534
|
getCameraOrigin,
|
|
533
535
|
cameraHysteresis,
|
|
@@ -554,8 +556,9 @@ function Terrain({
|
|
|
554
556
|
innerTileSegments,
|
|
555
557
|
skirtScale,
|
|
556
558
|
elevationScale,
|
|
559
|
+
radius,
|
|
557
560
|
elevation,
|
|
558
|
-
|
|
561
|
+
topology,
|
|
559
562
|
terrainFieldFilter,
|
|
560
563
|
getCameraOrigin,
|
|
561
564
|
cameraHysteresis,
|
|
@@ -584,8 +587,9 @@ function Terrain({
|
|
|
584
587
|
innerTileSegments,
|
|
585
588
|
skirtScale,
|
|
586
589
|
elevationScale,
|
|
590
|
+
radius,
|
|
587
591
|
elevation,
|
|
588
|
-
|
|
592
|
+
topology,
|
|
589
593
|
terrainFieldFilter,
|
|
590
594
|
getCameraOrigin,
|
|
591
595
|
cameraHysteresis,
|
package/dist/index.d.cts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
-
import { ElevationCallback,
|
|
2
|
+
import { ElevationCallback, Topology, TerrainGraph, TerrainTasks, TerrainQuery, TerrainSurfaceQuery, TerrainSphereQuery, TerrainRaycast } from '@hello-terrain/three';
|
|
3
|
+
export { TerrainQuery, TerrainRaycast, TerrainRaycastResult, TerrainSample, TerrainSphereQuery, TerrainSurfaceSample, TerrainSurfaceSampleBatch, TerrainTile } from '@hello-terrain/three';
|
|
3
4
|
import { Task } from '@hello-terrain/work';
|
|
4
5
|
import { ThreeElements, RootState } from '@react-three/fiber';
|
|
5
6
|
import { ReactNode } from 'react';
|
|
6
|
-
import { WebGPURenderer } from 'three/webgpu';
|
|
7
7
|
import { ShaderCallNodeInternal } from 'three/src/nodes/TSL.js';
|
|
8
|
+
import { WebGPURenderer } from 'three/webgpu';
|
|
8
9
|
|
|
9
10
|
type TerrainVector3Like = {
|
|
10
11
|
x: number;
|
|
@@ -19,6 +20,10 @@ interface TerrainNodes {
|
|
|
19
20
|
}
|
|
20
21
|
interface TerrainRuntime {
|
|
21
22
|
query: TerrainQuery | null;
|
|
23
|
+
/** Generic closed-surface query; `null` on flat surfaces. */
|
|
24
|
+
surfaceQuery: TerrainSurfaceQuery | null;
|
|
25
|
+
/** Cube-sphere query; `null` unless the topology uses the cubeSphere projection. */
|
|
26
|
+
sphereQuery: TerrainSphereQuery | null;
|
|
22
27
|
raycast: TerrainRaycast | null;
|
|
23
28
|
}
|
|
24
29
|
interface TerrainHandle extends TerrainNodes {
|
|
@@ -26,6 +31,7 @@ interface TerrainHandle extends TerrainNodes {
|
|
|
26
31
|
tasks: TerrainTasks;
|
|
27
32
|
runtime: TerrainRuntime;
|
|
28
33
|
ready: boolean;
|
|
34
|
+
topology?: Topology | null;
|
|
29
35
|
}
|
|
30
36
|
interface TerrainOptions {
|
|
31
37
|
rootSize?: number;
|
|
@@ -35,8 +41,9 @@ interface TerrainOptions {
|
|
|
35
41
|
innerTileSegments?: number;
|
|
36
42
|
skirtScale?: number;
|
|
37
43
|
elevationScale?: number;
|
|
44
|
+
radius?: number;
|
|
38
45
|
elevation?: ElevationCallback;
|
|
39
|
-
|
|
46
|
+
topology?: Topology | null;
|
|
40
47
|
terrainFieldFilter?: "nearest" | "linear";
|
|
41
48
|
getCameraOrigin?: (state: RootState) => TerrainVector3Like;
|
|
42
49
|
cameraHysteresis?: number;
|
|
@@ -48,7 +55,7 @@ interface TerrainProps extends TerrainPrimitiveProps, TerrainOptions {
|
|
|
48
55
|
children: (nodes: TerrainNodes) => ReactNode;
|
|
49
56
|
}
|
|
50
57
|
|
|
51
|
-
declare function Terrain({ terrain: providedTerrain, children, rootSize, origin, maxLevel, innerTileSegments, skirtScale, elevationScale, elevation,
|
|
58
|
+
declare function Terrain({ terrain: providedTerrain, children, rootSize, origin, maxLevel, innerTileSegments, skirtScale, elevationScale, radius, elevation, topology, terrainFieldFilter, getCameraOrigin, cameraHysteresis, tasks, maxNodes, ...primitiveProps }: TerrainProps): react_jsx_runtime.JSX.Element;
|
|
52
59
|
|
|
53
60
|
interface TerrainProviderProps {
|
|
54
61
|
value: TerrainHandle;
|
package/dist/index.d.mts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
-
import { ElevationCallback,
|
|
2
|
+
import { ElevationCallback, Topology, TerrainGraph, TerrainTasks, TerrainQuery, TerrainSurfaceQuery, TerrainSphereQuery, TerrainRaycast } from '@hello-terrain/three';
|
|
3
|
+
export { TerrainQuery, TerrainRaycast, TerrainRaycastResult, TerrainSample, TerrainSphereQuery, TerrainSurfaceSample, TerrainSurfaceSampleBatch, TerrainTile } from '@hello-terrain/three';
|
|
3
4
|
import { Task } from '@hello-terrain/work';
|
|
4
5
|
import { ThreeElements, RootState } from '@react-three/fiber';
|
|
5
6
|
import { ReactNode } from 'react';
|
|
6
|
-
import { WebGPURenderer } from 'three/webgpu';
|
|
7
7
|
import { ShaderCallNodeInternal } from 'three/src/nodes/TSL.js';
|
|
8
|
+
import { WebGPURenderer } from 'three/webgpu';
|
|
8
9
|
|
|
9
10
|
type TerrainVector3Like = {
|
|
10
11
|
x: number;
|
|
@@ -19,6 +20,10 @@ interface TerrainNodes {
|
|
|
19
20
|
}
|
|
20
21
|
interface TerrainRuntime {
|
|
21
22
|
query: TerrainQuery | null;
|
|
23
|
+
/** Generic closed-surface query; `null` on flat surfaces. */
|
|
24
|
+
surfaceQuery: TerrainSurfaceQuery | null;
|
|
25
|
+
/** Cube-sphere query; `null` unless the topology uses the cubeSphere projection. */
|
|
26
|
+
sphereQuery: TerrainSphereQuery | null;
|
|
22
27
|
raycast: TerrainRaycast | null;
|
|
23
28
|
}
|
|
24
29
|
interface TerrainHandle extends TerrainNodes {
|
|
@@ -26,6 +31,7 @@ interface TerrainHandle extends TerrainNodes {
|
|
|
26
31
|
tasks: TerrainTasks;
|
|
27
32
|
runtime: TerrainRuntime;
|
|
28
33
|
ready: boolean;
|
|
34
|
+
topology?: Topology | null;
|
|
29
35
|
}
|
|
30
36
|
interface TerrainOptions {
|
|
31
37
|
rootSize?: number;
|
|
@@ -35,8 +41,9 @@ interface TerrainOptions {
|
|
|
35
41
|
innerTileSegments?: number;
|
|
36
42
|
skirtScale?: number;
|
|
37
43
|
elevationScale?: number;
|
|
44
|
+
radius?: number;
|
|
38
45
|
elevation?: ElevationCallback;
|
|
39
|
-
|
|
46
|
+
topology?: Topology | null;
|
|
40
47
|
terrainFieldFilter?: "nearest" | "linear";
|
|
41
48
|
getCameraOrigin?: (state: RootState) => TerrainVector3Like;
|
|
42
49
|
cameraHysteresis?: number;
|
|
@@ -48,7 +55,7 @@ interface TerrainProps extends TerrainPrimitiveProps, TerrainOptions {
|
|
|
48
55
|
children: (nodes: TerrainNodes) => ReactNode;
|
|
49
56
|
}
|
|
50
57
|
|
|
51
|
-
declare function Terrain({ terrain: providedTerrain, children, rootSize, origin, maxLevel, innerTileSegments, skirtScale, elevationScale, elevation,
|
|
58
|
+
declare function Terrain({ terrain: providedTerrain, children, rootSize, origin, maxLevel, innerTileSegments, skirtScale, elevationScale, radius, elevation, topology, terrainFieldFilter, getCameraOrigin, cameraHysteresis, tasks, maxNodes, ...primitiveProps }: TerrainProps): react_jsx_runtime.JSX.Element;
|
|
52
59
|
|
|
53
60
|
interface TerrainProviderProps {
|
|
54
61
|
value: TerrainHandle;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
-
import { ElevationCallback,
|
|
2
|
+
import { ElevationCallback, Topology, TerrainGraph, TerrainTasks, TerrainQuery, TerrainSurfaceQuery, TerrainSphereQuery, TerrainRaycast } from '@hello-terrain/three';
|
|
3
|
+
export { TerrainQuery, TerrainRaycast, TerrainRaycastResult, TerrainSample, TerrainSphereQuery, TerrainSurfaceSample, TerrainSurfaceSampleBatch, TerrainTile } from '@hello-terrain/three';
|
|
3
4
|
import { Task } from '@hello-terrain/work';
|
|
4
5
|
import { ThreeElements, RootState } from '@react-three/fiber';
|
|
5
6
|
import { ReactNode } from 'react';
|
|
6
|
-
import { WebGPURenderer } from 'three/webgpu';
|
|
7
7
|
import { ShaderCallNodeInternal } from 'three/src/nodes/TSL.js';
|
|
8
|
+
import { WebGPURenderer } from 'three/webgpu';
|
|
8
9
|
|
|
9
10
|
type TerrainVector3Like = {
|
|
10
11
|
x: number;
|
|
@@ -19,6 +20,10 @@ interface TerrainNodes {
|
|
|
19
20
|
}
|
|
20
21
|
interface TerrainRuntime {
|
|
21
22
|
query: TerrainQuery | null;
|
|
23
|
+
/** Generic closed-surface query; `null` on flat surfaces. */
|
|
24
|
+
surfaceQuery: TerrainSurfaceQuery | null;
|
|
25
|
+
/** Cube-sphere query; `null` unless the topology uses the cubeSphere projection. */
|
|
26
|
+
sphereQuery: TerrainSphereQuery | null;
|
|
22
27
|
raycast: TerrainRaycast | null;
|
|
23
28
|
}
|
|
24
29
|
interface TerrainHandle extends TerrainNodes {
|
|
@@ -26,6 +31,7 @@ interface TerrainHandle extends TerrainNodes {
|
|
|
26
31
|
tasks: TerrainTasks;
|
|
27
32
|
runtime: TerrainRuntime;
|
|
28
33
|
ready: boolean;
|
|
34
|
+
topology?: Topology | null;
|
|
29
35
|
}
|
|
30
36
|
interface TerrainOptions {
|
|
31
37
|
rootSize?: number;
|
|
@@ -35,8 +41,9 @@ interface TerrainOptions {
|
|
|
35
41
|
innerTileSegments?: number;
|
|
36
42
|
skirtScale?: number;
|
|
37
43
|
elevationScale?: number;
|
|
44
|
+
radius?: number;
|
|
38
45
|
elevation?: ElevationCallback;
|
|
39
|
-
|
|
46
|
+
topology?: Topology | null;
|
|
40
47
|
terrainFieldFilter?: "nearest" | "linear";
|
|
41
48
|
getCameraOrigin?: (state: RootState) => TerrainVector3Like;
|
|
42
49
|
cameraHysteresis?: number;
|
|
@@ -48,7 +55,7 @@ interface TerrainProps extends TerrainPrimitiveProps, TerrainOptions {
|
|
|
48
55
|
children: (nodes: TerrainNodes) => ReactNode;
|
|
49
56
|
}
|
|
50
57
|
|
|
51
|
-
declare function Terrain({ terrain: providedTerrain, children, rootSize, origin, maxLevel, innerTileSegments, skirtScale, elevationScale, elevation,
|
|
58
|
+
declare function Terrain({ terrain: providedTerrain, children, rootSize, origin, maxLevel, innerTileSegments, skirtScale, elevationScale, radius, elevation, topology, terrainFieldFilter, getCameraOrigin, cameraHysteresis, tasks, maxNodes, ...primitiveProps }: TerrainProps): react_jsx_runtime.JSX.Element;
|
|
52
59
|
|
|
53
60
|
interface TerrainProviderProps {
|
|
54
61
|
value: TerrainHandle;
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx } from 'react/jsx-runtime';
|
|
2
|
-
import { terrainTasks, elevationFn, rootSize, origin, maxLevel, maxNodes, innerTileSegments, skirtScale, elevationScale,
|
|
2
|
+
import { terrainTasks, elevationFn, rootSize, origin, maxLevel, maxNodes, innerTileSegments, skirtScale, elevationScale, radius, topology, terrainFieldFilter, quadtreeUpdate, terrainGraph, TerrainMesh } from '@hello-terrain/three';
|
|
3
3
|
import { useFrame } from '@react-three/fiber';
|
|
4
4
|
import { createContext, useContext, useRef, useLayoutEffect, useCallback, useMemo, useState, useEffect, isValidElement, cloneElement } from 'react';
|
|
5
5
|
import { task } from '@hello-terrain/work';
|
|
@@ -19,10 +19,15 @@ function useTerrainContext() {
|
|
|
19
19
|
|
|
20
20
|
function createSyncTerrainRuntimeTask(runtime) {
|
|
21
21
|
return task((get, work) => {
|
|
22
|
-
const
|
|
22
|
+
const queryContext = get(terrainTasks.terrainQuery);
|
|
23
|
+
const query = queryContext.query;
|
|
24
|
+
const surfaceQuery = queryContext.surfaceQuery;
|
|
25
|
+
const sphereQuery = queryContext.sphereQuery;
|
|
23
26
|
const raycast = get(terrainTasks.terrainRaycast);
|
|
24
27
|
return work(() => {
|
|
25
28
|
runtime.query = query;
|
|
29
|
+
runtime.surfaceQuery = surfaceQuery;
|
|
30
|
+
runtime.sphereQuery = sphereQuery;
|
|
26
31
|
runtime.raycast = raycast;
|
|
27
32
|
return runtime;
|
|
28
33
|
});
|
|
@@ -71,8 +76,9 @@ function useTerrainParams(graph, options) {
|
|
|
71
76
|
innerTileSegments: nextInnerTileSegments,
|
|
72
77
|
skirtScale: nextSkirtScale,
|
|
73
78
|
elevationScale: nextElevationScale,
|
|
79
|
+
radius: nextRadius,
|
|
74
80
|
elevation,
|
|
75
|
-
|
|
81
|
+
topology: nextTopology,
|
|
76
82
|
terrainFieldFilter: nextTerrainFieldFilter
|
|
77
83
|
} = options;
|
|
78
84
|
useLayoutEffect(() => {
|
|
@@ -130,9 +136,16 @@ function useTerrainParams(graph, options) {
|
|
|
130
136
|
resetOrSet(
|
|
131
137
|
graph,
|
|
132
138
|
ownedParamIds,
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
() =>
|
|
139
|
+
radius,
|
|
140
|
+
nextRadius,
|
|
141
|
+
() => nextRadius
|
|
142
|
+
);
|
|
143
|
+
resetOrSet(
|
|
144
|
+
graph,
|
|
145
|
+
ownedParamIds,
|
|
146
|
+
topology,
|
|
147
|
+
nextTopology,
|
|
148
|
+
() => nextTopology
|
|
136
149
|
);
|
|
137
150
|
resetOrSet(
|
|
138
151
|
graph,
|
|
@@ -159,8 +172,9 @@ function useTerrainParams(graph, options) {
|
|
|
159
172
|
nextInnerTileSegments,
|
|
160
173
|
nextSkirtScale,
|
|
161
174
|
nextElevationScale,
|
|
175
|
+
nextRadius,
|
|
162
176
|
elevation,
|
|
163
|
-
|
|
177
|
+
nextTopology,
|
|
164
178
|
nextTerrainFieldFilter
|
|
165
179
|
]);
|
|
166
180
|
}
|
|
@@ -314,6 +328,8 @@ function useTerrain(options = {}) {
|
|
|
314
328
|
const runtime = useMemo(
|
|
315
329
|
() => ({
|
|
316
330
|
query: null,
|
|
331
|
+
surfaceQuery: null,
|
|
332
|
+
sphereQuery: null,
|
|
317
333
|
raycast: null
|
|
318
334
|
}),
|
|
319
335
|
[]
|
|
@@ -326,44 +342,27 @@ function useTerrain(options = {}) {
|
|
|
326
342
|
const [terrainNodes, setTerrainNodes] = useState(terrainNodesRef.current);
|
|
327
343
|
const readyRef = useRef(false);
|
|
328
344
|
const [ready, setReady] = useState(false);
|
|
329
|
-
const emitTerrainNodes = useCallback(
|
|
330
|
-
(
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
[]
|
|
342
|
-
);
|
|
343
|
-
const getTerrainNodes = useCallback(
|
|
344
|
-
() => terrainNodesRef.current,
|
|
345
|
-
[]
|
|
346
|
-
);
|
|
345
|
+
const emitTerrainNodes = useCallback((nextTerrainNodes) => {
|
|
346
|
+
if (!isMountedRef.current) return;
|
|
347
|
+
setTerrainNodes((prevTerrainNodes) => {
|
|
348
|
+
if (prevTerrainNodes.positionNode === nextTerrainNodes.positionNode) {
|
|
349
|
+
terrainNodesRef.current = prevTerrainNodes;
|
|
350
|
+
return prevTerrainNodes;
|
|
351
|
+
}
|
|
352
|
+
terrainNodesRef.current = nextTerrainNodes;
|
|
353
|
+
return nextTerrainNodes;
|
|
354
|
+
});
|
|
355
|
+
}, []);
|
|
356
|
+
const getTerrainNodes = useCallback(() => terrainNodesRef.current, []);
|
|
347
357
|
const emitReady = useCallback((nextReady) => {
|
|
348
358
|
if (!isMountedRef.current) return;
|
|
349
359
|
readyRef.current = nextReady;
|
|
350
360
|
setReady((prevReady) => prevReady === nextReady ? prevReady : nextReady);
|
|
351
361
|
}, []);
|
|
352
|
-
const getReady = useCallback(
|
|
353
|
-
|
|
354
|
-
[]
|
|
355
|
-
);
|
|
356
|
-
const syncTerrainRuntimeTask = useMemo(
|
|
357
|
-
() => createSyncTerrainRuntimeTask(runtime),
|
|
358
|
-
[runtime]
|
|
359
|
-
);
|
|
362
|
+
const getReady = useCallback(() => readyRef.current, []);
|
|
363
|
+
const syncTerrainRuntimeTask = useMemo(() => createSyncTerrainRuntimeTask(runtime), [runtime]);
|
|
360
364
|
const syncTerrainNodesTask = useMemo(
|
|
361
|
-
() => createSyncTerrainNodesTask(
|
|
362
|
-
getTerrainNodes,
|
|
363
|
-
emitTerrainNodes,
|
|
364
|
-
getReady,
|
|
365
|
-
emitReady
|
|
366
|
-
),
|
|
365
|
+
() => createSyncTerrainNodesTask(getTerrainNodes, emitTerrainNodes, getReady, emitReady),
|
|
367
366
|
[emitReady, emitTerrainNodes, getReady, getTerrainNodes]
|
|
368
367
|
);
|
|
369
368
|
const graph = useMemo(() => {
|
|
@@ -375,19 +374,17 @@ function useTerrain(options = {}) {
|
|
|
375
374
|
nextGraph.add(syncTerrainNodesTask);
|
|
376
375
|
return nextGraph;
|
|
377
376
|
}, [options.tasks, syncTerrainNodesTask, syncTerrainRuntimeTask]);
|
|
378
|
-
const runnerTargets = useMemo(
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
[options.tasks, syncTerrainNodesTask, syncTerrainRuntimeTask]
|
|
390
|
-
);
|
|
377
|
+
const runnerTargets = useMemo(() => {
|
|
378
|
+
const userTasks = options.tasks ?? [];
|
|
379
|
+
return [
|
|
380
|
+
...userTasks,
|
|
381
|
+
terrainTasks.executeCompute,
|
|
382
|
+
terrainTasks.terrainReadback,
|
|
383
|
+
terrainTasks.gpuSpatialIndexUpload,
|
|
384
|
+
syncTerrainRuntimeTask,
|
|
385
|
+
syncTerrainNodesTask
|
|
386
|
+
];
|
|
387
|
+
}, [options.tasks, syncTerrainNodesTask, syncTerrainRuntimeTask]);
|
|
391
388
|
const stopTerrainRunner = useTerrainRunner({
|
|
392
389
|
graph,
|
|
393
390
|
targets: runnerTargets,
|
|
@@ -421,31 +418,34 @@ function useTerrain(options = {}) {
|
|
|
421
418
|
setReady(false);
|
|
422
419
|
}, [graph, runtime]);
|
|
423
420
|
useTerrainParams(graph, options);
|
|
421
|
+
const topology = options.topology ?? null;
|
|
424
422
|
return useMemo(
|
|
425
423
|
() => ({
|
|
426
424
|
graph,
|
|
427
425
|
tasks: terrainTasks,
|
|
428
426
|
runtime,
|
|
429
427
|
ready,
|
|
428
|
+
topology,
|
|
430
429
|
...terrainNodes
|
|
431
430
|
}),
|
|
432
|
-
[graph, ready, runtime, terrainNodes]
|
|
431
|
+
[graph, ready, runtime, topology, terrainNodes]
|
|
433
432
|
);
|
|
434
433
|
}
|
|
435
434
|
|
|
436
|
-
function useTerrainMesh(innerTileSegments, maxNodes) {
|
|
435
|
+
function useTerrainMesh(innerTileSegments, maxNodes, flipWinding) {
|
|
437
436
|
const [mesh] = useState(
|
|
438
437
|
() => new TerrainMesh({
|
|
439
|
-
innerTileSegments
|
|
440
|
-
maxNodes: maxNodes ?? 1024
|
|
438
|
+
...innerTileSegments !== void 0 ? { innerTileSegments } : {},
|
|
439
|
+
maxNodes: maxNodes ?? 1024,
|
|
440
|
+
flipWinding
|
|
441
441
|
})
|
|
442
442
|
);
|
|
443
|
-
useEffect(() => {
|
|
444
|
-
mesh.innerTileSegments = innerTileSegments ?? 13;
|
|
445
|
-
}, [mesh, innerTileSegments]);
|
|
446
443
|
useEffect(() => {
|
|
447
444
|
mesh.maxNodes = maxNodes ?? 1024;
|
|
448
445
|
}, [mesh, maxNodes]);
|
|
446
|
+
useEffect(() => {
|
|
447
|
+
mesh.flipWinding = flipWinding;
|
|
448
|
+
}, [mesh, flipWinding]);
|
|
449
449
|
useEffect(() => {
|
|
450
450
|
return () => {
|
|
451
451
|
mesh.geometry.dispose();
|
|
@@ -459,6 +459,13 @@ function syncTerrainMesh(mesh, terrain) {
|
|
|
459
459
|
mesh.count = leaves.count;
|
|
460
460
|
mesh.instanceMatrix.needsUpdate = true;
|
|
461
461
|
}
|
|
462
|
+
const uniforms = terrain.graph.peek(terrainTasks.updateUniforms);
|
|
463
|
+
if (uniforms) {
|
|
464
|
+
const segments = uniforms.uInnerTileSegments.value;
|
|
465
|
+
if (typeof segments === "number") {
|
|
466
|
+
mesh.innerTileSegments = segments;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
462
469
|
const raycast = terrain.runtime.raycast;
|
|
463
470
|
if (mesh.terrainRaycast !== raycast) {
|
|
464
471
|
mesh.terrainRaycast = raycast;
|
|
@@ -479,25 +486,18 @@ function TerrainWithHandle({
|
|
|
479
486
|
maxNodes,
|
|
480
487
|
...primitiveProps
|
|
481
488
|
}) {
|
|
482
|
-
const
|
|
489
|
+
const flipWinding = terrain.topology?.projection?.faceOutward ?? false;
|
|
490
|
+
const mesh = useTerrainMesh(innerTileSegments, maxNodes, flipWinding);
|
|
483
491
|
const { visible: primitiveVisible = true, ...restPrimitiveProps } = primitiveProps;
|
|
484
492
|
useFrame(() => {
|
|
485
493
|
syncTerrainMesh(mesh, terrain);
|
|
486
494
|
});
|
|
487
|
-
return /* @__PURE__ */ jsx(TerrainProvider, { value: terrain, children: /* @__PURE__ */ jsx(
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
children: terrain.ready ? attachTerrainMaterial(
|
|
494
|
-
children({
|
|
495
|
-
positionNode: terrain.positionNode
|
|
496
|
-
}),
|
|
497
|
-
terrain
|
|
498
|
-
) : null
|
|
499
|
-
}
|
|
500
|
-
) });
|
|
495
|
+
return /* @__PURE__ */ jsx(TerrainProvider, { value: terrain, children: /* @__PURE__ */ jsx("primitive", { object: mesh, visible: terrain.ready && primitiveVisible, ...restPrimitiveProps, children: terrain.ready ? attachTerrainMaterial(
|
|
496
|
+
children({
|
|
497
|
+
positionNode: terrain.positionNode
|
|
498
|
+
}),
|
|
499
|
+
terrain
|
|
500
|
+
) : null }) });
|
|
501
501
|
}
|
|
502
502
|
function InternalTerrain(props) {
|
|
503
503
|
const {
|
|
@@ -508,8 +508,9 @@ function InternalTerrain(props) {
|
|
|
508
508
|
innerTileSegments,
|
|
509
509
|
skirtScale,
|
|
510
510
|
elevationScale,
|
|
511
|
+
radius,
|
|
511
512
|
elevation,
|
|
512
|
-
|
|
513
|
+
topology,
|
|
513
514
|
terrainFieldFilter,
|
|
514
515
|
getCameraOrigin,
|
|
515
516
|
cameraHysteresis,
|
|
@@ -524,8 +525,9 @@ function InternalTerrain(props) {
|
|
|
524
525
|
innerTileSegments,
|
|
525
526
|
skirtScale,
|
|
526
527
|
elevationScale,
|
|
528
|
+
radius,
|
|
527
529
|
elevation,
|
|
528
|
-
|
|
530
|
+
topology,
|
|
529
531
|
terrainFieldFilter,
|
|
530
532
|
getCameraOrigin,
|
|
531
533
|
cameraHysteresis,
|
|
@@ -552,8 +554,9 @@ function Terrain({
|
|
|
552
554
|
innerTileSegments,
|
|
553
555
|
skirtScale,
|
|
554
556
|
elevationScale,
|
|
557
|
+
radius,
|
|
555
558
|
elevation,
|
|
556
|
-
|
|
559
|
+
topology,
|
|
557
560
|
terrainFieldFilter,
|
|
558
561
|
getCameraOrigin,
|
|
559
562
|
cameraHysteresis,
|
|
@@ -582,8 +585,9 @@ function Terrain({
|
|
|
582
585
|
innerTileSegments,
|
|
583
586
|
skirtScale,
|
|
584
587
|
elevationScale,
|
|
588
|
+
radius,
|
|
585
589
|
elevation,
|
|
586
|
-
|
|
590
|
+
topology,
|
|
587
591
|
terrainFieldFilter,
|
|
588
592
|
getCameraOrigin,
|
|
589
593
|
cameraHysteresis,
|
package/package.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "https://github.com/kenjinp/hello-terrain.git"
|
|
8
8
|
},
|
|
9
|
-
"version": "0.0.0-alpha.
|
|
9
|
+
"version": "0.0.0-alpha.12",
|
|
10
10
|
"type": "module",
|
|
11
11
|
"main": "./dist/index.mjs",
|
|
12
12
|
"module": "./dist/index.mjs",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"dist"
|
|
23
23
|
],
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@hello-terrain/three": "0.0.0-alpha.
|
|
25
|
+
"@hello-terrain/three": "0.0.0-alpha.12",
|
|
26
26
|
"@hello-terrain/work": "0.3.0"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
"build": "unbuild",
|
|
52
52
|
"release": "pnpm run build && pnpm publish --access=public",
|
|
53
53
|
"test": "vitest",
|
|
54
|
+
"typecheck": "tsc -p tsconfig.json --noEmit --composite false",
|
|
54
55
|
"lint": "oxlint -c node_modules/@config/oxlint/react.json",
|
|
55
56
|
"format": "oxfmt -c node_modules/@config/oxfmt/base.json --write ."
|
|
56
57
|
}
|