@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/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 (consistent diagonal, no rotational symmetry):
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: consistent diagonal direction (all triangles "point" the same way)
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 = true;
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: 2048,
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 textureSpaceToVectorSpace = tsl.Fn(([value]) => {
254
- return tsl.remap(value, tsl.float(0), tsl.float(1), tsl.float(-1), tsl.float(1));
255
- });
256
- const vectorSpaceToTextureSpace = tsl.Fn(([value]) => {
257
- return tsl.remap(value, tsl.float(-1), tsl.float(1), tsl.float(0), tsl.float(1));
258
- });
259
- const blendAngleCorrectedNormals = tsl.Fn(([n1, n2]) => {
260
- const t = tsl.vec3(n1.x, n1.y, n1.z.add(1));
261
- const u = tsl.vec3(n2.x.negate(), n2.y.negate(), n2.z);
262
- const r = t.mul(tsl.dot(t, u)).sub(u.mul(t.z)).normalize();
263
- return r;
264
- });
265
- const deriveNormalZ = tsl.Fn(([normalXY]) => {
266
- const xy = normalXY.toVar();
267
- const z = xy.x.mul(xy.x).add(xy.y.mul(xy.y)).oneMinus().max(0).sqrt();
268
- return tsl.vec3(xy.x, xy.y, z);
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 isSkirtVertex = tsl.Fn(([segments]) => {
272
- const segmentsNode = typeof segments === "number" ? tsl.int(segments) : segments;
273
- const vIndex = tsl.int(tsl.vertexIndex);
274
- const segmentEdges = tsl.int(segmentsNode.add(3));
275
- const vx = vIndex.mod(segmentEdges);
276
- const vy = vIndex.div(segmentEdges);
277
- const last = segmentEdges.sub(tsl.int(1));
278
- return vx.equal(tsl.int(0)).or(vx.equal(last)).or(vy.equal(tsl.int(0))).or(vy.equal(last));
279
- });
280
- const isSkirtUV = tsl.Fn(([segments]) => {
281
- const segmentsNode = typeof segments === "number" ? tsl.int(segments) : segments;
282
- const ux = tsl.uv().x;
283
- const uy = tsl.uv().y;
284
- const segmentCount = segmentsNode.add(2);
285
- const segmentStep = tsl.float(1).div(segmentCount);
286
- const innerX = ux.greaterThan(segmentStep).and(ux.lessThan(segmentStep.oneMinus()));
287
- const innerY = uy.greaterThan(segmentStep).and(uy.lessThan(segmentStep.oneMinus()));
288
- return innerX.and(innerY).not();
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 createTileWorldPosition(leafStorage, terrainUniforms) {
292
- return tsl.Fn(() => {
293
- const skirtVertex = isSkirtVertex(terrainUniforms.uInnerTileSegments);
294
- const nodeIndex = tsl.int(tsl.instanceIndex);
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
- const rootSize = terrainUniforms.uRootSize.toVar();
300
- const rootOrigin = terrainUniforms.uRootOrigin.toVar();
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 size = rootSize.div(tsl.pow(tsl.float(2), nodeLevel.toFloat()));
303
- const halfRoot = rootSize.mul(half);
304
- const centerX = rootOrigin.x.add(nodeX.add(half).mul(size)).sub(halfRoot);
305
- const centerZ = rootOrigin.z.add(nodeY.add(half).mul(size)).sub(halfRoot);
306
- const clampedX = tsl.positionLocal.x.max(half.negate()).min(half);
307
- const clampedZ = tsl.positionLocal.z.max(half.negate()).min(half);
308
- const worldX = centerX.add(clampedX.mul(size));
309
- const worldZ = centerZ.add(clampedZ.mul(size));
310
- const baseY = rootOrigin.y;
311
- const skirtY = baseY.sub(terrainUniforms.uSkirtScale.toVar());
312
- const worldY = tsl.select(skirtVertex, skirtY, baseY);
313
- return tsl.vec3(worldX, worldY, worldZ);
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 Uint32Array(capacity),
331
- y: new Uint32Array(capacity)
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 Uint32Array(maxNodes),
358
- y: new Uint32Array(maxNodes),
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 >>> 0;
380
- store.y[id] = tile.y >>> 0;
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
- for (let s = 0; s < surface.spaceCount; s++) {
519
- const rootId = allocNode(state.store, { space: s, level: 0, x: 0, y: 0 });
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.store.roots[s] = rootId;
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 s = 0; s < surface.spaceCount; s++) {
559
- stack[sp++] = store.roots[s];
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 >>> 0;
802
- out.y = ny >>> 0;
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 instanceIdTask = work.task(() => crypto.randomUUID()).displayName("terrainInstanceIdTask").cache("once");
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 surface = createFlatSurface({
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 uHeightmapScale = tsl.uniform(tsl.float(params.heightmapScale)).setName(`uHeightmapScale${suffix}`);
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
- uHeightmapScale
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
- heightmapScale: get(heightmapScale),
1179
+ elevationScale: get(elevationScale),
948
1180
  instanceId: get(instanceIdTask)
949
1181
  };
950
1182
  return work(() => createTerrainUniforms(uniformParams));
951
- }).displayName("createTerrainUniformsTask").cache("once");
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 heightmapScaleVal = get(heightmapScale);
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.uHeightmapScale.value = heightmapScaleVal;
1200
+ terrainUniformsContext.uElevationScale.value = elevationScaleVal;
969
1201
  return terrainUniformsContext;
970
1202
  });
971
- }).displayName("updateTerrainUniformsTask");
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
- return work(() => createTileWorldPosition(leafStorage, terrainUniforms));
977
- }).displayName("terrainVertextPositionNodeTask");
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.heightmapScale = heightmapScale;
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;