@driftengine/core 4.8.7 → 4.10.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 (263) hide show
  1. package/CHANGELOG.json +123 -11
  2. package/README.md +43 -38
  3. package/dist/index.d.ts +8 -1
  4. package/dist/index.js +11 -3
  5. package/dist/render/ambientHarmonics.d.ts +1 -1
  6. package/dist/render/ambientHarmonics.js +1 -1
  7. package/dist/render/areaLights.d.ts +4 -4
  8. package/dist/render/areaLights.js +1 -1
  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 +19 -4
  12. package/dist/render/backend/createRenderer.d.ts +6 -0
  13. package/dist/render/backend/createRenderer.js +4 -1
  14. package/dist/render/backend/select.js +11 -5
  15. package/dist/render/backend/webgl2/boneAnimations.d.ts +29 -0
  16. package/dist/render/backend/webgl2/boneAnimations.js +80 -0
  17. package/dist/render/backend/webgl2/instanced.d.ts +5 -1
  18. package/dist/render/backend/webgl2/instanced.js +13 -1
  19. package/dist/render/backend/webgl2/renderer.d.ts +180 -4
  20. package/dist/render/backend/webgl2/renderer.js +550 -40
  21. package/dist/render/backend/webgl2/skinPaletteTexture.d.ts +5 -3
  22. package/dist/render/backend/webgl2/skinPaletteTexture.js +6 -4
  23. package/dist/render/backend/webgpu/boneAnimations.d.ts +34 -0
  24. package/dist/render/backend/webgpu/boneAnimations.js +76 -0
  25. package/dist/render/backend/webgpu/buffers.d.ts +19 -4
  26. package/dist/render/backend/webgpu/buffers.js +18 -11
  27. package/dist/render/backend/webgpu/compressedUpload.d.ts +0 -1
  28. package/dist/render/backend/webgpu/compressedUpload.js +53 -8
  29. package/dist/render/backend/webgpu/depthPass.d.ts +59 -3
  30. package/dist/render/backend/webgpu/depthPass.js +49 -14
  31. package/dist/render/backend/webgpu/device.d.ts +7 -1
  32. package/dist/render/backend/webgpu/device.js +31 -3
  33. package/dist/render/backend/webgpu/drawCommand.d.ts +18 -4
  34. package/dist/render/backend/webgpu/drawCommand.js +10 -3
  35. package/dist/render/backend/webgpu/flatPass.d.ts +100 -42
  36. package/dist/render/backend/webgpu/flatPass.js +191 -43
  37. package/dist/render/backend/webgpu/instanced.d.ts +14 -1
  38. package/dist/render/backend/webgpu/instanced.js +24 -1
  39. package/dist/render/backend/webgpu/materialStore.d.ts +101 -0
  40. package/dist/render/backend/webgpu/materialStore.js +265 -0
  41. package/dist/render/backend/webgpu/pipelineCache.d.ts +6 -0
  42. package/dist/render/backend/webgpu/pipelineCache.js +7 -0
  43. package/dist/render/backend/webgpu/postPass.d.ts +51 -9
  44. package/dist/render/backend/webgpu/postPass.js +16 -2
  45. package/dist/render/backend/webgpu/reflectionSurfacePass.d.ts +30 -0
  46. package/dist/render/backend/webgpu/reflectionSurfacePass.js +100 -0
  47. package/dist/render/backend/webgpu/renderer.d.ts +283 -37
  48. package/dist/render/backend/webgpu/renderer.js +1363 -202
  49. package/dist/render/backend/webgpu/skinPaletteRing.d.ts +7 -3
  50. package/dist/render/backend/webgpu/skinPaletteRing.js +13 -7
  51. package/dist/render/backend/webgpu/skinPaletteTexture.d.ts +6 -0
  52. package/dist/render/backend/webgpu/skinPaletteTexture.js +7 -1
  53. package/dist/render/backend/webgpu/staticBundles.d.ts +125 -0
  54. package/dist/render/backend/webgpu/staticBundles.js +241 -0
  55. package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +10 -3
  56. package/dist/render/backend/webgpu/surfaceTexturePass.js +20 -2
  57. package/dist/render/backend/webgpu/uniformRing.d.ts +21 -0
  58. package/dist/render/backend/webgpu/uniformRing.js +9 -1
  59. package/dist/render/bloomChain.d.ts +3 -3
  60. package/dist/render/boneAnimation.d.ts +92 -0
  61. package/dist/render/boneAnimation.js +133 -0
  62. package/dist/render/compressedSource.d.ts +33 -13
  63. package/dist/render/compressedSource.js +70 -25
  64. package/dist/render/displayRange.d.ts +31 -0
  65. package/dist/render/displayRange.js +47 -0
  66. package/dist/render/filmicCurve.d.ts +74 -0
  67. package/dist/render/filmicCurve.js +203 -0
  68. package/dist/render/fog.d.ts +4 -1
  69. package/dist/render/fog.js +12 -2
  70. package/dist/render/frame/resources.d.ts +2 -2
  71. package/dist/render/frame/resources.js +6 -0
  72. package/dist/render/frameReflections.d.ts +40 -0
  73. package/dist/render/frameReflections.js +48 -0
  74. package/dist/render/glCompressed.js +83 -0
  75. package/dist/render/instances.d.ts +26 -0
  76. package/dist/render/lightBudget.d.ts +10 -0
  77. package/dist/render/lightBudget.js +10 -0
  78. package/dist/render/lightmap.d.ts +1 -1
  79. package/dist/render/mesh.d.ts +5 -0
  80. package/dist/render/mesh.js +6 -0
  81. package/dist/render/pass.d.ts +49 -0
  82. package/dist/render/pointLightSelection.d.ts +4 -4
  83. package/dist/render/reflectionSurfaceTargets.d.ts +28 -0
  84. package/dist/render/reflectionSurfaceTargets.js +99 -0
  85. package/dist/render/renderQuality.d.ts +39 -8
  86. package/dist/render/renderQuality.js +4 -0
  87. package/dist/render/sceneTarget.d.ts +5 -0
  88. package/dist/render/sceneTarget.js +24 -1
  89. package/dist/render/screenSpaceReflection.d.ts +7 -1
  90. package/dist/render/screenSpaceReflection.js +5 -0
  91. package/dist/render/shaders/arcane.d.ts +1 -1
  92. package/dist/render/shaders/bolt.d.ts +1 -1
  93. package/dist/render/shaders/boneAnimation.d.ts +19 -0
  94. package/dist/render/shaders/boneAnimation.js +63 -0
  95. package/dist/render/shaders/caustics.d.ts +1 -1
  96. package/dist/render/shaders/depth.d.ts +7 -0
  97. package/dist/render/shaders/depth.js +35 -10
  98. package/dist/render/shaders/film.d.ts +1 -1
  99. package/dist/render/shaders/filmLook.js +2 -1
  100. package/dist/render/shaders/fire.d.ts +1 -1
  101. package/dist/render/shaders/flat/glassTint.d.ts +1 -1
  102. package/dist/render/shaders/flat/glassTint.js +2 -0
  103. package/dist/render/shaders/flat/index.d.ts +16 -0
  104. package/dist/render/shaders/flat/index.js +33 -5
  105. package/dist/render/shaders/flat/main.d.ts +1 -1
  106. package/dist/render/shaders/flat/main.js +43 -5
  107. package/dist/render/shaders/flat/models.js +1 -1
  108. package/dist/render/shaders/flat/overlay.d.ts +16 -0
  109. package/dist/render/shaders/flat/overlay.js +117 -0
  110. package/dist/render/shaders/flat/pointShadow.js +3 -3
  111. package/dist/render/shaders/flat/preamble.js +38 -24
  112. package/dist/render/shaders/flat/reflectionSurface.d.ts +21 -0
  113. package/dist/render/shaders/flat/reflectionSurface.js +61 -0
  114. package/dist/render/shaders/fog.d.ts +1 -1
  115. package/dist/render/shaders/fog.js +9 -2
  116. package/dist/render/shaders/fringe.d.ts +29 -0
  117. package/dist/render/shaders/fringe.js +55 -0
  118. package/dist/render/shaders/generated/arcane.wgsl.d.ts +1 -1
  119. package/dist/render/shaders/generated/arcane.wgsl.js +1 -1
  120. package/dist/render/shaders/generated/bolt.wgsl.d.ts +1 -1
  121. package/dist/render/shaders/generated/bolt.wgsl.js +1 -1
  122. package/dist/render/shaders/generated/caustics.wgsl.d.ts +1 -1
  123. package/dist/render/shaders/generated/caustics.wgsl.js +1 -1
  124. package/dist/render/shaders/generated/depth.wgsl.d.ts +93 -0
  125. package/dist/render/shaders/generated/depth.wgsl.js +93 -0
  126. package/dist/render/shaders/generated/film.wgsl.d.ts +1 -1
  127. package/dist/render/shaders/generated/film.wgsl.js +1 -1
  128. package/dist/render/shaders/generated/fire.wgsl.d.ts +1 -1
  129. package/dist/render/shaders/generated/fire.wgsl.js +1 -1
  130. package/dist/render/shaders/generated/flat.wgsl.d.ts +3254 -2920
  131. package/dist/render/shaders/generated/flat.wgsl.js +3292 -2957
  132. package/dist/render/shaders/generated/line.wgsl.d.ts +1 -1
  133. package/dist/render/shaders/generated/line.wgsl.js +1 -1
  134. package/dist/render/shaders/generated/particle.wgsl.d.ts +4 -4
  135. package/dist/render/shaders/generated/particle.wgsl.js +4 -4
  136. package/dist/render/shaders/generated/rush.wgsl.d.ts +56 -26
  137. package/dist/render/shaders/generated/rush.wgsl.js +56 -26
  138. package/dist/render/shaders/generated/scatter.wgsl.d.ts +1 -1
  139. package/dist/render/shaders/generated/scatter.wgsl.js +1 -1
  140. package/dist/render/shaders/generated/smoke.wgsl.d.ts +1 -1
  141. package/dist/render/shaders/generated/smoke.wgsl.js +1 -1
  142. package/dist/render/shaders/generated/ssrResolve.wgsl.d.ts +29 -0
  143. package/dist/render/shaders/generated/ssrResolve.wgsl.js +29 -0
  144. package/dist/render/shaders/generated/ssrTrace.wgsl.d.ts +22 -2
  145. package/dist/render/shaders/generated/ssrTrace.wgsl.js +22 -2
  146. package/dist/render/shaders/generated/water.wgsl.d.ts +1 -1
  147. package/dist/render/shaders/generated/water.wgsl.js +1 -1
  148. package/dist/render/shaders/gpudriven/fog.wgsl.d.ts +2 -2
  149. package/dist/render/shaders/gpudriven/fog.wgsl.js +11 -3
  150. package/dist/render/shaders/line.d.ts +1 -1
  151. package/dist/render/shaders/outputTransform.d.ts +7 -0
  152. package/dist/render/shaders/outputTransform.js +28 -14
  153. package/dist/render/shaders/particle.d.ts +3 -3
  154. package/dist/render/shaders/rush.js +54 -11
  155. package/dist/render/shaders/scatter.d.ts +1 -1
  156. package/dist/render/shaders/smoke.d.ts +1 -1
  157. package/dist/render/shaders/ssrResolve.d.ts +17 -0
  158. package/dist/render/shaders/ssrResolve.js +51 -0
  159. package/dist/render/shaders/ssrTrace.js +26 -0
  160. package/dist/render/shaders/vertexChannel.d.ts +7 -0
  161. package/dist/render/shaders/vertexChannel.js +26 -0
  162. package/dist/render/shaders/water.d.ts +1 -1
  163. package/dist/render/ssrPass.d.ts +16 -0
  164. package/dist/render/ssrPass.js +106 -1
  165. package/dist/render/staticDraws.d.ts +47 -0
  166. package/dist/render/staticDraws.js +43 -0
  167. package/dist/render/surfaceOverlay.d.ts +132 -0
  168. package/dist/render/surfaceOverlay.js +85 -0
  169. package/dist/render/surfaceTexture.d.ts +57 -7
  170. package/dist/render/surfaceTexture.js +125 -17
  171. package/dist/render/temporalAa.d.ts +14 -0
  172. package/dist/render/temporalAa.js +20 -0
  173. package/dist/render/vertexDefaults.d.ts +17 -0
  174. package/dist/render/vertexDefaults.js +24 -0
  175. package/package.json +6 -6
  176. package/src/index.ts +24 -2
  177. package/src/render/ambientHarmonics.ts +1 -1
  178. package/src/render/areaLights.ts +4 -4
  179. package/src/render/atmosphere.ts +22 -1
  180. package/src/render/backend/api.ts +22 -3
  181. package/src/render/backend/createRenderer.ts +18 -1
  182. package/src/render/backend/select.ts +10 -5
  183. package/src/render/backend/webgl2/boneAnimations.ts +105 -0
  184. package/src/render/backend/webgl2/instanced.ts +20 -1
  185. package/src/render/backend/webgl2/renderer.ts +672 -37
  186. package/src/render/backend/webgl2/skinPaletteTexture.ts +6 -4
  187. package/src/render/backend/webgpu/boneAnimations.ts +97 -0
  188. package/src/render/backend/webgpu/buffers.ts +42 -16
  189. package/src/render/backend/webgpu/compressedUpload.ts +59 -9
  190. package/src/render/backend/webgpu/depthPass.ts +51 -10
  191. package/src/render/backend/webgpu/device.ts +42 -3
  192. package/src/render/backend/webgpu/drawCommand.ts +22 -4
  193. package/src/render/backend/webgpu/flatPass.ts +250 -40
  194. package/src/render/backend/webgpu/instanced.ts +32 -1
  195. package/src/render/backend/webgpu/materialStore.ts +276 -0
  196. package/src/render/backend/webgpu/pipelineCache.ts +7 -0
  197. package/src/render/backend/webgpu/postPass.ts +18 -0
  198. package/src/render/backend/webgpu/reflectionSurfacePass.ts +114 -0
  199. package/src/render/backend/webgpu/renderer.ts +1655 -230
  200. package/src/render/backend/webgpu/skinPaletteRing.ts +13 -7
  201. package/src/render/backend/webgpu/skinPaletteTexture.ts +7 -1
  202. package/src/render/backend/webgpu/staticBundles.ts +363 -0
  203. package/src/render/backend/webgpu/surfaceTexturePass.ts +29 -3
  204. package/src/render/backend/webgpu/uniformRing.ts +10 -1
  205. package/src/render/bloomChain.ts +3 -3
  206. package/src/render/boneAnimation.ts +223 -0
  207. package/src/render/compressedSource.ts +121 -29
  208. package/src/render/displayRange.ts +61 -0
  209. package/src/render/filmicCurve.ts +229 -0
  210. package/src/render/fog.ts +19 -3
  211. package/src/render/frame/resources.ts +6 -0
  212. package/src/render/frameReflections.ts +72 -0
  213. package/src/render/glCompressed.ts +83 -0
  214. package/src/render/instances.ts +27 -0
  215. package/src/render/lightBudget.ts +11 -0
  216. package/src/render/lightmap.ts +1 -1
  217. package/src/render/mesh.ts +6 -0
  218. package/src/render/pass.ts +53 -1
  219. package/src/render/pointLightSelection.ts +4 -4
  220. package/src/render/reflectionSurfaceTargets.ts +105 -0
  221. package/src/render/renderQuality.ts +44 -8
  222. package/src/render/sceneTarget.ts +30 -2
  223. package/src/render/screenSpaceReflection.ts +7 -1
  224. package/src/render/shaders/boneAnimation.ts +63 -0
  225. package/src/render/shaders/depth.ts +45 -10
  226. package/src/render/shaders/filmLook.ts +2 -1
  227. package/src/render/shaders/flat/glassTint.ts +5 -1
  228. package/src/render/shaders/flat/index.ts +49 -5
  229. package/src/render/shaders/flat/main.ts +43 -5
  230. package/src/render/shaders/flat/models.ts +1 -1
  231. package/src/render/shaders/flat/overlay.ts +117 -0
  232. package/src/render/shaders/flat/pointShadow.ts +3 -3
  233. package/src/render/shaders/flat/preamble.ts +38 -24
  234. package/src/render/shaders/flat/reflectionSurface.ts +61 -0
  235. package/src/render/shaders/fog.ts +9 -2
  236. package/src/render/shaders/fringe.ts +59 -0
  237. package/src/render/shaders/generated/arcane.wgsl.ts +1 -1
  238. package/src/render/shaders/generated/bolt.wgsl.ts +1 -1
  239. package/src/render/shaders/generated/caustics.wgsl.ts +1 -1
  240. package/src/render/shaders/generated/depth.wgsl.ts +95 -0
  241. package/src/render/shaders/generated/film.wgsl.ts +1 -1
  242. package/src/render/shaders/generated/fire.wgsl.ts +1 -1
  243. package/src/render/shaders/generated/flat.wgsl.ts +3292 -2957
  244. package/src/render/shaders/generated/line.wgsl.ts +1 -1
  245. package/src/render/shaders/generated/particle.wgsl.ts +4 -4
  246. package/src/render/shaders/generated/rush.wgsl.ts +56 -26
  247. package/src/render/shaders/generated/scatter.wgsl.ts +1 -1
  248. package/src/render/shaders/generated/smoke.wgsl.ts +1 -1
  249. package/src/render/shaders/generated/ssrResolve.wgsl.ts +30 -0
  250. package/src/render/shaders/generated/ssrTrace.wgsl.ts +22 -2
  251. package/src/render/shaders/generated/water.wgsl.ts +1 -1
  252. package/src/render/shaders/gpudriven/fog.wgsl.ts +11 -3
  253. package/src/render/shaders/outputTransform.ts +30 -14
  254. package/src/render/shaders/rush.ts +54 -11
  255. package/src/render/shaders/ssrResolve.ts +52 -0
  256. package/src/render/shaders/ssrTrace.ts +26 -0
  257. package/src/render/shaders/vertexChannel.ts +27 -0
  258. package/src/render/ssrPass.ts +138 -1
  259. package/src/render/staticDraws.ts +95 -0
  260. package/src/render/surfaceOverlay.ts +195 -0
  261. package/src/render/surfaceTexture.ts +161 -27
  262. package/src/render/temporalAa.ts +20 -0
  263. package/src/render/vertexDefaults.ts +28 -0
@@ -15,6 +15,11 @@ import {
15
15
  glShadowDepthFunc,
16
16
  } from '../../depthConvention.ts';
17
17
  import type { FrameView } from '../../frameView.ts';
18
+ import { maskOf } from '../../frame/resources.ts';
19
+
20
+ /** The two copies a registered pass may declare it reads. See `drawPass`. */
21
+ const COLOR_SNAPSHOT = maskOf('colorSnapshot');
22
+ const DEPTH_SNAPSHOT = maskOf('depthSnapshot');
18
23
  import type { Mover } from '../../recon/mover.ts';
19
24
  import { resolveGlass } from '../../glass.ts';
20
25
  import type { GlassOptions, ResolvedGlass } from '../../glass.ts';
@@ -31,6 +36,12 @@ import {
31
36
  } from '../../temporalAa.ts';
32
37
  import { DecalPass } from '../../decalPass.ts';
33
38
  import { SsrPass } from '../../ssrPass.ts';
39
+ import { ReflectionSurfaceTargets } from '../../reflectionSurfaceTargets.ts';
40
+ import {
41
+ frameReflectionsRefused,
42
+ resolveFrameReflections,
43
+ type ResolvedFrameReflections,
44
+ } from '../../frameReflections.ts';
34
45
  import { GlobalMediumPass } from '../../globalMediumPass.ts';
35
46
  import {
36
47
  DEFAULT_GLOBAL_MEDIUM,
@@ -81,11 +92,18 @@ import { isWeakGpuFamily, webgl2RendererName } from '../../gpuCapability.ts';
81
92
  import { GpuTimer } from '../../gpuTimer.ts';
82
93
  import { compileProgram, uniformLocations } from '../../shader.ts';
83
94
  import {
95
+ countUniformVectors,
84
96
  FULL_LIGHT_BUDGET,
85
97
  type LightBudget,
86
98
  nextLightBudget,
87
99
  planLightBudget,
88
100
  } from '../../uniformVectorBudget.ts';
101
+ import {
102
+ OVERLAY_FLOATS,
103
+ overlayLays,
104
+ packSurfaceOverlay,
105
+ type SurfaceOverlay,
106
+ } from '../../surfaceOverlay.ts';
89
107
  import { FILM_FRAG, FILM_VERT } from '../../shaders/film.ts';
90
108
  import { SceneTarget } from '../../sceneTarget.ts';
91
109
  import type { ColourGradeLut } from '../../colourGrade.ts';
@@ -200,6 +218,8 @@ import {
200
218
  DEPTH_CUTOUT_FRAG,
201
219
  DEPTH_CUTOUT_VERT,
202
220
  DEPTH_FRAG,
221
+ DEPTH_INSTANCED_ANIMATED_CUTOUT_VERT,
222
+ DEPTH_INSTANCED_ANIMATED_VERT,
203
223
  DEPTH_INSTANCED_CUTOUT_VERT,
204
224
  DEPTH_INSTANCED_VERT,
205
225
  DEPTH_SKINNED_VERT,
@@ -214,6 +234,8 @@ import { GlassCasterList, type GlassReplaySink } from '../../glassCasters.ts';
214
234
  import { SunGlassTint } from '../../sunGlassTint.ts';
215
235
  import { GlassTintPrograms, type GlassTintProgram } from './glassTintPrograms.ts';
216
236
  import type { SceneCasterMaterial, ShadowCasterSink, ShadowCasters } from '../../shadowCasters.ts';
237
+ import { captureStaticDraws, replayStaticDraws } from '../../staticDraws.ts';
238
+ import type { StaticDrawsHandle, StaticDrawsList } from '../../staticDraws.ts';
217
239
  import { FACE_COUNT, PointShadowMap, type PointGlassDraw } from '../../pointShadowMap.ts';
218
240
  import { createResolvedPointShadows, DEFAULT_SOURCE_RADIUS } from '../../pointShadowImage.ts';
219
241
  import {
@@ -259,7 +281,11 @@ import { SurfaceTexture } from '../../surfaceTexture.ts';
259
281
  import { sceneCaptureTexels } from '../../sceneCapture.ts';
260
282
  import { GlSceneCaptureTarget } from './sceneCaptureTarget.ts';
261
283
  import { glCompressedFormats } from '../../glCompressed.ts';
262
- import type { CompressedTextureFormat, SurfaceSource } from '../../compressedSource.ts';
284
+ import type {
285
+ CompressedTextureFormat,
286
+ CompressedTextureSource,
287
+ SurfaceSource,
288
+ } from '../../compressedSource.ts';
263
289
  import type { SurfaceMaterial, SurfaceTextureOptions } from '../../surfaceTexture.ts';
264
290
  import { PlumeRenderer } from '../../plumeRenderer.ts';
265
291
  import { createScatterDeform, resolveScatterDeform } from '../../scatterDeform.ts';
@@ -293,8 +319,22 @@ import { TextRenderer } from '../../textRenderer.ts';
293
319
  import { SdfTextRenderer } from '../../sdfTextRenderer.ts';
294
320
  import type { SdfFont } from '../../sdfFont.ts';
295
321
  import type { SdfTextStyle } from '../../sdfTextLayout.ts';
296
- import { MOTION_BLUR_MAX_UV, RUSH_REACH_UV } from '../../vertexDefaults.ts';
297
- import { OUTPUT_TRANSFORM_CODE } from '../../vertexDefaults.ts';
322
+ import {
323
+ MOTION_BLUR_MAX_UV,
324
+ OBJECT_BLUR_UNKEPT,
325
+ RUSH_REACH_UV,
326
+ clampMotionShare,
327
+ } from '../../vertexDefaults.ts';
328
+ import { OUTPUT_TRANSFORM_CODE, forwardTransformCode } from '../../vertexDefaults.ts';
329
+ import type { DisplayRange } from '../../displayRange.ts';
330
+ import {
331
+ DEFAULT_FILMIC_CURVE,
332
+ FILMIC_CLAMPED,
333
+ FILMIC_CONSTANTS,
334
+ FILMIC_WITHOUT_COMPOSITE,
335
+ resolveFilmicCurve,
336
+ type FilmicCurve,
337
+ } from '../../filmicCurve.ts';
298
338
  import type { RenderBackend } from '../api.ts';
299
339
  import { deviceSnappedCellSize } from '../../textLayout.ts';
300
340
  import type { TextStyle } from '../../textLayout.ts';
@@ -303,6 +343,14 @@ import type { WindStreakOptions } from '../../windStreakRenderer.ts';
303
343
  import type { WindField } from '../../windField.ts';
304
344
  import type { FlockParams } from '../../flockRenderer.ts';
305
345
  import { InstancedBatch } from './instanced.ts';
346
+ import { GlBoneAnimation } from './boneAnimations.ts';
347
+ import { packBoneAnimation } from '../../boneAnimation.ts';
348
+ import type { BoneAnimationClip, BoneAnimationHandle } from '../../boneAnimation.ts';
349
+ import {
350
+ BONE_PLACES_TEXTURE_UNIT,
351
+ BONE_TURNS_TEXTURE_UNIT,
352
+ INSTANCE_CLOCKS_TEXTURE_UNIT,
353
+ } from '../../lightBudget.ts';
306
354
  import type { InstancedOptions, MeshInstances } from '../../instances.ts';
307
355
  import { cutoutOf } from '../../cutoutCaster.ts';
308
356
  import { CUTOUT_RESOLVE_CODE, resolveCutout } from '../../cutoutDither.ts';
@@ -1348,6 +1396,10 @@ export class WebGL2Renderer implements RendererApi {
1348
1396
  private readonly reflectionQueue = new ReflectionQueue();
1349
1397
  /** Built on the first frame that submits a surface, and never for a scene with none. */
1350
1398
  private ssr: SsrPass | null = null;
1399
+ /** The frame's materials' reflections, resolved; null where off. See `frameReflections.ts`. */
1400
+ private readonly frameReflections: ResolvedFrameReflections | null;
1401
+ /** Their two maps on the scene's framebuffer, null where they cannot be drawn. */
1402
+ private readonly reflectionTargets: ReflectionSurfaceTargets | null;
1351
1403
  /** Said once where a reflection cannot be traced at all. */
1352
1404
  private reflectionsRefused = false;
1353
1405
  /** Where the frame's camera stands, which is what gives a reconstructed normal its sign. */
@@ -1430,6 +1482,8 @@ export class WebGL2Renderer implements RendererApi {
1430
1482
  * exactly as it asked to be and nothing about it changes.
1431
1483
  */
1432
1484
  private exposure: number;
1485
+ /** The `filmic` curve's eight constants, held until `setFilmicCurve` is called again. */
1486
+ private readonly filmConstants = new Float32Array(FILMIC_CONSTANTS);
1433
1487
  /**
1434
1488
  * This frame's veil colour, and how much of it to composite. See `setFrameVeil`.
1435
1489
  *
@@ -1544,6 +1598,11 @@ export class WebGL2Renderer implements RendererApi {
1544
1598
  private readonly depthCutoutUniforms: Record<string, WebGLUniformLocation>;
1545
1599
  private readonly depthInstancedCutoutProgram: WebGLProgram;
1546
1600
  private readonly depthInstancedCutoutUniforms: Record<string, WebGLUniformLocation>;
1601
+ /** A crowd's casters: the instanced depth programs that play a bone animation. */
1602
+ private readonly depthAnimatedProgram: WebGLProgram;
1603
+ private readonly depthAnimatedUniforms: Record<string, WebGLUniformLocation>;
1604
+ private readonly depthAnimatedCutoutProgram: WebGLProgram;
1605
+ private readonly depthAnimatedCutoutUniforms: Record<string, WebGLUniformLocation>;
1547
1606
  private readonly scatterDepthProgram: WebGLProgram;
1548
1607
  private readonly scatterDepthUniforms: Record<string, WebGLUniformLocation>;
1549
1608
  /** The sun's static, moving and peeled maps, as layers of one array. See `shadowMap.ts`. */
@@ -1631,6 +1690,15 @@ export class WebGL2Renderer implements RendererApi {
1631
1690
  private warnedNoPointShadowArray = false;
1632
1691
  /** Said once, for the retired ambient dial. */
1633
1692
  private warnedEnvironmentAmbient = false;
1693
+ /** The overlay the draws wear, `uOverlay`: see `setSurfaceOverlay`. */
1694
+ private readonly overlayFloats = new Float32Array(OVERLAY_FLOATS);
1695
+ /** Its atlas, and the 2D copy of it bound where the refraction copy goes. */
1696
+ private overlayMaps: SurfaceTexture | null = null;
1697
+ private overlayTexture: WebGLTexture | null = null;
1698
+ /** Whether this device's lit stage has room for the overlay's vectors: asked once. */
1699
+ private overlayRoom: boolean | null = null;
1700
+ private warnedOverlayRoom = false;
1701
+ private warnedOverlayBlocks = false;
1634
1702
  /** Whether a probe is being baked, so the passes that would recurse into one can tell. */
1635
1703
  private probePassActive = false;
1636
1704
  /**
@@ -1955,24 +2023,33 @@ export class WebGL2Renderer implements RendererApi {
1955
2023
  const gpuBatch = batch as InstancedBatch;
1956
2024
  const count = Math.min(data.count, gpuBatch.capacity);
1957
2025
  if (count === 0) return;
2026
+ const clip = gpuBatch.animation;
2027
+ if (clip !== null && clip.disposed) return;
1958
2028
  this.shadowDrawBudget.ask();
1959
2029
  const twoSided = material?.doubleSided === true;
1960
2030
  if (twoSided) gl.disable(gl.CULL_FACE);
1961
2031
  const cutout = cutoutOf(material);
1962
2032
  if (cutout !== null) {
1963
- gl.useProgram(this.depthInstancedCutoutProgram);
1964
- this.bindCutoutDepth(this.depthInstancedCutoutUniforms, cutout);
2033
+ /* A crowd casts where its clip has moved it, through the animated variant. */
2034
+ const uniforms =
2035
+ clip === null ? this.depthInstancedCutoutUniforms : this.depthAnimatedCutoutUniforms;
2036
+ gl.useProgram(
2037
+ clip === null ? this.depthInstancedCutoutProgram : this.depthAnimatedCutoutProgram,
2038
+ );
2039
+ this.bindCutoutDepth(uniforms, cutout);
2040
+ if (clip !== null) this.bindAnimation(uniforms, gpuBatch);
1965
2041
  gpuBatch.draw(gl, count);
1966
2042
  if (twoSided && this.depthPassCullsFaces) gl.enable(gl.CULL_FACE);
1967
2043
  gl.useProgram(this.depthProgram);
1968
2044
  this.restoreSurfaceTextureUnit();
1969
2045
  return;
1970
2046
  }
1971
- const u = this.depthInstancedUniforms;
1972
- gl.useProgram(this.depthInstancedProgram);
2047
+ const u = clip === null ? this.depthInstancedUniforms : this.depthAnimatedUniforms;
2048
+ gl.useProgram(clip === null ? this.depthInstancedProgram : this.depthAnimatedProgram);
1973
2049
  gl.uniformMatrix4fv(u['uLightViewProj'] ?? null, false, this.activeDepthViewProj);
1974
2050
  gl.uniform1i(u['uPeelShadowLayer'] ?? null, this.activeDepthPeel ? 1 : 0);
1975
2051
  if (this.activeDepthPeel) gl.uniform1i(u['uPreviousShadowMap'] ?? null, 0);
2052
+ if (clip !== null) this.bindAnimation(u, gpuBatch);
1976
2053
  gpuBatch.draw(gl, count);
1977
2054
  if (twoSided && this.depthPassCullsFaces) gl.enable(gl.CULL_FACE);
1978
2055
  gl.useProgram(this.depthProgram);
@@ -2298,6 +2375,8 @@ export class WebGL2Renderer implements RendererApi {
2298
2375
  gl: WebGL2RenderingContext;
2299
2376
  outputTransform: number;
2300
2377
  outputExposure: number;
2378
+ sceneColor: WebGLTexture | null;
2379
+ sceneDepth: WebGLTexture | null;
2301
2380
  } | null = null;
2302
2381
  private passDevice: PassDevice | null = null;
2303
2382
 
@@ -2309,6 +2388,9 @@ export class WebGL2Renderer implements RendererApi {
2309
2388
  * call. `init` runs now rather than at the first frame, because building a pipeline in the
2310
2389
  * frame loop is the allocation the house rules are about.
2311
2390
  */
2391
+ /** Each registered pass's declared reads, as a mask, resolved once at registration. */
2392
+ private readonly passReads = new Map<PassHandle, number>();
2393
+
2312
2394
  registerPass(definition: PassDefinition): PassHandle {
2313
2395
  const handle = registerIn(this.passes, definition);
2314
2396
  /*
@@ -2327,6 +2409,8 @@ export class WebGL2Renderer implements RendererApi {
2327
2409
  depthCorrection: correction,
2328
2410
  };
2329
2411
  if (definition.prepare !== undefined) this.preparingPasses++;
2412
+ /* Resolved once: `maskOf` builds an array, and the frame path may not. */
2413
+ this.passReads.set(handle, maskOf(...(definition.reads ?? [])));
2330
2414
  definition.init?.(this.passDevice);
2331
2415
  return handle;
2332
2416
  }
@@ -2378,10 +2462,28 @@ export class WebGL2Renderer implements RendererApi {
2378
2462
  gl: this.gl,
2379
2463
  outputTransform: 0,
2380
2464
  outputExposure: 1,
2465
+ sceneColor: null,
2466
+ sceneDepth: null,
2381
2467
  };
2382
2468
  const context = this.passContext;
2383
2469
  context.outputTransform = this.gradeCode();
2384
2470
  context.outputExposure = this.gradeExposure();
2471
+ /*
2472
+ * The copies a pass declared it reads, taken here with everything drawn before it: colour
2473
+ * afresh at each reading pass, as WebGPU's `takePassColour` does, so a full-screen effect drawn
2474
+ * last reads the whole frame; depth afresh too, since a blit is the whole cost of either here.
2475
+ * Not in a mirror, a probe's face or a capture, whose pictures are not the frame's.
2476
+ */
2477
+ const reads = this.passReads.get(handle) ?? 0;
2478
+ const own = !this.reflectionPassActive && !this.probePassActive && !this.capturePassActive;
2479
+ context.sceneColor =
2480
+ own && (reads & COLOR_SNAPSHOT) !== 0
2481
+ ? (this.sceneTarget?.snapshotColor(true) ?? null)
2482
+ : null;
2483
+ context.sceneDepth =
2484
+ own && (reads & DEPTH_SNAPSHOT) !== 0
2485
+ ? (this.sceneTarget?.snapshotDepth(true) ?? null)
2486
+ : null;
2385
2487
  definition.draw(context as PassContext);
2386
2488
  }
2387
2489
 
@@ -2389,6 +2491,7 @@ export class WebGL2Renderer implements RendererApi {
2389
2491
  unregisterPass(handle: PassHandle): void {
2390
2492
  const definition = unregisterIn(this.passes, handle);
2391
2493
  if (definition?.prepare !== undefined) this.preparingPasses--;
2494
+ this.passReads.delete(handle);
2392
2495
  if (definition !== undefined && this.passDevice !== null) definition.dispose?.(this.passDevice);
2393
2496
  }
2394
2497
 
@@ -2559,10 +2662,11 @@ export class WebGL2Renderer implements RendererApi {
2559
2662
  gl.bindTexture(gl.TEXTURE_2D, this.clusterTexture);
2560
2663
  gl.uniform1i(u['uClusterTable'] ?? null, CLUSTER_TABLE_TEXTURE_UNIT);
2561
2664
  gl.uniform1i(u['uClustered'] ?? null, on ? 1 : 0);
2562
- if (!on) return;
2563
2665
 
2564
2666
  /*
2565
- * The frustum, read off the projection rather than taken as a parameter.
2667
+ * The frustum, read off the projection rather than taken as a parameter — **and with the view,
2668
+ * written whether or not the lights are clustered**: a surface overlay's rim finds its place on
2669
+ * the screen through the same two (`overlayScreen`).
2566
2670
  *
2567
2671
  * A perspective matrix carries `1 / tan(fovY/2)` at [5] and that over the aspect at [0], and
2568
2672
  * the near and far planes in [14] and [10]. Deriving them here means the binner and the shader
@@ -2580,6 +2684,7 @@ export class WebGL2Renderer implements RendererApi {
2580
2684
  this.clusterFrustum[3] = aspect;
2581
2685
  gl.uniform4fv(u['uClusterFrustum'] ?? null, this.clusterFrustum);
2582
2686
  gl.uniformMatrix4fv(u['uView'] ?? null, false, camera.view);
2687
+ if (!on) return;
2583
2688
 
2584
2689
  this.clusterTable ??= createClusterTable();
2585
2690
  /* The rectangles past the fixed arm's go into the table. See `ClusterLightSet.areas`. */
@@ -2683,6 +2788,13 @@ export class WebGL2Renderer implements RendererApi {
2683
2788
  this.quality = resolveRenderQuality(qualityOptions);
2684
2789
  /* The grade this world asked for, until a frame says otherwise. See `setOutputExposure`. */
2685
2790
  this.exposure = this.quality.outputExposure;
2791
+ resolveFilmicCurve(DEFAULT_FILMIC_CURVE, this.filmConstants);
2792
+ if (
2793
+ this.quality.outputTransform === 'filmic' &&
2794
+ !(this.quality.screenEffects && this.quality.hdrScene)
2795
+ ) {
2796
+ console.warn(FILMIC_WITHOUT_COMPOSITE);
2797
+ }
2686
2798
  /*
2687
2799
  * Multisampling, but only when anything can use it.
2688
2800
  *
@@ -2981,6 +3093,29 @@ export class WebGL2Renderer implements RendererApi {
2981
3093
  this.quality.discardResolvedAttachments,
2982
3094
  )
2983
3095
  : null;
3096
+ /*
3097
+ * The frame's materials' reflections, where the frame can carry them: scene light to swap the
3098
+ * probe's share in, and one sample, which the trace's depth copy and both maps are. Said once
3099
+ * where it cannot, in the words the other backend uses. See `frameReflections.ts`.
3100
+ */
3101
+ this.frameReflections = resolveFrameReflections(this.quality.screenSpaceReflections);
3102
+ const reflectionsRefusal =
3103
+ this.frameReflections === null
3104
+ ? null
3105
+ : this.sceneTarget === null || !this.sceneTarget.keepsRange
3106
+ ? 'the frame keeps no scene light to swap the probe in (`screenEffects` and `hdrScene`)'
3107
+ : this.sceneTarget.sampleCount > 1
3108
+ ? 'a multisampled frame has no single depth or colour to march through'
3109
+ : null;
3110
+ if (reflectionsRefusal !== null) {
3111
+ console.warn(`WebGL2: ${frameReflectionsRefused(reflectionsRefusal)}`);
3112
+ }
3113
+ this.reflectionTargets =
3114
+ this.frameReflections !== null && reflectionsRefusal === null
3115
+ ? new ReflectionSurfaceTargets(gl)
3116
+ : null;
3117
+ /* The lit programs write the maps from the first frame they are rebuilt in. See `litOn`. */
3118
+ if (this.reflectionTargets !== null) this.askLit('REFLECTION_MAPS');
2984
3119
  /* Screen-space skin needs the composite to add into; without one it is pre-integrated, said once. */
2985
3120
  if (this.quality.skinScattering === 'screen-space' && this.sceneTarget === null) {
2986
3121
  console.warn(
@@ -3077,10 +3212,33 @@ export class WebGL2Renderer implements RendererApi {
3077
3212
  this.depthInstancedCutoutProgram,
3078
3213
  'depth-instanced-cutout',
3079
3214
  );
3215
+ this.depthAnimatedProgram = compileProgram(
3216
+ gl,
3217
+ DEPTH_INSTANCED_ANIMATED_VERT,
3218
+ DEPTH_FRAG,
3219
+ 'depth-instanced-animated',
3220
+ );
3221
+ this.depthAnimatedUniforms = uniformLocations(
3222
+ gl,
3223
+ this.depthAnimatedProgram,
3224
+ 'depth-instanced-animated',
3225
+ );
3226
+ this.depthAnimatedCutoutProgram = compileProgram(
3227
+ gl,
3228
+ DEPTH_INSTANCED_ANIMATED_CUTOUT_VERT,
3229
+ DEPTH_CUTOUT_FRAG,
3230
+ 'depth-instanced-animated-cutout',
3231
+ );
3232
+ this.depthAnimatedCutoutUniforms = uniformLocations(
3233
+ gl,
3234
+ this.depthAnimatedCutoutProgram,
3235
+ 'depth-instanced-animated-cutout',
3236
+ );
3080
3237
  /* The cutout map is always read from the surface unit, which the draw borrows and gives back. */
3081
3238
  for (const [program, uniforms] of [
3082
3239
  [this.depthCutoutProgram, this.depthCutoutUniforms],
3083
3240
  [this.depthInstancedCutoutProgram, this.depthInstancedCutoutUniforms],
3241
+ [this.depthAnimatedCutoutProgram, this.depthAnimatedCutoutUniforms],
3084
3242
  ] as const) {
3085
3243
  gl.useProgram(program);
3086
3244
  gl.uniform1i(uniforms['uCutoutMap'] ?? null, SURFACE_TEXTURE_UNIT);
@@ -3274,6 +3432,8 @@ export class WebGL2Renderer implements RendererApi {
3274
3432
  this.lightVolumeProgram,
3275
3433
  this.depthCutoutProgram,
3276
3434
  this.depthInstancedCutoutProgram,
3435
+ this.depthAnimatedProgram,
3436
+ this.depthAnimatedCutoutProgram,
3277
3437
  ]) {
3278
3438
  gl.deleteProgram(program);
3279
3439
  }
@@ -3620,13 +3780,19 @@ export class WebGL2Renderer implements RendererApi {
3620
3780
  *
3621
3781
  * Not a hot path — it re-uploads the whole image and regenerates the mip chain.
3622
3782
  */
3623
- updateSurfaceTexture(texture: SurfaceTexture, source: TexImageSource): void {
3783
+ updateSurfaceTexture(
3784
+ texture: SurfaceTexture,
3785
+ source: TexImageSource | CompressedTextureSource,
3786
+ ): void {
3624
3787
  if (this.contextLost) return;
3625
- texture.update(this.gl, source);
3788
+ texture.update(this.gl, source, this.compressedFormats);
3789
+ /* New pixels are a new copy for the overlay's slot: see `SurfaceTexture.flatLayer`. */
3790
+ if (texture === this.overlayMaps) this.bindOverlayMaps(texture);
3626
3791
  }
3627
3792
 
3628
3793
  disposeSurfaceTexture(texture: SurfaceTexture): void {
3629
3794
  if (this.contextLost) return;
3795
+ if (texture === this.overlayMaps) this.bindOverlayMaps(null);
3630
3796
  texture.dispose(this.gl);
3631
3797
  this.captureTargets.get(texture)?.dispose(this.gl);
3632
3798
  this.captureTargets.delete(texture);
@@ -3856,6 +4022,13 @@ export class WebGL2Renderer implements RendererApi {
3856
4022
  this.materialDoubleSided = material?.doubleSided === true;
3857
4023
  this.materialHasMaps = materialHasMaps(material);
3858
4024
  this.currentMaterial = material;
4025
+ const reflectivity = material?.reflectivity;
4026
+ const environmentGain = material?.environmentGain;
4027
+ this.materialReflectivity =
4028
+ reflectivity === undefined ? null : Math.min(1, Math.max(0, reflectivity));
4029
+ this.materialEnvironmentGain =
4030
+ environmentGain === undefined ? null : Math.max(0, environmentGain);
4031
+ this.writeEnvironmentDials();
3859
4032
  /* What the blur spreads this skin by: the last material to name a profile says. */
3860
4033
  const model = material?.model;
3861
4034
  warnUnpagedLightmap(model, material?.modelMap);
@@ -3903,7 +4076,8 @@ export class WebGL2Renderer implements RendererApi {
3903
4076
  const v = this.cutoutVector;
3904
4077
  v[0] = material?.cutout ?? 0;
3905
4078
  v[1] = CUTOUT_RESOLVE_CODE[this.cutoutResolveNow(material, translucent)];
3906
- v[2] = this.cutoutFrame;
4079
+ /* z unused: the dither's frame is the pass's, in `uHighlightMin.w`. See `uCutout`. */
4080
+ v[2] = 0;
3907
4081
  /* The material's lighting channels, which the lamps' are compared with. See `uCutout`. */
3908
4082
  v[3] = lightChannelsOf(material?.lightChannels);
3909
4083
  return v;
@@ -4021,6 +4195,8 @@ export class WebGL2Renderer implements RendererApi {
4021
4195
  /* The first physical highlight asked for is when the lit programs start carrying one. */
4022
4196
  if (material?.physicalSpecular === true) this.askLit('PHYSICAL_SPECULAR');
4023
4197
  gl.uniform4fv(u['uModelParams'] ?? null, this.modelParams);
4198
+ /* The draw's overlay, which a program born mid-pass must wear too: see `setSurfaceOverlay`. */
4199
+ gl.uniform4fv(u['uOverlay'] ?? null, this.overlayFloats);
4024
4200
 
4025
4201
  /* The albedo's effects table, or the stand-in the shader reads as none. Before the early
4026
4202
  return, so a material with no albedo still leaves a complete texture on the unit. */
@@ -4115,11 +4291,97 @@ export class WebGL2Renderer implements RendererApi {
4115
4291
  */
4116
4292
  setSurfaceReflectivity(amount: number): void {
4117
4293
  if (this.contextLost) return;
4294
+ this.passReflectivity = Math.min(1, Math.max(0, amount));
4295
+ this.writeEnvironmentDials();
4296
+ }
4297
+
4298
+ /**
4299
+ * The reflectivity and environment gain the setters gave the pass, and the ones the material
4300
+ * set now states, or null where it states none (`SurfaceMaterial.reflectivity`). A draw wears the
4301
+ * material's where it has one and the pass's otherwise.
4302
+ */
4303
+ private passReflectivity = 0;
4304
+ private passEnvironmentGain = 1;
4305
+ private materialReflectivity: number | null = null;
4306
+ private materialEnvironmentGain: number | null = null;
4307
+
4308
+ private writeEnvironmentDials(): void {
4118
4309
  this.materials.dirty();
4119
- this.environmentDials[1] = Math.min(1, Math.max(0, amount));
4310
+ this.environmentDials[1] = this.materialReflectivity ?? this.passReflectivity;
4311
+ this.environmentDials[2] = this.materialEnvironmentGain ?? this.passEnvironmentGain;
4120
4312
  this.writeFlatVector('uEnvironmentDials', this.environmentDials);
4121
4313
  }
4122
4314
 
4315
+ /**
4316
+ * The overlay the following draws wear — a rim of light, a dissolve, wrinkles by region — or null
4317
+ * for none. Pass state, as the material is: `bindMeshPass` puts it back to none. See
4318
+ * `surfaceOverlay.ts` for what it lays and `shaders/flat/overlay.ts` for how.
4319
+ *
4320
+ * **Refused, said once, where the lit stage has no room for it.** Fifteen vectors more than the
4321
+ * program this device fits: counted from the source as `fitFlatProgram` counts, and on a device
4322
+ * whose limit is the 256 an Adreno 740 reports, the eight-light build at 254 has none. The draws
4323
+ * then wear nothing, which is the picture a game without the feature draws. Its atlas is bound as
4324
+ * a 2D copy of its first layer, in the refraction copy's unit (`SurfaceTexture.flatLayer`); a
4325
+ * compressed atlas has no copy and is read as none, also said once.
4326
+ */
4327
+ setSurfaceOverlay(overlay: SurfaceOverlay | null): void {
4328
+ if (this.contextLost) return;
4329
+ let wanted = overlay;
4330
+ if (overlayLays(wanted) && !this.overlayFits()) {
4331
+ if (!this.warnedOverlayRoom) {
4332
+ this.warnedOverlayRoom = true;
4333
+ console.warn(
4334
+ 'Renderer: setSurfaceOverlay is refused on this device. The overlay takes fifteen ' +
4335
+ 'fragment uniform vectors and the lit shader already uses all but ' +
4336
+ `${this.overlayRoomLeft()} of the ${this.fragmentVectorLimit()} this GPU offers; ` +
4337
+ 'draws wear no overlay. A lower `maxLights` makes the room, or WebGPU has it.',
4338
+ );
4339
+ }
4340
+ wanted = null;
4341
+ }
4342
+ packSurfaceOverlay(wanted, this.overlayFloats);
4343
+ if (overlayLays(wanted)) this.askLit('SURFACE_OVERLAY');
4344
+ this.materials.dirty();
4345
+ this.writeFlatVector('uOverlay', this.overlayFloats);
4346
+ this.bindOverlayMaps((wanted?.maps ?? null) as SurfaceTexture | null);
4347
+ }
4348
+
4349
+ /** The overlay's atlas into the refraction copy's unit, as a 2D copy, or the empty texture. */
4350
+ private bindOverlayMaps(maps: SurfaceTexture | null): void {
4351
+ const { gl } = this;
4352
+ this.overlayMaps = maps;
4353
+ this.overlayTexture = maps === null ? null : maps.flatLayer(gl);
4354
+ if (maps !== null && this.overlayTexture === null && !this.warnedOverlayBlocks) {
4355
+ this.warnedOverlayBlocks = true;
4356
+ console.warn(
4357
+ "Renderer: a surface overlay's maps are a compressed texture, which WebGL2 cannot copy " +
4358
+ 'into the 2D image the lit stage reads them from. The overlay is drawn without its ' +
4359
+ 'images; upload the atlas from an image instead.',
4360
+ );
4361
+ }
4362
+ gl.activeTexture(gl.TEXTURE0 + REFRACT_SCENE_TEXTURE_UNIT);
4363
+ gl.bindTexture(gl.TEXTURE_2D, this.overlayTexture ?? this.emptyTexture2D);
4364
+ }
4365
+
4366
+ /** The most fragment uniform vectors this GPU offers a stage. */
4367
+ private fragmentVectorLimit(): number {
4368
+ return this.gl.getParameter(this.gl.MAX_FRAGMENT_UNIFORM_VECTORS) as number;
4369
+ }
4370
+
4371
+ /** What the lit shader at this renderer's budget leaves of the limit, with no overlay. */
4372
+ private overlayRoomLeft(): number {
4373
+ return (
4374
+ this.fragmentVectorLimit() - countUniformVectors(this.flatFragAt(this.lightBudget, false))
4375
+ );
4376
+ }
4377
+
4378
+ /** Whether the lit shader with the overlay compiled in fits this GPU: counted once. */
4379
+ private overlayFits(): boolean {
4380
+ this.overlayRoom ??=
4381
+ countUniformVectors(this.flatFragAt(this.lightBudget, true)) <= this.fragmentVectorLimit();
4382
+ return this.overlayRoom;
4383
+ }
4384
+
4123
4385
  /**
4124
4386
  * How bright the environment the surfaces drawn next reflect is. 1 is the default and identity.
4125
4387
  *
@@ -4134,9 +4396,8 @@ export class WebGL2Renderer implements RendererApi {
4134
4396
  */
4135
4397
  setEnvironmentGain(gain: number): void {
4136
4398
  if (this.contextLost) return;
4137
- this.materials.dirty();
4138
- this.environmentDials[2] = Math.max(0, gain);
4139
- this.writeFlatVector('uEnvironmentDials', this.environmentDials);
4399
+ this.passEnvironmentGain = Math.max(0, gain);
4400
+ this.writeEnvironmentDials();
4140
4401
  }
4141
4402
 
4142
4403
  /**
@@ -4419,8 +4680,104 @@ export class WebGL2Renderer implements RendererApi {
4419
4680
  'locations the joint indices and weights occupy, and this mesh supplies them.',
4420
4681
  );
4421
4682
  }
4683
+ const animation = (options.animation ?? null) as GlBoneAnimation | null;
4684
+ /* The bone a vertex follows is its second coordinates' u, which a mesh carries in its grain
4685
+ lane; a mesh with none has nothing to name a bone by. See `boneAnimation.ts`. */
4686
+ if (animation !== null && !mesh.hasGrain) {
4687
+ throw new Error(
4688
+ 'WebGL2: a bone animation reads each vertex’s bone from the mesh’s second coordinates ' +
4689
+ '(`MeshData.lightmapUvs`), and this mesh has none.',
4690
+ );
4691
+ }
4422
4692
  this.ensureInstancedProgram();
4423
- return new InstancedBatch(this.gl, mesh, capacity, options.cull ?? false);
4693
+ if (animation !== null) this.ensureAnimatedProgram();
4694
+ return new InstancedBatch(this.gl, mesh, capacity, options.cull ?? false, animation);
4695
+ }
4696
+
4697
+ /**
4698
+ * A bone animation an instanced batch can play (`InstancedOptions.animation`): per frame, a turn
4699
+ * and a place for each bone, which every vertex of the batch's mesh follows — the bone its second
4700
+ * coordinates' u names, times `boneScale` — at each instance's own moment of the clip. What a
4701
+ * crowd is made of: every instance moves at the frame's rate on its own clock, in the colour pass
4702
+ * and in every shadow, with nothing on the processor after this. See `boneAnimation.ts` for the
4703
+ * clip's layout and the arithmetic, which both backends run.
4704
+ *
4705
+ * What it gives up against skinning is the blend: a vertex follows one bone. Refused, with what
4706
+ * was wrong, for a clip whose numbers are not its size, or past 2,048 bones or frames.
4707
+ */
4708
+ createBoneAnimation(clip: BoneAnimationClip): BoneAnimationHandle {
4709
+ return new GlBoneAnimation(this.gl, packBoneAnimation(clip));
4710
+ }
4711
+
4712
+ /** Release a clip. A batch still playing it draws nothing, and casts nothing, after. */
4713
+ disposeBoneAnimation(animation: BoneAnimationHandle): void {
4714
+ const clip = animation as GlBoneAnimation;
4715
+ if (this.contextLost || clip.disposed) return;
4716
+ clip.dispose(this.gl);
4717
+ }
4718
+
4719
+ /**
4720
+ * The clock a bone animation plays by this frame, in seconds on the caller's clock: an instance's
4721
+ * moment is this times its rate plus its phase (`MeshInstances.clocks`). **Set it before the
4722
+ * shadows are drawn**, once a frame, as the wind is set: a shadow drawn under one clock and its
4723
+ * caster under another come apart at the frame's rate. 0 until set, which holds every crowd at
4724
+ * the moment its phases name.
4725
+ */
4726
+ setAnimationTime(seconds: number): void {
4727
+ if (Number.isFinite(seconds)) this.animationTime = seconds;
4728
+ }
4729
+
4730
+ /** The clock `setAnimationTime` set. */
4731
+ private animationTime = 0;
4732
+
4733
+ /**
4734
+ * An animated batch's clip, clocks and numbers into whichever program is bound: the three vertex
4735
+ * textures on units of their own (`lightBudget.ts`), the clip's speed and bone scale, the clock.
4736
+ */
4737
+ private bindAnimation(u: Record<string, WebGLUniformLocation>, batch: InstancedBatch): void {
4738
+ const clip = batch.animation;
4739
+ const clocks = batch.clocks;
4740
+ if (clip === null || clocks === null) return;
4741
+ const { gl } = this;
4742
+ gl.activeTexture(gl.TEXTURE0 + BONE_PLACES_TEXTURE_UNIT);
4743
+ gl.bindTexture(gl.TEXTURE_2D, clip.places);
4744
+ gl.activeTexture(gl.TEXTURE0 + BONE_TURNS_TEXTURE_UNIT);
4745
+ gl.bindTexture(gl.TEXTURE_2D, clip.turns);
4746
+ gl.activeTexture(gl.TEXTURE0 + INSTANCE_CLOCKS_TEXTURE_UNIT);
4747
+ gl.bindTexture(gl.TEXTURE_2D, clocks.texture);
4748
+ gl.uniform1i(u['uBonePlaces'] ?? null, BONE_PLACES_TEXTURE_UNIT);
4749
+ gl.uniform1i(u['uBoneTurns'] ?? null, BONE_TURNS_TEXTURE_UNIT);
4750
+ gl.uniform1i(u['uInstanceClocks'] ?? null, INSTANCE_CLOCKS_TEXTURE_UNIT);
4751
+ gl.uniform2f(u['uBoneClip'] ?? null, clip.framesPerSecond, clip.boneScale);
4752
+ gl.uniform1f(u['uSceneTime'] ?? null, this.animationTime);
4753
+ }
4754
+
4755
+ /** The animated instanced program, compiled on first use. See `ensureInstancedProgram`. */
4756
+ private flatAnimatedProgram: WebGLProgram | null = null;
4757
+ private flatAnimatedUniforms: Record<string, WebGLUniformLocation> | null = null;
4758
+
4759
+ /** Compile the animated variant, once, and feed it this frame's pass state. */
4760
+ private ensureAnimatedProgram(): void {
4761
+ if (this.contextLost || this.flatAnimatedProgram !== null) return;
4762
+ const { gl } = this;
4763
+ const program = compileProgram(
4764
+ gl,
4765
+ flatVert({ skinned: false, morphed: false, instanced: true, animated: true }),
4766
+ this.flatFragSource,
4767
+ 'flat.animated',
4768
+ );
4769
+ const uniforms = uniformLocations(gl, program, 'flat.animated');
4770
+ this.flatAnimatedProgram = program;
4771
+ this.flatAnimatedUniforms = uniforms;
4772
+ this.flatTargets.push({ program, uniforms });
4773
+ const camera = this.lastPassCamera;
4774
+ const env = this.lastPassEnv;
4775
+ if (camera !== null && env !== null) {
4776
+ gl.useProgram(program);
4777
+ this.writeMeshPassState(uniforms, camera, env);
4778
+ this.writeMaterialState(uniforms, this.currentMaterial);
4779
+ this.useFlatProgram();
4780
+ }
4424
4781
  }
4425
4782
 
4426
4783
  /** Push placement and colour. Only the live prefix. */
@@ -4483,18 +4840,29 @@ export class WebGL2Renderer implements RendererApi {
4483
4840
  order. See `InstancedOptions.cull`. */
4484
4841
  const culling = !blend && batch.cull;
4485
4842
  if (culling && !batchBoxVisible(batch.box, this.frustum, this.occlusion)) return;
4843
+ /* A batch playing a bone animation takes the animated variant; a disposed clip draws nothing. */
4844
+ const clip = batch.animation;
4845
+ if (clip !== null && clip.disposed) return;
4486
4846
  this.drawBudget.ask();
4487
4847
  this.ensureInstancedProgram();
4848
+ if (clip !== null) this.ensureAnimatedProgram();
4488
4849
  /* A material with a model takes that model's instanced program; see `modelProgram`. */
4489
4850
  const modelKind = this.currentMaterial?.model?.kind ?? null;
4490
- const modelled = modelKind === null ? null : this.modelProgram('instanced', modelKind);
4491
- const program = modelled?.program ?? this.flatInstancedProgram;
4492
- const u = modelled?.uniforms ?? this.flatInstancedUniforms;
4851
+ const modelled =
4852
+ modelKind === null
4853
+ ? null
4854
+ : this.modelProgram(clip === null ? 'instanced' : 'animated', modelKind);
4855
+ const program =
4856
+ modelled?.program ?? (clip === null ? this.flatInstancedProgram : this.flatAnimatedProgram);
4857
+ const u =
4858
+ modelled?.uniforms ??
4859
+ (clip === null ? this.flatInstancedUniforms : this.flatAnimatedUniforms);
4493
4860
  if (program === null || u === null) return;
4494
4861
 
4495
4862
  const { gl } = this;
4496
4863
  gl.useProgram(program);
4497
4864
  gl.uniform1i(u['uHasTangents'] ?? null, mesh.hasTangents ? 1 : 0);
4865
+ if (clip !== null) this.bindAnimation(u, batch);
4498
4866
 
4499
4867
  const lit = options.lit ?? true;
4500
4868
  /* Added light fades in the medium rather than receding into it. See `drawFog.ts`. */
@@ -4555,8 +4923,16 @@ export class WebGL2Renderer implements RendererApi {
4555
4923
  if (coverage) gl.enable(gl.SAMPLE_ALPHA_TO_COVERAGE);
4556
4924
  if (ditheredBlend)
4557
4925
  gl.uniform4fv(u['uCutout'] ?? null, this.cutoutUniform(this.currentMaterial, true));
4558
- if (culling && cullsInstances(count, mesh.indexCount)) batch.drawCulled(gl, data, this.frustum);
4559
- else batch.draw(gl, count);
4926
+ const keeps = !blend && this.reflectionKeeps();
4927
+ if (keeps) this.reflectionTargets?.on();
4928
+ /* An animated batch reads its clocks by instance index, which the culled copy would reorder:
4929
+ it is culled whole and never instance by instance. */
4930
+ if (culling && clip === null && cullsInstances(count, mesh.indexCount)) {
4931
+ batch.drawCulled(gl, data, this.frustum);
4932
+ } else {
4933
+ batch.draw(gl, count);
4934
+ }
4935
+ if (keeps) this.reflectionTargets?.off();
4560
4936
  if (coverage) gl.disable(gl.SAMPLE_ALPHA_TO_COVERAGE);
4561
4937
  if (ditheredBlend)
4562
4938
  gl.uniform4fv(u['uCutout'] ?? null, this.cutoutUniform(this.currentMaterial, false));
@@ -4617,7 +4993,9 @@ export class WebGL2Renderer implements RendererApi {
4617
4993
  glass.transmission,
4618
4994
  glass.frost,
4619
4995
  );
4620
- gl.uniform3fv(u['uRefractTint'] ?? null, options.refractTint ?? WHITE_TINT);
4996
+ /* w: this framebuffer's first row is the bottom, so up the screen is up the rows. */
4997
+ const tint = options.refractTint ?? WHITE_TINT;
4998
+ gl.uniform4f(u['uRefractTint'] ?? null, tint[0] ?? 1, tint[1] ?? 1, tint[2] ?? 1, 1);
4621
4999
  gl.uniform3fv(u['uGlassTint'] ?? null, glass.tint);
4622
5000
  return true;
4623
5001
  }
@@ -4627,7 +5005,8 @@ export class WebGL2Renderer implements RendererApi {
4627
5005
  const { gl } = this;
4628
5006
  gl.uniform4f(u['uSeeThrough'] ?? null, 0, 0, 0, 0);
4629
5007
  gl.activeTexture(gl.TEXTURE0 + REFRACT_SCENE_TEXTURE_UNIT);
4630
- gl.bindTexture(gl.TEXTURE_2D, this.emptyTexture2D);
5008
+ /* The overlay's atlas, where one is held: it shares the unit. See `setSurfaceOverlay`. */
5009
+ gl.bindTexture(gl.TEXTURE_2D, this.overlayTexture ?? this.emptyTexture2D);
4631
5010
  }
4632
5011
 
4633
5012
  disposeMesh(mesh: Mesh): void {
@@ -4872,7 +5251,7 @@ export class WebGL2Renderer implements RendererApi {
4872
5251
  */
4873
5252
  private gradeCode(): number {
4874
5253
  return this.sceneTarget === null || !this.quality.hdrScene
4875
- ? OUTPUT_TRANSFORM_CODE[this.quality.outputTransform]
5254
+ ? forwardTransformCode(this.quality.outputTransform)
4876
5255
  : 0;
4877
5256
  }
4878
5257
 
@@ -5927,6 +6306,71 @@ export class WebGL2Renderer implements RendererApi {
5927
6306
  casters(this.casterSink);
5928
6307
  }
5929
6308
 
6309
+ /**
6310
+ * Draws that do not change between frames, enumerated once and kept: what a stage, a set or any
6311
+ * world that stands still draws, recorded so a frame replays it rather than issuing it.
6312
+ *
6313
+ * **The same enumeration `drawSceneCasters` takes**, called once, here: rigid meshes and instanced
6314
+ * batches, each with its material. A skinned mesh or a scatter batch is refused by name, since
6315
+ * both change every frame. What it captures is what the enumeration said at the time — the
6316
+ * matrices and the instance counts are copied, so a caller's scratch is free to move on — while
6317
+ * the meshes, batches, textures and materials stay the caller's: a list must be disposed, or made
6318
+ * again, before anything it holds is, and made again when a material it holds is changed.
6319
+ *
6320
+ * **What it is for is the frame's processor time.** WebGPU records the list into a render bundle
6321
+ * per view and replays it with one call, where a draw issued a frame at a time costs its encoding
6322
+ * every frame and again for every view that draws it. This backend has no bundles and replays the
6323
+ * entries through the scene-caster path every frame, at the cost a frame drawing them itself pays;
6324
+ * the picture is the same.
6325
+ *
6326
+ * What a list gives up against drawing each frame is culling: every entry is drawn in every view,
6327
+ * because a bundle cannot leave one out. A world split into lists by region culls by list.
6328
+ */
6329
+ createStaticDraws(casters: ShadowCasters): StaticDrawsHandle {
6330
+ return captureStaticDraws(casters);
6331
+ }
6332
+
6333
+ /**
6334
+ * Draw a list into the open mesh pass, under the camera, the lights and the fog `bindMeshPass`
6335
+ * set — the frame's, a mirror's, a capture's. Opaque draws, through each entry's material.
6336
+ *
6337
+ * **Its draws are rigid and wear no overlay**, so a skin palette, morph weights, cloth or a surface
6338
+ * overlay set before it is put back to none, as `bindMeshPass` puts an overlay back; and no
6339
+ * material is left set after it, which is what `setMaterial(null)` leaves. The state a pass set —
6340
+ * the grain, the relief, the fog, the grade — is the list's to draw with, as it is any draw's, and
6341
+ * a list drawn under a different one is recorded again for it on WebGPU.
6342
+ */
6343
+ drawStaticDraws(draws: StaticDrawsHandle): void {
6344
+ if (this.contextLost) return;
6345
+ const list = draws as StaticDrawsList;
6346
+ if (list.disposed) {
6347
+ this.warnStaticDisposed();
6348
+ return;
6349
+ }
6350
+ this.setSkinPalette(null);
6351
+ this.setMorphWeights(null);
6352
+ this.setCloth(null);
6353
+ this.setSurfaceOverlay(null);
6354
+ this.setMaterial(null);
6355
+ this.sceneCasterMaterial = undefined;
6356
+ replayStaticDraws(list, this.sceneCasterSink);
6357
+ this.setMaterial(null);
6358
+ this.sceneCasterMaterial = undefined;
6359
+ }
6360
+
6361
+ /** Release a list. Its meshes, batches and materials are the caller's and are not released. */
6362
+ disposeStaticDraws(draws: StaticDrawsHandle): void {
6363
+ (draws as StaticDrawsList).disposed = true;
6364
+ }
6365
+
6366
+ /** A disposed list drawn, said once: the frame loop does not throw. */
6367
+ private warnStaticDisposed(): void {
6368
+ if (this.warnedStaticDisposed) return;
6369
+ this.warnedStaticDisposed = true;
6370
+ console.warn('drawStaticDraws: this list was disposed, and draws nothing.');
6371
+ }
6372
+ private warnedStaticDisposed = false;
6373
+
5930
6374
  /**
5931
6375
  * Draw the pass's glass twice: where the nearest pane is, and what the panes let through.
5932
6376
  *
@@ -6116,7 +6560,7 @@ export class WebGL2Renderer implements RendererApi {
6116
6560
  * throws a sentence with the device's numbers in it rather than a bare driver string.
6117
6561
  */
6118
6562
  /** The lit fragment source this renderer's profile compiles, at a light budget. */
6119
- private flatFragAt(budget: LightBudget): string {
6563
+ private flatFragAt(budget: LightBudget, overlay = this.litOn.SURFACE_OVERLAY): string {
6120
6564
  return flatFrag({
6121
6565
  pointShadows: this.quality.pointShadows,
6122
6566
  directionalShadows: this.quality.directionalShadows,
@@ -6129,6 +6573,8 @@ export class WebGL2Renderer implements RendererApi {
6129
6573
  surfaceEffects: this.litOn.SURFACE_EFFECTS,
6130
6574
  driftLight: this.litOn.DRIFT_LIGHT,
6131
6575
  physicalSpecular: this.litOn.PHYSICAL_SPECULAR,
6576
+ surfaceOverlay: overlay,
6577
+ reflectionMaps: this.litOn.REFLECTION_MAPS,
6132
6578
  maxLights: budget.maxLights,
6133
6579
  maxAreaLights: budget.maxAreaLights,
6134
6580
  });
@@ -6153,9 +6599,10 @@ export class WebGL2Renderer implements RendererApi {
6153
6599
  instanced: boolean,
6154
6600
  label: string,
6155
6601
  eight = false,
6602
+ animated = false,
6156
6603
  ): { program: WebGLProgram; uniforms: Record<string, WebGLUniformLocation> } | null => {
6157
6604
  if (old === null) return null;
6158
- const vertex = flatVert({ skinned, morphed, instanced });
6605
+ const vertex = flatVert({ skinned, morphed, instanced, animated });
6159
6606
  const program = compileProgram(
6160
6607
  gl,
6161
6608
  eight ? skinEight(vertex) : vertex,
@@ -6188,6 +6635,17 @@ export class WebGL2Renderer implements RendererApi {
6188
6635
  const instanced = recompile(this.flatInstancedProgram, false, false, true, 'flat.instanced');
6189
6636
  this.flatInstancedProgram = instanced?.program ?? null;
6190
6637
  this.flatInstancedUniforms = instanced?.uniforms ?? null;
6638
+ const animated = recompile(
6639
+ this.flatAnimatedProgram,
6640
+ false,
6641
+ false,
6642
+ true,
6643
+ 'flat.animated',
6644
+ false,
6645
+ true,
6646
+ );
6647
+ this.flatAnimatedProgram = animated?.program ?? null;
6648
+ this.flatAnimatedUniforms = animated?.uniforms ?? null;
6191
6649
 
6192
6650
  for (const [key, entry] of this.clothFlat) {
6193
6651
  const eight = key.startsWith('8');
@@ -6218,7 +6676,7 @@ export class WebGL2Renderer implements RendererApi {
6218
6676
  }
6219
6677
 
6220
6678
  this.flatTargets.length = 0;
6221
- for (const target of [plain, skinned, morphed, both, skinned8, both8, instanced]) {
6679
+ for (const target of [plain, skinned, morphed, both, skinned8, both8, instanced, animated]) {
6222
6680
  if (target !== null) this.flatTargets.push(target);
6223
6681
  }
6224
6682
  for (const target of this.clothFlat.values()) this.flatTargets.push(target);
@@ -6954,11 +7412,22 @@ export class WebGL2Renderer implements RendererApi {
6954
7412
  * Set before `beginFrame` and held until changed, like `setSpeedRush`, which it sits
6955
7413
  * beside for the same reason: both describe how the finished image is presented rather
6956
7414
  * than anything in the world.
7415
+ *
7416
+ * `maxShare` is the longest smear, as a share of the frame (0.03 unless given, at most half): a
7417
+ * fast turn softens the frame rather than erasing it. **Each drawn object's own motion is
7418
+ * followed only under a reconstruction on WebGPU**, which draws the motion target it reads; here,
7419
+ * and on WebGPU without one, the camera's motion alone smears the frame, and a draw that names
7420
+ * where it was last frame is told so once.
6957
7421
  */
6958
- setCameraMotionBlur(scale: number): void {
7422
+ setCameraMotionBlur(scale: number, maxShare = MOTION_BLUR_MAX_UV): void {
6959
7423
  this.motionBlurScale = Math.min(Math.max(scale, 0), 1);
7424
+ this.motionBlurMax = clampMotionShare(maxShare);
6960
7425
  }
6961
7426
 
7427
+ /** The longest smear, as a share of the frame. See `setCameraMotionBlur`. */
7428
+ private motionBlurMax = MOTION_BLUR_MAX_UV;
7429
+ private warnedObjectBlur = false;
7430
+
6962
7431
  /**
6963
7432
  * The camera has cut: this frame is a new shot, not the next moment of the last one.
6964
7433
  *
@@ -7170,6 +7639,47 @@ export class WebGL2Renderer implements RendererApi {
7170
7639
  : this.quality.outputExposure;
7171
7640
  }
7172
7641
 
7642
+ /**
7643
+ * The `filmic` curve this and every later frame is graded through, until it is set again: a
7644
+ * grading volume's five numbers, solved once here into the constants the resolve reads. Ignored
7645
+ * unless `outputTransform` is `filmic`. A number out of range is clamped into it and said once,
7646
+ * since a frame loop must not throw. See `filmicCurve.ts`.
7647
+ */
7648
+ setFilmicCurve(curve: FilmicCurve): void {
7649
+ if (resolveFilmicCurve(curve, this.filmConstants) && !this.warnedFilmClamped) {
7650
+ this.warnedFilmClamped = true;
7651
+ console.warn(FILMIC_CLAMPED);
7652
+ }
7653
+ }
7654
+
7655
+ private warnedFilmClamped = false;
7656
+
7657
+ /**
7658
+ * The range the frame goes out in: `'high'` where the profile asked for `highDynamicRange`, the
7659
+ * frame holds light past white and the display shows it — on WebGPU — and `'standard'` otherwise.
7660
+ * **Always standard here**: this backend draws into a canvas of eight bits a channel, which has
7661
+ * nothing above white to show. Typed as the question rather than this backend's answer, because
7662
+ * the shared surface is derived from this class.
7663
+ */
7664
+ readonly displayRange: DisplayRange = 'standard';
7665
+
7666
+ /** Why it is that range, in words, for a game choosing its look and for a bug report. */
7667
+ get displayRangeReason(): string {
7668
+ return this.quality.highDynamicRange
7669
+ ? 'WebGL2 draws into an eight-bit canvas, which has no range above white'
7670
+ : 'highDynamicRange was not asked for';
7671
+ }
7672
+
7673
+ /**
7674
+ * How bright paper white and the display's peak are, in one unit, held until changed: their
7675
+ * ratio is how far above white the highlights may run where `displayRange` is `'high'`, and
7676
+ * nothing changes where it is standard, which is always here.
7677
+ */
7678
+ setDisplayLuminance(paperWhite: number, peak: number): void {
7679
+ void paperWhite;
7680
+ void peak;
7681
+ }
7682
+
7173
7683
  /**
7174
7684
  * Composite a flat colour over the finished frame — for a cut dipping to white or to black.
7175
7685
  *
@@ -7261,6 +7771,21 @@ export class WebGL2Renderer implements RendererApi {
7261
7771
  this.sceneTarget.setVignette(strength);
7262
7772
  }
7263
7773
 
7774
+ /**
7775
+ * The lens's colour fringe, held until changed: `intensity` is a percentage, 0 is none, and red
7776
+ * and green are pulled toward the centre by their wavelength's distance from blue, growing from
7777
+ * `start` — a share of the half-frame, 0 the centre — to the whole of it at the edge. Applied to
7778
+ * scene light at the composite's first read. Needs `screenEffects`, and says so once rather than
7779
+ * doing nothing. See `shaders/fringe.ts`.
7780
+ */
7781
+ setChromaticAberration(intensity: number, start = 0): void {
7782
+ if (this.sceneTarget === null) {
7783
+ this.warnLookWithoutComposite(intensity);
7784
+ return;
7785
+ }
7786
+ this.sceneTarget.setChromaticAberration(intensity, start);
7787
+ }
7788
+
7264
7789
  /**
7265
7790
  * Film grain, held until changed: an amplitude in display values (0 is none, 0.03 is a fine
7266
7791
  * grain) and this frame's seed. **The seed is the caller's**, because the engine takes time from
@@ -7451,6 +7976,8 @@ export class WebGL2Renderer implements RendererApi {
7451
7976
 
7452
7977
  beginFrame(clearColor: Vec3): void {
7453
7978
  if (this.contextLost) return;
7979
+ /* One frame of the jitter's sequence however many times the frame binds its camera. */
7980
+ this.temporalHistory.nextFrame();
7454
7981
  this.skinScattered = false;
7455
7982
  this.skinScatter?.reset();
7456
7983
  if (this.litAsked) {
@@ -7520,6 +8047,8 @@ export class WebGL2Renderer implements RendererApi {
7520
8047
  }
7521
8048
 
7522
8049
  this.sceneTarget?.begin(this.canvas.width, this.canvas.height);
8050
+ /* The materials' maps on the scene's framebuffer, cleared and disabled: `reflectionKeeps`. */
8051
+ this.reflectionTargets?.begin(this.canvas.width, this.canvas.height);
7523
8052
  gl.clearColor(clearColor[0], clearColor[1], clearColor[2], 1);
7524
8053
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
7525
8054
  }
@@ -7576,7 +8105,7 @@ export class WebGL2Renderer implements RendererApi {
7576
8105
  motion = {
7577
8106
  reprojection: this.reprojection as Float32Array,
7578
8107
  strength: blur,
7579
- max: MOTION_BLUR_MAX_UV,
8108
+ max: this.motionBlurMax,
7580
8109
  };
7581
8110
  }
7582
8111
  }
@@ -7668,7 +8197,11 @@ export class WebGL2Renderer implements RendererApi {
7668
8197
 
7669
8198
  /* The grade travels to the resolve only where the mesh pass gave it up. See `bindMeshPass`. */
7670
8199
  const lateGrade = this.quality.hdrScene
7671
- ? { transform: OUTPUT_TRANSFORM_CODE[this.quality.outputTransform], exposure: this.exposure }
8200
+ ? {
8201
+ transform: OUTPUT_TRANSFORM_CODE[this.quality.outputTransform] ?? 0,
8202
+ exposure: this.exposure,
8203
+ film: this.filmConstants,
8204
+ }
7672
8205
  : { transform: 0, exposure: 1 };
7673
8206
  /*
7674
8207
  * Depth of field, on the same ceiling-and-dial split as bloom above: `depthOfField` decides
@@ -7744,6 +8277,49 @@ export class WebGL2Renderer implements RendererApi {
7744
8277
  * drawn over a reflective floor rather than under it, and before the post chain so bloom and
7745
8278
  * the grade see a reflection exactly as they would see any other part of the picture.
7746
8279
  */
8280
+ /*
8281
+ * **The frame's materials' reflections first**, on the same terms: every pixel the opaque lit
8282
+ * draws left a reflection for, marched and swapped for the probe's share. The maps leave the
8283
+ * scene's framebuffer before the resolve draws into it while sampling them.
8284
+ */
8285
+ const reflectionTargets = this.reflectionTargets;
8286
+ const probeMap = reflectionTargets?.probeTexture ?? null;
8287
+ const tintMap = reflectionTargets?.tintTexture ?? null;
8288
+ if (
8289
+ reflectionTargets !== null &&
8290
+ this.frameReflections !== null &&
8291
+ probeMap !== null &&
8292
+ tintMap !== null &&
8293
+ this.frameViewProj !== null &&
8294
+ this.sceneTarget !== null
8295
+ ) {
8296
+ const scene = this.sceneTarget.colorAttachment();
8297
+ const depth = this.sceneTarget.snapshotDepth(true);
8298
+ if (scene !== null && depth !== null) {
8299
+ this.ssr ??= new SsrPass(this.gl, this.sceneTarget.keepsRange);
8300
+ this.ssr.traceMaterials(
8301
+ scene,
8302
+ depth,
8303
+ probeMap,
8304
+ tintMap,
8305
+ this.frameViewProj,
8306
+ this.frameEye,
8307
+ this.canvas.width,
8308
+ this.canvas.height,
8309
+ REFLECTION_EDGE_FADE,
8310
+ this.frameReflections,
8311
+ );
8312
+ this.sceneTarget.bind();
8313
+ reflectionTargets.detach();
8314
+ this.ssr.resolveMaterials(
8315
+ probeMap,
8316
+ tintMap,
8317
+ this.canvas.width,
8318
+ this.canvas.height,
8319
+ this.frameReflections,
8320
+ );
8321
+ }
8322
+ }
7747
8323
  if (this.reflectionQueue.length > 0 && this.frameViewProj !== null) {
7748
8324
  const scene = this.sceneTarget?.colorAttachment() ?? null;
7749
8325
  const depth = this.sceneTarget?.snapshotDepth(true) ?? null;
@@ -8127,9 +8703,17 @@ export class WebGL2Renderer implements RendererApi {
8127
8703
  this.materialHasMaps = false;
8128
8704
  this.currentMaterial = null;
8129
8705
  this.currentSurfaceTexture = null;
8706
+ /* Nor an overlay: the atlas's unit is given the empty texture with the refraction copy's. */
8707
+ this.overlayFloats.fill(0);
8708
+ this.overlayMaps = null;
8709
+ this.overlayTexture = null;
8130
8710
  /* Nor an ambient of its own: the frame's, until a draw asks. See `setAmbientSH`. */
8131
8711
  this.ambientSH.fill(0);
8132
8712
  /* No reflection and an environment at its own brightness; the emission the world asks for. */
8713
+ this.passReflectivity = 0;
8714
+ this.passEnvironmentGain = 1;
8715
+ this.materialReflectivity = null;
8716
+ this.materialEnvironmentGain = null;
8133
8717
  this.environmentDials[1] = 0;
8134
8718
  this.environmentDials[2] = 1;
8135
8719
  this.emission[0] = env.emissiveGain;
@@ -8283,12 +8867,14 @@ export class WebGL2Renderer implements RendererApi {
8283
8867
  * unless a draw sets the strength above zero.
8284
8868
  */
8285
8869
  gl.activeTexture(gl.TEXTURE0 + REFRACT_SCENE_TEXTURE_UNIT);
8286
- gl.bindTexture(gl.TEXTURE_2D, this.emptyTexture2D);
8870
+ gl.bindTexture(gl.TEXTURE_2D, this.overlayTexture ?? this.emptyTexture2D);
8287
8871
  gl.uniform1i(u['uRefractScene'] ?? null, REFRACT_SCENE_TEXTURE_UNIT);
8872
+ /* The draws' overlay, none at a pass's start, and its atlas above: see `setSurfaceOverlay`. */
8873
+ gl.uniform4fv(u['uOverlay'] ?? null, this.overlayFloats);
8288
8874
  /* Neither refracting nor glass, which is every draw that does not say otherwise. See
8289
8875
  `bindSeeThrough` for the four numbers in the one vector. */
8290
8876
  gl.uniform4f(u['uSeeThrough'] ?? null, 0, 0, 0, 0);
8291
- gl.uniform3fv(u['uRefractTint'] ?? null, WHITE_TINT);
8877
+ gl.uniform4f(u['uRefractTint'] ?? null, 1, 1, 1, 1);
8292
8878
  gl.uniform3fv(u['uGlassTint'] ?? null, WHITE_TINT);
8293
8879
  gl.uniform3fv(u['uDirectionalDir'] ?? null, env.directionalDir);
8294
8880
  gl.uniform3fv(u['uDirectionalColor'] ?? null, env.directionalColor);
@@ -8411,7 +8997,16 @@ export class WebGL2Renderer implements RendererApi {
8411
8997
  gl.uniform1i(u['uPeeledShadowEnabled'] ?? null, this.peelFilled ? 1 : 0);
8412
8998
  gl.uniform1f(u['uShadowMaxSlope'] ?? null, this.quality.directionalShadowMaxSlope);
8413
8999
  gl.uniform1i(u['uShadowFilterTaps'] ?? null, this.quality.shadowFilterTaps);
8414
- gl.uniform3fv(u['uHighlightMin'] ?? null, env.highlightMin);
9000
+ /* The near corner, and in its w the frame the cutout dither moves with. See `uHighlightMin`.
9001
+ An environment written by hand may leave the box out, as the far corner says below. */
9002
+ const min = env.highlightMin as ArrayLike<number> | undefined;
9003
+ gl.uniform4f(
9004
+ u['uHighlightMin'] ?? null,
9005
+ min?.[0] ?? 0,
9006
+ min?.[1] ?? 0,
9007
+ min?.[2] ?? 0,
9008
+ this.cutoutFrame,
9009
+ );
8415
9010
  /* The far corner, and the gain in its w. See `uHighlightMax`. */
8416
9011
  /* An environment written by hand may leave the box out; then there is none, as before. */
8417
9012
  const max = env.highlightMax ?? NO_HIGHLIGHT;
@@ -8693,9 +9288,18 @@ export class WebGL2Renderer implements RendererApi {
8693
9288
  * here, so there is no motion target for this to be written into, and a mover passed here is
8694
9289
  * neither read nor advanced.
8695
9290
  */
8696
- _previousModel: ReadonlyMat4 | Mover | null = null,
9291
+ previousModel: ReadonlyMat4 | Mover | null = null,
8697
9292
  ): void {
8698
9293
  if (this.contextLost) return;
9294
+ /* A mover's own motion is not blurred here: see `setCameraMotionBlur`. Said once. */
9295
+ if (
9296
+ previousModel !== null &&
9297
+ !this.warnedObjectBlur &&
9298
+ this.quality.cameraMotionBlur * this.motionBlurScale > 0
9299
+ ) {
9300
+ this.warnedObjectBlur = true;
9301
+ console.warn(OBJECT_BLUR_UNKEPT);
9302
+ }
8699
9303
  /* Geometry that has not all arrived is not drawn, nor a mesh with none. See `Mesh.complete`
8700
9304
  and `Mesh.indexCount`. */
8701
9305
  if (!mesh.complete || mesh.indexCount === 0) return;
@@ -8726,7 +9330,11 @@ export class WebGL2Renderer implements RendererApi {
8726
9330
  * light and its colour, each into the blur's own target.
8727
9331
  */
8728
9332
  const splits = this.skinSplits();
9333
+ /* An opaque lit draw into the frame writes its material's reflection too: see `reflectionKeeps`. */
9334
+ const keeps = this.reflectionKeeps();
9335
+ if (keeps) this.reflectionTargets?.on();
8729
9336
  this.drawMeshHalf(mesh, model, depthLayer, tint, splits ? 'scene' : 'whole');
9337
+ if (keeps) this.reflectionTargets?.off();
8730
9338
  if (splits && this.skinScatter !== null) {
8731
9339
  this.skinScatter.begin();
8732
9340
  this.drawMeshHalf(mesh, model, depthLayer, tint, 'diffuse');
@@ -8741,6 +9349,21 @@ export class WebGL2Renderer implements RendererApi {
8741
9349
  * skin yet, this is the frame's own picture — not a mirror's or a probe's, nor an overlay after
8742
9350
  * the frame — and the blur's target can be had.
8743
9351
  */
9352
+ /**
9353
+ * Whether a draw now is the frame's own, opaque and lit, and so writes the two maps beside its
9354
+ * colour: not a mirror's, a probe's or a capture's, whose framebuffers have no maps, and not after
9355
+ * the frame. See `reflectionSurfaceTargets.ts`.
9356
+ */
9357
+ private reflectionKeeps(): boolean {
9358
+ return (
9359
+ this.reflectionTargets !== null &&
9360
+ !this.reflectionPassActive &&
9361
+ !this.probePassActive &&
9362
+ !this.capturePassActive &&
9363
+ !this.framePresented
9364
+ );
9365
+ }
9366
+
8744
9367
  private skinSplits(): boolean {
8745
9368
  const skin = this.skinScatter;
8746
9369
  const scene = this.sceneTarget;
@@ -8769,10 +9392,16 @@ export class WebGL2Renderer implements RendererApi {
8769
9392
  const scene = this.sceneTarget;
8770
9393
  const projection = this.frameProjection;
8771
9394
  if (skin === null || scene === null || this.skinScattered) return;
9395
+ /*
9396
+ * **Not the frame's picture, so not the frame's moment**: a blended draw in a mirror, a probe
9397
+ * bake or a scene capture lands over none of the frame's skin. Latching there switched the
9398
+ * spread off for the frame, so a capture with a glowing sign in it left every face drawn after
9399
+ * it flat. `scripts/capture-check.mjs`.
9400
+ */
9401
+ if (this.reflectionPassActive || this.probePassActive || this.capturePassActive) return;
8772
9402
  /* Latched whether or not skin was drawn: a skin drawn after this point is the whole surface. */
8773
9403
  this.skinScattered = true;
8774
9404
  if (!skin.pending) return;
8775
- if (this.reflectionPassActive || this.probePassActive || this.capturePassActive) return;
8776
9405
  if (projection === null) return;
8777
9406
  const depth = scene.snapshotDepth(true);
8778
9407
  mat4.invert(this.skinInverseProjection, projection);
@@ -9839,6 +10468,12 @@ function litVertexOf(variant: string): string {
9839
10468
  }
9840
10469
  const skinned = variant.startsWith('skinned') || variant.startsWith('both');
9841
10470
  const morphed = variant === 'morphed' || variant.startsWith('both');
9842
- const vertex = flatVert({ skinned, morphed, instanced: variant === 'instanced' });
10471
+ const animated = variant === 'animated';
10472
+ const vertex = flatVert({
10473
+ skinned,
10474
+ morphed,
10475
+ instanced: variant === 'instanced' || animated,
10476
+ animated,
10477
+ });
9843
10478
  return variant.endsWith('8') ? skinEight(vertex) : vertex;
9844
10479
  }