@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
@@ -41,6 +41,7 @@ import { resolveTransmission } from '../../transmission.js';
41
41
  import { resolveOcclusionFade } from '../../occlusionFade.js';
42
42
  import { orthographicDepthSpan } from '../../lightMatrix.js';
43
43
  import { LAYER_FLOATS, layerMaskMap, layeredProjection, layersFitBeside, packSurfaceLayers, } from '../../surfaceLayers.js';
44
+ import { LOOK_FLOATS, asksLayerLooks, packLayerLooks } from '../../surfaceLayerLooks.js';
44
45
  import { FILM_FRAG, FILM_VERT } from '../../shaders/film.js';
45
46
  import { SceneTarget } from '../../sceneTarget.js';
46
47
  import { FILM_LOOK_WITHOUT_COMPOSITE } from '../../shaders/filmLook.js';
@@ -118,7 +119,9 @@ import { FlockRenderer } from '../../flockRenderer.js';
118
119
  import { TextRenderer } from '../../textRenderer.js';
119
120
  import { SdfTextRenderer } from '../../sdfTextRenderer.js';
120
121
  import { MOTION_BLUR_MAX_UV, OBJECT_BLUR_UNKEPT, RUSH_REACH_UV, clampMotionShare, } from '../../vertexDefaults.js';
121
- import { OUTPUT_TRANSFORM_CODE, forwardTransformCode } from '../../vertexDefaults.js';
122
+ import { OUTPUT_TRANSFORM_CODE } from '../../vertexDefaults.js';
123
+ import { gradeColorInto } from '../../gradeColor.js';
124
+ import { interfaceGrade, passGradeCode, passGradeExposure } from '../../passGrade.js';
122
125
  import { DEFAULT_FILMIC_CURVE, FILMIC_CLAMPED, FILMIC_CONSTANTS, FILMIC_WITHOUT_COMPOSITE, resolveFilmicCurve, } from '../../filmicCurve.js';
123
126
  import { deviceSnappedCellSize } from '../../textLayout.js';
124
127
  import { WindStreakRenderer } from '../../windStreakRenderer.js';
@@ -963,15 +966,22 @@ export class WebGL2Renderer {
963
966
  transmission = new Float32Array([-1, -1, -1, 0]);
964
967
  /** `uLayers`, the current material's layers packed. See `surfaceLayers.ts`. */
965
968
  layerFloats = new Float32Array(LAYER_FLOATS);
969
+ /** `uLayerLooks`, the current material's layer picks and looks packed. See `surfaceLayerLooks.ts`. */
970
+ lookFloats = new Float32Array(LOOK_FLOATS);
966
971
  /** Whether the lit stage has room for layers' two vectors, and which refusals were said. */
967
972
  layersRoom = null;
968
973
  warnedLayersRoom = false;
974
+ /** Whether it has room for the layers' looks beside them, and whether the refusal was said. */
975
+ looksRoom = null;
976
+ warnedLooksRoom = false;
969
977
  warnedLayersModelMap = false;
970
978
  warnedLayersTriplanar = false;
971
979
  warnedOverlayRoom = false;
972
980
  warnedOverlayBlocks = false;
973
981
  /** Whether a probe is being baked, so the passes that would recurse into one can tell. */
974
982
  probePassActive = false;
983
+ /** `clearOf`'s scratch: a clear colour as the open target takes it. */
984
+ gradedClear = [0, 0, 0];
975
985
  /**
976
986
  * Whether a scene capture is being drawn (`captureScene`): an off-screen pass like a bake, so the
977
987
  * frame's own view, its refraction copy and skin's spread are left alone, but lit as the frame is.
@@ -1685,8 +1695,8 @@ export class WebGL2Renderer {
1685
1695
  sceneDepth: null,
1686
1696
  };
1687
1697
  const context = this.passContext;
1688
- context.outputTransform = this.gradeCode();
1689
- context.outputExposure = this.gradeExposure();
1698
+ context.outputTransform = this.passGrade();
1699
+ context.outputExposure = this.passGradeExposure();
1690
1700
  /*
1691
1701
  * The copies a pass declared it reads, taken here with everything drawn before it: colour
1692
1702
  * afresh at each reading pass, as WebGPU's `takePassColour` does, so a full-screen effect drawn
@@ -2815,11 +2825,11 @@ export class WebGL2Renderer {
2815
2825
  finally {
2816
2826
  target.end(gl);
2817
2827
  gl.frontFace(gl.CCW);
2828
+ this.capturePassActive = false;
2818
2829
  this.useFlatProgram();
2819
- gl.uniform1i(this.flatUniforms['uOutputTransform'] ?? null, this.gradeCode());
2820
- gl.uniform1f(this.flatUniforms['uOutputExposure'] ?? null, this.gradeExposure());
2830
+ gl.uniform1i(this.flatUniforms['uOutputTransform'] ?? null, this.passGrade());
2831
+ gl.uniform1f(this.flatUniforms['uOutputExposure'] ?? null, this.passGradeExposure());
2821
2832
  this.materials.dirty();
2822
- this.capturePassActive = false;
2823
2833
  this.temporalJittering = jittering;
2824
2834
  this.restoreViewport();
2825
2835
  }
@@ -3267,6 +3277,10 @@ export class WebGL2Renderer {
3267
3277
  if (layers !== null)
3268
3278
  this.askLit('LAYERED');
3269
3279
  gl.uniform4fv(u['uLayers'] ?? null, this.layerFloats);
3280
+ packLayerLooks(layers, this.lookFloats);
3281
+ if (this.looksFit(layers))
3282
+ this.askLit('LAYER_LOOKS');
3283
+ gl.uniform4fv(u['uLayerLooks'] ?? null, this.lookFloats);
3270
3284
  gl.uniform1i(u['uModelMap'] ?? null, MODEL_MAP_TEXTURE_UNIT);
3271
3285
  packModel(material?.model ?? null, modelMap !== null, this.modelParams, material?.physicalSpecular === true);
3272
3286
  /* The first physical highlight asked for is when the lit programs start carrying one. */
@@ -3539,6 +3553,27 @@ export class WebGL2Renderer {
3539
3553
  }
3540
3554
  return null;
3541
3555
  }
3556
+ /**
3557
+ * Whether a layered material's picks and looks are drawn: asked for, and room for their fifteen
3558
+ * vectors beside the layers' four. Refused, the layers are drawn reading layer `i` of their arrays
3559
+ * as they are, said once — which on a material sharing its arrays reads the wrong textures, so
3560
+ * the sentence names the way out.
3561
+ */
3562
+ looksFit(layers) {
3563
+ if (!asksLayerLooks(layers))
3564
+ return false;
3565
+ this.looksRoom ??= this.switchFits('LAYER_LOOKS', 'LAYERED');
3566
+ if (this.looksRoom)
3567
+ return true;
3568
+ if (!this.warnedLooksRoom) {
3569
+ this.warnedLooksRoom = true;
3570
+ console.warn("Renderer: a layered material's picks and looks are refused on this device. They take " +
3571
+ 'fifteen fragment uniform vectors the lit shader does not have, so its layers read layer ' +
3572
+ 'i of its arrays, untinted, at one repeat each. A lower `maxLights` makes the room, or ' +
3573
+ 'WebGPU has it.');
3574
+ }
3575
+ return false;
3576
+ }
3542
3577
  /** Says once that a layered material's triplanar projection is laid on the horizontal plane. */
3543
3578
  warnLayersTriplanar() {
3544
3579
  if (this.warnedLayersTriplanar)
@@ -3547,9 +3582,14 @@ export class WebGL2Renderer {
3547
3582
  console.warn("Renderer: a layered material's triplanar projection is laid on the horizontal plane: every " +
3548
3583
  "layer's maps on three planes would be three times the reads. See `SurfaceMaterial.layers`.");
3549
3584
  }
3550
- /** Whether the lit shader with `feature` compiled in fits this GPU's fragment uniform vectors. */
3551
- switchFits(feature) {
3585
+ /**
3586
+ * Whether the lit shader with `feature` compiled in, and `beside` with it where a switch means
3587
+ * nothing alone, fits this GPU's fragment uniform vectors.
3588
+ */
3589
+ switchFits(feature, beside) {
3552
3590
  const asIf = { [feature]: true };
3591
+ if (beside !== undefined)
3592
+ asIf[beside] = true;
3553
3593
  return (countUniformVectors(this.flatFragAt(this.lightBudget, asIf)) <= this.fragmentVectorLimit());
3554
3594
  }
3555
3595
  /**
@@ -4069,7 +4109,7 @@ export class WebGL2Renderer {
4069
4109
  if (fog !== this.surfaceFog)
4070
4110
  gl.uniform1i(u['uFogEnabled'] ?? null, fog);
4071
4111
  if (!toneMapped)
4072
- gl.uniform1i(u['uOutputTransform'] ?? null, Math.min(this.gradeCode(), 1));
4112
+ gl.uniform1i(u['uOutputTransform'] ?? null, Math.min(this.passGrade(), 1));
4073
4113
  if (blend) {
4074
4114
  gl.enable(gl.BLEND);
4075
4115
  if (options.additive === true)
@@ -4140,7 +4180,7 @@ export class WebGL2Renderer {
4140
4180
  if (fog !== this.surfaceFog)
4141
4181
  gl.uniform1i(u['uFogEnabled'] ?? null, this.surfaceFog);
4142
4182
  if (!toneMapped)
4143
- gl.uniform1i(u['uOutputTransform'] ?? null, this.gradeCode());
4183
+ gl.uniform1i(u['uOutputTransform'] ?? null, this.passGrade());
4144
4184
  if (own)
4145
4185
  this.materials.dirty();
4146
4186
  this.useFlatProgram();
@@ -4292,7 +4332,7 @@ export class WebGL2Renderer {
4292
4332
  // raining dust into the sea.
4293
4333
  const surface = env.underwater?.surfaceY;
4294
4334
  const submerged = surface !== undefined && (camera.position[1] ?? 0) < surface;
4295
- if (streaks.draw(this.gl, this.frameViewFor(camera), wind, timeSeconds, tint, submerged)) {
4335
+ if (streaks.draw(this.gl, this.frameViewFor(camera), wind, timeSeconds, tint, submerged, this.passGrade(), this.passGradeExposure())) {
4296
4336
  this.windStreakBudget.ask();
4297
4337
  }
4298
4338
  }
@@ -4320,7 +4360,7 @@ export class WebGL2Renderer {
4320
4360
  */
4321
4361
  drawText(text, viewportWidth, viewportHeight, originX, originY, style, timeSec) {
4322
4362
  this.scatterSkin();
4323
- if (text.draw(viewportWidth, viewportHeight, originX, originY, style, timeSec)) {
4363
+ if (text.draw(viewportWidth, viewportHeight, originX, originY, style, timeSec, interfaceGrade(this.passGrade()), 1)) {
4324
4364
  this.textBudget.ask();
4325
4365
  }
4326
4366
  }
@@ -4387,7 +4427,9 @@ export class WebGL2Renderer {
4387
4427
  * real draw call touches the unit in the first place.
4388
4428
  */
4389
4429
  /**
4390
- * The output transform a forward pass should apply, and the exposure it should apply it at.
4430
+ * The grade a pass applies to what it draws into the target open now, and the exposure it
4431
+ * applies it at. The decision is `passGrade.ts`'s, for both backends; this only gathers the
4432
+ * state it is taken from.
4391
4433
  *
4392
4434
  * **Only where that pass is genuinely the last one to touch the frame.** With a scene target
4393
4435
  * the resolve grades, so a pass that graded as well would be applying a curve to its own
@@ -4395,20 +4437,34 @@ export class WebGL2Renderer {
4395
4437
  * `screenEffects` off there is no resolve at all and every pass writes straight to the canvas,
4396
4438
  * so each one has to grade itself or the frame comes out half converted: linear values in an
4397
4439
  * eight-bit buffer read as display values, which is a saturated, clipped version of the colour
4398
- * the caller asked for rather than a slightly different one.
4440
+ * the caller asked for rather than a slightly different one. A probe's face or a capture is
4441
+ * never graded, and past the present every pass grades itself, composite or not.
4399
4442
  *
4400
4443
  * One pair of accessors rather than the expression written out at each site, because that is
4401
4444
  * how the mesh pass and the particle pass came to disagree in the first place: the mesh pass
4402
4445
  * gated, and the particle pass uploaded the transform unconditionally to a stage that did not
4403
4446
  * yet declare it.
4404
4447
  */
4405
- gradeCode() {
4406
- return this.sceneTarget === null || !this.quality.hdrScene
4407
- ? forwardTransformCode(this.quality.outputTransform)
4408
- : 0;
4448
+ passGrade() {
4449
+ const storing = this.probePassActive || this.capturePassActive;
4450
+ const transform = this.quality.outputTransform;
4451
+ return passGradeCode(storing, this.framePresented, this.resolveGrades(), transform);
4452
+ }
4453
+ passGradeExposure() {
4454
+ const storing = this.probePassActive || this.capturePassActive;
4455
+ return passGradeExposure(storing, this.framePresented, this.resolveGrades(), this.exposure);
4456
+ }
4457
+ /** Whether the frame's resolve applies the transform, so the passes before it draw linear. */
4458
+ resolveGrades() {
4459
+ return this.sceneTarget !== null && this.quality.hdrScene;
4409
4460
  }
4410
- gradeExposure() {
4411
- return this.sceneTarget === null || !this.quality.hdrScene ? this.exposure : 1;
4461
+ /**
4462
+ * A clear colour as the target open now takes it: graded as a pass would grade it, so a colour
4463
+ * handed to the renderer is linear wherever it lands — the clear too, which the resolve grades
4464
+ * with everything else under `hdrScene`. See `gradeColor.ts`. Into one scratch, read at once.
4465
+ */
4466
+ clearOf(color) {
4467
+ return gradeColorInto(this.passGrade(), this.passGradeExposure(), color, this.gradedClear);
4412
4468
  }
4413
4469
  drawSdfText(handle, model, color, opacity) {
4414
4470
  this.scatterSkin();
@@ -4418,7 +4474,7 @@ export class WebGL2Renderer {
4418
4474
  if (viewProj === null)
4419
4475
  return;
4420
4476
  /* Geometry, so the remapped matrix: `frameViewProj` itself stays raw for the motion blur. */
4421
- if (handle.draw(this.sceneMatrix(viewProj), model, color, opacity, this.gradeCode(), this.gradeExposure())) {
4477
+ if (handle.draw(this.sceneMatrix(viewProj), model, color, opacity, this.passGrade(), this.passGradeExposure())) {
4422
4478
  this.sdfTextBudget.ask();
4423
4479
  this.restoreSurfaceTextureUnit();
4424
4480
  }
@@ -4432,7 +4488,7 @@ export class WebGL2Renderer {
4432
4488
  }
4433
4489
  drawFlock(flock, camera, timeSeconds, params, tint, windX = 0, windZ = 0) {
4434
4490
  this.flockBudget.ask();
4435
- flock.draw(this.gl, this.frameViewFor(camera), timeSeconds, params, tint, windX, windZ);
4491
+ flock.draw(this.gl, this.frameViewFor(camera), timeSeconds, params, tint, windX, windZ, this.passGrade(), this.passGradeExposure());
4436
4492
  }
4437
4493
  /**
4438
4494
  * Build a scatter batch and fill it. Takes the data rather than handing back
@@ -4516,8 +4572,8 @@ export class WebGL2Renderer {
4516
4572
  the uniform at all, so it was a write to a location that did not exist; now that the
4517
4573
  stage grades itself it has to obey the same "only where this pass is last" rule the mesh
4518
4574
  pass does, or a scene with a composite grades its particles twice. */
4519
- gl.uniform1i(u['uOutputTransform'] ?? null, this.gradeCode());
4520
- gl.uniform1f(u['uOutputExposure'] ?? null, this.gradeExposure());
4575
+ gl.uniform1i(u['uOutputTransform'] ?? null, this.passGrade());
4576
+ gl.uniform1f(u['uOutputExposure'] ?? null, this.passGradeExposure());
4521
4577
  gl.uniform3fv(u['uAmbient'] ?? null, env.ambient);
4522
4578
  gl.uniform3fv(u['uAmbientGround'] ?? null, env.ambientGround ?? env.ambient);
4523
4579
  gl.uniform1i(u['uNoiseOctaves'] ?? null, this.quality.plumeNoiseOctaves);
@@ -4595,6 +4651,8 @@ export class WebGL2Renderer {
4595
4651
  gl.uniform3fv(u['uCoreColor'] ?? null, core);
4596
4652
  gl.uniform3fv(u['uEdgeColor'] ?? null, edge);
4597
4653
  gl.uniform1f(u['uCoreGain'] ?? null, coreGain);
4654
+ gl.uniform1i(u['uOutputTransform'] ?? null, this.passGrade());
4655
+ gl.uniform1f(u['uOutputExposure'] ?? null, this.passGradeExposure());
4598
4656
  bindAtmosphere(gl, u, env, this.reflectionPassActive ? this.reflectionAtmosphereY : (camera.position[1] ?? 0), this.quality.underwaterAtmosphere);
4599
4657
  batch.drawTo(gl, count);
4600
4658
  }
@@ -4652,8 +4710,8 @@ export class WebGL2Renderer {
4652
4710
  gl.uniform3fv(u['uColor'] ?? null, color);
4653
4711
  gl.uniform1f(u['uOpacity'] ?? null, opacity);
4654
4712
  gl.uniform1f(u['uSoftness'] ?? null, softness);
4655
- gl.uniform1i(u['uOutputTransform'] ?? null, this.gradeCode());
4656
- gl.uniform1f(u['uOutputExposure'] ?? null, this.gradeExposure());
4713
+ gl.uniform1i(u['uOutputTransform'] ?? null, this.passGrade());
4714
+ gl.uniform1f(u['uOutputExposure'] ?? null, this.passGradeExposure());
4657
4715
  bindAtmosphere(gl, u, env, this.reflectionPassActive ? this.reflectionAtmosphereY : (camera.position[1] ?? 0), this.quality.underwaterAtmosphere);
4658
4716
  lines.drawTo(gl, count, additive);
4659
4717
  }
@@ -4686,6 +4744,8 @@ export class WebGL2Renderer {
4686
4744
  gl.uniform3fv(u['uDirectionalColor'] ?? null, env.directionalColor);
4687
4745
  gl.uniform3fv(u['uAmbient'] ?? null, env.ambient);
4688
4746
  gl.uniform3fv(u['uCameraPos'] ?? null, camera.position);
4747
+ gl.uniform1i(u['uOutputTransform'] ?? null, this.passGrade());
4748
+ gl.uniform1f(u['uOutputExposure'] ?? null, this.passGradeExposure());
4689
4749
  bindAtmosphere(gl, u, env, this.reflectionPassActive ? this.reflectionAtmosphereY : (camera.position[1] ?? 0), this.quality.underwaterAtmosphere);
4690
4750
  // Foliage is thin and seen from both faces; culling it loses half of it.
4691
4751
  gl.disable(gl.CULL_FACE);
@@ -4819,13 +4879,13 @@ export class WebGL2Renderer {
4819
4879
  // --- Draws that own their context ----------------------------------------
4820
4880
  drawPlumes(plumes, camera, timeSeconds, env, windX = 0, windZ = 0, originX = 0, originY = 0, originZ = 0) {
4821
4881
  this.scatterSkin();
4822
- plumes.draw(this.gl, this.frameViewFor(camera), timeSeconds, env, this.quality.underwaterAtmosphere, this.reflectionPassActive ? this.reflectionAtmosphereY : (camera.position[1] ?? 0), this.reflectionPassActive ? (this.planarReflection?.clipPlane ?? null) : null, windX, windZ, originX, originY, originZ);
4882
+ plumes.draw(this.gl, this.frameViewFor(camera), timeSeconds, env, this.quality.underwaterAtmosphere, this.reflectionPassActive ? this.reflectionAtmosphereY : (camera.position[1] ?? 0), this.reflectionPassActive ? (this.planarReflection?.clipPlane ?? null) : null, windX, windZ, originX, originY, originZ, this.passGrade(), this.passGradeExposure());
4823
4883
  }
4824
4884
  drawWater(water, camera, timeSeconds, settings, env, windX = 0, windZ = 0) {
4825
4885
  if (!this.quality.water)
4826
4886
  return;
4827
4887
  this.waterBudget.ask();
4828
- water.draw(this.gl, this.frameViewFor(camera), timeSeconds, settings, env.directionalDir, env.directionalColor, env.ambient, env, this.quality.underwaterAtmosphere, this.reflectionReadyThisFrame ? this.planarReflection : null, env, this.quality.pointLightFalloff, windX, windZ);
4888
+ water.draw(this.gl, this.frameViewFor(camera), timeSeconds, settings, env.directionalDir, env.directionalColor, env.ambient, env, this.quality.underwaterAtmosphere, this.reflectionReadyThisFrame ? this.planarReflection : null, env, this.quality.pointLightFalloff, windX, windZ, this.passGrade(), this.passGradeExposure());
4829
4889
  }
4830
4890
  /**
4831
4891
  * Draw after the opaque scene: this is light added to surfaces already shaded,
@@ -4835,7 +4895,7 @@ export class WebGL2Renderer {
4835
4895
  this.scatterSkin();
4836
4896
  if (caustics === null || !this.quality.water)
4837
4897
  return;
4838
- const drew = caustics.draw(this.gl, this.frameViewFor(camera), timeSeconds, env.directionalDir, env.directionalColor, env.ambient, env, this.quality.underwaterAtmosphere, windX, windZ, strength);
4898
+ const drew = caustics.draw(this.gl, this.frameViewFor(camera), timeSeconds, env.directionalDir, env.directionalColor, env.ambient, env, this.quality.underwaterAtmosphere, windX, windZ, strength, this.passGrade(), this.passGradeExposure());
4839
4899
  if (drew)
4840
4900
  this.causticsBudget.ask();
4841
4901
  }
@@ -4872,7 +4932,7 @@ export class WebGL2Renderer {
4872
4932
  return null;
4873
4933
  this.reflectionReadyThisFrame = false;
4874
4934
  this.reflectionAtmosphereY = source.position[1] ?? 0;
4875
- const camera = reflection.begin(this.gl, this.canvas.width, this.canvas.height, source, planeY, clearColor);
4935
+ const camera = reflection.begin(this.gl, this.canvas.width, this.canvas.height, source, planeY, this.clearOf(clearColor));
4876
4936
  this.gpuTimer.begin('reflection');
4877
4937
  this.reflectionPassActive = true;
4878
4938
  return camera;
@@ -5495,6 +5555,7 @@ export class WebGL2Renderer {
5495
5555
  worldUvs: asIf.WORLD_UVS ?? this.litOn.WORLD_UVS,
5496
5556
  movingSun: asIf.MOVING_SUN ?? this.litOn.MOVING_SUN,
5497
5557
  layered: asIf.LAYERED ?? this.litOn.LAYERED,
5558
+ layerLooks: asIf.LAYER_LOOKS ?? this.litOn.LAYER_LOOKS,
5498
5559
  maxLights: budget.maxLights,
5499
5560
  maxAreaLights: budget.maxAreaLights,
5500
5561
  });
@@ -6009,7 +6070,8 @@ export class WebGL2Renderer {
6009
6070
  gl.clear(gl.DEPTH_BUFFER_BIT);
6010
6071
  }
6011
6072
  else {
6012
- gl.clearColor(clearColor[0], clearColor[1], clearColor[2], 1);
6073
+ const clear = this.clearOf(clearColor);
6074
+ gl.clearColor(clear[0], clear[1], clear[2], 1);
6013
6075
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
6014
6076
  }
6015
6077
  /*
@@ -6103,6 +6165,9 @@ export class WebGL2Renderer {
6103
6165
  gl.uniformMatrix4fv(this.panelUniforms['uClipCorrection'] ?? null, false, IDENTITY_MAT4);
6104
6166
  gl.uniform3fv(this.panelUniforms['uColor'] ?? null, color);
6105
6167
  gl.uniform1f(this.panelUniforms['uAlpha'] ?? null, alpha);
6168
+ /* An interface: the screen encode, never the curve. See `interfaceGrade`. */
6169
+ gl.uniform1i(this.panelUniforms['uOutputTransform'] ?? null, interfaceGrade(this.passGrade()));
6170
+ gl.uniform1f(this.panelUniforms['uOutputExposure'] ?? null, 1);
6106
6171
  gl.drawArrays(gl.TRIANGLES, 0, 6);
6107
6172
  gl.bindVertexArray(null);
6108
6173
  gl.depthMask(true);
@@ -6785,7 +6850,8 @@ export class WebGL2Renderer {
6785
6850
  this.sceneTarget?.begin(this.canvas.width, this.canvas.height);
6786
6851
  /* The materials' maps on the scene's framebuffer, cleared and disabled: `reflectionKeeps`. */
6787
6852
  this.reflectionTargets?.begin(this.canvas.width, this.canvas.height);
6788
- gl.clearColor(clearColor[0], clearColor[1], clearColor[2], 1);
6853
+ const clear = this.clearOf(clearColor);
6854
+ gl.clearColor(clear[0], clear[1], clear[2], 1);
6789
6855
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
6790
6856
  }
6791
6857
  /**
@@ -6920,7 +6986,7 @@ export class WebGL2Renderer {
6920
6986
  exposure: this.exposure,
6921
6987
  film: this.filmConstants,
6922
6988
  }
6923
- : { transform: 0, exposure: 1 };
6989
+ : { transform: 0, exposure: 1, sceneGraded: this.passGrade() !== 0 };
6924
6990
  /*
6925
6991
  * Depth of field, on the same ceiling-and-dial split as bloom above: `depthOfField` decides
6926
6992
  * how far a defocused point may spread and this frame's `setDepthOfField` decides where the
@@ -6970,7 +7036,7 @@ export class WebGL2Renderer {
6970
7036
  else if (this.sceneTarget !== null) {
6971
7037
  this.decals ??= new DecalPass(this.gl);
6972
7038
  this.sceneTarget.bind();
6973
- this.decals.drawDecals(this.decalQueue, marks, this.frameViewProj, this.frameEye, this.canvas.width, this.canvas.height);
7039
+ this.decals.drawDecals(this.decalQueue, marks, this.frameViewProj, this.frameEye, this.canvas.width, this.canvas.height, this.passGrade());
6974
7040
  }
6975
7041
  }
6976
7042
  /*
@@ -7122,7 +7188,7 @@ export class WebGL2Renderer {
7122
7188
  /* The march finished on the default framebuffer, which is not where the frame is being
7123
7189
  drawn. See `resolveReflections`, which is the same two-step for the same reason. */
7124
7190
  this.sceneTarget.bind();
7125
- this.medium.compositeMedium(depthCopy, this.canvas.width, this.canvas.height);
7191
+ this.medium.compositeMedium(depthCopy, this.canvas.width, this.canvas.height, this.passGrade(), this.passGradeExposure());
7126
7192
  }
7127
7193
  }
7128
7194
  }
@@ -7351,6 +7417,7 @@ export class WebGL2Renderer {
7351
7417
  resolveTransmission(null, this.transmission);
7352
7418
  /* Nor layers: one layer until a material asks for more. */
7353
7419
  this.layerFloats.fill(0);
7420
+ this.lookFloats.fill(0);
7354
7421
  this.overlayMaps = null;
7355
7422
  this.overlayTexture = null;
7356
7423
  /* Nor an ambient of its own: the frame's, until a draw asks. See `setAmbientSH`. */
@@ -7494,6 +7561,7 @@ export class WebGL2Renderer {
7494
7561
  gl.uniform4fv(u['uOverlay'] ?? null, this.overlayFloats);
7495
7562
  gl.uniform4fv(u['uWorldUv'] ?? null, this.projection);
7496
7563
  gl.uniform4fv(u['uLayers'] ?? null, this.layerFloats);
7564
+ gl.uniform4fv(u['uLayerLooks'] ?? null, this.lookFloats);
7497
7565
  /* Neither refracting nor glass, which is every draw that does not say otherwise. See
7498
7566
  `bindSeeThrough` for the four numbers in the one vector. */
7499
7567
  gl.uniform4f(u['uSeeThrough'] ?? null, 0, 0, 0, 0);
@@ -7541,12 +7609,11 @@ export class WebGL2Renderer {
7541
7609
  * exists to keep. With `screenEffects` off there is no resolve at all and the mesh pass
7542
7610
  * writes straight to the canvas, so it stays where it always was.
7543
7611
  *
7544
- * The consequence worth stating: full grading needs the composite. Without it the sky, the
7545
- * particles, the film and the water are still ungraded beside a world that is, which is the
7546
- * inconsistency that existed everywhere before this moved.
7612
+ * Every other pass grades through the same gate, `passGrade`, so a frame without a composite
7613
+ * is graded pass by pass to the curve a composite would have applied once.
7547
7614
  */
7548
- gl.uniform1i(u['uOutputTransform'] ?? null, this.gradeCode());
7549
- gl.uniform1f(u['uOutputExposure'] ?? null, this.gradeExposure());
7615
+ gl.uniform1i(u['uOutputTransform'] ?? null, this.passGrade());
7616
+ gl.uniform1f(u['uOutputExposure'] ?? null, this.passGradeExposure());
7550
7617
  gl.uniform3fv(u['uCameraPos'] ?? null, camera.position);
7551
7618
  /*
7552
7619
  * Solid, every frame, before anything asks otherwise. A GL uniform starts at
@@ -7999,7 +8066,7 @@ export class WebGL2Renderer {
7999
8066
  return;
8000
8067
  const depth = scene.snapshotDepth(true);
8001
8068
  mat4.invert(this.skinInverseProjection, projection);
8002
- skin.spread(scene, depth, this.emptyTexture2D, projection, this.skinInverseProjection);
8069
+ skin.spread(scene, depth, this.emptyTexture2D, projection, this.skinInverseProjection, this.passGrade(), this.passGradeExposure());
8003
8070
  this.useFlatProgram();
8004
8071
  }
8005
8072
  /** One draw of a mesh, as `drawMesh` describes, of the half of a skin `half` names. */
@@ -8322,7 +8389,7 @@ export class WebGL2Renderer {
8322
8389
  means rather than dropping the whole transform. `Math.min` rather than a literal, so a
8323
8390
  renderer asked for `none` stays at none and one asked for `srgb` is already there. */
8324
8391
  if (!toneMapped)
8325
- gl.uniform1i(u['uOutputTransform'] ?? null, Math.min(this.gradeCode(), 1));
8392
+ gl.uniform1i(u['uOutputTransform'] ?? null, Math.min(this.passGrade(), 1));
8326
8393
  /* Scoped and handed back like every other state here, so a tinted draw cannot leak its
8327
8394
  colour onto the next one. See `TranslucentMeshOptions.tint`. */
8328
8395
  const tint = options.tint ?? null;
@@ -8407,7 +8474,7 @@ export class WebGL2Renderer {
8407
8474
  if (fog !== this.surfaceFog)
8408
8475
  gl.uniform1i(u['uFogEnabled'] ?? null, this.surfaceFog);
8409
8476
  if (!toneMapped)
8410
- gl.uniform1i(u['uOutputTransform'] ?? null, this.gradeCode());
8477
+ gl.uniform1i(u['uOutputTransform'] ?? null, this.passGrade());
8411
8478
  if (own)
8412
8479
  this.materials.dirty();
8413
8480
  }
@@ -8496,7 +8563,7 @@ export class WebGL2Renderer {
8496
8563
  /*
8497
8564
  * **A probe stores radiance, not display pixels.**
8498
8565
  *
8499
- * `gradeCode()` is the ACES curve and the sRGB encode whenever the frame has no HDR target,
8566
+ * `passGrade()` is the ACES curve and the sRGB encode whenever the frame has no HDR target,
8500
8567
  * so every face was baked through the whole output transform and the shader then sampled the
8501
8568
  * result as though it were light. Two things came of that and both read as "the reflection is
8502
8569
  * dull": the curve pulls everything bright toward 1 *before* it reaches the cube, so no
@@ -8546,16 +8613,17 @@ export class WebGL2Renderer {
8546
8613
  }
8547
8614
  finally {
8548
8615
  this.gl.frontFace(this.gl.CCW);
8549
- this.useFlatProgram();
8550
- this.gl.uniform1i(this.flatUniforms['uOutputTransform'] ?? null, this.gradeCode());
8551
- this.gl.uniform1f(this.flatUniforms['uOutputExposure'] ?? null, this.gradeExposure());
8552
- this.materials.dirty();
8553
8616
  /*
8554
8617
  * Cleared even if a caller's own draw threw, because leaving this set would keep every
8555
- * later frame reading the placeholder and quietly reflecting nothing.
8618
+ * later frame reading the placeholder and quietly reflecting nothing. Before the grade is
8619
+ * put back, which asks it.
8556
8620
  */
8557
8621
  this.probePassActive = false;
8558
8622
  this.probeBounce = false;
8623
+ this.useFlatProgram();
8624
+ this.gl.uniform1i(this.flatUniforms['uOutputTransform'] ?? null, this.passGrade());
8625
+ this.gl.uniform1f(this.flatUniforms['uOutputExposure'] ?? null, this.passGradeExposure());
8626
+ this.materials.dirty();
8559
8627
  /* Back to the canvas, or to the frame's own target if one is bound. See `SceneTarget`. */
8560
8628
  if (this.sceneTarget !== null && !this.framePresented)
8561
8629
  this.sceneTarget.bind();
@@ -8739,6 +8807,8 @@ export class WebGL2Renderer {
8739
8807
  gl.uniformMatrix4fv(u['uModel'] ?? null, false, model);
8740
8808
  gl.uniform3fv(u['uCameraPos'] ?? null, camera.position);
8741
8809
  gl.uniform1f(u['uStrength'] ?? null, Math.min(shown, 1));
8810
+ gl.uniform1i(u['uOutputTransform'] ?? null, this.passGrade());
8811
+ gl.uniform1f(u['uOutputExposure'] ?? null, this.passGradeExposure());
8742
8812
  gl.uniform1f(u['uLength'] ?? null, length);
8743
8813
  gl.uniform1f(u['uSpread'] ?? null, spread);
8744
8814
  gl.uniform1f(u['uDust'] ?? null, clamp(options.dust ?? 0, 0, 1));
@@ -8871,6 +8941,8 @@ export class WebGL2Renderer {
8871
8941
  gl.uniform3fv(u['uCameraPos'] ?? null, camera.position);
8872
8942
  gl.uniform1f(u['uTime'] ?? null, timeSeconds);
8873
8943
  gl.uniform1f(u['uSheen'] ?? null, sheen);
8944
+ gl.uniform1i(u['uOutputTransform'] ?? null, this.passGrade());
8945
+ gl.uniform1f(u['uOutputExposure'] ?? null, this.passGradeExposure());
8874
8946
  gl.uniform1f(u['uFilmRoughness'] ?? null, Math.min(1, Math.max(0, options.roughness ?? 0)));
8875
8947
  gl.uniform1f(u['uFilmRoughnessCycles'] ?? null, Math.max(0.01, options.roughnessCyclesPerMetre ?? 60));
8876
8948
  gl.uniform1i(u['uClipEnabled'] ?? null, this.reflectionPassActive ? 1 : 0);
@@ -8914,6 +8986,8 @@ export class WebGL2Renderer {
8914
8986
  gl.uniform1f(u['uMoonPhase'] ?? null, sky.moonPhase);
8915
8987
  gl.uniform1f(u['uNightFactor'] ?? null, sky.nightFactor);
8916
8988
  gl.uniform2f(u['uCloudOffset'] ?? null, sky.cloudOffsetX, sky.cloudOffsetZ);
8989
+ gl.uniform1i(u['uOutputTransform'] ?? null, this.passGrade());
8990
+ gl.uniform1f(u['uOutputExposure'] ?? null, this.passGradeExposure());
8917
8991
  bindAtmosphere(gl, u, env, this.reflectionPassActive ? this.reflectionAtmosphereY : (camera.position[1] ?? 0), this.quality.underwaterAtmosphere);
8918
8992
  gl.bindVertexArray(this.skyVao);
8919
8993
  gl.depthMask(false);
@@ -46,7 +46,9 @@ export declare class GlSkinScatter {
46
46
  * is the frame's depth as a single-sampled texture, or null where none could be had — and then
47
47
  * the diffuse goes back unspread rather than not at all, so the skin keeps its light.
48
48
  */
49
- spread(scene: SceneTarget, depth: WebGLTexture | null, empty: WebGLTexture, projection: ArrayLike<number>, inverse: ArrayLike<number>): void;
49
+ spread(scene: SceneTarget, depth: WebGLTexture | null, empty: WebGLTexture, projection: ArrayLike<number>, inverse: ArrayLike<number>,
50
+ /** The frame's grade, which the second axis applies to the light it adds. */
51
+ outputTransform?: number, outputExposure?: number): void;
50
52
  /** Spent at the frame's start: what was drawn into the target was last frame's. */
51
53
  reset(): void;
52
54
  /** A multisampled target into its single-sampled texture. */
@@ -123,7 +123,9 @@ export class GlSkinScatter {
123
123
  * is the frame's depth as a single-sampled texture, or null where none could be had — and then
124
124
  * the diffuse goes back unspread rather than not at all, so the skin keeps its light.
125
125
  */
126
- spread(scene, depth, empty, projection, inverse) {
126
+ spread(scene, depth, empty, projection, inverse,
127
+ /** The frame's grade, which the second axis applies to the light it adds. */
128
+ outputTransform = 0, outputExposure = 1) {
127
129
  const { gl } = this;
128
130
  if (this.multisampled !== null) {
129
131
  this.resolve(this.drawFramebuffer, this.resolveFramebuffer);
@@ -139,6 +141,8 @@ export class GlSkinScatter {
139
141
  gl.uniform1i(u['uSkin'] ?? null, SKIN_BLUR_TEXTURE_UNIT);
140
142
  gl.uniform1i(u['uDepth'] ?? null, SKIN_BLUR_DEPTH_UNIT);
141
143
  gl.uniform1i(u['uAlbedo'] ?? null, SKIN_BLUR_ALBEDO_UNIT);
144
+ gl.uniform1i(u['uOutputTransform'] ?? null, outputTransform);
145
+ gl.uniform1f(u['uOutputExposure'] ?? null, outputExposure);
142
146
  gl.activeTexture(gl.TEXTURE0 + SKIN_BLUR_ALBEDO_UNIT);
143
147
  gl.bindTexture(gl.TEXTURE_2D, this.albedo ?? empty);
144
148
  /* The four terms that carry a depth back to view-space metres: the occlusion blur's own. */
@@ -11,7 +11,7 @@ import type { PipelineCache } from './pipelineCache.ts';
11
11
  export declare const BOLT_VERT_FIELDS: UniformFields;
12
12
  export declare const BOLT_VERT_SIZE: 96;
13
13
  export declare const BOLT_FRAG_FIELDS: UniformFields;
14
- export declare const BOLT_FRAG_SIZE: 128;
14
+ export declare const BOLT_FRAG_SIZE: 144;
15
15
  /** A batch of arcs on the device, behind the surface's opaque handle. */
16
16
  export interface GpuBolts {
17
17
  readonly fromBuffer: GPUBuffer;
@@ -10,7 +10,7 @@ import type { PipelineCache } from './pipelineCache.ts';
10
10
  export declare const CAUSTICS_VERT_FIELDS: UniformFields;
11
11
  export declare const CAUSTICS_VERT_SIZE: 64;
12
12
  export declare const CAUSTICS_FRAG_FIELDS: UniformFields;
13
- export declare const CAUSTICS_FRAG_SIZE: 144;
13
+ export declare const CAUSTICS_FRAG_SIZE: 160;
14
14
  /** A lit surface on the device, behind the API's opaque handle. */
15
15
  export interface GpuCaustics {
16
16
  readonly vertexBuffers: readonly GPUBuffer[];
@@ -14,7 +14,7 @@ import type { PipelineCache } from './pipelineCache.ts';
14
14
  export declare const FILM_VERT_FIELDS: UniformFields;
15
15
  export declare const FILM_VERT_SIZE: 128;
16
16
  export declare const FILM_FRAG_FIELDS: UniformFields;
17
- export declare const FILM_FRAG_SIZE: 144;
17
+ export declare const FILM_FRAG_SIZE: 160;
18
18
  export declare function createFilmBindGroupLayout(device: GPUDevice): GPUBindGroupLayout;
19
19
  /** Bound once against both rings; a dynamic offset per film. See `createPanelBindGroup`. */
20
20
  export declare function createFilmBindGroup(device: GPUDevice, layout: GPUBindGroupLayout, vertices: GPUBuffer, fragments: GPUBuffer, reflection: GPUTextureView, sampler: GPUSampler): GPUBindGroup;
@@ -10,7 +10,7 @@ import type { PipelineCache } from './pipelineCache.ts';
10
10
  export declare const FLOCK_VERT_FIELDS: UniformFields;
11
11
  export declare const FLOCK_VERT_SIZE: 112;
12
12
  export declare const FLOCK_FRAG_FIELDS: UniformFields;
13
- export declare const FLOCK_FRAG_SIZE: 16;
13
+ export declare const FLOCK_FRAG_SIZE: 32;
14
14
  /** A flock on the device, behind the surface's opaque handle. */
15
15
  export interface GpuFlock {
16
16
  readonly corners: GPUBuffer;
@@ -19,7 +19,9 @@ import type { PassContext, PassDefinition, PassDevice, PrepareContext } from '..
19
19
  * can look at, and looking is what a number cannot replace: if the geometry is in the right places
20
20
  * at the right sizes the composition is right, and if a cascade boundary shows as a seam the fade
21
21
  * is wrong. Wave 4A's own recorded failure — a phantom surface that passed 111,907 parity samples
22
- * and was found by looking at a picture — is why this exists at all.
22
+ * and was found by looking at a picture — is why this exists at all. **So it is not graded**, alone
23
+ * of the passes that draw into a frame: what it shows are the field's values, which a curve would
24
+ * bend into a picture of something else.
23
25
  *
24
26
  * **The field it marches is one frame old**, because the renderer composes at `endFrame` and
25
27
  * `prepare` runs at `beginFrame`. `PrepareContext.distanceField` says why that is right rather
@@ -660,7 +660,16 @@ export declare class GpuDrivenPass implements PassDefinition {
660
660
  private readonly scheduled;
661
661
  private readonly graphScratch;
662
662
  private readonly shape;
663
- private gradeWarned;
663
+ /**
664
+ * The frame's grade for the blit, `PassContext.outputTransform` and its exposure, and the code it
665
+ * was last written with, so a frame writes it only when it changes. See `present.wgsl.ts`.
666
+ */
667
+ private gradeBuffer;
668
+ private readonly gradeStaging;
669
+ private readonly gradeInts;
670
+ private readonly gradeFloats;
671
+ private gradeWrittenCode;
672
+ private gradeWrittenExposure;
664
673
  constructor(scene: StreamingScene, materials: readonly GpuDrivenMaterial[], shadow?: GpuDrivenShadowOptions);
665
674
  /** How many clusters the scene holds, which every per-cluster dispatch is sized by. */
666
675
  get clusterCount(): number;