@driftengine/core 4.12.0 → 4.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (207) hide show
  1. package/CHANGELOG.json +77 -0
  2. package/README.md +18 -9
  3. package/dist/geometry/meshBuilder.d.ts +10 -0
  4. package/dist/geometry/meshBuilder.js +11 -2
  5. package/dist/index.d.ts +3 -2
  6. package/dist/index.js +3 -2
  7. package/dist/math/color.d.ts +25 -0
  8. package/dist/math/color.js +37 -0
  9. package/dist/render/backend/webgl2/renderer.d.ts +17 -4
  10. package/dist/render/backend/webgl2/renderer.js +82 -47
  11. package/dist/render/backend/webgl2/skinScatter.d.ts +3 -1
  12. package/dist/render/backend/webgl2/skinScatter.js +5 -1
  13. package/dist/render/backend/webgpu/boltPass.d.ts +1 -1
  14. package/dist/render/backend/webgpu/causticsPass.d.ts +1 -1
  15. package/dist/render/backend/webgpu/filmPass.d.ts +1 -1
  16. package/dist/render/backend/webgpu/flockPass.d.ts +1 -1
  17. package/dist/render/backend/webgpu/giFieldPass.d.ts +3 -1
  18. package/dist/render/backend/webgpu/gpuDrivenPass.d.ts +10 -1
  19. package/dist/render/backend/webgpu/gpuDrivenPass.js +28 -11
  20. package/dist/render/backend/webgpu/panelPass.d.ts +1 -1
  21. package/dist/render/backend/webgpu/postPass.d.ts +2 -2
  22. package/dist/render/backend/webgpu/renderer.d.ts +19 -8
  23. package/dist/render/backend/webgpu/renderer.js +94 -38
  24. package/dist/render/backend/webgpu/scatterPass.d.ts +1 -1
  25. package/dist/render/backend/webgpu/skinScatterPass.d.ts +5 -1
  26. package/dist/render/backend/webgpu/skinScatterPass.js +9 -3
  27. package/dist/render/backend/webgpu/skyPass.d.ts +11 -1
  28. package/dist/render/backend/webgpu/waterPass.d.ts +1 -1
  29. package/dist/render/backend/webgpu/windStreakPass.d.ts +1 -1
  30. package/dist/render/causticsRenderer.d.ts +3 -1
  31. package/dist/render/causticsRenderer.js +5 -1
  32. package/dist/render/decalPass.d.ts +3 -1
  33. package/dist/render/decalPass.js +4 -1
  34. package/dist/render/flockRenderer.d.ts +1 -1
  35. package/dist/render/flockRenderer.js +4 -1
  36. package/dist/render/globalMediumPass.d.ts +3 -1
  37. package/dist/render/globalMediumPass.js +5 -1
  38. package/dist/render/gradeColor.d.ts +21 -0
  39. package/dist/render/gradeColor.js +64 -0
  40. package/dist/render/lightMatrix.d.ts +13 -0
  41. package/dist/render/lightMatrix.js +17 -0
  42. package/dist/render/passGrade.d.ts +13 -0
  43. package/dist/render/passGrade.js +47 -0
  44. package/dist/render/plumeRenderer.d.ts +1 -1
  45. package/dist/render/plumeRenderer.js +4 -1
  46. package/dist/render/renderQuality.d.ts +57 -18
  47. package/dist/render/renderQuality.js +3 -3
  48. package/dist/render/sceneTarget.d.ts +2 -0
  49. package/dist/render/sceneTarget.js +1 -0
  50. package/dist/render/sdfTextRenderer.d.ts +1 -1
  51. package/dist/render/sdfTextRenderer.js +1 -1
  52. package/dist/render/shaders/arcane.d.ts +1 -1
  53. package/dist/render/shaders/arcane.js +5 -1
  54. package/dist/render/shaders/bolt.d.ts +1 -1
  55. package/dist/render/shaders/bolt.js +4 -1
  56. package/dist/render/shaders/caustics.d.ts +1 -1
  57. package/dist/render/shaders/caustics.js +4 -1
  58. package/dist/render/shaders/decalProject.js +12 -2
  59. package/dist/render/shaders/film.d.ts +1 -1
  60. package/dist/render/shaders/film.js +22 -9
  61. package/dist/render/shaders/fire.d.ts +1 -1
  62. package/dist/render/shaders/fire.js +4 -1
  63. package/dist/render/shaders/flat/layerLooks.js +11 -19
  64. package/dist/render/shaders/flat/layered.js +33 -18
  65. package/dist/render/shaders/flock.d.ts +1 -1
  66. package/dist/render/shaders/flock.js +4 -1
  67. package/dist/render/shaders/generated/arcane.wgsl.d.ts +11 -1
  68. package/dist/render/shaders/generated/arcane.wgsl.js +11 -1
  69. package/dist/render/shaders/generated/bolt.wgsl.d.ts +12 -2
  70. package/dist/render/shaders/generated/bolt.wgsl.js +12 -2
  71. package/dist/render/shaders/generated/caustics.wgsl.d.ts +12 -2
  72. package/dist/render/shaders/generated/caustics.wgsl.js +12 -2
  73. package/dist/render/shaders/generated/decalProject.wgsl.d.ts +6 -1
  74. package/dist/render/shaders/generated/decalProject.wgsl.js +6 -1
  75. package/dist/render/shaders/generated/film.wgsl.d.ts +12 -2
  76. package/dist/render/shaders/generated/film.wgsl.js +12 -2
  77. package/dist/render/shaders/generated/fire.wgsl.d.ts +11 -1
  78. package/dist/render/shaders/generated/fire.wgsl.js +11 -1
  79. package/dist/render/shaders/generated/flat.wgsl.js +23 -23
  80. package/dist/render/shaders/generated/flock.wgsl.d.ts +12 -2
  81. package/dist/render/shaders/generated/flock.wgsl.js +12 -2
  82. package/dist/render/shaders/generated/globalMedium.wgsl.d.ts +12 -2
  83. package/dist/render/shaders/generated/globalMedium.wgsl.js +12 -2
  84. package/dist/render/shaders/generated/lightVolume.wgsl.d.ts +36 -16
  85. package/dist/render/shaders/generated/lightVolume.wgsl.js +39 -19
  86. package/dist/render/shaders/generated/panel.wgsl.d.ts +12 -2
  87. package/dist/render/shaders/generated/panel.wgsl.js +12 -2
  88. package/dist/render/shaders/generated/rush.wgsl.d.ts +31 -26
  89. package/dist/render/shaders/generated/rush.wgsl.js +31 -26
  90. package/dist/render/shaders/generated/scatter.wgsl.d.ts +12 -2
  91. package/dist/render/shaders/generated/scatter.wgsl.js +12 -2
  92. package/dist/render/shaders/generated/skinBlur.wgsl.d.ts +11 -1
  93. package/dist/render/shaders/generated/skinBlur.wgsl.js +11 -1
  94. package/dist/render/shaders/generated/sky.wgsl.d.ts +12 -2
  95. package/dist/render/shaders/generated/sky.wgsl.js +12 -2
  96. package/dist/render/shaders/generated/smoke.wgsl.d.ts +11 -1
  97. package/dist/render/shaders/generated/smoke.wgsl.js +11 -1
  98. package/dist/render/shaders/generated/text.wgsl.d.ts +11 -1
  99. package/dist/render/shaders/generated/text.wgsl.js +11 -1
  100. package/dist/render/shaders/generated/water.wgsl.d.ts +12 -2
  101. package/dist/render/shaders/generated/water.wgsl.js +12 -2
  102. package/dist/render/shaders/generated/windStreaks.wgsl.d.ts +12 -2
  103. package/dist/render/shaders/generated/windStreaks.wgsl.js +12 -2
  104. package/dist/render/shaders/globalMedium.js +14 -1
  105. package/dist/render/shaders/gpudriven/present.wgsl.d.ts +15 -6
  106. package/dist/render/shaders/gpudriven/present.wgsl.js +71 -8
  107. package/dist/render/shaders/lightVolume.js +4 -1
  108. package/dist/render/shaders/line.d.ts +1 -1
  109. package/dist/render/shaders/outputTransform.d.ts +8 -2
  110. package/dist/render/shaders/outputTransform.js +18 -11
  111. package/dist/render/shaders/panel.d.ts +1 -1
  112. package/dist/render/shaders/panel.js +5 -1
  113. package/dist/render/shaders/particle.d.ts +3 -3
  114. package/dist/render/shaders/rush.js +8 -0
  115. package/dist/render/shaders/scatter.d.ts +1 -1
  116. package/dist/render/shaders/scatter.js +4 -1
  117. package/dist/render/shaders/sdfText.d.ts +1 -1
  118. package/dist/render/shaders/skinBlur.js +5 -1
  119. package/dist/render/shaders/sky.d.ts +1 -1
  120. package/dist/render/shaders/sky.js +5 -1
  121. package/dist/render/shaders/smoke.d.ts +1 -1
  122. package/dist/render/shaders/smoke.js +4 -1
  123. package/dist/render/shaders/text.d.ts +1 -1
  124. package/dist/render/shaders/text.js +4 -1
  125. package/dist/render/shaders/water.d.ts +1 -1
  126. package/dist/render/shaders/water.js +12 -2
  127. package/dist/render/shaders/windStreaks.d.ts +1 -1
  128. package/dist/render/shaders/windStreaks.js +4 -1
  129. package/dist/render/surfaceLayerLooks.d.ts +16 -4
  130. package/dist/render/surfaceLayerLooks.js +33 -14
  131. package/dist/render/surfaceLayers.d.ts +2 -2
  132. package/dist/render/textRenderer.d.ts +3 -1
  133. package/dist/render/textRenderer.js +7 -1
  134. package/dist/render/vertexDefaults.d.ts +1 -1
  135. package/dist/render/vertexDefaults.js +1 -1
  136. package/dist/render/waterRenderer.d.ts +1 -1
  137. package/dist/render/waterRenderer.js +4 -1
  138. package/dist/render/windStreakRenderer.d.ts +3 -1
  139. package/dist/render/windStreakRenderer.js +5 -1
  140. package/package.json +6 -6
  141. package/src/geometry/meshBuilder.ts +21 -2
  142. package/src/index.ts +11 -2
  143. package/src/math/color.ts +39 -0
  144. package/src/render/backend/webgl2/renderer.ts +134 -44
  145. package/src/render/backend/webgl2/skinScatter.ts +5 -0
  146. package/src/render/backend/webgpu/giFieldPass.ts +3 -1
  147. package/src/render/backend/webgpu/gpuDrivenPass.ts +29 -13
  148. package/src/render/backend/webgpu/renderer.ts +104 -37
  149. package/src/render/backend/webgpu/skinScatterPass.ts +17 -1
  150. package/src/render/causticsRenderer.ts +5 -0
  151. package/src/render/decalPass.ts +3 -0
  152. package/src/render/flockRenderer.ts +5 -0
  153. package/src/render/globalMediumPass.ts +10 -1
  154. package/src/render/gradeColor.ts +90 -0
  155. package/src/render/lightMatrix.ts +32 -0
  156. package/src/render/passGrade.ts +58 -0
  157. package/src/render/plumeRenderer.ts +5 -0
  158. package/src/render/renderQuality.ts +60 -21
  159. package/src/render/sceneTarget.ts +3 -0
  160. package/src/render/sdfTextRenderer.ts +1 -1
  161. package/src/render/shaders/arcane.ts +5 -1
  162. package/src/render/shaders/bolt.ts +4 -1
  163. package/src/render/shaders/caustics.ts +4 -1
  164. package/src/render/shaders/decalProject.ts +12 -2
  165. package/src/render/shaders/film.ts +22 -9
  166. package/src/render/shaders/fire.ts +4 -1
  167. package/src/render/shaders/flat/layerLooks.ts +11 -19
  168. package/src/render/shaders/flat/layered.ts +33 -18
  169. package/src/render/shaders/flock.ts +4 -1
  170. package/src/render/shaders/generated/arcane.wgsl.ts +11 -1
  171. package/src/render/shaders/generated/bolt.wgsl.ts +12 -2
  172. package/src/render/shaders/generated/caustics.wgsl.ts +12 -2
  173. package/src/render/shaders/generated/decalProject.wgsl.ts +6 -1
  174. package/src/render/shaders/generated/film.wgsl.ts +12 -2
  175. package/src/render/shaders/generated/fire.wgsl.ts +11 -1
  176. package/src/render/shaders/generated/flat.wgsl.ts +23 -23
  177. package/src/render/shaders/generated/flock.wgsl.ts +12 -2
  178. package/src/render/shaders/generated/globalMedium.wgsl.ts +12 -2
  179. package/src/render/shaders/generated/lightVolume.wgsl.ts +39 -19
  180. package/src/render/shaders/generated/panel.wgsl.ts +12 -2
  181. package/src/render/shaders/generated/rush.wgsl.ts +31 -26
  182. package/src/render/shaders/generated/scatter.wgsl.ts +12 -2
  183. package/src/render/shaders/generated/skinBlur.wgsl.ts +11 -1
  184. package/src/render/shaders/generated/sky.wgsl.ts +12 -2
  185. package/src/render/shaders/generated/smoke.wgsl.ts +11 -1
  186. package/src/render/shaders/generated/text.wgsl.ts +11 -1
  187. package/src/render/shaders/generated/water.wgsl.ts +12 -2
  188. package/src/render/shaders/generated/windStreaks.wgsl.ts +12 -2
  189. package/src/render/shaders/globalMedium.ts +14 -1
  190. package/src/render/shaders/gpudriven/present.wgsl.ts +72 -8
  191. package/src/render/shaders/lightVolume.ts +4 -1
  192. package/src/render/shaders/outputTransform.ts +19 -11
  193. package/src/render/shaders/panel.ts +5 -1
  194. package/src/render/shaders/rush.ts +8 -0
  195. package/src/render/shaders/scatter.ts +4 -1
  196. package/src/render/shaders/skinBlur.ts +5 -1
  197. package/src/render/shaders/sky.ts +5 -1
  198. package/src/render/shaders/smoke.ts +4 -1
  199. package/src/render/shaders/text.ts +4 -1
  200. package/src/render/shaders/water.ts +12 -2
  201. package/src/render/shaders/windStreaks.ts +4 -1
  202. package/src/render/surfaceLayerLooks.ts +49 -18
  203. package/src/render/surfaceLayers.ts +2 -2
  204. package/src/render/textRenderer.ts +6 -0
  205. package/src/render/vertexDefaults.ts +1 -1
  206. package/src/render/waterRenderer.ts +5 -0
  207. package/src/render/windStreakRenderer.ts +5 -0
@@ -119,7 +119,9 @@ import { FlockRenderer } from '../../flockRenderer.js';
119
119
  import { TextRenderer } from '../../textRenderer.js';
120
120
  import { SdfTextRenderer } from '../../sdfTextRenderer.js';
121
121
  import { MOTION_BLUR_MAX_UV, OBJECT_BLUR_UNKEPT, RUSH_REACH_UV, clampMotionShare, } from '../../vertexDefaults.js';
122
- 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';
123
125
  import { DEFAULT_FILMIC_CURVE, FILMIC_CLAMPED, FILMIC_CONSTANTS, FILMIC_WITHOUT_COMPOSITE, resolveFilmicCurve, } from '../../filmicCurve.js';
124
126
  import { deviceSnappedCellSize } from '../../textLayout.js';
125
127
  import { WindStreakRenderer } from '../../windStreakRenderer.js';
@@ -978,6 +980,8 @@ export class WebGL2Renderer {
978
980
  warnedOverlayBlocks = false;
979
981
  /** Whether a probe is being baked, so the passes that would recurse into one can tell. */
980
982
  probePassActive = false;
983
+ /** `clearOf`'s scratch: a clear colour as the open target takes it. */
984
+ gradedClear = [0, 0, 0];
981
985
  /**
982
986
  * Whether a scene capture is being drawn (`captureScene`): an off-screen pass like a bake, so the
983
987
  * frame's own view, its refraction copy and skin's spread are left alone, but lit as the frame is.
@@ -1691,8 +1695,8 @@ export class WebGL2Renderer {
1691
1695
  sceneDepth: null,
1692
1696
  };
1693
1697
  const context = this.passContext;
1694
- context.outputTransform = this.gradeCode();
1695
- context.outputExposure = this.gradeExposure();
1698
+ context.outputTransform = this.passGrade();
1699
+ context.outputExposure = this.passGradeExposure();
1696
1700
  /*
1697
1701
  * The copies a pass declared it reads, taken here with everything drawn before it: colour
1698
1702
  * afresh at each reading pass, as WebGPU's `takePassColour` does, so a full-screen effect drawn
@@ -2821,11 +2825,11 @@ export class WebGL2Renderer {
2821
2825
  finally {
2822
2826
  target.end(gl);
2823
2827
  gl.frontFace(gl.CCW);
2828
+ this.capturePassActive = false;
2824
2829
  this.useFlatProgram();
2825
- gl.uniform1i(this.flatUniforms['uOutputTransform'] ?? null, this.gradeCode());
2826
- 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());
2827
2832
  this.materials.dirty();
2828
- this.capturePassActive = false;
2829
2833
  this.temporalJittering = jittering;
2830
2834
  this.restoreViewport();
2831
2835
  }
@@ -4105,7 +4109,7 @@ export class WebGL2Renderer {
4105
4109
  if (fog !== this.surfaceFog)
4106
4110
  gl.uniform1i(u['uFogEnabled'] ?? null, fog);
4107
4111
  if (!toneMapped)
4108
- gl.uniform1i(u['uOutputTransform'] ?? null, Math.min(this.gradeCode(), 1));
4112
+ gl.uniform1i(u['uOutputTransform'] ?? null, Math.min(this.passGrade(), 1));
4109
4113
  if (blend) {
4110
4114
  gl.enable(gl.BLEND);
4111
4115
  if (options.additive === true)
@@ -4176,7 +4180,7 @@ export class WebGL2Renderer {
4176
4180
  if (fog !== this.surfaceFog)
4177
4181
  gl.uniform1i(u['uFogEnabled'] ?? null, this.surfaceFog);
4178
4182
  if (!toneMapped)
4179
- gl.uniform1i(u['uOutputTransform'] ?? null, this.gradeCode());
4183
+ gl.uniform1i(u['uOutputTransform'] ?? null, this.passGrade());
4180
4184
  if (own)
4181
4185
  this.materials.dirty();
4182
4186
  this.useFlatProgram();
@@ -4328,7 +4332,7 @@ export class WebGL2Renderer {
4328
4332
  // raining dust into the sea.
4329
4333
  const surface = env.underwater?.surfaceY;
4330
4334
  const submerged = surface !== undefined && (camera.position[1] ?? 0) < surface;
4331
- 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())) {
4332
4336
  this.windStreakBudget.ask();
4333
4337
  }
4334
4338
  }
@@ -4356,7 +4360,7 @@ export class WebGL2Renderer {
4356
4360
  */
4357
4361
  drawText(text, viewportWidth, viewportHeight, originX, originY, style, timeSec) {
4358
4362
  this.scatterSkin();
4359
- if (text.draw(viewportWidth, viewportHeight, originX, originY, style, timeSec)) {
4363
+ if (text.draw(viewportWidth, viewportHeight, originX, originY, style, timeSec, interfaceGrade(this.passGrade()), 1)) {
4360
4364
  this.textBudget.ask();
4361
4365
  }
4362
4366
  }
@@ -4423,7 +4427,9 @@ export class WebGL2Renderer {
4423
4427
  * real draw call touches the unit in the first place.
4424
4428
  */
4425
4429
  /**
4426
- * 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.
4427
4433
  *
4428
4434
  * **Only where that pass is genuinely the last one to touch the frame.** With a scene target
4429
4435
  * the resolve grades, so a pass that graded as well would be applying a curve to its own
@@ -4431,20 +4437,34 @@ export class WebGL2Renderer {
4431
4437
  * `screenEffects` off there is no resolve at all and every pass writes straight to the canvas,
4432
4438
  * so each one has to grade itself or the frame comes out half converted: linear values in an
4433
4439
  * eight-bit buffer read as display values, which is a saturated, clipped version of the colour
4434
- * 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.
4435
4442
  *
4436
4443
  * One pair of accessors rather than the expression written out at each site, because that is
4437
4444
  * how the mesh pass and the particle pass came to disagree in the first place: the mesh pass
4438
4445
  * gated, and the particle pass uploaded the transform unconditionally to a stage that did not
4439
4446
  * yet declare it.
4440
4447
  */
4441
- gradeCode() {
4442
- return this.sceneTarget === null || !this.quality.hdrScene
4443
- ? forwardTransformCode(this.quality.outputTransform)
4444
- : 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);
4445
4456
  }
4446
- gradeExposure() {
4447
- return this.sceneTarget === null || !this.quality.hdrScene ? this.exposure : 1;
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;
4460
+ }
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);
4448
4468
  }
4449
4469
  drawSdfText(handle, model, color, opacity) {
4450
4470
  this.scatterSkin();
@@ -4454,7 +4474,7 @@ export class WebGL2Renderer {
4454
4474
  if (viewProj === null)
4455
4475
  return;
4456
4476
  /* Geometry, so the remapped matrix: `frameViewProj` itself stays raw for the motion blur. */
4457
- 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())) {
4458
4478
  this.sdfTextBudget.ask();
4459
4479
  this.restoreSurfaceTextureUnit();
4460
4480
  }
@@ -4468,7 +4488,7 @@ export class WebGL2Renderer {
4468
4488
  }
4469
4489
  drawFlock(flock, camera, timeSeconds, params, tint, windX = 0, windZ = 0) {
4470
4490
  this.flockBudget.ask();
4471
- 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());
4472
4492
  }
4473
4493
  /**
4474
4494
  * Build a scatter batch and fill it. Takes the data rather than handing back
@@ -4552,8 +4572,8 @@ export class WebGL2Renderer {
4552
4572
  the uniform at all, so it was a write to a location that did not exist; now that the
4553
4573
  stage grades itself it has to obey the same "only where this pass is last" rule the mesh
4554
4574
  pass does, or a scene with a composite grades its particles twice. */
4555
- gl.uniform1i(u['uOutputTransform'] ?? null, this.gradeCode());
4556
- gl.uniform1f(u['uOutputExposure'] ?? null, this.gradeExposure());
4575
+ gl.uniform1i(u['uOutputTransform'] ?? null, this.passGrade());
4576
+ gl.uniform1f(u['uOutputExposure'] ?? null, this.passGradeExposure());
4557
4577
  gl.uniform3fv(u['uAmbient'] ?? null, env.ambient);
4558
4578
  gl.uniform3fv(u['uAmbientGround'] ?? null, env.ambientGround ?? env.ambient);
4559
4579
  gl.uniform1i(u['uNoiseOctaves'] ?? null, this.quality.plumeNoiseOctaves);
@@ -4631,6 +4651,8 @@ export class WebGL2Renderer {
4631
4651
  gl.uniform3fv(u['uCoreColor'] ?? null, core);
4632
4652
  gl.uniform3fv(u['uEdgeColor'] ?? null, edge);
4633
4653
  gl.uniform1f(u['uCoreGain'] ?? null, coreGain);
4654
+ gl.uniform1i(u['uOutputTransform'] ?? null, this.passGrade());
4655
+ gl.uniform1f(u['uOutputExposure'] ?? null, this.passGradeExposure());
4634
4656
  bindAtmosphere(gl, u, env, this.reflectionPassActive ? this.reflectionAtmosphereY : (camera.position[1] ?? 0), this.quality.underwaterAtmosphere);
4635
4657
  batch.drawTo(gl, count);
4636
4658
  }
@@ -4688,8 +4710,8 @@ export class WebGL2Renderer {
4688
4710
  gl.uniform3fv(u['uColor'] ?? null, color);
4689
4711
  gl.uniform1f(u['uOpacity'] ?? null, opacity);
4690
4712
  gl.uniform1f(u['uSoftness'] ?? null, softness);
4691
- gl.uniform1i(u['uOutputTransform'] ?? null, this.gradeCode());
4692
- gl.uniform1f(u['uOutputExposure'] ?? null, this.gradeExposure());
4713
+ gl.uniform1i(u['uOutputTransform'] ?? null, this.passGrade());
4714
+ gl.uniform1f(u['uOutputExposure'] ?? null, this.passGradeExposure());
4693
4715
  bindAtmosphere(gl, u, env, this.reflectionPassActive ? this.reflectionAtmosphereY : (camera.position[1] ?? 0), this.quality.underwaterAtmosphere);
4694
4716
  lines.drawTo(gl, count, additive);
4695
4717
  }
@@ -4722,6 +4744,8 @@ export class WebGL2Renderer {
4722
4744
  gl.uniform3fv(u['uDirectionalColor'] ?? null, env.directionalColor);
4723
4745
  gl.uniform3fv(u['uAmbient'] ?? null, env.ambient);
4724
4746
  gl.uniform3fv(u['uCameraPos'] ?? null, camera.position);
4747
+ gl.uniform1i(u['uOutputTransform'] ?? null, this.passGrade());
4748
+ gl.uniform1f(u['uOutputExposure'] ?? null, this.passGradeExposure());
4725
4749
  bindAtmosphere(gl, u, env, this.reflectionPassActive ? this.reflectionAtmosphereY : (camera.position[1] ?? 0), this.quality.underwaterAtmosphere);
4726
4750
  // Foliage is thin and seen from both faces; culling it loses half of it.
4727
4751
  gl.disable(gl.CULL_FACE);
@@ -4855,13 +4879,13 @@ export class WebGL2Renderer {
4855
4879
  // --- Draws that own their context ----------------------------------------
4856
4880
  drawPlumes(plumes, camera, timeSeconds, env, windX = 0, windZ = 0, originX = 0, originY = 0, originZ = 0) {
4857
4881
  this.scatterSkin();
4858
- 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());
4859
4883
  }
4860
4884
  drawWater(water, camera, timeSeconds, settings, env, windX = 0, windZ = 0) {
4861
4885
  if (!this.quality.water)
4862
4886
  return;
4863
4887
  this.waterBudget.ask();
4864
- 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());
4865
4889
  }
4866
4890
  /**
4867
4891
  * Draw after the opaque scene: this is light added to surfaces already shaded,
@@ -4871,7 +4895,7 @@ export class WebGL2Renderer {
4871
4895
  this.scatterSkin();
4872
4896
  if (caustics === null || !this.quality.water)
4873
4897
  return;
4874
- 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());
4875
4899
  if (drew)
4876
4900
  this.causticsBudget.ask();
4877
4901
  }
@@ -4908,7 +4932,7 @@ export class WebGL2Renderer {
4908
4932
  return null;
4909
4933
  this.reflectionReadyThisFrame = false;
4910
4934
  this.reflectionAtmosphereY = source.position[1] ?? 0;
4911
- 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));
4912
4936
  this.gpuTimer.begin('reflection');
4913
4937
  this.reflectionPassActive = true;
4914
4938
  return camera;
@@ -6046,7 +6070,8 @@ export class WebGL2Renderer {
6046
6070
  gl.clear(gl.DEPTH_BUFFER_BIT);
6047
6071
  }
6048
6072
  else {
6049
- 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);
6050
6075
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
6051
6076
  }
6052
6077
  /*
@@ -6140,6 +6165,9 @@ export class WebGL2Renderer {
6140
6165
  gl.uniformMatrix4fv(this.panelUniforms['uClipCorrection'] ?? null, false, IDENTITY_MAT4);
6141
6166
  gl.uniform3fv(this.panelUniforms['uColor'] ?? null, color);
6142
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);
6143
6171
  gl.drawArrays(gl.TRIANGLES, 0, 6);
6144
6172
  gl.bindVertexArray(null);
6145
6173
  gl.depthMask(true);
@@ -6822,7 +6850,8 @@ export class WebGL2Renderer {
6822
6850
  this.sceneTarget?.begin(this.canvas.width, this.canvas.height);
6823
6851
  /* The materials' maps on the scene's framebuffer, cleared and disabled: `reflectionKeeps`. */
6824
6852
  this.reflectionTargets?.begin(this.canvas.width, this.canvas.height);
6825
- 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);
6826
6855
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
6827
6856
  }
6828
6857
  /**
@@ -6957,7 +6986,7 @@ export class WebGL2Renderer {
6957
6986
  exposure: this.exposure,
6958
6987
  film: this.filmConstants,
6959
6988
  }
6960
- : { transform: 0, exposure: 1 };
6989
+ : { transform: 0, exposure: 1, sceneGraded: this.passGrade() !== 0 };
6961
6990
  /*
6962
6991
  * Depth of field, on the same ceiling-and-dial split as bloom above: `depthOfField` decides
6963
6992
  * how far a defocused point may spread and this frame's `setDepthOfField` decides where the
@@ -7007,7 +7036,7 @@ export class WebGL2Renderer {
7007
7036
  else if (this.sceneTarget !== null) {
7008
7037
  this.decals ??= new DecalPass(this.gl);
7009
7038
  this.sceneTarget.bind();
7010
- 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());
7011
7040
  }
7012
7041
  }
7013
7042
  /*
@@ -7159,7 +7188,7 @@ export class WebGL2Renderer {
7159
7188
  /* The march finished on the default framebuffer, which is not where the frame is being
7160
7189
  drawn. See `resolveReflections`, which is the same two-step for the same reason. */
7161
7190
  this.sceneTarget.bind();
7162
- 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());
7163
7192
  }
7164
7193
  }
7165
7194
  }
@@ -7580,12 +7609,11 @@ export class WebGL2Renderer {
7580
7609
  * exists to keep. With `screenEffects` off there is no resolve at all and the mesh pass
7581
7610
  * writes straight to the canvas, so it stays where it always was.
7582
7611
  *
7583
- * The consequence worth stating: full grading needs the composite. Without it the sky, the
7584
- * particles, the film and the water are still ungraded beside a world that is, which is the
7585
- * 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.
7586
7614
  */
7587
- gl.uniform1i(u['uOutputTransform'] ?? null, this.gradeCode());
7588
- gl.uniform1f(u['uOutputExposure'] ?? null, this.gradeExposure());
7615
+ gl.uniform1i(u['uOutputTransform'] ?? null, this.passGrade());
7616
+ gl.uniform1f(u['uOutputExposure'] ?? null, this.passGradeExposure());
7589
7617
  gl.uniform3fv(u['uCameraPos'] ?? null, camera.position);
7590
7618
  /*
7591
7619
  * Solid, every frame, before anything asks otherwise. A GL uniform starts at
@@ -8038,7 +8066,7 @@ export class WebGL2Renderer {
8038
8066
  return;
8039
8067
  const depth = scene.snapshotDepth(true);
8040
8068
  mat4.invert(this.skinInverseProjection, projection);
8041
- skin.spread(scene, depth, this.emptyTexture2D, projection, this.skinInverseProjection);
8069
+ skin.spread(scene, depth, this.emptyTexture2D, projection, this.skinInverseProjection, this.passGrade(), this.passGradeExposure());
8042
8070
  this.useFlatProgram();
8043
8071
  }
8044
8072
  /** One draw of a mesh, as `drawMesh` describes, of the half of a skin `half` names. */
@@ -8361,7 +8389,7 @@ export class WebGL2Renderer {
8361
8389
  means rather than dropping the whole transform. `Math.min` rather than a literal, so a
8362
8390
  renderer asked for `none` stays at none and one asked for `srgb` is already there. */
8363
8391
  if (!toneMapped)
8364
- gl.uniform1i(u['uOutputTransform'] ?? null, Math.min(this.gradeCode(), 1));
8392
+ gl.uniform1i(u['uOutputTransform'] ?? null, Math.min(this.passGrade(), 1));
8365
8393
  /* Scoped and handed back like every other state here, so a tinted draw cannot leak its
8366
8394
  colour onto the next one. See `TranslucentMeshOptions.tint`. */
8367
8395
  const tint = options.tint ?? null;
@@ -8446,7 +8474,7 @@ export class WebGL2Renderer {
8446
8474
  if (fog !== this.surfaceFog)
8447
8475
  gl.uniform1i(u['uFogEnabled'] ?? null, this.surfaceFog);
8448
8476
  if (!toneMapped)
8449
- gl.uniform1i(u['uOutputTransform'] ?? null, this.gradeCode());
8477
+ gl.uniform1i(u['uOutputTransform'] ?? null, this.passGrade());
8450
8478
  if (own)
8451
8479
  this.materials.dirty();
8452
8480
  }
@@ -8535,7 +8563,7 @@ export class WebGL2Renderer {
8535
8563
  /*
8536
8564
  * **A probe stores radiance, not display pixels.**
8537
8565
  *
8538
- * `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,
8539
8567
  * so every face was baked through the whole output transform and the shader then sampled the
8540
8568
  * result as though it were light. Two things came of that and both read as "the reflection is
8541
8569
  * dull": the curve pulls everything bright toward 1 *before* it reaches the cube, so no
@@ -8585,16 +8613,17 @@ export class WebGL2Renderer {
8585
8613
  }
8586
8614
  finally {
8587
8615
  this.gl.frontFace(this.gl.CCW);
8588
- this.useFlatProgram();
8589
- this.gl.uniform1i(this.flatUniforms['uOutputTransform'] ?? null, this.gradeCode());
8590
- this.gl.uniform1f(this.flatUniforms['uOutputExposure'] ?? null, this.gradeExposure());
8591
- this.materials.dirty();
8592
8616
  /*
8593
8617
  * Cleared even if a caller's own draw threw, because leaving this set would keep every
8594
- * 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.
8595
8620
  */
8596
8621
  this.probePassActive = false;
8597
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();
8598
8627
  /* Back to the canvas, or to the frame's own target if one is bound. See `SceneTarget`. */
8599
8628
  if (this.sceneTarget !== null && !this.framePresented)
8600
8629
  this.sceneTarget.bind();
@@ -8778,6 +8807,8 @@ export class WebGL2Renderer {
8778
8807
  gl.uniformMatrix4fv(u['uModel'] ?? null, false, model);
8779
8808
  gl.uniform3fv(u['uCameraPos'] ?? null, camera.position);
8780
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());
8781
8812
  gl.uniform1f(u['uLength'] ?? null, length);
8782
8813
  gl.uniform1f(u['uSpread'] ?? null, spread);
8783
8814
  gl.uniform1f(u['uDust'] ?? null, clamp(options.dust ?? 0, 0, 1));
@@ -8910,6 +8941,8 @@ export class WebGL2Renderer {
8910
8941
  gl.uniform3fv(u['uCameraPos'] ?? null, camera.position);
8911
8942
  gl.uniform1f(u['uTime'] ?? null, timeSeconds);
8912
8943
  gl.uniform1f(u['uSheen'] ?? null, sheen);
8944
+ gl.uniform1i(u['uOutputTransform'] ?? null, this.passGrade());
8945
+ gl.uniform1f(u['uOutputExposure'] ?? null, this.passGradeExposure());
8913
8946
  gl.uniform1f(u['uFilmRoughness'] ?? null, Math.min(1, Math.max(0, options.roughness ?? 0)));
8914
8947
  gl.uniform1f(u['uFilmRoughnessCycles'] ?? null, Math.max(0.01, options.roughnessCyclesPerMetre ?? 60));
8915
8948
  gl.uniform1i(u['uClipEnabled'] ?? null, this.reflectionPassActive ? 1 : 0);
@@ -8953,6 +8986,8 @@ export class WebGL2Renderer {
8953
8986
  gl.uniform1f(u['uMoonPhase'] ?? null, sky.moonPhase);
8954
8987
  gl.uniform1f(u['uNightFactor'] ?? null, sky.nightFactor);
8955
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());
8956
8991
  bindAtmosphere(gl, u, env, this.reflectionPassActive ? this.reflectionAtmosphereY : (camera.position[1] ?? 0), this.quality.underwaterAtmosphere);
8957
8992
  gl.bindVertexArray(this.skyVao);
8958
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;
@@ -646,7 +646,16 @@ export class GpuDrivenPass {
646
646
  shadowed: true,
647
647
  blended: false,
648
648
  };
649
- gradeWarned = false;
649
+ /**
650
+ * The frame's grade for the blit, `PassContext.outputTransform` and its exposure, and the code it
651
+ * was last written with, so a frame writes it only when it changes. See `present.wgsl.ts`.
652
+ */
653
+ gradeBuffer = null;
654
+ gradeStaging = new ArrayBuffer(16);
655
+ gradeInts = new Int32Array(this.gradeStaging);
656
+ gradeFloats = new Float32Array(this.gradeStaging);
657
+ gradeWrittenCode = Number.NaN;
658
+ gradeWrittenExposure = Number.NaN;
650
659
  constructor(scene, materials, shadow = {}) {
651
660
  if (materials.length > MATERIAL_SLOTS) {
652
661
  throw new Error(`[driftengine] the gpu-driven pass holds ${MATERIAL_SLOTS} materials and was given ` +
@@ -1937,10 +1946,17 @@ export class GpuDrivenPass {
1937
1946
  { binding: 21, resource: this.glassSamplerFor(device) },
1938
1947
  ],
1939
1948
  });
1949
+ this.gradeBuffer ??= device.createBuffer({
1950
+ label: 'gpu-driven blit grade',
1951
+ size: this.gradeStaging.byteLength,
1952
+ usage: UNIFORM | COPY_DST,
1953
+ });
1954
+ this.gradeWrittenCode = Number.NaN;
1940
1955
  this.blitGroup = device.createBindGroup({
1941
1956
  layout: this.blitPipeline.getBindGroupLayout(0),
1942
1957
  entries: [
1943
1958
  { binding: 0, resource: this.colourView },
1959
+ { binding: 2, resource: { buffer: this.gradeBuffer } },
1944
1960
  /* The depth the raster wrote, sampled so the blit can hand it to the frame. */
1945
1961
  ...(this.presentDepth
1946
1962
  ? [
@@ -2867,16 +2883,15 @@ export class GpuDrivenPass {
2867
2883
  return;
2868
2884
  if (this.blitPipeline === null || this.blitGroup === null)
2869
2885
  return;
2870
- if (ctx.outputTransform !== 0 && !this.gradeWarned) {
2871
- this.gradeWarned = true;
2872
- /*
2873
- * The pass writes linear light and expects a composite to grade it. A frame with no
2874
- * composite grades per pass, and applying the curve here would be a second copy of it —
2875
- * which is the thing `SHADE_MATERIAL_WGSL` is built to avoid. Said once, out loud, because
2876
- * a silently ungraded frame is a difference somebody would attribute to the pipeline.
2877
- */
2878
- console.warn('[driftengine] the gpu-driven pass writes linear light and this frame has no composite ' +
2879
- 'to grade it, so the picture will be flat. Use a quality profile with a scene target.');
2886
+ /* The frame's grade, written when it changes: 0 where the resolve grades. See present.wgsl.ts. */
2887
+ const changed = ctx.outputTransform !== this.gradeWrittenCode ||
2888
+ ctx.outputExposure !== this.gradeWrittenExposure;
2889
+ if (changed && this.device !== null && this.gradeBuffer !== null) {
2890
+ this.gradeWrittenCode = ctx.outputTransform;
2891
+ this.gradeWrittenExposure = ctx.outputExposure;
2892
+ this.gradeInts[0] = ctx.outputTransform;
2893
+ this.gradeFloats[1] = ctx.outputExposure;
2894
+ this.device.queue.writeBuffer(this.gradeBuffer, 0, this.gradeStaging);
2880
2895
  }
2881
2896
  ctx.pass.setPipeline(this.blitPipeline);
2882
2897
  ctx.pass.setBindGroup(0, this.blitGroup);
@@ -2888,6 +2903,8 @@ export class GpuDrivenPass {
2888
2903
  if (device.backend !== 'webgpu')
2889
2904
  return;
2890
2905
  this.disposeTargets();
2906
+ this.gradeBuffer?.destroy();
2907
+ this.gradeBuffer = null;
2891
2908
  this.shadowTexture?.destroy();
2892
2909
  this.glassTintTexture?.destroy();
2893
2910
  this.glassTintNone?.destroy();
@@ -13,7 +13,7 @@ import type { PipelineCache } from './pipelineCache.ts';
13
13
  export declare const PANEL_VERT_FIELDS: UniformFields;
14
14
  export declare const PANEL_VERT_SIZE: 96;
15
15
  export declare const PANEL_FRAG_FIELDS: UniformFields;
16
- export declare const PANEL_FRAG_SIZE: 16;
16
+ export declare const PANEL_FRAG_SIZE: 32;
17
17
  export declare const PANEL_VERTEX_COUNT: number;
18
18
  export declare function createPanelCorners(device: GPUDevice): GPUBuffer;
19
19
  export declare function createPanelBindGroupLayout(device: GPUDevice): GPUBindGroupLayout;
@@ -26,7 +26,7 @@ import type { PipelineCache } from './pipelineCache.ts';
26
26
  * float target the generated shaders can sample as an ordinary texture.
27
27
  */
28
28
  export declare const RUSH_FRAG_FIELDS: UniformFields;
29
- export declare const RUSH_FRAG_SIZE: 272;
29
+ export declare const RUSH_FRAG_SIZE: 288;
30
30
  declare const RUSH_UNIFORMS: 1;
31
31
  declare const RUSH_TEXTURES: {
32
32
  readonly uScene: {
@@ -77,7 +77,7 @@ export declare const AO_BLUR_SIZE: 48;
77
77
  export declare const MEDIUM_FRAG_FIELDS: UniformFields;
78
78
  export declare const MEDIUM_FRAG_SIZE: 304;
79
79
  export declare const MEDIUM_UPSAMPLE_FIELDS: UniformFields;
80
- export declare const MEDIUM_UPSAMPLE_SIZE: 32;
80
+ export declare const MEDIUM_UPSAMPLE_SIZE: 48;
81
81
  export declare const BLOOM_PREFILTER_FIELDS: UniformFields;
82
82
  /**
83
83
  * The other two stages' blocks, read by name like every other block here.