@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,154 @@
1
+ /**
2
+ * Local exposure: each region of the frame brought part of the way toward the frame's own
3
+ * brightness, so shade and sun can both be read in one picture.
4
+ *
5
+ * **A tone curve maps one range, and a sunlit courtyard is two.** Measured in a bought courtyard at
6
+ * noon, the shaded arcades hold a fiftieth of the light the sunlit paving does, which is what the
7
+ * place is: a canyon lit mostly by its own walls. Exposed for the paving the arcades go to black,
8
+ * exposed for the arcades the paving goes to white, and no curve fixes both, because a curve is a
9
+ * function of a pixel's brightness and the same brightness has to mean shade in one place and sun in
10
+ * another. A photographer brackets; an eye adapts by region. This is the second.
11
+ *
12
+ * **A bilateral grid over the exposure meter's tiles.** The grid pass writes, for each of the
13
+ * meter's 32 × 32 tiles and each of ten two-stop bands of log luminance, the sum and the share of
14
+ * the tile's taps that fell in that band, and in an eleventh block the tile's mean. The composite
15
+ * reads the grid at the pixel's place and in the pixel's own band, filtered in both, so a shaded
16
+ * pixel beside sunlit ones takes the shade's level rather than the tile's: that is what keeps the
17
+ * lift from haloing across the edge of a shadow. The band's level is blended 0.4 toward the tile's
18
+ * mean, which is what stops a texture's own light and dark being flattened into one another.
19
+ *
20
+ * **The gain is `strength · (held − local)` stops**, clamped to three: at 0.5 a region five stops
21
+ * under the frame is lifted two and a half, and one two over is brought down one. The held value is
22
+ * eye adaptation's, so the frame as a whole still adapts and regions are moved relative to it.
23
+ *
24
+ * What it gives up is some of the contrast between regions, which is the point, and a halo a tile
25
+ * wide where two regions share a band and differ in brightness. What would make it wrong is a scene
26
+ * whose drama is the contrast itself: a single shaft into a dark room wants the room dark, and the
27
+ * strength is the caller's for that reason.
28
+ */
29
+ import { METER_GRID, METER_MAX_LOG2, METER_MIN_LOG2, METER_TAPS } from './exposure.ts';
30
+
31
+ /** Bands of log luminance, and how many stops each is wide. */
32
+ export const LOCAL_BINS = 10;
33
+ export const LOCAL_BIN_STOPS = (METER_MAX_LOG2 - METER_MIN_LOG2) / LOCAL_BINS;
34
+ /** The grid's width in texels: a block of tiles per band, and one for the tiles' means. */
35
+ export const LOCAL_GRID_WIDTH = METER_GRID * (LOCAL_BINS + 1);
36
+ /** How far a pixel's band level is blended toward its tile's mean. */
37
+ export const LOCAL_BLURRED_BLEND = 0.4;
38
+ /** The furthest a region is moved either way, in stops. */
39
+ export const LOCAL_RANGE_STOPS = 3;
40
+
41
+ const BLOCKS = (LOCAL_BINS + 1).toFixed(1);
42
+ const GRID = METER_GRID.toFixed(1);
43
+ const MIN = METER_MIN_LOG2.toFixed(1);
44
+ const MAX = METER_MAX_LOG2.toFixed(1);
45
+ const STOPS = LOCAL_BIN_STOPS.toFixed(1);
46
+ const LAST = (LOCAL_BINS - 1).toFixed(1);
47
+
48
+ /**
49
+ * The grid: one texel a tile a band. Each reads its tile's 8 × 8 taps, as the meter does, and keeps
50
+ * the ones in its band; the last block keeps them all. Sum and count are both divided by the taps,
51
+ * so the filtered pair in the composite is still a weighted mean.
52
+ */
53
+ export const EXPOSURE_LOCAL_FRAG = `#version 300 es
54
+ precision highp float;
55
+
56
+ in vec2 vUv;
57
+
58
+ /** The finished scene, in scene light, before any exposure or curve. */
59
+ uniform highp sampler2D uScene;
60
+
61
+ out vec2 fragColor;
62
+
63
+ void main() {
64
+ float uvBlock = vUv.x * ${BLOCKS};
65
+ float band = floor(uvBlock);
66
+ vec2 corner = vec2(fract(uvBlock), vUv.y) - vec2(0.5 / ${GRID});
67
+ float sum = 0.0;
68
+ float count = 0.0;
69
+ for (int y = 0; y < ${METER_TAPS}; y++) {
70
+ for (int x = 0; x < ${METER_TAPS}; x++) {
71
+ vec2 uv = corner + (vec2(float(x), float(y)) + 0.5) / ${(METER_GRID * METER_TAPS).toFixed(1)};
72
+ vec3 c = textureLod(uScene, uv, 0.0).rgb;
73
+ float level = clamp(log2(max(dot(c, vec3(0.2126, 0.7152, 0.0722)), 1e-8)), ${MIN}, ${MAX});
74
+ float tapBand = floor(clamp((level - ${MIN}) / ${STOPS}, 0.0, ${LAST}));
75
+ float kept = band > ${LAST} || tapBand == band ? 1.0 : 0.0;
76
+ sum += level * kept;
77
+ count += kept;
78
+ }
79
+ }
80
+ fragColor = vec2(sum, count) / ${(METER_TAPS * METER_TAPS).toFixed(1)};
81
+ }
82
+ `;
83
+
84
+ /**
85
+ * The composite's half. Needs `AUTO_EXPOSURE_GLSL` before it, for the held brightness.
86
+ *
87
+ * A block is read clamped half a texel inside its own tiles, so the filter never reaches into the
88
+ * neighbouring band's block. Every fetch is at level zero: the grid has one level, and the branch
89
+ * on the strength above them is uniform but the pixel's band is not.
90
+ */
91
+ export const LOCAL_EXPOSURE_GLSL = `
92
+ /** How far each region is brought toward the frame, 0 to 1. 0 is off and the whole of the off path. */
93
+ uniform float uLocalExposure;
94
+ /** The bilateral grid: a block of tiles per band of brightness, and one of the tiles' means. */
95
+ uniform highp sampler2D uExposureLocal;
96
+
97
+ vec2 localBlock(vec2 uv, float block) {
98
+ vec2 inside = clamp(uv, vec2(0.5 / ${GRID}), vec2(1.0 - 0.5 / ${GRID}));
99
+ return textureLod(uExposureLocal, vec2((block + inside.x) / ${BLOCKS}, inside.y), 0.0).rg;
100
+ }
101
+
102
+ float localExposureGain(vec2 uv, vec3 light) {
103
+ if (uLocalExposure <= 0.0) return 1.0;
104
+ float held = texelFetch(uExposureHeld, ivec2(0, 0), 0).r;
105
+ float pixel = clamp(log2(max(dot(light, vec3(0.2126, 0.7152, 0.0722)), 1e-8)), ${MIN}, ${MAX});
106
+ float b = (pixel - ${MIN}) / ${STOPS} - 0.5;
107
+ float lower = clamp(floor(b), 0.0, ${LAST});
108
+ float upper = min(lower + 1.0, ${LAST});
109
+ vec2 banded = mix(localBlock(uv, lower), localBlock(uv, upper), clamp(b - lower, 0.0, 1.0));
110
+ float tileMean = localBlock(uv, ${LOCAL_BINS.toFixed(1)}).r;
111
+ float bilateral = banded.y > 1e-3 ? banded.x / banded.y : tileMean;
112
+ float local = mix(bilateral, tileMean, ${LOCAL_BLURRED_BLEND.toFixed(1)});
113
+ float stops = uLocalExposure * (held - local);
114
+ return exp2(clamp(stops, -${LOCAL_RANGE_STOPS.toFixed(1)}, ${LOCAL_RANGE_STOPS.toFixed(1)}));
115
+ }
116
+ `;
117
+
118
+ const clamp = (value: number, low: number, high: number): number =>
119
+ Math.min(Math.max(value, low), high);
120
+
121
+ /** The two bands a pixel at `pixelLog2` reads, and how much of the upper, as the shader finds them. */
122
+ export function localBin(pixelLog2: number): { lower: number; upper: number; weight: number } {
123
+ const pixel = clamp(pixelLog2, METER_MIN_LOG2, METER_MAX_LOG2);
124
+ const b = (pixel - METER_MIN_LOG2) / LOCAL_BIN_STOPS - 0.5;
125
+ const lower = clamp(Math.floor(b), 0, LOCAL_BINS - 1);
126
+ return { lower, upper: Math.min(lower + 1, LOCAL_BINS - 1), weight: clamp(b - lower, 0, 1) };
127
+ }
128
+
129
+ /** A pixel's local level from its two bands' sums and counts and its tile's mean, as the shader. */
130
+ export function sliceLocal(
131
+ sumLower: number,
132
+ countLower: number,
133
+ sumUpper: number,
134
+ countUpper: number,
135
+ weight: number,
136
+ tileMean: number,
137
+ ): number {
138
+ const sum = sumLower + (sumUpper - sumLower) * weight;
139
+ const count = countLower + (countUpper - countLower) * weight;
140
+ const bilateral = count > 1e-3 ? sum / count : tileMean;
141
+ return bilateral + (tileMean - bilateral) * LOCAL_BLURRED_BLEND;
142
+ }
143
+
144
+ /** A region's gain for a local level and the frame's held one at `strength`, as the composite. */
145
+ export function localExposureGain(localLog2: number, heldLog2: number, strength: number): number {
146
+ if (!(strength > 0)) return 1;
147
+ const stops = strength * (heldLog2 - localLog2);
148
+ return 2 ** clamp(stops, -LOCAL_RANGE_STOPS, LOCAL_RANGE_STOPS);
149
+ }
150
+
151
+ /** A local exposure strength as the shader takes it: 0 to 1, anything else off. */
152
+ export function clampLocalExposure(strength: number): number {
153
+ return Number.isFinite(strength) ? clamp(strength, 0, 1) : 0;
154
+ }
@@ -39,6 +39,12 @@ import { OCTAHEDRAL_GLSL } from './octahedral.ts';
39
39
  */
40
40
  export const PREFILTER_FRAG = `#version 300 es
41
41
  precision highp float;
42
+ /*
43
+ * highp int as well, because the radical inverse below is a 32-bit bit reversal and the fragment
44
+ * default for int and uint is mediump. A driver that computes mediump in sixteen bits returns 0
45
+ * for every sample, and every cosine sample of the diffuse level lands on its own normal.
46
+ */
47
+ precision highp int;
42
48
 
43
49
  ${OCTAHEDRAL_GLSL}
44
50
 
@@ -22,9 +22,17 @@
22
22
  /* The vertex stage is `FULLSCREEN_VERT`, shared with the occlusion pass and its blur. */
23
23
  import { glslSceneDepthToNdc } from '../depthConvention.ts';
24
24
  import { DEPTH_OF_FIELD_GLSL } from './depthOfField.ts';
25
+ import { AUTO_EXPOSURE_GLSL } from './exposure.ts';
26
+ import { LOCAL_EXPOSURE_GLSL } from './localExposure.ts';
27
+ import { FILM_LOOK_GLSL } from './filmLook.ts';
25
28
 
26
29
  export const RUSH_FRAG = `#version 300 es
27
30
  precision highp float;
31
+ /*
32
+ * highp int as well, for the film grain's hash, which multiplies and shifts uints. The fragment
33
+ * default is mediump, and a sixteen-bit driver would hand back a hash of sixteen bits.
34
+ */
35
+ precision highp int;
28
36
 
29
37
  in vec2 vUv;
30
38
 
@@ -135,6 +143,11 @@ uniform float uBloomStrength;
135
143
  */
136
144
  uniform vec3 uVeilColor;
137
145
  uniform float uVeilAlpha;
146
+ ${FILM_LOOK_GLSL}
147
+
148
+ /* Eye adaptation's held brightness and the gain it asks for. See exposure.ts. */
149
+ ${AUTO_EXPOSURE_GLSL}
150
+ ${LOCAL_EXPOSURE_GLSL}
138
151
 
139
152
  /**
140
153
  * The colour grade: a lookup table over display values, sampled after the tone map.
@@ -293,6 +306,24 @@ vec3 cameraBlur(vec3 scene) {
293
306
  return sum / 9.0;
294
307
  }
295
308
 
309
+ /**
310
+ * Everything after the light is complete, in the order the header of each step argues: the lens
311
+ * darkens its corners in scene light, the curve and the grade turn light into a picture, the veil
312
+ * fades it, and the print's grain sits on what is left. One function, so the two exits of \`main\`
313
+ * cannot finish a frame differently.
314
+ */
315
+ vec3 finish(vec3 light) {
316
+ /*
317
+ * The eye's adaptation, in scene light and before the lens, as a camera's exposure would be; and
318
+ * each region's share of it, read from the light before either scales it, because that is the
319
+ * light the grid measured.
320
+ */
321
+ float local = localExposureGain(vUv, light);
322
+ light *= autoExposureGain() * local;
323
+ vec3 lensed = withVignette(light, vUv, vec2(textureSize(uScene, 0)));
324
+ return withGrain(withVeil(applyColourGrade(grade(lensed))), gl_FragCoord.xy);
325
+ }
326
+
296
327
  void main() {
297
328
  vec3 scene = textureLod(uScene, vUv, 0.0).rgb;
298
329
  if (uMotionStrength > 0.0) scene = cameraBlur(scene);
@@ -330,7 +361,7 @@ void main() {
330
361
  float edge = smoothstep(inner, 1.0, radius);
331
362
  float amount = edge * uStrength;
332
363
  if (amount <= 0.0) {
333
- fragColor = vec4(withVeil(applyColourGrade(grade(withBloom(scene * ao)))), 1.0);
364
+ fragColor = vec4(finish(withBloom(scene * ao)), 1.0);
334
365
  return;
335
366
  }
336
367
 
@@ -349,9 +380,6 @@ void main() {
349
380
  }
350
381
  vec3 blurred = sum / 7.0;
351
382
 
352
- fragColor = vec4(
353
- withVeil(applyColourGrade(grade(withBloom(mix(scene, blurred, amount) * ao)))),
354
- 1.0
355
- );
383
+ fragColor = vec4(finish(withBloom(mix(scene, blurred, amount) * ao)), 1.0);
356
384
  }
357
385
  `;
@@ -56,7 +56,9 @@ export const TANGENT_FRAME_GLSL = `mat3 tangentFrame(vec3 n, vec3 worldPos, vec2
56
56
  * generateTangents does the same per vertex and says why: dropping it is a normal map that
57
57
  * shears wherever the geometry curves.
58
58
  */
59
- vec3 attrT = normalize(tangent.xyz - n * dot(n, tangent.xyz));
59
+ vec3 along = tangent.xyz - n * dot(n, tangent.xyz);
60
+ float alongSq = dot(along, along);
61
+ vec3 attrT = along * inversesqrt(max(alongSq, 1e-12));
60
62
  /*
61
63
  * The bitangent's sign, which is the whole reason w is stored. An artist mapping the left and
62
64
  * right of a model onto one patch of texture gives one side a mirrored frame, and a bitangent
@@ -64,7 +66,17 @@ export const TANGENT_FRAME_GLSL = `mat3 tangentFrame(vec3 n, vec3 worldPos, vec2
64
66
  */
65
67
  vec3 attrB = cross(n, attrT) * tangent.w;
66
68
 
67
- vec3 t = hasTangents != 0 ? attrT : derivedT;
68
- vec3 b = hasTangents != 0 ? attrB : derivedB;
69
+ /*
70
+ * **And only where there is a frame to keep.** A tangent parallel to its normal, or two that
71
+ * nearly cancel across one triangle at a mirrored seam, leaves nothing after the subtraction,
72
+ * and a normalised nothing is NaN. One such pixel in one probe face was a whole courtyard drawn
73
+ * black: a probe convolves its faces into every direction, and the bounce carries that into
74
+ * every other probe. So where nothing is left, the derived frame stands in. What it gives up is a
75
+ * direction on a pixel that had none; what would make it wrong is a threshold large enough to
76
+ * catch a frame that is merely short, and a unit tangent interpolated is nowhere near 1e-4.
77
+ */
78
+ bool framed = hasTangents != 0 && alongSq > 1e-8;
79
+ vec3 t = framed ? attrT : derivedT;
80
+ vec3 b = framed ? attrB : derivedB;
69
81
  return mat3(t, b, n);
70
82
  }`;
@@ -111,6 +111,44 @@ vec3 clipToNeighbourhood(vec3 history, vec3 lo, vec3 hi) {
111
111
  return centre + offset / ratio;
112
112
  }
113
113
 
114
+ /*
115
+ * **The history read through a Catmull-Rom filter, not a bilinear one.**
116
+ *
117
+ * The reprojection lands between texels nearly everywhere, and a bilinear fetch there is a small blur;
118
+ * applied to a picture that is itself last frame's blend, it compounds every frame, so a still camera
119
+ * settled on a picture measurably softer than one frame of it. A courtyard's stone read as out of
120
+ * focus beside the same frame with the resolve off. Catmull-Rom passes a straight ramp through exactly
121
+ * and keeps the edge between two texels an edge, which is what the history is for.
122
+ *
123
+ * Five bilinear fetches rather than sixteen point ones: the middle two weights of each axis share a
124
+ * fetch placed between them, and the four corners, which carry a few hundredths, are left out and
125
+ * the rest renormalised. Its negative lobes can ring past what the frame holds, which the
126
+ * neighbourhood clip after it takes back, and the floor at zero keeps a float target out of the
127
+ * negatives meanwhile.
128
+ */
129
+ vec3 historyCatmullRom(vec2 uv) {
130
+ vec2 size = 1.0 / uTexel;
131
+ vec2 at = uv * size;
132
+ vec2 base = floor(at - 0.5) + 0.5;
133
+ vec2 f = at - base;
134
+ vec2 w0 = f * (-0.5 + f * (1.0 - 0.5 * f));
135
+ vec2 w1 = 1.0 + f * f * (-2.5 + 1.5 * f);
136
+ vec2 w2 = f * (0.5 + f * (2.0 - 1.5 * f));
137
+ vec2 w3 = f * f * (-0.5 + 0.5 * f);
138
+ vec2 w12 = w1 + w2;
139
+ vec2 at0 = (base - 1.0) * uTexel;
140
+ vec2 at3 = (base + 2.0) * uTexel;
141
+ vec2 at12 = (base + w2 / w12) * uTexel;
142
+ vec3 sum =
143
+ textureLod(uHistory, vec2(at12.x, at0.y), 0.0).rgb * (w12.x * w0.y) +
144
+ textureLod(uHistory, vec2(at0.x, at12.y), 0.0).rgb * (w0.x * w12.y) +
145
+ textureLod(uHistory, at12, 0.0).rgb * (w12.x * w12.y) +
146
+ textureLod(uHistory, vec2(at3.x, at12.y), 0.0).rgb * (w3.x * w12.y) +
147
+ textureLod(uHistory, vec2(at12.x, at3.y), 0.0).rgb * (w12.x * w3.y);
148
+ float weight = w12.x * w0.y + w0.x * w12.y + w12.x * w12.y + w3.x * w12.y + w12.x * w3.y;
149
+ return max(sum / weight, vec3(0.0));
150
+ }
151
+
114
152
  void main() {
115
153
  vec3 current = textureLod(uScene, vUv, 0.0).rgb;
116
154
 
@@ -143,7 +181,7 @@ void main() {
143
181
  vec3 lo;
144
182
  vec3 hi;
145
183
  neighbourhood(lo, hi);
146
- vec3 history = textureLod(uHistory, wasUv, 0.0).rgb;
184
+ vec3 history = historyCatmullRom(wasUv);
147
185
  vec3 bounded = clipToNeighbourhood(history, lo, hi);
148
186
 
149
187
  fragColor = vec4(mix(current, bounded, uHistoryBlend), 1.0);
@@ -27,9 +27,11 @@ import type { SurfaceTextureHandle } from './backend/api.ts';
27
27
  * enumeration serves the directional cascade and every point-light cubemap.
28
28
  *
29
29
  * **Every draw may also carry its material, which is what lets one enumeration serve a colour
30
- * pass as well as a depth one.** A depth pass ignores it, correctly: a material cannot change a
31
- * depth, which is why this interface was right to carry only a handle, a matrix and a palette for
32
- * as long as depth was the only thing replaying it. What that cost was measured from outside. A
30
+ * pass as well as a depth one.** A depth pass reads one thing from it, since 4.4.0: whether the
31
+ * caster is a cutout (`cutoutOf`), so a leaf card casts the leaf rather than the card. Everything
32
+ * else a material says cannot change a depth, which is why this interface carried only a handle, a
33
+ * matrix and a palette for as long as depth was the only thing replaying it — and why, until then,
34
+ * every alpha-cut surface cast its whole quad. What that cost was measured from outside. A
33
35
  * consumer wanting the scene from a second viewpoint — a mirror, a probe face — had one option,
34
36
  * which was to run its whole draw path again; the first attempt to avoid it replayed this sink
35
37
  * into the mirror and got **every car unpainted**, because the list carries no material. So the
@@ -9,6 +9,7 @@
9
9
  * "zero textures" rule is about the art direction (no image assets in the
10
10
  * payload), and nothing here adds a byte to it.
11
11
  */
12
+ import { SHADOW_DEPTH_CLEAR } from './depthConvention.ts';
12
13
  import { DEFAULT_RENDER_QUALITY } from './renderQuality.ts';
13
14
 
14
15
  export class ShadowMap {
@@ -54,6 +55,19 @@ export class ShadowMap {
54
55
  throw new Error(`ShadowMap: framebuffer incomplete (0x${status.toString(16)})`);
55
56
  }
56
57
 
58
+ /*
59
+ * **Cleared once, here, to the far plane, because nothing else says what the texture holds.**
60
+ * WebGL zeroes it, and a zero compared with `LEQUAL` is an occluder at the light itself, so
61
+ * a layer no pass has drawn put the whole world in shade. ANGLE on Vulkan hands the same
62
+ * texture back reading 1, which is lit, and that is what every capture here ran on: a scene
63
+ * with a second depth layer it never peeled lost its sun only under ANGLE's GL backend, the
64
+ * one Chrome on Linux runs by default. The frame's clear value is borrowed and put back.
65
+ */
66
+ const frameClear = gl.getParameter(gl.DEPTH_CLEAR_VALUE) as number;
67
+ gl.clearDepth(SHADOW_DEPTH_CLEAR);
68
+ gl.clear(gl.DEPTH_BUFFER_BIT);
69
+ gl.clearDepth(frameClear);
70
+
57
71
  gl.bindFramebuffer(gl.FRAMEBUFFER, null);
58
72
  gl.bindTexture(gl.TEXTURE_2D, null);
59
73
  }
@@ -60,6 +60,11 @@ export interface SurfaceMaterial<Texture = SurfaceTexture> {
60
60
  vScale?: number;
61
61
  /** Alpha below which a fragment is discarded. See `uAlbedoCutout`. */
62
62
  cutout?: number;
63
+ /**
64
+ * Whether both faces are seen: glTF's `doubleSided`, what a curtain or a leaf card is. Drawn
65
+ * without culling, and a back face is lit as its front, with the normal turned to the viewer.
66
+ */
67
+ doubleSided?: boolean;
63
68
  /**
64
69
  * How hard the normal map turns the shading normal.
65
70
  *