@driftengine/core 4.1.2 → 4.1.4
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 +52 -0
- package/README.md +1 -1
- package/dist/index.js +12 -2
- package/dist/render/backend/webgpu/gpuDrivenPass.d.ts +62 -0
- package/dist/render/backend/webgpu/gpuDrivenPass.js +192 -45
- package/dist/render/backend/webgpu/renderer.js +10 -2
- package/dist/render/shaders/flat/main.d.ts +1 -1
- package/dist/render/shaders/flat/main.js +91 -5
- package/dist/render/shaders/flat/pointShadow.d.ts +1 -1
- package/dist/render/shaders/flat/pointShadow.js +42 -16
- package/dist/render/shaders/flat/preamble.js +44 -7
- package/dist/render/shaders/generated/flat.wgsl.js +16 -16
- package/dist/render/textLayout.d.ts +58 -0
- package/dist/render/textLayout.js +83 -0
- package/dist/render/textRenderer.js +7 -3
- package/package.json +6 -6
- package/src/index.ts +12 -2
- package/src/render/backend/webgpu/gpuDrivenPass.ts +195 -47
- package/src/render/backend/webgpu/renderer.ts +14 -2
- package/src/render/shaders/flat/main.ts +91 -5
- package/src/render/shaders/flat/pointShadow.ts +42 -16
- package/src/render/shaders/flat/preamble.ts +44 -7
- package/src/render/shaders/generated/flat.wgsl.ts +16 -16
- package/src/render/textLayout.ts +88 -0
- package/src/render/textRenderer.ts +22 -3
|
@@ -793,6 +793,27 @@ export const MAIN_GLSL = `void main() {
|
|
|
793
793
|
*/
|
|
794
794
|
float lightShade = 1.0;
|
|
795
795
|
|
|
796
|
+
/*
|
|
797
|
+
* **The lamps' light twice: once as it arrives, once as their shadows leave it.**
|
|
798
|
+
*
|
|
799
|
+
* Every point light's contribution used to go straight into \`lit\`. It is accumulated here
|
|
800
|
+
* instead, in two sums that differ only by the shadow term, so that after the loop there is
|
|
801
|
+
* a single number per channel saying how much of the lamps' light survived — and *that* is
|
|
802
|
+
* the thing the resolve below can average across a pair of pixels without touching albedo,
|
|
803
|
+
* a highlight, or anything else the frame has in it.
|
|
804
|
+
*
|
|
805
|
+
* **The two sums are not an approximation.** \`lampOpen\` multiplied by
|
|
806
|
+
* \`lampShadowed / lampOpen\` is \`lampShadowed\`, per channel, whatever mixture of lamps
|
|
807
|
+
* and shadows produced it. With the resolve switched off this is arithmetically the line it
|
|
808
|
+
* replaces; a lamp that is shadowed and a lamp beside it that is not keep their own shares.
|
|
809
|
+
*
|
|
810
|
+
* **What it costs** is two vector adds per light and six floats of live state. **What would
|
|
811
|
+
* make it wrong** is a term wanting to sit between the lamps and \`lit\` — a per-light
|
|
812
|
+
* tone map, say — because these two are summed before anything can get between them.
|
|
813
|
+
*/
|
|
814
|
+
vec3 lampOpen = vec3(0.0);
|
|
815
|
+
vec3 lampShadowed = vec3(0.0);
|
|
816
|
+
|
|
796
817
|
/*
|
|
797
818
|
* Which froxel this fragment is in.
|
|
798
819
|
*
|
|
@@ -1192,7 +1213,13 @@ export const MAIN_GLSL = `void main() {
|
|
|
1192
1213
|
*
|
|
1193
1214
|
* At metal 0 this is one multiply by 1.0 away from the expression it replaces.
|
|
1194
1215
|
*/
|
|
1195
|
-
|
|
1216
|
+
/*
|
|
1217
|
+
* Held back from \`lit\` until the loop is over, so the shadow can be resolved across a
|
|
1218
|
+
* pair of pixels first. See the block below the loop; nothing else about this term moved.
|
|
1219
|
+
*/
|
|
1220
|
+
vec3 lampDiffuse = albedo * lightColor * ndl * shape * lightWeight * (1.0 - metal);
|
|
1221
|
+
lampOpen += lampDiffuse;
|
|
1222
|
+
lampShadowed += lampDiffuse * shaded;
|
|
1196
1223
|
|
|
1197
1224
|
/*
|
|
1198
1225
|
* A lamp reflected in a polished surface, which point lights could not do at all.
|
|
@@ -1222,8 +1249,12 @@ export const MAIN_GLSL = `void main() {
|
|
|
1222
1249
|
vec3(1.0),
|
|
1223
1250
|
pow(1.0 - max(dot(toEyeLamp, lampHalfway), 0.0), 5.0) * metal
|
|
1224
1251
|
);
|
|
1225
|
-
|
|
1226
|
-
|
|
1252
|
+
/* Held back with the diffuse half, and shadowed by the same term it always was. */
|
|
1253
|
+
vec3 lampHighlight =
|
|
1254
|
+
lightColor * sphereLobe(max(dot(n, lampHalfway), 0.0), surfaceRoughness, lightSourceRadius, dist)
|
|
1255
|
+
* lampSpec * shape * lightWeight;
|
|
1256
|
+
lampOpen += lampHighlight;
|
|
1257
|
+
lampShadowed += lampHighlight * shaded;
|
|
1227
1258
|
}
|
|
1228
1259
|
/*
|
|
1229
1260
|
* The most any light in range shadows this point, for the emissive term below —
|
|
@@ -1371,7 +1402,10 @@ export const MAIN_GLSL = `void main() {
|
|
|
1371
1402
|
}
|
|
1372
1403
|
#endif
|
|
1373
1404
|
|
|
1374
|
-
|
|
1405
|
+
/* Held back with the point lamps', and shadowed by the same term it always was. */
|
|
1406
|
+
vec3 areaDiffuse = albedo * uAreaLightColor[a] * form * (1.0 - metal);
|
|
1407
|
+
lampOpen += areaDiffuse;
|
|
1408
|
+
lampShadowed += areaDiffuse * areaOccl;
|
|
1375
1409
|
|
|
1376
1410
|
if (vSpecular > 0.0 || metal > 0.0) {
|
|
1377
1411
|
/*
|
|
@@ -1396,11 +1430,63 @@ export const MAIN_GLSL = `void main() {
|
|
|
1396
1430
|
if (coverage > 0.0) {
|
|
1397
1431
|
vec2 areaDfg = envBrdfApprox(max(dot(n, toEyeArea), 0.0), surfaceRoughness);
|
|
1398
1432
|
vec3 areaSpec = specColor * areaDfg.x + vec3(areaDfg.y);
|
|
1399
|
-
|
|
1433
|
+
vec3 areaHighlight = areaSpec * uAreaLightColor[a] * coverage;
|
|
1434
|
+
lampOpen += areaHighlight;
|
|
1435
|
+
lampShadowed += areaHighlight * areaOccl;
|
|
1400
1436
|
}
|
|
1401
1437
|
}
|
|
1402
1438
|
}
|
|
1403
1439
|
|
|
1440
|
+
/*
|
|
1441
|
+
* **The other half of the shadow filter: the two pixels of a column pair, folded together.**
|
|
1442
|
+
*
|
|
1443
|
+
* \`pointShadow\` turns its twelve taps by half a golden angle on every other column, so a
|
|
1444
|
+
* pair of neighbouring pixels holds twenty-four places on the disk between them and neither
|
|
1445
|
+
* holds more than twelve. This is where the pair becomes one filter. \`dFdx\` of a value is
|
|
1446
|
+
* the difference across the pair, and half of it, signed by which of the two this pixel is,
|
|
1447
|
+
* is exactly the average of both — so each pixel ends up shaded by all twenty-four taps
|
|
1448
|
+
* while paying for twelve. It is the pairing \`ambientOcclusion.ts\` describes between its
|
|
1449
|
+
* rotation tile and its blur, at the smallest size that needs no second pass.
|
|
1450
|
+
*
|
|
1451
|
+
* **After both lamp loops rather than beside the taps, and that is why this block exists at
|
|
1452
|
+
* all.** A derivative is only defined where every pixel of the quad reaches it, and neither
|
|
1453
|
+
* loop above reaches anything uniformly: the point loop \`continue\`s on falloff and on
|
|
1454
|
+
* facing, and in the clustered arm its trip count is read from a froxel, so two neighbouring
|
|
1455
|
+
* pixels can be shading different lights with a froxel boundary drawn across the screen
|
|
1456
|
+
* where they stop agreeing. A \`dFdx\` in there is the 2026-08-07 rule's own case, and it
|
|
1457
|
+
* would also invite a compiler to flatten the loop. Measured, too: with the turn taken from
|
|
1458
|
+
* a per-fragment hash instead of the column, the same resolve made the penumbra *worse*
|
|
1459
|
+
* than no resolve at all — 3.14 against 3.00 — because the hardware here answers a coarse
|
|
1460
|
+
* derivative and the second row of a quad then borrows the first row's difference. The
|
|
1461
|
+
* column tile is what makes that borrowing correct, so the two halves are one mechanism.
|
|
1462
|
+
*
|
|
1463
|
+
* Both lamp kinds are in the sums, because a rectangle's shadow is filtered by the same
|
|
1464
|
+
* twelve taps and stippled the same way.
|
|
1465
|
+
*
|
|
1466
|
+
* **Clamped, and the clamp is a guard rather than tidying.** A fragment killed by the
|
|
1467
|
+
* cutout or the clip plane above leaves its lane without a value for its neighbour to
|
|
1468
|
+
* difference against, and GLSL ES says nothing about what comes back. What could reach the
|
|
1469
|
+
* frame is a shadow term outside 0..1 on the pixel beside a cutout hole, which the clamp
|
|
1470
|
+
* turns into a pixel that is merely fully lit or fully shadowed. **What would make this
|
|
1471
|
+
* wrong** is a consumer whose cutout edges are most of the frame — dense foliage drawn as
|
|
1472
|
+
* one alpha-tested sheet — where the honest answer is to resolve in a pass of its own,
|
|
1473
|
+
* against a shadow term written to a target.
|
|
1474
|
+
*
|
|
1475
|
+
* The floor under the divisor costs nothing and answers the case where no lamp reached this
|
|
1476
|
+
* pixel at all: both sums are zero there, so the ratio is zero and multiplying it back by
|
|
1477
|
+
* the floor adds nothing.
|
|
1478
|
+
*/
|
|
1479
|
+
vec3 lampOpenFloor = max(lampOpen, vec3(1e-6));
|
|
1480
|
+
vec3 lampShade = lampShadowed / lampOpenFloor;
|
|
1481
|
+
#if POINT_SHADOWS
|
|
1482
|
+
lampShade = clamp(
|
|
1483
|
+
lampShade - dFdx(lampShade) * (float(int(gl_FragCoord.x) & 1) - 0.5),
|
|
1484
|
+
0.0,
|
|
1485
|
+
1.0
|
|
1486
|
+
);
|
|
1487
|
+
#endif
|
|
1488
|
+
lit += lampOpenFloor * lampShade;
|
|
1489
|
+
|
|
1404
1490
|
/*
|
|
1405
1491
|
* Self-illuminated geometry, dimmed where a lamp's shadow crosses it.
|
|
1406
1492
|
*
|
|
@@ -116,8 +116,8 @@ export const POINTSHADOW_GLSL = `float pointShadow(
|
|
|
116
116
|
float strength = 1.0 - penumbra * penumbra;
|
|
117
117
|
|
|
118
118
|
/*
|
|
119
|
-
* Taps are placed on a disk *perpendicular to the light ray*,
|
|
120
|
-
*
|
|
119
|
+
* Taps are placed on a disk *perpendicular to the light ray*, turned by half a golden
|
|
120
|
+
* angle on every other column of pixels.
|
|
121
121
|
*
|
|
122
122
|
* Offsetting the ray by fixed 3D vectors was the earlier approach and it has
|
|
123
123
|
* two faults that only show on a wide filter. The offsets are not
|
|
@@ -127,8 +127,33 @@ export const POINTSHADOW_GLSL = `float pointShadow(
|
|
|
127
127
|
* tap pattern — reported, on a brazier behind a rank of columns, as strange
|
|
128
128
|
* straight lines across the light pool.
|
|
129
129
|
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
130
|
+
* **The cure for that was a per-fragment hash, and it is what made the penumbra
|
|
131
|
+
* stipple.** Twelve taps are a coarse estimate of how much of the emitter this point can
|
|
132
|
+
* see; turning them by an unrelated angle at every pixel makes each pixel's error
|
|
133
|
+
* independent of its neighbour's, which is the definition of noise. Something has to
|
|
134
|
+
* average that back out, and this engine's lit pass is the last thing to touch the frame —
|
|
135
|
+
* there is no blur and no temporal resolve behind it, so the noise reached the screen. Two
|
|
136
|
+
* games reported the same coarse cross-hatch across a light pool.
|
|
137
|
+
*
|
|
138
|
+
* **So the turn comes from a two-pixel tile instead, and the pair is averaged afterwards.**
|
|
139
|
+
* A pixel's own x parity picks one of two tap sets half a golden angle apart, and
|
|
140
|
+
* \`main\` folds the pair together once the light loop is over — see the resolve there. The
|
|
141
|
+
* pair's twenty-four taps are the filter; each pixel pays for twelve of them.
|
|
142
|
+
*
|
|
143
|
+
* **A *column* pair rather than a 2x2 square, and that was measured rather than assumed.**
|
|
144
|
+
* Four turns on a square tile, resolved with both derivatives, came back at 3.88 against this
|
|
145
|
+
* pair's 2.46 on the same build — because the second derivative hands the lower row of a quad
|
|
146
|
+
* the upper row's difference on hardware that answers a coarse derivative, and with four turns
|
|
147
|
+
* that is the wrong difference. Two turns keyed on the column alone make it the right one,
|
|
148
|
+
* which is why the tile's shape and the resolve are one decision rather than two.
|
|
149
|
+
*
|
|
150
|
+
* **Neither half works alone**, which is the same pairing \`ambientOcclusion.ts\` describes
|
|
151
|
+
* between its rotation tile and its blur. Measured on \`demo/dev/pointshadow.html\` at
|
|
152
|
+
* \`?exposure=6\`, as mean absolute luminance difference to the right and down neighbour over
|
|
153
|
+
* the mover's penumbra, on WebGL2: **4.60** for the per-fragment hash this replaces, **2.88**
|
|
154
|
+
* for this tile with no resolve — and a one-pixel vertical comb in the picture, which is the
|
|
155
|
+
* tile itself on screen — against **2.37** for the pair resolved. A frame with the shadow
|
|
156
|
+
* switched off measures 0.81, and WebGPU answers 4.56 and 2.33 for the same two builds.
|
|
132
157
|
*
|
|
133
158
|
* **Unchanged by the move to an octahedral map, and that is the point.** The offsets are
|
|
134
159
|
* angular perturbations of a direction, so every tap is still a direction and octEncode is
|
|
@@ -139,17 +164,19 @@ export const POINTSHADOW_GLSL = `float pointShadow(
|
|
|
139
164
|
vec3 up = abs(fwd.y) < 0.9 ? vec3(0.0, 1.0, 0.0) : vec3(1.0, 0.0, 0.0);
|
|
140
165
|
vec3 tu = normalize(cross(up, fwd));
|
|
141
166
|
vec3 tv = cross(fwd, tu);
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
167
|
+
/*
|
|
168
|
+
* Integer arithmetic, because this is a lattice: a shader compiler is free to implement
|
|
169
|
+
* \`mod(x, 2.0)\` as a multiply by a reciprocal, and a GPU here answers 3 for \`3.0 % 3.0\`.
|
|
170
|
+
* \`gl_FragCoord.x\` is the pixel's centre, so truncating it is the pixel's own column.
|
|
171
|
+
*/
|
|
172
|
+
float angle = float(int(gl_FragCoord.x) & 1) * PCF_PAIR_TURN;
|
|
146
173
|
float ca = cos(angle);
|
|
147
174
|
float sa = sin(angle);
|
|
148
175
|
|
|
149
176
|
float lit = 0.0;
|
|
150
177
|
for (int i = 0; i < ${MAX_SHADOW_FILTER_TAPS}; i++) {
|
|
151
178
|
if (i >= uShadowFilterTaps) break;
|
|
152
|
-
vec2 disk = PCF_OFFSETS[i]
|
|
179
|
+
vec2 disk = PCF_OFFSETS[i];
|
|
153
180
|
vec2 turned = vec2(disk.x * ca - disk.y * sa, disk.x * sa + disk.y * ca);
|
|
154
181
|
// Perpendicular to the ray, and *not* scaled by its length: the offsets are
|
|
155
182
|
// an angular perturbation of a direction, which is what the encoding takes.
|
|
@@ -267,20 +294,19 @@ float areaShadow(
|
|
|
267
294
|
float strength = 1.0 - penumbra * penumbra;
|
|
268
295
|
|
|
269
296
|
/*
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
*
|
|
297
|
+
* Turned inside the unit disk *before* the ellipse maps it, so the half-turn that separates
|
|
298
|
+
* the two halves of a pixel pair survives the anisotropy. The same two-pixel tile the round
|
|
299
|
+
* filter uses and for the same reasons — see \`pointShadow\`, which carries the measurement;
|
|
300
|
+
* a rectangle's shadow stippled exactly as a lamp's did, because it is the same twelve taps.
|
|
274
301
|
*/
|
|
275
|
-
float angle =
|
|
276
|
-
* 6.28318530718;
|
|
302
|
+
float angle = float(int(gl_FragCoord.x) & 1) * PCF_PAIR_TURN;
|
|
277
303
|
float ca = cos(angle);
|
|
278
304
|
float sa = sin(angle);
|
|
279
305
|
|
|
280
306
|
float lit = 0.0;
|
|
281
307
|
for (int i = 0; i < ${MAX_SHADOW_FILTER_TAPS}; i++) {
|
|
282
308
|
if (i >= uShadowFilterTaps) break;
|
|
283
|
-
vec2 disk = PCF_OFFSETS[i]
|
|
309
|
+
vec2 disk = PCF_OFFSETS[i];
|
|
284
310
|
vec2 turned = vec2(disk.x * ca - disk.y * sa, disk.x * sa + disk.y * ca);
|
|
285
311
|
vec3 sampleDir = toFrag + alongX * (turned.x * widthX) + alongY * (turned.y * widthY);
|
|
286
312
|
float stored = textureLod(maps, vec3(octEncode(sampleDir), layer), 0.0).r;
|
|
@@ -883,16 +883,53 @@ uniform float uLiveAreaShadowWeight[MAX_AREA_LIGHTS];
|
|
|
883
883
|
* where a bias means what it looks like it means.
|
|
884
884
|
*/
|
|
885
885
|
/**
|
|
886
|
-
*
|
|
887
|
-
*
|
|
888
|
-
*
|
|
886
|
+
* Twelve places on a disk across the light ray, for filtering an omnidirectional shadow.
|
|
887
|
+
*
|
|
888
|
+
* **These were eight, and the count is why a penumbra was stippled.** The list used to be the
|
|
889
|
+
* eight corners and six faces of a cube, carried as \`vec3\` from the era when a tap offset a
|
|
890
|
+
* direction by a 3D vector. Nothing has read the third component since the map became
|
|
891
|
+
* octahedral — both filters take \`.xy\` — and flattening a cube's corners onto a plane makes
|
|
892
|
+
* pairs of them coincide: \`(1, 1, 1)\` and \`(1, 1, -1)\` are one 2D offset, four times over. So
|
|
893
|
+
* twelve taps asked eight questions and answered four of them twice, and every one of the eight
|
|
894
|
+
* sat on the rim at radius 1 or 1.414 with nothing in between. A coverage estimate from a ring
|
|
895
|
+
* moves in steps of two twelfths as the penumbra's edge sweeps a doubled pair across it, which
|
|
896
|
+
* is the size of the speckle that was reported.
|
|
897
|
+
*
|
|
898
|
+
* **The replacement is the golden-angle disk this repository already uses for ambient
|
|
899
|
+
* occlusion** — angle \`i\` turns of 137.5 degrees, radius \`sqrt((k + 0.5) / 12)\` — so the taps
|
|
900
|
+
* are spread evenly over the area rather than crowded on the rim or at the centre, and no two
|
|
901
|
+
* coincide. The nearest pair is 0.355 apart where the old set had a pair at zero.
|
|
902
|
+
*
|
|
903
|
+
* **The radii are dealt round rather than taken in order, and that is what makes a short filter
|
|
904
|
+
* work.** \`shadowFilterTaps\` is 4, 8 or 12 and the loop below simply stops early, so every
|
|
905
|
+
* prefix of this list is a filter somebody ships: in spiral order the first four taps would be
|
|
906
|
+
* the four innermost and a low profile would quietly get a filter a third of the width it asked
|
|
907
|
+
* for. Interleaved, the first four already span 0.21 to 0.91 of the disk's reach and sit around
|
|
908
|
+
* its centre rather than to one side, which is a small filter rather than a narrow one.
|
|
909
|
+
*
|
|
910
|
+
* **What it gives up** is a little softness. The old set put two thirds of its taps at the
|
|
911
|
+
* outermost radius, so it read the penumbra's widest part twice as often as an even disk does
|
|
912
|
+
* and drew a shadow edge softer than the emitter's size calls for; an even disk is the honest
|
|
913
|
+
* estimate and is very slightly crisper. **What would make it wrong** is a caller wanting the
|
|
914
|
+
* filter's *reach* changed — that is \`MAX_FILTER_RADIUS\`, and the outermost tap here is 1.384
|
|
915
|
+
* against the old 1.414 precisely so this change does not move it.
|
|
889
916
|
*/
|
|
890
917
|
#if POINT_SHADOWS
|
|
891
|
-
const
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
918
|
+
const vec2 PCF_OFFSETS[${MAX_SHADOW_FILTER_TAPS}] = vec2[${MAX_SHADOW_FILTER_TAPS}](
|
|
919
|
+
vec2( 0.289, 0.000), vec2(-0.767, 0.703), vec2( 0.067, -0.761), vec2( 0.766, 0.999),
|
|
920
|
+
vec2(-0.492, -0.087), vec2( 0.943, -0.600), vec2(-0.225, 0.836), vec2(-0.610, -1.174),
|
|
921
|
+
vec2( 0.606, 0.221), vec2(-1.100, 0.454), vec2( 0.406, -0.867), vec2( 0.414, 1.321)
|
|
895
922
|
);
|
|
923
|
+
/**
|
|
924
|
+
* The angle between the two tap sets a pair of neighbouring pixels uses. See \`pointShadow\`.
|
|
925
|
+
*
|
|
926
|
+
* Half the golden angle, because the offsets above are a golden-angle spiral: turning the whole
|
|
927
|
+
* set by half a step drops the second pixel's taps between the first pixel's arms rather than
|
|
928
|
+
* on top of them. Measured on the pair, the closest two of the twenty-four are 0.305 apart,
|
|
929
|
+
* against 0.175 for a half turn and 0.233 for a whole golden angle — so this is the choice that
|
|
930
|
+
* makes the pair's combined filter as even as the twelve it is built from.
|
|
931
|
+
*/
|
|
932
|
+
const float PCF_PAIR_TURN = 1.19998161;
|
|
896
933
|
#endif
|
|
897
934
|
|
|
898
935
|
/** A finite point-light shadow loses strength with the light that casts it. */`;
|