@driftengine/core 4.12.0 → 4.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (207) hide show
  1. package/CHANGELOG.json +66 -0
  2. package/README.md +18 -9
  3. package/dist/geometry/meshBuilder.d.ts +10 -0
  4. package/dist/geometry/meshBuilder.js +11 -2
  5. package/dist/index.d.ts +3 -2
  6. package/dist/index.js +3 -2
  7. package/dist/math/color.d.ts +25 -0
  8. package/dist/math/color.js +37 -0
  9. package/dist/render/backend/webgl2/renderer.d.ts +17 -4
  10. package/dist/render/backend/webgl2/renderer.js +82 -47
  11. package/dist/render/backend/webgl2/skinScatter.d.ts +3 -1
  12. package/dist/render/backend/webgl2/skinScatter.js +5 -1
  13. package/dist/render/backend/webgpu/boltPass.d.ts +1 -1
  14. package/dist/render/backend/webgpu/causticsPass.d.ts +1 -1
  15. package/dist/render/backend/webgpu/filmPass.d.ts +1 -1
  16. package/dist/render/backend/webgpu/flockPass.d.ts +1 -1
  17. package/dist/render/backend/webgpu/giFieldPass.d.ts +3 -1
  18. package/dist/render/backend/webgpu/gpuDrivenPass.d.ts +10 -1
  19. package/dist/render/backend/webgpu/gpuDrivenPass.js +28 -11
  20. package/dist/render/backend/webgpu/panelPass.d.ts +1 -1
  21. package/dist/render/backend/webgpu/postPass.d.ts +2 -2
  22. package/dist/render/backend/webgpu/renderer.d.ts +19 -8
  23. package/dist/render/backend/webgpu/renderer.js +94 -38
  24. package/dist/render/backend/webgpu/scatterPass.d.ts +1 -1
  25. package/dist/render/backend/webgpu/skinScatterPass.d.ts +5 -1
  26. package/dist/render/backend/webgpu/skinScatterPass.js +9 -3
  27. package/dist/render/backend/webgpu/skyPass.d.ts +11 -1
  28. package/dist/render/backend/webgpu/waterPass.d.ts +1 -1
  29. package/dist/render/backend/webgpu/windStreakPass.d.ts +1 -1
  30. package/dist/render/causticsRenderer.d.ts +3 -1
  31. package/dist/render/causticsRenderer.js +5 -1
  32. package/dist/render/decalPass.d.ts +3 -1
  33. package/dist/render/decalPass.js +4 -1
  34. package/dist/render/flockRenderer.d.ts +1 -1
  35. package/dist/render/flockRenderer.js +4 -1
  36. package/dist/render/globalMediumPass.d.ts +3 -1
  37. package/dist/render/globalMediumPass.js +5 -1
  38. package/dist/render/gradeColor.d.ts +21 -0
  39. package/dist/render/gradeColor.js +64 -0
  40. package/dist/render/lightMatrix.d.ts +13 -0
  41. package/dist/render/lightMatrix.js +17 -0
  42. package/dist/render/passGrade.d.ts +13 -0
  43. package/dist/render/passGrade.js +47 -0
  44. package/dist/render/plumeRenderer.d.ts +1 -1
  45. package/dist/render/plumeRenderer.js +4 -1
  46. package/dist/render/renderQuality.d.ts +57 -18
  47. package/dist/render/renderQuality.js +3 -3
  48. package/dist/render/sceneTarget.d.ts +2 -0
  49. package/dist/render/sceneTarget.js +1 -0
  50. package/dist/render/sdfTextRenderer.d.ts +1 -1
  51. package/dist/render/sdfTextRenderer.js +1 -1
  52. package/dist/render/shaders/arcane.d.ts +1 -1
  53. package/dist/render/shaders/arcane.js +5 -1
  54. package/dist/render/shaders/bolt.d.ts +1 -1
  55. package/dist/render/shaders/bolt.js +4 -1
  56. package/dist/render/shaders/caustics.d.ts +1 -1
  57. package/dist/render/shaders/caustics.js +4 -1
  58. package/dist/render/shaders/decalProject.js +12 -2
  59. package/dist/render/shaders/film.d.ts +1 -1
  60. package/dist/render/shaders/film.js +22 -9
  61. package/dist/render/shaders/fire.d.ts +1 -1
  62. package/dist/render/shaders/fire.js +4 -1
  63. package/dist/render/shaders/flat/layerLooks.js +11 -19
  64. package/dist/render/shaders/flat/layered.js +33 -18
  65. package/dist/render/shaders/flock.d.ts +1 -1
  66. package/dist/render/shaders/flock.js +4 -1
  67. package/dist/render/shaders/generated/arcane.wgsl.d.ts +11 -1
  68. package/dist/render/shaders/generated/arcane.wgsl.js +11 -1
  69. package/dist/render/shaders/generated/bolt.wgsl.d.ts +12 -2
  70. package/dist/render/shaders/generated/bolt.wgsl.js +12 -2
  71. package/dist/render/shaders/generated/caustics.wgsl.d.ts +12 -2
  72. package/dist/render/shaders/generated/caustics.wgsl.js +12 -2
  73. package/dist/render/shaders/generated/decalProject.wgsl.d.ts +6 -1
  74. package/dist/render/shaders/generated/decalProject.wgsl.js +6 -1
  75. package/dist/render/shaders/generated/film.wgsl.d.ts +12 -2
  76. package/dist/render/shaders/generated/film.wgsl.js +12 -2
  77. package/dist/render/shaders/generated/fire.wgsl.d.ts +11 -1
  78. package/dist/render/shaders/generated/fire.wgsl.js +11 -1
  79. package/dist/render/shaders/generated/flat.wgsl.js +23 -23
  80. package/dist/render/shaders/generated/flock.wgsl.d.ts +12 -2
  81. package/dist/render/shaders/generated/flock.wgsl.js +12 -2
  82. package/dist/render/shaders/generated/globalMedium.wgsl.d.ts +12 -2
  83. package/dist/render/shaders/generated/globalMedium.wgsl.js +12 -2
  84. package/dist/render/shaders/generated/lightVolume.wgsl.d.ts +36 -16
  85. package/dist/render/shaders/generated/lightVolume.wgsl.js +39 -19
  86. package/dist/render/shaders/generated/panel.wgsl.d.ts +12 -2
  87. package/dist/render/shaders/generated/panel.wgsl.js +12 -2
  88. package/dist/render/shaders/generated/rush.wgsl.d.ts +31 -26
  89. package/dist/render/shaders/generated/rush.wgsl.js +31 -26
  90. package/dist/render/shaders/generated/scatter.wgsl.d.ts +12 -2
  91. package/dist/render/shaders/generated/scatter.wgsl.js +12 -2
  92. package/dist/render/shaders/generated/skinBlur.wgsl.d.ts +11 -1
  93. package/dist/render/shaders/generated/skinBlur.wgsl.js +11 -1
  94. package/dist/render/shaders/generated/sky.wgsl.d.ts +12 -2
  95. package/dist/render/shaders/generated/sky.wgsl.js +12 -2
  96. package/dist/render/shaders/generated/smoke.wgsl.d.ts +11 -1
  97. package/dist/render/shaders/generated/smoke.wgsl.js +11 -1
  98. package/dist/render/shaders/generated/text.wgsl.d.ts +11 -1
  99. package/dist/render/shaders/generated/text.wgsl.js +11 -1
  100. package/dist/render/shaders/generated/water.wgsl.d.ts +12 -2
  101. package/dist/render/shaders/generated/water.wgsl.js +12 -2
  102. package/dist/render/shaders/generated/windStreaks.wgsl.d.ts +12 -2
  103. package/dist/render/shaders/generated/windStreaks.wgsl.js +12 -2
  104. package/dist/render/shaders/globalMedium.js +14 -1
  105. package/dist/render/shaders/gpudriven/present.wgsl.d.ts +15 -6
  106. package/dist/render/shaders/gpudriven/present.wgsl.js +71 -8
  107. package/dist/render/shaders/lightVolume.js +4 -1
  108. package/dist/render/shaders/line.d.ts +1 -1
  109. package/dist/render/shaders/outputTransform.d.ts +8 -2
  110. package/dist/render/shaders/outputTransform.js +18 -11
  111. package/dist/render/shaders/panel.d.ts +1 -1
  112. package/dist/render/shaders/panel.js +5 -1
  113. package/dist/render/shaders/particle.d.ts +3 -3
  114. package/dist/render/shaders/rush.js +8 -0
  115. package/dist/render/shaders/scatter.d.ts +1 -1
  116. package/dist/render/shaders/scatter.js +4 -1
  117. package/dist/render/shaders/sdfText.d.ts +1 -1
  118. package/dist/render/shaders/skinBlur.js +5 -1
  119. package/dist/render/shaders/sky.d.ts +1 -1
  120. package/dist/render/shaders/sky.js +5 -1
  121. package/dist/render/shaders/smoke.d.ts +1 -1
  122. package/dist/render/shaders/smoke.js +4 -1
  123. package/dist/render/shaders/text.d.ts +1 -1
  124. package/dist/render/shaders/text.js +4 -1
  125. package/dist/render/shaders/water.d.ts +1 -1
  126. package/dist/render/shaders/water.js +12 -2
  127. package/dist/render/shaders/windStreaks.d.ts +1 -1
  128. package/dist/render/shaders/windStreaks.js +4 -1
  129. package/dist/render/surfaceLayerLooks.d.ts +16 -4
  130. package/dist/render/surfaceLayerLooks.js +33 -14
  131. package/dist/render/surfaceLayers.d.ts +2 -2
  132. package/dist/render/textRenderer.d.ts +3 -1
  133. package/dist/render/textRenderer.js +7 -1
  134. package/dist/render/vertexDefaults.d.ts +1 -1
  135. package/dist/render/vertexDefaults.js +1 -1
  136. package/dist/render/waterRenderer.d.ts +1 -1
  137. package/dist/render/waterRenderer.js +4 -1
  138. package/dist/render/windStreakRenderer.d.ts +3 -1
  139. package/dist/render/windStreakRenderer.js +5 -1
  140. package/package.json +6 -6
  141. package/src/geometry/meshBuilder.ts +21 -2
  142. package/src/index.ts +11 -2
  143. package/src/math/color.ts +39 -0
  144. package/src/render/backend/webgl2/renderer.ts +134 -44
  145. package/src/render/backend/webgl2/skinScatter.ts +5 -0
  146. package/src/render/backend/webgpu/giFieldPass.ts +3 -1
  147. package/src/render/backend/webgpu/gpuDrivenPass.ts +29 -13
  148. package/src/render/backend/webgpu/renderer.ts +104 -37
  149. package/src/render/backend/webgpu/skinScatterPass.ts +17 -1
  150. package/src/render/causticsRenderer.ts +5 -0
  151. package/src/render/decalPass.ts +3 -0
  152. package/src/render/flockRenderer.ts +5 -0
  153. package/src/render/globalMediumPass.ts +10 -1
  154. package/src/render/gradeColor.ts +90 -0
  155. package/src/render/lightMatrix.ts +32 -0
  156. package/src/render/passGrade.ts +58 -0
  157. package/src/render/plumeRenderer.ts +5 -0
  158. package/src/render/renderQuality.ts +60 -21
  159. package/src/render/sceneTarget.ts +3 -0
  160. package/src/render/sdfTextRenderer.ts +1 -1
  161. package/src/render/shaders/arcane.ts +5 -1
  162. package/src/render/shaders/bolt.ts +4 -1
  163. package/src/render/shaders/caustics.ts +4 -1
  164. package/src/render/shaders/decalProject.ts +12 -2
  165. package/src/render/shaders/film.ts +22 -9
  166. package/src/render/shaders/fire.ts +4 -1
  167. package/src/render/shaders/flat/layerLooks.ts +11 -19
  168. package/src/render/shaders/flat/layered.ts +33 -18
  169. package/src/render/shaders/flock.ts +4 -1
  170. package/src/render/shaders/generated/arcane.wgsl.ts +11 -1
  171. package/src/render/shaders/generated/bolt.wgsl.ts +12 -2
  172. package/src/render/shaders/generated/caustics.wgsl.ts +12 -2
  173. package/src/render/shaders/generated/decalProject.wgsl.ts +6 -1
  174. package/src/render/shaders/generated/film.wgsl.ts +12 -2
  175. package/src/render/shaders/generated/fire.wgsl.ts +11 -1
  176. package/src/render/shaders/generated/flat.wgsl.ts +23 -23
  177. package/src/render/shaders/generated/flock.wgsl.ts +12 -2
  178. package/src/render/shaders/generated/globalMedium.wgsl.ts +12 -2
  179. package/src/render/shaders/generated/lightVolume.wgsl.ts +39 -19
  180. package/src/render/shaders/generated/panel.wgsl.ts +12 -2
  181. package/src/render/shaders/generated/rush.wgsl.ts +31 -26
  182. package/src/render/shaders/generated/scatter.wgsl.ts +12 -2
  183. package/src/render/shaders/generated/skinBlur.wgsl.ts +11 -1
  184. package/src/render/shaders/generated/sky.wgsl.ts +12 -2
  185. package/src/render/shaders/generated/smoke.wgsl.ts +11 -1
  186. package/src/render/shaders/generated/text.wgsl.ts +11 -1
  187. package/src/render/shaders/generated/water.wgsl.ts +12 -2
  188. package/src/render/shaders/generated/windStreaks.wgsl.ts +12 -2
  189. package/src/render/shaders/globalMedium.ts +14 -1
  190. package/src/render/shaders/gpudriven/present.wgsl.ts +72 -8
  191. package/src/render/shaders/lightVolume.ts +4 -1
  192. package/src/render/shaders/outputTransform.ts +19 -11
  193. package/src/render/shaders/panel.ts +5 -1
  194. package/src/render/shaders/rush.ts +8 -0
  195. package/src/render/shaders/scatter.ts +4 -1
  196. package/src/render/shaders/skinBlur.ts +5 -1
  197. package/src/render/shaders/sky.ts +5 -1
  198. package/src/render/shaders/smoke.ts +4 -1
  199. package/src/render/shaders/text.ts +4 -1
  200. package/src/render/shaders/water.ts +12 -2
  201. package/src/render/shaders/windStreaks.ts +4 -1
  202. package/src/render/surfaceLayerLooks.ts +49 -18
  203. package/src/render/surfaceLayers.ts +2 -2
  204. package/src/render/textRenderer.ts +6 -0
  205. package/src/render/vertexDefaults.ts +1 -1
  206. package/src/render/waterRenderer.ts +5 -0
  207. package/src/render/windStreakRenderer.ts +5 -0
@@ -99,7 +99,7 @@ export declare class WebGPURenderer implements RendererApi {
99
99
  * **Resolved by the caller and held, rather than defaulted per use.** Every shadow term
100
100
  * below reads it, and the constants they replaced were `DEFAULT_RENDER_QUALITY` copies —
101
101
  * which measured wrong the moment a scene overrode one. `demo/dayClock` resolves
102
- * `directionalShadowMaxSlope` to 9 where the default is 3.
102
+ * `directionalShadowMaxSlope` to 9 where the default was 3 (60 since 4.13.0).
103
103
  */
104
104
  readonly quality: Readonly<RenderQuality>;
105
105
  /** Whether `endFrame` put an image on the canvas. Read by the recorder and the harness. */
@@ -716,6 +716,7 @@ export declare class WebGPURenderer implements RendererApi {
716
716
  private readonly mediumUpsampleUniforms;
717
717
  private readonly mediumUpsampleStaging;
718
718
  private readonly mediumUpsampleFloats;
719
+ private readonly mediumUpsampleInts;
719
720
  private mediumUpsampleGroup;
720
721
  /** The inverse projection the composite linearises a depth with, built once per frame. */
721
722
  private readonly mediumInvProjection;
@@ -944,6 +945,12 @@ export declare class WebGPURenderer implements RendererApi {
944
945
  */
945
946
  private readonly skyUniforms;
946
947
  private readonly skyFloats;
948
+ /** The same block's ints: the grade the sky draws with. */
949
+ private readonly skyInts;
950
+ /** `clearOf`'s scratch: a clear colour as the open target takes it. */
951
+ private readonly gradedClear;
952
+ /** An inset's clear as its quad's uniform, rewritten in place: the colour and an alpha of 1. */
953
+ private readonly insetClear;
947
954
  private readonly skyBindGroup;
948
955
  /** The camera's inverse with the clip correction undone, rebuilt once a frame. */
949
956
  private readonly skyInvViewProj;
@@ -1026,6 +1033,8 @@ export declare class WebGPURenderer implements RendererApi {
1026
1033
  private decalUniforms;
1027
1034
  private readonly decalStaging;
1028
1035
  private readonly decalFloats;
1036
+ /** The same slots' ints: the grade the frame under the marks was drawn with. */
1037
+ private readonly decalInts;
1029
1038
  private decalBindGroup;
1030
1039
  /** Said once where a mark cannot be drawn at all, rather than dropped in silence. */
1031
1040
  private decalsRefused;
@@ -1546,14 +1555,16 @@ export declare class WebGPURenderer implements RendererApi {
1546
1555
  * array allocated with the batch, so the frame loop still allocates nothing.
1547
1556
  */
1548
1557
  /**
1549
- * The output transform a forward pass should apply, and the exposure it should apply it at.
1550
- *
1551
- * The WebGPU half of `Renderer.gradeCode`, and it has to agree with it draw for draw: this is
1552
- * one verb with two implementations, and a frame that grades on one backend and not the other
1553
- * is the silent kind of disagreement. See that method for what the gate is protecting.
1558
+ * The grade a pass applies to what it draws into the target open now, and the exposure it
1559
+ * applies it at: the WebGPU half of `Renderer.passGrade`, and it has to agree with it draw for
1560
+ * draw. The decision is `passGrade.ts`'s, so both halves only gather the state it is taken from.
1554
1561
  */
1555
- private gradeCode;
1556
- private gradeExposure;
1562
+ private passGrade;
1563
+ private passGradeExposure;
1564
+ /** Whether the frame's resolve applies the transform, so the passes before it draw linear. */
1565
+ private resolveGrades;
1566
+ /** A clear colour as the open target takes it, graded as a pass would. See `gradeColor.ts`. */
1567
+ private clearOf;
1557
1568
  drawParticles(batch: GpuParticles, data: ParticleInstances, camera: Camera, env: Environment, timeSeconds: number): void;
1558
1569
  /**
1559
1570
  * Draw particles a caller's own compute wrote, where they lie. See `DeviceParticles`.
@@ -75,7 +75,9 @@ import { createSdfTextBindGroup, createSdfTextBindGroupLayout, GpuSdfText, sdfTe
75
75
  import { createFilmBindGroup, createFilmBindGroupLayout, filmPipeline, FILM_FRAG_FIELDS, FILM_FRAG_SIZE, FILM_VERT_FIELDS, FILM_VERT_SIZE, } from './filmPass.js';
76
76
  import { reflectionTargetSize } from '../../planarReflectionDraw.js';
77
77
  import { drawingBufferSize } from '../../drawingBuffer.js';
78
- import { MOTION_BLUR_MAX_UV, OBJECT_BLUR_UNKEPT, OUTPUT_TRANSFORM_CODE, RUSH_REACH_UV, clampMotionShare, forwardTransformCode, } from '../../vertexDefaults.js';
78
+ import { MOTION_BLUR_MAX_UV, OBJECT_BLUR_UNKEPT, OUTPUT_TRANSFORM_CODE, RUSH_REACH_UV, clampMotionShare, } from '../../vertexDefaults.js';
79
+ import { gradeColorInto } from '../../gradeColor.js';
80
+ import { interfaceGrade, passGradeCode, passGradeExposure } from '../../passGrade.js';
79
81
  import { FRINGE_FLOATS, resolveFringe } from '../../shaders/fringe.js';
80
82
  import { displayHeadroom } from '../../displayRange.js';
81
83
  import { DEFAULT_FILMIC_CURVE, FILMIC_CLAMPED, FILMIC_CONSTANTS, FILMIC_WITHOUT_COMPOSITE, resolveFilmicCurve, } from '../../filmicCurve.js';
@@ -834,7 +836,7 @@ export class WebGPURenderer {
834
836
  * **Resolved by the caller and held, rather than defaulted per use.** Every shadow term
835
837
  * below reads it, and the constants they replaced were `DEFAULT_RENDER_QUALITY` copies —
836
838
  * which measured wrong the moment a scene overrode one. `demo/dayClock` resolves
837
- * `directionalShadowMaxSlope` to 9 where the default is 3.
839
+ * `directionalShadowMaxSlope` to 9 where the default was 3 (60 since 4.13.0).
838
840
  */
839
841
  quality;
840
842
  /** Whether `endFrame` put an image on the canvas. Read by the recorder and the harness. */
@@ -1519,6 +1521,7 @@ export class WebGPURenderer {
1519
1521
  mediumUpsampleUniforms;
1520
1522
  mediumUpsampleStaging = new ArrayBuffer(MEDIUM_UPSAMPLE_SIZE);
1521
1523
  mediumUpsampleFloats = new Float32Array(this.mediumUpsampleStaging);
1524
+ mediumUpsampleInts = new Int32Array(this.mediumUpsampleStaging);
1522
1525
  mediumUpsampleGroup = null;
1523
1526
  /** The inverse projection the composite linearises a depth with, built once per frame. */
1524
1527
  mediumInvProjection = mat4.create();
@@ -1764,6 +1767,12 @@ export class WebGPURenderer {
1764
1767
  */
1765
1768
  skyUniforms;
1766
1769
  skyFloats = new Float32Array(SKY_UNIFORM_SIZE / 4);
1770
+ /** The same block's ints: the grade the sky draws with. */
1771
+ skyInts = new Int32Array(this.skyFloats.buffer);
1772
+ /** `clearOf`'s scratch: a clear colour as the open target takes it. */
1773
+ gradedClear = [0, 0, 0];
1774
+ /** An inset's clear as its quad's uniform, rewritten in place: the colour and an alpha of 1. */
1775
+ insetClear = new Float32Array([0, 0, 0, 1]);
1767
1776
  skyBindGroup;
1768
1777
  /** The camera's inverse with the clip correction undone, rebuilt once a frame. */
1769
1778
  skyInvViewProj = new Float32Array(16);
@@ -1847,6 +1856,8 @@ export class WebGPURenderer {
1847
1856
  decalUniforms;
1848
1857
  decalStaging = new ArrayBuffer(DECAL_SLOT * MAX_DRAWN_DECALS);
1849
1858
  decalFloats = new Float32Array(this.decalStaging);
1859
+ /** The same slots' ints: the grade the frame under the marks was drawn with. */
1860
+ decalInts = new Int32Array(this.decalStaging);
1850
1861
  decalBindGroup = null;
1851
1862
  /** Said once where a mark cannot be drawn at all, rather than dropped in silence. */
1852
1863
  decalsRefused = false;
@@ -2017,9 +2028,10 @@ export class WebGPURenderer {
2017
2028
  * still owed.
2018
2029
  */
2019
2030
  this.pass = null;
2020
- this.mirrorClearColor[0] = clearColor[0];
2021
- this.mirrorClearColor[1] = clearColor[1];
2022
- this.mirrorClearColor[2] = clearColor[2];
2031
+ const mirrorClear = this.clearOf(clearColor);
2032
+ this.mirrorClearColor[0] = mirrorClear[0];
2033
+ this.mirrorClearColor[1] = mirrorClear[1];
2034
+ this.mirrorClearColor[2] = mirrorClear[2];
2023
2035
  this.pendingClearMask |= MIRROR_TARGET;
2024
2036
  this.reflectionPassActive = true;
2025
2037
  /* Closed in `endPlanarReflection`, matching `renderer.ts:2054`. */
@@ -2171,6 +2183,8 @@ export class WebGPURenderer {
2171
2183
  i[atF('uClipEnabled')] = this.reflectionPassActive ? 1 : 0;
2172
2184
  f.set(this.reflection?.clipPlane ?? NO_CLIP_PLANE, atF('uClipPlane'));
2173
2185
  f[atF('uSheen')] = sheen;
2186
+ i[atF('uOutputTransform')] = this.passGrade();
2187
+ f[atF('uOutputExposure')] = this.passGradeExposure();
2174
2188
  f[atF('uFilmRoughness')] = Math.min(1, Math.max(0, options.roughness ?? 0));
2175
2189
  f[atF('uFilmRoughnessCycles')] = Math.max(0.01, options.roughnessCyclesPerMetre ?? 60);
2176
2190
  const medium = resolveAtmosphere(env, this.atmosphereHeight(camera), this.quality.underwaterAtmosphere, this.medium);
@@ -3020,6 +3034,9 @@ export class WebGPURenderer {
3020
3034
  const fragAt = (name) => PANEL_FRAG_FIELDS[name]?.offset ?? 0;
3021
3035
  this.panelFrags.writeFloats(fragmentSlot, fragAt('uColor'), color);
3022
3036
  this.panelFrags.writeFloat(fragmentSlot, fragAt('uAlpha'), alpha);
3037
+ /* An interface: the screen encode, never the curve. See `interfaceGrade`. */
3038
+ this.panelFrags.writeInt(fragmentSlot, fragAt('uOutputTransform'), interfaceGrade(this.passGrade()));
3039
+ this.panelFrags.writeFloat(fragmentSlot, fragAt('uOutputExposure'), 1);
3023
3040
  const panelPipe = panelPipeline(this.targetPipelines(), this.surface.device, this.panelLayout);
3024
3041
  /* `allocate` already returns a byte offset, not an index. Multiplying by the stride
3025
3042
  again put the second panel 64 KB into a 16 KB ring — the device said so, in as many
@@ -3127,8 +3144,8 @@ export class WebGPURenderer {
3127
3144
  font.atlasWidth,
3128
3145
  font.atlasHeight,
3129
3146
  ]);
3130
- this.sdfTextFrags.writeInt(fragmentSlot, fragAt('uOutputTransform'), this.gradeCode());
3131
- this.sdfTextFrags.writeFloat(fragmentSlot, fragAt('uOutputExposure'), this.gradeExposure());
3147
+ this.sdfTextFrags.writeInt(fragmentSlot, fragAt('uOutputTransform'), this.passGrade());
3148
+ this.sdfTextFrags.writeFloat(fragmentSlot, fragAt('uOutputExposure'), this.passGradeExposure());
3132
3149
  const sdfPipe = sdfTextPipeline(this.blendedPipelines(), this.surface.device, this.sdfTextLayout);
3133
3150
  const sdfGroup = this.sdfTextBindGroupFor(atlas);
3134
3151
  const sdfCommand = this.recordBlended(0);
@@ -3230,6 +3247,9 @@ export class WebGPURenderer {
3230
3247
  this.textFrags.writeFloats(fragmentSlot, fragAt('uColor'), style.color);
3231
3248
  this.textFrags.writeFloat(fragmentSlot, fragAt('uGlow'), style.glow);
3232
3249
  this.textFrags.writeFloat(fragmentSlot, fragAt('uAlpha'), style.alpha);
3250
+ /* An interface: the screen encode, never the curve. See `interfaceGrade`. */
3251
+ this.textFrags.writeInt(fragmentSlot, fragAt('uOutputTransform'), interfaceGrade(this.passGrade()));
3252
+ this.textFrags.writeFloat(fragmentSlot, fragAt('uOutputExposure'), 1);
3233
3253
  const pass = this.openTextPass();
3234
3254
  if (pass === null)
3235
3255
  return;
@@ -3573,19 +3593,26 @@ export class WebGPURenderer {
3573
3593
  * array allocated with the batch, so the frame loop still allocates nothing.
3574
3594
  */
3575
3595
  /**
3576
- * The output transform a forward pass should apply, and the exposure it should apply it at.
3577
- *
3578
- * The WebGPU half of `Renderer.gradeCode`, and it has to agree with it draw for draw: this is
3579
- * one verb with two implementations, and a frame that grades on one backend and not the other
3580
- * is the silent kind of disagreement. See that method for what the gate is protecting.
3596
+ * The grade a pass applies to what it draws into the target open now, and the exposure it
3597
+ * applies it at: the WebGPU half of `Renderer.passGrade`, and it has to agree with it draw for
3598
+ * draw. The decision is `passGrade.ts`'s, so both halves only gather the state it is taken from.
3581
3599
  */
3582
- gradeCode() {
3583
- return !this.hasComposite || !this.quality.hdrScene
3584
- ? forwardTransformCode(this.quality.outputTransform)
3585
- : 0;
3600
+ passGrade() {
3601
+ const storing = this.probePassActive || this.capturePassActive;
3602
+ const transform = this.quality.outputTransform;
3603
+ return passGradeCode(storing, this.framePresented, this.resolveGrades(), transform);
3604
+ }
3605
+ passGradeExposure() {
3606
+ const storing = this.probePassActive || this.capturePassActive;
3607
+ return passGradeExposure(storing, this.framePresented, this.resolveGrades(), this.exposure);
3608
+ }
3609
+ /** Whether the frame's resolve applies the transform, so the passes before it draw linear. */
3610
+ resolveGrades() {
3611
+ return this.hasComposite && this.quality.hdrScene;
3586
3612
  }
3587
- gradeExposure() {
3588
- return !this.hasComposite || !this.quality.hdrScene ? this.exposure : 1;
3613
+ /** A clear colour as the open target takes it, graded as a pass would. See `gradeColor.ts`. */
3614
+ clearOf(color) {
3615
+ return gradeColorInto(this.passGrade(), this.passGradeExposure(), color, this.gradedClear);
3589
3616
  }
3590
3617
  drawParticles(batch, data, camera, env, timeSeconds) {
3591
3618
  if (!this.canDraw() || data.count === 0)
@@ -3687,8 +3714,8 @@ export class WebGPURenderer {
3687
3714
  i[atF('uFogEnabled')] = batch.fog ? 1 : 0;
3688
3715
  /* Every material's fragment stage declares these, so they are unconditional like
3689
3716
  `uCameraPos` rather than guarded like the material-specific fields above. */
3690
- i[atF('uOutputTransform')] = this.gradeCode();
3691
- f[atF('uOutputExposure')] = this.gradeExposure();
3717
+ i[atF('uOutputTransform')] = this.passGrade();
3718
+ f[atF('uOutputExposure')] = this.passGradeExposure();
3692
3719
  const spriteGroup = batch.material === 'sprite' ? this.spriteFades(batch, camera, f) : null;
3693
3720
  device.queue.writeBuffer(batch.vertUniforms, 0, batch.vertStaging);
3694
3721
  device.queue.writeBuffer(batch.fragUniforms, 0, batch.fragStaging);
@@ -3894,6 +3921,8 @@ export class WebGPURenderer {
3894
3921
  frags.writeInt(fragmentSlot, atF('uFogMode'), medium.fogMode);
3895
3922
  frags.writeFloat(fragmentSlot, atF('uFogNear'), medium.fogNear);
3896
3923
  frags.writeFloat(fragmentSlot, atF('uFogFar'), medium.fogFar);
3924
+ frags.writeInt(fragmentSlot, atF('uOutputTransform'), this.passGrade());
3925
+ frags.writeFloat(fragmentSlot, atF('uOutputExposure'), this.passGradeExposure());
3897
3926
  /*
3898
3927
  * **Written for every material, not only the one that reads it.** `arcane` is the only
3899
3928
  * plume whose block declares `uTint`, and the slot it lands in is past the end of fire's
@@ -4045,6 +4074,8 @@ export class WebGPURenderer {
4045
4074
  /* No reflection pass yet: the shader's own "off" path, not an improvised one. */
4046
4075
  i[atF('uReflectionEnabled')] = mirror === null ? 0 : 1;
4047
4076
  i[atF('uReflectionFilterTaps')] = water.reflectionFilterTaps;
4077
+ i[atF('uOutputTransform')] = this.passGrade();
4078
+ f[atF('uOutputExposure')] = this.passGradeExposure();
4048
4079
  f[atF('uReflectionTexelSize')] = mirror === null ? 1 : 1 / mirror.width;
4049
4080
  f[atF('uReflectionTexelSize') + 1] = mirror === null ? 1 : 1 / mirror.height;
4050
4081
  /*
@@ -4174,6 +4205,8 @@ export class WebGPURenderer {
4174
4205
  ring.writeFloats(fragmentSlot, at('uDustOffset'), options.driftM ?? NO_DRIFT);
4175
4206
  ring.writeFloat(fragmentSlot, at('uNear'), options.nearM ?? 0);
4176
4207
  ring.writeInt(fragmentSlot, at('uSamples'), this.quality.lightVolumeSamples);
4208
+ ring.writeInt(fragmentSlot, at('uOutputTransform'), this.passGrade());
4209
+ ring.writeFloat(fragmentSlot, at('uOutputExposure'), this.passGradeExposure());
4177
4210
  /*
4178
4211
  * The clamp that makes a beam end on the floor, and the snapshot it reads.
4179
4212
  *
@@ -4348,6 +4381,10 @@ export class WebGPURenderer {
4348
4381
  v[at('uCellSize')] = streaks.cellSize;
4349
4382
  v[at('uTime')] = timeSeconds;
4350
4383
  this.windStreakFragFloats.set(tint, (WIND_STREAK_FRAG_FIELDS.uTint.offset ?? 0) / 4);
4384
+ this.windStreakFragBlock[WIND_STREAK_FRAG_FIELDS.uOutputTransform.offset / 4] =
4385
+ this.passGrade();
4386
+ this.windStreakFragFloats[WIND_STREAK_FRAG_FIELDS.uOutputExposure.offset / 4] =
4387
+ this.passGradeExposure();
4351
4388
  this.windStreakVerts.writeBlock(vertexSlot, this.windStreakVertBlock);
4352
4389
  this.windStreakFrags.writeBlock(fragmentSlot, this.windStreakFragBlock);
4353
4390
  const windPipe = windStreakPipeline(this.blendedPipelines(), this.surface.device, this.windStreakLayout);
@@ -4433,6 +4470,8 @@ export class WebGPURenderer {
4433
4470
  v[at('uWind')] = windX;
4434
4471
  v[at('uWind') + 1] = windZ;
4435
4472
  this.flockFragFloats.set(tint, (FLOCK_FRAG_FIELDS.uTint.offset ?? 0) / 4);
4473
+ this.flockFragBlock[FLOCK_FRAG_FIELDS.uOutputTransform.offset / 4] = this.passGrade();
4474
+ this.flockFragFloats[FLOCK_FRAG_FIELDS.uOutputExposure.offset / 4] = this.passGradeExposure();
4436
4475
  this.flockVerts.writeBlock(vertexSlot, this.flockVertBlock);
4437
4476
  this.flockFrags.writeBlock(fragmentSlot, this.flockFragBlock);
4438
4477
  const flockPipe = flockPipeline(this.pipelines, this.surface.device, this.flockLayout);
@@ -4525,6 +4564,8 @@ export class WebGPURenderer {
4525
4564
  f.set(core, atF('uCoreColor'));
4526
4565
  f.set(edge, atF('uEdgeColor'));
4527
4566
  f[atF('uCoreGain')] = coreGain;
4567
+ i[atF('uOutputTransform')] = this.passGrade();
4568
+ f[atF('uOutputExposure')] = this.passGradeExposure();
4528
4569
  f.set(camera.position, atF('uCameraPos'));
4529
4570
  /* The medium, through the same resolver every other pass here binds from. */
4530
4571
  const medium = resolveAtmosphere(env, camera.position[1] ?? 0, this.quality.underwaterAtmosphere, this.medium);
@@ -4636,8 +4677,8 @@ export class WebGPURenderer {
4636
4677
  this.lineFrags.writeFloat(fragmentSlot, atF('uOpacity'), opacity);
4637
4678
  this.lineFrags.writeFloats(fragmentSlot, atF('uCameraPos'), camera.position);
4638
4679
  this.lineFrags.writeFloat(fragmentSlot, atF('uSoftness'), softness);
4639
- this.lineFrags.writeInt(fragmentSlot, atF('uOutputTransform'), this.gradeCode());
4640
- this.lineFrags.writeFloat(fragmentSlot, atF('uOutputExposure'), this.gradeExposure());
4680
+ this.lineFrags.writeInt(fragmentSlot, atF('uOutputTransform'), this.passGrade());
4681
+ this.lineFrags.writeFloat(fragmentSlot, atF('uOutputExposure'), this.passGradeExposure());
4641
4682
  /* The medium, through the same resolver every other pass here binds from. */
4642
4683
  const medium = resolveAtmosphere(env, camera.position[1] ?? 0, this.quality.underwaterAtmosphere, this.medium);
4643
4684
  this.lineFrags.writeFloats(fragmentSlot, atF('uFogColor'), medium.fogColor);
@@ -6734,9 +6775,10 @@ export class WebGPURenderer {
6734
6775
  * at 92 MB a frame at 824x1830. `ensurePass` opens it on the first draw that wants it, which
6735
6776
  * for such a scene is after the last mirror, so it is opened once and cleared once.
6736
6777
  */
6737
- this.pendingClearColor[0] = clearColor[0];
6738
- this.pendingClearColor[1] = clearColor[1];
6739
- this.pendingClearColor[2] = clearColor[2];
6778
+ const frameClear = this.clearOf(clearColor);
6779
+ this.pendingClearColor[0] = frameClear[0];
6780
+ this.pendingClearColor[1] = frameClear[1];
6781
+ this.pendingClearColor[2] = frameClear[2];
6740
6782
  this.pendingClearMask = SCENE_TARGET;
6741
6783
  if (this.quality.deferFramePass) {
6742
6784
  this.pass = null;
@@ -7438,8 +7480,8 @@ export class WebGPURenderer {
7438
7480
  * `screenEffects` off, or on without `hdrScene`, the mesh pass writes what reaches the
7439
7481
  * canvas and grades as it always did.
7440
7482
  */
7441
- i[at('uOutputTransform')] = this.gradeCode();
7442
- f[at('uOutputExposure')] = this.gradeExposure();
7483
+ i[at('uOutputTransform')] = this.passGrade();
7484
+ f[at('uOutputExposure')] = this.passGradeExposure();
7443
7485
  /* The projection occlusion needs, settled by the pass that has a camera. */
7444
7486
  this.frameProjection = camera.projection;
7445
7487
  /*
@@ -8157,7 +8199,7 @@ export class WebGPURenderer {
8157
8199
  /* `1` is sRGB alone: the conversion without the curve. `Math.min` rather than a literal, so
8158
8200
  a renderer asked for `none` stays at none. See `TranslucentMeshOptions.toneMapped`. */
8159
8201
  if (!toneMapped) {
8160
- this.perFrameInts[this.materialField('uOutputTransform')] = Math.min(this.gradeCode(), 1);
8202
+ this.perFrameInts[this.materialField('uOutputTransform')] = Math.min(this.passGrade(), 1);
8161
8203
  }
8162
8204
  /*
8163
8205
  * **Refraction, and the snapshot it reads is taken here or the draw does not refract.**
@@ -8298,7 +8340,7 @@ export class WebGPURenderer {
8298
8340
  if (refracting)
8299
8341
  this.releaseSeeThrough();
8300
8342
  if (!toneMapped) {
8301
- this.perFrameInts[this.materialField('uOutputTransform')] = this.gradeCode();
8343
+ this.perFrameInts[this.materialField('uOutputTransform')] = this.passGrade();
8302
8344
  }
8303
8345
  if (ditheredBlend) {
8304
8346
  this.perFrameFloats[this.materialField('uCutout') + 1] =
@@ -8655,7 +8697,7 @@ export class WebGPURenderer {
8655
8697
  this.perFrameInts[this.materialField('uFogEnabled')] = fog;
8656
8698
  }
8657
8699
  if (!toneMapped) {
8658
- this.perFrameInts[this.materialField('uOutputTransform')] = Math.min(this.gradeCode(), 1);
8700
+ this.perFrameInts[this.materialField('uOutputTransform')] = Math.min(this.passGrade(), 1);
8659
8701
  }
8660
8702
  const depthWrite = options.depthWrite ?? true;
8661
8703
  const layer = Math.min(Math.max(Math.round(options.depthLayer ?? 0), 0), MAX_DEPTH_LAYER);
@@ -8738,7 +8780,7 @@ export class WebGPURenderer {
8738
8780
  this.perFrameInts[this.materialField('uFogEnabled')] = this.surfaceFog;
8739
8781
  }
8740
8782
  if (!toneMapped) {
8741
- this.perFrameInts[this.materialField('uOutputTransform')] = this.gradeCode();
8783
+ this.perFrameInts[this.materialField('uOutputTransform')] = this.passGrade();
8742
8784
  }
8743
8785
  if (refracting)
8744
8786
  this.releaseSeeThrough();
@@ -8837,6 +8879,8 @@ export class WebGPURenderer {
8837
8879
  i[atF('uFogMode')] = medium.fogMode;
8838
8880
  f[atF('uFogNear')] = medium.fogNear;
8839
8881
  f[atF('uFogFar')] = medium.fogFar;
8882
+ i[atF('uOutputTransform')] = this.passGrade();
8883
+ f[atF('uOutputExposure')] = this.passGradeExposure();
8840
8884
  const fragmentSlot = this.scatterFrags.allocate();
8841
8885
  if (fragmentSlot === null)
8842
8886
  return;
@@ -10228,6 +10272,8 @@ export class WebGPURenderer {
10228
10272
  const medium = resolveAtmosphere(env, this.atmosphereHeight(camera), this.quality.underwaterAtmosphere, this.medium);
10229
10273
  f.set(medium.underwaterColor, at('uUnderwaterColor'));
10230
10274
  f[at('uUnderwaterFactor')] = medium.underwaterFactor;
10275
+ this.skyInts[at('uOutputTransform')] = this.passGrade();
10276
+ f[at('uOutputExposure')] = this.passGradeExposure();
10231
10277
  const slot = this.skyUniforms.allocate();
10232
10278
  if (slot === null) {
10233
10279
  if (!this.warnedSkyFull) {
@@ -10392,6 +10438,8 @@ export class WebGPURenderer {
10392
10438
  ? (OUTPUT_TRANSFORM_CODE[this.quality.outputTransform] ?? 0)
10393
10439
  : 0;
10394
10440
  f[at('uOutputExposure')] = graded ? this.exposure : 1;
10441
+ /* The occlusion multiplied in as an encoded pixel takes it, where the passes graded the scene. */
10442
+ i[at('uSceneGraded')] = this.passGrade() !== 0 ? 1 : 0;
10395
10443
  f.set(this.filmA, at('uFilmA'));
10396
10444
  f.set(this.filmB, at('uFilmB'));
10397
10445
  f[at('uDisplayHeadroom')] = this.headroom;
@@ -10926,8 +10974,8 @@ export class WebGPURenderer {
10926
10974
  });
10927
10975
  const context = this.passContext;
10928
10976
  context.pass = pass;
10929
- context.outputTransform = this.gradeCode();
10930
- context.outputExposure = this.gradeExposure();
10977
+ context.outputTransform = this.passGrade();
10978
+ context.outputExposure = this.passGradeExposure();
10931
10979
  context.jitter = this.latePassJitter;
10932
10980
  context.format = RECON_HISTORY_FORMAT;
10933
10981
  context.depthFormat = DEPTH_FORMAT;
@@ -11480,6 +11528,7 @@ export class WebGPURenderer {
11480
11528
  f[base + at('uDecalOpacity')] = decal.opacity;
11481
11529
  f[base + at('uDecalFacingCos')] = decal.facingCos;
11482
11530
  f[base + at('uDecalSoftness')] = decal.softness;
11531
+ this.decalInts[base + at('uOutputTransform')] = this.passGrade();
11483
11532
  slot++;
11484
11533
  });
11485
11534
  device.queue.writeBuffer(this.decalUniforms, 0, this.decalStaging, 0, slot * DECAL_SLOT);
@@ -11689,6 +11738,8 @@ export class WebGPURenderer {
11689
11738
  g[upsampleAt('uDepthToViewZ') + 1] = inv[14] ?? 0;
11690
11739
  g[upsampleAt('uDepthToViewZ') + 2] = inv[11] ?? 0;
11691
11740
  g[upsampleAt('uDepthToViewZ') + 3] = inv[15] ?? 1;
11741
+ this.mediumUpsampleInts[upsampleAt('uOutputTransform')] = this.passGrade();
11742
+ g[upsampleAt('uOutputExposure')] = this.passGradeExposure();
11692
11743
  device.queue.writeBuffer(this.mediumUpsampleUniforms, 0, this.mediumUpsampleStaging);
11693
11744
  const march = encoder.beginRenderPass({
11694
11745
  label: 'post.medium',
@@ -12222,7 +12273,7 @@ export class WebGPURenderer {
12222
12273
  this.resolveDepth(encoder);
12223
12274
  mat4.invert(this.skinInverseProjection, projection);
12224
12275
  const multisampled = this.compositeMsaa();
12225
- skin.spread(encoder, this.pipelines, resolved, multisampled ?? target, multisampled === null ? null : target, this.samples, projection, this.skinInverseProjection, () => this.gpuTimer.writesFor());
12276
+ skin.spread(encoder, this.pipelines, resolved, multisampled ?? target, multisampled === null ? null : target, this.samples, projection, this.skinInverseProjection, () => this.gpuTimer.writesFor(), this.passGrade(), this.passGradeExposure());
12226
12277
  this.pass = encoder.beginRenderPass({
12227
12278
  label: 'skin.scattered',
12228
12279
  timestampWrites: this.gpuTimer.writesFor(),
@@ -13374,8 +13425,8 @@ export class WebGPURenderer {
13374
13425
  const context = this.passContext;
13375
13426
  context.pass = pass;
13376
13427
  /* The frame's grade, so a contributed pass can obey the 2026-08-17 rule. See `PassContext`. */
13377
- context.outputTransform = this.gradeCode();
13378
- context.outputExposure = this.gradeExposure();
13428
+ context.outputTransform = this.passGrade();
13429
+ context.outputExposure = this.passGradeExposure();
13379
13430
  context.format = target.format;
13380
13431
  context.samples = target.sampleCount;
13381
13432
  return context;
@@ -13833,8 +13884,11 @@ export class WebGPURenderer {
13833
13884
  const slot = this.insetUniforms.allocate();
13834
13885
  if (slot !== null) {
13835
13886
  /* Only the colour: the quad covers the viewport set above, so it needs no geometry. */
13836
- const color = clearColor ?? BLACK_CLEAR;
13837
- this.insetUniforms.writeFloats(slot, 0, [color[0], color[1], color[2], 1]);
13887
+ const color = this.clearOf(clearColor ?? BLACK_CLEAR);
13888
+ this.insetClear[0] = color[0];
13889
+ this.insetClear[1] = color[1];
13890
+ this.insetClear[2] = color[2];
13891
+ this.insetUniforms.writeFloats(slot, 0, this.insetClear);
13838
13892
  const insetPipe = insetPipeline(this.targetPipelines(), this.surface.device, this.insetLayout, clearColor !== null);
13839
13893
  /*
13840
13894
  * Recorded even though the viewport it fills is state on the pass, because it is set
@@ -14427,6 +14481,8 @@ export class WebGPURenderer {
14427
14481
  f[atF('uTime')] = timeSeconds;
14428
14482
  f[atF('uMaxDrop')] = CAUSTICS_MAX_DROP_M;
14429
14483
  f[atF('uStrength')] = strength;
14484
+ i[atF('uOutputTransform')] = this.passGrade();
14485
+ f[atF('uOutputExposure')] = this.passGradeExposure();
14430
14486
  f.set(env.directionalDir, atF('uLightDir'));
14431
14487
  /*
14432
14488
  * What the water is reflecting: the dominant source plus a share of the ambient standing in
@@ -21,7 +21,7 @@ export declare const SCATTER_DEPTH_SIZE: 240;
21
21
  export declare const SCATTER_VERT_FIELDS: UniformFields;
22
22
  export declare const SCATTER_VERT_SIZE: 240;
23
23
  export declare const SCATTER_FRAG_FIELDS: UniformFields;
24
- export declare const SCATTER_FRAG_SIZE: 128;
24
+ export declare const SCATTER_FRAG_SIZE: 144;
25
25
  export declare function scatterVertexLayouts(): GPUVertexBufferLayout[];
26
26
  /** Geometry and placement on the device, behind the opaque handle the surface hands out. */
27
27
  export interface GpuScatter {
@@ -7,6 +7,8 @@ export declare class SkinScatterPass {
7
7
  private readonly layout;
8
8
  private readonly uniforms;
9
9
  private readonly staging;
10
+ /** The same blocks' ints: the frame's grade, which the second axis applies. */
11
+ private readonly stagingInts;
10
12
  private readonly linear;
11
13
  private readonly nearest;
12
14
  private readonly commands;
@@ -48,7 +50,9 @@ export declare class SkinScatterPass {
48
50
  * attachment its pass draws into, `resolve` where a multisampled one resolves, `samples` its
49
51
  * count. `projection` is the frame's, `height` the frame's height in pixels.
50
52
  */
51
- spread(encoder: GPUCommandEncoder, pipelines: PipelineCache, depth: GPUTextureView, frame: GPUTextureView, resolve: GPUTextureView | null, samples: number, projection: ArrayLike<number>, inverseProjection: ArrayLike<number>, timestampWrites: () => GPURenderPassTimestampWrites | undefined): void;
53
+ spread(encoder: GPUCommandEncoder, pipelines: PipelineCache, depth: GPUTextureView, frame: GPUTextureView, resolve: GPUTextureView | null, samples: number, projection: ArrayLike<number>, inverseProjection: ArrayLike<number>, timestampWrites: () => GPURenderPassTimestampWrites | undefined,
54
+ /** The frame's grade, which the second axis applies to the light it adds. */
55
+ outputTransform?: number, outputExposure?: number): void;
52
56
  /** Spent at the frame's start: the kept draws were this frame's. */
53
57
  reset(): void;
54
58
  dispose(): void;
@@ -36,6 +36,8 @@ export class SkinScatterPass {
36
36
  layout;
37
37
  uniforms;
38
38
  staging = new Float32Array((SLOT * 2) / 4);
39
+ /** The same blocks' ints: the frame's grade, which the second axis applies. */
40
+ stagingInts = new Int32Array(this.staging.buffer);
39
41
  linear;
40
42
  nearest;
41
43
  commands = createCommandPool(16);
@@ -182,7 +184,9 @@ export class SkinScatterPass {
182
184
  * attachment its pass draws into, `resolve` where a multisampled one resolves, `samples` its
183
185
  * count. `projection` is the frame's, `height` the frame's height in pixels.
184
186
  */
185
- spread(encoder, pipelines, depth, frame, resolve, samples, projection, inverseProjection, timestampWrites) {
187
+ spread(encoder, pipelines, depth, frame, resolve, samples, projection, inverseProjection, timestampWrites,
188
+ /** The frame's grade, which the second axis applies to the light it adds. */
189
+ outputTransform = 0, outputExposure = 1) {
186
190
  const diffuse = this.diffuse;
187
191
  if (diffuse === null || this.diffuseView === null || this.acrossView === null)
188
192
  return;
@@ -193,7 +197,7 @@ export class SkinScatterPass {
193
197
  this.groupDown = this.group('skin.blurDown', this.acrossView, depth);
194
198
  this.groupsDepth = depth;
195
199
  }
196
- this.writeUniforms(diffuse.width, diffuse.height, projection, inverseProjection);
200
+ this.writeUniforms(diffuse.width, diffuse.height, projection, inverseProjection, outputTransform, outputExposure);
197
201
  const across = postPipeline(pipelines, this.device, this.layout, 'skin.blurAcross', SKIN_BLUR_FRAG_WGSL, SKIN_TARGET_FORMAT);
198
202
  const down = postPipelineTargets(pipelines, this.device, this.layout, `skin.blurDown:${pipelines.format}:${samples}`, SKIN_BLUR_FRAG_WGSL, [
199
203
  {
@@ -261,7 +265,7 @@ export class SkinScatterPass {
261
265
  });
262
266
  }
263
267
  /** Both axes' blocks: the step along each, and what they share. Column-major matrices. */
264
- writeUniforms(width, height, projection, inverse) {
268
+ writeUniforms(width, height, projection, inverse, outputTransform, outputExposure) {
265
269
  const f = this.staging;
266
270
  const fields = BINDINGS.fields;
267
271
  for (let axis = 0; axis < 2; axis += 1) {
@@ -280,6 +284,8 @@ export class SkinScatterPass {
280
284
  f.set(this.profiles, base + fields.uProfiles.offset / 4);
281
285
  /* The colour is applied once, on the axis that adds into the frame. */
282
286
  f[base + fields.uApplyAlbedo.offset / 4] = axis;
287
+ this.stagingInts[base + fields.uOutputTransform.offset / 4] = outputTransform;
288
+ f[base + fields.uOutputExposure.offset / 4] = outputExposure;
283
289
  }
284
290
  this.device.queue.writeBuffer(this.uniforms, 0, f);
285
291
  }
@@ -88,8 +88,18 @@ export declare const SKY_FIELDS: {
88
88
  readonly size: 4;
89
89
  readonly type: "float";
90
90
  };
91
+ readonly uOutputTransform: {
92
+ readonly offset: 224;
93
+ readonly size: 4;
94
+ readonly type: "int";
95
+ };
96
+ readonly uOutputExposure: {
97
+ readonly offset: 228;
98
+ readonly size: 4;
99
+ readonly type: "float";
100
+ };
91
101
  };
92
- export declare const SKY_UNIFORM_SIZE: 224;
102
+ export declare const SKY_UNIFORM_SIZE: 240;
93
103
  export declare function createSkyBindGroupLayout(device: GPUDevice): GPUBindGroupLayout;
94
104
  export declare function createSkyBindGroup(device: GPUDevice, layout: GPUBindGroupLayout, uniforms: GPUBuffer): GPUBindGroup;
95
105
  /**
@@ -11,7 +11,7 @@ import type { UniformFields } from './scatterPass.ts';
11
11
  export declare const WATER_VERT_FIELDS: UniformFields;
12
12
  export declare const WATER_VERT_SIZE: 208;
13
13
  export declare const WATER_FRAG_FIELDS: UniformFields;
14
- export declare const WATER_FRAG_SIZE: 1504;
14
+ export declare const WATER_FRAG_SIZE: 1520;
15
15
  /** The two meshes and the numbers the grid decided, behind the surface's opaque handle. */
16
16
  export interface GpuWater {
17
17
  /** The ocean: near sheet and far skirt in one buffer. */
@@ -14,7 +14,7 @@ import type { PipelineCache } from './pipelineCache.ts';
14
14
  export declare const WIND_STREAK_VERT_FIELDS: UniformFields;
15
15
  export declare const WIND_STREAK_VERT_SIZE: 128;
16
16
  export declare const WIND_STREAK_FRAG_FIELDS: UniformFields;
17
- export declare const WIND_STREAK_FRAG_SIZE: 16;
17
+ export declare const WIND_STREAK_FRAG_SIZE: 32;
18
18
  /** A lattice on the device, behind the surface's opaque handle. */
19
19
  export interface GpuWindStreaks {
20
20
  readonly corners: GPUBuffer;
@@ -60,6 +60,8 @@ export declare class CausticsRenderer {
60
60
  *
61
61
  * Answers whether it drew, which the renderer counts on its frame budget.
62
62
  */
63
- draw(gl: WebGL2RenderingContext, camera: FrameView, timeSeconds: number, directionalDir: Vec3, directionalColor: Vec3, ambient: Vec3, atmosphere: Atmosphere, underwaterEnabled: boolean, windX?: number, windZ?: number, strength?: number): boolean;
63
+ draw(gl: WebGL2RenderingContext, camera: FrameView, timeSeconds: number, directionalDir: Vec3, directionalColor: Vec3, ambient: Vec3, atmosphere: Atmosphere, underwaterEnabled: boolean, windX?: number, windZ?: number, strength?: number,
64
+ /** The renderer's `passGrade`: the frame's, but none in a probe's face or a capture. */
65
+ outputTransform?: number, outputExposure?: number): boolean;
64
66
  dispose(gl: WebGL2RenderingContext): void;
65
67
  }
@@ -66,11 +66,15 @@ export class CausticsRenderer {
66
66
  *
67
67
  * Answers whether it drew, which the renderer counts on its frame budget.
68
68
  */
69
- draw(gl, camera, timeSeconds, directionalDir, directionalColor, ambient, atmosphere, underwaterEnabled, windX = 0, windZ = 0, strength = 1) {
69
+ draw(gl, camera, timeSeconds, directionalDir, directionalColor, ambient, atmosphere, underwaterEnabled, windX = 0, windZ = 0, strength = 1,
70
+ /** The renderer's `passGrade`: the frame's, but none in a probe's face or a capture. */
71
+ outputTransform = 0, outputExposure = 1) {
70
72
  if (this.vertexCount === 0)
71
73
  return false;
72
74
  const u = this.uniforms;
73
75
  gl.useProgram(this.program);
76
+ gl.uniform1i(u['uOutputTransform'] ?? null, outputTransform);
77
+ gl.uniform1f(u['uOutputExposure'] ?? null, outputExposure);
74
78
  gl.uniformMatrix4fv(u['uViewProj'] ?? null, false, camera.viewProjection);
75
79
  gl.uniform3fv(u['uCameraPos'] ?? null, camera.position);
76
80
  gl.uniform1f(u['uTime'] ?? null, timeSeconds);
@@ -29,6 +29,8 @@ export declare class DecalPass {
29
29
  * turns a stored depth back into the clip space that matrix was built in, and pairing it with
30
30
  * the corrected one applies the remap twice. `lightVolumeDraw.ts` carries that derivation.
31
31
  */
32
- drawDecals(queue: DecalQueue, depth: WebGLTexture, viewProjection: ReadonlyMat4, eye: ArrayLike<number>, width: number, height: number): void;
32
+ drawDecals(queue: DecalQueue, depth: WebGLTexture, viewProjection: ReadonlyMat4, eye: ArrayLike<number>, width: number, height: number,
33
+ /** The renderer's `passGrade`: how the frame under the marks was graded, 0 for not yet. */
34
+ outputTransform?: number): void;
33
35
  dispose(): void;
34
36
  }
@@ -42,7 +42,9 @@ export class DecalPass {
42
42
  * turns a stored depth back into the clip space that matrix was built in, and pairing it with
43
43
  * the corrected one applies the remap twice. `lightVolumeDraw.ts` carries that derivation.
44
44
  */
45
- drawDecals(queue, depth, viewProjection, eye, width, height) {
45
+ drawDecals(queue, depth, viewProjection, eye, width, height,
46
+ /** The renderer's `passGrade`: how the frame under the marks was graded, 0 for not yet. */
47
+ outputTransform = 0) {
46
48
  if (queue.length === 0)
47
49
  return;
48
50
  const { gl } = this;
@@ -50,6 +52,7 @@ export class DecalPass {
50
52
  mat4.invert(this.depthToWorld, viewProjection);
51
53
  mat4.multiply(this.depthToWorld, this.depthToWorld, DEPTH_01_TO_CLIP);
52
54
  gl.useProgram(this.program);
55
+ gl.uniform1i(u['uOutputTransform'] ?? null, outputTransform);
53
56
  gl.bindVertexArray(this.vao);
54
57
  gl.disable(gl.DEPTH_TEST);
55
58
  gl.depthMask(false);
@@ -32,6 +32,6 @@ export declare class FlockRenderer {
32
32
  private readonly buffers;
33
33
  private readonly vertexCount;
34
34
  constructor(gl: WebGL2RenderingContext, count: number);
35
- draw(gl: WebGL2RenderingContext, camera: FrameView, timeSeconds: number, params: FlockParams, tint: Vec3, windX?: number, windZ?: number): void;
35
+ draw(gl: WebGL2RenderingContext, camera: FrameView, timeSeconds: number, params: FlockParams, tint: Vec3, windX?: number, windZ?: number, outputTransform?: number, outputExposure?: number): void;
36
36
  dispose(gl: WebGL2RenderingContext): void;
37
37
  }
@@ -34,9 +34,12 @@ export class FlockRenderer {
34
34
  attach(indices, 2, 1);
35
35
  gl.bindVertexArray(null);
36
36
  }
37
- draw(gl, camera, timeSeconds, params, tint, windX = 0, windZ = 0) {
37
+ draw(gl, camera, timeSeconds, params, tint, windX = 0, windZ = 0, outputTransform = 0, outputExposure = 1) {
38
38
  const u = this.uniforms;
39
39
  gl.useProgram(this.program);
40
+ /* The renderer's `passGrade`: the frame's, but none in a probe's face or a capture. */
41
+ gl.uniform1i(u['uOutputTransform'] ?? null, outputTransform);
42
+ gl.uniform1f(u['uOutputExposure'] ?? null, outputExposure);
40
43
  gl.uniformMatrix4fv(u['uViewProj'] ?? null, false, camera.viewProjection);
41
44
  /*
42
45
  * No `uCameraPos` here, and the shader no longer declares one. It fed the banking that