ice3 0.2.0 → 0.2.2

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 (39) hide show
  1. package/README.md +7 -6
  2. package/dist/_types/assets/ICEGLBReader.d.ts +8 -1
  3. package/dist/_types/assets/ICEPreparedGLB.d.ts +26 -0
  4. package/dist/_types/assets/ICEPreviewGeometryPacket.d.ts +60 -0
  5. package/dist/_types/assets/index.d.ts +3 -0
  6. package/dist/_types/core/camera/ICEPerspectiveCamera.d.ts +8 -2
  7. package/dist/_types/core/geometry/ICEGeometry.d.ts +13 -0
  8. package/dist/_types/core/geometry/ICEIndexedGeometry.d.ts +3 -1
  9. package/dist/_types/core/index.d.ts +2 -2
  10. package/dist/_types/core/material/ICEBasicMaterial.d.ts +52 -0
  11. package/dist/_types/renderer/InstanceBatch.d.ts +7 -0
  12. package/dist/_types/renderer/SceneBounds.d.ts +16 -0
  13. package/dist/_types/renderer/ShadowFrustum.d.ts +20 -0
  14. package/dist/_types/renderer/WebGL2DepthMath.d.ts +22 -0
  15. package/dist/_types/renderer/WebGL2GeometryCache.d.ts +16 -2
  16. package/dist/_types/renderer/WebGL2InstanceBatch.d.ts +1 -1
  17. package/dist/_types/renderer/WebGL2Program.d.ts +68 -0
  18. package/dist/_types/renderer/WebGL2RendererBackend.d.ts +44 -0
  19. package/dist/_types/renderer/WebGL2SceneDepth.d.ts +19 -0
  20. package/dist/_types/renderer/WebGL2Shader.d.ts +11 -0
  21. package/dist/_types/renderer/WebGL2ShadowMap.d.ts +42 -0
  22. package/dist/_types/renderer/WebGL2TextureCache.d.ts +31 -0
  23. package/dist/_types/renderer/WebGL2Uv.d.ts +16 -0
  24. package/dist/_types/renderer/WebGPUDrawStorage.d.ts +1 -0
  25. package/dist/_types/renderer/WebGPUGeometryCache.d.ts +16 -2
  26. package/dist/_types/renderer/WebGPURenderPipeline.d.ts +1 -1
  27. package/dist/_types/renderer/WebGPURendererBackend.d.ts +2 -0
  28. package/dist/_types/renderer/index.d.ts +1 -1
  29. package/dist/_types/renderer/types.d.ts +9 -1
  30. package/dist/_types/runtime/ICE3.d.ts +8 -0
  31. package/dist/_types/runtime/asset/ICEAssetSceneBuilder.d.ts +8 -2
  32. package/dist/_types/runtime/asset/ICEPreviewPacketLoader.d.ts +25 -0
  33. package/dist/_types/runtime/asset/ICETilesetLoader.d.ts +0 -1
  34. package/dist/_types/runtime/index.d.ts +2 -1
  35. package/dist/_types/runtime/input/ICEOrbitController.d.ts +2 -0
  36. package/dist/index.d.ts +2 -2
  37. package/dist/index.js +2742 -242
  38. package/dist/index.js.map +4 -4
  39. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -315,6 +315,7 @@ var ICEIndexedGeometry = class {
315
315
  edgeIndices;
316
316
  colors;
317
317
  semanticIndices;
318
+ identity;
318
319
  constructor(options) {
319
320
  if (!options.id.trim()) {
320
321
  throw new TypeError("ICE geometry ID must not be empty");
@@ -343,6 +344,11 @@ var ICEIndexedGeometry = class {
343
344
  throw new TypeError("ICE semantic index count must match the vertex count");
344
345
  }
345
346
  this.id = options.id;
347
+ this.identity = Object.freeze({
348
+ geometryId: options.identity?.geometryId ?? options.id,
349
+ topologyDigest: options.identity?.topologyDigest ?? "",
350
+ attributeDigest: options.identity?.attributeDigest ?? ""
351
+ });
346
352
  this.positions = options.positions;
347
353
  this.indices = options.indices;
348
354
  if (options.edgeIndices) {
@@ -376,25 +382,48 @@ var ICEIndexedGeometry = class {
376
382
  edgeIndexCount: this.edgeIndices?.length ?? 0,
377
383
  colorCount: this.colors ? this.positions.length / 3 : 0,
378
384
  ...this.semanticIndices ? { semanticIndexCount: this.semanticIndices.length } : {},
385
+ ...this.identity.topologyDigest || this.identity.attributeDigest ? { identity: this.identity } : {},
379
386
  bounds: this.getBounds()
380
387
  };
381
388
  }
382
389
  };
383
390
 
384
391
  // packages/core/dist/material/ICEBasicMaterial.js
392
+ var DEFAULT_ROUGHNESS = 0.85;
385
393
  var ICEBasicMaterial = class {
386
394
  typeId = "ice3:BasicMaterial";
387
395
  color;
388
396
  edgeColor;
397
+ shading;
398
+ map;
399
+ normalMap;
400
+ ormMap;
401
+ metallic;
402
+ roughness;
403
+ emissive;
389
404
  constructor(options) {
390
405
  this.color = options.color;
391
406
  this.edgeColor = options.edgeColor;
407
+ this.shading = options.shading;
408
+ this.map = options.map;
409
+ this.normalMap = options.normalMap;
410
+ this.ormMap = options.ormMap;
411
+ this.metallic = options.metallic ?? 0;
412
+ this.roughness = options.roughness ?? DEFAULT_ROUGHNESS;
413
+ this.emissive = options.emissive;
392
414
  }
393
415
  toJSON() {
394
416
  return {
395
417
  typeId: this.typeId,
396
418
  color: this.color,
397
- ...this.edgeColor ? { edgeColor: this.edgeColor } : {}
419
+ ...this.edgeColor ? { edgeColor: this.edgeColor } : {},
420
+ ...this.shading ? { shading: this.shading } : {},
421
+ ...this.map ? { map: this.map } : {},
422
+ ...this.normalMap ? { normalMap: this.normalMap } : {},
423
+ ...this.ormMap ? { ormMap: this.ormMap } : {},
424
+ ...this.metallic !== 0 ? { metallic: this.metallic } : {},
425
+ ...this.roughness !== DEFAULT_ROUGHNESS ? { roughness: this.roughness } : {},
426
+ ...this.emissive ? { emissive: this.emissive } : {}
398
427
  };
399
428
  }
400
429
  };
@@ -426,16 +455,31 @@ var ICEPerspectiveCamera = class {
426
455
  this.lookTarget = new ICEVector3(...target);
427
456
  return this;
428
457
  }
458
+ setClipPlanes(near, far) {
459
+ if (!Number.isFinite(near) || !Number.isFinite(far) || near <= 0 || far <= near) {
460
+ throw new RangeError("Camera near/far values are invalid");
461
+ }
462
+ this.near = near;
463
+ this.far = far;
464
+ return this;
465
+ }
429
466
  getPosition() {
430
467
  return this.eye;
431
468
  }
432
469
  getTarget() {
433
470
  return this.lookTarget;
434
471
  }
472
+ /**
473
+ * 投影矩阵。屏幕空间效果要从深度重建视空间坐标,光有 viewProjection 不够。
474
+ */
475
+ getProjectionMatrix(aspect) {
476
+ return new ICEMatrix4().perspective(this.fov, aspect, this.near, this.far);
477
+ }
478
+ getViewMatrix() {
479
+ return new ICEMatrix4().lookAt(this.eye, this.lookTarget, new ICEVector3(0, 1, 0));
480
+ }
435
481
  getViewProjectionMatrix(aspect) {
436
- const projection = new ICEMatrix4().perspective(this.fov, aspect, this.near, this.far);
437
- const view = new ICEMatrix4().lookAt(this.eye, this.lookTarget, new ICEVector3(0, 1, 0));
438
- return projection.multiply(view);
482
+ return this.getProjectionMatrix(aspect).multiply(this.getViewMatrix());
439
483
  }
440
484
  };
441
485
 
@@ -765,6 +809,7 @@ var ICEMesh = class _ICEMesh extends ICENode {
765
809
  static fromJSON(value) {
766
810
  const geometry = value.geometry;
767
811
  const material = value.material;
812
+ const shading = material.shading === "directional" || material.shading === "flat" || material.shading === "water" ? material.shading : void 0;
768
813
  return new _ICEMesh({
769
814
  id: value.id,
770
815
  displayName: typeof value.displayName === "string" ? value.displayName : value.id,
@@ -781,7 +826,8 @@ var ICEMesh = class _ICEMesh extends ICENode {
781
826
  }),
782
827
  material: new ICEBasicMaterial({
783
828
  color: String(material.color),
784
- ...typeof material.edgeColor === "string" ? { edgeColor: material.edgeColor } : {}
829
+ ...typeof material.edgeColor === "string" ? { edgeColor: material.edgeColor } : {},
830
+ ...shading ? { shading } : {}
785
831
  })
786
832
  });
787
833
  }
@@ -909,17 +955,26 @@ function isRenderBatch(node) {
909
955
  }
910
956
 
911
957
  // packages/renderer/dist/WebGL2Program.js
912
- var VERTEX_SOURCE = `
913
- attribute vec3 a_position;
914
- attribute vec3 a_color;
915
- attribute vec4 a_instanceColumn0;
916
- attribute vec4 a_instanceColumn1;
917
- attribute vec4 a_instanceColumn2;
918
- attribute vec4 a_instanceColumn3;
958
+ var LIGHT_DIRECTION = [-0.35, 0.85, 0.4];
959
+ var LIGHTING = Object.freeze({
960
+ ambient: 0.72,
961
+ direct: 0.72,
962
+ sky: 0.18,
963
+ minimum: 0.92,
964
+ maximum: 1.42
965
+ });
966
+ var VERTEX_SOURCE = `#version 300 es
967
+ in vec3 a_position;
968
+ in vec3 a_color;
969
+ in vec4 a_instanceColumn0;
970
+ in vec4 a_instanceColumn1;
971
+ in vec4 a_instanceColumn2;
972
+ in vec4 a_instanceColumn3;
919
973
  uniform mat4 u_worldViewProjection;
920
974
  uniform vec2 u_clipOffset;
921
975
  uniform float u_useInstancing;
922
- varying vec3 v_color;
976
+ out vec3 v_color;
977
+ out vec3 v_worldPosition;
923
978
 
924
979
  void main() {
925
980
  mat4 instanceMatrix = mat4(
@@ -937,17 +992,313 @@ void main() {
937
992
  gl_Position = u_worldViewProjection * position;
938
993
  gl_Position.xy += u_clipOffset * gl_Position.w;
939
994
  v_color = a_color;
995
+ v_worldPosition = position.xyz;
940
996
  }
941
997
  `;
942
- var FRAGMENT_SOURCE = `
998
+ var FRAGMENT_SOURCE = `#version 300 es
943
999
  precision highp float;
944
1000
  uniform vec4 u_color;
1001
+ uniform vec4 u_clipPlane;
1002
+ uniform float u_pickMode;
945
1003
  uniform float u_useVertexColor;
946
- varying vec3 v_color;
1004
+ uniform float u_useDirectionalShading;
1005
+ uniform float u_waterShading;
1006
+ uniform vec3 u_cameraPosition;
1007
+ uniform sampler2D u_shadowMap;
1008
+ uniform mat4 u_lightViewProjection;
1009
+ uniform float u_shadowEnabled;
1010
+ uniform float u_shadowNormalOffset;
1011
+ uniform float u_shadowDebug;
1012
+ uniform sampler2D u_baseColorMap;
1013
+ uniform float u_useBaseColorMap;
1014
+ uniform vec2 u_uvScale;
1015
+ uniform sampler2D u_normalMap;
1016
+ uniform float u_useNormalMap;
1017
+ uniform sampler2D u_ormMap;
1018
+ uniform float u_useOrmMap;
1019
+ uniform float u_ormOcclusion;
1020
+ uniform float u_ormMetallicRoughness;
1021
+ uniform float u_normalScale;
1022
+ uniform float u_metallic;
1023
+ uniform float u_roughness;
1024
+ uniform vec3 u_emissive;
1025
+ uniform sampler2D u_sceneDepth;
1026
+ uniform float u_useSceneDepth;
1027
+ uniform mat4 u_projection;
1028
+ uniform vec2 u_viewport;
1029
+ uniform vec2 u_clipRange;
1030
+ uniform float u_aoRadius;
1031
+ uniform float u_aoStrength;
1032
+ in vec3 v_color;
1033
+ in vec3 v_worldPosition;
1034
+ out vec4 outputColor;
1035
+
1036
+ // \u7531\u5C4F\u5E55 UV \u4E0E\u6DF1\u5EA6\u7F13\u51B2\u91CD\u5EFA\u89C6\u7A7A\u95F4\u5750\u6807\uFF08\u4E0E WebGL2DepthMath \u7684\u6362\u7B97\u4E00\u81F4\uFF09\u3002
1037
+ vec3 viewPositionAt(vec2 uv, float tanHalfFov, float aspect, float near, float far) {
1038
+ float depth = texture(u_sceneDepth, uv).r;
1039
+ float ndcZ = depth * 2.0 - 1.0;
1040
+ float linear = (2.0 * near * far) / (far + near - ndcZ * (far - near));
1041
+ vec2 ndc = uv * 2.0 - 1.0;
1042
+ return vec3(ndc.x * tanHalfFov * aspect * linear, ndc.y * tanHalfFov * linear, -linear);
1043
+ }
947
1044
 
948
1045
  void main() {
949
- gl_FragColor = vec4(
950
- u_color.rgb * mix(vec3(1.0), v_color, step(0.5, u_useVertexColor)),
1046
+ // Section plane: discard the half-space on the negative side. A zero plane
1047
+ // (0,0,0,0) evaluates to 0 and keeps everything, so it doubles as "off".
1048
+ if (dot(v_worldPosition, u_clipPlane.xyz) + u_clipPlane.w < 0.0) {
1049
+ discard;
1050
+ }
1051
+ if (u_pickMode > 0.5) {
1052
+ outputColor = u_color;
1053
+ return;
1054
+ }
1055
+ vec3 color = u_color.rgb
1056
+ * mix(vec3(1.0), v_color, step(0.5, u_useVertexColor));
1057
+ vec3 normal = normalize(cross(
1058
+ dFdx(v_worldPosition),
1059
+ dFdy(v_worldPosition)
1060
+ ));
1061
+ // Box-projected UVs make the tangent frame analytic: the projection plane's
1062
+ // two world axes are the tangents, so no vertex tangent channel is needed.
1063
+ vec3 tangent = vec3(0.0);
1064
+ vec3 bitangent = vec3(0.0);
1065
+ vec3 axisWeight = abs(normal);
1066
+ if (axisWeight.x >= axisWeight.y && axisWeight.x >= axisWeight.z) {
1067
+ tangent = vec3(0.0, 0.0, -sign(normal.x));
1068
+ bitangent = vec3(0.0, 1.0, 0.0);
1069
+ } else if (axisWeight.y >= axisWeight.z) {
1070
+ tangent = vec3(1.0, 0.0, 0.0);
1071
+ bitangent = vec3(0.0, 0.0, sign(normal.y));
1072
+ } else {
1073
+ tangent = vec3(sign(normal.z), 0.0, 0.0);
1074
+ bitangent = vec3(0.0, 1.0, 0.0);
1075
+ }
1076
+ // \u4E09\u5F20\u8D34\u56FE\u5171\u7528\u540C\u4E00\u5957\u76D2\u6295\u5F71 UV\uFF1A\u6CD5\u7EBF\u4E3B\u5BFC\u8F74\u51B3\u5B9A\u6295\u5F71\u5E73\u9762\uFF0C\u5355\u4F4D\u662F\u7C73\u3002
1077
+ // \u5199\u4E00\u6B21\u5C31\u591F\u4E86\u2014\u2014\u5E95\u56FE\u3001\u6CD5\u7EBF\u56FE\u3001ORM \u5404\u5199\u4E00\u904D\u8FDF\u65E9\u4F1A\u6F02\u3002
1078
+ vec2 surfaceUv;
1079
+ if (axisWeight.x >= axisWeight.y && axisWeight.x >= axisWeight.z) {
1080
+ surfaceUv = vec2(-sign(normal.x) * v_worldPosition.z, v_worldPosition.y);
1081
+ } else if (axisWeight.y >= axisWeight.z) {
1082
+ surfaceUv = vec2(v_worldPosition.x, sign(normal.y) * v_worldPosition.z);
1083
+ } else {
1084
+ surfaceUv = vec2(sign(normal.z) * v_worldPosition.x, v_worldPosition.y);
1085
+ }
1086
+ surfaceUv *= u_uvScale;
1087
+ if (u_useNormalMap > 0.5) {
1088
+ vec3 sampled = texture(u_normalMap, surfaceUv).xyz * 2.0 - 1.0;
1089
+ sampled.xy *= u_normalScale;
1090
+ normal = normalize(
1091
+ normalize(tangent) * sampled.x
1092
+ + normalize(bitangent) * sampled.y
1093
+ + normal * sampled.z
1094
+ );
1095
+ }
1096
+ // \u4E3B\u5149 + \u534A\u7403\u5929\u5149\u3002\u6CD5\u7EBF\u7531\u5C4F\u5E55\u7A7A\u95F4\u5BFC\u6570\u6C42\u51FA\uFF0C\u671D\u5411\u4E0E\u7ED5\u5E8F\u4E00\u81F4\uFF1A
1097
+ // \u8FCE\u7740\u5149\u7684\u4E00\u9762\u5403\u6EE1\u76F4\u5C04\uFF0C\u80CC\u5149\u9762\u53EA\u7559 35% \u7684\u53CD\u5F39\u5149\uFF0C\u6697\u90E8\u4E0D\u81F3\u4E8E\u7CCA\u6210\u4E00\u7247\u3002
1098
+ vec3 lightDirection = normalize(vec3(${LIGHT_DIRECTION.join(", ")}));
1099
+ float ndl = dot(normal, lightDirection);
1100
+ float key = max(ndl, 0.0) + 0.35 * max(-ndl, 0.0);
1101
+ float sky = 0.5 + 0.5 * normal.y;
1102
+ // ORM\uFF1AR \u906E\u853D\u3001G \u7C97\u7CD9\u5EA6\u3001B \u91D1\u5C5E\u5EA6\uFF0C\u548C glTF \u7684\u6253\u5305\u7EA6\u5B9A\u4E00\u81F4\u3002
1103
+ // \u906E\u853D\u53EA\u538B\u95F4\u63A5\u5149\uFF08\u73AF\u5883\u4E0E\u5929\u5149\uFF09\uFF0C\u76F4\u5C04\u7167\u5EA6\u4E0D\u53D7\u5F71\u54CD\u2014\u2014\u5426\u5219\u9634\u5F71\u548C\u8D34\u56FE\u4F1A\u4E92\u76F8\u6253\u67B6\u3002
1104
+ float surfaceOcclusion = 1.0;
1105
+ float surfaceMetallic = u_metallic;
1106
+ float surfaceRoughness = clamp(u_roughness, 0.04, 1.0);
1107
+ if (u_useOrmMap > 0.5) {
1108
+ vec3 orm = texture(u_ormMap, surfaceUv).rgb;
1109
+ if (u_ormOcclusion > 0.5) surfaceOcclusion = clamp(orm.r, 0.0, 1.0);
1110
+ if (u_ormMetallicRoughness > 0.5) {
1111
+ surfaceRoughness = clamp(surfaceRoughness * orm.g, 0.04, 1.0);
1112
+ surfaceMetallic = clamp(surfaceMetallic * orm.b, 0.0, 1.0);
1113
+ }
1114
+ }
1115
+ // \u76F4\u5C04\u5149\u5F3A\u5EA6\u6309\u771F\u5B9E\u65E5\u7167\u7ED9\uFF1A\u671D\u4E0A\u7684\u9762\u4F1A\u8D85\u8FC7 1\uFF0C\u8FC7\u66DD\u5230\u63A5\u8FD1\u767D\u8272\u662F\u5BF9\u7684\uFF0C
1116
+ // \u4E4B\u524D\u4E0A\u9650\u538B\u5230 1 \u9644\u8FD1\uFF0C\u6574\u4E2A\u5382\u533A\u5C31\u6C38\u8FDC\u6652\u4E0D\u5230\u592A\u9633\uFF0C\u770B\u7740\u53D1\u95F7\u53D1\u6697\u3002
1117
+ float shade = clamp(
1118
+ (${LIGHTING.ambient} + ${LIGHTING.sky} * sky) * surfaceOcclusion
1119
+ + ${LIGHTING.direct} * key,
1120
+ ${LIGHTING.minimum},
1121
+ ${LIGHTING.maximum}
1122
+ );
1123
+ // Base-colour texture. There is no UV vertex channel: the projection is
1124
+ // derived from the surface normal's dominant axis, in metres, which matches
1125
+ // the coordinates Studio writes into exported glTF.
1126
+ if (u_useBaseColorMap > 0.5) {
1127
+ color *= texture(u_baseColorMap, surfaceUv).rgb;
1128
+ }
1129
+ vec3 lit = color * mix(1.0, shade, u_useDirectionalShading);
1130
+
1131
+ // \u8868\u9762\u9897\u7C92\uFF1A\u6309\u4E16\u754C\u5750\u6807\u53D6\u4E24\u5C42\u54C8\u5E0C\uFF0C\u6253\u6563\u6574\u7247\u7EAF\u8272\u5E26\u6765\u7684\u5851\u6599\u611F\u3002
1132
+ // \u5E45\u5EA6\u63A7\u5236\u5728 \xB16% \u5185\uFF0C\u8FDC\u4E86\u770B\u4E0D\u51FA\u3001\u8FD1\u4E86\u6709\u8D28\u611F\uFF0C\u4E0D\u4F1A\u55A7\u5BBE\u593A\u4E3B\u3002
1133
+ vec3 fine = floor(v_worldPosition * 4.5);
1134
+ vec3 coarse = floor(v_worldPosition * 0.42);
1135
+ float grain = fract(sin(dot(fine, vec3(12.9898, 78.233, 37.719))) * 43758.5453);
1136
+ float stain = fract(sin(dot(coarse, vec3(39.346, 11.135, 83.155))) * 24634.6345);
1137
+ // \u4F4E\u9891\u6C61\u6E0D\u8D1F\u8D23\u5927\u9762\u79EF\u4E0D\u5300\uFF0C\u9AD8\u9891\u9897\u7C92\u8D1F\u8D23\u8FD1\u770B\u7684\u624B\u611F\u3002
1138
+ lit *= 0.94 + 0.06 * grain + 0.05 * (stain - 0.5);
1139
+
1140
+ // Liquid surfaces use an analytic Fresnel reflection plus shallow ripple
1141
+ // normals. The effect is opt-in through material shading and remains
1142
+ // independent from any application-specific material naming.
1143
+ if (u_waterShading > 0.5) {
1144
+ float waveA = sin(v_worldPosition.x * 1.1
1145
+ + sin(v_worldPosition.z * 0.43) * 2.0);
1146
+ float waveB = sin(v_worldPosition.z * 1.7
1147
+ + v_worldPosition.x * 0.31);
1148
+ float waveC = sin((v_worldPosition.x + v_worldPosition.z) * 2.3);
1149
+ vec3 rippleNormal = normalize(vec3(
1150
+ waveA * 0.09 + waveC * 0.025,
1151
+ 1.0,
1152
+ waveB * 0.09 - waveC * 0.02
1153
+ ));
1154
+ vec3 viewDirection = normalize(u_cameraPosition - v_worldPosition);
1155
+ float facing = clamp(dot(rippleNormal, viewDirection), 0.0, 1.0);
1156
+ float fresnel = pow(1.0 - facing, 2.4);
1157
+ vec3 reflectedSky = vec3(0.22, 0.34, 0.36);
1158
+ vec3 surfaceColor = mix(
1159
+ color,
1160
+ reflectedSky,
1161
+ 0.30 + 0.30 * fresnel
1162
+ );
1163
+ surfaceColor *= 0.92 + 0.09 * waveA + 0.05 * waveB;
1164
+ vec3 halfDirection = normalize(lightDirection + viewDirection);
1165
+ float sparkle = pow(
1166
+ max(dot(rippleNormal, halfDirection), 0.0),
1167
+ 36.0
1168
+ );
1169
+ lit = mix(lit, surfaceColor * shade, 0.92)
1170
+ + vec3(0.42, 0.52, 0.54) * sparkle * 0.55;
1171
+ }
1172
+
1173
+ // \u5C4F\u5E55\u7A7A\u95F4\u906E\u853D\uFF1A\u5148\u628A\u6DF1\u5EA6\u8FD8\u539F\u6210\u89C6\u7A7A\u95F4\u5750\u6807\uFF0C\u518D\u6309**\u4E16\u754C\u534A\u5F84**\u53D6\u6837\u3002
1174
+ // \u76F4\u63A5\u6BD4\u8F83 NDC \u6DF1\u5EA6\u5DEE\u8868\u8FBE\u4E0D\u4E86"1 \u7C73"\u8FD9\u79CD\u91CF\uFF08\u8FDC\u7AEF\u5343\u5206\u4E4B\u51E0\u5C31\u8DE8\u4E86\u5341\u7C73\uFF09\uFF0C
1175
+ // \u8FD9\u4E5F\u662F SPEC-007 \u524D\u4E09\u8F6E\u5931\u8D25\u7684\u539F\u56E0\u3002
1176
+ if (u_useSceneDepth > 0.5) {
1177
+ vec2 screenUv = gl_FragCoord.xy / u_viewport;
1178
+ float tanHalfFov = 1.0 / u_projection[1][1];
1179
+ float aspect = u_projection[1][1] / u_projection[0][0];
1180
+ float near = u_clipRange.x;
1181
+ float far = u_clipRange.y;
1182
+
1183
+ vec3 centre = viewPositionAt(screenUv, tanHalfFov, aspect, near, far);
1184
+ // \u6CD5\u7EBF\u7531\u89C6\u7A7A\u95F4\u4F4D\u7F6E\u7684\u5C4F\u5E55\u5BFC\u6570\u6C42\u51FA\uFF1A\u51E0\u4F55\u6CA1\u6709\u6CD5\u7EBF\u5C5E\u6027\uFF0C\u4E5F\u4E0D\u9700\u8981\u3002
1185
+ vec3 viewNormal = normalize(cross(dFdx(centre), dFdy(centre)));
1186
+ float centreDepth = -centre.z;
1187
+
1188
+ // \u4E16\u754C\u534A\u5F84\u6362\u7B97\u6210\u50CF\u7D20\u534A\u5F84\uFF1A\u8D8A\u8FDC\u8D8A\u5C0F\uFF0C\u8FD1\u666F\u8FDC\u666F\u624D\u90FD\u6709\u610F\u4E49\u3002
1189
+ float pixelRadius = (u_aoRadius / max(1e-4, centreDepth * tanHalfFov)) * (u_viewport.y * 0.5);
1190
+ // \u4EA4\u9519\u68AF\u5EA6\u566A\u58F0\uFF08IGN\uFF09\u4EE3\u66FF sin \u54C8\u5E0C\uFF1A\u540C\u6837\u662F\u9010\u50CF\u7D20\u6270\u52A8\uFF0C\u4F46\u5B83\u5728\u76F8\u90BB\u50CF\u7D20\u95F4\u662F
1191
+ // \u6E10\u53D8\u800C\u975E\u8DF3\u53D8\uFF0C\u91C7\u6837\u7F3A\u53E3\u770B\u8D77\u6765\u50CF\u7EC6\u5BC6\u6296\u52A8\uFF0C\u4E0D\u50CF\u6912\u76D0\u566A\u70B9\u3002
1192
+ float rotation = fract(52.9829189 * fract(dot(gl_FragCoord.xy, vec2(0.06711056, 0.00583715)))) * 6.2831853;
1193
+
1194
+ // \u4E09\u5708\u53D6\u6837\uFF1A0.35 / 0.65 / 1.0 \u500D\u534A\u5F84\uFF0C\u6743\u91CD 1.0 / 0.7 / 0.45\uFF0C\u5206\u522B\u8D1F\u8D23
1195
+ // \u8D34\u5730\u63A5\u89E6\u3001\u7F1D\u9699\u4E2D\u90E8\u3001\u5927\u9762\u79EF\u5939\u89D2\u3002\u6837\u672C\u6309\u4E0E\u6CD5\u7EBF\u7684\u5939\u89D2\u52A0\u6743\u2014\u2014\u6B63\u5BF9\u7740\u7684\u5E73\u9762
1196
+ // \u4E0D\u8BE5\u88AB\u5468\u56F4\u540C\u6837\u5E73\u7684\u4E1C\u897F\u538B\u6697\uFF0C\u53EA\u6709"\u7ACB\u8D77\u6765\u6321\u4F4F\u6211"\u7684\u624D\u7B97\u6CD5\u3002
1197
+ float occlusion = 0.0;
1198
+ float weightSum = 0.0;
1199
+ for (int ring = 0; ring < 3; ring++) {
1200
+ // \u4E09\u5708\uFF1A\u8D34\u5730\u63A5\u89E6 / \u7F1D\u9699\u4E2D\u90E8 / \u5927\u9762\u79EF\u5939\u89D2\u3002\u6CA1\u6709\u6A21\u7CCA\u901A\u9053\u65F6\uFF0C\u9760\u5708\u5C42\u5206\u644A
1201
+ // \u91C7\u6837\u5BC6\u5EA6\u662F\u538B\u566A\u70B9\u6700\u76F4\u63A5\u7684\u529E\u6CD5\uFF08\u6A21\u7CCA\u8981\u989D\u5916\u7684 FBO \u4E0E\u5168\u5C4F\u901A\u9053\uFF09\u3002
1202
+ float ringScale = ring == 0 ? 0.35 : (ring == 1 ? 0.65 : 1.0);
1203
+ float ringWeight = ring == 0 ? 1.0 : (ring == 1 ? 0.7 : 0.45);
1204
+ for (int i = 0; i < 8; i++) {
1205
+ float angle = rotation + float(i) * 0.7853982 + float(ring) * 0.2618;
1206
+ vec2 offset = vec2(cos(angle), sin(angle)) * pixelRadius * ringScale;
1207
+ vec2 sampleUv = screenUv + offset / u_viewport;
1208
+ if (sampleUv.x < 0.0 || sampleUv.x > 1.0 || sampleUv.y < 0.0 || sampleUv.y > 1.0) continue;
1209
+ vec3 delta = viewPositionAt(sampleUv, tanHalfFov, aspect, near, far) - centre;
1210
+ float distance = length(delta);
1211
+ if (distance < 0.0001 || distance > u_aoRadius) continue;
1212
+ float alignment = dot(delta, viewNormal) / distance;
1213
+ if (alignment <= 0.15) continue;
1214
+ occlusion += ringWeight * alignment;
1215
+ weightSum += ringWeight;
1216
+ }
1217
+ }
1218
+ if (weightSum > 0.0) {
1219
+ occlusion /= weightSum;
1220
+ lit *= 1.0 - clamp(occlusion, 0.0, 1.0) * u_aoStrength;
1221
+ }
1222
+ }
1223
+
1224
+ // \u91D1\u5C5E\u6CA1\u6709\u6F2B\u53CD\u5C04\uFF1A\u5B83\u7684\u989C\u8272\u5168\u90E8\u6765\u81EA\u53CD\u5C04\u3002\u5148\u628A\u6F2B\u53CD\u5C04\u6309 metallic \u6536\u6389\uFF0C
1225
+ // \u518D\u7528\u53CD\u5C04\u9879\u8865\u56DE\u6765\uFF0C\u5426\u5219\u91D1\u5C5E\u548C\u5851\u6599\u5728\u540C\u4E00\u4E2A\u592A\u9633\u4E0B\u53EA\u662F"\u989C\u8272\u4E0D\u540C"\u3002
1226
+ {
1227
+ vec3 viewDirection = normalize(u_cameraPosition - v_worldPosition);
1228
+ vec3 halfVector = normalize(lightDirection + viewDirection);
1229
+ float metallic = clamp(surfaceMetallic, 0.0, 1.0);
1230
+ float roughness = clamp(surfaceRoughness, 0.04, 1.0);
1231
+ vec3 reflectance = mix(vec3(0.04), color, metallic);
1232
+
1233
+ float diffuseGain = 1.0 - metallic * 0.85;
1234
+ float facing = max(dot(normal, lightDirection), 0.0);
1235
+
1236
+ // \u9AD8\u5149\uFF1A\u7C97\u7CD9\u5EA6\u51B3\u5B9A\u53F6\u74E3\u5BBD\u5EA6\u4E0E\u5F3A\u5EA6\uFF08\u6307\u6570 96 \u2192 4\uFF0C\u589E\u76CA 1.0 \u2192 0.18\uFF09\u3002
1237
+ float shininess = mix(96.0, 4.0, roughness * roughness);
1238
+ float highlight = pow(max(dot(normal, halfVector), 0.0), shininess);
1239
+ float specularGain = mix(1.0, 0.18, roughness);
1240
+
1241
+ // \u73AF\u5883\u53CD\u5C04\uFF1A\u6309\u53CD\u5C04\u65B9\u5411\u53D6\u5929\u7A7A/\u5730\u9762\u8272\uFF0C\u63A0\u5C04\u89D2\u518D\u53E0\u4E00\u5C42 Fresnel\u3002
1242
+ // \u53EA\u770B\u6CD5\u7EBF\u7684\u8BDD\uFF0C\u5E73\u8EBA\u7684\u91D1\u5C5E\u4F1A\u548C\u7ACB\u7740\u7684\u91D1\u5C5E\u4E00\u4E2A\u989C\u8272\u2014\u2014\u73B0\u5B9E\u91CC\u524D\u8005\u53CD\u5C04\u5929\u9876\u3001
1243
+ // \u540E\u8005\u53CD\u5C04\u5730\u5E73\u7EBF\uFF0C\u6070\u6070\u662F\u91D1\u5C5E\u6700\u5BB9\u6613\u8FA8\u8BA4\u7684\u7279\u5F81\u3002
1244
+ vec3 reflected = reflect(-viewDirection, normal);
1245
+ float skyMix = clamp(reflected.y * 0.9 + 0.35, 0.0, 1.0);
1246
+ vec3 skyColor = mix(vec3(0.15, 0.16, 0.17), vec3(0.62, 0.72, 0.84), skyMix);
1247
+ float fresnel = pow(1.0 - clamp(dot(normal, viewDirection), 0.0, 1.0), 4.0);
1248
+ float envGain = (0.22 + 0.55 * fresnel) * mix(0.4, 1.0, metallic);
1249
+
1250
+ float litScale = diffuseGain;
1251
+ lit *= litScale;
1252
+ lit += reflectance * (highlight * specularGain * mix(0.35, 1.0, facing)
1253
+ + skyColor * envGain);
1254
+ lit += u_emissive;
1255
+ }
1256
+
1257
+ // \u9634\u5F71\uFF1A\u628A\u4E16\u754C\u5750\u6807\u6295\u5230\u5149\u6E90\u7A7A\u95F4\uFF0C\u4E0E\u6DF1\u5EA6\u56FE\u6BD4\u8F83\u3002\u53D6 3\xD73 \u91C7\u6837\u7684\u5E73\u5747\u505A\u8F7B\u5FAE\u67D4\u5316\u3002
1258
+ // shadowLit \u5728\u8C03\u8BD5\u901A\u9053\u91CC\u76F4\u63A5\u8F93\u51FA\uFF1A1 = \u53D7\u5149\uFF0C0 = \u88AB\u906E\u3002
1259
+ float shadowLit = 1.0;
1260
+ if (u_shadowEnabled > 0.5) {
1261
+ // \u91C7\u6837\u70B9\u6CBF\u6CD5\u7EBF\u63A8\u51FA 1.5 \u4E2A\u7EB9\u7D20\uFF1A\u5E73\u8EBA\u5728\u5149\u91CC\u7684\u9762\uFF08\u5C4B\u9876\u3001\u5730\u9762\uFF09\u5728\u6BCF\u4E2A\u7EB9\u7D20\u5185
1262
+ // \u672C\u8EAB\u5C31\u6709\u6DF1\u5EA6\u5DEE\uFF0C\u9760\u6DF1\u5EA6\u504F\u5DEE\u53BB\u8FFD\u8FD9\u4E2A\u5DEE\u53EA\u4F1A\u8BA9\u5F71\u5B50\u8131\u79BB\u7269\u4F53\uFF08peter-panning\uFF09\u3002
1263
+ // \u628A\u6BD4\u8F83\u70B9\u62AC\u79BB\u8868\u9762\u624D\u662F\u6B63\u89E3\uFF0C\u504F\u79FB\u91CF\u6309\u7EB9\u7D20\u6362\u7B97\uFF0C\u548C\u8D34\u56FE\u5206\u8FA8\u7387\u89E3\u8026\u3002
1264
+ vec3 shadowPosition = v_worldPosition + normal * u_shadowNormalOffset;
1265
+ vec4 lightSpace = u_lightViewProjection * vec4(shadowPosition, 1.0);
1266
+ vec3 projected = lightSpace.xyz / max(lightSpace.w, 1e-5) * 0.5 + 0.5;
1267
+ if (projected.x > 0.002 && projected.x < 0.998
1268
+ && projected.y > 0.002 && projected.y < 0.998
1269
+ && projected.z > 0.0 && projected.z < 1.0) {
1270
+ // \u89C6\u9525\u8D34\u5408\u573A\u666F\u540E\u6DF1\u5EA6\u8DE8\u5EA6\u53EA\u6709\u767E\u7C73\u91CF\u7EA7\uFF0C\u504F\u5DEE\u4E5F\u5C31\u53EF\u4EE5\u6309\u7EB9\u7406\u5355\u5143\u7ED9\u3002
1271
+ // \u9634\u5F71 pass \u53EA\u753B\u80CC\u9762\uFF08cull FRONT\uFF09\uFF0C\u53D7\u5149\u9762\u548C\u6DF1\u5EA6\u56FE\u5929\u7136\u9519\u5F00\uFF0C\u57FA\u6570\u53EF\u4EE5\u5F88\u5C0F\uFF1B
1272
+ // \u659C\u9762\u6700\u654F\u611F\uFF0C\u4ECD\u6309 N\xB7L \u653E\u5927\uFF08\u9634\u5F71\u75E4\u75AE\u4E0E\u9634\u5F71\u8131\u79BB\u4E4B\u95F4\u7684\u90A3\u6761\u7A84\u7F1D\uFF09\u3002
1273
+ float ndlShadow = max(dot(normal, lightDirection), 0.0);
1274
+ float bias = 0.0006 + 0.0012 * (1.0 - ndlShadow);
1275
+ float lit2 = 0.0;
1276
+ vec2 texel = vec2(1.0 / 2048.0);
1277
+ for (int dx = -1; dx <= 1; dx++) {
1278
+ for (int dy = -1; dy <= 1; dy++) {
1279
+ float depth = texture(u_shadowMap, projected.xy + vec2(float(dx), float(dy)) * texel).r;
1280
+ lit2 += projected.z - bias > depth ? 0.0 : 1.0;
1281
+ }
1282
+ }
1283
+ shadowLit = lit2 / 9.0;
1284
+ lit *= mix(0.68, 1.0, shadowLit);
1285
+
1286
+ }
1287
+ }
1288
+ // \u8C03\u8BD5\u901A\u9053\uFF1A?shadowdebug=1 \u770B\u906E\u853D\u5224\u5B9A\uFF0C=2 \u770B\u6DF1\u5EA6\u56FE\u672C\u8EAB\u3002
1289
+ if (u_shadowDebug > 0.5) {
1290
+ if (u_shadowDebug < 1.5) {
1291
+ outputColor = vec4(shadowLit, 0.12, 0.12, 1.0);
1292
+ } else {
1293
+ float stored = texture(u_shadowMap, gl_FragCoord.xy / u_viewport).r;
1294
+ outputColor = vec4(vec3(stored), 1.0);
1295
+ }
1296
+ return;
1297
+ }
1298
+ // \u753B\u5E03\u6309 sRGB \u89E3\u91CA\u5199\u5165\u503C\uFF0C\u800C\u6750\u8D28\u662F\u7EBF\u6027\u7A7A\u95F4\u7684\u5206\u91CF\uFF0C\u5FC5\u987B\u505A\u4E00\u6B21\u7F16\u7801\u3002
1299
+ // \u4E0D\u505A\u7684\u8BDD\u7EBF\u6027 0.017 \u4F1A\u5F53\u4F5C sRGB 0.017 \u663E\u793A\uFF0C\u6DF1\u8272\u6750\u8D28\u5168\u90E8\u584C\u6210\u9ED1\u8272\u3002
1300
+ outputColor = vec4(
1301
+ pow(max(lit, vec3(0.0)), vec3(1.0 / 2.2)),
951
1302
  u_color.a
952
1303
  );
953
1304
  }
@@ -963,7 +1314,37 @@ var WebGL2Program = class _WebGL2Program {
963
1314
  colorLocation;
964
1315
  clipOffsetLocation;
965
1316
  vertexColorLocation;
1317
+ directionalShadingLocation;
1318
+ waterShadingLocation;
1319
+ cameraPositionLocation;
1320
+ shadowMapLocation;
1321
+ lightMatrixLocation;
1322
+ shadowEnabledLocation;
1323
+ shadowNormalOffsetLocation;
1324
+ shadowDebugLocation;
966
1325
  instancingLocation;
1326
+ pickModeLocation;
1327
+ clipPlaneLocation;
1328
+ baseColorMapLocation;
1329
+ useBaseColorMapLocation;
1330
+ uvScaleLocation;
1331
+ normalMapLocation;
1332
+ ormMapLocation;
1333
+ useOrmMapLocation;
1334
+ ormOcclusionLocation;
1335
+ ormMetallicRoughnessLocation;
1336
+ useNormalMapLocation;
1337
+ normalScaleLocation;
1338
+ metallicLocation;
1339
+ roughnessLocation;
1340
+ emissiveLocation;
1341
+ sceneDepthLocation;
1342
+ useSceneDepthLocation;
1343
+ projectionLocation;
1344
+ viewportLocation;
1345
+ clipRangeLocation;
1346
+ aoRadiusLocation;
1347
+ aoStrengthLocation;
967
1348
  constructor(gl) {
968
1349
  this.gl = gl;
969
1350
  const program = this.createProgram(VERTEX_SOURCE, FRAGMENT_SOURCE);
@@ -971,8 +1352,38 @@ var WebGL2Program = class _WebGL2Program {
971
1352
  const colorLocation = gl.getUniformLocation(program, "u_color");
972
1353
  const clipOffsetLocation = gl.getUniformLocation(program, "u_clipOffset");
973
1354
  const vertexColorLocation = gl.getUniformLocation(program, "u_useVertexColor");
1355
+ const directionalShadingLocation = gl.getUniformLocation(program, "u_useDirectionalShading");
1356
+ const waterShadingLocation = gl.getUniformLocation(program, "u_waterShading");
1357
+ const cameraPositionLocation = gl.getUniformLocation(program, "u_cameraPosition");
974
1358
  const instancingLocation = gl.getUniformLocation(program, "u_useInstancing");
975
- if (!matrixLocation || !colorLocation || !clipOffsetLocation || !vertexColorLocation || !instancingLocation) {
1359
+ const pickModeLocation = gl.getUniformLocation(program, "u_pickMode");
1360
+ const clipPlaneLocation = gl.getUniformLocation(program, "u_clipPlane");
1361
+ const baseColorMapLocation = gl.getUniformLocation(program, "u_baseColorMap");
1362
+ const useBaseColorMapLocation = gl.getUniformLocation(program, "u_useBaseColorMap");
1363
+ const uvScaleLocation = gl.getUniformLocation(program, "u_uvScale");
1364
+ const normalMapLocation = gl.getUniformLocation(program, "u_normalMap");
1365
+ const ormMapLocation = gl.getUniformLocation(program, "u_ormMap");
1366
+ const useOrmMapLocation = gl.getUniformLocation(program, "u_useOrmMap");
1367
+ const ormOcclusionLocation = gl.getUniformLocation(program, "u_ormOcclusion");
1368
+ const ormMetallicRoughnessLocation = gl.getUniformLocation(program, "u_ormMetallicRoughness");
1369
+ const useNormalMapLocation = gl.getUniformLocation(program, "u_useNormalMap");
1370
+ const normalScaleLocation = gl.getUniformLocation(program, "u_normalScale");
1371
+ const metallicLocation = gl.getUniformLocation(program, "u_metallic");
1372
+ const roughnessLocation = gl.getUniformLocation(program, "u_roughness");
1373
+ const emissiveLocation = gl.getUniformLocation(program, "u_emissive");
1374
+ const sceneDepthLocation = gl.getUniformLocation(program, "u_sceneDepth");
1375
+ const useSceneDepthLocation = gl.getUniformLocation(program, "u_useSceneDepth");
1376
+ const projectionLocation = gl.getUniformLocation(program, "u_projection");
1377
+ const viewportLocation = gl.getUniformLocation(program, "u_viewport");
1378
+ const clipRangeLocation = gl.getUniformLocation(program, "u_clipRange");
1379
+ const aoRadiusLocation = gl.getUniformLocation(program, "u_aoRadius");
1380
+ const aoStrengthLocation = gl.getUniformLocation(program, "u_aoStrength");
1381
+ const shadowMapLocation = gl.getUniformLocation(program, "u_shadowMap");
1382
+ const lightMatrixLocation = gl.getUniformLocation(program, "u_lightViewProjection");
1383
+ const shadowEnabledLocation = gl.getUniformLocation(program, "u_shadowEnabled");
1384
+ const shadowNormalOffsetLocation = gl.getUniformLocation(program, "u_shadowNormalOffset");
1385
+ const shadowDebugLocation = gl.getUniformLocation(program, "u_shadowDebug");
1386
+ if (!matrixLocation || !colorLocation || !clipOffsetLocation || !vertexColorLocation || !directionalShadingLocation || !waterShadingLocation || !cameraPositionLocation || !instancingLocation || !pickModeLocation) {
976
1387
  gl.deleteProgram(program);
977
1388
  throw new ICEEngineError("ICE3_SHADER_LINK_FAILED", "ICE3_SHADER_LINK_FAILED: shader uniforms are missing");
978
1389
  }
@@ -981,7 +1392,81 @@ var WebGL2Program = class _WebGL2Program {
981
1392
  this.colorLocation = colorLocation;
982
1393
  this.clipOffsetLocation = clipOffsetLocation;
983
1394
  this.vertexColorLocation = vertexColorLocation;
1395
+ this.directionalShadingLocation = directionalShadingLocation;
1396
+ this.waterShadingLocation = waterShadingLocation;
1397
+ this.cameraPositionLocation = cameraPositionLocation;
1398
+ this.shadowMapLocation = shadowMapLocation;
1399
+ this.lightMatrixLocation = lightMatrixLocation;
1400
+ this.shadowEnabledLocation = shadowEnabledLocation;
1401
+ this.shadowNormalOffsetLocation = shadowNormalOffsetLocation;
1402
+ this.shadowDebugLocation = shadowDebugLocation;
984
1403
  this.instancingLocation = instancingLocation;
1404
+ this.pickModeLocation = pickModeLocation;
1405
+ this.clipPlaneLocation = clipPlaneLocation;
1406
+ this.baseColorMapLocation = baseColorMapLocation;
1407
+ this.useBaseColorMapLocation = useBaseColorMapLocation;
1408
+ this.uvScaleLocation = uvScaleLocation;
1409
+ this.normalMapLocation = normalMapLocation;
1410
+ this.ormMapLocation = ormMapLocation;
1411
+ this.useOrmMapLocation = useOrmMapLocation;
1412
+ this.ormOcclusionLocation = ormOcclusionLocation;
1413
+ this.ormMetallicRoughnessLocation = ormMetallicRoughnessLocation;
1414
+ this.useNormalMapLocation = useNormalMapLocation;
1415
+ this.normalScaleLocation = normalScaleLocation;
1416
+ this.metallicLocation = metallicLocation;
1417
+ this.roughnessLocation = roughnessLocation;
1418
+ this.emissiveLocation = emissiveLocation;
1419
+ this.sceneDepthLocation = sceneDepthLocation;
1420
+ this.useSceneDepthLocation = useSceneDepthLocation;
1421
+ this.projectionLocation = projectionLocation;
1422
+ this.viewportLocation = viewportLocation;
1423
+ this.clipRangeLocation = clipRangeLocation;
1424
+ this.aoRadiusLocation = aoRadiusLocation;
1425
+ this.aoStrengthLocation = aoStrengthLocation;
1426
+ if (sceneDepthLocation) {
1427
+ gl.useProgram(program);
1428
+ gl.uniform1i(sceneDepthLocation, 2);
1429
+ }
1430
+ if (useSceneDepthLocation) {
1431
+ gl.useProgram(program);
1432
+ gl.uniform1f(useSceneDepthLocation, 0);
1433
+ }
1434
+ if (aoRadiusLocation) {
1435
+ gl.useProgram(program);
1436
+ gl.uniform1f(aoRadiusLocation, 2.5);
1437
+ }
1438
+ if (emissiveLocation) {
1439
+ gl.useProgram(program);
1440
+ gl.uniform3f(emissiveLocation, 0, 0, 0);
1441
+ }
1442
+ if (normalMapLocation) {
1443
+ gl.useProgram(program);
1444
+ gl.uniform1i(normalMapLocation, 1);
1445
+ }
1446
+ if (ormMapLocation) {
1447
+ gl.useProgram(program);
1448
+ gl.uniform1i(ormMapLocation, 4);
1449
+ }
1450
+ if (useNormalMapLocation) {
1451
+ gl.useProgram(program);
1452
+ gl.uniform1f(useNormalMapLocation, 0);
1453
+ }
1454
+ if (baseColorMapLocation) {
1455
+ gl.useProgram(program);
1456
+ gl.uniform1i(baseColorMapLocation, 0);
1457
+ }
1458
+ if (useBaseColorMapLocation) {
1459
+ gl.useProgram(program);
1460
+ gl.uniform1f(useBaseColorMapLocation, 0);
1461
+ }
1462
+ if (clipPlaneLocation) {
1463
+ gl.useProgram(program);
1464
+ gl.uniform4f(clipPlaneLocation, 0, 0, 0, 0);
1465
+ }
1466
+ if (shadowMapLocation) {
1467
+ gl.useProgram(program);
1468
+ gl.uniform1i(shadowMapLocation, 3);
1469
+ }
985
1470
  }
986
1471
  use() {
987
1472
  this.gl.useProgram(this.program);
@@ -992,15 +1477,156 @@ var WebGL2Program = class _WebGL2Program {
992
1477
  setColor(color) {
993
1478
  this.gl.uniform4f(this.colorLocation, ...color);
994
1479
  }
1480
+ /**
1481
+ * Bind a base-colour texture to unit 0 and enable sampling. Passing `null`
1482
+ * restores flat shading without touching the bound texture.
1483
+ */
1484
+ setBaseColorMap(texture, scale) {
1485
+ const gl = this.gl;
1486
+ if (!this.useBaseColorMapLocation)
1487
+ return;
1488
+ if (!texture) {
1489
+ gl.uniform1f(this.useBaseColorMapLocation, 0);
1490
+ return;
1491
+ }
1492
+ gl.activeTexture(gl.TEXTURE0);
1493
+ gl.bindTexture(gl.TEXTURE_2D, texture);
1494
+ gl.uniform1f(this.useBaseColorMapLocation, 1);
1495
+ if (this.uvScaleLocation) {
1496
+ gl.uniform2f(this.uvScaleLocation, scale[0], scale[1]);
1497
+ }
1498
+ }
1499
+ /** Bind a tangent-space normal map to unit 1; `null` disables perturbation. */
1500
+ setNormalMap(texture, strength) {
1501
+ const gl = this.gl;
1502
+ if (!this.useNormalMapLocation)
1503
+ return;
1504
+ if (!texture) {
1505
+ gl.uniform1f(this.useNormalMapLocation, 0);
1506
+ return;
1507
+ }
1508
+ gl.activeTexture(gl.TEXTURE1);
1509
+ gl.bindTexture(gl.TEXTURE_2D, texture);
1510
+ gl.uniform1f(this.useNormalMapLocation, 1);
1511
+ if (this.normalScaleLocation) {
1512
+ gl.uniform1f(this.normalScaleLocation, strength);
1513
+ }
1514
+ }
1515
+ /**
1516
+ * Bind the packed ORM map. `occlusion` / `metallicRoughness` describe which
1517
+ * halves of the image are meaningful; both false disables sampling.
1518
+ */
1519
+ setOrmMap(texture, occlusion, metallicRoughness) {
1520
+ const gl = this.gl;
1521
+ if (!this.useOrmMapLocation)
1522
+ return;
1523
+ if (!texture || !occlusion && !metallicRoughness) {
1524
+ gl.uniform1f(this.useOrmMapLocation, 0);
1525
+ return;
1526
+ }
1527
+ gl.activeTexture(gl.TEXTURE4);
1528
+ gl.bindTexture(gl.TEXTURE_2D, texture);
1529
+ gl.uniform1f(this.useOrmMapLocation, 1);
1530
+ if (this.ormOcclusionLocation) {
1531
+ gl.uniform1f(this.ormOcclusionLocation, occlusion ? 1 : 0);
1532
+ }
1533
+ if (this.ormMetallicRoughnessLocation) {
1534
+ gl.uniform1f(this.ormMetallicRoughnessLocation, metallicRoughness ? 1 : 0);
1535
+ }
1536
+ }
1537
+ /** Metallic-roughness factors plus emission for the current draw. */
1538
+ setSurface(metallic, roughness, emissive) {
1539
+ const gl = this.gl;
1540
+ if (this.metallicLocation)
1541
+ gl.uniform1f(this.metallicLocation, metallic);
1542
+ if (this.roughnessLocation)
1543
+ gl.uniform1f(this.roughnessLocation, roughness);
1544
+ if (this.emissiveLocation) {
1545
+ if (emissive)
1546
+ gl.uniform3f(this.emissiveLocation, emissive[0], emissive[1], emissive[2]);
1547
+ else
1548
+ gl.uniform3f(this.emissiveLocation, 0, 0, 0);
1549
+ }
1550
+ }
1551
+ /** 绑定相机深度纹理(unit 2)并传入重建视空间所需的相机参数。 */
1552
+ setSceneDepth(texture, projection, near, far, deviceWidth, deviceHeight) {
1553
+ const gl = this.gl;
1554
+ if (!this.useSceneDepthLocation)
1555
+ return;
1556
+ if (!texture || !projection) {
1557
+ gl.uniform1f(this.useSceneDepthLocation, 0);
1558
+ return;
1559
+ }
1560
+ gl.activeTexture(gl.TEXTURE2);
1561
+ gl.bindTexture(gl.TEXTURE_2D, texture);
1562
+ gl.uniform1f(this.useSceneDepthLocation, 1);
1563
+ if (this.projectionLocation) {
1564
+ gl.uniformMatrix4fv(this.projectionLocation, false, projection.elements);
1565
+ }
1566
+ if (this.viewportLocation) {
1567
+ gl.uniform2f(this.viewportLocation, deviceWidth, deviceHeight);
1568
+ }
1569
+ if (this.clipRangeLocation) {
1570
+ gl.uniform2f(this.clipRangeLocation, near, far);
1571
+ }
1572
+ }
1573
+ /** 遮蔽半径(米)与强度,供运行时调节。强度 0 等于关闭。 */
1574
+ setOcclusion(radius, strength) {
1575
+ const gl = this.gl;
1576
+ if (this.aoRadiusLocation)
1577
+ gl.uniform1f(this.aoRadiusLocation, radius);
1578
+ if (this.aoStrengthLocation)
1579
+ gl.uniform1f(this.aoStrengthLocation, strength);
1580
+ }
995
1581
  setClipOffset(x, y) {
996
1582
  this.gl.uniform2f(this.clipOffsetLocation, x, y);
997
1583
  }
1584
+ setClipPlane(plane) {
1585
+ if (!this.clipPlaneLocation)
1586
+ return;
1587
+ this.gl.uniform4f(this.clipPlaneLocation, plane[0], plane[1], plane[2], plane[3]);
1588
+ }
998
1589
  setVertexColors(enabled) {
999
1590
  this.gl.uniform1f(this.vertexColorLocation, enabled ? 1 : 0);
1000
1591
  }
1592
+ /** 绑定阴影贴图与光源矩阵;关闭时着色器跳过采样。 */
1593
+ setShadow(enabled, texture, matrix, normalOffset = 0, unit = 3) {
1594
+ const gl = this.gl;
1595
+ gl.uniform1f(this.shadowEnabledLocation, enabled && texture && matrix ? 1 : 0);
1596
+ if (!enabled || !texture || !matrix)
1597
+ return;
1598
+ gl.activeTexture(gl.TEXTURE0 + unit);
1599
+ gl.bindTexture(gl.TEXTURE_2D, texture);
1600
+ if (this.shadowMapLocation)
1601
+ gl.uniform1i(this.shadowMapLocation, unit);
1602
+ if (this.lightMatrixLocation)
1603
+ gl.uniformMatrix4fv(this.lightMatrixLocation, false, matrix);
1604
+ if (this.shadowNormalOffsetLocation) {
1605
+ gl.uniform1f(this.shadowNormalOffsetLocation, normalOffset);
1606
+ }
1607
+ gl.activeTexture(gl.TEXTURE0);
1608
+ }
1609
+ /** 阴影调试通道:0 关闭,1 输出遮蔽判定,2 输出深度图。 */
1610
+ setShadowDebug(mode) {
1611
+ if (!this.shadowDebugLocation)
1612
+ return;
1613
+ this.gl.uniform1f(this.shadowDebugLocation, mode);
1614
+ }
1615
+ setDirectionalShading(enabled) {
1616
+ this.gl.uniform1f(this.directionalShadingLocation, enabled ? 1 : 0);
1617
+ }
1618
+ setWaterShading(enabled) {
1619
+ this.gl.uniform1f(this.waterShadingLocation, enabled ? 1 : 0);
1620
+ }
1621
+ setCameraPosition(position) {
1622
+ this.gl.uniform3f(this.cameraPositionLocation, ...position);
1623
+ }
1001
1624
  setInstancing(enabled) {
1002
1625
  this.gl.uniform1f(this.instancingLocation, enabled ? 1 : 0);
1003
1626
  }
1627
+ setPickMode(enabled) {
1628
+ this.gl.uniform1f(this.pickModeLocation, enabled ? 1 : 0);
1629
+ }
1004
1630
  dispose() {
1005
1631
  this.gl.deleteProgram(this.program);
1006
1632
  }
@@ -1077,12 +1703,24 @@ function collectInstanceBatches(renderables, output = []) {
1077
1703
  semanticIds: group.map((renderable) => renderable.mesh.selectionId),
1078
1704
  semanticIndices,
1079
1705
  meshIds: group.map((renderable) => renderable.id),
1080
- materialKey: `${material.color}:${material.edgeColor ?? ""}`
1706
+ materialKey: [
1707
+ material.color,
1708
+ material.edgeColor ?? "",
1709
+ material.shading ?? ""
1710
+ ].join(":")
1081
1711
  });
1082
1712
  }
1083
1713
  }
1084
1714
  return output;
1085
1715
  }
1716
+ function renderablesOutsideInstanceBatches(renderables, batches) {
1717
+ const batched = /* @__PURE__ */ new Set();
1718
+ for (const batch of batches) {
1719
+ for (const id of batch.meshIds)
1720
+ batched.add(id);
1721
+ }
1722
+ return renderables.filter((renderable) => !batched.has(renderable.id));
1723
+ }
1086
1724
 
1087
1725
  // packages/renderer/dist/WebGL2InstanceBatch.js
1088
1726
  var WebGL2InstanceCache = class {
@@ -1127,10 +1765,10 @@ var WebGL2InstanceCache = class {
1127
1765
  gl.bindBuffer(gl.ARRAY_BUFFER, matrixBuffer);
1128
1766
  gl.bufferData(gl.ARRAY_BUFFER, batch.instanceMatrices, gl.DYNAMIC_DRAW);
1129
1767
  for (let column = 0; column < 4; column += 1) {
1130
- const location = WebGL2Program.INSTANCE_MATRIX_ATTRIBUTE + column;
1131
- gl.enableVertexAttribArray(location);
1132
- gl.vertexAttribPointer(location, 4, gl.FLOAT, false, 64, column * 16);
1133
- gl.vertexAttribDivisor(location, 1);
1768
+ const location2 = WebGL2Program.INSTANCE_MATRIX_ATTRIBUTE + column;
1769
+ gl.enableVertexAttribArray(location2);
1770
+ gl.vertexAttribPointer(location2, 4, gl.FLOAT, false, 64, column * 16);
1771
+ gl.vertexAttribDivisor(location2, 1);
1134
1772
  }
1135
1773
  gl.bindBuffer(gl.ARRAY_BUFFER, semanticBuffer);
1136
1774
  gl.bufferData(gl.ARRAY_BUFFER, batch.semanticIndices, gl.DYNAMIC_DRAW);
@@ -1199,8 +1837,12 @@ var WebGL2GeometryCache = class {
1199
1837
  entries = /* @__PURE__ */ new Map();
1200
1838
  active = /* @__PURE__ */ new Set();
1201
1839
  pinned = /* @__PURE__ */ new Set();
1840
+ objectKeys = /* @__PURE__ */ new WeakMap();
1841
+ legacyKeys = /* @__PURE__ */ new WeakMap();
1842
+ objectKeySequence = 0;
1202
1843
  gl = null;
1203
1844
  uploads = 0;
1845
+ updatedBytes = 0;
1204
1846
  evictions = 0;
1205
1847
  frameId = 0;
1206
1848
  inactiveGraceFrames;
@@ -1214,12 +1856,24 @@ var WebGL2GeometryCache = class {
1214
1856
  }
1215
1857
  retain(gl, geometry) {
1216
1858
  this.gl = gl;
1217
- this.active.add(geometry);
1218
- const existing = this.entries.get(geometry);
1859
+ const key = this.keyFor(geometry);
1860
+ this.active.add(key);
1861
+ const existing = this.entries.get(key);
1219
1862
  if (existing) {
1220
- existing.lastUsedFrame = this.frameId;
1221
- return existing;
1863
+ if (this.canReuse(existing, geometry)) {
1864
+ existing.lastUsedFrame = this.frameId;
1865
+ return existing;
1866
+ }
1867
+ }
1868
+ const next = this.createEntry(gl, geometry, key);
1869
+ if (existing) {
1870
+ this.deleteEntry(existing, false);
1222
1871
  }
1872
+ this.entries.set(key, next);
1873
+ this.uploads += 1;
1874
+ return next;
1875
+ }
1876
+ createEntry(gl, geometry, key) {
1223
1877
  const vertexArray = gl.createVertexArray();
1224
1878
  const positionBuffer = gl.createBuffer();
1225
1879
  const triangleIndexBuffer = gl.createBuffer();
@@ -1260,6 +1914,10 @@ var WebGL2GeometryCache = class {
1260
1914
  }
1261
1915
  gl.bindVertexArray(null);
1262
1916
  const entry = {
1917
+ geometry,
1918
+ geometryId: geometry.identity?.geometryId ?? key,
1919
+ attributeDigest: geometry.identity?.attributeDigest ?? "",
1920
+ topologyDigest: geometry.identity?.topologyDigest ?? "",
1263
1921
  vertexArray,
1264
1922
  positionBuffer,
1265
1923
  triangleIndexBuffer,
@@ -1271,28 +1929,33 @@ var WebGL2GeometryCache = class {
1271
1929
  bytes: geometry.positions.byteLength + geometry.indices.byteLength + (geometry.edgeIndices?.byteLength ?? 0) + (geometry.colors?.byteLength ?? 0) + (geometry.semanticIndices?.byteLength ?? 0),
1272
1930
  lastUsedFrame: this.frameId
1273
1931
  };
1274
- this.entries.set(geometry, entry);
1275
- this.uploads += 1;
1276
1932
  return entry;
1277
1933
  }
1278
1934
  beginFrame() {
1279
1935
  this.frameId += 1;
1280
1936
  this.active = /* @__PURE__ */ new Set();
1281
1937
  }
1282
- pin(gl, geometry) {
1283
- this.pinned.add(geometry);
1938
+ pin(gl, geometry, update) {
1939
+ const key = this.keyFor(geometry);
1940
+ this.pinned.add(key);
1941
+ const existing = this.entries.get(key);
1942
+ if (existing && update && this.sameTopology(existing, geometry)) {
1943
+ this.gl = gl;
1944
+ this.applyPositionUpdate(gl, existing, geometry, update);
1945
+ return existing;
1946
+ }
1284
1947
  const entry = this.retain(gl, geometry);
1285
1948
  this.enforceBudget();
1286
1949
  if (this.totalBytes > this.maxBytes) {
1287
- this.pinned.delete(geometry);
1288
- this.deleteEntry(entry);
1289
- this.entries.delete(geometry);
1950
+ this.pinned.delete(key);
1951
+ this.deleteEntry(entry, false);
1952
+ this.entries.delete(key);
1290
1953
  throw new Error("ICE3_GPU_RESOURCE_FAILED: geometry exceeds the GPU residency budget");
1291
1954
  }
1292
1955
  return entry;
1293
1956
  }
1294
1957
  unpin(geometry) {
1295
- this.pinned.delete(geometry);
1958
+ this.pinned.delete(this.keyFor(geometry));
1296
1959
  }
1297
1960
  endFrame() {
1298
1961
  for (const [geometry, entry] of this.entries) {
@@ -1327,10 +1990,13 @@ var WebGL2GeometryCache = class {
1327
1990
  get uploadCount() {
1328
1991
  return this.uploads;
1329
1992
  }
1993
+ get updateBytes() {
1994
+ return this.updatedBytes;
1995
+ }
1330
1996
  get evictionCount() {
1331
1997
  return this.evictions;
1332
1998
  }
1333
- deleteEntry(entry) {
1999
+ deleteEntry(entry, eviction = true) {
1334
2000
  if (!this.gl)
1335
2001
  return;
1336
2002
  this.gl.deleteBuffer(entry.positionBuffer);
@@ -1339,7 +2005,9 @@ var WebGL2GeometryCache = class {
1339
2005
  this.gl.deleteBuffer(entry.colorBuffer);
1340
2006
  this.gl.deleteBuffer(entry.semanticBuffer);
1341
2007
  this.gl.deleteVertexArray(entry.vertexArray);
1342
- this.evictions += 1;
2008
+ if (eviction) {
2009
+ this.evictions += 1;
2010
+ }
1343
2011
  }
1344
2012
  enforceBudget() {
1345
2013
  let total = this.totalBytes;
@@ -1354,7 +2022,96 @@ var WebGL2GeometryCache = class {
1354
2022
  total -= entry.bytes;
1355
2023
  }
1356
2024
  }
2025
+ applyPositionUpdate(gl, entry, geometry, update) {
2026
+ let updated = 0;
2027
+ try {
2028
+ gl.bindBuffer(gl.ARRAY_BUFFER, entry.positionBuffer);
2029
+ for (const range of update.positionRanges) {
2030
+ if (range.byteOffset % 4 !== 0 || range.byteLength % 4 !== 0 || range.byteOffset + range.byteLength > geometry.positions.byteLength) {
2031
+ throw new Error("invalid position range");
2032
+ }
2033
+ gl.bufferSubData(gl.ARRAY_BUFFER, range.byteOffset, new Uint8Array(geometry.positions.buffer, geometry.positions.byteOffset + range.byteOffset, range.byteLength));
2034
+ updated += range.byteLength;
2035
+ }
2036
+ } catch (error) {
2037
+ throw new Error("ICE3_GPU_RESOURCE_FAILED: geometry update failed", {
2038
+ cause: error
2039
+ });
2040
+ }
2041
+ entry.geometry = geometry;
2042
+ entry.lastUsedFrame = this.frameId;
2043
+ this.updatedBytes += updated;
2044
+ }
2045
+ canReuse(existing, geometry) {
2046
+ if (existing.geometry === geometry) {
2047
+ return true;
2048
+ }
2049
+ if (!this.sameTopology(existing, geometry)) {
2050
+ return false;
2051
+ }
2052
+ const existingDigest = existing.attributeDigest;
2053
+ const nextDigest = geometry.identity?.attributeDigest ?? "";
2054
+ return (existing.geometry.positions === geometry.positions || equalNumberArrays(existing.geometry.positions, geometry.positions)) && equalOptionalNumberArrays(existing.geometry.colors, geometry.colors) && equalOptionalNumberArrays(existing.geometry.semanticIndices, geometry.semanticIndices) && equalOptionalNumberArrays(existing.geometry.edgeIndices, geometry.edgeIndices) || existingDigest !== "" && existingDigest === nextDigest;
2055
+ }
2056
+ sameTopology(existing, geometry) {
2057
+ const existingDigest = existing.topologyDigest;
2058
+ const nextDigest = geometry.identity?.topologyDigest ?? "";
2059
+ return existing.geometry.indices.constructor === geometry.indices.constructor && existing.geometry.indices.length === geometry.indices.length && (existing.geometry.indices === geometry.indices || existingDigest !== "" && existingDigest === nextDigest || equalTypedArrays(existing.geometry.indices, geometry.indices));
2060
+ }
2061
+ keyFor(geometry) {
2062
+ const identity = geometry.identity?.geometryId;
2063
+ if (identity) {
2064
+ return `identity:${identity}`;
2065
+ }
2066
+ const id = geometry.id;
2067
+ if (typeof id === "string" && id.trim()) {
2068
+ const cached = this.legacyKeys.get(geometry);
2069
+ if (cached)
2070
+ return cached;
2071
+ const key2 = [
2072
+ "content",
2073
+ geometry.positions.length,
2074
+ geometry.indices.length,
2075
+ contentFingerprint(geometry.positions),
2076
+ contentFingerprint(geometry.indices),
2077
+ geometry.colors ? contentFingerprint(geometry.colors) : 0,
2078
+ geometry.edgeIndices ? contentFingerprint(geometry.edgeIndices) : 0,
2079
+ geometry.semanticIndices ? contentFingerprint(geometry.semanticIndices) : 0
2080
+ ].join(":");
2081
+ this.legacyKeys.set(geometry, key2);
2082
+ return key2;
2083
+ }
2084
+ const existing = this.objectKeys.get(geometry);
2085
+ if (existing) {
2086
+ return existing;
2087
+ }
2088
+ const key = `object:${this.objectKeySequence}`;
2089
+ this.objectKeySequence += 1;
2090
+ this.objectKeys.set(geometry, key);
2091
+ return key;
2092
+ }
1357
2093
  };
2094
+ function equalTypedArrays(left, right) {
2095
+ return left.length === right.length && left.every((value, index) => value === right[index]);
2096
+ }
2097
+ function equalNumberArrays(left, right) {
2098
+ return left.length === right.length && left.every((value, index) => value === right[index]);
2099
+ }
2100
+ function equalOptionalNumberArrays(left, right) {
2101
+ if (left === void 0 || right === void 0) {
2102
+ return left === right;
2103
+ }
2104
+ return left.length === right.length && left.every((value, index) => value === right[index]);
2105
+ }
2106
+ function contentFingerprint(values) {
2107
+ let hash = 2166136261;
2108
+ const bytes = new Uint8Array(values.buffer, values.byteOffset, values.byteLength);
2109
+ for (const byte of bytes) {
2110
+ hash ^= byte;
2111
+ hash = Math.imul(hash, 16777619) >>> 0;
2112
+ }
2113
+ return hash;
2114
+ }
1358
2115
 
1359
2116
  // packages/renderer/dist/WebGL2PickCodec.js
1360
2117
  var MAX_SELECTION_INDEX = 16777215;
@@ -1561,6 +2318,7 @@ var WebGL2Picker = class {
1561
2318
  gl.depthFunc(gl.LEQUAL);
1562
2319
  gl.disable(gl.BLEND);
1563
2320
  this.program.use();
2321
+ this.program.setPickMode(true);
1564
2322
  this.program.setClipOffset(0, 0);
1565
2323
  this.program.setInstancing(false);
1566
2324
  const aspect = frame.viewport.width / Math.max(frame.viewport.height, 1);
@@ -1706,13 +2464,529 @@ var WebGL2Picker = class {
1706
2464
  }
1707
2465
  };
1708
2466
 
2467
+ // packages/renderer/dist/WebGL2Shader.js
2468
+ function createShaderProgram(gl, vertexSource, fragmentSource, label) {
2469
+ const vertex = compileShader(gl, gl.VERTEX_SHADER, vertexSource, `${label}:vertex`);
2470
+ const fragment = compileShader(gl, gl.FRAGMENT_SHADER, fragmentSource, `${label}:fragment`);
2471
+ const program = gl.createProgram();
2472
+ if (!program) {
2473
+ throw new ICEEngineError("ICE3_SHADER_LINK_FAILED", `ICE3_SHADER_LINK_FAILED: ${label}: program allocation failed`);
2474
+ }
2475
+ gl.attachShader(program, vertex);
2476
+ gl.attachShader(program, fragment);
2477
+ gl.linkProgram(program);
2478
+ gl.deleteShader(vertex);
2479
+ gl.deleteShader(fragment);
2480
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
2481
+ const log = gl.getProgramInfoLog(program) ?? "unknown link error";
2482
+ gl.deleteProgram(program);
2483
+ throw new ICEEngineError("ICE3_SHADER_LINK_FAILED", `ICE3_SHADER_LINK_FAILED: ${label}: ${log}`);
2484
+ }
2485
+ return program;
2486
+ }
2487
+ function compileShader(gl, type, source, label) {
2488
+ const shader = gl.createShader(type);
2489
+ if (!shader) {
2490
+ throw new ICEEngineError("ICE3_SHADER_COMPILE_FAILED", `ICE3_SHADER_COMPILE_FAILED: ${label}: allocation failed`);
2491
+ }
2492
+ gl.shaderSource(shader, source);
2493
+ gl.compileShader(shader);
2494
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
2495
+ const log = gl.getShaderInfoLog(shader) ?? "unknown compile error";
2496
+ gl.deleteShader(shader);
2497
+ throw new ICEEngineError("ICE3_SHADER_COMPILE_FAILED", `ICE3_SHADER_COMPILE_FAILED: ${label}: ${log}`);
2498
+ }
2499
+ return shader;
2500
+ }
2501
+
2502
+ // packages/renderer/dist/ShadowFrustum.js
2503
+ var SHADOW_LIGHT_DIRECTION = [-0.35, 0.85, 0.4];
2504
+ var BOUNDS_PADDING = 0.08;
2505
+ function shadowFrustum(bounds, mapSize = 2048) {
2506
+ const box = bounds ?? { min: [-50, -5, -50], max: [650, 60, 650] };
2507
+ const centre = [
2508
+ (box.min[0] + box.max[0]) / 2,
2509
+ (box.min[1] + box.max[1]) / 2,
2510
+ (box.min[2] + box.max[2]) / 2
2511
+ ];
2512
+ const radius = Math.max(1, 0.5 * Math.hypot(box.max[0] - box.min[0], box.max[1] - box.min[1], box.max[2] - box.min[2]));
2513
+ const pad = radius * BOUNDS_PADDING;
2514
+ const direction = normalize2(SHADOW_LIGHT_DIRECTION);
2515
+ const distance2 = radius * 2 + pad * 2;
2516
+ const eye = [
2517
+ centre[0] + direction[0] * distance2,
2518
+ centre[1] + direction[1] * distance2,
2519
+ centre[2] + direction[2] * distance2
2520
+ ];
2521
+ const view = new ICEMatrix4().lookAt(new ICEVector3(eye[0], eye[1], eye[2]), new ICEVector3(centre[0], centre[1], centre[2]), new ICEVector3(0, 1, 0));
2522
+ let minX = Infinity;
2523
+ let maxX = -Infinity;
2524
+ let minY = Infinity;
2525
+ let maxY = -Infinity;
2526
+ let minZ = Infinity;
2527
+ let maxZ = -Infinity;
2528
+ for (let corner = 0; corner < 8; corner += 1) {
2529
+ const point = new ICEVector3(corner & 1 ? box.max[0] : box.min[0], corner & 2 ? box.max[1] : box.min[1], corner & 4 ? box.max[2] : box.min[2]);
2530
+ const projected = view.transformPoint(point);
2531
+ minX = Math.min(minX, projected[0]);
2532
+ maxX = Math.max(maxX, projected[0]);
2533
+ minY = Math.min(minY, projected[1]);
2534
+ maxY = Math.max(maxY, projected[1]);
2535
+ minZ = Math.min(minZ, projected[2]);
2536
+ maxZ = Math.max(maxZ, projected[2]);
2537
+ }
2538
+ const projection = orthographic(minX - pad, maxX + pad, minY - pad, maxY + pad, -maxZ - pad, -minZ + pad);
2539
+ const width = maxX - minX + pad * 2;
2540
+ const height = maxY - minY + pad * 2;
2541
+ return {
2542
+ matrix: multiply(projection, view.elements),
2543
+ texelWorld: Math.max(width, height) / mapSize
2544
+ };
2545
+ }
2546
+ function normalize2(value) {
2547
+ const length = Math.hypot(value[0], value[1], value[2]) || 1;
2548
+ return [value[0] / length, value[1] / length, value[2] / length];
2549
+ }
2550
+ function orthographic(left, right, bottom, top, near, far) {
2551
+ const lr = 1 / (left - right);
2552
+ const bt = 1 / (bottom - top);
2553
+ const nf = 1 / (near - far);
2554
+ return [
2555
+ -2 * lr,
2556
+ 0,
2557
+ 0,
2558
+ 0,
2559
+ 0,
2560
+ -2 * bt,
2561
+ 0,
2562
+ 0,
2563
+ 0,
2564
+ 0,
2565
+ 2 * nf,
2566
+ 0,
2567
+ (left + right) * lr,
2568
+ (top + bottom) * bt,
2569
+ (far + near) * nf,
2570
+ 1
2571
+ ];
2572
+ }
2573
+ function multiply(a, b) {
2574
+ const out = new Float32Array(16);
2575
+ for (let column = 0; column < 4; column += 1) {
2576
+ for (let row = 0; row < 4; row += 1) {
2577
+ let sum = 0;
2578
+ for (let k = 0; k < 4; k += 1) {
2579
+ sum += (a[k * 4 + row] ?? 0) * (b[column * 4 + k] ?? 0);
2580
+ }
2581
+ out[column * 4 + row] = sum;
2582
+ }
2583
+ }
2584
+ return out;
2585
+ }
2586
+
2587
+ // packages/renderer/dist/WebGL2ShadowMap.js
2588
+ var VERTEX_SOURCE3 = `#version 300 es
2589
+ layout(location = 0) in vec3 a_position;
2590
+ layout(location = 3) in mat4 a_instanceMatrix;
2591
+ uniform mat4 u_matrix;
2592
+ uniform float u_useInstancing;
2593
+ void main() {
2594
+ vec4 local = vec4(a_position, 1.0);
2595
+ vec4 world = mix(local, a_instanceMatrix * local, u_useInstancing);
2596
+ gl_Position = u_matrix * world;
2597
+ }
2598
+ `;
2599
+ var FRAGMENT_SOURCE3 = `#version 300 es
2600
+ precision highp float;
2601
+ void main() {
2602
+ // \u53EA\u5199\u6DF1\u5EA6\uFF0C\u989C\u8272\u4E0D\u9700\u8981\u3002
2603
+ }
2604
+ `;
2605
+ var SHADOW_SIZE = 2048;
2606
+ var WebGL2ShadowMap = class {
2607
+ gl;
2608
+ /** 最近一次着色器编译/链接失败的原因,供诊断读取。 */
2609
+ lastError = null;
2610
+ framebuffer = null;
2611
+ depthTexture = null;
2612
+ program = null;
2613
+ matrixLocation = null;
2614
+ instancingLocation = null;
2615
+ matrixScratch = new Float32Array(16);
2616
+ texelWorldValue = 1;
2617
+ constructor(gl) {
2618
+ this.gl = gl;
2619
+ this.createResources();
2620
+ }
2621
+ /** 深度纹理,供主着色器采样。 */
2622
+ get texture() {
2623
+ return this.depthTexture;
2624
+ }
2625
+ get matrix() {
2626
+ return this.matrixScratch;
2627
+ }
2628
+ /** 一个深度纹素在世界空间的边长(米)。法线偏移按它换算,别用魔数。 */
2629
+ get texelWorld() {
2630
+ return this.texelWorldValue;
2631
+ }
2632
+ /**
2633
+ * 按场景包围盒拟合视锥。每帧调用没有意义——场景内容不变时矩阵也不会变,
2634
+ * 所以由调用方决定何时重算(包围盒变化或首帧)。
2635
+ */
2636
+ setBounds(bounds) {
2637
+ const frustum = shadowFrustum(bounds, SHADOW_SIZE);
2638
+ this.matrixScratch.set(frustum.matrix);
2639
+ this.texelWorldValue = frustum.texelWorld;
2640
+ }
2641
+ /** 绑定阴影帧缓冲,返回是否成功。 */
2642
+ begin() {
2643
+ const gl = this.gl;
2644
+ if (!this.framebuffer || !this.program)
2645
+ return false;
2646
+ gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer);
2647
+ gl.viewport(0, 0, SHADOW_SIZE, SHADOW_SIZE);
2648
+ gl.disable(gl.POLYGON_OFFSET_FILL);
2649
+ gl.clear(gl.DEPTH_BUFFER_BIT);
2650
+ gl.enable(gl.DEPTH_TEST);
2651
+ gl.disable(gl.CULL_FACE);
2652
+ gl.useProgram(this.program);
2653
+ gl.uniformMatrix4fv(this.matrixLocation, false, this.matrixScratch);
2654
+ gl.uniform1f(this.instancingLocation, 1);
2655
+ return true;
2656
+ }
2657
+ /**
2658
+ * 非实例化绘制:矩阵换成 `lightViewProjection × world`,顶点位置直接当世界坐标用。
2659
+ *
2660
+ * 阴影通道原先只会画实例化批次,瓦片(渲染批次)与落单网格因此完全不投影。
2661
+ * 补这两条路径靠的就是这个入口——和 `WebGL2SceneDepth` 同一套写法。
2662
+ */
2663
+ setMatrix(matrix) {
2664
+ this.gl.uniformMatrix4fv(this.matrixLocation, false, matrix);
2665
+ }
2666
+ setInstancing(enabled) {
2667
+ this.gl.uniform1f(this.instancingLocation, enabled ? 1 : 0);
2668
+ }
2669
+ /** 解绑,恢复默认帧缓冲。 */
2670
+ end(width, height) {
2671
+ const gl = this.gl;
2672
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
2673
+ gl.disable(gl.CULL_FACE);
2674
+ gl.enable(gl.POLYGON_OFFSET_FILL);
2675
+ gl.viewport(0, 0, width, height);
2676
+ }
2677
+ dispose() {
2678
+ const gl = this.gl;
2679
+ if (this.depthTexture)
2680
+ gl.deleteTexture(this.depthTexture);
2681
+ if (this.framebuffer)
2682
+ gl.deleteFramebuffer(this.framebuffer);
2683
+ if (this.program)
2684
+ gl.deleteProgram(this.program);
2685
+ this.depthTexture = null;
2686
+ this.framebuffer = null;
2687
+ this.program = null;
2688
+ }
2689
+ createResources() {
2690
+ const gl = this.gl;
2691
+ this.depthTexture = gl.createTexture();
2692
+ gl.bindTexture(gl.TEXTURE_2D, this.depthTexture);
2693
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.DEPTH_COMPONENT24, SHADOW_SIZE, SHADOW_SIZE, 0, gl.DEPTH_COMPONENT, gl.UNSIGNED_INT, null);
2694
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
2695
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
2696
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
2697
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
2698
+ this.framebuffer = gl.createFramebuffer();
2699
+ gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer);
2700
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, this.depthTexture, 0);
2701
+ gl.drawBuffers([gl.NONE]);
2702
+ gl.readBuffer(gl.NONE);
2703
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
2704
+ this.program = this.createProgram(VERTEX_SOURCE3, FRAGMENT_SOURCE3);
2705
+ this.matrixLocation = this.program ? gl.getUniformLocation(this.program, "u_matrix") : null;
2706
+ this.instancingLocation = this.program ? gl.getUniformLocation(this.program, "u_useInstancing") : null;
2707
+ this.setBounds(null);
2708
+ }
2709
+ createProgram(vertex, fragment) {
2710
+ try {
2711
+ return createShaderProgram(this.gl, vertex, fragment, "shadow");
2712
+ } catch (error) {
2713
+ this.lastError = error instanceof Error ? error.message : String(error);
2714
+ return null;
2715
+ }
2716
+ }
2717
+ };
2718
+
2719
+ // packages/renderer/dist/SceneBounds.js
2720
+ var localBoundsCache = /* @__PURE__ */ new WeakMap();
2721
+ function geometryLocalBounds(geometry) {
2722
+ const cached = localBoundsCache.get(geometry);
2723
+ if (cached)
2724
+ return cached;
2725
+ const positions = geometry.positions;
2726
+ if (!positions || positions.length < 3)
2727
+ return null;
2728
+ let minX = Infinity;
2729
+ let minY = Infinity;
2730
+ let minZ = Infinity;
2731
+ let maxX = -Infinity;
2732
+ let maxY = -Infinity;
2733
+ let maxZ = -Infinity;
2734
+ for (let index = 0; index + 2 < positions.length; index += 3) {
2735
+ const x = positions[index];
2736
+ const y = positions[index + 1];
2737
+ const z = positions[index + 2];
2738
+ if (x < minX)
2739
+ minX = x;
2740
+ if (y < minY)
2741
+ minY = y;
2742
+ if (z < minZ)
2743
+ minZ = z;
2744
+ if (x > maxX)
2745
+ maxX = x;
2746
+ if (y > maxY)
2747
+ maxY = y;
2748
+ if (z > maxZ)
2749
+ maxZ = z;
2750
+ }
2751
+ const bounds = { min: [minX, minY, minZ], max: [maxX, maxY, maxZ] };
2752
+ localBoundsCache.set(geometry, bounds);
2753
+ return bounds;
2754
+ }
2755
+ function accumulateSceneBounds(renderables) {
2756
+ let minX = Infinity;
2757
+ let minY = Infinity;
2758
+ let minZ = Infinity;
2759
+ let maxX = -Infinity;
2760
+ let maxY = -Infinity;
2761
+ let maxZ = -Infinity;
2762
+ let seen = false;
2763
+ for (const renderable of renderables) {
2764
+ const local = geometryLocalBounds(renderable.mesh.geometry);
2765
+ if (!local)
2766
+ continue;
2767
+ const matrix = renderable.worldMatrix;
2768
+ for (let corner = 0; corner < 8; corner += 1) {
2769
+ const x = corner & 1 ? local.max[0] : local.min[0];
2770
+ const y = corner & 2 ? local.max[1] : local.min[1];
2771
+ const z = corner & 4 ? local.max[2] : local.min[2];
2772
+ const worldX = matrix[0] * x + matrix[4] * y + matrix[8] * z + matrix[12];
2773
+ const worldY = matrix[1] * x + matrix[5] * y + matrix[9] * z + matrix[13];
2774
+ const worldZ = matrix[2] * x + matrix[6] * y + matrix[10] * z + matrix[14];
2775
+ if (worldX < minX)
2776
+ minX = worldX;
2777
+ if (worldY < minY)
2778
+ minY = worldY;
2779
+ if (worldZ < minZ)
2780
+ minZ = worldZ;
2781
+ if (worldX > maxX)
2782
+ maxX = worldX;
2783
+ if (worldY > maxY)
2784
+ maxY = worldY;
2785
+ if (worldZ > maxZ)
2786
+ maxZ = worldZ;
2787
+ seen = true;
2788
+ }
2789
+ }
2790
+ return seen ? { min: [minX, minY, minZ], max: [maxX, maxY, maxZ] } : null;
2791
+ }
2792
+
2793
+ // packages/renderer/dist/WebGL2SceneDepth.js
2794
+ var VERTEX_SOURCE4 = `#version 300 es
2795
+ layout(location = 0) in vec3 a_position;
2796
+ layout(location = 3) in mat4 a_instanceMatrix;
2797
+ uniform mat4 u_matrix;
2798
+ uniform float u_useInstancing;
2799
+ void main() {
2800
+ vec4 local = vec4(a_position, 1.0);
2801
+ vec4 world = mix(local, a_instanceMatrix * local, u_useInstancing);
2802
+ gl_Position = u_matrix * world;
2803
+ }
2804
+ `;
2805
+ var FRAGMENT_SOURCE4 = `#version 300 es
2806
+ precision highp float;
2807
+ void main() {}
2808
+ `;
2809
+ var WebGL2SceneDepth = class {
2810
+ gl;
2811
+ framebuffer;
2812
+ depthTexture = null;
2813
+ program;
2814
+ matrixLocation;
2815
+ instancingLocation;
2816
+ width = 0;
2817
+ height = 0;
2818
+ constructor(gl) {
2819
+ this.gl = gl;
2820
+ this.program = createShaderProgram(gl, VERTEX_SOURCE4, FRAGMENT_SOURCE4, "scene-depth");
2821
+ this.matrixLocation = gl.getUniformLocation(this.program, "u_matrix");
2822
+ this.instancingLocation = gl.getUniformLocation(this.program, "u_useInstancing");
2823
+ const framebuffer = gl.createFramebuffer();
2824
+ if (!framebuffer)
2825
+ throw new Error("ICE3_FRAMEBUFFER_FAILED: scene depth");
2826
+ this.framebuffer = framebuffer;
2827
+ }
2828
+ get texture() {
2829
+ return this.depthTexture;
2830
+ }
2831
+ begin(viewProjection, width, height) {
2832
+ const gl = this.gl;
2833
+ this.ensureTexture(Math.max(1, width), Math.max(1, height));
2834
+ gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer);
2835
+ gl.viewport(0, 0, this.width, this.height);
2836
+ gl.disable(gl.POLYGON_OFFSET_FILL);
2837
+ gl.disable(gl.BLEND);
2838
+ gl.depthMask(true);
2839
+ gl.clear(gl.DEPTH_BUFFER_BIT);
2840
+ gl.enable(gl.DEPTH_TEST);
2841
+ gl.useProgram(this.program);
2842
+ gl.uniformMatrix4fv(this.matrixLocation, false, viewProjection);
2843
+ gl.uniform1f(this.instancingLocation, 0);
2844
+ }
2845
+ setMatrix(matrix) {
2846
+ this.gl.uniformMatrix4fv(this.matrixLocation, false, matrix);
2847
+ }
2848
+ setInstancing(enabled) {
2849
+ this.gl.uniform1f(this.instancingLocation, enabled ? 1 : 0);
2850
+ }
2851
+ end(width, height) {
2852
+ const gl = this.gl;
2853
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
2854
+ gl.enable(gl.POLYGON_OFFSET_FILL);
2855
+ gl.viewport(0, 0, width, height);
2856
+ }
2857
+ dispose() {
2858
+ const gl = this.gl;
2859
+ if (this.depthTexture)
2860
+ gl.deleteTexture(this.depthTexture);
2861
+ gl.deleteFramebuffer(this.framebuffer);
2862
+ gl.deleteProgram(this.program);
2863
+ this.depthTexture = null;
2864
+ }
2865
+ ensureTexture(width, height) {
2866
+ const gl = this.gl;
2867
+ gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer);
2868
+ if (this.depthTexture && this.width === width && this.height === height) {
2869
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, this.depthTexture, 0);
2870
+ return;
2871
+ }
2872
+ if (this.depthTexture)
2873
+ gl.deleteTexture(this.depthTexture);
2874
+ this.width = width;
2875
+ this.height = height;
2876
+ const texture = gl.createTexture();
2877
+ gl.bindTexture(gl.TEXTURE_2D, texture);
2878
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.DEPTH_COMPONENT24, width, height, 0, gl.DEPTH_COMPONENT, gl.UNSIGNED_INT, null);
2879
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
2880
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
2881
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
2882
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
2883
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, texture, 0);
2884
+ gl.drawBuffers([gl.NONE]);
2885
+ gl.readBuffer(gl.NONE);
2886
+ this.depthTexture = texture;
2887
+ }
2888
+ };
2889
+
2890
+ // packages/renderer/dist/WebGL2TextureCache.js
2891
+ var REPEAT = 10497;
2892
+ var fetchImageLoader = async (url) => {
2893
+ const response = await fetch(url);
2894
+ if (!response.ok) {
2895
+ throw new Error(`ICE3_TEXTURE_FETCH_FAILED: ${response.status} ${url}`);
2896
+ }
2897
+ return createImageBitmap(await response.blob());
2898
+ };
2899
+ var WebGL2TextureCache = class {
2900
+ gl;
2901
+ load;
2902
+ onUpload;
2903
+ entries = /* @__PURE__ */ new Map();
2904
+ disposed = false;
2905
+ constructor(gl, load = fetchImageLoader, onUpload) {
2906
+ this.gl = gl;
2907
+ this.load = load;
2908
+ this.onUpload = onUpload;
2909
+ }
2910
+ /** Texture for `url`, or `null` while it is still loading (or after failure). */
2911
+ texture(url) {
2912
+ if (this.disposed)
2913
+ return null;
2914
+ const existing = this.entries.get(url);
2915
+ if (existing) {
2916
+ return existing.state === "ready" ? existing.texture : null;
2917
+ }
2918
+ this.entries.set(url, { state: "pending" });
2919
+ void this.load(url).then((image) => {
2920
+ if (this.disposed) {
2921
+ image.close?.();
2922
+ return;
2923
+ }
2924
+ const texture = this.upload(image);
2925
+ if (this.disposed) {
2926
+ this.gl.deleteTexture(texture);
2927
+ return;
2928
+ }
2929
+ this.entries.set(url, { state: "ready", texture });
2930
+ this.onUpload?.();
2931
+ }).catch(() => {
2932
+ if (!this.disposed)
2933
+ this.entries.set(url, { state: "failed" });
2934
+ });
2935
+ return null;
2936
+ }
2937
+ stats() {
2938
+ let loaded = 0;
2939
+ let pending = 0;
2940
+ let failed2 = 0;
2941
+ for (const entry of this.entries.values()) {
2942
+ if (entry.state === "ready")
2943
+ loaded += 1;
2944
+ else if (entry.state === "pending")
2945
+ pending += 1;
2946
+ else
2947
+ failed2 += 1;
2948
+ }
2949
+ return { loaded, pending, failed: failed2 };
2950
+ }
2951
+ dispose() {
2952
+ if (this.disposed)
2953
+ return;
2954
+ this.disposed = true;
2955
+ for (const entry of this.entries.values()) {
2956
+ if (entry.state === "ready")
2957
+ this.gl.deleteTexture(entry.texture);
2958
+ }
2959
+ this.entries.clear();
2960
+ }
2961
+ upload(image) {
2962
+ const gl = this.gl;
2963
+ const texture = gl.createTexture();
2964
+ if (!texture) {
2965
+ throw new Error("ICE3_TEXTURE_CREATE_FAILED");
2966
+ }
2967
+ gl.bindTexture(gl.TEXTURE_2D, texture);
2968
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
2969
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, REPEAT);
2970
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, REPEAT);
2971
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
2972
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
2973
+ gl.generateMipmap(gl.TEXTURE_2D);
2974
+ gl.bindTexture(gl.TEXTURE_2D, null);
2975
+ image.close?.();
2976
+ return texture;
2977
+ }
2978
+ };
2979
+
2980
+ // packages/renderer/dist/WebGL2Uv.js
2981
+ var DEFAULT_UV_SCALE = [1, 1];
2982
+
1709
2983
  // packages/renderer/dist/Color.js
1710
2984
  var COLOR_CACHE_LIMIT = 128;
1711
2985
  var COLOR_CACHE = /* @__PURE__ */ new Map();
1712
2986
  function colorToRgba(color) {
1713
2987
  const normalized = color.startsWith("#") ? color.slice(1) : color;
1714
2988
  const expanded = normalized.length === 3 ? normalized.split("").map((part) => part + part).join("") : normalized;
1715
- if (!/^[0-9a-fA-F]{6}$/.test(expanded)) {
2989
+ if (!/^[0-9a-fA-F]{6}([0-9a-fA-F]{2})?$/.test(expanded)) {
1716
2990
  throw new Error(`Unsupported ICE basic material color: ${color}`);
1717
2991
  }
1718
2992
  const key = expanded.toLowerCase();
@@ -1723,7 +2997,7 @@ function colorToRgba(color) {
1723
2997
  Number.parseInt(expanded.slice(0, 2), 16) / 255,
1724
2998
  Number.parseInt(expanded.slice(2, 4), 16) / 255,
1725
2999
  Number.parseInt(expanded.slice(4, 6), 16) / 255,
1726
- 1
3000
+ expanded.length === 8 ? Number.parseInt(expanded.slice(6, 8), 16) / 255 : 1
1727
3001
  ]);
1728
3002
  if (COLOR_CACHE.size >= COLOR_CACHE_LIMIT) {
1729
3003
  const oldest = COLOR_CACHE.keys().next().value;
@@ -1781,6 +3055,9 @@ var EDGE_PIXEL_OFFSETS = [
1781
3055
  ];
1782
3056
  var SELECTION_RGBA = [255 / 255, 208 / 255, 0];
1783
3057
  var SELECTION_TINT_ALPHA = 0.65;
3058
+ function isTransparentColor(color) {
3059
+ return colorToRgba(color)[3] < 0.999;
3060
+ }
1784
3061
  var WebGL2RendererBackend = class {
1785
3062
  capabilities = {
1786
3063
  backend: "webgl2",
@@ -1794,6 +3071,12 @@ var WebGL2RendererBackend = class {
1794
3071
  onContextLost;
1795
3072
  onContextRestored;
1796
3073
  program = null;
3074
+ clipPlane = [0, 0, 0, 0];
3075
+ shadowMap = null;
3076
+ /** 方向光阴影开关:默认关闭,由 `initialize({ shadows })` 或页面地址 `?shadows=` 打开。 */
3077
+ shadowEnabled = false;
3078
+ /** `?shadowdebug=1` 输出遮蔽判定,`=2` 输出深度图;排查阴影问题的唯一可信手段。 */
3079
+ shadowDebug = 0;
1797
3080
  semanticProgram = null;
1798
3081
  geometryCache = null;
1799
3082
  instanceCache = null;
@@ -1821,10 +3104,108 @@ var WebGL2RendererBackend = class {
1821
3104
  gpuEvictions: 0,
1822
3105
  gpuBytes: 0
1823
3106
  };
3107
+ textures = null;
3108
+ sceneDepth = null;
3109
+ occlusionRadius = 2.5;
3110
+ /**
3111
+ * 三圈采样比双圈多找到约 1.7 个亮度点的遮蔽(同参数 96.36 → 94.65),原样沿用到
3112
+ * 默认值就把厂区压得发闷。0.30 是回调后的值:整帧 95.70(无遮蔽 99.59),
3113
+ * 高光像素 878(无遮蔽 1088),接触暗部仍看得见。详见 SPEC-007 第 5.5 节。
3114
+ */
3115
+ occlusionStrength = 0.3;
3116
+ options = null;
3117
+ /**
3118
+ * Bind the material's colour and (if it has one) its base-colour texture.
3119
+ * Called for every opaque/shaded draw so a batched draw and a single mesh draw
3120
+ * cannot drift apart.
3121
+ */
3122
+ applyMaterial(material) {
3123
+ const program = this.program;
3124
+ if (!program)
3125
+ return;
3126
+ program.setColor(colorToRgba(material.color));
3127
+ const map = material.map;
3128
+ const texture = map && this.textures ? this.textures.texture(map.url) : null;
3129
+ program.setBaseColorMap(texture, map?.scale ?? material.ormMap?.scale ?? DEFAULT_UV_SCALE);
3130
+ const normalMap = material.normalMap;
3131
+ const normalTexture = normalMap && this.textures ? this.textures.texture(normalMap.url) : null;
3132
+ program.setNormalMap(normalTexture, normalMap?.strength ?? 1);
3133
+ const ormMap = material.ormMap;
3134
+ const ormTexture = ormMap && this.textures ? this.textures.texture(ormMap.url) : null;
3135
+ program.setOrmMap(ormTexture, ormMap?.occlusion !== false, ormMap?.metallicRoughness !== false);
3136
+ program.setSurface(material.metallic, material.roughness, material.emissive);
3137
+ }
3138
+ /** Edge overlays are line work: never sampled, always flat. */
3139
+ applyEdgeMaterial(material) {
3140
+ const program = this.program;
3141
+ if (!program)
3142
+ return;
3143
+ program.setColor(colorToRgba(material.edgeColor ?? material.color));
3144
+ program.setBaseColorMap(null, DEFAULT_UV_SCALE);
3145
+ program.setNormalMap(null, 1);
3146
+ program.setOrmMap(null, false, false);
3147
+ program.setSurface(0, 0.85, void 0);
3148
+ }
3149
+ /**
3150
+ * `?ao=<radius>,<strength>` 覆盖遮蔽参数(`ao=off` 关闭)。
3151
+ *
3152
+ * 屏幕空间遮蔽的合适参数与场景尺度强相关,靠改代码重编译来找太慢。这里只读一次
3153
+ * 页面地址,纯属调参通道,不参与渲染决策。
3154
+ */
3155
+ readOcclusionOverride() {
3156
+ if (typeof location === "undefined")
3157
+ return;
3158
+ const value = new URLSearchParams(location.search).get("ao");
3159
+ if (!value)
3160
+ return;
3161
+ if (value === "off") {
3162
+ this.occlusionStrength = 0;
3163
+ return;
3164
+ }
3165
+ const parts = value.split(",").map(Number);
3166
+ const radius = parts[0];
3167
+ const strength = parts[1];
3168
+ if (radius !== void 0 && Number.isFinite(radius) && radius > 0) {
3169
+ this.occlusionRadius = radius;
3170
+ }
3171
+ if (strength !== void 0 && Number.isFinite(strength) && strength >= 0) {
3172
+ this.occlusionStrength = strength;
3173
+ }
3174
+ }
3175
+ /**
3176
+ * `?shadows=1` / `?shadows=0` 覆盖阴影开关,与 `?ao` 同属调参通道。
3177
+ *
3178
+ * 阴影是一次额外全场景 pass,默认不开;演示与对比时靠地址栏切换,
3179
+ * 不必为看清"有影子/没影子"重编译一次。
3180
+ */
3181
+ readShadowOverride() {
3182
+ if (typeof location === "undefined")
3183
+ return;
3184
+ const params = new URLSearchParams(location.search);
3185
+ const value = params.get("shadows");
3186
+ if (value !== null) {
3187
+ this.shadowEnabled = value !== "0" && value !== "off";
3188
+ }
3189
+ const debug = Number(params.get("shadowdebug"));
3190
+ this.shadowDebug = Number.isFinite(debug) ? Math.max(0, Math.min(2, Math.trunc(debug))) : 0;
3191
+ if (this.shadowDebug > 0)
3192
+ this.shadowEnabled = true;
3193
+ }
3194
+ ensureTextureCache() {
3195
+ if (this.textures || !this.gl)
3196
+ return;
3197
+ this.textures = new WebGL2TextureCache(this.gl, void 0, () => {
3198
+ this.options?.onUploadReady?.();
3199
+ });
3200
+ }
1824
3201
  handleContextLost = (event) => {
1825
3202
  event.preventDefault();
1826
3203
  this.picker = null;
1827
3204
  this.program = null;
3205
+ this.sceneDepth?.dispose();
3206
+ this.sceneDepth = null;
3207
+ this.shadowMap?.dispose();
3208
+ this.shadowMap = null;
1828
3209
  this.geometryCache?.clear();
1829
3210
  this.geometryCache = null;
1830
3211
  this.instanceCache?.clear();
@@ -1843,6 +3224,7 @@ var WebGL2RendererBackend = class {
1843
3224
  this.onContextRestored?.();
1844
3225
  };
1845
3226
  initialize(options) {
3227
+ this.options = options;
1846
3228
  const gl = options.canvas.getContext("webgl2", {
1847
3229
  alpha: true,
1848
3230
  antialias: true,
@@ -1866,6 +3248,11 @@ var WebGL2RendererBackend = class {
1866
3248
  gl.enable(gl.POLYGON_OFFSET_FILL);
1867
3249
  gl.polygonOffset(2, 4);
1868
3250
  this.createProgram(gl);
3251
+ this.ensureTextureCache();
3252
+ this.sceneDepth = new WebGL2SceneDepth(gl);
3253
+ this.readOcclusionOverride();
3254
+ this.shadowEnabled = options.shadows ?? false;
3255
+ this.readShadowOverride();
1869
3256
  this.canvas.addEventListener("webglcontextlost", this.handleContextLost);
1870
3257
  this.canvas.addEventListener("webglcontextrestored", this.handleContextRestored);
1871
3258
  }
@@ -1888,9 +3275,21 @@ var WebGL2RendererBackend = class {
1888
3275
  }
1889
3276
  this.geometryCache.pin(this.gl, geometry);
1890
3277
  }
3278
+ prepareGeometry(geometry, update) {
3279
+ if (!this.gl || !this.geometryCache) {
3280
+ throw new ICEEngineError("ICE3_ENGINE_NOT_READY", "Renderer is not initialized");
3281
+ }
3282
+ this.geometryCache.pin(this.gl, geometry, update);
3283
+ }
3284
+ releaseGeometry(geometry) {
3285
+ this.geometryCache?.unpin(geometry);
3286
+ }
1891
3287
  releaseBatchGeometry(geometry) {
1892
3288
  this.geometryCache?.unpin(geometry);
1893
3289
  }
3290
+ setClipPlane(plane) {
3291
+ this.clipPlane = plane ? [plane[0], plane[1], plane[2], plane[3]] : [0, 0, 0, 0];
3292
+ }
1894
3293
  render(frame) {
1895
3294
  if (!this.gl || !this.program || !this.geometryCache) {
1896
3295
  throw new ICEEngineError("ICE3_ENGINE_NOT_READY", "Renderer is not initialized");
@@ -1913,7 +3312,12 @@ var WebGL2RendererBackend = class {
1913
3312
  gpuBytes: 0
1914
3313
  };
1915
3314
  this.program.use();
3315
+ this.program.setClipPlane(this.clipPlane);
1916
3316
  this.program.setClipOffset(0, 0);
3317
+ this.program.setDirectionalShading(false);
3318
+ this.program.setWaterShading(false);
3319
+ const cameraPosition = frame.camera.getPosition().toArray();
3320
+ this.program.setCameraPosition(cameraPosition);
1917
3321
  this.geometryCache.beginFrame();
1918
3322
  this.instanceCache.beginFrame();
1919
3323
  this.instancedMeshIds.clear();
@@ -1923,94 +3327,228 @@ var WebGL2RendererBackend = class {
1923
3327
  const renderables = collectRenderables(frame.scene, this.renderablesScratch);
1924
3328
  const instances = collectInstanceBatches(renderables, this.instancesScratch);
1925
3329
  const batches = collectRenderBatches(frame.scene, this.batchesScratch);
3330
+ const standalone = renderablesOutsideInstanceBatches(renderables, instances);
1926
3331
  this.profiler.mark("collect");
1927
- this.program.setInstancing(true);
1928
- this.profiler.mark("prepareAndUpload");
1929
- for (const batch of instances) {
1930
- const instance = this.instanceCache.retain(gl, this.geometryCache, batch);
1931
- if (!instance)
1932
- continue;
1933
- for (const id of batch.meshIds)
1934
- this.instancedMeshIds.add(id);
1935
- mvp.elements.set(viewProjection.elements);
1936
- this.program.setMatrix(mvp);
1937
- this.program.setColor(colorToRgba(batch.material.color));
1938
- this.program.setVertexColors(Boolean(batch.geometry.colors));
1939
- gl.bindVertexArray(instance.vertexArray);
1940
- gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, instance.triangleIndexBuffer);
1941
- gl.drawElementsInstanced(gl.TRIANGLES, instance.triangleCount, batch.geometry.indices instanceof Uint16Array ? gl.UNSIGNED_SHORT : gl.UNSIGNED_INT, 0, instance.instanceCount);
1942
- this.frameStats.drawCalls += 1;
1943
- if (batch.material.edgeColor && instance.edgeCount > 0) {
1944
- this.program.setVertexColors(false);
1945
- this.program.setColor(colorToRgba(batch.material.edgeColor));
1946
- gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, instance.edgeIndexBuffer);
1947
- for (const [offsetX, offsetY] of EDGE_PIXEL_OFFSETS) {
1948
- this.program.setClipOffset(offsetX * clipPixelX, offsetY * clipPixelY);
1949
- gl.drawElementsInstanced(gl.LINES, instance.edgeCount, batch.geometry.edgeIndices instanceof Uint16Array ? gl.UNSIGNED_SHORT : gl.UNSIGNED_INT, 0, instance.instanceCount);
1950
- this.frameStats.drawCalls += 1;
1951
- }
3332
+ let aoReady = false;
3333
+ if (this.sceneDepth) {
3334
+ const deviceWidth = Math.max(1, Math.round(frame.viewport.width * this.dpr));
3335
+ const deviceHeight = Math.max(1, Math.round(frame.viewport.height * this.dpr));
3336
+ this.sceneDepth.begin(viewProjection.elements, deviceWidth, deviceHeight);
3337
+ this.sceneDepth.setInstancing(true);
3338
+ for (const batch of instances) {
3339
+ if (isTransparentColor(batch.material.color))
3340
+ continue;
3341
+ const instance = this.instanceCache.retain(gl, this.geometryCache, batch);
3342
+ if (!instance)
3343
+ continue;
3344
+ gl.bindVertexArray(instance.vertexArray);
3345
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, instance.triangleIndexBuffer);
3346
+ gl.drawElementsInstanced(gl.TRIANGLES, instance.triangleCount, batch.geometry.indices instanceof Uint16Array ? gl.UNSIGNED_SHORT : gl.UNSIGNED_INT, 0, instance.instanceCount);
1952
3347
  }
3348
+ this.sceneDepth.setInstancing(false);
3349
+ for (const draw of batches) {
3350
+ if (isTransparentColor(draw.batch.material.color))
3351
+ continue;
3352
+ world.elements.set(draw.worldMatrix);
3353
+ mvp.elements.set(viewProjection.elements);
3354
+ mvp.multiply(world);
3355
+ this.sceneDepth.setMatrix(mvp.elements);
3356
+ const geometry = this.geometryCache.retain(gl, draw.batch.geometry);
3357
+ gl.bindVertexArray(geometry.vertexArray);
3358
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, geometry.triangleIndexBuffer);
3359
+ gl.drawElements(gl.TRIANGLES, geometry.triangleCount, draw.batch.geometry.indices instanceof Uint16Array ? gl.UNSIGNED_SHORT : gl.UNSIGNED_INT, 0);
3360
+ }
3361
+ for (const renderable of standalone) {
3362
+ if (isTransparentColor(renderable.mesh.material.color))
3363
+ continue;
3364
+ world.elements.set(renderable.worldMatrix);
3365
+ mvp.elements.set(viewProjection.elements);
3366
+ mvp.multiply(world);
3367
+ this.sceneDepth.setMatrix(mvp.elements);
3368
+ const geometry = this.geometryCache.retain(gl, renderable.mesh.geometry);
3369
+ gl.bindVertexArray(geometry.vertexArray);
3370
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, geometry.triangleIndexBuffer);
3371
+ gl.drawElements(gl.TRIANGLES, geometry.triangleCount, renderable.mesh.geometry.indices instanceof Uint16Array ? gl.UNSIGNED_SHORT : gl.UNSIGNED_INT, 0);
3372
+ }
3373
+ this.sceneDepth.end(deviceWidth, deviceHeight);
3374
+ aoReady = true;
3375
+ this.program.use();
1953
3376
  }
1954
- this.program.setInstancing(false);
1955
- this.program.setClipOffset(0, 0);
1956
- for (const draw of batches) {
1957
- this.program.setClipOffset(0, 0);
1958
- mvp.elements.set(viewProjection.elements);
1959
- world.elements.set(draw.worldMatrix);
1960
- mvp.multiply(world);
1961
- this.program.setMatrix(mvp);
1962
- this.program.setColor(colorToRgba(draw.batch.material.color));
1963
- const geometry = this.geometryCache.retain(gl, draw.batch.geometry);
1964
- this.program.setVertexColors(Boolean(draw.batch.geometry.colors));
1965
- gl.bindVertexArray(geometry.vertexArray);
1966
- gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, geometry.triangleIndexBuffer);
1967
- gl.drawElements(gl.TRIANGLES, geometry.triangleCount, draw.batch.geometry.indices instanceof Uint16Array ? gl.UNSIGNED_SHORT : gl.UNSIGNED_INT, 0);
1968
- this.frameStats.drawCalls += 1;
1969
- this.frameStats.renderedBatches += 1;
1970
- if (draw.batch.material.edgeColor) {
1971
- this.program.setVertexColors(false);
1972
- this.program.setColor(colorToRgba(draw.batch.material.edgeColor));
1973
- gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, geometry.edgeIndexBuffer);
1974
- for (const [offsetX, offsetY] of EDGE_PIXEL_OFFSETS) {
1975
- this.program.setClipOffset(offsetX * clipPixelX, offsetY * clipPixelY);
1976
- gl.drawElements(gl.LINES, geometry.edgeCount, draw.batch.geometry.edgeIndices instanceof Uint16Array ? gl.UNSIGNED_SHORT : gl.UNSIGNED_INT, 0);
1977
- this.frameStats.drawCalls += 1;
1978
- }
3377
+ if (this.shadowEnabled && this.shadowMap) {
3378
+ this.shadowMap.setBounds(accumulateSceneBounds(renderables));
3379
+ }
3380
+ if (this.shadowEnabled && this.shadowMap?.begin()) {
3381
+ const lightViewProjection = this.shadowMap.matrix;
3382
+ for (const batch of instances) {
3383
+ if (isTransparentColor(batch.material.color))
3384
+ continue;
3385
+ const instance = this.instanceCache.retain(gl, this.geometryCache, batch);
3386
+ if (!instance)
3387
+ continue;
3388
+ gl.bindVertexArray(instance.vertexArray);
3389
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, instance.triangleIndexBuffer);
3390
+ gl.drawElementsInstanced(gl.TRIANGLES, instance.triangleCount, batch.geometry.indices instanceof Uint16Array ? gl.UNSIGNED_SHORT : gl.UNSIGNED_INT, 0, instance.instanceCount);
3391
+ }
3392
+ this.shadowMap.setInstancing(false);
3393
+ for (const draw of batches) {
3394
+ if (isTransparentColor(draw.batch.material.color))
3395
+ continue;
3396
+ world.elements.set(draw.worldMatrix);
3397
+ mvp.elements.set(lightViewProjection);
3398
+ mvp.multiply(world);
3399
+ this.shadowMap.setMatrix(mvp.elements);
3400
+ const geometry = this.geometryCache.retain(gl, draw.batch.geometry);
3401
+ gl.bindVertexArray(geometry.vertexArray);
3402
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, geometry.triangleIndexBuffer);
3403
+ gl.drawElements(gl.TRIANGLES, geometry.triangleCount, draw.batch.geometry.indices instanceof Uint16Array ? gl.UNSIGNED_SHORT : gl.UNSIGNED_INT, 0);
3404
+ }
3405
+ for (const renderable of standalone) {
3406
+ if (isTransparentColor(renderable.mesh.material.color))
3407
+ continue;
3408
+ world.elements.set(renderable.worldMatrix);
3409
+ mvp.elements.set(lightViewProjection);
3410
+ mvp.multiply(world);
3411
+ this.shadowMap.setMatrix(mvp.elements);
3412
+ const geometry = this.geometryCache.retain(gl, renderable.mesh.geometry);
3413
+ gl.bindVertexArray(geometry.vertexArray);
3414
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, geometry.triangleIndexBuffer);
3415
+ gl.drawElements(gl.TRIANGLES, geometry.triangleCount, renderable.mesh.geometry.indices instanceof Uint16Array ? gl.UNSIGNED_SHORT : gl.UNSIGNED_INT, 0);
1979
3416
  }
3417
+ this.shadowMap.end(frame.viewport.width * this.dpr, frame.viewport.height * this.dpr);
3418
+ this.program.use();
1980
3419
  }
1981
- for (const renderable of renderables) {
1982
- if (this.instancedMeshIds.has(renderable.id))
1983
- continue;
3420
+ if (this.shadowMap) {
3421
+ this.program.use();
3422
+ this.program.setShadow(
3423
+ this.shadowEnabled,
3424
+ this.shadowMap.texture,
3425
+ this.shadowMap.matrix,
3426
+ // 1.5 个纹素:够把平躺在光里的面抬离自身深度,又不会让影子明显离开物体。
3427
+ this.shadowMap.texelWorld * 1.5
3428
+ );
3429
+ this.program.setShadowDebug(this.shadowDebug);
3430
+ }
3431
+ this.program.setSceneDepth(aoReady ? this.sceneDepth.texture : null, frame.camera.getProjectionMatrix(aspect), frame.camera.near, frame.camera.far, Math.max(1, frame.viewport.width * this.dpr), Math.max(1, frame.viewport.height * this.dpr));
3432
+ this.program.setOcclusion(this.occlusionRadius, this.occlusionStrength);
3433
+ for (const transparentPass of [false, true]) {
3434
+ if (transparentPass) {
3435
+ gl.enable(gl.BLEND);
3436
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
3437
+ gl.depthMask(false);
3438
+ } else {
3439
+ gl.disable(gl.BLEND);
3440
+ gl.depthMask(true);
3441
+ }
3442
+ this.program.setInstancing(true);
3443
+ this.profiler.mark("prepareAndUpload");
3444
+ for (const batch of instances) {
3445
+ if (isTransparentColor(batch.material.color) !== transparentPass)
3446
+ continue;
3447
+ const instance = this.instanceCache.retain(gl, this.geometryCache, batch);
3448
+ if (!instance)
3449
+ continue;
3450
+ for (const id of batch.meshIds)
3451
+ this.instancedMeshIds.add(id);
3452
+ mvp.elements.set(viewProjection.elements);
3453
+ this.program.setMatrix(mvp);
3454
+ this.applyMaterial(batch.material);
3455
+ this.program.setVertexColors(Boolean(batch.geometry.colors));
3456
+ this.program.setDirectionalShading(batch.material.shading !== "flat");
3457
+ this.program.setWaterShading(batch.material.shading === "water");
3458
+ gl.bindVertexArray(instance.vertexArray);
3459
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, instance.triangleIndexBuffer);
3460
+ gl.drawElementsInstanced(gl.TRIANGLES, instance.triangleCount, batch.geometry.indices instanceof Uint16Array ? gl.UNSIGNED_SHORT : gl.UNSIGNED_INT, 0, instance.instanceCount);
3461
+ this.frameStats.drawCalls += 1;
3462
+ if (batch.material.edgeColor && instance.edgeCount > 0) {
3463
+ this.program.setVertexColors(false);
3464
+ this.program.setDirectionalShading(false);
3465
+ this.program.setWaterShading(false);
3466
+ this.applyEdgeMaterial(batch.material);
3467
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, instance.edgeIndexBuffer);
3468
+ for (const [offsetX, offsetY] of EDGE_PIXEL_OFFSETS) {
3469
+ this.program.setClipOffset(offsetX * clipPixelX, offsetY * clipPixelY);
3470
+ gl.drawElementsInstanced(gl.LINES, instance.edgeCount, batch.geometry.edgeIndices instanceof Uint16Array ? gl.UNSIGNED_SHORT : gl.UNSIGNED_INT, 0, instance.instanceCount);
3471
+ this.frameStats.drawCalls += 1;
3472
+ }
3473
+ }
3474
+ }
3475
+ this.program.setInstancing(false);
1984
3476
  this.program.setClipOffset(0, 0);
1985
- mvp.elements.set(viewProjection.elements);
1986
- world.elements.set(renderable.worldMatrix);
1987
- mvp.multiply(world);
1988
- this.program.setMatrix(mvp);
1989
- this.program.setColor(colorToRgba(renderable.mesh.material.color));
1990
- const geometry = this.geometryCache.retain(gl, renderable.mesh.geometry);
1991
- this.program.setVertexColors(Boolean(renderable.mesh.geometry.colors));
1992
- gl.bindVertexArray(geometry.vertexArray);
1993
- gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, geometry.triangleIndexBuffer);
1994
- gl.drawElements(gl.TRIANGLES, geometry.triangleCount, renderable.mesh.geometry.indices instanceof Uint16Array ? gl.UNSIGNED_SHORT : gl.UNSIGNED_INT, 0);
1995
- this.frameStats.drawCalls += 1;
1996
- if (renderable.mesh.material.edgeColor) {
1997
- this.program.setVertexColors(false);
1998
- this.program.setColor(colorToRgba(renderable.mesh.material.edgeColor));
1999
- gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, geometry.edgeIndexBuffer);
2000
- for (const [offsetX, offsetY] of EDGE_PIXEL_OFFSETS) {
2001
- this.program.setClipOffset(offsetX * clipPixelX, offsetY * clipPixelY);
2002
- gl.drawElements(gl.LINES, geometry.edgeCount, renderable.mesh.geometry.edgeIndices instanceof Uint16Array ? gl.UNSIGNED_SHORT : gl.UNSIGNED_INT, 0);
2003
- this.frameStats.drawCalls += 1;
3477
+ for (const draw of batches) {
3478
+ if (isTransparentColor(draw.batch.material.color) !== transparentPass)
3479
+ continue;
3480
+ this.program.setClipOffset(0, 0);
3481
+ mvp.elements.set(viewProjection.elements);
3482
+ world.elements.set(draw.worldMatrix);
3483
+ mvp.multiply(world);
3484
+ this.program.setMatrix(mvp);
3485
+ this.applyMaterial(draw.batch.material);
3486
+ const geometry = this.geometryCache.retain(gl, draw.batch.geometry);
3487
+ this.program.setVertexColors(Boolean(draw.batch.geometry.colors));
3488
+ this.program.setDirectionalShading(draw.batch.material.shading !== "flat");
3489
+ this.program.setWaterShading(draw.batch.material.shading === "water");
3490
+ gl.bindVertexArray(geometry.vertexArray);
3491
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, geometry.triangleIndexBuffer);
3492
+ gl.drawElements(gl.TRIANGLES, geometry.triangleCount, draw.batch.geometry.indices instanceof Uint16Array ? gl.UNSIGNED_SHORT : gl.UNSIGNED_INT, 0);
3493
+ this.frameStats.drawCalls += 1;
3494
+ this.frameStats.renderedBatches += 1;
3495
+ if (draw.batch.material.edgeColor) {
3496
+ this.program.setVertexColors(false);
3497
+ this.program.setDirectionalShading(false);
3498
+ this.program.setWaterShading(false);
3499
+ this.applyEdgeMaterial(draw.batch.material);
3500
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, geometry.edgeIndexBuffer);
3501
+ for (const [offsetX, offsetY] of EDGE_PIXEL_OFFSETS) {
3502
+ this.program.setClipOffset(offsetX * clipPixelX, offsetY * clipPixelY);
3503
+ gl.drawElements(gl.LINES, geometry.edgeCount, draw.batch.geometry.edgeIndices instanceof Uint16Array ? gl.UNSIGNED_SHORT : gl.UNSIGNED_INT, 0);
3504
+ this.frameStats.drawCalls += 1;
3505
+ }
3506
+ }
3507
+ }
3508
+ for (const renderable of renderables) {
3509
+ if (isTransparentColor(renderable.mesh.material.color) !== transparentPass)
3510
+ continue;
3511
+ if (this.instancedMeshIds.has(renderable.id))
3512
+ continue;
3513
+ this.program.setClipOffset(0, 0);
3514
+ mvp.elements.set(viewProjection.elements);
3515
+ world.elements.set(renderable.worldMatrix);
3516
+ mvp.multiply(world);
3517
+ this.program.setMatrix(mvp);
3518
+ this.applyMaterial(renderable.mesh.material);
3519
+ const geometry = this.geometryCache.retain(gl, renderable.mesh.geometry);
3520
+ this.program.setVertexColors(Boolean(renderable.mesh.geometry.colors));
3521
+ this.program.setDirectionalShading(renderable.mesh.material.shading !== "flat");
3522
+ this.program.setWaterShading(renderable.mesh.material.shading === "water");
3523
+ gl.bindVertexArray(geometry.vertexArray);
3524
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, geometry.triangleIndexBuffer);
3525
+ gl.drawElements(gl.TRIANGLES, geometry.triangleCount, renderable.mesh.geometry.indices instanceof Uint16Array ? gl.UNSIGNED_SHORT : gl.UNSIGNED_INT, 0);
3526
+ this.frameStats.drawCalls += 1;
3527
+ if (renderable.mesh.material.edgeColor) {
3528
+ this.program.setVertexColors(false);
3529
+ this.program.setDirectionalShading(false);
3530
+ this.program.setWaterShading(false);
3531
+ this.applyEdgeMaterial(renderable.mesh.material);
3532
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, geometry.edgeIndexBuffer);
3533
+ for (const [offsetX, offsetY] of EDGE_PIXEL_OFFSETS) {
3534
+ this.program.setClipOffset(offsetX * clipPixelX, offsetY * clipPixelY);
3535
+ gl.drawElements(gl.LINES, geometry.edgeCount, renderable.mesh.geometry.edgeIndices instanceof Uint16Array ? gl.UNSIGNED_SHORT : gl.UNSIGNED_INT, 0);
3536
+ this.frameStats.drawCalls += 1;
3537
+ }
2004
3538
  }
3539
+ this.frameStats.renderedMeshes += 1;
2005
3540
  }
2006
- this.frameStats.renderedMeshes += 1;
2007
3541
  }
3542
+ gl.depthMask(true);
3543
+ gl.disable(gl.BLEND);
2008
3544
  if (frame.selection.length > 0) {
2009
3545
  const selected = new Set(frame.selection);
2010
3546
  gl.enable(gl.BLEND);
2011
3547
  gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
2012
3548
  gl.depthMask(false);
3549
+ gl.polygonOffset(-1, -4);
2013
3550
  this.program.setVertexColors(false);
3551
+ this.program.setDirectionalShading(false);
2014
3552
  this.program.setColor([
2015
3553
  SELECTION_RGBA[0],
2016
3554
  SELECTION_RGBA[1],
@@ -2056,6 +3594,7 @@ var WebGL2RendererBackend = class {
2056
3594
  if (this.semanticProgram) {
2057
3595
  this.semanticProgram.use();
2058
3596
  this.semanticProgram.setInstancing(false);
3597
+ gl.polygonOffset(-1, -4);
2059
3598
  for (const draw of batches) {
2060
3599
  for (let semanticIndex = 0; semanticIndex < draw.batch.semanticSelectionIds.length; semanticIndex += 1) {
2061
3600
  if (!selected.has(draw.batch.semanticSelectionIds[semanticIndex])) {
@@ -2073,6 +3612,7 @@ var WebGL2RendererBackend = class {
2073
3612
  }
2074
3613
  }
2075
3614
  this.semanticProgram.setInstancing(true);
3615
+ gl.polygonOffset(-1, -4);
2076
3616
  mvp.elements.set(viewProjection.elements);
2077
3617
  this.semanticProgram.setMatrix(mvp);
2078
3618
  for (const batch of instances) {
@@ -2089,6 +3629,7 @@ var WebGL2RendererBackend = class {
2089
3629
  }
2090
3630
  }
2091
3631
  gl.depthMask(true);
3632
+ gl.polygonOffset(2, 4);
2092
3633
  gl.disable(gl.BLEND);
2093
3634
  this.program.use();
2094
3635
  this.program.setClipOffset(0, 0);
@@ -2173,6 +3714,7 @@ var WebGL2RendererBackend = class {
2173
3714
  this.geometryCache = new WebGL2GeometryCache();
2174
3715
  this.instanceCache = new WebGL2InstanceCache();
2175
3716
  this.picker = new WebGL2Picker(gl);
3717
+ this.shadowMap = new WebGL2ShadowMap(gl);
2176
3718
  }
2177
3719
  };
2178
3720
 
@@ -2310,8 +3852,8 @@ async function executeProbe(gpu, options) {
2310
3852
  primitive: { topology: "triangle-list" }
2311
3853
  });
2312
3854
  const texture = context.getCurrentTexture();
2313
- const encoder = device.createCommandEncoder();
2314
- const pass = encoder.beginRenderPass({
3855
+ const encoder2 = device.createCommandEncoder();
3856
+ const pass = encoder2.beginRenderPass({
2315
3857
  colorAttachments: [{
2316
3858
  view: texture.createView(),
2317
3859
  clearValue: { r: 0, g: 0, b: 0, a: 1 },
@@ -2322,7 +3864,7 @@ async function executeProbe(gpu, options) {
2322
3864
  pass.setPipeline(pipeline);
2323
3865
  pass.draw(3);
2324
3866
  pass.end();
2325
- device.queue.submit([encoder.finish()]);
3867
+ device.queue.submit([encoder2.finish()]);
2326
3868
  const lost = await Promise.race([
2327
3869
  device.lost.then((info) => info.message || info.reason),
2328
3870
  new Promise((resolve) => {
@@ -2428,9 +3970,13 @@ var WebGPUGeometryCache = class {
2428
3970
  entries = /* @__PURE__ */ new Map();
2429
3971
  active = /* @__PURE__ */ new Set();
2430
3972
  pinned = /* @__PURE__ */ new Set();
3973
+ objectKeys = /* @__PURE__ */ new WeakMap();
3974
+ legacyKeys = /* @__PURE__ */ new WeakMap();
3975
+ objectKeySequence = 0;
2431
3976
  inactiveGraceFrames;
2432
3977
  maxBytes;
2433
3978
  uploads = 0;
3979
+ updatedBytes = 0;
2434
3980
  evictions = 0;
2435
3981
  frameId = 0;
2436
3982
  resourceGeneration = 0;
@@ -2443,12 +3989,23 @@ var WebGPUGeometryCache = class {
2443
3989
  }
2444
3990
  }
2445
3991
  retain(geometry) {
2446
- this.active.add(geometry);
2447
- const existing = this.entries.get(geometry);
3992
+ const key = this.keyFor(geometry);
3993
+ this.active.add(key);
3994
+ const existing = this.entries.get(key);
2448
3995
  if (existing) {
2449
- existing.lastUsedFrame = this.frameId;
2450
- return existing;
3996
+ if (this.canReuse(existing, geometry)) {
3997
+ existing.lastUsedFrame = this.frameId;
3998
+ return existing;
3999
+ }
4000
+ }
4001
+ const next = this.createEntry(geometry, key);
4002
+ if (existing) {
4003
+ this.deleteEntry(existing, false);
2451
4004
  }
4005
+ this.entries.set(key, next);
4006
+ return next;
4007
+ }
4008
+ createEntry(geometry, key) {
2452
4009
  const buffers = [];
2453
4010
  try {
2454
4011
  const vertexCount = geometry.positions.length / 3;
@@ -2473,6 +4030,10 @@ var WebGPUGeometryCache = class {
2473
4030
  if (semanticBuffer)
2474
4031
  buffers.push(semanticBuffer);
2475
4032
  const entry = {
4033
+ geometry,
4034
+ geometryId: geometry.identity?.geometryId ?? key,
4035
+ attributeDigest: geometry.identity?.attributeDigest ?? "",
4036
+ topologyDigest: geometry.identity?.topologyDigest ?? "",
2476
4037
  vertexBuffer,
2477
4038
  triangleIndexBuffer,
2478
4039
  edgeIndexBuffer,
@@ -2484,7 +4045,6 @@ var WebGPUGeometryCache = class {
2484
4045
  bytes: buffers.reduce((sum, buffer) => sum + buffer.size, 0),
2485
4046
  lastUsedFrame: this.frameId
2486
4047
  };
2487
- this.entries.set(geometry, entry);
2488
4048
  this.uploads += buffers.length;
2489
4049
  this.resourceGeneration += 1;
2490
4050
  return entry;
@@ -2496,19 +4056,25 @@ var WebGPUGeometryCache = class {
2496
4056
  throw new ICEEngineError("ICE3_GPU_RESOURCE_FAILED", "ICE3_GPU_RESOURCE_FAILED: geometry upload failed", { cause: error });
2497
4057
  }
2498
4058
  }
2499
- pin(geometry) {
2500
- this.pinned.add(geometry);
4059
+ pin(geometry, update) {
4060
+ const key = this.keyFor(geometry);
4061
+ this.pinned.add(key);
4062
+ const existing = this.entries.get(key);
4063
+ if (existing && update && this.sameTopology(existing, geometry)) {
4064
+ this.applyPositionUpdate(existing, geometry, update);
4065
+ return existing;
4066
+ }
2501
4067
  const entry = this.retain(geometry);
2502
4068
  this.enforceBudget();
2503
4069
  if (this.totalBytes > this.maxBytes) {
2504
- this.pinned.delete(geometry);
2505
- this.deleteEntry(geometry, entry);
4070
+ this.pinned.delete(key);
4071
+ this.deleteEntry(entry);
2506
4072
  throw new ICEEngineError("ICE3_GPU_RESOURCE_FAILED", "ICE3_GPU_RESOURCE_FAILED: geometry exceeds the GPU residency budget");
2507
4073
  }
2508
4074
  return entry;
2509
4075
  }
2510
4076
  unpin(geometry) {
2511
- this.pinned.delete(geometry);
4077
+ this.pinned.delete(this.keyFor(geometry));
2512
4078
  }
2513
4079
  beginFrame() {
2514
4080
  this.frameId += 1;
@@ -2521,13 +4087,13 @@ var WebGPUGeometryCache = class {
2521
4087
  if (this.frameId - entry.lastUsedFrame < this.inactiveGraceFrames) {
2522
4088
  continue;
2523
4089
  }
2524
- this.deleteEntry(geometry, entry);
4090
+ this.deleteEntry(entry);
2525
4091
  }
2526
4092
  this.enforceBudget();
2527
4093
  }
2528
4094
  clear() {
2529
4095
  for (const [geometry, entry] of this.entries) {
2530
- this.deleteEntry(geometry, entry);
4096
+ this.deleteEntry(entry);
2531
4097
  }
2532
4098
  this.active.clear();
2533
4099
  this.pinned.clear();
@@ -2541,6 +4107,9 @@ var WebGPUGeometryCache = class {
2541
4107
  get uploadCount() {
2542
4108
  return this.uploads;
2543
4109
  }
4110
+ get updateBytes() {
4111
+ return this.updatedBytes;
4112
+ }
2544
4113
  get evictionCount() {
2545
4114
  return this.evictions;
2546
4115
  }
@@ -2555,20 +4124,120 @@ var WebGPUGeometryCache = class {
2555
4124
  for (const [geometry, entry] of candidates) {
2556
4125
  if (total <= this.maxBytes)
2557
4126
  break;
2558
- this.deleteEntry(geometry, entry);
4127
+ this.deleteEntry(entry);
2559
4128
  total -= entry.bytes;
2560
4129
  }
2561
4130
  }
2562
- deleteEntry(geometry, entry) {
4131
+ deleteEntry(entry, eviction = true) {
2563
4132
  entry.vertexBuffer.destroy();
2564
4133
  entry.triangleIndexBuffer.destroy();
2565
4134
  entry.edgeIndexBuffer?.destroy();
2566
4135
  entry.semanticBuffer?.destroy();
2567
- this.entries.delete(geometry);
2568
- this.evictions += 1;
4136
+ for (const [key, candidate] of this.entries) {
4137
+ if (candidate === entry) {
4138
+ this.entries.delete(key);
4139
+ break;
4140
+ }
4141
+ }
4142
+ if (eviction) {
4143
+ this.evictions += 1;
4144
+ }
2569
4145
  this.resourceGeneration += 1;
2570
4146
  }
4147
+ applyPositionUpdate(entry, geometry, update) {
4148
+ let updated = 0;
4149
+ try {
4150
+ for (const range of update.positionRanges) {
4151
+ if (range.byteOffset % 12 !== 0 || range.byteLength % 12 !== 0 || range.byteOffset + range.byteLength > geometry.positions.byteLength) {
4152
+ throw new Error("invalid position range");
4153
+ }
4154
+ const vertexOffset = range.byteOffset / 12;
4155
+ const vertexCount = range.byteLength / 12;
4156
+ const vertices = new Float32Array(vertexCount * 6);
4157
+ const totalVertices = geometry.positions.length / 3;
4158
+ const colorStride = geometry.colors ? geometry.colors.length / totalVertices : 0;
4159
+ for (let index = 0; index < vertexCount; index += 1) {
4160
+ const sourceVertex = vertexOffset + index;
4161
+ vertices[index * 6] = geometry.positions[sourceVertex * 3];
4162
+ vertices[index * 6 + 1] = geometry.positions[sourceVertex * 3 + 1];
4163
+ vertices[index * 6 + 2] = geometry.positions[sourceVertex * 3 + 2];
4164
+ vertices[index * 6 + 3] = geometry.colors?.[sourceVertex * colorStride] ?? 1;
4165
+ vertices[index * 6 + 4] = geometry.colors?.[sourceVertex * colorStride + 1] ?? 1;
4166
+ vertices[index * 6 + 5] = geometry.colors?.[sourceVertex * colorStride + 2] ?? 1;
4167
+ }
4168
+ this.device.queue.writeBuffer(entry.vertexBuffer, range.byteOffset * 2, vertices);
4169
+ updated += range.byteLength;
4170
+ }
4171
+ } catch (error) {
4172
+ throw new ICEEngineError("ICE3_GPU_RESOURCE_FAILED", "ICE3_GPU_RESOURCE_FAILED: geometry update failed", { cause: error });
4173
+ }
4174
+ entry.geometry = geometry;
4175
+ entry.lastUsedFrame = this.frameId;
4176
+ this.updatedBytes += updated;
4177
+ }
4178
+ canReuse(existing, geometry) {
4179
+ if (existing.geometry === geometry) {
4180
+ return true;
4181
+ }
4182
+ if (!this.sameTopology(existing, geometry)) {
4183
+ return false;
4184
+ }
4185
+ return (existing.geometry.positions === geometry.positions || equalNumberArrays2(existing.geometry.positions, geometry.positions)) && equalOptionalNumberArrays2(existing.geometry.colors, geometry.colors) && equalOptionalNumberArrays2(existing.geometry.semanticIndices, geometry.semanticIndices) && equalOptionalNumberArrays2(existing.geometry.edgeIndices, geometry.edgeIndices) || existing.attributeDigest !== "" && existing.attributeDigest === geometry.identity?.attributeDigest;
4186
+ }
4187
+ sameTopology(existing, geometry) {
4188
+ return existing.geometry.indices.constructor === geometry.indices.constructor && existing.geometry.indices.length === geometry.indices.length && (existing.geometry.indices === geometry.indices || existing.topologyDigest !== "" && existing.topologyDigest === geometry.identity?.topologyDigest);
4189
+ }
4190
+ keyFor(geometry) {
4191
+ const identity = geometry.identity?.geometryId;
4192
+ if (identity) {
4193
+ return `identity:${identity}`;
4194
+ }
4195
+ const id = geometry.id;
4196
+ if (typeof id === "string" && id.trim()) {
4197
+ const cached = this.legacyKeys.get(geometry);
4198
+ if (cached)
4199
+ return cached;
4200
+ const key2 = [
4201
+ "content",
4202
+ geometry.positions.length,
4203
+ geometry.indices.length,
4204
+ contentFingerprint2(geometry.positions),
4205
+ contentFingerprint2(geometry.indices),
4206
+ geometry.colors ? contentFingerprint2(geometry.colors) : 0,
4207
+ geometry.edgeIndices ? contentFingerprint2(geometry.edgeIndices) : 0,
4208
+ geometry.semanticIndices ? contentFingerprint2(geometry.semanticIndices) : 0
4209
+ ].join(":");
4210
+ this.legacyKeys.set(geometry, key2);
4211
+ return key2;
4212
+ }
4213
+ const existing = this.objectKeys.get(geometry);
4214
+ if (existing) {
4215
+ return existing;
4216
+ }
4217
+ const key = `object:${this.objectKeySequence}`;
4218
+ this.objectKeySequence += 1;
4219
+ this.objectKeys.set(geometry, key);
4220
+ return key;
4221
+ }
2571
4222
  };
4223
+ function contentFingerprint2(values) {
4224
+ let hash = 2166136261;
4225
+ const bytes = new Uint8Array(values.buffer, values.byteOffset, values.byteLength);
4226
+ for (const byte of bytes) {
4227
+ hash ^= byte;
4228
+ hash = Math.imul(hash, 16777619) >>> 0;
4229
+ }
4230
+ return hash;
4231
+ }
4232
+ function equalNumberArrays2(left, right) {
4233
+ return left.length === right.length && left.every((value, index) => value === right[index]);
4234
+ }
4235
+ function equalOptionalNumberArrays2(left, right) {
4236
+ if (left === void 0 || right === void 0) {
4237
+ return left === right;
4238
+ }
4239
+ return left.length === right.length && left.every((value, index) => value === right[index]);
4240
+ }
2572
4241
 
2573
4242
  // packages/renderer/dist/WebGPUInstanceCache.js
2574
4243
  var WebGPUInstanceCache = class {
@@ -2664,7 +4333,7 @@ function sameBytes(left, right) {
2664
4333
 
2665
4334
  // packages/renderer/dist/WebGPURenderPipeline.js
2666
4335
  var WEBGPU_UNIFORM_STRIDE = 256;
2667
- var WEBGPU_UNIFORM_BYTES = 128;
4336
+ var WEBGPU_UNIFORM_BYTES = 144;
2668
4337
  var SHADER_SOURCE = (selectionMask, storageInstances, indirectInstances) => `
2669
4338
  struct FrameUniforms {
2670
4339
  worldViewProjection: mat4x4<f32>,
@@ -2680,6 +4349,7 @@ struct FrameUniforms {
2680
4349
  useSelectionMask: f32,
2681
4350
  pageBase: f32,
2682
4351
  useIndirect: f32,
4352
+ useLighting: f32,
2683
4353
  }
2684
4354
 
2685
4355
  ${storageInstances ? `
@@ -2696,6 +4366,7 @@ struct VertexOutput {
2696
4366
  @builtin(position) position: vec4<f32>,
2697
4367
  @location(0) color: vec4<f32>,
2698
4368
  @location(1) @interpolate(flat) semantic: u32,
4369
+ @location(2) worldPosition: vec4<f32>,
2699
4370
  }
2700
4371
 
2701
4372
  @group(0) @binding(0) var<uniform> frame: FrameUniforms;
@@ -2748,6 +4419,7 @@ fn vertexMain(
2748
4419
  mix(vec3<f32>(1.0), color, frame.useVertexColor) * frame.color.rgb,
2749
4420
  frame.color.a
2750
4421
  );
4422
+ output.worldPosition = worldPosition;
2751
4423
  output.semantic = select(
2752
4424
  vertexSemantic,
2753
4425
  instanceSemantic,
@@ -2791,7 +4463,24 @@ fn fragmentMain(input: VertexOutput) -> @location(0) vec4<f32> {
2791
4463
  );
2792
4464
  }
2793
4465
  }` : ""}
2794
- return input.color;
4466
+ var outputColor = input.color;
4467
+ if (frame.useLighting > 0.5) {
4468
+ let normal = normalize(cross(
4469
+ dpdx(input.worldPosition.xyz),
4470
+ dpdy(input.worldPosition.xyz)
4471
+ ));
4472
+ let lightDirection = normalize(vec3<f32>(-0.35, 0.85, 0.4));
4473
+ let diffuse = dot(normal, lightDirection);
4474
+ let key = max(diffuse, 0.0) + 0.35 * max(-diffuse, 0.0);
4475
+ let sky = 0.5 + 0.5 * normal.y;
4476
+ let shade = clamp(
4477
+ 0.72 + 0.72 * key + 0.18 * sky,
4478
+ 0.92,
4479
+ 1.42
4480
+ );
4481
+ outputColor = vec4<f32>(outputColor.rgb * shade, outputColor.a);
4482
+ }
4483
+ return outputColor;
2795
4484
  }
2796
4485
  `;
2797
4486
  var VERTEX_LAYOUT = [
@@ -2946,7 +4635,14 @@ var WebGPURenderPipeline = class _WebGPURenderPipeline {
2946
4635
  depthStencil: {
2947
4636
  format: "depth24plus",
2948
4637
  depthCompare: "less-equal",
2949
- depthWriteEnabled: !selection
4638
+ depthWriteEnabled: !selection,
4639
+ // Selected triangles are redrawn after opaque geometry. Pull that
4640
+ // overlay slightly forward instead of competing at an equal depth.
4641
+ ...selection && topology === "triangle-list" ? {
4642
+ depthBias: -1,
4643
+ depthBiasSlopeScale: -1,
4644
+ depthBiasClamp: 0
4645
+ } : {}
2950
4646
  },
2951
4647
  multisample: { count: 1 }
2952
4648
  });
@@ -2995,12 +4691,12 @@ var WebGPUTimestampProfiler = class {
2995
4691
  querySetFor(frameId) {
2996
4692
  return this.slotFor(frameId).querySet;
2997
4693
  }
2998
- resolveFrame(encoder, frameId) {
4694
+ resolveFrame(encoder2, frameId) {
2999
4695
  const slot = this.slotFor(frameId);
3000
4696
  if (slot.pending) {
3001
4697
  throw new Error("ICE3_TIMESTAMP_SLOT_BUSY");
3002
4698
  }
3003
- encoder.resolveQuerySet(slot.querySet, 0, 2, slot.buffer, 0);
4699
+ encoder2.resolveQuerySet(slot.querySet, 0, 2, slot.buffer, 0);
3004
4700
  this.pending.push(this.slotIndex(frameId));
3005
4701
  slot.pending = true;
3006
4702
  }
@@ -3168,6 +4864,7 @@ var WebGPUDrawStorage = class {
3168
4864
  this.uniformBuffer[offset + 29] = draw.useSelectionMask ? 1 : 0;
3169
4865
  this.uniformBuffer[offset + 30] = draw.pageBase ?? 0;
3170
4866
  this.uniformBuffer[offset + 31] = draw.useIndirect ? 1 : 0;
4867
+ this.uniformBuffer[offset + 32] = draw.useLighting ? 1 : 0;
3171
4868
  this.count += 1;
3172
4869
  return index;
3173
4870
  }
@@ -3252,13 +4949,13 @@ var WebGPUBundleCache = class {
3252
4949
  get(key, record3) {
3253
4950
  if (this.key === key && this.bundle)
3254
4951
  return this.bundle;
3255
- const encoder = this.device.createRenderBundleEncoder({
4952
+ const encoder2 = this.device.createRenderBundleEncoder({
3256
4953
  label: `ice3:bundle:${key}`,
3257
4954
  colorFormats: [this.format],
3258
4955
  depthStencilFormat: "depth24plus"
3259
4956
  });
3260
- record3(encoder);
3261
- const bundle = encoder.finish();
4957
+ record3(encoder2);
4958
+ const bundle = encoder2.finish();
3262
4959
  this.key = key;
3263
4960
  this.bundle = bundle;
3264
4961
  this.recreations += 1;
@@ -3596,8 +5293,8 @@ var WebGPUVisibilityEncoder = class _WebGPUVisibilityEncoder {
3596
5293
  this.entries.set(page.buffer, entries);
3597
5294
  return entry;
3598
5295
  }
3599
- encode(encoder, entry, page, uniformBuffer, uniformOffset) {
3600
- encoder.clearBuffer(entry.indirectBuffer, 4, 4);
5296
+ encode(encoder2, entry, page, uniformBuffer, uniformOffset) {
5297
+ encoder2.clearBuffer(entry.indirectBuffer, 4, 4);
3601
5298
  const bindGroup = this.device.createBindGroup({
3602
5299
  label: "ice3:visibility-bindings",
3603
5300
  layout: this.bindGroupLayout,
@@ -3628,7 +5325,7 @@ var WebGPUVisibilityEncoder = class _WebGPUVisibilityEncoder {
3628
5325
  }
3629
5326
  ]
3630
5327
  });
3631
- const pass = encoder.beginComputePass({ label: "ice3:visibility-pass" });
5328
+ const pass = encoder2.beginComputePass({ label: "ice3:visibility-pass" });
3632
5329
  pass.setPipeline(this.pipeline);
3633
5330
  pass.setBindGroup(0, bindGroup, [uniformOffset]);
3634
5331
  pass.dispatchWorkgroups(Math.ceil(page.instanceCount / 64));
@@ -3640,9 +5337,9 @@ var WebGPUVisibilityEncoder = class _WebGPUVisibilityEncoder {
3640
5337
  size: 4,
3641
5338
  usage: 1 | 8
3642
5339
  });
3643
- const encoder = this.device.createCommandEncoder();
3644
- encoder.copyBufferToBuffer(entry.indirectBuffer, 4, readback, 0, 4);
3645
- this.device.queue.submit([encoder.finish()]);
5340
+ const encoder2 = this.device.createCommandEncoder();
5341
+ encoder2.copyBufferToBuffer(entry.indirectBuffer, 4, readback, 0, 4);
5342
+ this.device.queue.submit([encoder2.finish()]);
3646
5343
  try {
3647
5344
  await readback.mapAsync(1);
3648
5345
  const visibleInstances = new Uint32Array(readback.getMappedRange())[0];
@@ -3815,8 +5512,8 @@ var WebGPUPicker = class {
3815
5512
  }
3816
5513
  }]
3817
5514
  });
3818
- const encoder = this.device.createCommandEncoder();
3819
- const pass = encoder.beginRenderPass({
5515
+ const encoder2 = this.device.createCommandEncoder();
5516
+ const pass = encoder2.beginRenderPass({
3820
5517
  colorAttachments: [{
3821
5518
  view: this.target.createView(),
3822
5519
  clearValue: { r: 0, g: 0, b: 0, a: 1 },
@@ -3848,7 +5545,7 @@ var WebGPUPicker = class {
3848
5545
  size: 256,
3849
5546
  usage: COPY_DST | MAP_READ
3850
5547
  });
3851
- encoder.copyTextureToBuffer({
5548
+ encoder2.copyTextureToBuffer({
3852
5549
  texture: this.target,
3853
5550
  origin: {
3854
5551
  x: pixelX,
@@ -3860,7 +5557,7 @@ var WebGPUPicker = class {
3860
5557
  bytesPerRow: 256,
3861
5558
  rowsPerImage: 1
3862
5559
  }, { width: 1, height: 1, depthOrArrayLayers: 1 });
3863
- this.device.queue.submit([encoder.finish()]);
5560
+ this.device.queue.submit([encoder2.finish()]);
3864
5561
  let pixel;
3865
5562
  try {
3866
5563
  await readback.mapAsync(MAP_READ);
@@ -4214,6 +5911,12 @@ var WebGPURendererBackend = class _WebGPURendererBackend {
4214
5911
  prepareBatchGeometry(geometry) {
4215
5912
  this.requireGeometryCache().pin(geometry);
4216
5913
  }
5914
+ prepareGeometry(geometry, update) {
5915
+ this.requireGeometryCache().pin(geometry, update);
5916
+ }
5917
+ releaseGeometry(geometry) {
5918
+ this.geometryCache?.unpin(geometry);
5919
+ }
4217
5920
  releaseBatchGeometry(geometry) {
4218
5921
  this.geometryCache?.unpin(geometry);
4219
5922
  }
@@ -4341,6 +6044,7 @@ var WebGPURendererBackend = class _WebGPURendererBackend {
4341
6044
  color: colorToRgba(batch.material.color),
4342
6045
  useVertexColor: Boolean(batch.geometry.colors),
4343
6046
  useInstancing: true,
6047
+ useLighting: batch.material.shading === "directional" || batch.material.shading === "water",
4344
6048
  pipeline: pagePipeline.triangle,
4345
6049
  useSelectionMask: selectionMaskEnabled,
4346
6050
  storagePage: page,
@@ -4368,6 +6072,7 @@ var WebGPURendererBackend = class _WebGPURendererBackend {
4368
6072
  color: colorToRgba(batch.material.color),
4369
6073
  useVertexColor: Boolean(batch.geometry.colors),
4370
6074
  useInstancing: true,
6075
+ useLighting: batch.material.shading === "directional" || batch.material.shading === "water",
4371
6076
  pipeline: this.pipeline.triangle,
4372
6077
  useSelectionMask: selectionMaskEnabled
4373
6078
  }, viewProjection.elements, batch.semanticIds);
@@ -4412,6 +6117,7 @@ var WebGPURendererBackend = class _WebGPURendererBackend {
4412
6117
  color: colorToRgba(draw.batch.material.color),
4413
6118
  useVertexColor: Boolean(draw.batch.geometry.colors),
4414
6119
  useInstancing: false,
6120
+ useLighting: draw.batch.material.shading === "directional" || draw.batch.material.shading === "water",
4415
6121
  pipeline: this.pipeline.triangle,
4416
6122
  useSelectionMask: selectionMaskEnabled
4417
6123
  }, matrix, draw.batch.semanticSelectionIds);
@@ -4459,6 +6165,7 @@ var WebGPURendererBackend = class _WebGPURendererBackend {
4459
6165
  color: colorToRgba(renderable.mesh.material.color),
4460
6166
  useVertexColor: Boolean(renderable.mesh.geometry.colors),
4461
6167
  useInstancing: false,
6168
+ useLighting: renderable.mesh.material.shading === "directional" || renderable.mesh.material.shading === "water",
4462
6169
  pipeline: this.pipeline.triangle,
4463
6170
  useSelectionMask: selectionMaskEnabled
4464
6171
  }, matrix, [renderable.mesh.selectionId]);
@@ -4527,26 +6234,26 @@ var WebGPURendererBackend = class _WebGPURendererBackend {
4527
6234
  this.frameBindGroupKey = bindGroupKey;
4528
6235
  }
4529
6236
  const bindGroup = this.frameBindGroup;
4530
- const encodeDraws = (encoder2) => {
6237
+ const encodeDraws = (encoder3) => {
4531
6238
  for (let index = 0; index < drawList.length; index += 1) {
4532
6239
  const draw = drawList[index];
4533
- encoder2.setPipeline(draw.pipeline);
6240
+ encoder3.setPipeline(draw.pipeline);
4534
6241
  const drawBindGroup = draw.storagePage ? this.storageBindGroupFor(draw.storagePage, draw.visibilityEntry?.visibleBuffer) : bindGroup;
4535
- encoder2.setBindGroup(0, drawBindGroup, [
6242
+ encoder3.setBindGroup(0, drawBindGroup, [
4536
6243
  index * WEBGPU_UNIFORM_STRIDE
4537
6244
  ]);
4538
- encoder2.setVertexBuffer(0, draw.geometry.vertexBuffer);
6245
+ encoder3.setVertexBuffer(0, draw.geometry.vertexBuffer);
4539
6246
  if (draw.storagePage) {
4540
- encoder2.setVertexBuffer(1, draw.semanticBuffer);
6247
+ encoder3.setVertexBuffer(1, draw.semanticBuffer);
4541
6248
  } else {
4542
- encoder2.setVertexBuffer(1, draw.instanceBuffer);
4543
- encoder2.setVertexBuffer(2, draw.semanticBuffer);
6249
+ encoder3.setVertexBuffer(1, draw.instanceBuffer);
6250
+ encoder3.setVertexBuffer(2, draw.semanticBuffer);
4544
6251
  }
4545
- encoder2.setIndexBuffer(draw.edge ? draw.geometry.edgeIndexBuffer : draw.geometry.triangleIndexBuffer, draw.geometry.indexFormat);
6252
+ encoder3.setIndexBuffer(draw.edge ? draw.geometry.edgeIndexBuffer : draw.geometry.triangleIndexBuffer, draw.geometry.indexFormat);
4546
6253
  if (draw.visibilityEntry) {
4547
- encoder2.drawIndexedIndirect(draw.visibilityEntry.indirectBuffer, 0);
6254
+ encoder3.drawIndexedIndirect(draw.visibilityEntry.indirectBuffer, 0);
4548
6255
  } else {
4549
- encoder2.drawIndexed(draw.edge ? draw.geometry.edgeCount : draw.geometry.triangleCount, draw.instanceCount, 0, 0, draw.storagePage?.firstInstance ?? 0);
6256
+ encoder3.drawIndexed(draw.edge ? draw.geometry.edgeCount : draw.geometry.triangleCount, draw.instanceCount, 0, 0, draw.storagePage?.firstInstance ?? 0);
4550
6257
  }
4551
6258
  }
4552
6259
  };
@@ -4572,7 +6279,7 @@ var WebGPURendererBackend = class _WebGPURendererBackend {
4572
6279
  }
4573
6280
  }
4574
6281
  const targetTexture = context.getCurrentTexture();
4575
- const encoder = this.device.createCommandEncoder();
6282
+ const encoder2 = this.device.createCommandEncoder();
4576
6283
  if (this.gpuDrivenActive) {
4577
6284
  const encodedVisibility = /* @__PURE__ */ new Set();
4578
6285
  for (let index = 0; index < drawList.length; index += 1) {
@@ -4581,12 +6288,12 @@ var WebGPURendererBackend = class _WebGPURendererBackend {
4581
6288
  continue;
4582
6289
  }
4583
6290
  encodedVisibility.add(draw.visibilityEntry);
4584
- this.visibilityEncoder.encode(encoder, draw.visibilityEntry, draw.storagePage, this.uniformBuffer, index * WEBGPU_UNIFORM_STRIDE);
6291
+ this.visibilityEncoder.encode(encoder2, draw.visibilityEntry, draw.storagePage, this.uniformBuffer, index * WEBGPU_UNIFORM_STRIDE);
4585
6292
  }
4586
6293
  }
4587
6294
  const timestampProfiler = this.captureRequest || !this.timestampProfilingEnabled ? null : this.timestampProfiler;
4588
6295
  const timestampQuerySet = timestampProfiler?.querySetFor(frame.frameId);
4589
- const pass = encoder.beginRenderPass({
6296
+ const pass = encoder2.beginRenderPass({
4590
6297
  colorAttachments: [{
4591
6298
  view: targetTexture.createView(),
4592
6299
  clearValue: {
@@ -4627,10 +6334,10 @@ var WebGPURendererBackend = class _WebGPURendererBackend {
4627
6334
  this.frameStats.drawCalls += drawList.length;
4628
6335
  pass.end();
4629
6336
  if (timestampProfiler) {
4630
- timestampProfiler.resolveFrame(encoder, frame.frameId);
6337
+ timestampProfiler.resolveFrame(encoder2, frame.frameId);
4631
6338
  }
4632
6339
  if (this.captureRequest) {
4633
- encoder.copyTextureToBuffer({ texture: targetTexture }, {
6340
+ encoder2.copyTextureToBuffer({ texture: targetTexture }, {
4634
6341
  buffer: this.captureRequest.buffer,
4635
6342
  bytesPerRow: this.captureRequest.bytesPerRow,
4636
6343
  rowsPerImage: this.canvas.height
@@ -4641,7 +6348,7 @@ var WebGPURendererBackend = class _WebGPURendererBackend {
4641
6348
  });
4642
6349
  this.captureRequest = null;
4643
6350
  }
4644
- const commandBuffer = encoder.finish();
6351
+ const commandBuffer = encoder2.finish();
4645
6352
  this.profiler.mark("encode");
4646
6353
  this.device.queue.submit([commandBuffer]);
4647
6354
  this.profiler.mark("submit");
@@ -5493,18 +7200,18 @@ var ICEInputRouter = class {
5493
7200
  function pickSceneNode(scene, camera, viewport, point) {
5494
7201
  const eye = camera.getPosition();
5495
7202
  const target = camera.getTarget();
5496
- const forward = normalize2([
7203
+ const forward = normalize3([
5497
7204
  target.x - eye.x,
5498
7205
  target.y - eye.y,
5499
7206
  target.z - eye.z
5500
7207
  ]);
5501
- const right = normalize2(cross2(forward, [0, 1, 0]));
5502
- const up = normalize2(cross2(right, forward));
7208
+ const right = normalize3(cross2(forward, [0, 1, 0]));
7209
+ const up = normalize3(cross2(right, forward));
5503
7210
  const aspect = viewport.width / Math.max(viewport.height, 1);
5504
7211
  const tangent = Math.tan(camera.fov * Math.PI / 360);
5505
7212
  const ndcX = point.x / viewport.width * 2 - 1;
5506
7213
  const ndcY = 1 - point.y / viewport.height * 2;
5507
- const direction = normalize2([
7214
+ const direction = normalize3([
5508
7215
  forward[0] + right[0] * ndcX * aspect * tangent + up[0] * ndcY * tangent,
5509
7216
  forward[1] + right[1] * ndcX * aspect * tangent + up[1] * ndcY * tangent,
5510
7217
  forward[2] + right[2] * ndcX * aspect * tangent + up[2] * ndcY * tangent
@@ -5537,7 +7244,7 @@ function pickSceneNode(scene, camera, viewport, point) {
5537
7244
  visit2(scene, true);
5538
7245
  return nearest;
5539
7246
  }
5540
- var normalize2 = (value) => {
7247
+ var normalize3 = (value) => {
5541
7248
  const length = Math.hypot(...value);
5542
7249
  return length > 0 ? [value[0] / length, value[1] / length, value[2] / length] : [0, 0, 0];
5543
7250
  };
@@ -5847,6 +7554,7 @@ var ICE3 = class extends ICEEventDispatcher {
5847
7554
  generatedOverlayCanvas = false;
5848
7555
  scene = null;
5849
7556
  camera = null;
7557
+ clipPlane = [0, 0, 0, 0];
5850
7558
  viewport = { x: 0, y: 0, width: 1, height: 1 };
5851
7559
  dpr = 1;
5852
7560
  frameId = 0;
@@ -5942,6 +7650,10 @@ var ICE3 = class extends ICEEventDispatcher {
5942
7650
  if (this.state === "destroyed")
5943
7651
  return;
5944
7652
  this.recordDiagnostic(error.code, error.message, "error");
7653
+ },
7654
+ onUploadReady: () => {
7655
+ if (this.state === "running")
7656
+ this.requestRender("upload-ready");
5945
7657
  }
5946
7658
  });
5947
7659
  window.addEventListener("resize", this.handleWindowResize);
@@ -5986,6 +7698,17 @@ var ICE3 = class extends ICEEventDispatcher {
5986
7698
  this.requestRender("background-color");
5987
7699
  return this;
5988
7700
  }
7701
+ /**
7702
+ * Section plane `[nx, ny, nz, d]`: fragments with `dot(world, n) + d < 0`
7703
+ * are discarded. `null` (or a zero plane) disables clipping.
7704
+ */
7705
+ setClipPlane(plane) {
7706
+ this.assertActive();
7707
+ this.clipPlane = plane ? [plane[0], plane[1], plane[2], plane[3]] : [0, 0, 0, 0];
7708
+ this.backend.setClipPlane?.(this.clipPlane);
7709
+ this.requestRender("clip-plane");
7710
+ return this;
7711
+ }
5989
7712
  prepareRenderBatchGeometry(geometry) {
5990
7713
  if (!this.backend.prepareBatchGeometry) {
5991
7714
  throw new ICEEngineError("ICE3_RENDER_BATCH_ADAPTER_UNAVAILABLE", "The active renderer cannot stage adaptive batch geometry");
@@ -5997,6 +7720,17 @@ var ICE3 = class extends ICEEventDispatcher {
5997
7720
  this.backend.releaseBatchGeometry?.(geometry);
5998
7721
  return this;
5999
7722
  }
7723
+ prepareGeometry(geometry, update) {
7724
+ if (!this.backend.prepareGeometry) {
7725
+ throw new ICEEngineError("ICE3_RENDER_GEOMETRY_UNAVAILABLE", "The active renderer cannot stage geometry updates");
7726
+ }
7727
+ this.backend.prepareGeometry(geometry, update);
7728
+ return this;
7729
+ }
7730
+ releaseGeometry(geometry) {
7731
+ this.backend.releaseGeometry?.(geometry);
7732
+ return this;
7733
+ }
6000
7734
  addLayer(layer) {
6001
7735
  this.assertActive();
6002
7736
  if (this.layers.some((item) => item.id === layer.id)) {
@@ -6460,7 +8194,11 @@ var MAX_FRAME_DELTA_MS = 64;
6460
8194
  var SETTLE_EPSILON = 1e-5;
6461
8195
  var WHEEL_LINE_HEIGHT_PX = 16;
6462
8196
  var MAX_WHEEL_STEP_PX = 240;
6463
- var MOVE_SPEED_PER_DISTANCE = 0.75;
8197
+ var ZOOM_DISTANCE_PER_HOME_PIXEL = 1e-3;
8198
+ var PAN_SPEED_PER_HOME_DISTANCE = 0.75;
8199
+ var MIN_KEY_TAP_MS = 16;
8200
+ var MIN_ZOOM_FRACTION = 5e-3;
8201
+ var ABSOLUTE_MIN_ZOOM_DISTANCE = 0.2;
6464
8202
  var NAVIGATION_KEYS = /* @__PURE__ */ new Set([
6465
8203
  "KeyW",
6466
8204
  "KeyA",
@@ -6478,6 +8216,8 @@ var ICEOrbitController = class {
6478
8216
  yaw;
6479
8217
  pitch;
6480
8218
  distance;
8219
+ minDistance;
8220
+ maxDistance;
6481
8221
  targetYaw;
6482
8222
  targetPitch;
6483
8223
  targetDistance;
@@ -6559,7 +8299,12 @@ var ICEOrbitController = class {
6559
8299
  return;
6560
8300
  }
6561
8301
  this.prepareMotion();
8302
+ const alreadyPressed = this.pressedKeys.has(input.code);
6562
8303
  this.pressedKeys.add(input.code);
8304
+ if (!alreadyPressed && !input.repeat) {
8305
+ this.applyKeyboardMovement(MIN_KEY_TAP_MS);
8306
+ this.applyPosition();
8307
+ }
6563
8308
  this.engine.requestRender("camera-keyboard-movement");
6564
8309
  };
6565
8310
  onKeyUp = (event) => {
@@ -6603,7 +8348,9 @@ var ICEOrbitController = class {
6603
8348
  const offsetY = eye.y - target.y;
6604
8349
  const offsetZ = eye.z - target.z;
6605
8350
  this.target = target.toArray();
6606
- this.distance = Math.max(0.2, Math.hypot(offsetX, offsetY, offsetZ));
8351
+ this.distance = Math.max(ABSOLUTE_MIN_ZOOM_DISTANCE, Math.hypot(offsetX, offsetY, offsetZ));
8352
+ this.maxDistance = this.distance;
8353
+ this.minDistance = Math.min(this.maxDistance, Math.max(ABSOLUTE_MIN_ZOOM_DISTANCE, this.maxDistance * MIN_ZOOM_FRACTION));
6607
8354
  this.yaw = Math.atan2(offsetX, offsetZ);
6608
8355
  this.pitch = Math.asin(clamp(offsetY / this.distance, -1, 1));
6609
8356
  this.targetYaw = this.yaw;
@@ -6623,7 +8370,7 @@ var ICEOrbitController = class {
6623
8370
  ];
6624
8371
  const radius = Math.max(0.1, Math.hypot(target.bounds.max[0] - target.bounds.min[0], target.bounds.max[1] - target.bounds.min[1], target.bounds.max[2] - target.bounds.min[2]) / 2);
6625
8372
  this.target = center;
6626
- this.targetDistance = radius / Math.sin(this.camera.fov * Math.PI / 360) * padding;
8373
+ this.targetDistance = clamp(radius / Math.sin(this.camera.fov * Math.PI / 360) * padding, this.minDistance, this.maxDistance);
6627
8374
  if (options.animate === false) {
6628
8375
  this.distance = this.targetDistance;
6629
8376
  this.applyPosition();
@@ -6642,7 +8389,7 @@ var ICEOrbitController = class {
6642
8389
  zoomBy(delta) {
6643
8390
  this.assertActive();
6644
8391
  this.prepareMotion();
6645
- this.targetDistance = clamp(this.targetDistance * Math.exp(delta * 1e-3), 0.2, 100);
8392
+ this.targetDistance = clamp(this.targetDistance + delta * this.maxDistance * ZOOM_DISTANCE_PER_HOME_PIXEL, this.minDistance, this.maxDistance);
6646
8393
  this.engine.requestRender("camera-zoom");
6647
8394
  return this;
6648
8395
  }
@@ -6688,7 +8435,7 @@ var ICEOrbitController = class {
6688
8435
  if (magnitude === 0) {
6689
8436
  return;
6690
8437
  }
6691
- const distance2 = this.distance * MOVE_SPEED_PER_DISTANCE * (deltaMs / 1e3) / magnitude;
8438
+ const distance2 = this.maxDistance * PAN_SPEED_PER_HOME_DISTANCE * (deltaMs / 1e3) / magnitude;
6692
8439
  const sinYaw = Math.sin(this.yaw);
6693
8440
  const cosYaw = Math.cos(this.yaw);
6694
8441
  const sinPitch = Math.sin(this.pitch);
@@ -6754,7 +8501,14 @@ function buildAssetScene(snapshot) {
6754
8501
  }));
6755
8502
  const materials = snapshot.materials.map((material) => new ICEBasicMaterial({
6756
8503
  color: material.baseColor,
6757
- ...material.edgeColor ? { edgeColor: material.edgeColor } : {}
8504
+ ...material.edgeColor ? { edgeColor: material.edgeColor } : {},
8505
+ ...material.shading ? { shading: material.shading } : {},
8506
+ ...material.map ? { map: material.map } : {},
8507
+ ...material.normalMap ? { normalMap: material.normalMap } : {},
8508
+ ...material.ormMap ? { ormMap: material.ormMap } : {},
8509
+ ...material.metallic !== void 0 ? { metallic: material.metallic } : {},
8510
+ ...material.roughness !== void 0 ? { roughness: material.roughness } : {},
8511
+ ...material.emissive ? { emissive: material.emissive } : {}
6758
8512
  }));
6759
8513
  const meshesByNode = /* @__PURE__ */ new Map();
6760
8514
  for (const mesh of snapshot.meshes) {
@@ -6829,8 +8583,123 @@ function buildAssetScene(snapshot) {
6829
8583
  stats: snapshot.stats
6830
8584
  };
6831
8585
  }
8586
+ function buildPreviewPacketScene(packet, previousGeometries = /* @__PURE__ */ new Map()) {
8587
+ const geometries = /* @__PURE__ */ new Map();
8588
+ const geometryUpdates = /* @__PURE__ */ new Map();
8589
+ for (const source of packet.geometries) {
8590
+ const previous = previousGeometries.get(source.geometryId);
8591
+ if (previous && previous.identity.topologyDigest === source.topologyDigest && previous.identity.attributeDigest === source.attributeDigest) {
8592
+ geometries.set(source.geometryId, previous);
8593
+ continue;
8594
+ }
8595
+ const identity = Object.freeze({
8596
+ geometryId: source.geometryId,
8597
+ topologyDigest: source.topologyDigest,
8598
+ attributeDigest: source.attributeDigest
8599
+ });
8600
+ const geometry = new ICEIndexedGeometry({
8601
+ id: source.residentGeometryId ?? source.geometryId,
8602
+ identity,
8603
+ positions: source.positions,
8604
+ indices: source.indices
8605
+ });
8606
+ geometries.set(source.geometryId, geometry);
8607
+ const attributeRanges = source.ranges.filter((range) => range.usage === "vertex_attributes");
8608
+ const attributeStart = Math.min(...attributeRanges.map((range) => range.byteOffset));
8609
+ geometryUpdates.set(source.geometryId, Object.freeze({
8610
+ positionRanges: Object.freeze(attributeRanges.map((range) => Object.freeze({
8611
+ byteOffset: range.byteOffset - attributeStart,
8612
+ byteLength: range.byteLength
8613
+ })))
8614
+ }));
8615
+ }
8616
+ const materials = new Map(packet.materials.map((material) => [
8617
+ material.materialId,
8618
+ new ICEBasicMaterial({
8619
+ color: colorHex(material.baseColor)
8620
+ })
8621
+ ]));
8622
+ const defaultMaterial = new ICEBasicMaterial({
8623
+ color: "#9aa6b2"
8624
+ });
8625
+ const scene = new ICEScene({ id: `packet:${packet.revision}:scene` });
8626
+ const nodes = /* @__PURE__ */ new Map();
8627
+ for (const source of packet.nodes) {
8628
+ const geometry = source.geometryId ? geometries.get(source.geometryId) : void 0;
8629
+ const material = source.materialId ? materials.get(source.materialId) ?? defaultMaterial : defaultMaterial;
8630
+ const options = {
8631
+ id: source.nodeId,
8632
+ displayName: source.name,
8633
+ selectionId: source.nodeId,
8634
+ matrix: source.matrix
8635
+ };
8636
+ const node = geometry ? new ICEMesh({
8637
+ ...options,
8638
+ geometry,
8639
+ material
8640
+ }) : new ICENode(options);
8641
+ node.visible = source.visible;
8642
+ nodes.set(source.nodeId, node);
8643
+ }
8644
+ for (const source of packet.nodes) {
8645
+ const node = nodes.get(source.nodeId);
8646
+ const parent = source.parentId ? nodes.get(source.parentId) : void 0;
8647
+ if (source.parentId && !parent) {
8648
+ throw new Error(`ICE3_PREVIEW_PACKET_INVALID: parent node ${source.parentId} is missing`);
8649
+ }
8650
+ (parent ?? scene).addChild(node);
8651
+ }
8652
+ return {
8653
+ scene,
8654
+ geometries,
8655
+ geometryUpdates
8656
+ };
8657
+ }
8658
+ function colorHex(color) {
8659
+ const channel = (value) => Math.round(Math.max(0, Math.min(1, value)) * 255).toString(16).padStart(2, "0");
8660
+ return `#${channel(color[0])}${channel(color[1])}${channel(color[2])}`;
8661
+ }
6832
8662
 
6833
8663
  // packages/assets/dist/ICEProfileValidator.js
8664
+ function validateTextures(json) {
8665
+ const images = records(json.images);
8666
+ const textures = records(json.textures);
8667
+ const samplers = records(json.samplers);
8668
+ for (const image of images) {
8669
+ const uri = image.uri;
8670
+ if (typeof uri !== "string" || !uri || uri.startsWith("/") || uri.includes("..") || uri.includes("://") || image.bufferView !== void 0) {
8671
+ throw assetError("ICE3_ASSET_UNSUPPORTED_FEATURE", "images must reference a relative file path");
8672
+ }
8673
+ }
8674
+ for (const texture of textures) {
8675
+ const source = Number(texture.source);
8676
+ if (!Number.isInteger(source) || source < 0 || source >= images.length) {
8677
+ throw assetError("ICE3_ASSET_INVALID_GLB", "texture source is out of range");
8678
+ }
8679
+ if (texture.sampler !== void 0) {
8680
+ const sampler = Number(texture.sampler);
8681
+ if (!Number.isInteger(sampler) || sampler < 0 || sampler >= samplers.length) {
8682
+ throw assetError("ICE3_ASSET_INVALID_GLB", "texture sampler is out of range");
8683
+ }
8684
+ }
8685
+ }
8686
+ for (const material of records(json.materials)) {
8687
+ const slots = [
8688
+ ["baseColorTexture", record(record(material.pbrMetallicRoughness)?.baseColorTexture)],
8689
+ ["normalTexture", record(material.normalTexture)],
8690
+ ["metallicRoughnessTexture", record(record(material.pbrMetallicRoughness)?.metallicRoughnessTexture)],
8691
+ ["occlusionTexture", record(material.occlusionTexture)]
8692
+ ];
8693
+ for (const [slot, reference] of slots) {
8694
+ if (!reference)
8695
+ continue;
8696
+ const index = Number(reference.index);
8697
+ if (!Number.isInteger(index) || index < 0 || index >= textures.length) {
8698
+ throw assetError("ICE3_ASSET_INVALID_GLB", `${slot} index is out of range`);
8699
+ }
8700
+ }
8701
+ }
8702
+ }
6834
8703
  function validatePreparedProfile(json, binary) {
6835
8704
  const asset = record(json.asset);
6836
8705
  const profile = record(record(asset?.extras)?.ice3);
@@ -6858,15 +8727,13 @@ function validatePreparedProfile(json, binary) {
6858
8727
  "animations",
6859
8728
  "skins",
6860
8729
  "cameras",
6861
- "textures",
6862
- "images",
6863
- "samplers",
6864
8730
  "lights"
6865
8731
  ]) {
6866
8732
  if (Array.isArray(json[field]) && json[field].length > 0) {
6867
8733
  throw assetError("ICE3_ASSET_UNSUPPORTED_FEATURE", `${field} are unsupported by the M1 profile`);
6868
8734
  }
6869
8735
  }
8736
+ validateTextures(json);
6870
8737
  if (records(json.scenes).length !== 1 || json.scene !== 0 || records(json.buffers).length !== 1 || records(json.buffers)[0]?.uri !== void 0 || Number(records(json.buffers)[0]?.byteLength) > binary.byteLength) {
6871
8738
  throw assetError("ICE3_ASSET_INVALID_GLB", "profile container fields are invalid");
6872
8739
  }
@@ -6948,9 +8815,9 @@ function validatePreparedStats(json, profile) {
6948
8815
  let primitiveCount = 0;
6949
8816
  let triangleCount = 0;
6950
8817
  let edgeSegmentCount = 0;
6951
- const selectionNodes = /* @__PURE__ */ new Set();
8818
+ const selectionIds = /* @__PURE__ */ new Set();
6952
8819
  const referencedMeshes = /* @__PURE__ */ new Set();
6953
- for (const [nodeIndex, node] of nodes.entries()) {
8820
+ for (const node of nodes) {
6954
8821
  if (node.mesh === void 0)
6955
8822
  continue;
6956
8823
  const mesh = meshes[Number(node.mesh)];
@@ -6960,7 +8827,10 @@ function validatePreparedStats(json, profile) {
6960
8827
  for (const primitive of records(mesh.primitives)) {
6961
8828
  const mode = Number(primitive.mode ?? 4);
6962
8829
  if (mode === 4) {
6963
- selectionNodes.add(nodeIndex);
8830
+ const nodeProfile = record(record(node.extras)?.ice3);
8831
+ const selectionId = nodeProfile?.selectionId ?? nodeProfile?.id;
8832
+ if (typeof selectionId === "string")
8833
+ selectionIds.add(selectionId);
6964
8834
  }
6965
8835
  }
6966
8836
  }
@@ -6986,7 +8856,7 @@ function validatePreparedStats(json, profile) {
6986
8856
  triangleCount,
6987
8857
  edgeSegmentCount,
6988
8858
  materialCount: records(json.materials).length,
6989
- selectionGroupCount: Array.isArray(batchingSelectionIds) ? new Set(batchingSelectionIds).size : selectionNodes.size
8859
+ selectionGroupCount: Array.isArray(batchingSelectionIds) ? new Set(batchingSelectionIds).size : selectionIds.size
6990
8860
  };
6991
8861
  for (const [key, value] of Object.entries(expected)) {
6992
8862
  if (stats?.[key] !== value) {
@@ -7021,13 +8891,15 @@ function assetError(code, message) {
7021
8891
  }
7022
8892
 
7023
8893
  // packages/assets/dist/ICEGLBReader.js
7024
- function readPreparedGLB(bytes) {
8894
+ function readPreparedGLB(bytes, options = {}) {
7025
8895
  const { json, binary, profile } = parseGLB(bytes);
7026
8896
  const accessors = records(json.accessors);
7027
8897
  const views = records(json.bufferViews);
7028
8898
  const sourceNodes = records(json.nodes);
7029
8899
  const sourceMeshes = records(json.meshes);
7030
8900
  const sourceMaterials = records(json.materials);
8901
+ const sourceTextures = records(json.textures);
8902
+ const sourceImages = records(json.images);
7031
8903
  const activeScene = records(json.scenes)[Number(json.scene ?? 0)];
7032
8904
  const sceneRoots = integers(activeScene?.nodes);
7033
8905
  const parents = new Array(sourceNodes.length).fill(null);
@@ -7076,7 +8948,7 @@ function readPreparedGLB(bytes) {
7076
8948
  const geometries = [];
7077
8949
  const materials = [];
7078
8950
  const meshes = [];
7079
- const selectionNodes = /* @__PURE__ */ new Set();
8951
+ const selectionIds = /* @__PURE__ */ new Set();
7080
8952
  let primitiveCount = 0;
7081
8953
  let triangleCount = 0;
7082
8954
  let edgeSegmentCount = 0;
@@ -7108,11 +8980,20 @@ function readPreparedGLB(bytes) {
7108
8980
  edgeSegmentCount += (edgeIndices?.length ?? 0) / 2;
7109
8981
  const baseMaterial = sourceMaterials[Number(primitive.material)];
7110
8982
  const edgeMaterial = edgePrimitive ? sourceMaterials[Number(edgePrimitive.material)] : void 0;
8983
+ const shading = record(record(baseMaterial?.extras)?.ice3)?.shading;
7111
8984
  const materialIndex = materials.length;
8985
+ const map = materialMap(baseMaterial, sourceTextures, sourceImages, options.baseUrl);
8986
+ const normalMap = normalMaterialMap(baseMaterial, sourceTextures, sourceImages, options.baseUrl);
8987
+ const ormMap = ormMaterialMap(baseMaterial, sourceTextures, sourceImages, options.baseUrl);
7112
8988
  materials.push({
7113
8989
  id: `${meshId}/material/${primitiveIndex}`,
7114
8990
  baseColor: materialColor(baseMaterial),
7115
- ...edgeMaterial ? { edgeColor: materialColor(edgeMaterial) } : {}
8991
+ ...edgeMaterial ? { edgeColor: materialColor(edgeMaterial) } : {},
8992
+ ...shading === "directional" || shading === "water" ? { shading } : {},
8993
+ ...map ? { map } : {},
8994
+ ...normalMap ? { normalMap } : {},
8995
+ ...ormMap ? { ormMap } : {},
8996
+ ...surfaceFactors(baseMaterial)
7116
8997
  });
7117
8998
  const bounds = boundsFromAccessor(accessors[positionIndex]);
7118
8999
  const geometryIndex = geometries.length;
@@ -7126,7 +9007,7 @@ function readPreparedGLB(bytes) {
7126
9007
  bounds
7127
9008
  });
7128
9009
  for (const entry of nodesForMesh) {
7129
- selectionNodes.add(entry.index);
9010
+ selectionIds.add(nodes[entry.index].selectionId);
7130
9011
  meshes.push({
7131
9012
  id: `${meshId}/primitive/${primitiveIndex}/${entry.index}`,
7132
9013
  node: entry.index,
@@ -7144,7 +9025,7 @@ function readPreparedGLB(bytes) {
7144
9025
  triangleCount,
7145
9026
  edgeSegmentCount,
7146
9027
  materialCount: sourceMaterials.length,
7147
- selectionGroupCount: selectionNodes.size
9028
+ selectionGroupCount: batching ? new Set(batching.semanticSelectionIds).size : selectionIds.size
7148
9029
  };
7149
9030
  const contract = preparedContract(profile);
7150
9031
  return {
@@ -7168,7 +9049,7 @@ function readBatchingMetadata(profile, sourceNodes, sourceMeshes, sourceMaterial
7168
9049
  }
7169
9050
  const semanticIds = stringArray(raw.semanticIds, "semanticIds");
7170
9051
  const semanticNodeIds = stringArray(raw.semanticNodeIds, "semanticNodeIds");
7171
- const semanticSelectionIds = stringArray(raw.semanticSelectionIds, "semanticSelectionIds");
9052
+ const semanticSelectionIds = stringArray(raw.semanticSelectionIds, "semanticSelectionIds", false);
7172
9053
  if (semanticIds.length === 0 || semanticNodeIds.length !== semanticIds.length || semanticSelectionIds.length !== semanticIds.length) {
7173
9054
  throw assetError2("ICE3_ASSET_BATCH_INVALID", "semantic arrays are inconsistent");
7174
9055
  }
@@ -7235,8 +9116,8 @@ function readBatchingMetadata(profile, sourceNodes, sourceMeshes, sourceMaterial
7235
9116
  batches
7236
9117
  };
7237
9118
  }
7238
- function stringArray(value, field) {
7239
- if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string" || !entry.trim()) || new Set(value).size !== value.length) {
9119
+ function stringArray(value, field, unique = true) {
9120
+ if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string" || !entry.trim()) || unique && new Set(value).size !== value.length) {
7240
9121
  throw assetError2("ICE3_ASSET_BATCH_INVALID", `${field} is invalid`);
7241
9122
  }
7242
9123
  return value;
@@ -7422,13 +9303,102 @@ function boundsFromProfile(value) {
7422
9303
  max: [Number(max[0]), Number(max[1]), Number(max[2])]
7423
9304
  };
7424
9305
  }
9306
+ function materialMap(material, textures, images, baseUrl) {
9307
+ const reference = record(record(material?.pbrMetallicRoughness)?.baseColorTexture);
9308
+ if (!reference)
9309
+ return void 0;
9310
+ const texture = textures[Number(reference.index)];
9311
+ const uri = record(images[Number(record(texture)?.source)])?.uri;
9312
+ if (typeof uri !== "string" || uri.length === 0)
9313
+ return void 0;
9314
+ const transform = record(record(reference.extensions)?.KHR_texture_transform);
9315
+ const scale = numbers(transform?.scale, 2);
9316
+ const su = scale?.[0];
9317
+ const sv = scale?.[1];
9318
+ return {
9319
+ url: resolveTextureUrl(uri, baseUrl),
9320
+ ...typeof su === "number" && typeof sv === "number" && su > 0 && sv > 0 ? { scale: [su, sv] } : {}
9321
+ };
9322
+ }
9323
+ function surfaceFactors(material) {
9324
+ const pbr = record(material?.pbrMetallicRoughness);
9325
+ const metallic = unitOrUndefined(pbr?.metallicFactor);
9326
+ const roughness = unitOrUndefined(pbr?.roughnessFactor);
9327
+ const emissive = numbers(material?.emissiveFactor, 3);
9328
+ return {
9329
+ ...metallic !== void 0 ? { metallic } : {},
9330
+ ...roughness !== void 0 ? { roughness } : {},
9331
+ ...emissive && emissive.some((value) => value > 0) ? { emissive: [emissive[0], emissive[1], emissive[2]] } : {}
9332
+ };
9333
+ }
9334
+ function unitOrUndefined(value) {
9335
+ const parsed = Number(value);
9336
+ if (!Number.isFinite(parsed))
9337
+ return void 0;
9338
+ return Math.min(1, Math.max(0, parsed));
9339
+ }
9340
+ function normalMaterialMap(material, textures, images, baseUrl) {
9341
+ const reference = record(material?.normalTexture);
9342
+ if (!reference)
9343
+ return void 0;
9344
+ const texture = textures[Number(reference.index)];
9345
+ const uri = record(images[Number(record(texture)?.source)])?.uri;
9346
+ if (typeof uri !== "string" || uri.length === 0)
9347
+ return void 0;
9348
+ const strength = Number(reference.scale);
9349
+ return {
9350
+ url: resolveTextureUrl(uri, baseUrl),
9351
+ ...Number.isFinite(strength) && strength >= 0 ? { strength } : {}
9352
+ };
9353
+ }
9354
+ function ormMaterialMap(material, textures, images, baseUrl) {
9355
+ const metallicRoughness = textureReference(record(record(material?.pbrMetallicRoughness)?.metallicRoughnessTexture), textures, images, baseUrl);
9356
+ const occlusion = textureReference(record(material?.occlusionTexture), textures, images, baseUrl);
9357
+ if (!metallicRoughness && !occlusion)
9358
+ return void 0;
9359
+ const url = metallicRoughness?.url ?? occlusion.url;
9360
+ const scale = metallicRoughness?.scale ?? occlusion?.scale;
9361
+ return {
9362
+ url,
9363
+ ...scale ? { scale } : {},
9364
+ occlusion: occlusion?.url === url,
9365
+ metallicRoughness: metallicRoughness !== void 0
9366
+ };
9367
+ }
9368
+ function textureReference(reference, textures, images, baseUrl) {
9369
+ if (!reference)
9370
+ return void 0;
9371
+ const texture = textures[Number(reference.index)];
9372
+ const uri = record(images[Number(record(texture)?.source)])?.uri;
9373
+ if (typeof uri !== "string" || uri.length === 0)
9374
+ return void 0;
9375
+ const transform = record(record(reference.extensions)?.KHR_texture_transform);
9376
+ const scale = numbers(transform?.scale, 2);
9377
+ const su = scale?.[0];
9378
+ const sv = scale?.[1];
9379
+ return {
9380
+ url: resolveTextureUrl(uri, baseUrl),
9381
+ ...typeof su === "number" && typeof sv === "number" && su > 0 && sv > 0 ? { scale: [su, sv] } : {}
9382
+ };
9383
+ }
9384
+ function resolveTextureUrl(uri, baseUrl) {
9385
+ if (!baseUrl)
9386
+ return uri;
9387
+ try {
9388
+ return new URL(uri, baseUrl).href;
9389
+ } catch {
9390
+ return uri;
9391
+ }
9392
+ }
7425
9393
  function materialColor(material) {
7426
9394
  if (!material)
7427
9395
  return "#808080";
7428
9396
  const color = record(material.pbrMetallicRoughness)?.baseColorFactor;
7429
9397
  if (!Array.isArray(color) || color.length < 3)
7430
9398
  return "#808080";
7431
- return `#${color.slice(0, 3).map((value) => Math.round(Math.min(1, Math.max(0, Number(value))) * 255).toString(16).padStart(2, "0")).join("")}`;
9399
+ const alpha = color.length >= 4 ? Number(color[3]) : 1;
9400
+ const channels = color.slice(0, alpha < 1 ? 4 : 3).map((value) => Math.round(Math.min(1, Math.max(0, Number(value))) * 255).toString(16).padStart(2, "0"));
9401
+ return `#${channels.join("")}`;
7432
9402
  }
7433
9403
  function integers(value) {
7434
9404
  return Array.isArray(value) ? value.filter((entry) => Number.isInteger(entry)) : [];
@@ -7443,6 +9413,494 @@ function assetError2(code, message) {
7443
9413
  return new Error(`${code}: ${message}`);
7444
9414
  }
7445
9415
 
9416
+ // packages/assets/dist/ICEPreviewGeometryPacket.js
9417
+ var MAGIC = "ICE3PGP1";
9418
+ var HEADER_BYTES = 120;
9419
+ var MANIFEST_LENGTH_OFFSET = 40;
9420
+ var BINARY_LENGTH_OFFSET = 48;
9421
+ var MANIFEST_DIGEST_OFFSET = 56;
9422
+ var CONTENT_DIGEST_OFFSET = 88;
9423
+ var MAX_PACKET_BYTES = 256 * 1024 * 1024;
9424
+ var MAX_NODES = 1e6;
9425
+ var MAX_GEOMETRIES = 25e4;
9426
+ var MAX_RANGES = 64;
9427
+ var encoder = new TextEncoder();
9428
+ var decoder = new TextDecoder("utf-8", { fatal: true });
9429
+ function parsePreviewGeometryPacket(input) {
9430
+ const bytes = input instanceof Uint8Array ? input : new Uint8Array(input);
9431
+ if (bytes.byteLength < HEADER_BYTES || bytes.byteLength > MAX_PACKET_BYTES) {
9432
+ throw new RangeError("ICE3 preview packet size is invalid");
9433
+ }
9434
+ if (decoder.decode(bytes.subarray(0, 8)) !== MAGIC) {
9435
+ throw new TypeError("ICE3 preview packet magic is invalid");
9436
+ }
9437
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
9438
+ const schemaVersion = view.getUint16(8, true);
9439
+ const transportVersion = view.getUint16(10, true);
9440
+ const headerBytes = view.getUint32(12, true);
9441
+ if (schemaVersion !== 1 || transportVersion !== 1 || headerBytes !== HEADER_BYTES) {
9442
+ throw new TypeError("ICE3 preview packet version is unsupported");
9443
+ }
9444
+ const manifestLength = readSafeU64(view, MANIFEST_LENGTH_OFFSET);
9445
+ const binaryLength = readSafeU64(view, BINARY_LENGTH_OFFSET);
9446
+ if (manifestLength === 0 || manifestLength % 8 !== 0 || HEADER_BYTES + manifestLength + binaryLength !== bytes.byteLength) {
9447
+ throw new RangeError("ICE3 preview packet lengths are invalid");
9448
+ }
9449
+ const manifestStart = HEADER_BYTES;
9450
+ const manifestEnd = manifestStart + manifestLength;
9451
+ const manifestBytes = bytes.subarray(manifestStart, manifestEnd);
9452
+ const binary = bytes.subarray(manifestEnd);
9453
+ const expectedManifestDigest = bytes.subarray(MANIFEST_DIGEST_OFFSET, MANIFEST_DIGEST_OFFSET + 32);
9454
+ if (!equalBytes(sha256(manifestBytes), expectedManifestDigest)) {
9455
+ throw new TypeError("ICE3 preview packet manifest digest is invalid");
9456
+ }
9457
+ const contentHash = sha256(concatBytes(manifestBytes, binary));
9458
+ const expectedContentDigest = bytes.subarray(CONTENT_DIGEST_OFFSET, CONTENT_DIGEST_OFFSET + 32);
9459
+ if (!equalBytes(contentHash, expectedContentDigest)) {
9460
+ throw new TypeError("ICE3 preview packet content digest is invalid");
9461
+ }
9462
+ let candidate;
9463
+ try {
9464
+ candidate = JSON.parse(decoder.decode(manifestBytes));
9465
+ } catch {
9466
+ throw new TypeError("ICE3 preview packet manifest is invalid");
9467
+ }
9468
+ const manifest = validateManifest(candidate, binary.byteLength);
9469
+ return {
9470
+ ...manifest,
9471
+ geometries: manifest.geometries.map((geometry) => decodeGeometry(geometry, binary)),
9472
+ binary
9473
+ };
9474
+ }
9475
+ function applyPreviewGeometryDelta(base, delta) {
9476
+ if (delta.parentRevision !== base.revision) {
9477
+ throw new RangeError(`ICE3 preview packet parent revision ${delta.parentRevision ?? "missing"} does not match ${base.revision}`);
9478
+ }
9479
+ const nodes = mergeById(base.nodes, delta.nodes, delta.removedNodeIds, (node) => node.nodeId);
9480
+ const geometries = mergeGeometries(base.geometries, delta, delta.removedGeometryIds);
9481
+ const materials = mergeById(base.materials, delta.materials, [], (material) => material.materialId);
9482
+ return {
9483
+ schemaVersion: delta.schemaVersion,
9484
+ transportVersion: delta.transportVersion,
9485
+ revision: delta.revision,
9486
+ parentRevision: delta.parentRevision,
9487
+ sourceDigest: delta.sourceDigest,
9488
+ backend: delta.backend,
9489
+ residencyMode: delta.residencyMode,
9490
+ nodes,
9491
+ geometries,
9492
+ materials,
9493
+ removedNodeIds: [],
9494
+ removedGeometryIds: [],
9495
+ binary: delta.binary
9496
+ };
9497
+ }
9498
+ function mergeGeometries(baseGeometries, delta, removedGeometryIds) {
9499
+ const removed = new Set(removedGeometryIds);
9500
+ const updates = new Map(delta.geometries.map((geometry) => [geometry.geometryId, geometry]));
9501
+ const output = [];
9502
+ for (const base of baseGeometries) {
9503
+ if (removed.has(base.geometryId)) {
9504
+ continue;
9505
+ }
9506
+ const update = updates.get(base.geometryId);
9507
+ output.push(update ? mergeGeometry(base, update) : base);
9508
+ updates.delete(base.geometryId);
9509
+ }
9510
+ output.push(...updates.values());
9511
+ return Object.freeze(output);
9512
+ }
9513
+ function mergeGeometry(base, update) {
9514
+ if (base === update) {
9515
+ return base;
9516
+ }
9517
+ return freezeGeometry(update, base.attributeDigest === update.attributeDigest ? base.positions : update.positions, base.topologyDigest === update.topologyDigest ? base.indices : update.indices);
9518
+ }
9519
+ function validateManifest(value, binaryLength) {
9520
+ if (!isRecord(value)) {
9521
+ throw invalidManifest();
9522
+ }
9523
+ const nodes = readArray(value.nodes, MAX_NODES).map(readNode);
9524
+ const geometries = readArray(value.geometries, MAX_GEOMETRIES).map(readGeometryRecord);
9525
+ const materials = readArray(value.materials, MAX_GEOMETRIES).map(readMaterial);
9526
+ const removedNodeIds = readStringArray(value.removedNodeIds, MAX_NODES);
9527
+ const removedGeometryIds = readStringArray(value.removedGeometryIds, MAX_GEOMETRIES);
9528
+ const manifest = {
9529
+ schemaVersion: readVersion(value.schemaVersion, 1),
9530
+ transportVersion: readVersion(value.transportVersion, 1),
9531
+ revision: readSafeInteger(value.revision, 0, Number.MAX_SAFE_INTEGER),
9532
+ parentRevision: value.parentRevision === null ? null : readSafeInteger(value.parentRevision, 0, Number.MAX_SAFE_INTEGER),
9533
+ sourceDigest: readDigest(value.sourceDigest),
9534
+ backend: readEnum(value.backend, ["cpu", "gpu"]),
9535
+ residencyMode: readEnum(value.residencyMode, ["cpu", "cold", "warm", "resident", "snapshot"]),
9536
+ nodes,
9537
+ geometries,
9538
+ materials,
9539
+ removedNodeIds,
9540
+ removedGeometryIds
9541
+ };
9542
+ if (manifest.parentRevision !== null && manifest.parentRevision >= manifest.revision) {
9543
+ throw invalidManifest();
9544
+ }
9545
+ ensureUnique(nodes, (node) => node.nodeId);
9546
+ ensureUnique(geometries, (geometry) => geometry.geometryId);
9547
+ ensureUnique(materials, (material) => material.materialId);
9548
+ for (const geometry of geometries) {
9549
+ for (const range of geometry.ranges) {
9550
+ if (range.byteOffset % 4 !== 0 || range.byteLength === 0 || range.byteOffset + range.byteLength > binaryLength) {
9551
+ throw invalidManifest();
9552
+ }
9553
+ }
9554
+ }
9555
+ return manifest;
9556
+ }
9557
+ function readNode(value, index) {
9558
+ if (!isRecord(value)) {
9559
+ throw new TypeError(`ICE3 preview node ${index} is invalid`);
9560
+ }
9561
+ const matrix = readArray(value.matrix, 16);
9562
+ if (matrix.some((entry) => typeof entry !== "number" || !Number.isFinite(entry))) {
9563
+ throw new TypeError(`ICE3 preview node ${index} matrix is invalid`);
9564
+ }
9565
+ return {
9566
+ nodeId: readNonEmptyString(value.nodeId),
9567
+ parentId: readNullableString(value.parentId),
9568
+ name: readString(value.name),
9569
+ matrix: Object.freeze(matrix),
9570
+ materialId: readNullableString(value.materialId),
9571
+ geometryId: readNullableString(value.geometryId),
9572
+ visible: readBoolean(value.visible)
9573
+ };
9574
+ }
9575
+ function readGeometryRecord(value, index) {
9576
+ if (!isRecord(value)) {
9577
+ throw new TypeError(`ICE3 preview geometry ${index} is invalid`);
9578
+ }
9579
+ const indexWidth = readSafeInteger(value.indexWidth, 1, 8);
9580
+ if (indexWidth !== 2 && indexWidth !== 4) {
9581
+ throw new TypeError(`ICE3 preview geometry ${index} index width is invalid`);
9582
+ }
9583
+ const ranges = readArray(value.ranges, MAX_RANGES).map(readRange);
9584
+ return {
9585
+ geometryId: readNonEmptyString(value.geometryId),
9586
+ residentGeometryId: readNullableString(value.residentGeometryId),
9587
+ topologyDigest: readDigest(value.topologyDigest),
9588
+ attributeDigest: readDigest(value.attributeDigest),
9589
+ vertexCount: readSafeInteger(value.vertexCount, 1, 1e8),
9590
+ indexCount: readSafeInteger(value.indexCount, 0, 3e8),
9591
+ indexWidth,
9592
+ ranges: Object.freeze(ranges)
9593
+ };
9594
+ }
9595
+ function readRange(value, index) {
9596
+ if (!isRecord(value)) {
9597
+ throw new TypeError(`ICE3 preview range ${index} is invalid`);
9598
+ }
9599
+ return {
9600
+ byteOffset: readSafeInteger(value.byteOffset, 0, MAX_PACKET_BYTES),
9601
+ byteLength: readSafeInteger(value.byteLength, 1, MAX_PACKET_BYTES),
9602
+ usage: readEnum(value.usage, ["vertex_attributes", "indices"])
9603
+ };
9604
+ }
9605
+ function readMaterial(value, index) {
9606
+ if (!isRecord(value)) {
9607
+ throw new TypeError(`ICE3 preview material ${index} is invalid`);
9608
+ }
9609
+ const baseColor = readFiniteArray(value.baseColor, 4);
9610
+ return {
9611
+ materialId: readNonEmptyString(value.materialId),
9612
+ baseColor: Object.freeze(baseColor),
9613
+ metallic: readFiniteNumber(value.metallic),
9614
+ roughness: readFiniteNumber(value.roughness),
9615
+ opacity: readFiniteNumber(value.opacity)
9616
+ };
9617
+ }
9618
+ function decodeGeometry(geometry, binary) {
9619
+ const attributes = geometry.ranges.find((range) => range.usage === "vertex_attributes");
9620
+ const topology = geometry.ranges.find((range) => range.usage === "indices");
9621
+ if (!attributes && !topology) {
9622
+ return freezeGeometry(encodeIdentity(geometry), new Float32Array(), new Uint32Array());
9623
+ }
9624
+ const positions = attributes ? decodePositions(attributes, geometry.vertexCount, binary) : new Float32Array(geometry.vertexCount * 3);
9625
+ const indices = topology ? decodeIndices(topology, geometry.indexCount, geometry.indexWidth, binary) : new Uint32Array();
9626
+ return freezeGeometry(encodeIdentity(geometry), positions, indices);
9627
+ }
9628
+ function decodePositions(range, vertexCount, binary) {
9629
+ const bytesPerVertex = range.byteLength / vertexCount;
9630
+ const components = bytesPerVertex / 4;
9631
+ if (!Number.isInteger(components) || components < 3) {
9632
+ throw new TypeError("ICE3 preview position range has an invalid stride");
9633
+ }
9634
+ const source = new Float32Array(binary.buffer, binary.byteOffset + range.byteOffset, range.byteLength / 4);
9635
+ if (components === 3) {
9636
+ return source;
9637
+ }
9638
+ const positions = new Float32Array(vertexCount * 3);
9639
+ for (let vertex = 0; vertex < vertexCount; vertex += 1) {
9640
+ const sourceOffset = vertex * components;
9641
+ positions[vertex * 3] = source[sourceOffset];
9642
+ positions[vertex * 3 + 1] = source[sourceOffset + 1];
9643
+ positions[vertex * 3 + 2] = source[sourceOffset + 2];
9644
+ }
9645
+ return positions;
9646
+ }
9647
+ function decodeIndices(range, indexCount, indexWidth, binary) {
9648
+ if (range.byteLength !== indexCount * indexWidth) {
9649
+ throw new TypeError("ICE3 preview index range has an invalid length");
9650
+ }
9651
+ const offset = binary.byteOffset + range.byteOffset;
9652
+ return indexWidth === 2 ? new Uint16Array(binary.buffer, offset, indexCount) : new Uint32Array(binary.buffer, offset, indexCount);
9653
+ }
9654
+ function freezeGeometry(identity, positions, indices) {
9655
+ return Object.freeze({
9656
+ ...identity,
9657
+ positions,
9658
+ indices
9659
+ });
9660
+ }
9661
+ function encodeIdentity(geometry) {
9662
+ return geometry;
9663
+ }
9664
+ function mergeById(base, updates, removed, id) {
9665
+ const removedIds = new Set(removed);
9666
+ const updateMap = new Map(updates.map((value) => [id(value), value]));
9667
+ const output = base.filter((value) => !removedIds.has(id(value))).map((value) => updateMap.get(id(value)) ?? value);
9668
+ const baseIds = new Set(base.map(id));
9669
+ for (const value of updates) {
9670
+ if (!baseIds.has(id(value))) {
9671
+ output.push(value);
9672
+ }
9673
+ }
9674
+ return Object.freeze(output);
9675
+ }
9676
+ function readArray(value, maximum) {
9677
+ if (!Array.isArray(value) || value.length > maximum) {
9678
+ throw invalidManifest();
9679
+ }
9680
+ return value;
9681
+ }
9682
+ function readStringArray(value, maximum) {
9683
+ return Object.freeze(readArray(value, maximum).map(readString));
9684
+ }
9685
+ function readFiniteArray(value, length) {
9686
+ const values = readArray(value, length);
9687
+ if (values.length !== length || values.some((entry) => typeof entry !== "number" || !Number.isFinite(entry))) {
9688
+ throw invalidManifest();
9689
+ }
9690
+ return values;
9691
+ }
9692
+ function readVersion(value, expected) {
9693
+ if (value !== expected) {
9694
+ throw new TypeError("ICE3 preview packet version is unsupported");
9695
+ }
9696
+ return expected;
9697
+ }
9698
+ function readSafeInteger(value, minimum, maximum) {
9699
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < minimum || value > maximum) {
9700
+ throw invalidManifest();
9701
+ }
9702
+ return value;
9703
+ }
9704
+ function readFiniteNumber(value) {
9705
+ if (typeof value !== "number" || !Number.isFinite(value)) {
9706
+ throw invalidManifest();
9707
+ }
9708
+ return value;
9709
+ }
9710
+ function readBoolean(value) {
9711
+ if (typeof value !== "boolean") {
9712
+ throw invalidManifest();
9713
+ }
9714
+ return value;
9715
+ }
9716
+ function readString(value) {
9717
+ if (typeof value !== "string") {
9718
+ throw invalidManifest();
9719
+ }
9720
+ return value;
9721
+ }
9722
+ function readNonEmptyString(value) {
9723
+ const result = readString(value);
9724
+ if (!result.trim()) {
9725
+ throw invalidManifest();
9726
+ }
9727
+ return result;
9728
+ }
9729
+ function readNullableString(value) {
9730
+ return value === null ? null : readString(value);
9731
+ }
9732
+ function readDigest(value) {
9733
+ const digest = readString(value);
9734
+ if (!/^[0-9a-fA-F]{64}$/.test(digest)) {
9735
+ throw invalidManifest();
9736
+ }
9737
+ return digest.toLowerCase();
9738
+ }
9739
+ function readEnum(value, allowed) {
9740
+ if (typeof value !== "string" || !allowed.includes(value)) {
9741
+ throw invalidManifest();
9742
+ }
9743
+ return value;
9744
+ }
9745
+ function ensureUnique(values, id) {
9746
+ if (new Set(values.map(id)).size !== values.length) {
9747
+ throw invalidManifest();
9748
+ }
9749
+ }
9750
+ function invalidManifest() {
9751
+ return new TypeError("ICE3 preview packet manifest is invalid");
9752
+ }
9753
+ function isRecord(value) {
9754
+ return typeof value === "object" && value !== null && !Array.isArray(value);
9755
+ }
9756
+ function readSafeU64(view, offset) {
9757
+ const value = view.getBigUint64(offset, true);
9758
+ if (value > BigInt(Number.MAX_SAFE_INTEGER)) {
9759
+ throw new RangeError("ICE3 preview packet length exceeds safe integer range");
9760
+ }
9761
+ return Number(value);
9762
+ }
9763
+ function concatBytes(left, right) {
9764
+ const output = new Uint8Array(left.length + right.length);
9765
+ output.set(left);
9766
+ output.set(right, left.length);
9767
+ return output;
9768
+ }
9769
+ function equalBytes(left, right) {
9770
+ return left.length === right.length && left.every((value, index) => value === right[index]);
9771
+ }
9772
+ var SHA256_CONSTANTS = new Uint32Array([
9773
+ 1116352408,
9774
+ 1899447441,
9775
+ 3049323471,
9776
+ 3921009573,
9777
+ 961987163,
9778
+ 1508970993,
9779
+ 2453635748,
9780
+ 2870763221,
9781
+ 3624381080,
9782
+ 310598401,
9783
+ 607225278,
9784
+ 1426881987,
9785
+ 1925078388,
9786
+ 2162078206,
9787
+ 2614888103,
9788
+ 3248222580,
9789
+ 3835390401,
9790
+ 4022224774,
9791
+ 264347078,
9792
+ 604807628,
9793
+ 770255983,
9794
+ 1249150122,
9795
+ 1555081692,
9796
+ 1996064986,
9797
+ 2554220882,
9798
+ 2821834349,
9799
+ 2952996808,
9800
+ 3210313671,
9801
+ 3336571891,
9802
+ 3584528711,
9803
+ 113926993,
9804
+ 338241895,
9805
+ 666307205,
9806
+ 773529912,
9807
+ 1294757372,
9808
+ 1396182291,
9809
+ 1695183700,
9810
+ 1986661051,
9811
+ 2177026350,
9812
+ 2456956037,
9813
+ 2730485921,
9814
+ 2820302411,
9815
+ 3259730800,
9816
+ 3345764771,
9817
+ 3516065817,
9818
+ 3600352804,
9819
+ 4094571909,
9820
+ 275423344,
9821
+ 430227734,
9822
+ 506948616,
9823
+ 659060556,
9824
+ 883997877,
9825
+ 958139571,
9826
+ 1322822218,
9827
+ 1537002063,
9828
+ 1747873779,
9829
+ 1955562222,
9830
+ 2024104815,
9831
+ 2227730452,
9832
+ 2361852424,
9833
+ 2428436474,
9834
+ 2756734187,
9835
+ 3204031479,
9836
+ 3329325298
9837
+ ]);
9838
+ function sha256(input) {
9839
+ const bitLength = input.length * 8;
9840
+ const paddedLength = Math.ceil((input.length + 9) / 64) * 64;
9841
+ const data = new Uint8Array(paddedLength);
9842
+ data.set(input);
9843
+ data[input.length] = 128;
9844
+ const view = new DataView(data.buffer);
9845
+ view.setUint32(paddedLength - 8, Math.floor(bitLength / 4294967296));
9846
+ view.setUint32(paddedLength - 4, bitLength >>> 0);
9847
+ const hash = new Uint32Array([
9848
+ 1779033703,
9849
+ 3144134277,
9850
+ 1013904242,
9851
+ 2773480762,
9852
+ 1359893119,
9853
+ 2600822924,
9854
+ 528734635,
9855
+ 1541459225
9856
+ ]);
9857
+ const words = new Uint32Array(64);
9858
+ for (let offset = 0; offset < data.length; offset += 64) {
9859
+ for (let index = 0; index < 16; index += 1) {
9860
+ words[index] = view.getUint32(offset + index * 4);
9861
+ }
9862
+ for (let index = 16; index < 64; index += 1) {
9863
+ const s0 = rotateRight(words[index - 15], 7) ^ rotateRight(words[index - 15], 18) ^ words[index - 15] >>> 3;
9864
+ const s1 = rotateRight(words[index - 2], 17) ^ rotateRight(words[index - 2], 19) ^ words[index - 2] >>> 10;
9865
+ words[index] = words[index - 16] + s0 + words[index - 7] + s1 >>> 0;
9866
+ }
9867
+ let [a, b, c, d, e, f, g, h] = hash;
9868
+ for (let index = 0; index < 64; index += 1) {
9869
+ const sum1 = rotateRight(e, 6) ^ rotateRight(e, 11) ^ rotateRight(e, 25);
9870
+ const choice = e & f ^ ~e & g;
9871
+ const temp1 = h + sum1 + choice + SHA256_CONSTANTS[index] + words[index] >>> 0;
9872
+ const sum0 = rotateRight(a, 2) ^ rotateRight(a, 13) ^ rotateRight(a, 22);
9873
+ const majority = a & b ^ a & c ^ b & c;
9874
+ const temp2 = sum0 + majority >>> 0;
9875
+ h = g;
9876
+ g = f;
9877
+ f = e;
9878
+ e = d + temp1 >>> 0;
9879
+ d = c;
9880
+ c = b;
9881
+ b = a;
9882
+ a = temp1 + temp2 >>> 0;
9883
+ }
9884
+ hash[0] = hash[0] + a >>> 0;
9885
+ hash[1] = hash[1] + b >>> 0;
9886
+ hash[2] = hash[2] + c >>> 0;
9887
+ hash[3] = hash[3] + d >>> 0;
9888
+ hash[4] = hash[4] + e >>> 0;
9889
+ hash[5] = hash[5] + f >>> 0;
9890
+ hash[6] = hash[6] + g >>> 0;
9891
+ hash[7] = hash[7] + h >>> 0;
9892
+ }
9893
+ const output = new Uint8Array(32);
9894
+ const outputView = new DataView(output.buffer);
9895
+ for (let index = 0; index < hash.length; index += 1) {
9896
+ outputView.setUint32(index * 4, hash[index], false);
9897
+ }
9898
+ return output;
9899
+ }
9900
+ function rotateRight(value, bits) {
9901
+ return value >>> bits | value << 32 - bits;
9902
+ }
9903
+
7446
9904
  // packages/assets/dist/ICETilesetReader.js
7447
9905
  var IDENTITY = [
7448
9906
  1,
@@ -7715,7 +10173,11 @@ function parseTransform(value) {
7715
10173
  return matrix;
7716
10174
  }
7717
10175
  function assertContains(parent, child, id) {
7718
- if ([0, 1, 2].some((axis) => child.min[axis] < parent.min[axis] - EPSILON || child.max[axis] > parent.max[axis] + EPSILON)) {
10176
+ if ([0, 1, 2].some((axis) => {
10177
+ const scale = Math.max(1, Math.abs(parent.min[axis]), Math.abs(parent.max[axis]), Math.abs(child.min[axis]), Math.abs(child.max[axis]));
10178
+ const tolerance = EPSILON * scale;
10179
+ return child.min[axis] < parent.min[axis] - tolerance || child.max[axis] > parent.max[axis] + tolerance;
10180
+ })) {
7719
10181
  throw invalid(`child bounds escape parent tile ${id}`);
7720
10182
  }
7721
10183
  }
@@ -7828,7 +10290,7 @@ var ICEAssetLoader = class {
7828
10290
  throw new Error(`ICE3_ASSET_FETCH_FAILED: HTTP ${response.status}`);
7829
10291
  }
7830
10292
  const bytes = await response.arrayBuffer();
7831
- const snapshot = readPreparedGLB(bytes);
10293
+ const snapshot = readPreparedGLB(bytes, { baseUrl: url });
7832
10294
  return { bytes, snapshot };
7833
10295
  }
7834
10296
  };
@@ -8618,7 +11080,7 @@ var ICETilesetHandle = class extends ICEEventDispatcher {
8618
11080
  adaptivePlanningSignature = "";
8619
11081
  renderBatchingMode;
8620
11082
  visibleCoverageIds = /* @__PURE__ */ new Set();
8621
- constructor(engine, snapshot, camera, scheduler, fetch, maxConcurrentTiles, maxCommitsPerFrame, maxScreenSpaceErrorPixels, lodSettleDelayMs, renderBatching, callerSignal) {
11083
+ constructor(engine, snapshot, camera, scheduler, fetch2, maxConcurrentTiles, maxCommitsPerFrame, maxScreenSpaceErrorPixels, lodSettleDelayMs, renderBatching, callerSignal) {
8622
11084
  super();
8623
11085
  this.engine = engine;
8624
11086
  this.snapshot = snapshot;
@@ -8649,7 +11111,7 @@ var ICETilesetHandle = class extends ICEEventDispatcher {
8649
11111
  });
8650
11112
  }
8651
11113
  this.assetLoader = new ICEAssetLoader(engine, {
8652
- fetch: (input, init) => fetch(input, {
11114
+ fetch: (input, init) => fetch2(input, {
8653
11115
  ...init ?? {},
8654
11116
  signal: this.abortController.signal
8655
11117
  })
@@ -9212,12 +11674,6 @@ var ICETilesetHandle = class extends ICEEventDispatcher {
9212
11674
  this.assetLoader.clear(uri);
9213
11675
  this.states.set(tileId, "evicted");
9214
11676
  this.emit("TILESET_TILE_UNLOADED", tileId);
9215
- const remainingIds = /* @__PURE__ */ new Set();
9216
- collectNodeAndSelectionIDs(this.scene, remainingIds);
9217
- const selection = this.engine.getSelection();
9218
- if (selection.some((id) => !remainingIds.has(id))) {
9219
- this.engine.setSelection(selection.filter((id) => remainingIds.has(id)));
9220
- }
9221
11677
  this.engine.requestRender("tileset-tile-unloaded");
9222
11678
  }
9223
11679
  hasPendingVisibleTargets() {
@@ -9271,7 +11727,6 @@ var ICETilesetHandle = class extends ICEEventDispatcher {
9271
11727
  this.failedLodFallbacks.delete(fallbackId);
9272
11728
  }
9273
11729
  this.visibleCoverageIds = targetIds;
9274
- this.reconcileSelectionVisibility();
9275
11730
  this.engine.requestRender("tileset-visible-target-committed");
9276
11731
  if (!this.readySettled) {
9277
11732
  this.readySettled = true;
@@ -9309,17 +11764,6 @@ var ICETilesetHandle = class extends ICEEventDispatcher {
9309
11764
  }
9310
11765
  return visible;
9311
11766
  }
9312
- reconcileSelectionVisibility() {
9313
- const visibleIds = /* @__PURE__ */ new Set();
9314
- for (const child of this.scene.children) {
9315
- collectVisibleNodeAndSelectionIDs(child, visibleIds);
9316
- }
9317
- const selection = this.engine.getSelection();
9318
- const visibleSelection = selection.filter((id) => visibleIds.has(id));
9319
- if (visibleSelection.length !== selection.length) {
9320
- this.engine.setSelection(visibleSelection);
9321
- }
9322
- }
9323
11767
  rejectReadyIfVisibleSetFailed() {
9324
11768
  if (this.readySettled || !this.workSet)
9325
11769
  return;
@@ -9728,34 +12172,6 @@ function collectNodeIDs(node, output) {
9728
12172
  for (const child of node.children)
9729
12173
  collectNodeIDs(child, output);
9730
12174
  }
9731
- function collectNodeAndSelectionIDs(node, output) {
9732
- output.add(node.id);
9733
- if (node instanceof ICEMesh)
9734
- output.add(node.selectionId);
9735
- if (node instanceof ICERenderBatch) {
9736
- for (const selectionId of node.semanticSelectionIds) {
9737
- output.add(selectionId);
9738
- }
9739
- }
9740
- for (const child of node.children) {
9741
- collectNodeAndSelectionIDs(child, output);
9742
- }
9743
- }
9744
- function collectVisibleNodeAndSelectionIDs(node, output) {
9745
- if (!node.visible)
9746
- return;
9747
- output.add(node.id);
9748
- if (node instanceof ICEMesh)
9749
- output.add(node.selectionId);
9750
- if (node instanceof ICERenderBatch) {
9751
- for (const selectionId of node.semanticSelectionIds) {
9752
- output.add(selectionId);
9753
- }
9754
- }
9755
- for (const child of node.children) {
9756
- collectVisibleNodeAndSelectionIDs(child, output);
9757
- }
9758
- }
9759
12175
  function throwIfAborted(signal) {
9760
12176
  if (signal?.aborted)
9761
12177
  throw abortedTilesetError();
@@ -9766,6 +12182,72 @@ function isAbortError(error) {
9766
12182
  function abortedTilesetError() {
9767
12183
  return new Error("ICE3_TILESET_ABORTED: Tileset loading was aborted");
9768
12184
  }
12185
+
12186
+ // packages/runtime/dist/asset/ICEPreviewPacketLoader.js
12187
+ var ICEPreviewPacketLoader = class {
12188
+ fetcher;
12189
+ pinGeometry;
12190
+ geometries = /* @__PURE__ */ new Map();
12191
+ publishedScene = null;
12192
+ constructor(options = {}) {
12193
+ const normalized = typeof options === "function" ? { fetch: options } : options;
12194
+ this.fetcher = normalized.fetch ?? globalThis.fetch.bind(globalThis);
12195
+ this.pinGeometry = normalized.pinGeometry;
12196
+ }
12197
+ get scene() {
12198
+ return this.publishedScene;
12199
+ }
12200
+ get currentGeometries() {
12201
+ return this.geometries;
12202
+ }
12203
+ async load(url, current) {
12204
+ let bytes;
12205
+ try {
12206
+ const response = await this.fetcher(url, {
12207
+ headers: {
12208
+ accept: "application/vnd.ice3.preview-geometry; version=1"
12209
+ }
12210
+ });
12211
+ if (!response.ok) {
12212
+ return { kind: "fallback", reason: "packet-unavailable" };
12213
+ }
12214
+ bytes = new Uint8Array(await response.arrayBuffer());
12215
+ } catch {
12216
+ return { kind: "fallback", reason: "packet-unavailable" };
12217
+ }
12218
+ let incoming;
12219
+ try {
12220
+ incoming = parsePreviewGeometryPacket(bytes);
12221
+ } catch {
12222
+ return { kind: "fallback", reason: "packet-invalid" };
12223
+ }
12224
+ let packet = incoming;
12225
+ if (incoming.parentRevision !== null) {
12226
+ if (!current || incoming.parentRevision !== current.revision) {
12227
+ return { kind: "fallback", reason: "packet-revision-mismatch" };
12228
+ }
12229
+ packet = applyPreviewGeometryDelta(current, incoming);
12230
+ }
12231
+ let built;
12232
+ try {
12233
+ built = buildPreviewPacketScene(packet, this.geometries);
12234
+ if (this.pinGeometry) {
12235
+ for (const [geometryId, update] of built.geometryUpdates) {
12236
+ const geometry = built.geometries.get(geometryId);
12237
+ if (!geometry) {
12238
+ throw new Error("ICE3_PREVIEW_PACKET_INVALID: geometry is missing");
12239
+ }
12240
+ this.pinGeometry(geometry, update);
12241
+ }
12242
+ }
12243
+ } catch {
12244
+ return { kind: "fallback", reason: "packet-invalid" };
12245
+ }
12246
+ this.geometries = built.geometries;
12247
+ this.publishedScene = built.scene;
12248
+ return { kind: "packet", packet };
12249
+ }
12250
+ };
9769
12251
  export {
9770
12252
  ICE3,
9771
12253
  ICEAssetLoader,
@@ -9778,13 +12260,31 @@ export {
9778
12260
  ICENode,
9779
12261
  ICEOrbitController,
9780
12262
  ICEPerspectiveCamera,
12263
+ ICEPreviewPacketLoader,
9781
12264
  ICEScene,
9782
12265
  ICEScreenLayer,
9783
12266
  ICETilesetHandle,
9784
12267
  ICETilesetLoader,
9785
12268
  ICETypeRegistry,
9786
12269
  ICEWorldLayer,
12270
+ collectSceneItems,
9787
12271
  deserializeScene,
9788
12272
  serializeScene
9789
12273
  };
12274
+ //! 阴影视锥:按场景包围盒拟合方向光的正交投影。
12275
+ //!
12276
+ //! 早期实现把光照视锥写死成 860 米见方、960 米深。两个后果都很贵:
12277
+ //! 2048 的深度图摊到 860 米是每纹素 0.42 米(薄墙的影子直接消失),
12278
+ //! 而深度偏差 0.0015 在 960 米的跨度上等于 1.4 米,短影子被整段吃掉。
12279
+ //! 视锥贴合场景后,偏差回到"厘米"量级,贴图分辨率也才有意义。
12280
+ //! 方向光阴影贴图。
12281
+ //! 一次深度 pass 把场景从光源方向渲染进深度纹理,主 pass 再按世界坐标比较深度。
12282
+ //! 只处理单方向光、单张贴图;视锥按场景包围盒拟合(见 ShadowFrustum)。
12283
+ //! 场景包围盒:给阴影视锥用。
12284
+ //! 引擎里没有现成的场景 bounds,而阴影视锥必须贴合实际内容——写死范围会让
12285
+ //! 深度精度和贴图分辨率一起浪费(见 ShadowFrustum)。这里按"几何局部包围盒
12286
+ //! 缓存 + 世界矩阵变换"累加,避免每帧扫描几十万个顶点。
12287
+ //! 相机深度通道:屏幕空间遮蔽的来源。
12288
+ //! 从**相机**方向渲染一遍深度。必须覆盖全部几何路径——瓦片走渲染批次,其余走实例化
12289
+ //! 批次;只画一条会让深度图大半是空的(SPEC-007)。
9790
12290
  //# sourceMappingURL=index.js.map