@driftengine/core 4.12.0 → 4.13.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 (207) hide show
  1. package/CHANGELOG.json +66 -0
  2. package/README.md +18 -9
  3. package/dist/geometry/meshBuilder.d.ts +10 -0
  4. package/dist/geometry/meshBuilder.js +11 -2
  5. package/dist/index.d.ts +3 -2
  6. package/dist/index.js +3 -2
  7. package/dist/math/color.d.ts +25 -0
  8. package/dist/math/color.js +37 -0
  9. package/dist/render/backend/webgl2/renderer.d.ts +17 -4
  10. package/dist/render/backend/webgl2/renderer.js +82 -47
  11. package/dist/render/backend/webgl2/skinScatter.d.ts +3 -1
  12. package/dist/render/backend/webgl2/skinScatter.js +5 -1
  13. package/dist/render/backend/webgpu/boltPass.d.ts +1 -1
  14. package/dist/render/backend/webgpu/causticsPass.d.ts +1 -1
  15. package/dist/render/backend/webgpu/filmPass.d.ts +1 -1
  16. package/dist/render/backend/webgpu/flockPass.d.ts +1 -1
  17. package/dist/render/backend/webgpu/giFieldPass.d.ts +3 -1
  18. package/dist/render/backend/webgpu/gpuDrivenPass.d.ts +10 -1
  19. package/dist/render/backend/webgpu/gpuDrivenPass.js +28 -11
  20. package/dist/render/backend/webgpu/panelPass.d.ts +1 -1
  21. package/dist/render/backend/webgpu/postPass.d.ts +2 -2
  22. package/dist/render/backend/webgpu/renderer.d.ts +19 -8
  23. package/dist/render/backend/webgpu/renderer.js +94 -38
  24. package/dist/render/backend/webgpu/scatterPass.d.ts +1 -1
  25. package/dist/render/backend/webgpu/skinScatterPass.d.ts +5 -1
  26. package/dist/render/backend/webgpu/skinScatterPass.js +9 -3
  27. package/dist/render/backend/webgpu/skyPass.d.ts +11 -1
  28. package/dist/render/backend/webgpu/waterPass.d.ts +1 -1
  29. package/dist/render/backend/webgpu/windStreakPass.d.ts +1 -1
  30. package/dist/render/causticsRenderer.d.ts +3 -1
  31. package/dist/render/causticsRenderer.js +5 -1
  32. package/dist/render/decalPass.d.ts +3 -1
  33. package/dist/render/decalPass.js +4 -1
  34. package/dist/render/flockRenderer.d.ts +1 -1
  35. package/dist/render/flockRenderer.js +4 -1
  36. package/dist/render/globalMediumPass.d.ts +3 -1
  37. package/dist/render/globalMediumPass.js +5 -1
  38. package/dist/render/gradeColor.d.ts +21 -0
  39. package/dist/render/gradeColor.js +64 -0
  40. package/dist/render/lightMatrix.d.ts +13 -0
  41. package/dist/render/lightMatrix.js +17 -0
  42. package/dist/render/passGrade.d.ts +13 -0
  43. package/dist/render/passGrade.js +47 -0
  44. package/dist/render/plumeRenderer.d.ts +1 -1
  45. package/dist/render/plumeRenderer.js +4 -1
  46. package/dist/render/renderQuality.d.ts +57 -18
  47. package/dist/render/renderQuality.js +3 -3
  48. package/dist/render/sceneTarget.d.ts +2 -0
  49. package/dist/render/sceneTarget.js +1 -0
  50. package/dist/render/sdfTextRenderer.d.ts +1 -1
  51. package/dist/render/sdfTextRenderer.js +1 -1
  52. package/dist/render/shaders/arcane.d.ts +1 -1
  53. package/dist/render/shaders/arcane.js +5 -1
  54. package/dist/render/shaders/bolt.d.ts +1 -1
  55. package/dist/render/shaders/bolt.js +4 -1
  56. package/dist/render/shaders/caustics.d.ts +1 -1
  57. package/dist/render/shaders/caustics.js +4 -1
  58. package/dist/render/shaders/decalProject.js +12 -2
  59. package/dist/render/shaders/film.d.ts +1 -1
  60. package/dist/render/shaders/film.js +22 -9
  61. package/dist/render/shaders/fire.d.ts +1 -1
  62. package/dist/render/shaders/fire.js +4 -1
  63. package/dist/render/shaders/flat/layerLooks.js +11 -19
  64. package/dist/render/shaders/flat/layered.js +33 -18
  65. package/dist/render/shaders/flock.d.ts +1 -1
  66. package/dist/render/shaders/flock.js +4 -1
  67. package/dist/render/shaders/generated/arcane.wgsl.d.ts +11 -1
  68. package/dist/render/shaders/generated/arcane.wgsl.js +11 -1
  69. package/dist/render/shaders/generated/bolt.wgsl.d.ts +12 -2
  70. package/dist/render/shaders/generated/bolt.wgsl.js +12 -2
  71. package/dist/render/shaders/generated/caustics.wgsl.d.ts +12 -2
  72. package/dist/render/shaders/generated/caustics.wgsl.js +12 -2
  73. package/dist/render/shaders/generated/decalProject.wgsl.d.ts +6 -1
  74. package/dist/render/shaders/generated/decalProject.wgsl.js +6 -1
  75. package/dist/render/shaders/generated/film.wgsl.d.ts +12 -2
  76. package/dist/render/shaders/generated/film.wgsl.js +12 -2
  77. package/dist/render/shaders/generated/fire.wgsl.d.ts +11 -1
  78. package/dist/render/shaders/generated/fire.wgsl.js +11 -1
  79. package/dist/render/shaders/generated/flat.wgsl.js +23 -23
  80. package/dist/render/shaders/generated/flock.wgsl.d.ts +12 -2
  81. package/dist/render/shaders/generated/flock.wgsl.js +12 -2
  82. package/dist/render/shaders/generated/globalMedium.wgsl.d.ts +12 -2
  83. package/dist/render/shaders/generated/globalMedium.wgsl.js +12 -2
  84. package/dist/render/shaders/generated/lightVolume.wgsl.d.ts +36 -16
  85. package/dist/render/shaders/generated/lightVolume.wgsl.js +39 -19
  86. package/dist/render/shaders/generated/panel.wgsl.d.ts +12 -2
  87. package/dist/render/shaders/generated/panel.wgsl.js +12 -2
  88. package/dist/render/shaders/generated/rush.wgsl.d.ts +31 -26
  89. package/dist/render/shaders/generated/rush.wgsl.js +31 -26
  90. package/dist/render/shaders/generated/scatter.wgsl.d.ts +12 -2
  91. package/dist/render/shaders/generated/scatter.wgsl.js +12 -2
  92. package/dist/render/shaders/generated/skinBlur.wgsl.d.ts +11 -1
  93. package/dist/render/shaders/generated/skinBlur.wgsl.js +11 -1
  94. package/dist/render/shaders/generated/sky.wgsl.d.ts +12 -2
  95. package/dist/render/shaders/generated/sky.wgsl.js +12 -2
  96. package/dist/render/shaders/generated/smoke.wgsl.d.ts +11 -1
  97. package/dist/render/shaders/generated/smoke.wgsl.js +11 -1
  98. package/dist/render/shaders/generated/text.wgsl.d.ts +11 -1
  99. package/dist/render/shaders/generated/text.wgsl.js +11 -1
  100. package/dist/render/shaders/generated/water.wgsl.d.ts +12 -2
  101. package/dist/render/shaders/generated/water.wgsl.js +12 -2
  102. package/dist/render/shaders/generated/windStreaks.wgsl.d.ts +12 -2
  103. package/dist/render/shaders/generated/windStreaks.wgsl.js +12 -2
  104. package/dist/render/shaders/globalMedium.js +14 -1
  105. package/dist/render/shaders/gpudriven/present.wgsl.d.ts +15 -6
  106. package/dist/render/shaders/gpudriven/present.wgsl.js +71 -8
  107. package/dist/render/shaders/lightVolume.js +4 -1
  108. package/dist/render/shaders/line.d.ts +1 -1
  109. package/dist/render/shaders/outputTransform.d.ts +8 -2
  110. package/dist/render/shaders/outputTransform.js +18 -11
  111. package/dist/render/shaders/panel.d.ts +1 -1
  112. package/dist/render/shaders/panel.js +5 -1
  113. package/dist/render/shaders/particle.d.ts +3 -3
  114. package/dist/render/shaders/rush.js +8 -0
  115. package/dist/render/shaders/scatter.d.ts +1 -1
  116. package/dist/render/shaders/scatter.js +4 -1
  117. package/dist/render/shaders/sdfText.d.ts +1 -1
  118. package/dist/render/shaders/skinBlur.js +5 -1
  119. package/dist/render/shaders/sky.d.ts +1 -1
  120. package/dist/render/shaders/sky.js +5 -1
  121. package/dist/render/shaders/smoke.d.ts +1 -1
  122. package/dist/render/shaders/smoke.js +4 -1
  123. package/dist/render/shaders/text.d.ts +1 -1
  124. package/dist/render/shaders/text.js +4 -1
  125. package/dist/render/shaders/water.d.ts +1 -1
  126. package/dist/render/shaders/water.js +12 -2
  127. package/dist/render/shaders/windStreaks.d.ts +1 -1
  128. package/dist/render/shaders/windStreaks.js +4 -1
  129. package/dist/render/surfaceLayerLooks.d.ts +16 -4
  130. package/dist/render/surfaceLayerLooks.js +33 -14
  131. package/dist/render/surfaceLayers.d.ts +2 -2
  132. package/dist/render/textRenderer.d.ts +3 -1
  133. package/dist/render/textRenderer.js +7 -1
  134. package/dist/render/vertexDefaults.d.ts +1 -1
  135. package/dist/render/vertexDefaults.js +1 -1
  136. package/dist/render/waterRenderer.d.ts +1 -1
  137. package/dist/render/waterRenderer.js +4 -1
  138. package/dist/render/windStreakRenderer.d.ts +3 -1
  139. package/dist/render/windStreakRenderer.js +5 -1
  140. package/package.json +6 -6
  141. package/src/geometry/meshBuilder.ts +21 -2
  142. package/src/index.ts +11 -2
  143. package/src/math/color.ts +39 -0
  144. package/src/render/backend/webgl2/renderer.ts +134 -44
  145. package/src/render/backend/webgl2/skinScatter.ts +5 -0
  146. package/src/render/backend/webgpu/giFieldPass.ts +3 -1
  147. package/src/render/backend/webgpu/gpuDrivenPass.ts +29 -13
  148. package/src/render/backend/webgpu/renderer.ts +104 -37
  149. package/src/render/backend/webgpu/skinScatterPass.ts +17 -1
  150. package/src/render/causticsRenderer.ts +5 -0
  151. package/src/render/decalPass.ts +3 -0
  152. package/src/render/flockRenderer.ts +5 -0
  153. package/src/render/globalMediumPass.ts +10 -1
  154. package/src/render/gradeColor.ts +90 -0
  155. package/src/render/lightMatrix.ts +32 -0
  156. package/src/render/passGrade.ts +58 -0
  157. package/src/render/plumeRenderer.ts +5 -0
  158. package/src/render/renderQuality.ts +60 -21
  159. package/src/render/sceneTarget.ts +3 -0
  160. package/src/render/sdfTextRenderer.ts +1 -1
  161. package/src/render/shaders/arcane.ts +5 -1
  162. package/src/render/shaders/bolt.ts +4 -1
  163. package/src/render/shaders/caustics.ts +4 -1
  164. package/src/render/shaders/decalProject.ts +12 -2
  165. package/src/render/shaders/film.ts +22 -9
  166. package/src/render/shaders/fire.ts +4 -1
  167. package/src/render/shaders/flat/layerLooks.ts +11 -19
  168. package/src/render/shaders/flat/layered.ts +33 -18
  169. package/src/render/shaders/flock.ts +4 -1
  170. package/src/render/shaders/generated/arcane.wgsl.ts +11 -1
  171. package/src/render/shaders/generated/bolt.wgsl.ts +12 -2
  172. package/src/render/shaders/generated/caustics.wgsl.ts +12 -2
  173. package/src/render/shaders/generated/decalProject.wgsl.ts +6 -1
  174. package/src/render/shaders/generated/film.wgsl.ts +12 -2
  175. package/src/render/shaders/generated/fire.wgsl.ts +11 -1
  176. package/src/render/shaders/generated/flat.wgsl.ts +23 -23
  177. package/src/render/shaders/generated/flock.wgsl.ts +12 -2
  178. package/src/render/shaders/generated/globalMedium.wgsl.ts +12 -2
  179. package/src/render/shaders/generated/lightVolume.wgsl.ts +39 -19
  180. package/src/render/shaders/generated/panel.wgsl.ts +12 -2
  181. package/src/render/shaders/generated/rush.wgsl.ts +31 -26
  182. package/src/render/shaders/generated/scatter.wgsl.ts +12 -2
  183. package/src/render/shaders/generated/skinBlur.wgsl.ts +11 -1
  184. package/src/render/shaders/generated/sky.wgsl.ts +12 -2
  185. package/src/render/shaders/generated/smoke.wgsl.ts +11 -1
  186. package/src/render/shaders/generated/text.wgsl.ts +11 -1
  187. package/src/render/shaders/generated/water.wgsl.ts +12 -2
  188. package/src/render/shaders/generated/windStreaks.wgsl.ts +12 -2
  189. package/src/render/shaders/globalMedium.ts +14 -1
  190. package/src/render/shaders/gpudriven/present.wgsl.ts +72 -8
  191. package/src/render/shaders/lightVolume.ts +4 -1
  192. package/src/render/shaders/outputTransform.ts +19 -11
  193. package/src/render/shaders/panel.ts +5 -1
  194. package/src/render/shaders/rush.ts +8 -0
  195. package/src/render/shaders/scatter.ts +4 -1
  196. package/src/render/shaders/skinBlur.ts +5 -1
  197. package/src/render/shaders/sky.ts +5 -1
  198. package/src/render/shaders/smoke.ts +4 -1
  199. package/src/render/shaders/text.ts +4 -1
  200. package/src/render/shaders/water.ts +12 -2
  201. package/src/render/shaders/windStreaks.ts +4 -1
  202. package/src/render/surfaceLayerLooks.ts +49 -18
  203. package/src/render/surfaceLayers.ts +2 -2
  204. package/src/render/textRenderer.ts +6 -0
  205. package/src/render/vertexDefaults.ts +1 -1
  206. package/src/render/waterRenderer.ts +5 -0
  207. package/src/render/windStreakRenderer.ts +5 -0
@@ -1,6 +1,7 @@
1
1
  import { GrowableF32, GrowableU32 } from './growable.ts';
2
2
  import type { MeshData } from '../render/mesh.ts';
3
3
  import type { Vec3 } from '../math/color.ts';
4
+ import { decodeSrgbInPlace } from '../math/color.ts';
4
5
 
5
6
  /**
6
7
  * The emissive colour of a vertex that has not been given one: negative, which no real
@@ -23,6 +24,16 @@ export interface MeshBuildOptions {
23
24
  * sample them. On, for a mesh that will be drawn with a `SurfaceTexture`.
24
25
  */
25
26
  planarUvs?: boolean;
27
+ /**
28
+ * The space the builder's colours were given in. `'linear'`, the default, is the renderer's own:
29
+ * light as a surface reflects it. `'srgb'` is a colour picked by eye, on a screen or in a paint
30
+ * program, and decodes every vertex's colour and emissive colour to linear here, once — as a
31
+ * texture of colours uploaded as sRGB is decoded — so a world built from hand-picked colours draws
32
+ * them as picked under the default `outputTransform`, where the same numbers taken as linear come
33
+ * out pale. What it gives up: nothing at the colour picked; a colour meant as light rather than as
34
+ * a look wants a linear number of its own.
35
+ */
36
+ colorSpace?: 'linear' | 'srgb';
26
37
  }
27
38
 
28
39
  /**
@@ -1083,14 +1094,22 @@ export class MeshBuilder {
1083
1094
  /* Same promise as `specular`: a mesh nothing bound uploads no skinning buffers, and
1084
1095
  `MeshData` requires both arrays or neither. */
1085
1096
  const skinned = this.anyJoint;
1097
+ /* Colours picked by eye, decoded once here: see `MeshBuildOptions.colorSpace`. */
1098
+ const picked = options.colorSpace === 'srgb';
1086
1099
  return {
1087
1100
  positions: this.positions.toTyped(),
1088
1101
  normals: this.normals.toTyped(),
1089
- colors: this.colors.toTyped(),
1102
+ colors: picked ? decodeSrgbInPlace(this.colors.toTyped()) : this.colors.toTyped(),
1090
1103
  emissive: this.emissive.toTyped(),
1091
1104
  ...(shines ? { specular: this.specular.toTyped() } : {}),
1092
1105
  ...(textured ? { relief: this.relief.toTyped() } : {}),
1093
- ...(tinted ? { emissiveColor: this.emissiveColor.toTyped() } : {}),
1106
+ ...(tinted
1107
+ ? {
1108
+ emissiveColor: picked
1109
+ ? decodeSrgbInPlace(this.emissiveColor.toTyped())
1110
+ : this.emissiveColor.toTyped(),
1111
+ }
1112
+ : {}),
1094
1113
  ...(rough ? { roughness: this.roughness.toTyped() } : {}),
1095
1114
  ...(mineral ? { grain: this.grain.toTyped() } : {}),
1096
1115
  ...(skinned ? { joints: this.expandedJoints(), weights: this.expandedWeights() } : {}),
package/src/index.ts CHANGED
@@ -96,7 +96,15 @@ export {
96
96
  wrapAngle,
97
97
  lerpAngle,
98
98
  } from './math/scalar.ts';
99
- export { hslToRgb, mixColor, mixColorInto, scaleColor } from './math/color.ts';
99
+ export {
100
+ decodeSrgbInPlace,
101
+ hslToRgb,
102
+ mixColor,
103
+ mixColorInto,
104
+ scaleColor,
105
+ srgbColor,
106
+ srgbToLinear,
107
+ } from './math/color.ts';
100
108
  export type { Vec3 } from './math/color.ts';
101
109
 
102
110
  export { Camera } from './render/camera.ts';
@@ -707,7 +715,7 @@ export type { LightGrid } from './render/lightGrid.ts';
707
715
  export type { PointLightBuffer, PointLightSource } from './render/pointLightSelection.ts';
708
716
  export { createFlame, flameFrequencyHz, updateFlame } from './render/flameLight.ts';
709
717
  export type { Flame, FlameOptions } from './render/flameLight.ts';
710
- export { computeLightMatrix } from './render/lightMatrix.ts';
718
+ export { computeLightMatrix, computeLightMatrixForBounds } from './render/lightMatrix.ts';
711
719
  export { PlumeRenderer } from './render/plumeRenderer.ts';
712
720
  export { createEnvironment } from './render/backend/webgl2/renderer.ts';
713
721
  export type { InsetRect } from './render/backend/webgl2/renderer.ts';
@@ -750,6 +758,7 @@ export type { ShellStation, ShellFillOptions } from './geometry/shellFill.ts';
750
758
  * this line the only way for it to comply was to copy, which is what the rule forbids.
751
759
  */
752
760
  export { OUTPUT_TRANSFORM_GLSL } from './render/shaders/outputTransform.ts';
761
+ export { interfaceGrade } from './render/passGrade.ts';
753
762
  export { PLUME_VERT, FIRE_FRAG } from './render/shaders/fire.ts';
754
763
  export { ARCANE_FRAG } from './render/shaders/arcane.ts';
755
764
  export { SMOKE_FRAG } from './render/shaders/smoke.ts';
package/src/math/color.ts CHANGED
@@ -35,3 +35,42 @@ export function mixColorInto(out: Vec3, a: Vec3, b: Vec3, t: number): void {
35
35
  export function scaleColor(color: Vec3, factor: number): Vec3 {
36
36
  return [color[0] * factor, color[1] * factor, color[2] * factor];
37
37
  }
38
+
39
+ /**
40
+ * A colour picked by eye — on a screen, in a paint program, as a CSS colour — as the linear value
41
+ * the renderer lights, blends, fogs and grades: each channel through the sRGB curve, which the
42
+ * default `outputTransform`, `'srgb'`, applies the other way at the end, so a colour drawn as it is,
43
+ * a sky, a clear or a surface lit at exactly 1, comes out as the colour picked.
44
+ *
45
+ * **Why a colour needs it**: everything the renderer draws is linear and encoded at the end, so a
46
+ * colour typed as it looks comes out too light — a brown picked as `(0.55, 0.3, 0.15)` draws as
47
+ * tan. **What it gives up**: nothing at the colour picked. **What would make it wrong** is a number
48
+ * that is an amount rather than a look: a light's intensity, a multiplier, a strength, which are
49
+ * linear already. Allocates, so it is for building a world rather than for a frame.
50
+ */
51
+ export function srgbColor(r: number, g: number, b: number): Vec3 {
52
+ return [srgbToLinear(r), srgbToLinear(g), srgbToLinear(b)];
53
+ }
54
+
55
+ /**
56
+ * Every value of `values` decoded from sRGB in place, but a negative one, which is a sentinel and
57
+ * not a colour (`MeshBuilder`'s emissive colour left to the albedo). For whole arrays of colours
58
+ * picked by eye; see `MeshBuildOptions.colorSpace`.
59
+ */
60
+ export function decodeSrgbInPlace(values: Float32Array): Float32Array {
61
+ for (let i = 0; i < values.length; i++) {
62
+ const value = values[i] as number;
63
+ if (value >= 0) values[i] = srgbToLinear(value);
64
+ }
65
+ return values;
66
+ }
67
+
68
+ /**
69
+ * One channel, 0 to 1, through the sRGB decode: the straight segment below 0.04045. For a colour
70
+ * that arrives as numbers per channel rather than as a triple, a packed hex token being the usual
71
+ * one; allocates nothing.
72
+ */
73
+ export function srgbToLinear(value: number): number {
74
+ // determinism: build-time — a colour for drawing, which no tick reads
75
+ return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
76
+ }
@@ -340,7 +340,9 @@ import {
340
340
  RUSH_REACH_UV,
341
341
  clampMotionShare,
342
342
  } from '../../vertexDefaults.ts';
343
- import { OUTPUT_TRANSFORM_CODE, forwardTransformCode } from '../../vertexDefaults.ts';
343
+ import { OUTPUT_TRANSFORM_CODE } from '../../vertexDefaults.ts';
344
+ import { gradeColorInto } from '../../gradeColor.ts';
345
+ import { interfaceGrade, passGradeCode, passGradeExposure } from '../../passGrade.ts';
344
346
  import type { DisplayRange } from '../../displayRange.ts';
345
347
  import {
346
348
  DEFAULT_FILMIC_CURVE,
@@ -1798,6 +1800,8 @@ export class WebGL2Renderer implements RendererApi {
1798
1800
  private warnedOverlayBlocks = false;
1799
1801
  /** Whether a probe is being baked, so the passes that would recurse into one can tell. */
1800
1802
  private probePassActive = false;
1803
+ /** `clearOf`'s scratch: a clear colour as the open target takes it. */
1804
+ private readonly gradedClear: Vec3 = [0, 0, 0];
1801
1805
  /**
1802
1806
  * Whether a scene capture is being drawn (`captureScene`): an off-screen pass like a bake, so the
1803
1807
  * frame's own view, its refraction copy and skin's spread are left alone, but lit as the frame is.
@@ -2563,8 +2567,8 @@ export class WebGL2Renderer implements RendererApi {
2563
2567
  sceneDepth: null,
2564
2568
  };
2565
2569
  const context = this.passContext;
2566
- context.outputTransform = this.gradeCode();
2567
- context.outputExposure = this.gradeExposure();
2570
+ context.outputTransform = this.passGrade();
2571
+ context.outputExposure = this.passGradeExposure();
2568
2572
  /*
2569
2573
  * The copies a pass declared it reads, taken here with everything drawn before it: colour
2570
2574
  * afresh at each reading pass, as WebGPU's `takePassColour` does, so a full-screen effect drawn
@@ -3893,11 +3897,11 @@ export class WebGL2Renderer implements RendererApi {
3893
3897
  } finally {
3894
3898
  target.end(gl);
3895
3899
  gl.frontFace(gl.CCW);
3900
+ this.capturePassActive = false;
3896
3901
  this.useFlatProgram();
3897
- gl.uniform1i(this.flatUniforms['uOutputTransform'] ?? null, this.gradeCode());
3898
- gl.uniform1f(this.flatUniforms['uOutputExposure'] ?? null, this.gradeExposure());
3902
+ gl.uniform1i(this.flatUniforms['uOutputTransform'] ?? null, this.passGrade());
3903
+ gl.uniform1f(this.flatUniforms['uOutputExposure'] ?? null, this.passGradeExposure());
3899
3904
  this.materials.dirty();
3900
- this.capturePassActive = false;
3901
3905
  this.temporalJittering = jittering;
3902
3906
  this.restoreViewport();
3903
3907
  }
@@ -5289,7 +5293,7 @@ export class WebGL2Renderer implements RendererApi {
5289
5293
  */
5290
5294
  if (!lit) gl.uniform1i(u['uLightingEnabled'] ?? null, 0);
5291
5295
  if (fog !== this.surfaceFog) gl.uniform1i(u['uFogEnabled'] ?? null, fog);
5292
- if (!toneMapped) gl.uniform1i(u['uOutputTransform'] ?? null, Math.min(this.gradeCode(), 1));
5296
+ if (!toneMapped) gl.uniform1i(u['uOutputTransform'] ?? null, Math.min(this.passGrade(), 1));
5293
5297
 
5294
5298
  if (blend) {
5295
5299
  gl.enable(gl.BLEND);
@@ -5347,7 +5351,7 @@ export class WebGL2Renderer implements RendererApi {
5347
5351
  if (refracting) this.unbindSeeThrough(u);
5348
5352
  if (!lit) gl.uniform1i(u['uLightingEnabled'] ?? null, 1);
5349
5353
  if (fog !== this.surfaceFog) gl.uniform1i(u['uFogEnabled'] ?? null, this.surfaceFog);
5350
- if (!toneMapped) gl.uniform1i(u['uOutputTransform'] ?? null, this.gradeCode());
5354
+ if (!toneMapped) gl.uniform1i(u['uOutputTransform'] ?? null, this.passGrade());
5351
5355
  if (own) this.materials.dirty();
5352
5356
  this.useFlatProgram();
5353
5357
  }
@@ -5523,7 +5527,18 @@ export class WebGL2Renderer implements RendererApi {
5523
5527
  // raining dust into the sea.
5524
5528
  const surface = env.underwater?.surfaceY;
5525
5529
  const submerged = surface !== undefined && (camera.position[1] ?? 0) < surface;
5526
- if (streaks.draw(this.gl, this.frameViewFor(camera), wind, timeSeconds, tint, submerged)) {
5530
+ if (
5531
+ streaks.draw(
5532
+ this.gl,
5533
+ this.frameViewFor(camera),
5534
+ wind,
5535
+ timeSeconds,
5536
+ tint,
5537
+ submerged,
5538
+ this.passGrade(),
5539
+ this.passGradeExposure(),
5540
+ )
5541
+ ) {
5527
5542
  this.windStreakBudget.ask();
5528
5543
  }
5529
5544
  }
@@ -5563,7 +5578,18 @@ export class WebGL2Renderer implements RendererApi {
5563
5578
  timeSec: number,
5564
5579
  ): void {
5565
5580
  this.scatterSkin();
5566
- if (text.draw(viewportWidth, viewportHeight, originX, originY, style, timeSec)) {
5581
+ if (
5582
+ text.draw(
5583
+ viewportWidth,
5584
+ viewportHeight,
5585
+ originX,
5586
+ originY,
5587
+ style,
5588
+ timeSec,
5589
+ interfaceGrade(this.passGrade()),
5590
+ 1,
5591
+ )
5592
+ ) {
5567
5593
  this.textBudget.ask();
5568
5594
  }
5569
5595
  }
@@ -5642,7 +5668,9 @@ export class WebGL2Renderer implements RendererApi {
5642
5668
  * real draw call touches the unit in the first place.
5643
5669
  */
5644
5670
  /**
5645
- * The output transform a forward pass should apply, and the exposure it should apply it at.
5671
+ * The grade a pass applies to what it draws into the target open now, and the exposure it
5672
+ * applies it at. The decision is `passGrade.ts`'s, for both backends; this only gathers the
5673
+ * state it is taken from.
5646
5674
  *
5647
5675
  * **Only where that pass is genuinely the last one to touch the frame.** With a scene target
5648
5676
  * the resolve grades, so a pass that graded as well would be applying a curve to its own
@@ -5650,21 +5678,37 @@ export class WebGL2Renderer implements RendererApi {
5650
5678
  * `screenEffects` off there is no resolve at all and every pass writes straight to the canvas,
5651
5679
  * so each one has to grade itself or the frame comes out half converted: linear values in an
5652
5680
  * eight-bit buffer read as display values, which is a saturated, clipped version of the colour
5653
- * the caller asked for rather than a slightly different one.
5681
+ * the caller asked for rather than a slightly different one. A probe's face or a capture is
5682
+ * never graded, and past the present every pass grades itself, composite or not.
5654
5683
  *
5655
5684
  * One pair of accessors rather than the expression written out at each site, because that is
5656
5685
  * how the mesh pass and the particle pass came to disagree in the first place: the mesh pass
5657
5686
  * gated, and the particle pass uploaded the transform unconditionally to a stage that did not
5658
5687
  * yet declare it.
5659
5688
  */
5660
- private gradeCode(): number {
5661
- return this.sceneTarget === null || !this.quality.hdrScene
5662
- ? forwardTransformCode(this.quality.outputTransform)
5663
- : 0;
5689
+ private passGrade(): number {
5690
+ const storing = this.probePassActive || this.capturePassActive;
5691
+ const transform = this.quality.outputTransform;
5692
+ return passGradeCode(storing, this.framePresented, this.resolveGrades(), transform);
5693
+ }
5694
+
5695
+ private passGradeExposure(): number {
5696
+ const storing = this.probePassActive || this.capturePassActive;
5697
+ return passGradeExposure(storing, this.framePresented, this.resolveGrades(), this.exposure);
5664
5698
  }
5665
5699
 
5666
- private gradeExposure(): number {
5667
- return this.sceneTarget === null || !this.quality.hdrScene ? this.exposure : 1;
5700
+ /** Whether the frame's resolve applies the transform, so the passes before it draw linear. */
5701
+ private resolveGrades(): boolean {
5702
+ return this.sceneTarget !== null && this.quality.hdrScene;
5703
+ }
5704
+
5705
+ /**
5706
+ * A clear colour as the target open now takes it: graded as a pass would grade it, so a colour
5707
+ * handed to the renderer is linear wherever it lands — the clear too, which the resolve grades
5708
+ * with everything else under `hdrScene`. See `gradeColor.ts`. Into one scratch, read at once.
5709
+ */
5710
+ private clearOf(color: Readonly<Vec3>): Vec3 {
5711
+ return gradeColorInto(this.passGrade(), this.passGradeExposure(), color, this.gradedClear);
5668
5712
  }
5669
5713
 
5670
5714
  drawSdfText(handle: SdfTextRenderer, model: Float32Array, color: Vec3, opacity: number): void {
@@ -5679,8 +5723,8 @@ export class WebGL2Renderer implements RendererApi {
5679
5723
  model,
5680
5724
  color,
5681
5725
  opacity,
5682
- this.gradeCode(),
5683
- this.gradeExposure(),
5726
+ this.passGrade(),
5727
+ this.passGradeExposure(),
5684
5728
  )
5685
5729
  ) {
5686
5730
  this.sdfTextBudget.ask();
@@ -5707,7 +5751,17 @@ export class WebGL2Renderer implements RendererApi {
5707
5751
  windZ = 0,
5708
5752
  ): void {
5709
5753
  this.flockBudget.ask();
5710
- flock.draw(this.gl, this.frameViewFor(camera), timeSeconds, params, tint, windX, windZ);
5754
+ flock.draw(
5755
+ this.gl,
5756
+ this.frameViewFor(camera),
5757
+ timeSeconds,
5758
+ params,
5759
+ tint,
5760
+ windX,
5761
+ windZ,
5762
+ this.passGrade(),
5763
+ this.passGradeExposure(),
5764
+ );
5711
5765
  }
5712
5766
 
5713
5767
  /**
@@ -5808,8 +5862,8 @@ export class WebGL2Renderer implements RendererApi {
5808
5862
  the uniform at all, so it was a write to a location that did not exist; now that the
5809
5863
  stage grades itself it has to obey the same "only where this pass is last" rule the mesh
5810
5864
  pass does, or a scene with a composite grades its particles twice. */
5811
- gl.uniform1i(u['uOutputTransform'] ?? null, this.gradeCode());
5812
- gl.uniform1f(u['uOutputExposure'] ?? null, this.gradeExposure());
5865
+ gl.uniform1i(u['uOutputTransform'] ?? null, this.passGrade());
5866
+ gl.uniform1f(u['uOutputExposure'] ?? null, this.passGradeExposure());
5813
5867
  gl.uniform3fv(u['uAmbient'] ?? null, env.ambient);
5814
5868
  gl.uniform3fv(u['uAmbientGround'] ?? null, env.ambientGround ?? env.ambient);
5815
5869
  gl.uniform1i(u['uNoiseOctaves'] ?? null, this.quality.plumeNoiseOctaves);
@@ -5917,6 +5971,8 @@ export class WebGL2Renderer implements RendererApi {
5917
5971
  gl.uniform3fv(u['uCoreColor'] ?? null, core);
5918
5972
  gl.uniform3fv(u['uEdgeColor'] ?? null, edge);
5919
5973
  gl.uniform1f(u['uCoreGain'] ?? null, coreGain);
5974
+ gl.uniform1i(u['uOutputTransform'] ?? null, this.passGrade());
5975
+ gl.uniform1f(u['uOutputExposure'] ?? null, this.passGradeExposure());
5920
5976
  bindAtmosphere(
5921
5977
  gl,
5922
5978
  u,
@@ -5992,8 +6048,8 @@ export class WebGL2Renderer implements RendererApi {
5992
6048
  gl.uniform3fv(u['uColor'] ?? null, color);
5993
6049
  gl.uniform1f(u['uOpacity'] ?? null, opacity);
5994
6050
  gl.uniform1f(u['uSoftness'] ?? null, softness);
5995
- gl.uniform1i(u['uOutputTransform'] ?? null, this.gradeCode());
5996
- gl.uniform1f(u['uOutputExposure'] ?? null, this.gradeExposure());
6051
+ gl.uniform1i(u['uOutputTransform'] ?? null, this.passGrade());
6052
+ gl.uniform1f(u['uOutputExposure'] ?? null, this.passGradeExposure());
5997
6053
  bindAtmosphere(
5998
6054
  gl,
5999
6055
  u,
@@ -6042,6 +6098,8 @@ export class WebGL2Renderer implements RendererApi {
6042
6098
  gl.uniform3fv(u['uDirectionalColor'] ?? null, env.directionalColor);
6043
6099
  gl.uniform3fv(u['uAmbient'] ?? null, env.ambient);
6044
6100
  gl.uniform3fv(u['uCameraPos'] ?? null, camera.position);
6101
+ gl.uniform1i(u['uOutputTransform'] ?? null, this.passGrade());
6102
+ gl.uniform1f(u['uOutputExposure'] ?? null, this.passGradeExposure());
6045
6103
  bindAtmosphere(
6046
6104
  gl,
6047
6105
  u,
@@ -6236,6 +6294,8 @@ export class WebGL2Renderer implements RendererApi {
6236
6294
  originX,
6237
6295
  originY,
6238
6296
  originZ,
6297
+ this.passGrade(),
6298
+ this.passGradeExposure(),
6239
6299
  );
6240
6300
  }
6241
6301
 
@@ -6265,6 +6325,8 @@ export class WebGL2Renderer implements RendererApi {
6265
6325
  this.quality.pointLightFalloff,
6266
6326
  windX,
6267
6327
  windZ,
6328
+ this.passGrade(),
6329
+ this.passGradeExposure(),
6268
6330
  );
6269
6331
  }
6270
6332
 
@@ -6295,6 +6357,8 @@ export class WebGL2Renderer implements RendererApi {
6295
6357
  windX,
6296
6358
  windZ,
6297
6359
  strength,
6360
+ this.passGrade(),
6361
+ this.passGradeExposure(),
6298
6362
  );
6299
6363
  if (drew) this.causticsBudget.ask();
6300
6364
  }
@@ -6337,7 +6401,7 @@ export class WebGL2Renderer implements RendererApi {
6337
6401
  this.canvas.height,
6338
6402
  source,
6339
6403
  planeY,
6340
- clearColor,
6404
+ this.clearOf(clearColor),
6341
6405
  );
6342
6406
  this.gpuTimer.begin('reflection');
6343
6407
  this.reflectionPassActive = true;
@@ -7651,7 +7715,8 @@ export class WebGL2Renderer implements RendererApi {
7651
7715
  // scene rather than over a rectangle of flat colour.
7652
7716
  gl.clear(gl.DEPTH_BUFFER_BIT);
7653
7717
  } else {
7654
- gl.clearColor(clearColor[0], clearColor[1], clearColor[2], 1);
7718
+ const clear = this.clearOf(clearColor);
7719
+ gl.clearColor(clear[0], clear[1], clear[2], 1);
7655
7720
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
7656
7721
  }
7657
7722
  /*
@@ -7761,6 +7826,9 @@ export class WebGL2Renderer implements RendererApi {
7761
7826
  gl.uniformMatrix4fv(this.panelUniforms['uClipCorrection'] ?? null, false, IDENTITY_MAT4);
7762
7827
  gl.uniform3fv(this.panelUniforms['uColor'] ?? null, color);
7763
7828
  gl.uniform1f(this.panelUniforms['uAlpha'] ?? null, alpha);
7829
+ /* An interface: the screen encode, never the curve. See `interfaceGrade`. */
7830
+ gl.uniform1i(this.panelUniforms['uOutputTransform'] ?? null, interfaceGrade(this.passGrade()));
7831
+ gl.uniform1f(this.panelUniforms['uOutputExposure'] ?? null, 1);
7764
7832
 
7765
7833
  gl.drawArrays(gl.TRIANGLES, 0, 6);
7766
7834
 
@@ -8487,7 +8555,8 @@ export class WebGL2Renderer implements RendererApi {
8487
8555
  this.sceneTarget?.begin(this.canvas.width, this.canvas.height);
8488
8556
  /* The materials' maps on the scene's framebuffer, cleared and disabled: `reflectionKeeps`. */
8489
8557
  this.reflectionTargets?.begin(this.canvas.width, this.canvas.height);
8490
- gl.clearColor(clearColor[0], clearColor[1], clearColor[2], 1);
8558
+ const clear = this.clearOf(clearColor);
8559
+ gl.clearColor(clear[0], clear[1], clear[2], 1);
8491
8560
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
8492
8561
  }
8493
8562
 
@@ -8642,7 +8711,7 @@ export class WebGL2Renderer implements RendererApi {
8642
8711
  exposure: this.exposure,
8643
8712
  film: this.filmConstants,
8644
8713
  }
8645
- : { transform: 0, exposure: 1 };
8714
+ : { transform: 0, exposure: 1, sceneGraded: this.passGrade() !== 0 };
8646
8715
  /*
8647
8716
  * Depth of field, on the same ceiling-and-dial split as bloom above: `depthOfField` decides
8648
8717
  * how far a defocused point may spread and this frame's `setDepthOfField` decides where the
@@ -8705,6 +8774,7 @@ export class WebGL2Renderer implements RendererApi {
8705
8774
  this.frameEye,
8706
8775
  this.canvas.width,
8707
8776
  this.canvas.height,
8777
+ this.passGrade(),
8708
8778
  );
8709
8779
  }
8710
8780
  }
@@ -8902,7 +8972,13 @@ export class WebGL2Renderer implements RendererApi {
8902
8972
  /* The march finished on the default framebuffer, which is not where the frame is being
8903
8973
  drawn. See `resolveReflections`, which is the same two-step for the same reason. */
8904
8974
  this.sceneTarget.bind();
8905
- this.medium.compositeMedium(depthCopy, this.canvas.width, this.canvas.height);
8975
+ this.medium.compositeMedium(
8976
+ depthCopy,
8977
+ this.canvas.width,
8978
+ this.canvas.height,
8979
+ this.passGrade(),
8980
+ this.passGradeExposure(),
8981
+ );
8906
8982
  }
8907
8983
  }
8908
8984
  }
@@ -9381,12 +9457,11 @@ export class WebGL2Renderer implements RendererApi {
9381
9457
  * exists to keep. With `screenEffects` off there is no resolve at all and the mesh pass
9382
9458
  * writes straight to the canvas, so it stays where it always was.
9383
9459
  *
9384
- * The consequence worth stating: full grading needs the composite. Without it the sky, the
9385
- * particles, the film and the water are still ungraded beside a world that is, which is the
9386
- * inconsistency that existed everywhere before this moved.
9460
+ * Every other pass grades through the same gate, `passGrade`, so a frame without a composite
9461
+ * is graded pass by pass to the curve a composite would have applied once.
9387
9462
  */
9388
- gl.uniform1i(u['uOutputTransform'] ?? null, this.gradeCode());
9389
- gl.uniform1f(u['uOutputExposure'] ?? null, this.gradeExposure());
9463
+ gl.uniform1i(u['uOutputTransform'] ?? null, this.passGrade());
9464
+ gl.uniform1f(u['uOutputExposure'] ?? null, this.passGradeExposure());
9390
9465
  gl.uniform3fv(u['uCameraPos'] ?? null, camera.position);
9391
9466
  /*
9392
9467
  * Solid, every frame, before anything asks otherwise. A GL uniform starts at
@@ -9875,7 +9950,15 @@ export class WebGL2Renderer implements RendererApi {
9875
9950
  if (projection === null) return;
9876
9951
  const depth = scene.snapshotDepth(true);
9877
9952
  mat4.invert(this.skinInverseProjection, projection);
9878
- skin.spread(scene, depth, this.emptyTexture2D, projection, this.skinInverseProjection);
9953
+ skin.spread(
9954
+ scene,
9955
+ depth,
9956
+ this.emptyTexture2D,
9957
+ projection,
9958
+ this.skinInverseProjection,
9959
+ this.passGrade(),
9960
+ this.passGradeExposure(),
9961
+ );
9879
9962
  this.useFlatProgram();
9880
9963
  }
9881
9964
 
@@ -10188,7 +10271,7 @@ export class WebGL2Renderer implements RendererApi {
10188
10271
  /* `1` is sRGB alone: the conversion without the curve, which is what dropping the tone map
10189
10272
  means rather than dropping the whole transform. `Math.min` rather than a literal, so a
10190
10273
  renderer asked for `none` stays at none and one asked for `srgb` is already there. */
10191
- if (!toneMapped) gl.uniform1i(u['uOutputTransform'] ?? null, Math.min(this.gradeCode(), 1));
10274
+ if (!toneMapped) gl.uniform1i(u['uOutputTransform'] ?? null, Math.min(this.passGrade(), 1));
10192
10275
  /* Scoped and handed back like every other state here, so a tinted draw cannot leak its
10193
10276
  colour onto the next one. See `TranslucentMeshOptions.tint`. */
10194
10277
  const tint = options.tint ?? null;
@@ -10259,7 +10342,7 @@ export class WebGL2Renderer implements RendererApi {
10259
10342
  cannot inherit a material from the draw before it. */
10260
10343
  if (!lit) gl.uniform1i(u['uLightingEnabled'] ?? null, 1);
10261
10344
  if (fog !== this.surfaceFog) gl.uniform1i(u['uFogEnabled'] ?? null, this.surfaceFog);
10262
- if (!toneMapped) gl.uniform1i(u['uOutputTransform'] ?? null, this.gradeCode());
10345
+ if (!toneMapped) gl.uniform1i(u['uOutputTransform'] ?? null, this.passGrade());
10263
10346
  if (own) this.materials.dirty();
10264
10347
  }
10265
10348
 
@@ -10362,7 +10445,7 @@ export class WebGL2Renderer implements RendererApi {
10362
10445
  /*
10363
10446
  * **A probe stores radiance, not display pixels.**
10364
10447
  *
10365
- * `gradeCode()` is the ACES curve and the sRGB encode whenever the frame has no HDR target,
10448
+ * `passGrade()` is the ACES curve and the sRGB encode whenever the frame has no HDR target,
10366
10449
  * so every face was baked through the whole output transform and the shader then sampled the
10367
10450
  * result as though it were light. Two things came of that and both read as "the reflection is
10368
10451
  * dull": the curve pulls everything bright toward 1 *before* it reaches the cube, so no
@@ -10418,16 +10501,17 @@ export class WebGL2Renderer implements RendererApi {
10418
10501
  }
10419
10502
  } finally {
10420
10503
  this.gl.frontFace(this.gl.CCW);
10421
- this.useFlatProgram();
10422
- this.gl.uniform1i(this.flatUniforms['uOutputTransform'] ?? null, this.gradeCode());
10423
- this.gl.uniform1f(this.flatUniforms['uOutputExposure'] ?? null, this.gradeExposure());
10424
- this.materials.dirty();
10425
10504
  /*
10426
10505
  * Cleared even if a caller's own draw threw, because leaving this set would keep every
10427
- * later frame reading the placeholder and quietly reflecting nothing.
10506
+ * later frame reading the placeholder and quietly reflecting nothing. Before the grade is
10507
+ * put back, which asks it.
10428
10508
  */
10429
10509
  this.probePassActive = false;
10430
10510
  this.probeBounce = false;
10511
+ this.useFlatProgram();
10512
+ this.gl.uniform1i(this.flatUniforms['uOutputTransform'] ?? null, this.passGrade());
10513
+ this.gl.uniform1f(this.flatUniforms['uOutputExposure'] ?? null, this.passGradeExposure());
10514
+ this.materials.dirty();
10431
10515
  /* Back to the canvas, or to the frame's own target if one is bound. See `SceneTarget`. */
10432
10516
  if (this.sceneTarget !== null && !this.framePresented) this.sceneTarget.bind();
10433
10517
  else {
@@ -10655,6 +10739,8 @@ export class WebGL2Renderer implements RendererApi {
10655
10739
  gl.uniformMatrix4fv(u['uModel'] ?? null, false, model);
10656
10740
  gl.uniform3fv(u['uCameraPos'] ?? null, camera.position);
10657
10741
  gl.uniform1f(u['uStrength'] ?? null, Math.min(shown, 1));
10742
+ gl.uniform1i(u['uOutputTransform'] ?? null, this.passGrade());
10743
+ gl.uniform1f(u['uOutputExposure'] ?? null, this.passGradeExposure());
10658
10744
  gl.uniform1f(u['uLength'] ?? null, length);
10659
10745
  gl.uniform1f(u['uSpread'] ?? null, spread);
10660
10746
  gl.uniform1f(u['uDust'] ?? null, clamp(options.dust ?? 0, 0, 1));
@@ -10820,6 +10906,8 @@ export class WebGL2Renderer implements RendererApi {
10820
10906
  gl.uniform3fv(u['uCameraPos'] ?? null, camera.position);
10821
10907
  gl.uniform1f(u['uTime'] ?? null, timeSeconds);
10822
10908
  gl.uniform1f(u['uSheen'] ?? null, sheen);
10909
+ gl.uniform1i(u['uOutputTransform'] ?? null, this.passGrade());
10910
+ gl.uniform1f(u['uOutputExposure'] ?? null, this.passGradeExposure());
10823
10911
  gl.uniform1f(u['uFilmRoughness'] ?? null, Math.min(1, Math.max(0, options.roughness ?? 0)));
10824
10912
  gl.uniform1f(
10825
10913
  u['uFilmRoughnessCycles'] ?? null,
@@ -10875,6 +10963,8 @@ export class WebGL2Renderer implements RendererApi {
10875
10963
  gl.uniform1f(u['uMoonPhase'] ?? null, sky.moonPhase);
10876
10964
  gl.uniform1f(u['uNightFactor'] ?? null, sky.nightFactor);
10877
10965
  gl.uniform2f(u['uCloudOffset'] ?? null, sky.cloudOffsetX, sky.cloudOffsetZ);
10966
+ gl.uniform1i(u['uOutputTransform'] ?? null, this.passGrade());
10967
+ gl.uniform1f(u['uOutputExposure'] ?? null, this.passGradeExposure());
10878
10968
  bindAtmosphere(
10879
10969
  gl,
10880
10970
  u,
@@ -147,6 +147,9 @@ export class GlSkinScatter {
147
147
  empty: WebGLTexture,
148
148
  projection: ArrayLike<number>,
149
149
  inverse: ArrayLike<number>,
150
+ /** The frame's grade, which the second axis applies to the light it adds. */
151
+ outputTransform = 0,
152
+ outputExposure = 1,
150
153
  ): void {
151
154
  const { gl } = this;
152
155
  if (this.multisampled !== null) {
@@ -163,6 +166,8 @@ export class GlSkinScatter {
163
166
  gl.uniform1i(u['uSkin'] ?? null, SKIN_BLUR_TEXTURE_UNIT);
164
167
  gl.uniform1i(u['uDepth'] ?? null, SKIN_BLUR_DEPTH_UNIT);
165
168
  gl.uniform1i(u['uAlbedo'] ?? null, SKIN_BLUR_ALBEDO_UNIT);
169
+ gl.uniform1i(u['uOutputTransform'] ?? null, outputTransform);
170
+ gl.uniform1f(u['uOutputExposure'] ?? null, outputExposure);
166
171
  gl.activeTexture(gl.TEXTURE0 + SKIN_BLUR_ALBEDO_UNIT);
167
172
  gl.bindTexture(gl.TEXTURE_2D, this.albedo ?? empty);
168
173
  /* The four terms that carry a depth back to view-space metres: the occlusion blur's own. */
@@ -41,7 +41,9 @@ const STORAGE = 0x0080;
41
41
  * can look at, and looking is what a number cannot replace: if the geometry is in the right places
42
42
  * at the right sizes the composition is right, and if a cascade boundary shows as a seam the fade
43
43
  * is wrong. Wave 4A's own recorded failure — a phantom surface that passed 111,907 parity samples
44
- * and was found by looking at a picture — is why this exists at all.
44
+ * and was found by looking at a picture — is why this exists at all. **So it is not graded**, alone
45
+ * of the passes that draw into a frame: what it shows are the field's values, which a curve would
46
+ * bend into a picture of something else.
45
47
  *
46
48
  * **The field it marches is one frame old**, because the renderer composes at `endFrame` and
47
49
  * `prepare` runs at `beginFrame`. `PrepareContext.distanceField` says why that is right rather
@@ -962,7 +962,16 @@ export class GpuDrivenPass implements PassDefinition {
962
962
  shadowed: true,
963
963
  blended: false,
964
964
  };
965
- private gradeWarned = false;
965
+ /**
966
+ * The frame's grade for the blit, `PassContext.outputTransform` and its exposure, and the code it
967
+ * was last written with, so a frame writes it only when it changes. See `present.wgsl.ts`.
968
+ */
969
+ private gradeBuffer: GPUBuffer | null = null;
970
+ private readonly gradeStaging = new ArrayBuffer(16);
971
+ private readonly gradeInts = new Int32Array(this.gradeStaging);
972
+ private readonly gradeFloats = new Float32Array(this.gradeStaging);
973
+ private gradeWrittenCode = Number.NaN;
974
+ private gradeWrittenExposure = Number.NaN;
966
975
 
967
976
  constructor(
968
977
  scene: StreamingScene,
@@ -2410,10 +2419,17 @@ export class GpuDrivenPass implements PassDefinition {
2410
2419
  ],
2411
2420
  });
2412
2421
 
2422
+ this.gradeBuffer ??= device.createBuffer({
2423
+ label: 'gpu-driven blit grade',
2424
+ size: this.gradeStaging.byteLength,
2425
+ usage: UNIFORM | COPY_DST,
2426
+ });
2427
+ this.gradeWrittenCode = Number.NaN;
2413
2428
  this.blitGroup = device.createBindGroup({
2414
2429
  layout: (this.blitPipeline as GPURenderPipeline).getBindGroupLayout(0),
2415
2430
  entries: [
2416
2431
  { binding: 0, resource: this.colourView as GPUTextureView },
2432
+ { binding: 2, resource: { buffer: this.gradeBuffer } },
2417
2433
  /* The depth the raster wrote, sampled so the blit can hand it to the frame. */
2418
2434
  ...(this.presentDepth
2419
2435
  ? [
@@ -3429,18 +3445,16 @@ export class GpuDrivenPass implements PassDefinition {
3429
3445
  /* The same gate as `prepare`: an unanswered or refused pipeline set encodes nothing at all. */
3430
3446
  if (!this.usable) return;
3431
3447
  if (this.blitPipeline === null || this.blitGroup === null) return;
3432
- if (ctx.outputTransform !== 0 && !this.gradeWarned) {
3433
- this.gradeWarned = true;
3434
- /*
3435
- * The pass writes linear light and expects a composite to grade it. A frame with no
3436
- * composite grades per pass, and applying the curve here would be a second copy of it —
3437
- * which is the thing `SHADE_MATERIAL_WGSL` is built to avoid. Said once, out loud, because
3438
- * a silently ungraded frame is a difference somebody would attribute to the pipeline.
3439
- */
3440
- console.warn(
3441
- '[driftengine] the gpu-driven pass writes linear light and this frame has no composite ' +
3442
- 'to grade it, so the picture will be flat. Use a quality profile with a scene target.',
3443
- );
3448
+ /* The frame's grade, written when it changes: 0 where the resolve grades. See present.wgsl.ts. */
3449
+ const changed =
3450
+ ctx.outputTransform !== this.gradeWrittenCode ||
3451
+ ctx.outputExposure !== this.gradeWrittenExposure;
3452
+ if (changed && this.device !== null && this.gradeBuffer !== null) {
3453
+ this.gradeWrittenCode = ctx.outputTransform;
3454
+ this.gradeWrittenExposure = ctx.outputExposure;
3455
+ this.gradeInts[0] = ctx.outputTransform;
3456
+ this.gradeFloats[1] = ctx.outputExposure;
3457
+ this.device.queue.writeBuffer(this.gradeBuffer, 0, this.gradeStaging);
3444
3458
  }
3445
3459
  ctx.pass.setPipeline(this.blitPipeline);
3446
3460
  ctx.pass.setBindGroup(0, this.blitGroup);
@@ -3452,6 +3466,8 @@ export class GpuDrivenPass implements PassDefinition {
3452
3466
  this.scene.detach();
3453
3467
  if (device.backend !== 'webgpu') return;
3454
3468
  this.disposeTargets();
3469
+ this.gradeBuffer?.destroy();
3470
+ this.gradeBuffer = null;
3455
3471
  this.shadowTexture?.destroy();
3456
3472
  this.glassTintTexture?.destroy();
3457
3473
  this.glassTintNone?.destroy();