@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
@@ -1,4 +1,5 @@
1
1
  import { glslFarDepth } from '../depthConvention.ts';
2
+ import { OUTPUT_TRANSFORM_GLSL } from './outputTransform.ts';
2
3
  /**
3
4
  * Procedural gradient sky drawn as a single attribute-less fullscreen triangle.
4
5
  * Ray direction is reconstructed per-fragment from the inverse view-projection,
@@ -77,6 +78,7 @@ uniform vec3 uUnderwaterColor;
77
78
  uniform float uUnderwaterFactor;
78
79
 
79
80
  out vec4 outColor;
81
+ ${OUTPUT_TRANSFORM_GLSL}
80
82
 
81
83
  float hash21(vec2 p) {
82
84
  return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);
@@ -273,6 +275,8 @@ void main() {
273
275
  float waterLight = mix(0.62, 1.08, clamp(dir.y * 0.5 + 0.5, 0.0, 1.0));
274
276
  col = mix(col, uUnderwaterColor * waterLight, uUnderwaterFactor);
275
277
 
276
- outColor = vec4(col, 1.0);
278
+ /* Graded where nothing after this will grade it, as every pass is: its colours are the same
279
+ light as the fog the world fades into. 0 in a probe's face, whose cube stores radiance. */
280
+ outColor = vec4(applyOutputTransform(col), 1.0);
277
281
  }
278
282
  `;
@@ -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
  * Smoke: a column that rises, expands and dissipates. Shares the plume
@@ -22,6 +23,7 @@ uniform vec4 uClipPlane;
22
23
  uniform int uClipEnabled;
23
24
 
24
25
  out vec4 outColor;
26
+ ${OUTPUT_TRANSFORM_GLSL}
25
27
 
26
28
  float hash21(vec2 p) {
27
29
  return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);
@@ -84,6 +86,7 @@ void main() {
84
86
  float fog = mediumFog(vDistance, vWorldPos.y);
85
87
  col = mix(col, atmosphereColor, fog);
86
88
  float alpha = clamp(density * 0.95, 0.0, 1.0) * emerge * dissipate * 0.13;
87
- outColor = vec4(col, alpha * (1.0 - fog));
89
+ /* Graded where nothing after this will grade it, as every pass is. The alpha is coverage. */
90
+ outColor = vec4(applyOutputTransform(col), alpha * (1.0 - fog));
88
91
  }
89
92
  `;
@@ -1,3 +1,4 @@
1
+ import { OUTPUT_TRANSFORM_GLSL } from './outputTransform.ts';
1
2
  /**
2
3
  * Screen-space 3D text.
3
4
  *
@@ -147,6 +148,7 @@ uniform float uGlow;
147
148
  uniform float uAlpha;
148
149
 
149
150
  out vec4 fragColor;
151
+ ${OUTPUT_TRANSFORM_GLSL}
150
152
 
151
153
  void main() {
152
154
  /*
@@ -161,5 +163,6 @@ void main() {
161
163
  vec3 lit = uColor * lambert;
162
164
  // The glow rides the arrival: brightest as a character lands, settling after.
163
165
  vec3 emissive = uColor * uGlow * (0.45 + 0.55 * vChar);
164
- fragColor = vec4(lit + emissive, uAlpha);
166
+ /* Graded where nothing after this will grade it, as every pass is. The alpha is coverage. */
167
+ fragColor = vec4(applyOutputTransform(lit + emissive), uAlpha);
165
168
  }`;
@@ -2,6 +2,7 @@ import { MAX_POINT_LIGHTS } from '../lightBudget.ts';
2
2
  import { MAX_WATER_REFLECTION_FILTER_TAPS } from '../renderQuality.ts';
3
3
  import { GERSTNER_GLSL } from './gerstner.ts';
4
4
  import { FOG_GLSL } from './fog.ts';
5
+ import { OUTPUT_TRANSFORM_GLSL } from './outputTransform.ts';
5
6
 
6
7
  /**
7
8
  * The sky-ocean, as an actual wave simulation rather than a tinted plane.
@@ -198,6 +199,7 @@ uniform sampler2D uReflectionMap;
198
199
  uniform int uReflectionEnabled;
199
200
  uniform vec2 uReflectionTexelSize;
200
201
  uniform int uReflectionFilterTaps;
202
+ ${OUTPUT_TRANSFORM_GLSL}
201
203
 
202
204
  const vec2 REFLECTION_FILTER[${MAX_WATER_REFLECTION_FILTER_TAPS}] = vec2[${MAX_WATER_REFLECTION_FILTER_TAPS}](
203
205
  vec2( 0.00, 0.00), vec2(-0.62, -0.31), vec2( 0.54, 0.43),
@@ -276,6 +278,14 @@ void main() {
276
278
  float dist = distance(vWorldPos, uCameraPos);
277
279
  float fog = mediumFog(dist, vWorldPos.y);
278
280
  col = mix(col, atmosphereColor, fog);
281
+ /*
282
+ * **Graded here, before the mirror is mixed in**, as every pass grades what it draws when
283
+ * nothing after it will: the mirror's own passes graded what it holds, so mixing first would put
284
+ * the curve on it twice. The air's colour, which stands in where the mirror shows nothing, is
285
+ * graded to meet it, as the sky behind it is.
286
+ */
287
+ col = applyOutputTransform(col);
288
+ vec3 gradedAir = applyOutputTransform(atmosphereColor);
279
289
 
280
290
  // Transparency is angle-dependent, like real water: looking down you see
281
291
  // straight through to the seabed, at grazing angles it turns reflective.
@@ -289,7 +299,7 @@ void main() {
289
299
  // Project the displaced surface into the mirrored camera. The Gerstner
290
300
  // displacement supplies broad distortion; the analytic normal adds the
291
301
  // smaller ripples that make reflected silhouettes move with each crest.
292
- vec3 reflected = atmosphereColor;
302
+ vec3 reflected = gradedAir;
293
303
  float reflectionValid = 0.0;
294
304
  if (uReflectionEnabled != 0 && vReflectionClip.w > 0.0) {
295
305
  vec2 reflectionUv = vReflectionClip.xy / vReflectionClip.w * 0.5 + 0.5;
@@ -324,7 +334,7 @@ void main() {
324
334
  filterWeight += weight;
325
335
  }
326
336
  sceneReflection /= max(filterWeight, 0.001);
327
- reflected = mix(atmosphereColor, sceneReflection, reflectionValid);
337
+ reflected = mix(gradedAir, sceneReflection, reflectionValid);
328
338
  }
329
339
  // The atmosphere-colour fallback preserves the old sky reflection when a
330
340
  // quality profile disables the extra scene pass. Foam scatters rather than
@@ -1,3 +1,4 @@
1
+ import { OUTPUT_TRANSFORM_GLSL } from './outputTransform.ts';
1
2
  /**
2
3
  * Visible wind: streaks of dust and debris carried past the camera.
3
4
  *
@@ -117,6 +118,7 @@ in float vFade;
117
118
  in float vAlong;
118
119
  uniform vec3 uTint;
119
120
  out vec4 outColor;
121
+ ${OUTPUT_TRANSFORM_GLSL}
120
122
 
121
123
  void main() {
122
124
  // Tapered at both ends, so a streak reads as motion rather than as a stick.
@@ -125,6 +127,7 @@ void main() {
125
127
  float taper = sin(vAlong * 3.14159);
126
128
  float alpha = vFade * taper * taper * 0.13;
127
129
  if (alpha <= 0.002) discard;
128
- outColor = vec4(uTint, alpha);
130
+ /* Graded where nothing after this will grade it, as every pass is. The alpha is coverage. */
131
+ outColor = vec4(applyOutputTransform(uTint), alpha);
129
132
  }
130
133
  `;
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Which layer of a layered material's arrays each of its layers reads, and how each looks: the
3
+ * fifteen vectors `shaders/flat/layerLooks.ts` reads, packed from `SurfaceMaterial.layers`.
4
+ *
5
+ * **Why a material needs these at all.** Layers read from the material's own arrays in order, so
6
+ * a texture worn by fifty materials in fifty tints had to be baked fifty times, each with its tint,
7
+ * its roughness range and its normal strength in the pixels. Picked by index from arrays the
8
+ * materials share, and given their look as numbers, each texture is held once: a set of layered
9
+ * materials built from a few textures costs those textures rather than one copy per material.
10
+ *
11
+ * **What it gives up**: fifteen fragment uniform vectors, so it is a lit switch of its own,
12
+ * `LAYER_LOOKS`, which a device short of vectors refuses while still drawing the layers; a layer's
13
+ * look is a handful of numbers, not a graph; and every layer is still read wherever any shows, as
14
+ * `surfaceLayers.ts` says, now from arrays that may be large.
15
+ */
16
+ import type { Vec3 } from '../math/color.ts';
17
+ import type { SurfaceLayers } from './surfaceLayers.ts';
18
+ import { MAX_SURFACE_LAYERS, across } from './surfaceLayers.ts';
19
+
20
+ /**
21
+ * One layer's own look, applied to what it reads before the layers are blended. Every field is the
22
+ * identity where it is absent, so a material names only what differs.
23
+ */
24
+ export interface SurfaceLayerLook {
25
+ /** Multiplies the layer's colour, linear. Above 1 brightens it. Absent: `[1, 1, 1]`. */
26
+ readonly tint?: Vec3;
27
+ /** The roughness the ORM's green channel is spread over, from its 0 to its 1. Absent: `[0, 1]`. */
28
+ readonly roughness?: readonly [number, number];
29
+ /** The metalness the ORM's blue channel is spread over, the same way. Absent: `[0, 1]`. */
30
+ readonly metalness?: readonly [number, number];
31
+ /**
32
+ * How far the layer's normal map bends the surface: its tangent-space x and y times this, before
33
+ * the layers' normals are blended. 0 is flat, 1 the map as it is. Absent: 1.
34
+ */
35
+ readonly normalStrength?: number;
36
+ /**
37
+ * The layer's specular, in the units a mesh's vertices carry theirs: how strong a highlight is,
38
+ * and under `physicalSpecular` the reflectance head-on, `0.08 × specular`. Blended across the
39
+ * layers as their colours are. Absent: the surface's own, as its vertices give it.
40
+ */
41
+ readonly specular?: number;
42
+ }
43
+
44
+ /** What an occlusion read at the mesh's own coordinates darkens, and how far. */
45
+ export interface SurfaceMeshOcclusion {
46
+ /** 0 to 1: what `into` names times `mix(1, occlusion, strength)`. Absent: 1. */
47
+ readonly strength?: number;
48
+ /**
49
+ * `'color'`, the default, darkens the blended colour, and so every light on it, the sun's too.
50
+ * `'ambient'` darkens the ambient light alone, sky, ground and probe, as an ambient occlusion
51
+ * does, leaving the sun, the lamps and the reflections as they were.
52
+ */
53
+ readonly into?: 'color' | 'ambient';
54
+ /**
55
+ * The colour's own strength, from 0 up and past 1: the blended colour times
56
+ * `max(0, 1 − colorStrength × (1 − occlusion))`, which up to 1 is the `mix` above and past it
57
+ * darkens faster than the occlusion does. Absent: `strength` where `into` is the colour, and 0
58
+ * where it is the ambient light — so named beside `into: 'ambient'` it darkens both, each at its
59
+ * own strength, as a material taking its ambient occlusion and a darker colour from one map does.
60
+ *
61
+ * **What it gives up**: past 1 the colour is black wherever `1 − occlusion` reaches
62
+ * `1 / colorStrength`, and black under every light, the sun's too, which is what such a material
63
+ * asks for and what an ambient occlusion alone would never do.
64
+ */
65
+ readonly colorStrength?: number;
66
+ /** The range the occlusion's red is spread over, from its 0 to its 1. Absent: `[0, 1]`. */
67
+ readonly range?: readonly [number, number];
68
+ }
69
+
70
+ /** Floats `packLayerLooks` writes: fifteen vectors. */
71
+ export const LOOK_FLOATS = 60;
72
+
73
+ /** The specular a layer is packed with when it names none: the surface's own, as its vertices give it. */
74
+ const OWN_SPECULAR = -1;
75
+
76
+ /**
77
+ * Whether a material asks for anything the `LAYER_LOOKS` switch carries: layers picked by index,
78
+ * extra maps placed somewhere other than just past the layers, a look, a repeat that is a pair, or
79
+ * a mesh occlusion.
80
+ */
81
+ export function asksLayerLooks<Texture>(
82
+ layers: SurfaceLayers<Texture> | null | undefined,
83
+ ): boolean {
84
+ if (layers === null || layers === undefined) return false;
85
+ return (
86
+ layers.arrayLayers !== undefined ||
87
+ layers.extrasAt !== undefined ||
88
+ layers.looks !== undefined ||
89
+ layers.repeats.some((repeat) => typeof repeat !== 'number') ||
90
+ colorOcclusion(layers.meshOcclusion) > 0 ||
91
+ ambientOcclusion(layers.meshOcclusion) > 0
92
+ );
93
+ }
94
+
95
+ /**
96
+ * `layers` into `out`, fifteen vectors, the identity wherever a material says nothing — which is
97
+ * also what a layered material that asks for none of this is drawn with once another has switched
98
+ * `LAYER_LOOKS` on, so it must draw exactly as it did before.
99
+ *
100
+ * - **0 to 7**: the array layer each layer reads, base first (layer `i` where not given); where the
101
+ * maps beyond the layers start (the layer count where not given); the mesh occlusion's strength
102
+ * on the colour, from 0 up, and on the ambient light, 0 to 1.
103
+ * - **8 to 47**: two vectors a layer, base first: its tint and its normal strength, then its
104
+ * roughness range and its metalness range.
105
+ * - **48 to 59**: each layer's specular (−1 for the surface's own), each layer's repeat down (its
106
+ * repeat across where it gave one number), and the range the occlusion is spread over.
107
+ *
108
+ * An array layer that is not a whole number at or above 0 reads layer `i`; a start that is not one
109
+ * is the count; a strength is held to 0..1, but for the colour's own, which is held at or above 0; a tint channel, a normal strength or a specular that is
110
+ * not a number at or above 0 is the identity; a repeat down that is not a positive number is the
111
+ * repeat across; a range end that is not a number is the identity's end.
112
+ */
113
+ export function packLayerLooks<Texture>(
114
+ layers: SurfaceLayers<Texture> | null | undefined,
115
+ out: Float32Array,
116
+ ): void {
117
+ const count = Math.min(layers?.repeats.length ?? 0, MAX_SURFACE_LAYERS);
118
+ const picks = layers?.arrayLayers;
119
+ for (let i = 0; i < MAX_SURFACE_LAYERS; i++) out[i] = arrayLayer(picks?.[i], i);
120
+ out[5] = arrayLayer(layers?.extrasAt, count);
121
+ const occlusion = layers?.meshOcclusion;
122
+ out[6] = colorOcclusion(occlusion);
123
+ out[7] = ambientOcclusion(occlusion);
124
+ for (let i = 0; i < MAX_SURFACE_LAYERS; i++) {
125
+ const look = layers?.looks?.[i] ?? null;
126
+ const at = 8 + i * 8;
127
+ out[at] = atLeastZero(look?.tint?.[0], 1);
128
+ out[at + 1] = atLeastZero(look?.tint?.[1], 1);
129
+ out[at + 2] = atLeastZero(look?.tint?.[2], 1);
130
+ out[at + 3] = atLeastZero(look?.normalStrength, 1);
131
+ out[at + 4] = finiteOr(look?.roughness?.[0], 0);
132
+ out[at + 5] = finiteOr(look?.roughness?.[1], 1);
133
+ out[at + 6] = finiteOr(look?.metalness?.[0], 0);
134
+ out[at + 7] = finiteOr(look?.metalness?.[1], 1);
135
+ out[48 + i] = atLeastZero(look?.specular, OWN_SPECULAR);
136
+ const repeat = layers?.repeats[i];
137
+ const acrossIt = positiveOr(across(repeat), 1);
138
+ out[53 + i] = typeof repeat === 'object' ? positiveOr(repeat[1], acrossIt) : acrossIt;
139
+ }
140
+ const range = typeof occlusion === 'object' ? occlusion.range : undefined;
141
+ out[58] = finiteOr(range?.[0], 0);
142
+ out[59] = finiteOr(range?.[1], 1);
143
+ }
144
+
145
+ /** The strength `meshOcclusion` names, held to 0..1: a number's own, an object's or 1, and 0 for none. */
146
+ function occlusionStrength(occlusion: number | SurfaceMeshOcclusion | undefined): number {
147
+ const strength =
148
+ typeof occlusion === 'number'
149
+ ? occlusion
150
+ : occlusion === undefined
151
+ ? 0
152
+ : (occlusion.strength ?? 1);
153
+ return Number.isFinite(strength) ? Math.min(Math.max(strength, 0), 1) : 0;
154
+ }
155
+
156
+ /** How hard the occlusion darkens the colour: its own strength where named, else `into`'s. */
157
+ function colorOcclusion(occlusion: number | SurfaceMeshOcclusion | undefined): number {
158
+ if (typeof occlusion === 'object' && occlusion.colorStrength !== undefined)
159
+ return atLeastZero(occlusion.colorStrength, 0);
160
+ return typeof occlusion === 'object' && occlusion.into === 'ambient'
161
+ ? 0
162
+ : occlusionStrength(occlusion);
163
+ }
164
+
165
+ /** How hard the occlusion darkens the ambient light: its strength where that is its target. */
166
+ function ambientOcclusion(occlusion: number | SurfaceMeshOcclusion | undefined): number {
167
+ return typeof occlusion === 'object' && occlusion.into === 'ambient'
168
+ ? occlusionStrength(occlusion)
169
+ : 0;
170
+ }
171
+
172
+ function arrayLayer(value: number | undefined, otherwise: number): number {
173
+ return value !== undefined && Number.isInteger(value) && value >= 0 ? value : otherwise;
174
+ }
175
+
176
+ function atLeastZero(value: number | undefined, otherwise: number): number {
177
+ return value !== undefined && Number.isFinite(value) && value >= 0 ? value : otherwise;
178
+ }
179
+
180
+ function positiveOr(value: number | undefined, otherwise: number): number {
181
+ return value !== undefined && Number.isFinite(value) && value > 0 ? value : otherwise;
182
+ }
183
+
184
+ function finiteOr(value: number | undefined, otherwise: number): number {
185
+ return value !== undefined && Number.isFinite(value) ? value : otherwise;
186
+ }
@@ -2,6 +2,7 @@
2
2
  * A material blended from layers by a mask: `SurfaceMaterial.layers`, the four vectors
3
3
  * `shaders/flat/layered.ts` reads it from, and the decisions both backends take about it.
4
4
  */
5
+ import type { SurfaceLayerLook, SurfaceMeshOcclusion } from './surfaceLayerLooks.ts';
5
6
  import type { SurfaceProjection } from './surfaceProjection.ts';
6
7
 
7
8
  /** The most layers a material blends: a base and one for each of the mask's four channels. */
@@ -10,8 +11,9 @@ export const MAX_SURFACE_LAYERS = 5;
10
11
  /**
11
12
  * Up to five layers, each its own colour, normal and roughness at its own repeat, blended by a mask.
12
13
  *
13
- * **Layer `i` is layer `i` of the material's arrays**: `albedo`, `normal` and `orm` each an array
14
- * whose layers are the material's layers in order, read at the mesh's coordinates times
14
+ * **Layer `i` is layer `i` of the material's arrays**, unless `arrayLayers` picks another: `albedo`,
15
+ * `normal` and `orm` each an array whose layers are the material's layers, read at the mesh's
16
+ * coordinates times
15
17
  * `repeats[i]`, so rock can repeat forty times across a cliff while the moss on it repeats three.
16
18
  * Where the material has a `projection`, the layers are read at the world's horizontal position
17
19
  * instead, so a repeat is so many a metre (with the projection's own scale at 1). **The mask** is
@@ -23,8 +25,13 @@ export const MAX_SURFACE_LAYERS = 5;
23
25
  * **Maps beyond the layers ride the material's own arrays, after its layers**, so they take no
24
26
  * texture binding of their own — the lit stage has none to spare: a mask the ORM array carries
25
27
  * (`mask: 'orm'`) is the ORM array's layer just past the layers, an `addMask` the one after that,
26
- * and `meshNormal` the normal array's layer just past the layers. Each shares its array's size and
27
- * format, so a four-channel mask wants an array that stores four.
28
+ * a `meshOcclusion` the one after any of those, and `meshNormal` the normal array's layer just past
29
+ * the layers. `extrasAt` moves where "just past" is, for materials that share arrays. Each shares
30
+ * its array's size and format, so a four-channel mask wants an array that stores four.
31
+ *
32
+ * **Shared arrays and a look of each layer's own** — `arrayLayers`, `extrasAt`, `looks` and
33
+ * `meshOcclusion` — are a lit switch of their own, `LAYER_LOOKS`, and `surfaceLayerLooks.ts` says
34
+ * what they cost.
28
35
  *
29
36
  * **What it gives up**: every layer's maps are read wherever any shows, three reads a layer, so five
30
37
  * layers are fifteen reads where one material is three; layers share an array's size and format; a
@@ -40,8 +47,12 @@ export interface SurfaceLayers<Texture> {
40
47
  * vertex colour's red, green and blue, which then weights the layers instead of tinting them.
41
48
  */
42
49
  readonly mask: Texture | 'orm' | 'vertex';
43
- /** Each layer's repeats across the mesh's coordinates, base first: one to five of them. */
44
- readonly repeats: readonly number[];
50
+ /**
51
+ * Each layer's repeats across the mesh's coordinates, base first: one to five of them. A pair is a
52
+ * repeat across and a repeat down apart, moss stretched along a trunk; a pair is drawn through the
53
+ * `LAYER_LOOKS` switch, which a single number does not need.
54
+ */
55
+ readonly repeats: readonly (number | readonly [number, number])[];
45
56
  /** The layer the emissive map glows on, 0 to 4. Absent: everywhere, as on any material. */
46
57
  readonly emissiveLayer?: number;
47
58
  /**
@@ -77,6 +88,34 @@ export interface SurfaceLayers<Texture> {
77
88
  * layer repeating forty times across it cannot carry.
78
89
  */
79
90
  readonly meshNormal?: boolean;
91
+ /**
92
+ * Which layer of the material's albedo, normal and ORM arrays each layer reads, base first, so
93
+ * many materials can share one array of each kind and hold every texture once. Absent, or past
94
+ * its end, layer `i` reads layer `i`. The same index reads all three arrays, so a texture's three
95
+ * maps sit at the same layer of each.
96
+ */
97
+ readonly arrayLayers?: readonly number[];
98
+ /**
99
+ * The layer of each array where the maps beyond the layers start: the ORM array's mask, added
100
+ * mask and mesh occlusion, in that order, and the normal array's mesh normal. Absent: just past
101
+ * the layers, at the layer count — which is no place for them once materials share arrays.
102
+ */
103
+ readonly extrasAt?: number;
104
+ /**
105
+ * Each layer's own look, base first: a tint, the ranges its ORM's roughness and metalness are
106
+ * spread over, and how far its normal map bends the surface. Absent or null for a layer: the map
107
+ * as it is.
108
+ */
109
+ readonly looks?: readonly (SurfaceLayerLook | null | undefined)[];
110
+ /**
111
+ * An occlusion read at the mesh's own coordinates — the red of the ORM array's layer after the
112
+ * layers and any mask or added mask the array carries — darkening the surface by a strength from 0
113
+ * to 1: the large shading of a cliff that layers repeating across it cannot carry, as
114
+ * `meshNormal` carries its shape. A number is that strength, darkening the blended colour; the
115
+ * object form also chooses what it darkens, the colour and the ambient light at strengths of their
116
+ * own, and the range its red is spread over. Absent or 0: none.
117
+ */
118
+ readonly meshOcclusion?: number | SurfaceMeshOcclusion;
80
119
  }
81
120
 
82
121
  /** Floats `packSurfaceLayers` writes: four vectors. */
@@ -104,7 +143,8 @@ export function packSurfaceLayers<Texture>(
104
143
  out.fill(0);
105
144
  if (layers === null || layers === undefined || layers.repeats.length === 0) return;
106
145
  const count = Math.min(layers.repeats.length, MAX_SURFACE_LAYERS);
107
- for (let i = 0; i < MAX_SURFACE_LAYERS; i++) out[i] = positive(i < count ? layers.repeats[i] : 1);
146
+ for (let i = 0; i < MAX_SURFACE_LAYERS; i++)
147
+ out[i] = positive(i < count ? across(layers.repeats[i]) : 1);
108
148
  out[5] = count;
109
149
  const glow = layers.emissiveLayer;
110
150
  out[6] = glow !== undefined && Number.isInteger(glow) && glow >= 0 && glow < count ? glow : -1;
@@ -162,6 +202,11 @@ export function layeredProjection<Texture>(
162
202
  return { kind: 'planar', scale: projection.scale };
163
203
  }
164
204
 
205
+ /** A layer's repeat across: the number, or the first of a pair. The second is `LAYER_LOOKS`'. */
206
+ export function across(repeat: number | readonly [number, number] | undefined): number | undefined {
207
+ return typeof repeat === 'number' || repeat === undefined ? repeat : repeat[0];
208
+ }
209
+
165
210
  /** A layer an added mask or the facing may name: one a channel lays, 1 to `count − 1`, or −1. */
166
211
  function laidLayer(layer: number | undefined, count: number): number {
167
212
  return layer !== undefined && Number.isInteger(layer) && layer >= 1 && layer < count ? layer : -1;
@@ -80,11 +80,17 @@ export class TextRenderer {
80
80
  originY: number,
81
81
  style: TextStyle,
82
82
  timeSec: number,
83
+ /** The renderer's `passGrade`: the frame's, but none in a probe's face or a capture. */
84
+ outputTransform = 0,
85
+ outputExposure = 1,
83
86
  ): boolean {
84
87
  if (this.layout.instanceCount === 0 || style.alpha <= 0) return false;
85
88
  const gl = this.gl;
86
89
 
87
90
  gl.useProgram(this.program);
91
+ const grade = this.uniforms['uOutputTransform'];
92
+ if (grade !== undefined) gl.uniform1i(grade, outputTransform);
93
+ setFloat(gl, this.uniforms['uOutputExposure'], outputExposure);
88
94
  gl.bindVertexArray(this.vao);
89
95
 
90
96
  /*
@@ -119,7 +119,7 @@ export const OUTPUT_TRANSFORM_CODE: Readonly<Record<string, number>> = {
119
119
  /**
120
120
  * The code a **forward** pass grades with: the transform's own, except `filmic`, which only the
121
121
  * composite carries the constants for, and which a pass grading itself therefore draws as `aces`.
122
- * Both backends' `gradeCode` read this, so neither can hand a forward stage a code it lacks.
122
+ * `passGrade.ts` reads this for both backends, so neither can hand a forward stage a code it lacks.
123
123
  */
124
124
  export function forwardTransformCode(transform: string): number {
125
125
  return transform === 'filmic' ? 2 : (OUTPUT_TRANSFORM_CODE[transform] ?? 0);
@@ -253,10 +253,15 @@ export class WaterRenderer {
253
253
  lightFalloff: 'smooth' | 'inverseSquare' = 'smooth',
254
254
  windX = 0,
255
255
  windZ = 0,
256
+ outputTransform = 0,
257
+ outputExposure = 1,
256
258
  ): void {
257
259
  const u = this.uniforms;
258
260
  const reflectionReady = reflection?.isReadyFor(settings.level) ?? false;
259
261
  gl.useProgram(this.program);
262
+ /* The renderer's `passGrade`: the frame's, but none in a probe's face or a capture. */
263
+ gl.uniform1i(u['uOutputTransform'] ?? null, outputTransform);
264
+ gl.uniform1f(u['uOutputExposure'] ?? null, outputExposure);
260
265
  gl.uniformMatrix4fv(u['uViewProj'] ?? null, false, camera.viewProjection);
261
266
  gl.uniformMatrix4fv(
262
267
  u['uReflectionViewProj'] ?? null,
@@ -86,6 +86,9 @@ export class WindStreakRenderer {
86
86
  tint: Vec3,
87
87
  /** True when the camera is below the waterline. */
88
88
  submerged = false,
89
+ /** The renderer's `passGrade`: the frame's, but none in a probe's face or a capture. */
90
+ outputTransform = 0,
91
+ outputExposure = 1,
89
92
  ): boolean {
90
93
  /* Chosen in `windStreakDraw.ts` so both backends show the same weather. */
91
94
  const settled = resolveWindStreaks(
@@ -111,6 +114,8 @@ export class WindStreakRenderer {
111
114
  gl.uniform1f(u['uCellSize'] ?? null, this.cellSize);
112
115
  gl.uniform1f(u['uTime'] ?? null, timeSeconds);
113
116
  gl.uniform3fv(u['uTint'] ?? null, tint);
117
+ gl.uniform1i(u['uOutputTransform'] ?? null, outputTransform);
118
+ gl.uniform1f(u['uOutputExposure'] ?? null, outputExposure);
114
119
 
115
120
  gl.enable(gl.BLEND);
116
121
  blendCovering(gl, gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);