@hello-terrain/three 0.0.0-alpha.10 → 0.0.0-alpha.11
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 +1374 -590
- package/dist/index.d.cts +488 -245
- package/dist/index.d.mts +488 -245
- package/dist/index.d.ts +488 -245
- package/dist/index.mjs +1355 -586
- package/package.json +3 -2
package/dist/index.mjs
CHANGED
|
@@ -1,32 +1,31 @@
|
|
|
1
1
|
import { BufferGeometry, BufferAttribute, RGBAFormat, ClampToEdgeWrapping, HalfFloatType, FloatType, LinearFilter, NearestFilter, Vector3 } from 'three';
|
|
2
2
|
import { MeshStandardNodeMaterial, InstancedMesh, InstancedBufferAttribute, StorageTexture, StorageArrayTexture, StorageBufferAttribute, Vector3 as Vector3$1 } from 'three/webgpu';
|
|
3
3
|
import { param, task, graph } from '@hello-terrain/work';
|
|
4
|
-
import { uniform, Fn,
|
|
4
|
+
import { float, uniform, Fn, globalId, int, vec2, uint, If, textureStore, uvec3, vec4, texture, ivec2, ivec3, textureLoad, select, vec3, pow, storage, workgroupArray, localId, workgroupId, min, Loop, max, workgroupBarrier, Break, vertexIndex, uv, instanceIndex, positionLocal, normalLocal, remap, dot as dot$1, varyingProperty, mx_noise_float, mix } from 'three/tsl';
|
|
5
5
|
import { Fn as Fn$1 } from 'three/src/nodes/TSL.js';
|
|
6
6
|
|
|
7
7
|
class TerrainGeometry extends BufferGeometry {
|
|
8
|
-
|
|
8
|
+
/**
|
|
9
|
+
* @param flipWinding Reverse triangle winding so front faces point the
|
|
10
|
+
* opposite way. The default winding makes flat tiles front-face `+Y`; the
|
|
11
|
+
* cube-sphere maps `(u→right, v→up)`, which would otherwise leave the
|
|
12
|
+
* planet's outer shell back-facing, so it passes `flipWinding` to render
|
|
13
|
+
* the outer surface with `FrontSide`.
|
|
14
|
+
*/
|
|
15
|
+
constructor(innerSegments = 14, extendUV = false, flipWinding = false) {
|
|
9
16
|
super();
|
|
10
17
|
if (innerSegments < 1 || !Number.isFinite(innerSegments) || !Number.isInteger(innerSegments)) {
|
|
11
|
-
throw new Error(
|
|
12
|
-
`Invalid innerSegments: ${innerSegments}. Must be a positive integer.`
|
|
13
|
-
);
|
|
18
|
+
throw new Error(`Invalid innerSegments: ${innerSegments}. Must be a positive integer.`);
|
|
14
19
|
}
|
|
15
20
|
try {
|
|
16
|
-
this.setIndex(this.generateIndices(innerSegments));
|
|
21
|
+
this.setIndex(this.generateIndices(innerSegments, flipWinding));
|
|
17
22
|
this.setAttribute(
|
|
18
23
|
"position",
|
|
19
|
-
new BufferAttribute(
|
|
20
|
-
new Float32Array(this.generatePositions(innerSegments)),
|
|
21
|
-
3
|
|
22
|
-
)
|
|
24
|
+
new BufferAttribute(new Float32Array(this.generatePositions(innerSegments)), 3)
|
|
23
25
|
);
|
|
24
26
|
this.setAttribute(
|
|
25
27
|
"normal",
|
|
26
|
-
new BufferAttribute(
|
|
27
|
-
new Float32Array(this.generateNormals(innerSegments)),
|
|
28
|
-
3
|
|
29
|
-
)
|
|
28
|
+
new BufferAttribute(new Float32Array(this.generateNormals(innerSegments)), 3)
|
|
30
29
|
);
|
|
31
30
|
this.setAttribute(
|
|
32
31
|
"uv",
|
|
@@ -101,12 +100,16 @@ class TerrainGeometry extends BufferGeometry {
|
|
|
101
100
|
* triangle 1: a, c, b
|
|
102
101
|
* triangle 2: b, c, d
|
|
103
102
|
*/
|
|
104
|
-
generateIndices(innerSegments) {
|
|
103
|
+
generateIndices(innerSegments, flipWinding = false) {
|
|
105
104
|
const innerEdgeVertexCount = innerSegments + 1;
|
|
106
105
|
const edgeVertexCountWithSkirt = innerEdgeVertexCount + 2;
|
|
107
106
|
const indices = [];
|
|
108
107
|
const cellsPerEdge = edgeVertexCountWithSkirt - 1;
|
|
109
108
|
const mid = Math.floor(cellsPerEdge / 2);
|
|
109
|
+
const pushTri = (v0, v1, v2) => {
|
|
110
|
+
if (flipWinding) indices.push(v0, v2, v1);
|
|
111
|
+
else indices.push(v0, v1, v2);
|
|
112
|
+
};
|
|
110
113
|
for (let y = 0; y < cellsPerEdge; y++) {
|
|
111
114
|
for (let x = 0; x < cellsPerEdge; x++) {
|
|
112
115
|
const a = y * edgeVertexCountWithSkirt + x;
|
|
@@ -123,11 +126,11 @@ class TerrainGeometry extends BufferGeometry {
|
|
|
123
126
|
useDefaultDiagonal = (x + y) % 2 === 0;
|
|
124
127
|
}
|
|
125
128
|
if (useDefaultDiagonal) {
|
|
126
|
-
|
|
127
|
-
|
|
129
|
+
pushTri(a, d, b);
|
|
130
|
+
pushTri(a, c, d);
|
|
128
131
|
} else {
|
|
129
|
-
|
|
130
|
-
|
|
132
|
+
pushTri(a, c, b);
|
|
133
|
+
pushTri(b, c, d);
|
|
131
134
|
}
|
|
132
135
|
}
|
|
133
136
|
}
|
|
@@ -218,32 +221,70 @@ class TerrainGeometry extends BufferGeometry {
|
|
|
218
221
|
}
|
|
219
222
|
}
|
|
220
223
|
|
|
224
|
+
const rootSize = param(256).displayName("rootSize");
|
|
225
|
+
const origin = param({
|
|
226
|
+
x: 0,
|
|
227
|
+
y: 0,
|
|
228
|
+
z: 0
|
|
229
|
+
}).displayName("origin");
|
|
230
|
+
const innerTileSegments = param(61).displayName("innerTileSegments");
|
|
231
|
+
const skirtScale = param(100).displayName("skirtScale");
|
|
232
|
+
const elevationScale = param(1).displayName("elevationScale");
|
|
233
|
+
const radius = param(1e3).displayName("radius");
|
|
234
|
+
const maxNodes = param(1024).displayName("maxNodes");
|
|
235
|
+
const maxLevel = param(16).displayName("maxLevel");
|
|
236
|
+
const quadtreeUpdate = param({
|
|
237
|
+
cameraOrigin: { x: 0, y: 0, z: 0 },
|
|
238
|
+
mode: "distance",
|
|
239
|
+
distanceFactor: 1.5
|
|
240
|
+
}).displayName("quadtreeUpdate");
|
|
241
|
+
const topology = param(null).displayName("topology");
|
|
242
|
+
const terrainFieldFilter = param("linear").displayName(
|
|
243
|
+
"terrainFieldFilter"
|
|
244
|
+
);
|
|
245
|
+
const elevationFn = param(() => float(0));
|
|
246
|
+
|
|
221
247
|
const defaultTerrainMeshParams = {
|
|
222
|
-
innerTileSegments
|
|
248
|
+
// Source of truth is the `innerTileSegments` param itself.
|
|
249
|
+
innerTileSegments: innerTileSegments.get(),
|
|
223
250
|
maxNodes: 1024,
|
|
224
|
-
material: new MeshStandardNodeMaterial()
|
|
251
|
+
material: new MeshStandardNodeMaterial(),
|
|
252
|
+
flipWinding: false
|
|
225
253
|
};
|
|
226
254
|
class TerrainMesh extends InstancedMesh {
|
|
227
255
|
_innerTileSegments;
|
|
228
256
|
_maxNodes;
|
|
257
|
+
_flipWinding;
|
|
229
258
|
terrainRaycast = null;
|
|
230
259
|
constructor(params = defaultTerrainMeshParams) {
|
|
231
260
|
const mergedParams = { ...defaultTerrainMeshParams, ...params };
|
|
232
|
-
const { innerTileSegments, maxNodes, material } = mergedParams;
|
|
233
|
-
const geometry = new TerrainGeometry(innerTileSegments, true);
|
|
261
|
+
const { innerTileSegments, maxNodes, material, flipWinding } = mergedParams;
|
|
262
|
+
const geometry = new TerrainGeometry(innerTileSegments, true, flipWinding);
|
|
234
263
|
super(geometry, material, maxNodes);
|
|
235
264
|
this.frustumCulled = false;
|
|
236
265
|
this._innerTileSegments = innerTileSegments;
|
|
237
266
|
this._maxNodes = maxNodes;
|
|
267
|
+
this._flipWinding = flipWinding;
|
|
238
268
|
}
|
|
239
269
|
get innerTileSegments() {
|
|
240
270
|
return this._innerTileSegments;
|
|
241
271
|
}
|
|
242
272
|
set innerTileSegments(tileSegments) {
|
|
273
|
+
if (tileSegments === this._innerTileSegments) return;
|
|
243
274
|
const oldGeometry = this.geometry;
|
|
244
|
-
this.geometry = new TerrainGeometry(tileSegments, true);
|
|
275
|
+
this.geometry = new TerrainGeometry(tileSegments, true, this._flipWinding);
|
|
245
276
|
this._innerTileSegments = tileSegments;
|
|
246
|
-
setTimeout(oldGeometry.dispose);
|
|
277
|
+
setTimeout(() => oldGeometry.dispose());
|
|
278
|
+
}
|
|
279
|
+
get flipWinding() {
|
|
280
|
+
return this._flipWinding;
|
|
281
|
+
}
|
|
282
|
+
set flipWinding(flip) {
|
|
283
|
+
if (flip === this._flipWinding) return;
|
|
284
|
+
const oldGeometry = this.geometry;
|
|
285
|
+
this.geometry = new TerrainGeometry(this._innerTileSegments, true, flip);
|
|
286
|
+
this._flipWinding = flip;
|
|
287
|
+
setTimeout(() => oldGeometry.dispose());
|
|
247
288
|
}
|
|
248
289
|
get maxNodes() {
|
|
249
290
|
return this._maxNodes;
|
|
@@ -309,13 +350,8 @@ function compileComputePipeline(stages, width, options) {
|
|
|
309
350
|
WORKGROUP_X,
|
|
310
351
|
WORKGROUP_Y
|
|
311
352
|
];
|
|
312
|
-
const preferSingleKernelWhenPossible = options?.preferSingleKernelWhenPossible ?? true;
|
|
313
353
|
const uInstanceCount = uniform(0, "uint");
|
|
314
|
-
let singleKernel;
|
|
315
354
|
const stagedKernelCache = /* @__PURE__ */ new Map();
|
|
316
|
-
function canRunSingleKernel(widthValue, limits) {
|
|
317
|
-
return widthValue <= limits.maxWorkgroupSizeX && widthValue <= limits.maxWorkgroupSizeY && widthValue * widthValue <= limits.maxWorkgroupInvocations;
|
|
318
|
-
}
|
|
319
355
|
function clampWorkgroupToLimits(requested, limits) {
|
|
320
356
|
let x = Math.max(1, Math.floor(requested[0]));
|
|
321
357
|
let y = Math.max(1, Math.floor(requested[1]));
|
|
@@ -331,37 +367,6 @@ function compileComputePipeline(stages, width, options) {
|
|
|
331
367
|
);
|
|
332
368
|
return [x, y];
|
|
333
369
|
}
|
|
334
|
-
function buildSingleKernel(workgroupSize) {
|
|
335
|
-
return Fn(() => {
|
|
336
|
-
bindings?.forEach((b) => b.toVar());
|
|
337
|
-
const fWidth = float(width);
|
|
338
|
-
const activeIndex = globalId.z;
|
|
339
|
-
const nodeIndex = int(activeIndex).toVar();
|
|
340
|
-
const iWidth = int(width);
|
|
341
|
-
const ix = int(globalId.x);
|
|
342
|
-
const iy = int(globalId.y);
|
|
343
|
-
const texelSize = vec2(1, 1).div(fWidth);
|
|
344
|
-
const localCoordinates = vec2(globalId.x, globalId.y);
|
|
345
|
-
const localUVCoords = localCoordinates.div(fWidth);
|
|
346
|
-
const verticesPerNode = iWidth.mul(iWidth);
|
|
347
|
-
const globalIndex = int(nodeIndex).mul(verticesPerNode).add(iy.mul(iWidth).add(ix));
|
|
348
|
-
const inBounds = ix.lessThan(iWidth).and(iy.lessThan(iWidth)).and(uint(activeIndex).lessThan(uInstanceCount)).toVar();
|
|
349
|
-
for (let i = 0; i < stages.length; i++) {
|
|
350
|
-
if (i > 0) {
|
|
351
|
-
workgroupBarrier();
|
|
352
|
-
}
|
|
353
|
-
If(inBounds, () => {
|
|
354
|
-
stages[i](
|
|
355
|
-
nodeIndex,
|
|
356
|
-
globalIndex,
|
|
357
|
-
localUVCoords,
|
|
358
|
-
localCoordinates,
|
|
359
|
-
texelSize
|
|
360
|
-
);
|
|
361
|
-
});
|
|
362
|
-
}
|
|
363
|
-
})().computeKernel(workgroupSize);
|
|
364
|
-
}
|
|
365
370
|
function buildStagedKernels(workgroupSize) {
|
|
366
371
|
return stages.map(
|
|
367
372
|
(stage) => Fn(() => {
|
|
@@ -392,15 +397,7 @@ function compileComputePipeline(stages, width, options) {
|
|
|
392
397
|
}
|
|
393
398
|
function execute(renderer, instanceCount) {
|
|
394
399
|
const limits = getDeviceComputeLimits(renderer);
|
|
395
|
-
const canUseSingleKernel = preferSingleKernelWhenPossible && canRunSingleKernel(width, limits);
|
|
396
400
|
uInstanceCount.value = instanceCount;
|
|
397
|
-
if (canUseSingleKernel) {
|
|
398
|
-
if (!singleKernel) {
|
|
399
|
-
singleKernel = buildSingleKernel([width, width, 1]);
|
|
400
|
-
}
|
|
401
|
-
renderer.compute(singleKernel, [1, 1, instanceCount]);
|
|
402
|
-
return;
|
|
403
|
-
}
|
|
404
401
|
const [workgroupX, workgroupY] = clampWorkgroupToLimits(
|
|
405
402
|
preferredWorkgroup,
|
|
406
403
|
limits
|
|
@@ -543,40 +540,9 @@ function AtlasBackend(edgeVertexCount, tileCount, options) {
|
|
|
543
540
|
}
|
|
544
541
|
};
|
|
545
542
|
}
|
|
546
|
-
function
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
const tex = new StorageArrayTexture(
|
|
550
|
-
edgeVertexCount,
|
|
551
|
-
edgeVertexCount,
|
|
552
|
-
tileCount
|
|
553
|
-
);
|
|
554
|
-
configureStorageTexture(tex, options.format, options.filter);
|
|
555
|
-
return {
|
|
556
|
-
backendType: "texture-3d",
|
|
557
|
-
get edgeVertexCount() {
|
|
558
|
-
return currentEdgeVertexCount;
|
|
559
|
-
},
|
|
560
|
-
get tileCount() {
|
|
561
|
-
return currentTileCount;
|
|
562
|
-
},
|
|
563
|
-
texture: tex,
|
|
564
|
-
uv(ix, iy, _tileIndex) {
|
|
565
|
-
return vec2(ix.toFloat(), iy.toFloat());
|
|
566
|
-
},
|
|
567
|
-
texel(ix, iy, tileIndex) {
|
|
568
|
-
return ivec3(ix, iy, tileIndex);
|
|
569
|
-
},
|
|
570
|
-
sample(u, v, tileIndex) {
|
|
571
|
-
return texture(tex, vec2(u, v)).depth(int(tileIndex));
|
|
572
|
-
},
|
|
573
|
-
resize(width, height, nextTileCount) {
|
|
574
|
-
currentEdgeVertexCount = width;
|
|
575
|
-
currentTileCount = nextTileCount;
|
|
576
|
-
tex.setSize(width, height, nextTileCount);
|
|
577
|
-
tex.needsUpdate = true;
|
|
578
|
-
}
|
|
579
|
-
};
|
|
543
|
+
function texture3DBackend(edgeVertexCount, tileCount, options) {
|
|
544
|
+
const storage = ArrayTextureBackend(edgeVertexCount, tileCount, options);
|
|
545
|
+
return { ...storage, backendType: "texture-3d" };
|
|
580
546
|
}
|
|
581
547
|
function tryGetDeviceLimits(renderer) {
|
|
582
548
|
const backend = renderer;
|
|
@@ -590,7 +556,7 @@ function createTerrainFieldStorage(edgeVertexCount, tileCount, renderer, options
|
|
|
590
556
|
return AtlasBackend(edgeVertexCount, tileCount, { filter, format });
|
|
591
557
|
}
|
|
592
558
|
if (forcedBackend === "texture-3d") {
|
|
593
|
-
return
|
|
559
|
+
return texture3DBackend(edgeVertexCount, tileCount, { filter, format });
|
|
594
560
|
}
|
|
595
561
|
if (forcedBackend === "array-texture") {
|
|
596
562
|
return ArrayTextureBackend(edgeVertexCount, tileCount, { filter, format });
|
|
@@ -633,10 +599,6 @@ function sampleTerrainField(storage, u, v, tileIndex) {
|
|
|
633
599
|
function sampleTerrainFieldElevation(storage, u, v, tileIndex) {
|
|
634
600
|
return sampleTerrainField(storage, u, v, tileIndex).r;
|
|
635
601
|
}
|
|
636
|
-
function sampleTerrainFieldNormal(storage, u, v, tileIndex) {
|
|
637
|
-
const raw = sampleTerrainField(storage, u, v, tileIndex);
|
|
638
|
-
return vec2(raw.g, raw.b);
|
|
639
|
-
}
|
|
640
602
|
function packTerrainFieldSample(height, normalXZ, extra = float(0)) {
|
|
641
603
|
return vec4(height, normalXZ.x, normalXZ.y, extra);
|
|
642
604
|
}
|
|
@@ -663,23 +625,132 @@ const createElevation = (tile, uniforms, elevationFn) => {
|
|
|
663
625
|
};
|
|
664
626
|
};
|
|
665
627
|
|
|
666
|
-
|
|
628
|
+
const CUBE_FACE_COUNT = 6;
|
|
629
|
+
const CUBE_FACES = [
|
|
630
|
+
// 0: +X
|
|
631
|
+
{ forward: [1, 0, 0], right: [0, 0, -1], up: [0, 1, 0] },
|
|
632
|
+
// 1: -X
|
|
633
|
+
{ forward: [-1, 0, 0], right: [0, 0, 1], up: [0, 1, 0] },
|
|
634
|
+
// 2: +Y (north pole)
|
|
635
|
+
{ forward: [0, 1, 0], right: [1, 0, 0], up: [0, 0, -1] },
|
|
636
|
+
// 3: -Y (south pole)
|
|
637
|
+
{ forward: [0, -1, 0], right: [1, 0, 0], up: [0, 0, 1] },
|
|
638
|
+
// 4: +Z
|
|
639
|
+
{ forward: [0, 0, 1], right: [1, 0, 0], up: [0, 1, 0] },
|
|
640
|
+
// 5: -Z
|
|
641
|
+
{ forward: [0, 0, -1], right: [-1, 0, 0], up: [0, 1, 0] }
|
|
642
|
+
];
|
|
643
|
+
|
|
644
|
+
function vec3Const(v) {
|
|
645
|
+
return vec3(float(v[0]), float(v[1]), float(v[2]));
|
|
646
|
+
}
|
|
647
|
+
function selectFaceVec3(face, pick) {
|
|
648
|
+
const last = CUBE_FACES.length - 1;
|
|
649
|
+
let acc = vec3Const(pick(CUBE_FACES[last]));
|
|
650
|
+
for (let i = last - 1; i >= 0; i--) {
|
|
651
|
+
acc = select(int(face).equal(int(i)), vec3Const(pick(CUBE_FACES[i])), acc);
|
|
652
|
+
}
|
|
653
|
+
return acc;
|
|
654
|
+
}
|
|
655
|
+
function cubeFaceBasis(face) {
|
|
656
|
+
return {
|
|
657
|
+
forward: selectFaceVec3(face, (f) => f.forward),
|
|
658
|
+
right: selectFaceVec3(face, (f) => f.right),
|
|
659
|
+
up: selectFaceVec3(face, (f) => f.up)
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
function cubeFacePoint(basis, u, v) {
|
|
663
|
+
const s = float(u).mul(2).sub(1);
|
|
664
|
+
const t = float(v).mul(2).sub(1);
|
|
665
|
+
return basis.forward.add(basis.right.mul(s)).add(basis.up.mul(t));
|
|
666
|
+
}
|
|
667
|
+
function cubeFaceDirection(basis, u, v) {
|
|
668
|
+
return cubeFacePoint(basis, u, v).normalize();
|
|
669
|
+
}
|
|
670
|
+
function tangentFromAxis(dir, axis) {
|
|
671
|
+
return axis.sub(dir.mul(dir.dot(axis))).normalize();
|
|
672
|
+
}
|
|
673
|
+
function unpackTangentNormal(nx, nz) {
|
|
674
|
+
const ny = float(1).sub(nx.mul(nx)).sub(nz.mul(nz)).max(float(0)).sqrt();
|
|
675
|
+
return vec3(nx, ny, nz);
|
|
676
|
+
}
|
|
677
|
+
function sphereTangentFrameNormal(dir, basis, tangentNormal) {
|
|
678
|
+
const n = vec3(tangentNormal);
|
|
679
|
+
const tu = tangentFromAxis(dir, basis.right);
|
|
680
|
+
const tv = tangentFromAxis(dir, basis.up);
|
|
681
|
+
return tu.mul(n.x).add(dir.mul(n.y)).add(tv.mul(n.z)).normalize();
|
|
682
|
+
}
|
|
683
|
+
function cubeFaceFromDirection(dir) {
|
|
684
|
+
const d = vec3(dir);
|
|
685
|
+
const ax = d.x.abs();
|
|
686
|
+
const ay = d.y.abs();
|
|
687
|
+
const az = d.z.abs();
|
|
688
|
+
const faceX = select(d.x.greaterThanEqual(float(0)), int(0), int(1));
|
|
689
|
+
const faceY = select(d.y.greaterThanEqual(float(0)), int(2), int(3));
|
|
690
|
+
const faceZ = select(d.z.greaterThanEqual(float(0)), int(4), int(5));
|
|
691
|
+
const xDominant = ax.greaterThanEqual(ay).and(ax.greaterThanEqual(az));
|
|
692
|
+
const yDominant = ay.greaterThanEqual(ax).and(ay.greaterThanEqual(az));
|
|
693
|
+
return select(xDominant, faceX, select(yDominant, faceY, faceZ));
|
|
694
|
+
}
|
|
695
|
+
function cubeFaceUVFromDirection(basis, dir) {
|
|
696
|
+
const d = vec3(dir);
|
|
697
|
+
const p = d.div(d.dot(basis.forward));
|
|
698
|
+
const s = p.dot(basis.right);
|
|
699
|
+
const t = p.dot(basis.up);
|
|
700
|
+
return vec2(s.add(float(1)).mul(float(0.5)), t.add(float(1)).mul(float(0.5)));
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
const HALF_PI = Math.PI * 0.5;
|
|
704
|
+
const FIELD_INNER_TEXEL_OFFSET = 1.5;
|
|
705
|
+
const FIELD_EDGE_EXTRA_TEXELS = 3;
|
|
706
|
+
function sphereTileArcLength(radius, levelDivisor) {
|
|
707
|
+
return radius * HALF_PI / levelDivisor;
|
|
708
|
+
}
|
|
709
|
+
function decodeLeafTile(leafStorage, nodeIndex) {
|
|
710
|
+
const nodeOffset = int(nodeIndex).mul(int(4));
|
|
711
|
+
return {
|
|
712
|
+
level: leafStorage.node.element(nodeOffset).toInt(),
|
|
713
|
+
x: leafStorage.node.element(nodeOffset.add(int(1))).toFloat(),
|
|
714
|
+
y: leafStorage.node.element(nodeOffset.add(int(2))).toFloat(),
|
|
715
|
+
face: leafStorage.node.element(nodeOffset.add(int(3))).toInt()
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
function faceUVFromTileLocal(tile, localU, localV) {
|
|
719
|
+
const n = pow(float(2), tile.level.toFloat());
|
|
720
|
+
return vec2(tile.x.add(localU).div(n), tile.y.add(localV).div(n));
|
|
721
|
+
}
|
|
722
|
+
function createTileCompute(leafStorage, uniforms, projection = "flat") {
|
|
723
|
+
const isSphere = projection === "cubeSphere";
|
|
667
724
|
const tileLevel = Fn(([nodeIndex]) => {
|
|
668
|
-
|
|
669
|
-
|
|
725
|
+
return decodeLeafTile(leafStorage, nodeIndex).level;
|
|
726
|
+
});
|
|
727
|
+
const tileFace = Fn(([nodeIndex]) => {
|
|
728
|
+
return decodeLeafTile(leafStorage, nodeIndex).face;
|
|
670
729
|
});
|
|
671
730
|
const tileOriginVec2 = Fn(([nodeIndex]) => {
|
|
672
|
-
const
|
|
673
|
-
|
|
674
|
-
const nodeY = leafStorage.node.element(nodeOffset.add(int(2))).toFloat();
|
|
675
|
-
return vec2(nodeX, nodeY);
|
|
731
|
+
const tile = decodeLeafTile(leafStorage, nodeIndex);
|
|
732
|
+
return vec2(tile.x, tile.y);
|
|
676
733
|
});
|
|
677
734
|
const tileSize = Fn(([nodeIndex]) => {
|
|
678
|
-
const rootSize = uniforms.uRootSize.toVar();
|
|
679
735
|
const level = tileLevel(nodeIndex);
|
|
680
|
-
|
|
736
|
+
const divisor = pow(float(2), level.toFloat());
|
|
737
|
+
if (isSphere) {
|
|
738
|
+
return uniforms.uRadius.toVar().mul(float(HALF_PI)).div(divisor);
|
|
739
|
+
}
|
|
740
|
+
const rootSize = uniforms.uRootSize.toVar();
|
|
741
|
+
return float(rootSize).div(divisor);
|
|
742
|
+
});
|
|
743
|
+
const tileFaceUV = Fn(([nodeIndex, ix, iy]) => {
|
|
744
|
+
const tile = decodeLeafTile(leafStorage, nodeIndex);
|
|
745
|
+
const fInnerSegments = uniforms.uInnerTileSegments.toVar().toFloat();
|
|
746
|
+
const localU = int(ix).toFloat().sub(float(1)).div(fInnerSegments);
|
|
747
|
+
const localV = int(iy).toFloat().sub(float(1)).div(fInnerSegments);
|
|
748
|
+
return faceUVFromTileLocal(tile, localU, localV);
|
|
681
749
|
});
|
|
682
750
|
const rootUVCompute = Fn(([nodeIndex, ix, iy]) => {
|
|
751
|
+
if (isSphere) {
|
|
752
|
+
return tileFaceUV(nodeIndex, ix, iy);
|
|
753
|
+
}
|
|
683
754
|
const nodeVec2 = tileOriginVec2(nodeIndex);
|
|
684
755
|
const nodeX = nodeVec2.x;
|
|
685
756
|
const nodeY = nodeVec2.y;
|
|
@@ -704,6 +775,12 @@ function createTileCompute(leafStorage, uniforms) {
|
|
|
704
775
|
const tileVertexWorldPositionCompute = Fn(
|
|
705
776
|
([nodeIndex, ix, iy]) => {
|
|
706
777
|
const rootOrigin = uniforms.uRootOrigin.toVar();
|
|
778
|
+
if (isSphere) {
|
|
779
|
+
const faceUV = tileFaceUV(nodeIndex, ix, iy);
|
|
780
|
+
const basis = cubeFaceBasis(tileFace(nodeIndex));
|
|
781
|
+
const dir = cubeFaceDirection(basis, faceUV.x, faceUV.y);
|
|
782
|
+
return rootOrigin.add(dir.mul(uniforms.uRadius.toVar()));
|
|
783
|
+
}
|
|
707
784
|
const nodeVec2 = tileOriginVec2(nodeIndex);
|
|
708
785
|
const nodeX = nodeVec2.x;
|
|
709
786
|
const nodeY = nodeVec2.y;
|
|
@@ -722,36 +799,22 @@ function createTileCompute(leafStorage, uniforms) {
|
|
|
722
799
|
);
|
|
723
800
|
return {
|
|
724
801
|
tileLevel,
|
|
802
|
+
tileFace,
|
|
725
803
|
tileOriginVec2,
|
|
726
804
|
tileSize,
|
|
805
|
+
tileFaceUV,
|
|
727
806
|
rootUVCompute,
|
|
728
807
|
tileVertexWorldPositionCompute
|
|
729
808
|
};
|
|
730
809
|
}
|
|
731
|
-
function tileLocalToFieldUV
|
|
732
|
-
const edge = float(innerSegments).add(float(
|
|
733
|
-
return float(localCoord).mul(float(innerSegments)).add(float(
|
|
810
|
+
function tileLocalToFieldUV(localCoord, innerSegments) {
|
|
811
|
+
const edge = float(innerSegments).add(float(FIELD_EDGE_EXTRA_TEXELS));
|
|
812
|
+
return float(localCoord).mul(float(innerSegments)).add(float(FIELD_INNER_TEXEL_OFFSET)).div(edge);
|
|
813
|
+
}
|
|
814
|
+
function tileLocalToFieldUVNumber(localCoord, innerSegments) {
|
|
815
|
+
const edge = innerSegments + FIELD_EDGE_EXTRA_TEXELS;
|
|
816
|
+
return (localCoord * innerSegments + FIELD_INNER_TEXEL_OFFSET) / edge;
|
|
734
817
|
}
|
|
735
|
-
|
|
736
|
-
const rootSize = param(256).displayName("rootSize");
|
|
737
|
-
const origin = param({
|
|
738
|
-
x: 0,
|
|
739
|
-
y: 0,
|
|
740
|
-
z: 0
|
|
741
|
-
}).displayName("origin");
|
|
742
|
-
const innerTileSegments = param(13).displayName("innerTileSegments");
|
|
743
|
-
const skirtScale = param(100).displayName("skirtScale");
|
|
744
|
-
const elevationScale = param(1).displayName("elevationScale");
|
|
745
|
-
const maxNodes = param(1024).displayName("maxNodes");
|
|
746
|
-
const maxLevel = param(16).displayName("maxLevel");
|
|
747
|
-
const quadtreeUpdate = param({
|
|
748
|
-
cameraOrigin: { x: 0, y: 0, z: 0 },
|
|
749
|
-
mode: "distance",
|
|
750
|
-
distanceFactor: 1.5
|
|
751
|
-
}).displayName("quadtreeUpdate");
|
|
752
|
-
const surface = param(null).displayName("surface");
|
|
753
|
-
const terrainFieldFilter = param("linear").displayName("terrainFieldFilter");
|
|
754
|
-
const elevationFn = param(() => float(0));
|
|
755
818
|
|
|
756
819
|
function createLeafStorage(maxNodes) {
|
|
757
820
|
const data = new Int32Array(maxNodes * 4);
|
|
@@ -936,10 +999,10 @@ function buildLeafIndex(leaves, out) {
|
|
|
936
999
|
return index;
|
|
937
1000
|
}
|
|
938
1001
|
|
|
939
|
-
function createState(cfg,
|
|
940
|
-
const store = createNodeStore(cfg.maxNodes,
|
|
1002
|
+
function createState(cfg, topology) {
|
|
1003
|
+
const store = createNodeStore(cfg.maxNodes, topology.spaceCount);
|
|
941
1004
|
const scratchRootTiles = [];
|
|
942
|
-
for (let i = 0; i <
|
|
1005
|
+
for (let i = 0; i < topology.maxRootCount; i++) {
|
|
943
1006
|
scratchRootTiles.push({ space: 0, level: 0, x: 0, y: 0 });
|
|
944
1007
|
}
|
|
945
1008
|
return {
|
|
@@ -949,7 +1012,7 @@ function createState(cfg, surface) {
|
|
|
949
1012
|
leafNodeIds: new Uint32Array(cfg.maxNodes),
|
|
950
1013
|
leafIndex: createSpatialIndex(cfg.maxNodes),
|
|
951
1014
|
stack: new Uint32Array(cfg.maxNodes),
|
|
952
|
-
rootNodeIds: new Uint32Array(
|
|
1015
|
+
rootNodeIds: new Uint32Array(topology.maxRootCount),
|
|
953
1016
|
rootCount: 0,
|
|
954
1017
|
splitQueue: new Uint32Array(cfg.maxNodes),
|
|
955
1018
|
splitStamp: new Uint16Array(cfg.maxNodes),
|
|
@@ -958,25 +1021,25 @@ function createState(cfg, surface) {
|
|
|
958
1021
|
scratchNeighbor: { space: 0, level: 0, x: 0, y: 0 },
|
|
959
1022
|
scratchBounds: { cx: 0, cy: 0, cz: 0, r: 0 },
|
|
960
1023
|
scratchRootTiles,
|
|
961
|
-
spaceCount:
|
|
1024
|
+
spaceCount: topology.spaceCount
|
|
962
1025
|
};
|
|
963
1026
|
}
|
|
964
|
-
function beginUpdate(state,
|
|
965
|
-
if (
|
|
1027
|
+
function beginUpdate(state, topology, params) {
|
|
1028
|
+
if (topology.spaceCount !== state.spaceCount) {
|
|
966
1029
|
throw new Error(
|
|
967
|
-
`
|
|
1030
|
+
`Topology spaceCount changed (${state.spaceCount} -> ${topology.spaceCount}). Create a new quadtree state.`
|
|
968
1031
|
);
|
|
969
1032
|
}
|
|
970
|
-
if (
|
|
1033
|
+
if (topology.maxRootCount !== state.rootNodeIds.length) {
|
|
971
1034
|
throw new Error(
|
|
972
|
-
`
|
|
1035
|
+
`Topology maxRootCount changed (${state.rootNodeIds.length} -> ${topology.maxRootCount}). Create a new quadtree state.`
|
|
973
1036
|
);
|
|
974
1037
|
}
|
|
975
1038
|
beginFrame(state.store);
|
|
976
1039
|
state.rootCount = 0;
|
|
977
|
-
const rootCount =
|
|
978
|
-
if (rootCount < 0 || rootCount >
|
|
979
|
-
throw new Error(`
|
|
1040
|
+
const rootCount = topology.rootTiles(params.cameraOrigin, state.scratchRootTiles);
|
|
1041
|
+
if (rootCount < 0 || rootCount > topology.maxRootCount) {
|
|
1042
|
+
throw new Error(`Topology returned invalid root count (${rootCount}).`);
|
|
980
1043
|
}
|
|
981
1044
|
for (let i = 0; i < rootCount; i++) {
|
|
982
1045
|
const rootId = allocNode(state.store, state.scratchRootTiles[i]);
|
|
@@ -1013,7 +1076,7 @@ function shouldSplit(bounds, level, maxLevel, params) {
|
|
|
1013
1076
|
return safeDistSq < threshold * threshold;
|
|
1014
1077
|
}
|
|
1015
1078
|
|
|
1016
|
-
function refineLeaves(state,
|
|
1079
|
+
function refineLeaves(state, topology, params, outLeaves) {
|
|
1017
1080
|
const leaves = outLeaves ?? state.leaves;
|
|
1018
1081
|
resetLeafSet(leaves);
|
|
1019
1082
|
const store = state.store;
|
|
@@ -1034,7 +1097,7 @@ function refineLeaves(state, surface, params, outLeaves) {
|
|
|
1034
1097
|
tile.x = x;
|
|
1035
1098
|
tile.y = y;
|
|
1036
1099
|
const bounds = state.scratchBounds;
|
|
1037
|
-
|
|
1100
|
+
topology.tileBounds(tile, params.cameraOrigin, bounds);
|
|
1038
1101
|
if (hasChildren(store, nodeId)) {
|
|
1039
1102
|
const base = store.firstChild[nodeId];
|
|
1040
1103
|
stack[sp++] = base + 3;
|
|
@@ -1082,7 +1145,7 @@ function scheduleSplit(state, nodeId, count) {
|
|
|
1082
1145
|
state.splitQueue[count] = nodeId;
|
|
1083
1146
|
return count + 1;
|
|
1084
1147
|
}
|
|
1085
|
-
function balance2to1(state,
|
|
1148
|
+
function balance2to1(state, topology, params, leaves) {
|
|
1086
1149
|
const maxIters = state.cfg.maxLevel + 1;
|
|
1087
1150
|
for (let iter = 0; iter < maxIters; iter++) {
|
|
1088
1151
|
const index = buildLeafIndex(leaves, state.leafIndex);
|
|
@@ -1103,7 +1166,7 @@ function balance2to1(state, surface, params, leaves) {
|
|
|
1103
1166
|
tile.x = leafX >>> shift;
|
|
1104
1167
|
tile.y = leafY >>> shift;
|
|
1105
1168
|
const neighbor = state.scratchNeighbor;
|
|
1106
|
-
if (!
|
|
1169
|
+
if (!topology.neighborSameLevel(tile, dir, neighbor)) break;
|
|
1107
1170
|
const j = lookupSpatialIndexRaw(
|
|
1108
1171
|
index,
|
|
1109
1172
|
neighbor.space,
|
|
@@ -1127,18 +1190,38 @@ function balance2to1(state, surface, params, leaves) {
|
|
|
1127
1190
|
if (base !== U32_EMPTY) anySplit = true;
|
|
1128
1191
|
}
|
|
1129
1192
|
if (!anySplit) return leaves;
|
|
1130
|
-
refineLeaves(state,
|
|
1193
|
+
refineLeaves(state, topology, params, leaves);
|
|
1131
1194
|
}
|
|
1132
1195
|
return leaves;
|
|
1133
1196
|
}
|
|
1134
1197
|
|
|
1135
|
-
function update(state,
|
|
1136
|
-
const
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
const
|
|
1140
|
-
const
|
|
1141
|
-
|
|
1198
|
+
function update(state, topology, params, outLeaves) {
|
|
1199
|
+
const cam = params.cameraOrigin;
|
|
1200
|
+
const elevation = params.elevationAtCameraXZ ?? 0;
|
|
1201
|
+
const origX = cam.x;
|
|
1202
|
+
const origY = cam.y;
|
|
1203
|
+
const origZ = cam.z;
|
|
1204
|
+
if (topology.projection === "cubeSphere") {
|
|
1205
|
+
const center = topology.center ?? { x: 0, y: 0, z: 0 };
|
|
1206
|
+
const dx = cam.x - center.x;
|
|
1207
|
+
const dy = cam.y - center.y;
|
|
1208
|
+
const dz = cam.z - center.z;
|
|
1209
|
+
const len = Math.hypot(dx, dy, dz);
|
|
1210
|
+
if (len > 1e-12) {
|
|
1211
|
+
const inv = elevation / len;
|
|
1212
|
+
cam.x -= dx * inv;
|
|
1213
|
+
cam.y -= dy * inv;
|
|
1214
|
+
cam.z -= dz * inv;
|
|
1215
|
+
}
|
|
1216
|
+
} else {
|
|
1217
|
+
cam.y -= elevation;
|
|
1218
|
+
}
|
|
1219
|
+
beginUpdate(state, topology, params);
|
|
1220
|
+
const leaves = refineLeaves(state, topology, params, outLeaves);
|
|
1221
|
+
const result = balance2to1(state, topology, params, leaves);
|
|
1222
|
+
cam.x = origX;
|
|
1223
|
+
cam.y = origY;
|
|
1224
|
+
cam.z = origZ;
|
|
1142
1225
|
return result;
|
|
1143
1226
|
}
|
|
1144
1227
|
|
|
@@ -1146,7 +1229,7 @@ const scratchTile = { space: 0, level: 0, x: 0, y: 0 };
|
|
|
1146
1229
|
const scratchNbr = { space: 0, level: 0, x: 0, y: 0 };
|
|
1147
1230
|
const scratchParentTile = { space: 0, level: 0, x: 0, y: 0 };
|
|
1148
1231
|
const scratchParentNbr = { space: 0, level: 0, x: 0, y: 0 };
|
|
1149
|
-
function buildSeams2to1(
|
|
1232
|
+
function buildSeams2to1(topology, leaves, outSeams, outIndex) {
|
|
1150
1233
|
if (outSeams.capacity < leaves.count) {
|
|
1151
1234
|
throw new Error("SeamTable capacity is smaller than LeafSet.count.");
|
|
1152
1235
|
}
|
|
@@ -1167,7 +1250,7 @@ function buildSeams2to1(surface, leaves, outSeams, outIndex) {
|
|
|
1167
1250
|
scratchTile.level = level;
|
|
1168
1251
|
scratchTile.x = x;
|
|
1169
1252
|
scratchTile.y = y;
|
|
1170
|
-
if (!
|
|
1253
|
+
if (!topology.neighborSameLevel(scratchTile, dir, scratchNbr)) continue;
|
|
1171
1254
|
let j = lookupSpatialIndexRaw(index, scratchNbr.space, scratchNbr.level, scratchNbr.x, scratchNbr.y);
|
|
1172
1255
|
if (j !== U32_EMPTY) {
|
|
1173
1256
|
neighbors[outOffset + 0] = j;
|
|
@@ -1180,7 +1263,7 @@ function buildSeams2to1(surface, leaves, outSeams, outIndex) {
|
|
|
1180
1263
|
scratchParentTile.level = level - 1;
|
|
1181
1264
|
scratchParentTile.x = px;
|
|
1182
1265
|
scratchParentTile.y = py;
|
|
1183
|
-
if (
|
|
1266
|
+
if (topology.neighborSameLevel(scratchParentTile, dir, scratchParentNbr)) {
|
|
1184
1267
|
j = lookupSpatialIndexRaw(
|
|
1185
1268
|
index,
|
|
1186
1269
|
scratchParentNbr.space,
|
|
@@ -1236,10 +1319,10 @@ function buildSeams2to1(surface, leaves, outSeams, outIndex) {
|
|
|
1236
1319
|
return outSeams;
|
|
1237
1320
|
}
|
|
1238
1321
|
|
|
1239
|
-
function
|
|
1322
|
+
function createFlatTopology(cfg) {
|
|
1240
1323
|
const halfRoot = 0.5 * cfg.rootSize;
|
|
1241
1324
|
const maxHeight = cfg.maxHeight ?? 0;
|
|
1242
|
-
const
|
|
1325
|
+
const topology = {
|
|
1243
1326
|
spaceCount: 1,
|
|
1244
1327
|
maxRootCount: 1,
|
|
1245
1328
|
neighborSameLevel(tile, dir, out) {
|
|
@@ -1294,10 +1377,10 @@ function createFlatSurface(cfg) {
|
|
|
1294
1377
|
return 1;
|
|
1295
1378
|
}
|
|
1296
1379
|
};
|
|
1297
|
-
return
|
|
1380
|
+
return topology;
|
|
1298
1381
|
}
|
|
1299
1382
|
|
|
1300
|
-
function
|
|
1383
|
+
function createInfiniteFlatTopology(cfg) {
|
|
1301
1384
|
const halfRoot = 0.5 * cfg.rootSize;
|
|
1302
1385
|
const maxHeight = cfg.maxHeight ?? 0;
|
|
1303
1386
|
const rootGridRadius = Math.max(0, Math.floor(cfg.rootGridRadius ?? 1));
|
|
@@ -1361,18 +1444,158 @@ function createInfiniteFlatSurface(cfg) {
|
|
|
1361
1444
|
};
|
|
1362
1445
|
}
|
|
1363
1446
|
|
|
1364
|
-
|
|
1447
|
+
const DEG_TO_RAD = Math.PI / 180;
|
|
1448
|
+
const RAD_TO_DEG = 180 / Math.PI;
|
|
1449
|
+
function dot(a, b) {
|
|
1450
|
+
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
1451
|
+
}
|
|
1452
|
+
function faceUVToCube(face, u, v, out) {
|
|
1453
|
+
const f = CUBE_FACES[face];
|
|
1454
|
+
const s = 2 * u - 1;
|
|
1455
|
+
const t = 2 * v - 1;
|
|
1456
|
+
out[0] = f.forward[0] + s * f.right[0] + t * f.up[0];
|
|
1457
|
+
out[1] = f.forward[1] + s * f.right[1] + t * f.up[1];
|
|
1458
|
+
out[2] = f.forward[2] + s * f.right[2] + t * f.up[2];
|
|
1459
|
+
}
|
|
1460
|
+
function directionToFace(d) {
|
|
1461
|
+
const ax = Math.abs(d[0]);
|
|
1462
|
+
const ay = Math.abs(d[1]);
|
|
1463
|
+
const az = Math.abs(d[2]);
|
|
1464
|
+
if (ax >= ay && ax >= az) return d[0] >= 0 ? 0 : 1;
|
|
1465
|
+
if (ay >= ax && ay >= az) return d[1] >= 0 ? 2 : 3;
|
|
1466
|
+
return d[2] >= 0 ? 4 : 5;
|
|
1467
|
+
}
|
|
1468
|
+
function directionToFaceUV(face, d, out) {
|
|
1469
|
+
const f = CUBE_FACES[face];
|
|
1470
|
+
const denom = dot(d, f.forward);
|
|
1471
|
+
const inv = 1 / denom;
|
|
1472
|
+
const px = d[0] * inv;
|
|
1473
|
+
const py = d[1] * inv;
|
|
1474
|
+
const pz = d[2] * inv;
|
|
1475
|
+
const p = [px, py, pz];
|
|
1476
|
+
const s = dot(p, f.right);
|
|
1477
|
+
const t = dot(p, f.up);
|
|
1478
|
+
out[0] = (s + 1) * 0.5;
|
|
1479
|
+
out[1] = (t + 1) * 0.5;
|
|
1480
|
+
}
|
|
1481
|
+
function latLongToDirection(latDeg, lonDeg, out) {
|
|
1482
|
+
const lat = latDeg * DEG_TO_RAD;
|
|
1483
|
+
const lon = lonDeg * DEG_TO_RAD;
|
|
1484
|
+
const cosLat = Math.cos(lat);
|
|
1485
|
+
out[0] = cosLat * Math.sin(lon);
|
|
1486
|
+
out[1] = Math.sin(lat);
|
|
1487
|
+
out[2] = cosLat * Math.cos(lon);
|
|
1488
|
+
}
|
|
1489
|
+
function directionToLatLong(d) {
|
|
1490
|
+
const len = Math.hypot(d[0], d[1], d[2]) || 1;
|
|
1491
|
+
const y = Math.max(-1, Math.min(1, d[1] / len));
|
|
1492
|
+
return {
|
|
1493
|
+
latitude: Math.asin(y) * RAD_TO_DEG,
|
|
1494
|
+
longitude: Math.atan2(d[0], d[2]) * RAD_TO_DEG
|
|
1495
|
+
};
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
function createCubeSphereTopology(cfg) {
|
|
1499
|
+
const radius = cfg.radius;
|
|
1500
|
+
const maxHeight = cfg.maxHeight ?? 0;
|
|
1501
|
+
const center = cfg.center ?? { x: 0, y: 0, z: 0 };
|
|
1502
|
+
const cube = [0, 0, 0];
|
|
1503
|
+
const uv = [0, 0];
|
|
1504
|
+
function crossFaceNeighbor(face, level, nx, ny, out) {
|
|
1505
|
+
const n = 1 << level;
|
|
1506
|
+
const u = (nx + 0.5) / n;
|
|
1507
|
+
const v = (ny + 0.5) / n;
|
|
1508
|
+
faceUVToCube(face, u, v, cube);
|
|
1509
|
+
const len = Math.hypot(cube[0], cube[1], cube[2]);
|
|
1510
|
+
const dir = [cube[0] / len, cube[1] / len, cube[2] / len];
|
|
1511
|
+
const nbrFace = directionToFace(dir);
|
|
1512
|
+
directionToFaceUV(nbrFace, dir, uv);
|
|
1513
|
+
let bx = Math.floor(uv[0] * n);
|
|
1514
|
+
let by = Math.floor(uv[1] * n);
|
|
1515
|
+
if (bx < 0) bx = 0;
|
|
1516
|
+
else if (bx > n - 1) bx = n - 1;
|
|
1517
|
+
if (by < 0) by = 0;
|
|
1518
|
+
else if (by > n - 1) by = n - 1;
|
|
1519
|
+
out.space = nbrFace;
|
|
1520
|
+
out.level = level;
|
|
1521
|
+
out.x = bx;
|
|
1522
|
+
out.y = by;
|
|
1523
|
+
}
|
|
1365
1524
|
return {
|
|
1366
1525
|
spaceCount: 6,
|
|
1367
1526
|
maxRootCount: 6,
|
|
1368
|
-
neighborSameLevel(
|
|
1369
|
-
|
|
1527
|
+
neighborSameLevel(tile, dir, out) {
|
|
1528
|
+
const level = tile.level;
|
|
1529
|
+
const n = 1 << level;
|
|
1530
|
+
let nx = tile.x;
|
|
1531
|
+
let ny = tile.y;
|
|
1532
|
+
switch (dir) {
|
|
1533
|
+
case 0:
|
|
1534
|
+
nx -= 1;
|
|
1535
|
+
break;
|
|
1536
|
+
case 1:
|
|
1537
|
+
nx += 1;
|
|
1538
|
+
break;
|
|
1539
|
+
case 2:
|
|
1540
|
+
ny -= 1;
|
|
1541
|
+
break;
|
|
1542
|
+
case 3:
|
|
1543
|
+
ny += 1;
|
|
1544
|
+
break;
|
|
1545
|
+
}
|
|
1546
|
+
if (nx >= 0 && ny >= 0 && nx < n && ny < n) {
|
|
1547
|
+
out.space = tile.space;
|
|
1548
|
+
out.level = level;
|
|
1549
|
+
out.x = nx;
|
|
1550
|
+
out.y = ny;
|
|
1551
|
+
return true;
|
|
1552
|
+
}
|
|
1553
|
+
crossFaceNeighbor(tile.space, level, nx, ny, out);
|
|
1554
|
+
return true;
|
|
1370
1555
|
},
|
|
1371
|
-
tileBounds(
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1556
|
+
tileBounds(tile, cameraOrigin, out) {
|
|
1557
|
+
const level = tile.level;
|
|
1558
|
+
const n = 1 << level;
|
|
1559
|
+
const u0 = tile.x / n;
|
|
1560
|
+
const u1 = (tile.x + 1) / n;
|
|
1561
|
+
const v0 = tile.y / n;
|
|
1562
|
+
const v1 = (tile.y + 1) / n;
|
|
1563
|
+
const cornersU = [u0, u1, u0, u1];
|
|
1564
|
+
const cornersV = [v0, v0, v1, v1];
|
|
1565
|
+
let sumX = 0;
|
|
1566
|
+
let sumY = 0;
|
|
1567
|
+
let sumZ = 0;
|
|
1568
|
+
const px = [0, 0, 0, 0];
|
|
1569
|
+
const py = [0, 0, 0, 0];
|
|
1570
|
+
const pz = [0, 0, 0, 0];
|
|
1571
|
+
for (let i = 0; i < 4; i++) {
|
|
1572
|
+
faceUVToCube(tile.space, cornersU[i], cornersV[i], cube);
|
|
1573
|
+
const len = Math.hypot(cube[0], cube[1], cube[2]);
|
|
1574
|
+
const sx = center.x + cube[0] / len * radius;
|
|
1575
|
+
const sy = center.y + cube[1] / len * radius;
|
|
1576
|
+
const sz = center.z + cube[2] / len * radius;
|
|
1577
|
+
px[i] = sx;
|
|
1578
|
+
py[i] = sy;
|
|
1579
|
+
pz[i] = sz;
|
|
1580
|
+
sumX += sx;
|
|
1581
|
+
sumY += sy;
|
|
1582
|
+
sumZ += sz;
|
|
1583
|
+
}
|
|
1584
|
+
const cX = sumX * 0.25;
|
|
1585
|
+
const cY = sumY * 0.25;
|
|
1586
|
+
const cZ = sumZ * 0.25;
|
|
1587
|
+
let maxDistSq = 0;
|
|
1588
|
+
for (let i = 0; i < 4; i++) {
|
|
1589
|
+
const dx = px[i] - cX;
|
|
1590
|
+
const dy = py[i] - cY;
|
|
1591
|
+
const dz = pz[i] - cZ;
|
|
1592
|
+
const dSq = dx * dx + dy * dy + dz * dz;
|
|
1593
|
+
if (dSq > maxDistSq) maxDistSq = dSq;
|
|
1594
|
+
}
|
|
1595
|
+
out.cx = cX - cameraOrigin.x;
|
|
1596
|
+
out.cy = cY - cameraOrigin.y;
|
|
1597
|
+
out.cz = cZ - cameraOrigin.z;
|
|
1598
|
+
out.r = Math.sqrt(maxDistSq) + maxHeight;
|
|
1376
1599
|
},
|
|
1377
1600
|
rootTiles(_cameraOrigin, out) {
|
|
1378
1601
|
for (let s = 0; s < 6; s++) {
|
|
@@ -1383,10 +1606,141 @@ function createCubeSphereSurface(_cfg) {
|
|
|
1383
1606
|
root.y = 0;
|
|
1384
1607
|
}
|
|
1385
1608
|
return 6;
|
|
1386
|
-
}
|
|
1609
|
+
},
|
|
1610
|
+
projection: "cubeSphere",
|
|
1611
|
+
radius,
|
|
1612
|
+
center
|
|
1387
1613
|
};
|
|
1388
1614
|
}
|
|
1389
1615
|
|
|
1616
|
+
function readHeight(elevation, shape, leafIndex, ix, iy) {
|
|
1617
|
+
const base = leafIndex * shape.verticesPerNode;
|
|
1618
|
+
return elevation[base + iy * shape.edgeVertexCount + ix] ?? 0;
|
|
1619
|
+
}
|
|
1620
|
+
function sampleGridBilinear(elevation, shape, leafIndex, gx, gy) {
|
|
1621
|
+
const max = shape.edgeVertexCount - 1;
|
|
1622
|
+
const x = Math.max(0, Math.min(max, gx));
|
|
1623
|
+
const y = Math.max(0, Math.min(max, gy));
|
|
1624
|
+
const x0 = Math.floor(x);
|
|
1625
|
+
const y0 = Math.floor(y);
|
|
1626
|
+
const x1 = Math.min(max, x0 + 1);
|
|
1627
|
+
const y1 = Math.min(max, y0 + 1);
|
|
1628
|
+
const tx = x - x0;
|
|
1629
|
+
const ty = y - y0;
|
|
1630
|
+
const h00 = readHeight(elevation, shape, leafIndex, x0, y0);
|
|
1631
|
+
const h10 = readHeight(elevation, shape, leafIndex, x1, y0);
|
|
1632
|
+
const h01 = readHeight(elevation, shape, leafIndex, x0, y1);
|
|
1633
|
+
const h11 = readHeight(elevation, shape, leafIndex, x1, y1);
|
|
1634
|
+
const hx0 = h00 + (h10 - h00) * tx;
|
|
1635
|
+
const hx1 = h01 + (h11 - h01) * tx;
|
|
1636
|
+
return hx0 + (hx1 - hx0) * ty;
|
|
1637
|
+
}
|
|
1638
|
+
function elevationGradientAt(elevation, shape, leafIndex, gx, gy, stepWorld, elevationScale, out) {
|
|
1639
|
+
const hLeft = sampleGridBilinear(elevation, shape, leafIndex, gx - 1, gy);
|
|
1640
|
+
const hRight = sampleGridBilinear(elevation, shape, leafIndex, gx + 1, gy);
|
|
1641
|
+
const hUp = sampleGridBilinear(elevation, shape, leafIndex, gx, gy - 1);
|
|
1642
|
+
const hDown = sampleGridBilinear(elevation, shape, leafIndex, gx, gy + 1);
|
|
1643
|
+
const inv2Step = 0.5 / stepWorld;
|
|
1644
|
+
out.dhdu = (hRight - hLeft) * elevationScale * inv2Step;
|
|
1645
|
+
out.dhdv = (hDown - hUp) * elevationScale * inv2Step;
|
|
1646
|
+
return out;
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
const MISSED_LOOKUP = Object.freeze({
|
|
1650
|
+
found: false,
|
|
1651
|
+
leafIndex: -1,
|
|
1652
|
+
space: -1,
|
|
1653
|
+
level: -1,
|
|
1654
|
+
tileX: -1,
|
|
1655
|
+
tileY: -1,
|
|
1656
|
+
tileSize: 0,
|
|
1657
|
+
localU: 0,
|
|
1658
|
+
localV: 0
|
|
1659
|
+
});
|
|
1660
|
+
function lookupTile(index, config, worldX, worldZ) {
|
|
1661
|
+
const halfRoot = config.rootSize * 0.5;
|
|
1662
|
+
for (let level = config.maxLevel; level >= 0; level -= 1) {
|
|
1663
|
+
const scale = 2 ** level;
|
|
1664
|
+
const tileSize = config.rootSize / scale;
|
|
1665
|
+
const tileX = Math.floor((worldX - config.originX + halfRoot) / tileSize);
|
|
1666
|
+
const tileY = Math.floor((worldZ - config.originZ + halfRoot) / tileSize);
|
|
1667
|
+
const leafIndex = lookupSpatialIndexRaw(index, 0, level, tileX, tileY);
|
|
1668
|
+
if (leafIndex !== U32_EMPTY) {
|
|
1669
|
+
const tileMinX = config.originX + tileX * tileSize - halfRoot;
|
|
1670
|
+
const tileMinZ = config.originZ + tileY * tileSize - halfRoot;
|
|
1671
|
+
return {
|
|
1672
|
+
found: true,
|
|
1673
|
+
leafIndex,
|
|
1674
|
+
space: 0,
|
|
1675
|
+
level,
|
|
1676
|
+
tileX,
|
|
1677
|
+
tileY,
|
|
1678
|
+
tileSize,
|
|
1679
|
+
localU: (worldX - tileMinX) / tileSize,
|
|
1680
|
+
localV: (worldZ - tileMinZ) / tileSize
|
|
1681
|
+
};
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
return MISSED_LOOKUP;
|
|
1685
|
+
}
|
|
1686
|
+
function clamp01(value) {
|
|
1687
|
+
return value < 0 ? 0 : value > 1 ? 1 : value;
|
|
1688
|
+
}
|
|
1689
|
+
function lookupTileByFaceUV(index, config, face, u, v) {
|
|
1690
|
+
for (let level = config.maxLevel; level >= 0; level -= 1) {
|
|
1691
|
+
const n = 2 ** level;
|
|
1692
|
+
let tileX = Math.floor(u * n);
|
|
1693
|
+
let tileY = Math.floor(v * n);
|
|
1694
|
+
if (tileX < 0) tileX = 0;
|
|
1695
|
+
else if (tileX > n - 1) tileX = n - 1;
|
|
1696
|
+
if (tileY < 0) tileY = 0;
|
|
1697
|
+
else if (tileY > n - 1) tileY = n - 1;
|
|
1698
|
+
const leafIndex = lookupSpatialIndexRaw(index, face, level, tileX, tileY);
|
|
1699
|
+
if (leafIndex !== U32_EMPTY) {
|
|
1700
|
+
const tileSize = sphereTileArcLength(config.radius, n);
|
|
1701
|
+
return {
|
|
1702
|
+
found: true,
|
|
1703
|
+
leafIndex,
|
|
1704
|
+
space: face,
|
|
1705
|
+
level,
|
|
1706
|
+
tileX,
|
|
1707
|
+
tileY,
|
|
1708
|
+
tileSize,
|
|
1709
|
+
localU: clamp01(u * n - tileX),
|
|
1710
|
+
localV: clamp01(v * n - tileY)
|
|
1711
|
+
};
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
return MISSED_LOOKUP;
|
|
1715
|
+
}
|
|
1716
|
+
function lookupTileForDirection(index, config, dx, dy, dz, dirScratch, uvScratch) {
|
|
1717
|
+
if (config.projection !== "cubeSphere") return MISSED_LOOKUP;
|
|
1718
|
+
const len = Math.hypot(dx, dy, dz);
|
|
1719
|
+
if (len === 0) return MISSED_LOOKUP;
|
|
1720
|
+
dirScratch[0] = dx / len;
|
|
1721
|
+
dirScratch[1] = dy / len;
|
|
1722
|
+
dirScratch[2] = dz / len;
|
|
1723
|
+
const face = directionToFace(dirScratch);
|
|
1724
|
+
directionToFaceUV(face, dirScratch, uvScratch);
|
|
1725
|
+
return lookupTileByFaceUV(index, config, face, uvScratch[0], uvScratch[1]);
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
function createTerrainSnapshotState(maxNodes, totalElements) {
|
|
1729
|
+
return {
|
|
1730
|
+
frontElevation: new Float32Array(totalElements),
|
|
1731
|
+
backElevation: new Float32Array(totalElements),
|
|
1732
|
+
frontIndex: createSpatialIndex(maxNodes),
|
|
1733
|
+
backIndex: createSpatialIndex(maxNodes),
|
|
1734
|
+
frontTileBounds: new Float32Array(maxNodes * 2),
|
|
1735
|
+
backTileBounds: new Float32Array(maxNodes * 2),
|
|
1736
|
+
frontLeafCount: 0,
|
|
1737
|
+
globalRange: null,
|
|
1738
|
+
hasSnapshot: false,
|
|
1739
|
+
readbackPending: false,
|
|
1740
|
+
generation: 0,
|
|
1741
|
+
lastScheduledStampGen: -1
|
|
1742
|
+
};
|
|
1743
|
+
}
|
|
1390
1744
|
function cloneSpatialIndex(target, source) {
|
|
1391
1745
|
if (target.size !== source.size) {
|
|
1392
1746
|
throw new Error(
|
|
@@ -1402,218 +1756,278 @@ function cloneSpatialIndex(target, source) {
|
|
|
1402
1756
|
target.keysY.set(source.keysY);
|
|
1403
1757
|
target.values.set(source.values);
|
|
1404
1758
|
}
|
|
1405
|
-
function
|
|
1406
|
-
|
|
1407
|
-
|
|
1759
|
+
function triggerSnapshotReadback(state, renderer, attribute, spatialIndex, boundsAttribute, captured) {
|
|
1760
|
+
if (state.readbackPending) return;
|
|
1761
|
+
const withReadback = renderer;
|
|
1762
|
+
if (!withReadback.getArrayBufferAsync) return;
|
|
1763
|
+
if (spatialIndex.stampGen === state.lastScheduledStampGen) return;
|
|
1764
|
+
cloneSpatialIndex(state.backIndex, spatialIndex);
|
|
1765
|
+
state.lastScheduledStampGen = spatialIndex.stampGen;
|
|
1766
|
+
const { activeLeafCount, totalElements, elevationScale, originY } = captured;
|
|
1767
|
+
state.readbackPending = true;
|
|
1768
|
+
const elevationPromise = withReadback.getArrayBufferAsync(attribute);
|
|
1769
|
+
const boundsPromise = boundsAttribute ? withReadback.getArrayBufferAsync(boundsAttribute) : null;
|
|
1770
|
+
const onComplete = (elevResult, boundsResult) => {
|
|
1771
|
+
const data = new Float32Array(elevResult);
|
|
1772
|
+
state.backElevation.fill(0);
|
|
1773
|
+
state.backElevation.set(data.subarray(0, totalElements));
|
|
1774
|
+
let boundsValid = activeLeafCount === 0;
|
|
1775
|
+
if (boundsResult) {
|
|
1776
|
+
const rawBounds = new Float32Array(boundsResult);
|
|
1777
|
+
state.backTileBounds.fill(0);
|
|
1778
|
+
state.backTileBounds.set(rawBounds.subarray(0, activeLeafCount * 2));
|
|
1779
|
+
for (let i = 0; i < activeLeafCount; i += 1) {
|
|
1780
|
+
if ((rawBounds[i * 2 + 1] ?? 0) !== 0) {
|
|
1781
|
+
boundsValid = true;
|
|
1782
|
+
break;
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
const oldFrontElevation = state.frontElevation;
|
|
1787
|
+
const oldFrontIndex = state.frontIndex;
|
|
1788
|
+
state.frontElevation = state.backElevation;
|
|
1789
|
+
state.frontIndex = state.backIndex;
|
|
1790
|
+
state.frontLeafCount = activeLeafCount;
|
|
1791
|
+
state.backElevation = oldFrontElevation;
|
|
1792
|
+
state.backIndex = oldFrontIndex;
|
|
1793
|
+
if (boundsResult && boundsValid) {
|
|
1794
|
+
const oldFrontBounds = state.frontTileBounds;
|
|
1795
|
+
state.frontTileBounds = state.backTileBounds;
|
|
1796
|
+
state.backTileBounds = oldFrontBounds;
|
|
1797
|
+
}
|
|
1798
|
+
if (boundsResult && boundsValid && activeLeafCount > 0) {
|
|
1799
|
+
let gMin = Infinity;
|
|
1800
|
+
let gMax = -Infinity;
|
|
1801
|
+
for (let i = 0; i < activeLeafCount; i++) {
|
|
1802
|
+
const rawMin = state.frontTileBounds[i * 2];
|
|
1803
|
+
const rawMax = state.frontTileBounds[i * 2 + 1];
|
|
1804
|
+
const a = originY + rawMin * elevationScale;
|
|
1805
|
+
const b = originY + rawMax * elevationScale;
|
|
1806
|
+
gMin = Math.min(gMin, a, b);
|
|
1807
|
+
gMax = Math.max(gMax, a, b);
|
|
1808
|
+
}
|
|
1809
|
+
state.globalRange = { min: gMin, max: gMax };
|
|
1810
|
+
}
|
|
1811
|
+
state.hasSnapshot = true;
|
|
1812
|
+
state.generation += 1;
|
|
1813
|
+
};
|
|
1814
|
+
if (boundsPromise) {
|
|
1815
|
+
Promise.all([elevationPromise, boundsPromise]).then(([elev, bounds]) => onComplete(elev, bounds)).finally(() => {
|
|
1816
|
+
state.readbackPending = false;
|
|
1817
|
+
});
|
|
1818
|
+
} else {
|
|
1819
|
+
elevationPromise.then((elev) => onComplete(elev, null)).finally(() => {
|
|
1820
|
+
state.readbackPending = false;
|
|
1821
|
+
});
|
|
1822
|
+
}
|
|
1408
1823
|
}
|
|
1824
|
+
|
|
1409
1825
|
function createCpuTerrainCache(maxNodes, initialConfig) {
|
|
1410
1826
|
let config = initialConfig;
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
let frontElevation = new Float32Array(totalElements);
|
|
1415
|
-
let backElevation = new Float32Array(totalElements);
|
|
1416
|
-
let frontIndex = createSpatialIndex(maxNodes);
|
|
1417
|
-
let backIndex = createSpatialIndex(maxNodes);
|
|
1418
|
-
let frontTileBounds = new Float32Array(maxNodes * 2);
|
|
1419
|
-
let backTileBounds = new Float32Array(maxNodes * 2);
|
|
1420
|
-
let frontLeafCount = 0;
|
|
1421
|
-
let globalRange = null;
|
|
1422
|
-
let hasSnapshot = false;
|
|
1423
|
-
let readbackPending = false;
|
|
1424
|
-
let generationCount = 0;
|
|
1425
|
-
let lastScheduledStampGen = -1;
|
|
1426
|
-
const readHeight = (leafIndex, ix, iy) => {
|
|
1427
|
-
const base = leafIndex * verticesPerNode;
|
|
1428
|
-
return frontElevation[base + iy * edgeVertexCount + ix] ?? 0;
|
|
1827
|
+
const shape = {
|
|
1828
|
+
edgeVertexCount: config.innerTileSegments + 3,
|
|
1829
|
+
verticesPerNode: 0
|
|
1429
1830
|
};
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
const
|
|
1443
|
-
const
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
return
|
|
1831
|
+
shape.verticesPerNode = shape.edgeVertexCount * shape.edgeVertexCount;
|
|
1832
|
+
let totalElements = maxNodes * shape.verticesPerNode;
|
|
1833
|
+
const state = createTerrainSnapshotState(
|
|
1834
|
+
maxNodes,
|
|
1835
|
+
totalElements
|
|
1836
|
+
);
|
|
1837
|
+
const dirScratch = [0, 0, 0];
|
|
1838
|
+
const uvScratch = [0, 0];
|
|
1839
|
+
const llScratch = [0, 0, 0];
|
|
1840
|
+
const gridScratch = { gx: 0, gy: 0 };
|
|
1841
|
+
const gradientScratch = { dhdu: 0, dhdv: 0 };
|
|
1842
|
+
const gridCoordsFromLookup = (lookup) => {
|
|
1843
|
+
const fieldU = tileLocalToFieldUVNumber(lookup.localU, config.innerTileSegments);
|
|
1844
|
+
const fieldV = tileLocalToFieldUVNumber(lookup.localV, config.innerTileSegments);
|
|
1845
|
+
gridScratch.gx = fieldU * (shape.edgeVertexCount - 1);
|
|
1846
|
+
gridScratch.gy = fieldV * (shape.edgeVertexCount - 1);
|
|
1847
|
+
return gridScratch;
|
|
1848
|
+
};
|
|
1849
|
+
const rawHeightFromLookup = (lookup) => {
|
|
1850
|
+
const g = gridCoordsFromLookup(lookup);
|
|
1851
|
+
return sampleGridBilinear(state.frontElevation, shape, lookup.leafIndex, g.gx, g.gy);
|
|
1447
1852
|
};
|
|
1448
1853
|
const computeNormal = (leafIndex, gx, gy, tileSize) => {
|
|
1449
|
-
const hLeft = sampleGridBilinear(leafIndex, gx - 1, gy);
|
|
1450
|
-
const hRight = sampleGridBilinear(leafIndex, gx + 1, gy);
|
|
1451
|
-
const hUp = sampleGridBilinear(leafIndex, gx, gy - 1);
|
|
1452
|
-
const hDown = sampleGridBilinear(leafIndex, gx, gy + 1);
|
|
1453
1854
|
const stepWorld = tileSize / config.innerTileSegments;
|
|
1454
|
-
const
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1855
|
+
const { dhdu, dhdv } = elevationGradientAt(
|
|
1856
|
+
state.frontElevation,
|
|
1857
|
+
shape,
|
|
1858
|
+
leafIndex,
|
|
1859
|
+
gx,
|
|
1860
|
+
gy,
|
|
1861
|
+
stepWorld,
|
|
1862
|
+
config.elevationScale,
|
|
1863
|
+
gradientScratch
|
|
1864
|
+
);
|
|
1865
|
+
return new Vector3(-dhdu, 1, -dhdv).normalize();
|
|
1458
1866
|
};
|
|
1459
|
-
const
|
|
1460
|
-
const
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
localV: 0
|
|
1497
|
-
};
|
|
1867
|
+
const computeSphereNormal = (leafIndex, gx, gy, tileSize, face, dirX, dirY, dirZ) => {
|
|
1868
|
+
const stepWorld = tileSize / config.innerTileSegments;
|
|
1869
|
+
const { dhdu, dhdv } = elevationGradientAt(
|
|
1870
|
+
state.frontElevation,
|
|
1871
|
+
shape,
|
|
1872
|
+
leafIndex,
|
|
1873
|
+
gx,
|
|
1874
|
+
gy,
|
|
1875
|
+
stepWorld,
|
|
1876
|
+
config.elevationScale,
|
|
1877
|
+
gradientScratch
|
|
1878
|
+
);
|
|
1879
|
+
const f = CUBE_FACES[face];
|
|
1880
|
+
const dDotR = dirX * f.right[0] + dirY * f.right[1] + dirZ * f.right[2];
|
|
1881
|
+
let tux = f.right[0] - dirX * dDotR;
|
|
1882
|
+
let tuy = f.right[1] - dirY * dDotR;
|
|
1883
|
+
let tuz = f.right[2] - dirZ * dDotR;
|
|
1884
|
+
const tuLen = Math.hypot(tux, tuy, tuz) || 1;
|
|
1885
|
+
tux /= tuLen;
|
|
1886
|
+
tuy /= tuLen;
|
|
1887
|
+
tuz /= tuLen;
|
|
1888
|
+
const dDotU = dirX * f.up[0] + dirY * f.up[1] + dirZ * f.up[2];
|
|
1889
|
+
let tvx = f.up[0] - dirX * dDotU;
|
|
1890
|
+
let tvy = f.up[1] - dirY * dDotU;
|
|
1891
|
+
let tvz = f.up[2] - dirZ * dDotU;
|
|
1892
|
+
const tvLen = Math.hypot(tvx, tvy, tvz) || 1;
|
|
1893
|
+
tvx /= tvLen;
|
|
1894
|
+
tvy /= tvLen;
|
|
1895
|
+
tvz /= tvLen;
|
|
1896
|
+
const nx = -dhdu;
|
|
1897
|
+
const ny = 1;
|
|
1898
|
+
const nz = -dhdv;
|
|
1899
|
+
return new Vector3(
|
|
1900
|
+
tux * nx + dirX * ny + tvx * nz,
|
|
1901
|
+
tuy * nx + dirY * ny + tvy * nz,
|
|
1902
|
+
tuz * nx + dirZ * ny + tvz * nz
|
|
1903
|
+
).normalize();
|
|
1498
1904
|
};
|
|
1499
1905
|
const sampleFromLookup = (lookup) => {
|
|
1500
|
-
const
|
|
1501
|
-
const fieldV = tileLocalToFieldUV(lookup.localV, config.innerTileSegments);
|
|
1502
|
-
const gx = fieldU * (edgeVertexCount - 1);
|
|
1503
|
-
const gy = fieldV * (edgeVertexCount - 1);
|
|
1504
|
-
const height = sampleGridBilinear(lookup.leafIndex, gx, gy);
|
|
1906
|
+
const height = rawHeightFromLookup(lookup);
|
|
1505
1907
|
const scaledHeight = config.originY + height * config.elevationScale;
|
|
1506
|
-
const normal = computeNormal(
|
|
1908
|
+
const normal = computeNormal(
|
|
1909
|
+
lookup.leafIndex,
|
|
1910
|
+
gridScratch.gx,
|
|
1911
|
+
gridScratch.gy,
|
|
1912
|
+
lookup.tileSize
|
|
1913
|
+
);
|
|
1507
1914
|
return { elevation: scaledHeight, normal, valid: true };
|
|
1508
1915
|
};
|
|
1509
|
-
const sampleElevationFromLookup = (lookup) => {
|
|
1510
|
-
const fieldU = tileLocalToFieldUV(lookup.localU, config.innerTileSegments);
|
|
1511
|
-
const fieldV = tileLocalToFieldUV(lookup.localV, config.innerTileSegments);
|
|
1512
|
-
const gx = fieldU * (edgeVertexCount - 1);
|
|
1513
|
-
const gy = fieldV * (edgeVertexCount - 1);
|
|
1514
|
-
const height = sampleGridBilinear(lookup.leafIndex, gx, gy);
|
|
1515
|
-
const scaledHeight = config.originY + height * config.elevationScale;
|
|
1516
|
-
return { elevation: scaledHeight, valid: true };
|
|
1517
|
-
};
|
|
1518
1916
|
const sampleTerrain = (worldX, worldZ) => {
|
|
1519
|
-
if (!hasSnapshot) {
|
|
1917
|
+
if (!state.hasSnapshot) {
|
|
1520
1918
|
return { elevation: 0, normal: new Vector3(0, 1, 0), valid: false };
|
|
1521
1919
|
}
|
|
1522
|
-
const lookup = lookupTile(worldX, worldZ);
|
|
1920
|
+
const lookup = lookupTile(state.frontIndex, config, worldX, worldZ);
|
|
1523
1921
|
if (!lookup.found) {
|
|
1524
1922
|
return { elevation: 0, normal: new Vector3(0, 1, 0), valid: false };
|
|
1525
1923
|
}
|
|
1526
1924
|
return sampleFromLookup(lookup);
|
|
1527
1925
|
};
|
|
1528
1926
|
const getElevation = (worldX, worldZ) => {
|
|
1529
|
-
if (!hasSnapshot) {
|
|
1927
|
+
if (!state.hasSnapshot) {
|
|
1530
1928
|
return { elevation: 0, valid: false };
|
|
1531
1929
|
}
|
|
1532
|
-
const lookup = lookupTile(worldX, worldZ);
|
|
1930
|
+
const lookup = lookupTile(state.frontIndex, config, worldX, worldZ);
|
|
1533
1931
|
if (!lookup.found) {
|
|
1534
1932
|
return { elevation: 0, valid: false };
|
|
1535
1933
|
}
|
|
1536
|
-
|
|
1934
|
+
const height = rawHeightFromLookup(lookup);
|
|
1935
|
+
return {
|
|
1936
|
+
elevation: config.originY + height * config.elevationScale,
|
|
1937
|
+
valid: true
|
|
1938
|
+
};
|
|
1939
|
+
};
|
|
1940
|
+
const invalidSurfaceSample = (dx, dy, dz) => ({
|
|
1941
|
+
position: new Vector3(),
|
|
1942
|
+
normal: new Vector3(0, 1, 0),
|
|
1943
|
+
direction: new Vector3(dx, dy, dz),
|
|
1944
|
+
elevation: 0,
|
|
1945
|
+
valid: false
|
|
1946
|
+
});
|
|
1947
|
+
const lookupDirection = (dx, dy, dz) => lookupTileForDirection(state.frontIndex, config, dx, dy, dz, dirScratch, uvScratch);
|
|
1948
|
+
const sampleSurfaceByDirection = (dx, dy, dz) => {
|
|
1949
|
+
if (!state.hasSnapshot || config.projection !== "cubeSphere") {
|
|
1950
|
+
return invalidSurfaceSample(dx, dy, dz);
|
|
1951
|
+
}
|
|
1952
|
+
const len = Math.hypot(dx, dy, dz);
|
|
1953
|
+
if (len === 0) return invalidSurfaceSample(0, 0, 0);
|
|
1954
|
+
const nx = dx / len;
|
|
1955
|
+
const ny = dy / len;
|
|
1956
|
+
const nz = dz / len;
|
|
1957
|
+
const lookup = lookupDirection(nx, ny, nz);
|
|
1958
|
+
if (!lookup.found) return invalidSurfaceSample(nx, ny, nz);
|
|
1959
|
+
const height = rawHeightFromLookup(lookup);
|
|
1960
|
+
const elevation = height * config.elevationScale;
|
|
1961
|
+
const r = config.radius + elevation;
|
|
1962
|
+
const position = new Vector3(
|
|
1963
|
+
config.originX + nx * r,
|
|
1964
|
+
config.originY + ny * r,
|
|
1965
|
+
config.originZ + nz * r
|
|
1966
|
+
);
|
|
1967
|
+
const normal = computeSphereNormal(
|
|
1968
|
+
lookup.leafIndex,
|
|
1969
|
+
gridScratch.gx,
|
|
1970
|
+
gridScratch.gy,
|
|
1971
|
+
lookup.tileSize,
|
|
1972
|
+
lookup.space,
|
|
1973
|
+
nx,
|
|
1974
|
+
ny,
|
|
1975
|
+
nz
|
|
1976
|
+
);
|
|
1977
|
+
return {
|
|
1978
|
+
position,
|
|
1979
|
+
normal,
|
|
1980
|
+
direction: new Vector3(nx, ny, nz),
|
|
1981
|
+
elevation,
|
|
1982
|
+
valid: true
|
|
1983
|
+
};
|
|
1984
|
+
};
|
|
1985
|
+
const tileFromLookup = (lookup) => {
|
|
1986
|
+
if (!lookup.found) return null;
|
|
1987
|
+
return {
|
|
1988
|
+
space: lookup.space,
|
|
1989
|
+
level: lookup.level,
|
|
1990
|
+
x: lookup.tileX,
|
|
1991
|
+
y: lookup.tileY,
|
|
1992
|
+
index: lookup.leafIndex
|
|
1993
|
+
};
|
|
1994
|
+
};
|
|
1995
|
+
const tileBoundsFromLookup = (lookup, elevationBase) => {
|
|
1996
|
+
if (!lookup.found || lookup.leafIndex >= state.frontLeafCount) return null;
|
|
1997
|
+
const rawMin = state.frontTileBounds[lookup.leafIndex * 2];
|
|
1998
|
+
const rawMax = state.frontTileBounds[lookup.leafIndex * 2 + 1];
|
|
1999
|
+
const a = elevationBase + rawMin * config.elevationScale;
|
|
2000
|
+
const b = elevationBase + rawMax * config.elevationScale;
|
|
2001
|
+
return {
|
|
2002
|
+
space: lookup.space,
|
|
2003
|
+
level: lookup.level,
|
|
2004
|
+
x: lookup.tileX,
|
|
2005
|
+
y: lookup.tileY,
|
|
2006
|
+
index: lookup.leafIndex,
|
|
2007
|
+
minElevation: Math.min(a, b),
|
|
2008
|
+
maxElevation: Math.max(a, b)
|
|
2009
|
+
};
|
|
1537
2010
|
};
|
|
1538
2011
|
const api = {
|
|
1539
2012
|
get generation() {
|
|
1540
|
-
return
|
|
2013
|
+
return state.generation;
|
|
1541
2014
|
},
|
|
1542
2015
|
get ready() {
|
|
1543
|
-
return hasSnapshot;
|
|
2016
|
+
return state.hasSnapshot;
|
|
1544
2017
|
},
|
|
1545
2018
|
updateConfig(nextConfig) {
|
|
1546
2019
|
config = nextConfig;
|
|
1547
|
-
edgeVertexCount = config.innerTileSegments + 3;
|
|
1548
|
-
verticesPerNode = edgeVertexCount * edgeVertexCount;
|
|
1549
|
-
totalElements = maxNodes * verticesPerNode;
|
|
2020
|
+
shape.edgeVertexCount = config.innerTileSegments + 3;
|
|
2021
|
+
shape.verticesPerNode = shape.edgeVertexCount * shape.edgeVertexCount;
|
|
2022
|
+
totalElements = maxNodes * shape.verticesPerNode;
|
|
1550
2023
|
},
|
|
1551
2024
|
triggerReadback(renderer, attribute, spatialIndex, boundsAttribute, activeLeafCount) {
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
const capturedLeafCount = activeLeafCount ?? 0;
|
|
1559
|
-
const capturedScale = config.elevationScale;
|
|
1560
|
-
const capturedOriginY = config.originY;
|
|
1561
|
-
readbackPending = true;
|
|
1562
|
-
const elevationPromise = withReadback.getArrayBufferAsync(attribute);
|
|
1563
|
-
const boundsPromise = boundsAttribute ? withReadback.getArrayBufferAsync(boundsAttribute) : null;
|
|
1564
|
-
const onComplete = (elevResult, boundsResult) => {
|
|
1565
|
-
const data = new Float32Array(elevResult);
|
|
1566
|
-
backElevation.fill(0);
|
|
1567
|
-
backElevation.set(data.subarray(0, totalElements));
|
|
1568
|
-
let boundsValid = capturedLeafCount === 0;
|
|
1569
|
-
if (boundsResult) {
|
|
1570
|
-
const rawBounds = new Float32Array(boundsResult);
|
|
1571
|
-
backTileBounds.fill(0);
|
|
1572
|
-
backTileBounds.set(rawBounds.subarray(0, capturedLeafCount * 2));
|
|
1573
|
-
for (let i = 0; i < capturedLeafCount; i += 1) {
|
|
1574
|
-
if ((rawBounds[i * 2 + 1] ?? 0) !== 0) {
|
|
1575
|
-
boundsValid = true;
|
|
1576
|
-
break;
|
|
1577
|
-
}
|
|
1578
|
-
}
|
|
1579
|
-
}
|
|
1580
|
-
const oldFrontElevation = frontElevation;
|
|
1581
|
-
const oldFrontIndex = frontIndex;
|
|
1582
|
-
frontElevation = backElevation;
|
|
1583
|
-
frontIndex = backIndex;
|
|
1584
|
-
frontLeafCount = capturedLeafCount;
|
|
1585
|
-
backElevation = oldFrontElevation;
|
|
1586
|
-
backIndex = oldFrontIndex;
|
|
1587
|
-
if (boundsResult && boundsValid) {
|
|
1588
|
-
const oldFrontBounds = frontTileBounds;
|
|
1589
|
-
frontTileBounds = backTileBounds;
|
|
1590
|
-
backTileBounds = oldFrontBounds;
|
|
1591
|
-
}
|
|
1592
|
-
if (boundsResult && boundsValid && capturedLeafCount > 0) {
|
|
1593
|
-
let gMin = Infinity;
|
|
1594
|
-
let gMax = -Infinity;
|
|
1595
|
-
for (let i = 0; i < capturedLeafCount; i++) {
|
|
1596
|
-
const rawMin = frontTileBounds[i * 2];
|
|
1597
|
-
const rawMax = frontTileBounds[i * 2 + 1];
|
|
1598
|
-
const a = capturedOriginY + rawMin * capturedScale;
|
|
1599
|
-
const b = capturedOriginY + rawMax * capturedScale;
|
|
1600
|
-
gMin = Math.min(gMin, a, b);
|
|
1601
|
-
gMax = Math.max(gMax, a, b);
|
|
1602
|
-
}
|
|
1603
|
-
globalRange = { min: gMin, max: gMax };
|
|
1604
|
-
}
|
|
1605
|
-
hasSnapshot = true;
|
|
1606
|
-
generationCount += 1;
|
|
1607
|
-
};
|
|
1608
|
-
if (boundsPromise) {
|
|
1609
|
-
Promise.all([elevationPromise, boundsPromise]).then(([elev, bounds]) => onComplete(elev, bounds)).finally(() => {
|
|
1610
|
-
readbackPending = false;
|
|
1611
|
-
});
|
|
1612
|
-
} else {
|
|
1613
|
-
elevationPromise.then((elev) => onComplete(elev, null)).finally(() => {
|
|
1614
|
-
readbackPending = false;
|
|
1615
|
-
});
|
|
1616
|
-
}
|
|
2025
|
+
triggerSnapshotReadback(state, renderer, attribute, spatialIndex, boundsAttribute, {
|
|
2026
|
+
activeLeafCount: activeLeafCount ?? 0,
|
|
2027
|
+
totalElements,
|
|
2028
|
+
elevationScale: config.elevationScale,
|
|
2029
|
+
originY: config.originY
|
|
2030
|
+
});
|
|
1617
2031
|
},
|
|
1618
2032
|
getElevation(worldX, worldZ) {
|
|
1619
2033
|
const sample = getElevation(worldX, worldZ);
|
|
@@ -1623,43 +2037,26 @@ function createCpuTerrainCache(maxNodes, initialConfig) {
|
|
|
1623
2037
|
return sampleTerrain(worldX, worldZ).normal;
|
|
1624
2038
|
},
|
|
1625
2039
|
getTile(worldX, worldZ) {
|
|
1626
|
-
if (!hasSnapshot) return null;
|
|
1627
|
-
|
|
1628
|
-
if (!lookup.found) return null;
|
|
1629
|
-
return {
|
|
1630
|
-
level: lookup.level,
|
|
1631
|
-
x: lookup.tileX,
|
|
1632
|
-
y: lookup.tileY,
|
|
1633
|
-
index: lookup.leafIndex
|
|
1634
|
-
};
|
|
2040
|
+
if (!state.hasSnapshot) return null;
|
|
2041
|
+
return tileFromLookup(lookupTile(state.frontIndex, config, worldX, worldZ));
|
|
1635
2042
|
},
|
|
1636
2043
|
getTileBounds(worldX, worldZ) {
|
|
1637
|
-
if (!hasSnapshot) return null;
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
const a = config.originY + rawMin * config.elevationScale;
|
|
1643
|
-
const b = config.originY + rawMax * config.elevationScale;
|
|
1644
|
-
return {
|
|
1645
|
-
level: lookup.level,
|
|
1646
|
-
x: lookup.tileX,
|
|
1647
|
-
y: lookup.tileY,
|
|
1648
|
-
index: lookup.leafIndex,
|
|
1649
|
-
minElevation: Math.min(a, b),
|
|
1650
|
-
maxElevation: Math.max(a, b)
|
|
1651
|
-
};
|
|
2044
|
+
if (!state.hasSnapshot) return null;
|
|
2045
|
+
return tileBoundsFromLookup(
|
|
2046
|
+
lookupTile(state.frontIndex, config, worldX, worldZ),
|
|
2047
|
+
config.originY
|
|
2048
|
+
);
|
|
1652
2049
|
},
|
|
1653
2050
|
getGlobalElevationRange() {
|
|
1654
|
-
return globalRange;
|
|
2051
|
+
return state.globalRange;
|
|
1655
2052
|
},
|
|
1656
2053
|
sampleTerrainBatch(positions) {
|
|
1657
2054
|
const count = Math.floor(positions.length / 2);
|
|
1658
2055
|
const elevations = new Float32Array(count);
|
|
1659
2056
|
const normals = new Float32Array(count * 3);
|
|
1660
2057
|
const valid = new Uint8Array(count);
|
|
1661
|
-
if (!hasSnapshot) {
|
|
1662
|
-
return { elevations, normals, valid, generation:
|
|
2058
|
+
if (!state.hasSnapshot) {
|
|
2059
|
+
return { elevations, normals, valid, generation: state.generation };
|
|
1663
2060
|
}
|
|
1664
2061
|
let lastTile;
|
|
1665
2062
|
for (let i = 0; i < count; i += 1) {
|
|
@@ -1670,6 +2067,7 @@ function createCpuTerrainCache(maxNodes, initialConfig) {
|
|
|
1670
2067
|
lookup = {
|
|
1671
2068
|
found: true,
|
|
1672
2069
|
leafIndex: lastTile.leafIndex,
|
|
2070
|
+
space: 0,
|
|
1673
2071
|
level: lastTile.level,
|
|
1674
2072
|
tileX: lastTile.tileX,
|
|
1675
2073
|
tileY: lastTile.tileY,
|
|
@@ -1678,7 +2076,7 @@ function createCpuTerrainCache(maxNodes, initialConfig) {
|
|
|
1678
2076
|
localV: (worldZ - lastTile.tileMinZ) / lastTile.tileSize
|
|
1679
2077
|
};
|
|
1680
2078
|
} else {
|
|
1681
|
-
lookup = lookupTile(worldX, worldZ);
|
|
2079
|
+
lookup = lookupTile(state.frontIndex, config, worldX, worldZ);
|
|
1682
2080
|
if (lookup.found) {
|
|
1683
2081
|
lastTile = {
|
|
1684
2082
|
leafIndex: lookup.leafIndex,
|
|
@@ -1704,9 +2102,133 @@ function createCpuTerrainCache(maxNodes, initialConfig) {
|
|
|
1704
2102
|
normals[i * 3 + 2] = sample.normal.z;
|
|
1705
2103
|
valid[i] = 1;
|
|
1706
2104
|
}
|
|
1707
|
-
return { elevations, normals, valid, generation:
|
|
2105
|
+
return { elevations, normals, valid, generation: state.generation };
|
|
2106
|
+
},
|
|
2107
|
+
sampleTerrain,
|
|
2108
|
+
// --- Cube-sphere queries ---
|
|
2109
|
+
sampleTerrainByDirection(direction) {
|
|
2110
|
+
return sampleSurfaceByDirection(direction.x, direction.y, direction.z);
|
|
2111
|
+
},
|
|
2112
|
+
sampleTerrainByPosition(position) {
|
|
2113
|
+
return sampleSurfaceByDirection(
|
|
2114
|
+
position.x - config.originX,
|
|
2115
|
+
position.y - config.originY,
|
|
2116
|
+
position.z - config.originZ
|
|
2117
|
+
);
|
|
2118
|
+
},
|
|
2119
|
+
sampleTerrainByLatLong(latitudeDeg, longitudeDeg) {
|
|
2120
|
+
latLongToDirection(latitudeDeg, longitudeDeg, llScratch);
|
|
2121
|
+
return sampleSurfaceByDirection(llScratch[0], llScratch[1], llScratch[2]);
|
|
1708
2122
|
},
|
|
1709
|
-
|
|
2123
|
+
getElevationByDirection(direction) {
|
|
2124
|
+
const sample = sampleSurfaceByDirection(direction.x, direction.y, direction.z);
|
|
2125
|
+
return sample.valid ? sample.elevation : null;
|
|
2126
|
+
},
|
|
2127
|
+
getElevationByPosition(position) {
|
|
2128
|
+
const sample = sampleSurfaceByDirection(
|
|
2129
|
+
position.x - config.originX,
|
|
2130
|
+
position.y - config.originY,
|
|
2131
|
+
position.z - config.originZ
|
|
2132
|
+
);
|
|
2133
|
+
return sample.valid ? sample.elevation : null;
|
|
2134
|
+
},
|
|
2135
|
+
getElevationByLatLong(latitudeDeg, longitudeDeg) {
|
|
2136
|
+
latLongToDirection(latitudeDeg, longitudeDeg, llScratch);
|
|
2137
|
+
const sample = sampleSurfaceByDirection(llScratch[0], llScratch[1], llScratch[2]);
|
|
2138
|
+
return sample.valid ? sample.elevation : null;
|
|
2139
|
+
},
|
|
2140
|
+
getNormalByDirection(direction) {
|
|
2141
|
+
const sample = sampleSurfaceByDirection(direction.x, direction.y, direction.z);
|
|
2142
|
+
return sample.valid ? sample.normal : null;
|
|
2143
|
+
},
|
|
2144
|
+
getNormalByPosition(position) {
|
|
2145
|
+
const sample = sampleSurfaceByDirection(
|
|
2146
|
+
position.x - config.originX,
|
|
2147
|
+
position.y - config.originY,
|
|
2148
|
+
position.z - config.originZ
|
|
2149
|
+
);
|
|
2150
|
+
return sample.valid ? sample.normal : null;
|
|
2151
|
+
},
|
|
2152
|
+
getNormalByLatLong(latitudeDeg, longitudeDeg) {
|
|
2153
|
+
latLongToDirection(latitudeDeg, longitudeDeg, llScratch);
|
|
2154
|
+
const sample = sampleSurfaceByDirection(llScratch[0], llScratch[1], llScratch[2]);
|
|
2155
|
+
return sample.valid ? sample.normal : null;
|
|
2156
|
+
},
|
|
2157
|
+
getTileByDirection(direction) {
|
|
2158
|
+
if (!state.hasSnapshot) return null;
|
|
2159
|
+
return tileFromLookup(lookupDirection(direction.x, direction.y, direction.z));
|
|
2160
|
+
},
|
|
2161
|
+
getTileByPosition(position) {
|
|
2162
|
+
if (!state.hasSnapshot) return null;
|
|
2163
|
+
return tileFromLookup(
|
|
2164
|
+
lookupDirection(
|
|
2165
|
+
position.x - config.originX,
|
|
2166
|
+
position.y - config.originY,
|
|
2167
|
+
position.z - config.originZ
|
|
2168
|
+
)
|
|
2169
|
+
);
|
|
2170
|
+
},
|
|
2171
|
+
getTileByLatLong(latitudeDeg, longitudeDeg) {
|
|
2172
|
+
if (!state.hasSnapshot) return null;
|
|
2173
|
+
latLongToDirection(latitudeDeg, longitudeDeg, llScratch);
|
|
2174
|
+
return tileFromLookup(lookupDirection(llScratch[0], llScratch[1], llScratch[2]));
|
|
2175
|
+
},
|
|
2176
|
+
getTileBoundsByDirection(direction) {
|
|
2177
|
+
if (!state.hasSnapshot) return null;
|
|
2178
|
+
return tileBoundsFromLookup(
|
|
2179
|
+
lookupDirection(direction.x, direction.y, direction.z),
|
|
2180
|
+
0
|
|
2181
|
+
);
|
|
2182
|
+
},
|
|
2183
|
+
getTileBoundsByPosition(position) {
|
|
2184
|
+
if (!state.hasSnapshot) return null;
|
|
2185
|
+
return tileBoundsFromLookup(
|
|
2186
|
+
lookupDirection(
|
|
2187
|
+
position.x - config.originX,
|
|
2188
|
+
position.y - config.originY,
|
|
2189
|
+
position.z - config.originZ
|
|
2190
|
+
),
|
|
2191
|
+
0
|
|
2192
|
+
);
|
|
2193
|
+
},
|
|
2194
|
+
getTileBoundsByLatLong(latitudeDeg, longitudeDeg) {
|
|
2195
|
+
if (!state.hasSnapshot) return null;
|
|
2196
|
+
latLongToDirection(latitudeDeg, longitudeDeg, llScratch);
|
|
2197
|
+
return tileBoundsFromLookup(
|
|
2198
|
+
lookupDirection(llScratch[0], llScratch[1], llScratch[2]),
|
|
2199
|
+
0
|
|
2200
|
+
);
|
|
2201
|
+
},
|
|
2202
|
+
sampleTerrainBatchByDirection(directions) {
|
|
2203
|
+
const count = Math.floor(directions.length / 3);
|
|
2204
|
+
const positions = new Float32Array(count * 3);
|
|
2205
|
+
const normals = new Float32Array(count * 3);
|
|
2206
|
+
const elevations = new Float32Array(count);
|
|
2207
|
+
const valid = new Uint8Array(count);
|
|
2208
|
+
if (!state.hasSnapshot || config.projection !== "cubeSphere") {
|
|
2209
|
+
return { positions, normals, elevations, valid, generation: state.generation };
|
|
2210
|
+
}
|
|
2211
|
+
for (let i = 0; i < count; i += 1) {
|
|
2212
|
+
const sample = sampleSurfaceByDirection(
|
|
2213
|
+
directions[i * 3] ?? 0,
|
|
2214
|
+
directions[i * 3 + 1] ?? 0,
|
|
2215
|
+
directions[i * 3 + 2] ?? 0
|
|
2216
|
+
);
|
|
2217
|
+
if (!sample.valid) {
|
|
2218
|
+
normals[i * 3 + 1] = 1;
|
|
2219
|
+
continue;
|
|
2220
|
+
}
|
|
2221
|
+
positions[i * 3] = sample.position.x;
|
|
2222
|
+
positions[i * 3 + 1] = sample.position.y;
|
|
2223
|
+
positions[i * 3 + 2] = sample.position.z;
|
|
2224
|
+
normals[i * 3] = sample.normal.x;
|
|
2225
|
+
normals[i * 3 + 1] = sample.normal.y;
|
|
2226
|
+
normals[i * 3 + 2] = sample.normal.z;
|
|
2227
|
+
elevations[i] = sample.elevation;
|
|
2228
|
+
valid[i] = 1;
|
|
2229
|
+
}
|
|
2230
|
+
return { positions, normals, elevations, valid, generation: state.generation };
|
|
2231
|
+
}
|
|
1710
2232
|
};
|
|
1711
2233
|
return api;
|
|
1712
2234
|
}
|
|
@@ -1739,6 +2261,61 @@ function createTerrainQuery(cache) {
|
|
|
1739
2261
|
}
|
|
1740
2262
|
};
|
|
1741
2263
|
}
|
|
2264
|
+
function createTerrainSphereQuery(cache) {
|
|
2265
|
+
return {
|
|
2266
|
+
get generation() {
|
|
2267
|
+
return cache.generation;
|
|
2268
|
+
},
|
|
2269
|
+
getElevationByDirection(direction) {
|
|
2270
|
+
return cache.getElevationByDirection(direction);
|
|
2271
|
+
},
|
|
2272
|
+
getElevationByPosition(position) {
|
|
2273
|
+
return cache.getElevationByPosition(position);
|
|
2274
|
+
},
|
|
2275
|
+
getElevationByLatLong(latitudeDeg, longitudeDeg) {
|
|
2276
|
+
return cache.getElevationByLatLong(latitudeDeg, longitudeDeg);
|
|
2277
|
+
},
|
|
2278
|
+
getNormalByDirection(direction) {
|
|
2279
|
+
return cache.getNormalByDirection(direction);
|
|
2280
|
+
},
|
|
2281
|
+
getNormalByPosition(position) {
|
|
2282
|
+
return cache.getNormalByPosition(position);
|
|
2283
|
+
},
|
|
2284
|
+
getNormalByLatLong(latitudeDeg, longitudeDeg) {
|
|
2285
|
+
return cache.getNormalByLatLong(latitudeDeg, longitudeDeg);
|
|
2286
|
+
},
|
|
2287
|
+
sampleTerrainByDirection(direction) {
|
|
2288
|
+
return cache.sampleTerrainByDirection(direction);
|
|
2289
|
+
},
|
|
2290
|
+
sampleTerrainByPosition(position) {
|
|
2291
|
+
return cache.sampleTerrainByPosition(position);
|
|
2292
|
+
},
|
|
2293
|
+
sampleTerrainByLatLong(latitudeDeg, longitudeDeg) {
|
|
2294
|
+
return cache.sampleTerrainByLatLong(latitudeDeg, longitudeDeg);
|
|
2295
|
+
},
|
|
2296
|
+
getTileByDirection(direction) {
|
|
2297
|
+
return cache.getTileByDirection(direction);
|
|
2298
|
+
},
|
|
2299
|
+
getTileByPosition(position) {
|
|
2300
|
+
return cache.getTileByPosition(position);
|
|
2301
|
+
},
|
|
2302
|
+
getTileByLatLong(latitudeDeg, longitudeDeg) {
|
|
2303
|
+
return cache.getTileByLatLong(latitudeDeg, longitudeDeg);
|
|
2304
|
+
},
|
|
2305
|
+
getTileBoundsByDirection(direction) {
|
|
2306
|
+
return cache.getTileBoundsByDirection(direction);
|
|
2307
|
+
},
|
|
2308
|
+
getTileBoundsByPosition(position) {
|
|
2309
|
+
return cache.getTileBoundsByPosition(position);
|
|
2310
|
+
},
|
|
2311
|
+
getTileBoundsByLatLong(latitudeDeg, longitudeDeg) {
|
|
2312
|
+
return cache.getTileBoundsByLatLong(latitudeDeg, longitudeDeg);
|
|
2313
|
+
},
|
|
2314
|
+
sampleTerrainBatchByDirection(directions) {
|
|
2315
|
+
return cache.sampleTerrainBatchByDirection(directions);
|
|
2316
|
+
}
|
|
2317
|
+
};
|
|
2318
|
+
}
|
|
1742
2319
|
|
|
1743
2320
|
const WGSIZE = 64;
|
|
1744
2321
|
function buildReductionKernel(elevationFieldNode, boundsNode, verticesPerNode) {
|
|
@@ -1808,8 +2385,11 @@ const terrainQueryTask = task((get, work) => {
|
|
|
1808
2385
|
const rootSizeValue = get(rootSize);
|
|
1809
2386
|
const originValue = get(origin);
|
|
1810
2387
|
const elevationScaleValue = get(elevationScale);
|
|
2388
|
+
const radiusValue = get(radius);
|
|
2389
|
+
const topologyValue = get(topologyTask);
|
|
2390
|
+
const projectionValue = topologyValue.projection ?? "flat";
|
|
1811
2391
|
return work((prev) => {
|
|
1812
|
-
const shapeKey = `${maxNodesValue}:${innerTileSegmentsValue}`;
|
|
2392
|
+
const shapeKey = `${maxNodesValue}:${innerTileSegmentsValue}:${projectionValue}`;
|
|
1813
2393
|
const configValues = {
|
|
1814
2394
|
rootSize: rootSizeValue,
|
|
1815
2395
|
originX: originValue.x,
|
|
@@ -1817,16 +2397,20 @@ const terrainQueryTask = task((get, work) => {
|
|
|
1817
2397
|
originZ: originValue.z,
|
|
1818
2398
|
innerTileSegments: innerTileSegmentsValue,
|
|
1819
2399
|
elevationScale: elevationScaleValue,
|
|
1820
|
-
maxLevel: maxLevelValue
|
|
2400
|
+
maxLevel: maxLevelValue,
|
|
2401
|
+
projection: projectionValue,
|
|
2402
|
+
radius: topologyValue.radius ?? radiusValue
|
|
1821
2403
|
};
|
|
1822
2404
|
let cache = prev?.cache;
|
|
1823
2405
|
let query = prev?.query;
|
|
2406
|
+
let sphereQuery = prev?.sphereQuery ?? null;
|
|
1824
2407
|
if (!cache || !query || prev?.shapeKey !== shapeKey) {
|
|
1825
2408
|
cache = createCpuTerrainCache(maxNodesValue, configValues);
|
|
1826
2409
|
query = createTerrainQuery(cache);
|
|
2410
|
+
sphereQuery = projectionValue === "cubeSphere" ? createTerrainSphereQuery(cache) : null;
|
|
1827
2411
|
}
|
|
1828
2412
|
cache.updateConfig(configValues);
|
|
1829
|
-
return { cache, query, shapeKey };
|
|
2413
|
+
return { cache, query, sphereQuery, shapeKey };
|
|
1830
2414
|
});
|
|
1831
2415
|
}).displayName("terrainQueryTask");
|
|
1832
2416
|
const terrainReadbackTask = task(
|
|
@@ -1849,38 +2433,44 @@ const terrainReadbackTask = task(
|
|
|
1849
2433
|
}
|
|
1850
2434
|
).displayName("terrainReadbackTask").lane("gpu");
|
|
1851
2435
|
|
|
1852
|
-
const
|
|
1853
|
-
const
|
|
2436
|
+
const topologyTask = task((get, work) => {
|
|
2437
|
+
const customTopology = get(topology);
|
|
1854
2438
|
const rootSizeVal = get(rootSize);
|
|
1855
2439
|
const originVal = get(origin);
|
|
1856
2440
|
return work(() => {
|
|
1857
|
-
if (
|
|
1858
|
-
return
|
|
2441
|
+
if (customTopology) return customTopology;
|
|
2442
|
+
return createFlatTopology({ rootSize: rootSizeVal, origin: originVal });
|
|
1859
2443
|
});
|
|
1860
|
-
}).displayName("
|
|
2444
|
+
}).displayName("topologyTask");
|
|
1861
2445
|
const quadtreeConfigTask = task((get, work) => {
|
|
1862
|
-
const
|
|
2446
|
+
const topologyVal = get(topologyTask);
|
|
1863
2447
|
const maxNodesVal = get(maxNodes);
|
|
1864
2448
|
const maxLevelVal = get(maxLevel);
|
|
1865
2449
|
return work(() => {
|
|
1866
|
-
const state = createState({ maxNodes: maxNodesVal, maxLevel: maxLevelVal },
|
|
2450
|
+
const state = createState({ maxNodes: maxNodesVal, maxLevel: maxLevelVal }, topologyVal);
|
|
1867
2451
|
return {
|
|
1868
2452
|
state,
|
|
1869
|
-
|
|
2453
|
+
topology: topologyVal
|
|
1870
2454
|
};
|
|
1871
2455
|
});
|
|
1872
2456
|
}).displayName("quadtreeConfigTask");
|
|
1873
2457
|
const quadtreeUpdateTask = task((get, work) => {
|
|
1874
2458
|
const quadtreeConfig = get(quadtreeConfigTask);
|
|
1875
2459
|
const quadtreeUpdateConfig = get(quadtreeUpdate);
|
|
1876
|
-
const { query: terrainQuery } = get(terrainQueryTask);
|
|
2460
|
+
const { query: terrainQuery, sphereQuery } = get(terrainQueryTask);
|
|
1877
2461
|
let outLeaves = void 0;
|
|
2462
|
+
const cameraPosition = new Vector3();
|
|
1878
2463
|
return work(() => {
|
|
1879
2464
|
const cam = quadtreeUpdateConfig.cameraOrigin;
|
|
1880
|
-
|
|
2465
|
+
if (sphereQuery) {
|
|
2466
|
+
cameraPosition.set(cam.x, cam.y, cam.z);
|
|
2467
|
+
quadtreeUpdateConfig.elevationAtCameraXZ = sphereQuery.getElevationByPosition(cameraPosition) ?? 0;
|
|
2468
|
+
} else {
|
|
2469
|
+
quadtreeUpdateConfig.elevationAtCameraXZ = terrainQuery.getElevation(cam.x, cam.z) ?? 0;
|
|
2470
|
+
}
|
|
1881
2471
|
outLeaves = update(
|
|
1882
2472
|
quadtreeConfig.state,
|
|
1883
|
-
quadtreeConfig.
|
|
2473
|
+
quadtreeConfig.topology,
|
|
1884
2474
|
quadtreeUpdateConfig,
|
|
1885
2475
|
outLeaves
|
|
1886
2476
|
);
|
|
@@ -1902,7 +2492,7 @@ const leafGpuBufferTask = task((get, work) => {
|
|
|
1902
2492
|
leafStorage.data[offset] = leafSet.level[i] ?? 0;
|
|
1903
2493
|
leafStorage.data[offset + 1] = leafSet.x[i] ?? 0;
|
|
1904
2494
|
leafStorage.data[offset + 2] = leafSet.y[i] ?? 0;
|
|
1905
|
-
leafStorage.data[offset + 3] =
|
|
2495
|
+
leafStorage.data[offset + 3] = leafSet.space[i] ?? 0;
|
|
1906
2496
|
}
|
|
1907
2497
|
leafStorage.attribute.needsUpdate = true;
|
|
1908
2498
|
leafStorage.node.needsUpdate = true;
|
|
@@ -1944,12 +2534,14 @@ function createTerrainUniforms(params) {
|
|
|
1944
2534
|
);
|
|
1945
2535
|
const uSkirtScale = uniform(float(params.skirtScale)).setName(`uSkirtScale${suffix}`);
|
|
1946
2536
|
const uElevationScale = uniform(float(params.elevationScale)).setName(`uElevationScale${suffix}`);
|
|
2537
|
+
const uRadius = uniform(float(params.radius)).setName(`uRadius${suffix}`);
|
|
1947
2538
|
return {
|
|
1948
2539
|
uRootOrigin,
|
|
1949
2540
|
uRootSize,
|
|
1950
2541
|
uInnerTileSegments,
|
|
1951
2542
|
uSkirtScale,
|
|
1952
|
-
uElevationScale
|
|
2543
|
+
uElevationScale,
|
|
2544
|
+
uRadius
|
|
1953
2545
|
};
|
|
1954
2546
|
}
|
|
1955
2547
|
|
|
@@ -1963,6 +2555,7 @@ const createUniformsTask = task((get, work) => {
|
|
|
1963
2555
|
innerTileSegments: get(innerTileSegments),
|
|
1964
2556
|
skirtScale: get(skirtScale),
|
|
1965
2557
|
elevationScale: get(elevationScale),
|
|
2558
|
+
radius: get(radius),
|
|
1966
2559
|
instanceId: get(instanceIdTask)
|
|
1967
2560
|
};
|
|
1968
2561
|
return work(() => createTerrainUniforms(uniformParams));
|
|
@@ -1974,6 +2567,7 @@ const updateUniformsTask = task((get, work) => {
|
|
|
1974
2567
|
const innerTileSegmentsVal = get(innerTileSegments);
|
|
1975
2568
|
const skirtScaleVal = get(skirtScale);
|
|
1976
2569
|
const elevationScaleVal = get(elevationScale);
|
|
2570
|
+
const radiusVal = get(radius);
|
|
1977
2571
|
return work(() => {
|
|
1978
2572
|
terrainUniformsContext.uRootSize.value = rootSizeVal;
|
|
1979
2573
|
terrainUniformsContext.uRootOrigin.value = scratchVector3.set(
|
|
@@ -1984,6 +2578,7 @@ const updateUniformsTask = task((get, work) => {
|
|
|
1984
2578
|
terrainUniformsContext.uInnerTileSegments.value = innerTileSegmentsVal;
|
|
1985
2579
|
terrainUniformsContext.uSkirtScale.value = skirtScaleVal;
|
|
1986
2580
|
terrainUniformsContext.uElevationScale.value = elevationScaleVal;
|
|
2581
|
+
terrainUniformsContext.uRadius.value = radiusVal;
|
|
1987
2582
|
return terrainUniformsContext;
|
|
1988
2583
|
});
|
|
1989
2584
|
}).displayName("updateUniformsTask");
|
|
@@ -2006,8 +2601,9 @@ const createElevationFieldContextTask = task((get, work) => {
|
|
|
2006
2601
|
const tileNodesTask = task((get, work) => {
|
|
2007
2602
|
const leafStorage = get(leafStorageTask);
|
|
2008
2603
|
const uniforms = get(updateUniformsTask);
|
|
2604
|
+
const topology = get(topologyTask);
|
|
2009
2605
|
return work(() => {
|
|
2010
|
-
return createTileCompute(leafStorage, uniforms);
|
|
2606
|
+
return createTileCompute(leafStorage, uniforms, topology.projection ?? "flat");
|
|
2011
2607
|
});
|
|
2012
2608
|
}).displayName("tileNodesTask");
|
|
2013
2609
|
const elevationFieldStageTask = task((get, work) => {
|
|
@@ -2104,33 +2700,14 @@ const terrainFieldStageTask = task((get, work) => {
|
|
|
2104
2700
|
});
|
|
2105
2701
|
}).displayName("terrainFieldStageTask");
|
|
2106
2702
|
|
|
2107
|
-
const compileComputeTask =
|
|
2108
|
-
const pipeline = get(terrainFieldStageTask);
|
|
2109
|
-
const edgeVertexCount = get(innerTileSegments) + 3;
|
|
2110
|
-
return work(
|
|
2111
|
-
() => compileComputePipeline(pipeline, edgeVertexCount, {
|
|
2112
|
-
preferSingleKernelWhenPossible: false
|
|
2113
|
-
})
|
|
2114
|
-
);
|
|
2115
|
-
}).displayName("compileComputeTask");
|
|
2116
|
-
const executeComputeTask = task(
|
|
2117
|
-
(get, work, { resources }) => {
|
|
2118
|
-
const { execute } = get(compileComputeTask);
|
|
2119
|
-
const leafState = get(leafGpuBufferTask);
|
|
2120
|
-
return work(
|
|
2121
|
-
() => resources?.renderer ? execute(resources.renderer, leafState.count) : () => {
|
|
2122
|
-
}
|
|
2123
|
-
);
|
|
2124
|
-
}
|
|
2125
|
-
).displayName("executeComputeTask").lane("gpu");
|
|
2703
|
+
const { compile: compileComputeTask, execute: executeComputeTask } = createComputePipelineTasks(terrainFieldStageTask);
|
|
2126
2704
|
function createComputePipelineTasks(leafStageTask) {
|
|
2127
2705
|
const compile = task((get, work) => {
|
|
2128
2706
|
const pipeline = get(leafStageTask);
|
|
2129
2707
|
const edgeVertexCount = get(innerTileSegments) + 3;
|
|
2130
2708
|
return work(
|
|
2131
2709
|
() => compileComputePipeline(pipeline, edgeVertexCount, {
|
|
2132
|
-
|
|
2133
|
-
})
|
|
2710
|
+
})
|
|
2134
2711
|
);
|
|
2135
2712
|
}).displayName("compileComputeTask");
|
|
2136
2713
|
const execute = task(
|
|
@@ -2271,6 +2848,38 @@ const createTileIndexFromWorldPosition = (spatialIndex, uniforms, maxLevel) => {
|
|
|
2271
2848
|
return vec3(tileIndex.toFloat(), tileU, tileV);
|
|
2272
2849
|
});
|
|
2273
2850
|
};
|
|
2851
|
+
const createTileIndexFromDirection = (spatialIndex, maxLevel) => {
|
|
2852
|
+
const lookup = createGpuSpatialLookup(spatialIndex);
|
|
2853
|
+
const levelCount = Math.max(1, maxLevel + 1);
|
|
2854
|
+
return Fn(([direction]) => {
|
|
2855
|
+
const dir = vec3(direction).normalize().toVar();
|
|
2856
|
+
const face = cubeFaceFromDirection(dir).toVar();
|
|
2857
|
+
const basis = cubeFaceBasis(face);
|
|
2858
|
+
const faceUV = cubeFaceUVFromDirection(basis, dir).toVar();
|
|
2859
|
+
const u = faceUV.x.toVar();
|
|
2860
|
+
const v = faceUV.y.toVar();
|
|
2861
|
+
const tileIndex = int(-1).toVar();
|
|
2862
|
+
const tileU = float(0).toVar();
|
|
2863
|
+
const tileV = float(0).toVar();
|
|
2864
|
+
const i = int(0).toVar();
|
|
2865
|
+
Loop(levelCount, () => {
|
|
2866
|
+
const level = int(maxLevel).sub(i).toVar();
|
|
2867
|
+
const n = pow(float(2), level.toFloat()).toVar();
|
|
2868
|
+
const nInt = int(n).toVar();
|
|
2869
|
+
const tileX = u.mul(n).floor().toInt().max(int(0)).min(nInt.sub(int(1))).toVar();
|
|
2870
|
+
const tileY = v.mul(n).floor().toInt().max(int(0)).min(nInt.sub(int(1))).toVar();
|
|
2871
|
+
const maybeIndex = lookup(face, level, tileX, tileY).toVar();
|
|
2872
|
+
If(maybeIndex.greaterThanEqual(int(0)), () => {
|
|
2873
|
+
tileIndex.assign(maybeIndex);
|
|
2874
|
+
tileU.assign(u.mul(n).sub(tileX.toFloat()));
|
|
2875
|
+
tileV.assign(v.mul(n).sub(tileY.toFloat()));
|
|
2876
|
+
Break();
|
|
2877
|
+
});
|
|
2878
|
+
i.addAssign(1);
|
|
2879
|
+
});
|
|
2880
|
+
return vec3(tileIndex.toFloat(), tileU, tileV);
|
|
2881
|
+
});
|
|
2882
|
+
};
|
|
2274
2883
|
|
|
2275
2884
|
const gpuSpatialIndexStorageTask = task((get, work) => {
|
|
2276
2885
|
const maxNodesValue = get(maxNodes);
|
|
@@ -2286,43 +2895,44 @@ const gpuSpatialIndexUploadTask = task((get, work) => {
|
|
|
2286
2895
|
});
|
|
2287
2896
|
}).displayName("gpuSpatialIndexUploadTask");
|
|
2288
2897
|
|
|
2898
|
+
function packedSampleFromTileResult(params, tileResult) {
|
|
2899
|
+
const tileIndex = int(tileResult.x).toVar();
|
|
2900
|
+
const safeTileIndex = tileIndex.max(int(0)).toVar();
|
|
2901
|
+
const fieldU = tileLocalToFieldUV(
|
|
2902
|
+
tileResult.y,
|
|
2903
|
+
params.uniforms.uInnerTileSegments
|
|
2904
|
+
).toVar();
|
|
2905
|
+
const fieldV = tileLocalToFieldUV(
|
|
2906
|
+
tileResult.z,
|
|
2907
|
+
params.uniforms.uInnerTileSegments
|
|
2908
|
+
).toVar();
|
|
2909
|
+
const found = tileIndex.greaterThanEqual(int(0)).toVar();
|
|
2910
|
+
const sampled = sampleTerrainField(
|
|
2911
|
+
params.terrainFieldStorage,
|
|
2912
|
+
fieldU,
|
|
2913
|
+
fieldV,
|
|
2914
|
+
safeTileIndex
|
|
2915
|
+
).toVar();
|
|
2916
|
+
const normal = unpackTangentNormal(sampled.g, sampled.b);
|
|
2917
|
+
const valid = found.select(float(1), float(0)).toVar();
|
|
2918
|
+
return vec4(sampled.r, normal.x, normal.y, normal.z).mul(valid);
|
|
2919
|
+
}
|
|
2289
2920
|
function createTerrainSampleNode(params) {
|
|
2290
2921
|
const tileLookup = createTileIndexFromWorldPosition(
|
|
2291
2922
|
params.spatialIndex,
|
|
2292
2923
|
params.uniforms,
|
|
2293
|
-
maxLevel
|
|
2924
|
+
params.maxLevel
|
|
2294
2925
|
);
|
|
2295
2926
|
return Fn(([worldX, worldZ]) => {
|
|
2296
2927
|
const tileResult = tileLookup(worldX, worldZ).toVar();
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
const fieldV = tileLocalToFieldUV$1(
|
|
2306
|
-
v,
|
|
2307
|
-
params.uniforms.uInnerTileSegments
|
|
2308
|
-
).toVar();
|
|
2309
|
-
const found = tileIndex.greaterThanEqual(int(0)).toVar();
|
|
2310
|
-
const sampled = sampleTerrainField(
|
|
2311
|
-
params.terrainFieldStorage,
|
|
2312
|
-
fieldU,
|
|
2313
|
-
fieldV,
|
|
2314
|
-
safeTileIndex
|
|
2315
|
-
).toVar();
|
|
2316
|
-
const nx = sampled.g.toVar();
|
|
2317
|
-
const nz = sampled.b.toVar();
|
|
2318
|
-
const ny = float(1).sub(nx.mul(nx)).sub(nz.mul(nz)).max(0).sqrt();
|
|
2319
|
-
const valid = found.select(float(1), float(0)).toVar();
|
|
2320
|
-
return vec4(
|
|
2321
|
-
sampled.r.mul(valid),
|
|
2322
|
-
nx.mul(valid),
|
|
2323
|
-
ny.mul(valid),
|
|
2324
|
-
nz.mul(valid)
|
|
2325
|
-
);
|
|
2928
|
+
return packedSampleFromTileResult(params, tileResult);
|
|
2929
|
+
});
|
|
2930
|
+
}
|
|
2931
|
+
function createTerrainSampleNodeByDirection(params) {
|
|
2932
|
+
const tileLookup = createTileIndexFromDirection(params.spatialIndex, params.maxLevel);
|
|
2933
|
+
return Fn(([direction]) => {
|
|
2934
|
+
const tileResult = tileLookup(direction).toVar();
|
|
2935
|
+
return packedSampleFromTileResult(params, tileResult);
|
|
2326
2936
|
});
|
|
2327
2937
|
}
|
|
2328
2938
|
function createTerrainSampler(params) {
|
|
@@ -2354,16 +2964,14 @@ function createTerrainSampler(params) {
|
|
|
2354
2964
|
const sampleElevation = Fn(
|
|
2355
2965
|
([worldX, worldZ]) => terrainSampleAt(worldX, worldZ).x
|
|
2356
2966
|
);
|
|
2357
|
-
const sampleNormal = Fn(
|
|
2358
|
-
(
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
)
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
([worldX, worldZ]) => terrainSampleAt(worldX, worldZ).y.abs().add(terrainSampleAt(worldX, worldZ).z.abs()).add(terrainSampleAt(worldX, worldZ).w.abs()).greaterThan(float(0)).select(float(1), float(0))
|
|
2366
|
-
);
|
|
2967
|
+
const sampleNormal = Fn(([worldX, worldZ]) => {
|
|
2968
|
+
const sample = terrainSampleAt(worldX, worldZ).toVar();
|
|
2969
|
+
return vec3(sample.y, sample.z, sample.w);
|
|
2970
|
+
});
|
|
2971
|
+
const sampleValidity = Fn(([worldX, worldZ]) => {
|
|
2972
|
+
const sample = terrainSampleAt(worldX, worldZ).toVar();
|
|
2973
|
+
return sample.y.abs().add(sample.z.abs()).add(sample.w.abs()).greaterThan(float(0)).select(float(1), float(0));
|
|
2974
|
+
});
|
|
2367
2975
|
const evaluateElevation = Fn(
|
|
2368
2976
|
([worldX, worldZ]) => evaluateElevationAt(worldX, worldZ)
|
|
2369
2977
|
);
|
|
@@ -2390,7 +2998,7 @@ function createTerrainSampler(params) {
|
|
|
2390
2998
|
}
|
|
2391
2999
|
);
|
|
2392
3000
|
const evaluateNormal = (worldX, worldZ, epsilon) => evaluateNormalNode(worldX, worldZ, epsilon ?? float(0.1));
|
|
2393
|
-
|
|
3001
|
+
const sampler = {
|
|
2394
3002
|
sampleElevation,
|
|
2395
3003
|
sampleNormal,
|
|
2396
3004
|
sampleTerrain,
|
|
@@ -2398,6 +3006,26 @@ function createTerrainSampler(params) {
|
|
|
2398
3006
|
evaluateElevation,
|
|
2399
3007
|
evaluateNormal
|
|
2400
3008
|
};
|
|
3009
|
+
if (params.projection === "cubeSphere") {
|
|
3010
|
+
const terrainSampleByDir = createTerrainSampleNodeByDirection(params);
|
|
3011
|
+
sampler.sampleTerrainByDirection = Fn(
|
|
3012
|
+
([direction]) => terrainSampleByDir(direction)
|
|
3013
|
+
);
|
|
3014
|
+
sampler.sampleElevationByDirection = Fn(
|
|
3015
|
+
([direction]) => terrainSampleByDir(direction).x
|
|
3016
|
+
);
|
|
3017
|
+
sampler.sampleValidityByDirection = Fn(([direction]) => {
|
|
3018
|
+
const sample = terrainSampleByDir(direction).toVar();
|
|
3019
|
+
return sample.y.abs().add(sample.z.abs()).add(sample.w.abs()).greaterThan(float(0)).select(float(1), float(0));
|
|
3020
|
+
});
|
|
3021
|
+
sampler.sampleNormalByDirection = Fn(([direction]) => {
|
|
3022
|
+
const dir = vec3(direction).normalize().toVar();
|
|
3023
|
+
const packed = terrainSampleByDir(direction).toVar();
|
|
3024
|
+
const basis = cubeFaceBasis(cubeFaceFromDirection(dir));
|
|
3025
|
+
return sphereTangentFrameNormal(dir, basis, vec3(packed.y, packed.z, packed.w));
|
|
3026
|
+
});
|
|
3027
|
+
}
|
|
3028
|
+
return sampler;
|
|
2401
3029
|
}
|
|
2402
3030
|
|
|
2403
3031
|
const createTerrainSamplerTask = task((get, work) => {
|
|
@@ -2405,12 +3033,16 @@ const createTerrainSamplerTask = task((get, work) => {
|
|
|
2405
3033
|
const spatialIndex = get(gpuSpatialIndexStorageTask);
|
|
2406
3034
|
const uniforms = get(updateUniformsTask);
|
|
2407
3035
|
const elevationCallback = get(elevationFn);
|
|
3036
|
+
const maxLevelValue = get(maxLevel);
|
|
3037
|
+
const projection = get(topologyTask).projection ?? "flat";
|
|
2408
3038
|
return work(
|
|
2409
3039
|
() => createTerrainSampler({
|
|
2410
3040
|
terrainFieldStorage,
|
|
2411
3041
|
spatialIndex,
|
|
2412
3042
|
uniforms,
|
|
2413
|
-
elevationCallback
|
|
3043
|
+
elevationCallback,
|
|
3044
|
+
maxLevel: maxLevelValue,
|
|
3045
|
+
projection
|
|
2414
3046
|
})
|
|
2415
3047
|
);
|
|
2416
3048
|
}).displayName("createTerrainSamplerTask");
|
|
@@ -2437,18 +3069,14 @@ const isSkirtUV = Fn(([segments]) => {
|
|
|
2437
3069
|
|
|
2438
3070
|
function createTileBaseWorldPosition(leafStorage, terrainUniforms) {
|
|
2439
3071
|
return Fn(() => {
|
|
2440
|
-
const
|
|
2441
|
-
const nodeOffset = nodeIndex.mul(int(4));
|
|
2442
|
-
const nodeLevel = leafStorage.node.element(nodeOffset).toInt();
|
|
2443
|
-
const nodeX = leafStorage.node.element(nodeOffset.add(int(1))).toFloat();
|
|
2444
|
-
const nodeY = leafStorage.node.element(nodeOffset.add(int(2))).toFloat();
|
|
3072
|
+
const tile = decodeLeafTile(leafStorage, int(instanceIndex));
|
|
2445
3073
|
const rootSize = terrainUniforms.uRootSize.toVar();
|
|
2446
3074
|
const rootOrigin = terrainUniforms.uRootOrigin.toVar();
|
|
2447
3075
|
const half = float(0.5);
|
|
2448
|
-
const size = rootSize.div(pow(float(2),
|
|
3076
|
+
const size = rootSize.div(pow(float(2), tile.level.toFloat()));
|
|
2449
3077
|
const halfRoot = rootSize.mul(half);
|
|
2450
|
-
const centerX = rootOrigin.x.add(
|
|
2451
|
-
const centerZ = rootOrigin.z.add(
|
|
3078
|
+
const centerX = rootOrigin.x.add(tile.x.add(half).mul(size)).sub(halfRoot);
|
|
3079
|
+
const centerZ = rootOrigin.z.add(tile.y.add(half).mul(size)).sub(halfRoot);
|
|
2452
3080
|
const clampedX = positionLocal.x.max(half.negate()).min(half);
|
|
2453
3081
|
const clampedZ = positionLocal.z.max(half.negate()).min(half);
|
|
2454
3082
|
const worldX = centerX.add(clampedX.mul(size));
|
|
@@ -2459,49 +3087,78 @@ function createTileBaseWorldPosition(leafStorage, terrainUniforms) {
|
|
|
2459
3087
|
function createTileElevation(terrainUniforms, terrainFieldStorage) {
|
|
2460
3088
|
if (!terrainFieldStorage) return float(0);
|
|
2461
3089
|
const innerSegs = terrainUniforms.uInnerTileSegments;
|
|
2462
|
-
const u = tileLocalToFieldUV
|
|
2463
|
-
const v = tileLocalToFieldUV
|
|
2464
|
-
return sampleTerrainFieldElevation(
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
).mul(terrainUniforms.uElevationScale);
|
|
2470
|
-
}
|
|
2471
|
-
function createNormalAssignment(terrainUniforms, terrainFieldStorage) {
|
|
3090
|
+
const u = tileLocalToFieldUV(positionLocal.x.add(float(0.5)), innerSegs);
|
|
3091
|
+
const v = tileLocalToFieldUV(positionLocal.z.add(float(0.5)), innerSegs);
|
|
3092
|
+
return sampleTerrainFieldElevation(terrainFieldStorage, u, v, int(instanceIndex)).mul(
|
|
3093
|
+
terrainUniforms.uElevationScale
|
|
3094
|
+
);
|
|
3095
|
+
}
|
|
3096
|
+
function createNormalAssignment(leafStorage, terrainUniforms, terrainFieldStorage, projection = "flat") {
|
|
2472
3097
|
if (!terrainFieldStorage) return;
|
|
3098
|
+
normalLocal.assign(
|
|
3099
|
+
createTileLocalNormal(leafStorage, terrainUniforms, terrainFieldStorage, projection)
|
|
3100
|
+
);
|
|
3101
|
+
}
|
|
3102
|
+
function loadTangentNormal(terrainUniforms, terrainFieldStorage) {
|
|
2473
3103
|
const nodeIndex = int(instanceIndex);
|
|
2474
3104
|
const edgeVertexCount = int(terrainUniforms.uInnerTileSegments.add(3));
|
|
2475
3105
|
const localVertexIndex = int(vertexIndex);
|
|
2476
3106
|
const ix = localVertexIndex.mod(edgeVertexCount);
|
|
2477
3107
|
const iy = localVertexIndex.div(edgeVertexCount);
|
|
2478
|
-
const normalXZ = loadTerrainFieldNormal(
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
);
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
3108
|
+
const normalXZ = loadTerrainFieldNormal(terrainFieldStorage, ix, iy, nodeIndex);
|
|
3109
|
+
const normal = unpackTangentNormal(normalXZ.x, normalXZ.y);
|
|
3110
|
+
return { ix, iy, normal };
|
|
3111
|
+
}
|
|
3112
|
+
function createTileLocalNormal(leafStorage, terrainUniforms, terrainFieldStorage, projection = "flat") {
|
|
3113
|
+
if (!terrainFieldStorage) return vec3(0, 1, 0);
|
|
3114
|
+
if (projection === "cubeSphere") {
|
|
3115
|
+
return Fn(() => {
|
|
3116
|
+
const { ix, iy, normal } = loadTangentNormal(terrainUniforms, terrainFieldStorage);
|
|
3117
|
+
const tile = decodeLeafTile(leafStorage, int(instanceIndex));
|
|
3118
|
+
const innerSeg = terrainUniforms.uInnerTileSegments.toVar().toFloat();
|
|
3119
|
+
const localU = ix.toFloat().sub(float(1)).div(innerSeg).max(float(0)).min(float(1));
|
|
3120
|
+
const localV = iy.toFloat().sub(float(1)).div(innerSeg).max(float(0)).min(float(1));
|
|
3121
|
+
const faceUV = faceUVFromTileLocal(tile, localU, localV);
|
|
3122
|
+
const basis = cubeFaceBasis(tile.face);
|
|
3123
|
+
const dir = cubeFaceDirection(basis, faceUV.x, faceUV.y);
|
|
3124
|
+
return sphereTangentFrameNormal(dir, basis, normal);
|
|
3125
|
+
})();
|
|
3126
|
+
}
|
|
3127
|
+
return Fn(() => {
|
|
3128
|
+
const { normal } = loadTangentNormal(terrainUniforms, terrainFieldStorage);
|
|
3129
|
+
return normal;
|
|
3130
|
+
})();
|
|
3131
|
+
}
|
|
3132
|
+
function createCubeSphereWorldPosition(leafStorage, terrainUniforms, terrainFieldStorage) {
|
|
3133
|
+
return Fn(() => {
|
|
3134
|
+
const tile = decodeLeafTile(leafStorage, int(instanceIndex));
|
|
3135
|
+
const half = float(0.5);
|
|
3136
|
+
const localU = positionLocal.x.max(half.negate()).min(half).add(half);
|
|
3137
|
+
const localV = positionLocal.z.max(half.negate()).min(half).add(half);
|
|
3138
|
+
const faceUV = faceUVFromTileLocal(tile, localU, localV);
|
|
3139
|
+
const basis = cubeFaceBasis(tile.face);
|
|
3140
|
+
const dir = cubeFaceDirection(basis, faceUV.x, faceUV.y);
|
|
3141
|
+
const yElevation = createTileElevation(terrainUniforms, terrainFieldStorage);
|
|
3142
|
+
const baseRadius = terrainUniforms.uRadius.toVar().add(yElevation);
|
|
3143
|
+
const skirtVertex = isSkirtVertex(terrainUniforms.uInnerTileSegments);
|
|
3144
|
+
const r = select(skirtVertex, baseRadius.sub(terrainUniforms.uSkirtScale.toVar()), baseRadius);
|
|
3145
|
+
createNormalAssignment(leafStorage, terrainUniforms, terrainFieldStorage, "cubeSphere");
|
|
3146
|
+
const origin = terrainUniforms.uRootOrigin.toVar();
|
|
3147
|
+
return origin.add(dir.mul(r));
|
|
3148
|
+
})();
|
|
3149
|
+
}
|
|
3150
|
+
function createTileWorldPosition(leafStorage, terrainUniforms, terrainFieldStorage, projection = "flat") {
|
|
3151
|
+
if (projection === "cubeSphere") {
|
|
3152
|
+
return createCubeSphereWorldPosition(leafStorage, terrainUniforms, terrainFieldStorage);
|
|
3153
|
+
}
|
|
3154
|
+
const baseWorldPosition = createTileBaseWorldPosition(leafStorage, terrainUniforms);
|
|
2495
3155
|
return Fn(() => {
|
|
2496
3156
|
const base = baseWorldPosition();
|
|
2497
|
-
const yElevation = createTileElevation(
|
|
2498
|
-
terrainUniforms,
|
|
2499
|
-
terrainFieldStorage
|
|
2500
|
-
);
|
|
3157
|
+
const yElevation = createTileElevation(terrainUniforms, terrainFieldStorage);
|
|
2501
3158
|
const skirtVertex = isSkirtVertex(terrainUniforms.uInnerTileSegments);
|
|
2502
3159
|
const skirtY = base.y.add(yElevation).sub(terrainUniforms.uSkirtScale.toVar());
|
|
2503
3160
|
const worldY = select(skirtVertex, skirtY, base.y.add(yElevation));
|
|
2504
|
-
createNormalAssignment(terrainUniforms, terrainFieldStorage);
|
|
3161
|
+
createNormalAssignment(leafStorage, terrainUniforms, terrainFieldStorage, "flat");
|
|
2505
3162
|
return vec3(base.x, worldY, base.z);
|
|
2506
3163
|
})();
|
|
2507
3164
|
}
|
|
@@ -2510,11 +3167,13 @@ const positionNodeTask = task((get, work) => {
|
|
|
2510
3167
|
const leafStorage = get(leafStorageTask);
|
|
2511
3168
|
const terrainUniforms = get(updateUniformsTask);
|
|
2512
3169
|
const terrainFieldStorage = get(createTerrainFieldTextureTask);
|
|
3170
|
+
const topology = get(topologyTask);
|
|
2513
3171
|
return work(
|
|
2514
3172
|
() => createTileWorldPosition(
|
|
2515
3173
|
leafStorage,
|
|
2516
3174
|
terrainUniforms,
|
|
2517
|
-
terrainFieldStorage
|
|
3175
|
+
terrainFieldStorage,
|
|
3176
|
+
topology.projection ?? "flat"
|
|
2518
3177
|
)
|
|
2519
3178
|
);
|
|
2520
3179
|
}).displayName("positionNodeTask");
|
|
@@ -2555,71 +3214,33 @@ function getTerrainBounds(config) {
|
|
|
2555
3214
|
maxZ: config.originZ + halfRoot
|
|
2556
3215
|
};
|
|
2557
3216
|
}
|
|
2558
|
-
function
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
if (
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
3217
|
+
function terrainSignedDistance(query, worldX, worldY, worldZ, skipBoundsFastPath) {
|
|
3218
|
+
if (!skipBoundsFastPath) {
|
|
3219
|
+
const tileBounds = query.getTileBounds(worldX, worldZ);
|
|
3220
|
+
if (tileBounds) {
|
|
3221
|
+
if (worldY > tileBounds.maxElevation) {
|
|
3222
|
+
return worldY - tileBounds.maxElevation;
|
|
3223
|
+
}
|
|
3224
|
+
if (worldY < tileBounds.minElevation) {
|
|
3225
|
+
return worldY - tileBounds.minElevation;
|
|
3226
|
+
}
|
|
2566
3227
|
}
|
|
2567
3228
|
}
|
|
2568
3229
|
const elevation = query.getElevation(worldX, worldZ);
|
|
2569
3230
|
if (!Number.isFinite(elevation)) return void 0;
|
|
2570
3231
|
return worldY - elevation;
|
|
2571
3232
|
}
|
|
2572
|
-
function
|
|
2573
|
-
const elevation = query.getElevation(worldX, worldZ);
|
|
2574
|
-
if (!Number.isFinite(elevation)) return void 0;
|
|
2575
|
-
return worldY - elevation;
|
|
2576
|
-
}
|
|
2577
|
-
function cpuRaycast(query, ray, config, options) {
|
|
2578
|
-
const bounds = getTerrainBounds(config);
|
|
2579
|
-
const segment = intersectRayAabb(
|
|
2580
|
-
ray,
|
|
2581
|
-
bounds.minX,
|
|
2582
|
-
config.minY,
|
|
2583
|
-
bounds.minZ,
|
|
2584
|
-
bounds.maxX,
|
|
2585
|
-
config.maxY,
|
|
2586
|
-
bounds.maxZ
|
|
2587
|
-
);
|
|
2588
|
-
if (!segment) return null;
|
|
2589
|
-
const maxDistance = options?.maxDistance ?? Number.POSITIVE_INFINITY;
|
|
2590
|
-
let startT = Math.max(0, segment.tMin);
|
|
2591
|
-
const endT = Math.min(segment.tMax, maxDistance);
|
|
2592
|
-
if (endT < startT) return null;
|
|
2593
|
-
const maxSteps = Math.max(8, options?.maxSteps ?? 128);
|
|
2594
|
-
const refinementSteps = Math.max(1, options?.refinementSteps ?? 8);
|
|
2595
|
-
const point = new Vector3();
|
|
3233
|
+
function marchSignedDistance(ray, startT, endT, stepSignedDistanceAt, refineSignedDistanceAt, options, point) {
|
|
2596
3234
|
let prevT = startT;
|
|
2597
3235
|
ray.at(prevT, point);
|
|
2598
|
-
let prevSignedDistance =
|
|
2599
|
-
query,
|
|
2600
|
-
point.x,
|
|
2601
|
-
point.y,
|
|
2602
|
-
point.z
|
|
2603
|
-
);
|
|
3236
|
+
let prevSignedDistance = stepSignedDistanceAt(point.x, point.y, point.z);
|
|
2604
3237
|
if (prevSignedDistance !== void 0 && prevSignedDistance <= 0) {
|
|
2605
|
-
|
|
2606
|
-
if (!sample.valid) return null;
|
|
2607
|
-
point.y = sample.elevation;
|
|
2608
|
-
return {
|
|
2609
|
-
position: point.clone(),
|
|
2610
|
-
normal: sample.normal.clone(),
|
|
2611
|
-
distance: ray.origin.distanceTo(point)
|
|
2612
|
-
};
|
|
3238
|
+
return startT;
|
|
2613
3239
|
}
|
|
2614
|
-
for (let i = 1; i <= maxSteps; i += 1) {
|
|
2615
|
-
const t = startT + (endT - startT) * i / maxSteps;
|
|
3240
|
+
for (let i = 1; i <= options.maxSteps; i += 1) {
|
|
3241
|
+
const t = startT + (endT - startT) * i / options.maxSteps;
|
|
2616
3242
|
ray.at(t, point);
|
|
2617
|
-
const signedDistance =
|
|
2618
|
-
query,
|
|
2619
|
-
point.x,
|
|
2620
|
-
point.y,
|
|
2621
|
-
point.z
|
|
2622
|
-
);
|
|
3243
|
+
const signedDistance = stepSignedDistanceAt(point.x, point.y, point.z);
|
|
2623
3244
|
if (signedDistance === void 0) {
|
|
2624
3245
|
prevSignedDistance = void 0;
|
|
2625
3246
|
prevT = t;
|
|
@@ -2628,15 +3249,10 @@ function cpuRaycast(query, ray, config, options) {
|
|
|
2628
3249
|
if (prevSignedDistance !== void 0 && prevSignedDistance > 0 && signedDistance <= 0) {
|
|
2629
3250
|
let lo = prevT;
|
|
2630
3251
|
let hi = t;
|
|
2631
|
-
for (let r = 0; r < refinementSteps; r += 1) {
|
|
3252
|
+
for (let r = 0; r < options.refinementSteps; r += 1) {
|
|
2632
3253
|
const mid = (lo + hi) * 0.5;
|
|
2633
3254
|
ray.at(mid, point);
|
|
2634
|
-
const midDistance =
|
|
2635
|
-
query,
|
|
2636
|
-
point.x,
|
|
2637
|
-
point.y,
|
|
2638
|
-
point.z
|
|
2639
|
-
);
|
|
3255
|
+
const midDistance = refineSignedDistanceAt(point.x, point.y, point.z);
|
|
2640
3256
|
if (midDistance === void 0) {
|
|
2641
3257
|
lo = mid;
|
|
2642
3258
|
continue;
|
|
@@ -2644,22 +3260,53 @@ function cpuRaycast(query, ray, config, options) {
|
|
|
2644
3260
|
if (midDistance > 0) lo = mid;
|
|
2645
3261
|
else hi = mid;
|
|
2646
3262
|
}
|
|
2647
|
-
|
|
2648
|
-
ray.at(hitT, point);
|
|
2649
|
-
const sample = query.sampleTerrain(point.x, point.z);
|
|
2650
|
-
if (!sample.valid) return null;
|
|
2651
|
-
point.y = sample.elevation;
|
|
2652
|
-
return {
|
|
2653
|
-
position: point.clone(),
|
|
2654
|
-
normal: sample.normal.clone(),
|
|
2655
|
-
distance: ray.origin.distanceTo(point)
|
|
2656
|
-
};
|
|
3263
|
+
return hi;
|
|
2657
3264
|
}
|
|
2658
3265
|
prevSignedDistance = signedDistance;
|
|
2659
3266
|
prevT = t;
|
|
2660
3267
|
}
|
|
2661
3268
|
return null;
|
|
2662
3269
|
}
|
|
3270
|
+
function cpuRaycast(query, ray, config, options) {
|
|
3271
|
+
const bounds = getTerrainBounds(config);
|
|
3272
|
+
const segment = intersectRayAabb(
|
|
3273
|
+
ray,
|
|
3274
|
+
bounds.minX,
|
|
3275
|
+
config.minY,
|
|
3276
|
+
bounds.minZ,
|
|
3277
|
+
bounds.maxX,
|
|
3278
|
+
config.maxY,
|
|
3279
|
+
bounds.maxZ
|
|
3280
|
+
);
|
|
3281
|
+
if (!segment) return null;
|
|
3282
|
+
const maxDistance = options?.maxDistance ?? Number.POSITIVE_INFINITY;
|
|
3283
|
+
const startT = Math.max(0, segment.tMin);
|
|
3284
|
+
const endT = Math.min(segment.tMax, maxDistance);
|
|
3285
|
+
if (endT < startT) return null;
|
|
3286
|
+
const point = new Vector3();
|
|
3287
|
+
const hitT = marchSignedDistance(
|
|
3288
|
+
ray,
|
|
3289
|
+
startT,
|
|
3290
|
+
endT,
|
|
3291
|
+
(px, py, pz) => terrainSignedDistance(query, px, py, pz, false),
|
|
3292
|
+
(px, py, pz) => terrainSignedDistance(query, px, py, pz, true),
|
|
3293
|
+
{
|
|
3294
|
+
maxSteps: Math.max(8, options?.maxSteps ?? 128),
|
|
3295
|
+
refinementSteps: Math.max(1, options?.refinementSteps ?? 8)
|
|
3296
|
+
},
|
|
3297
|
+
point
|
|
3298
|
+
);
|
|
3299
|
+
if (hitT === null) return null;
|
|
3300
|
+
ray.at(hitT, point);
|
|
3301
|
+
const sample = query.sampleTerrain(point.x, point.z);
|
|
3302
|
+
if (!sample.valid) return null;
|
|
3303
|
+
point.y = sample.elevation;
|
|
3304
|
+
return {
|
|
3305
|
+
position: point.clone(),
|
|
3306
|
+
normal: sample.normal.clone(),
|
|
3307
|
+
distance: ray.origin.distanceTo(point)
|
|
3308
|
+
};
|
|
3309
|
+
}
|
|
2663
3310
|
function cpuRaycastBoundsOnly(ray, config, options) {
|
|
2664
3311
|
const bounds = getTerrainBounds(config);
|
|
2665
3312
|
const planeY = (config.minY + config.maxY) * 0.5;
|
|
@@ -2680,12 +3327,102 @@ function cpuRaycastBoundsOnly(ray, config, options) {
|
|
|
2680
3327
|
distance: ray.origin.distanceTo(point)
|
|
2681
3328
|
};
|
|
2682
3329
|
}
|
|
3330
|
+
function intersectRaySphere(ray, cx, cy, cz, radius) {
|
|
3331
|
+
const ox = ray.origin.x - cx;
|
|
3332
|
+
const oy = ray.origin.y - cy;
|
|
3333
|
+
const oz = ray.origin.z - cz;
|
|
3334
|
+
const dx = ray.direction.x;
|
|
3335
|
+
const dy = ray.direction.y;
|
|
3336
|
+
const dz = ray.direction.z;
|
|
3337
|
+
const a = dx * dx + dy * dy + dz * dz;
|
|
3338
|
+
const b = 2 * (ox * dx + oy * dy + oz * dz);
|
|
3339
|
+
const c = ox * ox + oy * oy + oz * oz - radius * radius;
|
|
3340
|
+
const disc = b * b - 4 * a * c;
|
|
3341
|
+
if (disc < 0) return null;
|
|
3342
|
+
const sqrtDisc = Math.sqrt(disc);
|
|
3343
|
+
const inv2a = 1 / (2 * a);
|
|
3344
|
+
return { t0: (-b - sqrtDisc) * inv2a, t1: (-b + sqrtDisc) * inv2a };
|
|
3345
|
+
}
|
|
3346
|
+
function sphereSignedDistance(query, config, px, py, pz, scratchDir) {
|
|
3347
|
+
const cx = config.centerX ?? 0;
|
|
3348
|
+
const cy = config.centerY ?? 0;
|
|
3349
|
+
const cz = config.centerZ ?? 0;
|
|
3350
|
+
const radius = config.radius ?? 0;
|
|
3351
|
+
const dx = px - cx;
|
|
3352
|
+
const dy = py - cy;
|
|
3353
|
+
const dz = pz - cz;
|
|
3354
|
+
const dist = Math.hypot(dx, dy, dz);
|
|
3355
|
+
scratchDir.set(dx, dy, dz);
|
|
3356
|
+
const elevation = query.getElevationByDirection(scratchDir);
|
|
3357
|
+
if (elevation === null) return void 0;
|
|
3358
|
+
return dist - (radius + elevation);
|
|
3359
|
+
}
|
|
3360
|
+
function cubeSphereRaycast(query, ray, config, options) {
|
|
3361
|
+
const cx = config.centerX ?? 0;
|
|
3362
|
+
const cy = config.centerY ?? 0;
|
|
3363
|
+
const cz = config.centerZ ?? 0;
|
|
3364
|
+
const radius = config.radius ?? 0;
|
|
3365
|
+
const outerRadius = config.maxRadius ?? radius;
|
|
3366
|
+
const shell = intersectRaySphere(ray, cx, cy, cz, outerRadius);
|
|
3367
|
+
if (!shell) return null;
|
|
3368
|
+
const maxDistance = options?.maxDistance ?? Number.POSITIVE_INFINITY;
|
|
3369
|
+
const startT = Math.max(0, shell.t0);
|
|
3370
|
+
const endT = Math.min(shell.t1, maxDistance);
|
|
3371
|
+
if (endT < startT) return null;
|
|
3372
|
+
const scratchDir = new Vector3();
|
|
3373
|
+
const point = new Vector3();
|
|
3374
|
+
const signedDistanceAt = (px, py, pz) => sphereSignedDistance(query, config, px, py, pz, scratchDir);
|
|
3375
|
+
const hitT = marchSignedDistance(
|
|
3376
|
+
ray,
|
|
3377
|
+
startT,
|
|
3378
|
+
endT,
|
|
3379
|
+
signedDistanceAt,
|
|
3380
|
+
signedDistanceAt,
|
|
3381
|
+
{
|
|
3382
|
+
maxSteps: Math.max(8, options?.maxSteps ?? 256),
|
|
3383
|
+
refinementSteps: Math.max(1, options?.refinementSteps ?? 12)
|
|
3384
|
+
},
|
|
3385
|
+
point
|
|
3386
|
+
);
|
|
3387
|
+
if (hitT === null) return null;
|
|
3388
|
+
ray.at(hitT, point);
|
|
3389
|
+
const sample = query.sampleTerrainByPosition(point);
|
|
3390
|
+
if (!sample.valid) return null;
|
|
3391
|
+
return {
|
|
3392
|
+
position: sample.position.clone(),
|
|
3393
|
+
normal: sample.normal.clone(),
|
|
3394
|
+
distance: ray.origin.distanceTo(sample.position)
|
|
3395
|
+
};
|
|
3396
|
+
}
|
|
3397
|
+
function cubeSphereRaycastBoundsOnly(ray, config, options) {
|
|
3398
|
+
const cx = config.centerX ?? 0;
|
|
3399
|
+
const cy = config.centerY ?? 0;
|
|
3400
|
+
const cz = config.centerZ ?? 0;
|
|
3401
|
+
const radius = config.radius ?? 0;
|
|
3402
|
+
const shell = intersectRaySphere(ray, cx, cy, cz, radius);
|
|
3403
|
+
if (!shell) return null;
|
|
3404
|
+
const maxDistance = options?.maxDistance ?? Number.POSITIVE_INFINITY;
|
|
3405
|
+
const t = shell.t0 >= 0 ? shell.t0 : shell.t1;
|
|
3406
|
+
if (t < 0 || t > maxDistance) return null;
|
|
3407
|
+
const point = new Vector3();
|
|
3408
|
+
ray.at(t, point);
|
|
3409
|
+
const normal = new Vector3(point.x - cx, point.y - cy, point.z - cz).normalize();
|
|
3410
|
+
return { position: point, normal, distance: ray.origin.distanceTo(point) };
|
|
3411
|
+
}
|
|
2683
3412
|
|
|
2684
3413
|
function createTerrainRaycast(params) {
|
|
2685
3414
|
return {
|
|
2686
3415
|
pick(ray, options) {
|
|
2687
3416
|
const config = params.getConfig();
|
|
2688
3417
|
const terrainQuery = params.getTerrainQuery();
|
|
3418
|
+
if (config.projection === "cubeSphere") {
|
|
3419
|
+
const sphereQuery = params.getSphereQuery();
|
|
3420
|
+
if (sphereQuery) {
|
|
3421
|
+
const precise = cubeSphereRaycast(sphereQuery, ray, config, options);
|
|
3422
|
+
if (precise) return precise;
|
|
3423
|
+
}
|
|
3424
|
+
return cubeSphereRaycastBoundsOnly(ray, config, options);
|
|
3425
|
+
}
|
|
2689
3426
|
if (terrainQuery) {
|
|
2690
3427
|
const precise = cpuRaycast(terrainQuery, ray, config, options);
|
|
2691
3428
|
if (precise) return precise;
|
|
@@ -2710,29 +3447,47 @@ const BOUNDS_PADDING = 1;
|
|
|
2710
3447
|
const RAYCAST_STATE = Symbol("terrainRaycastTaskState");
|
|
2711
3448
|
const terrainRaycastTask = task(
|
|
2712
3449
|
(get, work) => {
|
|
2713
|
-
const { query: terrainQuery } = get(terrainQueryTask);
|
|
3450
|
+
const { query: terrainQuery, sphereQuery } = get(terrainQueryTask);
|
|
2714
3451
|
const rootSizeValue = get(rootSize);
|
|
2715
3452
|
const originValue = get(origin);
|
|
2716
3453
|
const elevationScaleValue = get(elevationScale);
|
|
3454
|
+
const radiusValue = get(radius);
|
|
3455
|
+
const topologyValue = get(topologyTask);
|
|
3456
|
+
const projection = topologyValue.projection ?? "flat";
|
|
3457
|
+
const sphereRadius = topologyValue.radius ?? radiusValue;
|
|
2717
3458
|
return work((prev) => {
|
|
2718
3459
|
let raycast = prev;
|
|
2719
3460
|
let state = raycast?.[RAYCAST_STATE];
|
|
2720
3461
|
if (!state) {
|
|
2721
3462
|
state = {
|
|
2722
3463
|
terrainQuery: null,
|
|
3464
|
+
sphereQuery: null,
|
|
2723
3465
|
bounds: {
|
|
2724
3466
|
rootSize: 0,
|
|
2725
3467
|
originX: 0,
|
|
2726
3468
|
originZ: 0,
|
|
2727
3469
|
minY: 0,
|
|
2728
|
-
maxY: 0
|
|
3470
|
+
maxY: 0,
|
|
3471
|
+
projection: "flat",
|
|
3472
|
+
centerX: 0,
|
|
3473
|
+
centerY: 0,
|
|
3474
|
+
centerZ: 0,
|
|
3475
|
+
radius: 0,
|
|
3476
|
+
minRadius: 0,
|
|
3477
|
+
maxRadius: 0
|
|
2729
3478
|
}
|
|
2730
3479
|
};
|
|
2731
3480
|
}
|
|
2732
3481
|
state.terrainQuery = terrainQuery;
|
|
3482
|
+
state.sphereQuery = sphereQuery;
|
|
2733
3483
|
state.bounds.rootSize = rootSizeValue;
|
|
2734
3484
|
state.bounds.originX = originValue.x;
|
|
2735
3485
|
state.bounds.originZ = originValue.z;
|
|
3486
|
+
state.bounds.projection = projection;
|
|
3487
|
+
state.bounds.centerX = originValue.x;
|
|
3488
|
+
state.bounds.centerY = originValue.y;
|
|
3489
|
+
state.bounds.centerZ = originValue.z;
|
|
3490
|
+
state.bounds.radius = sphereRadius;
|
|
2736
3491
|
const range = terrainQuery.getGlobalElevationRange();
|
|
2737
3492
|
if (range) {
|
|
2738
3493
|
state.bounds.minY = range.min - BOUNDS_PADDING;
|
|
@@ -2742,9 +3497,19 @@ const terrainRaycastTask = task(
|
|
|
2742
3497
|
state.bounds.minY = originValue.y - verticalExtent;
|
|
2743
3498
|
state.bounds.maxY = originValue.y + verticalExtent;
|
|
2744
3499
|
}
|
|
3500
|
+
const elevationExtent = Math.max(1, Math.abs(elevationScaleValue));
|
|
3501
|
+
let dispMin = -elevationExtent;
|
|
3502
|
+
let dispMax = elevationExtent;
|
|
3503
|
+
if (range) {
|
|
3504
|
+
dispMin = range.min - originValue.y;
|
|
3505
|
+
dispMax = range.max - originValue.y;
|
|
3506
|
+
}
|
|
3507
|
+
state.bounds.minRadius = Math.max(0, sphereRadius + dispMin - BOUNDS_PADDING);
|
|
3508
|
+
state.bounds.maxRadius = sphereRadius + dispMax + BOUNDS_PADDING;
|
|
2745
3509
|
if (!raycast) {
|
|
2746
3510
|
raycast = createTerrainRaycast({
|
|
2747
3511
|
getTerrainQuery: () => state.terrainQuery,
|
|
3512
|
+
getSphereQuery: () => state.sphereQuery,
|
|
2748
3513
|
getConfig: () => state.bounds
|
|
2749
3514
|
});
|
|
2750
3515
|
}
|
|
@@ -2754,15 +3519,12 @@ const terrainRaycastTask = task(
|
|
|
2754
3519
|
}
|
|
2755
3520
|
).displayName("terrainRaycastTask");
|
|
2756
3521
|
|
|
2757
|
-
function terrainGraph() {
|
|
2758
|
-
return graph().add(instanceIdTask).add(quadtreeConfigTask).add(quadtreeUpdateTask).add(leafStorageTask).add(surfaceTask).add(leafGpuBufferTask).add(gpuSpatialIndexStorageTask).add(gpuSpatialIndexUploadTask).add(createUniformsTask).add(updateUniformsTask).add(positionNodeTask).add(createElevationFieldContextTask).add(tileNodesTask).add(createTerrainFieldTextureTask).add(createTerrainSamplerTask).add(elevationFieldStageTask).add(terrainFieldStageTask).add(compileComputeTask).add(executeComputeTask).add(tileBoundsContextTask).add(tileBoundsReductionTask).add(terrainQueryTask).add(terrainReadbackTask).add(terrainRaycastTask);
|
|
2759
|
-
}
|
|
2760
3522
|
const terrainTasks = {
|
|
2761
3523
|
instanceId: instanceIdTask,
|
|
2762
3524
|
quadtreeConfig: quadtreeConfigTask,
|
|
2763
3525
|
quadtreeUpdate: quadtreeUpdateTask,
|
|
2764
3526
|
leafStorage: leafStorageTask,
|
|
2765
|
-
|
|
3527
|
+
topology: topologyTask,
|
|
2766
3528
|
leafGpuBuffer: leafGpuBufferTask,
|
|
2767
3529
|
gpuSpatialIndexStorage: gpuSpatialIndexStorageTask,
|
|
2768
3530
|
gpuSpatialIndexUpload: gpuSpatialIndexUploadTask,
|
|
@@ -2783,6 +3545,13 @@ const terrainTasks = {
|
|
|
2783
3545
|
terrainReadback: terrainReadbackTask,
|
|
2784
3546
|
terrainRaycast: terrainRaycastTask
|
|
2785
3547
|
};
|
|
3548
|
+
function terrainGraph() {
|
|
3549
|
+
const g = graph();
|
|
3550
|
+
for (const t of Object.values(terrainTasks)) {
|
|
3551
|
+
g.add(t);
|
|
3552
|
+
}
|
|
3553
|
+
return g;
|
|
3554
|
+
}
|
|
2786
3555
|
|
|
2787
3556
|
const textureSpaceToVectorSpace = Fn(([value]) => {
|
|
2788
3557
|
return remap(value, float(0), float(1), float(-1), float(1));
|
|
@@ -2793,7 +3562,7 @@ const vectorSpaceToTextureSpace = Fn(([value]) => {
|
|
|
2793
3562
|
const blendAngleCorrectedNormals = Fn(([n1, n2]) => {
|
|
2794
3563
|
const t = vec3(n1.x, n1.y, n1.z.add(1));
|
|
2795
3564
|
const u = vec3(n2.x.negate(), n2.y.negate(), n2.z);
|
|
2796
|
-
const r = t.mul(dot(t, u)).sub(u.mul(t.z)).normalize();
|
|
3565
|
+
const r = t.mul(dot$1(t, u)).sub(u.mul(t.z)).normalize();
|
|
2797
3566
|
return r;
|
|
2798
3567
|
});
|
|
2799
3568
|
const deriveNormalZ = Fn(([normalXY]) => {
|
|
@@ -2836,4 +3605,4 @@ const voronoiCells = Fn((params) => {
|
|
|
2836
3605
|
return k;
|
|
2837
3606
|
});
|
|
2838
3607
|
|
|
2839
|
-
export { ArrayTextureBackend, AtlasBackend, Dir, TerrainGeometry, TerrainMesh,
|
|
3608
|
+
export { ArrayTextureBackend, AtlasBackend, CUBE_FACES, CUBE_FACE_COUNT, Dir, TerrainGeometry, TerrainMesh, U32_EMPTY, allocLeafSet, allocSeamTable, beginUpdate, blendAngleCorrectedNormals, buildLeafIndex, buildSeams2to1, compileComputeTask, createComputePipelineTasks, createCubeSphereTopology, createElevationFieldContextTask, createFlatTopology, createInfiniteFlatTopology, createSpatialIndex, createState, createTerrainFieldStorage, createTerrainFieldTextureTask, createTerrainQuery, createTerrainRaycast, createTerrainSampler, createTerrainSamplerTask, createTerrainSphereQuery, createTerrainUniforms, createUniformsTask, cubeFaceBasis, cubeFaceDirection, cubeFaceFromDirection, cubeFacePoint, cubeFaceUVFromDirection, deriveNormalZ, directionToFace, directionToFaceUV, directionToLatLong, elevationFieldStageTask, elevationFn, elevationScale, executeComputeTask, faceUVToCube, getDeviceComputeLimits, gpuSpatialIndexStorageTask, gpuSpatialIndexUploadTask, innerTileSegments, instanceIdTask, isSkirtUV, isSkirtVertex, latLongToDirection, leafGpuBufferTask, leafStorageTask, loadTerrainField, loadTerrainFieldElevation, loadTerrainFieldNormal, maxLevel, maxNodes, origin, packTerrainFieldSample, positionNodeTask, quadtreeConfigTask, quadtreeUpdate, quadtreeUpdateTask, radius, resetLeafSet, resetSeamTable, rootSize, sampleTerrainField, sampleTerrainFieldElevation, skirtScale, sphereTangentFrameNormal, storeTerrainField, tangentFromAxis, terrainFieldFilter, terrainFieldStageTask, terrainGraph, terrainQueryTask, terrainRaycastTask, terrainReadbackTask, terrainTasks, textureSpaceToVectorSpace, tileNodesTask, topology, topologyTask, unpackTangentNormal, update, updateUniformsTask, vElevation, vGlobalVertexIndex, vectorSpaceToTextureSpace, voronoiCells };
|