@driftengine/core 4.5.0 → 4.6.1

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 (209) hide show
  1. package/CHANGELOG.json +62 -0
  2. package/README.md +1 -1
  3. package/dist/geometry/boxCsg.d.ts +28 -0
  4. package/dist/geometry/boxCsg.js +139 -0
  5. package/dist/geometry/csg.d.ts +36 -0
  6. package/dist/geometry/csg.js +329 -0
  7. package/dist/geometry/meshBuilder.js +13 -0
  8. package/dist/geometry/meshConcat.js +1 -0
  9. package/dist/geometry/pixelFont.js +2 -0
  10. package/dist/geometry/solid.d.ts +61 -0
  11. package/dist/geometry/solid.js +152 -0
  12. package/dist/geometry/solidFlat.d.ts +35 -0
  13. package/dist/geometry/solidFlat.js +428 -0
  14. package/dist/geometry/solidHarness.d.ts +19 -0
  15. package/dist/geometry/solidHarness.js +66 -0
  16. package/dist/geometry/solidNormals.d.ts +17 -0
  17. package/dist/geometry/solidNormals.js +86 -0
  18. package/dist/geometry/solidRound.d.ts +41 -0
  19. package/dist/geometry/solidRound.js +323 -0
  20. package/dist/geometry/solidWriter.d.ts +16 -0
  21. package/dist/geometry/solidWriter.js +42 -0
  22. package/dist/index.d.ts +23 -2
  23. package/dist/index.js +13 -1
  24. package/dist/render/backend/api.d.ts +17 -3
  25. package/dist/render/backend/drawFog.d.ts +26 -0
  26. package/dist/render/backend/drawFog.js +30 -0
  27. package/dist/render/backend/materialChanges.d.ts +2 -1
  28. package/dist/render/backend/materialChanges.js +6 -1
  29. package/dist/render/backend/webgl2/instanced.d.ts +24 -5
  30. package/dist/render/backend/webgl2/instanced.js +72 -15
  31. package/dist/render/backend/webgl2/renderer.d.ts +122 -8
  32. package/dist/render/backend/webgl2/renderer.js +249 -128
  33. package/dist/render/backend/webgpu/buffers.d.ts +17 -0
  34. package/dist/render/backend/webgpu/buffers.js +34 -3
  35. package/dist/render/backend/webgpu/depthPass.js +5 -1
  36. package/dist/render/backend/webgpu/drawCommand.d.ts +5 -0
  37. package/dist/render/backend/webgpu/drawCommand.js +1 -0
  38. package/dist/render/backend/webgpu/flatPass.d.ts +4 -1
  39. package/dist/render/backend/webgpu/flatPass.js +38 -9
  40. package/dist/render/backend/webgpu/glassTintPass.js +2 -1
  41. package/dist/render/backend/webgpu/instanceCullPass.d.ts +34 -0
  42. package/dist/render/backend/webgpu/instanceCullPass.js +144 -0
  43. package/dist/render/backend/webgpu/instanced.d.ts +11 -1
  44. package/dist/render/backend/webgpu/instanced.js +21 -3
  45. package/dist/render/backend/webgpu/latePass.d.ts +6 -2
  46. package/dist/render/backend/webgpu/latePass.js +11 -4
  47. package/dist/render/backend/webgpu/motionPass.js +2 -1
  48. package/dist/render/backend/webgpu/postPass.d.ts +1 -1
  49. package/dist/render/backend/webgpu/renderer.d.ts +100 -10
  50. package/dist/render/backend/webgpu/renderer.js +478 -214
  51. package/dist/render/backend/webgpu/sdfTextPass.js +6 -1
  52. package/dist/render/backend/webgpu/shaders/instanceCull.wgsl.d.ts +18 -0
  53. package/dist/render/backend/webgpu/shaders/instanceCull.wgsl.js +59 -0
  54. package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +17 -3
  55. package/dist/render/backend/webgpu/surfaceTexturePass.js +77 -38
  56. package/dist/render/driftLight/bake.d.ts +7 -0
  57. package/dist/render/driftLight/bake.js +61 -56
  58. package/dist/render/driftLight/denseField.d.ts +46 -0
  59. package/dist/render/driftLight/denseField.js +109 -0
  60. package/dist/render/driftLight/lightField.d.ts +10 -5
  61. package/dist/render/driftLight/lightField.js +25 -19
  62. package/dist/render/driftLight/presence.d.ts +50 -0
  63. package/dist/render/driftLight/presence.js +43 -0
  64. package/dist/render/driftLight/uniforms.d.ts +3 -3
  65. package/dist/render/driftLight/uniforms.js +5 -6
  66. package/dist/render/driftLight/worldLightField.d.ts +41 -0
  67. package/dist/render/driftLight/worldLightField.js +66 -0
  68. package/dist/render/fixtureAtlas.d.ts +20 -0
  69. package/dist/render/fixtureAtlas.js +49 -0
  70. package/dist/render/instanceCull.d.ts +63 -0
  71. package/dist/render/instanceCull.js +160 -0
  72. package/dist/render/instances.d.ts +15 -0
  73. package/dist/render/lightBudget.d.ts +22 -33
  74. package/dist/render/lightBudget.js +24 -35
  75. package/dist/render/lightGrid.d.ts +42 -0
  76. package/dist/render/lightGrid.js +87 -0
  77. package/dist/render/mesh.d.ts +23 -24
  78. package/dist/render/mesh.js +56 -68
  79. package/dist/render/occlusion.d.ts +10 -0
  80. package/dist/render/occlusion.js +36 -7
  81. package/dist/render/pointLightSelection.d.ts +7 -0
  82. package/dist/render/pointLightSelection.js +29 -4
  83. package/dist/render/rainField.d.ts +13 -0
  84. package/dist/render/rainField.js +27 -5
  85. package/dist/render/recon/lateDepth.d.ts +11 -0
  86. package/dist/render/recon/lateDepth.js +70 -0
  87. package/dist/render/recon/resolve.d.ts +34 -4
  88. package/dist/render/recon/resolve.js +161 -20
  89. package/dist/render/rendererHarness.js +4 -0
  90. package/dist/render/sceneTarget.d.ts +2 -0
  91. package/dist/render/sceneTarget.js +2 -0
  92. package/dist/render/shaders/depth.js +12 -9
  93. package/dist/render/shaders/flat/driftLight.d.ts +1 -1
  94. package/dist/render/shaders/flat/driftLight.js +32 -7
  95. package/dist/render/shaders/flat/index.js +5 -3
  96. package/dist/render/shaders/flat/main.d.ts +1 -1
  97. package/dist/render/shaders/flat/main.js +75 -23
  98. package/dist/render/shaders/flat/preamble.js +75 -20
  99. package/dist/render/shaders/flat/surfaceEffects.d.ts +29 -0
  100. package/dist/render/shaders/flat/surfaceEffects.js +152 -0
  101. package/dist/render/shaders/generated/depth.wgsl.d.ts +8 -8
  102. package/dist/render/shaders/generated/depth.wgsl.js +8 -8
  103. package/dist/render/shaders/generated/flat.wgsl.d.ts +1320 -1320
  104. package/dist/render/shaders/generated/flat.wgsl.js +1359 -1345
  105. package/dist/render/shaders/generated/lightVolume.wgsl.js +18 -4
  106. package/dist/render/shaders/generated/rush.wgsl.d.ts +21 -16
  107. package/dist/render/shaders/generated/rush.wgsl.js +21 -16
  108. package/dist/render/shaders/generated/sdfText.wgsl.d.ts +2 -2
  109. package/dist/render/shaders/generated/sdfText.wgsl.js +2 -2
  110. package/dist/render/shaders/generated/text.wgsl.d.ts +1 -1
  111. package/dist/render/shaders/generated/text.wgsl.js +1 -1
  112. package/dist/render/shaders/gi/driftLight.wgsl.d.ts +1 -1
  113. package/dist/render/shaders/gi/driftLight.wgsl.js +27 -4
  114. package/dist/render/shaders/recon/lateDepth.wgsl.js +38 -13
  115. package/dist/render/shaders/recon/resolve.wgsl.d.ts +8 -7
  116. package/dist/render/shaders/recon/resolve.wgsl.js +161 -27
  117. package/dist/render/shaders/rush.js +11 -1
  118. package/dist/render/shaders/sdfText.d.ts +1 -1
  119. package/dist/render/shaders/sdfText.js +3 -2
  120. package/dist/render/shaders/text.d.ts +1 -1
  121. package/dist/render/shaders/text.js +18 -9
  122. package/dist/render/surfaceEffects.d.ts +83 -0
  123. package/dist/render/surfaceEffects.js +75 -0
  124. package/dist/render/surfaceScene.d.ts +8 -0
  125. package/dist/render/surfaceScene.js +8 -0
  126. package/dist/render/surfaceTexture.d.ts +18 -2
  127. package/dist/render/surfaceTexture.js +68 -15
  128. package/dist/render/temporalAa.d.ts +25 -6
  129. package/dist/render/temporalAa.js +45 -15
  130. package/dist/render/textureSource.d.ts +30 -0
  131. package/dist/render/textureSource.js +53 -0
  132. package/dist/render/vertexDefaults.js +2 -0
  133. package/dist/world/hlod.d.ts +104 -0
  134. package/dist/world/hlod.js +214 -0
  135. package/package.json +6 -6
  136. package/src/geometry/boxCsg.ts +186 -0
  137. package/src/geometry/csg.ts +384 -0
  138. package/src/geometry/meshBuilder.ts +15 -0
  139. package/src/geometry/meshConcat.ts +1 -0
  140. package/src/geometry/pixelFont.ts +2 -0
  141. package/src/geometry/solid.ts +189 -0
  142. package/src/geometry/solidFlat.ts +522 -0
  143. package/src/geometry/solidHarness.ts +83 -0
  144. package/src/geometry/solidNormals.ts +103 -0
  145. package/src/geometry/solidRound.ts +443 -0
  146. package/src/geometry/solidWriter.ts +58 -0
  147. package/src/index.ts +51 -1
  148. package/src/render/backend/api.ts +21 -2
  149. package/src/render/backend/drawFog.ts +31 -0
  150. package/src/render/backend/materialChanges.ts +9 -2
  151. package/src/render/backend/webgl2/instanced.ts +80 -14
  152. package/src/render/backend/webgl2/renderer.ts +311 -150
  153. package/src/render/backend/webgpu/buffers.ts +49 -3
  154. package/src/render/backend/webgpu/depthPass.ts +5 -1
  155. package/src/render/backend/webgpu/drawCommand.ts +6 -0
  156. package/src/render/backend/webgpu/flatPass.ts +41 -11
  157. package/src/render/backend/webgpu/glassTintPass.ts +2 -1
  158. package/src/render/backend/webgpu/instanceCullPass.ts +165 -0
  159. package/src/render/backend/webgpu/instanced.ts +21 -3
  160. package/src/render/backend/webgpu/latePass.ts +12 -3
  161. package/src/render/backend/webgpu/motionPass.ts +2 -1
  162. package/src/render/backend/webgpu/renderer.ts +556 -239
  163. package/src/render/backend/webgpu/sdfTextPass.ts +6 -1
  164. package/src/render/backend/webgpu/shaders/instanceCull.wgsl.ts +61 -0
  165. package/src/render/backend/webgpu/surfaceTexturePass.ts +90 -50
  166. package/src/render/driftLight/bake.ts +85 -62
  167. package/src/render/driftLight/denseField.ts +149 -0
  168. package/src/render/driftLight/lightField.ts +33 -18
  169. package/src/render/driftLight/presence.ts +69 -0
  170. package/src/render/driftLight/uniforms.ts +8 -9
  171. package/src/render/driftLight/worldLightField.ts +83 -0
  172. package/src/render/fixtureAtlas.ts +63 -0
  173. package/src/render/instanceCull.ts +205 -0
  174. package/src/render/instances.ts +16 -0
  175. package/src/render/lightBudget.ts +25 -36
  176. package/src/render/lightGrid.ts +130 -0
  177. package/src/render/mesh.ts +64 -72
  178. package/src/render/occlusion.ts +51 -7
  179. package/src/render/pointLightSelection.ts +72 -4
  180. package/src/render/rainField.ts +39 -5
  181. package/src/render/recon/lateDepth.ts +83 -0
  182. package/src/render/recon/resolve.ts +206 -20
  183. package/src/render/rendererHarness.ts +4 -0
  184. package/src/render/sceneTarget.ts +4 -0
  185. package/src/render/shaders/depth.ts +12 -9
  186. package/src/render/shaders/flat/driftLight.ts +32 -7
  187. package/src/render/shaders/flat/index.ts +5 -3
  188. package/src/render/shaders/flat/main.ts +75 -23
  189. package/src/render/shaders/flat/preamble.ts +75 -20
  190. package/src/render/shaders/flat/surfaceEffects.ts +152 -0
  191. package/src/render/shaders/generated/depth.wgsl.ts +8 -8
  192. package/src/render/shaders/generated/flat.wgsl.ts +1361 -1345
  193. package/src/render/shaders/generated/lightVolume.wgsl.ts +20 -4
  194. package/src/render/shaders/generated/rush.wgsl.ts +21 -16
  195. package/src/render/shaders/generated/sdfText.wgsl.ts +2 -2
  196. package/src/render/shaders/generated/text.wgsl.ts +1 -1
  197. package/src/render/shaders/gi/driftLight.wgsl.ts +27 -4
  198. package/src/render/shaders/recon/lateDepth.wgsl.ts +38 -13
  199. package/src/render/shaders/recon/resolve.wgsl.ts +161 -27
  200. package/src/render/shaders/rush.ts +11 -1
  201. package/src/render/shaders/sdfText.ts +3 -2
  202. package/src/render/shaders/text.ts +18 -9
  203. package/src/render/surfaceEffects.ts +132 -0
  204. package/src/render/surfaceScene.ts +16 -0
  205. package/src/render/surfaceTexture.ts +110 -19
  206. package/src/render/temporalAa.ts +78 -14
  207. package/src/render/textureSource.ts +72 -0
  208. package/src/render/vertexDefaults.ts +2 -0
  209. package/src/world/hlod.ts +290 -0
@@ -0,0 +1,160 @@
1
+ import { boxInFrustum } from '../math/frustum.js';
2
+ /** How many planes a frustum holds, four floats each. */
3
+ const PLANES = 6;
4
+ /**
5
+ * Indices a culling batch must draw, all its instances together, before culling it instance by
6
+ * instance pays; below it the batch is drawn whole once its box is seen (`batchBoxVisible`).
7
+ *
8
+ * **What a per-instance cull costs is fixed a batch, and what it saves grows with the batch.** On
9
+ * WebGPU it is a dispatch and an indirect draw, and Chrome validates every indirect draw in its GPU
10
+ * process — measured at 8 µs apiece in a city frame whose six hundred small prop batches took one
11
+ * each, 52 fps against 70 with those draws gone. On WebGL2 it is a loop over the instances and an
12
+ * upload of the survivors. A few thousand indices cost a device less than either.
13
+ *
14
+ * What it gives up is the instances of a small batch that stand out of view once its box is in it,
15
+ * which the device transforms and clips for nothing. What would make it wrong is a device whose
16
+ * vertex stage limits the frame while the CPU idles — where this wants to come down.
17
+ */
18
+ export const INSTANCE_CULL_MIN_INDICES = 1 << 14;
19
+ /**
20
+ * Whether a culling batch of `count` instances of a mesh of `indexCount` indices is culled instance
21
+ * by instance, or drawn whole once its box is seen. See `INSTANCE_CULL_MIN_INDICES`.
22
+ */
23
+ export function cullsInstances(count, indexCount) {
24
+ return count * indexCount >= INSTANCE_CULL_MIN_INDICES;
25
+ }
26
+ /**
27
+ * Keep the instances of `data` whose spheres meet `frustum`, compacted in order into `out`, and
28
+ * return how many. `out` must hold as many as `data` does; its count is set.
29
+ */
30
+ export function cullInstances(data, local, frustum, out) {
31
+ const count = Math.min(data.count, data.capacity, out.capacity);
32
+ const models = data.models;
33
+ const tints = data.tints;
34
+ let kept = 0;
35
+ for (let i = 0; i < count; i++) {
36
+ sphereOf(models, i, local, scratch);
37
+ if (!sphereMeets(frustum, scratch))
38
+ continue;
39
+ /* Copied float by float: a subarray would be a view allocated per instance, per frame. */
40
+ const from = i * 16;
41
+ const to = kept * 16;
42
+ for (let k = 0; k < 16; k++)
43
+ out.models[to + k] = models[from + k];
44
+ out.tints[kept * 3] = tints[i * 3];
45
+ out.tints[kept * 3 + 1] = tints[i * 3 + 1];
46
+ out.tints[kept * 3 + 2] = tints[i * 3 + 2];
47
+ kept++;
48
+ }
49
+ out.count = kept;
50
+ return kept;
51
+ }
52
+ /** `sphereInFrustum` over a packed (x, y, z, radius): kept unless wholly beyond one plane. */
53
+ function sphereMeets(frustum, sphere) {
54
+ const x = sphere[0];
55
+ const y = sphere[1];
56
+ const z = sphere[2];
57
+ const radius = sphere[3];
58
+ for (let plane = 0; plane < PLANES; plane++) {
59
+ const at = plane * 4;
60
+ const distance = frustum[at] * x +
61
+ frustum[at + 1] * y +
62
+ frustum[at + 2] * z +
63
+ frustum[at + 3];
64
+ if (distance < -radius)
65
+ return false;
66
+ }
67
+ return true;
68
+ }
69
+ /**
70
+ * The world box around every instance's sphere, for testing a whole batch at once: (minX, minY,
71
+ * minZ, maxX, maxY, maxZ) into `out`. A batch holding nothing writes a min above its max.
72
+ *
73
+ * **A box, not a sphere, and the difference is the whole point for a flat batch.** A region of props
74
+ * a hundred metres square has a sphere seventy metres deep underground, where nothing occludes it,
75
+ * so a sphere test never finds a region hidden. The box stays as flat as the props are.
76
+ */
77
+ export function instancesBox(data, local, out) {
78
+ const count = Math.min(data.count, data.capacity);
79
+ if (count === 0) {
80
+ out[0] = 1;
81
+ out[1] = 1;
82
+ out[2] = 1;
83
+ out[3] = -1;
84
+ out[4] = -1;
85
+ out[5] = -1;
86
+ return;
87
+ }
88
+ let minX = Infinity;
89
+ let minY = Infinity;
90
+ let minZ = Infinity;
91
+ let maxX = -Infinity;
92
+ let maxY = -Infinity;
93
+ let maxZ = -Infinity;
94
+ for (let i = 0; i < count; i++) {
95
+ sphereOf(data.models, i, local, scratch);
96
+ const r = scratch[3];
97
+ minX = Math.min(minX, scratch[0] - r);
98
+ minY = Math.min(minY, scratch[1] - r);
99
+ minZ = Math.min(minZ, scratch[2] - r);
100
+ maxX = Math.max(maxX, scratch[0] + r);
101
+ maxY = Math.max(maxY, scratch[1] + r);
102
+ maxZ = Math.max(maxZ, scratch[2] + r);
103
+ }
104
+ out[0] = minX;
105
+ out[1] = minY;
106
+ out[2] = minZ;
107
+ out[3] = maxX;
108
+ out[4] = maxY;
109
+ out[5] = maxZ;
110
+ }
111
+ const scratch = new Float32Array(4);
112
+ /** Instance `i`'s world sphere into `out` as (x, y, z, radius). */
113
+ function sphereOf(models, i, local, out) {
114
+ const m = i * 16;
115
+ const cx = local.centre[0] ?? 0;
116
+ const cy = local.centre[1] ?? 0;
117
+ const cz = local.centre[2] ?? 0;
118
+ const m0 = models[m];
119
+ const m1 = models[m + 1];
120
+ const m2 = models[m + 2];
121
+ const m4 = models[m + 4];
122
+ const m5 = models[m + 5];
123
+ const m6 = models[m + 6];
124
+ const m8 = models[m + 8];
125
+ const m9 = models[m + 9];
126
+ const m10 = models[m + 10];
127
+ out[0] = m0 * cx + m4 * cy + m8 * cz + models[m + 12];
128
+ out[1] = m1 * cx + m5 * cy + m9 * cz + models[m + 13];
129
+ out[2] = m2 * cx + m6 * cy + m10 * cz + models[m + 14];
130
+ out[3] =
131
+ local.radius *
132
+ Math.sqrt(Math.max(m0 * m0 + m1 * m1 + m2 * m2, m4 * m4 + m5 * m5 + m6 * m6, m8 * m8 + m9 * m9 + m10 * m10));
133
+ }
134
+ /**
135
+ * Whether a batch's box (`instancesBox`) can be seen at all: inside the frustum, and not behind the
136
+ * declared occluders. One test for a whole region's props, run before any instance is looked at.
137
+ */
138
+ export function batchBoxVisible(box, frustum, occluder) {
139
+ const minX = box[0];
140
+ const minY = box[1];
141
+ const minZ = box[2];
142
+ const maxX = box[3];
143
+ const maxY = box[4];
144
+ const maxZ = box[5];
145
+ if (minX > maxX)
146
+ return false;
147
+ if (!boxInFrustum(frustum, minX, minY, minZ, maxX, maxY, maxZ))
148
+ return false;
149
+ if (occluder === null)
150
+ return true;
151
+ boxMin[0] = minX;
152
+ boxMin[1] = minY;
153
+ boxMin[2] = minZ;
154
+ boxMax[0] = maxX;
155
+ boxMax[1] = maxY;
156
+ boxMax[2] = maxZ;
157
+ return !occluder.occludedBox(boxMin, boxMax);
158
+ }
159
+ const boxMin = new Float32Array(3);
160
+ const boxMax = new Float32Array(3);
@@ -67,3 +67,18 @@ export declare const INSTANCE_STRIDE: number;
67
67
  * Writes nothing beyond `count`, and allocates nothing: `out` is the caller's staging array.
68
68
  */
69
69
  export declare function packInstances(instances: MeshInstances, out: Float32Array): void;
70
+ /** How a batch made by `createInstanced` behaves. */
71
+ export interface InstancedOptions {
72
+ /**
73
+ * Cull the camera draw: the whole batch skipped when its instances' sphere is out of view or behind
74
+ * the declared occluders, and otherwise each instance kept only where its sphere meets the view —
75
+ * on the CPU on WebGL2, as compute into an indirect draw on WebGPU — once the batch is large enough
76
+ * to repay that (`cullsInstances`); a smaller one is drawn whole. Shadow, mirror-free motion and
77
+ * blended draws still see every instance. Off by default, because a batch small enough to draw whole
78
+ * pays for nothing it would save.
79
+ *
80
+ * **For a batch spread over space** — a prop across a region, a crowd, traffic. A batch in one
81
+ * place gains only the whole-batch test, which it could as well do itself.
82
+ */
83
+ readonly cull?: boolean;
84
+ }
@@ -131,22 +131,20 @@ export declare const ENVIRONMENT_TEXTURE_UNIT: number;
131
131
  */
132
132
  export declare const CLUSTER_TABLE_TEXTURE_UNIT: number;
133
133
  /**
134
- * Where the photometric atlas binds: one row per IES profile, any number of them.
135
- *
136
- * **One unit for every profile a world holds, which is the whole reason it is an atlas.** A
137
- * fixture's distribution is a curve rather than an image, so a row of 128 floats holds one and a
138
- * texture holds as many as a consumer loads. Per-profile textures would have spent the remaining
139
- * units on the third fixture.
140
- *
141
- * **Spent unconditionally, for the same reason the froxel table is**, and the argument is at that
142
- * declaration: the lookup is a branch on a value rather than a permutation, so the sampler is
143
- * declared in every profile and a declared sampler needs a complete texture bound whether or not
144
- * the branch reads it. With no profile loaded that texture is a single row of ones, which is the
145
- * multiplicative identity — a row of zeros would switch off every light that reached it.
146
- *
147
- * The tenth of sixteen. `CAPABILITIES.md` §3 keeps the running count.
134
+ * Where the fixture atlas binds: a fixture's spot cookies and its photometric profiles, one texture.
135
+ *
136
+ * **Two tables in one binding since 2026-09-30**, which is the fold `textureUnitBudget.test.ts` asks
137
+ * the next sampler for: the widest lit shader bound fifteen of the sixteen fragment samplers WebGL2
138
+ * guarantees, and the cookie atlas and the photometric atlas were two small float tables read with
139
+ * `textureLod`, both 128 texels across. See `fixtureAtlas.ts` for the layout and why cookies sit on
140
+ * top.
141
+ *
142
+ * **Spent unconditionally, for the reason the froxel table is**: both lookups are branches on values
143
+ * rather than permutations, so the sampler is declared in every profile and a declared sampler needs
144
+ * a complete texture. With nothing loaded that is a single row of ones — the multiplicative
145
+ * identity, since a row of zeros would switch off every light that reached it.
148
146
  */
149
- export declare const IES_ATLAS_TEXTURE_UNIT: number;
147
+ export declare const FIXTURE_ATLAS_TEXTURE_UNIT: number;
150
148
  /**
151
149
  * The joint palette, read by the **vertex** stage.
152
150
  *
@@ -173,21 +171,6 @@ export declare const SKIN_PALETTE_TEXTURE_UNIT: number;
173
171
  * of sixteen spoken for, three free.
174
172
  */
175
173
  export declare const MORPH_DELTA_TEXTURE_UNIT: number;
176
- /**
177
- * Where the spot-light cookie atlas binds: one row of square tiles, one tile a cookie.
178
- *
179
- * **The fourteenth of sixteen, and two are left.** `CAPABILITIES.md` §3 keeps the running count —
180
- * and had been three units stale for as long as the IES atlas, the skin palette and the morph
181
- * deltas have existed, which is why the number is derived in `textureUnitBudget.test.ts` and only
182
- * quoted in prose.
183
- *
184
- * **Spent unconditionally, for the reason the froxel table and the IES atlas are**: a declared
185
- * sampler needs a complete texture bound whether or not the branch reads it, so a one-texel white
186
- * placeholder stands in it when no consumer has loaded a cookie. White rather than black, because
187
- * it multiplies a light's colour and a black stand-in would switch off every light that reached
188
- * it — the identity `packIesAtlas` chooses for the same reason.
189
- */
190
- export declare const COOKIE_ATLAS_TEXTURE_UNIT: number;
191
174
  /**
192
175
  * The colour the frame had already drawn, for a refracting surface to sample.
193
176
  *
@@ -219,11 +202,17 @@ export declare const DRIFT_LIGHT_ATLAS_TEXTURE_UNIT: number;
219
202
  */
220
203
  export declare const SUN_GLASS_TINT_TEXTURE_UNIT: number;
221
204
  /**
222
- * What each lamp keeps through glass (`pointGlassTint.ts`), the lit program's fifteenth fragment
223
- * sampler and the last one that leaves a sixteenth spare. Unit sixteen of the combined pool, which
224
- * WebGL2 guarantees thirty-two of; what a stage may declare is the sixteen, and this is fifteen.
205
+ * What each lamp keeps through glass (`pointGlassTint.ts`), a fragment sampler of the lit program.
206
+ * A unit of the combined pool, which WebGL2 guarantees thirty-two of; what a stage may declare is
207
+ * sixteen, which `textureUnitBudget.test.ts` counts from the shader source.
225
208
  */
226
209
  export declare const POINT_GLASS_TINT_TEXTURE_UNIT: number;
210
+ /**
211
+ * Each surface layer's effects table (`surfaceEffects.ts`), bound beside the albedo array it
212
+ * describes. **Paid for by folding**: the cookie and photometric atlases became one fixture atlas
213
+ * to free the sampler this takes, so the lit stage still keeps one of its sixteen spare.
214
+ */
215
+ export declare const SURFACE_EFFECTS_TEXTURE_UNIT: number;
227
216
  /**
228
217
  * A cookie's tile, in texels a side.
229
218
  *
@@ -129,22 +129,20 @@ export const ENVIRONMENT_TEXTURE_UNIT = EMISSIVE_TEXTURE_UNIT + 1;
129
129
  */
130
130
  export const CLUSTER_TABLE_TEXTURE_UNIT = ENVIRONMENT_TEXTURE_UNIT + 1;
131
131
  /**
132
- * Where the photometric atlas binds: one row per IES profile, any number of them.
133
- *
134
- * **One unit for every profile a world holds, which is the whole reason it is an atlas.** A
135
- * fixture's distribution is a curve rather than an image, so a row of 128 floats holds one and a
136
- * texture holds as many as a consumer loads. Per-profile textures would have spent the remaining
137
- * units on the third fixture.
138
- *
139
- * **Spent unconditionally, for the same reason the froxel table is**, and the argument is at that
140
- * declaration: the lookup is a branch on a value rather than a permutation, so the sampler is
141
- * declared in every profile and a declared sampler needs a complete texture bound whether or not
142
- * the branch reads it. With no profile loaded that texture is a single row of ones, which is the
143
- * multiplicative identity — a row of zeros would switch off every light that reached it.
144
- *
145
- * The tenth of sixteen. `CAPABILITIES.md` §3 keeps the running count.
132
+ * Where the fixture atlas binds: a fixture's spot cookies and its photometric profiles, one texture.
133
+ *
134
+ * **Two tables in one binding since 2026-09-30**, which is the fold `textureUnitBudget.test.ts` asks
135
+ * the next sampler for: the widest lit shader bound fifteen of the sixteen fragment samplers WebGL2
136
+ * guarantees, and the cookie atlas and the photometric atlas were two small float tables read with
137
+ * `textureLod`, both 128 texels across. See `fixtureAtlas.ts` for the layout and why cookies sit on
138
+ * top.
139
+ *
140
+ * **Spent unconditionally, for the reason the froxel table is**: both lookups are branches on values
141
+ * rather than permutations, so the sampler is declared in every profile and a declared sampler needs
142
+ * a complete texture. With nothing loaded that is a single row of ones — the multiplicative
143
+ * identity, since a row of zeros would switch off every light that reached it.
146
144
  */
147
- export const IES_ATLAS_TEXTURE_UNIT = CLUSTER_TABLE_TEXTURE_UNIT + 1;
145
+ export const FIXTURE_ATLAS_TEXTURE_UNIT = CLUSTER_TABLE_TEXTURE_UNIT + 1;
148
146
  /**
149
147
  * The joint palette, read by the **vertex** stage.
150
148
  *
@@ -162,7 +160,7 @@ export const IES_ATLAS_TEXTURE_UNIT = CLUSTER_TABLE_TEXTURE_UNIT + 1;
162
160
  * What it costs is one of the five units left after the IES atlas. What would make it wrong is
163
161
  * the lit pass growing past sixteen, which `textureUnitBudget.test.ts` asserts against.
164
162
  */
165
- export const SKIN_PALETTE_TEXTURE_UNIT = IES_ATLAS_TEXTURE_UNIT + 1;
163
+ export const SKIN_PALETTE_TEXTURE_UNIT = FIXTURE_ATLAS_TEXTURE_UNIT + 1;
166
164
  /**
167
165
  * A mesh's morph deltas, also read by the vertex stage, and also out of this same sixteen.
168
166
  *
@@ -171,21 +169,6 @@ export const SKIN_PALETTE_TEXTURE_UNIT = IES_ATLAS_TEXTURE_UNIT + 1;
171
169
  * of sixteen spoken for, three free.
172
170
  */
173
171
  export const MORPH_DELTA_TEXTURE_UNIT = SKIN_PALETTE_TEXTURE_UNIT + 1;
174
- /**
175
- * Where the spot-light cookie atlas binds: one row of square tiles, one tile a cookie.
176
- *
177
- * **The fourteenth of sixteen, and two are left.** `CAPABILITIES.md` §3 keeps the running count —
178
- * and had been three units stale for as long as the IES atlas, the skin palette and the morph
179
- * deltas have existed, which is why the number is derived in `textureUnitBudget.test.ts` and only
180
- * quoted in prose.
181
- *
182
- * **Spent unconditionally, for the reason the froxel table and the IES atlas are**: a declared
183
- * sampler needs a complete texture bound whether or not the branch reads it, so a one-texel white
184
- * placeholder stands in it when no consumer has loaded a cookie. White rather than black, because
185
- * it multiplies a light's colour and a black stand-in would switch off every light that reached
186
- * it — the identity `packIesAtlas` chooses for the same reason.
187
- */
188
- export const COOKIE_ATLAS_TEXTURE_UNIT = MORPH_DELTA_TEXTURE_UNIT + 1;
189
172
  /**
190
173
  * The colour the frame had already drawn, for a refracting surface to sample.
191
174
  *
@@ -199,7 +182,7 @@ export const COOKIE_ATLAS_TEXTURE_UNIT = MORPH_DELTA_TEXTURE_UNIT + 1;
199
182
  * cubemaps became one array, which is what freed the units this one is spending.
200
183
  * `textureUnitBudget.test.ts` carries the phone that budget was written for.
201
184
  */
202
- export const REFRACT_SCENE_TEXTURE_UNIT = COOKIE_ATLAS_TEXTURE_UNIT + 1;
185
+ export const REFRACT_SCENE_TEXTURE_UNIT = MORPH_DELTA_TEXTURE_UNIT + 1;
203
186
  /**
204
187
  * DriftLight's brick index and atlas, the lit program's fourteenth and fifteenth fragment samplers.
205
188
  *
@@ -217,11 +200,17 @@ export const DRIFT_LIGHT_ATLAS_TEXTURE_UNIT = DRIFT_LIGHT_INDEX_TEXTURE_UNIT + 1
217
200
  */
218
201
  export const SUN_GLASS_TINT_TEXTURE_UNIT = DRIFT_LIGHT_ATLAS_TEXTURE_UNIT + 1;
219
202
  /**
220
- * What each lamp keeps through glass (`pointGlassTint.ts`), the lit program's fifteenth fragment
221
- * sampler and the last one that leaves a sixteenth spare. Unit sixteen of the combined pool, which
222
- * WebGL2 guarantees thirty-two of; what a stage may declare is the sixteen, and this is fifteen.
203
+ * What each lamp keeps through glass (`pointGlassTint.ts`), a fragment sampler of the lit program.
204
+ * A unit of the combined pool, which WebGL2 guarantees thirty-two of; what a stage may declare is
205
+ * sixteen, which `textureUnitBudget.test.ts` counts from the shader source.
223
206
  */
224
207
  export const POINT_GLASS_TINT_TEXTURE_UNIT = SUN_GLASS_TINT_TEXTURE_UNIT + 1;
208
+ /**
209
+ * Each surface layer's effects table (`surfaceEffects.ts`), bound beside the albedo array it
210
+ * describes. **Paid for by folding**: the cookie and photometric atlases became one fixture atlas
211
+ * to free the sampler this takes, so the lit stage still keeps one of its sixteen spare.
212
+ */
213
+ export const SURFACE_EFFECTS_TEXTURE_UNIT = POINT_GLASS_TINT_TEXTURE_UNIT + 1;
225
214
  /**
226
215
  * A cookie's tile, in texels a side.
227
216
  *
@@ -0,0 +1,42 @@
1
+ /**
2
+ * A world's fixed lights bucketed once, so the frame's exact choice looks only at the lights near
3
+ * the eye rather than at every one of them.
4
+ *
5
+ * **The choice must be the full scan's, exactly, and needs no sort to be.** Both of the choice's
6
+ * lists break a tie in distance by source index, so the order the lights are met in cannot reach the
7
+ * result: what reaches it is the full scan's candidates minus only lights no query could keep —
8
+ * every light whose cell lies within `viewRange + maxRadius` of the eye is gathered, and the
9
+ * selection's own `viewRange + radius` test does the rest.
10
+ *
11
+ * **Cost stated**: built once, from where the lights stood. A light that moves or changes its reach
12
+ * must be re-bucketed by the caller — this is for the city's fixed thousands, and a scene's handful
13
+ * of movers stays with the plain scan.
14
+ *
15
+ * Nothing here allocates after construction; the gather writes into the grid's own scratch.
16
+ */
17
+ import type { PointLightSource } from './pointLightSelection.ts';
18
+ /** The most cells a grid may span, which keeps a stray light far away from costing a gigabyte. */
19
+ export declare const LIGHT_GRID_MAX_CELLS: number;
20
+ export interface LightGrid {
21
+ readonly sources: readonly PointLightSource[];
22
+ /** Metres a cell spans on each axis. */
23
+ readonly cell: number;
24
+ /** The world position of cell (0, 0, 0)'s corner. */
25
+ readonly origin: readonly [number, number, number];
26
+ readonly dims: readonly [number, number, number];
27
+ /** Where cell `c`'s lights start in `lights`; `start[c + 1]` is where they end. */
28
+ readonly start: Uint32Array;
29
+ /** Source indices by cell, ascending within each. */
30
+ readonly lights: Uint32Array;
31
+ /** The largest reach of any source, which widens every query. */
32
+ readonly maxRadius: number;
33
+ /** The last gather, cell by cell: `gathered[0..count)`. */
34
+ readonly gathered: Int32Array;
35
+ }
36
+ /** Bucket `sources` into cells of `cell` metres. Built once; not a frame path. */
37
+ export declare function createLightGrid(sources: readonly PointLightSource[], cell: number): LightGrid;
38
+ /**
39
+ * Gather into `grid.gathered`, cell by cell, every light whose cell lies within `reach` metres of
40
+ * (x, y, z) on each axis, and answer how many.
41
+ */
42
+ export declare function gatherLights(grid: LightGrid, x: number, y: number, z: number, reach: number): number;
@@ -0,0 +1,87 @@
1
+ /** The most cells a grid may span, which keeps a stray light far away from costing a gigabyte. */
2
+ export const LIGHT_GRID_MAX_CELLS = 1 << 22;
3
+ /** Bucket `sources` into cells of `cell` metres. Built once; not a frame path. */
4
+ export function createLightGrid(sources, cell) {
5
+ if (!(cell > 0))
6
+ throw new Error(`createLightGrid: a cell of ${String(cell)} metres`);
7
+ let minX = Infinity;
8
+ let minY = Infinity;
9
+ let minZ = Infinity;
10
+ let maxX = -Infinity;
11
+ let maxY = -Infinity;
12
+ let maxZ = -Infinity;
13
+ let maxRadius = 0;
14
+ for (const light of sources) {
15
+ minX = Math.min(minX, light.x);
16
+ minY = Math.min(minY, light.y);
17
+ minZ = Math.min(minZ, light.z);
18
+ maxX = Math.max(maxX, light.x);
19
+ maxY = Math.max(maxY, light.y);
20
+ maxZ = Math.max(maxZ, light.z);
21
+ maxRadius = Math.max(maxRadius, light.radius);
22
+ }
23
+ if (sources.length === 0)
24
+ minX = minY = minZ = maxX = maxY = maxZ = 0;
25
+ const dims = [
26
+ Math.floor((maxX - minX) / cell) + 1,
27
+ Math.floor((maxY - minY) / cell) + 1,
28
+ Math.floor((maxZ - minZ) / cell) + 1,
29
+ ];
30
+ const cells = dims[0] * dims[1] * dims[2];
31
+ if (cells > LIGHT_GRID_MAX_CELLS) {
32
+ throw new Error(`createLightGrid: these lights span ${dims.join(' × ')} cells of ${cell} m, past ` +
33
+ `${LIGHT_GRID_MAX_CELLS}; a larger cell fits them.`);
34
+ }
35
+ const cellOf = (light) => Math.floor((light.x - minX) / cell) +
36
+ dims[0] * (Math.floor((light.y - minY) / cell) + dims[1] * Math.floor((light.z - minZ) / cell));
37
+ const start = new Uint32Array(cells + 1);
38
+ for (const light of sources) {
39
+ const c = cellOf(light);
40
+ start[c + 1] = start[c + 1] + 1;
41
+ }
42
+ for (let c = 0; c < cells; c++)
43
+ start[c + 1] = start[c + 1] + start[c];
44
+ const lights = new Uint32Array(sources.length);
45
+ const cursor = start.slice(0, cells);
46
+ for (let index = 0; index < sources.length; index++) {
47
+ const c = cellOf(sources[index]);
48
+ lights[cursor[c]] = index;
49
+ cursor[c] = cursor[c] + 1;
50
+ }
51
+ return {
52
+ sources,
53
+ cell,
54
+ origin: [minX, minY, minZ],
55
+ dims,
56
+ start,
57
+ lights,
58
+ maxRadius,
59
+ gathered: new Int32Array(sources.length),
60
+ };
61
+ }
62
+ /**
63
+ * Gather into `grid.gathered`, cell by cell, every light whose cell lies within `reach` metres of
64
+ * (x, y, z) on each axis, and answer how many.
65
+ */
66
+ export function gatherLights(grid, x, y, z, reach) {
67
+ const { cell, origin, dims, start, lights, gathered } = grid;
68
+ const x0 = Math.max(0, Math.floor((x - reach - origin[0]) / cell));
69
+ const x1 = Math.min(dims[0] - 1, Math.floor((x + reach - origin[0]) / cell));
70
+ const y0 = Math.max(0, Math.floor((y - reach - origin[1]) / cell));
71
+ const y1 = Math.min(dims[1] - 1, Math.floor((y + reach - origin[1]) / cell));
72
+ const z0 = Math.max(0, Math.floor((z - reach - origin[2]) / cell));
73
+ const z1 = Math.min(dims[2] - 1, Math.floor((z + reach - origin[2]) / cell));
74
+ let count = 0;
75
+ for (let cz = z0; cz <= z1; cz++) {
76
+ for (let cy = y0; cy <= y1; cy++) {
77
+ const row = dims[0] * (cy + dims[1] * cz);
78
+ for (let cx = x0; cx <= x1; cx++) {
79
+ const c = row + cx;
80
+ for (let at = start[c]; at < start[c + 1]; at++) {
81
+ gathered[count++] = lights[at];
82
+ }
83
+ }
84
+ }
85
+ }
86
+ return count;
87
+ }
@@ -7,6 +7,9 @@ import type { MeshData } from '@driftengine/drft';
7
7
  export declare class Mesh {
8
8
  private readonly vao;
9
9
  private readonly buffers;
10
+ /** Every attribute bound from a buffer, so a batch can build a vertex array of its own. */
11
+ private readonly layout;
12
+ private indexBuffer;
10
13
  /**
11
14
  * The position and normal buffers, kept so `update` can rewrite them.
12
15
  *
@@ -16,7 +19,13 @@ export declare class Mesh {
16
19
  private readonly deformable;
17
20
  /** How many floats the position buffer holds, so an update of the wrong size is refused. */
18
21
  private readonly positionFloats;
19
- private readonly indexCount;
22
+ /**
23
+ * Indices one draw of it issues; the batch-size rule reads it (`cullsInstances`). **At zero the
24
+ * mesh has nothing to draw and no verb draws it**, as on WebGPU (`GpuMesh.indexCount`): a draw of
25
+ * nothing is silent here but is still a call and a counted draw, and the two backends count the
26
+ * same frame.
27
+ */
28
+ readonly indexCount: number;
20
29
  /**
21
30
  * The constant values this mesh's *absent* attributes are shaded with, re-applied on
22
31
  * every draw.
@@ -94,30 +103,18 @@ export declare class Mesh {
94
103
  /** Caller is responsible for having the flat program + uniforms bound. */
95
104
  draw(gl: WebGL2RenderingContext): void;
96
105
  /**
97
- * Bind one buffer's per-instance attributes to this mesh's vertex array.
98
- *
99
- * **One batch per mesh, and the vertex array is why.** A `Mesh` owns exactly one, so a second
100
- * batch would rebind locations 11 to 15 on the array the first still draws through — and what
101
- * comes out is one batch's instances placed by the other's matrices, which reads as a transform
102
- * bug in the consumer's own code rather than as a misuse of this.
103
- *
104
- * **What would make it wrong** is a consumer legitimately wanting two batches of one mesh,
105
- * which wants a vertex array per batch rather than a flag here.
106
- */
107
- attachInstances(gl: WebGL2RenderingContext, buffer: WebGLBuffer, stride: number): void;
108
- /** The instance columns, bound. Called at attach and again after a plain draw disabled them. */
109
- private bindInstanceColumns;
110
- private instanceBuffer;
111
- private instanceStride;
112
- /** Whether `attachInstances` has already claimed locations 11 to 15 on this mesh's array. */
113
- private instanced;
114
- /**
115
- * Draw `count` instances through the attributes `attachInstances` bound.
116
- *
117
- * The constants are re-applied exactly as `draw` does — a vertex array restores the arrays and
118
- * not the constant attributes, and an absent optional attribute is a constant here.
106
+ * A vertex array for one instanced batch: every attribute this mesh binds from a buffer, its index
107
+ * buffer, and the batch's instance columns — the model matrix at 11 to 14, the tint at 15.
108
+ *
109
+ * **A batch owns its own array**, which is what lets a mesh have more than one batch: a prop drawn
110
+ * once per region is one mesh and a batch per region. With one array a mesh had one batch, since a
111
+ * second would rebind the columns the first draws through and place one batch's instances by the
112
+ * other's matrices. The arrays share every buffer, so a batch costs its instances and nothing of
113
+ * the geometry. The caller deletes what this returns.
119
114
  */
120
- drawInstances(gl: WebGL2RenderingContext, count: number): void;
115
+ createInstanceArray(gl: WebGL2RenderingContext, instances: WebGLBuffer, stride: number): WebGLVertexArrayObject;
116
+ /** Draw `count` instances through a batch's own vertex array. See `createInstanceArray`. */
117
+ drawInstancesThrough(gl: WebGL2RenderingContext, vao: WebGLVertexArrayObject, count: number): void;
121
118
  dispose(gl: WebGL2RenderingContext): void;
122
119
  /**
123
120
  * Rewrite this mesh's positions, and its normals where the caller has them.
@@ -179,3 +176,5 @@ export interface IncrementalMesh {
179
176
  * them before a byte of it has landed.
180
177
  */
181
178
  export declare function createMeshIncremental(gl: WebGL2RenderingContext, data: MeshData, dynamic?: boolean): IncrementalMesh;
179
+ /** Texture coordinates and layers interleaved as (u, v, layer), one row a vertex. */
180
+ export declare function packUvLayers(uvs: Float32Array, layers: Float32Array): Float32Array;