ice3 0.2.1 → 0.2.3

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
@@ -389,22 +389,41 @@ var ICEIndexedGeometry = class {
389
389
  };
390
390
 
391
391
  // packages/core/dist/material/ICEBasicMaterial.js
392
+ var DEFAULT_ROUGHNESS = 0.85;
392
393
  var ICEBasicMaterial = class {
393
394
  typeId = "ice3:BasicMaterial";
394
395
  color;
395
396
  edgeColor;
396
397
  shading;
398
+ map;
399
+ normalMap;
400
+ ormMap;
401
+ metallic;
402
+ roughness;
403
+ emissive;
397
404
  constructor(options) {
398
405
  this.color = options.color;
399
406
  this.edgeColor = options.edgeColor;
400
407
  this.shading = options.shading;
408
+ this.map = options.map;
409
+ this.normalMap = options.normalMap;
410
+ this.ormMap = options.ormMap;
411
+ this.metallic = options.metallic ?? 0;
412
+ this.roughness = options.roughness ?? DEFAULT_ROUGHNESS;
413
+ this.emissive = options.emissive;
401
414
  }
402
415
  toJSON() {
403
416
  return {
404
417
  typeId: this.typeId,
405
418
  color: this.color,
406
419
  ...this.edgeColor ? { edgeColor: this.edgeColor } : {},
407
- ...this.shading ? { shading: this.shading } : {}
420
+ ...this.shading ? { shading: this.shading } : {},
421
+ ...this.map ? { map: this.map } : {},
422
+ ...this.normalMap ? { normalMap: this.normalMap } : {},
423
+ ...this.ormMap ? { ormMap: this.ormMap } : {},
424
+ ...this.metallic !== 0 ? { metallic: this.metallic } : {},
425
+ ...this.roughness !== DEFAULT_ROUGHNESS ? { roughness: this.roughness } : {},
426
+ ...this.emissive ? { emissive: this.emissive } : {}
408
427
  };
409
428
  }
410
429
  };
@@ -450,10 +469,17 @@ var ICEPerspectiveCamera = class {
450
469
  getTarget() {
451
470
  return this.lookTarget;
452
471
  }
472
+ /**
473
+ * 投影矩阵。屏幕空间效果要从深度重建视空间坐标,光有 viewProjection 不够。
474
+ */
475
+ getProjectionMatrix(aspect) {
476
+ return new ICEMatrix4().perspective(this.fov, aspect, this.near, this.far);
477
+ }
478
+ getViewMatrix() {
479
+ return new ICEMatrix4().lookAt(this.eye, this.lookTarget, new ICEVector3(0, 1, 0));
480
+ }
453
481
  getViewProjectionMatrix(aspect) {
454
- const projection = new ICEMatrix4().perspective(this.fov, aspect, this.near, this.far);
455
- const view = new ICEMatrix4().lookAt(this.eye, this.lookTarget, new ICEVector3(0, 1, 0));
456
- return projection.multiply(view);
482
+ return this.getProjectionMatrix(aspect).multiply(this.getViewMatrix());
457
483
  }
458
484
  };
459
485
 
@@ -972,6 +998,7 @@ void main() {
972
998
  var FRAGMENT_SOURCE = `#version 300 es
973
999
  precision highp float;
974
1000
  uniform vec4 u_color;
1001
+ uniform vec4 u_clipPlane;
975
1002
  uniform float u_pickMode;
976
1003
  uniform float u_useVertexColor;
977
1004
  uniform float u_useDirectionalShading;
@@ -980,11 +1007,47 @@ uniform vec3 u_cameraPosition;
980
1007
  uniform sampler2D u_shadowMap;
981
1008
  uniform mat4 u_lightViewProjection;
982
1009
  uniform float u_shadowEnabled;
1010
+ uniform float u_shadowNormalOffset;
1011
+ uniform float u_shadowDebug;
1012
+ uniform sampler2D u_baseColorMap;
1013
+ uniform float u_useBaseColorMap;
1014
+ uniform vec2 u_uvScale;
1015
+ uniform sampler2D u_normalMap;
1016
+ uniform float u_useNormalMap;
1017
+ uniform sampler2D u_ormMap;
1018
+ uniform float u_useOrmMap;
1019
+ uniform float u_ormOcclusion;
1020
+ uniform float u_ormMetallicRoughness;
1021
+ uniform float u_normalScale;
1022
+ uniform float u_metallic;
1023
+ uniform float u_roughness;
1024
+ uniform vec3 u_emissive;
1025
+ uniform sampler2D u_sceneDepth;
1026
+ uniform float u_useSceneDepth;
1027
+ uniform mat4 u_projection;
1028
+ uniform vec2 u_viewport;
1029
+ uniform vec2 u_clipRange;
1030
+ uniform float u_aoRadius;
1031
+ uniform float u_aoStrength;
983
1032
  in vec3 v_color;
984
1033
  in vec3 v_worldPosition;
985
1034
  out vec4 outputColor;
986
1035
 
1036
+ // \u7531\u5C4F\u5E55 UV \u4E0E\u6DF1\u5EA6\u7F13\u51B2\u91CD\u5EFA\u89C6\u7A7A\u95F4\u5750\u6807\uFF08\u4E0E WebGL2DepthMath \u7684\u6362\u7B97\u4E00\u81F4\uFF09\u3002
1037
+ vec3 viewPositionAt(vec2 uv, float tanHalfFov, float aspect, float near, float far) {
1038
+ float depth = texture(u_sceneDepth, uv).r;
1039
+ float ndcZ = depth * 2.0 - 1.0;
1040
+ float linear = (2.0 * near * far) / (far + near - ndcZ * (far - near));
1041
+ vec2 ndc = uv * 2.0 - 1.0;
1042
+ return vec3(ndc.x * tanHalfFov * aspect * linear, ndc.y * tanHalfFov * linear, -linear);
1043
+ }
1044
+
987
1045
  void main() {
1046
+ // Section plane: discard the half-space on the negative side. A zero plane
1047
+ // (0,0,0,0) evaluates to 0 and keeps everything, so it doubles as "off".
1048
+ if (dot(v_worldPosition, u_clipPlane.xyz) + u_clipPlane.w < 0.0) {
1049
+ discard;
1050
+ }
988
1051
  if (u_pickMode > 0.5) {
989
1052
  outputColor = u_color;
990
1053
  return;
@@ -995,19 +1058,74 @@ void main() {
995
1058
  dFdx(v_worldPosition),
996
1059
  dFdy(v_worldPosition)
997
1060
  ));
1061
+ // Box-projected UVs make the tangent frame analytic: the projection plane's
1062
+ // two world axes are the tangents, so no vertex tangent channel is needed.
1063
+ vec3 tangent = vec3(0.0);
1064
+ vec3 bitangent = vec3(0.0);
1065
+ vec3 axisWeight = abs(normal);
1066
+ if (axisWeight.x >= axisWeight.y && axisWeight.x >= axisWeight.z) {
1067
+ tangent = vec3(0.0, 0.0, -sign(normal.x));
1068
+ bitangent = vec3(0.0, 1.0, 0.0);
1069
+ } else if (axisWeight.y >= axisWeight.z) {
1070
+ tangent = vec3(1.0, 0.0, 0.0);
1071
+ bitangent = vec3(0.0, 0.0, sign(normal.y));
1072
+ } else {
1073
+ tangent = vec3(sign(normal.z), 0.0, 0.0);
1074
+ bitangent = vec3(0.0, 1.0, 0.0);
1075
+ }
1076
+ // \u4E09\u5F20\u8D34\u56FE\u5171\u7528\u540C\u4E00\u5957\u76D2\u6295\u5F71 UV\uFF1A\u6CD5\u7EBF\u4E3B\u5BFC\u8F74\u51B3\u5B9A\u6295\u5F71\u5E73\u9762\uFF0C\u5355\u4F4D\u662F\u7C73\u3002
1077
+ // \u5199\u4E00\u6B21\u5C31\u591F\u4E86\u2014\u2014\u5E95\u56FE\u3001\u6CD5\u7EBF\u56FE\u3001ORM \u5404\u5199\u4E00\u904D\u8FDF\u65E9\u4F1A\u6F02\u3002
1078
+ vec2 surfaceUv;
1079
+ if (axisWeight.x >= axisWeight.y && axisWeight.x >= axisWeight.z) {
1080
+ surfaceUv = vec2(-sign(normal.x) * v_worldPosition.z, v_worldPosition.y);
1081
+ } else if (axisWeight.y >= axisWeight.z) {
1082
+ surfaceUv = vec2(v_worldPosition.x, sign(normal.y) * v_worldPosition.z);
1083
+ } else {
1084
+ surfaceUv = vec2(sign(normal.z) * v_worldPosition.x, v_worldPosition.y);
1085
+ }
1086
+ surfaceUv *= u_uvScale;
1087
+ if (u_useNormalMap > 0.5) {
1088
+ vec3 sampled = texture(u_normalMap, surfaceUv).xyz * 2.0 - 1.0;
1089
+ sampled.xy *= u_normalScale;
1090
+ normal = normalize(
1091
+ normalize(tangent) * sampled.x
1092
+ + normalize(bitangent) * sampled.y
1093
+ + normal * sampled.z
1094
+ );
1095
+ }
998
1096
  // \u4E3B\u5149 + \u534A\u7403\u5929\u5149\u3002\u6CD5\u7EBF\u7531\u5C4F\u5E55\u7A7A\u95F4\u5BFC\u6570\u6C42\u51FA\uFF0C\u671D\u5411\u4E0E\u7ED5\u5E8F\u4E00\u81F4\uFF1A
999
1097
  // \u8FCE\u7740\u5149\u7684\u4E00\u9762\u5403\u6EE1\u76F4\u5C04\uFF0C\u80CC\u5149\u9762\u53EA\u7559 35% \u7684\u53CD\u5F39\u5149\uFF0C\u6697\u90E8\u4E0D\u81F3\u4E8E\u7CCA\u6210\u4E00\u7247\u3002
1000
1098
  vec3 lightDirection = normalize(vec3(${LIGHT_DIRECTION.join(", ")}));
1001
1099
  float ndl = dot(normal, lightDirection);
1002
1100
  float key = max(ndl, 0.0) + 0.35 * max(-ndl, 0.0);
1003
1101
  float sky = 0.5 + 0.5 * normal.y;
1102
+ // ORM\uFF1AR \u906E\u853D\u3001G \u7C97\u7CD9\u5EA6\u3001B \u91D1\u5C5E\u5EA6\uFF0C\u548C glTF \u7684\u6253\u5305\u7EA6\u5B9A\u4E00\u81F4\u3002
1103
+ // \u906E\u853D\u53EA\u538B\u95F4\u63A5\u5149\uFF08\u73AF\u5883\u4E0E\u5929\u5149\uFF09\uFF0C\u76F4\u5C04\u7167\u5EA6\u4E0D\u53D7\u5F71\u54CD\u2014\u2014\u5426\u5219\u9634\u5F71\u548C\u8D34\u56FE\u4F1A\u4E92\u76F8\u6253\u67B6\u3002
1104
+ float surfaceOcclusion = 1.0;
1105
+ float surfaceMetallic = u_metallic;
1106
+ float surfaceRoughness = clamp(u_roughness, 0.04, 1.0);
1107
+ if (u_useOrmMap > 0.5) {
1108
+ vec3 orm = texture(u_ormMap, surfaceUv).rgb;
1109
+ if (u_ormOcclusion > 0.5) surfaceOcclusion = clamp(orm.r, 0.0, 1.0);
1110
+ if (u_ormMetallicRoughness > 0.5) {
1111
+ surfaceRoughness = clamp(surfaceRoughness * orm.g, 0.04, 1.0);
1112
+ surfaceMetallic = clamp(surfaceMetallic * orm.b, 0.0, 1.0);
1113
+ }
1114
+ }
1004
1115
  // \u76F4\u5C04\u5149\u5F3A\u5EA6\u6309\u771F\u5B9E\u65E5\u7167\u7ED9\uFF1A\u671D\u4E0A\u7684\u9762\u4F1A\u8D85\u8FC7 1\uFF0C\u8FC7\u66DD\u5230\u63A5\u8FD1\u767D\u8272\u662F\u5BF9\u7684\uFF0C
1005
1116
  // \u4E4B\u524D\u4E0A\u9650\u538B\u5230 1 \u9644\u8FD1\uFF0C\u6574\u4E2A\u5382\u533A\u5C31\u6C38\u8FDC\u6652\u4E0D\u5230\u592A\u9633\uFF0C\u770B\u7740\u53D1\u95F7\u53D1\u6697\u3002
1006
1117
  float shade = clamp(
1007
- ${LIGHTING.ambient} + ${LIGHTING.direct} * key + ${LIGHTING.sky} * sky,
1118
+ (${LIGHTING.ambient} + ${LIGHTING.sky} * sky) * surfaceOcclusion
1119
+ + ${LIGHTING.direct} * key,
1008
1120
  ${LIGHTING.minimum},
1009
1121
  ${LIGHTING.maximum}
1010
1122
  );
1123
+ // Base-colour texture. There is no UV vertex channel: the projection is
1124
+ // derived from the surface normal's dominant axis, in metres, which matches
1125
+ // the coordinates Studio writes into exported glTF.
1126
+ if (u_useBaseColorMap > 0.5) {
1127
+ color *= texture(u_baseColorMap, surfaceUv).rgb;
1128
+ }
1011
1129
  vec3 lit = color * mix(1.0, shade, u_useDirectionalShading);
1012
1130
 
1013
1131
  // \u8868\u9762\u9897\u7C92\uFF1A\u6309\u4E16\u754C\u5750\u6807\u53D6\u4E24\u5C42\u54C8\u5E0C\uFF0C\u6253\u6563\u6574\u7247\u7EAF\u8272\u5E26\u6765\u7684\u5851\u6599\u611F\u3002
@@ -1052,20 +1170,108 @@ void main() {
1052
1170
  + vec3(0.42, 0.52, 0.54) * sparkle * 0.55;
1053
1171
  }
1054
1172
 
1173
+ // \u5C4F\u5E55\u7A7A\u95F4\u906E\u853D\uFF1A\u5148\u628A\u6DF1\u5EA6\u8FD8\u539F\u6210\u89C6\u7A7A\u95F4\u5750\u6807\uFF0C\u518D\u6309**\u4E16\u754C\u534A\u5F84**\u53D6\u6837\u3002
1174
+ // \u76F4\u63A5\u6BD4\u8F83 NDC \u6DF1\u5EA6\u5DEE\u8868\u8FBE\u4E0D\u4E86"1 \u7C73"\u8FD9\u79CD\u91CF\uFF08\u8FDC\u7AEF\u5343\u5206\u4E4B\u51E0\u5C31\u8DE8\u4E86\u5341\u7C73\uFF09\uFF0C
1175
+ // \u8FD9\u4E5F\u662F SPEC-007 \u524D\u4E09\u8F6E\u5931\u8D25\u7684\u539F\u56E0\u3002
1176
+ if (u_useSceneDepth > 0.5) {
1177
+ vec2 screenUv = gl_FragCoord.xy / u_viewport;
1178
+ float tanHalfFov = 1.0 / u_projection[1][1];
1179
+ float aspect = u_projection[1][1] / u_projection[0][0];
1180
+ float near = u_clipRange.x;
1181
+ float far = u_clipRange.y;
1182
+
1183
+ vec3 centre = viewPositionAt(screenUv, tanHalfFov, aspect, near, far);
1184
+ // \u6CD5\u7EBF\u7531\u89C6\u7A7A\u95F4\u4F4D\u7F6E\u7684\u5C4F\u5E55\u5BFC\u6570\u6C42\u51FA\uFF1A\u51E0\u4F55\u6CA1\u6709\u6CD5\u7EBF\u5C5E\u6027\uFF0C\u4E5F\u4E0D\u9700\u8981\u3002
1185
+ vec3 viewNormal = normalize(cross(dFdx(centre), dFdy(centre)));
1186
+ float centreDepth = -centre.z;
1187
+
1188
+ // \u4E16\u754C\u534A\u5F84\u6362\u7B97\u6210\u50CF\u7D20\u534A\u5F84\uFF1A\u8D8A\u8FDC\u8D8A\u5C0F\uFF0C\u8FD1\u666F\u8FDC\u666F\u624D\u90FD\u6709\u610F\u4E49\u3002
1189
+ float pixelRadius = (u_aoRadius / max(1e-4, centreDepth * tanHalfFov)) * (u_viewport.y * 0.5);
1190
+ // \u4EA4\u9519\u68AF\u5EA6\u566A\u58F0\uFF08IGN\uFF09\u4EE3\u66FF sin \u54C8\u5E0C\uFF1A\u540C\u6837\u662F\u9010\u50CF\u7D20\u6270\u52A8\uFF0C\u4F46\u5B83\u5728\u76F8\u90BB\u50CF\u7D20\u95F4\u662F
1191
+ // \u6E10\u53D8\u800C\u975E\u8DF3\u53D8\uFF0C\u91C7\u6837\u7F3A\u53E3\u770B\u8D77\u6765\u50CF\u7EC6\u5BC6\u6296\u52A8\uFF0C\u4E0D\u50CF\u6912\u76D0\u566A\u70B9\u3002
1192
+ float rotation = fract(52.9829189 * fract(dot(gl_FragCoord.xy, vec2(0.06711056, 0.00583715)))) * 6.2831853;
1193
+
1194
+ // \u4E09\u5708\u53D6\u6837\uFF1A0.35 / 0.65 / 1.0 \u500D\u534A\u5F84\uFF0C\u6743\u91CD 1.0 / 0.7 / 0.45\uFF0C\u5206\u522B\u8D1F\u8D23
1195
+ // \u8D34\u5730\u63A5\u89E6\u3001\u7F1D\u9699\u4E2D\u90E8\u3001\u5927\u9762\u79EF\u5939\u89D2\u3002\u6837\u672C\u6309\u4E0E\u6CD5\u7EBF\u7684\u5939\u89D2\u52A0\u6743\u2014\u2014\u6B63\u5BF9\u7740\u7684\u5E73\u9762
1196
+ // \u4E0D\u8BE5\u88AB\u5468\u56F4\u540C\u6837\u5E73\u7684\u4E1C\u897F\u538B\u6697\uFF0C\u53EA\u6709"\u7ACB\u8D77\u6765\u6321\u4F4F\u6211"\u7684\u624D\u7B97\u6CD5\u3002
1197
+ float occlusion = 0.0;
1198
+ float weightSum = 0.0;
1199
+ for (int ring = 0; ring < 3; ring++) {
1200
+ // \u4E09\u5708\uFF1A\u8D34\u5730\u63A5\u89E6 / \u7F1D\u9699\u4E2D\u90E8 / \u5927\u9762\u79EF\u5939\u89D2\u3002\u6CA1\u6709\u6A21\u7CCA\u901A\u9053\u65F6\uFF0C\u9760\u5708\u5C42\u5206\u644A
1201
+ // \u91C7\u6837\u5BC6\u5EA6\u662F\u538B\u566A\u70B9\u6700\u76F4\u63A5\u7684\u529E\u6CD5\uFF08\u6A21\u7CCA\u8981\u989D\u5916\u7684 FBO \u4E0E\u5168\u5C4F\u901A\u9053\uFF09\u3002
1202
+ float ringScale = ring == 0 ? 0.35 : (ring == 1 ? 0.65 : 1.0);
1203
+ float ringWeight = ring == 0 ? 1.0 : (ring == 1 ? 0.7 : 0.45);
1204
+ for (int i = 0; i < 8; i++) {
1205
+ float angle = rotation + float(i) * 0.7853982 + float(ring) * 0.2618;
1206
+ vec2 offset = vec2(cos(angle), sin(angle)) * pixelRadius * ringScale;
1207
+ vec2 sampleUv = screenUv + offset / u_viewport;
1208
+ if (sampleUv.x < 0.0 || sampleUv.x > 1.0 || sampleUv.y < 0.0 || sampleUv.y > 1.0) continue;
1209
+ vec3 delta = viewPositionAt(sampleUv, tanHalfFov, aspect, near, far) - centre;
1210
+ float distance = length(delta);
1211
+ if (distance < 0.0001 || distance > u_aoRadius) continue;
1212
+ float alignment = dot(delta, viewNormal) / distance;
1213
+ if (alignment <= 0.15) continue;
1214
+ occlusion += ringWeight * alignment;
1215
+ weightSum += ringWeight;
1216
+ }
1217
+ }
1218
+ if (weightSum > 0.0) {
1219
+ occlusion /= weightSum;
1220
+ lit *= 1.0 - clamp(occlusion, 0.0, 1.0) * u_aoStrength;
1221
+ }
1222
+ }
1223
+
1224
+ // \u91D1\u5C5E\u6CA1\u6709\u6F2B\u53CD\u5C04\uFF1A\u5B83\u7684\u989C\u8272\u5168\u90E8\u6765\u81EA\u53CD\u5C04\u3002\u5148\u628A\u6F2B\u53CD\u5C04\u6309 metallic \u6536\u6389\uFF0C
1225
+ // \u518D\u7528\u53CD\u5C04\u9879\u8865\u56DE\u6765\uFF0C\u5426\u5219\u91D1\u5C5E\u548C\u5851\u6599\u5728\u540C\u4E00\u4E2A\u592A\u9633\u4E0B\u53EA\u662F"\u989C\u8272\u4E0D\u540C"\u3002
1226
+ {
1227
+ vec3 viewDirection = normalize(u_cameraPosition - v_worldPosition);
1228
+ vec3 halfVector = normalize(lightDirection + viewDirection);
1229
+ float metallic = clamp(surfaceMetallic, 0.0, 1.0);
1230
+ float roughness = clamp(surfaceRoughness, 0.04, 1.0);
1231
+ vec3 reflectance = mix(vec3(0.04), color, metallic);
1232
+
1233
+ float diffuseGain = 1.0 - metallic * 0.85;
1234
+ float facing = max(dot(normal, lightDirection), 0.0);
1235
+
1236
+ // \u9AD8\u5149\uFF1A\u7C97\u7CD9\u5EA6\u51B3\u5B9A\u53F6\u74E3\u5BBD\u5EA6\u4E0E\u5F3A\u5EA6\uFF08\u6307\u6570 96 \u2192 4\uFF0C\u589E\u76CA 1.0 \u2192 0.18\uFF09\u3002
1237
+ float shininess = mix(96.0, 4.0, roughness * roughness);
1238
+ float highlight = pow(max(dot(normal, halfVector), 0.0), shininess);
1239
+ float specularGain = mix(1.0, 0.18, roughness);
1240
+
1241
+ // \u73AF\u5883\u53CD\u5C04\uFF1A\u6309\u53CD\u5C04\u65B9\u5411\u53D6\u5929\u7A7A/\u5730\u9762\u8272\uFF0C\u63A0\u5C04\u89D2\u518D\u53E0\u4E00\u5C42 Fresnel\u3002
1242
+ // \u53EA\u770B\u6CD5\u7EBF\u7684\u8BDD\uFF0C\u5E73\u8EBA\u7684\u91D1\u5C5E\u4F1A\u548C\u7ACB\u7740\u7684\u91D1\u5C5E\u4E00\u4E2A\u989C\u8272\u2014\u2014\u73B0\u5B9E\u91CC\u524D\u8005\u53CD\u5C04\u5929\u9876\u3001
1243
+ // \u540E\u8005\u53CD\u5C04\u5730\u5E73\u7EBF\uFF0C\u6070\u6070\u662F\u91D1\u5C5E\u6700\u5BB9\u6613\u8FA8\u8BA4\u7684\u7279\u5F81\u3002
1244
+ vec3 reflected = reflect(-viewDirection, normal);
1245
+ float skyMix = clamp(reflected.y * 0.9 + 0.35, 0.0, 1.0);
1246
+ vec3 skyColor = mix(vec3(0.15, 0.16, 0.17), vec3(0.62, 0.72, 0.84), skyMix);
1247
+ float fresnel = pow(1.0 - clamp(dot(normal, viewDirection), 0.0, 1.0), 4.0);
1248
+ float envGain = (0.22 + 0.55 * fresnel) * mix(0.4, 1.0, metallic);
1249
+
1250
+ float litScale = diffuseGain;
1251
+ lit *= litScale;
1252
+ lit += reflectance * (highlight * specularGain * mix(0.35, 1.0, facing)
1253
+ + skyColor * envGain);
1254
+ lit += u_emissive;
1255
+ }
1256
+
1055
1257
  // \u9634\u5F71\uFF1A\u628A\u4E16\u754C\u5750\u6807\u6295\u5230\u5149\u6E90\u7A7A\u95F4\uFF0C\u4E0E\u6DF1\u5EA6\u56FE\u6BD4\u8F83\u3002\u53D6 3\xD73 \u91C7\u6837\u7684\u5E73\u5747\u505A\u8F7B\u5FAE\u67D4\u5316\u3002
1258
+ // shadowLit \u5728\u8C03\u8BD5\u901A\u9053\u91CC\u76F4\u63A5\u8F93\u51FA\uFF1A1 = \u53D7\u5149\uFF0C0 = \u88AB\u906E\u3002
1259
+ float shadowLit = 1.0;
1056
1260
  if (u_shadowEnabled > 0.5) {
1057
- vec4 lightSpace = u_lightViewProjection * vec4(v_worldPosition, 1.0);
1261
+ // \u91C7\u6837\u70B9\u6CBF\u6CD5\u7EBF\u63A8\u51FA 1.5 \u4E2A\u7EB9\u7D20\uFF1A\u5E73\u8EBA\u5728\u5149\u91CC\u7684\u9762\uFF08\u5C4B\u9876\u3001\u5730\u9762\uFF09\u5728\u6BCF\u4E2A\u7EB9\u7D20\u5185
1262
+ // \u672C\u8EAB\u5C31\u6709\u6DF1\u5EA6\u5DEE\uFF0C\u9760\u6DF1\u5EA6\u504F\u5DEE\u53BB\u8FFD\u8FD9\u4E2A\u5DEE\u53EA\u4F1A\u8BA9\u5F71\u5B50\u8131\u79BB\u7269\u4F53\uFF08peter-panning\uFF09\u3002
1263
+ // \u628A\u6BD4\u8F83\u70B9\u62AC\u79BB\u8868\u9762\u624D\u662F\u6B63\u89E3\uFF0C\u504F\u79FB\u91CF\u6309\u7EB9\u7D20\u6362\u7B97\uFF0C\u548C\u8D34\u56FE\u5206\u8FA8\u7387\u89E3\u8026\u3002
1264
+ vec3 shadowPosition = v_worldPosition + normal * u_shadowNormalOffset;
1265
+ vec4 lightSpace = u_lightViewProjection * vec4(shadowPosition, 1.0);
1058
1266
  vec3 projected = lightSpace.xyz / max(lightSpace.w, 1e-5) * 0.5 + 0.5;
1059
1267
  if (projected.x > 0.002 && projected.x < 0.998
1060
1268
  && projected.y > 0.002 && projected.y < 0.998
1061
1269
  && 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
1270
+ // \u89C6\u9525\u8D34\u5408\u573A\u666F\u540E\u6DF1\u5EA6\u8DE8\u5EA6\u53EA\u6709\u767E\u7C73\u91CF\u7EA7\uFF0C\u504F\u5DEE\u4E5F\u5C31\u53EF\u4EE5\u6309\u7EB9\u7406\u5355\u5143\u7ED9\u3002
1271
+ // \u9634\u5F71 pass \u53EA\u753B\u80CC\u9762\uFF08cull FRONT\uFF09\uFF0C\u53D7\u5149\u9762\u548C\u6DF1\u5EA6\u56FE\u5929\u7136\u9519\u5F00\uFF0C\u57FA\u6570\u53EF\u4EE5\u5F88\u5C0F\uFF1B
1272
+ // \u659C\u9762\u6700\u654F\u611F\uFF0C\u4ECD\u6309 N\xB7L \u653E\u5927\uFF08\u9634\u5F71\u75E4\u75AE\u4E0E\u9634\u5F71\u8131\u79BB\u4E4B\u95F4\u7684\u90A3\u6761\u7A84\u7F1D\uFF09\u3002
1064
1273
  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);
1274
+ float bias = 0.0006 + 0.0012 * (1.0 - ndlShadow);
1069
1275
  float lit2 = 0.0;
1070
1276
  vec2 texel = vec2(1.0 / 2048.0);
1071
1277
  for (int dx = -1; dx <= 1; dx++) {
@@ -1074,10 +1280,21 @@ void main() {
1074
1280
  lit2 += projected.z - bias > depth ? 0.0 : 1.0;
1075
1281
  }
1076
1282
  }
1077
- lit *= mix(0.68, 1.0, lit2 / 9.0);
1283
+ shadowLit = lit2 / 9.0;
1284
+ lit *= mix(0.68, 1.0, shadowLit);
1078
1285
 
1079
1286
  }
1080
1287
  }
1288
+ // \u8C03\u8BD5\u901A\u9053\uFF1A?shadowdebug=1 \u770B\u906E\u853D\u5224\u5B9A\uFF0C=2 \u770B\u6DF1\u5EA6\u56FE\u672C\u8EAB\u3002
1289
+ if (u_shadowDebug > 0.5) {
1290
+ if (u_shadowDebug < 1.5) {
1291
+ outputColor = vec4(shadowLit, 0.12, 0.12, 1.0);
1292
+ } else {
1293
+ float stored = texture(u_shadowMap, gl_FragCoord.xy / u_viewport).r;
1294
+ outputColor = vec4(vec3(stored), 1.0);
1295
+ }
1296
+ return;
1297
+ }
1081
1298
  // \u753B\u5E03\u6309 sRGB \u89E3\u91CA\u5199\u5165\u503C\uFF0C\u800C\u6750\u8D28\u662F\u7EBF\u6027\u7A7A\u95F4\u7684\u5206\u91CF\uFF0C\u5FC5\u987B\u505A\u4E00\u6B21\u7F16\u7801\u3002
1082
1299
  // \u4E0D\u505A\u7684\u8BDD\u7EBF\u6027 0.017 \u4F1A\u5F53\u4F5C sRGB 0.017 \u663E\u793A\uFF0C\u6DF1\u8272\u6750\u8D28\u5168\u90E8\u584C\u6210\u9ED1\u8272\u3002
1083
1300
  outputColor = vec4(
@@ -1103,8 +1320,31 @@ var WebGL2Program = class _WebGL2Program {
1103
1320
  shadowMapLocation;
1104
1321
  lightMatrixLocation;
1105
1322
  shadowEnabledLocation;
1323
+ shadowNormalOffsetLocation;
1324
+ shadowDebugLocation;
1106
1325
  instancingLocation;
1107
1326
  pickModeLocation;
1327
+ clipPlaneLocation;
1328
+ baseColorMapLocation;
1329
+ useBaseColorMapLocation;
1330
+ uvScaleLocation;
1331
+ normalMapLocation;
1332
+ ormMapLocation;
1333
+ useOrmMapLocation;
1334
+ ormOcclusionLocation;
1335
+ ormMetallicRoughnessLocation;
1336
+ useNormalMapLocation;
1337
+ normalScaleLocation;
1338
+ metallicLocation;
1339
+ roughnessLocation;
1340
+ emissiveLocation;
1341
+ sceneDepthLocation;
1342
+ useSceneDepthLocation;
1343
+ projectionLocation;
1344
+ viewportLocation;
1345
+ clipRangeLocation;
1346
+ aoRadiusLocation;
1347
+ aoStrengthLocation;
1108
1348
  constructor(gl) {
1109
1349
  this.gl = gl;
1110
1350
  const program = this.createProgram(VERTEX_SOURCE, FRAGMENT_SOURCE);
@@ -1117,9 +1357,32 @@ var WebGL2Program = class _WebGL2Program {
1117
1357
  const cameraPositionLocation = gl.getUniformLocation(program, "u_cameraPosition");
1118
1358
  const instancingLocation = gl.getUniformLocation(program, "u_useInstancing");
1119
1359
  const pickModeLocation = gl.getUniformLocation(program, "u_pickMode");
1360
+ const clipPlaneLocation = gl.getUniformLocation(program, "u_clipPlane");
1361
+ const baseColorMapLocation = gl.getUniformLocation(program, "u_baseColorMap");
1362
+ const useBaseColorMapLocation = gl.getUniformLocation(program, "u_useBaseColorMap");
1363
+ const uvScaleLocation = gl.getUniformLocation(program, "u_uvScale");
1364
+ const normalMapLocation = gl.getUniformLocation(program, "u_normalMap");
1365
+ const ormMapLocation = gl.getUniformLocation(program, "u_ormMap");
1366
+ const useOrmMapLocation = gl.getUniformLocation(program, "u_useOrmMap");
1367
+ const ormOcclusionLocation = gl.getUniformLocation(program, "u_ormOcclusion");
1368
+ const ormMetallicRoughnessLocation = gl.getUniformLocation(program, "u_ormMetallicRoughness");
1369
+ const useNormalMapLocation = gl.getUniformLocation(program, "u_useNormalMap");
1370
+ const normalScaleLocation = gl.getUniformLocation(program, "u_normalScale");
1371
+ const metallicLocation = gl.getUniformLocation(program, "u_metallic");
1372
+ const roughnessLocation = gl.getUniformLocation(program, "u_roughness");
1373
+ const emissiveLocation = gl.getUniformLocation(program, "u_emissive");
1374
+ const sceneDepthLocation = gl.getUniformLocation(program, "u_sceneDepth");
1375
+ const useSceneDepthLocation = gl.getUniformLocation(program, "u_useSceneDepth");
1376
+ const projectionLocation = gl.getUniformLocation(program, "u_projection");
1377
+ const viewportLocation = gl.getUniformLocation(program, "u_viewport");
1378
+ const clipRangeLocation = gl.getUniformLocation(program, "u_clipRange");
1379
+ const aoRadiusLocation = gl.getUniformLocation(program, "u_aoRadius");
1380
+ const aoStrengthLocation = gl.getUniformLocation(program, "u_aoStrength");
1120
1381
  const shadowMapLocation = gl.getUniformLocation(program, "u_shadowMap");
1121
1382
  const lightMatrixLocation = gl.getUniformLocation(program, "u_lightViewProjection");
1122
1383
  const shadowEnabledLocation = gl.getUniformLocation(program, "u_shadowEnabled");
1384
+ const shadowNormalOffsetLocation = gl.getUniformLocation(program, "u_shadowNormalOffset");
1385
+ const shadowDebugLocation = gl.getUniformLocation(program, "u_shadowDebug");
1123
1386
  if (!matrixLocation || !colorLocation || !clipOffsetLocation || !vertexColorLocation || !directionalShadingLocation || !waterShadingLocation || !cameraPositionLocation || !instancingLocation || !pickModeLocation) {
1124
1387
  gl.deleteProgram(program);
1125
1388
  throw new ICEEngineError("ICE3_SHADER_LINK_FAILED", "ICE3_SHADER_LINK_FAILED: shader uniforms are missing");
@@ -1135,11 +1398,71 @@ var WebGL2Program = class _WebGL2Program {
1135
1398
  this.shadowMapLocation = shadowMapLocation;
1136
1399
  this.lightMatrixLocation = lightMatrixLocation;
1137
1400
  this.shadowEnabledLocation = shadowEnabledLocation;
1138
- this.shadowMapLocation = shadowMapLocation;
1139
- this.lightMatrixLocation = lightMatrixLocation;
1140
- this.shadowEnabledLocation = shadowEnabledLocation;
1401
+ this.shadowNormalOffsetLocation = shadowNormalOffsetLocation;
1402
+ this.shadowDebugLocation = shadowDebugLocation;
1141
1403
  this.instancingLocation = instancingLocation;
1142
1404
  this.pickModeLocation = pickModeLocation;
1405
+ this.clipPlaneLocation = clipPlaneLocation;
1406
+ this.baseColorMapLocation = baseColorMapLocation;
1407
+ this.useBaseColorMapLocation = useBaseColorMapLocation;
1408
+ this.uvScaleLocation = uvScaleLocation;
1409
+ this.normalMapLocation = normalMapLocation;
1410
+ this.ormMapLocation = ormMapLocation;
1411
+ this.useOrmMapLocation = useOrmMapLocation;
1412
+ this.ormOcclusionLocation = ormOcclusionLocation;
1413
+ this.ormMetallicRoughnessLocation = ormMetallicRoughnessLocation;
1414
+ this.useNormalMapLocation = useNormalMapLocation;
1415
+ this.normalScaleLocation = normalScaleLocation;
1416
+ this.metallicLocation = metallicLocation;
1417
+ this.roughnessLocation = roughnessLocation;
1418
+ this.emissiveLocation = emissiveLocation;
1419
+ this.sceneDepthLocation = sceneDepthLocation;
1420
+ this.useSceneDepthLocation = useSceneDepthLocation;
1421
+ this.projectionLocation = projectionLocation;
1422
+ this.viewportLocation = viewportLocation;
1423
+ this.clipRangeLocation = clipRangeLocation;
1424
+ this.aoRadiusLocation = aoRadiusLocation;
1425
+ this.aoStrengthLocation = aoStrengthLocation;
1426
+ if (sceneDepthLocation) {
1427
+ gl.useProgram(program);
1428
+ gl.uniform1i(sceneDepthLocation, 2);
1429
+ }
1430
+ if (useSceneDepthLocation) {
1431
+ gl.useProgram(program);
1432
+ gl.uniform1f(useSceneDepthLocation, 0);
1433
+ }
1434
+ if (aoRadiusLocation) {
1435
+ gl.useProgram(program);
1436
+ gl.uniform1f(aoRadiusLocation, 2.5);
1437
+ }
1438
+ if (emissiveLocation) {
1439
+ gl.useProgram(program);
1440
+ gl.uniform3f(emissiveLocation, 0, 0, 0);
1441
+ }
1442
+ if (normalMapLocation) {
1443
+ gl.useProgram(program);
1444
+ gl.uniform1i(normalMapLocation, 1);
1445
+ }
1446
+ if (ormMapLocation) {
1447
+ gl.useProgram(program);
1448
+ gl.uniform1i(ormMapLocation, 4);
1449
+ }
1450
+ if (useNormalMapLocation) {
1451
+ gl.useProgram(program);
1452
+ gl.uniform1f(useNormalMapLocation, 0);
1453
+ }
1454
+ if (baseColorMapLocation) {
1455
+ gl.useProgram(program);
1456
+ gl.uniform1i(baseColorMapLocation, 0);
1457
+ }
1458
+ if (useBaseColorMapLocation) {
1459
+ gl.useProgram(program);
1460
+ gl.uniform1f(useBaseColorMapLocation, 0);
1461
+ }
1462
+ if (clipPlaneLocation) {
1463
+ gl.useProgram(program);
1464
+ gl.uniform4f(clipPlaneLocation, 0, 0, 0, 0);
1465
+ }
1143
1466
  if (shadowMapLocation) {
1144
1467
  gl.useProgram(program);
1145
1468
  gl.uniform1i(shadowMapLocation, 3);
@@ -1154,14 +1477,120 @@ var WebGL2Program = class _WebGL2Program {
1154
1477
  setColor(color) {
1155
1478
  this.gl.uniform4f(this.colorLocation, ...color);
1156
1479
  }
1480
+ /**
1481
+ * Bind a base-colour texture to unit 0 and enable sampling. Passing `null`
1482
+ * restores flat shading without touching the bound texture.
1483
+ */
1484
+ setBaseColorMap(texture, scale) {
1485
+ const gl = this.gl;
1486
+ if (!this.useBaseColorMapLocation)
1487
+ return;
1488
+ if (!texture) {
1489
+ gl.uniform1f(this.useBaseColorMapLocation, 0);
1490
+ return;
1491
+ }
1492
+ gl.activeTexture(gl.TEXTURE0);
1493
+ gl.bindTexture(gl.TEXTURE_2D, texture);
1494
+ gl.uniform1f(this.useBaseColorMapLocation, 1);
1495
+ if (this.uvScaleLocation) {
1496
+ gl.uniform2f(this.uvScaleLocation, scale[0], scale[1]);
1497
+ }
1498
+ }
1499
+ /** Bind a tangent-space normal map to unit 1; `null` disables perturbation. */
1500
+ setNormalMap(texture, strength) {
1501
+ const gl = this.gl;
1502
+ if (!this.useNormalMapLocation)
1503
+ return;
1504
+ if (!texture) {
1505
+ gl.uniform1f(this.useNormalMapLocation, 0);
1506
+ return;
1507
+ }
1508
+ gl.activeTexture(gl.TEXTURE1);
1509
+ gl.bindTexture(gl.TEXTURE_2D, texture);
1510
+ gl.uniform1f(this.useNormalMapLocation, 1);
1511
+ if (this.normalScaleLocation) {
1512
+ gl.uniform1f(this.normalScaleLocation, strength);
1513
+ }
1514
+ }
1515
+ /**
1516
+ * Bind the packed ORM map. `occlusion` / `metallicRoughness` describe which
1517
+ * halves of the image are meaningful; both false disables sampling.
1518
+ */
1519
+ setOrmMap(texture, occlusion, metallicRoughness) {
1520
+ const gl = this.gl;
1521
+ if (!this.useOrmMapLocation)
1522
+ return;
1523
+ if (!texture || !occlusion && !metallicRoughness) {
1524
+ gl.uniform1f(this.useOrmMapLocation, 0);
1525
+ return;
1526
+ }
1527
+ gl.activeTexture(gl.TEXTURE4);
1528
+ gl.bindTexture(gl.TEXTURE_2D, texture);
1529
+ gl.uniform1f(this.useOrmMapLocation, 1);
1530
+ if (this.ormOcclusionLocation) {
1531
+ gl.uniform1f(this.ormOcclusionLocation, occlusion ? 1 : 0);
1532
+ }
1533
+ if (this.ormMetallicRoughnessLocation) {
1534
+ gl.uniform1f(this.ormMetallicRoughnessLocation, metallicRoughness ? 1 : 0);
1535
+ }
1536
+ }
1537
+ /** Metallic-roughness factors plus emission for the current draw. */
1538
+ setSurface(metallic, roughness, emissive) {
1539
+ const gl = this.gl;
1540
+ if (this.metallicLocation)
1541
+ gl.uniform1f(this.metallicLocation, metallic);
1542
+ if (this.roughnessLocation)
1543
+ gl.uniform1f(this.roughnessLocation, roughness);
1544
+ if (this.emissiveLocation) {
1545
+ if (emissive)
1546
+ gl.uniform3f(this.emissiveLocation, emissive[0], emissive[1], emissive[2]);
1547
+ else
1548
+ gl.uniform3f(this.emissiveLocation, 0, 0, 0);
1549
+ }
1550
+ }
1551
+ /** 绑定相机深度纹理(unit 2)并传入重建视空间所需的相机参数。 */
1552
+ setSceneDepth(texture, projection, near, far, deviceWidth, deviceHeight) {
1553
+ const gl = this.gl;
1554
+ if (!this.useSceneDepthLocation)
1555
+ return;
1556
+ if (!texture || !projection) {
1557
+ gl.uniform1f(this.useSceneDepthLocation, 0);
1558
+ return;
1559
+ }
1560
+ gl.activeTexture(gl.TEXTURE2);
1561
+ gl.bindTexture(gl.TEXTURE_2D, texture);
1562
+ gl.uniform1f(this.useSceneDepthLocation, 1);
1563
+ if (this.projectionLocation) {
1564
+ gl.uniformMatrix4fv(this.projectionLocation, false, projection.elements);
1565
+ }
1566
+ if (this.viewportLocation) {
1567
+ gl.uniform2f(this.viewportLocation, deviceWidth, deviceHeight);
1568
+ }
1569
+ if (this.clipRangeLocation) {
1570
+ gl.uniform2f(this.clipRangeLocation, near, far);
1571
+ }
1572
+ }
1573
+ /** 遮蔽半径(米)与强度,供运行时调节。强度 0 等于关闭。 */
1574
+ setOcclusion(radius, strength) {
1575
+ const gl = this.gl;
1576
+ if (this.aoRadiusLocation)
1577
+ gl.uniform1f(this.aoRadiusLocation, radius);
1578
+ if (this.aoStrengthLocation)
1579
+ gl.uniform1f(this.aoStrengthLocation, strength);
1580
+ }
1157
1581
  setClipOffset(x, y) {
1158
1582
  this.gl.uniform2f(this.clipOffsetLocation, x, y);
1159
1583
  }
1584
+ setClipPlane(plane) {
1585
+ if (!this.clipPlaneLocation)
1586
+ return;
1587
+ this.gl.uniform4f(this.clipPlaneLocation, plane[0], plane[1], plane[2], plane[3]);
1588
+ }
1160
1589
  setVertexColors(enabled) {
1161
1590
  this.gl.uniform1f(this.vertexColorLocation, enabled ? 1 : 0);
1162
1591
  }
1163
1592
  /** 绑定阴影贴图与光源矩阵;关闭时着色器跳过采样。 */
1164
- setShadow(enabled, texture, matrix, unit = 3) {
1593
+ setShadow(enabled, texture, matrix, normalOffset = 0, unit = 3) {
1165
1594
  const gl = this.gl;
1166
1595
  gl.uniform1f(this.shadowEnabledLocation, enabled && texture && matrix ? 1 : 0);
1167
1596
  if (!enabled || !texture || !matrix)
@@ -1172,8 +1601,17 @@ var WebGL2Program = class _WebGL2Program {
1172
1601
  gl.uniform1i(this.shadowMapLocation, unit);
1173
1602
  if (this.lightMatrixLocation)
1174
1603
  gl.uniformMatrix4fv(this.lightMatrixLocation, false, matrix);
1604
+ if (this.shadowNormalOffsetLocation) {
1605
+ gl.uniform1f(this.shadowNormalOffsetLocation, normalOffset);
1606
+ }
1175
1607
  gl.activeTexture(gl.TEXTURE0);
1176
1608
  }
1609
+ /** 阴影调试通道:0 关闭,1 输出遮蔽判定,2 输出深度图。 */
1610
+ setShadowDebug(mode) {
1611
+ if (!this.shadowDebugLocation)
1612
+ return;
1613
+ this.gl.uniform1f(this.shadowDebugLocation, mode);
1614
+ }
1177
1615
  setDirectionalShading(enabled) {
1178
1616
  this.gl.uniform1f(this.directionalShadingLocation, enabled ? 1 : 0);
1179
1617
  }
@@ -1275,6 +1713,14 @@ function collectInstanceBatches(renderables, output = []) {
1275
1713
  }
1276
1714
  return output;
1277
1715
  }
1716
+ function renderablesOutsideInstanceBatches(renderables, batches) {
1717
+ const batched = /* @__PURE__ */ new Set();
1718
+ for (const batch of batches) {
1719
+ for (const id of batch.meshIds)
1720
+ batched.add(id);
1721
+ }
1722
+ return renderables.filter((renderable) => !batched.has(renderable.id));
1723
+ }
1278
1724
 
1279
1725
  // packages/renderer/dist/WebGL2InstanceBatch.js
1280
1726
  var WebGL2InstanceCache = class {
@@ -1319,10 +1765,10 @@ var WebGL2InstanceCache = class {
1319
1765
  gl.bindBuffer(gl.ARRAY_BUFFER, matrixBuffer);
1320
1766
  gl.bufferData(gl.ARRAY_BUFFER, batch.instanceMatrices, gl.DYNAMIC_DRAW);
1321
1767
  for (let column = 0; column < 4; column += 1) {
1322
- const location = WebGL2Program.INSTANCE_MATRIX_ATTRIBUTE + column;
1323
- gl.enableVertexAttribArray(location);
1324
- gl.vertexAttribPointer(location, 4, gl.FLOAT, false, 64, column * 16);
1325
- gl.vertexAttribDivisor(location, 1);
1768
+ const location2 = WebGL2Program.INSTANCE_MATRIX_ATTRIBUTE + column;
1769
+ gl.enableVertexAttribArray(location2);
1770
+ gl.vertexAttribPointer(location2, 4, gl.FLOAT, false, 64, column * 16);
1771
+ gl.vertexAttribDivisor(location2, 1);
1326
1772
  }
1327
1773
  gl.bindBuffer(gl.ARRAY_BUFFER, semanticBuffer);
1328
1774
  gl.bufferData(gl.ARRAY_BUFFER, batch.semanticIndices, gl.DYNAMIC_DRAW);
@@ -2018,14 +2464,136 @@ var WebGL2Picker = class {
2018
2464
  }
2019
2465
  };
2020
2466
 
2467
+ // packages/renderer/dist/WebGL2Shader.js
2468
+ function createShaderProgram(gl, vertexSource, fragmentSource, label) {
2469
+ const vertex = compileShader(gl, gl.VERTEX_SHADER, vertexSource, `${label}:vertex`);
2470
+ const fragment = compileShader(gl, gl.FRAGMENT_SHADER, fragmentSource, `${label}:fragment`);
2471
+ const program = gl.createProgram();
2472
+ if (!program) {
2473
+ throw new ICEEngineError("ICE3_SHADER_LINK_FAILED", `ICE3_SHADER_LINK_FAILED: ${label}: program allocation failed`);
2474
+ }
2475
+ gl.attachShader(program, vertex);
2476
+ gl.attachShader(program, fragment);
2477
+ gl.linkProgram(program);
2478
+ gl.deleteShader(vertex);
2479
+ gl.deleteShader(fragment);
2480
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
2481
+ const log = gl.getProgramInfoLog(program) ?? "unknown link error";
2482
+ gl.deleteProgram(program);
2483
+ throw new ICEEngineError("ICE3_SHADER_LINK_FAILED", `ICE3_SHADER_LINK_FAILED: ${label}: ${log}`);
2484
+ }
2485
+ return program;
2486
+ }
2487
+ function compileShader(gl, type, source, label) {
2488
+ const shader = gl.createShader(type);
2489
+ if (!shader) {
2490
+ throw new ICEEngineError("ICE3_SHADER_COMPILE_FAILED", `ICE3_SHADER_COMPILE_FAILED: ${label}: allocation failed`);
2491
+ }
2492
+ gl.shaderSource(shader, source);
2493
+ gl.compileShader(shader);
2494
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
2495
+ const log = gl.getShaderInfoLog(shader) ?? "unknown compile error";
2496
+ gl.deleteShader(shader);
2497
+ throw new ICEEngineError("ICE3_SHADER_COMPILE_FAILED", `ICE3_SHADER_COMPILE_FAILED: ${label}: ${log}`);
2498
+ }
2499
+ return shader;
2500
+ }
2501
+
2502
+ // packages/renderer/dist/ShadowFrustum.js
2503
+ var SHADOW_LIGHT_DIRECTION = [-0.35, 0.85, 0.4];
2504
+ var BOUNDS_PADDING = 0.08;
2505
+ function shadowFrustum(bounds, mapSize = 2048) {
2506
+ const box = bounds ?? { min: [-50, -5, -50], max: [650, 60, 650] };
2507
+ const centre = [
2508
+ (box.min[0] + box.max[0]) / 2,
2509
+ (box.min[1] + box.max[1]) / 2,
2510
+ (box.min[2] + box.max[2]) / 2
2511
+ ];
2512
+ const radius = Math.max(1, 0.5 * Math.hypot(box.max[0] - box.min[0], box.max[1] - box.min[1], box.max[2] - box.min[2]));
2513
+ const pad = radius * BOUNDS_PADDING;
2514
+ const direction = normalize2(SHADOW_LIGHT_DIRECTION);
2515
+ const distance2 = radius * 2 + pad * 2;
2516
+ const eye = [
2517
+ centre[0] + direction[0] * distance2,
2518
+ centre[1] + direction[1] * distance2,
2519
+ centre[2] + direction[2] * distance2
2520
+ ];
2521
+ const view = new ICEMatrix4().lookAt(new ICEVector3(eye[0], eye[1], eye[2]), new ICEVector3(centre[0], centre[1], centre[2]), new ICEVector3(0, 1, 0));
2522
+ let minX = Infinity;
2523
+ let maxX = -Infinity;
2524
+ let minY = Infinity;
2525
+ let maxY = -Infinity;
2526
+ let minZ = Infinity;
2527
+ let maxZ = -Infinity;
2528
+ for (let corner = 0; corner < 8; corner += 1) {
2529
+ const point = new ICEVector3(corner & 1 ? box.max[0] : box.min[0], corner & 2 ? box.max[1] : box.min[1], corner & 4 ? box.max[2] : box.min[2]);
2530
+ const projected = view.transformPoint(point);
2531
+ minX = Math.min(minX, projected[0]);
2532
+ maxX = Math.max(maxX, projected[0]);
2533
+ minY = Math.min(minY, projected[1]);
2534
+ maxY = Math.max(maxY, projected[1]);
2535
+ minZ = Math.min(minZ, projected[2]);
2536
+ maxZ = Math.max(maxZ, projected[2]);
2537
+ }
2538
+ const projection = orthographic(minX - pad, maxX + pad, minY - pad, maxY + pad, -maxZ - pad, -minZ + pad);
2539
+ const width = maxX - minX + pad * 2;
2540
+ const height = maxY - minY + pad * 2;
2541
+ return {
2542
+ matrix: multiply(projection, view.elements),
2543
+ texelWorld: Math.max(width, height) / mapSize
2544
+ };
2545
+ }
2546
+ function normalize2(value) {
2547
+ const length = Math.hypot(value[0], value[1], value[2]) || 1;
2548
+ return [value[0] / length, value[1] / length, value[2] / length];
2549
+ }
2550
+ function orthographic(left, right, bottom, top, near, far) {
2551
+ const lr = 1 / (left - right);
2552
+ const bt = 1 / (bottom - top);
2553
+ const nf = 1 / (near - far);
2554
+ return [
2555
+ -2 * lr,
2556
+ 0,
2557
+ 0,
2558
+ 0,
2559
+ 0,
2560
+ -2 * bt,
2561
+ 0,
2562
+ 0,
2563
+ 0,
2564
+ 0,
2565
+ 2 * nf,
2566
+ 0,
2567
+ (left + right) * lr,
2568
+ (top + bottom) * bt,
2569
+ (far + near) * nf,
2570
+ 1
2571
+ ];
2572
+ }
2573
+ function multiply(a, b) {
2574
+ const out = new Float32Array(16);
2575
+ for (let column = 0; column < 4; column += 1) {
2576
+ for (let row = 0; row < 4; row += 1) {
2577
+ let sum = 0;
2578
+ for (let k = 0; k < 4; k += 1) {
2579
+ sum += (a[k * 4 + row] ?? 0) * (b[column * 4 + k] ?? 0);
2580
+ }
2581
+ out[column * 4 + row] = sum;
2582
+ }
2583
+ }
2584
+ return out;
2585
+ }
2586
+
2021
2587
  // packages/renderer/dist/WebGL2ShadowMap.js
2022
2588
  var VERTEX_SOURCE3 = `#version 300 es
2023
2589
  layout(location = 0) in vec3 a_position;
2024
2590
  layout(location = 3) in mat4 a_instanceMatrix;
2025
- uniform mat4 u_lightViewProjection;
2591
+ uniform mat4 u_matrix;
2592
+ uniform float u_useInstancing;
2026
2593
  void main() {
2027
- vec4 world = a_instanceMatrix * vec4(a_position, 1.0);
2028
- gl_Position = u_lightViewProjection * world;
2594
+ vec4 local = vec4(a_position, 1.0);
2595
+ vec4 world = mix(local, a_instanceMatrix * local, u_useInstancing);
2596
+ gl_Position = u_matrix * world;
2029
2597
  }
2030
2598
  `;
2031
2599
  var FRAGMENT_SOURCE3 = `#version 300 es
@@ -2034,18 +2602,18 @@ void main() {
2034
2602
  // \u53EA\u5199\u6DF1\u5EA6\uFF0C\u989C\u8272\u4E0D\u9700\u8981\u3002
2035
2603
  }
2036
2604
  `;
2037
- var LIGHT_DIRECTION2 = [-0.35, 0.85, 0.4];
2038
2605
  var SHADOW_SIZE = 2048;
2039
- var ORTHO_EXTENT = 430;
2040
- var ORTHO_NEAR = -260;
2041
- var ORTHO_FAR = 700;
2042
2606
  var WebGL2ShadowMap = class {
2043
2607
  gl;
2608
+ /** 最近一次着色器编译/链接失败的原因,供诊断读取。 */
2609
+ lastError = null;
2044
2610
  framebuffer = null;
2045
2611
  depthTexture = null;
2046
2612
  program = null;
2047
- lightMatrixLocation = null;
2613
+ matrixLocation = null;
2614
+ instancingLocation = null;
2048
2615
  matrixScratch = new Float32Array(16);
2616
+ texelWorldValue = 1;
2049
2617
  constructor(gl) {
2050
2618
  this.gl = gl;
2051
2619
  this.createResources();
@@ -2057,6 +2625,19 @@ var WebGL2ShadowMap = class {
2057
2625
  get matrix() {
2058
2626
  return this.matrixScratch;
2059
2627
  }
2628
+ /** 一个深度纹素在世界空间的边长(米)。法线偏移按它换算,别用魔数。 */
2629
+ get texelWorld() {
2630
+ return this.texelWorldValue;
2631
+ }
2632
+ /**
2633
+ * 按场景包围盒拟合视锥。每帧调用没有意义——场景内容不变时矩阵也不会变,
2634
+ * 所以由调用方决定何时重算(包围盒变化或首帧)。
2635
+ */
2636
+ setBounds(bounds) {
2637
+ const frustum = shadowFrustum(bounds, SHADOW_SIZE);
2638
+ this.matrixScratch.set(frustum.matrix);
2639
+ this.texelWorldValue = frustum.texelWorld;
2640
+ }
2060
2641
  /** 绑定阴影帧缓冲,返回是否成功。 */
2061
2642
  begin() {
2062
2643
  const gl = this.gl;
@@ -2067,12 +2648,24 @@ var WebGL2ShadowMap = class {
2067
2648
  gl.disable(gl.POLYGON_OFFSET_FILL);
2068
2649
  gl.clear(gl.DEPTH_BUFFER_BIT);
2069
2650
  gl.enable(gl.DEPTH_TEST);
2070
- gl.enable(gl.CULL_FACE);
2071
- gl.cullFace(gl.FRONT);
2651
+ gl.disable(gl.CULL_FACE);
2072
2652
  gl.useProgram(this.program);
2073
- gl.uniformMatrix4fv(this.lightMatrixLocation, false, this.matrixScratch);
2653
+ gl.uniformMatrix4fv(this.matrixLocation, false, this.matrixScratch);
2654
+ gl.uniform1f(this.instancingLocation, 1);
2074
2655
  return true;
2075
2656
  }
2657
+ /**
2658
+ * 非实例化绘制:矩阵换成 `lightViewProjection × world`,顶点位置直接当世界坐标用。
2659
+ *
2660
+ * 阴影通道原先只会画实例化批次,瓦片(渲染批次)与落单网格因此完全不投影。
2661
+ * 补这两条路径靠的就是这个入口——和 `WebGL2SceneDepth` 同一套写法。
2662
+ */
2663
+ setMatrix(matrix) {
2664
+ this.gl.uniformMatrix4fv(this.matrixLocation, false, matrix);
2665
+ }
2666
+ setInstancing(enabled) {
2667
+ this.gl.uniform1f(this.instancingLocation, enabled ? 1 : 0);
2668
+ }
2076
2669
  /** 解绑,恢复默认帧缓冲。 */
2077
2670
  end(width, height) {
2078
2671
  const gl = this.gl;
@@ -2109,130 +2702,283 @@ var WebGL2ShadowMap = class {
2109
2702
  gl.readBuffer(gl.NONE);
2110
2703
  gl.bindFramebuffer(gl.FRAMEBUFFER, null);
2111
2704
  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));
2705
+ this.matrixLocation = this.program ? gl.getUniformLocation(this.program, "u_matrix") : null;
2706
+ this.instancingLocation = this.program ? gl.getUniformLocation(this.program, "u_useInstancing") : null;
2707
+ this.setBounds(null);
2127
2708
  }
2128
2709
  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);
2710
+ try {
2711
+ return createShaderProgram(this.gl, vertex, fragment, "shadow");
2712
+ } catch (error) {
2713
+ this.lastError = error instanceof Error ? error.message : String(error);
2142
2714
  return null;
2143
2715
  }
2144
- return program;
2145
2716
  }
2146
- compile(type, source) {
2717
+ };
2718
+
2719
+ // packages/renderer/dist/SceneBounds.js
2720
+ var localBoundsCache = /* @__PURE__ */ new WeakMap();
2721
+ function geometryLocalBounds(geometry) {
2722
+ const cached = localBoundsCache.get(geometry);
2723
+ if (cached)
2724
+ return cached;
2725
+ const positions = geometry.positions;
2726
+ if (!positions || positions.length < 3)
2727
+ return null;
2728
+ let minX = Infinity;
2729
+ let minY = Infinity;
2730
+ let minZ = Infinity;
2731
+ let maxX = -Infinity;
2732
+ let maxY = -Infinity;
2733
+ let maxZ = -Infinity;
2734
+ for (let index = 0; index + 2 < positions.length; index += 3) {
2735
+ const x = positions[index];
2736
+ const y = positions[index + 1];
2737
+ const z = positions[index + 2];
2738
+ if (x < minX)
2739
+ minX = x;
2740
+ if (y < minY)
2741
+ minY = y;
2742
+ if (z < minZ)
2743
+ minZ = z;
2744
+ if (x > maxX)
2745
+ maxX = x;
2746
+ if (y > maxY)
2747
+ maxY = y;
2748
+ if (z > maxZ)
2749
+ maxZ = z;
2750
+ }
2751
+ const bounds = { min: [minX, minY, minZ], max: [maxX, maxY, maxZ] };
2752
+ localBoundsCache.set(geometry, bounds);
2753
+ return bounds;
2754
+ }
2755
+ function accumulateSceneBounds(renderables) {
2756
+ let minX = Infinity;
2757
+ let minY = Infinity;
2758
+ let minZ = Infinity;
2759
+ let maxX = -Infinity;
2760
+ let maxY = -Infinity;
2761
+ let maxZ = -Infinity;
2762
+ let seen = false;
2763
+ for (const renderable of renderables) {
2764
+ const local = geometryLocalBounds(renderable.mesh.geometry);
2765
+ if (!local)
2766
+ continue;
2767
+ const matrix = renderable.worldMatrix;
2768
+ for (let corner = 0; corner < 8; corner += 1) {
2769
+ const x = corner & 1 ? local.max[0] : local.min[0];
2770
+ const y = corner & 2 ? local.max[1] : local.min[1];
2771
+ const z = corner & 4 ? local.max[2] : local.min[2];
2772
+ const worldX = matrix[0] * x + matrix[4] * y + matrix[8] * z + matrix[12];
2773
+ const worldY = matrix[1] * x + matrix[5] * y + matrix[9] * z + matrix[13];
2774
+ const worldZ = matrix[2] * x + matrix[6] * y + matrix[10] * z + matrix[14];
2775
+ if (worldX < minX)
2776
+ minX = worldX;
2777
+ if (worldY < minY)
2778
+ minY = worldY;
2779
+ if (worldZ < minZ)
2780
+ minZ = worldZ;
2781
+ if (worldX > maxX)
2782
+ maxX = worldX;
2783
+ if (worldY > maxY)
2784
+ maxY = worldY;
2785
+ if (worldZ > maxZ)
2786
+ maxZ = worldZ;
2787
+ seen = true;
2788
+ }
2789
+ }
2790
+ return seen ? { min: [minX, minY, minZ], max: [maxX, maxY, maxZ] } : null;
2791
+ }
2792
+
2793
+ // packages/renderer/dist/WebGL2SceneDepth.js
2794
+ var VERTEX_SOURCE4 = `#version 300 es
2795
+ layout(location = 0) in vec3 a_position;
2796
+ layout(location = 3) in mat4 a_instanceMatrix;
2797
+ uniform mat4 u_matrix;
2798
+ uniform float u_useInstancing;
2799
+ void main() {
2800
+ vec4 local = vec4(a_position, 1.0);
2801
+ vec4 world = mix(local, a_instanceMatrix * local, u_useInstancing);
2802
+ gl_Position = u_matrix * world;
2803
+ }
2804
+ `;
2805
+ var FRAGMENT_SOURCE4 = `#version 300 es
2806
+ precision highp float;
2807
+ void main() {}
2808
+ `;
2809
+ var WebGL2SceneDepth = class {
2810
+ gl;
2811
+ framebuffer;
2812
+ depthTexture = null;
2813
+ program;
2814
+ matrixLocation;
2815
+ instancingLocation;
2816
+ width = 0;
2817
+ height = 0;
2818
+ constructor(gl) {
2819
+ this.gl = gl;
2820
+ this.program = createShaderProgram(gl, VERTEX_SOURCE4, FRAGMENT_SOURCE4, "scene-depth");
2821
+ this.matrixLocation = gl.getUniformLocation(this.program, "u_matrix");
2822
+ this.instancingLocation = gl.getUniformLocation(this.program, "u_useInstancing");
2823
+ const framebuffer = gl.createFramebuffer();
2824
+ if (!framebuffer)
2825
+ throw new Error("ICE3_FRAMEBUFFER_FAILED: scene depth");
2826
+ this.framebuffer = framebuffer;
2827
+ }
2828
+ get texture() {
2829
+ return this.depthTexture;
2830
+ }
2831
+ begin(viewProjection, width, height) {
2147
2832
  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;
2833
+ this.ensureTexture(Math.max(1, width), Math.max(1, height));
2834
+ gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer);
2835
+ gl.viewport(0, 0, this.width, this.height);
2836
+ gl.disable(gl.POLYGON_OFFSET_FILL);
2837
+ gl.disable(gl.BLEND);
2838
+ gl.depthMask(true);
2839
+ gl.clear(gl.DEPTH_BUFFER_BIT);
2840
+ gl.enable(gl.DEPTH_TEST);
2841
+ gl.useProgram(this.program);
2842
+ gl.uniformMatrix4fv(this.matrixLocation, false, viewProjection);
2843
+ gl.uniform1f(this.instancingLocation, 0);
2844
+ }
2845
+ setMatrix(matrix) {
2846
+ this.gl.uniformMatrix4fv(this.matrixLocation, false, matrix);
2847
+ }
2848
+ setInstancing(enabled) {
2849
+ this.gl.uniform1f(this.instancingLocation, enabled ? 1 : 0);
2850
+ }
2851
+ end(width, height) {
2852
+ const gl = this.gl;
2853
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
2854
+ gl.enable(gl.POLYGON_OFFSET_FILL);
2855
+ gl.viewport(0, 0, width, height);
2856
+ }
2857
+ dispose() {
2858
+ const gl = this.gl;
2859
+ if (this.depthTexture)
2860
+ gl.deleteTexture(this.depthTexture);
2861
+ gl.deleteFramebuffer(this.framebuffer);
2862
+ gl.deleteProgram(this.program);
2863
+ this.depthTexture = null;
2864
+ }
2865
+ ensureTexture(width, height) {
2866
+ const gl = this.gl;
2867
+ gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer);
2868
+ if (this.depthTexture && this.width === width && this.height === height) {
2869
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, this.depthTexture, 0);
2870
+ return;
2156
2871
  }
2157
- return shader;
2872
+ if (this.depthTexture)
2873
+ gl.deleteTexture(this.depthTexture);
2874
+ this.width = width;
2875
+ this.height = height;
2876
+ const texture = gl.createTexture();
2877
+ gl.bindTexture(gl.TEXTURE_2D, texture);
2878
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.DEPTH_COMPONENT24, width, height, 0, gl.DEPTH_COMPONENT, gl.UNSIGNED_INT, null);
2879
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
2880
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
2881
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
2882
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
2883
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, texture, 0);
2884
+ gl.drawBuffers([gl.NONE]);
2885
+ gl.readBuffer(gl.NONE);
2886
+ this.depthTexture = texture;
2158
2887
  }
2159
2888
  };
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);
2889
+
2890
+ // packages/renderer/dist/WebGL2TextureCache.js
2891
+ var REPEAT = 10497;
2892
+ var fetchImageLoader = async (url) => {
2893
+ const response = await fetch(url);
2894
+ if (!response.ok) {
2895
+ throw new Error(`ICE3_TEXTURE_FETCH_FAILED: ${response.status} ${url}`);
2896
+ }
2897
+ return createImageBitmap(await response.blob());
2898
+ };
2899
+ var WebGL2TextureCache = class {
2900
+ gl;
2901
+ load;
2902
+ onUpload;
2903
+ entries = /* @__PURE__ */ new Map();
2904
+ disposed = false;
2905
+ constructor(gl, load = fetchImageLoader, onUpload) {
2906
+ this.gl = gl;
2907
+ this.load = load;
2908
+ this.onUpload = onUpload;
2909
+ }
2910
+ /** Texture for `url`, or `null` while it is still loading (or after failure). */
2911
+ texture(url) {
2912
+ if (this.disposed)
2913
+ return null;
2914
+ const existing = this.entries.get(url);
2915
+ if (existing) {
2916
+ return existing.state === "ready" ? existing.texture : null;
2917
+ }
2918
+ this.entries.set(url, { state: "pending" });
2919
+ void this.load(url).then((image) => {
2920
+ if (this.disposed) {
2921
+ image.close?.();
2922
+ return;
2230
2923
  }
2231
- out[column * 4 + row] = sum;
2924
+ const texture = this.upload(image);
2925
+ if (this.disposed) {
2926
+ this.gl.deleteTexture(texture);
2927
+ return;
2928
+ }
2929
+ this.entries.set(url, { state: "ready", texture });
2930
+ this.onUpload?.();
2931
+ }).catch(() => {
2932
+ if (!this.disposed)
2933
+ this.entries.set(url, { state: "failed" });
2934
+ });
2935
+ return null;
2936
+ }
2937
+ stats() {
2938
+ let loaded = 0;
2939
+ let pending = 0;
2940
+ let failed2 = 0;
2941
+ for (const entry of this.entries.values()) {
2942
+ if (entry.state === "ready")
2943
+ loaded += 1;
2944
+ else if (entry.state === "pending")
2945
+ pending += 1;
2946
+ else
2947
+ failed2 += 1;
2232
2948
  }
2949
+ return { loaded, pending, failed: failed2 };
2233
2950
  }
2234
- return out;
2235
- }
2951
+ dispose() {
2952
+ if (this.disposed)
2953
+ return;
2954
+ this.disposed = true;
2955
+ for (const entry of this.entries.values()) {
2956
+ if (entry.state === "ready")
2957
+ this.gl.deleteTexture(entry.texture);
2958
+ }
2959
+ this.entries.clear();
2960
+ }
2961
+ upload(image) {
2962
+ const gl = this.gl;
2963
+ const texture = gl.createTexture();
2964
+ if (!texture) {
2965
+ throw new Error("ICE3_TEXTURE_CREATE_FAILED");
2966
+ }
2967
+ gl.bindTexture(gl.TEXTURE_2D, texture);
2968
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
2969
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, REPEAT);
2970
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, REPEAT);
2971
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
2972
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
2973
+ gl.generateMipmap(gl.TEXTURE_2D);
2974
+ gl.bindTexture(gl.TEXTURE_2D, null);
2975
+ image.close?.();
2976
+ return texture;
2977
+ }
2978
+ };
2979
+
2980
+ // packages/renderer/dist/WebGL2Uv.js
2981
+ var DEFAULT_UV_SCALE = [1, 1];
2236
2982
 
2237
2983
  // packages/renderer/dist/Color.js
2238
2984
  var COLOR_CACHE_LIMIT = 128;
@@ -2325,9 +3071,16 @@ var WebGL2RendererBackend = class {
2325
3071
  onContextLost;
2326
3072
  onContextRestored;
2327
3073
  program = null;
3074
+ clipPlane = [0, 0, 0, 0];
2328
3075
  shadowMap = null;
2329
- // 阴影采样开关。定位掉深度偏差之前保持关闭,避免把错误的明暗关系画出来。
3076
+ /**
3077
+ * 方向光阴影开关。默认开启:没有影子的厂区看起来像浮在地上,
3078
+ * 而多一次全场景深度 pass 的代价远小于这个观感损失。
3079
+ * 要关掉用 `initialize({ shadows: false })` 或页面地址 `?shadows=0`。
3080
+ */
2330
3081
  shadowEnabled = false;
3082
+ /** `?shadowdebug=1` 输出遮蔽判定,`=2` 输出深度图;排查阴影问题的唯一可信手段。 */
3083
+ shadowDebug = 0;
2331
3084
  semanticProgram = null;
2332
3085
  geometryCache = null;
2333
3086
  instanceCache = null;
@@ -2355,10 +3108,106 @@ var WebGL2RendererBackend = class {
2355
3108
  gpuEvictions: 0,
2356
3109
  gpuBytes: 0
2357
3110
  };
3111
+ textures = null;
3112
+ sceneDepth = null;
3113
+ occlusionRadius = 2.5;
3114
+ /**
3115
+ * 三圈采样比双圈多找到约 1.7 个亮度点的遮蔽(同参数 96.36 → 94.65),原样沿用到
3116
+ * 默认值就把厂区压得发闷。0.30 是回调后的值:整帧 95.70(无遮蔽 99.59),
3117
+ * 高光像素 878(无遮蔽 1088),接触暗部仍看得见。详见 SPEC-007 第 5.5 节。
3118
+ */
3119
+ occlusionStrength = 0.3;
3120
+ options = null;
3121
+ /**
3122
+ * Bind the material's colour and (if it has one) its base-colour texture.
3123
+ * Called for every opaque/shaded draw so a batched draw and a single mesh draw
3124
+ * cannot drift apart.
3125
+ */
3126
+ applyMaterial(material) {
3127
+ const program = this.program;
3128
+ if (!program)
3129
+ return;
3130
+ program.setColor(colorToRgba(material.color));
3131
+ const map = material.map;
3132
+ const texture = map && this.textures ? this.textures.texture(map.url) : null;
3133
+ program.setBaseColorMap(texture, map?.scale ?? material.ormMap?.scale ?? DEFAULT_UV_SCALE);
3134
+ const normalMap = material.normalMap;
3135
+ const normalTexture = normalMap && this.textures ? this.textures.texture(normalMap.url) : null;
3136
+ program.setNormalMap(normalTexture, normalMap?.strength ?? 1);
3137
+ const ormMap = material.ormMap;
3138
+ const ormTexture = ormMap && this.textures ? this.textures.texture(ormMap.url) : null;
3139
+ program.setOrmMap(ormTexture, ormMap?.occlusion !== false, ormMap?.metallicRoughness !== false);
3140
+ program.setSurface(material.metallic, material.roughness, material.emissive);
3141
+ }
3142
+ /** Edge overlays are line work: never sampled, always flat. */
3143
+ applyEdgeMaterial(material) {
3144
+ const program = this.program;
3145
+ if (!program)
3146
+ return;
3147
+ program.setColor(colorToRgba(material.edgeColor ?? material.color));
3148
+ program.setBaseColorMap(null, DEFAULT_UV_SCALE);
3149
+ program.setNormalMap(null, 1);
3150
+ program.setOrmMap(null, false, false);
3151
+ program.setSurface(0, 0.85, void 0);
3152
+ }
3153
+ /**
3154
+ * `?ao=<radius>,<strength>` 覆盖遮蔽参数(`ao=off` 关闭)。
3155
+ *
3156
+ * 屏幕空间遮蔽的合适参数与场景尺度强相关,靠改代码重编译来找太慢。这里只读一次
3157
+ * 页面地址,纯属调参通道,不参与渲染决策。
3158
+ */
3159
+ readOcclusionOverride() {
3160
+ if (typeof location === "undefined")
3161
+ return;
3162
+ const value = new URLSearchParams(location.search).get("ao");
3163
+ if (!value)
3164
+ return;
3165
+ if (value === "off") {
3166
+ this.occlusionStrength = 0;
3167
+ return;
3168
+ }
3169
+ const parts = value.split(",").map(Number);
3170
+ const radius = parts[0];
3171
+ const strength = parts[1];
3172
+ if (radius !== void 0 && Number.isFinite(radius) && radius > 0) {
3173
+ this.occlusionRadius = radius;
3174
+ }
3175
+ if (strength !== void 0 && Number.isFinite(strength) && strength >= 0) {
3176
+ this.occlusionStrength = strength;
3177
+ }
3178
+ }
3179
+ /**
3180
+ * `?shadows=1` / `?shadows=0` 覆盖阴影开关,与 `?ao` 同属调参通道。
3181
+ *
3182
+ * 阴影是一次额外全场景 pass,默认不开;演示与对比时靠地址栏切换,
3183
+ * 不必为看清"有影子/没影子"重编译一次。
3184
+ */
3185
+ readShadowOverride() {
3186
+ if (typeof location === "undefined")
3187
+ return;
3188
+ const params = new URLSearchParams(location.search);
3189
+ const value = params.get("shadows");
3190
+ if (value !== null) {
3191
+ this.shadowEnabled = value !== "0" && value !== "off";
3192
+ }
3193
+ const debug = Number(params.get("shadowdebug"));
3194
+ this.shadowDebug = Number.isFinite(debug) ? Math.max(0, Math.min(2, Math.trunc(debug))) : 0;
3195
+ if (this.shadowDebug > 0)
3196
+ this.shadowEnabled = true;
3197
+ }
3198
+ ensureTextureCache() {
3199
+ if (this.textures || !this.gl)
3200
+ return;
3201
+ this.textures = new WebGL2TextureCache(this.gl, void 0, () => {
3202
+ this.options?.onUploadReady?.();
3203
+ });
3204
+ }
2358
3205
  handleContextLost = (event) => {
2359
3206
  event.preventDefault();
2360
3207
  this.picker = null;
2361
3208
  this.program = null;
3209
+ this.sceneDepth?.dispose();
3210
+ this.sceneDepth = null;
2362
3211
  this.shadowMap?.dispose();
2363
3212
  this.shadowMap = null;
2364
3213
  this.geometryCache?.clear();
@@ -2379,6 +3228,7 @@ var WebGL2RendererBackend = class {
2379
3228
  this.onContextRestored?.();
2380
3229
  };
2381
3230
  initialize(options) {
3231
+ this.options = options;
2382
3232
  const gl = options.canvas.getContext("webgl2", {
2383
3233
  alpha: true,
2384
3234
  antialias: true,
@@ -2402,6 +3252,11 @@ var WebGL2RendererBackend = class {
2402
3252
  gl.enable(gl.POLYGON_OFFSET_FILL);
2403
3253
  gl.polygonOffset(2, 4);
2404
3254
  this.createProgram(gl);
3255
+ this.ensureTextureCache();
3256
+ this.sceneDepth = new WebGL2SceneDepth(gl);
3257
+ this.readOcclusionOverride();
3258
+ this.shadowEnabled = options.shadows ?? true;
3259
+ this.readShadowOverride();
2405
3260
  this.canvas.addEventListener("webglcontextlost", this.handleContextLost);
2406
3261
  this.canvas.addEventListener("webglcontextrestored", this.handleContextRestored);
2407
3262
  }
@@ -2436,6 +3291,9 @@ var WebGL2RendererBackend = class {
2436
3291
  releaseBatchGeometry(geometry) {
2437
3292
  this.geometryCache?.unpin(geometry);
2438
3293
  }
3294
+ setClipPlane(plane) {
3295
+ this.clipPlane = plane ? [plane[0], plane[1], plane[2], plane[3]] : [0, 0, 0, 0];
3296
+ }
2439
3297
  render(frame) {
2440
3298
  if (!this.gl || !this.program || !this.geometryCache) {
2441
3299
  throw new ICEEngineError("ICE3_ENGINE_NOT_READY", "Renderer is not initialized");
@@ -2458,6 +3316,7 @@ var WebGL2RendererBackend = class {
2458
3316
  gpuBytes: 0
2459
3317
  };
2460
3318
  this.program.use();
3319
+ this.program.setClipPlane(this.clipPlane);
2461
3320
  this.program.setClipOffset(0, 0);
2462
3321
  this.program.setDirectionalShading(false);
2463
3322
  this.program.setWaterShading(false);
@@ -2472,8 +3331,58 @@ var WebGL2RendererBackend = class {
2472
3331
  const renderables = collectRenderables(frame.scene, this.renderablesScratch);
2473
3332
  const instances = collectInstanceBatches(renderables, this.instancesScratch);
2474
3333
  const batches = collectRenderBatches(frame.scene, this.batchesScratch);
3334
+ const standalone = renderablesOutsideInstanceBatches(renderables, instances);
2475
3335
  this.profiler.mark("collect");
3336
+ let aoReady = false;
3337
+ if (this.sceneDepth) {
3338
+ const deviceWidth = Math.max(1, Math.round(frame.viewport.width * this.dpr));
3339
+ const deviceHeight = Math.max(1, Math.round(frame.viewport.height * this.dpr));
3340
+ this.sceneDepth.begin(viewProjection.elements, deviceWidth, deviceHeight);
3341
+ this.sceneDepth.setInstancing(true);
3342
+ for (const batch of instances) {
3343
+ if (isTransparentColor(batch.material.color))
3344
+ continue;
3345
+ const instance = this.instanceCache.retain(gl, this.geometryCache, batch);
3346
+ if (!instance)
3347
+ continue;
3348
+ gl.bindVertexArray(instance.vertexArray);
3349
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, instance.triangleIndexBuffer);
3350
+ gl.drawElementsInstanced(gl.TRIANGLES, instance.triangleCount, batch.geometry.indices instanceof Uint16Array ? gl.UNSIGNED_SHORT : gl.UNSIGNED_INT, 0, instance.instanceCount);
3351
+ }
3352
+ this.sceneDepth.setInstancing(false);
3353
+ for (const draw of batches) {
3354
+ if (isTransparentColor(draw.batch.material.color))
3355
+ continue;
3356
+ world.elements.set(draw.worldMatrix);
3357
+ mvp.elements.set(viewProjection.elements);
3358
+ mvp.multiply(world);
3359
+ this.sceneDepth.setMatrix(mvp.elements);
3360
+ const geometry = this.geometryCache.retain(gl, draw.batch.geometry);
3361
+ gl.bindVertexArray(geometry.vertexArray);
3362
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, geometry.triangleIndexBuffer);
3363
+ gl.drawElements(gl.TRIANGLES, geometry.triangleCount, draw.batch.geometry.indices instanceof Uint16Array ? gl.UNSIGNED_SHORT : gl.UNSIGNED_INT, 0);
3364
+ }
3365
+ for (const renderable of standalone) {
3366
+ if (isTransparentColor(renderable.mesh.material.color))
3367
+ continue;
3368
+ world.elements.set(renderable.worldMatrix);
3369
+ mvp.elements.set(viewProjection.elements);
3370
+ mvp.multiply(world);
3371
+ this.sceneDepth.setMatrix(mvp.elements);
3372
+ const geometry = this.geometryCache.retain(gl, renderable.mesh.geometry);
3373
+ gl.bindVertexArray(geometry.vertexArray);
3374
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, geometry.triangleIndexBuffer);
3375
+ gl.drawElements(gl.TRIANGLES, geometry.triangleCount, renderable.mesh.geometry.indices instanceof Uint16Array ? gl.UNSIGNED_SHORT : gl.UNSIGNED_INT, 0);
3376
+ }
3377
+ this.sceneDepth.end(deviceWidth, deviceHeight);
3378
+ aoReady = true;
3379
+ this.program.use();
3380
+ }
3381
+ if (this.shadowEnabled && this.shadowMap) {
3382
+ this.shadowMap.setBounds(accumulateSceneBounds(renderables));
3383
+ }
2476
3384
  if (this.shadowEnabled && this.shadowMap?.begin()) {
3385
+ const lightViewProjection = this.shadowMap.matrix;
2477
3386
  for (const batch of instances) {
2478
3387
  if (isTransparentColor(batch.material.color))
2479
3388
  continue;
@@ -2484,10 +3393,47 @@ var WebGL2RendererBackend = class {
2484
3393
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, instance.triangleIndexBuffer);
2485
3394
  gl.drawElementsInstanced(gl.TRIANGLES, instance.triangleCount, batch.geometry.indices instanceof Uint16Array ? gl.UNSIGNED_SHORT : gl.UNSIGNED_INT, 0, instance.instanceCount);
2486
3395
  }
3396
+ this.shadowMap.setInstancing(false);
3397
+ for (const draw of batches) {
3398
+ if (isTransparentColor(draw.batch.material.color))
3399
+ continue;
3400
+ world.elements.set(draw.worldMatrix);
3401
+ mvp.elements.set(lightViewProjection);
3402
+ mvp.multiply(world);
3403
+ this.shadowMap.setMatrix(mvp.elements);
3404
+ const geometry = this.geometryCache.retain(gl, draw.batch.geometry);
3405
+ gl.bindVertexArray(geometry.vertexArray);
3406
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, geometry.triangleIndexBuffer);
3407
+ gl.drawElements(gl.TRIANGLES, geometry.triangleCount, draw.batch.geometry.indices instanceof Uint16Array ? gl.UNSIGNED_SHORT : gl.UNSIGNED_INT, 0);
3408
+ }
3409
+ for (const renderable of standalone) {
3410
+ if (isTransparentColor(renderable.mesh.material.color))
3411
+ continue;
3412
+ world.elements.set(renderable.worldMatrix);
3413
+ mvp.elements.set(lightViewProjection);
3414
+ mvp.multiply(world);
3415
+ this.shadowMap.setMatrix(mvp.elements);
3416
+ const geometry = this.geometryCache.retain(gl, renderable.mesh.geometry);
3417
+ gl.bindVertexArray(geometry.vertexArray);
3418
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, geometry.triangleIndexBuffer);
3419
+ gl.drawElements(gl.TRIANGLES, geometry.triangleCount, renderable.mesh.geometry.indices instanceof Uint16Array ? gl.UNSIGNED_SHORT : gl.UNSIGNED_INT, 0);
3420
+ }
2487
3421
  this.shadowMap.end(frame.viewport.width * this.dpr, frame.viewport.height * this.dpr);
2488
3422
  this.program.use();
2489
- this.program.setShadow(this.shadowEnabled, this.shadowMap.texture, this.shadowMap.matrix);
2490
3423
  }
3424
+ if (this.shadowMap) {
3425
+ this.program.use();
3426
+ this.program.setShadow(
3427
+ this.shadowEnabled,
3428
+ this.shadowMap.texture,
3429
+ this.shadowMap.matrix,
3430
+ // 1.5 个纹素:够把平躺在光里的面抬离自身深度,又不会让影子明显离开物体。
3431
+ this.shadowMap.texelWorld * 1.5
3432
+ );
3433
+ this.program.setShadowDebug(this.shadowDebug);
3434
+ }
3435
+ this.program.setSceneDepth(aoReady ? this.sceneDepth.texture : null, frame.camera.getProjectionMatrix(aspect), frame.camera.near, frame.camera.far, Math.max(1, frame.viewport.width * this.dpr), Math.max(1, frame.viewport.height * this.dpr));
3436
+ this.program.setOcclusion(this.occlusionRadius, this.occlusionStrength);
2491
3437
  for (const transparentPass of [false, true]) {
2492
3438
  if (transparentPass) {
2493
3439
  gl.enable(gl.BLEND);
@@ -2509,7 +3455,7 @@ var WebGL2RendererBackend = class {
2509
3455
  this.instancedMeshIds.add(id);
2510
3456
  mvp.elements.set(viewProjection.elements);
2511
3457
  this.program.setMatrix(mvp);
2512
- this.program.setColor(colorToRgba(batch.material.color));
3458
+ this.applyMaterial(batch.material);
2513
3459
  this.program.setVertexColors(Boolean(batch.geometry.colors));
2514
3460
  this.program.setDirectionalShading(batch.material.shading !== "flat");
2515
3461
  this.program.setWaterShading(batch.material.shading === "water");
@@ -2521,7 +3467,7 @@ var WebGL2RendererBackend = class {
2521
3467
  this.program.setVertexColors(false);
2522
3468
  this.program.setDirectionalShading(false);
2523
3469
  this.program.setWaterShading(false);
2524
- this.program.setColor(colorToRgba(batch.material.edgeColor));
3470
+ this.applyEdgeMaterial(batch.material);
2525
3471
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, instance.edgeIndexBuffer);
2526
3472
  for (const [offsetX, offsetY] of EDGE_PIXEL_OFFSETS) {
2527
3473
  this.program.setClipOffset(offsetX * clipPixelX, offsetY * clipPixelY);
@@ -2540,7 +3486,7 @@ var WebGL2RendererBackend = class {
2540
3486
  world.elements.set(draw.worldMatrix);
2541
3487
  mvp.multiply(world);
2542
3488
  this.program.setMatrix(mvp);
2543
- this.program.setColor(colorToRgba(draw.batch.material.color));
3489
+ this.applyMaterial(draw.batch.material);
2544
3490
  const geometry = this.geometryCache.retain(gl, draw.batch.geometry);
2545
3491
  this.program.setVertexColors(Boolean(draw.batch.geometry.colors));
2546
3492
  this.program.setDirectionalShading(draw.batch.material.shading !== "flat");
@@ -2554,7 +3500,7 @@ var WebGL2RendererBackend = class {
2554
3500
  this.program.setVertexColors(false);
2555
3501
  this.program.setDirectionalShading(false);
2556
3502
  this.program.setWaterShading(false);
2557
- this.program.setColor(colorToRgba(draw.batch.material.edgeColor));
3503
+ this.applyEdgeMaterial(draw.batch.material);
2558
3504
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, geometry.edgeIndexBuffer);
2559
3505
  for (const [offsetX, offsetY] of EDGE_PIXEL_OFFSETS) {
2560
3506
  this.program.setClipOffset(offsetX * clipPixelX, offsetY * clipPixelY);
@@ -2573,7 +3519,7 @@ var WebGL2RendererBackend = class {
2573
3519
  world.elements.set(renderable.worldMatrix);
2574
3520
  mvp.multiply(world);
2575
3521
  this.program.setMatrix(mvp);
2576
- this.program.setColor(colorToRgba(renderable.mesh.material.color));
3522
+ this.applyMaterial(renderable.mesh.material);
2577
3523
  const geometry = this.geometryCache.retain(gl, renderable.mesh.geometry);
2578
3524
  this.program.setVertexColors(Boolean(renderable.mesh.geometry.colors));
2579
3525
  this.program.setDirectionalShading(renderable.mesh.material.shading !== "flat");
@@ -2586,7 +3532,7 @@ var WebGL2RendererBackend = class {
2586
3532
  this.program.setVertexColors(false);
2587
3533
  this.program.setDirectionalShading(false);
2588
3534
  this.program.setWaterShading(false);
2589
- this.program.setColor(colorToRgba(renderable.mesh.material.edgeColor));
3535
+ this.applyEdgeMaterial(renderable.mesh.material);
2590
3536
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, geometry.edgeIndexBuffer);
2591
3537
  for (const [offsetX, offsetY] of EDGE_PIXEL_OFFSETS) {
2592
3538
  this.program.setClipOffset(offsetX * clipPixelX, offsetY * clipPixelY);
@@ -6263,8 +7209,8 @@ function pickSceneNode(scene, camera, viewport, point) {
6263
7209
  target.y - eye.y,
6264
7210
  target.z - eye.z
6265
7211
  ]);
6266
- const right = normalize3(cross3(forward, [0, 1, 0]));
6267
- const up = normalize3(cross3(right, forward));
7212
+ const right = normalize3(cross2(forward, [0, 1, 0]));
7213
+ const up = normalize3(cross2(right, forward));
6268
7214
  const aspect = viewport.width / Math.max(viewport.height, 1);
6269
7215
  const tangent = Math.tan(camera.fov * Math.PI / 360);
6270
7216
  const ndcX = point.x / viewport.width * 2 - 1;
@@ -6306,7 +7252,7 @@ var normalize3 = (value) => {
6306
7252
  const length = Math.hypot(...value);
6307
7253
  return length > 0 ? [value[0] / length, value[1] / length, value[2] / length] : [0, 0, 0];
6308
7254
  };
6309
- var cross3 = (left, right) => [
7255
+ var cross2 = (left, right) => [
6310
7256
  left[1] * right[2] - left[2] * right[1],
6311
7257
  left[2] * right[0] - left[0] * right[2],
6312
7258
  left[0] * right[1] - left[1] * right[0]
@@ -6612,6 +7558,7 @@ var ICE3 = class extends ICEEventDispatcher {
6612
7558
  generatedOverlayCanvas = false;
6613
7559
  scene = null;
6614
7560
  camera = null;
7561
+ clipPlane = [0, 0, 0, 0];
6615
7562
  viewport = { x: 0, y: 0, width: 1, height: 1 };
6616
7563
  dpr = 1;
6617
7564
  frameId = 0;
@@ -6707,6 +7654,10 @@ var ICE3 = class extends ICEEventDispatcher {
6707
7654
  if (this.state === "destroyed")
6708
7655
  return;
6709
7656
  this.recordDiagnostic(error.code, error.message, "error");
7657
+ },
7658
+ onUploadReady: () => {
7659
+ if (this.state === "running")
7660
+ this.requestRender("upload-ready");
6710
7661
  }
6711
7662
  });
6712
7663
  window.addEventListener("resize", this.handleWindowResize);
@@ -6751,6 +7702,17 @@ var ICE3 = class extends ICEEventDispatcher {
6751
7702
  this.requestRender("background-color");
6752
7703
  return this;
6753
7704
  }
7705
+ /**
7706
+ * Section plane `[nx, ny, nz, d]`: fragments with `dot(world, n) + d < 0`
7707
+ * are discarded. `null` (or a zero plane) disables clipping.
7708
+ */
7709
+ setClipPlane(plane) {
7710
+ this.assertActive();
7711
+ this.clipPlane = plane ? [plane[0], plane[1], plane[2], plane[3]] : [0, 0, 0, 0];
7712
+ this.backend.setClipPlane?.(this.clipPlane);
7713
+ this.requestRender("clip-plane");
7714
+ return this;
7715
+ }
6754
7716
  prepareRenderBatchGeometry(geometry) {
6755
7717
  if (!this.backend.prepareBatchGeometry) {
6756
7718
  throw new ICEEngineError("ICE3_RENDER_BATCH_ADAPTER_UNAVAILABLE", "The active renderer cannot stage adaptive batch geometry");
@@ -7544,7 +8506,13 @@ function buildAssetScene(snapshot) {
7544
8506
  const materials = snapshot.materials.map((material) => new ICEBasicMaterial({
7545
8507
  color: material.baseColor,
7546
8508
  ...material.edgeColor ? { edgeColor: material.edgeColor } : {},
7547
- ...material.shading ? { shading: material.shading } : {}
8509
+ ...material.shading ? { shading: material.shading } : {},
8510
+ ...material.map ? { map: material.map } : {},
8511
+ ...material.normalMap ? { normalMap: material.normalMap } : {},
8512
+ ...material.ormMap ? { ormMap: material.ormMap } : {},
8513
+ ...material.metallic !== void 0 ? { metallic: material.metallic } : {},
8514
+ ...material.roughness !== void 0 ? { roughness: material.roughness } : {},
8515
+ ...material.emissive ? { emissive: material.emissive } : {}
7548
8516
  }));
7549
8517
  const meshesByNode = /* @__PURE__ */ new Map();
7550
8518
  for (const mesh of snapshot.meshes) {
@@ -7697,6 +8665,45 @@ function colorHex(color) {
7697
8665
  }
7698
8666
 
7699
8667
  // packages/assets/dist/ICEProfileValidator.js
8668
+ function validateTextures(json) {
8669
+ const images = records(json.images);
8670
+ const textures = records(json.textures);
8671
+ const samplers = records(json.samplers);
8672
+ for (const image of images) {
8673
+ const uri = image.uri;
8674
+ if (typeof uri !== "string" || !uri || uri.startsWith("/") || uri.includes("..") || uri.includes("://") || image.bufferView !== void 0) {
8675
+ throw assetError("ICE3_ASSET_UNSUPPORTED_FEATURE", "images must reference a relative file path");
8676
+ }
8677
+ }
8678
+ for (const texture of textures) {
8679
+ const source = Number(texture.source);
8680
+ if (!Number.isInteger(source) || source < 0 || source >= images.length) {
8681
+ throw assetError("ICE3_ASSET_INVALID_GLB", "texture source is out of range");
8682
+ }
8683
+ if (texture.sampler !== void 0) {
8684
+ const sampler = Number(texture.sampler);
8685
+ if (!Number.isInteger(sampler) || sampler < 0 || sampler >= samplers.length) {
8686
+ throw assetError("ICE3_ASSET_INVALID_GLB", "texture sampler is out of range");
8687
+ }
8688
+ }
8689
+ }
8690
+ for (const material of records(json.materials)) {
8691
+ const slots = [
8692
+ ["baseColorTexture", record(record(material.pbrMetallicRoughness)?.baseColorTexture)],
8693
+ ["normalTexture", record(material.normalTexture)],
8694
+ ["metallicRoughnessTexture", record(record(material.pbrMetallicRoughness)?.metallicRoughnessTexture)],
8695
+ ["occlusionTexture", record(material.occlusionTexture)]
8696
+ ];
8697
+ for (const [slot, reference] of slots) {
8698
+ if (!reference)
8699
+ continue;
8700
+ const index = Number(reference.index);
8701
+ if (!Number.isInteger(index) || index < 0 || index >= textures.length) {
8702
+ throw assetError("ICE3_ASSET_INVALID_GLB", `${slot} index is out of range`);
8703
+ }
8704
+ }
8705
+ }
8706
+ }
7700
8707
  function validatePreparedProfile(json, binary) {
7701
8708
  const asset = record(json.asset);
7702
8709
  const profile = record(record(asset?.extras)?.ice3);
@@ -7724,15 +8731,13 @@ function validatePreparedProfile(json, binary) {
7724
8731
  "animations",
7725
8732
  "skins",
7726
8733
  "cameras",
7727
- "textures",
7728
- "images",
7729
- "samplers",
7730
8734
  "lights"
7731
8735
  ]) {
7732
8736
  if (Array.isArray(json[field]) && json[field].length > 0) {
7733
8737
  throw assetError("ICE3_ASSET_UNSUPPORTED_FEATURE", `${field} are unsupported by the M1 profile`);
7734
8738
  }
7735
8739
  }
8740
+ validateTextures(json);
7736
8741
  if (records(json.scenes).length !== 1 || json.scene !== 0 || records(json.buffers).length !== 1 || records(json.buffers)[0]?.uri !== void 0 || Number(records(json.buffers)[0]?.byteLength) > binary.byteLength) {
7737
8742
  throw assetError("ICE3_ASSET_INVALID_GLB", "profile container fields are invalid");
7738
8743
  }
@@ -7890,13 +8895,15 @@ function assetError(code, message) {
7890
8895
  }
7891
8896
 
7892
8897
  // packages/assets/dist/ICEGLBReader.js
7893
- function readPreparedGLB(bytes) {
8898
+ function readPreparedGLB(bytes, options = {}) {
7894
8899
  const { json, binary, profile } = parseGLB(bytes);
7895
8900
  const accessors = records(json.accessors);
7896
8901
  const views = records(json.bufferViews);
7897
8902
  const sourceNodes = records(json.nodes);
7898
8903
  const sourceMeshes = records(json.meshes);
7899
8904
  const sourceMaterials = records(json.materials);
8905
+ const sourceTextures = records(json.textures);
8906
+ const sourceImages = records(json.images);
7900
8907
  const activeScene = records(json.scenes)[Number(json.scene ?? 0)];
7901
8908
  const sceneRoots = integers(activeScene?.nodes);
7902
8909
  const parents = new Array(sourceNodes.length).fill(null);
@@ -7979,11 +8986,18 @@ function readPreparedGLB(bytes) {
7979
8986
  const edgeMaterial = edgePrimitive ? sourceMaterials[Number(edgePrimitive.material)] : void 0;
7980
8987
  const shading = record(record(baseMaterial?.extras)?.ice3)?.shading;
7981
8988
  const materialIndex = materials.length;
8989
+ const map = materialMap(baseMaterial, sourceTextures, sourceImages, options.baseUrl);
8990
+ const normalMap = normalMaterialMap(baseMaterial, sourceTextures, sourceImages, options.baseUrl);
8991
+ const ormMap = ormMaterialMap(baseMaterial, sourceTextures, sourceImages, options.baseUrl);
7982
8992
  materials.push({
7983
8993
  id: `${meshId}/material/${primitiveIndex}`,
7984
8994
  baseColor: materialColor(baseMaterial),
7985
8995
  ...edgeMaterial ? { edgeColor: materialColor(edgeMaterial) } : {},
7986
- ...shading === "directional" || shading === "water" ? { shading } : {}
8996
+ ...shading === "directional" || shading === "water" ? { shading } : {},
8997
+ ...map ? { map } : {},
8998
+ ...normalMap ? { normalMap } : {},
8999
+ ...ormMap ? { ormMap } : {},
9000
+ ...surfaceFactors(baseMaterial)
7987
9001
  });
7988
9002
  const bounds = boundsFromAccessor(accessors[positionIndex]);
7989
9003
  const geometryIndex = geometries.length;
@@ -8293,6 +9307,93 @@ function boundsFromProfile(value) {
8293
9307
  max: [Number(max[0]), Number(max[1]), Number(max[2])]
8294
9308
  };
8295
9309
  }
9310
+ function materialMap(material, textures, images, baseUrl) {
9311
+ const reference = record(record(material?.pbrMetallicRoughness)?.baseColorTexture);
9312
+ if (!reference)
9313
+ return void 0;
9314
+ const texture = textures[Number(reference.index)];
9315
+ const uri = record(images[Number(record(texture)?.source)])?.uri;
9316
+ if (typeof uri !== "string" || uri.length === 0)
9317
+ return void 0;
9318
+ const transform = record(record(reference.extensions)?.KHR_texture_transform);
9319
+ const scale = numbers(transform?.scale, 2);
9320
+ const su = scale?.[0];
9321
+ const sv = scale?.[1];
9322
+ return {
9323
+ url: resolveTextureUrl(uri, baseUrl),
9324
+ ...typeof su === "number" && typeof sv === "number" && su > 0 && sv > 0 ? { scale: [su, sv] } : {}
9325
+ };
9326
+ }
9327
+ function surfaceFactors(material) {
9328
+ const pbr = record(material?.pbrMetallicRoughness);
9329
+ const metallic = unitOrUndefined(pbr?.metallicFactor);
9330
+ const roughness = unitOrUndefined(pbr?.roughnessFactor);
9331
+ const emissive = numbers(material?.emissiveFactor, 3);
9332
+ return {
9333
+ ...metallic !== void 0 ? { metallic } : {},
9334
+ ...roughness !== void 0 ? { roughness } : {},
9335
+ ...emissive && emissive.some((value) => value > 0) ? { emissive: [emissive[0], emissive[1], emissive[2]] } : {}
9336
+ };
9337
+ }
9338
+ function unitOrUndefined(value) {
9339
+ const parsed = Number(value);
9340
+ if (!Number.isFinite(parsed))
9341
+ return void 0;
9342
+ return Math.min(1, Math.max(0, parsed));
9343
+ }
9344
+ function normalMaterialMap(material, textures, images, baseUrl) {
9345
+ const reference = record(material?.normalTexture);
9346
+ if (!reference)
9347
+ return void 0;
9348
+ const texture = textures[Number(reference.index)];
9349
+ const uri = record(images[Number(record(texture)?.source)])?.uri;
9350
+ if (typeof uri !== "string" || uri.length === 0)
9351
+ return void 0;
9352
+ const strength = Number(reference.scale);
9353
+ return {
9354
+ url: resolveTextureUrl(uri, baseUrl),
9355
+ ...Number.isFinite(strength) && strength >= 0 ? { strength } : {}
9356
+ };
9357
+ }
9358
+ function ormMaterialMap(material, textures, images, baseUrl) {
9359
+ const metallicRoughness = textureReference(record(record(material?.pbrMetallicRoughness)?.metallicRoughnessTexture), textures, images, baseUrl);
9360
+ const occlusion = textureReference(record(material?.occlusionTexture), textures, images, baseUrl);
9361
+ if (!metallicRoughness && !occlusion)
9362
+ return void 0;
9363
+ const url = metallicRoughness?.url ?? occlusion.url;
9364
+ const scale = metallicRoughness?.scale ?? occlusion?.scale;
9365
+ return {
9366
+ url,
9367
+ ...scale ? { scale } : {},
9368
+ occlusion: occlusion?.url === url,
9369
+ metallicRoughness: metallicRoughness !== void 0
9370
+ };
9371
+ }
9372
+ function textureReference(reference, textures, images, baseUrl) {
9373
+ if (!reference)
9374
+ return void 0;
9375
+ const texture = textures[Number(reference.index)];
9376
+ const uri = record(images[Number(record(texture)?.source)])?.uri;
9377
+ if (typeof uri !== "string" || uri.length === 0)
9378
+ return void 0;
9379
+ const transform = record(record(reference.extensions)?.KHR_texture_transform);
9380
+ const scale = numbers(transform?.scale, 2);
9381
+ const su = scale?.[0];
9382
+ const sv = scale?.[1];
9383
+ return {
9384
+ url: resolveTextureUrl(uri, baseUrl),
9385
+ ...typeof su === "number" && typeof sv === "number" && su > 0 && sv > 0 ? { scale: [su, sv] } : {}
9386
+ };
9387
+ }
9388
+ function resolveTextureUrl(uri, baseUrl) {
9389
+ if (!baseUrl)
9390
+ return uri;
9391
+ try {
9392
+ return new URL(uri, baseUrl).href;
9393
+ } catch {
9394
+ return uri;
9395
+ }
9396
+ }
8296
9397
  function materialColor(material) {
8297
9398
  if (!material)
8298
9399
  return "#808080";
@@ -9193,7 +10294,7 @@ var ICEAssetLoader = class {
9193
10294
  throw new Error(`ICE3_ASSET_FETCH_FAILED: HTTP ${response.status}`);
9194
10295
  }
9195
10296
  const bytes = await response.arrayBuffer();
9196
- const snapshot = readPreparedGLB(bytes);
10297
+ const snapshot = readPreparedGLB(bytes, { baseUrl: url });
9197
10298
  return { bytes, snapshot };
9198
10299
  }
9199
10300
  };
@@ -9983,7 +11084,7 @@ var ICETilesetHandle = class extends ICEEventDispatcher {
9983
11084
  adaptivePlanningSignature = "";
9984
11085
  renderBatchingMode;
9985
11086
  visibleCoverageIds = /* @__PURE__ */ new Set();
9986
- constructor(engine, snapshot, camera, scheduler, fetch, maxConcurrentTiles, maxCommitsPerFrame, maxScreenSpaceErrorPixels, lodSettleDelayMs, renderBatching, callerSignal) {
11087
+ constructor(engine, snapshot, camera, scheduler, fetch2, maxConcurrentTiles, maxCommitsPerFrame, maxScreenSpaceErrorPixels, lodSettleDelayMs, renderBatching, callerSignal) {
9987
11088
  super();
9988
11089
  this.engine = engine;
9989
11090
  this.snapshot = snapshot;
@@ -10014,7 +11115,7 @@ var ICETilesetHandle = class extends ICEEventDispatcher {
10014
11115
  });
10015
11116
  }
10016
11117
  this.assetLoader = new ICEAssetLoader(engine, {
10017
- fetch: (input, init) => fetch(input, {
11118
+ fetch: (input, init) => fetch2(input, {
10018
11119
  ...init ?? {},
10019
11120
  signal: this.abortController.signal
10020
11121
  })
@@ -11174,8 +12275,20 @@ export {
11174
12275
  deserializeScene,
11175
12276
  serializeScene
11176
12277
  };
11177
- //! 方向光阴影贴图。
12278
+ //! 阴影视锥:按场景包围盒拟合方向光的正交投影。
11178
12279
  //!
12280
+ //! 早期实现把光照视锥写死成 860 米见方、960 米深。两个后果都很贵:
12281
+ //! 2048 的深度图摊到 860 米是每纹素 0.42 米(薄墙的影子直接消失),
12282
+ //! 而深度偏差 0.0015 在 960 米的跨度上等于 1.4 米,短影子被整段吃掉。
12283
+ //! 视锥贴合场景后,偏差回到"厘米"量级,贴图分辨率也才有意义。
12284
+ //! 方向光阴影贴图。
11179
12285
  //! 一次深度 pass 把场景从光源方向渲染进深度纹理,主 pass 再按世界坐标比较深度。
11180
- //! 只处理单方向光、单张贴图,够覆盖当前查看器的需要;级联与软阴影留给后续。
12286
+ //! 只处理单方向光、单张贴图;视锥按场景包围盒拟合(见 ShadowFrustum)。
12287
+ //! 场景包围盒:给阴影视锥用。
12288
+ //! 引擎里没有现成的场景 bounds,而阴影视锥必须贴合实际内容——写死范围会让
12289
+ //! 深度精度和贴图分辨率一起浪费(见 ShadowFrustum)。这里按"几何局部包围盒
12290
+ //! 缓存 + 世界矩阵变换"累加,避免每帧扫描几十万个顶点。
12291
+ //! 相机深度通道:屏幕空间遮蔽的来源。
12292
+ //! 从**相机**方向渲染一遍深度。必须覆盖全部几何路径——瓦片走渲染批次,其余走实例化
12293
+ //! 批次;只画一条会让深度图大半是空的(SPEC-007)。
11181
12294
  //# sourceMappingURL=index.js.map