@driftengine/core 4.8.5 → 4.8.7

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 (214) hide show
  1. package/CHANGELOG.json +4392 -4250
  2. package/README.md +1 -1
  3. package/dist/index.d.ts +12 -4
  4. package/dist/index.js +12 -2
  5. package/dist/render/ambientHarmonics.d.ts +46 -0
  6. package/dist/render/ambientHarmonics.js +80 -0
  7. package/dist/render/anisotropicLobe.d.ts +10 -0
  8. package/dist/render/anisotropicLobe.js +19 -0
  9. package/dist/render/areaLights.d.ts +54 -0
  10. package/dist/render/areaLights.js +55 -0
  11. package/dist/render/backend/api.d.ts +43 -2
  12. package/dist/render/backend/webgl2/instanced.d.ts +2 -0
  13. package/dist/render/backend/webgl2/instanced.js +11 -2
  14. package/dist/render/backend/webgl2/renderer.d.ts +120 -11
  15. package/dist/render/backend/webgl2/renderer.js +356 -53
  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/webgl2/skinScatter.d.ts +14 -0
  19. package/dist/render/backend/webgl2/skinScatter.js +88 -26
  20. package/dist/render/backend/webgpu/buffers.js +2 -1
  21. package/dist/render/backend/webgpu/clusterBinner.js +3 -2
  22. package/dist/render/backend/webgpu/flatPass.d.ts +7 -5
  23. package/dist/render/backend/webgpu/flatPass.js +15 -13
  24. package/dist/render/backend/webgpu/particlePass.d.ts +37 -4
  25. package/dist/render/backend/webgpu/particlePass.js +81 -12
  26. package/dist/render/backend/webgpu/postPass.d.ts +6 -1
  27. package/dist/render/backend/webgpu/postPass.js +7 -2
  28. package/dist/render/backend/webgpu/renderer.d.ts +93 -8
  29. package/dist/render/backend/webgpu/renderer.js +483 -93
  30. package/dist/render/backend/webgpu/sceneCaptureTarget.d.ts +24 -0
  31. package/dist/render/backend/webgpu/sceneCaptureTarget.js +59 -0
  32. package/dist/render/backend/webgpu/shaders/clusterBin.wgsl.d.ts +1 -1
  33. package/dist/render/backend/webgpu/shaders/clusterBin.wgsl.js +41 -1
  34. package/dist/render/backend/webgpu/skinPaletteRing.d.ts +40 -28
  35. package/dist/render/backend/webgpu/skinPaletteRing.js +69 -32
  36. package/dist/render/backend/webgpu/skinScatterPass.d.ts +10 -2
  37. package/dist/render/backend/webgpu/skinScatterPass.js +59 -9
  38. package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +5 -1
  39. package/dist/render/backend/webgpu/surfaceTexturePass.js +45 -5
  40. package/dist/render/backend/webgpu/waterPass.d.ts +1 -1
  41. package/dist/render/bloomChain.d.ts +44 -0
  42. package/dist/render/bloomChain.js +28 -0
  43. package/dist/render/bloomPass.d.ts +3 -1
  44. package/dist/render/bloomPass.js +16 -2
  45. package/dist/render/clusteredLights.d.ts +78 -6
  46. package/dist/render/clusteredLights.js +214 -13
  47. package/dist/render/driftLight/bake.d.ts +2 -0
  48. package/dist/render/driftLight/bake.js +1 -1
  49. package/dist/render/driftLight/falloff.d.ts +4 -2
  50. package/dist/render/driftLight/falloff.js +8 -2
  51. package/dist/render/fixtureAtlas.js +1 -1
  52. package/dist/render/gi/bounceLights.d.ts +3 -2
  53. package/dist/render/gi/bounceLights.js +8 -3
  54. package/dist/render/instanceCull.js +12 -0
  55. package/dist/render/instances.d.ts +47 -11
  56. package/dist/render/instances.js +24 -9
  57. package/dist/render/lightBudget.d.ts +38 -2
  58. package/dist/render/lightBudget.js +48 -5
  59. package/dist/render/lightmap.d.ts +116 -0
  60. package/dist/render/lightmap.js +140 -0
  61. package/dist/render/mesh.js +2 -1
  62. package/dist/render/particleBatch.d.ts +59 -3
  63. package/dist/render/particleBatch.js +53 -2
  64. package/dist/render/particleMaterial.d.ts +6 -1
  65. package/dist/render/particleMaterial.js +10 -2
  66. package/dist/render/particlePool.d.ts +32 -0
  67. package/dist/render/particlePool.js +9 -0
  68. package/dist/render/particleSort.d.ts +34 -0
  69. package/dist/render/particleSort.js +96 -0
  70. package/dist/render/pointLightSelection.d.ts +13 -0
  71. package/dist/render/pointLightSelection.js +3 -1
  72. package/dist/render/renderQuality.d.ts +8 -7
  73. package/dist/render/rendererHarness.js +3 -0
  74. package/dist/render/rgb9e5.d.ts +22 -0
  75. package/dist/render/rgb9e5.js +47 -0
  76. package/dist/render/sceneCapture.d.ts +29 -0
  77. package/dist/render/sceneCapture.js +36 -0
  78. package/dist/render/sceneTarget.d.ts +5 -0
  79. package/dist/render/sceneTarget.js +8 -1
  80. package/dist/render/screenSpaceReflection.d.ts +27 -1
  81. package/dist/render/screenSpaceReflection.js +9 -0
  82. package/dist/render/shaders/bloom.d.ts +8 -4
  83. package/dist/render/shaders/bloom.js +20 -3
  84. package/dist/render/shaders/bolt.d.ts +1 -1
  85. package/dist/render/shaders/bolt.js +7 -1
  86. package/dist/render/shaders/depth.js +11 -0
  87. package/dist/render/shaders/flat/anisotropic.d.ts +1 -1
  88. package/dist/render/shaders/flat/anisotropic.js +37 -3
  89. package/dist/render/shaders/flat/areaLight.d.ts +10 -0
  90. package/dist/render/shaders/flat/areaLight.js +142 -0
  91. package/dist/render/shaders/flat/glassTint.d.ts +3 -2
  92. package/dist/render/shaders/flat/glassTint.js +3 -1
  93. package/dist/render/shaders/flat/index.d.ts +5 -0
  94. package/dist/render/shaders/flat/index.js +46 -6
  95. package/dist/render/shaders/flat/lobes.d.ts +6 -0
  96. package/dist/render/shaders/flat/lobes.js +49 -43
  97. package/dist/render/shaders/flat/main.d.ts +1 -1
  98. package/dist/render/shaders/flat/main.js +158 -121
  99. package/dist/render/shaders/flat/models.d.ts +21 -6
  100. package/dist/render/shaders/flat/models.js +161 -17
  101. package/dist/render/shaders/flat/normalMap.d.ts +10 -0
  102. package/dist/render/shaders/flat/normalMap.js +25 -0
  103. package/dist/render/shaders/flat/preamble.js +84 -40
  104. package/dist/render/shaders/flat/skin.js +6 -5
  105. package/dist/render/shaders/generated/bloom.wgsl.d.ts +13 -3
  106. package/dist/render/shaders/generated/bloom.wgsl.js +13 -3
  107. package/dist/render/shaders/generated/bolt.wgsl.d.ts +1 -1
  108. package/dist/render/shaders/generated/bolt.wgsl.js +1 -1
  109. package/dist/render/shaders/generated/depth.wgsl.d.ts +4 -4
  110. package/dist/render/shaders/generated/depth.wgsl.js +4 -4
  111. package/dist/render/shaders/generated/flat.wgsl.d.ts +1647 -2044
  112. package/dist/render/shaders/generated/flat.wgsl.js +1685 -2082
  113. package/dist/render/shaders/generated/particle.wgsl.d.ts +115 -1
  114. package/dist/render/shaders/generated/particle.wgsl.js +115 -1
  115. package/dist/render/shaders/generated/skinBlur.wgsl.d.ts +12 -2
  116. package/dist/render/shaders/generated/skinBlur.wgsl.js +12 -2
  117. package/dist/render/shaders/generated/ssrTrace.wgsl.d.ts +11 -6
  118. package/dist/render/shaders/generated/ssrTrace.wgsl.js +11 -6
  119. package/dist/render/shaders/generated/water.wgsl.d.ts +23 -16
  120. package/dist/render/shaders/generated/water.wgsl.js +23 -16
  121. package/dist/render/shaders/gi/probeBake.wgsl.js +9 -5
  122. package/dist/render/shaders/particle.d.ts +17 -1
  123. package/dist/render/shaders/particle.js +133 -1
  124. package/dist/render/shaders/recon/motion.wgsl.d.ts +1 -1
  125. package/dist/render/shaders/recon/motion.wgsl.js +13 -2
  126. package/dist/render/shaders/skinBlur.js +25 -3
  127. package/dist/render/shaders/ssrTrace.js +20 -1
  128. package/dist/render/shaders/water.d.ts +1 -1
  129. package/dist/render/shaders/water.js +12 -3
  130. package/dist/render/skinBlur.d.ts +8 -5
  131. package/dist/render/skinBlur.js +8 -5
  132. package/dist/render/skinScatter.d.ts +3 -1
  133. package/dist/render/skinScatter.js +3 -1
  134. package/dist/render/ssrPass.js +1 -0
  135. package/dist/render/surfaceModel.d.ts +49 -8
  136. package/dist/render/surfaceModel.js +54 -8
  137. package/dist/render/surfaceTexture.d.ts +38 -1
  138. package/dist/render/surfaceTexture.js +37 -2
  139. package/package.json +6 -6
  140. package/src/index.ts +26 -3
  141. package/src/render/ambientHarmonics.ts +92 -0
  142. package/src/render/anisotropicLobe.ts +29 -0
  143. package/src/render/areaLights.ts +97 -0
  144. package/src/render/backend/api.ts +61 -1
  145. package/src/render/backend/webgl2/instanced.ts +12 -2
  146. package/src/render/backend/webgl2/renderer.ts +470 -59
  147. package/src/render/backend/webgl2/sceneCaptureTarget.ts +130 -0
  148. package/src/render/backend/webgl2/skinScatter.ts +120 -43
  149. package/src/render/backend/webgpu/buffers.ts +2 -1
  150. package/src/render/backend/webgpu/clusterBinner.ts +3 -1
  151. package/src/render/backend/webgpu/flatPass.ts +18 -13
  152. package/src/render/backend/webgpu/particlePass.ts +119 -12
  153. package/src/render/backend/webgpu/postPass.ts +7 -2
  154. package/src/render/backend/webgpu/renderer.ts +589 -113
  155. package/src/render/backend/webgpu/sceneCaptureTarget.ts +63 -0
  156. package/src/render/backend/webgpu/shaders/clusterBin.wgsl.ts +41 -1
  157. package/src/render/backend/webgpu/skinPaletteRing.ts +65 -33
  158. package/src/render/backend/webgpu/skinScatterPass.ts +59 -9
  159. package/src/render/backend/webgpu/surfaceTexturePass.ts +57 -6
  160. package/src/render/bloomChain.ts +69 -0
  161. package/src/render/bloomPass.ts +26 -2
  162. package/src/render/clusteredLights.ts +259 -12
  163. package/src/render/driftLight/bake.ts +3 -1
  164. package/src/render/driftLight/falloff.ts +13 -2
  165. package/src/render/fixtureAtlas.ts +1 -1
  166. package/src/render/gi/bounceLights.ts +12 -3
  167. package/src/render/instanceCull.ts +9 -0
  168. package/src/render/instances.ts +61 -12
  169. package/src/render/lightBudget.ts +88 -4
  170. package/src/render/lightmap.ts +243 -0
  171. package/src/render/mesh.ts +2 -1
  172. package/src/render/particleBatch.ts +95 -4
  173. package/src/render/particleMaterial.ts +11 -1
  174. package/src/render/particlePool.ts +36 -0
  175. package/src/render/particleSort.ts +133 -0
  176. package/src/render/pointLightSelection.ts +20 -1
  177. package/src/render/renderQuality.ts +8 -7
  178. package/src/render/rendererHarness.ts +3 -0
  179. package/src/render/rgb9e5.ts +52 -0
  180. package/src/render/sceneCapture.ts +56 -0
  181. package/src/render/sceneTarget.ts +18 -1
  182. package/src/render/screenSpaceReflection.ts +33 -1
  183. package/src/render/shaders/bloom.ts +20 -3
  184. package/src/render/shaders/bolt.ts +7 -1
  185. package/src/render/shaders/depth.ts +11 -0
  186. package/src/render/shaders/flat/anisotropic.ts +37 -3
  187. package/src/render/shaders/flat/areaLight.ts +142 -0
  188. package/src/render/shaders/flat/glassTint.ts +9 -2
  189. package/src/render/shaders/flat/index.ts +51 -6
  190. package/src/render/shaders/flat/lobes.ts +50 -43
  191. package/src/render/shaders/flat/main.ts +158 -121
  192. package/src/render/shaders/flat/models.ts +170 -21
  193. package/src/render/shaders/flat/normalMap.ts +25 -0
  194. package/src/render/shaders/flat/preamble.ts +84 -40
  195. package/src/render/shaders/flat/skin.ts +6 -5
  196. package/src/render/shaders/generated/bloom.wgsl.ts +13 -3
  197. package/src/render/shaders/generated/bolt.wgsl.ts +1 -1
  198. package/src/render/shaders/generated/depth.wgsl.ts +4 -4
  199. package/src/render/shaders/generated/flat.wgsl.ts +1685 -2082
  200. package/src/render/shaders/generated/particle.wgsl.ts +116 -1
  201. package/src/render/shaders/generated/skinBlur.wgsl.ts +12 -2
  202. package/src/render/shaders/generated/ssrTrace.wgsl.ts +11 -6
  203. package/src/render/shaders/generated/water.wgsl.ts +23 -16
  204. package/src/render/shaders/gi/probeBake.wgsl.ts +9 -5
  205. package/src/render/shaders/particle.ts +134 -1
  206. package/src/render/shaders/recon/motion.wgsl.ts +13 -2
  207. package/src/render/shaders/skinBlur.ts +25 -3
  208. package/src/render/shaders/ssrTrace.ts +20 -1
  209. package/src/render/shaders/water.ts +12 -3
  210. package/src/render/skinBlur.ts +8 -5
  211. package/src/render/skinScatter.ts +3 -1
  212. package/src/render/ssrPass.ts +1 -0
  213. package/src/render/surfaceModel.ts +82 -10
  214. package/src/render/surfaceTexture.ts +101 -3
@@ -63,7 +63,9 @@ import {
63
63
  clusteredMode,
64
64
  createClusterLightSet,
65
65
  createClusterTable,
66
+ clusteredLightTotal,
66
67
  fillClusterLightSet,
68
+ lightChannelsOf,
67
69
  } from '../../clusteredLights.ts';
68
70
  import type {
69
71
  PassContext,
@@ -88,7 +90,14 @@ import { FILM_FRAG, FILM_VERT } from '../../shaders/film.ts';
88
90
  import { SceneTarget } from '../../sceneTarget.ts';
89
91
  import type { ColourGradeLut } from '../../colourGrade.ts';
90
92
  import { FILM_LOOK_WITHOUT_COMPOSITE } from '../../shaders/filmLook.ts';
91
- import { bloomProfileWarning, bloomThresholdOf } from '../../bloomChain.ts';
93
+ import { AMBIENT_SH_FLOATS, packAmbientSH } from '../../ambientHarmonics.ts';
94
+ import {
95
+ bloomProfileWarning,
96
+ bloomThresholdOf,
97
+ defaultBloomResponse,
98
+ resolveBloomResponse,
99
+ type BloomResponse,
100
+ } from '../../bloomChain.ts';
92
101
  import {
93
102
  createEmptyTexture2D,
94
103
  createEmptyTexture2DArray,
@@ -127,9 +136,21 @@ import {
127
136
  MORPH_DELTA_TEXTURE_UNIT,
128
137
  SKIN_PALETTE_TEXTURE_UNIT,
129
138
  } from '../../lightBudget.ts';
130
- import { MODEL_PARAM_FLOATS, eyeAxisInWorld, packModel } from '../../surfaceModel.ts';
139
+ import {
140
+ MODEL_PARAM_FLOATS,
141
+ eyeAxisInWorld,
142
+ packModel,
143
+ skinProfileDistance,
144
+ } from '../../surfaceModel.ts';
131
145
  import type { SurfaceModelKind } from '../../surfaceModel.ts';
132
- import { modelBound, skinHalfBound } from '../../shaders/flat/models.ts';
146
+ import { lightmapRegionsBound, modelBound, skinHalfBound } from '../../shaders/flat/models.ts';
147
+ import {
148
+ lightmapTexels,
149
+ warnRegionsWithoutPage,
150
+ warnUnpagedLightmap,
151
+ withLightmapUvs,
152
+ } from '../../lightmap.ts';
153
+ import type { LightmapPage } from '../../lightmap.ts';
133
154
  import type { SkinHalf } from '../../shaders/flat/models.ts';
134
155
  import { GlSkinScatter } from './skinScatter.ts';
135
156
  import {
@@ -224,6 +245,7 @@ import {
224
245
  bindAreaLights,
225
246
  bindAreaShadows,
226
247
  openCones,
248
+ PARTICLE_DEPTH_UNIT,
227
249
  } from '../../lightBudget.ts';
228
250
  import { resolveRenderQuality } from '../../renderQuality.ts';
229
251
  import type { RenderQuality, RenderQualityOptions } from '../../renderQuality.ts';
@@ -234,6 +256,8 @@ import type { IncrementalMesh } from '../../mesh.ts';
234
256
  import type { MeshData } from '../../mesh.ts';
235
257
  import type { MeshOptions } from '../api.ts';
236
258
  import { SurfaceTexture } from '../../surfaceTexture.ts';
259
+ import { sceneCaptureTexels } from '../../sceneCapture.ts';
260
+ import { GlSceneCaptureTarget } from './sceneCaptureTarget.ts';
237
261
  import { glCompressedFormats } from '../../glCompressed.ts';
238
262
  import type { CompressedTextureFormat, SurfaceSource } from '../../compressedSource.ts';
239
263
  import type { SurfaceMaterial, SurfaceTextureOptions } from '../../surfaceTexture.ts';
@@ -257,7 +281,7 @@ import { PlanarReflection } from '../../planarReflection.ts';
257
281
  import { InstancedMesh } from '../../instancedMesh.ts';
258
282
  import { ParticleBatch } from '../../particleBatch.ts';
259
283
  import type { ParticleBatchOptions } from '../../particleBatch.ts';
260
- import type { ParticleInstances } from '../../particlePool.ts';
284
+ import type { DeviceParticles, ParticleInstances } from '../../particlePool.ts';
261
285
  import { BoltBatch } from '../../boltBatch.ts';
262
286
  import type { BoltSegments } from '../../boltPool.ts';
263
287
  import { LineBatch } from '../../lineBatch.ts';
@@ -418,6 +442,11 @@ export interface Environment extends Atmosphere {
418
442
  lightDirections: Float32Array;
419
443
  /** Two per active light: the cosine of the inner cone angle, then of the outer. */
420
444
  lightConeCos: Float32Array;
445
+ /**
446
+ * One per active light: its own falloff exponent, or 0 for the frame's. Optional: absent is 0 for
447
+ * every light. See `PointLightSource.falloffExponent`.
448
+ */
449
+ lightFalloffExponents?: Float32Array;
421
450
  /** One per active light: a row of the photometric atlas, or −1 for none. */
422
451
  lightIesProfiles: Float32Array;
423
452
  /**
@@ -430,6 +459,12 @@ export interface Environment extends Atmosphere {
430
459
  lightIesAxes: Float32Array;
431
460
  /** One per active light: a tile of the cookie atlas, or −1 for none. */
432
461
  lightCookies: Float32Array;
462
+ /**
463
+ * One per active light: the lighting channels it lights, a mask from 1 to 255. Absent, short, or
464
+ * a light at 1 is the default channel every surface is on too. See `PointLightSet.lightChannels`
465
+ * and `SurfaceMaterial.lightChannels`.
466
+ */
467
+ lightChannels?: Float32Array;
433
468
  /**
434
469
  * The rectangular emitters this frame, or null for none.
435
470
  *
@@ -508,6 +543,8 @@ export function createEnvironment(overrides: Partial<Environment> = {}): Environ
508
543
  lightIesAxes: new Float32Array(MAX_POINT_LIGHTS * 3),
509
544
  /* −1, because tile 0 is a real cookie and no light here has asked for one. */
510
545
  lightCookies: new Float32Array(MAX_POINT_LIGHTS).fill(-1),
546
+ /* Channel 1, the default every surface is on, so every light lights everything. */
547
+ lightChannels: new Float32Array(MAX_POINT_LIGHTS).fill(1),
511
548
  /* None, so the shader's loop breaks before it reads anything. */
512
549
  areaLights: null,
513
550
  activeLightWorldIndices: new Int32Array(MAX_POINT_LIGHTS).fill(-1),
@@ -618,6 +655,9 @@ export interface SkyColors {
618
655
  export { MAX_DEPTH_LAYER } from '../../depthConvention.ts';
619
656
 
620
657
  /** The identity tint, handed back after any draw that asked for one. See `drawMesh`. */
658
+ /** The far corner of no highlight box: what an environment that names none reads. */
659
+ const NO_HIGHLIGHT: readonly number[] = [0, 0, 0];
660
+
621
661
  const WHITE_TINT = new Float32Array([1, 1, 1]);
622
662
  /** The lit stage's clip plane when there is no mirror: zero, which clips nothing. */
623
663
  const NO_CLIP_PLANE = new Float32Array(4);
@@ -1364,6 +1404,23 @@ export class WebGL2Renderer implements RendererApi {
1364
1404
  private motionBlurScale = 1;
1365
1405
  /** How much of the bloom ceiling this frame takes. See `setBloom`. */
1366
1406
  private bloomScale = 1;
1407
+ /** The draw's own ambient, as `packAmbientSH` writes it; zeros are the frame's. */
1408
+ private readonly ambientSH = new Float32Array(AMBIENT_SH_FLOATS);
1409
+ /** Scratch for a sprite's depth conversion, so a particle draw allocates nothing. */
1410
+ private readonly spriteInverseProjection = mat4.create();
1411
+ /** Said once: a sprite asked for a soft edge where no depth copy could be had. */
1412
+ private warnedSpriteDepth = false;
1413
+ /** Said once: more rectangles than the fixed arm shades, and no froxel table for the rest. */
1414
+ private warnedAreaOverflow = false;
1415
+ /**
1416
+ * `uEnvironmentDials` as it stands: which chain the probe holds, the reflectivity, the gain. One
1417
+ * vector on the device, so written whole, and kept here so a setter moving one keeps the others.
1418
+ */
1419
+ private readonly environmentDials = new Float32Array(3);
1420
+ /** `uEmission` as it stands: the emissive gain and the night factor. */
1421
+ private readonly emission = new Float32Array([1, 0]);
1422
+ /** The ramp and each level's tint, as `resolveBloomResponse` writes them; the subtraction and white. */
1423
+ private readonly bloomResponse = defaultBloomResponse();
1367
1424
  /** In scene units, once `setBloom` has moved it; null reads the profile's. */
1368
1425
  private bloomThresholdSet: number | null = null;
1369
1426
  /**
@@ -1576,6 +1633,15 @@ export class WebGL2Renderer implements RendererApi {
1576
1633
  private warnedEnvironmentAmbient = false;
1577
1634
  /** Whether a probe is being baked, so the passes that would recurse into one can tell. */
1578
1635
  private probePassActive = false;
1636
+ /**
1637
+ * Whether a scene capture is being drawn (`captureScene`): an off-screen pass like a bake, so the
1638
+ * frame's own view, its refraction copy and skin's spread are left alone, but lit as the frame is.
1639
+ */
1640
+ private capturePassActive = false;
1641
+ /** Each capture's framebuffer, beside the texture a material binds. See `createSceneCapture`. */
1642
+ private readonly captureTargets = new WeakMap<SurfaceTexture, GlSceneCaptureTarget>();
1643
+ /** Said once, where a driver refuses to draw into a capture. */
1644
+ private warnedCapture = false;
1579
1645
  /** Whether the bake under way lights its faces by the grid. `ProbeBakeOptions.bounce`. */
1580
1646
  private probeBounce = false;
1581
1647
  /** The clip-control extension, or null where this context does not offer it. */
@@ -1647,12 +1713,14 @@ export class WebGL2Renderer implements RendererApi {
1647
1713
  * the camera's matrix unchanged and multiplies nothing.
1648
1714
  */
1649
1715
  private viewProjFor(camera: { readonly viewProjection: ReadonlyMat4 }): ReadonlyMat4 {
1650
- const scene = this.probePassActive
1651
- ? (cubeFaceProjection(
1652
- this.cubeFaceViewProj,
1653
- this.sceneMatrix(camera.viewProjection) as unknown as ArrayLike<number>,
1654
- ) as unknown as ReadonlyMat4)
1655
- : this.sceneMatrix(camera.viewProjection);
1716
+ /* A capture's rows are flipped as a probe face's are, so it reads as an image does. */
1717
+ const scene =
1718
+ this.probePassActive || this.capturePassActive
1719
+ ? (cubeFaceProjection(
1720
+ this.cubeFaceViewProj,
1721
+ this.sceneMatrix(camera.viewProjection) as unknown as ArrayLike<number>,
1722
+ ) as unknown as ReadonlyMat4)
1723
+ : this.sceneMatrix(camera.viewProjection);
1656
1724
  /*
1657
1725
  * **The jitter goes on here and nowhere else.** This is the one funnel every geometry upload
1658
1726
  * passes through, so one line covers the mesh passes, the sky, the water and the effects
@@ -2460,9 +2528,7 @@ export class WebGL2Renderer implements RendererApi {
2460
2528
  gl.activeTexture(gl.TEXTURE0 + FIXTURE_ATLAS_TEXTURE_UNIT);
2461
2529
  gl.bindTexture(gl.TEXTURE_2D, this.fixtureTexture);
2462
2530
  gl.uniform1i(u['uFixtureAtlas'] ?? null, FIXTURE_ATLAS_TEXTURE_UNIT);
2463
- gl.uniform1f(u['uIesAtlasRows'] ?? null, this.iesRows);
2464
- gl.uniform1f(u['uIesPlaneCount'] ?? null, this.iesPlanes);
2465
- gl.uniform1f(u['uCookieTiles'] ?? null, this.cookieTiles);
2531
+ gl.uniform3f(u['uFixtureShape'] ?? null, this.iesRows, this.iesPlanes, this.cookieTiles);
2466
2532
  gl.activeTexture(gl.TEXTURE0);
2467
2533
  }
2468
2534
 
@@ -2516,7 +2582,8 @@ export class WebGL2Renderer implements RendererApi {
2516
2582
  gl.uniformMatrix4fv(u['uView'] ?? null, false, camera.view);
2517
2583
 
2518
2584
  this.clusterTable ??= createClusterTable();
2519
- const lights = fillClusterLightSet(env, this.clusterLights);
2585
+ /* The rectangles past the fixed arm's go into the table. See `ClusterLightSet.areas`. */
2586
+ const lights = fillClusterLightSet(env, this.clusterLights, this.shadedAreaLights);
2520
2587
  /* A slot is read only under `POINT_SHADOWS`; see `lightHasFixture`. */
2521
2588
  const shadowSlots = this.quality.pointShadows ? this.lightBudget.maxLights : 0;
2522
2589
  buildLightClusters(
@@ -2536,7 +2603,10 @@ export class WebGL2Renderer implements RendererApi {
2536
2603
  */
2537
2604
  shadowSlots,
2538
2605
  );
2539
- gl.uniform1i(u['uClustered'] ?? null, clusteredMode(lights, lights.count, shadowSlots));
2606
+ gl.uniform1i(
2607
+ u['uClustered'] ?? null,
2608
+ clusteredMode(lights, clusteredLightTotal(lights), shadowSlots),
2609
+ );
2540
2610
  gl.texSubImage2D(
2541
2611
  gl.TEXTURE_2D,
2542
2612
  0,
@@ -3252,8 +3322,9 @@ export class WebGL2Renderer implements RendererApi {
3252
3322
  * one. Measured on hardware: WebGL2 drew neither the bend nor the bulge while WebGPU drew
3253
3323
  * both, which is the two-backend comparison doing exactly what it is for.
3254
3324
  */
3255
- this.warmVariants(data);
3256
- return new Mesh(this.gl, data, options.dynamic === true);
3325
+ const drawn = withLightmapUvs(data);
3326
+ this.warmVariants(drawn);
3327
+ return new Mesh(this.gl, drawn, options.dynamic === true);
3257
3328
  }
3258
3329
 
3259
3330
  /**
@@ -3266,8 +3337,9 @@ export class WebGL2Renderer implements RendererApi {
3266
3337
  * the signal that it was abandoned.
3267
3338
  */
3268
3339
  createMeshIncremental(data: MeshData, options: MeshOptions = {}): IncrementalMesh {
3269
- this.warmVariants(data);
3270
- const { mesh, upload } = createMeshIncremental(this.gl, data, options.dynamic === true);
3340
+ const drawn = withLightmapUvs(data);
3341
+ this.warmVariants(drawn);
3342
+ const { mesh, upload } = createMeshIncremental(this.gl, drawn, options.dynamic === true);
3271
3343
  return {
3272
3344
  mesh,
3273
3345
  upload: {
@@ -3445,6 +3517,97 @@ export class WebGL2Renderer implements RendererApi {
3445
3517
  return new SurfaceTexture(this.gl, sources, options, this.compressedFormats);
3446
3518
  }
3447
3519
 
3520
+ /** A baked lightmap page, as a `lightmapModel` material's `modelMap`. See `RendererApi`. */
3521
+ createLightmap(page: LightmapPage): SurfaceTexture {
3522
+ return new SurfaceTexture(this.gl, lightmapTexels(page), { wrap: 'clamp', mipmap: false });
3523
+ }
3524
+
3525
+ /**
3526
+ * A texture the scene can be drawn into, for a material to show. See `RendererApi` and
3527
+ * `sceneCapture.ts`. Half floats where the frame keeps range, as a probe does.
3528
+ */
3529
+ createSceneCapture(width: number, height: number): SurfaceTexture {
3530
+ const float = this.quality.hdrScene && this.gl.getExtension('EXT_color_buffer_float') !== null;
3531
+ const texture = new SurfaceTexture(this.gl, sceneCaptureTexels(width, height, float), {
3532
+ wrap: 'clamp',
3533
+ mipmap: false,
3534
+ });
3535
+ this.captureTargets.set(
3536
+ texture,
3537
+ new GlSceneCaptureTarget(
3538
+ this.gl,
3539
+ texture,
3540
+ width,
3541
+ height,
3542
+ this.sceneTarget?.sampleCount ?? 1,
3543
+ float,
3544
+ ),
3545
+ );
3546
+ return texture;
3547
+ }
3548
+
3549
+ /**
3550
+ * Draw the scene into a capture from `camera`: `draw` is handed the camera, its matrices updated
3551
+ * for the capture's shape, and submits the scene as it would to the screen. See `RendererApi`.
3552
+ *
3553
+ * **Radiance, as a probe bake stores it**: the output transform and exposure are held off for the
3554
+ * pass, so the frame grades a screen showing it once. No temporal jitter, which a capture has no
3555
+ * resolve to undo. Returns whether it drew: false for a texture that is not a capture, or one the
3556
+ * driver will not draw into.
3557
+ */
3558
+ captureScene(
3559
+ capture: SurfaceTexture,
3560
+ camera: Camera,
3561
+ clearColor: Vec3,
3562
+ draw: (camera: Camera) => void,
3563
+ ): boolean {
3564
+ if (this.contextLost) return false;
3565
+ const target = this.captureTargets.get(capture);
3566
+ if (target === undefined) return false;
3567
+ if (!target.usable) {
3568
+ if (!this.warnedCapture) {
3569
+ this.warnedCapture = true;
3570
+ console.warn(
3571
+ 'Renderer: this driver will not draw into a scene capture, so it keeps its clear. ' +
3572
+ 'Nothing else is affected.',
3573
+ );
3574
+ }
3575
+ return false;
3576
+ }
3577
+ camera.updateMatrices(target.width / target.height);
3578
+ const { gl } = this;
3579
+ const jittering = this.temporalJittering;
3580
+ this.temporalJittering = false;
3581
+ this.capturePassActive = true;
3582
+ this.useFlatProgram();
3583
+ gl.uniform1i(this.flatUniforms['uOutputTransform'] ?? null, 0);
3584
+ gl.uniform1f(this.flatUniforms['uOutputExposure'] ?? null, 1);
3585
+ this.materials.dirty();
3586
+ target.begin(gl, clearColor, this.depthClear);
3587
+ /*
3588
+ * **Rows flipped, as a probe face's are.** A GL framebuffer's first row is its bottom and an
3589
+ * image's is its top, so a capture drawn upright would be shown upside down by every material
3590
+ * reading it as the image it is; `viewProjFor` negates clip y for the pass, and negating one axis
3591
+ * reverses every triangle's winding, which the front face follows. WebGPU's rows already run
3592
+ * the image's way.
3593
+ */
3594
+ gl.frontFace(gl.CW);
3595
+ try {
3596
+ draw(camera);
3597
+ } finally {
3598
+ target.end(gl);
3599
+ gl.frontFace(gl.CCW);
3600
+ this.useFlatProgram();
3601
+ gl.uniform1i(this.flatUniforms['uOutputTransform'] ?? null, this.gradeCode());
3602
+ gl.uniform1f(this.flatUniforms['uOutputExposure'] ?? null, this.gradeExposure());
3603
+ this.materials.dirty();
3604
+ this.capturePassActive = false;
3605
+ this.temporalJittering = jittering;
3606
+ this.restoreViewport();
3607
+ }
3608
+ return true;
3609
+ }
3610
+
3448
3611
  /**
3449
3612
  * Replace a surface texture's pixels, keeping the GPU object and its sampler state.
3450
3613
  *
@@ -3465,6 +3628,8 @@ export class WebGL2Renderer implements RendererApi {
3465
3628
  disposeSurfaceTexture(texture: SurfaceTexture): void {
3466
3629
  if (this.contextLost) return;
3467
3630
  texture.dispose(this.gl);
3631
+ this.captureTargets.get(texture)?.dispose(this.gl);
3632
+ this.captureTargets.delete(texture);
3468
3633
  }
3469
3634
 
3470
3635
  /**
@@ -3522,7 +3687,7 @@ export class WebGL2Renderer implements RendererApi {
3522
3687
  const label = `flat.${variant}.${kind}.${half}`;
3523
3688
  const program = compileProgram(
3524
3689
  gl,
3525
- litVertexOf(variant),
3690
+ modelVertexOf(variant, kind),
3526
3691
  skinHalfBound(modelBound(this.flatFragSource, kind), half),
3527
3692
  label,
3528
3693
  );
@@ -3693,12 +3858,13 @@ export class WebGL2Renderer implements RendererApi {
3693
3858
  this.currentMaterial = material;
3694
3859
  /* What the blur spreads this skin by: the last material to name a profile says. */
3695
3860
  const model = material?.model;
3861
+ warnUnpagedLightmap(model, material?.modelMap);
3696
3862
  if (model?.kind === 'skin' && this.skinScatter !== null) {
3697
3863
  this.skinScatter.setProfile(
3698
3864
  model.profile,
3699
- model.scatterColor[0] * model.radius,
3700
- model.scatterColor[1] * model.radius,
3701
- model.scatterColor[2] * model.radius,
3865
+ skinProfileDistance(model, 0),
3866
+ skinProfileDistance(model, 1),
3867
+ skinProfileDistance(model, 2),
3702
3868
  );
3703
3869
  }
3704
3870
  /*
@@ -3738,7 +3904,8 @@ export class WebGL2Renderer implements RendererApi {
3738
3904
  v[0] = material?.cutout ?? 0;
3739
3905
  v[1] = CUTOUT_RESOLVE_CODE[this.cutoutResolveNow(material, translucent)];
3740
3906
  v[2] = this.cutoutFrame;
3741
- v[3] = 0;
3907
+ /* The material's lighting channels, which the lamps' are compared with. See `uCutout`. */
3908
+ v[3] = lightChannelsOf(material?.lightChannels);
3742
3909
  return v;
3743
3910
  }
3744
3911
 
@@ -3845,7 +4012,14 @@ export class WebGL2Renderer implements RendererApi {
3845
4012
  gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.emptyTexture2DArray);
3846
4013
  if (modelMap !== null) modelMap.bind(gl, MODEL_MAP_TEXTURE_UNIT);
3847
4014
  gl.uniform1i(u['uModelMap'] ?? null, MODEL_MAP_TEXTURE_UNIT);
3848
- packModel(material?.model ?? null, modelMap !== null, this.modelParams);
4015
+ packModel(
4016
+ material?.model ?? null,
4017
+ modelMap !== null,
4018
+ this.modelParams,
4019
+ material?.physicalSpecular === true,
4020
+ );
4021
+ /* The first physical highlight asked for is when the lit programs start carrying one. */
4022
+ if (material?.physicalSpecular === true) this.askLit('PHYSICAL_SPECULAR');
3849
4023
  gl.uniform4fv(u['uModelParams'] ?? null, this.modelParams);
3850
4024
 
3851
4025
  /* The albedo's effects table, or the stand-in the shader reads as none. Before the early
@@ -3866,6 +4040,8 @@ export class WebGL2Renderer implements RendererApi {
3866
4040
  gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.emptyTexture2DArray);
3867
4041
  this.materialFlags[0] = 0;
3868
4042
  gl.uniform4iv(u['uMaterialFlags'] ?? null, this.materialFlags);
4043
+ /* No image to cut, and the channels all the same: a material needs no albedo to name them. */
4044
+ gl.uniform4f(u['uCutout'] ?? null, 0, 0, 0, lightChannelsOf(material?.lightChannels));
3869
4045
  return;
3870
4046
  }
3871
4047
  albedo.bind(gl, SURFACE_TEXTURE_UNIT);
@@ -3906,17 +4082,27 @@ export class WebGL2Renderer implements RendererApi {
3906
4082
  }
3907
4083
 
3908
4084
  /**
3909
- * A pass-level scale over the grain the geometry itself declared, 0 to 1.
4085
+ * The ambient the following draws take, as nine spherical-harmonic coefficients of incoming
4086
+ * radiance — red, green and blue of each, twenty-seven numbers — or `null` for the frame's own:
4087
+ * the sky's gradient, or the probes'. See `ambientHarmonics.ts` for the basis and the axes.
3910
4088
  *
3911
- * Which surfaces are mineral is now `MeshData.grain`, stated per vertex through
3912
- * `MeshBuilder.setGrain` — because it is a property of a material, and absent means none.
3913
- * This stays as the override for the case that has no answer to state: an imported model
3914
- * arrives with no grain information at all, and a caller drawing one decides for it.
3915
- *
3916
- * Pass state, like `setSurfaceTexture`, because a material covers many draws. The default
3917
- * is 1 and `bindMeshPass` restores it, so it scales geometry that declared grain and
3918
- * leaves geometry that did not exactly as it is.
4089
+ * **For a thing lit by where it stands**: a consumer sampling a stage's baked volume of indirect
4090
+ * light at a character every frame sets the result around that character's draws, and the
4091
+ * character takes the light of its spot rather than the frame's. It replaces the diffuse ambient
4092
+ * only — a glossy surface still reflects the probes — and it is pass state like the material,
4093
+ * which `bindMeshPass` clears, so a mirror or a probe bake drawn after does not inherit it.
3919
4094
  */
4095
+ setAmbientSH(coefficients: ArrayLike<number> | null): void {
4096
+ if (this.contextLost) return;
4097
+ packAmbientSH(coefficients, this.ambientSH);
4098
+ this.materials.dirty();
4099
+ for (const target of this.flatTargets) {
4100
+ this.gl.useProgram(target.program);
4101
+ this.gl.uniform4fv(target.uniforms['uAmbientSH[0]'] ?? null, this.ambientSH);
4102
+ }
4103
+ this.useFlatProgram();
4104
+ }
4105
+
3920
4106
  /**
3921
4107
  * How much of the environment the following draws mirror, 0 to 1.
3922
4108
  *
@@ -3930,7 +4116,8 @@ export class WebGL2Renderer implements RendererApi {
3930
4116
  setSurfaceReflectivity(amount: number): void {
3931
4117
  if (this.contextLost) return;
3932
4118
  this.materials.dirty();
3933
- this.writeFlatFloat('uReflectivity', Math.min(1, Math.max(0, amount)));
4119
+ this.environmentDials[1] = Math.min(1, Math.max(0, amount));
4120
+ this.writeFlatVector('uEnvironmentDials', this.environmentDials);
3934
4121
  }
3935
4122
 
3936
4123
  /**
@@ -3948,7 +4135,8 @@ export class WebGL2Renderer implements RendererApi {
3948
4135
  setEnvironmentGain(gain: number): void {
3949
4136
  if (this.contextLost) return;
3950
4137
  this.materials.dirty();
3951
- this.writeFlatFloat('uEnvironmentGain', Math.max(0, gain));
4138
+ this.environmentDials[2] = Math.max(0, gain);
4139
+ this.writeFlatVector('uEnvironmentDials', this.environmentDials);
3952
4140
  }
3953
4141
 
3954
4142
  /**
@@ -3974,6 +4162,18 @@ export class WebGL2Renderer implements RendererApi {
3974
4162
  );
3975
4163
  }
3976
4164
 
4165
+ /**
4166
+ * A pass-level scale over the grain the geometry itself declared, 0 to 1.
4167
+ *
4168
+ * Which surfaces are mineral is now `MeshData.grain`, stated per vertex through
4169
+ * `MeshBuilder.setGrain` — because it is a property of a material, and absent means none.
4170
+ * This stays as the override for the case that has no answer to state: an imported model
4171
+ * arrives with no grain information at all, and a caller drawing one decides for it.
4172
+ *
4173
+ * Pass state, like `setSurfaceTexture`, because a material covers many draws. The default
4174
+ * is 1 and `bindMeshPass` restores it, so it scales geometry that declared grain and
4175
+ * leaves geometry that did not exactly as it is.
4176
+ */
3977
4177
  setSurfaceGrain(amount: number): void {
3978
4178
  if (this.contextLost) return;
3979
4179
  this.materials.dirty();
@@ -4019,6 +4219,18 @@ export class WebGL2Renderer implements RendererApi {
4019
4219
  this.useFlatProgram();
4020
4220
  }
4021
4221
 
4222
+ /** A vector of material state, two to four floats, into every flat program. */
4223
+ private writeFlatVector(name: string, value: Float32Array): void {
4224
+ for (const target of this.flatTargets) {
4225
+ this.gl.useProgram(target.program);
4226
+ const at = target.uniforms[name] ?? null;
4227
+ if (value.length === 2) this.gl.uniform2fv(at, value);
4228
+ else if (value.length === 3) this.gl.uniform3fv(at, value);
4229
+ else this.gl.uniform4fv(at, value);
4230
+ }
4231
+ this.useFlatProgram();
4232
+ }
4233
+
4022
4234
  /** One int of material state, into every flat program, for `writeFlatFloat`'s reason. */
4023
4235
  private writeFlatInt(name: string, value: number): void {
4024
4236
  for (const target of this.flatTargets) {
@@ -4083,8 +4295,13 @@ export class WebGL2Renderer implements RendererApi {
4083
4295
  setSurfaceRelief(amount: number, cyclesPerMetre = 60): void {
4084
4296
  if (this.contextLost) return;
4085
4297
  this.materials.dirty();
4086
- this.writeFlatFloat('uRelief', Math.min(1, Math.max(0, amount)));
4087
- this.writeFlatFloat('uReliefCycles', Math.max(0.01, cyclesPerMetre));
4298
+ const strength = Math.min(1, Math.max(0, amount));
4299
+ const cycles = Math.max(0.01, cyclesPerMetre);
4300
+ for (const target of this.flatTargets) {
4301
+ this.gl.useProgram(target.program);
4302
+ this.gl.uniform2f(target.uniforms['uRelief'] ?? null, strength, cycles);
4303
+ }
4304
+ this.useFlatProgram();
4088
4305
  }
4089
4306
 
4090
4307
  /**
@@ -4142,7 +4359,8 @@ export class WebGL2Renderer implements RendererApi {
4142
4359
  setEmissiveGain(gain: number): void {
4143
4360
  if (this.contextLost) return;
4144
4361
  this.materials.dirty();
4145
- this.writeFlatFloat('uEmissiveGain', gain);
4362
+ this.emission[0] = gain;
4363
+ this.writeFlatVector('uEmission', this.emission);
4146
4364
  }
4147
4365
 
4148
4366
  /* -- Instanced meshes --------------------------------------------------------------- */
@@ -4254,6 +4472,7 @@ export class WebGL2Renderer implements RendererApi {
4254
4472
  options: TranslucentMeshOptions,
4255
4473
  ): void {
4256
4474
  if (this.contextLost) return;
4475
+ warnRegionsWithoutPage(this.currentMaterial?.model, data.lightmapRegions);
4257
4476
  const mesh = batch.mesh;
4258
4477
  /* Nor a batch of a mesh with nothing in it: see `Mesh.indexCount`. */
4259
4478
  if (!mesh.complete || mesh.indexCount === 0) return;
@@ -4384,7 +4603,7 @@ export class WebGL2Renderer implements RendererApi {
4384
4603
  const refraction = options.refraction ?? 0;
4385
4604
  const glassy = resolveGlass(options.glass, this.glassScratch);
4386
4605
  if (refraction <= 0 && !glassy) return false;
4387
- if (this.probePassActive || this.reflectionPassActive) return false;
4606
+ if (this.probePassActive || this.reflectionPassActive || this.capturePassActive) return false;
4388
4607
  const snapshot = this.sceneTarget?.snapshotColor(false, glassy) ?? null;
4389
4608
  if (snapshot === null) return false;
4390
4609
  gl.activeTexture(gl.TEXTURE0 + REFRACT_SCENE_TEXTURE_UNIT);
@@ -4746,6 +4965,29 @@ export class WebGL2Renderer implements RendererApi {
4746
4965
  return new ParticleBatch(this.gl, capacity, options);
4747
4966
  }
4748
4967
 
4968
+ /**
4969
+ * Particles a caller's own compute wrote: WebGPU's alone, since this backend has no compute stage
4970
+ * to have written them (`computeSupported`). Said once, and nothing is drawn.
4971
+ */
4972
+ drawDeviceParticles(
4973
+ _batch: ParticleBatch,
4974
+ _particles: DeviceParticles,
4975
+ _camera: Camera,
4976
+ _env: Environment,
4977
+ _timeSeconds: number,
4978
+ ): void {
4979
+ if (this.warnedDeviceParticles) return;
4980
+ this.warnedDeviceParticles = true;
4981
+ console.warn(
4982
+ 'Renderer: drawDeviceParticles draws particles a compute shader wrote, and WebGL2 has no ' +
4983
+ 'compute stage, so nothing is drawn. Check `computeSupported`, and draw a pool with ' +
4984
+ '`drawParticles` where it is false.',
4985
+ );
4986
+ }
4987
+
4988
+ /** Said once: see `drawDeviceParticles`. */
4989
+ private warnedDeviceParticles = false;
4990
+
4749
4991
  /**
4750
4992
  * Draw a live particle pool.
4751
4993
  *
@@ -4767,8 +5009,9 @@ export class WebGL2Renderer implements RendererApi {
4767
5009
  const { gl } = this;
4768
5010
  const u = batch.uniforms;
4769
5011
  gl.useProgram(batch.program);
4770
- batch.upload(gl, data);
5012
+ batch.upload(gl, data, camera.position);
4771
5013
  batch.bindMaterial(gl);
5014
+ if (batch.material === 'sprite') this.bindSpriteFades(batch, camera);
4772
5015
  gl.uniformMatrix4fv(u['uViewProj'] ?? null, false, this.viewProjFor(camera));
4773
5016
  gl.uniform3fv(u['uCameraPos'] ?? null, camera.position);
4774
5017
  gl.uniform1f(u['uTime'] ?? null, timeSeconds);
@@ -4800,6 +5043,53 @@ export class WebGL2Renderer implements RendererApi {
4800
5043
  if (this.materialDoubleSided) gl.enable(gl.CULL_FACE);
4801
5044
  }
4802
5045
 
5046
+ /**
5047
+ * A sprite's two fades and the depth copy the soft one reads. The copy is the frame's opaque
5048
+ * depth, so it is taken only in the frame's own picture: in a mirror or a probe's face it would
5049
+ * be another view's, and the edge stays hard there. Without a scene target there is no copy at
5050
+ * all, and the renderer says so once rather than drawing a hard edge that was asked to be soft.
5051
+ */
5052
+ private bindSpriteFades(batch: ParticleBatch, camera: Camera): void {
5053
+ const { gl } = this;
5054
+ const u = batch.uniforms;
5055
+ const scene = this.sceneTarget;
5056
+ let depth: WebGLTexture | null = null;
5057
+ if (
5058
+ batch.softDepth > 0 &&
5059
+ !this.reflectionPassActive &&
5060
+ !this.probePassActive &&
5061
+ !this.capturePassActive
5062
+ ) {
5063
+ depth = scene?.snapshotDepth(true) ?? null;
5064
+ if (depth === null && !this.warnedSpriteDepth) {
5065
+ this.warnedSpriteDepth = true;
5066
+ console.warn(
5067
+ "WebGL2: a 'sprite' asked for a soft edge, which reads a copy of the frame's depth, and " +
5068
+ 'this profile keeps none — screenEffects is off, or the driver refused the copy. The ' +
5069
+ 'edge is hard.',
5070
+ );
5071
+ }
5072
+ }
5073
+ gl.activeTexture(gl.TEXTURE0 + PARTICLE_DEPTH_UNIT);
5074
+ gl.bindTexture(gl.TEXTURE_2D, depth ?? this.emptyTexture2D);
5075
+ gl.uniform1i(u['uDepth'] ?? null, PARTICLE_DEPTH_UNIT);
5076
+ gl.uniform4f(u['uFade'] ?? null, batch.softDepth, batch.cameraFade, depth === null ? 0 : 1, 0);
5077
+ mat4.invert(this.spriteInverseProjection, camera.projection);
5078
+ const inverse = this.spriteInverseProjection;
5079
+ gl.uniform4f(
5080
+ u['uDepthToViewZ'] ?? null,
5081
+ inverse[10] ?? 0,
5082
+ inverse[14] ?? 0,
5083
+ inverse[11] ?? 0,
5084
+ inverse[15] ?? 1,
5085
+ );
5086
+ gl.uniform2f(
5087
+ u['uViewport'] ?? null,
5088
+ scene?.frameWidth ?? gl.drawingBufferWidth,
5089
+ scene?.frameHeight ?? gl.drawingBufferHeight,
5090
+ );
5091
+ }
5092
+
4803
5093
  /** Build a batch for electrical arcs. `capacity` is in segments, not arcs. */
4804
5094
  createBolts(segmentCapacity: number, label = 'bolts'): BoltBatch {
4805
5095
  return new BoltBatch(this.gl, segmentCapacity, label);
@@ -5838,6 +6128,7 @@ export class WebGL2Renderer implements RendererApi {
5838
6128
  lightFixtures: this.litOn.LIGHT_FIXTURES,
5839
6129
  surfaceEffects: this.litOn.SURFACE_EFFECTS,
5840
6130
  driftLight: this.litOn.DRIFT_LIGHT,
6131
+ physicalSpecular: this.litOn.PHYSICAL_SPECULAR,
5841
6132
  maxLights: budget.maxLights,
5842
6133
  maxAreaLights: budget.maxAreaLights,
5843
6134
  });
@@ -5918,7 +6209,7 @@ export class WebGL2Renderer implements RendererApi {
5918
6209
  const label = `flat.${variant}.${kind}.${half}`;
5919
6210
  const program = compileProgram(
5920
6211
  gl,
5921
- litVertexOf(variant),
6212
+ modelVertexOf(variant, kind),
5922
6213
  skinHalfBound(modelBound(this.flatFragSource, kind), half),
5923
6214
  label,
5924
6215
  );
@@ -6800,10 +7091,17 @@ export class WebGL2Renderer implements RendererApi {
6800
7091
  * and wrong for a day whose exposure spans 2.5 to 14: a courtyard blooming at noon, or candles
6801
7092
  * never blooming at night. A caller whose exposure moves passes what it wants on screen over the
6802
7093
  * exposure. Omitted leaves it where it was, so a caller that never passes one keeps the profile's.
7094
+ *
7095
+ * **`response` is how a colour comes in past the threshold and what colour each octave of the
7096
+ * halo takes** — a ramp in place of the subtraction, and a tint a level — held until moved like
7097
+ * the threshold; `null` goes back to the subtraction and white. See `BloomResponse`: it is what
7098
+ * matches a stage built for another engine's bloom, whose tints are how a strength past 1 is
7099
+ * asked for.
6803
7100
  */
6804
- setBloom(scale: number, threshold?: number): void {
7101
+ setBloom(scale: number, threshold?: number, response?: BloomResponse | null): void {
6805
7102
  this.bloomScale = Math.min(Math.max(scale, 0), 1);
6806
7103
  if (threshold !== undefined) this.bloomThresholdSet = bloomThresholdOf(threshold);
7104
+ if (response !== undefined) resolveBloomResponse(response, this.bloomResponse);
6807
7105
  }
6808
7106
 
6809
7107
  /**
@@ -7364,6 +7662,7 @@ export class WebGL2Renderer implements RendererApi {
7364
7662
  ? {
7365
7663
  strength: bloomStrength,
7366
7664
  threshold: this.bloomThresholdSet ?? this.quality.bloomThreshold,
7665
+ response: this.bloomResponse,
7367
7666
  }
7368
7667
  : undefined;
7369
7668
 
@@ -7699,6 +7998,19 @@ export class WebGL2Renderer implements RendererApi {
7699
7998
  bindMeshPass(camera: Camera, env: Environment): void {
7700
7999
  /* The pass's own material is reopened, so the next draw opens one. See `materialChanges.ts`. */
7701
8000
  this.materials.dirty();
8001
+ /* Rectangles past the fixed arm's are the froxel table's, and without it nobody shades them. */
8002
+ if (
8003
+ !this.warnedAreaOverflow &&
8004
+ !this.quality.clusteredLights &&
8005
+ (env.areaLights?.count ?? 0) > this.shadedAreaLights
8006
+ ) {
8007
+ this.warnedAreaOverflow = true;
8008
+ console.warn(
8009
+ `WebGL2: ${env.areaLights?.count ?? 0} rectangular lights and ${this.shadedAreaLights} ` +
8010
+ 'shaded without clusteredLights: the rest are not drawn. Turn clusteredLights on, and the ' +
8011
+ 'froxel table shades every one past the fixed ones, unshadowed.',
8012
+ );
8013
+ }
7702
8014
  /*
7703
8015
  * Captured here rather than taken as an argument to `endFrame`, because this is the
7704
8016
  * camera the world was drawn with and a caller should not have to hand it over twice.
@@ -7712,7 +8024,12 @@ export class WebGL2Renderer implements RendererApi {
7712
8024
  * other position, and the anti-flicker's period never started.
7713
8025
  */
7714
8026
  if (this.framePresented) this.temporalJittering = false;
7715
- if (!this.reflectionPassActive && !this.probePassActive && !this.framePresented) {
8027
+ if (
8028
+ !this.reflectionPassActive &&
8029
+ !this.probePassActive &&
8030
+ !this.capturePassActive &&
8031
+ !this.framePresented
8032
+ ) {
7716
8033
  this.frameViewProj = camera.viewProjection;
7717
8034
  /* Copied rather than referenced: a caller moves its camera in place, and the decal pass
7718
8035
  reads this at the end of the frame. */
@@ -7810,6 +8127,13 @@ export class WebGL2Renderer implements RendererApi {
7810
8127
  this.materialHasMaps = false;
7811
8128
  this.currentMaterial = null;
7812
8129
  this.currentSurfaceTexture = null;
8130
+ /* Nor an ambient of its own: the frame's, until a draw asks. See `setAmbientSH`. */
8131
+ this.ambientSH.fill(0);
8132
+ /* No reflection and an environment at its own brightness; the emission the world asks for. */
8133
+ this.environmentDials[1] = 0;
8134
+ this.environmentDials[2] = 1;
8135
+ this.emission[0] = env.emissiveGain;
8136
+ this.emission[1] = env.nightFactor;
7813
8137
  for (const target of this.flatTargets) {
7814
8138
  this.gl.useProgram(target.program);
7815
8139
  this.writeMeshPassState(target.uniforms, camera, env);
@@ -8036,13 +8360,18 @@ export class WebGL2Renderer implements RendererApi {
8036
8360
  this.oitWeight = 0;
8037
8361
  this.ditherFade = 0;
8038
8362
  gl.uniform2f(u['uWriteMode'] ?? null, 0, 0);
8039
- /* No environment reflection unless a caller asks, so existing scenes are unchanged. */
8040
- gl.uniform1f(u['uReflectivity'] ?? null, 0);
8041
- /* 1, not 0: this one is a multiplier and its identity is one. See `setEnvironmentGain`. */
8042
- gl.uniform1f(u['uEnvironmentGain'] ?? null, 1);
8363
+ /* The draw's own ambient as it stands, which `bindMeshPass` has just cleared: a program built
8364
+ in the middle of a pass takes what the caller set. See `setAmbientSH`. */
8365
+ gl.uniform4fv(u['uAmbientSH[0]'] ?? null, this.ambientSH);
8366
+ /*
8367
+ * Which chain the probe holds, no environment reflection, and an environment at its own
8368
+ * brightness — the reflectivity's identity is 0 and the gain's is 1 — as `bindMeshPass` set
8369
+ * them, or as a draw since has: a program built in the middle of a pass takes what stands.
8370
+ * See `setSurfaceReflectivity` and `setEnvironmentGain`.
8371
+ */
8372
+ gl.uniform3fv(u['uEnvironmentDials'] ?? null, this.environmentDials);
8043
8373
  /* And no relief unless a caller asks, for the same reason. */
8044
- gl.uniform1f(u['uRelief'] ?? null, 0);
8045
- gl.uniform1f(u['uReliefCycles'] ?? null, 60);
8374
+ gl.uniform2f(u['uRelief'] ?? null, 0, 60);
8046
8375
  /* And no relief read off a surface texture either, for the same reason. */
8047
8376
  gl.uniform1f(u['uTextureRelief'] ?? null, 0);
8048
8377
  /* The mirror's plane in a reflection pass, and zero — which clips nothing — in every other. */
@@ -8060,8 +8389,8 @@ export class WebGL2Renderer implements RendererApi {
8060
8389
  this.reflectionPassActive ? this.reflectionAtmosphereY : (camera.position[1] ?? 0),
8061
8390
  this.quality.underwaterAtmosphere,
8062
8391
  );
8063
- gl.uniform1f(u['uEmissiveGain'] ?? null, env.emissiveGain);
8064
- gl.uniform1f(u['uNightFactor'] ?? null, env.nightFactor);
8392
+ /* The emissive gain and the night factor, as the pass set them. See `uEmission`. */
8393
+ gl.uniform2fv(u['uEmission'] ?? null, this.emission);
8065
8394
  /* Absent means zero, so a world written before this term existed adds nothing. */
8066
8395
  gl.uniform1f(u['uNightEmissive'] ?? null, env.nightEmissive ?? 0);
8067
8396
  /*
@@ -8083,8 +8412,16 @@ export class WebGL2Renderer implements RendererApi {
8083
8412
  gl.uniform1f(u['uShadowMaxSlope'] ?? null, this.quality.directionalShadowMaxSlope);
8084
8413
  gl.uniform1i(u['uShadowFilterTaps'] ?? null, this.quality.shadowFilterTaps);
8085
8414
  gl.uniform3fv(u['uHighlightMin'] ?? null, env.highlightMin);
8086
- gl.uniform3fv(u['uHighlightMax'] ?? null, env.highlightMax);
8087
- gl.uniform1f(u['uHighlightGain'] ?? null, env.highlightGain);
8415
+ /* The far corner, and the gain in its w. See `uHighlightMax`. */
8416
+ /* An environment written by hand may leave the box out; then there is none, as before. */
8417
+ const max = env.highlightMax ?? NO_HIGHLIGHT;
8418
+ gl.uniform4f(
8419
+ u['uHighlightMax'] ?? null,
8420
+ max[0] ?? 0,
8421
+ max[1] ?? 0,
8422
+ max[2] ?? 0,
8423
+ env.highlightGain ?? 0,
8424
+ );
8088
8425
 
8089
8426
  bindPointLights(gl, u, env, this.quality.pointLightFalloff);
8090
8427
  this.bindClusters(u, camera, env);
@@ -8184,7 +8521,8 @@ export class WebGL2Renderer implements RendererApi {
8184
8521
  * between them; there is one array, so `RenderQuality.environmentPrefilter` decides what the
8185
8522
  * convolution writes and a profile that changes it has to bake again.
8186
8523
  */
8187
- gl.uniform1f(u['uEnvironmentPrefiltered'] ?? null, this.quality.environmentPrefilter ? 1 : 0);
8524
+ this.environmentDials[0] = this.quality.environmentPrefilter ? 1 : 0;
8525
+ gl.uniform3fv(u['uEnvironmentDials'] ?? null, this.environmentDials);
8188
8526
  gl.uniform1f(u['uEnvironmentMaxLod'] ?? null, array?.ggxMaxLevel ?? 0);
8189
8527
  /*
8190
8528
  * The map's edge, which used to be `exp2(maxLod)` and is not any more: the chain stops one
@@ -8383,12 +8721,17 @@ export class WebGL2Renderer implements RendererApi {
8383
8721
  if (this.quality.cullDraws && this.occluded(mesh.bounds, model)) return;
8384
8722
  this.drawBudget.ask();
8385
8723
  this.takeMaterial();
8386
- /* Skin under the screen-space blur draws its two halves: the frame's, then at once its diffuse. */
8724
+ /*
8725
+ * Skin under the screen-space blur draws its halves: the frame's, then at once its diffuse's
8726
+ * light and its colour, each into the blur's own target.
8727
+ */
8387
8728
  const splits = this.skinSplits();
8388
8729
  this.drawMeshHalf(mesh, model, depthLayer, tint, splits ? 'scene' : 'whole');
8389
8730
  if (splits && this.skinScatter !== null) {
8390
8731
  this.skinScatter.begin();
8391
8732
  this.drawMeshHalf(mesh, model, depthLayer, tint, 'diffuse');
8733
+ this.skinScatter.beginAlbedo();
8734
+ this.drawMeshHalf(mesh, model, depthLayer, tint, 'albedo');
8392
8735
  this.sceneTarget?.bind();
8393
8736
  }
8394
8737
  }
@@ -8408,6 +8751,7 @@ export class WebGL2Renderer implements RendererApi {
8408
8751
  !this.skinScattered &&
8409
8752
  !this.reflectionPassActive &&
8410
8753
  !this.probePassActive &&
8754
+ !this.capturePassActive &&
8411
8755
  !this.framePresented &&
8412
8756
  skin.ready(scene)
8413
8757
  );
@@ -8428,7 +8772,8 @@ export class WebGL2Renderer implements RendererApi {
8428
8772
  /* Latched whether or not skin was drawn: a skin drawn after this point is the whole surface. */
8429
8773
  this.skinScattered = true;
8430
8774
  if (!skin.pending) return;
8431
- if (this.reflectionPassActive || this.probePassActive || projection === null) return;
8775
+ if (this.reflectionPassActive || this.probePassActive || this.capturePassActive) return;
8776
+ if (projection === null) return;
8432
8777
  const depth = scene.snapshotDepth(true);
8433
8778
  mat4.invert(this.skinInverseProjection, projection);
8434
8779
  skin.spread(scene, depth, this.emptyTexture2D, projection, this.skinInverseProjection);
@@ -8574,8 +8919,9 @@ export class WebGL2Renderer implements RendererApi {
8574
8919
  * depth 1.0 against a buffer cleared to 1.0, and under `LESS` it would not draw at
8575
8920
  * all. Film, caustics and text lie flush on surfaces for the same reason.
8576
8921
  */
8577
- /* A skin's diffuse half is the same surface again: it finds the depth its frame half wrote. */
8578
- const again = half === 'diffuse';
8922
+ /* A skin's diffuse and colour halves are the same surface again: each finds the depth its
8923
+ frame half wrote. */
8924
+ const again = half === 'diffuse' || half === 'albedo';
8579
8925
  gl.depthFunc(again ? gl.EQUAL : this.reversedDepth ? gl.GREATER : gl.LESS);
8580
8926
  if (again) gl.depthMask(false);
8581
8927
  /* A dithered cutout in a multisampled frame with no temporal resolve: its share becomes the
@@ -9030,6 +9376,62 @@ export class WebGL2Renderer implements RendererApi {
9030
9376
  return true;
9031
9377
  }
9032
9378
 
9379
+ /**
9380
+ * One layer of the declared probe grid from an image, the way `setEnvironmentImage` fills a
9381
+ * single probe: the same equirectangular image of linear RGB floats, projected onto the capture
9382
+ * cube and prefiltered into that layer, the others left as they are. Returns false — and says
9383
+ * why — where there is no probe, no grid, or no such layer.
9384
+ *
9385
+ * **What it is for**: a scene baked somewhere else. Another engine's reflection captures are
9386
+ * images taken where each one stands; a consumer fits them to a lattice — for each point, the
9387
+ * capture whose reach holds it — and hands each point its image here, where `bakeProbe` would
9388
+ * have drawn this engine's own world into it.
9389
+ *
9390
+ * `options.irradiance` decides, at grid scope as a bake's does, whether the grid also lights the
9391
+ * scene's diffuse ambient. A grid that crossfades writes the set being swept, as a bake does.
9392
+ */
9393
+ setProbeLayerImage(
9394
+ layer: number,
9395
+ image: {
9396
+ readonly width: number;
9397
+ readonly height: number;
9398
+ readonly data: Float32Array;
9399
+ },
9400
+ options?: ProbeBakeOptions,
9401
+ ): boolean {
9402
+ if (this.contextLost) return false;
9403
+ const probe = this.probeCapture;
9404
+ const array = this.probeArray;
9405
+ const grid = this.probes;
9406
+ const sweeps = this.probeSweeps;
9407
+ if (probe === null || array === null || grid === null || sweeps === null) {
9408
+ console.warn(
9409
+ 'Renderer: setProbeLayerImage needs a reflection probe (`reflectionProbeSize`) and a grid ' +
9410
+ 'declared with setProbeGrid; the image was ignored.',
9411
+ );
9412
+ return false;
9413
+ }
9414
+ if (!Number.isInteger(layer) || layer < 0 || layer >= grid.layers) {
9415
+ console.warn(
9416
+ `Renderer: setProbeLayerImage was given layer ${layer}, and the grid has ${grid.layers}.`,
9417
+ );
9418
+ return false;
9419
+ }
9420
+ if (!probe.upload(this.gl, equirectToCubeFaces(image, probe.size))) return false;
9421
+ this.probeAmbient = options?.irradiance ?? true;
9422
+ this.environmentPrefilter?.run(
9423
+ this.gl,
9424
+ probe,
9425
+ array,
9426
+ sweeps.writeLayer(layer),
9427
+ ENVIRONMENT_TEXTURE_UNIT,
9428
+ this.quality.environmentPrefilterSamples,
9429
+ this.quality.environmentPrefilter,
9430
+ );
9431
+ sweeps.baked(layer);
9432
+ return true;
9433
+ }
9434
+
9033
9435
  /**
9034
9436
  * Draw a volume of light: a beam from a lamp, a shaft through a window, the cone under a
9035
9437
  * street light.
@@ -9420,6 +9822,15 @@ const NO_MODEL_PARAMS = new Float32Array(MODEL_PARAM_FLOATS);
9420
9822
  * The lit vertex stage a program of this variant compiles: the standard programs' and the cloth's,
9421
9823
  * which `modelProgram` pairs a model's fragment stage with.
9422
9824
  */
9825
+ /**
9826
+ * The vertex stage a model's program draws with: the variant's own, but for a lightmapped instanced
9827
+ * batch, whose tint lane is each instance's region of the page. See `lightmap.ts`.
9828
+ */
9829
+ function modelVertexOf(variant: string, kind: SurfaceModelKind): string {
9830
+ const vertex = litVertexOf(variant);
9831
+ return kind === 'lightmap' && variant === 'instanced' ? lightmapRegionsBound(vertex) : vertex;
9832
+ }
9833
+
9423
9834
  function litVertexOf(variant: string): string {
9424
9835
  if (variant.startsWith('cloth')) {
9425
9836
  const eight = variant[5] === '8';