@hello-terrain/three 0.0.0-alpha.10 → 0.0.0-alpha.12
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 +2856 -1277
- package/dist/index.d.cts +739 -226
- package/dist/index.d.mts +739 -226
- package/dist/index.d.ts +739 -226
- package/dist/index.mjs +2828 -1273
- package/package.json +4 -3
package/dist/index.mjs
CHANGED
|
@@ -1,42 +1,47 @@
|
|
|
1
1
|
import { BufferGeometry, BufferAttribute, RGBAFormat, ClampToEdgeWrapping, HalfFloatType, FloatType, LinearFilter, NearestFilter, Vector3 } from 'three';
|
|
2
2
|
import { MeshStandardNodeMaterial, InstancedMesh, InstancedBufferAttribute, StorageTexture, StorageArrayTexture, StorageBufferAttribute, Vector3 as Vector3$1 } from 'three/webgpu';
|
|
3
3
|
import { param, task, graph } from '@hello-terrain/work';
|
|
4
|
-
import { uniform, Fn,
|
|
4
|
+
import { float, uniform, Fn, globalId, int, vec2, uint, If, vec3, textureLoad, ivec2, textureStore, uvec3, vec4, texture, ivec3, pow, storage, cross, vertexIndex, uv, instanceIndex, positionLocal, select, normalLocal, Loop, Break, sin, cos, bool, workgroupArray, localId, workgroupId, min, max, workgroupBarrier, remap, dot as dot$1, varyingProperty, mx_noise_float, mix } from 'three/tsl';
|
|
5
5
|
import { Fn as Fn$1 } from 'three/src/nodes/TSL.js';
|
|
6
6
|
|
|
7
7
|
class TerrainGeometry extends BufferGeometry {
|
|
8
|
-
|
|
8
|
+
/**
|
|
9
|
+
* @param flipWinding Reverse triangle winding so front faces point the
|
|
10
|
+
* opposite way. The default winding makes flat tiles front-face `+Y`; the
|
|
11
|
+
* cube-sphere maps `(u→right, v→up)`, which would otherwise leave the
|
|
12
|
+
* planet's outer shell back-facing, so it passes `flipWinding` to render
|
|
13
|
+
* the outer surface with `FrontSide`.
|
|
14
|
+
*/
|
|
15
|
+
constructor(innerSegments = 14, extendUV = false, flipWinding = false) {
|
|
9
16
|
super();
|
|
10
17
|
if (innerSegments < 1 || !Number.isFinite(innerSegments) || !Number.isInteger(innerSegments)) {
|
|
11
|
-
throw new Error(
|
|
12
|
-
`Invalid innerSegments: ${innerSegments}. Must be a positive integer.`
|
|
13
|
-
);
|
|
18
|
+
throw new Error(`Invalid innerSegments: ${innerSegments}. Must be a positive integer.`);
|
|
14
19
|
}
|
|
15
20
|
try {
|
|
16
|
-
this.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
const index = this.generateIndices(innerSegments, flipWinding);
|
|
22
|
+
const indexAttribute = new BufferAttribute(new Uint32Array(index), 1);
|
|
23
|
+
indexAttribute.name = "terrainIndex";
|
|
24
|
+
this.setIndex(indexAttribute);
|
|
25
|
+
const positionAttribute = new BufferAttribute(
|
|
26
|
+
new Float32Array(this.generatePositions(innerSegments)),
|
|
27
|
+
3
|
|
23
28
|
);
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
)
|
|
29
|
+
positionAttribute.name = "terrainPosition";
|
|
30
|
+
this.setAttribute("position", positionAttribute);
|
|
31
|
+
const normalAttribute = new BufferAttribute(
|
|
32
|
+
new Float32Array(this.generateNormals(innerSegments)),
|
|
33
|
+
3
|
|
30
34
|
);
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
)
|
|
35
|
+
normalAttribute.name = "terrainNormal";
|
|
36
|
+
this.setAttribute("normal", normalAttribute);
|
|
37
|
+
const uvAttribute = new BufferAttribute(
|
|
38
|
+
new Float32Array(
|
|
39
|
+
extendUV ? this.generateUvsExtended(innerSegments) : this.generateUvsOnlyInner(innerSegments)
|
|
40
|
+
),
|
|
41
|
+
2
|
|
39
42
|
);
|
|
43
|
+
uvAttribute.name = "terrainUv";
|
|
44
|
+
this.setAttribute("uv", uvAttribute);
|
|
40
45
|
} catch (error) {
|
|
41
46
|
console.error("Error creating TerrainGeometry:", error);
|
|
42
47
|
throw error;
|
|
@@ -101,12 +106,16 @@ class TerrainGeometry extends BufferGeometry {
|
|
|
101
106
|
* triangle 1: a, c, b
|
|
102
107
|
* triangle 2: b, c, d
|
|
103
108
|
*/
|
|
104
|
-
generateIndices(innerSegments) {
|
|
109
|
+
generateIndices(innerSegments, flipWinding = false) {
|
|
105
110
|
const innerEdgeVertexCount = innerSegments + 1;
|
|
106
111
|
const edgeVertexCountWithSkirt = innerEdgeVertexCount + 2;
|
|
107
112
|
const indices = [];
|
|
108
113
|
const cellsPerEdge = edgeVertexCountWithSkirt - 1;
|
|
109
114
|
const mid = Math.floor(cellsPerEdge / 2);
|
|
115
|
+
const pushTri = (v0, v1, v2) => {
|
|
116
|
+
if (flipWinding) indices.push(v0, v2, v1);
|
|
117
|
+
else indices.push(v0, v1, v2);
|
|
118
|
+
};
|
|
110
119
|
for (let y = 0; y < cellsPerEdge; y++) {
|
|
111
120
|
for (let x = 0; x < cellsPerEdge; x++) {
|
|
112
121
|
const a = y * edgeVertexCountWithSkirt + x;
|
|
@@ -123,11 +132,11 @@ class TerrainGeometry extends BufferGeometry {
|
|
|
123
132
|
useDefaultDiagonal = (x + y) % 2 === 0;
|
|
124
133
|
}
|
|
125
134
|
if (useDefaultDiagonal) {
|
|
126
|
-
|
|
127
|
-
|
|
135
|
+
pushTri(a, d, b);
|
|
136
|
+
pushTri(a, c, d);
|
|
128
137
|
} else {
|
|
129
|
-
|
|
130
|
-
|
|
138
|
+
pushTri(a, c, b);
|
|
139
|
+
pushTri(b, c, d);
|
|
131
140
|
}
|
|
132
141
|
}
|
|
133
142
|
}
|
|
@@ -218,32 +227,71 @@ class TerrainGeometry extends BufferGeometry {
|
|
|
218
227
|
}
|
|
219
228
|
}
|
|
220
229
|
|
|
230
|
+
const rootSize = param(256).displayName("rootSize");
|
|
231
|
+
const origin = param({
|
|
232
|
+
x: 0,
|
|
233
|
+
y: 0,
|
|
234
|
+
z: 0
|
|
235
|
+
}).displayName("origin");
|
|
236
|
+
const innerTileSegments = param(61).displayName("innerTileSegments");
|
|
237
|
+
const skirtScale = param(100).displayName("skirtScale");
|
|
238
|
+
const elevationScale = param(1).displayName("elevationScale");
|
|
239
|
+
const radius = param(1e3).displayName("radius");
|
|
240
|
+
const maxNodes = param(1024).displayName("maxNodes");
|
|
241
|
+
const maxLevel = param(16).displayName("maxLevel");
|
|
242
|
+
const quadtreeUpdate = param({
|
|
243
|
+
cameraOrigin: { x: 0, y: 0, z: 0 },
|
|
244
|
+
mode: "distance",
|
|
245
|
+
distanceFactor: 1.5
|
|
246
|
+
}).displayName("quadtreeUpdate");
|
|
247
|
+
const topology = param(null).displayName("topology");
|
|
248
|
+
const terrainFieldFilter = param("linear").displayName(
|
|
249
|
+
"terrainFieldFilter"
|
|
250
|
+
);
|
|
251
|
+
const elevationFn = param(() => float(0));
|
|
252
|
+
|
|
221
253
|
const defaultTerrainMeshParams = {
|
|
222
|
-
innerTileSegments
|
|
254
|
+
// Source of truth is the `innerTileSegments` param itself.
|
|
255
|
+
innerTileSegments: innerTileSegments.get(),
|
|
223
256
|
maxNodes: 1024,
|
|
224
|
-
material: new MeshStandardNodeMaterial()
|
|
257
|
+
material: new MeshStandardNodeMaterial(),
|
|
258
|
+
flipWinding: false
|
|
225
259
|
};
|
|
226
260
|
class TerrainMesh extends InstancedMesh {
|
|
227
261
|
_innerTileSegments;
|
|
228
262
|
_maxNodes;
|
|
263
|
+
_flipWinding;
|
|
229
264
|
terrainRaycast = null;
|
|
230
265
|
constructor(params = defaultTerrainMeshParams) {
|
|
231
266
|
const mergedParams = { ...defaultTerrainMeshParams, ...params };
|
|
232
|
-
const { innerTileSegments, maxNodes, material } = mergedParams;
|
|
233
|
-
const geometry = new TerrainGeometry(innerTileSegments, true);
|
|
267
|
+
const { innerTileSegments, maxNodes, material, flipWinding } = mergedParams;
|
|
268
|
+
const geometry = new TerrainGeometry(innerTileSegments, true, flipWinding);
|
|
234
269
|
super(geometry, material, maxNodes);
|
|
270
|
+
this.instanceMatrix.name = "terrainInstanceMatrix";
|
|
235
271
|
this.frustumCulled = false;
|
|
236
272
|
this._innerTileSegments = innerTileSegments;
|
|
237
273
|
this._maxNodes = maxNodes;
|
|
274
|
+
this._flipWinding = flipWinding;
|
|
238
275
|
}
|
|
239
276
|
get innerTileSegments() {
|
|
240
277
|
return this._innerTileSegments;
|
|
241
278
|
}
|
|
242
279
|
set innerTileSegments(tileSegments) {
|
|
280
|
+
if (tileSegments === this._innerTileSegments) return;
|
|
243
281
|
const oldGeometry = this.geometry;
|
|
244
|
-
this.geometry = new TerrainGeometry(tileSegments, true);
|
|
282
|
+
this.geometry = new TerrainGeometry(tileSegments, true, this._flipWinding);
|
|
245
283
|
this._innerTileSegments = tileSegments;
|
|
246
|
-
setTimeout(oldGeometry.dispose);
|
|
284
|
+
setTimeout(() => oldGeometry.dispose());
|
|
285
|
+
}
|
|
286
|
+
get flipWinding() {
|
|
287
|
+
return this._flipWinding;
|
|
288
|
+
}
|
|
289
|
+
set flipWinding(flip) {
|
|
290
|
+
if (flip === this._flipWinding) return;
|
|
291
|
+
const oldGeometry = this.geometry;
|
|
292
|
+
this.geometry = new TerrainGeometry(this._innerTileSegments, true, flip);
|
|
293
|
+
this._flipWinding = flip;
|
|
294
|
+
setTimeout(() => oldGeometry.dispose());
|
|
247
295
|
}
|
|
248
296
|
get maxNodes() {
|
|
249
297
|
return this._maxNodes;
|
|
@@ -258,12 +306,14 @@ class TerrainMesh extends InstancedMesh {
|
|
|
258
306
|
const oldMatrixArray = this.instanceMatrix.array;
|
|
259
307
|
nextMatrix.set(oldMatrixArray.subarray(0, Math.min(oldMatrixArray.length, nextMatrix.length)));
|
|
260
308
|
this.instanceMatrix = new InstancedBufferAttribute(nextMatrix, 16);
|
|
309
|
+
this.instanceMatrix.name = "terrainInstanceMatrix";
|
|
261
310
|
if (this.instanceColor) {
|
|
262
311
|
const itemSize = this.instanceColor.itemSize;
|
|
263
312
|
const nextColor = new Float32Array(maxNodes * itemSize);
|
|
264
313
|
const oldColorArray = this.instanceColor.array;
|
|
265
314
|
nextColor.set(oldColorArray.subarray(0, Math.min(oldColorArray.length, nextColor.length)));
|
|
266
315
|
this.instanceColor = new InstancedBufferAttribute(nextColor, itemSize);
|
|
316
|
+
this.instanceColor.name = "terrainInstanceColor";
|
|
267
317
|
}
|
|
268
318
|
this._maxNodes = maxNodes;
|
|
269
319
|
this.count = Math.min(this.count, maxNodes);
|
|
@@ -309,13 +359,8 @@ function compileComputePipeline(stages, width, options) {
|
|
|
309
359
|
WORKGROUP_X,
|
|
310
360
|
WORKGROUP_Y
|
|
311
361
|
];
|
|
312
|
-
const
|
|
313
|
-
const uInstanceCount = uniform(0, "uint");
|
|
314
|
-
let singleKernel;
|
|
362
|
+
const uInstanceCount = uniform(0, "uint").setName("uInstanceCount");
|
|
315
363
|
const stagedKernelCache = /* @__PURE__ */ new Map();
|
|
316
|
-
function canRunSingleKernel(widthValue, limits) {
|
|
317
|
-
return widthValue <= limits.maxWorkgroupSizeX && widthValue <= limits.maxWorkgroupSizeY && widthValue * widthValue <= limits.maxWorkgroupInvocations;
|
|
318
|
-
}
|
|
319
364
|
function clampWorkgroupToLimits(requested, limits) {
|
|
320
365
|
let x = Math.max(1, Math.floor(requested[0]));
|
|
321
366
|
let y = Math.max(1, Math.floor(requested[1]));
|
|
@@ -331,37 +376,6 @@ function compileComputePipeline(stages, width, options) {
|
|
|
331
376
|
);
|
|
332
377
|
return [x, y];
|
|
333
378
|
}
|
|
334
|
-
function buildSingleKernel(workgroupSize) {
|
|
335
|
-
return Fn(() => {
|
|
336
|
-
bindings?.forEach((b) => b.toVar());
|
|
337
|
-
const fWidth = float(width);
|
|
338
|
-
const activeIndex = globalId.z;
|
|
339
|
-
const nodeIndex = int(activeIndex).toVar();
|
|
340
|
-
const iWidth = int(width);
|
|
341
|
-
const ix = int(globalId.x);
|
|
342
|
-
const iy = int(globalId.y);
|
|
343
|
-
const texelSize = vec2(1, 1).div(fWidth);
|
|
344
|
-
const localCoordinates = vec2(globalId.x, globalId.y);
|
|
345
|
-
const localUVCoords = localCoordinates.div(fWidth);
|
|
346
|
-
const verticesPerNode = iWidth.mul(iWidth);
|
|
347
|
-
const globalIndex = int(nodeIndex).mul(verticesPerNode).add(iy.mul(iWidth).add(ix));
|
|
348
|
-
const inBounds = ix.lessThan(iWidth).and(iy.lessThan(iWidth)).and(uint(activeIndex).lessThan(uInstanceCount)).toVar();
|
|
349
|
-
for (let i = 0; i < stages.length; i++) {
|
|
350
|
-
if (i > 0) {
|
|
351
|
-
workgroupBarrier();
|
|
352
|
-
}
|
|
353
|
-
If(inBounds, () => {
|
|
354
|
-
stages[i](
|
|
355
|
-
nodeIndex,
|
|
356
|
-
globalIndex,
|
|
357
|
-
localUVCoords,
|
|
358
|
-
localCoordinates,
|
|
359
|
-
texelSize
|
|
360
|
-
);
|
|
361
|
-
});
|
|
362
|
-
}
|
|
363
|
-
})().computeKernel(workgroupSize);
|
|
364
|
-
}
|
|
365
379
|
function buildStagedKernels(workgroupSize) {
|
|
366
380
|
return stages.map(
|
|
367
381
|
(stage) => Fn(() => {
|
|
@@ -392,15 +406,7 @@ function compileComputePipeline(stages, width, options) {
|
|
|
392
406
|
}
|
|
393
407
|
function execute(renderer, instanceCount) {
|
|
394
408
|
const limits = getDeviceComputeLimits(renderer);
|
|
395
|
-
const canUseSingleKernel = preferSingleKernelWhenPossible && canRunSingleKernel(width, limits);
|
|
396
409
|
uInstanceCount.value = instanceCount;
|
|
397
|
-
if (canUseSingleKernel) {
|
|
398
|
-
if (!singleKernel) {
|
|
399
|
-
singleKernel = buildSingleKernel([width, width, 1]);
|
|
400
|
-
}
|
|
401
|
-
renderer.compute(singleKernel, [1, 1, instanceCount]);
|
|
402
|
-
return;
|
|
403
|
-
}
|
|
404
410
|
const [workgroupX, workgroupY] = clampWorkgroupToLimits(
|
|
405
411
|
preferredWorkgroup,
|
|
406
412
|
limits
|
|
@@ -444,6 +450,7 @@ function ArrayTextureBackend(edgeVertexCount, tileCount, options) {
|
|
|
444
450
|
edgeVertexCount,
|
|
445
451
|
tileCount
|
|
446
452
|
);
|
|
453
|
+
tex.name = "terrainField";
|
|
447
454
|
configureStorageTexture(tex, options.format, options.filter);
|
|
448
455
|
return {
|
|
449
456
|
backendType: "array-texture",
|
|
@@ -487,6 +494,7 @@ function AtlasBackend(edgeVertexCount, tileCount, options) {
|
|
|
487
494
|
let tilesPerRow = Math.max(1, Math.ceil(Math.sqrt(tileCount)));
|
|
488
495
|
const atlasSize = tilesPerRow * edgeVertexCount;
|
|
489
496
|
const tex = new StorageTexture(atlasSize, atlasSize);
|
|
497
|
+
tex.name = "terrainFieldAtlas";
|
|
490
498
|
configureStorageTexture(tex, options.format, options.filter);
|
|
491
499
|
return {
|
|
492
500
|
backendType: "atlas",
|
|
@@ -543,40 +551,9 @@ function AtlasBackend(edgeVertexCount, tileCount, options) {
|
|
|
543
551
|
}
|
|
544
552
|
};
|
|
545
553
|
}
|
|
546
|
-
function
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
const tex = new StorageArrayTexture(
|
|
550
|
-
edgeVertexCount,
|
|
551
|
-
edgeVertexCount,
|
|
552
|
-
tileCount
|
|
553
|
-
);
|
|
554
|
-
configureStorageTexture(tex, options.format, options.filter);
|
|
555
|
-
return {
|
|
556
|
-
backendType: "texture-3d",
|
|
557
|
-
get edgeVertexCount() {
|
|
558
|
-
return currentEdgeVertexCount;
|
|
559
|
-
},
|
|
560
|
-
get tileCount() {
|
|
561
|
-
return currentTileCount;
|
|
562
|
-
},
|
|
563
|
-
texture: tex,
|
|
564
|
-
uv(ix, iy, _tileIndex) {
|
|
565
|
-
return vec2(ix.toFloat(), iy.toFloat());
|
|
566
|
-
},
|
|
567
|
-
texel(ix, iy, tileIndex) {
|
|
568
|
-
return ivec3(ix, iy, tileIndex);
|
|
569
|
-
},
|
|
570
|
-
sample(u, v, tileIndex) {
|
|
571
|
-
return texture(tex, vec2(u, v)).depth(int(tileIndex));
|
|
572
|
-
},
|
|
573
|
-
resize(width, height, nextTileCount) {
|
|
574
|
-
currentEdgeVertexCount = width;
|
|
575
|
-
currentTileCount = nextTileCount;
|
|
576
|
-
tex.setSize(width, height, nextTileCount);
|
|
577
|
-
tex.needsUpdate = true;
|
|
578
|
-
}
|
|
579
|
-
};
|
|
554
|
+
function texture3DBackend(edgeVertexCount, tileCount, options) {
|
|
555
|
+
const storage = ArrayTextureBackend(edgeVertexCount, tileCount, options);
|
|
556
|
+
return { ...storage, backendType: "texture-3d" };
|
|
580
557
|
}
|
|
581
558
|
function tryGetDeviceLimits(renderer) {
|
|
582
559
|
const backend = renderer;
|
|
@@ -590,7 +567,7 @@ function createTerrainFieldStorage(edgeVertexCount, tileCount, renderer, options
|
|
|
590
567
|
return AtlasBackend(edgeVertexCount, tileCount, { filter, format });
|
|
591
568
|
}
|
|
592
569
|
if (forcedBackend === "texture-3d") {
|
|
593
|
-
return
|
|
570
|
+
return texture3DBackend(edgeVertexCount, tileCount, { filter, format });
|
|
594
571
|
}
|
|
595
572
|
if (forcedBackend === "array-texture") {
|
|
596
573
|
return ArrayTextureBackend(edgeVertexCount, tileCount, { filter, format });
|
|
@@ -625,7 +602,7 @@ function loadTerrainFieldElevation(storage, ix, iy, tileIndex) {
|
|
|
625
602
|
}
|
|
626
603
|
function loadTerrainFieldNormal(storage, ix, iy, tileIndex) {
|
|
627
604
|
const raw = loadTerrainField(storage, ix, iy, tileIndex);
|
|
628
|
-
return
|
|
605
|
+
return vec3(raw.g, raw.b, raw.a);
|
|
629
606
|
}
|
|
630
607
|
function sampleTerrainField(storage, u, v, tileIndex) {
|
|
631
608
|
return storage.sample(u, v, tileIndex);
|
|
@@ -633,12 +610,8 @@ function sampleTerrainField(storage, u, v, tileIndex) {
|
|
|
633
610
|
function sampleTerrainFieldElevation(storage, u, v, tileIndex) {
|
|
634
611
|
return sampleTerrainField(storage, u, v, tileIndex).r;
|
|
635
612
|
}
|
|
636
|
-
function
|
|
637
|
-
|
|
638
|
-
return vec2(raw.g, raw.b);
|
|
639
|
-
}
|
|
640
|
-
function packTerrainFieldSample(height, normalXZ, extra = float(0)) {
|
|
641
|
-
return vec4(height, normalXZ.x, normalXZ.y, extra);
|
|
613
|
+
function packTerrainFieldSample(height, normal) {
|
|
614
|
+
return vec4(height, normal.x, normal.y, normal.z);
|
|
642
615
|
}
|
|
643
616
|
|
|
644
617
|
const createElevation = (tile, uniforms, elevationFn) => {
|
|
@@ -663,99 +636,70 @@ const createElevation = (tile, uniforms, elevationFn) => {
|
|
|
663
636
|
};
|
|
664
637
|
};
|
|
665
638
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
639
|
+
const HALF_PI = Math.PI * 0.5;
|
|
640
|
+
const FIELD_INNER_TEXEL_OFFSET = 1.5;
|
|
641
|
+
const FIELD_EDGE_EXTRA_TEXELS = 3;
|
|
642
|
+
function sphereTileArcLength(radius, levelDivisor) {
|
|
643
|
+
return radius * HALF_PI / levelDivisor;
|
|
644
|
+
}
|
|
645
|
+
function decodeLeafTile(leafStorage, nodeIndex) {
|
|
646
|
+
const nodeOffset = int(nodeIndex).mul(int(4));
|
|
647
|
+
return {
|
|
648
|
+
level: leafStorage.node.element(nodeOffset).toInt(),
|
|
649
|
+
x: leafStorage.node.element(nodeOffset.add(int(1))).toFloat(),
|
|
650
|
+
y: leafStorage.node.element(nodeOffset.add(int(2))).toFloat(),
|
|
651
|
+
face: leafStorage.node.element(nodeOffset.add(int(3))).toInt()
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
function faceUVFromTileLocal(tile, localU, localV, baseU = float(1), baseV = float(1)) {
|
|
655
|
+
const levelScale = pow(float(2), tile.level.toFloat());
|
|
656
|
+
const nU = baseU.mul(levelScale);
|
|
657
|
+
const nV = baseV.mul(levelScale);
|
|
658
|
+
return vec2(tile.x.add(localU).div(nU), tile.y.add(localV).div(nV));
|
|
659
|
+
}
|
|
660
|
+
function createTileCompute(leafStorage, uniforms, projection) {
|
|
661
|
+
const baseU = float(projection.baseResolution?.u ?? 1);
|
|
662
|
+
const baseV = float(projection.baseResolution?.v ?? 1);
|
|
663
|
+
const tileLevel = Fn(([nodeIndex]) => decodeLeafTile(leafStorage, nodeIndex).level);
|
|
664
|
+
const tileFace = Fn(([nodeIndex]) => decodeLeafTile(leafStorage, nodeIndex).face);
|
|
671
665
|
const tileOriginVec2 = Fn(([nodeIndex]) => {
|
|
672
|
-
const
|
|
673
|
-
|
|
674
|
-
const nodeY = leafStorage.node.element(nodeOffset.add(int(2))).toFloat();
|
|
675
|
-
return vec2(nodeX, nodeY);
|
|
676
|
-
});
|
|
677
|
-
const tileSize = Fn(([nodeIndex]) => {
|
|
678
|
-
const rootSize = uniforms.uRootSize.toVar();
|
|
679
|
-
const level = tileLevel(nodeIndex);
|
|
680
|
-
return float(rootSize).div(pow(float(2), level.toFloat()));
|
|
666
|
+
const tile = decodeLeafTile(leafStorage, nodeIndex);
|
|
667
|
+
return vec2(tile.x, tile.y);
|
|
681
668
|
});
|
|
682
|
-
const
|
|
683
|
-
const
|
|
684
|
-
const nodeX = nodeVec2.x;
|
|
685
|
-
const nodeY = nodeVec2.y;
|
|
686
|
-
const rootSize = uniforms.uRootSize.toVar();
|
|
687
|
-
const rootOrigin = uniforms.uRootOrigin.toVar();
|
|
688
|
-
const size = tileSize(nodeIndex);
|
|
689
|
-
const half = float(0.5);
|
|
690
|
-
const halfRoot = float(rootSize).mul(half);
|
|
669
|
+
const tileFaceUV = Fn(([nodeIndex, ix, iy]) => {
|
|
670
|
+
const tile = decodeLeafTile(leafStorage, nodeIndex);
|
|
691
671
|
const fInnerSegments = uniforms.uInnerTileSegments.toVar().toFloat();
|
|
692
|
-
const
|
|
693
|
-
const
|
|
694
|
-
|
|
695
|
-
const worldX = rootOrigin.x.add(absX.mul(texelSpacing)).sub(halfRoot);
|
|
696
|
-
const worldZ = rootOrigin.z.add(absY.mul(texelSpacing)).sub(halfRoot);
|
|
697
|
-
const centeredX = worldX.sub(rootOrigin.x);
|
|
698
|
-
const centeredZ = worldZ.sub(rootOrigin.z);
|
|
699
|
-
return vec2(
|
|
700
|
-
centeredX.div(rootSize).add(half),
|
|
701
|
-
centeredZ.div(rootSize).mul(float(-1)).add(half)
|
|
702
|
-
);
|
|
672
|
+
const localU = int(ix).toFloat().sub(float(1)).div(fInnerSegments);
|
|
673
|
+
const localV = int(iy).toFloat().sub(float(1)).div(fInnerSegments);
|
|
674
|
+
return faceUVFromTileLocal(tile, localU, localV, baseU, baseV);
|
|
703
675
|
});
|
|
704
|
-
const
|
|
705
|
-
(
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
const size = tileSize(nodeIndex);
|
|
712
|
-
const half = float(0.5);
|
|
713
|
-
const halfRoot = float(rootSize).mul(half);
|
|
714
|
-
const fInnerSegments = uniforms.uInnerTileSegments.toVar().toFloat();
|
|
715
|
-
const texelSpacing = size.div(fInnerSegments);
|
|
716
|
-
const absX = nodeX.mul(fInnerSegments).add(int(ix).toFloat().sub(float(1)));
|
|
717
|
-
const absY = nodeY.mul(fInnerSegments).add(int(iy).toFloat().sub(float(1)));
|
|
718
|
-
const worldX = rootOrigin.x.add(absX.mul(texelSpacing)).sub(halfRoot);
|
|
719
|
-
const worldZ = rootOrigin.z.add(absY.mul(texelSpacing)).sub(halfRoot);
|
|
720
|
-
return vec3(worldX, rootOrigin.y, worldZ);
|
|
721
|
-
}
|
|
722
|
-
);
|
|
676
|
+
const shared = {
|
|
677
|
+
tileLevel: (nodeIndex) => tileLevel(nodeIndex),
|
|
678
|
+
tileFace: (nodeIndex) => tileFace(nodeIndex),
|
|
679
|
+
tileOriginVec2: (nodeIndex) => tileOriginVec2(nodeIndex),
|
|
680
|
+
tileFaceUV: (nodeIndex, ix, iy) => tileFaceUV(nodeIndex, ix, iy)
|
|
681
|
+
};
|
|
682
|
+
const parts = projection.gpu.createTileComputeParts({ leafStorage, uniforms, shared });
|
|
723
683
|
return {
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
tileVertexWorldPositionCompute
|
|
684
|
+
...shared,
|
|
685
|
+
tileSize: parts.tileSize,
|
|
686
|
+
rootUVCompute: parts.rootUV,
|
|
687
|
+
tileVertexWorldPositionCompute: parts.tileVertexWorldPosition
|
|
729
688
|
};
|
|
730
689
|
}
|
|
731
|
-
function tileLocalToFieldUV
|
|
732
|
-
const edge = float(innerSegments).add(float(
|
|
733
|
-
return float(localCoord).mul(float(innerSegments)).add(float(
|
|
690
|
+
function tileLocalToFieldUV(localCoord, innerSegments) {
|
|
691
|
+
const edge = float(innerSegments).add(float(FIELD_EDGE_EXTRA_TEXELS));
|
|
692
|
+
return float(localCoord).mul(float(innerSegments)).add(float(FIELD_INNER_TEXEL_OFFSET)).div(edge);
|
|
693
|
+
}
|
|
694
|
+
function tileLocalToFieldUVNumber(localCoord, innerSegments) {
|
|
695
|
+
const edge = innerSegments + FIELD_EDGE_EXTRA_TEXELS;
|
|
696
|
+
return (localCoord * innerSegments + FIELD_INNER_TEXEL_OFFSET) / edge;
|
|
734
697
|
}
|
|
735
|
-
|
|
736
|
-
const rootSize = param(256).displayName("rootSize");
|
|
737
|
-
const origin = param({
|
|
738
|
-
x: 0,
|
|
739
|
-
y: 0,
|
|
740
|
-
z: 0
|
|
741
|
-
}).displayName("origin");
|
|
742
|
-
const innerTileSegments = param(13).displayName("innerTileSegments");
|
|
743
|
-
const skirtScale = param(100).displayName("skirtScale");
|
|
744
|
-
const elevationScale = param(1).displayName("elevationScale");
|
|
745
|
-
const maxNodes = param(1024).displayName("maxNodes");
|
|
746
|
-
const maxLevel = param(16).displayName("maxLevel");
|
|
747
|
-
const quadtreeUpdate = param({
|
|
748
|
-
cameraOrigin: { x: 0, y: 0, z: 0 },
|
|
749
|
-
mode: "distance",
|
|
750
|
-
distanceFactor: 1.5
|
|
751
|
-
}).displayName("quadtreeUpdate");
|
|
752
|
-
const surface = param(null).displayName("surface");
|
|
753
|
-
const terrainFieldFilter = param("linear").displayName("terrainFieldFilter");
|
|
754
|
-
const elevationFn = param(() => float(0));
|
|
755
698
|
|
|
756
699
|
function createLeafStorage(maxNodes) {
|
|
757
700
|
const data = new Int32Array(maxNodes * 4);
|
|
758
701
|
const attribute = new StorageBufferAttribute(data, 4);
|
|
702
|
+
attribute.name = "leafStorage";
|
|
759
703
|
const node = storage(attribute, "i32", 1).toReadOnly().setName("leafStorage");
|
|
760
704
|
return { data, attribute, node };
|
|
761
705
|
}
|
|
@@ -848,12 +792,12 @@ function ensureChildren(store, parentId) {
|
|
|
848
792
|
return childBase;
|
|
849
793
|
}
|
|
850
794
|
|
|
851
|
-
function nextPow2$
|
|
795
|
+
function nextPow2$2(n) {
|
|
852
796
|
let x = 1;
|
|
853
797
|
while (x < n) x <<= 1;
|
|
854
798
|
return x;
|
|
855
799
|
}
|
|
856
|
-
function mix32$
|
|
800
|
+
function mix32$2(x) {
|
|
857
801
|
x >>>= 0;
|
|
858
802
|
x ^= x >>> 16;
|
|
859
803
|
x = Math.imul(x, 2146121005) >>> 0;
|
|
@@ -862,12 +806,12 @@ function mix32$1(x) {
|
|
|
862
806
|
x ^= x >>> 16;
|
|
863
807
|
return x >>> 0;
|
|
864
808
|
}
|
|
865
|
-
function hashKey$
|
|
866
|
-
const h = space & 255 ^ (level & 255) << 8 ^ mix32$
|
|
867
|
-
return mix32$
|
|
809
|
+
function hashKey$2(space, level, x, y) {
|
|
810
|
+
const h = space & 255 ^ (level & 255) << 8 ^ mix32$2(x) >>> 0 ^ mix32$2(y) >>> 0;
|
|
811
|
+
return mix32$2(h);
|
|
868
812
|
}
|
|
869
813
|
function createSpatialIndex(maxEntries) {
|
|
870
|
-
const size = nextPow2$
|
|
814
|
+
const size = nextPow2$2(Math.max(2, maxEntries * 2));
|
|
871
815
|
return {
|
|
872
816
|
size,
|
|
873
817
|
mask: size - 1,
|
|
@@ -892,7 +836,7 @@ function insertSpatialIndexRaw(index, space, level, x, y, value) {
|
|
|
892
836
|
const l = level & 255;
|
|
893
837
|
const xx = x >>> 0;
|
|
894
838
|
const yy = y >>> 0;
|
|
895
|
-
let slot = hashKey$
|
|
839
|
+
let slot = hashKey$2(s, l, xx, yy) & index.mask;
|
|
896
840
|
for (let probes = 0; probes < index.size; probes++) {
|
|
897
841
|
if (index.stamp[slot] !== index.stampGen) {
|
|
898
842
|
index.stamp[slot] = index.stampGen;
|
|
@@ -916,7 +860,7 @@ function lookupSpatialIndexRaw(index, space, level, x, y) {
|
|
|
916
860
|
const l = level & 255;
|
|
917
861
|
const xx = x >>> 0;
|
|
918
862
|
const yy = y >>> 0;
|
|
919
|
-
let slot = hashKey$
|
|
863
|
+
let slot = hashKey$2(s, l, xx, yy) & index.mask;
|
|
920
864
|
for (let probes = 0; probes < index.size; probes++) {
|
|
921
865
|
if (index.stamp[slot] !== index.stampGen) return U32_EMPTY;
|
|
922
866
|
if (index.keysSpace[slot] === s && index.keysLevel[slot] === l && index.keysX[slot] === xx && index.keysY[slot] === yy) {
|
|
@@ -936,10 +880,10 @@ function buildLeafIndex(leaves, out) {
|
|
|
936
880
|
return index;
|
|
937
881
|
}
|
|
938
882
|
|
|
939
|
-
function createState(cfg,
|
|
940
|
-
const store = createNodeStore(cfg.maxNodes,
|
|
883
|
+
function createState(cfg, topology) {
|
|
884
|
+
const store = createNodeStore(cfg.maxNodes, topology.spaceCount);
|
|
941
885
|
const scratchRootTiles = [];
|
|
942
|
-
for (let i = 0; i <
|
|
886
|
+
for (let i = 0; i < topology.maxRootCount; i++) {
|
|
943
887
|
scratchRootTiles.push({ space: 0, level: 0, x: 0, y: 0 });
|
|
944
888
|
}
|
|
945
889
|
return {
|
|
@@ -949,7 +893,7 @@ function createState(cfg, surface) {
|
|
|
949
893
|
leafNodeIds: new Uint32Array(cfg.maxNodes),
|
|
950
894
|
leafIndex: createSpatialIndex(cfg.maxNodes),
|
|
951
895
|
stack: new Uint32Array(cfg.maxNodes),
|
|
952
|
-
rootNodeIds: new Uint32Array(
|
|
896
|
+
rootNodeIds: new Uint32Array(topology.maxRootCount),
|
|
953
897
|
rootCount: 0,
|
|
954
898
|
splitQueue: new Uint32Array(cfg.maxNodes),
|
|
955
899
|
splitStamp: new Uint16Array(cfg.maxNodes),
|
|
@@ -957,26 +901,27 @@ function createState(cfg, surface) {
|
|
|
957
901
|
scratchTile: { space: 0, level: 0, x: 0, y: 0 },
|
|
958
902
|
scratchNeighbor: { space: 0, level: 0, x: 0, y: 0 },
|
|
959
903
|
scratchBounds: { cx: 0, cy: 0, cz: 0, r: 0 },
|
|
904
|
+
scratchElevationRange: { min: 0, max: 0 },
|
|
960
905
|
scratchRootTiles,
|
|
961
|
-
spaceCount:
|
|
906
|
+
spaceCount: topology.spaceCount
|
|
962
907
|
};
|
|
963
908
|
}
|
|
964
|
-
function beginUpdate(state,
|
|
965
|
-
if (
|
|
909
|
+
function beginUpdate(state, topology, params) {
|
|
910
|
+
if (topology.spaceCount !== state.spaceCount) {
|
|
966
911
|
throw new Error(
|
|
967
|
-
`
|
|
912
|
+
`Topology spaceCount changed (${state.spaceCount} -> ${topology.spaceCount}). Create a new quadtree state.`
|
|
968
913
|
);
|
|
969
914
|
}
|
|
970
|
-
if (
|
|
915
|
+
if (topology.maxRootCount !== state.rootNodeIds.length) {
|
|
971
916
|
throw new Error(
|
|
972
|
-
`
|
|
917
|
+
`Topology maxRootCount changed (${state.rootNodeIds.length} -> ${topology.maxRootCount}). Create a new quadtree state.`
|
|
973
918
|
);
|
|
974
919
|
}
|
|
975
920
|
beginFrame(state.store);
|
|
976
921
|
state.rootCount = 0;
|
|
977
|
-
const rootCount =
|
|
978
|
-
if (rootCount < 0 || rootCount >
|
|
979
|
-
throw new Error(`
|
|
922
|
+
const rootCount = topology.rootTiles(params.cameraOrigin, state.scratchRootTiles);
|
|
923
|
+
if (rootCount < 0 || rootCount > topology.maxRootCount) {
|
|
924
|
+
throw new Error(`Topology returned invalid root count (${rootCount}).`);
|
|
980
925
|
}
|
|
981
926
|
for (let i = 0; i < rootCount; i++) {
|
|
982
927
|
const rootId = allocNode(state.store, state.scratchRootTiles[i]);
|
|
@@ -1013,7 +958,7 @@ function shouldSplit(bounds, level, maxLevel, params) {
|
|
|
1013
958
|
return safeDistSq < threshold * threshold;
|
|
1014
959
|
}
|
|
1015
960
|
|
|
1016
|
-
function refineLeaves(state,
|
|
961
|
+
function refineLeaves(state, topology, params, outLeaves) {
|
|
1017
962
|
const leaves = outLeaves ?? state.leaves;
|
|
1018
963
|
resetLeafSet(leaves);
|
|
1019
964
|
const store = state.store;
|
|
@@ -1034,7 +979,14 @@ function refineLeaves(state, surface, params, outLeaves) {
|
|
|
1034
979
|
tile.x = x;
|
|
1035
980
|
tile.y = y;
|
|
1036
981
|
const bounds = state.scratchBounds;
|
|
1037
|
-
|
|
982
|
+
let elevationRange;
|
|
983
|
+
if (params.tileElevationRange) {
|
|
984
|
+
const range = state.scratchElevationRange;
|
|
985
|
+
if (params.tileElevationRange(space, level, x, y, range)) {
|
|
986
|
+
elevationRange = range;
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
topology.tileBounds(tile, params.cameraOrigin, bounds, elevationRange);
|
|
1038
990
|
if (hasChildren(store, nodeId)) {
|
|
1039
991
|
const base = store.firstChild[nodeId];
|
|
1040
992
|
stack[sp++] = base + 3;
|
|
@@ -1082,7 +1034,7 @@ function scheduleSplit(state, nodeId, count) {
|
|
|
1082
1034
|
state.splitQueue[count] = nodeId;
|
|
1083
1035
|
return count + 1;
|
|
1084
1036
|
}
|
|
1085
|
-
function balance2to1(state,
|
|
1037
|
+
function balance2to1(state, topology, params, leaves) {
|
|
1086
1038
|
const maxIters = state.cfg.maxLevel + 1;
|
|
1087
1039
|
for (let iter = 0; iter < maxIters; iter++) {
|
|
1088
1040
|
const index = buildLeafIndex(leaves, state.leafIndex);
|
|
@@ -1103,7 +1055,7 @@ function balance2to1(state, surface, params, leaves) {
|
|
|
1103
1055
|
tile.x = leafX >>> shift;
|
|
1104
1056
|
tile.y = leafY >>> shift;
|
|
1105
1057
|
const neighbor = state.scratchNeighbor;
|
|
1106
|
-
if (!
|
|
1058
|
+
if (!topology.neighborSameLevel(tile, dir, neighbor)) break;
|
|
1107
1059
|
const j = lookupSpatialIndexRaw(
|
|
1108
1060
|
index,
|
|
1109
1061
|
neighbor.space,
|
|
@@ -1127,18 +1079,24 @@ function balance2to1(state, surface, params, leaves) {
|
|
|
1127
1079
|
if (base !== U32_EMPTY) anySplit = true;
|
|
1128
1080
|
}
|
|
1129
1081
|
if (!anySplit) return leaves;
|
|
1130
|
-
refineLeaves(state,
|
|
1082
|
+
refineLeaves(state, topology, params, leaves);
|
|
1131
1083
|
}
|
|
1132
1084
|
return leaves;
|
|
1133
1085
|
}
|
|
1134
1086
|
|
|
1135
|
-
function update(state,
|
|
1136
|
-
const
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
const
|
|
1140
|
-
const
|
|
1141
|
-
|
|
1087
|
+
function update(state, topology, params, outLeaves) {
|
|
1088
|
+
const cam = params.cameraOrigin;
|
|
1089
|
+
const elevation = params.elevationAtCameraXZ ?? 0;
|
|
1090
|
+
const origX = cam.x;
|
|
1091
|
+
const origY = cam.y;
|
|
1092
|
+
const origZ = cam.z;
|
|
1093
|
+
topology.projection.cpu.cameraSurfaceOffset(cam, elevation);
|
|
1094
|
+
beginUpdate(state, topology, params);
|
|
1095
|
+
const leaves = refineLeaves(state, topology, params, outLeaves);
|
|
1096
|
+
const result = balance2to1(state, topology, params, leaves);
|
|
1097
|
+
cam.x = origX;
|
|
1098
|
+
cam.y = origY;
|
|
1099
|
+
cam.z = origZ;
|
|
1142
1100
|
return result;
|
|
1143
1101
|
}
|
|
1144
1102
|
|
|
@@ -1146,7 +1104,7 @@ const scratchTile = { space: 0, level: 0, x: 0, y: 0 };
|
|
|
1146
1104
|
const scratchNbr = { space: 0, level: 0, x: 0, y: 0 };
|
|
1147
1105
|
const scratchParentTile = { space: 0, level: 0, x: 0, y: 0 };
|
|
1148
1106
|
const scratchParentNbr = { space: 0, level: 0, x: 0, y: 0 };
|
|
1149
|
-
function buildSeams2to1(
|
|
1107
|
+
function buildSeams2to1(topology, leaves, outSeams, outIndex) {
|
|
1150
1108
|
if (outSeams.capacity < leaves.count) {
|
|
1151
1109
|
throw new Error("SeamTable capacity is smaller than LeafSet.count.");
|
|
1152
1110
|
}
|
|
@@ -1167,7 +1125,7 @@ function buildSeams2to1(surface, leaves, outSeams, outIndex) {
|
|
|
1167
1125
|
scratchTile.level = level;
|
|
1168
1126
|
scratchTile.x = x;
|
|
1169
1127
|
scratchTile.y = y;
|
|
1170
|
-
if (!
|
|
1128
|
+
if (!topology.neighborSameLevel(scratchTile, dir, scratchNbr)) continue;
|
|
1171
1129
|
let j = lookupSpatialIndexRaw(index, scratchNbr.space, scratchNbr.level, scratchNbr.x, scratchNbr.y);
|
|
1172
1130
|
if (j !== U32_EMPTY) {
|
|
1173
1131
|
neighbors[outOffset + 0] = j;
|
|
@@ -1180,7 +1138,7 @@ function buildSeams2to1(surface, leaves, outSeams, outIndex) {
|
|
|
1180
1138
|
scratchParentTile.level = level - 1;
|
|
1181
1139
|
scratchParentTile.x = px;
|
|
1182
1140
|
scratchParentTile.y = py;
|
|
1183
|
-
if (
|
|
1141
|
+
if (topology.neighborSameLevel(scratchParentTile, dir, scratchParentNbr)) {
|
|
1184
1142
|
j = lookupSpatialIndexRaw(
|
|
1185
1143
|
index,
|
|
1186
1144
|
scratchParentNbr.space,
|
|
@@ -1236,157 +1194,2158 @@ function buildSeams2to1(surface, leaves, outSeams, outIndex) {
|
|
|
1236
1194
|
return outSeams;
|
|
1237
1195
|
}
|
|
1238
1196
|
|
|
1239
|
-
function
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
const
|
|
1247
|
-
const
|
|
1248
|
-
const
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
ny = y - 1;
|
|
1260
|
-
break;
|
|
1261
|
-
case Dir.BOTTOM:
|
|
1262
|
-
ny = y + 1;
|
|
1263
|
-
break;
|
|
1264
|
-
}
|
|
1265
|
-
if (nx < 0 || ny < 0) return false;
|
|
1266
|
-
const maxCoord = (1 << level) - 1;
|
|
1267
|
-
if (nx > maxCoord || ny > maxCoord) return false;
|
|
1268
|
-
out.space = 0;
|
|
1269
|
-
out.level = level;
|
|
1270
|
-
out.x = nx;
|
|
1271
|
-
out.y = ny;
|
|
1272
|
-
return true;
|
|
1273
|
-
},
|
|
1274
|
-
tileBounds(tile, cameraOrigin, out) {
|
|
1275
|
-
const level = tile.level;
|
|
1276
|
-
const scale = 1 / (1 << level);
|
|
1277
|
-
const size = cfg.rootSize * scale;
|
|
1278
|
-
const minX = cfg.origin.x + (tile.x * size - halfRoot);
|
|
1279
|
-
const minZ = cfg.origin.z + (tile.y * size - halfRoot);
|
|
1280
|
-
const centerX = minX + 0.5 * size;
|
|
1281
|
-
const centerY = cfg.origin.y;
|
|
1282
|
-
const centerZ = minZ + 0.5 * size;
|
|
1283
|
-
out.cx = centerX - cameraOrigin.x;
|
|
1284
|
-
out.cy = centerY - cameraOrigin.y;
|
|
1285
|
-
out.cz = centerZ - cameraOrigin.z;
|
|
1286
|
-
out.r = 0.7071067811865476 * size + maxHeight;
|
|
1287
|
-
},
|
|
1288
|
-
rootTiles(_cameraOrigin, out) {
|
|
1289
|
-
const root = out[0];
|
|
1290
|
-
root.space = 0;
|
|
1291
|
-
root.level = 0;
|
|
1292
|
-
root.x = 0;
|
|
1293
|
-
root.y = 0;
|
|
1294
|
-
return 1;
|
|
1197
|
+
function createFlatNormalFromElevationField(elevationFieldNode, edgeVertexCount) {
|
|
1198
|
+
return Fn(
|
|
1199
|
+
([nodeIndex, tileSize, ix, iy, elevationScale]) => {
|
|
1200
|
+
const iEdge = int(edgeVertexCount);
|
|
1201
|
+
const verticesPerNode = iEdge.mul(iEdge);
|
|
1202
|
+
const baseOffset = int(nodeIndex).mul(verticesPerNode);
|
|
1203
|
+
const xLeft = int(ix).sub(int(1));
|
|
1204
|
+
const xRight = int(ix).add(int(1));
|
|
1205
|
+
const yUp = int(iy).sub(int(1));
|
|
1206
|
+
const yDown = int(iy).add(int(1));
|
|
1207
|
+
const hLeft = elevationFieldNode.element(baseOffset.add(int(iy).mul(iEdge).add(xLeft))).mul(elevationScale);
|
|
1208
|
+
const hRight = elevationFieldNode.element(baseOffset.add(int(iy).mul(iEdge).add(xRight))).mul(elevationScale);
|
|
1209
|
+
const hUp = elevationFieldNode.element(baseOffset.add(yUp.mul(iEdge).add(int(ix)))).mul(elevationScale);
|
|
1210
|
+
const hDown = elevationFieldNode.element(baseOffset.add(yDown.mul(iEdge).add(int(ix)))).mul(elevationScale);
|
|
1211
|
+
const innerSegments = float(iEdge).sub(float(3));
|
|
1212
|
+
const stepWorld = tileSize.div(innerSegments);
|
|
1213
|
+
const inv2Step = float(0.5).div(stepWorld);
|
|
1214
|
+
const dhdx = float(hRight).sub(float(hLeft)).mul(inv2Step);
|
|
1215
|
+
const dhdz = float(hDown).sub(float(hUp)).mul(inv2Step);
|
|
1216
|
+
return vec3(dhdx.negate(), float(1), dhdz.negate()).normalize();
|
|
1295
1217
|
}
|
|
1296
|
-
|
|
1297
|
-
return surface;
|
|
1218
|
+
);
|
|
1298
1219
|
}
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
break;
|
|
1321
|
-
case Dir.BOTTOM:
|
|
1322
|
-
ny = tile.y + 1;
|
|
1323
|
-
break;
|
|
1324
|
-
}
|
|
1325
|
-
out.space = tile.space;
|
|
1326
|
-
out.level = tile.level;
|
|
1327
|
-
out.x = nx;
|
|
1328
|
-
out.y = ny;
|
|
1329
|
-
return true;
|
|
1330
|
-
},
|
|
1331
|
-
tileBounds(tile, cameraOrigin, out) {
|
|
1332
|
-
const level = tile.level;
|
|
1333
|
-
const scale = 1 / (1 << level);
|
|
1334
|
-
const size = cfg.rootSize * scale;
|
|
1335
|
-
const minX = cfg.origin.x + (tile.x * size - halfRoot);
|
|
1336
|
-
const minZ = cfg.origin.z + (tile.y * size - halfRoot);
|
|
1337
|
-
const centerX = minX + 0.5 * size;
|
|
1338
|
-
const centerY = cfg.origin.y;
|
|
1339
|
-
const centerZ = minZ + 0.5 * size;
|
|
1340
|
-
out.cx = centerX - cameraOrigin.x;
|
|
1341
|
-
out.cy = centerY - cameraOrigin.y;
|
|
1342
|
-
out.cz = centerZ - cameraOrigin.z;
|
|
1343
|
-
out.r = 0.7071067811865476 * size + maxHeight;
|
|
1344
|
-
},
|
|
1345
|
-
rootTiles(cameraOrigin, out) {
|
|
1346
|
-
const camRootX = Math.floor((cameraOrigin.x - cfg.origin.x + halfRoot) / cfg.rootSize);
|
|
1347
|
-
const camRootY = Math.floor((cameraOrigin.z - cfg.origin.z + halfRoot) / cfg.rootSize);
|
|
1348
|
-
let index = 0;
|
|
1349
|
-
for (let dy = -rootGridRadius; dy <= rootGridRadius; dy++) {
|
|
1350
|
-
for (let dx = -rootGridRadius; dx <= rootGridRadius; dx++) {
|
|
1351
|
-
const root = out[index];
|
|
1352
|
-
root.space = 0;
|
|
1353
|
-
root.level = 0;
|
|
1354
|
-
root.x = camRootX + dx;
|
|
1355
|
-
root.y = camRootY + dy;
|
|
1356
|
-
index++;
|
|
1357
|
-
}
|
|
1358
|
-
}
|
|
1359
|
-
return index;
|
|
1360
|
-
}
|
|
1361
|
-
};
|
|
1220
|
+
function createDisplacedSurfaceNormalFromElevationField(elevationFieldNode, edgeVertexCount, makeSurfaceFns) {
|
|
1221
|
+
return Fn(([nodeIndex, ix, iy, elevationScale]) => {
|
|
1222
|
+
const iEdge = int(edgeVertexCount);
|
|
1223
|
+
const verticesPerNode = iEdge.mul(iEdge);
|
|
1224
|
+
const baseOffset = int(nodeIndex).mul(verticesPerNode);
|
|
1225
|
+
const xLeft = int(ix).sub(int(1));
|
|
1226
|
+
const xRight = int(ix).add(int(1));
|
|
1227
|
+
const yUp = int(iy).sub(int(1));
|
|
1228
|
+
const yDown = int(iy).add(int(1));
|
|
1229
|
+
const heightAt = (gx, gy) => elevationFieldNode.element(baseOffset.add(gy.mul(iEdge).add(gx))).mul(elevationScale);
|
|
1230
|
+
const { positionAt, dirAt } = makeSurfaceFns(nodeIndex);
|
|
1231
|
+
const pLeft = positionAt(xLeft, int(iy), heightAt(xLeft, int(iy)));
|
|
1232
|
+
const pRight = positionAt(xRight, int(iy), heightAt(xRight, int(iy)));
|
|
1233
|
+
const pUp = positionAt(int(ix), yUp, heightAt(int(ix), yUp));
|
|
1234
|
+
const pDown = positionAt(int(ix), yDown, heightAt(int(ix), yDown));
|
|
1235
|
+
const tangentU = pRight.sub(pLeft);
|
|
1236
|
+
const tangentV = pDown.sub(pUp);
|
|
1237
|
+
const normal = cross(tangentU, tangentV).normalize();
|
|
1238
|
+
const dir = dirAt(int(ix), int(iy));
|
|
1239
|
+
return normal.mul(normal.dot(dir).sign());
|
|
1240
|
+
});
|
|
1362
1241
|
}
|
|
1363
1242
|
|
|
1364
|
-
|
|
1243
|
+
const isSkirtVertex = Fn(([segments]) => {
|
|
1244
|
+
const segmentsNode = typeof segments === "number" ? int(segments) : segments;
|
|
1245
|
+
const vIndex = int(vertexIndex);
|
|
1246
|
+
const segmentEdges = int(segmentsNode.add(3));
|
|
1247
|
+
const vx = vIndex.mod(segmentEdges);
|
|
1248
|
+
const vy = vIndex.div(segmentEdges);
|
|
1249
|
+
const last = segmentEdges.sub(int(1));
|
|
1250
|
+
return vx.equal(int(0)).or(vx.equal(last)).or(vy.equal(int(0))).or(vy.equal(last));
|
|
1251
|
+
});
|
|
1252
|
+
const isSkirtUV = Fn(([segments]) => {
|
|
1253
|
+
const segmentsNode = typeof segments === "number" ? int(segments) : segments;
|
|
1254
|
+
const ux = uv().x;
|
|
1255
|
+
const uy = uv().y;
|
|
1256
|
+
const segmentCount = segmentsNode.add(2);
|
|
1257
|
+
const segmentStep = float(1).div(segmentCount);
|
|
1258
|
+
const innerX = ux.greaterThan(segmentStep).and(ux.lessThan(segmentStep.oneMinus()));
|
|
1259
|
+
const innerY = uy.greaterThan(segmentStep).and(uy.lessThan(segmentStep.oneMinus()));
|
|
1260
|
+
return innerX.and(innerY).not();
|
|
1261
|
+
});
|
|
1262
|
+
|
|
1263
|
+
function createTileElevation(terrainUniforms, terrainFieldStorage) {
|
|
1264
|
+
if (!terrainFieldStorage) return float(0);
|
|
1265
|
+
const innerSegs = terrainUniforms.uInnerTileSegments;
|
|
1266
|
+
const u = tileLocalToFieldUV(positionLocal.x.add(float(0.5)), innerSegs);
|
|
1267
|
+
const v = tileLocalToFieldUV(positionLocal.z.add(float(0.5)), innerSegs);
|
|
1268
|
+
return sampleTerrainFieldElevation(terrainFieldStorage, u, v, int(instanceIndex)).mul(
|
|
1269
|
+
terrainUniforms.uElevationScale
|
|
1270
|
+
);
|
|
1271
|
+
}
|
|
1272
|
+
function loadWorldNormal(terrainUniforms, terrainFieldStorage) {
|
|
1273
|
+
const nodeIndex = int(instanceIndex);
|
|
1274
|
+
const edgeVertexCount = int(terrainUniforms.uInnerTileSegments.add(3));
|
|
1275
|
+
const localVertexIndex = int(vertexIndex);
|
|
1276
|
+
const ix = localVertexIndex.mod(edgeVertexCount);
|
|
1277
|
+
const iy = localVertexIndex.div(edgeVertexCount);
|
|
1278
|
+
return loadTerrainFieldNormal(terrainFieldStorage, ix, iy, nodeIndex);
|
|
1279
|
+
}
|
|
1280
|
+
function assignWorldNormal(terrainUniforms, terrainFieldStorage) {
|
|
1281
|
+
if (!terrainFieldStorage) return;
|
|
1282
|
+
normalLocal.assign(Fn(() => loadWorldNormal(terrainUniforms, terrainFieldStorage))());
|
|
1283
|
+
}
|
|
1284
|
+
function createFlatRenderVertexPosition(leafStorage, terrainUniforms, terrainFieldStorage) {
|
|
1285
|
+
return Fn(() => {
|
|
1286
|
+
const tile = decodeLeafTile(leafStorage, int(instanceIndex));
|
|
1287
|
+
const rootSize = terrainUniforms.uRootSize.toVar();
|
|
1288
|
+
const rootOrigin = terrainUniforms.uRootOrigin.toVar();
|
|
1289
|
+
const half = float(0.5);
|
|
1290
|
+
const size = rootSize.div(pow(float(2), tile.level.toFloat()));
|
|
1291
|
+
const halfRoot = rootSize.mul(half);
|
|
1292
|
+
const centerX = rootOrigin.x.add(tile.x.add(half).mul(size)).sub(halfRoot);
|
|
1293
|
+
const centerZ = rootOrigin.z.add(tile.y.add(half).mul(size)).sub(halfRoot);
|
|
1294
|
+
const clampedX = positionLocal.x.max(half.negate()).min(half);
|
|
1295
|
+
const clampedZ = positionLocal.z.max(half.negate()).min(half);
|
|
1296
|
+
const worldX = centerX.add(clampedX.mul(size));
|
|
1297
|
+
const worldZ = centerZ.add(clampedZ.mul(size));
|
|
1298
|
+
const yElevation = createTileElevation(terrainUniforms, terrainFieldStorage);
|
|
1299
|
+
const skirtVertex = isSkirtVertex(terrainUniforms.uInnerTileSegments);
|
|
1300
|
+
const baseY = rootOrigin.y.add(yElevation);
|
|
1301
|
+
const skirtY = baseY.sub(terrainUniforms.uSkirtScale.toVar());
|
|
1302
|
+
const worldY = select(skirtVertex, skirtY, baseY);
|
|
1303
|
+
assignWorldNormal(terrainUniforms, terrainFieldStorage);
|
|
1304
|
+
return vec3(worldX, worldY, worldZ);
|
|
1305
|
+
})();
|
|
1306
|
+
}
|
|
1307
|
+
function createCurvedRenderVertexPosition(leafStorage, terrainUniforms, terrainFieldStorage, surfacePoint, baseU = 1, baseV = 1) {
|
|
1308
|
+
const fBaseU = float(baseU);
|
|
1309
|
+
const fBaseV = float(baseV);
|
|
1310
|
+
return Fn(() => {
|
|
1311
|
+
const tile = decodeLeafTile(leafStorage, int(instanceIndex));
|
|
1312
|
+
const half = float(0.5);
|
|
1313
|
+
const localU = positionLocal.x.max(half.negate()).min(half).add(half);
|
|
1314
|
+
const localV = positionLocal.z.max(half.negate()).min(half).add(half);
|
|
1315
|
+
const faceUV = faceUVFromTileLocal(tile, localU, localV, fBaseU, fBaseV);
|
|
1316
|
+
const yElevation = createTileElevation(terrainUniforms, terrainFieldStorage);
|
|
1317
|
+
const skirtVertex = isSkirtVertex(terrainUniforms.uInnerTileSegments);
|
|
1318
|
+
const displacement = select(
|
|
1319
|
+
skirtVertex,
|
|
1320
|
+
yElevation.sub(terrainUniforms.uSkirtScale.toVar()),
|
|
1321
|
+
yElevation
|
|
1322
|
+
);
|
|
1323
|
+
assignWorldNormal(terrainUniforms, terrainFieldStorage);
|
|
1324
|
+
return surfacePoint(tile, faceUV, displacement);
|
|
1325
|
+
})();
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
const TWO_PI$2 = Math.PI * 2;
|
|
1329
|
+
function wrap01(t) {
|
|
1330
|
+
const w = t - Math.floor(t);
|
|
1331
|
+
return w >= 1 ? w - 1 : w;
|
|
1332
|
+
}
|
|
1333
|
+
function torusUVToPoint(u, v, majorRadius, minorRadius, displacement, center, out, invert = false) {
|
|
1334
|
+
const theta = TWO_PI$2 * u;
|
|
1335
|
+
const phi = TWO_PI$2 * v;
|
|
1336
|
+
const sinT = Math.sin(theta);
|
|
1337
|
+
const cosT = Math.cos(theta);
|
|
1338
|
+
const sinP = Math.sin(phi);
|
|
1339
|
+
const cosP = Math.cos(phi);
|
|
1340
|
+
const disp = invert ? -displacement : displacement;
|
|
1341
|
+
const tube = minorRadius + disp;
|
|
1342
|
+
const ring = majorRadius + tube * cosP;
|
|
1343
|
+
out[0] = center.x + ring * sinT;
|
|
1344
|
+
out[1] = center.y + tube * sinP;
|
|
1345
|
+
out[2] = center.z + ring * cosT;
|
|
1346
|
+
}
|
|
1347
|
+
function torusOutwardNormal$1(u, v, out, invert = false) {
|
|
1348
|
+
const theta = TWO_PI$2 * u;
|
|
1349
|
+
const phi = TWO_PI$2 * v;
|
|
1350
|
+
const sinT = Math.sin(theta);
|
|
1351
|
+
const cosT = Math.cos(theta);
|
|
1352
|
+
const sinP = Math.sin(phi);
|
|
1353
|
+
const cosP = Math.cos(phi);
|
|
1354
|
+
const s = invert ? -1 : 1;
|
|
1355
|
+
out[0] = cosP * sinT * s;
|
|
1356
|
+
out[1] = sinP * s;
|
|
1357
|
+
out[2] = cosP * cosT * s;
|
|
1358
|
+
}
|
|
1359
|
+
function positionToTorusParams(px, py, pz, majorRadius, center, out) {
|
|
1360
|
+
const qx = px - center.x;
|
|
1361
|
+
const qy = py - center.y;
|
|
1362
|
+
const qz = pz - center.z;
|
|
1363
|
+
const theta = Math.atan2(qx, qz);
|
|
1364
|
+
const rho = Math.hypot(qx, qz);
|
|
1365
|
+
const a = rho - majorRadius;
|
|
1366
|
+
const phi = Math.atan2(qy, a);
|
|
1367
|
+
out.u = wrap01(theta / TWO_PI$2);
|
|
1368
|
+
out.v = wrap01(phi / TWO_PI$2);
|
|
1369
|
+
out.tubeDistance = Math.hypot(a, qy);
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
function intersectRayAabb(ray, minX, minY, minZ, maxX, maxY, maxZ) {
|
|
1373
|
+
let tMin = -Infinity;
|
|
1374
|
+
let tMax = Infinity;
|
|
1375
|
+
const origin = ray.origin;
|
|
1376
|
+
const dir = ray.direction;
|
|
1377
|
+
const slab = (originAxis, dirAxis, minAxis, maxAxis) => {
|
|
1378
|
+
if (Math.abs(dirAxis) < 1e-8) {
|
|
1379
|
+
if (originAxis < minAxis || originAxis > maxAxis) return false;
|
|
1380
|
+
return true;
|
|
1381
|
+
}
|
|
1382
|
+
const inv = 1 / dirAxis;
|
|
1383
|
+
let t0 = (minAxis - originAxis) * inv;
|
|
1384
|
+
let t1 = (maxAxis - originAxis) * inv;
|
|
1385
|
+
if (t0 > t1) {
|
|
1386
|
+
const tmp = t0;
|
|
1387
|
+
t0 = t1;
|
|
1388
|
+
t1 = tmp;
|
|
1389
|
+
}
|
|
1390
|
+
tMin = Math.max(tMin, t0);
|
|
1391
|
+
tMax = Math.min(tMax, t1);
|
|
1392
|
+
return tMax >= tMin;
|
|
1393
|
+
};
|
|
1394
|
+
if (!slab(origin.x, dir.x, minX, maxX) || !slab(origin.y, dir.y, minY, maxY) || !slab(origin.z, dir.z, minZ, maxZ)) {
|
|
1395
|
+
return null;
|
|
1396
|
+
}
|
|
1397
|
+
return { tMin, tMax };
|
|
1398
|
+
}
|
|
1399
|
+
function getTerrainBounds(config) {
|
|
1400
|
+
const halfRoot = config.rootSize * 0.5;
|
|
1401
|
+
return {
|
|
1402
|
+
minX: config.originX - halfRoot,
|
|
1403
|
+
maxX: config.originX + halfRoot,
|
|
1404
|
+
minZ: config.originZ - halfRoot,
|
|
1405
|
+
maxZ: config.originZ + halfRoot
|
|
1406
|
+
};
|
|
1407
|
+
}
|
|
1408
|
+
function terrainSignedDistance(query, worldX, worldY, worldZ, skipBoundsFastPath) {
|
|
1409
|
+
if (!skipBoundsFastPath) {
|
|
1410
|
+
const tileBounds = query.getTileBounds(worldX, worldZ);
|
|
1411
|
+
if (tileBounds) {
|
|
1412
|
+
if (worldY > tileBounds.maxElevation) {
|
|
1413
|
+
return worldY - tileBounds.maxElevation;
|
|
1414
|
+
}
|
|
1415
|
+
if (worldY < tileBounds.minElevation) {
|
|
1416
|
+
return worldY - tileBounds.minElevation;
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
const elevation = query.getElevation(worldX, worldZ);
|
|
1421
|
+
if (!Number.isFinite(elevation)) return void 0;
|
|
1422
|
+
return worldY - elevation;
|
|
1423
|
+
}
|
|
1424
|
+
function marchSignedDistance(ray, startT, endT, stepSignedDistanceAt, refineSignedDistanceAt, options, point) {
|
|
1425
|
+
let prevT = startT;
|
|
1426
|
+
ray.at(prevT, point);
|
|
1427
|
+
let prevSignedDistance = stepSignedDistanceAt(point.x, point.y, point.z);
|
|
1428
|
+
if (prevSignedDistance !== void 0 && prevSignedDistance <= 0) {
|
|
1429
|
+
return startT;
|
|
1430
|
+
}
|
|
1431
|
+
for (let i = 1; i <= options.maxSteps; i += 1) {
|
|
1432
|
+
const t = startT + (endT - startT) * i / options.maxSteps;
|
|
1433
|
+
ray.at(t, point);
|
|
1434
|
+
const signedDistance = stepSignedDistanceAt(point.x, point.y, point.z);
|
|
1435
|
+
if (signedDistance === void 0) {
|
|
1436
|
+
prevSignedDistance = void 0;
|
|
1437
|
+
prevT = t;
|
|
1438
|
+
continue;
|
|
1439
|
+
}
|
|
1440
|
+
if (prevSignedDistance !== void 0 && prevSignedDistance > 0 && signedDistance <= 0) {
|
|
1441
|
+
let lo = prevT;
|
|
1442
|
+
let hi = t;
|
|
1443
|
+
for (let r = 0; r < options.refinementSteps; r += 1) {
|
|
1444
|
+
const mid = (lo + hi) * 0.5;
|
|
1445
|
+
ray.at(mid, point);
|
|
1446
|
+
const midDistance = refineSignedDistanceAt(point.x, point.y, point.z);
|
|
1447
|
+
if (midDistance === void 0) {
|
|
1448
|
+
lo = mid;
|
|
1449
|
+
continue;
|
|
1450
|
+
}
|
|
1451
|
+
if (midDistance > 0) lo = mid;
|
|
1452
|
+
else hi = mid;
|
|
1453
|
+
}
|
|
1454
|
+
return hi;
|
|
1455
|
+
}
|
|
1456
|
+
prevSignedDistance = signedDistance;
|
|
1457
|
+
prevT = t;
|
|
1458
|
+
}
|
|
1459
|
+
return null;
|
|
1460
|
+
}
|
|
1461
|
+
function cpuRaycast(query, ray, config, options) {
|
|
1462
|
+
const bounds = getTerrainBounds(config);
|
|
1463
|
+
const segment = intersectRayAabb(
|
|
1464
|
+
ray,
|
|
1465
|
+
bounds.minX,
|
|
1466
|
+
config.minY,
|
|
1467
|
+
bounds.minZ,
|
|
1468
|
+
bounds.maxX,
|
|
1469
|
+
config.maxY,
|
|
1470
|
+
bounds.maxZ
|
|
1471
|
+
);
|
|
1472
|
+
if (!segment) return null;
|
|
1473
|
+
const maxDistance = options?.maxDistance ?? Number.POSITIVE_INFINITY;
|
|
1474
|
+
const startT = Math.max(0, segment.tMin);
|
|
1475
|
+
const endT = Math.min(segment.tMax, maxDistance);
|
|
1476
|
+
if (endT < startT) return null;
|
|
1477
|
+
const point = new Vector3();
|
|
1478
|
+
const hitT = marchSignedDistance(
|
|
1479
|
+
ray,
|
|
1480
|
+
startT,
|
|
1481
|
+
endT,
|
|
1482
|
+
(px, py, pz) => terrainSignedDistance(query, px, py, pz, false),
|
|
1483
|
+
(px, py, pz) => terrainSignedDistance(query, px, py, pz, true),
|
|
1484
|
+
{
|
|
1485
|
+
maxSteps: Math.max(8, options?.maxSteps ?? 128),
|
|
1486
|
+
refinementSteps: Math.max(1, options?.refinementSteps ?? 8)
|
|
1487
|
+
},
|
|
1488
|
+
point
|
|
1489
|
+
);
|
|
1490
|
+
if (hitT === null) return null;
|
|
1491
|
+
ray.at(hitT, point);
|
|
1492
|
+
const sample = query.sampleTerrain(point.x, point.z);
|
|
1493
|
+
if (!sample.valid) return null;
|
|
1494
|
+
point.y = sample.elevation;
|
|
1495
|
+
return {
|
|
1496
|
+
position: point.clone(),
|
|
1497
|
+
normal: sample.normal.clone(),
|
|
1498
|
+
distance: ray.origin.distanceTo(point)
|
|
1499
|
+
};
|
|
1500
|
+
}
|
|
1501
|
+
function cpuRaycastBoundsOnly(ray, config, options) {
|
|
1502
|
+
const bounds = getTerrainBounds(config);
|
|
1503
|
+
const planeY = (config.minY + config.maxY) * 0.5;
|
|
1504
|
+
const dirY = ray.direction.y;
|
|
1505
|
+
if (Math.abs(dirY) < 1e-8) return null;
|
|
1506
|
+
const t = (planeY - ray.origin.y) / dirY;
|
|
1507
|
+
if (t < 0) return null;
|
|
1508
|
+
const maxDistance = options?.maxDistance ?? Number.POSITIVE_INFINITY;
|
|
1509
|
+
if (t > maxDistance) return null;
|
|
1510
|
+
const point = new Vector3();
|
|
1511
|
+
ray.at(t, point);
|
|
1512
|
+
if (point.x < bounds.minX || point.x > bounds.maxX || point.z < bounds.minZ || point.z > bounds.maxZ) {
|
|
1513
|
+
return null;
|
|
1514
|
+
}
|
|
1515
|
+
return {
|
|
1516
|
+
position: point,
|
|
1517
|
+
normal: new Vector3(0, 1, 0),
|
|
1518
|
+
distance: ray.origin.distanceTo(point)
|
|
1519
|
+
};
|
|
1520
|
+
}
|
|
1521
|
+
function intersectRaySphere(ray, cx, cy, cz, radius) {
|
|
1522
|
+
const ox = ray.origin.x - cx;
|
|
1523
|
+
const oy = ray.origin.y - cy;
|
|
1524
|
+
const oz = ray.origin.z - cz;
|
|
1525
|
+
const dx = ray.direction.x;
|
|
1526
|
+
const dy = ray.direction.y;
|
|
1527
|
+
const dz = ray.direction.z;
|
|
1528
|
+
const a = dx * dx + dy * dy + dz * dz;
|
|
1529
|
+
const b = 2 * (ox * dx + oy * dy + oz * dz);
|
|
1530
|
+
const c = ox * ox + oy * oy + oz * oz - radius * radius;
|
|
1531
|
+
const disc = b * b - 4 * a * c;
|
|
1532
|
+
if (disc < 0) return null;
|
|
1533
|
+
const sqrtDisc = Math.sqrt(disc);
|
|
1534
|
+
const inv2a = 1 / (2 * a);
|
|
1535
|
+
return { t0: (-b - sqrtDisc) * inv2a, t1: (-b + sqrtDisc) * inv2a };
|
|
1536
|
+
}
|
|
1537
|
+
function sphereSignedDistance(query, params, px, py, pz, scratchDir) {
|
|
1538
|
+
const dx = px - params.centerX;
|
|
1539
|
+
const dy = py - params.centerY;
|
|
1540
|
+
const dz = pz - params.centerZ;
|
|
1541
|
+
const dist = Math.hypot(dx, dy, dz);
|
|
1542
|
+
scratchDir.set(dx, dy, dz);
|
|
1543
|
+
const elevation = query.getElevationByDirection(scratchDir);
|
|
1544
|
+
if (elevation === null) return void 0;
|
|
1545
|
+
const s = params.invert ? -1 : 1;
|
|
1546
|
+
return s * (dist - (params.radius + s * elevation));
|
|
1547
|
+
}
|
|
1548
|
+
function cubeSphereRaycast(query, ray, params, options) {
|
|
1549
|
+
const shell = intersectRaySphere(
|
|
1550
|
+
ray,
|
|
1551
|
+
params.centerX,
|
|
1552
|
+
params.centerY,
|
|
1553
|
+
params.centerZ,
|
|
1554
|
+
params.maxRadius
|
|
1555
|
+
);
|
|
1556
|
+
if (!shell) return null;
|
|
1557
|
+
const maxDistance = options?.maxDistance ?? Number.POSITIVE_INFINITY;
|
|
1558
|
+
const startT = Math.max(0, shell.t0);
|
|
1559
|
+
const endT = Math.min(shell.t1, maxDistance);
|
|
1560
|
+
if (endT < startT) return null;
|
|
1561
|
+
const scratchDir = new Vector3();
|
|
1562
|
+
const point = new Vector3();
|
|
1563
|
+
const signedDistanceAt = (px, py, pz) => sphereSignedDistance(query, params, px, py, pz, scratchDir);
|
|
1564
|
+
const hitT = marchSignedDistance(
|
|
1565
|
+
ray,
|
|
1566
|
+
startT,
|
|
1567
|
+
endT,
|
|
1568
|
+
signedDistanceAt,
|
|
1569
|
+
signedDistanceAt,
|
|
1570
|
+
{
|
|
1571
|
+
maxSteps: Math.max(8, options?.maxSteps ?? 256),
|
|
1572
|
+
refinementSteps: Math.max(1, options?.refinementSteps ?? 12)
|
|
1573
|
+
},
|
|
1574
|
+
point
|
|
1575
|
+
);
|
|
1576
|
+
if (hitT === null) return null;
|
|
1577
|
+
ray.at(hitT, point);
|
|
1578
|
+
const sample = query.sampleTerrainByPosition(point);
|
|
1579
|
+
if (!sample.valid) return null;
|
|
1580
|
+
return {
|
|
1581
|
+
position: sample.position.clone(),
|
|
1582
|
+
normal: sample.normal.clone(),
|
|
1583
|
+
distance: ray.origin.distanceTo(sample.position)
|
|
1584
|
+
};
|
|
1585
|
+
}
|
|
1586
|
+
function cubeSphereRaycastBoundsOnly(ray, params, options) {
|
|
1587
|
+
const shell = intersectRaySphere(ray, params.centerX, params.centerY, params.centerZ, params.radius);
|
|
1588
|
+
if (!shell) return null;
|
|
1589
|
+
const maxDistance = options?.maxDistance ?? Number.POSITIVE_INFINITY;
|
|
1590
|
+
const t = shell.t0 >= 0 ? shell.t0 : shell.t1;
|
|
1591
|
+
if (t < 0 || t > maxDistance) return null;
|
|
1592
|
+
const point = new Vector3();
|
|
1593
|
+
ray.at(t, point);
|
|
1594
|
+
const normal = new Vector3(
|
|
1595
|
+
point.x - params.centerX,
|
|
1596
|
+
point.y - params.centerY,
|
|
1597
|
+
point.z - params.centerZ
|
|
1598
|
+
).normalize();
|
|
1599
|
+
if (params.invert) normal.negate();
|
|
1600
|
+
return { position: point, normal, distance: ray.origin.distanceTo(point) };
|
|
1601
|
+
}
|
|
1602
|
+
function torusSignedDistance(query, params, px, py, pz, scratchPoint, scratchParams) {
|
|
1603
|
+
positionToTorusParams(
|
|
1604
|
+
px,
|
|
1605
|
+
py,
|
|
1606
|
+
pz,
|
|
1607
|
+
params.majorRadius,
|
|
1608
|
+
{ x: params.centerX, y: params.centerY, z: params.centerZ },
|
|
1609
|
+
scratchParams
|
|
1610
|
+
);
|
|
1611
|
+
scratchPoint.set(px, py, pz);
|
|
1612
|
+
const elevation = query.getElevationByPosition(scratchPoint);
|
|
1613
|
+
if (elevation === null) return void 0;
|
|
1614
|
+
const s = params.invert ? -1 : 1;
|
|
1615
|
+
return s * (scratchParams.tubeDistance - (params.minorRadius + s * elevation));
|
|
1616
|
+
}
|
|
1617
|
+
function torusRaycast(query, ray, params, options) {
|
|
1618
|
+
const shell = intersectRaySphere(
|
|
1619
|
+
ray,
|
|
1620
|
+
params.centerX,
|
|
1621
|
+
params.centerY,
|
|
1622
|
+
params.centerZ,
|
|
1623
|
+
params.outerRadius
|
|
1624
|
+
);
|
|
1625
|
+
if (!shell) return null;
|
|
1626
|
+
const maxDistance = options?.maxDistance ?? Number.POSITIVE_INFINITY;
|
|
1627
|
+
const startT = Math.max(0, shell.t0);
|
|
1628
|
+
const endT = Math.min(shell.t1, maxDistance);
|
|
1629
|
+
if (endT < startT) return null;
|
|
1630
|
+
const scratchPoint = new Vector3();
|
|
1631
|
+
const scratchParams = { u: 0, v: 0, tubeDistance: 0 };
|
|
1632
|
+
const point = new Vector3();
|
|
1633
|
+
const signedDistanceAt = (px, py, pz) => torusSignedDistance(query, params, px, py, pz, scratchPoint, scratchParams);
|
|
1634
|
+
const hitT = marchSignedDistance(
|
|
1635
|
+
ray,
|
|
1636
|
+
startT,
|
|
1637
|
+
endT,
|
|
1638
|
+
signedDistanceAt,
|
|
1639
|
+
signedDistanceAt,
|
|
1640
|
+
{
|
|
1641
|
+
maxSteps: Math.max(8, options?.maxSteps ?? 256),
|
|
1642
|
+
refinementSteps: Math.max(1, options?.refinementSteps ?? 12)
|
|
1643
|
+
},
|
|
1644
|
+
point
|
|
1645
|
+
);
|
|
1646
|
+
if (hitT === null) return null;
|
|
1647
|
+
ray.at(hitT, point);
|
|
1648
|
+
const sample = query.sampleTerrainByPosition(point);
|
|
1649
|
+
if (!sample.valid) return null;
|
|
1650
|
+
return {
|
|
1651
|
+
position: sample.position.clone(),
|
|
1652
|
+
normal: sample.normal.clone(),
|
|
1653
|
+
distance: ray.origin.distanceTo(sample.position)
|
|
1654
|
+
};
|
|
1655
|
+
}
|
|
1656
|
+
function torusRaycastBoundsOnly(ray, params, options) {
|
|
1657
|
+
const shell = intersectRaySphere(
|
|
1658
|
+
ray,
|
|
1659
|
+
params.centerX,
|
|
1660
|
+
params.centerY,
|
|
1661
|
+
params.centerZ,
|
|
1662
|
+
params.outerRadius
|
|
1663
|
+
);
|
|
1664
|
+
if (!shell) return null;
|
|
1665
|
+
const maxDistance = options?.maxDistance ?? Number.POSITIVE_INFINITY;
|
|
1666
|
+
const t = shell.t0 >= 0 ? shell.t0 : shell.t1;
|
|
1667
|
+
if (t < 0 || t > maxDistance) return null;
|
|
1668
|
+
const point = new Vector3();
|
|
1669
|
+
ray.at(t, point);
|
|
1670
|
+
const normal = new Vector3(
|
|
1671
|
+
point.x - params.centerX,
|
|
1672
|
+
point.y - params.centerY,
|
|
1673
|
+
point.z - params.centerZ
|
|
1674
|
+
).normalize();
|
|
1675
|
+
if (params.invert) normal.negate();
|
|
1676
|
+
return { position: point, normal, distance: ray.origin.distanceTo(point) };
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
function createTerrainQuery(cache) {
|
|
1680
|
+
return {
|
|
1681
|
+
get generation() {
|
|
1682
|
+
return cache.generation;
|
|
1683
|
+
},
|
|
1684
|
+
getElevation(worldX, worldZ) {
|
|
1685
|
+
return cache.getElevation(worldX, worldZ);
|
|
1686
|
+
},
|
|
1687
|
+
getNormal(worldX, worldZ) {
|
|
1688
|
+
return cache.getNormal(worldX, worldZ);
|
|
1689
|
+
},
|
|
1690
|
+
getTile(worldX, worldZ) {
|
|
1691
|
+
return cache.getTile(worldX, worldZ);
|
|
1692
|
+
},
|
|
1693
|
+
getTileBounds(worldX, worldZ) {
|
|
1694
|
+
return cache.getTileBounds(worldX, worldZ);
|
|
1695
|
+
},
|
|
1696
|
+
getGlobalElevationRange() {
|
|
1697
|
+
return cache.getGlobalElevationRange();
|
|
1698
|
+
},
|
|
1699
|
+
sampleTerrain(worldX, worldZ) {
|
|
1700
|
+
return cache.sampleTerrain(worldX, worldZ);
|
|
1701
|
+
},
|
|
1702
|
+
sampleTerrainBatch(positions) {
|
|
1703
|
+
return cache.sampleTerrainBatch(positions);
|
|
1704
|
+
}
|
|
1705
|
+
};
|
|
1706
|
+
}
|
|
1707
|
+
function createTerrainSurfaceQuery(cache) {
|
|
1708
|
+
return {
|
|
1709
|
+
get generation() {
|
|
1710
|
+
return cache.generation;
|
|
1711
|
+
},
|
|
1712
|
+
getElevationByPosition(position) {
|
|
1713
|
+
return cache.getElevationBySurfacePosition(position.x, position.y, position.z);
|
|
1714
|
+
},
|
|
1715
|
+
getNormalByPosition(position) {
|
|
1716
|
+
return cache.getNormalBySurfacePosition(position.x, position.y, position.z);
|
|
1717
|
+
},
|
|
1718
|
+
sampleTerrainByPosition(position) {
|
|
1719
|
+
return cache.sampleSurfaceByPosition(position.x, position.y, position.z);
|
|
1720
|
+
},
|
|
1721
|
+
getTileByPosition(position) {
|
|
1722
|
+
return cache.getTileBySurfacePosition(position.x, position.y, position.z);
|
|
1723
|
+
},
|
|
1724
|
+
getTileBoundsByPosition(position) {
|
|
1725
|
+
return cache.getTileBoundsBySurfacePosition(position.x, position.y, position.z);
|
|
1726
|
+
},
|
|
1727
|
+
sampleTerrainBatchByPosition(positions) {
|
|
1728
|
+
return cache.sampleSurfaceBatchByPosition(positions);
|
|
1729
|
+
}
|
|
1730
|
+
};
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
function createFlatTileComputeParts(ctx) {
|
|
1734
|
+
const { uniforms, shared } = ctx;
|
|
1735
|
+
const tileSize = Fn(([nodeIndex]) => {
|
|
1736
|
+
const level = shared.tileLevel(nodeIndex);
|
|
1737
|
+
const divisor = pow(float(2), level.toFloat());
|
|
1738
|
+
return float(uniforms.uRootSize.toVar()).div(divisor);
|
|
1739
|
+
});
|
|
1740
|
+
const rootUV = Fn(([nodeIndex, ix, iy]) => {
|
|
1741
|
+
const nodeVec2 = shared.tileOriginVec2(nodeIndex);
|
|
1742
|
+
const nodeX = nodeVec2.x;
|
|
1743
|
+
const nodeY = nodeVec2.y;
|
|
1744
|
+
const rootSize = uniforms.uRootSize.toVar();
|
|
1745
|
+
const rootOrigin = uniforms.uRootOrigin.toVar();
|
|
1746
|
+
const size = tileSize(nodeIndex);
|
|
1747
|
+
const half = float(0.5);
|
|
1748
|
+
const halfRoot = float(rootSize).mul(half);
|
|
1749
|
+
const fInnerSegments = uniforms.uInnerTileSegments.toVar().toFloat();
|
|
1750
|
+
const texelSpacing = size.div(fInnerSegments);
|
|
1751
|
+
const absX = nodeX.mul(fInnerSegments).add(int(ix).toFloat().sub(float(1)));
|
|
1752
|
+
const absY = nodeY.mul(fInnerSegments).add(int(iy).toFloat().sub(float(1)));
|
|
1753
|
+
const worldX = rootOrigin.x.add(absX.mul(texelSpacing)).sub(halfRoot);
|
|
1754
|
+
const worldZ = rootOrigin.z.add(absY.mul(texelSpacing)).sub(halfRoot);
|
|
1755
|
+
const centeredX = worldX.sub(rootOrigin.x);
|
|
1756
|
+
const centeredZ = worldZ.sub(rootOrigin.z);
|
|
1757
|
+
return vec2(
|
|
1758
|
+
centeredX.div(rootSize).add(half),
|
|
1759
|
+
centeredZ.div(rootSize).mul(float(-1)).add(half)
|
|
1760
|
+
);
|
|
1761
|
+
});
|
|
1762
|
+
const tileVertexWorldPosition = Fn(([nodeIndex, ix, iy]) => {
|
|
1763
|
+
const rootOrigin = uniforms.uRootOrigin.toVar();
|
|
1764
|
+
const nodeVec2 = shared.tileOriginVec2(nodeIndex);
|
|
1765
|
+
const nodeX = nodeVec2.x;
|
|
1766
|
+
const nodeY = nodeVec2.y;
|
|
1767
|
+
const rootSize = uniforms.uRootSize.toVar();
|
|
1768
|
+
const size = tileSize(nodeIndex);
|
|
1769
|
+
const half = float(0.5);
|
|
1770
|
+
const halfRoot = float(rootSize).mul(half);
|
|
1771
|
+
const fInnerSegments = uniforms.uInnerTileSegments.toVar().toFloat();
|
|
1772
|
+
const texelSpacing = size.div(fInnerSegments);
|
|
1773
|
+
const absX = nodeX.mul(fInnerSegments).add(int(ix).toFloat().sub(float(1)));
|
|
1774
|
+
const absY = nodeY.mul(fInnerSegments).add(int(iy).toFloat().sub(float(1)));
|
|
1775
|
+
const worldX = rootOrigin.x.add(absX.mul(texelSpacing)).sub(halfRoot);
|
|
1776
|
+
const worldZ = rootOrigin.z.add(absY.mul(texelSpacing)).sub(halfRoot);
|
|
1777
|
+
return vec3(worldX, rootOrigin.y, worldZ);
|
|
1778
|
+
});
|
|
1779
|
+
return {
|
|
1780
|
+
tileSize: (nodeIndex) => tileSize(nodeIndex),
|
|
1781
|
+
rootUV: (nodeIndex, ix, iy) => rootUV(nodeIndex, ix, iy),
|
|
1782
|
+
tileVertexWorldPosition: (nodeIndex, ix, iy) => tileVertexWorldPosition(nodeIndex, ix, iy)
|
|
1783
|
+
};
|
|
1784
|
+
}
|
|
1785
|
+
function createFlatProjection() {
|
|
1786
|
+
return {
|
|
1787
|
+
kind: "flat",
|
|
1788
|
+
faceOutward: false,
|
|
1789
|
+
gpu: {
|
|
1790
|
+
renderVertexPosition(ctx) {
|
|
1791
|
+
return createFlatRenderVertexPosition(ctx.leafStorage, ctx.uniforms, ctx.terrainFieldStorage);
|
|
1792
|
+
},
|
|
1793
|
+
createTileComputeParts: createFlatTileComputeParts,
|
|
1794
|
+
createFieldNormal(ctx) {
|
|
1795
|
+
const computeNormal = createFlatNormalFromElevationField(
|
|
1796
|
+
ctx.elevationFieldNode,
|
|
1797
|
+
ctx.edgeVertexCount
|
|
1798
|
+
);
|
|
1799
|
+
return (nodeIndex, ix, iy) => computeNormal(nodeIndex, ctx.tile.tileSize(nodeIndex), ix, iy, ctx.uniforms.uElevationScale);
|
|
1800
|
+
}
|
|
1801
|
+
},
|
|
1802
|
+
cpu: {
|
|
1803
|
+
cameraSurfaceOffset(cam, elevation) {
|
|
1804
|
+
cam.y -= elevation;
|
|
1805
|
+
},
|
|
1806
|
+
createSurfaceOps() {
|
|
1807
|
+
return null;
|
|
1808
|
+
},
|
|
1809
|
+
createRuntimeQueries(cache) {
|
|
1810
|
+
return { query: createTerrainQuery(cache), surfaceQuery: null, sphereQuery: null };
|
|
1811
|
+
},
|
|
1812
|
+
raycast(ctx) {
|
|
1813
|
+
const { ray, options, terrainQuery, config } = ctx;
|
|
1814
|
+
if (terrainQuery) {
|
|
1815
|
+
const precise = cpuRaycast(terrainQuery, ray, config, options);
|
|
1816
|
+
if (precise) return precise;
|
|
1817
|
+
}
|
|
1818
|
+
const coarse = cpuRaycastBoundsOnly(ray, config, options);
|
|
1819
|
+
if (coarse && terrainQuery) {
|
|
1820
|
+
const sample = terrainQuery.sampleTerrain(coarse.position.x, coarse.position.z);
|
|
1821
|
+
if (sample.valid) {
|
|
1822
|
+
coarse.position.y = sample.elevation;
|
|
1823
|
+
coarse.normal.copy(sample.normal);
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
return coarse;
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
};
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
function createFlatTopology(cfg) {
|
|
1833
|
+
const halfRoot = 0.5 * cfg.rootSize;
|
|
1834
|
+
const topology = {
|
|
1835
|
+
spaceCount: 1,
|
|
1836
|
+
maxRootCount: 1,
|
|
1837
|
+
projection: createFlatProjection(),
|
|
1838
|
+
neighborSameLevel(tile, dir, out) {
|
|
1839
|
+
const level = tile.level;
|
|
1840
|
+
const x = tile.x;
|
|
1841
|
+
const y = tile.y;
|
|
1842
|
+
let nx = x;
|
|
1843
|
+
let ny = y;
|
|
1844
|
+
switch (dir) {
|
|
1845
|
+
case Dir.LEFT:
|
|
1846
|
+
nx = x - 1;
|
|
1847
|
+
break;
|
|
1848
|
+
case Dir.RIGHT:
|
|
1849
|
+
nx = x + 1;
|
|
1850
|
+
break;
|
|
1851
|
+
case Dir.TOP:
|
|
1852
|
+
ny = y - 1;
|
|
1853
|
+
break;
|
|
1854
|
+
case Dir.BOTTOM:
|
|
1855
|
+
ny = y + 1;
|
|
1856
|
+
break;
|
|
1857
|
+
}
|
|
1858
|
+
if (nx < 0 || ny < 0) return false;
|
|
1859
|
+
const maxCoord = (1 << level) - 1;
|
|
1860
|
+
if (nx > maxCoord || ny > maxCoord) return false;
|
|
1861
|
+
out.space = 0;
|
|
1862
|
+
out.level = level;
|
|
1863
|
+
out.x = nx;
|
|
1864
|
+
out.y = ny;
|
|
1865
|
+
return true;
|
|
1866
|
+
},
|
|
1867
|
+
tileBounds(tile, cameraOrigin, out, elevationRange) {
|
|
1868
|
+
const level = tile.level;
|
|
1869
|
+
const scale = 1 / (1 << level);
|
|
1870
|
+
const size = cfg.rootSize * scale;
|
|
1871
|
+
const minX = cfg.origin.x + (tile.x * size - halfRoot);
|
|
1872
|
+
const minZ = cfg.origin.z + (tile.y * size - halfRoot);
|
|
1873
|
+
const centerX = minX + 0.5 * size;
|
|
1874
|
+
const centerZ = minZ + 0.5 * size;
|
|
1875
|
+
const centerY = cfg.origin.y + (elevationRange ? (elevationRange.min + elevationRange.max) * 0.5 : 0);
|
|
1876
|
+
out.cx = centerX - cameraOrigin.x;
|
|
1877
|
+
out.cy = centerY - cameraOrigin.y;
|
|
1878
|
+
out.cz = centerZ - cameraOrigin.z;
|
|
1879
|
+
const halfDiag = 0.7071067811865476 * size;
|
|
1880
|
+
const vertExtent = elevationRange ? Math.max(Math.abs(elevationRange.min), Math.abs(elevationRange.max)) : 0;
|
|
1881
|
+
out.r = halfDiag + vertExtent;
|
|
1882
|
+
},
|
|
1883
|
+
rootTiles(_cameraOrigin, out) {
|
|
1884
|
+
const root = out[0];
|
|
1885
|
+
root.space = 0;
|
|
1886
|
+
root.level = 0;
|
|
1887
|
+
root.x = 0;
|
|
1888
|
+
root.y = 0;
|
|
1889
|
+
return 1;
|
|
1890
|
+
}
|
|
1891
|
+
};
|
|
1892
|
+
return topology;
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
function createInfiniteFlatTopology(cfg) {
|
|
1896
|
+
const halfRoot = 0.5 * cfg.rootSize;
|
|
1897
|
+
const rootGridRadius = Math.max(0, Math.floor(cfg.rootGridRadius ?? 1));
|
|
1898
|
+
const rootWidth = rootGridRadius * 2 + 1;
|
|
1899
|
+
return {
|
|
1900
|
+
spaceCount: 1,
|
|
1901
|
+
maxRootCount: rootWidth * rootWidth,
|
|
1902
|
+
projection: createFlatProjection(),
|
|
1903
|
+
neighborSameLevel(tile, dir, out) {
|
|
1904
|
+
let nx = tile.x;
|
|
1905
|
+
let ny = tile.y;
|
|
1906
|
+
switch (dir) {
|
|
1907
|
+
case Dir.LEFT:
|
|
1908
|
+
nx = tile.x - 1;
|
|
1909
|
+
break;
|
|
1910
|
+
case Dir.RIGHT:
|
|
1911
|
+
nx = tile.x + 1;
|
|
1912
|
+
break;
|
|
1913
|
+
case Dir.TOP:
|
|
1914
|
+
ny = tile.y - 1;
|
|
1915
|
+
break;
|
|
1916
|
+
case Dir.BOTTOM:
|
|
1917
|
+
ny = tile.y + 1;
|
|
1918
|
+
break;
|
|
1919
|
+
}
|
|
1920
|
+
out.space = tile.space;
|
|
1921
|
+
out.level = tile.level;
|
|
1922
|
+
out.x = nx;
|
|
1923
|
+
out.y = ny;
|
|
1924
|
+
return true;
|
|
1925
|
+
},
|
|
1926
|
+
tileBounds(tile, cameraOrigin, out, elevationRange) {
|
|
1927
|
+
const level = tile.level;
|
|
1928
|
+
const scale = 1 / (1 << level);
|
|
1929
|
+
const size = cfg.rootSize * scale;
|
|
1930
|
+
const minX = cfg.origin.x + (tile.x * size - halfRoot);
|
|
1931
|
+
const minZ = cfg.origin.z + (tile.y * size - halfRoot);
|
|
1932
|
+
const centerX = minX + 0.5 * size;
|
|
1933
|
+
const centerZ = minZ + 0.5 * size;
|
|
1934
|
+
const centerY = cfg.origin.y + (elevationRange ? (elevationRange.min + elevationRange.max) * 0.5 : 0);
|
|
1935
|
+
out.cx = centerX - cameraOrigin.x;
|
|
1936
|
+
out.cy = centerY - cameraOrigin.y;
|
|
1937
|
+
out.cz = centerZ - cameraOrigin.z;
|
|
1938
|
+
const halfDiag = 0.7071067811865476 * size;
|
|
1939
|
+
const vertExtent = elevationRange ? Math.max(Math.abs(elevationRange.min), Math.abs(elevationRange.max)) : 0;
|
|
1940
|
+
out.r = halfDiag + vertExtent;
|
|
1941
|
+
},
|
|
1942
|
+
rootTiles(cameraOrigin, out) {
|
|
1943
|
+
const camRootX = Math.floor((cameraOrigin.x - cfg.origin.x + halfRoot) / cfg.rootSize);
|
|
1944
|
+
const camRootY = Math.floor((cameraOrigin.z - cfg.origin.z + halfRoot) / cfg.rootSize);
|
|
1945
|
+
let index = 0;
|
|
1946
|
+
for (let dy = -rootGridRadius; dy <= rootGridRadius; dy++) {
|
|
1947
|
+
for (let dx = -rootGridRadius; dx <= rootGridRadius; dx++) {
|
|
1948
|
+
const root = out[index];
|
|
1949
|
+
root.space = 0;
|
|
1950
|
+
root.level = 0;
|
|
1951
|
+
root.x = camRootX + dx;
|
|
1952
|
+
root.y = camRootY + dy;
|
|
1953
|
+
index++;
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
return index;
|
|
1957
|
+
}
|
|
1958
|
+
};
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
const CUBE_FACE_COUNT = 6;
|
|
1962
|
+
const CUBE_FACES = [
|
|
1963
|
+
// 0: +X
|
|
1964
|
+
{ forward: [1, 0, 0], right: [0, 0, -1], up: [0, 1, 0] },
|
|
1965
|
+
// 1: -X
|
|
1966
|
+
{ forward: [-1, 0, 0], right: [0, 0, 1], up: [0, 1, 0] },
|
|
1967
|
+
// 2: +Y (north pole)
|
|
1968
|
+
{ forward: [0, 1, 0], right: [1, 0, 0], up: [0, 0, -1] },
|
|
1969
|
+
// 3: -Y (south pole)
|
|
1970
|
+
{ forward: [0, -1, 0], right: [1, 0, 0], up: [0, 0, 1] },
|
|
1971
|
+
// 4: +Z
|
|
1972
|
+
{ forward: [0, 0, 1], right: [1, 0, 0], up: [0, 1, 0] },
|
|
1973
|
+
// 5: -Z
|
|
1974
|
+
{ forward: [0, 0, -1], right: [-1, 0, 0], up: [0, 1, 0] }
|
|
1975
|
+
];
|
|
1976
|
+
|
|
1977
|
+
function vec3Const(v) {
|
|
1978
|
+
return vec3(float(v[0]), float(v[1]), float(v[2]));
|
|
1979
|
+
}
|
|
1980
|
+
function selectFaceVec3(face, pick) {
|
|
1981
|
+
const last = CUBE_FACES.length - 1;
|
|
1982
|
+
let acc = vec3Const(pick(CUBE_FACES[last]));
|
|
1983
|
+
for (let i = last - 1; i >= 0; i--) {
|
|
1984
|
+
acc = select(int(face).equal(int(i)), vec3Const(pick(CUBE_FACES[i])), acc);
|
|
1985
|
+
}
|
|
1986
|
+
return acc;
|
|
1987
|
+
}
|
|
1988
|
+
function cubeFaceBasis(face) {
|
|
1989
|
+
return {
|
|
1990
|
+
forward: selectFaceVec3(face, (f) => f.forward),
|
|
1991
|
+
right: selectFaceVec3(face, (f) => f.right),
|
|
1992
|
+
up: selectFaceVec3(face, (f) => f.up)
|
|
1993
|
+
};
|
|
1994
|
+
}
|
|
1995
|
+
function cubeFacePoint(basis, u, v) {
|
|
1996
|
+
const s = float(u).mul(2).sub(1);
|
|
1997
|
+
const t = float(v).mul(2).sub(1);
|
|
1998
|
+
return basis.forward.add(basis.right.mul(s)).add(basis.up.mul(t));
|
|
1999
|
+
}
|
|
2000
|
+
function cubeFaceDirection(basis, u, v) {
|
|
2001
|
+
return cubeFacePoint(basis, u, v).normalize();
|
|
2002
|
+
}
|
|
2003
|
+
function tangentFromAxis(dir, axis) {
|
|
2004
|
+
return axis.sub(dir.mul(dir.dot(axis))).normalize();
|
|
2005
|
+
}
|
|
2006
|
+
function unpackTangentNormal(nx, nz) {
|
|
2007
|
+
const ny = float(1).sub(nx.mul(nx)).sub(nz.mul(nz)).max(float(0)).sqrt();
|
|
2008
|
+
return vec3(nx, ny, nz);
|
|
2009
|
+
}
|
|
2010
|
+
function sphereTangentFrameNormal(dir, basis, tangentNormal) {
|
|
2011
|
+
const n = vec3(tangentNormal);
|
|
2012
|
+
const tu = tangentFromAxis(dir, basis.right);
|
|
2013
|
+
const tv = tangentFromAxis(dir, basis.up);
|
|
2014
|
+
return tu.mul(n.x).add(dir.mul(n.y)).add(tv.mul(n.z)).normalize();
|
|
2015
|
+
}
|
|
2016
|
+
function cubeFaceFromDirection(dir) {
|
|
2017
|
+
const d = vec3(dir);
|
|
2018
|
+
const ax = d.x.abs();
|
|
2019
|
+
const ay = d.y.abs();
|
|
2020
|
+
const az = d.z.abs();
|
|
2021
|
+
const faceX = select(d.x.greaterThanEqual(float(0)), int(0), int(1));
|
|
2022
|
+
const faceY = select(d.y.greaterThanEqual(float(0)), int(2), int(3));
|
|
2023
|
+
const faceZ = select(d.z.greaterThanEqual(float(0)), int(4), int(5));
|
|
2024
|
+
const xDominant = ax.greaterThanEqual(ay).and(ax.greaterThanEqual(az));
|
|
2025
|
+
const yDominant = ay.greaterThanEqual(ax).and(ay.greaterThanEqual(az));
|
|
2026
|
+
return select(xDominant, faceX, select(yDominant, faceY, faceZ));
|
|
2027
|
+
}
|
|
2028
|
+
function cubeFaceUVFromDirection(basis, dir) {
|
|
2029
|
+
const d = vec3(dir);
|
|
2030
|
+
const p = d.div(d.dot(basis.forward));
|
|
2031
|
+
const s = p.dot(basis.right);
|
|
2032
|
+
const t = p.dot(basis.up);
|
|
2033
|
+
return vec2(s.add(float(1)).mul(float(0.5)), t.add(float(1)).mul(float(0.5)));
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
const DEG_TO_RAD = Math.PI / 180;
|
|
2037
|
+
const RAD_TO_DEG = 180 / Math.PI;
|
|
2038
|
+
function dot(a, b) {
|
|
2039
|
+
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
2040
|
+
}
|
|
2041
|
+
function faceUVToCube(face, u, v, out) {
|
|
2042
|
+
const f = CUBE_FACES[face];
|
|
2043
|
+
const s = 2 * u - 1;
|
|
2044
|
+
const t = 2 * v - 1;
|
|
2045
|
+
out[0] = f.forward[0] + s * f.right[0] + t * f.up[0];
|
|
2046
|
+
out[1] = f.forward[1] + s * f.right[1] + t * f.up[1];
|
|
2047
|
+
out[2] = f.forward[2] + s * f.right[2] + t * f.up[2];
|
|
2048
|
+
}
|
|
2049
|
+
function directionToFace(d) {
|
|
2050
|
+
const ax = Math.abs(d[0]);
|
|
2051
|
+
const ay = Math.abs(d[1]);
|
|
2052
|
+
const az = Math.abs(d[2]);
|
|
2053
|
+
if (ax >= ay && ax >= az) return d[0] >= 0 ? 0 : 1;
|
|
2054
|
+
if (ay >= ax && ay >= az) return d[1] >= 0 ? 2 : 3;
|
|
2055
|
+
return d[2] >= 0 ? 4 : 5;
|
|
2056
|
+
}
|
|
2057
|
+
function directionToFaceUV(face, d, out) {
|
|
2058
|
+
const f = CUBE_FACES[face];
|
|
2059
|
+
const denom = dot(d, f.forward);
|
|
2060
|
+
const inv = 1 / denom;
|
|
2061
|
+
const px = d[0] * inv;
|
|
2062
|
+
const py = d[1] * inv;
|
|
2063
|
+
const pz = d[2] * inv;
|
|
2064
|
+
const p = [px, py, pz];
|
|
2065
|
+
const s = dot(p, f.right);
|
|
2066
|
+
const t = dot(p, f.up);
|
|
2067
|
+
out[0] = (s + 1) * 0.5;
|
|
2068
|
+
out[1] = (t + 1) * 0.5;
|
|
2069
|
+
}
|
|
2070
|
+
function latLongToDirection(latDeg, lonDeg, out) {
|
|
2071
|
+
const lat = latDeg * DEG_TO_RAD;
|
|
2072
|
+
const lon = lonDeg * DEG_TO_RAD;
|
|
2073
|
+
const cosLat = Math.cos(lat);
|
|
2074
|
+
out[0] = cosLat * Math.sin(lon);
|
|
2075
|
+
out[1] = Math.sin(lat);
|
|
2076
|
+
out[2] = cosLat * Math.cos(lon);
|
|
2077
|
+
}
|
|
2078
|
+
function directionToLatLong(d) {
|
|
2079
|
+
const len = Math.hypot(d[0], d[1], d[2]) || 1;
|
|
2080
|
+
const y = Math.max(-1, Math.min(1, d[1] / len));
|
|
2081
|
+
return {
|
|
2082
|
+
latitude: Math.asin(y) * RAD_TO_DEG,
|
|
2083
|
+
longitude: Math.atan2(d[0], d[2]) * RAD_TO_DEG
|
|
2084
|
+
};
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
function readHeight(elevation, shape, leafIndex, ix, iy) {
|
|
2088
|
+
const base = leafIndex * shape.verticesPerNode;
|
|
2089
|
+
return elevation[base + iy * shape.edgeVertexCount + ix] ?? 0;
|
|
2090
|
+
}
|
|
2091
|
+
function sampleGridBilinear(elevation, shape, leafIndex, gx, gy) {
|
|
2092
|
+
const max = shape.edgeVertexCount - 1;
|
|
2093
|
+
const x = Math.max(0, Math.min(max, gx));
|
|
2094
|
+
const y = Math.max(0, Math.min(max, gy));
|
|
2095
|
+
const x0 = Math.floor(x);
|
|
2096
|
+
const y0 = Math.floor(y);
|
|
2097
|
+
const x1 = Math.min(max, x0 + 1);
|
|
2098
|
+
const y1 = Math.min(max, y0 + 1);
|
|
2099
|
+
const tx = x - x0;
|
|
2100
|
+
const ty = y - y0;
|
|
2101
|
+
const h00 = readHeight(elevation, shape, leafIndex, x0, y0);
|
|
2102
|
+
const h10 = readHeight(elevation, shape, leafIndex, x1, y0);
|
|
2103
|
+
const h01 = readHeight(elevation, shape, leafIndex, x0, y1);
|
|
2104
|
+
const h11 = readHeight(elevation, shape, leafIndex, x1, y1);
|
|
2105
|
+
const hx0 = h00 + (h10 - h00) * tx;
|
|
2106
|
+
const hx1 = h01 + (h11 - h01) * tx;
|
|
2107
|
+
return hx0 + (hx1 - hx0) * ty;
|
|
2108
|
+
}
|
|
2109
|
+
function elevationGradientAt(elevation, shape, leafIndex, gx, gy, stepWorld, elevationScale, out) {
|
|
2110
|
+
const hLeft = sampleGridBilinear(elevation, shape, leafIndex, gx - 1, gy);
|
|
2111
|
+
const hRight = sampleGridBilinear(elevation, shape, leafIndex, gx + 1, gy);
|
|
2112
|
+
const hUp = sampleGridBilinear(elevation, shape, leafIndex, gx, gy - 1);
|
|
2113
|
+
const hDown = sampleGridBilinear(elevation, shape, leafIndex, gx, gy + 1);
|
|
2114
|
+
const inv2Step = 0.5 / stepWorld;
|
|
2115
|
+
out.dhdu = (hRight - hLeft) * elevationScale * inv2Step;
|
|
2116
|
+
out.dhdv = (hDown - hUp) * elevationScale * inv2Step;
|
|
2117
|
+
return out;
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
function createElevationFunction(callback) {
|
|
2121
|
+
const tslFunction = (args) => {
|
|
2122
|
+
const params = {
|
|
2123
|
+
worldPosition: args.worldPosition,
|
|
2124
|
+
rootSize: args.rootSize,
|
|
2125
|
+
rootUV: args.rootUV,
|
|
2126
|
+
tileUV: args.tileUV,
|
|
2127
|
+
tileLevel: args.tileLevel,
|
|
2128
|
+
tileSize: args.tileSize,
|
|
2129
|
+
tileOriginVec2: args.tileOriginVec2,
|
|
2130
|
+
nodeIndex: args.nodeIndex
|
|
2131
|
+
};
|
|
2132
|
+
return callback(params);
|
|
2133
|
+
};
|
|
2134
|
+
return Fn$1(tslFunction);
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
const SLOT_STRIDE = 6;
|
|
2138
|
+
function nextPow2$1(n) {
|
|
2139
|
+
let x = 1;
|
|
2140
|
+
while (x < n) x <<= 1;
|
|
2141
|
+
return x;
|
|
2142
|
+
}
|
|
2143
|
+
function createGpuSpatialIndex(maxEntries) {
|
|
2144
|
+
const size = nextPow2$1(Math.max(2, maxEntries * 2));
|
|
2145
|
+
const data = new Uint32Array(size * SLOT_STRIDE);
|
|
2146
|
+
const attribute = new StorageBufferAttribute(data, SLOT_STRIDE);
|
|
2147
|
+
attribute.name = "gpuSpatialIndex";
|
|
2148
|
+
const node = storage(attribute, "u32", 1).toReadOnly().setName("gpuSpatialIndex");
|
|
2149
|
+
const stampGen = uniform(uint(1)).setName("uGpuSpatialIndexStampGen");
|
|
2150
|
+
return {
|
|
2151
|
+
data,
|
|
2152
|
+
size,
|
|
2153
|
+
mask: size - 1,
|
|
2154
|
+
stampGen,
|
|
2155
|
+
attribute,
|
|
2156
|
+
node
|
|
2157
|
+
};
|
|
2158
|
+
}
|
|
2159
|
+
function uploadGpuSpatialIndex(gpuIndex, cpuIndex) {
|
|
2160
|
+
if (gpuIndex.size !== cpuIndex.size) {
|
|
2161
|
+
throw new Error(
|
|
2162
|
+
`Spatial index size mismatch (gpu=${gpuIndex.size}, cpu=${cpuIndex.size}).`
|
|
2163
|
+
);
|
|
2164
|
+
}
|
|
2165
|
+
for (let i = 0; i < cpuIndex.size; i += 1) {
|
|
2166
|
+
const base = i * SLOT_STRIDE;
|
|
2167
|
+
gpuIndex.data[base] = cpuIndex.stamp[i] ?? 0;
|
|
2168
|
+
gpuIndex.data[base + 1] = cpuIndex.keysSpace[i] ?? 0;
|
|
2169
|
+
gpuIndex.data[base + 2] = cpuIndex.keysLevel[i] ?? 0;
|
|
2170
|
+
gpuIndex.data[base + 3] = cpuIndex.keysX[i] ?? 0;
|
|
2171
|
+
gpuIndex.data[base + 4] = cpuIndex.keysY[i] ?? 0;
|
|
2172
|
+
gpuIndex.data[base + 5] = cpuIndex.values[i] ?? 0;
|
|
2173
|
+
}
|
|
2174
|
+
gpuIndex.stampGen.value = cpuIndex.stampGen >>> 0;
|
|
2175
|
+
gpuIndex.attribute.needsUpdate = true;
|
|
2176
|
+
gpuIndex.node.needsUpdate = true;
|
|
2177
|
+
}
|
|
2178
|
+
function readGpuSpatialIndexValue(spatialIndex, slot, fieldOffset) {
|
|
2179
|
+
const offset = int(slot).mul(int(SLOT_STRIDE)).add(int(fieldOffset));
|
|
2180
|
+
return spatialIndex.node.element(offset).toUint();
|
|
2181
|
+
}
|
|
2182
|
+
const mix32$1 = Fn(([x]) => {
|
|
2183
|
+
const v = uint(x).toVar();
|
|
2184
|
+
v.assign(v.bitXor(v.shiftRight(uint(16))));
|
|
2185
|
+
v.assign(v.mul(uint(2146121005)));
|
|
2186
|
+
v.assign(v.bitXor(v.shiftRight(uint(15))));
|
|
2187
|
+
v.assign(v.mul(uint(2221713035)));
|
|
2188
|
+
v.assign(v.bitXor(v.shiftRight(uint(16))));
|
|
2189
|
+
return v;
|
|
2190
|
+
});
|
|
2191
|
+
const hashKey$1 = Fn(([space, level, x, y]) => {
|
|
2192
|
+
const s = uint(space).bitAnd(uint(255));
|
|
2193
|
+
const l = uint(level).bitAnd(uint(255));
|
|
2194
|
+
const h = s.bitXor(l.shiftLeft(uint(8))).bitXor(mix32$1(uint(x))).bitXor(mix32$1(uint(y)));
|
|
2195
|
+
return mix32$1(h);
|
|
2196
|
+
});
|
|
2197
|
+
const createGpuSpatialLookup = (spatialIndex) => {
|
|
2198
|
+
const slotCount = spatialIndex.size;
|
|
2199
|
+
const mask = uint(spatialIndex.mask);
|
|
2200
|
+
const stampGen = spatialIndex.stampGen.toUint();
|
|
2201
|
+
const emptyValue = int(-1);
|
|
2202
|
+
return Fn(([space, level, x, y]) => {
|
|
2203
|
+
const s = uint(space).bitAnd(uint(255));
|
|
2204
|
+
const l = uint(level).bitAnd(uint(255));
|
|
2205
|
+
const xx = uint(x);
|
|
2206
|
+
const yy = uint(y);
|
|
2207
|
+
const result = emptyValue.toVar();
|
|
2208
|
+
const slot = hashKey$1(s, l, xx, yy).bitAnd(mask).toVar();
|
|
2209
|
+
const probes = int(0).toVar();
|
|
2210
|
+
Loop(slotCount, () => {
|
|
2211
|
+
const stamp = readGpuSpatialIndexValue(spatialIndex, slot, 0);
|
|
2212
|
+
If(stamp.notEqual(stampGen), () => {
|
|
2213
|
+
Break();
|
|
2214
|
+
});
|
|
2215
|
+
const ks = readGpuSpatialIndexValue(spatialIndex, slot, 1);
|
|
2216
|
+
const kl = readGpuSpatialIndexValue(spatialIndex, slot, 2);
|
|
2217
|
+
const kx = readGpuSpatialIndexValue(spatialIndex, slot, 3);
|
|
2218
|
+
const ky = readGpuSpatialIndexValue(spatialIndex, slot, 4);
|
|
2219
|
+
If(
|
|
2220
|
+
ks.equal(s).and(kl.equal(l)).and(kx.equal(xx)).and(ky.equal(yy)),
|
|
2221
|
+
() => {
|
|
2222
|
+
result.assign(int(readGpuSpatialIndexValue(spatialIndex, slot, 5)));
|
|
2223
|
+
Break();
|
|
2224
|
+
}
|
|
2225
|
+
);
|
|
2226
|
+
slot.assign(slot.add(uint(1)).bitAnd(mask));
|
|
2227
|
+
probes.addAssign(1);
|
|
2228
|
+
});
|
|
2229
|
+
return result;
|
|
2230
|
+
});
|
|
2231
|
+
};
|
|
2232
|
+
const createTileIndexFromWorldPosition = (spatialIndex, uniforms, maxLevel) => {
|
|
2233
|
+
const lookup = createGpuSpatialLookup(spatialIndex);
|
|
2234
|
+
const levelCount = Math.max(1, maxLevel + 1);
|
|
2235
|
+
return Fn(([worldX, worldZ]) => {
|
|
2236
|
+
const rootOrigin = uniforms.uRootOrigin.toVar();
|
|
2237
|
+
const rootSize = uniforms.uRootSize.toVar();
|
|
2238
|
+
const halfRoot = rootSize.mul(float(0.5));
|
|
2239
|
+
const tileIndex = int(-1).toVar();
|
|
2240
|
+
const tileU = float(0).toVar();
|
|
2241
|
+
const tileV = float(0).toVar();
|
|
2242
|
+
const i = int(0).toVar();
|
|
2243
|
+
Loop(levelCount, () => {
|
|
2244
|
+
const level = int(maxLevel).sub(i).toVar();
|
|
2245
|
+
const scale = pow(float(2), level.toFloat());
|
|
2246
|
+
const tileSize = rootSize.div(scale);
|
|
2247
|
+
const tileX = worldX.sub(rootOrigin.x).add(halfRoot).div(tileSize).floor().toInt();
|
|
2248
|
+
const tileY = worldZ.sub(rootOrigin.z).add(halfRoot).div(tileSize).floor().toInt();
|
|
2249
|
+
const maybeIndex = lookup(int(0), level, tileX, tileY).toVar();
|
|
2250
|
+
If(maybeIndex.greaterThanEqual(int(0)), () => {
|
|
2251
|
+
const minX = rootOrigin.x.add(tileX.toFloat().mul(tileSize)).sub(halfRoot);
|
|
2252
|
+
const minZ = rootOrigin.z.add(tileY.toFloat().mul(tileSize)).sub(halfRoot);
|
|
2253
|
+
tileIndex.assign(maybeIndex);
|
|
2254
|
+
tileU.assign(worldX.sub(minX).div(tileSize));
|
|
2255
|
+
tileV.assign(worldZ.sub(minZ).div(tileSize));
|
|
2256
|
+
Break();
|
|
2257
|
+
});
|
|
2258
|
+
i.addAssign(1);
|
|
2259
|
+
});
|
|
2260
|
+
return vec3(tileIndex.toFloat(), tileU, tileV);
|
|
2261
|
+
});
|
|
2262
|
+
};
|
|
2263
|
+
const createTileIndexFromDirection = (spatialIndex, maxLevel) => {
|
|
2264
|
+
const lookup = createGpuSpatialLookup(spatialIndex);
|
|
2265
|
+
const levelCount = Math.max(1, maxLevel + 1);
|
|
2266
|
+
return Fn(([direction]) => {
|
|
2267
|
+
const dir = vec3(direction).normalize().toVar();
|
|
2268
|
+
const face = cubeFaceFromDirection(dir).toVar();
|
|
2269
|
+
const basis = cubeFaceBasis(face);
|
|
2270
|
+
const faceUV = cubeFaceUVFromDirection(basis, dir).toVar();
|
|
2271
|
+
const u = faceUV.x.toVar();
|
|
2272
|
+
const v = faceUV.y.toVar();
|
|
2273
|
+
const tileIndex = int(-1).toVar();
|
|
2274
|
+
const tileU = float(0).toVar();
|
|
2275
|
+
const tileV = float(0).toVar();
|
|
2276
|
+
const i = int(0).toVar();
|
|
2277
|
+
Loop(levelCount, () => {
|
|
2278
|
+
const level = int(maxLevel).sub(i).toVar();
|
|
2279
|
+
const n = pow(float(2), level.toFloat()).toVar();
|
|
2280
|
+
const nInt = int(n).toVar();
|
|
2281
|
+
const tileX = u.mul(n).floor().toInt().max(int(0)).min(nInt.sub(int(1))).toVar();
|
|
2282
|
+
const tileY = v.mul(n).floor().toInt().max(int(0)).min(nInt.sub(int(1))).toVar();
|
|
2283
|
+
const maybeIndex = lookup(face, level, tileX, tileY).toVar();
|
|
2284
|
+
If(maybeIndex.greaterThanEqual(int(0)), () => {
|
|
2285
|
+
tileIndex.assign(maybeIndex);
|
|
2286
|
+
tileU.assign(u.mul(n).sub(tileX.toFloat()));
|
|
2287
|
+
tileV.assign(v.mul(n).sub(tileY.toFloat()));
|
|
2288
|
+
Break();
|
|
2289
|
+
});
|
|
2290
|
+
i.addAssign(1);
|
|
2291
|
+
});
|
|
2292
|
+
return vec3(tileIndex.toFloat(), tileU, tileV);
|
|
2293
|
+
});
|
|
2294
|
+
};
|
|
2295
|
+
|
|
2296
|
+
function packedSampleFromTileResult(params, tileResult) {
|
|
2297
|
+
const tileIndex = int(tileResult.x).toVar();
|
|
2298
|
+
const safeTileIndex = tileIndex.max(int(0)).toVar();
|
|
2299
|
+
const fieldU = tileLocalToFieldUV(
|
|
2300
|
+
tileResult.y,
|
|
2301
|
+
params.uniforms.uInnerTileSegments
|
|
2302
|
+
).toVar();
|
|
2303
|
+
const fieldV = tileLocalToFieldUV(
|
|
2304
|
+
tileResult.z,
|
|
2305
|
+
params.uniforms.uInnerTileSegments
|
|
2306
|
+
).toVar();
|
|
2307
|
+
const found = tileIndex.greaterThanEqual(int(0)).toVar();
|
|
2308
|
+
const sampled = sampleTerrainField(
|
|
2309
|
+
params.terrainFieldStorage,
|
|
2310
|
+
fieldU,
|
|
2311
|
+
fieldV,
|
|
2312
|
+
safeTileIndex
|
|
2313
|
+
).toVar();
|
|
2314
|
+
const normal = vec3(sampled.g, sampled.b, sampled.a);
|
|
2315
|
+
const valid = found.select(float(1), float(0)).toVar();
|
|
2316
|
+
return vec4(sampled.r, normal.x, normal.y, normal.z).mul(valid);
|
|
2317
|
+
}
|
|
2318
|
+
function createTerrainSampleNode(params) {
|
|
2319
|
+
const tileLookup = createTileIndexFromWorldPosition(
|
|
2320
|
+
params.spatialIndex,
|
|
2321
|
+
params.uniforms,
|
|
2322
|
+
params.maxLevel
|
|
2323
|
+
);
|
|
2324
|
+
return Fn(([worldX, worldZ]) => {
|
|
2325
|
+
const tileResult = tileLookup(worldX, worldZ).toVar();
|
|
2326
|
+
return packedSampleFromTileResult(params, tileResult);
|
|
2327
|
+
});
|
|
2328
|
+
}
|
|
2329
|
+
function createTerrainSampleNodeByDirection(params) {
|
|
2330
|
+
const tileLookup = createTileIndexFromDirection(params.spatialIndex, params.maxLevel);
|
|
2331
|
+
return Fn(([direction]) => {
|
|
2332
|
+
const tileResult = tileLookup(direction).toVar();
|
|
2333
|
+
return packedSampleFromTileResult(params, tileResult);
|
|
2334
|
+
});
|
|
2335
|
+
}
|
|
2336
|
+
function augmentCubeSphereSampler(sampler, params) {
|
|
2337
|
+
const terrainSampleByDir = createTerrainSampleNodeByDirection(params);
|
|
2338
|
+
sampler.sampleTerrainByDirection = Fn(([direction]) => terrainSampleByDir(direction));
|
|
2339
|
+
sampler.sampleElevationByDirection = Fn(
|
|
2340
|
+
([direction]) => terrainSampleByDir(direction).x
|
|
2341
|
+
);
|
|
2342
|
+
sampler.sampleValidityByDirection = Fn(([direction]) => {
|
|
2343
|
+
const sample = terrainSampleByDir(direction).toVar();
|
|
2344
|
+
return sample.y.abs().add(sample.z.abs()).add(sample.w.abs()).greaterThan(float(0)).select(float(1), float(0));
|
|
2345
|
+
});
|
|
2346
|
+
sampler.sampleNormalByDirection = Fn(([direction]) => {
|
|
2347
|
+
const packed = terrainSampleByDir(direction).toVar();
|
|
2348
|
+
return vec3(packed.y, packed.z, packed.w).normalize();
|
|
2349
|
+
});
|
|
2350
|
+
}
|
|
2351
|
+
function createTerrainSampler(params) {
|
|
2352
|
+
const elevationNode = createElevationFunction(params.elevationCallback);
|
|
2353
|
+
const terrainSampleAt = createTerrainSampleNode(params);
|
|
2354
|
+
const evaluateElevationAt = Fn(([worldX, worldZ]) => {
|
|
2355
|
+
const rootOrigin = params.uniforms.uRootOrigin.toVar();
|
|
2356
|
+
const rootSize = params.uniforms.uRootSize.toVar();
|
|
2357
|
+
const centeredX = worldX.sub(rootOrigin.x);
|
|
2358
|
+
const centeredZ = worldZ.sub(rootOrigin.z);
|
|
2359
|
+
const rootUV = vec2(
|
|
2360
|
+
centeredX.div(rootSize).add(0.5),
|
|
2361
|
+
centeredZ.div(rootSize).mul(float(-1)).add(0.5)
|
|
2362
|
+
).toVar();
|
|
2363
|
+
return elevationNode({
|
|
2364
|
+
worldPosition: vec3(worldX, rootOrigin.y, worldZ),
|
|
2365
|
+
rootSize,
|
|
2366
|
+
rootUV,
|
|
2367
|
+
tileUV: rootUV,
|
|
2368
|
+
tileLevel: int(0),
|
|
2369
|
+
tileSize: rootSize,
|
|
2370
|
+
tileOriginVec2: vec2(0, 0),
|
|
2371
|
+
nodeIndex: int(0)
|
|
2372
|
+
});
|
|
2373
|
+
});
|
|
2374
|
+
const sampleTerrain = Fn(
|
|
2375
|
+
([worldX, worldZ]) => terrainSampleAt(worldX, worldZ)
|
|
2376
|
+
);
|
|
2377
|
+
const sampleElevation = Fn(
|
|
2378
|
+
([worldX, worldZ]) => terrainSampleAt(worldX, worldZ).x
|
|
2379
|
+
);
|
|
2380
|
+
const sampleNormal = Fn(([worldX, worldZ]) => {
|
|
2381
|
+
const sample = terrainSampleAt(worldX, worldZ).toVar();
|
|
2382
|
+
return vec3(sample.y, sample.z, sample.w);
|
|
2383
|
+
});
|
|
2384
|
+
const sampleValidity = Fn(([worldX, worldZ]) => {
|
|
2385
|
+
const sample = terrainSampleAt(worldX, worldZ).toVar();
|
|
2386
|
+
return sample.y.abs().add(sample.z.abs()).add(sample.w.abs()).greaterThan(float(0)).select(float(1), float(0));
|
|
2387
|
+
});
|
|
2388
|
+
const evaluateElevation = Fn(
|
|
2389
|
+
([worldX, worldZ]) => evaluateElevationAt(worldX, worldZ)
|
|
2390
|
+
);
|
|
2391
|
+
const evaluateNormalNode = Fn(
|
|
2392
|
+
([worldX, worldZ, epsilon]) => {
|
|
2393
|
+
const eps = epsilon ?? float(0.1);
|
|
2394
|
+
const elevationScale = params.uniforms.uElevationScale.toVar();
|
|
2395
|
+
const hL = evaluateElevationAt(worldX.sub(eps), worldZ).mul(
|
|
2396
|
+
elevationScale
|
|
2397
|
+
);
|
|
2398
|
+
const hR = evaluateElevationAt(worldX.add(eps), worldZ).mul(
|
|
2399
|
+
elevationScale
|
|
2400
|
+
);
|
|
2401
|
+
const hD = evaluateElevationAt(worldX, worldZ.sub(eps)).mul(
|
|
2402
|
+
elevationScale
|
|
2403
|
+
);
|
|
2404
|
+
const hU = evaluateElevationAt(worldX, worldZ.add(eps)).mul(
|
|
2405
|
+
elevationScale
|
|
2406
|
+
);
|
|
2407
|
+
const inv2eps = float(0.5).div(eps);
|
|
2408
|
+
const dhdx = hR.sub(hL).mul(inv2eps);
|
|
2409
|
+
const dhdz = hU.sub(hD).mul(inv2eps);
|
|
2410
|
+
return vec3(dhdx.negate(), float(1), dhdz.negate()).normalize();
|
|
2411
|
+
}
|
|
2412
|
+
);
|
|
2413
|
+
const evaluateNormal = (worldX, worldZ, epsilon) => evaluateNormalNode(worldX, worldZ, epsilon ?? float(0.1));
|
|
2414
|
+
const sampler = {
|
|
2415
|
+
sampleElevation,
|
|
2416
|
+
sampleNormal,
|
|
2417
|
+
sampleTerrain,
|
|
2418
|
+
sampleValidity,
|
|
2419
|
+
evaluateElevation,
|
|
2420
|
+
evaluateNormal
|
|
2421
|
+
};
|
|
2422
|
+
params.projection.gpu.augmentSampler?.(sampler, params);
|
|
2423
|
+
return sampler;
|
|
2424
|
+
}
|
|
2425
|
+
|
|
2426
|
+
const RAYCAST_PADDING$1 = 1;
|
|
2427
|
+
function createSphereTileComputeParts(ctx) {
|
|
2428
|
+
const { uniforms, shared } = ctx;
|
|
2429
|
+
const tileSize = Fn(([nodeIndex]) => {
|
|
2430
|
+
const level = shared.tileLevel(nodeIndex);
|
|
2431
|
+
const divisor = pow(float(2), level.toFloat());
|
|
2432
|
+
return uniforms.uRadius.toVar().mul(float(HALF_PI)).div(divisor);
|
|
2433
|
+
});
|
|
2434
|
+
const tileVertexWorldPosition = Fn(([nodeIndex, ix, iy]) => {
|
|
2435
|
+
const rootOrigin = uniforms.uRootOrigin.toVar();
|
|
2436
|
+
const faceUV = shared.tileFaceUV(nodeIndex, ix, iy);
|
|
2437
|
+
const basis = cubeFaceBasis(shared.tileFace(nodeIndex));
|
|
2438
|
+
const dir = cubeFaceDirection(basis, faceUV.x, faceUV.y);
|
|
2439
|
+
return rootOrigin.add(dir.mul(uniforms.uRadius.toVar()));
|
|
2440
|
+
});
|
|
2441
|
+
return {
|
|
2442
|
+
tileSize: (nodeIndex) => tileSize(nodeIndex),
|
|
2443
|
+
rootUV: (nodeIndex, ix, iy) => shared.tileFaceUV(nodeIndex, ix, iy),
|
|
2444
|
+
tileVertexWorldPosition: (nodeIndex, ix, iy) => tileVertexWorldPosition(nodeIndex, ix, iy)
|
|
2445
|
+
};
|
|
2446
|
+
}
|
|
2447
|
+
function createCubeSphereProjection(config) {
|
|
2448
|
+
const radius = config.radius;
|
|
2449
|
+
const center = config.center ?? { x: 0, y: 0, z: 0 };
|
|
2450
|
+
const invert = config.invert ?? false;
|
|
2451
|
+
const cubeScratch = [0, 0, 0];
|
|
2452
|
+
const uvScratch = [0, 0];
|
|
2453
|
+
const dirScratch = [0, 0, 0];
|
|
2454
|
+
const posLeft = [0, 0, 0];
|
|
2455
|
+
const posRight = [0, 0, 0];
|
|
2456
|
+
const posUp = [0, 0, 0];
|
|
2457
|
+
const posDown = [0, 0, 0];
|
|
2458
|
+
const neighborPos = (face, u, v, height, out) => {
|
|
2459
|
+
faceUVToCube(face, u, v, cubeScratch);
|
|
2460
|
+
const len = Math.hypot(cubeScratch[0], cubeScratch[1], cubeScratch[2]) || 1;
|
|
2461
|
+
const r = invert ? (radius - height) / len : (radius + height) / len;
|
|
2462
|
+
out[0] = cubeScratch[0] * r;
|
|
2463
|
+
out[1] = cubeScratch[1] * r;
|
|
2464
|
+
out[2] = cubeScratch[2] * r;
|
|
2465
|
+
};
|
|
2466
|
+
const surfaceOps = {
|
|
2467
|
+
positionToKey(px, py, pz, out) {
|
|
2468
|
+
const dx = px - center.x;
|
|
2469
|
+
const dy = py - center.y;
|
|
2470
|
+
const dz = pz - center.z;
|
|
2471
|
+
const len = Math.hypot(dx, dy, dz);
|
|
2472
|
+
if (len === 0) return false;
|
|
2473
|
+
const nx = dx / len;
|
|
2474
|
+
const ny = dy / len;
|
|
2475
|
+
const nz = dz / len;
|
|
2476
|
+
const dirSign = invert ? -1 : 1;
|
|
2477
|
+
dirScratch[0] = nx;
|
|
2478
|
+
dirScratch[1] = ny;
|
|
2479
|
+
dirScratch[2] = nz;
|
|
2480
|
+
const face = directionToFace(dirScratch);
|
|
2481
|
+
directionToFaceUV(face, dirScratch, uvScratch);
|
|
2482
|
+
out.space = face;
|
|
2483
|
+
out.u = uvScratch[0];
|
|
2484
|
+
out.v = uvScratch[1];
|
|
2485
|
+
out.dirX = nx * dirSign;
|
|
2486
|
+
out.dirY = ny * dirSign;
|
|
2487
|
+
out.dirZ = nz * dirSign;
|
|
2488
|
+
return true;
|
|
2489
|
+
},
|
|
2490
|
+
surfacePosition(key, elevation, outVec) {
|
|
2491
|
+
const r = invert ? radius - elevation : radius + elevation;
|
|
2492
|
+
outVec.set(center.x + key.dirX * r, center.y + key.dirY * r, center.z + key.dirZ * r);
|
|
2493
|
+
},
|
|
2494
|
+
surfaceNormal(key, ctx) {
|
|
2495
|
+
const scale = ctx.elevationScale;
|
|
2496
|
+
const duv = 1 / (ctx.innerTileSegments * 2 ** ctx.level);
|
|
2497
|
+
dirScratch[0] = key.dirX;
|
|
2498
|
+
dirScratch[1] = key.dirY;
|
|
2499
|
+
dirScratch[2] = key.dirZ;
|
|
2500
|
+
directionToFaceUV(key.space, dirScratch, uvScratch);
|
|
2501
|
+
const u = uvScratch[0];
|
|
2502
|
+
const v = uvScratch[1];
|
|
2503
|
+
const hLeft = sampleGridBilinear(ctx.elevation, ctx.shape, ctx.leafIndex, ctx.gx - 1, ctx.gy) * scale;
|
|
2504
|
+
const hRight = sampleGridBilinear(ctx.elevation, ctx.shape, ctx.leafIndex, ctx.gx + 1, ctx.gy) * scale;
|
|
2505
|
+
const hUp = sampleGridBilinear(ctx.elevation, ctx.shape, ctx.leafIndex, ctx.gx, ctx.gy - 1) * scale;
|
|
2506
|
+
const hDown = sampleGridBilinear(ctx.elevation, ctx.shape, ctx.leafIndex, ctx.gx, ctx.gy + 1) * scale;
|
|
2507
|
+
neighborPos(key.space, u - duv, v, hLeft, posLeft);
|
|
2508
|
+
neighborPos(key.space, u + duv, v, hRight, posRight);
|
|
2509
|
+
neighborPos(key.space, u, v - duv, hUp, posUp);
|
|
2510
|
+
neighborPos(key.space, u, v + duv, hDown, posDown);
|
|
2511
|
+
const tux = posRight[0] - posLeft[0];
|
|
2512
|
+
const tuy = posRight[1] - posLeft[1];
|
|
2513
|
+
const tuz = posRight[2] - posLeft[2];
|
|
2514
|
+
const tvx = posDown[0] - posUp[0];
|
|
2515
|
+
const tvy = posDown[1] - posUp[1];
|
|
2516
|
+
const tvz = posDown[2] - posUp[2];
|
|
2517
|
+
let nx = tuy * tvz - tuz * tvy;
|
|
2518
|
+
let ny = tuz * tvx - tux * tvz;
|
|
2519
|
+
let nz = tux * tvy - tuy * tvx;
|
|
2520
|
+
if (nx * key.dirX + ny * key.dirY + nz * key.dirZ < 0) {
|
|
2521
|
+
nx = -nx;
|
|
2522
|
+
ny = -ny;
|
|
2523
|
+
nz = -nz;
|
|
2524
|
+
}
|
|
2525
|
+
return new Vector3(nx, ny, nz).normalize();
|
|
2526
|
+
}
|
|
2527
|
+
};
|
|
2528
|
+
return {
|
|
2529
|
+
kind: "cubeSphere",
|
|
2530
|
+
radius,
|
|
2531
|
+
center,
|
|
2532
|
+
faceOutward: !invert,
|
|
2533
|
+
gpu: {
|
|
2534
|
+
renderVertexPosition(ctx) {
|
|
2535
|
+
return createCurvedRenderVertexPosition(
|
|
2536
|
+
ctx.leafStorage,
|
|
2537
|
+
ctx.uniforms,
|
|
2538
|
+
ctx.terrainFieldStorage,
|
|
2539
|
+
(tile, faceUV, displacement) => {
|
|
2540
|
+
const basis = cubeFaceBasis(tile.face);
|
|
2541
|
+
const dir = cubeFaceDirection(basis, faceUV.x, faceUV.y);
|
|
2542
|
+
const r = invert ? ctx.uniforms.uRadius.toVar().sub(displacement) : ctx.uniforms.uRadius.toVar().add(displacement);
|
|
2543
|
+
return ctx.uniforms.uRootOrigin.toVar().add(dir.mul(r));
|
|
2544
|
+
}
|
|
2545
|
+
);
|
|
2546
|
+
},
|
|
2547
|
+
createTileComputeParts: createSphereTileComputeParts,
|
|
2548
|
+
createFieldNormal(ctx) {
|
|
2549
|
+
const computeNormal = createDisplacedSurfaceNormalFromElevationField(
|
|
2550
|
+
ctx.elevationFieldNode,
|
|
2551
|
+
ctx.edgeVertexCount,
|
|
2552
|
+
(nodeIndex) => {
|
|
2553
|
+
const basis = cubeFaceBasis(ctx.tile.tileFace(nodeIndex));
|
|
2554
|
+
const r = ctx.uniforms.uRadius.toVar();
|
|
2555
|
+
return {
|
|
2556
|
+
positionAt: (gx, gy, height) => {
|
|
2557
|
+
const uv = ctx.tile.tileFaceUV(nodeIndex, gx, gy);
|
|
2558
|
+
const dir = cubeFaceDirection(basis, uv.x, uv.y);
|
|
2559
|
+
return invert ? dir.mul(r.sub(height)) : dir.mul(r.add(height));
|
|
2560
|
+
},
|
|
2561
|
+
dirAt: (gx, gy) => {
|
|
2562
|
+
const uv = ctx.tile.tileFaceUV(nodeIndex, gx, gy);
|
|
2563
|
+
const dir = cubeFaceDirection(basis, uv.x, uv.y);
|
|
2564
|
+
return invert ? dir.negate() : dir;
|
|
2565
|
+
}
|
|
2566
|
+
};
|
|
2567
|
+
}
|
|
2568
|
+
);
|
|
2569
|
+
return (nodeIndex, ix, iy) => computeNormal(nodeIndex, ix, iy, ctx.uniforms.uElevationScale);
|
|
2570
|
+
},
|
|
2571
|
+
augmentSampler: augmentCubeSphereSampler
|
|
2572
|
+
},
|
|
2573
|
+
cpu: {
|
|
2574
|
+
cameraSurfaceOffset(cam, elevation) {
|
|
2575
|
+
const dx = cam.x - center.x;
|
|
2576
|
+
const dy = cam.y - center.y;
|
|
2577
|
+
const dz = cam.z - center.z;
|
|
2578
|
+
const len = Math.hypot(dx, dy, dz);
|
|
2579
|
+
if (len > 1e-12) {
|
|
2580
|
+
const sign = invert ? 1 : -1;
|
|
2581
|
+
const inv = sign * elevation / len;
|
|
2582
|
+
cam.x += dx * inv;
|
|
2583
|
+
cam.y += dy * inv;
|
|
2584
|
+
cam.z += dz * inv;
|
|
2585
|
+
}
|
|
2586
|
+
},
|
|
2587
|
+
createSurfaceOps() {
|
|
2588
|
+
return surfaceOps;
|
|
2589
|
+
},
|
|
2590
|
+
createRuntimeQueries(cache) {
|
|
2591
|
+
const query = createTerrainQuery(cache);
|
|
2592
|
+
const surfaceQuery = createTerrainSurfaceQuery(cache);
|
|
2593
|
+
const sphereQuery = createCubeSphereQuery(surfaceQuery, center);
|
|
2594
|
+
return { query, surfaceQuery, sphereQuery };
|
|
2595
|
+
},
|
|
2596
|
+
raycast(ctx) {
|
|
2597
|
+
const range = ctx.terrainQuery?.getGlobalElevationRange();
|
|
2598
|
+
const dispMax = range ? Math.max(0, range.max - center.y) : radius * 0.1;
|
|
2599
|
+
const outerPadding = invert ? 0 : dispMax + RAYCAST_PADDING$1;
|
|
2600
|
+
const params = {
|
|
2601
|
+
centerX: center.x,
|
|
2602
|
+
centerY: center.y,
|
|
2603
|
+
centerZ: center.z,
|
|
2604
|
+
radius,
|
|
2605
|
+
maxRadius: radius + outerPadding,
|
|
2606
|
+
invert
|
|
2607
|
+
};
|
|
2608
|
+
if (ctx.sphereQuery) {
|
|
2609
|
+
const precise = cubeSphereRaycast(ctx.sphereQuery, ctx.ray, params, ctx.options);
|
|
2610
|
+
if (precise) return precise;
|
|
2611
|
+
}
|
|
2612
|
+
return cubeSphereRaycastBoundsOnly(ctx.ray, params, ctx.options);
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2615
|
+
};
|
|
2616
|
+
}
|
|
2617
|
+
function createCubeSphereQuery(surfaceQuery, center) {
|
|
2618
|
+
const scratch = new Vector3();
|
|
2619
|
+
const ll = [0, 0, 0];
|
|
2620
|
+
const positionFromDirection = (dx, dy, dz) => scratch.set(center.x + dx, center.y + dy, center.z + dz);
|
|
2621
|
+
return {
|
|
2622
|
+
get generation() {
|
|
2623
|
+
return surfaceQuery.generation;
|
|
2624
|
+
},
|
|
2625
|
+
getElevationByPosition: (position) => surfaceQuery.getElevationByPosition(position),
|
|
2626
|
+
getNormalByPosition: (position) => surfaceQuery.getNormalByPosition(position),
|
|
2627
|
+
sampleTerrainByPosition: (position) => surfaceQuery.sampleTerrainByPosition(position),
|
|
2628
|
+
getTileByPosition: (position) => surfaceQuery.getTileByPosition(position),
|
|
2629
|
+
getTileBoundsByPosition: (position) => surfaceQuery.getTileBoundsByPosition(position),
|
|
2630
|
+
sampleTerrainBatchByPosition: (positions) => surfaceQuery.sampleTerrainBatchByPosition(positions),
|
|
2631
|
+
getElevationByDirection: (direction) => surfaceQuery.getElevationByPosition(positionFromDirection(direction.x, direction.y, direction.z)),
|
|
2632
|
+
getNormalByDirection: (direction) => surfaceQuery.getNormalByPosition(positionFromDirection(direction.x, direction.y, direction.z)),
|
|
2633
|
+
sampleTerrainByDirection: (direction) => surfaceQuery.sampleTerrainByPosition(positionFromDirection(direction.x, direction.y, direction.z)),
|
|
2634
|
+
getTileByDirection: (direction) => surfaceQuery.getTileByPosition(positionFromDirection(direction.x, direction.y, direction.z)),
|
|
2635
|
+
getTileBoundsByDirection: (direction) => surfaceQuery.getTileBoundsByPosition(
|
|
2636
|
+
positionFromDirection(direction.x, direction.y, direction.z)
|
|
2637
|
+
),
|
|
2638
|
+
getElevationByLatLong: (lat, lon) => {
|
|
2639
|
+
latLongToDirection(lat, lon, ll);
|
|
2640
|
+
return surfaceQuery.getElevationByPosition(positionFromDirection(ll[0], ll[1], ll[2]));
|
|
2641
|
+
},
|
|
2642
|
+
getNormalByLatLong: (lat, lon) => {
|
|
2643
|
+
latLongToDirection(lat, lon, ll);
|
|
2644
|
+
return surfaceQuery.getNormalByPosition(positionFromDirection(ll[0], ll[1], ll[2]));
|
|
2645
|
+
},
|
|
2646
|
+
sampleTerrainByLatLong: (lat, lon) => {
|
|
2647
|
+
latLongToDirection(lat, lon, ll);
|
|
2648
|
+
return surfaceQuery.sampleTerrainByPosition(positionFromDirection(ll[0], ll[1], ll[2]));
|
|
2649
|
+
},
|
|
2650
|
+
getTileByLatLong: (lat, lon) => {
|
|
2651
|
+
latLongToDirection(lat, lon, ll);
|
|
2652
|
+
return surfaceQuery.getTileByPosition(positionFromDirection(ll[0], ll[1], ll[2]));
|
|
2653
|
+
},
|
|
2654
|
+
getTileBoundsByLatLong: (lat, lon) => {
|
|
2655
|
+
latLongToDirection(lat, lon, ll);
|
|
2656
|
+
return surfaceQuery.getTileBoundsByPosition(positionFromDirection(ll[0], ll[1], ll[2]));
|
|
2657
|
+
},
|
|
2658
|
+
sampleTerrainBatchByDirection: (directions) => {
|
|
2659
|
+
const count = Math.floor(directions.length / 3);
|
|
2660
|
+
const positions = new Float32Array(count * 3);
|
|
2661
|
+
for (let i = 0; i < count; i += 1) {
|
|
2662
|
+
positions[i * 3] = center.x + (directions[i * 3] ?? 0);
|
|
2663
|
+
positions[i * 3 + 1] = center.y + (directions[i * 3 + 1] ?? 0);
|
|
2664
|
+
positions[i * 3 + 2] = center.z + (directions[i * 3 + 2] ?? 0);
|
|
2665
|
+
}
|
|
2666
|
+
return surfaceQuery.sampleTerrainBatchByPosition(positions);
|
|
2667
|
+
}
|
|
2668
|
+
};
|
|
2669
|
+
}
|
|
2670
|
+
|
|
2671
|
+
function createCubeSphereTopology(cfg) {
|
|
2672
|
+
const radius = cfg.radius;
|
|
2673
|
+
const center = cfg.center ?? { x: 0, y: 0, z: 0 };
|
|
2674
|
+
const cube = [0, 0, 0];
|
|
2675
|
+
const uv = [0, 0];
|
|
2676
|
+
const px = new Float64Array(8);
|
|
2677
|
+
const py = new Float64Array(8);
|
|
2678
|
+
const pz = new Float64Array(8);
|
|
2679
|
+
function crossFaceNeighbor(face, level, nx, ny, out) {
|
|
2680
|
+
const n = 1 << level;
|
|
2681
|
+
const u = (nx + 0.5) / n;
|
|
2682
|
+
const v = (ny + 0.5) / n;
|
|
2683
|
+
faceUVToCube(face, u, v, cube);
|
|
2684
|
+
const len = Math.hypot(cube[0], cube[1], cube[2]);
|
|
2685
|
+
const dir = [cube[0] / len, cube[1] / len, cube[2] / len];
|
|
2686
|
+
const nbrFace = directionToFace(dir);
|
|
2687
|
+
directionToFaceUV(nbrFace, dir, uv);
|
|
2688
|
+
let bx = Math.floor(uv[0] * n);
|
|
2689
|
+
let by = Math.floor(uv[1] * n);
|
|
2690
|
+
if (bx < 0) bx = 0;
|
|
2691
|
+
else if (bx > n - 1) bx = n - 1;
|
|
2692
|
+
if (by < 0) by = 0;
|
|
2693
|
+
else if (by > n - 1) by = n - 1;
|
|
2694
|
+
out.space = nbrFace;
|
|
2695
|
+
out.level = level;
|
|
2696
|
+
out.x = bx;
|
|
2697
|
+
out.y = by;
|
|
2698
|
+
}
|
|
2699
|
+
return {
|
|
2700
|
+
spaceCount: 6,
|
|
2701
|
+
maxRootCount: 6,
|
|
2702
|
+
projection: createCubeSphereProjection({ radius, center, invert: cfg.invert }),
|
|
2703
|
+
radius,
|
|
2704
|
+
center,
|
|
2705
|
+
neighborSameLevel(tile, dir, out) {
|
|
2706
|
+
const level = tile.level;
|
|
2707
|
+
const n = 1 << level;
|
|
2708
|
+
let nx = tile.x;
|
|
2709
|
+
let ny = tile.y;
|
|
2710
|
+
switch (dir) {
|
|
2711
|
+
case 0:
|
|
2712
|
+
nx -= 1;
|
|
2713
|
+
break;
|
|
2714
|
+
case 1:
|
|
2715
|
+
nx += 1;
|
|
2716
|
+
break;
|
|
2717
|
+
case 2:
|
|
2718
|
+
ny -= 1;
|
|
2719
|
+
break;
|
|
2720
|
+
case 3:
|
|
2721
|
+
ny += 1;
|
|
2722
|
+
break;
|
|
2723
|
+
}
|
|
2724
|
+
if (nx >= 0 && ny >= 0 && nx < n && ny < n) {
|
|
2725
|
+
out.space = tile.space;
|
|
2726
|
+
out.level = level;
|
|
2727
|
+
out.x = nx;
|
|
2728
|
+
out.y = ny;
|
|
2729
|
+
return true;
|
|
2730
|
+
}
|
|
2731
|
+
crossFaceNeighbor(tile.space, level, nx, ny, out);
|
|
2732
|
+
return true;
|
|
2733
|
+
},
|
|
2734
|
+
tileBounds(tile, cameraOrigin, out, elevationRange) {
|
|
2735
|
+
const level = tile.level;
|
|
2736
|
+
const n = 1 << level;
|
|
2737
|
+
const u0 = tile.x / n;
|
|
2738
|
+
const u1 = (tile.x + 1) / n;
|
|
2739
|
+
const v0 = tile.y / n;
|
|
2740
|
+
const v1 = (tile.y + 1) / n;
|
|
2741
|
+
const cornersU = [u0, u1, u0, u1];
|
|
2742
|
+
const cornersV = [v0, v0, v1, v1];
|
|
2743
|
+
const disps = elevationRange ? [elevationRange.min, elevationRange.max] : [0];
|
|
2744
|
+
let pointCount = 0;
|
|
2745
|
+
let sumX = 0;
|
|
2746
|
+
let sumY = 0;
|
|
2747
|
+
let sumZ = 0;
|
|
2748
|
+
for (let i = 0; i < 4; i++) {
|
|
2749
|
+
faceUVToCube(tile.space, cornersU[i], cornersV[i], cube);
|
|
2750
|
+
const len = Math.hypot(cube[0], cube[1], cube[2]);
|
|
2751
|
+
const dirX = cube[0] / len;
|
|
2752
|
+
const dirY = cube[1] / len;
|
|
2753
|
+
const dirZ = cube[2] / len;
|
|
2754
|
+
for (let di = 0; di < disps.length; di++) {
|
|
2755
|
+
const shellRadius = radius + disps[di];
|
|
2756
|
+
const sx = center.x + dirX * shellRadius;
|
|
2757
|
+
const sy = center.y + dirY * shellRadius;
|
|
2758
|
+
const sz = center.z + dirZ * shellRadius;
|
|
2759
|
+
px[pointCount] = sx;
|
|
2760
|
+
py[pointCount] = sy;
|
|
2761
|
+
pz[pointCount] = sz;
|
|
2762
|
+
sumX += sx;
|
|
2763
|
+
sumY += sy;
|
|
2764
|
+
sumZ += sz;
|
|
2765
|
+
pointCount += 1;
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
const cX = sumX / pointCount;
|
|
2769
|
+
const cY = sumY / pointCount;
|
|
2770
|
+
const cZ = sumZ / pointCount;
|
|
2771
|
+
let maxDistSq = 0;
|
|
2772
|
+
for (let i = 0; i < pointCount; i++) {
|
|
2773
|
+
const dx = px[i] - cX;
|
|
2774
|
+
const dy = py[i] - cY;
|
|
2775
|
+
const dz = pz[i] - cZ;
|
|
2776
|
+
const dSq = dx * dx + dy * dy + dz * dz;
|
|
2777
|
+
if (dSq > maxDistSq) maxDistSq = dSq;
|
|
2778
|
+
}
|
|
2779
|
+
out.cx = cX - cameraOrigin.x;
|
|
2780
|
+
out.cy = cY - cameraOrigin.y;
|
|
2781
|
+
out.cz = cZ - cameraOrigin.z;
|
|
2782
|
+
out.r = Math.sqrt(maxDistSq);
|
|
2783
|
+
},
|
|
2784
|
+
rootTiles(_cameraOrigin, out) {
|
|
2785
|
+
for (let s = 0; s < 6; s++) {
|
|
2786
|
+
const root = out[s];
|
|
2787
|
+
root.space = s;
|
|
2788
|
+
root.level = 0;
|
|
2789
|
+
root.x = 0;
|
|
2790
|
+
root.y = 0;
|
|
2791
|
+
}
|
|
2792
|
+
return 6;
|
|
2793
|
+
}
|
|
2794
|
+
};
|
|
2795
|
+
}
|
|
2796
|
+
|
|
2797
|
+
const TWO_PI$1 = Math.PI * 2;
|
|
2798
|
+
function torusPosition(geometry, u, v, displacement) {
|
|
2799
|
+
const theta = float(u).mul(TWO_PI$1);
|
|
2800
|
+
const phi = float(v).mul(TWO_PI$1);
|
|
2801
|
+
const sinT = sin(theta);
|
|
2802
|
+
const cosT = cos(theta);
|
|
2803
|
+
const sinP = sin(phi);
|
|
2804
|
+
const cosP = cos(phi);
|
|
2805
|
+
const disp = select(bool(geometry.invert), displacement.negate(), displacement);
|
|
2806
|
+
const tube = disp.add(float(geometry.minorRadius));
|
|
2807
|
+
const ring = tube.mul(cosP).add(float(geometry.majorRadius));
|
|
2808
|
+
return vec3(
|
|
2809
|
+
ring.mul(sinT).add(float(geometry.center.x)),
|
|
2810
|
+
tube.mul(sinP).add(float(geometry.center.y)),
|
|
2811
|
+
ring.mul(cosT).add(float(geometry.center.z))
|
|
2812
|
+
);
|
|
2813
|
+
}
|
|
2814
|
+
function torusOutwardNormal(u, v, invert) {
|
|
2815
|
+
const theta = float(u).mul(TWO_PI$1);
|
|
2816
|
+
const phi = float(v).mul(TWO_PI$1);
|
|
2817
|
+
const sinT = sin(theta);
|
|
2818
|
+
const cosT = cos(theta);
|
|
2819
|
+
const sinP = sin(phi);
|
|
2820
|
+
const cosP = cos(phi);
|
|
2821
|
+
const normal = vec3(cosP.mul(sinT), sinP, cosP.mul(cosT)).normalize();
|
|
2822
|
+
return select(bool(invert), normal.negate(), normal);
|
|
2823
|
+
}
|
|
2824
|
+
|
|
2825
|
+
const TWO_PI = Math.PI * 2;
|
|
2826
|
+
const RAYCAST_PADDING = 1;
|
|
2827
|
+
const ZERO_CENTER = { x: 0, y: 0, z: 0 };
|
|
2828
|
+
function createTorusTileComputeParts(ctx, geometry) {
|
|
2829
|
+
const { shared } = ctx;
|
|
2830
|
+
const tileSize = Fn(([nodeIndex]) => {
|
|
2831
|
+
const level = shared.tileLevel(nodeIndex);
|
|
2832
|
+
const levelScale = pow(float(2), level.toFloat());
|
|
2833
|
+
return float(TWO_PI * geometry.majorRadius).div(float(geometry.baseU).mul(levelScale));
|
|
2834
|
+
});
|
|
2835
|
+
const tileVertexWorldPosition = Fn(([nodeIndex, ix, iy]) => {
|
|
2836
|
+
const faceUV = shared.tileFaceUV(nodeIndex, ix, iy);
|
|
2837
|
+
return torusPosition(geometry, faceUV.x, faceUV.y, float(0));
|
|
2838
|
+
});
|
|
2839
|
+
return {
|
|
2840
|
+
tileSize: (nodeIndex) => tileSize(nodeIndex),
|
|
2841
|
+
rootUV: (nodeIndex, ix, iy) => shared.tileFaceUV(nodeIndex, ix, iy),
|
|
2842
|
+
tileVertexWorldPosition: (nodeIndex, ix, iy) => tileVertexWorldPosition(nodeIndex, ix, iy)
|
|
2843
|
+
};
|
|
2844
|
+
}
|
|
2845
|
+
function createTorusProjection(config) {
|
|
2846
|
+
const majorRadius = config.majorRadius;
|
|
2847
|
+
const minorRadius = config.minorRadius;
|
|
2848
|
+
const center = config.center ?? { x: 0, y: 0, z: 0 };
|
|
2849
|
+
const invert = config.invert ?? false;
|
|
2850
|
+
const baseU = config.baseU ?? 1;
|
|
2851
|
+
const baseV = config.baseV ?? 1;
|
|
2852
|
+
const geometry = { majorRadius, minorRadius, center, invert, baseU};
|
|
2853
|
+
const params = { u: 0, v: 0, tubeDistance: 0 };
|
|
2854
|
+
const normalScratch = [0, 0, 0];
|
|
2855
|
+
const posLeft = [0, 0, 0];
|
|
2856
|
+
const posRight = [0, 0, 0];
|
|
2857
|
+
const posUp = [0, 0, 0];
|
|
2858
|
+
const posDown = [0, 0, 0];
|
|
2859
|
+
const surfaceOps = {
|
|
2860
|
+
positionToKey(px, py, pz, out) {
|
|
2861
|
+
positionToTorusParams(px, py, pz, majorRadius, center, params);
|
|
2862
|
+
torusOutwardNormal$1(params.u, params.v, normalScratch, invert);
|
|
2863
|
+
out.space = 0;
|
|
2864
|
+
out.u = params.u;
|
|
2865
|
+
out.v = params.v;
|
|
2866
|
+
out.dirX = normalScratch[0];
|
|
2867
|
+
out.dirY = normalScratch[1];
|
|
2868
|
+
out.dirZ = normalScratch[2];
|
|
2869
|
+
return true;
|
|
2870
|
+
},
|
|
2871
|
+
surfacePosition(key, elevation, outVec) {
|
|
2872
|
+
torusUVToPoint(
|
|
2873
|
+
key.u,
|
|
2874
|
+
key.v,
|
|
2875
|
+
majorRadius,
|
|
2876
|
+
minorRadius,
|
|
2877
|
+
elevation,
|
|
2878
|
+
center,
|
|
2879
|
+
normalScratch,
|
|
2880
|
+
invert
|
|
2881
|
+
);
|
|
2882
|
+
outVec.set(normalScratch[0], normalScratch[1], normalScratch[2]);
|
|
2883
|
+
},
|
|
2884
|
+
surfaceNormal(key, ctx) {
|
|
2885
|
+
const scale = ctx.elevationScale;
|
|
2886
|
+
const levelScale = 2 ** ctx.level;
|
|
2887
|
+
const duvU = 1 / (ctx.innerTileSegments * baseU * levelScale);
|
|
2888
|
+
const duvV = 1 / (ctx.innerTileSegments * baseV * levelScale);
|
|
2889
|
+
const u = key.u;
|
|
2890
|
+
const v = key.v;
|
|
2891
|
+
const hLeft = sampleGridBilinear(ctx.elevation, ctx.shape, ctx.leafIndex, ctx.gx - 1, ctx.gy) * scale;
|
|
2892
|
+
const hRight = sampleGridBilinear(ctx.elevation, ctx.shape, ctx.leafIndex, ctx.gx + 1, ctx.gy) * scale;
|
|
2893
|
+
const hUp = sampleGridBilinear(ctx.elevation, ctx.shape, ctx.leafIndex, ctx.gx, ctx.gy - 1) * scale;
|
|
2894
|
+
const hDown = sampleGridBilinear(ctx.elevation, ctx.shape, ctx.leafIndex, ctx.gx, ctx.gy + 1) * scale;
|
|
2895
|
+
torusUVToPoint(u - duvU, v, majorRadius, minorRadius, hLeft, ZERO_CENTER, posLeft, invert);
|
|
2896
|
+
torusUVToPoint(u + duvU, v, majorRadius, minorRadius, hRight, ZERO_CENTER, posRight, invert);
|
|
2897
|
+
torusUVToPoint(u, v - duvV, majorRadius, minorRadius, hUp, ZERO_CENTER, posUp, invert);
|
|
2898
|
+
torusUVToPoint(u, v + duvV, majorRadius, minorRadius, hDown, ZERO_CENTER, posDown, invert);
|
|
2899
|
+
const tux = posRight[0] - posLeft[0];
|
|
2900
|
+
const tuy = posRight[1] - posLeft[1];
|
|
2901
|
+
const tuz = posRight[2] - posLeft[2];
|
|
2902
|
+
const tvx = posDown[0] - posUp[0];
|
|
2903
|
+
const tvy = posDown[1] - posUp[1];
|
|
2904
|
+
const tvz = posDown[2] - posUp[2];
|
|
2905
|
+
let nx = tuy * tvz - tuz * tvy;
|
|
2906
|
+
let ny = tuz * tvx - tux * tvz;
|
|
2907
|
+
let nz = tux * tvy - tuy * tvx;
|
|
2908
|
+
if (nx * key.dirX + ny * key.dirY + nz * key.dirZ < 0) {
|
|
2909
|
+
nx = -nx;
|
|
2910
|
+
ny = -ny;
|
|
2911
|
+
nz = -nz;
|
|
2912
|
+
}
|
|
2913
|
+
return new Vector3(nx, ny, nz).normalize();
|
|
2914
|
+
}
|
|
2915
|
+
};
|
|
1365
2916
|
return {
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
2917
|
+
kind: "torus",
|
|
2918
|
+
radius: majorRadius + minorRadius,
|
|
2919
|
+
center,
|
|
2920
|
+
faceOutward: !invert,
|
|
2921
|
+
baseResolution: { u: baseU, v: baseV },
|
|
2922
|
+
gpu: {
|
|
2923
|
+
renderVertexPosition(ctx) {
|
|
2924
|
+
return createCurvedRenderVertexPosition(
|
|
2925
|
+
ctx.leafStorage,
|
|
2926
|
+
ctx.uniforms,
|
|
2927
|
+
ctx.terrainFieldStorage,
|
|
2928
|
+
(_tile, faceUV, displacement) => torusPosition(geometry, faceUV.x, faceUV.y, displacement),
|
|
2929
|
+
baseU,
|
|
2930
|
+
baseV
|
|
2931
|
+
);
|
|
2932
|
+
},
|
|
2933
|
+
createTileComputeParts: (ctx) => createTorusTileComputeParts(ctx, geometry),
|
|
2934
|
+
createFieldNormal(ctx) {
|
|
2935
|
+
const computeNormal = createDisplacedSurfaceNormalFromElevationField(
|
|
2936
|
+
ctx.elevationFieldNode,
|
|
2937
|
+
ctx.edgeVertexCount,
|
|
2938
|
+
(nodeIndex) => ({
|
|
2939
|
+
positionAt: (gx, gy, height) => {
|
|
2940
|
+
const uv = ctx.tile.tileFaceUV(nodeIndex, gx, gy);
|
|
2941
|
+
return torusPosition(geometry, uv.x, uv.y, height);
|
|
2942
|
+
},
|
|
2943
|
+
dirAt: (gx, gy) => {
|
|
2944
|
+
const uv = ctx.tile.tileFaceUV(nodeIndex, gx, gy);
|
|
2945
|
+
return torusOutwardNormal(uv.x, uv.y, invert);
|
|
2946
|
+
}
|
|
2947
|
+
})
|
|
2948
|
+
);
|
|
2949
|
+
return (nodeIndex, ix, iy) => computeNormal(nodeIndex, ix, iy, ctx.uniforms.uElevationScale);
|
|
2950
|
+
}
|
|
2951
|
+
},
|
|
2952
|
+
cpu: {
|
|
2953
|
+
cameraSurfaceOffset(cam, elevation) {
|
|
2954
|
+
positionToTorusParams(cam.x, cam.y, cam.z, majorRadius, center, params);
|
|
2955
|
+
torusOutwardNormal$1(params.u, params.v, normalScratch, invert);
|
|
2956
|
+
cam.x -= normalScratch[0] * elevation;
|
|
2957
|
+
cam.y -= normalScratch[1] * elevation;
|
|
2958
|
+
cam.z -= normalScratch[2] * elevation;
|
|
2959
|
+
},
|
|
2960
|
+
createSurfaceOps() {
|
|
2961
|
+
return surfaceOps;
|
|
2962
|
+
},
|
|
2963
|
+
createRuntimeQueries(cache) {
|
|
2964
|
+
const query = createTerrainQuery(cache);
|
|
2965
|
+
const surfaceQuery = createTerrainSurfaceQuery(cache);
|
|
2966
|
+
return { query, surfaceQuery, sphereQuery: null };
|
|
2967
|
+
},
|
|
2968
|
+
raycast(ctx) {
|
|
2969
|
+
const range = ctx.terrainQuery?.getGlobalElevationRange();
|
|
2970
|
+
const dispMax = range ? Math.max(0, range.max - ctx.config.originY) : minorRadius * 0.5;
|
|
2971
|
+
const outerPadding = invert ? 0 : dispMax + RAYCAST_PADDING;
|
|
2972
|
+
const raycastParams = {
|
|
2973
|
+
centerX: center.x,
|
|
2974
|
+
centerY: center.y,
|
|
2975
|
+
centerZ: center.z,
|
|
2976
|
+
majorRadius,
|
|
2977
|
+
minorRadius,
|
|
2978
|
+
outerRadius: majorRadius + minorRadius + outerPadding,
|
|
2979
|
+
invert
|
|
2980
|
+
};
|
|
2981
|
+
if (ctx.surfaceQuery) {
|
|
2982
|
+
const precise = torusRaycast(ctx.surfaceQuery, ctx.ray, raycastParams, ctx.options);
|
|
2983
|
+
if (precise) return precise;
|
|
2984
|
+
}
|
|
2985
|
+
return torusRaycastBoundsOnly(ctx.ray, raycastParams, ctx.options);
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2988
|
+
};
|
|
2989
|
+
}
|
|
2990
|
+
|
|
2991
|
+
function createTorusTopology(cfg) {
|
|
2992
|
+
const majorRadius = cfg.majorRadius;
|
|
2993
|
+
const minorRadius = cfg.minorRadius;
|
|
2994
|
+
const center = cfg.center ?? { x: 0, y: 0, z: 0 };
|
|
2995
|
+
const invert = cfg.invert ?? false;
|
|
2996
|
+
const baseU = Math.max(1, Math.round(majorRadius / minorRadius));
|
|
2997
|
+
const baseV = 1;
|
|
2998
|
+
const corner = [0, 0, 0];
|
|
2999
|
+
const px = new Float64Array(18);
|
|
3000
|
+
const py = new Float64Array(18);
|
|
3001
|
+
const pz = new Float64Array(18);
|
|
3002
|
+
const wrap = (value, n) => (value % n + n) % n;
|
|
3003
|
+
const levelResolution = (level) => {
|
|
3004
|
+
const levelScale = 2 ** level;
|
|
3005
|
+
return { nU: baseU * levelScale, nV: baseV * levelScale };
|
|
3006
|
+
};
|
|
3007
|
+
return {
|
|
3008
|
+
spaceCount: 1,
|
|
3009
|
+
maxRootCount: baseU * baseV,
|
|
3010
|
+
projection: createTorusProjection({
|
|
3011
|
+
majorRadius,
|
|
3012
|
+
minorRadius,
|
|
3013
|
+
center,
|
|
3014
|
+
invert,
|
|
3015
|
+
baseU,
|
|
3016
|
+
baseV
|
|
3017
|
+
}),
|
|
3018
|
+
radius: majorRadius + minorRadius,
|
|
3019
|
+
center,
|
|
3020
|
+
neighborSameLevel(tile, dir, out) {
|
|
3021
|
+
const { nU, nV } = levelResolution(tile.level);
|
|
3022
|
+
let nx = tile.x;
|
|
3023
|
+
let ny = tile.y;
|
|
3024
|
+
switch (dir) {
|
|
3025
|
+
case Dir.LEFT:
|
|
3026
|
+
nx -= 1;
|
|
3027
|
+
break;
|
|
3028
|
+
case Dir.RIGHT:
|
|
3029
|
+
nx += 1;
|
|
3030
|
+
break;
|
|
3031
|
+
case Dir.TOP:
|
|
3032
|
+
ny -= 1;
|
|
3033
|
+
break;
|
|
3034
|
+
case Dir.BOTTOM:
|
|
3035
|
+
ny += 1;
|
|
3036
|
+
break;
|
|
3037
|
+
}
|
|
3038
|
+
out.space = 0;
|
|
3039
|
+
out.level = tile.level;
|
|
3040
|
+
out.x = wrap(nx, nU);
|
|
3041
|
+
out.y = wrap(ny, nV);
|
|
3042
|
+
return true;
|
|
1370
3043
|
},
|
|
1371
|
-
tileBounds(
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
3044
|
+
tileBounds(tile, cameraOrigin, out, elevationRange) {
|
|
3045
|
+
const { nU, nV } = levelResolution(tile.level);
|
|
3046
|
+
const u0 = tile.x / nU;
|
|
3047
|
+
const v0 = tile.y / nV;
|
|
3048
|
+
const stepU = 1 / nU;
|
|
3049
|
+
const stepV = 1 / nV;
|
|
3050
|
+
const disps = elevationRange ? [elevationRange.min, elevationRange.max] : [0];
|
|
3051
|
+
let pointCount = 0;
|
|
3052
|
+
let sumX = 0;
|
|
3053
|
+
let sumY = 0;
|
|
3054
|
+
let sumZ = 0;
|
|
3055
|
+
for (let sj = 0; sj <= 2; sj++) {
|
|
3056
|
+
for (let si = 0; si <= 2; si++) {
|
|
3057
|
+
const u = u0 + si * stepU / 2;
|
|
3058
|
+
const v = v0 + sj * stepV / 2;
|
|
3059
|
+
for (let di = 0; di < disps.length; di++) {
|
|
3060
|
+
torusUVToPoint(u, v, majorRadius, minorRadius, disps[di], center, corner, invert);
|
|
3061
|
+
px[pointCount] = corner[0];
|
|
3062
|
+
py[pointCount] = corner[1];
|
|
3063
|
+
pz[pointCount] = corner[2];
|
|
3064
|
+
sumX += corner[0];
|
|
3065
|
+
sumY += corner[1];
|
|
3066
|
+
sumZ += corner[2];
|
|
3067
|
+
pointCount += 1;
|
|
3068
|
+
}
|
|
3069
|
+
}
|
|
3070
|
+
}
|
|
3071
|
+
const cX = sumX / pointCount;
|
|
3072
|
+
const cY = sumY / pointCount;
|
|
3073
|
+
const cZ = sumZ / pointCount;
|
|
3074
|
+
let maxDistSq = 0;
|
|
3075
|
+
for (let i = 0; i < pointCount; i++) {
|
|
3076
|
+
const dx = px[i] - cX;
|
|
3077
|
+
const dy = py[i] - cY;
|
|
3078
|
+
const dz = pz[i] - cZ;
|
|
3079
|
+
const dSq = dx * dx + dy * dy + dz * dz;
|
|
3080
|
+
if (dSq > maxDistSq) maxDistSq = dSq;
|
|
3081
|
+
}
|
|
3082
|
+
out.cx = cX - cameraOrigin.x;
|
|
3083
|
+
out.cy = cY - cameraOrigin.y;
|
|
3084
|
+
out.cz = cZ - cameraOrigin.z;
|
|
3085
|
+
out.r = Math.sqrt(maxDistSq);
|
|
1376
3086
|
},
|
|
1377
3087
|
rootTiles(_cameraOrigin, out) {
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
3088
|
+
let count = 0;
|
|
3089
|
+
for (let y = 0; y < baseV; y++) {
|
|
3090
|
+
for (let x = 0; x < baseU; x++) {
|
|
3091
|
+
const root = out[count];
|
|
3092
|
+
root.space = 0;
|
|
3093
|
+
root.level = 0;
|
|
3094
|
+
root.x = x;
|
|
3095
|
+
root.y = y;
|
|
3096
|
+
count += 1;
|
|
3097
|
+
}
|
|
1384
3098
|
}
|
|
1385
|
-
return
|
|
3099
|
+
return count;
|
|
1386
3100
|
}
|
|
1387
3101
|
};
|
|
1388
3102
|
}
|
|
1389
3103
|
|
|
3104
|
+
function nextPow2(n) {
|
|
3105
|
+
let x = 1;
|
|
3106
|
+
while (x < n) x <<= 1;
|
|
3107
|
+
return x;
|
|
3108
|
+
}
|
|
3109
|
+
function mix32(x) {
|
|
3110
|
+
x >>>= 0;
|
|
3111
|
+
x ^= x >>> 16;
|
|
3112
|
+
x = Math.imul(x, 2146121005) >>> 0;
|
|
3113
|
+
x ^= x >>> 15;
|
|
3114
|
+
x = Math.imul(x, 2221713035) >>> 0;
|
|
3115
|
+
x ^= x >>> 16;
|
|
3116
|
+
return x >>> 0;
|
|
3117
|
+
}
|
|
3118
|
+
function hashKey(space, level, x, y) {
|
|
3119
|
+
const h = space & 255 ^ (level & 255) << 8 ^ mix32(x) >>> 0 ^ mix32(y) >>> 0;
|
|
3120
|
+
return mix32(h);
|
|
3121
|
+
}
|
|
3122
|
+
function createTileElevationPyramid(maxNodes, maxLevel) {
|
|
3123
|
+
const size = nextPow2(Math.max(2, maxNodes * (maxLevel + 1) * 2));
|
|
3124
|
+
return {
|
|
3125
|
+
size,
|
|
3126
|
+
mask: size - 1,
|
|
3127
|
+
stampGen: 1,
|
|
3128
|
+
stamp: new Uint16Array(size),
|
|
3129
|
+
keysSpace: new Uint8Array(size),
|
|
3130
|
+
keysLevel: new Uint8Array(size),
|
|
3131
|
+
keysX: new Uint32Array(size),
|
|
3132
|
+
keysY: new Uint32Array(size),
|
|
3133
|
+
mins: new Float32Array(size),
|
|
3134
|
+
maxs: new Float32Array(size)
|
|
3135
|
+
};
|
|
3136
|
+
}
|
|
3137
|
+
function beginPyramidGeneration(pyramid) {
|
|
3138
|
+
pyramid.stampGen = pyramid.stampGen + 1 & 65535;
|
|
3139
|
+
if (pyramid.stampGen === 0) {
|
|
3140
|
+
pyramid.stamp.fill(0);
|
|
3141
|
+
pyramid.stampGen = 1;
|
|
3142
|
+
}
|
|
3143
|
+
}
|
|
3144
|
+
function mergeRange(pyramid, space, level, x, y, min, max) {
|
|
3145
|
+
const s = space & 255;
|
|
3146
|
+
const l = level & 255;
|
|
3147
|
+
const xx = x >>> 0;
|
|
3148
|
+
const yy = y >>> 0;
|
|
3149
|
+
let slot = hashKey(s, l, xx, yy) & pyramid.mask;
|
|
3150
|
+
for (let probes = 0; probes < pyramid.size; probes++) {
|
|
3151
|
+
if (pyramid.stamp[slot] !== pyramid.stampGen) {
|
|
3152
|
+
pyramid.stamp[slot] = pyramid.stampGen;
|
|
3153
|
+
pyramid.keysSpace[slot] = s;
|
|
3154
|
+
pyramid.keysLevel[slot] = l;
|
|
3155
|
+
pyramid.keysX[slot] = xx;
|
|
3156
|
+
pyramid.keysY[slot] = yy;
|
|
3157
|
+
pyramid.mins[slot] = min;
|
|
3158
|
+
pyramid.maxs[slot] = max;
|
|
3159
|
+
return;
|
|
3160
|
+
}
|
|
3161
|
+
if (pyramid.keysSpace[slot] === s && pyramid.keysLevel[slot] === l && pyramid.keysX[slot] === xx && pyramid.keysY[slot] === yy) {
|
|
3162
|
+
if (min < pyramid.mins[slot]) pyramid.mins[slot] = min;
|
|
3163
|
+
if (max > pyramid.maxs[slot]) pyramid.maxs[slot] = max;
|
|
3164
|
+
return;
|
|
3165
|
+
}
|
|
3166
|
+
slot = slot + 1 & pyramid.mask;
|
|
3167
|
+
}
|
|
3168
|
+
throw new Error("TileElevationPyramid is full (no empty slot found).");
|
|
3169
|
+
}
|
|
3170
|
+
function buildTileElevationPyramid(pyramid, index, tileBounds, leafCount) {
|
|
3171
|
+
beginPyramidGeneration(pyramid);
|
|
3172
|
+
const stampGen = index.stampGen;
|
|
3173
|
+
for (let slot = 0; slot < index.size; slot++) {
|
|
3174
|
+
if (index.stamp[slot] !== stampGen) continue;
|
|
3175
|
+
const leafIndex = index.values[slot];
|
|
3176
|
+
if (leafIndex >= leafCount) continue;
|
|
3177
|
+
const space = index.keysSpace[slot];
|
|
3178
|
+
const level = index.keysLevel[slot];
|
|
3179
|
+
const x = index.keysX[slot];
|
|
3180
|
+
const y = index.keysY[slot];
|
|
3181
|
+
const rawMin = tileBounds[leafIndex * 2];
|
|
3182
|
+
const rawMax = tileBounds[leafIndex * 2 + 1];
|
|
3183
|
+
for (let ancestorLevel = level; ancestorLevel >= 0; ancestorLevel--) {
|
|
3184
|
+
const shift = level - ancestorLevel;
|
|
3185
|
+
mergeRange(
|
|
3186
|
+
pyramid,
|
|
3187
|
+
space,
|
|
3188
|
+
ancestorLevel,
|
|
3189
|
+
x >>> shift,
|
|
3190
|
+
y >>> shift,
|
|
3191
|
+
rawMin,
|
|
3192
|
+
rawMax
|
|
3193
|
+
);
|
|
3194
|
+
}
|
|
3195
|
+
}
|
|
3196
|
+
}
|
|
3197
|
+
function lookupTileElevationRange(pyramid, space, level, x, y, out) {
|
|
3198
|
+
const s = space & 255;
|
|
3199
|
+
const l = level & 255;
|
|
3200
|
+
const xx = x >>> 0;
|
|
3201
|
+
const yy = y >>> 0;
|
|
3202
|
+
let slot = hashKey(s, l, xx, yy) & pyramid.mask;
|
|
3203
|
+
for (let probes = 0; probes < pyramid.size; probes++) {
|
|
3204
|
+
if (pyramid.stamp[slot] !== pyramid.stampGen) return false;
|
|
3205
|
+
if (pyramid.keysSpace[slot] === s && pyramid.keysLevel[slot] === l && pyramid.keysX[slot] === xx && pyramid.keysY[slot] === yy) {
|
|
3206
|
+
out.min = pyramid.mins[slot];
|
|
3207
|
+
out.max = pyramid.maxs[slot];
|
|
3208
|
+
return true;
|
|
3209
|
+
}
|
|
3210
|
+
slot = slot + 1 & pyramid.mask;
|
|
3211
|
+
}
|
|
3212
|
+
return false;
|
|
3213
|
+
}
|
|
3214
|
+
|
|
3215
|
+
const MISSED_LOOKUP = Object.freeze({
|
|
3216
|
+
found: false,
|
|
3217
|
+
leafIndex: -1,
|
|
3218
|
+
space: -1,
|
|
3219
|
+
level: -1,
|
|
3220
|
+
tileX: -1,
|
|
3221
|
+
tileY: -1,
|
|
3222
|
+
tileSize: 0,
|
|
3223
|
+
localU: 0,
|
|
3224
|
+
localV: 0
|
|
3225
|
+
});
|
|
3226
|
+
function lookupTile(index, config, worldX, worldZ) {
|
|
3227
|
+
const halfRoot = config.rootSize * 0.5;
|
|
3228
|
+
for (let level = config.maxLevel; level >= 0; level -= 1) {
|
|
3229
|
+
const scale = 2 ** level;
|
|
3230
|
+
const tileSize = config.rootSize / scale;
|
|
3231
|
+
const tileX = Math.floor((worldX - config.originX + halfRoot) / tileSize);
|
|
3232
|
+
const tileY = Math.floor((worldZ - config.originZ + halfRoot) / tileSize);
|
|
3233
|
+
const leafIndex = lookupSpatialIndexRaw(index, 0, level, tileX, tileY);
|
|
3234
|
+
if (leafIndex !== U32_EMPTY) {
|
|
3235
|
+
const tileMinX = config.originX + tileX * tileSize - halfRoot;
|
|
3236
|
+
const tileMinZ = config.originZ + tileY * tileSize - halfRoot;
|
|
3237
|
+
return {
|
|
3238
|
+
found: true,
|
|
3239
|
+
leafIndex,
|
|
3240
|
+
space: 0,
|
|
3241
|
+
level,
|
|
3242
|
+
tileX,
|
|
3243
|
+
tileY,
|
|
3244
|
+
tileSize,
|
|
3245
|
+
localU: (worldX - tileMinX) / tileSize,
|
|
3246
|
+
localV: (worldZ - tileMinZ) / tileSize
|
|
3247
|
+
};
|
|
3248
|
+
}
|
|
3249
|
+
}
|
|
3250
|
+
return MISSED_LOOKUP;
|
|
3251
|
+
}
|
|
3252
|
+
function clamp01(value) {
|
|
3253
|
+
return value < 0 ? 0 : value > 1 ? 1 : value;
|
|
3254
|
+
}
|
|
3255
|
+
function lookupTileByFaceUV(index, config, face, u, v) {
|
|
3256
|
+
const baseU = config.baseU ?? 1;
|
|
3257
|
+
const baseV = config.baseV ?? 1;
|
|
3258
|
+
for (let level = config.maxLevel; level >= 0; level -= 1) {
|
|
3259
|
+
const levelScale = 2 ** level;
|
|
3260
|
+
const nU = baseU * levelScale;
|
|
3261
|
+
const nV = baseV * levelScale;
|
|
3262
|
+
let tileX = Math.floor(u * nU);
|
|
3263
|
+
let tileY = Math.floor(v * nV);
|
|
3264
|
+
if (tileX < 0) tileX = 0;
|
|
3265
|
+
else if (tileX > nU - 1) tileX = nU - 1;
|
|
3266
|
+
if (tileY < 0) tileY = 0;
|
|
3267
|
+
else if (tileY > nV - 1) tileY = nV - 1;
|
|
3268
|
+
const leafIndex = lookupSpatialIndexRaw(index, face, level, tileX, tileY);
|
|
3269
|
+
if (leafIndex !== U32_EMPTY) {
|
|
3270
|
+
const tileSize = sphereTileArcLength(config.radius, nU);
|
|
3271
|
+
return {
|
|
3272
|
+
found: true,
|
|
3273
|
+
leafIndex,
|
|
3274
|
+
space: face,
|
|
3275
|
+
level,
|
|
3276
|
+
tileX,
|
|
3277
|
+
tileY,
|
|
3278
|
+
tileSize,
|
|
3279
|
+
localU: clamp01(u * nU - tileX),
|
|
3280
|
+
localV: clamp01(v * nV - tileY)
|
|
3281
|
+
};
|
|
3282
|
+
}
|
|
3283
|
+
}
|
|
3284
|
+
return MISSED_LOOKUP;
|
|
3285
|
+
}
|
|
3286
|
+
|
|
3287
|
+
function createReadbackSlot() {
|
|
3288
|
+
return { buffer: null, size: 0 };
|
|
3289
|
+
}
|
|
3290
|
+
function getBackend(renderer) {
|
|
3291
|
+
return renderer.backend;
|
|
3292
|
+
}
|
|
3293
|
+
function canDeviceReadback(renderer) {
|
|
3294
|
+
const backend = getBackend(renderer);
|
|
3295
|
+
return Boolean(backend?.device) && typeof backend?.get === "function";
|
|
3296
|
+
}
|
|
3297
|
+
async function readStorageBufferInto(renderer, attribute, slot, target, elementCount, label) {
|
|
3298
|
+
const backend = getBackend(renderer);
|
|
3299
|
+
const device = backend?.device;
|
|
3300
|
+
const source = backend?.get?.(attribute)?.buffer;
|
|
3301
|
+
if (!device || !source) return false;
|
|
3302
|
+
const requestedBytes = elementCount * Float32Array.BYTES_PER_ELEMENT;
|
|
3303
|
+
const copyBytes = Math.min(requestedBytes, source.size);
|
|
3304
|
+
if (copyBytes <= 0) return true;
|
|
3305
|
+
if (!slot.buffer || slot.size !== source.size) {
|
|
3306
|
+
slot.buffer?.destroy();
|
|
3307
|
+
slot.buffer = device.createBuffer({
|
|
3308
|
+
label,
|
|
3309
|
+
size: source.size,
|
|
3310
|
+
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
|
|
3311
|
+
});
|
|
3312
|
+
slot.size = source.size;
|
|
3313
|
+
}
|
|
3314
|
+
const staging = slot.buffer;
|
|
3315
|
+
const encoder = device.createCommandEncoder({ label });
|
|
3316
|
+
encoder.copyBufferToBuffer(source, 0, staging, 0, copyBytes);
|
|
3317
|
+
device.queue.submit([encoder.finish()]);
|
|
3318
|
+
await staging.mapAsync(GPUMapMode.READ, 0, copyBytes);
|
|
3319
|
+
const mapped = new Float32Array(staging.getMappedRange(0, copyBytes));
|
|
3320
|
+
target.set(mapped);
|
|
3321
|
+
staging.unmap();
|
|
3322
|
+
return true;
|
|
3323
|
+
}
|
|
3324
|
+
function disposeReadbackSlot(slot) {
|
|
3325
|
+
slot.buffer?.destroy();
|
|
3326
|
+
slot.buffer = null;
|
|
3327
|
+
slot.size = 0;
|
|
3328
|
+
}
|
|
3329
|
+
|
|
3330
|
+
function createTerrainSnapshotState(maxNodes, maxLevel, totalElements) {
|
|
3331
|
+
return {
|
|
3332
|
+
frontElevation: new Float32Array(totalElements),
|
|
3333
|
+
backElevation: new Float32Array(totalElements),
|
|
3334
|
+
frontIndex: createSpatialIndex(maxNodes),
|
|
3335
|
+
backIndex: createSpatialIndex(maxNodes),
|
|
3336
|
+
frontTileBounds: new Float32Array(maxNodes * 2),
|
|
3337
|
+
backTileBounds: new Float32Array(maxNodes * 2),
|
|
3338
|
+
frontLeafCount: 0,
|
|
3339
|
+
globalRange: null,
|
|
3340
|
+
hasSnapshot: false,
|
|
3341
|
+
readbackPending: false,
|
|
3342
|
+
generation: 0,
|
|
3343
|
+
lastScheduledStampGen: -1,
|
|
3344
|
+
elevationReadback: createReadbackSlot(),
|
|
3345
|
+
boundsReadback: createReadbackSlot(),
|
|
3346
|
+
elevationPyramid: createTileElevationPyramid(maxNodes, maxLevel)
|
|
3347
|
+
};
|
|
3348
|
+
}
|
|
1390
3349
|
function cloneSpatialIndex(target, source) {
|
|
1391
3350
|
if (target.size !== source.size) {
|
|
1392
3351
|
throw new Error(
|
|
@@ -1402,218 +3361,308 @@ function cloneSpatialIndex(target, source) {
|
|
|
1402
3361
|
target.keysY.set(source.keysY);
|
|
1403
3362
|
target.values.set(source.values);
|
|
1404
3363
|
}
|
|
1405
|
-
function
|
|
1406
|
-
|
|
1407
|
-
|
|
3364
|
+
function triggerSnapshotReadback(state, renderer, attribute, spatialIndex, boundsAttribute, captured) {
|
|
3365
|
+
if (state.readbackPending) return;
|
|
3366
|
+
const withReadback = renderer;
|
|
3367
|
+
const useDeviceReadback = canDeviceReadback(renderer);
|
|
3368
|
+
if (!useDeviceReadback && !withReadback.getArrayBufferAsync) return;
|
|
3369
|
+
if (spatialIndex.stampGen === state.lastScheduledStampGen) return;
|
|
3370
|
+
cloneSpatialIndex(state.backIndex, spatialIndex);
|
|
3371
|
+
state.lastScheduledStampGen = spatialIndex.stampGen;
|
|
3372
|
+
const { activeLeafCount, totalElements, verticesPerNode, elevationScale, originY } = captured;
|
|
3373
|
+
state.readbackPending = true;
|
|
3374
|
+
const applySnapshot = (boundsFilled) => {
|
|
3375
|
+
let boundsValid = activeLeafCount === 0;
|
|
3376
|
+
if (boundsFilled) {
|
|
3377
|
+
for (let i = 0; i < activeLeafCount; i += 1) {
|
|
3378
|
+
if ((state.backTileBounds[i * 2 + 1] ?? 0) !== 0) {
|
|
3379
|
+
boundsValid = true;
|
|
3380
|
+
break;
|
|
3381
|
+
}
|
|
3382
|
+
}
|
|
3383
|
+
}
|
|
3384
|
+
const oldFrontElevation = state.frontElevation;
|
|
3385
|
+
const oldFrontIndex = state.frontIndex;
|
|
3386
|
+
state.frontElevation = state.backElevation;
|
|
3387
|
+
state.frontIndex = state.backIndex;
|
|
3388
|
+
state.frontLeafCount = activeLeafCount;
|
|
3389
|
+
state.backElevation = oldFrontElevation;
|
|
3390
|
+
state.backIndex = oldFrontIndex;
|
|
3391
|
+
if (boundsFilled && boundsValid) {
|
|
3392
|
+
const oldFrontBounds = state.frontTileBounds;
|
|
3393
|
+
state.frontTileBounds = state.backTileBounds;
|
|
3394
|
+
state.backTileBounds = oldFrontBounds;
|
|
3395
|
+
}
|
|
3396
|
+
if (boundsFilled && boundsValid && activeLeafCount > 0) {
|
|
3397
|
+
let gMin = Infinity;
|
|
3398
|
+
let gMax = -Infinity;
|
|
3399
|
+
for (let i = 0; i < activeLeafCount; i++) {
|
|
3400
|
+
const rawMin = state.frontTileBounds[i * 2];
|
|
3401
|
+
const rawMax = state.frontTileBounds[i * 2 + 1];
|
|
3402
|
+
const a = originY + rawMin * elevationScale;
|
|
3403
|
+
const b = originY + rawMax * elevationScale;
|
|
3404
|
+
gMin = Math.min(gMin, a, b);
|
|
3405
|
+
gMax = Math.max(gMax, a, b);
|
|
3406
|
+
}
|
|
3407
|
+
state.globalRange = { min: gMin, max: gMax };
|
|
3408
|
+
buildTileElevationPyramid(
|
|
3409
|
+
state.elevationPyramid,
|
|
3410
|
+
state.frontIndex,
|
|
3411
|
+
state.frontTileBounds,
|
|
3412
|
+
activeLeafCount
|
|
3413
|
+
);
|
|
3414
|
+
}
|
|
3415
|
+
state.hasSnapshot = true;
|
|
3416
|
+
state.generation += 1;
|
|
3417
|
+
};
|
|
3418
|
+
if (useDeviceReadback) {
|
|
3419
|
+
const runDeviceReadback = async () => {
|
|
3420
|
+
state.backElevation.fill(0);
|
|
3421
|
+
await readStorageBufferInto(
|
|
3422
|
+
renderer,
|
|
3423
|
+
attribute,
|
|
3424
|
+
state.elevationReadback,
|
|
3425
|
+
state.backElevation,
|
|
3426
|
+
activeLeafCount * verticesPerNode,
|
|
3427
|
+
"terrainElevationReadback"
|
|
3428
|
+
);
|
|
3429
|
+
let boundsFilled = false;
|
|
3430
|
+
if (boundsAttribute) {
|
|
3431
|
+
state.backTileBounds.fill(0);
|
|
3432
|
+
boundsFilled = await readStorageBufferInto(
|
|
3433
|
+
renderer,
|
|
3434
|
+
boundsAttribute,
|
|
3435
|
+
state.boundsReadback,
|
|
3436
|
+
state.backTileBounds,
|
|
3437
|
+
activeLeafCount * 2,
|
|
3438
|
+
"terrainBoundsReadback"
|
|
3439
|
+
);
|
|
3440
|
+
}
|
|
3441
|
+
applySnapshot(boundsFilled);
|
|
3442
|
+
};
|
|
3443
|
+
runDeviceReadback().finally(() => {
|
|
3444
|
+
state.readbackPending = false;
|
|
3445
|
+
});
|
|
3446
|
+
return;
|
|
3447
|
+
}
|
|
3448
|
+
const onComplete = (elevResult, boundsResult) => {
|
|
3449
|
+
const data = new Float32Array(elevResult);
|
|
3450
|
+
state.backElevation.fill(0);
|
|
3451
|
+
state.backElevation.set(data.subarray(0, totalElements));
|
|
3452
|
+
let boundsFilled = false;
|
|
3453
|
+
if (boundsResult) {
|
|
3454
|
+
const rawBounds = new Float32Array(boundsResult);
|
|
3455
|
+
state.backTileBounds.fill(0);
|
|
3456
|
+
state.backTileBounds.set(rawBounds.subarray(0, activeLeafCount * 2));
|
|
3457
|
+
boundsFilled = true;
|
|
3458
|
+
}
|
|
3459
|
+
applySnapshot(boundsFilled);
|
|
3460
|
+
};
|
|
3461
|
+
const elevationPromise = withReadback.getArrayBufferAsync(attribute);
|
|
3462
|
+
const boundsPromise = boundsAttribute ? withReadback.getArrayBufferAsync(boundsAttribute) : null;
|
|
3463
|
+
if (boundsPromise) {
|
|
3464
|
+
Promise.all([elevationPromise, boundsPromise]).then(([elev, bounds]) => onComplete(elev, bounds)).finally(() => {
|
|
3465
|
+
state.readbackPending = false;
|
|
3466
|
+
});
|
|
3467
|
+
} else {
|
|
3468
|
+
elevationPromise.then((elev) => onComplete(elev, null)).finally(() => {
|
|
3469
|
+
state.readbackPending = false;
|
|
3470
|
+
});
|
|
3471
|
+
}
|
|
1408
3472
|
}
|
|
1409
|
-
function
|
|
3473
|
+
function disposeSnapshotReadback(state) {
|
|
3474
|
+
disposeReadbackSlot(state.elevationReadback);
|
|
3475
|
+
disposeReadbackSlot(state.boundsReadback);
|
|
3476
|
+
}
|
|
3477
|
+
|
|
3478
|
+
function createCpuTerrainCache(maxNodes, initialConfig, surfaceOps) {
|
|
1410
3479
|
let config = initialConfig;
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
let frontElevation = new Float32Array(totalElements);
|
|
1415
|
-
let backElevation = new Float32Array(totalElements);
|
|
1416
|
-
let frontIndex = createSpatialIndex(maxNodes);
|
|
1417
|
-
let backIndex = createSpatialIndex(maxNodes);
|
|
1418
|
-
let frontTileBounds = new Float32Array(maxNodes * 2);
|
|
1419
|
-
let backTileBounds = new Float32Array(maxNodes * 2);
|
|
1420
|
-
let frontLeafCount = 0;
|
|
1421
|
-
let globalRange = null;
|
|
1422
|
-
let hasSnapshot = false;
|
|
1423
|
-
let readbackPending = false;
|
|
1424
|
-
let generationCount = 0;
|
|
1425
|
-
let lastScheduledStampGen = -1;
|
|
1426
|
-
const readHeight = (leafIndex, ix, iy) => {
|
|
1427
|
-
const base = leafIndex * verticesPerNode;
|
|
1428
|
-
return frontElevation[base + iy * edgeVertexCount + ix] ?? 0;
|
|
3480
|
+
const shape = {
|
|
3481
|
+
edgeVertexCount: config.innerTileSegments + 3,
|
|
3482
|
+
verticesPerNode: 0
|
|
1429
3483
|
};
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
3484
|
+
shape.verticesPerNode = shape.edgeVertexCount * shape.edgeVertexCount;
|
|
3485
|
+
let totalElements = maxNodes * shape.verticesPerNode;
|
|
3486
|
+
const state = createTerrainSnapshotState(
|
|
3487
|
+
maxNodes,
|
|
3488
|
+
initialConfig.maxLevel,
|
|
3489
|
+
totalElements
|
|
3490
|
+
);
|
|
3491
|
+
const gridScratch = { gx: 0, gy: 0 };
|
|
3492
|
+
const gradientScratch = { dhdu: 0, dhdv: 0 };
|
|
3493
|
+
const keyScratch = { space: 0, u: 0, v: 0, dirX: 0, dirY: 0, dirZ: 0 };
|
|
3494
|
+
const surfaceLookupConfig = () => ({
|
|
3495
|
+
rootSize: config.rootSize,
|
|
3496
|
+
originX: config.originX,
|
|
3497
|
+
originZ: config.originZ,
|
|
3498
|
+
maxLevel: config.maxLevel,
|
|
3499
|
+
radius: config.radius,
|
|
3500
|
+
baseU: config.baseU,
|
|
3501
|
+
baseV: config.baseV
|
|
3502
|
+
});
|
|
3503
|
+
const gridCoordsFromLookup = (lookup) => {
|
|
3504
|
+
const fieldU = tileLocalToFieldUVNumber(lookup.localU, config.innerTileSegments);
|
|
3505
|
+
const fieldV = tileLocalToFieldUVNumber(lookup.localV, config.innerTileSegments);
|
|
3506
|
+
gridScratch.gx = fieldU * (shape.edgeVertexCount - 1);
|
|
3507
|
+
gridScratch.gy = fieldV * (shape.edgeVertexCount - 1);
|
|
3508
|
+
return gridScratch;
|
|
3509
|
+
};
|
|
3510
|
+
const rawHeightFromLookup = (lookup) => {
|
|
3511
|
+
const g = gridCoordsFromLookup(lookup);
|
|
3512
|
+
return sampleGridBilinear(state.frontElevation, shape, lookup.leafIndex, g.gx, g.gy);
|
|
1447
3513
|
};
|
|
1448
3514
|
const computeNormal = (leafIndex, gx, gy, tileSize) => {
|
|
1449
|
-
const hLeft = sampleGridBilinear(leafIndex, gx - 1, gy);
|
|
1450
|
-
const hRight = sampleGridBilinear(leafIndex, gx + 1, gy);
|
|
1451
|
-
const hUp = sampleGridBilinear(leafIndex, gx, gy - 1);
|
|
1452
|
-
const hDown = sampleGridBilinear(leafIndex, gx, gy + 1);
|
|
1453
3515
|
const stepWorld = tileSize / config.innerTileSegments;
|
|
1454
|
-
const
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
const tileY = Math.floor((worldZ - config.originZ + halfRoot) / tileSize);
|
|
1466
|
-
const leafIndex = lookupSpatialIndexRaw(
|
|
1467
|
-
frontIndex,
|
|
1468
|
-
0,
|
|
1469
|
-
level,
|
|
1470
|
-
tileX,
|
|
1471
|
-
tileY
|
|
1472
|
-
);
|
|
1473
|
-
if (leafIndex !== U32_EMPTY) {
|
|
1474
|
-
const tileMinX = config.originX + tileX * tileSize - halfRoot;
|
|
1475
|
-
const tileMinZ = config.originZ + tileY * tileSize - halfRoot;
|
|
1476
|
-
return {
|
|
1477
|
-
found: true,
|
|
1478
|
-
leafIndex,
|
|
1479
|
-
level,
|
|
1480
|
-
tileX,
|
|
1481
|
-
tileY,
|
|
1482
|
-
tileSize,
|
|
1483
|
-
localU: (worldX - tileMinX) / tileSize,
|
|
1484
|
-
localV: (worldZ - tileMinZ) / tileSize
|
|
1485
|
-
};
|
|
1486
|
-
}
|
|
1487
|
-
}
|
|
1488
|
-
return {
|
|
1489
|
-
found: false,
|
|
1490
|
-
leafIndex: -1,
|
|
1491
|
-
level: -1,
|
|
1492
|
-
tileX: -1,
|
|
1493
|
-
tileY: -1,
|
|
1494
|
-
tileSize: 0,
|
|
1495
|
-
localU: 0,
|
|
1496
|
-
localV: 0
|
|
1497
|
-
};
|
|
3516
|
+
const { dhdu, dhdv } = elevationGradientAt(
|
|
3517
|
+
state.frontElevation,
|
|
3518
|
+
shape,
|
|
3519
|
+
leafIndex,
|
|
3520
|
+
gx,
|
|
3521
|
+
gy,
|
|
3522
|
+
stepWorld,
|
|
3523
|
+
config.elevationScale,
|
|
3524
|
+
gradientScratch
|
|
3525
|
+
);
|
|
3526
|
+
return new Vector3(-dhdu, 1, -dhdv).normalize();
|
|
1498
3527
|
};
|
|
1499
3528
|
const sampleFromLookup = (lookup) => {
|
|
1500
|
-
const
|
|
1501
|
-
const fieldV = tileLocalToFieldUV(lookup.localV, config.innerTileSegments);
|
|
1502
|
-
const gx = fieldU * (edgeVertexCount - 1);
|
|
1503
|
-
const gy = fieldV * (edgeVertexCount - 1);
|
|
1504
|
-
const height = sampleGridBilinear(lookup.leafIndex, gx, gy);
|
|
3529
|
+
const height = rawHeightFromLookup(lookup);
|
|
1505
3530
|
const scaledHeight = config.originY + height * config.elevationScale;
|
|
1506
|
-
const normal = computeNormal(lookup.leafIndex, gx, gy, lookup.tileSize);
|
|
3531
|
+
const normal = computeNormal(lookup.leafIndex, gridScratch.gx, gridScratch.gy, lookup.tileSize);
|
|
1507
3532
|
return { elevation: scaledHeight, normal, valid: true };
|
|
1508
3533
|
};
|
|
1509
|
-
const sampleElevationFromLookup = (lookup) => {
|
|
1510
|
-
const fieldU = tileLocalToFieldUV(lookup.localU, config.innerTileSegments);
|
|
1511
|
-
const fieldV = tileLocalToFieldUV(lookup.localV, config.innerTileSegments);
|
|
1512
|
-
const gx = fieldU * (edgeVertexCount - 1);
|
|
1513
|
-
const gy = fieldV * (edgeVertexCount - 1);
|
|
1514
|
-
const height = sampleGridBilinear(lookup.leafIndex, gx, gy);
|
|
1515
|
-
const scaledHeight = config.originY + height * config.elevationScale;
|
|
1516
|
-
return { elevation: scaledHeight, valid: true };
|
|
1517
|
-
};
|
|
1518
3534
|
const sampleTerrain = (worldX, worldZ) => {
|
|
1519
|
-
if (!hasSnapshot) {
|
|
3535
|
+
if (!state.hasSnapshot) {
|
|
1520
3536
|
return { elevation: 0, normal: new Vector3(0, 1, 0), valid: false };
|
|
1521
3537
|
}
|
|
1522
|
-
const lookup = lookupTile(worldX, worldZ);
|
|
3538
|
+
const lookup = lookupTile(state.frontIndex, config, worldX, worldZ);
|
|
1523
3539
|
if (!lookup.found) {
|
|
1524
3540
|
return { elevation: 0, normal: new Vector3(0, 1, 0), valid: false };
|
|
1525
3541
|
}
|
|
1526
3542
|
return sampleFromLookup(lookup);
|
|
1527
3543
|
};
|
|
1528
3544
|
const getElevation = (worldX, worldZ) => {
|
|
1529
|
-
if (!hasSnapshot) {
|
|
3545
|
+
if (!state.hasSnapshot) {
|
|
1530
3546
|
return { elevation: 0, valid: false };
|
|
1531
3547
|
}
|
|
1532
|
-
const lookup = lookupTile(worldX, worldZ);
|
|
3548
|
+
const lookup = lookupTile(state.frontIndex, config, worldX, worldZ);
|
|
1533
3549
|
if (!lookup.found) {
|
|
1534
3550
|
return { elevation: 0, valid: false };
|
|
1535
3551
|
}
|
|
1536
|
-
|
|
3552
|
+
const height = rawHeightFromLookup(lookup);
|
|
3553
|
+
return {
|
|
3554
|
+
elevation: config.originY + height * config.elevationScale,
|
|
3555
|
+
valid: true
|
|
3556
|
+
};
|
|
3557
|
+
};
|
|
3558
|
+
const tileFromLookup = (lookup) => {
|
|
3559
|
+
if (!lookup.found) return null;
|
|
3560
|
+
return {
|
|
3561
|
+
space: lookup.space,
|
|
3562
|
+
level: lookup.level,
|
|
3563
|
+
x: lookup.tileX,
|
|
3564
|
+
y: lookup.tileY,
|
|
3565
|
+
index: lookup.leafIndex
|
|
3566
|
+
};
|
|
3567
|
+
};
|
|
3568
|
+
const tileBoundsFromLookup = (lookup, elevationBase) => {
|
|
3569
|
+
if (!lookup.found || lookup.leafIndex >= state.frontLeafCount) return null;
|
|
3570
|
+
const rawMin = state.frontTileBounds[lookup.leafIndex * 2];
|
|
3571
|
+
const rawMax = state.frontTileBounds[lookup.leafIndex * 2 + 1];
|
|
3572
|
+
const a = elevationBase + rawMin * config.elevationScale;
|
|
3573
|
+
const b = elevationBase + rawMax * config.elevationScale;
|
|
3574
|
+
return {
|
|
3575
|
+
space: lookup.space,
|
|
3576
|
+
level: lookup.level,
|
|
3577
|
+
x: lookup.tileX,
|
|
3578
|
+
y: lookup.tileY,
|
|
3579
|
+
index: lookup.leafIndex,
|
|
3580
|
+
minElevation: Math.min(a, b),
|
|
3581
|
+
maxElevation: Math.max(a, b)
|
|
3582
|
+
};
|
|
3583
|
+
};
|
|
3584
|
+
const invalidSurfaceSample = (dx, dy, dz) => ({
|
|
3585
|
+
position: new Vector3(),
|
|
3586
|
+
normal: new Vector3(0, 1, 0),
|
|
3587
|
+
direction: new Vector3(dx, dy, dz),
|
|
3588
|
+
elevation: 0,
|
|
3589
|
+
valid: false
|
|
3590
|
+
});
|
|
3591
|
+
const surfaceLookup = (px, py, pz) => {
|
|
3592
|
+
if (!surfaceOps || !surfaceOps.positionToKey(px, py, pz, keyScratch)) {
|
|
3593
|
+
return { found: false };
|
|
3594
|
+
}
|
|
3595
|
+
return lookupTileByFaceUV(
|
|
3596
|
+
state.frontIndex,
|
|
3597
|
+
surfaceLookupConfig(),
|
|
3598
|
+
keyScratch.space,
|
|
3599
|
+
keyScratch.u,
|
|
3600
|
+
keyScratch.v
|
|
3601
|
+
);
|
|
3602
|
+
};
|
|
3603
|
+
const sampleSurfaceByPosition = (px, py, pz) => {
|
|
3604
|
+
if (!state.hasSnapshot || !surfaceOps) return invalidSurfaceSample(0, 1, 0);
|
|
3605
|
+
if (!surfaceOps.positionToKey(px, py, pz, keyScratch)) {
|
|
3606
|
+
return invalidSurfaceSample(0, 1, 0);
|
|
3607
|
+
}
|
|
3608
|
+
const key = keyScratch;
|
|
3609
|
+
const lookup = lookupTileByFaceUV(
|
|
3610
|
+
state.frontIndex,
|
|
3611
|
+
surfaceLookupConfig(),
|
|
3612
|
+
key.space,
|
|
3613
|
+
key.u,
|
|
3614
|
+
key.v
|
|
3615
|
+
);
|
|
3616
|
+
if (!lookup.found) return invalidSurfaceSample(key.dirX, key.dirY, key.dirZ);
|
|
3617
|
+
const height = rawHeightFromLookup(lookup);
|
|
3618
|
+
const elevation = height * config.elevationScale;
|
|
3619
|
+
const position = new Vector3();
|
|
3620
|
+
surfaceOps.surfacePosition(key, elevation, position);
|
|
3621
|
+
const normal = surfaceOps.surfaceNormal(key, {
|
|
3622
|
+
elevation: state.frontElevation,
|
|
3623
|
+
shape,
|
|
3624
|
+
leafIndex: lookup.leafIndex,
|
|
3625
|
+
gx: gridScratch.gx,
|
|
3626
|
+
gy: gridScratch.gy,
|
|
3627
|
+
innerTileSegments: config.innerTileSegments,
|
|
3628
|
+
elevationScale: config.elevationScale,
|
|
3629
|
+
level: lookup.level
|
|
3630
|
+
});
|
|
3631
|
+
return {
|
|
3632
|
+
position,
|
|
3633
|
+
normal,
|
|
3634
|
+
direction: new Vector3(key.dirX, key.dirY, key.dirZ),
|
|
3635
|
+
elevation,
|
|
3636
|
+
valid: true
|
|
3637
|
+
};
|
|
1537
3638
|
};
|
|
1538
3639
|
const api = {
|
|
1539
3640
|
get generation() {
|
|
1540
|
-
return
|
|
3641
|
+
return state.generation;
|
|
1541
3642
|
},
|
|
1542
3643
|
get ready() {
|
|
1543
|
-
return hasSnapshot;
|
|
3644
|
+
return state.hasSnapshot;
|
|
3645
|
+
},
|
|
3646
|
+
get hasSurface() {
|
|
3647
|
+
return surfaceOps !== null;
|
|
1544
3648
|
},
|
|
1545
3649
|
updateConfig(nextConfig) {
|
|
1546
3650
|
config = nextConfig;
|
|
1547
|
-
edgeVertexCount = config.innerTileSegments + 3;
|
|
1548
|
-
verticesPerNode = edgeVertexCount * edgeVertexCount;
|
|
1549
|
-
totalElements = maxNodes * verticesPerNode;
|
|
3651
|
+
shape.edgeVertexCount = config.innerTileSegments + 3;
|
|
3652
|
+
shape.verticesPerNode = shape.edgeVertexCount * shape.edgeVertexCount;
|
|
3653
|
+
totalElements = maxNodes * shape.verticesPerNode;
|
|
1550
3654
|
},
|
|
1551
3655
|
triggerReadback(renderer, attribute, spatialIndex, boundsAttribute, activeLeafCount) {
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
const elevationPromise = withReadback.getArrayBufferAsync(attribute);
|
|
1563
|
-
const boundsPromise = boundsAttribute ? withReadback.getArrayBufferAsync(boundsAttribute) : null;
|
|
1564
|
-
const onComplete = (elevResult, boundsResult) => {
|
|
1565
|
-
const data = new Float32Array(elevResult);
|
|
1566
|
-
backElevation.fill(0);
|
|
1567
|
-
backElevation.set(data.subarray(0, totalElements));
|
|
1568
|
-
let boundsValid = capturedLeafCount === 0;
|
|
1569
|
-
if (boundsResult) {
|
|
1570
|
-
const rawBounds = new Float32Array(boundsResult);
|
|
1571
|
-
backTileBounds.fill(0);
|
|
1572
|
-
backTileBounds.set(rawBounds.subarray(0, capturedLeafCount * 2));
|
|
1573
|
-
for (let i = 0; i < capturedLeafCount; i += 1) {
|
|
1574
|
-
if ((rawBounds[i * 2 + 1] ?? 0) !== 0) {
|
|
1575
|
-
boundsValid = true;
|
|
1576
|
-
break;
|
|
1577
|
-
}
|
|
1578
|
-
}
|
|
1579
|
-
}
|
|
1580
|
-
const oldFrontElevation = frontElevation;
|
|
1581
|
-
const oldFrontIndex = frontIndex;
|
|
1582
|
-
frontElevation = backElevation;
|
|
1583
|
-
frontIndex = backIndex;
|
|
1584
|
-
frontLeafCount = capturedLeafCount;
|
|
1585
|
-
backElevation = oldFrontElevation;
|
|
1586
|
-
backIndex = oldFrontIndex;
|
|
1587
|
-
if (boundsResult && boundsValid) {
|
|
1588
|
-
const oldFrontBounds = frontTileBounds;
|
|
1589
|
-
frontTileBounds = backTileBounds;
|
|
1590
|
-
backTileBounds = oldFrontBounds;
|
|
1591
|
-
}
|
|
1592
|
-
if (boundsResult && boundsValid && capturedLeafCount > 0) {
|
|
1593
|
-
let gMin = Infinity;
|
|
1594
|
-
let gMax = -Infinity;
|
|
1595
|
-
for (let i = 0; i < capturedLeafCount; i++) {
|
|
1596
|
-
const rawMin = frontTileBounds[i * 2];
|
|
1597
|
-
const rawMax = frontTileBounds[i * 2 + 1];
|
|
1598
|
-
const a = capturedOriginY + rawMin * capturedScale;
|
|
1599
|
-
const b = capturedOriginY + rawMax * capturedScale;
|
|
1600
|
-
gMin = Math.min(gMin, a, b);
|
|
1601
|
-
gMax = Math.max(gMax, a, b);
|
|
1602
|
-
}
|
|
1603
|
-
globalRange = { min: gMin, max: gMax };
|
|
1604
|
-
}
|
|
1605
|
-
hasSnapshot = true;
|
|
1606
|
-
generationCount += 1;
|
|
1607
|
-
};
|
|
1608
|
-
if (boundsPromise) {
|
|
1609
|
-
Promise.all([elevationPromise, boundsPromise]).then(([elev, bounds]) => onComplete(elev, bounds)).finally(() => {
|
|
1610
|
-
readbackPending = false;
|
|
1611
|
-
});
|
|
1612
|
-
} else {
|
|
1613
|
-
elevationPromise.then((elev) => onComplete(elev, null)).finally(() => {
|
|
1614
|
-
readbackPending = false;
|
|
1615
|
-
});
|
|
1616
|
-
}
|
|
3656
|
+
triggerSnapshotReadback(state, renderer, attribute, spatialIndex, boundsAttribute, {
|
|
3657
|
+
activeLeafCount: activeLeafCount ?? 0,
|
|
3658
|
+
totalElements,
|
|
3659
|
+
verticesPerNode: shape.verticesPerNode,
|
|
3660
|
+
elevationScale: config.elevationScale,
|
|
3661
|
+
originY: config.originY
|
|
3662
|
+
});
|
|
3663
|
+
},
|
|
3664
|
+
dispose() {
|
|
3665
|
+
disposeSnapshotReadback(state);
|
|
1617
3666
|
},
|
|
1618
3667
|
getElevation(worldX, worldZ) {
|
|
1619
3668
|
const sample = getElevation(worldX, worldZ);
|
|
@@ -1623,43 +3672,26 @@ function createCpuTerrainCache(maxNodes, initialConfig) {
|
|
|
1623
3672
|
return sampleTerrain(worldX, worldZ).normal;
|
|
1624
3673
|
},
|
|
1625
3674
|
getTile(worldX, worldZ) {
|
|
1626
|
-
if (!hasSnapshot) return null;
|
|
1627
|
-
|
|
1628
|
-
if (!lookup.found) return null;
|
|
1629
|
-
return {
|
|
1630
|
-
level: lookup.level,
|
|
1631
|
-
x: lookup.tileX,
|
|
1632
|
-
y: lookup.tileY,
|
|
1633
|
-
index: lookup.leafIndex
|
|
1634
|
-
};
|
|
3675
|
+
if (!state.hasSnapshot) return null;
|
|
3676
|
+
return tileFromLookup(lookupTile(state.frontIndex, config, worldX, worldZ));
|
|
1635
3677
|
},
|
|
1636
3678
|
getTileBounds(worldX, worldZ) {
|
|
1637
|
-
if (!hasSnapshot) return null;
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
const a = config.originY + rawMin * config.elevationScale;
|
|
1643
|
-
const b = config.originY + rawMax * config.elevationScale;
|
|
1644
|
-
return {
|
|
1645
|
-
level: lookup.level,
|
|
1646
|
-
x: lookup.tileX,
|
|
1647
|
-
y: lookup.tileY,
|
|
1648
|
-
index: lookup.leafIndex,
|
|
1649
|
-
minElevation: Math.min(a, b),
|
|
1650
|
-
maxElevation: Math.max(a, b)
|
|
1651
|
-
};
|
|
3679
|
+
if (!state.hasSnapshot) return null;
|
|
3680
|
+
return tileBoundsFromLookup(
|
|
3681
|
+
lookupTile(state.frontIndex, config, worldX, worldZ),
|
|
3682
|
+
config.originY
|
|
3683
|
+
);
|
|
1652
3684
|
},
|
|
1653
3685
|
getGlobalElevationRange() {
|
|
1654
|
-
return globalRange;
|
|
3686
|
+
return state.globalRange;
|
|
1655
3687
|
},
|
|
1656
3688
|
sampleTerrainBatch(positions) {
|
|
1657
3689
|
const count = Math.floor(positions.length / 2);
|
|
1658
3690
|
const elevations = new Float32Array(count);
|
|
1659
3691
|
const normals = new Float32Array(count * 3);
|
|
1660
3692
|
const valid = new Uint8Array(count);
|
|
1661
|
-
if (!hasSnapshot) {
|
|
1662
|
-
return { elevations, normals, valid, generation:
|
|
3693
|
+
if (!state.hasSnapshot) {
|
|
3694
|
+
return { elevations, normals, valid, generation: state.generation };
|
|
1663
3695
|
}
|
|
1664
3696
|
let lastTile;
|
|
1665
3697
|
for (let i = 0; i < count; i += 1) {
|
|
@@ -1670,6 +3702,7 @@ function createCpuTerrainCache(maxNodes, initialConfig) {
|
|
|
1670
3702
|
lookup = {
|
|
1671
3703
|
found: true,
|
|
1672
3704
|
leafIndex: lastTile.leafIndex,
|
|
3705
|
+
space: 0,
|
|
1673
3706
|
level: lastTile.level,
|
|
1674
3707
|
tileX: lastTile.tileX,
|
|
1675
3708
|
tileY: lastTile.tileY,
|
|
@@ -1678,7 +3711,7 @@ function createCpuTerrainCache(maxNodes, initialConfig) {
|
|
|
1678
3711
|
localV: (worldZ - lastTile.tileMinZ) / lastTile.tileSize
|
|
1679
3712
|
};
|
|
1680
3713
|
} else {
|
|
1681
|
-
lookup = lookupTile(worldX, worldZ);
|
|
3714
|
+
lookup = lookupTile(state.frontIndex, config, worldX, worldZ);
|
|
1682
3715
|
if (lookup.found) {
|
|
1683
3716
|
lastTile = {
|
|
1684
3717
|
leafIndex: lookup.leafIndex,
|
|
@@ -1704,40 +3737,63 @@ function createCpuTerrainCache(maxNodes, initialConfig) {
|
|
|
1704
3737
|
normals[i * 3 + 2] = sample.normal.z;
|
|
1705
3738
|
valid[i] = 1;
|
|
1706
3739
|
}
|
|
1707
|
-
return { elevations, normals, valid, generation:
|
|
1708
|
-
},
|
|
1709
|
-
sampleTerrain
|
|
1710
|
-
};
|
|
1711
|
-
return api;
|
|
1712
|
-
}
|
|
1713
|
-
|
|
1714
|
-
function createTerrainQuery(cache) {
|
|
1715
|
-
return {
|
|
1716
|
-
get generation() {
|
|
1717
|
-
return cache.generation;
|
|
1718
|
-
},
|
|
1719
|
-
getElevation(worldX, worldZ) {
|
|
1720
|
-
return cache.getElevation(worldX, worldZ);
|
|
3740
|
+
return { elevations, normals, valid, generation: state.generation };
|
|
1721
3741
|
},
|
|
1722
|
-
|
|
1723
|
-
|
|
3742
|
+
sampleTerrain,
|
|
3743
|
+
// ── Generic surface ──
|
|
3744
|
+
sampleSurfaceByPosition,
|
|
3745
|
+
getElevationBySurfacePosition(px, py, pz) {
|
|
3746
|
+
const sample = sampleSurfaceByPosition(px, py, pz);
|
|
3747
|
+
return sample.valid ? sample.elevation : null;
|
|
1724
3748
|
},
|
|
1725
|
-
|
|
1726
|
-
|
|
3749
|
+
getNormalBySurfacePosition(px, py, pz) {
|
|
3750
|
+
const sample = sampleSurfaceByPosition(px, py, pz);
|
|
3751
|
+
return sample.valid ? sample.normal : null;
|
|
1727
3752
|
},
|
|
1728
|
-
|
|
1729
|
-
|
|
3753
|
+
getTileBySurfacePosition(px, py, pz) {
|
|
3754
|
+
if (!state.hasSnapshot || !surfaceOps) return null;
|
|
3755
|
+
return tileFromLookup(surfaceLookup(px, py, pz));
|
|
1730
3756
|
},
|
|
1731
|
-
|
|
1732
|
-
|
|
3757
|
+
getTileBoundsBySurfacePosition(px, py, pz) {
|
|
3758
|
+
if (!state.hasSnapshot || !surfaceOps) return null;
|
|
3759
|
+
return tileBoundsFromLookup(surfaceLookup(px, py, pz), 0);
|
|
1733
3760
|
},
|
|
1734
|
-
|
|
1735
|
-
|
|
3761
|
+
sampleSurfaceBatchByPosition(positions) {
|
|
3762
|
+
const count = Math.floor(positions.length / 3);
|
|
3763
|
+
const outPositions = new Float32Array(count * 3);
|
|
3764
|
+
const normals = new Float32Array(count * 3);
|
|
3765
|
+
const elevations = new Float32Array(count);
|
|
3766
|
+
const valid = new Uint8Array(count);
|
|
3767
|
+
if (!state.hasSnapshot || !surfaceOps) {
|
|
3768
|
+
return { positions: outPositions, normals, elevations, valid, generation: state.generation };
|
|
3769
|
+
}
|
|
3770
|
+
for (let i = 0; i < count; i += 1) {
|
|
3771
|
+
const sample = sampleSurfaceByPosition(
|
|
3772
|
+
positions[i * 3] ?? 0,
|
|
3773
|
+
positions[i * 3 + 1] ?? 0,
|
|
3774
|
+
positions[i * 3 + 2] ?? 0
|
|
3775
|
+
);
|
|
3776
|
+
if (!sample.valid) {
|
|
3777
|
+
normals[i * 3 + 1] = 1;
|
|
3778
|
+
continue;
|
|
3779
|
+
}
|
|
3780
|
+
outPositions[i * 3] = sample.position.x;
|
|
3781
|
+
outPositions[i * 3 + 1] = sample.position.y;
|
|
3782
|
+
outPositions[i * 3 + 2] = sample.position.z;
|
|
3783
|
+
normals[i * 3] = sample.normal.x;
|
|
3784
|
+
normals[i * 3 + 1] = sample.normal.y;
|
|
3785
|
+
normals[i * 3 + 2] = sample.normal.z;
|
|
3786
|
+
elevations[i] = sample.elevation;
|
|
3787
|
+
valid[i] = 1;
|
|
3788
|
+
}
|
|
3789
|
+
return { positions: outPositions, normals, elevations, valid, generation: state.generation };
|
|
1736
3790
|
},
|
|
1737
|
-
|
|
1738
|
-
|
|
3791
|
+
getTileElevationRange(space, level, x, y, out) {
|
|
3792
|
+
if (!state.hasSnapshot) return false;
|
|
3793
|
+
return lookupTileElevationRange(state.elevationPyramid, space, level, x, y, out);
|
|
1739
3794
|
}
|
|
1740
3795
|
};
|
|
3796
|
+
return api;
|
|
1741
3797
|
}
|
|
1742
3798
|
|
|
1743
3799
|
const WGSIZE = 64;
|
|
@@ -1781,7 +3837,10 @@ const tileBoundsContextTask = task((get, work) => {
|
|
|
1781
3837
|
return work(() => {
|
|
1782
3838
|
const data = new Float32Array(maxNodesValue * 2);
|
|
1783
3839
|
const attribute = new StorageBufferAttribute(data, 1);
|
|
1784
|
-
|
|
3840
|
+
attribute.name = "tileBounds";
|
|
3841
|
+
const node = storage(attribute, "float", maxNodesValue * 2).setName(
|
|
3842
|
+
"tileBounds"
|
|
3843
|
+
);
|
|
1785
3844
|
const verticesPerNode = edgeVertexCount * edgeVertexCount;
|
|
1786
3845
|
const kernel = buildReductionKernel(elevationFieldContext.node, node, verticesPerNode);
|
|
1787
3846
|
return { data, attribute, node, kernel };
|
|
@@ -1808,8 +3867,11 @@ const terrainQueryTask = task((get, work) => {
|
|
|
1808
3867
|
const rootSizeValue = get(rootSize);
|
|
1809
3868
|
const originValue = get(origin);
|
|
1810
3869
|
const elevationScaleValue = get(elevationScale);
|
|
3870
|
+
const radiusValue = get(radius);
|
|
3871
|
+
const topologyValue = get(topologyTask);
|
|
3872
|
+
const projection = topologyValue.projection;
|
|
1811
3873
|
return work((prev) => {
|
|
1812
|
-
const shapeKey = `${maxNodesValue}:${innerTileSegmentsValue}`;
|
|
3874
|
+
const shapeKey = `${maxNodesValue}:${innerTileSegmentsValue}:${projection.kind}`;
|
|
1813
3875
|
const configValues = {
|
|
1814
3876
|
rootSize: rootSizeValue,
|
|
1815
3877
|
originX: originValue.x,
|
|
@@ -1817,16 +3879,25 @@ const terrainQueryTask = task((get, work) => {
|
|
|
1817
3879
|
originZ: originValue.z,
|
|
1818
3880
|
innerTileSegments: innerTileSegmentsValue,
|
|
1819
3881
|
elevationScale: elevationScaleValue,
|
|
1820
|
-
maxLevel: maxLevelValue
|
|
3882
|
+
maxLevel: maxLevelValue,
|
|
3883
|
+
radius: topologyValue.radius ?? radiusValue,
|
|
3884
|
+
baseU: projection.baseResolution?.u ?? 1,
|
|
3885
|
+
baseV: projection.baseResolution?.v ?? 1
|
|
1821
3886
|
};
|
|
1822
3887
|
let cache = prev?.cache;
|
|
1823
3888
|
let query = prev?.query;
|
|
3889
|
+
let surfaceQuery = prev?.surfaceQuery ?? null;
|
|
3890
|
+
let sphereQuery = prev?.sphereQuery ?? null;
|
|
1824
3891
|
if (!cache || !query || prev?.shapeKey !== shapeKey) {
|
|
1825
|
-
cache
|
|
1826
|
-
|
|
3892
|
+
prev?.cache?.dispose();
|
|
3893
|
+
cache = createCpuTerrainCache(maxNodesValue, configValues, projection.cpu.createSurfaceOps());
|
|
3894
|
+
const runtime = projection.cpu.createRuntimeQueries(cache);
|
|
3895
|
+
query = runtime.query;
|
|
3896
|
+
surfaceQuery = runtime.surfaceQuery;
|
|
3897
|
+
sphereQuery = runtime.sphereQuery;
|
|
1827
3898
|
}
|
|
1828
3899
|
cache.updateConfig(configValues);
|
|
1829
|
-
return { cache, query, shapeKey };
|
|
3900
|
+
return { cache, query, surfaceQuery, sphereQuery, shapeKey };
|
|
1830
3901
|
});
|
|
1831
3902
|
}).displayName("terrainQueryTask");
|
|
1832
3903
|
const terrainReadbackTask = task(
|
|
@@ -1849,38 +3920,54 @@ const terrainReadbackTask = task(
|
|
|
1849
3920
|
}
|
|
1850
3921
|
).displayName("terrainReadbackTask").lane("gpu");
|
|
1851
3922
|
|
|
1852
|
-
const
|
|
1853
|
-
const
|
|
3923
|
+
const topologyTask = task((get, work) => {
|
|
3924
|
+
const customTopology = get(topology);
|
|
1854
3925
|
const rootSizeVal = get(rootSize);
|
|
1855
3926
|
const originVal = get(origin);
|
|
1856
3927
|
return work(() => {
|
|
1857
|
-
if (
|
|
1858
|
-
return
|
|
3928
|
+
if (customTopology) return customTopology;
|
|
3929
|
+
return createFlatTopology({ rootSize: rootSizeVal, origin: originVal });
|
|
1859
3930
|
});
|
|
1860
|
-
}).displayName("
|
|
3931
|
+
}).displayName("topologyTask");
|
|
1861
3932
|
const quadtreeConfigTask = task((get, work) => {
|
|
1862
|
-
const
|
|
3933
|
+
const topologyVal = get(topologyTask);
|
|
1863
3934
|
const maxNodesVal = get(maxNodes);
|
|
1864
3935
|
const maxLevelVal = get(maxLevel);
|
|
1865
3936
|
return work(() => {
|
|
1866
|
-
const state = createState({ maxNodes: maxNodesVal, maxLevel: maxLevelVal },
|
|
3937
|
+
const state = createState({ maxNodes: maxNodesVal, maxLevel: maxLevelVal }, topologyVal);
|
|
1867
3938
|
return {
|
|
1868
3939
|
state,
|
|
1869
|
-
|
|
3940
|
+
topology: topologyVal
|
|
1870
3941
|
};
|
|
1871
3942
|
});
|
|
1872
3943
|
}).displayName("quadtreeConfigTask");
|
|
1873
3944
|
const quadtreeUpdateTask = task((get, work) => {
|
|
1874
3945
|
const quadtreeConfig = get(quadtreeConfigTask);
|
|
1875
3946
|
const quadtreeUpdateConfig = get(quadtreeUpdate);
|
|
1876
|
-
const { query: terrainQuery } = get(terrainQueryTask);
|
|
3947
|
+
const { query: terrainQuery, surfaceQuery, cache } = get(terrainQueryTask);
|
|
3948
|
+
const elevationScaleValue = get(elevationScale);
|
|
1877
3949
|
let outLeaves = void 0;
|
|
3950
|
+
const cameraPosition = new Vector3();
|
|
3951
|
+
const elevationRangeScratch = { min: 0, max: 0 };
|
|
1878
3952
|
return work(() => {
|
|
1879
3953
|
const cam = quadtreeUpdateConfig.cameraOrigin;
|
|
1880
|
-
|
|
3954
|
+
if (surfaceQuery) {
|
|
3955
|
+
cameraPosition.set(cam.x, cam.y, cam.z);
|
|
3956
|
+
quadtreeUpdateConfig.elevationAtCameraXZ = surfaceQuery.getElevationByPosition(cameraPosition) ?? 0;
|
|
3957
|
+
} else {
|
|
3958
|
+
quadtreeUpdateConfig.elevationAtCameraXZ = terrainQuery.getElevation(cam.x, cam.z) ?? 0;
|
|
3959
|
+
}
|
|
3960
|
+
quadtreeUpdateConfig.tileElevationRange = (space, level, x, y, out) => {
|
|
3961
|
+
if (!cache.getTileElevationRange(space, level, x, y, elevationRangeScratch)) {
|
|
3962
|
+
return false;
|
|
3963
|
+
}
|
|
3964
|
+
out.min = elevationRangeScratch.min * elevationScaleValue;
|
|
3965
|
+
out.max = elevationRangeScratch.max * elevationScaleValue;
|
|
3966
|
+
return true;
|
|
3967
|
+
};
|
|
1881
3968
|
outLeaves = update(
|
|
1882
3969
|
quadtreeConfig.state,
|
|
1883
|
-
quadtreeConfig.
|
|
3970
|
+
quadtreeConfig.topology,
|
|
1884
3971
|
quadtreeUpdateConfig,
|
|
1885
3972
|
outLeaves
|
|
1886
3973
|
);
|
|
@@ -1902,7 +3989,7 @@ const leafGpuBufferTask = task((get, work) => {
|
|
|
1902
3989
|
leafStorage.data[offset] = leafSet.level[i] ?? 0;
|
|
1903
3990
|
leafStorage.data[offset + 1] = leafSet.x[i] ?? 0;
|
|
1904
3991
|
leafStorage.data[offset + 2] = leafSet.y[i] ?? 0;
|
|
1905
|
-
leafStorage.data[offset + 3] =
|
|
3992
|
+
leafStorage.data[offset + 3] = leafSet.space[i] ?? 0;
|
|
1906
3993
|
}
|
|
1907
3994
|
leafStorage.attribute.needsUpdate = true;
|
|
1908
3995
|
leafStorage.node.needsUpdate = true;
|
|
@@ -1915,23 +4002,6 @@ const leafGpuBufferTask = task((get, work) => {
|
|
|
1915
4002
|
});
|
|
1916
4003
|
}).displayName("leafGpuBufferTask");
|
|
1917
4004
|
|
|
1918
|
-
function createElevationFunction(callback) {
|
|
1919
|
-
const tslFunction = (args) => {
|
|
1920
|
-
const params = {
|
|
1921
|
-
worldPosition: args.worldPosition,
|
|
1922
|
-
rootSize: args.rootSize,
|
|
1923
|
-
rootUV: args.rootUV,
|
|
1924
|
-
tileUV: args.tileUV,
|
|
1925
|
-
tileLevel: args.tileLevel,
|
|
1926
|
-
tileSize: args.tileSize,
|
|
1927
|
-
tileOriginVec2: args.tileOriginVec2,
|
|
1928
|
-
nodeIndex: args.nodeIndex
|
|
1929
|
-
};
|
|
1930
|
-
return callback(params);
|
|
1931
|
-
};
|
|
1932
|
-
return Fn$1(tslFunction);
|
|
1933
|
-
}
|
|
1934
|
-
|
|
1935
4005
|
function createTerrainUniforms(params) {
|
|
1936
4006
|
const sanitizedId = params.instanceId?.replace(/-/g, "_");
|
|
1937
4007
|
const suffix = sanitizedId ? `_${sanitizedId}` : "";
|
|
@@ -1944,12 +4014,14 @@ function createTerrainUniforms(params) {
|
|
|
1944
4014
|
);
|
|
1945
4015
|
const uSkirtScale = uniform(float(params.skirtScale)).setName(`uSkirtScale${suffix}`);
|
|
1946
4016
|
const uElevationScale = uniform(float(params.elevationScale)).setName(`uElevationScale${suffix}`);
|
|
4017
|
+
const uRadius = uniform(float(params.radius)).setName(`uRadius${suffix}`);
|
|
1947
4018
|
return {
|
|
1948
4019
|
uRootOrigin,
|
|
1949
4020
|
uRootSize,
|
|
1950
4021
|
uInnerTileSegments,
|
|
1951
4022
|
uSkirtScale,
|
|
1952
|
-
uElevationScale
|
|
4023
|
+
uElevationScale,
|
|
4024
|
+
uRadius
|
|
1953
4025
|
};
|
|
1954
4026
|
}
|
|
1955
4027
|
|
|
@@ -1963,6 +4035,7 @@ const createUniformsTask = task((get, work) => {
|
|
|
1963
4035
|
innerTileSegments: get(innerTileSegments),
|
|
1964
4036
|
skirtScale: get(skirtScale),
|
|
1965
4037
|
elevationScale: get(elevationScale),
|
|
4038
|
+
radius: get(radius),
|
|
1966
4039
|
instanceId: get(instanceIdTask)
|
|
1967
4040
|
};
|
|
1968
4041
|
return work(() => createTerrainUniforms(uniformParams));
|
|
@@ -1974,6 +4047,7 @@ const updateUniformsTask = task((get, work) => {
|
|
|
1974
4047
|
const innerTileSegmentsVal = get(innerTileSegments);
|
|
1975
4048
|
const skirtScaleVal = get(skirtScale);
|
|
1976
4049
|
const elevationScaleVal = get(elevationScale);
|
|
4050
|
+
const radiusVal = get(radius);
|
|
1977
4051
|
return work(() => {
|
|
1978
4052
|
terrainUniformsContext.uRootSize.value = rootSizeVal;
|
|
1979
4053
|
terrainUniformsContext.uRootOrigin.value = scratchVector3.set(
|
|
@@ -1984,6 +4058,7 @@ const updateUniformsTask = task((get, work) => {
|
|
|
1984
4058
|
terrainUniformsContext.uInnerTileSegments.value = innerTileSegmentsVal;
|
|
1985
4059
|
terrainUniformsContext.uSkirtScale.value = skirtScaleVal;
|
|
1986
4060
|
terrainUniformsContext.uElevationScale.value = elevationScaleVal;
|
|
4061
|
+
terrainUniformsContext.uRadius.value = radiusVal;
|
|
1987
4062
|
return terrainUniformsContext;
|
|
1988
4063
|
});
|
|
1989
4064
|
}).displayName("updateUniformsTask");
|
|
@@ -1995,7 +4070,8 @@ const createElevationFieldContextTask = task((get, work) => {
|
|
|
1995
4070
|
return work(() => {
|
|
1996
4071
|
const data = new Float32Array(totalElements);
|
|
1997
4072
|
const attribute = new StorageBufferAttribute(data, 1);
|
|
1998
|
-
|
|
4073
|
+
attribute.name = "elevationField";
|
|
4074
|
+
const node = storage(attribute, "float", totalElements).setName("elevationField");
|
|
1999
4075
|
return {
|
|
2000
4076
|
data,
|
|
2001
4077
|
attribute,
|
|
@@ -2006,8 +4082,9 @@ const createElevationFieldContextTask = task((get, work) => {
|
|
|
2006
4082
|
const tileNodesTask = task((get, work) => {
|
|
2007
4083
|
const leafStorage = get(leafStorageTask);
|
|
2008
4084
|
const uniforms = get(updateUniformsTask);
|
|
4085
|
+
const topology = get(topologyTask);
|
|
2009
4086
|
return work(() => {
|
|
2010
|
-
return createTileCompute(leafStorage, uniforms);
|
|
4087
|
+
return createTileCompute(leafStorage, uniforms, topology.projection);
|
|
2011
4088
|
});
|
|
2012
4089
|
}).displayName("tileNodesTask");
|
|
2013
4090
|
const elevationFieldStageTask = task((get, work) => {
|
|
@@ -2042,30 +4119,6 @@ const createTerrainFieldTextureTask = task(
|
|
|
2042
4119
|
);
|
|
2043
4120
|
}
|
|
2044
4121
|
).displayName("createTerrainFieldTextureTask");
|
|
2045
|
-
function createNormalFromElevationField(elevationFieldNode, edgeVertexCount) {
|
|
2046
|
-
return Fn(
|
|
2047
|
-
([nodeIndex, tileSize, ix, iy, elevationScale]) => {
|
|
2048
|
-
const iEdge = int(edgeVertexCount);
|
|
2049
|
-
const verticesPerNode = iEdge.mul(iEdge);
|
|
2050
|
-
const baseOffset = int(nodeIndex).mul(verticesPerNode);
|
|
2051
|
-
const xLeft = int(ix).sub(int(1));
|
|
2052
|
-
const xRight = int(ix).add(int(1));
|
|
2053
|
-
const yUp = int(iy).sub(int(1));
|
|
2054
|
-
const yDown = int(iy).add(int(1));
|
|
2055
|
-
const hLeft = elevationFieldNode.element(baseOffset.add(int(iy).mul(iEdge).add(xLeft))).mul(elevationScale);
|
|
2056
|
-
const hRight = elevationFieldNode.element(baseOffset.add(int(iy).mul(iEdge).add(xRight))).mul(elevationScale);
|
|
2057
|
-
const hUp = elevationFieldNode.element(baseOffset.add(yUp.mul(iEdge).add(int(ix)))).mul(elevationScale);
|
|
2058
|
-
const hDown = elevationFieldNode.element(baseOffset.add(yDown.mul(iEdge).add(int(ix)))).mul(elevationScale);
|
|
2059
|
-
const innerSegments = float(iEdge).sub(float(3));
|
|
2060
|
-
const stepWorld = tileSize.div(innerSegments);
|
|
2061
|
-
const inv2Step = float(0.5).div(stepWorld);
|
|
2062
|
-
const dhdx = float(hRight).sub(float(hLeft)).mul(inv2Step);
|
|
2063
|
-
const dhdz = float(hDown).sub(float(hUp)).mul(inv2Step);
|
|
2064
|
-
const normal = vec3(dhdx.negate(), float(1), dhdz.negate()).normalize();
|
|
2065
|
-
return vec2(normal.x, normal.z);
|
|
2066
|
-
}
|
|
2067
|
-
);
|
|
2068
|
-
}
|
|
2069
4122
|
const terrainFieldStageTask = task((get, work) => {
|
|
2070
4123
|
const upstream = get(elevationFieldStageTask);
|
|
2071
4124
|
const elevationFieldContext = get(createElevationFieldContextTask);
|
|
@@ -2073,635 +4126,115 @@ const terrainFieldStageTask = task((get, work) => {
|
|
|
2073
4126
|
const tileEdgeVertexCount = get(innerTileSegments) + 3;
|
|
2074
4127
|
const tile = get(tileNodesTask);
|
|
2075
4128
|
const uniforms = get(updateUniformsTask);
|
|
4129
|
+
const topology = get(topologyTask);
|
|
2076
4130
|
return work(() => {
|
|
2077
|
-
const computeNormal =
|
|
2078
|
-
elevationFieldContext.node,
|
|
2079
|
-
tileEdgeVertexCount
|
|
2080
|
-
|
|
4131
|
+
const computeNormal = topology.projection.gpu.createFieldNormal({
|
|
4132
|
+
elevationFieldNode: elevationFieldContext.node,
|
|
4133
|
+
edgeVertexCount: tileEdgeVertexCount,
|
|
4134
|
+
tile,
|
|
4135
|
+
uniforms
|
|
4136
|
+
});
|
|
2081
4137
|
return [
|
|
2082
4138
|
...upstream,
|
|
2083
4139
|
(nodeIndex, globalVertexIndex, _uv, localCoordinates) => {
|
|
2084
4140
|
const ix = int(localCoordinates.x);
|
|
2085
4141
|
const iy = int(localCoordinates.y);
|
|
2086
|
-
const tileSize = tile.tileSize(nodeIndex);
|
|
2087
4142
|
const height = elevationFieldContext.node.element(globalVertexIndex);
|
|
2088
|
-
const
|
|
2089
|
-
nodeIndex,
|
|
2090
|
-
tileSize,
|
|
2091
|
-
ix,
|
|
2092
|
-
iy,
|
|
2093
|
-
uniforms.uElevationScale
|
|
2094
|
-
);
|
|
4143
|
+
const normal = computeNormal(nodeIndex, ix, iy);
|
|
2095
4144
|
storeTerrainField(
|
|
2096
4145
|
terrainFieldStorage,
|
|
2097
4146
|
ix,
|
|
2098
4147
|
iy,
|
|
2099
4148
|
nodeIndex,
|
|
2100
|
-
packTerrainFieldSample(height,
|
|
4149
|
+
packTerrainFieldSample(height, normal)
|
|
2101
4150
|
);
|
|
2102
|
-
}
|
|
2103
|
-
];
|
|
2104
|
-
});
|
|
2105
|
-
}).displayName("terrainFieldStageTask");
|
|
2106
|
-
|
|
2107
|
-
const compileComputeTask = task((get, work) => {
|
|
2108
|
-
const pipeline = get(terrainFieldStageTask);
|
|
2109
|
-
const edgeVertexCount = get(innerTileSegments) + 3;
|
|
2110
|
-
return work(
|
|
2111
|
-
() => compileComputePipeline(pipeline, edgeVertexCount, {
|
|
2112
|
-
preferSingleKernelWhenPossible: false
|
|
2113
|
-
})
|
|
2114
|
-
);
|
|
2115
|
-
}).displayName("compileComputeTask");
|
|
2116
|
-
const executeComputeTask = task(
|
|
2117
|
-
(get, work, { resources }) => {
|
|
2118
|
-
const { execute } = get(compileComputeTask);
|
|
2119
|
-
const leafState = get(leafGpuBufferTask);
|
|
2120
|
-
return work(
|
|
2121
|
-
() => resources?.renderer ? execute(resources.renderer, leafState.count) : () => {
|
|
2122
|
-
}
|
|
2123
|
-
);
|
|
2124
|
-
}
|
|
2125
|
-
).displayName("executeComputeTask").lane("gpu");
|
|
2126
|
-
function createComputePipelineTasks(leafStageTask) {
|
|
2127
|
-
const compile = task((get, work) => {
|
|
2128
|
-
const pipeline = get(leafStageTask);
|
|
2129
|
-
const edgeVertexCount = get(innerTileSegments) + 3;
|
|
2130
|
-
return work(
|
|
2131
|
-
() => compileComputePipeline(pipeline, edgeVertexCount, {
|
|
2132
|
-
preferSingleKernelWhenPossible: false
|
|
2133
|
-
})
|
|
2134
|
-
);
|
|
2135
|
-
}).displayName("compileComputeTask");
|
|
2136
|
-
const execute = task(
|
|
2137
|
-
(get, work, { resources }) => {
|
|
2138
|
-
const { execute: run } = get(compile);
|
|
2139
|
-
const leafState = get(leafGpuBufferTask);
|
|
2140
|
-
return work(
|
|
2141
|
-
() => resources?.renderer ? run(resources.renderer, leafState.count) : () => {
|
|
2142
|
-
}
|
|
2143
|
-
);
|
|
2144
|
-
}
|
|
2145
|
-
).displayName("executeComputeTask").lane("gpu");
|
|
2146
|
-
return { compile, execute };
|
|
2147
|
-
}
|
|
2148
|
-
|
|
2149
|
-
const SLOT_STRIDE = 6;
|
|
2150
|
-
function nextPow2(n) {
|
|
2151
|
-
let x = 1;
|
|
2152
|
-
while (x < n) x <<= 1;
|
|
2153
|
-
return x;
|
|
2154
|
-
}
|
|
2155
|
-
function createGpuSpatialIndex(maxEntries) {
|
|
2156
|
-
const size = nextPow2(Math.max(2, maxEntries * 2));
|
|
2157
|
-
const data = new Uint32Array(size * SLOT_STRIDE);
|
|
2158
|
-
const attribute = new StorageBufferAttribute(data, SLOT_STRIDE);
|
|
2159
|
-
const node = storage(attribute, "u32", 1).toReadOnly().setName("gpuSpatialIndex");
|
|
2160
|
-
const stampGen = uniform(uint(1)).setName("uGpuSpatialIndexStampGen");
|
|
2161
|
-
return {
|
|
2162
|
-
data,
|
|
2163
|
-
size,
|
|
2164
|
-
mask: size - 1,
|
|
2165
|
-
stampGen,
|
|
2166
|
-
attribute,
|
|
2167
|
-
node
|
|
2168
|
-
};
|
|
2169
|
-
}
|
|
2170
|
-
function uploadGpuSpatialIndex(gpuIndex, cpuIndex) {
|
|
2171
|
-
if (gpuIndex.size !== cpuIndex.size) {
|
|
2172
|
-
throw new Error(
|
|
2173
|
-
`Spatial index size mismatch (gpu=${gpuIndex.size}, cpu=${cpuIndex.size}).`
|
|
2174
|
-
);
|
|
2175
|
-
}
|
|
2176
|
-
for (let i = 0; i < cpuIndex.size; i += 1) {
|
|
2177
|
-
const base = i * SLOT_STRIDE;
|
|
2178
|
-
gpuIndex.data[base] = cpuIndex.stamp[i] ?? 0;
|
|
2179
|
-
gpuIndex.data[base + 1] = cpuIndex.keysSpace[i] ?? 0;
|
|
2180
|
-
gpuIndex.data[base + 2] = cpuIndex.keysLevel[i] ?? 0;
|
|
2181
|
-
gpuIndex.data[base + 3] = cpuIndex.keysX[i] ?? 0;
|
|
2182
|
-
gpuIndex.data[base + 4] = cpuIndex.keysY[i] ?? 0;
|
|
2183
|
-
gpuIndex.data[base + 5] = cpuIndex.values[i] ?? 0;
|
|
2184
|
-
}
|
|
2185
|
-
gpuIndex.stampGen.value = cpuIndex.stampGen >>> 0;
|
|
2186
|
-
gpuIndex.attribute.needsUpdate = true;
|
|
2187
|
-
gpuIndex.node.needsUpdate = true;
|
|
2188
|
-
}
|
|
2189
|
-
function readGpuSpatialIndexValue(spatialIndex, slot, fieldOffset) {
|
|
2190
|
-
const offset = int(slot).mul(int(SLOT_STRIDE)).add(int(fieldOffset));
|
|
2191
|
-
return spatialIndex.node.element(offset).toUint();
|
|
2192
|
-
}
|
|
2193
|
-
const mix32 = Fn(([x]) => {
|
|
2194
|
-
const v = uint(x).toVar();
|
|
2195
|
-
v.assign(v.bitXor(v.shiftRight(uint(16))));
|
|
2196
|
-
v.assign(v.mul(uint(2146121005)));
|
|
2197
|
-
v.assign(v.bitXor(v.shiftRight(uint(15))));
|
|
2198
|
-
v.assign(v.mul(uint(2221713035)));
|
|
2199
|
-
v.assign(v.bitXor(v.shiftRight(uint(16))));
|
|
2200
|
-
return v;
|
|
2201
|
-
});
|
|
2202
|
-
const hashKey = Fn(([space, level, x, y]) => {
|
|
2203
|
-
const s = uint(space).bitAnd(uint(255));
|
|
2204
|
-
const l = uint(level).bitAnd(uint(255));
|
|
2205
|
-
const h = s.bitXor(l.shiftLeft(uint(8))).bitXor(mix32(uint(x))).bitXor(mix32(uint(y)));
|
|
2206
|
-
return mix32(h);
|
|
2207
|
-
});
|
|
2208
|
-
const createGpuSpatialLookup = (spatialIndex) => {
|
|
2209
|
-
const slotCount = spatialIndex.size;
|
|
2210
|
-
const mask = uint(spatialIndex.mask);
|
|
2211
|
-
const stampGen = spatialIndex.stampGen.toUint();
|
|
2212
|
-
const emptyValue = int(-1);
|
|
2213
|
-
return Fn(([space, level, x, y]) => {
|
|
2214
|
-
const s = uint(space).bitAnd(uint(255));
|
|
2215
|
-
const l = uint(level).bitAnd(uint(255));
|
|
2216
|
-
const xx = uint(x);
|
|
2217
|
-
const yy = uint(y);
|
|
2218
|
-
const result = emptyValue.toVar();
|
|
2219
|
-
const slot = hashKey(s, l, xx, yy).bitAnd(mask).toVar();
|
|
2220
|
-
const probes = int(0).toVar();
|
|
2221
|
-
Loop(slotCount, () => {
|
|
2222
|
-
const stamp = readGpuSpatialIndexValue(spatialIndex, slot, 0);
|
|
2223
|
-
If(stamp.notEqual(stampGen), () => {
|
|
2224
|
-
Break();
|
|
2225
|
-
});
|
|
2226
|
-
const ks = readGpuSpatialIndexValue(spatialIndex, slot, 1);
|
|
2227
|
-
const kl = readGpuSpatialIndexValue(spatialIndex, slot, 2);
|
|
2228
|
-
const kx = readGpuSpatialIndexValue(spatialIndex, slot, 3);
|
|
2229
|
-
const ky = readGpuSpatialIndexValue(spatialIndex, slot, 4);
|
|
2230
|
-
If(
|
|
2231
|
-
ks.equal(s).and(kl.equal(l)).and(kx.equal(xx)).and(ky.equal(yy)),
|
|
2232
|
-
() => {
|
|
2233
|
-
result.assign(int(readGpuSpatialIndexValue(spatialIndex, slot, 5)));
|
|
2234
|
-
Break();
|
|
2235
|
-
}
|
|
2236
|
-
);
|
|
2237
|
-
slot.assign(slot.add(uint(1)).bitAnd(mask));
|
|
2238
|
-
probes.addAssign(1);
|
|
2239
|
-
});
|
|
2240
|
-
return result;
|
|
2241
|
-
});
|
|
2242
|
-
};
|
|
2243
|
-
const createTileIndexFromWorldPosition = (spatialIndex, uniforms, maxLevel) => {
|
|
2244
|
-
const lookup = createGpuSpatialLookup(spatialIndex);
|
|
2245
|
-
const levelCount = Math.max(1, maxLevel + 1);
|
|
2246
|
-
return Fn(([worldX, worldZ]) => {
|
|
2247
|
-
const rootOrigin = uniforms.uRootOrigin.toVar();
|
|
2248
|
-
const rootSize = uniforms.uRootSize.toVar();
|
|
2249
|
-
const halfRoot = rootSize.mul(float(0.5));
|
|
2250
|
-
const tileIndex = int(-1).toVar();
|
|
2251
|
-
const tileU = float(0).toVar();
|
|
2252
|
-
const tileV = float(0).toVar();
|
|
2253
|
-
const i = int(0).toVar();
|
|
2254
|
-
Loop(levelCount, () => {
|
|
2255
|
-
const level = int(maxLevel).sub(i).toVar();
|
|
2256
|
-
const scale = pow(float(2), level.toFloat());
|
|
2257
|
-
const tileSize = rootSize.div(scale);
|
|
2258
|
-
const tileX = worldX.sub(rootOrigin.x).add(halfRoot).div(tileSize).floor().toInt();
|
|
2259
|
-
const tileY = worldZ.sub(rootOrigin.z).add(halfRoot).div(tileSize).floor().toInt();
|
|
2260
|
-
const maybeIndex = lookup(int(0), level, tileX, tileY).toVar();
|
|
2261
|
-
If(maybeIndex.greaterThanEqual(int(0)), () => {
|
|
2262
|
-
const minX = rootOrigin.x.add(tileX.toFloat().mul(tileSize)).sub(halfRoot);
|
|
2263
|
-
const minZ = rootOrigin.z.add(tileY.toFloat().mul(tileSize)).sub(halfRoot);
|
|
2264
|
-
tileIndex.assign(maybeIndex);
|
|
2265
|
-
tileU.assign(worldX.sub(minX).div(tileSize));
|
|
2266
|
-
tileV.assign(worldZ.sub(minZ).div(tileSize));
|
|
2267
|
-
Break();
|
|
2268
|
-
});
|
|
2269
|
-
i.addAssign(1);
|
|
2270
|
-
});
|
|
2271
|
-
return vec3(tileIndex.toFloat(), tileU, tileV);
|
|
2272
|
-
});
|
|
2273
|
-
};
|
|
2274
|
-
|
|
2275
|
-
const gpuSpatialIndexStorageTask = task((get, work) => {
|
|
2276
|
-
const maxNodesValue = get(maxNodes);
|
|
2277
|
-
return work(() => createGpuSpatialIndex(maxNodesValue));
|
|
2278
|
-
}).displayName("gpuSpatialIndexStorageTask");
|
|
2279
|
-
const gpuSpatialIndexUploadTask = task((get, work) => {
|
|
2280
|
-
const quadtreeConfig = get(quadtreeConfigTask);
|
|
2281
|
-
get(quadtreeUpdateTask);
|
|
2282
|
-
const gpuSpatialIndex = get(gpuSpatialIndexStorageTask);
|
|
2283
|
-
return work(() => {
|
|
2284
|
-
uploadGpuSpatialIndex(gpuSpatialIndex, quadtreeConfig.state.leafIndex);
|
|
2285
|
-
return gpuSpatialIndex;
|
|
2286
|
-
});
|
|
2287
|
-
}).displayName("gpuSpatialIndexUploadTask");
|
|
2288
|
-
|
|
2289
|
-
function createTerrainSampleNode(params) {
|
|
2290
|
-
const tileLookup = createTileIndexFromWorldPosition(
|
|
2291
|
-
params.spatialIndex,
|
|
2292
|
-
params.uniforms,
|
|
2293
|
-
maxLevel.get()
|
|
2294
|
-
);
|
|
2295
|
-
return Fn(([worldX, worldZ]) => {
|
|
2296
|
-
const tileResult = tileLookup(worldX, worldZ).toVar();
|
|
2297
|
-
const tileIndex = int(tileResult.x).toVar();
|
|
2298
|
-
const safeTileIndex = tileIndex.max(int(0)).toVar();
|
|
2299
|
-
const u = tileResult.y.toVar();
|
|
2300
|
-
const v = tileResult.z.toVar();
|
|
2301
|
-
const fieldU = tileLocalToFieldUV$1(
|
|
2302
|
-
u,
|
|
2303
|
-
params.uniforms.uInnerTileSegments
|
|
2304
|
-
).toVar();
|
|
2305
|
-
const fieldV = tileLocalToFieldUV$1(
|
|
2306
|
-
v,
|
|
2307
|
-
params.uniforms.uInnerTileSegments
|
|
2308
|
-
).toVar();
|
|
2309
|
-
const found = tileIndex.greaterThanEqual(int(0)).toVar();
|
|
2310
|
-
const sampled = sampleTerrainField(
|
|
2311
|
-
params.terrainFieldStorage,
|
|
2312
|
-
fieldU,
|
|
2313
|
-
fieldV,
|
|
2314
|
-
safeTileIndex
|
|
2315
|
-
).toVar();
|
|
2316
|
-
const nx = sampled.g.toVar();
|
|
2317
|
-
const nz = sampled.b.toVar();
|
|
2318
|
-
const ny = float(1).sub(nx.mul(nx)).sub(nz.mul(nz)).max(0).sqrt();
|
|
2319
|
-
const valid = found.select(float(1), float(0)).toVar();
|
|
2320
|
-
return vec4(
|
|
2321
|
-
sampled.r.mul(valid),
|
|
2322
|
-
nx.mul(valid),
|
|
2323
|
-
ny.mul(valid),
|
|
2324
|
-
nz.mul(valid)
|
|
2325
|
-
);
|
|
2326
|
-
});
|
|
2327
|
-
}
|
|
2328
|
-
function createTerrainSampler(params) {
|
|
2329
|
-
const elevationNode = createElevationFunction(params.elevationCallback);
|
|
2330
|
-
const terrainSampleAt = createTerrainSampleNode(params);
|
|
2331
|
-
const evaluateElevationAt = Fn(([worldX, worldZ]) => {
|
|
2332
|
-
const rootOrigin = params.uniforms.uRootOrigin.toVar();
|
|
2333
|
-
const rootSize = params.uniforms.uRootSize.toVar();
|
|
2334
|
-
const centeredX = worldX.sub(rootOrigin.x);
|
|
2335
|
-
const centeredZ = worldZ.sub(rootOrigin.z);
|
|
2336
|
-
const rootUV = vec2(
|
|
2337
|
-
centeredX.div(rootSize).add(0.5),
|
|
2338
|
-
centeredZ.div(rootSize).mul(float(-1)).add(0.5)
|
|
2339
|
-
).toVar();
|
|
2340
|
-
return elevationNode({
|
|
2341
|
-
worldPosition: vec3(worldX, rootOrigin.y, worldZ),
|
|
2342
|
-
rootSize,
|
|
2343
|
-
rootUV,
|
|
2344
|
-
tileUV: rootUV,
|
|
2345
|
-
tileLevel: int(0),
|
|
2346
|
-
tileSize: rootSize,
|
|
2347
|
-
tileOriginVec2: vec2(0, 0),
|
|
2348
|
-
nodeIndex: int(0)
|
|
2349
|
-
});
|
|
4151
|
+
}
|
|
4152
|
+
];
|
|
2350
4153
|
});
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
const evaluateNormalNode = Fn(
|
|
2371
|
-
([worldX, worldZ, epsilon]) => {
|
|
2372
|
-
const eps = epsilon ?? float(0.1);
|
|
2373
|
-
const elevationScale = params.uniforms.uElevationScale.toVar();
|
|
2374
|
-
const hL = evaluateElevationAt(worldX.sub(eps), worldZ).mul(
|
|
2375
|
-
elevationScale
|
|
2376
|
-
);
|
|
2377
|
-
const hR = evaluateElevationAt(worldX.add(eps), worldZ).mul(
|
|
2378
|
-
elevationScale
|
|
2379
|
-
);
|
|
2380
|
-
const hD = evaluateElevationAt(worldX, worldZ.sub(eps)).mul(
|
|
2381
|
-
elevationScale
|
|
2382
|
-
);
|
|
2383
|
-
const hU = evaluateElevationAt(worldX, worldZ.add(eps)).mul(
|
|
2384
|
-
elevationScale
|
|
4154
|
+
}).displayName("terrainFieldStageTask");
|
|
4155
|
+
|
|
4156
|
+
const { compile: compileComputeTask, execute: executeComputeTask } = createComputePipelineTasks(terrainFieldStageTask);
|
|
4157
|
+
function createComputePipelineTasks(leafStageTask) {
|
|
4158
|
+
const compile = task((get, work) => {
|
|
4159
|
+
const pipeline = get(leafStageTask);
|
|
4160
|
+
const edgeVertexCount = get(innerTileSegments) + 3;
|
|
4161
|
+
return work(
|
|
4162
|
+
() => compileComputePipeline(pipeline, edgeVertexCount, {
|
|
4163
|
+
})
|
|
4164
|
+
);
|
|
4165
|
+
}).displayName("compileComputeTask");
|
|
4166
|
+
const execute = task(
|
|
4167
|
+
(get, work, { resources }) => {
|
|
4168
|
+
const { execute: run } = get(compile);
|
|
4169
|
+
const leafState = get(leafGpuBufferTask);
|
|
4170
|
+
return work(
|
|
4171
|
+
() => resources?.renderer ? run(resources.renderer, leafState.count) : () => {
|
|
4172
|
+
}
|
|
2385
4173
|
);
|
|
2386
|
-
const inv2eps = float(0.5).div(eps);
|
|
2387
|
-
const dhdx = hR.sub(hL).mul(inv2eps);
|
|
2388
|
-
const dhdz = hU.sub(hD).mul(inv2eps);
|
|
2389
|
-
return vec3(dhdx.negate(), float(1), dhdz.negate()).normalize();
|
|
2390
4174
|
}
|
|
2391
|
-
);
|
|
2392
|
-
|
|
2393
|
-
return {
|
|
2394
|
-
sampleElevation,
|
|
2395
|
-
sampleNormal,
|
|
2396
|
-
sampleTerrain,
|
|
2397
|
-
sampleValidity,
|
|
2398
|
-
evaluateElevation,
|
|
2399
|
-
evaluateNormal
|
|
2400
|
-
};
|
|
4175
|
+
).displayName("executeComputeTask").lane("gpu");
|
|
4176
|
+
return { compile, execute };
|
|
2401
4177
|
}
|
|
2402
4178
|
|
|
4179
|
+
const gpuSpatialIndexStorageTask = task((get, work) => {
|
|
4180
|
+
const maxNodesValue = get(maxNodes);
|
|
4181
|
+
return work(() => createGpuSpatialIndex(maxNodesValue));
|
|
4182
|
+
}).displayName("gpuSpatialIndexStorageTask");
|
|
4183
|
+
const gpuSpatialIndexUploadTask = task((get, work) => {
|
|
4184
|
+
const quadtreeConfig = get(quadtreeConfigTask);
|
|
4185
|
+
get(quadtreeUpdateTask);
|
|
4186
|
+
const gpuSpatialIndex = get(gpuSpatialIndexStorageTask);
|
|
4187
|
+
return work(() => {
|
|
4188
|
+
uploadGpuSpatialIndex(gpuSpatialIndex, quadtreeConfig.state.leafIndex);
|
|
4189
|
+
return gpuSpatialIndex;
|
|
4190
|
+
});
|
|
4191
|
+
}).displayName("gpuSpatialIndexUploadTask");
|
|
4192
|
+
|
|
2403
4193
|
const createTerrainSamplerTask = task((get, work) => {
|
|
2404
4194
|
const terrainFieldStorage = get(createTerrainFieldTextureTask);
|
|
2405
4195
|
const spatialIndex = get(gpuSpatialIndexStorageTask);
|
|
2406
4196
|
const uniforms = get(updateUniformsTask);
|
|
2407
4197
|
const elevationCallback = get(elevationFn);
|
|
4198
|
+
const maxLevelValue = get(maxLevel);
|
|
4199
|
+
const projection = get(topologyTask).projection;
|
|
2408
4200
|
return work(
|
|
2409
4201
|
() => createTerrainSampler({
|
|
2410
4202
|
terrainFieldStorage,
|
|
2411
4203
|
spatialIndex,
|
|
2412
4204
|
uniforms,
|
|
2413
|
-
elevationCallback
|
|
4205
|
+
elevationCallback,
|
|
4206
|
+
maxLevel: maxLevelValue,
|
|
4207
|
+
projection
|
|
2414
4208
|
})
|
|
2415
4209
|
);
|
|
2416
4210
|
}).displayName("createTerrainSamplerTask");
|
|
2417
4211
|
|
|
2418
|
-
const isSkirtVertex = Fn(([segments]) => {
|
|
2419
|
-
const segmentsNode = typeof segments === "number" ? int(segments) : segments;
|
|
2420
|
-
const vIndex = int(vertexIndex);
|
|
2421
|
-
const segmentEdges = int(segmentsNode.add(3));
|
|
2422
|
-
const vx = vIndex.mod(segmentEdges);
|
|
2423
|
-
const vy = vIndex.div(segmentEdges);
|
|
2424
|
-
const last = segmentEdges.sub(int(1));
|
|
2425
|
-
return vx.equal(int(0)).or(vx.equal(last)).or(vy.equal(int(0))).or(vy.equal(last));
|
|
2426
|
-
});
|
|
2427
|
-
const isSkirtUV = Fn(([segments]) => {
|
|
2428
|
-
const segmentsNode = typeof segments === "number" ? int(segments) : segments;
|
|
2429
|
-
const ux = uv().x;
|
|
2430
|
-
const uy = uv().y;
|
|
2431
|
-
const segmentCount = segmentsNode.add(2);
|
|
2432
|
-
const segmentStep = float(1).div(segmentCount);
|
|
2433
|
-
const innerX = ux.greaterThan(segmentStep).and(ux.lessThan(segmentStep.oneMinus()));
|
|
2434
|
-
const innerY = uy.greaterThan(segmentStep).and(uy.lessThan(segmentStep.oneMinus()));
|
|
2435
|
-
return innerX.and(innerY).not();
|
|
2436
|
-
});
|
|
2437
|
-
|
|
2438
|
-
function createTileBaseWorldPosition(leafStorage, terrainUniforms) {
|
|
2439
|
-
return Fn(() => {
|
|
2440
|
-
const nodeIndex = int(instanceIndex);
|
|
2441
|
-
const nodeOffset = nodeIndex.mul(int(4));
|
|
2442
|
-
const nodeLevel = leafStorage.node.element(nodeOffset).toInt();
|
|
2443
|
-
const nodeX = leafStorage.node.element(nodeOffset.add(int(1))).toFloat();
|
|
2444
|
-
const nodeY = leafStorage.node.element(nodeOffset.add(int(2))).toFloat();
|
|
2445
|
-
const rootSize = terrainUniforms.uRootSize.toVar();
|
|
2446
|
-
const rootOrigin = terrainUniforms.uRootOrigin.toVar();
|
|
2447
|
-
const half = float(0.5);
|
|
2448
|
-
const size = rootSize.div(pow(float(2), nodeLevel.toFloat()));
|
|
2449
|
-
const halfRoot = rootSize.mul(half);
|
|
2450
|
-
const centerX = rootOrigin.x.add(nodeX.add(half).mul(size)).sub(halfRoot);
|
|
2451
|
-
const centerZ = rootOrigin.z.add(nodeY.add(half).mul(size)).sub(halfRoot);
|
|
2452
|
-
const clampedX = positionLocal.x.max(half.negate()).min(half);
|
|
2453
|
-
const clampedZ = positionLocal.z.max(half.negate()).min(half);
|
|
2454
|
-
const worldX = centerX.add(clampedX.mul(size));
|
|
2455
|
-
const worldZ = centerZ.add(clampedZ.mul(size));
|
|
2456
|
-
return vec3(worldX, rootOrigin.y, worldZ);
|
|
2457
|
-
});
|
|
2458
|
-
}
|
|
2459
|
-
function createTileElevation(terrainUniforms, terrainFieldStorage) {
|
|
2460
|
-
if (!terrainFieldStorage) return float(0);
|
|
2461
|
-
const innerSegs = terrainUniforms.uInnerTileSegments;
|
|
2462
|
-
const u = tileLocalToFieldUV$1(positionLocal.x.add(float(0.5)), innerSegs);
|
|
2463
|
-
const v = tileLocalToFieldUV$1(positionLocal.z.add(float(0.5)), innerSegs);
|
|
2464
|
-
return sampleTerrainFieldElevation(
|
|
2465
|
-
terrainFieldStorage,
|
|
2466
|
-
u,
|
|
2467
|
-
v,
|
|
2468
|
-
int(instanceIndex)
|
|
2469
|
-
).mul(terrainUniforms.uElevationScale);
|
|
2470
|
-
}
|
|
2471
|
-
function createNormalAssignment(terrainUniforms, terrainFieldStorage) {
|
|
2472
|
-
if (!terrainFieldStorage) return;
|
|
2473
|
-
const nodeIndex = int(instanceIndex);
|
|
2474
|
-
const edgeVertexCount = int(terrainUniforms.uInnerTileSegments.add(3));
|
|
2475
|
-
const localVertexIndex = int(vertexIndex);
|
|
2476
|
-
const ix = localVertexIndex.mod(edgeVertexCount);
|
|
2477
|
-
const iy = localVertexIndex.div(edgeVertexCount);
|
|
2478
|
-
const normalXZ = loadTerrainFieldNormal(
|
|
2479
|
-
terrainFieldStorage,
|
|
2480
|
-
ix,
|
|
2481
|
-
iy,
|
|
2482
|
-
nodeIndex
|
|
2483
|
-
);
|
|
2484
|
-
const nx = normalXZ.x;
|
|
2485
|
-
const nz = normalXZ.y;
|
|
2486
|
-
const nySq = float(1).sub(nx.mul(nx)).sub(nz.mul(nz)).max(float(0));
|
|
2487
|
-
const ny = nySq.sqrt();
|
|
2488
|
-
normalLocal.assign(vec3(nx, ny, nz));
|
|
2489
|
-
}
|
|
2490
|
-
function createTileWorldPosition(leafStorage, terrainUniforms, terrainFieldStorage) {
|
|
2491
|
-
const baseWorldPosition = createTileBaseWorldPosition(
|
|
2492
|
-
leafStorage,
|
|
2493
|
-
terrainUniforms
|
|
2494
|
-
);
|
|
2495
|
-
return Fn(() => {
|
|
2496
|
-
const base = baseWorldPosition();
|
|
2497
|
-
const yElevation = createTileElevation(
|
|
2498
|
-
terrainUniforms,
|
|
2499
|
-
terrainFieldStorage
|
|
2500
|
-
);
|
|
2501
|
-
const skirtVertex = isSkirtVertex(terrainUniforms.uInnerTileSegments);
|
|
2502
|
-
const skirtY = base.y.add(yElevation).sub(terrainUniforms.uSkirtScale.toVar());
|
|
2503
|
-
const worldY = select(skirtVertex, skirtY, base.y.add(yElevation));
|
|
2504
|
-
createNormalAssignment(terrainUniforms, terrainFieldStorage);
|
|
2505
|
-
return vec3(base.x, worldY, base.z);
|
|
2506
|
-
})();
|
|
2507
|
-
}
|
|
2508
|
-
|
|
2509
4212
|
const positionNodeTask = task((get, work) => {
|
|
2510
4213
|
const leafStorage = get(leafStorageTask);
|
|
2511
4214
|
const terrainUniforms = get(updateUniformsTask);
|
|
2512
4215
|
const terrainFieldStorage = get(createTerrainFieldTextureTask);
|
|
4216
|
+
const topology = get(topologyTask);
|
|
2513
4217
|
return work(
|
|
2514
|
-
() =>
|
|
4218
|
+
() => topology.projection.gpu.renderVertexPosition({
|
|
2515
4219
|
leafStorage,
|
|
2516
|
-
terrainUniforms,
|
|
4220
|
+
uniforms: terrainUniforms,
|
|
2517
4221
|
terrainFieldStorage
|
|
2518
|
-
)
|
|
4222
|
+
})
|
|
2519
4223
|
);
|
|
2520
4224
|
}).displayName("positionNodeTask");
|
|
2521
4225
|
|
|
2522
|
-
function intersectRayAabb(ray, minX, minY, minZ, maxX, maxY, maxZ) {
|
|
2523
|
-
let tMin = -Infinity;
|
|
2524
|
-
let tMax = Infinity;
|
|
2525
|
-
const origin = ray.origin;
|
|
2526
|
-
const dir = ray.direction;
|
|
2527
|
-
const slab = (originAxis, dirAxis, minAxis, maxAxis) => {
|
|
2528
|
-
if (Math.abs(dirAxis) < 1e-8) {
|
|
2529
|
-
if (originAxis < minAxis || originAxis > maxAxis) return false;
|
|
2530
|
-
return true;
|
|
2531
|
-
}
|
|
2532
|
-
const inv = 1 / dirAxis;
|
|
2533
|
-
let t0 = (minAxis - originAxis) * inv;
|
|
2534
|
-
let t1 = (maxAxis - originAxis) * inv;
|
|
2535
|
-
if (t0 > t1) {
|
|
2536
|
-
const tmp = t0;
|
|
2537
|
-
t0 = t1;
|
|
2538
|
-
t1 = tmp;
|
|
2539
|
-
}
|
|
2540
|
-
tMin = Math.max(tMin, t0);
|
|
2541
|
-
tMax = Math.min(tMax, t1);
|
|
2542
|
-
return tMax >= tMin;
|
|
2543
|
-
};
|
|
2544
|
-
if (!slab(origin.x, dir.x, minX, maxX) || !slab(origin.y, dir.y, minY, maxY) || !slab(origin.z, dir.z, minZ, maxZ)) {
|
|
2545
|
-
return null;
|
|
2546
|
-
}
|
|
2547
|
-
return { tMin, tMax };
|
|
2548
|
-
}
|
|
2549
|
-
function getTerrainBounds(config) {
|
|
2550
|
-
const halfRoot = config.rootSize * 0.5;
|
|
2551
|
-
return {
|
|
2552
|
-
minX: config.originX - halfRoot,
|
|
2553
|
-
maxX: config.originX + halfRoot,
|
|
2554
|
-
minZ: config.originZ - halfRoot,
|
|
2555
|
-
maxZ: config.originZ + halfRoot
|
|
2556
|
-
};
|
|
2557
|
-
}
|
|
2558
|
-
function terrainSignedDistanceFromBounds(query, worldX, worldY, worldZ) {
|
|
2559
|
-
const tileBounds = query.getTileBounds(worldX, worldZ);
|
|
2560
|
-
if (tileBounds) {
|
|
2561
|
-
if (worldY > tileBounds.maxElevation) {
|
|
2562
|
-
return worldY - tileBounds.maxElevation;
|
|
2563
|
-
}
|
|
2564
|
-
if (worldY < tileBounds.minElevation) {
|
|
2565
|
-
return worldY - tileBounds.minElevation;
|
|
2566
|
-
}
|
|
2567
|
-
}
|
|
2568
|
-
const elevation = query.getElevation(worldX, worldZ);
|
|
2569
|
-
if (!Number.isFinite(elevation)) return void 0;
|
|
2570
|
-
return worldY - elevation;
|
|
2571
|
-
}
|
|
2572
|
-
function terrainSignedDistancePrecise(query, worldX, worldY, worldZ) {
|
|
2573
|
-
const elevation = query.getElevation(worldX, worldZ);
|
|
2574
|
-
if (!Number.isFinite(elevation)) return void 0;
|
|
2575
|
-
return worldY - elevation;
|
|
2576
|
-
}
|
|
2577
|
-
function cpuRaycast(query, ray, config, options) {
|
|
2578
|
-
const bounds = getTerrainBounds(config);
|
|
2579
|
-
const segment = intersectRayAabb(
|
|
2580
|
-
ray,
|
|
2581
|
-
bounds.minX,
|
|
2582
|
-
config.minY,
|
|
2583
|
-
bounds.minZ,
|
|
2584
|
-
bounds.maxX,
|
|
2585
|
-
config.maxY,
|
|
2586
|
-
bounds.maxZ
|
|
2587
|
-
);
|
|
2588
|
-
if (!segment) return null;
|
|
2589
|
-
const maxDistance = options?.maxDistance ?? Number.POSITIVE_INFINITY;
|
|
2590
|
-
let startT = Math.max(0, segment.tMin);
|
|
2591
|
-
const endT = Math.min(segment.tMax, maxDistance);
|
|
2592
|
-
if (endT < startT) return null;
|
|
2593
|
-
const maxSteps = Math.max(8, options?.maxSteps ?? 128);
|
|
2594
|
-
const refinementSteps = Math.max(1, options?.refinementSteps ?? 8);
|
|
2595
|
-
const point = new Vector3();
|
|
2596
|
-
let prevT = startT;
|
|
2597
|
-
ray.at(prevT, point);
|
|
2598
|
-
let prevSignedDistance = terrainSignedDistanceFromBounds(
|
|
2599
|
-
query,
|
|
2600
|
-
point.x,
|
|
2601
|
-
point.y,
|
|
2602
|
-
point.z
|
|
2603
|
-
);
|
|
2604
|
-
if (prevSignedDistance !== void 0 && prevSignedDistance <= 0) {
|
|
2605
|
-
const sample = query.sampleTerrain(point.x, point.z);
|
|
2606
|
-
if (!sample.valid) return null;
|
|
2607
|
-
point.y = sample.elevation;
|
|
2608
|
-
return {
|
|
2609
|
-
position: point.clone(),
|
|
2610
|
-
normal: sample.normal.clone(),
|
|
2611
|
-
distance: ray.origin.distanceTo(point)
|
|
2612
|
-
};
|
|
2613
|
-
}
|
|
2614
|
-
for (let i = 1; i <= maxSteps; i += 1) {
|
|
2615
|
-
const t = startT + (endT - startT) * i / maxSteps;
|
|
2616
|
-
ray.at(t, point);
|
|
2617
|
-
const signedDistance = terrainSignedDistanceFromBounds(
|
|
2618
|
-
query,
|
|
2619
|
-
point.x,
|
|
2620
|
-
point.y,
|
|
2621
|
-
point.z
|
|
2622
|
-
);
|
|
2623
|
-
if (signedDistance === void 0) {
|
|
2624
|
-
prevSignedDistance = void 0;
|
|
2625
|
-
prevT = t;
|
|
2626
|
-
continue;
|
|
2627
|
-
}
|
|
2628
|
-
if (prevSignedDistance !== void 0 && prevSignedDistance > 0 && signedDistance <= 0) {
|
|
2629
|
-
let lo = prevT;
|
|
2630
|
-
let hi = t;
|
|
2631
|
-
for (let r = 0; r < refinementSteps; r += 1) {
|
|
2632
|
-
const mid = (lo + hi) * 0.5;
|
|
2633
|
-
ray.at(mid, point);
|
|
2634
|
-
const midDistance = terrainSignedDistancePrecise(
|
|
2635
|
-
query,
|
|
2636
|
-
point.x,
|
|
2637
|
-
point.y,
|
|
2638
|
-
point.z
|
|
2639
|
-
);
|
|
2640
|
-
if (midDistance === void 0) {
|
|
2641
|
-
lo = mid;
|
|
2642
|
-
continue;
|
|
2643
|
-
}
|
|
2644
|
-
if (midDistance > 0) lo = mid;
|
|
2645
|
-
else hi = mid;
|
|
2646
|
-
}
|
|
2647
|
-
const hitT = hi;
|
|
2648
|
-
ray.at(hitT, point);
|
|
2649
|
-
const sample = query.sampleTerrain(point.x, point.z);
|
|
2650
|
-
if (!sample.valid) return null;
|
|
2651
|
-
point.y = sample.elevation;
|
|
2652
|
-
return {
|
|
2653
|
-
position: point.clone(),
|
|
2654
|
-
normal: sample.normal.clone(),
|
|
2655
|
-
distance: ray.origin.distanceTo(point)
|
|
2656
|
-
};
|
|
2657
|
-
}
|
|
2658
|
-
prevSignedDistance = signedDistance;
|
|
2659
|
-
prevT = t;
|
|
2660
|
-
}
|
|
2661
|
-
return null;
|
|
2662
|
-
}
|
|
2663
|
-
function cpuRaycastBoundsOnly(ray, config, options) {
|
|
2664
|
-
const bounds = getTerrainBounds(config);
|
|
2665
|
-
const planeY = (config.minY + config.maxY) * 0.5;
|
|
2666
|
-
const dirY = ray.direction.y;
|
|
2667
|
-
if (Math.abs(dirY) < 1e-8) return null;
|
|
2668
|
-
const t = (planeY - ray.origin.y) / dirY;
|
|
2669
|
-
if (t < 0) return null;
|
|
2670
|
-
const maxDistance = options?.maxDistance ?? Number.POSITIVE_INFINITY;
|
|
2671
|
-
if (t > maxDistance) return null;
|
|
2672
|
-
const point = new Vector3();
|
|
2673
|
-
ray.at(t, point);
|
|
2674
|
-
if (point.x < bounds.minX || point.x > bounds.maxX || point.z < bounds.minZ || point.z > bounds.maxZ) {
|
|
2675
|
-
return null;
|
|
2676
|
-
}
|
|
2677
|
-
return {
|
|
2678
|
-
position: point,
|
|
2679
|
-
normal: new Vector3(0, 1, 0),
|
|
2680
|
-
distance: ray.origin.distanceTo(point)
|
|
2681
|
-
};
|
|
2682
|
-
}
|
|
2683
|
-
|
|
2684
4226
|
function createTerrainRaycast(params) {
|
|
2685
4227
|
return {
|
|
2686
4228
|
pick(ray, options) {
|
|
2687
|
-
const
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
coarse.position.x,
|
|
2697
|
-
coarse.position.z
|
|
2698
|
-
);
|
|
2699
|
-
if (sample.valid) {
|
|
2700
|
-
coarse.position.y = sample.elevation;
|
|
2701
|
-
coarse.normal.copy(sample.normal);
|
|
2702
|
-
}
|
|
2703
|
-
}
|
|
2704
|
-
return coarse;
|
|
4229
|
+
const projection = params.getProjection();
|
|
4230
|
+
return projection.cpu.raycast({
|
|
4231
|
+
ray,
|
|
4232
|
+
options,
|
|
4233
|
+
terrainQuery: params.getTerrainQuery(),
|
|
4234
|
+
surfaceQuery: params.getSurfaceQuery(),
|
|
4235
|
+
sphereQuery: params.getSphereQuery(),
|
|
4236
|
+
config: params.getConfig()
|
|
4237
|
+
});
|
|
2705
4238
|
}
|
|
2706
4239
|
};
|
|
2707
4240
|
}
|
|
@@ -2710,42 +4243,60 @@ const BOUNDS_PADDING = 1;
|
|
|
2710
4243
|
const RAYCAST_STATE = Symbol("terrainRaycastTaskState");
|
|
2711
4244
|
const terrainRaycastTask = task(
|
|
2712
4245
|
(get, work) => {
|
|
2713
|
-
const { query: terrainQuery } = get(terrainQueryTask);
|
|
4246
|
+
const { query: terrainQuery, surfaceQuery, sphereQuery } = get(terrainQueryTask);
|
|
2714
4247
|
const rootSizeValue = get(rootSize);
|
|
2715
4248
|
const originValue = get(origin);
|
|
2716
4249
|
const elevationScaleValue = get(elevationScale);
|
|
4250
|
+
const projection = get(topologyTask).projection;
|
|
2717
4251
|
return work((prev) => {
|
|
2718
4252
|
let raycast = prev;
|
|
2719
4253
|
let state = raycast?.[RAYCAST_STATE];
|
|
2720
4254
|
if (!state) {
|
|
2721
4255
|
state = {
|
|
4256
|
+
projection,
|
|
2722
4257
|
terrainQuery: null,
|
|
2723
|
-
|
|
4258
|
+
surfaceQuery: null,
|
|
4259
|
+
sphereQuery: null,
|
|
4260
|
+
config: {
|
|
2724
4261
|
rootSize: 0,
|
|
2725
4262
|
originX: 0,
|
|
4263
|
+
originY: 0,
|
|
2726
4264
|
originZ: 0,
|
|
2727
4265
|
minY: 0,
|
|
2728
|
-
maxY: 0
|
|
4266
|
+
maxY: 0,
|
|
4267
|
+
centerX: 0,
|
|
4268
|
+
centerY: 0,
|
|
4269
|
+
centerZ: 0
|
|
2729
4270
|
}
|
|
2730
4271
|
};
|
|
2731
4272
|
}
|
|
4273
|
+
state.projection = projection;
|
|
2732
4274
|
state.terrainQuery = terrainQuery;
|
|
2733
|
-
state.
|
|
2734
|
-
state.
|
|
2735
|
-
state.
|
|
4275
|
+
state.surfaceQuery = surfaceQuery;
|
|
4276
|
+
state.sphereQuery = sphereQuery;
|
|
4277
|
+
state.config.rootSize = rootSizeValue;
|
|
4278
|
+
state.config.originX = originValue.x;
|
|
4279
|
+
state.config.originY = originValue.y;
|
|
4280
|
+
state.config.originZ = originValue.z;
|
|
4281
|
+
state.config.centerX = projection.center?.x ?? originValue.x;
|
|
4282
|
+
state.config.centerY = projection.center?.y ?? originValue.y;
|
|
4283
|
+
state.config.centerZ = projection.center?.z ?? originValue.z;
|
|
2736
4284
|
const range = terrainQuery.getGlobalElevationRange();
|
|
2737
4285
|
if (range) {
|
|
2738
|
-
state.
|
|
2739
|
-
state.
|
|
4286
|
+
state.config.minY = range.min - BOUNDS_PADDING;
|
|
4287
|
+
state.config.maxY = range.max + BOUNDS_PADDING;
|
|
2740
4288
|
} else {
|
|
2741
4289
|
const verticalExtent = Math.max(1, Math.abs(elevationScaleValue) * 2);
|
|
2742
|
-
state.
|
|
2743
|
-
state.
|
|
4290
|
+
state.config.minY = originValue.y - verticalExtent;
|
|
4291
|
+
state.config.maxY = originValue.y + verticalExtent;
|
|
2744
4292
|
}
|
|
2745
4293
|
if (!raycast) {
|
|
2746
4294
|
raycast = createTerrainRaycast({
|
|
4295
|
+
getProjection: () => state.projection,
|
|
2747
4296
|
getTerrainQuery: () => state.terrainQuery,
|
|
2748
|
-
|
|
4297
|
+
getSurfaceQuery: () => state.surfaceQuery,
|
|
4298
|
+
getSphereQuery: () => state.sphereQuery,
|
|
4299
|
+
getConfig: () => state.config
|
|
2749
4300
|
});
|
|
2750
4301
|
}
|
|
2751
4302
|
raycast[RAYCAST_STATE] = state;
|
|
@@ -2754,15 +4305,12 @@ const terrainRaycastTask = task(
|
|
|
2754
4305
|
}
|
|
2755
4306
|
).displayName("terrainRaycastTask");
|
|
2756
4307
|
|
|
2757
|
-
function terrainGraph() {
|
|
2758
|
-
return graph().add(instanceIdTask).add(quadtreeConfigTask).add(quadtreeUpdateTask).add(leafStorageTask).add(surfaceTask).add(leafGpuBufferTask).add(gpuSpatialIndexStorageTask).add(gpuSpatialIndexUploadTask).add(createUniformsTask).add(updateUniformsTask).add(positionNodeTask).add(createElevationFieldContextTask).add(tileNodesTask).add(createTerrainFieldTextureTask).add(createTerrainSamplerTask).add(elevationFieldStageTask).add(terrainFieldStageTask).add(compileComputeTask).add(executeComputeTask).add(tileBoundsContextTask).add(tileBoundsReductionTask).add(terrainQueryTask).add(terrainReadbackTask).add(terrainRaycastTask);
|
|
2759
|
-
}
|
|
2760
4308
|
const terrainTasks = {
|
|
2761
4309
|
instanceId: instanceIdTask,
|
|
2762
4310
|
quadtreeConfig: quadtreeConfigTask,
|
|
2763
4311
|
quadtreeUpdate: quadtreeUpdateTask,
|
|
2764
4312
|
leafStorage: leafStorageTask,
|
|
2765
|
-
|
|
4313
|
+
topology: topologyTask,
|
|
2766
4314
|
leafGpuBuffer: leafGpuBufferTask,
|
|
2767
4315
|
gpuSpatialIndexStorage: gpuSpatialIndexStorageTask,
|
|
2768
4316
|
gpuSpatialIndexUpload: gpuSpatialIndexUploadTask,
|
|
@@ -2783,6 +4331,13 @@ const terrainTasks = {
|
|
|
2783
4331
|
terrainReadback: terrainReadbackTask,
|
|
2784
4332
|
terrainRaycast: terrainRaycastTask
|
|
2785
4333
|
};
|
|
4334
|
+
function terrainGraph() {
|
|
4335
|
+
const g = graph();
|
|
4336
|
+
for (const t of Object.values(terrainTasks)) {
|
|
4337
|
+
g.add(t);
|
|
4338
|
+
}
|
|
4339
|
+
return g;
|
|
4340
|
+
}
|
|
2786
4341
|
|
|
2787
4342
|
const textureSpaceToVectorSpace = Fn(([value]) => {
|
|
2788
4343
|
return remap(value, float(0), float(1), float(-1), float(1));
|
|
@@ -2793,7 +4348,7 @@ const vectorSpaceToTextureSpace = Fn(([value]) => {
|
|
|
2793
4348
|
const blendAngleCorrectedNormals = Fn(([n1, n2]) => {
|
|
2794
4349
|
const t = vec3(n1.x, n1.y, n1.z.add(1));
|
|
2795
4350
|
const u = vec3(n2.x.negate(), n2.y.negate(), n2.z);
|
|
2796
|
-
const r = t.mul(dot(t, u)).sub(u.mul(t.z)).normalize();
|
|
4351
|
+
const r = t.mul(dot$1(t, u)).sub(u.mul(t.z)).normalize();
|
|
2797
4352
|
return r;
|
|
2798
4353
|
});
|
|
2799
4354
|
const deriveNormalZ = Fn(([normalXY]) => {
|
|
@@ -2836,4 +4391,4 @@ const voronoiCells = Fn((params) => {
|
|
|
2836
4391
|
return k;
|
|
2837
4392
|
});
|
|
2838
4393
|
|
|
2839
|
-
export { ArrayTextureBackend, AtlasBackend, Dir, TerrainGeometry, TerrainMesh,
|
|
4394
|
+
export { ArrayTextureBackend, AtlasBackend, CUBE_FACES, CUBE_FACE_COUNT, Dir, TerrainGeometry, TerrainMesh, U32_EMPTY, allocLeafSet, allocSeamTable, augmentCubeSphereSampler, beginUpdate, blendAngleCorrectedNormals, buildLeafIndex, buildSeams2to1, compileComputeTask, createComputePipelineTasks, createCubeSphereProjection, createCubeSphereTopology, createElevationFieldContextTask, createFlatProjection, createFlatTopology, createInfiniteFlatTopology, createSpatialIndex, createState, createTerrainFieldStorage, createTerrainFieldTextureTask, createTerrainQuery, createTerrainRaycast, createTerrainSampler, createTerrainSamplerTask, createTerrainSurfaceQuery, createTerrainUniforms, createTorusProjection, createTorusTopology, createUniformsTask, cubeFaceBasis, cubeFaceDirection, cubeFaceFromDirection, cubeFacePoint, cubeFaceUVFromDirection, deriveNormalZ, directionToFace, directionToFaceUV, directionToLatLong, elevationFieldStageTask, elevationFn, elevationScale, executeComputeTask, faceUVToCube, getDeviceComputeLimits, gpuSpatialIndexStorageTask, gpuSpatialIndexUploadTask, innerTileSegments, instanceIdTask, isSkirtUV, isSkirtVertex, latLongToDirection, leafGpuBufferTask, leafStorageTask, loadTerrainField, loadTerrainFieldElevation, loadTerrainFieldNormal, maxLevel, maxNodes, origin, packTerrainFieldSample, positionNodeTask, positionToTorusParams, quadtreeConfigTask, quadtreeUpdate, quadtreeUpdateTask, radius, resetLeafSet, resetSeamTable, rootSize, sampleTerrainField, sampleTerrainFieldElevation, skirtScale, sphereTangentFrameNormal, storeTerrainField, tangentFromAxis, terrainFieldFilter, terrainFieldStageTask, terrainGraph, terrainQueryTask, terrainRaycastTask, terrainReadbackTask, terrainTasks, textureSpaceToVectorSpace, tileNodesTask, topology, topologyTask, torusOutwardNormal$1 as torusOutwardNormal, torusUVToPoint, unpackTangentNormal, update, updateUniformsTask, vElevation, vGlobalVertexIndex, vectorSpaceToTextureSpace, voronoiCells, wrap01 };
|