@driftengine/core 4.7.2 → 4.7.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.
@@ -938,7 +938,7 @@ export const MAIN_GLSL = `void main() {
938
938
  int clusterBaseTexel = LIGHT_REGION_TEXELS
939
939
  + (clusterTileX + clusterTileY * CLUSTER_X + clusterSlice * CLUSTER_X * CLUSTER_Y)
940
940
  * CLUSTER_TEXELS;
941
- int clusterLights = uClustered == 1 ? int(clusterTexel(clusterBaseTexel).x) : 0;
941
+ int clusterLights = uClustered != 0 ? int(clusterTexel(clusterBaseTexel).x) : 0;
942
942
 
943
943
  /* The fixture atlas's size, asked once: its cookie band and its profile rows are found from it. */
944
944
  vec2 fixtureSize = vec2(textureSize(uFixtureAtlas, 0));
@@ -967,7 +967,7 @@ export const MAIN_GLSL = `void main() {
967
967
  float iesProfile;
968
968
  vec3 iesAxis;
969
969
  float cookie;
970
- if (uClustered == 1) {
970
+ if (uClustered != 0) {
971
971
  if (slot >= clusterLights) break;
972
972
  /*
973
973
  * Four indices to a texel. Selected with compares rather than by indexing the vector,
@@ -982,32 +982,65 @@ export const MAIN_GLSL = `void main() {
982
982
  i = int(packed);
983
983
  int record = i * LIGHT_TEXELS;
984
984
  uvec4 rec0 = clusterTexel(record);
985
- uvec4 rec1 = clusterTexel(record + 1);
986
- uvec4 rec2 = clusterTexel(record + 2);
987
985
  lightPos = vec3(
988
986
  uintBitsToFloat(rec0.x), uintBitsToFloat(rec0.y), uintBitsToFloat(rec0.z));
989
987
  lightRadius = uintBitsToFloat(rec0.w);
988
+ /*
989
+ * **Out of reach is decided on the first texel, before the rest of the record is read.**
990
+ * A froxel holds every light whose sphere touches its box, so most of the lights a fragment
991
+ * walks never reach it, and each one used to cost all five texels before the falloff below
992
+ * said so. Both falloffs are zero at and past the radius, so this skips only lights the
993
+ * \`continue\` after the falloff would have skipped anyway: 17.5 ms of a 4K main pass with
994
+ * 320 lamps was 12.3 with this and the profile guard below, byte for byte the same picture.
995
+ */
996
+ if (length(lightPos - vWorldPos) >= lightRadius) continue;
997
+ /*
998
+ * **The second texel, and for a plain point light the last one.** The emitter size and the
999
+ * weight are two halves in one word, which is how nine numbers fit in two texels;
1000
+ * \`unpackHalf2x16\` widens them exactly, and the fixed arm reads the same two values rounded
1001
+ * the same way, so the arms stay one picture. See \`packSizeAndWeight\`.
1002
+ */
1003
+ uvec4 rec1 = clusterTexel(record + 1);
990
1004
  lightColor = vec3(
991
1005
  uintBitsToFloat(rec1.x), uintBitsToFloat(rec1.y), uintBitsToFloat(rec1.z));
992
- lightSourceRadius = uintBitsToFloat(rec1.w);
993
- lightWeight = uintBitsToFloat(rec2.x);
994
- shadowSlot = int(uintBitsToFloat(rec2.y));
1006
+ vec2 sizeAndWeight = unpackHalf2x16(rec1.w);
1007
+ lightSourceRadius = abs(sizeAndWeight.x);
1008
+ lightWeight = sizeAndWeight.y;
995
1009
  /*
996
- * The fourth texel, which is the cone. See LIGHT_RECORD in clusteredLights.ts: the
997
- * direction spans rec2.z, rec2.w and rec3.x, because the record is a run of floats rather
998
- * than a struct and a vec3 does not respect texel boundaries.
1010
+ * **The fixture, read only for a light that has one.** What a plain light's record holds in
1011
+ * the last three texels is exactly these values, so a light carrying no shadow slot, cone,
1012
+ * profile or cookie shades the same whether they are read or assumed, and at heavy overlap
1013
+ * reading them was a third of the pass. A branch on a value from a texture around
1014
+ * \`texelFetch\` is not the 2026-08-07 case: an integer fetch takes no derivative.
1015
+ *
1016
+ * **Inside a branch on \`uClustered\` first**, which is 2 only in a frame where some light
1017
+ * carries a fixture. The test on each light's flag is a branch on a value read from a
1018
+ * texture, which a compiler may run both sides of, and it cost 1.3 ms of a 4K pass whose 320
1019
+ * lamps were all plain; a uniform branch costs nothing, so such a frame never reaches it.
999
1020
  */
1000
- uvec4 rec3 = clusterTexel(record + 3);
1001
- lightDir = vec3(
1002
- uintBitsToFloat(rec2.z), uintBitsToFloat(rec2.w), uintBitsToFloat(rec3.x));
1003
- lightCone = vec2(uintBitsToFloat(rec3.y), uintBitsToFloat(rec3.z));
1004
- iesProfile = uintBitsToFloat(rec3.w);
1005
- /* The fifth texel, which is the photometric azimuth. See LIGHT_RECORD in clusteredLights.ts
1006
- for why it costs a texel rather than being derived from the direction. */
1007
- uvec4 rec4 = clusterTexel(record + 4);
1008
- iesAxis = vec3(
1009
- uintBitsToFloat(rec4.x), uintBitsToFloat(rec4.y), uintBitsToFloat(rec4.z));
1010
- cookie = uintBitsToFloat(rec4.w);
1021
+ shadowSlot = -1;
1022
+ lightDir = vec3(0.0);
1023
+ lightCone = vec2(-1.0, -2.0);
1024
+ iesProfile = -1.0;
1025
+ iesAxis = vec3(0.0);
1026
+ cookie = -1.0;
1027
+ if (uClustered == 2) {
1028
+ if ((rec1.w & LIGHT_FIXTURE_FLAG) != 0u) {
1029
+ uvec4 rec2 = clusterTexel(record + 2);
1030
+ uvec4 rec3 = clusterTexel(record + 3);
1031
+ uvec4 rec4 = clusterTexel(record + 4);
1032
+ shadowSlot = int(uintBitsToFloat(rec2.x));
1033
+ lightDir = vec3(
1034
+ uintBitsToFloat(rec2.y), uintBitsToFloat(rec2.z), uintBitsToFloat(rec2.w));
1035
+ lightCone = vec2(uintBitsToFloat(rec3.x), uintBitsToFloat(rec3.y));
1036
+ iesProfile = uintBitsToFloat(rec3.z);
1037
+ cookie = uintBitsToFloat(rec3.w);
1038
+ /* The photometric azimuth. See LIGHT_RECORD in clusteredLights.ts for why it costs a
1039
+ texel rather than being derived from the direction. */
1040
+ iesAxis = vec3(
1041
+ uintBitsToFloat(rec4.x), uintBitsToFloat(rec4.y), uintBitsToFloat(rec4.z));
1042
+ }
1043
+ }
1011
1044
  } else {
1012
1045
  i = slot;
1013
1046
  if (i >= uLightCount || i >= MAX_LIGHTS) break;
@@ -1226,20 +1259,25 @@ export const MAIN_GLSL = `void main() {
1226
1259
  * and the result is selected afterwards with a \`mix\` on a compare — which is arithmetic
1227
1260
  * rather than control flow.
1228
1261
  *
1229
- * **What it costs** is one texture fetch per light per fragment on every scene, whether or
1230
- * not anything loaded a profile. **What would make it wrong** is a scene where that fetch is
1231
- * measurable against the shadow taps in the same loop, which would be a scene with many
1232
- * lights and no shadows at all — at which point the honest answer is a permutation flag, and
1233
- * \`ARCHITECTURE.md\` section 1 has the arithmetic for whether it earns one.
1262
+ * **And skipped outright where nothing loaded a profile**, behind \`uIesAtlasRows\`, which is
1263
+ * one while the atlas holds only its row of ones. A uniform is provably uniform control
1264
+ * flow, so this branch is not the one the rule forbids. It was paid on every scene until a
1265
+ * 4K frame of 320 lamps and no shadows measured it at 0.7 ms of the main pass, rising to
1266
+ * 2.3 ms with the lamps overlapping, which is the scene this note used to say would make
1267
+ * the fetch wrong. Every light then reads 1, which is what \`step(0.0, iesProfile)\` gave
1268
+ * each of them anyway.
1234
1269
  */
1235
1270
  float iesRow = max(iesProfile, 0.0);
1236
1271
  /* The direction the fixture throws toward this fragment, which both angles are measured on. */
1237
1272
  vec3 iesRay = -toLight / max(dist, 1e-4);
1238
1273
  /* The angle from the fixture's axis, as a fraction of the 0-to-180 arc every row spans. */
1239
- float iesAngle = acos(clamp(dot(iesRay, lightDir), -1.0, 1.0));
1240
- /* Clamped to the row's first and last texel centres, which is what the edge clamp of a
1241
- texture exactly one row wide gave; the atlas can be wider now, with cookies beside it. */
1242
- float iesU = clamp(iesAngle / PI_IES * IES_WIDTH, 0.5, IES_WIDTH - 0.5) / fixtureSize.x;
1274
+ float iesU = 0.0;
1275
+ if (uIesAtlasRows > 1.0) {
1276
+ float iesAngle = acos(clamp(dot(iesRay, lightDir), -1.0, 1.0));
1277
+ /* Clamped to the row's first and last texel centres, which is what the edge clamp of a
1278
+ texture exactly one row wide gave; the atlas can be wider now, with cookies beside it. */
1279
+ iesU = clamp(iesAngle / PI_IES * IES_WIDTH, 0.5, IES_WIDTH - 0.5) / fixtureSize.x;
1280
+ }
1243
1281
  /*
1244
1282
  * **The azimuth about the fixture's own axis**, behind a branch on a uniform so a scene whose
1245
1283
  * profiles are all axially symmetric pays none of this arithmetic. A \`uniform float\` is
@@ -1314,14 +1352,15 @@ export const MAIN_GLSL = `void main() {
1314
1352
  * version this replaced took two explicit fetches and a \`mix\`, and cost **18,315 gzipped
1315
1353
  * bytes on \`core-only\`** across the sixteen fragment permutations.
1316
1354
  */
1317
- float iesBase = iesRow * (uIesPlaneCount + 1.0);
1318
- /* The profiles are the atlas's bottom rows, below any cookies. */
1319
- float iesTop = fixtureSize.y - uIesAtlasRows;
1320
- float iesV = uIesAtlasRows <= 1.0
1321
- ? (iesTop + 0.5) / fixtureSize.y
1322
- : (iesTop + iesBase + iesTurns * uIesPlaneCount + 0.5) / fixtureSize.y;
1323
- float iesGain = textureLod(uFixtureAtlas, vec2(iesU, iesV), 0.0).r;
1324
- float photometric = mix(1.0, iesGain, step(0.0, iesProfile));
1355
+ float photometric = 1.0;
1356
+ if (uIesAtlasRows > 1.0) {
1357
+ float iesBase = iesRow * (uIesPlaneCount + 1.0);
1358
+ /* The profiles are the atlas's bottom rows, below any cookies. */
1359
+ float iesTop = fixtureSize.y - uIesAtlasRows;
1360
+ float iesV = (iesTop + iesBase + iesTurns * uIesPlaneCount + 0.5) / fixtureSize.y;
1361
+ float iesGain = textureLod(uFixtureAtlas, vec2(iesU, iesV), 0.0).r;
1362
+ photometric = mix(1.0, iesGain, step(0.0, iesProfile));
1363
+ }
1325
1364
 
1326
1365
  float shape = (uLightFalloff == 1 ? falloff : falloff * falloff) * coneFalloff * photometric;
1327
1366
  glassGlow += lightColor * backNdl * shape * lightWeight * shaded * lampGlass;
@@ -1,7 +1,7 @@
1
1
  /** Version, inputs, uniforms and the defines the rest of the fragment shader is written against. */
2
2
  import { COOKIE_TILE } from '../../lightBudget.js';
3
3
  import { IES_ATLAS_WIDTH } from '../../iesProfile.js';
4
- import { CLUSTER_TEXELS, CLUSTER_X, CLUSTER_Y, CLUSTER_Z, LIGHT_REGION_TEXELS, LIGHT_TEXELS, MAX_LIGHTS_PER_CLUSTER, TABLE_WIDTH, } from '../../clusteredLights.js';
4
+ import { CLUSTER_TEXELS, CLUSTER_X, CLUSTER_Y, CLUSTER_Z, LIGHT_FIXTURE_FLAG, LIGHT_REGION_TEXELS, LIGHT_TEXELS, MAX_LIGHTS_PER_CLUSTER, TABLE_WIDTH, } from '../../clusteredLights.js';
5
5
  /**
6
6
  * How much roughness a relieved or normal-mapped surface gains per unit of relief.
7
7
  *
@@ -647,6 +647,7 @@ uniform float uHighlightGain;
647
647
  #define CLUSTER_TEXELS ${CLUSTER_TEXELS}
648
648
  #define MAX_LIGHTS_PER_CLUSTER ${MAX_LIGHTS_PER_CLUSTER}
649
649
  #define LIGHT_TEXELS ${LIGHT_TEXELS}
650
+ #define LIGHT_FIXTURE_FLAG ${LIGHT_FIXTURE_FLAG}u
650
651
  #define LIGHT_REGION_TEXELS ${LIGHT_REGION_TEXELS}
651
652
  #define CLUSTER_TABLE_WIDTH ${TABLE_WIDTH}
652
653
  uniform highp usampler2D uClusterTable; // wgsl:share shadow