@driftengine/core 4.8.6 → 4.9.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 (253) hide show
  1. package/CHANGELOG.json +132 -0
  2. package/README.md +1 -1
  3. package/dist/index.d.ts +7 -2
  4. package/dist/index.js +8 -1
  5. package/dist/render/anisotropicLobe.d.ts +10 -0
  6. package/dist/render/anisotropicLobe.js +19 -0
  7. package/dist/render/areaLights.d.ts +35 -0
  8. package/dist/render/areaLights.js +35 -0
  9. package/dist/render/atmosphere.d.ts +13 -0
  10. package/dist/render/atmosphere.js +9 -1
  11. package/dist/render/backend/api.d.ts +41 -3
  12. package/dist/render/backend/createRenderer.d.ts +6 -0
  13. package/dist/render/backend/createRenderer.js +4 -1
  14. package/dist/render/backend/webgl2/renderer.d.ts +140 -16
  15. package/dist/render/backend/webgl2/renderer.js +436 -44
  16. package/dist/render/backend/webgl2/sceneCaptureTarget.d.ts +29 -0
  17. package/dist/render/backend/webgl2/sceneCaptureTarget.js +96 -0
  18. package/dist/render/backend/webgpu/device.d.ts +7 -1
  19. package/dist/render/backend/webgpu/device.js +31 -3
  20. package/dist/render/backend/webgpu/flatPass.d.ts +16 -1
  21. package/dist/render/backend/webgpu/flatPass.js +58 -26
  22. package/dist/render/backend/webgpu/particlePass.js +2 -1
  23. package/dist/render/backend/webgpu/postPass.d.ts +51 -9
  24. package/dist/render/backend/webgpu/postPass.js +16 -2
  25. package/dist/render/backend/webgpu/reflectionSurfacePass.d.ts +30 -0
  26. package/dist/render/backend/webgpu/reflectionSurfacePass.js +100 -0
  27. package/dist/render/backend/webgpu/renderer.d.ts +166 -3
  28. package/dist/render/backend/webgpu/renderer.js +738 -83
  29. package/dist/render/backend/webgpu/sceneCaptureTarget.d.ts +24 -0
  30. package/dist/render/backend/webgpu/sceneCaptureTarget.js +59 -0
  31. package/dist/render/backend/webgpu/shaders/clusterBin.wgsl.d.ts +1 -1
  32. package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +4 -1
  33. package/dist/render/backend/webgpu/surfaceTexturePass.js +34 -8
  34. package/dist/render/backend/webgpu/waterPass.d.ts +1 -1
  35. package/dist/render/clusteredLights.d.ts +23 -5
  36. package/dist/render/clusteredLights.js +35 -8
  37. package/dist/render/displayRange.d.ts +31 -0
  38. package/dist/render/displayRange.js +47 -0
  39. package/dist/render/driftLight/bake.d.ts +2 -0
  40. package/dist/render/driftLight/bake.js +1 -1
  41. package/dist/render/driftLight/falloff.d.ts +4 -2
  42. package/dist/render/driftLight/falloff.js +8 -2
  43. package/dist/render/filmicCurve.d.ts +74 -0
  44. package/dist/render/filmicCurve.js +203 -0
  45. package/dist/render/fog.d.ts +4 -1
  46. package/dist/render/fog.js +12 -2
  47. package/dist/render/frame/resources.d.ts +2 -2
  48. package/dist/render/frame/resources.js +6 -0
  49. package/dist/render/frameReflections.d.ts +40 -0
  50. package/dist/render/frameReflections.js +48 -0
  51. package/dist/render/gi/bounceLights.d.ts +3 -2
  52. package/dist/render/gi/bounceLights.js +8 -3
  53. package/dist/render/instanceCull.js +5 -0
  54. package/dist/render/instances.d.ts +19 -5
  55. package/dist/render/instances.js +9 -3
  56. package/dist/render/lightBudget.d.ts +12 -1
  57. package/dist/render/lightBudget.js +23 -3
  58. package/dist/render/lightmap.d.ts +23 -9
  59. package/dist/render/lightmap.js +40 -18
  60. package/dist/render/particlePool.d.ts +21 -0
  61. package/dist/render/particlePool.js +7 -0
  62. package/dist/render/pass.d.ts +49 -0
  63. package/dist/render/pointLightSelection.d.ts +13 -0
  64. package/dist/render/pointLightSelection.js +3 -1
  65. package/dist/render/reflectionSurfaceTargets.d.ts +28 -0
  66. package/dist/render/reflectionSurfaceTargets.js +99 -0
  67. package/dist/render/renderQuality.d.ts +47 -15
  68. package/dist/render/renderQuality.js +4 -0
  69. package/dist/render/rendererHarness.js +3 -0
  70. package/dist/render/rgb9e5.d.ts +22 -0
  71. package/dist/render/rgb9e5.js +47 -0
  72. package/dist/render/sceneCapture.d.ts +29 -0
  73. package/dist/render/sceneCapture.js +36 -0
  74. package/dist/render/sceneTarget.d.ts +5 -0
  75. package/dist/render/sceneTarget.js +24 -1
  76. package/dist/render/screenSpaceReflection.d.ts +32 -1
  77. package/dist/render/screenSpaceReflection.js +14 -0
  78. package/dist/render/shaders/arcane.d.ts +1 -1
  79. package/dist/render/shaders/bolt.d.ts +1 -1
  80. package/dist/render/shaders/bolt.js +7 -1
  81. package/dist/render/shaders/caustics.d.ts +1 -1
  82. package/dist/render/shaders/depth.js +11 -0
  83. package/dist/render/shaders/film.d.ts +1 -1
  84. package/dist/render/shaders/filmLook.js +2 -1
  85. package/dist/render/shaders/fire.d.ts +1 -1
  86. package/dist/render/shaders/flat/anisotropic.d.ts +1 -1
  87. package/dist/render/shaders/flat/anisotropic.js +37 -3
  88. package/dist/render/shaders/flat/areaLight.d.ts +1 -1
  89. package/dist/render/shaders/flat/areaLight.js +46 -0
  90. package/dist/render/shaders/flat/glassTint.d.ts +3 -2
  91. package/dist/render/shaders/flat/glassTint.js +5 -1
  92. package/dist/render/shaders/flat/index.d.ts +15 -0
  93. package/dist/render/shaders/flat/index.js +22 -2
  94. package/dist/render/shaders/flat/lobes.d.ts +6 -0
  95. package/dist/render/shaders/flat/lobes.js +49 -43
  96. package/dist/render/shaders/flat/main.d.ts +1 -1
  97. package/dist/render/shaders/flat/main.js +85 -22
  98. package/dist/render/shaders/flat/models.d.ts +5 -2
  99. package/dist/render/shaders/flat/models.js +100 -16
  100. package/dist/render/shaders/flat/overlay.d.ts +16 -0
  101. package/dist/render/shaders/flat/overlay.js +117 -0
  102. package/dist/render/shaders/flat/pointShadow.js +1 -1
  103. package/dist/render/shaders/flat/preamble.js +10 -3
  104. package/dist/render/shaders/flat/reflectionSurface.d.ts +21 -0
  105. package/dist/render/shaders/flat/reflectionSurface.js +61 -0
  106. package/dist/render/shaders/fog.d.ts +1 -1
  107. package/dist/render/shaders/fog.js +9 -2
  108. package/dist/render/shaders/fringe.d.ts +29 -0
  109. package/dist/render/shaders/fringe.js +55 -0
  110. package/dist/render/shaders/generated/arcane.wgsl.d.ts +1 -1
  111. package/dist/render/shaders/generated/arcane.wgsl.js +1 -1
  112. package/dist/render/shaders/generated/bolt.wgsl.d.ts +1 -1
  113. package/dist/render/shaders/generated/bolt.wgsl.js +1 -1
  114. package/dist/render/shaders/generated/caustics.wgsl.d.ts +1 -1
  115. package/dist/render/shaders/generated/caustics.wgsl.js +1 -1
  116. package/dist/render/shaders/generated/depth.wgsl.d.ts +4 -4
  117. package/dist/render/shaders/generated/depth.wgsl.js +4 -4
  118. package/dist/render/shaders/generated/film.wgsl.d.ts +1 -1
  119. package/dist/render/shaders/generated/film.wgsl.js +1 -1
  120. package/dist/render/shaders/generated/fire.wgsl.d.ts +1 -1
  121. package/dist/render/shaders/generated/fire.wgsl.js +1 -1
  122. package/dist/render/shaders/generated/flat.wgsl.d.ts +192 -16
  123. package/dist/render/shaders/generated/flat.wgsl.js +230 -54
  124. package/dist/render/shaders/generated/line.wgsl.d.ts +1 -1
  125. package/dist/render/shaders/generated/line.wgsl.js +1 -1
  126. package/dist/render/shaders/generated/particle.wgsl.d.ts +4 -4
  127. package/dist/render/shaders/generated/particle.wgsl.js +4 -4
  128. package/dist/render/shaders/generated/rush.wgsl.d.ts +56 -26
  129. package/dist/render/shaders/generated/rush.wgsl.js +56 -26
  130. package/dist/render/shaders/generated/scatter.wgsl.d.ts +1 -1
  131. package/dist/render/shaders/generated/scatter.wgsl.js +1 -1
  132. package/dist/render/shaders/generated/smoke.wgsl.d.ts +1 -1
  133. package/dist/render/shaders/generated/smoke.wgsl.js +1 -1
  134. package/dist/render/shaders/generated/ssrResolve.wgsl.d.ts +29 -0
  135. package/dist/render/shaders/generated/ssrResolve.wgsl.js +29 -0
  136. package/dist/render/shaders/generated/ssrTrace.wgsl.d.ts +32 -7
  137. package/dist/render/shaders/generated/ssrTrace.wgsl.js +32 -7
  138. package/dist/render/shaders/generated/water.wgsl.d.ts +23 -16
  139. package/dist/render/shaders/generated/water.wgsl.js +23 -16
  140. package/dist/render/shaders/gi/probeBake.wgsl.js +9 -5
  141. package/dist/render/shaders/gpudriven/fog.wgsl.d.ts +2 -2
  142. package/dist/render/shaders/gpudriven/fog.wgsl.js +11 -3
  143. package/dist/render/shaders/line.d.ts +1 -1
  144. package/dist/render/shaders/particle.d.ts +3 -3
  145. package/dist/render/shaders/recon/motion.wgsl.d.ts +1 -1
  146. package/dist/render/shaders/recon/motion.wgsl.js +13 -2
  147. package/dist/render/shaders/rush.js +54 -11
  148. package/dist/render/shaders/scatter.d.ts +1 -1
  149. package/dist/render/shaders/smoke.d.ts +1 -1
  150. package/dist/render/shaders/ssrResolve.d.ts +17 -0
  151. package/dist/render/shaders/ssrResolve.js +51 -0
  152. package/dist/render/shaders/ssrTrace.js +46 -1
  153. package/dist/render/shaders/water.d.ts +1 -1
  154. package/dist/render/shaders/water.js +12 -3
  155. package/dist/render/skinScatter.d.ts +3 -1
  156. package/dist/render/skinScatter.js +3 -1
  157. package/dist/render/ssrPass.d.ts +16 -0
  158. package/dist/render/ssrPass.js +107 -1
  159. package/dist/render/surfaceModel.d.ts +29 -8
  160. package/dist/render/surfaceModel.js +33 -9
  161. package/dist/render/surfaceOverlay.d.ts +132 -0
  162. package/dist/render/surfaceOverlay.js +85 -0
  163. package/dist/render/surfaceTexture.d.ts +44 -1
  164. package/dist/render/surfaceTexture.js +96 -5
  165. package/dist/render/temporalAa.d.ts +14 -0
  166. package/dist/render/temporalAa.js +20 -0
  167. package/dist/render/vertexDefaults.d.ts +17 -0
  168. package/dist/render/vertexDefaults.js +24 -0
  169. package/package.json +6 -6
  170. package/src/index.ts +22 -2
  171. package/src/render/anisotropicLobe.ts +29 -0
  172. package/src/render/areaLights.ts +58 -0
  173. package/src/render/atmosphere.ts +22 -1
  174. package/src/render/backend/api.ts +58 -2
  175. package/src/render/backend/createRenderer.ts +18 -1
  176. package/src/render/backend/webgl2/renderer.ts +548 -50
  177. package/src/render/backend/webgl2/sceneCaptureTarget.ts +130 -0
  178. package/src/render/backend/webgpu/device.ts +42 -3
  179. package/src/render/backend/webgpu/flatPass.ts +63 -26
  180. package/src/render/backend/webgpu/particlePass.ts +2 -1
  181. package/src/render/backend/webgpu/postPass.ts +18 -0
  182. package/src/render/backend/webgpu/reflectionSurfacePass.ts +114 -0
  183. package/src/render/backend/webgpu/renderer.ts +927 -100
  184. package/src/render/backend/webgpu/sceneCaptureTarget.ts +63 -0
  185. package/src/render/backend/webgpu/surfaceTexturePass.ts +44 -14
  186. package/src/render/clusteredLights.ts +41 -8
  187. package/src/render/displayRange.ts +61 -0
  188. package/src/render/driftLight/bake.ts +3 -1
  189. package/src/render/driftLight/falloff.ts +13 -2
  190. package/src/render/filmicCurve.ts +229 -0
  191. package/src/render/fog.ts +19 -3
  192. package/src/render/frame/resources.ts +6 -0
  193. package/src/render/frameReflections.ts +72 -0
  194. package/src/render/gi/bounceLights.ts +12 -3
  195. package/src/render/instanceCull.ts +4 -0
  196. package/src/render/instances.ts +26 -6
  197. package/src/render/lightBudget.ts +40 -2
  198. package/src/render/lightmap.ts +64 -23
  199. package/src/render/particlePool.ts +23 -0
  200. package/src/render/pass.ts +53 -1
  201. package/src/render/pointLightSelection.ts +20 -1
  202. package/src/render/reflectionSurfaceTargets.ts +105 -0
  203. package/src/render/renderQuality.ts +52 -15
  204. package/src/render/rendererHarness.ts +3 -0
  205. package/src/render/rgb9e5.ts +52 -0
  206. package/src/render/sceneCapture.ts +56 -0
  207. package/src/render/sceneTarget.ts +30 -2
  208. package/src/render/screenSpaceReflection.ts +38 -1
  209. package/src/render/shaders/bolt.ts +7 -1
  210. package/src/render/shaders/depth.ts +11 -0
  211. package/src/render/shaders/filmLook.ts +2 -1
  212. package/src/render/shaders/flat/anisotropic.ts +37 -3
  213. package/src/render/shaders/flat/areaLight.ts +46 -0
  214. package/src/render/shaders/flat/glassTint.ts +13 -2
  215. package/src/render/shaders/flat/index.ts +37 -2
  216. package/src/render/shaders/flat/lobes.ts +50 -43
  217. package/src/render/shaders/flat/main.ts +85 -22
  218. package/src/render/shaders/flat/models.ts +100 -16
  219. package/src/render/shaders/flat/overlay.ts +117 -0
  220. package/src/render/shaders/flat/pointShadow.ts +1 -1
  221. package/src/render/shaders/flat/preamble.ts +10 -3
  222. package/src/render/shaders/flat/reflectionSurface.ts +61 -0
  223. package/src/render/shaders/fog.ts +9 -2
  224. package/src/render/shaders/fringe.ts +59 -0
  225. package/src/render/shaders/generated/arcane.wgsl.ts +1 -1
  226. package/src/render/shaders/generated/bolt.wgsl.ts +1 -1
  227. package/src/render/shaders/generated/caustics.wgsl.ts +1 -1
  228. package/src/render/shaders/generated/depth.wgsl.ts +4 -4
  229. package/src/render/shaders/generated/film.wgsl.ts +1 -1
  230. package/src/render/shaders/generated/fire.wgsl.ts +1 -1
  231. package/src/render/shaders/generated/flat.wgsl.ts +230 -54
  232. package/src/render/shaders/generated/line.wgsl.ts +1 -1
  233. package/src/render/shaders/generated/particle.wgsl.ts +4 -4
  234. package/src/render/shaders/generated/rush.wgsl.ts +56 -26
  235. package/src/render/shaders/generated/scatter.wgsl.ts +1 -1
  236. package/src/render/shaders/generated/smoke.wgsl.ts +1 -1
  237. package/src/render/shaders/generated/ssrResolve.wgsl.ts +30 -0
  238. package/src/render/shaders/generated/ssrTrace.wgsl.ts +32 -7
  239. package/src/render/shaders/generated/water.wgsl.ts +23 -16
  240. package/src/render/shaders/gi/probeBake.wgsl.ts +9 -5
  241. package/src/render/shaders/gpudriven/fog.wgsl.ts +11 -3
  242. package/src/render/shaders/recon/motion.wgsl.ts +13 -2
  243. package/src/render/shaders/rush.ts +54 -11
  244. package/src/render/shaders/ssrResolve.ts +52 -0
  245. package/src/render/shaders/ssrTrace.ts +46 -1
  246. package/src/render/shaders/water.ts +12 -3
  247. package/src/render/skinScatter.ts +3 -1
  248. package/src/render/ssrPass.ts +139 -1
  249. package/src/render/surfaceModel.ts +47 -10
  250. package/src/render/surfaceOverlay.ts +195 -0
  251. package/src/render/surfaceTexture.ts +136 -9
  252. package/src/render/temporalAa.ts +20 -0
  253. package/src/render/vertexDefaults.ts +28 -0
@@ -1,4 +1,6 @@
1
- import { POINT_LIGHT_COS_INNER, POINT_LIGHT_COS_OUTER, lampSourceRadius, lampWeight, lightChannelsOf, } from './clusteredLights.js';
1
+ /** Fixed point-light budget shared by CPU selection and the flat shader. */
2
+ import { MAX_AREA_LIGHTS, packAreaSizesAndDoors } from './areaLights.js';
3
+ import { POINT_LIGHT_COS_INNER, POINT_LIGHT_COS_OUTER, lampSourceRadius, lampWeight, lightChannelsOf, lightFalloffExponentOf, } from './clusteredLights.js';
2
4
  /**
3
5
  * How many point lights the shading pass shades against at once.
4
6
  *
@@ -302,6 +304,8 @@ export const SDF_TEXT_TEXTURE_UNIT = SURFACE_TEXTURE_UNIT;
302
304
  export const POINT_SHADOW_POOL = MAX_POINT_LIGHTS + 4;
303
305
  /** Weights are optional per consumer; a short array means every light is fully present. */
304
306
  const FULLY_PRESENT = new Float32Array(MAX_POINT_LIGHTS).fill(1);
307
+ /** The fixed arm's rectangles as `uAreaLightSize` holds them; refilled per bind, allocated once. */
308
+ const AREA_SIZES_AND_DOORS = new Float32Array(MAX_AREA_LIGHTS * 4);
305
309
  /** No size at all, which is the behaviour every scene had before a light could have one. */
306
310
  const POINT_SOURCES = new Float32Array(MAX_POINT_LIGHTS);
307
311
  /**
@@ -394,7 +398,8 @@ export function bindPointLights(gl, uniforms, lights, falloff) {
394
398
  * every lamp switched off.
395
399
  */
396
400
  gl.uniform3fv(uniforms['uLightDir[0]'] ?? null, r.directions);
397
- gl.uniform2fv(uniforms['uLightCone[0]'] ?? null, r.coneCos);
401
+ /* The cone and, in z, the light's own falloff exponent. See `conesAndFalloffs`. */
402
+ gl.uniform4fv(uniforms['uLightCone[0]'] ?? null, r.conesAndFalloffs);
398
403
  gl.uniform1fv(uniforms['uLightIesProfile[0]'] ?? null, r.iesProfiles);
399
404
  /* The azimuth and, in w, the channels. See `axesAndChannels`. */
400
405
  gl.uniform4fv(uniforms['uLightIesAxis[0]'] ?? null, r.axesAndChannels);
@@ -421,7 +426,9 @@ export function bindAreaLights(gl, uniforms, lights) {
421
426
  gl.uniform3fv(uniforms['uAreaLightColor[0]'] ?? null, lights.colors);
422
427
  gl.uniform3fv(uniforms['uAreaLightRight[0]'] ?? null, lights.right);
423
428
  gl.uniform3fv(uniforms['uAreaLightUp[0]'] ?? null, lights.up);
424
- gl.uniform2fv(uniforms['uAreaLightSize[0]'] ?? null, lights.sizes);
429
+ /* The half extents and, in zw, the barn doors. See `packAreaSizesAndDoors`. */
430
+ packAreaSizesAndDoors(lights, AREA_SIZES_AND_DOORS);
431
+ gl.uniform4fv(uniforms['uAreaLightSize[0]'] ?? null, AREA_SIZES_AND_DOORS);
425
432
  gl.uniform1fv(uniforms['uAreaLightTwoSided[0]'] ?? null, lights.twoSided);
426
433
  }
427
434
  /**
@@ -463,6 +470,7 @@ const glLights = {
463
470
  iesAxes: NO_IES_AXES,
464
471
  cookies: NO_COOKIES,
465
472
  axesAndChannels: new Float32Array(MAX_POINT_LIGHTS * 4),
473
+ conesAndFalloffs: new Float32Array(MAX_POINT_LIGHTS * 4),
466
474
  heldSourceRadii: new Float32Array(MAX_POINT_LIGHTS),
467
475
  heldWeights: new Float32Array(MAX_POINT_LIGHTS),
468
476
  };
@@ -493,6 +501,7 @@ export function resolvePointLights(lights, falloff, out) {
493
501
  out.iesAxes = NO_IES_AXES;
494
502
  out.cookies = NO_COOKIES;
495
503
  packAxesAndChannels(NO_IES_AXES, undefined, out.axesAndChannels);
504
+ packConesAndFalloffs(OPEN_CONES, undefined, out.conesAndFalloffs);
496
505
  return out;
497
506
  }
498
507
  /*
@@ -574,8 +583,19 @@ export function resolvePointLights(lights, falloff, out) {
574
583
  out.iesAxes = iesAxes;
575
584
  out.cookies = cookies;
576
585
  packAxesAndChannels(iesAxes, lights.lightChannels, out.axesAndChannels);
586
+ packConesAndFalloffs(coneCos, lights.lightFalloffExponents, out.conesAndFalloffs);
577
587
  return out;
578
588
  }
589
+ /** The cone and the falloff exponent, four a light, as `uLightCone` holds them. */
590
+ function packConesAndFalloffs(cones, exponents, out) {
591
+ const named = (exponents?.length ?? 0) >= MAX_POINT_LIGHTS ? exponents : undefined;
592
+ for (let light = 0; light < MAX_POINT_LIGHTS; light++) {
593
+ out[light * 4] = cones[light * 2] ?? POINT_LIGHT_COS_INNER;
594
+ out[light * 4 + 1] = cones[light * 2 + 1] ?? POINT_LIGHT_COS_OUTER;
595
+ out[light * 4 + 2] = lightFalloffExponentOf(named?.[light]);
596
+ out[light * 4 + 3] = 0;
597
+ }
598
+ }
579
599
  /** The azimuth and the channels, four a light, as `uLightIesAxis` holds them. */
580
600
  function packAxesAndChannels(axes, channels, out) {
581
601
  const named = (channels?.length ?? 0) >= MAX_POINT_LIGHTS ? channels : undefined;
@@ -35,10 +35,12 @@ export interface LightmapPage {
35
35
  readonly width: number;
36
36
  readonly height: number;
37
37
  /**
38
- * Linear irradiance, three floats a texel, row-major from the top, in the same units every other
39
- * light here is in — a bake from another engine scaled by that engine's exposure, as its lights are.
38
+ * Linear irradiance, row-major from the top, in the same units every other light here is in — a
39
+ * bake from another engine scaled by that engine's exposure, as its lights are. Either three
40
+ * floats a texel, or one word a texel already packed as rgb9e5 (`rgb9e5.ts`), which is how such a
41
+ * bake usually arrives and which uploads with no conversion at all.
40
42
  */
41
- readonly irradiance: Float32Array;
43
+ readonly irradiance: Float32Array | Uint32Array;
42
44
  /**
43
45
  * The direction the light arrives from, four bytes a texel: a first-order spherical harmonic in
44
46
  * this engine's world axes, each value stored as `v * 0.5 + 0.5` — xyz the axis, w the constant.
@@ -54,20 +56,32 @@ export declare const LIGHTMAP_LAYERS = 2;
54
56
  export type LightmapRegion = readonly [number, number, number, number];
55
57
  /** The whole page. */
56
58
  export declare const WHOLE_PAGE: LightmapRegion;
57
- /** A page as both backends upload it: `layers` images of half-float RGBA, row-major from the top. */
59
+ /**
60
+ * A page as both backends upload it: two layers of four bytes a texel, row-major from the top. The
61
+ * first is the irradiance's rgb9e5 word, byte by byte from its lowest; the second is the direction.
62
+ *
63
+ * **Eight bytes a texel, and the shader filters it.** Half floats were sixteen: a stage of 49 pages
64
+ * and 21.8 million texels held 348 MB, where this holds 174. The price is that the irradiance cannot
65
+ * be filtered as it is stored — a bilinear blend of two words with different exponents is not the
66
+ * blend of the colours they hold — so the lit stage fetches a texel's four neighbours, decodes each
67
+ * and blends the colours (`modelBaked` in `shaders/flat/models.ts`): four fetches a layer where one
68
+ * filtered sample was, on lightmapped surfaces alone. **What would make it wrong** is every device
69
+ * a page reaches sampling `rgb9e5ufloat`, which WebGL2 does as `RGB9_E5` too; it would then be a
70
+ * texture of its own, filtered by the device, and a binding the lit stage has no room for.
71
+ */
58
72
  export interface LightmapTexels {
59
73
  readonly kind: 'lightmap-texels';
60
74
  readonly width: number;
61
75
  readonly height: number;
62
- readonly layers: number;
63
- readonly texels: Uint16Array;
76
+ /** Each layer's bytes, in layer order: the irradiance, then the direction. */
77
+ readonly layers: readonly Uint8Array[];
64
78
  }
65
79
  /** Whether a surface texture's source is a page's texels rather than an image or blocks. */
66
80
  export declare function isLightmapTexels(source: unknown): source is LightmapTexels;
67
81
  /**
68
- * A page as both backends upload it: two layers of half-float RGBA, the irradiance with an alpha of
69
- * one and the direction as the fractions its bytes are. Refuses a page whose arrays are not its
70
- * size, naming the one that is short.
82
+ * A page as both backends upload it (see `LightmapTexels`). Refuses a page whose arrays are not its
83
+ * size, naming the one that is short. Packed irradiance is read where it lies, with no copy; float
84
+ * irradiance is packed once, here.
71
85
  */
72
86
  export declare function lightmapTexels(page: LightmapPage): LightmapTexels;
73
87
  /**
@@ -1,4 +1,4 @@
1
- import { toHalfFloats } from './halfFloat.js';
1
+ import { packRgb9e5 } from './rgb9e5.js';
2
2
  /** Layers a page is uploaded as: the irradiance, then the direction. */
3
3
  export const LIGHTMAP_LAYERS = 2;
4
4
  /** The whole page. */
@@ -10,9 +10,9 @@ export function isLightmapTexels(source) {
10
10
  source.kind === 'lightmap-texels');
11
11
  }
12
12
  /**
13
- * A page as both backends upload it: two layers of half-float RGBA, the irradiance with an alpha of
14
- * one and the direction as the fractions its bytes are. Refuses a page whose arrays are not its
15
- * size, naming the one that is short.
13
+ * A page as both backends upload it (see `LightmapTexels`). Refuses a page whose arrays are not its
14
+ * size, naming the one that is short. Packed irradiance is read where it lies, with no copy; float
15
+ * irradiance is packed once, here.
16
16
  */
17
17
  export function lightmapTexels(page) {
18
18
  const { width, height } = page;
@@ -20,32 +20,54 @@ export function lightmapTexels(page) {
20
20
  throw new Error(`createLightmap: a ${width} by ${height} page has nothing in it`);
21
21
  }
22
22
  const texels = width * height;
23
- if (page.irradiance.length !== texels * 3) {
24
- throw new Error(`createLightmap: ${page.irradiance.length} irradiance floats for a ${width} by ${height} ` +
25
- `page, which wants ${texels * 3}: three a texel`);
23
+ const irradiance = page.irradiance;
24
+ const packed = irradiance instanceof Uint32Array;
25
+ const wanted = packed ? texels : texels * 3;
26
+ if (irradiance.length !== wanted) {
27
+ throw new Error(`createLightmap: ${irradiance.length} irradiance ${packed ? 'words' : 'floats'} for a ` +
28
+ `${width} by ${height} page, which wants ${wanted}: ` +
29
+ (packed ? 'one rgb9e5 word a texel' : 'three a texel'));
26
30
  }
27
31
  if (page.direction.length !== texels * 4) {
28
32
  throw new Error(`createLightmap: ${page.direction.length} direction bytes for a ${width} by ${height} ` +
29
33
  `page, which wants ${texels * 4}: four a texel`);
30
34
  }
31
- const floats = new Float32Array(texels * 4 * LIGHTMAP_LAYERS);
32
- for (let t = 0; t < texels; t++) {
33
- floats[t * 4] = page.irradiance[t * 3];
34
- floats[t * 4 + 1] = page.irradiance[t * 3 + 1];
35
- floats[t * 4 + 2] = page.irradiance[t * 3 + 2];
36
- floats[t * 4 + 3] = 1;
37
- const at = (texels + t) * 4;
38
- for (let c = 0; c < 4; c++)
39
- floats[at + c] = page.direction[t * 4 + c] / 255;
35
+ let words;
36
+ if (packed) {
37
+ words = irradiance;
38
+ }
39
+ else {
40
+ words = new Uint32Array(texels);
41
+ for (let t = 0; t < texels; t++) {
42
+ words[t] = packRgb9e5(irradiance[t * 3], irradiance[t * 3 + 1], irradiance[t * 3 + 2]);
43
+ }
40
44
  }
41
45
  return {
42
46
  kind: 'lightmap-texels',
43
47
  width,
44
48
  height,
45
- layers: LIGHTMAP_LAYERS,
46
- texels: toHalfFloats(floats),
49
+ layers: [littleEndianBytes(words), page.direction],
47
50
  };
48
51
  }
52
+ /** Whether this platform lays a word out lowest byte first, which the shader's decode assumes. */
53
+ const LITTLE_ENDIAN = new Uint8Array(new Uint32Array([1]).buffer)[0] === 1;
54
+ /**
55
+ * A page's words as bytes, lowest first: a view of the same memory on every little-endian platform,
56
+ * which is every one a browser runs on, and a reordered copy anywhere else.
57
+ */
58
+ function littleEndianBytes(words) {
59
+ const bytes = new Uint8Array(words.buffer, words.byteOffset, words.length * 4);
60
+ if (LITTLE_ENDIAN)
61
+ return bytes;
62
+ const swapped = new Uint8Array(bytes.length);
63
+ for (let i = 0; i < bytes.length; i += 4) {
64
+ swapped[i] = bytes[i + 3];
65
+ swapped[i + 1] = bytes[i + 2];
66
+ swapped[i + 2] = bytes[i + 1];
67
+ swapped[i + 3] = bytes[i];
68
+ }
69
+ return swapped;
70
+ }
49
71
  /**
50
72
  * A mesh with its second coordinates moved onto the grain and relief streams they ride, or the
51
73
  * mesh itself where it has none. Called by both backends' `createMesh`, before anything records
@@ -50,6 +50,27 @@ export interface ParticleInstances {
50
50
  count: number;
51
51
  capacity: number;
52
52
  }
53
+ /**
54
+ * Floats one particle takes in a buffer a caller's own compute writes (`DeviceParticles`), in this
55
+ * order: position 3, half-width 1, roll 1, colour 3, opacity 1, age 1, seed 1, velocity 3, a
56
+ * sprite's frame 1 and its half-height 1 — `ParticleInstances`' streams, interleaved as the pool's
57
+ * own upload interleaves them. 64 bytes a particle.
58
+ */
59
+ export declare const DEVICE_PARTICLE_FLOATS = 16;
60
+ /**
61
+ * Particles a caller's own compute shader wrote into a buffer of its own (`registerCompute`), drawn
62
+ * where they lie by `drawDeviceParticles` rather than read back and uploaded again. WebGPU only.
63
+ *
64
+ * **Not sorted**, as a pool's are where its batch asks: the device holds them, so an alpha-blended
65
+ * set that must be drawn far to near sorts itself. A slot with a half-width of 0 draws nothing, which
66
+ * is how a fixed-size buffer holds fewer live particles than it has room for.
67
+ */
68
+ export interface DeviceParticles {
69
+ /** A buffer with `VERTEX` usage, `DEVICE_PARTICLE_FLOATS` floats a particle. */
70
+ readonly buffer: GPUBuffer;
71
+ /** How many particles from its start to draw; no more than the buffer holds. */
72
+ readonly count: number;
73
+ }
53
74
  /**
54
75
  * A pool of short-lived particles, simulated on the CPU and drawn as instances.
55
76
  *
@@ -1,4 +1,11 @@
1
1
  import { createInstanceData, writeInstance } from './instancedMesh.js';
2
+ /**
3
+ * Floats one particle takes in a buffer a caller's own compute writes (`DeviceParticles`), in this
4
+ * order: position 3, half-width 1, roll 1, colour 3, opacity 1, age 1, seed 1, velocity 3, a
5
+ * sprite's frame 1 and its half-height 1 — `ParticleInstances`' streams, interleaved as the pool's
6
+ * own upload interleaves them. 64 bytes a particle.
7
+ */
8
+ export const DEVICE_PARTICLE_FLOATS = 16;
2
9
  function createParticleInstances(capacity) {
3
10
  return {
4
11
  positions: new Float32Array(capacity * 3),
@@ -117,6 +117,19 @@ export type PassContext = {
117
117
  } & ({
118
118
  readonly backend: 'webgl2';
119
119
  readonly gl: WebGL2RenderingContext;
120
+ /**
121
+ * The frame's colour with everything drawn before this pass, for a pass declaring
122
+ * `reads: ['colorSnapshot']`, and null for one that does not or where the frame keeps no
123
+ * scene target (`screenEffects` off). A copy, never the attachment being drawn into, which
124
+ * WebGL2 cannot sample; taken afresh at each such pass's draw. WebGPU hands the same copy
125
+ * over in `PrepareContext.sceneColor`, where a bind group has to be built before the pass.
126
+ */
127
+ readonly sceneColor: WebGLTexture | null;
128
+ /**
129
+ * The frame's depth as it stands, for a pass declaring `reads: ['depthSnapshot']`: a copy, at
130
+ * the buffer's own values and reversed as it is (1 near, 0 far). Null as `sceneColor` is.
131
+ */
132
+ readonly sceneDepth: WebGLTexture | null;
120
133
  } | {
121
134
  readonly backend: 'webgpu';
122
135
  readonly pass: GPURenderPassEncoder;
@@ -213,6 +226,42 @@ export type PrepareContext = {
213
226
  * should not apply it either; the renderer's own verbs do not jitter a mirror.
214
227
  */
215
228
  readonly jitter: Float32Array;
229
+ /**
230
+ * The frame's single-sample copy of its depth, or null where the frame keeps none (no
231
+ * composite: `screenEffects` off).
232
+ *
233
+ * One `r32float` texel a pixel holding the depth buffer's own value, reversed as the
234
+ * buffer is (`REVERSED_DEPTH`: 1 at the near plane, 0 at the far one). Unfilterable, so a
235
+ * pass binds it as `'unfilterable-float'` and reads it with a non-filtering sampler or
236
+ * `textureLoad`.
237
+ *
238
+ * **Handed over here, filled at the draw.** The view is the one this frame's copy will be
239
+ * written into, so a pass builds its bind groups with it before the frame's pass opens; the
240
+ * copy itself is taken when a pass that declares `reads: ['depthSnapshot']` is drawn, with
241
+ * everything drawn before it, which is what the soft edge of a translucent particle
242
+ * needs. A pass that does not declare that read finds whatever the frame last wrote there.
243
+ */
244
+ readonly sceneDepth: GPUTextureView | null;
245
+ /**
246
+ * The frame's colour, for a pass that declares `reads: ['colorSnapshot']`, or null where no
247
+ * registered pass does or the frame keeps no composite (`screenEffects` off).
248
+ *
249
+ * At the scene's own size and format — under a reconstruction, the render's rather than the
250
+ * output's, since that is the picture a registered pass draws into — and filterable.
251
+ * **Handed over here, refilled at the draw**: the view is the texture each reading pass's
252
+ * draw copies the frame into, with everything drawn before it, so a distortion samples the
253
+ * scene behind it and a full-screen effect drawn last reads the whole frame and writes over
254
+ * it, in scene light, before the tone curve. A copy a reading pass, where `sceneDepth` is one
255
+ * a frame.
256
+ *
257
+ * **Under a reconstruction a pass that reads it is drawn after the upscale**, over the
258
+ * reconstructed picture and after every blended draw, whatever order it was asked in — at the
259
+ * output's size and the reconstruction's format, which `PassContext.format` names at that
260
+ * draw, so a pass drawn there keeps a pipeline for it. The view is then the output-size copy.
261
+ * `sceneDepth` stays the render's opaque depth: at a ratio above 1 its texels are coarser by
262
+ * the ratio, and a blended draw is in neither depth, as it never is.
263
+ */
264
+ readonly sceneColor: GPUTextureView | null;
216
265
  };
217
266
  /**
218
267
  * The world's distance field on the device, as the two buffers and four numbers that address it.
@@ -69,6 +69,14 @@ export interface PointLightSource {
69
69
  * whose uniform budget has none to spare.
70
70
  */
71
71
  inLightField?: boolean;
72
+ /**
73
+ * How the light fades with distance when it names its own way: `(1 - (d/R)^2)^n` with no distance
74
+ * term, `R` its radius and `n` this. Unreal's falloff for a light without inverse-square falloff,
75
+ * so a rig authored there — a fill of brightness 160 across 164 m, a key at exponent 8 — lights
76
+ * the space it was authored for rather than one that ends a few metres out. Absent or 0, the
77
+ * frame's own falloff (`pointLightFalloff`), which is every light before 4.8.7.
78
+ */
79
+ falloffExponent?: number;
72
80
  }
73
81
  export interface PointLightBuffer {
74
82
  readonly positions: Float32Array;
@@ -126,6 +134,11 @@ export interface PointLightBuffer {
126
134
  readonly directions: Float32Array;
127
135
  /** Two per slot: the cosine of the inner cone angle, then of the outer. */
128
136
  readonly coneCos: Float32Array;
137
+ /**
138
+ * One per slot: the light's own falloff exponent, or 0 for the frame's falloff. See
139
+ * `PointLightSource.falloffExponent`.
140
+ */
141
+ readonly falloffExponents: Float32Array;
129
142
  /** One per slot: a row of the photometric atlas, or −1 for a light with no profile. */
130
143
  readonly iesProfiles: Float32Array;
131
144
  /** Internal insertion-sort storage; exposed only to keep one flat object. */
@@ -1,5 +1,5 @@
1
1
  /** Allocation-free selection and packing of point lights for a fixed shader budget. */
2
- import { POINT_LIGHT_COS_INNER, POINT_LIGHT_COS_OUTER } from './clusteredLights.js';
2
+ import { POINT_LIGHT_COS_INNER, POINT_LIGHT_COS_OUTER, lightFalloffExponentOf, } from './clusteredLights.js';
3
3
  import { MAX_POINT_LIGHTS, POINT_SHADOW_POOL } from './lightBudget.js';
4
4
  import { gatherLights } from './lightGrid.js';
5
5
  export const DEFAULT_POINT_LIGHT_VIEW_RANGE = 180;
@@ -150,6 +150,7 @@ export function createPointLightBuffer(capacity = MAX_POINT_LIGHTS) {
150
150
  weights: new Float32Array(shaded),
151
151
  directions: new Float32Array(shaded * 3),
152
152
  coneCos: new Float32Array(shaded * 2),
153
+ falloffExponents: new Float32Array(shaded),
153
154
  /* −1 rather than 0, because row 0 is a real fixture and a light that asked for none must not
154
155
  be shaped by whichever profile happened to load first. */
155
156
  iesProfiles: new Float32Array(shaded).fill(-1),
@@ -382,6 +383,7 @@ function choose(sources, visit, visitCount, x, y, z, out, timeSeconds, viewRange
382
383
  * Restricting it to spots would be a smaller feature for no saving.
383
384
  */
384
385
  out.iesProfiles[slot] = light.iesProfile ?? -1;
386
+ out.falloffExponents[slot] = lightFalloffExponentOf(light.falloffExponent);
385
387
  }
386
388
  out.count = chosen;
387
389
  /*
@@ -0,0 +1,28 @@
1
+ export declare class ReflectionSurfaceTargets {
2
+ private readonly gl;
3
+ private probe;
4
+ private tint;
5
+ private width;
6
+ private height;
7
+ private readonly all;
8
+ private readonly colourAlone;
9
+ constructor(gl: WebGL2RenderingContext);
10
+ /** The environment as the frame shows it, and the roughness in alpha. */
11
+ get probeTexture(): WebGLTexture | null;
12
+ /** What a found reflection is multiplied by. */
13
+ get tintTexture(): WebGLTexture | null;
14
+ /**
15
+ * At the frame's start, with the scene's framebuffer bound: both at its size, attached, cleared to
16
+ * nothing reflected, and left disabled.
17
+ */
18
+ begin(width: number, height: number): void;
19
+ /** Around an opaque lit draw into the frame: its two extra outputs land. */
20
+ on(): void;
21
+ /** And after it: only the colour does. */
22
+ off(): void;
23
+ /** With the scene's framebuffer bound, after the trace has read them: see the header. */
24
+ detach(): void;
25
+ dispose(): void;
26
+ private make;
27
+ private release;
28
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * The frame's materials' reflection maps on WebGL2: two half-float textures on the scene's own
3
+ * framebuffer, beside its colour, written by the opaque lit draws as they are drawn. What the two
4
+ * hold is `shaders/flat/reflectionSurface.ts`'s to say; this is where they live on this backend.
5
+ *
6
+ * **Written in the frame's own pass, where WebGPU replays the draws in a second one.** A WebGL2
7
+ * program writing an output its framebuffer has no buffer enabled for loses that output and nothing
8
+ * else, so the maps are enabled around an opaque lit draw (`on`, `off`) and stay disabled for every
9
+ * other: the sky, glass, text and particles write no material, and a draw into a mirror, a probe or
10
+ * a capture is into another framebuffer, whose draw buffers this never touches.
11
+ *
12
+ * **Detached once the trace has read them** (`detach`), because the resolve then draws into the
13
+ * scene's framebuffer while sampling both, and a texture sampled while attached to the framebuffer
14
+ * being drawn is a feedback loop whether or not its buffer is enabled. `begin` attaches them again.
15
+ */
16
+ const ZERO = new Float32Array(4);
17
+ export class ReflectionSurfaceTargets {
18
+ gl;
19
+ probe = null;
20
+ tint = null;
21
+ width = 0;
22
+ height = 0;
23
+ all;
24
+ colourAlone;
25
+ constructor(gl) {
26
+ this.gl = gl;
27
+ this.all = [gl.COLOR_ATTACHMENT0, gl.COLOR_ATTACHMENT1, gl.COLOR_ATTACHMENT2];
28
+ this.colourAlone = [gl.COLOR_ATTACHMENT0];
29
+ }
30
+ /** The environment as the frame shows it, and the roughness in alpha. */
31
+ get probeTexture() {
32
+ return this.probe;
33
+ }
34
+ /** What a found reflection is multiplied by. */
35
+ get tintTexture() {
36
+ return this.tint;
37
+ }
38
+ /**
39
+ * At the frame's start, with the scene's framebuffer bound: both at its size, attached, cleared to
40
+ * nothing reflected, and left disabled.
41
+ */
42
+ begin(width, height) {
43
+ const { gl } = this;
44
+ if (this.probe === null || this.width !== width || this.height !== height) {
45
+ this.release();
46
+ this.probe = this.make(width, height);
47
+ this.tint = this.make(width, height);
48
+ this.width = width;
49
+ this.height = height;
50
+ }
51
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT1, gl.TEXTURE_2D, this.probe, 0);
52
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT2, gl.TEXTURE_2D, this.tint, 0);
53
+ gl.drawBuffers(this.all);
54
+ gl.clearBufferfv(gl.COLOR, 1, ZERO);
55
+ gl.clearBufferfv(gl.COLOR, 2, ZERO);
56
+ gl.drawBuffers(this.colourAlone);
57
+ }
58
+ /** Around an opaque lit draw into the frame: its two extra outputs land. */
59
+ on() {
60
+ this.gl.drawBuffers(this.all);
61
+ }
62
+ /** And after it: only the colour does. */
63
+ off() {
64
+ this.gl.drawBuffers(this.colourAlone);
65
+ }
66
+ /** With the scene's framebuffer bound, after the trace has read them: see the header. */
67
+ detach() {
68
+ const { gl } = this;
69
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT1, gl.TEXTURE_2D, null, 0);
70
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT2, gl.TEXTURE_2D, null, 0);
71
+ }
72
+ dispose() {
73
+ this.release();
74
+ }
75
+ make(width, height) {
76
+ const { gl } = this;
77
+ const texture = gl.createTexture();
78
+ if (texture === null)
79
+ throw new Error('ReflectionSurfaceTargets: createTexture failed');
80
+ /* Unit 0, chosen rather than inherited, for the reason `SsrPass` gives. */
81
+ gl.activeTexture(gl.TEXTURE0);
82
+ gl.bindTexture(gl.TEXTURE_2D, texture);
83
+ gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGBA16F, width, height);
84
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
85
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
86
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
87
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
88
+ gl.bindTexture(gl.TEXTURE_2D, null);
89
+ return texture;
90
+ }
91
+ release() {
92
+ if (this.probe !== null)
93
+ this.gl.deleteTexture(this.probe);
94
+ if (this.tint !== null)
95
+ this.gl.deleteTexture(this.tint);
96
+ this.probe = null;
97
+ this.tint = null;
98
+ }
99
+ }
@@ -1,4 +1,5 @@
1
1
  /** Construction-time GPU quality controls shared by every renderer resource. */
2
+ import type { FrameReflections } from './frameReflections.ts';
2
3
  /**
3
4
  * What happens to a shaded colour on its way to the framebuffer.
4
5
  *
@@ -20,8 +21,14 @@
20
21
  * red where `srgb` clips each channel and turns its core pink and wide. It is the curve a ported
21
22
  * game's own resolve applied — for a port that has to match one — and it has none of ACES's toe or
22
23
  * desaturation. See `HIGHLIGHT_SHOULDER_GLSL` for the arithmetic.
24
+ *
25
+ * `filmic` is a film curve shaped by five numbers — a slope, a toe, a shoulder and how far black
26
+ * and white clip — that holds a mid grey of 0.18 at 0.18 where `aces` gives about 0.11, so a look
27
+ * graded against such a curve is matched by its numbers (`setFilmicCurve`) rather than by a lookup
28
+ * table. Applied by the composite alone, so it needs `screenEffects` and `hdrScene`; without them
29
+ * every pass grades with `aces` and the renderer says so once. See `filmicCurve.ts`.
23
30
  */
24
- export type OutputTransform = 'none' | 'srgb' | 'aces' | 'shoulder';
31
+ export type OutputTransform = 'none' | 'srgb' | 'aces' | 'shoulder' | 'filmic';
25
32
  /**
26
33
  * The three settings whose accepted values are a closed set, written as one.
27
34
  *
@@ -209,13 +216,14 @@ export interface RenderQuality {
209
216
  * **`'pre-integrated'`, the default**: the lit stage's fit alone — a terminator softened and
210
217
  * reddened by the surface's curvature, light through thin parts, a penumbra each channel crosses
211
218
  * as far as it travels — at no cost beyond the skin pipeline itself. **`'screen-space'`** adds
212
- * Burley's blur in the picture as well: a skin draw writes its diffuse to a target of its own (it
213
- * is drawn twice to do so), which is spread across the pixels its scatter distance covers and
214
- * added back before anything blended is drawn, so light crosses a shadow's edge and a nostril's
215
- * rim in the picture rather than in a fit. See `skinBlur.ts`.
216
- *
217
- * Construction-time: two half-float targets the size of the frame, and a multisampled one under
218
- * multisampling, made at the first skin draw. It needs the composite (`screenEffects`), which is
219
+ * Burley's blur in the picture as well: a skin draw writes the light its diffuse is made of to a
220
+ * target of its own and its colour to another (it is drawn three times to do so), and the light is
221
+ * spread across the pixels its scatter distance covers, multiplied by the colour and added back
222
+ * before anything blended is drawn, so light crosses a shadow's edge and a nostril's rim in the
223
+ * picture rather than in a fit. See `skinBlur.ts`.
224
+ *
225
+ * Construction-time: three half-float targets the size of the frame, and two multisampled ones
226
+ * under multisampling, made at the first skin draw. It needs the composite (`screenEffects`), which is
219
227
  * where the frame is still linear light to add to; without one, skin is pre-integrated and the
220
228
  * renderer says so once. What would make the default wrong is a close-up face that reads as wax
221
229
  * at a shadow's edge — that is what the blur is for.
@@ -344,6 +352,18 @@ export interface RenderQuality {
344
352
  * eight-bit and this buys only the consistent grading.
345
353
  */
346
354
  readonly hdrScene: boolean;
355
+ /**
356
+ * Ask for a high dynamic range display: a canvas whose highlights run past paper white to as
357
+ * far as the display allows, where it reports one. Off by default.
358
+ *
359
+ * **Asked for, not assumed.** It needs WebGPU, a display reporting a high dynamic range, and a
360
+ * composite that holds the light to give it (`screenEffects` and `hdrScene`); where any of those
361
+ * is missing the frame goes out as it always did and the renderer says which in
362
+ * `displayRangeReason`. `displayRange` is the answer a game reads to choose a look graded for the
363
+ * output it got. `setDisplayLuminance` sets how far above paper white the highlights may reach.
364
+ * WebGL2 has no extended-range canvas to draw into, so there it is always the standard range.
365
+ */
366
+ readonly highDynamicRange: boolean;
347
367
  /** Camera-depth water tint and scattering across every visible pass. */
348
368
  readonly underwaterAtmosphere: boolean;
349
369
  /** Procedural noise octaves evaluated by plume shaders. */
@@ -511,14 +531,14 @@ export interface RenderQuality {
511
531
  */
512
532
  readonly sceneSamples: number;
513
533
  /**
514
- * Camera motion blur, 0 to 1. Off by default.
534
+ * Motion blur, 0 to 1. Off by default.
515
535
  *
516
- * Camera rather than per-object, and that is a decision rather than a stage. Per-object
517
- * blur needs a velocity buffer: every mesh writing its own screen-space motion into a
518
- * second render target, so a second set of matrices per draw and a wider G-buffer, which
519
- * is a structural change to a forward renderer that writes one colour target. Camera blur
520
- * needs only the previous view-projection, which is one matrix, and it covers the case
521
- * that actually matters — a camera whipping round a turntable or through a world.
536
+ * By the camera's motion everywhere, and **by each drawn object's own motion where a
537
+ * reconstruction draws one** (DriftTR, WebGPU): a mover's pixels smear along its path while a
538
+ * still stage stays sharp. Elsewhere a velocity buffer would be every mesh writing its motion into
539
+ * a second target — a structural change to a forward renderer — so those frames blur by the
540
+ * camera alone, which needs only the previous view-projection, and say so once when a draw names
541
+ * where it was. `setCameraMotionBlur` is the per-frame dial and the longest smear.
522
542
  *
523
543
  * Runs inside the composite pass, so it costs a depth sample and eight taps on the pixels
524
544
  * that are moving and nothing on a still frame. Needs `screenEffects`, since without the
@@ -569,6 +589,18 @@ export interface RenderQuality {
569
589
  * Needs `screenEffects`, since the resolve composites over the finished scene.
570
590
  */
571
591
  readonly orderIndependent: boolean;
592
+ /**
593
+ * Every opaque lit surface reflects the frame by its own material: a ray marched against the
594
+ * frame's depth swaps the probes' share of each pixel for what it finds, at the pixel's own
595
+ * roughness and reflectance, and a surface past `maxRoughness` keeps the probes alone. Off by
596
+ * default; `true` for the defaults, or the numbers. See `frameReflections.ts`.
597
+ *
598
+ * Needs `screenEffects` and `hdrScene`, since the swap is made in scene light, and a frame of one
599
+ * sample; it says once where it cannot be drawn. What it costs on WebGPU is every opaque lit draw
600
+ * a second time without its lamps, into two half-float targets, and a march per reflective pixel;
601
+ * on WebGL2 the same two targets, written beside the colour by the opaque lit draws themselves.
602
+ */
603
+ readonly screenSpaceReflections: boolean | FrameReflections;
572
604
  /**
573
605
  * Depth of field: how wide the blur may reach, as a fraction of the frame's height. Off by
574
606
  * default.
@@ -85,9 +85,11 @@ export const DEFAULT_RENDER_QUALITY = Object.freeze({
85
85
  sceneSamples: 1,
86
86
  planarReflections: false,
87
87
  hdrScene: false,
88
+ highDynamicRange: false,
88
89
  cameraMotionBlur: 0,
89
90
  temporalAa: false,
90
91
  orderIndependent: false,
92
+ screenSpaceReflections: false,
91
93
  depthOfField: 0,
92
94
  reconstruction: 0,
93
95
  indirectLight: false,
@@ -227,9 +229,11 @@ export function resolveRenderQuality(options = {}) {
227
229
  sceneSamples: Math.max(1, Math.round(options.sceneSamples ?? DEFAULT_RENDER_QUALITY.sceneSamples)),
228
230
  planarReflections: options.planarReflections ?? DEFAULT_RENDER_QUALITY.planarReflections,
229
231
  hdrScene: options.hdrScene ?? DEFAULT_RENDER_QUALITY.hdrScene,
232
+ highDynamicRange: options.highDynamicRange ?? DEFAULT_RENDER_QUALITY.highDynamicRange,
230
233
  cameraMotionBlur: Math.min(1, Math.max(0, options.cameraMotionBlur ?? DEFAULT_RENDER_QUALITY.cameraMotionBlur)),
231
234
  temporalAa: options.temporalAa ?? DEFAULT_RENDER_QUALITY.temporalAa,
232
235
  orderIndependent: options.orderIndependent ?? DEFAULT_RENDER_QUALITY.orderIndependent,
236
+ screenSpaceReflections: options.screenSpaceReflections ?? DEFAULT_RENDER_QUALITY.screenSpaceReflections,
233
237
  /* Off, or a ratio at least 1.3 and at most 2 — never a number between zero and the range. */
234
238
  reconstruction: resolveReconstruction(options.reconstruction),
235
239
  indirectLight: options.indirectLight ?? DEFAULT_RENDER_QUALITY.indirectLight,
@@ -76,6 +76,9 @@ export function recordingGl(options = {}) {
76
76
  ONE: 1,
77
77
  ONE_MINUS_SRC_ALPHA: 0x0303,
78
78
  TRIANGLES: 0x0004,
79
+ /* The winding a pass names, so a test can tell which one it chose. */
80
+ CW: 0x0900,
81
+ CCW: 0x0901,
79
82
  TEXTURE0: 0x84c0,
80
83
  DEPTH_BUFFER_BIT: 0x00000100,
81
84
  DEPTH_TEST: 0x0b71,