@driftengine/core 4.3.0 → 4.4.1

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 (218) hide show
  1. package/CHANGELOG.json +282 -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/ambientOcclusionPass.js +5 -1
  12. package/dist/render/backend/createRenderer.d.ts +13 -1
  13. package/dist/render/backend/createRenderer.js +6 -5
  14. package/dist/render/backend/webgl2/renderer.d.ts +147 -1
  15. package/dist/render/backend/webgl2/renderer.js +402 -23
  16. package/dist/render/backend/webgpu/buffers.js +46 -8
  17. package/dist/render/backend/webgpu/depthPass.d.ts +81 -3
  18. package/dist/render/backend/webgpu/depthPass.js +75 -14
  19. package/dist/render/backend/webgpu/exposurePass.d.ts +39 -0
  20. package/dist/render/backend/webgpu/exposurePass.js +190 -0
  21. package/dist/render/backend/webgpu/fieldCompose.d.ts +11 -0
  22. package/dist/render/backend/webgpu/fieldCompose.js +13 -0
  23. package/dist/render/backend/webgpu/flatPass.d.ts +3 -1
  24. package/dist/render/backend/webgpu/flatPass.js +12 -5
  25. package/dist/render/backend/webgpu/particlePass.d.ts +8 -0
  26. package/dist/render/backend/webgpu/particlePass.js +1 -0
  27. package/dist/render/backend/webgpu/postPass.d.ts +13 -3
  28. package/dist/render/backend/webgpu/probeBake.d.ts +59 -3
  29. package/dist/render/backend/webgpu/probeBake.js +97 -7
  30. package/dist/render/backend/webgpu/probeTargets.d.ts +65 -0
  31. package/dist/render/backend/webgpu/probeTargets.js +180 -0
  32. package/dist/render/backend/webgpu/renderer.d.ts +174 -11
  33. package/dist/render/backend/webgpu/renderer.js +682 -252
  34. package/dist/render/backend/webgpu/scatterPass.js +7 -7
  35. package/dist/render/backend/webgpu/shaders/clusterBin.wgsl.d.ts +1 -1
  36. package/dist/render/backend/webgpu/skinPaletteRing.d.ts +4 -0
  37. package/dist/render/backend/webgpu/skinPaletteRing.js +9 -0
  38. package/dist/render/backend/webgpu/skyPass.js +5 -1
  39. package/dist/render/backend/webgpu/uniformRing.d.ts +31 -1
  40. package/dist/render/backend/webgpu/uniformRing.js +53 -3
  41. package/dist/render/bloomChain.d.ts +6 -0
  42. package/dist/render/bloomChain.js +8 -0
  43. package/dist/render/clusteredLights.d.ts +19 -11
  44. package/dist/render/clusteredLights.js +96 -45
  45. package/dist/render/cutoutCaster.d.ts +23 -0
  46. package/dist/render/cutoutCaster.js +20 -0
  47. package/dist/render/driftLight/bake.d.ts +34 -0
  48. package/dist/render/driftLight/bake.js +95 -0
  49. package/dist/render/driftLight/falloff.d.ts +16 -0
  50. package/dist/render/driftLight/falloff.js +23 -0
  51. package/dist/render/driftLight/layout.d.ts +59 -0
  52. package/dist/render/driftLight/layout.js +138 -0
  53. package/dist/render/driftLight/lightField.d.ts +108 -0
  54. package/dist/render/driftLight/lightField.js +179 -0
  55. package/dist/render/driftLight/uniforms.d.ts +28 -0
  56. package/dist/render/driftLight/uniforms.js +41 -0
  57. package/dist/render/driftLight/visibility.d.ts +18 -0
  58. package/dist/render/driftLight/visibility.js +47 -0
  59. package/dist/render/emptyTexture.d.ts +6 -0
  60. package/dist/render/emptyTexture.js +19 -0
  61. package/dist/render/exposurePass.d.ts +48 -0
  62. package/dist/render/exposurePass.js +172 -0
  63. package/dist/render/flameLight.d.ts +71 -0
  64. package/dist/render/flameLight.js +63 -0
  65. package/dist/render/gi/bounceLights.d.ts +34 -0
  66. package/dist/render/gi/bounceLights.js +66 -0
  67. package/dist/render/gi/fieldReach.d.ts +12 -0
  68. package/dist/render/gi/fieldReach.js +62 -0
  69. package/dist/render/gi/globalField.d.ts +8 -0
  70. package/dist/render/gi/globalField.js +31 -0
  71. package/dist/render/gi/probeTrace.d.ts +3 -1
  72. package/dist/render/gi/probeTrace.js +3 -1
  73. package/dist/render/gpuCapability.js +19 -4
  74. package/dist/render/lightBudget.d.ts +11 -0
  75. package/dist/render/lightBudget.js +11 -0
  76. package/dist/render/pointLightSelection.d.ts +16 -0
  77. package/dist/render/pointLightSelection.js +178 -83
  78. package/dist/render/pointShadowBudget.d.ts +12 -2
  79. package/dist/render/pointShadowBudget.js +16 -10
  80. package/dist/render/probeGrid.d.ts +11 -1
  81. package/dist/render/probeGrid.js +5 -2
  82. package/dist/render/probeSweeps.d.ts +45 -0
  83. package/dist/render/probeSweeps.js +77 -0
  84. package/dist/render/reflectionProbe.d.ts +43 -1
  85. package/dist/render/reflectionProbe.js +17 -3
  86. package/dist/render/renderQuality.d.ts +3 -1
  87. package/dist/render/rendererHarness.d.ts +9 -0
  88. package/dist/render/rendererHarness.js +18 -3
  89. package/dist/render/sceneTarget.d.ts +20 -0
  90. package/dist/render/sceneTarget.js +62 -1
  91. package/dist/render/shaders/ambientOcclusion.d.ts +63 -6
  92. package/dist/render/shaders/ambientOcclusion.js +188 -71
  93. package/dist/render/shaders/cutoutCoverage.d.ts +31 -0
  94. package/dist/render/shaders/cutoutCoverage.js +40 -0
  95. package/dist/render/shaders/depth.d.ts +25 -0
  96. package/dist/render/shaders/depth.js +69 -3
  97. package/dist/render/shaders/exposure.d.ts +52 -0
  98. package/dist/render/shaders/exposure.js +124 -0
  99. package/dist/render/shaders/filmLook.d.ts +47 -0
  100. package/dist/render/shaders/filmLook.js +98 -0
  101. package/dist/render/shaders/flat/directionalShadow.js +35 -4
  102. package/dist/render/shaders/flat/driftLight.d.ts +29 -0
  103. package/dist/render/shaders/flat/driftLight.js +75 -0
  104. package/dist/render/shaders/flat/index.js +5 -0
  105. package/dist/render/shaders/flat/lobes.js +13 -0
  106. package/dist/render/shaders/flat/main.d.ts +1 -1
  107. package/dist/render/shaders/flat/main.js +68 -9
  108. package/dist/render/shaders/flat/preamble.js +23 -5
  109. package/dist/render/shaders/flat/probeGrid.js +46 -3
  110. package/dist/render/shaders/generated/ambientOcclusion.wgsl.d.ts +8 -3
  111. package/dist/render/shaders/generated/ambientOcclusion.wgsl.js +8 -3
  112. package/dist/render/shaders/generated/depth.wgsl.d.ts +100 -0
  113. package/dist/render/shaders/generated/depth.wgsl.js +100 -0
  114. package/dist/render/shaders/generated/exposure.wgsl.d.ts +42 -0
  115. package/dist/render/shaders/generated/exposure.wgsl.js +48 -0
  116. package/dist/render/shaders/generated/flat.wgsl.d.ts +1800 -1344
  117. package/dist/render/shaders/generated/flat.wgsl.js +1816 -1360
  118. package/dist/render/shaders/generated/localExposure.wgsl.d.ts +18 -0
  119. package/dist/render/shaders/generated/localExposure.wgsl.js +24 -0
  120. package/dist/render/shaders/generated/rush.wgsl.d.ts +40 -10
  121. package/dist/render/shaders/generated/rush.wgsl.js +40 -10
  122. package/dist/render/shaders/generated/temporalResolve.wgsl.d.ts +1 -1
  123. package/dist/render/shaders/generated/temporalResolve.wgsl.js +1 -1
  124. package/dist/render/shaders/gi/driftLight.wgsl.d.ts +16 -0
  125. package/dist/render/shaders/gi/driftLight.wgsl.js +49 -0
  126. package/dist/render/shaders/gi/probeBake.wgsl.d.ts +29 -4
  127. package/dist/render/shaders/gi/probeBake.wgsl.js +190 -20
  128. package/dist/render/shaders/globalMedium.js +4 -0
  129. package/dist/render/shaders/localExposure.d.ts +35 -0
  130. package/dist/render/shaders/localExposure.js +138 -0
  131. package/dist/render/shaders/prefilter.d.ts +1 -1
  132. package/dist/render/shaders/prefilter.js +6 -0
  133. package/dist/render/shaders/rush.js +33 -5
  134. package/dist/render/shaders/tangentFrame.d.ts +1 -1
  135. package/dist/render/shaders/tangentFrame.js +15 -3
  136. package/dist/render/shaders/temporalResolve.js +39 -1
  137. package/dist/render/shadowCasters.d.ts +5 -3
  138. package/dist/render/shadowMap.js +13 -0
  139. package/dist/render/surfaceTexture.d.ts +5 -0
  140. package/package.json +6 -6
  141. package/scripts/gpuCompute.mjs +24 -3
  142. package/scripts/png.mjs +23 -12
  143. package/src/cinematic/cameraPath.ts +178 -0
  144. package/src/environment/daylight.ts +152 -0
  145. package/src/geometry/meshConcat.ts +100 -0
  146. package/src/index.ts +32 -0
  147. package/src/render/ambientOcclusionPass.ts +5 -1
  148. package/src/render/backend/createRenderer.ts +22 -6
  149. package/src/render/backend/webgl2/renderer.ts +439 -20
  150. package/src/render/backend/webgpu/buffers.ts +51 -8
  151. package/src/render/backend/webgpu/depthPass.ts +78 -10
  152. package/src/render/backend/webgpu/exposurePass.ts +234 -0
  153. package/src/render/backend/webgpu/fieldCompose.ts +15 -0
  154. package/src/render/backend/webgpu/flatPass.ts +10 -2
  155. package/src/render/backend/webgpu/particlePass.ts +9 -0
  156. package/src/render/backend/webgpu/probeBake.ts +143 -9
  157. package/src/render/backend/webgpu/probeTargets.ts +211 -0
  158. package/src/render/backend/webgpu/renderer.ts +808 -273
  159. package/src/render/backend/webgpu/scatterPass.ts +7 -7
  160. package/src/render/backend/webgpu/skinPaletteRing.ts +10 -0
  161. package/src/render/backend/webgpu/skyPass.ts +5 -1
  162. package/src/render/backend/webgpu/uniformRing.ts +49 -3
  163. package/src/render/bloomChain.ts +9 -0
  164. package/src/render/clusteredLights.ts +108 -63
  165. package/src/render/cutoutCaster.ts +44 -0
  166. package/src/render/driftLight/bake.ts +125 -0
  167. package/src/render/driftLight/falloff.ts +26 -0
  168. package/src/render/driftLight/layout.ts +177 -0
  169. package/src/render/driftLight/lightField.ts +227 -0
  170. package/src/render/driftLight/uniforms.ts +61 -0
  171. package/src/render/driftLight/visibility.ts +58 -0
  172. package/src/render/emptyTexture.ts +41 -0
  173. package/src/render/exposurePass.ts +186 -0
  174. package/src/render/flameLight.ts +135 -0
  175. package/src/render/gi/bounceLights.ts +68 -0
  176. package/src/render/gi/fieldReach.ts +74 -0
  177. package/src/render/gi/globalField.ts +39 -0
  178. package/src/render/gi/probeTrace.ts +3 -1
  179. package/src/render/gpuCapability.ts +18 -5
  180. package/src/render/lightBudget.ts +12 -0
  181. package/src/render/pointLightSelection.ts +191 -79
  182. package/src/render/pointShadowBudget.ts +16 -8
  183. package/src/render/probeGrid.ts +13 -2
  184. package/src/render/probeSweeps.ts +80 -0
  185. package/src/render/reflectionProbe.ts +54 -2
  186. package/src/render/renderQuality.ts +3 -1
  187. package/src/render/rendererHarness.ts +25 -3
  188. package/src/render/sceneTarget.ts +75 -1
  189. package/src/render/shaders/ambientOcclusion.ts +205 -71
  190. package/src/render/shaders/cutoutCoverage.ts +43 -0
  191. package/src/render/shaders/depth.ts +80 -3
  192. package/src/render/shaders/exposure.ts +128 -0
  193. package/src/render/shaders/filmLook.ts +111 -0
  194. package/src/render/shaders/flat/directionalShadow.ts +35 -4
  195. package/src/render/shaders/flat/driftLight.ts +75 -0
  196. package/src/render/shaders/flat/index.ts +5 -0
  197. package/src/render/shaders/flat/lobes.ts +13 -0
  198. package/src/render/shaders/flat/main.ts +68 -9
  199. package/src/render/shaders/flat/preamble.ts +23 -5
  200. package/src/render/shaders/flat/probeGrid.ts +46 -3
  201. package/src/render/shaders/generated/ambientOcclusion.wgsl.ts +8 -3
  202. package/src/render/shaders/generated/depth.wgsl.ts +103 -0
  203. package/src/render/shaders/generated/exposure.wgsl.ts +51 -0
  204. package/src/render/shaders/generated/flat.wgsl.ts +1816 -1360
  205. package/src/render/shaders/generated/localExposure.wgsl.ts +26 -0
  206. package/src/render/shaders/generated/rush.wgsl.ts +40 -10
  207. package/src/render/shaders/generated/temporalResolve.wgsl.ts +1 -1
  208. package/src/render/shaders/gi/driftLight.wgsl.ts +49 -0
  209. package/src/render/shaders/gi/probeBake.wgsl.ts +191 -20
  210. package/src/render/shaders/globalMedium.ts +4 -0
  211. package/src/render/shaders/localExposure.ts +154 -0
  212. package/src/render/shaders/prefilter.ts +6 -0
  213. package/src/render/shaders/rush.ts +33 -5
  214. package/src/render/shaders/tangentFrame.ts +15 -3
  215. package/src/render/shaders/temporalResolve.ts +39 -1
  216. package/src/render/shadowCasters.ts +5 -3
  217. package/src/render/shadowMap.ts +14 -0
  218. package/src/render/surfaceTexture.ts +5 -0
@@ -25,13 +25,23 @@ import { glslIsFarDepth, glslSceneDepthToNdc } from '../depthConvention.js';
25
25
  * wherever the depth test cuts the window short, as a dotted line along every edge.
26
26
  */
27
27
  /**
28
- * The estimate. Writes one channel: 1 is open sky, 0 is fully enclosed.
28
+ * The estimate. Writes one channel at `AO_STORE`: that is open sky, 0 is fully enclosed, and the
29
+ * second axis of the blur decodes it.
29
30
  *
30
- * The estimator is the Alchemy/scalable-AO one — for each neighbour, the vector v from this
31
- * surface to it, weighted max(0, v·n) / (v·v). Two properties earn it its place over a
32
- * hemisphere of offset points: nothing has to be oriented, so there is no tangent frame and
33
- * no rotation matrix per pixel, and the 1 / v·v term falls away on its own, so a neighbour
34
- * further off contributes less without a second range test doing it by hand.
31
+ * **Horizons, not neighbours: ground-truth ambient occlusion (Jimenez et al. 2016).** Each pixel
32
+ * walks two lines across the screen, both ways, and keeps the highest thing it can see along each
33
+ * half: the horizon. The sky left between the two horizons of a line, weighted by the cosine to the
34
+ * surface's own normal, is that line's visibility in closed form, and two lines a pixel with the
35
+ * rotation tile below turning them is the hemisphere. It replaced a sum over twelve neighbours that
36
+ * counted anything within the radius as a blocker, however thin: cloth hanging a hand in front of a
37
+ * column darkened the column behind it, which read as a halo round every curtain in a courtyard.
38
+ *
39
+ * **What stops the halo is two rules about the horizon**, both the paper's. A sample fades out of
40
+ * the horizon as it approaches the radius, so a far wall does not shade a floor; and once the walk
41
+ * has passed the highest sample, the horizon sinks back toward what it sees next, so a thin thing
42
+ * in front raises it only while the walk is behind that thing. What that gives up is the shade under
43
+ * a thick object seen edge on, which reads as a thin one; the thickness is a constant because depth
44
+ * alone cannot say how deep anything is.
35
45
  */
36
46
  export const AO_FRAG = `#version 300 es
37
47
  precision highp float;
@@ -68,16 +78,18 @@ uniform float uRadius;
68
78
 
69
79
  out float fragColor;
70
80
 
71
- /** How many depths each pixel compares itself against. Fixed, so the cost is fixed. */
72
- const int AO_TAPS = 12;
81
+ /** Lines across the screen a pixel walks, each both ways. Fixed, so the cost is fixed. */
82
+ const int AO_SLICES = 2;
83
+ /** Depth samples each way along a line: two lines, two ways, six steps is 24 a pixel. */
84
+ const int AO_STEPS = 6;
73
85
  /**
74
- * The golden angle, in radians.
86
+ * How far the horizon sinks back toward each later sample once the walk has passed the highest one.
75
87
  *
76
- * Successive taps turned by it never line up into spokes, which a rational fraction of a
77
- * turn does: a kernel that repeats a direction is blind along it, and it is blind the same
78
- * way in every pixel, so it reads as a pattern in the surface rather than as noise.
88
+ * The paper's thickness heuristic. At zero anything within the radius shades for good, which is the
89
+ * halo; at one a horizon lasts one sample and a real corner loses its shade. A fifth keeps a corner,
90
+ * whose samples keep rising, and lets a curtain's shade drop within the few samples past its edge.
79
91
  */
80
- const float AO_GOLDEN_ANGLE = 2.39996323;
92
+ const float AO_THIN = 0.2;
81
93
  /**
82
94
  * How far the sampling may reach across the frame, whatever the depth says.
83
95
  *
@@ -86,25 +98,6 @@ const float AO_GOLDEN_ANGLE = 2.39996323;
86
98
  * being contact shading and becomes an expensive wide darkening. 6% of the frame.
87
99
  */
88
100
  const float AO_MAX_REACH_UV = 0.06;
89
- /**
90
- * Self-occlusion guard, as a fraction of the pixel's own view depth.
91
- *
92
- * Depth is quantised, so a flat surface samples itself very slightly in front of itself and
93
- * every flat surface in the world comes back faintly dirty. The error grows with distance,
94
- * which is why the guard is proportional to depth rather than a constant.
95
- */
96
- const float AO_BIAS = 0.004;
97
- /**
98
- * The floor under the 1 / (v·v) weighting, in square metres.
99
- *
100
- * Not a guard against dividing by zero, which is what a value near the float epsilon would
101
- * be. The nearest taps land about a centimetre away, where v·v is 1e-4 and the weight is ten
102
- * thousand — so a depth quantised to a millimetre yields several units of occlusion out of
103
- * nothing at all. Measured that way first, with 1e-4 here: a flat floor came back as static.
104
- * A hundredth of a square metre is a tenth of a metre of separation, which is the scale
105
- * below which two samples are the same surface rather than one occluding the other.
106
- */
107
- const float AO_EPSILON = 0.01;
108
101
  /**
109
102
  * How many times further one neighbour may be than the other before the pair straddles an edge.
110
103
  *
@@ -125,6 +118,8 @@ const float AO_EDGE_RATIO = 8.0;
125
118
  * and far below the step at any edge worth shading — a kerb against a deck is metres of it.
126
119
  */
127
120
  const float AO_EDGE_FLOOR = 0.05;
121
+ /** Stored at half, so a turn the closed form credits past one reaches the blur whole. See AO_STORE in the module. */
122
+ const float AO_STORE = 0.5;
128
123
 
129
124
  /** View-space position of whatever is at this pixel, from its depth alone. */
130
125
  vec3 viewPosition(vec2 uv, float depth) {
@@ -133,6 +128,17 @@ vec3 viewPosition(vec2 uv, float depth) {
133
128
  return view.xyz / view.w;
134
129
  }
135
130
 
131
+ /**
132
+ * The cosine-weighted sky between two horizon angles h0 < 0 < h1, measured from the eye, for a
133
+ * normal at the angle given in the same plane: the paper's closed form. 1 for an open plane facing
134
+ * the eye.
135
+ */
136
+ float gtaoArc(float h0, float h1, float normal) {
137
+ float c = cos(normal);
138
+ float s2 = 2.0 * sin(normal);
139
+ return 0.25 * (-cos(2.0 * h0 - normal) + c + h0 * s2) + 0.25 * (-cos(2.0 * h1 - normal) + c + h1 * s2);
140
+ }
141
+
136
142
  void main() {
137
143
  /*
138
144
  * One output pixel, in UV, along each axis.
@@ -214,15 +220,16 @@ void main() {
214
220
  vec3 n = normalize(cross(dpdx, dpdy));
215
221
  /*
216
222
  * Turned to face the eye. Which sign the cross product comes out with is a fact about the
217
- * screen-space winding and the handedness of the projection, and a normal pointing away
218
- * from the camera makes every v·n come back zero, which is a frame with no occlusion in it
219
- * anywhere. In view space the eye is the origin, so a normal facing it satisfies n·p < 0.
223
+ * screen-space winding and the handedness of the projection, and a normal pointing away from the
224
+ * camera puts every horizon behind the surface. In view space the eye is the origin, so a normal
225
+ * facing it satisfies n·p < 0.
220
226
  */
221
227
  if (dot(n, p) > 0.0) n = -n;
228
+ vec3 toEye = normalize(-p);
222
229
 
223
230
  /*
224
- * The radius as a fraction of the frame. A metre subtends less of the screen the further
225
- * away it is, which is the entire reason this is computed per pixel rather than handed in.
231
+ * The radius as a fraction of the frame. A metre subtends less of the screen the further away it
232
+ * is, which is the entire reason this is computed per pixel rather than handed in.
226
233
  */
227
234
  vec2 reach = min(
228
235
  uRadius * uProjScale * 0.5 / max(-p.z, 1e-3),
@@ -230,41 +237,77 @@ void main() {
230
237
  );
231
238
 
232
239
  /*
233
- * A rotation that repeats every four pixels: sixteen distinct turns laid out over a 4x4
234
- * tile. Paired with the four-wide blur that follows, every blurred pixel averages all
235
- * sixteen, which is what makes twelve taps enough. A per-pixel hash would decorrelate
236
- * neighbours better in principle and is worse here, because the blur can then only soften
237
- * the noise rather than complete the kernel.
240
+ * A rotation that repeats every four pixels: sixteen turns laid out over a 4x4 tile, spanning the
241
+ * quarter turn between one line and the next, so the eight-wide blur that follows averages every
242
+ * turn of both lines. The first step's distance is staggered by the same tile, so neighbouring
243
+ * pixels do not all sample the same rings either.
238
244
  */
239
245
  float tile = mod(gl_FragCoord.x, 4.0) + 4.0 * mod(gl_FragCoord.y, 4.0);
240
- float turn = tile * (6.2831853 / 16.0);
241
-
242
- float sum = 0.0;
243
- for (int i = 0; i < AO_TAPS; i++) {
244
- float t = (float(i) + 0.5) / float(AO_TAPS);
245
- float angle = turn + float(i) * AO_GOLDEN_ANGLE;
246
- /* The square root spreads the taps evenly over the disk; without it they crowd the
247
- centre, where they are measuring the pixel's own depth. */
248
- vec2 uv = vUv + vec2(cos(angle), sin(angle)) * sqrt(t) * reach;
246
+ float turn = tile * (1.5707963 / 16.0);
247
+ float stagger = fract(tile * 0.618034);
248
+ /* One output pixel along each axis, in UV. */
249
+ vec2 pixel = vec2(length(stepX), length(stepY));
249
250
 
251
+ float visible = 0.0;
252
+ for (int slice = 0; slice < AO_SLICES; slice++) {
253
+ float angle = turn + float(slice) * (3.1415927 / float(AO_SLICES));
250
254
  /*
251
- * textureLod, not texture, and it is the rule rather than a preference: this sample sits
252
- * inside a loop whose iterations a compiler cannot prove uniform, and an implicit
253
- * derivative there is what lets one flatten the body and pay for every arm. The fetch is
254
- * identical — the depth texture has one storage level and NEAREST filters, so there is no
255
- * mip for a derivative to select. See AGENTS.md, 2026-08-07.
255
+ * **Even in pixels, not in UV.** A UV step across a wide frame is longer than one down it, so
256
+ * turns spread evenly in UV crowd toward the horizontal once they reach the screen, and on a
257
+ * floor the horizontal lines are the ones that see least of its normal: every flat floor and
258
+ * ceiling came back at 0.82 open, measured, where the construction gives 0.97. A pixel is as
259
+ * wide as it is tall in view space, so a direction even in pixels is even in the world. It is
260
+ * scaled here into UV, and the reach is taken down the frame, whose UV is the same pixels.
256
261
  */
257
- float sampled = textureLod(uDepth, uv, 0.0).r;
258
- if (${glslIsFarDepth('sampled')}) continue;
262
+ vec2 screen = vec2(cos(angle) * pixel.x / pixel.y, sin(angle));
263
+ /*
264
+ * The line in view space, taken through the same reconstruction the samples use: a step along
265
+ * the screen at this pixel's own depth, so the samples lie in the plane the arc is measured in
266
+ * whatever the frame's aspect and whichever way a backend's clip space counts up.
267
+ */
268
+ vec3 along = normalize(viewPosition(vUv + screen * 1e-3, depth) - p);
269
+ vec3 across = normalize(along - toEye * dot(along, toEye));
270
+ vec3 axis = cross(across, toEye);
271
+ /* The normal projected into the line's plane, and its angle from the eye toward +across. */
272
+ vec3 projected = n - axis * dot(n, axis);
273
+ float projectedLength = length(projected);
274
+ if (projectedLength < 1e-4) continue;
275
+ float normalAngle = atan(dot(projected, across), dot(projected, toEye));
259
276
 
260
- vec3 v = viewPosition(uv, sampled) - p;
261
- float vv = dot(v, v);
262
- /* Past the radius it is a different surface rather than a neighbour of this one. */
263
- if (vv > uRadius * uRadius) continue;
264
- sum += max(0.0, dot(v, n) - AO_BIAS * (-p.z)) / (vv + AO_EPSILON);
277
+ /* The highest cosine seen each way. */
278
+ float horizon0 = -1.0;
279
+ float horizon1 = -1.0;
280
+ for (int k = 0; k < AO_STEPS; k++) {
281
+ float t = (float(k) + stagger) / float(AO_STEPS);
282
+ /* Squared, so the steps crowd the pixel, where contact shade lives. */
283
+ vec2 stride = screen * reach.y * (t * t + 0.5 / float(AO_STEPS * AO_STEPS));
284
+ for (int side = 0; side < 2; side++) {
285
+ vec2 uv = side == 0 ? vUv - stride : vUv + stride;
286
+ /*
287
+ * textureLod, not texture, and it is the rule rather than a preference: this sits inside a
288
+ * loop whose iterations a compiler cannot prove uniform. The depth texture has one level and
289
+ * NEAREST filters, so the fetch is identical. See AGENTS.md, 2026-08-07.
290
+ */
291
+ float sampled = textureLod(uDepth, uv, 0.0).r;
292
+ if (${glslIsFarDepth('sampled')}) continue;
293
+ vec3 v = viewPosition(uv, sampled) - p;
294
+ float distance2 = dot(v, v);
295
+ if (distance2 < 1e-8) continue;
296
+ float cosine = dot(v, toEye) * inversesqrt(distance2);
297
+ /* Faded to nothing at the radius: a far wall is not an occluder of this surface. */
298
+ float fade = clamp(1.0 - distance2 / (uRadius * uRadius), 0.0, 1.0);
299
+ cosine = mix(-1.0, cosine, fade);
300
+ if (side == 0) horizon0 = cosine > horizon0 ? cosine : mix(horizon0, cosine, AO_THIN);
301
+ else horizon1 = cosine > horizon1 ? cosine : mix(horizon1, cosine, AO_THIN);
302
+ }
303
+ }
304
+ /* Angles from the eye, negative toward -across, and never below the surface's own tangent. */
305
+ float h0 = max(-acos(clamp(horizon0, -1.0, 1.0)), normalAngle - 1.5707963);
306
+ float h1 = min(acos(clamp(horizon1, -1.0, 1.0)), normalAngle + 1.5707963);
307
+ visible += projectedLength * gtaoArc(h0, h1, normalAngle);
265
308
  }
266
309
 
267
- fragColor = clamp(1.0 - 2.0 * uRadius * sum / float(AO_TAPS), 0.0, 1.0);
310
+ fragColor = clamp(visible / float(AO_SLICES) * AO_STORE, 0.0, 1.0);
268
311
  }
269
312
  `;
270
313
  /**
@@ -293,12 +336,14 @@ uniform vec2 uStep;
293
336
  * asks that question five times per pixel.
294
337
  */
295
338
  uniform vec4 uDepthToViewZ;
339
+ /** One across, carrying the estimate's AO_STORE scale; its inverse down, decoding it. */
340
+ uniform float uAoScale;
296
341
 
297
342
  out float fragColor;
298
343
 
299
344
  /**
300
- * How far apart two pixels may be, as a fraction of their distance from the eye, before they
301
- * stop being the same surface.
345
+ * How far a neighbour may sit from the depth this pixel's own surface predicts there, as a
346
+ * fraction of that depth, before it stops being the same surface.
302
347
  *
303
348
  * Relative rather than absolute, because the same 5 cm step is a cliff on a dashboard and
304
349
  * nothing at all on a far wall.
@@ -311,8 +356,19 @@ float viewZ(float depth) {
311
356
  }
312
357
 
313
358
  void main() {
314
- float centre = viewZ(textureLod(uDepth, vUv, 0.0).r);
315
- float tolerance = AO_BLUR_DEPTH_TOLERANCE * abs(centre) + 1e-4;
359
+ float depth = textureLod(uDepth, vUv, 0.0).r;
360
+ float own = textureLod(uAo, vUv, 0.0).r;
361
+ /* Nothing to average over the sky, and no surface to predict one from. */
362
+ if (${glslIsFarDepth('depth')}) {
363
+ fragColor = min(own * uAoScale, 1.0);
364
+ return;
365
+ }
366
+ float centre = viewZ(depth);
367
+ /* Each tap is compared with this pixel's plane, not with its depth. See blurWeight in the module. */
368
+ float inverse = 1.0 / centre;
369
+ float stepAfter = 1.0 / viewZ(textureLod(uDepth, vUv + uStep, 0.0).r) - inverse;
370
+ float stepBefore = inverse - 1.0 / viewZ(textureLod(uDepth, vUv - uStep, 0.0).r);
371
+ float slope = abs(stepAfter) < abs(stepBefore) ? stepAfter : stepBefore;
316
372
  float sum = 0.0;
317
373
  float weight = 0.0;
318
374
  /*
@@ -340,13 +396,74 @@ void main() {
340
396
  for (int i = -3; i <= 4; i++) {
341
397
  vec2 uv = vUv + uStep * float(i);
342
398
  float z = viewZ(textureLod(uDepth, uv, 0.0).r);
343
- /* One if this neighbour is on the same surface, falling to zero across an edge. */
344
- float w = max(0.0, 1.0 - abs(z - centre) / tolerance);
399
+ /* One if this neighbour is on this pixel's surface, falling to zero as it leaves it. */
400
+ float w = max(0.0, 1.0 - abs(z * (inverse + slope * float(i)) - 1.0) / AO_BLUR_DEPTH_TOLERANCE);
345
401
  sum += textureLod(uAo, uv, 0.0).r * w;
346
402
  weight += w;
347
403
  }
348
404
  /* An isolated pixel — a thin rail against a far wall — keeps its own estimate rather than
349
405
  dividing by nothing. */
350
- fragColor = weight > 0.0 ? sum / weight : textureLod(uAo, vUv, 0.0).r;
406
+ float blurred = weight > 0.0 ? sum / weight : own;
407
+ fragColor = min(blurred * uAoScale, 1.0);
351
408
  }
352
409
  `;
410
+ /**
411
+ * The estimate's closed form, written out once more so a test can pin it to hand-derived values:
412
+ * the cosine-weighted sky between horizon angles `h0 < 0 < h1` for a normal at `normal`, all in
413
+ * radians from the eye within one line's plane.
414
+ */
415
+ export function gtaoArc(h0, h1, normal) {
416
+ const c = Math.cos(normal);
417
+ const s2 = 2 * Math.sin(normal);
418
+ return (0.25 * (-Math.cos(2 * h0 - normal) + c + h0 * s2) +
419
+ 0.25 * (-Math.cos(2 * h1 - normal) + c + h1 * s2));
420
+ }
421
+ /** One step of a horizon: up to a higher sample, or a fifth of the way down toward a lower one. */
422
+ export function nextHorizon(horizon, cosine) {
423
+ return cosine > horizon ? cosine : horizon + (cosine - horizon) * AO_THIN;
424
+ }
425
+ /** The shader's thickness heuristic. See `AO_THIN` inside `AO_FRAG`. */
426
+ export const AO_THIN = 0.2;
427
+ /**
428
+ * What the estimate is multiplied by before it is stored, and divided by once the blur is done.
429
+ *
430
+ * **Half, because an unblurred estimate is allowed past one.** A pixel's two lines see a plane
431
+ * turned away from the eye from two directions, and the closed form credits one of them more than
432
+ * the whole sky and the other less: it is the average over every turn that is exactly one, and the
433
+ * average is what the blur is for. Stored as it came, an eight-bit target clipped every turn above
434
+ * one before the blur could average it, and every surface seen edge on came back a few percent
435
+ * shaded with nothing on it. A line reaches π/2 at most, so half fits the whole range.
436
+ *
437
+ * What it gives up is one bit of the intermediate: steps of 1/128 rather than 1/256 until the
438
+ * second axis decodes, which the eight taps a pixel averages carry below what a frame can show.
439
+ */
440
+ export const AO_STORE = 0.5;
441
+ /** How far a neighbour may sit from this pixel's surface, as a fraction of its distance. */
442
+ export const AO_BLUR_DEPTH_TOLERANCE = 0.02;
443
+ /**
444
+ * The blur's weight for a neighbour `offset` pixels along the axis, from view depths: this pixel's
445
+ * own, the two either side of it, and the neighbour's. The shader's lines written out again.
446
+ *
447
+ * **The neighbour is compared with this pixel's plane, not with this pixel's depth.** Comparing
448
+ * depths refused every neighbour of a surface seen edge on: a floor forty metres out changes its
449
+ * depth by four percent a row, twice the tolerance, so the pass down the frame kept the centre alone
450
+ * and left the rotation tile's rows standing — four-pixel stripes across every ramp and far floor,
451
+ * reported as strips on a track. On a plane 1/z is affine in the screen, so one step of it predicts
452
+ * the plane's depth at every tap exactly, and a tap is refused only for leaving the plane.
453
+ *
454
+ * The step is taken from whichever neighbour is closer to the centre in 1/z. At a silhouette that is
455
+ * the one on this pixel's own side, so the far surface cannot bend the prediction toward it; on a
456
+ * plane the two agree to a quantisation step, which moves a weight by nothing a frame shows. What it
457
+ * gives up is a crease: past the fold between a floor and a wall the prediction runs on along the
458
+ * floor, so the wall's taps fade out over the tolerance rather than being kept.
459
+ *
460
+ * **Written here rather than in the shader**, because a comment inside the GLSL is part of the string
461
+ * a consumer ships.
462
+ */
463
+ export function blurWeight(centre, before, after, neighbour, offset) {
464
+ const inverse = 1 / centre;
465
+ const stepAfter = 1 / after - inverse;
466
+ const stepBefore = inverse - 1 / before;
467
+ const slope = Math.abs(stepAfter) < Math.abs(stepBefore) ? stepAfter : stepBefore;
468
+ return Math.max(0, 1 - Math.abs(neighbour * (inverse + slope * offset) - 1) / AO_BLUR_DEPTH_TOLERANCE);
469
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * A cutout that keeps its coverage down the mip chain.
3
+ *
4
+ * **Averaging alpha thins everything that is thinner than a texel.** Each level of a mip chain is
5
+ * the mean of four texels of the one above, so a needle one texel wide on a clear background is 0.5
6
+ * at the next level, 0.25 at the one after, and gone against a cutoff of 0.5 two levels down. A
7
+ * tree that is dense up close then stands bare a few metres off, which on a bought courtyard was a
8
+ * cypress drawn as bare branches at 1280 pixels across and in full leaf at 2000.
9
+ *
10
+ * **So the test scales alpha up by the level it is sampling**: a quarter more per level, the figure
11
+ * Ben Golus measured on foliage in "Anti-aliased Alpha Test" (2017). Only the test sees the scaled
12
+ * value; what a translucent draw blends with is the texture's own alpha, and a material with no
13
+ * cutoff is untouched. What it gives up is exactness: coverage is held roughly rather than measured
14
+ * per image, which Ignacio Castaño's method would do at upload from a readback this engine does not
15
+ * make. What would make it wrong is an image whose mips were built to keep coverage already, which
16
+ * this would then thicken.
17
+ *
18
+ * The level is estimated from the texture coordinate's screen derivatives, the isotropic formula;
19
+ * a sampler with anisotropic filtering reads a lower level at a grazing angle, so there a cutout
20
+ * comes out slightly fuller than its own mip would have made it.
21
+ */
22
+ /** How much more alpha each mip level down is credited with in the test. */
23
+ export declare const CUTOUT_MIP_SCALE = 0.25;
24
+ /**
25
+ * `cutoutAlpha(alpha, texels)`: the alpha the cutout test should see, for a sample at `texels`, the
26
+ * texture coordinate times the texture's size. Called in uniform control flow, since it takes
27
+ * derivatives.
28
+ */
29
+ export declare const CUTOUT_COVERAGE_GLSL: string;
30
+ /** The same, given the level, for a test to pin: what the shader compares against the cutoff. */
31
+ export declare function cutoutAlpha(alpha: number, level: number): number;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * A cutout that keeps its coverage down the mip chain.
3
+ *
4
+ * **Averaging alpha thins everything that is thinner than a texel.** Each level of a mip chain is
5
+ * the mean of four texels of the one above, so a needle one texel wide on a clear background is 0.5
6
+ * at the next level, 0.25 at the one after, and gone against a cutoff of 0.5 two levels down. A
7
+ * tree that is dense up close then stands bare a few metres off, which on a bought courtyard was a
8
+ * cypress drawn as bare branches at 1280 pixels across and in full leaf at 2000.
9
+ *
10
+ * **So the test scales alpha up by the level it is sampling**: a quarter more per level, the figure
11
+ * Ben Golus measured on foliage in "Anti-aliased Alpha Test" (2017). Only the test sees the scaled
12
+ * value; what a translucent draw blends with is the texture's own alpha, and a material with no
13
+ * cutoff is untouched. What it gives up is exactness: coverage is held roughly rather than measured
14
+ * per image, which Ignacio Castaño's method would do at upload from a readback this engine does not
15
+ * make. What would make it wrong is an image whose mips were built to keep coverage already, which
16
+ * this would then thicken.
17
+ *
18
+ * The level is estimated from the texture coordinate's screen derivatives, the isotropic formula;
19
+ * a sampler with anisotropic filtering reads a lower level at a grazing angle, so there a cutout
20
+ * comes out slightly fuller than its own mip would have made it.
21
+ */
22
+ /** How much more alpha each mip level down is credited with in the test. */
23
+ export const CUTOUT_MIP_SCALE = 0.25;
24
+ /**
25
+ * `cutoutAlpha(alpha, texels)`: the alpha the cutout test should see, for a sample at `texels`, the
26
+ * texture coordinate times the texture's size. Called in uniform control flow, since it takes
27
+ * derivatives.
28
+ */
29
+ export const CUTOUT_COVERAGE_GLSL = `
30
+ float cutoutAlpha(float alpha, vec2 texels) {
31
+ vec2 dx = dFdx(texels);
32
+ vec2 dy = dFdy(texels);
33
+ float level = max(0.0, 0.5 * log2(max(dot(dx, dx), dot(dy, dy))));
34
+ return alpha * (1.0 + level * ${CUTOUT_MIP_SCALE.toFixed(2)});
35
+ }
36
+ `;
37
+ /** The same, given the level, for a test to pin: what the shader compares against the cutoff. */
38
+ export function cutoutAlpha(alpha, level) {
39
+ return alpha * (1 + Math.max(0, level) * CUTOUT_MIP_SCALE);
40
+ }
@@ -11,4 +11,29 @@ export declare const DEPTH_SKINNED_VERT: string;
11
11
  * a missing variant, and is the shape the per-draw ring exists to prevent one level up.
12
12
  */
13
13
  export declare const DEPTH_INSTANCED_VERT: string;
14
+ /**
15
+ * The variants a cutout caster casts through: a leaf card, a chain link, a fence, whose shape is in
16
+ * its texture's alpha rather than in its geometry.
17
+ *
18
+ * **Variants, not a uniform gate on the plain program**, for this file's own reason: a gate would
19
+ * need the UV, a varying and a bound texture on every opaque caster, and a branch on a varying is
20
+ * not provably uniform, which forbids the implicit derivative the sample below wants. A variant
21
+ * leaves every opaque caster byte for byte as it was. What it costs is one more program each, and
22
+ * a skinned cutout, which nothing has asked for, is not among them.
23
+ */
24
+ export declare const DEPTH_CUTOUT_VERT: string;
25
+ export declare const DEPTH_INSTANCED_CUTOUT_VERT: string;
14
26
  export declare const DEPTH_FRAG = "#version 300 es\nprecision highp float;\nin vec4 vLightPosition;\n\nuniform highp sampler2D uPreviousShadowMap;\nuniform int uPeelShadowLayer;\n\nvoid main() {\n if (uPeelShadowLayer != 0) {\n vec3 p = vLightPosition.xyz / vLightPosition.w;\n vec2 uv = p.xy * 0.5 + 0.5;\n float previousDepth = texture(uPreviousShadowMap, uv).r;\n // Discard the already-recorded surface and everything in front of it. The\n // ordinary depth test then stores the next independently fading occluder.\n if (gl_FragCoord.z <= previousDepth + 0.00001) discard;\n }\n}\n";
27
+ /**
28
+ * The cutout caster's depth: the plain pass, and a texel of alpha under the cutoff writes nothing.
29
+ *
30
+ * **The sample is the first statement, in uniform control flow**, so its implicit derivative picks
31
+ * the mip the shadow map's texel footprint wants; inside a branch it would be undefined (AGENTS.md,
32
+ * 2026-08-07). The peel's own sample becomes an explicit-level fetch for the same reason, and is
33
+ * bit for bit what it was: that map has one level and a nearest sampler. The discard is last, so
34
+ * nothing samples after it.
35
+ *
36
+ * `uCutoutMap` is declared after `uPreviousShadowMap`, so the peel keeps its bindings and the cutout
37
+ * map takes the next.
38
+ */
39
+ export declare const DEPTH_CUTOUT_FRAG: string;
@@ -1,4 +1,5 @@
1
1
  import { resolveConditionals } from './conditionals.js';
2
+ import { CUTOUT_COVERAGE_GLSL } from './cutoutCoverage.js';
2
3
  import { SKINNING_GLSL } from './skinning.js';
3
4
  import { CHANNEL_ATTRIBUTE, CHANNEL_BEND } from './vertexChannel.js';
4
5
  /**
@@ -55,7 +56,22 @@ ${CHANNEL_ATTRIBUTE}
55
56
  ${CHANNEL_BEND}
56
57
  #endif
57
58
 
59
+ #if CUTOUT
60
+ /*
61
+ * **What a cutout caster adds, and where.** The UV at location 5, where every mesh carries it; two
62
+ * uniforms declared after every other, so no field of the plain variant moves and a binder written
63
+ * against it stays right for this one; and two outputs after vLightPosition, which keeps location 0.
64
+ */
65
+ layout(location = 5) in vec2 aUv;
66
+ uniform vec2 uUvScale;
67
+ uniform float uAlphaCutout;
68
+ #endif
69
+
58
70
  out vec4 vLightPosition;
71
+ #if CUTOUT
72
+ out vec2 vUv;
73
+ flat out float vAlphaCutout;
74
+ #endif
59
75
 
60
76
  void main() {
61
77
  #if SKINNED
@@ -79,12 +95,16 @@ void main() {
79
95
  #endif
80
96
  vLightPosition = uLightViewProj * vec4(bent, world.w);
81
97
  gl_Position = vLightPosition;
98
+ #if CUTOUT
99
+ vUv = aUv * uUvScale;
100
+ vAlphaCutout = uAlphaCutout;
101
+ #endif
82
102
  }
83
103
  `;
84
104
  /** The rigid variant: the world, a prop, a mesh with no rig behind it. */
85
- export const DEPTH_VERT = resolveConditionals(DEPTH_VERT_SOURCE, { SKINNED: false, INSTANCED: false }, 'depth');
105
+ export const DEPTH_VERT = resolveConditionals(DEPTH_VERT_SOURCE, { SKINNED: false, INSTANCED: false, CUTOUT: false }, 'depth');
86
106
  /** The skinned variant, which reads a joint palette and moves the vertex by it. */
87
- export const DEPTH_SKINNED_VERT = resolveConditionals(DEPTH_VERT_SOURCE, { SKINNED: true, INSTANCED: false }, 'depth-skinned');
107
+ export const DEPTH_SKINNED_VERT = resolveConditionals(DEPTH_VERT_SOURCE, { SKINNED: true, INSTANCED: false, CUTOUT: false }, 'depth-skinned');
88
108
  /**
89
109
  * The variant an instanced draw casts its shadow through.
90
110
  *
@@ -93,7 +113,19 @@ export const DEPTH_SKINNED_VERT = resolveConditionals(DEPTH_VERT_SOURCE, { SKINN
93
113
  * right place and the rest stacked underneath it, which reads as a lighting fault rather than as
94
114
  * a missing variant, and is the shape the per-draw ring exists to prevent one level up.
95
115
  */
96
- export const DEPTH_INSTANCED_VERT = resolveConditionals(DEPTH_VERT_SOURCE, { SKINNED: false, INSTANCED: true }, 'depth-instanced');
116
+ export const DEPTH_INSTANCED_VERT = resolveConditionals(DEPTH_VERT_SOURCE, { SKINNED: false, INSTANCED: true, CUTOUT: false }, 'depth-instanced');
117
+ /**
118
+ * The variants a cutout caster casts through: a leaf card, a chain link, a fence, whose shape is in
119
+ * its texture's alpha rather than in its geometry.
120
+ *
121
+ * **Variants, not a uniform gate on the plain program**, for this file's own reason: a gate would
122
+ * need the UV, a varying and a bound texture on every opaque caster, and a branch on a varying is
123
+ * not provably uniform, which forbids the implicit derivative the sample below wants. A variant
124
+ * leaves every opaque caster byte for byte as it was. What it costs is one more program each, and
125
+ * a skinned cutout, which nothing has asked for, is not among them.
126
+ */
127
+ export const DEPTH_CUTOUT_VERT = resolveConditionals(DEPTH_VERT_SOURCE, { SKINNED: false, INSTANCED: false, CUTOUT: true }, 'depth-cutout');
128
+ export const DEPTH_INSTANCED_CUTOUT_VERT = resolveConditionals(DEPTH_VERT_SOURCE, { SKINNED: false, INSTANCED: true, CUTOUT: true }, 'depth-instanced-cutout');
97
129
  export const DEPTH_FRAG = `#version 300 es
98
130
  precision highp float;
99
131
  in vec4 vLightPosition;
@@ -112,3 +144,37 @@ void main() {
112
144
  }
113
145
  }
114
146
  `;
147
+ /**
148
+ * The cutout caster's depth: the plain pass, and a texel of alpha under the cutoff writes nothing.
149
+ *
150
+ * **The sample is the first statement, in uniform control flow**, so its implicit derivative picks
151
+ * the mip the shadow map's texel footprint wants; inside a branch it would be undefined (AGENTS.md,
152
+ * 2026-08-07). The peel's own sample becomes an explicit-level fetch for the same reason, and is
153
+ * bit for bit what it was: that map has one level and a nearest sampler. The discard is last, so
154
+ * nothing samples after it.
155
+ *
156
+ * `uCutoutMap` is declared after `uPreviousShadowMap`, so the peel keeps its bindings and the cutout
157
+ * map takes the next.
158
+ */
159
+ export const DEPTH_CUTOUT_FRAG = `#version 300 es
160
+ precision highp float;
161
+ in vec4 vLightPosition;
162
+ in vec2 vUv;
163
+ flat in float vAlphaCutout;
164
+
165
+ uniform highp sampler2D uPreviousShadowMap;
166
+ uniform int uPeelShadowLayer;
167
+ uniform highp sampler2D uCutoutMap;
168
+ ${CUTOUT_COVERAGE_GLSL}
169
+ void main() {
170
+ /* Credited for its mip level as the surface's own test is, so a leaf casts the shape it draws. */
171
+ float alpha = cutoutAlpha(texture(uCutoutMap, vUv).a, vUv * vec2(textureSize(uCutoutMap, 0)));
172
+ if (uPeelShadowLayer != 0) {
173
+ vec3 p = vLightPosition.xyz / vLightPosition.w;
174
+ vec2 uv = p.xy * 0.5 + 0.5;
175
+ float previousDepth = textureLod(uPreviousShadowMap, uv, 0.0).r;
176
+ if (gl_FragCoord.z <= previousDepth + 0.00001) discard;
177
+ }
178
+ if (alpha < vAlphaCutout) discard;
179
+ }
180
+ `;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Eye adaptation: the frame's own brightness, measured on the GPU and followed over time.
3
+ *
4
+ * **A fixed exposure is right for one view and wrong for the next.** A courtyard with the sun on
5
+ * its upper walls and its floor in the shade of a 19 m wall is several stops darker at ground level
6
+ * than from the gallery, and an exposure set for one draws the other either clipped or black. A
7
+ * camera meters and an eye adapts; this does both, from the finished scene, with no readback.
8
+ *
9
+ * **Two passes, both tiny.** The meter writes a 32×32 grid, each texel the mean of 8×8 taps of log
10
+ * luminance over its share of the frame. The adaptation reads all 1,024 of them in one texel,
11
+ * averages them into the frame's log-mean luminance, and moves the held value toward it by a blend
12
+ * the caller's frame time decides. The composite reads the held value and scales the scene toward
13
+ * middle grey. Log luminance, because an eye's response is to ratios: one sunlit pixel beside a
14
+ * dark frame should not set the exposure the way an arithmetic mean lets it.
15
+ *
16
+ * What it gives up is metering by region: every pixel counts the same, so a small bright window in
17
+ * a dark room pulls less than a photographer's spot meter would. What would change it is a scene
18
+ * that needs a subject held at a brightness, and the answer then is a centre weight in the meter.
19
+ */
20
+ /** Texels a side in the meter's grid, and taps a side within each. */
21
+ export declare const METER_GRID = 32;
22
+ export declare const METER_TAPS = 8;
23
+ /** The luminance the meter clamps to, in stops: a black pixel is not minus infinity. */
24
+ export declare const METER_MIN_LOG2 = -12;
25
+ export declare const METER_MAX_LOG2 = 8;
26
+ /** What full adaptation brings the frame's log-mean luminance to: middle grey. */
27
+ export declare const ADAPTED_KEY = 0.18;
28
+ /** The furthest adaptation moves the exposure either way, in stops. A black loading screen is not a
29
+ * reason to multiply the next frame by a thousand. */
30
+ export declare const ADAPT_RANGE_STOPS = 6;
31
+ /**
32
+ * How fast the held brightness follows the measured one, per second: 1.5 closes about 78% of the
33
+ * way in a second. An eye takes longer to open than to close, and a camera's meter is faster than
34
+ * either; one rate is what a scene that cuts between shots can predict.
35
+ */
36
+ export declare const ADAPT_RATE = 1.5;
37
+ export declare const EXPOSURE_METER_FRAG: string;
38
+ export declare const EXPOSURE_ADAPT_FRAG: string;
39
+ /**
40
+ * `uAutoExposure`, `uExposureHeld` and `autoExposureGain`, for the composite. The gain scales scene
41
+ * light before the tone curve, toward the key by the fraction the caller asked for.
42
+ */
43
+ export declare const AUTO_EXPOSURE_GLSL: string;
44
+ /**
45
+ * The fraction of the way toward this frame's measurement to move, after `dtSec` seconds. A cut,
46
+ * or a first frame with nothing held, snaps.
47
+ */
48
+ export declare function adaptBlend(dtSec: number, snap: boolean): number;
49
+ /** The scene's gain for a held log-mean `heldLog2` at `strength`, as the composite computes it. */
50
+ export declare function autoExposureGain(heldLog2: number, strength: number): number;
51
+ /** An adaptation strength as the shader takes it: 0 to 1, anything else off. */
52
+ export declare function clampAutoExposure(strength: number): number;