@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
@@ -1,4 +1,6 @@
1
1
  import type { SurfaceTextureOptions } from '../../surfaceTexture.ts';
2
+ import type { LightmapTexels } from '../../lightmap.ts';
3
+ import type { SceneCaptureTexels } from '../../sceneCapture.ts';
2
4
  import type { CompressedTextureFormat, SurfaceSource } from '../../compressedSource.ts';
3
5
  import type { PipelineCache } from './pipelineCache.ts';
4
6
  /** How many levels an image of this size has, counting the one it arrives with. */
@@ -62,11 +64,13 @@ export declare class GpuSurfaceTexture {
62
64
  effectsView: GPUTextureView | null;
63
65
  /** The block format a compressed texture holds, or null for one uploaded from images. */
64
66
  private readonly blockFormat;
65
- constructor(device: GPUDevice, pipelines: PipelineCache, source: SurfaceSource | readonly SurfaceSource[], options?: SurfaceTextureOptions, compressed?: readonly CompressedTextureFormat[]);
67
+ constructor(device: GPUDevice, pipelines: PipelineCache, source: SurfaceSource | readonly SurfaceSource[] | LightmapTexels | SceneCaptureTexels, options?: SurfaceTextureOptions, compressed?: readonly CompressedTextureFormat[]);
66
68
  /** A view of every layer, as the shaders' `texture_2d_array` bindings read it. */
67
69
  private arrayView;
68
70
  /** The view every binding of this image holds. A new one when `update` changes the size. */
69
71
  get view(): GPUTextureView;
72
+ /** The only layer as a plain 2D view, which a scene capture's pass resolves into. */
73
+ layerView(): GPUTextureView | null;
70
74
  /**
71
75
  * Replace the pixels, keeping the handle and its sampler.
72
76
  *
@@ -1,5 +1,7 @@
1
1
  import { SURFACE_EFFECT_TEXELS, packSurfaceEffects } from '../../surfaceEffects.js';
2
2
  import { toHalfFloats } from '../../halfFloat.js';
3
+ import { isLightmapTexels } from '../../lightmap.js';
4
+ import { isSceneCaptureTexels } from '../../sceneCapture.js';
3
5
  import { layerSize, refuseArrayUpdate, sourceSize } from '../../textureSource.js';
4
6
  import { compressedLayers, planBlocks, refuseBlockUpdate } from '../../compressedSource.js';
5
7
  import { createCompressedTexture } from './compressedUpload.js';
@@ -155,13 +157,46 @@ export class GpuSurfaceTexture {
155
157
  constructor(device, pipelines, source, options = {}, compressed = []) {
156
158
  this.device = device;
157
159
  this.pipelines = pipelines;
158
- const listed = (Array.isArray(source) ? source : [source]);
160
+ /* A scene capture's target: one empty layer the world is drawn into. See `sceneCapture.ts`. */
161
+ const capture = isSceneCaptureTexels(source) ? source : null;
162
+ /* A lightmap's page: two layers of bytes, uploaded as they are. See `lightmap.ts`. */
163
+ const baked = isLightmapTexels(source) ? source : null;
164
+ const listed = (baked !== null || capture !== null ? [] : Array.isArray(source) ? source : [source]);
159
165
  const blocks = compressedLayers(listed);
160
166
  const srgb = (options.colorSpace ?? 'linear') === 'srgb';
161
167
  /* `SRGB8_ALPHA8`'s equivalent. Decoded in the sampler, before filtering, which is the only
162
- place it is correct — `surfaceTexture.ts` makes the argument in full. */
163
- this.format = srgb ? 'rgba8unorm-srgb' : 'rgba8unorm';
164
- if (blocks !== null) {
168
+ place it is correct — `surfaceTexture.ts` makes the argument in full. A capture takes the
169
+ world's own format, which its pass resolves into. */
170
+ this.format =
171
+ capture !== null
172
+ ? pipelines.format
173
+ : srgb && baked === null
174
+ ? 'rgba8unorm-srgb'
175
+ : 'rgba8unorm';
176
+ if (capture !== null) {
177
+ this.layers = 1;
178
+ this.blockFormat = null;
179
+ this.mipmapped = false;
180
+ this.width = capture.width;
181
+ this.height = capture.height;
182
+ this.levels = 1;
183
+ this.texture = this.allocate();
184
+ }
185
+ else if (baked !== null) {
186
+ /* No chain: a page is read at its own resolution, and a level of it would blend regions. */
187
+ this.layers = baked.layers.length;
188
+ this.blockFormat = null;
189
+ this.mipmapped = false;
190
+ this.width = baked.width;
191
+ this.height = baked.height;
192
+ this.levels = 1;
193
+ this.texture = this.allocate();
194
+ /* A layer at a time, so a page's arrays are uploaded where they lie rather than joined. */
195
+ for (let layer = 0; layer < this.layers; layer++) {
196
+ device.queue.writeTexture({ texture: this.texture, origin: [0, 0, layer] }, baked.layers[layer], { bytesPerRow: baked.width * 4, rowsPerImage: baked.height }, { width: baked.width, height: baked.height, depthOrArrayLayers: 1 });
197
+ }
198
+ }
199
+ else if (blocks !== null) {
165
200
  /* Refused before anything is allocated; the stored chain, or level 0 alone where no chain
166
201
  was asked for. See `compressedUpload.ts`. */
167
202
  const plan = planBlocks(blocks, srgb, compressed);
@@ -216,8 +251,9 @@ export class GpuSurfaceTexture {
216
251
  ? Math.max(1, Math.floor(options.anisotropy ?? DEFAULT_ANISOTROPY))
217
252
  : 1,
218
253
  });
219
- if (blocks === null)
254
+ if (blocks === null && baked === null && capture === null) {
220
255
  this.upload(listed);
256
+ }
221
257
  /* Half floats, as `renderer.ts` uploads it: filterable, so the ordinary float layout takes it,
222
258
  and read by `textureLoad` alone, so the filter never runs. */
223
259
  const effects = options.effects ?? [];
@@ -241,6 +277,10 @@ export class GpuSurfaceTexture {
241
277
  get view() {
242
278
  return this.current;
243
279
  }
280
+ /** The only layer as a plain 2D view, which a scene capture's pass resolves into. */
281
+ layerView() {
282
+ return (this.texture?.createView({ dimension: '2d', baseArrayLayer: 0, arrayLayerCount: 1 }) ?? null);
283
+ }
244
284
  /**
245
285
  * Replace the pixels, keeping the handle and its sampler.
246
286
  *
@@ -11,7 +11,7 @@ import type { UniformFields } from './scatterPass.ts';
11
11
  export declare const WATER_VERT_FIELDS: UniformFields;
12
12
  export declare const WATER_VERT_SIZE: 208;
13
13
  export declare const WATER_FRAG_FIELDS: UniformFields;
14
- export declare const WATER_FRAG_SIZE: 1248;
14
+ export declare const WATER_FRAG_SIZE: 1504;
15
15
  /** The two meshes and the numbers the grid decided, behind the surface's opaque handle. */
16
16
  export interface GpuWater {
17
17
  /** The ocean: near sheet and far skirt in one buffer. */
@@ -57,6 +57,50 @@ export declare function bloomProfileWarning(quality: {
57
57
  readonly hdrScene: boolean;
58
58
  readonly bloomThreshold: number;
59
59
  }): string | null;
60
+ /**
61
+ * How a frame's bloom answers the light it is given, beside the strength and threshold `setBloom`
62
+ * already takes. Absent, or a field absent, is the response every frame had before 4.8.6.
63
+ *
64
+ * **Why it exists: one band over the colour less the threshold washes a stage built for another
65
+ * engine.** Unreal's standard bloom keeps `saturate((L − threshold) / 2)` of a colour rather than
66
+ * the colour less the threshold, and spreads it over six bands, each in a tint of its own. A
67
+ * stage authored against that — a threshold of 0.1 and tints summing below one — came out white
68
+ * here, because subtracting so low a threshold keeps nearly every light in the frame at full
69
+ * strength. These are the two things that differ, as numbers.
70
+ */
71
+ export interface BloomResponse {
72
+ /**
73
+ * Scene units over which a colour comes in, from none of it at the threshold to all of it this
74
+ * far past. **0, the default, subtracts instead**: a colour keeps what it has past the threshold,
75
+ * the response since bloom shipped. Unreal's is 2 after exposure, so a frame exposed by `e` that
76
+ * wants it passes `2 / e`, as it passes the threshold over the exposure.
77
+ *
78
+ * Brightness is still the largest channel rather than luminance, for the reason
79
+ * `shaders/bloom.ts` gives; on a neutral colour the two are equal.
80
+ */
81
+ readonly ramp?: number;
82
+ /**
83
+ * Each level's colour, three numbers a level, finest first: the halo at a few pixels first and
84
+ * at a twentieth of the frame last. **White, the default, is the identity**; a level not named
85
+ * takes white. Not clamped above one, which is how a tint carries a strength past the 0..1 the
86
+ * dial gives — a stage asking for twice the light names tints twice as bright.
87
+ *
88
+ * At most `BLOOM_LEVELS` levels. A frame too small for that many uses the first it has room for.
89
+ */
90
+ readonly tints?: ArrayLike<number>;
91
+ }
92
+ /** Floats `resolveBloomResponse` writes: the ramp, then a tint a level. */
93
+ export declare const BLOOM_RESPONSE_FLOATS: number;
94
+ /**
95
+ * A response as both backends bind it, into a caller-owned `out` of `BLOOM_RESPONSE_FLOATS`: the
96
+ * ramp, then `BLOOM_LEVELS` tints, white where none was named. Refuses a tint list that is not
97
+ * whole colours or names more levels than the pyramid has, naming the length — a caller error with
98
+ * one correct outcome. A ramp that is not a positive number is the subtraction; a tint channel
99
+ * that is not a number or is below zero is zero.
100
+ */
101
+ export declare function resolveBloomResponse(response: BloomResponse | null | undefined, out: Float32Array): void;
102
+ /** The response every frame had before 4.8.6: the subtraction, every level white. */
103
+ export declare function defaultBloomResponse(): Float32Array;
60
104
  /** One level of the pyramid: level 0 is half the frame and each one after it half again. */
61
105
  export interface BloomLevelSize {
62
106
  readonly width: number;
@@ -67,6 +67,34 @@ export function bloomProfileWarning(quality) {
67
67
  }
68
68
  return null;
69
69
  }
70
+ /** Floats `resolveBloomResponse` writes: the ramp, then a tint a level. */
71
+ export const BLOOM_RESPONSE_FLOATS = 1 + BLOOM_LEVELS * 3;
72
+ /**
73
+ * A response as both backends bind it, into a caller-owned `out` of `BLOOM_RESPONSE_FLOATS`: the
74
+ * ramp, then `BLOOM_LEVELS` tints, white where none was named. Refuses a tint list that is not
75
+ * whole colours or names more levels than the pyramid has, naming the length — a caller error with
76
+ * one correct outcome. A ramp that is not a positive number is the subtraction; a tint channel
77
+ * that is not a number or is below zero is zero.
78
+ */
79
+ export function resolveBloomResponse(response, out) {
80
+ const ramp = response?.ramp ?? 0;
81
+ out[0] = Number.isFinite(ramp) && ramp > 0 ? ramp : 0;
82
+ const tints = response?.tints;
83
+ if (tints !== undefined && (tints.length % 3 !== 0 || tints.length > BLOOM_LEVELS * 3)) {
84
+ throw new Error(`setBloom: ${tints.length} tint values is not up to ${BLOOM_LEVELS} whole colours, three ` +
85
+ 'numbers a level, finest first');
86
+ }
87
+ for (let i = 0; i < BLOOM_LEVELS * 3; i++) {
88
+ const value = tints !== undefined && i < tints.length ? tints[i] : 1;
89
+ out[1 + i] = Number.isFinite(value) && value > 0 ? value : 0;
90
+ }
91
+ }
92
+ /** The response every frame had before 4.8.6: the subtraction, every level white. */
93
+ export function defaultBloomResponse() {
94
+ const out = new Float32Array(BLOOM_RESPONSE_FLOATS);
95
+ resolveBloomResponse(null, out);
96
+ return out;
97
+ }
70
98
  /**
71
99
  * The pyramid for a frame of this size, largest first, or an empty list where none would fit.
72
100
  *
@@ -46,7 +46,9 @@ export declare class BloomPass {
46
46
  * `threshold` is in scene units, so it only separates a light from white paint on a target
47
47
  * that kept the range. The caller owns that decision and the warning that goes with it.
48
48
  */
49
- run(scene: WebGLTexture, width: number, height: number, threshold: number): WebGLTexture | null;
49
+ run(scene: WebGLTexture, width: number, height: number, threshold: number,
50
+ /** The ramp, then each level's tint: `resolveBloomResponse`'s floats. */
51
+ response: Float32Array): WebGLTexture | null;
50
52
  /** One source texel in UV, which is what the thirteen taps are laid out in. */
51
53
  private uploadTexel;
52
54
  private release;
@@ -130,7 +130,9 @@ export class BloomPass {
130
130
  * `threshold` is in scene units, so it only separates a light from white paint on a target
131
131
  * that kept the range. The caller owns that decision and the warning that goes with it.
132
132
  */
133
- run(scene, width, height, threshold) {
133
+ run(scene, width, height, threshold,
134
+ /** The ramp, then each level's tint: `resolveBloomResponse`'s floats. */
135
+ response) {
134
136
  if (!this.ensureSize(width, height))
135
137
  return null;
136
138
  const { gl } = this;
@@ -153,6 +155,7 @@ export class BloomPass {
153
155
  gl.bindTexture(gl.TEXTURE_2D, scene);
154
156
  gl.uniform1i(this.prefilterUniforms['uSource'] ?? null, 0);
155
157
  gl.uniform1f(this.prefilterUniforms['uThreshold'] ?? null, threshold);
158
+ gl.uniform1f(this.prefilterUniforms['uRamp'] ?? null, response[0] ?? 0);
156
159
  this.uploadTexel(this.prefilterUniforms, width, height);
157
160
  gl.drawArrays(gl.TRIANGLES, 0, 3);
158
161
  /* Down the chain. Each level reads the one above it, which was written a draw ago. */
@@ -174,8 +177,13 @@ export class BloomPass {
174
177
  * every octave at once. Additive blending rather than a second target to ping-pong through:
175
178
  * a level is only ever read while a *different* level is bound, so there is no feedback.
176
179
  */
180
+ /*
181
+ * Each level's tint rides the blend: the level being added onto is multiplied by its own as the
182
+ * octave below is added, and the deepest by `uScale` on the first draw, the only one reading it
183
+ * before anything has been added to it. White everywhere is `ONE, ONE` exactly.
184
+ */
177
185
  gl.enable(gl.BLEND);
178
- gl.blendFunc(gl.ONE, gl.ONE);
186
+ gl.blendFunc(gl.ONE, gl.CONSTANT_COLOR);
179
187
  gl.useProgram(this.upsampleProgram);
180
188
  gl.uniform1i(this.upsampleUniforms['uSource'] ?? null, 0);
181
189
  gl.uniform1f(this.upsampleUniforms['uRadius'] ?? null, BLOOM_FILTER_RADIUS_UV);
@@ -184,11 +192,17 @@ export class BloomPass {
184
192
  const source = levels[index];
185
193
  if (target === undefined || source === undefined)
186
194
  break;
195
+ const deepest = index === levels.length - 1;
196
+ const into = 1 + (index - 1) * 3;
197
+ const from = 1 + index * 3;
198
+ gl.blendColor(response[into] ?? 1, response[into + 1] ?? 1, response[into + 2] ?? 1, 1);
199
+ gl.uniform3f(this.upsampleUniforms['uScale'] ?? null, deepest ? (response[from] ?? 1) : 1, deepest ? (response[from + 1] ?? 1) : 1, deepest ? (response[from + 2] ?? 1) : 1);
187
200
  gl.bindFramebuffer(gl.FRAMEBUFFER, target.framebuffer);
188
201
  gl.viewport(0, 0, target.width, target.height);
189
202
  gl.bindTexture(gl.TEXTURE_2D, source.texture);
190
203
  gl.drawArrays(gl.TRIANGLES, 0, 3);
191
204
  }
205
+ gl.blendColor(0, 0, 0, 0);
192
206
  gl.disable(gl.BLEND);
193
207
  gl.bindVertexArray(null);
194
208
  gl.bindFramebuffer(gl.FRAMEBUFFER, null);
@@ -1,3 +1,4 @@
1
+ import type { AreaLightBuffer } from './areaLights.ts';
1
2
  /**
2
3
  * The froxel table: what it looks like in memory, and the CPU binner that fills it.
3
4
  *
@@ -84,7 +85,7 @@ export declare const MAX_CLUSTERED_LIGHTS = 320;
84
85
  * it cannot. That is what lets a light index the shadow uniforms by a number read from a texture,
85
86
  * and it is the same fact the octahedral array texture was built on.
86
87
  */
87
- export declare const LIGHT_TEXELS = 5;
88
+ export declare const LIGHT_TEXELS = 6;
88
89
  /**
89
90
  * Where each field of a light's record sits, as an offset in `uint`s from `lightBase`.
90
91
  *
@@ -114,7 +115,11 @@ export declare const LIGHT_TEXELS = 5;
114
115
  * **Reordered 2026-10-02 so a plain light's fields are the first eight**, which is what lets the
115
116
  * shader stop after two texels. Nine values are needed by every light and eight fit in two
116
117
  * texels, so the emitter size and the weight share slot 7 as two halves; see `packSizeAndWeight`.
117
- * Slot 19 is spare.
118
+ *
119
+ * **A sixth texel arrived in 4.8.7 for a light's own falloff exponent**, the fifth's last slot
120
+ * having gone to the channels: 6 KB more of a table near 300, read only for a light whose record
121
+ * carries a fixture, which a light with an exponent does. Slots 21 and 22 are a rectangle's barn
122
+ * doors and spare on a lamp; slot 23 is spare.
118
123
  */
119
124
  export declare const LIGHT_RECORD: {
120
125
  readonly positionX: 0;
@@ -139,11 +144,36 @@ export declare const LIGHT_RECORD: {
139
144
  readonly iesAxisX: 16;
140
145
  readonly iesAxisY: 17;
141
146
  readonly iesAxisZ: 18;
147
+ /**
148
+ * The light's lighting channels, a mask (`PointLightSet.lightChannels`), in the slot that was
149
+ * spare. A light naming channels other than the default is read as carrying a fixture, so its
150
+ * last texels are fetched; a plain light's are not, and the shader takes the default for it.
151
+ */
152
+ readonly channels: 19;
153
+ /** The light's own falloff exponent, or 0 for the frame's falloff. `PointLightSource.falloffExponent`. */
154
+ readonly falloffExponent: 20;
155
+ /** A rectangle's barn doors: the cosine of their angle from its normal, then their length in metres. */
156
+ readonly barnDoorCos: 21;
157
+ readonly barnDoorLength: 22;
142
158
  };
159
+ /** The lighting channel every light and every surface is on unless it says otherwise. */
160
+ export declare const DEFAULT_LIGHT_CHANNELS = 1;
161
+ /**
162
+ * A light's channels as the shader compares them: a whole mask from 1 to 255, or the default where
163
+ * none was named or what was named is not one. The froxel record and the uniform path both write
164
+ * this, so a light named badly is on channel 1 in both rather than on two different guesses.
165
+ */
166
+ export declare function lightChannelsOf(value: number | undefined): number;
167
+ /**
168
+ * A light's own falloff exponent as the shader reads it: a positive finite number, or 0 for the
169
+ * frame's falloff where none was named or what was named is not one. The record and the uniform
170
+ * path both write this, so a light named badly falls off by the frame's rule in both.
171
+ */
172
+ export declare function lightFalloffExponentOf(value: number | undefined): number;
143
173
  /** Texels the lit pass reads for a light whose record carries no fixture. */
144
174
  export declare const PLAIN_LIGHT_TEXELS = 2;
145
175
  /**
146
- * The bit of `sizeAndWeight` that says the record's last three texels hold something: the sign of
176
+ * The bit of `sizeAndWeight` that says the record's last four texels hold something: the sign of
147
177
  * the emitter size's half, which a size never needs.
148
178
  */
149
179
  export declare const LIGHT_FIXTURE_FLAG = 32768;
@@ -270,6 +300,18 @@ export interface ClusterLightSet {
270
300
  readonly cookies?: Float32Array;
271
301
  /** One per light: an index into the IES atlas, or −1. */
272
302
  readonly iesProfiles?: Float32Array;
303
+ /** One per light: its lighting channels. See `PointLightSet.lightChannels`. */
304
+ readonly channels?: Float32Array;
305
+ /** One per light: its own falloff exponent, or 0. See `PointLightSource.falloffExponent`. */
306
+ readonly falloffExponents?: Float32Array;
307
+ /**
308
+ * The rectangles shaded through the table, after the `count` point lights: those of `areas` from
309
+ * `areaFrom` on, `areaCount` of them. The ones before `areaFrom` are the fixed arm's, which is
310
+ * where a rectangle's shadow lives. Absent, there are none.
311
+ */
312
+ readonly areas?: AreaLightBuffer | null;
313
+ readonly areaFrom?: number;
314
+ readonly areaCount?: number;
273
315
  }
274
316
  /** Where `fillClusterLightSet` reads a frame's lights from: the arrays `Environment` carries. */
275
317
  export interface ClusterLightArrays {
@@ -284,6 +326,9 @@ export interface ClusterLightArrays {
284
326
  readonly lightIesProfiles?: Float32Array;
285
327
  readonly lightIesAxes?: Float32Array;
286
328
  readonly lightCookies?: Float32Array;
329
+ readonly lightChannels?: Float32Array;
330
+ readonly lightFalloffExponents?: Float32Array;
331
+ readonly areaLights?: AreaLightBuffer | null;
287
332
  }
288
333
  /** A `ClusterLightSet` a renderer refills every frame. */
289
334
  export type ClusterLightScratch = {
@@ -305,10 +350,12 @@ export declare function createClusterLightSet(): ClusterLightScratch;
305
350
  * radius in its place, an emitter as wide as the light reaches, which widened every lamp's
306
351
  * highlight on a specular surface on the clustered arm alone.
307
352
  */
308
- export declare function fillClusterLightSet(env: ClusterLightArrays, out: ClusterLightScratch): ClusterLightSet;
353
+ export declare function fillClusterLightSet(env: ClusterLightArrays, out: ClusterLightScratch,
354
+ /** How many rectangles the fixed arm shades; the rest of the buffer is clustered. */
355
+ areaFrom?: number): ClusterLightSet;
309
356
  /**
310
- * Whether a light's record needs its last three texels: it holds a shadow slot, a cone, a profile
311
- * or a cookie. Everything else is a plain light, read in `PLAIN_LIGHT_TEXELS`.
357
+ * Whether a light's record needs its last four texels: it holds a shadow slot, a cone, a profile,
358
+ * a cookie, channels of its own or a falloff exponent of its own. Everything else is a plain light, read in `PLAIN_LIGHT_TEXELS`.
312
359
  *
313
360
  * **`shadowSlots` is zero where the lit shader has no point shadows**, and both binners are handed
314
361
  * it that way: a slot is only read under `POINT_SHADOWS`, so a light holding one in a build without
@@ -343,6 +390,8 @@ export declare function clusteredMode(lights: ClusterLightSet, count: number, sh
343
390
  * is a light without a map, which is what a scene with two hundred lamps has anyway.
344
391
  */
345
392
  export declare function writeLightRecord(lights: ClusterLightSet, light: number, shadowSlots: number, out: Uint32Array, at: number): void;
393
+ /** How many lights the table carries: the point lights and the clustered rectangles after them. */
394
+ export declare function clusteredLightTotal(lights: ClusterLightSet): number;
346
395
  /**
347
396
  * Fill the table with this frame's light assignment, and answer how many lights it carried.
348
397
  *
@@ -389,3 +438,26 @@ export declare function writeLightRecord(lights: ClusterLightSet, light: number,
389
438
  * Fills a caller-owned table and allocates nothing.
390
439
  */
391
440
  export declare function buildLightClusters(lights: ClusterLightSet, view: Float32Array, near: number, far: number, tanHalfFovY: number, aspect: number, table: Uint32Array, shadowSlots: number): number;
441
+ /**
442
+ * How far a froxel lies outside a spot's cone, in metres, or a negative number where the cone
443
+ * reaches it: positive is a froxel the light cannot shade. Everything in view space with depth
444
+ * positive, as the binner holds it; `axis` is unit length and `cosA`, `sinA` are the outer cone's.
445
+ *
446
+ * **The froxel's bounding sphere against the infinite cone**, after Wronski's cone test for
447
+ * clustered spots. With `V` from the apex to the sphere's centre at an angle `θ` from the axis and
448
+ * `A` the cone's half-angle, the centre's distance from the cone is `|V| sin(θ − A)` — which is
449
+ * `cosA |V⊥| − (V·axis) sinA` — while `θ − A` is under a right angle, and `|V|` past it, where the
450
+ * nearest point of the cone is its apex. Inside the cone it is negative. So the gap is exact for
451
+ * the sphere, and the sphere holds the froxel: the test can only keep a light, never drop one the
452
+ * shader would draw. The apex case is what drops a froxel behind a spot whose sphere reaches it,
453
+ * which the formula alone underestimates.
454
+ *
455
+ * **What it gives up** is tightness at the near end of the frustum, where a froxel is a long thin
456
+ * box whose sphere is much larger than it: a spot grazing such a froxel is kept. **What would make
457
+ * it wrong** is a profile showing those near froxels hold the cost; testing the box's corners is
458
+ * the tight version, at eight dot products a pair.
459
+ *
460
+ * Exported so the conformance page can tell a disagreement at the cone's edge from a real one; the
461
+ * GPU binner writes the same arithmetic in WGSL.
462
+ */
463
+ export declare function coneBoxGap(px: number, py: number, pz: number, axisX: number, axisY: number, axisZ: number, cosA: number, sinA: number, box: Float32Array): number;