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