@hello-terrain/three 0.0.0-alpha.6 → 0.0.0-alpha.8

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,7 +1,7 @@
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, texture, 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 {
@@ -274,42 +274,356 @@ class TerrainMesh extends InstancedMesh {
274
274
  }
275
275
  }
276
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
+
277
287
  const WORKGROUP_X = 16;
278
288
  const WORKGROUP_Y = 16;
279
- function compileComputePipeline(stages, width, bindings) {
280
- const workgroupSize = [WORKGROUP_X, WORKGROUP_Y, 1];
281
- const dispatchX = Math.ceil(width / WORKGROUP_X);
282
- 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;
283
296
  const uInstanceCount = uniform(0, "uint");
284
- const computeShader = Fn(() => {
285
- const fWidth = float(width);
286
- const activeIndex = globalId.z;
287
- const nodeIndex = int(activeIndex).toVar();
288
- const iWidth = int(width);
289
- const ix = int(globalId.x);
290
- const iy = int(globalId.y);
291
- const texelSize = vec2(1, 1).div(fWidth);
292
- const localCoordinates = vec2(globalId.x, globalId.y);
293
- const localUVCoords = localCoordinates.div(fWidth);
294
- const verticesPerNode = iWidth.mul(iWidth);
295
- const globalIndex = int(nodeIndex).mul(verticesPerNode).add(iy.mul(iWidth).add(ix));
296
- const inBounds = ix.lessThan(iWidth).and(iy.lessThan(iWidth)).and(uint(activeIndex).lessThan(uInstanceCount)).toVar();
297
- for (let i = 0; i < stages.length; i++) {
298
- if (i > 0) {
299
- 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
+ });
300
345
  }
301
- If(inBounds, () => {
302
- stages[i](nodeIndex, globalIndex, localUVCoords, localCoordinates, texelSize);
303
- });
304
- }
305
- })().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
+ }
306
376
  function execute(renderer, instanceCount) {
377
+ const limits = getDeviceComputeLimits(renderer);
378
+ const canUseSingleKernel = preferSingleKernelWhenPossible && canRunSingleKernel(width, limits);
307
379
  uInstanceCount.value = instanceCount;
308
- 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
+ }
309
402
  }
310
403
  return { execute };
311
404
  }
312
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(texture2, format, filter) {
413
+ texture2.format = RGBAFormat;
414
+ texture2.type = resolveType(format);
415
+ texture2.magFilter = resolveFilter(filter);
416
+ texture2.minFilter = resolveFilter(filter);
417
+ texture2.wrapS = ClampToEdgeWrapping;
418
+ texture2.wrapT = ClampToEdgeWrapping;
419
+ texture2.generateMipmaps = false;
420
+ texture2.needsUpdate = true;
421
+ }
422
+ function ArrayTextureBackend(edgeVertexCount, tileCount, options) {
423
+ let currentEdgeVertexCount = edgeVertexCount;
424
+ let currentTileCount = tileCount;
425
+ const tex = new StorageArrayTexture(
426
+ edgeVertexCount,
427
+ edgeVertexCount,
428
+ tileCount
429
+ );
430
+ configureStorageTexture(tex, 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: tex,
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
+ sample(u, v, tileIndex) {
447
+ return texture(tex, vec2(u, v)).depth(int(tileIndex));
448
+ },
449
+ resize(width, height, nextTileCount) {
450
+ currentEdgeVertexCount = width;
451
+ currentTileCount = nextTileCount;
452
+ tex.setSize(width, height, nextTileCount);
453
+ tex.needsUpdate = true;
454
+ }
455
+ };
456
+ }
457
+ function atlasCoord(tilesPerRow, edgeVertexCount, ix, iy, tileIndex) {
458
+ const tilesPerRowNode = int(tilesPerRow);
459
+ const edge = int(edgeVertexCount);
460
+ const tile = int(tileIndex);
461
+ const col = tile.mod(tilesPerRowNode);
462
+ const row = tile.div(tilesPerRowNode);
463
+ const atlasX = col.mul(edge).add(int(ix));
464
+ const atlasY = row.mul(edge).add(int(iy));
465
+ return { atlasX, atlasY };
466
+ }
467
+ function AtlasBackend(edgeVertexCount, tileCount, options) {
468
+ let currentEdgeVertexCount = edgeVertexCount;
469
+ let currentTileCount = tileCount;
470
+ let tilesPerRow = Math.max(1, Math.ceil(Math.sqrt(tileCount)));
471
+ const atlasSize = tilesPerRow * edgeVertexCount;
472
+ const tex = new StorageTexture(atlasSize, atlasSize);
473
+ configureStorageTexture(tex, options.format, options.filter);
474
+ return {
475
+ backendType: "atlas",
476
+ get edgeVertexCount() {
477
+ return currentEdgeVertexCount;
478
+ },
479
+ get tileCount() {
480
+ return currentTileCount;
481
+ },
482
+ texture: tex,
483
+ uv(ix, iy, tileIndex) {
484
+ const { atlasX, atlasY } = atlasCoord(
485
+ tilesPerRow,
486
+ currentEdgeVertexCount,
487
+ ix,
488
+ iy,
489
+ tileIndex
490
+ );
491
+ const currentAtlasSize = float(tilesPerRow * currentEdgeVertexCount);
492
+ return vec2(
493
+ atlasX.toFloat().add(0.5).div(currentAtlasSize),
494
+ atlasY.toFloat().add(0.5).div(currentAtlasSize)
495
+ );
496
+ },
497
+ texel(ix, iy, tileIndex) {
498
+ const { atlasX, atlasY } = atlasCoord(
499
+ tilesPerRow,
500
+ currentEdgeVertexCount,
501
+ ix,
502
+ iy,
503
+ tileIndex
504
+ );
505
+ return ivec2(atlasX, atlasY);
506
+ },
507
+ sample(u, v, tileIndex) {
508
+ const tile = int(tileIndex);
509
+ const tilesPerRowNode = int(tilesPerRow);
510
+ const col = tile.mod(tilesPerRowNode);
511
+ const row = tile.div(tilesPerRowNode);
512
+ const invTilesPerRow = float(1 / tilesPerRow);
513
+ const atlasU = col.toFloat().add(u).mul(invTilesPerRow);
514
+ const atlasV = row.toFloat().add(v).mul(invTilesPerRow);
515
+ return texture(tex, vec2(atlasU, atlasV));
516
+ },
517
+ resize(width, height, nextTileCount) {
518
+ currentEdgeVertexCount = width;
519
+ currentTileCount = nextTileCount;
520
+ tilesPerRow = Math.max(1, Math.ceil(Math.sqrt(nextTileCount)));
521
+ const nextAtlasSize = tilesPerRow * width;
522
+ const image = tex.image;
523
+ image.width = nextAtlasSize;
524
+ image.height = nextAtlasSize;
525
+ tex.needsUpdate = true;
526
+ }
527
+ };
528
+ }
529
+ function Texture3DBackend(edgeVertexCount, tileCount, options) {
530
+ let currentEdgeVertexCount = edgeVertexCount;
531
+ let currentTileCount = tileCount;
532
+ const tex = new StorageArrayTexture(
533
+ edgeVertexCount,
534
+ edgeVertexCount,
535
+ tileCount
536
+ );
537
+ configureStorageTexture(tex, options.format, options.filter);
538
+ return {
539
+ backendType: "texture-3d",
540
+ get edgeVertexCount() {
541
+ return currentEdgeVertexCount;
542
+ },
543
+ get tileCount() {
544
+ return currentTileCount;
545
+ },
546
+ texture: tex,
547
+ uv(ix, iy, _tileIndex) {
548
+ return vec2(ix.toFloat(), iy.toFloat());
549
+ },
550
+ texel(ix, iy, tileIndex) {
551
+ return ivec3(ix, iy, tileIndex);
552
+ },
553
+ sample(u, v, tileIndex) {
554
+ return texture(tex, vec2(u, v)).depth(int(tileIndex));
555
+ },
556
+ resize(width, height, nextTileCount) {
557
+ currentEdgeVertexCount = width;
558
+ currentTileCount = nextTileCount;
559
+ tex.setSize(width, height, nextTileCount);
560
+ tex.needsUpdate = true;
561
+ }
562
+ };
563
+ }
564
+ function tryGetDeviceLimits(renderer) {
565
+ const backend = renderer;
566
+ return backend.backend?.device?.limits ?? {};
567
+ }
568
+ function createTerrainFieldStorage(edgeVertexCount, tileCount, renderer, options = {}) {
569
+ const filter = options.filter ?? "linear";
570
+ const format = options.format ?? "rgba16float";
571
+ const forcedBackend = options.backend;
572
+ if (forcedBackend === "atlas") {
573
+ return AtlasBackend(edgeVertexCount, tileCount, { filter, format });
574
+ }
575
+ if (forcedBackend === "texture-3d") {
576
+ return Texture3DBackend(edgeVertexCount, tileCount, { filter, format });
577
+ }
578
+ if (forcedBackend === "array-texture") {
579
+ return ArrayTextureBackend(edgeVertexCount, tileCount, { filter, format });
580
+ }
581
+ const DEFAULT_MAX_TEXTURE_ARRAY_LAYERS = 256;
582
+ const maxLayers = renderer ? tryGetDeviceLimits(renderer).maxTextureArrayLayers ?? DEFAULT_MAX_TEXTURE_ARRAY_LAYERS : DEFAULT_MAX_TEXTURE_ARRAY_LAYERS;
583
+ if (tileCount > maxLayers) {
584
+ return AtlasBackend(edgeVertexCount, tileCount, { filter, format });
585
+ }
586
+ return ArrayTextureBackend(edgeVertexCount, tileCount, { filter, format });
587
+ }
588
+ function storeTerrainField(storage, ix, iy, tileIndex, value) {
589
+ if (storage.backendType === "array-texture" || storage.backendType === "texture-3d") {
590
+ return textureStore(
591
+ storage.texture,
592
+ uvec3(int(ix), int(iy), int(tileIndex)),
593
+ value
594
+ );
595
+ }
596
+ return textureStore(storage.texture, storage.texel(ix, iy, tileIndex), value);
597
+ }
598
+ function loadTerrainField(storage, ix, iy, tileIndex) {
599
+ if (storage.backendType === "array-texture" || storage.backendType === "texture-3d") {
600
+ return textureLoad(storage.texture, ivec2(int(ix), int(iy)), int(0)).depth(
601
+ int(tileIndex)
602
+ );
603
+ }
604
+ return textureLoad(storage.texture, storage.texel(ix, iy, tileIndex), int(0));
605
+ }
606
+ function loadTerrainFieldElevation(storage, ix, iy, tileIndex) {
607
+ return loadTerrainField(storage, ix, iy, tileIndex).r;
608
+ }
609
+ function loadTerrainFieldNormal(storage, ix, iy, tileIndex) {
610
+ const raw = loadTerrainField(storage, ix, iy, tileIndex);
611
+ return vec2(raw.g, raw.b);
612
+ }
613
+ function sampleTerrainField(storage, u, v, tileIndex) {
614
+ return storage.sample(u, v, tileIndex);
615
+ }
616
+ function sampleTerrainFieldElevation(storage, u, v, tileIndex) {
617
+ return sampleTerrainField(storage, u, v, tileIndex).r;
618
+ }
619
+ function sampleTerrainFieldNormal(storage, u, v, tileIndex) {
620
+ const raw = sampleTerrainField(storage, u, v, tileIndex);
621
+ return vec2(raw.g, raw.b);
622
+ }
623
+ function packTerrainFieldSample(height, normalXZ, extra = float(0)) {
624
+ return vec4(height, normalXZ.x, normalXZ.y, extra);
625
+ }
626
+
313
627
  const createElevation = (tile, uniforms, elevationFn) => {
314
628
  return function perVertexElevation(nodeIndex, localCoordinates) {
315
629
  const ix = int(localCoordinates.x);
@@ -331,7 +645,7 @@ const createElevation = (tile, uniforms, elevationFn) => {
331
645
  });
332
646
  };
333
647
  };
334
- const readElevationFieldAtPositionLocal = (elevationFieldBuffer, edgeVertexCount, positionLocal) => Fn(() => {
648
+ const readElevationFieldAtPositionLocal = (terrainFieldStorage, edgeVertexCount, positionLocal) => Fn(() => {
335
649
  const nodeIndex = int(instanceIndex);
336
650
  const intEdge = int(edgeVertexCount);
337
651
  const innerSegments = int(edgeVertexCount).sub(3);
@@ -343,10 +657,12 @@ const readElevationFieldAtPositionLocal = (elevationFieldBuffer, edgeVertexCount
343
657
  const y = v.mul(fInnerSegments).round().toInt().add(int(1));
344
658
  const xClamped = min(max(x, int(0)), last);
345
659
  const yClamped = min(max(y, int(0)), last);
346
- const verticesPerNode = intEdge.mul(intEdge);
347
- const perNodeVertexIndex = yClamped.mul(intEdge).add(xClamped);
348
- const globalVertexIndex = nodeIndex.mul(verticesPerNode).add(perNodeVertexIndex);
349
- return elevationFieldBuffer.element(globalVertexIndex);
660
+ return loadTerrainFieldElevation(
661
+ terrainFieldStorage,
662
+ xClamped,
663
+ yClamped,
664
+ nodeIndex
665
+ );
350
666
  });
351
667
 
352
668
  function createTileCompute(leafStorage, uniforms) {
@@ -432,6 +748,7 @@ const quadtreeUpdate = param({
432
748
  distanceFactor: 1.5
433
749
  }).displayName("quadtreeUpdate");
434
750
  const surface = param(null).displayName("surface");
751
+ const terrainFieldFilter = param("linear").displayName("terrainFieldFilter");
435
752
  const elevationFn = param(() => float(0));
436
753
 
437
754
  function createLeafStorage(maxNodes) {
@@ -1239,21 +1556,21 @@ const elevationFieldStageTask = task((get, work) => {
1239
1556
  });
1240
1557
  }).displayName("elevationFieldStageTask");
1241
1558
 
1242
- const createNormalFieldContextTask = task((get, work) => {
1243
- const edgeVertexCount = get(innerTileSegments) + 3;
1244
- const verticesPerNode = edgeVertexCount * edgeVertexCount;
1245
- const totalElements = get(maxNodes) * verticesPerNode;
1246
- return work(() => {
1247
- const data = new Uint32Array(totalElements);
1248
- const attribute = new StorageBufferAttribute(data, 1);
1249
- const node = storage(attribute, "uint", totalElements);
1250
- return {
1251
- data,
1252
- attribute,
1253
- node
1254
- };
1255
- });
1256
- }).displayName("createNormalFieldContextTask");
1559
+ const createTerrainFieldTextureTask = task(
1560
+ (get, work, { resources }) => {
1561
+ const edgeVertexCount = get(innerTileSegments) + 3;
1562
+ const maxNodesValue = get(maxNodes);
1563
+ const filter = get(terrainFieldFilter);
1564
+ return work(
1565
+ () => createTerrainFieldStorage(
1566
+ edgeVertexCount,
1567
+ maxNodesValue,
1568
+ resources?.renderer,
1569
+ { filter }
1570
+ )
1571
+ );
1572
+ }
1573
+ ).displayName("createTerrainFieldTextureTask");
1257
1574
  function createNormalFromElevationField(elevationFieldNode, edgeVertexCount) {
1258
1575
  return Fn(
1259
1576
  ([nodeIndex, tileSize, ix, iy, elevationScale]) => {
@@ -1278,10 +1595,10 @@ function createNormalFromElevationField(elevationFieldNode, edgeVertexCount) {
1278
1595
  }
1279
1596
  );
1280
1597
  }
1281
- const normalFieldStageTask = task((get, work) => {
1598
+ const terrainFieldStageTask = task((get, work) => {
1282
1599
  const upstream = get(elevationFieldStageTask);
1283
1600
  const elevationFieldContext = get(createElevationFieldContextTask);
1284
- const normalFieldContext = get(createNormalFieldContextTask);
1601
+ const terrainFieldStorage = get(createTerrainFieldTextureTask);
1285
1602
  const tileEdgeVertexCount = get(innerTileSegments) + 3;
1286
1603
  const tile = get(tileNodesTask);
1287
1604
  const uniforms = get(createUniformsTask);
@@ -1296,6 +1613,7 @@ const normalFieldStageTask = task((get, work) => {
1296
1613
  const ix = int(localCoordinates.x);
1297
1614
  const iy = int(localCoordinates.y);
1298
1615
  const tileSize = tile.tileSize(nodeIndex);
1616
+ const height = elevationFieldContext.node.element(globalVertexIndex);
1299
1617
  const normalXZ = computeNormal(
1300
1618
  nodeIndex,
1301
1619
  tileSize,
@@ -1303,58 +1621,60 @@ const normalFieldStageTask = task((get, work) => {
1303
1621
  iy,
1304
1622
  uniforms.uElevationScale
1305
1623
  );
1306
- normalFieldContext.node.element(globalVertexIndex).assign(packHalf2x16(normalXZ));
1624
+ storeTerrainField(
1625
+ terrainFieldStorage,
1626
+ ix,
1627
+ iy,
1628
+ nodeIndex,
1629
+ packTerrainFieldSample(height, normalXZ)
1630
+ );
1307
1631
  }
1308
1632
  ];
1309
1633
  });
1310
- }).displayName("normalFieldStageTask");
1634
+ }).displayName("terrainFieldStageTask");
1311
1635
 
1312
1636
  const compileComputeTask = task((get, work) => {
1313
- const pipeline = get(normalFieldStageTask);
1637
+ const pipeline = get(terrainFieldStageTask);
1314
1638
  const edgeVertexCount = get(innerTileSegments) + 3;
1315
- return work(() => compileComputePipeline(pipeline, edgeVertexCount));
1316
- }).displayName("compileComputeTask");
1317
- const executeComputeTask = task((get, work, { resources }) => {
1318
- const { execute } = get(compileComputeTask);
1319
- const leafState = get(leafGpuBufferTask);
1320
1639
  return work(
1321
- () => resources?.renderer ? execute(resources.renderer, leafState.count) : () => {
1322
- }
1640
+ () => compileComputePipeline(pipeline, edgeVertexCount, {
1641
+ preferSingleKernelWhenPossible: false
1642
+ })
1323
1643
  );
1324
- }).displayName("executeComputeTask").lane("gpu");
1644
+ }).displayName("compileComputeTask");
1645
+ const executeComputeTask = task(
1646
+ (get, work, { resources }) => {
1647
+ const { execute } = get(compileComputeTask);
1648
+ const leafState = get(leafGpuBufferTask);
1649
+ return work(
1650
+ () => resources?.renderer ? execute(resources.renderer, leafState.count) : () => {
1651
+ }
1652
+ );
1653
+ }
1654
+ ).displayName("executeComputeTask").lane("gpu");
1325
1655
  function createComputePipelineTasks(leafStageTask) {
1326
1656
  const compile = task((get, work) => {
1327
1657
  const pipeline = get(leafStageTask);
1328
1658
  const edgeVertexCount = get(innerTileSegments) + 3;
1329
- return work(() => compileComputePipeline(pipeline, edgeVertexCount));
1659
+ return work(
1660
+ () => compileComputePipeline(pipeline, edgeVertexCount, {
1661
+ preferSingleKernelWhenPossible: false
1662
+ })
1663
+ );
1330
1664
  }).displayName("compileComputeTask");
1331
- const execute = task((get, work, { resources }) => {
1332
- const { execute: run } = get(compile);
1333
- const leafState = get(leafGpuBufferTask);
1334
- return work(() => resources?.renderer ? run(resources.renderer, leafState.count) : () => {
1335
- });
1336
- }).displayName("executeComputeTask").lane("gpu");
1665
+ const execute = task(
1666
+ (get, work, { resources }) => {
1667
+ const { execute: run } = get(compile);
1668
+ const leafState = get(leafGpuBufferTask);
1669
+ return work(
1670
+ () => resources?.renderer ? run(resources.renderer, leafState.count) : () => {
1671
+ }
1672
+ );
1673
+ }
1674
+ ).displayName("executeComputeTask").lane("gpu");
1337
1675
  return { compile, execute };
1338
1676
  }
1339
1677
 
1340
- const textureSpaceToVectorSpace = Fn(([value]) => {
1341
- return remap(value, float(0), float(1), float(-1), float(1));
1342
- });
1343
- const vectorSpaceToTextureSpace = Fn(([value]) => {
1344
- return remap(value, float(-1), float(1), float(0), float(1));
1345
- });
1346
- const blendAngleCorrectedNormals = Fn(([n1, n2]) => {
1347
- const t = vec3(n1.x, n1.y, n1.z.add(1));
1348
- const u = vec3(n2.x.negate(), n2.y.negate(), n2.z);
1349
- const r = t.mul(dot(t, u)).sub(u.mul(t.z)).normalize();
1350
- return r;
1351
- });
1352
- const deriveNormalZ = Fn(([normalXY]) => {
1353
- const xy = normalXY.toVar();
1354
- const z = xy.x.mul(xy.x).add(xy.y.mul(xy.y)).oneMinus().max(0).sqrt();
1355
- return vec3(xy.x, xy.y, z);
1356
- });
1357
-
1358
1678
  const isSkirtVertex = Fn(([segments]) => {
1359
1679
  const segmentsNode = typeof segments === "number" ? int(segments) : segments;
1360
1680
  const vIndex = int(vertexIndex);
@@ -1396,37 +1716,40 @@ function createTileBaseWorldPosition(leafStorage, terrainUniforms) {
1396
1716
  return vec3(worldX, rootOrigin.y, worldZ);
1397
1717
  });
1398
1718
  }
1399
- function createTileElevation(terrainUniforms, elevationFieldBufferNode) {
1400
- if (!elevationFieldBufferNode) return float(0);
1719
+ function createTileElevation(terrainUniforms, terrainFieldStorage) {
1720
+ if (!terrainFieldStorage) return float(0);
1401
1721
  const edgeVertexCount = terrainUniforms.uInnerTileSegments.add(3);
1402
1722
  return readElevationFieldAtPositionLocal(
1403
- elevationFieldBufferNode,
1723
+ terrainFieldStorage,
1404
1724
  edgeVertexCount,
1405
1725
  positionLocal
1406
1726
  )().mul(
1407
1727
  terrainUniforms.uElevationScale
1408
1728
  );
1409
1729
  }
1410
- function createNormalAssignment(terrainUniforms, normalFieldBufferNode) {
1411
- if (!normalFieldBufferNode) return;
1730
+ function createNormalAssignment(terrainUniforms, terrainFieldStorage) {
1731
+ if (!terrainFieldStorage) return;
1412
1732
  const nodeIndex = int(instanceIndex);
1413
- const intEdge = int(terrainUniforms.uInnerTileSegments.add(3));
1414
- const verticesPerNode = intEdge.mul(intEdge);
1415
- const globalVertexIndex = nodeIndex.mul(verticesPerNode).add(int(vertexIndex));
1416
- const packed = normalFieldBufferNode.element(globalVertexIndex);
1417
- const normalXZ = unpackHalf2x16(packed);
1418
- const reconstructed = deriveNormalZ(normalXZ);
1419
- normalLocal.assign(vec3(reconstructed.x, reconstructed.z, reconstructed.y));
1733
+ const edgeVertexCount = int(terrainUniforms.uInnerTileSegments.add(3));
1734
+ const localVertexIndex = int(vertexIndex);
1735
+ const ix = localVertexIndex.mod(edgeVertexCount);
1736
+ const iy = localVertexIndex.div(edgeVertexCount);
1737
+ const normalXZ = loadTerrainFieldNormal(terrainFieldStorage, ix, iy, nodeIndex);
1738
+ const nx = normalXZ.x;
1739
+ const nz = normalXZ.y;
1740
+ const nySq = float(1).sub(nx.mul(nx)).sub(nz.mul(nz)).max(float(0));
1741
+ const ny = nySq.sqrt();
1742
+ normalLocal.assign(vec3(nx, ny, nz));
1420
1743
  }
1421
- function createTileWorldPosition(leafStorage, terrainUniforms, elevationFieldBufferNode, normalFieldBufferNode) {
1744
+ function createTileWorldPosition(leafStorage, terrainUniforms, terrainFieldStorage) {
1422
1745
  const baseWorldPosition = createTileBaseWorldPosition(leafStorage, terrainUniforms);
1423
1746
  return Fn(() => {
1424
1747
  const base = baseWorldPosition();
1425
- const yElevation = createTileElevation(terrainUniforms, elevationFieldBufferNode);
1748
+ const yElevation = createTileElevation(terrainUniforms, terrainFieldStorage);
1426
1749
  const skirtVertex = isSkirtVertex(terrainUniforms.uInnerTileSegments);
1427
1750
  const skirtY = base.y.add(yElevation).sub(terrainUniforms.uSkirtScale.toVar());
1428
1751
  const worldY = select(skirtVertex, skirtY, base.y.add(yElevation));
1429
- createNormalAssignment(terrainUniforms, normalFieldBufferNode);
1752
+ createNormalAssignment(terrainUniforms, terrainFieldStorage);
1430
1753
  return vec3(base.x, worldY, base.z);
1431
1754
  })();
1432
1755
  }
@@ -1434,20 +1757,18 @@ function createTileWorldPosition(leafStorage, terrainUniforms, elevationFieldBuf
1434
1757
  const positionNodeTask = task((get, work) => {
1435
1758
  const leafStorage = get(leafStorageTask);
1436
1759
  const terrainUniforms = get(createUniformsTask);
1437
- const elevationFieldContext = get(createElevationFieldContextTask);
1438
- const normalFieldContext = get(createNormalFieldContextTask);
1760
+ const terrainFieldStorage = get(createTerrainFieldTextureTask);
1439
1761
  return work(
1440
1762
  () => createTileWorldPosition(
1441
1763
  leafStorage,
1442
1764
  terrainUniforms,
1443
- elevationFieldContext.node,
1444
- normalFieldContext.node
1765
+ terrainFieldStorage
1445
1766
  )
1446
1767
  );
1447
1768
  }).displayName("positionNodeTask");
1448
1769
 
1449
1770
  function terrainGraph() {
1450
- 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);
1771
+ 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);
1451
1772
  }
1452
1773
  const terrainTasks = {
1453
1774
  instanceId: instanceIdTask,
@@ -1461,13 +1782,31 @@ const terrainTasks = {
1461
1782
  positionNode: positionNodeTask,
1462
1783
  createElevationFieldContext: createElevationFieldContextTask,
1463
1784
  createTileNodes: tileNodesTask,
1464
- createNormalFieldContext: createNormalFieldContextTask,
1785
+ createTerrainFieldTexture: createTerrainFieldTextureTask,
1465
1786
  elevationFieldStage: elevationFieldStageTask,
1466
- normalFieldStage: normalFieldStageTask,
1787
+ terrainFieldStage: terrainFieldStageTask,
1467
1788
  compileCompute: compileComputeTask,
1468
1789
  executeCompute: executeComputeTask
1469
1790
  };
1470
1791
 
1792
+ const textureSpaceToVectorSpace = Fn(([value]) => {
1793
+ return remap(value, float(0), float(1), float(-1), float(1));
1794
+ });
1795
+ const vectorSpaceToTextureSpace = Fn(([value]) => {
1796
+ return remap(value, float(-1), float(1), float(0), float(1));
1797
+ });
1798
+ const blendAngleCorrectedNormals = Fn(([n1, n2]) => {
1799
+ const t = vec3(n1.x, n1.y, n1.z.add(1));
1800
+ const u = vec3(n2.x.negate(), n2.y.negate(), n2.z);
1801
+ const r = t.mul(dot(t, u)).sub(u.mul(t.z)).normalize();
1802
+ return r;
1803
+ });
1804
+ const deriveNormalZ = Fn(([normalXY]) => {
1805
+ const xy = normalXY.toVar();
1806
+ const z = xy.x.mul(xy.x).add(xy.y.mul(xy.y)).oneMinus().max(0).sqrt();
1807
+ return vec3(xy.x, xy.y, z);
1808
+ });
1809
+
1471
1810
  const vGlobalVertexIndex = /* @__PURE__ */ varyingProperty("int", "vGlobalVertexIndex");
1472
1811
  const vElevation = /* @__PURE__ */ varyingProperty("f32", "vElevation");
1473
1812
 
@@ -1502,4 +1841,4 @@ const voronoiCells = Fn((params) => {
1502
1841
  return k;
1503
1842
  });
1504
1843
 
1505
- 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 };
1844
+ 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, sampleTerrainField, sampleTerrainFieldElevation, sampleTerrainFieldNormal, skirtScale, storeTerrainField, surface, surfaceTask, terrainFieldFilter, terrainFieldStageTask, terrainGraph, terrainTasks, textureSpaceToVectorSpace, tileNodesTask, update, updateUniformsTask, vElevation, vGlobalVertexIndex, vectorSpaceToTextureSpace, voronoiCells };