@driftengine/core 4.7.1 → 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.
- package/CHANGELOG.json +27 -0
- package/dist/render/backend/webgl2/renderer.js +7 -17
- package/dist/render/backend/webgpu/clusterBinner.js +5 -41
- package/dist/render/backend/webgpu/renderer.js +9 -17
- package/dist/render/backend/webgpu/shaders/clusterBin.wgsl.d.ts +1 -1
- package/dist/render/backend/webgpu/shaders/clusterBin.wgsl.js +9 -5
- package/dist/render/clusteredLights.d.ts +136 -13
- package/dist/render/clusteredLights.js +225 -51
- package/dist/render/lightBudget.d.ts +8 -0
- package/dist/render/lightBudget.js +10 -3
- package/dist/render/shaders/flat/main.d.ts +1 -1
- package/dist/render/shaders/flat/main.js +77 -38
- package/dist/render/shaders/flat/preamble.js +2 -1
- package/dist/render/shaders/generated/flat.wgsl.js +1 -1
- package/package.json +6 -6
- package/src/render/backend/webgl2/renderer.ts +10 -26
- package/src/render/backend/webgpu/clusterBinner.ts +11 -44
- package/src/render/backend/webgpu/renderer.ts +14 -25
- package/src/render/backend/webgpu/shaders/clusterBin.wgsl.ts +9 -5
- package/src/render/clusteredLights.ts +272 -58
- package/src/render/lightBudget.ts +23 -3
- package/src/render/shaders/flat/main.ts +77 -38
- package/src/render/shaders/flat/preamble.ts +2 -0
- package/src/render/shaders/generated/flat.wgsl.ts +1 -1
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
/** Fixed point-light budget shared by CPU selection and the flat shader. */
|
|
2
2
|
import type { AreaLightBuffer } from './areaLights.ts';
|
|
3
3
|
import type { ResolvedAreaShadows } from './areaShadowSet.ts';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
POINT_LIGHT_COS_INNER,
|
|
6
|
+
POINT_LIGHT_COS_OUTER,
|
|
7
|
+
lampSourceRadius,
|
|
8
|
+
lampWeight,
|
|
9
|
+
} from './clusteredLights.ts';
|
|
5
10
|
|
|
6
11
|
/**
|
|
7
12
|
* How many point lights the shading pass shades against at once.
|
|
@@ -531,6 +536,14 @@ export interface ResolvedPointLights {
|
|
|
531
536
|
iesProfiles: Float32Array;
|
|
532
537
|
iesAxes: Float32Array;
|
|
533
538
|
cookies: Float32Array;
|
|
539
|
+
/**
|
|
540
|
+
* Owned, `MAX_POINT_LIGHTS` long: the emitter sizes and weights as the froxel record carries
|
|
541
|
+
* them, at half precision. `sourceRadii` and `weights` point here once a light set resolves, so
|
|
542
|
+
* a lamp on the uniform path and the same lamp in the table shade with the same numbers. See
|
|
543
|
+
* `lampSourceRadius`.
|
|
544
|
+
*/
|
|
545
|
+
readonly heldSourceRadii: Float32Array;
|
|
546
|
+
readonly heldWeights: Float32Array;
|
|
534
547
|
}
|
|
535
548
|
|
|
536
549
|
/** One per GL binder, refilled per call: holds references, allocates nothing. */
|
|
@@ -547,6 +560,8 @@ const glLights: ResolvedPointLights = {
|
|
|
547
560
|
iesProfiles: NO_PROFILES,
|
|
548
561
|
iesAxes: NO_IES_AXES,
|
|
549
562
|
cookies: NO_COOKIES,
|
|
563
|
+
heldSourceRadii: new Float32Array(MAX_POINT_LIGHTS),
|
|
564
|
+
heldWeights: new Float32Array(MAX_POINT_LIGHTS),
|
|
550
565
|
};
|
|
551
566
|
|
|
552
567
|
/**
|
|
@@ -659,8 +674,13 @@ export function resolvePointLights(
|
|
|
659
674
|
out.positions = positions;
|
|
660
675
|
out.colors = colors;
|
|
661
676
|
out.radii = radii;
|
|
662
|
-
|
|
663
|
-
|
|
677
|
+
/* Rounded as the clustered record rounds them; see `heldSourceRadii`. */
|
|
678
|
+
for (let light = 0; light < MAX_POINT_LIGHTS; light++) {
|
|
679
|
+
out.heldSourceRadii[light] = lampSourceRadius(sourceRadii[light] ?? 0);
|
|
680
|
+
out.heldWeights[light] = lampWeight(weights[light] ?? 1);
|
|
681
|
+
}
|
|
682
|
+
out.sourceRadii = out.heldSourceRadii;
|
|
683
|
+
out.weights = out.heldWeights;
|
|
664
684
|
out.directions = directions;
|
|
665
685
|
out.coneCos = coneCos;
|
|
666
686
|
out.iesProfiles = iesProfiles;
|
|
@@ -939,7 +939,7 @@ export const MAIN_GLSL = `void main() {
|
|
|
939
939
|
int clusterBaseTexel = LIGHT_REGION_TEXELS
|
|
940
940
|
+ (clusterTileX + clusterTileY * CLUSTER_X + clusterSlice * CLUSTER_X * CLUSTER_Y)
|
|
941
941
|
* CLUSTER_TEXELS;
|
|
942
|
-
int clusterLights = uClustered
|
|
942
|
+
int clusterLights = uClustered != 0 ? int(clusterTexel(clusterBaseTexel).x) : 0;
|
|
943
943
|
|
|
944
944
|
/* The fixture atlas's size, asked once: its cookie band and its profile rows are found from it. */
|
|
945
945
|
vec2 fixtureSize = vec2(textureSize(uFixtureAtlas, 0));
|
|
@@ -968,7 +968,7 @@ export const MAIN_GLSL = `void main() {
|
|
|
968
968
|
float iesProfile;
|
|
969
969
|
vec3 iesAxis;
|
|
970
970
|
float cookie;
|
|
971
|
-
if (uClustered
|
|
971
|
+
if (uClustered != 0) {
|
|
972
972
|
if (slot >= clusterLights) break;
|
|
973
973
|
/*
|
|
974
974
|
* Four indices to a texel. Selected with compares rather than by indexing the vector,
|
|
@@ -983,32 +983,65 @@ export const MAIN_GLSL = `void main() {
|
|
|
983
983
|
i = int(packed);
|
|
984
984
|
int record = i * LIGHT_TEXELS;
|
|
985
985
|
uvec4 rec0 = clusterTexel(record);
|
|
986
|
-
uvec4 rec1 = clusterTexel(record + 1);
|
|
987
|
-
uvec4 rec2 = clusterTexel(record + 2);
|
|
988
986
|
lightPos = vec3(
|
|
989
987
|
uintBitsToFloat(rec0.x), uintBitsToFloat(rec0.y), uintBitsToFloat(rec0.z));
|
|
990
988
|
lightRadius = uintBitsToFloat(rec0.w);
|
|
989
|
+
/*
|
|
990
|
+
* **Out of reach is decided on the first texel, before the rest of the record is read.**
|
|
991
|
+
* A froxel holds every light whose sphere touches its box, so most of the lights a fragment
|
|
992
|
+
* walks never reach it, and each one used to cost all five texels before the falloff below
|
|
993
|
+
* said so. Both falloffs are zero at and past the radius, so this skips only lights the
|
|
994
|
+
* \`continue\` after the falloff would have skipped anyway: 17.5 ms of a 4K main pass with
|
|
995
|
+
* 320 lamps was 12.3 with this and the profile guard below, byte for byte the same picture.
|
|
996
|
+
*/
|
|
997
|
+
if (length(lightPos - vWorldPos) >= lightRadius) continue;
|
|
998
|
+
/*
|
|
999
|
+
* **The second texel, and for a plain point light the last one.** The emitter size and the
|
|
1000
|
+
* weight are two halves in one word, which is how nine numbers fit in two texels;
|
|
1001
|
+
* \`unpackHalf2x16\` widens them exactly, and the fixed arm reads the same two values rounded
|
|
1002
|
+
* the same way, so the arms stay one picture. See \`packSizeAndWeight\`.
|
|
1003
|
+
*/
|
|
1004
|
+
uvec4 rec1 = clusterTexel(record + 1);
|
|
991
1005
|
lightColor = vec3(
|
|
992
1006
|
uintBitsToFloat(rec1.x), uintBitsToFloat(rec1.y), uintBitsToFloat(rec1.z));
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
1007
|
+
vec2 sizeAndWeight = unpackHalf2x16(rec1.w);
|
|
1008
|
+
lightSourceRadius = abs(sizeAndWeight.x);
|
|
1009
|
+
lightWeight = sizeAndWeight.y;
|
|
996
1010
|
/*
|
|
997
|
-
* The
|
|
998
|
-
*
|
|
999
|
-
*
|
|
1011
|
+
* **The fixture, read only for a light that has one.** What a plain light's record holds in
|
|
1012
|
+
* the last three texels is exactly these values, so a light carrying no shadow slot, cone,
|
|
1013
|
+
* profile or cookie shades the same whether they are read or assumed, and at heavy overlap
|
|
1014
|
+
* reading them was a third of the pass. A branch on a value from a texture around
|
|
1015
|
+
* \`texelFetch\` is not the 2026-08-07 case: an integer fetch takes no derivative.
|
|
1016
|
+
*
|
|
1017
|
+
* **Inside a branch on \`uClustered\` first**, which is 2 only in a frame where some light
|
|
1018
|
+
* carries a fixture. The test on each light's flag is a branch on a value read from a
|
|
1019
|
+
* texture, which a compiler may run both sides of, and it cost 1.3 ms of a 4K pass whose 320
|
|
1020
|
+
* lamps were all plain; a uniform branch costs nothing, so such a frame never reaches it.
|
|
1000
1021
|
*/
|
|
1001
|
-
|
|
1002
|
-
lightDir = vec3(
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1022
|
+
shadowSlot = -1;
|
|
1023
|
+
lightDir = vec3(0.0);
|
|
1024
|
+
lightCone = vec2(-1.0, -2.0);
|
|
1025
|
+
iesProfile = -1.0;
|
|
1026
|
+
iesAxis = vec3(0.0);
|
|
1027
|
+
cookie = -1.0;
|
|
1028
|
+
if (uClustered == 2) {
|
|
1029
|
+
if ((rec1.w & LIGHT_FIXTURE_FLAG) != 0u) {
|
|
1030
|
+
uvec4 rec2 = clusterTexel(record + 2);
|
|
1031
|
+
uvec4 rec3 = clusterTexel(record + 3);
|
|
1032
|
+
uvec4 rec4 = clusterTexel(record + 4);
|
|
1033
|
+
shadowSlot = int(uintBitsToFloat(rec2.x));
|
|
1034
|
+
lightDir = vec3(
|
|
1035
|
+
uintBitsToFloat(rec2.y), uintBitsToFloat(rec2.z), uintBitsToFloat(rec2.w));
|
|
1036
|
+
lightCone = vec2(uintBitsToFloat(rec3.x), uintBitsToFloat(rec3.y));
|
|
1037
|
+
iesProfile = uintBitsToFloat(rec3.z);
|
|
1038
|
+
cookie = uintBitsToFloat(rec3.w);
|
|
1039
|
+
/* The photometric azimuth. See LIGHT_RECORD in clusteredLights.ts for why it costs a
|
|
1040
|
+
texel rather than being derived from the direction. */
|
|
1041
|
+
iesAxis = vec3(
|
|
1042
|
+
uintBitsToFloat(rec4.x), uintBitsToFloat(rec4.y), uintBitsToFloat(rec4.z));
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1012
1045
|
} else {
|
|
1013
1046
|
i = slot;
|
|
1014
1047
|
if (i >= uLightCount || i >= MAX_LIGHTS) break;
|
|
@@ -1227,20 +1260,25 @@ export const MAIN_GLSL = `void main() {
|
|
|
1227
1260
|
* and the result is selected afterwards with a \`mix\` on a compare — which is arithmetic
|
|
1228
1261
|
* rather than control flow.
|
|
1229
1262
|
*
|
|
1230
|
-
* **
|
|
1231
|
-
*
|
|
1232
|
-
*
|
|
1233
|
-
*
|
|
1234
|
-
*
|
|
1263
|
+
* **And skipped outright where nothing loaded a profile**, behind \`uIesAtlasRows\`, which is
|
|
1264
|
+
* one while the atlas holds only its row of ones. A uniform is provably uniform control
|
|
1265
|
+
* flow, so this branch is not the one the rule forbids. It was paid on every scene until a
|
|
1266
|
+
* 4K frame of 320 lamps and no shadows measured it at 0.7 ms of the main pass, rising to
|
|
1267
|
+
* 2.3 ms with the lamps overlapping, which is the scene this note used to say would make
|
|
1268
|
+
* the fetch wrong. Every light then reads 1, which is what \`step(0.0, iesProfile)\` gave
|
|
1269
|
+
* each of them anyway.
|
|
1235
1270
|
*/
|
|
1236
1271
|
float iesRow = max(iesProfile, 0.0);
|
|
1237
1272
|
/* The direction the fixture throws toward this fragment, which both angles are measured on. */
|
|
1238
1273
|
vec3 iesRay = -toLight / max(dist, 1e-4);
|
|
1239
1274
|
/* The angle from the fixture's axis, as a fraction of the 0-to-180 arc every row spans. */
|
|
1240
|
-
float
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1275
|
+
float iesU = 0.0;
|
|
1276
|
+
if (uIesAtlasRows > 1.0) {
|
|
1277
|
+
float iesAngle = acos(clamp(dot(iesRay, lightDir), -1.0, 1.0));
|
|
1278
|
+
/* Clamped to the row's first and last texel centres, which is what the edge clamp of a
|
|
1279
|
+
texture exactly one row wide gave; the atlas can be wider now, with cookies beside it. */
|
|
1280
|
+
iesU = clamp(iesAngle / PI_IES * IES_WIDTH, 0.5, IES_WIDTH - 0.5) / fixtureSize.x;
|
|
1281
|
+
}
|
|
1244
1282
|
/*
|
|
1245
1283
|
* **The azimuth about the fixture's own axis**, behind a branch on a uniform so a scene whose
|
|
1246
1284
|
* profiles are all axially symmetric pays none of this arithmetic. A \`uniform float\` is
|
|
@@ -1315,14 +1353,15 @@ export const MAIN_GLSL = `void main() {
|
|
|
1315
1353
|
* version this replaced took two explicit fetches and a \`mix\`, and cost **18,315 gzipped
|
|
1316
1354
|
* bytes on \`core-only\`** across the sixteen fragment permutations.
|
|
1317
1355
|
*/
|
|
1318
|
-
float
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1356
|
+
float photometric = 1.0;
|
|
1357
|
+
if (uIesAtlasRows > 1.0) {
|
|
1358
|
+
float iesBase = iesRow * (uIesPlaneCount + 1.0);
|
|
1359
|
+
/* The profiles are the atlas's bottom rows, below any cookies. */
|
|
1360
|
+
float iesTop = fixtureSize.y - uIesAtlasRows;
|
|
1361
|
+
float iesV = (iesTop + iesBase + iesTurns * uIesPlaneCount + 0.5) / fixtureSize.y;
|
|
1362
|
+
float iesGain = textureLod(uFixtureAtlas, vec2(iesU, iesV), 0.0).r;
|
|
1363
|
+
photometric = mix(1.0, iesGain, step(0.0, iesProfile));
|
|
1364
|
+
}
|
|
1326
1365
|
|
|
1327
1366
|
float shape = (uLightFalloff == 1 ? falloff : falloff * falloff) * coneFalloff * photometric;
|
|
1328
1367
|
glassGlow += lightColor * backNdl * shape * lightWeight * shaded * lampGlass;
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
CLUSTER_X,
|
|
7
7
|
CLUSTER_Y,
|
|
8
8
|
CLUSTER_Z,
|
|
9
|
+
LIGHT_FIXTURE_FLAG,
|
|
9
10
|
LIGHT_REGION_TEXELS,
|
|
10
11
|
LIGHT_TEXELS,
|
|
11
12
|
MAX_LIGHTS_PER_CLUSTER,
|
|
@@ -659,6 +660,7 @@ uniform float uHighlightGain;
|
|
|
659
660
|
#define CLUSTER_TEXELS ${CLUSTER_TEXELS}
|
|
660
661
|
#define MAX_LIGHTS_PER_CLUSTER ${MAX_LIGHTS_PER_CLUSTER}
|
|
661
662
|
#define LIGHT_TEXELS ${LIGHT_TEXELS}
|
|
663
|
+
#define LIGHT_FIXTURE_FLAG ${LIGHT_FIXTURE_FLAG}u
|
|
662
664
|
#define LIGHT_REGION_TEXELS ${LIGHT_REGION_TEXELS}
|
|
663
665
|
#define CLUSTER_TABLE_WIDTH ${TABLE_WIDTH}
|
|
664
666
|
uniform highp usampler2D uClusterTable; // wgsl:share shadow
|