@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
@@ -22,7 +22,7 @@ import { batchBoxVisible, cullsInstances } from '../../instanceCull.js';
22
22
  import { OcclusionBuffer } from '../../occlusion.js';
23
23
  import { boundsVisible } from '../../visibility.js';
24
24
  import { createPassRegistry, drainRegistry, passAt, registerIn, unregisterIn } from '../../pass.js';
25
- import { TABLE_HEIGHT, TABLE_WIDTH, buildLightClusters, clusteredMode, createClusterLightSet, createClusterTable, fillClusterLightSet, } from '../../clusteredLights.js';
25
+ import { TABLE_HEIGHT, TABLE_WIDTH, buildLightClusters, clusteredMode, createClusterLightSet, createClusterTable, clusteredLightTotal, fillClusterLightSet, lightChannelsOf, } from '../../clusteredLights.js';
26
26
  import { clamp } from '../../../math/scalar.js';
27
27
  import { attachContextLoss } from '../../contextLoss.js';
28
28
  import { isWeakGpuFamily, webgl2RendererName } from '../../gpuCapability.js';
@@ -32,7 +32,8 @@ import { FULL_LIGHT_BUDGET, nextLightBudget, planLightBudget, } from '../../unif
32
32
  import { FILM_FRAG, FILM_VERT } from '../../shaders/film.js';
33
33
  import { SceneTarget } from '../../sceneTarget.js';
34
34
  import { FILM_LOOK_WITHOUT_COMPOSITE } from '../../shaders/filmLook.js';
35
- import { bloomProfileWarning, bloomThresholdOf } from '../../bloomChain.js';
35
+ import { AMBIENT_SH_FLOATS, packAmbientSH } from '../../ambientHarmonics.js';
36
+ import { bloomProfileWarning, bloomThresholdOf, defaultBloomResponse, resolveBloomResponse, } from '../../bloomChain.js';
36
37
  import { createEmptyTexture2D, createEmptyTexture2DArray, createEmptyTexture3D, createEmptyTextureCube, } from '../../emptyTexture.js';
37
38
  import { LightField } from '../../driftLight/lightField.js';
38
39
  import { WorldLightField } from '../../driftLight/worldLightField.js';
@@ -40,8 +41,9 @@ import { createDriftLightUniforms, resolveDriftLight } from '../../driftLight/un
40
41
  import { flatFrag, flatVert } from '../../shaders/flat/index.js';
41
42
  import { skinEight } from '../../shaders/skinning.js';
42
43
  import { MODEL_MAP_TEXTURE_UNIT, MORPH_DELTA_TEXTURE_UNIT, SKIN_PALETTE_TEXTURE_UNIT, } from '../../lightBudget.js';
43
- import { MODEL_PARAM_FLOATS, eyeAxisInWorld, packModel } from '../../surfaceModel.js';
44
- import { modelBound, skinHalfBound } from '../../shaders/flat/models.js';
44
+ import { MODEL_PARAM_FLOATS, eyeAxisInWorld, packModel, skinProfileDistance, } from '../../surfaceModel.js';
45
+ import { lightmapRegionsBound, modelBound, skinHalfBound } from '../../shaders/flat/models.js';
46
+ import { lightmapTexels, warnRegionsWithoutPage, warnUnpagedLightmap, withLightmapUvs, } from '../../lightmap.js';
45
47
  import { GlSkinScatter } from './skinScatter.js';
46
48
  import { CLOTH_BINDING_TEXTURE_UNIT, CLOTH_PARTICLES_TEXTURE_UNIT, CLOTH_REST_TEXTURE_UNIT, } from '../../lightBudget.js';
47
49
  import { SkinPaletteTexture } from './skinPaletteTexture.js';
@@ -79,11 +81,13 @@ import { GlassTintPrograms } from './glassTintPrograms.js';
79
81
  import { FACE_COUNT, PointShadowMap } from '../../pointShadowMap.js';
80
82
  import { createResolvedPointShadows, DEFAULT_SOURCE_RADIUS } from '../../pointShadowImage.js';
81
83
  import { createBakeScratch, runPointShadowBakes, selectCastingLights, } from '../../pointShadowBudget.js';
82
- import { DIRECTIONAL_SHADOW_UNITS, SUN_GLASS_TINT_TEXTURE_UNIT, POINT_GLASS_TINT_TEXTURE_UNIT, LIVE_POINT_SHADOW_MAPS, MAX_POINT_LIGHTS, NORMAL_TEXTURE_UNIT, ORM_TEXTURE_UNIT, POINT_SHADOW_POOL, POINT_SHADOW_UNITS, CLUSTER_TABLE_TEXTURE_UNIT, EMISSIVE_TEXTURE_UNIT, ENVIRONMENT_TEXTURE_UNIT, SURFACE_TEXTURE_UNIT, bindPointLights, REFRACT_SCENE_TEXTURE_UNIT, DRIFT_LIGHT_ATLAS_TEXTURE_UNIT, DRIFT_LIGHT_INDEX_TEXTURE_UNIT, COOKIE_TILE, FIXTURE_ATLAS_TEXTURE_UNIT, SURFACE_EFFECTS_TEXTURE_UNIT, bindAreaLights, bindAreaShadows, openCones, } from '../../lightBudget.js';
84
+ import { DIRECTIONAL_SHADOW_UNITS, SUN_GLASS_TINT_TEXTURE_UNIT, POINT_GLASS_TINT_TEXTURE_UNIT, LIVE_POINT_SHADOW_MAPS, MAX_POINT_LIGHTS, NORMAL_TEXTURE_UNIT, ORM_TEXTURE_UNIT, POINT_SHADOW_POOL, POINT_SHADOW_UNITS, CLUSTER_TABLE_TEXTURE_UNIT, EMISSIVE_TEXTURE_UNIT, ENVIRONMENT_TEXTURE_UNIT, SURFACE_TEXTURE_UNIT, bindPointLights, REFRACT_SCENE_TEXTURE_UNIT, DRIFT_LIGHT_ATLAS_TEXTURE_UNIT, DRIFT_LIGHT_INDEX_TEXTURE_UNIT, COOKIE_TILE, FIXTURE_ATLAS_TEXTURE_UNIT, SURFACE_EFFECTS_TEXTURE_UNIT, bindAreaLights, bindAreaShadows, openCones, PARTICLE_DEPTH_UNIT, } from '../../lightBudget.js';
83
85
  import { resolveRenderQuality } from '../../renderQuality.js';
84
86
  import { drawingBufferSize } from '../../drawingBuffer.js';
85
87
  import { Mesh, createMeshIncremental } from '../../mesh.js';
86
88
  import { SurfaceTexture } from '../../surfaceTexture.js';
89
+ import { sceneCaptureTexels } from '../../sceneCapture.js';
90
+ import { GlSceneCaptureTarget } from './sceneCaptureTarget.js';
87
91
  import { glCompressedFormats } from '../../glCompressed.js';
88
92
  import { PlumeRenderer } from '../../plumeRenderer.js';
89
93
  import { createScatterDeform, resolveScatterDeform } from '../../scatterDeform.js';
@@ -180,6 +184,8 @@ export function createEnvironment(overrides = {}) {
180
184
  lightIesAxes: new Float32Array(MAX_POINT_LIGHTS * 3),
181
185
  /* −1, because tile 0 is a real cookie and no light here has asked for one. */
182
186
  lightCookies: new Float32Array(MAX_POINT_LIGHTS).fill(-1),
187
+ /* Channel 1, the default every surface is on, so every light lights everything. */
188
+ lightChannels: new Float32Array(MAX_POINT_LIGHTS).fill(1),
183
189
  /* None, so the shader's loop breaks before it reads anything. */
184
190
  areaLights: null,
185
191
  activeLightWorldIndices: new Int32Array(MAX_POINT_LIGHTS).fill(-1),
@@ -257,6 +263,8 @@ const IDENTITY_MAT4 = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
257
263
  */
258
264
  export { MAX_DEPTH_LAYER } from '../../depthConvention.js';
259
265
  /** The identity tint, handed back after any draw that asked for one. See `drawMesh`. */
266
+ /** The far corner of no highlight box: what an environment that names none reads. */
267
+ const NO_HIGHLIGHT = [0, 0, 0];
260
268
  const WHITE_TINT = new Float32Array([1, 1, 1]);
261
269
  /** The lit stage's clip plane when there is no mirror: zero, which clips nothing. */
262
270
  const NO_CLIP_PLANE = new Float32Array(4);
@@ -640,6 +648,23 @@ export class WebGL2Renderer {
640
648
  motionBlurScale = 1;
641
649
  /** How much of the bloom ceiling this frame takes. See `setBloom`. */
642
650
  bloomScale = 1;
651
+ /** The draw's own ambient, as `packAmbientSH` writes it; zeros are the frame's. */
652
+ ambientSH = new Float32Array(AMBIENT_SH_FLOATS);
653
+ /** Scratch for a sprite's depth conversion, so a particle draw allocates nothing. */
654
+ spriteInverseProjection = mat4.create();
655
+ /** Said once: a sprite asked for a soft edge where no depth copy could be had. */
656
+ warnedSpriteDepth = false;
657
+ /** Said once: more rectangles than the fixed arm shades, and no froxel table for the rest. */
658
+ warnedAreaOverflow = false;
659
+ /**
660
+ * `uEnvironmentDials` as it stands: which chain the probe holds, the reflectivity, the gain. One
661
+ * vector on the device, so written whole, and kept here so a setter moving one keeps the others.
662
+ */
663
+ environmentDials = new Float32Array(3);
664
+ /** `uEmission` as it stands: the emissive gain and the night factor. */
665
+ emission = new Float32Array([1, 0]);
666
+ /** The ramp and each level's tint, as `resolveBloomResponse` writes them; the subtraction and white. */
667
+ bloomResponse = defaultBloomResponse();
643
668
  /** In scene units, once `setBloom` has moved it; null reads the profile's. */
644
669
  bloomThresholdSet = null;
645
670
  /**
@@ -852,6 +877,15 @@ export class WebGL2Renderer {
852
877
  warnedEnvironmentAmbient = false;
853
878
  /** Whether a probe is being baked, so the passes that would recurse into one can tell. */
854
879
  probePassActive = false;
880
+ /**
881
+ * Whether a scene capture is being drawn (`captureScene`): an off-screen pass like a bake, so the
882
+ * frame's own view, its refraction copy and skin's spread are left alone, but lit as the frame is.
883
+ */
884
+ capturePassActive = false;
885
+ /** Each capture's framebuffer, beside the texture a material binds. See `createSceneCapture`. */
886
+ captureTargets = new WeakMap();
887
+ /** Said once, where a driver refuses to draw into a capture. */
888
+ warnedCapture = false;
855
889
  /** Whether the bake under way lights its faces by the grid. `ProbeBakeOptions.bounce`. */
856
890
  probeBounce = false;
857
891
  /** The clip-control extension, or null where this context does not offer it. */
@@ -918,7 +952,8 @@ export class WebGL2Renderer {
918
952
  * the camera's matrix unchanged and multiplies nothing.
919
953
  */
920
954
  viewProjFor(camera) {
921
- const scene = this.probePassActive
955
+ /* A capture's rows are flipped as a probe face's are, so it reads as an image does. */
956
+ const scene = this.probePassActive || this.capturePassActive
922
957
  ? cubeFaceProjection(this.cubeFaceViewProj, this.sceneMatrix(camera.viewProjection))
923
958
  : this.sceneMatrix(camera.viewProjection);
924
959
  /*
@@ -1675,9 +1710,7 @@ export class WebGL2Renderer {
1675
1710
  gl.activeTexture(gl.TEXTURE0 + FIXTURE_ATLAS_TEXTURE_UNIT);
1676
1711
  gl.bindTexture(gl.TEXTURE_2D, this.fixtureTexture);
1677
1712
  gl.uniform1i(u['uFixtureAtlas'] ?? null, FIXTURE_ATLAS_TEXTURE_UNIT);
1678
- gl.uniform1f(u['uIesAtlasRows'] ?? null, this.iesRows);
1679
- gl.uniform1f(u['uIesPlaneCount'] ?? null, this.iesPlanes);
1680
- gl.uniform1f(u['uCookieTiles'] ?? null, this.cookieTiles);
1713
+ gl.uniform3f(u['uFixtureShape'] ?? null, this.iesRows, this.iesPlanes, this.cookieTiles);
1681
1714
  gl.activeTexture(gl.TEXTURE0);
1682
1715
  }
1683
1716
  bindClusters(u, camera, env) {
@@ -1723,7 +1756,8 @@ export class WebGL2Renderer {
1723
1756
  gl.uniform4fv(u['uClusterFrustum'] ?? null, this.clusterFrustum);
1724
1757
  gl.uniformMatrix4fv(u['uView'] ?? null, false, camera.view);
1725
1758
  this.clusterTable ??= createClusterTable();
1726
- const lights = fillClusterLightSet(env, this.clusterLights);
1759
+ /* The rectangles past the fixed arm's go into the table. See `ClusterLightSet.areas`. */
1760
+ const lights = fillClusterLightSet(env, this.clusterLights, this.shadedAreaLights);
1727
1761
  /* A slot is read only under `POINT_SHADOWS`; see `lightHasFixture`. */
1728
1762
  const shadowSlots = this.quality.pointShadows ? this.lightBudget.maxLights : 0;
1729
1763
  buildLightClusters(lights, camera.view, near, far, tanHalfFovY, aspect, this.clusterTable,
@@ -1735,7 +1769,7 @@ export class WebGL2Renderer {
1735
1769
  * guards the read as well, for a build that reaches it another way.
1736
1770
  */
1737
1771
  shadowSlots);
1738
- gl.uniform1i(u['uClustered'] ?? null, clusteredMode(lights, lights.count, shadowSlots));
1772
+ gl.uniform1i(u['uClustered'] ?? null, clusteredMode(lights, clusteredLightTotal(lights), shadowSlots));
1739
1773
  gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, TABLE_WIDTH, TABLE_HEIGHT, gl.RGBA_INTEGER, gl.UNSIGNED_INT, this.clusterTable);
1740
1774
  }
1741
1775
  /**
@@ -2340,8 +2374,9 @@ export class WebGL2Renderer {
2340
2374
  * one. Measured on hardware: WebGL2 drew neither the bend nor the bulge while WebGPU drew
2341
2375
  * both, which is the two-backend comparison doing exactly what it is for.
2342
2376
  */
2343
- this.warmVariants(data);
2344
- return new Mesh(this.gl, data, options.dynamic === true);
2377
+ const drawn = withLightmapUvs(data);
2378
+ this.warmVariants(drawn);
2379
+ return new Mesh(this.gl, drawn, options.dynamic === true);
2345
2380
  }
2346
2381
  /**
2347
2382
  * Geometry in, handle out — but the geometry lands over as many frames as the caller gives it.
@@ -2353,8 +2388,9 @@ export class WebGL2Renderer {
2353
2388
  * the signal that it was abandoned.
2354
2389
  */
2355
2390
  createMeshIncremental(data, options = {}) {
2356
- this.warmVariants(data);
2357
- const { mesh, upload } = createMeshIncremental(this.gl, data, options.dynamic === true);
2391
+ const drawn = withLightmapUvs(data);
2392
+ this.warmVariants(drawn);
2393
+ const { mesh, upload } = createMeshIncremental(this.gl, drawn, options.dynamic === true);
2358
2394
  return {
2359
2395
  mesh,
2360
2396
  upload: {
@@ -2517,6 +2553,80 @@ export class WebGL2Renderer {
2517
2553
  createSurfaceTextureArray(sources, options = {}) {
2518
2554
  return new SurfaceTexture(this.gl, sources, options, this.compressedFormats);
2519
2555
  }
2556
+ /** A baked lightmap page, as a `lightmapModel` material's `modelMap`. See `RendererApi`. */
2557
+ createLightmap(page) {
2558
+ return new SurfaceTexture(this.gl, lightmapTexels(page), { wrap: 'clamp', mipmap: false });
2559
+ }
2560
+ /**
2561
+ * A texture the scene can be drawn into, for a material to show. See `RendererApi` and
2562
+ * `sceneCapture.ts`. Half floats where the frame keeps range, as a probe does.
2563
+ */
2564
+ createSceneCapture(width, height) {
2565
+ const float = this.quality.hdrScene && this.gl.getExtension('EXT_color_buffer_float') !== null;
2566
+ const texture = new SurfaceTexture(this.gl, sceneCaptureTexels(width, height, float), {
2567
+ wrap: 'clamp',
2568
+ mipmap: false,
2569
+ });
2570
+ this.captureTargets.set(texture, new GlSceneCaptureTarget(this.gl, texture, width, height, this.sceneTarget?.sampleCount ?? 1, float));
2571
+ return texture;
2572
+ }
2573
+ /**
2574
+ * Draw the scene into a capture from `camera`: `draw` is handed the camera, its matrices updated
2575
+ * for the capture's shape, and submits the scene as it would to the screen. See `RendererApi`.
2576
+ *
2577
+ * **Radiance, as a probe bake stores it**: the output transform and exposure are held off for the
2578
+ * pass, so the frame grades a screen showing it once. No temporal jitter, which a capture has no
2579
+ * resolve to undo. Returns whether it drew: false for a texture that is not a capture, or one the
2580
+ * driver will not draw into.
2581
+ */
2582
+ captureScene(capture, camera, clearColor, draw) {
2583
+ if (this.contextLost)
2584
+ return false;
2585
+ const target = this.captureTargets.get(capture);
2586
+ if (target === undefined)
2587
+ return false;
2588
+ if (!target.usable) {
2589
+ if (!this.warnedCapture) {
2590
+ this.warnedCapture = true;
2591
+ console.warn('Renderer: this driver will not draw into a scene capture, so it keeps its clear. ' +
2592
+ 'Nothing else is affected.');
2593
+ }
2594
+ return false;
2595
+ }
2596
+ camera.updateMatrices(target.width / target.height);
2597
+ const { gl } = this;
2598
+ const jittering = this.temporalJittering;
2599
+ this.temporalJittering = false;
2600
+ this.capturePassActive = true;
2601
+ this.useFlatProgram();
2602
+ gl.uniform1i(this.flatUniforms['uOutputTransform'] ?? null, 0);
2603
+ gl.uniform1f(this.flatUniforms['uOutputExposure'] ?? null, 1);
2604
+ this.materials.dirty();
2605
+ target.begin(gl, clearColor, this.depthClear);
2606
+ /*
2607
+ * **Rows flipped, as a probe face's are.** A GL framebuffer's first row is its bottom and an
2608
+ * image's is its top, so a capture drawn upright would be shown upside down by every material
2609
+ * reading it as the image it is; `viewProjFor` negates clip y for the pass, and negating one axis
2610
+ * reverses every triangle's winding, which the front face follows. WebGPU's rows already run
2611
+ * the image's way.
2612
+ */
2613
+ gl.frontFace(gl.CW);
2614
+ try {
2615
+ draw(camera);
2616
+ }
2617
+ finally {
2618
+ target.end(gl);
2619
+ gl.frontFace(gl.CCW);
2620
+ this.useFlatProgram();
2621
+ gl.uniform1i(this.flatUniforms['uOutputTransform'] ?? null, this.gradeCode());
2622
+ gl.uniform1f(this.flatUniforms['uOutputExposure'] ?? null, this.gradeExposure());
2623
+ this.materials.dirty();
2624
+ this.capturePassActive = false;
2625
+ this.temporalJittering = jittering;
2626
+ this.restoreViewport();
2627
+ }
2628
+ return true;
2629
+ }
2520
2630
  /**
2521
2631
  * Replace a surface texture's pixels, keeping the GPU object and its sampler state.
2522
2632
  *
@@ -2538,6 +2648,8 @@ export class WebGL2Renderer {
2538
2648
  if (this.contextLost)
2539
2649
  return;
2540
2650
  texture.dispose(this.gl);
2651
+ this.captureTargets.get(texture)?.dispose(this.gl);
2652
+ this.captureTargets.delete(texture);
2541
2653
  }
2542
2654
  /**
2543
2655
  * Choose the surface texture the following `drawMesh` calls sample, or `null` for none.
@@ -2590,7 +2702,7 @@ export class WebGL2Renderer {
2590
2702
  return held;
2591
2703
  const { gl } = this;
2592
2704
  const label = `flat.${variant}.${kind}.${half}`;
2593
- const program = compileProgram(gl, litVertexOf(variant), skinHalfBound(modelBound(this.flatFragSource, kind), half), label);
2705
+ const program = compileProgram(gl, modelVertexOf(variant, kind), skinHalfBound(modelBound(this.flatFragSource, kind), half), label);
2594
2706
  const made = { program, uniforms: uniformLocations(gl, program, label) };
2595
2707
  this.modelFlat.set(key, made);
2596
2708
  this.flatTargets.push(made);
@@ -2738,8 +2850,9 @@ export class WebGL2Renderer {
2738
2850
  this.currentMaterial = material;
2739
2851
  /* What the blur spreads this skin by: the last material to name a profile says. */
2740
2852
  const model = material?.model;
2853
+ warnUnpagedLightmap(model, material?.modelMap);
2741
2854
  if (model?.kind === 'skin' && this.skinScatter !== null) {
2742
- this.skinScatter.setProfile(model.profile, model.scatterColor[0] * model.radius, model.scatterColor[1] * model.radius, model.scatterColor[2] * model.radius);
2855
+ this.skinScatter.setProfile(model.profile, skinProfileDistance(model, 0), skinProfileDistance(model, 1), skinProfileDistance(model, 2));
2743
2856
  }
2744
2857
  /*
2745
2858
  * Written to every flat program, because material state persists across draws and a skinned
@@ -2773,7 +2886,8 @@ export class WebGL2Renderer {
2773
2886
  v[0] = material?.cutout ?? 0;
2774
2887
  v[1] = CUTOUT_RESOLVE_CODE[this.cutoutResolveNow(material, translucent)];
2775
2888
  v[2] = this.cutoutFrame;
2776
- v[3] = 0;
2889
+ /* The material's lighting channels, which the lamps' are compared with. See `uCutout`. */
2890
+ v[3] = lightChannelsOf(material?.lightChannels);
2777
2891
  return v;
2778
2892
  }
2779
2893
  /**
@@ -2862,7 +2976,10 @@ export class WebGL2Renderer {
2862
2976
  if (modelMap !== null)
2863
2977
  modelMap.bind(gl, MODEL_MAP_TEXTURE_UNIT);
2864
2978
  gl.uniform1i(u['uModelMap'] ?? null, MODEL_MAP_TEXTURE_UNIT);
2865
- packModel(material?.model ?? null, modelMap !== null, this.modelParams);
2979
+ packModel(material?.model ?? null, modelMap !== null, this.modelParams, material?.physicalSpecular === true);
2980
+ /* The first physical highlight asked for is when the lit programs start carrying one. */
2981
+ if (material?.physicalSpecular === true)
2982
+ this.askLit('PHYSICAL_SPECULAR');
2866
2983
  gl.uniform4fv(u['uModelParams'] ?? null, this.modelParams);
2867
2984
  /* The albedo's effects table, or the stand-in the shader reads as none. Before the early
2868
2985
  return, so a material with no albedo still leaves a complete texture on the unit. */
@@ -2884,6 +3001,8 @@ export class WebGL2Renderer {
2884
3001
  gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.emptyTexture2DArray);
2885
3002
  this.materialFlags[0] = 0;
2886
3003
  gl.uniform4iv(u['uMaterialFlags'] ?? null, this.materialFlags);
3004
+ /* No image to cut, and the channels all the same: a material needs no albedo to name them. */
3005
+ gl.uniform4f(u['uCutout'] ?? null, 0, 0, 0, lightChannelsOf(material?.lightChannels));
2887
3006
  return;
2888
3007
  }
2889
3008
  albedo.bind(gl, SURFACE_TEXTURE_UNIT);
@@ -2921,17 +3040,27 @@ export class WebGL2Renderer {
2921
3040
  gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.emptyTexture2DArray);
2922
3041
  }
2923
3042
  /**
2924
- * A pass-level scale over the grain the geometry itself declared, 0 to 1.
3043
+ * The ambient the following draws take, as nine spherical-harmonic coefficients of incoming
3044
+ * radiance — red, green and blue of each, twenty-seven numbers — or `null` for the frame's own:
3045
+ * the sky's gradient, or the probes'. See `ambientHarmonics.ts` for the basis and the axes.
2925
3046
  *
2926
- * Which surfaces are mineral is now `MeshData.grain`, stated per vertex through
2927
- * `MeshBuilder.setGrain` — because it is a property of a material, and absent means none.
2928
- * This stays as the override for the case that has no answer to state: an imported model
2929
- * arrives with no grain information at all, and a caller drawing one decides for it.
2930
- *
2931
- * Pass state, like `setSurfaceTexture`, because a material covers many draws. The default
2932
- * is 1 and `bindMeshPass` restores it, so it scales geometry that declared grain and
2933
- * leaves geometry that did not exactly as it is.
3047
+ * **For a thing lit by where it stands**: a consumer sampling a stage's baked volume of indirect
3048
+ * light at a character every frame sets the result around that character's draws, and the
3049
+ * character takes the light of its spot rather than the frame's. It replaces the diffuse ambient
3050
+ * only — a glossy surface still reflects the probes — and it is pass state like the material,
3051
+ * which `bindMeshPass` clears, so a mirror or a probe bake drawn after does not inherit it.
2934
3052
  */
3053
+ setAmbientSH(coefficients) {
3054
+ if (this.contextLost)
3055
+ return;
3056
+ packAmbientSH(coefficients, this.ambientSH);
3057
+ this.materials.dirty();
3058
+ for (const target of this.flatTargets) {
3059
+ this.gl.useProgram(target.program);
3060
+ this.gl.uniform4fv(target.uniforms['uAmbientSH[0]'] ?? null, this.ambientSH);
3061
+ }
3062
+ this.useFlatProgram();
3063
+ }
2935
3064
  /**
2936
3065
  * How much of the environment the following draws mirror, 0 to 1.
2937
3066
  *
@@ -2946,7 +3075,8 @@ export class WebGL2Renderer {
2946
3075
  if (this.contextLost)
2947
3076
  return;
2948
3077
  this.materials.dirty();
2949
- this.writeFlatFloat('uReflectivity', Math.min(1, Math.max(0, amount)));
3078
+ this.environmentDials[1] = Math.min(1, Math.max(0, amount));
3079
+ this.writeFlatVector('uEnvironmentDials', this.environmentDials);
2950
3080
  }
2951
3081
  /**
2952
3082
  * How bright the environment the surfaces drawn next reflect is. 1 is the default and identity.
@@ -2964,7 +3094,8 @@ export class WebGL2Renderer {
2964
3094
  if (this.contextLost)
2965
3095
  return;
2966
3096
  this.materials.dirty();
2967
- this.writeFlatFloat('uEnvironmentGain', Math.max(0, gain));
3097
+ this.environmentDials[2] = Math.max(0, gain);
3098
+ this.writeFlatVector('uEnvironmentDials', this.environmentDials);
2968
3099
  }
2969
3100
  /**
2970
3101
  * Kept as a no-op that says so once, because it was a stand-in for a term that now exists.
@@ -2987,6 +3118,18 @@ export class WebGL2Renderer {
2987
3118
  console.warn('Renderer: setEnvironmentAmbient no longer does anything. A baked probe now lights a ' +
2988
3119
  'surface through projected irradiance, and there is nothing left to tune.');
2989
3120
  }
3121
+ /**
3122
+ * A pass-level scale over the grain the geometry itself declared, 0 to 1.
3123
+ *
3124
+ * Which surfaces are mineral is now `MeshData.grain`, stated per vertex through
3125
+ * `MeshBuilder.setGrain` — because it is a property of a material, and absent means none.
3126
+ * This stays as the override for the case that has no answer to state: an imported model
3127
+ * arrives with no grain information at all, and a caller drawing one decides for it.
3128
+ *
3129
+ * Pass state, like `setSurfaceTexture`, because a material covers many draws. The default
3130
+ * is 1 and `bindMeshPass` restores it, so it scales geometry that declared grain and
3131
+ * leaves geometry that did not exactly as it is.
3132
+ */
2990
3133
  setSurfaceGrain(amount) {
2991
3134
  if (this.contextLost)
2992
3135
  return;
@@ -3031,6 +3174,20 @@ export class WebGL2Renderer {
3031
3174
  }
3032
3175
  this.useFlatProgram();
3033
3176
  }
3177
+ /** A vector of material state, two to four floats, into every flat program. */
3178
+ writeFlatVector(name, value) {
3179
+ for (const target of this.flatTargets) {
3180
+ this.gl.useProgram(target.program);
3181
+ const at = target.uniforms[name] ?? null;
3182
+ if (value.length === 2)
3183
+ this.gl.uniform2fv(at, value);
3184
+ else if (value.length === 3)
3185
+ this.gl.uniform3fv(at, value);
3186
+ else
3187
+ this.gl.uniform4fv(at, value);
3188
+ }
3189
+ this.useFlatProgram();
3190
+ }
3034
3191
  /** One int of material state, into every flat program, for `writeFlatFloat`'s reason. */
3035
3192
  writeFlatInt(name, value) {
3036
3193
  for (const target of this.flatTargets) {
@@ -3094,8 +3251,13 @@ export class WebGL2Renderer {
3094
3251
  if (this.contextLost)
3095
3252
  return;
3096
3253
  this.materials.dirty();
3097
- this.writeFlatFloat('uRelief', Math.min(1, Math.max(0, amount)));
3098
- this.writeFlatFloat('uReliefCycles', Math.max(0.01, cyclesPerMetre));
3254
+ const strength = Math.min(1, Math.max(0, amount));
3255
+ const cycles = Math.max(0.01, cyclesPerMetre);
3256
+ for (const target of this.flatTargets) {
3257
+ this.gl.useProgram(target.program);
3258
+ this.gl.uniform2f(target.uniforms['uRelief'] ?? null, strength, cycles);
3259
+ }
3260
+ this.useFlatProgram();
3099
3261
  }
3100
3262
  /**
3101
3263
  * How hard the bound surface texture's own luminance turns the shading normal, 0 for not at all.
@@ -3153,7 +3315,8 @@ export class WebGL2Renderer {
3153
3315
  if (this.contextLost)
3154
3316
  return;
3155
3317
  this.materials.dirty();
3156
- this.writeFlatFloat('uEmissiveGain', gain);
3318
+ this.emission[0] = gain;
3319
+ this.writeFlatVector('uEmission', this.emission);
3157
3320
  }
3158
3321
  /* -- Instanced meshes --------------------------------------------------------------- */
3159
3322
  /** The instanced program, compiled on first use. See `ensureInstancedProgram`. */
@@ -3241,6 +3404,7 @@ export class WebGL2Renderer {
3241
3404
  submitInstanced(batch, data, opacity, blend, options) {
3242
3405
  if (this.contextLost)
3243
3406
  return;
3407
+ warnRegionsWithoutPage(this.currentMaterial?.model, data.lightmapRegions);
3244
3408
  const mesh = batch.mesh;
3245
3409
  /* Nor a batch of a mesh with nothing in it: see `Mesh.indexCount`. */
3246
3410
  if (!mesh.complete || mesh.indexCount === 0)
@@ -3387,7 +3551,7 @@ export class WebGL2Renderer {
3387
3551
  const glassy = resolveGlass(options.glass, this.glassScratch);
3388
3552
  if (refraction <= 0 && !glassy)
3389
3553
  return false;
3390
- if (this.probePassActive || this.reflectionPassActive)
3554
+ if (this.probePassActive || this.reflectionPassActive || this.capturePassActive)
3391
3555
  return false;
3392
3556
  const snapshot = this.sceneTarget?.snapshotColor(false, glassy) ?? null;
3393
3557
  if (snapshot === null)
@@ -3686,6 +3850,20 @@ export class WebGL2Renderer {
3686
3850
  createParticles(capacity, options) {
3687
3851
  return new ParticleBatch(this.gl, capacity, options);
3688
3852
  }
3853
+ /**
3854
+ * Particles a caller's own compute wrote: WebGPU's alone, since this backend has no compute stage
3855
+ * to have written them (`computeSupported`). Said once, and nothing is drawn.
3856
+ */
3857
+ drawDeviceParticles(_batch, _particles, _camera, _env, _timeSeconds) {
3858
+ if (this.warnedDeviceParticles)
3859
+ return;
3860
+ this.warnedDeviceParticles = true;
3861
+ console.warn('Renderer: drawDeviceParticles draws particles a compute shader wrote, and WebGL2 has no ' +
3862
+ 'compute stage, so nothing is drawn. Check `computeSupported`, and draw a pool with ' +
3863
+ '`drawParticles` where it is false.');
3864
+ }
3865
+ /** Said once: see `drawDeviceParticles`. */
3866
+ warnedDeviceParticles = false;
3689
3867
  /**
3690
3868
  * Draw a live particle pool.
3691
3869
  *
@@ -3702,8 +3880,10 @@ export class WebGL2Renderer {
3702
3880
  const { gl } = this;
3703
3881
  const u = batch.uniforms;
3704
3882
  gl.useProgram(batch.program);
3705
- batch.upload(gl, data);
3883
+ batch.upload(gl, data, camera.position);
3706
3884
  batch.bindMaterial(gl);
3885
+ if (batch.material === 'sprite')
3886
+ this.bindSpriteFades(batch, camera);
3707
3887
  gl.uniformMatrix4fv(u['uViewProj'] ?? null, false, this.viewProjFor(camera));
3708
3888
  gl.uniform3fv(u['uCameraPos'] ?? null, camera.position);
3709
3889
  gl.uniform1f(u['uTime'] ?? null, timeSeconds);
@@ -3730,6 +3910,38 @@ export class WebGL2Renderer {
3730
3910
  if (this.materialDoubleSided)
3731
3911
  gl.enable(gl.CULL_FACE);
3732
3912
  }
3913
+ /**
3914
+ * A sprite's two fades and the depth copy the soft one reads. The copy is the frame's opaque
3915
+ * depth, so it is taken only in the frame's own picture: in a mirror or a probe's face it would
3916
+ * be another view's, and the edge stays hard there. Without a scene target there is no copy at
3917
+ * all, and the renderer says so once rather than drawing a hard edge that was asked to be soft.
3918
+ */
3919
+ bindSpriteFades(batch, camera) {
3920
+ const { gl } = this;
3921
+ const u = batch.uniforms;
3922
+ const scene = this.sceneTarget;
3923
+ let depth = null;
3924
+ if (batch.softDepth > 0 &&
3925
+ !this.reflectionPassActive &&
3926
+ !this.probePassActive &&
3927
+ !this.capturePassActive) {
3928
+ depth = scene?.snapshotDepth(true) ?? null;
3929
+ if (depth === null && !this.warnedSpriteDepth) {
3930
+ this.warnedSpriteDepth = true;
3931
+ console.warn("WebGL2: a 'sprite' asked for a soft edge, which reads a copy of the frame's depth, and " +
3932
+ 'this profile keeps none — screenEffects is off, or the driver refused the copy. The ' +
3933
+ 'edge is hard.');
3934
+ }
3935
+ }
3936
+ gl.activeTexture(gl.TEXTURE0 + PARTICLE_DEPTH_UNIT);
3937
+ gl.bindTexture(gl.TEXTURE_2D, depth ?? this.emptyTexture2D);
3938
+ gl.uniform1i(u['uDepth'] ?? null, PARTICLE_DEPTH_UNIT);
3939
+ gl.uniform4f(u['uFade'] ?? null, batch.softDepth, batch.cameraFade, depth === null ? 0 : 1, 0);
3940
+ mat4.invert(this.spriteInverseProjection, camera.projection);
3941
+ const inverse = this.spriteInverseProjection;
3942
+ gl.uniform4f(u['uDepthToViewZ'] ?? null, inverse[10] ?? 0, inverse[14] ?? 0, inverse[11] ?? 0, inverse[15] ?? 1);
3943
+ gl.uniform2f(u['uViewport'] ?? null, scene?.frameWidth ?? gl.drawingBufferWidth, scene?.frameHeight ?? gl.drawingBufferHeight);
3944
+ }
3733
3945
  /** Build a batch for electrical arcs. `capacity` is in segments, not arcs. */
3734
3946
  createBolts(segmentCapacity, label = 'bolts') {
3735
3947
  return new BoltBatch(this.gl, segmentCapacity, label);
@@ -4584,6 +4796,7 @@ export class WebGL2Renderer {
4584
4796
  lightFixtures: this.litOn.LIGHT_FIXTURES,
4585
4797
  surfaceEffects: this.litOn.SURFACE_EFFECTS,
4586
4798
  driftLight: this.litOn.DRIFT_LIGHT,
4799
+ physicalSpecular: this.litOn.PHYSICAL_SPECULAR,
4587
4800
  maxLights: budget.maxLights,
4588
4801
  maxAreaLights: budget.maxAreaLights,
4589
4802
  });
@@ -4643,7 +4856,7 @@ export class WebGL2Renderer {
4643
4856
  for (const [key, entry] of this.modelFlat) {
4644
4857
  const [variant, kind, half] = key.split('|');
4645
4858
  const label = `flat.${variant}.${kind}.${half}`;
4646
- const program = compileProgram(gl, litVertexOf(variant), skinHalfBound(modelBound(this.flatFragSource, kind), half), label);
4859
+ const program = compileProgram(gl, modelVertexOf(variant, kind), skinHalfBound(modelBound(this.flatFragSource, kind), half), label);
4647
4860
  gl.deleteProgram(entry.program);
4648
4861
  this.modelFlat.set(key, { program, uniforms: uniformLocations(gl, program, label) });
4649
4862
  }
@@ -5386,11 +5599,19 @@ export class WebGL2Renderer {
5386
5599
  * and wrong for a day whose exposure spans 2.5 to 14: a courtyard blooming at noon, or candles
5387
5600
  * never blooming at night. A caller whose exposure moves passes what it wants on screen over the
5388
5601
  * exposure. Omitted leaves it where it was, so a caller that never passes one keeps the profile's.
5602
+ *
5603
+ * **`response` is how a colour comes in past the threshold and what colour each octave of the
5604
+ * halo takes** — a ramp in place of the subtraction, and a tint a level — held until moved like
5605
+ * the threshold; `null` goes back to the subtraction and white. See `BloomResponse`: it is what
5606
+ * matches a stage built for another engine's bloom, whose tints are how a strength past 1 is
5607
+ * asked for.
5389
5608
  */
5390
- setBloom(scale, threshold) {
5609
+ setBloom(scale, threshold, response) {
5391
5610
  this.bloomScale = Math.min(Math.max(scale, 0), 1);
5392
5611
  if (threshold !== undefined)
5393
5612
  this.bloomThresholdSet = bloomThresholdOf(threshold);
5613
+ if (response !== undefined)
5614
+ resolveBloomResponse(response, this.bloomResponse);
5394
5615
  }
5395
5616
  /**
5396
5617
  * How thick the air is, this frame: a global participating medium filling the whole frustum.
@@ -5906,6 +6127,7 @@ export class WebGL2Renderer {
5906
6127
  ? {
5907
6128
  strength: bloomStrength,
5908
6129
  threshold: this.bloomThresholdSet ?? this.quality.bloomThreshold,
6130
+ response: this.bloomResponse,
5909
6131
  }
5910
6132
  : undefined;
5911
6133
  /* The grade travels to the resolve only where the mesh pass gave it up. See `bindMeshPass`. */
@@ -6188,6 +6410,15 @@ export class WebGL2Renderer {
6188
6410
  bindMeshPass(camera, env) {
6189
6411
  /* The pass's own material is reopened, so the next draw opens one. See `materialChanges.ts`. */
6190
6412
  this.materials.dirty();
6413
+ /* Rectangles past the fixed arm's are the froxel table's, and without it nobody shades them. */
6414
+ if (!this.warnedAreaOverflow &&
6415
+ !this.quality.clusteredLights &&
6416
+ (env.areaLights?.count ?? 0) > this.shadedAreaLights) {
6417
+ this.warnedAreaOverflow = true;
6418
+ console.warn(`WebGL2: ${env.areaLights?.count ?? 0} rectangular lights and ${this.shadedAreaLights} ` +
6419
+ 'shaded without clusteredLights: the rest are not drawn. Turn clusteredLights on, and the ' +
6420
+ 'froxel table shades every one past the fixed ones, unshadowed.');
6421
+ }
6191
6422
  /*
6192
6423
  * Captured here rather than taken as an argument to `endFrame`, because this is the
6193
6424
  * camera the world was drawn with and a caller should not have to hand it over twice.
@@ -6202,7 +6433,10 @@ export class WebGL2Renderer {
6202
6433
  */
6203
6434
  if (this.framePresented)
6204
6435
  this.temporalJittering = false;
6205
- if (!this.reflectionPassActive && !this.probePassActive && !this.framePresented) {
6436
+ if (!this.reflectionPassActive &&
6437
+ !this.probePassActive &&
6438
+ !this.capturePassActive &&
6439
+ !this.framePresented) {
6206
6440
  this.frameViewProj = camera.viewProjection;
6207
6441
  /* Copied rather than referenced: a caller moves its camera in place, and the decal pass
6208
6442
  reads this at the end of the frame. */
@@ -6297,6 +6531,13 @@ export class WebGL2Renderer {
6297
6531
  this.materialHasMaps = false;
6298
6532
  this.currentMaterial = null;
6299
6533
  this.currentSurfaceTexture = null;
6534
+ /* Nor an ambient of its own: the frame's, until a draw asks. See `setAmbientSH`. */
6535
+ this.ambientSH.fill(0);
6536
+ /* No reflection and an environment at its own brightness; the emission the world asks for. */
6537
+ this.environmentDials[1] = 0;
6538
+ this.environmentDials[2] = 1;
6539
+ this.emission[0] = env.emissiveGain;
6540
+ this.emission[1] = env.nightFactor;
6300
6541
  for (const target of this.flatTargets) {
6301
6542
  this.gl.useProgram(target.program);
6302
6543
  this.writeMeshPassState(target.uniforms, camera, env);
@@ -6492,13 +6733,18 @@ export class WebGL2Renderer {
6492
6733
  this.oitWeight = 0;
6493
6734
  this.ditherFade = 0;
6494
6735
  gl.uniform2f(u['uWriteMode'] ?? null, 0, 0);
6495
- /* No environment reflection unless a caller asks, so existing scenes are unchanged. */
6496
- gl.uniform1f(u['uReflectivity'] ?? null, 0);
6497
- /* 1, not 0: this one is a multiplier and its identity is one. See `setEnvironmentGain`. */
6498
- gl.uniform1f(u['uEnvironmentGain'] ?? null, 1);
6736
+ /* The draw's own ambient as it stands, which `bindMeshPass` has just cleared: a program built
6737
+ in the middle of a pass takes what the caller set. See `setAmbientSH`. */
6738
+ gl.uniform4fv(u['uAmbientSH[0]'] ?? null, this.ambientSH);
6739
+ /*
6740
+ * Which chain the probe holds, no environment reflection, and an environment at its own
6741
+ * brightness — the reflectivity's identity is 0 and the gain's is 1 — as `bindMeshPass` set
6742
+ * them, or as a draw since has: a program built in the middle of a pass takes what stands.
6743
+ * See `setSurfaceReflectivity` and `setEnvironmentGain`.
6744
+ */
6745
+ gl.uniform3fv(u['uEnvironmentDials'] ?? null, this.environmentDials);
6499
6746
  /* And no relief unless a caller asks, for the same reason. */
6500
- gl.uniform1f(u['uRelief'] ?? null, 0);
6501
- gl.uniform1f(u['uReliefCycles'] ?? null, 60);
6747
+ gl.uniform2f(u['uRelief'] ?? null, 0, 60);
6502
6748
  /* And no relief read off a surface texture either, for the same reason. */
6503
6749
  gl.uniform1f(u['uTextureRelief'] ?? null, 0);
6504
6750
  /* The mirror's plane in a reflection pass, and zero — which clips nothing — in every other. */
@@ -6507,8 +6753,8 @@ export class WebGL2Renderer {
6507
6753
  resolveSurfaceScene(env, this.surfaceScene);
6508
6754
  gl.uniform4fv(u['uSurfaceScene'] ?? null, this.surfaceScene);
6509
6755
  bindAtmosphere(gl, u, env, this.reflectionPassActive ? this.reflectionAtmosphereY : (camera.position[1] ?? 0), this.quality.underwaterAtmosphere);
6510
- gl.uniform1f(u['uEmissiveGain'] ?? null, env.emissiveGain);
6511
- gl.uniform1f(u['uNightFactor'] ?? null, env.nightFactor);
6756
+ /* The emissive gain and the night factor, as the pass set them. See `uEmission`. */
6757
+ gl.uniform2fv(u['uEmission'] ?? null, this.emission);
6512
6758
  /* Absent means zero, so a world written before this term existed adds nothing. */
6513
6759
  gl.uniform1f(u['uNightEmissive'] ?? null, env.nightEmissive ?? 0);
6514
6760
  /*
@@ -6530,8 +6776,10 @@ export class WebGL2Renderer {
6530
6776
  gl.uniform1f(u['uShadowMaxSlope'] ?? null, this.quality.directionalShadowMaxSlope);
6531
6777
  gl.uniform1i(u['uShadowFilterTaps'] ?? null, this.quality.shadowFilterTaps);
6532
6778
  gl.uniform3fv(u['uHighlightMin'] ?? null, env.highlightMin);
6533
- gl.uniform3fv(u['uHighlightMax'] ?? null, env.highlightMax);
6534
- gl.uniform1f(u['uHighlightGain'] ?? null, env.highlightGain);
6779
+ /* The far corner, and the gain in its w. See `uHighlightMax`. */
6780
+ /* An environment written by hand may leave the box out; then there is none, as before. */
6781
+ const max = env.highlightMax ?? NO_HIGHLIGHT;
6782
+ gl.uniform4f(u['uHighlightMax'] ?? null, max[0] ?? 0, max[1] ?? 0, max[2] ?? 0, env.highlightGain ?? 0);
6535
6783
  bindPointLights(gl, u, env, this.quality.pointLightFalloff);
6536
6784
  this.bindClusters(u, camera, env);
6537
6785
  this.bindIesAtlas(u);
@@ -6627,7 +6875,8 @@ export class WebGL2Renderer {
6627
6875
  * between them; there is one array, so `RenderQuality.environmentPrefilter` decides what the
6628
6876
  * convolution writes and a profile that changes it has to bake again.
6629
6877
  */
6630
- gl.uniform1f(u['uEnvironmentPrefiltered'] ?? null, this.quality.environmentPrefilter ? 1 : 0);
6878
+ this.environmentDials[0] = this.quality.environmentPrefilter ? 1 : 0;
6879
+ gl.uniform3fv(u['uEnvironmentDials'] ?? null, this.environmentDials);
6631
6880
  gl.uniform1f(u['uEnvironmentMaxLod'] ?? null, array?.ggxMaxLevel ?? 0);
6632
6881
  /*
6633
6882
  * The map's edge, which used to be `exp2(maxLod)` and is not any more: the chain stops one
@@ -6817,12 +7066,17 @@ export class WebGL2Renderer {
6817
7066
  return;
6818
7067
  this.drawBudget.ask();
6819
7068
  this.takeMaterial();
6820
- /* Skin under the screen-space blur draws its two halves: the frame's, then at once its diffuse. */
7069
+ /*
7070
+ * Skin under the screen-space blur draws its halves: the frame's, then at once its diffuse's
7071
+ * light and its colour, each into the blur's own target.
7072
+ */
6821
7073
  const splits = this.skinSplits();
6822
7074
  this.drawMeshHalf(mesh, model, depthLayer, tint, splits ? 'scene' : 'whole');
6823
7075
  if (splits && this.skinScatter !== null) {
6824
7076
  this.skinScatter.begin();
6825
7077
  this.drawMeshHalf(mesh, model, depthLayer, tint, 'diffuse');
7078
+ this.skinScatter.beginAlbedo();
7079
+ this.drawMeshHalf(mesh, model, depthLayer, tint, 'albedo');
6826
7080
  this.sceneTarget?.bind();
6827
7081
  }
6828
7082
  }
@@ -6840,6 +7094,7 @@ export class WebGL2Renderer {
6840
7094
  !this.skinScattered &&
6841
7095
  !this.reflectionPassActive &&
6842
7096
  !this.probePassActive &&
7097
+ !this.capturePassActive &&
6843
7098
  !this.framePresented &&
6844
7099
  skin.ready(scene));
6845
7100
  }
@@ -6860,7 +7115,9 @@ export class WebGL2Renderer {
6860
7115
  this.skinScattered = true;
6861
7116
  if (!skin.pending)
6862
7117
  return;
6863
- if (this.reflectionPassActive || this.probePassActive || projection === null)
7118
+ if (this.reflectionPassActive || this.probePassActive || this.capturePassActive)
7119
+ return;
7120
+ if (projection === null)
6864
7121
  return;
6865
7122
  const depth = scene.snapshotDepth(true);
6866
7123
  mat4.invert(this.skinInverseProjection, projection);
@@ -6996,8 +7253,9 @@ export class WebGL2Renderer {
6996
7253
  * depth 1.0 against a buffer cleared to 1.0, and under `LESS` it would not draw at
6997
7254
  * all. Film, caustics and text lie flush on surfaces for the same reason.
6998
7255
  */
6999
- /* A skin's diffuse half is the same surface again: it finds the depth its frame half wrote. */
7000
- const again = half === 'diffuse';
7256
+ /* A skin's diffuse and colour halves are the same surface again: each finds the depth its
7257
+ frame half wrote. */
7258
+ const again = half === 'diffuse' || half === 'albedo';
7001
7259
  gl.depthFunc(again ? gl.EQUAL : this.reversedDepth ? gl.GREATER : gl.LESS);
7002
7260
  if (again)
7003
7261
  gl.depthMask(false);
@@ -7441,6 +7699,43 @@ export class WebGL2Renderer {
7441
7699
  this.probeSweeps?.baked(0);
7442
7700
  return true;
7443
7701
  }
7702
+ /**
7703
+ * One layer of the declared probe grid from an image, the way `setEnvironmentImage` fills a
7704
+ * single probe: the same equirectangular image of linear RGB floats, projected onto the capture
7705
+ * cube and prefiltered into that layer, the others left as they are. Returns false — and says
7706
+ * why — where there is no probe, no grid, or no such layer.
7707
+ *
7708
+ * **What it is for**: a scene baked somewhere else. Another engine's reflection captures are
7709
+ * images taken where each one stands; a consumer fits them to a lattice — for each point, the
7710
+ * capture whose reach holds it — and hands each point its image here, where `bakeProbe` would
7711
+ * have drawn this engine's own world into it.
7712
+ *
7713
+ * `options.irradiance` decides, at grid scope as a bake's does, whether the grid also lights the
7714
+ * scene's diffuse ambient. A grid that crossfades writes the set being swept, as a bake does.
7715
+ */
7716
+ setProbeLayerImage(layer, image, options) {
7717
+ if (this.contextLost)
7718
+ return false;
7719
+ const probe = this.probeCapture;
7720
+ const array = this.probeArray;
7721
+ const grid = this.probes;
7722
+ const sweeps = this.probeSweeps;
7723
+ if (probe === null || array === null || grid === null || sweeps === null) {
7724
+ console.warn('Renderer: setProbeLayerImage needs a reflection probe (`reflectionProbeSize`) and a grid ' +
7725
+ 'declared with setProbeGrid; the image was ignored.');
7726
+ return false;
7727
+ }
7728
+ if (!Number.isInteger(layer) || layer < 0 || layer >= grid.layers) {
7729
+ console.warn(`Renderer: setProbeLayerImage was given layer ${layer}, and the grid has ${grid.layers}.`);
7730
+ return false;
7731
+ }
7732
+ if (!probe.upload(this.gl, equirectToCubeFaces(image, probe.size)))
7733
+ return false;
7734
+ this.probeAmbient = options?.irradiance ?? true;
7735
+ this.environmentPrefilter?.run(this.gl, probe, array, sweeps.writeLayer(layer), ENVIRONMENT_TEXTURE_UNIT, this.quality.environmentPrefilterSamples, this.quality.environmentPrefilter);
7736
+ sweeps.baked(layer);
7737
+ return true;
7738
+ }
7444
7739
  /**
7445
7740
  * Draw a volume of light: a beam from a lamp, a shaft through a window, the cone under a
7446
7741
  * street light.
@@ -7758,6 +8053,14 @@ const NO_MODEL_PARAMS = new Float32Array(MODEL_PARAM_FLOATS);
7758
8053
  * The lit vertex stage a program of this variant compiles: the standard programs' and the cloth's,
7759
8054
  * which `modelProgram` pairs a model's fragment stage with.
7760
8055
  */
8056
+ /**
8057
+ * The vertex stage a model's program draws with: the variant's own, but for a lightmapped instanced
8058
+ * batch, whose tint lane is each instance's region of the page. See `lightmap.ts`.
8059
+ */
8060
+ function modelVertexOf(variant, kind) {
8061
+ const vertex = litVertexOf(variant);
8062
+ return kind === 'lightmap' && variant === 'instanced' ? lightmapRegionsBound(vertex) : vertex;
8063
+ }
7761
8064
  function litVertexOf(variant) {
7762
8065
  if (variant.startsWith('cloth')) {
7763
8066
  const eight = variant[5] === '8';