@driftengine/core 4.11.1 → 4.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. package/CHANGELOG.json +102 -0
  2. package/README.md +18 -9
  3. package/dist/geometry/meshBuilder.d.ts +10 -0
  4. package/dist/geometry/meshBuilder.js +11 -2
  5. package/dist/index.d.ts +5 -2
  6. package/dist/index.js +3 -2
  7. package/dist/math/color.d.ts +25 -0
  8. package/dist/math/color.js +37 -0
  9. package/dist/render/backend/webgl2/renderer.d.ts +33 -5
  10. package/dist/render/backend/webgl2/renderer.js +123 -49
  11. package/dist/render/backend/webgl2/skinScatter.d.ts +3 -1
  12. package/dist/render/backend/webgl2/skinScatter.js +5 -1
  13. package/dist/render/backend/webgpu/boltPass.d.ts +1 -1
  14. package/dist/render/backend/webgpu/causticsPass.d.ts +1 -1
  15. package/dist/render/backend/webgpu/filmPass.d.ts +1 -1
  16. package/dist/render/backend/webgpu/flockPass.d.ts +1 -1
  17. package/dist/render/backend/webgpu/giFieldPass.d.ts +3 -1
  18. package/dist/render/backend/webgpu/gpuDrivenPass.d.ts +10 -1
  19. package/dist/render/backend/webgpu/gpuDrivenPass.js +28 -11
  20. package/dist/render/backend/webgpu/panelPass.d.ts +1 -1
  21. package/dist/render/backend/webgpu/postPass.d.ts +2 -2
  22. package/dist/render/backend/webgpu/renderer.d.ts +21 -8
  23. package/dist/render/backend/webgpu/renderer.js +103 -38
  24. package/dist/render/backend/webgpu/scatterPass.d.ts +1 -1
  25. package/dist/render/backend/webgpu/skinScatterPass.d.ts +5 -1
  26. package/dist/render/backend/webgpu/skinScatterPass.js +9 -3
  27. package/dist/render/backend/webgpu/skyPass.d.ts +11 -1
  28. package/dist/render/backend/webgpu/waterPass.d.ts +1 -1
  29. package/dist/render/backend/webgpu/windStreakPass.d.ts +1 -1
  30. package/dist/render/causticsRenderer.d.ts +3 -1
  31. package/dist/render/causticsRenderer.js +5 -1
  32. package/dist/render/decalPass.d.ts +3 -1
  33. package/dist/render/decalPass.js +4 -1
  34. package/dist/render/flockRenderer.d.ts +1 -1
  35. package/dist/render/flockRenderer.js +4 -1
  36. package/dist/render/globalMediumPass.d.ts +3 -1
  37. package/dist/render/globalMediumPass.js +5 -1
  38. package/dist/render/gradeColor.d.ts +21 -0
  39. package/dist/render/gradeColor.js +64 -0
  40. package/dist/render/lightMatrix.d.ts +13 -0
  41. package/dist/render/lightMatrix.js +17 -0
  42. package/dist/render/passGrade.d.ts +13 -0
  43. package/dist/render/passGrade.js +47 -0
  44. package/dist/render/plumeRenderer.d.ts +1 -1
  45. package/dist/render/plumeRenderer.js +4 -1
  46. package/dist/render/renderQuality.d.ts +57 -18
  47. package/dist/render/renderQuality.js +3 -3
  48. package/dist/render/sceneTarget.d.ts +2 -0
  49. package/dist/render/sceneTarget.js +1 -0
  50. package/dist/render/sdfTextRenderer.d.ts +1 -1
  51. package/dist/render/sdfTextRenderer.js +1 -1
  52. package/dist/render/shaders/arcane.d.ts +1 -1
  53. package/dist/render/shaders/arcane.js +5 -1
  54. package/dist/render/shaders/bolt.d.ts +1 -1
  55. package/dist/render/shaders/bolt.js +4 -1
  56. package/dist/render/shaders/caustics.d.ts +1 -1
  57. package/dist/render/shaders/caustics.js +4 -1
  58. package/dist/render/shaders/decalProject.js +12 -2
  59. package/dist/render/shaders/film.d.ts +1 -1
  60. package/dist/render/shaders/film.js +22 -9
  61. package/dist/render/shaders/fire.d.ts +1 -1
  62. package/dist/render/shaders/fire.js +4 -1
  63. package/dist/render/shaders/flat/areaLight.d.ts +1 -1
  64. package/dist/render/shaders/flat/areaLight.js +1 -1
  65. package/dist/render/shaders/flat/glassTint.d.ts +1 -1
  66. package/dist/render/shaders/flat/glassTint.js +1 -0
  67. package/dist/render/shaders/flat/index.d.ts +5 -0
  68. package/dist/render/shaders/flat/index.js +1 -1
  69. package/dist/render/shaders/flat/layerLooks.d.ts +16 -0
  70. package/dist/render/shaders/flat/layerLooks.js +108 -0
  71. package/dist/render/shaders/flat/layered.d.ts +6 -4
  72. package/dist/render/shaders/flat/layered.js +77 -13
  73. package/dist/render/shaders/flat/main.d.ts +1 -1
  74. package/dist/render/shaders/flat/main.js +7 -2
  75. package/dist/render/shaders/flat/models.js +1 -1
  76. package/dist/render/shaders/flat/preamble.js +6 -0
  77. package/dist/render/shaders/flock.d.ts +1 -1
  78. package/dist/render/shaders/flock.js +4 -1
  79. package/dist/render/shaders/generated/arcane.wgsl.d.ts +11 -1
  80. package/dist/render/shaders/generated/arcane.wgsl.js +11 -1
  81. package/dist/render/shaders/generated/bolt.wgsl.d.ts +12 -2
  82. package/dist/render/shaders/generated/bolt.wgsl.js +12 -2
  83. package/dist/render/shaders/generated/caustics.wgsl.d.ts +12 -2
  84. package/dist/render/shaders/generated/caustics.wgsl.js +12 -2
  85. package/dist/render/shaders/generated/decalProject.wgsl.d.ts +6 -1
  86. package/dist/render/shaders/generated/decalProject.wgsl.js +6 -1
  87. package/dist/render/shaders/generated/film.wgsl.d.ts +12 -2
  88. package/dist/render/shaders/generated/film.wgsl.js +12 -2
  89. package/dist/render/shaders/generated/fire.wgsl.d.ts +11 -1
  90. package/dist/render/shaders/generated/fire.wgsl.js +11 -1
  91. package/dist/render/shaders/generated/flat.wgsl.d.ts +144 -16
  92. package/dist/render/shaders/generated/flat.wgsl.js +183 -55
  93. package/dist/render/shaders/generated/flock.wgsl.d.ts +12 -2
  94. package/dist/render/shaders/generated/flock.wgsl.js +12 -2
  95. package/dist/render/shaders/generated/globalMedium.wgsl.d.ts +12 -2
  96. package/dist/render/shaders/generated/globalMedium.wgsl.js +12 -2
  97. package/dist/render/shaders/generated/lightVolume.wgsl.d.ts +36 -16
  98. package/dist/render/shaders/generated/lightVolume.wgsl.js +39 -19
  99. package/dist/render/shaders/generated/panel.wgsl.d.ts +12 -2
  100. package/dist/render/shaders/generated/panel.wgsl.js +12 -2
  101. package/dist/render/shaders/generated/rush.wgsl.d.ts +31 -26
  102. package/dist/render/shaders/generated/rush.wgsl.js +31 -26
  103. package/dist/render/shaders/generated/scatter.wgsl.d.ts +12 -2
  104. package/dist/render/shaders/generated/scatter.wgsl.js +12 -2
  105. package/dist/render/shaders/generated/skinBlur.wgsl.d.ts +11 -1
  106. package/dist/render/shaders/generated/skinBlur.wgsl.js +11 -1
  107. package/dist/render/shaders/generated/sky.wgsl.d.ts +12 -2
  108. package/dist/render/shaders/generated/sky.wgsl.js +12 -2
  109. package/dist/render/shaders/generated/smoke.wgsl.d.ts +11 -1
  110. package/dist/render/shaders/generated/smoke.wgsl.js +11 -1
  111. package/dist/render/shaders/generated/text.wgsl.d.ts +11 -1
  112. package/dist/render/shaders/generated/text.wgsl.js +11 -1
  113. package/dist/render/shaders/generated/water.wgsl.d.ts +12 -2
  114. package/dist/render/shaders/generated/water.wgsl.js +12 -2
  115. package/dist/render/shaders/generated/windStreaks.wgsl.d.ts +12 -2
  116. package/dist/render/shaders/generated/windStreaks.wgsl.js +12 -2
  117. package/dist/render/shaders/globalMedium.js +14 -1
  118. package/dist/render/shaders/gpudriven/present.wgsl.d.ts +15 -6
  119. package/dist/render/shaders/gpudriven/present.wgsl.js +71 -8
  120. package/dist/render/shaders/lightVolume.js +4 -1
  121. package/dist/render/shaders/line.d.ts +1 -1
  122. package/dist/render/shaders/outputTransform.d.ts +8 -2
  123. package/dist/render/shaders/outputTransform.js +18 -11
  124. package/dist/render/shaders/panel.d.ts +1 -1
  125. package/dist/render/shaders/panel.js +5 -1
  126. package/dist/render/shaders/particle.d.ts +3 -3
  127. package/dist/render/shaders/rush.js +8 -0
  128. package/dist/render/shaders/scatter.d.ts +1 -1
  129. package/dist/render/shaders/scatter.js +4 -1
  130. package/dist/render/shaders/sdfText.d.ts +1 -1
  131. package/dist/render/shaders/skinBlur.js +5 -1
  132. package/dist/render/shaders/sky.d.ts +1 -1
  133. package/dist/render/shaders/sky.js +5 -1
  134. package/dist/render/shaders/smoke.d.ts +1 -1
  135. package/dist/render/shaders/smoke.js +4 -1
  136. package/dist/render/shaders/text.d.ts +1 -1
  137. package/dist/render/shaders/text.js +4 -1
  138. package/dist/render/shaders/water.d.ts +1 -1
  139. package/dist/render/shaders/water.js +12 -2
  140. package/dist/render/shaders/windStreaks.d.ts +1 -1
  141. package/dist/render/shaders/windStreaks.js +4 -1
  142. package/dist/render/surfaceLayerLooks.d.ts +92 -0
  143. package/dist/render/surfaceLayerLooks.js +102 -0
  144. package/dist/render/surfaceLayers.d.ts +47 -6
  145. package/dist/render/surfaceLayers.js +5 -1
  146. package/dist/render/textRenderer.d.ts +3 -1
  147. package/dist/render/textRenderer.js +7 -1
  148. package/dist/render/vertexDefaults.d.ts +1 -1
  149. package/dist/render/vertexDefaults.js +1 -1
  150. package/dist/render/waterRenderer.d.ts +1 -1
  151. package/dist/render/waterRenderer.js +4 -1
  152. package/dist/render/windStreakRenderer.d.ts +3 -1
  153. package/dist/render/windStreakRenderer.js +5 -1
  154. package/package.json +6 -6
  155. package/scripts/browser.d.mts +5 -0
  156. package/scripts/browser.mjs +68 -14
  157. package/scripts/look.d.mts +81 -0
  158. package/scripts/look.mjs +380 -0
  159. package/src/geometry/meshBuilder.ts +21 -2
  160. package/src/index.ts +13 -2
  161. package/src/math/color.ts +39 -0
  162. package/src/render/backend/webgl2/renderer.ts +174 -46
  163. package/src/render/backend/webgl2/skinScatter.ts +5 -0
  164. package/src/render/backend/webgpu/giFieldPass.ts +3 -1
  165. package/src/render/backend/webgpu/gpuDrivenPass.ts +29 -13
  166. package/src/render/backend/webgpu/renderer.ts +112 -37
  167. package/src/render/backend/webgpu/skinScatterPass.ts +17 -1
  168. package/src/render/causticsRenderer.ts +5 -0
  169. package/src/render/decalPass.ts +3 -0
  170. package/src/render/flockRenderer.ts +5 -0
  171. package/src/render/globalMediumPass.ts +10 -1
  172. package/src/render/gradeColor.ts +90 -0
  173. package/src/render/lightMatrix.ts +32 -0
  174. package/src/render/passGrade.ts +58 -0
  175. package/src/render/plumeRenderer.ts +5 -0
  176. package/src/render/renderQuality.ts +60 -21
  177. package/src/render/sceneTarget.ts +3 -0
  178. package/src/render/sdfTextRenderer.ts +1 -1
  179. package/src/render/shaders/arcane.ts +5 -1
  180. package/src/render/shaders/bolt.ts +4 -1
  181. package/src/render/shaders/caustics.ts +4 -1
  182. package/src/render/shaders/decalProject.ts +12 -2
  183. package/src/render/shaders/film.ts +22 -9
  184. package/src/render/shaders/fire.ts +4 -1
  185. package/src/render/shaders/flat/areaLight.ts +1 -1
  186. package/src/render/shaders/flat/glassTint.ts +3 -1
  187. package/src/render/shaders/flat/index.ts +6 -1
  188. package/src/render/shaders/flat/layerLooks.ts +108 -0
  189. package/src/render/shaders/flat/layered.ts +78 -13
  190. package/src/render/shaders/flat/main.ts +7 -2
  191. package/src/render/shaders/flat/models.ts +1 -1
  192. package/src/render/shaders/flat/preamble.ts +6 -0
  193. package/src/render/shaders/flock.ts +4 -1
  194. package/src/render/shaders/generated/arcane.wgsl.ts +11 -1
  195. package/src/render/shaders/generated/bolt.wgsl.ts +12 -2
  196. package/src/render/shaders/generated/caustics.wgsl.ts +12 -2
  197. package/src/render/shaders/generated/decalProject.wgsl.ts +6 -1
  198. package/src/render/shaders/generated/film.wgsl.ts +12 -2
  199. package/src/render/shaders/generated/fire.wgsl.ts +11 -1
  200. package/src/render/shaders/generated/flat.wgsl.ts +183 -55
  201. package/src/render/shaders/generated/flock.wgsl.ts +12 -2
  202. package/src/render/shaders/generated/globalMedium.wgsl.ts +12 -2
  203. package/src/render/shaders/generated/lightVolume.wgsl.ts +39 -19
  204. package/src/render/shaders/generated/panel.wgsl.ts +12 -2
  205. package/src/render/shaders/generated/rush.wgsl.ts +31 -26
  206. package/src/render/shaders/generated/scatter.wgsl.ts +12 -2
  207. package/src/render/shaders/generated/skinBlur.wgsl.ts +11 -1
  208. package/src/render/shaders/generated/sky.wgsl.ts +12 -2
  209. package/src/render/shaders/generated/smoke.wgsl.ts +11 -1
  210. package/src/render/shaders/generated/text.wgsl.ts +11 -1
  211. package/src/render/shaders/generated/water.wgsl.ts +12 -2
  212. package/src/render/shaders/generated/windStreaks.wgsl.ts +12 -2
  213. package/src/render/shaders/globalMedium.ts +14 -1
  214. package/src/render/shaders/gpudriven/present.wgsl.ts +72 -8
  215. package/src/render/shaders/lightVolume.ts +4 -1
  216. package/src/render/shaders/outputTransform.ts +19 -11
  217. package/src/render/shaders/panel.ts +5 -1
  218. package/src/render/shaders/rush.ts +8 -0
  219. package/src/render/shaders/scatter.ts +4 -1
  220. package/src/render/shaders/skinBlur.ts +5 -1
  221. package/src/render/shaders/sky.ts +5 -1
  222. package/src/render/shaders/smoke.ts +4 -1
  223. package/src/render/shaders/text.ts +4 -1
  224. package/src/render/shaders/water.ts +12 -2
  225. package/src/render/shaders/windStreaks.ts +4 -1
  226. package/src/render/surfaceLayerLooks.ts +186 -0
  227. package/src/render/surfaceLayers.ts +52 -7
  228. package/src/render/textRenderer.ts +6 -0
  229. package/src/render/vertexDefaults.ts +1 -1
  230. package/src/render/waterRenderer.ts +5 -0
  231. package/src/render/windStreakRenderer.ts +5 -0
@@ -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.
@@ -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;
@@ -1189,6 +1198,8 @@ export declare class WebGPURenderer implements RendererApi {
1189
1198
  private readonly projection;
1190
1199
  /** `uLayers`, the material's layers packed by `packSurfaceLayers`. */
1191
1200
  private readonly layerFloats;
1201
+ /** `uLayerLooks`, the material's layer picks and looks packed by `packLayerLooks`. */
1202
+ private readonly lookFloats;
1192
1203
  /**
1193
1204
  * `uGlassTint` as the material leaves it, its light from behind and that light's colour, which a
1194
1205
  * pane overwrites for its own draw and `releaseSeeThrough` puts back. See `transmission.ts`.
@@ -1544,14 +1555,16 @@ export declare class WebGPURenderer implements RendererApi {
1544
1555
  * array allocated with the batch, so the frame loop still allocates nothing.
1545
1556
  */
1546
1557
  /**
1547
- * The output transform a forward pass should apply, and the exposure it should apply it at.
1548
- *
1549
- * The WebGPU half of `Renderer.gradeCode`, and it has to agree with it draw for draw: this is
1550
- * one verb with two implementations, and a frame that grades on one backend and not the other
1551
- * 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.
1552
1561
  */
1553
- private gradeCode;
1554
- 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;
1555
1568
  drawParticles(batch: GpuParticles, data: ParticleInstances, camera: Camera, env: Environment, timeSeconds: number): void;
1556
1569
  /**
1557
1570
  * Draw particles a caller's own compute wrote, where they lie. See `DeviceParticles`.
@@ -67,6 +67,7 @@ import { resolveTransmission } from '../../transmission.js';
67
67
  import { resolveOcclusionFade } from '../../occlusionFade.js';
68
68
  import { orthographicDepthSpan } from '../../lightMatrix.js';
69
69
  import { LAYER_FLOATS, layerMaskMap, layeredProjection, layersFitBeside, packSurfaceLayers, } from '../../surfaceLayers.js';
70
+ import { LOOK_FLOATS, asksLayerLooks, packLayerLooks } from '../../surfaceLayerLooks.js';
70
71
  import { frameReflectionsRefused, resolveFrameReflections, } from '../../frameReflections.js';
71
72
  import { REFLECTION_SURFACE_FORMAT, ReflectionSurfacePass } from './reflectionSurfacePass.js';
72
73
  import { validateClothBinding } from '../../clothBindingData.js';
@@ -74,7 +75,9 @@ import { createSdfTextBindGroup, createSdfTextBindGroupLayout, GpuSdfText, sdfTe
74
75
  import { createFilmBindGroup, createFilmBindGroupLayout, filmPipeline, FILM_FRAG_FIELDS, FILM_FRAG_SIZE, FILM_VERT_FIELDS, FILM_VERT_SIZE, } from './filmPass.js';
75
76
  import { reflectionTargetSize } from '../../planarReflectionDraw.js';
76
77
  import { drawingBufferSize } from '../../drawingBuffer.js';
77
- 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';
78
81
  import { FRINGE_FLOATS, resolveFringe } from '../../shaders/fringe.js';
79
82
  import { displayHeadroom } from '../../displayRange.js';
80
83
  import { DEFAULT_FILMIC_CURVE, FILMIC_CLAMPED, FILMIC_CONSTANTS, FILMIC_WITHOUT_COMPOSITE, resolveFilmicCurve, } from '../../filmicCurve.js';
@@ -833,7 +836,7 @@ export class WebGPURenderer {
833
836
  * **Resolved by the caller and held, rather than defaulted per use.** Every shadow term
834
837
  * below reads it, and the constants they replaced were `DEFAULT_RENDER_QUALITY` copies —
835
838
  * which measured wrong the moment a scene overrode one. `demo/dayClock` resolves
836
- * `directionalShadowMaxSlope` to 9 where the default is 3.
839
+ * `directionalShadowMaxSlope` to 9 where the default was 3 (60 since 4.13.0).
837
840
  */
838
841
  quality;
839
842
  /** Whether `endFrame` put an image on the canvas. Read by the recorder and the harness. */
@@ -1518,6 +1521,7 @@ export class WebGPURenderer {
1518
1521
  mediumUpsampleUniforms;
1519
1522
  mediumUpsampleStaging = new ArrayBuffer(MEDIUM_UPSAMPLE_SIZE);
1520
1523
  mediumUpsampleFloats = new Float32Array(this.mediumUpsampleStaging);
1524
+ mediumUpsampleInts = new Int32Array(this.mediumUpsampleStaging);
1521
1525
  mediumUpsampleGroup = null;
1522
1526
  /** The inverse projection the composite linearises a depth with, built once per frame. */
1523
1527
  mediumInvProjection = mat4.create();
@@ -1763,6 +1767,12 @@ export class WebGPURenderer {
1763
1767
  */
1764
1768
  skyUniforms;
1765
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]);
1766
1776
  skyBindGroup;
1767
1777
  /** The camera's inverse with the clip correction undone, rebuilt once a frame. */
1768
1778
  skyInvViewProj = new Float32Array(16);
@@ -1846,6 +1856,8 @@ export class WebGPURenderer {
1846
1856
  decalUniforms;
1847
1857
  decalStaging = new ArrayBuffer(DECAL_SLOT * MAX_DRAWN_DECALS);
1848
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);
1849
1861
  decalBindGroup = null;
1850
1862
  /** Said once where a mark cannot be drawn at all, rather than dropped in silence. */
1851
1863
  decalsRefused = false;
@@ -2016,9 +2028,10 @@ export class WebGPURenderer {
2016
2028
  * still owed.
2017
2029
  */
2018
2030
  this.pass = null;
2019
- this.mirrorClearColor[0] = clearColor[0];
2020
- this.mirrorClearColor[1] = clearColor[1];
2021
- 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];
2022
2035
  this.pendingClearMask |= MIRROR_TARGET;
2023
2036
  this.reflectionPassActive = true;
2024
2037
  /* Closed in `endPlanarReflection`, matching `renderer.ts:2054`. */
@@ -2170,6 +2183,8 @@ export class WebGPURenderer {
2170
2183
  i[atF('uClipEnabled')] = this.reflectionPassActive ? 1 : 0;
2171
2184
  f.set(this.reflection?.clipPlane ?? NO_CLIP_PLANE, atF('uClipPlane'));
2172
2185
  f[atF('uSheen')] = sheen;
2186
+ i[atF('uOutputTransform')] = this.passGrade();
2187
+ f[atF('uOutputExposure')] = this.passGradeExposure();
2173
2188
  f[atF('uFilmRoughness')] = Math.min(1, Math.max(0, options.roughness ?? 0));
2174
2189
  f[atF('uFilmRoughnessCycles')] = Math.max(0.01, options.roughnessCyclesPerMetre ?? 60);
2175
2190
  const medium = resolveAtmosphere(env, this.atmosphereHeight(camera), this.quality.underwaterAtmosphere, this.medium);
@@ -2256,6 +2271,8 @@ export class WebGPURenderer {
2256
2271
  projection = new Float32Array(PROJECTION_FLOATS);
2257
2272
  /** `uLayers`, the material's layers packed by `packSurfaceLayers`. */
2258
2273
  layerFloats = new Float32Array(LAYER_FLOATS);
2274
+ /** `uLayerLooks`, the material's layer picks and looks packed by `packLayerLooks`. */
2275
+ lookFloats = new Float32Array(LOOK_FLOATS);
2259
2276
  /**
2260
2277
  * `uGlassTint` as the material leaves it, its light from behind and that light's colour, which a
2261
2278
  * pane overwrites for its own draw and `releaseSeeThrough` puts back. See `transmission.ts`.
@@ -2745,6 +2762,11 @@ export class WebGPURenderer {
2745
2762
  f.set(this.layerFloats, this.materialField('uLayers'));
2746
2763
  if (layers !== null)
2747
2764
  this.enableLit('LAYERED');
2765
+ /* And which layer of shared arrays each reads, with its look: layerLooks.ts. */
2766
+ packLayerLooks(layers, this.lookFloats);
2767
+ f.set(this.lookFloats, this.materialField('uLayerLooks'));
2768
+ if (asksLayerLooks(layers))
2769
+ this.enableLit('LAYER_LOOKS');
2748
2770
  f.set(this.modelParams, this.materialField('uModelParams'));
2749
2771
  });
2750
2772
  /*
@@ -3012,6 +3034,9 @@ export class WebGPURenderer {
3012
3034
  const fragAt = (name) => PANEL_FRAG_FIELDS[name]?.offset ?? 0;
3013
3035
  this.panelFrags.writeFloats(fragmentSlot, fragAt('uColor'), color);
3014
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);
3015
3040
  const panelPipe = panelPipeline(this.targetPipelines(), this.surface.device, this.panelLayout);
3016
3041
  /* `allocate` already returns a byte offset, not an index. Multiplying by the stride
3017
3042
  again put the second panel 64 KB into a 16 KB ring — the device said so, in as many
@@ -3119,8 +3144,8 @@ export class WebGPURenderer {
3119
3144
  font.atlasWidth,
3120
3145
  font.atlasHeight,
3121
3146
  ]);
3122
- this.sdfTextFrags.writeInt(fragmentSlot, fragAt('uOutputTransform'), this.gradeCode());
3123
- 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());
3124
3149
  const sdfPipe = sdfTextPipeline(this.blendedPipelines(), this.surface.device, this.sdfTextLayout);
3125
3150
  const sdfGroup = this.sdfTextBindGroupFor(atlas);
3126
3151
  const sdfCommand = this.recordBlended(0);
@@ -3222,6 +3247,9 @@ export class WebGPURenderer {
3222
3247
  this.textFrags.writeFloats(fragmentSlot, fragAt('uColor'), style.color);
3223
3248
  this.textFrags.writeFloat(fragmentSlot, fragAt('uGlow'), style.glow);
3224
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);
3225
3253
  const pass = this.openTextPass();
3226
3254
  if (pass === null)
3227
3255
  return;
@@ -3565,19 +3593,26 @@ export class WebGPURenderer {
3565
3593
  * array allocated with the batch, so the frame loop still allocates nothing.
3566
3594
  */
3567
3595
  /**
3568
- * The output transform a forward pass should apply, and the exposure it should apply it at.
3569
- *
3570
- * The WebGPU half of `Renderer.gradeCode`, and it has to agree with it draw for draw: this is
3571
- * one verb with two implementations, and a frame that grades on one backend and not the other
3572
- * 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.
3573
3599
  */
3574
- gradeCode() {
3575
- return !this.hasComposite || !this.quality.hdrScene
3576
- ? forwardTransformCode(this.quality.outputTransform)
3577
- : 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;
3578
3612
  }
3579
- gradeExposure() {
3580
- 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);
3581
3616
  }
3582
3617
  drawParticles(batch, data, camera, env, timeSeconds) {
3583
3618
  if (!this.canDraw() || data.count === 0)
@@ -3679,8 +3714,8 @@ export class WebGPURenderer {
3679
3714
  i[atF('uFogEnabled')] = batch.fog ? 1 : 0;
3680
3715
  /* Every material's fragment stage declares these, so they are unconditional like
3681
3716
  `uCameraPos` rather than guarded like the material-specific fields above. */
3682
- i[atF('uOutputTransform')] = this.gradeCode();
3683
- f[atF('uOutputExposure')] = this.gradeExposure();
3717
+ i[atF('uOutputTransform')] = this.passGrade();
3718
+ f[atF('uOutputExposure')] = this.passGradeExposure();
3684
3719
  const spriteGroup = batch.material === 'sprite' ? this.spriteFades(batch, camera, f) : null;
3685
3720
  device.queue.writeBuffer(batch.vertUniforms, 0, batch.vertStaging);
3686
3721
  device.queue.writeBuffer(batch.fragUniforms, 0, batch.fragStaging);
@@ -3886,6 +3921,8 @@ export class WebGPURenderer {
3886
3921
  frags.writeInt(fragmentSlot, atF('uFogMode'), medium.fogMode);
3887
3922
  frags.writeFloat(fragmentSlot, atF('uFogNear'), medium.fogNear);
3888
3923
  frags.writeFloat(fragmentSlot, atF('uFogFar'), medium.fogFar);
3924
+ frags.writeInt(fragmentSlot, atF('uOutputTransform'), this.passGrade());
3925
+ frags.writeFloat(fragmentSlot, atF('uOutputExposure'), this.passGradeExposure());
3889
3926
  /*
3890
3927
  * **Written for every material, not only the one that reads it.** `arcane` is the only
3891
3928
  * plume whose block declares `uTint`, and the slot it lands in is past the end of fire's
@@ -4037,6 +4074,8 @@ export class WebGPURenderer {
4037
4074
  /* No reflection pass yet: the shader's own "off" path, not an improvised one. */
4038
4075
  i[atF('uReflectionEnabled')] = mirror === null ? 0 : 1;
4039
4076
  i[atF('uReflectionFilterTaps')] = water.reflectionFilterTaps;
4077
+ i[atF('uOutputTransform')] = this.passGrade();
4078
+ f[atF('uOutputExposure')] = this.passGradeExposure();
4040
4079
  f[atF('uReflectionTexelSize')] = mirror === null ? 1 : 1 / mirror.width;
4041
4080
  f[atF('uReflectionTexelSize') + 1] = mirror === null ? 1 : 1 / mirror.height;
4042
4081
  /*
@@ -4166,6 +4205,8 @@ export class WebGPURenderer {
4166
4205
  ring.writeFloats(fragmentSlot, at('uDustOffset'), options.driftM ?? NO_DRIFT);
4167
4206
  ring.writeFloat(fragmentSlot, at('uNear'), options.nearM ?? 0);
4168
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());
4169
4210
  /*
4170
4211
  * The clamp that makes a beam end on the floor, and the snapshot it reads.
4171
4212
  *
@@ -4340,6 +4381,10 @@ export class WebGPURenderer {
4340
4381
  v[at('uCellSize')] = streaks.cellSize;
4341
4382
  v[at('uTime')] = timeSeconds;
4342
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();
4343
4388
  this.windStreakVerts.writeBlock(vertexSlot, this.windStreakVertBlock);
4344
4389
  this.windStreakFrags.writeBlock(fragmentSlot, this.windStreakFragBlock);
4345
4390
  const windPipe = windStreakPipeline(this.blendedPipelines(), this.surface.device, this.windStreakLayout);
@@ -4425,6 +4470,8 @@ export class WebGPURenderer {
4425
4470
  v[at('uWind')] = windX;
4426
4471
  v[at('uWind') + 1] = windZ;
4427
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();
4428
4475
  this.flockVerts.writeBlock(vertexSlot, this.flockVertBlock);
4429
4476
  this.flockFrags.writeBlock(fragmentSlot, this.flockFragBlock);
4430
4477
  const flockPipe = flockPipeline(this.pipelines, this.surface.device, this.flockLayout);
@@ -4517,6 +4564,8 @@ export class WebGPURenderer {
4517
4564
  f.set(core, atF('uCoreColor'));
4518
4565
  f.set(edge, atF('uEdgeColor'));
4519
4566
  f[atF('uCoreGain')] = coreGain;
4567
+ i[atF('uOutputTransform')] = this.passGrade();
4568
+ f[atF('uOutputExposure')] = this.passGradeExposure();
4520
4569
  f.set(camera.position, atF('uCameraPos'));
4521
4570
  /* The medium, through the same resolver every other pass here binds from. */
4522
4571
  const medium = resolveAtmosphere(env, camera.position[1] ?? 0, this.quality.underwaterAtmosphere, this.medium);
@@ -4628,8 +4677,8 @@ export class WebGPURenderer {
4628
4677
  this.lineFrags.writeFloat(fragmentSlot, atF('uOpacity'), opacity);
4629
4678
  this.lineFrags.writeFloats(fragmentSlot, atF('uCameraPos'), camera.position);
4630
4679
  this.lineFrags.writeFloat(fragmentSlot, atF('uSoftness'), softness);
4631
- this.lineFrags.writeInt(fragmentSlot, atF('uOutputTransform'), this.gradeCode());
4632
- 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());
4633
4682
  /* The medium, through the same resolver every other pass here binds from. */
4634
4683
  const medium = resolveAtmosphere(env, camera.position[1] ?? 0, this.quality.underwaterAtmosphere, this.medium);
4635
4684
  this.lineFrags.writeFloats(fragmentSlot, atF('uFogColor'), medium.fogColor);
@@ -6726,9 +6775,10 @@ export class WebGPURenderer {
6726
6775
  * at 92 MB a frame at 824x1830. `ensurePass` opens it on the first draw that wants it, which
6727
6776
  * for such a scene is after the last mirror, so it is opened once and cleared once.
6728
6777
  */
6729
- this.pendingClearColor[0] = clearColor[0];
6730
- this.pendingClearColor[1] = clearColor[1];
6731
- 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];
6732
6782
  this.pendingClearMask = SCENE_TARGET;
6733
6783
  if (this.quality.deferFramePass) {
6734
6784
  this.pass = null;
@@ -7186,6 +7236,7 @@ export class WebGPURenderer {
7186
7236
  f.set(this.transmission, at('uGlassTint'));
7187
7237
  /* Nor layers: one layer until a material asks for more. */
7188
7238
  f.fill(0, at('uLayers'), at('uLayers') + LAYER_FLOATS);
7239
+ f.fill(0, at('uLayerLooks'), at('uLayerLooks') + LOOK_FLOATS);
7189
7240
  f[at('uCutout')] = 0;
7190
7241
  f[at('uCutout') + 1] = 0;
7191
7242
  this.cutoutResolveStaged = 'hard';
@@ -7429,8 +7480,8 @@ export class WebGPURenderer {
7429
7480
  * `screenEffects` off, or on without `hdrScene`, the mesh pass writes what reaches the
7430
7481
  * canvas and grades as it always did.
7431
7482
  */
7432
- i[at('uOutputTransform')] = this.gradeCode();
7433
- f[at('uOutputExposure')] = this.gradeExposure();
7483
+ i[at('uOutputTransform')] = this.passGrade();
7484
+ f[at('uOutputExposure')] = this.passGradeExposure();
7434
7485
  /* The projection occlusion needs, settled by the pass that has a camera. */
7435
7486
  this.frameProjection = camera.projection;
7436
7487
  /*
@@ -8148,7 +8199,7 @@ export class WebGPURenderer {
8148
8199
  /* `1` is sRGB alone: the conversion without the curve. `Math.min` rather than a literal, so
8149
8200
  a renderer asked for `none` stays at none. See `TranslucentMeshOptions.toneMapped`. */
8150
8201
  if (!toneMapped) {
8151
- this.perFrameInts[this.materialField('uOutputTransform')] = Math.min(this.gradeCode(), 1);
8202
+ this.perFrameInts[this.materialField('uOutputTransform')] = Math.min(this.passGrade(), 1);
8152
8203
  }
8153
8204
  /*
8154
8205
  * **Refraction, and the snapshot it reads is taken here or the draw does not refract.**
@@ -8289,7 +8340,7 @@ export class WebGPURenderer {
8289
8340
  if (refracting)
8290
8341
  this.releaseSeeThrough();
8291
8342
  if (!toneMapped) {
8292
- this.perFrameInts[this.materialField('uOutputTransform')] = this.gradeCode();
8343
+ this.perFrameInts[this.materialField('uOutputTransform')] = this.passGrade();
8293
8344
  }
8294
8345
  if (ditheredBlend) {
8295
8346
  this.perFrameFloats[this.materialField('uCutout') + 1] =
@@ -8646,7 +8697,7 @@ export class WebGPURenderer {
8646
8697
  this.perFrameInts[this.materialField('uFogEnabled')] = fog;
8647
8698
  }
8648
8699
  if (!toneMapped) {
8649
- this.perFrameInts[this.materialField('uOutputTransform')] = Math.min(this.gradeCode(), 1);
8700
+ this.perFrameInts[this.materialField('uOutputTransform')] = Math.min(this.passGrade(), 1);
8650
8701
  }
8651
8702
  const depthWrite = options.depthWrite ?? true;
8652
8703
  const layer = Math.min(Math.max(Math.round(options.depthLayer ?? 0), 0), MAX_DEPTH_LAYER);
@@ -8729,7 +8780,7 @@ export class WebGPURenderer {
8729
8780
  this.perFrameInts[this.materialField('uFogEnabled')] = this.surfaceFog;
8730
8781
  }
8731
8782
  if (!toneMapped) {
8732
- this.perFrameInts[this.materialField('uOutputTransform')] = this.gradeCode();
8783
+ this.perFrameInts[this.materialField('uOutputTransform')] = this.passGrade();
8733
8784
  }
8734
8785
  if (refracting)
8735
8786
  this.releaseSeeThrough();
@@ -8828,6 +8879,8 @@ export class WebGPURenderer {
8828
8879
  i[atF('uFogMode')] = medium.fogMode;
8829
8880
  f[atF('uFogNear')] = medium.fogNear;
8830
8881
  f[atF('uFogFar')] = medium.fogFar;
8882
+ i[atF('uOutputTransform')] = this.passGrade();
8883
+ f[atF('uOutputExposure')] = this.passGradeExposure();
8831
8884
  const fragmentSlot = this.scatterFrags.allocate();
8832
8885
  if (fragmentSlot === null)
8833
8886
  return;
@@ -10219,6 +10272,8 @@ export class WebGPURenderer {
10219
10272
  const medium = resolveAtmosphere(env, this.atmosphereHeight(camera), this.quality.underwaterAtmosphere, this.medium);
10220
10273
  f.set(medium.underwaterColor, at('uUnderwaterColor'));
10221
10274
  f[at('uUnderwaterFactor')] = medium.underwaterFactor;
10275
+ this.skyInts[at('uOutputTransform')] = this.passGrade();
10276
+ f[at('uOutputExposure')] = this.passGradeExposure();
10222
10277
  const slot = this.skyUniforms.allocate();
10223
10278
  if (slot === null) {
10224
10279
  if (!this.warnedSkyFull) {
@@ -10383,6 +10438,8 @@ export class WebGPURenderer {
10383
10438
  ? (OUTPUT_TRANSFORM_CODE[this.quality.outputTransform] ?? 0)
10384
10439
  : 0;
10385
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;
10386
10443
  f.set(this.filmA, at('uFilmA'));
10387
10444
  f.set(this.filmB, at('uFilmB'));
10388
10445
  f[at('uDisplayHeadroom')] = this.headroom;
@@ -10917,8 +10974,8 @@ export class WebGPURenderer {
10917
10974
  });
10918
10975
  const context = this.passContext;
10919
10976
  context.pass = pass;
10920
- context.outputTransform = this.gradeCode();
10921
- context.outputExposure = this.gradeExposure();
10977
+ context.outputTransform = this.passGrade();
10978
+ context.outputExposure = this.passGradeExposure();
10922
10979
  context.jitter = this.latePassJitter;
10923
10980
  context.format = RECON_HISTORY_FORMAT;
10924
10981
  context.depthFormat = DEPTH_FORMAT;
@@ -11471,6 +11528,7 @@ export class WebGPURenderer {
11471
11528
  f[base + at('uDecalOpacity')] = decal.opacity;
11472
11529
  f[base + at('uDecalFacingCos')] = decal.facingCos;
11473
11530
  f[base + at('uDecalSoftness')] = decal.softness;
11531
+ this.decalInts[base + at('uOutputTransform')] = this.passGrade();
11474
11532
  slot++;
11475
11533
  });
11476
11534
  device.queue.writeBuffer(this.decalUniforms, 0, this.decalStaging, 0, slot * DECAL_SLOT);
@@ -11680,6 +11738,8 @@ export class WebGPURenderer {
11680
11738
  g[upsampleAt('uDepthToViewZ') + 1] = inv[14] ?? 0;
11681
11739
  g[upsampleAt('uDepthToViewZ') + 2] = inv[11] ?? 0;
11682
11740
  g[upsampleAt('uDepthToViewZ') + 3] = inv[15] ?? 1;
11741
+ this.mediumUpsampleInts[upsampleAt('uOutputTransform')] = this.passGrade();
11742
+ g[upsampleAt('uOutputExposure')] = this.passGradeExposure();
11683
11743
  device.queue.writeBuffer(this.mediumUpsampleUniforms, 0, this.mediumUpsampleStaging);
11684
11744
  const march = encoder.beginRenderPass({
11685
11745
  label: 'post.medium',
@@ -12213,7 +12273,7 @@ export class WebGPURenderer {
12213
12273
  this.resolveDepth(encoder);
12214
12274
  mat4.invert(this.skinInverseProjection, projection);
12215
12275
  const multisampled = this.compositeMsaa();
12216
- 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());
12217
12277
  this.pass = encoder.beginRenderPass({
12218
12278
  label: 'skin.scattered',
12219
12279
  timestampWrites: this.gpuTimer.writesFor(),
@@ -13365,8 +13425,8 @@ export class WebGPURenderer {
13365
13425
  const context = this.passContext;
13366
13426
  context.pass = pass;
13367
13427
  /* The frame's grade, so a contributed pass can obey the 2026-08-17 rule. See `PassContext`. */
13368
- context.outputTransform = this.gradeCode();
13369
- context.outputExposure = this.gradeExposure();
13428
+ context.outputTransform = this.passGrade();
13429
+ context.outputExposure = this.passGradeExposure();
13370
13430
  context.format = target.format;
13371
13431
  context.samples = target.sampleCount;
13372
13432
  return context;
@@ -13824,8 +13884,11 @@ export class WebGPURenderer {
13824
13884
  const slot = this.insetUniforms.allocate();
13825
13885
  if (slot !== null) {
13826
13886
  /* Only the colour: the quad covers the viewport set above, so it needs no geometry. */
13827
- const color = clearColor ?? BLACK_CLEAR;
13828
- 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);
13829
13892
  const insetPipe = insetPipeline(this.targetPipelines(), this.surface.device, this.insetLayout, clearColor !== null);
13830
13893
  /*
13831
13894
  * Recorded even though the viewport it fills is state on the pass, because it is set
@@ -14418,6 +14481,8 @@ export class WebGPURenderer {
14418
14481
  f[atF('uTime')] = timeSeconds;
14419
14482
  f[atF('uMaxDrop')] = CAUSTICS_MAX_DROP_M;
14420
14483
  f[atF('uStrength')] = strength;
14484
+ i[atF('uOutputTransform')] = this.passGrade();
14485
+ f[atF('uOutputExposure')] = this.passGradeExposure();
14421
14486
  f.set(env.directionalDir, atF('uLightDir'));
14422
14487
  /*
14423
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;