s3m-three 0.4.25 → 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-jc9Oly5K.js → s3mWorker-BfSBNvF5.js} +1 -1
- package/dist/browser/s3m-three.iife.ext-three.js +119 -36
- package/dist/browser/s3m-three.iife.js +119 -36
- 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/CustomAttributes.d.ts +7 -0
- package/dist/core/CustomAttributes.d.ts.map +1 -0
- package/dist/core/CustomAttributes.js +48 -0
- package/dist/core/CustomAttributes.js.map +1 -0
- package/dist/core/Draco.d.ts.map +1 -1
- package/dist/core/Draco.js +149 -2
- package/dist/core/Draco.js.map +1 -1
- package/dist/core/MeshOpt.d.ts.map +1 -1
- package/dist/core/MeshOpt.js +2 -0
- package/dist/core/MeshOpt.js.map +1 -1
- package/dist/core/S3M1Parser.d.ts.map +1 -1
- package/dist/core/S3M1Parser.js +114 -27
- package/dist/core/S3M1Parser.js.map +1 -1
- package/dist/core/S3MBParser.d.ts +5 -2
- package/dist/core/S3MBParser.d.ts.map +1 -1
- package/dist/core/S3MBParser.js +27 -12
- package/dist/core/S3MBParser.js.map +1 -1
- package/dist/core/TileGeometry.d.ts +8 -0
- package/dist/core/TileGeometry.d.ts.map +1 -1
- package/dist/core/TileGeometry.js +45 -7
- package/dist/core/TileGeometry.js.map +1 -1
- package/dist/core/types.d.ts +1 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.d.ts +8 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- 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 +67 -4
- package/dist/render/GeometryBuilder.js.map +1 -1
- package/dist/render/MotionBlurPass.d.ts +130 -0
- package/dist/render/MotionBlurPass.d.ts.map +1 -0
- package/dist/render/MotionBlurPass.js +394 -0
- package/dist/render/MotionBlurPass.js.map +1 -0
- package/dist/render/S3MViewer.d.ts +99 -2
- package/dist/render/S3MViewer.d.ts.map +1 -1
- package/dist/render/S3MViewer.js +345 -13
- 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 +75 -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 +62 -1
- package/dist/tileset/S3MTileset.d.ts.map +1 -1
- package/dist/tileset/S3MTileset.js +273 -6
- package/dist/tileset/S3MTileset.js.map +1 -1
- package/dist/worker/S3MWorkerClient.d.ts +1 -1
- package/dist/worker/S3MWorkerClient.d.ts.map +1 -1
- package/dist/worker/S3MWorkerClient.js +2 -2
- package/dist/worker/S3MWorkerClient.js.map +1 -1
- package/dist/worker/s3mWorker.js +844 -635
- package/dist/worker/s3mWorker.js.map +1 -1
- package/docs/API.md +36 -5
- package/docs/FORMAT-NOTES.md +10 -0
- package/package.json +1 -1
- package/src/core/BakeMercator.ts +96 -0
- package/src/core/CustomAttributes.ts +53 -0
- package/src/core/Draco.ts +168 -6
- package/src/core/MeshOpt.ts +2 -0
- package/src/core/S3M1Parser.ts +102 -22
- package/src/core/S3MBParser.ts +33 -14
- package/src/core/TileGeometry.ts +60 -7
- package/src/core/types.ts +1 -0
- package/src/index.ts +15 -3
- package/src/render/DatasetManager.ts +43 -5
- package/src/render/GeoAdapter.ts +139 -11
- package/src/render/GeometryBuilder.ts +90 -4
- package/src/render/MotionBlurPass.ts +453 -0
- package/src/render/S3MViewer.ts +384 -17
- 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 +73 -0
- package/src/tileset/S3MTileset.ts +300 -6
- package/src/vendor/draco_decoder.module.d.ts +15 -1
- package/src/worker/S3MWorkerClient.ts +7 -2
- package/src/worker/s3mWorker.ts +6 -2
package/dist/render/S3MViewer.js
CHANGED
|
@@ -35,6 +35,7 @@ import { decodeTextureWithWorker } from '../worker/S3MWorkerClient.js';
|
|
|
35
35
|
import { STYLE, STYLE_LABELS, SELECT_MODE, DEFAULT_STYLE, createSharedUniforms, createStyledMaterial, createOutlineMaterial, restoreBaseBlendState, } from './StyleMaterial.js';
|
|
36
36
|
import { applyPointCloudStyle, isOwnedPointCloudMaterial } from './PointCloudMaterial.js';
|
|
37
37
|
import { Picker } from './Picker.js';
|
|
38
|
+
import { MotionBlurPass, normalizeMotionBlur, autoMotionBlurDamp } from './MotionBlurPass.js';
|
|
38
39
|
import { Animator } from '../anim/Animator.js';
|
|
39
40
|
import { buildClipPlanes, lineOfSight as computeLineOfSight, measureSegment, polygonArea, polygonAreaHorizontal, polygonPerimeter, polylineLength, polylineLengthHorizontal, } from '../analysis/index.js';
|
|
40
41
|
import { skylineFromDepth } from '../analysis/skyline.js';
|
|
@@ -44,10 +45,19 @@ import { Path } from '../anim/Path.js';
|
|
|
44
45
|
import { GPUPathManager } from './GPUPathManager.js';
|
|
45
46
|
import { AttributeQuery } from '../core/AttributeQuery.js';
|
|
46
47
|
import { lngLatFromWorld, worldFromLngLat } from './GeoAdapter.js';
|
|
48
|
+
import { computeTilePlacement } from './tilePlacement.js';
|
|
47
49
|
import { DatasetManager, } from './DatasetManager.js';
|
|
48
50
|
import { FeatureStyleManager, extractEulerAngles, } from './FeatureStyleManager.js';
|
|
49
51
|
import { GroundManager, } from './GroundManager.js';
|
|
50
52
|
export { GroundManager, DatasetManager, FeatureStyleManager };
|
|
53
|
+
/** 稳健归一化 depthMode 入参,默认 'scene' */
|
|
54
|
+
export function normalizeDepthMode(mode) {
|
|
55
|
+
if (mode === 'clear')
|
|
56
|
+
return 'clear';
|
|
57
|
+
if (mode === 'auto')
|
|
58
|
+
return 'auto';
|
|
59
|
+
return 'scene';
|
|
60
|
+
}
|
|
51
61
|
function styledMaterialOf(mesh) {
|
|
52
62
|
const m = Array.isArray(mesh.material) ? mesh.material[0] : mesh.material;
|
|
53
63
|
const styled = m;
|
|
@@ -70,6 +80,11 @@ function clampOpacity(v) {
|
|
|
70
80
|
}
|
|
71
81
|
/** 低于这个透明度就当作完全不可见:跳过绘制,避免全透明物体写深度 */
|
|
72
82
|
const OPACITY_EPS = 1e-3;
|
|
83
|
+
/**
|
|
84
|
+
* 拾取反解瓦片级放置时的临时矩阵。命中点→规范 ENU 是一次独立查询(鼠标事件),
|
|
85
|
+
* 复用一个实例避免每次分配;不可重入(`_toCanonicalScene` 内部不递归调用)。
|
|
86
|
+
*/
|
|
87
|
+
const _tilePlacementInverse = new THREE.Matrix4();
|
|
73
88
|
/* ------------------------------------------------------------------ 可动画的样式键 */
|
|
74
89
|
/**
|
|
75
90
|
* 能逐帧插值的标量样式键。
|
|
@@ -152,6 +167,8 @@ export class S3MViewer {
|
|
|
152
167
|
* 传了值就只认自己,多图层同页各用各的,互不覆盖。
|
|
153
168
|
*/
|
|
154
169
|
_materialQuality;
|
|
170
|
+
_lodTransition;
|
|
171
|
+
_fallbackOffset;
|
|
155
172
|
/**
|
|
156
173
|
* 本图层的颜色管线。
|
|
157
174
|
*
|
|
@@ -180,6 +197,7 @@ export class S3MViewer {
|
|
|
180
197
|
}
|
|
181
198
|
set sceneAnchor(val) {
|
|
182
199
|
this._datasetManager.sceneAnchor = val;
|
|
200
|
+
this._reapplyTilePlacements();
|
|
183
201
|
}
|
|
184
202
|
/** 拾取表 / pickId 池 / 射线求交(见 viewer/PickService.ts) */
|
|
185
203
|
_picks;
|
|
@@ -241,6 +259,19 @@ export class S3MViewer {
|
|
|
241
259
|
_unsupported;
|
|
242
260
|
/** LOD 判定屏幕高度的 DPR 上限。Infinity = 不截断(默认,行为同旧版) */
|
|
243
261
|
_lodPixelRatioCap = Infinity;
|
|
262
|
+
/**
|
|
263
|
+
* 运动模糊配置(默认 null = 关闭)。归一化后的形状见 `normalizeMotionBlur`;
|
|
264
|
+
* pass 延迟到首帧才建(构造期 renderer 都还没有)。
|
|
265
|
+
*/
|
|
266
|
+
_motionBlurCfg = null;
|
|
267
|
+
_motionBlur = null;
|
|
268
|
+
/**
|
|
269
|
+
* 瓦片级放置(默认关)。关时 `_registerTile` 完全不碰瓦片矩阵,代码路径与改动前
|
|
270
|
+
* 逐字一致;开时每块瓦片用自己的中心纬度算尺度,见 `tilePlacement.ts`。
|
|
271
|
+
*/
|
|
272
|
+
_perTilePlacement;
|
|
273
|
+
/** 本层绘制前深度缓冲的处理策略:'scene' | 'clear' | 'auto',默认 'scene' */
|
|
274
|
+
_depthMode = 'scene';
|
|
244
275
|
/** 宿主请求重画(MapLibre 传 triggerRepaint,纯 three 宿主传自己的渲染循环) */
|
|
245
276
|
_requestRepaint() {
|
|
246
277
|
this._onRequestRepaint();
|
|
@@ -254,12 +285,23 @@ export class S3MViewer {
|
|
|
254
285
|
this._lodRangeScale = options.lodRangeScale ?? 1.0;
|
|
255
286
|
this._unsupported = options.unsupported ?? 'warn';
|
|
256
287
|
this._lodPixelRatioCap = options.lodPixelRatioCap ?? Infinity;
|
|
288
|
+
// 瓦片级放置默认开启:旧行为是拿单一常量切平面拟合随纬度变化的墨卡托比例,属于 bug 修复,
|
|
289
|
+
// 所以默认值跟着改(见 AGENTS.md 不变式 ①)。显式传 false 仍可回退。
|
|
290
|
+
this._perTilePlacement = options.perTilePlacement !== false;
|
|
291
|
+
this._depthMode = normalizeDepthMode(options.depthMode);
|
|
257
292
|
setTextureSizeCap(options.maxTextureSize ?? Infinity);
|
|
293
|
+
// 运动模糊:默认关闭;开了也只在 renderFrame 里按需接管「场景→屏幕」这一步
|
|
294
|
+
this._motionBlurCfg = normalizeMotionBlur(options.motionBlur);
|
|
258
295
|
// 只记本层的档位,**不写模块级全局**:以前这里 setMaterialQuality(...) 会把全局
|
|
259
296
|
// 改掉,于是同页后创建的图层顺手把先创建的图层也打回去了(多层互相覆盖的根因)。
|
|
260
297
|
this._materialQuality = normalizeMaterialQuality(options.materialQuality);
|
|
261
298
|
// 之后 addDataset 的数据集按这个默认建材质(数据集自己指定则用自己的)
|
|
262
299
|
this._datasetManager.defaultMaterialQuality = this._materialQuality;
|
|
300
|
+
this._lodTransition = options.lodTransition ?? 'binary';
|
|
301
|
+
this._fallbackOffset = options.fallbackOffset;
|
|
302
|
+
this._datasetManager.defaultLodTransition = this._lodTransition;
|
|
303
|
+
this._datasetManager.defaultFallbackOffset = this._fallbackOffset;
|
|
304
|
+
this._datasetManager.defaultBakeMercator = options.bakeMercator;
|
|
263
305
|
if (options.totalMaxTiles) {
|
|
264
306
|
this._datasetManager.totalMaxTiles = options.totalMaxTiles;
|
|
265
307
|
}
|
|
@@ -334,6 +376,7 @@ export class S3MViewer {
|
|
|
334
376
|
return self._smidMeshes;
|
|
335
377
|
},
|
|
336
378
|
lngLatFromWorld: (x, y) => self._lngLatFromWorld(x, y),
|
|
379
|
+
toCanonicalScene: (p, mesh) => self._toCanonicalScene(p, mesh),
|
|
337
380
|
gpuPathPoseAt: (smid) => self._gpuPathPoseAt(smid),
|
|
338
381
|
isFeatureHidden: (smid, batchId) => self._isFeatureHidden(smid, batchId),
|
|
339
382
|
});
|
|
@@ -355,9 +398,18 @@ export class S3MViewer {
|
|
|
355
398
|
styleTarget: this._styleTarget,
|
|
356
399
|
animator: this.animator,
|
|
357
400
|
pickables: this._picks.pickables,
|
|
358
|
-
scene
|
|
359
|
-
|
|
360
|
-
|
|
401
|
+
// mount() / restoreContext() 都会换掉 scene / renderer / camera,这里必须用
|
|
402
|
+
// getter 拿「当下」的实例 —— 拆分 EdgeDecorator 时曾误写成值快照,mount 之后
|
|
403
|
+
// host.renderer 恒为 null,GPU 描边(和 loadCreases 挂线)静默失效
|
|
404
|
+
get scene() {
|
|
405
|
+
return self.scene;
|
|
406
|
+
},
|
|
407
|
+
get renderer() {
|
|
408
|
+
return self.renderer;
|
|
409
|
+
},
|
|
410
|
+
get camera() {
|
|
411
|
+
return self.camera;
|
|
412
|
+
},
|
|
361
413
|
authenticate: this._authenticate,
|
|
362
414
|
requestRepaint: () => this.triggerRepaint(),
|
|
363
415
|
});
|
|
@@ -482,6 +534,8 @@ export class S3MViewer {
|
|
|
482
534
|
this._lights.dispose();
|
|
483
535
|
this.outlineMaterial.dispose();
|
|
484
536
|
this._edges.disposeGpuEdge();
|
|
537
|
+
this._motionBlur?.dispose();
|
|
538
|
+
this._motionBlur = null;
|
|
485
539
|
this.edgeMaterial.dispose();
|
|
486
540
|
this.selectionEdgeMaterial.dispose();
|
|
487
541
|
this.scene?.clear();
|
|
@@ -553,7 +607,15 @@ export class S3MViewer {
|
|
|
553
607
|
.filter((d) => !!d?.configUrl)
|
|
554
608
|
.map((d) => ({ ...d, polygonOffset: d.polygonOffset ?? options.polygonOffset }));
|
|
555
609
|
if (options.configUrl)
|
|
556
|
-
return [{
|
|
610
|
+
return [{
|
|
611
|
+
id: 'default',
|
|
612
|
+
name: '默认数据集',
|
|
613
|
+
configUrl: options.configUrl,
|
|
614
|
+
useWorker: options.useWorker,
|
|
615
|
+
polygonOffset: options.polygonOffset,
|
|
616
|
+
lodTransition: options.lodTransition,
|
|
617
|
+
fallbackOffset: options.fallbackOffset,
|
|
618
|
+
}];
|
|
557
619
|
return [];
|
|
558
620
|
}
|
|
559
621
|
/** 追加一个数据集。图层 addLayer 之前调用会抛错。 */
|
|
@@ -561,7 +623,12 @@ export class S3MViewer {
|
|
|
561
623
|
if (!this.scene)
|
|
562
624
|
throw new Error('[s3m] 图层尚未 addLayer,不能添加数据集');
|
|
563
625
|
// 图层级默认值:数据集自己指定了就用自己的,见 `S3MViewerOptions.unsupported`
|
|
564
|
-
const merged = {
|
|
626
|
+
const merged = {
|
|
627
|
+
...ds,
|
|
628
|
+
unsupported: ds.unsupported ?? this._unsupported,
|
|
629
|
+
lodTransition: ds.lodTransition ?? this._lodTransition,
|
|
630
|
+
fallbackOffset: ds.fallbackOffset ?? this._fallbackOffset,
|
|
631
|
+
};
|
|
565
632
|
const entry = await this._datasetManager.addDataset(merged, this.scene, this.geo, this.uniforms, this._lodRangeScale, this.options.buildTile, this.options.maxTiles ?? 64, this.options.maxConcurrent ?? 4, {
|
|
566
633
|
onTile: (node, e) => {
|
|
567
634
|
this._registerTile(node, e);
|
|
@@ -603,6 +670,8 @@ export class S3MViewer {
|
|
|
603
670
|
/** 当地图 CRS 投影切换或宿主 map 挂载后,重新计算场景锚点与各数据集 offset */
|
|
604
671
|
recomputeAnchors() {
|
|
605
672
|
this._datasetManager.recomputeAnchors(this.geo);
|
|
673
|
+
// 锚点变了 → 每块瓦片的 k 与 position 都要重算(关这项功能时是空转,一次遍历都不做)
|
|
674
|
+
this._reapplyTilePlacements();
|
|
606
675
|
this._heights.refreshAll();
|
|
607
676
|
this._requestRepaint();
|
|
608
677
|
}
|
|
@@ -1338,6 +1407,20 @@ export class S3MViewer {
|
|
|
1338
1407
|
}
|
|
1339
1408
|
/* ---------------------------------------------------------------- 瓦片注册 */
|
|
1340
1409
|
_registerTile(node, entry) {
|
|
1410
|
+
// 必须在 add / updateMatrixWorld 之前设置:后者会拿当前 matrix 算 matrixWorld 并缓存
|
|
1411
|
+
if (this._perTilePlacement && node.sphere) {
|
|
1412
|
+
this._applyTilePlacement(node.group, node.sphere.center, entry);
|
|
1413
|
+
}
|
|
1414
|
+
else {
|
|
1415
|
+
const sceneAnchor = this.sceneAnchor;
|
|
1416
|
+
if (sceneAnchor) {
|
|
1417
|
+
const sEast = sceneAnchor.meterInMercatorEast ?? sceneAnchor.meterInMercator;
|
|
1418
|
+
const sNorth = sceneAnchor.meterInMercatorNorth ?? sceneAnchor.meterInMercator;
|
|
1419
|
+
const sUp = sceneAnchor.meterInMercatorUp ?? sEast;
|
|
1420
|
+
const center = node.sphere ? [node.sphere.center.x, node.sphere.center.y, node.sphere.center.z] : [0, 0, 0];
|
|
1421
|
+
this._updateTileUniforms(node.group, center, [1.0 / sEast, 1.0 / sNorth, 1.0 / sUp]);
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1341
1424
|
entry.root.add(node.group);
|
|
1342
1425
|
node.group.updateMatrixWorld(true);
|
|
1343
1426
|
const hasNativeEdges = node.group.userData.hasNativeEdges === true;
|
|
@@ -1407,6 +1490,124 @@ export class S3MViewer {
|
|
|
1407
1490
|
this._sel.replayOnto(mesh);
|
|
1408
1491
|
}
|
|
1409
1492
|
}
|
|
1493
|
+
/**
|
|
1494
|
+
* 把一块瓦片摆到「用它自己中心纬度算出的尺度」上(`perTilePlacement` 开启时才走)。
|
|
1495
|
+
*
|
|
1496
|
+
* 矩阵是 `T(position) · S(k) · T(-pivot)`,展开后即 `v ↦ position + k·(v - pivot)`。
|
|
1497
|
+
* 直接用 `makeScale + setPosition` 一次写好,省掉两个临时矩阵:
|
|
1498
|
+
* `T(t)·S(k)·T(-c)` 的元素其实就是对角 `k` 加平移 `t - k·c`。
|
|
1499
|
+
*
|
|
1500
|
+
* 只写 `matrix`,不碰 `rotation / position / scale` —— three 在
|
|
1501
|
+
* `matrixAutoUpdate = false` 时直接使用 `matrix`,那些字段留着无害,
|
|
1502
|
+
* 但别让 `updateMatrix()` 有机会覆盖回来。
|
|
1503
|
+
*/
|
|
1504
|
+
_applyTilePlacement(group, center, entry) {
|
|
1505
|
+
const sceneAnchor = this.sceneAnchor;
|
|
1506
|
+
if (!sceneAnchor || !entry.anchor)
|
|
1507
|
+
return;
|
|
1508
|
+
const p = computeTilePlacement({
|
|
1509
|
+
center,
|
|
1510
|
+
datasetAnchor: entry.anchor,
|
|
1511
|
+
sceneAnchor,
|
|
1512
|
+
datasetOffset: entry.offset,
|
|
1513
|
+
geo: this.geo,
|
|
1514
|
+
});
|
|
1515
|
+
if (!p)
|
|
1516
|
+
return;
|
|
1517
|
+
const kEast = p.scaleEast ?? p.scale;
|
|
1518
|
+
const kNorth = p.scaleNorth ?? p.scale;
|
|
1519
|
+
const kUp = kEast;
|
|
1520
|
+
group.matrixAutoUpdate = false;
|
|
1521
|
+
group.matrix.makeScale(kEast, kNorth, kUp);
|
|
1522
|
+
group.matrix.setPosition(p.position[0] - kEast * p.pivot[0], p.position[1] - kNorth * p.pivot[1], p.position[2] - kUp * p.pivot[2]);
|
|
1523
|
+
group.matrixWorldNeedsUpdate = true;
|
|
1524
|
+
// 重锚点后要能重算:中心只属于注册那一刻,存到 group 上,不依赖 tileset 内部结构
|
|
1525
|
+
group.userData.tileCenter = {
|
|
1526
|
+
x: center.x,
|
|
1527
|
+
y: center.y,
|
|
1528
|
+
z: center.z,
|
|
1529
|
+
};
|
|
1530
|
+
// 逆矩阵缓存:_toCanonicalScene 常态路径无需每次拾取反复求逆,失效点仅在瓦片放置或重锚点
|
|
1531
|
+
const inv = (group.userData.placementInverse ??= new THREE.Matrix4());
|
|
1532
|
+
inv.copy(group.matrix).invert();
|
|
1533
|
+
const sSceneEast = sceneAnchor.meterInMercatorEast ?? sceneAnchor.meterInMercator;
|
|
1534
|
+
const sSceneNorth = sceneAnchor.meterInMercatorNorth ?? sceneAnchor.meterInMercator;
|
|
1535
|
+
const sSceneUp = sceneAnchor.meterInMercatorUp ?? sSceneEast;
|
|
1536
|
+
const invScale = p.invTileScale ?? [1.0 / (sSceneEast * kEast), 1.0 / (sSceneNorth * kNorth), 1.0 / (sSceneUp * kUp)];
|
|
1537
|
+
this._updateTileUniforms(group, p.pivot, invScale);
|
|
1538
|
+
}
|
|
1539
|
+
_updateTileUniforms(group, pivot, invTileScale) {
|
|
1540
|
+
group.traverse((obj) => {
|
|
1541
|
+
const mesh = obj;
|
|
1542
|
+
if ((obj.isMesh || obj.isPoints) && obj.material) {
|
|
1543
|
+
const mats = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
|
|
1544
|
+
for (const m of mats) {
|
|
1545
|
+
const uPivot = m.userData?.uTilePivot;
|
|
1546
|
+
const uScale = m.userData?.uInvTileScale;
|
|
1547
|
+
if (uPivot?.value) {
|
|
1548
|
+
uPivot.value.set(pivot[0], pivot[1], pivot[2]);
|
|
1549
|
+
}
|
|
1550
|
+
if (uScale?.value) {
|
|
1551
|
+
uScale.value.set(invTileScale[0], invTileScale[1], invTileScale[2]);
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
});
|
|
1556
|
+
}
|
|
1557
|
+
/**
|
|
1558
|
+
* 世界坐标 → **规范**场景 ENU:把瓦片级放置的缩放反解掉。
|
|
1559
|
+
*
|
|
1560
|
+
* `perTilePlacement` 开启后,瓦片几何是「绕自身中心缩放 k 倍、再摆到精确位置」的,
|
|
1561
|
+
* 所以命中点的世界坐标不再等于数据本身的 ENU。拾取要报的是**坐标本身**——
|
|
1562
|
+
* 一个屋顶的经纬度不该因为我们怎么缩放它而改变,否则同一个点在 20 km 外会比
|
|
1563
|
+
* 1 km 内偏出几米,量算、贴地、外部标注全都跟着漂。
|
|
1564
|
+
*
|
|
1565
|
+
* 反解只需要一次矩阵求逆:父级 `entry.root` 只有平移(`layoutDataset` 只写
|
|
1566
|
+
* `position`),所以世界坐标 `p` 对应的瓦片局部坐标是
|
|
1567
|
+
*
|
|
1568
|
+
* ```
|
|
1569
|
+
* v = group.matrix⁻¹ · (p − root.position)
|
|
1570
|
+
* canonical = root.position + v
|
|
1571
|
+
* ```
|
|
1572
|
+
*
|
|
1573
|
+
* 关掉本功能、或网格不在已放置的瓦片里时**原样返回同一个对象**,
|
|
1574
|
+
* 默认路径一次矩阵运算都不做。
|
|
1575
|
+
*/
|
|
1576
|
+
_toCanonicalScene(p, mesh) {
|
|
1577
|
+
if (!this._perTilePlacement || !mesh)
|
|
1578
|
+
return p;
|
|
1579
|
+
// 网格直接挂在瓦片 group 下(`_registerTile` 就是遍历 node.group.children);
|
|
1580
|
+
// 往上找到第一个带 tileCenter 的祖先,避免依赖层级深度
|
|
1581
|
+
let group = mesh.parent;
|
|
1582
|
+
while (group && !group.userData.tileCenter)
|
|
1583
|
+
group = group.parent;
|
|
1584
|
+
if (!group)
|
|
1585
|
+
return p;
|
|
1586
|
+
const root = group.parent;
|
|
1587
|
+
if (!root)
|
|
1588
|
+
return p;
|
|
1589
|
+
return p
|
|
1590
|
+
.clone()
|
|
1591
|
+
.sub(root.position)
|
|
1592
|
+
.applyMatrix4(group.userData.placementInverse ?? _tilePlacementInverse.copy(group.matrix).invert())
|
|
1593
|
+
.add(root.position);
|
|
1594
|
+
}
|
|
1595
|
+
/**
|
|
1596
|
+
* 场景锚点变了(`recomputeAnchors`)之后,所有已注册瓦片的放置都要重算 ——
|
|
1597
|
+
* `k` 与 `position` 都依赖场景锚点。没存 `tileCenter` 的(未开启本功能时注册的)
|
|
1598
|
+
* 一律跳过,保证默认路径不产生任何副作用。
|
|
1599
|
+
*/
|
|
1600
|
+
_reapplyTilePlacements() {
|
|
1601
|
+
for (const entry of this._datasetManager.values()) {
|
|
1602
|
+
if (!entry.anchor)
|
|
1603
|
+
continue;
|
|
1604
|
+
for (const child of entry.root.children) {
|
|
1605
|
+
const c = child.userData.tileCenter;
|
|
1606
|
+
if (c)
|
|
1607
|
+
this._applyTilePlacement(child, c, entry);
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1410
1611
|
/**
|
|
1411
1612
|
* 新瓦片淡入。
|
|
1412
1613
|
*
|
|
@@ -1861,9 +2062,88 @@ export class S3MViewer {
|
|
|
1861
2062
|
this._requestRepaint();
|
|
1862
2063
|
}
|
|
1863
2064
|
/** 当前材质质感档位(本图层指定了就用本图层的,否则是模块级默认) */
|
|
2065
|
+
get lodTransition() {
|
|
2066
|
+
return this._lodTransition;
|
|
2067
|
+
}
|
|
2068
|
+
setLodTransition(mode) {
|
|
2069
|
+
if (this._lodTransition === mode)
|
|
2070
|
+
return;
|
|
2071
|
+
this._lodTransition = mode;
|
|
2072
|
+
this._datasetManager.setDefaultLodTransition(mode);
|
|
2073
|
+
this.triggerRepaint();
|
|
2074
|
+
}
|
|
1864
2075
|
get materialQuality() {
|
|
1865
2076
|
return this._materialQuality ?? getMaterialQuality();
|
|
1866
2077
|
}
|
|
2078
|
+
/**
|
|
2079
|
+
* 运行时开关运动模糊后处理;`null` / 非法入参 = 关闭并立即释放 RT 显存。
|
|
2080
|
+
*
|
|
2081
|
+
* 这是纯视觉补偿(见 `motionBlur` 选项),对渲染管线唯一的结构性影响是开启期间
|
|
2082
|
+
* 「场景 → 屏幕」改走 RT 中转(合成 quad 会补回色调映射 / sRGB / 深度)。
|
|
2083
|
+
* WebGL1 上开了也自动回退直渲。`damp: 'auto'` 每帧按实时 FPS 重新解析。
|
|
2084
|
+
*/
|
|
2085
|
+
setMotionBlur(cfg) {
|
|
2086
|
+
this._motionBlurCfg = normalizeMotionBlur(cfg);
|
|
2087
|
+
if (!this._motionBlurCfg) {
|
|
2088
|
+
this._motionBlur?.dispose();
|
|
2089
|
+
this._motionBlur = null;
|
|
2090
|
+
}
|
|
2091
|
+
// 走公共 triggerRepaint:宿主 / 单测会覆盖前者来观察重绘(同其它 setter 的观测点)
|
|
2092
|
+
this.triggerRepaint();
|
|
2093
|
+
}
|
|
2094
|
+
/** 当前运动模糊配置(归一化后的副本);null = 关闭 */
|
|
2095
|
+
/**
|
|
2096
|
+
* 运行时修改深度处理策略('scene' | 'clear' | 'auto')。
|
|
2097
|
+
*/
|
|
2098
|
+
setDepthMode(mode) {
|
|
2099
|
+
const next = normalizeDepthMode(mode);
|
|
2100
|
+
if (this._depthMode !== next) {
|
|
2101
|
+
this._depthMode = next;
|
|
2102
|
+
this.triggerRepaint();
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
/** 当前深度策略('scene' | 'clear' | 'auto',默认 'scene') */
|
|
2106
|
+
get depthMode() {
|
|
2107
|
+
return this._depthMode;
|
|
2108
|
+
}
|
|
2109
|
+
/**
|
|
2110
|
+
* 当前帧实际生效的深度处理模式('scene' | 'clear')。
|
|
2111
|
+
* - 'clear': 显式指定 clear,或 auto 模式下命中地面覆盖/倾斜摄影/DEM等地面类数据
|
|
2112
|
+
* - 'scene': 显式指定 scene,或 auto 模式下无地面类数据
|
|
2113
|
+
*/
|
|
2114
|
+
resolveDepthMode() {
|
|
2115
|
+
if (this._depthMode === 'clear')
|
|
2116
|
+
return 'clear';
|
|
2117
|
+
if (this._depthMode === 'scene')
|
|
2118
|
+
return 'scene';
|
|
2119
|
+
// 'auto': 按数据集与地面覆盖特征推断
|
|
2120
|
+
if (this.groundManager && this.groundManager.mode !== 'none') {
|
|
2121
|
+
return 'clear';
|
|
2122
|
+
}
|
|
2123
|
+
for (const entry of this._datasetManager.datasets.values()) {
|
|
2124
|
+
if (!entry.visible)
|
|
2125
|
+
continue;
|
|
2126
|
+
if (entry.options.depthMode === 'clear')
|
|
2127
|
+
return 'clear';
|
|
2128
|
+
if (entry.options.depthMode === 'scene')
|
|
2129
|
+
continue;
|
|
2130
|
+
if (entry.tileset?.isS3MV1)
|
|
2131
|
+
return 'clear';
|
|
2132
|
+
const cfg = entry.tileset?.config;
|
|
2133
|
+
if (cfg) {
|
|
2134
|
+
const typeStr = `${cfg.fileType ?? ''} ${cfg.dataType ?? ''} ${cfg.datasetType ?? ''} ${cfg.cacheType ?? ''} ${cfg.fileExtentName ?? ''}`;
|
|
2135
|
+
if (/osgb|oblique|ground|dem|tin|terrain/i.test(typeStr))
|
|
2136
|
+
return 'clear';
|
|
2137
|
+
}
|
|
2138
|
+
if (entry.options.configUrl && /osgb|oblique/i.test(entry.options.configUrl)) {
|
|
2139
|
+
return 'clear';
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
return 'scene';
|
|
2143
|
+
}
|
|
2144
|
+
get motionBlur() {
|
|
2145
|
+
return this._motionBlurCfg ? { ...this._motionBlurCfg } : null;
|
|
2146
|
+
}
|
|
1867
2147
|
/**
|
|
1868
2148
|
* 运行时切换**本图层**的颜色管线(`'linear'` | `'direct'`)。
|
|
1869
2149
|
*
|
|
@@ -2154,10 +2434,11 @@ export class S3MViewer {
|
|
|
2154
2434
|
const anchor = this.sceneAnchor;
|
|
2155
2435
|
if (!anchor)
|
|
2156
2436
|
return [0, 0, 0];
|
|
2157
|
-
const
|
|
2437
|
+
const sEast = anchor.meterInMercatorEast ?? anchor.meterInMercator;
|
|
2438
|
+
const sNorth = anchor.meterInMercatorNorth ?? anchor.meterInMercator;
|
|
2158
2439
|
const w = worldFromLngLat(this.geo, p.lng, p.lat, p.altitude);
|
|
2159
2440
|
// 注意 y 取反:three 的 y 向上,墨卡托 y 向下(与 PickService.pickPosition 互逆)
|
|
2160
|
-
return [(w.x - anchor.x) /
|
|
2441
|
+
return [(w.x - anchor.x) / sEast, -(w.y - anchor.y) / sNorth, p.altitude - (anchor.altitude ?? 0)];
|
|
2161
2442
|
}
|
|
2162
2443
|
/**
|
|
2163
2444
|
* 经纬度 + 绝对高程 → 场景 ENU 米制坐标。
|
|
@@ -2178,8 +2459,9 @@ export class S3MViewer {
|
|
|
2178
2459
|
const anchor = this.sceneAnchor;
|
|
2179
2460
|
if (!anchor)
|
|
2180
2461
|
return { lng: 0, lat: 0, altitude: v[2] };
|
|
2181
|
-
const
|
|
2182
|
-
const
|
|
2462
|
+
const sEast = anchor.meterInMercatorEast ?? anchor.meterInMercator;
|
|
2463
|
+
const sNorth = anchor.meterInMercatorNorth ?? anchor.meterInMercator;
|
|
2464
|
+
const [lng, lat] = lngLatFromWorld(this.geo, v[0] * sEast + anchor.x, -v[1] * sNorth + anchor.y);
|
|
2183
2465
|
return { lng, lat, altitude: v[2] + (anchor.altitude ?? 0) };
|
|
2184
2466
|
}
|
|
2185
2467
|
/**
|
|
@@ -2553,6 +2835,33 @@ export class S3MViewer {
|
|
|
2553
2835
|
_sortSplats() {
|
|
2554
2836
|
this._splats.sort(this.camera, this._pointClouds);
|
|
2555
2837
|
}
|
|
2838
|
+
/**
|
|
2839
|
+
* 运动模糊路径(`_motionBlurCfg` 非空时接管「场景 → 屏幕」)。
|
|
2840
|
+
*
|
|
2841
|
+
* 返回 false = 不能用(WebGL1 / 尺寸非法),调用方回退直渲 —— 配置保留不丢,
|
|
2842
|
+
* 上下文恢复成 WebGL2 后下一帧自动生效。
|
|
2843
|
+
* `damp: 'auto'` 在这里按实时 FPS 解析(fps 为 0 的启动阶段按 60fps 保守取值),
|
|
2844
|
+
* `maxOffset` 已在 `normalizeMotionBlur` 里归一化(默认 0.05,屏宽百分比)。
|
|
2845
|
+
*/
|
|
2846
|
+
_renderMotionBlur(width, height) {
|
|
2847
|
+
const renderer = this.renderer;
|
|
2848
|
+
const scene = this.scene;
|
|
2849
|
+
const camera = this.camera;
|
|
2850
|
+
const cfg = this._motionBlurCfg;
|
|
2851
|
+
if (!renderer || !scene || !camera || !cfg)
|
|
2852
|
+
return false;
|
|
2853
|
+
this._motionBlur ||= new MotionBlurPass();
|
|
2854
|
+
if (!this._motionBlur.supported(renderer))
|
|
2855
|
+
return false;
|
|
2856
|
+
this._motionBlur.render(renderer, scene, camera, width, height, {
|
|
2857
|
+
mode: cfg.mode,
|
|
2858
|
+
damp: cfg.damp === 'auto' ? autoMotionBlurDamp(this.fps) : cfg.damp,
|
|
2859
|
+
maxOffset: cfg.maxOffset,
|
|
2860
|
+
mvp: this._frameMvp,
|
|
2861
|
+
mvpInverse: camera.projectionMatrixInverse,
|
|
2862
|
+
});
|
|
2863
|
+
return true;
|
|
2864
|
+
}
|
|
2556
2865
|
/**
|
|
2557
2866
|
* 渲染一帧。所有宿主相关量都由调用方喂进来,viewer 不碰任何地图 API:
|
|
2558
2867
|
*
|
|
@@ -2578,9 +2887,11 @@ export class S3MViewer {
|
|
|
2578
2887
|
// 脏标记正常由微任务刷掉;这里再兜一次底,保证「本帧画出来的」一定是最新样式
|
|
2579
2888
|
this.flushOverrides();
|
|
2580
2889
|
const canvas = this.canvas;
|
|
2581
|
-
const
|
|
2890
|
+
const sEast = this.sceneAnchor.meterInMercatorEast ?? this.sceneAnchor.meterInMercator;
|
|
2891
|
+
const sNorth = this.sceneAnchor.meterInMercatorNorth ?? this.sceneAnchor.meterInMercator;
|
|
2892
|
+
const sUp = this.sceneAnchor.meterInMercatorUp ?? sEast;
|
|
2582
2893
|
// ENU → 世界坐标:x 向东,y 向北(墨卡托 y 朝南,所以取负),z 向上
|
|
2583
|
-
this._enuMatrix.set(
|
|
2894
|
+
this._enuMatrix.set(sEast, 0, 0, this.sceneAnchor.x, 0, -sNorth, 0, this.sceneAnchor.y, 0, 0, sUp, this.sceneAnchor.z, 0, 0, 0, 1);
|
|
2584
2895
|
// 正交模式由宿主在传入 mvp 前自行替换(见 s3m-maplibre 的 S3MLayer._applyOrtho),
|
|
2585
2896
|
// 视图矩阵是从宿主输入反推的(P⁻¹×MVP),不会和宿主的 pan/zoom/pitch 错位。
|
|
2586
2897
|
const mvp = this._frameMvp.fromArray(opts.mvp);
|
|
@@ -2627,15 +2938,36 @@ export class S3MViewer {
|
|
|
2627
2938
|
// 泼溅的最小像素半径 / 抗锯齿低通同样要把 ndc 换算成像素
|
|
2628
2939
|
setSplatViewport(canvas.width, canvas.height);
|
|
2629
2940
|
this.renderer.setViewport(0, 0, canvas.width, canvas.height);
|
|
2941
|
+
const effectiveDepthMode = (this.uniforms.uOpacity?.value ?? 1) > OPACITY_EPS
|
|
2942
|
+
? this.resolveDepthMode()
|
|
2943
|
+
: 'scene';
|
|
2944
|
+
if (effectiveDepthMode === 'clear' && gl) {
|
|
2945
|
+
// gl.clear 受 depthMask 约束。上一帧若以 depthWrite=false 的材质收尾(棱线、半透明
|
|
2946
|
+
// 地表覆盖、半透明点云、泼溅),GL 的 depthMask 会停在 false —— three 帧末不恢复它,
|
|
2947
|
+
// 而 resetState 排在 clear 之后,救不了这一步。不自己打开就是一次静默的空操作:
|
|
2948
|
+
// 画面上的 z-fighting 原样保留,且没有任何报错可查。
|
|
2949
|
+
gl.depthMask(true);
|
|
2950
|
+
// 只清深度,不清颜色 —— 颜色是 MapLibre 已经画好的底图
|
|
2951
|
+
gl.clear(gl.DEPTH_BUFFER_BIT);
|
|
2952
|
+
}
|
|
2953
|
+
// 清完之后必须让 three 知道状态被外部改过,否则它的状态缓存会认为自己仍然正确
|
|
2630
2954
|
this.renderer.resetState();
|
|
2631
2955
|
// 完全透明时什么都不画:透明物体照样写深度,画了会在底图上挖出一个洞。
|
|
2632
2956
|
// 这里跳过的是「本层的绘制」,宿主自己的清屏 / 底图照旧。
|
|
2633
2957
|
if ((this.uniforms.uOpacity?.value ?? 1) > OPACITY_EPS) {
|
|
2634
2958
|
this._sortSplats();
|
|
2635
|
-
this.
|
|
2959
|
+
if (this._motionBlurCfg && this._renderMotionBlur(canvas.width, canvas.height)) {
|
|
2960
|
+
// 运动模糊接管了「场景 → 屏幕」:场景进 RT,合成 quad 补回色调映射 / sRGB / 深度
|
|
2961
|
+
}
|
|
2962
|
+
else {
|
|
2963
|
+
this.renderer.render(this.scene, this.camera);
|
|
2964
|
+
}
|
|
2636
2965
|
// GPU 描边必须画在主场景之后:它的全屏 quad 参与宿主深度测试,
|
|
2637
2966
|
// 依赖本帧已经写好的深度(被地形挡住的地方不该描边)
|
|
2638
|
-
|
|
2967
|
+
// clear 模式下宿主深度已清除,描边会穿透地形产生伪影,故只在非 clear 时画。
|
|
2968
|
+
if (effectiveDepthMode !== 'clear') {
|
|
2969
|
+
this._edges.renderGpuEdges(canvas);
|
|
2970
|
+
}
|
|
2639
2971
|
}
|
|
2640
2972
|
this._frame++;
|
|
2641
2973
|
const now = performance.now();
|