@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
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Which grade a pass applies to what it draws, decided once for both backends.
3
+ *
4
+ * **Every pass grades where nothing after it will**, so a colour handed to the renderer means linear
5
+ * light wherever it lands. Three facts decide where that is:
6
+ *
7
+ * - **A probe's face or a capture stores radiance** for something else to light with or to show, so
8
+ * it is never graded. The frame grades what it reads of one; graded at the bake as well, the curve
9
+ * would be applied twice.
10
+ * - **A frame whose resolve grades** (`hdrScene` over a scene target) leaves every pass before the
11
+ * resolve linear, because grading early throws away the range the float target exists to keep.
12
+ * - **Past the present, a pass lands on the canvas the resolve has already written**, so nothing
13
+ * after it will grade it and it grades itself with the frame's forward code, composite or not. An
14
+ * interface drawn after `endFrame` escapes the screen-space chain; it does not escape the grade.
15
+ *
16
+ * **An interface takes the screen encode and not the curve** (`interfaceGrade`). A tone curve and
17
+ * an exposure are how a scene's light is brought to the screen, and a caption or a panel is not
18
+ * light: its colour was picked for the screen, so it is encoded wherever the frame is and comes out
19
+ * as picked under every transform, at every exposure. What it gives up is an interface drawn inside
20
+ * a frame whose resolve grades (`hdrScene`): that one is part of the picture the resolve tone maps,
21
+ * and is drawn after `endFrame` to stay out of it. What would make any of this wrong is a composite
22
+ * step running after the present, which none does.
23
+ */
24
+ import { forwardTransformCode } from './vertexDefaults.ts';
25
+
26
+ /**
27
+ * The `uOutputTransform` a pass uploads: `storing` inside a probe's face or a capture, `presented`
28
+ * once the frame's resolve has run, `resolveGrades` where that resolve applies the transform.
29
+ */
30
+ export function passGradeCode(
31
+ storing: boolean,
32
+ presented: boolean,
33
+ resolveGrades: boolean,
34
+ outputTransform: string,
35
+ ): number {
36
+ if (storing) return 0;
37
+ return presented || !resolveGrades ? forwardTransformCode(outputTransform) : 0;
38
+ }
39
+
40
+ /** The `uOutputExposure` beside it: the frame's exposure wherever the pass grades, else one. */
41
+ export function passGradeExposure(
42
+ storing: boolean,
43
+ presented: boolean,
44
+ resolveGrades: boolean,
45
+ exposure: number,
46
+ ): number {
47
+ if (storing) return 1;
48
+ return presented || !resolveGrades ? exposure : 1;
49
+ }
50
+
51
+ /**
52
+ * The code an interface draws with, given the pass's: the screen encode wherever the pass grades at
53
+ * all, and nothing where it does not. Never a curve, so never an exposure either: a screen-space
54
+ * caption, a panel or a 2D layer keeps the colour it was picked as whatever the scene is graded by.
55
+ */
56
+ export function interfaceGrade(passCode: number): number {
57
+ return passCode === 0 ? 0 : 1;
58
+ }
@@ -173,6 +173,8 @@ export class PlumeRenderer {
173
173
  originX = 0,
174
174
  originY = 0,
175
175
  originZ = 0,
176
+ outputTransform = 0,
177
+ outputExposure = 1,
176
178
  ): void {
177
179
  if (this.indexCount === 0) return;
178
180
  const u = this.uniforms;
@@ -200,6 +202,9 @@ export class PlumeRenderer {
200
202
  gl.uniform3f(u['uTint'] ?? null, this.tint[0], this.tint[1], this.tint[2]);
201
203
  gl.uniform1i(u['uClipEnabled'] ?? null, clipPlane === null ? 0 : 1);
202
204
  if (clipPlane !== null) gl.uniform4fv(u['uClipPlane'] ?? null, clipPlane);
205
+ /* The renderer's `passGrade`: the frame's, but none in a probe's face or a capture. */
206
+ gl.uniform1i(u['uOutputTransform'] ?? null, outputTransform);
207
+ gl.uniform1f(u['uOutputExposure'] ?? null, outputExposure);
203
208
  bindAtmosphere(gl, u, atmosphere, atmosphereCameraY, underwaterEnabled);
204
209
 
205
210
  gl.enable(gl.BLEND);
@@ -61,22 +61,35 @@ export type SkinScattering = 'pre-integrated' | 'screen-space';
61
61
 
62
62
  export interface RenderQuality {
63
63
  /**
64
- * The transfer curve applied at the end of every shaded pass.
65
- *
66
- * Construction-time, like every other quality control here, and `none` by default so
67
- * no existing world changes by a single bit.
68
- *
69
- * **The symptom, written down because nobody searches for a transfer curve they do not know
70
- * exists.** With `none`, values above 1 clip flat rather than rolling off, so a scene with
71
- * bright sources against dark surroundings reads as crushed and desaturated: the highlights
72
- * go to a hard white with no colour left in them and the midtones look muddy. That is
73
- * reported by eye as "dark", "flat" or "not vivid", which sounds like a lighting problem, so
74
- * that is where people look. A consumer built, tuned and shipped six worlds before anybody
75
- * said the word. If a scene looks like that, try `outputTransform: 'aces'` with an
76
- * `outputExposure` before touching a single light.
77
- *
78
- * The default stays `none`, and that is not the fault: a curve that changed every existing
79
- * consumer's output would be far worse than one nobody found.
64
+ * The transfer curve applied at the end of every shaded pass. Construction-time, like every
65
+ * other quality control here.
66
+ *
67
+ * **`srgb` by default since 4.13.0, the encoding a display expects**: the light the renderer
68
+ * computes is linear, and a texture's colours are decoded to linear when they are read, so a
69
+ * frame written without encoding them back is a frame of linear values read as display values —
70
+ * every midtone far too dark. It is the encoding most renderers apply by default, so a model
71
+ * drawn here with no options reads as it does elsewhere. **What it gives up** is a highlight's
72
+ * colour: above 1 each channel clips on its own, so a bright saturated source goes to white
73
+ * rather than rolling off, which a curve with a shoulder (`aces`, `shoulder`) is for.
74
+ *
75
+ * **`none` was the default until 4.13.0, and it is the frame that reads as dark**: values
76
+ * written as they are computed, which is right only for a world whose colours were chosen by
77
+ * eye as display values and lit to match. It was reported by eye as "dark", "flat" or "not
78
+ * vivid", which sounds like a lighting problem, so that is where people looked; a consumer
79
+ * built, tuned and shipped six worlds under it before anybody said the word, and a model drawn
80
+ * from the manual came out darker than in any other engine. A world graded under it says
81
+ * `outputTransform: 'none'` and draws exactly as it did. **What would make `srgb` the wrong
82
+ * default** is a renderer whose colours are not linear, which this one's are not.
83
+ *
84
+ * **Every pass applies it where nothing after it will**, and the clear colour with them, so a
85
+ * colour handed to the renderer is linear wherever it lands: a frame with `hdrScene` is graded
86
+ * once at the resolve, and one without it is graded pass by pass to the same curve. A colour
87
+ * picked by eye is a display value, so it is stated through `srgbColor` (or a mesh built with
88
+ * `colorSpace: 'srgb'`), which gives back exactly the colour picked where a surface is lit at 1
89
+ * under `srgb`, and through the curve under a tone curve. **An interface** (screen text, a panel,
90
+ * the 2D layer) takes the screen encode and never the curve or the exposure, so its colours come
91
+ * out as picked under every transform; inside an `hdrScene` frame it is part of the picture the
92
+ * resolve grades, and is drawn after `endFrame` to stay out of the curve.
80
93
  */
81
94
  readonly outputTransform: OutputTransform;
82
95
  /**
@@ -270,9 +283,35 @@ export interface RenderQuality {
270
283
  * at a shadow's edge — that is what the blur is for.
271
284
  */
272
285
  readonly skinScattering: SkinScattering;
273
- /** Horizontal world-space reach over which a directional shadow dissolves. */
286
+ /**
287
+ * How far a sun shadow may reach along the ground from its caster, in metres, before it
288
+ * dissolves. **Not a distance from the camera**: a shadow's own length, from where it leaves its
289
+ * caster to where it lands, fading from 35% of this to all of it.
290
+ *
291
+ * **1,000 since 4.13.0, so no building's shadow dissolves**: the fade starts at 350 m, past any
292
+ * wall, tower or canyon a shadow map covers at a useful resolution, so what bounds a shadow is
293
+ * its map, whose edge already fades. **What it gives up** is a way to hide a long shadow's
294
+ * stair-stepping at a low sun, which is what a short reach was for. It was 6 until 4.13.0: a
295
+ * tuning for short casters, which left a building's walls and arcades casting nothing on its
296
+ * floor under a high sun — the shadows vanished silently and read as random lighting. **What
297
+ * would make it wrong** is a scene whose shadows are too coarse to show at their full length;
298
+ * there a reach of the length that looks right is the setting, and the map around the viewer
299
+ * (`computeLightMatrix`) the better fix.
300
+ */
274
301
  readonly directionalShadowMaxDistance: number;
275
- /** Maximum horizontal projection per vertical metre before low-angle shadows vanish. */
302
+ /**
303
+ * How low the sun may stand before its shadows fade, as the ground a metre of height may shadow:
304
+ * the sun's horizontal reach per vertical metre, so 1 is 45° of elevation and 60 about 1°. Every
305
+ * sun shadow fades from 65% of this to all of it.
306
+ *
307
+ * **60 since 4.13.0: shadows hold until the sun is about a degree above the horizon**, the
308
+ * stretch of a day when they are longest and most worth seeing. **What it gives up**: a sun on
309
+ * the horizon throws shadows kilometres long that a map can hold only as stripes, and the
310
+ * last degree is where they fade rather than show. It was 3 until 4.13.0, which dropped every
311
+ * sun shadow below about 18° — an evening without shadows. **What would make it wrong** is a
312
+ * scene whose low sun streaks; a smaller number here is the setting, at the cost of the
313
+ * shadows going earlier in the evening.
314
+ */
276
315
  readonly directionalShadowMaxSlope: number;
277
316
  /**
278
317
  * Metres a point light may drift from where its shadow was baked before re-baking.
@@ -958,7 +997,7 @@ export const DIRECTIONAL_SHADOW_FADE_START = 0.65;
958
997
  * six times, while the directional map spends all of it on one focused area.
959
998
  */
960
999
  export const DEFAULT_RENDER_QUALITY: Readonly<RenderQuality> = Object.freeze({
961
- outputTransform: 'none',
1000
+ outputTransform: 'srgb',
962
1001
  outputExposure: 1,
963
1002
  pointLightFalloff: 'smooth',
964
1003
  /**
@@ -1006,8 +1045,8 @@ export const DEFAULT_RENDER_QUALITY: Readonly<RenderQuality> = Object.freeze({
1006
1045
  glassShadows: 'full',
1007
1046
  pipelineCompile: 'wait',
1008
1047
  skinScattering: 'pre-integrated',
1009
- directionalShadowMaxDistance: 6,
1010
- directionalShadowMaxSlope: 3,
1048
+ directionalShadowMaxDistance: 1000,
1049
+ directionalShadowMaxSlope: 60,
1011
1050
  pointShadowRebakeDistance: 0.001,
1012
1051
  pointShadowFacesPerFrame: 2,
1013
1052
  /*
@@ -785,6 +785,8 @@ export class SceneTarget {
785
785
  readonly transform: number;
786
786
  readonly exposure: number;
787
787
  readonly film?: Float32Array;
788
+ /** Whether the forward passes graded the scene because this resolve does not. */
789
+ readonly sceneGraded?: boolean;
788
790
  } = {
789
791
  transform: 0,
790
792
  exposure: 1,
@@ -1030,6 +1032,7 @@ export class SceneTarget {
1030
1032
  gl.uniform1f(this.uniforms['uReach'] ?? null, reachUv);
1031
1033
  gl.uniform1i(this.uniforms['uOutputTransform'] ?? null, grade.transform);
1032
1034
  gl.uniform1f(this.uniforms['uOutputExposure'] ?? null, grade.exposure);
1035
+ gl.uniform1i(this.uniforms['uSceneGraded'] ?? null, grade.sceneGraded === true ? 1 : 0);
1033
1036
  if (grade.film !== undefined) {
1034
1037
  gl.uniform4fv(this.uniforms['uFilmA'] ?? null, grade.film, 0, 4);
1035
1038
  gl.uniform4fv(this.uniforms['uFilmB'] ?? null, grade.film, 4, 4);
@@ -112,7 +112,7 @@ export class SdfTextRenderer {
112
112
  */
113
113
  /**
114
114
  * `outputTransform` and `outputExposure` come from the renderer rather than from here, because
115
- * whether this pass grades depends on whether anything after it will. See `Renderer.gradeCode`.
115
+ * whether this pass grades depends on whether anything after it will. See `passGrade.ts`.
116
116
  */
117
117
  draw(
118
118
  viewProj: ReadonlyMat4,
@@ -1,4 +1,5 @@
1
1
  import { FOG_GLSL } from './fog.ts';
2
+ import { OUTPUT_TRANSFORM_GLSL } from './outputTransform.ts';
2
3
 
3
4
  /**
4
5
  * An arcane aura: the volume a floating object sits inside.
@@ -31,6 +32,8 @@ ${FOG_GLSL}
31
32
  uniform int uNoiseOctaves;
32
33
  uniform vec4 uClipPlane;
33
34
  uniform int uClipEnabled;
35
+ /* Before \`uTint\`, where fire and smoke declare it, so one field table serves all three. */
36
+ ${OUTPUT_TRANSFORM_GLSL}
34
37
  /** The caller's colour, so an aura shifts with the world's own palette. */
35
38
  uniform vec3 uTint;
36
39
 
@@ -98,6 +101,7 @@ void main() {
98
101
  col = mix(col, mediumColor(), fog);
99
102
  alpha *= 1.0 - fog;
100
103
 
101
- outColor = vec4(col, alpha);
104
+ /* Graded where nothing after this will grade it, as every pass is. The alpha is coverage. */
105
+ outColor = vec4(applyOutputTransform(col), alpha);
102
106
  }
103
107
  `;
@@ -1,4 +1,5 @@
1
1
  import { FOG_GLSL } from './fog.ts';
2
+ import { OUTPUT_TRANSFORM_GLSL } from './outputTransform.ts';
2
3
 
3
4
  /**
4
5
  * Electrical arcs: a filament with a saturating core and a wide soft glow.
@@ -95,6 +96,7 @@ ${FOG_GLSL}
95
96
  uniform vec3 uCameraPos;
96
97
 
97
98
  out vec4 outColor;
99
+ ${OUTPUT_TRANSFORM_GLSL}
98
100
 
99
101
  float hash11(float p) {
100
102
  return fract(sin(p * 12.9898) * 43758.5453123);
@@ -165,6 +167,7 @@ void main() {
165
167
  * faint: at a typical energy of 0.5 it delivered a quarter of what it asked for.
166
168
  * Every emissive material here has to pick one channel and mean it.
167
169
  */
168
- outColor = vec4(lit * energy * (1.0 - fog), 1.0);
170
+ /* Graded where nothing after this will grade it, as every pass is, light added included. */
171
+ outColor = vec4(applyOutputTransform(lit * energy * (1.0 - fog)), 1.0);
169
172
  }
170
173
  `;
@@ -1,5 +1,6 @@
1
1
  import { GERSTNER_GLSL } from './gerstner.ts';
2
2
  import { FOG_GLSL } from './fog.ts';
3
+ import { OUTPUT_TRANSFORM_GLSL } from './outputTransform.ts';
3
4
 
4
5
  /**
5
6
  * Light off water, projected onto what covers it.
@@ -78,6 +79,7 @@ uniform float uMaxDrop;
78
79
  ${FOG_GLSL}
79
80
 
80
81
  out vec4 outColor;
82
+ ${OUTPUT_TRANSFORM_GLSL}
81
83
 
82
84
  ${GERSTNER_GLSL}
83
85
 
@@ -164,6 +166,7 @@ void main() {
164
166
  float fog = mediumFog(distance(vWorldPos, uCameraPos), vWorldPos.y);
165
167
 
166
168
  // Additive: this is light arriving at a surface that is already shaded.
167
- outColor = vec4(uTint * (caustic * uStrength * reach * (1.0 - fog)), 1.0);
169
+ /* Graded where nothing after this will grade it, as every pass is, light added included. */
170
+ outColor = vec4(applyOutputTransform(uTint * (caustic * uStrength * reach * (1.0 - fog))), 1.0);
168
171
  }
169
172
  `;
@@ -24,6 +24,7 @@
24
24
  */
25
25
 
26
26
  import { glslIsFarDepth } from '../depthConvention.ts';
27
+ import { LINEAR_TO_SRGB_GLSL } from './outputTransform.ts';
27
28
 
28
29
  export const DECAL_PROJECT_FRAG = `#version 300 es
29
30
  precision highp float;
@@ -63,7 +64,9 @@ uniform float uDecalOpacity;
63
64
  uniform float uDecalFacingCos;
64
65
  /** How much of the mark's radius is edge. */
65
66
  uniform float uDecalSoftness;
66
-
67
+ /** How the frame under the marks was graded, 0 where the resolve grades it. See renderQuality.ts. */
68
+ uniform int uOutputTransform;
69
+ ${LINEAR_TO_SRGB_GLSL}
67
70
  out vec4 fragColor;
68
71
 
69
72
  void main() {
@@ -122,6 +125,13 @@ void main() {
122
125
  mark *= ${glslIsFarDepth('stored')} ? 0.0 : 1.0;
123
126
 
124
127
  /* White is the identity of a multiply, so a pixel the projector missed changes nothing. */
125
- fragColor = vec4(mix(vec3(1.0), uDecalColor, mark * uDecalOpacity), 1.0);
128
+ vec3 multiply = mix(vec3(1.0), uDecalColor, mark * uDecalOpacity);
129
+ /*
130
+ * **The multiply as an encoded pixel takes it**, where the frame was graded before this: the
131
+ * receiver is a display value, and darkening its light by a factor is darkening the display value
132
+ * by that factor encoded — exact through the sRGB encode, whose power carries a product through,
133
+ * and close through a curve, which bends it. A frame graded at the resolve multiplies linear light.
134
+ */
135
+ fragColor = vec4(mix(multiply, linearToSrgb(multiply), float(uOutputTransform != 0)), 1.0);
126
136
  }
127
137
  `;
@@ -1,4 +1,5 @@
1
1
  import { FOG_GLSL } from './fog.ts';
2
+ import { OUTPUT_TRANSFORM_GLSL } from './outputTransform.ts';
2
3
 
3
4
  /**
4
5
  * A thin, wet, iridescent film lying on a surface.
@@ -122,6 +123,7 @@ uniform float uFilmRoughness;
122
123
  uniform float uFilmRoughnessCycles;
123
124
 
124
125
  out vec4 fragColor;
126
+ ${OUTPUT_TRANSFORM_GLSL}
125
127
 
126
128
  /** A hue wheel, without a texture. Standard 6-segment ramp. */
127
129
  vec3 hue(float h) {
@@ -199,6 +201,19 @@ void main() {
199
201
  vec3 base = vTint * 0.9;
200
202
  vec3 color = base + sheen * uSheen * (0.22 + grazing * 0.75);
201
203
 
204
+ /*
205
+ * **Graded before the mirror is mixed in**, as every pass grades what it draws when nothing after
206
+ * it will, because the mirror's own passes graded what it holds. The fog that used to follow the
207
+ * mirror distributes over the mix, so the film's own colour is fogged and graded here, and what
208
+ * the mirror shows is fogged toward the same air, graded, where it is mixed in below.
209
+ */
210
+ // The same fog every other pass applies — and now literally the same code, which
211
+ // it was not: this ran its own curve while claiming otherwise, so a slick receded
212
+ // at a different rate than the deck it was lying on.
213
+ float fog = mediumFog(length(uCameraPos - vWorld), vWorld.y);
214
+ vec3 gradedAir = applyOutputTransform(mediumColor());
215
+ vec3 graded = applyOutputTransform(mix(color, mediumColor(), fog));
216
+
202
217
  /*
203
218
  * The mirrored scene, under the sheen rather than instead of it.
204
219
  *
@@ -208,7 +223,7 @@ void main() {
208
223
  *
209
224
  * Weighted by the grazing term as well as by strength, because that is what a reflection does:
210
225
  * a wet surface underfoot shows almost nothing and the same surface down the street shows the
211
- * whole scene. Added before the fog so the image recedes with everything else in the frame.
226
+ * whole scene. Fogged as the film is, so the image recedes with everything else in the frame.
212
227
  */
213
228
  if (uReflectionEnabled != 0 && uReflectionStrength > 0.0 && vReflectionClip.w > 0.0) {
214
229
  vec2 reflectionUv = vReflectionClip.xy / vReflectionClip.w * 0.5 + 0.5;
@@ -254,15 +269,13 @@ void main() {
254
269
  valid = min(valid, smoothstep(0.0, 0.025, min(movedBorder.x, movedBorder.y)));
255
270
  }
256
271
  vec3 mirrored = textureLod(uReflectionMap, clamp(reflectionUv, 0.0, 1.0), 0.0).rgb;
257
- color = mix(color, mirrored, valid * uReflectionStrength * (0.25 + grazing * 0.75));
272
+ graded = mix(
273
+ graded,
274
+ mix(mirrored, gradedAir, fog),
275
+ valid * uReflectionStrength * (0.25 + grazing * 0.75)
276
+ );
258
277
  }
259
278
 
260
- // The same fog every other pass applies — and now literally the same code, which
261
- // it was not: this ran its own curve while claiming otherwise, so a slick receded
262
- // at a different rate than the deck it was lying on.
263
- float fog = mediumFog(length(uCameraPos - vWorld), vWorld.y);
264
- color = mix(color, mediumColor(), fog);
265
-
266
279
  /*
267
280
  * Alpha from coverage, squared.
268
281
  *
@@ -280,6 +293,6 @@ void main() {
280
293
  */
281
294
  float coverage = clamp(vCoverage, 0.0, 1.0);
282
295
  float alpha = mix(coverage * coverage, coverage, 0.65) * 0.92;
283
- fragColor = vec4(color, alpha);
296
+ fragColor = vec4(graded, alpha);
284
297
  }
285
298
  `;
@@ -1,4 +1,5 @@
1
1
  import { FOG_GLSL } from './fog.ts';
2
+ import { OUTPUT_TRANSFORM_GLSL } from './outputTransform.ts';
2
3
 
3
4
  /**
4
5
  * Procedural flames: camera-facing quads whose shape and colour come entirely
@@ -116,6 +117,7 @@ uniform vec4 uClipPlane;
116
117
  uniform int uClipEnabled;
117
118
 
118
119
  out vec4 outColor;
120
+ ${OUTPUT_TRANSFORM_GLSL}
119
121
 
120
122
  float hash21(vec2 p) {
121
123
  return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);
@@ -175,6 +177,7 @@ void main() {
175
177
  col = mix(col, mediumColor(), fog);
176
178
  alpha *= 1.0 - fog;
177
179
 
178
- outColor = vec4(col, alpha);
180
+ /* Graded where nothing after this will grade it, as every pass is. The alpha is coverage. */
181
+ outColor = vec4(applyOutputTransform(col), alpha);
179
182
  }
180
183
  `;
@@ -22,10 +22,9 @@ float layerExtras() { return uLayers[1].y; }
22
22
  vec3 layerTint(int i) { return vec3(1.0); }
23
23
  vec2 layerRanges(int i, vec2 roughMetal) { return roughMetal; }
24
24
  vec3 layerStrengthen(int i, vec3 texel) { return texel; }
25
- float layerSpecular(int i, float own) { return own; }
25
+ float layerNamedSpecular(int i) { return -1.0; }
26
26
  float layerRepeatDown(int i, float across) { return across; }
27
- float layerOcclusionStrength() { return 0.0; }
28
- bool layerOcclusionAmbient() { return false; }
27
+ vec2 layerOcclusionStrengths() { return vec2(0.0); }
29
28
  vec2 layerOcclusionRange() { return vec2(0.0, 1.0); }
30
29
  `;
31
30
  }
@@ -35,7 +34,7 @@ const bool LAYER_LOOKS = true; // wgsl:override
35
34
  /*
36
35
  * Fifteen vectors, as surfaceLayerLooks.ts packs them: the array layer each of the five reads, base
37
36
  * first (floats 0 to 4); where the maps beyond the layers start (5), the mesh occlusion's strength
38
- * (6) and whether it darkens the ambient light rather than the colour (7); then two vectors a
37
+ * on the colour, from 0 up (6), and on the ambient light (7); then two vectors a
39
38
  * layer, base first: its tint (x y z) and normal strength (w), then its roughness range (x to y)
40
39
  * and metalness range (z to w); then each layer's specular, -1 for the surface's own (48 to 52), its
41
40
  * repeat down (53 to 57), and the occlusion's range (58 to 59).
@@ -83,13 +82,10 @@ vec3 layerStrengthen(int i, vec3 texel) {
83
82
  return texel;
84
83
  }
85
84
 
86
- /* The layer's specular, or the surface's own where it names none. */
87
- float layerSpecular(int i, float own) {
88
- if (LAYER_LOOKS) {
89
- float named = layerLook(48 + i);
90
- return named < 0.0 ? own : named;
91
- }
92
- return own;
85
+ /* The specular the layer names, or -1 where it names none and the surface's own stands. */
86
+ float layerNamedSpecular(int i) {
87
+ if (LAYER_LOOKS) return layerLook(48 + i);
88
+ return -1.0;
93
89
  }
94
90
 
95
91
  /* The layer's repeat down, which is its repeat across unless it gave two. */
@@ -98,14 +94,10 @@ float layerRepeatDown(int i, float across) {
98
94
  return across;
99
95
  }
100
96
 
101
- float layerOcclusionStrength() {
102
- if (LAYER_LOOKS) return uLayerLooks[1].z;
103
- return 0.0;
104
- }
105
-
106
- bool layerOcclusionAmbient() {
107
- if (LAYER_LOOKS) return uLayerLooks[1].w > 0.5;
108
- return false;
97
+ /* The mesh occlusion's strength on the colour (x), from 0 up, and on the ambient light (y). */
98
+ vec2 layerOcclusionStrengths() {
99
+ if (LAYER_LOOKS) return uLayerLooks[1].zw;
100
+ return vec2(0.0);
109
101
  }
110
102
 
111
103
  vec2 layerOcclusionRange() {
@@ -61,8 +61,12 @@ ${layerLooksGlsl(looks)}
61
61
  /* Each layer's share of this point, the base's and layers 1 to 4's: worked out once, in layeredBegin. */
62
62
  float lShareBase;
63
63
  vec4 lShare;
64
- /* What an occlusion read at the mesh's coordinates leaves of this point, 1 where none: layeredBegin. */
65
- float lMeshOcclusion;
64
+ /*
65
+ * What an occlusion read at the mesh's coordinates leaves of this point's colour and of its ambient
66
+ * light, 1 where none: layeredBegin.
67
+ */
68
+ float lColorOcclusion;
69
+ float lAmbientOcclusion;
66
70
 
67
71
  bool layered() {
68
72
  return uLayers[1].y > 0.5;
@@ -103,15 +107,18 @@ vec4 layerPick(float layer) {
103
107
  void layeredBegin() {
104
108
  /*
105
109
  * The occlusion at the mesh's coordinates, once: the ORM array's layer past the layers and past
106
- * any mask and added mask it carries, spread over its range and taken at its strength.
110
+ * any mask and added mask it carries, spread over its range and taken at each strength. The
111
+ * colour's may pass 1, darkening faster than the occlusion does, and is held at black.
107
112
  */
108
- lMeshOcclusion = 1.0;
109
- float strength = layerOcclusionStrength();
110
- if (strength > 0.0) {
113
+ lColorOcclusion = 1.0;
114
+ lAmbientOcclusion = 1.0;
115
+ vec2 strength = layerOcclusionStrengths();
116
+ if (strength.x > 0.0 || strength.y > 0.0) {
111
117
  float at = layerExtras() + (abs(uLayers[1].w - 1.0) < 0.5 ? 1.0 : 0.0) + (uLayers[2].z > 0.5 ? 1.0 : 0.0);
112
118
  vec2 range = layerOcclusionRange();
113
- float occlusion = mix(range.x, range.y, texture(uOrmMap, vec3(vUv.xy, at)).r);
114
- lMeshOcclusion = mix(1.0, occlusion, strength);
119
+ float bare = 1.0 - mix(range.x, range.y, texture(uOrmMap, vec3(vUv.xy, at)).r);
120
+ lColorOcclusion = max(0.0, 1.0 - strength.x * bare);
121
+ lAmbientOcclusion = 1.0 - strength.y * bare;
115
122
  }
116
123
  vec4 m = layerWeights();
117
124
  if (uLayers[2].z > 0.5) {
@@ -158,7 +165,7 @@ vec3 layeredColor(vec3 color) {
158
165
 
159
166
  /*
160
167
  * Colour by the shares, each layer's tinted, and the base layer's coverage, which a cutout reads;
161
- * then darkened by the occlusion at the mesh's coordinates, where it is the colour that takes it.
168
+ * then darkened by the occlusion at the mesh's coordinates, at the colour's strength.
162
169
  */
163
170
  vec4 layeredAlbedo() {
164
171
  vec4 sum = vec4(0.0);
@@ -168,28 +175,36 @@ vec4 layeredAlbedo() {
168
175
  sum.rgb += t.rgb * layerTint(i) * layerShare(i);
169
176
  if (i == 0) sum.a = t.a;
170
177
  }
171
- if (!layerOcclusionAmbient()) sum.rgb *= lMeshOcclusion;
178
+ sum.rgb *= lColorOcclusion;
172
179
  return sum;
173
180
  }
174
181
 
175
- /* What the ambient light keeps: the occlusion at the mesh's coordinates, where it takes it. */
182
+ /* What the ambient light keeps: the occlusion at the mesh's coordinates, at the ambient's strength. */
176
183
  float layeredAmbient() {
177
- return layerOcclusionAmbient() ? lMeshOcclusion : 1.0;
184
+ return lAmbientOcclusion;
178
185
  }
179
186
 
180
187
  /*
181
- * The specular by the shares, each layer's own or the surface's where it names none. The surface's
182
- * own, untouched, where no material has asked for looks: shares summing to one in floating point
183
- * are not quite one, and a layered material that names nothing must draw exactly as before.
188
+ * The specular by the shares: each layer's named one at its share, and the surface's own over the
189
+ * layers naming none, once at most. Under a summed blend the shares add past 1 where the weights
190
+ * do, and a surface whose layers leave the specular to it reflects its own, not its own times that
191
+ * sum; under a blend laid over, the shares never pass 1 and this is the sum it always was. What it
192
+ * gives up: named speculars still add as their colours do, so a summed blend that names them
193
+ * reflects their sum. The surface's own, untouched, where no material has asked for looks: shares
194
+ * summing to one in floating point are not quite one, and a layered material that names nothing
195
+ * must draw exactly as before.
184
196
  */
185
197
  float layeredSpecular(float own) {
186
198
  if (LAYER_LOOKS) {
187
- float sum = 0.0;
199
+ float named = 0.0;
200
+ float unnamed = 0.0;
188
201
  for (int i = 0; i < 5; i++) {
189
202
  if (float(i) >= uLayers[1].y) break;
190
- sum += layerSpecular(i, own) * layerShare(i);
203
+ float specular = layerNamedSpecular(i);
204
+ if (specular < 0.0) unnamed += layerShare(i);
205
+ else named += specular * layerShare(i);
191
206
  }
192
- return sum;
207
+ return named + own * min(unnamed, 1.0);
193
208
  }
194
209
  return own;
195
210
  }
@@ -1,3 +1,4 @@
1
+ import { OUTPUT_TRANSFORM_GLSL } from './outputTransform.ts';
1
2
  /**
2
3
  * Birds: two triangles each, circling on a path computed entirely in the vertex
3
4
  * shader.
@@ -96,8 +97,10 @@ precision highp float;
96
97
  in float vShade;
97
98
  uniform vec3 uTint;
98
99
  out vec4 outColor;
100
+ ${OUTPUT_TRANSFORM_GLSL}
99
101
 
100
102
  void main() {
101
- outColor = vec4(uTint * vShade, 1.0);
103
+ /* Graded where nothing after this will grade it, as every pass is. */
104
+ outColor = vec4(applyOutputTransform(uTint * vShade), 1.0);
102
105
  }
103
106
  `;