@driftengine/core 4.1.4 → 4.1.5

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.
Files changed (42) hide show
  1. package/CHANGELOG.json +3796 -3770
  2. package/dist/render/backend/webgl2/renderer.d.ts +15 -0
  3. package/dist/render/backend/webgl2/renderer.js +28 -7
  4. package/dist/render/backend/webgpu/pointShadowPass.d.ts +4 -0
  5. package/dist/render/backend/webgpu/pointShadowPass.js +10 -0
  6. package/dist/render/backend/webgpu/renderer.js +7 -2
  7. package/dist/render/depthConvention.d.ts +19 -0
  8. package/dist/render/depthConvention.js +21 -0
  9. package/dist/render/livePointShadowSet.js +8 -0
  10. package/dist/render/pointShadowBudget.js +59 -5
  11. package/dist/render/pointShadowImage.d.ts +40 -0
  12. package/dist/render/pointShadowImage.js +20 -0
  13. package/dist/render/pointShadowMap.d.ts +4 -0
  14. package/dist/render/pointShadowMap.js +10 -0
  15. package/dist/render/pointShadowSystem.js +9 -0
  16. package/dist/render/renderQuality.d.ts +27 -4
  17. package/dist/render/renderQuality.js +1 -1
  18. package/dist/render/shaders/flat/main.d.ts +1 -1
  19. package/dist/render/shaders/flat/main.js +13 -5
  20. package/dist/render/shaders/flat/pointShadow.d.ts +1 -1
  21. package/dist/render/shaders/flat/pointShadow.js +42 -30
  22. package/dist/render/shaders/flat/preamble.js +26 -2
  23. package/dist/render/shaders/generated/flat.wgsl.d.ts +16 -16
  24. package/dist/render/shaders/generated/flat.wgsl.js +24 -24
  25. package/dist/render/shaders/sky.d.ts +10 -6
  26. package/dist/render/shaders/sky.js +35 -2
  27. package/package.json +6 -6
  28. package/src/render/backend/webgl2/renderer.ts +28 -7
  29. package/src/render/backend/webgpu/pointShadowPass.ts +13 -0
  30. package/src/render/backend/webgpu/renderer.ts +12 -2
  31. package/src/render/depthConvention.ts +22 -0
  32. package/src/render/livePointShadowSet.ts +7 -0
  33. package/src/render/pointShadowBudget.ts +58 -5
  34. package/src/render/pointShadowImage.ts +51 -0
  35. package/src/render/pointShadowMap.ts +13 -0
  36. package/src/render/pointShadowSystem.ts +8 -0
  37. package/src/render/renderQuality.ts +28 -5
  38. package/src/render/shaders/flat/main.ts +13 -5
  39. package/src/render/shaders/flat/pointShadow.ts +42 -30
  40. package/src/render/shaders/flat/preamble.ts +26 -2
  41. package/src/render/shaders/generated/flat.wgsl.ts +24 -24
  42. package/src/render/shaders/sky.ts +37 -2
@@ -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 = `float pointShadow(
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
- lit += 1.0 - shadowReach(max(dist - storedDistance, 0.0), far);
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
- float shaded = lit / float(max(uShadowFilterTaps, 1));
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
- lit += 1.0 - shadowReach(max(dist - storedDistance, 0.0), far);
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
- float shaded = lit / float(max(uShadowFilterTaps, 1));
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
- uniform float uPointShadowFar[MAX_LIGHTS];
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
- uniform float uLivePointShadowFar[MAX_LIGHTS];
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];