@hello-terrain/three 0.0.0-alpha.4 → 0.0.0-alpha.5

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