s3m-three 0.1.0 → 0.2.0

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 (44) hide show
  1. package/LICENSE +202 -202
  2. package/dist/render/DatasetManager.d.ts +10 -3
  3. package/dist/render/DatasetManager.d.ts.map +1 -1
  4. package/dist/render/DatasetManager.js +14 -5
  5. package/dist/render/DatasetManager.js.map +1 -1
  6. package/dist/render/EdgeLines.js +46 -46
  7. package/dist/render/GroundManager.d.ts +9 -0
  8. package/dist/render/GroundManager.d.ts.map +1 -1
  9. package/dist/render/GroundManager.js +13 -1
  10. package/dist/render/GroundManager.js.map +1 -1
  11. package/dist/render/Picker.js +66 -66
  12. package/dist/render/S3MViewer.d.ts +11 -3
  13. package/dist/render/S3MViewer.d.ts.map +1 -1
  14. package/dist/render/S3MViewer.js +18 -3
  15. package/dist/render/S3MViewer.js.map +1 -1
  16. package/dist/render/StyleMaterial.js +265 -265
  17. package/dist/vendor/meshopt_decoder.module.js +118 -118
  18. package/docs/API.md +20 -0
  19. package/package.json +1 -1
  20. package/src/core/BinaryReader.ts +116 -116
  21. package/src/core/DXTTextureDecode.ts +260 -260
  22. package/src/core/FeatureInfo.ts +150 -150
  23. package/src/core/MeshOpt.ts +401 -401
  24. package/src/core/S3MBParser.ts +590 -590
  25. package/src/core/types.ts +222 -222
  26. package/src/render/DatasetManager.ts +17 -6
  27. package/src/render/EdgeLines.ts +343 -343
  28. package/src/render/GeometryBuilder.ts +840 -840
  29. package/src/render/GroundManager.ts +14 -1
  30. package/src/render/Picker.ts +293 -293
  31. package/src/render/S3MViewer.ts +18 -3
  32. package/src/render/StyleMaterial.ts +602 -602
  33. package/src/render/TextureFactory.ts +134 -134
  34. package/src/tileset/S3MTileset.ts +552 -552
  35. package/src/vendor/meshopt_decoder.module.d.ts +31 -31
  36. package/src/vendor/meshopt_decoder.module.js +118 -118
  37. package/dist/render/CoordinateHelper.d.ts +0 -90
  38. package/dist/render/CoordinateHelper.d.ts.map +0 -1
  39. package/dist/render/CoordinateHelper.js +0 -85
  40. package/dist/render/CoordinateHelper.js.map +0 -1
  41. package/dist/render/S3MLayer.d.ts +0 -457
  42. package/dist/render/S3MLayer.d.ts.map +0 -1
  43. package/dist/render/S3MLayer.js +0 -1860
  44. package/dist/render/S3MLayer.js.map +0 -1
@@ -1,602 +1,602 @@
1
- /**
2
- * S3M 着色系统
3
- *
4
- * 设计要点:**不重建几何、不重建材质**,所有样式靠 uniform 在片元着色器里算。
5
- * 这样「白膜 → 按高度 → 动态扫描」之间切换只是改一个 int,几十万三角面零开销。
6
- *
7
- * uniform 分两类:
8
- * shared —— 全局共享同一个对象引用,改一次全场景生效
9
- * own —— 每个 Mesh 独有(比如选中单体编号),避免互相干扰
10
- */
11
- import * as THREE from 'three';
12
- import type { StyleUniforms } from '../core/types.js';
13
- import { DEFAULT_FLOOR_HEIGHT } from '../core/FeatureInfo.js';
14
-
15
- /** 着色模式,数值与着色器里的 uMode 一一对应 */
16
- export const STYLE = {
17
- WHITE: 0, // 白膜:忽略贴图与顶点色,纯色 + 光照
18
- TEXTURE: 1, // 原色:贴图 × 顶点色
19
- SOLID: 2, // 单色
20
- HEIGHT: 3, // 按高度连续渐变
21
- FLOORS: 4, // 按楼层离散分层(高度 / 3.2m 取整)
22
- BATCH: 5, // 按单体随机色,便于区分建筑
23
- DYNAMIC: 6, // 动态扫描:扫描线沿高度循环
24
- TOP_SIDE: 7, // 顶面/侧面分色:顶面一种颜色,侧面另一种颜色
25
- CUSTOM: 8, // 自定义/复合着色:顶面分色 + 侧面高度渐变
26
- } as const;
27
-
28
- export type StyleMode = (typeof STYLE)[keyof typeof STYLE];
29
-
30
- export const STYLE_LABELS: Record<StyleMode, string> = {
31
- [STYLE.WHITE]: '白膜',
32
- [STYLE.TEXTURE]: '原色',
33
- [STYLE.SOLID]: '单色',
34
- [STYLE.HEIGHT]: '按高度',
35
- [STYLE.FLOORS]: '按楼层',
36
- [STYLE.BATCH]: '按单体',
37
- [STYLE.DYNAMIC]: '动态扫描',
38
- [STYLE.TOP_SIDE]: '顶/侧分色',
39
- [STYLE.CUSTOM]: '自定义',
40
- };
41
-
42
- /** 选中单体的高亮模式 */
43
- export const SELECT_MODE = {
44
- /** 纯色覆盖(经典白模 / 纯色高亮) */
45
- REPLACE: 'replace',
46
- /** 叠加在原图上(半透明混合,保留原模型贴图与门窗细节) */
47
- OVERLAY: 'overlay',
48
- /** 叠乘模式(与原模型贴图乘算,明暗对比强烈) */
49
- MULTIPLY: 'multiply',
50
- /** 仅描边与自发光(原图本体不染色) */
51
- GLOW_ONLY: 'glowOnly',
52
- } as const;
53
-
54
- export type SelectMode = (typeof SELECT_MODE)[keyof typeof SELECT_MODE];
55
-
56
- export const SELECT_MODE_LABELS: Record<SelectMode, string> = {
57
- [SELECT_MODE.OVERLAY]: '叠加原图 (Overlay)',
58
- [SELECT_MODE.REPLACE]: '纯色覆盖 (Replace)',
59
- [SELECT_MODE.MULTIPLY]: '叠乘模式 (Multiply)',
60
- [SELECT_MODE.GLOW_ONLY]: '仅描边发光 (Glow)',
61
- };
62
-
63
- /* ------------------------------------------------------------------ 样式配置 */
64
-
65
- /** 图层整体样式(宿主无关,S3MViewer 与 s3m-maplibre 的 S3MLayer 共用) */
66
- export interface S3MStyle {
67
- /** 着色模式,见 STYLE */
68
- mode: StyleMode;
69
- /** 主色:白膜 / 单色 / 渐变起点 / 动态扫描底色 */
70
- colorA: string;
71
- /** 渐变终点 / 楼层高段色 */
72
- colorB: string;
73
- /** 动态扫描的高亮带颜色 */
74
- colorC: string;
75
- /** 选中单体的高亮色 */
76
- selectColor: string;
77
- /** 选中单体的高亮模式:'overlay' 叠加原图 | 'replace' 纯色覆盖 | 'multiply' 叠乘 | 'glowOnly' 仅描边发光 */
78
- selectMode?: SelectMode;
79
- /** 选中高亮叠色强度 / 透明度(0~1,默认 0.65) */
80
- selectOpacity?: number;
81
- /** 选中自发光强度(0~1,默认 0.35) */
82
- selectEmissive?: number;
83
- /** 选中单体垂直拔高高度(米,默认 0) */
84
- selectElevation?: number;
85
- /** 楼层估算层高(米) */
86
- floorHeight: number;
87
- /** 动态扫描速度倍率 */
88
- speed: number;
89
- /** 整体不透明度,< 1 时自动开启混合并关闭深度写入 */
90
- opacity: number;
91
- /** 'flat' 平面着色(白模更干净) | 'smooth' 平滑着色(倾斜摄影更自然) */
92
- shading: 'flat' | 'smooth';
93
- /** 选中单体时是否压暗其余建筑 */
94
- dimOthers: boolean;
95
- /** 未选中建筑压暗系数(0~1,默认 0.42) */
96
- dimFactor?: number;
97
- /** 是否给选中单体描边 */
98
- outline: boolean;
99
- /** 选中描边的画法:'edges' 单体棱线(默认)| 'hull' 背面外扩轮廓 */
100
- outlineStyle?: 'edges' | 'hull';
101
- /** 描边颜色(默认跟随 selectColor) */
102
- outlineColor?: string;
103
- /** 外扩轮廓的粗细(米,默认 0.9,仅 outlineStyle: 'hull' 生效) */
104
- outlineWidth?: number;
105
- /** 选中棱线的粗细(像素,默认 3,仅 outlineStyle: 'edges' 生效,缩放时保持恒定) */
106
- outlineWidthPx?: number;
107
- /** 是否显示几何棱线(CPU 端生成,有开销) */
108
- edges: boolean;
109
- /** 全局棱线粗细(像素,默认 1.2) */
110
- edgeWidth?: number;
111
- edgeColor: string;
112
- edgeOpacity?: number;
113
- roofThreshold?: number;
114
- /** 是否与模型原色贴图叠加混合(0 = 纯色覆盖,1 = 叠乘原色贴图) */
115
- texMix?: number;
116
- /** 半透明时是否保持深度写入(默认 true,保持写入可彻底遮挡背面与内部棱线,避免背面透出) */
117
- depthWrite?: boolean;
118
- }
119
-
120
- export const DEFAULT_STYLE: S3MStyle = {
121
- mode: STYLE.TEXTURE,
122
- colorA: '#dfe6ee',
123
- colorB: '#1d4ed8',
124
- colorC: '#fbbf24',
125
- selectColor: '#22d3ee',
126
- selectMode: SELECT_MODE.OVERLAY,
127
- selectOpacity: 0.65,
128
- selectEmissive: 0.35,
129
- selectElevation: 0,
130
- floorHeight: DEFAULT_FLOOR_HEIGHT,
131
- speed: 1,
132
- opacity: 1,
133
- shading: 'flat',
134
- dimOthers: true,
135
- dimFactor: 0.42,
136
- outline: true,
137
- outlineStyle: 'edges',
138
- outlineColor: '#22d3ee',
139
- outlineWidth: 0.9,
140
- outlineWidthPx: 3,
141
- edges: false,
142
- edgeWidth: 1.2,
143
- edgeColor: '#334155',
144
- edgeOpacity: 0.45,
145
- roofThreshold: 0.65,
146
- texMix: 0,
147
- depthWrite: true,
148
- };
149
-
150
- export interface StyledMaterial extends THREE.MeshStandardMaterial {
151
- userData: {
152
- uSelBatch: { value: number };
153
- uSelWhole: { value: number };
154
- uBatchTex: { value: THREE.Texture };
155
- uBatchTexWidth: { value: number };
156
- uBatchTexActive: { value: number };
157
- isStyled: true;
158
- [key: string]: unknown;
159
- };
160
- }
161
-
162
- const DUMMY_DATA = new Float32Array([-1, -1, -1, 0, 0, 0, 0, 0]);
163
- export const DUMMY_BATCH_TEX = new THREE.DataTexture(DUMMY_DATA, 1, 2, THREE.RGBAFormat, THREE.FloatType);
164
- DUMMY_BATCH_TEX.minFilter = THREE.NearestFilter;
165
- DUMMY_BATCH_TEX.magFilter = THREE.NearestFilter;
166
- DUMMY_BATCH_TEX.needsUpdate = true;
167
-
168
- /** 全局共享 uniform(所有材质引用同一批对象,改 .value 即全场景生效) */
169
- export function createSharedUniforms(): StyleUniforms {
170
- return {
171
- uMode: { value: STYLE.TEXTURE },
172
- uColorA: { value: new THREE.Color('#dfe6ee') },
173
- uColorB: { value: new THREE.Color('#1d4ed8') },
174
- uColorC: { value: new THREE.Color('#fbbf24') },
175
- uTime: { value: 0 },
176
- uHeightMin: { value: 0 },
177
- uHeightMax: { value: 120 },
178
- uFloorHeight: { value: 3.2 },
179
- /** 0 = 不压暗其他建筑;1 = 选中时其余建筑压暗 */
180
- uDim: { value: 0 },
181
- /** 其余建筑压暗系数(默认 0.42) */
182
- uDimFactor: { value: 0.42 },
183
- /** 选中单体的高亮色 */
184
- uSelColor: { value: new THREE.Color('#22d3ee') },
185
- /** 高亮强度,0 表示不做选中着色 */
186
- uSelMix: { value: 0 },
187
- /** 选中高亮混合模式:0 = 纯色覆盖, 1 = 叠加原图(mix), 2 = 叠乘(multiply), 3 = 仅描边发光(glowOnly) */
188
- uSelMode: { value: 1 },
189
- /** 选中高亮叠色强度 / 透明度(0~1,默认 0.65) */
190
- uSelOpacity: { value: 0.65 },
191
- /** 选中自发光强度(0~1,默认 0.35) */
192
- uSelEmissive: { value: 0.35 },
193
- /** 选中单体垂直拔高高度(米,默认 0) */
194
- uSelElevation: { value: 0 },
195
- /** 高度过滤下界 */
196
- uFilterMinZ: { value: -99999 },
197
- /** 高度过滤上界 */
198
- uFilterMaxZ: { value: 99999 },
199
- /** 是否启用高度过滤 */
200
- uFilterHeightActive: { value: 0 },
201
- /** 顶面法线判定阈值:Z 分量余弦大于该值判定为顶面 */
202
- uRoofThreshold: { value: 0.65 },
203
- /** 与模型原色贴图混合强度:0 = 纯色覆盖,1 = 叠乘原色贴图 */
204
- uTexMix: { value: 0 },
205
- };
206
- }
207
-
208
- const VERT_PARS = /* glsl */ `
209
- attribute float aBatchId;
210
- attribute vec3 aColor;
211
- // 实例化几何没有逐顶点 batchId:顶点是 base mesh 的,单体粒度落在实例上
212
- #ifdef USE_INSTANCING
213
- attribute float aInstanceBatchId;
214
- #endif
215
- uniform float uSelBatch;
216
- uniform float uSelWhole;
217
- uniform float uSelElevation;
218
- uniform sampler2D uBatchTex;
219
- uniform float uBatchTexWidth;
220
- uniform int uBatchTexActive;
221
- varying float vBatchId;
222
- varying vec3 vVertColor;
223
- varying float vWorldZ;
224
- varying vec3 vWorldNormal;
225
- varying vec3 vWorldPos;
226
-
227
- /** 当前图元所属单体的控制位:实例化取实例属性,其余取逐顶点属性 */
228
- float s3mBatchRaw() {
229
- #ifdef USE_INSTANCING
230
- return aInstanceBatchId;
231
- #else
232
- return aBatchId;
233
- #endif
234
- }
235
- `;
236
-
237
- const VERT_ELEVATION = /* glsl */ `
238
- float s3mBatch = s3mBatchRaw();
239
- vec3 s3mCustomOffset = vec3( 0.0 );
240
- if ( uBatchTexActive > 0 && s3mBatch >= 0.0 ) {
241
- vec4 bState = texture2D( uBatchTex, vec2( ( s3mBatch + 0.5 ) / uBatchTexWidth, 0.25 ) );
242
- if ( bState.a < -0.5 ) {
243
- transformed = vec3( 0.0 );
244
- } else {
245
- vec4 bOffset = texture2D( uBatchTex, vec2( ( s3mBatch + 0.5 ) / uBatchTexWidth, 0.75 ) );
246
- s3mCustomOffset = bOffset.xyz;
247
- if ( s3mCustomOffset.z == 0.0 && bState.a > 0.0 ) {
248
- s3mCustomOffset.z = bState.a;
249
- }
250
- }
251
- }
252
-
253
- float s3mSelElev = max( step( 0.0, uSelBatch ) * step( abs( s3mBatch - uSelBatch ), 0.5 ), uSelWhole );
254
- // 实例化时这是局部坐标,实例矩阵只含平移 + 绕 z 旋转(白模摆位),所以偏移仍沿局部轴生效
255
- transformed += s3mCustomOffset;
256
- transformed.z += s3mSelElev * uSelElevation;
257
- `;
258
-
259
- const VERT_WORLD = /* glsl */ `
260
- vec4 s3mWorld = vec4( transformed, 1.0 );
261
- #ifdef USE_INSTANCING
262
- s3mWorld = instanceMatrix * s3mWorld;
263
- #endif
264
- s3mWorld = modelMatrix * s3mWorld;
265
- vWorldZ = s3mWorld.z;
266
- vWorldPos = s3mWorld.xyz;
267
- vBatchId = s3mBatchRaw();
268
- vVertColor = aColor;
269
-
270
- #ifdef USE_INSTANCING
271
- mat3 s3mNormMat = mat3( modelMatrix ) * mat3( instanceMatrix );
272
- #else
273
- mat3 s3mNormMat = mat3( modelMatrix );
274
- #endif
275
- vWorldNormal = normalize( s3mNormMat * normal );
276
- `;
277
-
278
- const FRAG_PARS = /* glsl */ `
279
- uniform int uMode;
280
- uniform vec3 uColorA;
281
- uniform vec3 uColorB;
282
- uniform vec3 uColorC;
283
- uniform vec3 uSelColor;
284
- uniform int uSelMode;
285
- uniform float uSelOpacity;
286
- uniform float uSelEmissive;
287
- uniform float uDimFactor;
288
- uniform sampler2D uBatchTex;
289
- uniform float uBatchTexWidth;
290
- uniform int uBatchTexActive;
291
- uniform float uFilterMinZ;
292
- uniform float uFilterMaxZ;
293
- uniform int uFilterHeightActive;
294
- uniform float uTime;
295
- uniform float uHeightMin;
296
- uniform float uHeightMax;
297
- uniform float uFloorHeight;
298
- uniform float uDim;
299
- uniform float uSelMix;
300
- uniform float uSelBatch;
301
- uniform float uSelWhole;
302
- uniform float uRoofThreshold;
303
- uniform float uTexMix;
304
- varying float vBatchId;
305
- varying vec3 vVertColor;
306
- varying float vWorldZ;
307
- varying vec3 vWorldNormal;
308
- varying vec3 vWorldPos;
309
-
310
- float s3mHash( float p ) {
311
- p = fract( p * 0.1031 );
312
- p *= p + 33.33;
313
- p *= p + p;
314
- return fract( p );
315
- }
316
-
317
- /**
318
- * 取整后的 batchId。
319
- *
320
- * vBatchId 是 varying:GPU 按像素做透视校正插值,即便一个三角形三个顶点的
321
- * aBatchId 完全相等,三次乘加各自的舍入误差也不会正好抵消,结果会带
322
- * 1e-7 量级的相对误差,而且逐像素不同(不同 GPU / 驱动还不一样)。
323
- *
324
- * 这个误差本身无害,但后面的哈希是混沌放大器:p → fract(p*0.1031) →
325
- * p*(p+33.33) → p*2p,每一级都把误差放大几十倍,三级下来 1e-7 就能放大到 O(1)。
326
- * 实测 batchId≈4.2 万时,1e-7 的抖动会让哈希输出在 0.20~0.78 之间乱跳,
327
- * 直接表现为整屏彩色噪点。所以凡是要「按 id 取值」的地方都必须先取整。
328
- */
329
- float s3mBatchId() {
330
- return floor( vBatchId + 0.5 );
331
- }
332
-
333
- vec3 s3mHsv( float h, float s, float v ) {
334
- vec3 k = vec3( 1.0, 2.0 / 3.0, 1.0 / 3.0 );
335
- vec3 p = abs( fract( vec3( h ) + k.xyz ) * 6.0 - vec3( 3.0 ) );
336
- return v * mix( vec3( 1.0 ), clamp( p - vec3( 1.0 ), 0.0, 1.0 ), s );
337
- }
338
- `;
339
-
340
- const FRAG_COLOR = /* glsl */ `
341
- float s3mH = clamp( ( vWorldZ - uHeightMin ) / max( uHeightMax - uHeightMin, 0.001 ), 0.0, 1.0 );
342
- vec3 s3mBase = diffuseColor.rgb; // 已含 diffuse × map
343
- // 所有按单体取值(配色 / 状态纹理 / 选中判定)都走取整后的 id,避免插值噪声
344
- float s3mBid = s3mBatchId();
345
-
346
- // 法线:优先使用顶点插值法线;若退化或为平面着色,使用屏幕导数精确计算真实几何法线
347
- vec3 s3mNorm = length( vWorldNormal ) > 0.1 ? normalize( vWorldNormal ) : normalize( cross( dFdx( vWorldPos ), dFdy( vWorldPos ) ) );
348
-
349
- if ( uMode == 0 ) {
350
- // 白膜:丢掉贴图,只留光照
351
- diffuseColor.rgb = uColorA;
352
- } else if ( uMode == 1 ) {
353
- // 原色:贴图 × 顶点色
354
- diffuseColor.rgb = s3mBase * vVertColor;
355
- } else if ( uMode == 2 ) {
356
- diffuseColor.rgb = uColorA;
357
- } else if ( uMode == 3 ) {
358
- // 按高度连续渐变
359
- diffuseColor.rgb = mix( uColorA, uColorB, s3mH );
360
- } else if ( uMode == 4 ) {
361
- // 按楼层:先算楼层数再归一化,形成分层色带
362
- float floors = floor( ( vWorldZ - uHeightMin ) / max( uFloorHeight, 0.5 ) );
363
- float total = max( floor( ( uHeightMax - uHeightMin ) / max( uFloorHeight, 0.5 ) ), 1.0 );
364
- diffuseColor.rgb = mix( uColorA, uColorB, floors / total );
365
- } else if ( uMode == 5 ) {
366
- // 按单体:同一个 batchId 同色,相邻单体色相拉开(必须用取整后的 id,见 s3mBatchId)
367
- diffuseColor.rgb = s3mHsv( s3mHash( s3mBid * 7.31 + 1.0 ), 0.52, 0.95 );
368
- } else if ( uMode == 6 ) {
369
- // 动态扫描:底色按高度渐变,一条亮带自下而上循环
370
- float scan = fract( uTime * 0.18 );
371
- float glow = smoothstep( 0.10, 0.0, abs( s3mH - scan ) );
372
- diffuseColor.rgb = mix( uColorA, uColorB, s3mH ) + uColorC * glow * 1.2;
373
- } else if ( uMode == 7 ) {
374
- // 顶面/侧面分色:uColorA 为顶面(屋顶),uColorB 为侧面(立面)
375
- // 局部 ENU 坐标系以 Z 为朝天方向,abs(s3mNorm.z) 接近 1 为顶面/底面,接近 0 为立面
376
- float th = uRoofThreshold > 0.0 ? uRoofThreshold : 0.65;
377
- float isRoof = smoothstep( th - 0.08, th + 0.08, abs( s3mNorm.z ) );
378
- diffuseColor.rgb = mix( uColorB, uColorA, isRoof );
379
- } else {
380
- // 自定义复合模式:顶面使用 uColorA,侧面叠加高度渐变(uColorB 到 uColorC)
381
- float th = uRoofThreshold > 0.0 ? uRoofThreshold : 0.65;
382
- float isRoof = smoothstep( th - 0.08, th + 0.08, abs( s3mNorm.z ) );
383
- vec3 sideGrad = mix( uColorB, uColorC, s3mH );
384
- diffuseColor.rgb = mix( sideGrad, uColorA, isRoof );
385
- }
386
-
387
- // 叠加在原色贴图上:保留窗框、玻璃幕墙、材质纹理与顶点色
388
- if ( uMode != 1 && uTexMix > 0.001 ) {
389
- vec3 origTex = s3mBase * vVertColor;
390
- vec3 overlay = origTex * diffuseColor.rgb * 1.35;
391
- diffuseColor.rgb = mix( diffuseColor.rgb, overlay, uTexMix );
392
- }
393
-
394
- // 1. 高度范围过滤
395
- if ( uFilterHeightActive > 0 ) {
396
- if ( vWorldZ < uFilterMinZ || vWorldZ > uFilterMaxZ ) discard;
397
- }
398
-
399
- // 2. 单体状态过滤(隐藏)与单体独立改色
400
- bool hasCustomColor = false;
401
- if ( uBatchTexActive > 0 && s3mBid >= 0.0 ) {
402
- // 用取整后的 id 采样:否则逐像素的插值误差会让纹素在边界上来回跳,同样出彩色噪点
403
- vec4 bState = texture2D( uBatchTex, vec2( ( s3mBid + 0.5 ) / uBatchTexWidth, 0.25 ) );
404
- if ( bState.a < -0.5 ) {
405
- discard; // 属于被过滤隐藏的单体
406
- }
407
- if ( bState.r >= 0.0 ) {
408
- // 单体独立自定义颜色
409
- diffuseColor.rgb = bState.rgb;
410
- hasCustomColor = true;
411
- }
412
- }
413
-
414
- // 3. 选中单体高亮 + 其余压暗
415
- float s3mSel = max( step( 0.0, uSelBatch ) * step( abs( s3mBid - uSelBatch ), 0.5 ), uSelWhole );
416
- if ( s3mSel > 0.5 && uSelMix > 0.001 ) {
417
- if ( hasCustomColor ) {
418
- // 若当前建筑已有用户固定的独立颜色,选中时保留该颜色,仅做轻微高亮叠色提示
419
- if ( uSelMode == 0 || uSelMode == 1 ) {
420
- diffuseColor.rgb = mix( diffuseColor.rgb, uSelColor, uSelOpacity * 0.35 );
421
- }
422
- } else {
423
- if ( uSelMode == 0 ) {
424
- // 纯色覆盖(经典纯高亮色)
425
- diffuseColor.rgb = uSelColor;
426
- } else if ( uSelMode == 1 ) {
427
- // 叠加原图(半透明混色,保留纹理贴图)
428
- diffuseColor.rgb = mix( diffuseColor.rgb, uSelColor, uSelOpacity );
429
- } else if ( uSelMode == 2 ) {
430
- // 叠乘模式(明暗对比强)
431
- diffuseColor.rgb = mix( diffuseColor.rgb, diffuseColor.rgb * uSelColor * 1.5, uSelOpacity );
432
- }
433
- }
434
- // uSelMode == 3 为 glowOnly,保持 diffuseColor.rgb 不变
435
- } else {
436
- diffuseColor.rgb *= mix( 1.0, uDimFactor, uDim );
437
- }
438
- `;
439
-
440
- const FRAG_EMISSIVE = /* glsl */ `
441
- float s3mSelE = max( step( 0.0, uSelBatch ) * step( abs( s3mBatchId() - uSelBatch ), 0.5 ), uSelWhole );
442
- // glowOnly 模式下表面不自发光,只保留描边边框;其他模式下适度发光
443
- if ( uSelMode != 3 ) {
444
- totalEmissiveRadiance += uSelColor * s3mSelE * uSelMix * ( uSelEmissive * 0.4 );
445
- }
446
- `;
447
-
448
- export interface StyledMaterialOptions {
449
- uniforms: StyleUniforms;
450
- map?: THREE.Texture | null;
451
- color?: THREE.Color;
452
- side?: THREE.Side;
453
- transparent?: boolean;
454
- opacity?: number;
455
- roughness?: number;
456
- flatShading?: boolean;
457
- }
458
-
459
- /**
460
- * 创建一个带样式的 PBR 材质。
461
- * 每个 Mesh 独享一个材质实例,但 uniform 对象是全局共享的。
462
- */
463
- export function createStyledMaterial(opts: StyledMaterialOptions): StyledMaterial {
464
- const uniforms = opts.uniforms;
465
- const mat = new THREE.MeshStandardMaterial({
466
- color: opts.color ?? new THREE.Color(0xffffff),
467
- map: opts.map ?? null,
468
- roughness: opts.roughness ?? 0.95,
469
- metalness: 0,
470
- side: opts.side ?? THREE.DoubleSide,
471
- transparent: !!opts.transparent,
472
- opacity: opts.opacity ?? 1,
473
- depthWrite: opts.transparent ? false : true,
474
- // 白模 / OSGB 的法线多是共享顶点平均出来的,平滑插值会让墙面出现斑驳,
475
- // 默认走平面着色(片元里用导数算法线),墙面才干净。
476
- flatShading: opts.flatShading !== false,
477
- // 顶点色、贴图全部在自定义 chunk 里处理,不让 three 插手
478
- vertexColors: false,
479
- }) as StyledMaterial;
480
-
481
- // 选中单体编号:每个 Mesh 独立,默认 -1 表示无选中
482
- const uSelBatch = { value: -1 };
483
- // 没有单体化信息的几何(batchId = -1)只能整块选中
484
- const uSelWhole = { value: 0 };
485
- const uBatchTex = { value: DUMMY_BATCH_TEX };
486
- const uBatchTexWidth = { value: 1.0 };
487
- const uBatchTexActive = { value: 0 };
488
-
489
- mat.onBeforeCompile = (shader) => {
490
- Object.assign(shader.uniforms, uniforms, {
491
- uSelBatch,
492
- uSelWhole,
493
- uBatchTex,
494
- uBatchTexWidth,
495
- uBatchTexActive,
496
- });
497
-
498
- shader.vertexShader = shader.vertexShader
499
- .replace('#include <common>', `#include <common>\n${VERT_PARS}`)
500
- .replace('#include <begin_vertex>', `#include <begin_vertex>\n${VERT_ELEVATION}`)
501
- .replace('#include <worldpos_vertex>', VERT_WORLD);
502
-
503
- shader.fragmentShader = shader.fragmentShader
504
- .replace('#include <common>', `#include <common>\n${FRAG_PARS}`)
505
- .replace('#include <color_fragment>', `#include <color_fragment>\n${FRAG_COLOR}`)
506
- .replace('#include <emissivemap_fragment>', `#include <emissivemap_fragment>\n${FRAG_EMISSIVE}`);
507
- };
508
-
509
- // 不同材质共用同一段注入代码,必须让 three 认为它们的 program 可复用
510
- mat.customProgramCacheKey = () => 's3m-styled';
511
-
512
- mat.userData.uSelBatch = uSelBatch;
513
- mat.userData.uSelWhole = uSelWhole;
514
- mat.userData.uBatchTex = uBatchTex;
515
- mat.userData.uBatchTexWidth = uBatchTexWidth;
516
- mat.userData.uBatchTexActive = uBatchTexActive;
517
- mat.userData.isStyled = true;
518
- return mat;
519
- }
520
-
521
- /* ------------------------------------------------------------------ 选中描边 */
522
-
523
- const OUTLINE_VERT = /* glsl */ `
524
- attribute float aBatchId;
525
- uniform float uSelBatch;
526
- uniform float uSelWhole;
527
- uniform float uWidth;
528
- uniform float uElevation;
529
- uniform sampler2D uBatchTex;
530
- uniform float uBatchTexWidth;
531
- uniform int uBatchTexActive;
532
- varying float vSel;
533
- void main() {
534
- vSel = max( step( 0.0, uSelBatch ) * step( abs( aBatchId - uSelBatch ), 0.5 ), uSelWhole );
535
- vec3 s3mCustomOffset = vec3( 0.0 );
536
- if ( uBatchTexActive > 0 && aBatchId >= 0.0 ) {
537
- vec4 bState = texture2D( uBatchTex, vec2( ( aBatchId + 0.5 ) / uBatchTexWidth, 0.25 ) );
538
- if ( bState.a < -0.5 ) {
539
- vSel = 0.0; // 过滤隐藏的单体不显示描边
540
- } else {
541
- vec4 bOffset = texture2D( uBatchTex, vec2( ( aBatchId + 0.5 ) / uBatchTexWidth, 0.75 ) );
542
- s3mCustomOffset = bOffset.xyz;
543
- if ( s3mCustomOffset.z == 0.0 && bState.a > 0.0 ) {
544
- s3mCustomOffset.z = bState.a;
545
- }
546
- }
547
- }
548
- vec3 p = position + normal * uWidth * vSel;
549
- p += s3mCustomOffset;
550
- p.z += vSel * uElevation;
551
- gl_Position = projectionMatrix * modelViewMatrix * vec4( p, 1.0 );
552
- }
553
- `;
554
-
555
- const OUTLINE_FRAG = /* glsl */ `
556
- uniform vec3 uColor;
557
- varying float vSel;
558
- void main() {
559
- if ( vSel < 0.5 ) discard;
560
- gl_FragColor = vec4( uColor, 1.0 );
561
- }
562
- `;
563
-
564
- export interface OutlineMaterial extends THREE.ShaderMaterial {
565
- uniforms: {
566
- uSelBatch: { value: number };
567
- uSelWhole: { value: number };
568
- uWidth: { value: number };
569
- uElevation: { value: number };
570
- uBatchTex: { value: THREE.Texture };
571
- uBatchTexWidth: { value: number };
572
- uBatchTexActive: { value: number };
573
- uColor: { value: THREE.Color };
574
- };
575
- }
576
-
577
- /** 选中单体的描边材质(背面外扩法) */
578
- export function createOutlineMaterial(shared: StyleUniforms, initialWidth = 0.9): OutlineMaterial {
579
- const mat = new THREE.ShaderMaterial({
580
- uniforms: {
581
- uSelBatch: { value: -1 },
582
- uSelWhole: { value: 0 },
583
- uWidth: { value: initialWidth },
584
- uElevation: { value: 0 },
585
- uBatchTex: { value: DUMMY_BATCH_TEX },
586
- uBatchTexWidth: { value: 1.0 },
587
- uBatchTexActive: { value: 0 },
588
- uColor: { value: shared.uSelColor.value.clone() },
589
- },
590
- vertexShader: OUTLINE_VERT,
591
- fragmentShader: OUTLINE_FRAG,
592
- side: THREE.BackSide,
593
- depthWrite: false,
594
- transparent: false,
595
- // 描边要压在建筑后面一点,避免和本体 z-fighting
596
- polygonOffset: true,
597
- polygonOffsetFactor: 1,
598
- polygonOffsetUnits: 1,
599
- }) as OutlineMaterial;
600
- (mat.userData as Record<string, unknown>).isOutline = true;
601
- return mat;
602
- }
1
+ /**
2
+ * S3M 着色系统
3
+ *
4
+ * 设计要点:**不重建几何、不重建材质**,所有样式靠 uniform 在片元着色器里算。
5
+ * 这样「白膜 → 按高度 → 动态扫描」之间切换只是改一个 int,几十万三角面零开销。
6
+ *
7
+ * uniform 分两类:
8
+ * shared —— 全局共享同一个对象引用,改一次全场景生效
9
+ * own —— 每个 Mesh 独有(比如选中单体编号),避免互相干扰
10
+ */
11
+ import * as THREE from 'three';
12
+ import type { StyleUniforms } from '../core/types.js';
13
+ import { DEFAULT_FLOOR_HEIGHT } from '../core/FeatureInfo.js';
14
+
15
+ /** 着色模式,数值与着色器里的 uMode 一一对应 */
16
+ export const STYLE = {
17
+ WHITE: 0, // 白膜:忽略贴图与顶点色,纯色 + 光照
18
+ TEXTURE: 1, // 原色:贴图 × 顶点色
19
+ SOLID: 2, // 单色
20
+ HEIGHT: 3, // 按高度连续渐变
21
+ FLOORS: 4, // 按楼层离散分层(高度 / 3.2m 取整)
22
+ BATCH: 5, // 按单体随机色,便于区分建筑
23
+ DYNAMIC: 6, // 动态扫描:扫描线沿高度循环
24
+ TOP_SIDE: 7, // 顶面/侧面分色:顶面一种颜色,侧面另一种颜色
25
+ CUSTOM: 8, // 自定义/复合着色:顶面分色 + 侧面高度渐变
26
+ } as const;
27
+
28
+ export type StyleMode = (typeof STYLE)[keyof typeof STYLE];
29
+
30
+ export const STYLE_LABELS: Record<StyleMode, string> = {
31
+ [STYLE.WHITE]: '白膜',
32
+ [STYLE.TEXTURE]: '原色',
33
+ [STYLE.SOLID]: '单色',
34
+ [STYLE.HEIGHT]: '按高度',
35
+ [STYLE.FLOORS]: '按楼层',
36
+ [STYLE.BATCH]: '按单体',
37
+ [STYLE.DYNAMIC]: '动态扫描',
38
+ [STYLE.TOP_SIDE]: '顶/侧分色',
39
+ [STYLE.CUSTOM]: '自定义',
40
+ };
41
+
42
+ /** 选中单体的高亮模式 */
43
+ export const SELECT_MODE = {
44
+ /** 纯色覆盖(经典白模 / 纯色高亮) */
45
+ REPLACE: 'replace',
46
+ /** 叠加在原图上(半透明混合,保留原模型贴图与门窗细节) */
47
+ OVERLAY: 'overlay',
48
+ /** 叠乘模式(与原模型贴图乘算,明暗对比强烈) */
49
+ MULTIPLY: 'multiply',
50
+ /** 仅描边与自发光(原图本体不染色) */
51
+ GLOW_ONLY: 'glowOnly',
52
+ } as const;
53
+
54
+ export type SelectMode = (typeof SELECT_MODE)[keyof typeof SELECT_MODE];
55
+
56
+ export const SELECT_MODE_LABELS: Record<SelectMode, string> = {
57
+ [SELECT_MODE.OVERLAY]: '叠加原图 (Overlay)',
58
+ [SELECT_MODE.REPLACE]: '纯色覆盖 (Replace)',
59
+ [SELECT_MODE.MULTIPLY]: '叠乘模式 (Multiply)',
60
+ [SELECT_MODE.GLOW_ONLY]: '仅描边发光 (Glow)',
61
+ };
62
+
63
+ /* ------------------------------------------------------------------ 样式配置 */
64
+
65
+ /** 图层整体样式(宿主无关,S3MViewer 与 s3m-maplibre 的 S3MLayer 共用) */
66
+ export interface S3MStyle {
67
+ /** 着色模式,见 STYLE */
68
+ mode: StyleMode;
69
+ /** 主色:白膜 / 单色 / 渐变起点 / 动态扫描底色 */
70
+ colorA: string;
71
+ /** 渐变终点 / 楼层高段色 */
72
+ colorB: string;
73
+ /** 动态扫描的高亮带颜色 */
74
+ colorC: string;
75
+ /** 选中单体的高亮色 */
76
+ selectColor: string;
77
+ /** 选中单体的高亮模式:'overlay' 叠加原图 | 'replace' 纯色覆盖 | 'multiply' 叠乘 | 'glowOnly' 仅描边发光 */
78
+ selectMode?: SelectMode;
79
+ /** 选中高亮叠色强度 / 透明度(0~1,默认 0.65) */
80
+ selectOpacity?: number;
81
+ /** 选中自发光强度(0~1,默认 0.35) */
82
+ selectEmissive?: number;
83
+ /** 选中单体垂直拔高高度(米,默认 0) */
84
+ selectElevation?: number;
85
+ /** 楼层估算层高(米) */
86
+ floorHeight: number;
87
+ /** 动态扫描速度倍率 */
88
+ speed: number;
89
+ /** 整体不透明度,< 1 时自动开启混合并关闭深度写入 */
90
+ opacity: number;
91
+ /** 'flat' 平面着色(白模更干净) | 'smooth' 平滑着色(倾斜摄影更自然) */
92
+ shading: 'flat' | 'smooth';
93
+ /** 选中单体时是否压暗其余建筑 */
94
+ dimOthers: boolean;
95
+ /** 未选中建筑压暗系数(0~1,默认 0.42) */
96
+ dimFactor?: number;
97
+ /** 是否给选中单体描边 */
98
+ outline: boolean;
99
+ /** 选中描边的画法:'edges' 单体棱线(默认)| 'hull' 背面外扩轮廓 */
100
+ outlineStyle?: 'edges' | 'hull';
101
+ /** 描边颜色(默认跟随 selectColor) */
102
+ outlineColor?: string;
103
+ /** 外扩轮廓的粗细(米,默认 0.9,仅 outlineStyle: 'hull' 生效) */
104
+ outlineWidth?: number;
105
+ /** 选中棱线的粗细(像素,默认 3,仅 outlineStyle: 'edges' 生效,缩放时保持恒定) */
106
+ outlineWidthPx?: number;
107
+ /** 是否显示几何棱线(CPU 端生成,有开销) */
108
+ edges: boolean;
109
+ /** 全局棱线粗细(像素,默认 1.2) */
110
+ edgeWidth?: number;
111
+ edgeColor: string;
112
+ edgeOpacity?: number;
113
+ roofThreshold?: number;
114
+ /** 是否与模型原色贴图叠加混合(0 = 纯色覆盖,1 = 叠乘原色贴图) */
115
+ texMix?: number;
116
+ /** 半透明时是否保持深度写入(默认 true,保持写入可彻底遮挡背面与内部棱线,避免背面透出) */
117
+ depthWrite?: boolean;
118
+ }
119
+
120
+ export const DEFAULT_STYLE: S3MStyle = {
121
+ mode: STYLE.TEXTURE,
122
+ colorA: '#dfe6ee',
123
+ colorB: '#1d4ed8',
124
+ colorC: '#fbbf24',
125
+ selectColor: '#22d3ee',
126
+ selectMode: SELECT_MODE.OVERLAY,
127
+ selectOpacity: 0.65,
128
+ selectEmissive: 0.35,
129
+ selectElevation: 0,
130
+ floorHeight: DEFAULT_FLOOR_HEIGHT,
131
+ speed: 1,
132
+ opacity: 1,
133
+ shading: 'flat',
134
+ dimOthers: true,
135
+ dimFactor: 0.42,
136
+ outline: true,
137
+ outlineStyle: 'edges',
138
+ outlineColor: '#22d3ee',
139
+ outlineWidth: 0.9,
140
+ outlineWidthPx: 3,
141
+ edges: false,
142
+ edgeWidth: 1.2,
143
+ edgeColor: '#334155',
144
+ edgeOpacity: 0.45,
145
+ roofThreshold: 0.65,
146
+ texMix: 0,
147
+ depthWrite: true,
148
+ };
149
+
150
+ export interface StyledMaterial extends THREE.MeshStandardMaterial {
151
+ userData: {
152
+ uSelBatch: { value: number };
153
+ uSelWhole: { value: number };
154
+ uBatchTex: { value: THREE.Texture };
155
+ uBatchTexWidth: { value: number };
156
+ uBatchTexActive: { value: number };
157
+ isStyled: true;
158
+ [key: string]: unknown;
159
+ };
160
+ }
161
+
162
+ const DUMMY_DATA = new Float32Array([-1, -1, -1, 0, 0, 0, 0, 0]);
163
+ export const DUMMY_BATCH_TEX = new THREE.DataTexture(DUMMY_DATA, 1, 2, THREE.RGBAFormat, THREE.FloatType);
164
+ DUMMY_BATCH_TEX.minFilter = THREE.NearestFilter;
165
+ DUMMY_BATCH_TEX.magFilter = THREE.NearestFilter;
166
+ DUMMY_BATCH_TEX.needsUpdate = true;
167
+
168
+ /** 全局共享 uniform(所有材质引用同一批对象,改 .value 即全场景生效) */
169
+ export function createSharedUniforms(): StyleUniforms {
170
+ return {
171
+ uMode: { value: STYLE.TEXTURE },
172
+ uColorA: { value: new THREE.Color('#dfe6ee') },
173
+ uColorB: { value: new THREE.Color('#1d4ed8') },
174
+ uColorC: { value: new THREE.Color('#fbbf24') },
175
+ uTime: { value: 0 },
176
+ uHeightMin: { value: 0 },
177
+ uHeightMax: { value: 120 },
178
+ uFloorHeight: { value: 3.2 },
179
+ /** 0 = 不压暗其他建筑;1 = 选中时其余建筑压暗 */
180
+ uDim: { value: 0 },
181
+ /** 其余建筑压暗系数(默认 0.42) */
182
+ uDimFactor: { value: 0.42 },
183
+ /** 选中单体的高亮色 */
184
+ uSelColor: { value: new THREE.Color('#22d3ee') },
185
+ /** 高亮强度,0 表示不做选中着色 */
186
+ uSelMix: { value: 0 },
187
+ /** 选中高亮混合模式:0 = 纯色覆盖, 1 = 叠加原图(mix), 2 = 叠乘(multiply), 3 = 仅描边发光(glowOnly) */
188
+ uSelMode: { value: 1 },
189
+ /** 选中高亮叠色强度 / 透明度(0~1,默认 0.65) */
190
+ uSelOpacity: { value: 0.65 },
191
+ /** 选中自发光强度(0~1,默认 0.35) */
192
+ uSelEmissive: { value: 0.35 },
193
+ /** 选中单体垂直拔高高度(米,默认 0) */
194
+ uSelElevation: { value: 0 },
195
+ /** 高度过滤下界 */
196
+ uFilterMinZ: { value: -99999 },
197
+ /** 高度过滤上界 */
198
+ uFilterMaxZ: { value: 99999 },
199
+ /** 是否启用高度过滤 */
200
+ uFilterHeightActive: { value: 0 },
201
+ /** 顶面法线判定阈值:Z 分量余弦大于该值判定为顶面 */
202
+ uRoofThreshold: { value: 0.65 },
203
+ /** 与模型原色贴图混合强度:0 = 纯色覆盖,1 = 叠乘原色贴图 */
204
+ uTexMix: { value: 0 },
205
+ };
206
+ }
207
+
208
+ const VERT_PARS = /* glsl */ `
209
+ attribute float aBatchId;
210
+ attribute vec3 aColor;
211
+ // 实例化几何没有逐顶点 batchId:顶点是 base mesh 的,单体粒度落在实例上
212
+ #ifdef USE_INSTANCING
213
+ attribute float aInstanceBatchId;
214
+ #endif
215
+ uniform float uSelBatch;
216
+ uniform float uSelWhole;
217
+ uniform float uSelElevation;
218
+ uniform sampler2D uBatchTex;
219
+ uniform float uBatchTexWidth;
220
+ uniform int uBatchTexActive;
221
+ varying float vBatchId;
222
+ varying vec3 vVertColor;
223
+ varying float vWorldZ;
224
+ varying vec3 vWorldNormal;
225
+ varying vec3 vWorldPos;
226
+
227
+ /** 当前图元所属单体的控制位:实例化取实例属性,其余取逐顶点属性 */
228
+ float s3mBatchRaw() {
229
+ #ifdef USE_INSTANCING
230
+ return aInstanceBatchId;
231
+ #else
232
+ return aBatchId;
233
+ #endif
234
+ }
235
+ `;
236
+
237
+ const VERT_ELEVATION = /* glsl */ `
238
+ float s3mBatch = s3mBatchRaw();
239
+ vec3 s3mCustomOffset = vec3( 0.0 );
240
+ if ( uBatchTexActive > 0 && s3mBatch >= 0.0 ) {
241
+ vec4 bState = texture2D( uBatchTex, vec2( ( s3mBatch + 0.5 ) / uBatchTexWidth, 0.25 ) );
242
+ if ( bState.a < -0.5 ) {
243
+ transformed = vec3( 0.0 );
244
+ } else {
245
+ vec4 bOffset = texture2D( uBatchTex, vec2( ( s3mBatch + 0.5 ) / uBatchTexWidth, 0.75 ) );
246
+ s3mCustomOffset = bOffset.xyz;
247
+ if ( s3mCustomOffset.z == 0.0 && bState.a > 0.0 ) {
248
+ s3mCustomOffset.z = bState.a;
249
+ }
250
+ }
251
+ }
252
+
253
+ float s3mSelElev = max( step( 0.0, uSelBatch ) * step( abs( s3mBatch - uSelBatch ), 0.5 ), uSelWhole );
254
+ // 实例化时这是局部坐标,实例矩阵只含平移 + 绕 z 旋转(白模摆位),所以偏移仍沿局部轴生效
255
+ transformed += s3mCustomOffset;
256
+ transformed.z += s3mSelElev * uSelElevation;
257
+ `;
258
+
259
+ const VERT_WORLD = /* glsl */ `
260
+ vec4 s3mWorld = vec4( transformed, 1.0 );
261
+ #ifdef USE_INSTANCING
262
+ s3mWorld = instanceMatrix * s3mWorld;
263
+ #endif
264
+ s3mWorld = modelMatrix * s3mWorld;
265
+ vWorldZ = s3mWorld.z;
266
+ vWorldPos = s3mWorld.xyz;
267
+ vBatchId = s3mBatchRaw();
268
+ vVertColor = aColor;
269
+
270
+ #ifdef USE_INSTANCING
271
+ mat3 s3mNormMat = mat3( modelMatrix ) * mat3( instanceMatrix );
272
+ #else
273
+ mat3 s3mNormMat = mat3( modelMatrix );
274
+ #endif
275
+ vWorldNormal = normalize( s3mNormMat * normal );
276
+ `;
277
+
278
+ const FRAG_PARS = /* glsl */ `
279
+ uniform int uMode;
280
+ uniform vec3 uColorA;
281
+ uniform vec3 uColorB;
282
+ uniform vec3 uColorC;
283
+ uniform vec3 uSelColor;
284
+ uniform int uSelMode;
285
+ uniform float uSelOpacity;
286
+ uniform float uSelEmissive;
287
+ uniform float uDimFactor;
288
+ uniform sampler2D uBatchTex;
289
+ uniform float uBatchTexWidth;
290
+ uniform int uBatchTexActive;
291
+ uniform float uFilterMinZ;
292
+ uniform float uFilterMaxZ;
293
+ uniform int uFilterHeightActive;
294
+ uniform float uTime;
295
+ uniform float uHeightMin;
296
+ uniform float uHeightMax;
297
+ uniform float uFloorHeight;
298
+ uniform float uDim;
299
+ uniform float uSelMix;
300
+ uniform float uSelBatch;
301
+ uniform float uSelWhole;
302
+ uniform float uRoofThreshold;
303
+ uniform float uTexMix;
304
+ varying float vBatchId;
305
+ varying vec3 vVertColor;
306
+ varying float vWorldZ;
307
+ varying vec3 vWorldNormal;
308
+ varying vec3 vWorldPos;
309
+
310
+ float s3mHash( float p ) {
311
+ p = fract( p * 0.1031 );
312
+ p *= p + 33.33;
313
+ p *= p + p;
314
+ return fract( p );
315
+ }
316
+
317
+ /**
318
+ * 取整后的 batchId。
319
+ *
320
+ * vBatchId 是 varying:GPU 按像素做透视校正插值,即便一个三角形三个顶点的
321
+ * aBatchId 完全相等,三次乘加各自的舍入误差也不会正好抵消,结果会带
322
+ * 1e-7 量级的相对误差,而且逐像素不同(不同 GPU / 驱动还不一样)。
323
+ *
324
+ * 这个误差本身无害,但后面的哈希是混沌放大器:p → fract(p*0.1031) →
325
+ * p*(p+33.33) → p*2p,每一级都把误差放大几十倍,三级下来 1e-7 就能放大到 O(1)。
326
+ * 实测 batchId≈4.2 万时,1e-7 的抖动会让哈希输出在 0.20~0.78 之间乱跳,
327
+ * 直接表现为整屏彩色噪点。所以凡是要「按 id 取值」的地方都必须先取整。
328
+ */
329
+ float s3mBatchId() {
330
+ return floor( vBatchId + 0.5 );
331
+ }
332
+
333
+ vec3 s3mHsv( float h, float s, float v ) {
334
+ vec3 k = vec3( 1.0, 2.0 / 3.0, 1.0 / 3.0 );
335
+ vec3 p = abs( fract( vec3( h ) + k.xyz ) * 6.0 - vec3( 3.0 ) );
336
+ return v * mix( vec3( 1.0 ), clamp( p - vec3( 1.0 ), 0.0, 1.0 ), s );
337
+ }
338
+ `;
339
+
340
+ const FRAG_COLOR = /* glsl */ `
341
+ float s3mH = clamp( ( vWorldZ - uHeightMin ) / max( uHeightMax - uHeightMin, 0.001 ), 0.0, 1.0 );
342
+ vec3 s3mBase = diffuseColor.rgb; // 已含 diffuse × map
343
+ // 所有按单体取值(配色 / 状态纹理 / 选中判定)都走取整后的 id,避免插值噪声
344
+ float s3mBid = s3mBatchId();
345
+
346
+ // 法线:优先使用顶点插值法线;若退化或为平面着色,使用屏幕导数精确计算真实几何法线
347
+ vec3 s3mNorm = length( vWorldNormal ) > 0.1 ? normalize( vWorldNormal ) : normalize( cross( dFdx( vWorldPos ), dFdy( vWorldPos ) ) );
348
+
349
+ if ( uMode == 0 ) {
350
+ // 白膜:丢掉贴图,只留光照
351
+ diffuseColor.rgb = uColorA;
352
+ } else if ( uMode == 1 ) {
353
+ // 原色:贴图 × 顶点色
354
+ diffuseColor.rgb = s3mBase * vVertColor;
355
+ } else if ( uMode == 2 ) {
356
+ diffuseColor.rgb = uColorA;
357
+ } else if ( uMode == 3 ) {
358
+ // 按高度连续渐变
359
+ diffuseColor.rgb = mix( uColorA, uColorB, s3mH );
360
+ } else if ( uMode == 4 ) {
361
+ // 按楼层:先算楼层数再归一化,形成分层色带
362
+ float floors = floor( ( vWorldZ - uHeightMin ) / max( uFloorHeight, 0.5 ) );
363
+ float total = max( floor( ( uHeightMax - uHeightMin ) / max( uFloorHeight, 0.5 ) ), 1.0 );
364
+ diffuseColor.rgb = mix( uColorA, uColorB, floors / total );
365
+ } else if ( uMode == 5 ) {
366
+ // 按单体:同一个 batchId 同色,相邻单体色相拉开(必须用取整后的 id,见 s3mBatchId)
367
+ diffuseColor.rgb = s3mHsv( s3mHash( s3mBid * 7.31 + 1.0 ), 0.52, 0.95 );
368
+ } else if ( uMode == 6 ) {
369
+ // 动态扫描:底色按高度渐变,一条亮带自下而上循环
370
+ float scan = fract( uTime * 0.18 );
371
+ float glow = smoothstep( 0.10, 0.0, abs( s3mH - scan ) );
372
+ diffuseColor.rgb = mix( uColorA, uColorB, s3mH ) + uColorC * glow * 1.2;
373
+ } else if ( uMode == 7 ) {
374
+ // 顶面/侧面分色:uColorA 为顶面(屋顶),uColorB 为侧面(立面)
375
+ // 局部 ENU 坐标系以 Z 为朝天方向,abs(s3mNorm.z) 接近 1 为顶面/底面,接近 0 为立面
376
+ float th = uRoofThreshold > 0.0 ? uRoofThreshold : 0.65;
377
+ float isRoof = smoothstep( th - 0.08, th + 0.08, abs( s3mNorm.z ) );
378
+ diffuseColor.rgb = mix( uColorB, uColorA, isRoof );
379
+ } else {
380
+ // 自定义复合模式:顶面使用 uColorA,侧面叠加高度渐变(uColorB 到 uColorC)
381
+ float th = uRoofThreshold > 0.0 ? uRoofThreshold : 0.65;
382
+ float isRoof = smoothstep( th - 0.08, th + 0.08, abs( s3mNorm.z ) );
383
+ vec3 sideGrad = mix( uColorB, uColorC, s3mH );
384
+ diffuseColor.rgb = mix( sideGrad, uColorA, isRoof );
385
+ }
386
+
387
+ // 叠加在原色贴图上:保留窗框、玻璃幕墙、材质纹理与顶点色
388
+ if ( uMode != 1 && uTexMix > 0.001 ) {
389
+ vec3 origTex = s3mBase * vVertColor;
390
+ vec3 overlay = origTex * diffuseColor.rgb * 1.35;
391
+ diffuseColor.rgb = mix( diffuseColor.rgb, overlay, uTexMix );
392
+ }
393
+
394
+ // 1. 高度范围过滤
395
+ if ( uFilterHeightActive > 0 ) {
396
+ if ( vWorldZ < uFilterMinZ || vWorldZ > uFilterMaxZ ) discard;
397
+ }
398
+
399
+ // 2. 单体状态过滤(隐藏)与单体独立改色
400
+ bool hasCustomColor = false;
401
+ if ( uBatchTexActive > 0 && s3mBid >= 0.0 ) {
402
+ // 用取整后的 id 采样:否则逐像素的插值误差会让纹素在边界上来回跳,同样出彩色噪点
403
+ vec4 bState = texture2D( uBatchTex, vec2( ( s3mBid + 0.5 ) / uBatchTexWidth, 0.25 ) );
404
+ if ( bState.a < -0.5 ) {
405
+ discard; // 属于被过滤隐藏的单体
406
+ }
407
+ if ( bState.r >= 0.0 ) {
408
+ // 单体独立自定义颜色
409
+ diffuseColor.rgb = bState.rgb;
410
+ hasCustomColor = true;
411
+ }
412
+ }
413
+
414
+ // 3. 选中单体高亮 + 其余压暗
415
+ float s3mSel = max( step( 0.0, uSelBatch ) * step( abs( s3mBid - uSelBatch ), 0.5 ), uSelWhole );
416
+ if ( s3mSel > 0.5 && uSelMix > 0.001 ) {
417
+ if ( hasCustomColor ) {
418
+ // 若当前建筑已有用户固定的独立颜色,选中时保留该颜色,仅做轻微高亮叠色提示
419
+ if ( uSelMode == 0 || uSelMode == 1 ) {
420
+ diffuseColor.rgb = mix( diffuseColor.rgb, uSelColor, uSelOpacity * 0.35 );
421
+ }
422
+ } else {
423
+ if ( uSelMode == 0 ) {
424
+ // 纯色覆盖(经典纯高亮色)
425
+ diffuseColor.rgb = uSelColor;
426
+ } else if ( uSelMode == 1 ) {
427
+ // 叠加原图(半透明混色,保留纹理贴图)
428
+ diffuseColor.rgb = mix( diffuseColor.rgb, uSelColor, uSelOpacity );
429
+ } else if ( uSelMode == 2 ) {
430
+ // 叠乘模式(明暗对比强)
431
+ diffuseColor.rgb = mix( diffuseColor.rgb, diffuseColor.rgb * uSelColor * 1.5, uSelOpacity );
432
+ }
433
+ }
434
+ // uSelMode == 3 为 glowOnly,保持 diffuseColor.rgb 不变
435
+ } else {
436
+ diffuseColor.rgb *= mix( 1.0, uDimFactor, uDim );
437
+ }
438
+ `;
439
+
440
+ const FRAG_EMISSIVE = /* glsl */ `
441
+ float s3mSelE = max( step( 0.0, uSelBatch ) * step( abs( s3mBatchId() - uSelBatch ), 0.5 ), uSelWhole );
442
+ // glowOnly 模式下表面不自发光,只保留描边边框;其他模式下适度发光
443
+ if ( uSelMode != 3 ) {
444
+ totalEmissiveRadiance += uSelColor * s3mSelE * uSelMix * ( uSelEmissive * 0.4 );
445
+ }
446
+ `;
447
+
448
+ export interface StyledMaterialOptions {
449
+ uniforms: StyleUniforms;
450
+ map?: THREE.Texture | null;
451
+ color?: THREE.Color;
452
+ side?: THREE.Side;
453
+ transparent?: boolean;
454
+ opacity?: number;
455
+ roughness?: number;
456
+ flatShading?: boolean;
457
+ }
458
+
459
+ /**
460
+ * 创建一个带样式的 PBR 材质。
461
+ * 每个 Mesh 独享一个材质实例,但 uniform 对象是全局共享的。
462
+ */
463
+ export function createStyledMaterial(opts: StyledMaterialOptions): StyledMaterial {
464
+ const uniforms = opts.uniforms;
465
+ const mat = new THREE.MeshStandardMaterial({
466
+ color: opts.color ?? new THREE.Color(0xffffff),
467
+ map: opts.map ?? null,
468
+ roughness: opts.roughness ?? 0.95,
469
+ metalness: 0,
470
+ side: opts.side ?? THREE.DoubleSide,
471
+ transparent: !!opts.transparent,
472
+ opacity: opts.opacity ?? 1,
473
+ depthWrite: opts.transparent ? false : true,
474
+ // 白模 / OSGB 的法线多是共享顶点平均出来的,平滑插值会让墙面出现斑驳,
475
+ // 默认走平面着色(片元里用导数算法线),墙面才干净。
476
+ flatShading: opts.flatShading !== false,
477
+ // 顶点色、贴图全部在自定义 chunk 里处理,不让 three 插手
478
+ vertexColors: false,
479
+ }) as StyledMaterial;
480
+
481
+ // 选中单体编号:每个 Mesh 独立,默认 -1 表示无选中
482
+ const uSelBatch = { value: -1 };
483
+ // 没有单体化信息的几何(batchId = -1)只能整块选中
484
+ const uSelWhole = { value: 0 };
485
+ const uBatchTex = { value: DUMMY_BATCH_TEX };
486
+ const uBatchTexWidth = { value: 1.0 };
487
+ const uBatchTexActive = { value: 0 };
488
+
489
+ mat.onBeforeCompile = (shader) => {
490
+ Object.assign(shader.uniforms, uniforms, {
491
+ uSelBatch,
492
+ uSelWhole,
493
+ uBatchTex,
494
+ uBatchTexWidth,
495
+ uBatchTexActive,
496
+ });
497
+
498
+ shader.vertexShader = shader.vertexShader
499
+ .replace('#include <common>', `#include <common>\n${VERT_PARS}`)
500
+ .replace('#include <begin_vertex>', `#include <begin_vertex>\n${VERT_ELEVATION}`)
501
+ .replace('#include <worldpos_vertex>', VERT_WORLD);
502
+
503
+ shader.fragmentShader = shader.fragmentShader
504
+ .replace('#include <common>', `#include <common>\n${FRAG_PARS}`)
505
+ .replace('#include <color_fragment>', `#include <color_fragment>\n${FRAG_COLOR}`)
506
+ .replace('#include <emissivemap_fragment>', `#include <emissivemap_fragment>\n${FRAG_EMISSIVE}`);
507
+ };
508
+
509
+ // 不同材质共用同一段注入代码,必须让 three 认为它们的 program 可复用
510
+ mat.customProgramCacheKey = () => 's3m-styled';
511
+
512
+ mat.userData.uSelBatch = uSelBatch;
513
+ mat.userData.uSelWhole = uSelWhole;
514
+ mat.userData.uBatchTex = uBatchTex;
515
+ mat.userData.uBatchTexWidth = uBatchTexWidth;
516
+ mat.userData.uBatchTexActive = uBatchTexActive;
517
+ mat.userData.isStyled = true;
518
+ return mat;
519
+ }
520
+
521
+ /* ------------------------------------------------------------------ 选中描边 */
522
+
523
+ const OUTLINE_VERT = /* glsl */ `
524
+ attribute float aBatchId;
525
+ uniform float uSelBatch;
526
+ uniform float uSelWhole;
527
+ uniform float uWidth;
528
+ uniform float uElevation;
529
+ uniform sampler2D uBatchTex;
530
+ uniform float uBatchTexWidth;
531
+ uniform int uBatchTexActive;
532
+ varying float vSel;
533
+ void main() {
534
+ vSel = max( step( 0.0, uSelBatch ) * step( abs( aBatchId - uSelBatch ), 0.5 ), uSelWhole );
535
+ vec3 s3mCustomOffset = vec3( 0.0 );
536
+ if ( uBatchTexActive > 0 && aBatchId >= 0.0 ) {
537
+ vec4 bState = texture2D( uBatchTex, vec2( ( aBatchId + 0.5 ) / uBatchTexWidth, 0.25 ) );
538
+ if ( bState.a < -0.5 ) {
539
+ vSel = 0.0; // 过滤隐藏的单体不显示描边
540
+ } else {
541
+ vec4 bOffset = texture2D( uBatchTex, vec2( ( aBatchId + 0.5 ) / uBatchTexWidth, 0.75 ) );
542
+ s3mCustomOffset = bOffset.xyz;
543
+ if ( s3mCustomOffset.z == 0.0 && bState.a > 0.0 ) {
544
+ s3mCustomOffset.z = bState.a;
545
+ }
546
+ }
547
+ }
548
+ vec3 p = position + normal * uWidth * vSel;
549
+ p += s3mCustomOffset;
550
+ p.z += vSel * uElevation;
551
+ gl_Position = projectionMatrix * modelViewMatrix * vec4( p, 1.0 );
552
+ }
553
+ `;
554
+
555
+ const OUTLINE_FRAG = /* glsl */ `
556
+ uniform vec3 uColor;
557
+ varying float vSel;
558
+ void main() {
559
+ if ( vSel < 0.5 ) discard;
560
+ gl_FragColor = vec4( uColor, 1.0 );
561
+ }
562
+ `;
563
+
564
+ export interface OutlineMaterial extends THREE.ShaderMaterial {
565
+ uniforms: {
566
+ uSelBatch: { value: number };
567
+ uSelWhole: { value: number };
568
+ uWidth: { value: number };
569
+ uElevation: { value: number };
570
+ uBatchTex: { value: THREE.Texture };
571
+ uBatchTexWidth: { value: number };
572
+ uBatchTexActive: { value: number };
573
+ uColor: { value: THREE.Color };
574
+ };
575
+ }
576
+
577
+ /** 选中单体的描边材质(背面外扩法) */
578
+ export function createOutlineMaterial(shared: StyleUniforms, initialWidth = 0.9): OutlineMaterial {
579
+ const mat = new THREE.ShaderMaterial({
580
+ uniforms: {
581
+ uSelBatch: { value: -1 },
582
+ uSelWhole: { value: 0 },
583
+ uWidth: { value: initialWidth },
584
+ uElevation: { value: 0 },
585
+ uBatchTex: { value: DUMMY_BATCH_TEX },
586
+ uBatchTexWidth: { value: 1.0 },
587
+ uBatchTexActive: { value: 0 },
588
+ uColor: { value: shared.uSelColor.value.clone() },
589
+ },
590
+ vertexShader: OUTLINE_VERT,
591
+ fragmentShader: OUTLINE_FRAG,
592
+ side: THREE.BackSide,
593
+ depthWrite: false,
594
+ transparent: false,
595
+ // 描边要压在建筑后面一点,避免和本体 z-fighting
596
+ polygonOffset: true,
597
+ polygonOffsetFactor: 1,
598
+ polygonOffsetUnits: 1,
599
+ }) as OutlineMaterial;
600
+ (mat.userData as Record<string, unknown>).isOutline = true;
601
+ return mat;
602
+ }