@hello-terrain/three 0.0.0-alpha.6 → 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,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, 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,330 @@ 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(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
+
313
601
  const createElevation = (tile, uniforms, elevationFn) => {
314
602
  return function perVertexElevation(nodeIndex, localCoordinates) {
315
603
  const ix = int(localCoordinates.x);
@@ -331,7 +619,7 @@ const createElevation = (tile, uniforms, elevationFn) => {
331
619
  });
332
620
  };
333
621
  };
334
- const readElevationFieldAtPositionLocal = (elevationFieldBuffer, edgeVertexCount, positionLocal) => Fn(() => {
622
+ const readElevationFieldAtPositionLocal = (terrainFieldStorage, edgeVertexCount, positionLocal) => Fn(() => {
335
623
  const nodeIndex = int(instanceIndex);
336
624
  const intEdge = int(edgeVertexCount);
337
625
  const innerSegments = int(edgeVertexCount).sub(3);
@@ -343,10 +631,12 @@ const readElevationFieldAtPositionLocal = (elevationFieldBuffer, edgeVertexCount
343
631
  const y = v.mul(fInnerSegments).round().toInt().add(int(1));
344
632
  const xClamped = min(max(x, int(0)), last);
345
633
  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);
634
+ return loadTerrainFieldElevation(
635
+ terrainFieldStorage,
636
+ xClamped,
637
+ yClamped,
638
+ nodeIndex
639
+ );
350
640
  });
351
641
 
352
642
  function createTileCompute(leafStorage, uniforms) {
@@ -1239,21 +1529,19 @@ const elevationFieldStageTask = task((get, work) => {
1239
1529
  });
1240
1530
  }).displayName("elevationFieldStageTask");
1241
1531
 
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");
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");
1257
1545
  function createNormalFromElevationField(elevationFieldNode, edgeVertexCount) {
1258
1546
  return Fn(
1259
1547
  ([nodeIndex, tileSize, ix, iy, elevationScale]) => {
@@ -1278,10 +1566,10 @@ function createNormalFromElevationField(elevationFieldNode, edgeVertexCount) {
1278
1566
  }
1279
1567
  );
1280
1568
  }
1281
- const normalFieldStageTask = task((get, work) => {
1569
+ const terrainFieldStageTask = task((get, work) => {
1282
1570
  const upstream = get(elevationFieldStageTask);
1283
1571
  const elevationFieldContext = get(createElevationFieldContextTask);
1284
- const normalFieldContext = get(createNormalFieldContextTask);
1572
+ const terrainFieldStorage = get(createTerrainFieldTextureTask);
1285
1573
  const tileEdgeVertexCount = get(innerTileSegments) + 3;
1286
1574
  const tile = get(tileNodesTask);
1287
1575
  const uniforms = get(createUniformsTask);
@@ -1296,6 +1584,7 @@ const normalFieldStageTask = task((get, work) => {
1296
1584
  const ix = int(localCoordinates.x);
1297
1585
  const iy = int(localCoordinates.y);
1298
1586
  const tileSize = tile.tileSize(nodeIndex);
1587
+ const height = elevationFieldContext.node.element(globalVertexIndex);
1299
1588
  const normalXZ = computeNormal(
1300
1589
  nodeIndex,
1301
1590
  tileSize,
@@ -1303,58 +1592,60 @@ const normalFieldStageTask = task((get, work) => {
1303
1592
  iy,
1304
1593
  uniforms.uElevationScale
1305
1594
  );
1306
- normalFieldContext.node.element(globalVertexIndex).assign(packHalf2x16(normalXZ));
1595
+ storeTerrainField(
1596
+ terrainFieldStorage,
1597
+ ix,
1598
+ iy,
1599
+ nodeIndex,
1600
+ packTerrainFieldSample(height, normalXZ)
1601
+ );
1307
1602
  }
1308
1603
  ];
1309
1604
  });
1310
- }).displayName("normalFieldStageTask");
1605
+ }).displayName("terrainFieldStageTask");
1311
1606
 
1312
1607
  const compileComputeTask = task((get, work) => {
1313
- const pipeline = get(normalFieldStageTask);
1608
+ const pipeline = get(terrainFieldStageTask);
1314
1609
  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
1610
  return work(
1321
- () => resources?.renderer ? execute(resources.renderer, leafState.count) : () => {
1322
- }
1611
+ () => compileComputePipeline(pipeline, edgeVertexCount, {
1612
+ preferSingleKernelWhenPossible: false
1613
+ })
1323
1614
  );
1324
- }).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");
1325
1626
  function createComputePipelineTasks(leafStageTask) {
1326
1627
  const compile = task((get, work) => {
1327
1628
  const pipeline = get(leafStageTask);
1328
1629
  const edgeVertexCount = get(innerTileSegments) + 3;
1329
- return work(() => compileComputePipeline(pipeline, edgeVertexCount));
1630
+ return work(
1631
+ () => compileComputePipeline(pipeline, edgeVertexCount, {
1632
+ preferSingleKernelWhenPossible: false
1633
+ })
1634
+ );
1330
1635
  }).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");
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");
1337
1646
  return { compile, execute };
1338
1647
  }
1339
1648
 
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
1649
  const isSkirtVertex = Fn(([segments]) => {
1359
1650
  const segmentsNode = typeof segments === "number" ? int(segments) : segments;
1360
1651
  const vIndex = int(vertexIndex);
@@ -1396,37 +1687,40 @@ function createTileBaseWorldPosition(leafStorage, terrainUniforms) {
1396
1687
  return vec3(worldX, rootOrigin.y, worldZ);
1397
1688
  });
1398
1689
  }
1399
- function createTileElevation(terrainUniforms, elevationFieldBufferNode) {
1400
- if (!elevationFieldBufferNode) return float(0);
1690
+ function createTileElevation(terrainUniforms, terrainFieldStorage) {
1691
+ if (!terrainFieldStorage) return float(0);
1401
1692
  const edgeVertexCount = terrainUniforms.uInnerTileSegments.add(3);
1402
1693
  return readElevationFieldAtPositionLocal(
1403
- elevationFieldBufferNode,
1694
+ terrainFieldStorage,
1404
1695
  edgeVertexCount,
1405
1696
  positionLocal
1406
1697
  )().mul(
1407
1698
  terrainUniforms.uElevationScale
1408
1699
  );
1409
1700
  }
1410
- function createNormalAssignment(terrainUniforms, normalFieldBufferNode) {
1411
- if (!normalFieldBufferNode) return;
1701
+ function createNormalAssignment(terrainUniforms, terrainFieldStorage) {
1702
+ if (!terrainFieldStorage) return;
1412
1703
  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));
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));
1420
1714
  }
1421
- function createTileWorldPosition(leafStorage, terrainUniforms, elevationFieldBufferNode, normalFieldBufferNode) {
1715
+ function createTileWorldPosition(leafStorage, terrainUniforms, terrainFieldStorage) {
1422
1716
  const baseWorldPosition = createTileBaseWorldPosition(leafStorage, terrainUniforms);
1423
1717
  return Fn(() => {
1424
1718
  const base = baseWorldPosition();
1425
- const yElevation = createTileElevation(terrainUniforms, elevationFieldBufferNode);
1719
+ const yElevation = createTileElevation(terrainUniforms, terrainFieldStorage);
1426
1720
  const skirtVertex = isSkirtVertex(terrainUniforms.uInnerTileSegments);
1427
1721
  const skirtY = base.y.add(yElevation).sub(terrainUniforms.uSkirtScale.toVar());
1428
1722
  const worldY = select(skirtVertex, skirtY, base.y.add(yElevation));
1429
- createNormalAssignment(terrainUniforms, normalFieldBufferNode);
1723
+ createNormalAssignment(terrainUniforms, terrainFieldStorage);
1430
1724
  return vec3(base.x, worldY, base.z);
1431
1725
  })();
1432
1726
  }
@@ -1434,20 +1728,18 @@ function createTileWorldPosition(leafStorage, terrainUniforms, elevationFieldBuf
1434
1728
  const positionNodeTask = task((get, work) => {
1435
1729
  const leafStorage = get(leafStorageTask);
1436
1730
  const terrainUniforms = get(createUniformsTask);
1437
- const elevationFieldContext = get(createElevationFieldContextTask);
1438
- const normalFieldContext = get(createNormalFieldContextTask);
1731
+ const terrainFieldStorage = get(createTerrainFieldTextureTask);
1439
1732
  return work(
1440
1733
  () => createTileWorldPosition(
1441
1734
  leafStorage,
1442
1735
  terrainUniforms,
1443
- elevationFieldContext.node,
1444
- normalFieldContext.node
1736
+ terrainFieldStorage
1445
1737
  )
1446
1738
  );
1447
1739
  }).displayName("positionNodeTask");
1448
1740
 
1449
1741
  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);
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);
1451
1743
  }
1452
1744
  const terrainTasks = {
1453
1745
  instanceId: instanceIdTask,
@@ -1461,13 +1753,31 @@ const terrainTasks = {
1461
1753
  positionNode: positionNodeTask,
1462
1754
  createElevationFieldContext: createElevationFieldContextTask,
1463
1755
  createTileNodes: tileNodesTask,
1464
- createNormalFieldContext: createNormalFieldContextTask,
1756
+ createTerrainFieldTexture: createTerrainFieldTextureTask,
1465
1757
  elevationFieldStage: elevationFieldStageTask,
1466
- normalFieldStage: normalFieldStageTask,
1758
+ terrainFieldStage: terrainFieldStageTask,
1467
1759
  compileCompute: compileComputeTask,
1468
1760
  executeCompute: executeComputeTask
1469
1761
  };
1470
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
+
1471
1781
  const vGlobalVertexIndex = /* @__PURE__ */ varyingProperty("int", "vGlobalVertexIndex");
1472
1782
  const vElevation = /* @__PURE__ */ varyingProperty("f32", "vElevation");
1473
1783
 
@@ -1502,4 +1812,4 @@ const voronoiCells = Fn((params) => {
1502
1812
  return k;
1503
1813
  });
1504
1814
 
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 };
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 };