@driftengine/core 4.8.6 → 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 (139) hide show
  1. package/CHANGELOG.json +71 -0
  2. package/README.md +1 -1
  3. package/dist/index.d.ts +2 -2
  4. package/dist/index.js +1 -1
  5. package/dist/render/anisotropicLobe.d.ts +10 -0
  6. package/dist/render/anisotropicLobe.js +19 -0
  7. package/dist/render/areaLights.d.ts +35 -0
  8. package/dist/render/areaLights.js +35 -0
  9. package/dist/render/backend/api.d.ts +33 -3
  10. package/dist/render/backend/webgl2/renderer.d.ts +50 -14
  11. package/dist/render/backend/webgl2/renderer.js +141 -30
  12. package/dist/render/backend/webgl2/sceneCaptureTarget.d.ts +29 -0
  13. package/dist/render/backend/webgl2/sceneCaptureTarget.js +96 -0
  14. package/dist/render/backend/webgpu/particlePass.js +2 -1
  15. package/dist/render/backend/webgpu/renderer.d.ts +35 -1
  16. package/dist/render/backend/webgpu/renderer.js +155 -16
  17. package/dist/render/backend/webgpu/sceneCaptureTarget.d.ts +24 -0
  18. package/dist/render/backend/webgpu/sceneCaptureTarget.js +59 -0
  19. package/dist/render/backend/webgpu/shaders/clusterBin.wgsl.d.ts +1 -1
  20. package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +4 -1
  21. package/dist/render/backend/webgpu/surfaceTexturePass.js +34 -8
  22. package/dist/render/backend/webgpu/waterPass.d.ts +1 -1
  23. package/dist/render/clusteredLights.d.ts +23 -5
  24. package/dist/render/clusteredLights.js +35 -8
  25. package/dist/render/driftLight/bake.d.ts +2 -0
  26. package/dist/render/driftLight/bake.js +1 -1
  27. package/dist/render/driftLight/falloff.d.ts +4 -2
  28. package/dist/render/driftLight/falloff.js +8 -2
  29. package/dist/render/gi/bounceLights.d.ts +3 -2
  30. package/dist/render/gi/bounceLights.js +8 -3
  31. package/dist/render/instanceCull.js +5 -0
  32. package/dist/render/instances.d.ts +19 -5
  33. package/dist/render/instances.js +9 -3
  34. package/dist/render/lightBudget.d.ts +12 -1
  35. package/dist/render/lightBudget.js +23 -3
  36. package/dist/render/lightmap.d.ts +23 -9
  37. package/dist/render/lightmap.js +40 -18
  38. package/dist/render/particlePool.d.ts +21 -0
  39. package/dist/render/particlePool.js +7 -0
  40. package/dist/render/pointLightSelection.d.ts +13 -0
  41. package/dist/render/pointLightSelection.js +3 -1
  42. package/dist/render/renderQuality.d.ts +8 -7
  43. package/dist/render/rendererHarness.js +3 -0
  44. package/dist/render/rgb9e5.d.ts +22 -0
  45. package/dist/render/rgb9e5.js +47 -0
  46. package/dist/render/sceneCapture.d.ts +29 -0
  47. package/dist/render/sceneCapture.js +36 -0
  48. package/dist/render/screenSpaceReflection.d.ts +27 -1
  49. package/dist/render/screenSpaceReflection.js +9 -0
  50. package/dist/render/shaders/bolt.d.ts +1 -1
  51. package/dist/render/shaders/bolt.js +7 -1
  52. package/dist/render/shaders/depth.js +11 -0
  53. package/dist/render/shaders/flat/anisotropic.d.ts +1 -1
  54. package/dist/render/shaders/flat/anisotropic.js +37 -3
  55. package/dist/render/shaders/flat/areaLight.d.ts +1 -1
  56. package/dist/render/shaders/flat/areaLight.js +46 -0
  57. package/dist/render/shaders/flat/glassTint.d.ts +3 -2
  58. package/dist/render/shaders/flat/glassTint.js +3 -1
  59. package/dist/render/shaders/flat/index.d.ts +5 -0
  60. package/dist/render/shaders/flat/index.js +16 -2
  61. package/dist/render/shaders/flat/lobes.d.ts +6 -0
  62. package/dist/render/shaders/flat/lobes.js +49 -43
  63. package/dist/render/shaders/flat/main.d.ts +1 -1
  64. package/dist/render/shaders/flat/main.js +57 -22
  65. package/dist/render/shaders/flat/models.d.ts +5 -2
  66. package/dist/render/shaders/flat/models.js +100 -16
  67. package/dist/render/shaders/flat/preamble.js +10 -3
  68. package/dist/render/shaders/generated/bolt.wgsl.d.ts +1 -1
  69. package/dist/render/shaders/generated/bolt.wgsl.js +1 -1
  70. package/dist/render/shaders/generated/depth.wgsl.d.ts +4 -4
  71. package/dist/render/shaders/generated/depth.wgsl.js +4 -4
  72. package/dist/render/shaders/generated/flat.wgsl.d.ts +16 -0
  73. package/dist/render/shaders/generated/flat.wgsl.js +54 -38
  74. package/dist/render/shaders/generated/ssrTrace.wgsl.d.ts +11 -6
  75. package/dist/render/shaders/generated/ssrTrace.wgsl.js +11 -6
  76. package/dist/render/shaders/generated/water.wgsl.d.ts +23 -16
  77. package/dist/render/shaders/generated/water.wgsl.js +23 -16
  78. package/dist/render/shaders/gi/probeBake.wgsl.js +9 -5
  79. package/dist/render/shaders/recon/motion.wgsl.d.ts +1 -1
  80. package/dist/render/shaders/recon/motion.wgsl.js +13 -2
  81. package/dist/render/shaders/ssrTrace.js +20 -1
  82. package/dist/render/shaders/water.d.ts +1 -1
  83. package/dist/render/shaders/water.js +12 -3
  84. package/dist/render/skinScatter.d.ts +3 -1
  85. package/dist/render/skinScatter.js +3 -1
  86. package/dist/render/ssrPass.js +1 -0
  87. package/dist/render/surfaceModel.d.ts +29 -8
  88. package/dist/render/surfaceModel.js +33 -9
  89. package/dist/render/surfaceTexture.d.ts +25 -1
  90. package/dist/render/surfaceTexture.js +27 -5
  91. package/package.json +6 -6
  92. package/src/index.ts +6 -2
  93. package/src/render/anisotropicLobe.ts +29 -0
  94. package/src/render/areaLights.ts +58 -0
  95. package/src/render/backend/api.ts +49 -2
  96. package/src/render/backend/webgl2/renderer.ts +192 -38
  97. package/src/render/backend/webgl2/sceneCaptureTarget.ts +130 -0
  98. package/src/render/backend/webgpu/particlePass.ts +2 -1
  99. package/src/render/backend/webgpu/renderer.ts +194 -19
  100. package/src/render/backend/webgpu/sceneCaptureTarget.ts +63 -0
  101. package/src/render/backend/webgpu/surfaceTexturePass.ts +44 -14
  102. package/src/render/clusteredLights.ts +41 -8
  103. package/src/render/driftLight/bake.ts +3 -1
  104. package/src/render/driftLight/falloff.ts +13 -2
  105. package/src/render/gi/bounceLights.ts +12 -3
  106. package/src/render/instanceCull.ts +4 -0
  107. package/src/render/instances.ts +26 -6
  108. package/src/render/lightBudget.ts +40 -2
  109. package/src/render/lightmap.ts +64 -23
  110. package/src/render/particlePool.ts +23 -0
  111. package/src/render/pointLightSelection.ts +20 -1
  112. package/src/render/renderQuality.ts +8 -7
  113. package/src/render/rendererHarness.ts +3 -0
  114. package/src/render/rgb9e5.ts +52 -0
  115. package/src/render/sceneCapture.ts +56 -0
  116. package/src/render/screenSpaceReflection.ts +33 -1
  117. package/src/render/shaders/bolt.ts +7 -1
  118. package/src/render/shaders/depth.ts +11 -0
  119. package/src/render/shaders/flat/anisotropic.ts +37 -3
  120. package/src/render/shaders/flat/areaLight.ts +46 -0
  121. package/src/render/shaders/flat/glassTint.ts +9 -2
  122. package/src/render/shaders/flat/index.ts +21 -2
  123. package/src/render/shaders/flat/lobes.ts +50 -43
  124. package/src/render/shaders/flat/main.ts +57 -22
  125. package/src/render/shaders/flat/models.ts +100 -16
  126. package/src/render/shaders/flat/preamble.ts +10 -3
  127. package/src/render/shaders/generated/bolt.wgsl.ts +1 -1
  128. package/src/render/shaders/generated/depth.wgsl.ts +4 -4
  129. package/src/render/shaders/generated/flat.wgsl.ts +54 -38
  130. package/src/render/shaders/generated/ssrTrace.wgsl.ts +11 -6
  131. package/src/render/shaders/generated/water.wgsl.ts +23 -16
  132. package/src/render/shaders/gi/probeBake.wgsl.ts +9 -5
  133. package/src/render/shaders/recon/motion.wgsl.ts +13 -2
  134. package/src/render/shaders/ssrTrace.ts +20 -1
  135. package/src/render/shaders/water.ts +12 -3
  136. package/src/render/skinScatter.ts +3 -1
  137. package/src/render/ssrPass.ts +1 -0
  138. package/src/render/surfaceModel.ts +47 -10
  139. package/src/render/surfaceTexture.ts +69 -9
package/CHANGELOG.json CHANGED
@@ -1,4 +1,75 @@
1
1
  [
2
+ {
3
+ "version": "4.8.7",
4
+ "date": "2026-10-06",
5
+ "entries": [
6
+ {
7
+ "kind": "changed",
8
+ "short": "A lightmap page is 8 bytes a texel, half what it was: the irradiance is an rgb9e5 word the lit stage decodes and filters, and a bake that arrives packed uploads where it lies.",
9
+ "text": "`createLightmap` stored a page as two half-float layers, 16 bytes a texel, so a stage of 49 pages and 21.8 million texels took 348 MB on the device. The irradiance is now one rgb9e5 word a texel, three 9-bit mantissas over a shared 5-bit exponent in the layout of `EXT_texture_shared_exponent` and WebGPU's `rgb9e5ufloat`, beside the direction's four bytes, in two `rgba8` layers that the lit stage decodes and filters itself: 8 bytes a texel, 174 MB for the same stage. `LightmapPage.irradiance` takes a `Uint32Array` of packed words as a bake hands them over, uploaded where they lie with no float array in between, as well as floats, which are packed once at upload. What it gives up is precision in a texel's smaller channels, which share the largest one's exponent, and anything past 65,408; light has neither to lose. On `demo/dev/lightmap.html` a page of one value is still 0 pixels from the same ambient raised by `setAmbientSH`, and the room is within one level of 4.8.6's on both backends."
10
+ },
11
+ {
12
+ "kind": "changed",
13
+ "short": "The BC decode worker is behind its own specifier, so a game importing `@driftengine/assets` no longer has it in its build: name `spawnBcWorker` as `DrftLoader`'s `bcWorker` to decode in it.",
14
+ "text": "`DrftLoader` decoded a BC texture the device cannot take as blocks in a worker it built with `new Worker(new URL('./bcWorker.ts', import.meta.url))` inside the package's barrel, and a bundler writes that worker out at transform time, before tree-shaking, so every game importing anything from `@driftengine/assets` carried the BC decoder's chunk from 4.8.4 on, whether or not it loaded a BC texture. The gate that asserts no barrel names a worker bundled each barrel against the built packages and skipped one that would not bundle, so it saw this one only when another test's `npm pack` had built them first, which is how it passed three releases. The factory is now `spawnBcWorker` at `@driftengine/assets/src/bcWorkers.ts`, as the physics island pool is, and a loader takes it as `bcWorker`; without it BC textures decode on the main thread and the loader says so once, naming the option. The gate bundles from source now. Every scene and page in this repository that loads a model names the worker."
15
+ },
16
+ {
17
+ "kind": "added",
18
+ "short": "`PointLightSource.falloffExponent` lights by `(1 - (d/R)^2)^n` with no distance term, Unreal's falloff for a light without inverse square, in both arms and on water, DriftLight and DriftRay.",
19
+ "text": "A point or spot light may name its own falloff: `(1 - (d/R)^2)^n` with `R` its radius and `n` the exponent, and no inverse square, which is how Unreal lights a light that does not use inverse-square falloff. A rig authored there keeps its shape: a fill meant to light a whole hall across a hundred metres does, where the frame's falloff had it gone a few metres out. 0, or absent, is the frame's falloff as before. The selection carries it in `PointLightBuffer.falloffExponents` and the environment in `lightFalloffExponents`; the fixed arm reads it in the third lane of the cone uniform, which a two-float array already paid a whole row for, so the eight-light build spends no uniform row on it, and the froxel table's record grows to six texels. Water, DriftLight's volume and DriftRay's bounce fall off by it too, so a light lands the same way on every surface it reaches. On `demo/dev/clustered.html` one light drawn by the fixed arm and by the table is 0 pixels apart on both backends, and an exponent of 8 moves 42,123 pixels against none."
20
+ },
21
+ {
22
+ "kind": "added",
23
+ "short": "`AreaLightSource.barnDoorAngle` and `barnDoorLength` hang barn doors on a rectangle: each fragment is lit by the part the doors leave in view, its highlight included, in both arms.",
24
+ "text": "A rectangle may carry barn doors, a flap hinged at each edge standing `barnDoorAngle` degrees from its normal and `barnDoorLength` metres long, 20 cm when absent, as Unreal's rect lights do. At 90 they fold flat and hide nothing; smaller narrows the light to the opening they leave. Each fragment shades the part of the rectangle no door's tip hides, one axis at a time, so the form factor and the integrated highlight both come from that smaller rectangle, in the fixed arm, which reads the doors in the size uniform's two spare lanes, and in the froxel table's record. Each door is taken as endless along its hinge, so where two meet the corner they hide is square rather than mitred, and a shadowed rectangle's map is still baked from its whole face. Measured from straight above a 1.2 by 0.5 m panel 1.2 m over a floor with 20 degree, 30 cm doors: nothing changes within the 1.04 by 0.69 m box the door tips leave whole, nothing is lit past 4.94 and 2.32 m where they hide all of it, the fixed and clustered arms are 0 pixels apart, and doors at 90 degrees are 0 pixels from none, on both backends."
25
+ },
26
+ {
27
+ "kind": "added",
28
+ "short": "`SurfaceMaterial.physicalSpecular` shades a lamp's and the sun's highlight as GGX's own `pi D Vis F N.L` with the specular attribute as F0, on the standard, lightmap and anisotropic models.",
29
+ "text": "The engine's highlight is a look control: it peaks at the specular attribute times one however rough the surface, so strength and width are set apart. A material with `physicalSpecular: true` takes GGX's own term instead, `pi * D * Vis * F * N.L` with Smith's height-correlated masking, as Unreal shades and as this engine's skin and eye already did. Its peak is `1 / (4 alpha^2)` of the light head-on, so a polished surface's highlight is several times the look's and a rough one's lower and wider; the specular attribute is read as the reflectance at normal incidence, Unreal's `0.08 * Specular`, and Fresnel brightens every surface toward grazing rather than a metal alone. Lamps, widened by their size as before, and the sun take it, on the standard, lightmap and anisotropic models; a rectangle's highlight was already the lobe integrated over it with that F0. The code is a lit switch, compiled into the lit programs the first time a material asks, so a scene that never does carries none of it. Measured with a lamp and an eye mirrored about a floor at roughness 0.5, where the term works out by hand to 4.4376: the physical highlight at 1/4.4377 of the light reads the look's middle pixel to within a level on both backends, from a lamp and from the sun, and the anisotropic model's physical lobe at strength 0 is the standard one to within a level everywhere."
30
+ },
31
+ {
32
+ "kind": "added",
33
+ "short": "`ReflectiveSurface.fresnel` reads `strength` as the surface's reflectance head-on and weighs each pixel by the split-sum BRDF at its view and `roughness`, so a reflection follows the camera.",
34
+ "text": "A screen-space reflection's `strength` was one share over its box, so a floor reflected as much underfoot as toward the horizon and a consumer re-matched the share to its camera's angle each frame. With `fresnel: true` the strength is the surface's reflectance head-on and each pixel returns `F0 * A + B` of what its ray finds, the split-sum environment BRDF at its own view and the box's `roughness`: Karis's fit, now one GLSL text that the lit stage and the reflection pass both include. The pass reads no material, so the reflectance and roughness are the box's, and nothing occludes the reflection where the lit stage's ambient occlusion would. Off by default, the one share as before. Measured on `demo/dev/ssr.html`: at a reflectance of 1 and roughness 0 it is 0 pixels from a strength of 1, as the fit says, and at a pixel whose view works out by hand to N.V 0.344, reflectance 0.04 matches the constant strength the fit gives there, 0.153, to within a level on both backends."
35
+ },
36
+ {
37
+ "kind": "added",
38
+ "short": "`MeshInstances.uvRegions` gives each instance its own cell of its texture, so mesh particles on different flipbook frames, or props on different tiles of an atlas, are one draw.",
39
+ "text": "Four floats an instance, `[scaleU, scaleV, offsetU, offsetV]`, applied to the mesh's texture coordinates before the material's own scale and offset, in the lit, depth and motion stages alike, so a cutout instance's shadow is cut by its own cell. They ride the bottom row of the instance's matrix, which a placement made of a turn, a scale and a move leaves at `[0, 0, 0, 1]` and which every stage drawing an instance now rebuilds as that, so they cost no attribute and no byte; an instanced placement must be affine, as every placement of a solid object is. Absent, every instance wears the whole texture, and a culled copy carries the cells into a target that has the array. On `demo/dev/instanceCells.html` eight cards each on their own cell of a strip, in one instanced draw, are 0 pixels from eight single draws with the cell in their materials, opaque and blended, on both backends; the published scenes and the instanced shadow, cull and opacity pages are 0 pixels from 4.8.6 on WebGPU."
40
+ },
41
+ {
42
+ "kind": "added",
43
+ "short": "`createSceneCapture` and `captureScene` draw the world from a camera of your own into a texture any material shows, a stadium screen's emissive or a monitor's, on both backends.",
44
+ "text": "`createSceneCapture(width, height)` makes a texture the scene can be drawn into, and `captureScene(capture, camera, clearColor, draw)` draws it: the callback is handed the camera, its matrices shaped to the capture, and submits the world exactly as it would to the screen. The texture then goes wherever an image goes, as a material's `emissive` for a screen in the world showing what another camera sees, a monitor or a security feed. It holds radiance, as a reflection probe does: the output transform and exposure are held off for the pass, at the world's colour format and sample count, half floats under `hdrScene`, so the frame grades the screen once with everything around it. It is the probe bake's arrangement and the mesh pass alone: the sky, water and particles are in it only if the callback draws them, nothing after the world runs on it, it has no mip chain, and the frame's camera, jitter, motion, refraction copy and skin spread are left alone while it draws. WebGL2 draws a capture's rows flipped, as it does a probe face's, so it reads as an image does. A second camera's view shown on a quad filling the frame is 0 pixels from that view drawn directly, single-sampled on both backends and at four samples on WebGPU, where WebGL2's multisampled capture is WebGPU's to the pixel; and a frame with a capture in it is 0 pixels from the frame without one outside the screen."
45
+ },
46
+ {
47
+ "kind": "added",
48
+ "short": "`drawDeviceParticles` draws a particle pool's material from a buffer your own compute shader wrote, where it lies, with nothing read back. WebGPU only; WebGL2 says so once.",
49
+ "text": "A simulation can run entirely on the device: a compute shader of the consumer's own, registered with `registerCompute`, writes its particles into a buffer of its own, and `drawDeviceParticles(batch, { buffer, count }, camera, env, time)` draws the pool's material, blend, flipbook, fades and fog from that buffer in place of the one the pool uploads into. Each particle is `DEVICE_PARTICLE_FLOATS`, sixteen floats in a `ParticleInstances`' order, and the buffer needs `VERTEX` usage beside `STORAGE`; a slot of half-width 0 draws nothing, so a fixed-size buffer can hold fewer live particles than it has room for, and a count past what the buffer holds is cut to it. The particles are not sorted. WebGL2 has no compute stage, so there it says so once and draws nothing. On `demo/dev/spriteParticles.html` sixteen sprites written by a compute shader are 0 pixels from the same sixteen uploaded, soft edges included."
50
+ },
51
+ {
52
+ "kind": "fixed",
53
+ "short": "A skin's `radius` is how far its light travels, as documented: the profile is at two fifths of it, so screen-space skin no longer smears a face seen up close over tens of pixels.",
54
+ "text": "`skinModel`'s `radius` was documented as how far light travels beneath the surface before it leaves, and was handed to Burley's profile as its distance `d` itself. That profile carries light 2.5 `d` on average and the blur's taps reach 9 `d`, so light travelled two and a half times as far as the model said, and `skinScattering: 'screen-space'` spread a face at arm's length over sixty pixels: a nose, lips and brows washed into one blur. The profile is now at `radius / 2.5` in both paths, the screen-space blur and the lit stage's pre-integrated fit, transmission and penumbra, so the two still agree. Measured first rather than assumed: on a step of light at 0.75 m and 800 by 600, the blur matched the profile's own masses at the old distance exactly, edges at the predicted taps, so the kernel was right and the distance was not; it now reaches 26 pixels where it reached 65, on both backends. A skin authored against the old meaning and looking right keeps its look with its `radius` raised by 2.5."
55
+ },
56
+ {
57
+ "kind": "fixed",
58
+ "short": "The anisotropic model on a mesh with neither tangents nor texture coordinates drew black, a direction normalised from zero; it takes a direction in the surface now.",
59
+ "text": "A brushed surface takes its direction from the tangent frame, and a mesh carrying neither tangents nor coordinates gives a frame of zeros, which normalised to a NaN and drew the whole surface black. It takes a direction in the surface's plane instead, across the world's up, so the highlight is stretched somewhere. Measured on a floor with no coordinates: the anisotropic model at strength 0 draws the standard highlight to within a level on both backends, where it drew nothing."
60
+ },
61
+ {
62
+ "kind": "fixed",
63
+ "short": "Arcs stay finite under multisampling: an edge pixel shaded off the quad took the filament's side coordinate past one and its powers to NaN, which bloom spread into a white disc.",
64
+ "text": "A multisampled target shades a pixel on a quad's edge at its centre, which can lie outside the quad. There an arc's side coordinate ran past one, the filament's `across` went negative, and `pow` of a negative number is NaN on both backends: one dark speck, which bloom then spread into a white disc the size of its blur around every arc. `across` is clamped before its powers. Found by a scene drawing short arcs at four samples."
65
+ },
66
+ {
67
+ "kind": "note",
68
+ "short": "The lit stage carries the falloff exponent and barn doors in every permutation: 2.8 to 6.4% more instructions on RADV at a phone's viewport, and the same registers.",
69
+ "text": "What every lit pipeline now compiles was measured on RADV with a phone's viewport, against 4.8.6: the fixed-arm lit pixel shader is 5,778 instructions where it was 5,618 on WebGPU and 5,171 where it was 4,989 on WebGL2, both still 96 registers; the clustered one is 6,831 where it was 6,507 and 6,313 where it was 5,934, still 120 and 108. Registers set how many fragments a phone's GPU keeps in flight, and they did not move. The physical highlight is a lit switch and costs nothing until a material asks for it. Core's gzipped size is 828.1 KB, 29.0 KB over 4.8.6, almost all of it the generated WGSL, whose sixteen lit permutations each carry the new light loop."
70
+ }
71
+ ]
72
+ },
2
73
  {
3
74
  "version": "4.8.6",
4
75
  "date": "2026-10-05",
package/README.md CHANGED
@@ -6,7 +6,7 @@ carries the loop, the renderer, cameras, input, the scene graph, geometry, text
6
6
  and the whole of [`@driftengine/physics`](https://driftengine.dev/docs/simulation/rigid-bodies),
7
7
  which it re-exports. The rest of the engine is separate packages you add when you need them.
8
8
 
9
- **Cost: 777.5 KB gzipped, importing `createRenderer`.** Measured by `scripts/size-gate.test.mjs`,
9
+ **Cost: 828.1 KB gzipped, importing `createRenderer`.** Measured by `scripts/size-gate.test.mjs`,
10
10
  which fails the build if the figure drifts by more than 3%.
11
11
 
12
12
  The manual is at **[driftengine.dev/docs](https://driftengine.dev/docs)**, with an example running
package/dist/index.d.ts CHANGED
@@ -367,8 +367,8 @@ export { seaStateForAgitation, seaStateForWind } from './render/seaState.ts';
367
367
  export type { SeaState } from './render/seaState.ts';
368
368
  export { InstancedMesh, createInstanceData, writeInstance } from './render/instancedMesh.ts';
369
369
  export { createMeshInstances, packInstances, INSTANCE_FLOATS, INSTANCE_STRIDE, } from './render/instances.ts';
370
- export { ParticlePool } from './render/particlePool.ts';
371
- export type { ParticlePoolOptions, ParticleInstances } from './render/particlePool.ts';
370
+ export { ParticlePool, DEVICE_PARTICLE_FLOATS } from './render/particlePool.ts';
371
+ export type { DeviceParticles, ParticlePoolOptions, ParticleInstances, } from './render/particlePool.ts';
372
372
  export { ParticleBatch } from './render/particleBatch.ts';
373
373
  export type { ParticleBatchOptions, ParticleBlend, ParticleFacing, } from './render/particleBatch.ts';
374
374
  export { PARTICLE_MOTE_FRAG, PARTICLE_SMOKE_FRAG, PARTICLE_SPARK_FRAG, PARTICLE_VERT, } from './render/shaders/particle.ts';
package/dist/index.js CHANGED
@@ -474,7 +474,7 @@ export { InstancedMesh, createInstanceData, writeInstance } from './render/insta
474
474
  * pitches and rolls and a yaw cannot say so.
475
475
  */
476
476
  export { createMeshInstances, packInstances, INSTANCE_FLOATS, INSTANCE_STRIDE, } from './render/instances.js';
477
- export { ParticlePool } from './render/particlePool.js';
477
+ export { ParticlePool, DEVICE_PARTICLE_FLOATS } from './render/particlePool.js';
478
478
  export { ParticleBatch } from './render/particleBatch.js';
479
479
  export { PARTICLE_MOTE_FRAG, PARTICLE_SMOKE_FRAG, PARTICLE_SPARK_FRAG, PARTICLE_VERT, } from './render/shaders/particle.js';
480
480
  export { BoltPool } from './render/boltPool.js';
@@ -5,3 +5,13 @@ export declare function anisotropicAlphas(roughness: number, strength: number, o
5
5
  * are `th`, `bh` and `nh`. 1 at the mirror direction.
6
6
  */
7
7
  export declare function anisotropicLobe(th: number, bh: number, nh: number, alphaT: number, alphaB: number): number;
8
+ /** Three components in the lobe's frame: along the direction, across it, along the normal. */
9
+ export type FrameVector = readonly [number, number, number];
10
+ /**
11
+ * The physical form of the same lobe, `π · D · Vis · N·L`, for a surface whose highlight is GGX's
12
+ * own (`SurfaceMaterial.physicalSpecular`): the stretched distribution normalised as a BRDF's, and
13
+ * Smith's height-correlated masking stretched with it (Heitz 2014, as Filament writes it), Fresnel
14
+ * left to the caller. The half vector, the light and the view are each in the lobe's frame. At
15
+ * strength 0 it is isotropic GGX, the term skin's and the eye's highlights are.
16
+ */
17
+ export declare function anisotropicPhysicalLobe(half: FrameVector, light: FrameVector, view: FrameVector, alphaT: number, alphaB: number): number;
@@ -31,3 +31,22 @@ export function anisotropicLobe(th, bh, nh, alphaT, alphaB) {
31
31
  const d = x * x + y * y + nh * nh;
32
32
  return 1 / Math.max(d * d, 1e-8);
33
33
  }
34
+ /**
35
+ * The physical form of the same lobe, `π · D · Vis · N·L`, for a surface whose highlight is GGX's
36
+ * own (`SurfaceMaterial.physicalSpecular`): the stretched distribution normalised as a BRDF's, and
37
+ * Smith's height-correlated masking stretched with it (Heitz 2014, as Filament writes it), Fresnel
38
+ * left to the caller. The half vector, the light and the view are each in the lobe's frame. At
39
+ * strength 0 it is isotropic GGX, the term skin's and the eye's highlights are.
40
+ */
41
+ export function anisotropicPhysicalLobe(half, light, view, alphaT, alphaB) {
42
+ const x = half[0] / alphaT;
43
+ const y = half[1] / alphaB;
44
+ const d = x * x + y * y + half[2] * half[2];
45
+ const distribution = 1 / (Math.PI * alphaT * alphaB * d * d);
46
+ const ndl = light[2];
47
+ const ndv = view[2];
48
+ const lambdaView = ndl * Math.hypot(alphaT * view[0], alphaB * view[1], ndv);
49
+ const lambdaLight = ndv * Math.hypot(alphaT * light[0], alphaB * light[1], ndl);
50
+ const visibility = 0.5 / Math.max(lambdaView + lambdaLight, 1e-5);
51
+ return Math.PI * distribution * visibility * ndl;
52
+ }
@@ -95,6 +95,22 @@ export interface AreaLightSource {
95
95
  * of a scene unit, and never inside the rectangle itself. Unreal's `AttenuationRadius`, in metres.
96
96
  */
97
97
  range?: number;
98
+ /**
99
+ * Barn doors: a flap hinged at each of the rectangle's four edges, standing this many degrees from
100
+ * its normal. 90, or absent, folds them flat and they hide nothing; smaller closes them, so the
101
+ * light narrows to the opening they leave — a rectangle at 50° throws a beam rather than filling
102
+ * its hemisphere. Unreal's `BarnDoorAngle`, whose default of 88° is nearly flat.
103
+ *
104
+ * What a fragment sees past the doors is the part of the rectangle no door's tip hides, one axis
105
+ * at a time, and the rectangle then shades as that smaller rectangle, its highlight included.
106
+ * Its shadow map is still baked from the whole rectangle's centre. **What it gives up**: each
107
+ * door is taken as endless along its hinge, so the two axes clip apart and the corner where two
108
+ * doors meet hides a square rather than the mitre two real flaps make. One-sided: a two-sided
109
+ * rectangle's back face has none.
110
+ */
111
+ barnDoorAngle?: number;
112
+ /** The barn doors' length in metres. Absent, 0.2: Unreal's default of 20 cm. */
113
+ barnDoorLength?: number;
98
114
  }
99
115
  /** The irradiance a derived `range` ends at, in scene units. See `AreaLightSource.range`. */
100
116
  export declare const AREA_RANGE_CUTOFF = 0.001;
@@ -109,8 +125,27 @@ export interface AreaLightBuffer {
109
125
  readonly twoSided: Float32Array;
110
126
  /** One a rectangle: how far its light reaches. See `AreaLightSource.range`. */
111
127
  readonly ranges?: Float32Array;
128
+ /**
129
+ * Two a rectangle: the cosine of its barn doors' angle from its normal, then their length in
130
+ * metres — 0 and 0 for none. See `AreaLightSource.barnDoorAngle`.
131
+ */
132
+ readonly barnDoors?: Float32Array;
112
133
  }
113
134
  export declare function createAreaLightBuffer(capacity?: number): AreaLightBuffer;
135
+ /** Unreal's default barn door length, in metres. See `AreaLightSource.barnDoorLength`. */
136
+ export declare const DEFAULT_BARN_DOOR_LENGTH = 0.2;
137
+ /**
138
+ * A rectangle's barn doors as the shader reads them, into `out[at]` and `out[at + 1]`: the cosine of
139
+ * their angle and their length. An angle not between 0 and 90 degrees, or a length not above zero,
140
+ * is no doors at all, written as 0 and 0 — which the shader reads as "nothing hidden".
141
+ */
142
+ export declare function barnDoorsOf(light: AreaLightSource, out: Float32Array, at: number): void;
143
+ /**
144
+ * The fixed arm's half extents and barn doors, four a rectangle as `uAreaLightSize` holds them: the
145
+ * two half extents, then the doors' cosine and length. Into a target the binder owns, for the first
146
+ * `MAX_AREA_LIGHTS` alone, which are all the fixed arm reads.
147
+ */
148
+ export declare function packAreaSizesAndDoors(lights: AreaLightBuffer, out: Float32Array): void;
114
149
  /**
115
150
  * How far a rectangle's light reaches when it names no range: where its irradiance on its own axis,
116
151
  * `L · A / d²` for a rectangle of radiance `L` and area `A` seen from far, falls to
@@ -20,8 +20,41 @@ export function createAreaLightBuffer(capacity = MAX_AREA_LIGHTS) {
20
20
  sizes: new Float32Array(capacity * 2),
21
21
  twoSided: new Float32Array(capacity),
22
22
  ranges: new Float32Array(capacity),
23
+ barnDoors: new Float32Array(capacity * 2),
23
24
  };
24
25
  }
26
+ /** Unreal's default barn door length, in metres. See `AreaLightSource.barnDoorLength`. */
27
+ export const DEFAULT_BARN_DOOR_LENGTH = 0.2;
28
+ /**
29
+ * A rectangle's barn doors as the shader reads them, into `out[at]` and `out[at + 1]`: the cosine of
30
+ * their angle and their length. An angle not between 0 and 90 degrees, or a length not above zero,
31
+ * is no doors at all, written as 0 and 0 — which the shader reads as "nothing hidden".
32
+ */
33
+ export function barnDoorsOf(light, out, at) {
34
+ const angle = light.barnDoorAngle;
35
+ const length = light.barnDoorLength ?? DEFAULT_BARN_DOOR_LENGTH;
36
+ if (angle === undefined || !(angle >= 0 && angle < 90) || !(length > 0)) {
37
+ out[at] = 0;
38
+ out[at + 1] = 0;
39
+ return;
40
+ }
41
+ out[at] = Math.cos((angle * Math.PI) / 180);
42
+ out[at + 1] = length;
43
+ }
44
+ /**
45
+ * The fixed arm's half extents and barn doors, four a rectangle as `uAreaLightSize` holds them: the
46
+ * two half extents, then the doors' cosine and length. Into a target the binder owns, for the first
47
+ * `MAX_AREA_LIGHTS` alone, which are all the fixed arm reads.
48
+ */
49
+ export function packAreaSizesAndDoors(lights, out) {
50
+ const doors = lights.barnDoors;
51
+ for (let a = 0; a < MAX_AREA_LIGHTS; a++) {
52
+ out[a * 4] = lights.sizes[a * 2] ?? 0;
53
+ out[a * 4 + 1] = lights.sizes[a * 2 + 1] ?? 0;
54
+ out[a * 4 + 2] = doors?.[a * 2] ?? 0;
55
+ out[a * 4 + 3] = doors?.[a * 2 + 1] ?? 0;
56
+ }
57
+ }
25
58
  /**
26
59
  * How far a rectangle's light reaches when it names no range: where its irradiance on its own axis,
27
60
  * `L · A / d²` for a rectangle of radiance `L` and area `A` seen from far, falls to
@@ -106,6 +139,8 @@ export function selectAreaLights(lights, out) {
106
139
  ? light.range
107
140
  : areaRange(light.r, light.g, light.b, light.halfWidth, light.halfHeight);
108
141
  }
142
+ if (out.barnDoors !== undefined)
143
+ barnDoorsOf(light, out.barnDoors, slot * 2);
109
144
  }
110
145
  return out;
111
146
  }
@@ -1,5 +1,6 @@
1
1
  import type { Bounds } from '../../math/bounds.ts';
2
2
  import type { Vec3 } from '../../math/color.ts';
3
+ import type { Camera } from '../camera.ts';
3
4
  import type { ReadonlyMat4 } from 'gl-matrix';
4
5
  import type { MeshData } from '../mesh.ts';
5
6
  import type { ProbeBakeOptions } from '../reflectionProbe.ts';
@@ -8,6 +9,7 @@ import type { InstancedOptions, MeshInstances } from '../instances.ts';
8
9
  import type { SurfaceMaterial } from '../surfaceTexture.ts';
9
10
  import type { SurfaceSource } from '../compressedSource.ts';
10
11
  import type { LightmapPage } from '../lightmap.ts';
12
+ import type { DeviceParticles } from '../particlePool.ts';
11
13
  import type { ClothBindingData } from '../clothBindingData.ts';
12
14
  import type { TextStyle } from '../textLayout.ts';
13
15
  import type { SdfFont } from '../sdfFont.ts';
@@ -187,7 +189,7 @@ export interface MeshOptions {
187
189
  }
188
190
  export type RendererApi = Omit<{
189
191
  [K in keyof Renderer]: Renderer[K];
190
- }, 'gpuTimer' | 'createMesh' | 'createMeshIncremental' | 'updateMesh' | 'drawMesh' | 'disposeMesh' | 'drawTranslucentMesh' | 'drawLightVolume' | 'drawFilm' | 'createScatter' | 'drawScatter' | 'uploadScatter' | 'disposeScatter' | 'createInstanced' | 'uploadInstanced' | 'drawInstanced' | 'drawTranslucentInstanced' | 'disposeInstanced' | 'createWater' | 'drawWater' | 'disposeWater' | 'createPlumes' | 'drawPlumes' | 'disposePlumes' | 'setPlumeScale' | 'createCaustics' | 'drawCaustics' | 'disposeCaustics' | 'createParticles' | 'drawParticles' | 'disposeParticles' | 'createWindStreaks' | 'drawWindStreaks' | 'disposeWindStreaks' | 'createFlock' | 'drawFlock' | 'disposeFlock' | 'createBolts' | 'drawBolts' | 'disposeBolts' | 'createLines' | 'drawLines' | 'disposeLines' | 'createText' | 'setText' | 'setPlate' | 'drawText' | 'textWidth' | 'disposeText' | 'createSdfText' | 'setSdfText' | 'drawSdfText' | 'disposeSdfText' | 'createSurfaceTexture' | 'createSurfaceTextureArray' | 'createLightmap' | 'updateSurfaceTexture' | 'disposeSurfaceTexture' | 'setSurfaceTexture' | 'setMaterial' | 'createClothBinding' | 'createClothParticles' | 'updateClothParticles' | 'setCloth' | 'disposeClothBinding' | 'disposeClothParticles'> & {
192
+ }, 'gpuTimer' | 'createMesh' | 'createMeshIncremental' | 'updateMesh' | 'drawMesh' | 'disposeMesh' | 'drawTranslucentMesh' | 'drawLightVolume' | 'drawFilm' | 'createScatter' | 'drawScatter' | 'uploadScatter' | 'disposeScatter' | 'createInstanced' | 'uploadInstanced' | 'drawInstanced' | 'drawTranslucentInstanced' | 'disposeInstanced' | 'createWater' | 'drawWater' | 'disposeWater' | 'createPlumes' | 'drawPlumes' | 'disposePlumes' | 'setPlumeScale' | 'createCaustics' | 'drawCaustics' | 'disposeCaustics' | 'createParticles' | 'drawParticles' | 'drawDeviceParticles' | 'disposeParticles' | 'createWindStreaks' | 'drawWindStreaks' | 'disposeWindStreaks' | 'createFlock' | 'drawFlock' | 'disposeFlock' | 'createBolts' | 'drawBolts' | 'disposeBolts' | 'createLines' | 'drawLines' | 'disposeLines' | 'createText' | 'setText' | 'setPlate' | 'drawText' | 'textWidth' | 'disposeText' | 'createSdfText' | 'setSdfText' | 'drawSdfText' | 'disposeSdfText' | 'createSurfaceTexture' | 'createSurfaceTextureArray' | 'createLightmap' | 'createSceneCapture' | 'captureScene' | 'updateSurfaceTexture' | 'disposeSurfaceTexture' | 'setSurfaceTexture' | 'setMaterial' | 'createClothBinding' | 'createClothParticles' | 'updateClothParticles' | 'setCloth' | 'disposeClothBinding' | 'disposeClothParticles'> & {
191
193
  /**
192
194
  * A mesh's cloth binding: which simulation triangle each of its vertices follows, where on it and
193
195
  * by how much (`ClothBindingData`). The mesh must be skinned — a binding blends each vertex
@@ -232,12 +234,33 @@ export type RendererApi = Omit<{
232
234
  createSurfaceTextureArray(sources: readonly SurfaceSource[], options?: Parameters<Renderer['createSurfaceTexture']>[1]): SurfaceTextureHandle;
233
235
  /**
234
236
  * A baked lightmap page on the device, as the texture a `lightmapModel` material takes for its
235
- * `modelMap`: two layers of half floats, the irradiance and the direction, filtered linearly and
236
- * clamped at its edges, with no mip chain. Disposed as any texture is, with
237
+ * `modelMap`: two layers of four bytes a texel, the irradiance as an rgb9e5 word and the
238
+ * direction, which the lit stage decodes and filters itself, clamped at the page's edges, with no
239
+ * mip chain. A page whose irradiance arrives packed uploads where it lies. Disposed as any texture is, with
237
240
  * `disposeSurfaceTexture`; `updateSurfaceTexture` refuses it, as it refuses any array. See
238
241
  * `lightmap.ts` for what a surface does with it.
239
242
  */
240
243
  createLightmap(page: LightmapPage): SurfaceTextureHandle;
244
+ /**
245
+ * A texture the scene can be drawn into from a camera of the caller's (`captureScene`), which a
246
+ * material then shows like any image — most often as its `emissive`, for a screen in the world
247
+ * showing what a camera elsewhere sees. `width` and `height` in pixels, 1 to 4096. It holds
248
+ * radiance, at the colour format a probe takes: half floats under `hdrScene`, bytes otherwise.
249
+ * Disposed as any texture is; `updateSurfaceTexture` refuses it. See `sceneCapture.ts`.
250
+ */
251
+ createSceneCapture(width: number, height: number): SurfaceTextureHandle;
252
+ /**
253
+ * Draw the scene into a capture from `camera`, at the moment a caller says, usually once a frame
254
+ * before the frame's own world. `draw` is handed the camera, its matrices updated for the
255
+ * capture's shape, and submits the scene exactly as it would to the screen — `bindMeshPass`,
256
+ * `drawMesh` and the rest; `bindMeshPass` the frame's own camera again afterwards. The output
257
+ * transform is held off, so a screen showing it is graded once, with the frame.
258
+ *
259
+ * **The mesh pass alone**, as a probe bake is: what a scene draws after its world is in the
260
+ * capture only if `draw` draws it, and no temporal resolve or effect after the world runs on it.
261
+ * Returns whether it drew: false for a texture that is not a capture.
262
+ */
263
+ captureScene(capture: SurfaceTextureHandle, camera: Camera, clearColor: Vec3, draw: (camera: Camera) => void): boolean;
241
264
  /**
242
265
  * Replace a texture's pixels, keeping the GPU object and its sampler state.
243
266
  *
@@ -444,6 +467,13 @@ export type RendererApi = Omit<{
444
467
  }): ParticleHandle;
445
468
  /** Draw a pool's live particles, after the opaque scene. */
446
469
  drawParticles(batch: ParticleHandle, data: Parameters<Renderer['drawParticles']>[1], camera: Parameters<Renderer['drawParticles']>[2], env: Parameters<Renderer['drawParticles']>[3], timeSeconds: number): void;
470
+ /**
471
+ * Draw a pool's particles from a buffer a caller's own compute wrote (`registerCompute`), where
472
+ * they lie: the pool's material, blend, flipbook and fades, its instance stream the caller's
473
+ * buffer. WebGPU only; WebGL2 says so once and draws nothing. Not sorted. See `DeviceParticles`
474
+ * for the layout.
475
+ */
476
+ drawDeviceParticles(batch: ParticleHandle, particles: DeviceParticles, camera: Parameters<Renderer['drawParticles']>[2], env: Parameters<Renderer['drawParticles']>[3], timeSeconds: number): void;
447
477
  /** Release a particle pool. */
448
478
  disposeParticles(batch: ParticleHandle): void;
449
479
  /** Draw a wet film over geometry, using a handle as the surface it lies on. */
@@ -61,7 +61,7 @@ import type { Atmosphere } from '../../atmosphere.ts';
61
61
  import { InstancedMesh } from '../../instancedMesh.ts';
62
62
  import { ParticleBatch } from '../../particleBatch.ts';
63
63
  import type { ParticleBatchOptions } from '../../particleBatch.ts';
64
- import type { ParticleInstances } from '../../particlePool.ts';
64
+ import type { DeviceParticles, ParticleInstances } from '../../particlePool.ts';
65
65
  import { BoltBatch } from '../../boltBatch.ts';
66
66
  import type { BoltSegments } from '../../boltPool.ts';
67
67
  import { LineBatch } from '../../lineBatch.ts';
@@ -198,6 +198,11 @@ export interface Environment extends Atmosphere {
198
198
  lightDirections: Float32Array;
199
199
  /** Two per active light: the cosine of the inner cone angle, then of the outer. */
200
200
  lightConeCos: Float32Array;
201
+ /**
202
+ * One per active light: its own falloff exponent, or 0 for the frame's. Optional: absent is 0 for
203
+ * every light. See `PointLightSource.falloffExponent`.
204
+ */
205
+ lightFalloffExponents?: Float32Array;
201
206
  /** One per active light: a row of the photometric atlas, or −1 for none. */
202
207
  lightIesProfiles: Float32Array;
203
208
  /**
@@ -1231,6 +1236,15 @@ export declare class WebGL2Renderer implements RendererApi {
1231
1236
  private warnedEnvironmentAmbient;
1232
1237
  /** Whether a probe is being baked, so the passes that would recurse into one can tell. */
1233
1238
  private probePassActive;
1239
+ /**
1240
+ * Whether a scene capture is being drawn (`captureScene`): an off-screen pass like a bake, so the
1241
+ * frame's own view, its refraction copy and skin's spread are left alone, but lit as the frame is.
1242
+ */
1243
+ private capturePassActive;
1244
+ /** Each capture's framebuffer, beside the texture a material binds. See `createSceneCapture`. */
1245
+ private readonly captureTargets;
1246
+ /** Said once, where a driver refuses to draw into a capture. */
1247
+ private warnedCapture;
1234
1248
  /** Whether the bake under way lights its faces by the grid. `ProbeBakeOptions.bounce`. */
1235
1249
  private probeBounce;
1236
1250
  /** The clip-control extension, or null where this context does not offer it. */
@@ -1833,6 +1847,21 @@ export declare class WebGL2Renderer implements RendererApi {
1833
1847
  createSurfaceTextureArray(sources: readonly SurfaceSource[], options?: SurfaceTextureOptions): SurfaceTexture;
1834
1848
  /** A baked lightmap page, as a `lightmapModel` material's `modelMap`. See `RendererApi`. */
1835
1849
  createLightmap(page: LightmapPage): SurfaceTexture;
1850
+ /**
1851
+ * A texture the scene can be drawn into, for a material to show. See `RendererApi` and
1852
+ * `sceneCapture.ts`. Half floats where the frame keeps range, as a probe does.
1853
+ */
1854
+ createSceneCapture(width: number, height: number): SurfaceTexture;
1855
+ /**
1856
+ * Draw the scene into a capture from `camera`: `draw` is handed the camera, its matrices updated
1857
+ * for the capture's shape, and submits the scene as it would to the screen. See `RendererApi`.
1858
+ *
1859
+ * **Radiance, as a probe bake stores it**: the output transform and exposure are held off for the
1860
+ * pass, so the frame grades a screen showing it once. No temporal jitter, which a capture has no
1861
+ * resolve to undo. Returns whether it drew: false for a texture that is not a capture, or one the
1862
+ * driver will not draw into.
1863
+ */
1864
+ captureScene(capture: SurfaceTexture, camera: Camera, clearColor: Vec3, draw: (camera: Camera) => void): boolean;
1836
1865
  /**
1837
1866
  * Replace a surface texture's pixels, keeping the GPU object and its sampler state.
1838
1867
  *
@@ -1963,18 +1992,6 @@ export declare class WebGL2Renderer implements RendererApi {
1963
1992
  * program state, and this puts it back to what `currentSurfaceTexture` says it should be.
1964
1993
  */
1965
1994
  private restoreSurfaceTextureUnit;
1966
- /**
1967
- * A pass-level scale over the grain the geometry itself declared, 0 to 1.
1968
- *
1969
- * Which surfaces are mineral is now `MeshData.grain`, stated per vertex through
1970
- * `MeshBuilder.setGrain` — because it is a property of a material, and absent means none.
1971
- * This stays as the override for the case that has no answer to state: an imported model
1972
- * arrives with no grain information at all, and a caller drawing one decides for it.
1973
- *
1974
- * Pass state, like `setSurfaceTexture`, because a material covers many draws. The default
1975
- * is 1 and `bindMeshPass` restores it, so it scales geometry that declared grain and
1976
- * leaves geometry that did not exactly as it is.
1977
- */
1978
1995
  /**
1979
1996
  * The ambient the following draws take, as nine spherical-harmonic coefficients of incoming
1980
1997
  * radiance — red, green and blue of each, twenty-seven numbers — or `null` for the frame's own:
@@ -1986,6 +2003,7 @@ export declare class WebGL2Renderer implements RendererApi {
1986
2003
  * only — a glossy surface still reflects the probes — and it is pass state like the material,
1987
2004
  * which `bindMeshPass` clears, so a mirror or a probe bake drawn after does not inherit it.
1988
2005
  */
2006
+ setAmbientSH(coefficients: ArrayLike<number> | null): void;
1989
2007
  /**
1990
2008
  * How much of the environment the following draws mirror, 0 to 1.
1991
2009
  *
@@ -1996,7 +2014,6 @@ export declare class WebGL2Renderer implements RendererApi {
1996
2014
  * Defaults to zero and bindMeshPass restores it, so a scene that never calls this looks
1997
2015
  * exactly as it did.
1998
2016
  */
1999
- setAmbientSH(coefficients: ArrayLike<number> | null): void;
2000
2017
  setSurfaceReflectivity(amount: number): void;
2001
2018
  /**
2002
2019
  * How bright the environment the surfaces drawn next reflect is. 1 is the default and identity.
@@ -2026,6 +2043,18 @@ export declare class WebGL2Renderer implements RendererApi {
2026
2043
  * a coordinated release is happening anyway.
2027
2044
  */
2028
2045
  setEnvironmentAmbient(_amount: number): void;
2046
+ /**
2047
+ * A pass-level scale over the grain the geometry itself declared, 0 to 1.
2048
+ *
2049
+ * Which surfaces are mineral is now `MeshData.grain`, stated per vertex through
2050
+ * `MeshBuilder.setGrain` — because it is a property of a material, and absent means none.
2051
+ * This stays as the override for the case that has no answer to state: an imported model
2052
+ * arrives with no grain information at all, and a caller drawing one decides for it.
2053
+ *
2054
+ * Pass state, like `setSurfaceTexture`, because a material covers many draws. The default
2055
+ * is 1 and `bindMeshPass` restores it, so it scales geometry that declared grain and
2056
+ * leaves geometry that did not exactly as it is.
2057
+ */
2029
2058
  setSurfaceGrain(amount: number): void;
2030
2059
  /**
2031
2060
  * Whether the surfaces drawn next meet the medium: distance fog and the global medium's haze.
@@ -2361,6 +2390,13 @@ export declare class WebGL2Renderer implements RendererApi {
2361
2390
  * emitters; it is a material that knows what a particle is.
2362
2391
  */
2363
2392
  createParticles(capacity: number, options: ParticleBatchOptions): ParticleBatch;
2393
+ /**
2394
+ * Particles a caller's own compute wrote: WebGPU's alone, since this backend has no compute stage
2395
+ * to have written them (`computeSupported`). Said once, and nothing is drawn.
2396
+ */
2397
+ drawDeviceParticles(_batch: ParticleBatch, _particles: DeviceParticles, _camera: Camera, _env: Environment, _timeSeconds: number): void;
2398
+ /** Said once: see `drawDeviceParticles`. */
2399
+ private warnedDeviceParticles;
2364
2400
  /**
2365
2401
  * Draw a live particle pool.
2366
2402
  *