ice3 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,6 +382,7 @@ 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
  }
@@ -386,15 +393,18 @@ var ICEBasicMaterial = class {
386
393
  typeId = "ice3:BasicMaterial";
387
394
  color;
388
395
  edgeColor;
396
+ shading;
389
397
  constructor(options) {
390
398
  this.color = options.color;
391
399
  this.edgeColor = options.edgeColor;
400
+ this.shading = options.shading;
392
401
  }
393
402
  toJSON() {
394
403
  return {
395
404
  typeId: this.typeId,
396
405
  color: this.color,
397
- ...this.edgeColor ? { edgeColor: this.edgeColor } : {}
406
+ ...this.edgeColor ? { edgeColor: this.edgeColor } : {},
407
+ ...this.shading ? { shading: this.shading } : {}
398
408
  };
399
409
  }
400
410
  };
@@ -426,6 +436,14 @@ var ICEPerspectiveCamera = class {
426
436
  this.lookTarget = new ICEVector3(...target);
427
437
  return this;
428
438
  }
439
+ setClipPlanes(near, far) {
440
+ if (!Number.isFinite(near) || !Number.isFinite(far) || near <= 0 || far <= near) {
441
+ throw new RangeError("Camera near/far values are invalid");
442
+ }
443
+ this.near = near;
444
+ this.far = far;
445
+ return this;
446
+ }
429
447
  getPosition() {
430
448
  return this.eye;
431
449
  }
@@ -765,6 +783,7 @@ var ICEMesh = class _ICEMesh extends ICENode {
765
783
  static fromJSON(value) {
766
784
  const geometry = value.geometry;
767
785
  const material = value.material;
786
+ const shading = material.shading === "directional" || material.shading === "flat" || material.shading === "water" ? material.shading : void 0;
768
787
  return new _ICEMesh({
769
788
  id: value.id,
770
789
  displayName: typeof value.displayName === "string" ? value.displayName : value.id,
@@ -781,7 +800,8 @@ var ICEMesh = class _ICEMesh extends ICENode {
781
800
  }),
782
801
  material: new ICEBasicMaterial({
783
802
  color: String(material.color),
784
- ...typeof material.edgeColor === "string" ? { edgeColor: material.edgeColor } : {}
803
+ ...typeof material.edgeColor === "string" ? { edgeColor: material.edgeColor } : {},
804
+ ...shading ? { shading } : {}
785
805
  })
786
806
  });
787
807
  }
@@ -909,17 +929,26 @@ function isRenderBatch(node) {
909
929
  }
910
930
 
911
931
  // 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;
932
+ var LIGHT_DIRECTION = [-0.35, 0.85, 0.4];
933
+ var LIGHTING = Object.freeze({
934
+ ambient: 0.72,
935
+ direct: 0.72,
936
+ sky: 0.18,
937
+ minimum: 0.92,
938
+ maximum: 1.42
939
+ });
940
+ var VERTEX_SOURCE = `#version 300 es
941
+ in vec3 a_position;
942
+ in vec3 a_color;
943
+ in vec4 a_instanceColumn0;
944
+ in vec4 a_instanceColumn1;
945
+ in vec4 a_instanceColumn2;
946
+ in vec4 a_instanceColumn3;
919
947
  uniform mat4 u_worldViewProjection;
920
948
  uniform vec2 u_clipOffset;
921
949
  uniform float u_useInstancing;
922
- varying vec3 v_color;
950
+ out vec3 v_color;
951
+ out vec3 v_worldPosition;
923
952
 
924
953
  void main() {
925
954
  mat4 instanceMatrix = mat4(
@@ -937,17 +966,122 @@ void main() {
937
966
  gl_Position = u_worldViewProjection * position;
938
967
  gl_Position.xy += u_clipOffset * gl_Position.w;
939
968
  v_color = a_color;
969
+ v_worldPosition = position.xyz;
940
970
  }
941
971
  `;
942
- var FRAGMENT_SOURCE = `
972
+ var FRAGMENT_SOURCE = `#version 300 es
943
973
  precision highp float;
944
974
  uniform vec4 u_color;
975
+ uniform float u_pickMode;
945
976
  uniform float u_useVertexColor;
946
- varying vec3 v_color;
977
+ uniform float u_useDirectionalShading;
978
+ uniform float u_waterShading;
979
+ uniform vec3 u_cameraPosition;
980
+ uniform sampler2D u_shadowMap;
981
+ uniform mat4 u_lightViewProjection;
982
+ uniform float u_shadowEnabled;
983
+ in vec3 v_color;
984
+ in vec3 v_worldPosition;
985
+ out vec4 outputColor;
947
986
 
948
987
  void main() {
949
- gl_FragColor = vec4(
950
- u_color.rgb * mix(vec3(1.0), v_color, step(0.5, u_useVertexColor)),
988
+ if (u_pickMode > 0.5) {
989
+ outputColor = u_color;
990
+ return;
991
+ }
992
+ vec3 color = u_color.rgb
993
+ * mix(vec3(1.0), v_color, step(0.5, u_useVertexColor));
994
+ vec3 normal = normalize(cross(
995
+ dFdx(v_worldPosition),
996
+ dFdy(v_worldPosition)
997
+ ));
998
+ // \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
999
+ // \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
1000
+ vec3 lightDirection = normalize(vec3(${LIGHT_DIRECTION.join(", ")}));
1001
+ float ndl = dot(normal, lightDirection);
1002
+ float key = max(ndl, 0.0) + 0.35 * max(-ndl, 0.0);
1003
+ float sky = 0.5 + 0.5 * normal.y;
1004
+ // \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
1005
+ // \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
1006
+ float shade = clamp(
1007
+ ${LIGHTING.ambient} + ${LIGHTING.direct} * key + ${LIGHTING.sky} * sky,
1008
+ ${LIGHTING.minimum},
1009
+ ${LIGHTING.maximum}
1010
+ );
1011
+ vec3 lit = color * mix(1.0, shade, u_useDirectionalShading);
1012
+
1013
+ // \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
1014
+ // \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
1015
+ vec3 fine = floor(v_worldPosition * 4.5);
1016
+ vec3 coarse = floor(v_worldPosition * 0.42);
1017
+ float grain = fract(sin(dot(fine, vec3(12.9898, 78.233, 37.719))) * 43758.5453);
1018
+ float stain = fract(sin(dot(coarse, vec3(39.346, 11.135, 83.155))) * 24634.6345);
1019
+ // \u4F4E\u9891\u6C61\u6E0D\u8D1F\u8D23\u5927\u9762\u79EF\u4E0D\u5300\uFF0C\u9AD8\u9891\u9897\u7C92\u8D1F\u8D23\u8FD1\u770B\u7684\u624B\u611F\u3002
1020
+ lit *= 0.94 + 0.06 * grain + 0.05 * (stain - 0.5);
1021
+
1022
+ // Liquid surfaces use an analytic Fresnel reflection plus shallow ripple
1023
+ // normals. The effect is opt-in through material shading and remains
1024
+ // independent from any application-specific material naming.
1025
+ if (u_waterShading > 0.5) {
1026
+ float waveA = sin(v_worldPosition.x * 1.1
1027
+ + sin(v_worldPosition.z * 0.43) * 2.0);
1028
+ float waveB = sin(v_worldPosition.z * 1.7
1029
+ + v_worldPosition.x * 0.31);
1030
+ float waveC = sin((v_worldPosition.x + v_worldPosition.z) * 2.3);
1031
+ vec3 rippleNormal = normalize(vec3(
1032
+ waveA * 0.09 + waveC * 0.025,
1033
+ 1.0,
1034
+ waveB * 0.09 - waveC * 0.02
1035
+ ));
1036
+ vec3 viewDirection = normalize(u_cameraPosition - v_worldPosition);
1037
+ float facing = clamp(dot(rippleNormal, viewDirection), 0.0, 1.0);
1038
+ float fresnel = pow(1.0 - facing, 2.4);
1039
+ vec3 reflectedSky = vec3(0.22, 0.34, 0.36);
1040
+ vec3 surfaceColor = mix(
1041
+ color,
1042
+ reflectedSky,
1043
+ 0.30 + 0.30 * fresnel
1044
+ );
1045
+ surfaceColor *= 0.92 + 0.09 * waveA + 0.05 * waveB;
1046
+ vec3 halfDirection = normalize(lightDirection + viewDirection);
1047
+ float sparkle = pow(
1048
+ max(dot(rippleNormal, halfDirection), 0.0),
1049
+ 36.0
1050
+ );
1051
+ lit = mix(lit, surfaceColor * shade, 0.92)
1052
+ + vec3(0.42, 0.52, 0.54) * sparkle * 0.55;
1053
+ }
1054
+
1055
+ // \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
1056
+ if (u_shadowEnabled > 0.5) {
1057
+ vec4 lightSpace = u_lightViewProjection * vec4(v_worldPosition, 1.0);
1058
+ vec3 projected = lightSpace.xyz / max(lightSpace.w, 1e-5) * 0.5 + 0.5;
1059
+ if (projected.x > 0.002 && projected.x < 0.998
1060
+ && projected.y > 0.002 && projected.y < 0.998
1061
+ && projected.z > 0.0 && projected.z < 1.0) {
1062
+ // \u659C\u9762\u5BF9\u6DF1\u5EA6\u7684\u8BEF\u5DEE\u6700\u654F\u611F\uFF0C\u6309 N\xB7L \u653E\u5927\u504F\u79FB\uFF1A
1063
+ // \u56FA\u5B9A\u5C0F\u504F\u79FB\u5728\u8FD9\u5957 960 \u7C73\u6DF1\u7684\u5149\u9525\u91CC\u7B49\u4E8E\u51E0\u7C73\uFF0C\u5899\u9762\u4F1A\u81EA\u5DF1\u628A\u81EA\u5DF1\u906E\u4F4F\uFF08\u9634\u5F71\u75E4\u75AE\uFF09\u3002
1064
+ float ndlShadow = max(dot(normal, lightDirection), 0.0);
1065
+ // \u504F\u79FB\u6309\u5149\u9525\u6DF1\u5EA6\uFF08\u8FD1 -260 / \u8FDC 700\uFF0C\u5171 960 \u7C73\uFF09\u6362\u7B97\uFF1A
1066
+ // 0.006 \u5BF9\u5E94\u7EA6 5.8 \u7C73\uFF0C\u6B63\u597D\u538B\u6389\u6DF1\u5EA6\u56FE\u4E0E\u9010\u50CF\u7D20\u6295\u5F71\u4E4B\u95F4\u7684\u7CFB\u7EDF\u8BEF\u5DEE\uFF0C
1067
+ // \u540C\u65F6\u8FD8\u80FD\u7559\u4F4F\u5341\u51E0\u7C73\u9AD8\u6784\u7B51\u7269\u7684\u6295\u5F71\u3002
1068
+ float bias = 0.0015 + 0.0025 * (1.0 - ndlShadow);
1069
+ float lit2 = 0.0;
1070
+ vec2 texel = vec2(1.0 / 2048.0);
1071
+ for (int dx = -1; dx <= 1; dx++) {
1072
+ for (int dy = -1; dy <= 1; dy++) {
1073
+ float depth = texture(u_shadowMap, projected.xy + vec2(float(dx), float(dy)) * texel).r;
1074
+ lit2 += projected.z - bias > depth ? 0.0 : 1.0;
1075
+ }
1076
+ }
1077
+ lit *= mix(0.68, 1.0, lit2 / 9.0);
1078
+
1079
+ }
1080
+ }
1081
+ // \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
1082
+ // \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
1083
+ outputColor = vec4(
1084
+ pow(max(lit, vec3(0.0)), vec3(1.0 / 2.2)),
951
1085
  u_color.a
952
1086
  );
953
1087
  }
@@ -963,7 +1097,14 @@ var WebGL2Program = class _WebGL2Program {
963
1097
  colorLocation;
964
1098
  clipOffsetLocation;
965
1099
  vertexColorLocation;
1100
+ directionalShadingLocation;
1101
+ waterShadingLocation;
1102
+ cameraPositionLocation;
1103
+ shadowMapLocation;
1104
+ lightMatrixLocation;
1105
+ shadowEnabledLocation;
966
1106
  instancingLocation;
1107
+ pickModeLocation;
967
1108
  constructor(gl) {
968
1109
  this.gl = gl;
969
1110
  const program = this.createProgram(VERTEX_SOURCE, FRAGMENT_SOURCE);
@@ -971,8 +1112,15 @@ var WebGL2Program = class _WebGL2Program {
971
1112
  const colorLocation = gl.getUniformLocation(program, "u_color");
972
1113
  const clipOffsetLocation = gl.getUniformLocation(program, "u_clipOffset");
973
1114
  const vertexColorLocation = gl.getUniformLocation(program, "u_useVertexColor");
1115
+ const directionalShadingLocation = gl.getUniformLocation(program, "u_useDirectionalShading");
1116
+ const waterShadingLocation = gl.getUniformLocation(program, "u_waterShading");
1117
+ const cameraPositionLocation = gl.getUniformLocation(program, "u_cameraPosition");
974
1118
  const instancingLocation = gl.getUniformLocation(program, "u_useInstancing");
975
- if (!matrixLocation || !colorLocation || !clipOffsetLocation || !vertexColorLocation || !instancingLocation) {
1119
+ const pickModeLocation = gl.getUniformLocation(program, "u_pickMode");
1120
+ const shadowMapLocation = gl.getUniformLocation(program, "u_shadowMap");
1121
+ const lightMatrixLocation = gl.getUniformLocation(program, "u_lightViewProjection");
1122
+ const shadowEnabledLocation = gl.getUniformLocation(program, "u_shadowEnabled");
1123
+ if (!matrixLocation || !colorLocation || !clipOffsetLocation || !vertexColorLocation || !directionalShadingLocation || !waterShadingLocation || !cameraPositionLocation || !instancingLocation || !pickModeLocation) {
976
1124
  gl.deleteProgram(program);
977
1125
  throw new ICEEngineError("ICE3_SHADER_LINK_FAILED", "ICE3_SHADER_LINK_FAILED: shader uniforms are missing");
978
1126
  }
@@ -981,7 +1129,21 @@ var WebGL2Program = class _WebGL2Program {
981
1129
  this.colorLocation = colorLocation;
982
1130
  this.clipOffsetLocation = clipOffsetLocation;
983
1131
  this.vertexColorLocation = vertexColorLocation;
1132
+ this.directionalShadingLocation = directionalShadingLocation;
1133
+ this.waterShadingLocation = waterShadingLocation;
1134
+ this.cameraPositionLocation = cameraPositionLocation;
1135
+ this.shadowMapLocation = shadowMapLocation;
1136
+ this.lightMatrixLocation = lightMatrixLocation;
1137
+ this.shadowEnabledLocation = shadowEnabledLocation;
1138
+ this.shadowMapLocation = shadowMapLocation;
1139
+ this.lightMatrixLocation = lightMatrixLocation;
1140
+ this.shadowEnabledLocation = shadowEnabledLocation;
984
1141
  this.instancingLocation = instancingLocation;
1142
+ this.pickModeLocation = pickModeLocation;
1143
+ if (shadowMapLocation) {
1144
+ gl.useProgram(program);
1145
+ gl.uniform1i(shadowMapLocation, 3);
1146
+ }
985
1147
  }
986
1148
  use() {
987
1149
  this.gl.useProgram(this.program);
@@ -998,9 +1160,35 @@ var WebGL2Program = class _WebGL2Program {
998
1160
  setVertexColors(enabled) {
999
1161
  this.gl.uniform1f(this.vertexColorLocation, enabled ? 1 : 0);
1000
1162
  }
1163
+ /** 绑定阴影贴图与光源矩阵;关闭时着色器跳过采样。 */
1164
+ setShadow(enabled, texture, matrix, unit = 3) {
1165
+ const gl = this.gl;
1166
+ gl.uniform1f(this.shadowEnabledLocation, enabled && texture && matrix ? 1 : 0);
1167
+ if (!enabled || !texture || !matrix)
1168
+ return;
1169
+ gl.activeTexture(gl.TEXTURE0 + unit);
1170
+ gl.bindTexture(gl.TEXTURE_2D, texture);
1171
+ if (this.shadowMapLocation)
1172
+ gl.uniform1i(this.shadowMapLocation, unit);
1173
+ if (this.lightMatrixLocation)
1174
+ gl.uniformMatrix4fv(this.lightMatrixLocation, false, matrix);
1175
+ gl.activeTexture(gl.TEXTURE0);
1176
+ }
1177
+ setDirectionalShading(enabled) {
1178
+ this.gl.uniform1f(this.directionalShadingLocation, enabled ? 1 : 0);
1179
+ }
1180
+ setWaterShading(enabled) {
1181
+ this.gl.uniform1f(this.waterShadingLocation, enabled ? 1 : 0);
1182
+ }
1183
+ setCameraPosition(position) {
1184
+ this.gl.uniform3f(this.cameraPositionLocation, ...position);
1185
+ }
1001
1186
  setInstancing(enabled) {
1002
1187
  this.gl.uniform1f(this.instancingLocation, enabled ? 1 : 0);
1003
1188
  }
1189
+ setPickMode(enabled) {
1190
+ this.gl.uniform1f(this.pickModeLocation, enabled ? 1 : 0);
1191
+ }
1004
1192
  dispose() {
1005
1193
  this.gl.deleteProgram(this.program);
1006
1194
  }
@@ -1077,7 +1265,11 @@ function collectInstanceBatches(renderables, output = []) {
1077
1265
  semanticIds: group.map((renderable) => renderable.mesh.selectionId),
1078
1266
  semanticIndices,
1079
1267
  meshIds: group.map((renderable) => renderable.id),
1080
- materialKey: `${material.color}:${material.edgeColor ?? ""}`
1268
+ materialKey: [
1269
+ material.color,
1270
+ material.edgeColor ?? "",
1271
+ material.shading ?? ""
1272
+ ].join(":")
1081
1273
  });
1082
1274
  }
1083
1275
  }
@@ -1199,8 +1391,12 @@ var WebGL2GeometryCache = class {
1199
1391
  entries = /* @__PURE__ */ new Map();
1200
1392
  active = /* @__PURE__ */ new Set();
1201
1393
  pinned = /* @__PURE__ */ new Set();
1394
+ objectKeys = /* @__PURE__ */ new WeakMap();
1395
+ legacyKeys = /* @__PURE__ */ new WeakMap();
1396
+ objectKeySequence = 0;
1202
1397
  gl = null;
1203
1398
  uploads = 0;
1399
+ updatedBytes = 0;
1204
1400
  evictions = 0;
1205
1401
  frameId = 0;
1206
1402
  inactiveGraceFrames;
@@ -1214,12 +1410,24 @@ var WebGL2GeometryCache = class {
1214
1410
  }
1215
1411
  retain(gl, geometry) {
1216
1412
  this.gl = gl;
1217
- this.active.add(geometry);
1218
- const existing = this.entries.get(geometry);
1413
+ const key = this.keyFor(geometry);
1414
+ this.active.add(key);
1415
+ const existing = this.entries.get(key);
1219
1416
  if (existing) {
1220
- existing.lastUsedFrame = this.frameId;
1221
- return existing;
1417
+ if (this.canReuse(existing, geometry)) {
1418
+ existing.lastUsedFrame = this.frameId;
1419
+ return existing;
1420
+ }
1421
+ }
1422
+ const next = this.createEntry(gl, geometry, key);
1423
+ if (existing) {
1424
+ this.deleteEntry(existing, false);
1222
1425
  }
1426
+ this.entries.set(key, next);
1427
+ this.uploads += 1;
1428
+ return next;
1429
+ }
1430
+ createEntry(gl, geometry, key) {
1223
1431
  const vertexArray = gl.createVertexArray();
1224
1432
  const positionBuffer = gl.createBuffer();
1225
1433
  const triangleIndexBuffer = gl.createBuffer();
@@ -1260,6 +1468,10 @@ var WebGL2GeometryCache = class {
1260
1468
  }
1261
1469
  gl.bindVertexArray(null);
1262
1470
  const entry = {
1471
+ geometry,
1472
+ geometryId: geometry.identity?.geometryId ?? key,
1473
+ attributeDigest: geometry.identity?.attributeDigest ?? "",
1474
+ topologyDigest: geometry.identity?.topologyDigest ?? "",
1263
1475
  vertexArray,
1264
1476
  positionBuffer,
1265
1477
  triangleIndexBuffer,
@@ -1271,28 +1483,33 @@ var WebGL2GeometryCache = class {
1271
1483
  bytes: geometry.positions.byteLength + geometry.indices.byteLength + (geometry.edgeIndices?.byteLength ?? 0) + (geometry.colors?.byteLength ?? 0) + (geometry.semanticIndices?.byteLength ?? 0),
1272
1484
  lastUsedFrame: this.frameId
1273
1485
  };
1274
- this.entries.set(geometry, entry);
1275
- this.uploads += 1;
1276
1486
  return entry;
1277
1487
  }
1278
1488
  beginFrame() {
1279
1489
  this.frameId += 1;
1280
1490
  this.active = /* @__PURE__ */ new Set();
1281
1491
  }
1282
- pin(gl, geometry) {
1283
- this.pinned.add(geometry);
1492
+ pin(gl, geometry, update) {
1493
+ const key = this.keyFor(geometry);
1494
+ this.pinned.add(key);
1495
+ const existing = this.entries.get(key);
1496
+ if (existing && update && this.sameTopology(existing, geometry)) {
1497
+ this.gl = gl;
1498
+ this.applyPositionUpdate(gl, existing, geometry, update);
1499
+ return existing;
1500
+ }
1284
1501
  const entry = this.retain(gl, geometry);
1285
1502
  this.enforceBudget();
1286
1503
  if (this.totalBytes > this.maxBytes) {
1287
- this.pinned.delete(geometry);
1288
- this.deleteEntry(entry);
1289
- this.entries.delete(geometry);
1504
+ this.pinned.delete(key);
1505
+ this.deleteEntry(entry, false);
1506
+ this.entries.delete(key);
1290
1507
  throw new Error("ICE3_GPU_RESOURCE_FAILED: geometry exceeds the GPU residency budget");
1291
1508
  }
1292
1509
  return entry;
1293
1510
  }
1294
1511
  unpin(geometry) {
1295
- this.pinned.delete(geometry);
1512
+ this.pinned.delete(this.keyFor(geometry));
1296
1513
  }
1297
1514
  endFrame() {
1298
1515
  for (const [geometry, entry] of this.entries) {
@@ -1327,10 +1544,13 @@ var WebGL2GeometryCache = class {
1327
1544
  get uploadCount() {
1328
1545
  return this.uploads;
1329
1546
  }
1547
+ get updateBytes() {
1548
+ return this.updatedBytes;
1549
+ }
1330
1550
  get evictionCount() {
1331
1551
  return this.evictions;
1332
1552
  }
1333
- deleteEntry(entry) {
1553
+ deleteEntry(entry, eviction = true) {
1334
1554
  if (!this.gl)
1335
1555
  return;
1336
1556
  this.gl.deleteBuffer(entry.positionBuffer);
@@ -1339,7 +1559,9 @@ var WebGL2GeometryCache = class {
1339
1559
  this.gl.deleteBuffer(entry.colorBuffer);
1340
1560
  this.gl.deleteBuffer(entry.semanticBuffer);
1341
1561
  this.gl.deleteVertexArray(entry.vertexArray);
1342
- this.evictions += 1;
1562
+ if (eviction) {
1563
+ this.evictions += 1;
1564
+ }
1343
1565
  }
1344
1566
  enforceBudget() {
1345
1567
  let total = this.totalBytes;
@@ -1354,7 +1576,96 @@ var WebGL2GeometryCache = class {
1354
1576
  total -= entry.bytes;
1355
1577
  }
1356
1578
  }
1579
+ applyPositionUpdate(gl, entry, geometry, update) {
1580
+ let updated = 0;
1581
+ try {
1582
+ gl.bindBuffer(gl.ARRAY_BUFFER, entry.positionBuffer);
1583
+ for (const range of update.positionRanges) {
1584
+ if (range.byteOffset % 4 !== 0 || range.byteLength % 4 !== 0 || range.byteOffset + range.byteLength > geometry.positions.byteLength) {
1585
+ throw new Error("invalid position range");
1586
+ }
1587
+ gl.bufferSubData(gl.ARRAY_BUFFER, range.byteOffset, new Uint8Array(geometry.positions.buffer, geometry.positions.byteOffset + range.byteOffset, range.byteLength));
1588
+ updated += range.byteLength;
1589
+ }
1590
+ } catch (error) {
1591
+ throw new Error("ICE3_GPU_RESOURCE_FAILED: geometry update failed", {
1592
+ cause: error
1593
+ });
1594
+ }
1595
+ entry.geometry = geometry;
1596
+ entry.lastUsedFrame = this.frameId;
1597
+ this.updatedBytes += updated;
1598
+ }
1599
+ canReuse(existing, geometry) {
1600
+ if (existing.geometry === geometry) {
1601
+ return true;
1602
+ }
1603
+ if (!this.sameTopology(existing, geometry)) {
1604
+ return false;
1605
+ }
1606
+ const existingDigest = existing.attributeDigest;
1607
+ const nextDigest = geometry.identity?.attributeDigest ?? "";
1608
+ 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;
1609
+ }
1610
+ sameTopology(existing, geometry) {
1611
+ const existingDigest = existing.topologyDigest;
1612
+ const nextDigest = geometry.identity?.topologyDigest ?? "";
1613
+ 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));
1614
+ }
1615
+ keyFor(geometry) {
1616
+ const identity = geometry.identity?.geometryId;
1617
+ if (identity) {
1618
+ return `identity:${identity}`;
1619
+ }
1620
+ const id = geometry.id;
1621
+ if (typeof id === "string" && id.trim()) {
1622
+ const cached = this.legacyKeys.get(geometry);
1623
+ if (cached)
1624
+ return cached;
1625
+ const key2 = [
1626
+ "content",
1627
+ geometry.positions.length,
1628
+ geometry.indices.length,
1629
+ contentFingerprint(geometry.positions),
1630
+ contentFingerprint(geometry.indices),
1631
+ geometry.colors ? contentFingerprint(geometry.colors) : 0,
1632
+ geometry.edgeIndices ? contentFingerprint(geometry.edgeIndices) : 0,
1633
+ geometry.semanticIndices ? contentFingerprint(geometry.semanticIndices) : 0
1634
+ ].join(":");
1635
+ this.legacyKeys.set(geometry, key2);
1636
+ return key2;
1637
+ }
1638
+ const existing = this.objectKeys.get(geometry);
1639
+ if (existing) {
1640
+ return existing;
1641
+ }
1642
+ const key = `object:${this.objectKeySequence}`;
1643
+ this.objectKeySequence += 1;
1644
+ this.objectKeys.set(geometry, key);
1645
+ return key;
1646
+ }
1357
1647
  };
1648
+ function equalTypedArrays(left, right) {
1649
+ return left.length === right.length && left.every((value, index) => value === right[index]);
1650
+ }
1651
+ function equalNumberArrays(left, right) {
1652
+ return left.length === right.length && left.every((value, index) => value === right[index]);
1653
+ }
1654
+ function equalOptionalNumberArrays(left, right) {
1655
+ if (left === void 0 || right === void 0) {
1656
+ return left === right;
1657
+ }
1658
+ return left.length === right.length && left.every((value, index) => value === right[index]);
1659
+ }
1660
+ function contentFingerprint(values) {
1661
+ let hash = 2166136261;
1662
+ const bytes = new Uint8Array(values.buffer, values.byteOffset, values.byteLength);
1663
+ for (const byte of bytes) {
1664
+ hash ^= byte;
1665
+ hash = Math.imul(hash, 16777619) >>> 0;
1666
+ }
1667
+ return hash;
1668
+ }
1358
1669
 
1359
1670
  // packages/renderer/dist/WebGL2PickCodec.js
1360
1671
  var MAX_SELECTION_INDEX = 16777215;
@@ -1561,6 +1872,7 @@ var WebGL2Picker = class {
1561
1872
  gl.depthFunc(gl.LEQUAL);
1562
1873
  gl.disable(gl.BLEND);
1563
1874
  this.program.use();
1875
+ this.program.setPickMode(true);
1564
1876
  this.program.setClipOffset(0, 0);
1565
1877
  this.program.setInstancing(false);
1566
1878
  const aspect = frame.viewport.width / Math.max(frame.viewport.height, 1);
@@ -1706,13 +2018,229 @@ var WebGL2Picker = class {
1706
2018
  }
1707
2019
  };
1708
2020
 
2021
+ // packages/renderer/dist/WebGL2ShadowMap.js
2022
+ var VERTEX_SOURCE3 = `#version 300 es
2023
+ layout(location = 0) in vec3 a_position;
2024
+ layout(location = 3) in mat4 a_instanceMatrix;
2025
+ uniform mat4 u_lightViewProjection;
2026
+ void main() {
2027
+ vec4 world = a_instanceMatrix * vec4(a_position, 1.0);
2028
+ gl_Position = u_lightViewProjection * world;
2029
+ }
2030
+ `;
2031
+ var FRAGMENT_SOURCE3 = `#version 300 es
2032
+ precision highp float;
2033
+ void main() {
2034
+ // \u53EA\u5199\u6DF1\u5EA6\uFF0C\u989C\u8272\u4E0D\u9700\u8981\u3002
2035
+ }
2036
+ `;
2037
+ var LIGHT_DIRECTION2 = [-0.35, 0.85, 0.4];
2038
+ var SHADOW_SIZE = 2048;
2039
+ var ORTHO_EXTENT = 430;
2040
+ var ORTHO_NEAR = -260;
2041
+ var ORTHO_FAR = 700;
2042
+ var WebGL2ShadowMap = class {
2043
+ gl;
2044
+ framebuffer = null;
2045
+ depthTexture = null;
2046
+ program = null;
2047
+ lightMatrixLocation = null;
2048
+ matrixScratch = new Float32Array(16);
2049
+ constructor(gl) {
2050
+ this.gl = gl;
2051
+ this.createResources();
2052
+ }
2053
+ /** 深度纹理,供主着色器采样。 */
2054
+ get texture() {
2055
+ return this.depthTexture;
2056
+ }
2057
+ get matrix() {
2058
+ return this.matrixScratch;
2059
+ }
2060
+ /** 绑定阴影帧缓冲,返回是否成功。 */
2061
+ begin() {
2062
+ const gl = this.gl;
2063
+ if (!this.framebuffer || !this.program)
2064
+ return false;
2065
+ gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer);
2066
+ gl.viewport(0, 0, SHADOW_SIZE, SHADOW_SIZE);
2067
+ gl.disable(gl.POLYGON_OFFSET_FILL);
2068
+ gl.clear(gl.DEPTH_BUFFER_BIT);
2069
+ gl.enable(gl.DEPTH_TEST);
2070
+ gl.enable(gl.CULL_FACE);
2071
+ gl.cullFace(gl.FRONT);
2072
+ gl.useProgram(this.program);
2073
+ gl.uniformMatrix4fv(this.lightMatrixLocation, false, this.matrixScratch);
2074
+ return true;
2075
+ }
2076
+ /** 解绑,恢复默认帧缓冲。 */
2077
+ end(width, height) {
2078
+ const gl = this.gl;
2079
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
2080
+ gl.disable(gl.CULL_FACE);
2081
+ gl.enable(gl.POLYGON_OFFSET_FILL);
2082
+ gl.viewport(0, 0, width, height);
2083
+ }
2084
+ dispose() {
2085
+ const gl = this.gl;
2086
+ if (this.depthTexture)
2087
+ gl.deleteTexture(this.depthTexture);
2088
+ if (this.framebuffer)
2089
+ gl.deleteFramebuffer(this.framebuffer);
2090
+ if (this.program)
2091
+ gl.deleteProgram(this.program);
2092
+ this.depthTexture = null;
2093
+ this.framebuffer = null;
2094
+ this.program = null;
2095
+ }
2096
+ createResources() {
2097
+ const gl = this.gl;
2098
+ this.depthTexture = gl.createTexture();
2099
+ gl.bindTexture(gl.TEXTURE_2D, this.depthTexture);
2100
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.DEPTH_COMPONENT24, SHADOW_SIZE, SHADOW_SIZE, 0, gl.DEPTH_COMPONENT, gl.UNSIGNED_INT, null);
2101
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
2102
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
2103
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
2104
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
2105
+ this.framebuffer = gl.createFramebuffer();
2106
+ gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer);
2107
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, this.depthTexture, 0);
2108
+ gl.drawBuffers([gl.NONE]);
2109
+ gl.readBuffer(gl.NONE);
2110
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
2111
+ this.program = this.createProgram(VERTEX_SOURCE3, FRAGMENT_SOURCE3);
2112
+ this.lightMatrixLocation = this.program ? gl.getUniformLocation(this.program, "u_lightViewProjection") : null;
2113
+ this.updateLightMatrix();
2114
+ }
2115
+ /** 正交光源矩阵:覆盖整片场地,与主着色器的光方向一致。 */
2116
+ updateLightMatrix() {
2117
+ const direction = normalize2(LIGHT_DIRECTION2);
2118
+ const target = [300, 0, 0];
2119
+ const eye = [
2120
+ target[0] + direction[0] * 400,
2121
+ target[1] + direction[1] * 400,
2122
+ target[2] + direction[2] * 400
2123
+ ];
2124
+ const view = lookAt(eye, target, [0, 1, 0]);
2125
+ const projection = orthographic(-ORTHO_EXTENT, ORTHO_EXTENT, -ORTHO_EXTENT, ORTHO_EXTENT, ORTHO_NEAR, ORTHO_FAR);
2126
+ this.matrixScratch.set(multiply(projection, view));
2127
+ }
2128
+ createProgram(vertex, fragment) {
2129
+ const gl = this.gl;
2130
+ const vertexShader = this.compile(gl.VERTEX_SHADER, vertex);
2131
+ const fragmentShader = this.compile(gl.FRAGMENT_SHADER, fragment);
2132
+ if (!vertexShader || !fragmentShader)
2133
+ return null;
2134
+ const program = gl.createProgram();
2135
+ gl.attachShader(program, vertexShader);
2136
+ gl.attachShader(program, fragmentShader);
2137
+ gl.linkProgram(program);
2138
+ gl.deleteShader(vertexShader);
2139
+ gl.deleteShader(fragmentShader);
2140
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
2141
+ gl.deleteProgram(program);
2142
+ return null;
2143
+ }
2144
+ return program;
2145
+ }
2146
+ compile(type, source) {
2147
+ const gl = this.gl;
2148
+ const shader = gl.createShader(type);
2149
+ if (!shader)
2150
+ return null;
2151
+ gl.shaderSource(shader, source);
2152
+ gl.compileShader(shader);
2153
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
2154
+ gl.deleteShader(shader);
2155
+ return null;
2156
+ }
2157
+ return shader;
2158
+ }
2159
+ };
2160
+ function normalize2(value) {
2161
+ const length = Math.hypot(value[0], value[1], value[2]) || 1;
2162
+ return [value[0] / length, value[1] / length, value[2] / length];
2163
+ }
2164
+ function subtract2(a, b) {
2165
+ return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
2166
+ }
2167
+ function cross2(a, b) {
2168
+ return [
2169
+ a[1] * b[2] - a[2] * b[1],
2170
+ a[2] * b[0] - a[0] * b[2],
2171
+ a[0] * b[1] - a[1] * b[0]
2172
+ ];
2173
+ }
2174
+ function dot2(a, b) {
2175
+ return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
2176
+ }
2177
+ function lookAt(eye, target, up) {
2178
+ const forward = normalize2(subtract2(eye, target));
2179
+ const right = normalize2(cross2(up, forward));
2180
+ const cameraUp = cross2(forward, right);
2181
+ return [
2182
+ right[0],
2183
+ right[1],
2184
+ right[2],
2185
+ 0,
2186
+ cameraUp[0],
2187
+ cameraUp[1],
2188
+ cameraUp[2],
2189
+ 0,
2190
+ forward[0],
2191
+ forward[1],
2192
+ forward[2],
2193
+ 0,
2194
+ -dot2(right, eye),
2195
+ -dot2(cameraUp, eye),
2196
+ -dot2(forward, eye),
2197
+ 1
2198
+ ];
2199
+ }
2200
+ function orthographic(left, right, bottom, top, near, far) {
2201
+ const lr = 1 / (left - right);
2202
+ const bt = 1 / (bottom - top);
2203
+ const nf = 1 / (near - far);
2204
+ return [
2205
+ -2 * lr,
2206
+ 0,
2207
+ 0,
2208
+ 0,
2209
+ 0,
2210
+ -2 * bt,
2211
+ 0,
2212
+ 0,
2213
+ 0,
2214
+ 0,
2215
+ 2 * nf,
2216
+ 0,
2217
+ (left + right) * lr,
2218
+ (top + bottom) * bt,
2219
+ (far + near) * nf,
2220
+ 1
2221
+ ];
2222
+ }
2223
+ function multiply(a, b) {
2224
+ const out = new Array(16).fill(0);
2225
+ for (let column = 0; column < 4; column += 1) {
2226
+ for (let row = 0; row < 4; row += 1) {
2227
+ let sum = 0;
2228
+ for (let k = 0; k < 4; k += 1) {
2229
+ sum += (a[k * 4 + row] ?? 0) * (b[column * 4 + k] ?? 0);
2230
+ }
2231
+ out[column * 4 + row] = sum;
2232
+ }
2233
+ }
2234
+ return out;
2235
+ }
2236
+
1709
2237
  // packages/renderer/dist/Color.js
1710
2238
  var COLOR_CACHE_LIMIT = 128;
1711
2239
  var COLOR_CACHE = /* @__PURE__ */ new Map();
1712
2240
  function colorToRgba(color) {
1713
2241
  const normalized = color.startsWith("#") ? color.slice(1) : color;
1714
2242
  const expanded = normalized.length === 3 ? normalized.split("").map((part) => part + part).join("") : normalized;
1715
- if (!/^[0-9a-fA-F]{6}$/.test(expanded)) {
2243
+ if (!/^[0-9a-fA-F]{6}([0-9a-fA-F]{2})?$/.test(expanded)) {
1716
2244
  throw new Error(`Unsupported ICE basic material color: ${color}`);
1717
2245
  }
1718
2246
  const key = expanded.toLowerCase();
@@ -1723,7 +2251,7 @@ function colorToRgba(color) {
1723
2251
  Number.parseInt(expanded.slice(0, 2), 16) / 255,
1724
2252
  Number.parseInt(expanded.slice(2, 4), 16) / 255,
1725
2253
  Number.parseInt(expanded.slice(4, 6), 16) / 255,
1726
- 1
2254
+ expanded.length === 8 ? Number.parseInt(expanded.slice(6, 8), 16) / 255 : 1
1727
2255
  ]);
1728
2256
  if (COLOR_CACHE.size >= COLOR_CACHE_LIMIT) {
1729
2257
  const oldest = COLOR_CACHE.keys().next().value;
@@ -1781,6 +2309,9 @@ var EDGE_PIXEL_OFFSETS = [
1781
2309
  ];
1782
2310
  var SELECTION_RGBA = [255 / 255, 208 / 255, 0];
1783
2311
  var SELECTION_TINT_ALPHA = 0.65;
2312
+ function isTransparentColor(color) {
2313
+ return colorToRgba(color)[3] < 0.999;
2314
+ }
1784
2315
  var WebGL2RendererBackend = class {
1785
2316
  capabilities = {
1786
2317
  backend: "webgl2",
@@ -1794,6 +2325,9 @@ var WebGL2RendererBackend = class {
1794
2325
  onContextLost;
1795
2326
  onContextRestored;
1796
2327
  program = null;
2328
+ shadowMap = null;
2329
+ // 阴影采样开关。定位掉深度偏差之前保持关闭,避免把错误的明暗关系画出来。
2330
+ shadowEnabled = false;
1797
2331
  semanticProgram = null;
1798
2332
  geometryCache = null;
1799
2333
  instanceCache = null;
@@ -1825,6 +2359,8 @@ var WebGL2RendererBackend = class {
1825
2359
  event.preventDefault();
1826
2360
  this.picker = null;
1827
2361
  this.program = null;
2362
+ this.shadowMap?.dispose();
2363
+ this.shadowMap = null;
1828
2364
  this.geometryCache?.clear();
1829
2365
  this.geometryCache = null;
1830
2366
  this.instanceCache?.clear();
@@ -1888,6 +2424,15 @@ var WebGL2RendererBackend = class {
1888
2424
  }
1889
2425
  this.geometryCache.pin(this.gl, geometry);
1890
2426
  }
2427
+ prepareGeometry(geometry, update) {
2428
+ if (!this.gl || !this.geometryCache) {
2429
+ throw new ICEEngineError("ICE3_ENGINE_NOT_READY", "Renderer is not initialized");
2430
+ }
2431
+ this.geometryCache.pin(this.gl, geometry, update);
2432
+ }
2433
+ releaseGeometry(geometry) {
2434
+ this.geometryCache?.unpin(geometry);
2435
+ }
1891
2436
  releaseBatchGeometry(geometry) {
1892
2437
  this.geometryCache?.unpin(geometry);
1893
2438
  }
@@ -1914,6 +2459,10 @@ var WebGL2RendererBackend = class {
1914
2459
  };
1915
2460
  this.program.use();
1916
2461
  this.program.setClipOffset(0, 0);
2462
+ this.program.setDirectionalShading(false);
2463
+ this.program.setWaterShading(false);
2464
+ const cameraPosition = frame.camera.getPosition().toArray();
2465
+ this.program.setCameraPosition(cameraPosition);
1917
2466
  this.geometryCache.beginFrame();
1918
2467
  this.instanceCache.beginFrame();
1919
2468
  this.instancedMeshIds.clear();
@@ -1924,93 +2473,140 @@ var WebGL2RendererBackend = class {
1924
2473
  const instances = collectInstanceBatches(renderables, this.instancesScratch);
1925
2474
  const batches = collectRenderBatches(frame.scene, this.batchesScratch);
1926
2475
  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
- }
2476
+ if (this.shadowEnabled && this.shadowMap?.begin()) {
2477
+ for (const batch of instances) {
2478
+ if (isTransparentColor(batch.material.color))
2479
+ continue;
2480
+ const instance = this.instanceCache.retain(gl, this.geometryCache, batch);
2481
+ if (!instance)
2482
+ continue;
2483
+ gl.bindVertexArray(instance.vertexArray);
2484
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, instance.triangleIndexBuffer);
2485
+ gl.drawElementsInstanced(gl.TRIANGLES, instance.triangleCount, batch.geometry.indices instanceof Uint16Array ? gl.UNSIGNED_SHORT : gl.UNSIGNED_INT, 0, instance.instanceCount);
1952
2486
  }
2487
+ this.shadowMap.end(frame.viewport.width * this.dpr, frame.viewport.height * this.dpr);
2488
+ this.program.use();
2489
+ this.program.setShadow(this.shadowEnabled, this.shadowMap.texture, this.shadowMap.matrix);
1953
2490
  }
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;
2491
+ for (const transparentPass of [false, true]) {
2492
+ if (transparentPass) {
2493
+ gl.enable(gl.BLEND);
2494
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
2495
+ gl.depthMask(false);
2496
+ } else {
2497
+ gl.disable(gl.BLEND);
2498
+ gl.depthMask(true);
2499
+ }
2500
+ this.program.setInstancing(true);
2501
+ this.profiler.mark("prepareAndUpload");
2502
+ for (const batch of instances) {
2503
+ if (isTransparentColor(batch.material.color) !== transparentPass)
2504
+ continue;
2505
+ const instance = this.instanceCache.retain(gl, this.geometryCache, batch);
2506
+ if (!instance)
2507
+ continue;
2508
+ for (const id of batch.meshIds)
2509
+ this.instancedMeshIds.add(id);
2510
+ mvp.elements.set(viewProjection.elements);
2511
+ this.program.setMatrix(mvp);
2512
+ this.program.setColor(colorToRgba(batch.material.color));
2513
+ this.program.setVertexColors(Boolean(batch.geometry.colors));
2514
+ this.program.setDirectionalShading(batch.material.shading !== "flat");
2515
+ this.program.setWaterShading(batch.material.shading === "water");
2516
+ gl.bindVertexArray(instance.vertexArray);
2517
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, instance.triangleIndexBuffer);
2518
+ gl.drawElementsInstanced(gl.TRIANGLES, instance.triangleCount, batch.geometry.indices instanceof Uint16Array ? gl.UNSIGNED_SHORT : gl.UNSIGNED_INT, 0, instance.instanceCount);
2519
+ this.frameStats.drawCalls += 1;
2520
+ if (batch.material.edgeColor && instance.edgeCount > 0) {
2521
+ this.program.setVertexColors(false);
2522
+ this.program.setDirectionalShading(false);
2523
+ this.program.setWaterShading(false);
2524
+ this.program.setColor(colorToRgba(batch.material.edgeColor));
2525
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, instance.edgeIndexBuffer);
2526
+ for (const [offsetX, offsetY] of EDGE_PIXEL_OFFSETS) {
2527
+ this.program.setClipOffset(offsetX * clipPixelX, offsetY * clipPixelY);
2528
+ gl.drawElementsInstanced(gl.LINES, instance.edgeCount, batch.geometry.edgeIndices instanceof Uint16Array ? gl.UNSIGNED_SHORT : gl.UNSIGNED_INT, 0, instance.instanceCount);
2529
+ this.frameStats.drawCalls += 1;
2530
+ }
1978
2531
  }
1979
2532
  }
1980
- }
1981
- for (const renderable of renderables) {
1982
- if (this.instancedMeshIds.has(renderable.id))
1983
- continue;
2533
+ this.program.setInstancing(false);
1984
2534
  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;
2535
+ for (const draw of batches) {
2536
+ if (isTransparentColor(draw.batch.material.color) !== transparentPass)
2537
+ continue;
2538
+ this.program.setClipOffset(0, 0);
2539
+ mvp.elements.set(viewProjection.elements);
2540
+ world.elements.set(draw.worldMatrix);
2541
+ mvp.multiply(world);
2542
+ this.program.setMatrix(mvp);
2543
+ this.program.setColor(colorToRgba(draw.batch.material.color));
2544
+ const geometry = this.geometryCache.retain(gl, draw.batch.geometry);
2545
+ this.program.setVertexColors(Boolean(draw.batch.geometry.colors));
2546
+ this.program.setDirectionalShading(draw.batch.material.shading !== "flat");
2547
+ this.program.setWaterShading(draw.batch.material.shading === "water");
2548
+ gl.bindVertexArray(geometry.vertexArray);
2549
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, geometry.triangleIndexBuffer);
2550
+ gl.drawElements(gl.TRIANGLES, geometry.triangleCount, draw.batch.geometry.indices instanceof Uint16Array ? gl.UNSIGNED_SHORT : gl.UNSIGNED_INT, 0);
2551
+ this.frameStats.drawCalls += 1;
2552
+ this.frameStats.renderedBatches += 1;
2553
+ if (draw.batch.material.edgeColor) {
2554
+ this.program.setVertexColors(false);
2555
+ this.program.setDirectionalShading(false);
2556
+ this.program.setWaterShading(false);
2557
+ this.program.setColor(colorToRgba(draw.batch.material.edgeColor));
2558
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, geometry.edgeIndexBuffer);
2559
+ for (const [offsetX, offsetY] of EDGE_PIXEL_OFFSETS) {
2560
+ this.program.setClipOffset(offsetX * clipPixelX, offsetY * clipPixelY);
2561
+ gl.drawElements(gl.LINES, geometry.edgeCount, draw.batch.geometry.edgeIndices instanceof Uint16Array ? gl.UNSIGNED_SHORT : gl.UNSIGNED_INT, 0);
2562
+ this.frameStats.drawCalls += 1;
2563
+ }
2004
2564
  }
2005
2565
  }
2006
- this.frameStats.renderedMeshes += 1;
2566
+ for (const renderable of renderables) {
2567
+ if (isTransparentColor(renderable.mesh.material.color) !== transparentPass)
2568
+ continue;
2569
+ if (this.instancedMeshIds.has(renderable.id))
2570
+ continue;
2571
+ this.program.setClipOffset(0, 0);
2572
+ mvp.elements.set(viewProjection.elements);
2573
+ world.elements.set(renderable.worldMatrix);
2574
+ mvp.multiply(world);
2575
+ this.program.setMatrix(mvp);
2576
+ this.program.setColor(colorToRgba(renderable.mesh.material.color));
2577
+ const geometry = this.geometryCache.retain(gl, renderable.mesh.geometry);
2578
+ this.program.setVertexColors(Boolean(renderable.mesh.geometry.colors));
2579
+ this.program.setDirectionalShading(renderable.mesh.material.shading !== "flat");
2580
+ this.program.setWaterShading(renderable.mesh.material.shading === "water");
2581
+ gl.bindVertexArray(geometry.vertexArray);
2582
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, geometry.triangleIndexBuffer);
2583
+ gl.drawElements(gl.TRIANGLES, geometry.triangleCount, renderable.mesh.geometry.indices instanceof Uint16Array ? gl.UNSIGNED_SHORT : gl.UNSIGNED_INT, 0);
2584
+ this.frameStats.drawCalls += 1;
2585
+ if (renderable.mesh.material.edgeColor) {
2586
+ this.program.setVertexColors(false);
2587
+ this.program.setDirectionalShading(false);
2588
+ this.program.setWaterShading(false);
2589
+ this.program.setColor(colorToRgba(renderable.mesh.material.edgeColor));
2590
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, geometry.edgeIndexBuffer);
2591
+ for (const [offsetX, offsetY] of EDGE_PIXEL_OFFSETS) {
2592
+ this.program.setClipOffset(offsetX * clipPixelX, offsetY * clipPixelY);
2593
+ gl.drawElements(gl.LINES, geometry.edgeCount, renderable.mesh.geometry.edgeIndices instanceof Uint16Array ? gl.UNSIGNED_SHORT : gl.UNSIGNED_INT, 0);
2594
+ this.frameStats.drawCalls += 1;
2595
+ }
2596
+ }
2597
+ this.frameStats.renderedMeshes += 1;
2598
+ }
2007
2599
  }
2600
+ gl.depthMask(true);
2601
+ gl.disable(gl.BLEND);
2008
2602
  if (frame.selection.length > 0) {
2009
2603
  const selected = new Set(frame.selection);
2010
2604
  gl.enable(gl.BLEND);
2011
2605
  gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
2012
2606
  gl.depthMask(false);
2607
+ gl.polygonOffset(-1, -4);
2013
2608
  this.program.setVertexColors(false);
2609
+ this.program.setDirectionalShading(false);
2014
2610
  this.program.setColor([
2015
2611
  SELECTION_RGBA[0],
2016
2612
  SELECTION_RGBA[1],
@@ -2056,6 +2652,7 @@ var WebGL2RendererBackend = class {
2056
2652
  if (this.semanticProgram) {
2057
2653
  this.semanticProgram.use();
2058
2654
  this.semanticProgram.setInstancing(false);
2655
+ gl.polygonOffset(-1, -4);
2059
2656
  for (const draw of batches) {
2060
2657
  for (let semanticIndex = 0; semanticIndex < draw.batch.semanticSelectionIds.length; semanticIndex += 1) {
2061
2658
  if (!selected.has(draw.batch.semanticSelectionIds[semanticIndex])) {
@@ -2073,6 +2670,7 @@ var WebGL2RendererBackend = class {
2073
2670
  }
2074
2671
  }
2075
2672
  this.semanticProgram.setInstancing(true);
2673
+ gl.polygonOffset(-1, -4);
2076
2674
  mvp.elements.set(viewProjection.elements);
2077
2675
  this.semanticProgram.setMatrix(mvp);
2078
2676
  for (const batch of instances) {
@@ -2089,6 +2687,7 @@ var WebGL2RendererBackend = class {
2089
2687
  }
2090
2688
  }
2091
2689
  gl.depthMask(true);
2690
+ gl.polygonOffset(2, 4);
2092
2691
  gl.disable(gl.BLEND);
2093
2692
  this.program.use();
2094
2693
  this.program.setClipOffset(0, 0);
@@ -2173,6 +2772,7 @@ var WebGL2RendererBackend = class {
2173
2772
  this.geometryCache = new WebGL2GeometryCache();
2174
2773
  this.instanceCache = new WebGL2InstanceCache();
2175
2774
  this.picker = new WebGL2Picker(gl);
2775
+ this.shadowMap = new WebGL2ShadowMap(gl);
2176
2776
  }
2177
2777
  };
2178
2778
 
@@ -2310,8 +2910,8 @@ async function executeProbe(gpu, options) {
2310
2910
  primitive: { topology: "triangle-list" }
2311
2911
  });
2312
2912
  const texture = context.getCurrentTexture();
2313
- const encoder = device.createCommandEncoder();
2314
- const pass = encoder.beginRenderPass({
2913
+ const encoder2 = device.createCommandEncoder();
2914
+ const pass = encoder2.beginRenderPass({
2315
2915
  colorAttachments: [{
2316
2916
  view: texture.createView(),
2317
2917
  clearValue: { r: 0, g: 0, b: 0, a: 1 },
@@ -2322,7 +2922,7 @@ async function executeProbe(gpu, options) {
2322
2922
  pass.setPipeline(pipeline);
2323
2923
  pass.draw(3);
2324
2924
  pass.end();
2325
- device.queue.submit([encoder.finish()]);
2925
+ device.queue.submit([encoder2.finish()]);
2326
2926
  const lost = await Promise.race([
2327
2927
  device.lost.then((info) => info.message || info.reason),
2328
2928
  new Promise((resolve) => {
@@ -2428,9 +3028,13 @@ var WebGPUGeometryCache = class {
2428
3028
  entries = /* @__PURE__ */ new Map();
2429
3029
  active = /* @__PURE__ */ new Set();
2430
3030
  pinned = /* @__PURE__ */ new Set();
3031
+ objectKeys = /* @__PURE__ */ new WeakMap();
3032
+ legacyKeys = /* @__PURE__ */ new WeakMap();
3033
+ objectKeySequence = 0;
2431
3034
  inactiveGraceFrames;
2432
3035
  maxBytes;
2433
3036
  uploads = 0;
3037
+ updatedBytes = 0;
2434
3038
  evictions = 0;
2435
3039
  frameId = 0;
2436
3040
  resourceGeneration = 0;
@@ -2443,12 +3047,23 @@ var WebGPUGeometryCache = class {
2443
3047
  }
2444
3048
  }
2445
3049
  retain(geometry) {
2446
- this.active.add(geometry);
2447
- const existing = this.entries.get(geometry);
3050
+ const key = this.keyFor(geometry);
3051
+ this.active.add(key);
3052
+ const existing = this.entries.get(key);
2448
3053
  if (existing) {
2449
- existing.lastUsedFrame = this.frameId;
2450
- return existing;
3054
+ if (this.canReuse(existing, geometry)) {
3055
+ existing.lastUsedFrame = this.frameId;
3056
+ return existing;
3057
+ }
3058
+ }
3059
+ const next = this.createEntry(geometry, key);
3060
+ if (existing) {
3061
+ this.deleteEntry(existing, false);
2451
3062
  }
3063
+ this.entries.set(key, next);
3064
+ return next;
3065
+ }
3066
+ createEntry(geometry, key) {
2452
3067
  const buffers = [];
2453
3068
  try {
2454
3069
  const vertexCount = geometry.positions.length / 3;
@@ -2473,6 +3088,10 @@ var WebGPUGeometryCache = class {
2473
3088
  if (semanticBuffer)
2474
3089
  buffers.push(semanticBuffer);
2475
3090
  const entry = {
3091
+ geometry,
3092
+ geometryId: geometry.identity?.geometryId ?? key,
3093
+ attributeDigest: geometry.identity?.attributeDigest ?? "",
3094
+ topologyDigest: geometry.identity?.topologyDigest ?? "",
2476
3095
  vertexBuffer,
2477
3096
  triangleIndexBuffer,
2478
3097
  edgeIndexBuffer,
@@ -2484,7 +3103,6 @@ var WebGPUGeometryCache = class {
2484
3103
  bytes: buffers.reduce((sum, buffer) => sum + buffer.size, 0),
2485
3104
  lastUsedFrame: this.frameId
2486
3105
  };
2487
- this.entries.set(geometry, entry);
2488
3106
  this.uploads += buffers.length;
2489
3107
  this.resourceGeneration += 1;
2490
3108
  return entry;
@@ -2496,19 +3114,25 @@ var WebGPUGeometryCache = class {
2496
3114
  throw new ICEEngineError("ICE3_GPU_RESOURCE_FAILED", "ICE3_GPU_RESOURCE_FAILED: geometry upload failed", { cause: error });
2497
3115
  }
2498
3116
  }
2499
- pin(geometry) {
2500
- this.pinned.add(geometry);
3117
+ pin(geometry, update) {
3118
+ const key = this.keyFor(geometry);
3119
+ this.pinned.add(key);
3120
+ const existing = this.entries.get(key);
3121
+ if (existing && update && this.sameTopology(existing, geometry)) {
3122
+ this.applyPositionUpdate(existing, geometry, update);
3123
+ return existing;
3124
+ }
2501
3125
  const entry = this.retain(geometry);
2502
3126
  this.enforceBudget();
2503
3127
  if (this.totalBytes > this.maxBytes) {
2504
- this.pinned.delete(geometry);
2505
- this.deleteEntry(geometry, entry);
3128
+ this.pinned.delete(key);
3129
+ this.deleteEntry(entry);
2506
3130
  throw new ICEEngineError("ICE3_GPU_RESOURCE_FAILED", "ICE3_GPU_RESOURCE_FAILED: geometry exceeds the GPU residency budget");
2507
3131
  }
2508
3132
  return entry;
2509
3133
  }
2510
3134
  unpin(geometry) {
2511
- this.pinned.delete(geometry);
3135
+ this.pinned.delete(this.keyFor(geometry));
2512
3136
  }
2513
3137
  beginFrame() {
2514
3138
  this.frameId += 1;
@@ -2521,13 +3145,13 @@ var WebGPUGeometryCache = class {
2521
3145
  if (this.frameId - entry.lastUsedFrame < this.inactiveGraceFrames) {
2522
3146
  continue;
2523
3147
  }
2524
- this.deleteEntry(geometry, entry);
3148
+ this.deleteEntry(entry);
2525
3149
  }
2526
3150
  this.enforceBudget();
2527
3151
  }
2528
3152
  clear() {
2529
3153
  for (const [geometry, entry] of this.entries) {
2530
- this.deleteEntry(geometry, entry);
3154
+ this.deleteEntry(entry);
2531
3155
  }
2532
3156
  this.active.clear();
2533
3157
  this.pinned.clear();
@@ -2541,6 +3165,9 @@ var WebGPUGeometryCache = class {
2541
3165
  get uploadCount() {
2542
3166
  return this.uploads;
2543
3167
  }
3168
+ get updateBytes() {
3169
+ return this.updatedBytes;
3170
+ }
2544
3171
  get evictionCount() {
2545
3172
  return this.evictions;
2546
3173
  }
@@ -2555,20 +3182,120 @@ var WebGPUGeometryCache = class {
2555
3182
  for (const [geometry, entry] of candidates) {
2556
3183
  if (total <= this.maxBytes)
2557
3184
  break;
2558
- this.deleteEntry(geometry, entry);
3185
+ this.deleteEntry(entry);
2559
3186
  total -= entry.bytes;
2560
3187
  }
2561
3188
  }
2562
- deleteEntry(geometry, entry) {
3189
+ deleteEntry(entry, eviction = true) {
2563
3190
  entry.vertexBuffer.destroy();
2564
3191
  entry.triangleIndexBuffer.destroy();
2565
3192
  entry.edgeIndexBuffer?.destroy();
2566
3193
  entry.semanticBuffer?.destroy();
2567
- this.entries.delete(geometry);
2568
- this.evictions += 1;
3194
+ for (const [key, candidate] of this.entries) {
3195
+ if (candidate === entry) {
3196
+ this.entries.delete(key);
3197
+ break;
3198
+ }
3199
+ }
3200
+ if (eviction) {
3201
+ this.evictions += 1;
3202
+ }
2569
3203
  this.resourceGeneration += 1;
2570
3204
  }
3205
+ applyPositionUpdate(entry, geometry, update) {
3206
+ let updated = 0;
3207
+ try {
3208
+ for (const range of update.positionRanges) {
3209
+ if (range.byteOffset % 12 !== 0 || range.byteLength % 12 !== 0 || range.byteOffset + range.byteLength > geometry.positions.byteLength) {
3210
+ throw new Error("invalid position range");
3211
+ }
3212
+ const vertexOffset = range.byteOffset / 12;
3213
+ const vertexCount = range.byteLength / 12;
3214
+ const vertices = new Float32Array(vertexCount * 6);
3215
+ const totalVertices = geometry.positions.length / 3;
3216
+ const colorStride = geometry.colors ? geometry.colors.length / totalVertices : 0;
3217
+ for (let index = 0; index < vertexCount; index += 1) {
3218
+ const sourceVertex = vertexOffset + index;
3219
+ vertices[index * 6] = geometry.positions[sourceVertex * 3];
3220
+ vertices[index * 6 + 1] = geometry.positions[sourceVertex * 3 + 1];
3221
+ vertices[index * 6 + 2] = geometry.positions[sourceVertex * 3 + 2];
3222
+ vertices[index * 6 + 3] = geometry.colors?.[sourceVertex * colorStride] ?? 1;
3223
+ vertices[index * 6 + 4] = geometry.colors?.[sourceVertex * colorStride + 1] ?? 1;
3224
+ vertices[index * 6 + 5] = geometry.colors?.[sourceVertex * colorStride + 2] ?? 1;
3225
+ }
3226
+ this.device.queue.writeBuffer(entry.vertexBuffer, range.byteOffset * 2, vertices);
3227
+ updated += range.byteLength;
3228
+ }
3229
+ } catch (error) {
3230
+ throw new ICEEngineError("ICE3_GPU_RESOURCE_FAILED", "ICE3_GPU_RESOURCE_FAILED: geometry update failed", { cause: error });
3231
+ }
3232
+ entry.geometry = geometry;
3233
+ entry.lastUsedFrame = this.frameId;
3234
+ this.updatedBytes += updated;
3235
+ }
3236
+ canReuse(existing, geometry) {
3237
+ if (existing.geometry === geometry) {
3238
+ return true;
3239
+ }
3240
+ if (!this.sameTopology(existing, geometry)) {
3241
+ return false;
3242
+ }
3243
+ 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;
3244
+ }
3245
+ sameTopology(existing, geometry) {
3246
+ 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);
3247
+ }
3248
+ keyFor(geometry) {
3249
+ const identity = geometry.identity?.geometryId;
3250
+ if (identity) {
3251
+ return `identity:${identity}`;
3252
+ }
3253
+ const id = geometry.id;
3254
+ if (typeof id === "string" && id.trim()) {
3255
+ const cached = this.legacyKeys.get(geometry);
3256
+ if (cached)
3257
+ return cached;
3258
+ const key2 = [
3259
+ "content",
3260
+ geometry.positions.length,
3261
+ geometry.indices.length,
3262
+ contentFingerprint2(geometry.positions),
3263
+ contentFingerprint2(geometry.indices),
3264
+ geometry.colors ? contentFingerprint2(geometry.colors) : 0,
3265
+ geometry.edgeIndices ? contentFingerprint2(geometry.edgeIndices) : 0,
3266
+ geometry.semanticIndices ? contentFingerprint2(geometry.semanticIndices) : 0
3267
+ ].join(":");
3268
+ this.legacyKeys.set(geometry, key2);
3269
+ return key2;
3270
+ }
3271
+ const existing = this.objectKeys.get(geometry);
3272
+ if (existing) {
3273
+ return existing;
3274
+ }
3275
+ const key = `object:${this.objectKeySequence}`;
3276
+ this.objectKeySequence += 1;
3277
+ this.objectKeys.set(geometry, key);
3278
+ return key;
3279
+ }
2571
3280
  };
3281
+ function contentFingerprint2(values) {
3282
+ let hash = 2166136261;
3283
+ const bytes = new Uint8Array(values.buffer, values.byteOffset, values.byteLength);
3284
+ for (const byte of bytes) {
3285
+ hash ^= byte;
3286
+ hash = Math.imul(hash, 16777619) >>> 0;
3287
+ }
3288
+ return hash;
3289
+ }
3290
+ function equalNumberArrays2(left, right) {
3291
+ return left.length === right.length && left.every((value, index) => value === right[index]);
3292
+ }
3293
+ function equalOptionalNumberArrays2(left, right) {
3294
+ if (left === void 0 || right === void 0) {
3295
+ return left === right;
3296
+ }
3297
+ return left.length === right.length && left.every((value, index) => value === right[index]);
3298
+ }
2572
3299
 
2573
3300
  // packages/renderer/dist/WebGPUInstanceCache.js
2574
3301
  var WebGPUInstanceCache = class {
@@ -2664,7 +3391,7 @@ function sameBytes(left, right) {
2664
3391
 
2665
3392
  // packages/renderer/dist/WebGPURenderPipeline.js
2666
3393
  var WEBGPU_UNIFORM_STRIDE = 256;
2667
- var WEBGPU_UNIFORM_BYTES = 128;
3394
+ var WEBGPU_UNIFORM_BYTES = 144;
2668
3395
  var SHADER_SOURCE = (selectionMask, storageInstances, indirectInstances) => `
2669
3396
  struct FrameUniforms {
2670
3397
  worldViewProjection: mat4x4<f32>,
@@ -2680,6 +3407,7 @@ struct FrameUniforms {
2680
3407
  useSelectionMask: f32,
2681
3408
  pageBase: f32,
2682
3409
  useIndirect: f32,
3410
+ useLighting: f32,
2683
3411
  }
2684
3412
 
2685
3413
  ${storageInstances ? `
@@ -2696,6 +3424,7 @@ struct VertexOutput {
2696
3424
  @builtin(position) position: vec4<f32>,
2697
3425
  @location(0) color: vec4<f32>,
2698
3426
  @location(1) @interpolate(flat) semantic: u32,
3427
+ @location(2) worldPosition: vec4<f32>,
2699
3428
  }
2700
3429
 
2701
3430
  @group(0) @binding(0) var<uniform> frame: FrameUniforms;
@@ -2748,6 +3477,7 @@ fn vertexMain(
2748
3477
  mix(vec3<f32>(1.0), color, frame.useVertexColor) * frame.color.rgb,
2749
3478
  frame.color.a
2750
3479
  );
3480
+ output.worldPosition = worldPosition;
2751
3481
  output.semantic = select(
2752
3482
  vertexSemantic,
2753
3483
  instanceSemantic,
@@ -2791,7 +3521,24 @@ fn fragmentMain(input: VertexOutput) -> @location(0) vec4<f32> {
2791
3521
  );
2792
3522
  }
2793
3523
  }` : ""}
2794
- return input.color;
3524
+ var outputColor = input.color;
3525
+ if (frame.useLighting > 0.5) {
3526
+ let normal = normalize(cross(
3527
+ dpdx(input.worldPosition.xyz),
3528
+ dpdy(input.worldPosition.xyz)
3529
+ ));
3530
+ let lightDirection = normalize(vec3<f32>(-0.35, 0.85, 0.4));
3531
+ let diffuse = dot(normal, lightDirection);
3532
+ let key = max(diffuse, 0.0) + 0.35 * max(-diffuse, 0.0);
3533
+ let sky = 0.5 + 0.5 * normal.y;
3534
+ let shade = clamp(
3535
+ 0.72 + 0.72 * key + 0.18 * sky,
3536
+ 0.92,
3537
+ 1.42
3538
+ );
3539
+ outputColor = vec4<f32>(outputColor.rgb * shade, outputColor.a);
3540
+ }
3541
+ return outputColor;
2795
3542
  }
2796
3543
  `;
2797
3544
  var VERTEX_LAYOUT = [
@@ -2946,7 +3693,14 @@ var WebGPURenderPipeline = class _WebGPURenderPipeline {
2946
3693
  depthStencil: {
2947
3694
  format: "depth24plus",
2948
3695
  depthCompare: "less-equal",
2949
- depthWriteEnabled: !selection
3696
+ depthWriteEnabled: !selection,
3697
+ // Selected triangles are redrawn after opaque geometry. Pull that
3698
+ // overlay slightly forward instead of competing at an equal depth.
3699
+ ...selection && topology === "triangle-list" ? {
3700
+ depthBias: -1,
3701
+ depthBiasSlopeScale: -1,
3702
+ depthBiasClamp: 0
3703
+ } : {}
2950
3704
  },
2951
3705
  multisample: { count: 1 }
2952
3706
  });
@@ -2995,12 +3749,12 @@ var WebGPUTimestampProfiler = class {
2995
3749
  querySetFor(frameId) {
2996
3750
  return this.slotFor(frameId).querySet;
2997
3751
  }
2998
- resolveFrame(encoder, frameId) {
3752
+ resolveFrame(encoder2, frameId) {
2999
3753
  const slot = this.slotFor(frameId);
3000
3754
  if (slot.pending) {
3001
3755
  throw new Error("ICE3_TIMESTAMP_SLOT_BUSY");
3002
3756
  }
3003
- encoder.resolveQuerySet(slot.querySet, 0, 2, slot.buffer, 0);
3757
+ encoder2.resolveQuerySet(slot.querySet, 0, 2, slot.buffer, 0);
3004
3758
  this.pending.push(this.slotIndex(frameId));
3005
3759
  slot.pending = true;
3006
3760
  }
@@ -3168,6 +3922,7 @@ var WebGPUDrawStorage = class {
3168
3922
  this.uniformBuffer[offset + 29] = draw.useSelectionMask ? 1 : 0;
3169
3923
  this.uniformBuffer[offset + 30] = draw.pageBase ?? 0;
3170
3924
  this.uniformBuffer[offset + 31] = draw.useIndirect ? 1 : 0;
3925
+ this.uniformBuffer[offset + 32] = draw.useLighting ? 1 : 0;
3171
3926
  this.count += 1;
3172
3927
  return index;
3173
3928
  }
@@ -3252,13 +4007,13 @@ var WebGPUBundleCache = class {
3252
4007
  get(key, record3) {
3253
4008
  if (this.key === key && this.bundle)
3254
4009
  return this.bundle;
3255
- const encoder = this.device.createRenderBundleEncoder({
4010
+ const encoder2 = this.device.createRenderBundleEncoder({
3256
4011
  label: `ice3:bundle:${key}`,
3257
4012
  colorFormats: [this.format],
3258
4013
  depthStencilFormat: "depth24plus"
3259
4014
  });
3260
- record3(encoder);
3261
- const bundle = encoder.finish();
4015
+ record3(encoder2);
4016
+ const bundle = encoder2.finish();
3262
4017
  this.key = key;
3263
4018
  this.bundle = bundle;
3264
4019
  this.recreations += 1;
@@ -3596,8 +4351,8 @@ var WebGPUVisibilityEncoder = class _WebGPUVisibilityEncoder {
3596
4351
  this.entries.set(page.buffer, entries);
3597
4352
  return entry;
3598
4353
  }
3599
- encode(encoder, entry, page, uniformBuffer, uniformOffset) {
3600
- encoder.clearBuffer(entry.indirectBuffer, 4, 4);
4354
+ encode(encoder2, entry, page, uniformBuffer, uniformOffset) {
4355
+ encoder2.clearBuffer(entry.indirectBuffer, 4, 4);
3601
4356
  const bindGroup = this.device.createBindGroup({
3602
4357
  label: "ice3:visibility-bindings",
3603
4358
  layout: this.bindGroupLayout,
@@ -3628,7 +4383,7 @@ var WebGPUVisibilityEncoder = class _WebGPUVisibilityEncoder {
3628
4383
  }
3629
4384
  ]
3630
4385
  });
3631
- const pass = encoder.beginComputePass({ label: "ice3:visibility-pass" });
4386
+ const pass = encoder2.beginComputePass({ label: "ice3:visibility-pass" });
3632
4387
  pass.setPipeline(this.pipeline);
3633
4388
  pass.setBindGroup(0, bindGroup, [uniformOffset]);
3634
4389
  pass.dispatchWorkgroups(Math.ceil(page.instanceCount / 64));
@@ -3640,9 +4395,9 @@ var WebGPUVisibilityEncoder = class _WebGPUVisibilityEncoder {
3640
4395
  size: 4,
3641
4396
  usage: 1 | 8
3642
4397
  });
3643
- const encoder = this.device.createCommandEncoder();
3644
- encoder.copyBufferToBuffer(entry.indirectBuffer, 4, readback, 0, 4);
3645
- this.device.queue.submit([encoder.finish()]);
4398
+ const encoder2 = this.device.createCommandEncoder();
4399
+ encoder2.copyBufferToBuffer(entry.indirectBuffer, 4, readback, 0, 4);
4400
+ this.device.queue.submit([encoder2.finish()]);
3646
4401
  try {
3647
4402
  await readback.mapAsync(1);
3648
4403
  const visibleInstances = new Uint32Array(readback.getMappedRange())[0];
@@ -3815,8 +4570,8 @@ var WebGPUPicker = class {
3815
4570
  }
3816
4571
  }]
3817
4572
  });
3818
- const encoder = this.device.createCommandEncoder();
3819
- const pass = encoder.beginRenderPass({
4573
+ const encoder2 = this.device.createCommandEncoder();
4574
+ const pass = encoder2.beginRenderPass({
3820
4575
  colorAttachments: [{
3821
4576
  view: this.target.createView(),
3822
4577
  clearValue: { r: 0, g: 0, b: 0, a: 1 },
@@ -3848,7 +4603,7 @@ var WebGPUPicker = class {
3848
4603
  size: 256,
3849
4604
  usage: COPY_DST | MAP_READ
3850
4605
  });
3851
- encoder.copyTextureToBuffer({
4606
+ encoder2.copyTextureToBuffer({
3852
4607
  texture: this.target,
3853
4608
  origin: {
3854
4609
  x: pixelX,
@@ -3860,7 +4615,7 @@ var WebGPUPicker = class {
3860
4615
  bytesPerRow: 256,
3861
4616
  rowsPerImage: 1
3862
4617
  }, { width: 1, height: 1, depthOrArrayLayers: 1 });
3863
- this.device.queue.submit([encoder.finish()]);
4618
+ this.device.queue.submit([encoder2.finish()]);
3864
4619
  let pixel;
3865
4620
  try {
3866
4621
  await readback.mapAsync(MAP_READ);
@@ -4214,6 +4969,12 @@ var WebGPURendererBackend = class _WebGPURendererBackend {
4214
4969
  prepareBatchGeometry(geometry) {
4215
4970
  this.requireGeometryCache().pin(geometry);
4216
4971
  }
4972
+ prepareGeometry(geometry, update) {
4973
+ this.requireGeometryCache().pin(geometry, update);
4974
+ }
4975
+ releaseGeometry(geometry) {
4976
+ this.geometryCache?.unpin(geometry);
4977
+ }
4217
4978
  releaseBatchGeometry(geometry) {
4218
4979
  this.geometryCache?.unpin(geometry);
4219
4980
  }
@@ -4341,6 +5102,7 @@ var WebGPURendererBackend = class _WebGPURendererBackend {
4341
5102
  color: colorToRgba(batch.material.color),
4342
5103
  useVertexColor: Boolean(batch.geometry.colors),
4343
5104
  useInstancing: true,
5105
+ useLighting: batch.material.shading === "directional" || batch.material.shading === "water",
4344
5106
  pipeline: pagePipeline.triangle,
4345
5107
  useSelectionMask: selectionMaskEnabled,
4346
5108
  storagePage: page,
@@ -4368,6 +5130,7 @@ var WebGPURendererBackend = class _WebGPURendererBackend {
4368
5130
  color: colorToRgba(batch.material.color),
4369
5131
  useVertexColor: Boolean(batch.geometry.colors),
4370
5132
  useInstancing: true,
5133
+ useLighting: batch.material.shading === "directional" || batch.material.shading === "water",
4371
5134
  pipeline: this.pipeline.triangle,
4372
5135
  useSelectionMask: selectionMaskEnabled
4373
5136
  }, viewProjection.elements, batch.semanticIds);
@@ -4412,6 +5175,7 @@ var WebGPURendererBackend = class _WebGPURendererBackend {
4412
5175
  color: colorToRgba(draw.batch.material.color),
4413
5176
  useVertexColor: Boolean(draw.batch.geometry.colors),
4414
5177
  useInstancing: false,
5178
+ useLighting: draw.batch.material.shading === "directional" || draw.batch.material.shading === "water",
4415
5179
  pipeline: this.pipeline.triangle,
4416
5180
  useSelectionMask: selectionMaskEnabled
4417
5181
  }, matrix, draw.batch.semanticSelectionIds);
@@ -4459,6 +5223,7 @@ var WebGPURendererBackend = class _WebGPURendererBackend {
4459
5223
  color: colorToRgba(renderable.mesh.material.color),
4460
5224
  useVertexColor: Boolean(renderable.mesh.geometry.colors),
4461
5225
  useInstancing: false,
5226
+ useLighting: renderable.mesh.material.shading === "directional" || renderable.mesh.material.shading === "water",
4462
5227
  pipeline: this.pipeline.triangle,
4463
5228
  useSelectionMask: selectionMaskEnabled
4464
5229
  }, matrix, [renderable.mesh.selectionId]);
@@ -4527,26 +5292,26 @@ var WebGPURendererBackend = class _WebGPURendererBackend {
4527
5292
  this.frameBindGroupKey = bindGroupKey;
4528
5293
  }
4529
5294
  const bindGroup = this.frameBindGroup;
4530
- const encodeDraws = (encoder2) => {
5295
+ const encodeDraws = (encoder3) => {
4531
5296
  for (let index = 0; index < drawList.length; index += 1) {
4532
5297
  const draw = drawList[index];
4533
- encoder2.setPipeline(draw.pipeline);
5298
+ encoder3.setPipeline(draw.pipeline);
4534
5299
  const drawBindGroup = draw.storagePage ? this.storageBindGroupFor(draw.storagePage, draw.visibilityEntry?.visibleBuffer) : bindGroup;
4535
- encoder2.setBindGroup(0, drawBindGroup, [
5300
+ encoder3.setBindGroup(0, drawBindGroup, [
4536
5301
  index * WEBGPU_UNIFORM_STRIDE
4537
5302
  ]);
4538
- encoder2.setVertexBuffer(0, draw.geometry.vertexBuffer);
5303
+ encoder3.setVertexBuffer(0, draw.geometry.vertexBuffer);
4539
5304
  if (draw.storagePage) {
4540
- encoder2.setVertexBuffer(1, draw.semanticBuffer);
5305
+ encoder3.setVertexBuffer(1, draw.semanticBuffer);
4541
5306
  } else {
4542
- encoder2.setVertexBuffer(1, draw.instanceBuffer);
4543
- encoder2.setVertexBuffer(2, draw.semanticBuffer);
5307
+ encoder3.setVertexBuffer(1, draw.instanceBuffer);
5308
+ encoder3.setVertexBuffer(2, draw.semanticBuffer);
4544
5309
  }
4545
- encoder2.setIndexBuffer(draw.edge ? draw.geometry.edgeIndexBuffer : draw.geometry.triangleIndexBuffer, draw.geometry.indexFormat);
5310
+ encoder3.setIndexBuffer(draw.edge ? draw.geometry.edgeIndexBuffer : draw.geometry.triangleIndexBuffer, draw.geometry.indexFormat);
4546
5311
  if (draw.visibilityEntry) {
4547
- encoder2.drawIndexedIndirect(draw.visibilityEntry.indirectBuffer, 0);
5312
+ encoder3.drawIndexedIndirect(draw.visibilityEntry.indirectBuffer, 0);
4548
5313
  } else {
4549
- encoder2.drawIndexed(draw.edge ? draw.geometry.edgeCount : draw.geometry.triangleCount, draw.instanceCount, 0, 0, draw.storagePage?.firstInstance ?? 0);
5314
+ encoder3.drawIndexed(draw.edge ? draw.geometry.edgeCount : draw.geometry.triangleCount, draw.instanceCount, 0, 0, draw.storagePage?.firstInstance ?? 0);
4550
5315
  }
4551
5316
  }
4552
5317
  };
@@ -4572,7 +5337,7 @@ var WebGPURendererBackend = class _WebGPURendererBackend {
4572
5337
  }
4573
5338
  }
4574
5339
  const targetTexture = context.getCurrentTexture();
4575
- const encoder = this.device.createCommandEncoder();
5340
+ const encoder2 = this.device.createCommandEncoder();
4576
5341
  if (this.gpuDrivenActive) {
4577
5342
  const encodedVisibility = /* @__PURE__ */ new Set();
4578
5343
  for (let index = 0; index < drawList.length; index += 1) {
@@ -4581,12 +5346,12 @@ var WebGPURendererBackend = class _WebGPURendererBackend {
4581
5346
  continue;
4582
5347
  }
4583
5348
  encodedVisibility.add(draw.visibilityEntry);
4584
- this.visibilityEncoder.encode(encoder, draw.visibilityEntry, draw.storagePage, this.uniformBuffer, index * WEBGPU_UNIFORM_STRIDE);
5349
+ this.visibilityEncoder.encode(encoder2, draw.visibilityEntry, draw.storagePage, this.uniformBuffer, index * WEBGPU_UNIFORM_STRIDE);
4585
5350
  }
4586
5351
  }
4587
5352
  const timestampProfiler = this.captureRequest || !this.timestampProfilingEnabled ? null : this.timestampProfiler;
4588
5353
  const timestampQuerySet = timestampProfiler?.querySetFor(frame.frameId);
4589
- const pass = encoder.beginRenderPass({
5354
+ const pass = encoder2.beginRenderPass({
4590
5355
  colorAttachments: [{
4591
5356
  view: targetTexture.createView(),
4592
5357
  clearValue: {
@@ -4627,10 +5392,10 @@ var WebGPURendererBackend = class _WebGPURendererBackend {
4627
5392
  this.frameStats.drawCalls += drawList.length;
4628
5393
  pass.end();
4629
5394
  if (timestampProfiler) {
4630
- timestampProfiler.resolveFrame(encoder, frame.frameId);
5395
+ timestampProfiler.resolveFrame(encoder2, frame.frameId);
4631
5396
  }
4632
5397
  if (this.captureRequest) {
4633
- encoder.copyTextureToBuffer({ texture: targetTexture }, {
5398
+ encoder2.copyTextureToBuffer({ texture: targetTexture }, {
4634
5399
  buffer: this.captureRequest.buffer,
4635
5400
  bytesPerRow: this.captureRequest.bytesPerRow,
4636
5401
  rowsPerImage: this.canvas.height
@@ -4641,7 +5406,7 @@ var WebGPURendererBackend = class _WebGPURendererBackend {
4641
5406
  });
4642
5407
  this.captureRequest = null;
4643
5408
  }
4644
- const commandBuffer = encoder.finish();
5409
+ const commandBuffer = encoder2.finish();
4645
5410
  this.profiler.mark("encode");
4646
5411
  this.device.queue.submit([commandBuffer]);
4647
5412
  this.profiler.mark("submit");
@@ -5493,18 +6258,18 @@ var ICEInputRouter = class {
5493
6258
  function pickSceneNode(scene, camera, viewport, point) {
5494
6259
  const eye = camera.getPosition();
5495
6260
  const target = camera.getTarget();
5496
- const forward = normalize2([
6261
+ const forward = normalize3([
5497
6262
  target.x - eye.x,
5498
6263
  target.y - eye.y,
5499
6264
  target.z - eye.z
5500
6265
  ]);
5501
- const right = normalize2(cross2(forward, [0, 1, 0]));
5502
- const up = normalize2(cross2(right, forward));
6266
+ const right = normalize3(cross3(forward, [0, 1, 0]));
6267
+ const up = normalize3(cross3(right, forward));
5503
6268
  const aspect = viewport.width / Math.max(viewport.height, 1);
5504
6269
  const tangent = Math.tan(camera.fov * Math.PI / 360);
5505
6270
  const ndcX = point.x / viewport.width * 2 - 1;
5506
6271
  const ndcY = 1 - point.y / viewport.height * 2;
5507
- const direction = normalize2([
6272
+ const direction = normalize3([
5508
6273
  forward[0] + right[0] * ndcX * aspect * tangent + up[0] * ndcY * tangent,
5509
6274
  forward[1] + right[1] * ndcX * aspect * tangent + up[1] * ndcY * tangent,
5510
6275
  forward[2] + right[2] * ndcX * aspect * tangent + up[2] * ndcY * tangent
@@ -5537,11 +6302,11 @@ function pickSceneNode(scene, camera, viewport, point) {
5537
6302
  visit2(scene, true);
5538
6303
  return nearest;
5539
6304
  }
5540
- var normalize2 = (value) => {
6305
+ var normalize3 = (value) => {
5541
6306
  const length = Math.hypot(...value);
5542
6307
  return length > 0 ? [value[0] / length, value[1] / length, value[2] / length] : [0, 0, 0];
5543
6308
  };
5544
- var cross2 = (left, right) => [
6309
+ var cross3 = (left, right) => [
5545
6310
  left[1] * right[2] - left[2] * right[1],
5546
6311
  left[2] * right[0] - left[0] * right[2],
5547
6312
  left[0] * right[1] - left[1] * right[0]
@@ -5997,6 +6762,17 @@ var ICE3 = class extends ICEEventDispatcher {
5997
6762
  this.backend.releaseBatchGeometry?.(geometry);
5998
6763
  return this;
5999
6764
  }
6765
+ prepareGeometry(geometry, update) {
6766
+ if (!this.backend.prepareGeometry) {
6767
+ throw new ICEEngineError("ICE3_RENDER_GEOMETRY_UNAVAILABLE", "The active renderer cannot stage geometry updates");
6768
+ }
6769
+ this.backend.prepareGeometry(geometry, update);
6770
+ return this;
6771
+ }
6772
+ releaseGeometry(geometry) {
6773
+ this.backend.releaseGeometry?.(geometry);
6774
+ return this;
6775
+ }
6000
6776
  addLayer(layer) {
6001
6777
  this.assertActive();
6002
6778
  if (this.layers.some((item) => item.id === layer.id)) {
@@ -6460,7 +7236,11 @@ var MAX_FRAME_DELTA_MS = 64;
6460
7236
  var SETTLE_EPSILON = 1e-5;
6461
7237
  var WHEEL_LINE_HEIGHT_PX = 16;
6462
7238
  var MAX_WHEEL_STEP_PX = 240;
6463
- var MOVE_SPEED_PER_DISTANCE = 0.75;
7239
+ var ZOOM_DISTANCE_PER_HOME_PIXEL = 1e-3;
7240
+ var PAN_SPEED_PER_HOME_DISTANCE = 0.75;
7241
+ var MIN_KEY_TAP_MS = 16;
7242
+ var MIN_ZOOM_FRACTION = 5e-3;
7243
+ var ABSOLUTE_MIN_ZOOM_DISTANCE = 0.2;
6464
7244
  var NAVIGATION_KEYS = /* @__PURE__ */ new Set([
6465
7245
  "KeyW",
6466
7246
  "KeyA",
@@ -6478,6 +7258,8 @@ var ICEOrbitController = class {
6478
7258
  yaw;
6479
7259
  pitch;
6480
7260
  distance;
7261
+ minDistance;
7262
+ maxDistance;
6481
7263
  targetYaw;
6482
7264
  targetPitch;
6483
7265
  targetDistance;
@@ -6559,7 +7341,12 @@ var ICEOrbitController = class {
6559
7341
  return;
6560
7342
  }
6561
7343
  this.prepareMotion();
7344
+ const alreadyPressed = this.pressedKeys.has(input.code);
6562
7345
  this.pressedKeys.add(input.code);
7346
+ if (!alreadyPressed && !input.repeat) {
7347
+ this.applyKeyboardMovement(MIN_KEY_TAP_MS);
7348
+ this.applyPosition();
7349
+ }
6563
7350
  this.engine.requestRender("camera-keyboard-movement");
6564
7351
  };
6565
7352
  onKeyUp = (event) => {
@@ -6603,7 +7390,9 @@ var ICEOrbitController = class {
6603
7390
  const offsetY = eye.y - target.y;
6604
7391
  const offsetZ = eye.z - target.z;
6605
7392
  this.target = target.toArray();
6606
- this.distance = Math.max(0.2, Math.hypot(offsetX, offsetY, offsetZ));
7393
+ this.distance = Math.max(ABSOLUTE_MIN_ZOOM_DISTANCE, Math.hypot(offsetX, offsetY, offsetZ));
7394
+ this.maxDistance = this.distance;
7395
+ this.minDistance = Math.min(this.maxDistance, Math.max(ABSOLUTE_MIN_ZOOM_DISTANCE, this.maxDistance * MIN_ZOOM_FRACTION));
6607
7396
  this.yaw = Math.atan2(offsetX, offsetZ);
6608
7397
  this.pitch = Math.asin(clamp(offsetY / this.distance, -1, 1));
6609
7398
  this.targetYaw = this.yaw;
@@ -6623,7 +7412,7 @@ var ICEOrbitController = class {
6623
7412
  ];
6624
7413
  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
7414
  this.target = center;
6626
- this.targetDistance = radius / Math.sin(this.camera.fov * Math.PI / 360) * padding;
7415
+ this.targetDistance = clamp(radius / Math.sin(this.camera.fov * Math.PI / 360) * padding, this.minDistance, this.maxDistance);
6627
7416
  if (options.animate === false) {
6628
7417
  this.distance = this.targetDistance;
6629
7418
  this.applyPosition();
@@ -6642,7 +7431,7 @@ var ICEOrbitController = class {
6642
7431
  zoomBy(delta) {
6643
7432
  this.assertActive();
6644
7433
  this.prepareMotion();
6645
- this.targetDistance = clamp(this.targetDistance * Math.exp(delta * 1e-3), 0.2, 100);
7434
+ this.targetDistance = clamp(this.targetDistance + delta * this.maxDistance * ZOOM_DISTANCE_PER_HOME_PIXEL, this.minDistance, this.maxDistance);
6646
7435
  this.engine.requestRender("camera-zoom");
6647
7436
  return this;
6648
7437
  }
@@ -6688,7 +7477,7 @@ var ICEOrbitController = class {
6688
7477
  if (magnitude === 0) {
6689
7478
  return;
6690
7479
  }
6691
- const distance2 = this.distance * MOVE_SPEED_PER_DISTANCE * (deltaMs / 1e3) / magnitude;
7480
+ const distance2 = this.maxDistance * PAN_SPEED_PER_HOME_DISTANCE * (deltaMs / 1e3) / magnitude;
6692
7481
  const sinYaw = Math.sin(this.yaw);
6693
7482
  const cosYaw = Math.cos(this.yaw);
6694
7483
  const sinPitch = Math.sin(this.pitch);
@@ -6754,7 +7543,8 @@ function buildAssetScene(snapshot) {
6754
7543
  }));
6755
7544
  const materials = snapshot.materials.map((material) => new ICEBasicMaterial({
6756
7545
  color: material.baseColor,
6757
- ...material.edgeColor ? { edgeColor: material.edgeColor } : {}
7546
+ ...material.edgeColor ? { edgeColor: material.edgeColor } : {},
7547
+ ...material.shading ? { shading: material.shading } : {}
6758
7548
  }));
6759
7549
  const meshesByNode = /* @__PURE__ */ new Map();
6760
7550
  for (const mesh of snapshot.meshes) {
@@ -6829,6 +7619,82 @@ function buildAssetScene(snapshot) {
6829
7619
  stats: snapshot.stats
6830
7620
  };
6831
7621
  }
7622
+ function buildPreviewPacketScene(packet, previousGeometries = /* @__PURE__ */ new Map()) {
7623
+ const geometries = /* @__PURE__ */ new Map();
7624
+ const geometryUpdates = /* @__PURE__ */ new Map();
7625
+ for (const source of packet.geometries) {
7626
+ const previous = previousGeometries.get(source.geometryId);
7627
+ if (previous && previous.identity.topologyDigest === source.topologyDigest && previous.identity.attributeDigest === source.attributeDigest) {
7628
+ geometries.set(source.geometryId, previous);
7629
+ continue;
7630
+ }
7631
+ const identity = Object.freeze({
7632
+ geometryId: source.geometryId,
7633
+ topologyDigest: source.topologyDigest,
7634
+ attributeDigest: source.attributeDigest
7635
+ });
7636
+ const geometry = new ICEIndexedGeometry({
7637
+ id: source.residentGeometryId ?? source.geometryId,
7638
+ identity,
7639
+ positions: source.positions,
7640
+ indices: source.indices
7641
+ });
7642
+ geometries.set(source.geometryId, geometry);
7643
+ const attributeRanges = source.ranges.filter((range) => range.usage === "vertex_attributes");
7644
+ const attributeStart = Math.min(...attributeRanges.map((range) => range.byteOffset));
7645
+ geometryUpdates.set(source.geometryId, Object.freeze({
7646
+ positionRanges: Object.freeze(attributeRanges.map((range) => Object.freeze({
7647
+ byteOffset: range.byteOffset - attributeStart,
7648
+ byteLength: range.byteLength
7649
+ })))
7650
+ }));
7651
+ }
7652
+ const materials = new Map(packet.materials.map((material) => [
7653
+ material.materialId,
7654
+ new ICEBasicMaterial({
7655
+ color: colorHex(material.baseColor)
7656
+ })
7657
+ ]));
7658
+ const defaultMaterial = new ICEBasicMaterial({
7659
+ color: "#9aa6b2"
7660
+ });
7661
+ const scene = new ICEScene({ id: `packet:${packet.revision}:scene` });
7662
+ const nodes = /* @__PURE__ */ new Map();
7663
+ for (const source of packet.nodes) {
7664
+ const geometry = source.geometryId ? geometries.get(source.geometryId) : void 0;
7665
+ const material = source.materialId ? materials.get(source.materialId) ?? defaultMaterial : defaultMaterial;
7666
+ const options = {
7667
+ id: source.nodeId,
7668
+ displayName: source.name,
7669
+ selectionId: source.nodeId,
7670
+ matrix: source.matrix
7671
+ };
7672
+ const node = geometry ? new ICEMesh({
7673
+ ...options,
7674
+ geometry,
7675
+ material
7676
+ }) : new ICENode(options);
7677
+ node.visible = source.visible;
7678
+ nodes.set(source.nodeId, node);
7679
+ }
7680
+ for (const source of packet.nodes) {
7681
+ const node = nodes.get(source.nodeId);
7682
+ const parent = source.parentId ? nodes.get(source.parentId) : void 0;
7683
+ if (source.parentId && !parent) {
7684
+ throw new Error(`ICE3_PREVIEW_PACKET_INVALID: parent node ${source.parentId} is missing`);
7685
+ }
7686
+ (parent ?? scene).addChild(node);
7687
+ }
7688
+ return {
7689
+ scene,
7690
+ geometries,
7691
+ geometryUpdates
7692
+ };
7693
+ }
7694
+ function colorHex(color) {
7695
+ const channel = (value) => Math.round(Math.max(0, Math.min(1, value)) * 255).toString(16).padStart(2, "0");
7696
+ return `#${channel(color[0])}${channel(color[1])}${channel(color[2])}`;
7697
+ }
6832
7698
 
6833
7699
  // packages/assets/dist/ICEProfileValidator.js
6834
7700
  function validatePreparedProfile(json, binary) {
@@ -6948,9 +7814,9 @@ function validatePreparedStats(json, profile) {
6948
7814
  let primitiveCount = 0;
6949
7815
  let triangleCount = 0;
6950
7816
  let edgeSegmentCount = 0;
6951
- const selectionNodes = /* @__PURE__ */ new Set();
7817
+ const selectionIds = /* @__PURE__ */ new Set();
6952
7818
  const referencedMeshes = /* @__PURE__ */ new Set();
6953
- for (const [nodeIndex, node] of nodes.entries()) {
7819
+ for (const node of nodes) {
6954
7820
  if (node.mesh === void 0)
6955
7821
  continue;
6956
7822
  const mesh = meshes[Number(node.mesh)];
@@ -6960,7 +7826,10 @@ function validatePreparedStats(json, profile) {
6960
7826
  for (const primitive of records(mesh.primitives)) {
6961
7827
  const mode = Number(primitive.mode ?? 4);
6962
7828
  if (mode === 4) {
6963
- selectionNodes.add(nodeIndex);
7829
+ const nodeProfile = record(record(node.extras)?.ice3);
7830
+ const selectionId = nodeProfile?.selectionId ?? nodeProfile?.id;
7831
+ if (typeof selectionId === "string")
7832
+ selectionIds.add(selectionId);
6964
7833
  }
6965
7834
  }
6966
7835
  }
@@ -6986,7 +7855,7 @@ function validatePreparedStats(json, profile) {
6986
7855
  triangleCount,
6987
7856
  edgeSegmentCount,
6988
7857
  materialCount: records(json.materials).length,
6989
- selectionGroupCount: Array.isArray(batchingSelectionIds) ? new Set(batchingSelectionIds).size : selectionNodes.size
7858
+ selectionGroupCount: Array.isArray(batchingSelectionIds) ? new Set(batchingSelectionIds).size : selectionIds.size
6990
7859
  };
6991
7860
  for (const [key, value] of Object.entries(expected)) {
6992
7861
  if (stats?.[key] !== value) {
@@ -7076,7 +7945,7 @@ function readPreparedGLB(bytes) {
7076
7945
  const geometries = [];
7077
7946
  const materials = [];
7078
7947
  const meshes = [];
7079
- const selectionNodes = /* @__PURE__ */ new Set();
7948
+ const selectionIds = /* @__PURE__ */ new Set();
7080
7949
  let primitiveCount = 0;
7081
7950
  let triangleCount = 0;
7082
7951
  let edgeSegmentCount = 0;
@@ -7108,11 +7977,13 @@ function readPreparedGLB(bytes) {
7108
7977
  edgeSegmentCount += (edgeIndices?.length ?? 0) / 2;
7109
7978
  const baseMaterial = sourceMaterials[Number(primitive.material)];
7110
7979
  const edgeMaterial = edgePrimitive ? sourceMaterials[Number(edgePrimitive.material)] : void 0;
7980
+ const shading = record(record(baseMaterial?.extras)?.ice3)?.shading;
7111
7981
  const materialIndex = materials.length;
7112
7982
  materials.push({
7113
7983
  id: `${meshId}/material/${primitiveIndex}`,
7114
7984
  baseColor: materialColor(baseMaterial),
7115
- ...edgeMaterial ? { edgeColor: materialColor(edgeMaterial) } : {}
7985
+ ...edgeMaterial ? { edgeColor: materialColor(edgeMaterial) } : {},
7986
+ ...shading === "directional" || shading === "water" ? { shading } : {}
7116
7987
  });
7117
7988
  const bounds = boundsFromAccessor(accessors[positionIndex]);
7118
7989
  const geometryIndex = geometries.length;
@@ -7126,7 +7997,7 @@ function readPreparedGLB(bytes) {
7126
7997
  bounds
7127
7998
  });
7128
7999
  for (const entry of nodesForMesh) {
7129
- selectionNodes.add(entry.index);
8000
+ selectionIds.add(nodes[entry.index].selectionId);
7130
8001
  meshes.push({
7131
8002
  id: `${meshId}/primitive/${primitiveIndex}/${entry.index}`,
7132
8003
  node: entry.index,
@@ -7144,7 +8015,7 @@ function readPreparedGLB(bytes) {
7144
8015
  triangleCount,
7145
8016
  edgeSegmentCount,
7146
8017
  materialCount: sourceMaterials.length,
7147
- selectionGroupCount: selectionNodes.size
8018
+ selectionGroupCount: batching ? new Set(batching.semanticSelectionIds).size : selectionIds.size
7148
8019
  };
7149
8020
  const contract = preparedContract(profile);
7150
8021
  return {
@@ -7168,7 +8039,7 @@ function readBatchingMetadata(profile, sourceNodes, sourceMeshes, sourceMaterial
7168
8039
  }
7169
8040
  const semanticIds = stringArray(raw.semanticIds, "semanticIds");
7170
8041
  const semanticNodeIds = stringArray(raw.semanticNodeIds, "semanticNodeIds");
7171
- const semanticSelectionIds = stringArray(raw.semanticSelectionIds, "semanticSelectionIds");
8042
+ const semanticSelectionIds = stringArray(raw.semanticSelectionIds, "semanticSelectionIds", false);
7172
8043
  if (semanticIds.length === 0 || semanticNodeIds.length !== semanticIds.length || semanticSelectionIds.length !== semanticIds.length) {
7173
8044
  throw assetError2("ICE3_ASSET_BATCH_INVALID", "semantic arrays are inconsistent");
7174
8045
  }
@@ -7235,8 +8106,8 @@ function readBatchingMetadata(profile, sourceNodes, sourceMeshes, sourceMaterial
7235
8106
  batches
7236
8107
  };
7237
8108
  }
7238
- function stringArray(value, field) {
7239
- if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string" || !entry.trim()) || new Set(value).size !== value.length) {
8109
+ function stringArray(value, field, unique = true) {
8110
+ if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string" || !entry.trim()) || unique && new Set(value).size !== value.length) {
7240
8111
  throw assetError2("ICE3_ASSET_BATCH_INVALID", `${field} is invalid`);
7241
8112
  }
7242
8113
  return value;
@@ -7428,7 +8299,9 @@ function materialColor(material) {
7428
8299
  const color = record(material.pbrMetallicRoughness)?.baseColorFactor;
7429
8300
  if (!Array.isArray(color) || color.length < 3)
7430
8301
  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("")}`;
8302
+ const alpha = color.length >= 4 ? Number(color[3]) : 1;
8303
+ 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"));
8304
+ return `#${channels.join("")}`;
7432
8305
  }
7433
8306
  function integers(value) {
7434
8307
  return Array.isArray(value) ? value.filter((entry) => Number.isInteger(entry)) : [];
@@ -7443,6 +8316,494 @@ function assetError2(code, message) {
7443
8316
  return new Error(`${code}: ${message}`);
7444
8317
  }
7445
8318
 
8319
+ // packages/assets/dist/ICEPreviewGeometryPacket.js
8320
+ var MAGIC = "ICE3PGP1";
8321
+ var HEADER_BYTES = 120;
8322
+ var MANIFEST_LENGTH_OFFSET = 40;
8323
+ var BINARY_LENGTH_OFFSET = 48;
8324
+ var MANIFEST_DIGEST_OFFSET = 56;
8325
+ var CONTENT_DIGEST_OFFSET = 88;
8326
+ var MAX_PACKET_BYTES = 256 * 1024 * 1024;
8327
+ var MAX_NODES = 1e6;
8328
+ var MAX_GEOMETRIES = 25e4;
8329
+ var MAX_RANGES = 64;
8330
+ var encoder = new TextEncoder();
8331
+ var decoder = new TextDecoder("utf-8", { fatal: true });
8332
+ function parsePreviewGeometryPacket(input) {
8333
+ const bytes = input instanceof Uint8Array ? input : new Uint8Array(input);
8334
+ if (bytes.byteLength < HEADER_BYTES || bytes.byteLength > MAX_PACKET_BYTES) {
8335
+ throw new RangeError("ICE3 preview packet size is invalid");
8336
+ }
8337
+ if (decoder.decode(bytes.subarray(0, 8)) !== MAGIC) {
8338
+ throw new TypeError("ICE3 preview packet magic is invalid");
8339
+ }
8340
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
8341
+ const schemaVersion = view.getUint16(8, true);
8342
+ const transportVersion = view.getUint16(10, true);
8343
+ const headerBytes = view.getUint32(12, true);
8344
+ if (schemaVersion !== 1 || transportVersion !== 1 || headerBytes !== HEADER_BYTES) {
8345
+ throw new TypeError("ICE3 preview packet version is unsupported");
8346
+ }
8347
+ const manifestLength = readSafeU64(view, MANIFEST_LENGTH_OFFSET);
8348
+ const binaryLength = readSafeU64(view, BINARY_LENGTH_OFFSET);
8349
+ if (manifestLength === 0 || manifestLength % 8 !== 0 || HEADER_BYTES + manifestLength + binaryLength !== bytes.byteLength) {
8350
+ throw new RangeError("ICE3 preview packet lengths are invalid");
8351
+ }
8352
+ const manifestStart = HEADER_BYTES;
8353
+ const manifestEnd = manifestStart + manifestLength;
8354
+ const manifestBytes = bytes.subarray(manifestStart, manifestEnd);
8355
+ const binary = bytes.subarray(manifestEnd);
8356
+ const expectedManifestDigest = bytes.subarray(MANIFEST_DIGEST_OFFSET, MANIFEST_DIGEST_OFFSET + 32);
8357
+ if (!equalBytes(sha256(manifestBytes), expectedManifestDigest)) {
8358
+ throw new TypeError("ICE3 preview packet manifest digest is invalid");
8359
+ }
8360
+ const contentHash = sha256(concatBytes(manifestBytes, binary));
8361
+ const expectedContentDigest = bytes.subarray(CONTENT_DIGEST_OFFSET, CONTENT_DIGEST_OFFSET + 32);
8362
+ if (!equalBytes(contentHash, expectedContentDigest)) {
8363
+ throw new TypeError("ICE3 preview packet content digest is invalid");
8364
+ }
8365
+ let candidate;
8366
+ try {
8367
+ candidate = JSON.parse(decoder.decode(manifestBytes));
8368
+ } catch {
8369
+ throw new TypeError("ICE3 preview packet manifest is invalid");
8370
+ }
8371
+ const manifest = validateManifest(candidate, binary.byteLength);
8372
+ return {
8373
+ ...manifest,
8374
+ geometries: manifest.geometries.map((geometry) => decodeGeometry(geometry, binary)),
8375
+ binary
8376
+ };
8377
+ }
8378
+ function applyPreviewGeometryDelta(base, delta) {
8379
+ if (delta.parentRevision !== base.revision) {
8380
+ throw new RangeError(`ICE3 preview packet parent revision ${delta.parentRevision ?? "missing"} does not match ${base.revision}`);
8381
+ }
8382
+ const nodes = mergeById(base.nodes, delta.nodes, delta.removedNodeIds, (node) => node.nodeId);
8383
+ const geometries = mergeGeometries(base.geometries, delta, delta.removedGeometryIds);
8384
+ const materials = mergeById(base.materials, delta.materials, [], (material) => material.materialId);
8385
+ return {
8386
+ schemaVersion: delta.schemaVersion,
8387
+ transportVersion: delta.transportVersion,
8388
+ revision: delta.revision,
8389
+ parentRevision: delta.parentRevision,
8390
+ sourceDigest: delta.sourceDigest,
8391
+ backend: delta.backend,
8392
+ residencyMode: delta.residencyMode,
8393
+ nodes,
8394
+ geometries,
8395
+ materials,
8396
+ removedNodeIds: [],
8397
+ removedGeometryIds: [],
8398
+ binary: delta.binary
8399
+ };
8400
+ }
8401
+ function mergeGeometries(baseGeometries, delta, removedGeometryIds) {
8402
+ const removed = new Set(removedGeometryIds);
8403
+ const updates = new Map(delta.geometries.map((geometry) => [geometry.geometryId, geometry]));
8404
+ const output = [];
8405
+ for (const base of baseGeometries) {
8406
+ if (removed.has(base.geometryId)) {
8407
+ continue;
8408
+ }
8409
+ const update = updates.get(base.geometryId);
8410
+ output.push(update ? mergeGeometry(base, update) : base);
8411
+ updates.delete(base.geometryId);
8412
+ }
8413
+ output.push(...updates.values());
8414
+ return Object.freeze(output);
8415
+ }
8416
+ function mergeGeometry(base, update) {
8417
+ if (base === update) {
8418
+ return base;
8419
+ }
8420
+ return freezeGeometry(update, base.attributeDigest === update.attributeDigest ? base.positions : update.positions, base.topologyDigest === update.topologyDigest ? base.indices : update.indices);
8421
+ }
8422
+ function validateManifest(value, binaryLength) {
8423
+ if (!isRecord(value)) {
8424
+ throw invalidManifest();
8425
+ }
8426
+ const nodes = readArray(value.nodes, MAX_NODES).map(readNode);
8427
+ const geometries = readArray(value.geometries, MAX_GEOMETRIES).map(readGeometryRecord);
8428
+ const materials = readArray(value.materials, MAX_GEOMETRIES).map(readMaterial);
8429
+ const removedNodeIds = readStringArray(value.removedNodeIds, MAX_NODES);
8430
+ const removedGeometryIds = readStringArray(value.removedGeometryIds, MAX_GEOMETRIES);
8431
+ const manifest = {
8432
+ schemaVersion: readVersion(value.schemaVersion, 1),
8433
+ transportVersion: readVersion(value.transportVersion, 1),
8434
+ revision: readSafeInteger(value.revision, 0, Number.MAX_SAFE_INTEGER),
8435
+ parentRevision: value.parentRevision === null ? null : readSafeInteger(value.parentRevision, 0, Number.MAX_SAFE_INTEGER),
8436
+ sourceDigest: readDigest(value.sourceDigest),
8437
+ backend: readEnum(value.backend, ["cpu", "gpu"]),
8438
+ residencyMode: readEnum(value.residencyMode, ["cpu", "cold", "warm", "resident", "snapshot"]),
8439
+ nodes,
8440
+ geometries,
8441
+ materials,
8442
+ removedNodeIds,
8443
+ removedGeometryIds
8444
+ };
8445
+ if (manifest.parentRevision !== null && manifest.parentRevision >= manifest.revision) {
8446
+ throw invalidManifest();
8447
+ }
8448
+ ensureUnique(nodes, (node) => node.nodeId);
8449
+ ensureUnique(geometries, (geometry) => geometry.geometryId);
8450
+ ensureUnique(materials, (material) => material.materialId);
8451
+ for (const geometry of geometries) {
8452
+ for (const range of geometry.ranges) {
8453
+ if (range.byteOffset % 4 !== 0 || range.byteLength === 0 || range.byteOffset + range.byteLength > binaryLength) {
8454
+ throw invalidManifest();
8455
+ }
8456
+ }
8457
+ }
8458
+ return manifest;
8459
+ }
8460
+ function readNode(value, index) {
8461
+ if (!isRecord(value)) {
8462
+ throw new TypeError(`ICE3 preview node ${index} is invalid`);
8463
+ }
8464
+ const matrix = readArray(value.matrix, 16);
8465
+ if (matrix.some((entry) => typeof entry !== "number" || !Number.isFinite(entry))) {
8466
+ throw new TypeError(`ICE3 preview node ${index} matrix is invalid`);
8467
+ }
8468
+ return {
8469
+ nodeId: readNonEmptyString(value.nodeId),
8470
+ parentId: readNullableString(value.parentId),
8471
+ name: readString(value.name),
8472
+ matrix: Object.freeze(matrix),
8473
+ materialId: readNullableString(value.materialId),
8474
+ geometryId: readNullableString(value.geometryId),
8475
+ visible: readBoolean(value.visible)
8476
+ };
8477
+ }
8478
+ function readGeometryRecord(value, index) {
8479
+ if (!isRecord(value)) {
8480
+ throw new TypeError(`ICE3 preview geometry ${index} is invalid`);
8481
+ }
8482
+ const indexWidth = readSafeInteger(value.indexWidth, 1, 8);
8483
+ if (indexWidth !== 2 && indexWidth !== 4) {
8484
+ throw new TypeError(`ICE3 preview geometry ${index} index width is invalid`);
8485
+ }
8486
+ const ranges = readArray(value.ranges, MAX_RANGES).map(readRange);
8487
+ return {
8488
+ geometryId: readNonEmptyString(value.geometryId),
8489
+ residentGeometryId: readNullableString(value.residentGeometryId),
8490
+ topologyDigest: readDigest(value.topologyDigest),
8491
+ attributeDigest: readDigest(value.attributeDigest),
8492
+ vertexCount: readSafeInteger(value.vertexCount, 1, 1e8),
8493
+ indexCount: readSafeInteger(value.indexCount, 0, 3e8),
8494
+ indexWidth,
8495
+ ranges: Object.freeze(ranges)
8496
+ };
8497
+ }
8498
+ function readRange(value, index) {
8499
+ if (!isRecord(value)) {
8500
+ throw new TypeError(`ICE3 preview range ${index} is invalid`);
8501
+ }
8502
+ return {
8503
+ byteOffset: readSafeInteger(value.byteOffset, 0, MAX_PACKET_BYTES),
8504
+ byteLength: readSafeInteger(value.byteLength, 1, MAX_PACKET_BYTES),
8505
+ usage: readEnum(value.usage, ["vertex_attributes", "indices"])
8506
+ };
8507
+ }
8508
+ function readMaterial(value, index) {
8509
+ if (!isRecord(value)) {
8510
+ throw new TypeError(`ICE3 preview material ${index} is invalid`);
8511
+ }
8512
+ const baseColor = readFiniteArray(value.baseColor, 4);
8513
+ return {
8514
+ materialId: readNonEmptyString(value.materialId),
8515
+ baseColor: Object.freeze(baseColor),
8516
+ metallic: readFiniteNumber(value.metallic),
8517
+ roughness: readFiniteNumber(value.roughness),
8518
+ opacity: readFiniteNumber(value.opacity)
8519
+ };
8520
+ }
8521
+ function decodeGeometry(geometry, binary) {
8522
+ const attributes = geometry.ranges.find((range) => range.usage === "vertex_attributes");
8523
+ const topology = geometry.ranges.find((range) => range.usage === "indices");
8524
+ if (!attributes && !topology) {
8525
+ return freezeGeometry(encodeIdentity(geometry), new Float32Array(), new Uint32Array());
8526
+ }
8527
+ const positions = attributes ? decodePositions(attributes, geometry.vertexCount, binary) : new Float32Array(geometry.vertexCount * 3);
8528
+ const indices = topology ? decodeIndices(topology, geometry.indexCount, geometry.indexWidth, binary) : new Uint32Array();
8529
+ return freezeGeometry(encodeIdentity(geometry), positions, indices);
8530
+ }
8531
+ function decodePositions(range, vertexCount, binary) {
8532
+ const bytesPerVertex = range.byteLength / vertexCount;
8533
+ const components = bytesPerVertex / 4;
8534
+ if (!Number.isInteger(components) || components < 3) {
8535
+ throw new TypeError("ICE3 preview position range has an invalid stride");
8536
+ }
8537
+ const source = new Float32Array(binary.buffer, binary.byteOffset + range.byteOffset, range.byteLength / 4);
8538
+ if (components === 3) {
8539
+ return source;
8540
+ }
8541
+ const positions = new Float32Array(vertexCount * 3);
8542
+ for (let vertex = 0; vertex < vertexCount; vertex += 1) {
8543
+ const sourceOffset = vertex * components;
8544
+ positions[vertex * 3] = source[sourceOffset];
8545
+ positions[vertex * 3 + 1] = source[sourceOffset + 1];
8546
+ positions[vertex * 3 + 2] = source[sourceOffset + 2];
8547
+ }
8548
+ return positions;
8549
+ }
8550
+ function decodeIndices(range, indexCount, indexWidth, binary) {
8551
+ if (range.byteLength !== indexCount * indexWidth) {
8552
+ throw new TypeError("ICE3 preview index range has an invalid length");
8553
+ }
8554
+ const offset = binary.byteOffset + range.byteOffset;
8555
+ return indexWidth === 2 ? new Uint16Array(binary.buffer, offset, indexCount) : new Uint32Array(binary.buffer, offset, indexCount);
8556
+ }
8557
+ function freezeGeometry(identity, positions, indices) {
8558
+ return Object.freeze({
8559
+ ...identity,
8560
+ positions,
8561
+ indices
8562
+ });
8563
+ }
8564
+ function encodeIdentity(geometry) {
8565
+ return geometry;
8566
+ }
8567
+ function mergeById(base, updates, removed, id) {
8568
+ const removedIds = new Set(removed);
8569
+ const updateMap = new Map(updates.map((value) => [id(value), value]));
8570
+ const output = base.filter((value) => !removedIds.has(id(value))).map((value) => updateMap.get(id(value)) ?? value);
8571
+ const baseIds = new Set(base.map(id));
8572
+ for (const value of updates) {
8573
+ if (!baseIds.has(id(value))) {
8574
+ output.push(value);
8575
+ }
8576
+ }
8577
+ return Object.freeze(output);
8578
+ }
8579
+ function readArray(value, maximum) {
8580
+ if (!Array.isArray(value) || value.length > maximum) {
8581
+ throw invalidManifest();
8582
+ }
8583
+ return value;
8584
+ }
8585
+ function readStringArray(value, maximum) {
8586
+ return Object.freeze(readArray(value, maximum).map(readString));
8587
+ }
8588
+ function readFiniteArray(value, length) {
8589
+ const values = readArray(value, length);
8590
+ if (values.length !== length || values.some((entry) => typeof entry !== "number" || !Number.isFinite(entry))) {
8591
+ throw invalidManifest();
8592
+ }
8593
+ return values;
8594
+ }
8595
+ function readVersion(value, expected) {
8596
+ if (value !== expected) {
8597
+ throw new TypeError("ICE3 preview packet version is unsupported");
8598
+ }
8599
+ return expected;
8600
+ }
8601
+ function readSafeInteger(value, minimum, maximum) {
8602
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < minimum || value > maximum) {
8603
+ throw invalidManifest();
8604
+ }
8605
+ return value;
8606
+ }
8607
+ function readFiniteNumber(value) {
8608
+ if (typeof value !== "number" || !Number.isFinite(value)) {
8609
+ throw invalidManifest();
8610
+ }
8611
+ return value;
8612
+ }
8613
+ function readBoolean(value) {
8614
+ if (typeof value !== "boolean") {
8615
+ throw invalidManifest();
8616
+ }
8617
+ return value;
8618
+ }
8619
+ function readString(value) {
8620
+ if (typeof value !== "string") {
8621
+ throw invalidManifest();
8622
+ }
8623
+ return value;
8624
+ }
8625
+ function readNonEmptyString(value) {
8626
+ const result = readString(value);
8627
+ if (!result.trim()) {
8628
+ throw invalidManifest();
8629
+ }
8630
+ return result;
8631
+ }
8632
+ function readNullableString(value) {
8633
+ return value === null ? null : readString(value);
8634
+ }
8635
+ function readDigest(value) {
8636
+ const digest = readString(value);
8637
+ if (!/^[0-9a-fA-F]{64}$/.test(digest)) {
8638
+ throw invalidManifest();
8639
+ }
8640
+ return digest.toLowerCase();
8641
+ }
8642
+ function readEnum(value, allowed) {
8643
+ if (typeof value !== "string" || !allowed.includes(value)) {
8644
+ throw invalidManifest();
8645
+ }
8646
+ return value;
8647
+ }
8648
+ function ensureUnique(values, id) {
8649
+ if (new Set(values.map(id)).size !== values.length) {
8650
+ throw invalidManifest();
8651
+ }
8652
+ }
8653
+ function invalidManifest() {
8654
+ return new TypeError("ICE3 preview packet manifest is invalid");
8655
+ }
8656
+ function isRecord(value) {
8657
+ return typeof value === "object" && value !== null && !Array.isArray(value);
8658
+ }
8659
+ function readSafeU64(view, offset) {
8660
+ const value = view.getBigUint64(offset, true);
8661
+ if (value > BigInt(Number.MAX_SAFE_INTEGER)) {
8662
+ throw new RangeError("ICE3 preview packet length exceeds safe integer range");
8663
+ }
8664
+ return Number(value);
8665
+ }
8666
+ function concatBytes(left, right) {
8667
+ const output = new Uint8Array(left.length + right.length);
8668
+ output.set(left);
8669
+ output.set(right, left.length);
8670
+ return output;
8671
+ }
8672
+ function equalBytes(left, right) {
8673
+ return left.length === right.length && left.every((value, index) => value === right[index]);
8674
+ }
8675
+ var SHA256_CONSTANTS = new Uint32Array([
8676
+ 1116352408,
8677
+ 1899447441,
8678
+ 3049323471,
8679
+ 3921009573,
8680
+ 961987163,
8681
+ 1508970993,
8682
+ 2453635748,
8683
+ 2870763221,
8684
+ 3624381080,
8685
+ 310598401,
8686
+ 607225278,
8687
+ 1426881987,
8688
+ 1925078388,
8689
+ 2162078206,
8690
+ 2614888103,
8691
+ 3248222580,
8692
+ 3835390401,
8693
+ 4022224774,
8694
+ 264347078,
8695
+ 604807628,
8696
+ 770255983,
8697
+ 1249150122,
8698
+ 1555081692,
8699
+ 1996064986,
8700
+ 2554220882,
8701
+ 2821834349,
8702
+ 2952996808,
8703
+ 3210313671,
8704
+ 3336571891,
8705
+ 3584528711,
8706
+ 113926993,
8707
+ 338241895,
8708
+ 666307205,
8709
+ 773529912,
8710
+ 1294757372,
8711
+ 1396182291,
8712
+ 1695183700,
8713
+ 1986661051,
8714
+ 2177026350,
8715
+ 2456956037,
8716
+ 2730485921,
8717
+ 2820302411,
8718
+ 3259730800,
8719
+ 3345764771,
8720
+ 3516065817,
8721
+ 3600352804,
8722
+ 4094571909,
8723
+ 275423344,
8724
+ 430227734,
8725
+ 506948616,
8726
+ 659060556,
8727
+ 883997877,
8728
+ 958139571,
8729
+ 1322822218,
8730
+ 1537002063,
8731
+ 1747873779,
8732
+ 1955562222,
8733
+ 2024104815,
8734
+ 2227730452,
8735
+ 2361852424,
8736
+ 2428436474,
8737
+ 2756734187,
8738
+ 3204031479,
8739
+ 3329325298
8740
+ ]);
8741
+ function sha256(input) {
8742
+ const bitLength = input.length * 8;
8743
+ const paddedLength = Math.ceil((input.length + 9) / 64) * 64;
8744
+ const data = new Uint8Array(paddedLength);
8745
+ data.set(input);
8746
+ data[input.length] = 128;
8747
+ const view = new DataView(data.buffer);
8748
+ view.setUint32(paddedLength - 8, Math.floor(bitLength / 4294967296));
8749
+ view.setUint32(paddedLength - 4, bitLength >>> 0);
8750
+ const hash = new Uint32Array([
8751
+ 1779033703,
8752
+ 3144134277,
8753
+ 1013904242,
8754
+ 2773480762,
8755
+ 1359893119,
8756
+ 2600822924,
8757
+ 528734635,
8758
+ 1541459225
8759
+ ]);
8760
+ const words = new Uint32Array(64);
8761
+ for (let offset = 0; offset < data.length; offset += 64) {
8762
+ for (let index = 0; index < 16; index += 1) {
8763
+ words[index] = view.getUint32(offset + index * 4);
8764
+ }
8765
+ for (let index = 16; index < 64; index += 1) {
8766
+ const s0 = rotateRight(words[index - 15], 7) ^ rotateRight(words[index - 15], 18) ^ words[index - 15] >>> 3;
8767
+ const s1 = rotateRight(words[index - 2], 17) ^ rotateRight(words[index - 2], 19) ^ words[index - 2] >>> 10;
8768
+ words[index] = words[index - 16] + s0 + words[index - 7] + s1 >>> 0;
8769
+ }
8770
+ let [a, b, c, d, e, f, g, h] = hash;
8771
+ for (let index = 0; index < 64; index += 1) {
8772
+ const sum1 = rotateRight(e, 6) ^ rotateRight(e, 11) ^ rotateRight(e, 25);
8773
+ const choice = e & f ^ ~e & g;
8774
+ const temp1 = h + sum1 + choice + SHA256_CONSTANTS[index] + words[index] >>> 0;
8775
+ const sum0 = rotateRight(a, 2) ^ rotateRight(a, 13) ^ rotateRight(a, 22);
8776
+ const majority = a & b ^ a & c ^ b & c;
8777
+ const temp2 = sum0 + majority >>> 0;
8778
+ h = g;
8779
+ g = f;
8780
+ f = e;
8781
+ e = d + temp1 >>> 0;
8782
+ d = c;
8783
+ c = b;
8784
+ b = a;
8785
+ a = temp1 + temp2 >>> 0;
8786
+ }
8787
+ hash[0] = hash[0] + a >>> 0;
8788
+ hash[1] = hash[1] + b >>> 0;
8789
+ hash[2] = hash[2] + c >>> 0;
8790
+ hash[3] = hash[3] + d >>> 0;
8791
+ hash[4] = hash[4] + e >>> 0;
8792
+ hash[5] = hash[5] + f >>> 0;
8793
+ hash[6] = hash[6] + g >>> 0;
8794
+ hash[7] = hash[7] + h >>> 0;
8795
+ }
8796
+ const output = new Uint8Array(32);
8797
+ const outputView = new DataView(output.buffer);
8798
+ for (let index = 0; index < hash.length; index += 1) {
8799
+ outputView.setUint32(index * 4, hash[index], false);
8800
+ }
8801
+ return output;
8802
+ }
8803
+ function rotateRight(value, bits) {
8804
+ return value >>> bits | value << 32 - bits;
8805
+ }
8806
+
7446
8807
  // packages/assets/dist/ICETilesetReader.js
7447
8808
  var IDENTITY = [
7448
8809
  1,
@@ -7715,7 +9076,11 @@ function parseTransform(value) {
7715
9076
  return matrix;
7716
9077
  }
7717
9078
  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)) {
9079
+ if ([0, 1, 2].some((axis) => {
9080
+ 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]));
9081
+ const tolerance = EPSILON * scale;
9082
+ return child.min[axis] < parent.min[axis] - tolerance || child.max[axis] > parent.max[axis] + tolerance;
9083
+ })) {
7719
9084
  throw invalid(`child bounds escape parent tile ${id}`);
7720
9085
  }
7721
9086
  }
@@ -9212,12 +10577,6 @@ var ICETilesetHandle = class extends ICEEventDispatcher {
9212
10577
  this.assetLoader.clear(uri);
9213
10578
  this.states.set(tileId, "evicted");
9214
10579
  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
10580
  this.engine.requestRender("tileset-tile-unloaded");
9222
10581
  }
9223
10582
  hasPendingVisibleTargets() {
@@ -9271,7 +10630,6 @@ var ICETilesetHandle = class extends ICEEventDispatcher {
9271
10630
  this.failedLodFallbacks.delete(fallbackId);
9272
10631
  }
9273
10632
  this.visibleCoverageIds = targetIds;
9274
- this.reconcileSelectionVisibility();
9275
10633
  this.engine.requestRender("tileset-visible-target-committed");
9276
10634
  if (!this.readySettled) {
9277
10635
  this.readySettled = true;
@@ -9309,17 +10667,6 @@ var ICETilesetHandle = class extends ICEEventDispatcher {
9309
10667
  }
9310
10668
  return visible;
9311
10669
  }
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
10670
  rejectReadyIfVisibleSetFailed() {
9324
10671
  if (this.readySettled || !this.workSet)
9325
10672
  return;
@@ -9728,34 +11075,6 @@ function collectNodeIDs(node, output) {
9728
11075
  for (const child of node.children)
9729
11076
  collectNodeIDs(child, output);
9730
11077
  }
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
11078
  function throwIfAborted(signal) {
9760
11079
  if (signal?.aborted)
9761
11080
  throw abortedTilesetError();
@@ -9766,6 +11085,72 @@ function isAbortError(error) {
9766
11085
  function abortedTilesetError() {
9767
11086
  return new Error("ICE3_TILESET_ABORTED: Tileset loading was aborted");
9768
11087
  }
11088
+
11089
+ // packages/runtime/dist/asset/ICEPreviewPacketLoader.js
11090
+ var ICEPreviewPacketLoader = class {
11091
+ fetcher;
11092
+ pinGeometry;
11093
+ geometries = /* @__PURE__ */ new Map();
11094
+ publishedScene = null;
11095
+ constructor(options = {}) {
11096
+ const normalized = typeof options === "function" ? { fetch: options } : options;
11097
+ this.fetcher = normalized.fetch ?? globalThis.fetch.bind(globalThis);
11098
+ this.pinGeometry = normalized.pinGeometry;
11099
+ }
11100
+ get scene() {
11101
+ return this.publishedScene;
11102
+ }
11103
+ get currentGeometries() {
11104
+ return this.geometries;
11105
+ }
11106
+ async load(url, current) {
11107
+ let bytes;
11108
+ try {
11109
+ const response = await this.fetcher(url, {
11110
+ headers: {
11111
+ accept: "application/vnd.ice3.preview-geometry; version=1"
11112
+ }
11113
+ });
11114
+ if (!response.ok) {
11115
+ return { kind: "fallback", reason: "packet-unavailable" };
11116
+ }
11117
+ bytes = new Uint8Array(await response.arrayBuffer());
11118
+ } catch {
11119
+ return { kind: "fallback", reason: "packet-unavailable" };
11120
+ }
11121
+ let incoming;
11122
+ try {
11123
+ incoming = parsePreviewGeometryPacket(bytes);
11124
+ } catch {
11125
+ return { kind: "fallback", reason: "packet-invalid" };
11126
+ }
11127
+ let packet = incoming;
11128
+ if (incoming.parentRevision !== null) {
11129
+ if (!current || incoming.parentRevision !== current.revision) {
11130
+ return { kind: "fallback", reason: "packet-revision-mismatch" };
11131
+ }
11132
+ packet = applyPreviewGeometryDelta(current, incoming);
11133
+ }
11134
+ let built;
11135
+ try {
11136
+ built = buildPreviewPacketScene(packet, this.geometries);
11137
+ if (this.pinGeometry) {
11138
+ for (const [geometryId, update] of built.geometryUpdates) {
11139
+ const geometry = built.geometries.get(geometryId);
11140
+ if (!geometry) {
11141
+ throw new Error("ICE3_PREVIEW_PACKET_INVALID: geometry is missing");
11142
+ }
11143
+ this.pinGeometry(geometry, update);
11144
+ }
11145
+ }
11146
+ } catch {
11147
+ return { kind: "fallback", reason: "packet-invalid" };
11148
+ }
11149
+ this.geometries = built.geometries;
11150
+ this.publishedScene = built.scene;
11151
+ return { kind: "packet", packet };
11152
+ }
11153
+ };
9769
11154
  export {
9770
11155
  ICE3,
9771
11156
  ICEAssetLoader,
@@ -9778,13 +11163,19 @@ export {
9778
11163
  ICENode,
9779
11164
  ICEOrbitController,
9780
11165
  ICEPerspectiveCamera,
11166
+ ICEPreviewPacketLoader,
9781
11167
  ICEScene,
9782
11168
  ICEScreenLayer,
9783
11169
  ICETilesetHandle,
9784
11170
  ICETilesetLoader,
9785
11171
  ICETypeRegistry,
9786
11172
  ICEWorldLayer,
11173
+ collectSceneItems,
9787
11174
  deserializeScene,
9788
11175
  serializeScene
9789
11176
  };
11177
+ //! 方向光阴影贴图。
11178
+ //!
11179
+ //! 一次深度 pass 把场景从光源方向渲染进深度纹理,主 pass 再按世界坐标比较深度。
11180
+ //! 只处理单方向光、单张贴图,够覆盖当前查看器的需要;级联与软阴影留给后续。
9790
11181
  //# sourceMappingURL=index.js.map