@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
@@ -2,6 +2,8 @@ import { layerSize, refuseArrayUpdate, sourceSize } from './textureSource.js';
2
2
  import { compressedLayers, planBlocks, refuseBlockUpdate } from './compressedSource.js';
3
3
  import { uploadCompressedArray } from './glCompressed.js';
4
4
  import { SURFACE_EFFECT_TEXELS, packSurfaceEffects } from './surfaceEffects.js';
5
+ import { isLightmapTexels } from './lightmap.js';
6
+ import { isSceneCaptureTexels } from './sceneCapture.js';
5
7
  const DEFAULT_ANISOTROPY = 4;
6
8
  /**
7
9
  * An image uploaded once and bound per draw.
@@ -29,7 +31,11 @@ export class SurfaceTexture {
29
31
  * where `compressed` — the formats this context samples — has theirs; see `compressedSource.ts`.
30
32
  */
31
33
  constructor(gl, source, options = {}, compressed = []) {
32
- const listed = (Array.isArray(source) ? source : [source]);
34
+ /* A scene capture's target: one empty layer the renderer draws into. See `sceneCapture.ts`. */
35
+ const capture = isSceneCaptureTexels(source) ? source : null;
36
+ /* A lightmap's page: two layers of bytes, uploaded as they are. See `lightmap.ts`. */
37
+ const baked = isLightmapTexels(source) ? source : null;
38
+ const listed = (baked !== null || capture !== null ? [] : Array.isArray(source) ? source : [source]);
33
39
  const blocks = compressedLayers(listed);
34
40
  this.srgb = (options.colorSpace ?? 'linear') === 'srgb';
35
41
  /* Refused before anything is allocated, so a refusal leaves no texture behind. */
@@ -40,7 +46,29 @@ export class SurfaceTexture {
40
46
  this.texture = texture;
41
47
  const wrap = (options.wrap ?? 'repeat') === 'repeat' ? gl.REPEAT : gl.CLAMP_TO_EDGE;
42
48
  gl.bindTexture(gl.TEXTURE_2D_ARRAY, texture);
43
- if (blocks !== null && plan !== null) {
49
+ if (capture !== null) {
50
+ /* Immutable storage, one level: a framebuffer attachment, and radiance read linear. */
51
+ this.layers = 1;
52
+ this.blockFormat = null;
53
+ this.mipmapped = false;
54
+ gl.texStorage3D(gl.TEXTURE_2D_ARRAY, 1, capture.float ? gl.RGBA16F : gl.RGBA8, capture.width, capture.height, 1);
55
+ }
56
+ else if (baked !== null) {
57
+ /* No chain: a page is read at its own resolution, and a level of it would blend regions. */
58
+ this.layers = baked.layers.length;
59
+ this.blockFormat = null;
60
+ this.mipmapped = false;
61
+ /* A typed array refuses either flag set, so both are put down here rather than assumed. */
62
+ gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
63
+ gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
64
+ /* Bytes, and read by `texelFetch` alone: the shader decodes and filters. See `lightmap.ts`. */
65
+ gl.texImage3D(gl.TEXTURE_2D_ARRAY, 0, gl.RGBA8, baked.width, baked.height, this.layers, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
66
+ /* A layer at a time, so a page's arrays are uploaded where they lie rather than joined. */
67
+ for (let layer = 0; layer < this.layers; layer++) {
68
+ gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, 0, 0, 0, layer, baked.width, baked.height, 1, gl.RGBA, gl.UNSIGNED_BYTE, baked.layers[layer]);
69
+ }
70
+ }
71
+ else if (blocks !== null && plan !== null) {
44
72
  /* The stored chain, or level 0 alone where no chain was asked for. Nothing is generated:
45
73
  `generateMipmap` cannot write a compressed format. */
46
74
  this.layers = blocks.length;
@@ -122,6 +150,13 @@ export class SurfaceTexture {
122
150
  gl.generateMipmap(gl.TEXTURE_2D_ARRAY);
123
151
  gl.bindTexture(gl.TEXTURE_2D_ARRAY, null);
124
152
  }
153
+ /**
154
+ * Attach the only layer as the bound framebuffer's colour: a scene capture's target, drawn into
155
+ * by `captureScene`. Engine-internal, as `bind` is.
156
+ */
157
+ attachColor(gl) {
158
+ gl.framebufferTextureLayer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, this.texture, 0, 0);
159
+ }
125
160
  /** Bind to a unit for sampling. Engine-internal: the renderer owns unit assignment. */
126
161
  bind(gl, unit) {
127
162
  gl.activeTexture(gl.TEXTURE0 + unit);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@driftengine/core",
3
- "version": "4.8.5",
3
+ "version": "4.8.7",
4
4
  "description": "The runtime of DriftEngine, a WebGPU game engine with a WebGL2 fallback: renderer, loop, input, cameras, scene graph and physics",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -29,8 +29,8 @@
29
29
  ],
30
30
  "sideEffects": false,
31
31
  "dependencies": {
32
- "@driftengine/drft": "4.8.5",
33
- "@driftengine/physics": "4.8.5",
32
+ "@driftengine/drft": "4.8.7",
33
+ "@driftengine/physics": "4.8.7",
34
34
  "gl-matrix": "^3.4.3"
35
35
  },
36
36
  "author": "Drift Technologies",
@@ -67,8 +67,8 @@
67
67
  "prepack": "node ../../scripts/build.mjs @driftengine/core"
68
68
  },
69
69
  "devDependencies": {
70
- "@driftengine/nav": "4.8.5",
71
- "@driftengine/network": "4.8.5",
72
- "@driftengine/texture": "4.8.5"
70
+ "@driftengine/nav": "4.8.7",
71
+ "@driftengine/network": "4.8.7",
72
+ "@driftengine/texture": "4.8.7"
73
73
  }
74
74
  }
package/src/index.ts CHANGED
@@ -454,6 +454,12 @@ export type { ReflectionOptions, ReflectiveSample } from './render/gi/reflection
454
454
  */
455
455
  export { GRADE_PLACEHOLDER_SIZE, MAX_GRADE_SIZE, identityGradeLut } from './render/colourGrade.ts';
456
456
  export type { ColourGradeLut } from './render/colourGrade.ts';
457
+ /**
458
+ * How bloom answers past its threshold — a ramp in place of the subtraction, and a tint for each
459
+ * of the pyramid's `BLOOM_LEVELS` octaves — the third argument of `setBloom`.
460
+ */
461
+ export { BLOOM_LEVELS } from './render/bloomChain.ts';
462
+ export type { BloomResponse } from './render/bloomChain.ts';
457
463
  export { probeDevice } from './render/backend/probe.ts';
458
464
  export type { ProbeVerdict } from './render/backend/probe.ts';
459
465
  /*
@@ -491,15 +497,28 @@ export type { ClothBindingData } from './render/clothBindingData.ts';
491
497
  * A frozen descriptor made by a factory and set as `SurfaceMaterial.model`; each is a pipeline of
492
498
  * its own, compiled the first time a draw asks.
493
499
  */
494
- export { anisotropicModel, eyeModel, hairModel, skinModel } from './render/surfaceModel.ts';
500
+ export {
501
+ anisotropicModel,
502
+ eyeModel,
503
+ hairModel,
504
+ lightmapModel,
505
+ skinModel,
506
+ } from './render/surfaceModel.ts';
495
507
  export type {
496
508
  AnisotropicModel,
497
509
  EyeModel,
498
510
  HairModel,
511
+ LightmapModel,
499
512
  SkinModel,
500
513
  SurfaceModel,
501
514
  SurfaceModelKind,
502
515
  } from './render/surfaceModel.ts';
516
+ /*
517
+ * A baked lightmap: the page a consumer decoded, which `createLightmap` uploads as a lightmapped
518
+ * material's `modelMap`, and the region of it a material or an instance reads.
519
+ */
520
+ export { WHOLE_PAGE } from './render/lightmap.ts';
521
+ export type { LightmapPage, LightmapRegion } from './render/lightmap.ts';
503
522
  /*
504
523
  * A skinned cloth solved where the renderer can: compute on WebGPU, the CPU under WebGL2. A function
505
524
  * rather than a renderer method so only a game that imports it carries the solver.
@@ -1046,8 +1065,12 @@ export {
1046
1065
  INSTANCE_FLOATS,
1047
1066
  INSTANCE_STRIDE,
1048
1067
  } from './render/instances.ts';
1049
- export { ParticlePool } from './render/particlePool.ts';
1050
- export type { ParticlePoolOptions, ParticleInstances } from './render/particlePool.ts';
1068
+ export { ParticlePool, DEVICE_PARTICLE_FLOATS } from './render/particlePool.ts';
1069
+ export type {
1070
+ DeviceParticles,
1071
+ ParticlePoolOptions,
1072
+ ParticleInstances,
1073
+ } from './render/particlePool.ts';
1051
1074
  export { ParticleBatch } from './render/particleBatch.ts';
1052
1075
  export type {
1053
1076
  ParticleBatchOptions,
@@ -0,0 +1,92 @@
1
+ /**
2
+ * A draw's own ambient light, as nine spherical-harmonic coefficients: what `setAmbientSH` takes,
3
+ * how both backends pack it, and the arithmetic the lit stage evaluates, which its tests hold.
4
+ *
5
+ * **Why a draw needs its own.** The engine's diffuse ambient is one answer for a whole frame — the
6
+ * sky-and-ground gradient, or the probe grid's irradiance — so everything standing in one place is
7
+ * lit alike. A stage with a baked volume of indirect light (Unreal's volumetric lightmap) lights a
8
+ * character by sampling that volume where the character stands; a consumer that samples it each
9
+ * frame hands the result here, around that character's draws, and the character takes the light of
10
+ * the spot it is in rather than the frame's average.
11
+ *
12
+ * **The coefficients are of incoming radiance**, `L(ω) ≈ Σ c_lm Y_lm(ω)`, on the standard real
13
+ * basis in its usual order — `Y00`, then `Y1,-1 Y1,0 Y1,1`, then `Y2,-2 Y2,-1 Y2,0 Y2,1 Y2,2` —
14
+ * evaluated on the engine's own world axes as written: `Y1,-1 = 0.488603 y`, `Y1,0 = 0.488603 z`,
15
+ * `Y1,1 = 0.488603 x`, `Y2,-2 = 1.092548 xy`, `Y2,-1 = 1.092548 yz`, `Y2,0 = 0.315392 (3z² − 1)`,
16
+ * `Y2,1 = 1.092548 xz`, `Y2,2 = 0.546274 (x² − y²)`, with y up. Three numbers a coefficient, red
17
+ * green blue, twenty-seven in all. A source in another engine's axes is rotated into these first.
18
+ *
19
+ * **What the surface receives is the cosine convolution**, Ramamoorthi and Hanrahan's: the bands
20
+ * scaled by π, 2π/3 and π/4 and divided by π, which is the irradiance over π a Lambertian surface
21
+ * multiplies its albedo by — the same quantity the gradient and the probes hand the shader. A
22
+ * constant radiance `L` everywhere is `c00 = L / 0.282095` and lights a surface exactly as an
23
+ * ambient of `L` does. Negative ringing, which a second-order fit of a sharp source has, is clamped
24
+ * to zero.
25
+ *
26
+ * **What it gives up** is the reflections: these replace the diffuse ambient only, and a glossy
27
+ * surface still reflects the frame's probes or sky. **What would make it wrong** is light the
28
+ * volume holds at a frequency nine coefficients cannot carry — a sharp sun patch — which is the
29
+ * stage's own direct light's job and not this.
30
+ */
31
+
32
+ /** Numbers `setAmbientSH` takes: nine coefficients of three channels. */
33
+ export const AMBIENT_SH_VALUES = 27;
34
+
35
+ /** Floats the lit stage's `uAmbientSH` holds: the coefficients, and whether they are on. */
36
+ export const AMBIENT_SH_FLOATS = 28;
37
+
38
+ /**
39
+ * The coefficients as the lit stage reads them, into `out` of `AMBIENT_SH_FLOATS`: the twenty-seven
40
+ * numbers in order and a last one that is 1, or every float zero for `null` — the frame's own
41
+ * ambient. Refuses a list that is not twenty-seven numbers, naming its length.
42
+ */
43
+ export function packAmbientSH(coefficients: ArrayLike<number> | null, out: Float32Array): void {
44
+ if (coefficients === null) {
45
+ out.fill(0);
46
+ return;
47
+ }
48
+ if (coefficients.length !== AMBIENT_SH_VALUES) {
49
+ throw new Error(
50
+ `setAmbientSH: ${coefficients.length} values, where nine coefficients of red, green and ` +
51
+ `blue are ${AMBIENT_SH_VALUES}`,
52
+ );
53
+ }
54
+ for (let i = 0; i < AMBIENT_SH_VALUES; i++) {
55
+ const value = coefficients[i] as number;
56
+ out[i] = Number.isFinite(value) ? value : 0;
57
+ }
58
+ out[AMBIENT_SH_VALUES] = 1;
59
+ }
60
+
61
+ /** The band factors of the cosine convolution, over π: 1, 2/3 and 1/4. */
62
+ const BAND = [1, 2 / 3, 1 / 4] as const;
63
+
64
+ /**
65
+ * The ambient a surface facing `nx, ny, nz` (unit) receives from these coefficients, into `out`
66
+ * (three floats): what `ambientHarmonics` in the lit stage computes. Run at no point by the engine;
67
+ * the reference its tests hold.
68
+ */
69
+ export function ambientFromSH(
70
+ coefficients: ArrayLike<number>,
71
+ nx: number,
72
+ ny: number,
73
+ nz: number,
74
+ out: Float32Array,
75
+ ): void {
76
+ const basis = [
77
+ 0.282095 * BAND[0],
78
+ 0.488603 * ny * BAND[1],
79
+ 0.488603 * nz * BAND[1],
80
+ 0.488603 * nx * BAND[1],
81
+ 1.092548 * nx * ny * BAND[2],
82
+ 1.092548 * ny * nz * BAND[2],
83
+ 0.315392 * (3 * nz * nz - 1) * BAND[2],
84
+ 1.092548 * nx * nz * BAND[2],
85
+ 0.546274 * (nx * nx - ny * ny) * BAND[2],
86
+ ];
87
+ for (let c = 0; c < 3; c++) {
88
+ let sum = 0;
89
+ for (let k = 0; k < 9; k++) sum += (coefficients[k * 3 + c] as number) * (basis[k] as number);
90
+ out[c] = Math.max(sum, 0);
91
+ }
92
+ }
@@ -39,3 +39,32 @@ export function anisotropicLobe(
39
39
  const d = x * x + y * y + nh * nh;
40
40
  return 1 / Math.max(d * d, 1e-8);
41
41
  }
42
+
43
+ /** Three components in the lobe's frame: along the direction, across it, along the normal. */
44
+ export type FrameVector = readonly [number, number, number];
45
+
46
+ /**
47
+ * The physical form of the same lobe, `π · D · Vis · N·L`, for a surface whose highlight is GGX's
48
+ * own (`SurfaceMaterial.physicalSpecular`): the stretched distribution normalised as a BRDF's, and
49
+ * Smith's height-correlated masking stretched with it (Heitz 2014, as Filament writes it), Fresnel
50
+ * left to the caller. The half vector, the light and the view are each in the lobe's frame. At
51
+ * strength 0 it is isotropic GGX, the term skin's and the eye's highlights are.
52
+ */
53
+ export function anisotropicPhysicalLobe(
54
+ half: FrameVector,
55
+ light: FrameVector,
56
+ view: FrameVector,
57
+ alphaT: number,
58
+ alphaB: number,
59
+ ): number {
60
+ const x = half[0] / alphaT;
61
+ const y = half[1] / alphaB;
62
+ const d = x * x + y * y + half[2] * half[2];
63
+ const distribution = 1 / (Math.PI * alphaT * alphaB * d * d);
64
+ const ndl = light[2];
65
+ const ndv = view[2];
66
+ const lambdaView = ndl * Math.hypot(alphaT * view[0], alphaB * view[1], ndv);
67
+ const lambdaLight = ndv * Math.hypot(alphaT * light[0], alphaB * light[1], ndl);
68
+ const visibility = 0.5 / Math.max(lambdaView + lambdaLight, 1e-5);
69
+ return Math.PI * distribution * visibility * ndl;
70
+ }
@@ -88,8 +88,36 @@ export interface AreaLightSource {
88
88
  * every caster, not just the fixture.
89
89
  */
90
90
  shadowNear?: number;
91
+ /**
92
+ * How far the rectangle's light reaches, in metres from its centre: past it the light is zero,
93
+ * windowed down to it, so a clustered rectangle can be binned into the froxels it reaches and no
94
+ * others. **Read only where the rectangle is shaded through the froxel table** — the ones past
95
+ * `maxAreaLights` with `clusteredLights` on; the fixed four light everything they face, as they
96
+ * always have. Absent, it is where the rectangle's irradiance on its axis falls to a thousandth
97
+ * of a scene unit, and never inside the rectangle itself. Unreal's `AttenuationRadius`, in metres.
98
+ */
99
+ range?: number;
100
+ /**
101
+ * Barn doors: a flap hinged at each of the rectangle's four edges, standing this many degrees from
102
+ * its normal. 90, or absent, folds them flat and they hide nothing; smaller closes them, so the
103
+ * light narrows to the opening they leave — a rectangle at 50° throws a beam rather than filling
104
+ * its hemisphere. Unreal's `BarnDoorAngle`, whose default of 88° is nearly flat.
105
+ *
106
+ * What a fragment sees past the doors is the part of the rectangle no door's tip hides, one axis
107
+ * at a time, and the rectangle then shades as that smaller rectangle, its highlight included.
108
+ * Its shadow map is still baked from the whole rectangle's centre. **What it gives up**: each
109
+ * door is taken as endless along its hinge, so the two axes clip apart and the corner where two
110
+ * doors meet hides a square rather than the mitre two real flaps make. One-sided: a two-sided
111
+ * rectangle's back face has none.
112
+ */
113
+ barnDoorAngle?: number;
114
+ /** The barn doors' length in metres. Absent, 0.2: Unreal's default of 20 cm. */
115
+ barnDoorLength?: number;
91
116
  }
92
117
 
118
+ /** The irradiance a derived `range` ends at, in scene units. See `AreaLightSource.range`. */
119
+ export const AREA_RANGE_CUTOFF = 1e-3;
120
+
93
121
  /** The five arrays the shader declares, filled once a frame from whatever a caller supplied. */
94
122
  export interface AreaLightBuffer {
95
123
  count: number;
@@ -99,6 +127,13 @@ export interface AreaLightBuffer {
99
127
  readonly up: Float32Array;
100
128
  readonly sizes: Float32Array;
101
129
  readonly twoSided: Float32Array;
130
+ /** One a rectangle: how far its light reaches. See `AreaLightSource.range`. */
131
+ readonly ranges?: Float32Array;
132
+ /**
133
+ * Two a rectangle: the cosine of its barn doors' angle from its normal, then their length in
134
+ * metres — 0 and 0 for none. See `AreaLightSource.barnDoorAngle`.
135
+ */
136
+ readonly barnDoors?: Float32Array;
102
137
  }
103
138
 
104
139
  export function createAreaLightBuffer(capacity: number = MAX_AREA_LIGHTS): AreaLightBuffer {
@@ -110,9 +145,64 @@ export function createAreaLightBuffer(capacity: number = MAX_AREA_LIGHTS): AreaL
110
145
  up: new Float32Array(capacity * 3),
111
146
  sizes: new Float32Array(capacity * 2),
112
147
  twoSided: new Float32Array(capacity),
148
+ ranges: new Float32Array(capacity),
149
+ barnDoors: new Float32Array(capacity * 2),
113
150
  };
114
151
  }
115
152
 
153
+ /** Unreal's default barn door length, in metres. See `AreaLightSource.barnDoorLength`. */
154
+ export const DEFAULT_BARN_DOOR_LENGTH = 0.2;
155
+
156
+ /**
157
+ * A rectangle's barn doors as the shader reads them, into `out[at]` and `out[at + 1]`: the cosine of
158
+ * their angle and their length. An angle not between 0 and 90 degrees, or a length not above zero,
159
+ * is no doors at all, written as 0 and 0 — which the shader reads as "nothing hidden".
160
+ */
161
+ export function barnDoorsOf(light: AreaLightSource, out: Float32Array, at: number): void {
162
+ const angle = light.barnDoorAngle;
163
+ const length = light.barnDoorLength ?? DEFAULT_BARN_DOOR_LENGTH;
164
+ if (angle === undefined || !(angle >= 0 && angle < 90) || !(length > 0)) {
165
+ out[at] = 0;
166
+ out[at + 1] = 0;
167
+ return;
168
+ }
169
+ out[at] = Math.cos((angle * Math.PI) / 180);
170
+ out[at + 1] = length;
171
+ }
172
+
173
+ /**
174
+ * The fixed arm's half extents and barn doors, four a rectangle as `uAreaLightSize` holds them: the
175
+ * two half extents, then the doors' cosine and length. Into a target the binder owns, for the first
176
+ * `MAX_AREA_LIGHTS` alone, which are all the fixed arm reads.
177
+ */
178
+ export function packAreaSizesAndDoors(lights: AreaLightBuffer, out: Float32Array): void {
179
+ const doors = lights.barnDoors;
180
+ for (let a = 0; a < MAX_AREA_LIGHTS; a++) {
181
+ out[a * 4] = lights.sizes[a * 2] ?? 0;
182
+ out[a * 4 + 1] = lights.sizes[a * 2 + 1] ?? 0;
183
+ out[a * 4 + 2] = doors?.[a * 2] ?? 0;
184
+ out[a * 4 + 3] = doors?.[a * 2 + 1] ?? 0;
185
+ }
186
+ }
187
+
188
+ /**
189
+ * How far a rectangle's light reaches when it names no range: where its irradiance on its own axis,
190
+ * `L · A / d²` for a rectangle of radiance `L` and area `A` seen from far, falls to
191
+ * `AREA_RANGE_CUTOFF`. Never less than the rectangle's own half-diagonal.
192
+ */
193
+ export function areaRange(
194
+ r: number,
195
+ g: number,
196
+ b: number,
197
+ halfWidth: number,
198
+ halfHeight: number,
199
+ ): number {
200
+ const radiance = Math.max(r, g, b, 0);
201
+ const area = 4 * halfWidth * halfHeight;
202
+ const diagonal = Math.hypot(halfWidth, halfHeight);
203
+ return Math.max(Math.sqrt((radiance * area) / AREA_RANGE_CUTOFF), diagonal);
204
+ }
205
+
116
206
  /**
117
207
  * Fill the buffer from a caller's list, normalising the axes and orthogonalising them.
118
208
  *
@@ -188,6 +278,13 @@ export function selectAreaLights(
188
278
  out.sizes[slot * 2] = Math.max(1e-4, light.halfWidth);
189
279
  out.sizes[slot * 2 + 1] = Math.max(1e-4, light.halfHeight);
190
280
  out.twoSided[slot] = light.twoSided === true ? 1 : 0;
281
+ if (out.ranges !== undefined) {
282
+ out.ranges[slot] =
283
+ light.range !== undefined && light.range > 0
284
+ ? light.range
285
+ : areaRange(light.r, light.g, light.b, light.halfWidth, light.halfHeight);
286
+ }
287
+ if (out.barnDoors !== undefined) barnDoorsOf(light, out.barnDoors, slot * 2);
191
288
  }
192
289
 
193
290
  return out;
@@ -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';
@@ -7,6 +8,8 @@ import type { Renderer, TranslucentMeshOptions } from './webgl2/renderer.ts';
7
8
  import type { InstancedOptions, MeshInstances } from '../instances.ts';
8
9
  import type { SurfaceMaterial } from '../surfaceTexture.ts';
9
10
  import type { SurfaceSource } from '../compressedSource.ts';
11
+ import type { LightmapPage } from '../lightmap.ts';
12
+ import type { DeviceParticles } from '../particlePool.ts';
10
13
  import type { ClothBindingData } from '../clothBindingData.ts';
11
14
  import type { TextStyle } from '../textLayout.ts';
12
15
  import type { SdfFont } from '../sdfFont.ts';
@@ -275,6 +278,7 @@ export type RendererApi = Omit<
275
278
  | 'disposeCaustics'
276
279
  | 'createParticles'
277
280
  | 'drawParticles'
281
+ | 'drawDeviceParticles'
278
282
  | 'disposeParticles'
279
283
  | 'createWindStreaks'
280
284
  | 'drawWindStreaks'
@@ -300,6 +304,9 @@ export type RendererApi = Omit<
300
304
  | 'disposeSdfText'
301
305
  | 'createSurfaceTexture'
302
306
  | 'createSurfaceTextureArray'
307
+ | 'createLightmap'
308
+ | 'createSceneCapture'
309
+ | 'captureScene'
303
310
  | 'updateSurfaceTexture'
304
311
  | 'disposeSurfaceTexture'
305
312
  | 'setSurfaceTexture'
@@ -361,6 +368,43 @@ export type RendererApi = Omit<
361
368
  options?: Parameters<Renderer['createSurfaceTexture']>[1],
362
369
  ): SurfaceTextureHandle;
363
370
 
371
+ /**
372
+ * A baked lightmap page on the device, as the texture a `lightmapModel` material takes for its
373
+ * `modelMap`: two layers of four bytes a texel, the irradiance as an rgb9e5 word and the
374
+ * direction, which the lit stage decodes and filters itself, clamped at the page's edges, with no
375
+ * mip chain. A page whose irradiance arrives packed uploads where it lies. Disposed as any texture is, with
376
+ * `disposeSurfaceTexture`; `updateSurfaceTexture` refuses it, as it refuses any array. See
377
+ * `lightmap.ts` for what a surface does with it.
378
+ */
379
+ createLightmap(page: LightmapPage): SurfaceTextureHandle;
380
+
381
+ /**
382
+ * A texture the scene can be drawn into from a camera of the caller's (`captureScene`), which a
383
+ * material then shows like any image — most often as its `emissive`, for a screen in the world
384
+ * showing what a camera elsewhere sees. `width` and `height` in pixels, 1 to 4096. It holds
385
+ * radiance, at the colour format a probe takes: half floats under `hdrScene`, bytes otherwise.
386
+ * Disposed as any texture is; `updateSurfaceTexture` refuses it. See `sceneCapture.ts`.
387
+ */
388
+ createSceneCapture(width: number, height: number): SurfaceTextureHandle;
389
+
390
+ /**
391
+ * Draw the scene into a capture from `camera`, at the moment a caller says, usually once a frame
392
+ * before the frame's own world. `draw` is handed the camera, its matrices updated for the
393
+ * capture's shape, and submits the scene exactly as it would to the screen — `bindMeshPass`,
394
+ * `drawMesh` and the rest; `bindMeshPass` the frame's own camera again afterwards. The output
395
+ * transform is held off, so a screen showing it is graded once, with the frame.
396
+ *
397
+ * **The mesh pass alone**, as a probe bake is: what a scene draws after its world is in the
398
+ * capture only if `draw` draws it, and no temporal resolve or effect after the world runs on it.
399
+ * Returns whether it drew: false for a texture that is not a capture.
400
+ */
401
+ captureScene(
402
+ capture: SurfaceTextureHandle,
403
+ camera: Camera,
404
+ clearColor: Vec3,
405
+ draw: (camera: Camera) => void,
406
+ ): boolean;
407
+
364
408
  /**
365
409
  * Replace a texture's pixels, keeping the GPU object and its sampler state.
366
410
  *
@@ -708,8 +752,10 @@ export type RendererApi = Omit<
708
752
  */
709
753
  createParticles(
710
754
  capacity: number,
711
- options: Omit<Parameters<Renderer['createParticles']>[1], 'reuse'> & {
755
+ options: Omit<Parameters<Renderer['createParticles']>[1], 'reuse' | 'texture'> & {
712
756
  readonly reuse?: ParticleHandle;
757
+ /** A `'sprite'`'s image. Re-typed to the handle for `reuse`'s reason. */
758
+ readonly texture?: SurfaceTextureHandle;
713
759
  },
714
760
  ): ParticleHandle;
715
761
 
@@ -722,6 +768,20 @@ export type RendererApi = Omit<
722
768
  timeSeconds: number,
723
769
  ): void;
724
770
 
771
+ /**
772
+ * Draw a pool's particles from a buffer a caller's own compute wrote (`registerCompute`), where
773
+ * they lie: the pool's material, blend, flipbook and fades, its instance stream the caller's
774
+ * buffer. WebGPU only; WebGL2 says so once and draws nothing. Not sorted. See `DeviceParticles`
775
+ * for the layout.
776
+ */
777
+ drawDeviceParticles(
778
+ batch: ParticleHandle,
779
+ particles: DeviceParticles,
780
+ camera: Parameters<Renderer['drawParticles']>[2],
781
+ env: Parameters<Renderer['drawParticles']>[3],
782
+ timeSeconds: number,
783
+ ): void;
784
+
725
785
  /** Release a particle pool. */
726
786
  disposeParticles(batch: ParticleHandle): void;
727
787
 
@@ -34,6 +34,8 @@ export class InstancedBatch {
34
34
  private readonly buffer: WebGLBuffer;
35
35
  private readonly vao: WebGLVertexArrayObject;
36
36
  private readonly staging: Float32Array;
37
+ /** The survivors of a lightmapped batch's cull, made the first time one is culled. */
38
+ private keptRegioned: MeshInstances | null = null;
37
39
  private readonly kept: {
38
40
  readonly buffer: WebGLBuffer;
39
41
  readonly vao: WebGLVertexArrayObject;
@@ -80,9 +82,17 @@ export class InstancedBatch {
80
82
  drawCulled(gl: WebGL2RenderingContext, data: MeshInstances, frustum: FrustumPlanes): number {
81
83
  const kept = this.kept;
82
84
  if (kept === null) return 0;
83
- const count = cullInstances(data, this.mesh.bounds, frustum, kept.instances);
85
+ /* A lightmapped batch's survivors keep their regions, in a copy that carries them. */
86
+ const into =
87
+ data.lightmapRegions === undefined
88
+ ? kept.instances
89
+ : (this.keptRegioned ??= {
90
+ ...createMeshInstances(this.capacity),
91
+ lightmapRegions: new Float32Array(this.capacity * 4),
92
+ });
93
+ const count = cullInstances(data, this.mesh.bounds, frustum, into);
84
94
  if (count === 0) return 0;
85
- packInstances(kept.instances, this.staging);
95
+ packInstances(into, this.staging);
86
96
  gl.bindBuffer(gl.ARRAY_BUFFER, kept.buffer);
87
97
  gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.staging, 0, count * INSTANCE_FLOATS);
88
98
  this.mesh.drawInstancesThrough(gl, kept.vao, count);