@driftengine/core 4.2.1 → 4.4.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.
Files changed (215) hide show
  1. package/CHANGELOG.json +272 -0
  2. package/README.md +1 -1
  3. package/dist/cinematic/cameraPath.d.ts +53 -0
  4. package/dist/cinematic/cameraPath.js +118 -0
  5. package/dist/environment/daylight.d.ts +77 -0
  6. package/dist/environment/daylight.js +77 -0
  7. package/dist/geometry/meshConcat.d.ts +9 -0
  8. package/dist/geometry/meshConcat.js +101 -0
  9. package/dist/index.d.ts +12 -2
  10. package/dist/index.js +14 -1
  11. package/dist/render/backend/createRenderer.d.ts +13 -1
  12. package/dist/render/backend/createRenderer.js +6 -5
  13. package/dist/render/backend/webgl2/renderer.d.ts +166 -1
  14. package/dist/render/backend/webgl2/renderer.js +424 -23
  15. package/dist/render/backend/webgpu/buffers.js +46 -8
  16. package/dist/render/backend/webgpu/depthPass.d.ts +81 -3
  17. package/dist/render/backend/webgpu/depthPass.js +75 -14
  18. package/dist/render/backend/webgpu/exposurePass.d.ts +39 -0
  19. package/dist/render/backend/webgpu/exposurePass.js +190 -0
  20. package/dist/render/backend/webgpu/fieldCompose.d.ts +11 -0
  21. package/dist/render/backend/webgpu/fieldCompose.js +13 -0
  22. package/dist/render/backend/webgpu/flatPass.d.ts +3 -1
  23. package/dist/render/backend/webgpu/flatPass.js +12 -5
  24. package/dist/render/backend/webgpu/particlePass.d.ts +8 -0
  25. package/dist/render/backend/webgpu/particlePass.js +1 -0
  26. package/dist/render/backend/webgpu/postPass.d.ts +12 -2
  27. package/dist/render/backend/webgpu/probeBake.d.ts +59 -3
  28. package/dist/render/backend/webgpu/probeBake.js +97 -7
  29. package/dist/render/backend/webgpu/probeTargets.d.ts +65 -0
  30. package/dist/render/backend/webgpu/probeTargets.js +180 -0
  31. package/dist/render/backend/webgpu/renderer.d.ts +176 -11
  32. package/dist/render/backend/webgpu/renderer.js +680 -250
  33. package/dist/render/backend/webgpu/scatterPass.js +7 -7
  34. package/dist/render/backend/webgpu/shaders/clusterBin.wgsl.d.ts +1 -1
  35. package/dist/render/backend/webgpu/skinPaletteRing.d.ts +4 -0
  36. package/dist/render/backend/webgpu/skinPaletteRing.js +9 -0
  37. package/dist/render/backend/webgpu/skyPass.js +5 -1
  38. package/dist/render/backend/webgpu/uniformRing.d.ts +31 -1
  39. package/dist/render/backend/webgpu/uniformRing.js +53 -3
  40. package/dist/render/bloomChain.d.ts +6 -0
  41. package/dist/render/bloomChain.js +8 -0
  42. package/dist/render/clusteredLights.d.ts +19 -11
  43. package/dist/render/clusteredLights.js +96 -45
  44. package/dist/render/cutoutCaster.d.ts +23 -0
  45. package/dist/render/cutoutCaster.js +20 -0
  46. package/dist/render/driftLight/bake.d.ts +34 -0
  47. package/dist/render/driftLight/bake.js +95 -0
  48. package/dist/render/driftLight/falloff.d.ts +16 -0
  49. package/dist/render/driftLight/falloff.js +23 -0
  50. package/dist/render/driftLight/layout.d.ts +59 -0
  51. package/dist/render/driftLight/layout.js +138 -0
  52. package/dist/render/driftLight/lightField.d.ts +108 -0
  53. package/dist/render/driftLight/lightField.js +179 -0
  54. package/dist/render/driftLight/uniforms.d.ts +28 -0
  55. package/dist/render/driftLight/uniforms.js +41 -0
  56. package/dist/render/driftLight/visibility.d.ts +18 -0
  57. package/dist/render/driftLight/visibility.js +47 -0
  58. package/dist/render/emptyTexture.d.ts +6 -0
  59. package/dist/render/emptyTexture.js +19 -0
  60. package/dist/render/exposurePass.d.ts +48 -0
  61. package/dist/render/exposurePass.js +172 -0
  62. package/dist/render/flameLight.d.ts +71 -0
  63. package/dist/render/flameLight.js +63 -0
  64. package/dist/render/gi/bounceLights.d.ts +34 -0
  65. package/dist/render/gi/bounceLights.js +66 -0
  66. package/dist/render/gi/fieldReach.d.ts +12 -0
  67. package/dist/render/gi/fieldReach.js +62 -0
  68. package/dist/render/gi/globalField.d.ts +8 -0
  69. package/dist/render/gi/globalField.js +31 -0
  70. package/dist/render/gi/probeTrace.d.ts +3 -1
  71. package/dist/render/gi/probeTrace.js +3 -1
  72. package/dist/render/gpuCapability.js +19 -4
  73. package/dist/render/lightBudget.d.ts +11 -0
  74. package/dist/render/lightBudget.js +11 -0
  75. package/dist/render/pointLightSelection.d.ts +16 -0
  76. package/dist/render/pointLightSelection.js +178 -83
  77. package/dist/render/probeGrid.d.ts +11 -1
  78. package/dist/render/probeGrid.js +5 -2
  79. package/dist/render/probeSweeps.d.ts +45 -0
  80. package/dist/render/probeSweeps.js +77 -0
  81. package/dist/render/reflectionProbe.d.ts +43 -1
  82. package/dist/render/reflectionProbe.js +17 -3
  83. package/dist/render/renderQuality.d.ts +3 -1
  84. package/dist/render/rendererHarness.d.ts +9 -0
  85. package/dist/render/rendererHarness.js +18 -3
  86. package/dist/render/sceneTarget.d.ts +20 -0
  87. package/dist/render/sceneTarget.js +62 -1
  88. package/dist/render/shaders/agx.d.ts +31 -0
  89. package/dist/render/shaders/agx.js +81 -0
  90. package/dist/render/shaders/ambientOcclusion.d.ts +24 -5
  91. package/dist/render/shaders/ambientOcclusion.js +120 -63
  92. package/dist/render/shaders/cutoutCoverage.d.ts +31 -0
  93. package/dist/render/shaders/cutoutCoverage.js +40 -0
  94. package/dist/render/shaders/depth.d.ts +25 -0
  95. package/dist/render/shaders/depth.js +69 -3
  96. package/dist/render/shaders/exposure.d.ts +52 -0
  97. package/dist/render/shaders/exposure.js +124 -0
  98. package/dist/render/shaders/filmLook.d.ts +47 -0
  99. package/dist/render/shaders/filmLook.js +98 -0
  100. package/dist/render/shaders/flat/directionalShadow.js +35 -4
  101. package/dist/render/shaders/flat/driftLight.d.ts +29 -0
  102. package/dist/render/shaders/flat/driftLight.js +75 -0
  103. package/dist/render/shaders/flat/index.js +5 -0
  104. package/dist/render/shaders/flat/lobes.js +13 -0
  105. package/dist/render/shaders/flat/main.d.ts +1 -1
  106. package/dist/render/shaders/flat/main.js +68 -9
  107. package/dist/render/shaders/flat/preamble.js +23 -5
  108. package/dist/render/shaders/flat/probeGrid.js +46 -3
  109. package/dist/render/shaders/generated/ambientOcclusion.wgsl.d.ts +1 -1
  110. package/dist/render/shaders/generated/ambientOcclusion.wgsl.js +1 -1
  111. package/dist/render/shaders/generated/depth.wgsl.d.ts +100 -0
  112. package/dist/render/shaders/generated/depth.wgsl.js +100 -0
  113. package/dist/render/shaders/generated/exposure.wgsl.d.ts +42 -0
  114. package/dist/render/shaders/generated/exposure.wgsl.js +48 -0
  115. package/dist/render/shaders/generated/flat.wgsl.d.ts +1800 -1344
  116. package/dist/render/shaders/generated/flat.wgsl.js +1816 -1360
  117. package/dist/render/shaders/generated/localExposure.wgsl.d.ts +18 -0
  118. package/dist/render/shaders/generated/localExposure.wgsl.js +24 -0
  119. package/dist/render/shaders/generated/rush.wgsl.d.ts +40 -10
  120. package/dist/render/shaders/generated/rush.wgsl.js +40 -10
  121. package/dist/render/shaders/generated/temporalResolve.wgsl.d.ts +1 -1
  122. package/dist/render/shaders/generated/temporalResolve.wgsl.js +1 -1
  123. package/dist/render/shaders/gi/driftLight.wgsl.d.ts +16 -0
  124. package/dist/render/shaders/gi/driftLight.wgsl.js +49 -0
  125. package/dist/render/shaders/gi/probeBake.wgsl.d.ts +29 -4
  126. package/dist/render/shaders/gi/probeBake.wgsl.js +190 -20
  127. package/dist/render/shaders/globalMedium.js +4 -0
  128. package/dist/render/shaders/localExposure.d.ts +35 -0
  129. package/dist/render/shaders/localExposure.js +138 -0
  130. package/dist/render/shaders/prefilter.d.ts +1 -1
  131. package/dist/render/shaders/prefilter.js +6 -0
  132. package/dist/render/shaders/rush.js +33 -5
  133. package/dist/render/shaders/tangentFrame.d.ts +1 -1
  134. package/dist/render/shaders/tangentFrame.js +15 -3
  135. package/dist/render/shaders/temporalResolve.js +39 -1
  136. package/dist/render/shadowCasters.d.ts +5 -3
  137. package/dist/render/shadowMap.js +13 -0
  138. package/dist/render/surfaceTexture.d.ts +5 -0
  139. package/package.json +6 -6
  140. package/scripts/gpuCompute.mjs +24 -3
  141. package/scripts/png.mjs +23 -12
  142. package/src/cinematic/cameraPath.ts +178 -0
  143. package/src/environment/daylight.ts +152 -0
  144. package/src/geometry/meshConcat.ts +100 -0
  145. package/src/index.ts +32 -0
  146. package/src/render/backend/createRenderer.ts +22 -6
  147. package/src/render/backend/webgl2/renderer.ts +462 -20
  148. package/src/render/backend/webgpu/buffers.ts +51 -8
  149. package/src/render/backend/webgpu/depthPass.ts +78 -10
  150. package/src/render/backend/webgpu/exposurePass.ts +234 -0
  151. package/src/render/backend/webgpu/fieldCompose.ts +15 -0
  152. package/src/render/backend/webgpu/flatPass.ts +10 -2
  153. package/src/render/backend/webgpu/particlePass.ts +9 -0
  154. package/src/render/backend/webgpu/probeBake.ts +143 -9
  155. package/src/render/backend/webgpu/probeTargets.ts +211 -0
  156. package/src/render/backend/webgpu/renderer.ts +807 -271
  157. package/src/render/backend/webgpu/scatterPass.ts +7 -7
  158. package/src/render/backend/webgpu/skinPaletteRing.ts +10 -0
  159. package/src/render/backend/webgpu/skyPass.ts +5 -1
  160. package/src/render/backend/webgpu/uniformRing.ts +49 -3
  161. package/src/render/bloomChain.ts +9 -0
  162. package/src/render/clusteredLights.ts +108 -63
  163. package/src/render/cutoutCaster.ts +44 -0
  164. package/src/render/driftLight/bake.ts +125 -0
  165. package/src/render/driftLight/falloff.ts +26 -0
  166. package/src/render/driftLight/layout.ts +177 -0
  167. package/src/render/driftLight/lightField.ts +227 -0
  168. package/src/render/driftLight/uniforms.ts +61 -0
  169. package/src/render/driftLight/visibility.ts +58 -0
  170. package/src/render/emptyTexture.ts +41 -0
  171. package/src/render/exposurePass.ts +186 -0
  172. package/src/render/flameLight.ts +135 -0
  173. package/src/render/gi/bounceLights.ts +68 -0
  174. package/src/render/gi/fieldReach.ts +74 -0
  175. package/src/render/gi/globalField.ts +39 -0
  176. package/src/render/gi/probeTrace.ts +3 -1
  177. package/src/render/gpuCapability.ts +18 -5
  178. package/src/render/lightBudget.ts +12 -0
  179. package/src/render/pointLightSelection.ts +191 -79
  180. package/src/render/probeGrid.ts +13 -2
  181. package/src/render/probeSweeps.ts +80 -0
  182. package/src/render/reflectionProbe.ts +54 -2
  183. package/src/render/renderQuality.ts +3 -1
  184. package/src/render/rendererHarness.ts +25 -3
  185. package/src/render/sceneTarget.ts +75 -1
  186. package/src/render/shaders/ambientOcclusion.ts +125 -63
  187. package/src/render/shaders/cutoutCoverage.ts +43 -0
  188. package/src/render/shaders/depth.ts +80 -3
  189. package/src/render/shaders/exposure.ts +128 -0
  190. package/src/render/shaders/filmLook.ts +111 -0
  191. package/src/render/shaders/flat/directionalShadow.ts +35 -4
  192. package/src/render/shaders/flat/driftLight.ts +75 -0
  193. package/src/render/shaders/flat/index.ts +5 -0
  194. package/src/render/shaders/flat/lobes.ts +13 -0
  195. package/src/render/shaders/flat/main.ts +68 -9
  196. package/src/render/shaders/flat/preamble.ts +23 -5
  197. package/src/render/shaders/flat/probeGrid.ts +46 -3
  198. package/src/render/shaders/generated/ambientOcclusion.wgsl.ts +1 -1
  199. package/src/render/shaders/generated/depth.wgsl.ts +103 -0
  200. package/src/render/shaders/generated/exposure.wgsl.ts +51 -0
  201. package/src/render/shaders/generated/flat.wgsl.ts +1816 -1360
  202. package/src/render/shaders/generated/localExposure.wgsl.ts +26 -0
  203. package/src/render/shaders/generated/rush.wgsl.ts +40 -10
  204. package/src/render/shaders/generated/temporalResolve.wgsl.ts +1 -1
  205. package/src/render/shaders/gi/driftLight.wgsl.ts +49 -0
  206. package/src/render/shaders/gi/probeBake.wgsl.ts +191 -20
  207. package/src/render/shaders/globalMedium.ts +4 -0
  208. package/src/render/shaders/localExposure.ts +154 -0
  209. package/src/render/shaders/prefilter.ts +6 -0
  210. package/src/render/shaders/rush.ts +33 -5
  211. package/src/render/shaders/tangentFrame.ts +15 -3
  212. package/src/render/shaders/temporalResolve.ts +39 -1
  213. package/src/render/shadowCasters.ts +5 -3
  214. package/src/render/shadowMap.ts +14 -0
  215. package/src/render/surfaceTexture.ts +5 -0
@@ -0,0 +1,47 @@
1
+ /**
2
+ * The lens and the print: a vignette in scene light, and grain in display values.
3
+ *
4
+ * **The vignette is the lens's, so it acts on light rather than on the picture.** A lens loses light
5
+ * toward its corners by roughly the fourth power of the cosine of the angle off axis, which for a
6
+ * point `r` from the centre is `1 / (1 + k r^2)^2`. Applied to scene light before the tone curve,
7
+ * a bright corner rolls off through the curve's shoulder the way a photographed one does, where a
8
+ * vignette painted over the finished picture greys it. `r` is measured to the frame's corner on a
9
+ * circle, not an ellipse, so a wide window is not darker at its sides than at its top.
10
+ *
11
+ * **The grain is the print's, so it acts on display values after the grade and the veil.** It is
12
+ * one value a pixel, the same on all three channels, weighted toward the midtones where film shows
13
+ * it and kept faint in deep shadow and highlight, where adding it would lift black and grey white.
14
+ * It also dithers: a dark gradient quantised to eight bits bands, and a noise of a few levels breaks
15
+ * the bands up. **The seed is the caller's**, because the engine takes time from its caller: a held
16
+ * capture passes the same seed and is identical run to run, and a playing scene passes a new one
17
+ * each frame and the grain moves as film does.
18
+ *
19
+ * Zero strength for each is the whole of its off path: a branch on a uniform, and the frame is what
20
+ * it was byte for byte.
21
+ */
22
+ /**
23
+ * `uVignette`, `uGrain`, `withVignette` and `withGrain`. The rush composite includes it once.
24
+ *
25
+ * `withVignette` takes the pixel's coordinate and the scene's size, since a distance to the corner
26
+ * has to know the aspect. `withGrain` takes the fragment's integer position, so the grain is one
27
+ * value a pixel whatever the resolution.
28
+ */
29
+ export declare const FILM_LOOK_GLSL = "\n/** How strongly the lens darkens its corners: 0 is none, 0.5 about 1.2 stops at the corner. */\nuniform float uVignette;\n/** x: the grain's amplitude in display values, 0 for none. y: this frame's seed, the caller's. */\nuniform vec2 uGrain;\n\nvec3 withVignette(vec3 c, vec2 uv, vec2 size) {\n if (uVignette <= 0.0) return c;\n vec2 d = (uv - 0.5) * size;\n vec2 corner = 0.5 * size;\n float r2 = dot(d, d) / dot(corner, corner);\n float falloff = 1.0 + uVignette * r2;\n return c / (falloff * falloff);\n}\n\nfloat grainNoise(uvec2 p, uint seed) {\n uint h = p.x * 1664525u + p.y * 1013904223u + seed * 2654435769u;\n h ^= h >> 16u;\n h *= 2246822519u;\n h ^= h >> 13u;\n h *= 3266489917u;\n h ^= h >> 16u;\n return float(h) * (1.0 / 4294967295.0);\n}\n\nvec3 withGrain(vec3 c, vec2 fragCoord) {\n if (uGrain.x <= 0.0) return c;\n float luma = dot(clamp(c, 0.0, 1.0), vec3(0.2126, 0.7152, 0.0722));\n float weight = 0.25 + 3.0 * luma * (1.0 - luma);\n float n = grainNoise(uvec2(fragCoord), uint(uGrain.y)) - 0.5;\n return c + vec3(2.0 * n * uGrain.x * weight);\n}\n";
30
+ /**
31
+ * The vignette's factor at `uv` on a `width` by `height` frame, as the shader computes it.
32
+ * The shader's arithmetic written out once more, so a test can pin it to hand-derived values.
33
+ */
34
+ export declare function vignetteFactor(u: number, v: number, width: number, height: number, strength: number): number;
35
+ /** How much of the grain a display luminance takes: full in the midtones, a quarter at the ends. */
36
+ export declare function grainWeight(luma: number): number;
37
+ /** Both backends' words when there is no composite to apply the look in. */
38
+ export declare const FILM_LOOK_WITHOUT_COMPOSITE: string;
39
+ /** A vignette strength as the shader takes it: 0 to 4, anything else off. */
40
+ export declare function clampVignette(strength: number): number;
41
+ /** A grain amplitude in display values: 0 to 0.5, anything else off. */
42
+ export declare function clampGrain(strength: number): number;
43
+ /**
44
+ * A seed as the shader reads it: a whole number below 2^24, which a 32-bit float carries exactly,
45
+ * so the same seed is the same grain on both backends. Any number a caller has is accepted.
46
+ */
47
+ export declare function grainSeed(seed: number): number;
@@ -0,0 +1,98 @@
1
+ /**
2
+ * The lens and the print: a vignette in scene light, and grain in display values.
3
+ *
4
+ * **The vignette is the lens's, so it acts on light rather than on the picture.** A lens loses light
5
+ * toward its corners by roughly the fourth power of the cosine of the angle off axis, which for a
6
+ * point `r` from the centre is `1 / (1 + k r^2)^2`. Applied to scene light before the tone curve,
7
+ * a bright corner rolls off through the curve's shoulder the way a photographed one does, where a
8
+ * vignette painted over the finished picture greys it. `r` is measured to the frame's corner on a
9
+ * circle, not an ellipse, so a wide window is not darker at its sides than at its top.
10
+ *
11
+ * **The grain is the print's, so it acts on display values after the grade and the veil.** It is
12
+ * one value a pixel, the same on all three channels, weighted toward the midtones where film shows
13
+ * it and kept faint in deep shadow and highlight, where adding it would lift black and grey white.
14
+ * It also dithers: a dark gradient quantised to eight bits bands, and a noise of a few levels breaks
15
+ * the bands up. **The seed is the caller's**, because the engine takes time from its caller: a held
16
+ * capture passes the same seed and is identical run to run, and a playing scene passes a new one
17
+ * each frame and the grain moves as film does.
18
+ *
19
+ * Zero strength for each is the whole of its off path: a branch on a uniform, and the frame is what
20
+ * it was byte for byte.
21
+ */
22
+ /**
23
+ * `uVignette`, `uGrain`, `withVignette` and `withGrain`. The rush composite includes it once.
24
+ *
25
+ * `withVignette` takes the pixel's coordinate and the scene's size, since a distance to the corner
26
+ * has to know the aspect. `withGrain` takes the fragment's integer position, so the grain is one
27
+ * value a pixel whatever the resolution.
28
+ */
29
+ export const FILM_LOOK_GLSL = `
30
+ /** How strongly the lens darkens its corners: 0 is none, 0.5 about 1.2 stops at the corner. */
31
+ uniform float uVignette;
32
+ /** x: the grain's amplitude in display values, 0 for none. y: this frame's seed, the caller's. */
33
+ uniform vec2 uGrain;
34
+
35
+ vec3 withVignette(vec3 c, vec2 uv, vec2 size) {
36
+ if (uVignette <= 0.0) return c;
37
+ vec2 d = (uv - 0.5) * size;
38
+ vec2 corner = 0.5 * size;
39
+ float r2 = dot(d, d) / dot(corner, corner);
40
+ float falloff = 1.0 + uVignette * r2;
41
+ return c / (falloff * falloff);
42
+ }
43
+
44
+ float grainNoise(uvec2 p, uint seed) {
45
+ uint h = p.x * 1664525u + p.y * 1013904223u + seed * 2654435769u;
46
+ h ^= h >> 16u;
47
+ h *= 2246822519u;
48
+ h ^= h >> 13u;
49
+ h *= 3266489917u;
50
+ h ^= h >> 16u;
51
+ return float(h) * (1.0 / 4294967295.0);
52
+ }
53
+
54
+ vec3 withGrain(vec3 c, vec2 fragCoord) {
55
+ if (uGrain.x <= 0.0) return c;
56
+ float luma = dot(clamp(c, 0.0, 1.0), vec3(0.2126, 0.7152, 0.0722));
57
+ float weight = 0.25 + 3.0 * luma * (1.0 - luma);
58
+ float n = grainNoise(uvec2(fragCoord), uint(uGrain.y)) - 0.5;
59
+ return c + vec3(2.0 * n * uGrain.x * weight);
60
+ }
61
+ `;
62
+ /**
63
+ * The vignette's factor at `uv` on a `width` by `height` frame, as the shader computes it.
64
+ * The shader's arithmetic written out once more, so a test can pin it to hand-derived values.
65
+ */
66
+ export function vignetteFactor(u, v, width, height, strength) {
67
+ if (strength <= 0)
68
+ return 1;
69
+ const dx = (u - 0.5) * width;
70
+ const dy = (v - 0.5) * height;
71
+ const r2 = (dx * dx + dy * dy) / (0.25 * (width * width + height * height));
72
+ const falloff = 1 + strength * r2;
73
+ return 1 / (falloff * falloff);
74
+ }
75
+ /** How much of the grain a display luminance takes: full in the midtones, a quarter at the ends. */
76
+ export function grainWeight(luma) {
77
+ const l = Math.min(Math.max(luma, 0), 1);
78
+ return 0.25 + 3 * l * (1 - l);
79
+ }
80
+ /** Both backends' words when there is no composite to apply the look in. */
81
+ export const FILM_LOOK_WITHOUT_COMPOSITE = '[driftengine] setVignette and setFilmGrain need `screenEffects`: without a composite every ' +
82
+ 'pass is the last thing to touch the frame, so there is no one place to apply a lens or a ' +
83
+ 'print. The look was not applied.';
84
+ /** A vignette strength as the shader takes it: 0 to 4, anything else off. */
85
+ export function clampVignette(strength) {
86
+ return Number.isFinite(strength) ? Math.min(Math.max(strength, 0), 4) : 0;
87
+ }
88
+ /** A grain amplitude in display values: 0 to 0.5, anything else off. */
89
+ export function clampGrain(strength) {
90
+ return Number.isFinite(strength) ? Math.min(Math.max(strength, 0), 0.5) : 0;
91
+ }
92
+ /**
93
+ * A seed as the shader reads it: a whole number below 2^24, which a 32-bit float carries exactly,
94
+ * so the same seed is the same grain on both backends. Any number a caller has is accepted.
95
+ */
96
+ export function grainSeed(seed) {
97
+ return Number.isFinite(seed) ? Math.floor(Math.abs(seed)) % 16777216 : 0;
98
+ }
@@ -67,10 +67,13 @@ float shadowFactor(float directionalNdl) {
67
67
  // Near a surface's own light terminator its direct-light term is already
68
68
  // approaching zero, while the orthographic projection of that surface
69
69
  // collapses toward a line. No finite contact-safe bias can represent that
70
- // slope reliably. Fade only the shadow modulation through this narrow band;
71
- // the Lambert term still supplies the physically dominant falloff.
70
+ // slope reliably, so through this band the fine filter below hands over to the
71
+ // coarse test after it. \`directionalNdl\` is the surface's own, before any normal
72
+ // map: the band is about the receiving plane, and a map's tilt says nothing about it.
72
73
  float receiverFade = smoothstep(0.08, 0.20, directionalNdl);
73
- if (receiverFade <= 0.0) return 1.0;
74
+ /* A surface turned from the light takes none of it, so there is nothing to shadow; what reads
75
+ this there is the emissive share, which has always seen such a surface as unshadowed. */
76
+ if (directionalNdl <= 0.0) return 1.0;
74
77
 
75
78
  // The receiver stays at its real position, computed above. A normal offset changes
76
79
  // direction at a hard mesh edge and tears one continuous shadow where it crosses from
@@ -103,6 +106,34 @@ float shadowFactor(float directionalNdl) {
103
106
  );
104
107
  if (lowElevationFade <= 0.0) return 1.0;
105
108
 
109
+ /*
110
+ * **The coarse test: is there anything a metre or more between this point and the sun.**
111
+ *
112
+ * One tap at the receiver's own texel, with a metre of tolerance where the filter below allows
113
+ * fourteen centimetres. It cannot see a contact shadow, and it does not need the plane's slope:
114
+ * a grazing plane's own depth moves about a centimetre times the tangent of its angle to the
115
+ * light across a texel, which stays under a metre down to a hundredth of n·l on a map with
116
+ * centimetre texels. What it does see is a roof.
117
+ *
118
+ * **This band used to return fully lit**, on the argument that the Lambert term was already
119
+ * near zero there. Under a high sun that is every vertical wall: at 70° of elevation a wall's
120
+ * n·l is at most 0.34, and one turned from the sun's bearing sits inside the band. A gallery's
121
+ * back wall under its vault took the sun through the vault, and so did the faces of its
122
+ * columns, which read as lit stone in shade; with a normal map carrying that n·l per texel it
123
+ * was a field of white specks. What would make this wrong is a map whose texels are tens of
124
+ * centimetres, where a grazing plane's own depth moves more than the tolerance and the band
125
+ * draws the acne it exists to avoid.
126
+ */
127
+ float coarseCompare = p.z - 1.0 / uShadowDepthSpan;
128
+ float coarse =
129
+ directionalVisibility(p.z, coarseCompare, textureLod(uStaticShadowMap, p.xy, 0.0).r) *
130
+ directionalVisibility(p.z, coarseCompare, textureLod(uDynamicShadowMap, p.xy, 0.0).r);
131
+ if (uPeeledShadowEnabled != 0) {
132
+ coarse *= directionalVisibility(p.z, coarseCompare, textureLod(uPeeledShadowMap, p.xy, 0.0).r);
133
+ }
134
+ float strength = uShadowStrength * edgeFade * lowElevationFade;
135
+ if (receiverFade <= 0.0) return mix(1.0, coarse, strength);
136
+
106
137
  float texel = 1.35 / uShadowMapSize;
107
138
  /*
108
139
  * How far the plane compensation below may carry a tap's depth, in the map's own units.
@@ -181,7 +212,7 @@ float shadowFactor(float directionalNdl) {
181
212
  // conventional one-depth map would discard; movers remain independent too.
182
213
  float lit = staticLit * peeledLit * dynamicLit;
183
214
 
184
- return mix(1.0, lit, uShadowStrength * edgeFade * lowElevationFade * receiverFade);
215
+ return mix(1.0, mix(coarse, lit, receiverFade), strength);
185
216
  }
186
217
  #endif
187
218
 
@@ -0,0 +1,29 @@
1
+ /**
2
+ * DriftLight in the lit shader: how much of a pixel the summed light stands in for, and the summed
3
+ * light itself, read from the field's sparse volume.
4
+ *
5
+ * **The split is per pixel.** Inside the frame's completeness radius (`uDriftLight.y`, measured from
6
+ * the eye) every light that reaches the pixel was chosen, so it is shaded exactly; past it the
7
+ * volume stands in, and across the band before the radius the two crossfade. The choice was made
8
+ * about some centre, which need not be the eye; `uniforms.ts` takes the distance between the two
9
+ * off the radius, so a pixel inside it is inside the choice's radius too. A field light's
10
+ * exact contribution is scaled by the share left over (`main.ts`, where its weight's sign is read),
11
+ * so a light is counted once however the pixel is split.
12
+ *
13
+ * **One index fetch and two filtered ones.** The index finds the brick; the brick's four samples a
14
+ * side share their faces with the next brick's, so the hardware's trilinear lookup inside one is
15
+ * seamless across the join. The light texel carries in its fourth channel whether each sample is a
16
+ * sample at all, and dividing by the filtered value drops the samples buried in walls rather than
17
+ * blending their darkness into the surface. The lookup is nudged half a spacing along the normal,
18
+ * which keeps a surface reading the air in front of it rather than the wall it is part of.
19
+ *
20
+ * **Two vectors of uniforms, and the sizes read off the textures.** The lit stage sits two vectors
21
+ * under the 256 a reported part offers at eight lights, and the first cut of this, at five, took that
22
+ * part down a rung to four. So the grid's extent and the atlas's are what `textureSize` answers, and
23
+ * the radius is measured from `uCameraPos`, which the stage already has, rather than from a centre
24
+ * of its own. What it gives up is two integer queries a pixel past the radius.
25
+ *
26
+ * `textureLod` throughout, by the 2026-08-07 rule: this is reached through branches no compiler can
27
+ * prove uniform. Both samplers are declared `highp` because GLSL ES gives 3D samplers no default.
28
+ */
29
+ export declare const DRIFT_LIGHT_GLSL = "\n/** x how much of the summed light is in the frame (0 off), y the radius, z the band, w the scale. */\nuniform vec4 uDriftLight;\n/** xyz the first brick's first sample; w metres between samples, a third of a brick. */\nuniform vec4 uDriftLightOrigin;\nuniform highp usampler3D uDriftLightIndex; // wgsl:share shadow\nuniform highp sampler3D uDriftLightAtlas;\n\n/** The share of this pixel's field light the volume stands in for: 0 inside the radius, 1 past it. */\nfloat driftLightShare(vec3 world) {\n if (uDriftLight.x <= 0.0) return 0.0;\n float away = distance(world, uCameraPos);\n return smoothstep(uDriftLight.y - uDriftLight.z, uDriftLight.y, away) * uDriftLight.x;\n}\n\n/** The summed light arriving on a surface at `world` facing `n`. */\nvec3 driftLightIrradiance(vec3 world, vec3 n) {\n float spacing = uDriftLightOrigin.w;\n vec3 at = (world + n * (0.5 * spacing) - uDriftLightOrigin.xyz) / (3.0 * spacing);\n ivec3 cell = ivec3(floor(at));\n if (any(lessThan(cell, ivec3(0))) || any(greaterThanEqual(cell, textureSize(uDriftLightIndex, 0)))) {\n return vec3(0.0);\n }\n uint id = texelFetch(uDriftLightIndex, cell, 0).r;\n if (id == 0u) return vec3(0.0);\n /* A brick is eight texels across (its light block, then its direction block) and four up and deep. */\n ivec3 size = textureSize(uDriftLightAtlas, 0);\n int brick = int(id) - 1;\n int across = size.x / 8;\n int down = size.y / 4;\n vec3 corner = vec3(\n float((brick % across) * 8),\n float(((brick / across) % down) * 4),\n float((brick / (across * down)) * 4)\n );\n vec3 texel = 1.0 / vec3(size);\n vec3 inside = (at - vec3(cell)) * 3.0 + 0.5;\n vec4 light = textureLod(uDriftLightAtlas, (corner + inside) * texel, 0.0);\n if (light.a < 1e-3) return vec3(0.0);\n vec4 toward = textureLod(uDriftLightAtlas, (corner + vec3(4.0, 0.0, 0.0) + inside) * texel, 0.0);\n vec3 arriving = light.rgb / light.a;\n vec3 from = toward.xyz / light.a;\n /* Lambert for a single light, a quarter for light arriving from everywhere: see `bake.ts`. */\n float facing = max(dot(n, from), 0.0) + (1.0 - min(length(from), 1.0)) * 0.25;\n return arriving * facing;\n}\n";
@@ -0,0 +1,75 @@
1
+ /**
2
+ * DriftLight in the lit shader: how much of a pixel the summed light stands in for, and the summed
3
+ * light itself, read from the field's sparse volume.
4
+ *
5
+ * **The split is per pixel.** Inside the frame's completeness radius (`uDriftLight.y`, measured from
6
+ * the eye) every light that reaches the pixel was chosen, so it is shaded exactly; past it the
7
+ * volume stands in, and across the band before the radius the two crossfade. The choice was made
8
+ * about some centre, which need not be the eye; `uniforms.ts` takes the distance between the two
9
+ * off the radius, so a pixel inside it is inside the choice's radius too. A field light's
10
+ * exact contribution is scaled by the share left over (`main.ts`, where its weight's sign is read),
11
+ * so a light is counted once however the pixel is split.
12
+ *
13
+ * **One index fetch and two filtered ones.** The index finds the brick; the brick's four samples a
14
+ * side share their faces with the next brick's, so the hardware's trilinear lookup inside one is
15
+ * seamless across the join. The light texel carries in its fourth channel whether each sample is a
16
+ * sample at all, and dividing by the filtered value drops the samples buried in walls rather than
17
+ * blending their darkness into the surface. The lookup is nudged half a spacing along the normal,
18
+ * which keeps a surface reading the air in front of it rather than the wall it is part of.
19
+ *
20
+ * **Two vectors of uniforms, and the sizes read off the textures.** The lit stage sits two vectors
21
+ * under the 256 a reported part offers at eight lights, and the first cut of this, at five, took that
22
+ * part down a rung to four. So the grid's extent and the atlas's are what `textureSize` answers, and
23
+ * the radius is measured from `uCameraPos`, which the stage already has, rather than from a centre
24
+ * of its own. What it gives up is two integer queries a pixel past the radius.
25
+ *
26
+ * `textureLod` throughout, by the 2026-08-07 rule: this is reached through branches no compiler can
27
+ * prove uniform. Both samplers are declared `highp` because GLSL ES gives 3D samplers no default.
28
+ */
29
+ export const DRIFT_LIGHT_GLSL = /* glsl */ `
30
+ /** x how much of the summed light is in the frame (0 off), y the radius, z the band, w the scale. */
31
+ uniform vec4 uDriftLight;
32
+ /** xyz the first brick's first sample; w metres between samples, a third of a brick. */
33
+ uniform vec4 uDriftLightOrigin;
34
+ uniform highp usampler3D uDriftLightIndex; // wgsl:share shadow
35
+ uniform highp sampler3D uDriftLightAtlas;
36
+
37
+ /** The share of this pixel's field light the volume stands in for: 0 inside the radius, 1 past it. */
38
+ float driftLightShare(vec3 world) {
39
+ if (uDriftLight.x <= 0.0) return 0.0;
40
+ float away = distance(world, uCameraPos);
41
+ return smoothstep(uDriftLight.y - uDriftLight.z, uDriftLight.y, away) * uDriftLight.x;
42
+ }
43
+
44
+ /** The summed light arriving on a surface at \`world\` facing \`n\`. */
45
+ vec3 driftLightIrradiance(vec3 world, vec3 n) {
46
+ float spacing = uDriftLightOrigin.w;
47
+ vec3 at = (world + n * (0.5 * spacing) - uDriftLightOrigin.xyz) / (3.0 * spacing);
48
+ ivec3 cell = ivec3(floor(at));
49
+ if (any(lessThan(cell, ivec3(0))) || any(greaterThanEqual(cell, textureSize(uDriftLightIndex, 0)))) {
50
+ return vec3(0.0);
51
+ }
52
+ uint id = texelFetch(uDriftLightIndex, cell, 0).r;
53
+ if (id == 0u) return vec3(0.0);
54
+ /* A brick is eight texels across (its light block, then its direction block) and four up and deep. */
55
+ ivec3 size = textureSize(uDriftLightAtlas, 0);
56
+ int brick = int(id) - 1;
57
+ int across = size.x / 8;
58
+ int down = size.y / 4;
59
+ vec3 corner = vec3(
60
+ float((brick % across) * 8),
61
+ float(((brick / across) % down) * 4),
62
+ float((brick / (across * down)) * 4)
63
+ );
64
+ vec3 texel = 1.0 / vec3(size);
65
+ vec3 inside = (at - vec3(cell)) * 3.0 + 0.5;
66
+ vec4 light = textureLod(uDriftLightAtlas, (corner + inside) * texel, 0.0);
67
+ if (light.a < 1e-3) return vec3(0.0);
68
+ vec4 toward = textureLod(uDriftLightAtlas, (corner + vec3(4.0, 0.0, 0.0) + inside) * texel, 0.0);
69
+ vec3 arriving = light.rgb / light.a;
70
+ vec3 from = toward.xyz / light.a;
71
+ /* Lambert for a single light, a quarter for light arriving from everywhere: see \`bake.ts\`. */
72
+ float facing = max(dot(n, from), 0.0) + (1.0 - min(length(from), 1.0)) * 0.25;
73
+ return arriving * facing;
74
+ }
75
+ `;
@@ -2,6 +2,7 @@ import { resolveConditionals } from '../conditionals.js';
2
2
  import { OCTAHEDRAL_GLSL } from '../octahedral.js';
3
3
  import { PROBEGRID_GLSL } from './probeGrid.js';
4
4
  import { TANGENT_FRAME_GLSL } from '../tangentFrame.js';
5
+ import { CUTOUT_COVERAGE_GLSL } from '../cutoutCoverage.js';
5
6
  import { preambleGlsl } from './preamble.js';
6
7
  import { LOBES_GLSL } from './lobes.js';
7
8
  import { POINTSHADOW_GLSL } from './pointShadow.js';
@@ -10,6 +11,7 @@ import { MORPH_GLSL } from '../morph.js';
10
11
  import { SKINNING_GLSL } from '../skinning.js';
11
12
  import { SURFACE_GLSL } from './surface.js';
12
13
  import { MAIN_GLSL } from './main.js';
14
+ import { DRIFT_LIGHT_GLSL } from './driftLight.js';
13
15
  import { CHANNEL_ATTRIBUTE, CHANNEL_BEND } from '../vertexChannel.js';
14
16
  import { FULL_LIGHT_BUDGET } from '../../uniformVectorBudget.js';
15
17
  /**
@@ -377,6 +379,9 @@ export function flatFrag(options) {
377
379
  * and `main` is unconditional by construction.
378
380
  */
379
381
  TANGENT_FRAME_GLSL,
382
+ /* Unconditional for the same reason, and called only where a cutoff was asked for. */
383
+ CUTOUT_COVERAGE_GLSL,
384
+ DRIFT_LIGHT_GLSL,
380
385
  MAIN_GLSL,
381
386
  ].join('\n'), {
382
387
  POINT_SHADOWS: options.pointShadows,
@@ -71,6 +71,19 @@ const float PENUMBRA_FADE = 1.6;
71
71
  */
72
72
  const float MAX_FILTER_RADIUS = 0.25;
73
73
 
74
+ /**
75
+ * The weight below which a point light's shadow is not looked up at all.
76
+ *
77
+ * A light's shadow is blended toward none as the light falls off, \`mix(1, occl, weight)\` with the
78
+ * weight twice the falloff, so a shadow looked up under this floor changes at most three per cent
79
+ * of a light that has already all but faded: with the inverse-square falloff, past about eight
80
+ * metres from it. Measured on a lantern-lit courtyard, the lookups the floor skips were most of a
81
+ * night's frame: 14 ms of 26 at 1440p, a blocker tap and twelve filter taps for every light in every
82
+ * froxel. What it gives up is the last three per cent of a distant light's shadow, which the blend
83
+ * was already taking away; what would make it wrong is a falloff that stays strong far out.
84
+ */
85
+ const float POINT_SHADOW_MIN_WEIGHT = 0.03;
86
+
74
87
  /**
75
88
  * How much of a self-lit surface's glow a lamp's shadow may take.
76
89
  *