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