@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
@@ -84,7 +84,7 @@ export const MAX_CLUSTERED_LIGHTS = 320;
84
84
  * it cannot. That is what lets a light index the shadow uniforms by a number read from a texture,
85
85
  * and it is the same fact the octahedral array texture was built on.
86
86
  */
87
- export const LIGHT_TEXELS = 5;
87
+ export const LIGHT_TEXELS = 6;
88
88
  /**
89
89
  * Where each field of a light's record sits, as an offset in `uint`s from `lightBase`.
90
90
  *
@@ -114,7 +114,11 @@ export const LIGHT_TEXELS = 5;
114
114
  * **Reordered 2026-10-02 so a plain light's fields are the first eight**, which is what lets the
115
115
  * shader stop after two texels. Nine values are needed by every light and eight fit in two
116
116
  * texels, so the emitter size and the weight share slot 7 as two halves; see `packSizeAndWeight`.
117
- * Slot 19 is spare.
117
+ *
118
+ * **A sixth texel arrived in 4.8.7 for a light's own falloff exponent**, the fifth's last slot
119
+ * having gone to the channels: 6 KB more of a table near 300, read only for a light whose record
120
+ * carries a fixture, which a light with an exponent does. Slots 21 and 22 are a rectangle's barn
121
+ * doors and spare on a lamp; slot 23 is spare.
118
122
  */
119
123
  export const LIGHT_RECORD = {
120
124
  positionX: 0,
@@ -139,11 +143,43 @@ export const LIGHT_RECORD = {
139
143
  iesAxisX: 16,
140
144
  iesAxisY: 17,
141
145
  iesAxisZ: 18,
146
+ /**
147
+ * The light's lighting channels, a mask (`PointLightSet.lightChannels`), in the slot that was
148
+ * spare. A light naming channels other than the default is read as carrying a fixture, so its
149
+ * last texels are fetched; a plain light's are not, and the shader takes the default for it.
150
+ */
151
+ channels: 19,
152
+ /** The light's own falloff exponent, or 0 for the frame's falloff. `PointLightSource.falloffExponent`. */
153
+ falloffExponent: 20,
154
+ /** A rectangle's barn doors: the cosine of their angle from its normal, then their length in metres. */
155
+ barnDoorCos: 21,
156
+ barnDoorLength: 22,
142
157
  };
158
+ /** The lighting channel every light and every surface is on unless it says otherwise. */
159
+ export const DEFAULT_LIGHT_CHANNELS = 1;
160
+ /**
161
+ * A light's channels as the shader compares them: a whole mask from 1 to 255, or the default where
162
+ * none was named or what was named is not one. The froxel record and the uniform path both write
163
+ * this, so a light named badly is on channel 1 in both rather than on two different guesses.
164
+ */
165
+ export function lightChannelsOf(value) {
166
+ if (value === undefined || !Number.isInteger(value) || value < 1 || value > 255) {
167
+ return DEFAULT_LIGHT_CHANNELS;
168
+ }
169
+ return value;
170
+ }
171
+ /**
172
+ * A light's own falloff exponent as the shader reads it: a positive finite number, or 0 for the
173
+ * frame's falloff where none was named or what was named is not one. The record and the uniform
174
+ * path both write this, so a light named badly falls off by the frame's rule in both.
175
+ */
176
+ export function lightFalloffExponentOf(value) {
177
+ return value !== undefined && Number.isFinite(value) && value > 0 ? value : 0;
178
+ }
143
179
  /** Texels the lit pass reads for a light whose record carries no fixture. */
144
180
  export const PLAIN_LIGHT_TEXELS = 2;
145
181
  /**
146
- * The bit of `sizeAndWeight` that says the record's last three texels hold something: the sign of
182
+ * The bit of `sizeAndWeight` that says the record's last four texels hold something: the sign of
147
183
  * the emitter size's half, which a size never needs.
148
184
  */
149
185
  export const LIGHT_FIXTURE_FLAG = 0x8000;
@@ -354,8 +390,14 @@ export function createClusterLightSet() {
354
390
  * radius in its place, an emitter as wide as the light reaches, which widened every lamp's
355
391
  * highlight on a specular surface on the clustered arm alone.
356
392
  */
357
- export function fillClusterLightSet(env, out) {
393
+ export function fillClusterLightSet(env, out,
394
+ /** How many rectangles the fixed arm shades; the rest of the buffer is clustered. */
395
+ areaFrom = 0) {
358
396
  out.count = Math.min(env.lightCount ?? 0, MAX_CLUSTERED_LIGHTS);
397
+ const areas = env.areaLights ?? null;
398
+ out.areas = areas;
399
+ out.areaFrom = areaFrom;
400
+ out.areaCount = Math.max(0, Math.min((areas?.count ?? 0) - areaFrom, MAX_CLUSTERED_LIGHTS - out.count));
359
401
  out.positions = env.lightPositions;
360
402
  out.colors = env.lightColors;
361
403
  out.radii = env.lightRadii;
@@ -366,6 +408,8 @@ export function fillClusterLightSet(env, out) {
366
408
  out.iesProfiles = env.lightIesProfiles;
367
409
  out.iesAxes = env.lightIesAxes;
368
410
  out.cookies = env.lightCookies;
411
+ out.channels = env.lightChannels;
412
+ out.falloffExponents = env.lightFalloffExponents;
369
413
  return out;
370
414
  }
371
415
  /*
@@ -410,20 +454,25 @@ function distanceSqToBounds(x, y, z, b) {
410
454
  return dx * dx + dy * dy + dz * dz;
411
455
  }
412
456
  /**
413
- * Whether a light's record needs its last three texels: it holds a shadow slot, a cone, a profile
414
- * or a cookie. Everything else is a plain light, read in `PLAIN_LIGHT_TEXELS`.
457
+ * Whether a light's record needs its last four texels: it holds a shadow slot, a cone, a profile,
458
+ * a cookie, channels of its own or a falloff exponent of its own. Everything else is a plain light, read in `PLAIN_LIGHT_TEXELS`.
415
459
  *
416
460
  * **`shadowSlots` is zero where the lit shader has no point shadows**, and both binners are handed
417
461
  * it that way: a slot is only read under `POINT_SHADOWS`, so a light holding one in a build without
418
462
  * them was paying for three texels the shader then ignored.
419
463
  */
420
464
  export function lightHasFixture(lights, light, shadowSlots) {
465
+ /* A rectangle's record always carries its axes and extents past the second texel. */
466
+ if (light >= lights.count)
467
+ return true;
421
468
  const cone = lights.coneCos;
422
469
  return (light < shadowSlots ||
423
470
  (cone?.[light * 2] ?? POINT_LIGHT_COS_INNER) !== POINT_LIGHT_COS_INNER ||
424
471
  (cone?.[light * 2 + 1] ?? POINT_LIGHT_COS_OUTER) !== POINT_LIGHT_COS_OUTER ||
425
472
  (lights.iesProfiles?.[light] ?? NO_IES_PROFILE) >= 0 ||
426
- (lights.cookies?.[light] ?? NO_IES_PROFILE) >= 0);
473
+ (lights.cookies?.[light] ?? NO_IES_PROFILE) >= 0 ||
474
+ lightChannelsOf(lights.channels?.[light]) !== DEFAULT_LIGHT_CHANNELS ||
475
+ lightFalloffExponentOf(lights.falloffExponents?.[light]) > 0);
427
476
  }
428
477
  /**
429
478
  * The value of `uClustered` for a frame: 0 with clustering off, 1 when every light is plain, 2 when
@@ -459,6 +508,10 @@ export function clusteredMode(lights, count, shadowSlots) {
459
508
  * is a light without a map, which is what a scene with two hundred lamps has anyway.
460
509
  */
461
510
  export function writeLightRecord(lights, light, shadowSlots, out, at) {
511
+ if (light >= lights.count) {
512
+ writeAreaRecord(lights, light, out, at);
513
+ return;
514
+ }
462
515
  const directions = lights.directions;
463
516
  const cone = lights.coneCos;
464
517
  const axes = lights.iesAxes;
@@ -490,7 +543,72 @@ export function writeLightRecord(lights, light, shadowSlots, out, at) {
490
543
  out[at + LIGHT_RECORD.iesAxisX] = floatBits(axes?.[light * 3] ?? 0);
491
544
  out[at + LIGHT_RECORD.iesAxisY] = floatBits(axes?.[light * 3 + 1] ?? 0);
492
545
  out[at + LIGHT_RECORD.iesAxisZ] = floatBits(axes?.[light * 3 + 2] ?? 0);
493
- out[at + 19] = 0;
546
+ out[at + LIGHT_RECORD.channels] = floatBits(lightChannelsOf(lights.channels?.[light]));
547
+ out[at + LIGHT_RECORD.falloffExponent] = floatBits(lightFalloffExponentOf(lights.falloffExponents?.[light]));
548
+ out[at + LIGHT_RECORD.barnDoorCos] = 0;
549
+ out[at + LIGHT_RECORD.barnDoorLength] = 0;
550
+ out[at + LIGHT_RECORD.barnDoorLength + 1] = 0;
551
+ }
552
+ /**
553
+ * Which rectangle of the area buffer clustered light `light` is, for a light past the point
554
+ * lights. See `ClusterLightSet.areas`.
555
+ */
556
+ function areaSlot(lights, light) {
557
+ return (lights.areaFrom ?? 0) + light - lights.count;
558
+ }
559
+ /**
560
+ * A rectangle's record, which the shader tells from a lamp's by its **negative radius** — its
561
+ * reach, negated — and reads to its sixth texel: sidedness where a lamp keeps its shadow slot,
562
+ * the right axis where a spot keeps its direction, the half extents where its cone is, the up
563
+ * axis where the azimuth is, the channels, and the barn doors in the sixth. Never shadowed: a rectangle's occlusion is
564
+ * the fixed arm's, which is why the first `maxAreaLights` stay there.
565
+ */
566
+ function writeAreaRecord(lights, light, out, at) {
567
+ const areas = lights.areas;
568
+ const a = areaSlot(lights, light);
569
+ const pos = areas?.positions;
570
+ const color = areas?.colors;
571
+ const right = areas?.right;
572
+ const up = areas?.up;
573
+ const sizes = areas?.sizes;
574
+ out[at + LIGHT_RECORD.positionX] = floatBits(pos?.[a * 3] ?? 0);
575
+ out[at + LIGHT_RECORD.positionY] = floatBits(pos?.[a * 3 + 1] ?? 0);
576
+ out[at + LIGHT_RECORD.positionZ] = floatBits(pos?.[a * 3 + 2] ?? 0);
577
+ out[at + LIGHT_RECORD.radius] = floatBits(-areaReach(lights, light));
578
+ out[at + LIGHT_RECORD.colorR] = floatBits(color?.[a * 3] ?? 0);
579
+ out[at + LIGHT_RECORD.colorG] = floatBits(color?.[a * 3 + 1] ?? 0);
580
+ out[at + LIGHT_RECORD.colorB] = floatBits(color?.[a * 3 + 2] ?? 0);
581
+ out[at + LIGHT_RECORD.sizeAndWeight] = packSizeAndWeight(0, 1, true);
582
+ out[at + LIGHT_RECORD.shadowSlot] = floatBits(areas?.twoSided[a] ?? 0);
583
+ out[at + LIGHT_RECORD.directionX] = floatBits(right?.[a * 3] ?? 1);
584
+ out[at + LIGHT_RECORD.directionY] = floatBits(right?.[a * 3 + 1] ?? 0);
585
+ out[at + LIGHT_RECORD.directionZ] = floatBits(right?.[a * 3 + 2] ?? 0);
586
+ out[at + LIGHT_RECORD.cosInner] = floatBits(sizes?.[a * 2] ?? 0);
587
+ out[at + LIGHT_RECORD.cosOuter] = floatBits(sizes?.[a * 2 + 1] ?? 0);
588
+ out[at + LIGHT_RECORD.iesProfile] = floatBits(NO_IES_PROFILE);
589
+ out[at + LIGHT_RECORD.cookie] = floatBits(NO_IES_PROFILE);
590
+ out[at + LIGHT_RECORD.iesAxisX] = floatBits(up?.[a * 3] ?? 0);
591
+ out[at + LIGHT_RECORD.iesAxisY] = floatBits(up?.[a * 3 + 1] ?? 1);
592
+ out[at + LIGHT_RECORD.iesAxisZ] = floatBits(up?.[a * 3 + 2] ?? 0);
593
+ out[at + LIGHT_RECORD.channels] = floatBits(DEFAULT_LIGHT_CHANNELS);
594
+ out[at + LIGHT_RECORD.falloffExponent] = 0;
595
+ out[at + LIGHT_RECORD.barnDoorCos] = floatBits(areas?.barnDoors?.[a * 2] ?? 0);
596
+ out[at + LIGHT_RECORD.barnDoorLength] = floatBits(areas?.barnDoors?.[a * 2 + 1] ?? 0);
597
+ out[at + LIGHT_RECORD.barnDoorLength + 1] = 0;
598
+ }
599
+ /** A clustered rectangle's reach, which its record carries negated. See `AreaLightSource.range`. */
600
+ function areaReach(lights, light) {
601
+ const areas = lights.areas;
602
+ const a = areaSlot(lights, light);
603
+ const named = areas?.ranges?.[a] ?? 0;
604
+ if (named > 0)
605
+ return named;
606
+ const sizes = areas?.sizes;
607
+ return Math.hypot(sizes?.[a * 2] ?? 0, sizes?.[a * 2 + 1] ?? 0);
608
+ }
609
+ /** How many lights the table carries: the point lights and the clustered rectangles after them. */
610
+ export function clusteredLightTotal(lights) {
611
+ return Math.min(lights.count + (lights.areaCount ?? 0), MAX_CLUSTERED_LIGHTS);
494
612
  }
495
613
  /**
496
614
  * Fill the table with this frame's light assignment, and answer how many lights it carried.
@@ -538,7 +656,7 @@ export function writeLightRecord(lights, light, shadowSlots, out, at) {
538
656
  * Fills a caller-owned table and allocates nothing.
539
657
  */
540
658
  export function buildLightClusters(lights, view, near, far, tanHalfFovY, aspect, table, shadowSlots) {
541
- const count = Math.min(lights.count, MAX_CLUSTERED_LIGHTS);
659
+ const count = clusteredLightTotal(lights);
542
660
  /*
543
661
  * The whole cluster region, not just the counts.
544
662
  *
@@ -553,10 +671,14 @@ export function buildLightClusters(lights, view, near, far, tanHalfFovY, aspect,
553
671
  for (let k = 0; k <= CLUSTER_Z; k++)
554
672
  sliceDepths[k] = sliceNearDepth(k, near, far);
555
673
  for (let light = 0; light < count; light++) {
556
- const x = lights.positions[light * 3] ?? 0;
557
- const y = lights.positions[light * 3 + 1] ?? 0;
558
- const z = lights.positions[light * 3 + 2] ?? 0;
559
- const radius = lights.radii[light] ?? 0;
674
+ /* A rectangle past the lamps is placed by its centre and binned by its reach. */
675
+ const area = light >= lights.count;
676
+ const from = area ? (lights.areas?.positions ?? lights.positions) : lights.positions;
677
+ const slot = area ? areaSlot(lights, light) : light;
678
+ const x = from[slot * 3] ?? 0;
679
+ const y = from[slot * 3 + 1] ?? 0;
680
+ const z = from[slot * 3 + 2] ?? 0;
681
+ const radius = area ? areaReach(lights, light) : (lights.radii[light] ?? 0);
560
682
  /* The record, as the shader reads it. World space; see the note above. */
561
683
  writeLightRecord(lights, light, shadowSlots, table, lightBase(light));
562
684
  /* Column-major, as gl-matrix builds it. The view looks down -z, so depth is -vz. */
@@ -570,6 +692,39 @@ export function buildLightClusters(lights, view, near, far, tanHalfFovY, aspect,
570
692
  /* Entirely behind the camera, so no cluster can reach it. */
571
693
  if (depth + radius <= 0)
572
694
  continue;
695
+ /*
696
+ * **A spot is binned by its cone as well as its sphere.** A 10° spot reaching thirty metres
697
+ * touches every froxel its sphere does and lights a sliver of them, and a stage rigged with
698
+ * hundreds of such fixtures put every one of them in nearly every froxel near the floor: 313
699
+ * lights cost 11.7 ms of a 1600x900 frame. The cone is tested against each froxel's bounding
700
+ * sphere in view space, which is conservative, by the construction `coneBoxGap` describes.
701
+ */
702
+ const cosOuter = lights.coneCos?.[light * 2 + 1] ?? POINT_LIGHT_COS_OUTER;
703
+ const directions = lights.directions;
704
+ let coneCos = POINT_LIGHT_COS_OUTER;
705
+ let coneSin = 0;
706
+ let axisX = 0;
707
+ let axisY = 0;
708
+ let axisZ = 0;
709
+ if (!area && directions !== undefined && cosOuter > -1) {
710
+ const dx = directions[light * 3] ?? 0;
711
+ const dy = directions[light * 3 + 1] ?? 0;
712
+ const dz = directions[light * 3 + 2] ?? 0;
713
+ const ax = (view[0] ?? 0) * dx + (view[4] ?? 0) * dy + (view[8] ?? 0) * dz;
714
+ const ay = (view[1] ?? 0) * dx + (view[5] ?? 0) * dy + (view[9] ?? 0) * dz;
715
+ const az = -((view[2] ?? 0) * dx + (view[6] ?? 0) * dy + (view[10] ?? 0) * dz);
716
+ const length = Math.sqrt(ax * ax + ay * ay + az * az);
717
+ /* The shader measures the cone against the direction as given, so a long one is a wider
718
+ cone: `dot(L, d) >= c` is `dot(L, d / |d|) >= c / |d|`. */
719
+ if (length > 1e-6 && cosOuter / length > -1) {
720
+ coneCos = cosOuter / length;
721
+ coneSin = Math.sqrt(Math.max(0, 1 - coneCos * coneCos));
722
+ axisX = ax / length;
723
+ axisY = ay / length;
724
+ axisZ = az / length;
725
+ }
726
+ }
727
+ const spot = coneCos > -1;
573
728
  /*
574
729
  * The slices the sphere could possibly touch. The tiles are derived per slice, below.
575
730
  *
@@ -617,6 +772,10 @@ export function buildLightClusters(lights, view, near, far, tanHalfFovY, aspect,
617
772
  sliceBounds(i, j, zNear, zFar, halfWNear, halfWFar, halfHNear, halfHFar, bounds);
618
773
  if (distanceSqToBounds(vx, vy, depth, bounds) > radiusSq)
619
774
  continue;
775
+ if (spot &&
776
+ coneBoxGap(vx, vy, depth, axisX, axisY, axisZ, coneCos, coneSin, bounds) > 0) {
777
+ continue;
778
+ }
620
779
  insert(table, i + j * CLUSTER_X + k * CLUSTER_X * CLUSTER_Y, light, bounds);
621
780
  }
622
781
  }
@@ -624,6 +783,48 @@ export function buildLightClusters(lights, view, near, far, tanHalfFovY, aspect,
624
783
  }
625
784
  return count;
626
785
  }
786
+ /**
787
+ * How far a froxel lies outside a spot's cone, in metres, or a negative number where the cone
788
+ * reaches it: positive is a froxel the light cannot shade. Everything in view space with depth
789
+ * positive, as the binner holds it; `axis` is unit length and `cosA`, `sinA` are the outer cone's.
790
+ *
791
+ * **The froxel's bounding sphere against the infinite cone**, after Wronski's cone test for
792
+ * clustered spots. With `V` from the apex to the sphere's centre at an angle `θ` from the axis and
793
+ * `A` the cone's half-angle, the centre's distance from the cone is `|V| sin(θ − A)` — which is
794
+ * `cosA |V⊥| − (V·axis) sinA` — while `θ − A` is under a right angle, and `|V|` past it, where the
795
+ * nearest point of the cone is its apex. Inside the cone it is negative. So the gap is exact for
796
+ * the sphere, and the sphere holds the froxel: the test can only keep a light, never drop one the
797
+ * shader would draw. The apex case is what drops a froxel behind a spot whose sphere reaches it,
798
+ * which the formula alone underestimates.
799
+ *
800
+ * **What it gives up** is tightness at the near end of the frustum, where a froxel is a long thin
801
+ * box whose sphere is much larger than it: a spot grazing such a froxel is kept. **What would make
802
+ * it wrong** is a profile showing those near froxels hold the cost; testing the box's corners is
803
+ * the tight version, at eight dot products a pair.
804
+ *
805
+ * Exported so the conformance page can tell a disagreement at the cone's edge from a real one; the
806
+ * GPU binner writes the same arithmetic in WGSL.
807
+ */
808
+ export function coneBoxGap(px, py, pz, axisX, axisY, axisZ, cosA, sinA, box) {
809
+ const b0 = box[0] ?? 0;
810
+ const b1 = box[1] ?? 0;
811
+ const b2 = box[2] ?? 0;
812
+ const b3 = box[3] ?? 0;
813
+ const b4 = box[4] ?? 0;
814
+ const b5 = box[5] ?? 0;
815
+ const ex = (b3 - b0) * 0.5;
816
+ const ey = (b4 - b1) * 0.5;
817
+ const ez = (b5 - b2) * 0.5;
818
+ const vx = (b0 + b3) * 0.5 - px;
819
+ const vy = (b1 + b4) * 0.5 - py;
820
+ const vz = (b2 + b5) * 0.5 - pz;
821
+ const along = vx * axisX + vy * axisY + vz * axisZ;
822
+ const lengthSq = vx * vx + vy * vy + vz * vz;
823
+ const across = Math.sqrt(Math.max(lengthSq - along * along, 0));
824
+ const reach = Math.sqrt(lengthSq);
825
+ const distance = along < -sinA * reach ? reach : cosA * across - along * sinA;
826
+ return distance - Math.sqrt(ex * ex + ey * ey + ez * ez);
827
+ }
627
828
  /** An NDC coordinate to a tile index, clamped. */
628
829
  function tileOf(ndc, tiles) {
629
830
  const tile = Math.floor((ndc + 1) * 0.5 * tiles);
@@ -26,6 +26,8 @@ export interface FieldLight extends LightReach {
26
26
  readonly b: number;
27
27
  /** The emitter's physical radius, which widens its penumbra. */
28
28
  readonly sourceRadius: number;
29
+ /** Its own falloff exponent, overriding the frame's above zero. See `pointLightShape`. */
30
+ readonly falloffExponent?: number;
29
31
  }
30
32
  /**
31
33
  * Fill `light` and `direction`, `BRICK_TEXELS * 4` floats each, x fastest, for brick `brick`.
@@ -66,7 +66,7 @@ export function sampleFieldLight(px, py, pz, lights, candidates, first, last, fa
66
66
  const dist = Math.hypot(dx, dy, dz);
67
67
  if (dist >= source.radius)
68
68
  continue;
69
- const shape = pointLightShape(dist, source.radius, falloff);
69
+ const shape = pointLightShape(dist, source.radius, falloff, source.falloffExponent);
70
70
  if (shape <= 0)
71
71
  continue;
72
72
  const seen = distance === null
@@ -9,8 +9,10 @@
9
9
  *
10
10
  * Both of the renderer's falloffs: `smooth`, the default, is a linear ramp squared, reaching zero at
11
11
  * the radius; `inverseSquare` is physical, windowed so it still reaches zero at the radius, and
12
- * floored at ten centimetres so a surface touching a light is not infinitely bright.
12
+ * floored at ten centimetres so a surface touching a light is not infinitely bright. And a light's
13
+ * own, which overrides both: `(1 - (d/R)^2)^n` with no distance term, for an exponent `n` above zero
14
+ * (`PointLightSource.falloffExponent`).
13
15
  */
14
16
  export type PointLightFalloff = 'smooth' | 'inverseSquare';
15
17
  /** The shader's `shape` for an unshaped point light: no cone, no photometric profile. */
16
- export declare function pointLightShape(dist: number, radius: number, falloff: PointLightFalloff): number;
18
+ export declare function pointLightShape(dist: number, radius: number, falloff: PointLightFalloff, exponent?: number): number;
@@ -9,10 +9,16 @@
9
9
  *
10
10
  * Both of the renderer's falloffs: `smooth`, the default, is a linear ramp squared, reaching zero at
11
11
  * the radius; `inverseSquare` is physical, windowed so it still reaches zero at the radius, and
12
- * floored at ten centimetres so a surface touching a light is not infinitely bright.
12
+ * floored at ten centimetres so a surface touching a light is not infinitely bright. And a light's
13
+ * own, which overrides both: `(1 - (d/R)^2)^n` with no distance term, for an exponent `n` above zero
14
+ * (`PointLightSource.falloffExponent`).
13
15
  */
14
16
  /** The shader's `shape` for an unshaped point light: no cone, no photometric profile. */
15
- export function pointLightShape(dist, radius, falloff) {
17
+ export function pointLightShape(dist, radius, falloff, exponent = 0) {
18
+ if (exponent > 0) {
19
+ const reach = dist / Math.max(radius, 1e-4);
20
+ return Math.min(1, Math.max(0, 1 - reach * reach)) ** exponent;
21
+ }
16
22
  if (falloff === 'inverseSquare') {
17
23
  const ratio = dist / Math.max(radius, 1e-4);
18
24
  const window = Math.min(1, Math.max(0, 1 - ratio ** 4));
@@ -12,7 +12,7 @@
12
12
  * only ever reaches forward — to the next plane, and from the last to the repeat of the first — so
13
13
  * past its last row it meets the texture's edge and the clamp, never a cookie. A cookie's own
14
14
  * half-texel inset keeps it inside its band on both axes. The shader finds the bands from
15
- * `textureSize` and `uIesAtlasRows`, so the fold cost no uniform.
15
+ * `textureSize` and `uFixtureShape.x`, so the fold cost no uniform.
16
16
  *
17
17
  * **What it gives up**: every texel is four half floats where a profile needed one, a few kilobytes
18
18
  * a scene; and setting either table rebuilds the whole texture, which a consumer does once per set.
@@ -26,9 +26,10 @@ import type { PointLightSet } from '../lightBudget.ts';
26
26
  export declare const MAX_BOUNCE_LIGHTS = 32;
27
27
  /**
28
28
  * Floats a light takes: where it is and how far it reaches; its colour at its weight and the cosine
29
- * of its inner cone; the way it points and the cosine of its outer cone.
29
+ * of its inner cone; the way it points and the cosine of its outer cone; and its own falloff
30
+ * exponent, 0 for the frame's, in a fourth row of its own (`PointLightSource.falloffExponent`).
30
31
  */
31
- export declare const BOUNCE_LIGHT_FLOATS = 12;
32
+ export declare const BOUNCE_LIGHT_FLOATS = 16;
32
33
  export declare function createBounceLights(): Float32Array;
33
34
  /** Pack the lights of `lights` the bounce reads into `out`, and answer how many. */
34
35
  export declare function resolveBounceLights(lights: PointLightSet | null, out: Float32Array): number;
@@ -21,14 +21,15 @@
21
21
  * What it gives up against the frame: a photometric profile and a cookie, which the bounce reads as
22
22
  * a plain cone, and a light's specular, which a diffuse bounce has no use for.
23
23
  */
24
- import { POINT_LIGHT_COS_INNER, POINT_LIGHT_COS_OUTER } from '../clusteredLights.js';
24
+ import { POINT_LIGHT_COS_INNER, POINT_LIGHT_COS_OUTER, lightFalloffExponentOf, } from '../clusteredLights.js';
25
25
  /** Lights the probe bake shades a hit with. */
26
26
  export const MAX_BOUNCE_LIGHTS = 32;
27
27
  /**
28
28
  * Floats a light takes: where it is and how far it reaches; its colour at its weight and the cosine
29
- * of its inner cone; the way it points and the cosine of its outer cone.
29
+ * of its inner cone; the way it points and the cosine of its outer cone; and its own falloff
30
+ * exponent, 0 for the frame's, in a fourth row of its own (`PointLightSource.falloffExponent`).
30
31
  */
31
- export const BOUNCE_LIGHT_FLOATS = 12;
32
+ export const BOUNCE_LIGHT_FLOATS = 16;
32
33
  export function createBounceLights() {
33
34
  return new Float32Array(MAX_BOUNCE_LIGHTS * BOUNCE_LIGHT_FLOATS);
34
35
  }
@@ -60,6 +61,10 @@ export function resolveBounceLights(lights, out) {
60
61
  out[at + 9] = lights.lightDirections?.[i * 3 + 1] ?? 0;
61
62
  out[at + 10] = lights.lightDirections?.[i * 3 + 2] ?? 0;
62
63
  out[at + 11] = lights.lightConeCos?.[i * 2 + 1] ?? POINT_LIGHT_COS_OUTER;
64
+ out[at + 12] = lightFalloffExponentOf(lights.lightFalloffExponents?.[i]);
65
+ out[at + 13] = 0;
66
+ out[at + 14] = 0;
67
+ out[at + 15] = 0;
63
68
  count++;
64
69
  }
65
70
  return count;
@@ -44,6 +44,18 @@ export function cullInstances(data, local, frustum, out) {
44
44
  out.tints[kept * 3] = tints[i * 3];
45
45
  out.tints[kept * 3 + 1] = tints[i * 3 + 1];
46
46
  out.tints[kept * 3 + 2] = tints[i * 3 + 2];
47
+ if (out.alphas !== undefined)
48
+ out.alphas[kept] = data.alphas?.[i] ?? 1;
49
+ const regions = data.lightmapRegions;
50
+ if (regions !== undefined && out.lightmapRegions !== undefined) {
51
+ for (let k = 0; k < 4; k++)
52
+ out.lightmapRegions[kept * 4 + k] = regions[i * 4 + k];
53
+ }
54
+ const cells = data.uvRegions;
55
+ if (cells !== undefined && out.uvRegions !== undefined) {
56
+ for (let k = 0; k < 4; k++)
57
+ out.uvRegions[kept * 4 + k] = cells[i * 4 + k];
58
+ }
47
59
  kept++;
48
60
  }
49
61
  out.count = kept;
@@ -10,10 +10,11 @@
10
10
  * plant. This one carries a full transform, because a vehicle pitches and rolls on its suspension
11
11
  * and a yaw cannot say so.
12
12
  *
13
- * **What it gives up** is a matrix per instance where a scatter spends five floats: 76 bytes
13
+ * **What it gives up** is a matrix per instance where a scatter spends five floats: 80 bytes
14
14
  * against 44, and no wind. **What would make it wrong** is a caller wanting per-instance
15
- * anything else — an opacity, a morph weight — which wants another attribute, and the sixteen
16
- * WebGL2 guarantees are already spent.
15
+ * anything beyond an opacity and a texture cell — a morph weight, say — which wants another
16
+ * attribute, and the sixteen WebGL2 guarantees are already spent. The opacity rides the float the
17
+ * stride was padded with and the cell the matrix's bottom row, which is why neither costs a byte.
17
18
  */
18
19
  export interface MeshInstances {
19
20
  /**
@@ -30,6 +31,40 @@ export interface MeshInstances {
30
31
  * — the alternative is a second batch per colour, which is the thing this exists to avoid.
31
32
  */
32
33
  readonly tints: Float32Array;
34
+ /**
35
+ * One float each, the instance's opacity from 0 to 1, which a blended draw
36
+ * (`drawTranslucentInstanced`) multiplies into the batch's own. **Absent, or an instance past its
37
+ * end, is 1** — wholly opaque, which is what every instance was before 4.8.6.
38
+ *
39
+ * What lets one draw carry particles fading at different rates: without it a consumer drew a
40
+ * batch per opacity level, each a draw and a material of its own. It is the instance's twin of
41
+ * the per-vertex alpha lane (`MeshData.channel`), multiplied in where that lane is.
42
+ */
43
+ readonly alphas?: Float32Array;
44
+ /**
45
+ * Four floats each, the instance's region of its lightmap page: `[scaleU, scaleV, biasU, biasV]`,
46
+ * read by a draw whose material is a `lightmapModel` — instances of one mesh in one material sit
47
+ * in different regions of one page, each applied before the material's own region. **They ride
48
+ * the tint and the opacity**, which a lightmapped instance gives up (see `lightmap.ts`): where
49
+ * these are given, those are not read. **A lightmapped batch needs them**, since a bake is
50
+ * different light at each instance; one drawn without them reads its tints as its regions, and
51
+ * either mismatch is said once on the console.
52
+ */
53
+ readonly lightmapRegions?: Float32Array;
54
+ /**
55
+ * Four floats each, the instance's cell of its texture: `[scaleU, scaleV, offsetU, offsetV]`,
56
+ * applied to the mesh's coordinates before the material's own scale and offset, in every pass
57
+ * that reads them — so one draw carries particles in different cells of a flipbook, or props
58
+ * wearing different tiles of one atlas. **Absent, or past `count`, is the whole texture**,
59
+ * `[1, 1, 0, 0]`, which is what every instance read before 4.8.7.
60
+ *
61
+ * **It rides the matrix's bottom row**, which an instance's placement — a turn, a scale and a
62
+ * move — leaves at `[0, 0, 0, 1]`, and which every stage drawing an instance rebuilds as that.
63
+ * So a placement must be affine, as every placement of a solid object is; a projective one is
64
+ * drawn as its affine part. A culled batch (`cullInstances`) carries the cells only into a
65
+ * target that has the array.
66
+ */
67
+ readonly uvRegions?: Float32Array;
33
68
  /** How many instances the arrays hold. Fixed at creation; the buffers are sized from it. */
34
69
  readonly capacity: number;
35
70
  /** How many are live. The rest of the buffer is neither uploaded nor drawn. */
@@ -44,20 +79,21 @@ export interface MeshInstances {
44
79
  */
45
80
  export declare function createMeshInstances(capacity: number): MeshInstances;
46
81
  /**
47
- * Floats one instance occupies in the interleaved vertex buffer: sixteen of matrix, three of
48
- * tint, and one of padding.
82
+ * Floats one instance occupies in the interleaved vertex buffer: sixteen of matrix, the bottom row
83
+ * of which carries the texture cell, three of tint, and the opacity.
49
84
  *
50
- * **Padded to twenty so the stride is 80 bytes rather than 76.** A vertex buffer's stride must be
51
- * a multiple of four on both backends, which 76 already is — the padding is for the *attribute*
52
- * offsets: the tint sits at byte 64 and a three-float attribute ending at 76 leaves the next
53
- * instance's first column starting there, which is legal but puts every second instance on an
54
- * offset no driver aligns well. One wasted float an instance is 4 bytes against 76.
85
+ * **Twenty so the stride is 80 bytes rather than 76.** A vertex buffer's stride must be a multiple
86
+ * of four on both backends, which 76 already is — the twentieth float was padding for the
87
+ * *attribute* offsets: the tint sits at byte 64 and a three-float attribute ending at 76 leaves the
88
+ * next instance's first column starting there, which is legal but puts every second instance on an
89
+ * offset no driver aligns well. Since 4.8.6 it carries the instance's opacity, read with the tint
90
+ * as one four-float attribute, so the opacity cost no attribute and no byte.
55
91
  */
56
92
  export declare const INSTANCE_FLOATS = 20;
57
93
  /** Bytes one instance occupies. See `INSTANCE_FLOATS`. */
58
94
  export declare const INSTANCE_STRIDE: number;
59
95
  /**
60
- * Interleave `count` instances into `out`, matrix then tint, at `INSTANCE_FLOATS` apart.
96
+ * Interleave `count` instances into `out`, matrix, tint and opacity, at `INSTANCE_FLOATS` apart.
61
97
  *
62
98
  * **Interleaved rather than two buffers** so a batch's upload is one `writeBuffer` and an
63
99
  * instance's colour sits beside the matrix that places it. Shared by both backends, because
@@ -9,25 +9,27 @@ export function createMeshInstances(capacity) {
9
9
  return {
10
10
  models: new Float32Array(capacity * 16),
11
11
  tints: new Float32Array(capacity * 3),
12
+ alphas: new Float32Array(capacity).fill(1),
12
13
  capacity,
13
14
  count: 0,
14
15
  };
15
16
  }
16
17
  /**
17
- * Floats one instance occupies in the interleaved vertex buffer: sixteen of matrix, three of
18
- * tint, and one of padding.
18
+ * Floats one instance occupies in the interleaved vertex buffer: sixteen of matrix, the bottom row
19
+ * of which carries the texture cell, three of tint, and the opacity.
19
20
  *
20
- * **Padded to twenty so the stride is 80 bytes rather than 76.** A vertex buffer's stride must be
21
- * a multiple of four on both backends, which 76 already is — the padding is for the *attribute*
22
- * offsets: the tint sits at byte 64 and a three-float attribute ending at 76 leaves the next
23
- * instance's first column starting there, which is legal but puts every second instance on an
24
- * offset no driver aligns well. One wasted float an instance is 4 bytes against 76.
21
+ * **Twenty so the stride is 80 bytes rather than 76.** A vertex buffer's stride must be a multiple
22
+ * of four on both backends, which 76 already is — the twentieth float was padding for the
23
+ * *attribute* offsets: the tint sits at byte 64 and a three-float attribute ending at 76 leaves the
24
+ * next instance's first column starting there, which is legal but puts every second instance on an
25
+ * offset no driver aligns well. Since 4.8.6 it carries the instance's opacity, read with the tint
26
+ * as one four-float attribute, so the opacity cost no attribute and no byte.
25
27
  */
26
28
  export const INSTANCE_FLOATS = 20;
27
29
  /** Bytes one instance occupies. See `INSTANCE_FLOATS`. */
28
30
  export const INSTANCE_STRIDE = INSTANCE_FLOATS * 4;
29
31
  /**
30
- * Interleave `count` instances into `out`, matrix then tint, at `INSTANCE_FLOATS` apart.
32
+ * Interleave `count` instances into `out`, matrix, tint and opacity, at `INSTANCE_FLOATS` apart.
31
33
  *
32
34
  * **Interleaved rather than two buffers** so a batch's upload is one `writeBuffer` and an
33
35
  * instance's colour sits beside the matrix that places it. Shared by both backends, because
@@ -37,16 +39,29 @@ export const INSTANCE_STRIDE = INSTANCE_FLOATS * 4;
37
39
  * Writes nothing beyond `count`, and allocates nothing: `out` is the caller's staging array.
38
40
  */
39
41
  export function packInstances(instances, out) {
40
- const { models, tints } = instances;
42
+ const { models, tints, alphas, lightmapRegions, uvRegions } = instances;
41
43
  const count = Math.min(instances.count, instances.capacity);
42
44
  for (let i = 0; i < count; i += 1) {
43
45
  const at = i * INSTANCE_FLOATS;
44
46
  const m = i * 16;
45
47
  for (let c = 0; c < 16; c += 1)
46
48
  out[at + c] = models[m + c];
49
+ /* The texture cell in the bottom row an affine placement leaves free. See `uvRegions`. */
50
+ const cell = i * 4;
51
+ out[at + 3] = uvRegions?.[cell] ?? 1;
52
+ out[at + 7] = uvRegions?.[cell + 1] ?? 1;
53
+ out[at + 11] = uvRegions?.[cell + 2] ?? 0;
54
+ out[at + 15] = uvRegions?.[cell + 3] ?? 0;
55
+ if (lightmapRegions !== undefined) {
56
+ /* The region in the tint's and the opacity's place. See `lightmapRegions`. */
57
+ for (let c = 0; c < 4; c += 1)
58
+ out[at + 16 + c] = lightmapRegions[i * 4 + c];
59
+ continue;
60
+ }
47
61
  const t = i * 3;
48
62
  out[at + 16] = tints[t];
49
63
  out[at + 17] = tints[t + 1];
50
64
  out[at + 18] = tints[t + 2];
65
+ out[at + 19] = alphas?.[i] ?? 1;
51
66
  }
52
67
  }