@hello-terrain/three 0.0.0-alpha.1 → 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/README.md +179 -23
- package/dist/index.cjs +3489 -26
- package/dist/index.d.cts +1058 -11
- package/dist/index.d.mts +1058 -11
- package/dist/index.d.ts +1058 -11
- package/dist/index.mjs +3401 -29
- package/package.json +14 -3
package/dist/index.mjs
CHANGED
|
@@ -1,29 +1,31 @@
|
|
|
1
|
-
import { BufferGeometry, BufferAttribute } from 'three';
|
|
2
|
-
import {
|
|
1
|
+
import { BufferGeometry, BufferAttribute, RGBAFormat, ClampToEdgeWrapping, HalfFloatType, FloatType, LinearFilter, NearestFilter, Vector3 } from 'three';
|
|
2
|
+
import { MeshStandardNodeMaterial, InstancedMesh, InstancedBufferAttribute, StorageTexture, StorageArrayTexture, StorageBufferAttribute, Vector3 as Vector3$1 } from 'three/webgpu';
|
|
3
|
+
import { param, task, graph } from '@hello-terrain/work';
|
|
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
|
+
import { Fn as Fn$1 } from 'three/src/nodes/TSL.js';
|
|
3
6
|
|
|
4
7
|
class TerrainGeometry extends BufferGeometry {
|
|
5
|
-
|
|
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) {
|
|
6
16
|
super();
|
|
7
17
|
if (innerSegments < 1 || !Number.isFinite(innerSegments) || !Number.isInteger(innerSegments)) {
|
|
8
|
-
throw new Error(
|
|
9
|
-
`Invalid innerSegments: ${innerSegments}. Must be a positive integer.`
|
|
10
|
-
);
|
|
18
|
+
throw new Error(`Invalid innerSegments: ${innerSegments}. Must be a positive integer.`);
|
|
11
19
|
}
|
|
12
20
|
try {
|
|
13
|
-
this.setIndex(this.generateIndices(innerSegments));
|
|
21
|
+
this.setIndex(this.generateIndices(innerSegments, flipWinding));
|
|
14
22
|
this.setAttribute(
|
|
15
23
|
"position",
|
|
16
|
-
new BufferAttribute(
|
|
17
|
-
new Float32Array(this.generatePositions(innerSegments)),
|
|
18
|
-
3
|
|
19
|
-
)
|
|
24
|
+
new BufferAttribute(new Float32Array(this.generatePositions(innerSegments)), 3)
|
|
20
25
|
);
|
|
21
26
|
this.setAttribute(
|
|
22
27
|
"normal",
|
|
23
|
-
new BufferAttribute(
|
|
24
|
-
new Float32Array(this.generateNormals(innerSegments)),
|
|
25
|
-
3
|
|
26
|
-
)
|
|
28
|
+
new BufferAttribute(new Float32Array(this.generateNormals(innerSegments)), 3)
|
|
27
29
|
);
|
|
28
30
|
this.setAttribute(
|
|
29
31
|
"uv",
|
|
@@ -59,17 +61,19 @@ class TerrainGeometry extends BufferGeometry {
|
|
|
59
61
|
* | / | \ | / | \ |
|
|
60
62
|
* o---o---o---o---o
|
|
61
63
|
*
|
|
62
|
-
* INNER GRID (
|
|
63
|
-
* o---o---o
|
|
64
|
-
* | \ | \ |
|
|
65
|
-
* o---o---o
|
|
66
|
-
* | \ | \ |
|
|
67
|
-
* o---o---o
|
|
64
|
+
* INNER GRID (alternating diagonals — checkerboard pattern):
|
|
65
|
+
* o---o---o---o---o
|
|
66
|
+
* | \ | / | \ | / |
|
|
67
|
+
* o---o---o---o---o
|
|
68
|
+
* | / | \ | / | \ |
|
|
69
|
+
* o---o---o---o---o
|
|
70
|
+
* | \ | / | \ | / |
|
|
71
|
+
* o---o---o---o---o
|
|
68
72
|
*
|
|
69
73
|
* Where o = vertex
|
|
70
74
|
* Each square cell is split into 2 triangles.
|
|
71
75
|
* - Skirt cells (outer ring): diagonal flip based on quadrant for corner correctness
|
|
72
|
-
* - Inner cells:
|
|
76
|
+
* - Inner cells: alternating diagonal via (x+y)%2 to reduce interpolation artifacts
|
|
73
77
|
*
|
|
74
78
|
* Vertex layout (for innerSegments = 2):
|
|
75
79
|
*
|
|
@@ -96,12 +100,16 @@ class TerrainGeometry extends BufferGeometry {
|
|
|
96
100
|
* triangle 1: a, c, b
|
|
97
101
|
* triangle 2: b, c, d
|
|
98
102
|
*/
|
|
99
|
-
generateIndices(innerSegments) {
|
|
103
|
+
generateIndices(innerSegments, flipWinding = false) {
|
|
100
104
|
const innerEdgeVertexCount = innerSegments + 1;
|
|
101
105
|
const edgeVertexCountWithSkirt = innerEdgeVertexCount + 2;
|
|
102
106
|
const indices = [];
|
|
103
107
|
const cellsPerEdge = edgeVertexCountWithSkirt - 1;
|
|
104
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
|
+
};
|
|
105
113
|
for (let y = 0; y < cellsPerEdge; y++) {
|
|
106
114
|
for (let x = 0; x < cellsPerEdge; x++) {
|
|
107
115
|
const a = y * edgeVertexCountWithSkirt + x;
|
|
@@ -115,14 +123,14 @@ class TerrainGeometry extends BufferGeometry {
|
|
|
115
123
|
const topHalf = y < mid;
|
|
116
124
|
useDefaultDiagonal = leftHalf && topHalf || !leftHalf && !topHalf;
|
|
117
125
|
} else {
|
|
118
|
-
useDefaultDiagonal =
|
|
126
|
+
useDefaultDiagonal = (x + y) % 2 === 0;
|
|
119
127
|
}
|
|
120
128
|
if (useDefaultDiagonal) {
|
|
121
|
-
|
|
122
|
-
|
|
129
|
+
pushTri(a, d, b);
|
|
130
|
+
pushTri(a, c, d);
|
|
123
131
|
} else {
|
|
124
|
-
|
|
125
|
-
|
|
132
|
+
pushTri(a, c, b);
|
|
133
|
+
pushTri(b, c, d);
|
|
126
134
|
}
|
|
127
135
|
}
|
|
128
136
|
}
|
|
@@ -213,6 +221,2832 @@ class TerrainGeometry extends BufferGeometry {
|
|
|
213
221
|
}
|
|
214
222
|
}
|
|
215
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
|
+
|
|
247
|
+
const defaultTerrainMeshParams = {
|
|
248
|
+
// Source of truth is the `innerTileSegments` param itself.
|
|
249
|
+
innerTileSegments: innerTileSegments.get(),
|
|
250
|
+
maxNodes: 1024,
|
|
251
|
+
material: new MeshStandardNodeMaterial(),
|
|
252
|
+
flipWinding: false
|
|
253
|
+
};
|
|
254
|
+
class TerrainMesh extends InstancedMesh {
|
|
255
|
+
_innerTileSegments;
|
|
256
|
+
_maxNodes;
|
|
257
|
+
_flipWinding;
|
|
258
|
+
terrainRaycast = null;
|
|
259
|
+
constructor(params = defaultTerrainMeshParams) {
|
|
260
|
+
const mergedParams = { ...defaultTerrainMeshParams, ...params };
|
|
261
|
+
const { innerTileSegments, maxNodes, material, flipWinding } = mergedParams;
|
|
262
|
+
const geometry = new TerrainGeometry(innerTileSegments, true, flipWinding);
|
|
263
|
+
super(geometry, material, maxNodes);
|
|
264
|
+
this.frustumCulled = false;
|
|
265
|
+
this._innerTileSegments = innerTileSegments;
|
|
266
|
+
this._maxNodes = maxNodes;
|
|
267
|
+
this._flipWinding = flipWinding;
|
|
268
|
+
}
|
|
269
|
+
get innerTileSegments() {
|
|
270
|
+
return this._innerTileSegments;
|
|
271
|
+
}
|
|
272
|
+
set innerTileSegments(tileSegments) {
|
|
273
|
+
if (tileSegments === this._innerTileSegments) return;
|
|
274
|
+
const oldGeometry = this.geometry;
|
|
275
|
+
this.geometry = new TerrainGeometry(tileSegments, true, this._flipWinding);
|
|
276
|
+
this._innerTileSegments = tileSegments;
|
|
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());
|
|
288
|
+
}
|
|
289
|
+
get maxNodes() {
|
|
290
|
+
return this._maxNodes;
|
|
291
|
+
}
|
|
292
|
+
set maxNodes(maxNodes) {
|
|
293
|
+
if (!Number.isInteger(maxNodes) || maxNodes < 1) {
|
|
294
|
+
throw new Error(`Invalid maxNodes: ${maxNodes}. Must be a positive integer.`);
|
|
295
|
+
}
|
|
296
|
+
if (maxNodes === this._maxNodes) return;
|
|
297
|
+
const oldMax = this._maxNodes;
|
|
298
|
+
const nextMatrix = new Float32Array(maxNodes * 16);
|
|
299
|
+
const oldMatrixArray = this.instanceMatrix.array;
|
|
300
|
+
nextMatrix.set(oldMatrixArray.subarray(0, Math.min(oldMatrixArray.length, nextMatrix.length)));
|
|
301
|
+
this.instanceMatrix = new InstancedBufferAttribute(nextMatrix, 16);
|
|
302
|
+
if (this.instanceColor) {
|
|
303
|
+
const itemSize = this.instanceColor.itemSize;
|
|
304
|
+
const nextColor = new Float32Array(maxNodes * itemSize);
|
|
305
|
+
const oldColorArray = this.instanceColor.array;
|
|
306
|
+
nextColor.set(oldColorArray.subarray(0, Math.min(oldColorArray.length, nextColor.length)));
|
|
307
|
+
this.instanceColor = new InstancedBufferAttribute(nextColor, itemSize);
|
|
308
|
+
}
|
|
309
|
+
this._maxNodes = maxNodes;
|
|
310
|
+
this.count = Math.min(this.count, maxNodes);
|
|
311
|
+
this.instanceMatrix.needsUpdate = true;
|
|
312
|
+
if (this.instanceColor) this.instanceColor.needsUpdate = true;
|
|
313
|
+
if (maxNodes < oldMax && this.count >= maxNodes) {
|
|
314
|
+
this.count = maxNodes;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
raycast(raycaster, intersects) {
|
|
318
|
+
if (!this.terrainRaycast) {
|
|
319
|
+
super.raycast(raycaster, intersects);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
const result = this.terrainRaycast.pick(raycaster.ray);
|
|
323
|
+
if (!result) return;
|
|
324
|
+
intersects.push({
|
|
325
|
+
distance: result.distance,
|
|
326
|
+
point: result.position.clone(),
|
|
327
|
+
normal: result.normal.clone(),
|
|
328
|
+
object: this,
|
|
329
|
+
face: null,
|
|
330
|
+
faceIndex: -1
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function getDeviceComputeLimits(renderer) {
|
|
336
|
+
const backend = renderer.backend;
|
|
337
|
+
const limits = backend?.device?.limits;
|
|
338
|
+
return {
|
|
339
|
+
maxWorkgroupSizeX: limits?.maxComputeWorkgroupSizeX ?? 256,
|
|
340
|
+
maxWorkgroupSizeY: limits?.maxComputeWorkgroupSizeY ?? 256,
|
|
341
|
+
maxWorkgroupInvocations: limits?.maxComputeWorkgroupInvocations ?? 256
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const WORKGROUP_X = 16;
|
|
346
|
+
const WORKGROUP_Y = 16;
|
|
347
|
+
function compileComputePipeline(stages, width, options) {
|
|
348
|
+
const bindings = options?.bindings;
|
|
349
|
+
const preferredWorkgroup = options?.workgroupSize ?? [
|
|
350
|
+
WORKGROUP_X,
|
|
351
|
+
WORKGROUP_Y
|
|
352
|
+
];
|
|
353
|
+
const uInstanceCount = uniform(0, "uint");
|
|
354
|
+
const stagedKernelCache = /* @__PURE__ */ new Map();
|
|
355
|
+
function clampWorkgroupToLimits(requested, limits) {
|
|
356
|
+
let x = Math.max(1, Math.floor(requested[0]));
|
|
357
|
+
let y = Math.max(1, Math.floor(requested[1]));
|
|
358
|
+
x = Math.min(x, limits.maxWorkgroupSizeX);
|
|
359
|
+
y = Math.min(y, limits.maxWorkgroupSizeY);
|
|
360
|
+
y = Math.min(
|
|
361
|
+
y,
|
|
362
|
+
Math.max(1, Math.floor(limits.maxWorkgroupInvocations / x))
|
|
363
|
+
);
|
|
364
|
+
x = Math.min(
|
|
365
|
+
x,
|
|
366
|
+
Math.max(1, Math.floor(limits.maxWorkgroupInvocations / y))
|
|
367
|
+
);
|
|
368
|
+
return [x, y];
|
|
369
|
+
}
|
|
370
|
+
function buildStagedKernels(workgroupSize) {
|
|
371
|
+
return stages.map(
|
|
372
|
+
(stage) => Fn(() => {
|
|
373
|
+
bindings?.forEach((b) => b.toVar());
|
|
374
|
+
const fWidth = float(width);
|
|
375
|
+
const activeIndex = globalId.z;
|
|
376
|
+
const nodeIndex = int(activeIndex).toVar();
|
|
377
|
+
const iWidth = int(width);
|
|
378
|
+
const ix = int(globalId.x);
|
|
379
|
+
const iy = int(globalId.y);
|
|
380
|
+
const texelSize = vec2(1, 1).div(fWidth);
|
|
381
|
+
const localCoordinates = vec2(globalId.x, globalId.y);
|
|
382
|
+
const localUVCoords = localCoordinates.div(fWidth);
|
|
383
|
+
const verticesPerNode = iWidth.mul(iWidth);
|
|
384
|
+
const globalIndex = int(nodeIndex).mul(verticesPerNode).add(iy.mul(iWidth).add(ix));
|
|
385
|
+
const inBounds = ix.lessThan(iWidth).and(iy.lessThan(iWidth)).and(uint(activeIndex).lessThan(uInstanceCount)).toVar();
|
|
386
|
+
If(inBounds, () => {
|
|
387
|
+
stage(
|
|
388
|
+
nodeIndex,
|
|
389
|
+
globalIndex,
|
|
390
|
+
localUVCoords,
|
|
391
|
+
localCoordinates,
|
|
392
|
+
texelSize
|
|
393
|
+
);
|
|
394
|
+
});
|
|
395
|
+
})().computeKernel(workgroupSize)
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
function execute(renderer, instanceCount) {
|
|
399
|
+
const limits = getDeviceComputeLimits(renderer);
|
|
400
|
+
uInstanceCount.value = instanceCount;
|
|
401
|
+
const [workgroupX, workgroupY] = clampWorkgroupToLimits(
|
|
402
|
+
preferredWorkgroup,
|
|
403
|
+
limits
|
|
404
|
+
);
|
|
405
|
+
const cacheKey = `${workgroupX}x${workgroupY}`;
|
|
406
|
+
let stagedKernels = stagedKernelCache.get(cacheKey);
|
|
407
|
+
if (!stagedKernels) {
|
|
408
|
+
stagedKernels = buildStagedKernels([workgroupX, workgroupY, 1]);
|
|
409
|
+
stagedKernelCache.set(cacheKey, stagedKernels);
|
|
410
|
+
}
|
|
411
|
+
const dispatchX = Math.ceil(width / workgroupX);
|
|
412
|
+
const dispatchY = Math.ceil(width / workgroupY);
|
|
413
|
+
for (const kernel of stagedKernels) {
|
|
414
|
+
renderer.compute(kernel, [dispatchX, dispatchY, instanceCount]);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return { execute };
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function resolveType(format) {
|
|
421
|
+
return format === "rgba16float" ? HalfFloatType : FloatType;
|
|
422
|
+
}
|
|
423
|
+
function resolveFilter(mode) {
|
|
424
|
+
return mode === "linear" ? LinearFilter : NearestFilter;
|
|
425
|
+
}
|
|
426
|
+
function configureStorageTexture(texture2, format, filter) {
|
|
427
|
+
texture2.format = RGBAFormat;
|
|
428
|
+
texture2.type = resolveType(format);
|
|
429
|
+
texture2.magFilter = resolveFilter(filter);
|
|
430
|
+
texture2.minFilter = resolveFilter(filter);
|
|
431
|
+
texture2.wrapS = ClampToEdgeWrapping;
|
|
432
|
+
texture2.wrapT = ClampToEdgeWrapping;
|
|
433
|
+
texture2.generateMipmaps = false;
|
|
434
|
+
texture2.needsUpdate = true;
|
|
435
|
+
}
|
|
436
|
+
function ArrayTextureBackend(edgeVertexCount, tileCount, options) {
|
|
437
|
+
let currentEdgeVertexCount = edgeVertexCount;
|
|
438
|
+
let currentTileCount = tileCount;
|
|
439
|
+
const tex = new StorageArrayTexture(
|
|
440
|
+
edgeVertexCount,
|
|
441
|
+
edgeVertexCount,
|
|
442
|
+
tileCount
|
|
443
|
+
);
|
|
444
|
+
configureStorageTexture(tex, options.format, options.filter);
|
|
445
|
+
return {
|
|
446
|
+
backendType: "array-texture",
|
|
447
|
+
get edgeVertexCount() {
|
|
448
|
+
return currentEdgeVertexCount;
|
|
449
|
+
},
|
|
450
|
+
get tileCount() {
|
|
451
|
+
return currentTileCount;
|
|
452
|
+
},
|
|
453
|
+
texture: tex,
|
|
454
|
+
uv(ix, iy, _tileIndex) {
|
|
455
|
+
return vec2(ix.toFloat(), iy.toFloat());
|
|
456
|
+
},
|
|
457
|
+
texel(ix, iy, tileIndex) {
|
|
458
|
+
return ivec3(ix, iy, tileIndex);
|
|
459
|
+
},
|
|
460
|
+
sample(u, v, tileIndex) {
|
|
461
|
+
return texture(tex, vec2(u, v)).depth(int(tileIndex));
|
|
462
|
+
},
|
|
463
|
+
resize(width, height, nextTileCount) {
|
|
464
|
+
currentEdgeVertexCount = width;
|
|
465
|
+
currentTileCount = nextTileCount;
|
|
466
|
+
tex.setSize(width, height, nextTileCount);
|
|
467
|
+
tex.needsUpdate = true;
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
function atlasCoord(tilesPerRow, edgeVertexCount, ix, iy, tileIndex) {
|
|
472
|
+
const tilesPerRowNode = int(tilesPerRow);
|
|
473
|
+
const edge = int(edgeVertexCount);
|
|
474
|
+
const tile = int(tileIndex);
|
|
475
|
+
const col = tile.mod(tilesPerRowNode);
|
|
476
|
+
const row = tile.div(tilesPerRowNode);
|
|
477
|
+
const atlasX = col.mul(edge).add(int(ix));
|
|
478
|
+
const atlasY = row.mul(edge).add(int(iy));
|
|
479
|
+
return { atlasX, atlasY };
|
|
480
|
+
}
|
|
481
|
+
function AtlasBackend(edgeVertexCount, tileCount, options) {
|
|
482
|
+
let currentEdgeVertexCount = edgeVertexCount;
|
|
483
|
+
let currentTileCount = tileCount;
|
|
484
|
+
let tilesPerRow = Math.max(1, Math.ceil(Math.sqrt(tileCount)));
|
|
485
|
+
const atlasSize = tilesPerRow * edgeVertexCount;
|
|
486
|
+
const tex = new StorageTexture(atlasSize, atlasSize);
|
|
487
|
+
configureStorageTexture(tex, options.format, options.filter);
|
|
488
|
+
return {
|
|
489
|
+
backendType: "atlas",
|
|
490
|
+
get edgeVertexCount() {
|
|
491
|
+
return currentEdgeVertexCount;
|
|
492
|
+
},
|
|
493
|
+
get tileCount() {
|
|
494
|
+
return currentTileCount;
|
|
495
|
+
},
|
|
496
|
+
texture: tex,
|
|
497
|
+
uv(ix, iy, tileIndex) {
|
|
498
|
+
const { atlasX, atlasY } = atlasCoord(
|
|
499
|
+
tilesPerRow,
|
|
500
|
+
currentEdgeVertexCount,
|
|
501
|
+
ix,
|
|
502
|
+
iy,
|
|
503
|
+
tileIndex
|
|
504
|
+
);
|
|
505
|
+
const currentAtlasSize = float(tilesPerRow * currentEdgeVertexCount);
|
|
506
|
+
return vec2(
|
|
507
|
+
atlasX.toFloat().add(0.5).div(currentAtlasSize),
|
|
508
|
+
atlasY.toFloat().add(0.5).div(currentAtlasSize)
|
|
509
|
+
);
|
|
510
|
+
},
|
|
511
|
+
texel(ix, iy, tileIndex) {
|
|
512
|
+
const { atlasX, atlasY } = atlasCoord(
|
|
513
|
+
tilesPerRow,
|
|
514
|
+
currentEdgeVertexCount,
|
|
515
|
+
ix,
|
|
516
|
+
iy,
|
|
517
|
+
tileIndex
|
|
518
|
+
);
|
|
519
|
+
return ivec2(atlasX, atlasY);
|
|
520
|
+
},
|
|
521
|
+
sample(u, v, tileIndex) {
|
|
522
|
+
const tile = int(tileIndex);
|
|
523
|
+
const tilesPerRowNode = int(tilesPerRow);
|
|
524
|
+
const col = tile.mod(tilesPerRowNode);
|
|
525
|
+
const row = tile.div(tilesPerRowNode);
|
|
526
|
+
const invTilesPerRow = float(1 / tilesPerRow);
|
|
527
|
+
const atlasU = col.toFloat().add(u).mul(invTilesPerRow);
|
|
528
|
+
const atlasV = row.toFloat().add(v).mul(invTilesPerRow);
|
|
529
|
+
return texture(tex, vec2(atlasU, atlasV));
|
|
530
|
+
},
|
|
531
|
+
resize(width, height, nextTileCount) {
|
|
532
|
+
currentEdgeVertexCount = width;
|
|
533
|
+
currentTileCount = nextTileCount;
|
|
534
|
+
tilesPerRow = Math.max(1, Math.ceil(Math.sqrt(nextTileCount)));
|
|
535
|
+
const nextAtlasSize = tilesPerRow * width;
|
|
536
|
+
const image = tex.image;
|
|
537
|
+
image.width = nextAtlasSize;
|
|
538
|
+
image.height = nextAtlasSize;
|
|
539
|
+
tex.needsUpdate = true;
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
function texture3DBackend(edgeVertexCount, tileCount, options) {
|
|
544
|
+
const storage = ArrayTextureBackend(edgeVertexCount, tileCount, options);
|
|
545
|
+
return { ...storage, backendType: "texture-3d" };
|
|
546
|
+
}
|
|
547
|
+
function tryGetDeviceLimits(renderer) {
|
|
548
|
+
const backend = renderer;
|
|
549
|
+
return backend.backend?.device?.limits ?? {};
|
|
550
|
+
}
|
|
551
|
+
function createTerrainFieldStorage(edgeVertexCount, tileCount, renderer, options = {}) {
|
|
552
|
+
const filter = options.filter ?? "linear";
|
|
553
|
+
const format = options.format ?? "rgba16float";
|
|
554
|
+
const forcedBackend = options.backend;
|
|
555
|
+
if (forcedBackend === "atlas") {
|
|
556
|
+
return AtlasBackend(edgeVertexCount, tileCount, { filter, format });
|
|
557
|
+
}
|
|
558
|
+
if (forcedBackend === "texture-3d") {
|
|
559
|
+
return texture3DBackend(edgeVertexCount, tileCount, { filter, format });
|
|
560
|
+
}
|
|
561
|
+
if (forcedBackend === "array-texture") {
|
|
562
|
+
return ArrayTextureBackend(edgeVertexCount, tileCount, { filter, format });
|
|
563
|
+
}
|
|
564
|
+
const DEFAULT_MAX_TEXTURE_ARRAY_LAYERS = 256;
|
|
565
|
+
const maxLayers = renderer ? tryGetDeviceLimits(renderer).maxTextureArrayLayers ?? DEFAULT_MAX_TEXTURE_ARRAY_LAYERS : DEFAULT_MAX_TEXTURE_ARRAY_LAYERS;
|
|
566
|
+
if (tileCount > maxLayers) {
|
|
567
|
+
return AtlasBackend(edgeVertexCount, tileCount, { filter, format });
|
|
568
|
+
}
|
|
569
|
+
return ArrayTextureBackend(edgeVertexCount, tileCount, { filter, format });
|
|
570
|
+
}
|
|
571
|
+
function storeTerrainField(storage, ix, iy, tileIndex, value) {
|
|
572
|
+
if (storage.backendType === "array-texture" || storage.backendType === "texture-3d") {
|
|
573
|
+
return textureStore(
|
|
574
|
+
storage.texture,
|
|
575
|
+
uvec3(int(ix), int(iy), int(tileIndex)),
|
|
576
|
+
value
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
return textureStore(storage.texture, storage.texel(ix, iy, tileIndex), value);
|
|
580
|
+
}
|
|
581
|
+
function loadTerrainField(storage, ix, iy, tileIndex) {
|
|
582
|
+
if (storage.backendType === "array-texture" || storage.backendType === "texture-3d") {
|
|
583
|
+
return textureLoad(storage.texture, ivec2(int(ix), int(iy)), int(0)).depth(
|
|
584
|
+
int(tileIndex)
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
return textureLoad(storage.texture, storage.texel(ix, iy, tileIndex), int(0));
|
|
588
|
+
}
|
|
589
|
+
function loadTerrainFieldElevation(storage, ix, iy, tileIndex) {
|
|
590
|
+
return loadTerrainField(storage, ix, iy, tileIndex).r;
|
|
591
|
+
}
|
|
592
|
+
function loadTerrainFieldNormal(storage, ix, iy, tileIndex) {
|
|
593
|
+
const raw = loadTerrainField(storage, ix, iy, tileIndex);
|
|
594
|
+
return vec2(raw.g, raw.b);
|
|
595
|
+
}
|
|
596
|
+
function sampleTerrainField(storage, u, v, tileIndex) {
|
|
597
|
+
return storage.sample(u, v, tileIndex);
|
|
598
|
+
}
|
|
599
|
+
function sampleTerrainFieldElevation(storage, u, v, tileIndex) {
|
|
600
|
+
return sampleTerrainField(storage, u, v, tileIndex).r;
|
|
601
|
+
}
|
|
602
|
+
function packTerrainFieldSample(height, normalXZ, extra = float(0)) {
|
|
603
|
+
return vec4(height, normalXZ.x, normalXZ.y, extra);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const createElevation = (tile, uniforms, elevationFn) => {
|
|
607
|
+
return function perVertexElevation(nodeIndex, localCoordinates) {
|
|
608
|
+
const ix = int(localCoordinates.x);
|
|
609
|
+
const iy = int(localCoordinates.y);
|
|
610
|
+
const edgeVertexCount = uniforms.uInnerTileSegments.toVar().add(int(3));
|
|
611
|
+
const tileUV = localCoordinates.toFloat().div(edgeVertexCount.toFloat());
|
|
612
|
+
const rootUV = tile.rootUVCompute(nodeIndex, ix, iy);
|
|
613
|
+
const worldPosition = tile.tileVertexWorldPositionCompute(nodeIndex, ix, iy).setName("worldPositionWithSkirt");
|
|
614
|
+
const rootSize = uniforms.uRootSize.toVar();
|
|
615
|
+
return elevationFn({
|
|
616
|
+
worldPosition,
|
|
617
|
+
rootSize,
|
|
618
|
+
rootUV,
|
|
619
|
+
tileOriginVec2: tile.tileOriginVec2(nodeIndex),
|
|
620
|
+
tileSize: tile.tileSize(nodeIndex),
|
|
621
|
+
tileLevel: tile.tileLevel(nodeIndex),
|
|
622
|
+
nodeIndex: int(nodeIndex),
|
|
623
|
+
tileUV
|
|
624
|
+
});
|
|
625
|
+
};
|
|
626
|
+
};
|
|
627
|
+
|
|
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";
|
|
724
|
+
const tileLevel = Fn(([nodeIndex]) => {
|
|
725
|
+
return decodeLeafTile(leafStorage, nodeIndex).level;
|
|
726
|
+
});
|
|
727
|
+
const tileFace = Fn(([nodeIndex]) => {
|
|
728
|
+
return decodeLeafTile(leafStorage, nodeIndex).face;
|
|
729
|
+
});
|
|
730
|
+
const tileOriginVec2 = Fn(([nodeIndex]) => {
|
|
731
|
+
const tile = decodeLeafTile(leafStorage, nodeIndex);
|
|
732
|
+
return vec2(tile.x, tile.y);
|
|
733
|
+
});
|
|
734
|
+
const tileSize = Fn(([nodeIndex]) => {
|
|
735
|
+
const level = tileLevel(nodeIndex);
|
|
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);
|
|
749
|
+
});
|
|
750
|
+
const rootUVCompute = Fn(([nodeIndex, ix, iy]) => {
|
|
751
|
+
if (isSphere) {
|
|
752
|
+
return tileFaceUV(nodeIndex, ix, iy);
|
|
753
|
+
}
|
|
754
|
+
const nodeVec2 = tileOriginVec2(nodeIndex);
|
|
755
|
+
const nodeX = nodeVec2.x;
|
|
756
|
+
const nodeY = nodeVec2.y;
|
|
757
|
+
const rootSize = uniforms.uRootSize.toVar();
|
|
758
|
+
const rootOrigin = uniforms.uRootOrigin.toVar();
|
|
759
|
+
const size = tileSize(nodeIndex);
|
|
760
|
+
const half = float(0.5);
|
|
761
|
+
const halfRoot = float(rootSize).mul(half);
|
|
762
|
+
const fInnerSegments = uniforms.uInnerTileSegments.toVar().toFloat();
|
|
763
|
+
const texelSpacing = size.div(fInnerSegments);
|
|
764
|
+
const absX = nodeX.mul(fInnerSegments).add(int(ix).toFloat().sub(float(1)));
|
|
765
|
+
const absY = nodeY.mul(fInnerSegments).add(int(iy).toFloat().sub(float(1)));
|
|
766
|
+
const worldX = rootOrigin.x.add(absX.mul(texelSpacing)).sub(halfRoot);
|
|
767
|
+
const worldZ = rootOrigin.z.add(absY.mul(texelSpacing)).sub(halfRoot);
|
|
768
|
+
const centeredX = worldX.sub(rootOrigin.x);
|
|
769
|
+
const centeredZ = worldZ.sub(rootOrigin.z);
|
|
770
|
+
return vec2(
|
|
771
|
+
centeredX.div(rootSize).add(half),
|
|
772
|
+
centeredZ.div(rootSize).mul(float(-1)).add(half)
|
|
773
|
+
);
|
|
774
|
+
});
|
|
775
|
+
const tileVertexWorldPositionCompute = Fn(
|
|
776
|
+
([nodeIndex, ix, iy]) => {
|
|
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
|
+
}
|
|
784
|
+
const nodeVec2 = tileOriginVec2(nodeIndex);
|
|
785
|
+
const nodeX = nodeVec2.x;
|
|
786
|
+
const nodeY = nodeVec2.y;
|
|
787
|
+
const rootSize = uniforms.uRootSize.toVar();
|
|
788
|
+
const size = tileSize(nodeIndex);
|
|
789
|
+
const half = float(0.5);
|
|
790
|
+
const halfRoot = float(rootSize).mul(half);
|
|
791
|
+
const fInnerSegments = uniforms.uInnerTileSegments.toVar().toFloat();
|
|
792
|
+
const texelSpacing = size.div(fInnerSegments);
|
|
793
|
+
const absX = nodeX.mul(fInnerSegments).add(int(ix).toFloat().sub(float(1)));
|
|
794
|
+
const absY = nodeY.mul(fInnerSegments).add(int(iy).toFloat().sub(float(1)));
|
|
795
|
+
const worldX = rootOrigin.x.add(absX.mul(texelSpacing)).sub(halfRoot);
|
|
796
|
+
const worldZ = rootOrigin.z.add(absY.mul(texelSpacing)).sub(halfRoot);
|
|
797
|
+
return vec3(worldX, rootOrigin.y, worldZ);
|
|
798
|
+
}
|
|
799
|
+
);
|
|
800
|
+
return {
|
|
801
|
+
tileLevel,
|
|
802
|
+
tileFace,
|
|
803
|
+
tileOriginVec2,
|
|
804
|
+
tileSize,
|
|
805
|
+
tileFaceUV,
|
|
806
|
+
rootUVCompute,
|
|
807
|
+
tileVertexWorldPositionCompute
|
|
808
|
+
};
|
|
809
|
+
}
|
|
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;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function createLeafStorage(maxNodes) {
|
|
820
|
+
const data = new Int32Array(maxNodes * 4);
|
|
821
|
+
const attribute = new StorageBufferAttribute(data, 4);
|
|
822
|
+
const node = storage(attribute, "i32", 1).toReadOnly().setName("leafStorage");
|
|
823
|
+
return { data, attribute, node };
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
const Dir = {
|
|
827
|
+
LEFT: 0,
|
|
828
|
+
RIGHT: 1,
|
|
829
|
+
TOP: 2,
|
|
830
|
+
BOTTOM: 3
|
|
831
|
+
};
|
|
832
|
+
const U32_EMPTY = 4294967295;
|
|
833
|
+
function allocLeafSet(capacity) {
|
|
834
|
+
return {
|
|
835
|
+
capacity,
|
|
836
|
+
count: 0,
|
|
837
|
+
space: new Uint8Array(capacity),
|
|
838
|
+
level: new Uint8Array(capacity),
|
|
839
|
+
x: new Int32Array(capacity),
|
|
840
|
+
y: new Int32Array(capacity)
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
function resetLeafSet(leaves) {
|
|
844
|
+
leaves.count = 0;
|
|
845
|
+
}
|
|
846
|
+
function allocSeamTable(capacity) {
|
|
847
|
+
return {
|
|
848
|
+
capacity,
|
|
849
|
+
count: 0,
|
|
850
|
+
stride: 8,
|
|
851
|
+
neighbors: new Uint32Array(capacity * 8)
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
function resetSeamTable(seams) {
|
|
855
|
+
seams.count = 0;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
function createNodeStore(maxNodes, spaceCount) {
|
|
859
|
+
return {
|
|
860
|
+
maxNodes,
|
|
861
|
+
nodesUsed: 0,
|
|
862
|
+
currentGen: 1,
|
|
863
|
+
gen: new Uint16Array(maxNodes),
|
|
864
|
+
space: new Uint8Array(maxNodes),
|
|
865
|
+
level: new Uint8Array(maxNodes),
|
|
866
|
+
x: new Int32Array(maxNodes),
|
|
867
|
+
y: new Int32Array(maxNodes),
|
|
868
|
+
firstChild: new Uint32Array(maxNodes),
|
|
869
|
+
flags: new Uint8Array(maxNodes),
|
|
870
|
+
roots: new Uint32Array(spaceCount)
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
function beginFrame(store) {
|
|
874
|
+
store.nodesUsed = 0;
|
|
875
|
+
store.currentGen = store.currentGen + 1 & 65535;
|
|
876
|
+
if (store.currentGen === 0) {
|
|
877
|
+
store.gen.fill(0);
|
|
878
|
+
store.currentGen = 1;
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
function allocNode(store, tile) {
|
|
882
|
+
const id = store.nodesUsed;
|
|
883
|
+
if (id >= store.maxNodes) return U32_EMPTY;
|
|
884
|
+
store.nodesUsed = id + 1;
|
|
885
|
+
store.gen[id] = store.currentGen;
|
|
886
|
+
store.space[id] = tile.space;
|
|
887
|
+
store.level[id] = tile.level;
|
|
888
|
+
store.x[id] = tile.x;
|
|
889
|
+
store.y[id] = tile.y;
|
|
890
|
+
store.firstChild[id] = U32_EMPTY;
|
|
891
|
+
store.flags[id] = 0;
|
|
892
|
+
return id;
|
|
893
|
+
}
|
|
894
|
+
function hasChildren(store, nodeId) {
|
|
895
|
+
return store.firstChild[nodeId] !== U32_EMPTY;
|
|
896
|
+
}
|
|
897
|
+
function ensureChildren(store, parentId) {
|
|
898
|
+
const existing = store.firstChild[parentId];
|
|
899
|
+
if (existing !== U32_EMPTY) return existing;
|
|
900
|
+
const childBase = store.nodesUsed;
|
|
901
|
+
if (childBase + 4 > store.maxNodes) return U32_EMPTY;
|
|
902
|
+
const space = store.space[parentId];
|
|
903
|
+
const level = store.level[parentId] + 1;
|
|
904
|
+
const px = store.x[parentId] << 1;
|
|
905
|
+
const py = store.y[parentId] << 1;
|
|
906
|
+
allocNode(store, { space, level, x: px, y: py });
|
|
907
|
+
allocNode(store, { space, level, x: px + 1, y: py });
|
|
908
|
+
allocNode(store, { space, level, x: px, y: py + 1 });
|
|
909
|
+
allocNode(store, { space, level, x: px + 1, y: py + 1 });
|
|
910
|
+
store.firstChild[parentId] = childBase;
|
|
911
|
+
return childBase;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
function nextPow2$1(n) {
|
|
915
|
+
let x = 1;
|
|
916
|
+
while (x < n) x <<= 1;
|
|
917
|
+
return x;
|
|
918
|
+
}
|
|
919
|
+
function mix32$1(x) {
|
|
920
|
+
x >>>= 0;
|
|
921
|
+
x ^= x >>> 16;
|
|
922
|
+
x = Math.imul(x, 2146121005) >>> 0;
|
|
923
|
+
x ^= x >>> 15;
|
|
924
|
+
x = Math.imul(x, 2221713035) >>> 0;
|
|
925
|
+
x ^= x >>> 16;
|
|
926
|
+
return x >>> 0;
|
|
927
|
+
}
|
|
928
|
+
function hashKey$1(space, level, x, y) {
|
|
929
|
+
const h = space & 255 ^ (level & 255) << 8 ^ mix32$1(x) >>> 0 ^ mix32$1(y) >>> 0;
|
|
930
|
+
return mix32$1(h);
|
|
931
|
+
}
|
|
932
|
+
function createSpatialIndex(maxEntries) {
|
|
933
|
+
const size = nextPow2$1(Math.max(2, maxEntries * 2));
|
|
934
|
+
return {
|
|
935
|
+
size,
|
|
936
|
+
mask: size - 1,
|
|
937
|
+
stampGen: 1,
|
|
938
|
+
stamp: new Uint16Array(size),
|
|
939
|
+
keysSpace: new Uint8Array(size),
|
|
940
|
+
keysLevel: new Uint8Array(size),
|
|
941
|
+
keysX: new Uint32Array(size),
|
|
942
|
+
keysY: new Uint32Array(size),
|
|
943
|
+
values: new Uint32Array(size)
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
function resetSpatialIndex(index) {
|
|
947
|
+
index.stampGen = index.stampGen + 1 & 65535;
|
|
948
|
+
if (index.stampGen === 0) {
|
|
949
|
+
index.stamp.fill(0);
|
|
950
|
+
index.stampGen = 1;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
function insertSpatialIndexRaw(index, space, level, x, y, value) {
|
|
954
|
+
const s = space & 255;
|
|
955
|
+
const l = level & 255;
|
|
956
|
+
const xx = x >>> 0;
|
|
957
|
+
const yy = y >>> 0;
|
|
958
|
+
let slot = hashKey$1(s, l, xx, yy) & index.mask;
|
|
959
|
+
for (let probes = 0; probes < index.size; probes++) {
|
|
960
|
+
if (index.stamp[slot] !== index.stampGen) {
|
|
961
|
+
index.stamp[slot] = index.stampGen;
|
|
962
|
+
index.keysSpace[slot] = s;
|
|
963
|
+
index.keysLevel[slot] = l;
|
|
964
|
+
index.keysX[slot] = xx;
|
|
965
|
+
index.keysY[slot] = yy;
|
|
966
|
+
index.values[slot] = value >>> 0;
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
if (index.keysSpace[slot] === s && index.keysLevel[slot] === l && index.keysX[slot] === xx && index.keysY[slot] === yy) {
|
|
970
|
+
index.values[slot] = value >>> 0;
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
973
|
+
slot = slot + 1 & index.mask;
|
|
974
|
+
}
|
|
975
|
+
throw new Error("SpatialIndex is full (no empty slot found).");
|
|
976
|
+
}
|
|
977
|
+
function lookupSpatialIndexRaw(index, space, level, x, y) {
|
|
978
|
+
const s = space & 255;
|
|
979
|
+
const l = level & 255;
|
|
980
|
+
const xx = x >>> 0;
|
|
981
|
+
const yy = y >>> 0;
|
|
982
|
+
let slot = hashKey$1(s, l, xx, yy) & index.mask;
|
|
983
|
+
for (let probes = 0; probes < index.size; probes++) {
|
|
984
|
+
if (index.stamp[slot] !== index.stampGen) return U32_EMPTY;
|
|
985
|
+
if (index.keysSpace[slot] === s && index.keysLevel[slot] === l && index.keysX[slot] === xx && index.keysY[slot] === yy) {
|
|
986
|
+
return index.values[slot];
|
|
987
|
+
}
|
|
988
|
+
slot = slot + 1 & index.mask;
|
|
989
|
+
}
|
|
990
|
+
return U32_EMPTY;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
function buildLeafIndex(leaves, out) {
|
|
994
|
+
const index = out ?? createSpatialIndex(leaves.count);
|
|
995
|
+
resetSpatialIndex(index);
|
|
996
|
+
for (let i = 0; i < leaves.count; i++) {
|
|
997
|
+
insertSpatialIndexRaw(index, leaves.space[i], leaves.level[i], leaves.x[i], leaves.y[i], i);
|
|
998
|
+
}
|
|
999
|
+
return index;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
function createState(cfg, topology) {
|
|
1003
|
+
const store = createNodeStore(cfg.maxNodes, topology.spaceCount);
|
|
1004
|
+
const scratchRootTiles = [];
|
|
1005
|
+
for (let i = 0; i < topology.maxRootCount; i++) {
|
|
1006
|
+
scratchRootTiles.push({ space: 0, level: 0, x: 0, y: 0 });
|
|
1007
|
+
}
|
|
1008
|
+
return {
|
|
1009
|
+
cfg,
|
|
1010
|
+
store,
|
|
1011
|
+
leaves: allocLeafSet(cfg.maxNodes),
|
|
1012
|
+
leafNodeIds: new Uint32Array(cfg.maxNodes),
|
|
1013
|
+
leafIndex: createSpatialIndex(cfg.maxNodes),
|
|
1014
|
+
stack: new Uint32Array(cfg.maxNodes),
|
|
1015
|
+
rootNodeIds: new Uint32Array(topology.maxRootCount),
|
|
1016
|
+
rootCount: 0,
|
|
1017
|
+
splitQueue: new Uint32Array(cfg.maxNodes),
|
|
1018
|
+
splitStamp: new Uint16Array(cfg.maxNodes),
|
|
1019
|
+
splitGen: 1,
|
|
1020
|
+
scratchTile: { space: 0, level: 0, x: 0, y: 0 },
|
|
1021
|
+
scratchNeighbor: { space: 0, level: 0, x: 0, y: 0 },
|
|
1022
|
+
scratchBounds: { cx: 0, cy: 0, cz: 0, r: 0 },
|
|
1023
|
+
scratchRootTiles,
|
|
1024
|
+
spaceCount: topology.spaceCount
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
function beginUpdate(state, topology, params) {
|
|
1028
|
+
if (topology.spaceCount !== state.spaceCount) {
|
|
1029
|
+
throw new Error(
|
|
1030
|
+
`Topology spaceCount changed (${state.spaceCount} -> ${topology.spaceCount}). Create a new quadtree state.`
|
|
1031
|
+
);
|
|
1032
|
+
}
|
|
1033
|
+
if (topology.maxRootCount !== state.rootNodeIds.length) {
|
|
1034
|
+
throw new Error(
|
|
1035
|
+
`Topology maxRootCount changed (${state.rootNodeIds.length} -> ${topology.maxRootCount}). Create a new quadtree state.`
|
|
1036
|
+
);
|
|
1037
|
+
}
|
|
1038
|
+
beginFrame(state.store);
|
|
1039
|
+
state.rootCount = 0;
|
|
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}).`);
|
|
1043
|
+
}
|
|
1044
|
+
for (let i = 0; i < rootCount; i++) {
|
|
1045
|
+
const rootId = allocNode(state.store, state.scratchRootTiles[i]);
|
|
1046
|
+
if (rootId === U32_EMPTY) {
|
|
1047
|
+
throw new Error("Failed to allocate root node (maxNodes too small).");
|
|
1048
|
+
}
|
|
1049
|
+
state.rootNodeIds[i] = rootId;
|
|
1050
|
+
state.rootCount = i + 1;
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
function shouldSplit(bounds, level, maxLevel, params) {
|
|
1055
|
+
if (level >= maxLevel) return false;
|
|
1056
|
+
const mode = params.mode ?? "distance";
|
|
1057
|
+
const cx = bounds.cx;
|
|
1058
|
+
const cy = bounds.cy;
|
|
1059
|
+
const cz = bounds.cz;
|
|
1060
|
+
const distSq = cx * cx + cy * cy + cz * cz;
|
|
1061
|
+
const safeDistSq = distSq > 1e-12 ? distSq : 1e-12;
|
|
1062
|
+
if (mode === "screen") {
|
|
1063
|
+
const proj = params.projectionFactor ?? 0;
|
|
1064
|
+
const target = params.targetPixels ?? 0;
|
|
1065
|
+
if (proj <= 0 || target <= 0) {
|
|
1066
|
+
const f2 = params.distanceFactor ?? 2;
|
|
1067
|
+
const threshold2 = bounds.r * f2;
|
|
1068
|
+
return safeDistSq < threshold2 * threshold2;
|
|
1069
|
+
}
|
|
1070
|
+
const left = bounds.r * bounds.r * proj * proj;
|
|
1071
|
+
const right = safeDistSq * target * target;
|
|
1072
|
+
return left > right;
|
|
1073
|
+
}
|
|
1074
|
+
const f = params.distanceFactor ?? 2;
|
|
1075
|
+
const threshold = bounds.r * f;
|
|
1076
|
+
return safeDistSq < threshold * threshold;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
function refineLeaves(state, topology, params, outLeaves) {
|
|
1080
|
+
const leaves = outLeaves ?? state.leaves;
|
|
1081
|
+
resetLeafSet(leaves);
|
|
1082
|
+
const store = state.store;
|
|
1083
|
+
const stack = state.stack;
|
|
1084
|
+
let sp = 0;
|
|
1085
|
+
for (let i = 0; i < state.rootCount; i++) {
|
|
1086
|
+
stack[sp++] = state.rootNodeIds[i];
|
|
1087
|
+
}
|
|
1088
|
+
while (sp > 0) {
|
|
1089
|
+
const nodeId = stack[--sp];
|
|
1090
|
+
const level = store.level[nodeId];
|
|
1091
|
+
const space = store.space[nodeId];
|
|
1092
|
+
const x = store.x[nodeId];
|
|
1093
|
+
const y = store.y[nodeId];
|
|
1094
|
+
const tile = state.scratchTile;
|
|
1095
|
+
tile.space = space;
|
|
1096
|
+
tile.level = level;
|
|
1097
|
+
tile.x = x;
|
|
1098
|
+
tile.y = y;
|
|
1099
|
+
const bounds = state.scratchBounds;
|
|
1100
|
+
topology.tileBounds(tile, params.cameraOrigin, bounds);
|
|
1101
|
+
if (hasChildren(store, nodeId)) {
|
|
1102
|
+
const base = store.firstChild[nodeId];
|
|
1103
|
+
stack[sp++] = base + 3;
|
|
1104
|
+
stack[sp++] = base + 2;
|
|
1105
|
+
stack[sp++] = base + 1;
|
|
1106
|
+
stack[sp++] = base + 0;
|
|
1107
|
+
continue;
|
|
1108
|
+
}
|
|
1109
|
+
const split = shouldSplit(bounds, level, state.cfg.maxLevel, params);
|
|
1110
|
+
if (split) {
|
|
1111
|
+
const base = ensureChildren(store, nodeId);
|
|
1112
|
+
if (base !== U32_EMPTY) {
|
|
1113
|
+
stack[sp++] = base + 3;
|
|
1114
|
+
stack[sp++] = base + 2;
|
|
1115
|
+
stack[sp++] = base + 1;
|
|
1116
|
+
stack[sp++] = base + 0;
|
|
1117
|
+
continue;
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
const i = leaves.count;
|
|
1121
|
+
if (i >= leaves.capacity) {
|
|
1122
|
+
throw new Error("LeafSet capacity exceeded.");
|
|
1123
|
+
}
|
|
1124
|
+
leaves.space[i] = space;
|
|
1125
|
+
leaves.level[i] = level;
|
|
1126
|
+
leaves.x[i] = x;
|
|
1127
|
+
leaves.y[i] = y;
|
|
1128
|
+
state.leafNodeIds[i] = nodeId;
|
|
1129
|
+
leaves.count = i + 1;
|
|
1130
|
+
}
|
|
1131
|
+
return leaves;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
function resetSplitMarks(state) {
|
|
1135
|
+
state.splitGen = state.splitGen + 1 & 65535;
|
|
1136
|
+
if (state.splitGen === 0) {
|
|
1137
|
+
state.splitStamp.fill(0);
|
|
1138
|
+
state.splitGen = 1;
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
function scheduleSplit(state, nodeId, count) {
|
|
1142
|
+
if (nodeId === U32_EMPTY) return count;
|
|
1143
|
+
if (state.splitStamp[nodeId] === state.splitGen) return count;
|
|
1144
|
+
state.splitStamp[nodeId] = state.splitGen;
|
|
1145
|
+
state.splitQueue[count] = nodeId;
|
|
1146
|
+
return count + 1;
|
|
1147
|
+
}
|
|
1148
|
+
function balance2to1(state, topology, params, leaves) {
|
|
1149
|
+
const maxIters = state.cfg.maxLevel + 1;
|
|
1150
|
+
for (let iter = 0; iter < maxIters; iter++) {
|
|
1151
|
+
const index = buildLeafIndex(leaves, state.leafIndex);
|
|
1152
|
+
resetSplitMarks(state);
|
|
1153
|
+
let splitCount = 0;
|
|
1154
|
+
for (let i = 0; i < leaves.count; i++) {
|
|
1155
|
+
const leafLevel = leaves.level[i];
|
|
1156
|
+
if (leafLevel < 2) continue;
|
|
1157
|
+
const leafSpace = leaves.space[i];
|
|
1158
|
+
const leafX = leaves.x[i];
|
|
1159
|
+
const leafY = leaves.y[i];
|
|
1160
|
+
for (let dir = 0; dir < 4; dir++) {
|
|
1161
|
+
for (let candidateLevel = leafLevel - 2; candidateLevel >= 0; candidateLevel--) {
|
|
1162
|
+
const shift = leafLevel - candidateLevel;
|
|
1163
|
+
const tile = state.scratchTile;
|
|
1164
|
+
tile.space = leafSpace;
|
|
1165
|
+
tile.level = candidateLevel;
|
|
1166
|
+
tile.x = leafX >>> shift;
|
|
1167
|
+
tile.y = leafY >>> shift;
|
|
1168
|
+
const neighbor = state.scratchNeighbor;
|
|
1169
|
+
if (!topology.neighborSameLevel(tile, dir, neighbor)) break;
|
|
1170
|
+
const j = lookupSpatialIndexRaw(
|
|
1171
|
+
index,
|
|
1172
|
+
neighbor.space,
|
|
1173
|
+
neighbor.level,
|
|
1174
|
+
neighbor.x,
|
|
1175
|
+
neighbor.y
|
|
1176
|
+
);
|
|
1177
|
+
if (j !== U32_EMPTY) {
|
|
1178
|
+
splitCount = scheduleSplit(state, state.leafNodeIds[j], splitCount);
|
|
1179
|
+
break;
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
if (splitCount === 0) return leaves;
|
|
1185
|
+
let anySplit = false;
|
|
1186
|
+
for (let k = 0; k < splitCount; k++) {
|
|
1187
|
+
const nodeId = state.splitQueue[k];
|
|
1188
|
+
if (state.store.level[nodeId] >= state.cfg.maxLevel) continue;
|
|
1189
|
+
const base = ensureChildren(state.store, nodeId);
|
|
1190
|
+
if (base !== U32_EMPTY) anySplit = true;
|
|
1191
|
+
}
|
|
1192
|
+
if (!anySplit) return leaves;
|
|
1193
|
+
refineLeaves(state, topology, params, leaves);
|
|
1194
|
+
}
|
|
1195
|
+
return leaves;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
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;
|
|
1225
|
+
return result;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
const scratchTile = { space: 0, level: 0, x: 0, y: 0 };
|
|
1229
|
+
const scratchNbr = { space: 0, level: 0, x: 0, y: 0 };
|
|
1230
|
+
const scratchParentTile = { space: 0, level: 0, x: 0, y: 0 };
|
|
1231
|
+
const scratchParentNbr = { space: 0, level: 0, x: 0, y: 0 };
|
|
1232
|
+
function buildSeams2to1(topology, leaves, outSeams, outIndex) {
|
|
1233
|
+
if (outSeams.capacity < leaves.count) {
|
|
1234
|
+
throw new Error("SeamTable capacity is smaller than LeafSet.count.");
|
|
1235
|
+
}
|
|
1236
|
+
const index = buildLeafIndex(leaves, outIndex);
|
|
1237
|
+
outSeams.count = leaves.count;
|
|
1238
|
+
const neighbors = outSeams.neighbors;
|
|
1239
|
+
for (let i = 0; i < leaves.count; i++) {
|
|
1240
|
+
const base = i * 8;
|
|
1241
|
+
const space = leaves.space[i];
|
|
1242
|
+
const level = leaves.level[i];
|
|
1243
|
+
const x = leaves.x[i];
|
|
1244
|
+
const y = leaves.y[i];
|
|
1245
|
+
for (let dir = 0; dir < 4; dir++) {
|
|
1246
|
+
const outOffset = base + dir * 2;
|
|
1247
|
+
neighbors[outOffset + 0] = U32_EMPTY;
|
|
1248
|
+
neighbors[outOffset + 1] = U32_EMPTY;
|
|
1249
|
+
scratchTile.space = space;
|
|
1250
|
+
scratchTile.level = level;
|
|
1251
|
+
scratchTile.x = x;
|
|
1252
|
+
scratchTile.y = y;
|
|
1253
|
+
if (!topology.neighborSameLevel(scratchTile, dir, scratchNbr)) continue;
|
|
1254
|
+
let j = lookupSpatialIndexRaw(index, scratchNbr.space, scratchNbr.level, scratchNbr.x, scratchNbr.y);
|
|
1255
|
+
if (j !== U32_EMPTY) {
|
|
1256
|
+
neighbors[outOffset + 0] = j;
|
|
1257
|
+
continue;
|
|
1258
|
+
}
|
|
1259
|
+
if (level > 0) {
|
|
1260
|
+
const px = x >>> 1;
|
|
1261
|
+
const py = y >>> 1;
|
|
1262
|
+
scratchParentTile.space = space;
|
|
1263
|
+
scratchParentTile.level = level - 1;
|
|
1264
|
+
scratchParentTile.x = px;
|
|
1265
|
+
scratchParentTile.y = py;
|
|
1266
|
+
if (topology.neighborSameLevel(scratchParentTile, dir, scratchParentNbr)) {
|
|
1267
|
+
j = lookupSpatialIndexRaw(
|
|
1268
|
+
index,
|
|
1269
|
+
scratchParentNbr.space,
|
|
1270
|
+
scratchParentNbr.level,
|
|
1271
|
+
scratchParentNbr.x,
|
|
1272
|
+
scratchParentNbr.y
|
|
1273
|
+
);
|
|
1274
|
+
if (j !== U32_EMPTY) {
|
|
1275
|
+
neighbors[outOffset + 0] = j;
|
|
1276
|
+
continue;
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
const childLevel = scratchNbr.level + 1;
|
|
1281
|
+
const x2 = scratchNbr.x << 1 >>> 0;
|
|
1282
|
+
const y2 = scratchNbr.y << 1 >>> 0;
|
|
1283
|
+
let ax = 0;
|
|
1284
|
+
let ay = 0;
|
|
1285
|
+
let bx = 0;
|
|
1286
|
+
let by = 0;
|
|
1287
|
+
switch (dir) {
|
|
1288
|
+
case Dir.LEFT:
|
|
1289
|
+
ax = x2 + 1;
|
|
1290
|
+
ay = y2;
|
|
1291
|
+
bx = x2 + 1;
|
|
1292
|
+
by = y2 + 1;
|
|
1293
|
+
break;
|
|
1294
|
+
case Dir.RIGHT:
|
|
1295
|
+
ax = x2;
|
|
1296
|
+
ay = y2;
|
|
1297
|
+
bx = x2;
|
|
1298
|
+
by = y2 + 1;
|
|
1299
|
+
break;
|
|
1300
|
+
case Dir.TOP:
|
|
1301
|
+
ax = x2;
|
|
1302
|
+
ay = y2 + 1;
|
|
1303
|
+
bx = x2 + 1;
|
|
1304
|
+
by = y2 + 1;
|
|
1305
|
+
break;
|
|
1306
|
+
case Dir.BOTTOM:
|
|
1307
|
+
ax = x2;
|
|
1308
|
+
ay = y2;
|
|
1309
|
+
bx = x2 + 1;
|
|
1310
|
+
by = y2;
|
|
1311
|
+
break;
|
|
1312
|
+
}
|
|
1313
|
+
j = lookupSpatialIndexRaw(index, scratchNbr.space, childLevel, ax, ay);
|
|
1314
|
+
if (j !== U32_EMPTY) neighbors[outOffset + 0] = j;
|
|
1315
|
+
j = lookupSpatialIndexRaw(index, scratchNbr.space, childLevel, bx, by);
|
|
1316
|
+
if (j !== U32_EMPTY) neighbors[outOffset + 1] = j;
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
return outSeams;
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
function createFlatTopology(cfg) {
|
|
1323
|
+
const halfRoot = 0.5 * cfg.rootSize;
|
|
1324
|
+
const maxHeight = cfg.maxHeight ?? 0;
|
|
1325
|
+
const topology = {
|
|
1326
|
+
spaceCount: 1,
|
|
1327
|
+
maxRootCount: 1,
|
|
1328
|
+
neighborSameLevel(tile, dir, out) {
|
|
1329
|
+
const level = tile.level;
|
|
1330
|
+
const x = tile.x;
|
|
1331
|
+
const y = tile.y;
|
|
1332
|
+
let nx = x;
|
|
1333
|
+
let ny = y;
|
|
1334
|
+
switch (dir) {
|
|
1335
|
+
case Dir.LEFT:
|
|
1336
|
+
nx = x - 1;
|
|
1337
|
+
break;
|
|
1338
|
+
case Dir.RIGHT:
|
|
1339
|
+
nx = x + 1;
|
|
1340
|
+
break;
|
|
1341
|
+
case Dir.TOP:
|
|
1342
|
+
ny = y - 1;
|
|
1343
|
+
break;
|
|
1344
|
+
case Dir.BOTTOM:
|
|
1345
|
+
ny = y + 1;
|
|
1346
|
+
break;
|
|
1347
|
+
}
|
|
1348
|
+
if (nx < 0 || ny < 0) return false;
|
|
1349
|
+
const maxCoord = (1 << level) - 1;
|
|
1350
|
+
if (nx > maxCoord || ny > maxCoord) return false;
|
|
1351
|
+
out.space = 0;
|
|
1352
|
+
out.level = level;
|
|
1353
|
+
out.x = nx;
|
|
1354
|
+
out.y = ny;
|
|
1355
|
+
return true;
|
|
1356
|
+
},
|
|
1357
|
+
tileBounds(tile, cameraOrigin, out) {
|
|
1358
|
+
const level = tile.level;
|
|
1359
|
+
const scale = 1 / (1 << level);
|
|
1360
|
+
const size = cfg.rootSize * scale;
|
|
1361
|
+
const minX = cfg.origin.x + (tile.x * size - halfRoot);
|
|
1362
|
+
const minZ = cfg.origin.z + (tile.y * size - halfRoot);
|
|
1363
|
+
const centerX = minX + 0.5 * size;
|
|
1364
|
+
const centerY = cfg.origin.y;
|
|
1365
|
+
const centerZ = minZ + 0.5 * size;
|
|
1366
|
+
out.cx = centerX - cameraOrigin.x;
|
|
1367
|
+
out.cy = centerY - cameraOrigin.y;
|
|
1368
|
+
out.cz = centerZ - cameraOrigin.z;
|
|
1369
|
+
out.r = 0.7071067811865476 * size + maxHeight;
|
|
1370
|
+
},
|
|
1371
|
+
rootTiles(_cameraOrigin, out) {
|
|
1372
|
+
const root = out[0];
|
|
1373
|
+
root.space = 0;
|
|
1374
|
+
root.level = 0;
|
|
1375
|
+
root.x = 0;
|
|
1376
|
+
root.y = 0;
|
|
1377
|
+
return 1;
|
|
1378
|
+
}
|
|
1379
|
+
};
|
|
1380
|
+
return topology;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
function createInfiniteFlatTopology(cfg) {
|
|
1384
|
+
const halfRoot = 0.5 * cfg.rootSize;
|
|
1385
|
+
const maxHeight = cfg.maxHeight ?? 0;
|
|
1386
|
+
const rootGridRadius = Math.max(0, Math.floor(cfg.rootGridRadius ?? 1));
|
|
1387
|
+
const rootWidth = rootGridRadius * 2 + 1;
|
|
1388
|
+
return {
|
|
1389
|
+
spaceCount: 1,
|
|
1390
|
+
maxRootCount: rootWidth * rootWidth,
|
|
1391
|
+
neighborSameLevel(tile, dir, out) {
|
|
1392
|
+
let nx = tile.x;
|
|
1393
|
+
let ny = tile.y;
|
|
1394
|
+
switch (dir) {
|
|
1395
|
+
case Dir.LEFT:
|
|
1396
|
+
nx = tile.x - 1;
|
|
1397
|
+
break;
|
|
1398
|
+
case Dir.RIGHT:
|
|
1399
|
+
nx = tile.x + 1;
|
|
1400
|
+
break;
|
|
1401
|
+
case Dir.TOP:
|
|
1402
|
+
ny = tile.y - 1;
|
|
1403
|
+
break;
|
|
1404
|
+
case Dir.BOTTOM:
|
|
1405
|
+
ny = tile.y + 1;
|
|
1406
|
+
break;
|
|
1407
|
+
}
|
|
1408
|
+
out.space = tile.space;
|
|
1409
|
+
out.level = tile.level;
|
|
1410
|
+
out.x = nx;
|
|
1411
|
+
out.y = ny;
|
|
1412
|
+
return true;
|
|
1413
|
+
},
|
|
1414
|
+
tileBounds(tile, cameraOrigin, out) {
|
|
1415
|
+
const level = tile.level;
|
|
1416
|
+
const scale = 1 / (1 << level);
|
|
1417
|
+
const size = cfg.rootSize * scale;
|
|
1418
|
+
const minX = cfg.origin.x + (tile.x * size - halfRoot);
|
|
1419
|
+
const minZ = cfg.origin.z + (tile.y * size - halfRoot);
|
|
1420
|
+
const centerX = minX + 0.5 * size;
|
|
1421
|
+
const centerY = cfg.origin.y;
|
|
1422
|
+
const centerZ = minZ + 0.5 * size;
|
|
1423
|
+
out.cx = centerX - cameraOrigin.x;
|
|
1424
|
+
out.cy = centerY - cameraOrigin.y;
|
|
1425
|
+
out.cz = centerZ - cameraOrigin.z;
|
|
1426
|
+
out.r = 0.7071067811865476 * size + maxHeight;
|
|
1427
|
+
},
|
|
1428
|
+
rootTiles(cameraOrigin, out) {
|
|
1429
|
+
const camRootX = Math.floor((cameraOrigin.x - cfg.origin.x + halfRoot) / cfg.rootSize);
|
|
1430
|
+
const camRootY = Math.floor((cameraOrigin.z - cfg.origin.z + halfRoot) / cfg.rootSize);
|
|
1431
|
+
let index = 0;
|
|
1432
|
+
for (let dy = -rootGridRadius; dy <= rootGridRadius; dy++) {
|
|
1433
|
+
for (let dx = -rootGridRadius; dx <= rootGridRadius; dx++) {
|
|
1434
|
+
const root = out[index];
|
|
1435
|
+
root.space = 0;
|
|
1436
|
+
root.level = 0;
|
|
1437
|
+
root.x = camRootX + dx;
|
|
1438
|
+
root.y = camRootY + dy;
|
|
1439
|
+
index++;
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
return index;
|
|
1443
|
+
}
|
|
1444
|
+
};
|
|
1445
|
+
}
|
|
1446
|
+
|
|
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
|
+
}
|
|
1524
|
+
return {
|
|
1525
|
+
spaceCount: 6,
|
|
1526
|
+
maxRootCount: 6,
|
|
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;
|
|
1555
|
+
},
|
|
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;
|
|
1599
|
+
},
|
|
1600
|
+
rootTiles(_cameraOrigin, out) {
|
|
1601
|
+
for (let s = 0; s < 6; s++) {
|
|
1602
|
+
const root = out[s];
|
|
1603
|
+
root.space = s;
|
|
1604
|
+
root.level = 0;
|
|
1605
|
+
root.x = 0;
|
|
1606
|
+
root.y = 0;
|
|
1607
|
+
}
|
|
1608
|
+
return 6;
|
|
1609
|
+
},
|
|
1610
|
+
projection: "cubeSphere",
|
|
1611
|
+
radius,
|
|
1612
|
+
center
|
|
1613
|
+
};
|
|
1614
|
+
}
|
|
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
|
+
}
|
|
1744
|
+
function cloneSpatialIndex(target, source) {
|
|
1745
|
+
if (target.size !== source.size) {
|
|
1746
|
+
throw new Error(
|
|
1747
|
+
`SpatialIndex size mismatch (target=${target.size}, source=${source.size}).`
|
|
1748
|
+
);
|
|
1749
|
+
}
|
|
1750
|
+
target.mask = source.mask;
|
|
1751
|
+
target.stampGen = source.stampGen;
|
|
1752
|
+
target.stamp.set(source.stamp);
|
|
1753
|
+
target.keysSpace.set(source.keysSpace);
|
|
1754
|
+
target.keysLevel.set(source.keysLevel);
|
|
1755
|
+
target.keysX.set(source.keysX);
|
|
1756
|
+
target.keysY.set(source.keysY);
|
|
1757
|
+
target.values.set(source.values);
|
|
1758
|
+
}
|
|
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
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
function createCpuTerrainCache(maxNodes, initialConfig) {
|
|
1826
|
+
let config = initialConfig;
|
|
1827
|
+
const shape = {
|
|
1828
|
+
edgeVertexCount: config.innerTileSegments + 3,
|
|
1829
|
+
verticesPerNode: 0
|
|
1830
|
+
};
|
|
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);
|
|
1852
|
+
};
|
|
1853
|
+
const computeNormal = (leafIndex, gx, gy, tileSize) => {
|
|
1854
|
+
const stepWorld = tileSize / config.innerTileSegments;
|
|
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();
|
|
1866
|
+
};
|
|
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();
|
|
1904
|
+
};
|
|
1905
|
+
const sampleFromLookup = (lookup) => {
|
|
1906
|
+
const height = rawHeightFromLookup(lookup);
|
|
1907
|
+
const scaledHeight = config.originY + height * config.elevationScale;
|
|
1908
|
+
const normal = computeNormal(
|
|
1909
|
+
lookup.leafIndex,
|
|
1910
|
+
gridScratch.gx,
|
|
1911
|
+
gridScratch.gy,
|
|
1912
|
+
lookup.tileSize
|
|
1913
|
+
);
|
|
1914
|
+
return { elevation: scaledHeight, normal, valid: true };
|
|
1915
|
+
};
|
|
1916
|
+
const sampleTerrain = (worldX, worldZ) => {
|
|
1917
|
+
if (!state.hasSnapshot) {
|
|
1918
|
+
return { elevation: 0, normal: new Vector3(0, 1, 0), valid: false };
|
|
1919
|
+
}
|
|
1920
|
+
const lookup = lookupTile(state.frontIndex, config, worldX, worldZ);
|
|
1921
|
+
if (!lookup.found) {
|
|
1922
|
+
return { elevation: 0, normal: new Vector3(0, 1, 0), valid: false };
|
|
1923
|
+
}
|
|
1924
|
+
return sampleFromLookup(lookup);
|
|
1925
|
+
};
|
|
1926
|
+
const getElevation = (worldX, worldZ) => {
|
|
1927
|
+
if (!state.hasSnapshot) {
|
|
1928
|
+
return { elevation: 0, valid: false };
|
|
1929
|
+
}
|
|
1930
|
+
const lookup = lookupTile(state.frontIndex, config, worldX, worldZ);
|
|
1931
|
+
if (!lookup.found) {
|
|
1932
|
+
return { elevation: 0, valid: false };
|
|
1933
|
+
}
|
|
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
|
+
};
|
|
2010
|
+
};
|
|
2011
|
+
const api = {
|
|
2012
|
+
get generation() {
|
|
2013
|
+
return state.generation;
|
|
2014
|
+
},
|
|
2015
|
+
get ready() {
|
|
2016
|
+
return state.hasSnapshot;
|
|
2017
|
+
},
|
|
2018
|
+
updateConfig(nextConfig) {
|
|
2019
|
+
config = nextConfig;
|
|
2020
|
+
shape.edgeVertexCount = config.innerTileSegments + 3;
|
|
2021
|
+
shape.verticesPerNode = shape.edgeVertexCount * shape.edgeVertexCount;
|
|
2022
|
+
totalElements = maxNodes * shape.verticesPerNode;
|
|
2023
|
+
},
|
|
2024
|
+
triggerReadback(renderer, attribute, spatialIndex, boundsAttribute, activeLeafCount) {
|
|
2025
|
+
triggerSnapshotReadback(state, renderer, attribute, spatialIndex, boundsAttribute, {
|
|
2026
|
+
activeLeafCount: activeLeafCount ?? 0,
|
|
2027
|
+
totalElements,
|
|
2028
|
+
elevationScale: config.elevationScale,
|
|
2029
|
+
originY: config.originY
|
|
2030
|
+
});
|
|
2031
|
+
},
|
|
2032
|
+
getElevation(worldX, worldZ) {
|
|
2033
|
+
const sample = getElevation(worldX, worldZ);
|
|
2034
|
+
return sample.valid ? sample.elevation : null;
|
|
2035
|
+
},
|
|
2036
|
+
getNormal(worldX, worldZ) {
|
|
2037
|
+
return sampleTerrain(worldX, worldZ).normal;
|
|
2038
|
+
},
|
|
2039
|
+
getTile(worldX, worldZ) {
|
|
2040
|
+
if (!state.hasSnapshot) return null;
|
|
2041
|
+
return tileFromLookup(lookupTile(state.frontIndex, config, worldX, worldZ));
|
|
2042
|
+
},
|
|
2043
|
+
getTileBounds(worldX, worldZ) {
|
|
2044
|
+
if (!state.hasSnapshot) return null;
|
|
2045
|
+
return tileBoundsFromLookup(
|
|
2046
|
+
lookupTile(state.frontIndex, config, worldX, worldZ),
|
|
2047
|
+
config.originY
|
|
2048
|
+
);
|
|
2049
|
+
},
|
|
2050
|
+
getGlobalElevationRange() {
|
|
2051
|
+
return state.globalRange;
|
|
2052
|
+
},
|
|
2053
|
+
sampleTerrainBatch(positions) {
|
|
2054
|
+
const count = Math.floor(positions.length / 2);
|
|
2055
|
+
const elevations = new Float32Array(count);
|
|
2056
|
+
const normals = new Float32Array(count * 3);
|
|
2057
|
+
const valid = new Uint8Array(count);
|
|
2058
|
+
if (!state.hasSnapshot) {
|
|
2059
|
+
return { elevations, normals, valid, generation: state.generation };
|
|
2060
|
+
}
|
|
2061
|
+
let lastTile;
|
|
2062
|
+
for (let i = 0; i < count; i += 1) {
|
|
2063
|
+
const worldX = positions[i * 2] ?? 0;
|
|
2064
|
+
const worldZ = positions[i * 2 + 1] ?? 0;
|
|
2065
|
+
let lookup;
|
|
2066
|
+
if (lastTile && worldX >= lastTile.tileMinX && worldX <= lastTile.tileMinX + lastTile.tileSize && worldZ >= lastTile.tileMinZ && worldZ <= lastTile.tileMinZ + lastTile.tileSize) {
|
|
2067
|
+
lookup = {
|
|
2068
|
+
found: true,
|
|
2069
|
+
leafIndex: lastTile.leafIndex,
|
|
2070
|
+
space: 0,
|
|
2071
|
+
level: lastTile.level,
|
|
2072
|
+
tileX: lastTile.tileX,
|
|
2073
|
+
tileY: lastTile.tileY,
|
|
2074
|
+
tileSize: lastTile.tileSize,
|
|
2075
|
+
localU: (worldX - lastTile.tileMinX) / lastTile.tileSize,
|
|
2076
|
+
localV: (worldZ - lastTile.tileMinZ) / lastTile.tileSize
|
|
2077
|
+
};
|
|
2078
|
+
} else {
|
|
2079
|
+
lookup = lookupTile(state.frontIndex, config, worldX, worldZ);
|
|
2080
|
+
if (lookup.found) {
|
|
2081
|
+
lastTile = {
|
|
2082
|
+
leafIndex: lookup.leafIndex,
|
|
2083
|
+
level: lookup.level,
|
|
2084
|
+
tileX: lookup.tileX,
|
|
2085
|
+
tileY: lookup.tileY,
|
|
2086
|
+
tileSize: lookup.tileSize,
|
|
2087
|
+
tileMinX: worldX - lookup.localU * lookup.tileSize,
|
|
2088
|
+
tileMinZ: worldZ - lookup.localV * lookup.tileSize
|
|
2089
|
+
};
|
|
2090
|
+
} else {
|
|
2091
|
+
lastTile = void 0;
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
if (!lookup?.found) {
|
|
2095
|
+
normals[i * 3 + 1] = 1;
|
|
2096
|
+
continue;
|
|
2097
|
+
}
|
|
2098
|
+
const sample = sampleFromLookup(lookup);
|
|
2099
|
+
elevations[i] = sample.elevation;
|
|
2100
|
+
normals[i * 3] = sample.normal.x;
|
|
2101
|
+
normals[i * 3 + 1] = sample.normal.y;
|
|
2102
|
+
normals[i * 3 + 2] = sample.normal.z;
|
|
2103
|
+
valid[i] = 1;
|
|
2104
|
+
}
|
|
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]);
|
|
2122
|
+
},
|
|
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
|
+
}
|
|
2232
|
+
};
|
|
2233
|
+
return api;
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
function createTerrainQuery(cache) {
|
|
2237
|
+
return {
|
|
2238
|
+
get generation() {
|
|
2239
|
+
return cache.generation;
|
|
2240
|
+
},
|
|
2241
|
+
getElevation(worldX, worldZ) {
|
|
2242
|
+
return cache.getElevation(worldX, worldZ);
|
|
2243
|
+
},
|
|
2244
|
+
getNormal(worldX, worldZ) {
|
|
2245
|
+
return cache.getNormal(worldX, worldZ);
|
|
2246
|
+
},
|
|
2247
|
+
getTile(worldX, worldZ) {
|
|
2248
|
+
return cache.getTile(worldX, worldZ);
|
|
2249
|
+
},
|
|
2250
|
+
getTileBounds(worldX, worldZ) {
|
|
2251
|
+
return cache.getTileBounds(worldX, worldZ);
|
|
2252
|
+
},
|
|
2253
|
+
getGlobalElevationRange() {
|
|
2254
|
+
return cache.getGlobalElevationRange();
|
|
2255
|
+
},
|
|
2256
|
+
sampleTerrain(worldX, worldZ) {
|
|
2257
|
+
return cache.sampleTerrain(worldX, worldZ);
|
|
2258
|
+
},
|
|
2259
|
+
sampleTerrainBatch(positions) {
|
|
2260
|
+
return cache.sampleTerrainBatch(positions);
|
|
2261
|
+
}
|
|
2262
|
+
};
|
|
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
|
+
}
|
|
2319
|
+
|
|
2320
|
+
const WGSIZE = 64;
|
|
2321
|
+
function buildReductionKernel(elevationFieldNode, boundsNode, verticesPerNode) {
|
|
2322
|
+
const elemsPerThread = Math.ceil(verticesPerNode / WGSIZE);
|
|
2323
|
+
return Fn(() => {
|
|
2324
|
+
const sharedMin = workgroupArray("float", WGSIZE);
|
|
2325
|
+
const sharedMax = workgroupArray("float", WGSIZE);
|
|
2326
|
+
const tid = int(localId.x);
|
|
2327
|
+
const tileIdx = int(workgroupId.z);
|
|
2328
|
+
const baseOffset = tileIdx.mul(int(verticesPerNode));
|
|
2329
|
+
const start = tid.mul(int(elemsPerThread));
|
|
2330
|
+
const end = min(start.add(int(elemsPerThread)), int(verticesPerNode));
|
|
2331
|
+
const localMin = float(1e10).toVar("localMin");
|
|
2332
|
+
const localMax = float(-1e10).toVar("localMax");
|
|
2333
|
+
Loop({ start, end, type: "int", condition: "<" }, ({ i }) => {
|
|
2334
|
+
const h = elevationFieldNode.element(baseOffset.add(i));
|
|
2335
|
+
localMin.assign(min(localMin, h));
|
|
2336
|
+
localMax.assign(max(localMax, h));
|
|
2337
|
+
});
|
|
2338
|
+
sharedMin.element(tid).assign(localMin);
|
|
2339
|
+
sharedMax.element(tid).assign(localMax);
|
|
2340
|
+
workgroupBarrier();
|
|
2341
|
+
If(tid.equal(int(0)), () => {
|
|
2342
|
+
const finalMin = float(1e10).toVar("finalMin");
|
|
2343
|
+
const finalMax = float(-1e10).toVar("finalMax");
|
|
2344
|
+
Loop(WGSIZE, ({ i }) => {
|
|
2345
|
+
finalMin.assign(min(finalMin, sharedMin.element(i)));
|
|
2346
|
+
finalMax.assign(max(finalMax, sharedMax.element(i)));
|
|
2347
|
+
});
|
|
2348
|
+
const outIdx = tileIdx.mul(int(2));
|
|
2349
|
+
boundsNode.element(outIdx).assign(finalMin);
|
|
2350
|
+
boundsNode.element(outIdx.add(int(1))).assign(finalMax);
|
|
2351
|
+
});
|
|
2352
|
+
})().computeKernel([WGSIZE, 1, 1]);
|
|
2353
|
+
}
|
|
2354
|
+
const tileBoundsContextTask = task((get, work) => {
|
|
2355
|
+
const elevationFieldContext = get(createElevationFieldContextTask);
|
|
2356
|
+
const maxNodesValue = get(maxNodes);
|
|
2357
|
+
const edgeVertexCount = get(innerTileSegments) + 3;
|
|
2358
|
+
return work(() => {
|
|
2359
|
+
const data = new Float32Array(maxNodesValue * 2);
|
|
2360
|
+
const attribute = new StorageBufferAttribute(data, 1);
|
|
2361
|
+
const node = storage(attribute, "float", maxNodesValue * 2);
|
|
2362
|
+
const verticesPerNode = edgeVertexCount * edgeVertexCount;
|
|
2363
|
+
const kernel = buildReductionKernel(elevationFieldContext.node, node, verticesPerNode);
|
|
2364
|
+
return { data, attribute, node, kernel };
|
|
2365
|
+
});
|
|
2366
|
+
}).displayName("tileBoundsContextTask");
|
|
2367
|
+
const tileBoundsReductionTask = task(
|
|
2368
|
+
(get, work, { resources }) => {
|
|
2369
|
+
get(executeComputeTask);
|
|
2370
|
+
const boundsContext = get(tileBoundsContextTask);
|
|
2371
|
+
const leafState = get(leafGpuBufferTask);
|
|
2372
|
+
return work(() => {
|
|
2373
|
+
if (resources?.renderer && leafState.count > 0) {
|
|
2374
|
+
resources.renderer.compute(boundsContext.kernel, [1, 1, leafState.count]);
|
|
2375
|
+
}
|
|
2376
|
+
return boundsContext;
|
|
2377
|
+
});
|
|
2378
|
+
}
|
|
2379
|
+
).displayName("tileBoundsReductionTask").lane("gpu");
|
|
2380
|
+
|
|
2381
|
+
const terrainQueryTask = task((get, work) => {
|
|
2382
|
+
const maxNodesValue = get(maxNodes);
|
|
2383
|
+
const innerTileSegmentsValue = get(innerTileSegments);
|
|
2384
|
+
const maxLevelValue = get(maxLevel);
|
|
2385
|
+
const rootSizeValue = get(rootSize);
|
|
2386
|
+
const originValue = get(origin);
|
|
2387
|
+
const elevationScaleValue = get(elevationScale);
|
|
2388
|
+
const radiusValue = get(radius);
|
|
2389
|
+
const topologyValue = get(topologyTask);
|
|
2390
|
+
const projectionValue = topologyValue.projection ?? "flat";
|
|
2391
|
+
return work((prev) => {
|
|
2392
|
+
const shapeKey = `${maxNodesValue}:${innerTileSegmentsValue}:${projectionValue}`;
|
|
2393
|
+
const configValues = {
|
|
2394
|
+
rootSize: rootSizeValue,
|
|
2395
|
+
originX: originValue.x,
|
|
2396
|
+
originY: originValue.y,
|
|
2397
|
+
originZ: originValue.z,
|
|
2398
|
+
innerTileSegments: innerTileSegmentsValue,
|
|
2399
|
+
elevationScale: elevationScaleValue,
|
|
2400
|
+
maxLevel: maxLevelValue,
|
|
2401
|
+
projection: projectionValue,
|
|
2402
|
+
radius: topologyValue.radius ?? radiusValue
|
|
2403
|
+
};
|
|
2404
|
+
let cache = prev?.cache;
|
|
2405
|
+
let query = prev?.query;
|
|
2406
|
+
let sphereQuery = prev?.sphereQuery ?? null;
|
|
2407
|
+
if (!cache || !query || prev?.shapeKey !== shapeKey) {
|
|
2408
|
+
cache = createCpuTerrainCache(maxNodesValue, configValues);
|
|
2409
|
+
query = createTerrainQuery(cache);
|
|
2410
|
+
sphereQuery = projectionValue === "cubeSphere" ? createTerrainSphereQuery(cache) : null;
|
|
2411
|
+
}
|
|
2412
|
+
cache.updateConfig(configValues);
|
|
2413
|
+
return { cache, query, sphereQuery, shapeKey };
|
|
2414
|
+
});
|
|
2415
|
+
}).displayName("terrainQueryTask");
|
|
2416
|
+
const terrainReadbackTask = task(
|
|
2417
|
+
(get, work, { resources }) => {
|
|
2418
|
+
const boundsContext = get(tileBoundsReductionTask);
|
|
2419
|
+
const elevationFieldContext = get(createElevationFieldContextTask);
|
|
2420
|
+
const quadtreeConfig = get(quadtreeConfigTask);
|
|
2421
|
+
const leafState = get(leafGpuBufferTask);
|
|
2422
|
+
const { cache } = get(terrainQueryTask);
|
|
2423
|
+
return work(() => {
|
|
2424
|
+
if (!resources?.renderer) return;
|
|
2425
|
+
cache.triggerReadback(
|
|
2426
|
+
resources.renderer,
|
|
2427
|
+
elevationFieldContext.attribute,
|
|
2428
|
+
quadtreeConfig.state.leafIndex,
|
|
2429
|
+
boundsContext.attribute,
|
|
2430
|
+
leafState.count
|
|
2431
|
+
);
|
|
2432
|
+
});
|
|
2433
|
+
}
|
|
2434
|
+
).displayName("terrainReadbackTask").lane("gpu");
|
|
2435
|
+
|
|
2436
|
+
const topologyTask = task((get, work) => {
|
|
2437
|
+
const customTopology = get(topology);
|
|
2438
|
+
const rootSizeVal = get(rootSize);
|
|
2439
|
+
const originVal = get(origin);
|
|
2440
|
+
return work(() => {
|
|
2441
|
+
if (customTopology) return customTopology;
|
|
2442
|
+
return createFlatTopology({ rootSize: rootSizeVal, origin: originVal });
|
|
2443
|
+
});
|
|
2444
|
+
}).displayName("topologyTask");
|
|
2445
|
+
const quadtreeConfigTask = task((get, work) => {
|
|
2446
|
+
const topologyVal = get(topologyTask);
|
|
2447
|
+
const maxNodesVal = get(maxNodes);
|
|
2448
|
+
const maxLevelVal = get(maxLevel);
|
|
2449
|
+
return work(() => {
|
|
2450
|
+
const state = createState({ maxNodes: maxNodesVal, maxLevel: maxLevelVal }, topologyVal);
|
|
2451
|
+
return {
|
|
2452
|
+
state,
|
|
2453
|
+
topology: topologyVal
|
|
2454
|
+
};
|
|
2455
|
+
});
|
|
2456
|
+
}).displayName("quadtreeConfigTask");
|
|
2457
|
+
const quadtreeUpdateTask = task((get, work) => {
|
|
2458
|
+
const quadtreeConfig = get(quadtreeConfigTask);
|
|
2459
|
+
const quadtreeUpdateConfig = get(quadtreeUpdate);
|
|
2460
|
+
const { query: terrainQuery, sphereQuery } = get(terrainQueryTask);
|
|
2461
|
+
let outLeaves = void 0;
|
|
2462
|
+
const cameraPosition = new Vector3();
|
|
2463
|
+
return work(() => {
|
|
2464
|
+
const cam = quadtreeUpdateConfig.cameraOrigin;
|
|
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
|
+
}
|
|
2471
|
+
outLeaves = update(
|
|
2472
|
+
quadtreeConfig.state,
|
|
2473
|
+
quadtreeConfig.topology,
|
|
2474
|
+
quadtreeUpdateConfig,
|
|
2475
|
+
outLeaves
|
|
2476
|
+
);
|
|
2477
|
+
return outLeaves;
|
|
2478
|
+
});
|
|
2479
|
+
}).displayName("quadtreeUpdateTask");
|
|
2480
|
+
const leafStorageTask = task((get, work) => {
|
|
2481
|
+
const maxNodesVal = get(maxNodes);
|
|
2482
|
+
return work(() => createLeafStorage(maxNodesVal));
|
|
2483
|
+
}).displayName("leafStorageTask");
|
|
2484
|
+
const leafGpuBufferTask = task((get, work) => {
|
|
2485
|
+
const leafSet = get(quadtreeUpdateTask);
|
|
2486
|
+
const leafStorage = get(leafStorageTask);
|
|
2487
|
+
return work(() => {
|
|
2488
|
+
const bufferCapacity = leafStorage.data.length / 4;
|
|
2489
|
+
const leafCount = Math.min(leafSet.count, bufferCapacity);
|
|
2490
|
+
for (let i = 0; i < leafCount; i += 1) {
|
|
2491
|
+
const offset = i * 4;
|
|
2492
|
+
leafStorage.data[offset] = leafSet.level[i] ?? 0;
|
|
2493
|
+
leafStorage.data[offset + 1] = leafSet.x[i] ?? 0;
|
|
2494
|
+
leafStorage.data[offset + 2] = leafSet.y[i] ?? 0;
|
|
2495
|
+
leafStorage.data[offset + 3] = leafSet.space[i] ?? 0;
|
|
2496
|
+
}
|
|
2497
|
+
leafStorage.attribute.needsUpdate = true;
|
|
2498
|
+
leafStorage.node.needsUpdate = true;
|
|
2499
|
+
return {
|
|
2500
|
+
count: leafCount,
|
|
2501
|
+
data: leafStorage.data,
|
|
2502
|
+
attribute: leafStorage.attribute,
|
|
2503
|
+
node: leafStorage.node
|
|
2504
|
+
};
|
|
2505
|
+
});
|
|
2506
|
+
}).displayName("leafGpuBufferTask");
|
|
2507
|
+
|
|
2508
|
+
function createElevationFunction(callback) {
|
|
2509
|
+
const tslFunction = (args) => {
|
|
2510
|
+
const params = {
|
|
2511
|
+
worldPosition: args.worldPosition,
|
|
2512
|
+
rootSize: args.rootSize,
|
|
2513
|
+
rootUV: args.rootUV,
|
|
2514
|
+
tileUV: args.tileUV,
|
|
2515
|
+
tileLevel: args.tileLevel,
|
|
2516
|
+
tileSize: args.tileSize,
|
|
2517
|
+
tileOriginVec2: args.tileOriginVec2,
|
|
2518
|
+
nodeIndex: args.nodeIndex
|
|
2519
|
+
};
|
|
2520
|
+
return callback(params);
|
|
2521
|
+
};
|
|
2522
|
+
return Fn$1(tslFunction);
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
function createTerrainUniforms(params) {
|
|
2526
|
+
const sanitizedId = params.instanceId?.replace(/-/g, "_");
|
|
2527
|
+
const suffix = sanitizedId ? `_${sanitizedId}` : "";
|
|
2528
|
+
const uRootOrigin = uniform(
|
|
2529
|
+
new Vector3$1(params.rootOrigin.x, params.rootOrigin.y, params.rootOrigin.z)
|
|
2530
|
+
).setName(`uRootOrigin${suffix}`);
|
|
2531
|
+
const uRootSize = uniform(float(params.rootSize)).setName(`uRootSize${suffix}`);
|
|
2532
|
+
const uInnerTileSegments = uniform(int(params.innerTileSegments)).setName(
|
|
2533
|
+
`uInnerTileSegments${suffix}`
|
|
2534
|
+
);
|
|
2535
|
+
const uSkirtScale = uniform(float(params.skirtScale)).setName(`uSkirtScale${suffix}`);
|
|
2536
|
+
const uElevationScale = uniform(float(params.elevationScale)).setName(`uElevationScale${suffix}`);
|
|
2537
|
+
const uRadius = uniform(float(params.radius)).setName(`uRadius${suffix}`);
|
|
2538
|
+
return {
|
|
2539
|
+
uRootOrigin,
|
|
2540
|
+
uRootSize,
|
|
2541
|
+
uInnerTileSegments,
|
|
2542
|
+
uSkirtScale,
|
|
2543
|
+
uElevationScale,
|
|
2544
|
+
uRadius
|
|
2545
|
+
};
|
|
2546
|
+
}
|
|
2547
|
+
|
|
2548
|
+
const instanceIdTask = task(() => crypto.randomUUID()).displayName("instanceIdTask").cache("once");
|
|
2549
|
+
|
|
2550
|
+
const scratchVector3 = new Vector3();
|
|
2551
|
+
const createUniformsTask = task((get, work) => {
|
|
2552
|
+
const uniformParams = {
|
|
2553
|
+
rootOrigin: get(origin),
|
|
2554
|
+
rootSize: get(rootSize),
|
|
2555
|
+
innerTileSegments: get(innerTileSegments),
|
|
2556
|
+
skirtScale: get(skirtScale),
|
|
2557
|
+
elevationScale: get(elevationScale),
|
|
2558
|
+
radius: get(radius),
|
|
2559
|
+
instanceId: get(instanceIdTask)
|
|
2560
|
+
};
|
|
2561
|
+
return work(() => createTerrainUniforms(uniformParams));
|
|
2562
|
+
}).displayName("createUniformsTask").cache("once");
|
|
2563
|
+
const updateUniformsTask = task((get, work) => {
|
|
2564
|
+
const terrainUniformsContext = get(createUniformsTask);
|
|
2565
|
+
const rootSizeVal = get(rootSize);
|
|
2566
|
+
const rootOrigin = get(origin);
|
|
2567
|
+
const innerTileSegmentsVal = get(innerTileSegments);
|
|
2568
|
+
const skirtScaleVal = get(skirtScale);
|
|
2569
|
+
const elevationScaleVal = get(elevationScale);
|
|
2570
|
+
const radiusVal = get(radius);
|
|
2571
|
+
return work(() => {
|
|
2572
|
+
terrainUniformsContext.uRootSize.value = rootSizeVal;
|
|
2573
|
+
terrainUniformsContext.uRootOrigin.value = scratchVector3.set(
|
|
2574
|
+
rootOrigin.x,
|
|
2575
|
+
rootOrigin.y,
|
|
2576
|
+
rootOrigin.z
|
|
2577
|
+
);
|
|
2578
|
+
terrainUniformsContext.uInnerTileSegments.value = innerTileSegmentsVal;
|
|
2579
|
+
terrainUniformsContext.uSkirtScale.value = skirtScaleVal;
|
|
2580
|
+
terrainUniformsContext.uElevationScale.value = elevationScaleVal;
|
|
2581
|
+
terrainUniformsContext.uRadius.value = radiusVal;
|
|
2582
|
+
return terrainUniformsContext;
|
|
2583
|
+
});
|
|
2584
|
+
}).displayName("updateUniformsTask");
|
|
2585
|
+
|
|
2586
|
+
const createElevationFieldContextTask = task((get, work) => {
|
|
2587
|
+
const edgeVertexCount = get(innerTileSegments) + 3;
|
|
2588
|
+
const verticesPerNode = edgeVertexCount * edgeVertexCount;
|
|
2589
|
+
const totalElements = get(maxNodes) * verticesPerNode;
|
|
2590
|
+
return work(() => {
|
|
2591
|
+
const data = new Float32Array(totalElements);
|
|
2592
|
+
const attribute = new StorageBufferAttribute(data, 1);
|
|
2593
|
+
const node = storage(attribute, "float", totalElements);
|
|
2594
|
+
return {
|
|
2595
|
+
data,
|
|
2596
|
+
attribute,
|
|
2597
|
+
node
|
|
2598
|
+
};
|
|
2599
|
+
});
|
|
2600
|
+
}).displayName("createElevationFieldContextTask");
|
|
2601
|
+
const tileNodesTask = task((get, work) => {
|
|
2602
|
+
const leafStorage = get(leafStorageTask);
|
|
2603
|
+
const uniforms = get(updateUniformsTask);
|
|
2604
|
+
const topology = get(topologyTask);
|
|
2605
|
+
return work(() => {
|
|
2606
|
+
return createTileCompute(leafStorage, uniforms, topology.projection ?? "flat");
|
|
2607
|
+
});
|
|
2608
|
+
}).displayName("tileNodesTask");
|
|
2609
|
+
const elevationFieldStageTask = task((get, work) => {
|
|
2610
|
+
const tile = get(tileNodesTask);
|
|
2611
|
+
const uniforms = get(updateUniformsTask);
|
|
2612
|
+
const elevationFieldContext = get(createElevationFieldContextTask);
|
|
2613
|
+
const userElevationFn = get(elevationFn);
|
|
2614
|
+
return work(() => {
|
|
2615
|
+
const heightFn = createElevationFunction(userElevationFn);
|
|
2616
|
+
const heightWriteFn = createElevation(tile, uniforms, heightFn);
|
|
2617
|
+
return [
|
|
2618
|
+
(nodeIndex, globalVertexIndex, _uv, localCoordinates) => {
|
|
2619
|
+
const height = heightWriteFn(nodeIndex, localCoordinates);
|
|
2620
|
+
elevationFieldContext.node.element(globalVertexIndex).assign(height);
|
|
2621
|
+
}
|
|
2622
|
+
];
|
|
2623
|
+
});
|
|
2624
|
+
}).displayName("elevationFieldStageTask");
|
|
2625
|
+
|
|
2626
|
+
const createTerrainFieldTextureTask = task(
|
|
2627
|
+
(get, work, { resources }) => {
|
|
2628
|
+
const edgeVertexCount = get(innerTileSegments) + 3;
|
|
2629
|
+
const maxNodesValue = get(maxNodes);
|
|
2630
|
+
const filter = get(terrainFieldFilter);
|
|
2631
|
+
return work(
|
|
2632
|
+
() => createTerrainFieldStorage(
|
|
2633
|
+
edgeVertexCount,
|
|
2634
|
+
maxNodesValue,
|
|
2635
|
+
resources?.renderer,
|
|
2636
|
+
{ filter }
|
|
2637
|
+
)
|
|
2638
|
+
);
|
|
2639
|
+
}
|
|
2640
|
+
).displayName("createTerrainFieldTextureTask");
|
|
2641
|
+
function createNormalFromElevationField(elevationFieldNode, edgeVertexCount) {
|
|
2642
|
+
return Fn(
|
|
2643
|
+
([nodeIndex, tileSize, ix, iy, elevationScale]) => {
|
|
2644
|
+
const iEdge = int(edgeVertexCount);
|
|
2645
|
+
const verticesPerNode = iEdge.mul(iEdge);
|
|
2646
|
+
const baseOffset = int(nodeIndex).mul(verticesPerNode);
|
|
2647
|
+
const xLeft = int(ix).sub(int(1));
|
|
2648
|
+
const xRight = int(ix).add(int(1));
|
|
2649
|
+
const yUp = int(iy).sub(int(1));
|
|
2650
|
+
const yDown = int(iy).add(int(1));
|
|
2651
|
+
const hLeft = elevationFieldNode.element(baseOffset.add(int(iy).mul(iEdge).add(xLeft))).mul(elevationScale);
|
|
2652
|
+
const hRight = elevationFieldNode.element(baseOffset.add(int(iy).mul(iEdge).add(xRight))).mul(elevationScale);
|
|
2653
|
+
const hUp = elevationFieldNode.element(baseOffset.add(yUp.mul(iEdge).add(int(ix)))).mul(elevationScale);
|
|
2654
|
+
const hDown = elevationFieldNode.element(baseOffset.add(yDown.mul(iEdge).add(int(ix)))).mul(elevationScale);
|
|
2655
|
+
const innerSegments = float(iEdge).sub(float(3));
|
|
2656
|
+
const stepWorld = tileSize.div(innerSegments);
|
|
2657
|
+
const inv2Step = float(0.5).div(stepWorld);
|
|
2658
|
+
const dhdx = float(hRight).sub(float(hLeft)).mul(inv2Step);
|
|
2659
|
+
const dhdz = float(hDown).sub(float(hUp)).mul(inv2Step);
|
|
2660
|
+
const normal = vec3(dhdx.negate(), float(1), dhdz.negate()).normalize();
|
|
2661
|
+
return vec2(normal.x, normal.z);
|
|
2662
|
+
}
|
|
2663
|
+
);
|
|
2664
|
+
}
|
|
2665
|
+
const terrainFieldStageTask = task((get, work) => {
|
|
2666
|
+
const upstream = get(elevationFieldStageTask);
|
|
2667
|
+
const elevationFieldContext = get(createElevationFieldContextTask);
|
|
2668
|
+
const terrainFieldStorage = get(createTerrainFieldTextureTask);
|
|
2669
|
+
const tileEdgeVertexCount = get(innerTileSegments) + 3;
|
|
2670
|
+
const tile = get(tileNodesTask);
|
|
2671
|
+
const uniforms = get(updateUniformsTask);
|
|
2672
|
+
return work(() => {
|
|
2673
|
+
const computeNormal = createNormalFromElevationField(
|
|
2674
|
+
elevationFieldContext.node,
|
|
2675
|
+
tileEdgeVertexCount
|
|
2676
|
+
);
|
|
2677
|
+
return [
|
|
2678
|
+
...upstream,
|
|
2679
|
+
(nodeIndex, globalVertexIndex, _uv, localCoordinates) => {
|
|
2680
|
+
const ix = int(localCoordinates.x);
|
|
2681
|
+
const iy = int(localCoordinates.y);
|
|
2682
|
+
const tileSize = tile.tileSize(nodeIndex);
|
|
2683
|
+
const height = elevationFieldContext.node.element(globalVertexIndex);
|
|
2684
|
+
const normalXZ = computeNormal(
|
|
2685
|
+
nodeIndex,
|
|
2686
|
+
tileSize,
|
|
2687
|
+
ix,
|
|
2688
|
+
iy,
|
|
2689
|
+
uniforms.uElevationScale
|
|
2690
|
+
);
|
|
2691
|
+
storeTerrainField(
|
|
2692
|
+
terrainFieldStorage,
|
|
2693
|
+
ix,
|
|
2694
|
+
iy,
|
|
2695
|
+
nodeIndex,
|
|
2696
|
+
packTerrainFieldSample(height, normalXZ)
|
|
2697
|
+
);
|
|
2698
|
+
}
|
|
2699
|
+
];
|
|
2700
|
+
});
|
|
2701
|
+
}).displayName("terrainFieldStageTask");
|
|
2702
|
+
|
|
2703
|
+
const { compile: compileComputeTask, execute: executeComputeTask } = createComputePipelineTasks(terrainFieldStageTask);
|
|
2704
|
+
function createComputePipelineTasks(leafStageTask) {
|
|
2705
|
+
const compile = task((get, work) => {
|
|
2706
|
+
const pipeline = get(leafStageTask);
|
|
2707
|
+
const edgeVertexCount = get(innerTileSegments) + 3;
|
|
2708
|
+
return work(
|
|
2709
|
+
() => compileComputePipeline(pipeline, edgeVertexCount, {
|
|
2710
|
+
})
|
|
2711
|
+
);
|
|
2712
|
+
}).displayName("compileComputeTask");
|
|
2713
|
+
const execute = task(
|
|
2714
|
+
(get, work, { resources }) => {
|
|
2715
|
+
const { execute: run } = get(compile);
|
|
2716
|
+
const leafState = get(leafGpuBufferTask);
|
|
2717
|
+
return work(
|
|
2718
|
+
() => resources?.renderer ? run(resources.renderer, leafState.count) : () => {
|
|
2719
|
+
}
|
|
2720
|
+
);
|
|
2721
|
+
}
|
|
2722
|
+
).displayName("executeComputeTask").lane("gpu");
|
|
2723
|
+
return { compile, execute };
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2726
|
+
const SLOT_STRIDE = 6;
|
|
2727
|
+
function nextPow2(n) {
|
|
2728
|
+
let x = 1;
|
|
2729
|
+
while (x < n) x <<= 1;
|
|
2730
|
+
return x;
|
|
2731
|
+
}
|
|
2732
|
+
function createGpuSpatialIndex(maxEntries) {
|
|
2733
|
+
const size = nextPow2(Math.max(2, maxEntries * 2));
|
|
2734
|
+
const data = new Uint32Array(size * SLOT_STRIDE);
|
|
2735
|
+
const attribute = new StorageBufferAttribute(data, SLOT_STRIDE);
|
|
2736
|
+
const node = storage(attribute, "u32", 1).toReadOnly().setName("gpuSpatialIndex");
|
|
2737
|
+
const stampGen = uniform(uint(1)).setName("uGpuSpatialIndexStampGen");
|
|
2738
|
+
return {
|
|
2739
|
+
data,
|
|
2740
|
+
size,
|
|
2741
|
+
mask: size - 1,
|
|
2742
|
+
stampGen,
|
|
2743
|
+
attribute,
|
|
2744
|
+
node
|
|
2745
|
+
};
|
|
2746
|
+
}
|
|
2747
|
+
function uploadGpuSpatialIndex(gpuIndex, cpuIndex) {
|
|
2748
|
+
if (gpuIndex.size !== cpuIndex.size) {
|
|
2749
|
+
throw new Error(
|
|
2750
|
+
`Spatial index size mismatch (gpu=${gpuIndex.size}, cpu=${cpuIndex.size}).`
|
|
2751
|
+
);
|
|
2752
|
+
}
|
|
2753
|
+
for (let i = 0; i < cpuIndex.size; i += 1) {
|
|
2754
|
+
const base = i * SLOT_STRIDE;
|
|
2755
|
+
gpuIndex.data[base] = cpuIndex.stamp[i] ?? 0;
|
|
2756
|
+
gpuIndex.data[base + 1] = cpuIndex.keysSpace[i] ?? 0;
|
|
2757
|
+
gpuIndex.data[base + 2] = cpuIndex.keysLevel[i] ?? 0;
|
|
2758
|
+
gpuIndex.data[base + 3] = cpuIndex.keysX[i] ?? 0;
|
|
2759
|
+
gpuIndex.data[base + 4] = cpuIndex.keysY[i] ?? 0;
|
|
2760
|
+
gpuIndex.data[base + 5] = cpuIndex.values[i] ?? 0;
|
|
2761
|
+
}
|
|
2762
|
+
gpuIndex.stampGen.value = cpuIndex.stampGen >>> 0;
|
|
2763
|
+
gpuIndex.attribute.needsUpdate = true;
|
|
2764
|
+
gpuIndex.node.needsUpdate = true;
|
|
2765
|
+
}
|
|
2766
|
+
function readGpuSpatialIndexValue(spatialIndex, slot, fieldOffset) {
|
|
2767
|
+
const offset = int(slot).mul(int(SLOT_STRIDE)).add(int(fieldOffset));
|
|
2768
|
+
return spatialIndex.node.element(offset).toUint();
|
|
2769
|
+
}
|
|
2770
|
+
const mix32 = Fn(([x]) => {
|
|
2771
|
+
const v = uint(x).toVar();
|
|
2772
|
+
v.assign(v.bitXor(v.shiftRight(uint(16))));
|
|
2773
|
+
v.assign(v.mul(uint(2146121005)));
|
|
2774
|
+
v.assign(v.bitXor(v.shiftRight(uint(15))));
|
|
2775
|
+
v.assign(v.mul(uint(2221713035)));
|
|
2776
|
+
v.assign(v.bitXor(v.shiftRight(uint(16))));
|
|
2777
|
+
return v;
|
|
2778
|
+
});
|
|
2779
|
+
const hashKey = Fn(([space, level, x, y]) => {
|
|
2780
|
+
const s = uint(space).bitAnd(uint(255));
|
|
2781
|
+
const l = uint(level).bitAnd(uint(255));
|
|
2782
|
+
const h = s.bitXor(l.shiftLeft(uint(8))).bitXor(mix32(uint(x))).bitXor(mix32(uint(y)));
|
|
2783
|
+
return mix32(h);
|
|
2784
|
+
});
|
|
2785
|
+
const createGpuSpatialLookup = (spatialIndex) => {
|
|
2786
|
+
const slotCount = spatialIndex.size;
|
|
2787
|
+
const mask = uint(spatialIndex.mask);
|
|
2788
|
+
const stampGen = spatialIndex.stampGen.toUint();
|
|
2789
|
+
const emptyValue = int(-1);
|
|
2790
|
+
return Fn(([space, level, x, y]) => {
|
|
2791
|
+
const s = uint(space).bitAnd(uint(255));
|
|
2792
|
+
const l = uint(level).bitAnd(uint(255));
|
|
2793
|
+
const xx = uint(x);
|
|
2794
|
+
const yy = uint(y);
|
|
2795
|
+
const result = emptyValue.toVar();
|
|
2796
|
+
const slot = hashKey(s, l, xx, yy).bitAnd(mask).toVar();
|
|
2797
|
+
const probes = int(0).toVar();
|
|
2798
|
+
Loop(slotCount, () => {
|
|
2799
|
+
const stamp = readGpuSpatialIndexValue(spatialIndex, slot, 0);
|
|
2800
|
+
If(stamp.notEqual(stampGen), () => {
|
|
2801
|
+
Break();
|
|
2802
|
+
});
|
|
2803
|
+
const ks = readGpuSpatialIndexValue(spatialIndex, slot, 1);
|
|
2804
|
+
const kl = readGpuSpatialIndexValue(spatialIndex, slot, 2);
|
|
2805
|
+
const kx = readGpuSpatialIndexValue(spatialIndex, slot, 3);
|
|
2806
|
+
const ky = readGpuSpatialIndexValue(spatialIndex, slot, 4);
|
|
2807
|
+
If(
|
|
2808
|
+
ks.equal(s).and(kl.equal(l)).and(kx.equal(xx)).and(ky.equal(yy)),
|
|
2809
|
+
() => {
|
|
2810
|
+
result.assign(int(readGpuSpatialIndexValue(spatialIndex, slot, 5)));
|
|
2811
|
+
Break();
|
|
2812
|
+
}
|
|
2813
|
+
);
|
|
2814
|
+
slot.assign(slot.add(uint(1)).bitAnd(mask));
|
|
2815
|
+
probes.addAssign(1);
|
|
2816
|
+
});
|
|
2817
|
+
return result;
|
|
2818
|
+
});
|
|
2819
|
+
};
|
|
2820
|
+
const createTileIndexFromWorldPosition = (spatialIndex, uniforms, maxLevel) => {
|
|
2821
|
+
const lookup = createGpuSpatialLookup(spatialIndex);
|
|
2822
|
+
const levelCount = Math.max(1, maxLevel + 1);
|
|
2823
|
+
return Fn(([worldX, worldZ]) => {
|
|
2824
|
+
const rootOrigin = uniforms.uRootOrigin.toVar();
|
|
2825
|
+
const rootSize = uniforms.uRootSize.toVar();
|
|
2826
|
+
const halfRoot = rootSize.mul(float(0.5));
|
|
2827
|
+
const tileIndex = int(-1).toVar();
|
|
2828
|
+
const tileU = float(0).toVar();
|
|
2829
|
+
const tileV = float(0).toVar();
|
|
2830
|
+
const i = int(0).toVar();
|
|
2831
|
+
Loop(levelCount, () => {
|
|
2832
|
+
const level = int(maxLevel).sub(i).toVar();
|
|
2833
|
+
const scale = pow(float(2), level.toFloat());
|
|
2834
|
+
const tileSize = rootSize.div(scale);
|
|
2835
|
+
const tileX = worldX.sub(rootOrigin.x).add(halfRoot).div(tileSize).floor().toInt();
|
|
2836
|
+
const tileY = worldZ.sub(rootOrigin.z).add(halfRoot).div(tileSize).floor().toInt();
|
|
2837
|
+
const maybeIndex = lookup(int(0), level, tileX, tileY).toVar();
|
|
2838
|
+
If(maybeIndex.greaterThanEqual(int(0)), () => {
|
|
2839
|
+
const minX = rootOrigin.x.add(tileX.toFloat().mul(tileSize)).sub(halfRoot);
|
|
2840
|
+
const minZ = rootOrigin.z.add(tileY.toFloat().mul(tileSize)).sub(halfRoot);
|
|
2841
|
+
tileIndex.assign(maybeIndex);
|
|
2842
|
+
tileU.assign(worldX.sub(minX).div(tileSize));
|
|
2843
|
+
tileV.assign(worldZ.sub(minZ).div(tileSize));
|
|
2844
|
+
Break();
|
|
2845
|
+
});
|
|
2846
|
+
i.addAssign(1);
|
|
2847
|
+
});
|
|
2848
|
+
return vec3(tileIndex.toFloat(), tileU, tileV);
|
|
2849
|
+
});
|
|
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
|
+
};
|
|
2883
|
+
|
|
2884
|
+
const gpuSpatialIndexStorageTask = task((get, work) => {
|
|
2885
|
+
const maxNodesValue = get(maxNodes);
|
|
2886
|
+
return work(() => createGpuSpatialIndex(maxNodesValue));
|
|
2887
|
+
}).displayName("gpuSpatialIndexStorageTask");
|
|
2888
|
+
const gpuSpatialIndexUploadTask = task((get, work) => {
|
|
2889
|
+
const quadtreeConfig = get(quadtreeConfigTask);
|
|
2890
|
+
get(quadtreeUpdateTask);
|
|
2891
|
+
const gpuSpatialIndex = get(gpuSpatialIndexStorageTask);
|
|
2892
|
+
return work(() => {
|
|
2893
|
+
uploadGpuSpatialIndex(gpuSpatialIndex, quadtreeConfig.state.leafIndex);
|
|
2894
|
+
return gpuSpatialIndex;
|
|
2895
|
+
});
|
|
2896
|
+
}).displayName("gpuSpatialIndexUploadTask");
|
|
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
|
+
}
|
|
2920
|
+
function createTerrainSampleNode(params) {
|
|
2921
|
+
const tileLookup = createTileIndexFromWorldPosition(
|
|
2922
|
+
params.spatialIndex,
|
|
2923
|
+
params.uniforms,
|
|
2924
|
+
params.maxLevel
|
|
2925
|
+
);
|
|
2926
|
+
return Fn(([worldX, worldZ]) => {
|
|
2927
|
+
const tileResult = tileLookup(worldX, worldZ).toVar();
|
|
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);
|
|
2936
|
+
});
|
|
2937
|
+
}
|
|
2938
|
+
function createTerrainSampler(params) {
|
|
2939
|
+
const elevationNode = createElevationFunction(params.elevationCallback);
|
|
2940
|
+
const terrainSampleAt = createTerrainSampleNode(params);
|
|
2941
|
+
const evaluateElevationAt = Fn(([worldX, worldZ]) => {
|
|
2942
|
+
const rootOrigin = params.uniforms.uRootOrigin.toVar();
|
|
2943
|
+
const rootSize = params.uniforms.uRootSize.toVar();
|
|
2944
|
+
const centeredX = worldX.sub(rootOrigin.x);
|
|
2945
|
+
const centeredZ = worldZ.sub(rootOrigin.z);
|
|
2946
|
+
const rootUV = vec2(
|
|
2947
|
+
centeredX.div(rootSize).add(0.5),
|
|
2948
|
+
centeredZ.div(rootSize).mul(float(-1)).add(0.5)
|
|
2949
|
+
).toVar();
|
|
2950
|
+
return elevationNode({
|
|
2951
|
+
worldPosition: vec3(worldX, rootOrigin.y, worldZ),
|
|
2952
|
+
rootSize,
|
|
2953
|
+
rootUV,
|
|
2954
|
+
tileUV: rootUV,
|
|
2955
|
+
tileLevel: int(0),
|
|
2956
|
+
tileSize: rootSize,
|
|
2957
|
+
tileOriginVec2: vec2(0, 0),
|
|
2958
|
+
nodeIndex: int(0)
|
|
2959
|
+
});
|
|
2960
|
+
});
|
|
2961
|
+
const sampleTerrain = Fn(
|
|
2962
|
+
([worldX, worldZ]) => terrainSampleAt(worldX, worldZ)
|
|
2963
|
+
);
|
|
2964
|
+
const sampleElevation = Fn(
|
|
2965
|
+
([worldX, worldZ]) => terrainSampleAt(worldX, worldZ).x
|
|
2966
|
+
);
|
|
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
|
+
});
|
|
2975
|
+
const evaluateElevation = Fn(
|
|
2976
|
+
([worldX, worldZ]) => evaluateElevationAt(worldX, worldZ)
|
|
2977
|
+
);
|
|
2978
|
+
const evaluateNormalNode = Fn(
|
|
2979
|
+
([worldX, worldZ, epsilon]) => {
|
|
2980
|
+
const eps = epsilon ?? float(0.1);
|
|
2981
|
+
const elevationScale = params.uniforms.uElevationScale.toVar();
|
|
2982
|
+
const hL = evaluateElevationAt(worldX.sub(eps), worldZ).mul(
|
|
2983
|
+
elevationScale
|
|
2984
|
+
);
|
|
2985
|
+
const hR = evaluateElevationAt(worldX.add(eps), worldZ).mul(
|
|
2986
|
+
elevationScale
|
|
2987
|
+
);
|
|
2988
|
+
const hD = evaluateElevationAt(worldX, worldZ.sub(eps)).mul(
|
|
2989
|
+
elevationScale
|
|
2990
|
+
);
|
|
2991
|
+
const hU = evaluateElevationAt(worldX, worldZ.add(eps)).mul(
|
|
2992
|
+
elevationScale
|
|
2993
|
+
);
|
|
2994
|
+
const inv2eps = float(0.5).div(eps);
|
|
2995
|
+
const dhdx = hR.sub(hL).mul(inv2eps);
|
|
2996
|
+
const dhdz = hU.sub(hD).mul(inv2eps);
|
|
2997
|
+
return vec3(dhdx.negate(), float(1), dhdz.negate()).normalize();
|
|
2998
|
+
}
|
|
2999
|
+
);
|
|
3000
|
+
const evaluateNormal = (worldX, worldZ, epsilon) => evaluateNormalNode(worldX, worldZ, epsilon ?? float(0.1));
|
|
3001
|
+
const sampler = {
|
|
3002
|
+
sampleElevation,
|
|
3003
|
+
sampleNormal,
|
|
3004
|
+
sampleTerrain,
|
|
3005
|
+
sampleValidity,
|
|
3006
|
+
evaluateElevation,
|
|
3007
|
+
evaluateNormal
|
|
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;
|
|
3029
|
+
}
|
|
3030
|
+
|
|
3031
|
+
const createTerrainSamplerTask = task((get, work) => {
|
|
3032
|
+
const terrainFieldStorage = get(createTerrainFieldTextureTask);
|
|
3033
|
+
const spatialIndex = get(gpuSpatialIndexStorageTask);
|
|
3034
|
+
const uniforms = get(updateUniformsTask);
|
|
3035
|
+
const elevationCallback = get(elevationFn);
|
|
3036
|
+
const maxLevelValue = get(maxLevel);
|
|
3037
|
+
const projection = get(topologyTask).projection ?? "flat";
|
|
3038
|
+
return work(
|
|
3039
|
+
() => createTerrainSampler({
|
|
3040
|
+
terrainFieldStorage,
|
|
3041
|
+
spatialIndex,
|
|
3042
|
+
uniforms,
|
|
3043
|
+
elevationCallback,
|
|
3044
|
+
maxLevel: maxLevelValue,
|
|
3045
|
+
projection
|
|
3046
|
+
})
|
|
3047
|
+
);
|
|
3048
|
+
}).displayName("createTerrainSamplerTask");
|
|
3049
|
+
|
|
216
3050
|
const isSkirtVertex = Fn(([segments]) => {
|
|
217
3051
|
const segmentsNode = typeof segments === "number" ? int(segments) : segments;
|
|
218
3052
|
const vIndex = int(vertexIndex);
|
|
@@ -233,4 +3067,542 @@ const isSkirtUV = Fn(([segments]) => {
|
|
|
233
3067
|
return innerX.and(innerY).not();
|
|
234
3068
|
});
|
|
235
3069
|
|
|
236
|
-
|
|
3070
|
+
function createTileBaseWorldPosition(leafStorage, terrainUniforms) {
|
|
3071
|
+
return Fn(() => {
|
|
3072
|
+
const tile = decodeLeafTile(leafStorage, int(instanceIndex));
|
|
3073
|
+
const rootSize = terrainUniforms.uRootSize.toVar();
|
|
3074
|
+
const rootOrigin = terrainUniforms.uRootOrigin.toVar();
|
|
3075
|
+
const half = float(0.5);
|
|
3076
|
+
const size = rootSize.div(pow(float(2), tile.level.toFloat()));
|
|
3077
|
+
const halfRoot = rootSize.mul(half);
|
|
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);
|
|
3080
|
+
const clampedX = positionLocal.x.max(half.negate()).min(half);
|
|
3081
|
+
const clampedZ = positionLocal.z.max(half.negate()).min(half);
|
|
3082
|
+
const worldX = centerX.add(clampedX.mul(size));
|
|
3083
|
+
const worldZ = centerZ.add(clampedZ.mul(size));
|
|
3084
|
+
return vec3(worldX, rootOrigin.y, worldZ);
|
|
3085
|
+
});
|
|
3086
|
+
}
|
|
3087
|
+
function createTileElevation(terrainUniforms, terrainFieldStorage) {
|
|
3088
|
+
if (!terrainFieldStorage) return float(0);
|
|
3089
|
+
const innerSegs = terrainUniforms.uInnerTileSegments;
|
|
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") {
|
|
3097
|
+
if (!terrainFieldStorage) return;
|
|
3098
|
+
normalLocal.assign(
|
|
3099
|
+
createTileLocalNormal(leafStorage, terrainUniforms, terrainFieldStorage, projection)
|
|
3100
|
+
);
|
|
3101
|
+
}
|
|
3102
|
+
function loadTangentNormal(terrainUniforms, terrainFieldStorage) {
|
|
3103
|
+
const nodeIndex = int(instanceIndex);
|
|
3104
|
+
const edgeVertexCount = int(terrainUniforms.uInnerTileSegments.add(3));
|
|
3105
|
+
const localVertexIndex = int(vertexIndex);
|
|
3106
|
+
const ix = localVertexIndex.mod(edgeVertexCount);
|
|
3107
|
+
const iy = localVertexIndex.div(edgeVertexCount);
|
|
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);
|
|
3155
|
+
return Fn(() => {
|
|
3156
|
+
const base = baseWorldPosition();
|
|
3157
|
+
const yElevation = createTileElevation(terrainUniforms, terrainFieldStorage);
|
|
3158
|
+
const skirtVertex = isSkirtVertex(terrainUniforms.uInnerTileSegments);
|
|
3159
|
+
const skirtY = base.y.add(yElevation).sub(terrainUniforms.uSkirtScale.toVar());
|
|
3160
|
+
const worldY = select(skirtVertex, skirtY, base.y.add(yElevation));
|
|
3161
|
+
createNormalAssignment(leafStorage, terrainUniforms, terrainFieldStorage, "flat");
|
|
3162
|
+
return vec3(base.x, worldY, base.z);
|
|
3163
|
+
})();
|
|
3164
|
+
}
|
|
3165
|
+
|
|
3166
|
+
const positionNodeTask = task((get, work) => {
|
|
3167
|
+
const leafStorage = get(leafStorageTask);
|
|
3168
|
+
const terrainUniforms = get(updateUniformsTask);
|
|
3169
|
+
const terrainFieldStorage = get(createTerrainFieldTextureTask);
|
|
3170
|
+
const topology = get(topologyTask);
|
|
3171
|
+
return work(
|
|
3172
|
+
() => createTileWorldPosition(
|
|
3173
|
+
leafStorage,
|
|
3174
|
+
terrainUniforms,
|
|
3175
|
+
terrainFieldStorage,
|
|
3176
|
+
topology.projection ?? "flat"
|
|
3177
|
+
)
|
|
3178
|
+
);
|
|
3179
|
+
}).displayName("positionNodeTask");
|
|
3180
|
+
|
|
3181
|
+
function intersectRayAabb(ray, minX, minY, minZ, maxX, maxY, maxZ) {
|
|
3182
|
+
let tMin = -Infinity;
|
|
3183
|
+
let tMax = Infinity;
|
|
3184
|
+
const origin = ray.origin;
|
|
3185
|
+
const dir = ray.direction;
|
|
3186
|
+
const slab = (originAxis, dirAxis, minAxis, maxAxis) => {
|
|
3187
|
+
if (Math.abs(dirAxis) < 1e-8) {
|
|
3188
|
+
if (originAxis < minAxis || originAxis > maxAxis) return false;
|
|
3189
|
+
return true;
|
|
3190
|
+
}
|
|
3191
|
+
const inv = 1 / dirAxis;
|
|
3192
|
+
let t0 = (minAxis - originAxis) * inv;
|
|
3193
|
+
let t1 = (maxAxis - originAxis) * inv;
|
|
3194
|
+
if (t0 > t1) {
|
|
3195
|
+
const tmp = t0;
|
|
3196
|
+
t0 = t1;
|
|
3197
|
+
t1 = tmp;
|
|
3198
|
+
}
|
|
3199
|
+
tMin = Math.max(tMin, t0);
|
|
3200
|
+
tMax = Math.min(tMax, t1);
|
|
3201
|
+
return tMax >= tMin;
|
|
3202
|
+
};
|
|
3203
|
+
if (!slab(origin.x, dir.x, minX, maxX) || !slab(origin.y, dir.y, minY, maxY) || !slab(origin.z, dir.z, minZ, maxZ)) {
|
|
3204
|
+
return null;
|
|
3205
|
+
}
|
|
3206
|
+
return { tMin, tMax };
|
|
3207
|
+
}
|
|
3208
|
+
function getTerrainBounds(config) {
|
|
3209
|
+
const halfRoot = config.rootSize * 0.5;
|
|
3210
|
+
return {
|
|
3211
|
+
minX: config.originX - halfRoot,
|
|
3212
|
+
maxX: config.originX + halfRoot,
|
|
3213
|
+
minZ: config.originZ - halfRoot,
|
|
3214
|
+
maxZ: config.originZ + halfRoot
|
|
3215
|
+
};
|
|
3216
|
+
}
|
|
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
|
+
}
|
|
3227
|
+
}
|
|
3228
|
+
}
|
|
3229
|
+
const elevation = query.getElevation(worldX, worldZ);
|
|
3230
|
+
if (!Number.isFinite(elevation)) return void 0;
|
|
3231
|
+
return worldY - elevation;
|
|
3232
|
+
}
|
|
3233
|
+
function marchSignedDistance(ray, startT, endT, stepSignedDistanceAt, refineSignedDistanceAt, options, point) {
|
|
3234
|
+
let prevT = startT;
|
|
3235
|
+
ray.at(prevT, point);
|
|
3236
|
+
let prevSignedDistance = stepSignedDistanceAt(point.x, point.y, point.z);
|
|
3237
|
+
if (prevSignedDistance !== void 0 && prevSignedDistance <= 0) {
|
|
3238
|
+
return startT;
|
|
3239
|
+
}
|
|
3240
|
+
for (let i = 1; i <= options.maxSteps; i += 1) {
|
|
3241
|
+
const t = startT + (endT - startT) * i / options.maxSteps;
|
|
3242
|
+
ray.at(t, point);
|
|
3243
|
+
const signedDistance = stepSignedDistanceAt(point.x, point.y, point.z);
|
|
3244
|
+
if (signedDistance === void 0) {
|
|
3245
|
+
prevSignedDistance = void 0;
|
|
3246
|
+
prevT = t;
|
|
3247
|
+
continue;
|
|
3248
|
+
}
|
|
3249
|
+
if (prevSignedDistance !== void 0 && prevSignedDistance > 0 && signedDistance <= 0) {
|
|
3250
|
+
let lo = prevT;
|
|
3251
|
+
let hi = t;
|
|
3252
|
+
for (let r = 0; r < options.refinementSteps; r += 1) {
|
|
3253
|
+
const mid = (lo + hi) * 0.5;
|
|
3254
|
+
ray.at(mid, point);
|
|
3255
|
+
const midDistance = refineSignedDistanceAt(point.x, point.y, point.z);
|
|
3256
|
+
if (midDistance === void 0) {
|
|
3257
|
+
lo = mid;
|
|
3258
|
+
continue;
|
|
3259
|
+
}
|
|
3260
|
+
if (midDistance > 0) lo = mid;
|
|
3261
|
+
else hi = mid;
|
|
3262
|
+
}
|
|
3263
|
+
return hi;
|
|
3264
|
+
}
|
|
3265
|
+
prevSignedDistance = signedDistance;
|
|
3266
|
+
prevT = t;
|
|
3267
|
+
}
|
|
3268
|
+
return null;
|
|
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
|
+
}
|
|
3310
|
+
function cpuRaycastBoundsOnly(ray, config, options) {
|
|
3311
|
+
const bounds = getTerrainBounds(config);
|
|
3312
|
+
const planeY = (config.minY + config.maxY) * 0.5;
|
|
3313
|
+
const dirY = ray.direction.y;
|
|
3314
|
+
if (Math.abs(dirY) < 1e-8) return null;
|
|
3315
|
+
const t = (planeY - ray.origin.y) / dirY;
|
|
3316
|
+
if (t < 0) return null;
|
|
3317
|
+
const maxDistance = options?.maxDistance ?? Number.POSITIVE_INFINITY;
|
|
3318
|
+
if (t > maxDistance) return null;
|
|
3319
|
+
const point = new Vector3();
|
|
3320
|
+
ray.at(t, point);
|
|
3321
|
+
if (point.x < bounds.minX || point.x > bounds.maxX || point.z < bounds.minZ || point.z > bounds.maxZ) {
|
|
3322
|
+
return null;
|
|
3323
|
+
}
|
|
3324
|
+
return {
|
|
3325
|
+
position: point,
|
|
3326
|
+
normal: new Vector3(0, 1, 0),
|
|
3327
|
+
distance: ray.origin.distanceTo(point)
|
|
3328
|
+
};
|
|
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
|
+
}
|
|
3412
|
+
|
|
3413
|
+
function createTerrainRaycast(params) {
|
|
3414
|
+
return {
|
|
3415
|
+
pick(ray, options) {
|
|
3416
|
+
const config = params.getConfig();
|
|
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
|
+
}
|
|
3426
|
+
if (terrainQuery) {
|
|
3427
|
+
const precise = cpuRaycast(terrainQuery, ray, config, options);
|
|
3428
|
+
if (precise) return precise;
|
|
3429
|
+
}
|
|
3430
|
+
const coarse = cpuRaycastBoundsOnly(ray, config, options);
|
|
3431
|
+
if (coarse && terrainQuery) {
|
|
3432
|
+
const sample = terrainQuery.sampleTerrain(
|
|
3433
|
+
coarse.position.x,
|
|
3434
|
+
coarse.position.z
|
|
3435
|
+
);
|
|
3436
|
+
if (sample.valid) {
|
|
3437
|
+
coarse.position.y = sample.elevation;
|
|
3438
|
+
coarse.normal.copy(sample.normal);
|
|
3439
|
+
}
|
|
3440
|
+
}
|
|
3441
|
+
return coarse;
|
|
3442
|
+
}
|
|
3443
|
+
};
|
|
3444
|
+
}
|
|
3445
|
+
|
|
3446
|
+
const BOUNDS_PADDING = 1;
|
|
3447
|
+
const RAYCAST_STATE = Symbol("terrainRaycastTaskState");
|
|
3448
|
+
const terrainRaycastTask = task(
|
|
3449
|
+
(get, work) => {
|
|
3450
|
+
const { query: terrainQuery, sphereQuery } = get(terrainQueryTask);
|
|
3451
|
+
const rootSizeValue = get(rootSize);
|
|
3452
|
+
const originValue = get(origin);
|
|
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;
|
|
3458
|
+
return work((prev) => {
|
|
3459
|
+
let raycast = prev;
|
|
3460
|
+
let state = raycast?.[RAYCAST_STATE];
|
|
3461
|
+
if (!state) {
|
|
3462
|
+
state = {
|
|
3463
|
+
terrainQuery: null,
|
|
3464
|
+
sphereQuery: null,
|
|
3465
|
+
bounds: {
|
|
3466
|
+
rootSize: 0,
|
|
3467
|
+
originX: 0,
|
|
3468
|
+
originZ: 0,
|
|
3469
|
+
minY: 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
|
|
3478
|
+
}
|
|
3479
|
+
};
|
|
3480
|
+
}
|
|
3481
|
+
state.terrainQuery = terrainQuery;
|
|
3482
|
+
state.sphereQuery = sphereQuery;
|
|
3483
|
+
state.bounds.rootSize = rootSizeValue;
|
|
3484
|
+
state.bounds.originX = originValue.x;
|
|
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;
|
|
3491
|
+
const range = terrainQuery.getGlobalElevationRange();
|
|
3492
|
+
if (range) {
|
|
3493
|
+
state.bounds.minY = range.min - BOUNDS_PADDING;
|
|
3494
|
+
state.bounds.maxY = range.max + BOUNDS_PADDING;
|
|
3495
|
+
} else {
|
|
3496
|
+
const verticalExtent = Math.max(1, Math.abs(elevationScaleValue) * 2);
|
|
3497
|
+
state.bounds.minY = originValue.y - verticalExtent;
|
|
3498
|
+
state.bounds.maxY = originValue.y + verticalExtent;
|
|
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;
|
|
3509
|
+
if (!raycast) {
|
|
3510
|
+
raycast = createTerrainRaycast({
|
|
3511
|
+
getTerrainQuery: () => state.terrainQuery,
|
|
3512
|
+
getSphereQuery: () => state.sphereQuery,
|
|
3513
|
+
getConfig: () => state.bounds
|
|
3514
|
+
});
|
|
3515
|
+
}
|
|
3516
|
+
raycast[RAYCAST_STATE] = state;
|
|
3517
|
+
return raycast;
|
|
3518
|
+
});
|
|
3519
|
+
}
|
|
3520
|
+
).displayName("terrainRaycastTask");
|
|
3521
|
+
|
|
3522
|
+
const terrainTasks = {
|
|
3523
|
+
instanceId: instanceIdTask,
|
|
3524
|
+
quadtreeConfig: quadtreeConfigTask,
|
|
3525
|
+
quadtreeUpdate: quadtreeUpdateTask,
|
|
3526
|
+
leafStorage: leafStorageTask,
|
|
3527
|
+
topology: topologyTask,
|
|
3528
|
+
leafGpuBuffer: leafGpuBufferTask,
|
|
3529
|
+
gpuSpatialIndexStorage: gpuSpatialIndexStorageTask,
|
|
3530
|
+
gpuSpatialIndexUpload: gpuSpatialIndexUploadTask,
|
|
3531
|
+
createUniforms: createUniformsTask,
|
|
3532
|
+
updateUniforms: updateUniformsTask,
|
|
3533
|
+
positionNode: positionNodeTask,
|
|
3534
|
+
createElevationFieldContext: createElevationFieldContextTask,
|
|
3535
|
+
createTileNodes: tileNodesTask,
|
|
3536
|
+
createTerrainFieldTexture: createTerrainFieldTextureTask,
|
|
3537
|
+
createTerrainSampler: createTerrainSamplerTask,
|
|
3538
|
+
elevationFieldStage: elevationFieldStageTask,
|
|
3539
|
+
terrainFieldStage: terrainFieldStageTask,
|
|
3540
|
+
compileCompute: compileComputeTask,
|
|
3541
|
+
executeCompute: executeComputeTask,
|
|
3542
|
+
tileBoundsContext: tileBoundsContextTask,
|
|
3543
|
+
tileBoundsReduction: tileBoundsReductionTask,
|
|
3544
|
+
terrainQuery: terrainQueryTask,
|
|
3545
|
+
terrainReadback: terrainReadbackTask,
|
|
3546
|
+
terrainRaycast: terrainRaycastTask
|
|
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
|
+
}
|
|
3555
|
+
|
|
3556
|
+
const textureSpaceToVectorSpace = Fn(([value]) => {
|
|
3557
|
+
return remap(value, float(0), float(1), float(-1), float(1));
|
|
3558
|
+
});
|
|
3559
|
+
const vectorSpaceToTextureSpace = Fn(([value]) => {
|
|
3560
|
+
return remap(value, float(-1), float(1), float(0), float(1));
|
|
3561
|
+
});
|
|
3562
|
+
const blendAngleCorrectedNormals = Fn(([n1, n2]) => {
|
|
3563
|
+
const t = vec3(n1.x, n1.y, n1.z.add(1));
|
|
3564
|
+
const u = vec3(n2.x.negate(), n2.y.negate(), n2.z);
|
|
3565
|
+
const r = t.mul(dot$1(t, u)).sub(u.mul(t.z)).normalize();
|
|
3566
|
+
return r;
|
|
3567
|
+
});
|
|
3568
|
+
const deriveNormalZ = Fn(([normalXY]) => {
|
|
3569
|
+
const xy = normalXY.toVar();
|
|
3570
|
+
const z = xy.x.mul(xy.x).add(xy.y.mul(xy.y)).oneMinus().max(0).sqrt();
|
|
3571
|
+
return vec3(xy.x, xy.y, z);
|
|
3572
|
+
});
|
|
3573
|
+
|
|
3574
|
+
const vGlobalVertexIndex = /* @__PURE__ */ varyingProperty("int", "vGlobalVertexIndex");
|
|
3575
|
+
const vElevation = /* @__PURE__ */ varyingProperty("f32", "vElevation");
|
|
3576
|
+
|
|
3577
|
+
const cellCenter = Fn(({ cell }) => {
|
|
3578
|
+
return cell.add(mx_noise_float(cell.mul(Math.PI)));
|
|
3579
|
+
});
|
|
3580
|
+
const voronoiCells = Fn((params) => {
|
|
3581
|
+
const scale = float(params.scale);
|
|
3582
|
+
const facet = float(params.facet);
|
|
3583
|
+
const seed = float(params.seed);
|
|
3584
|
+
const pos = params.uv.mul(scale).add(seed);
|
|
3585
|
+
const midCell = pos.round().toVar();
|
|
3586
|
+
const minCell = midCell.toVar();
|
|
3587
|
+
const minDist = float(1).toVar();
|
|
3588
|
+
const cell = vec3(0, 0, 0).toVar();
|
|
3589
|
+
const dist = float().toVar();
|
|
3590
|
+
const i = float(0).toVar();
|
|
3591
|
+
Loop(27, () => {
|
|
3592
|
+
const ix = i.mod(3).sub(1);
|
|
3593
|
+
const iy = i.div(3).floor().mod(3).sub(1);
|
|
3594
|
+
const iz = i.div(9).floor().sub(1);
|
|
3595
|
+
cell.assign(midCell.add(vec3(ix, iy, iz)));
|
|
3596
|
+
dist.assign(pos.distance(cellCenter({ cell })).add(mx_noise_float(pos).div(5)));
|
|
3597
|
+
If(dist.lessThan(minDist), () => {
|
|
3598
|
+
minDist.assign(dist);
|
|
3599
|
+
minCell.assign(cell);
|
|
3600
|
+
});
|
|
3601
|
+
i.addAssign(1);
|
|
3602
|
+
});
|
|
3603
|
+
const n = mx_noise_float(minCell.mul(Math.PI)).toVar();
|
|
3604
|
+
const k = mix(minDist, n.add(1).div(2), facet);
|
|
3605
|
+
return k;
|
|
3606
|
+
});
|
|
3607
|
+
|
|
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 };
|