s3m-three 0.3.8 → 0.3.9

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 (36) hide show
  1. package/dist/browser/s3m-three.iife.ext-three.js +187 -46
  2. package/dist/browser/s3m-three.iife.js +187 -46
  3. package/dist/core/types.d.ts +4 -0
  4. package/dist/core/types.d.ts.map +1 -1
  5. package/dist/index.d.ts +3 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +2 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/render/EdgeLines.d.ts +6 -0
  10. package/dist/render/EdgeLines.d.ts.map +1 -1
  11. package/dist/render/EdgeLines.js +41 -13
  12. package/dist/render/EdgeLines.js.map +1 -1
  13. package/dist/render/FeatureStyleManager.d.ts +45 -0
  14. package/dist/render/FeatureStyleManager.d.ts.map +1 -1
  15. package/dist/render/FeatureStyleManager.js +320 -59
  16. package/dist/render/FeatureStyleManager.js.map +1 -1
  17. package/dist/render/OrthoProjection.d.ts +6 -0
  18. package/dist/render/OrthoProjection.d.ts.map +1 -1
  19. package/dist/render/OrthoProjection.js +48 -0
  20. package/dist/render/OrthoProjection.js.map +1 -1
  21. package/dist/render/S3MViewer.d.ts +19 -0
  22. package/dist/render/S3MViewer.d.ts.map +1 -1
  23. package/dist/render/S3MViewer.js +122 -6
  24. package/dist/render/S3MViewer.js.map +1 -1
  25. package/dist/render/StyleMaterial.d.ts +9 -0
  26. package/dist/render/StyleMaterial.d.ts.map +1 -1
  27. package/dist/render/StyleMaterial.js +153 -21
  28. package/dist/render/StyleMaterial.js.map +1 -1
  29. package/package.json +1 -1
  30. package/src/core/types.ts +2 -0
  31. package/src/index.ts +6 -0
  32. package/src/render/EdgeLines.ts +43 -13
  33. package/src/render/FeatureStyleManager.ts +405 -64
  34. package/src/render/OrthoProjection.ts +52 -0
  35. package/src/render/S3MViewer.ts +162 -5
  36. package/src/render/StyleMaterial.ts +157 -21
@@ -94,6 +94,8 @@ import {
94
94
  type FieldElevationOptions,
95
95
  type FieldElevationResult,
96
96
  type S3MFilter,
97
+ resolveRotationMatrix,
98
+ extractEulerAngles,
97
99
  } from './FeatureStyleManager.js';
98
100
 
99
101
  import {
@@ -304,7 +306,7 @@ const STYLE_SCALAR_KEYS = [
304
306
  const STYLE_COLOR_KEYS = ['colorA', 'colorB', 'colorC', 'selectColor', 'edgeColor'] as const;
305
307
 
306
308
  /** 可动画的单体属性 */
307
- const FEATURE_PROPS = ['translation', 'elevation', 'opacity', 'color'] as const;
309
+ const FEATURE_PROPS = ['translation', 'elevation', 'opacity', 'color', 'rotation'] as const;
308
310
  type FeatureProp = (typeof FEATURE_PROPS)[number];
309
311
 
310
312
  /** 单体沿折线运动的参数 */
@@ -948,6 +950,111 @@ export class S3MViewer {
948
950
  this.moveFeature(smid, [cur[0] + delta[0], cur[1] + delta[1], cur[2] + delta[2]], transition);
949
951
  }
950
952
 
953
+ /**
954
+ * 让单体进行刚体姿态旋转(在 GPU 顶点着色器中以 pivot 轴心进行 3x3 矩阵旋转变换)。
955
+ *
956
+ * @param smid 单体 SMID
957
+ * @param rotation 旋转弧度或姿态:数字(绕 Z 轴 yaw 弧度)| [rx, ry, rz] 欧拉角 | [x,y,z,w] 四元数 | Euler/Quaternion/Matrix
958
+ * @param pivot 旋转中心轴心点 [px, py, pz](局部坐标,默认 [0, 0, 0])
959
+ * @param transition 动画过渡参数
960
+ */
961
+ rotateFeature(
962
+ smid: number,
963
+ rotation:
964
+ | number
965
+ | [number, number, number]
966
+ | [number, number, number, number]
967
+ | THREE.Euler
968
+ | THREE.Quaternion
969
+ | THREE.Matrix3
970
+ | THREE.Matrix4,
971
+ pivot?: [number, number, number],
972
+ transition?: TransitionOptions
973
+ ): void {
974
+ const cur = this._featureStyleManager.getFeatureStyle(smid) ?? {};
975
+ if (pivot) {
976
+ this._featureStyleManager.setFeatureStyle(smid, { ...cur, pivot }, this._meshesOf([smid]));
977
+ }
978
+ const targetEuler = extractEulerAngles(rotation);
979
+ const key = this._ensureFeatureChannel(smid, 'rotation');
980
+ this.animator.to(key, targetEuler, transition);
981
+ }
982
+
983
+ /** 让单体增量旋转的便捷写法 */
984
+ rotateFeatureBy(
985
+ smid: number,
986
+ deltaRotation: number | [number, number, number],
987
+ pivot?: [number, number, number],
988
+ transition?: TransitionOptions
989
+ ): void {
990
+ const curEuler = extractEulerAngles(this._featureStyleManager.getFeatureStyle(smid)?.rotation);
991
+ const d = typeof deltaRotation === 'number' ? [0, 0, deltaRotation] : deltaRotation;
992
+ this.rotateFeature(
993
+ smid,
994
+ [curEuler[0] + d[0], curEuler[1] + d[1], curEuler[2] + d[2]],
995
+ pivot,
996
+ transition
997
+ );
998
+ }
999
+
1000
+ /**
1001
+ * 双纹理 GPU 交叉淡化(跨状态 / 专题图平滑过渡)。
1002
+ *
1003
+ * 采用双纹理 (uBatchTex + uBatchTex2 + uBatchMix) 硬件混合,
1004
+ * 即使几千上万栋建筑全量切换配色/高度/显隐/姿态,也无需在 CPU 逐单体创建通道,
1005
+ * 由 GPU 顶点与片元着色器以 60FPS 零成本硬件插值完成过渡。
1006
+ */
1007
+ crossFadeStyles(
1008
+ targetStyles: Map<number, FeatureStyle> | Record<string | number, FeatureStyle>,
1009
+ transition?: TransitionOptions
1010
+ ): void {
1011
+ const targetMap =
1012
+ targetStyles instanceof Map
1013
+ ? targetStyles
1014
+ : new Map(Object.entries(targetStyles).map(([k, v]) => [Number(k), v]));
1015
+
1016
+ const meshes = this.getAllMeshes();
1017
+ this._featureStyleManager.prepareCrossFade(targetMap, meshes);
1018
+
1019
+ const key = 'batch.mix';
1020
+ if (!this.animator.has(key)) {
1021
+ this.animator.register(key, {
1022
+ dim: 1,
1023
+ read: (out) => {
1024
+ out[0] = this.uniforms.uBatchMix?.value ?? 0;
1025
+ },
1026
+ write: (v) => {
1027
+ if (this.uniforms.uBatchMix) {
1028
+ this.uniforms.uBatchMix.value = v[0];
1029
+ }
1030
+ this.triggerRepaint();
1031
+ },
1032
+ });
1033
+ }
1034
+
1035
+ const duration = transition?.duration ?? 0;
1036
+ const wantAnim = duration > 0 || !!transition?.spring;
1037
+ if (!wantAnim) {
1038
+ if (this.animator.has(key)) this.animator.cancel(key);
1039
+ if (this.uniforms.uBatchMix) this.uniforms.uBatchMix.value = 0;
1040
+ this._featureStyleManager.commitCrossFade(targetMap, meshes);
1041
+ this.triggerRepaint();
1042
+ transition?.onComplete?.();
1043
+ return;
1044
+ }
1045
+
1046
+ if (this.uniforms.uBatchMix) this.uniforms.uBatchMix.value = 0;
1047
+ this.animator.to(key, 1, {
1048
+ ...transition,
1049
+ onComplete: () => {
1050
+ if (this.uniforms.uBatchMix) this.uniforms.uBatchMix.value = 0;
1051
+ this._featureStyleManager.commitCrossFade(targetMap, meshes);
1052
+ this.triggerRepaint();
1053
+ transition?.onComplete?.();
1054
+ },
1055
+ });
1056
+ }
1057
+
951
1058
  /**
952
1059
  * 让单体沿折线运动。
953
1060
  *
@@ -1034,6 +1141,7 @@ export class S3MViewer {
1034
1141
  */
1035
1142
  animateFeature(smid: number, style: Partial<FeatureStyle>, transition?: TransitionOptions): void {
1036
1143
  if (style.translation) this.moveFeature(smid, style.translation, transition);
1144
+ if (style.rotation != null) this.rotateFeature(smid, style.rotation, style.pivot, transition);
1037
1145
  if (style.elevation != null) {
1038
1146
  this.animator.to(this._ensureFeatureChannel(smid, 'elevation'), style.elevation, transition);
1039
1147
  }
@@ -1127,7 +1235,7 @@ export class S3MViewer {
1127
1235
  const key = `feature.${smid}.${prop}`;
1128
1236
  if (this.animator.has(key)) return key;
1129
1237
 
1130
- const channel: Channel = prop === 'translation' || prop === 'color'
1238
+ const channel: Channel = prop === 'translation' || prop === 'color' || prop === 'rotation'
1131
1239
  ? {
1132
1240
  dim: 3,
1133
1241
  read: (out) => {
@@ -1136,6 +1244,12 @@ export class S3MViewer {
1136
1244
  out[0] = t?.[0] ?? 0;
1137
1245
  out[1] = t?.[1] ?? 0;
1138
1246
  out[2] = t?.[2] ?? 0;
1247
+ } else if (prop === 'rotation') {
1248
+ const r = this._featureStyleManager.getFeatureStyle(smid)?.rotation;
1249
+ const euler = extractEulerAngles(r);
1250
+ out[0] = euler[0];
1251
+ out[1] = euler[1];
1252
+ out[2] = euler[2];
1139
1253
  } else {
1140
1254
  // 没有自定义色时以图层主色为起点,插值才连续
1141
1255
  const hex = this._featureStyleManager.getFeatureStyle(smid)?.color ?? this.style.colorA;
@@ -1154,9 +1268,21 @@ export class S3MViewer {
1154
1268
  { ...cur, translation: [v[0], v[1], v[2]] },
1155
1269
  meshes
1156
1270
  );
1157
- // 有位移就关掉视锥剔除,否则移出原始包围球后整块瓦片会凭空消失
1158
- const moving = Math.abs(v[0]) > 1e-6 || Math.abs(v[1]) > 1e-6 || Math.abs(v[2]) > 1e-6;
1159
- for (const mesh of meshes) mesh.frustumCulled = !moving;
1271
+ // 有位移或旋转就关掉视锥剔除,否则移出原始包围球后整块瓦片会凭空消失
1272
+ const transMoving = Math.abs(v[0]) > 1e-6 || Math.abs(v[1]) > 1e-6 || Math.abs(v[2]) > 1e-6;
1273
+ const rotEuler = extractEulerAngles(cur.rotation);
1274
+ const rotMoving = Math.abs(rotEuler[0]) > 1e-6 || Math.abs(rotEuler[1]) > 1e-6 || Math.abs(rotEuler[2]) > 1e-6;
1275
+ for (const mesh of meshes) mesh.frustumCulled = !(transMoving || rotMoving);
1276
+ } else if (prop === 'rotation') {
1277
+ this._featureStyleManager.setFeatureStyle(
1278
+ smid,
1279
+ { ...cur, rotation: [v[0], v[1], v[2]] },
1280
+ meshes
1281
+ );
1282
+ const rotMoving = Math.abs(v[0]) > 1e-6 || Math.abs(v[1]) > 1e-6 || Math.abs(v[2]) > 1e-6;
1283
+ const t = cur.translation ?? [0, 0, 0];
1284
+ const transMoving = Math.abs(t[0]) > 1e-6 || Math.abs(t[1]) > 1e-6 || Math.abs(t[2]) > 1e-6;
1285
+ for (const mesh of meshes) mesh.frustumCulled = !(rotMoving || transMoving);
1160
1286
  } else {
1161
1287
  const c = this._colorScratch.setRGB(v[0], v[1], v[2]);
1162
1288
  this._featureStyleManager.setFeatureStyle(
@@ -2322,6 +2448,8 @@ export class S3MViewer {
2322
2448
  const batch = styled?.userData?.uBatchTex
2323
2449
  ? {
2324
2450
  uBatchTex: styled.userData.uBatchTex,
2451
+ uBatchTex2: styled.userData.uBatchTex2,
2452
+ uBatchMix: this.uniforms.uBatchMix,
2325
2453
  uBatchTexWidth: styled.userData.uBatchTexWidth,
2326
2454
  uBatchTexActive: styled.userData.uBatchTexActive,
2327
2455
  }
@@ -2335,6 +2463,8 @@ export class S3MViewer {
2335
2463
  const batch = styled?.userData?.uBatchTex
2336
2464
  ? {
2337
2465
  uBatchTex: styled.userData.uBatchTex,
2466
+ uBatchTex2: styled.userData.uBatchTex2,
2467
+ uBatchMix: this.uniforms.uBatchMix,
2338
2468
  uBatchTexWidth: styled.userData.uBatchTexWidth,
2339
2469
  uBatchTexActive: styled.userData.uBatchTexActive,
2340
2470
  }
@@ -2501,6 +2631,20 @@ export class S3MViewer {
2501
2631
  const trans = off.translation;
2502
2632
  const elev = off.elevation;
2503
2633
  const finalPos = hitPoint.clone();
2634
+
2635
+ if (off.rotation) {
2636
+ const rotMat = resolveRotationMatrix(off.rotation);
2637
+ if (rotMat) {
2638
+ const m3 = new THREE.Matrix3().set(
2639
+ rotMat[0], rotMat[1], rotMat[2],
2640
+ rotMat[3], rotMat[4], rotMat[5],
2641
+ rotMat[6], rotMat[7], rotMat[8]
2642
+ );
2643
+ const piv = off.pivot ? new THREE.Vector3(off.pivot[0], off.pivot[1], off.pivot[2]) : new THREE.Vector3(0, 0, 0);
2644
+ finalPos.sub(piv).applyMatrix3(m3).add(piv);
2645
+ }
2646
+ }
2647
+
2504
2648
  if (trans) finalPos.add(new THREE.Vector3(trans[0], trans[1], trans[2]));
2505
2649
  else if (elev) finalPos.z += elev;
2506
2650
 
@@ -2731,7 +2875,20 @@ export class S3MViewer {
2731
2875
  const trans = off.translation;
2732
2876
  const elev = off.elevation;
2733
2877
 
2878
+ const rotMat = off.rotation ? resolveRotationMatrix(off.rotation) : null;
2879
+ let rotM3: THREE.Matrix3 | null = null;
2880
+ let rotPiv: THREE.Vector3 | null = null;
2881
+ if (rotMat) {
2882
+ rotM3 = new THREE.Matrix3().set(
2883
+ rotMat[0], rotMat[1], rotMat[2],
2884
+ rotMat[3], rotMat[4], rotMat[5],
2885
+ rotMat[6], rotMat[7], rotMat[8]
2886
+ );
2887
+ rotPiv = off.pivot ? new THREE.Vector3(off.pivot[0], off.pivot[1], off.pivot[2]) : new THREE.Vector3(0, 0, 0);
2888
+ }
2889
+
2734
2890
  for (const c of corners) {
2891
+ if (rotM3 && rotPiv) c.sub(rotPiv).applyMatrix3(rotM3).add(rotPiv);
2735
2892
  if (trans) c.add(new THREE.Vector3(trans[0], trans[1], trans[2]));
2736
2893
  else if (elev) c.z += elev;
2737
2894
 
@@ -172,6 +172,7 @@ export interface StyledMaterial extends THREE.MeshStandardMaterial {
172
172
  uSelBatch: { value: number };
173
173
  uSelWhole: { value: number };
174
174
  uBatchTex: { value: THREE.Texture };
175
+ uBatchTex2: { value: THREE.Texture };
175
176
  uBatchTexWidth: { value: number };
176
177
  uBatchTexActive: { value: number };
177
178
  /** 单块瓦片的淡入系数(1 = 完全显示) */
@@ -181,8 +182,14 @@ export interface StyledMaterial extends THREE.MeshStandardMaterial {
181
182
  };
182
183
  }
183
184
 
184
- const DUMMY_DATA = new Float32Array([-1, -1, -1, 0, 0, 0, 0, 0]);
185
- export const DUMMY_BATCH_TEX = new THREE.DataTexture(DUMMY_DATA, 1, 2, THREE.RGBAFormat, THREE.FloatType);
185
+ const DUMMY_DATA = new Float32Array([
186
+ -1, -1, -1, 0, // Row 0: color rgb, elevation/hidden
187
+ 0, 0, 0, 0, // Row 1: translation xyz, opacity
188
+ 1, 0, 0, 0, // Row 2: rot row 0, pivot.x
189
+ 0, 1, 0, 0, // Row 3: rot row 1, pivot.y
190
+ 0, 0, 1, 0, // Row 4: rot row 2, pivot.z
191
+ ]);
192
+ export const DUMMY_BATCH_TEX = new THREE.DataTexture(DUMMY_DATA, 1, 5, THREE.RGBAFormat, THREE.FloatType);
186
193
  DUMMY_BATCH_TEX.minFilter = THREE.NearestFilter;
187
194
  DUMMY_BATCH_TEX.magFilter = THREE.NearestFilter;
188
195
  DUMMY_BATCH_TEX.needsUpdate = true;
@@ -226,6 +233,8 @@ export function createSharedUniforms(): StyleUniforms {
226
233
  uTexMix: { value: 0 },
227
234
  /** 基础底色增益 / 暗部补偿(0~1,默认 0.0) */
228
235
  uAmbientGain: { value: 0 },
236
+ /** 双纹理交叉淡化插值系数(0 = 纯 uBatchTex, 1 = 纯 uBatchTex2) */
237
+ uBatchMix: { value: 0 },
229
238
  /**
230
239
  * 图层整体透明度:在片元里乘进 diffuseColor.a。
231
240
  *
@@ -264,6 +273,8 @@ const VERT_PARS = /* glsl */ `
264
273
  uniform float uSelWhole;
265
274
  uniform float uSelElevation;
266
275
  uniform sampler2D uBatchTex;
276
+ uniform sampler2D uBatchTex2;
277
+ uniform float uBatchMix;
267
278
  uniform float uBatchTexWidth;
268
279
  uniform int uBatchTexActive;
269
280
  varying float vBatchId;
@@ -272,6 +283,11 @@ const VERT_PARS = /* glsl */ `
272
283
  varying vec3 vWorldNormal;
273
284
  varying vec3 vWorldPos;
274
285
 
286
+ // 跨 chunk 共享的顶点姿态变量(在 beginnormal_vertex 建立,begin_vertex / worldpos 使用)
287
+ vec3 s3mCustomOffset;
288
+ mat3 s3mRot;
289
+ vec3 s3mPivot;
290
+
275
291
  /** 当前图元所属单体的控制位:实例化取实例属性,其余取逐顶点属性 */
276
292
  float s3mBatchRaw() {
277
293
  #ifdef USE_INSTANCING
@@ -280,27 +296,85 @@ const VERT_PARS = /* glsl */ `
280
296
  return aBatchId;
281
297
  #endif
282
298
  }
299
+
300
+ vec4 s3mSampleBatch( sampler2D texA, sampler2D texB, vec2 uv, float mixVal ) {
301
+ vec4 vA = texture2D( texA, uv );
302
+ if ( mixVal <= 0.0001 ) return vA;
303
+ vec4 vB = texture2D( texB, uv );
304
+ return mix( vA, vB, mixVal );
305
+ }
306
+
307
+ vec4 s3mSampleBatchRow0( sampler2D texA, sampler2D texB, vec2 uv, float mixVal ) {
308
+ vec4 vA = texture2D( texA, uv );
309
+ if ( mixVal <= 0.0001 ) return vA;
310
+ vec4 vB = texture2D( texB, uv );
311
+ vec4 res = mix( vA, vB, mixVal );
312
+ if ( vA.r < 0.0 && vB.r >= 0.0 ) {
313
+ res.rgb = vB.rgb;
314
+ } else if ( vA.r >= 0.0 && vB.r < 0.0 ) {
315
+ res.rgb = vA.rgb;
316
+ }
317
+ if ( vA.a < -0.5 || vB.a < -0.5 ) {
318
+ res.a = ( mixVal >= 0.5 ) ? vB.a : vA.a;
319
+ }
320
+ return res;
321
+ }
322
+
323
+ vec4 s3mSampleBatchRow1( sampler2D texA, sampler2D texB, vec2 uv, float mixVal ) {
324
+ vec4 vA = texture2D( texA, uv );
325
+ if ( mixVal <= 0.0001 ) return vA;
326
+ vec4 vB = texture2D( texB, uv );
327
+ vec4 res = mix( vA, vB, mixVal );
328
+ if ( vA.a == 0.0 && vB.a > 0.0 ) {
329
+ res.a = mix( 1.0, vB.a, mixVal );
330
+ } else if ( vA.a > 0.0 && vB.a == 0.0 ) {
331
+ res.a = mix( vA.a, 1.0, mixVal );
332
+ }
333
+ return res;
334
+ }
283
335
  `;
284
336
 
285
- const VERT_ELEVATION = /* glsl */ `
286
- float s3mBatch = s3mBatchRaw();
287
- vec3 s3mCustomOffset = vec3( 0.0 );
288
- if ( uBatchTexActive > 0 && s3mBatch >= 0.0 ) {
289
- vec4 bState = texture2D( uBatchTex, vec2( ( s3mBatch + 0.5 ) / uBatchTexWidth, 0.25 ) );
290
- if ( bState.a < -0.5 ) {
291
- transformed = vec3( 0.0 );
292
- } else {
293
- vec4 bOffset = texture2D( uBatchTex, vec2( ( s3mBatch + 0.5 ) / uBatchTexWidth, 0.75 ) );
337
+ const VERT_SETUP = /* glsl */ `
338
+ s3mCustomOffset = vec3( 0.0 );
339
+ s3mRot = mat3( 1.0 );
340
+ s3mPivot = vec3( 0.0 );
341
+ float s3mBatchSetup = s3mBatchRaw();
342
+ if ( uBatchTexActive > 0 && s3mBatchSetup >= 0.0 ) {
343
+ float s3mU = ( s3mBatchSetup + 0.5 ) / uBatchTexWidth;
344
+ vec4 bState = s3mSampleBatchRow0( uBatchTex, uBatchTex2, vec2( s3mU, 0.1 ), uBatchMix );
345
+ if ( bState.a >= -0.5 ) {
346
+ vec4 bOffset = s3mSampleBatchRow1( uBatchTex, uBatchTex2, vec2( s3mU, 0.3 ), uBatchMix );
294
347
  s3mCustomOffset = bOffset.xyz;
295
348
  if ( s3mCustomOffset.z == 0.0 && bState.a > 0.0 ) {
296
349
  s3mCustomOffset.z = bState.a;
297
350
  }
351
+ vec4 r2 = s3mSampleBatch( uBatchTex, uBatchTex2, vec2( s3mU, 0.5 ), uBatchMix );
352
+ vec4 r3 = s3mSampleBatch( uBatchTex, uBatchTex2, vec2( s3mU, 0.7 ), uBatchMix );
353
+ vec4 r4 = s3mSampleBatch( uBatchTex, uBatchTex2, vec2( s3mU, 0.9 ), uBatchMix );
354
+ s3mRot = mat3(
355
+ vec3( r2.x, r3.x, r4.x ),
356
+ vec3( r2.y, r3.y, r4.y ),
357
+ vec3( r2.z, r3.z, r4.z )
358
+ );
359
+ s3mPivot = vec3( r2.w, r3.w, r4.w );
360
+ }
361
+ }
362
+ objectNormal = normalize( s3mRot * objectNormal );
363
+ `;
364
+
365
+ const VERT_ELEVATION = /* glsl */ `
366
+ float s3mBatchElev = s3mBatchRaw();
367
+ if ( uBatchTexActive > 0 && s3mBatchElev >= 0.0 ) {
368
+ float s3mU = ( s3mBatchElev + 0.5 ) / uBatchTexWidth;
369
+ vec4 bState = s3mSampleBatchRow0( uBatchTex, uBatchTex2, vec2( s3mU, 0.1 ), uBatchMix );
370
+ if ( bState.a < -0.5 ) {
371
+ transformed = vec3( 0.0 );
298
372
  }
299
373
  }
300
374
 
301
- float s3mSelElev = max( step( 0.0, uSelBatch ) * step( abs( s3mBatch - uSelBatch ), 0.5 ), uSelWhole );
302
- // 实例化时这是局部坐标,实例矩阵只含平移 + 绕 z 旋转(白模摆位),所以偏移仍沿局部轴生效
303
- transformed += s3mCustomOffset;
375
+ float s3mSelElev = max( step( 0.0, uSelBatch ) * step( abs( s3mBatchElev - uSelBatch ), 0.5 ), uSelWhole );
376
+ // 刚体旋转以 s3mPivot 为中心,再叠加自定义平移与选中拔高
377
+ transformed = s3mRot * ( transformed - s3mPivot ) + s3mPivot + s3mCustomOffset;
304
378
  transformed.z += s3mSelElev * uSelElevation;
305
379
  `;
306
380
 
@@ -328,7 +402,7 @@ const VERT_WORLD = /* glsl */ `
328
402
  #else
329
403
  mat3 s3mNormMat = mat3( modelMatrix );
330
404
  #endif
331
- vWorldNormal = normalize( s3mNormMat * normal );
405
+ vWorldNormal = normalize( s3mNormMat * ( s3mRot * normal ) );
332
406
  `;
333
407
 
334
408
  const FRAG_PARS = /* glsl */ `
@@ -411,6 +485,8 @@ const FRAG_PARS = /* glsl */ `
411
485
  uniform float uSelEmissive;
412
486
  uniform float uDimFactor;
413
487
  uniform sampler2D uBatchTex;
488
+ uniform sampler2D uBatchTex2;
489
+ uniform float uBatchMix;
414
490
  uniform float uBatchTexWidth;
415
491
  uniform int uBatchTexActive;
416
492
  uniform float uFilterMinZ;
@@ -436,6 +512,35 @@ const FRAG_PARS = /* glsl */ `
436
512
  varying vec3 vWorldNormal;
437
513
  varying vec3 vWorldPos;
438
514
 
515
+ vec4 s3mSampleBatchRow0( sampler2D texA, sampler2D texB, vec2 uv, float mixVal ) {
516
+ vec4 vA = texture2D( texA, uv );
517
+ if ( mixVal <= 0.0001 ) return vA;
518
+ vec4 vB = texture2D( texB, uv );
519
+ vec4 res = mix( vA, vB, mixVal );
520
+ if ( vA.r < 0.0 && vB.r >= 0.0 ) {
521
+ res.rgb = vB.rgb;
522
+ } else if ( vA.r >= 0.0 && vB.r < 0.0 ) {
523
+ res.rgb = vA.rgb;
524
+ }
525
+ if ( vA.a < -0.5 || vB.a < -0.5 ) {
526
+ res.a = ( mixVal >= 0.5 ) ? vB.a : vA.a;
527
+ }
528
+ return res;
529
+ }
530
+
531
+ vec4 s3mSampleBatchRow1( sampler2D texA, sampler2D texB, vec2 uv, float mixVal ) {
532
+ vec4 vA = texture2D( texA, uv );
533
+ if ( mixVal <= 0.0001 ) return vA;
534
+ vec4 vB = texture2D( texB, uv );
535
+ vec4 res = mix( vA, vB, mixVal );
536
+ if ( vA.a == 0.0 && vB.a > 0.0 ) {
537
+ res.a = mix( 1.0, vB.a, mixVal );
538
+ } else if ( vA.a > 0.0 && vB.a == 0.0 ) {
539
+ res.a = mix( vA.a, 1.0, mixVal );
540
+ }
541
+ return res;
542
+ }
543
+
439
544
  float s3mHash( float p ) {
440
545
  p = fract( p * 0.1031 );
441
546
  p *= p + 33.33;
@@ -531,7 +636,8 @@ const FRAG_COLOR = /* glsl */ `
531
636
  bool hasCustomColor = false;
532
637
  if ( uBatchTexActive > 0 && s3mBid >= 0.0 ) {
533
638
  // 用取整后的 id 采样:否则逐像素的插值误差会让纹素在边界上来回跳,同样出彩色噪点
534
- vec4 bState = texture2D( uBatchTex, vec2( ( s3mBid + 0.5 ) / uBatchTexWidth, 0.25 ) );
639
+ float s3mU = ( s3mBid + 0.5 ) / uBatchTexWidth;
640
+ vec4 bState = s3mSampleBatchRow0( uBatchTex, uBatchTex2, vec2( s3mU, 0.1 ), uBatchMix );
535
641
  if ( bState.a < -0.5 ) {
536
642
  discard; // 属于被过滤隐藏的单体
537
643
  }
@@ -542,7 +648,7 @@ const FRAG_COLOR = /* glsl */ `
542
648
  }
543
649
  // 单体独立透明度(状态纹理第 2 行 alpha,0 = 无覆盖)。
544
650
  // 与全局 uOpacity(diffuseColor.a 初值)和图集贴图自带 alpha 逐级叠乘
545
- vec4 bOffset = texture2D( uBatchTex, vec2( ( s3mBid + 0.5 ) / uBatchTexWidth, 0.75 ) );
651
+ vec4 bOffset = s3mSampleBatchRow1( uBatchTex, uBatchTex2, vec2( s3mU, 0.3 ), uBatchMix );
546
652
  if ( bOffset.a > 0.0 ) {
547
653
  diffuseColor.a *= bOffset.a;
548
654
  }
@@ -643,6 +749,7 @@ export function createStyledMaterial(opts: StyledMaterialOptions): StyledMateria
643
749
  // 没有单体化信息的几何(batchId = -1)只能整块选中
644
750
  const uSelWhole = { value: 0 };
645
751
  const uBatchTex = { value: DUMMY_BATCH_TEX };
752
+ const uBatchTex2 = { value: DUMMY_BATCH_TEX };
646
753
  const uBatchTexWidth = { value: 1.0 };
647
754
  const uBatchTexActive = { value: 0 };
648
755
  /** 单块瓦片的淡入系数:逐材质独享(每块网格一份材质),默认 1 = 完全显示 */
@@ -662,6 +769,7 @@ export function createStyledMaterial(opts: StyledMaterialOptions): StyledMateria
662
769
  uSelBatch,
663
770
  uSelWhole,
664
771
  uBatchTex,
772
+ uBatchTex2,
665
773
  uBatchTexWidth,
666
774
  uBatchTexActive,
667
775
  uTileFade,
@@ -672,6 +780,7 @@ export function createStyledMaterial(opts: StyledMaterialOptions): StyledMateria
672
780
 
673
781
  shader.vertexShader = shader.vertexShader
674
782
  .replace('#include <common>', `#include <common>\n${VERT_PARS}`)
783
+ .replace('#include <beginnormal_vertex>', `#include <beginnormal_vertex>\n${VERT_SETUP}`)
675
784
  .replace('#include <begin_vertex>', `#include <begin_vertex>\n${VERT_ELEVATION}`)
676
785
  .replace('#include <worldpos_vertex>', VERT_WORLD);
677
786
 
@@ -706,6 +815,7 @@ export function createStyledMaterial(opts: StyledMaterialOptions): StyledMateria
706
815
  mat.userData.uSelBatch = uSelBatch;
707
816
  mat.userData.uSelWhole = uSelWhole;
708
817
  mat.userData.uBatchTex = uBatchTex;
818
+ mat.userData.uBatchTex2 = uBatchTex2;
709
819
  mat.userData.uBatchTexWidth = uBatchTexWidth;
710
820
  mat.userData.uBatchTexActive = uBatchTexActive;
711
821
  mat.userData.uTileFade = uTileFade;
@@ -722,26 +832,48 @@ const OUTLINE_VERT = /* glsl */ `
722
832
  uniform float uWidth;
723
833
  uniform float uElevation;
724
834
  uniform sampler2D uBatchTex;
835
+ uniform sampler2D uBatchTex2;
836
+ uniform float uBatchMix;
725
837
  uniform float uBatchTexWidth;
726
838
  uniform int uBatchTexActive;
727
839
  varying float vSel;
840
+
841
+ vec4 s3mSampleBatch( sampler2D texA, sampler2D texB, vec2 uv, float mixVal ) {
842
+ vec4 vA = texture2D( texA, uv );
843
+ if ( mixVal <= 0.0001 ) return vA;
844
+ vec4 vB = texture2D( texB, uv );
845
+ return mix( vA, vB, mixVal );
846
+ }
847
+
728
848
  void main() {
729
849
  vSel = max( step( 0.0, uSelBatch ) * step( abs( aBatchId - uSelBatch ), 0.5 ), uSelWhole );
730
850
  vec3 s3mCustomOffset = vec3( 0.0 );
851
+ mat3 s3mRot = mat3( 1.0 );
852
+ vec3 s3mPivot = vec3( 0.0 );
731
853
  if ( uBatchTexActive > 0 && aBatchId >= 0.0 ) {
732
- vec4 bState = texture2D( uBatchTex, vec2( ( aBatchId + 0.5 ) / uBatchTexWidth, 0.25 ) );
854
+ float s3mU = ( aBatchId + 0.5 ) / uBatchTexWidth;
855
+ vec4 bState = s3mSampleBatch( uBatchTex, uBatchTex2, vec2( s3mU, 0.1 ), uBatchMix );
733
856
  if ( bState.a < -0.5 ) {
734
857
  vSel = 0.0; // 过滤隐藏的单体不显示描边
735
858
  } else {
736
- vec4 bOffset = texture2D( uBatchTex, vec2( ( aBatchId + 0.5 ) / uBatchTexWidth, 0.75 ) );
859
+ vec4 bOffset = s3mSampleBatch( uBatchTex, uBatchTex2, vec2( s3mU, 0.3 ), uBatchMix );
737
860
  s3mCustomOffset = bOffset.xyz;
738
861
  if ( s3mCustomOffset.z == 0.0 && bState.a > 0.0 ) {
739
862
  s3mCustomOffset.z = bState.a;
740
863
  }
864
+ vec4 r2 = s3mSampleBatch( uBatchTex, uBatchTex2, vec2( s3mU, 0.5 ), uBatchMix );
865
+ vec4 r3 = s3mSampleBatch( uBatchTex, uBatchTex2, vec2( s3mU, 0.7 ), uBatchMix );
866
+ vec4 r4 = s3mSampleBatch( uBatchTex, uBatchTex2, vec2( s3mU, 0.9 ), uBatchMix );
867
+ s3mRot = mat3(
868
+ vec3( r2.x, r3.x, r4.x ),
869
+ vec3( r2.y, r3.y, r4.y ),
870
+ vec3( r2.z, r3.z, r4.z )
871
+ );
872
+ s3mPivot = vec3( r2.w, r3.w, r4.w );
741
873
  }
742
874
  }
743
- vec3 p = position + normal * uWidth * vSel;
744
- p += s3mCustomOffset;
875
+ vec3 p = position + ( s3mRot * normal ) * uWidth * vSel;
876
+ p = s3mRot * ( p - s3mPivot ) + s3mPivot + s3mCustomOffset;
745
877
  p.z += vSel * uElevation;
746
878
  gl_Position = projectionMatrix * modelViewMatrix * vec4( p, 1.0 );
747
879
  }
@@ -763,6 +895,8 @@ export interface OutlineMaterial extends THREE.ShaderMaterial {
763
895
  uWidth: { value: number };
764
896
  uElevation: { value: number };
765
897
  uBatchTex: { value: THREE.Texture };
898
+ uBatchTex2: { value: THREE.Texture };
899
+ uBatchMix: { value: number };
766
900
  uBatchTexWidth: { value: number };
767
901
  uBatchTexActive: { value: number };
768
902
  uColor: { value: THREE.Color };
@@ -778,6 +912,8 @@ export function createOutlineMaterial(shared: StyleUniforms, initialWidth = 0.9)
778
912
  uWidth: { value: initialWidth },
779
913
  uElevation: { value: 0 },
780
914
  uBatchTex: { value: DUMMY_BATCH_TEX },
915
+ uBatchTex2: { value: DUMMY_BATCH_TEX },
916
+ uBatchMix: shared.uBatchMix ?? { value: 0 },
781
917
  uBatchTexWidth: { value: 1.0 },
782
918
  uBatchTexActive: { value: 0 },
783
919
  uColor: { value: shared.uSelColor.value.clone() },