@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
@@ -0,0 +1,105 @@
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
+
18
+ export class ReflectionSurfaceTargets {
19
+ private probe: WebGLTexture | null = null;
20
+ private tint: WebGLTexture | null = null;
21
+ private width = 0;
22
+ private height = 0;
23
+ private readonly all: number[];
24
+ private readonly colourAlone: number[];
25
+
26
+ constructor(private readonly gl: WebGL2RenderingContext) {
27
+ this.all = [gl.COLOR_ATTACHMENT0, gl.COLOR_ATTACHMENT1, gl.COLOR_ATTACHMENT2];
28
+ this.colourAlone = [gl.COLOR_ATTACHMENT0];
29
+ }
30
+
31
+ /** The environment as the frame shows it, and the roughness in alpha. */
32
+ get probeTexture(): WebGLTexture | null {
33
+ return this.probe;
34
+ }
35
+
36
+ /** What a found reflection is multiplied by. */
37
+ get tintTexture(): WebGLTexture | null {
38
+ return this.tint;
39
+ }
40
+
41
+ /**
42
+ * At the frame's start, with the scene's framebuffer bound: both at its size, attached, cleared to
43
+ * nothing reflected, and left disabled.
44
+ */
45
+ begin(width: number, height: number): void {
46
+ const { gl } = this;
47
+ if (this.probe === null || this.width !== width || this.height !== height) {
48
+ this.release();
49
+ this.probe = this.make(width, height);
50
+ this.tint = this.make(width, height);
51
+ this.width = width;
52
+ this.height = height;
53
+ }
54
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT1, gl.TEXTURE_2D, this.probe, 0);
55
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT2, gl.TEXTURE_2D, this.tint, 0);
56
+ gl.drawBuffers(this.all);
57
+ gl.clearBufferfv(gl.COLOR, 1, ZERO);
58
+ gl.clearBufferfv(gl.COLOR, 2, ZERO);
59
+ gl.drawBuffers(this.colourAlone);
60
+ }
61
+
62
+ /** Around an opaque lit draw into the frame: its two extra outputs land. */
63
+ on(): void {
64
+ this.gl.drawBuffers(this.all);
65
+ }
66
+
67
+ /** And after it: only the colour does. */
68
+ off(): void {
69
+ this.gl.drawBuffers(this.colourAlone);
70
+ }
71
+
72
+ /** With the scene's framebuffer bound, after the trace has read them: see the header. */
73
+ detach(): void {
74
+ const { gl } = this;
75
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT1, gl.TEXTURE_2D, null, 0);
76
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT2, gl.TEXTURE_2D, null, 0);
77
+ }
78
+
79
+ dispose(): void {
80
+ this.release();
81
+ }
82
+
83
+ private make(width: number, height: number): WebGLTexture {
84
+ const { gl } = this;
85
+ const texture = gl.createTexture();
86
+ if (texture === null) throw new Error('ReflectionSurfaceTargets: createTexture failed');
87
+ /* Unit 0, chosen rather than inherited, for the reason `SsrPass` gives. */
88
+ gl.activeTexture(gl.TEXTURE0);
89
+ gl.bindTexture(gl.TEXTURE_2D, texture);
90
+ gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGBA16F, width, height);
91
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
92
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
93
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
94
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
95
+ gl.bindTexture(gl.TEXTURE_2D, null);
96
+ return texture;
97
+ }
98
+
99
+ private release(): void {
100
+ if (this.probe !== null) this.gl.deleteTexture(this.probe);
101
+ if (this.tint !== null) this.gl.deleteTexture(this.tint);
102
+ this.probe = null;
103
+ this.tint = null;
104
+ }
105
+ }
@@ -1,5 +1,6 @@
1
1
  /** Construction-time GPU quality controls shared by every renderer resource. */
2
2
 
3
+ import type { FrameReflections } from './frameReflections.ts';
3
4
  import { MAX_AREA_LIGHTS } from './areaLights.ts';
4
5
  import { MAX_GLOBAL_MEDIUM_STEPS } from './globalMedium.ts';
5
6
  import { MAX_POINT_LIGHTS } from './lightBudget.ts';
@@ -26,8 +27,14 @@ import { PREFILTER_SAMPLE_COUNTS } from './prefilterEnvMap.ts';
26
27
  * red where `srgb` clips each channel and turns its core pink and wide. It is the curve a ported
27
28
  * game's own resolve applied — for a port that has to match one — and it has none of ACES's toe or
28
29
  * desaturation. See `HIGHLIGHT_SHOULDER_GLSL` for the arithmetic.
30
+ *
31
+ * `filmic` is a film curve shaped by five numbers — a slope, a toe, a shoulder and how far black
32
+ * and white clip — that holds a mid grey of 0.18 at 0.18 where `aces` gives about 0.11, so a look
33
+ * graded against such a curve is matched by its numbers (`setFilmicCurve`) rather than by a lookup
34
+ * table. Applied by the composite alone, so it needs `screenEffects` and `hdrScene`; without them
35
+ * every pass grades with `aces` and the renderer says so once. See `filmicCurve.ts`.
29
36
  */
30
- export type OutputTransform = 'none' | 'srgb' | 'aces' | 'shoulder';
37
+ export type OutputTransform = 'none' | 'srgb' | 'aces' | 'shoulder' | 'filmic';
31
38
 
32
39
  /**
33
40
  * The three settings whose accepted values are a closed set, written as one.
@@ -219,13 +226,14 @@ export interface RenderQuality {
219
226
  * **`'pre-integrated'`, the default**: the lit stage's fit alone — a terminator softened and
220
227
  * reddened by the surface's curvature, light through thin parts, a penumbra each channel crosses
221
228
  * as far as it travels — at no cost beyond the skin pipeline itself. **`'screen-space'`** adds
222
- * Burley's blur in the picture as well: a skin draw writes its diffuse to a target of its own (it
223
- * is drawn twice to do so), which is spread across the pixels its scatter distance covers and
224
- * added back before anything blended is drawn, so light crosses a shadow's edge and a nostril's
225
- * rim in the picture rather than in a fit. See `skinBlur.ts`.
226
- *
227
- * Construction-time: two half-float targets the size of the frame, and a multisampled one under
228
- * multisampling, made at the first skin draw. It needs the composite (`screenEffects`), which is
229
+ * Burley's blur in the picture as well: a skin draw writes the light its diffuse is made of to a
230
+ * target of its own and its colour to another (it is drawn three times to do so), and the light is
231
+ * spread across the pixels its scatter distance covers, multiplied by the colour and added back
232
+ * before anything blended is drawn, so light crosses a shadow's edge and a nostril's rim in the
233
+ * picture rather than in a fit. See `skinBlur.ts`.
234
+ *
235
+ * Construction-time: three half-float targets the size of the frame, and two multisampled ones
236
+ * under multisampling, made at the first skin draw. It needs the composite (`screenEffects`), which is
229
237
  * where the frame is still linear light to add to; without one, skin is pre-integrated and the
230
238
  * renderer says so once. What would make the default wrong is a close-up face that reads as wax
231
239
  * at a shadow's edge — that is what the blur is for.
@@ -354,6 +362,18 @@ export interface RenderQuality {
354
362
  * eight-bit and this buys only the consistent grading.
355
363
  */
356
364
  readonly hdrScene: boolean;
365
+ /**
366
+ * Ask for a high dynamic range display: a canvas whose highlights run past paper white to as
367
+ * far as the display allows, where it reports one. Off by default.
368
+ *
369
+ * **Asked for, not assumed.** It needs WebGPU, a display reporting a high dynamic range, and a
370
+ * composite that holds the light to give it (`screenEffects` and `hdrScene`); where any of those
371
+ * is missing the frame goes out as it always did and the renderer says which in
372
+ * `displayRangeReason`. `displayRange` is the answer a game reads to choose a look graded for the
373
+ * output it got. `setDisplayLuminance` sets how far above paper white the highlights may reach.
374
+ * WebGL2 has no extended-range canvas to draw into, so there it is always the standard range.
375
+ */
376
+ readonly highDynamicRange: boolean;
357
377
  /** Camera-depth water tint and scattering across every visible pass. */
358
378
  readonly underwaterAtmosphere: boolean;
359
379
  /** Procedural noise octaves evaluated by plume shaders. */
@@ -521,14 +541,14 @@ export interface RenderQuality {
521
541
  */
522
542
  readonly sceneSamples: number;
523
543
  /**
524
- * Camera motion blur, 0 to 1. Off by default.
544
+ * Motion blur, 0 to 1. Off by default.
525
545
  *
526
- * Camera rather than per-object, and that is a decision rather than a stage. Per-object
527
- * blur needs a velocity buffer: every mesh writing its own screen-space motion into a
528
- * second render target, so a second set of matrices per draw and a wider G-buffer, which
529
- * is a structural change to a forward renderer that writes one colour target. Camera blur
530
- * needs only the previous view-projection, which is one matrix, and it covers the case
531
- * that actually matters — a camera whipping round a turntable or through a world.
546
+ * By the camera's motion everywhere, and **by each drawn object's own motion where a
547
+ * reconstruction draws one** (DriftTR, WebGPU): a mover's pixels smear along its path while a
548
+ * still stage stays sharp. Elsewhere a velocity buffer would be every mesh writing its motion into
549
+ * a second target — a structural change to a forward renderer — so those frames blur by the
550
+ * camera alone, which needs only the previous view-projection, and say so once when a draw names
551
+ * where it was. `setCameraMotionBlur` is the per-frame dial and the longest smear.
532
552
  *
533
553
  * Runs inside the composite pass, so it costs a depth sample and eight taps on the pixels
534
554
  * that are moving and nothing on a still frame. Needs `screenEffects`, since without the
@@ -579,6 +599,18 @@ export interface RenderQuality {
579
599
  * Needs `screenEffects`, since the resolve composites over the finished scene.
580
600
  */
581
601
  readonly orderIndependent: boolean;
602
+ /**
603
+ * Every opaque lit surface reflects the frame by its own material: a ray marched against the
604
+ * frame's depth swaps the probes' share of each pixel for what it finds, at the pixel's own
605
+ * roughness and reflectance, and a surface past `maxRoughness` keeps the probes alone. Off by
606
+ * default; `true` for the defaults, or the numbers. See `frameReflections.ts`.
607
+ *
608
+ * Needs `screenEffects` and `hdrScene`, since the swap is made in scene light, and a frame of one
609
+ * sample; it says once where it cannot be drawn. What it costs on WebGPU is every opaque lit draw
610
+ * a second time without its lamps, into two half-float targets, and a march per reflective pixel;
611
+ * on WebGL2 the same two targets, written beside the colour by the opaque lit draws themselves.
612
+ */
613
+ readonly screenSpaceReflections: boolean | FrameReflections;
582
614
  /**
583
615
  * Depth of field: how wide the blur may reach, as a fraction of the frame's height. Off by
584
616
  * default.
@@ -963,9 +995,11 @@ export const DEFAULT_RENDER_QUALITY: Readonly<RenderQuality> = Object.freeze({
963
995
  sceneSamples: 1,
964
996
  planarReflections: false,
965
997
  hdrScene: false,
998
+ highDynamicRange: false,
966
999
  cameraMotionBlur: 0,
967
1000
  temporalAa: false,
968
1001
  orderIndependent: false,
1002
+ screenSpaceReflections: false,
969
1003
  depthOfField: 0,
970
1004
  reconstruction: 0,
971
1005
  indirectLight: false,
@@ -1125,12 +1159,15 @@ export function resolveRenderQuality(options: RenderQualityOptions = {}): Readon
1125
1159
  ),
1126
1160
  planarReflections: options.planarReflections ?? DEFAULT_RENDER_QUALITY.planarReflections,
1127
1161
  hdrScene: options.hdrScene ?? DEFAULT_RENDER_QUALITY.hdrScene,
1162
+ highDynamicRange: options.highDynamicRange ?? DEFAULT_RENDER_QUALITY.highDynamicRange,
1128
1163
  cameraMotionBlur: Math.min(
1129
1164
  1,
1130
1165
  Math.max(0, options.cameraMotionBlur ?? DEFAULT_RENDER_QUALITY.cameraMotionBlur),
1131
1166
  ),
1132
1167
  temporalAa: options.temporalAa ?? DEFAULT_RENDER_QUALITY.temporalAa,
1133
1168
  orderIndependent: options.orderIndependent ?? DEFAULT_RENDER_QUALITY.orderIndependent,
1169
+ screenSpaceReflections:
1170
+ options.screenSpaceReflections ?? DEFAULT_RENDER_QUALITY.screenSpaceReflections,
1134
1171
  /* Off, or a ratio at least 1.3 and at most 2 — never a number between zero and the range. */
1135
1172
  reconstruction: resolveReconstruction(options.reconstruction),
1136
1173
  indirectLight: options.indirectLight ?? DEFAULT_RENDER_QUALITY.indirectLight,
@@ -119,6 +119,9 @@ export function recordingGl(
119
119
  ONE: 1,
120
120
  ONE_MINUS_SRC_ALPHA: 0x0303,
121
121
  TRIANGLES: 0x0004,
122
+ /* The winding a pass names, so a test can tell which one it chose. */
123
+ CW: 0x0900,
124
+ CCW: 0x0901,
122
125
  TEXTURE0: 0x84c0,
123
126
  DEPTH_BUFFER_BIT: 0x00000100,
124
127
  DEPTH_TEST: 0x0b71,
@@ -0,0 +1,52 @@
1
+ /**
2
+ * rgb9e5: three 9-bit mantissas over one shared 5-bit exponent, an unsigned float colour in four
3
+ * bytes. `EXT_texture_shared_exponent`'s encoding, the one a GPU's `rgb9e5ufloat` reads, written
4
+ * out so a lightmap page can hold its irradiance in four bytes a texel against a half float's eight.
5
+ *
6
+ * **What it gives up**: precision in the smaller channels, which share the largest one's exponent —
7
+ * a channel a thousandth of its neighbour keeps about one bit — and anything negative or past
8
+ * 65,408, which it clamps. Light is neither, and a bake's colours rarely span more than a few
9
+ * octaves within one texel, so a lightmap loses nothing visible; a value that needs each channel
10
+ * exact wants a format of its own.
11
+ *
12
+ * The bits, low to high: red in 0–8, green in 9–17, blue in 18–26, the exponent in 27–31, biased by
13
+ * 15, and a value is `mantissa * 2^(exponent - 15 - 9)`.
14
+ */
15
+
16
+ const BIAS = 15;
17
+ const MANTISSA_BITS = 9;
18
+ const MANTISSA_VALUES = 1 << MANTISSA_BITS;
19
+ const MAX_EXPONENT = 31;
20
+
21
+ /** The largest value the format holds: 511/512 of 2^16. */
22
+ export const RGB9E5_MAX = ((MANTISSA_VALUES - 1) / MANTISSA_VALUES) * 2 ** (MAX_EXPONENT - BIAS);
23
+
24
+ const clampChannel = (v: number): number => (v > 0 ? Math.min(v, RGB9E5_MAX) : 0);
25
+
26
+ /** One colour packed into its 32 bits, rounding to nearest; NaN and negatives are zero. */
27
+ export function packRgb9e5(red: number, green: number, blue: number): number {
28
+ const r = clampChannel(red);
29
+ const g = clampChannel(green);
30
+ const b = clampChannel(blue);
31
+ const largest = Math.max(r, g, b);
32
+ let exponent = Math.max(-BIAS - 1, Math.floor(Math.log2(largest))) + 1 + BIAS;
33
+ let scale = 2 ** (exponent - BIAS - MANTISSA_BITS);
34
+ /* Rounding the largest channel up to 512 needs one more exponent, which the spec allows for. */
35
+ if (Math.floor(largest / scale + 0.5) === MANTISSA_VALUES) {
36
+ exponent += 1;
37
+ scale *= 2;
38
+ }
39
+ /* Inline rather than a helper: a page packs millions of texels, and a closure a call is one each. */
40
+ const mr = Math.min(MANTISSA_VALUES - 1, Math.floor(r / scale + 0.5));
41
+ const mg = Math.min(MANTISSA_VALUES - 1, Math.floor(g / scale + 0.5));
42
+ const mb = Math.min(MANTISSA_VALUES - 1, Math.floor(b / scale + 0.5));
43
+ return (mr | (mg << 9) | (mb << 18) | (exponent << 27)) >>> 0;
44
+ }
45
+
46
+ /** A packed colour's three channels, into `out[at…at+2]`. */
47
+ export function unpackRgb9e5(bits: number, out: { [index: number]: number }, at = 0): void {
48
+ const scale = 2 ** ((bits >>> 27) - BIAS - MANTISSA_BITS);
49
+ out[at] = (bits & 511) * scale;
50
+ out[at + 1] = ((bits >>> 9) & 511) * scale;
51
+ out[at + 2] = ((bits >>> 18) & 511) * scale;
52
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * A scene capture: a texture the world is drawn into from a camera of the caller's, which a material
3
+ * then shows like any image — a screen in the world showing what a camera elsewhere sees, a monitor,
4
+ * a security feed. The planar reflection's pass, for a caller's camera rather than a mirrored one.
5
+ *
6
+ * **It holds radiance, not display pixels**, as a probe does: drawn with the output transform held
7
+ * off, at the colour format a probe takes — half floats under `hdrScene`, bytes otherwise — and
8
+ * sampled linear, so a material showing it as its `emissive` emits the light the capture saw and the
9
+ * frame's grade then applies once, to the screen as to everything around it.
10
+ *
11
+ * **What it gives up**: it is the mesh pass alone, as a probe bake is — the sky, water, particles and
12
+ * anything else a scene draws after its world are in the capture only if the callback draws them;
13
+ * no temporal resolve, bloom or other effect after the world runs on it; and it has no mip chain, so
14
+ * a screen seen small and far shimmers where a mipmapped image would not.
15
+ */
16
+
17
+ /** What a capture's texture is made from, in place of an image: its size and colour format. */
18
+ export interface SceneCaptureTexels {
19
+ readonly kind: 'scene-capture';
20
+ readonly width: number;
21
+ readonly height: number;
22
+ /** Half floats, which hold light past one; bytes otherwise. */
23
+ readonly float: boolean;
24
+ }
25
+
26
+ /** The largest side a capture may have, the size every WebGL2 device must take. */
27
+ export const MAX_SCENE_CAPTURE_SIZE = 4096;
28
+
29
+ /** A capture's description, refused by name where a side is not a whole number of pixels it can be. */
30
+ export function sceneCaptureTexels(
31
+ width: number,
32
+ height: number,
33
+ float: boolean,
34
+ ): SceneCaptureTexels {
35
+ for (const [name, side] of [
36
+ ['width', width],
37
+ ['height', height],
38
+ ] as const) {
39
+ if (!Number.isInteger(side) || side < 1 || side > MAX_SCENE_CAPTURE_SIZE) {
40
+ throw new Error(
41
+ `createSceneCapture: ${name} must be a whole number from 1 to ${MAX_SCENE_CAPTURE_SIZE}, ` +
42
+ `not ${String(side)}`,
43
+ );
44
+ }
45
+ }
46
+ return { kind: 'scene-capture', width, height, float };
47
+ }
48
+
49
+ /** Whether a texture source is a capture's description rather than an image. */
50
+ export function isSceneCaptureTexels(source: unknown): source is SceneCaptureTexels {
51
+ return (
52
+ typeof source === 'object' &&
53
+ source !== null &&
54
+ (source as { kind?: unknown }).kind === 'scene-capture'
55
+ );
56
+ }
@@ -1,5 +1,6 @@
1
1
  import { glSceneDepthFormat } from './depthConvention.ts';
2
2
  import { clampGrain, clampVignette, grainSeed } from './shaders/filmLook.ts';
3
+ import { FRINGE_FLOATS, resolveFringe } from './shaders/fringe.ts';
3
4
  import { AmbientOcclusionPass } from './ambientOcclusionPass.ts';
4
5
  import { ExposurePass } from './exposurePass.ts';
5
6
  import { clampAutoExposure } from './shaders/exposure.ts';
@@ -99,6 +100,8 @@ export class SceneTarget {
99
100
  private gradeStrength = 0;
100
101
  /** The lens and the print, held like the grade. See `filmLook.ts`. */
101
102
  private vignette = 0;
103
+ /** The lens's colour fringe: red's and green's pull and where it begins. See `fringe.ts`. */
104
+ private readonly fringe = new Float32Array(FRINGE_FLOATS);
102
105
  private grain = 0;
103
106
  private grainSeed = 0;
104
107
  /** Eye adaptation: how far, and this frame's time step. See `Renderer.setAutoExposure`. */
@@ -702,6 +705,11 @@ export class SceneTarget {
702
705
  this.vignette = clampVignette(strength);
703
706
  }
704
707
 
708
+ /** The lens's colour fringe, held until changed. See `Renderer.setChromaticAberration`. */
709
+ setChromaticAberration(intensity: number, start: number): void {
710
+ resolveFringe(intensity, start, this.fringe);
711
+ }
712
+
705
713
  /** The print's grain and this frame's seed, held until changed. See `Renderer.setFilmGrain`. */
706
714
  setFilmGrain(strength: number, seed: number): void {
707
715
  this.grain = clampGrain(strength);
@@ -769,8 +777,15 @@ export class SceneTarget {
769
777
  resolve(
770
778
  strength: number,
771
779
  reachUv: number,
772
- /* The grade, applied here because this is the last pass. See RUSH_FRAG's own note. */
773
- grade: { readonly transform: number; readonly exposure: number } = {
780
+ /*
781
+ * The grade, applied here because this is the last pass. See RUSH_FRAG's own note. `film` is
782
+ * the `filmic` curve's eight constants (`resolveFilmicCurve`), read only under transform 4.
783
+ */
784
+ grade: {
785
+ readonly transform: number;
786
+ readonly exposure: number;
787
+ readonly film?: Float32Array;
788
+ } = {
774
789
  transform: 0,
775
790
  exposure: 1,
776
791
  },
@@ -1012,6 +1027,12 @@ export class SceneTarget {
1012
1027
  gl.uniform1f(this.uniforms['uReach'] ?? null, reachUv);
1013
1028
  gl.uniform1i(this.uniforms['uOutputTransform'] ?? null, grade.transform);
1014
1029
  gl.uniform1f(this.uniforms['uOutputExposure'] ?? null, grade.exposure);
1030
+ if (grade.film !== undefined) {
1031
+ gl.uniform4fv(this.uniforms['uFilmA'] ?? null, grade.film, 0, 4);
1032
+ gl.uniform4fv(this.uniforms['uFilmB'] ?? null, grade.film, 4, 4);
1033
+ }
1034
+ /* A standard range always here: see `Renderer.displayRange`. */
1035
+ gl.uniform1f(this.uniforms['uDisplayHeadroom'] ?? null, 1);
1015
1036
 
1016
1037
  /*
1017
1038
  * Depth on its own unit, and the reprojection with it. Bound every frame rather than
@@ -1113,6 +1134,12 @@ export class SceneTarget {
1113
1134
  this.uniforms['uLocalExposure'] ?? null,
1114
1135
  localGrid === null ? 0 : this.localExposure,
1115
1136
  );
1137
+ /* Each object's own motion is WebGPU's under a reconstruction; here the scene stands in on
1138
+ unit 7 and the flag says not to read it, so the camera's motion alone blurs. */
1139
+ gl.activeTexture(gl.TEXTURE7);
1140
+ gl.bindTexture(gl.TEXTURE_2D, scene);
1141
+ gl.uniform1i(this.uniforms['uMotion'] ?? null, 7);
1142
+ gl.uniform1f(this.uniforms['uObjectMotion'] ?? null, 0);
1116
1143
  gl.activeTexture(gl.TEXTURE0);
1117
1144
 
1118
1145
  /* No unit and no texture: a flat colour, not a sample. Uploaded unconditionally — two
@@ -1122,6 +1149,7 @@ export class SceneTarget {
1122
1149
  gl.uniform3fv(this.uniforms['uVeilColor'] ?? null, veilColor);
1123
1150
  gl.uniform1f(this.uniforms['uVeilAlpha'] ?? null, veilAlpha);
1124
1151
  gl.uniform1f(this.uniforms['uVignette'] ?? null, this.vignette);
1152
+ gl.uniform3fv(this.uniforms['uFringe'] ?? null, this.fringe);
1125
1153
  gl.uniform2f(this.uniforms['uGrain'] ?? null, this.grain, this.grainSeed);
1126
1154
 
1127
1155
  gl.bindVertexArray(this.vao);
@@ -27,6 +27,11 @@ import type { DecalBox } from './decalProjector.ts';
27
27
  * which is the same price the temporal row declined for a velocity buffer. A caller declares a
28
28
  * `ReflectiveSurface` instead: a box, and a facing rule for which surfaces inside it reflect. It is
29
29
  * the shape `DecalProjector` already has, for the same reason.
30
+ *
31
+ * **Or every surface by its own material**, where the renderer was built with
32
+ * `screenSpaceReflections`: the lit stage leaves two maps of what it reflected — not a G-buffer, the
33
+ * environment's share and its tint, which is all a swap needs — and the same trace runs in a
34
+ * material mode over every pixel. See `frameReflections.ts`.
30
35
  */
31
36
 
32
37
  /**
@@ -350,9 +355,30 @@ export function towardCameraFade(direction: ReadonlyVec3, viewDirection: Readonl
350
355
  return Math.min(1, Math.max(0, (along + 1) / 2));
351
356
  }
352
357
 
358
+ /**
359
+ * What a `ReflectiveSurface` is made with: its box, which way the surfaces inside it face, and how
360
+ * much and how far they reflect. Every number but the box has a default; see each field.
361
+ */
353
362
  export interface ReflectiveSurfaceOptions extends DecalBox {
354
- /** How much of what the ray finds lands on the surface, 0 to 1. */
363
+ /**
364
+ * How much of what the ray finds lands on the surface, 0 to 1: one share over the box, or with
365
+ * `fresnel` the surface's reflectance head-on.
366
+ */
355
367
  readonly strength?: number;
368
+ /**
369
+ * Whether `strength` is the surface's reflectance head-on, F0, rather than one share over the
370
+ * box. Each pixel then returns `F0 · A + B` of what its ray finds, the split-sum environment BRDF
371
+ * at its own view and `roughness` — Karis's fit, the one the lit stage reflects its environment
372
+ * by — so a floor returns more toward the horizon than underfoot, and the share follows the
373
+ * camera as Unreal weighs its reflections. **False by default**, the one share as before.
374
+ *
375
+ * **What it gives up**: the pass has no material buffer, so the roughness and F0 are the box's,
376
+ * not each pixel's, and nothing occludes the reflection where the lit stage's ambient occlusion
377
+ * would. A floor of one material is what it is for.
378
+ */
379
+ readonly fresnel?: boolean;
380
+ /** The surface's roughness, 0 to 1, that `fresnel` weighs by. 0, a polished surface, if absent. */
381
+ readonly roughness?: number;
356
382
  /** How far a ray may travel, world metres. */
357
383
  readonly reachM?: number;
358
384
  /** How deep a depth sample is treated as being. See `ScreenSpaceMarch.thicknessM`. */
@@ -382,6 +408,9 @@ export class ReflectiveSurface {
382
408
  readonly tint = new Float32Array(3);
383
409
 
384
410
  strength: number;
411
+ /** See `ReflectiveSurfaceOptions.fresnel`. */
412
+ fresnel: boolean;
413
+ roughness: number;
385
414
  reachM: number;
386
415
  thicknessM: number;
387
416
  steps: number;
@@ -405,6 +434,8 @@ export class ReflectiveSurface {
405
434
  /* Half, because a mirror is the rare case: a wet floor, polished stone and a car bonnet all
406
435
  return a fraction of what lands on them, and a consumer who wants a mirror asks for one. */
407
436
  this.strength = options.strength ?? 0.5;
437
+ this.fresnel = options.fresnel ?? false;
438
+ this.roughness = options.roughness ?? 0;
408
439
  /* Eight metres covers a room and a street corner. Further costs steps rather than accuracy,
409
440
  since the step is the reach divided by them. */
410
441
  this.reachM = options.reachM ?? 8;
@@ -469,6 +500,8 @@ export interface RecordedReflection {
469
500
  readonly axis: Float32Array;
470
501
  readonly tint: Float32Array;
471
502
  strength: number;
503
+ fresnel: boolean;
504
+ roughness: number;
472
505
  reachM: number;
473
506
  thicknessM: number;
474
507
  steps: number;
@@ -482,6 +515,8 @@ function newRecord(): RecordedReflection {
482
515
  axis: new Float32Array(3),
483
516
  tint: new Float32Array(3),
484
517
  strength: 0.5,
518
+ fresnel: false,
519
+ roughness: 0,
485
520
  reachM: 8,
486
521
  thicknessM: 0.25,
487
522
  steps: 24,
@@ -537,6 +572,8 @@ export class ReflectionQueue {
537
572
  record.axis.set(surface.axis);
538
573
  record.tint.set(surface.tint);
539
574
  record.strength = Math.min(1, Math.max(0, surface.strength));
575
+ record.fresnel = surface.fresnel;
576
+ record.roughness = Math.min(1, Math.max(0, surface.roughness));
540
577
  record.reachM = Math.max(0, surface.reachM);
541
578
  record.thicknessM = Math.max(0, surface.thicknessM);
542
579
  record.steps = Math.min(32, Math.max(1, Math.round(surface.steps)));
@@ -107,7 +107,13 @@ void main() {
107
107
  * the glow. A single falloff gives either a hard line with no atmosphere or a
108
108
  * soft smudge with no filament.
109
109
  */
110
- float across = 1.0 - abs(vSide);
110
+ /*
111
+ * Clamped, because a multisampled target shades a pixel on the quad's edge at its centre, which
112
+ * may lie outside the quad: there 'vSide' runs past one, 'across' goes negative, and 'pow' of a
113
+ * negative number is NaN. One NaN pixel is a dark speck; bloom then spreads it into a white disc
114
+ * the size of the blur. Found by a fight drawing short arcs at 4 samples.
115
+ */
116
+ float across = clamp(1.0 - abs(vSide), 0.0, 1.0);
111
117
  float core = pow(across, 10.0);
112
118
  /*
113
119
  * A *wide* halo, deliberately. This exponent is the single biggest control over
@@ -101,6 +101,12 @@ void main() {
101
101
  #endif
102
102
  #if INSTANCED
103
103
  mat4 model = mat4(aInstanceModel0, aInstanceModel1, aInstanceModel2, aInstanceModel3);
104
+ /* The bottom row is the instance's texture cell, rebuilt as the row it stands for: see flat. */
105
+ vec4 instanceCell = vec4(model[0].w, model[1].w, model[2].w, model[3].w);
106
+ model[0].w = 0.0;
107
+ model[1].w = 0.0;
108
+ model[2].w = 0.0;
109
+ model[3].w = 1.0;
104
110
  #else
105
111
  mat4 model = uModel;
106
112
  #endif
@@ -125,7 +131,12 @@ void main() {
125
131
  vLightPosition = uLightViewProj * vec4(bent, world.w);
126
132
  gl_Position = vLightPosition;
127
133
  #if CUTOUT
134
+ #if INSTANCED
135
+ /* A cutout instance's shadow is cut by its own cell, as its picture is. */
136
+ vUv = vec3((aUv.xy * instanceCell.xy + instanceCell.zw) * uUvScale + uUvOffset, aUv.z);
137
+ #else
128
138
  vUv = vec3(aUv.xy * uUvScale + uUvOffset, aUv.z);
139
+ #endif
129
140
  vAlphaCutout = uAlphaCutout;
130
141
  #endif
131
142
  #if GLASS
@@ -88,7 +88,8 @@ export function grainWeight(luma: number): number {
88
88
 
89
89
  /** Both backends' words when there is no composite to apply the look in. */
90
90
  export const FILM_LOOK_WITHOUT_COMPOSITE =
91
- '[driftengine] setVignette and setFilmGrain need `screenEffects`: without a composite every ' +
91
+ '[driftengine] setVignette, setFilmGrain and setChromaticAberration need `screenEffects`: ' +
92
+ 'without a composite every ' +
92
93
  'pass is the last thing to touch the frame, so there is no one place to apply a lens or a ' +
93
94
  'print. The look was not applied.';
94
95
 
@@ -30,7 +30,16 @@ void anisotropicSurface() {
30
30
  direction.x * uModelParams[0].y - direction.y * uModelParams[0].z,
31
31
  direction.x * uModelParams[0].z + direction.y * uModelParams[0].y
32
32
  );
33
- aAlong = normalize(mTangent * turned.x + mBitangent * turned.y);
33
+ vec3 along = mTangent * turned.x + mBitangent * turned.y;
34
+ /*
35
+ * A mesh with neither tangents nor texture coordinates has no frame to take a direction from,
36
+ * and normalising the zero it gives was a NaN that drew the whole surface black. Any direction in
37
+ * the surface stands in, the one across the world's up, so the highlight is stretched somewhere.
38
+ */
39
+ if (dot(along, along) < 1e-12) {
40
+ along = cross(abs(mNormal.y) < 0.999 ? vec3(0.0, 1.0, 0.0) : vec3(1.0, 0.0, 0.0), mNormal);
41
+ }
42
+ aAlong = normalize(along);
34
43
  aAcross = cross(mNormal, aAlong);
35
44
  aStrength = strength;
36
45
  }
@@ -50,12 +59,37 @@ float anisotropicLobe(vec3 h, float sourceRadius, float dist) {
50
59
  return energy / max(d * d, 1e-8);
51
60
  }
52
61
 
62
+ /**
63
+ * The same stretch as GGX's own BRDF, \`π · D · Vis · N·L\`, for a surface whose highlight is
64
+ * physical: the distribution normalised, and Smith's height-correlated masking stretched with it
65
+ * (Heitz 2014, as Filament writes it). \`anisotropicPhysicalLobe\` in anisotropicLobe.ts.
66
+ */
67
+ float anisotropicPhysical(vec3 l, vec3 h, float ndl, float sourceRadius, float dist) {
68
+ float alpha = max(mRoughness * mRoughness, MIN_LOBE_ALPHA);
69
+ float alongWidth = alpha + (1.0 - alpha) * aStrength * aStrength;
70
+ float grow = sourceRadius / max(2.0 * dist, 1e-3);
71
+ float along = clamp(alongWidth + grow, alongWidth, 1.0);
72
+ float across = clamp(alpha + grow, alpha, 1.0);
73
+ float x = dot(aAlong, h) / along;
74
+ float y = dot(aAcross, h) / across;
75
+ float z = dot(mNormal, h);
76
+ float d = x * x + y * y + z * z;
77
+ float distribution = 1.0 / max(3.14159265 * along * across * d * d, 1e-8);
78
+ float ndv = max(dot(mNormal, mToEye), 1e-4);
79
+ float viewed = ndl * length(vec3(along * dot(aAlong, mToEye), across * dot(aAcross, mToEye), ndv));
80
+ float lit = ndv * length(vec3(along * dot(aAlong, l), across * dot(aAcross, l), ndl));
81
+ return 3.14159265 * distribution * (0.5 / max(viewed + lit, 1e-5)) * ndl;
82
+ }
83
+
53
84
  void anisotropicLight(vec3 l, float sourceRadius, float dist) {
54
85
  float ndl = max(dot(mNormal, l), 0.0);
55
86
  vec3 h = normalize(l + mToEye);
56
- vec3 tint = mix(mSpecColor, vec3(1.0), pow(1.0 - max(dot(mToEye, h), 0.0), 5.0) * mMetal);
87
+ vec3 tint = highlightTint(mSpecColor, max(dot(mToEye, h), 0.0), mMetal);
57
88
  mDiffuse = mAlbedo * ndl * (1.0 - mMetal);
58
- mSpecular = ndl > 0.0 ? tint * anisotropicLobe(h, sourceRadius, dist) : vec3(0.0);
89
+ float lobe = physicalSpecular()
90
+ ? anisotropicPhysical(l, h, ndl, sourceRadius, dist)
91
+ : anisotropicLobe(h, sourceRadius, dist);
92
+ mSpecular = ndl > 0.0 ? tint * lobe : vec3(0.0);
59
93
  }
60
94
 
61
95
  /* The environment's normal, bent toward the stretch by its strength and the surface's roughness. */