@hello-terrain/three 0.0.0-alpha.4 → 0.0.0-alpha.6
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 +651 -123
- package/dist/index.d.cts +344 -143
- package/dist/index.d.mts +344 -143
- package/dist/index.d.ts +344 -143
- package/dist/index.mjs +638 -124
- package/package.json +4 -4
package/dist/index.cjs
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
const three = require('three');
|
|
4
4
|
const webgpu = require('three/webgpu');
|
|
5
|
-
const tsl = require('three/tsl');
|
|
6
5
|
const work = require('@hello-terrain/work');
|
|
6
|
+
const tsl = require('three/tsl');
|
|
7
|
+
const TSL_js = require('three/src/nodes/TSL.js');
|
|
7
8
|
|
|
8
9
|
class TerrainGeometry extends three.BufferGeometry {
|
|
9
10
|
constructor(innerSegments = 14, extendUV = false) {
|
|
@@ -63,17 +64,19 @@ class TerrainGeometry extends three.BufferGeometry {
|
|
|
63
64
|
* | / | \ | / | \ |
|
|
64
65
|
* o---o---o---o---o
|
|
65
66
|
*
|
|
66
|
-
* INNER GRID (
|
|
67
|
-
* o---o---o
|
|
68
|
-
* | \ | \ |
|
|
69
|
-
* o---o---o
|
|
70
|
-
* | \ | \ |
|
|
71
|
-
* o---o---o
|
|
67
|
+
* INNER GRID (alternating diagonals — checkerboard pattern):
|
|
68
|
+
* o---o---o---o---o
|
|
69
|
+
* | \ | / | \ | / |
|
|
70
|
+
* o---o---o---o---o
|
|
71
|
+
* | / | \ | / | \ |
|
|
72
|
+
* o---o---o---o---o
|
|
73
|
+
* | \ | / | \ | / |
|
|
74
|
+
* o---o---o---o---o
|
|
72
75
|
*
|
|
73
76
|
* Where o = vertex
|
|
74
77
|
* Each square cell is split into 2 triangles.
|
|
75
78
|
* - Skirt cells (outer ring): diagonal flip based on quadrant for corner correctness
|
|
76
|
-
* - Inner cells:
|
|
79
|
+
* - Inner cells: alternating diagonal via (x+y)%2 to reduce interpolation artifacts
|
|
77
80
|
*
|
|
78
81
|
* Vertex layout (for innerSegments = 2):
|
|
79
82
|
*
|
|
@@ -119,7 +122,7 @@ class TerrainGeometry extends three.BufferGeometry {
|
|
|
119
122
|
const topHalf = y < mid;
|
|
120
123
|
useDefaultDiagonal = leftHalf && topHalf || !leftHalf && !topHalf;
|
|
121
124
|
} else {
|
|
122
|
-
useDefaultDiagonal =
|
|
125
|
+
useDefaultDiagonal = (x + y) % 2 === 0;
|
|
123
126
|
}
|
|
124
127
|
if (useDefaultDiagonal) {
|
|
125
128
|
indices.push(a, d, b);
|
|
@@ -219,7 +222,7 @@ class TerrainGeometry extends three.BufferGeometry {
|
|
|
219
222
|
|
|
220
223
|
const defaultTerrainMeshParams = {
|
|
221
224
|
innerTileSegments: 14,
|
|
222
|
-
maxNodes:
|
|
225
|
+
maxNodes: 1024,
|
|
223
226
|
material: new webgpu.MeshStandardNodeMaterial()
|
|
224
227
|
};
|
|
225
228
|
class TerrainMesh extends webgpu.InstancedMesh {
|
|
@@ -230,6 +233,7 @@ class TerrainMesh extends webgpu.InstancedMesh {
|
|
|
230
233
|
const { innerTileSegments, maxNodes, material } = mergedParams;
|
|
231
234
|
const geometry = new TerrainGeometry(innerTileSegments, true);
|
|
232
235
|
super(geometry, material, maxNodes);
|
|
236
|
+
this.frustumCulled = false;
|
|
233
237
|
this._innerTileSegments = innerTileSegments;
|
|
234
238
|
this._maxNodes = maxNodes;
|
|
235
239
|
}
|
|
@@ -246,72 +250,197 @@ class TerrainMesh extends webgpu.InstancedMesh {
|
|
|
246
250
|
return this._maxNodes;
|
|
247
251
|
}
|
|
248
252
|
set maxNodes(maxNodes) {
|
|
253
|
+
if (!Number.isInteger(maxNodes) || maxNodes < 1) {
|
|
254
|
+
throw new Error(`Invalid maxNodes: ${maxNodes}. Must be a positive integer.`);
|
|
255
|
+
}
|
|
256
|
+
if (maxNodes === this._maxNodes) return;
|
|
257
|
+
const oldMax = this._maxNodes;
|
|
258
|
+
const nextMatrix = new Float32Array(maxNodes * 16);
|
|
259
|
+
const oldMatrixArray = this.instanceMatrix.array;
|
|
260
|
+
nextMatrix.set(oldMatrixArray.subarray(0, Math.min(oldMatrixArray.length, nextMatrix.length)));
|
|
261
|
+
this.instanceMatrix = new webgpu.InstancedBufferAttribute(nextMatrix, 16);
|
|
262
|
+
if (this.instanceColor) {
|
|
263
|
+
const itemSize = this.instanceColor.itemSize;
|
|
264
|
+
const nextColor = new Float32Array(maxNodes * itemSize);
|
|
265
|
+
const oldColorArray = this.instanceColor.array;
|
|
266
|
+
nextColor.set(oldColorArray.subarray(0, Math.min(oldColorArray.length, nextColor.length)));
|
|
267
|
+
this.instanceColor = new webgpu.InstancedBufferAttribute(nextColor, itemSize);
|
|
268
|
+
}
|
|
249
269
|
this._maxNodes = maxNodes;
|
|
270
|
+
this.count = Math.min(this.count, maxNodes);
|
|
271
|
+
this.instanceMatrix.needsUpdate = true;
|
|
272
|
+
if (this.instanceColor) this.instanceColor.needsUpdate = true;
|
|
273
|
+
if (maxNodes < oldMax && this.count >= maxNodes) {
|
|
274
|
+
this.count = maxNodes;
|
|
275
|
+
}
|
|
250
276
|
}
|
|
251
277
|
}
|
|
252
278
|
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
const
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
279
|
+
const WORKGROUP_X = 16;
|
|
280
|
+
const WORKGROUP_Y = 16;
|
|
281
|
+
function compileComputePipeline(stages, width, bindings) {
|
|
282
|
+
const workgroupSize = [WORKGROUP_X, WORKGROUP_Y, 1];
|
|
283
|
+
const dispatchX = Math.ceil(width / WORKGROUP_X);
|
|
284
|
+
const dispatchY = Math.ceil(width / WORKGROUP_Y);
|
|
285
|
+
const uInstanceCount = tsl.uniform(0, "uint");
|
|
286
|
+
const computeShader = tsl.Fn(() => {
|
|
287
|
+
const fWidth = tsl.float(width);
|
|
288
|
+
const activeIndex = tsl.globalId.z;
|
|
289
|
+
const nodeIndex = tsl.int(activeIndex).toVar();
|
|
290
|
+
const iWidth = tsl.int(width);
|
|
291
|
+
const ix = tsl.int(tsl.globalId.x);
|
|
292
|
+
const iy = tsl.int(tsl.globalId.y);
|
|
293
|
+
const texelSize = tsl.vec2(1, 1).div(fWidth);
|
|
294
|
+
const localCoordinates = tsl.vec2(tsl.globalId.x, tsl.globalId.y);
|
|
295
|
+
const localUVCoords = localCoordinates.div(fWidth);
|
|
296
|
+
const verticesPerNode = iWidth.mul(iWidth);
|
|
297
|
+
const globalIndex = tsl.int(nodeIndex).mul(verticesPerNode).add(iy.mul(iWidth).add(ix));
|
|
298
|
+
const inBounds = ix.lessThan(iWidth).and(iy.lessThan(iWidth)).and(tsl.uint(activeIndex).lessThan(uInstanceCount)).toVar();
|
|
299
|
+
for (let i = 0; i < stages.length; i++) {
|
|
300
|
+
if (i > 0) {
|
|
301
|
+
tsl.workgroupBarrier();
|
|
302
|
+
}
|
|
303
|
+
tsl.If(inBounds, () => {
|
|
304
|
+
stages[i](nodeIndex, globalIndex, localUVCoords, localCoordinates, texelSize);
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
})().computeKernel(workgroupSize);
|
|
308
|
+
function execute(renderer, instanceCount) {
|
|
309
|
+
uInstanceCount.value = instanceCount;
|
|
310
|
+
renderer.compute(computeShader, [dispatchX, dispatchY, instanceCount]);
|
|
311
|
+
}
|
|
312
|
+
return { execute };
|
|
313
|
+
}
|
|
270
314
|
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
315
|
+
const createElevation = (tile, uniforms, elevationFn) => {
|
|
316
|
+
return function perVertexElevation(nodeIndex, localCoordinates) {
|
|
317
|
+
const ix = tsl.int(localCoordinates.x);
|
|
318
|
+
const iy = tsl.int(localCoordinates.y);
|
|
319
|
+
const edgeVertexCount = uniforms.uInnerTileSegments.toVar().add(tsl.int(3));
|
|
320
|
+
const tileUV = localCoordinates.toFloat().div(edgeVertexCount.toFloat());
|
|
321
|
+
const rootUV = tile.rootUVCompute(nodeIndex, ix, iy);
|
|
322
|
+
const worldPosition = tile.tileVertexWorldPositionCompute(nodeIndex, ix, iy).setName("worldPositionWithSkirt");
|
|
323
|
+
const rootSize = uniforms.uRootSize.toVar();
|
|
324
|
+
return elevationFn({
|
|
325
|
+
worldPosition,
|
|
326
|
+
rootSize,
|
|
327
|
+
rootUV,
|
|
328
|
+
tileOriginVec2: tile.tileOriginVec2(nodeIndex),
|
|
329
|
+
tileSize: tile.tileSize(nodeIndex),
|
|
330
|
+
tileLevel: tile.tileLevel(nodeIndex),
|
|
331
|
+
nodeIndex: tsl.int(nodeIndex),
|
|
332
|
+
tileUV
|
|
333
|
+
});
|
|
334
|
+
};
|
|
335
|
+
};
|
|
336
|
+
const readElevationFieldAtPositionLocal = (elevationFieldBuffer, edgeVertexCount, positionLocal) => tsl.Fn(() => {
|
|
337
|
+
const nodeIndex = tsl.int(tsl.instanceIndex);
|
|
338
|
+
const intEdge = tsl.int(edgeVertexCount);
|
|
339
|
+
const innerSegments = tsl.int(edgeVertexCount).sub(3);
|
|
340
|
+
const fInnerSegments = tsl.float(innerSegments);
|
|
341
|
+
const last = intEdge.sub(tsl.int(1));
|
|
342
|
+
const u = positionLocal.x.add(tsl.float(0.5));
|
|
343
|
+
const v = positionLocal.z.add(tsl.float(0.5));
|
|
344
|
+
const x = u.mul(fInnerSegments).round().toInt().add(tsl.int(1));
|
|
345
|
+
const y = v.mul(fInnerSegments).round().toInt().add(tsl.int(1));
|
|
346
|
+
const xClamped = tsl.min(tsl.max(x, tsl.int(0)), last);
|
|
347
|
+
const yClamped = tsl.min(tsl.max(y, tsl.int(0)), last);
|
|
348
|
+
const verticesPerNode = intEdge.mul(intEdge);
|
|
349
|
+
const perNodeVertexIndex = yClamped.mul(intEdge).add(xClamped);
|
|
350
|
+
const globalVertexIndex = nodeIndex.mul(verticesPerNode).add(perNodeVertexIndex);
|
|
351
|
+
return elevationFieldBuffer.element(globalVertexIndex);
|
|
289
352
|
});
|
|
290
353
|
|
|
291
|
-
function
|
|
292
|
-
|
|
293
|
-
const
|
|
294
|
-
|
|
354
|
+
function createTileCompute(leafStorage, uniforms) {
|
|
355
|
+
const tileLevel = tsl.Fn(([nodeIndex]) => {
|
|
356
|
+
const nodeOffset = nodeIndex.mul(tsl.int(4));
|
|
357
|
+
return leafStorage.node.element(nodeOffset).toInt();
|
|
358
|
+
});
|
|
359
|
+
const tileOriginVec2 = tsl.Fn(([nodeIndex]) => {
|
|
295
360
|
const nodeOffset = nodeIndex.mul(tsl.int(4));
|
|
296
|
-
const nodeLevel = leafStorage.node.element(nodeOffset).toInt();
|
|
297
361
|
const nodeX = leafStorage.node.element(nodeOffset.add(tsl.int(1))).toFloat();
|
|
298
362
|
const nodeY = leafStorage.node.element(nodeOffset.add(tsl.int(2))).toFloat();
|
|
299
|
-
|
|
300
|
-
|
|
363
|
+
return tsl.vec2(nodeX, nodeY);
|
|
364
|
+
});
|
|
365
|
+
const tileSize = tsl.Fn(([nodeIndex]) => {
|
|
366
|
+
const rootSize = uniforms.uRootSize.toVar();
|
|
367
|
+
const level = tileLevel(nodeIndex);
|
|
368
|
+
return tsl.float(rootSize).div(tsl.pow(tsl.float(2), level.toFloat()));
|
|
369
|
+
});
|
|
370
|
+
const rootUVCompute = tsl.Fn(([nodeIndex, ix, iy]) => {
|
|
371
|
+
const nodeVec2 = tileOriginVec2(nodeIndex);
|
|
372
|
+
const nodeX = nodeVec2.x;
|
|
373
|
+
const nodeY = nodeVec2.y;
|
|
374
|
+
const rootSize = uniforms.uRootSize.toVar();
|
|
375
|
+
const rootOrigin = uniforms.uRootOrigin.toVar();
|
|
376
|
+
const size = tileSize(nodeIndex);
|
|
301
377
|
const half = tsl.float(0.5);
|
|
302
|
-
const
|
|
303
|
-
const
|
|
304
|
-
const
|
|
305
|
-
const
|
|
306
|
-
const
|
|
307
|
-
const
|
|
308
|
-
const
|
|
309
|
-
const
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
378
|
+
const halfRoot = tsl.float(rootSize).mul(half);
|
|
379
|
+
const fInnerSegments = uniforms.uInnerTileSegments.toVar().toFloat();
|
|
380
|
+
const texelSpacing = size.div(fInnerSegments);
|
|
381
|
+
const absX = nodeX.mul(fInnerSegments).add(tsl.int(ix).toFloat().sub(tsl.float(1)));
|
|
382
|
+
const absY = nodeY.mul(fInnerSegments).add(tsl.int(iy).toFloat().sub(tsl.float(1)));
|
|
383
|
+
const worldX = rootOrigin.x.add(absX.mul(texelSpacing)).sub(halfRoot);
|
|
384
|
+
const worldZ = rootOrigin.z.add(absY.mul(texelSpacing)).sub(halfRoot);
|
|
385
|
+
const centeredX = worldX.sub(rootOrigin.x);
|
|
386
|
+
const centeredZ = worldZ.sub(rootOrigin.z);
|
|
387
|
+
return tsl.vec2(
|
|
388
|
+
centeredX.div(rootSize).add(half),
|
|
389
|
+
centeredZ.div(rootSize).mul(tsl.float(-1)).add(half)
|
|
390
|
+
);
|
|
391
|
+
});
|
|
392
|
+
const tileVertexWorldPositionCompute = tsl.Fn(
|
|
393
|
+
([nodeIndex, ix, iy]) => {
|
|
394
|
+
const rootOrigin = uniforms.uRootOrigin.toVar();
|
|
395
|
+
const nodeVec2 = tileOriginVec2(nodeIndex);
|
|
396
|
+
const nodeX = nodeVec2.x;
|
|
397
|
+
const nodeY = nodeVec2.y;
|
|
398
|
+
const rootSize = uniforms.uRootSize.toVar();
|
|
399
|
+
const size = tileSize(nodeIndex);
|
|
400
|
+
const half = tsl.float(0.5);
|
|
401
|
+
const halfRoot = tsl.float(rootSize).mul(half);
|
|
402
|
+
const fInnerSegments = uniforms.uInnerTileSegments.toVar().toFloat();
|
|
403
|
+
const texelSpacing = size.div(fInnerSegments);
|
|
404
|
+
const absX = nodeX.mul(fInnerSegments).add(tsl.int(ix).toFloat().sub(tsl.float(1)));
|
|
405
|
+
const absY = nodeY.mul(fInnerSegments).add(tsl.int(iy).toFloat().sub(tsl.float(1)));
|
|
406
|
+
const worldX = rootOrigin.x.add(absX.mul(texelSpacing)).sub(halfRoot);
|
|
407
|
+
const worldZ = rootOrigin.z.add(absY.mul(texelSpacing)).sub(halfRoot);
|
|
408
|
+
return tsl.vec3(worldX, rootOrigin.y, worldZ);
|
|
409
|
+
}
|
|
410
|
+
);
|
|
411
|
+
return {
|
|
412
|
+
tileLevel,
|
|
413
|
+
tileOriginVec2,
|
|
414
|
+
tileSize,
|
|
415
|
+
rootUVCompute,
|
|
416
|
+
tileVertexWorldPositionCompute
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const rootSize = work.param(256).displayName("rootSize");
|
|
421
|
+
const origin = work.param({
|
|
422
|
+
x: 0,
|
|
423
|
+
y: 0,
|
|
424
|
+
z: 0
|
|
425
|
+
}).displayName("origin");
|
|
426
|
+
const innerTileSegments = work.param(13).displayName("innerTileSegments");
|
|
427
|
+
const skirtScale = work.param(100).displayName("skirtScale");
|
|
428
|
+
const elevationScale = work.param(1).displayName("elevationScale");
|
|
429
|
+
const maxNodes = work.param(1024).displayName("maxNodes");
|
|
430
|
+
const maxLevel = work.param(16).displayName("maxLevel");
|
|
431
|
+
const quadtreeUpdate = work.param({
|
|
432
|
+
cameraOrigin: { x: 0, y: 0, z: 0 },
|
|
433
|
+
mode: "distance",
|
|
434
|
+
distanceFactor: 1.5
|
|
435
|
+
}).displayName("quadtreeUpdate");
|
|
436
|
+
const surface = work.param(null).displayName("surface");
|
|
437
|
+
const elevationFn = work.param(() => tsl.float(0));
|
|
438
|
+
|
|
439
|
+
function createLeafStorage(maxNodes) {
|
|
440
|
+
const data = new Int32Array(maxNodes * 4);
|
|
441
|
+
const attribute = new webgpu.StorageBufferAttribute(data, 4);
|
|
442
|
+
const node = tsl.storage(attribute, "i32", 1).toReadOnly().setName("leafStorage");
|
|
443
|
+
return { data, attribute, node };
|
|
315
444
|
}
|
|
316
445
|
|
|
317
446
|
const Dir = {
|
|
@@ -327,8 +456,8 @@ function allocLeafSet(capacity) {
|
|
|
327
456
|
count: 0,
|
|
328
457
|
space: new Uint8Array(capacity),
|
|
329
458
|
level: new Uint8Array(capacity),
|
|
330
|
-
x: new
|
|
331
|
-
y: new
|
|
459
|
+
x: new Int32Array(capacity),
|
|
460
|
+
y: new Int32Array(capacity)
|
|
332
461
|
};
|
|
333
462
|
}
|
|
334
463
|
function resetLeafSet(leaves) {
|
|
@@ -354,8 +483,8 @@ function createNodeStore(maxNodes, spaceCount) {
|
|
|
354
483
|
gen: new Uint16Array(maxNodes),
|
|
355
484
|
space: new Uint8Array(maxNodes),
|
|
356
485
|
level: new Uint8Array(maxNodes),
|
|
357
|
-
x: new
|
|
358
|
-
y: new
|
|
486
|
+
x: new Int32Array(maxNodes),
|
|
487
|
+
y: new Int32Array(maxNodes),
|
|
359
488
|
firstChild: new Uint32Array(maxNodes),
|
|
360
489
|
flags: new Uint8Array(maxNodes),
|
|
361
490
|
roots: new Uint32Array(spaceCount)
|
|
@@ -376,8 +505,8 @@ function allocNode(store, tile) {
|
|
|
376
505
|
store.gen[id] = store.currentGen;
|
|
377
506
|
store.space[id] = tile.space;
|
|
378
507
|
store.level[id] = tile.level;
|
|
379
|
-
store.x[id] = tile.x
|
|
380
|
-
store.y[id] = tile.y
|
|
508
|
+
store.x[id] = tile.x;
|
|
509
|
+
store.y[id] = tile.y;
|
|
381
510
|
store.firstChild[id] = U32_EMPTY;
|
|
382
511
|
store.flags[id] = 0;
|
|
383
512
|
return id;
|
|
@@ -492,6 +621,10 @@ function buildLeafIndex(leaves, out) {
|
|
|
492
621
|
|
|
493
622
|
function createState(cfg, surface) {
|
|
494
623
|
const store = createNodeStore(cfg.maxNodes, surface.spaceCount);
|
|
624
|
+
const scratchRootTiles = [];
|
|
625
|
+
for (let i = 0; i < surface.maxRootCount; i++) {
|
|
626
|
+
scratchRootTiles.push({ space: 0, level: 0, x: 0, y: 0 });
|
|
627
|
+
}
|
|
495
628
|
return {
|
|
496
629
|
cfg,
|
|
497
630
|
store,
|
|
@@ -499,28 +632,42 @@ function createState(cfg, surface) {
|
|
|
499
632
|
leafNodeIds: new Uint32Array(cfg.maxNodes),
|
|
500
633
|
leafIndex: createSpatialIndex(cfg.maxNodes),
|
|
501
634
|
stack: new Uint32Array(cfg.maxNodes),
|
|
635
|
+
rootNodeIds: new Uint32Array(surface.maxRootCount),
|
|
636
|
+
rootCount: 0,
|
|
502
637
|
splitQueue: new Uint32Array(cfg.maxNodes),
|
|
503
638
|
splitStamp: new Uint16Array(cfg.maxNodes),
|
|
504
639
|
splitGen: 1,
|
|
505
640
|
scratchTile: { space: 0, level: 0, x: 0, y: 0 },
|
|
506
641
|
scratchNeighbor: { space: 0, level: 0, x: 0, y: 0 },
|
|
507
642
|
scratchBounds: { cx: 0, cy: 0, cz: 0, r: 0 },
|
|
643
|
+
scratchRootTiles,
|
|
508
644
|
spaceCount: surface.spaceCount
|
|
509
645
|
};
|
|
510
646
|
}
|
|
511
|
-
function beginUpdate(state, surface) {
|
|
647
|
+
function beginUpdate(state, surface, params) {
|
|
512
648
|
if (surface.spaceCount !== state.spaceCount) {
|
|
513
649
|
throw new Error(
|
|
514
650
|
`Surface spaceCount changed (${state.spaceCount} -> ${surface.spaceCount}). Create a new quadtree state.`
|
|
515
651
|
);
|
|
516
652
|
}
|
|
653
|
+
if (surface.maxRootCount !== state.rootNodeIds.length) {
|
|
654
|
+
throw new Error(
|
|
655
|
+
`Surface maxRootCount changed (${state.rootNodeIds.length} -> ${surface.maxRootCount}). Create a new quadtree state.`
|
|
656
|
+
);
|
|
657
|
+
}
|
|
517
658
|
beginFrame(state.store);
|
|
518
|
-
|
|
519
|
-
|
|
659
|
+
state.rootCount = 0;
|
|
660
|
+
const rootCount = surface.rootTiles(params.cameraOrigin, state.scratchRootTiles);
|
|
661
|
+
if (rootCount < 0 || rootCount > surface.maxRootCount) {
|
|
662
|
+
throw new Error(`Surface returned invalid root count (${rootCount}).`);
|
|
663
|
+
}
|
|
664
|
+
for (let i = 0; i < rootCount; i++) {
|
|
665
|
+
const rootId = allocNode(state.store, state.scratchRootTiles[i]);
|
|
520
666
|
if (rootId === U32_EMPTY) {
|
|
521
667
|
throw new Error("Failed to allocate root node (maxNodes too small).");
|
|
522
668
|
}
|
|
523
|
-
state.
|
|
669
|
+
state.rootNodeIds[i] = rootId;
|
|
670
|
+
state.rootCount = i + 1;
|
|
524
671
|
}
|
|
525
672
|
}
|
|
526
673
|
|
|
@@ -555,8 +702,8 @@ function refineLeaves(state, surface, params, outLeaves) {
|
|
|
555
702
|
const store = state.store;
|
|
556
703
|
const stack = state.stack;
|
|
557
704
|
let sp = 0;
|
|
558
|
-
for (let
|
|
559
|
-
stack[sp++] =
|
|
705
|
+
for (let i = 0; i < state.rootCount; i++) {
|
|
706
|
+
stack[sp++] = state.rootNodeIds[i];
|
|
560
707
|
}
|
|
561
708
|
while (sp > 0) {
|
|
562
709
|
const nodeId = stack[--sp];
|
|
@@ -669,7 +816,7 @@ function balance2to1(state, surface, params, leaves) {
|
|
|
669
816
|
}
|
|
670
817
|
|
|
671
818
|
function update(state, surface, params, outLeaves) {
|
|
672
|
-
beginUpdate(state, surface);
|
|
819
|
+
beginUpdate(state, surface, params);
|
|
673
820
|
const leaves = refineLeaves(state, surface, params, outLeaves);
|
|
674
821
|
return balance2to1(state, surface, params, leaves);
|
|
675
822
|
}
|
|
@@ -773,6 +920,7 @@ function createFlatSurface(cfg) {
|
|
|
773
920
|
const maxHeight = cfg.maxHeight ?? 0;
|
|
774
921
|
const surface = {
|
|
775
922
|
spaceCount: 1,
|
|
923
|
+
maxRootCount: 1,
|
|
776
924
|
neighborSameLevel(tile, dir, out) {
|
|
777
925
|
const level = tile.level;
|
|
778
926
|
const x = tile.x;
|
|
@@ -798,8 +946,8 @@ function createFlatSurface(cfg) {
|
|
|
798
946
|
if (nx > maxCoord || ny > maxCoord) return false;
|
|
799
947
|
out.space = 0;
|
|
800
948
|
out.level = level;
|
|
801
|
-
out.x = nx
|
|
802
|
-
out.y = ny
|
|
949
|
+
out.x = nx;
|
|
950
|
+
out.y = ny;
|
|
803
951
|
return true;
|
|
804
952
|
},
|
|
805
953
|
tileBounds(tile, cameraOrigin, out) {
|
|
@@ -815,14 +963,87 @@ function createFlatSurface(cfg) {
|
|
|
815
963
|
out.cy = centerY - cameraOrigin.y;
|
|
816
964
|
out.cz = centerZ - cameraOrigin.z;
|
|
817
965
|
out.r = 0.7071067811865476 * size + maxHeight;
|
|
966
|
+
},
|
|
967
|
+
rootTiles(_cameraOrigin, out) {
|
|
968
|
+
const root = out[0];
|
|
969
|
+
root.space = 0;
|
|
970
|
+
root.level = 0;
|
|
971
|
+
root.x = 0;
|
|
972
|
+
root.y = 0;
|
|
973
|
+
return 1;
|
|
818
974
|
}
|
|
819
975
|
};
|
|
820
976
|
return surface;
|
|
821
977
|
}
|
|
822
978
|
|
|
979
|
+
function createInfiniteFlatSurface(cfg) {
|
|
980
|
+
const halfRoot = 0.5 * cfg.rootSize;
|
|
981
|
+
const maxHeight = cfg.maxHeight ?? 0;
|
|
982
|
+
const rootGridRadius = Math.max(0, Math.floor(cfg.rootGridRadius ?? 1));
|
|
983
|
+
const rootWidth = rootGridRadius * 2 + 1;
|
|
984
|
+
return {
|
|
985
|
+
spaceCount: 1,
|
|
986
|
+
maxRootCount: rootWidth * rootWidth,
|
|
987
|
+
neighborSameLevel(tile, dir, out) {
|
|
988
|
+
let nx = tile.x;
|
|
989
|
+
let ny = tile.y;
|
|
990
|
+
switch (dir) {
|
|
991
|
+
case Dir.LEFT:
|
|
992
|
+
nx = tile.x - 1;
|
|
993
|
+
break;
|
|
994
|
+
case Dir.RIGHT:
|
|
995
|
+
nx = tile.x + 1;
|
|
996
|
+
break;
|
|
997
|
+
case Dir.TOP:
|
|
998
|
+
ny = tile.y - 1;
|
|
999
|
+
break;
|
|
1000
|
+
case Dir.BOTTOM:
|
|
1001
|
+
ny = tile.y + 1;
|
|
1002
|
+
break;
|
|
1003
|
+
}
|
|
1004
|
+
out.space = tile.space;
|
|
1005
|
+
out.level = tile.level;
|
|
1006
|
+
out.x = nx;
|
|
1007
|
+
out.y = ny;
|
|
1008
|
+
return true;
|
|
1009
|
+
},
|
|
1010
|
+
tileBounds(tile, cameraOrigin, out) {
|
|
1011
|
+
const level = tile.level;
|
|
1012
|
+
const scale = 1 / (1 << level);
|
|
1013
|
+
const size = cfg.rootSize * scale;
|
|
1014
|
+
const minX = cfg.origin.x + (tile.x * size - halfRoot);
|
|
1015
|
+
const minZ = cfg.origin.z + (tile.y * size - halfRoot);
|
|
1016
|
+
const centerX = minX + 0.5 * size;
|
|
1017
|
+
const centerY = cfg.origin.y;
|
|
1018
|
+
const centerZ = minZ + 0.5 * size;
|
|
1019
|
+
out.cx = centerX - cameraOrigin.x;
|
|
1020
|
+
out.cy = centerY - cameraOrigin.y;
|
|
1021
|
+
out.cz = centerZ - cameraOrigin.z;
|
|
1022
|
+
out.r = 0.7071067811865476 * size + maxHeight;
|
|
1023
|
+
},
|
|
1024
|
+
rootTiles(cameraOrigin, out) {
|
|
1025
|
+
const camRootX = Math.floor((cameraOrigin.x - cfg.origin.x + halfRoot) / cfg.rootSize);
|
|
1026
|
+
const camRootY = Math.floor((cameraOrigin.z - cfg.origin.z + halfRoot) / cfg.rootSize);
|
|
1027
|
+
let index = 0;
|
|
1028
|
+
for (let dy = -rootGridRadius; dy <= rootGridRadius; dy++) {
|
|
1029
|
+
for (let dx = -rootGridRadius; dx <= rootGridRadius; dx++) {
|
|
1030
|
+
const root = out[index];
|
|
1031
|
+
root.space = 0;
|
|
1032
|
+
root.level = 0;
|
|
1033
|
+
root.x = camRootX + dx;
|
|
1034
|
+
root.y = camRootY + dy;
|
|
1035
|
+
index++;
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
return index;
|
|
1039
|
+
}
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
|
|
823
1043
|
function createCubeSphereSurface(_cfg) {
|
|
824
1044
|
return {
|
|
825
1045
|
spaceCount: 6,
|
|
1046
|
+
maxRootCount: 6,
|
|
826
1047
|
neighborSameLevel(_tile, _dir, _out) {
|
|
827
1048
|
return false;
|
|
828
1049
|
},
|
|
@@ -831,41 +1052,38 @@ function createCubeSphereSurface(_cfg) {
|
|
|
831
1052
|
out.cy = 0;
|
|
832
1053
|
out.cz = 0;
|
|
833
1054
|
out.r = Number.MAX_VALUE;
|
|
1055
|
+
},
|
|
1056
|
+
rootTiles(_cameraOrigin, out) {
|
|
1057
|
+
for (let s = 0; s < 6; s++) {
|
|
1058
|
+
const root = out[s];
|
|
1059
|
+
root.space = s;
|
|
1060
|
+
root.level = 0;
|
|
1061
|
+
root.x = 0;
|
|
1062
|
+
root.y = 0;
|
|
1063
|
+
}
|
|
1064
|
+
return 6;
|
|
834
1065
|
}
|
|
835
1066
|
};
|
|
836
1067
|
}
|
|
837
1068
|
|
|
838
|
-
const
|
|
839
|
-
|
|
840
|
-
const rootSize = work.param(256).displayName("rootSize");
|
|
841
|
-
const origin = work.param({ x: 0, y: 0, z: 0 }).displayName(
|
|
842
|
-
"origin"
|
|
843
|
-
);
|
|
844
|
-
const innerTileSegments = work.param(14).displayName("innerTileSegments");
|
|
845
|
-
const skirtScale = work.param(100).displayName("skirtScale");
|
|
846
|
-
const heightmapScale = work.param(1).displayName("heightmapScale");
|
|
847
|
-
const maxNodes = work.param(1028).displayName("maxNodes");
|
|
848
|
-
const maxLevel = work.param(16).displayName("maxLevel");
|
|
849
|
-
const quadtreeUpdate = work.param({
|
|
850
|
-
cameraOrigin: { x: 0, y: 0, z: 0 },
|
|
851
|
-
mode: "distance",
|
|
852
|
-
distanceFactor: 1.5
|
|
853
|
-
}).displayName("quadtreeUpdate");
|
|
854
|
-
|
|
855
|
-
const quadtreeConfigTask = work.task((get, work) => {
|
|
1069
|
+
const surfaceTask = work.task((get, work) => {
|
|
1070
|
+
const customSurface = get(surface);
|
|
856
1071
|
const rootSizeVal = get(rootSize);
|
|
857
1072
|
const originVal = get(origin);
|
|
1073
|
+
return work(() => {
|
|
1074
|
+
if (customSurface) return customSurface;
|
|
1075
|
+
return createFlatSurface({ rootSize: rootSizeVal, origin: originVal });
|
|
1076
|
+
});
|
|
1077
|
+
}).displayName("surfaceTask");
|
|
1078
|
+
const quadtreeConfigTask = work.task((get, work) => {
|
|
1079
|
+
const surfaceVal = get(surfaceTask);
|
|
858
1080
|
const maxNodesVal = get(maxNodes);
|
|
859
1081
|
const maxLevelVal = get(maxLevel);
|
|
860
1082
|
return work(() => {
|
|
861
|
-
const
|
|
862
|
-
rootSize: rootSizeVal,
|
|
863
|
-
origin: originVal
|
|
864
|
-
});
|
|
865
|
-
const state = createState({ maxNodes: maxNodesVal, maxLevel: maxLevelVal }, surface);
|
|
1083
|
+
const state = createState({ maxNodes: maxNodesVal, maxLevel: maxLevelVal }, surfaceVal);
|
|
866
1084
|
return {
|
|
867
1085
|
state,
|
|
868
|
-
surface
|
|
1086
|
+
surface: surfaceVal
|
|
869
1087
|
};
|
|
870
1088
|
});
|
|
871
1089
|
}).displayName("quadtreeConfigTask");
|
|
@@ -885,12 +1103,7 @@ const quadtreeUpdateTask = work.task((get, work) => {
|
|
|
885
1103
|
}).displayName("quadtreeUpdateTask");
|
|
886
1104
|
const leafStorageTask = work.task((get, work) => {
|
|
887
1105
|
const maxNodesVal = get(maxNodes);
|
|
888
|
-
return work(() =>
|
|
889
|
-
const data = new Int32Array(maxNodesVal * 4);
|
|
890
|
-
const attribute = new webgpu.StorageBufferAttribute(data, 4);
|
|
891
|
-
const node = tsl.storage(attribute, "i32", 1).toReadOnly();
|
|
892
|
-
return { data, attribute, node };
|
|
893
|
-
});
|
|
1106
|
+
return work(() => createLeafStorage(maxNodesVal));
|
|
894
1107
|
}).displayName("leafStorageTask");
|
|
895
1108
|
const leafGpuBufferTask = work.task((get, work) => {
|
|
896
1109
|
const leafSet = get(quadtreeUpdateTask);
|
|
@@ -916,6 +1129,23 @@ const leafGpuBufferTask = work.task((get, work) => {
|
|
|
916
1129
|
});
|
|
917
1130
|
}).displayName("leafGpuBufferTask");
|
|
918
1131
|
|
|
1132
|
+
function createElevationFunction(callback) {
|
|
1133
|
+
const tslFunction = (args) => {
|
|
1134
|
+
const params = {
|
|
1135
|
+
worldPosition: args.worldPosition,
|
|
1136
|
+
rootSize: args.rootSize,
|
|
1137
|
+
rootUV: args.rootUV,
|
|
1138
|
+
tileUV: args.tileUV,
|
|
1139
|
+
tileLevel: args.tileLevel,
|
|
1140
|
+
tileSize: args.tileSize,
|
|
1141
|
+
tileOriginVec2: args.tileOriginVec2,
|
|
1142
|
+
nodeIndex: args.nodeIndex
|
|
1143
|
+
};
|
|
1144
|
+
return callback(params);
|
|
1145
|
+
};
|
|
1146
|
+
return TSL_js.Fn(tslFunction);
|
|
1147
|
+
}
|
|
1148
|
+
|
|
919
1149
|
function createTerrainUniforms(params) {
|
|
920
1150
|
const sanitizedId = params.instanceId?.replace(/-/g, "_");
|
|
921
1151
|
const suffix = sanitizedId ? `_${sanitizedId}` : "";
|
|
@@ -927,16 +1157,18 @@ function createTerrainUniforms(params) {
|
|
|
927
1157
|
`uInnerTileSegments${suffix}`
|
|
928
1158
|
);
|
|
929
1159
|
const uSkirtScale = tsl.uniform(tsl.float(params.skirtScale)).setName(`uSkirtScale${suffix}`);
|
|
930
|
-
const
|
|
1160
|
+
const uElevationScale = tsl.uniform(tsl.float(params.elevationScale)).setName(`uElevationScale${suffix}`);
|
|
931
1161
|
return {
|
|
932
1162
|
uRootOrigin,
|
|
933
1163
|
uRootSize,
|
|
934
1164
|
uInnerTileSegments,
|
|
935
1165
|
uSkirtScale,
|
|
936
|
-
|
|
1166
|
+
uElevationScale
|
|
937
1167
|
};
|
|
938
1168
|
}
|
|
939
1169
|
|
|
1170
|
+
const instanceIdTask = work.task(() => crypto.randomUUID()).displayName("instanceIdTask").cache("once");
|
|
1171
|
+
|
|
940
1172
|
const scratchVector3 = new three.Vector3();
|
|
941
1173
|
const createUniformsTask = work.task((get, work) => {
|
|
942
1174
|
const uniformParams = {
|
|
@@ -944,18 +1176,18 @@ const createUniformsTask = work.task((get, work) => {
|
|
|
944
1176
|
rootSize: get(rootSize),
|
|
945
1177
|
innerTileSegments: get(innerTileSegments),
|
|
946
1178
|
skirtScale: get(skirtScale),
|
|
947
|
-
|
|
1179
|
+
elevationScale: get(elevationScale),
|
|
948
1180
|
instanceId: get(instanceIdTask)
|
|
949
1181
|
};
|
|
950
1182
|
return work(() => createTerrainUniforms(uniformParams));
|
|
951
|
-
}).displayName("
|
|
1183
|
+
}).displayName("createUniformsTask").cache("once");
|
|
952
1184
|
const updateUniformsTask = work.task((get, work) => {
|
|
953
1185
|
const terrainUniformsContext = get(createUniformsTask);
|
|
954
1186
|
const rootSizeVal = get(rootSize);
|
|
955
1187
|
const rootOrigin = get(origin);
|
|
956
1188
|
const innerTileSegmentsVal = get(innerTileSegments);
|
|
957
1189
|
const skirtScaleVal = get(skirtScale);
|
|
958
|
-
const
|
|
1190
|
+
const elevationScaleVal = get(elevationScale);
|
|
959
1191
|
return work(() => {
|
|
960
1192
|
terrainUniformsContext.uRootSize.value = rootSizeVal;
|
|
961
1193
|
terrainUniformsContext.uRootOrigin.value = scratchVector3.set(
|
|
@@ -965,31 +1197,313 @@ const updateUniformsTask = work.task((get, work) => {
|
|
|
965
1197
|
);
|
|
966
1198
|
terrainUniformsContext.uInnerTileSegments.value = innerTileSegmentsVal;
|
|
967
1199
|
terrainUniformsContext.uSkirtScale.value = skirtScaleVal;
|
|
968
|
-
terrainUniformsContext.
|
|
1200
|
+
terrainUniformsContext.uElevationScale.value = elevationScaleVal;
|
|
969
1201
|
return terrainUniformsContext;
|
|
970
1202
|
});
|
|
971
|
-
}).displayName("
|
|
1203
|
+
}).displayName("updateUniformsTask");
|
|
1204
|
+
|
|
1205
|
+
const createElevationFieldContextTask = work.task((get, work) => {
|
|
1206
|
+
const edgeVertexCount = get(innerTileSegments) + 3;
|
|
1207
|
+
const verticesPerNode = edgeVertexCount * edgeVertexCount;
|
|
1208
|
+
const totalElements = get(maxNodes) * verticesPerNode;
|
|
1209
|
+
return work(() => {
|
|
1210
|
+
const data = new Float32Array(totalElements);
|
|
1211
|
+
const attribute = new webgpu.StorageBufferAttribute(data, 1);
|
|
1212
|
+
const node = tsl.storage(attribute, "float", totalElements);
|
|
1213
|
+
return {
|
|
1214
|
+
data,
|
|
1215
|
+
attribute,
|
|
1216
|
+
node
|
|
1217
|
+
};
|
|
1218
|
+
});
|
|
1219
|
+
}).displayName("createElevationFieldContextTask");
|
|
1220
|
+
const tileNodesTask = work.task((get, work) => {
|
|
1221
|
+
const leafStorage = get(leafStorageTask);
|
|
1222
|
+
const uniforms = get(createUniformsTask);
|
|
1223
|
+
return work(() => {
|
|
1224
|
+
return createTileCompute(leafStorage, uniforms);
|
|
1225
|
+
});
|
|
1226
|
+
}).displayName("tileNodesTask");
|
|
1227
|
+
const elevationFieldStageTask = work.task((get, work) => {
|
|
1228
|
+
const tile = get(tileNodesTask);
|
|
1229
|
+
const uniforms = get(createUniformsTask);
|
|
1230
|
+
const elevationFieldContext = get(createElevationFieldContextTask);
|
|
1231
|
+
const userElevationFn = get(elevationFn);
|
|
1232
|
+
return work(() => {
|
|
1233
|
+
const heightFn = createElevationFunction(userElevationFn);
|
|
1234
|
+
const heightWriteFn = createElevation(tile, uniforms, heightFn);
|
|
1235
|
+
return [
|
|
1236
|
+
(nodeIndex, globalVertexIndex, _uv, localCoordinates) => {
|
|
1237
|
+
const height = heightWriteFn(nodeIndex, localCoordinates);
|
|
1238
|
+
elevationFieldContext.node.element(globalVertexIndex).assign(height);
|
|
1239
|
+
}
|
|
1240
|
+
];
|
|
1241
|
+
});
|
|
1242
|
+
}).displayName("elevationFieldStageTask");
|
|
1243
|
+
|
|
1244
|
+
const createNormalFieldContextTask = work.task((get, work) => {
|
|
1245
|
+
const edgeVertexCount = get(innerTileSegments) + 3;
|
|
1246
|
+
const verticesPerNode = edgeVertexCount * edgeVertexCount;
|
|
1247
|
+
const totalElements = get(maxNodes) * verticesPerNode;
|
|
1248
|
+
return work(() => {
|
|
1249
|
+
const data = new Uint32Array(totalElements);
|
|
1250
|
+
const attribute = new webgpu.StorageBufferAttribute(data, 1);
|
|
1251
|
+
const node = tsl.storage(attribute, "uint", totalElements);
|
|
1252
|
+
return {
|
|
1253
|
+
data,
|
|
1254
|
+
attribute,
|
|
1255
|
+
node
|
|
1256
|
+
};
|
|
1257
|
+
});
|
|
1258
|
+
}).displayName("createNormalFieldContextTask");
|
|
1259
|
+
function createNormalFromElevationField(elevationFieldNode, edgeVertexCount) {
|
|
1260
|
+
return tsl.Fn(
|
|
1261
|
+
([nodeIndex, tileSize, ix, iy, elevationScale]) => {
|
|
1262
|
+
const iEdge = tsl.int(edgeVertexCount);
|
|
1263
|
+
const verticesPerNode = iEdge.mul(iEdge);
|
|
1264
|
+
const baseOffset = tsl.int(nodeIndex).mul(verticesPerNode);
|
|
1265
|
+
const xLeft = tsl.int(ix).sub(tsl.int(1));
|
|
1266
|
+
const xRight = tsl.int(ix).add(tsl.int(1));
|
|
1267
|
+
const yUp = tsl.int(iy).sub(tsl.int(1));
|
|
1268
|
+
const yDown = tsl.int(iy).add(tsl.int(1));
|
|
1269
|
+
const hLeft = elevationFieldNode.element(baseOffset.add(tsl.int(iy).mul(iEdge).add(xLeft))).mul(elevationScale);
|
|
1270
|
+
const hRight = elevationFieldNode.element(baseOffset.add(tsl.int(iy).mul(iEdge).add(xRight))).mul(elevationScale);
|
|
1271
|
+
const hUp = elevationFieldNode.element(baseOffset.add(yUp.mul(iEdge).add(tsl.int(ix)))).mul(elevationScale);
|
|
1272
|
+
const hDown = elevationFieldNode.element(baseOffset.add(yDown.mul(iEdge).add(tsl.int(ix)))).mul(elevationScale);
|
|
1273
|
+
const innerSegments = tsl.float(iEdge).sub(tsl.float(3));
|
|
1274
|
+
const stepWorld = tileSize.div(innerSegments);
|
|
1275
|
+
const inv2Step = tsl.float(0.5).div(stepWorld);
|
|
1276
|
+
const dhdx = tsl.float(hRight).sub(tsl.float(hLeft)).mul(inv2Step);
|
|
1277
|
+
const dhdz = tsl.float(hDown).sub(tsl.float(hUp)).mul(inv2Step);
|
|
1278
|
+
const normal = tsl.vec3(dhdx.negate(), tsl.float(1), dhdz.negate()).normalize();
|
|
1279
|
+
return tsl.vec2(normal.x, normal.z);
|
|
1280
|
+
}
|
|
1281
|
+
);
|
|
1282
|
+
}
|
|
1283
|
+
const normalFieldStageTask = work.task((get, work) => {
|
|
1284
|
+
const upstream = get(elevationFieldStageTask);
|
|
1285
|
+
const elevationFieldContext = get(createElevationFieldContextTask);
|
|
1286
|
+
const normalFieldContext = get(createNormalFieldContextTask);
|
|
1287
|
+
const tileEdgeVertexCount = get(innerTileSegments) + 3;
|
|
1288
|
+
const tile = get(tileNodesTask);
|
|
1289
|
+
const uniforms = get(createUniformsTask);
|
|
1290
|
+
return work(() => {
|
|
1291
|
+
const computeNormal = createNormalFromElevationField(
|
|
1292
|
+
elevationFieldContext.node,
|
|
1293
|
+
tileEdgeVertexCount
|
|
1294
|
+
);
|
|
1295
|
+
return [
|
|
1296
|
+
...upstream,
|
|
1297
|
+
(nodeIndex, globalVertexIndex, _uv, localCoordinates) => {
|
|
1298
|
+
const ix = tsl.int(localCoordinates.x);
|
|
1299
|
+
const iy = tsl.int(localCoordinates.y);
|
|
1300
|
+
const tileSize = tile.tileSize(nodeIndex);
|
|
1301
|
+
const normalXZ = computeNormal(
|
|
1302
|
+
nodeIndex,
|
|
1303
|
+
tileSize,
|
|
1304
|
+
ix,
|
|
1305
|
+
iy,
|
|
1306
|
+
uniforms.uElevationScale
|
|
1307
|
+
);
|
|
1308
|
+
normalFieldContext.node.element(globalVertexIndex).assign(tsl.packHalf2x16(normalXZ));
|
|
1309
|
+
}
|
|
1310
|
+
];
|
|
1311
|
+
});
|
|
1312
|
+
}).displayName("normalFieldStageTask");
|
|
1313
|
+
|
|
1314
|
+
const compileComputeTask = work.task((get, work) => {
|
|
1315
|
+
const pipeline = get(normalFieldStageTask);
|
|
1316
|
+
const edgeVertexCount = get(innerTileSegments) + 3;
|
|
1317
|
+
return work(() => compileComputePipeline(pipeline, edgeVertexCount));
|
|
1318
|
+
}).displayName("compileComputeTask");
|
|
1319
|
+
const executeComputeTask = work.task((get, work, { resources }) => {
|
|
1320
|
+
const { execute } = get(compileComputeTask);
|
|
1321
|
+
const leafState = get(leafGpuBufferTask);
|
|
1322
|
+
return work(
|
|
1323
|
+
() => resources?.renderer ? execute(resources.renderer, leafState.count) : () => {
|
|
1324
|
+
}
|
|
1325
|
+
);
|
|
1326
|
+
}).displayName("executeComputeTask").lane("gpu");
|
|
1327
|
+
function createComputePipelineTasks(leafStageTask) {
|
|
1328
|
+
const compile = work.task((get, work) => {
|
|
1329
|
+
const pipeline = get(leafStageTask);
|
|
1330
|
+
const edgeVertexCount = get(innerTileSegments) + 3;
|
|
1331
|
+
return work(() => compileComputePipeline(pipeline, edgeVertexCount));
|
|
1332
|
+
}).displayName("compileComputeTask");
|
|
1333
|
+
const execute = work.task((get, work, { resources }) => {
|
|
1334
|
+
const { execute: run } = get(compile);
|
|
1335
|
+
const leafState = get(leafGpuBufferTask);
|
|
1336
|
+
return work(() => resources?.renderer ? run(resources.renderer, leafState.count) : () => {
|
|
1337
|
+
});
|
|
1338
|
+
}).displayName("executeComputeTask").lane("gpu");
|
|
1339
|
+
return { compile, execute };
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
const textureSpaceToVectorSpace = tsl.Fn(([value]) => {
|
|
1343
|
+
return tsl.remap(value, tsl.float(0), tsl.float(1), tsl.float(-1), tsl.float(1));
|
|
1344
|
+
});
|
|
1345
|
+
const vectorSpaceToTextureSpace = tsl.Fn(([value]) => {
|
|
1346
|
+
return tsl.remap(value, tsl.float(-1), tsl.float(1), tsl.float(0), tsl.float(1));
|
|
1347
|
+
});
|
|
1348
|
+
const blendAngleCorrectedNormals = tsl.Fn(([n1, n2]) => {
|
|
1349
|
+
const t = tsl.vec3(n1.x, n1.y, n1.z.add(1));
|
|
1350
|
+
const u = tsl.vec3(n2.x.negate(), n2.y.negate(), n2.z);
|
|
1351
|
+
const r = t.mul(tsl.dot(t, u)).sub(u.mul(t.z)).normalize();
|
|
1352
|
+
return r;
|
|
1353
|
+
});
|
|
1354
|
+
const deriveNormalZ = tsl.Fn(([normalXY]) => {
|
|
1355
|
+
const xy = normalXY.toVar();
|
|
1356
|
+
const z = xy.x.mul(xy.x).add(xy.y.mul(xy.y)).oneMinus().max(0).sqrt();
|
|
1357
|
+
return tsl.vec3(xy.x, xy.y, z);
|
|
1358
|
+
});
|
|
1359
|
+
|
|
1360
|
+
const isSkirtVertex = tsl.Fn(([segments]) => {
|
|
1361
|
+
const segmentsNode = typeof segments === "number" ? tsl.int(segments) : segments;
|
|
1362
|
+
const vIndex = tsl.int(tsl.vertexIndex);
|
|
1363
|
+
const segmentEdges = tsl.int(segmentsNode.add(3));
|
|
1364
|
+
const vx = vIndex.mod(segmentEdges);
|
|
1365
|
+
const vy = vIndex.div(segmentEdges);
|
|
1366
|
+
const last = segmentEdges.sub(tsl.int(1));
|
|
1367
|
+
return vx.equal(tsl.int(0)).or(vx.equal(last)).or(vy.equal(tsl.int(0))).or(vy.equal(last));
|
|
1368
|
+
});
|
|
1369
|
+
const isSkirtUV = tsl.Fn(([segments]) => {
|
|
1370
|
+
const segmentsNode = typeof segments === "number" ? tsl.int(segments) : segments;
|
|
1371
|
+
const ux = tsl.uv().x;
|
|
1372
|
+
const uy = tsl.uv().y;
|
|
1373
|
+
const segmentCount = segmentsNode.add(2);
|
|
1374
|
+
const segmentStep = tsl.float(1).div(segmentCount);
|
|
1375
|
+
const innerX = ux.greaterThan(segmentStep).and(ux.lessThan(segmentStep.oneMinus()));
|
|
1376
|
+
const innerY = uy.greaterThan(segmentStep).and(uy.lessThan(segmentStep.oneMinus()));
|
|
1377
|
+
return innerX.and(innerY).not();
|
|
1378
|
+
});
|
|
1379
|
+
|
|
1380
|
+
function createTileBaseWorldPosition(leafStorage, terrainUniforms) {
|
|
1381
|
+
return tsl.Fn(() => {
|
|
1382
|
+
const nodeIndex = tsl.int(tsl.instanceIndex);
|
|
1383
|
+
const nodeOffset = nodeIndex.mul(tsl.int(4));
|
|
1384
|
+
const nodeLevel = leafStorage.node.element(nodeOffset).toInt();
|
|
1385
|
+
const nodeX = leafStorage.node.element(nodeOffset.add(tsl.int(1))).toFloat();
|
|
1386
|
+
const nodeY = leafStorage.node.element(nodeOffset.add(tsl.int(2))).toFloat();
|
|
1387
|
+
const rootSize = terrainUniforms.uRootSize.toVar();
|
|
1388
|
+
const rootOrigin = terrainUniforms.uRootOrigin.toVar();
|
|
1389
|
+
const half = tsl.float(0.5);
|
|
1390
|
+
const size = rootSize.div(tsl.pow(tsl.float(2), nodeLevel.toFloat()));
|
|
1391
|
+
const halfRoot = rootSize.mul(half);
|
|
1392
|
+
const centerX = rootOrigin.x.add(nodeX.add(half).mul(size)).sub(halfRoot);
|
|
1393
|
+
const centerZ = rootOrigin.z.add(nodeY.add(half).mul(size)).sub(halfRoot);
|
|
1394
|
+
const clampedX = tsl.positionLocal.x.max(half.negate()).min(half);
|
|
1395
|
+
const clampedZ = tsl.positionLocal.z.max(half.negate()).min(half);
|
|
1396
|
+
const worldX = centerX.add(clampedX.mul(size));
|
|
1397
|
+
const worldZ = centerZ.add(clampedZ.mul(size));
|
|
1398
|
+
return tsl.vec3(worldX, rootOrigin.y, worldZ);
|
|
1399
|
+
});
|
|
1400
|
+
}
|
|
1401
|
+
function createTileElevation(terrainUniforms, elevationFieldBufferNode) {
|
|
1402
|
+
if (!elevationFieldBufferNode) return tsl.float(0);
|
|
1403
|
+
const edgeVertexCount = terrainUniforms.uInnerTileSegments.add(3);
|
|
1404
|
+
return readElevationFieldAtPositionLocal(
|
|
1405
|
+
elevationFieldBufferNode,
|
|
1406
|
+
edgeVertexCount,
|
|
1407
|
+
tsl.positionLocal
|
|
1408
|
+
)().mul(
|
|
1409
|
+
terrainUniforms.uElevationScale
|
|
1410
|
+
);
|
|
1411
|
+
}
|
|
1412
|
+
function createNormalAssignment(terrainUniforms, normalFieldBufferNode) {
|
|
1413
|
+
if (!normalFieldBufferNode) return;
|
|
1414
|
+
const nodeIndex = tsl.int(tsl.instanceIndex);
|
|
1415
|
+
const intEdge = tsl.int(terrainUniforms.uInnerTileSegments.add(3));
|
|
1416
|
+
const verticesPerNode = intEdge.mul(intEdge);
|
|
1417
|
+
const globalVertexIndex = nodeIndex.mul(verticesPerNode).add(tsl.int(tsl.vertexIndex));
|
|
1418
|
+
const packed = normalFieldBufferNode.element(globalVertexIndex);
|
|
1419
|
+
const normalXZ = tsl.unpackHalf2x16(packed);
|
|
1420
|
+
const reconstructed = deriveNormalZ(normalXZ);
|
|
1421
|
+
tsl.normalLocal.assign(tsl.vec3(reconstructed.x, reconstructed.z, reconstructed.y));
|
|
1422
|
+
}
|
|
1423
|
+
function createTileWorldPosition(leafStorage, terrainUniforms, elevationFieldBufferNode, normalFieldBufferNode) {
|
|
1424
|
+
const baseWorldPosition = createTileBaseWorldPosition(leafStorage, terrainUniforms);
|
|
1425
|
+
return tsl.Fn(() => {
|
|
1426
|
+
const base = baseWorldPosition();
|
|
1427
|
+
const yElevation = createTileElevation(terrainUniforms, elevationFieldBufferNode);
|
|
1428
|
+
const skirtVertex = isSkirtVertex(terrainUniforms.uInnerTileSegments);
|
|
1429
|
+
const skirtY = base.y.add(yElevation).sub(terrainUniforms.uSkirtScale.toVar());
|
|
1430
|
+
const worldY = tsl.select(skirtVertex, skirtY, base.y.add(yElevation));
|
|
1431
|
+
createNormalAssignment(terrainUniforms, normalFieldBufferNode);
|
|
1432
|
+
return tsl.vec3(base.x, worldY, base.z);
|
|
1433
|
+
})();
|
|
1434
|
+
}
|
|
972
1435
|
|
|
973
1436
|
const positionNodeTask = work.task((get, work) => {
|
|
974
1437
|
const leafStorage = get(leafStorageTask);
|
|
975
1438
|
const terrainUniforms = get(createUniformsTask);
|
|
976
|
-
|
|
977
|
-
|
|
1439
|
+
const elevationFieldContext = get(createElevationFieldContextTask);
|
|
1440
|
+
const normalFieldContext = get(createNormalFieldContextTask);
|
|
1441
|
+
return work(
|
|
1442
|
+
() => createTileWorldPosition(
|
|
1443
|
+
leafStorage,
|
|
1444
|
+
terrainUniforms,
|
|
1445
|
+
elevationFieldContext.node,
|
|
1446
|
+
normalFieldContext.node
|
|
1447
|
+
)
|
|
1448
|
+
);
|
|
1449
|
+
}).displayName("positionNodeTask");
|
|
978
1450
|
|
|
979
1451
|
function terrainGraph() {
|
|
980
|
-
return work.graph().add(instanceIdTask).add(quadtreeConfigTask).add(quadtreeUpdateTask).add(leafStorageTask).add(leafGpuBufferTask).add(createUniformsTask).add(updateUniformsTask).add(positionNodeTask);
|
|
1452
|
+
return work.graph().add(instanceIdTask).add(quadtreeConfigTask).add(quadtreeUpdateTask).add(leafStorageTask).add(surfaceTask).add(leafGpuBufferTask).add(createUniformsTask).add(updateUniformsTask).add(positionNodeTask).add(createElevationFieldContextTask).add(tileNodesTask).add(createNormalFieldContextTask).add(elevationFieldStageTask).add(normalFieldStageTask).add(compileComputeTask).add(executeComputeTask);
|
|
981
1453
|
}
|
|
982
1454
|
const terrainTasks = {
|
|
983
1455
|
instanceId: instanceIdTask,
|
|
984
1456
|
quadtreeConfig: quadtreeConfigTask,
|
|
985
1457
|
quadtreeUpdate: quadtreeUpdateTask,
|
|
986
1458
|
leafStorage: leafStorageTask,
|
|
1459
|
+
surface: surfaceTask,
|
|
987
1460
|
leafGpuBuffer: leafGpuBufferTask,
|
|
988
1461
|
createUniforms: createUniformsTask,
|
|
989
1462
|
updateUniforms: updateUniformsTask,
|
|
990
|
-
positionNode: positionNodeTask
|
|
1463
|
+
positionNode: positionNodeTask,
|
|
1464
|
+
createElevationFieldContext: createElevationFieldContextTask,
|
|
1465
|
+
createTileNodes: tileNodesTask,
|
|
1466
|
+
createNormalFieldContext: createNormalFieldContextTask,
|
|
1467
|
+
elevationFieldStage: elevationFieldStageTask,
|
|
1468
|
+
normalFieldStage: normalFieldStageTask,
|
|
1469
|
+
compileCompute: compileComputeTask,
|
|
1470
|
+
executeCompute: executeComputeTask
|
|
991
1471
|
};
|
|
992
1472
|
|
|
1473
|
+
const vGlobalVertexIndex = /* @__PURE__ */ tsl.varyingProperty("int", "vGlobalVertexIndex");
|
|
1474
|
+
const vElevation = /* @__PURE__ */ tsl.varyingProperty("f32", "vElevation");
|
|
1475
|
+
|
|
1476
|
+
const cellCenter = tsl.Fn(({ cell }) => {
|
|
1477
|
+
return cell.add(tsl.mx_noise_float(cell.mul(Math.PI)));
|
|
1478
|
+
});
|
|
1479
|
+
const voronoiCells = tsl.Fn((params) => {
|
|
1480
|
+
const scale = tsl.float(params.scale);
|
|
1481
|
+
const facet = tsl.float(params.facet);
|
|
1482
|
+
const seed = tsl.float(params.seed);
|
|
1483
|
+
const pos = params.uv.mul(scale).add(seed);
|
|
1484
|
+
const midCell = pos.round().toVar();
|
|
1485
|
+
const minCell = midCell.toVar();
|
|
1486
|
+
const minDist = tsl.float(1).toVar();
|
|
1487
|
+
const cell = tsl.vec3(0, 0, 0).toVar();
|
|
1488
|
+
const dist = tsl.float().toVar();
|
|
1489
|
+
const i = tsl.float(0).toVar();
|
|
1490
|
+
tsl.Loop(27, () => {
|
|
1491
|
+
const ix = i.mod(3).sub(1);
|
|
1492
|
+
const iy = i.div(3).floor().mod(3).sub(1);
|
|
1493
|
+
const iz = i.div(9).floor().sub(1);
|
|
1494
|
+
cell.assign(midCell.add(tsl.vec3(ix, iy, iz)));
|
|
1495
|
+
dist.assign(pos.distance(cellCenter({ cell })).add(tsl.mx_noise_float(pos).div(5)));
|
|
1496
|
+
tsl.If(dist.lessThan(minDist), () => {
|
|
1497
|
+
minDist.assign(dist);
|
|
1498
|
+
minCell.assign(cell);
|
|
1499
|
+
});
|
|
1500
|
+
i.addAssign(1);
|
|
1501
|
+
});
|
|
1502
|
+
const n = tsl.mx_noise_float(minCell.mul(Math.PI)).toVar();
|
|
1503
|
+
const k = tsl.mix(minDist, n.add(1).div(2), facet);
|
|
1504
|
+
return k;
|
|
1505
|
+
});
|
|
1506
|
+
|
|
993
1507
|
exports.Dir = Dir;
|
|
994
1508
|
exports.TerrainGeometry = TerrainGeometry;
|
|
995
1509
|
exports.TerrainMesh = TerrainMesh;
|
|
@@ -1000,15 +1514,22 @@ exports.beginUpdate = beginUpdate;
|
|
|
1000
1514
|
exports.blendAngleCorrectedNormals = blendAngleCorrectedNormals;
|
|
1001
1515
|
exports.buildLeafIndex = buildLeafIndex;
|
|
1002
1516
|
exports.buildSeams2to1 = buildSeams2to1;
|
|
1517
|
+
exports.compileComputeTask = compileComputeTask;
|
|
1518
|
+
exports.createComputePipelineTasks = createComputePipelineTasks;
|
|
1003
1519
|
exports.createCubeSphereSurface = createCubeSphereSurface;
|
|
1520
|
+
exports.createElevationFieldContextTask = createElevationFieldContextTask;
|
|
1004
1521
|
exports.createFlatSurface = createFlatSurface;
|
|
1522
|
+
exports.createInfiniteFlatSurface = createInfiniteFlatSurface;
|
|
1523
|
+
exports.createNormalFieldContextTask = createNormalFieldContextTask;
|
|
1005
1524
|
exports.createSpatialIndex = createSpatialIndex;
|
|
1006
1525
|
exports.createState = createState;
|
|
1007
1526
|
exports.createTerrainUniforms = createTerrainUniforms;
|
|
1008
|
-
exports.createTileWorldPosition = createTileWorldPosition;
|
|
1009
1527
|
exports.createUniformsTask = createUniformsTask;
|
|
1010
1528
|
exports.deriveNormalZ = deriveNormalZ;
|
|
1011
|
-
exports.
|
|
1529
|
+
exports.elevationFieldStageTask = elevationFieldStageTask;
|
|
1530
|
+
exports.elevationFn = elevationFn;
|
|
1531
|
+
exports.elevationScale = elevationScale;
|
|
1532
|
+
exports.executeComputeTask = executeComputeTask;
|
|
1012
1533
|
exports.innerTileSegments = innerTileSegments;
|
|
1013
1534
|
exports.instanceIdTask = instanceIdTask;
|
|
1014
1535
|
exports.isSkirtUV = isSkirtUV;
|
|
@@ -1017,6 +1538,7 @@ exports.leafGpuBufferTask = leafGpuBufferTask;
|
|
|
1017
1538
|
exports.leafStorageTask = leafStorageTask;
|
|
1018
1539
|
exports.maxLevel = maxLevel;
|
|
1019
1540
|
exports.maxNodes = maxNodes;
|
|
1541
|
+
exports.normalFieldStageTask = normalFieldStageTask;
|
|
1020
1542
|
exports.origin = origin;
|
|
1021
1543
|
exports.positionNodeTask = positionNodeTask;
|
|
1022
1544
|
exports.quadtreeConfigTask = quadtreeConfigTask;
|
|
@@ -1026,9 +1548,15 @@ exports.resetLeafSet = resetLeafSet;
|
|
|
1026
1548
|
exports.resetSeamTable = resetSeamTable;
|
|
1027
1549
|
exports.rootSize = rootSize;
|
|
1028
1550
|
exports.skirtScale = skirtScale;
|
|
1551
|
+
exports.surface = surface;
|
|
1552
|
+
exports.surfaceTask = surfaceTask;
|
|
1029
1553
|
exports.terrainGraph = terrainGraph;
|
|
1030
1554
|
exports.terrainTasks = terrainTasks;
|
|
1031
1555
|
exports.textureSpaceToVectorSpace = textureSpaceToVectorSpace;
|
|
1556
|
+
exports.tileNodesTask = tileNodesTask;
|
|
1032
1557
|
exports.update = update;
|
|
1033
1558
|
exports.updateUniformsTask = updateUniformsTask;
|
|
1559
|
+
exports.vElevation = vElevation;
|
|
1560
|
+
exports.vGlobalVertexIndex = vGlobalVertexIndex;
|
|
1034
1561
|
exports.vectorSpaceToTextureSpace = vectorSpaceToTextureSpace;
|
|
1562
|
+
exports.voronoiCells = voronoiCells;
|