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.
Files changed (71) hide show
  1. package/dist/browser/assets/{s3mWorker-DHOVIrAU.js → s3mWorker-BfSBNvF5.js} +1 -1
  2. package/dist/browser/s3m-three.iife.ext-three.js +52 -40
  3. package/dist/browser/s3m-three.iife.js +52 -40
  4. package/dist/core/BakeMercator.d.ts +54 -0
  5. package/dist/core/BakeMercator.d.ts.map +1 -0
  6. package/dist/core/BakeMercator.js +65 -0
  7. package/dist/core/BakeMercator.js.map +1 -0
  8. package/dist/core/TileGeometry.d.ts +7 -0
  9. package/dist/core/TileGeometry.d.ts.map +1 -1
  10. package/dist/core/TileGeometry.js +20 -0
  11. package/dist/core/TileGeometry.js.map +1 -1
  12. package/dist/index.d.ts +4 -3
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +2 -1
  15. package/dist/index.js.map +1 -1
  16. package/dist/render/DatasetManager.d.ts +27 -0
  17. package/dist/render/DatasetManager.d.ts.map +1 -1
  18. package/dist/render/DatasetManager.js +18 -4
  19. package/dist/render/DatasetManager.js.map +1 -1
  20. package/dist/render/GeoAdapter.d.ts +49 -24
  21. package/dist/render/GeoAdapter.d.ts.map +1 -1
  22. package/dist/render/GeoAdapter.js +96 -8
  23. package/dist/render/GeoAdapter.js.map +1 -1
  24. package/dist/render/GeometryBuilder.d.ts +17 -0
  25. package/dist/render/GeometryBuilder.d.ts.map +1 -1
  26. package/dist/render/GeometryBuilder.js +31 -2
  27. package/dist/render/GeometryBuilder.js.map +1 -1
  28. package/dist/render/S3MViewer.d.ts +72 -2
  29. package/dist/render/S3MViewer.d.ts.map +1 -1
  30. package/dist/render/S3MViewer.js +269 -9
  31. package/dist/render/S3MViewer.js.map +1 -1
  32. package/dist/render/StyleMaterial.d.ts +5 -0
  33. package/dist/render/StyleMaterial.d.ts.map +1 -1
  34. package/dist/render/StyleMaterial.js +21 -0
  35. package/dist/render/StyleMaterial.js.map +1 -1
  36. package/dist/render/tilePlacement.d.ts +84 -0
  37. package/dist/render/tilePlacement.d.ts.map +1 -0
  38. package/dist/render/tilePlacement.js +71 -0
  39. package/dist/render/tilePlacement.js.map +1 -0
  40. package/dist/render/viewer/EdgeDecorator.d.ts +1 -0
  41. package/dist/render/viewer/EdgeDecorator.d.ts.map +1 -1
  42. package/dist/render/viewer/EdgeDecorator.js +3 -0
  43. package/dist/render/viewer/EdgeDecorator.js.map +1 -1
  44. package/dist/render/viewer/PickService.d.ts +13 -0
  45. package/dist/render/viewer/PickService.d.ts.map +1 -1
  46. package/dist/render/viewer/PickService.js +24 -15
  47. package/dist/render/viewer/PickService.js.map +1 -1
  48. package/dist/render/viewer/types.d.ts +53 -0
  49. package/dist/render/viewer/types.d.ts.map +1 -1
  50. package/dist/render/viewer/types.js.map +1 -1
  51. package/dist/tileset/S3MTileset.d.ts +43 -1
  52. package/dist/tileset/S3MTileset.d.ts.map +1 -1
  53. package/dist/tileset/S3MTileset.js +202 -2
  54. package/dist/tileset/S3MTileset.js.map +1 -1
  55. package/dist/worker/s3mWorker.js +115 -62
  56. package/dist/worker/s3mWorker.js.map +1 -1
  57. package/docs/API.md +18 -0
  58. package/package.json +1 -1
  59. package/src/core/BakeMercator.ts +96 -0
  60. package/src/core/TileGeometry.ts +26 -0
  61. package/src/index.ts +11 -1
  62. package/src/render/DatasetManager.ts +43 -5
  63. package/src/render/GeoAdapter.ts +139 -11
  64. package/src/render/GeometryBuilder.ts +53 -2
  65. package/src/render/S3MViewer.ts +306 -13
  66. package/src/render/StyleMaterial.ts +26 -0
  67. package/src/render/tilePlacement.ts +121 -0
  68. package/src/render/viewer/EdgeDecorator.ts +3 -0
  69. package/src/render/viewer/PickService.ts +44 -16
  70. package/src/render/viewer/types.ts +51 -0
  71. package/src/tileset/S3MTileset.ts +226 -3
package/docs/API.md CHANGED
@@ -170,7 +170,11 @@ new S3MViewer(options?: S3MViewerOptions)
170
170
  | (随 `useWorker`)纹理解码 | `boolean` | `true` | DXT / CRN / WEBP 纹理解码与 `.s3mb` 解析共用同一个 Worker(`core/TextureDecode.ts` 三方共用同一份实现,WEBP 走可 transfer 的 ImageBitmap);失败自动回主线程,产物逐字节一致 |
171
171
  | (随 `useWorker`)几何组装 | `boolean` | `true` | 顶点解交错 / 法线计算 / 同材质合批合并(`core/TileGeometry.ts`)随解析在 Worker 完成,主线程只做 three 对象包装;Worker 不可用时主线程跑同一份实现,产物一致 |
172
172
  | (随 `useWorker`)DXT 压缩直传 | 自动 | 自动 | 宿主 GL 同时支持 `WEBGL_compressed_texture_s3tc`(+srgb) 时,dxt1/dxt5(含 CRN 转码产物)不解码直接 `CompressedTexture` 上传,显存与上传带宽降为 1/4~1/8;块长与 4×4 对齐公式不符、flipY 图集、移动端无扩展时自动回退 RGBA 解码 |
173
+ | `perTilePlacement` | `boolean` | `true` | 瓦片级切平面放置与拾取反解(**默认开启**)。每块瓦片用自身中心纬度计算墨卡托比例与反解拾取,消除「全场景共用一个常量切平面」在大范围(距场景锚点 20 km 处 0.26% 误差,约 26 m;200 km 处 2.8 km)下的累积尺度误差与拾取偏移。逆矩阵自动缓存在瓦片 group 上避免拾取性能损耗。可显式传 `false` 回退旧版单一全场景切平面(下游若缓存了旧坐标或与旧数据对齐时使用) |
174
+ | `bakeMercator` | `boolean` | `undefined`(自动) | 全球大尺度下逐顶点烘焙精确 Web 墨卡托偏移(Issue #15)。消除大范围单瓦片内因墨卡托纬度非线性缩放导致的视差游动与呼吸效应。默认 `undefined`:当瓦片集根节点包围球半径 > 25 km 时自动启用;<= 25 km 时保持关闭(顶点 buffer 逐字节与旧版相同)。传 `true` / `false` 强制开关 |
173
175
  | `lodRangeScale` | `number` | `1.0` | LOD 切换距离缩放系数,> 1 更倾向保留粗模,< 1 提前细化 |
176
+ | `lodTransition` | `'binary' \| 'incremental'` | `'binary'` | LOD 细化过渡策略。`'binary'`(默认):整节点二值切换,所有子瓦片全部就绪或失败后整块替换;`'incremental'`:增量细化(primary / fallback 双列表 + LEQUAL 深度推移),子瓦片就绪一个显示一个,未就绪区域由最近父瓦片(fallback)垫底填充,消除整块跳变与慢 child 阻塞,极端角度接缝无缝衔接且不出洞 |
177
+ | `fallbackOffset` | `{ factor?: number; units?: number }` | `{ factor: 1, units: 2 }` | 增量细化模式下 fallback 兜底瓦片的材质深度偏移系数,向远处推移深度配合 gl.LEQUAL 消除共面 z-fighting |
174
178
  | `lodPixelRatioCap` | `number` | `Infinity` | LOD 判定屏幕高度的 **DPR 上限**(不截断)。默认按 `canvasHeight` 物理像素算,与官方 `calcPixFromCam` 同口径;3x DPR 手机上由此会多细化、多拉瓦片,传 `2` 即按「CSS 高度 × 2」截断 |
175
179
  | `anisotropyCap` | `number` | `Infinity` | 各向异性过滤上限(不封顶 = 取 GPU 最大值,通常 16)。移动端封到 4 / 8 肉眼通常无差,采样成本明显下降。**图层级**:运行时 `setAnisotropyCap()` 会立刻应用到已上传纹理(改采样参数 + 重传,不重解码、不重载瓦片),图集纹理恒为 1 |
176
180
  | `maxTextureSize` | `number` | `Infinity` | 纹理长边像素上限(不限制)。非图集的 RGBA / WebP 产物按 2× 盒式滤波减半到上限内;DXT 压缩直传与图集不受影响。**图层级**,但只影响之后解码的纹理(已解码的不重解) |
@@ -178,6 +182,7 @@ new S3MViewer(options?: S3MViewerOptions)
178
182
  | `materialQuality` | `'pbr' \| 'lambert'` | `'pbr'` | 光照模型。`lambert` 用 `MeshLambertMaterial` 替换 PBR,去掉 GGX 与环境球谐,片元约省一半;白模 / 纯色 / 按高度着色几乎无差。**图层级**:同页多个图层各用各的,互不覆盖;不传则跟随模块级默认(`setMaterialQuality`)。只影响之后创建的材质 |
179
183
  | `colorPipeline` | `'linear' \| 'direct'` | `'linear'` | 颜色管线。`direct` = 官方 iClient3D 的 sRGB 直乘(同时关掉纹理解码),`lightingPreset: 'supermap'` 会选它。**图层级**:同页多图层各用各的;已加载瓦片可运行时 `setColorPipeline(q, { duration })` 走 fade-through 平滑切过去(重建材质 + 重传纹理,不重下瓦片) |
180
184
  | `motionBlur` | `{ mode?: 'afterimage' \| 'velocity'; damp?: number \| 'auto'; maxOffset?: number }` | 不传 = 关闭 | 可选的运动模糊后处理(**默认关闭**,不传时渲染路径与旧版逐字一致)。低端 / 移动设备低帧率下,用帧间残影在感知层面填补卡顿与边缘闪烁。`afterimage`(缺省)= 当前帧 + 上一帧全屏混合,几乎零边际 GPU 成本;`velocity` = 按上一帧 ↔ 当前帧相机矩阵逐像素反推屏幕速度、沿速度方向 8 tap 采样(方向感知,只覆盖相机运动)。`damp` 为拖尾长度(仅 afterimage),`'auto'`(缺省)按实时 FPS 收敛(`0.55^(fps/60)` 封顶 0.92),数值钳 [0, 0.98];`maxOffset` 为拖影上限(仅 velocity),速度向量的最大屏幕偏移按绘图缓冲宽度的百分比计,缺省 `0.05`,数值钳 [0, 1]。运行时 `setMotionBlur(null)` 关闭并释放 RT。仅 WebGL2(WebGL1 静默回退直渲);已知限制:拖影只在当前帧几何覆盖范围内(合成 quad 依赖本帧深度保住遮挡正确性);RT 用 HalfFloatType 存线性值(高光不被 clamp,代价是三张全屏纹理显存 ×2) |
185
+ | `depthMode` | `'scene' \| 'clear' \| 'auto'` | `'scene'` | 本层绘制前深度缓冲处理策略。`'scene'`(默认):保留宿主深度,建筑/管线等离散对象受地形正常遮挡;`'clear'`:先清深度再画,贴地倾斜摄影/开挖地表/大范围 DEM 化模型消除共面 z-fighting;`'auto'`:按 ground 覆盖或数据集类型自动推断 |
181
186
  | `buildTile` | `BuildTileFn \| null` | — | 自定义瓦片构建器(测试 / 服务端渲染时注入) |
182
187
  | `attributes` | `AttributeQueryOptions` | — | 可选的 iServer 数据服务配置,配了才能用 `colorByField()` 等便利方法;不配就自己取数 + `setFeaturesStyle()`,见[业务字段](#业务字段锚点在库内对应关系在外部) |
183
188
  | `onRequestRepaint` | `() => void` | — | 宿主请求重画的信号:纯 three 宿主接自己的渲染循环 |
@@ -208,6 +213,15 @@ prewarmShading(): Promise<void> // 手动预热另一种 shading 变体
208
213
 
209
214
  `geo` 是可写的 `GeoAdapter | null`:`null` 用内置的墨卡托兜底(`worldFromLngLat` / `lngLatFromWorld`),地图宿主注入适配器以支持任意 CRS / 自定义投影。
210
215
 
216
+ 内置兜底使用高精度 **WGS84 椭球模型**($a=6378137, f=1/298.257223563$):
217
+ - `worldFromLngLat` 返回带有各向异性尺度的对象:`{ x, y, z, meterInMercatorEast, meterInMercatorNorth, meterInMercatorUp, meterInMercator, altitude }`
218
+ - 东西向尺度对应卯酉圈半径 $N(\varphi) = a / \sqrt{1 - e^2 \sin^2\varphi}$:$1 / (2\pi N \cos\varphi)$
219
+ - 南北向尺度对应子午圈半径 $M(\varphi) = a(1 - e^2) / (1 - e^2 \sin^2\varphi)^{3/2}$:$1 / (2\pi M \cos\varphi)$
220
+ - 竖直向高程尺度 `meterInMercatorUp` 取东向尺度(局部正圆保角)
221
+ - `meterInMercator` 保留作为向前兼容属性,取值从旧版球面近似 $1/(2\pi R\cos\varphi)$ 升级为真实的 WGS84 椭球东向尺度(属于精度修正,默认生效且更精准)
222
+ - 彻底消除传统单标量模型在南北/东西方向上 0.4%~0.67% 的各向异性拉伸与米级系统残差。
223
+ - 包导出了高精度大地测量工具:`primeVerticalRadius`, `meridianRadius`, `enuToLngLat`, `lngLatToEnu` 及 `WGS84_A`, `WGS84_E2` 常量。
224
+
211
225
  ### 数据集管理
212
226
 
213
227
  ```ts
@@ -516,6 +530,8 @@ type ClipOptions =
516
530
  ```ts
517
531
  setLodRangeScale(scale: number, datasetId?: string): void
518
532
  readonly lodRangeScale: number
533
+ readonly lodTransition: 'binary' | 'incremental'
534
+ setLodTransition(mode: 'binary' | 'incremental'): void
519
535
  setLodPixelRatioCap(cap: number): void // Infinity = 不截断(默认)
520
536
  readonly lodPixelRatioCap: number
521
537
  setMaxTriangles(n: number, datasetId?: string): void // Infinity = 不限制(默认)
@@ -527,6 +543,8 @@ readonly anisotropyCap: number
527
543
  setMaxTextureSize(n: number): void // Infinity = 不限制(默认)
528
544
  readonly maxTextureSize: number
529
545
  readonly maxImageCache: number
546
+ setDepthMode(mode: DepthMode): void // 运行时调整深度处理策略('scene' | 'clear' | 'auto')
547
+ readonly depthMode: DepthMode // 当前深度策略
530
548
  setMotionBlur(cfg: MotionBlurOptions | null): void // 运行时开关运动模糊;null = 关闭并释放 RT 显存
531
549
  readonly motionBlur: MotionBlurOptions | null // 当前配置(归一化后的副本)
532
550
  readonly stats: Stats
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "s3m-three",
3
- "version": "0.4.26",
3
+ "version": "0.4.27",
4
4
  "description": "SuperMap S3M 三维模型格式的 three.js 渲染内核:浏览器内直接解析 .s3mb 瓦片,无需转 3D Tiles,支持 LOD 调度、白膜/按高度/按单体/动态扫描着色、单体级 GPU 拾取。宿主无关,可单独用于任意 three 场景。",
5
5
  "type": "module",
6
6
  "repository": {
@@ -0,0 +1,96 @@
1
+ /**
2
+ * 全球大尺度精确 Web 墨卡托烘焙(issue #15)。
3
+ *
4
+ * ## 背景与原理
5
+ *
6
+ * S3M 场景通常只有一个切平面:`_enuMatrix` 用场景锚点纬度的常量 `meterInMercator`
7
+ * 把整个场景缩放进 Web 墨卡托。但在跨度超过几十公里的全球或大区域场景中,
8
+ * 墨卡托投影比例随纬度变化(k = 1 / cos(φ))。
9
+ * 即使有 `perTilePlacement`(issue #12)按瓦片重置了原点与缩放,瓦片内部仍然存在
10
+ * 线性切平面与真实墨卡托投影之间的微小残差。
11
+ *
12
+ * 烘焙方案:
13
+ * 1. 预先或在组装瓦片几何时,逐顶点计算其精确的 Web 墨卡托坐标;
14
+ * 2. 减去瓦片中心的精确墨卡托坐标,得到相对于瓦片中心的相对偏移量 `aMercatorOffset`(float32);
15
+ * 3. 为什么是相对偏移:
16
+ * Web 墨卡托全局坐标在 0..1 之间,若直接用 float32(24 位尾数)存储绝对坐标,
17
+ * 分辨率约为 40000000 / 2^24 ≈ 2.38 米;
18
+ * 若改为相对瓦片中心(瓦片半径通常 <= 100 km),float32 尾数精度可达:
19
+ * 100000 / 2^24 ≈ 0.0059 米(小于 6 毫米),常规 2 km 瓦片下更是达到 0.1 毫米以内。
20
+ * 4. 着色器重构:
21
+ * 顶点着色器在局部对象空间中还原出抵消瓦片尺度与中心平移后的坐标:
22
+ * `transformed = uTilePivot + vec3(aMercatorOffset.x, -aMercatorOffset.y, aMercatorOffset.z) * uInvTileScale;`
23
+ * 其中 `uInvTileScale = 1 / (sScene * k) = 1 / sTile`。
24
+ * 经过瓦片矩阵变换与 `_enuMatrix` 缩放平移后,刚好恒等于真实的世界墨卡托坐标,误差 < 1 cm。
25
+ */
26
+
27
+ import {
28
+ EARTH_RADIUS,
29
+ enuToLngLat as geoEnuToLngLat,
30
+ mercatorFromLngLat,
31
+ } from '../render/GeoAdapter.js';
32
+
33
+ /** 超过此半径(米)的瓦片在未指定参数时自动开启墨卡托烘焙(25 km) */
34
+ export const AUTO_BAKE_RADIUS_THRESHOLD = 25000;
35
+
36
+ export interface TileAnchorLike {
37
+ lon: number;
38
+ lat: number;
39
+ height?: number;
40
+ }
41
+
42
+ export interface TileCenterLike {
43
+ x: number;
44
+ y: number;
45
+ z?: number;
46
+ }
47
+
48
+ /**
49
+ * 局部 ENU 偏移(东, 北 米)→ 经纬度(对接 GeoAdapter 的 WGS84 高精度模型)。
50
+ */
51
+ export function enuToLngLat(anchor: TileAnchorLike, east: number, north: number): [number, number] {
52
+ return geoEnuToLngLat(anchor.lon, anchor.lat, east, north);
53
+ }
54
+
55
+ /**
56
+ * 逐顶点计算 Web 墨卡托相对于瓦片中心的偏移量。
57
+ *
58
+ * @param positions 顶点位置数组(x, y, z...)
59
+ * @param vertexCount 顶点数
60
+ * @param tileAnchor 数据集/瓦片地理锚点
61
+ * @param tileCenter 瓦片中心在数据集局部 ENU 中的位置(米)
62
+ * @param stride 每个顶点的分量数(默认为 3)
63
+ * @param target 可选的复用 Float32Array
64
+ * @returns 长度为 vertexCount * 3 的 Float32Array,存 [dx, dy, dz, ...]
65
+ */
66
+ export function bakeMercatorOffsets(
67
+ positions: ArrayLike<number>,
68
+ vertexCount: number,
69
+ tileAnchor: TileAnchorLike,
70
+ tileCenter: TileCenterLike,
71
+ stride = 3,
72
+ target?: Float32Array
73
+ ): Float32Array {
74
+ const out = target && target.length >= vertexCount * 3 ? target : new Float32Array(vertexCount * 3);
75
+
76
+ const centerAlt = (tileAnchor.height ?? 0) + (tileCenter.z ?? 0);
77
+ const [centerLon, centerLat] = enuToLngLat(tileAnchor, tileCenter.x, tileCenter.y);
78
+ const cMerc = mercatorFromLngLat(centerLon, centerLat, centerAlt);
79
+
80
+ for (let i = 0; i < vertexCount; i++) {
81
+ const idx = i * stride;
82
+ const vx = positions[idx];
83
+ const vy = positions[idx + 1];
84
+ const vz = positions[idx + 2] ?? 0;
85
+
86
+ const vAlt = (tileAnchor.height ?? 0) + vz;
87
+ const [vLon, vLat] = enuToLngLat(tileAnchor, vx, vy);
88
+ const vMerc = mercatorFromLngLat(vLon, vLat, vAlt);
89
+
90
+ out[i * 3 + 0] = vMerc.x - cMerc.x;
91
+ out[i * 3 + 1] = vMerc.y - cMerc.y;
92
+ out[i * 3 + 2] = vMerc.z - cMerc.z;
93
+ }
94
+
95
+ return out;
96
+ }
@@ -21,6 +21,7 @@ import type {
21
21
  VertexAttribute,
22
22
  } from './types.js';
23
23
  import { applyCustomAttributeSemantics } from './CustomAttributes.js';
24
+ import { bakeMercatorOffsets, type TileAnchorLike, type TileCenterLike } from './BakeMercator.js';
24
25
 
25
26
  /**
26
27
  * S3M 规范里 `DrawPrimitiveType` 的两个特殊值(原住 GeometryBuilder,随组装层
@@ -249,6 +250,8 @@ export interface PrimitiveAttributes {
249
250
  * 只有存在该流时才产生;解码语义见 core/TexCoordZ.ts。
250
251
  */
251
252
  aTexCoordZ?: PlainAttribute;
253
+ /** 逐顶点精确 Web 墨卡托偏移(Issue #15) */
254
+ aMercatorOffset?: PlainAttribute;
252
255
  }
253
256
 
254
257
  export interface PrimitiveData {
@@ -298,6 +301,10 @@ export interface AssembleOptions {
298
301
  * `useWorker: false` 或 Worker 崩溃降级时它落在主线程上。
299
302
  */
300
303
  dedupeCoincident?: boolean;
304
+ /** 是否烘焙精确 Web 墨卡托偏移(Issue #15) */
305
+ bakeMercator?: boolean;
306
+ tileAnchor?: TileAnchorLike;
307
+ tileCenter?: TileCenterLike;
301
308
  vertexAttributeDescript?: unknown;
302
309
  }
303
310
 
@@ -905,5 +912,24 @@ export function assembleTileGeometry(tile: ParsedTile, options: AssembleOptions
905
912
  }
906
913
  }
907
914
 
915
+ // 逐顶点烘焙精确 Web 墨卡托(Issue #15)
916
+ if (options.bakeMercator && options.tileAnchor) {
917
+ for (const p of primitives) {
918
+ const positions = p.attributes.position?.array;
919
+ if (!positions || p.vertexCount === 0) continue;
920
+ const center = options.tileCenter ?? {
921
+ x: p.bounds.sphereCenter[0],
922
+ y: p.bounds.sphereCenter[1],
923
+ z: p.bounds.sphereCenter[2],
924
+ };
925
+ const offsets = bakeMercatorOffsets(positions, p.vertexCount, options.tileAnchor, center);
926
+ p.attributes.aMercatorOffset = {
927
+ array: offsets,
928
+ components: 3,
929
+ normalized: false,
930
+ };
931
+ }
932
+ }
933
+
908
934
  return { primitives, stats };
909
935
  }
package/src/index.ts CHANGED
@@ -51,13 +51,21 @@ export type {
51
51
 
52
52
  // —— 地理坐标适配(宿主无关接口 + 墨卡托兜底)
53
53
  export {
54
+ WGS84_A,
55
+ WGS84_B,
56
+ WGS84_F,
57
+ WGS84_E2,
54
58
  EARTH_RADIUS,
59
+ primeVerticalRadius,
60
+ meridianRadius,
61
+ enuToLngLat,
62
+ lngLatToEnu,
55
63
  mercatorFromLngLat,
56
64
  lngLatFromMercator,
57
65
  worldFromLngLat,
58
66
  lngLatFromWorld,
59
67
  } from './render/GeoAdapter.js';
60
- export type { GeoAdapter } from './render/GeoAdapter.js';
68
+ export type { GeoAdapter, WorldCoordinateResult } from './render/GeoAdapter.js';
61
69
 
62
70
  // —— 瓦片层
63
71
  export { S3MTileset, parseConfigText, classifyDatasetKind } from './tileset/S3MTileset.js';
@@ -110,8 +118,10 @@ export type {
110
118
  GroundOptions,
111
119
  LightOptions,
112
120
  PathFollowOptions,
121
+ DepthMode,
113
122
  } from './render/S3MViewer.js';
114
123
 
124
+ export { normalizeDepthMode } from './render/S3MViewer.js';
115
125
  export { LIGHT_PRESETS, DEFAULT_LIGHT_PRESET, SUPERMAP_LIGHT_PRESET, resolveLightingPreset } from './render/lightingPreset.js';
116
126
  export type { LightingPreset, PresetLightIntensities } from './render/lightingPreset.js';
117
127
 
@@ -14,6 +14,7 @@ import type { Anchor } from '../core/types.js';
14
14
  import type { UnsupportedPolicy } from '../core/degradations.js';
15
15
  import { worldFromLngLat, type GeoAdapter } from './GeoAdapter.js';
16
16
  import type { TextureCreateOptions } from './TextureFactory.js';
17
+ import type { DepthMode } from './viewer/types.js';
17
18
 
18
19
  export type DatasetAlignment = 'origin' | 'bottom' | 'top' | 'center';
19
20
 
@@ -42,6 +43,11 @@ export interface S3MDatasetOptions {
42
43
  heightOffset?: number;
43
44
  /** 是否用 Web Worker 解析经典 .s3mb 瓦片(默认开,崩溃自动降级主线程),见 S3MTilesetOptions.useWorker */
44
45
  useWorker?: boolean;
46
+ /**
47
+ * 本数据集推荐的深度模式('scene' | 'clear' | 'auto')。
48
+ * 当图层/Viewer 的 depthMode 为 'auto' 时,若任一可见数据集标注为 'clear',则整层以 'clear' 绘制。
49
+ */
50
+ depthMode?: DepthMode;
45
51
  /** 三角面材质深度偏移(共面摩尔纹 / 重合面归属),见 BuildTileOptions.polygonOffset */
46
52
  polygonOffset?: { factor: number; units: number };
47
53
  /**
@@ -49,6 +55,7 @@ export interface S3MDatasetOptions {
49
55
  * 只影响该数据集**之后创建**的材质。
50
56
  */
51
57
  materialQuality?: 'pbr' | 'lambert';
58
+ bakeMercator?: boolean;
52
59
  /**
53
60
  * 是否对该数据集生成几何棱线(CPU 端 EdgesGeometry)。
54
61
  * - 不填:跟随全局 `style.edges`
@@ -65,6 +72,13 @@ export interface S3MDatasetOptions {
65
72
  * 目前只有 3DGS:按点渲染,做不出泼溅。见 `S3MTilesetOptions.unsupported`。
66
73
  */
67
74
  unsupported?: UnsupportedPolicy;
75
+ /**
76
+ * LOD 细化过渡策略:
77
+ * - 'binary'(默认):整节点二值切换
78
+ * - 'incremental':增量细化(primary / fallback 双列表 + LEQUAL 深度推移)
79
+ */
80
+ lodTransition?: 'binary' | 'incremental';
81
+ fallbackOffset?: { factor?: number; units?: number };
68
82
  }
69
83
 
70
84
  export interface S3MDatasetEntry {
@@ -97,13 +111,32 @@ export interface DatasetManagerCallbacks {
97
111
 
98
112
  export class DatasetManager {
99
113
  readonly datasets = new Map<string, S3MDatasetEntry>();
100
- sceneAnchor: { x: number; y: number; z: number; meterInMercator: number; altitude?: number } | null = null;
114
+ sceneAnchor: {
115
+ x: number;
116
+ y: number;
117
+ z: number;
118
+ meterInMercatorEast?: number;
119
+ meterInMercatorNorth?: number;
120
+ meterInMercatorUp?: number;
121
+ meterInMercator: number;
122
+ altitude?: number;
123
+ } | null = null;
101
124
  totalMaxTiles: number = 0;
102
125
  /**
103
126
  * 图层级默认光照模型档位(`undefined` = 跟随模块级默认)。
104
127
  * 数据集自己指定了 `materialQuality` 就用自己的。
105
128
  */
106
129
  defaultMaterialQuality: 'pbr' | 'lambert' | undefined = undefined;
130
+ defaultLodTransition: 'binary' | 'incremental' = 'binary';
131
+ defaultFallbackOffset?: { factor?: number; units?: number };
132
+
133
+ setDefaultLodTransition(mode: 'binary' | 'incremental'): void {
134
+ this.defaultLodTransition = mode;
135
+ for (const entry of this.datasets.values()) {
136
+ entry.tileset?.setLodTransition(mode);
137
+ }
138
+ }
139
+ defaultBakeMercator: boolean | undefined = undefined;
107
140
 
108
141
  /**
109
142
  * 图层档位变更:同步给已存在的数据集(它们之后加载的瓦片才用新档位;
@@ -264,6 +297,8 @@ export class DatasetManager {
264
297
  maxMemory: ds.maxMemory ?? maxMemory,
265
298
  maxImageCache: ds.maxImageCache ?? maxImageCache,
266
299
  lodRangeScale: ds.lodRangeScale ?? defaultLodRangeScale,
300
+ lodTransition: ds.lodTransition ?? this.defaultLodTransition,
301
+ fallbackOffset: ds.fallbackOffset ?? this.defaultFallbackOffset,
267
302
  buildTile: buildTile ?? null,
268
303
  polygonOffset: ds.polygonOffset,
269
304
  // 数据集 > 图层 > 模块级默认(undefined 一路传下去,由 createStyledMaterial 回落)
@@ -272,6 +307,7 @@ export class DatasetManager {
272
307
  colorPipeline: this.defaultColorPipeline,
273
308
  textureOptions: this.defaultTextureOptions,
274
309
  useWorker: ds.useWorker,
310
+ bakeMercator: ds.bakeMercator ?? this.defaultBakeMercator,
275
311
  authenticate: ds.authenticate ?? authenticate,
276
312
  unsupported: ds.unsupported,
277
313
  uniforms: uniforms,
@@ -359,12 +395,14 @@ export class DatasetManager {
359
395
  if (!this.sceneAnchor) {
360
396
  this.sceneAnchor = worldFromLngLat(geo, entry.anchor.lon, entry.anchor.lat, entry.anchor.height ?? 0);
361
397
  }
362
- const s = this.sceneAnchor.meterInMercator;
398
+ const sEast = this.sceneAnchor.meterInMercatorEast ?? this.sceneAnchor.meterInMercator;
399
+ const sNorth = this.sceneAnchor.meterInMercatorNorth ?? this.sceneAnchor.meterInMercator;
400
+ const sUp = sEast;
363
401
  const a = worldFromLngLat(geo, entry.anchor.lon, entry.anchor.lat, entry.anchor.height ?? 0);
364
402
  entry.offset = [
365
- (a.x - this.sceneAnchor.x) / s,
366
- -(a.y - this.sceneAnchor.y) / s,
367
- (a.z - this.sceneAnchor.z) / s + (entry.heightOffset ?? 0),
403
+ (a.x - this.sceneAnchor.x) / sEast,
404
+ -(a.y - this.sceneAnchor.y) / sNorth,
405
+ (a.z - this.sceneAnchor.z) / sUp + (entry.heightOffset ?? 0),
368
406
  ];
369
407
  entry.root.position.set(entry.offset[0], entry.offset[1], entry.offset[2]);
370
408
  entry.root.updateMatrixWorld(true);
@@ -6,39 +6,151 @@
6
6
  * 这个文件只定义接口与默认实现,不依赖任何宿主:
7
7
  *
8
8
  * - `GeoAdapter` —— 宿主需要提供的两个转换函数
9
- * - `mercatorFromLngLat` / `lngLatFromMercator` —— 标准 Web 墨卡托换算
9
+ * - `mercatorFromLngLat` / `lngLatFromMercator` —— 标准 Web 墨卡托换算(WGS84 椭球)
10
10
  * - `worldFromLngLat` / `lngLatFromWorld` —— 带兜底的封装:
11
11
  * 有 adapter 用 adapter(自动适配 MapLibre v5+ 任意 CRS / 自定义投影),
12
12
  * 没有就平滑回退到标准 Web 墨卡托。
13
+ * - `enuToLngLat` / `lngLatToEnu` —— WGS84 椭球高精度局部坐标互转
13
14
  *
14
15
  * MapLibre / Mapbox 的 adapter 实现见 s3m-maplibre 包的 MapLibreGeoAdapter.ts。
15
16
  */
16
17
 
17
- export const EARTH_RADIUS = 6378137;
18
+ /** WGS84 椭球长半轴 a(米) */
19
+ export const WGS84_A = 6378137.0;
20
+ /** WGS84 椭球扁率 f */
21
+ export const WGS84_F = 1 / 298.257223563;
22
+ /** WGS84 椭球短半轴 b(米) */
23
+ export const WGS84_B = WGS84_A * (1 - WGS84_F);
24
+ /** WGS84 椭球第一偏心率平方 e² = 2f - f² ≈ 0.0066943799901413165 */
25
+ export const WGS84_E2 = 2 * WGS84_F - WGS84_F * WGS84_F;
26
+
27
+ /** 保持向下兼容:EARTH_RADIUS 对应 WGS84 长半轴 */
28
+ export const EARTH_RADIUS = WGS84_A;
29
+
30
+ /**
31
+ * 卯酉圈曲率半径 N(φ) = a / √(1 - e² sin²φ)(米)
32
+ */
33
+ export function primeVerticalRadius(latRad: number): number {
34
+ const sin = Math.sin(latRad);
35
+ return WGS84_A / Math.sqrt(1 - WGS84_E2 * sin * sin);
36
+ }
37
+
38
+ /**
39
+ * 子午圈曲率半径 M(φ) = a(1 - e²) / (1 - e² sin²φ)^(3/2)(米)
40
+ */
41
+ export function meridianRadius(latRad: number): number {
42
+ const sin = Math.sin(latRad);
43
+ const w2 = 1 - WGS84_E2 * sin * sin;
44
+ return (WGS84_A * (1 - WGS84_E2)) / (w2 * Math.sqrt(w2));
45
+ }
46
+
47
+ /**
48
+ * 局部 ENU 米坐标 (east, north) → 经纬度 [lon, lat] (度)
49
+ * 基于 WGS84 椭球中点曲率半径反解,在 100 km 范围内误差 < 1 cm。
50
+ */
51
+ export function enuToLngLat(
52
+ anchorLon: number,
53
+ anchorLat: number,
54
+ east: number,
55
+ north: number
56
+ ): [number, number] {
57
+ const lat0 = (anchorLat * Math.PI) / 180;
58
+ const M0 = meridianRadius(lat0);
59
+ // 中点迭代一步:在数百公里尺度即可将纬度子午线积分误差压到亚毫米
60
+ const dLatApprox = north / M0;
61
+ const latMid = lat0 + dLatApprox * 0.5;
62
+ const M_mid = meridianRadius(latMid);
63
+ const latRad = lat0 + north / M_mid;
64
+
65
+ const N_lat = primeVerticalRadius(latRad);
66
+ const cosLat = Math.cos(latRad);
67
+ const dLon = Math.abs(cosLat) < 1e-12 ? 0 : east / (N_lat * cosLat);
68
+
69
+ return [anchorLon + (dLon * 180) / Math.PI, (latRad * 180) / Math.PI];
70
+ }
71
+
72
+ /**
73
+ * 经纬度 (lon, lat) → 局部 ENU 米坐标 [east, north]
74
+ * enuToLngLat 的逆运算。
75
+ */
76
+ export function lngLatToEnu(
77
+ anchorLon: number,
78
+ anchorLat: number,
79
+ lon: number,
80
+ lat: number
81
+ ): [number, number] {
82
+ const lat0 = (anchorLat * Math.PI) / 180;
83
+ const lat1 = (lat * Math.PI) / 180;
84
+ const latMid = (lat0 + lat1) * 0.5;
85
+ const north = (lat1 - lat0) * meridianRadius(latMid);
86
+
87
+ const N_lat = primeVerticalRadius(lat1);
88
+ const cosLat = Math.cos(lat1);
89
+ const dLon = ((lon - anchorLon) * Math.PI) / 180;
90
+ const east = dLon * N_lat * cosLat;
91
+
92
+ return [east, north];
93
+ }
94
+
95
+ /** 坐标转换结果类型 */
96
+ export interface WorldCoordinateResult {
97
+ x: number;
98
+ y: number;
99
+ z: number;
100
+ /** 东向:1 米 = 多少世界单位 (Web Mercator 东西向尺度 1 / (2π N cosφ)) */
101
+ meterInMercatorEast: number;
102
+ /** 北向:1 米 = 多少世界单位 (Web Mercator 南北向尺度 1 / (2π M cosφ)) */
103
+ meterInMercatorNorth: number;
104
+ /** 竖向(高程):1 米 = 多少世界单位(当前取东向尺度 meterInMercatorEast,以保持局部长方体水平与竖直比例) */
105
+ meterInMercatorUp: number;
106
+ /** 兼容旧代码:等同于 meterInMercatorEast。注意:从原先的正球近似更新为更精准的 WGS84 椭球东向尺度 */
107
+ meterInMercator: number;
108
+ altitude: number;
109
+ }
18
110
 
19
111
  /** 宿主地理坐标系能力:S3M 渲染层用到的就只有这两个方向 */
20
112
  export interface GeoAdapter {
21
- /** 经纬度 + 高程 → 归一化世界坐标,并给出「1 米 = 多少世界单位」 */
113
+ /** 经纬度 + 高程 → 归一化世界坐标,并给出「1 米 = 多少世界单位」(含各向异性 East / North) */
22
114
  worldFromLngLat(
23
115
  lng: number,
24
116
  lat: number,
25
117
  altitude?: number
26
- ): { x: number; y: number; z: number; meterInMercator: number; altitude: number };
118
+ ): WorldCoordinateResult;
27
119
  /** 归一化世界坐标 → 经纬度 [lng, lat] */
28
120
  lngLatFromWorld(x: number, y: number): [number, number];
29
121
  }
30
122
 
31
- /** 经纬度 → 墨卡托归一化坐标(世界为 1×1)—— 默认墨卡托兜底 */
123
+ /** 经纬度 → 墨卡托归一化坐标(世界为 1×1)—— 默认墨卡托兜底(WGS84 椭球曲率半径与各向异性尺度) */
32
124
  export function mercatorFromLngLat(
33
125
  lon: number,
34
126
  lat: number,
35
127
  altitude = 0
36
- ): { x: number; y: number; z: number; meterInMercator: number; altitude: number } {
37
- const sinLat = Math.sin((lat * Math.PI) / 180);
128
+ ): WorldCoordinateResult {
129
+ const latRad = (lat * Math.PI) / 180;
130
+ const sinLat = Math.sin(latRad);
131
+ const cosLat = Math.cos(latRad);
132
+ const safeCosLat = Math.max(Math.abs(cosLat), 1e-12);
133
+
38
134
  const x = (180 + lon) / 360;
39
135
  const y = 0.5 - (0.25 * Math.log((1 + sinLat) / (1 - sinLat))) / Math.PI;
40
- const meterInMercator = 1 / (2 * Math.PI * EARTH_RADIUS) / Math.cos((lat * Math.PI) / 180);
41
- return { x, y, z: altitude * meterInMercator, meterInMercator, altitude };
136
+
137
+ const N = primeVerticalRadius(latRad);
138
+ const M = meridianRadius(latRad);
139
+
140
+ const meterInMercatorEast = 1 / (2 * Math.PI * N * safeCosLat);
141
+ const meterInMercatorNorth = 1 / (2 * Math.PI * M * safeCosLat);
142
+ const meterInMercatorUp = meterInMercatorEast;
143
+
144
+ return {
145
+ x,
146
+ y,
147
+ z: altitude * meterInMercatorUp,
148
+ meterInMercatorEast,
149
+ meterInMercatorNorth,
150
+ meterInMercatorUp,
151
+ meterInMercator: meterInMercatorEast,
152
+ altitude,
153
+ };
42
154
  }
43
155
 
44
156
  /** 墨卡托归一化坐标 → 经纬度 —— 默认墨卡托兜底 */
@@ -58,8 +170,24 @@ export function worldFromLngLat(
58
170
  lon: number,
59
171
  lat: number,
60
172
  altitude = 0
61
- ): { x: number; y: number; z: number; meterInMercator: number; altitude: number } {
62
- if (adapter) return adapter.worldFromLngLat(lon, lat, altitude);
173
+ ): WorldCoordinateResult {
174
+ if (adapter) {
175
+ const res = adapter.worldFromLngLat(lon, lat, altitude);
176
+ const east = res.meterInMercatorEast ?? res.meterInMercator;
177
+ const north = res.meterInMercatorNorth ?? res.meterInMercator ?? east;
178
+ const up = res.meterInMercatorUp ?? east;
179
+ const legacy = res.meterInMercator ?? east;
180
+ return {
181
+ x: res.x,
182
+ y: res.y,
183
+ z: res.z,
184
+ meterInMercatorEast: east,
185
+ meterInMercatorNorth: north,
186
+ meterInMercatorUp: up,
187
+ meterInMercator: legacy,
188
+ altitude: res.altitude,
189
+ };
190
+ }
63
191
  return mercatorFromLngLat(lon, lat, altitude);
64
192
  }
65
193
 
@@ -18,6 +18,8 @@ import { createSharedUniforms, createStyledMaterial } from './StyleMaterial.js';
18
18
  import { isTransparentMaterial, minVertexAlpha } from './materialAlpha.js';
19
19
  import type { StyledMaterial } from './StyleMaterial.js';
20
20
  import { assembleTileGeometry, LINE_PRIMITIVE, POINT_PRIMITIVE } from '../core/TileGeometry.js';
21
+ import { bakeMercatorOffsets } from '../core/BakeMercator.js';
22
+ import { mercatorFromLngLat } from './GeoAdapter.js';
21
23
  import type { TileGeometryData, PrimitiveData } from '../core/TileGeometry.js';
22
24
  import { decodeScale, decodeRotation } from '../core/GaussianSplat.js';
23
25
  import { createSplatMaterial, createQuadGeometry, splatRaycast } from './SplatMaterial.js';
@@ -378,6 +380,15 @@ export interface BuildTileOptions {
378
380
  * 不传 = 各自读模块级默认。同样由图层的渲染 profile 带下来。
379
381
  */
380
382
  textureOptions?: TextureCreateOptions;
383
+ /**
384
+ * 是否烘焙精确 Web 墨卡托偏移属性(消除全球尺度下的视差游动)。
385
+ * 透传给 assembleTileGeometry 和 createStyledMaterial。
386
+ */
387
+ bakeMercator?: boolean;
388
+ /** 瓦片锚点(经纬度),烘焙墨卡托时使用 */
389
+ tileAnchor?: { lon: number; lat: number; height?: number };
390
+ /** 瓦片中心偏移(ENU 米),烘焙墨卡托时使用 */
391
+ tileCenter?: { x: number; y: number; z?: number };
381
392
  }
382
393
 
383
394
  export interface BuildTileResult {
@@ -475,7 +486,8 @@ export async function buildTileObjects(
475
486
  code?: string,
476
487
  hasAtlasRect = false,
477
488
  hasTexCoordZ = false,
478
- vertexAlphaMin = 1
489
+ vertexAlphaMin = 1,
490
+ hasMercator = false
479
491
  ): StyledMaterial => {
480
492
  const entry = materialList.find((e) => materialName(unwrapMaterial(e)) === code);
481
493
  const m = unwrapMaterial(entry);
@@ -546,6 +558,19 @@ export async function buildTileObjects(
546
558
  ? Math.max((m?.alphaCutoff as number) ?? 0, 0.01)
547
559
  : 0;
548
560
 
561
+ let initialInvScale: [number, number, number] | undefined;
562
+ if (options.tileAnchor) {
563
+ const tileMerc = mercatorFromLngLat(
564
+ options.tileAnchor.lon,
565
+ options.tileAnchor.lat,
566
+ options.tileAnchor.height
567
+ );
568
+ const sEast = tileMerc.meterInMercatorEast ?? tileMerc.meterInMercator;
569
+ const sNorth = tileMerc.meterInMercatorNorth ?? tileMerc.meterInMercator;
570
+ const sUp = tileMerc.meterInMercatorUp ?? sEast;
571
+ initialInvScale = [1.0 / sEast, 1.0 / sNorth, 1.0 / sUp];
572
+ }
573
+
549
574
  const mat = createStyledMaterial({
550
575
  uniforms,
551
576
  map,
@@ -562,6 +587,9 @@ export async function buildTileObjects(
562
587
  // 图层 / 数据集的档位;undefined 时 createStyledMaterial 自己回落到模块级默认
563
588
  materialQuality: options.materialQuality,
564
589
  colorPipeline: options.colorPipeline,
590
+ bakeMercator: hasMercator || options.bakeMercator,
591
+ tilePivot: options.tileCenter ? new THREE.Vector3(options.tileCenter.x, options.tileCenter.y, options.tileCenter.z) : undefined,
592
+ invTileScale: initialInvScale ? new THREE.Vector3(...initialInvScale) : undefined,
565
593
  });
566
594
  // 用户配置的深度偏移(共面摩尔纹 / 重合面归属的人工定序,见 BuildTileOptions 注释)
567
595
  if (options.polygonOffset) {
@@ -574,11 +602,32 @@ export async function buildTileObjects(
574
602
 
575
603
  // 3) 纯数据几何组装:Worker 解析时已顺带产出就直接用,否则主线程现场组装
576
604
  // (core/TileGeometry 的同一份实现,两条路径产物一致)
605
+ if (options.assembled && options.bakeMercator && options.tileAnchor) {
606
+ for (const prim of options.assembled.primitives) {
607
+ if (!prim.attributes.aMercatorOffset && prim.attributes.position) {
608
+ const offsets = bakeMercatorOffsets(
609
+ prim.attributes.position.array,
610
+ prim.vertexCount,
611
+ options.tileAnchor,
612
+ options.tileCenter ?? { x: 0, y: 0, z: 0 }
613
+
614
+ );
615
+ prim.attributes.aMercatorOffset = {
616
+ array: offsets,
617
+ components: 3,
618
+ normalized: false,
619
+ };
620
+ }
621
+ }
622
+ }
577
623
  const assembled =
578
624
  options.assembled ??
579
625
  assembleTileGeometry(tile, {
580
626
  batching: options.batching !== false,
581
627
  dedupeCoincident: options.dedupeCoincident,
628
+ bakeMercator: options.bakeMercator,
629
+ tileAnchor: options.tileAnchor,
630
+ tileCenter: options.tileCenter,
582
631
  });
583
632
  stats.vertices = assembled.stats.vertices;
584
633
  stats.edgeLines = assembled.stats.edgeLines;
@@ -610,11 +659,13 @@ export async function buildTileObjects(
610
659
  const geometry = packPrimitive(prim);
611
660
  const geoRef = tile.geoPackage[prim.geoNames[0]] ?? null;
612
661
  const vp = geoRef?.vertexPackage;
662
+ const hasMercator = !!prim.attributes.aMercatorOffset;
613
663
  const material = makeMaterial(
614
664
  prim.passNames[0],
615
665
  !!prim.attributes.aAtlasRect,
616
666
  !!prim.attributes.aTexCoordZ,
617
- minVertexAlpha(prim.attributes.aColor)
667
+ minVertexAlpha(prim.attributes.aColor),
668
+ hasMercator
618
669
  );
619
670
  const matrix =
620
671
  prim.matrix && prim.matrix.length === 16 ? new THREE.Matrix4().fromArray(prim.matrix) : null;