@hello-terrain/three 0.0.0-alpha.1 → 0.0.0-alpha.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +179 -23
- package/dist/index.cjs +2687 -8
- package/dist/index.d.cts +814 -10
- package/dist/index.d.mts +814 -10
- package/dist/index.d.ts +814 -10
- package/dist/index.mjs +2614 -11
- package/package.json +12 -2
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
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 { uniform, Fn, float, globalId, int, vec2, uint, If, workgroupBarrier, textureStore, uvec3, vec4, texture, ivec2, ivec3, textureLoad, pow, vec3, storage, workgroupArray, localId, workgroupId, min, Loop, max, Break, vertexIndex, uv, select, instanceIndex, positionLocal, normalLocal, remap, dot, 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
|
constructor(innerSegments = 14, extendUV = false) {
|
|
@@ -59,17 +62,19 @@ class TerrainGeometry extends BufferGeometry {
|
|
|
59
62
|
* | / | \ | / | \ |
|
|
60
63
|
* o---o---o---o---o
|
|
61
64
|
*
|
|
62
|
-
* INNER GRID (
|
|
63
|
-
* o---o---o
|
|
64
|
-
* | \ | \ |
|
|
65
|
-
* o---o---o
|
|
66
|
-
* | \ | \ |
|
|
67
|
-
* o---o---o
|
|
65
|
+
* INNER GRID (alternating diagonals — checkerboard pattern):
|
|
66
|
+
* o---o---o---o---o
|
|
67
|
+
* | \ | / | \ | / |
|
|
68
|
+
* o---o---o---o---o
|
|
69
|
+
* | / | \ | / | \ |
|
|
70
|
+
* o---o---o---o---o
|
|
71
|
+
* | \ | / | \ | / |
|
|
72
|
+
* o---o---o---o---o
|
|
68
73
|
*
|
|
69
74
|
* Where o = vertex
|
|
70
75
|
* Each square cell is split into 2 triangles.
|
|
71
76
|
* - Skirt cells (outer ring): diagonal flip based on quadrant for corner correctness
|
|
72
|
-
* - Inner cells:
|
|
77
|
+
* - Inner cells: alternating diagonal via (x+y)%2 to reduce interpolation artifacts
|
|
73
78
|
*
|
|
74
79
|
* Vertex layout (for innerSegments = 2):
|
|
75
80
|
*
|
|
@@ -115,7 +120,7 @@ class TerrainGeometry extends BufferGeometry {
|
|
|
115
120
|
const topHalf = y < mid;
|
|
116
121
|
useDefaultDiagonal = leftHalf && topHalf || !leftHalf && !topHalf;
|
|
117
122
|
} else {
|
|
118
|
-
useDefaultDiagonal =
|
|
123
|
+
useDefaultDiagonal = (x + y) % 2 === 0;
|
|
119
124
|
}
|
|
120
125
|
if (useDefaultDiagonal) {
|
|
121
126
|
indices.push(a, d, b);
|
|
@@ -213,6 +218,2203 @@ class TerrainGeometry extends BufferGeometry {
|
|
|
213
218
|
}
|
|
214
219
|
}
|
|
215
220
|
|
|
221
|
+
const defaultTerrainMeshParams = {
|
|
222
|
+
innerTileSegments: 14,
|
|
223
|
+
maxNodes: 1024,
|
|
224
|
+
material: new MeshStandardNodeMaterial()
|
|
225
|
+
};
|
|
226
|
+
class TerrainMesh extends InstancedMesh {
|
|
227
|
+
_innerTileSegments;
|
|
228
|
+
_maxNodes;
|
|
229
|
+
terrainRaycast = null;
|
|
230
|
+
constructor(params = defaultTerrainMeshParams) {
|
|
231
|
+
const mergedParams = { ...defaultTerrainMeshParams, ...params };
|
|
232
|
+
const { innerTileSegments, maxNodes, material } = mergedParams;
|
|
233
|
+
const geometry = new TerrainGeometry(innerTileSegments, true);
|
|
234
|
+
super(geometry, material, maxNodes);
|
|
235
|
+
this.frustumCulled = false;
|
|
236
|
+
this._innerTileSegments = innerTileSegments;
|
|
237
|
+
this._maxNodes = maxNodes;
|
|
238
|
+
}
|
|
239
|
+
get innerTileSegments() {
|
|
240
|
+
return this._innerTileSegments;
|
|
241
|
+
}
|
|
242
|
+
set innerTileSegments(tileSegments) {
|
|
243
|
+
const oldGeometry = this.geometry;
|
|
244
|
+
this.geometry = new TerrainGeometry(tileSegments, true);
|
|
245
|
+
this._innerTileSegments = tileSegments;
|
|
246
|
+
setTimeout(oldGeometry.dispose);
|
|
247
|
+
}
|
|
248
|
+
get maxNodes() {
|
|
249
|
+
return this._maxNodes;
|
|
250
|
+
}
|
|
251
|
+
set maxNodes(maxNodes) {
|
|
252
|
+
if (!Number.isInteger(maxNodes) || maxNodes < 1) {
|
|
253
|
+
throw new Error(`Invalid maxNodes: ${maxNodes}. Must be a positive integer.`);
|
|
254
|
+
}
|
|
255
|
+
if (maxNodes === this._maxNodes) return;
|
|
256
|
+
const oldMax = this._maxNodes;
|
|
257
|
+
const nextMatrix = new Float32Array(maxNodes * 16);
|
|
258
|
+
const oldMatrixArray = this.instanceMatrix.array;
|
|
259
|
+
nextMatrix.set(oldMatrixArray.subarray(0, Math.min(oldMatrixArray.length, nextMatrix.length)));
|
|
260
|
+
this.instanceMatrix = new InstancedBufferAttribute(nextMatrix, 16);
|
|
261
|
+
if (this.instanceColor) {
|
|
262
|
+
const itemSize = this.instanceColor.itemSize;
|
|
263
|
+
const nextColor = new Float32Array(maxNodes * itemSize);
|
|
264
|
+
const oldColorArray = this.instanceColor.array;
|
|
265
|
+
nextColor.set(oldColorArray.subarray(0, Math.min(oldColorArray.length, nextColor.length)));
|
|
266
|
+
this.instanceColor = new InstancedBufferAttribute(nextColor, itemSize);
|
|
267
|
+
}
|
|
268
|
+
this._maxNodes = maxNodes;
|
|
269
|
+
this.count = Math.min(this.count, maxNodes);
|
|
270
|
+
this.instanceMatrix.needsUpdate = true;
|
|
271
|
+
if (this.instanceColor) this.instanceColor.needsUpdate = true;
|
|
272
|
+
if (maxNodes < oldMax && this.count >= maxNodes) {
|
|
273
|
+
this.count = maxNodes;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
raycast(raycaster, intersects) {
|
|
277
|
+
if (!this.terrainRaycast) {
|
|
278
|
+
super.raycast(raycaster, intersects);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
const result = this.terrainRaycast.pick(raycaster.ray);
|
|
282
|
+
if (!result) return;
|
|
283
|
+
intersects.push({
|
|
284
|
+
distance: result.distance,
|
|
285
|
+
point: result.position.clone(),
|
|
286
|
+
normal: result.normal.clone(),
|
|
287
|
+
object: this,
|
|
288
|
+
face: null,
|
|
289
|
+
faceIndex: -1
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function getDeviceComputeLimits(renderer) {
|
|
295
|
+
const backend = renderer.backend;
|
|
296
|
+
const limits = backend?.device?.limits;
|
|
297
|
+
return {
|
|
298
|
+
maxWorkgroupSizeX: limits?.maxComputeWorkgroupSizeX ?? 256,
|
|
299
|
+
maxWorkgroupSizeY: limits?.maxComputeWorkgroupSizeY ?? 256,
|
|
300
|
+
maxWorkgroupInvocations: limits?.maxComputeWorkgroupInvocations ?? 256
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const WORKGROUP_X = 16;
|
|
305
|
+
const WORKGROUP_Y = 16;
|
|
306
|
+
function compileComputePipeline(stages, width, options) {
|
|
307
|
+
const bindings = options?.bindings;
|
|
308
|
+
const preferredWorkgroup = options?.workgroupSize ?? [
|
|
309
|
+
WORKGROUP_X,
|
|
310
|
+
WORKGROUP_Y
|
|
311
|
+
];
|
|
312
|
+
const preferSingleKernelWhenPossible = options?.preferSingleKernelWhenPossible ?? true;
|
|
313
|
+
const uInstanceCount = uniform(0, "uint");
|
|
314
|
+
let singleKernel;
|
|
315
|
+
const stagedKernelCache = /* @__PURE__ */ new Map();
|
|
316
|
+
function canRunSingleKernel(widthValue, limits) {
|
|
317
|
+
return widthValue <= limits.maxWorkgroupSizeX && widthValue <= limits.maxWorkgroupSizeY && widthValue * widthValue <= limits.maxWorkgroupInvocations;
|
|
318
|
+
}
|
|
319
|
+
function clampWorkgroupToLimits(requested, limits) {
|
|
320
|
+
let x = Math.max(1, Math.floor(requested[0]));
|
|
321
|
+
let y = Math.max(1, Math.floor(requested[1]));
|
|
322
|
+
x = Math.min(x, limits.maxWorkgroupSizeX);
|
|
323
|
+
y = Math.min(y, limits.maxWorkgroupSizeY);
|
|
324
|
+
y = Math.min(
|
|
325
|
+
y,
|
|
326
|
+
Math.max(1, Math.floor(limits.maxWorkgroupInvocations / x))
|
|
327
|
+
);
|
|
328
|
+
x = Math.min(
|
|
329
|
+
x,
|
|
330
|
+
Math.max(1, Math.floor(limits.maxWorkgroupInvocations / y))
|
|
331
|
+
);
|
|
332
|
+
return [x, y];
|
|
333
|
+
}
|
|
334
|
+
function buildSingleKernel(workgroupSize) {
|
|
335
|
+
return Fn(() => {
|
|
336
|
+
bindings?.forEach((b) => b.toVar());
|
|
337
|
+
const fWidth = float(width);
|
|
338
|
+
const activeIndex = globalId.z;
|
|
339
|
+
const nodeIndex = int(activeIndex).toVar();
|
|
340
|
+
const iWidth = int(width);
|
|
341
|
+
const ix = int(globalId.x);
|
|
342
|
+
const iy = int(globalId.y);
|
|
343
|
+
const texelSize = vec2(1, 1).div(fWidth);
|
|
344
|
+
const localCoordinates = vec2(globalId.x, globalId.y);
|
|
345
|
+
const localUVCoords = localCoordinates.div(fWidth);
|
|
346
|
+
const verticesPerNode = iWidth.mul(iWidth);
|
|
347
|
+
const globalIndex = int(nodeIndex).mul(verticesPerNode).add(iy.mul(iWidth).add(ix));
|
|
348
|
+
const inBounds = ix.lessThan(iWidth).and(iy.lessThan(iWidth)).and(uint(activeIndex).lessThan(uInstanceCount)).toVar();
|
|
349
|
+
for (let i = 0; i < stages.length; i++) {
|
|
350
|
+
if (i > 0) {
|
|
351
|
+
workgroupBarrier();
|
|
352
|
+
}
|
|
353
|
+
If(inBounds, () => {
|
|
354
|
+
stages[i](
|
|
355
|
+
nodeIndex,
|
|
356
|
+
globalIndex,
|
|
357
|
+
localUVCoords,
|
|
358
|
+
localCoordinates,
|
|
359
|
+
texelSize
|
|
360
|
+
);
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
})().computeKernel(workgroupSize);
|
|
364
|
+
}
|
|
365
|
+
function buildStagedKernels(workgroupSize) {
|
|
366
|
+
return stages.map(
|
|
367
|
+
(stage) => Fn(() => {
|
|
368
|
+
bindings?.forEach((b) => b.toVar());
|
|
369
|
+
const fWidth = float(width);
|
|
370
|
+
const activeIndex = globalId.z;
|
|
371
|
+
const nodeIndex = int(activeIndex).toVar();
|
|
372
|
+
const iWidth = int(width);
|
|
373
|
+
const ix = int(globalId.x);
|
|
374
|
+
const iy = int(globalId.y);
|
|
375
|
+
const texelSize = vec2(1, 1).div(fWidth);
|
|
376
|
+
const localCoordinates = vec2(globalId.x, globalId.y);
|
|
377
|
+
const localUVCoords = localCoordinates.div(fWidth);
|
|
378
|
+
const verticesPerNode = iWidth.mul(iWidth);
|
|
379
|
+
const globalIndex = int(nodeIndex).mul(verticesPerNode).add(iy.mul(iWidth).add(ix));
|
|
380
|
+
const inBounds = ix.lessThan(iWidth).and(iy.lessThan(iWidth)).and(uint(activeIndex).lessThan(uInstanceCount)).toVar();
|
|
381
|
+
If(inBounds, () => {
|
|
382
|
+
stage(
|
|
383
|
+
nodeIndex,
|
|
384
|
+
globalIndex,
|
|
385
|
+
localUVCoords,
|
|
386
|
+
localCoordinates,
|
|
387
|
+
texelSize
|
|
388
|
+
);
|
|
389
|
+
});
|
|
390
|
+
})().computeKernel(workgroupSize)
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
function execute(renderer, instanceCount) {
|
|
394
|
+
const limits = getDeviceComputeLimits(renderer);
|
|
395
|
+
const canUseSingleKernel = preferSingleKernelWhenPossible && canRunSingleKernel(width, limits);
|
|
396
|
+
uInstanceCount.value = instanceCount;
|
|
397
|
+
if (canUseSingleKernel) {
|
|
398
|
+
if (!singleKernel) {
|
|
399
|
+
singleKernel = buildSingleKernel([width, width, 1]);
|
|
400
|
+
}
|
|
401
|
+
renderer.compute(singleKernel, [1, 1, instanceCount]);
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
const [workgroupX, workgroupY] = clampWorkgroupToLimits(
|
|
405
|
+
preferredWorkgroup,
|
|
406
|
+
limits
|
|
407
|
+
);
|
|
408
|
+
const cacheKey = `${workgroupX}x${workgroupY}`;
|
|
409
|
+
let stagedKernels = stagedKernelCache.get(cacheKey);
|
|
410
|
+
if (!stagedKernels) {
|
|
411
|
+
stagedKernels = buildStagedKernels([workgroupX, workgroupY, 1]);
|
|
412
|
+
stagedKernelCache.set(cacheKey, stagedKernels);
|
|
413
|
+
}
|
|
414
|
+
const dispatchX = Math.ceil(width / workgroupX);
|
|
415
|
+
const dispatchY = Math.ceil(width / workgroupY);
|
|
416
|
+
for (const kernel of stagedKernels) {
|
|
417
|
+
renderer.compute(kernel, [dispatchX, dispatchY, instanceCount]);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
return { execute };
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function resolveType(format) {
|
|
424
|
+
return format === "rgba16float" ? HalfFloatType : FloatType;
|
|
425
|
+
}
|
|
426
|
+
function resolveFilter(mode) {
|
|
427
|
+
return mode === "linear" ? LinearFilter : NearestFilter;
|
|
428
|
+
}
|
|
429
|
+
function configureStorageTexture(texture2, format, filter) {
|
|
430
|
+
texture2.format = RGBAFormat;
|
|
431
|
+
texture2.type = resolveType(format);
|
|
432
|
+
texture2.magFilter = resolveFilter(filter);
|
|
433
|
+
texture2.minFilter = resolveFilter(filter);
|
|
434
|
+
texture2.wrapS = ClampToEdgeWrapping;
|
|
435
|
+
texture2.wrapT = ClampToEdgeWrapping;
|
|
436
|
+
texture2.generateMipmaps = false;
|
|
437
|
+
texture2.needsUpdate = true;
|
|
438
|
+
}
|
|
439
|
+
function ArrayTextureBackend(edgeVertexCount, tileCount, options) {
|
|
440
|
+
let currentEdgeVertexCount = edgeVertexCount;
|
|
441
|
+
let currentTileCount = tileCount;
|
|
442
|
+
const tex = new StorageArrayTexture(
|
|
443
|
+
edgeVertexCount,
|
|
444
|
+
edgeVertexCount,
|
|
445
|
+
tileCount
|
|
446
|
+
);
|
|
447
|
+
configureStorageTexture(tex, options.format, options.filter);
|
|
448
|
+
return {
|
|
449
|
+
backendType: "array-texture",
|
|
450
|
+
get edgeVertexCount() {
|
|
451
|
+
return currentEdgeVertexCount;
|
|
452
|
+
},
|
|
453
|
+
get tileCount() {
|
|
454
|
+
return currentTileCount;
|
|
455
|
+
},
|
|
456
|
+
texture: tex,
|
|
457
|
+
uv(ix, iy, _tileIndex) {
|
|
458
|
+
return vec2(ix.toFloat(), iy.toFloat());
|
|
459
|
+
},
|
|
460
|
+
texel(ix, iy, tileIndex) {
|
|
461
|
+
return ivec3(ix, iy, tileIndex);
|
|
462
|
+
},
|
|
463
|
+
sample(u, v, tileIndex) {
|
|
464
|
+
return texture(tex, vec2(u, v)).depth(int(tileIndex));
|
|
465
|
+
},
|
|
466
|
+
resize(width, height, nextTileCount) {
|
|
467
|
+
currentEdgeVertexCount = width;
|
|
468
|
+
currentTileCount = nextTileCount;
|
|
469
|
+
tex.setSize(width, height, nextTileCount);
|
|
470
|
+
tex.needsUpdate = true;
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
function atlasCoord(tilesPerRow, edgeVertexCount, ix, iy, tileIndex) {
|
|
475
|
+
const tilesPerRowNode = int(tilesPerRow);
|
|
476
|
+
const edge = int(edgeVertexCount);
|
|
477
|
+
const tile = int(tileIndex);
|
|
478
|
+
const col = tile.mod(tilesPerRowNode);
|
|
479
|
+
const row = tile.div(tilesPerRowNode);
|
|
480
|
+
const atlasX = col.mul(edge).add(int(ix));
|
|
481
|
+
const atlasY = row.mul(edge).add(int(iy));
|
|
482
|
+
return { atlasX, atlasY };
|
|
483
|
+
}
|
|
484
|
+
function AtlasBackend(edgeVertexCount, tileCount, options) {
|
|
485
|
+
let currentEdgeVertexCount = edgeVertexCount;
|
|
486
|
+
let currentTileCount = tileCount;
|
|
487
|
+
let tilesPerRow = Math.max(1, Math.ceil(Math.sqrt(tileCount)));
|
|
488
|
+
const atlasSize = tilesPerRow * edgeVertexCount;
|
|
489
|
+
const tex = new StorageTexture(atlasSize, atlasSize);
|
|
490
|
+
configureStorageTexture(tex, options.format, options.filter);
|
|
491
|
+
return {
|
|
492
|
+
backendType: "atlas",
|
|
493
|
+
get edgeVertexCount() {
|
|
494
|
+
return currentEdgeVertexCount;
|
|
495
|
+
},
|
|
496
|
+
get tileCount() {
|
|
497
|
+
return currentTileCount;
|
|
498
|
+
},
|
|
499
|
+
texture: tex,
|
|
500
|
+
uv(ix, iy, tileIndex) {
|
|
501
|
+
const { atlasX, atlasY } = atlasCoord(
|
|
502
|
+
tilesPerRow,
|
|
503
|
+
currentEdgeVertexCount,
|
|
504
|
+
ix,
|
|
505
|
+
iy,
|
|
506
|
+
tileIndex
|
|
507
|
+
);
|
|
508
|
+
const currentAtlasSize = float(tilesPerRow * currentEdgeVertexCount);
|
|
509
|
+
return vec2(
|
|
510
|
+
atlasX.toFloat().add(0.5).div(currentAtlasSize),
|
|
511
|
+
atlasY.toFloat().add(0.5).div(currentAtlasSize)
|
|
512
|
+
);
|
|
513
|
+
},
|
|
514
|
+
texel(ix, iy, tileIndex) {
|
|
515
|
+
const { atlasX, atlasY } = atlasCoord(
|
|
516
|
+
tilesPerRow,
|
|
517
|
+
currentEdgeVertexCount,
|
|
518
|
+
ix,
|
|
519
|
+
iy,
|
|
520
|
+
tileIndex
|
|
521
|
+
);
|
|
522
|
+
return ivec2(atlasX, atlasY);
|
|
523
|
+
},
|
|
524
|
+
sample(u, v, tileIndex) {
|
|
525
|
+
const tile = int(tileIndex);
|
|
526
|
+
const tilesPerRowNode = int(tilesPerRow);
|
|
527
|
+
const col = tile.mod(tilesPerRowNode);
|
|
528
|
+
const row = tile.div(tilesPerRowNode);
|
|
529
|
+
const invTilesPerRow = float(1 / tilesPerRow);
|
|
530
|
+
const atlasU = col.toFloat().add(u).mul(invTilesPerRow);
|
|
531
|
+
const atlasV = row.toFloat().add(v).mul(invTilesPerRow);
|
|
532
|
+
return texture(tex, vec2(atlasU, atlasV));
|
|
533
|
+
},
|
|
534
|
+
resize(width, height, nextTileCount) {
|
|
535
|
+
currentEdgeVertexCount = width;
|
|
536
|
+
currentTileCount = nextTileCount;
|
|
537
|
+
tilesPerRow = Math.max(1, Math.ceil(Math.sqrt(nextTileCount)));
|
|
538
|
+
const nextAtlasSize = tilesPerRow * width;
|
|
539
|
+
const image = tex.image;
|
|
540
|
+
image.width = nextAtlasSize;
|
|
541
|
+
image.height = nextAtlasSize;
|
|
542
|
+
tex.needsUpdate = true;
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
function Texture3DBackend(edgeVertexCount, tileCount, options) {
|
|
547
|
+
let currentEdgeVertexCount = edgeVertexCount;
|
|
548
|
+
let currentTileCount = tileCount;
|
|
549
|
+
const tex = new StorageArrayTexture(
|
|
550
|
+
edgeVertexCount,
|
|
551
|
+
edgeVertexCount,
|
|
552
|
+
tileCount
|
|
553
|
+
);
|
|
554
|
+
configureStorageTexture(tex, options.format, options.filter);
|
|
555
|
+
return {
|
|
556
|
+
backendType: "texture-3d",
|
|
557
|
+
get edgeVertexCount() {
|
|
558
|
+
return currentEdgeVertexCount;
|
|
559
|
+
},
|
|
560
|
+
get tileCount() {
|
|
561
|
+
return currentTileCount;
|
|
562
|
+
},
|
|
563
|
+
texture: tex,
|
|
564
|
+
uv(ix, iy, _tileIndex) {
|
|
565
|
+
return vec2(ix.toFloat(), iy.toFloat());
|
|
566
|
+
},
|
|
567
|
+
texel(ix, iy, tileIndex) {
|
|
568
|
+
return ivec3(ix, iy, tileIndex);
|
|
569
|
+
},
|
|
570
|
+
sample(u, v, tileIndex) {
|
|
571
|
+
return texture(tex, vec2(u, v)).depth(int(tileIndex));
|
|
572
|
+
},
|
|
573
|
+
resize(width, height, nextTileCount) {
|
|
574
|
+
currentEdgeVertexCount = width;
|
|
575
|
+
currentTileCount = nextTileCount;
|
|
576
|
+
tex.setSize(width, height, nextTileCount);
|
|
577
|
+
tex.needsUpdate = true;
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
function tryGetDeviceLimits(renderer) {
|
|
582
|
+
const backend = renderer;
|
|
583
|
+
return backend.backend?.device?.limits ?? {};
|
|
584
|
+
}
|
|
585
|
+
function createTerrainFieldStorage(edgeVertexCount, tileCount, renderer, options = {}) {
|
|
586
|
+
const filter = options.filter ?? "linear";
|
|
587
|
+
const format = options.format ?? "rgba16float";
|
|
588
|
+
const forcedBackend = options.backend;
|
|
589
|
+
if (forcedBackend === "atlas") {
|
|
590
|
+
return AtlasBackend(edgeVertexCount, tileCount, { filter, format });
|
|
591
|
+
}
|
|
592
|
+
if (forcedBackend === "texture-3d") {
|
|
593
|
+
return Texture3DBackend(edgeVertexCount, tileCount, { filter, format });
|
|
594
|
+
}
|
|
595
|
+
if (forcedBackend === "array-texture") {
|
|
596
|
+
return ArrayTextureBackend(edgeVertexCount, tileCount, { filter, format });
|
|
597
|
+
}
|
|
598
|
+
const DEFAULT_MAX_TEXTURE_ARRAY_LAYERS = 256;
|
|
599
|
+
const maxLayers = renderer ? tryGetDeviceLimits(renderer).maxTextureArrayLayers ?? DEFAULT_MAX_TEXTURE_ARRAY_LAYERS : DEFAULT_MAX_TEXTURE_ARRAY_LAYERS;
|
|
600
|
+
if (tileCount > maxLayers) {
|
|
601
|
+
return AtlasBackend(edgeVertexCount, tileCount, { filter, format });
|
|
602
|
+
}
|
|
603
|
+
return ArrayTextureBackend(edgeVertexCount, tileCount, { filter, format });
|
|
604
|
+
}
|
|
605
|
+
function storeTerrainField(storage, ix, iy, tileIndex, value) {
|
|
606
|
+
if (storage.backendType === "array-texture" || storage.backendType === "texture-3d") {
|
|
607
|
+
return textureStore(
|
|
608
|
+
storage.texture,
|
|
609
|
+
uvec3(int(ix), int(iy), int(tileIndex)),
|
|
610
|
+
value
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
return textureStore(storage.texture, storage.texel(ix, iy, tileIndex), value);
|
|
614
|
+
}
|
|
615
|
+
function loadTerrainField(storage, ix, iy, tileIndex) {
|
|
616
|
+
if (storage.backendType === "array-texture" || storage.backendType === "texture-3d") {
|
|
617
|
+
return textureLoad(storage.texture, ivec2(int(ix), int(iy)), int(0)).depth(
|
|
618
|
+
int(tileIndex)
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
return textureLoad(storage.texture, storage.texel(ix, iy, tileIndex), int(0));
|
|
622
|
+
}
|
|
623
|
+
function loadTerrainFieldElevation(storage, ix, iy, tileIndex) {
|
|
624
|
+
return loadTerrainField(storage, ix, iy, tileIndex).r;
|
|
625
|
+
}
|
|
626
|
+
function loadTerrainFieldNormal(storage, ix, iy, tileIndex) {
|
|
627
|
+
const raw = loadTerrainField(storage, ix, iy, tileIndex);
|
|
628
|
+
return vec2(raw.g, raw.b);
|
|
629
|
+
}
|
|
630
|
+
function sampleTerrainField(storage, u, v, tileIndex) {
|
|
631
|
+
return storage.sample(u, v, tileIndex);
|
|
632
|
+
}
|
|
633
|
+
function sampleTerrainFieldElevation(storage, u, v, tileIndex) {
|
|
634
|
+
return sampleTerrainField(storage, u, v, tileIndex).r;
|
|
635
|
+
}
|
|
636
|
+
function sampleTerrainFieldNormal(storage, u, v, tileIndex) {
|
|
637
|
+
const raw = sampleTerrainField(storage, u, v, tileIndex);
|
|
638
|
+
return vec2(raw.g, raw.b);
|
|
639
|
+
}
|
|
640
|
+
function packTerrainFieldSample(height, normalXZ, extra = float(0)) {
|
|
641
|
+
return vec4(height, normalXZ.x, normalXZ.y, extra);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const createElevation = (tile, uniforms, elevationFn) => {
|
|
645
|
+
return function perVertexElevation(nodeIndex, localCoordinates) {
|
|
646
|
+
const ix = int(localCoordinates.x);
|
|
647
|
+
const iy = int(localCoordinates.y);
|
|
648
|
+
const edgeVertexCount = uniforms.uInnerTileSegments.toVar().add(int(3));
|
|
649
|
+
const tileUV = localCoordinates.toFloat().div(edgeVertexCount.toFloat());
|
|
650
|
+
const rootUV = tile.rootUVCompute(nodeIndex, ix, iy);
|
|
651
|
+
const worldPosition = tile.tileVertexWorldPositionCompute(nodeIndex, ix, iy).setName("worldPositionWithSkirt");
|
|
652
|
+
const rootSize = uniforms.uRootSize.toVar();
|
|
653
|
+
return elevationFn({
|
|
654
|
+
worldPosition,
|
|
655
|
+
rootSize,
|
|
656
|
+
rootUV,
|
|
657
|
+
tileOriginVec2: tile.tileOriginVec2(nodeIndex),
|
|
658
|
+
tileSize: tile.tileSize(nodeIndex),
|
|
659
|
+
tileLevel: tile.tileLevel(nodeIndex),
|
|
660
|
+
nodeIndex: int(nodeIndex),
|
|
661
|
+
tileUV
|
|
662
|
+
});
|
|
663
|
+
};
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
function createTileCompute(leafStorage, uniforms) {
|
|
667
|
+
const tileLevel = Fn(([nodeIndex]) => {
|
|
668
|
+
const nodeOffset = nodeIndex.mul(int(4));
|
|
669
|
+
return leafStorage.node.element(nodeOffset).toInt();
|
|
670
|
+
});
|
|
671
|
+
const tileOriginVec2 = Fn(([nodeIndex]) => {
|
|
672
|
+
const nodeOffset = nodeIndex.mul(int(4));
|
|
673
|
+
const nodeX = leafStorage.node.element(nodeOffset.add(int(1))).toFloat();
|
|
674
|
+
const nodeY = leafStorage.node.element(nodeOffset.add(int(2))).toFloat();
|
|
675
|
+
return vec2(nodeX, nodeY);
|
|
676
|
+
});
|
|
677
|
+
const tileSize = Fn(([nodeIndex]) => {
|
|
678
|
+
const rootSize = uniforms.uRootSize.toVar();
|
|
679
|
+
const level = tileLevel(nodeIndex);
|
|
680
|
+
return float(rootSize).div(pow(float(2), level.toFloat()));
|
|
681
|
+
});
|
|
682
|
+
const rootUVCompute = Fn(([nodeIndex, ix, iy]) => {
|
|
683
|
+
const nodeVec2 = tileOriginVec2(nodeIndex);
|
|
684
|
+
const nodeX = nodeVec2.x;
|
|
685
|
+
const nodeY = nodeVec2.y;
|
|
686
|
+
const rootSize = uniforms.uRootSize.toVar();
|
|
687
|
+
const rootOrigin = uniforms.uRootOrigin.toVar();
|
|
688
|
+
const size = tileSize(nodeIndex);
|
|
689
|
+
const half = float(0.5);
|
|
690
|
+
const halfRoot = float(rootSize).mul(half);
|
|
691
|
+
const fInnerSegments = uniforms.uInnerTileSegments.toVar().toFloat();
|
|
692
|
+
const texelSpacing = size.div(fInnerSegments);
|
|
693
|
+
const absX = nodeX.mul(fInnerSegments).add(int(ix).toFloat().sub(float(1)));
|
|
694
|
+
const absY = nodeY.mul(fInnerSegments).add(int(iy).toFloat().sub(float(1)));
|
|
695
|
+
const worldX = rootOrigin.x.add(absX.mul(texelSpacing)).sub(halfRoot);
|
|
696
|
+
const worldZ = rootOrigin.z.add(absY.mul(texelSpacing)).sub(halfRoot);
|
|
697
|
+
const centeredX = worldX.sub(rootOrigin.x);
|
|
698
|
+
const centeredZ = worldZ.sub(rootOrigin.z);
|
|
699
|
+
return vec2(
|
|
700
|
+
centeredX.div(rootSize).add(half),
|
|
701
|
+
centeredZ.div(rootSize).mul(float(-1)).add(half)
|
|
702
|
+
);
|
|
703
|
+
});
|
|
704
|
+
const tileVertexWorldPositionCompute = Fn(
|
|
705
|
+
([nodeIndex, ix, iy]) => {
|
|
706
|
+
const rootOrigin = uniforms.uRootOrigin.toVar();
|
|
707
|
+
const nodeVec2 = tileOriginVec2(nodeIndex);
|
|
708
|
+
const nodeX = nodeVec2.x;
|
|
709
|
+
const nodeY = nodeVec2.y;
|
|
710
|
+
const rootSize = uniforms.uRootSize.toVar();
|
|
711
|
+
const size = tileSize(nodeIndex);
|
|
712
|
+
const half = float(0.5);
|
|
713
|
+
const halfRoot = float(rootSize).mul(half);
|
|
714
|
+
const fInnerSegments = uniforms.uInnerTileSegments.toVar().toFloat();
|
|
715
|
+
const texelSpacing = size.div(fInnerSegments);
|
|
716
|
+
const absX = nodeX.mul(fInnerSegments).add(int(ix).toFloat().sub(float(1)));
|
|
717
|
+
const absY = nodeY.mul(fInnerSegments).add(int(iy).toFloat().sub(float(1)));
|
|
718
|
+
const worldX = rootOrigin.x.add(absX.mul(texelSpacing)).sub(halfRoot);
|
|
719
|
+
const worldZ = rootOrigin.z.add(absY.mul(texelSpacing)).sub(halfRoot);
|
|
720
|
+
return vec3(worldX, rootOrigin.y, worldZ);
|
|
721
|
+
}
|
|
722
|
+
);
|
|
723
|
+
return {
|
|
724
|
+
tileLevel,
|
|
725
|
+
tileOriginVec2,
|
|
726
|
+
tileSize,
|
|
727
|
+
rootUVCompute,
|
|
728
|
+
tileVertexWorldPositionCompute
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
function tileLocalToFieldUV$1(localCoord, innerSegments) {
|
|
732
|
+
const edge = float(innerSegments).add(float(3));
|
|
733
|
+
return float(localCoord).mul(float(innerSegments)).add(float(1.5)).div(edge);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
const rootSize = param(256).displayName("rootSize");
|
|
737
|
+
const origin = param({
|
|
738
|
+
x: 0,
|
|
739
|
+
y: 0,
|
|
740
|
+
z: 0
|
|
741
|
+
}).displayName("origin");
|
|
742
|
+
const innerTileSegments = param(13).displayName("innerTileSegments");
|
|
743
|
+
const skirtScale = param(100).displayName("skirtScale");
|
|
744
|
+
const elevationScale = param(1).displayName("elevationScale");
|
|
745
|
+
const maxNodes = param(1024).displayName("maxNodes");
|
|
746
|
+
const maxLevel = param(16).displayName("maxLevel");
|
|
747
|
+
const quadtreeUpdate = param({
|
|
748
|
+
cameraOrigin: { x: 0, y: 0, z: 0 },
|
|
749
|
+
mode: "distance",
|
|
750
|
+
distanceFactor: 1.5
|
|
751
|
+
}).displayName("quadtreeUpdate");
|
|
752
|
+
const surface = param(null).displayName("surface");
|
|
753
|
+
const terrainFieldFilter = param("linear").displayName("terrainFieldFilter");
|
|
754
|
+
const elevationFn = param(() => float(0));
|
|
755
|
+
|
|
756
|
+
function createLeafStorage(maxNodes) {
|
|
757
|
+
const data = new Int32Array(maxNodes * 4);
|
|
758
|
+
const attribute = new StorageBufferAttribute(data, 4);
|
|
759
|
+
const node = storage(attribute, "i32", 1).toReadOnly().setName("leafStorage");
|
|
760
|
+
return { data, attribute, node };
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
const Dir = {
|
|
764
|
+
LEFT: 0,
|
|
765
|
+
RIGHT: 1,
|
|
766
|
+
TOP: 2,
|
|
767
|
+
BOTTOM: 3
|
|
768
|
+
};
|
|
769
|
+
const U32_EMPTY = 4294967295;
|
|
770
|
+
function allocLeafSet(capacity) {
|
|
771
|
+
return {
|
|
772
|
+
capacity,
|
|
773
|
+
count: 0,
|
|
774
|
+
space: new Uint8Array(capacity),
|
|
775
|
+
level: new Uint8Array(capacity),
|
|
776
|
+
x: new Int32Array(capacity),
|
|
777
|
+
y: new Int32Array(capacity)
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
function resetLeafSet(leaves) {
|
|
781
|
+
leaves.count = 0;
|
|
782
|
+
}
|
|
783
|
+
function allocSeamTable(capacity) {
|
|
784
|
+
return {
|
|
785
|
+
capacity,
|
|
786
|
+
count: 0,
|
|
787
|
+
stride: 8,
|
|
788
|
+
neighbors: new Uint32Array(capacity * 8)
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
function resetSeamTable(seams) {
|
|
792
|
+
seams.count = 0;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function createNodeStore(maxNodes, spaceCount) {
|
|
796
|
+
return {
|
|
797
|
+
maxNodes,
|
|
798
|
+
nodesUsed: 0,
|
|
799
|
+
currentGen: 1,
|
|
800
|
+
gen: new Uint16Array(maxNodes),
|
|
801
|
+
space: new Uint8Array(maxNodes),
|
|
802
|
+
level: new Uint8Array(maxNodes),
|
|
803
|
+
x: new Int32Array(maxNodes),
|
|
804
|
+
y: new Int32Array(maxNodes),
|
|
805
|
+
firstChild: new Uint32Array(maxNodes),
|
|
806
|
+
flags: new Uint8Array(maxNodes),
|
|
807
|
+
roots: new Uint32Array(spaceCount)
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
function beginFrame(store) {
|
|
811
|
+
store.nodesUsed = 0;
|
|
812
|
+
store.currentGen = store.currentGen + 1 & 65535;
|
|
813
|
+
if (store.currentGen === 0) {
|
|
814
|
+
store.gen.fill(0);
|
|
815
|
+
store.currentGen = 1;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
function allocNode(store, tile) {
|
|
819
|
+
const id = store.nodesUsed;
|
|
820
|
+
if (id >= store.maxNodes) return U32_EMPTY;
|
|
821
|
+
store.nodesUsed = id + 1;
|
|
822
|
+
store.gen[id] = store.currentGen;
|
|
823
|
+
store.space[id] = tile.space;
|
|
824
|
+
store.level[id] = tile.level;
|
|
825
|
+
store.x[id] = tile.x;
|
|
826
|
+
store.y[id] = tile.y;
|
|
827
|
+
store.firstChild[id] = U32_EMPTY;
|
|
828
|
+
store.flags[id] = 0;
|
|
829
|
+
return id;
|
|
830
|
+
}
|
|
831
|
+
function hasChildren(store, nodeId) {
|
|
832
|
+
return store.firstChild[nodeId] !== U32_EMPTY;
|
|
833
|
+
}
|
|
834
|
+
function ensureChildren(store, parentId) {
|
|
835
|
+
const existing = store.firstChild[parentId];
|
|
836
|
+
if (existing !== U32_EMPTY) return existing;
|
|
837
|
+
const childBase = store.nodesUsed;
|
|
838
|
+
if (childBase + 4 > store.maxNodes) return U32_EMPTY;
|
|
839
|
+
const space = store.space[parentId];
|
|
840
|
+
const level = store.level[parentId] + 1;
|
|
841
|
+
const px = store.x[parentId] << 1;
|
|
842
|
+
const py = store.y[parentId] << 1;
|
|
843
|
+
allocNode(store, { space, level, x: px, y: py });
|
|
844
|
+
allocNode(store, { space, level, x: px + 1, y: py });
|
|
845
|
+
allocNode(store, { space, level, x: px, y: py + 1 });
|
|
846
|
+
allocNode(store, { space, level, x: px + 1, y: py + 1 });
|
|
847
|
+
store.firstChild[parentId] = childBase;
|
|
848
|
+
return childBase;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
function nextPow2$1(n) {
|
|
852
|
+
let x = 1;
|
|
853
|
+
while (x < n) x <<= 1;
|
|
854
|
+
return x;
|
|
855
|
+
}
|
|
856
|
+
function mix32$1(x) {
|
|
857
|
+
x >>>= 0;
|
|
858
|
+
x ^= x >>> 16;
|
|
859
|
+
x = Math.imul(x, 2146121005) >>> 0;
|
|
860
|
+
x ^= x >>> 15;
|
|
861
|
+
x = Math.imul(x, 2221713035) >>> 0;
|
|
862
|
+
x ^= x >>> 16;
|
|
863
|
+
return x >>> 0;
|
|
864
|
+
}
|
|
865
|
+
function hashKey$1(space, level, x, y) {
|
|
866
|
+
const h = space & 255 ^ (level & 255) << 8 ^ mix32$1(x) >>> 0 ^ mix32$1(y) >>> 0;
|
|
867
|
+
return mix32$1(h);
|
|
868
|
+
}
|
|
869
|
+
function createSpatialIndex(maxEntries) {
|
|
870
|
+
const size = nextPow2$1(Math.max(2, maxEntries * 2));
|
|
871
|
+
return {
|
|
872
|
+
size,
|
|
873
|
+
mask: size - 1,
|
|
874
|
+
stampGen: 1,
|
|
875
|
+
stamp: new Uint16Array(size),
|
|
876
|
+
keysSpace: new Uint8Array(size),
|
|
877
|
+
keysLevel: new Uint8Array(size),
|
|
878
|
+
keysX: new Uint32Array(size),
|
|
879
|
+
keysY: new Uint32Array(size),
|
|
880
|
+
values: new Uint32Array(size)
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
function resetSpatialIndex(index) {
|
|
884
|
+
index.stampGen = index.stampGen + 1 & 65535;
|
|
885
|
+
if (index.stampGen === 0) {
|
|
886
|
+
index.stamp.fill(0);
|
|
887
|
+
index.stampGen = 1;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
function insertSpatialIndexRaw(index, space, level, x, y, value) {
|
|
891
|
+
const s = space & 255;
|
|
892
|
+
const l = level & 255;
|
|
893
|
+
const xx = x >>> 0;
|
|
894
|
+
const yy = y >>> 0;
|
|
895
|
+
let slot = hashKey$1(s, l, xx, yy) & index.mask;
|
|
896
|
+
for (let probes = 0; probes < index.size; probes++) {
|
|
897
|
+
if (index.stamp[slot] !== index.stampGen) {
|
|
898
|
+
index.stamp[slot] = index.stampGen;
|
|
899
|
+
index.keysSpace[slot] = s;
|
|
900
|
+
index.keysLevel[slot] = l;
|
|
901
|
+
index.keysX[slot] = xx;
|
|
902
|
+
index.keysY[slot] = yy;
|
|
903
|
+
index.values[slot] = value >>> 0;
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
if (index.keysSpace[slot] === s && index.keysLevel[slot] === l && index.keysX[slot] === xx && index.keysY[slot] === yy) {
|
|
907
|
+
index.values[slot] = value >>> 0;
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
slot = slot + 1 & index.mask;
|
|
911
|
+
}
|
|
912
|
+
throw new Error("SpatialIndex is full (no empty slot found).");
|
|
913
|
+
}
|
|
914
|
+
function lookupSpatialIndexRaw(index, space, level, x, y) {
|
|
915
|
+
const s = space & 255;
|
|
916
|
+
const l = level & 255;
|
|
917
|
+
const xx = x >>> 0;
|
|
918
|
+
const yy = y >>> 0;
|
|
919
|
+
let slot = hashKey$1(s, l, xx, yy) & index.mask;
|
|
920
|
+
for (let probes = 0; probes < index.size; probes++) {
|
|
921
|
+
if (index.stamp[slot] !== index.stampGen) return U32_EMPTY;
|
|
922
|
+
if (index.keysSpace[slot] === s && index.keysLevel[slot] === l && index.keysX[slot] === xx && index.keysY[slot] === yy) {
|
|
923
|
+
return index.values[slot];
|
|
924
|
+
}
|
|
925
|
+
slot = slot + 1 & index.mask;
|
|
926
|
+
}
|
|
927
|
+
return U32_EMPTY;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
function buildLeafIndex(leaves, out) {
|
|
931
|
+
const index = out ?? createSpatialIndex(leaves.count);
|
|
932
|
+
resetSpatialIndex(index);
|
|
933
|
+
for (let i = 0; i < leaves.count; i++) {
|
|
934
|
+
insertSpatialIndexRaw(index, leaves.space[i], leaves.level[i], leaves.x[i], leaves.y[i], i);
|
|
935
|
+
}
|
|
936
|
+
return index;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
function createState(cfg, surface) {
|
|
940
|
+
const store = createNodeStore(cfg.maxNodes, surface.spaceCount);
|
|
941
|
+
const scratchRootTiles = [];
|
|
942
|
+
for (let i = 0; i < surface.maxRootCount; i++) {
|
|
943
|
+
scratchRootTiles.push({ space: 0, level: 0, x: 0, y: 0 });
|
|
944
|
+
}
|
|
945
|
+
return {
|
|
946
|
+
cfg,
|
|
947
|
+
store,
|
|
948
|
+
leaves: allocLeafSet(cfg.maxNodes),
|
|
949
|
+
leafNodeIds: new Uint32Array(cfg.maxNodes),
|
|
950
|
+
leafIndex: createSpatialIndex(cfg.maxNodes),
|
|
951
|
+
stack: new Uint32Array(cfg.maxNodes),
|
|
952
|
+
rootNodeIds: new Uint32Array(surface.maxRootCount),
|
|
953
|
+
rootCount: 0,
|
|
954
|
+
splitQueue: new Uint32Array(cfg.maxNodes),
|
|
955
|
+
splitStamp: new Uint16Array(cfg.maxNodes),
|
|
956
|
+
splitGen: 1,
|
|
957
|
+
scratchTile: { space: 0, level: 0, x: 0, y: 0 },
|
|
958
|
+
scratchNeighbor: { space: 0, level: 0, x: 0, y: 0 },
|
|
959
|
+
scratchBounds: { cx: 0, cy: 0, cz: 0, r: 0 },
|
|
960
|
+
scratchRootTiles,
|
|
961
|
+
spaceCount: surface.spaceCount
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
function beginUpdate(state, surface, params) {
|
|
965
|
+
if (surface.spaceCount !== state.spaceCount) {
|
|
966
|
+
throw new Error(
|
|
967
|
+
`Surface spaceCount changed (${state.spaceCount} -> ${surface.spaceCount}). Create a new quadtree state.`
|
|
968
|
+
);
|
|
969
|
+
}
|
|
970
|
+
if (surface.maxRootCount !== state.rootNodeIds.length) {
|
|
971
|
+
throw new Error(
|
|
972
|
+
`Surface maxRootCount changed (${state.rootNodeIds.length} -> ${surface.maxRootCount}). Create a new quadtree state.`
|
|
973
|
+
);
|
|
974
|
+
}
|
|
975
|
+
beginFrame(state.store);
|
|
976
|
+
state.rootCount = 0;
|
|
977
|
+
const rootCount = surface.rootTiles(params.cameraOrigin, state.scratchRootTiles);
|
|
978
|
+
if (rootCount < 0 || rootCount > surface.maxRootCount) {
|
|
979
|
+
throw new Error(`Surface returned invalid root count (${rootCount}).`);
|
|
980
|
+
}
|
|
981
|
+
for (let i = 0; i < rootCount; i++) {
|
|
982
|
+
const rootId = allocNode(state.store, state.scratchRootTiles[i]);
|
|
983
|
+
if (rootId === U32_EMPTY) {
|
|
984
|
+
throw new Error("Failed to allocate root node (maxNodes too small).");
|
|
985
|
+
}
|
|
986
|
+
state.rootNodeIds[i] = rootId;
|
|
987
|
+
state.rootCount = i + 1;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
function shouldSplit(bounds, level, maxLevel, params) {
|
|
992
|
+
if (level >= maxLevel) return false;
|
|
993
|
+
const mode = params.mode ?? "distance";
|
|
994
|
+
const cx = bounds.cx;
|
|
995
|
+
const cy = bounds.cy;
|
|
996
|
+
const cz = bounds.cz;
|
|
997
|
+
const distSq = cx * cx + cy * cy + cz * cz;
|
|
998
|
+
const safeDistSq = distSq > 1e-12 ? distSq : 1e-12;
|
|
999
|
+
if (mode === "screen") {
|
|
1000
|
+
const proj = params.projectionFactor ?? 0;
|
|
1001
|
+
const target = params.targetPixels ?? 0;
|
|
1002
|
+
if (proj <= 0 || target <= 0) {
|
|
1003
|
+
const f2 = params.distanceFactor ?? 2;
|
|
1004
|
+
const threshold2 = bounds.r * f2;
|
|
1005
|
+
return safeDistSq < threshold2 * threshold2;
|
|
1006
|
+
}
|
|
1007
|
+
const left = bounds.r * bounds.r * proj * proj;
|
|
1008
|
+
const right = safeDistSq * target * target;
|
|
1009
|
+
return left > right;
|
|
1010
|
+
}
|
|
1011
|
+
const f = params.distanceFactor ?? 2;
|
|
1012
|
+
const threshold = bounds.r * f;
|
|
1013
|
+
return safeDistSq < threshold * threshold;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
function refineLeaves(state, surface, params, outLeaves) {
|
|
1017
|
+
const leaves = outLeaves ?? state.leaves;
|
|
1018
|
+
resetLeafSet(leaves);
|
|
1019
|
+
const store = state.store;
|
|
1020
|
+
const stack = state.stack;
|
|
1021
|
+
let sp = 0;
|
|
1022
|
+
for (let i = 0; i < state.rootCount; i++) {
|
|
1023
|
+
stack[sp++] = state.rootNodeIds[i];
|
|
1024
|
+
}
|
|
1025
|
+
while (sp > 0) {
|
|
1026
|
+
const nodeId = stack[--sp];
|
|
1027
|
+
const level = store.level[nodeId];
|
|
1028
|
+
const space = store.space[nodeId];
|
|
1029
|
+
const x = store.x[nodeId];
|
|
1030
|
+
const y = store.y[nodeId];
|
|
1031
|
+
const tile = state.scratchTile;
|
|
1032
|
+
tile.space = space;
|
|
1033
|
+
tile.level = level;
|
|
1034
|
+
tile.x = x;
|
|
1035
|
+
tile.y = y;
|
|
1036
|
+
const bounds = state.scratchBounds;
|
|
1037
|
+
surface.tileBounds(tile, params.cameraOrigin, bounds);
|
|
1038
|
+
if (hasChildren(store, nodeId)) {
|
|
1039
|
+
const base = store.firstChild[nodeId];
|
|
1040
|
+
stack[sp++] = base + 3;
|
|
1041
|
+
stack[sp++] = base + 2;
|
|
1042
|
+
stack[sp++] = base + 1;
|
|
1043
|
+
stack[sp++] = base + 0;
|
|
1044
|
+
continue;
|
|
1045
|
+
}
|
|
1046
|
+
const split = shouldSplit(bounds, level, state.cfg.maxLevel, params);
|
|
1047
|
+
if (split) {
|
|
1048
|
+
const base = ensureChildren(store, nodeId);
|
|
1049
|
+
if (base !== U32_EMPTY) {
|
|
1050
|
+
stack[sp++] = base + 3;
|
|
1051
|
+
stack[sp++] = base + 2;
|
|
1052
|
+
stack[sp++] = base + 1;
|
|
1053
|
+
stack[sp++] = base + 0;
|
|
1054
|
+
continue;
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
const i = leaves.count;
|
|
1058
|
+
if (i >= leaves.capacity) {
|
|
1059
|
+
throw new Error("LeafSet capacity exceeded.");
|
|
1060
|
+
}
|
|
1061
|
+
leaves.space[i] = space;
|
|
1062
|
+
leaves.level[i] = level;
|
|
1063
|
+
leaves.x[i] = x;
|
|
1064
|
+
leaves.y[i] = y;
|
|
1065
|
+
state.leafNodeIds[i] = nodeId;
|
|
1066
|
+
leaves.count = i + 1;
|
|
1067
|
+
}
|
|
1068
|
+
return leaves;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
function resetSplitMarks(state) {
|
|
1072
|
+
state.splitGen = state.splitGen + 1 & 65535;
|
|
1073
|
+
if (state.splitGen === 0) {
|
|
1074
|
+
state.splitStamp.fill(0);
|
|
1075
|
+
state.splitGen = 1;
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
function scheduleSplit(state, nodeId, count) {
|
|
1079
|
+
if (nodeId === U32_EMPTY) return count;
|
|
1080
|
+
if (state.splitStamp[nodeId] === state.splitGen) return count;
|
|
1081
|
+
state.splitStamp[nodeId] = state.splitGen;
|
|
1082
|
+
state.splitQueue[count] = nodeId;
|
|
1083
|
+
return count + 1;
|
|
1084
|
+
}
|
|
1085
|
+
function balance2to1(state, surface, params, leaves) {
|
|
1086
|
+
const maxIters = state.cfg.maxLevel + 1;
|
|
1087
|
+
for (let iter = 0; iter < maxIters; iter++) {
|
|
1088
|
+
const index = buildLeafIndex(leaves, state.leafIndex);
|
|
1089
|
+
resetSplitMarks(state);
|
|
1090
|
+
let splitCount = 0;
|
|
1091
|
+
for (let i = 0; i < leaves.count; i++) {
|
|
1092
|
+
const leafLevel = leaves.level[i];
|
|
1093
|
+
if (leafLevel < 2) continue;
|
|
1094
|
+
const leafSpace = leaves.space[i];
|
|
1095
|
+
const leafX = leaves.x[i];
|
|
1096
|
+
const leafY = leaves.y[i];
|
|
1097
|
+
for (let dir = 0; dir < 4; dir++) {
|
|
1098
|
+
for (let candidateLevel = leafLevel - 2; candidateLevel >= 0; candidateLevel--) {
|
|
1099
|
+
const shift = leafLevel - candidateLevel;
|
|
1100
|
+
const tile = state.scratchTile;
|
|
1101
|
+
tile.space = leafSpace;
|
|
1102
|
+
tile.level = candidateLevel;
|
|
1103
|
+
tile.x = leafX >>> shift;
|
|
1104
|
+
tile.y = leafY >>> shift;
|
|
1105
|
+
const neighbor = state.scratchNeighbor;
|
|
1106
|
+
if (!surface.neighborSameLevel(tile, dir, neighbor)) break;
|
|
1107
|
+
const j = lookupSpatialIndexRaw(
|
|
1108
|
+
index,
|
|
1109
|
+
neighbor.space,
|
|
1110
|
+
neighbor.level,
|
|
1111
|
+
neighbor.x,
|
|
1112
|
+
neighbor.y
|
|
1113
|
+
);
|
|
1114
|
+
if (j !== U32_EMPTY) {
|
|
1115
|
+
splitCount = scheduleSplit(state, state.leafNodeIds[j], splitCount);
|
|
1116
|
+
break;
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
if (splitCount === 0) return leaves;
|
|
1122
|
+
let anySplit = false;
|
|
1123
|
+
for (let k = 0; k < splitCount; k++) {
|
|
1124
|
+
const nodeId = state.splitQueue[k];
|
|
1125
|
+
if (state.store.level[nodeId] >= state.cfg.maxLevel) continue;
|
|
1126
|
+
const base = ensureChildren(state.store, nodeId);
|
|
1127
|
+
if (base !== U32_EMPTY) anySplit = true;
|
|
1128
|
+
}
|
|
1129
|
+
if (!anySplit) return leaves;
|
|
1130
|
+
refineLeaves(state, surface, params, leaves);
|
|
1131
|
+
}
|
|
1132
|
+
return leaves;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
function update(state, surface, params, outLeaves) {
|
|
1136
|
+
const origY = params.cameraOrigin.y;
|
|
1137
|
+
params.cameraOrigin.y -= params.elevationAtCameraXZ ?? 0;
|
|
1138
|
+
beginUpdate(state, surface, params);
|
|
1139
|
+
const leaves = refineLeaves(state, surface, params, outLeaves);
|
|
1140
|
+
const result = balance2to1(state, surface, params, leaves);
|
|
1141
|
+
params.cameraOrigin.y = origY;
|
|
1142
|
+
return result;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
const scratchTile = { space: 0, level: 0, x: 0, y: 0 };
|
|
1146
|
+
const scratchNbr = { space: 0, level: 0, x: 0, y: 0 };
|
|
1147
|
+
const scratchParentTile = { space: 0, level: 0, x: 0, y: 0 };
|
|
1148
|
+
const scratchParentNbr = { space: 0, level: 0, x: 0, y: 0 };
|
|
1149
|
+
function buildSeams2to1(surface, leaves, outSeams, outIndex) {
|
|
1150
|
+
if (outSeams.capacity < leaves.count) {
|
|
1151
|
+
throw new Error("SeamTable capacity is smaller than LeafSet.count.");
|
|
1152
|
+
}
|
|
1153
|
+
const index = buildLeafIndex(leaves, outIndex);
|
|
1154
|
+
outSeams.count = leaves.count;
|
|
1155
|
+
const neighbors = outSeams.neighbors;
|
|
1156
|
+
for (let i = 0; i < leaves.count; i++) {
|
|
1157
|
+
const base = i * 8;
|
|
1158
|
+
const space = leaves.space[i];
|
|
1159
|
+
const level = leaves.level[i];
|
|
1160
|
+
const x = leaves.x[i];
|
|
1161
|
+
const y = leaves.y[i];
|
|
1162
|
+
for (let dir = 0; dir < 4; dir++) {
|
|
1163
|
+
const outOffset = base + dir * 2;
|
|
1164
|
+
neighbors[outOffset + 0] = U32_EMPTY;
|
|
1165
|
+
neighbors[outOffset + 1] = U32_EMPTY;
|
|
1166
|
+
scratchTile.space = space;
|
|
1167
|
+
scratchTile.level = level;
|
|
1168
|
+
scratchTile.x = x;
|
|
1169
|
+
scratchTile.y = y;
|
|
1170
|
+
if (!surface.neighborSameLevel(scratchTile, dir, scratchNbr)) continue;
|
|
1171
|
+
let j = lookupSpatialIndexRaw(index, scratchNbr.space, scratchNbr.level, scratchNbr.x, scratchNbr.y);
|
|
1172
|
+
if (j !== U32_EMPTY) {
|
|
1173
|
+
neighbors[outOffset + 0] = j;
|
|
1174
|
+
continue;
|
|
1175
|
+
}
|
|
1176
|
+
if (level > 0) {
|
|
1177
|
+
const px = x >>> 1;
|
|
1178
|
+
const py = y >>> 1;
|
|
1179
|
+
scratchParentTile.space = space;
|
|
1180
|
+
scratchParentTile.level = level - 1;
|
|
1181
|
+
scratchParentTile.x = px;
|
|
1182
|
+
scratchParentTile.y = py;
|
|
1183
|
+
if (surface.neighborSameLevel(scratchParentTile, dir, scratchParentNbr)) {
|
|
1184
|
+
j = lookupSpatialIndexRaw(
|
|
1185
|
+
index,
|
|
1186
|
+
scratchParentNbr.space,
|
|
1187
|
+
scratchParentNbr.level,
|
|
1188
|
+
scratchParentNbr.x,
|
|
1189
|
+
scratchParentNbr.y
|
|
1190
|
+
);
|
|
1191
|
+
if (j !== U32_EMPTY) {
|
|
1192
|
+
neighbors[outOffset + 0] = j;
|
|
1193
|
+
continue;
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
const childLevel = scratchNbr.level + 1;
|
|
1198
|
+
const x2 = scratchNbr.x << 1 >>> 0;
|
|
1199
|
+
const y2 = scratchNbr.y << 1 >>> 0;
|
|
1200
|
+
let ax = 0;
|
|
1201
|
+
let ay = 0;
|
|
1202
|
+
let bx = 0;
|
|
1203
|
+
let by = 0;
|
|
1204
|
+
switch (dir) {
|
|
1205
|
+
case Dir.LEFT:
|
|
1206
|
+
ax = x2 + 1;
|
|
1207
|
+
ay = y2;
|
|
1208
|
+
bx = x2 + 1;
|
|
1209
|
+
by = y2 + 1;
|
|
1210
|
+
break;
|
|
1211
|
+
case Dir.RIGHT:
|
|
1212
|
+
ax = x2;
|
|
1213
|
+
ay = y2;
|
|
1214
|
+
bx = x2;
|
|
1215
|
+
by = y2 + 1;
|
|
1216
|
+
break;
|
|
1217
|
+
case Dir.TOP:
|
|
1218
|
+
ax = x2;
|
|
1219
|
+
ay = y2 + 1;
|
|
1220
|
+
bx = x2 + 1;
|
|
1221
|
+
by = y2 + 1;
|
|
1222
|
+
break;
|
|
1223
|
+
case Dir.BOTTOM:
|
|
1224
|
+
ax = x2;
|
|
1225
|
+
ay = y2;
|
|
1226
|
+
bx = x2 + 1;
|
|
1227
|
+
by = y2;
|
|
1228
|
+
break;
|
|
1229
|
+
}
|
|
1230
|
+
j = lookupSpatialIndexRaw(index, scratchNbr.space, childLevel, ax, ay);
|
|
1231
|
+
if (j !== U32_EMPTY) neighbors[outOffset + 0] = j;
|
|
1232
|
+
j = lookupSpatialIndexRaw(index, scratchNbr.space, childLevel, bx, by);
|
|
1233
|
+
if (j !== U32_EMPTY) neighbors[outOffset + 1] = j;
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
return outSeams;
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
function createFlatSurface(cfg) {
|
|
1240
|
+
const halfRoot = 0.5 * cfg.rootSize;
|
|
1241
|
+
const maxHeight = cfg.maxHeight ?? 0;
|
|
1242
|
+
const surface = {
|
|
1243
|
+
spaceCount: 1,
|
|
1244
|
+
maxRootCount: 1,
|
|
1245
|
+
neighborSameLevel(tile, dir, out) {
|
|
1246
|
+
const level = tile.level;
|
|
1247
|
+
const x = tile.x;
|
|
1248
|
+
const y = tile.y;
|
|
1249
|
+
let nx = x;
|
|
1250
|
+
let ny = y;
|
|
1251
|
+
switch (dir) {
|
|
1252
|
+
case Dir.LEFT:
|
|
1253
|
+
nx = x - 1;
|
|
1254
|
+
break;
|
|
1255
|
+
case Dir.RIGHT:
|
|
1256
|
+
nx = x + 1;
|
|
1257
|
+
break;
|
|
1258
|
+
case Dir.TOP:
|
|
1259
|
+
ny = y - 1;
|
|
1260
|
+
break;
|
|
1261
|
+
case Dir.BOTTOM:
|
|
1262
|
+
ny = y + 1;
|
|
1263
|
+
break;
|
|
1264
|
+
}
|
|
1265
|
+
if (nx < 0 || ny < 0) return false;
|
|
1266
|
+
const maxCoord = (1 << level) - 1;
|
|
1267
|
+
if (nx > maxCoord || ny > maxCoord) return false;
|
|
1268
|
+
out.space = 0;
|
|
1269
|
+
out.level = level;
|
|
1270
|
+
out.x = nx;
|
|
1271
|
+
out.y = ny;
|
|
1272
|
+
return true;
|
|
1273
|
+
},
|
|
1274
|
+
tileBounds(tile, cameraOrigin, out) {
|
|
1275
|
+
const level = tile.level;
|
|
1276
|
+
const scale = 1 / (1 << level);
|
|
1277
|
+
const size = cfg.rootSize * scale;
|
|
1278
|
+
const minX = cfg.origin.x + (tile.x * size - halfRoot);
|
|
1279
|
+
const minZ = cfg.origin.z + (tile.y * size - halfRoot);
|
|
1280
|
+
const centerX = minX + 0.5 * size;
|
|
1281
|
+
const centerY = cfg.origin.y;
|
|
1282
|
+
const centerZ = minZ + 0.5 * size;
|
|
1283
|
+
out.cx = centerX - cameraOrigin.x;
|
|
1284
|
+
out.cy = centerY - cameraOrigin.y;
|
|
1285
|
+
out.cz = centerZ - cameraOrigin.z;
|
|
1286
|
+
out.r = 0.7071067811865476 * size + maxHeight;
|
|
1287
|
+
},
|
|
1288
|
+
rootTiles(_cameraOrigin, out) {
|
|
1289
|
+
const root = out[0];
|
|
1290
|
+
root.space = 0;
|
|
1291
|
+
root.level = 0;
|
|
1292
|
+
root.x = 0;
|
|
1293
|
+
root.y = 0;
|
|
1294
|
+
return 1;
|
|
1295
|
+
}
|
|
1296
|
+
};
|
|
1297
|
+
return surface;
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
function createInfiniteFlatSurface(cfg) {
|
|
1301
|
+
const halfRoot = 0.5 * cfg.rootSize;
|
|
1302
|
+
const maxHeight = cfg.maxHeight ?? 0;
|
|
1303
|
+
const rootGridRadius = Math.max(0, Math.floor(cfg.rootGridRadius ?? 1));
|
|
1304
|
+
const rootWidth = rootGridRadius * 2 + 1;
|
|
1305
|
+
return {
|
|
1306
|
+
spaceCount: 1,
|
|
1307
|
+
maxRootCount: rootWidth * rootWidth,
|
|
1308
|
+
neighborSameLevel(tile, dir, out) {
|
|
1309
|
+
let nx = tile.x;
|
|
1310
|
+
let ny = tile.y;
|
|
1311
|
+
switch (dir) {
|
|
1312
|
+
case Dir.LEFT:
|
|
1313
|
+
nx = tile.x - 1;
|
|
1314
|
+
break;
|
|
1315
|
+
case Dir.RIGHT:
|
|
1316
|
+
nx = tile.x + 1;
|
|
1317
|
+
break;
|
|
1318
|
+
case Dir.TOP:
|
|
1319
|
+
ny = tile.y - 1;
|
|
1320
|
+
break;
|
|
1321
|
+
case Dir.BOTTOM:
|
|
1322
|
+
ny = tile.y + 1;
|
|
1323
|
+
break;
|
|
1324
|
+
}
|
|
1325
|
+
out.space = tile.space;
|
|
1326
|
+
out.level = tile.level;
|
|
1327
|
+
out.x = nx;
|
|
1328
|
+
out.y = ny;
|
|
1329
|
+
return true;
|
|
1330
|
+
},
|
|
1331
|
+
tileBounds(tile, cameraOrigin, out) {
|
|
1332
|
+
const level = tile.level;
|
|
1333
|
+
const scale = 1 / (1 << level);
|
|
1334
|
+
const size = cfg.rootSize * scale;
|
|
1335
|
+
const minX = cfg.origin.x + (tile.x * size - halfRoot);
|
|
1336
|
+
const minZ = cfg.origin.z + (tile.y * size - halfRoot);
|
|
1337
|
+
const centerX = minX + 0.5 * size;
|
|
1338
|
+
const centerY = cfg.origin.y;
|
|
1339
|
+
const centerZ = minZ + 0.5 * size;
|
|
1340
|
+
out.cx = centerX - cameraOrigin.x;
|
|
1341
|
+
out.cy = centerY - cameraOrigin.y;
|
|
1342
|
+
out.cz = centerZ - cameraOrigin.z;
|
|
1343
|
+
out.r = 0.7071067811865476 * size + maxHeight;
|
|
1344
|
+
},
|
|
1345
|
+
rootTiles(cameraOrigin, out) {
|
|
1346
|
+
const camRootX = Math.floor((cameraOrigin.x - cfg.origin.x + halfRoot) / cfg.rootSize);
|
|
1347
|
+
const camRootY = Math.floor((cameraOrigin.z - cfg.origin.z + halfRoot) / cfg.rootSize);
|
|
1348
|
+
let index = 0;
|
|
1349
|
+
for (let dy = -rootGridRadius; dy <= rootGridRadius; dy++) {
|
|
1350
|
+
for (let dx = -rootGridRadius; dx <= rootGridRadius; dx++) {
|
|
1351
|
+
const root = out[index];
|
|
1352
|
+
root.space = 0;
|
|
1353
|
+
root.level = 0;
|
|
1354
|
+
root.x = camRootX + dx;
|
|
1355
|
+
root.y = camRootY + dy;
|
|
1356
|
+
index++;
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
return index;
|
|
1360
|
+
}
|
|
1361
|
+
};
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
function createCubeSphereSurface(_cfg) {
|
|
1365
|
+
return {
|
|
1366
|
+
spaceCount: 6,
|
|
1367
|
+
maxRootCount: 6,
|
|
1368
|
+
neighborSameLevel(_tile, _dir, _out) {
|
|
1369
|
+
return false;
|
|
1370
|
+
},
|
|
1371
|
+
tileBounds(_tile, _cameraOrigin, out) {
|
|
1372
|
+
out.cx = 0;
|
|
1373
|
+
out.cy = 0;
|
|
1374
|
+
out.cz = 0;
|
|
1375
|
+
out.r = Number.MAX_VALUE;
|
|
1376
|
+
},
|
|
1377
|
+
rootTiles(_cameraOrigin, out) {
|
|
1378
|
+
for (let s = 0; s < 6; s++) {
|
|
1379
|
+
const root = out[s];
|
|
1380
|
+
root.space = s;
|
|
1381
|
+
root.level = 0;
|
|
1382
|
+
root.x = 0;
|
|
1383
|
+
root.y = 0;
|
|
1384
|
+
}
|
|
1385
|
+
return 6;
|
|
1386
|
+
}
|
|
1387
|
+
};
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
function cloneSpatialIndex(target, source) {
|
|
1391
|
+
if (target.size !== source.size) {
|
|
1392
|
+
throw new Error(
|
|
1393
|
+
`SpatialIndex size mismatch (target=${target.size}, source=${source.size}).`
|
|
1394
|
+
);
|
|
1395
|
+
}
|
|
1396
|
+
target.mask = source.mask;
|
|
1397
|
+
target.stampGen = source.stampGen;
|
|
1398
|
+
target.stamp.set(source.stamp);
|
|
1399
|
+
target.keysSpace.set(source.keysSpace);
|
|
1400
|
+
target.keysLevel.set(source.keysLevel);
|
|
1401
|
+
target.keysX.set(source.keysX);
|
|
1402
|
+
target.keysY.set(source.keysY);
|
|
1403
|
+
target.values.set(source.values);
|
|
1404
|
+
}
|
|
1405
|
+
function tileLocalToFieldUV(localCoord, innerSegments) {
|
|
1406
|
+
const edge = innerSegments + 3;
|
|
1407
|
+
return (localCoord * innerSegments + 1.5) / edge;
|
|
1408
|
+
}
|
|
1409
|
+
function createCpuTerrainCache(maxNodes, initialConfig) {
|
|
1410
|
+
let config = initialConfig;
|
|
1411
|
+
let edgeVertexCount = config.innerTileSegments + 3;
|
|
1412
|
+
let verticesPerNode = edgeVertexCount * edgeVertexCount;
|
|
1413
|
+
let totalElements = maxNodes * verticesPerNode;
|
|
1414
|
+
let frontElevation = new Float32Array(totalElements);
|
|
1415
|
+
let backElevation = new Float32Array(totalElements);
|
|
1416
|
+
let frontIndex = createSpatialIndex(maxNodes);
|
|
1417
|
+
let backIndex = createSpatialIndex(maxNodes);
|
|
1418
|
+
let frontTileBounds = new Float32Array(maxNodes * 2);
|
|
1419
|
+
let backTileBounds = new Float32Array(maxNodes * 2);
|
|
1420
|
+
let frontLeafCount = 0;
|
|
1421
|
+
let globalRange = null;
|
|
1422
|
+
let hasSnapshot = false;
|
|
1423
|
+
let readbackPending = false;
|
|
1424
|
+
let generationCount = 0;
|
|
1425
|
+
let lastScheduledStampGen = -1;
|
|
1426
|
+
const readHeight = (leafIndex, ix, iy) => {
|
|
1427
|
+
const base = leafIndex * verticesPerNode;
|
|
1428
|
+
return frontElevation[base + iy * edgeVertexCount + ix] ?? 0;
|
|
1429
|
+
};
|
|
1430
|
+
const sampleGridBilinear = (leafIndex, gx, gy) => {
|
|
1431
|
+
const max = edgeVertexCount - 1;
|
|
1432
|
+
const x = Math.max(0, Math.min(max, gx));
|
|
1433
|
+
const y = Math.max(0, Math.min(max, gy));
|
|
1434
|
+
const x0 = Math.floor(x);
|
|
1435
|
+
const y0 = Math.floor(y);
|
|
1436
|
+
const x1 = Math.min(max, x0 + 1);
|
|
1437
|
+
const y1 = Math.min(max, y0 + 1);
|
|
1438
|
+
const tx = x - x0;
|
|
1439
|
+
const ty = y - y0;
|
|
1440
|
+
const h00 = readHeight(leafIndex, x0, y0);
|
|
1441
|
+
const h10 = readHeight(leafIndex, x1, y0);
|
|
1442
|
+
const h01 = readHeight(leafIndex, x0, y1);
|
|
1443
|
+
const h11 = readHeight(leafIndex, x1, y1);
|
|
1444
|
+
const hx0 = h00 + (h10 - h00) * tx;
|
|
1445
|
+
const hx1 = h01 + (h11 - h01) * tx;
|
|
1446
|
+
return hx0 + (hx1 - hx0) * ty;
|
|
1447
|
+
};
|
|
1448
|
+
const computeNormal = (leafIndex, gx, gy, tileSize) => {
|
|
1449
|
+
const hLeft = sampleGridBilinear(leafIndex, gx - 1, gy);
|
|
1450
|
+
const hRight = sampleGridBilinear(leafIndex, gx + 1, gy);
|
|
1451
|
+
const hUp = sampleGridBilinear(leafIndex, gx, gy - 1);
|
|
1452
|
+
const hDown = sampleGridBilinear(leafIndex, gx, gy + 1);
|
|
1453
|
+
const stepWorld = tileSize / config.innerTileSegments;
|
|
1454
|
+
const inv2Step = 0.5 / stepWorld;
|
|
1455
|
+
const dhdx = (hRight - hLeft) * config.elevationScale * inv2Step;
|
|
1456
|
+
const dhdz = (hDown - hUp) * config.elevationScale * inv2Step;
|
|
1457
|
+
return new Vector3(-dhdx, 1, -dhdz).normalize();
|
|
1458
|
+
};
|
|
1459
|
+
const lookupTile = (worldX, worldZ) => {
|
|
1460
|
+
const halfRoot = config.rootSize * 0.5;
|
|
1461
|
+
for (let level = config.maxLevel; level >= 0; level -= 1) {
|
|
1462
|
+
const scale = 2 ** level;
|
|
1463
|
+
const tileSize = config.rootSize / scale;
|
|
1464
|
+
const tileX = Math.floor((worldX - config.originX + halfRoot) / tileSize);
|
|
1465
|
+
const tileY = Math.floor((worldZ - config.originZ + halfRoot) / tileSize);
|
|
1466
|
+
const leafIndex = lookupSpatialIndexRaw(
|
|
1467
|
+
frontIndex,
|
|
1468
|
+
0,
|
|
1469
|
+
level,
|
|
1470
|
+
tileX,
|
|
1471
|
+
tileY
|
|
1472
|
+
);
|
|
1473
|
+
if (leafIndex !== U32_EMPTY) {
|
|
1474
|
+
const tileMinX = config.originX + tileX * tileSize - halfRoot;
|
|
1475
|
+
const tileMinZ = config.originZ + tileY * tileSize - halfRoot;
|
|
1476
|
+
return {
|
|
1477
|
+
found: true,
|
|
1478
|
+
leafIndex,
|
|
1479
|
+
level,
|
|
1480
|
+
tileX,
|
|
1481
|
+
tileY,
|
|
1482
|
+
tileSize,
|
|
1483
|
+
localU: (worldX - tileMinX) / tileSize,
|
|
1484
|
+
localV: (worldZ - tileMinZ) / tileSize
|
|
1485
|
+
};
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
return {
|
|
1489
|
+
found: false,
|
|
1490
|
+
leafIndex: -1,
|
|
1491
|
+
level: -1,
|
|
1492
|
+
tileX: -1,
|
|
1493
|
+
tileY: -1,
|
|
1494
|
+
tileSize: 0,
|
|
1495
|
+
localU: 0,
|
|
1496
|
+
localV: 0
|
|
1497
|
+
};
|
|
1498
|
+
};
|
|
1499
|
+
const sampleFromLookup = (lookup) => {
|
|
1500
|
+
const fieldU = tileLocalToFieldUV(lookup.localU, config.innerTileSegments);
|
|
1501
|
+
const fieldV = tileLocalToFieldUV(lookup.localV, config.innerTileSegments);
|
|
1502
|
+
const gx = fieldU * (edgeVertexCount - 1);
|
|
1503
|
+
const gy = fieldV * (edgeVertexCount - 1);
|
|
1504
|
+
const height = sampleGridBilinear(lookup.leafIndex, gx, gy);
|
|
1505
|
+
const scaledHeight = config.originY + height * config.elevationScale;
|
|
1506
|
+
const normal = computeNormal(lookup.leafIndex, gx, gy, lookup.tileSize);
|
|
1507
|
+
return { elevation: scaledHeight, normal, valid: true };
|
|
1508
|
+
};
|
|
1509
|
+
const sampleElevationFromLookup = (lookup) => {
|
|
1510
|
+
const fieldU = tileLocalToFieldUV(lookup.localU, config.innerTileSegments);
|
|
1511
|
+
const fieldV = tileLocalToFieldUV(lookup.localV, config.innerTileSegments);
|
|
1512
|
+
const gx = fieldU * (edgeVertexCount - 1);
|
|
1513
|
+
const gy = fieldV * (edgeVertexCount - 1);
|
|
1514
|
+
const height = sampleGridBilinear(lookup.leafIndex, gx, gy);
|
|
1515
|
+
const scaledHeight = config.originY + height * config.elevationScale;
|
|
1516
|
+
return { elevation: scaledHeight, valid: true };
|
|
1517
|
+
};
|
|
1518
|
+
const sampleTerrain = (worldX, worldZ) => {
|
|
1519
|
+
if (!hasSnapshot) {
|
|
1520
|
+
return { elevation: 0, normal: new Vector3(0, 1, 0), valid: false };
|
|
1521
|
+
}
|
|
1522
|
+
const lookup = lookupTile(worldX, worldZ);
|
|
1523
|
+
if (!lookup.found) {
|
|
1524
|
+
return { elevation: 0, normal: new Vector3(0, 1, 0), valid: false };
|
|
1525
|
+
}
|
|
1526
|
+
return sampleFromLookup(lookup);
|
|
1527
|
+
};
|
|
1528
|
+
const getElevation = (worldX, worldZ) => {
|
|
1529
|
+
if (!hasSnapshot) {
|
|
1530
|
+
return { elevation: 0, valid: false };
|
|
1531
|
+
}
|
|
1532
|
+
const lookup = lookupTile(worldX, worldZ);
|
|
1533
|
+
if (!lookup.found) {
|
|
1534
|
+
return { elevation: 0, valid: false };
|
|
1535
|
+
}
|
|
1536
|
+
return sampleElevationFromLookup(lookup);
|
|
1537
|
+
};
|
|
1538
|
+
const api = {
|
|
1539
|
+
get generation() {
|
|
1540
|
+
return generationCount;
|
|
1541
|
+
},
|
|
1542
|
+
get ready() {
|
|
1543
|
+
return hasSnapshot;
|
|
1544
|
+
},
|
|
1545
|
+
updateConfig(nextConfig) {
|
|
1546
|
+
config = nextConfig;
|
|
1547
|
+
edgeVertexCount = config.innerTileSegments + 3;
|
|
1548
|
+
verticesPerNode = edgeVertexCount * edgeVertexCount;
|
|
1549
|
+
totalElements = maxNodes * verticesPerNode;
|
|
1550
|
+
},
|
|
1551
|
+
triggerReadback(renderer, attribute, spatialIndex, boundsAttribute, activeLeafCount) {
|
|
1552
|
+
if (readbackPending) return;
|
|
1553
|
+
const withReadback = renderer;
|
|
1554
|
+
if (!withReadback.getArrayBufferAsync) return;
|
|
1555
|
+
if (spatialIndex.stampGen === lastScheduledStampGen) return;
|
|
1556
|
+
cloneSpatialIndex(backIndex, spatialIndex);
|
|
1557
|
+
lastScheduledStampGen = spatialIndex.stampGen;
|
|
1558
|
+
const capturedLeafCount = activeLeafCount ?? 0;
|
|
1559
|
+
const capturedScale = config.elevationScale;
|
|
1560
|
+
const capturedOriginY = config.originY;
|
|
1561
|
+
readbackPending = true;
|
|
1562
|
+
const elevationPromise = withReadback.getArrayBufferAsync(attribute);
|
|
1563
|
+
const boundsPromise = boundsAttribute ? withReadback.getArrayBufferAsync(boundsAttribute) : null;
|
|
1564
|
+
const onComplete = (elevResult, boundsResult) => {
|
|
1565
|
+
const data = new Float32Array(elevResult);
|
|
1566
|
+
backElevation.fill(0);
|
|
1567
|
+
backElevation.set(data.subarray(0, totalElements));
|
|
1568
|
+
let boundsValid = capturedLeafCount === 0;
|
|
1569
|
+
if (boundsResult) {
|
|
1570
|
+
const rawBounds = new Float32Array(boundsResult);
|
|
1571
|
+
backTileBounds.fill(0);
|
|
1572
|
+
backTileBounds.set(rawBounds.subarray(0, capturedLeafCount * 2));
|
|
1573
|
+
for (let i = 0; i < capturedLeafCount; i += 1) {
|
|
1574
|
+
if ((rawBounds[i * 2 + 1] ?? 0) !== 0) {
|
|
1575
|
+
boundsValid = true;
|
|
1576
|
+
break;
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
const oldFrontElevation = frontElevation;
|
|
1581
|
+
const oldFrontIndex = frontIndex;
|
|
1582
|
+
frontElevation = backElevation;
|
|
1583
|
+
frontIndex = backIndex;
|
|
1584
|
+
frontLeafCount = capturedLeafCount;
|
|
1585
|
+
backElevation = oldFrontElevation;
|
|
1586
|
+
backIndex = oldFrontIndex;
|
|
1587
|
+
if (boundsResult && boundsValid) {
|
|
1588
|
+
const oldFrontBounds = frontTileBounds;
|
|
1589
|
+
frontTileBounds = backTileBounds;
|
|
1590
|
+
backTileBounds = oldFrontBounds;
|
|
1591
|
+
}
|
|
1592
|
+
if (boundsResult && boundsValid && capturedLeafCount > 0) {
|
|
1593
|
+
let gMin = Infinity;
|
|
1594
|
+
let gMax = -Infinity;
|
|
1595
|
+
for (let i = 0; i < capturedLeafCount; i++) {
|
|
1596
|
+
const rawMin = frontTileBounds[i * 2];
|
|
1597
|
+
const rawMax = frontTileBounds[i * 2 + 1];
|
|
1598
|
+
const a = capturedOriginY + rawMin * capturedScale;
|
|
1599
|
+
const b = capturedOriginY + rawMax * capturedScale;
|
|
1600
|
+
gMin = Math.min(gMin, a, b);
|
|
1601
|
+
gMax = Math.max(gMax, a, b);
|
|
1602
|
+
}
|
|
1603
|
+
globalRange = { min: gMin, max: gMax };
|
|
1604
|
+
}
|
|
1605
|
+
hasSnapshot = true;
|
|
1606
|
+
generationCount += 1;
|
|
1607
|
+
};
|
|
1608
|
+
if (boundsPromise) {
|
|
1609
|
+
Promise.all([elevationPromise, boundsPromise]).then(([elev, bounds]) => onComplete(elev, bounds)).finally(() => {
|
|
1610
|
+
readbackPending = false;
|
|
1611
|
+
});
|
|
1612
|
+
} else {
|
|
1613
|
+
elevationPromise.then((elev) => onComplete(elev, null)).finally(() => {
|
|
1614
|
+
readbackPending = false;
|
|
1615
|
+
});
|
|
1616
|
+
}
|
|
1617
|
+
},
|
|
1618
|
+
getElevation(worldX, worldZ) {
|
|
1619
|
+
const sample = getElevation(worldX, worldZ);
|
|
1620
|
+
return sample.valid ? sample.elevation : null;
|
|
1621
|
+
},
|
|
1622
|
+
getNormal(worldX, worldZ) {
|
|
1623
|
+
return sampleTerrain(worldX, worldZ).normal;
|
|
1624
|
+
},
|
|
1625
|
+
getTile(worldX, worldZ) {
|
|
1626
|
+
if (!hasSnapshot) return null;
|
|
1627
|
+
const lookup = lookupTile(worldX, worldZ);
|
|
1628
|
+
if (!lookup.found) return null;
|
|
1629
|
+
return {
|
|
1630
|
+
level: lookup.level,
|
|
1631
|
+
x: lookup.tileX,
|
|
1632
|
+
y: lookup.tileY,
|
|
1633
|
+
index: lookup.leafIndex
|
|
1634
|
+
};
|
|
1635
|
+
},
|
|
1636
|
+
getTileBounds(worldX, worldZ) {
|
|
1637
|
+
if (!hasSnapshot) return null;
|
|
1638
|
+
const lookup = lookupTile(worldX, worldZ);
|
|
1639
|
+
if (!lookup.found || lookup.leafIndex >= frontLeafCount) return null;
|
|
1640
|
+
const rawMin = frontTileBounds[lookup.leafIndex * 2];
|
|
1641
|
+
const rawMax = frontTileBounds[lookup.leafIndex * 2 + 1];
|
|
1642
|
+
const a = config.originY + rawMin * config.elevationScale;
|
|
1643
|
+
const b = config.originY + rawMax * config.elevationScale;
|
|
1644
|
+
return {
|
|
1645
|
+
level: lookup.level,
|
|
1646
|
+
x: lookup.tileX,
|
|
1647
|
+
y: lookup.tileY,
|
|
1648
|
+
index: lookup.leafIndex,
|
|
1649
|
+
minElevation: Math.min(a, b),
|
|
1650
|
+
maxElevation: Math.max(a, b)
|
|
1651
|
+
};
|
|
1652
|
+
},
|
|
1653
|
+
getGlobalElevationRange() {
|
|
1654
|
+
return globalRange;
|
|
1655
|
+
},
|
|
1656
|
+
sampleTerrainBatch(positions) {
|
|
1657
|
+
const count = Math.floor(positions.length / 2);
|
|
1658
|
+
const elevations = new Float32Array(count);
|
|
1659
|
+
const normals = new Float32Array(count * 3);
|
|
1660
|
+
const valid = new Uint8Array(count);
|
|
1661
|
+
if (!hasSnapshot) {
|
|
1662
|
+
return { elevations, normals, valid, generation: generationCount };
|
|
1663
|
+
}
|
|
1664
|
+
let lastTile;
|
|
1665
|
+
for (let i = 0; i < count; i += 1) {
|
|
1666
|
+
const worldX = positions[i * 2] ?? 0;
|
|
1667
|
+
const worldZ = positions[i * 2 + 1] ?? 0;
|
|
1668
|
+
let lookup;
|
|
1669
|
+
if (lastTile && worldX >= lastTile.tileMinX && worldX <= lastTile.tileMinX + lastTile.tileSize && worldZ >= lastTile.tileMinZ && worldZ <= lastTile.tileMinZ + lastTile.tileSize) {
|
|
1670
|
+
lookup = {
|
|
1671
|
+
found: true,
|
|
1672
|
+
leafIndex: lastTile.leafIndex,
|
|
1673
|
+
level: lastTile.level,
|
|
1674
|
+
tileX: lastTile.tileX,
|
|
1675
|
+
tileY: lastTile.tileY,
|
|
1676
|
+
tileSize: lastTile.tileSize,
|
|
1677
|
+
localU: (worldX - lastTile.tileMinX) / lastTile.tileSize,
|
|
1678
|
+
localV: (worldZ - lastTile.tileMinZ) / lastTile.tileSize
|
|
1679
|
+
};
|
|
1680
|
+
} else {
|
|
1681
|
+
lookup = lookupTile(worldX, worldZ);
|
|
1682
|
+
if (lookup.found) {
|
|
1683
|
+
lastTile = {
|
|
1684
|
+
leafIndex: lookup.leafIndex,
|
|
1685
|
+
level: lookup.level,
|
|
1686
|
+
tileX: lookup.tileX,
|
|
1687
|
+
tileY: lookup.tileY,
|
|
1688
|
+
tileSize: lookup.tileSize,
|
|
1689
|
+
tileMinX: worldX - lookup.localU * lookup.tileSize,
|
|
1690
|
+
tileMinZ: worldZ - lookup.localV * lookup.tileSize
|
|
1691
|
+
};
|
|
1692
|
+
} else {
|
|
1693
|
+
lastTile = void 0;
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
if (!lookup?.found) {
|
|
1697
|
+
normals[i * 3 + 1] = 1;
|
|
1698
|
+
continue;
|
|
1699
|
+
}
|
|
1700
|
+
const sample = sampleFromLookup(lookup);
|
|
1701
|
+
elevations[i] = sample.elevation;
|
|
1702
|
+
normals[i * 3] = sample.normal.x;
|
|
1703
|
+
normals[i * 3 + 1] = sample.normal.y;
|
|
1704
|
+
normals[i * 3 + 2] = sample.normal.z;
|
|
1705
|
+
valid[i] = 1;
|
|
1706
|
+
}
|
|
1707
|
+
return { elevations, normals, valid, generation: generationCount };
|
|
1708
|
+
},
|
|
1709
|
+
sampleTerrain
|
|
1710
|
+
};
|
|
1711
|
+
return api;
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
function createTerrainQuery(cache) {
|
|
1715
|
+
return {
|
|
1716
|
+
get generation() {
|
|
1717
|
+
return cache.generation;
|
|
1718
|
+
},
|
|
1719
|
+
getElevation(worldX, worldZ) {
|
|
1720
|
+
return cache.getElevation(worldX, worldZ);
|
|
1721
|
+
},
|
|
1722
|
+
getNormal(worldX, worldZ) {
|
|
1723
|
+
return cache.getNormal(worldX, worldZ);
|
|
1724
|
+
},
|
|
1725
|
+
getTile(worldX, worldZ) {
|
|
1726
|
+
return cache.getTile(worldX, worldZ);
|
|
1727
|
+
},
|
|
1728
|
+
getTileBounds(worldX, worldZ) {
|
|
1729
|
+
return cache.getTileBounds(worldX, worldZ);
|
|
1730
|
+
},
|
|
1731
|
+
getGlobalElevationRange() {
|
|
1732
|
+
return cache.getGlobalElevationRange();
|
|
1733
|
+
},
|
|
1734
|
+
sampleTerrain(worldX, worldZ) {
|
|
1735
|
+
return cache.sampleTerrain(worldX, worldZ);
|
|
1736
|
+
},
|
|
1737
|
+
sampleTerrainBatch(positions) {
|
|
1738
|
+
return cache.sampleTerrainBatch(positions);
|
|
1739
|
+
}
|
|
1740
|
+
};
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
const WGSIZE = 64;
|
|
1744
|
+
function buildReductionKernel(elevationFieldNode, boundsNode, verticesPerNode) {
|
|
1745
|
+
const elemsPerThread = Math.ceil(verticesPerNode / WGSIZE);
|
|
1746
|
+
return Fn(() => {
|
|
1747
|
+
const sharedMin = workgroupArray("float", WGSIZE);
|
|
1748
|
+
const sharedMax = workgroupArray("float", WGSIZE);
|
|
1749
|
+
const tid = int(localId.x);
|
|
1750
|
+
const tileIdx = int(workgroupId.z);
|
|
1751
|
+
const baseOffset = tileIdx.mul(int(verticesPerNode));
|
|
1752
|
+
const start = tid.mul(int(elemsPerThread));
|
|
1753
|
+
const end = min(start.add(int(elemsPerThread)), int(verticesPerNode));
|
|
1754
|
+
const localMin = float(1e10).toVar("localMin");
|
|
1755
|
+
const localMax = float(-1e10).toVar("localMax");
|
|
1756
|
+
Loop({ start, end, type: "int", condition: "<" }, ({ i }) => {
|
|
1757
|
+
const h = elevationFieldNode.element(baseOffset.add(i));
|
|
1758
|
+
localMin.assign(min(localMin, h));
|
|
1759
|
+
localMax.assign(max(localMax, h));
|
|
1760
|
+
});
|
|
1761
|
+
sharedMin.element(tid).assign(localMin);
|
|
1762
|
+
sharedMax.element(tid).assign(localMax);
|
|
1763
|
+
workgroupBarrier();
|
|
1764
|
+
If(tid.equal(int(0)), () => {
|
|
1765
|
+
const finalMin = float(1e10).toVar("finalMin");
|
|
1766
|
+
const finalMax = float(-1e10).toVar("finalMax");
|
|
1767
|
+
Loop(WGSIZE, ({ i }) => {
|
|
1768
|
+
finalMin.assign(min(finalMin, sharedMin.element(i)));
|
|
1769
|
+
finalMax.assign(max(finalMax, sharedMax.element(i)));
|
|
1770
|
+
});
|
|
1771
|
+
const outIdx = tileIdx.mul(int(2));
|
|
1772
|
+
boundsNode.element(outIdx).assign(finalMin);
|
|
1773
|
+
boundsNode.element(outIdx.add(int(1))).assign(finalMax);
|
|
1774
|
+
});
|
|
1775
|
+
})().computeKernel([WGSIZE, 1, 1]);
|
|
1776
|
+
}
|
|
1777
|
+
const tileBoundsContextTask = task((get, work) => {
|
|
1778
|
+
const elevationFieldContext = get(createElevationFieldContextTask);
|
|
1779
|
+
const maxNodesValue = get(maxNodes);
|
|
1780
|
+
const edgeVertexCount = get(innerTileSegments) + 3;
|
|
1781
|
+
return work(() => {
|
|
1782
|
+
const data = new Float32Array(maxNodesValue * 2);
|
|
1783
|
+
const attribute = new StorageBufferAttribute(data, 1);
|
|
1784
|
+
const node = storage(attribute, "float", maxNodesValue * 2);
|
|
1785
|
+
const verticesPerNode = edgeVertexCount * edgeVertexCount;
|
|
1786
|
+
const kernel = buildReductionKernel(elevationFieldContext.node, node, verticesPerNode);
|
|
1787
|
+
return { data, attribute, node, kernel };
|
|
1788
|
+
});
|
|
1789
|
+
}).displayName("tileBoundsContextTask");
|
|
1790
|
+
const tileBoundsReductionTask = task(
|
|
1791
|
+
(get, work, { resources }) => {
|
|
1792
|
+
get(executeComputeTask);
|
|
1793
|
+
const boundsContext = get(tileBoundsContextTask);
|
|
1794
|
+
const leafState = get(leafGpuBufferTask);
|
|
1795
|
+
return work(() => {
|
|
1796
|
+
if (resources?.renderer && leafState.count > 0) {
|
|
1797
|
+
resources.renderer.compute(boundsContext.kernel, [1, 1, leafState.count]);
|
|
1798
|
+
}
|
|
1799
|
+
return boundsContext;
|
|
1800
|
+
});
|
|
1801
|
+
}
|
|
1802
|
+
).displayName("tileBoundsReductionTask").lane("gpu");
|
|
1803
|
+
|
|
1804
|
+
const terrainQueryTask = task((get, work) => {
|
|
1805
|
+
const maxNodesValue = get(maxNodes);
|
|
1806
|
+
const innerTileSegmentsValue = get(innerTileSegments);
|
|
1807
|
+
const maxLevelValue = get(maxLevel);
|
|
1808
|
+
const rootSizeValue = get(rootSize);
|
|
1809
|
+
const originValue = get(origin);
|
|
1810
|
+
const elevationScaleValue = get(elevationScale);
|
|
1811
|
+
return work((prev) => {
|
|
1812
|
+
const shapeKey = `${maxNodesValue}:${innerTileSegmentsValue}`;
|
|
1813
|
+
const configValues = {
|
|
1814
|
+
rootSize: rootSizeValue,
|
|
1815
|
+
originX: originValue.x,
|
|
1816
|
+
originY: originValue.y,
|
|
1817
|
+
originZ: originValue.z,
|
|
1818
|
+
innerTileSegments: innerTileSegmentsValue,
|
|
1819
|
+
elevationScale: elevationScaleValue,
|
|
1820
|
+
maxLevel: maxLevelValue
|
|
1821
|
+
};
|
|
1822
|
+
let cache = prev?.cache;
|
|
1823
|
+
let query = prev?.query;
|
|
1824
|
+
if (!cache || !query || prev?.shapeKey !== shapeKey) {
|
|
1825
|
+
cache = createCpuTerrainCache(maxNodesValue, configValues);
|
|
1826
|
+
query = createTerrainQuery(cache);
|
|
1827
|
+
}
|
|
1828
|
+
cache.updateConfig(configValues);
|
|
1829
|
+
return { cache, query, shapeKey };
|
|
1830
|
+
});
|
|
1831
|
+
}).displayName("terrainQueryTask");
|
|
1832
|
+
const terrainReadbackTask = task(
|
|
1833
|
+
(get, work, { resources }) => {
|
|
1834
|
+
const boundsContext = get(tileBoundsReductionTask);
|
|
1835
|
+
const elevationFieldContext = get(createElevationFieldContextTask);
|
|
1836
|
+
const quadtreeConfig = get(quadtreeConfigTask);
|
|
1837
|
+
const leafState = get(leafGpuBufferTask);
|
|
1838
|
+
const { cache } = get(terrainQueryTask);
|
|
1839
|
+
return work(() => {
|
|
1840
|
+
if (!resources?.renderer) return;
|
|
1841
|
+
cache.triggerReadback(
|
|
1842
|
+
resources.renderer,
|
|
1843
|
+
elevationFieldContext.attribute,
|
|
1844
|
+
quadtreeConfig.state.leafIndex,
|
|
1845
|
+
boundsContext.attribute,
|
|
1846
|
+
leafState.count
|
|
1847
|
+
);
|
|
1848
|
+
});
|
|
1849
|
+
}
|
|
1850
|
+
).displayName("terrainReadbackTask").lane("gpu");
|
|
1851
|
+
|
|
1852
|
+
const surfaceTask = task((get, work) => {
|
|
1853
|
+
const customSurface = get(surface);
|
|
1854
|
+
const rootSizeVal = get(rootSize);
|
|
1855
|
+
const originVal = get(origin);
|
|
1856
|
+
return work(() => {
|
|
1857
|
+
if (customSurface) return customSurface;
|
|
1858
|
+
return createFlatSurface({ rootSize: rootSizeVal, origin: originVal });
|
|
1859
|
+
});
|
|
1860
|
+
}).displayName("surfaceTask");
|
|
1861
|
+
const quadtreeConfigTask = task((get, work) => {
|
|
1862
|
+
const surfaceVal = get(surfaceTask);
|
|
1863
|
+
const maxNodesVal = get(maxNodes);
|
|
1864
|
+
const maxLevelVal = get(maxLevel);
|
|
1865
|
+
return work(() => {
|
|
1866
|
+
const state = createState({ maxNodes: maxNodesVal, maxLevel: maxLevelVal }, surfaceVal);
|
|
1867
|
+
return {
|
|
1868
|
+
state,
|
|
1869
|
+
surface: surfaceVal
|
|
1870
|
+
};
|
|
1871
|
+
});
|
|
1872
|
+
}).displayName("quadtreeConfigTask");
|
|
1873
|
+
const quadtreeUpdateTask = task((get, work) => {
|
|
1874
|
+
const quadtreeConfig = get(quadtreeConfigTask);
|
|
1875
|
+
const quadtreeUpdateConfig = get(quadtreeUpdate);
|
|
1876
|
+
const { query: terrainQuery } = get(terrainQueryTask);
|
|
1877
|
+
let outLeaves = void 0;
|
|
1878
|
+
return work(() => {
|
|
1879
|
+
const cam = quadtreeUpdateConfig.cameraOrigin;
|
|
1880
|
+
quadtreeUpdateConfig.elevationAtCameraXZ = terrainQuery.getElevation(cam.x, cam.z) ?? 0;
|
|
1881
|
+
outLeaves = update(
|
|
1882
|
+
quadtreeConfig.state,
|
|
1883
|
+
quadtreeConfig.surface,
|
|
1884
|
+
quadtreeUpdateConfig,
|
|
1885
|
+
outLeaves
|
|
1886
|
+
);
|
|
1887
|
+
return outLeaves;
|
|
1888
|
+
});
|
|
1889
|
+
}).displayName("quadtreeUpdateTask");
|
|
1890
|
+
const leafStorageTask = task((get, work) => {
|
|
1891
|
+
const maxNodesVal = get(maxNodes);
|
|
1892
|
+
return work(() => createLeafStorage(maxNodesVal));
|
|
1893
|
+
}).displayName("leafStorageTask");
|
|
1894
|
+
const leafGpuBufferTask = task((get, work) => {
|
|
1895
|
+
const leafSet = get(quadtreeUpdateTask);
|
|
1896
|
+
const leafStorage = get(leafStorageTask);
|
|
1897
|
+
return work(() => {
|
|
1898
|
+
const bufferCapacity = leafStorage.data.length / 4;
|
|
1899
|
+
const leafCount = Math.min(leafSet.count, bufferCapacity);
|
|
1900
|
+
for (let i = 0; i < leafCount; i += 1) {
|
|
1901
|
+
const offset = i * 4;
|
|
1902
|
+
leafStorage.data[offset] = leafSet.level[i] ?? 0;
|
|
1903
|
+
leafStorage.data[offset + 1] = leafSet.x[i] ?? 0;
|
|
1904
|
+
leafStorage.data[offset + 2] = leafSet.y[i] ?? 0;
|
|
1905
|
+
leafStorage.data[offset + 3] = 1;
|
|
1906
|
+
}
|
|
1907
|
+
leafStorage.attribute.needsUpdate = true;
|
|
1908
|
+
leafStorage.node.needsUpdate = true;
|
|
1909
|
+
return {
|
|
1910
|
+
count: leafCount,
|
|
1911
|
+
data: leafStorage.data,
|
|
1912
|
+
attribute: leafStorage.attribute,
|
|
1913
|
+
node: leafStorage.node
|
|
1914
|
+
};
|
|
1915
|
+
});
|
|
1916
|
+
}).displayName("leafGpuBufferTask");
|
|
1917
|
+
|
|
1918
|
+
function createElevationFunction(callback) {
|
|
1919
|
+
const tslFunction = (args) => {
|
|
1920
|
+
const params = {
|
|
1921
|
+
worldPosition: args.worldPosition,
|
|
1922
|
+
rootSize: args.rootSize,
|
|
1923
|
+
rootUV: args.rootUV,
|
|
1924
|
+
tileUV: args.tileUV,
|
|
1925
|
+
tileLevel: args.tileLevel,
|
|
1926
|
+
tileSize: args.tileSize,
|
|
1927
|
+
tileOriginVec2: args.tileOriginVec2,
|
|
1928
|
+
nodeIndex: args.nodeIndex
|
|
1929
|
+
};
|
|
1930
|
+
return callback(params);
|
|
1931
|
+
};
|
|
1932
|
+
return Fn$1(tslFunction);
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
function createTerrainUniforms(params) {
|
|
1936
|
+
const sanitizedId = params.instanceId?.replace(/-/g, "_");
|
|
1937
|
+
const suffix = sanitizedId ? `_${sanitizedId}` : "";
|
|
1938
|
+
const uRootOrigin = uniform(
|
|
1939
|
+
new Vector3$1(params.rootOrigin.x, params.rootOrigin.y, params.rootOrigin.z)
|
|
1940
|
+
).setName(`uRootOrigin${suffix}`);
|
|
1941
|
+
const uRootSize = uniform(float(params.rootSize)).setName(`uRootSize${suffix}`);
|
|
1942
|
+
const uInnerTileSegments = uniform(int(params.innerTileSegments)).setName(
|
|
1943
|
+
`uInnerTileSegments${suffix}`
|
|
1944
|
+
);
|
|
1945
|
+
const uSkirtScale = uniform(float(params.skirtScale)).setName(`uSkirtScale${suffix}`);
|
|
1946
|
+
const uElevationScale = uniform(float(params.elevationScale)).setName(`uElevationScale${suffix}`);
|
|
1947
|
+
return {
|
|
1948
|
+
uRootOrigin,
|
|
1949
|
+
uRootSize,
|
|
1950
|
+
uInnerTileSegments,
|
|
1951
|
+
uSkirtScale,
|
|
1952
|
+
uElevationScale
|
|
1953
|
+
};
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
const instanceIdTask = task(() => crypto.randomUUID()).displayName("instanceIdTask").cache("once");
|
|
1957
|
+
|
|
1958
|
+
const scratchVector3 = new Vector3();
|
|
1959
|
+
const createUniformsTask = task((get, work) => {
|
|
1960
|
+
const uniformParams = {
|
|
1961
|
+
rootOrigin: get(origin),
|
|
1962
|
+
rootSize: get(rootSize),
|
|
1963
|
+
innerTileSegments: get(innerTileSegments),
|
|
1964
|
+
skirtScale: get(skirtScale),
|
|
1965
|
+
elevationScale: get(elevationScale),
|
|
1966
|
+
instanceId: get(instanceIdTask)
|
|
1967
|
+
};
|
|
1968
|
+
return work(() => createTerrainUniforms(uniformParams));
|
|
1969
|
+
}).displayName("createUniformsTask").cache("once");
|
|
1970
|
+
const updateUniformsTask = task((get, work) => {
|
|
1971
|
+
const terrainUniformsContext = get(createUniformsTask);
|
|
1972
|
+
const rootSizeVal = get(rootSize);
|
|
1973
|
+
const rootOrigin = get(origin);
|
|
1974
|
+
const innerTileSegmentsVal = get(innerTileSegments);
|
|
1975
|
+
const skirtScaleVal = get(skirtScale);
|
|
1976
|
+
const elevationScaleVal = get(elevationScale);
|
|
1977
|
+
return work(() => {
|
|
1978
|
+
terrainUniformsContext.uRootSize.value = rootSizeVal;
|
|
1979
|
+
terrainUniformsContext.uRootOrigin.value = scratchVector3.set(
|
|
1980
|
+
rootOrigin.x,
|
|
1981
|
+
rootOrigin.y,
|
|
1982
|
+
rootOrigin.z
|
|
1983
|
+
);
|
|
1984
|
+
terrainUniformsContext.uInnerTileSegments.value = innerTileSegmentsVal;
|
|
1985
|
+
terrainUniformsContext.uSkirtScale.value = skirtScaleVal;
|
|
1986
|
+
terrainUniformsContext.uElevationScale.value = elevationScaleVal;
|
|
1987
|
+
return terrainUniformsContext;
|
|
1988
|
+
});
|
|
1989
|
+
}).displayName("updateUniformsTask");
|
|
1990
|
+
|
|
1991
|
+
const createElevationFieldContextTask = task((get, work) => {
|
|
1992
|
+
const edgeVertexCount = get(innerTileSegments) + 3;
|
|
1993
|
+
const verticesPerNode = edgeVertexCount * edgeVertexCount;
|
|
1994
|
+
const totalElements = get(maxNodes) * verticesPerNode;
|
|
1995
|
+
return work(() => {
|
|
1996
|
+
const data = new Float32Array(totalElements);
|
|
1997
|
+
const attribute = new StorageBufferAttribute(data, 1);
|
|
1998
|
+
const node = storage(attribute, "float", totalElements);
|
|
1999
|
+
return {
|
|
2000
|
+
data,
|
|
2001
|
+
attribute,
|
|
2002
|
+
node
|
|
2003
|
+
};
|
|
2004
|
+
});
|
|
2005
|
+
}).displayName("createElevationFieldContextTask");
|
|
2006
|
+
const tileNodesTask = task((get, work) => {
|
|
2007
|
+
const leafStorage = get(leafStorageTask);
|
|
2008
|
+
const uniforms = get(updateUniformsTask);
|
|
2009
|
+
return work(() => {
|
|
2010
|
+
return createTileCompute(leafStorage, uniforms);
|
|
2011
|
+
});
|
|
2012
|
+
}).displayName("tileNodesTask");
|
|
2013
|
+
const elevationFieldStageTask = task((get, work) => {
|
|
2014
|
+
const tile = get(tileNodesTask);
|
|
2015
|
+
const uniforms = get(updateUniformsTask);
|
|
2016
|
+
const elevationFieldContext = get(createElevationFieldContextTask);
|
|
2017
|
+
const userElevationFn = get(elevationFn);
|
|
2018
|
+
return work(() => {
|
|
2019
|
+
const heightFn = createElevationFunction(userElevationFn);
|
|
2020
|
+
const heightWriteFn = createElevation(tile, uniforms, heightFn);
|
|
2021
|
+
return [
|
|
2022
|
+
(nodeIndex, globalVertexIndex, _uv, localCoordinates) => {
|
|
2023
|
+
const height = heightWriteFn(nodeIndex, localCoordinates);
|
|
2024
|
+
elevationFieldContext.node.element(globalVertexIndex).assign(height);
|
|
2025
|
+
}
|
|
2026
|
+
];
|
|
2027
|
+
});
|
|
2028
|
+
}).displayName("elevationFieldStageTask");
|
|
2029
|
+
|
|
2030
|
+
const createTerrainFieldTextureTask = task(
|
|
2031
|
+
(get, work, { resources }) => {
|
|
2032
|
+
const edgeVertexCount = get(innerTileSegments) + 3;
|
|
2033
|
+
const maxNodesValue = get(maxNodes);
|
|
2034
|
+
const filter = get(terrainFieldFilter);
|
|
2035
|
+
return work(
|
|
2036
|
+
() => createTerrainFieldStorage(
|
|
2037
|
+
edgeVertexCount,
|
|
2038
|
+
maxNodesValue,
|
|
2039
|
+
resources?.renderer,
|
|
2040
|
+
{ filter }
|
|
2041
|
+
)
|
|
2042
|
+
);
|
|
2043
|
+
}
|
|
2044
|
+
).displayName("createTerrainFieldTextureTask");
|
|
2045
|
+
function createNormalFromElevationField(elevationFieldNode, edgeVertexCount) {
|
|
2046
|
+
return Fn(
|
|
2047
|
+
([nodeIndex, tileSize, ix, iy, elevationScale]) => {
|
|
2048
|
+
const iEdge = int(edgeVertexCount);
|
|
2049
|
+
const verticesPerNode = iEdge.mul(iEdge);
|
|
2050
|
+
const baseOffset = int(nodeIndex).mul(verticesPerNode);
|
|
2051
|
+
const xLeft = int(ix).sub(int(1));
|
|
2052
|
+
const xRight = int(ix).add(int(1));
|
|
2053
|
+
const yUp = int(iy).sub(int(1));
|
|
2054
|
+
const yDown = int(iy).add(int(1));
|
|
2055
|
+
const hLeft = elevationFieldNode.element(baseOffset.add(int(iy).mul(iEdge).add(xLeft))).mul(elevationScale);
|
|
2056
|
+
const hRight = elevationFieldNode.element(baseOffset.add(int(iy).mul(iEdge).add(xRight))).mul(elevationScale);
|
|
2057
|
+
const hUp = elevationFieldNode.element(baseOffset.add(yUp.mul(iEdge).add(int(ix)))).mul(elevationScale);
|
|
2058
|
+
const hDown = elevationFieldNode.element(baseOffset.add(yDown.mul(iEdge).add(int(ix)))).mul(elevationScale);
|
|
2059
|
+
const innerSegments = float(iEdge).sub(float(3));
|
|
2060
|
+
const stepWorld = tileSize.div(innerSegments);
|
|
2061
|
+
const inv2Step = float(0.5).div(stepWorld);
|
|
2062
|
+
const dhdx = float(hRight).sub(float(hLeft)).mul(inv2Step);
|
|
2063
|
+
const dhdz = float(hDown).sub(float(hUp)).mul(inv2Step);
|
|
2064
|
+
const normal = vec3(dhdx.negate(), float(1), dhdz.negate()).normalize();
|
|
2065
|
+
return vec2(normal.x, normal.z);
|
|
2066
|
+
}
|
|
2067
|
+
);
|
|
2068
|
+
}
|
|
2069
|
+
const terrainFieldStageTask = task((get, work) => {
|
|
2070
|
+
const upstream = get(elevationFieldStageTask);
|
|
2071
|
+
const elevationFieldContext = get(createElevationFieldContextTask);
|
|
2072
|
+
const terrainFieldStorage = get(createTerrainFieldTextureTask);
|
|
2073
|
+
const tileEdgeVertexCount = get(innerTileSegments) + 3;
|
|
2074
|
+
const tile = get(tileNodesTask);
|
|
2075
|
+
const uniforms = get(updateUniformsTask);
|
|
2076
|
+
return work(() => {
|
|
2077
|
+
const computeNormal = createNormalFromElevationField(
|
|
2078
|
+
elevationFieldContext.node,
|
|
2079
|
+
tileEdgeVertexCount
|
|
2080
|
+
);
|
|
2081
|
+
return [
|
|
2082
|
+
...upstream,
|
|
2083
|
+
(nodeIndex, globalVertexIndex, _uv, localCoordinates) => {
|
|
2084
|
+
const ix = int(localCoordinates.x);
|
|
2085
|
+
const iy = int(localCoordinates.y);
|
|
2086
|
+
const tileSize = tile.tileSize(nodeIndex);
|
|
2087
|
+
const height = elevationFieldContext.node.element(globalVertexIndex);
|
|
2088
|
+
const normalXZ = computeNormal(
|
|
2089
|
+
nodeIndex,
|
|
2090
|
+
tileSize,
|
|
2091
|
+
ix,
|
|
2092
|
+
iy,
|
|
2093
|
+
uniforms.uElevationScale
|
|
2094
|
+
);
|
|
2095
|
+
storeTerrainField(
|
|
2096
|
+
terrainFieldStorage,
|
|
2097
|
+
ix,
|
|
2098
|
+
iy,
|
|
2099
|
+
nodeIndex,
|
|
2100
|
+
packTerrainFieldSample(height, normalXZ)
|
|
2101
|
+
);
|
|
2102
|
+
}
|
|
2103
|
+
];
|
|
2104
|
+
});
|
|
2105
|
+
}).displayName("terrainFieldStageTask");
|
|
2106
|
+
|
|
2107
|
+
const compileComputeTask = task((get, work) => {
|
|
2108
|
+
const pipeline = get(terrainFieldStageTask);
|
|
2109
|
+
const edgeVertexCount = get(innerTileSegments) + 3;
|
|
2110
|
+
return work(
|
|
2111
|
+
() => compileComputePipeline(pipeline, edgeVertexCount, {
|
|
2112
|
+
preferSingleKernelWhenPossible: false
|
|
2113
|
+
})
|
|
2114
|
+
);
|
|
2115
|
+
}).displayName("compileComputeTask");
|
|
2116
|
+
const executeComputeTask = task(
|
|
2117
|
+
(get, work, { resources }) => {
|
|
2118
|
+
const { execute } = get(compileComputeTask);
|
|
2119
|
+
const leafState = get(leafGpuBufferTask);
|
|
2120
|
+
return work(
|
|
2121
|
+
() => resources?.renderer ? execute(resources.renderer, leafState.count) : () => {
|
|
2122
|
+
}
|
|
2123
|
+
);
|
|
2124
|
+
}
|
|
2125
|
+
).displayName("executeComputeTask").lane("gpu");
|
|
2126
|
+
function createComputePipelineTasks(leafStageTask) {
|
|
2127
|
+
const compile = task((get, work) => {
|
|
2128
|
+
const pipeline = get(leafStageTask);
|
|
2129
|
+
const edgeVertexCount = get(innerTileSegments) + 3;
|
|
2130
|
+
return work(
|
|
2131
|
+
() => compileComputePipeline(pipeline, edgeVertexCount, {
|
|
2132
|
+
preferSingleKernelWhenPossible: false
|
|
2133
|
+
})
|
|
2134
|
+
);
|
|
2135
|
+
}).displayName("compileComputeTask");
|
|
2136
|
+
const execute = task(
|
|
2137
|
+
(get, work, { resources }) => {
|
|
2138
|
+
const { execute: run } = get(compile);
|
|
2139
|
+
const leafState = get(leafGpuBufferTask);
|
|
2140
|
+
return work(
|
|
2141
|
+
() => resources?.renderer ? run(resources.renderer, leafState.count) : () => {
|
|
2142
|
+
}
|
|
2143
|
+
);
|
|
2144
|
+
}
|
|
2145
|
+
).displayName("executeComputeTask").lane("gpu");
|
|
2146
|
+
return { compile, execute };
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
const SLOT_STRIDE = 6;
|
|
2150
|
+
function nextPow2(n) {
|
|
2151
|
+
let x = 1;
|
|
2152
|
+
while (x < n) x <<= 1;
|
|
2153
|
+
return x;
|
|
2154
|
+
}
|
|
2155
|
+
function createGpuSpatialIndex(maxEntries) {
|
|
2156
|
+
const size = nextPow2(Math.max(2, maxEntries * 2));
|
|
2157
|
+
const data = new Uint32Array(size * SLOT_STRIDE);
|
|
2158
|
+
const attribute = new StorageBufferAttribute(data, SLOT_STRIDE);
|
|
2159
|
+
const node = storage(attribute, "u32", 1).toReadOnly().setName("gpuSpatialIndex");
|
|
2160
|
+
const stampGen = uniform(uint(1)).setName("uGpuSpatialIndexStampGen");
|
|
2161
|
+
return {
|
|
2162
|
+
data,
|
|
2163
|
+
size,
|
|
2164
|
+
mask: size - 1,
|
|
2165
|
+
stampGen,
|
|
2166
|
+
attribute,
|
|
2167
|
+
node
|
|
2168
|
+
};
|
|
2169
|
+
}
|
|
2170
|
+
function uploadGpuSpatialIndex(gpuIndex, cpuIndex) {
|
|
2171
|
+
if (gpuIndex.size !== cpuIndex.size) {
|
|
2172
|
+
throw new Error(
|
|
2173
|
+
`Spatial index size mismatch (gpu=${gpuIndex.size}, cpu=${cpuIndex.size}).`
|
|
2174
|
+
);
|
|
2175
|
+
}
|
|
2176
|
+
for (let i = 0; i < cpuIndex.size; i += 1) {
|
|
2177
|
+
const base = i * SLOT_STRIDE;
|
|
2178
|
+
gpuIndex.data[base] = cpuIndex.stamp[i] ?? 0;
|
|
2179
|
+
gpuIndex.data[base + 1] = cpuIndex.keysSpace[i] ?? 0;
|
|
2180
|
+
gpuIndex.data[base + 2] = cpuIndex.keysLevel[i] ?? 0;
|
|
2181
|
+
gpuIndex.data[base + 3] = cpuIndex.keysX[i] ?? 0;
|
|
2182
|
+
gpuIndex.data[base + 4] = cpuIndex.keysY[i] ?? 0;
|
|
2183
|
+
gpuIndex.data[base + 5] = cpuIndex.values[i] ?? 0;
|
|
2184
|
+
}
|
|
2185
|
+
gpuIndex.stampGen.value = cpuIndex.stampGen >>> 0;
|
|
2186
|
+
gpuIndex.attribute.needsUpdate = true;
|
|
2187
|
+
gpuIndex.node.needsUpdate = true;
|
|
2188
|
+
}
|
|
2189
|
+
function readGpuSpatialIndexValue(spatialIndex, slot, fieldOffset) {
|
|
2190
|
+
const offset = int(slot).mul(int(SLOT_STRIDE)).add(int(fieldOffset));
|
|
2191
|
+
return spatialIndex.node.element(offset).toUint();
|
|
2192
|
+
}
|
|
2193
|
+
const mix32 = Fn(([x]) => {
|
|
2194
|
+
const v = uint(x).toVar();
|
|
2195
|
+
v.assign(v.bitXor(v.shiftRight(uint(16))));
|
|
2196
|
+
v.assign(v.mul(uint(2146121005)));
|
|
2197
|
+
v.assign(v.bitXor(v.shiftRight(uint(15))));
|
|
2198
|
+
v.assign(v.mul(uint(2221713035)));
|
|
2199
|
+
v.assign(v.bitXor(v.shiftRight(uint(16))));
|
|
2200
|
+
return v;
|
|
2201
|
+
});
|
|
2202
|
+
const hashKey = Fn(([space, level, x, y]) => {
|
|
2203
|
+
const s = uint(space).bitAnd(uint(255));
|
|
2204
|
+
const l = uint(level).bitAnd(uint(255));
|
|
2205
|
+
const h = s.bitXor(l.shiftLeft(uint(8))).bitXor(mix32(uint(x))).bitXor(mix32(uint(y)));
|
|
2206
|
+
return mix32(h);
|
|
2207
|
+
});
|
|
2208
|
+
const createGpuSpatialLookup = (spatialIndex) => {
|
|
2209
|
+
const slotCount = spatialIndex.size;
|
|
2210
|
+
const mask = uint(spatialIndex.mask);
|
|
2211
|
+
const stampGen = spatialIndex.stampGen.toUint();
|
|
2212
|
+
const emptyValue = int(-1);
|
|
2213
|
+
return Fn(([space, level, x, y]) => {
|
|
2214
|
+
const s = uint(space).bitAnd(uint(255));
|
|
2215
|
+
const l = uint(level).bitAnd(uint(255));
|
|
2216
|
+
const xx = uint(x);
|
|
2217
|
+
const yy = uint(y);
|
|
2218
|
+
const result = emptyValue.toVar();
|
|
2219
|
+
const slot = hashKey(s, l, xx, yy).bitAnd(mask).toVar();
|
|
2220
|
+
const probes = int(0).toVar();
|
|
2221
|
+
Loop(slotCount, () => {
|
|
2222
|
+
const stamp = readGpuSpatialIndexValue(spatialIndex, slot, 0);
|
|
2223
|
+
If(stamp.notEqual(stampGen), () => {
|
|
2224
|
+
Break();
|
|
2225
|
+
});
|
|
2226
|
+
const ks = readGpuSpatialIndexValue(spatialIndex, slot, 1);
|
|
2227
|
+
const kl = readGpuSpatialIndexValue(spatialIndex, slot, 2);
|
|
2228
|
+
const kx = readGpuSpatialIndexValue(spatialIndex, slot, 3);
|
|
2229
|
+
const ky = readGpuSpatialIndexValue(spatialIndex, slot, 4);
|
|
2230
|
+
If(
|
|
2231
|
+
ks.equal(s).and(kl.equal(l)).and(kx.equal(xx)).and(ky.equal(yy)),
|
|
2232
|
+
() => {
|
|
2233
|
+
result.assign(int(readGpuSpatialIndexValue(spatialIndex, slot, 5)));
|
|
2234
|
+
Break();
|
|
2235
|
+
}
|
|
2236
|
+
);
|
|
2237
|
+
slot.assign(slot.add(uint(1)).bitAnd(mask));
|
|
2238
|
+
probes.addAssign(1);
|
|
2239
|
+
});
|
|
2240
|
+
return result;
|
|
2241
|
+
});
|
|
2242
|
+
};
|
|
2243
|
+
const createTileIndexFromWorldPosition = (spatialIndex, uniforms, maxLevel) => {
|
|
2244
|
+
const lookup = createGpuSpatialLookup(spatialIndex);
|
|
2245
|
+
const levelCount = Math.max(1, maxLevel + 1);
|
|
2246
|
+
return Fn(([worldX, worldZ]) => {
|
|
2247
|
+
const rootOrigin = uniforms.uRootOrigin.toVar();
|
|
2248
|
+
const rootSize = uniforms.uRootSize.toVar();
|
|
2249
|
+
const halfRoot = rootSize.mul(float(0.5));
|
|
2250
|
+
const tileIndex = int(-1).toVar();
|
|
2251
|
+
const tileU = float(0).toVar();
|
|
2252
|
+
const tileV = float(0).toVar();
|
|
2253
|
+
const i = int(0).toVar();
|
|
2254
|
+
Loop(levelCount, () => {
|
|
2255
|
+
const level = int(maxLevel).sub(i).toVar();
|
|
2256
|
+
const scale = pow(float(2), level.toFloat());
|
|
2257
|
+
const tileSize = rootSize.div(scale);
|
|
2258
|
+
const tileX = worldX.sub(rootOrigin.x).add(halfRoot).div(tileSize).floor().toInt();
|
|
2259
|
+
const tileY = worldZ.sub(rootOrigin.z).add(halfRoot).div(tileSize).floor().toInt();
|
|
2260
|
+
const maybeIndex = lookup(int(0), level, tileX, tileY).toVar();
|
|
2261
|
+
If(maybeIndex.greaterThanEqual(int(0)), () => {
|
|
2262
|
+
const minX = rootOrigin.x.add(tileX.toFloat().mul(tileSize)).sub(halfRoot);
|
|
2263
|
+
const minZ = rootOrigin.z.add(tileY.toFloat().mul(tileSize)).sub(halfRoot);
|
|
2264
|
+
tileIndex.assign(maybeIndex);
|
|
2265
|
+
tileU.assign(worldX.sub(minX).div(tileSize));
|
|
2266
|
+
tileV.assign(worldZ.sub(minZ).div(tileSize));
|
|
2267
|
+
Break();
|
|
2268
|
+
});
|
|
2269
|
+
i.addAssign(1);
|
|
2270
|
+
});
|
|
2271
|
+
return vec3(tileIndex.toFloat(), tileU, tileV);
|
|
2272
|
+
});
|
|
2273
|
+
};
|
|
2274
|
+
|
|
2275
|
+
const gpuSpatialIndexStorageTask = task((get, work) => {
|
|
2276
|
+
const maxNodesValue = get(maxNodes);
|
|
2277
|
+
return work(() => createGpuSpatialIndex(maxNodesValue));
|
|
2278
|
+
}).displayName("gpuSpatialIndexStorageTask");
|
|
2279
|
+
const gpuSpatialIndexUploadTask = task((get, work) => {
|
|
2280
|
+
const quadtreeConfig = get(quadtreeConfigTask);
|
|
2281
|
+
get(quadtreeUpdateTask);
|
|
2282
|
+
const gpuSpatialIndex = get(gpuSpatialIndexStorageTask);
|
|
2283
|
+
return work(() => {
|
|
2284
|
+
uploadGpuSpatialIndex(gpuSpatialIndex, quadtreeConfig.state.leafIndex);
|
|
2285
|
+
return gpuSpatialIndex;
|
|
2286
|
+
});
|
|
2287
|
+
}).displayName("gpuSpatialIndexUploadTask");
|
|
2288
|
+
|
|
2289
|
+
function createTerrainSampleNode(params) {
|
|
2290
|
+
const tileLookup = createTileIndexFromWorldPosition(
|
|
2291
|
+
params.spatialIndex,
|
|
2292
|
+
params.uniforms,
|
|
2293
|
+
maxLevel.get()
|
|
2294
|
+
);
|
|
2295
|
+
return Fn(([worldX, worldZ]) => {
|
|
2296
|
+
const tileResult = tileLookup(worldX, worldZ).toVar();
|
|
2297
|
+
const tileIndex = int(tileResult.x).toVar();
|
|
2298
|
+
const safeTileIndex = tileIndex.max(int(0)).toVar();
|
|
2299
|
+
const u = tileResult.y.toVar();
|
|
2300
|
+
const v = tileResult.z.toVar();
|
|
2301
|
+
const fieldU = tileLocalToFieldUV$1(
|
|
2302
|
+
u,
|
|
2303
|
+
params.uniforms.uInnerTileSegments
|
|
2304
|
+
).toVar();
|
|
2305
|
+
const fieldV = tileLocalToFieldUV$1(
|
|
2306
|
+
v,
|
|
2307
|
+
params.uniforms.uInnerTileSegments
|
|
2308
|
+
).toVar();
|
|
2309
|
+
const found = tileIndex.greaterThanEqual(int(0)).toVar();
|
|
2310
|
+
const sampled = sampleTerrainField(
|
|
2311
|
+
params.terrainFieldStorage,
|
|
2312
|
+
fieldU,
|
|
2313
|
+
fieldV,
|
|
2314
|
+
safeTileIndex
|
|
2315
|
+
).toVar();
|
|
2316
|
+
const nx = sampled.g.toVar();
|
|
2317
|
+
const nz = sampled.b.toVar();
|
|
2318
|
+
const ny = float(1).sub(nx.mul(nx)).sub(nz.mul(nz)).max(0).sqrt();
|
|
2319
|
+
const valid = found.select(float(1), float(0)).toVar();
|
|
2320
|
+
return vec4(
|
|
2321
|
+
sampled.r.mul(valid),
|
|
2322
|
+
nx.mul(valid),
|
|
2323
|
+
ny.mul(valid),
|
|
2324
|
+
nz.mul(valid)
|
|
2325
|
+
);
|
|
2326
|
+
});
|
|
2327
|
+
}
|
|
2328
|
+
function createTerrainSampler(params) {
|
|
2329
|
+
const elevationNode = createElevationFunction(params.elevationCallback);
|
|
2330
|
+
const terrainSampleAt = createTerrainSampleNode(params);
|
|
2331
|
+
const evaluateElevationAt = Fn(([worldX, worldZ]) => {
|
|
2332
|
+
const rootOrigin = params.uniforms.uRootOrigin.toVar();
|
|
2333
|
+
const rootSize = params.uniforms.uRootSize.toVar();
|
|
2334
|
+
const centeredX = worldX.sub(rootOrigin.x);
|
|
2335
|
+
const centeredZ = worldZ.sub(rootOrigin.z);
|
|
2336
|
+
const rootUV = vec2(
|
|
2337
|
+
centeredX.div(rootSize).add(0.5),
|
|
2338
|
+
centeredZ.div(rootSize).mul(float(-1)).add(0.5)
|
|
2339
|
+
).toVar();
|
|
2340
|
+
return elevationNode({
|
|
2341
|
+
worldPosition: vec3(worldX, rootOrigin.y, worldZ),
|
|
2342
|
+
rootSize,
|
|
2343
|
+
rootUV,
|
|
2344
|
+
tileUV: rootUV,
|
|
2345
|
+
tileLevel: int(0),
|
|
2346
|
+
tileSize: rootSize,
|
|
2347
|
+
tileOriginVec2: vec2(0, 0),
|
|
2348
|
+
nodeIndex: int(0)
|
|
2349
|
+
});
|
|
2350
|
+
});
|
|
2351
|
+
const sampleTerrain = Fn(
|
|
2352
|
+
([worldX, worldZ]) => terrainSampleAt(worldX, worldZ)
|
|
2353
|
+
);
|
|
2354
|
+
const sampleElevation = Fn(
|
|
2355
|
+
([worldX, worldZ]) => terrainSampleAt(worldX, worldZ).x
|
|
2356
|
+
);
|
|
2357
|
+
const sampleNormal = Fn(
|
|
2358
|
+
([worldX, worldZ]) => vec3(
|
|
2359
|
+
terrainSampleAt(worldX, worldZ).y,
|
|
2360
|
+
terrainSampleAt(worldX, worldZ).z,
|
|
2361
|
+
terrainSampleAt(worldX, worldZ).w
|
|
2362
|
+
)
|
|
2363
|
+
);
|
|
2364
|
+
const sampleValidity = Fn(
|
|
2365
|
+
([worldX, worldZ]) => terrainSampleAt(worldX, worldZ).y.abs().add(terrainSampleAt(worldX, worldZ).z.abs()).add(terrainSampleAt(worldX, worldZ).w.abs()).greaterThan(float(0)).select(float(1), float(0))
|
|
2366
|
+
);
|
|
2367
|
+
const evaluateElevation = Fn(
|
|
2368
|
+
([worldX, worldZ]) => evaluateElevationAt(worldX, worldZ)
|
|
2369
|
+
);
|
|
2370
|
+
const evaluateNormalNode = Fn(
|
|
2371
|
+
([worldX, worldZ, epsilon]) => {
|
|
2372
|
+
const eps = epsilon ?? float(0.1);
|
|
2373
|
+
const elevationScale = params.uniforms.uElevationScale.toVar();
|
|
2374
|
+
const hL = evaluateElevationAt(worldX.sub(eps), worldZ).mul(
|
|
2375
|
+
elevationScale
|
|
2376
|
+
);
|
|
2377
|
+
const hR = evaluateElevationAt(worldX.add(eps), worldZ).mul(
|
|
2378
|
+
elevationScale
|
|
2379
|
+
);
|
|
2380
|
+
const hD = evaluateElevationAt(worldX, worldZ.sub(eps)).mul(
|
|
2381
|
+
elevationScale
|
|
2382
|
+
);
|
|
2383
|
+
const hU = evaluateElevationAt(worldX, worldZ.add(eps)).mul(
|
|
2384
|
+
elevationScale
|
|
2385
|
+
);
|
|
2386
|
+
const inv2eps = float(0.5).div(eps);
|
|
2387
|
+
const dhdx = hR.sub(hL).mul(inv2eps);
|
|
2388
|
+
const dhdz = hU.sub(hD).mul(inv2eps);
|
|
2389
|
+
return vec3(dhdx.negate(), float(1), dhdz.negate()).normalize();
|
|
2390
|
+
}
|
|
2391
|
+
);
|
|
2392
|
+
const evaluateNormal = (worldX, worldZ, epsilon) => evaluateNormalNode(worldX, worldZ, epsilon ?? float(0.1));
|
|
2393
|
+
return {
|
|
2394
|
+
sampleElevation,
|
|
2395
|
+
sampleNormal,
|
|
2396
|
+
sampleTerrain,
|
|
2397
|
+
sampleValidity,
|
|
2398
|
+
evaluateElevation,
|
|
2399
|
+
evaluateNormal
|
|
2400
|
+
};
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
const createTerrainSamplerTask = task((get, work) => {
|
|
2404
|
+
const terrainFieldStorage = get(createTerrainFieldTextureTask);
|
|
2405
|
+
const spatialIndex = get(gpuSpatialIndexStorageTask);
|
|
2406
|
+
const uniforms = get(updateUniformsTask);
|
|
2407
|
+
const elevationCallback = get(elevationFn);
|
|
2408
|
+
return work(
|
|
2409
|
+
() => createTerrainSampler({
|
|
2410
|
+
terrainFieldStorage,
|
|
2411
|
+
spatialIndex,
|
|
2412
|
+
uniforms,
|
|
2413
|
+
elevationCallback
|
|
2414
|
+
})
|
|
2415
|
+
);
|
|
2416
|
+
}).displayName("createTerrainSamplerTask");
|
|
2417
|
+
|
|
216
2418
|
const isSkirtVertex = Fn(([segments]) => {
|
|
217
2419
|
const segmentsNode = typeof segments === "number" ? int(segments) : segments;
|
|
218
2420
|
const vIndex = int(vertexIndex);
|
|
@@ -233,4 +2435,405 @@ const isSkirtUV = Fn(([segments]) => {
|
|
|
233
2435
|
return innerX.and(innerY).not();
|
|
234
2436
|
});
|
|
235
2437
|
|
|
236
|
-
|
|
2438
|
+
function createTileBaseWorldPosition(leafStorage, terrainUniforms) {
|
|
2439
|
+
return Fn(() => {
|
|
2440
|
+
const nodeIndex = int(instanceIndex);
|
|
2441
|
+
const nodeOffset = nodeIndex.mul(int(4));
|
|
2442
|
+
const nodeLevel = leafStorage.node.element(nodeOffset).toInt();
|
|
2443
|
+
const nodeX = leafStorage.node.element(nodeOffset.add(int(1))).toFloat();
|
|
2444
|
+
const nodeY = leafStorage.node.element(nodeOffset.add(int(2))).toFloat();
|
|
2445
|
+
const rootSize = terrainUniforms.uRootSize.toVar();
|
|
2446
|
+
const rootOrigin = terrainUniforms.uRootOrigin.toVar();
|
|
2447
|
+
const half = float(0.5);
|
|
2448
|
+
const size = rootSize.div(pow(float(2), nodeLevel.toFloat()));
|
|
2449
|
+
const halfRoot = rootSize.mul(half);
|
|
2450
|
+
const centerX = rootOrigin.x.add(nodeX.add(half).mul(size)).sub(halfRoot);
|
|
2451
|
+
const centerZ = rootOrigin.z.add(nodeY.add(half).mul(size)).sub(halfRoot);
|
|
2452
|
+
const clampedX = positionLocal.x.max(half.negate()).min(half);
|
|
2453
|
+
const clampedZ = positionLocal.z.max(half.negate()).min(half);
|
|
2454
|
+
const worldX = centerX.add(clampedX.mul(size));
|
|
2455
|
+
const worldZ = centerZ.add(clampedZ.mul(size));
|
|
2456
|
+
return vec3(worldX, rootOrigin.y, worldZ);
|
|
2457
|
+
});
|
|
2458
|
+
}
|
|
2459
|
+
function createTileElevation(terrainUniforms, terrainFieldStorage) {
|
|
2460
|
+
if (!terrainFieldStorage) return float(0);
|
|
2461
|
+
const innerSegs = terrainUniforms.uInnerTileSegments;
|
|
2462
|
+
const u = tileLocalToFieldUV$1(positionLocal.x.add(float(0.5)), innerSegs);
|
|
2463
|
+
const v = tileLocalToFieldUV$1(positionLocal.z.add(float(0.5)), innerSegs);
|
|
2464
|
+
return sampleTerrainFieldElevation(
|
|
2465
|
+
terrainFieldStorage,
|
|
2466
|
+
u,
|
|
2467
|
+
v,
|
|
2468
|
+
int(instanceIndex)
|
|
2469
|
+
).mul(terrainUniforms.uElevationScale);
|
|
2470
|
+
}
|
|
2471
|
+
function createNormalAssignment(terrainUniforms, terrainFieldStorage) {
|
|
2472
|
+
if (!terrainFieldStorage) return;
|
|
2473
|
+
const nodeIndex = int(instanceIndex);
|
|
2474
|
+
const edgeVertexCount = int(terrainUniforms.uInnerTileSegments.add(3));
|
|
2475
|
+
const localVertexIndex = int(vertexIndex);
|
|
2476
|
+
const ix = localVertexIndex.mod(edgeVertexCount);
|
|
2477
|
+
const iy = localVertexIndex.div(edgeVertexCount);
|
|
2478
|
+
const normalXZ = loadTerrainFieldNormal(
|
|
2479
|
+
terrainFieldStorage,
|
|
2480
|
+
ix,
|
|
2481
|
+
iy,
|
|
2482
|
+
nodeIndex
|
|
2483
|
+
);
|
|
2484
|
+
const nx = normalXZ.x;
|
|
2485
|
+
const nz = normalXZ.y;
|
|
2486
|
+
const nySq = float(1).sub(nx.mul(nx)).sub(nz.mul(nz)).max(float(0));
|
|
2487
|
+
const ny = nySq.sqrt();
|
|
2488
|
+
normalLocal.assign(vec3(nx, ny, nz));
|
|
2489
|
+
}
|
|
2490
|
+
function createTileWorldPosition(leafStorage, terrainUniforms, terrainFieldStorage) {
|
|
2491
|
+
const baseWorldPosition = createTileBaseWorldPosition(
|
|
2492
|
+
leafStorage,
|
|
2493
|
+
terrainUniforms
|
|
2494
|
+
);
|
|
2495
|
+
return Fn(() => {
|
|
2496
|
+
const base = baseWorldPosition();
|
|
2497
|
+
const yElevation = createTileElevation(
|
|
2498
|
+
terrainUniforms,
|
|
2499
|
+
terrainFieldStorage
|
|
2500
|
+
);
|
|
2501
|
+
const skirtVertex = isSkirtVertex(terrainUniforms.uInnerTileSegments);
|
|
2502
|
+
const skirtY = base.y.add(yElevation).sub(terrainUniforms.uSkirtScale.toVar());
|
|
2503
|
+
const worldY = select(skirtVertex, skirtY, base.y.add(yElevation));
|
|
2504
|
+
createNormalAssignment(terrainUniforms, terrainFieldStorage);
|
|
2505
|
+
return vec3(base.x, worldY, base.z);
|
|
2506
|
+
})();
|
|
2507
|
+
}
|
|
2508
|
+
|
|
2509
|
+
const positionNodeTask = task((get, work) => {
|
|
2510
|
+
const leafStorage = get(leafStorageTask);
|
|
2511
|
+
const terrainUniforms = get(updateUniformsTask);
|
|
2512
|
+
const terrainFieldStorage = get(createTerrainFieldTextureTask);
|
|
2513
|
+
return work(
|
|
2514
|
+
() => createTileWorldPosition(
|
|
2515
|
+
leafStorage,
|
|
2516
|
+
terrainUniforms,
|
|
2517
|
+
terrainFieldStorage
|
|
2518
|
+
)
|
|
2519
|
+
);
|
|
2520
|
+
}).displayName("positionNodeTask");
|
|
2521
|
+
|
|
2522
|
+
function intersectRayAabb(ray, minX, minY, minZ, maxX, maxY, maxZ) {
|
|
2523
|
+
let tMin = -Infinity;
|
|
2524
|
+
let tMax = Infinity;
|
|
2525
|
+
const origin = ray.origin;
|
|
2526
|
+
const dir = ray.direction;
|
|
2527
|
+
const slab = (originAxis, dirAxis, minAxis, maxAxis) => {
|
|
2528
|
+
if (Math.abs(dirAxis) < 1e-8) {
|
|
2529
|
+
if (originAxis < minAxis || originAxis > maxAxis) return false;
|
|
2530
|
+
return true;
|
|
2531
|
+
}
|
|
2532
|
+
const inv = 1 / dirAxis;
|
|
2533
|
+
let t0 = (minAxis - originAxis) * inv;
|
|
2534
|
+
let t1 = (maxAxis - originAxis) * inv;
|
|
2535
|
+
if (t0 > t1) {
|
|
2536
|
+
const tmp = t0;
|
|
2537
|
+
t0 = t1;
|
|
2538
|
+
t1 = tmp;
|
|
2539
|
+
}
|
|
2540
|
+
tMin = Math.max(tMin, t0);
|
|
2541
|
+
tMax = Math.min(tMax, t1);
|
|
2542
|
+
return tMax >= tMin;
|
|
2543
|
+
};
|
|
2544
|
+
if (!slab(origin.x, dir.x, minX, maxX) || !slab(origin.y, dir.y, minY, maxY) || !slab(origin.z, dir.z, minZ, maxZ)) {
|
|
2545
|
+
return null;
|
|
2546
|
+
}
|
|
2547
|
+
return { tMin, tMax };
|
|
2548
|
+
}
|
|
2549
|
+
function getTerrainBounds(config) {
|
|
2550
|
+
const halfRoot = config.rootSize * 0.5;
|
|
2551
|
+
return {
|
|
2552
|
+
minX: config.originX - halfRoot,
|
|
2553
|
+
maxX: config.originX + halfRoot,
|
|
2554
|
+
minZ: config.originZ - halfRoot,
|
|
2555
|
+
maxZ: config.originZ + halfRoot
|
|
2556
|
+
};
|
|
2557
|
+
}
|
|
2558
|
+
function terrainSignedDistanceFromBounds(query, worldX, worldY, worldZ) {
|
|
2559
|
+
const tileBounds = query.getTileBounds(worldX, worldZ);
|
|
2560
|
+
if (tileBounds) {
|
|
2561
|
+
if (worldY > tileBounds.maxElevation) {
|
|
2562
|
+
return worldY - tileBounds.maxElevation;
|
|
2563
|
+
}
|
|
2564
|
+
if (worldY < tileBounds.minElevation) {
|
|
2565
|
+
return worldY - tileBounds.minElevation;
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2568
|
+
const elevation = query.getElevation(worldX, worldZ);
|
|
2569
|
+
if (!Number.isFinite(elevation)) return void 0;
|
|
2570
|
+
return worldY - elevation;
|
|
2571
|
+
}
|
|
2572
|
+
function terrainSignedDistancePrecise(query, worldX, worldY, worldZ) {
|
|
2573
|
+
const elevation = query.getElevation(worldX, worldZ);
|
|
2574
|
+
if (!Number.isFinite(elevation)) return void 0;
|
|
2575
|
+
return worldY - elevation;
|
|
2576
|
+
}
|
|
2577
|
+
function cpuRaycast(query, ray, config, options) {
|
|
2578
|
+
const bounds = getTerrainBounds(config);
|
|
2579
|
+
const segment = intersectRayAabb(
|
|
2580
|
+
ray,
|
|
2581
|
+
bounds.minX,
|
|
2582
|
+
config.minY,
|
|
2583
|
+
bounds.minZ,
|
|
2584
|
+
bounds.maxX,
|
|
2585
|
+
config.maxY,
|
|
2586
|
+
bounds.maxZ
|
|
2587
|
+
);
|
|
2588
|
+
if (!segment) return null;
|
|
2589
|
+
const maxDistance = options?.maxDistance ?? Number.POSITIVE_INFINITY;
|
|
2590
|
+
let startT = Math.max(0, segment.tMin);
|
|
2591
|
+
const endT = Math.min(segment.tMax, maxDistance);
|
|
2592
|
+
if (endT < startT) return null;
|
|
2593
|
+
const maxSteps = Math.max(8, options?.maxSteps ?? 128);
|
|
2594
|
+
const refinementSteps = Math.max(1, options?.refinementSteps ?? 8);
|
|
2595
|
+
const point = new Vector3();
|
|
2596
|
+
let prevT = startT;
|
|
2597
|
+
ray.at(prevT, point);
|
|
2598
|
+
let prevSignedDistance = terrainSignedDistanceFromBounds(
|
|
2599
|
+
query,
|
|
2600
|
+
point.x,
|
|
2601
|
+
point.y,
|
|
2602
|
+
point.z
|
|
2603
|
+
);
|
|
2604
|
+
if (prevSignedDistance !== void 0 && prevSignedDistance <= 0) {
|
|
2605
|
+
const sample = query.sampleTerrain(point.x, point.z);
|
|
2606
|
+
if (!sample.valid) return null;
|
|
2607
|
+
point.y = sample.elevation;
|
|
2608
|
+
return {
|
|
2609
|
+
position: point.clone(),
|
|
2610
|
+
normal: sample.normal.clone(),
|
|
2611
|
+
distance: ray.origin.distanceTo(point)
|
|
2612
|
+
};
|
|
2613
|
+
}
|
|
2614
|
+
for (let i = 1; i <= maxSteps; i += 1) {
|
|
2615
|
+
const t = startT + (endT - startT) * i / maxSteps;
|
|
2616
|
+
ray.at(t, point);
|
|
2617
|
+
const signedDistance = terrainSignedDistanceFromBounds(
|
|
2618
|
+
query,
|
|
2619
|
+
point.x,
|
|
2620
|
+
point.y,
|
|
2621
|
+
point.z
|
|
2622
|
+
);
|
|
2623
|
+
if (signedDistance === void 0) {
|
|
2624
|
+
prevSignedDistance = void 0;
|
|
2625
|
+
prevT = t;
|
|
2626
|
+
continue;
|
|
2627
|
+
}
|
|
2628
|
+
if (prevSignedDistance !== void 0 && prevSignedDistance > 0 && signedDistance <= 0) {
|
|
2629
|
+
let lo = prevT;
|
|
2630
|
+
let hi = t;
|
|
2631
|
+
for (let r = 0; r < refinementSteps; r += 1) {
|
|
2632
|
+
const mid = (lo + hi) * 0.5;
|
|
2633
|
+
ray.at(mid, point);
|
|
2634
|
+
const midDistance = terrainSignedDistancePrecise(
|
|
2635
|
+
query,
|
|
2636
|
+
point.x,
|
|
2637
|
+
point.y,
|
|
2638
|
+
point.z
|
|
2639
|
+
);
|
|
2640
|
+
if (midDistance === void 0) {
|
|
2641
|
+
lo = mid;
|
|
2642
|
+
continue;
|
|
2643
|
+
}
|
|
2644
|
+
if (midDistance > 0) lo = mid;
|
|
2645
|
+
else hi = mid;
|
|
2646
|
+
}
|
|
2647
|
+
const hitT = hi;
|
|
2648
|
+
ray.at(hitT, point);
|
|
2649
|
+
const sample = query.sampleTerrain(point.x, point.z);
|
|
2650
|
+
if (!sample.valid) return null;
|
|
2651
|
+
point.y = sample.elevation;
|
|
2652
|
+
return {
|
|
2653
|
+
position: point.clone(),
|
|
2654
|
+
normal: sample.normal.clone(),
|
|
2655
|
+
distance: ray.origin.distanceTo(point)
|
|
2656
|
+
};
|
|
2657
|
+
}
|
|
2658
|
+
prevSignedDistance = signedDistance;
|
|
2659
|
+
prevT = t;
|
|
2660
|
+
}
|
|
2661
|
+
return null;
|
|
2662
|
+
}
|
|
2663
|
+
function cpuRaycastBoundsOnly(ray, config, options) {
|
|
2664
|
+
const bounds = getTerrainBounds(config);
|
|
2665
|
+
const planeY = (config.minY + config.maxY) * 0.5;
|
|
2666
|
+
const dirY = ray.direction.y;
|
|
2667
|
+
if (Math.abs(dirY) < 1e-8) return null;
|
|
2668
|
+
const t = (planeY - ray.origin.y) / dirY;
|
|
2669
|
+
if (t < 0) return null;
|
|
2670
|
+
const maxDistance = options?.maxDistance ?? Number.POSITIVE_INFINITY;
|
|
2671
|
+
if (t > maxDistance) return null;
|
|
2672
|
+
const point = new Vector3();
|
|
2673
|
+
ray.at(t, point);
|
|
2674
|
+
if (point.x < bounds.minX || point.x > bounds.maxX || point.z < bounds.minZ || point.z > bounds.maxZ) {
|
|
2675
|
+
return null;
|
|
2676
|
+
}
|
|
2677
|
+
return {
|
|
2678
|
+
position: point,
|
|
2679
|
+
normal: new Vector3(0, 1, 0),
|
|
2680
|
+
distance: ray.origin.distanceTo(point)
|
|
2681
|
+
};
|
|
2682
|
+
}
|
|
2683
|
+
|
|
2684
|
+
function createTerrainRaycast(params) {
|
|
2685
|
+
return {
|
|
2686
|
+
pick(ray, options) {
|
|
2687
|
+
const config = params.getConfig();
|
|
2688
|
+
const terrainQuery = params.getTerrainQuery();
|
|
2689
|
+
if (terrainQuery) {
|
|
2690
|
+
const precise = cpuRaycast(terrainQuery, ray, config, options);
|
|
2691
|
+
if (precise) return precise;
|
|
2692
|
+
}
|
|
2693
|
+
const coarse = cpuRaycastBoundsOnly(ray, config, options);
|
|
2694
|
+
if (coarse && terrainQuery) {
|
|
2695
|
+
const sample = terrainQuery.sampleTerrain(
|
|
2696
|
+
coarse.position.x,
|
|
2697
|
+
coarse.position.z
|
|
2698
|
+
);
|
|
2699
|
+
if (sample.valid) {
|
|
2700
|
+
coarse.position.y = sample.elevation;
|
|
2701
|
+
coarse.normal.copy(sample.normal);
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
return coarse;
|
|
2705
|
+
}
|
|
2706
|
+
};
|
|
2707
|
+
}
|
|
2708
|
+
|
|
2709
|
+
const BOUNDS_PADDING = 1;
|
|
2710
|
+
const RAYCAST_STATE = Symbol("terrainRaycastTaskState");
|
|
2711
|
+
const terrainRaycastTask = task(
|
|
2712
|
+
(get, work) => {
|
|
2713
|
+
const { query: terrainQuery } = get(terrainQueryTask);
|
|
2714
|
+
const rootSizeValue = get(rootSize);
|
|
2715
|
+
const originValue = get(origin);
|
|
2716
|
+
const elevationScaleValue = get(elevationScale);
|
|
2717
|
+
return work((prev) => {
|
|
2718
|
+
let raycast = prev;
|
|
2719
|
+
let state = raycast?.[RAYCAST_STATE];
|
|
2720
|
+
if (!state) {
|
|
2721
|
+
state = {
|
|
2722
|
+
terrainQuery: null,
|
|
2723
|
+
bounds: {
|
|
2724
|
+
rootSize: 0,
|
|
2725
|
+
originX: 0,
|
|
2726
|
+
originZ: 0,
|
|
2727
|
+
minY: 0,
|
|
2728
|
+
maxY: 0
|
|
2729
|
+
}
|
|
2730
|
+
};
|
|
2731
|
+
}
|
|
2732
|
+
state.terrainQuery = terrainQuery;
|
|
2733
|
+
state.bounds.rootSize = rootSizeValue;
|
|
2734
|
+
state.bounds.originX = originValue.x;
|
|
2735
|
+
state.bounds.originZ = originValue.z;
|
|
2736
|
+
const range = terrainQuery.getGlobalElevationRange();
|
|
2737
|
+
if (range) {
|
|
2738
|
+
state.bounds.minY = range.min - BOUNDS_PADDING;
|
|
2739
|
+
state.bounds.maxY = range.max + BOUNDS_PADDING;
|
|
2740
|
+
} else {
|
|
2741
|
+
const verticalExtent = Math.max(1, Math.abs(elevationScaleValue) * 2);
|
|
2742
|
+
state.bounds.minY = originValue.y - verticalExtent;
|
|
2743
|
+
state.bounds.maxY = originValue.y + verticalExtent;
|
|
2744
|
+
}
|
|
2745
|
+
if (!raycast) {
|
|
2746
|
+
raycast = createTerrainRaycast({
|
|
2747
|
+
getTerrainQuery: () => state.terrainQuery,
|
|
2748
|
+
getConfig: () => state.bounds
|
|
2749
|
+
});
|
|
2750
|
+
}
|
|
2751
|
+
raycast[RAYCAST_STATE] = state;
|
|
2752
|
+
return raycast;
|
|
2753
|
+
});
|
|
2754
|
+
}
|
|
2755
|
+
).displayName("terrainRaycastTask");
|
|
2756
|
+
|
|
2757
|
+
function terrainGraph() {
|
|
2758
|
+
return graph().add(instanceIdTask).add(quadtreeConfigTask).add(quadtreeUpdateTask).add(leafStorageTask).add(surfaceTask).add(leafGpuBufferTask).add(gpuSpatialIndexStorageTask).add(gpuSpatialIndexUploadTask).add(createUniformsTask).add(updateUniformsTask).add(positionNodeTask).add(createElevationFieldContextTask).add(tileNodesTask).add(createTerrainFieldTextureTask).add(createTerrainSamplerTask).add(elevationFieldStageTask).add(terrainFieldStageTask).add(compileComputeTask).add(executeComputeTask).add(tileBoundsContextTask).add(tileBoundsReductionTask).add(terrainQueryTask).add(terrainReadbackTask).add(terrainRaycastTask);
|
|
2759
|
+
}
|
|
2760
|
+
const terrainTasks = {
|
|
2761
|
+
instanceId: instanceIdTask,
|
|
2762
|
+
quadtreeConfig: quadtreeConfigTask,
|
|
2763
|
+
quadtreeUpdate: quadtreeUpdateTask,
|
|
2764
|
+
leafStorage: leafStorageTask,
|
|
2765
|
+
surface: surfaceTask,
|
|
2766
|
+
leafGpuBuffer: leafGpuBufferTask,
|
|
2767
|
+
gpuSpatialIndexStorage: gpuSpatialIndexStorageTask,
|
|
2768
|
+
gpuSpatialIndexUpload: gpuSpatialIndexUploadTask,
|
|
2769
|
+
createUniforms: createUniformsTask,
|
|
2770
|
+
updateUniforms: updateUniformsTask,
|
|
2771
|
+
positionNode: positionNodeTask,
|
|
2772
|
+
createElevationFieldContext: createElevationFieldContextTask,
|
|
2773
|
+
createTileNodes: tileNodesTask,
|
|
2774
|
+
createTerrainFieldTexture: createTerrainFieldTextureTask,
|
|
2775
|
+
createTerrainSampler: createTerrainSamplerTask,
|
|
2776
|
+
elevationFieldStage: elevationFieldStageTask,
|
|
2777
|
+
terrainFieldStage: terrainFieldStageTask,
|
|
2778
|
+
compileCompute: compileComputeTask,
|
|
2779
|
+
executeCompute: executeComputeTask,
|
|
2780
|
+
tileBoundsContext: tileBoundsContextTask,
|
|
2781
|
+
tileBoundsReduction: tileBoundsReductionTask,
|
|
2782
|
+
terrainQuery: terrainQueryTask,
|
|
2783
|
+
terrainReadback: terrainReadbackTask,
|
|
2784
|
+
terrainRaycast: terrainRaycastTask
|
|
2785
|
+
};
|
|
2786
|
+
|
|
2787
|
+
const textureSpaceToVectorSpace = Fn(([value]) => {
|
|
2788
|
+
return remap(value, float(0), float(1), float(-1), float(1));
|
|
2789
|
+
});
|
|
2790
|
+
const vectorSpaceToTextureSpace = Fn(([value]) => {
|
|
2791
|
+
return remap(value, float(-1), float(1), float(0), float(1));
|
|
2792
|
+
});
|
|
2793
|
+
const blendAngleCorrectedNormals = Fn(([n1, n2]) => {
|
|
2794
|
+
const t = vec3(n1.x, n1.y, n1.z.add(1));
|
|
2795
|
+
const u = vec3(n2.x.negate(), n2.y.negate(), n2.z);
|
|
2796
|
+
const r = t.mul(dot(t, u)).sub(u.mul(t.z)).normalize();
|
|
2797
|
+
return r;
|
|
2798
|
+
});
|
|
2799
|
+
const deriveNormalZ = Fn(([normalXY]) => {
|
|
2800
|
+
const xy = normalXY.toVar();
|
|
2801
|
+
const z = xy.x.mul(xy.x).add(xy.y.mul(xy.y)).oneMinus().max(0).sqrt();
|
|
2802
|
+
return vec3(xy.x, xy.y, z);
|
|
2803
|
+
});
|
|
2804
|
+
|
|
2805
|
+
const vGlobalVertexIndex = /* @__PURE__ */ varyingProperty("int", "vGlobalVertexIndex");
|
|
2806
|
+
const vElevation = /* @__PURE__ */ varyingProperty("f32", "vElevation");
|
|
2807
|
+
|
|
2808
|
+
const cellCenter = Fn(({ cell }) => {
|
|
2809
|
+
return cell.add(mx_noise_float(cell.mul(Math.PI)));
|
|
2810
|
+
});
|
|
2811
|
+
const voronoiCells = Fn((params) => {
|
|
2812
|
+
const scale = float(params.scale);
|
|
2813
|
+
const facet = float(params.facet);
|
|
2814
|
+
const seed = float(params.seed);
|
|
2815
|
+
const pos = params.uv.mul(scale).add(seed);
|
|
2816
|
+
const midCell = pos.round().toVar();
|
|
2817
|
+
const minCell = midCell.toVar();
|
|
2818
|
+
const minDist = float(1).toVar();
|
|
2819
|
+
const cell = vec3(0, 0, 0).toVar();
|
|
2820
|
+
const dist = float().toVar();
|
|
2821
|
+
const i = float(0).toVar();
|
|
2822
|
+
Loop(27, () => {
|
|
2823
|
+
const ix = i.mod(3).sub(1);
|
|
2824
|
+
const iy = i.div(3).floor().mod(3).sub(1);
|
|
2825
|
+
const iz = i.div(9).floor().sub(1);
|
|
2826
|
+
cell.assign(midCell.add(vec3(ix, iy, iz)));
|
|
2827
|
+
dist.assign(pos.distance(cellCenter({ cell })).add(mx_noise_float(pos).div(5)));
|
|
2828
|
+
If(dist.lessThan(minDist), () => {
|
|
2829
|
+
minDist.assign(dist);
|
|
2830
|
+
minCell.assign(cell);
|
|
2831
|
+
});
|
|
2832
|
+
i.addAssign(1);
|
|
2833
|
+
});
|
|
2834
|
+
const n = mx_noise_float(minCell.mul(Math.PI)).toVar();
|
|
2835
|
+
const k = mix(minDist, n.add(1).div(2), facet);
|
|
2836
|
+
return k;
|
|
2837
|
+
});
|
|
2838
|
+
|
|
2839
|
+
export { ArrayTextureBackend, AtlasBackend, Dir, TerrainGeometry, TerrainMesh, Texture3DBackend, U32_EMPTY, allocLeafSet, allocSeamTable, beginUpdate, blendAngleCorrectedNormals, buildLeafIndex, buildSeams2to1, compileComputeTask, createComputePipelineTasks, createCubeSphereSurface, createElevationFieldContextTask, createFlatSurface, createInfiniteFlatSurface, createSpatialIndex, createState, createTerrainFieldStorage, createTerrainFieldTextureTask, createTerrainQuery, createTerrainRaycast, createTerrainSampler, createTerrainSamplerTask, createTerrainUniforms, createUniformsTask, deriveNormalZ, elevationFieldStageTask, elevationFn, elevationScale, executeComputeTask, getDeviceComputeLimits, gpuSpatialIndexStorageTask, gpuSpatialIndexUploadTask, innerTileSegments, instanceIdTask, isSkirtUV, isSkirtVertex, leafGpuBufferTask, leafStorageTask, loadTerrainField, loadTerrainFieldElevation, loadTerrainFieldNormal, maxLevel, maxNodes, origin, packTerrainFieldSample, positionNodeTask, quadtreeConfigTask, quadtreeUpdate, quadtreeUpdateTask, resetLeafSet, resetSeamTable, rootSize, sampleTerrainField, sampleTerrainFieldElevation, sampleTerrainFieldNormal, skirtScale, storeTerrainField, surface, surfaceTask, terrainFieldFilter, terrainFieldStageTask, terrainGraph, terrainQueryTask, terrainRaycastTask, terrainReadbackTask, terrainTasks, textureSpaceToVectorSpace, tileNodesTask, update, updateUniformsTask, vElevation, vGlobalVertexIndex, vectorSpaceToTextureSpace, voronoiCells };
|