s3m-three 0.2.0 → 0.3.1

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 (114) hide show
  1. package/README.md +2 -2
  2. package/dist/browser/s3m-three.iife.ext-three.js +585 -0
  3. package/dist/browser/s3m-three.iife.js +4419 -0
  4. package/dist/browser.d.ts +3 -0
  5. package/dist/browser.d.ts.map +1 -0
  6. package/dist/browser.js +21 -0
  7. package/dist/browser.js.map +1 -0
  8. package/dist/core/AtlasAlpha.d.ts +31 -0
  9. package/dist/core/AtlasAlpha.d.ts.map +1 -0
  10. package/dist/core/AtlasAlpha.js +82 -0
  11. package/dist/core/AtlasAlpha.js.map +1 -0
  12. package/dist/core/AtlasPack.d.ts +57 -0
  13. package/dist/core/AtlasPack.d.ts.map +1 -0
  14. package/dist/core/AtlasPack.js +81 -0
  15. package/dist/core/AtlasPack.js.map +1 -0
  16. package/dist/core/AtlasUpgrade.d.ts +68 -0
  17. package/dist/core/AtlasUpgrade.d.ts.map +1 -0
  18. package/dist/core/AtlasUpgrade.js +154 -0
  19. package/dist/core/AtlasUpgrade.js.map +1 -0
  20. package/dist/core/DXTTextureDecode.d.ts.map +1 -1
  21. package/dist/core/DXTTextureDecode.js +28 -6
  22. package/dist/core/DXTTextureDecode.js.map +1 -1
  23. package/dist/core/Draco.d.ts +52 -0
  24. package/dist/core/Draco.d.ts.map +1 -0
  25. package/dist/core/Draco.js +323 -0
  26. package/dist/core/Draco.js.map +1 -0
  27. package/dist/core/MeshOpt.d.ts +14 -3
  28. package/dist/core/MeshOpt.d.ts.map +1 -1
  29. package/dist/core/MeshOpt.js +101 -18
  30. package/dist/core/MeshOpt.js.map +1 -1
  31. package/dist/core/S3M1Parser.d.ts +16 -0
  32. package/dist/core/S3M1Parser.d.ts.map +1 -0
  33. package/dist/core/S3M1Parser.js +367 -0
  34. package/dist/core/S3M1Parser.js.map +1 -0
  35. package/dist/core/S3MBParser.d.ts +43 -1
  36. package/dist/core/S3MBParser.d.ts.map +1 -1
  37. package/dist/core/S3MBParser.js +101 -23
  38. package/dist/core/S3MBParser.js.map +1 -1
  39. package/dist/core/S3MBlockParser.d.ts +209 -0
  40. package/dist/core/S3MBlockParser.d.ts.map +1 -0
  41. package/dist/core/S3MBlockParser.js +789 -0
  42. package/dist/core/S3MBlockParser.js.map +1 -0
  43. package/dist/core/TextureFormat.d.ts +0 -1
  44. package/dist/core/TextureFormat.d.ts.map +1 -1
  45. package/dist/core/TextureFormat.js +19 -2
  46. package/dist/core/TextureFormat.js.map +1 -1
  47. package/dist/core/types.d.ts +65 -0
  48. package/dist/core/types.d.ts.map +1 -1
  49. package/dist/index.d.ts +2 -1
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +2 -1
  52. package/dist/index.js.map +1 -1
  53. package/dist/render/DatasetManager.d.ts.map +1 -1
  54. package/dist/render/DatasetManager.js +5 -3
  55. package/dist/render/DatasetManager.js.map +1 -1
  56. package/dist/render/GeometryBuilder.d.ts +2 -0
  57. package/dist/render/GeometryBuilder.d.ts.map +1 -1
  58. package/dist/render/GeometryBuilder.js +185 -16
  59. package/dist/render/GeometryBuilder.js.map +1 -1
  60. package/dist/render/GroundManager.d.ts +57 -1
  61. package/dist/render/GroundManager.d.ts.map +1 -1
  62. package/dist/render/GroundManager.js +149 -21
  63. package/dist/render/GroundManager.js.map +1 -1
  64. package/dist/render/Picker.d.ts.map +1 -1
  65. package/dist/render/Picker.js +12 -4
  66. package/dist/render/Picker.js.map +1 -1
  67. package/dist/render/PointCloudMaterial.d.ts +36 -0
  68. package/dist/render/PointCloudMaterial.d.ts.map +1 -0
  69. package/dist/render/PointCloudMaterial.js +159 -0
  70. package/dist/render/PointCloudMaterial.js.map +1 -0
  71. package/dist/render/S3MViewer.d.ts +19 -0
  72. package/dist/render/S3MViewer.d.ts.map +1 -1
  73. package/dist/render/S3MViewer.js +143 -22
  74. package/dist/render/S3MViewer.js.map +1 -1
  75. package/dist/render/StyleMaterial.d.ts +6 -0
  76. package/dist/render/StyleMaterial.d.ts.map +1 -1
  77. package/dist/render/StyleMaterial.js +125 -2
  78. package/dist/render/StyleMaterial.js.map +1 -1
  79. package/dist/render/TextureFactory.d.ts.map +1 -1
  80. package/dist/render/TextureFactory.js +64 -10
  81. package/dist/render/TextureFactory.js.map +1 -1
  82. package/dist/tileset/S3MTileset.d.ts +152 -1
  83. package/dist/tileset/S3MTileset.d.ts.map +1 -1
  84. package/dist/tileset/S3MTileset.js +672 -54
  85. package/dist/tileset/S3MTileset.js.map +1 -1
  86. package/dist/vendor/crunch_decoder.module.js +12 -18
  87. package/dist/vendor/draco_decoder.module.js +3178 -0
  88. package/docs/API.md +112 -11
  89. package/package.json +5 -3
  90. package/src/browser.ts +21 -0
  91. package/src/core/AtlasAlpha.ts +82 -0
  92. package/src/core/AtlasPack.ts +127 -0
  93. package/src/core/AtlasUpgrade.ts +182 -0
  94. package/src/core/DXTTextureDecode.ts +28 -6
  95. package/src/core/Draco.ts +409 -0
  96. package/src/core/MeshOpt.ts +124 -18
  97. package/src/core/S3M1Parser.ts +430 -0
  98. package/src/core/S3MBParser.ts +112 -21
  99. package/src/core/S3MBlockParser.ts +953 -0
  100. package/src/core/TextureFormat.ts +20 -2
  101. package/src/core/types.ts +63 -0
  102. package/src/index.ts +2 -1
  103. package/src/render/DatasetManager.ts +5 -5
  104. package/src/render/GeometryBuilder.ts +207 -15
  105. package/src/render/GroundManager.ts +174 -20
  106. package/src/render/Picker.ts +13 -4
  107. package/src/render/PointCloudMaterial.ts +174 -0
  108. package/src/render/S3MViewer.ts +140 -24
  109. package/src/render/StyleMaterial.ts +135 -2
  110. package/src/render/TextureFactory.ts +67 -11
  111. package/src/tileset/S3MTileset.ts +704 -59
  112. package/src/vendor/crunch_decoder.module.js +12 -18
  113. package/src/vendor/draco_decoder.module.d.ts +128 -0
  114. package/src/vendor/draco_decoder.module.js +3178 -0
@@ -9,6 +9,12 @@
9
9
  * - 地表相交线(Z = 0):出入口穿透地表处有清晰物理交线。
10
10
  * 3. 支持「基坑开挖模式(Excavate)」与「地下透视模式(Translucent)」,
11
11
  * 可开辟虚拟基坑井口或全域透视地表。
12
+ *
13
+ * 关于「遮挡」:地表能不能挡住建筑,只取决于有没有一块**先于建筑绘制、并且写了深度**的
14
+ * 几何进到同一深度缓冲里。半透明覆盖网格 `depthWrite = false` 且排在 transparent 队列末尾,
15
+ * 它只是画在上面的一层色,不参与遮挡;真正的遮挡由独立的**纯深度代理**(`_depthMesh`,
16
+ * `colorWrite = false` + `renderOrder` 负值)负责,可用 `occlude` 开关控制。
17
+ * 与 MapLibre terrain 共用同一深度缓冲,两者是「深度来源」的关系而非替代关系。
12
18
  */
13
19
  import * as THREE from 'three';
14
20
 
@@ -28,6 +34,28 @@ export interface GroundOptions {
28
34
  excavationBounds?: [number, number, number, number] | null;
29
35
  /** 地表范围大小(米,默认 6000) */
30
36
  size?: number;
37
+ /**
38
+ * 地表高度是否跟随主数据集的抬升量(`setDatasetHeight`),默认 `true`。
39
+ *
40
+ * `true` 服务贴地工作流:模型整体抬到地面高程时,切割面必须跟着抬到同一高程,
41
+ * 否则整个模型会被齐根切掉。
42
+ * `false` 把地表钉死在当前高度(默认场景 z=0):高程调整只动模型不动切割面,
43
+ * 滑杆变成直观的「控制埋深 / 切掉多少」。
44
+ */
45
+ trackHeight?: boolean;
46
+ /**
47
+ * 地表是否作为深度遮挡源:开启后,位于地表以下的几何(地下站厅 / 隧道 / 管线)
48
+ * 会被地表真正切掉,而不是透过半透明地表显露。
49
+ *
50
+ * **与 mode 解耦**:`mode: 'none'` 时也可以单独开启 —— 不显示任何地表,
51
+ * 只留一块隐形地面把地面以下切掉。默认值取 `mode === 'excavate'`
52
+ * (开挖模式下坑外的地下部分本就该被土体挡住),`'translucent'` 下默认关闭
53
+ * —— 那一模式的全部意义就是「能看穿地表」。
54
+ *
55
+ * 实现上由一块 `colorWrite = false`、排在 opaque 队列最前的纯深度代理完成,
56
+ * 与 MapLibre terrain 写入的深度共用同一个深度缓冲,互不冲突(谁更近谁赢)。
57
+ */
58
+ occlude?: boolean;
31
59
  }
32
60
 
33
61
  export class GroundManager {
@@ -40,10 +68,20 @@ export class GroundManager {
40
68
  size: number;
41
69
  /** 物理地表网格的高度(米,场景 ENU Z 坐标),默认 0 */
42
70
  groundHeight = 0;
71
+ /** 地表是否作为深度遮挡源(见 `GroundOptions.occlude`) */
72
+ occlude: boolean;
73
+ /** 地表高度是否跟随主数据集抬升量(见 `GroundOptions.trackHeight`),默认 true */
74
+ trackHeight: boolean;
43
75
 
44
76
  readonly group: THREE.Group;
45
77
  private _mesh: THREE.Mesh | null = null;
46
78
  private _material: THREE.ShaderMaterial | null = null;
79
+ /** 纯深度代理:只写深度不写颜色,负责真正的遮挡 */
80
+ private _depthMesh: THREE.Mesh | null = null;
81
+ private _depthMaterial: THREE.ShaderMaterial | null = null;
82
+ private _geom: THREE.PlaneGeometry | null = null;
83
+ /** 用户显式指定过 occlude 后,setMode 不再自动改写它 */
84
+ private _occludeExplicit: boolean;
47
85
 
48
86
  constructor(options: GroundOptions = {}) {
49
87
  this.mode = options.mode ?? 'none';
@@ -53,14 +91,21 @@ export class GroundManager {
53
91
  this.gridSize = options.gridSize ?? 10.0;
54
92
  this.excavationBounds = options.excavationBounds ?? null;
55
93
  this.size = options.size ?? 6000;
94
+ this._occludeExplicit = options.occlude !== undefined;
95
+ this.occlude = options.occlude ?? this.mode === 'excavate';
96
+ this.trackHeight = options.trackHeight ?? true;
56
97
 
57
98
  this.group = new THREE.Group();
58
99
  this.group.name = 's3m-ground-surface';
59
100
  this._initMesh();
101
+ // 构造时就按当前 mode 应用一次:以前 depthWrite / 可见性只在 setMode 里改,
102
+ // 用 `{ mode: 'excavate' }` 建出来的实例会一直停在 depthWrite = false。
103
+ this._applyMode();
60
104
  }
61
105
 
62
106
  private _initMesh(): void {
63
107
  const geom = new THREE.PlaneGeometry(this.size, this.size, 1, 1);
108
+ this._geom = geom;
64
109
 
65
110
  // ENU 坐标系下,Z 为天(Up),PlaneGeometry 默认在 XY 平面上,正常法线正是 (0, 0, 1)!
66
111
  const material = new THREE.ShaderMaterial({
@@ -146,15 +191,96 @@ export class GroundManager {
146
191
 
147
192
  this._mesh = mesh;
148
193
  this._material = material;
149
- this.group.visible = this.mode !== 'none';
194
+
195
+ // ---- 纯深度代理 ----
196
+ // 遮挡是「片元绘制那一刻」由深度测试决定的:覆盖网格排在 transparent 队列末尾,
197
+ // 房子早就画完了,它写不写深度都挡不住任何东西。所以单独放一块只写深度、
198
+ // 不写颜色、排进 opaque 队列最前的孪生网格,让后续所有片元在它后面被裁掉。
199
+ const depthMaterial = new THREE.ShaderMaterial({
200
+ // 关掉颜色写入但仍走 depthWrite ⇒ 只留一层「隐形墙」
201
+ colorWrite: false,
202
+ depthWrite: true,
203
+ depthTest: true,
204
+ transparent: false, // 必须是 opaque,才能靠 renderOrder 抢到全场景最前
205
+ // 只挡「从上方看下去」的方向:相机钻到地下时不该被自己这层地表糊住
206
+ side: THREE.FrontSide,
207
+ uniforms: {
208
+ // 基坑开挖时,坑口是「挖开的地方」,地下必须还能看见 ⇒ 代理也要在这里开洞
209
+ uPunchPit: { value: 0 },
210
+ uHasExcavation: { value: 0 },
211
+ uExcavationMin: { value: new THREE.Vector2(-9999, -9999) },
212
+ uExcavationMax: { value: new THREE.Vector2(9999, 9999) },
213
+ },
214
+ vertexShader: /* glsl */ `
215
+ varying vec2 vEnuPos;
216
+ void main() {
217
+ vec4 worldPos = modelMatrix * vec4(position, 1.0);
218
+ vEnuPos = worldPos.xy;
219
+ gl_Position = projectionMatrix * viewMatrix * worldPos;
220
+ }
221
+ `,
222
+ fragmentShader: /* glsl */ `
223
+ uniform int uPunchPit;
224
+ uniform int uHasExcavation;
225
+ uniform vec2 uExcavationMin;
226
+ uniform vec2 uExcavationMax;
227
+ varying vec2 vEnuPos;
228
+ void main() {
229
+ if (uPunchPit == 1 && uHasExcavation == 1 &&
230
+ vEnuPos.x >= uExcavationMin.x && vEnuPos.x <= uExcavationMax.x &&
231
+ vEnuPos.y >= uExcavationMin.y && vEnuPos.y <= uExcavationMax.y) {
232
+ discard;
233
+ }
234
+ gl_FragColor = vec4(1.0);
235
+ }
236
+ `,
237
+ });
238
+
239
+ const depthMesh = new THREE.Mesh(geom, depthMaterial);
240
+ // 比覆盖网格低 2cm:避免与它等深度打架(覆盖层 LEQUAL 时仍能画上去)
241
+ depthMesh.position.set(0, 0, -0.02);
242
+ depthMesh.renderOrder = -1000;
243
+ depthMesh.name = 's3m-ground-depth';
244
+ depthMesh.visible = false;
245
+ this.group.add(depthMesh);
246
+
247
+ this._depthMesh = depthMesh;
248
+ this._depthMaterial = depthMaterial;
249
+
150
250
  this._syncUniforms();
151
251
  }
152
252
 
153
253
  /**
154
- * 每帧根据相机位置更新地表网格中心,实现地表随相机平移而无限延伸且网格线不漂移
254
+ * 把 mode / occlude 的状态一次性刷到材质和可见性上。
255
+ * 构造与 setMode / setOcclude 都走这里,避免出现「构造时漏设」的分叉。
256
+ *
257
+ * 覆盖网格与深度代理**各自独立控制**:
258
+ * - 覆盖网格(视觉)只跟 mode 走;
259
+ * - 深度代理(遮挡)只跟 occlude 走 —— mode 'none' 时也可以单独开一块
260
+ * 「隐形地面」把地面以下切掉,不显示任何地表。
261
+ */
262
+ private _applyMode(): void {
263
+ if (this._mesh) this._mesh.visible = this.mode !== 'none';
264
+ if (this._material) {
265
+ this._material.uniforms.uMode.value = this.mode === 'excavate' ? 2 : this.mode === 'translucent' ? 1 : 0;
266
+ // 覆盖网格排在最后,写深度的意义已经由代理接管,这里保留 excavate 的历史行为
267
+ this._material.depthWrite = this.mode === 'excavate';
268
+ }
269
+ if (this._depthMesh) {
270
+ this._depthMesh.visible = this.occlude;
271
+ if (this._depthMaterial) {
272
+ // 只有开挖模式才在坑口开洞;translucent / none + occlude 是「一刀切」语义
273
+ this._depthMaterial.uniforms.uPunchPit.value = this.mode === 'excavate' ? 1 : 0;
274
+ }
275
+ }
276
+ }
277
+
278
+ /**
279
+ * 每帧根据相机位置更新地表网格中心,实现地表随相机平移而无限延伸且网格线不漂移。
280
+ * 遮挡代理与地表模式已解耦,所以 mode 'none' 时也要跟着相机走。
155
281
  */
156
282
  update(camera?: THREE.Camera | null): void {
157
- if (!camera || this.mode === 'none') return;
283
+ if (!camera) return;
158
284
  this.group.position.set(camera.position.x, camera.position.y, this.groundHeight);
159
285
  }
160
286
 
@@ -171,11 +297,21 @@ export class GroundManager {
171
297
 
172
298
  setMode(mode: GroundMode): void {
173
299
  this.mode = mode;
174
- this.group.visible = mode !== 'none';
175
- if (this._material) {
176
- this._material.uniforms.uMode.value = mode === 'excavate' ? 2 : mode === 'translucent' ? 1 : 0;
177
- this._material.depthWrite = mode === 'excavate';
178
- }
300
+ // 没显式要过 occlude 的,跟着模式走:开挖模式才遮挡
301
+ if (!this._occludeExplicit) this.occlude = mode === 'excavate';
302
+ this._applyMode();
303
+ }
304
+
305
+ /**
306
+ * 开关「地表作为深度遮挡源」。
307
+ *
308
+ * 打开后地表以下的几何会被真正切掉(不再透过地表显露);关闭后回到纯视觉覆盖。
309
+ * 与宿主是否开启 MapLibre terrain 无关:两者都在同一个深度缓冲里比深度,谁更近谁赢。
310
+ */
311
+ setOcclude(enabled: boolean): void {
312
+ this.occlude = enabled;
313
+ this._occludeExplicit = true;
314
+ this._applyMode();
179
315
  }
180
316
 
181
317
  setOpacity(opacity: number): void {
@@ -237,22 +373,40 @@ export class GroundManager {
237
373
  }
238
374
 
239
375
  private _syncUniforms(): void {
240
- if (!this._material) return;
241
- const u = this._material.uniforms;
242
- if (this.excavationBounds) {
243
- u.uHasExcavation.value = 1;
244
- u.uExcavationMin.value.set(this.excavationBounds[0], this.excavationBounds[1]);
245
- u.uExcavationMax.value.set(this.excavationBounds[2], this.excavationBounds[3]);
246
- } else {
247
- u.uHasExcavation.value = 0;
376
+ const bounds = this.excavationBounds;
377
+ if (this._material) {
378
+ const u = this._material.uniforms;
379
+ if (bounds) {
380
+ u.uHasExcavation.value = 1;
381
+ u.uExcavationMin.value.set(bounds[0], bounds[1]);
382
+ u.uExcavationMax.value.set(bounds[2], bounds[3]);
383
+ } else {
384
+ u.uHasExcavation.value = 0;
385
+ }
386
+ }
387
+ // 深度代理要和覆盖网格用同一套基坑参数,否则坑口会出现「看得见色块、却被挡住地下」
388
+ if (this._depthMaterial) {
389
+ const d = this._depthMaterial.uniforms;
390
+ if (bounds) {
391
+ d.uHasExcavation.value = 1;
392
+ d.uExcavationMin.value.set(bounds[0], bounds[1]);
393
+ d.uExcavationMax.value.set(bounds[2], bounds[3]);
394
+ } else {
395
+ d.uHasExcavation.value = 0;
396
+ }
248
397
  }
249
398
  }
250
399
 
251
400
  dispose(): void {
252
- if (this._mesh) {
253
- this._mesh.geometry.dispose();
254
- this._material?.dispose();
255
- }
401
+ // 两块网格共用同一份 geometry,只释放一次
402
+ this._geom?.dispose();
403
+ this._geom = null;
404
+ this._material?.dispose();
405
+ this._material = null;
406
+ this._depthMaterial?.dispose();
407
+ this._depthMaterial = null;
408
+ this._mesh = null;
409
+ this._depthMesh = null;
256
410
  this.group.clear();
257
411
  }
258
412
  }
@@ -68,6 +68,9 @@ const PICK_VERT = /* glsl */ `
68
68
  #else
69
69
  gl_Position = projectionMatrix * modelViewMatrix * vec4( p, 1.0 );
70
70
  #endif
71
+ // 点云也用这条着色器:不写 gl_PointSize 时光栅化出来只有 1px,
72
+ // 跟显示尺寸(2px)不匹配,点云会很难点中。对 Mesh 图元这行被忽略。
73
+ gl_PointSize = 2.0;
71
74
  }
72
75
  `;
73
76
 
@@ -131,6 +134,11 @@ function createPickMaterial(objectId: number): THREE.ShaderMaterial {
131
134
  });
132
135
  }
133
136
 
137
+ /** 点云是 THREE.Points 不是 Mesh,拾取链路要放行 */
138
+ function isPointsObject(o: THREE.Object3D): o is THREE.Points {
139
+ return (o as unknown as THREE.Points).isPoints === true;
140
+ }
141
+
134
142
  export class Picker {
135
143
  readonly renderer: THREE.WebGLRenderer;
136
144
  /** 拾取用的渲染目标边长(像素),默认 1 —— 只要点击点那一格 */
@@ -194,12 +202,13 @@ export class Picker {
194
202
  const prevClearAlpha = renderer.getClearAlpha();
195
203
 
196
204
  // ---- 换材质
197
- const swapped: Array<[THREE.Mesh, THREE.Material | THREE.Material[]]> = [];
205
+ const swapped: Array<[THREE.Object3D, THREE.Material | THREE.Material[]]> = [];
198
206
  /** 这次拾取里哪些网格是实例化的(它们的 vBatch 编码的是实例序号) */
199
207
  const instancedPickIds = new Set<number>();
200
208
  scene.traverseVisible((o) => {
201
209
  const mesh = o as THREE.Mesh;
202
- if (!mesh.isMesh) return;
210
+ // 点云是 THREE.Points,同样要参与拾取
211
+ if (!mesh.isMesh && !isPointsObject(mesh)) return;
203
212
  const ud = mesh.userData as PickUserData;
204
213
  if (ud.pickId === undefined) return;
205
214
  if (!ud.pickMaterial) ud.pickMaterial = createPickMaterial(ud.pickId);
@@ -236,7 +245,7 @@ export class Picker {
236
245
  const hidden: THREE.Object3D[] = [];
237
246
  scene.traverseVisible((o) => {
238
247
  const mesh = o as THREE.Mesh;
239
- if (mesh.isMesh && (mesh.userData as PickUserData).pickId === undefined) {
248
+ if ((mesh.isMesh || isPointsObject(mesh)) && (mesh.userData as PickUserData).pickId === undefined) {
240
249
  hidden.push(mesh);
241
250
  mesh.visible = false;
242
251
  }
@@ -272,7 +281,7 @@ export class Picker {
272
281
  : { objectId, batchId: rawId };
273
282
  }
274
283
  } finally {
275
- for (const [mesh, mat] of swapped) mesh.material = mat;
284
+ for (const [obj, mat] of swapped) (obj as THREE.Mesh).material = mat;
276
285
  for (const o of hidden) o.visible = true;
277
286
  renderer.setRenderTarget(prevTarget);
278
287
  renderer.setViewport(prevViewport);
@@ -0,0 +1,174 @@
1
+ /**
2
+ * 点云着色
3
+ *
4
+ * 点云没有法线、没有 uv、没有楼层概念,所以 StyleMaterial 那套按面着色的模式
5
+ * (顶/侧分色、按楼层、动态扫描…)对 `THREE.Points` 天然不适用。这里只做三种
6
+ * 点云真正能表达的模式,其余一律回退顶点色:
7
+ *
8
+ * vertex 原色:顶点色上屏(Draco 点云在解析阶段已把分类码烘成分类色带)
9
+ * solid 单色:整个点云一个颜色
10
+ * height 按高度:顶点世界坐标 z 在 uColorA → uColorB 之间连续渐变
11
+ * selected 选中高亮:整块点云混成 uSelColor(点云没有单体粒度,只能整块亮)
12
+ *
13
+ * 两个设计要点:
14
+ * 1. height / selected 的 ShaderMaterial 直接**引用**全局共享 uniform 对象
15
+ * (`shared.uColorA` 等),所以 `_applyStyleUniforms()` 改一次颜色,
16
+ * 点云渐变自动跟着变,不需要额外同步。
17
+ * 2. 材质由本模块按 mode 创建并打上 `pcOwned` 标记,换 mode 时旧的自己
18
+ * 销毁;GeometryBuilder 里那个共享单例没有标记,替换时不碰它。
19
+ */
20
+ import * as THREE from 'three';
21
+ import { STYLE, type S3MStyle, type StyleMode } from './StyleMaterial.js';
22
+ import type { StyleUniforms } from '../core/types.js';
23
+
24
+ /** 点云实际持有的材质形态 */
25
+ export type PointCloudKind = 'vertex' | 'solid' | 'height' | 'selected';
26
+
27
+ /** 与 GeometryBuilder 里共享 PointsMaterial 保持同一个点尺寸(屏幕像素,不衰减) */
28
+ const POINT_SIZE = 2;
29
+
30
+ /** S3M 着色模式 → 点云形态。不适用于点云的模式统一回退顶点色。 */
31
+ export function pointCloudKindOf(mode: StyleMode): 'vertex' | 'solid' | 'height' {
32
+ if (mode === STYLE.SOLID) return 'solid';
33
+ if (mode === STYLE.HEIGHT) return 'height';
34
+ return 'vertex';
35
+ }
36
+
37
+ /* ------------------------------------------------------------------ 着色器 */
38
+
39
+ const POINT_VERT = /* glsl */ `
40
+ uniform float uPointSize;
41
+ varying float vWorldZ;
42
+ void main() {
43
+ vec4 world = modelMatrix * vec4( position, 1.0 );
44
+ vWorldZ = world.z;
45
+ gl_Position = projectionMatrix * viewMatrix * world;
46
+ gl_PointSize = uPointSize;
47
+ }
48
+ `;
49
+
50
+ const HEIGHT_FRAG = /* glsl */ `
51
+ uniform vec3 uColorA;
52
+ uniform vec3 uColorB;
53
+ uniform float uHeightMin;
54
+ uniform float uHeightMax;
55
+ uniform float uOpacity;
56
+ varying float vWorldZ;
57
+ void main() {
58
+ float h = clamp( ( vWorldZ - uHeightMin ) / max( uHeightMax - uHeightMin, 0.001 ), 0.0, 1.0 );
59
+ gl_FragColor = vec4( mix( uColorA, uColorB, h ), uOpacity );
60
+ }
61
+ `;
62
+
63
+ const SELECTED_FRAG = /* glsl */ `
64
+ uniform vec3 uSelColor;
65
+ uniform float uOpacity;
66
+ void main() {
67
+ gl_FragColor = vec4( uSelColor, uOpacity );
68
+ }
69
+ `;
70
+
71
+ function createPointCloudMaterial(
72
+ kind: PointCloudKind,
73
+ style: S3MStyle,
74
+ shared: StyleUniforms
75
+ ): THREE.Material {
76
+ let mat: THREE.Material;
77
+ if (kind === 'height') {
78
+ mat = new THREE.ShaderMaterial({
79
+ uniforms: {
80
+ // 直接引用共享 uniform 对象:换色不用重建材质
81
+ uColorA: shared.uColorA,
82
+ uColorB: shared.uColorB,
83
+ uHeightMin: shared.uHeightMin,
84
+ uHeightMax: shared.uHeightMax,
85
+ uPointSize: { value: POINT_SIZE },
86
+ uOpacity: { value: style.opacity ?? 1 },
87
+ },
88
+ vertexShader: POINT_VERT,
89
+ fragmentShader: HEIGHT_FRAG,
90
+ transparent: (style.opacity ?? 1) < 0.999,
91
+ depthWrite: (style.opacity ?? 1) >= 0.999,
92
+ });
93
+ } else if (kind === 'selected') {
94
+ mat = new THREE.ShaderMaterial({
95
+ uniforms: {
96
+ uSelColor: shared.uSelColor,
97
+ uPointSize: { value: POINT_SIZE },
98
+ uOpacity: { value: style.opacity ?? 1 },
99
+ },
100
+ vertexShader: POINT_VERT,
101
+ fragmentShader: SELECTED_FRAG,
102
+ transparent: (style.opacity ?? 1) < 0.999,
103
+ depthWrite: (style.opacity ?? 1) >= 0.999,
104
+ });
105
+ } else if (kind === 'solid') {
106
+ mat = new THREE.PointsMaterial({
107
+ size: POINT_SIZE,
108
+ sizeAttenuation: false,
109
+ color: new THREE.Color(style.colorA),
110
+ transparent: (style.opacity ?? 1) < 0.999,
111
+ opacity: style.opacity ?? 1,
112
+ depthWrite: (style.opacity ?? 1) >= 0.999,
113
+ });
114
+ } else {
115
+ mat = new THREE.PointsMaterial({
116
+ size: POINT_SIZE,
117
+ sizeAttenuation: false,
118
+ vertexColors: true,
119
+ transparent: (style.opacity ?? 1) < 0.999,
120
+ opacity: style.opacity ?? 1,
121
+ depthWrite: (style.opacity ?? 1) >= 0.999,
122
+ });
123
+ }
124
+ (mat.userData as { pcOwned?: boolean }).pcOwned = true;
125
+ return mat;
126
+ }
127
+
128
+ /**
129
+ * 把点云对象的材质同步到当前样式。
130
+ *
131
+ * kind 没变就只改不透明度 / 单色色值,不重建材质;变了才换,旧的自己销毁。
132
+ * 每次 setStyle 都会走到这里,代价是一次属性比较。
133
+ */
134
+ export function applyPointCloudStyle(
135
+ points: THREE.Points,
136
+ style: S3MStyle,
137
+ shared: StyleUniforms,
138
+ selected = false
139
+ ): void {
140
+ const ud = points.userData as { pcKind?: PointCloudKind; pcTransparent?: boolean };
141
+ const want: PointCloudKind = selected ? 'selected' : pointCloudKindOf(style.mode);
142
+
143
+ if (ud.pcKind !== want) {
144
+ const prev = points.material as THREE.Material | undefined;
145
+ points.material = createPointCloudMaterial(want, style, shared);
146
+ // 只销毁自己创建的;GeometryBuilder 的共享单例没有 pcOwned 标记,不能碰
147
+ if (prev && (prev.userData as { pcOwned?: boolean } | undefined)?.pcOwned) prev.dispose();
148
+ ud.pcKind = want;
149
+ ud.pcTransparent = undefined;
150
+ } else if (want === 'solid') {
151
+ (points.material as THREE.PointsMaterial).color?.set(style.colorA);
152
+ }
153
+
154
+ const o = Math.min(1, Math.max(0.05, style.opacity ?? 1));
155
+ const transparent = o < 0.999;
156
+ const keepDepth = style.depthWrite !== false;
157
+ const mat = points.material as THREE.Material & { uniforms?: Record<string, { value: unknown }> };
158
+ mat.transparent = transparent;
159
+ mat.opacity = o;
160
+ mat.depthWrite = transparent ? keepDepth : true;
161
+ // ShaderMaterial 的 .opacity 不参与渲染,透明度走自己的 uniform
162
+ if (mat.uniforms?.uOpacity) mat.uniforms.uOpacity.value = o;
163
+ // 只有混合开关真变了才标记重编译,避免每次 setStyle 都重编一遍
164
+ if (ud.pcTransparent !== transparent) {
165
+ ud.pcTransparent = transparent;
166
+ mat.needsUpdate = true;
167
+ }
168
+ }
169
+
170
+ /** 点云对象是否由本模块持有材质(卸载时用来决定要不要 dispose) */
171
+ export function isOwnedPointCloudMaterial(mat: THREE.Material | THREE.Material[] | undefined): boolean {
172
+ const m = Array.isArray(mat) ? mat[0] : mat;
173
+ return !!m && (m.userData as { pcOwned?: boolean } | undefined)?.pcOwned === true;
174
+ }