@driftengine/core 4.1.4 → 4.2.0
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 +3812 -3770
- package/dist/index.d.ts +1 -1
- package/dist/index.js +7 -1
- package/dist/render/backend/webgl2/renderer.d.ts +15 -0
- package/dist/render/backend/webgl2/renderer.js +28 -7
- package/dist/render/backend/webgpu/pointShadowPass.d.ts +4 -0
- package/dist/render/backend/webgpu/pointShadowPass.js +10 -0
- package/dist/render/backend/webgpu/renderer.js +11 -6
- package/dist/render/depthConvention.d.ts +19 -0
- package/dist/render/depthConvention.js +21 -0
- package/dist/render/livePointShadowSet.js +8 -0
- package/dist/render/pointShadowBudget.js +59 -5
- package/dist/render/pointShadowImage.d.ts +40 -0
- package/dist/render/pointShadowImage.js +20 -0
- package/dist/render/pointShadowMap.d.ts +4 -0
- package/dist/render/pointShadowMap.js +10 -0
- package/dist/render/pointShadowSystem.js +9 -0
- package/dist/render/renderQuality.d.ts +27 -4
- package/dist/render/renderQuality.js +1 -1
- package/dist/render/shaders/flat/main.d.ts +1 -1
- package/dist/render/shaders/flat/main.js +13 -5
- package/dist/render/shaders/flat/pointShadow.d.ts +1 -1
- package/dist/render/shaders/flat/pointShadow.js +42 -30
- package/dist/render/shaders/flat/preamble.js +26 -2
- package/dist/render/shaders/generated/flat.wgsl.d.ts +16 -16
- package/dist/render/shaders/generated/flat.wgsl.js +24 -24
- package/dist/render/shaders/sky.d.ts +10 -6
- package/dist/render/shaders/sky.js +35 -2
- package/dist/render/textRenderer.js +23 -4
- package/package.json +6 -6
- package/src/index.ts +11 -1
- package/src/render/backend/webgl2/renderer.ts +28 -7
- package/src/render/backend/webgpu/pointShadowPass.ts +13 -0
- package/src/render/backend/webgpu/renderer.ts +16 -10
- package/src/render/depthConvention.ts +22 -0
- package/src/render/livePointShadowSet.ts +7 -0
- package/src/render/pointShadowBudget.ts +58 -5
- package/src/render/pointShadowImage.ts +51 -0
- package/src/render/pointShadowMap.ts +13 -0
- package/src/render/pointShadowSystem.ts +8 -0
- package/src/render/renderQuality.ts +28 -5
- package/src/render/shaders/flat/main.ts +13 -5
- package/src/render/shaders/flat/pointShadow.ts +42 -30
- package/src/render/shaders/flat/preamble.ts +26 -2
- package/src/render/shaders/generated/flat.wgsl.ts +24 -24
- package/src/render/shaders/sky.ts +37 -2
- package/src/render/textRenderer.ts +23 -14
|
@@ -162,10 +162,33 @@ export interface RenderQuality {
|
|
|
162
162
|
* sampled lights flickering at once accounted for 174 of the frame's 296 draw
|
|
163
163
|
* calls, and for the game being unplayable exactly where lamps cluster.
|
|
164
164
|
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
165
|
+
* **It was 0.4 until 4.1.5, chosen to clear a flame's whole wander with room to spare** — so
|
|
166
|
+
* a flame never re-baked at all, and its shadow of the static world stood still while the live
|
|
167
|
+
* map under the same light, which is re-rendered every frame because it holds the movers, swung
|
|
168
|
+
* with the flame. One light, two shadows, visibly disagreeing about where it was. Reported as
|
|
169
|
+
* the runner's shadow following the fire and the brazier's not.
|
|
170
|
+
*
|
|
171
|
+
* **A millimetre instead, which is as near to "whenever it moved" as makes sense**, so a shadow
|
|
172
|
+
* travels with its light rather than freezing between re-bakes. A centimetre was tried first and
|
|
173
|
+
* left a visible vibration: a light wandering on a curve moves less than that in a frame near
|
|
174
|
+
* the turning points of its travel, so the shadow ran smoothly through the fast part and stepped
|
|
175
|
+
* through the slow part. What the tolerance is still for is not re-baking on arithmetic noise.
|
|
176
|
+
*
|
|
177
|
+
* A light that does not move never goes stale at any positive value, so the whole cost falls on
|
|
178
|
+
* the lights that need it, and it is bounded rather than proportional: one such light takes a
|
|
179
|
+
* whole cube in a frame and the rest dribble under the ordinary face budget.
|
|
180
|
+
*
|
|
181
|
+
* **What makes that affordable is not this number.** The 174-of-296 draw calls this paragraph
|
|
182
|
+
* used to cite was measured before `pointShadowFacesPerFrame` capped the whole static bake, and
|
|
183
|
+
* before `planPointShadowBakes` learned to put a chronically stale map behind every cold and
|
|
184
|
+
* settling one. A flame now spends leftover budget and can never hold another light's first
|
|
185
|
+
* image hostage. One such light also finishes its cube inside a single frame rather than
|
|
186
|
+
* dribbling it over three, which is what makes the motion smooth instead of stepped; see
|
|
187
|
+
* `pointShadowBudget.ts`.
|
|
188
|
+
*
|
|
189
|
+
* **What would make it wrong** is a caster set heavy enough that six passes over it does not
|
|
190
|
+
* fit the frame, where a stepped shadow is the better trade and this should go back up.
|
|
191
|
+
* `gpuTiming` is how to tell which, and it is a number rather than an opinion.
|
|
169
192
|
*/
|
|
170
193
|
readonly pointShadowRebakeDistance: number;
|
|
171
194
|
/**
|
|
@@ -815,7 +838,7 @@ export const DEFAULT_RENDER_QUALITY: Readonly<RenderQuality> = Object.freeze({
|
|
|
815
838
|
directionalShadowDepthLayers: 2,
|
|
816
839
|
directionalShadowMaxDistance: 6,
|
|
817
840
|
directionalShadowMaxSlope: 3,
|
|
818
|
-
pointShadowRebakeDistance: 0.
|
|
841
|
+
pointShadowRebakeDistance: 0.001,
|
|
819
842
|
pointShadowFacesPerFrame: 2,
|
|
820
843
|
/*
|
|
821
844
|
* Twelve, which is `LIVE_POINT_SHADOW_MAPS` cubemaps of six faces: exactly what the live
|
|
@@ -990,7 +990,7 @@ export const MAIN_GLSL = `void main() {
|
|
|
990
990
|
#if POINT_SHADOWS
|
|
991
991
|
float grazing = 1.0 - ndl;
|
|
992
992
|
float normalOffset = (0.06 + dist * 0.05) * grazing * grazing + 0.02;
|
|
993
|
-
vec3
|
|
993
|
+
vec3 receiverPoint = vWorldPos + n * normalOffset;
|
|
994
994
|
|
|
995
995
|
/*
|
|
996
996
|
* uPointShadowWeight is how present the cubemap is, and it is the difference
|
|
@@ -1031,8 +1031,15 @@ export const MAIN_GLSL = `void main() {
|
|
|
1031
1031
|
pointShadow(
|
|
1032
1032
|
uPointShadows,
|
|
1033
1033
|
float(layer),
|
|
1034
|
-
|
|
1035
|
-
|
|
1034
|
+
/*
|
|
1035
|
+
* **From where the map was baked, not from where the light is.** The two are apart
|
|
1036
|
+
* by up to \`pointShadowRebakeDistance\` for anything that wanders, and both things
|
|
1037
|
+
* this asks the picture move with that gap: the distance it compares against the
|
|
1038
|
+
* stored one, and which texel the single-tap blocker search lands in. See
|
|
1039
|
+
* \`uPointShadowOrigin\`.
|
|
1040
|
+
*/
|
|
1041
|
+
receiverPoint - uPointShadowProjection[shadowRead].xyz,
|
|
1042
|
+
uPointShadowProjection[shadowRead].w,
|
|
1036
1043
|
uPointShadowNear[shadowRead],
|
|
1037
1044
|
uPointShadowSize[shadowRead]
|
|
1038
1045
|
),
|
|
@@ -1051,8 +1058,9 @@ export const MAIN_GLSL = `void main() {
|
|
|
1051
1058
|
liveOccl = pointShadow(
|
|
1052
1059
|
uPointShadows,
|
|
1053
1060
|
float(liveLayer),
|
|
1054
|
-
|
|
1055
|
-
|
|
1061
|
+
/* Its own bake origin, for the reason the static layer above gives. */
|
|
1062
|
+
receiverPoint - uLivePointShadowProjection[shadowRead].xyz,
|
|
1063
|
+
uLivePointShadowProjection[shadowRead].w,
|
|
1056
1064
|
uLivePointShadowNear[shadowRead],
|
|
1057
1065
|
uLivePointShadowSize[shadowRead]
|
|
1058
1066
|
);
|
|
@@ -15,7 +15,40 @@
|
|
|
15
15
|
import { MAX_SHADOW_FILTER_TAPS } from '../../renderQuality.ts';
|
|
16
16
|
import { OUTPUT_TRANSFORM_GLSL } from '../outputTransform.ts';
|
|
17
17
|
|
|
18
|
-
export const POINTSHADOW_GLSL =
|
|
18
|
+
export const POINTSHADOW_GLSL = `/**
|
|
19
|
+
* How much of one tap's occlusion survives its own penumbra.
|
|
20
|
+
*
|
|
21
|
+
* **How much of a shadow survives, as well as how soft it is.** A penumbra does not merely blur
|
|
22
|
+
* with distance from its caster, it *takes over*: far enough from the occluder, relative to how
|
|
23
|
+
* big the source is, every point on the receiver can see part of the flame and there is no umbra
|
|
24
|
+
* left at all. The shadow is gone, not blurry. Without it, two columns lit by a brazier threw
|
|
25
|
+
* full-strength edges fifteen metres across the terrace, and the gap between them read as a
|
|
26
|
+
* spotlight cone with straight sides rather than as a fire.
|
|
27
|
+
*
|
|
28
|
+
* **Per tap, and it was once for the whole filter, taken from the centre tap alone.** That is a
|
|
29
|
+
* blocker search of one sample standing in for twelve, and it is right wherever the taps agree —
|
|
30
|
+
* inside an umbra they all find the same occluder, so this returns exactly what the shared value
|
|
31
|
+
* did, and the umbra is unchanged to the bit. It is wrong exactly where they disagree, which is
|
|
32
|
+
* the silhouette: there the centre ray *misses*, so the search reports no occluder, the spread is
|
|
33
|
+
* zero and the fade is switched off — while the taps that do land on the caster then draw at full
|
|
34
|
+
* strength. What that paints is a hard one-texel line around a shadow whose interior has
|
|
35
|
+
* correctly faded to nothing.
|
|
36
|
+
*
|
|
37
|
+
* Reported as shadows "faded out but NOT shadow outlines, generating long lines around the
|
|
38
|
+
* world", and on a brazier as a bare square outline lying on the ground with no shadow inside it.
|
|
39
|
+
*
|
|
40
|
+
* **What it costs** is two multiplies and a divide per occluded tap, against a tap that has
|
|
41
|
+
* already paid for a texture fetch. **What would make it wrong** is a filter wide enough that one
|
|
42
|
+
* tap's occluder is not the occluder a neighbouring tap is estimating for, which is the same
|
|
43
|
+
* limit the shared value had and is what \`MAX_FILTER_RADIUS\` bounds.
|
|
44
|
+
*/
|
|
45
|
+
float tapStrength(float receiverDistance, float occluderDistance, float sourceRadius) {
|
|
46
|
+
float spread = max(receiverDistance - occluderDistance, 0.0) / max(occluderDistance, 0.05);
|
|
47
|
+
float penumbra = clamp(sourceRadius * spread * PENUMBRA_FADE, 0.0, 1.0);
|
|
48
|
+
return 1.0 - penumbra * penumbra;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
float pointShadow(
|
|
19
52
|
highp sampler2DArray maps,
|
|
20
53
|
float layer,
|
|
21
54
|
vec3 toFrag,
|
|
@@ -97,24 +130,6 @@ export const POINTSHADOW_GLSL = `float pointShadow(
|
|
|
97
130
|
*/
|
|
98
131
|
float radius = clamp(sourceRadius * spread, 0.01, MAX_FILTER_RADIUS) + 0.012;
|
|
99
132
|
|
|
100
|
-
/*
|
|
101
|
-
* How much of a shadow survives, as well as how soft it is.
|
|
102
|
-
*
|
|
103
|
-
* This is the half that was missing, and it is why widening the filter never
|
|
104
|
-
* fixed anything. A penumbra does not merely blur with distance from its
|
|
105
|
-
* caster — it *takes over*. Far enough from the occluder, relative to how big
|
|
106
|
-
* the source is, every point on the receiver can see part of the flame and
|
|
107
|
-
* there is no umbra left at all: the shadow is gone, not blurry.
|
|
108
|
-
*
|
|
109
|
-
* Without it, two columns lit by a brazier threw full-strength edges fifteen
|
|
110
|
-
* metres across the terrace, and the gap between them read as a spotlight cone
|
|
111
|
-
* with straight sides rather than as a fire. Three attempts in, the note that
|
|
112
|
-
* unlocked it was that the pool should still fade naturally at its rim — a
|
|
113
|
-
* description of the falloff, not of the blur.
|
|
114
|
-
*/
|
|
115
|
-
float penumbra = clamp(sourceRadius * spread * PENUMBRA_FADE, 0.0, 1.0);
|
|
116
|
-
float strength = 1.0 - penumbra * penumbra;
|
|
117
|
-
|
|
118
133
|
/*
|
|
119
134
|
* Taps are placed on a disk *perpendicular to the light ray*, turned by half a golden
|
|
120
135
|
* angle on every other column of pixels.
|
|
@@ -191,14 +206,13 @@ export const POINTSHADOW_GLSL = `float pointShadow(
|
|
|
191
206
|
if (dist - bias > storedDistance) {
|
|
192
207
|
// A point shadow cannot outlive the finite light that casts it, so its
|
|
193
208
|
// occlusion fades smoothly before the light's far plane.
|
|
194
|
-
|
|
209
|
+
float reach = shadowReach(max(dist - storedDistance, 0.0), far);
|
|
210
|
+
lit += 1.0 - reach * tapStrength(dist, storedDistance, sourceRadius);
|
|
195
211
|
} else {
|
|
196
212
|
lit += 1.0;
|
|
197
213
|
}
|
|
198
214
|
}
|
|
199
|
-
|
|
200
|
-
// Fade the whole shadow toward lit as the penumbra swallows it.
|
|
201
|
-
return mix(1.0, shaded, strength);
|
|
215
|
+
return lit / float(max(uShadowFilterTaps, 1));
|
|
202
216
|
}
|
|
203
217
|
|
|
204
218
|
/**
|
|
@@ -289,10 +303,6 @@ float areaShadow(
|
|
|
289
303
|
float widthX = min(halfX * spread, MAX_FILTER_RADIUS) + 0.012;
|
|
290
304
|
float widthY = min(halfY * spread, MAX_FILTER_RADIUS) + 0.012;
|
|
291
305
|
|
|
292
|
-
/* The narrower half extent, for the reason in the header: it is what keeps a strip's umbra. */
|
|
293
|
-
float penumbra = clamp(min(halfX, halfY) * spread * PENUMBRA_FADE, 0.0, 1.0);
|
|
294
|
-
float strength = 1.0 - penumbra * penumbra;
|
|
295
|
-
|
|
296
306
|
/*
|
|
297
307
|
* Turned inside the unit disk *before* the ellipse maps it, so the half-turn that separates
|
|
298
308
|
* the two halves of a pixel pair survives the anisotropy. The same two-pixel tile the round
|
|
@@ -318,13 +328,15 @@ float areaShadow(
|
|
|
318
328
|
float storedDistance = stored * far;
|
|
319
329
|
if (dist - bias > storedDistance) {
|
|
320
330
|
/* An area light's occlusion cannot outlive the map that carries it either. */
|
|
321
|
-
|
|
331
|
+
float reach = shadowReach(max(dist - storedDistance, 0.0), far);
|
|
332
|
+
/* The narrower half extent, for the reason in the header: it is what keeps a strip's
|
|
333
|
+
umbra. Per tap, for the reason \`tapStrength\` gives. */
|
|
334
|
+
lit += 1.0 - reach * tapStrength(dist, storedDistance, min(halfX, halfY));
|
|
322
335
|
} else {
|
|
323
336
|
lit += 1.0;
|
|
324
337
|
}
|
|
325
338
|
}
|
|
326
|
-
|
|
327
|
-
return mix(1.0, shaded, strength);
|
|
339
|
+
return lit / float(max(uShadowFilterTaps, 1));
|
|
328
340
|
}
|
|
329
341
|
#endif
|
|
330
342
|
|
|
@@ -803,7 +803,30 @@ uniform float uAreaLightTwoSided[MAX_AREA_LIGHTS];
|
|
|
803
803
|
#if POINT_SHADOWS
|
|
804
804
|
/** Which array layer holds this light's shadow, or -1 for none. */
|
|
805
805
|
uniform int uPointShadowLayer[MAX_LIGHTS];
|
|
806
|
-
|
|
806
|
+
/*
|
|
807
|
+
* **The projection each map was rendered under: \`xyz\` where from, \`w\` how far.**
|
|
808
|
+
*
|
|
809
|
+
* The origin is the half that is new and it is **not** the light's position. A light may drift
|
|
810
|
+
* \`pointShadowRebakeDistance\` from its image before the image is re-rendered, because
|
|
811
|
+
* re-baking six faces of the static world on every frame of a flicker is what made a brazier
|
|
812
|
+
* the most expensive object in a scene. The ray below is built from this point rather than from
|
|
813
|
+
* \`uLightPos\`, so it is the ray the picture was drawn along and the drift costs nothing.
|
|
814
|
+
*
|
|
815
|
+
* **It rides with the far plane because a row is a row.** A default-block \`float[N]\` spends a
|
|
816
|
+
* whole uniform vector per element and uses one of its four components, so the far plane was
|
|
817
|
+
* already paying for three floats it threw away — see \`uniformVectorBudget.ts\`. Declared as a
|
|
818
|
+
* separate \`vec3\` array this would have cost sixteen more rows on each of the two sets, and
|
|
819
|
+
* the ladder answers a 256-vector phone by halving \`MAX_LIGHTS\`: the measured price of a
|
|
820
|
+
* separate array was **eight lights becoming four**. Folded in, the whole fix is free.
|
|
821
|
+
*
|
|
822
|
+
* **What it gives up** is that the shadow stops tracking the flame *within* the tolerance — a
|
|
823
|
+
* flicker no longer nudges the shadow it casts, because the image it is read from did not move
|
|
824
|
+
* either. That is the trade the tolerance was always making, now made honestly instead of by
|
|
825
|
+
* reading a picture from the wrong place. **What would make it wrong** is a tolerance large
|
|
826
|
+
* enough that a shadow visibly lags its light, which is a reason to lower
|
|
827
|
+
* \`pointShadowRebakeDistance\` rather than to sample from a point the image cannot answer for.
|
|
828
|
+
*/
|
|
829
|
+
uniform vec4 uPointShadowProjection[MAX_LIGHTS];
|
|
807
830
|
uniform float uPointShadowNear[MAX_LIGHTS];
|
|
808
831
|
/** Emitter radius per light — drives how soft its shadows are. */
|
|
809
832
|
uniform float uPointShadowSize[MAX_LIGHTS];
|
|
@@ -834,7 +857,8 @@ uniform highp sampler2DArray uPointShadows; // wgsl:share shadow
|
|
|
834
857
|
* light owns at most one of the two at a time.
|
|
835
858
|
*/
|
|
836
859
|
uniform int uLivePointShadowLayer[MAX_LIGHTS];
|
|
837
|
-
|
|
860
|
+
/** The live pair's own projection, by the same rule. See \`uPointShadowProjection\`. */
|
|
861
|
+
uniform vec4 uLivePointShadowProjection[MAX_LIGHTS];
|
|
838
862
|
uniform float uLivePointShadowNear[MAX_LIGHTS];
|
|
839
863
|
uniform float uLivePointShadowSize[MAX_LIGHTS];
|
|
840
864
|
uniform float uLivePointShadowWeight[MAX_LIGHTS];
|