@hello-terrain/three 0.0.0-alpha.10 → 0.0.0-alpha.11
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 +1374 -590
- package/dist/index.d.cts +488 -245
- package/dist/index.d.mts +488 -245
- package/dist/index.d.ts +488 -245
- package/dist/index.mjs +1355 -586
- package/package.json +3 -2
package/dist/index.cjs
CHANGED
|
@@ -7,28 +7,27 @@ 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.setIndex(this.generateIndices(innerSegments));
|
|
23
|
+
this.setIndex(this.generateIndices(innerSegments, flipWinding));
|
|
19
24
|
this.setAttribute(
|
|
20
25
|
"position",
|
|
21
|
-
new three.BufferAttribute(
|
|
22
|
-
new Float32Array(this.generatePositions(innerSegments)),
|
|
23
|
-
3
|
|
24
|
-
)
|
|
26
|
+
new three.BufferAttribute(new Float32Array(this.generatePositions(innerSegments)), 3)
|
|
25
27
|
);
|
|
26
28
|
this.setAttribute(
|
|
27
29
|
"normal",
|
|
28
|
-
new three.BufferAttribute(
|
|
29
|
-
new Float32Array(this.generateNormals(innerSegments)),
|
|
30
|
-
3
|
|
31
|
-
)
|
|
30
|
+
new three.BufferAttribute(new Float32Array(this.generateNormals(innerSegments)), 3)
|
|
32
31
|
);
|
|
33
32
|
this.setAttribute(
|
|
34
33
|
"uv",
|
|
@@ -103,12 +102,16 @@ class TerrainGeometry extends three.BufferGeometry {
|
|
|
103
102
|
* triangle 1: a, c, b
|
|
104
103
|
* triangle 2: b, c, d
|
|
105
104
|
*/
|
|
106
|
-
generateIndices(innerSegments) {
|
|
105
|
+
generateIndices(innerSegments, flipWinding = false) {
|
|
107
106
|
const innerEdgeVertexCount = innerSegments + 1;
|
|
108
107
|
const edgeVertexCountWithSkirt = innerEdgeVertexCount + 2;
|
|
109
108
|
const indices = [];
|
|
110
109
|
const cellsPerEdge = edgeVertexCountWithSkirt - 1;
|
|
111
110
|
const mid = Math.floor(cellsPerEdge / 2);
|
|
111
|
+
const pushTri = (v0, v1, v2) => {
|
|
112
|
+
if (flipWinding) indices.push(v0, v2, v1);
|
|
113
|
+
else indices.push(v0, v1, v2);
|
|
114
|
+
};
|
|
112
115
|
for (let y = 0; y < cellsPerEdge; y++) {
|
|
113
116
|
for (let x = 0; x < cellsPerEdge; x++) {
|
|
114
117
|
const a = y * edgeVertexCountWithSkirt + x;
|
|
@@ -125,11 +128,11 @@ class TerrainGeometry extends three.BufferGeometry {
|
|
|
125
128
|
useDefaultDiagonal = (x + y) % 2 === 0;
|
|
126
129
|
}
|
|
127
130
|
if (useDefaultDiagonal) {
|
|
128
|
-
|
|
129
|
-
|
|
131
|
+
pushTri(a, d, b);
|
|
132
|
+
pushTri(a, c, d);
|
|
130
133
|
} else {
|
|
131
|
-
|
|
132
|
-
|
|
134
|
+
pushTri(a, c, b);
|
|
135
|
+
pushTri(b, c, d);
|
|
133
136
|
}
|
|
134
137
|
}
|
|
135
138
|
}
|
|
@@ -220,32 +223,70 @@ class TerrainGeometry extends three.BufferGeometry {
|
|
|
220
223
|
}
|
|
221
224
|
}
|
|
222
225
|
|
|
226
|
+
const rootSize = work.param(256).displayName("rootSize");
|
|
227
|
+
const origin = work.param({
|
|
228
|
+
x: 0,
|
|
229
|
+
y: 0,
|
|
230
|
+
z: 0
|
|
231
|
+
}).displayName("origin");
|
|
232
|
+
const innerTileSegments = work.param(61).displayName("innerTileSegments");
|
|
233
|
+
const skirtScale = work.param(100).displayName("skirtScale");
|
|
234
|
+
const elevationScale = work.param(1).displayName("elevationScale");
|
|
235
|
+
const radius = work.param(1e3).displayName("radius");
|
|
236
|
+
const maxNodes = work.param(1024).displayName("maxNodes");
|
|
237
|
+
const maxLevel = work.param(16).displayName("maxLevel");
|
|
238
|
+
const quadtreeUpdate = work.param({
|
|
239
|
+
cameraOrigin: { x: 0, y: 0, z: 0 },
|
|
240
|
+
mode: "distance",
|
|
241
|
+
distanceFactor: 1.5
|
|
242
|
+
}).displayName("quadtreeUpdate");
|
|
243
|
+
const topology = work.param(null).displayName("topology");
|
|
244
|
+
const terrainFieldFilter = work.param("linear").displayName(
|
|
245
|
+
"terrainFieldFilter"
|
|
246
|
+
);
|
|
247
|
+
const elevationFn = work.param(() => tsl.float(0));
|
|
248
|
+
|
|
223
249
|
const defaultTerrainMeshParams = {
|
|
224
|
-
innerTileSegments
|
|
250
|
+
// Source of truth is the `innerTileSegments` param itself.
|
|
251
|
+
innerTileSegments: innerTileSegments.get(),
|
|
225
252
|
maxNodes: 1024,
|
|
226
|
-
material: new webgpu.MeshStandardNodeMaterial()
|
|
253
|
+
material: new webgpu.MeshStandardNodeMaterial(),
|
|
254
|
+
flipWinding: false
|
|
227
255
|
};
|
|
228
256
|
class TerrainMesh extends webgpu.InstancedMesh {
|
|
229
257
|
_innerTileSegments;
|
|
230
258
|
_maxNodes;
|
|
259
|
+
_flipWinding;
|
|
231
260
|
terrainRaycast = null;
|
|
232
261
|
constructor(params = defaultTerrainMeshParams) {
|
|
233
262
|
const mergedParams = { ...defaultTerrainMeshParams, ...params };
|
|
234
|
-
const { innerTileSegments, maxNodes, material } = mergedParams;
|
|
235
|
-
const geometry = new TerrainGeometry(innerTileSegments, true);
|
|
263
|
+
const { innerTileSegments, maxNodes, material, flipWinding } = mergedParams;
|
|
264
|
+
const geometry = new TerrainGeometry(innerTileSegments, true, flipWinding);
|
|
236
265
|
super(geometry, material, maxNodes);
|
|
237
266
|
this.frustumCulled = false;
|
|
238
267
|
this._innerTileSegments = innerTileSegments;
|
|
239
268
|
this._maxNodes = maxNodes;
|
|
269
|
+
this._flipWinding = flipWinding;
|
|
240
270
|
}
|
|
241
271
|
get innerTileSegments() {
|
|
242
272
|
return this._innerTileSegments;
|
|
243
273
|
}
|
|
244
274
|
set innerTileSegments(tileSegments) {
|
|
275
|
+
if (tileSegments === this._innerTileSegments) return;
|
|
245
276
|
const oldGeometry = this.geometry;
|
|
246
|
-
this.geometry = new TerrainGeometry(tileSegments, true);
|
|
277
|
+
this.geometry = new TerrainGeometry(tileSegments, true, this._flipWinding);
|
|
247
278
|
this._innerTileSegments = tileSegments;
|
|
248
|
-
setTimeout(oldGeometry.dispose);
|
|
279
|
+
setTimeout(() => oldGeometry.dispose());
|
|
280
|
+
}
|
|
281
|
+
get flipWinding() {
|
|
282
|
+
return this._flipWinding;
|
|
283
|
+
}
|
|
284
|
+
set flipWinding(flip) {
|
|
285
|
+
if (flip === this._flipWinding) return;
|
|
286
|
+
const oldGeometry = this.geometry;
|
|
287
|
+
this.geometry = new TerrainGeometry(this._innerTileSegments, true, flip);
|
|
288
|
+
this._flipWinding = flip;
|
|
289
|
+
setTimeout(() => oldGeometry.dispose());
|
|
249
290
|
}
|
|
250
291
|
get maxNodes() {
|
|
251
292
|
return this._maxNodes;
|
|
@@ -311,13 +352,8 @@ function compileComputePipeline(stages, width, options) {
|
|
|
311
352
|
WORKGROUP_X,
|
|
312
353
|
WORKGROUP_Y
|
|
313
354
|
];
|
|
314
|
-
const preferSingleKernelWhenPossible = options?.preferSingleKernelWhenPossible ?? true;
|
|
315
355
|
const uInstanceCount = tsl.uniform(0, "uint");
|
|
316
|
-
let singleKernel;
|
|
317
356
|
const stagedKernelCache = /* @__PURE__ */ new Map();
|
|
318
|
-
function canRunSingleKernel(widthValue, limits) {
|
|
319
|
-
return widthValue <= limits.maxWorkgroupSizeX && widthValue <= limits.maxWorkgroupSizeY && widthValue * widthValue <= limits.maxWorkgroupInvocations;
|
|
320
|
-
}
|
|
321
357
|
function clampWorkgroupToLimits(requested, limits) {
|
|
322
358
|
let x = Math.max(1, Math.floor(requested[0]));
|
|
323
359
|
let y = Math.max(1, Math.floor(requested[1]));
|
|
@@ -333,37 +369,6 @@ function compileComputePipeline(stages, width, options) {
|
|
|
333
369
|
);
|
|
334
370
|
return [x, y];
|
|
335
371
|
}
|
|
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
372
|
function buildStagedKernels(workgroupSize) {
|
|
368
373
|
return stages.map(
|
|
369
374
|
(stage) => tsl.Fn(() => {
|
|
@@ -394,15 +399,7 @@ function compileComputePipeline(stages, width, options) {
|
|
|
394
399
|
}
|
|
395
400
|
function execute(renderer, instanceCount) {
|
|
396
401
|
const limits = getDeviceComputeLimits(renderer);
|
|
397
|
-
const canUseSingleKernel = preferSingleKernelWhenPossible && canRunSingleKernel(width, limits);
|
|
398
402
|
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
403
|
const [workgroupX, workgroupY] = clampWorkgroupToLimits(
|
|
407
404
|
preferredWorkgroup,
|
|
408
405
|
limits
|
|
@@ -545,40 +542,9 @@ function AtlasBackend(edgeVertexCount, tileCount, options) {
|
|
|
545
542
|
}
|
|
546
543
|
};
|
|
547
544
|
}
|
|
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
|
-
};
|
|
545
|
+
function texture3DBackend(edgeVertexCount, tileCount, options) {
|
|
546
|
+
const storage = ArrayTextureBackend(edgeVertexCount, tileCount, options);
|
|
547
|
+
return { ...storage, backendType: "texture-3d" };
|
|
582
548
|
}
|
|
583
549
|
function tryGetDeviceLimits(renderer) {
|
|
584
550
|
const backend = renderer;
|
|
@@ -592,7 +558,7 @@ function createTerrainFieldStorage(edgeVertexCount, tileCount, renderer, options
|
|
|
592
558
|
return AtlasBackend(edgeVertexCount, tileCount, { filter, format });
|
|
593
559
|
}
|
|
594
560
|
if (forcedBackend === "texture-3d") {
|
|
595
|
-
return
|
|
561
|
+
return texture3DBackend(edgeVertexCount, tileCount, { filter, format });
|
|
596
562
|
}
|
|
597
563
|
if (forcedBackend === "array-texture") {
|
|
598
564
|
return ArrayTextureBackend(edgeVertexCount, tileCount, { filter, format });
|
|
@@ -635,10 +601,6 @@ function sampleTerrainField(storage, u, v, tileIndex) {
|
|
|
635
601
|
function sampleTerrainFieldElevation(storage, u, v, tileIndex) {
|
|
636
602
|
return sampleTerrainField(storage, u, v, tileIndex).r;
|
|
637
603
|
}
|
|
638
|
-
function sampleTerrainFieldNormal(storage, u, v, tileIndex) {
|
|
639
|
-
const raw = sampleTerrainField(storage, u, v, tileIndex);
|
|
640
|
-
return tsl.vec2(raw.g, raw.b);
|
|
641
|
-
}
|
|
642
604
|
function packTerrainFieldSample(height, normalXZ, extra = tsl.float(0)) {
|
|
643
605
|
return tsl.vec4(height, normalXZ.x, normalXZ.y, extra);
|
|
644
606
|
}
|
|
@@ -665,23 +627,132 @@ const createElevation = (tile, uniforms, elevationFn) => {
|
|
|
665
627
|
};
|
|
666
628
|
};
|
|
667
629
|
|
|
668
|
-
|
|
630
|
+
const CUBE_FACE_COUNT = 6;
|
|
631
|
+
const CUBE_FACES = [
|
|
632
|
+
// 0: +X
|
|
633
|
+
{ forward: [1, 0, 0], right: [0, 0, -1], up: [0, 1, 0] },
|
|
634
|
+
// 1: -X
|
|
635
|
+
{ forward: [-1, 0, 0], right: [0, 0, 1], up: [0, 1, 0] },
|
|
636
|
+
// 2: +Y (north pole)
|
|
637
|
+
{ forward: [0, 1, 0], right: [1, 0, 0], up: [0, 0, -1] },
|
|
638
|
+
// 3: -Y (south pole)
|
|
639
|
+
{ forward: [0, -1, 0], right: [1, 0, 0], up: [0, 0, 1] },
|
|
640
|
+
// 4: +Z
|
|
641
|
+
{ forward: [0, 0, 1], right: [1, 0, 0], up: [0, 1, 0] },
|
|
642
|
+
// 5: -Z
|
|
643
|
+
{ forward: [0, 0, -1], right: [-1, 0, 0], up: [0, 1, 0] }
|
|
644
|
+
];
|
|
645
|
+
|
|
646
|
+
function vec3Const(v) {
|
|
647
|
+
return tsl.vec3(tsl.float(v[0]), tsl.float(v[1]), tsl.float(v[2]));
|
|
648
|
+
}
|
|
649
|
+
function selectFaceVec3(face, pick) {
|
|
650
|
+
const last = CUBE_FACES.length - 1;
|
|
651
|
+
let acc = vec3Const(pick(CUBE_FACES[last]));
|
|
652
|
+
for (let i = last - 1; i >= 0; i--) {
|
|
653
|
+
acc = tsl.select(tsl.int(face).equal(tsl.int(i)), vec3Const(pick(CUBE_FACES[i])), acc);
|
|
654
|
+
}
|
|
655
|
+
return acc;
|
|
656
|
+
}
|
|
657
|
+
function cubeFaceBasis(face) {
|
|
658
|
+
return {
|
|
659
|
+
forward: selectFaceVec3(face, (f) => f.forward),
|
|
660
|
+
right: selectFaceVec3(face, (f) => f.right),
|
|
661
|
+
up: selectFaceVec3(face, (f) => f.up)
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
function cubeFacePoint(basis, u, v) {
|
|
665
|
+
const s = tsl.float(u).mul(2).sub(1);
|
|
666
|
+
const t = tsl.float(v).mul(2).sub(1);
|
|
667
|
+
return basis.forward.add(basis.right.mul(s)).add(basis.up.mul(t));
|
|
668
|
+
}
|
|
669
|
+
function cubeFaceDirection(basis, u, v) {
|
|
670
|
+
return cubeFacePoint(basis, u, v).normalize();
|
|
671
|
+
}
|
|
672
|
+
function tangentFromAxis(dir, axis) {
|
|
673
|
+
return axis.sub(dir.mul(dir.dot(axis))).normalize();
|
|
674
|
+
}
|
|
675
|
+
function unpackTangentNormal(nx, nz) {
|
|
676
|
+
const ny = tsl.float(1).sub(nx.mul(nx)).sub(nz.mul(nz)).max(tsl.float(0)).sqrt();
|
|
677
|
+
return tsl.vec3(nx, ny, nz);
|
|
678
|
+
}
|
|
679
|
+
function sphereTangentFrameNormal(dir, basis, tangentNormal) {
|
|
680
|
+
const n = tsl.vec3(tangentNormal);
|
|
681
|
+
const tu = tangentFromAxis(dir, basis.right);
|
|
682
|
+
const tv = tangentFromAxis(dir, basis.up);
|
|
683
|
+
return tu.mul(n.x).add(dir.mul(n.y)).add(tv.mul(n.z)).normalize();
|
|
684
|
+
}
|
|
685
|
+
function cubeFaceFromDirection(dir) {
|
|
686
|
+
const d = tsl.vec3(dir);
|
|
687
|
+
const ax = d.x.abs();
|
|
688
|
+
const ay = d.y.abs();
|
|
689
|
+
const az = d.z.abs();
|
|
690
|
+
const faceX = tsl.select(d.x.greaterThanEqual(tsl.float(0)), tsl.int(0), tsl.int(1));
|
|
691
|
+
const faceY = tsl.select(d.y.greaterThanEqual(tsl.float(0)), tsl.int(2), tsl.int(3));
|
|
692
|
+
const faceZ = tsl.select(d.z.greaterThanEqual(tsl.float(0)), tsl.int(4), tsl.int(5));
|
|
693
|
+
const xDominant = ax.greaterThanEqual(ay).and(ax.greaterThanEqual(az));
|
|
694
|
+
const yDominant = ay.greaterThanEqual(ax).and(ay.greaterThanEqual(az));
|
|
695
|
+
return tsl.select(xDominant, faceX, tsl.select(yDominant, faceY, faceZ));
|
|
696
|
+
}
|
|
697
|
+
function cubeFaceUVFromDirection(basis, dir) {
|
|
698
|
+
const d = tsl.vec3(dir);
|
|
699
|
+
const p = d.div(d.dot(basis.forward));
|
|
700
|
+
const s = p.dot(basis.right);
|
|
701
|
+
const t = p.dot(basis.up);
|
|
702
|
+
return tsl.vec2(s.add(tsl.float(1)).mul(tsl.float(0.5)), t.add(tsl.float(1)).mul(tsl.float(0.5)));
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
const HALF_PI = Math.PI * 0.5;
|
|
706
|
+
const FIELD_INNER_TEXEL_OFFSET = 1.5;
|
|
707
|
+
const FIELD_EDGE_EXTRA_TEXELS = 3;
|
|
708
|
+
function sphereTileArcLength(radius, levelDivisor) {
|
|
709
|
+
return radius * HALF_PI / levelDivisor;
|
|
710
|
+
}
|
|
711
|
+
function decodeLeafTile(leafStorage, nodeIndex) {
|
|
712
|
+
const nodeOffset = tsl.int(nodeIndex).mul(tsl.int(4));
|
|
713
|
+
return {
|
|
714
|
+
level: leafStorage.node.element(nodeOffset).toInt(),
|
|
715
|
+
x: leafStorage.node.element(nodeOffset.add(tsl.int(1))).toFloat(),
|
|
716
|
+
y: leafStorage.node.element(nodeOffset.add(tsl.int(2))).toFloat(),
|
|
717
|
+
face: leafStorage.node.element(nodeOffset.add(tsl.int(3))).toInt()
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
function faceUVFromTileLocal(tile, localU, localV) {
|
|
721
|
+
const n = tsl.pow(tsl.float(2), tile.level.toFloat());
|
|
722
|
+
return tsl.vec2(tile.x.add(localU).div(n), tile.y.add(localV).div(n));
|
|
723
|
+
}
|
|
724
|
+
function createTileCompute(leafStorage, uniforms, projection = "flat") {
|
|
725
|
+
const isSphere = projection === "cubeSphere";
|
|
669
726
|
const tileLevel = tsl.Fn(([nodeIndex]) => {
|
|
670
|
-
|
|
671
|
-
|
|
727
|
+
return decodeLeafTile(leafStorage, nodeIndex).level;
|
|
728
|
+
});
|
|
729
|
+
const tileFace = tsl.Fn(([nodeIndex]) => {
|
|
730
|
+
return decodeLeafTile(leafStorage, nodeIndex).face;
|
|
672
731
|
});
|
|
673
732
|
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);
|
|
733
|
+
const tile = decodeLeafTile(leafStorage, nodeIndex);
|
|
734
|
+
return tsl.vec2(tile.x, tile.y);
|
|
678
735
|
});
|
|
679
736
|
const tileSize = tsl.Fn(([nodeIndex]) => {
|
|
680
|
-
const rootSize = uniforms.uRootSize.toVar();
|
|
681
737
|
const level = tileLevel(nodeIndex);
|
|
682
|
-
|
|
738
|
+
const divisor = tsl.pow(tsl.float(2), level.toFloat());
|
|
739
|
+
if (isSphere) {
|
|
740
|
+
return uniforms.uRadius.toVar().mul(tsl.float(HALF_PI)).div(divisor);
|
|
741
|
+
}
|
|
742
|
+
const rootSize = uniforms.uRootSize.toVar();
|
|
743
|
+
return tsl.float(rootSize).div(divisor);
|
|
744
|
+
});
|
|
745
|
+
const tileFaceUV = tsl.Fn(([nodeIndex, ix, iy]) => {
|
|
746
|
+
const tile = decodeLeafTile(leafStorage, nodeIndex);
|
|
747
|
+
const fInnerSegments = uniforms.uInnerTileSegments.toVar().toFloat();
|
|
748
|
+
const localU = tsl.int(ix).toFloat().sub(tsl.float(1)).div(fInnerSegments);
|
|
749
|
+
const localV = tsl.int(iy).toFloat().sub(tsl.float(1)).div(fInnerSegments);
|
|
750
|
+
return faceUVFromTileLocal(tile, localU, localV);
|
|
683
751
|
});
|
|
684
752
|
const rootUVCompute = tsl.Fn(([nodeIndex, ix, iy]) => {
|
|
753
|
+
if (isSphere) {
|
|
754
|
+
return tileFaceUV(nodeIndex, ix, iy);
|
|
755
|
+
}
|
|
685
756
|
const nodeVec2 = tileOriginVec2(nodeIndex);
|
|
686
757
|
const nodeX = nodeVec2.x;
|
|
687
758
|
const nodeY = nodeVec2.y;
|
|
@@ -706,6 +777,12 @@ function createTileCompute(leafStorage, uniforms) {
|
|
|
706
777
|
const tileVertexWorldPositionCompute = tsl.Fn(
|
|
707
778
|
([nodeIndex, ix, iy]) => {
|
|
708
779
|
const rootOrigin = uniforms.uRootOrigin.toVar();
|
|
780
|
+
if (isSphere) {
|
|
781
|
+
const faceUV = tileFaceUV(nodeIndex, ix, iy);
|
|
782
|
+
const basis = cubeFaceBasis(tileFace(nodeIndex));
|
|
783
|
+
const dir = cubeFaceDirection(basis, faceUV.x, faceUV.y);
|
|
784
|
+
return rootOrigin.add(dir.mul(uniforms.uRadius.toVar()));
|
|
785
|
+
}
|
|
709
786
|
const nodeVec2 = tileOriginVec2(nodeIndex);
|
|
710
787
|
const nodeX = nodeVec2.x;
|
|
711
788
|
const nodeY = nodeVec2.y;
|
|
@@ -724,36 +801,22 @@ function createTileCompute(leafStorage, uniforms) {
|
|
|
724
801
|
);
|
|
725
802
|
return {
|
|
726
803
|
tileLevel,
|
|
804
|
+
tileFace,
|
|
727
805
|
tileOriginVec2,
|
|
728
806
|
tileSize,
|
|
807
|
+
tileFaceUV,
|
|
729
808
|
rootUVCompute,
|
|
730
809
|
tileVertexWorldPositionCompute
|
|
731
810
|
};
|
|
732
811
|
}
|
|
733
|
-
function tileLocalToFieldUV
|
|
734
|
-
const edge = tsl.float(innerSegments).add(tsl.float(
|
|
735
|
-
return tsl.float(localCoord).mul(tsl.float(innerSegments)).add(tsl.float(
|
|
812
|
+
function tileLocalToFieldUV(localCoord, innerSegments) {
|
|
813
|
+
const edge = tsl.float(innerSegments).add(tsl.float(FIELD_EDGE_EXTRA_TEXELS));
|
|
814
|
+
return tsl.float(localCoord).mul(tsl.float(innerSegments)).add(tsl.float(FIELD_INNER_TEXEL_OFFSET)).div(edge);
|
|
815
|
+
}
|
|
816
|
+
function tileLocalToFieldUVNumber(localCoord, innerSegments) {
|
|
817
|
+
const edge = innerSegments + FIELD_EDGE_EXTRA_TEXELS;
|
|
818
|
+
return (localCoord * innerSegments + FIELD_INNER_TEXEL_OFFSET) / edge;
|
|
736
819
|
}
|
|
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
820
|
|
|
758
821
|
function createLeafStorage(maxNodes) {
|
|
759
822
|
const data = new Int32Array(maxNodes * 4);
|
|
@@ -938,10 +1001,10 @@ function buildLeafIndex(leaves, out) {
|
|
|
938
1001
|
return index;
|
|
939
1002
|
}
|
|
940
1003
|
|
|
941
|
-
function createState(cfg,
|
|
942
|
-
const store = createNodeStore(cfg.maxNodes,
|
|
1004
|
+
function createState(cfg, topology) {
|
|
1005
|
+
const store = createNodeStore(cfg.maxNodes, topology.spaceCount);
|
|
943
1006
|
const scratchRootTiles = [];
|
|
944
|
-
for (let i = 0; i <
|
|
1007
|
+
for (let i = 0; i < topology.maxRootCount; i++) {
|
|
945
1008
|
scratchRootTiles.push({ space: 0, level: 0, x: 0, y: 0 });
|
|
946
1009
|
}
|
|
947
1010
|
return {
|
|
@@ -951,7 +1014,7 @@ function createState(cfg, surface) {
|
|
|
951
1014
|
leafNodeIds: new Uint32Array(cfg.maxNodes),
|
|
952
1015
|
leafIndex: createSpatialIndex(cfg.maxNodes),
|
|
953
1016
|
stack: new Uint32Array(cfg.maxNodes),
|
|
954
|
-
rootNodeIds: new Uint32Array(
|
|
1017
|
+
rootNodeIds: new Uint32Array(topology.maxRootCount),
|
|
955
1018
|
rootCount: 0,
|
|
956
1019
|
splitQueue: new Uint32Array(cfg.maxNodes),
|
|
957
1020
|
splitStamp: new Uint16Array(cfg.maxNodes),
|
|
@@ -960,25 +1023,25 @@ function createState(cfg, surface) {
|
|
|
960
1023
|
scratchNeighbor: { space: 0, level: 0, x: 0, y: 0 },
|
|
961
1024
|
scratchBounds: { cx: 0, cy: 0, cz: 0, r: 0 },
|
|
962
1025
|
scratchRootTiles,
|
|
963
|
-
spaceCount:
|
|
1026
|
+
spaceCount: topology.spaceCount
|
|
964
1027
|
};
|
|
965
1028
|
}
|
|
966
|
-
function beginUpdate(state,
|
|
967
|
-
if (
|
|
1029
|
+
function beginUpdate(state, topology, params) {
|
|
1030
|
+
if (topology.spaceCount !== state.spaceCount) {
|
|
968
1031
|
throw new Error(
|
|
969
|
-
`
|
|
1032
|
+
`Topology spaceCount changed (${state.spaceCount} -> ${topology.spaceCount}). Create a new quadtree state.`
|
|
970
1033
|
);
|
|
971
1034
|
}
|
|
972
|
-
if (
|
|
1035
|
+
if (topology.maxRootCount !== state.rootNodeIds.length) {
|
|
973
1036
|
throw new Error(
|
|
974
|
-
`
|
|
1037
|
+
`Topology maxRootCount changed (${state.rootNodeIds.length} -> ${topology.maxRootCount}). Create a new quadtree state.`
|
|
975
1038
|
);
|
|
976
1039
|
}
|
|
977
1040
|
beginFrame(state.store);
|
|
978
1041
|
state.rootCount = 0;
|
|
979
|
-
const rootCount =
|
|
980
|
-
if (rootCount < 0 || rootCount >
|
|
981
|
-
throw new Error(`
|
|
1042
|
+
const rootCount = topology.rootTiles(params.cameraOrigin, state.scratchRootTiles);
|
|
1043
|
+
if (rootCount < 0 || rootCount > topology.maxRootCount) {
|
|
1044
|
+
throw new Error(`Topology returned invalid root count (${rootCount}).`);
|
|
982
1045
|
}
|
|
983
1046
|
for (let i = 0; i < rootCount; i++) {
|
|
984
1047
|
const rootId = allocNode(state.store, state.scratchRootTiles[i]);
|
|
@@ -1015,7 +1078,7 @@ function shouldSplit(bounds, level, maxLevel, params) {
|
|
|
1015
1078
|
return safeDistSq < threshold * threshold;
|
|
1016
1079
|
}
|
|
1017
1080
|
|
|
1018
|
-
function refineLeaves(state,
|
|
1081
|
+
function refineLeaves(state, topology, params, outLeaves) {
|
|
1019
1082
|
const leaves = outLeaves ?? state.leaves;
|
|
1020
1083
|
resetLeafSet(leaves);
|
|
1021
1084
|
const store = state.store;
|
|
@@ -1036,7 +1099,7 @@ function refineLeaves(state, surface, params, outLeaves) {
|
|
|
1036
1099
|
tile.x = x;
|
|
1037
1100
|
tile.y = y;
|
|
1038
1101
|
const bounds = state.scratchBounds;
|
|
1039
|
-
|
|
1102
|
+
topology.tileBounds(tile, params.cameraOrigin, bounds);
|
|
1040
1103
|
if (hasChildren(store, nodeId)) {
|
|
1041
1104
|
const base = store.firstChild[nodeId];
|
|
1042
1105
|
stack[sp++] = base + 3;
|
|
@@ -1084,7 +1147,7 @@ function scheduleSplit(state, nodeId, count) {
|
|
|
1084
1147
|
state.splitQueue[count] = nodeId;
|
|
1085
1148
|
return count + 1;
|
|
1086
1149
|
}
|
|
1087
|
-
function balance2to1(state,
|
|
1150
|
+
function balance2to1(state, topology, params, leaves) {
|
|
1088
1151
|
const maxIters = state.cfg.maxLevel + 1;
|
|
1089
1152
|
for (let iter = 0; iter < maxIters; iter++) {
|
|
1090
1153
|
const index = buildLeafIndex(leaves, state.leafIndex);
|
|
@@ -1105,7 +1168,7 @@ function balance2to1(state, surface, params, leaves) {
|
|
|
1105
1168
|
tile.x = leafX >>> shift;
|
|
1106
1169
|
tile.y = leafY >>> shift;
|
|
1107
1170
|
const neighbor = state.scratchNeighbor;
|
|
1108
|
-
if (!
|
|
1171
|
+
if (!topology.neighborSameLevel(tile, dir, neighbor)) break;
|
|
1109
1172
|
const j = lookupSpatialIndexRaw(
|
|
1110
1173
|
index,
|
|
1111
1174
|
neighbor.space,
|
|
@@ -1129,18 +1192,38 @@ function balance2to1(state, surface, params, leaves) {
|
|
|
1129
1192
|
if (base !== U32_EMPTY) anySplit = true;
|
|
1130
1193
|
}
|
|
1131
1194
|
if (!anySplit) return leaves;
|
|
1132
|
-
refineLeaves(state,
|
|
1195
|
+
refineLeaves(state, topology, params, leaves);
|
|
1133
1196
|
}
|
|
1134
1197
|
return leaves;
|
|
1135
1198
|
}
|
|
1136
1199
|
|
|
1137
|
-
function update(state,
|
|
1138
|
-
const
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
const
|
|
1142
|
-
const
|
|
1143
|
-
|
|
1200
|
+
function update(state, topology, params, outLeaves) {
|
|
1201
|
+
const cam = params.cameraOrigin;
|
|
1202
|
+
const elevation = params.elevationAtCameraXZ ?? 0;
|
|
1203
|
+
const origX = cam.x;
|
|
1204
|
+
const origY = cam.y;
|
|
1205
|
+
const origZ = cam.z;
|
|
1206
|
+
if (topology.projection === "cubeSphere") {
|
|
1207
|
+
const center = topology.center ?? { x: 0, y: 0, z: 0 };
|
|
1208
|
+
const dx = cam.x - center.x;
|
|
1209
|
+
const dy = cam.y - center.y;
|
|
1210
|
+
const dz = cam.z - center.z;
|
|
1211
|
+
const len = Math.hypot(dx, dy, dz);
|
|
1212
|
+
if (len > 1e-12) {
|
|
1213
|
+
const inv = elevation / len;
|
|
1214
|
+
cam.x -= dx * inv;
|
|
1215
|
+
cam.y -= dy * inv;
|
|
1216
|
+
cam.z -= dz * inv;
|
|
1217
|
+
}
|
|
1218
|
+
} else {
|
|
1219
|
+
cam.y -= elevation;
|
|
1220
|
+
}
|
|
1221
|
+
beginUpdate(state, topology, params);
|
|
1222
|
+
const leaves = refineLeaves(state, topology, params, outLeaves);
|
|
1223
|
+
const result = balance2to1(state, topology, params, leaves);
|
|
1224
|
+
cam.x = origX;
|
|
1225
|
+
cam.y = origY;
|
|
1226
|
+
cam.z = origZ;
|
|
1144
1227
|
return result;
|
|
1145
1228
|
}
|
|
1146
1229
|
|
|
@@ -1148,7 +1231,7 @@ const scratchTile = { space: 0, level: 0, x: 0, y: 0 };
|
|
|
1148
1231
|
const scratchNbr = { space: 0, level: 0, x: 0, y: 0 };
|
|
1149
1232
|
const scratchParentTile = { space: 0, level: 0, x: 0, y: 0 };
|
|
1150
1233
|
const scratchParentNbr = { space: 0, level: 0, x: 0, y: 0 };
|
|
1151
|
-
function buildSeams2to1(
|
|
1234
|
+
function buildSeams2to1(topology, leaves, outSeams, outIndex) {
|
|
1152
1235
|
if (outSeams.capacity < leaves.count) {
|
|
1153
1236
|
throw new Error("SeamTable capacity is smaller than LeafSet.count.");
|
|
1154
1237
|
}
|
|
@@ -1169,7 +1252,7 @@ function buildSeams2to1(surface, leaves, outSeams, outIndex) {
|
|
|
1169
1252
|
scratchTile.level = level;
|
|
1170
1253
|
scratchTile.x = x;
|
|
1171
1254
|
scratchTile.y = y;
|
|
1172
|
-
if (!
|
|
1255
|
+
if (!topology.neighborSameLevel(scratchTile, dir, scratchNbr)) continue;
|
|
1173
1256
|
let j = lookupSpatialIndexRaw(index, scratchNbr.space, scratchNbr.level, scratchNbr.x, scratchNbr.y);
|
|
1174
1257
|
if (j !== U32_EMPTY) {
|
|
1175
1258
|
neighbors[outOffset + 0] = j;
|
|
@@ -1182,7 +1265,7 @@ function buildSeams2to1(surface, leaves, outSeams, outIndex) {
|
|
|
1182
1265
|
scratchParentTile.level = level - 1;
|
|
1183
1266
|
scratchParentTile.x = px;
|
|
1184
1267
|
scratchParentTile.y = py;
|
|
1185
|
-
if (
|
|
1268
|
+
if (topology.neighborSameLevel(scratchParentTile, dir, scratchParentNbr)) {
|
|
1186
1269
|
j = lookupSpatialIndexRaw(
|
|
1187
1270
|
index,
|
|
1188
1271
|
scratchParentNbr.space,
|
|
@@ -1238,10 +1321,10 @@ function buildSeams2to1(surface, leaves, outSeams, outIndex) {
|
|
|
1238
1321
|
return outSeams;
|
|
1239
1322
|
}
|
|
1240
1323
|
|
|
1241
|
-
function
|
|
1324
|
+
function createFlatTopology(cfg) {
|
|
1242
1325
|
const halfRoot = 0.5 * cfg.rootSize;
|
|
1243
1326
|
const maxHeight = cfg.maxHeight ?? 0;
|
|
1244
|
-
const
|
|
1327
|
+
const topology = {
|
|
1245
1328
|
spaceCount: 1,
|
|
1246
1329
|
maxRootCount: 1,
|
|
1247
1330
|
neighborSameLevel(tile, dir, out) {
|
|
@@ -1296,10 +1379,10 @@ function createFlatSurface(cfg) {
|
|
|
1296
1379
|
return 1;
|
|
1297
1380
|
}
|
|
1298
1381
|
};
|
|
1299
|
-
return
|
|
1382
|
+
return topology;
|
|
1300
1383
|
}
|
|
1301
1384
|
|
|
1302
|
-
function
|
|
1385
|
+
function createInfiniteFlatTopology(cfg) {
|
|
1303
1386
|
const halfRoot = 0.5 * cfg.rootSize;
|
|
1304
1387
|
const maxHeight = cfg.maxHeight ?? 0;
|
|
1305
1388
|
const rootGridRadius = Math.max(0, Math.floor(cfg.rootGridRadius ?? 1));
|
|
@@ -1363,18 +1446,158 @@ function createInfiniteFlatSurface(cfg) {
|
|
|
1363
1446
|
};
|
|
1364
1447
|
}
|
|
1365
1448
|
|
|
1366
|
-
|
|
1449
|
+
const DEG_TO_RAD = Math.PI / 180;
|
|
1450
|
+
const RAD_TO_DEG = 180 / Math.PI;
|
|
1451
|
+
function dot(a, b) {
|
|
1452
|
+
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
1453
|
+
}
|
|
1454
|
+
function faceUVToCube(face, u, v, out) {
|
|
1455
|
+
const f = CUBE_FACES[face];
|
|
1456
|
+
const s = 2 * u - 1;
|
|
1457
|
+
const t = 2 * v - 1;
|
|
1458
|
+
out[0] = f.forward[0] + s * f.right[0] + t * f.up[0];
|
|
1459
|
+
out[1] = f.forward[1] + s * f.right[1] + t * f.up[1];
|
|
1460
|
+
out[2] = f.forward[2] + s * f.right[2] + t * f.up[2];
|
|
1461
|
+
}
|
|
1462
|
+
function directionToFace(d) {
|
|
1463
|
+
const ax = Math.abs(d[0]);
|
|
1464
|
+
const ay = Math.abs(d[1]);
|
|
1465
|
+
const az = Math.abs(d[2]);
|
|
1466
|
+
if (ax >= ay && ax >= az) return d[0] >= 0 ? 0 : 1;
|
|
1467
|
+
if (ay >= ax && ay >= az) return d[1] >= 0 ? 2 : 3;
|
|
1468
|
+
return d[2] >= 0 ? 4 : 5;
|
|
1469
|
+
}
|
|
1470
|
+
function directionToFaceUV(face, d, out) {
|
|
1471
|
+
const f = CUBE_FACES[face];
|
|
1472
|
+
const denom = dot(d, f.forward);
|
|
1473
|
+
const inv = 1 / denom;
|
|
1474
|
+
const px = d[0] * inv;
|
|
1475
|
+
const py = d[1] * inv;
|
|
1476
|
+
const pz = d[2] * inv;
|
|
1477
|
+
const p = [px, py, pz];
|
|
1478
|
+
const s = dot(p, f.right);
|
|
1479
|
+
const t = dot(p, f.up);
|
|
1480
|
+
out[0] = (s + 1) * 0.5;
|
|
1481
|
+
out[1] = (t + 1) * 0.5;
|
|
1482
|
+
}
|
|
1483
|
+
function latLongToDirection(latDeg, lonDeg, out) {
|
|
1484
|
+
const lat = latDeg * DEG_TO_RAD;
|
|
1485
|
+
const lon = lonDeg * DEG_TO_RAD;
|
|
1486
|
+
const cosLat = Math.cos(lat);
|
|
1487
|
+
out[0] = cosLat * Math.sin(lon);
|
|
1488
|
+
out[1] = Math.sin(lat);
|
|
1489
|
+
out[2] = cosLat * Math.cos(lon);
|
|
1490
|
+
}
|
|
1491
|
+
function directionToLatLong(d) {
|
|
1492
|
+
const len = Math.hypot(d[0], d[1], d[2]) || 1;
|
|
1493
|
+
const y = Math.max(-1, Math.min(1, d[1] / len));
|
|
1494
|
+
return {
|
|
1495
|
+
latitude: Math.asin(y) * RAD_TO_DEG,
|
|
1496
|
+
longitude: Math.atan2(d[0], d[2]) * RAD_TO_DEG
|
|
1497
|
+
};
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
function createCubeSphereTopology(cfg) {
|
|
1501
|
+
const radius = cfg.radius;
|
|
1502
|
+
const maxHeight = cfg.maxHeight ?? 0;
|
|
1503
|
+
const center = cfg.center ?? { x: 0, y: 0, z: 0 };
|
|
1504
|
+
const cube = [0, 0, 0];
|
|
1505
|
+
const uv = [0, 0];
|
|
1506
|
+
function crossFaceNeighbor(face, level, nx, ny, out) {
|
|
1507
|
+
const n = 1 << level;
|
|
1508
|
+
const u = (nx + 0.5) / n;
|
|
1509
|
+
const v = (ny + 0.5) / n;
|
|
1510
|
+
faceUVToCube(face, u, v, cube);
|
|
1511
|
+
const len = Math.hypot(cube[0], cube[1], cube[2]);
|
|
1512
|
+
const dir = [cube[0] / len, cube[1] / len, cube[2] / len];
|
|
1513
|
+
const nbrFace = directionToFace(dir);
|
|
1514
|
+
directionToFaceUV(nbrFace, dir, uv);
|
|
1515
|
+
let bx = Math.floor(uv[0] * n);
|
|
1516
|
+
let by = Math.floor(uv[1] * n);
|
|
1517
|
+
if (bx < 0) bx = 0;
|
|
1518
|
+
else if (bx > n - 1) bx = n - 1;
|
|
1519
|
+
if (by < 0) by = 0;
|
|
1520
|
+
else if (by > n - 1) by = n - 1;
|
|
1521
|
+
out.space = nbrFace;
|
|
1522
|
+
out.level = level;
|
|
1523
|
+
out.x = bx;
|
|
1524
|
+
out.y = by;
|
|
1525
|
+
}
|
|
1367
1526
|
return {
|
|
1368
1527
|
spaceCount: 6,
|
|
1369
1528
|
maxRootCount: 6,
|
|
1370
|
-
neighborSameLevel(
|
|
1371
|
-
|
|
1529
|
+
neighborSameLevel(tile, dir, out) {
|
|
1530
|
+
const level = tile.level;
|
|
1531
|
+
const n = 1 << level;
|
|
1532
|
+
let nx = tile.x;
|
|
1533
|
+
let ny = tile.y;
|
|
1534
|
+
switch (dir) {
|
|
1535
|
+
case 0:
|
|
1536
|
+
nx -= 1;
|
|
1537
|
+
break;
|
|
1538
|
+
case 1:
|
|
1539
|
+
nx += 1;
|
|
1540
|
+
break;
|
|
1541
|
+
case 2:
|
|
1542
|
+
ny -= 1;
|
|
1543
|
+
break;
|
|
1544
|
+
case 3:
|
|
1545
|
+
ny += 1;
|
|
1546
|
+
break;
|
|
1547
|
+
}
|
|
1548
|
+
if (nx >= 0 && ny >= 0 && nx < n && ny < n) {
|
|
1549
|
+
out.space = tile.space;
|
|
1550
|
+
out.level = level;
|
|
1551
|
+
out.x = nx;
|
|
1552
|
+
out.y = ny;
|
|
1553
|
+
return true;
|
|
1554
|
+
}
|
|
1555
|
+
crossFaceNeighbor(tile.space, level, nx, ny, out);
|
|
1556
|
+
return true;
|
|
1372
1557
|
},
|
|
1373
|
-
tileBounds(
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1558
|
+
tileBounds(tile, cameraOrigin, out) {
|
|
1559
|
+
const level = tile.level;
|
|
1560
|
+
const n = 1 << level;
|
|
1561
|
+
const u0 = tile.x / n;
|
|
1562
|
+
const u1 = (tile.x + 1) / n;
|
|
1563
|
+
const v0 = tile.y / n;
|
|
1564
|
+
const v1 = (tile.y + 1) / n;
|
|
1565
|
+
const cornersU = [u0, u1, u0, u1];
|
|
1566
|
+
const cornersV = [v0, v0, v1, v1];
|
|
1567
|
+
let sumX = 0;
|
|
1568
|
+
let sumY = 0;
|
|
1569
|
+
let sumZ = 0;
|
|
1570
|
+
const px = [0, 0, 0, 0];
|
|
1571
|
+
const py = [0, 0, 0, 0];
|
|
1572
|
+
const pz = [0, 0, 0, 0];
|
|
1573
|
+
for (let i = 0; i < 4; i++) {
|
|
1574
|
+
faceUVToCube(tile.space, cornersU[i], cornersV[i], cube);
|
|
1575
|
+
const len = Math.hypot(cube[0], cube[1], cube[2]);
|
|
1576
|
+
const sx = center.x + cube[0] / len * radius;
|
|
1577
|
+
const sy = center.y + cube[1] / len * radius;
|
|
1578
|
+
const sz = center.z + cube[2] / len * radius;
|
|
1579
|
+
px[i] = sx;
|
|
1580
|
+
py[i] = sy;
|
|
1581
|
+
pz[i] = sz;
|
|
1582
|
+
sumX += sx;
|
|
1583
|
+
sumY += sy;
|
|
1584
|
+
sumZ += sz;
|
|
1585
|
+
}
|
|
1586
|
+
const cX = sumX * 0.25;
|
|
1587
|
+
const cY = sumY * 0.25;
|
|
1588
|
+
const cZ = sumZ * 0.25;
|
|
1589
|
+
let maxDistSq = 0;
|
|
1590
|
+
for (let i = 0; i < 4; i++) {
|
|
1591
|
+
const dx = px[i] - cX;
|
|
1592
|
+
const dy = py[i] - cY;
|
|
1593
|
+
const dz = pz[i] - cZ;
|
|
1594
|
+
const dSq = dx * dx + dy * dy + dz * dz;
|
|
1595
|
+
if (dSq > maxDistSq) maxDistSq = dSq;
|
|
1596
|
+
}
|
|
1597
|
+
out.cx = cX - cameraOrigin.x;
|
|
1598
|
+
out.cy = cY - cameraOrigin.y;
|
|
1599
|
+
out.cz = cZ - cameraOrigin.z;
|
|
1600
|
+
out.r = Math.sqrt(maxDistSq) + maxHeight;
|
|
1378
1601
|
},
|
|
1379
1602
|
rootTiles(_cameraOrigin, out) {
|
|
1380
1603
|
for (let s = 0; s < 6; s++) {
|
|
@@ -1385,10 +1608,141 @@ function createCubeSphereSurface(_cfg) {
|
|
|
1385
1608
|
root.y = 0;
|
|
1386
1609
|
}
|
|
1387
1610
|
return 6;
|
|
1388
|
-
}
|
|
1611
|
+
},
|
|
1612
|
+
projection: "cubeSphere",
|
|
1613
|
+
radius,
|
|
1614
|
+
center
|
|
1389
1615
|
};
|
|
1390
1616
|
}
|
|
1391
1617
|
|
|
1618
|
+
function readHeight(elevation, shape, leafIndex, ix, iy) {
|
|
1619
|
+
const base = leafIndex * shape.verticesPerNode;
|
|
1620
|
+
return elevation[base + iy * shape.edgeVertexCount + ix] ?? 0;
|
|
1621
|
+
}
|
|
1622
|
+
function sampleGridBilinear(elevation, shape, leafIndex, gx, gy) {
|
|
1623
|
+
const max = shape.edgeVertexCount - 1;
|
|
1624
|
+
const x = Math.max(0, Math.min(max, gx));
|
|
1625
|
+
const y = Math.max(0, Math.min(max, gy));
|
|
1626
|
+
const x0 = Math.floor(x);
|
|
1627
|
+
const y0 = Math.floor(y);
|
|
1628
|
+
const x1 = Math.min(max, x0 + 1);
|
|
1629
|
+
const y1 = Math.min(max, y0 + 1);
|
|
1630
|
+
const tx = x - x0;
|
|
1631
|
+
const ty = y - y0;
|
|
1632
|
+
const h00 = readHeight(elevation, shape, leafIndex, x0, y0);
|
|
1633
|
+
const h10 = readHeight(elevation, shape, leafIndex, x1, y0);
|
|
1634
|
+
const h01 = readHeight(elevation, shape, leafIndex, x0, y1);
|
|
1635
|
+
const h11 = readHeight(elevation, shape, leafIndex, x1, y1);
|
|
1636
|
+
const hx0 = h00 + (h10 - h00) * tx;
|
|
1637
|
+
const hx1 = h01 + (h11 - h01) * tx;
|
|
1638
|
+
return hx0 + (hx1 - hx0) * ty;
|
|
1639
|
+
}
|
|
1640
|
+
function elevationGradientAt(elevation, shape, leafIndex, gx, gy, stepWorld, elevationScale, out) {
|
|
1641
|
+
const hLeft = sampleGridBilinear(elevation, shape, leafIndex, gx - 1, gy);
|
|
1642
|
+
const hRight = sampleGridBilinear(elevation, shape, leafIndex, gx + 1, gy);
|
|
1643
|
+
const hUp = sampleGridBilinear(elevation, shape, leafIndex, gx, gy - 1);
|
|
1644
|
+
const hDown = sampleGridBilinear(elevation, shape, leafIndex, gx, gy + 1);
|
|
1645
|
+
const inv2Step = 0.5 / stepWorld;
|
|
1646
|
+
out.dhdu = (hRight - hLeft) * elevationScale * inv2Step;
|
|
1647
|
+
out.dhdv = (hDown - hUp) * elevationScale * inv2Step;
|
|
1648
|
+
return out;
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
const MISSED_LOOKUP = Object.freeze({
|
|
1652
|
+
found: false,
|
|
1653
|
+
leafIndex: -1,
|
|
1654
|
+
space: -1,
|
|
1655
|
+
level: -1,
|
|
1656
|
+
tileX: -1,
|
|
1657
|
+
tileY: -1,
|
|
1658
|
+
tileSize: 0,
|
|
1659
|
+
localU: 0,
|
|
1660
|
+
localV: 0
|
|
1661
|
+
});
|
|
1662
|
+
function lookupTile(index, config, worldX, worldZ) {
|
|
1663
|
+
const halfRoot = config.rootSize * 0.5;
|
|
1664
|
+
for (let level = config.maxLevel; level >= 0; level -= 1) {
|
|
1665
|
+
const scale = 2 ** level;
|
|
1666
|
+
const tileSize = config.rootSize / scale;
|
|
1667
|
+
const tileX = Math.floor((worldX - config.originX + halfRoot) / tileSize);
|
|
1668
|
+
const tileY = Math.floor((worldZ - config.originZ + halfRoot) / tileSize);
|
|
1669
|
+
const leafIndex = lookupSpatialIndexRaw(index, 0, level, tileX, tileY);
|
|
1670
|
+
if (leafIndex !== U32_EMPTY) {
|
|
1671
|
+
const tileMinX = config.originX + tileX * tileSize - halfRoot;
|
|
1672
|
+
const tileMinZ = config.originZ + tileY * tileSize - halfRoot;
|
|
1673
|
+
return {
|
|
1674
|
+
found: true,
|
|
1675
|
+
leafIndex,
|
|
1676
|
+
space: 0,
|
|
1677
|
+
level,
|
|
1678
|
+
tileX,
|
|
1679
|
+
tileY,
|
|
1680
|
+
tileSize,
|
|
1681
|
+
localU: (worldX - tileMinX) / tileSize,
|
|
1682
|
+
localV: (worldZ - tileMinZ) / tileSize
|
|
1683
|
+
};
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
return MISSED_LOOKUP;
|
|
1687
|
+
}
|
|
1688
|
+
function clamp01(value) {
|
|
1689
|
+
return value < 0 ? 0 : value > 1 ? 1 : value;
|
|
1690
|
+
}
|
|
1691
|
+
function lookupTileByFaceUV(index, config, face, u, v) {
|
|
1692
|
+
for (let level = config.maxLevel; level >= 0; level -= 1) {
|
|
1693
|
+
const n = 2 ** level;
|
|
1694
|
+
let tileX = Math.floor(u * n);
|
|
1695
|
+
let tileY = Math.floor(v * n);
|
|
1696
|
+
if (tileX < 0) tileX = 0;
|
|
1697
|
+
else if (tileX > n - 1) tileX = n - 1;
|
|
1698
|
+
if (tileY < 0) tileY = 0;
|
|
1699
|
+
else if (tileY > n - 1) tileY = n - 1;
|
|
1700
|
+
const leafIndex = lookupSpatialIndexRaw(index, face, level, tileX, tileY);
|
|
1701
|
+
if (leafIndex !== U32_EMPTY) {
|
|
1702
|
+
const tileSize = sphereTileArcLength(config.radius, n);
|
|
1703
|
+
return {
|
|
1704
|
+
found: true,
|
|
1705
|
+
leafIndex,
|
|
1706
|
+
space: face,
|
|
1707
|
+
level,
|
|
1708
|
+
tileX,
|
|
1709
|
+
tileY,
|
|
1710
|
+
tileSize,
|
|
1711
|
+
localU: clamp01(u * n - tileX),
|
|
1712
|
+
localV: clamp01(v * n - tileY)
|
|
1713
|
+
};
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
return MISSED_LOOKUP;
|
|
1717
|
+
}
|
|
1718
|
+
function lookupTileForDirection(index, config, dx, dy, dz, dirScratch, uvScratch) {
|
|
1719
|
+
if (config.projection !== "cubeSphere") return MISSED_LOOKUP;
|
|
1720
|
+
const len = Math.hypot(dx, dy, dz);
|
|
1721
|
+
if (len === 0) return MISSED_LOOKUP;
|
|
1722
|
+
dirScratch[0] = dx / len;
|
|
1723
|
+
dirScratch[1] = dy / len;
|
|
1724
|
+
dirScratch[2] = dz / len;
|
|
1725
|
+
const face = directionToFace(dirScratch);
|
|
1726
|
+
directionToFaceUV(face, dirScratch, uvScratch);
|
|
1727
|
+
return lookupTileByFaceUV(index, config, face, uvScratch[0], uvScratch[1]);
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
function createTerrainSnapshotState(maxNodes, totalElements) {
|
|
1731
|
+
return {
|
|
1732
|
+
frontElevation: new Float32Array(totalElements),
|
|
1733
|
+
backElevation: new Float32Array(totalElements),
|
|
1734
|
+
frontIndex: createSpatialIndex(maxNodes),
|
|
1735
|
+
backIndex: createSpatialIndex(maxNodes),
|
|
1736
|
+
frontTileBounds: new Float32Array(maxNodes * 2),
|
|
1737
|
+
backTileBounds: new Float32Array(maxNodes * 2),
|
|
1738
|
+
frontLeafCount: 0,
|
|
1739
|
+
globalRange: null,
|
|
1740
|
+
hasSnapshot: false,
|
|
1741
|
+
readbackPending: false,
|
|
1742
|
+
generation: 0,
|
|
1743
|
+
lastScheduledStampGen: -1
|
|
1744
|
+
};
|
|
1745
|
+
}
|
|
1392
1746
|
function cloneSpatialIndex(target, source) {
|
|
1393
1747
|
if (target.size !== source.size) {
|
|
1394
1748
|
throw new Error(
|
|
@@ -1404,218 +1758,278 @@ function cloneSpatialIndex(target, source) {
|
|
|
1404
1758
|
target.keysY.set(source.keysY);
|
|
1405
1759
|
target.values.set(source.values);
|
|
1406
1760
|
}
|
|
1407
|
-
function
|
|
1408
|
-
|
|
1409
|
-
|
|
1761
|
+
function triggerSnapshotReadback(state, renderer, attribute, spatialIndex, boundsAttribute, captured) {
|
|
1762
|
+
if (state.readbackPending) return;
|
|
1763
|
+
const withReadback = renderer;
|
|
1764
|
+
if (!withReadback.getArrayBufferAsync) return;
|
|
1765
|
+
if (spatialIndex.stampGen === state.lastScheduledStampGen) return;
|
|
1766
|
+
cloneSpatialIndex(state.backIndex, spatialIndex);
|
|
1767
|
+
state.lastScheduledStampGen = spatialIndex.stampGen;
|
|
1768
|
+
const { activeLeafCount, totalElements, elevationScale, originY } = captured;
|
|
1769
|
+
state.readbackPending = true;
|
|
1770
|
+
const elevationPromise = withReadback.getArrayBufferAsync(attribute);
|
|
1771
|
+
const boundsPromise = boundsAttribute ? withReadback.getArrayBufferAsync(boundsAttribute) : null;
|
|
1772
|
+
const onComplete = (elevResult, boundsResult) => {
|
|
1773
|
+
const data = new Float32Array(elevResult);
|
|
1774
|
+
state.backElevation.fill(0);
|
|
1775
|
+
state.backElevation.set(data.subarray(0, totalElements));
|
|
1776
|
+
let boundsValid = activeLeafCount === 0;
|
|
1777
|
+
if (boundsResult) {
|
|
1778
|
+
const rawBounds = new Float32Array(boundsResult);
|
|
1779
|
+
state.backTileBounds.fill(0);
|
|
1780
|
+
state.backTileBounds.set(rawBounds.subarray(0, activeLeafCount * 2));
|
|
1781
|
+
for (let i = 0; i < activeLeafCount; i += 1) {
|
|
1782
|
+
if ((rawBounds[i * 2 + 1] ?? 0) !== 0) {
|
|
1783
|
+
boundsValid = true;
|
|
1784
|
+
break;
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
const oldFrontElevation = state.frontElevation;
|
|
1789
|
+
const oldFrontIndex = state.frontIndex;
|
|
1790
|
+
state.frontElevation = state.backElevation;
|
|
1791
|
+
state.frontIndex = state.backIndex;
|
|
1792
|
+
state.frontLeafCount = activeLeafCount;
|
|
1793
|
+
state.backElevation = oldFrontElevation;
|
|
1794
|
+
state.backIndex = oldFrontIndex;
|
|
1795
|
+
if (boundsResult && boundsValid) {
|
|
1796
|
+
const oldFrontBounds = state.frontTileBounds;
|
|
1797
|
+
state.frontTileBounds = state.backTileBounds;
|
|
1798
|
+
state.backTileBounds = oldFrontBounds;
|
|
1799
|
+
}
|
|
1800
|
+
if (boundsResult && boundsValid && activeLeafCount > 0) {
|
|
1801
|
+
let gMin = Infinity;
|
|
1802
|
+
let gMax = -Infinity;
|
|
1803
|
+
for (let i = 0; i < activeLeafCount; i++) {
|
|
1804
|
+
const rawMin = state.frontTileBounds[i * 2];
|
|
1805
|
+
const rawMax = state.frontTileBounds[i * 2 + 1];
|
|
1806
|
+
const a = originY + rawMin * elevationScale;
|
|
1807
|
+
const b = originY + rawMax * elevationScale;
|
|
1808
|
+
gMin = Math.min(gMin, a, b);
|
|
1809
|
+
gMax = Math.max(gMax, a, b);
|
|
1810
|
+
}
|
|
1811
|
+
state.globalRange = { min: gMin, max: gMax };
|
|
1812
|
+
}
|
|
1813
|
+
state.hasSnapshot = true;
|
|
1814
|
+
state.generation += 1;
|
|
1815
|
+
};
|
|
1816
|
+
if (boundsPromise) {
|
|
1817
|
+
Promise.all([elevationPromise, boundsPromise]).then(([elev, bounds]) => onComplete(elev, bounds)).finally(() => {
|
|
1818
|
+
state.readbackPending = false;
|
|
1819
|
+
});
|
|
1820
|
+
} else {
|
|
1821
|
+
elevationPromise.then((elev) => onComplete(elev, null)).finally(() => {
|
|
1822
|
+
state.readbackPending = false;
|
|
1823
|
+
});
|
|
1824
|
+
}
|
|
1410
1825
|
}
|
|
1826
|
+
|
|
1411
1827
|
function createCpuTerrainCache(maxNodes, initialConfig) {
|
|
1412
1828
|
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;
|
|
1829
|
+
const shape = {
|
|
1830
|
+
edgeVertexCount: config.innerTileSegments + 3,
|
|
1831
|
+
verticesPerNode: 0
|
|
1431
1832
|
};
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
const
|
|
1445
|
-
const
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
return
|
|
1833
|
+
shape.verticesPerNode = shape.edgeVertexCount * shape.edgeVertexCount;
|
|
1834
|
+
let totalElements = maxNodes * shape.verticesPerNode;
|
|
1835
|
+
const state = createTerrainSnapshotState(
|
|
1836
|
+
maxNodes,
|
|
1837
|
+
totalElements
|
|
1838
|
+
);
|
|
1839
|
+
const dirScratch = [0, 0, 0];
|
|
1840
|
+
const uvScratch = [0, 0];
|
|
1841
|
+
const llScratch = [0, 0, 0];
|
|
1842
|
+
const gridScratch = { gx: 0, gy: 0 };
|
|
1843
|
+
const gradientScratch = { dhdu: 0, dhdv: 0 };
|
|
1844
|
+
const gridCoordsFromLookup = (lookup) => {
|
|
1845
|
+
const fieldU = tileLocalToFieldUVNumber(lookup.localU, config.innerTileSegments);
|
|
1846
|
+
const fieldV = tileLocalToFieldUVNumber(lookup.localV, config.innerTileSegments);
|
|
1847
|
+
gridScratch.gx = fieldU * (shape.edgeVertexCount - 1);
|
|
1848
|
+
gridScratch.gy = fieldV * (shape.edgeVertexCount - 1);
|
|
1849
|
+
return gridScratch;
|
|
1850
|
+
};
|
|
1851
|
+
const rawHeightFromLookup = (lookup) => {
|
|
1852
|
+
const g = gridCoordsFromLookup(lookup);
|
|
1853
|
+
return sampleGridBilinear(state.frontElevation, shape, lookup.leafIndex, g.gx, g.gy);
|
|
1449
1854
|
};
|
|
1450
1855
|
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
1856
|
const stepWorld = tileSize / config.innerTileSegments;
|
|
1456
|
-
const
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1857
|
+
const { dhdu, dhdv } = elevationGradientAt(
|
|
1858
|
+
state.frontElevation,
|
|
1859
|
+
shape,
|
|
1860
|
+
leafIndex,
|
|
1861
|
+
gx,
|
|
1862
|
+
gy,
|
|
1863
|
+
stepWorld,
|
|
1864
|
+
config.elevationScale,
|
|
1865
|
+
gradientScratch
|
|
1866
|
+
);
|
|
1867
|
+
return new three.Vector3(-dhdu, 1, -dhdv).normalize();
|
|
1460
1868
|
};
|
|
1461
|
-
const
|
|
1462
|
-
const
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
localV: 0
|
|
1499
|
-
};
|
|
1869
|
+
const computeSphereNormal = (leafIndex, gx, gy, tileSize, face, dirX, dirY, dirZ) => {
|
|
1870
|
+
const stepWorld = tileSize / config.innerTileSegments;
|
|
1871
|
+
const { dhdu, dhdv } = elevationGradientAt(
|
|
1872
|
+
state.frontElevation,
|
|
1873
|
+
shape,
|
|
1874
|
+
leafIndex,
|
|
1875
|
+
gx,
|
|
1876
|
+
gy,
|
|
1877
|
+
stepWorld,
|
|
1878
|
+
config.elevationScale,
|
|
1879
|
+
gradientScratch
|
|
1880
|
+
);
|
|
1881
|
+
const f = CUBE_FACES[face];
|
|
1882
|
+
const dDotR = dirX * f.right[0] + dirY * f.right[1] + dirZ * f.right[2];
|
|
1883
|
+
let tux = f.right[0] - dirX * dDotR;
|
|
1884
|
+
let tuy = f.right[1] - dirY * dDotR;
|
|
1885
|
+
let tuz = f.right[2] - dirZ * dDotR;
|
|
1886
|
+
const tuLen = Math.hypot(tux, tuy, tuz) || 1;
|
|
1887
|
+
tux /= tuLen;
|
|
1888
|
+
tuy /= tuLen;
|
|
1889
|
+
tuz /= tuLen;
|
|
1890
|
+
const dDotU = dirX * f.up[0] + dirY * f.up[1] + dirZ * f.up[2];
|
|
1891
|
+
let tvx = f.up[0] - dirX * dDotU;
|
|
1892
|
+
let tvy = f.up[1] - dirY * dDotU;
|
|
1893
|
+
let tvz = f.up[2] - dirZ * dDotU;
|
|
1894
|
+
const tvLen = Math.hypot(tvx, tvy, tvz) || 1;
|
|
1895
|
+
tvx /= tvLen;
|
|
1896
|
+
tvy /= tvLen;
|
|
1897
|
+
tvz /= tvLen;
|
|
1898
|
+
const nx = -dhdu;
|
|
1899
|
+
const ny = 1;
|
|
1900
|
+
const nz = -dhdv;
|
|
1901
|
+
return new three.Vector3(
|
|
1902
|
+
tux * nx + dirX * ny + tvx * nz,
|
|
1903
|
+
tuy * nx + dirY * ny + tvy * nz,
|
|
1904
|
+
tuz * nx + dirZ * ny + tvz * nz
|
|
1905
|
+
).normalize();
|
|
1500
1906
|
};
|
|
1501
1907
|
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);
|
|
1908
|
+
const height = rawHeightFromLookup(lookup);
|
|
1507
1909
|
const scaledHeight = config.originY + height * config.elevationScale;
|
|
1508
|
-
const normal = computeNormal(
|
|
1910
|
+
const normal = computeNormal(
|
|
1911
|
+
lookup.leafIndex,
|
|
1912
|
+
gridScratch.gx,
|
|
1913
|
+
gridScratch.gy,
|
|
1914
|
+
lookup.tileSize
|
|
1915
|
+
);
|
|
1509
1916
|
return { elevation: scaledHeight, normal, valid: true };
|
|
1510
1917
|
};
|
|
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
1918
|
const sampleTerrain = (worldX, worldZ) => {
|
|
1521
|
-
if (!hasSnapshot) {
|
|
1919
|
+
if (!state.hasSnapshot) {
|
|
1522
1920
|
return { elevation: 0, normal: new three.Vector3(0, 1, 0), valid: false };
|
|
1523
1921
|
}
|
|
1524
|
-
const lookup = lookupTile(worldX, worldZ);
|
|
1922
|
+
const lookup = lookupTile(state.frontIndex, config, worldX, worldZ);
|
|
1525
1923
|
if (!lookup.found) {
|
|
1526
1924
|
return { elevation: 0, normal: new three.Vector3(0, 1, 0), valid: false };
|
|
1527
1925
|
}
|
|
1528
1926
|
return sampleFromLookup(lookup);
|
|
1529
1927
|
};
|
|
1530
1928
|
const getElevation = (worldX, worldZ) => {
|
|
1531
|
-
if (!hasSnapshot) {
|
|
1929
|
+
if (!state.hasSnapshot) {
|
|
1532
1930
|
return { elevation: 0, valid: false };
|
|
1533
1931
|
}
|
|
1534
|
-
const lookup = lookupTile(worldX, worldZ);
|
|
1932
|
+
const lookup = lookupTile(state.frontIndex, config, worldX, worldZ);
|
|
1535
1933
|
if (!lookup.found) {
|
|
1536
1934
|
return { elevation: 0, valid: false };
|
|
1537
1935
|
}
|
|
1538
|
-
|
|
1936
|
+
const height = rawHeightFromLookup(lookup);
|
|
1937
|
+
return {
|
|
1938
|
+
elevation: config.originY + height * config.elevationScale,
|
|
1939
|
+
valid: true
|
|
1940
|
+
};
|
|
1941
|
+
};
|
|
1942
|
+
const invalidSurfaceSample = (dx, dy, dz) => ({
|
|
1943
|
+
position: new three.Vector3(),
|
|
1944
|
+
normal: new three.Vector3(0, 1, 0),
|
|
1945
|
+
direction: new three.Vector3(dx, dy, dz),
|
|
1946
|
+
elevation: 0,
|
|
1947
|
+
valid: false
|
|
1948
|
+
});
|
|
1949
|
+
const lookupDirection = (dx, dy, dz) => lookupTileForDirection(state.frontIndex, config, dx, dy, dz, dirScratch, uvScratch);
|
|
1950
|
+
const sampleSurfaceByDirection = (dx, dy, dz) => {
|
|
1951
|
+
if (!state.hasSnapshot || config.projection !== "cubeSphere") {
|
|
1952
|
+
return invalidSurfaceSample(dx, dy, dz);
|
|
1953
|
+
}
|
|
1954
|
+
const len = Math.hypot(dx, dy, dz);
|
|
1955
|
+
if (len === 0) return invalidSurfaceSample(0, 0, 0);
|
|
1956
|
+
const nx = dx / len;
|
|
1957
|
+
const ny = dy / len;
|
|
1958
|
+
const nz = dz / len;
|
|
1959
|
+
const lookup = lookupDirection(nx, ny, nz);
|
|
1960
|
+
if (!lookup.found) return invalidSurfaceSample(nx, ny, nz);
|
|
1961
|
+
const height = rawHeightFromLookup(lookup);
|
|
1962
|
+
const elevation = height * config.elevationScale;
|
|
1963
|
+
const r = config.radius + elevation;
|
|
1964
|
+
const position = new three.Vector3(
|
|
1965
|
+
config.originX + nx * r,
|
|
1966
|
+
config.originY + ny * r,
|
|
1967
|
+
config.originZ + nz * r
|
|
1968
|
+
);
|
|
1969
|
+
const normal = computeSphereNormal(
|
|
1970
|
+
lookup.leafIndex,
|
|
1971
|
+
gridScratch.gx,
|
|
1972
|
+
gridScratch.gy,
|
|
1973
|
+
lookup.tileSize,
|
|
1974
|
+
lookup.space,
|
|
1975
|
+
nx,
|
|
1976
|
+
ny,
|
|
1977
|
+
nz
|
|
1978
|
+
);
|
|
1979
|
+
return {
|
|
1980
|
+
position,
|
|
1981
|
+
normal,
|
|
1982
|
+
direction: new three.Vector3(nx, ny, nz),
|
|
1983
|
+
elevation,
|
|
1984
|
+
valid: true
|
|
1985
|
+
};
|
|
1986
|
+
};
|
|
1987
|
+
const tileFromLookup = (lookup) => {
|
|
1988
|
+
if (!lookup.found) return null;
|
|
1989
|
+
return {
|
|
1990
|
+
space: lookup.space,
|
|
1991
|
+
level: lookup.level,
|
|
1992
|
+
x: lookup.tileX,
|
|
1993
|
+
y: lookup.tileY,
|
|
1994
|
+
index: lookup.leafIndex
|
|
1995
|
+
};
|
|
1996
|
+
};
|
|
1997
|
+
const tileBoundsFromLookup = (lookup, elevationBase) => {
|
|
1998
|
+
if (!lookup.found || lookup.leafIndex >= state.frontLeafCount) return null;
|
|
1999
|
+
const rawMin = state.frontTileBounds[lookup.leafIndex * 2];
|
|
2000
|
+
const rawMax = state.frontTileBounds[lookup.leafIndex * 2 + 1];
|
|
2001
|
+
const a = elevationBase + rawMin * config.elevationScale;
|
|
2002
|
+
const b = elevationBase + rawMax * config.elevationScale;
|
|
2003
|
+
return {
|
|
2004
|
+
space: lookup.space,
|
|
2005
|
+
level: lookup.level,
|
|
2006
|
+
x: lookup.tileX,
|
|
2007
|
+
y: lookup.tileY,
|
|
2008
|
+
index: lookup.leafIndex,
|
|
2009
|
+
minElevation: Math.min(a, b),
|
|
2010
|
+
maxElevation: Math.max(a, b)
|
|
2011
|
+
};
|
|
1539
2012
|
};
|
|
1540
2013
|
const api = {
|
|
1541
2014
|
get generation() {
|
|
1542
|
-
return
|
|
2015
|
+
return state.generation;
|
|
1543
2016
|
},
|
|
1544
2017
|
get ready() {
|
|
1545
|
-
return hasSnapshot;
|
|
2018
|
+
return state.hasSnapshot;
|
|
1546
2019
|
},
|
|
1547
2020
|
updateConfig(nextConfig) {
|
|
1548
2021
|
config = nextConfig;
|
|
1549
|
-
edgeVertexCount = config.innerTileSegments + 3;
|
|
1550
|
-
verticesPerNode = edgeVertexCount * edgeVertexCount;
|
|
1551
|
-
totalElements = maxNodes * verticesPerNode;
|
|
2022
|
+
shape.edgeVertexCount = config.innerTileSegments + 3;
|
|
2023
|
+
shape.verticesPerNode = shape.edgeVertexCount * shape.edgeVertexCount;
|
|
2024
|
+
totalElements = maxNodes * shape.verticesPerNode;
|
|
1552
2025
|
},
|
|
1553
2026
|
triggerReadback(renderer, attribute, spatialIndex, boundsAttribute, activeLeafCount) {
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
const capturedLeafCount = activeLeafCount ?? 0;
|
|
1561
|
-
const capturedScale = config.elevationScale;
|
|
1562
|
-
const capturedOriginY = config.originY;
|
|
1563
|
-
readbackPending = true;
|
|
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
|
-
}
|
|
2027
|
+
triggerSnapshotReadback(state, renderer, attribute, spatialIndex, boundsAttribute, {
|
|
2028
|
+
activeLeafCount: activeLeafCount ?? 0,
|
|
2029
|
+
totalElements,
|
|
2030
|
+
elevationScale: config.elevationScale,
|
|
2031
|
+
originY: config.originY
|
|
2032
|
+
});
|
|
1619
2033
|
},
|
|
1620
2034
|
getElevation(worldX, worldZ) {
|
|
1621
2035
|
const sample = getElevation(worldX, worldZ);
|
|
@@ -1625,43 +2039,26 @@ function createCpuTerrainCache(maxNodes, initialConfig) {
|
|
|
1625
2039
|
return sampleTerrain(worldX, worldZ).normal;
|
|
1626
2040
|
},
|
|
1627
2041
|
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
|
-
};
|
|
2042
|
+
if (!state.hasSnapshot) return null;
|
|
2043
|
+
return tileFromLookup(lookupTile(state.frontIndex, config, worldX, worldZ));
|
|
1637
2044
|
},
|
|
1638
2045
|
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
|
-
};
|
|
2046
|
+
if (!state.hasSnapshot) return null;
|
|
2047
|
+
return tileBoundsFromLookup(
|
|
2048
|
+
lookupTile(state.frontIndex, config, worldX, worldZ),
|
|
2049
|
+
config.originY
|
|
2050
|
+
);
|
|
1654
2051
|
},
|
|
1655
2052
|
getGlobalElevationRange() {
|
|
1656
|
-
return globalRange;
|
|
2053
|
+
return state.globalRange;
|
|
1657
2054
|
},
|
|
1658
2055
|
sampleTerrainBatch(positions) {
|
|
1659
2056
|
const count = Math.floor(positions.length / 2);
|
|
1660
2057
|
const elevations = new Float32Array(count);
|
|
1661
2058
|
const normals = new Float32Array(count * 3);
|
|
1662
2059
|
const valid = new Uint8Array(count);
|
|
1663
|
-
if (!hasSnapshot) {
|
|
1664
|
-
return { elevations, normals, valid, generation:
|
|
2060
|
+
if (!state.hasSnapshot) {
|
|
2061
|
+
return { elevations, normals, valid, generation: state.generation };
|
|
1665
2062
|
}
|
|
1666
2063
|
let lastTile;
|
|
1667
2064
|
for (let i = 0; i < count; i += 1) {
|
|
@@ -1672,6 +2069,7 @@ function createCpuTerrainCache(maxNodes, initialConfig) {
|
|
|
1672
2069
|
lookup = {
|
|
1673
2070
|
found: true,
|
|
1674
2071
|
leafIndex: lastTile.leafIndex,
|
|
2072
|
+
space: 0,
|
|
1675
2073
|
level: lastTile.level,
|
|
1676
2074
|
tileX: lastTile.tileX,
|
|
1677
2075
|
tileY: lastTile.tileY,
|
|
@@ -1680,7 +2078,7 @@ function createCpuTerrainCache(maxNodes, initialConfig) {
|
|
|
1680
2078
|
localV: (worldZ - lastTile.tileMinZ) / lastTile.tileSize
|
|
1681
2079
|
};
|
|
1682
2080
|
} else {
|
|
1683
|
-
lookup = lookupTile(worldX, worldZ);
|
|
2081
|
+
lookup = lookupTile(state.frontIndex, config, worldX, worldZ);
|
|
1684
2082
|
if (lookup.found) {
|
|
1685
2083
|
lastTile = {
|
|
1686
2084
|
leafIndex: lookup.leafIndex,
|
|
@@ -1706,9 +2104,133 @@ function createCpuTerrainCache(maxNodes, initialConfig) {
|
|
|
1706
2104
|
normals[i * 3 + 2] = sample.normal.z;
|
|
1707
2105
|
valid[i] = 1;
|
|
1708
2106
|
}
|
|
1709
|
-
return { elevations, normals, valid, generation:
|
|
2107
|
+
return { elevations, normals, valid, generation: state.generation };
|
|
2108
|
+
},
|
|
2109
|
+
sampleTerrain,
|
|
2110
|
+
// --- Cube-sphere queries ---
|
|
2111
|
+
sampleTerrainByDirection(direction) {
|
|
2112
|
+
return sampleSurfaceByDirection(direction.x, direction.y, direction.z);
|
|
2113
|
+
},
|
|
2114
|
+
sampleTerrainByPosition(position) {
|
|
2115
|
+
return sampleSurfaceByDirection(
|
|
2116
|
+
position.x - config.originX,
|
|
2117
|
+
position.y - config.originY,
|
|
2118
|
+
position.z - config.originZ
|
|
2119
|
+
);
|
|
2120
|
+
},
|
|
2121
|
+
sampleTerrainByLatLong(latitudeDeg, longitudeDeg) {
|
|
2122
|
+
latLongToDirection(latitudeDeg, longitudeDeg, llScratch);
|
|
2123
|
+
return sampleSurfaceByDirection(llScratch[0], llScratch[1], llScratch[2]);
|
|
1710
2124
|
},
|
|
1711
|
-
|
|
2125
|
+
getElevationByDirection(direction) {
|
|
2126
|
+
const sample = sampleSurfaceByDirection(direction.x, direction.y, direction.z);
|
|
2127
|
+
return sample.valid ? sample.elevation : null;
|
|
2128
|
+
},
|
|
2129
|
+
getElevationByPosition(position) {
|
|
2130
|
+
const sample = sampleSurfaceByDirection(
|
|
2131
|
+
position.x - config.originX,
|
|
2132
|
+
position.y - config.originY,
|
|
2133
|
+
position.z - config.originZ
|
|
2134
|
+
);
|
|
2135
|
+
return sample.valid ? sample.elevation : null;
|
|
2136
|
+
},
|
|
2137
|
+
getElevationByLatLong(latitudeDeg, longitudeDeg) {
|
|
2138
|
+
latLongToDirection(latitudeDeg, longitudeDeg, llScratch);
|
|
2139
|
+
const sample = sampleSurfaceByDirection(llScratch[0], llScratch[1], llScratch[2]);
|
|
2140
|
+
return sample.valid ? sample.elevation : null;
|
|
2141
|
+
},
|
|
2142
|
+
getNormalByDirection(direction) {
|
|
2143
|
+
const sample = sampleSurfaceByDirection(direction.x, direction.y, direction.z);
|
|
2144
|
+
return sample.valid ? sample.normal : null;
|
|
2145
|
+
},
|
|
2146
|
+
getNormalByPosition(position) {
|
|
2147
|
+
const sample = sampleSurfaceByDirection(
|
|
2148
|
+
position.x - config.originX,
|
|
2149
|
+
position.y - config.originY,
|
|
2150
|
+
position.z - config.originZ
|
|
2151
|
+
);
|
|
2152
|
+
return sample.valid ? sample.normal : null;
|
|
2153
|
+
},
|
|
2154
|
+
getNormalByLatLong(latitudeDeg, longitudeDeg) {
|
|
2155
|
+
latLongToDirection(latitudeDeg, longitudeDeg, llScratch);
|
|
2156
|
+
const sample = sampleSurfaceByDirection(llScratch[0], llScratch[1], llScratch[2]);
|
|
2157
|
+
return sample.valid ? sample.normal : null;
|
|
2158
|
+
},
|
|
2159
|
+
getTileByDirection(direction) {
|
|
2160
|
+
if (!state.hasSnapshot) return null;
|
|
2161
|
+
return tileFromLookup(lookupDirection(direction.x, direction.y, direction.z));
|
|
2162
|
+
},
|
|
2163
|
+
getTileByPosition(position) {
|
|
2164
|
+
if (!state.hasSnapshot) return null;
|
|
2165
|
+
return tileFromLookup(
|
|
2166
|
+
lookupDirection(
|
|
2167
|
+
position.x - config.originX,
|
|
2168
|
+
position.y - config.originY,
|
|
2169
|
+
position.z - config.originZ
|
|
2170
|
+
)
|
|
2171
|
+
);
|
|
2172
|
+
},
|
|
2173
|
+
getTileByLatLong(latitudeDeg, longitudeDeg) {
|
|
2174
|
+
if (!state.hasSnapshot) return null;
|
|
2175
|
+
latLongToDirection(latitudeDeg, longitudeDeg, llScratch);
|
|
2176
|
+
return tileFromLookup(lookupDirection(llScratch[0], llScratch[1], llScratch[2]));
|
|
2177
|
+
},
|
|
2178
|
+
getTileBoundsByDirection(direction) {
|
|
2179
|
+
if (!state.hasSnapshot) return null;
|
|
2180
|
+
return tileBoundsFromLookup(
|
|
2181
|
+
lookupDirection(direction.x, direction.y, direction.z),
|
|
2182
|
+
0
|
|
2183
|
+
);
|
|
2184
|
+
},
|
|
2185
|
+
getTileBoundsByPosition(position) {
|
|
2186
|
+
if (!state.hasSnapshot) return null;
|
|
2187
|
+
return tileBoundsFromLookup(
|
|
2188
|
+
lookupDirection(
|
|
2189
|
+
position.x - config.originX,
|
|
2190
|
+
position.y - config.originY,
|
|
2191
|
+
position.z - config.originZ
|
|
2192
|
+
),
|
|
2193
|
+
0
|
|
2194
|
+
);
|
|
2195
|
+
},
|
|
2196
|
+
getTileBoundsByLatLong(latitudeDeg, longitudeDeg) {
|
|
2197
|
+
if (!state.hasSnapshot) return null;
|
|
2198
|
+
latLongToDirection(latitudeDeg, longitudeDeg, llScratch);
|
|
2199
|
+
return tileBoundsFromLookup(
|
|
2200
|
+
lookupDirection(llScratch[0], llScratch[1], llScratch[2]),
|
|
2201
|
+
0
|
|
2202
|
+
);
|
|
2203
|
+
},
|
|
2204
|
+
sampleTerrainBatchByDirection(directions) {
|
|
2205
|
+
const count = Math.floor(directions.length / 3);
|
|
2206
|
+
const positions = new Float32Array(count * 3);
|
|
2207
|
+
const normals = new Float32Array(count * 3);
|
|
2208
|
+
const elevations = new Float32Array(count);
|
|
2209
|
+
const valid = new Uint8Array(count);
|
|
2210
|
+
if (!state.hasSnapshot || config.projection !== "cubeSphere") {
|
|
2211
|
+
return { positions, normals, elevations, valid, generation: state.generation };
|
|
2212
|
+
}
|
|
2213
|
+
for (let i = 0; i < count; i += 1) {
|
|
2214
|
+
const sample = sampleSurfaceByDirection(
|
|
2215
|
+
directions[i * 3] ?? 0,
|
|
2216
|
+
directions[i * 3 + 1] ?? 0,
|
|
2217
|
+
directions[i * 3 + 2] ?? 0
|
|
2218
|
+
);
|
|
2219
|
+
if (!sample.valid) {
|
|
2220
|
+
normals[i * 3 + 1] = 1;
|
|
2221
|
+
continue;
|
|
2222
|
+
}
|
|
2223
|
+
positions[i * 3] = sample.position.x;
|
|
2224
|
+
positions[i * 3 + 1] = sample.position.y;
|
|
2225
|
+
positions[i * 3 + 2] = sample.position.z;
|
|
2226
|
+
normals[i * 3] = sample.normal.x;
|
|
2227
|
+
normals[i * 3 + 1] = sample.normal.y;
|
|
2228
|
+
normals[i * 3 + 2] = sample.normal.z;
|
|
2229
|
+
elevations[i] = sample.elevation;
|
|
2230
|
+
valid[i] = 1;
|
|
2231
|
+
}
|
|
2232
|
+
return { positions, normals, elevations, valid, generation: state.generation };
|
|
2233
|
+
}
|
|
1712
2234
|
};
|
|
1713
2235
|
return api;
|
|
1714
2236
|
}
|
|
@@ -1741,6 +2263,61 @@ function createTerrainQuery(cache) {
|
|
|
1741
2263
|
}
|
|
1742
2264
|
};
|
|
1743
2265
|
}
|
|
2266
|
+
function createTerrainSphereQuery(cache) {
|
|
2267
|
+
return {
|
|
2268
|
+
get generation() {
|
|
2269
|
+
return cache.generation;
|
|
2270
|
+
},
|
|
2271
|
+
getElevationByDirection(direction) {
|
|
2272
|
+
return cache.getElevationByDirection(direction);
|
|
2273
|
+
},
|
|
2274
|
+
getElevationByPosition(position) {
|
|
2275
|
+
return cache.getElevationByPosition(position);
|
|
2276
|
+
},
|
|
2277
|
+
getElevationByLatLong(latitudeDeg, longitudeDeg) {
|
|
2278
|
+
return cache.getElevationByLatLong(latitudeDeg, longitudeDeg);
|
|
2279
|
+
},
|
|
2280
|
+
getNormalByDirection(direction) {
|
|
2281
|
+
return cache.getNormalByDirection(direction);
|
|
2282
|
+
},
|
|
2283
|
+
getNormalByPosition(position) {
|
|
2284
|
+
return cache.getNormalByPosition(position);
|
|
2285
|
+
},
|
|
2286
|
+
getNormalByLatLong(latitudeDeg, longitudeDeg) {
|
|
2287
|
+
return cache.getNormalByLatLong(latitudeDeg, longitudeDeg);
|
|
2288
|
+
},
|
|
2289
|
+
sampleTerrainByDirection(direction) {
|
|
2290
|
+
return cache.sampleTerrainByDirection(direction);
|
|
2291
|
+
},
|
|
2292
|
+
sampleTerrainByPosition(position) {
|
|
2293
|
+
return cache.sampleTerrainByPosition(position);
|
|
2294
|
+
},
|
|
2295
|
+
sampleTerrainByLatLong(latitudeDeg, longitudeDeg) {
|
|
2296
|
+
return cache.sampleTerrainByLatLong(latitudeDeg, longitudeDeg);
|
|
2297
|
+
},
|
|
2298
|
+
getTileByDirection(direction) {
|
|
2299
|
+
return cache.getTileByDirection(direction);
|
|
2300
|
+
},
|
|
2301
|
+
getTileByPosition(position) {
|
|
2302
|
+
return cache.getTileByPosition(position);
|
|
2303
|
+
},
|
|
2304
|
+
getTileByLatLong(latitudeDeg, longitudeDeg) {
|
|
2305
|
+
return cache.getTileByLatLong(latitudeDeg, longitudeDeg);
|
|
2306
|
+
},
|
|
2307
|
+
getTileBoundsByDirection(direction) {
|
|
2308
|
+
return cache.getTileBoundsByDirection(direction);
|
|
2309
|
+
},
|
|
2310
|
+
getTileBoundsByPosition(position) {
|
|
2311
|
+
return cache.getTileBoundsByPosition(position);
|
|
2312
|
+
},
|
|
2313
|
+
getTileBoundsByLatLong(latitudeDeg, longitudeDeg) {
|
|
2314
|
+
return cache.getTileBoundsByLatLong(latitudeDeg, longitudeDeg);
|
|
2315
|
+
},
|
|
2316
|
+
sampleTerrainBatchByDirection(directions) {
|
|
2317
|
+
return cache.sampleTerrainBatchByDirection(directions);
|
|
2318
|
+
}
|
|
2319
|
+
};
|
|
2320
|
+
}
|
|
1744
2321
|
|
|
1745
2322
|
const WGSIZE = 64;
|
|
1746
2323
|
function buildReductionKernel(elevationFieldNode, boundsNode, verticesPerNode) {
|
|
@@ -1810,8 +2387,11 @@ const terrainQueryTask = work.task((get, work) => {
|
|
|
1810
2387
|
const rootSizeValue = get(rootSize);
|
|
1811
2388
|
const originValue = get(origin);
|
|
1812
2389
|
const elevationScaleValue = get(elevationScale);
|
|
2390
|
+
const radiusValue = get(radius);
|
|
2391
|
+
const topologyValue = get(topologyTask);
|
|
2392
|
+
const projectionValue = topologyValue.projection ?? "flat";
|
|
1813
2393
|
return work((prev) => {
|
|
1814
|
-
const shapeKey = `${maxNodesValue}:${innerTileSegmentsValue}`;
|
|
2394
|
+
const shapeKey = `${maxNodesValue}:${innerTileSegmentsValue}:${projectionValue}`;
|
|
1815
2395
|
const configValues = {
|
|
1816
2396
|
rootSize: rootSizeValue,
|
|
1817
2397
|
originX: originValue.x,
|
|
@@ -1819,16 +2399,20 @@ const terrainQueryTask = work.task((get, work) => {
|
|
|
1819
2399
|
originZ: originValue.z,
|
|
1820
2400
|
innerTileSegments: innerTileSegmentsValue,
|
|
1821
2401
|
elevationScale: elevationScaleValue,
|
|
1822
|
-
maxLevel: maxLevelValue
|
|
2402
|
+
maxLevel: maxLevelValue,
|
|
2403
|
+
projection: projectionValue,
|
|
2404
|
+
radius: topologyValue.radius ?? radiusValue
|
|
1823
2405
|
};
|
|
1824
2406
|
let cache = prev?.cache;
|
|
1825
2407
|
let query = prev?.query;
|
|
2408
|
+
let sphereQuery = prev?.sphereQuery ?? null;
|
|
1826
2409
|
if (!cache || !query || prev?.shapeKey !== shapeKey) {
|
|
1827
2410
|
cache = createCpuTerrainCache(maxNodesValue, configValues);
|
|
1828
2411
|
query = createTerrainQuery(cache);
|
|
2412
|
+
sphereQuery = projectionValue === "cubeSphere" ? createTerrainSphereQuery(cache) : null;
|
|
1829
2413
|
}
|
|
1830
2414
|
cache.updateConfig(configValues);
|
|
1831
|
-
return { cache, query, shapeKey };
|
|
2415
|
+
return { cache, query, sphereQuery, shapeKey };
|
|
1832
2416
|
});
|
|
1833
2417
|
}).displayName("terrainQueryTask");
|
|
1834
2418
|
const terrainReadbackTask = work.task(
|
|
@@ -1851,38 +2435,44 @@ const terrainReadbackTask = work.task(
|
|
|
1851
2435
|
}
|
|
1852
2436
|
).displayName("terrainReadbackTask").lane("gpu");
|
|
1853
2437
|
|
|
1854
|
-
const
|
|
1855
|
-
const
|
|
2438
|
+
const topologyTask = work.task((get, work) => {
|
|
2439
|
+
const customTopology = get(topology);
|
|
1856
2440
|
const rootSizeVal = get(rootSize);
|
|
1857
2441
|
const originVal = get(origin);
|
|
1858
2442
|
return work(() => {
|
|
1859
|
-
if (
|
|
1860
|
-
return
|
|
2443
|
+
if (customTopology) return customTopology;
|
|
2444
|
+
return createFlatTopology({ rootSize: rootSizeVal, origin: originVal });
|
|
1861
2445
|
});
|
|
1862
|
-
}).displayName("
|
|
2446
|
+
}).displayName("topologyTask");
|
|
1863
2447
|
const quadtreeConfigTask = work.task((get, work) => {
|
|
1864
|
-
const
|
|
2448
|
+
const topologyVal = get(topologyTask);
|
|
1865
2449
|
const maxNodesVal = get(maxNodes);
|
|
1866
2450
|
const maxLevelVal = get(maxLevel);
|
|
1867
2451
|
return work(() => {
|
|
1868
|
-
const state = createState({ maxNodes: maxNodesVal, maxLevel: maxLevelVal },
|
|
2452
|
+
const state = createState({ maxNodes: maxNodesVal, maxLevel: maxLevelVal }, topologyVal);
|
|
1869
2453
|
return {
|
|
1870
2454
|
state,
|
|
1871
|
-
|
|
2455
|
+
topology: topologyVal
|
|
1872
2456
|
};
|
|
1873
2457
|
});
|
|
1874
2458
|
}).displayName("quadtreeConfigTask");
|
|
1875
2459
|
const quadtreeUpdateTask = work.task((get, work) => {
|
|
1876
2460
|
const quadtreeConfig = get(quadtreeConfigTask);
|
|
1877
2461
|
const quadtreeUpdateConfig = get(quadtreeUpdate);
|
|
1878
|
-
const { query: terrainQuery } = get(terrainQueryTask);
|
|
2462
|
+
const { query: terrainQuery, sphereQuery } = get(terrainQueryTask);
|
|
1879
2463
|
let outLeaves = void 0;
|
|
2464
|
+
const cameraPosition = new three.Vector3();
|
|
1880
2465
|
return work(() => {
|
|
1881
2466
|
const cam = quadtreeUpdateConfig.cameraOrigin;
|
|
1882
|
-
|
|
2467
|
+
if (sphereQuery) {
|
|
2468
|
+
cameraPosition.set(cam.x, cam.y, cam.z);
|
|
2469
|
+
quadtreeUpdateConfig.elevationAtCameraXZ = sphereQuery.getElevationByPosition(cameraPosition) ?? 0;
|
|
2470
|
+
} else {
|
|
2471
|
+
quadtreeUpdateConfig.elevationAtCameraXZ = terrainQuery.getElevation(cam.x, cam.z) ?? 0;
|
|
2472
|
+
}
|
|
1883
2473
|
outLeaves = update(
|
|
1884
2474
|
quadtreeConfig.state,
|
|
1885
|
-
quadtreeConfig.
|
|
2475
|
+
quadtreeConfig.topology,
|
|
1886
2476
|
quadtreeUpdateConfig,
|
|
1887
2477
|
outLeaves
|
|
1888
2478
|
);
|
|
@@ -1904,7 +2494,7 @@ const leafGpuBufferTask = work.task((get, work) => {
|
|
|
1904
2494
|
leafStorage.data[offset] = leafSet.level[i] ?? 0;
|
|
1905
2495
|
leafStorage.data[offset + 1] = leafSet.x[i] ?? 0;
|
|
1906
2496
|
leafStorage.data[offset + 2] = leafSet.y[i] ?? 0;
|
|
1907
|
-
leafStorage.data[offset + 3] =
|
|
2497
|
+
leafStorage.data[offset + 3] = leafSet.space[i] ?? 0;
|
|
1908
2498
|
}
|
|
1909
2499
|
leafStorage.attribute.needsUpdate = true;
|
|
1910
2500
|
leafStorage.node.needsUpdate = true;
|
|
@@ -1946,12 +2536,14 @@ function createTerrainUniforms(params) {
|
|
|
1946
2536
|
);
|
|
1947
2537
|
const uSkirtScale = tsl.uniform(tsl.float(params.skirtScale)).setName(`uSkirtScale${suffix}`);
|
|
1948
2538
|
const uElevationScale = tsl.uniform(tsl.float(params.elevationScale)).setName(`uElevationScale${suffix}`);
|
|
2539
|
+
const uRadius = tsl.uniform(tsl.float(params.radius)).setName(`uRadius${suffix}`);
|
|
1949
2540
|
return {
|
|
1950
2541
|
uRootOrigin,
|
|
1951
2542
|
uRootSize,
|
|
1952
2543
|
uInnerTileSegments,
|
|
1953
2544
|
uSkirtScale,
|
|
1954
|
-
uElevationScale
|
|
2545
|
+
uElevationScale,
|
|
2546
|
+
uRadius
|
|
1955
2547
|
};
|
|
1956
2548
|
}
|
|
1957
2549
|
|
|
@@ -1965,6 +2557,7 @@ const createUniformsTask = work.task((get, work) => {
|
|
|
1965
2557
|
innerTileSegments: get(innerTileSegments),
|
|
1966
2558
|
skirtScale: get(skirtScale),
|
|
1967
2559
|
elevationScale: get(elevationScale),
|
|
2560
|
+
radius: get(radius),
|
|
1968
2561
|
instanceId: get(instanceIdTask)
|
|
1969
2562
|
};
|
|
1970
2563
|
return work(() => createTerrainUniforms(uniformParams));
|
|
@@ -1976,6 +2569,7 @@ const updateUniformsTask = work.task((get, work) => {
|
|
|
1976
2569
|
const innerTileSegmentsVal = get(innerTileSegments);
|
|
1977
2570
|
const skirtScaleVal = get(skirtScale);
|
|
1978
2571
|
const elevationScaleVal = get(elevationScale);
|
|
2572
|
+
const radiusVal = get(radius);
|
|
1979
2573
|
return work(() => {
|
|
1980
2574
|
terrainUniformsContext.uRootSize.value = rootSizeVal;
|
|
1981
2575
|
terrainUniformsContext.uRootOrigin.value = scratchVector3.set(
|
|
@@ -1986,6 +2580,7 @@ const updateUniformsTask = work.task((get, work) => {
|
|
|
1986
2580
|
terrainUniformsContext.uInnerTileSegments.value = innerTileSegmentsVal;
|
|
1987
2581
|
terrainUniformsContext.uSkirtScale.value = skirtScaleVal;
|
|
1988
2582
|
terrainUniformsContext.uElevationScale.value = elevationScaleVal;
|
|
2583
|
+
terrainUniformsContext.uRadius.value = radiusVal;
|
|
1989
2584
|
return terrainUniformsContext;
|
|
1990
2585
|
});
|
|
1991
2586
|
}).displayName("updateUniformsTask");
|
|
@@ -2008,8 +2603,9 @@ const createElevationFieldContextTask = work.task((get, work) => {
|
|
|
2008
2603
|
const tileNodesTask = work.task((get, work) => {
|
|
2009
2604
|
const leafStorage = get(leafStorageTask);
|
|
2010
2605
|
const uniforms = get(updateUniformsTask);
|
|
2606
|
+
const topology = get(topologyTask);
|
|
2011
2607
|
return work(() => {
|
|
2012
|
-
return createTileCompute(leafStorage, uniforms);
|
|
2608
|
+
return createTileCompute(leafStorage, uniforms, topology.projection ?? "flat");
|
|
2013
2609
|
});
|
|
2014
2610
|
}).displayName("tileNodesTask");
|
|
2015
2611
|
const elevationFieldStageTask = work.task((get, work) => {
|
|
@@ -2106,33 +2702,14 @@ const terrainFieldStageTask = work.task((get, work) => {
|
|
|
2106
2702
|
});
|
|
2107
2703
|
}).displayName("terrainFieldStageTask");
|
|
2108
2704
|
|
|
2109
|
-
const compileComputeTask =
|
|
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");
|
|
2705
|
+
const { compile: compileComputeTask, execute: executeComputeTask } = createComputePipelineTasks(terrainFieldStageTask);
|
|
2128
2706
|
function createComputePipelineTasks(leafStageTask) {
|
|
2129
2707
|
const compile = work.task((get, work) => {
|
|
2130
2708
|
const pipeline = get(leafStageTask);
|
|
2131
2709
|
const edgeVertexCount = get(innerTileSegments) + 3;
|
|
2132
2710
|
return work(
|
|
2133
2711
|
() => compileComputePipeline(pipeline, edgeVertexCount, {
|
|
2134
|
-
|
|
2135
|
-
})
|
|
2712
|
+
})
|
|
2136
2713
|
);
|
|
2137
2714
|
}).displayName("compileComputeTask");
|
|
2138
2715
|
const execute = work.task(
|
|
@@ -2273,6 +2850,38 @@ const createTileIndexFromWorldPosition = (spatialIndex, uniforms, maxLevel) => {
|
|
|
2273
2850
|
return tsl.vec3(tileIndex.toFloat(), tileU, tileV);
|
|
2274
2851
|
});
|
|
2275
2852
|
};
|
|
2853
|
+
const createTileIndexFromDirection = (spatialIndex, maxLevel) => {
|
|
2854
|
+
const lookup = createGpuSpatialLookup(spatialIndex);
|
|
2855
|
+
const levelCount = Math.max(1, maxLevel + 1);
|
|
2856
|
+
return tsl.Fn(([direction]) => {
|
|
2857
|
+
const dir = tsl.vec3(direction).normalize().toVar();
|
|
2858
|
+
const face = cubeFaceFromDirection(dir).toVar();
|
|
2859
|
+
const basis = cubeFaceBasis(face);
|
|
2860
|
+
const faceUV = cubeFaceUVFromDirection(basis, dir).toVar();
|
|
2861
|
+
const u = faceUV.x.toVar();
|
|
2862
|
+
const v = faceUV.y.toVar();
|
|
2863
|
+
const tileIndex = tsl.int(-1).toVar();
|
|
2864
|
+
const tileU = tsl.float(0).toVar();
|
|
2865
|
+
const tileV = tsl.float(0).toVar();
|
|
2866
|
+
const i = tsl.int(0).toVar();
|
|
2867
|
+
tsl.Loop(levelCount, () => {
|
|
2868
|
+
const level = tsl.int(maxLevel).sub(i).toVar();
|
|
2869
|
+
const n = tsl.pow(tsl.float(2), level.toFloat()).toVar();
|
|
2870
|
+
const nInt = tsl.int(n).toVar();
|
|
2871
|
+
const tileX = u.mul(n).floor().toInt().max(tsl.int(0)).min(nInt.sub(tsl.int(1))).toVar();
|
|
2872
|
+
const tileY = v.mul(n).floor().toInt().max(tsl.int(0)).min(nInt.sub(tsl.int(1))).toVar();
|
|
2873
|
+
const maybeIndex = lookup(face, level, tileX, tileY).toVar();
|
|
2874
|
+
tsl.If(maybeIndex.greaterThanEqual(tsl.int(0)), () => {
|
|
2875
|
+
tileIndex.assign(maybeIndex);
|
|
2876
|
+
tileU.assign(u.mul(n).sub(tileX.toFloat()));
|
|
2877
|
+
tileV.assign(v.mul(n).sub(tileY.toFloat()));
|
|
2878
|
+
tsl.Break();
|
|
2879
|
+
});
|
|
2880
|
+
i.addAssign(1);
|
|
2881
|
+
});
|
|
2882
|
+
return tsl.vec3(tileIndex.toFloat(), tileU, tileV);
|
|
2883
|
+
});
|
|
2884
|
+
};
|
|
2276
2885
|
|
|
2277
2886
|
const gpuSpatialIndexStorageTask = work.task((get, work) => {
|
|
2278
2887
|
const maxNodesValue = get(maxNodes);
|
|
@@ -2288,43 +2897,44 @@ const gpuSpatialIndexUploadTask = work.task((get, work) => {
|
|
|
2288
2897
|
});
|
|
2289
2898
|
}).displayName("gpuSpatialIndexUploadTask");
|
|
2290
2899
|
|
|
2900
|
+
function packedSampleFromTileResult(params, tileResult) {
|
|
2901
|
+
const tileIndex = tsl.int(tileResult.x).toVar();
|
|
2902
|
+
const safeTileIndex = tileIndex.max(tsl.int(0)).toVar();
|
|
2903
|
+
const fieldU = tileLocalToFieldUV(
|
|
2904
|
+
tileResult.y,
|
|
2905
|
+
params.uniforms.uInnerTileSegments
|
|
2906
|
+
).toVar();
|
|
2907
|
+
const fieldV = tileLocalToFieldUV(
|
|
2908
|
+
tileResult.z,
|
|
2909
|
+
params.uniforms.uInnerTileSegments
|
|
2910
|
+
).toVar();
|
|
2911
|
+
const found = tileIndex.greaterThanEqual(tsl.int(0)).toVar();
|
|
2912
|
+
const sampled = sampleTerrainField(
|
|
2913
|
+
params.terrainFieldStorage,
|
|
2914
|
+
fieldU,
|
|
2915
|
+
fieldV,
|
|
2916
|
+
safeTileIndex
|
|
2917
|
+
).toVar();
|
|
2918
|
+
const normal = unpackTangentNormal(sampled.g, sampled.b);
|
|
2919
|
+
const valid = found.select(tsl.float(1), tsl.float(0)).toVar();
|
|
2920
|
+
return tsl.vec4(sampled.r, normal.x, normal.y, normal.z).mul(valid);
|
|
2921
|
+
}
|
|
2291
2922
|
function createTerrainSampleNode(params) {
|
|
2292
2923
|
const tileLookup = createTileIndexFromWorldPosition(
|
|
2293
2924
|
params.spatialIndex,
|
|
2294
2925
|
params.uniforms,
|
|
2295
|
-
maxLevel
|
|
2926
|
+
params.maxLevel
|
|
2296
2927
|
);
|
|
2297
2928
|
return tsl.Fn(([worldX, worldZ]) => {
|
|
2298
2929
|
const tileResult = tileLookup(worldX, worldZ).toVar();
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
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
|
-
);
|
|
2930
|
+
return packedSampleFromTileResult(params, tileResult);
|
|
2931
|
+
});
|
|
2932
|
+
}
|
|
2933
|
+
function createTerrainSampleNodeByDirection(params) {
|
|
2934
|
+
const tileLookup = createTileIndexFromDirection(params.spatialIndex, params.maxLevel);
|
|
2935
|
+
return tsl.Fn(([direction]) => {
|
|
2936
|
+
const tileResult = tileLookup(direction).toVar();
|
|
2937
|
+
return packedSampleFromTileResult(params, tileResult);
|
|
2328
2938
|
});
|
|
2329
2939
|
}
|
|
2330
2940
|
function createTerrainSampler(params) {
|
|
@@ -2356,16 +2966,14 @@ function createTerrainSampler(params) {
|
|
|
2356
2966
|
const sampleElevation = tsl.Fn(
|
|
2357
2967
|
([worldX, worldZ]) => terrainSampleAt(worldX, worldZ).x
|
|
2358
2968
|
);
|
|
2359
|
-
const sampleNormal = tsl.Fn(
|
|
2360
|
-
(
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
)
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
([worldX, worldZ]) => terrainSampleAt(worldX, worldZ).y.abs().add(terrainSampleAt(worldX, worldZ).z.abs()).add(terrainSampleAt(worldX, worldZ).w.abs()).greaterThan(tsl.float(0)).select(tsl.float(1), tsl.float(0))
|
|
2368
|
-
);
|
|
2969
|
+
const sampleNormal = tsl.Fn(([worldX, worldZ]) => {
|
|
2970
|
+
const sample = terrainSampleAt(worldX, worldZ).toVar();
|
|
2971
|
+
return tsl.vec3(sample.y, sample.z, sample.w);
|
|
2972
|
+
});
|
|
2973
|
+
const sampleValidity = tsl.Fn(([worldX, worldZ]) => {
|
|
2974
|
+
const sample = terrainSampleAt(worldX, worldZ).toVar();
|
|
2975
|
+
return sample.y.abs().add(sample.z.abs()).add(sample.w.abs()).greaterThan(tsl.float(0)).select(tsl.float(1), tsl.float(0));
|
|
2976
|
+
});
|
|
2369
2977
|
const evaluateElevation = tsl.Fn(
|
|
2370
2978
|
([worldX, worldZ]) => evaluateElevationAt(worldX, worldZ)
|
|
2371
2979
|
);
|
|
@@ -2392,7 +3000,7 @@ function createTerrainSampler(params) {
|
|
|
2392
3000
|
}
|
|
2393
3001
|
);
|
|
2394
3002
|
const evaluateNormal = (worldX, worldZ, epsilon) => evaluateNormalNode(worldX, worldZ, epsilon ?? tsl.float(0.1));
|
|
2395
|
-
|
|
3003
|
+
const sampler = {
|
|
2396
3004
|
sampleElevation,
|
|
2397
3005
|
sampleNormal,
|
|
2398
3006
|
sampleTerrain,
|
|
@@ -2400,6 +3008,26 @@ function createTerrainSampler(params) {
|
|
|
2400
3008
|
evaluateElevation,
|
|
2401
3009
|
evaluateNormal
|
|
2402
3010
|
};
|
|
3011
|
+
if (params.projection === "cubeSphere") {
|
|
3012
|
+
const terrainSampleByDir = createTerrainSampleNodeByDirection(params);
|
|
3013
|
+
sampler.sampleTerrainByDirection = tsl.Fn(
|
|
3014
|
+
([direction]) => terrainSampleByDir(direction)
|
|
3015
|
+
);
|
|
3016
|
+
sampler.sampleElevationByDirection = tsl.Fn(
|
|
3017
|
+
([direction]) => terrainSampleByDir(direction).x
|
|
3018
|
+
);
|
|
3019
|
+
sampler.sampleValidityByDirection = tsl.Fn(([direction]) => {
|
|
3020
|
+
const sample = terrainSampleByDir(direction).toVar();
|
|
3021
|
+
return sample.y.abs().add(sample.z.abs()).add(sample.w.abs()).greaterThan(tsl.float(0)).select(tsl.float(1), tsl.float(0));
|
|
3022
|
+
});
|
|
3023
|
+
sampler.sampleNormalByDirection = tsl.Fn(([direction]) => {
|
|
3024
|
+
const dir = tsl.vec3(direction).normalize().toVar();
|
|
3025
|
+
const packed = terrainSampleByDir(direction).toVar();
|
|
3026
|
+
const basis = cubeFaceBasis(cubeFaceFromDirection(dir));
|
|
3027
|
+
return sphereTangentFrameNormal(dir, basis, tsl.vec3(packed.y, packed.z, packed.w));
|
|
3028
|
+
});
|
|
3029
|
+
}
|
|
3030
|
+
return sampler;
|
|
2403
3031
|
}
|
|
2404
3032
|
|
|
2405
3033
|
const createTerrainSamplerTask = work.task((get, work) => {
|
|
@@ -2407,12 +3035,16 @@ const createTerrainSamplerTask = work.task((get, work) => {
|
|
|
2407
3035
|
const spatialIndex = get(gpuSpatialIndexStorageTask);
|
|
2408
3036
|
const uniforms = get(updateUniformsTask);
|
|
2409
3037
|
const elevationCallback = get(elevationFn);
|
|
3038
|
+
const maxLevelValue = get(maxLevel);
|
|
3039
|
+
const projection = get(topologyTask).projection ?? "flat";
|
|
2410
3040
|
return work(
|
|
2411
3041
|
() => createTerrainSampler({
|
|
2412
3042
|
terrainFieldStorage,
|
|
2413
3043
|
spatialIndex,
|
|
2414
3044
|
uniforms,
|
|
2415
|
-
elevationCallback
|
|
3045
|
+
elevationCallback,
|
|
3046
|
+
maxLevel: maxLevelValue,
|
|
3047
|
+
projection
|
|
2416
3048
|
})
|
|
2417
3049
|
);
|
|
2418
3050
|
}).displayName("createTerrainSamplerTask");
|
|
@@ -2439,18 +3071,14 @@ const isSkirtUV = tsl.Fn(([segments]) => {
|
|
|
2439
3071
|
|
|
2440
3072
|
function createTileBaseWorldPosition(leafStorage, terrainUniforms) {
|
|
2441
3073
|
return tsl.Fn(() => {
|
|
2442
|
-
const
|
|
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();
|
|
3074
|
+
const tile = decodeLeafTile(leafStorage, tsl.int(tsl.instanceIndex));
|
|
2447
3075
|
const rootSize = terrainUniforms.uRootSize.toVar();
|
|
2448
3076
|
const rootOrigin = terrainUniforms.uRootOrigin.toVar();
|
|
2449
3077
|
const half = tsl.float(0.5);
|
|
2450
|
-
const size = rootSize.div(tsl.pow(tsl.float(2),
|
|
3078
|
+
const size = rootSize.div(tsl.pow(tsl.float(2), tile.level.toFloat()));
|
|
2451
3079
|
const halfRoot = rootSize.mul(half);
|
|
2452
|
-
const centerX = rootOrigin.x.add(
|
|
2453
|
-
const centerZ = rootOrigin.z.add(
|
|
3080
|
+
const centerX = rootOrigin.x.add(tile.x.add(half).mul(size)).sub(halfRoot);
|
|
3081
|
+
const centerZ = rootOrigin.z.add(tile.y.add(half).mul(size)).sub(halfRoot);
|
|
2454
3082
|
const clampedX = tsl.positionLocal.x.max(half.negate()).min(half);
|
|
2455
3083
|
const clampedZ = tsl.positionLocal.z.max(half.negate()).min(half);
|
|
2456
3084
|
const worldX = centerX.add(clampedX.mul(size));
|
|
@@ -2461,49 +3089,78 @@ function createTileBaseWorldPosition(leafStorage, terrainUniforms) {
|
|
|
2461
3089
|
function createTileElevation(terrainUniforms, terrainFieldStorage) {
|
|
2462
3090
|
if (!terrainFieldStorage) return tsl.float(0);
|
|
2463
3091
|
const innerSegs = terrainUniforms.uInnerTileSegments;
|
|
2464
|
-
const u = tileLocalToFieldUV
|
|
2465
|
-
const v = tileLocalToFieldUV
|
|
2466
|
-
return sampleTerrainFieldElevation(
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
).mul(terrainUniforms.uElevationScale);
|
|
2472
|
-
}
|
|
2473
|
-
function createNormalAssignment(terrainUniforms, terrainFieldStorage) {
|
|
3092
|
+
const u = tileLocalToFieldUV(tsl.positionLocal.x.add(tsl.float(0.5)), innerSegs);
|
|
3093
|
+
const v = tileLocalToFieldUV(tsl.positionLocal.z.add(tsl.float(0.5)), innerSegs);
|
|
3094
|
+
return sampleTerrainFieldElevation(terrainFieldStorage, u, v, tsl.int(tsl.instanceIndex)).mul(
|
|
3095
|
+
terrainUniforms.uElevationScale
|
|
3096
|
+
);
|
|
3097
|
+
}
|
|
3098
|
+
function createNormalAssignment(leafStorage, terrainUniforms, terrainFieldStorage, projection = "flat") {
|
|
2474
3099
|
if (!terrainFieldStorage) return;
|
|
3100
|
+
tsl.normalLocal.assign(
|
|
3101
|
+
createTileLocalNormal(leafStorage, terrainUniforms, terrainFieldStorage, projection)
|
|
3102
|
+
);
|
|
3103
|
+
}
|
|
3104
|
+
function loadTangentNormal(terrainUniforms, terrainFieldStorage) {
|
|
2475
3105
|
const nodeIndex = tsl.int(tsl.instanceIndex);
|
|
2476
3106
|
const edgeVertexCount = tsl.int(terrainUniforms.uInnerTileSegments.add(3));
|
|
2477
3107
|
const localVertexIndex = tsl.int(tsl.vertexIndex);
|
|
2478
3108
|
const ix = localVertexIndex.mod(edgeVertexCount);
|
|
2479
3109
|
const iy = localVertexIndex.div(edgeVertexCount);
|
|
2480
|
-
const normalXZ = loadTerrainFieldNormal(
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
);
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
3110
|
+
const normalXZ = loadTerrainFieldNormal(terrainFieldStorage, ix, iy, nodeIndex);
|
|
3111
|
+
const normal = unpackTangentNormal(normalXZ.x, normalXZ.y);
|
|
3112
|
+
return { ix, iy, normal };
|
|
3113
|
+
}
|
|
3114
|
+
function createTileLocalNormal(leafStorage, terrainUniforms, terrainFieldStorage, projection = "flat") {
|
|
3115
|
+
if (!terrainFieldStorage) return tsl.vec3(0, 1, 0);
|
|
3116
|
+
if (projection === "cubeSphere") {
|
|
3117
|
+
return tsl.Fn(() => {
|
|
3118
|
+
const { ix, iy, normal } = loadTangentNormal(terrainUniforms, terrainFieldStorage);
|
|
3119
|
+
const tile = decodeLeafTile(leafStorage, tsl.int(tsl.instanceIndex));
|
|
3120
|
+
const innerSeg = terrainUniforms.uInnerTileSegments.toVar().toFloat();
|
|
3121
|
+
const localU = ix.toFloat().sub(tsl.float(1)).div(innerSeg).max(tsl.float(0)).min(tsl.float(1));
|
|
3122
|
+
const localV = iy.toFloat().sub(tsl.float(1)).div(innerSeg).max(tsl.float(0)).min(tsl.float(1));
|
|
3123
|
+
const faceUV = faceUVFromTileLocal(tile, localU, localV);
|
|
3124
|
+
const basis = cubeFaceBasis(tile.face);
|
|
3125
|
+
const dir = cubeFaceDirection(basis, faceUV.x, faceUV.y);
|
|
3126
|
+
return sphereTangentFrameNormal(dir, basis, normal);
|
|
3127
|
+
})();
|
|
3128
|
+
}
|
|
3129
|
+
return tsl.Fn(() => {
|
|
3130
|
+
const { normal } = loadTangentNormal(terrainUniforms, terrainFieldStorage);
|
|
3131
|
+
return normal;
|
|
3132
|
+
})();
|
|
3133
|
+
}
|
|
3134
|
+
function createCubeSphereWorldPosition(leafStorage, terrainUniforms, terrainFieldStorage) {
|
|
3135
|
+
return tsl.Fn(() => {
|
|
3136
|
+
const tile = decodeLeafTile(leafStorage, tsl.int(tsl.instanceIndex));
|
|
3137
|
+
const half = tsl.float(0.5);
|
|
3138
|
+
const localU = tsl.positionLocal.x.max(half.negate()).min(half).add(half);
|
|
3139
|
+
const localV = tsl.positionLocal.z.max(half.negate()).min(half).add(half);
|
|
3140
|
+
const faceUV = faceUVFromTileLocal(tile, localU, localV);
|
|
3141
|
+
const basis = cubeFaceBasis(tile.face);
|
|
3142
|
+
const dir = cubeFaceDirection(basis, faceUV.x, faceUV.y);
|
|
3143
|
+
const yElevation = createTileElevation(terrainUniforms, terrainFieldStorage);
|
|
3144
|
+
const baseRadius = terrainUniforms.uRadius.toVar().add(yElevation);
|
|
3145
|
+
const skirtVertex = isSkirtVertex(terrainUniforms.uInnerTileSegments);
|
|
3146
|
+
const r = tsl.select(skirtVertex, baseRadius.sub(terrainUniforms.uSkirtScale.toVar()), baseRadius);
|
|
3147
|
+
createNormalAssignment(leafStorage, terrainUniforms, terrainFieldStorage, "cubeSphere");
|
|
3148
|
+
const origin = terrainUniforms.uRootOrigin.toVar();
|
|
3149
|
+
return origin.add(dir.mul(r));
|
|
3150
|
+
})();
|
|
3151
|
+
}
|
|
3152
|
+
function createTileWorldPosition(leafStorage, terrainUniforms, terrainFieldStorage, projection = "flat") {
|
|
3153
|
+
if (projection === "cubeSphere") {
|
|
3154
|
+
return createCubeSphereWorldPosition(leafStorage, terrainUniforms, terrainFieldStorage);
|
|
3155
|
+
}
|
|
3156
|
+
const baseWorldPosition = createTileBaseWorldPosition(leafStorage, terrainUniforms);
|
|
2497
3157
|
return tsl.Fn(() => {
|
|
2498
3158
|
const base = baseWorldPosition();
|
|
2499
|
-
const yElevation = createTileElevation(
|
|
2500
|
-
terrainUniforms,
|
|
2501
|
-
terrainFieldStorage
|
|
2502
|
-
);
|
|
3159
|
+
const yElevation = createTileElevation(terrainUniforms, terrainFieldStorage);
|
|
2503
3160
|
const skirtVertex = isSkirtVertex(terrainUniforms.uInnerTileSegments);
|
|
2504
3161
|
const skirtY = base.y.add(yElevation).sub(terrainUniforms.uSkirtScale.toVar());
|
|
2505
3162
|
const worldY = tsl.select(skirtVertex, skirtY, base.y.add(yElevation));
|
|
2506
|
-
createNormalAssignment(terrainUniforms, terrainFieldStorage);
|
|
3163
|
+
createNormalAssignment(leafStorage, terrainUniforms, terrainFieldStorage, "flat");
|
|
2507
3164
|
return tsl.vec3(base.x, worldY, base.z);
|
|
2508
3165
|
})();
|
|
2509
3166
|
}
|
|
@@ -2512,11 +3169,13 @@ const positionNodeTask = work.task((get, work) => {
|
|
|
2512
3169
|
const leafStorage = get(leafStorageTask);
|
|
2513
3170
|
const terrainUniforms = get(updateUniformsTask);
|
|
2514
3171
|
const terrainFieldStorage = get(createTerrainFieldTextureTask);
|
|
3172
|
+
const topology = get(topologyTask);
|
|
2515
3173
|
return work(
|
|
2516
3174
|
() => createTileWorldPosition(
|
|
2517
3175
|
leafStorage,
|
|
2518
3176
|
terrainUniforms,
|
|
2519
|
-
terrainFieldStorage
|
|
3177
|
+
terrainFieldStorage,
|
|
3178
|
+
topology.projection ?? "flat"
|
|
2520
3179
|
)
|
|
2521
3180
|
);
|
|
2522
3181
|
}).displayName("positionNodeTask");
|
|
@@ -2557,71 +3216,33 @@ function getTerrainBounds(config) {
|
|
|
2557
3216
|
maxZ: config.originZ + halfRoot
|
|
2558
3217
|
};
|
|
2559
3218
|
}
|
|
2560
|
-
function
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
if (
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
3219
|
+
function terrainSignedDistance(query, worldX, worldY, worldZ, skipBoundsFastPath) {
|
|
3220
|
+
if (!skipBoundsFastPath) {
|
|
3221
|
+
const tileBounds = query.getTileBounds(worldX, worldZ);
|
|
3222
|
+
if (tileBounds) {
|
|
3223
|
+
if (worldY > tileBounds.maxElevation) {
|
|
3224
|
+
return worldY - tileBounds.maxElevation;
|
|
3225
|
+
}
|
|
3226
|
+
if (worldY < tileBounds.minElevation) {
|
|
3227
|
+
return worldY - tileBounds.minElevation;
|
|
3228
|
+
}
|
|
2568
3229
|
}
|
|
2569
3230
|
}
|
|
2570
3231
|
const elevation = query.getElevation(worldX, worldZ);
|
|
2571
3232
|
if (!Number.isFinite(elevation)) return void 0;
|
|
2572
3233
|
return worldY - elevation;
|
|
2573
3234
|
}
|
|
2574
|
-
function
|
|
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();
|
|
3235
|
+
function marchSignedDistance(ray, startT, endT, stepSignedDistanceAt, refineSignedDistanceAt, options, point) {
|
|
2598
3236
|
let prevT = startT;
|
|
2599
3237
|
ray.at(prevT, point);
|
|
2600
|
-
let prevSignedDistance =
|
|
2601
|
-
query,
|
|
2602
|
-
point.x,
|
|
2603
|
-
point.y,
|
|
2604
|
-
point.z
|
|
2605
|
-
);
|
|
3238
|
+
let prevSignedDistance = stepSignedDistanceAt(point.x, point.y, point.z);
|
|
2606
3239
|
if (prevSignedDistance !== void 0 && prevSignedDistance <= 0) {
|
|
2607
|
-
|
|
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
|
-
};
|
|
3240
|
+
return startT;
|
|
2615
3241
|
}
|
|
2616
|
-
for (let i = 1; i <= maxSteps; i += 1) {
|
|
2617
|
-
const t = startT + (endT - startT) * i / maxSteps;
|
|
3242
|
+
for (let i = 1; i <= options.maxSteps; i += 1) {
|
|
3243
|
+
const t = startT + (endT - startT) * i / options.maxSteps;
|
|
2618
3244
|
ray.at(t, point);
|
|
2619
|
-
const signedDistance =
|
|
2620
|
-
query,
|
|
2621
|
-
point.x,
|
|
2622
|
-
point.y,
|
|
2623
|
-
point.z
|
|
2624
|
-
);
|
|
3245
|
+
const signedDistance = stepSignedDistanceAt(point.x, point.y, point.z);
|
|
2625
3246
|
if (signedDistance === void 0) {
|
|
2626
3247
|
prevSignedDistance = void 0;
|
|
2627
3248
|
prevT = t;
|
|
@@ -2630,15 +3251,10 @@ function cpuRaycast(query, ray, config, options) {
|
|
|
2630
3251
|
if (prevSignedDistance !== void 0 && prevSignedDistance > 0 && signedDistance <= 0) {
|
|
2631
3252
|
let lo = prevT;
|
|
2632
3253
|
let hi = t;
|
|
2633
|
-
for (let r = 0; r < refinementSteps; r += 1) {
|
|
3254
|
+
for (let r = 0; r < options.refinementSteps; r += 1) {
|
|
2634
3255
|
const mid = (lo + hi) * 0.5;
|
|
2635
3256
|
ray.at(mid, point);
|
|
2636
|
-
const midDistance =
|
|
2637
|
-
query,
|
|
2638
|
-
point.x,
|
|
2639
|
-
point.y,
|
|
2640
|
-
point.z
|
|
2641
|
-
);
|
|
3257
|
+
const midDistance = refineSignedDistanceAt(point.x, point.y, point.z);
|
|
2642
3258
|
if (midDistance === void 0) {
|
|
2643
3259
|
lo = mid;
|
|
2644
3260
|
continue;
|
|
@@ -2646,22 +3262,53 @@ function cpuRaycast(query, ray, config, options) {
|
|
|
2646
3262
|
if (midDistance > 0) lo = mid;
|
|
2647
3263
|
else hi = mid;
|
|
2648
3264
|
}
|
|
2649
|
-
|
|
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
|
-
};
|
|
3265
|
+
return hi;
|
|
2659
3266
|
}
|
|
2660
3267
|
prevSignedDistance = signedDistance;
|
|
2661
3268
|
prevT = t;
|
|
2662
3269
|
}
|
|
2663
3270
|
return null;
|
|
2664
3271
|
}
|
|
3272
|
+
function cpuRaycast(query, ray, config, options) {
|
|
3273
|
+
const bounds = getTerrainBounds(config);
|
|
3274
|
+
const segment = intersectRayAabb(
|
|
3275
|
+
ray,
|
|
3276
|
+
bounds.minX,
|
|
3277
|
+
config.minY,
|
|
3278
|
+
bounds.minZ,
|
|
3279
|
+
bounds.maxX,
|
|
3280
|
+
config.maxY,
|
|
3281
|
+
bounds.maxZ
|
|
3282
|
+
);
|
|
3283
|
+
if (!segment) return null;
|
|
3284
|
+
const maxDistance = options?.maxDistance ?? Number.POSITIVE_INFINITY;
|
|
3285
|
+
const startT = Math.max(0, segment.tMin);
|
|
3286
|
+
const endT = Math.min(segment.tMax, maxDistance);
|
|
3287
|
+
if (endT < startT) return null;
|
|
3288
|
+
const point = new three.Vector3();
|
|
3289
|
+
const hitT = marchSignedDistance(
|
|
3290
|
+
ray,
|
|
3291
|
+
startT,
|
|
3292
|
+
endT,
|
|
3293
|
+
(px, py, pz) => terrainSignedDistance(query, px, py, pz, false),
|
|
3294
|
+
(px, py, pz) => terrainSignedDistance(query, px, py, pz, true),
|
|
3295
|
+
{
|
|
3296
|
+
maxSteps: Math.max(8, options?.maxSteps ?? 128),
|
|
3297
|
+
refinementSteps: Math.max(1, options?.refinementSteps ?? 8)
|
|
3298
|
+
},
|
|
3299
|
+
point
|
|
3300
|
+
);
|
|
3301
|
+
if (hitT === null) return null;
|
|
3302
|
+
ray.at(hitT, point);
|
|
3303
|
+
const sample = query.sampleTerrain(point.x, point.z);
|
|
3304
|
+
if (!sample.valid) return null;
|
|
3305
|
+
point.y = sample.elevation;
|
|
3306
|
+
return {
|
|
3307
|
+
position: point.clone(),
|
|
3308
|
+
normal: sample.normal.clone(),
|
|
3309
|
+
distance: ray.origin.distanceTo(point)
|
|
3310
|
+
};
|
|
3311
|
+
}
|
|
2665
3312
|
function cpuRaycastBoundsOnly(ray, config, options) {
|
|
2666
3313
|
const bounds = getTerrainBounds(config);
|
|
2667
3314
|
const planeY = (config.minY + config.maxY) * 0.5;
|
|
@@ -2682,12 +3329,102 @@ function cpuRaycastBoundsOnly(ray, config, options) {
|
|
|
2682
3329
|
distance: ray.origin.distanceTo(point)
|
|
2683
3330
|
};
|
|
2684
3331
|
}
|
|
3332
|
+
function intersectRaySphere(ray, cx, cy, cz, radius) {
|
|
3333
|
+
const ox = ray.origin.x - cx;
|
|
3334
|
+
const oy = ray.origin.y - cy;
|
|
3335
|
+
const oz = ray.origin.z - cz;
|
|
3336
|
+
const dx = ray.direction.x;
|
|
3337
|
+
const dy = ray.direction.y;
|
|
3338
|
+
const dz = ray.direction.z;
|
|
3339
|
+
const a = dx * dx + dy * dy + dz * dz;
|
|
3340
|
+
const b = 2 * (ox * dx + oy * dy + oz * dz);
|
|
3341
|
+
const c = ox * ox + oy * oy + oz * oz - radius * radius;
|
|
3342
|
+
const disc = b * b - 4 * a * c;
|
|
3343
|
+
if (disc < 0) return null;
|
|
3344
|
+
const sqrtDisc = Math.sqrt(disc);
|
|
3345
|
+
const inv2a = 1 / (2 * a);
|
|
3346
|
+
return { t0: (-b - sqrtDisc) * inv2a, t1: (-b + sqrtDisc) * inv2a };
|
|
3347
|
+
}
|
|
3348
|
+
function sphereSignedDistance(query, config, px, py, pz, scratchDir) {
|
|
3349
|
+
const cx = config.centerX ?? 0;
|
|
3350
|
+
const cy = config.centerY ?? 0;
|
|
3351
|
+
const cz = config.centerZ ?? 0;
|
|
3352
|
+
const radius = config.radius ?? 0;
|
|
3353
|
+
const dx = px - cx;
|
|
3354
|
+
const dy = py - cy;
|
|
3355
|
+
const dz = pz - cz;
|
|
3356
|
+
const dist = Math.hypot(dx, dy, dz);
|
|
3357
|
+
scratchDir.set(dx, dy, dz);
|
|
3358
|
+
const elevation = query.getElevationByDirection(scratchDir);
|
|
3359
|
+
if (elevation === null) return void 0;
|
|
3360
|
+
return dist - (radius + elevation);
|
|
3361
|
+
}
|
|
3362
|
+
function cubeSphereRaycast(query, ray, config, options) {
|
|
3363
|
+
const cx = config.centerX ?? 0;
|
|
3364
|
+
const cy = config.centerY ?? 0;
|
|
3365
|
+
const cz = config.centerZ ?? 0;
|
|
3366
|
+
const radius = config.radius ?? 0;
|
|
3367
|
+
const outerRadius = config.maxRadius ?? radius;
|
|
3368
|
+
const shell = intersectRaySphere(ray, cx, cy, cz, outerRadius);
|
|
3369
|
+
if (!shell) return null;
|
|
3370
|
+
const maxDistance = options?.maxDistance ?? Number.POSITIVE_INFINITY;
|
|
3371
|
+
const startT = Math.max(0, shell.t0);
|
|
3372
|
+
const endT = Math.min(shell.t1, maxDistance);
|
|
3373
|
+
if (endT < startT) return null;
|
|
3374
|
+
const scratchDir = new three.Vector3();
|
|
3375
|
+
const point = new three.Vector3();
|
|
3376
|
+
const signedDistanceAt = (px, py, pz) => sphereSignedDistance(query, config, px, py, pz, scratchDir);
|
|
3377
|
+
const hitT = marchSignedDistance(
|
|
3378
|
+
ray,
|
|
3379
|
+
startT,
|
|
3380
|
+
endT,
|
|
3381
|
+
signedDistanceAt,
|
|
3382
|
+
signedDistanceAt,
|
|
3383
|
+
{
|
|
3384
|
+
maxSteps: Math.max(8, options?.maxSteps ?? 256),
|
|
3385
|
+
refinementSteps: Math.max(1, options?.refinementSteps ?? 12)
|
|
3386
|
+
},
|
|
3387
|
+
point
|
|
3388
|
+
);
|
|
3389
|
+
if (hitT === null) return null;
|
|
3390
|
+
ray.at(hitT, point);
|
|
3391
|
+
const sample = query.sampleTerrainByPosition(point);
|
|
3392
|
+
if (!sample.valid) return null;
|
|
3393
|
+
return {
|
|
3394
|
+
position: sample.position.clone(),
|
|
3395
|
+
normal: sample.normal.clone(),
|
|
3396
|
+
distance: ray.origin.distanceTo(sample.position)
|
|
3397
|
+
};
|
|
3398
|
+
}
|
|
3399
|
+
function cubeSphereRaycastBoundsOnly(ray, config, options) {
|
|
3400
|
+
const cx = config.centerX ?? 0;
|
|
3401
|
+
const cy = config.centerY ?? 0;
|
|
3402
|
+
const cz = config.centerZ ?? 0;
|
|
3403
|
+
const radius = config.radius ?? 0;
|
|
3404
|
+
const shell = intersectRaySphere(ray, cx, cy, cz, radius);
|
|
3405
|
+
if (!shell) return null;
|
|
3406
|
+
const maxDistance = options?.maxDistance ?? Number.POSITIVE_INFINITY;
|
|
3407
|
+
const t = shell.t0 >= 0 ? shell.t0 : shell.t1;
|
|
3408
|
+
if (t < 0 || t > maxDistance) return null;
|
|
3409
|
+
const point = new three.Vector3();
|
|
3410
|
+
ray.at(t, point);
|
|
3411
|
+
const normal = new three.Vector3(point.x - cx, point.y - cy, point.z - cz).normalize();
|
|
3412
|
+
return { position: point, normal, distance: ray.origin.distanceTo(point) };
|
|
3413
|
+
}
|
|
2685
3414
|
|
|
2686
3415
|
function createTerrainRaycast(params) {
|
|
2687
3416
|
return {
|
|
2688
3417
|
pick(ray, options) {
|
|
2689
3418
|
const config = params.getConfig();
|
|
2690
3419
|
const terrainQuery = params.getTerrainQuery();
|
|
3420
|
+
if (config.projection === "cubeSphere") {
|
|
3421
|
+
const sphereQuery = params.getSphereQuery();
|
|
3422
|
+
if (sphereQuery) {
|
|
3423
|
+
const precise = cubeSphereRaycast(sphereQuery, ray, config, options);
|
|
3424
|
+
if (precise) return precise;
|
|
3425
|
+
}
|
|
3426
|
+
return cubeSphereRaycastBoundsOnly(ray, config, options);
|
|
3427
|
+
}
|
|
2691
3428
|
if (terrainQuery) {
|
|
2692
3429
|
const precise = cpuRaycast(terrainQuery, ray, config, options);
|
|
2693
3430
|
if (precise) return precise;
|
|
@@ -2712,29 +3449,47 @@ const BOUNDS_PADDING = 1;
|
|
|
2712
3449
|
const RAYCAST_STATE = Symbol("terrainRaycastTaskState");
|
|
2713
3450
|
const terrainRaycastTask = work.task(
|
|
2714
3451
|
(get, work) => {
|
|
2715
|
-
const { query: terrainQuery } = get(terrainQueryTask);
|
|
3452
|
+
const { query: terrainQuery, sphereQuery } = get(terrainQueryTask);
|
|
2716
3453
|
const rootSizeValue = get(rootSize);
|
|
2717
3454
|
const originValue = get(origin);
|
|
2718
3455
|
const elevationScaleValue = get(elevationScale);
|
|
3456
|
+
const radiusValue = get(radius);
|
|
3457
|
+
const topologyValue = get(topologyTask);
|
|
3458
|
+
const projection = topologyValue.projection ?? "flat";
|
|
3459
|
+
const sphereRadius = topologyValue.radius ?? radiusValue;
|
|
2719
3460
|
return work((prev) => {
|
|
2720
3461
|
let raycast = prev;
|
|
2721
3462
|
let state = raycast?.[RAYCAST_STATE];
|
|
2722
3463
|
if (!state) {
|
|
2723
3464
|
state = {
|
|
2724
3465
|
terrainQuery: null,
|
|
3466
|
+
sphereQuery: null,
|
|
2725
3467
|
bounds: {
|
|
2726
3468
|
rootSize: 0,
|
|
2727
3469
|
originX: 0,
|
|
2728
3470
|
originZ: 0,
|
|
2729
3471
|
minY: 0,
|
|
2730
|
-
maxY: 0
|
|
3472
|
+
maxY: 0,
|
|
3473
|
+
projection: "flat",
|
|
3474
|
+
centerX: 0,
|
|
3475
|
+
centerY: 0,
|
|
3476
|
+
centerZ: 0,
|
|
3477
|
+
radius: 0,
|
|
3478
|
+
minRadius: 0,
|
|
3479
|
+
maxRadius: 0
|
|
2731
3480
|
}
|
|
2732
3481
|
};
|
|
2733
3482
|
}
|
|
2734
3483
|
state.terrainQuery = terrainQuery;
|
|
3484
|
+
state.sphereQuery = sphereQuery;
|
|
2735
3485
|
state.bounds.rootSize = rootSizeValue;
|
|
2736
3486
|
state.bounds.originX = originValue.x;
|
|
2737
3487
|
state.bounds.originZ = originValue.z;
|
|
3488
|
+
state.bounds.projection = projection;
|
|
3489
|
+
state.bounds.centerX = originValue.x;
|
|
3490
|
+
state.bounds.centerY = originValue.y;
|
|
3491
|
+
state.bounds.centerZ = originValue.z;
|
|
3492
|
+
state.bounds.radius = sphereRadius;
|
|
2738
3493
|
const range = terrainQuery.getGlobalElevationRange();
|
|
2739
3494
|
if (range) {
|
|
2740
3495
|
state.bounds.minY = range.min - BOUNDS_PADDING;
|
|
@@ -2744,9 +3499,19 @@ const terrainRaycastTask = work.task(
|
|
|
2744
3499
|
state.bounds.minY = originValue.y - verticalExtent;
|
|
2745
3500
|
state.bounds.maxY = originValue.y + verticalExtent;
|
|
2746
3501
|
}
|
|
3502
|
+
const elevationExtent = Math.max(1, Math.abs(elevationScaleValue));
|
|
3503
|
+
let dispMin = -elevationExtent;
|
|
3504
|
+
let dispMax = elevationExtent;
|
|
3505
|
+
if (range) {
|
|
3506
|
+
dispMin = range.min - originValue.y;
|
|
3507
|
+
dispMax = range.max - originValue.y;
|
|
3508
|
+
}
|
|
3509
|
+
state.bounds.minRadius = Math.max(0, sphereRadius + dispMin - BOUNDS_PADDING);
|
|
3510
|
+
state.bounds.maxRadius = sphereRadius + dispMax + BOUNDS_PADDING;
|
|
2747
3511
|
if (!raycast) {
|
|
2748
3512
|
raycast = createTerrainRaycast({
|
|
2749
3513
|
getTerrainQuery: () => state.terrainQuery,
|
|
3514
|
+
getSphereQuery: () => state.sphereQuery,
|
|
2750
3515
|
getConfig: () => state.bounds
|
|
2751
3516
|
});
|
|
2752
3517
|
}
|
|
@@ -2756,15 +3521,12 @@ const terrainRaycastTask = work.task(
|
|
|
2756
3521
|
}
|
|
2757
3522
|
).displayName("terrainRaycastTask");
|
|
2758
3523
|
|
|
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
3524
|
const terrainTasks = {
|
|
2763
3525
|
instanceId: instanceIdTask,
|
|
2764
3526
|
quadtreeConfig: quadtreeConfigTask,
|
|
2765
3527
|
quadtreeUpdate: quadtreeUpdateTask,
|
|
2766
3528
|
leafStorage: leafStorageTask,
|
|
2767
|
-
|
|
3529
|
+
topology: topologyTask,
|
|
2768
3530
|
leafGpuBuffer: leafGpuBufferTask,
|
|
2769
3531
|
gpuSpatialIndexStorage: gpuSpatialIndexStorageTask,
|
|
2770
3532
|
gpuSpatialIndexUpload: gpuSpatialIndexUploadTask,
|
|
@@ -2785,6 +3547,13 @@ const terrainTasks = {
|
|
|
2785
3547
|
terrainReadback: terrainReadbackTask,
|
|
2786
3548
|
terrainRaycast: terrainRaycastTask
|
|
2787
3549
|
};
|
|
3550
|
+
function terrainGraph() {
|
|
3551
|
+
const g = work.graph();
|
|
3552
|
+
for (const t of Object.values(terrainTasks)) {
|
|
3553
|
+
g.add(t);
|
|
3554
|
+
}
|
|
3555
|
+
return g;
|
|
3556
|
+
}
|
|
2788
3557
|
|
|
2789
3558
|
const textureSpaceToVectorSpace = tsl.Fn(([value]) => {
|
|
2790
3559
|
return tsl.remap(value, tsl.float(0), tsl.float(1), tsl.float(-1), tsl.float(1));
|
|
@@ -2840,10 +3609,11 @@ const voronoiCells = tsl.Fn((params) => {
|
|
|
2840
3609
|
|
|
2841
3610
|
exports.ArrayTextureBackend = ArrayTextureBackend;
|
|
2842
3611
|
exports.AtlasBackend = AtlasBackend;
|
|
3612
|
+
exports.CUBE_FACES = CUBE_FACES;
|
|
3613
|
+
exports.CUBE_FACE_COUNT = CUBE_FACE_COUNT;
|
|
2843
3614
|
exports.Dir = Dir;
|
|
2844
3615
|
exports.TerrainGeometry = TerrainGeometry;
|
|
2845
3616
|
exports.TerrainMesh = TerrainMesh;
|
|
2846
|
-
exports.Texture3DBackend = Texture3DBackend;
|
|
2847
3617
|
exports.U32_EMPTY = U32_EMPTY;
|
|
2848
3618
|
exports.allocLeafSet = allocLeafSet;
|
|
2849
3619
|
exports.allocSeamTable = allocSeamTable;
|
|
@@ -2853,10 +3623,10 @@ exports.buildLeafIndex = buildLeafIndex;
|
|
|
2853
3623
|
exports.buildSeams2to1 = buildSeams2to1;
|
|
2854
3624
|
exports.compileComputeTask = compileComputeTask;
|
|
2855
3625
|
exports.createComputePipelineTasks = createComputePipelineTasks;
|
|
2856
|
-
exports.
|
|
3626
|
+
exports.createCubeSphereTopology = createCubeSphereTopology;
|
|
2857
3627
|
exports.createElevationFieldContextTask = createElevationFieldContextTask;
|
|
2858
|
-
exports.
|
|
2859
|
-
exports.
|
|
3628
|
+
exports.createFlatTopology = createFlatTopology;
|
|
3629
|
+
exports.createInfiniteFlatTopology = createInfiniteFlatTopology;
|
|
2860
3630
|
exports.createSpatialIndex = createSpatialIndex;
|
|
2861
3631
|
exports.createState = createState;
|
|
2862
3632
|
exports.createTerrainFieldStorage = createTerrainFieldStorage;
|
|
@@ -2865,13 +3635,23 @@ exports.createTerrainQuery = createTerrainQuery;
|
|
|
2865
3635
|
exports.createTerrainRaycast = createTerrainRaycast;
|
|
2866
3636
|
exports.createTerrainSampler = createTerrainSampler;
|
|
2867
3637
|
exports.createTerrainSamplerTask = createTerrainSamplerTask;
|
|
3638
|
+
exports.createTerrainSphereQuery = createTerrainSphereQuery;
|
|
2868
3639
|
exports.createTerrainUniforms = createTerrainUniforms;
|
|
2869
3640
|
exports.createUniformsTask = createUniformsTask;
|
|
3641
|
+
exports.cubeFaceBasis = cubeFaceBasis;
|
|
3642
|
+
exports.cubeFaceDirection = cubeFaceDirection;
|
|
3643
|
+
exports.cubeFaceFromDirection = cubeFaceFromDirection;
|
|
3644
|
+
exports.cubeFacePoint = cubeFacePoint;
|
|
3645
|
+
exports.cubeFaceUVFromDirection = cubeFaceUVFromDirection;
|
|
2870
3646
|
exports.deriveNormalZ = deriveNormalZ;
|
|
3647
|
+
exports.directionToFace = directionToFace;
|
|
3648
|
+
exports.directionToFaceUV = directionToFaceUV;
|
|
3649
|
+
exports.directionToLatLong = directionToLatLong;
|
|
2871
3650
|
exports.elevationFieldStageTask = elevationFieldStageTask;
|
|
2872
3651
|
exports.elevationFn = elevationFn;
|
|
2873
3652
|
exports.elevationScale = elevationScale;
|
|
2874
3653
|
exports.executeComputeTask = executeComputeTask;
|
|
3654
|
+
exports.faceUVToCube = faceUVToCube;
|
|
2875
3655
|
exports.getDeviceComputeLimits = getDeviceComputeLimits;
|
|
2876
3656
|
exports.gpuSpatialIndexStorageTask = gpuSpatialIndexStorageTask;
|
|
2877
3657
|
exports.gpuSpatialIndexUploadTask = gpuSpatialIndexUploadTask;
|
|
@@ -2879,6 +3659,7 @@ exports.innerTileSegments = innerTileSegments;
|
|
|
2879
3659
|
exports.instanceIdTask = instanceIdTask;
|
|
2880
3660
|
exports.isSkirtUV = isSkirtUV;
|
|
2881
3661
|
exports.isSkirtVertex = isSkirtVertex;
|
|
3662
|
+
exports.latLongToDirection = latLongToDirection;
|
|
2882
3663
|
exports.leafGpuBufferTask = leafGpuBufferTask;
|
|
2883
3664
|
exports.leafStorageTask = leafStorageTask;
|
|
2884
3665
|
exports.loadTerrainField = loadTerrainField;
|
|
@@ -2892,16 +3673,16 @@ exports.positionNodeTask = positionNodeTask;
|
|
|
2892
3673
|
exports.quadtreeConfigTask = quadtreeConfigTask;
|
|
2893
3674
|
exports.quadtreeUpdate = quadtreeUpdate;
|
|
2894
3675
|
exports.quadtreeUpdateTask = quadtreeUpdateTask;
|
|
3676
|
+
exports.radius = radius;
|
|
2895
3677
|
exports.resetLeafSet = resetLeafSet;
|
|
2896
3678
|
exports.resetSeamTable = resetSeamTable;
|
|
2897
3679
|
exports.rootSize = rootSize;
|
|
2898
3680
|
exports.sampleTerrainField = sampleTerrainField;
|
|
2899
3681
|
exports.sampleTerrainFieldElevation = sampleTerrainFieldElevation;
|
|
2900
|
-
exports.sampleTerrainFieldNormal = sampleTerrainFieldNormal;
|
|
2901
3682
|
exports.skirtScale = skirtScale;
|
|
3683
|
+
exports.sphereTangentFrameNormal = sphereTangentFrameNormal;
|
|
2902
3684
|
exports.storeTerrainField = storeTerrainField;
|
|
2903
|
-
exports.
|
|
2904
|
-
exports.surfaceTask = surfaceTask;
|
|
3685
|
+
exports.tangentFromAxis = tangentFromAxis;
|
|
2905
3686
|
exports.terrainFieldFilter = terrainFieldFilter;
|
|
2906
3687
|
exports.terrainFieldStageTask = terrainFieldStageTask;
|
|
2907
3688
|
exports.terrainGraph = terrainGraph;
|
|
@@ -2911,6 +3692,9 @@ exports.terrainReadbackTask = terrainReadbackTask;
|
|
|
2911
3692
|
exports.terrainTasks = terrainTasks;
|
|
2912
3693
|
exports.textureSpaceToVectorSpace = textureSpaceToVectorSpace;
|
|
2913
3694
|
exports.tileNodesTask = tileNodesTask;
|
|
3695
|
+
exports.topology = topology;
|
|
3696
|
+
exports.topologyTask = topologyTask;
|
|
3697
|
+
exports.unpackTangentNormal = unpackTangentNormal;
|
|
2914
3698
|
exports.update = update;
|
|
2915
3699
|
exports.updateUniformsTask = updateUniformsTask;
|
|
2916
3700
|
exports.vElevation = vElevation;
|