s3m-three 0.4.26 → 0.4.27
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/browser/assets/{s3mWorker-DHOVIrAU.js → s3mWorker-BfSBNvF5.js} +1 -1
- package/dist/browser/s3m-three.iife.ext-three.js +52 -40
- package/dist/browser/s3m-three.iife.js +52 -40
- package/dist/core/BakeMercator.d.ts +54 -0
- package/dist/core/BakeMercator.d.ts.map +1 -0
- package/dist/core/BakeMercator.js +65 -0
- package/dist/core/BakeMercator.js.map +1 -0
- package/dist/core/TileGeometry.d.ts +7 -0
- package/dist/core/TileGeometry.d.ts.map +1 -1
- package/dist/core/TileGeometry.js +20 -0
- package/dist/core/TileGeometry.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/render/DatasetManager.d.ts +27 -0
- package/dist/render/DatasetManager.d.ts.map +1 -1
- package/dist/render/DatasetManager.js +18 -4
- package/dist/render/DatasetManager.js.map +1 -1
- package/dist/render/GeoAdapter.d.ts +49 -24
- package/dist/render/GeoAdapter.d.ts.map +1 -1
- package/dist/render/GeoAdapter.js +96 -8
- package/dist/render/GeoAdapter.js.map +1 -1
- package/dist/render/GeometryBuilder.d.ts +17 -0
- package/dist/render/GeometryBuilder.d.ts.map +1 -1
- package/dist/render/GeometryBuilder.js +31 -2
- package/dist/render/GeometryBuilder.js.map +1 -1
- package/dist/render/S3MViewer.d.ts +72 -2
- package/dist/render/S3MViewer.d.ts.map +1 -1
- package/dist/render/S3MViewer.js +269 -9
- package/dist/render/S3MViewer.js.map +1 -1
- package/dist/render/StyleMaterial.d.ts +5 -0
- package/dist/render/StyleMaterial.d.ts.map +1 -1
- package/dist/render/StyleMaterial.js +21 -0
- package/dist/render/StyleMaterial.js.map +1 -1
- package/dist/render/tilePlacement.d.ts +84 -0
- package/dist/render/tilePlacement.d.ts.map +1 -0
- package/dist/render/tilePlacement.js +71 -0
- package/dist/render/tilePlacement.js.map +1 -0
- package/dist/render/viewer/EdgeDecorator.d.ts +1 -0
- package/dist/render/viewer/EdgeDecorator.d.ts.map +1 -1
- package/dist/render/viewer/EdgeDecorator.js +3 -0
- package/dist/render/viewer/EdgeDecorator.js.map +1 -1
- package/dist/render/viewer/PickService.d.ts +13 -0
- package/dist/render/viewer/PickService.d.ts.map +1 -1
- package/dist/render/viewer/PickService.js +24 -15
- package/dist/render/viewer/PickService.js.map +1 -1
- package/dist/render/viewer/types.d.ts +53 -0
- package/dist/render/viewer/types.d.ts.map +1 -1
- package/dist/render/viewer/types.js.map +1 -1
- package/dist/tileset/S3MTileset.d.ts +43 -1
- package/dist/tileset/S3MTileset.d.ts.map +1 -1
- package/dist/tileset/S3MTileset.js +202 -2
- package/dist/tileset/S3MTileset.js.map +1 -1
- package/dist/worker/s3mWorker.js +115 -62
- package/dist/worker/s3mWorker.js.map +1 -1
- package/docs/API.md +18 -0
- package/package.json +1 -1
- package/src/core/BakeMercator.ts +96 -0
- package/src/core/TileGeometry.ts +26 -0
- package/src/index.ts +11 -1
- package/src/render/DatasetManager.ts +43 -5
- package/src/render/GeoAdapter.ts +139 -11
- package/src/render/GeometryBuilder.ts +53 -2
- package/src/render/S3MViewer.ts +306 -13
- package/src/render/StyleMaterial.ts +26 -0
- package/src/render/tilePlacement.ts +121 -0
- package/src/render/viewer/EdgeDecorator.ts +3 -0
- package/src/render/viewer/PickService.ts +44 -16
- package/src/render/viewer/types.ts +51 -0
- package/src/tileset/S3MTileset.ts +226 -3
|
@@ -49,11 +49,30 @@ export interface PickHost {
|
|
|
49
49
|
readonly scene: THREE.Scene | null;
|
|
50
50
|
readonly canvas: HTMLCanvasElement | null;
|
|
51
51
|
readonly style: { floorHeight: number };
|
|
52
|
-
readonly sceneAnchor: {
|
|
52
|
+
readonly sceneAnchor: {
|
|
53
|
+
x: number;
|
|
54
|
+
y: number;
|
|
55
|
+
z: number;
|
|
56
|
+
meterInMercatorEast?: number;
|
|
57
|
+
meterInMercatorNorth?: number;
|
|
58
|
+
meterInMercator: number;
|
|
59
|
+
altitude?: number;
|
|
60
|
+
} | null;
|
|
53
61
|
readonly datasets: Map<string, { name: string }>;
|
|
54
62
|
readonly featureStyleManager: FeatureStyleManager;
|
|
55
63
|
readonly smidMeshes: Map<number, Set<THREE.Mesh>>;
|
|
56
64
|
lngLatFromWorld(x: number, y: number): [number, number];
|
|
65
|
+
/**
|
|
66
|
+
* 世界坐标 → **规范**场景 ENU(把瓦片级放置的缩放反解掉)。
|
|
67
|
+
*
|
|
68
|
+
* `perTilePlacement` 开启时瓦片几何是绕自身中心缩放 `k` 倍后摆到正确位置的,
|
|
69
|
+
* 命中点的世界坐标因此不再等于「数据本身的 ENU」。拾取要报的是坐标本身,
|
|
70
|
+
* 不能带渲染相对底图的缩放,否则同一个屋顶在 20 km 外会比 1 km 内偏出几米,
|
|
71
|
+
* 量算、贴地、外部标注全都跟着错。
|
|
72
|
+
*
|
|
73
|
+
* 未开启本功能、或网格不在已放置的瓦片里时**原样返回**(默认路径零副作用)。
|
|
74
|
+
*/
|
|
75
|
+
toCanonicalScene(p: THREE.Vector3, mesh?: THREE.Object3D | null): THREE.Vector3;
|
|
57
76
|
gpuPathPoseAt(smid: number): { pos: [number, number, number]; yaw: number } | null;
|
|
58
77
|
isFeatureHidden(smid: number | null, batchId: number): boolean;
|
|
59
78
|
}
|
|
@@ -219,8 +238,10 @@ export class PickService {
|
|
|
219
238
|
if (box && this.host.sceneAnchor) {
|
|
220
239
|
const v = new THREE.Vector3((box.minX + box.maxX) / 2, (box.minY + box.maxY) / 2, (box.minZ + box.maxZ) / 2);
|
|
221
240
|
v.applyMatrix4(ref.mesh.matrixWorld);
|
|
222
|
-
const
|
|
223
|
-
|
|
241
|
+
const c = this.host.toCanonicalScene(v, ref.mesh);
|
|
242
|
+
const sEast = this.host.sceneAnchor.meterInMercatorEast ?? this.host.sceneAnchor.meterInMercator;
|
|
243
|
+
const sNorth = this.host.sceneAnchor.meterInMercatorNorth ?? this.host.sceneAnchor.meterInMercator;
|
|
244
|
+
centerLngLat = this.host.lngLatFromWorld(c.x * sEast + this.host.sceneAnchor.x, -c.y * sNorth + this.host.sceneAnchor.y);
|
|
224
245
|
}
|
|
225
246
|
|
|
226
247
|
// 精确命中点求交计算
|
|
@@ -237,7 +258,8 @@ export class PickService {
|
|
|
237
258
|
const off = smid != null ? this.host.featureStyleManager.getFeatureEffectiveOffset(smid) : {};
|
|
238
259
|
const trans = off.translation;
|
|
239
260
|
const elev = off.elevation;
|
|
240
|
-
|
|
261
|
+
// 先反解瓦片级放置,再叠单体位移 —— 位移是「米」,必须落在规范 ENU 里
|
|
262
|
+
const finalPos = this.host.toCanonicalScene(hitPoint, ref.mesh).clone();
|
|
241
263
|
|
|
242
264
|
if (off.rotation) {
|
|
243
265
|
const rotMat = resolveRotationMatrix(off.rotation);
|
|
@@ -256,8 +278,9 @@ export class PickService {
|
|
|
256
278
|
else if (elev) finalPos.z += elev;
|
|
257
279
|
|
|
258
280
|
point = [finalPos.x, finalPos.y, finalPos.z];
|
|
259
|
-
const
|
|
260
|
-
|
|
281
|
+
const sEast = this.host.sceneAnchor.meterInMercatorEast ?? this.host.sceneAnchor.meterInMercator;
|
|
282
|
+
const sNorth = this.host.sceneAnchor.meterInMercatorNorth ?? this.host.sceneAnchor.meterInMercator;
|
|
283
|
+
lngLat = this.host.lngLatFromWorld(finalPos.x * sEast + this.host.sceneAnchor.x, -finalPos.y * sNorth + this.host.sceneAnchor.y);
|
|
261
284
|
altitude = finalPos.z + (this.host.sceneAnchor.altitude ?? 0);
|
|
262
285
|
} else if (centerLngLat && box && this.host.sceneAnchor) {
|
|
263
286
|
lngLat = centerLngLat;
|
|
@@ -321,16 +344,18 @@ export class PickService {
|
|
|
321
344
|
floorHeight: this.host.style.floorHeight,
|
|
322
345
|
});
|
|
323
346
|
|
|
324
|
-
const
|
|
347
|
+
const sEast = anchor.meterInMercatorEast ?? anchor.meterInMercator;
|
|
348
|
+
const sNorth = anchor.meterInMercatorNorth ?? anchor.meterInMercator;
|
|
325
349
|
let point: [number, number, number] | null = null;
|
|
326
350
|
let lngLat: [number, number] | null = null;
|
|
327
351
|
let altitude: number | null = null;
|
|
328
352
|
let centerLngLat: [number, number] | null = null;
|
|
329
353
|
|
|
330
354
|
if (hitPoint) {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
355
|
+
const c = this.host.toCanonicalScene(hitPoint, mesh);
|
|
356
|
+
point = [c.x, c.y, c.z];
|
|
357
|
+
lngLat = this.host.lngLatFromWorld(c.x * sEast + anchor.x, -c.y * sNorth + anchor.y);
|
|
358
|
+
altitude = c.z + (anchor.altitude ?? 0);
|
|
334
359
|
// 泼溅命中的就是一个高斯,没有「单体中心」可言,用命中点当中心
|
|
335
360
|
centerLngLat = lngLat;
|
|
336
361
|
} else if (box) {
|
|
@@ -339,8 +364,9 @@ export class PickService {
|
|
|
339
364
|
(box.minY + box.maxY) / 2,
|
|
340
365
|
(box.minZ + box.maxZ) / 2
|
|
341
366
|
).applyMatrix4(mesh.matrixWorld);
|
|
342
|
-
|
|
343
|
-
|
|
367
|
+
const c = this.host.toCanonicalScene(v, mesh);
|
|
368
|
+
centerLngLat = this.host.lngLatFromWorld(c.x * sEast + anchor.x, -c.y * sNorth + anchor.y);
|
|
369
|
+
altitude = c.z + (anchor.altitude ?? 0);
|
|
344
370
|
}
|
|
345
371
|
|
|
346
372
|
return {
|
|
@@ -395,8 +421,9 @@ export class PickService {
|
|
|
395
421
|
const target = new THREE.Vector3();
|
|
396
422
|
const intersect = ray.intersectPlane(groundPlane, target);
|
|
397
423
|
if (intersect) {
|
|
398
|
-
const
|
|
399
|
-
const
|
|
424
|
+
const sEast = this.host.sceneAnchor.meterInMercatorEast ?? this.host.sceneAnchor.meterInMercator;
|
|
425
|
+
const sNorth = this.host.sceneAnchor.meterInMercatorNorth ?? this.host.sceneAnchor.meterInMercator;
|
|
426
|
+
const lngLat = this.host.lngLatFromWorld(intersect.x * sEast + this.host.sceneAnchor.x, -intersect.y * sNorth + this.host.sceneAnchor.y);
|
|
400
427
|
const altitude = intersect.z + (this.host.sceneAnchor.altitude ?? 0);
|
|
401
428
|
return {
|
|
402
429
|
lngLat,
|
|
@@ -643,8 +670,9 @@ export class PickService {
|
|
|
643
670
|
if (gpuPose) c.add(new THREE.Vector3(gpuPose.pos[0], gpuPose.pos[1], gpuPose.pos[2]));
|
|
644
671
|
|
|
645
672
|
c.applyMatrix4(mesh.matrixWorld);
|
|
646
|
-
const
|
|
647
|
-
const
|
|
673
|
+
const sEast = this.host.sceneAnchor.meterInMercatorEast ?? this.host.sceneAnchor.meterInMercator;
|
|
674
|
+
const sNorth = this.host.sceneAnchor.meterInMercatorNorth ?? this.host.sceneAnchor.meterInMercator;
|
|
675
|
+
const [lng, lat] = this.host.lngLatFromWorld(c.x * sEast + this.host.sceneAnchor.x, -c.y * sNorth + this.host.sceneAnchor.y);
|
|
648
676
|
if (lng < minLng) minLng = lng;
|
|
649
677
|
if (lng > maxLng) maxLng = lng;
|
|
650
678
|
if (lat < minLat) minLat = lat;
|
|
@@ -77,7 +77,23 @@ export interface LightOptions {
|
|
|
77
77
|
altitude?: number;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
/**
|
|
81
|
+
* 本层绘制前如何处理宿主已有的深度缓冲。
|
|
82
|
+
* - `'scene'` 保留(默认)。地形能遮挡本层几何,适用于建筑、管线等离散对象。
|
|
83
|
+
* - `'clear'` 先清深度再画,本层只与自己做深度测试。适用于贴地地面类数据(倾斜摄影、大范围 DEM 化模型等),消除共面 z-fighting。
|
|
84
|
+
* - `'auto'` 按数据集类型推断:有 ground 覆盖 / OSGB 倾斜摄影 / 地面类网格 → `'clear'`,否则 `'scene'`。
|
|
85
|
+
*/
|
|
86
|
+
export type DepthMode = 'scene' | 'clear' | 'auto';
|
|
87
|
+
|
|
80
88
|
export interface S3MViewerOptions {
|
|
89
|
+
/**
|
|
90
|
+
* 本层绘制前如何处理宿主已有的深度缓冲。
|
|
91
|
+
* - `'scene'` 保留(默认)。地形能遮挡本层几何,适用于建筑、管线等离散对象。
|
|
92
|
+
* - `'clear'` 先清深度再画,本层只与自己做深度测试。适用于贴地地面类数据。
|
|
93
|
+
* - `'auto'` 按数据集类型推断:有 ground 覆盖 / OSGB 倾斜摄影 / 地面类网格 → `'clear'`,否则 `'scene'`。
|
|
94
|
+
* 默认 `'scene'`,与改动前行为逐字一致。
|
|
95
|
+
*/
|
|
96
|
+
depthMode?: DepthMode;
|
|
81
97
|
/**
|
|
82
98
|
* 光照预设:
|
|
83
99
|
* - `'default'`(默认):现有 PBR 打光,亮部线性总量 ≈ 2.55;
|
|
@@ -134,6 +150,34 @@ export interface S3MViewerOptions {
|
|
|
134
150
|
* 所以默认不启用,由接入方按设备档位自行决定。运行时可用 `setLodPixelRatioCap()` 调整。
|
|
135
151
|
*/
|
|
136
152
|
lodPixelRatioCap?: number;
|
|
153
|
+
/**
|
|
154
|
+
* 瓦片级放置(默认 `true`)。
|
|
155
|
+
*
|
|
156
|
+
* 开启后每块瓦片用自己的中心纬度算 `meterInMercator`,几何绕自身中心缩放,
|
|
157
|
+
* 修正「全场景共用一个切平面」在大范围下的尺度误差。
|
|
158
|
+
*
|
|
159
|
+
* 误差量级(北纬 40°,距场景锚点的距离 → 相对尺度的偏差):
|
|
160
|
+
* `5 km → 0.065%`、`20 km → 0.26%(约 26 m)`、`200 km → 2.8%(约 2.8 km)`。
|
|
161
|
+
* 城市级(几公里)随手开无妨;多数据集相距几十公里、或单个数据集跨 20 km 以上时
|
|
162
|
+
* 必须开,否则远处的模型会整体偏大/偏小。
|
|
163
|
+
*
|
|
164
|
+
* 代价与遗留:
|
|
165
|
+
* - 每块瓦片的 `Object3D` 走 `matrixAutoUpdate = false`,多一次矩阵乘;
|
|
166
|
+
* - 场景 ENU 仍是**一个平面**,所以只是把误差按瓦片重新分摊(瓦片跨 2 km 时
|
|
167
|
+
* 剩余约 0.5 m),彻底消除要逐顶点烘焙墨卡托;
|
|
168
|
+
* - 拾取 / 量算的 `lngLat ↔ 场景 ENU` 换算仍用场景锚点的常量 s,在本模式下
|
|
169
|
+
* 保持修复前的误差量级(几何与反解不再自洽,但都不比之前差)。
|
|
170
|
+
*
|
|
171
|
+
* 见 `render/tilePlacement.ts` 的文件头。
|
|
172
|
+
*/
|
|
173
|
+
perTilePlacement?: boolean;
|
|
174
|
+
/**
|
|
175
|
+
* 是否对瓦片逐顶点烘焙精确 Web 墨卡托偏移(消除全球大尺度下的视差游动/呼吸效应)。
|
|
176
|
+
* - undefined(默认):自动判定。当瓦片集根节点包围球半径 > 25 km 时自动启用,<= 25 km 时保持关闭(逐字节保持原有 ENU 顶点缓冲不变)。
|
|
177
|
+
* - true:强制开启。
|
|
178
|
+
* - false:强制关闭。
|
|
179
|
+
*/
|
|
180
|
+
bakeMercator?: boolean;
|
|
137
181
|
/**
|
|
138
182
|
* 每个数据集驻留三角形总数的软上限(默认不限制)。
|
|
139
183
|
* 见 `S3MTilesetOptions.maxTriangles`:瓦片数管不住的场景(精模、大面片)用这条刹车。
|
|
@@ -237,6 +281,13 @@ export interface S3MViewerOptions {
|
|
|
237
281
|
* 编译着色器、卡一帧。预热在首帧之后异步做,期间跳一两帧渲染,肉眼不可见。
|
|
238
282
|
*/
|
|
239
283
|
prewarmShading?: boolean;
|
|
284
|
+
/**
|
|
285
|
+
* LOD 细化过渡策略:
|
|
286
|
+
* - 'binary'(默认):整节点二值切换
|
|
287
|
+
* - 'incremental':增量细化(primary / fallback 双列表 + LEQUAL 深度推移)
|
|
288
|
+
*/
|
|
289
|
+
lodTransition?: 'binary' | 'incremental';
|
|
290
|
+
fallbackOffset?: { factor?: number; units?: number };
|
|
240
291
|
}
|
|
241
292
|
|
|
242
293
|
/* ------------------------------------------------------------------ 内部类型 */
|
|
@@ -32,6 +32,7 @@ import type {
|
|
|
32
32
|
} from '../core/S3MBlockParser.js';
|
|
33
33
|
import type { Anchor, ParsedTile, Stats, StyleUniforms } from '../core/types.js';
|
|
34
34
|
import type { TileGeometryData } from '../core/TileGeometry.js';
|
|
35
|
+
import { AUTO_BAKE_RADIUS_THRESHOLD } from '../core/BakeMercator.js';
|
|
35
36
|
import type { TextureCreateOptions } from '../render/TextureFactory.js';
|
|
36
37
|
import type { Group, Texture } from 'three';
|
|
37
38
|
|
|
@@ -304,10 +305,14 @@ export type BuildTileFn = (
|
|
|
304
305
|
colorPipeline?: 'linear' | 'direct';
|
|
305
306
|
/** 纹理创建参数(透传给 buildTileObjects → createTexture) */
|
|
306
307
|
textureOptions?: TextureCreateOptions;
|
|
308
|
+
bakeMercator?: boolean;
|
|
309
|
+
tileAnchor?: { lon: number; lat: number; height?: number };
|
|
310
|
+
tileCenter?: { x: number; y: number; z?: number };
|
|
307
311
|
}
|
|
308
312
|
) => Promise<{ group: Group; stats: Partial<Stats> }>;
|
|
309
313
|
|
|
310
314
|
export interface S3MTilesetOptions {
|
|
315
|
+
bakeMercator?: boolean;
|
|
311
316
|
configUrl: string;
|
|
312
317
|
maxTiles?: number;
|
|
313
318
|
maxConcurrent?: number;
|
|
@@ -388,6 +393,18 @@ export interface S3MTilesetOptions {
|
|
|
388
393
|
* 'silent' —— 只记录到 `stats.degradations`,由 UI 自己决定怎么提示。
|
|
389
394
|
*/
|
|
390
395
|
unsupported?: UnsupportedPolicy;
|
|
396
|
+
/**
|
|
397
|
+
* LOD 细化过渡策略:
|
|
398
|
+
* - 'binary'(默认):整节点二值切换。所有子瓦片全部就绪或失败后,父瓦片整块隐藏、子瓦片整块接替。
|
|
399
|
+
* - 'incremental':增量细化(primary / fallback 双列表 + LEQUAL 深度推移)。
|
|
400
|
+
* 子瓦片就绪一个显示一个,未就绪区域由父级(fallback)兜底填充,消除整块跳变与慢 child 阻塞。
|
|
401
|
+
*/
|
|
402
|
+
lodTransition?: 'binary' | 'incremental';
|
|
403
|
+
/**
|
|
404
|
+
* 增量细化模式下 fallback 兜底瓦片的材质深度偏移系数。
|
|
405
|
+
* 默认 { factor: 1, units: 2 },通过向远处推移深度配合 gl.LEQUAL 消除共面 z-fighting。
|
|
406
|
+
*/
|
|
407
|
+
fallbackOffset?: { factor?: number; units?: number };
|
|
391
408
|
}
|
|
392
409
|
|
|
393
410
|
/** 估算单个纹理占用的显存字节数 */
|
|
@@ -505,6 +522,10 @@ export class S3MTileset {
|
|
|
505
522
|
*/
|
|
506
523
|
maxMemory: number;
|
|
507
524
|
lodRangeScale: number;
|
|
525
|
+
lodTransition: 'binary' | 'incremental';
|
|
526
|
+
fallbackOffset: { factor: number; units: number };
|
|
527
|
+
readonly primaryNodes = new Set<string>();
|
|
528
|
+
readonly fallbackNodes = new Set<string>();
|
|
508
529
|
|
|
509
530
|
/** 数据集配置声明的高度范围(米),若配置未声明则返回 null */
|
|
510
531
|
get heightRange(): { min: number; max: number } | null {
|
|
@@ -535,8 +556,16 @@ export class S3MTileset {
|
|
|
535
556
|
/** 请求量的硬上限(软上限 maxTiles 的超额余量),见构造函数注释 */
|
|
536
557
|
hardCap = 96;
|
|
537
558
|
/** 本数据集是 S3M 1.x(SuperMapCache,.s3m):子瓦地址规则与 3.x 不同 */
|
|
538
|
-
|
|
559
|
+
isS3MV1 = false;
|
|
539
560
|
private _buildTile: BuildTileFn | null;
|
|
561
|
+
private _bakeMercator?: boolean;
|
|
562
|
+
rootRadius?: number;
|
|
563
|
+
|
|
564
|
+
shouldBakeMercator(radius?: number): boolean {
|
|
565
|
+
if (this._bakeMercator !== undefined) return this._bakeMercator;
|
|
566
|
+
const r = radius ?? this.rootRadius ?? 0;
|
|
567
|
+
return r > AUTO_BAKE_RADIUS_THRESHOLD;
|
|
568
|
+
}
|
|
540
569
|
private readonly _polygonOffset: { factor: number; units: number } | undefined;
|
|
541
570
|
private _materialQuality: 'pbr' | 'lambert' | undefined;
|
|
542
571
|
private _colorPipeline: 'linear' | 'direct' | undefined;
|
|
@@ -550,6 +579,11 @@ export class S3MTileset {
|
|
|
550
579
|
constructor(options: S3MTilesetOptions) {
|
|
551
580
|
this.configUrl = options.configUrl;
|
|
552
581
|
this.lodRangeScale = options.lodRangeScale ?? 3.0;
|
|
582
|
+
this.lodTransition = options.lodTransition ?? 'binary';
|
|
583
|
+
this.fallbackOffset = {
|
|
584
|
+
factor: options.fallbackOffset?.factor ?? 1,
|
|
585
|
+
units: options.fallbackOffset?.units ?? 2,
|
|
586
|
+
};
|
|
553
587
|
this._onTile = options.onTile ?? (() => {});
|
|
554
588
|
this._onTileRemoved = options.onTileRemoved ?? (() => {});
|
|
555
589
|
this._onError = options.onError ?? ((e) => console.warn(e));
|
|
@@ -560,6 +594,7 @@ export class S3MTileset {
|
|
|
560
594
|
|
|
561
595
|
// 渲染层可注入:Node 环境下不需要 three,也能跑 URL / 解析链路
|
|
562
596
|
this._buildTile = options.buildTile ?? null;
|
|
597
|
+
this._bakeMercator = options.bakeMercator;
|
|
563
598
|
this._polygonOffset = options.polygonOffset;
|
|
564
599
|
this._materialQuality = options.materialQuality;
|
|
565
600
|
this._colorPipeline = options.colorPipeline;
|
|
@@ -903,6 +938,9 @@ export class S3MTileset {
|
|
|
903
938
|
if (refined !== allowRefined) continue;
|
|
904
939
|
// 正在显示且没有可见子瓦接手 → 踢了就是洞
|
|
905
940
|
if (!allowUncovered && node.group.visible) {
|
|
941
|
+
if (this.lodTransition === 'incremental') {
|
|
942
|
+
continue;
|
|
943
|
+
}
|
|
906
944
|
const covered = childUrls.some((u) => {
|
|
907
945
|
const c = this.nodes.get(u);
|
|
908
946
|
return !!c && c.group.visible;
|
|
@@ -1350,6 +1388,11 @@ export class S3MTileset {
|
|
|
1350
1388
|
// 解析成功但能力不完整的(目前只有 3DGS),在花力气建几何之前先决定处置
|
|
1351
1389
|
this._noteDegradations(tile);
|
|
1352
1390
|
|
|
1391
|
+
const lodForBake = this._pickLod(tile);
|
|
1392
|
+
if (this.rootRadius === undefined && lodForBake?.boundingSphere?.radius) {
|
|
1393
|
+
this.rootRadius = lodForBake.boundingSphere.radius;
|
|
1394
|
+
}
|
|
1395
|
+
const shouldBake = this.shouldBakeMercator(lodForBake?.boundingSphere?.radius);
|
|
1353
1396
|
const build = await this._builder();
|
|
1354
1397
|
const buildRes = await build(tile, {
|
|
1355
1398
|
textureCache: this.textureCache,
|
|
@@ -1359,6 +1402,9 @@ export class S3MTileset {
|
|
|
1359
1402
|
materialQuality: this._materialQuality,
|
|
1360
1403
|
colorPipeline: this._colorPipeline,
|
|
1361
1404
|
textureOptions: this._textureOptions,
|
|
1405
|
+
bakeMercator: shouldBake,
|
|
1406
|
+
tileAnchor: this.anchor,
|
|
1407
|
+
tileCenter: lodForBake?.boundingSphere?.center,
|
|
1362
1408
|
});
|
|
1363
1409
|
const { group, stats } = buildRes;
|
|
1364
1410
|
const texKeys = (buildRes as any).textureKeys ?? (group.userData?.textureKeys as string[] | undefined) ?? Object.keys(tile.textures ?? {});
|
|
@@ -1570,10 +1616,21 @@ export class S3MTileset {
|
|
|
1570
1616
|
if (!p || !this.nodes.has(p)) roots.add(url);
|
|
1571
1617
|
}
|
|
1572
1618
|
|
|
1573
|
-
|
|
1574
|
-
if (this.
|
|
1619
|
+
if (this.lodTransition === 'incremental') {
|
|
1620
|
+
if (this._updateVisibilityIncremental(roots, visited)) {
|
|
1575
1621
|
hasChanges = true;
|
|
1576
1622
|
}
|
|
1623
|
+
} else {
|
|
1624
|
+
for (const rootUrl of roots) {
|
|
1625
|
+
if (this._updateNodeVisibility(rootUrl, true, visited)) {
|
|
1626
|
+
hasChanges = true;
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
this.primaryNodes.clear();
|
|
1630
|
+
for (const [url, node] of this.nodes) {
|
|
1631
|
+
if (node.group.visible) this.primaryNodes.add(url);
|
|
1632
|
+
}
|
|
1633
|
+
this.fallbackNodes.clear();
|
|
1577
1634
|
}
|
|
1578
1635
|
|
|
1579
1636
|
// 防御:若有任何未被 root 树访问到的游离瓦片,全部隐藏
|
|
@@ -1583,6 +1640,9 @@ export class S3MTileset {
|
|
|
1583
1640
|
node.group.visible = false;
|
|
1584
1641
|
hasChanges = true;
|
|
1585
1642
|
}
|
|
1643
|
+
if (this.lodTransition === 'incremental') {
|
|
1644
|
+
this._restoreNodePolygonOffset(node);
|
|
1645
|
+
}
|
|
1586
1646
|
}
|
|
1587
1647
|
}
|
|
1588
1648
|
|
|
@@ -1674,6 +1734,169 @@ export class S3MTileset {
|
|
|
1674
1734
|
return changed;
|
|
1675
1735
|
}
|
|
1676
1736
|
|
|
1737
|
+
|
|
1738
|
+
getLodTransition(): 'binary' | 'incremental' {
|
|
1739
|
+
return this.lodTransition;
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
setLodTransition(mode: 'binary' | 'incremental'): void {
|
|
1743
|
+
if (this.lodTransition === mode) return;
|
|
1744
|
+
this.lodTransition = mode;
|
|
1745
|
+
if (mode === 'binary') {
|
|
1746
|
+
for (const node of this.nodes.values()) {
|
|
1747
|
+
this._restoreNodePolygonOffset(node);
|
|
1748
|
+
}
|
|
1749
|
+
this.fallbackNodes.clear();
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
private _applyNodeFallbackPolygonOffset(node: TileNode, depth: number): boolean {
|
|
1754
|
+
const factor = this.fallbackOffset.factor;
|
|
1755
|
+
const units = Math.max(2, (10 - Math.min(depth, 8)) * this.fallbackOffset.units);
|
|
1756
|
+
let changed = false;
|
|
1757
|
+
|
|
1758
|
+
node.group.traverse((obj) => {
|
|
1759
|
+
const target = obj as { isMesh?: boolean; isPoints?: boolean; isLine?: boolean; material?: any };
|
|
1760
|
+
if ((target.isMesh || target.isPoints || target.isLine) && target.material) {
|
|
1761
|
+
const materials = Array.isArray(target.material) ? target.material : [target.material];
|
|
1762
|
+
for (const mat of materials) {
|
|
1763
|
+
if (!mat) continue;
|
|
1764
|
+
if (!mat.userData) mat.userData = {};
|
|
1765
|
+
if (!mat.userData._basePolygonOffset) {
|
|
1766
|
+
mat.userData._basePolygonOffset = {
|
|
1767
|
+
polygonOffset: mat.polygonOffset === true,
|
|
1768
|
+
factor: mat.polygonOffsetFactor ?? 0,
|
|
1769
|
+
units: mat.polygonOffsetUnits ?? 0,
|
|
1770
|
+
};
|
|
1771
|
+
}
|
|
1772
|
+
const base = mat.userData._basePolygonOffset;
|
|
1773
|
+
const targetFactor = base.factor + factor;
|
|
1774
|
+
const targetUnits = base.units + units;
|
|
1775
|
+
if (
|
|
1776
|
+
mat.polygonOffset !== true ||
|
|
1777
|
+
mat.polygonOffsetFactor !== targetFactor ||
|
|
1778
|
+
mat.polygonOffsetUnits !== targetUnits
|
|
1779
|
+
) {
|
|
1780
|
+
mat.polygonOffset = true;
|
|
1781
|
+
mat.polygonOffsetFactor = targetFactor;
|
|
1782
|
+
mat.polygonOffsetUnits = targetUnits;
|
|
1783
|
+
changed = true;
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
});
|
|
1788
|
+
return changed;
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
private _restoreNodePolygonOffset(node: TileNode): boolean {
|
|
1792
|
+
let changed = false;
|
|
1793
|
+
node.group.traverse((obj) => {
|
|
1794
|
+
const target = obj as { isMesh?: boolean; isPoints?: boolean; isLine?: boolean; material?: any };
|
|
1795
|
+
if ((target.isMesh || target.isPoints || target.isLine) && target.material) {
|
|
1796
|
+
const materials = Array.isArray(target.material) ? target.material : [target.material];
|
|
1797
|
+
for (const mat of materials) {
|
|
1798
|
+
if (mat?.userData?._basePolygonOffset) {
|
|
1799
|
+
const base = mat.userData._basePolygonOffset;
|
|
1800
|
+
if (
|
|
1801
|
+
mat.polygonOffset !== base.polygonOffset ||
|
|
1802
|
+
mat.polygonOffsetFactor !== base.factor ||
|
|
1803
|
+
mat.polygonOffsetUnits !== base.units
|
|
1804
|
+
) {
|
|
1805
|
+
mat.polygonOffset = base.polygonOffset;
|
|
1806
|
+
mat.polygonOffsetFactor = base.factor;
|
|
1807
|
+
mat.polygonOffsetUnits = base.units;
|
|
1808
|
+
changed = true;
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
});
|
|
1814
|
+
return changed;
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
private _updateVisibilityIncremental(roots: Iterable<string>, visited: Set<string>): boolean {
|
|
1818
|
+
const nextPrimary = new Set<string>();
|
|
1819
|
+
const nextFallback = new Set<string>();
|
|
1820
|
+
const nodeDepths = new Map<string, number>();
|
|
1821
|
+
|
|
1822
|
+
const traverse = (url: string, depth: number) => {
|
|
1823
|
+
if (visited.has(url)) return;
|
|
1824
|
+
visited.add(url);
|
|
1825
|
+
nodeDepths.set(url, depth);
|
|
1826
|
+
|
|
1827
|
+
const node = this.nodes.get(url);
|
|
1828
|
+
if (!node) return;
|
|
1829
|
+
|
|
1830
|
+
const childUrls = node.childUrls ?? (node.childUrl ? [node.childUrl] : []);
|
|
1831
|
+
const hasChildren = childUrls.length > 0;
|
|
1832
|
+
const readyChildren = childUrls.filter((cUrl) => this.nodes.has(cUrl));
|
|
1833
|
+
const missingChildren = childUrls.filter(
|
|
1834
|
+
(cUrl) => !this.nodes.has(cUrl) && !this.failed.has(cUrl)
|
|
1835
|
+
);
|
|
1836
|
+
|
|
1837
|
+
const canRefine = !!node.needRefine && hasChildren && readyChildren.length > 0;
|
|
1838
|
+
|
|
1839
|
+
if (canRefine) {
|
|
1840
|
+
if (missingChildren.length > 0) {
|
|
1841
|
+
nextFallback.add(url);
|
|
1842
|
+
}
|
|
1843
|
+
for (const cUrl of readyChildren) {
|
|
1844
|
+
traverse(cUrl, depth + 1);
|
|
1845
|
+
}
|
|
1846
|
+
} else {
|
|
1847
|
+
nextPrimary.add(url);
|
|
1848
|
+
}
|
|
1849
|
+
};
|
|
1850
|
+
|
|
1851
|
+
for (const rootUrl of roots) {
|
|
1852
|
+
traverse(rootUrl, 0);
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
let changed = false;
|
|
1856
|
+
|
|
1857
|
+
for (const [url, node] of this.nodes) {
|
|
1858
|
+
if (!visited.has(url)) continue;
|
|
1859
|
+
|
|
1860
|
+
if (nextPrimary.has(url)) {
|
|
1861
|
+
if (!node.group.visible) {
|
|
1862
|
+
node.group.visible = true;
|
|
1863
|
+
changed = true;
|
|
1864
|
+
}
|
|
1865
|
+
const depth = nodeDepths.get(url) ?? 0;
|
|
1866
|
+
const targetOrder = 100 + depth;
|
|
1867
|
+
if (node.group.renderOrder !== targetOrder) {
|
|
1868
|
+
node.group.renderOrder = targetOrder;
|
|
1869
|
+
changed = true;
|
|
1870
|
+
}
|
|
1871
|
+
if (this._restoreNodePolygonOffset(node)) changed = true;
|
|
1872
|
+
} else if (nextFallback.has(url)) {
|
|
1873
|
+
if (!node.group.visible) {
|
|
1874
|
+
node.group.visible = true;
|
|
1875
|
+
changed = true;
|
|
1876
|
+
}
|
|
1877
|
+
const depth = nodeDepths.get(url) ?? 0;
|
|
1878
|
+
const targetOrder = depth;
|
|
1879
|
+
if (node.group.renderOrder !== targetOrder) {
|
|
1880
|
+
node.group.renderOrder = targetOrder;
|
|
1881
|
+
changed = true;
|
|
1882
|
+
}
|
|
1883
|
+
if (this._applyNodeFallbackPolygonOffset(node, depth)) changed = true;
|
|
1884
|
+
} else {
|
|
1885
|
+
if (node.group.visible) {
|
|
1886
|
+
node.group.visible = false;
|
|
1887
|
+
changed = true;
|
|
1888
|
+
}
|
|
1889
|
+
if (this._restoreNodePolygonOffset(node)) changed = true;
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
this.primaryNodes.clear();
|
|
1894
|
+
for (const u of nextPrimary) this.primaryNodes.add(u);
|
|
1895
|
+
this.fallbackNodes.clear();
|
|
1896
|
+
for (const u of nextFallback) this.fallbackNodes.add(u);
|
|
1897
|
+
|
|
1898
|
+
return changed;
|
|
1899
|
+
}
|
|
1677
1900
|
dispose(): void {
|
|
1678
1901
|
this._workerClient?.dispose();
|
|
1679
1902
|
for (const node of this.nodes.values()) {
|