@driftengine/core 4.11.1 → 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 (231) hide show
  1. package/CHANGELOG.json +102 -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 +5 -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 +33 -5
  10. package/dist/render/backend/webgl2/renderer.js +123 -49
  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 +21 -8
  23. package/dist/render/backend/webgpu/renderer.js +103 -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/areaLight.d.ts +1 -1
  64. package/dist/render/shaders/flat/areaLight.js +1 -1
  65. package/dist/render/shaders/flat/glassTint.d.ts +1 -1
  66. package/dist/render/shaders/flat/glassTint.js +1 -0
  67. package/dist/render/shaders/flat/index.d.ts +5 -0
  68. package/dist/render/shaders/flat/index.js +1 -1
  69. package/dist/render/shaders/flat/layerLooks.d.ts +16 -0
  70. package/dist/render/shaders/flat/layerLooks.js +108 -0
  71. package/dist/render/shaders/flat/layered.d.ts +6 -4
  72. package/dist/render/shaders/flat/layered.js +77 -13
  73. package/dist/render/shaders/flat/main.d.ts +1 -1
  74. package/dist/render/shaders/flat/main.js +7 -2
  75. package/dist/render/shaders/flat/models.js +1 -1
  76. package/dist/render/shaders/flat/preamble.js +6 -0
  77. package/dist/render/shaders/flock.d.ts +1 -1
  78. package/dist/render/shaders/flock.js +4 -1
  79. package/dist/render/shaders/generated/arcane.wgsl.d.ts +11 -1
  80. package/dist/render/shaders/generated/arcane.wgsl.js +11 -1
  81. package/dist/render/shaders/generated/bolt.wgsl.d.ts +12 -2
  82. package/dist/render/shaders/generated/bolt.wgsl.js +12 -2
  83. package/dist/render/shaders/generated/caustics.wgsl.d.ts +12 -2
  84. package/dist/render/shaders/generated/caustics.wgsl.js +12 -2
  85. package/dist/render/shaders/generated/decalProject.wgsl.d.ts +6 -1
  86. package/dist/render/shaders/generated/decalProject.wgsl.js +6 -1
  87. package/dist/render/shaders/generated/film.wgsl.d.ts +12 -2
  88. package/dist/render/shaders/generated/film.wgsl.js +12 -2
  89. package/dist/render/shaders/generated/fire.wgsl.d.ts +11 -1
  90. package/dist/render/shaders/generated/fire.wgsl.js +11 -1
  91. package/dist/render/shaders/generated/flat.wgsl.d.ts +144 -16
  92. package/dist/render/shaders/generated/flat.wgsl.js +183 -55
  93. package/dist/render/shaders/generated/flock.wgsl.d.ts +12 -2
  94. package/dist/render/shaders/generated/flock.wgsl.js +12 -2
  95. package/dist/render/shaders/generated/globalMedium.wgsl.d.ts +12 -2
  96. package/dist/render/shaders/generated/globalMedium.wgsl.js +12 -2
  97. package/dist/render/shaders/generated/lightVolume.wgsl.d.ts +36 -16
  98. package/dist/render/shaders/generated/lightVolume.wgsl.js +39 -19
  99. package/dist/render/shaders/generated/panel.wgsl.d.ts +12 -2
  100. package/dist/render/shaders/generated/panel.wgsl.js +12 -2
  101. package/dist/render/shaders/generated/rush.wgsl.d.ts +31 -26
  102. package/dist/render/shaders/generated/rush.wgsl.js +31 -26
  103. package/dist/render/shaders/generated/scatter.wgsl.d.ts +12 -2
  104. package/dist/render/shaders/generated/scatter.wgsl.js +12 -2
  105. package/dist/render/shaders/generated/skinBlur.wgsl.d.ts +11 -1
  106. package/dist/render/shaders/generated/skinBlur.wgsl.js +11 -1
  107. package/dist/render/shaders/generated/sky.wgsl.d.ts +12 -2
  108. package/dist/render/shaders/generated/sky.wgsl.js +12 -2
  109. package/dist/render/shaders/generated/smoke.wgsl.d.ts +11 -1
  110. package/dist/render/shaders/generated/smoke.wgsl.js +11 -1
  111. package/dist/render/shaders/generated/text.wgsl.d.ts +11 -1
  112. package/dist/render/shaders/generated/text.wgsl.js +11 -1
  113. package/dist/render/shaders/generated/water.wgsl.d.ts +12 -2
  114. package/dist/render/shaders/generated/water.wgsl.js +12 -2
  115. package/dist/render/shaders/generated/windStreaks.wgsl.d.ts +12 -2
  116. package/dist/render/shaders/generated/windStreaks.wgsl.js +12 -2
  117. package/dist/render/shaders/globalMedium.js +14 -1
  118. package/dist/render/shaders/gpudriven/present.wgsl.d.ts +15 -6
  119. package/dist/render/shaders/gpudriven/present.wgsl.js +71 -8
  120. package/dist/render/shaders/lightVolume.js +4 -1
  121. package/dist/render/shaders/line.d.ts +1 -1
  122. package/dist/render/shaders/outputTransform.d.ts +8 -2
  123. package/dist/render/shaders/outputTransform.js +18 -11
  124. package/dist/render/shaders/panel.d.ts +1 -1
  125. package/dist/render/shaders/panel.js +5 -1
  126. package/dist/render/shaders/particle.d.ts +3 -3
  127. package/dist/render/shaders/rush.js +8 -0
  128. package/dist/render/shaders/scatter.d.ts +1 -1
  129. package/dist/render/shaders/scatter.js +4 -1
  130. package/dist/render/shaders/sdfText.d.ts +1 -1
  131. package/dist/render/shaders/skinBlur.js +5 -1
  132. package/dist/render/shaders/sky.d.ts +1 -1
  133. package/dist/render/shaders/sky.js +5 -1
  134. package/dist/render/shaders/smoke.d.ts +1 -1
  135. package/dist/render/shaders/smoke.js +4 -1
  136. package/dist/render/shaders/text.d.ts +1 -1
  137. package/dist/render/shaders/text.js +4 -1
  138. package/dist/render/shaders/water.d.ts +1 -1
  139. package/dist/render/shaders/water.js +12 -2
  140. package/dist/render/shaders/windStreaks.d.ts +1 -1
  141. package/dist/render/shaders/windStreaks.js +4 -1
  142. package/dist/render/surfaceLayerLooks.d.ts +92 -0
  143. package/dist/render/surfaceLayerLooks.js +102 -0
  144. package/dist/render/surfaceLayers.d.ts +47 -6
  145. package/dist/render/surfaceLayers.js +5 -1
  146. package/dist/render/textRenderer.d.ts +3 -1
  147. package/dist/render/textRenderer.js +7 -1
  148. package/dist/render/vertexDefaults.d.ts +1 -1
  149. package/dist/render/vertexDefaults.js +1 -1
  150. package/dist/render/waterRenderer.d.ts +1 -1
  151. package/dist/render/waterRenderer.js +4 -1
  152. package/dist/render/windStreakRenderer.d.ts +3 -1
  153. package/dist/render/windStreakRenderer.js +5 -1
  154. package/package.json +6 -6
  155. package/scripts/browser.d.mts +5 -0
  156. package/scripts/browser.mjs +68 -14
  157. package/scripts/look.d.mts +81 -0
  158. package/scripts/look.mjs +380 -0
  159. package/src/geometry/meshBuilder.ts +21 -2
  160. package/src/index.ts +13 -2
  161. package/src/math/color.ts +39 -0
  162. package/src/render/backend/webgl2/renderer.ts +174 -46
  163. package/src/render/backend/webgl2/skinScatter.ts +5 -0
  164. package/src/render/backend/webgpu/giFieldPass.ts +3 -1
  165. package/src/render/backend/webgpu/gpuDrivenPass.ts +29 -13
  166. package/src/render/backend/webgpu/renderer.ts +112 -37
  167. package/src/render/backend/webgpu/skinScatterPass.ts +17 -1
  168. package/src/render/causticsRenderer.ts +5 -0
  169. package/src/render/decalPass.ts +3 -0
  170. package/src/render/flockRenderer.ts +5 -0
  171. package/src/render/globalMediumPass.ts +10 -1
  172. package/src/render/gradeColor.ts +90 -0
  173. package/src/render/lightMatrix.ts +32 -0
  174. package/src/render/passGrade.ts +58 -0
  175. package/src/render/plumeRenderer.ts +5 -0
  176. package/src/render/renderQuality.ts +60 -21
  177. package/src/render/sceneTarget.ts +3 -0
  178. package/src/render/sdfTextRenderer.ts +1 -1
  179. package/src/render/shaders/arcane.ts +5 -1
  180. package/src/render/shaders/bolt.ts +4 -1
  181. package/src/render/shaders/caustics.ts +4 -1
  182. package/src/render/shaders/decalProject.ts +12 -2
  183. package/src/render/shaders/film.ts +22 -9
  184. package/src/render/shaders/fire.ts +4 -1
  185. package/src/render/shaders/flat/areaLight.ts +1 -1
  186. package/src/render/shaders/flat/glassTint.ts +3 -1
  187. package/src/render/shaders/flat/index.ts +6 -1
  188. package/src/render/shaders/flat/layerLooks.ts +108 -0
  189. package/src/render/shaders/flat/layered.ts +78 -13
  190. package/src/render/shaders/flat/main.ts +7 -2
  191. package/src/render/shaders/flat/models.ts +1 -1
  192. package/src/render/shaders/flat/preamble.ts +6 -0
  193. package/src/render/shaders/flock.ts +4 -1
  194. package/src/render/shaders/generated/arcane.wgsl.ts +11 -1
  195. package/src/render/shaders/generated/bolt.wgsl.ts +12 -2
  196. package/src/render/shaders/generated/caustics.wgsl.ts +12 -2
  197. package/src/render/shaders/generated/decalProject.wgsl.ts +6 -1
  198. package/src/render/shaders/generated/film.wgsl.ts +12 -2
  199. package/src/render/shaders/generated/fire.wgsl.ts +11 -1
  200. package/src/render/shaders/generated/flat.wgsl.ts +183 -55
  201. package/src/render/shaders/generated/flock.wgsl.ts +12 -2
  202. package/src/render/shaders/generated/globalMedium.wgsl.ts +12 -2
  203. package/src/render/shaders/generated/lightVolume.wgsl.ts +39 -19
  204. package/src/render/shaders/generated/panel.wgsl.ts +12 -2
  205. package/src/render/shaders/generated/rush.wgsl.ts +31 -26
  206. package/src/render/shaders/generated/scatter.wgsl.ts +12 -2
  207. package/src/render/shaders/generated/skinBlur.wgsl.ts +11 -1
  208. package/src/render/shaders/generated/sky.wgsl.ts +12 -2
  209. package/src/render/shaders/generated/smoke.wgsl.ts +11 -1
  210. package/src/render/shaders/generated/text.wgsl.ts +11 -1
  211. package/src/render/shaders/generated/water.wgsl.ts +12 -2
  212. package/src/render/shaders/generated/windStreaks.wgsl.ts +12 -2
  213. package/src/render/shaders/globalMedium.ts +14 -1
  214. package/src/render/shaders/gpudriven/present.wgsl.ts +72 -8
  215. package/src/render/shaders/lightVolume.ts +4 -1
  216. package/src/render/shaders/outputTransform.ts +19 -11
  217. package/src/render/shaders/panel.ts +5 -1
  218. package/src/render/shaders/rush.ts +8 -0
  219. package/src/render/shaders/scatter.ts +4 -1
  220. package/src/render/shaders/skinBlur.ts +5 -1
  221. package/src/render/shaders/sky.ts +5 -1
  222. package/src/render/shaders/smoke.ts +4 -1
  223. package/src/render/shaders/text.ts +4 -1
  224. package/src/render/shaders/water.ts +12 -2
  225. package/src/render/shaders/windStreaks.ts +4 -1
  226. package/src/render/surfaceLayerLooks.ts +186 -0
  227. package/src/render/surfaceLayers.ts +52 -7
  228. package/src/render/textRenderer.ts +6 -0
  229. package/src/render/vertexDefaults.ts +1 -1
  230. package/src/render/waterRenderer.ts +5 -0
  231. package/src/render/windStreakRenderer.ts +5 -0
@@ -117,6 +117,7 @@ import {
117
117
  packSurfaceLayers,
118
118
  } from '../../surfaceLayers.ts';
119
119
  import type { SurfaceLayers } from '../../surfaceLayers.ts';
120
+ import { LOOK_FLOATS, asksLayerLooks, packLayerLooks } from '../../surfaceLayerLooks.ts';
120
121
  import type { SurfaceProjection } from '../../surfaceProjection.ts';
121
122
  import { FILM_FRAG, FILM_VERT } from '../../shaders/film.ts';
122
123
  import { SceneTarget } from '../../sceneTarget.ts';
@@ -339,7 +340,9 @@ import {
339
340
  RUSH_REACH_UV,
340
341
  clampMotionShare,
341
342
  } from '../../vertexDefaults.ts';
342
- 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';
343
346
  import type { DisplayRange } from '../../displayRange.ts';
344
347
  import {
345
348
  DEFAULT_FILMIC_CURVE,
@@ -1783,15 +1786,22 @@ export class WebGL2Renderer implements RendererApi {
1783
1786
  private readonly transmission = new Float32Array([-1, -1, -1, 0]);
1784
1787
  /** `uLayers`, the current material's layers packed. See `surfaceLayers.ts`. */
1785
1788
  private readonly layerFloats = new Float32Array(LAYER_FLOATS);
1789
+ /** `uLayerLooks`, the current material's layer picks and looks packed. See `surfaceLayerLooks.ts`. */
1790
+ private readonly lookFloats = new Float32Array(LOOK_FLOATS);
1786
1791
  /** Whether the lit stage has room for layers' two vectors, and which refusals were said. */
1787
1792
  private layersRoom: boolean | null = null;
1788
1793
  private warnedLayersRoom = false;
1794
+ /** Whether it has room for the layers' looks beside them, and whether the refusal was said. */
1795
+ private looksRoom: boolean | null = null;
1796
+ private warnedLooksRoom = false;
1789
1797
  private warnedLayersModelMap = false;
1790
1798
  private warnedLayersTriplanar = false;
1791
1799
  private warnedOverlayRoom = false;
1792
1800
  private warnedOverlayBlocks = false;
1793
1801
  /** Whether a probe is being baked, so the passes that would recurse into one can tell. */
1794
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];
1795
1805
  /**
1796
1806
  * Whether a scene capture is being drawn (`captureScene`): an off-screen pass like a bake, so the
1797
1807
  * frame's own view, its refraction copy and skin's spread are left alone, but lit as the frame is.
@@ -2557,8 +2567,8 @@ export class WebGL2Renderer implements RendererApi {
2557
2567
  sceneDepth: null,
2558
2568
  };
2559
2569
  const context = this.passContext;
2560
- context.outputTransform = this.gradeCode();
2561
- context.outputExposure = this.gradeExposure();
2570
+ context.outputTransform = this.passGrade();
2571
+ context.outputExposure = this.passGradeExposure();
2562
2572
  /*
2563
2573
  * The copies a pass declared it reads, taken here with everything drawn before it: colour
2564
2574
  * afresh at each reading pass, as WebGPU's `takePassColour` does, so a full-screen effect drawn
@@ -3887,11 +3897,11 @@ export class WebGL2Renderer implements RendererApi {
3887
3897
  } finally {
3888
3898
  target.end(gl);
3889
3899
  gl.frontFace(gl.CCW);
3900
+ this.capturePassActive = false;
3890
3901
  this.useFlatProgram();
3891
- gl.uniform1i(this.flatUniforms['uOutputTransform'] ?? null, this.gradeCode());
3892
- 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());
3893
3904
  this.materials.dirty();
3894
- this.capturePassActive = false;
3895
3905
  this.temporalJittering = jittering;
3896
3906
  this.restoreViewport();
3897
3907
  }
@@ -4398,6 +4408,9 @@ export class WebGL2Renderer implements RendererApi {
4398
4408
  packSurfaceLayers(layers, this.layerFloats);
4399
4409
  if (layers !== null) this.askLit('LAYERED');
4400
4410
  gl.uniform4fv(u['uLayers'] ?? null, this.layerFloats);
4411
+ packLayerLooks(layers, this.lookFloats);
4412
+ if (this.looksFit(layers)) this.askLit('LAYER_LOOKS');
4413
+ gl.uniform4fv(u['uLayerLooks'] ?? null, this.lookFloats);
4401
4414
  gl.uniform1i(u['uModelMap'] ?? null, MODEL_MAP_TEXTURE_UNIT);
4402
4415
  packModel(
4403
4416
  material?.model ?? null,
@@ -4691,6 +4704,28 @@ export class WebGL2Renderer implements RendererApi {
4691
4704
  return null;
4692
4705
  }
4693
4706
 
4707
+ /**
4708
+ * Whether a layered material's picks and looks are drawn: asked for, and room for their fifteen
4709
+ * vectors beside the layers' four. Refused, the layers are drawn reading layer `i` of their arrays
4710
+ * as they are, said once — which on a material sharing its arrays reads the wrong textures, so
4711
+ * the sentence names the way out.
4712
+ */
4713
+ private looksFit(layers: SurfaceLayers<SurfaceTexture> | null): boolean {
4714
+ if (!asksLayerLooks(layers)) return false;
4715
+ this.looksRoom ??= this.switchFits('LAYER_LOOKS', 'LAYERED');
4716
+ if (this.looksRoom) return true;
4717
+ if (!this.warnedLooksRoom) {
4718
+ this.warnedLooksRoom = true;
4719
+ console.warn(
4720
+ "Renderer: a layered material's picks and looks are refused on this device. They take " +
4721
+ 'fifteen fragment uniform vectors the lit shader does not have, so its layers read layer ' +
4722
+ 'i of its arrays, untinted, at one repeat each. A lower `maxLights` makes the room, or ' +
4723
+ 'WebGPU has it.',
4724
+ );
4725
+ }
4726
+ return false;
4727
+ }
4728
+
4694
4729
  /** Says once that a layered material's triplanar projection is laid on the horizontal plane. */
4695
4730
  private warnLayersTriplanar(): void {
4696
4731
  if (this.warnedLayersTriplanar) return;
@@ -4701,9 +4736,13 @@ export class WebGL2Renderer implements RendererApi {
4701
4736
  );
4702
4737
  }
4703
4738
 
4704
- /** Whether the lit shader with `feature` compiled in fits this GPU's fragment uniform vectors. */
4705
- private switchFits(feature: LitSwitch): boolean {
4739
+ /**
4740
+ * Whether the lit shader with `feature` compiled in, and `beside` with it where a switch means
4741
+ * nothing alone, fits this GPU's fragment uniform vectors.
4742
+ */
4743
+ private switchFits(feature: LitSwitch, beside?: LitSwitch): boolean {
4706
4744
  const asIf: Partial<Record<LitSwitch, boolean>> = { [feature]: true };
4745
+ if (beside !== undefined) asIf[beside] = true;
4707
4746
  return (
4708
4747
  countUniformVectors(this.flatFragAt(this.lightBudget, asIf)) <= this.fragmentVectorLimit()
4709
4748
  );
@@ -5254,7 +5293,7 @@ export class WebGL2Renderer implements RendererApi {
5254
5293
  */
5255
5294
  if (!lit) gl.uniform1i(u['uLightingEnabled'] ?? null, 0);
5256
5295
  if (fog !== this.surfaceFog) gl.uniform1i(u['uFogEnabled'] ?? null, fog);
5257
- 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));
5258
5297
 
5259
5298
  if (blend) {
5260
5299
  gl.enable(gl.BLEND);
@@ -5312,7 +5351,7 @@ export class WebGL2Renderer implements RendererApi {
5312
5351
  if (refracting) this.unbindSeeThrough(u);
5313
5352
  if (!lit) gl.uniform1i(u['uLightingEnabled'] ?? null, 1);
5314
5353
  if (fog !== this.surfaceFog) gl.uniform1i(u['uFogEnabled'] ?? null, this.surfaceFog);
5315
- if (!toneMapped) gl.uniform1i(u['uOutputTransform'] ?? null, this.gradeCode());
5354
+ if (!toneMapped) gl.uniform1i(u['uOutputTransform'] ?? null, this.passGrade());
5316
5355
  if (own) this.materials.dirty();
5317
5356
  this.useFlatProgram();
5318
5357
  }
@@ -5488,7 +5527,18 @@ export class WebGL2Renderer implements RendererApi {
5488
5527
  // raining dust into the sea.
5489
5528
  const surface = env.underwater?.surfaceY;
5490
5529
  const submerged = surface !== undefined && (camera.position[1] ?? 0) < surface;
5491
- 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
+ ) {
5492
5542
  this.windStreakBudget.ask();
5493
5543
  }
5494
5544
  }
@@ -5528,7 +5578,18 @@ export class WebGL2Renderer implements RendererApi {
5528
5578
  timeSec: number,
5529
5579
  ): void {
5530
5580
  this.scatterSkin();
5531
- 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
+ ) {
5532
5593
  this.textBudget.ask();
5533
5594
  }
5534
5595
  }
@@ -5607,7 +5668,9 @@ export class WebGL2Renderer implements RendererApi {
5607
5668
  * real draw call touches the unit in the first place.
5608
5669
  */
5609
5670
  /**
5610
- * 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.
5611
5674
  *
5612
5675
  * **Only where that pass is genuinely the last one to touch the frame.** With a scene target
5613
5676
  * the resolve grades, so a pass that graded as well would be applying a curve to its own
@@ -5615,21 +5678,37 @@ export class WebGL2Renderer implements RendererApi {
5615
5678
  * `screenEffects` off there is no resolve at all and every pass writes straight to the canvas,
5616
5679
  * so each one has to grade itself or the frame comes out half converted: linear values in an
5617
5680
  * eight-bit buffer read as display values, which is a saturated, clipped version of the colour
5618
- * 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.
5619
5683
  *
5620
5684
  * One pair of accessors rather than the expression written out at each site, because that is
5621
5685
  * how the mesh pass and the particle pass came to disagree in the first place: the mesh pass
5622
5686
  * gated, and the particle pass uploaded the transform unconditionally to a stage that did not
5623
5687
  * yet declare it.
5624
5688
  */
5625
- private gradeCode(): number {
5626
- return this.sceneTarget === null || !this.quality.hdrScene
5627
- ? forwardTransformCode(this.quality.outputTransform)
5628
- : 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);
5629
5693
  }
5630
5694
 
5631
- private gradeExposure(): number {
5632
- return this.sceneTarget === null || !this.quality.hdrScene ? this.exposure : 1;
5695
+ private passGradeExposure(): number {
5696
+ const storing = this.probePassActive || this.capturePassActive;
5697
+ return passGradeExposure(storing, this.framePresented, this.resolveGrades(), this.exposure);
5698
+ }
5699
+
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);
5633
5712
  }
5634
5713
 
5635
5714
  drawSdfText(handle: SdfTextRenderer, model: Float32Array, color: Vec3, opacity: number): void {
@@ -5644,8 +5723,8 @@ export class WebGL2Renderer implements RendererApi {
5644
5723
  model,
5645
5724
  color,
5646
5725
  opacity,
5647
- this.gradeCode(),
5648
- this.gradeExposure(),
5726
+ this.passGrade(),
5727
+ this.passGradeExposure(),
5649
5728
  )
5650
5729
  ) {
5651
5730
  this.sdfTextBudget.ask();
@@ -5672,7 +5751,17 @@ export class WebGL2Renderer implements RendererApi {
5672
5751
  windZ = 0,
5673
5752
  ): void {
5674
5753
  this.flockBudget.ask();
5675
- 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
+ );
5676
5765
  }
5677
5766
 
5678
5767
  /**
@@ -5773,8 +5862,8 @@ export class WebGL2Renderer implements RendererApi {
5773
5862
  the uniform at all, so it was a write to a location that did not exist; now that the
5774
5863
  stage grades itself it has to obey the same "only where this pass is last" rule the mesh
5775
5864
  pass does, or a scene with a composite grades its particles twice. */
5776
- gl.uniform1i(u['uOutputTransform'] ?? null, this.gradeCode());
5777
- gl.uniform1f(u['uOutputExposure'] ?? null, this.gradeExposure());
5865
+ gl.uniform1i(u['uOutputTransform'] ?? null, this.passGrade());
5866
+ gl.uniform1f(u['uOutputExposure'] ?? null, this.passGradeExposure());
5778
5867
  gl.uniform3fv(u['uAmbient'] ?? null, env.ambient);
5779
5868
  gl.uniform3fv(u['uAmbientGround'] ?? null, env.ambientGround ?? env.ambient);
5780
5869
  gl.uniform1i(u['uNoiseOctaves'] ?? null, this.quality.plumeNoiseOctaves);
@@ -5882,6 +5971,8 @@ export class WebGL2Renderer implements RendererApi {
5882
5971
  gl.uniform3fv(u['uCoreColor'] ?? null, core);
5883
5972
  gl.uniform3fv(u['uEdgeColor'] ?? null, edge);
5884
5973
  gl.uniform1f(u['uCoreGain'] ?? null, coreGain);
5974
+ gl.uniform1i(u['uOutputTransform'] ?? null, this.passGrade());
5975
+ gl.uniform1f(u['uOutputExposure'] ?? null, this.passGradeExposure());
5885
5976
  bindAtmosphere(
5886
5977
  gl,
5887
5978
  u,
@@ -5957,8 +6048,8 @@ export class WebGL2Renderer implements RendererApi {
5957
6048
  gl.uniform3fv(u['uColor'] ?? null, color);
5958
6049
  gl.uniform1f(u['uOpacity'] ?? null, opacity);
5959
6050
  gl.uniform1f(u['uSoftness'] ?? null, softness);
5960
- gl.uniform1i(u['uOutputTransform'] ?? null, this.gradeCode());
5961
- gl.uniform1f(u['uOutputExposure'] ?? null, this.gradeExposure());
6051
+ gl.uniform1i(u['uOutputTransform'] ?? null, this.passGrade());
6052
+ gl.uniform1f(u['uOutputExposure'] ?? null, this.passGradeExposure());
5962
6053
  bindAtmosphere(
5963
6054
  gl,
5964
6055
  u,
@@ -6007,6 +6098,8 @@ export class WebGL2Renderer implements RendererApi {
6007
6098
  gl.uniform3fv(u['uDirectionalColor'] ?? null, env.directionalColor);
6008
6099
  gl.uniform3fv(u['uAmbient'] ?? null, env.ambient);
6009
6100
  gl.uniform3fv(u['uCameraPos'] ?? null, camera.position);
6101
+ gl.uniform1i(u['uOutputTransform'] ?? null, this.passGrade());
6102
+ gl.uniform1f(u['uOutputExposure'] ?? null, this.passGradeExposure());
6010
6103
  bindAtmosphere(
6011
6104
  gl,
6012
6105
  u,
@@ -6201,6 +6294,8 @@ export class WebGL2Renderer implements RendererApi {
6201
6294
  originX,
6202
6295
  originY,
6203
6296
  originZ,
6297
+ this.passGrade(),
6298
+ this.passGradeExposure(),
6204
6299
  );
6205
6300
  }
6206
6301
 
@@ -6230,6 +6325,8 @@ export class WebGL2Renderer implements RendererApi {
6230
6325
  this.quality.pointLightFalloff,
6231
6326
  windX,
6232
6327
  windZ,
6328
+ this.passGrade(),
6329
+ this.passGradeExposure(),
6233
6330
  );
6234
6331
  }
6235
6332
 
@@ -6260,6 +6357,8 @@ export class WebGL2Renderer implements RendererApi {
6260
6357
  windX,
6261
6358
  windZ,
6262
6359
  strength,
6360
+ this.passGrade(),
6361
+ this.passGradeExposure(),
6263
6362
  );
6264
6363
  if (drew) this.causticsBudget.ask();
6265
6364
  }
@@ -6302,7 +6401,7 @@ export class WebGL2Renderer implements RendererApi {
6302
6401
  this.canvas.height,
6303
6402
  source,
6304
6403
  planeY,
6305
- clearColor,
6404
+ this.clearOf(clearColor),
6306
6405
  );
6307
6406
  this.gpuTimer.begin('reflection');
6308
6407
  this.reflectionPassActive = true;
@@ -6960,6 +7059,7 @@ export class WebGL2Renderer implements RendererApi {
6960
7059
  worldUvs: asIf.WORLD_UVS ?? this.litOn.WORLD_UVS,
6961
7060
  movingSun: asIf.MOVING_SUN ?? this.litOn.MOVING_SUN,
6962
7061
  layered: asIf.LAYERED ?? this.litOn.LAYERED,
7062
+ layerLooks: asIf.LAYER_LOOKS ?? this.litOn.LAYER_LOOKS,
6963
7063
  maxLights: budget.maxLights,
6964
7064
  maxAreaLights: budget.maxAreaLights,
6965
7065
  });
@@ -7615,7 +7715,8 @@ export class WebGL2Renderer implements RendererApi {
7615
7715
  // scene rather than over a rectangle of flat colour.
7616
7716
  gl.clear(gl.DEPTH_BUFFER_BIT);
7617
7717
  } else {
7618
- 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);
7619
7720
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
7620
7721
  }
7621
7722
  /*
@@ -7725,6 +7826,9 @@ export class WebGL2Renderer implements RendererApi {
7725
7826
  gl.uniformMatrix4fv(this.panelUniforms['uClipCorrection'] ?? null, false, IDENTITY_MAT4);
7726
7827
  gl.uniform3fv(this.panelUniforms['uColor'] ?? null, color);
7727
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);
7728
7832
 
7729
7833
  gl.drawArrays(gl.TRIANGLES, 0, 6);
7730
7834
 
@@ -8451,7 +8555,8 @@ export class WebGL2Renderer implements RendererApi {
8451
8555
  this.sceneTarget?.begin(this.canvas.width, this.canvas.height);
8452
8556
  /* The materials' maps on the scene's framebuffer, cleared and disabled: `reflectionKeeps`. */
8453
8557
  this.reflectionTargets?.begin(this.canvas.width, this.canvas.height);
8454
- 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);
8455
8560
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
8456
8561
  }
8457
8562
 
@@ -8606,7 +8711,7 @@ export class WebGL2Renderer implements RendererApi {
8606
8711
  exposure: this.exposure,
8607
8712
  film: this.filmConstants,
8608
8713
  }
8609
- : { transform: 0, exposure: 1 };
8714
+ : { transform: 0, exposure: 1, sceneGraded: this.passGrade() !== 0 };
8610
8715
  /*
8611
8716
  * Depth of field, on the same ceiling-and-dial split as bloom above: `depthOfField` decides
8612
8717
  * how far a defocused point may spread and this frame's `setDepthOfField` decides where the
@@ -8669,6 +8774,7 @@ export class WebGL2Renderer implements RendererApi {
8669
8774
  this.frameEye,
8670
8775
  this.canvas.width,
8671
8776
  this.canvas.height,
8777
+ this.passGrade(),
8672
8778
  );
8673
8779
  }
8674
8780
  }
@@ -8866,7 +8972,13 @@ export class WebGL2Renderer implements RendererApi {
8866
8972
  /* The march finished on the default framebuffer, which is not where the frame is being
8867
8973
  drawn. See `resolveReflections`, which is the same two-step for the same reason. */
8868
8974
  this.sceneTarget.bind();
8869
- 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
+ );
8870
8982
  }
8871
8983
  }
8872
8984
  }
@@ -9116,6 +9228,7 @@ export class WebGL2Renderer implements RendererApi {
9116
9228
  resolveTransmission(null, this.transmission);
9117
9229
  /* Nor layers: one layer until a material asks for more. */
9118
9230
  this.layerFloats.fill(0);
9231
+ this.lookFloats.fill(0);
9119
9232
  this.overlayMaps = null;
9120
9233
  this.overlayTexture = null;
9121
9234
  /* Nor an ambient of its own: the frame's, until a draw asks. See `setAmbientSH`. */
@@ -9290,6 +9403,7 @@ export class WebGL2Renderer implements RendererApi {
9290
9403
  gl.uniform4fv(u['uOverlay'] ?? null, this.overlayFloats);
9291
9404
  gl.uniform4fv(u['uWorldUv'] ?? null, this.projection);
9292
9405
  gl.uniform4fv(u['uLayers'] ?? null, this.layerFloats);
9406
+ gl.uniform4fv(u['uLayerLooks'] ?? null, this.lookFloats);
9293
9407
  /* Neither refracting nor glass, which is every draw that does not say otherwise. See
9294
9408
  `bindSeeThrough` for the four numbers in the one vector. */
9295
9409
  gl.uniform4f(u['uSeeThrough'] ?? null, 0, 0, 0, 0);
@@ -9343,12 +9457,11 @@ export class WebGL2Renderer implements RendererApi {
9343
9457
  * exists to keep. With `screenEffects` off there is no resolve at all and the mesh pass
9344
9458
  * writes straight to the canvas, so it stays where it always was.
9345
9459
  *
9346
- * The consequence worth stating: full grading needs the composite. Without it the sky, the
9347
- * particles, the film and the water are still ungraded beside a world that is, which is the
9348
- * 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.
9349
9462
  */
9350
- gl.uniform1i(u['uOutputTransform'] ?? null, this.gradeCode());
9351
- gl.uniform1f(u['uOutputExposure'] ?? null, this.gradeExposure());
9463
+ gl.uniform1i(u['uOutputTransform'] ?? null, this.passGrade());
9464
+ gl.uniform1f(u['uOutputExposure'] ?? null, this.passGradeExposure());
9352
9465
  gl.uniform3fv(u['uCameraPos'] ?? null, camera.position);
9353
9466
  /*
9354
9467
  * Solid, every frame, before anything asks otherwise. A GL uniform starts at
@@ -9837,7 +9950,15 @@ export class WebGL2Renderer implements RendererApi {
9837
9950
  if (projection === null) return;
9838
9951
  const depth = scene.snapshotDepth(true);
9839
9952
  mat4.invert(this.skinInverseProjection, projection);
9840
- 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
+ );
9841
9962
  this.useFlatProgram();
9842
9963
  }
9843
9964
 
@@ -10150,7 +10271,7 @@ export class WebGL2Renderer implements RendererApi {
10150
10271
  /* `1` is sRGB alone: the conversion without the curve, which is what dropping the tone map
10151
10272
  means rather than dropping the whole transform. `Math.min` rather than a literal, so a
10152
10273
  renderer asked for `none` stays at none and one asked for `srgb` is already there. */
10153
- 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));
10154
10275
  /* Scoped and handed back like every other state here, so a tinted draw cannot leak its
10155
10276
  colour onto the next one. See `TranslucentMeshOptions.tint`. */
10156
10277
  const tint = options.tint ?? null;
@@ -10221,7 +10342,7 @@ export class WebGL2Renderer implements RendererApi {
10221
10342
  cannot inherit a material from the draw before it. */
10222
10343
  if (!lit) gl.uniform1i(u['uLightingEnabled'] ?? null, 1);
10223
10344
  if (fog !== this.surfaceFog) gl.uniform1i(u['uFogEnabled'] ?? null, this.surfaceFog);
10224
- if (!toneMapped) gl.uniform1i(u['uOutputTransform'] ?? null, this.gradeCode());
10345
+ if (!toneMapped) gl.uniform1i(u['uOutputTransform'] ?? null, this.passGrade());
10225
10346
  if (own) this.materials.dirty();
10226
10347
  }
10227
10348
 
@@ -10324,7 +10445,7 @@ export class WebGL2Renderer implements RendererApi {
10324
10445
  /*
10325
10446
  * **A probe stores radiance, not display pixels.**
10326
10447
  *
10327
- * `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,
10328
10449
  * so every face was baked through the whole output transform and the shader then sampled the
10329
10450
  * result as though it were light. Two things came of that and both read as "the reflection is
10330
10451
  * dull": the curve pulls everything bright toward 1 *before* it reaches the cube, so no
@@ -10380,16 +10501,17 @@ export class WebGL2Renderer implements RendererApi {
10380
10501
  }
10381
10502
  } finally {
10382
10503
  this.gl.frontFace(this.gl.CCW);
10383
- this.useFlatProgram();
10384
- this.gl.uniform1i(this.flatUniforms['uOutputTransform'] ?? null, this.gradeCode());
10385
- this.gl.uniform1f(this.flatUniforms['uOutputExposure'] ?? null, this.gradeExposure());
10386
- this.materials.dirty();
10387
10504
  /*
10388
10505
  * Cleared even if a caller's own draw threw, because leaving this set would keep every
10389
- * 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.
10390
10508
  */
10391
10509
  this.probePassActive = false;
10392
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();
10393
10515
  /* Back to the canvas, or to the frame's own target if one is bound. See `SceneTarget`. */
10394
10516
  if (this.sceneTarget !== null && !this.framePresented) this.sceneTarget.bind();
10395
10517
  else {
@@ -10617,6 +10739,8 @@ export class WebGL2Renderer implements RendererApi {
10617
10739
  gl.uniformMatrix4fv(u['uModel'] ?? null, false, model);
10618
10740
  gl.uniform3fv(u['uCameraPos'] ?? null, camera.position);
10619
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());
10620
10744
  gl.uniform1f(u['uLength'] ?? null, length);
10621
10745
  gl.uniform1f(u['uSpread'] ?? null, spread);
10622
10746
  gl.uniform1f(u['uDust'] ?? null, clamp(options.dust ?? 0, 0, 1));
@@ -10782,6 +10906,8 @@ export class WebGL2Renderer implements RendererApi {
10782
10906
  gl.uniform3fv(u['uCameraPos'] ?? null, camera.position);
10783
10907
  gl.uniform1f(u['uTime'] ?? null, timeSeconds);
10784
10908
  gl.uniform1f(u['uSheen'] ?? null, sheen);
10909
+ gl.uniform1i(u['uOutputTransform'] ?? null, this.passGrade());
10910
+ gl.uniform1f(u['uOutputExposure'] ?? null, this.passGradeExposure());
10785
10911
  gl.uniform1f(u['uFilmRoughness'] ?? null, Math.min(1, Math.max(0, options.roughness ?? 0)));
10786
10912
  gl.uniform1f(
10787
10913
  u['uFilmRoughnessCycles'] ?? null,
@@ -10837,6 +10963,8 @@ export class WebGL2Renderer implements RendererApi {
10837
10963
  gl.uniform1f(u['uMoonPhase'] ?? null, sky.moonPhase);
10838
10964
  gl.uniform1f(u['uNightFactor'] ?? null, sky.nightFactor);
10839
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());
10840
10968
  bindAtmosphere(
10841
10969
  gl,
10842
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();