s3m-three 0.4.26 → 0.4.28
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-B2u-xt54.js +1 -0
- 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/S3M1Parser.d.ts +19 -0
- package/dist/core/S3M1Parser.d.ts.map +1 -1
- package/dist/core/S3M1Parser.js +179 -8
- package/dist/core/S3M1Parser.js.map +1 -1
- 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 +80 -2
- package/dist/render/S3MViewer.d.ts.map +1 -1
- package/dist/render/S3MViewer.js +299 -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 +217 -2
- package/dist/tileset/S3MTileset.js.map +1 -1
- package/dist/worker/s3mWorker.js +691 -508
- 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/S3M1Parser.ts +179 -9
- 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 +335 -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 +240 -3
- package/dist/browser/assets/s3mWorker-DHOVIrAU.js +0 -1
package/src/render/S3MViewer.ts
CHANGED
|
@@ -41,6 +41,7 @@ import type {
|
|
|
41
41
|
PickResult,
|
|
42
42
|
PickableRef,
|
|
43
43
|
PathFollowOptions,
|
|
44
|
+
DepthMode,
|
|
44
45
|
MeshUserData,
|
|
45
46
|
} from './viewer/types.js';
|
|
46
47
|
import { decodeTextureWithWorker } from '../worker/S3MWorkerClient.js';
|
|
@@ -114,6 +115,7 @@ import type {
|
|
|
114
115
|
} from '../core/types.js';
|
|
115
116
|
|
|
116
117
|
import { lngLatFromWorld, worldFromLngLat, type GeoAdapter } from './GeoAdapter.js';
|
|
118
|
+
import { computeTilePlacement } from './tilePlacement.js';
|
|
117
119
|
|
|
118
120
|
import {
|
|
119
121
|
DatasetManager,
|
|
@@ -176,9 +178,17 @@ export type {
|
|
|
176
178
|
S3MViewerOptions,
|
|
177
179
|
PickableRef,
|
|
178
180
|
PathFollowOptions,
|
|
181
|
+
DepthMode,
|
|
179
182
|
} from './viewer/types.js';
|
|
180
183
|
export type { MotionBlurOptions, MotionBlurConfig } from './MotionBlurPass.js';
|
|
181
184
|
|
|
185
|
+
/** 稳健归一化 depthMode 入参,默认 'scene' */
|
|
186
|
+
export function normalizeDepthMode(mode?: unknown): DepthMode {
|
|
187
|
+
if (mode === 'clear') return 'clear';
|
|
188
|
+
if (mode === 'auto') return 'auto';
|
|
189
|
+
return 'scene';
|
|
190
|
+
}
|
|
191
|
+
|
|
182
192
|
function styledMaterialOf(mesh: THREE.Mesh): StyledMaterial | null {
|
|
183
193
|
const m = Array.isArray(mesh.material) ? mesh.material[0] : mesh.material;
|
|
184
194
|
const styled = m as StyledMaterial | undefined;
|
|
@@ -205,6 +215,12 @@ function clampOpacity(v: number | null | undefined): number {
|
|
|
205
215
|
/** 低于这个透明度就当作完全不可见:跳过绘制,避免全透明物体写深度 */
|
|
206
216
|
const OPACITY_EPS = 1e-3;
|
|
207
217
|
|
|
218
|
+
/**
|
|
219
|
+
* 拾取反解瓦片级放置时的临时矩阵。命中点→规范 ENU 是一次独立查询(鼠标事件),
|
|
220
|
+
* 复用一个实例避免每次分配;不可重入(`_toCanonicalScene` 内部不递归调用)。
|
|
221
|
+
*/
|
|
222
|
+
const _tilePlacementInverse = new THREE.Matrix4();
|
|
223
|
+
|
|
208
224
|
/* ------------------------------------------------------------------ 可动画的样式键 */
|
|
209
225
|
|
|
210
226
|
/**
|
|
@@ -297,6 +313,8 @@ export class S3MViewer {
|
|
|
297
313
|
* 传了值就只认自己,多图层同页各用各的,互不覆盖。
|
|
298
314
|
*/
|
|
299
315
|
private _materialQuality: MaterialQuality | undefined;
|
|
316
|
+
private _lodTransition: 'binary' | 'incremental';
|
|
317
|
+
private _fallbackOffset?: { factor?: number; units?: number };
|
|
300
318
|
/**
|
|
301
319
|
* 本图层的颜色管线。
|
|
302
320
|
*
|
|
@@ -322,11 +340,32 @@ export class S3MViewer {
|
|
|
322
340
|
}
|
|
323
341
|
|
|
324
342
|
/** 场景主锚点(ENU 原点对应的世界坐标) */
|
|
325
|
-
get sceneAnchor(): {
|
|
343
|
+
get sceneAnchor(): {
|
|
344
|
+
x: number;
|
|
345
|
+
y: number;
|
|
346
|
+
z: number;
|
|
347
|
+
meterInMercatorEast?: number;
|
|
348
|
+
meterInMercatorNorth?: number;
|
|
349
|
+
meterInMercatorUp?: number;
|
|
350
|
+
meterInMercator: number;
|
|
351
|
+
altitude?: number;
|
|
352
|
+
} | null {
|
|
326
353
|
return this._datasetManager.sceneAnchor;
|
|
327
354
|
}
|
|
328
|
-
set sceneAnchor(
|
|
355
|
+
set sceneAnchor(
|
|
356
|
+
val: {
|
|
357
|
+
x: number;
|
|
358
|
+
y: number;
|
|
359
|
+
z: number;
|
|
360
|
+
meterInMercatorEast?: number;
|
|
361
|
+
meterInMercatorNorth?: number;
|
|
362
|
+
meterInMercatorUp?: number;
|
|
363
|
+
meterInMercator: number;
|
|
364
|
+
altitude?: number;
|
|
365
|
+
} | null
|
|
366
|
+
) {
|
|
329
367
|
this._datasetManager.sceneAnchor = val;
|
|
368
|
+
this._reapplyTilePlacements();
|
|
330
369
|
}
|
|
331
370
|
|
|
332
371
|
/** 拾取表 / pickId 池 / 射线求交(见 viewer/PickService.ts) */
|
|
@@ -402,6 +441,13 @@ export class S3MViewer {
|
|
|
402
441
|
*/
|
|
403
442
|
private _motionBlurCfg: MotionBlurConfig | null = null;
|
|
404
443
|
private _motionBlur: MotionBlurPass | null = null;
|
|
444
|
+
/**
|
|
445
|
+
* 瓦片级放置(默认关)。关时 `_registerTile` 完全不碰瓦片矩阵,代码路径与改动前
|
|
446
|
+
* 逐字一致;开时每块瓦片用自己的中心纬度算尺度,见 `tilePlacement.ts`。
|
|
447
|
+
*/
|
|
448
|
+
private readonly _perTilePlacement: boolean;
|
|
449
|
+
/** 本层绘制前深度缓冲的处理策略:'scene' | 'clear' | 'auto',默认 'scene' */
|
|
450
|
+
private _depthMode: DepthMode = 'scene';
|
|
405
451
|
|
|
406
452
|
/** 宿主请求重画(MapLibre 传 triggerRepaint,纯 three 宿主传自己的渲染循环) */
|
|
407
453
|
private _requestRepaint(): void {
|
|
@@ -418,6 +464,10 @@ export class S3MViewer {
|
|
|
418
464
|
this._lodRangeScale = options.lodRangeScale ?? 1.0;
|
|
419
465
|
this._unsupported = options.unsupported ?? 'warn';
|
|
420
466
|
this._lodPixelRatioCap = options.lodPixelRatioCap ?? Infinity;
|
|
467
|
+
// 瓦片级放置默认开启:旧行为是拿单一常量切平面拟合随纬度变化的墨卡托比例,属于 bug 修复,
|
|
468
|
+
// 所以默认值跟着改(见 AGENTS.md 不变式 ①)。显式传 false 仍可回退。
|
|
469
|
+
this._perTilePlacement = options.perTilePlacement !== false;
|
|
470
|
+
this._depthMode = normalizeDepthMode(options.depthMode);
|
|
421
471
|
setTextureSizeCap(options.maxTextureSize ?? Infinity);
|
|
422
472
|
// 运动模糊:默认关闭;开了也只在 renderFrame 里按需接管「场景→屏幕」这一步
|
|
423
473
|
this._motionBlurCfg = normalizeMotionBlur(options.motionBlur);
|
|
@@ -426,6 +476,11 @@ export class S3MViewer {
|
|
|
426
476
|
this._materialQuality = normalizeMaterialQuality(options.materialQuality);
|
|
427
477
|
// 之后 addDataset 的数据集按这个默认建材质(数据集自己指定则用自己的)
|
|
428
478
|
this._datasetManager.defaultMaterialQuality = this._materialQuality;
|
|
479
|
+
this._lodTransition = options.lodTransition ?? 'binary';
|
|
480
|
+
this._fallbackOffset = options.fallbackOffset;
|
|
481
|
+
this._datasetManager.defaultLodTransition = this._lodTransition;
|
|
482
|
+
this._datasetManager.defaultFallbackOffset = this._fallbackOffset;
|
|
483
|
+
this._datasetManager.defaultBakeMercator = options.bakeMercator;
|
|
429
484
|
if (options.totalMaxTiles) {
|
|
430
485
|
this._datasetManager.totalMaxTiles = options.totalMaxTiles;
|
|
431
486
|
}
|
|
@@ -507,6 +562,7 @@ export class S3MViewer {
|
|
|
507
562
|
return self._smidMeshes;
|
|
508
563
|
},
|
|
509
564
|
lngLatFromWorld: (x, y) => self._lngLatFromWorld(x, y),
|
|
565
|
+
toCanonicalScene: (p, mesh) => self._toCanonicalScene(p, mesh),
|
|
510
566
|
gpuPathPoseAt: (smid) => self._gpuPathPoseAt(smid),
|
|
511
567
|
isFeatureHidden: (smid, batchId) => self._isFeatureHidden(smid, batchId),
|
|
512
568
|
});
|
|
@@ -762,7 +818,15 @@ export class S3MViewer {
|
|
|
762
818
|
.filter((d) => !!d?.configUrl)
|
|
763
819
|
.map((d) => ({ ...d, polygonOffset: d.polygonOffset ?? options.polygonOffset }));
|
|
764
820
|
if (options.configUrl)
|
|
765
|
-
return [{
|
|
821
|
+
return [{
|
|
822
|
+
id: 'default',
|
|
823
|
+
name: '默认数据集',
|
|
824
|
+
configUrl: options.configUrl,
|
|
825
|
+
useWorker: options.useWorker,
|
|
826
|
+
polygonOffset: options.polygonOffset,
|
|
827
|
+
lodTransition: options.lodTransition,
|
|
828
|
+
fallbackOffset: options.fallbackOffset,
|
|
829
|
+
}];
|
|
766
830
|
return [];
|
|
767
831
|
}
|
|
768
832
|
|
|
@@ -770,7 +834,12 @@ export class S3MViewer {
|
|
|
770
834
|
async addDataset(ds: S3MDatasetOptions): Promise<S3MDatasetEntry> {
|
|
771
835
|
if (!this.scene) throw new Error('[s3m] 图层尚未 addLayer,不能添加数据集');
|
|
772
836
|
// 图层级默认值:数据集自己指定了就用自己的,见 `S3MViewerOptions.unsupported`
|
|
773
|
-
const merged: S3MDatasetOptions = {
|
|
837
|
+
const merged: S3MDatasetOptions = {
|
|
838
|
+
...ds,
|
|
839
|
+
unsupported: ds.unsupported ?? this._unsupported,
|
|
840
|
+
lodTransition: ds.lodTransition ?? this._lodTransition,
|
|
841
|
+
fallbackOffset: ds.fallbackOffset ?? this._fallbackOffset,
|
|
842
|
+
};
|
|
774
843
|
const entry = await this._datasetManager.addDataset(
|
|
775
844
|
merged,
|
|
776
845
|
this.scene,
|
|
@@ -828,6 +897,8 @@ export class S3MViewer {
|
|
|
828
897
|
/** 当地图 CRS 投影切换或宿主 map 挂载后,重新计算场景锚点与各数据集 offset */
|
|
829
898
|
recomputeAnchors(): void {
|
|
830
899
|
this._datasetManager.recomputeAnchors(this.geo);
|
|
900
|
+
// 锚点变了 → 每块瓦片的 k 与 position 都要重算(关这项功能时是空转,一次遍历都不做)
|
|
901
|
+
this._reapplyTilePlacements();
|
|
831
902
|
this._heights.refreshAll();
|
|
832
903
|
this._requestRepaint();
|
|
833
904
|
}
|
|
@@ -1665,6 +1736,32 @@ export class S3MViewer {
|
|
|
1665
1736
|
/* ---------------------------------------------------------------- 瓦片注册 */
|
|
1666
1737
|
|
|
1667
1738
|
private _registerTile(node: TileNode, entry: S3MDatasetEntry): void {
|
|
1739
|
+
// 必须在 add / updateMatrixWorld 之前设置:后者会拿当前 matrix 算 matrixWorld 并缓存
|
|
1740
|
+
//
|
|
1741
|
+
// ★ 触发条件**不能**是「有没有包围球」。S3M 1.x 的叶子瓦片 XML 是
|
|
1742
|
+
// `<PageLods><Geode>`(外面没有 `<PagedLOD>` 包装),`parseXmlLods` 解析出来
|
|
1743
|
+
// pageLods 为空 → sphere 为 null。而 `entry.root` 上挂着 align 算出来的整体偏移
|
|
1744
|
+
// (索菲亚教堂是 heightRange.min = 119.84),那个偏移是靠 placement 里的
|
|
1745
|
+
// `- datasetOffset` 抵消的。少摆一块,这块就带着偏移显示 —— 实测 L23 整层
|
|
1746
|
+
// 因此沉到地下 120 m,且 `localZ - worldZ` 恒等于偏移量。
|
|
1747
|
+
// 缺球时用几何包围盒中心兜底;连几何都没有就退回旧口径。
|
|
1748
|
+
const center = node.sphere?.center ?? this._localCenterOf(node.group);
|
|
1749
|
+
if (this._perTilePlacement && center) {
|
|
1750
|
+
this._applyTilePlacement(node.group, center, entry);
|
|
1751
|
+
}
|
|
1752
|
+
// 上面没摆成(关掉本功能 / 缺锚点 / 几何为空)时 matrixAutoUpdate 仍是 true,走旧口径
|
|
1753
|
+
if (node.group.matrixAutoUpdate) {
|
|
1754
|
+
const sceneAnchor = this.sceneAnchor;
|
|
1755
|
+
if (sceneAnchor) {
|
|
1756
|
+
const sEast = sceneAnchor.meterInMercatorEast ?? sceneAnchor.meterInMercator;
|
|
1757
|
+
const sNorth = sceneAnchor.meterInMercatorNorth ?? sceneAnchor.meterInMercator;
|
|
1758
|
+
const sUp = sceneAnchor.meterInMercatorUp ?? sEast;
|
|
1759
|
+
const c = node.sphere
|
|
1760
|
+
? ([node.sphere.center.x, node.sphere.center.y, node.sphere.center.z] as const)
|
|
1761
|
+
: ([0, 0, 0] as const);
|
|
1762
|
+
this._updateTileUniforms(node.group, c, [1.0 / sEast, 1.0 / sNorth, 1.0 / sUp]);
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1668
1765
|
entry.root.add(node.group);
|
|
1669
1766
|
node.group.updateMatrixWorld(true);
|
|
1670
1767
|
|
|
@@ -1736,6 +1833,152 @@ export class S3MViewer {
|
|
|
1736
1833
|
}
|
|
1737
1834
|
}
|
|
1738
1835
|
|
|
1836
|
+
/**
|
|
1837
|
+
* 瓦片几何在**数据集局部 ENU** 里的包围盒中心,给没有包围球的瓦片兜底。
|
|
1838
|
+
*
|
|
1839
|
+
* 调用时机固定在 `entry.root.add()` 之前:此时 group 还没挂到 root 上、matrix 还是
|
|
1840
|
+
* 单位阵,`setFromObject` 得到的就是顶点自己的坐标系,正好是摆放要的枢轴。
|
|
1841
|
+
* 挂上 root 之后再算会带上 root 的 offset,枢轴就偏了。
|
|
1842
|
+
*/
|
|
1843
|
+
private _localCenterOf(group: THREE.Object3D): { x: number; y: number; z: number } | null {
|
|
1844
|
+
if (!group.children.length) return null;
|
|
1845
|
+
const box = new THREE.Box3().setFromObject(group);
|
|
1846
|
+
if (box.isEmpty()) return null;
|
|
1847
|
+
const c = box.getCenter(new THREE.Vector3());
|
|
1848
|
+
if (!Number.isFinite(c.x) || !Number.isFinite(c.y) || !Number.isFinite(c.z)) return null;
|
|
1849
|
+
return { x: c.x, y: c.y, z: c.z };
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
/**
|
|
1853
|
+
* 把一块瓦片摆到「用它自己中心纬度算出的尺度」上(`perTilePlacement` 开启时才走)。
|
|
1854
|
+
*
|
|
1855
|
+
* 矩阵是 `T(position) · S(k) · T(-pivot)`,展开后即 `v ↦ position + k·(v - pivot)`。
|
|
1856
|
+
* 直接用 `makeScale + setPosition` 一次写好,省掉两个临时矩阵:
|
|
1857
|
+
* `T(t)·S(k)·T(-c)` 的元素其实就是对角 `k` 加平移 `t - k·c`。
|
|
1858
|
+
*
|
|
1859
|
+
* 只写 `matrix`,不碰 `rotation / position / scale` —— three 在
|
|
1860
|
+
* `matrixAutoUpdate = false` 时直接使用 `matrix`,那些字段留着无害,
|
|
1861
|
+
* 但别让 `updateMatrix()` 有机会覆盖回来。
|
|
1862
|
+
*/
|
|
1863
|
+
private _applyTilePlacement(
|
|
1864
|
+
group: THREE.Object3D,
|
|
1865
|
+
center: { x: number; y: number; z: number },
|
|
1866
|
+
entry: S3MDatasetEntry,
|
|
1867
|
+
): void {
|
|
1868
|
+
const sceneAnchor = this.sceneAnchor;
|
|
1869
|
+
if (!sceneAnchor || !entry.anchor) return;
|
|
1870
|
+
|
|
1871
|
+
const p = computeTilePlacement({
|
|
1872
|
+
center,
|
|
1873
|
+
datasetAnchor: entry.anchor,
|
|
1874
|
+
sceneAnchor,
|
|
1875
|
+
datasetOffset: entry.offset,
|
|
1876
|
+
geo: this.geo,
|
|
1877
|
+
});
|
|
1878
|
+
if (!p) return;
|
|
1879
|
+
|
|
1880
|
+
const kEast = p.scaleEast ?? p.scale;
|
|
1881
|
+
const kNorth = p.scaleNorth ?? p.scale;
|
|
1882
|
+
const kUp = kEast;
|
|
1883
|
+
group.matrixAutoUpdate = false;
|
|
1884
|
+
group.matrix.makeScale(kEast, kNorth, kUp);
|
|
1885
|
+
group.matrix.setPosition(
|
|
1886
|
+
p.position[0] - kEast * p.pivot[0],
|
|
1887
|
+
p.position[1] - kNorth * p.pivot[1],
|
|
1888
|
+
p.position[2] - kUp * p.pivot[2],
|
|
1889
|
+
);
|
|
1890
|
+
group.matrixWorldNeedsUpdate = true;
|
|
1891
|
+
// 重锚点后要能重算:中心只属于注册那一刻,存到 group 上,不依赖 tileset 内部结构
|
|
1892
|
+
(group.userData as { tileCenter?: { x: number; y: number; z: number } }).tileCenter = {
|
|
1893
|
+
x: center.x,
|
|
1894
|
+
y: center.y,
|
|
1895
|
+
z: center.z,
|
|
1896
|
+
};
|
|
1897
|
+
// 逆矩阵缓存:_toCanonicalScene 常态路径无需每次拾取反复求逆,失效点仅在瓦片放置或重锚点
|
|
1898
|
+
const inv = ((group.userData as { placementInverse?: THREE.Matrix4 }).placementInverse ??= new THREE.Matrix4());
|
|
1899
|
+
inv.copy(group.matrix).invert();
|
|
1900
|
+
|
|
1901
|
+
const sSceneEast = sceneAnchor.meterInMercatorEast ?? sceneAnchor.meterInMercator;
|
|
1902
|
+
const sSceneNorth = sceneAnchor.meterInMercatorNorth ?? sceneAnchor.meterInMercator;
|
|
1903
|
+
const sSceneUp = sceneAnchor.meterInMercatorUp ?? sSceneEast;
|
|
1904
|
+
const invScale = p.invTileScale ?? [1.0 / (sSceneEast * kEast), 1.0 / (sSceneNorth * kNorth), 1.0 / (sSceneUp * kUp)];
|
|
1905
|
+
this._updateTileUniforms(group, p.pivot, invScale);
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
private _updateTileUniforms(
|
|
1909
|
+
group: THREE.Object3D,
|
|
1910
|
+
pivot: readonly [number, number, number],
|
|
1911
|
+
invTileScale: readonly [number, number, number]
|
|
1912
|
+
): void {
|
|
1913
|
+
group.traverse((obj) => {
|
|
1914
|
+
const mesh = obj as THREE.Mesh;
|
|
1915
|
+
if (((obj as any).isMesh || (obj as any).isPoints) && (obj as any).material) {
|
|
1916
|
+
const mats = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
|
|
1917
|
+
for (const m of mats) {
|
|
1918
|
+
const uPivot = (m as any).userData?.uTilePivot;
|
|
1919
|
+
const uScale = (m as any).userData?.uInvTileScale;
|
|
1920
|
+
if (uPivot?.value) {
|
|
1921
|
+
uPivot.value.set(pivot[0], pivot[1], pivot[2]);
|
|
1922
|
+
}
|
|
1923
|
+
if (uScale?.value) {
|
|
1924
|
+
uScale.value.set(invTileScale[0], invTileScale[1], invTileScale[2]);
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
});
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
/**
|
|
1932
|
+
* 世界坐标 → **规范**场景 ENU:把瓦片级放置的缩放反解掉。
|
|
1933
|
+
*
|
|
1934
|
+
* `perTilePlacement` 开启后,瓦片几何是「绕自身中心缩放 k 倍、再摆到精确位置」的,
|
|
1935
|
+
* 所以命中点的世界坐标不再等于数据本身的 ENU。拾取要报的是**坐标本身**——
|
|
1936
|
+
* 一个屋顶的经纬度不该因为我们怎么缩放它而改变,否则同一个点在 20 km 外会比
|
|
1937
|
+
* 1 km 内偏出几米,量算、贴地、外部标注全都跟着漂。
|
|
1938
|
+
*
|
|
1939
|
+
* 反解只需要一次矩阵求逆:父级 `entry.root` 只有平移(`layoutDataset` 只写
|
|
1940
|
+
* `position`),所以世界坐标 `p` 对应的瓦片局部坐标是
|
|
1941
|
+
*
|
|
1942
|
+
* ```
|
|
1943
|
+
* v = group.matrix⁻¹ · (p − root.position)
|
|
1944
|
+
* canonical = root.position + v
|
|
1945
|
+
* ```
|
|
1946
|
+
*
|
|
1947
|
+
* 关掉本功能、或网格不在已放置的瓦片里时**原样返回同一个对象**,
|
|
1948
|
+
* 默认路径一次矩阵运算都不做。
|
|
1949
|
+
*/
|
|
1950
|
+
private _toCanonicalScene(p: THREE.Vector3, mesh?: THREE.Object3D | null): THREE.Vector3 {
|
|
1951
|
+
if (!this._perTilePlacement || !mesh) return p;
|
|
1952
|
+
// 网格直接挂在瓦片 group 下(`_registerTile` 就是遍历 node.group.children);
|
|
1953
|
+
// 往上找到第一个带 tileCenter 的祖先,避免依赖层级深度
|
|
1954
|
+
let group: THREE.Object3D | null = mesh.parent;
|
|
1955
|
+
while (group && !(group.userData as { tileCenter?: unknown }).tileCenter) group = group.parent;
|
|
1956
|
+
if (!group) return p;
|
|
1957
|
+
const root = group.parent;
|
|
1958
|
+
if (!root) return p;
|
|
1959
|
+
|
|
1960
|
+
return p
|
|
1961
|
+
.clone()
|
|
1962
|
+
.sub(root.position)
|
|
1963
|
+
.applyMatrix4((group.userData as { placementInverse?: THREE.Matrix4 }).placementInverse ?? _tilePlacementInverse.copy(group.matrix).invert())
|
|
1964
|
+
.add(root.position);
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
/**
|
|
1968
|
+
* 场景锚点变了(`recomputeAnchors`)之后,所有已注册瓦片的放置都要重算 ——
|
|
1969
|
+
* `k` 与 `position` 都依赖场景锚点。没存 `tileCenter` 的(未开启本功能时注册的)
|
|
1970
|
+
* 一律跳过,保证默认路径不产生任何副作用。
|
|
1971
|
+
*/
|
|
1972
|
+
private _reapplyTilePlacements(): void {
|
|
1973
|
+
for (const entry of this._datasetManager.values()) {
|
|
1974
|
+
if (!entry.anchor) continue;
|
|
1975
|
+
for (const child of entry.root.children) {
|
|
1976
|
+
const c = (child.userData as { tileCenter?: { x: number; y: number; z: number } }).tileCenter;
|
|
1977
|
+
if (c) this._applyTilePlacement(child, c, entry);
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1739
1982
|
/**
|
|
1740
1983
|
* 新瓦片淡入。
|
|
1741
1984
|
*
|
|
@@ -2200,6 +2443,17 @@ export class S3MViewer {
|
|
|
2200
2443
|
}
|
|
2201
2444
|
|
|
2202
2445
|
/** 当前材质质感档位(本图层指定了就用本图层的,否则是模块级默认) */
|
|
2446
|
+
get lodTransition(): 'binary' | 'incremental' {
|
|
2447
|
+
return this._lodTransition;
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
setLodTransition(mode: 'binary' | 'incremental'): void {
|
|
2451
|
+
if (this._lodTransition === mode) return;
|
|
2452
|
+
this._lodTransition = mode;
|
|
2453
|
+
this._datasetManager.setDefaultLodTransition(mode);
|
|
2454
|
+
this.triggerRepaint();
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2203
2457
|
get materialQuality(): MaterialQuality {
|
|
2204
2458
|
return this._materialQuality ?? getMaterialQuality();
|
|
2205
2459
|
}
|
|
@@ -2222,6 +2476,54 @@ export class S3MViewer {
|
|
|
2222
2476
|
}
|
|
2223
2477
|
|
|
2224
2478
|
/** 当前运动模糊配置(归一化后的副本);null = 关闭 */
|
|
2479
|
+
/**
|
|
2480
|
+
* 运行时修改深度处理策略('scene' | 'clear' | 'auto')。
|
|
2481
|
+
*/
|
|
2482
|
+
setDepthMode(mode: DepthMode): void {
|
|
2483
|
+
const next = normalizeDepthMode(mode);
|
|
2484
|
+
if (this._depthMode !== next) {
|
|
2485
|
+
this._depthMode = next;
|
|
2486
|
+
this.triggerRepaint();
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2489
|
+
|
|
2490
|
+
/** 当前深度策略('scene' | 'clear' | 'auto',默认 'scene') */
|
|
2491
|
+
get depthMode(): DepthMode {
|
|
2492
|
+
return this._depthMode;
|
|
2493
|
+
}
|
|
2494
|
+
|
|
2495
|
+
/**
|
|
2496
|
+
* 当前帧实际生效的深度处理模式('scene' | 'clear')。
|
|
2497
|
+
* - 'clear': 显式指定 clear,或 auto 模式下命中地面覆盖/倾斜摄影/DEM等地面类数据
|
|
2498
|
+
* - 'scene': 显式指定 scene,或 auto 模式下无地面类数据
|
|
2499
|
+
*/
|
|
2500
|
+
resolveDepthMode(): 'scene' | 'clear' {
|
|
2501
|
+
if (this._depthMode === 'clear') return 'clear';
|
|
2502
|
+
if (this._depthMode === 'scene') return 'scene';
|
|
2503
|
+
|
|
2504
|
+
// 'auto': 按数据集与地面覆盖特征推断
|
|
2505
|
+
if (this.groundManager && this.groundManager.mode !== 'none') {
|
|
2506
|
+
return 'clear';
|
|
2507
|
+
}
|
|
2508
|
+
|
|
2509
|
+
for (const entry of this._datasetManager.datasets.values()) {
|
|
2510
|
+
if (!entry.visible) continue;
|
|
2511
|
+
if (entry.options.depthMode === 'clear') return 'clear';
|
|
2512
|
+
if (entry.options.depthMode === 'scene') continue;
|
|
2513
|
+
if (entry.tileset?.isS3MV1) return 'clear';
|
|
2514
|
+
const cfg = entry.tileset?.config as Record<string, unknown> | null | undefined;
|
|
2515
|
+
if (cfg) {
|
|
2516
|
+
const typeStr = `${cfg.fileType ?? ''} ${cfg.dataType ?? ''} ${cfg.datasetType ?? ''} ${cfg.cacheType ?? ''} ${cfg.fileExtentName ?? ''}`;
|
|
2517
|
+
if (/osgb|oblique|ground|dem|tin|terrain/i.test(typeStr)) return 'clear';
|
|
2518
|
+
}
|
|
2519
|
+
if (entry.options.configUrl && /osgb|oblique/i.test(entry.options.configUrl)) {
|
|
2520
|
+
return 'clear';
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
|
|
2524
|
+
return 'scene';
|
|
2525
|
+
}
|
|
2526
|
+
|
|
2225
2527
|
get motionBlur(): MotionBlurOptions | null {
|
|
2226
2528
|
return this._motionBlurCfg ? { ...this._motionBlurCfg } : null;
|
|
2227
2529
|
}
|
|
@@ -2530,10 +2832,11 @@ export class S3MViewer {
|
|
|
2530
2832
|
private _sceneFromLngLat(p: LngLatAlt): Vec3 {
|
|
2531
2833
|
const anchor = this.sceneAnchor;
|
|
2532
2834
|
if (!anchor) return [0, 0, 0];
|
|
2533
|
-
const
|
|
2835
|
+
const sEast = anchor.meterInMercatorEast ?? anchor.meterInMercator;
|
|
2836
|
+
const sNorth = anchor.meterInMercatorNorth ?? anchor.meterInMercator;
|
|
2534
2837
|
const w = worldFromLngLat(this.geo, p.lng, p.lat, p.altitude);
|
|
2535
2838
|
// 注意 y 取反:three 的 y 向上,墨卡托 y 向下(与 PickService.pickPosition 互逆)
|
|
2536
|
-
return [(w.x - anchor.x) /
|
|
2839
|
+
return [(w.x - anchor.x) / sEast, -(w.y - anchor.y) / sNorth, p.altitude - (anchor.altitude ?? 0)];
|
|
2537
2840
|
}
|
|
2538
2841
|
|
|
2539
2842
|
/**
|
|
@@ -2556,8 +2859,9 @@ export class S3MViewer {
|
|
|
2556
2859
|
private _lngLatFromScene(v: Vec3): LngLatAlt {
|
|
2557
2860
|
const anchor = this.sceneAnchor;
|
|
2558
2861
|
if (!anchor) return { lng: 0, lat: 0, altitude: v[2] };
|
|
2559
|
-
const
|
|
2560
|
-
const
|
|
2862
|
+
const sEast = anchor.meterInMercatorEast ?? anchor.meterInMercator;
|
|
2863
|
+
const sNorth = anchor.meterInMercatorNorth ?? anchor.meterInMercator;
|
|
2864
|
+
const [lng, lat] = lngLatFromWorld(this.geo, v[0] * sEast + anchor.x, -v[1] * sNorth + anchor.y);
|
|
2561
2865
|
return { lng, lat, altitude: v[2] + (anchor.altitude ?? 0) };
|
|
2562
2866
|
}
|
|
2563
2867
|
|
|
@@ -3053,13 +3357,15 @@ export class S3MViewer {
|
|
|
3053
3357
|
this.flushOverrides();
|
|
3054
3358
|
|
|
3055
3359
|
const canvas = this.canvas;
|
|
3056
|
-
const
|
|
3360
|
+
const sEast = this.sceneAnchor.meterInMercatorEast ?? this.sceneAnchor.meterInMercator;
|
|
3361
|
+
const sNorth = this.sceneAnchor.meterInMercatorNorth ?? this.sceneAnchor.meterInMercator;
|
|
3362
|
+
const sUp = this.sceneAnchor.meterInMercatorUp ?? sEast;
|
|
3057
3363
|
|
|
3058
3364
|
// ENU → 世界坐标:x 向东,y 向北(墨卡托 y 朝南,所以取负),z 向上
|
|
3059
3365
|
this._enuMatrix.set(
|
|
3060
|
-
|
|
3061
|
-
0, -
|
|
3062
|
-
0, 0,
|
|
3366
|
+
sEast, 0, 0, this.sceneAnchor.x,
|
|
3367
|
+
0, -sNorth, 0, this.sceneAnchor.y,
|
|
3368
|
+
0, 0, sUp, this.sceneAnchor.z,
|
|
3063
3369
|
0, 0, 0, 1
|
|
3064
3370
|
);
|
|
3065
3371
|
|
|
@@ -3117,6 +3423,19 @@ export class S3MViewer {
|
|
|
3117
3423
|
setSplatViewport(canvas.width, canvas.height);
|
|
3118
3424
|
|
|
3119
3425
|
this.renderer.setViewport(0, 0, canvas.width, canvas.height);
|
|
3426
|
+
const effectiveDepthMode = (this.uniforms.uOpacity?.value ?? 1) > OPACITY_EPS
|
|
3427
|
+
? this.resolveDepthMode()
|
|
3428
|
+
: 'scene';
|
|
3429
|
+
if (effectiveDepthMode === 'clear' && gl) {
|
|
3430
|
+
// gl.clear 受 depthMask 约束。上一帧若以 depthWrite=false 的材质收尾(棱线、半透明
|
|
3431
|
+
// 地表覆盖、半透明点云、泼溅),GL 的 depthMask 会停在 false —— three 帧末不恢复它,
|
|
3432
|
+
// 而 resetState 排在 clear 之后,救不了这一步。不自己打开就是一次静默的空操作:
|
|
3433
|
+
// 画面上的 z-fighting 原样保留,且没有任何报错可查。
|
|
3434
|
+
gl.depthMask(true);
|
|
3435
|
+
// 只清深度,不清颜色 —— 颜色是 MapLibre 已经画好的底图
|
|
3436
|
+
gl.clear(gl.DEPTH_BUFFER_BIT);
|
|
3437
|
+
}
|
|
3438
|
+
// 清完之后必须让 three 知道状态被外部改过,否则它的状态缓存会认为自己仍然正确
|
|
3120
3439
|
this.renderer.resetState();
|
|
3121
3440
|
// 完全透明时什么都不画:透明物体照样写深度,画了会在底图上挖出一个洞。
|
|
3122
3441
|
// 这里跳过的是「本层的绘制」,宿主自己的清屏 / 底图照旧。
|
|
@@ -3129,7 +3448,10 @@ export class S3MViewer {
|
|
|
3129
3448
|
}
|
|
3130
3449
|
// GPU 描边必须画在主场景之后:它的全屏 quad 参与宿主深度测试,
|
|
3131
3450
|
// 依赖本帧已经写好的深度(被地形挡住的地方不该描边)
|
|
3132
|
-
|
|
3451
|
+
// clear 模式下宿主深度已清除,描边会穿透地形产生伪影,故只在非 clear 时画。
|
|
3452
|
+
if (effectiveDepthMode !== 'clear') {
|
|
3453
|
+
this._edges.renderGpuEdges(canvas);
|
|
3454
|
+
}
|
|
3133
3455
|
}
|
|
3134
3456
|
|
|
3135
3457
|
this._frame++;
|
|
@@ -446,6 +446,11 @@ export function createSharedUniforms(): StyleUniforms {
|
|
|
446
446
|
};
|
|
447
447
|
}
|
|
448
448
|
const VERT_PARS = /* glsl */ `
|
|
449
|
+
#ifdef S3M_BAKED_MERCATOR
|
|
450
|
+
attribute vec3 aMercatorOffset;
|
|
451
|
+
uniform vec3 uTilePivot;
|
|
452
|
+
uniform vec3 uInvTileScale;
|
|
453
|
+
#endif
|
|
449
454
|
attribute float aBatchId;
|
|
450
455
|
// vec4:alpha 是「材质不透明、顶点半透明」这类数据的唯一透明度来源(官方 baseColorWithAlpha = vColor)
|
|
451
456
|
attribute vec4 aColor;
|
|
@@ -624,6 +629,13 @@ const VERT_SETUP = /* glsl */ `
|
|
|
624
629
|
`;
|
|
625
630
|
|
|
626
631
|
const VERT_ELEVATION = /* glsl */ `
|
|
632
|
+
#ifdef S3M_BAKED_MERCATOR
|
|
633
|
+
transformed = uTilePivot + vec3(
|
|
634
|
+
aMercatorOffset.x * uInvTileScale.x,
|
|
635
|
+
-aMercatorOffset.y * uInvTileScale.y,
|
|
636
|
+
aMercatorOffset.z * uInvTileScale.z
|
|
637
|
+
);
|
|
638
|
+
#endif
|
|
627
639
|
float s3mBatchElev = s3mBatchRaw();
|
|
628
640
|
if ( uBatchTexActive > 0 && s3mBatchElev >= 0.0 ) {
|
|
629
641
|
float s3mU = ( s3mBatchElev + 0.5 ) / uBatchTexWidth;
|
|
@@ -1142,6 +1154,11 @@ export interface StyledMaterialOptions {
|
|
|
1142
1154
|
* 与 `materialQuality` 同一套:图层把自己的 profile 透传进来,同页互不污染。
|
|
1143
1155
|
*/
|
|
1144
1156
|
colorPipeline?: ColorPipeline;
|
|
1157
|
+
bakeMercator?: boolean;
|
|
1158
|
+
/** 瓦片变换中心在 object space 的位置,供烘焙着色器恢复绝对位置 */
|
|
1159
|
+
tilePivot?: THREE.Vector3;
|
|
1160
|
+
/** 逐瓦片逆尺度 (1 / sTileEast, 1 / sTileNorth, 1 / sTileUp),供烘焙着色器抵消各向异性缩放 */
|
|
1161
|
+
invTileScale?: THREE.Vector3;
|
|
1145
1162
|
}
|
|
1146
1163
|
|
|
1147
1164
|
/**
|
|
@@ -1246,12 +1263,19 @@ export function createStyledMaterial(opts: StyledMaterialOptions): StyledMateria
|
|
|
1246
1263
|
// supermap 完整版:官方 sRGB 直乘管线(配合 setSrgbTextureDecode(false))
|
|
1247
1264
|
const direct = (opts.colorPipeline ?? colorPipeline) === 'direct';
|
|
1248
1265
|
if (direct) mat.defines = { ...mat.defines, S3M_DIRECT_COLOR: '' };
|
|
1266
|
+
const bakeMercator = !!opts.bakeMercator;
|
|
1267
|
+
if (bakeMercator) mat.defines = { ...(mat.defines as Record<string, string>), S3M_BAKED_MERCATOR: '' };
|
|
1249
1268
|
// 官方 uTextureDim = (base宽, base高, bake宽, bake高),取实际绑定纹理的像素尺寸
|
|
1250
1269
|
const uAtlasDim = { value: new THREE.Vector2(opts.map?.image?.width || 1, opts.map?.image?.height || 1) };
|
|
1251
1270
|
const uAtlasDim1 = { value: new THREE.Vector2(secMap?.image?.width || 1, secMap?.image?.height || 1) };
|
|
1252
1271
|
|
|
1272
|
+
const uTilePivot = { value: opts.tilePivot ? opts.tilePivot.clone() : new THREE.Vector3(0, 0, 0) };
|
|
1273
|
+
const uInvTileScale = { value: opts.invTileScale ? opts.invTileScale.clone() : new THREE.Vector3(1, 1, 1) };
|
|
1274
|
+
|
|
1253
1275
|
mat.onBeforeCompile = (shader) => {
|
|
1254
1276
|
Object.assign(shader.uniforms, uniforms, {
|
|
1277
|
+
uTilePivot,
|
|
1278
|
+
uInvTileScale,
|
|
1255
1279
|
uSelBatch,
|
|
1256
1280
|
uSelWhole,
|
|
1257
1281
|
uFlatShading,
|
|
@@ -1349,6 +1373,8 @@ export function createStyledMaterial(opts: StyledMaterialOptions): StyledMateria
|
|
|
1349
1373
|
mat.userData.uTileFade = uTileFade;
|
|
1350
1374
|
// 平面 / 平滑着色开关:S3MViewer._applyShading 翻它,不触发重编译
|
|
1351
1375
|
mat.userData.uFlatShading = uFlatShading;
|
|
1376
|
+
mat.userData.uTilePivot = uTilePivot;
|
|
1377
|
+
mat.userData.uInvTileScale = uInvTileScale;
|
|
1352
1378
|
// 记录创建参数:materialQuality 运行时切换时按原参数重建材质(换类不换外观)
|
|
1353
1379
|
mat.userData.s3mOpts = opts;
|
|
1354
1380
|
// 记录实际生效的档位:重建 / 排查时不用再猜当时读的是哪一层设置
|