s3m-three 0.3.11 → 0.3.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser/s3m-three.iife.ext-three.js +102 -20
- package/dist/browser/s3m-three.iife.js +102 -20
- package/dist/render/EdgeLines.d.ts.map +1 -1
- package/dist/render/EdgeLines.js +5 -2
- package/dist/render/EdgeLines.js.map +1 -1
- package/dist/render/Picker.d.ts.map +1 -1
- package/dist/render/Picker.js +109 -12
- package/dist/render/Picker.js.map +1 -1
- package/dist/render/S3MViewer.d.ts +35 -0
- package/dist/render/S3MViewer.d.ts.map +1 -1
- package/dist/render/S3MViewer.js +168 -12
- package/dist/render/S3MViewer.js.map +1 -1
- package/dist/render/StyleMaterial.d.ts.map +1 -1
- package/dist/render/StyleMaterial.js +18 -4
- package/dist/render/StyleMaterial.js.map +1 -1
- package/docs/ANIMATION.md +17 -7
- package/docs/API.md +360 -6
- package/package.json +1 -1
- package/src/render/EdgeLines.ts +5 -2
- package/src/render/Picker.ts +111 -12
- package/src/render/S3MViewer.ts +174 -13
- package/src/render/StyleMaterial.ts +18 -4
package/src/render/Picker.ts
CHANGED
|
@@ -25,13 +25,26 @@ const PICK_VERT = /* glsl */ `
|
|
|
25
25
|
#endif
|
|
26
26
|
uniform float uObjectId;
|
|
27
27
|
uniform sampler2D uBatchTex;
|
|
28
|
+
uniform sampler2D uBatchTex2;
|
|
29
|
+
uniform float uBatchMix;
|
|
28
30
|
uniform float uBatchTexWidth;
|
|
29
31
|
uniform int uBatchTexActive;
|
|
32
|
+
uniform sampler2D uPathTex;
|
|
33
|
+
uniform float uPathCount;
|
|
34
|
+
uniform float uPathTime;
|
|
35
|
+
uniform int uPathTexActive;
|
|
30
36
|
uniform float uSelBatch;
|
|
31
37
|
uniform float uSelWhole;
|
|
32
38
|
uniform float uSelElevation;
|
|
33
39
|
varying float vBatch;
|
|
34
40
|
|
|
41
|
+
/** 交叉淡化期间也要读「屏幕上正在显示的那份」,否则过渡中点到的是旧位置 */
|
|
42
|
+
vec4 s3mPickSample( vec2 uv ) {
|
|
43
|
+
vec4 a = texture2D( uBatchTex, uv );
|
|
44
|
+
if ( uBatchMix <= 0.0001 ) return a;
|
|
45
|
+
return mix( a, texture2D( uBatchTex2, uv ), uBatchMix );
|
|
46
|
+
}
|
|
47
|
+
|
|
35
48
|
void main() {
|
|
36
49
|
float s3mBatch;
|
|
37
50
|
#ifdef USE_INSTANCING
|
|
@@ -43,22 +56,82 @@ const PICK_VERT = /* glsl */ `
|
|
|
43
56
|
vBatch = aBatchId;
|
|
44
57
|
#endif
|
|
45
58
|
|
|
46
|
-
//
|
|
47
|
-
//
|
|
59
|
+
// 单体状态必须和主着色器用同一份数据、同一套算法:位移 / 旋转 / 隐藏 / GPU 路径
|
|
60
|
+
// 全都只发生在顶点着色器里,拾取这一趟如果还按原始坐标画,动过的楼就点不中。
|
|
61
|
+
// 状态纹理共 6 行,行序与 StyleMaterial 的 VERT_SETUP 严格一一对应。
|
|
48
62
|
vec3 p = position;
|
|
63
|
+
mat3 rot = mat3( 1.0 );
|
|
64
|
+
vec3 pivot = vec3( 0.0 );
|
|
65
|
+
vec3 offset = vec3( 0.0 );
|
|
66
|
+
bool hidden = false;
|
|
67
|
+
|
|
49
68
|
if ( uBatchTexActive > 0 && s3mBatch >= 0.0 ) {
|
|
50
|
-
|
|
69
|
+
float u = ( s3mBatch + 0.5 ) / uBatchTexWidth;
|
|
70
|
+
// Row 0:rgb = 配色,a = 抬高 / < -0.5 表示隐藏
|
|
71
|
+
vec4 state = s3mPickSample( vec2( u, 0.08333333 ) );
|
|
51
72
|
if ( state.a < -0.5 ) {
|
|
52
|
-
|
|
73
|
+
hidden = true;
|
|
53
74
|
} else {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
75
|
+
// Row 1:平移(z 已包含抬高)
|
|
76
|
+
vec4 r1 = s3mPickSample( vec2( u, 0.25 ) );
|
|
77
|
+
offset = r1.xyz;
|
|
78
|
+
if ( offset.z == 0.0 && state.a > 0.0 ) offset.z = state.a;
|
|
79
|
+
|
|
80
|
+
// Row 2/3/4:3x3 姿态矩阵(列优先)+ 轴心
|
|
81
|
+
vec4 r2 = s3mPickSample( vec2( u, 0.41666667 ) );
|
|
82
|
+
vec4 r3 = s3mPickSample( vec2( u, 0.58333333 ) );
|
|
83
|
+
vec4 r4 = s3mPickSample( vec2( u, 0.75 ) );
|
|
84
|
+
rot = mat3(
|
|
85
|
+
vec3( r2.x, r3.x, r4.x ),
|
|
86
|
+
vec3( r2.y, r3.y, r4.y ),
|
|
87
|
+
vec3( r2.z, r3.z, r4.z )
|
|
88
|
+
);
|
|
89
|
+
pivot = vec3( r2.w, r3.w, r4.w );
|
|
90
|
+
|
|
91
|
+
// Row 5:GPU 路径动画,与主着色器逐行对齐(含 loop / pingpong / clamp 与自动朝向)
|
|
92
|
+
if ( uPathTexActive > 0 && uPathCount > 0.0 ) {
|
|
93
|
+
vec4 r5 = s3mPickSample( vec2( u, 0.91666667 ) );
|
|
94
|
+
float pathId = r5.x;
|
|
95
|
+
float speed = r5.y;
|
|
96
|
+
float phase = r5.z;
|
|
97
|
+
float mode = r5.w;
|
|
98
|
+
if ( pathId >= 0.0 && mode > 0.5 ) {
|
|
99
|
+
float t = phase + uPathTime * speed;
|
|
100
|
+
bool autoHeading = true;
|
|
101
|
+
if ( mode >= 4.0 ) {
|
|
102
|
+
autoHeading = false;
|
|
103
|
+
mode -= 3.0;
|
|
104
|
+
}
|
|
105
|
+
if ( mode < 1.5 ) {
|
|
106
|
+
t = fract( t );
|
|
107
|
+
} else if ( mode < 2.5 ) {
|
|
108
|
+
t = clamp( t, 0.0, 1.0 );
|
|
109
|
+
} else {
|
|
110
|
+
t = mod( t, 2.0 );
|
|
111
|
+
t = ( t > 1.0 ) ? ( 2.0 - t ) : t;
|
|
112
|
+
}
|
|
113
|
+
vec4 pathSample = texture2D(
|
|
114
|
+
uPathTex,
|
|
115
|
+
vec2( ( t * 255.0 + 0.5 ) / 256.0, ( pathId + 0.5 ) / uPathCount )
|
|
116
|
+
);
|
|
117
|
+
if ( autoHeading ) {
|
|
118
|
+
float c = cos( pathSample.w );
|
|
119
|
+
float s = sin( pathSample.w );
|
|
120
|
+
// 列优先:(c, s) 是第 0 列 → 绕 Z 转 +yaw,与 StyleMaterial / EdgeLines 逐位一致
|
|
121
|
+
rot = mat3( c, s, 0.0, -s, c, 0.0, 0.0, 0.0, 1.0 ) * rot;
|
|
122
|
+
}
|
|
123
|
+
offset += pathSample.xyz;
|
|
124
|
+
}
|
|
58
125
|
}
|
|
59
126
|
}
|
|
60
127
|
}
|
|
61
128
|
|
|
129
|
+
if ( hidden ) {
|
|
130
|
+
p = vec3( 0.0 );
|
|
131
|
+
} else {
|
|
132
|
+
p = rot * ( p - pivot ) + pivot + offset;
|
|
133
|
+
}
|
|
134
|
+
|
|
62
135
|
// 选中单体自身的拔高(selectElevation)
|
|
63
136
|
float sel = max( step( 0.0, uSelBatch ) * step( abs( s3mBatch - uSelBatch ), 0.5 ), uSelWhole );
|
|
64
137
|
p.z += sel * uSelElevation;
|
|
@@ -114,13 +187,28 @@ const DUMMY_STATE_TEX = (() => {
|
|
|
114
187
|
return tex;
|
|
115
188
|
})();
|
|
116
189
|
|
|
190
|
+
/** 没有 GPU 路径库时用的占位(1x1 全 0,采样出来 pathId = 0 但 uPathTexActive = 0 会跳过) */
|
|
191
|
+
const DUMMY_PATH_TEX = (() => {
|
|
192
|
+
const tex = new THREE.DataTexture(new Float32Array([0, 0, 0, 0]), 1, 1, THREE.RGBAFormat, THREE.FloatType);
|
|
193
|
+
tex.minFilter = THREE.NearestFilter;
|
|
194
|
+
tex.magFilter = THREE.NearestFilter;
|
|
195
|
+
tex.needsUpdate = true;
|
|
196
|
+
return tex;
|
|
197
|
+
})();
|
|
198
|
+
|
|
117
199
|
function createPickMaterial(objectId: number): THREE.ShaderMaterial {
|
|
118
200
|
return new THREE.ShaderMaterial({
|
|
119
201
|
uniforms: {
|
|
120
202
|
uObjectId: { value: objectId },
|
|
121
203
|
uBatchTex: { value: DUMMY_STATE_TEX },
|
|
204
|
+
uBatchTex2: { value: DUMMY_STATE_TEX },
|
|
205
|
+
uBatchMix: { value: 0 },
|
|
122
206
|
uBatchTexWidth: { value: 1 },
|
|
123
207
|
uBatchTexActive: { value: 0 },
|
|
208
|
+
uPathTex: { value: DUMMY_PATH_TEX },
|
|
209
|
+
uPathCount: { value: 1 },
|
|
210
|
+
uPathTime: { value: 0 },
|
|
211
|
+
uPathTexActive: { value: 0 },
|
|
124
212
|
uSelBatch: { value: -1 },
|
|
125
213
|
uSelWhole: { value: 0 },
|
|
126
214
|
uSelElevation: { value: 0 },
|
|
@@ -214,29 +302,40 @@ export class Picker {
|
|
|
214
302
|
if (!ud.pickMaterial) ud.pickMaterial = createPickMaterial(ud.pickId);
|
|
215
303
|
if ((mesh as THREE.InstancedMesh).isInstancedMesh) instancedPickIds.add(ud.pickId);
|
|
216
304
|
|
|
217
|
-
//
|
|
218
|
-
//
|
|
305
|
+
// 把当前的「单体状态纹理」抄给拾取材质:抬高 / 平移 / 姿态 / GPU 路径都是逐单体
|
|
306
|
+
// 写在里面的,抄的是同一个纹理与同一批 uniform 对象,主材质那边更新后这里自动跟着变。
|
|
219
307
|
const prev = mesh.material;
|
|
220
308
|
const pm = ud.pickMaterial as THREE.ShaderMaterial;
|
|
221
309
|
const src = (Array.isArray(prev) ? prev[0] : prev) as THREE.Material & {
|
|
222
310
|
userData?: Record<string, THREE.IUniform>;
|
|
311
|
+
uniforms?: Record<string, THREE.IUniform>;
|
|
223
312
|
};
|
|
224
313
|
const batchTex = src?.userData?.uBatchTex;
|
|
225
314
|
if (batchTex) {
|
|
226
315
|
pm.uniforms.uBatchTex.value = batchTex.value;
|
|
316
|
+
pm.uniforms.uBatchTex2.value = src?.userData?.uBatchTex2?.value ?? batchTex.value;
|
|
227
317
|
pm.uniforms.uBatchTexWidth.value = (src?.userData?.uBatchTexWidth?.value as number) ?? 1;
|
|
228
318
|
pm.uniforms.uBatchTexActive.value = (src?.userData?.uBatchTexActive?.value as number) ?? 0;
|
|
229
319
|
} else {
|
|
230
320
|
pm.uniforms.uBatchTex.value = DUMMY_STATE_TEX;
|
|
321
|
+
pm.uniforms.uBatchTex2.value = DUMMY_STATE_TEX;
|
|
231
322
|
pm.uniforms.uBatchTexWidth.value = 1;
|
|
232
323
|
pm.uniforms.uBatchTexActive.value = 0;
|
|
233
324
|
}
|
|
325
|
+
// 交叉淡化与 GPU 路径的 uniform 是共享对象(挂在材质 userData 上),
|
|
326
|
+
// 抄引用即可,不需要每帧同步数值
|
|
327
|
+
pm.uniforms.uBatchMix.value = (src?.userData?.uBatchMix?.value as number) ?? 0;
|
|
328
|
+
pm.uniforms.uPathTex.value = src?.userData?.uPathTex?.value ?? DUMMY_PATH_TEX;
|
|
329
|
+
pm.uniforms.uPathCount.value = (src?.userData?.uPathCount?.value as number) ?? 1;
|
|
330
|
+
pm.uniforms.uPathTime.value = (src?.userData?.uPathTime?.value as number) ?? 0;
|
|
331
|
+
pm.uniforms.uPathTexActive.value = (src?.userData?.uPathTexActive?.value as number) ?? 0;
|
|
234
332
|
// 选中那栋自身的拔高也要算,否则选中后再点它会点空
|
|
235
333
|
pm.uniforms.uSelBatch.value = (src?.userData?.uSelBatch?.value as number) ?? -1;
|
|
236
334
|
pm.uniforms.uSelWhole.value = (src?.userData?.uSelWhole?.value as number) ?? 0;
|
|
237
|
-
// uSelElevation 是共享 uniform,挂在材质 uniforms 上而不是 userData
|
|
238
335
|
pm.uniforms.uSelElevation.value =
|
|
239
|
-
(
|
|
336
|
+
(src?.userData?.uSelElevation?.value as number) ??
|
|
337
|
+
(src?.uniforms?.uSelElevation?.value as number) ??
|
|
338
|
+
0;
|
|
240
339
|
|
|
241
340
|
swapped.push([mesh, prev]);
|
|
242
341
|
mesh.material = pm;
|
package/src/render/S3MViewer.ts
CHANGED
|
@@ -234,6 +234,13 @@ export interface S3MViewerOptions {
|
|
|
234
234
|
onReady?: (entry: S3MDatasetEntry) => void;
|
|
235
235
|
onPick?: (result: PickResult | null) => void;
|
|
236
236
|
onError?: (error: unknown) => void;
|
|
237
|
+
/**
|
|
238
|
+
* 首次出现瓦片后自动预热另一种 `shading` 变体(默认开)。
|
|
239
|
+
*
|
|
240
|
+
* `flat` / `smooth` 是编译期变体,不预热的话第一次 `setStyle({ shading })` 会同步
|
|
241
|
+
* 编译着色器、卡一帧。预热在首帧之后异步做,期间跳一两帧渲染,肉眼不可见。
|
|
242
|
+
*/
|
|
243
|
+
prewarmShading?: boolean;
|
|
237
244
|
}
|
|
238
245
|
|
|
239
246
|
/* ------------------------------------------------------------------ 内部类型 */
|
|
@@ -281,6 +288,21 @@ function isPointCloudObject(o: THREE.Object3D): o is THREE.Points {
|
|
|
281
288
|
return (o as unknown as THREE.Points).isPoints === true;
|
|
282
289
|
}
|
|
283
290
|
|
|
291
|
+
/**
|
|
292
|
+
* 全局透明度钳到 [0, 1],**允许到 0**。
|
|
293
|
+
*
|
|
294
|
+
* 以前这里有个 0.05 下限,本意是「别让整层彻底看不见、又还在挡东西」。但那个问题
|
|
295
|
+
* 真正的来源不是透明度,而是「透明物体仍然写深度」:全透明还写深度就会在底图上挖洞。
|
|
296
|
+
* 现在到 0 时 `renderFrame()` 直接跳过绘制(不写颜色也不写深度),语义就干净了。
|
|
297
|
+
*/
|
|
298
|
+
function clampOpacity(v: number | null | undefined): number {
|
|
299
|
+
const n = v ?? 1;
|
|
300
|
+
return n < 0 ? 0 : n > 1 ? 1 : n;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** 低于这个透明度就当作完全不可见:跳过绘制,避免全透明物体写深度 */
|
|
304
|
+
const OPACITY_EPS = 1e-3;
|
|
305
|
+
|
|
284
306
|
/* ------------------------------------------------------------------ 可动画的样式键 */
|
|
285
307
|
|
|
286
308
|
/**
|
|
@@ -410,10 +432,18 @@ export class S3MViewer {
|
|
|
410
432
|
private _tileFadeSeq = 0;
|
|
411
433
|
/** setMode 编排的代次,用来作废被新调用顶掉的旧编排 */
|
|
412
434
|
private _modeGen = 0;
|
|
435
|
+
/** `shading` 另一种变体是否已预热(每个 viewer 只做一次) */
|
|
436
|
+
private _shadingPrewarmed = false;
|
|
437
|
+
/** 是否允许自动预热(选项 `prewarmShading`,默认开) */
|
|
438
|
+
private _prewarmShadingEnabled = true;
|
|
439
|
+
/** 预热进行中:期间跳过渲染,避免把「翻转后的变体」画到屏幕上 */
|
|
440
|
+
private _prewarming = false;
|
|
413
441
|
/** 路径运动的规格表:通道 key → 路径 + 偏移 */
|
|
414
442
|
private readonly _pathSpecs = new Map<string, { path: Path; offset: [number, number, number] }>();
|
|
415
443
|
/** 路径采样临时对象,避免每帧 new */
|
|
416
444
|
private readonly _pathScratch = new Float32Array(3);
|
|
445
|
+
/** 求切线用的第二个采样点(包围盒换算 GPU 路径朝向时用) */
|
|
446
|
+
private readonly _pathScratch2 = new Float32Array(3);
|
|
417
447
|
/** 预制棱线缓存(Crease Cache):从外部 source-creases 载入的全局统一 LineSegments,性能最高、视觉最干净 */
|
|
418
448
|
private _creaseLines: THREE.LineSegments | null = null;
|
|
419
449
|
/** 数据自带的棱线对象(LineSegments),改棱线颜色时只刷这些,不再全场景 traverse */
|
|
@@ -491,6 +521,7 @@ export class S3MViewer {
|
|
|
491
521
|
this.onReady = options.onReady ?? (() => {});
|
|
492
522
|
this.onPick = options.onPick ?? (() => {});
|
|
493
523
|
this.onError = options.onError ?? ((e) => console.warn('[s3m]', e));
|
|
524
|
+
this._prewarmShadingEnabled = options.prewarmShading !== false;
|
|
494
525
|
|
|
495
526
|
this.groundManager = new GroundManager(options.ground);
|
|
496
527
|
if (!this.scene) {
|
|
@@ -621,7 +652,7 @@ export class S3MViewer {
|
|
|
621
652
|
}
|
|
622
653
|
try {
|
|
623
654
|
// 旧上下文已失效,dispose 可能抛错,忽略即可
|
|
624
|
-
this.renderer?.dispose();
|
|
655
|
+
this.renderer?.dispose?.();
|
|
625
656
|
} catch {
|
|
626
657
|
/* noop */
|
|
627
658
|
}
|
|
@@ -660,7 +691,7 @@ export class S3MViewer {
|
|
|
660
691
|
this._pathSpecs.clear();
|
|
661
692
|
this._heightMin = Infinity;
|
|
662
693
|
this._heightMax = -Infinity;
|
|
663
|
-
this.renderer?.dispose();
|
|
694
|
+
this.renderer?.dispose?.();
|
|
664
695
|
this.renderer = null;
|
|
665
696
|
this.canvas = null;
|
|
666
697
|
}
|
|
@@ -1141,6 +1172,52 @@ export class S3MViewer {
|
|
|
1141
1172
|
this._pathSpecs.delete(key);
|
|
1142
1173
|
}
|
|
1143
1174
|
|
|
1175
|
+
/**
|
|
1176
|
+
* 换算 GPU 路径单体在**当前时间**的位置与朝向。
|
|
1177
|
+
*
|
|
1178
|
+
* 位移发生在顶点着色器里,JS 侧要拿到「屏幕上现在在哪」只能按同一套公式在 CPU 复算:
|
|
1179
|
+
* `t = phase + uPathTime * speed`,再按 loop 模式夹到 0~1,最后查弧长表。
|
|
1180
|
+
* 与 StyleMaterial 的 row5 分支逐行对应,改那边时这里必须同步。
|
|
1181
|
+
*/
|
|
1182
|
+
private _gpuPathPoseAt(smid: number): { pos: [number, number, number]; yaw: number } | null {
|
|
1183
|
+
const gp = this._featureStyleManager.getFeatureStyle(smid)?.gpuPath;
|
|
1184
|
+
if (!gp) return null;
|
|
1185
|
+
const path = this.gpuPathManager.getPath(gp.pathId);
|
|
1186
|
+
if (!path || path.length <= 0) return null;
|
|
1187
|
+
|
|
1188
|
+
const time = this.uniforms.uPathTime?.value ?? 0;
|
|
1189
|
+
let t = (gp.phase ?? 0) + time * (gp.speed ?? 0);
|
|
1190
|
+
const loop = gp.loop;
|
|
1191
|
+
if (loop === 'clamp' || loop === false) {
|
|
1192
|
+
t = Math.min(1, Math.max(0, t));
|
|
1193
|
+
} else if (loop === 'pingpong') {
|
|
1194
|
+
t = t % 2;
|
|
1195
|
+
if (t < 0) t += 2;
|
|
1196
|
+
t = t > 1 ? 2 - t : t;
|
|
1197
|
+
} else {
|
|
1198
|
+
t = t - Math.floor(t);
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
path.sampleAt(t, this._pathScratch);
|
|
1202
|
+
const pos: [number, number, number] = [this._pathScratch[0], this._pathScratch[1], this._pathScratch[2]];
|
|
1203
|
+
|
|
1204
|
+
let yaw = 0;
|
|
1205
|
+
if (gp.autoHeading !== false) {
|
|
1206
|
+
const d = 1e-3;
|
|
1207
|
+
path.sampleAt(Math.min(1, t + d), this._pathScratch2);
|
|
1208
|
+
let dx = this._pathScratch2[0] - pos[0];
|
|
1209
|
+
let dy = this._pathScratch2[1] - pos[1];
|
|
1210
|
+
if (dx * dx + dy * dy < 1e-8) {
|
|
1211
|
+
// 已经在终点:退回用「后一点减前一点」求切线
|
|
1212
|
+
path.sampleAt(Math.max(0, t - d), this._pathScratch2);
|
|
1213
|
+
dx = pos[0] - this._pathScratch2[0];
|
|
1214
|
+
dy = pos[1] - this._pathScratch2[1];
|
|
1215
|
+
}
|
|
1216
|
+
if (dx * dx + dy * dy >= 1e-8) yaw = Math.atan2(dy, dx);
|
|
1217
|
+
}
|
|
1218
|
+
return { pos, yaw };
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1144
1221
|
/**
|
|
1145
1222
|
* 注册 GPU 路径到路径库
|
|
1146
1223
|
* @param points 折线点数组或 Path 实例
|
|
@@ -1679,7 +1756,7 @@ export class S3MViewer {
|
|
|
1679
1756
|
} else {
|
|
1680
1757
|
const mat = styledMaterialOf(mesh);
|
|
1681
1758
|
if (mat) {
|
|
1682
|
-
const o =
|
|
1759
|
+
const o = clampOpacity(this.style.opacity);
|
|
1683
1760
|
// 不写 mat.opacity:全局透明度走共享 uOpacity,材质自身 opacity 保留模型自带 alpha
|
|
1684
1761
|
mat.transparent = o < 0.999;
|
|
1685
1762
|
const keepDepth = this.style.depthWrite !== false;
|
|
@@ -2110,7 +2187,7 @@ export class S3MViewer {
|
|
|
2110
2187
|
u.uAmbientGain.value = s.ambientGain;
|
|
2111
2188
|
}
|
|
2112
2189
|
if (u.uOpacity && s.opacity != null) {
|
|
2113
|
-
u.uOpacity.value =
|
|
2190
|
+
u.uOpacity.value = clampOpacity(s.opacity);
|
|
2114
2191
|
}
|
|
2115
2192
|
if (this.outlineMaterial) {
|
|
2116
2193
|
if (this.outlineMaterial.uniforms.uElevation && s.selectElevation != null) {
|
|
@@ -2135,7 +2212,7 @@ export class S3MViewer {
|
|
|
2135
2212
|
* 现在逐帧只写一个 float(`uOpacity`),混合开关只在**真正跨过阈值**时切一次。
|
|
2136
2213
|
*/
|
|
2137
2214
|
private _applyOpacity(): void {
|
|
2138
|
-
const o =
|
|
2215
|
+
const o = clampOpacity(this.style.opacity);
|
|
2139
2216
|
if (this.uniforms.uOpacity) this.uniforms.uOpacity.value = o;
|
|
2140
2217
|
|
|
2141
2218
|
const transparent = o < 0.999;
|
|
@@ -2185,6 +2262,59 @@ export class S3MViewer {
|
|
|
2185
2262
|
this._requestRepaint();
|
|
2186
2263
|
}
|
|
2187
2264
|
|
|
2265
|
+
/**
|
|
2266
|
+
* 预热另一种 `shading` 变体,让首次切换不再同步编译着色器。
|
|
2267
|
+
*
|
|
2268
|
+
* 做法是「把材质翻到另一种变体 → 编译 → 翻回来」:three 的 program 缓存按
|
|
2269
|
+
* cacheKey(含 flatShading / defines)分桶,编译一次就留在缓存里,之后
|
|
2270
|
+
* `_applyShading()` 切过去只是取缓存。
|
|
2271
|
+
*
|
|
2272
|
+
* 翻转期间材质是「错误变体」,所以用 `_prewarming` 让 renderFrame 跳过渲染——
|
|
2273
|
+
* 少画一两帧看不出来,把平滑的楼画成平面的就很明显。
|
|
2274
|
+
* 优先用 `compileAsync`(KHR_parallel_shader_compile),没有就退回同步 `compile`。
|
|
2275
|
+
*/
|
|
2276
|
+
async prewarmShading(): Promise<void> {
|
|
2277
|
+
if (this._shadingPrewarmed || this._prewarming) return;
|
|
2278
|
+
const r = this.renderer;
|
|
2279
|
+
const scene = this.scene;
|
|
2280
|
+
const camera = this.camera;
|
|
2281
|
+
if (!r || !scene || !camera) return;
|
|
2282
|
+
|
|
2283
|
+
const mats: THREE.MeshStandardMaterial[] = [];
|
|
2284
|
+
const seen = new Set<THREE.Material>();
|
|
2285
|
+
for (const p of this._pickables.values()) {
|
|
2286
|
+
const m = styledMaterialOf(p.mesh);
|
|
2287
|
+
if (!m || seen.has(m)) continue;
|
|
2288
|
+
seen.add(m);
|
|
2289
|
+
mats.push(m);
|
|
2290
|
+
}
|
|
2291
|
+
if (mats.length === 0) return;
|
|
2292
|
+
|
|
2293
|
+
this._shadingPrewarmed = true;
|
|
2294
|
+
this._prewarming = true;
|
|
2295
|
+
// 当前是 smooth(flatShading = false)就预热 flat,反之预热 smooth
|
|
2296
|
+
const target = this.style.shading === 'smooth';
|
|
2297
|
+
const saved = mats.map((m) => m.flatShading);
|
|
2298
|
+
for (const m of mats) m.flatShading = target;
|
|
2299
|
+
|
|
2300
|
+
try {
|
|
2301
|
+
const anyR = r as unknown as {
|
|
2302
|
+
compileAsync?: (s: THREE.Object3D, c: THREE.Camera) => Promise<unknown>;
|
|
2303
|
+
};
|
|
2304
|
+
if (typeof anyR.compileAsync === 'function') await anyR.compileAsync(scene, camera);
|
|
2305
|
+
else r.compile(scene, camera);
|
|
2306
|
+
} catch {
|
|
2307
|
+
// 预热失败不影响主流程:最坏情况退回「首次切换时编译一次」
|
|
2308
|
+
} finally {
|
|
2309
|
+
for (let i = 0; i < mats.length; i++) {
|
|
2310
|
+
mats[i].flatShading = saved[i];
|
|
2311
|
+
mats[i].needsUpdate = true;
|
|
2312
|
+
}
|
|
2313
|
+
this._prewarming = false;
|
|
2314
|
+
this._requestRepaint();
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
|
|
2188
2318
|
/**
|
|
2189
2319
|
* 高度范围增量维护。
|
|
2190
2320
|
*
|
|
@@ -2381,7 +2511,7 @@ export class S3MViewer {
|
|
|
2381
2511
|
const mat = new THREE.LineBasicMaterial({
|
|
2382
2512
|
color: new THREE.Color(s.edgeColor),
|
|
2383
2513
|
transparent: true,
|
|
2384
|
-
opacity: Math.min(1, Math.max(0, s.edgeOpacity ?? 0.6)) *
|
|
2514
|
+
opacity: Math.min(1, Math.max(0, s.edgeOpacity ?? 0.6)) * clampOpacity(s.opacity),
|
|
2385
2515
|
depthWrite: false,
|
|
2386
2516
|
});
|
|
2387
2517
|
const lines = new THREE.LineSegments(geo, mat);
|
|
@@ -2416,7 +2546,7 @@ export class S3MViewer {
|
|
|
2416
2546
|
const fade = this.uniforms.uEdgeFade?.value ?? 1;
|
|
2417
2547
|
const opacity =
|
|
2418
2548
|
Math.min(1, Math.max(0, s.edgeOpacity ?? 0.6)) *
|
|
2419
|
-
|
|
2549
|
+
clampOpacity(s.opacity) *
|
|
2420
2550
|
fade;
|
|
2421
2551
|
const u = this.edgeMaterial.uniforms;
|
|
2422
2552
|
u.uColor.value.set(s.edgeColor);
|
|
@@ -2438,7 +2568,7 @@ export class S3MViewer {
|
|
|
2438
2568
|
const baseOpacity = this.style.edgeOpacity ?? 0.6;
|
|
2439
2569
|
mat.opacity =
|
|
2440
2570
|
baseOpacity *
|
|
2441
|
-
|
|
2571
|
+
clampOpacity(this.style.opacity) *
|
|
2442
2572
|
(this.uniforms.uEdgeFade?.value ?? 1);
|
|
2443
2573
|
mat.transparent = true;
|
|
2444
2574
|
mat.polygonOffset = true;
|
|
@@ -2997,10 +3127,27 @@ export class S3MViewer {
|
|
|
2997
3127
|
rotPiv = off.pivot ? new THREE.Vector3(off.pivot[0], off.pivot[1], off.pivot[2]) : new THREE.Vector3(0, 0, 0);
|
|
2998
3128
|
}
|
|
2999
3129
|
|
|
3130
|
+
// GPU 路径:位移发生在顶点着色器里,包围盒要按同一套公式在 CPU 复算,
|
|
3131
|
+
// 否则「运动中的车」的包围盒会停在路径原点(相机 fitBounds / 属性过滤都会错)。
|
|
3132
|
+
const gpuPose = smid != null ? this._gpuPathPoseAt(smid) : null;
|
|
3133
|
+
let headM3: THREE.Matrix3 | null = null;
|
|
3134
|
+
if (gpuPose && gpuPose.yaw !== 0) {
|
|
3135
|
+
const c = Math.cos(gpuPose.yaw);
|
|
3136
|
+
const s = Math.sin(gpuPose.yaw);
|
|
3137
|
+
// 与着色器的 mat3( c, s, 0, -s, c, 0, 0, 0, 1 ) 等价:GLSL 是列优先,
|
|
3138
|
+
// three 的 Matrix3.set 是行优先,所以这里写成 (c, -s; s, c) 才是同一个矩阵。
|
|
3139
|
+
headM3 = new THREE.Matrix3().set(c, -s, 0, s, c, 0, 0, 0, 1);
|
|
3140
|
+
}
|
|
3141
|
+
|
|
3000
3142
|
for (const c of corners) {
|
|
3001
3143
|
if (rotM3 && rotPiv) c.sub(rotPiv).applyMatrix3(rotM3).add(rotPiv);
|
|
3144
|
+
if (headM3) {
|
|
3145
|
+
const piv = rotPiv ?? new THREE.Vector3(0, 0, 0);
|
|
3146
|
+
c.sub(piv).applyMatrix3(headM3).add(piv);
|
|
3147
|
+
}
|
|
3002
3148
|
if (trans) c.add(new THREE.Vector3(trans[0], trans[1], trans[2]));
|
|
3003
3149
|
else if (elev) c.z += elev;
|
|
3150
|
+
if (gpuPose) c.add(new THREE.Vector3(gpuPose.pos[0], gpuPose.pos[1], gpuPose.pos[2]));
|
|
3004
3151
|
|
|
3005
3152
|
c.applyMatrix4(mesh.matrixWorld);
|
|
3006
3153
|
const s = this.sceneAnchor.meterInMercator;
|
|
@@ -3243,7 +3390,7 @@ export class S3MViewer {
|
|
|
3243
3390
|
const u = this.selectionEdgeMaterial.uniforms;
|
|
3244
3391
|
u.uColor.value.set(s.outlineColor ?? s.selectColor);
|
|
3245
3392
|
u.uWidth.value = Math.max(0.1, s.outlineWidthPx ?? 3);
|
|
3246
|
-
u.uOpacity.value =
|
|
3393
|
+
u.uOpacity.value = clampOpacity(s.opacity);
|
|
3247
3394
|
// 只加全局 selectElevation:单体自己的拔高已经写在状态纹理第 2 行 z 里,
|
|
3248
3395
|
// 顶点着色器采样时带出来了,这里再加一次就会双重拔高
|
|
3249
3396
|
if (u.uElevation) u.uElevation.value = s.selectElevation ?? 0;
|
|
@@ -3340,6 +3487,10 @@ export class S3MViewer {
|
|
|
3340
3487
|
const gl = this.renderer.getContext() as WebGLRenderingContext | null;
|
|
3341
3488
|
if (gl && typeof gl.isContextLost === 'function' && gl.isContextLost()) return false;
|
|
3342
3489
|
|
|
3490
|
+
// shading 预热期间材质是「另一种变体」,这一帧先不画:少画一两帧看不出来,
|
|
3491
|
+
// 把平滑的楼画成平面的一眼就能看出来。
|
|
3492
|
+
if (this._prewarming) return true;
|
|
3493
|
+
|
|
3343
3494
|
// 先推进动画,再刷脏标记,最后渲染 —— 保证本帧画出来的是最新值
|
|
3344
3495
|
const animating = this.animator.tick();
|
|
3345
3496
|
|
|
@@ -3407,7 +3558,11 @@ export class S3MViewer {
|
|
|
3407
3558
|
|
|
3408
3559
|
this.renderer.setViewport(0, 0, canvas.width, canvas.height);
|
|
3409
3560
|
this.renderer.resetState();
|
|
3410
|
-
|
|
3561
|
+
// 完全透明时什么都不画:透明物体照样写深度,画了会在底图上挖出一个洞。
|
|
3562
|
+
// 这里跳过的是「本层的绘制」,宿主自己的清屏 / 底图照旧。
|
|
3563
|
+
if ((this.uniforms.uOpacity?.value ?? 1) > OPACITY_EPS) {
|
|
3564
|
+
this.renderer.render(this.scene, this.camera);
|
|
3565
|
+
}
|
|
3411
3566
|
|
|
3412
3567
|
this._frame++;
|
|
3413
3568
|
const now = performance.now();
|
|
@@ -3419,13 +3574,19 @@ export class S3MViewer {
|
|
|
3419
3574
|
|
|
3420
3575
|
// 动态着色 / 动画进行中 / 瓦片加载中 / 瓦片层级或可见性改变,都需要宿主继续驱动下一帧。
|
|
3421
3576
|
// 动画结束那一帧必须精确回到 false,否则 MapLibre 会永远重绘(耗电、发热)。
|
|
3422
|
-
|
|
3577
|
+
const needsNextFrame =
|
|
3423
3578
|
animating ||
|
|
3424
3579
|
this.style.mode === STYLE.DYNAMIC ||
|
|
3425
3580
|
gpuPathActive ||
|
|
3426
3581
|
this.stats.loading > 0 ||
|
|
3427
|
-
tilesetChanged
|
|
3428
|
-
|
|
3582
|
+
tilesetChanged;
|
|
3583
|
+
|
|
3584
|
+
// 首帧画完、场上有材质了,再把另一种 shading 变体编出来(异步,不挡当前帧)
|
|
3585
|
+
if (this._prewarmShadingEnabled && !this._shadingPrewarmed && this._pickables.size > 0) {
|
|
3586
|
+
void this.prewarmShading();
|
|
3587
|
+
}
|
|
3588
|
+
|
|
3589
|
+
return needsNextFrame;
|
|
3429
3590
|
}
|
|
3430
3591
|
|
|
3431
3592
|
/* ---------------------------------------------------------------- 只读属性 */
|
|
@@ -402,9 +402,12 @@ const VERT_SETUP = /* glsl */ `
|
|
|
402
402
|
if ( autoHeading ) {
|
|
403
403
|
float c = cos( pathYaw );
|
|
404
404
|
float s = sin( pathYaw );
|
|
405
|
+
|
|
406
|
+
// GLSL 的 mat3 是列优先:下面两行是「第 0 列 = (c, s)」,等价于绕 Z 转 +yaw,
|
|
407
|
+
// 把模型 +X 朝向对到切线方向(yaw = atan2(dy, dx))。写成 (c,-s; s,c) 会左右镜像。
|
|
405
408
|
mat3 rotHeading = mat3(
|
|
406
|
-
c,
|
|
407
|
-
s,
|
|
409
|
+
c, s, 0.0,
|
|
410
|
+
-s, c, 0.0,
|
|
408
411
|
0.0, 0.0, 1.0
|
|
409
412
|
);
|
|
410
413
|
s3mRot = rotHeading * s3mRot;
|
|
@@ -869,6 +872,14 @@ export function createStyledMaterial(opts: StyledMaterialOptions): StyledMateria
|
|
|
869
872
|
|
|
870
873
|
mat.userData.uSelBatch = uSelBatch;
|
|
871
874
|
mat.userData.uSelWhole = uSelWhole;
|
|
875
|
+
// 拾取材质要读这些共享 uniform 的当前值:挂引用而不是拷数值,主材质更新后拾取自动跟上
|
|
876
|
+
// (单测里可能不传共享 uniforms,这里按可选处理)
|
|
877
|
+
mat.userData.uSelElevation = uniforms?.uSelElevation;
|
|
878
|
+
mat.userData.uBatchMix = uniforms?.uBatchMix;
|
|
879
|
+
mat.userData.uPathTex = uniforms?.uPathTex;
|
|
880
|
+
mat.userData.uPathCount = uniforms?.uPathCount;
|
|
881
|
+
mat.userData.uPathTime = uniforms?.uPathTime;
|
|
882
|
+
mat.userData.uPathTexActive = uniforms?.uPathTexActive;
|
|
872
883
|
mat.userData.uBatchTex = uBatchTex;
|
|
873
884
|
mat.userData.uBatchTex2 = uBatchTex2;
|
|
874
885
|
mat.userData.uBatchTexWidth = uBatchTexWidth;
|
|
@@ -959,9 +970,12 @@ const OUTLINE_VERT = /* glsl */ `
|
|
|
959
970
|
if ( autoHeading ) {
|
|
960
971
|
float c = cos( pathYaw );
|
|
961
972
|
float s = sin( pathYaw );
|
|
973
|
+
|
|
974
|
+
// GLSL 的 mat3 是列优先:下面两行是「第 0 列 = (c, s)」,等价于绕 Z 转 +yaw,
|
|
975
|
+
// 把模型 +X 朝向对到切线方向(yaw = atan2(dy, dx))。写成 (c,-s; s,c) 会左右镜像。
|
|
962
976
|
mat3 rotHeading = mat3(
|
|
963
|
-
c,
|
|
964
|
-
s,
|
|
977
|
+
c, s, 0.0,
|
|
978
|
+
-s, c, 0.0,
|
|
965
979
|
0.0, 0.0, 1.0
|
|
966
980
|
);
|
|
967
981
|
s3mRot = rotHeading * s3mRot;
|