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

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.mjs CHANGED
@@ -1,24 +1,32 @@
1
- import { BufferGeometry, BufferAttribute, Vector3 as Vector3$1 } from 'three';
2
- import { MeshStandardNodeMaterial, InstancedMesh, InstancedBufferAttribute, StorageBufferAttribute, Vector3 } from 'three/webgpu';
1
+ import { BufferGeometry, BufferAttribute, RGBAFormat, ClampToEdgeWrapping, HalfFloatType, FloatType, LinearFilter, NearestFilter, Vector3 as Vector3$1 } from 'three';
2
+ import { MeshStandardNodeMaterial, InstancedMesh, InstancedBufferAttribute, StorageTexture, StorageArrayTexture, StorageBufferAttribute, Vector3 } from 'three/webgpu';
3
3
  import { param, task, graph } from '@hello-terrain/work';
4
- import { uniform, Fn, float, globalId, int, vec2, uint, workgroupBarrier, If, instanceIndex, min, max, pow, vec3, storage, packHalf2x16, remap, dot, vertexIndex, uv, select, positionLocal, unpackHalf2x16, normalLocal, varyingProperty, mx_noise_float, Loop, mix } from 'three/tsl';
4
+ import { uniform, Fn, float, globalId, int, vec2, uint, If, workgroupBarrier, textureStore, uvec3, vec4, ivec2, ivec3, textureLoad, instanceIndex, min, max, pow, vec3, storage, vertexIndex, uv, select, positionLocal, normalLocal, remap, dot, varyingProperty, mx_noise_float, Loop, mix } from 'three/tsl';
5
5
  import { Fn as Fn$1 } from 'three/src/nodes/TSL.js';
6
6
 
7
7
  class TerrainGeometry extends BufferGeometry {
8
8
  constructor(innerSegments = 14, extendUV = false) {
9
9
  super();
10
10
  if (innerSegments < 1 || !Number.isFinite(innerSegments) || !Number.isInteger(innerSegments)) {
11
- throw new Error(`Invalid innerSegments: ${innerSegments}. Must be a positive integer.`);
11
+ throw new Error(
12
+ `Invalid innerSegments: ${innerSegments}. Must be a positive integer.`
13
+ );
12
14
  }
13
15
  try {
14
16
  this.setIndex(this.generateIndices(innerSegments));
15
17
  this.setAttribute(
16
18
  "position",
17
- new BufferAttribute(new Float32Array(this.generatePositions(innerSegments)), 3)
19
+ new BufferAttribute(
20
+ new Float32Array(this.generatePositions(innerSegments)),
21
+ 3
22
+ )
18
23
  );
19
24
  this.setAttribute(
20
25
  "normal",
21
- new BufferAttribute(new Float32Array(this.generateNormals(innerSegments)), 3)
26
+ new BufferAttribute(
27
+ new Float32Array(this.generateNormals(innerSegments)),
28
+ 3
29
+ )
22
30
  );
23
31
  this.setAttribute(
24
32
  "uv",
@@ -54,17 +62,19 @@ class TerrainGeometry extends BufferGeometry {
54
62
  * | / | \ | / | \ |
55
63
  * o---o---o---o---o
56
64
  *
57
- * INNER GRID (consistent diagonal, no rotational symmetry):
58
- * o---o---o
59
- * | \ | \ |
60
- * o---o---o
61
- * | \ | \ |
62
- * o---o---o
65
+ * INNER GRID (alternating diagonals checkerboard pattern):
66
+ * o---o---o---o---o
67
+ * | \ | / | \ | / |
68
+ * o---o---o---o---o
69
+ * | / | \ | / | \ |
70
+ * o---o---o---o---o
71
+ * | \ | / | \ | / |
72
+ * o---o---o---o---o
63
73
  *
64
74
  * Where o = vertex
65
75
  * Each square cell is split into 2 triangles.
66
76
  * - Skirt cells (outer ring): diagonal flip based on quadrant for corner correctness
67
- * - Inner cells: consistent diagonal direction (all triangles "point" the same way)
77
+ * - Inner cells: alternating diagonal via (x+y)%2 to reduce interpolation artifacts
68
78
  *
69
79
  * Vertex layout (for innerSegments = 2):
70
80
  *
@@ -110,7 +120,7 @@ class TerrainGeometry extends BufferGeometry {
110
120
  const topHalf = y < mid;
111
121
  useDefaultDiagonal = leftHalf && topHalf || !leftHalf && !topHalf;
112
122
  } else {
113
- useDefaultDiagonal = true;
123
+ useDefaultDiagonal = (x + y) % 2 === 0;
114
124
  }
115
125
  if (useDefaultDiagonal) {
116
126
  indices.push(a, d, b);
@@ -264,42 +274,330 @@ class TerrainMesh extends InstancedMesh {
264
274
  }
265
275
  }
266
276
 
277
+ function getDeviceComputeLimits(renderer) {
278
+ const backend = renderer.backend;
279
+ const limits = backend?.device?.limits;
280
+ return {
281
+ maxWorkgroupSizeX: limits?.maxComputeWorkgroupSizeX ?? 256,
282
+ maxWorkgroupSizeY: limits?.maxComputeWorkgroupSizeY ?? 256,
283
+ maxWorkgroupInvocations: limits?.maxComputeWorkgroupInvocations ?? 256
284
+ };
285
+ }
286
+
267
287
  const WORKGROUP_X = 16;
268
288
  const WORKGROUP_Y = 16;
269
- function compileComputePipeline(stages, width, bindings) {
270
- const workgroupSize = [WORKGROUP_X, WORKGROUP_Y, 1];
271
- const dispatchX = Math.ceil(width / WORKGROUP_X);
272
- const dispatchY = Math.ceil(width / WORKGROUP_Y);
289
+ function compileComputePipeline(stages, width, options) {
290
+ const bindings = options?.bindings;
291
+ const preferredWorkgroup = options?.workgroupSize ?? [
292
+ WORKGROUP_X,
293
+ WORKGROUP_Y
294
+ ];
295
+ const preferSingleKernelWhenPossible = options?.preferSingleKernelWhenPossible ?? true;
273
296
  const uInstanceCount = uniform(0, "uint");
274
- const computeShader = Fn(() => {
275
- const fWidth = float(width);
276
- const activeIndex = globalId.z;
277
- const nodeIndex = int(activeIndex).toVar();
278
- const iWidth = int(width);
279
- const ix = int(globalId.x);
280
- const iy = int(globalId.y);
281
- const texelSize = vec2(1, 1).div(fWidth);
282
- const localCoordinates = vec2(globalId.x, globalId.y);
283
- const localUVCoords = localCoordinates.div(fWidth);
284
- const verticesPerNode = iWidth.mul(iWidth);
285
- const globalIndex = int(nodeIndex).mul(verticesPerNode).add(iy.mul(iWidth).add(ix));
286
- const inBounds = ix.lessThan(iWidth).and(iy.lessThan(iWidth)).and(uint(activeIndex).lessThan(uInstanceCount)).toVar();
287
- for (let i = 0; i < stages.length; i++) {
288
- if (i > 0) {
289
- workgroupBarrier();
297
+ let singleKernel;
298
+ const stagedKernelCache = /* @__PURE__ */ new Map();
299
+ function canRunSingleKernel(widthValue, limits) {
300
+ return widthValue <= limits.maxWorkgroupSizeX && widthValue <= limits.maxWorkgroupSizeY && widthValue * widthValue <= limits.maxWorkgroupInvocations;
301
+ }
302
+ function clampWorkgroupToLimits(requested, limits) {
303
+ let x = Math.max(1, Math.floor(requested[0]));
304
+ let y = Math.max(1, Math.floor(requested[1]));
305
+ x = Math.min(x, limits.maxWorkgroupSizeX);
306
+ y = Math.min(y, limits.maxWorkgroupSizeY);
307
+ y = Math.min(
308
+ y,
309
+ Math.max(1, Math.floor(limits.maxWorkgroupInvocations / x))
310
+ );
311
+ x = Math.min(
312
+ x,
313
+ Math.max(1, Math.floor(limits.maxWorkgroupInvocations / y))
314
+ );
315
+ return [x, y];
316
+ }
317
+ function buildSingleKernel(workgroupSize) {
318
+ return Fn(() => {
319
+ bindings?.forEach((b) => b.toVar());
320
+ const fWidth = float(width);
321
+ const activeIndex = globalId.z;
322
+ const nodeIndex = int(activeIndex).toVar();
323
+ const iWidth = int(width);
324
+ const ix = int(globalId.x);
325
+ const iy = int(globalId.y);
326
+ const texelSize = vec2(1, 1).div(fWidth);
327
+ const localCoordinates = vec2(globalId.x, globalId.y);
328
+ const localUVCoords = localCoordinates.div(fWidth);
329
+ const verticesPerNode = iWidth.mul(iWidth);
330
+ const globalIndex = int(nodeIndex).mul(verticesPerNode).add(iy.mul(iWidth).add(ix));
331
+ const inBounds = ix.lessThan(iWidth).and(iy.lessThan(iWidth)).and(uint(activeIndex).lessThan(uInstanceCount)).toVar();
332
+ for (let i = 0; i < stages.length; i++) {
333
+ if (i > 0) {
334
+ workgroupBarrier();
335
+ }
336
+ If(inBounds, () => {
337
+ stages[i](
338
+ nodeIndex,
339
+ globalIndex,
340
+ localUVCoords,
341
+ localCoordinates,
342
+ texelSize
343
+ );
344
+ });
290
345
  }
291
- If(inBounds, () => {
292
- stages[i](nodeIndex, globalIndex, localUVCoords, localCoordinates, texelSize);
293
- });
294
- }
295
- })().computeKernel(workgroupSize);
346
+ })().computeKernel(workgroupSize);
347
+ }
348
+ function buildStagedKernels(workgroupSize) {
349
+ return stages.map(
350
+ (stage) => Fn(() => {
351
+ bindings?.forEach((b) => b.toVar());
352
+ const fWidth = float(width);
353
+ const activeIndex = globalId.z;
354
+ const nodeIndex = int(activeIndex).toVar();
355
+ const iWidth = int(width);
356
+ const ix = int(globalId.x);
357
+ const iy = int(globalId.y);
358
+ const texelSize = vec2(1, 1).div(fWidth);
359
+ const localCoordinates = vec2(globalId.x, globalId.y);
360
+ const localUVCoords = localCoordinates.div(fWidth);
361
+ const verticesPerNode = iWidth.mul(iWidth);
362
+ const globalIndex = int(nodeIndex).mul(verticesPerNode).add(iy.mul(iWidth).add(ix));
363
+ const inBounds = ix.lessThan(iWidth).and(iy.lessThan(iWidth)).and(uint(activeIndex).lessThan(uInstanceCount)).toVar();
364
+ If(inBounds, () => {
365
+ stage(
366
+ nodeIndex,
367
+ globalIndex,
368
+ localUVCoords,
369
+ localCoordinates,
370
+ texelSize
371
+ );
372
+ });
373
+ })().computeKernel(workgroupSize)
374
+ );
375
+ }
296
376
  function execute(renderer, instanceCount) {
377
+ const limits = getDeviceComputeLimits(renderer);
378
+ const canUseSingleKernel = preferSingleKernelWhenPossible && canRunSingleKernel(width, limits);
297
379
  uInstanceCount.value = instanceCount;
298
- renderer.compute(computeShader, [dispatchX, dispatchY, instanceCount]);
380
+ if (canUseSingleKernel) {
381
+ if (!singleKernel) {
382
+ singleKernel = buildSingleKernel([width, width, 1]);
383
+ }
384
+ renderer.compute(singleKernel, [1, 1, instanceCount]);
385
+ return;
386
+ }
387
+ const [workgroupX, workgroupY] = clampWorkgroupToLimits(
388
+ preferredWorkgroup,
389
+ limits
390
+ );
391
+ const cacheKey = `${workgroupX}x${workgroupY}`;
392
+ let stagedKernels = stagedKernelCache.get(cacheKey);
393
+ if (!stagedKernels) {
394
+ stagedKernels = buildStagedKernels([workgroupX, workgroupY, 1]);
395
+ stagedKernelCache.set(cacheKey, stagedKernels);
396
+ }
397
+ const dispatchX = Math.ceil(width / workgroupX);
398
+ const dispatchY = Math.ceil(width / workgroupY);
399
+ for (const kernel of stagedKernels) {
400
+ renderer.compute(kernel, [dispatchX, dispatchY, instanceCount]);
401
+ }
299
402
  }
300
403
  return { execute };
301
404
  }
302
405
 
406
+ function resolveType(format) {
407
+ return format === "rgba16float" ? HalfFloatType : FloatType;
408
+ }
409
+ function resolveFilter(mode) {
410
+ return mode === "linear" ? LinearFilter : NearestFilter;
411
+ }
412
+ function configureStorageTexture(texture, format, filter) {
413
+ texture.format = RGBAFormat;
414
+ texture.type = resolveType(format);
415
+ texture.magFilter = resolveFilter(filter);
416
+ texture.minFilter = resolveFilter(filter);
417
+ texture.wrapS = ClampToEdgeWrapping;
418
+ texture.wrapT = ClampToEdgeWrapping;
419
+ texture.generateMipmaps = false;
420
+ texture.needsUpdate = true;
421
+ }
422
+ function ArrayTextureBackend(edgeVertexCount, tileCount, options) {
423
+ let currentEdgeVertexCount = edgeVertexCount;
424
+ let currentTileCount = tileCount;
425
+ const texture = new StorageArrayTexture(
426
+ edgeVertexCount,
427
+ edgeVertexCount,
428
+ tileCount
429
+ );
430
+ configureStorageTexture(texture, options.format, options.filter);
431
+ return {
432
+ backendType: "array-texture",
433
+ get edgeVertexCount() {
434
+ return currentEdgeVertexCount;
435
+ },
436
+ get tileCount() {
437
+ return currentTileCount;
438
+ },
439
+ texture,
440
+ uv(ix, iy, _tileIndex) {
441
+ return vec2(ix.toFloat(), iy.toFloat());
442
+ },
443
+ texel(ix, iy, tileIndex) {
444
+ return ivec3(ix, iy, tileIndex);
445
+ },
446
+ resize(width, height, nextTileCount) {
447
+ currentEdgeVertexCount = width;
448
+ currentTileCount = nextTileCount;
449
+ texture.setSize(width, height, nextTileCount);
450
+ texture.needsUpdate = true;
451
+ }
452
+ };
453
+ }
454
+ function atlasCoord(tilesPerRow, edgeVertexCount, ix, iy, tileIndex) {
455
+ const tilesPerRowNode = int(tilesPerRow);
456
+ const edge = int(edgeVertexCount);
457
+ const tile = int(tileIndex);
458
+ const col = tile.mod(tilesPerRowNode);
459
+ const row = tile.div(tilesPerRowNode);
460
+ const atlasX = col.mul(edge).add(int(ix));
461
+ const atlasY = row.mul(edge).add(int(iy));
462
+ return { atlasX, atlasY };
463
+ }
464
+ function AtlasBackend(edgeVertexCount, tileCount, options) {
465
+ let currentEdgeVertexCount = edgeVertexCount;
466
+ let currentTileCount = tileCount;
467
+ let tilesPerRow = Math.max(1, Math.ceil(Math.sqrt(tileCount)));
468
+ const atlasSize = tilesPerRow * edgeVertexCount;
469
+ const texture = new StorageTexture(atlasSize, atlasSize);
470
+ configureStorageTexture(texture, options.format, options.filter);
471
+ return {
472
+ backendType: "atlas",
473
+ get edgeVertexCount() {
474
+ return currentEdgeVertexCount;
475
+ },
476
+ get tileCount() {
477
+ return currentTileCount;
478
+ },
479
+ texture,
480
+ uv(ix, iy, tileIndex) {
481
+ const { atlasX, atlasY } = atlasCoord(
482
+ tilesPerRow,
483
+ currentEdgeVertexCount,
484
+ ix,
485
+ iy,
486
+ tileIndex
487
+ );
488
+ const currentAtlasSize = float(tilesPerRow * currentEdgeVertexCount);
489
+ return vec2(
490
+ atlasX.toFloat().add(0.5).div(currentAtlasSize),
491
+ atlasY.toFloat().add(0.5).div(currentAtlasSize)
492
+ );
493
+ },
494
+ texel(ix, iy, tileIndex) {
495
+ const { atlasX, atlasY } = atlasCoord(
496
+ tilesPerRow,
497
+ currentEdgeVertexCount,
498
+ ix,
499
+ iy,
500
+ tileIndex
501
+ );
502
+ return ivec2(atlasX, atlasY);
503
+ },
504
+ resize(width, height, nextTileCount) {
505
+ currentEdgeVertexCount = width;
506
+ currentTileCount = nextTileCount;
507
+ tilesPerRow = Math.max(1, Math.ceil(Math.sqrt(nextTileCount)));
508
+ const nextAtlasSize = tilesPerRow * width;
509
+ const image = texture.image;
510
+ image.width = nextAtlasSize;
511
+ image.height = nextAtlasSize;
512
+ texture.needsUpdate = true;
513
+ }
514
+ };
515
+ }
516
+ function Texture3DBackend(edgeVertexCount, tileCount, options) {
517
+ let currentEdgeVertexCount = edgeVertexCount;
518
+ let currentTileCount = tileCount;
519
+ const texture = new StorageArrayTexture(
520
+ edgeVertexCount,
521
+ edgeVertexCount,
522
+ tileCount
523
+ );
524
+ configureStorageTexture(texture, options.format, options.filter);
525
+ return {
526
+ backendType: "texture-3d",
527
+ get edgeVertexCount() {
528
+ return currentEdgeVertexCount;
529
+ },
530
+ get tileCount() {
531
+ return currentTileCount;
532
+ },
533
+ texture,
534
+ uv(ix, iy, _tileIndex) {
535
+ return vec2(ix.toFloat(), iy.toFloat());
536
+ },
537
+ texel(ix, iy, tileIndex) {
538
+ return ivec3(ix, iy, tileIndex);
539
+ },
540
+ resize(width, height, nextTileCount) {
541
+ currentEdgeVertexCount = width;
542
+ currentTileCount = nextTileCount;
543
+ texture.setSize(width, height, nextTileCount);
544
+ texture.needsUpdate = true;
545
+ }
546
+ };
547
+ }
548
+ function tryGetDeviceLimits(renderer) {
549
+ const backend = renderer;
550
+ return backend.backend?.device?.limits ?? {};
551
+ }
552
+ function createTerrainFieldStorage(edgeVertexCount, tileCount, renderer, options = {}) {
553
+ const filter = options.filter ?? "nearest";
554
+ const format = options.format ?? "rgba16float";
555
+ const forcedBackend = options.backend;
556
+ if (forcedBackend === "atlas") {
557
+ return AtlasBackend(edgeVertexCount, tileCount, { filter, format });
558
+ }
559
+ if (forcedBackend === "texture-3d") {
560
+ return Texture3DBackend(edgeVertexCount, tileCount, { filter, format });
561
+ }
562
+ if (forcedBackend === "array-texture") {
563
+ return ArrayTextureBackend(edgeVertexCount, tileCount, { filter, format });
564
+ }
565
+ const DEFAULT_MAX_TEXTURE_ARRAY_LAYERS = 256;
566
+ const maxLayers = renderer ? tryGetDeviceLimits(renderer).maxTextureArrayLayers ?? DEFAULT_MAX_TEXTURE_ARRAY_LAYERS : DEFAULT_MAX_TEXTURE_ARRAY_LAYERS;
567
+ if (tileCount > maxLayers) {
568
+ return AtlasBackend(edgeVertexCount, tileCount, { filter, format });
569
+ }
570
+ return ArrayTextureBackend(edgeVertexCount, tileCount, { filter, format });
571
+ }
572
+ function storeTerrainField(storage, ix, iy, tileIndex, value) {
573
+ if (storage.backendType === "array-texture" || storage.backendType === "texture-3d") {
574
+ return textureStore(
575
+ storage.texture,
576
+ uvec3(int(ix), int(iy), int(tileIndex)),
577
+ value
578
+ );
579
+ }
580
+ return textureStore(storage.texture, storage.texel(ix, iy, tileIndex), value);
581
+ }
582
+ function loadTerrainField(storage, ix, iy, tileIndex) {
583
+ if (storage.backendType === "array-texture" || storage.backendType === "texture-3d") {
584
+ return textureLoad(storage.texture, ivec2(int(ix), int(iy)), int(0)).depth(
585
+ int(tileIndex)
586
+ );
587
+ }
588
+ return textureLoad(storage.texture, storage.texel(ix, iy, tileIndex), int(0));
589
+ }
590
+ function loadTerrainFieldElevation(storage, ix, iy, tileIndex) {
591
+ return loadTerrainField(storage, ix, iy, tileIndex).r;
592
+ }
593
+ function loadTerrainFieldNormal(storage, ix, iy, tileIndex) {
594
+ const sample = loadTerrainField(storage, ix, iy, tileIndex);
595
+ return vec2(sample.g, sample.b);
596
+ }
597
+ function packTerrainFieldSample(height, normalXZ, extra = float(0)) {
598
+ return vec4(height, normalXZ.x, normalXZ.y, extra);
599
+ }
600
+
303
601
  const createElevation = (tile, uniforms, elevationFn) => {
304
602
  return function perVertexElevation(nodeIndex, localCoordinates) {
305
603
  const ix = int(localCoordinates.x);
@@ -321,7 +619,7 @@ const createElevation = (tile, uniforms, elevationFn) => {
321
619
  });
322
620
  };
323
621
  };
324
- const readElevationFieldAtPositionLocal = (elevationFieldBuffer, edgeVertexCount, positionLocal) => Fn(() => {
622
+ const readElevationFieldAtPositionLocal = (terrainFieldStorage, edgeVertexCount, positionLocal) => Fn(() => {
325
623
  const nodeIndex = int(instanceIndex);
326
624
  const intEdge = int(edgeVertexCount);
327
625
  const innerSegments = int(edgeVertexCount).sub(3);
@@ -333,10 +631,12 @@ const readElevationFieldAtPositionLocal = (elevationFieldBuffer, edgeVertexCount
333
631
  const y = v.mul(fInnerSegments).round().toInt().add(int(1));
334
632
  const xClamped = min(max(x, int(0)), last);
335
633
  const yClamped = min(max(y, int(0)), last);
336
- const verticesPerNode = intEdge.mul(intEdge);
337
- const perNodeVertexIndex = yClamped.mul(intEdge).add(xClamped);
338
- const globalVertexIndex = nodeIndex.mul(verticesPerNode).add(perNodeVertexIndex);
339
- return elevationFieldBuffer.element(globalVertexIndex);
634
+ return loadTerrainFieldElevation(
635
+ terrainFieldStorage,
636
+ xClamped,
637
+ yClamped,
638
+ nodeIndex
639
+ );
340
640
  });
341
641
 
342
642
  function createTileCompute(leafStorage, uniforms) {
@@ -1229,21 +1529,19 @@ const elevationFieldStageTask = task((get, work) => {
1229
1529
  });
1230
1530
  }).displayName("elevationFieldStageTask");
1231
1531
 
1232
- const createNormalFieldContextTask = task((get, work) => {
1233
- const edgeVertexCount = get(innerTileSegments) + 3;
1234
- const verticesPerNode = edgeVertexCount * edgeVertexCount;
1235
- const totalElements = get(maxNodes) * verticesPerNode;
1236
- return work(() => {
1237
- const data = new Uint32Array(totalElements);
1238
- const attribute = new StorageBufferAttribute(data, 1);
1239
- const node = storage(attribute, "uint", totalElements);
1240
- return {
1241
- data,
1242
- attribute,
1243
- node
1244
- };
1245
- });
1246
- }).displayName("createNormalFieldContextTask");
1532
+ const createTerrainFieldTextureTask = task(
1533
+ (get, work, { resources }) => {
1534
+ const edgeVertexCount = get(innerTileSegments) + 3;
1535
+ const maxNodesValue = get(maxNodes);
1536
+ return work(
1537
+ () => createTerrainFieldStorage(
1538
+ edgeVertexCount,
1539
+ maxNodesValue,
1540
+ resources?.renderer
1541
+ )
1542
+ );
1543
+ }
1544
+ ).displayName("createTerrainFieldTextureTask");
1247
1545
  function createNormalFromElevationField(elevationFieldNode, edgeVertexCount) {
1248
1546
  return Fn(
1249
1547
  ([nodeIndex, tileSize, ix, iy, elevationScale]) => {
@@ -1268,10 +1566,10 @@ function createNormalFromElevationField(elevationFieldNode, edgeVertexCount) {
1268
1566
  }
1269
1567
  );
1270
1568
  }
1271
- const normalFieldStageTask = task((get, work) => {
1569
+ const terrainFieldStageTask = task((get, work) => {
1272
1570
  const upstream = get(elevationFieldStageTask);
1273
1571
  const elevationFieldContext = get(createElevationFieldContextTask);
1274
- const normalFieldContext = get(createNormalFieldContextTask);
1572
+ const terrainFieldStorage = get(createTerrainFieldTextureTask);
1275
1573
  const tileEdgeVertexCount = get(innerTileSegments) + 3;
1276
1574
  const tile = get(tileNodesTask);
1277
1575
  const uniforms = get(createUniformsTask);
@@ -1286,6 +1584,7 @@ const normalFieldStageTask = task((get, work) => {
1286
1584
  const ix = int(localCoordinates.x);
1287
1585
  const iy = int(localCoordinates.y);
1288
1586
  const tileSize = tile.tileSize(nodeIndex);
1587
+ const height = elevationFieldContext.node.element(globalVertexIndex);
1289
1588
  const normalXZ = computeNormal(
1290
1589
  nodeIndex,
1291
1590
  tileSize,
@@ -1293,58 +1592,60 @@ const normalFieldStageTask = task((get, work) => {
1293
1592
  iy,
1294
1593
  uniforms.uElevationScale
1295
1594
  );
1296
- normalFieldContext.node.element(globalVertexIndex).assign(packHalf2x16(normalXZ));
1595
+ storeTerrainField(
1596
+ terrainFieldStorage,
1597
+ ix,
1598
+ iy,
1599
+ nodeIndex,
1600
+ packTerrainFieldSample(height, normalXZ)
1601
+ );
1297
1602
  }
1298
1603
  ];
1299
1604
  });
1300
- }).displayName("normalFieldStageTask");
1605
+ }).displayName("terrainFieldStageTask");
1301
1606
 
1302
1607
  const compileComputeTask = task((get, work) => {
1303
- const pipeline = get(normalFieldStageTask);
1608
+ const pipeline = get(terrainFieldStageTask);
1304
1609
  const edgeVertexCount = get(innerTileSegments) + 3;
1305
- return work(() => compileComputePipeline(pipeline, edgeVertexCount));
1306
- }).displayName("compileComputeTask");
1307
- const executeComputeTask = task((get, work, { resources }) => {
1308
- const { execute } = get(compileComputeTask);
1309
- const leafState = get(leafGpuBufferTask);
1310
1610
  return work(
1311
- () => resources?.renderer ? execute(resources.renderer, leafState.count) : () => {
1312
- }
1611
+ () => compileComputePipeline(pipeline, edgeVertexCount, {
1612
+ preferSingleKernelWhenPossible: false
1613
+ })
1313
1614
  );
1314
- }).displayName("executeComputeTask").lane("gpu");
1615
+ }).displayName("compileComputeTask");
1616
+ const executeComputeTask = task(
1617
+ (get, work, { resources }) => {
1618
+ const { execute } = get(compileComputeTask);
1619
+ const leafState = get(leafGpuBufferTask);
1620
+ return work(
1621
+ () => resources?.renderer ? execute(resources.renderer, leafState.count) : () => {
1622
+ }
1623
+ );
1624
+ }
1625
+ ).displayName("executeComputeTask").lane("gpu");
1315
1626
  function createComputePipelineTasks(leafStageTask) {
1316
1627
  const compile = task((get, work) => {
1317
1628
  const pipeline = get(leafStageTask);
1318
1629
  const edgeVertexCount = get(innerTileSegments) + 3;
1319
- return work(() => compileComputePipeline(pipeline, edgeVertexCount));
1630
+ return work(
1631
+ () => compileComputePipeline(pipeline, edgeVertexCount, {
1632
+ preferSingleKernelWhenPossible: false
1633
+ })
1634
+ );
1320
1635
  }).displayName("compileComputeTask");
1321
- const execute = task((get, work, { resources }) => {
1322
- const { execute: run } = get(compile);
1323
- const leafState = get(leafGpuBufferTask);
1324
- return work(() => resources?.renderer ? run(resources.renderer, leafState.count) : () => {
1325
- });
1326
- }).displayName("executeComputeTask").lane("gpu");
1636
+ const execute = task(
1637
+ (get, work, { resources }) => {
1638
+ const { execute: run } = get(compile);
1639
+ const leafState = get(leafGpuBufferTask);
1640
+ return work(
1641
+ () => resources?.renderer ? run(resources.renderer, leafState.count) : () => {
1642
+ }
1643
+ );
1644
+ }
1645
+ ).displayName("executeComputeTask").lane("gpu");
1327
1646
  return { compile, execute };
1328
1647
  }
1329
1648
 
1330
- const textureSpaceToVectorSpace = Fn(([value]) => {
1331
- return remap(value, float(0), float(1), float(-1), float(1));
1332
- });
1333
- const vectorSpaceToTextureSpace = Fn(([value]) => {
1334
- return remap(value, float(-1), float(1), float(0), float(1));
1335
- });
1336
- const blendAngleCorrectedNormals = Fn(([n1, n2]) => {
1337
- const t = vec3(n1.x, n1.y, n1.z.add(1));
1338
- const u = vec3(n2.x.negate(), n2.y.negate(), n2.z);
1339
- const r = t.mul(dot(t, u)).sub(u.mul(t.z)).normalize();
1340
- return r;
1341
- });
1342
- const deriveNormalZ = Fn(([normalXY]) => {
1343
- const xy = normalXY.toVar();
1344
- const z = xy.x.mul(xy.x).add(xy.y.mul(xy.y)).oneMinus().max(0).sqrt();
1345
- return vec3(xy.x, xy.y, z);
1346
- });
1347
-
1348
1649
  const isSkirtVertex = Fn(([segments]) => {
1349
1650
  const segmentsNode = typeof segments === "number" ? int(segments) : segments;
1350
1651
  const vIndex = int(vertexIndex);
@@ -1386,37 +1687,40 @@ function createTileBaseWorldPosition(leafStorage, terrainUniforms) {
1386
1687
  return vec3(worldX, rootOrigin.y, worldZ);
1387
1688
  });
1388
1689
  }
1389
- function createTileElevation(terrainUniforms, elevationFieldBufferNode) {
1390
- if (!elevationFieldBufferNode) return float(0);
1690
+ function createTileElevation(terrainUniforms, terrainFieldStorage) {
1691
+ if (!terrainFieldStorage) return float(0);
1391
1692
  const edgeVertexCount = terrainUniforms.uInnerTileSegments.add(3);
1392
1693
  return readElevationFieldAtPositionLocal(
1393
- elevationFieldBufferNode,
1694
+ terrainFieldStorage,
1394
1695
  edgeVertexCount,
1395
1696
  positionLocal
1396
1697
  )().mul(
1397
1698
  terrainUniforms.uElevationScale
1398
1699
  );
1399
1700
  }
1400
- function createNormalAssignment(terrainUniforms, normalFieldBufferNode) {
1401
- if (!normalFieldBufferNode) return;
1701
+ function createNormalAssignment(terrainUniforms, terrainFieldStorage) {
1702
+ if (!terrainFieldStorage) return;
1402
1703
  const nodeIndex = int(instanceIndex);
1403
- const intEdge = int(terrainUniforms.uInnerTileSegments.add(3));
1404
- const verticesPerNode = intEdge.mul(intEdge);
1405
- const globalVertexIndex = nodeIndex.mul(verticesPerNode).add(int(vertexIndex));
1406
- const packed = normalFieldBufferNode.element(globalVertexIndex);
1407
- const normalXZ = unpackHalf2x16(packed);
1408
- const reconstructed = deriveNormalZ(normalXZ);
1409
- normalLocal.assign(vec3(reconstructed.x, reconstructed.z, reconstructed.y));
1704
+ const edgeVertexCount = int(terrainUniforms.uInnerTileSegments.add(3));
1705
+ const localVertexIndex = int(vertexIndex);
1706
+ const ix = localVertexIndex.mod(edgeVertexCount);
1707
+ const iy = localVertexIndex.div(edgeVertexCount);
1708
+ const normalXZ = loadTerrainFieldNormal(terrainFieldStorage, ix, iy, nodeIndex);
1709
+ const nx = normalXZ.x;
1710
+ const nz = normalXZ.y;
1711
+ const nySq = float(1).sub(nx.mul(nx)).sub(nz.mul(nz)).max(float(0));
1712
+ const ny = nySq.sqrt();
1713
+ normalLocal.assign(vec3(nx, ny, nz));
1410
1714
  }
1411
- function createTileWorldPosition(leafStorage, terrainUniforms, elevationFieldBufferNode, normalFieldBufferNode) {
1715
+ function createTileWorldPosition(leafStorage, terrainUniforms, terrainFieldStorage) {
1412
1716
  const baseWorldPosition = createTileBaseWorldPosition(leafStorage, terrainUniforms);
1413
1717
  return Fn(() => {
1414
1718
  const base = baseWorldPosition();
1415
- const yElevation = createTileElevation(terrainUniforms, elevationFieldBufferNode);
1719
+ const yElevation = createTileElevation(terrainUniforms, terrainFieldStorage);
1416
1720
  const skirtVertex = isSkirtVertex(terrainUniforms.uInnerTileSegments);
1417
1721
  const skirtY = base.y.add(yElevation).sub(terrainUniforms.uSkirtScale.toVar());
1418
1722
  const worldY = select(skirtVertex, skirtY, base.y.add(yElevation));
1419
- createNormalAssignment(terrainUniforms, normalFieldBufferNode);
1723
+ createNormalAssignment(terrainUniforms, terrainFieldStorage);
1420
1724
  return vec3(base.x, worldY, base.z);
1421
1725
  })();
1422
1726
  }
@@ -1424,20 +1728,18 @@ function createTileWorldPosition(leafStorage, terrainUniforms, elevationFieldBuf
1424
1728
  const positionNodeTask = task((get, work) => {
1425
1729
  const leafStorage = get(leafStorageTask);
1426
1730
  const terrainUniforms = get(createUniformsTask);
1427
- const elevationFieldContext = get(createElevationFieldContextTask);
1428
- const normalFieldContext = get(createNormalFieldContextTask);
1731
+ const terrainFieldStorage = get(createTerrainFieldTextureTask);
1429
1732
  return work(
1430
1733
  () => createTileWorldPosition(
1431
1734
  leafStorage,
1432
1735
  terrainUniforms,
1433
- elevationFieldContext.node,
1434
- normalFieldContext.node
1736
+ terrainFieldStorage
1435
1737
  )
1436
1738
  );
1437
1739
  }).displayName("positionNodeTask");
1438
1740
 
1439
1741
  function terrainGraph() {
1440
- return 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);
1742
+ return graph().add(instanceIdTask).add(quadtreeConfigTask).add(quadtreeUpdateTask).add(leafStorageTask).add(surfaceTask).add(leafGpuBufferTask).add(createUniformsTask).add(updateUniformsTask).add(positionNodeTask).add(createElevationFieldContextTask).add(tileNodesTask).add(createTerrainFieldTextureTask).add(elevationFieldStageTask).add(terrainFieldStageTask).add(compileComputeTask).add(executeComputeTask);
1441
1743
  }
1442
1744
  const terrainTasks = {
1443
1745
  instanceId: instanceIdTask,
@@ -1451,13 +1753,31 @@ const terrainTasks = {
1451
1753
  positionNode: positionNodeTask,
1452
1754
  createElevationFieldContext: createElevationFieldContextTask,
1453
1755
  createTileNodes: tileNodesTask,
1454
- createNormalFieldContext: createNormalFieldContextTask,
1756
+ createTerrainFieldTexture: createTerrainFieldTextureTask,
1455
1757
  elevationFieldStage: elevationFieldStageTask,
1456
- normalFieldStage: normalFieldStageTask,
1758
+ terrainFieldStage: terrainFieldStageTask,
1457
1759
  compileCompute: compileComputeTask,
1458
1760
  executeCompute: executeComputeTask
1459
1761
  };
1460
1762
 
1763
+ const textureSpaceToVectorSpace = Fn(([value]) => {
1764
+ return remap(value, float(0), float(1), float(-1), float(1));
1765
+ });
1766
+ const vectorSpaceToTextureSpace = Fn(([value]) => {
1767
+ return remap(value, float(-1), float(1), float(0), float(1));
1768
+ });
1769
+ const blendAngleCorrectedNormals = Fn(([n1, n2]) => {
1770
+ const t = vec3(n1.x, n1.y, n1.z.add(1));
1771
+ const u = vec3(n2.x.negate(), n2.y.negate(), n2.z);
1772
+ const r = t.mul(dot(t, u)).sub(u.mul(t.z)).normalize();
1773
+ return r;
1774
+ });
1775
+ const deriveNormalZ = Fn(([normalXY]) => {
1776
+ const xy = normalXY.toVar();
1777
+ const z = xy.x.mul(xy.x).add(xy.y.mul(xy.y)).oneMinus().max(0).sqrt();
1778
+ return vec3(xy.x, xy.y, z);
1779
+ });
1780
+
1461
1781
  const vGlobalVertexIndex = /* @__PURE__ */ varyingProperty("int", "vGlobalVertexIndex");
1462
1782
  const vElevation = /* @__PURE__ */ varyingProperty("f32", "vElevation");
1463
1783
 
@@ -1492,4 +1812,4 @@ const voronoiCells = Fn((params) => {
1492
1812
  return k;
1493
1813
  });
1494
1814
 
1495
- export { Dir, TerrainGeometry, TerrainMesh, U32_EMPTY, allocLeafSet, allocSeamTable, beginUpdate, blendAngleCorrectedNormals, buildLeafIndex, buildSeams2to1, compileComputeTask, createComputePipelineTasks, createCubeSphereSurface, createElevationFieldContextTask, createFlatSurface, createInfiniteFlatSurface, createNormalFieldContextTask, createSpatialIndex, createState, createTerrainUniforms, createUniformsTask, deriveNormalZ, elevationFieldStageTask, elevationFn, elevationScale, executeComputeTask, innerTileSegments, instanceIdTask, isSkirtUV, isSkirtVertex, leafGpuBufferTask, leafStorageTask, maxLevel, maxNodes, normalFieldStageTask, origin, positionNodeTask, quadtreeConfigTask, quadtreeUpdate, quadtreeUpdateTask, resetLeafSet, resetSeamTable, rootSize, skirtScale, surface, surfaceTask, terrainGraph, terrainTasks, textureSpaceToVectorSpace, tileNodesTask, update, updateUniformsTask, vElevation, vGlobalVertexIndex, vectorSpaceToTextureSpace, voronoiCells };
1815
+ export { ArrayTextureBackend, AtlasBackend, Dir, TerrainGeometry, TerrainMesh, Texture3DBackend, U32_EMPTY, allocLeafSet, allocSeamTable, beginUpdate, blendAngleCorrectedNormals, buildLeafIndex, buildSeams2to1, compileComputeTask, createComputePipelineTasks, createCubeSphereSurface, createElevationFieldContextTask, createFlatSurface, createInfiniteFlatSurface, createSpatialIndex, createState, createTerrainFieldStorage, createTerrainFieldTextureTask, createTerrainUniforms, createUniformsTask, deriveNormalZ, elevationFieldStageTask, elevationFn, elevationScale, executeComputeTask, getDeviceComputeLimits, innerTileSegments, instanceIdTask, isSkirtUV, isSkirtVertex, leafGpuBufferTask, leafStorageTask, loadTerrainField, loadTerrainFieldElevation, loadTerrainFieldNormal, maxLevel, maxNodes, origin, packTerrainFieldSample, positionNodeTask, quadtreeConfigTask, quadtreeUpdate, quadtreeUpdateTask, resetLeafSet, resetSeamTable, rootSize, skirtScale, storeTerrainField, surface, surfaceTask, terrainFieldStageTask, terrainGraph, terrainTasks, textureSpaceToVectorSpace, tileNodesTask, update, updateUniformsTask, vElevation, vGlobalVertexIndex, vectorSpaceToTextureSpace, voronoiCells };