@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,384 @@
1
+ /**
2
+ * Boolean operations on closed solids — union, subtraction, intersection — by binary space
3
+ * partitioning.
4
+ *
5
+ * The algorithm is the one Evan Wallace's csg.js made well known, implemented afresh here — named
6
+ * at the point it is used, as `CREDITS.md` asks of a published method: each operand becomes a BSP tree of its own polygons, each tree clips the
7
+ * other's polygons to the region it keeps, and the survivors are rebuilt into one tree. Polygons
8
+ * carry position, normal and texture coordinates, interpolated where a plane splits them, so a cut
9
+ * face keeps the texture of the face it was cut from.
10
+ *
11
+ * **Only what the other operand reaches is split.** Each tree is used for its planes alone: an
12
+ * operand's own polygons are clipped as they arrived, never as its tree's construction cut them,
13
+ * and a polygon whose bounds clear the other operand's is passed through whole — kept or dropped
14
+ * as the operation says of everything outside that operand. The result is the two clipped lists
15
+ * side by side, where the published method rebuilds one tree from them and splits the second list
16
+ * along every plane of the first on the way. Neither changes the surface; together they are what
17
+ * kept a wall cut five times with its end faces at two triangles each instead of eight, and a
18
+ * city's worth of cut towers out of the millions.
19
+ *
20
+ * **What it gives up.** A polygon the other operand does reach is still split along every plane of
21
+ * its tree that crosses it, and nothing is re-merged, so a heavily cut solid grows, and the pieces
22
+ * meet with T-junctions: closed as a surface — its volume exact — but not edge for edge. Welding
23
+ * and simplification are the caller's afterwards. The tree is built iteratively, so a large
24
+ * operand cannot overflow the stack; clipping recurses to the tree's depth, which a solid of a few
25
+ * thousand faces keeps shallow.
26
+ *
27
+ * **Empty operands are answered before any tree is built**, because clipping against an empty tree
28
+ * keeps everything — which is the right answer for a union and the wrong one for an intersection.
29
+ */
30
+ import type { Solid } from './solid.ts';
31
+ import { emptySolid } from './solid.ts';
32
+ import { SolidWriter } from './solidWriter.ts';
33
+
34
+ /** Distance within which a point counts as on a plane. */
35
+ const EPSILON = 1e-5;
36
+
37
+ interface Vertex {
38
+ x: number;
39
+ y: number;
40
+ z: number;
41
+ nx: number;
42
+ ny: number;
43
+ nz: number;
44
+ u: number;
45
+ v: number;
46
+ }
47
+
48
+ interface Plane {
49
+ nx: number;
50
+ ny: number;
51
+ nz: number;
52
+ w: number;
53
+ }
54
+
55
+ interface Polygon {
56
+ vertices: Vertex[];
57
+ plane: Plane;
58
+ }
59
+
60
+ const COPLANAR = 0;
61
+ const FRONT = 1;
62
+ const BACK = 2;
63
+ const SPANNING = 3;
64
+
65
+ function planeFrom(a: Vertex, b: Vertex, c: Vertex): Plane | null {
66
+ const ux = b.x - a.x;
67
+ const uy = b.y - a.y;
68
+ const uz = b.z - a.z;
69
+ const vx = c.x - a.x;
70
+ const vy = c.y - a.y;
71
+ const vz = c.z - a.z;
72
+ let nx = uy * vz - uz * vy;
73
+ let ny = uz * vx - ux * vz;
74
+ let nz = ux * vy - uy * vx;
75
+ const len = Math.hypot(nx, ny, nz);
76
+ if (len < 1e-12) return null;
77
+ nx /= len;
78
+ ny /= len;
79
+ nz /= len;
80
+ return { nx, ny, nz, w: nx * a.x + ny * a.y + nz * a.z };
81
+ }
82
+
83
+ function flipPolygon(p: Polygon): Polygon {
84
+ const vertices = p.vertices
85
+ .slice()
86
+ .reverse()
87
+ .map((v) => ({ ...v, nx: -v.nx, ny: -v.ny, nz: -v.nz }));
88
+ return { vertices, plane: { nx: -p.plane.nx, ny: -p.plane.ny, nz: -p.plane.nz, w: -p.plane.w } };
89
+ }
90
+
91
+ function lerp(a: Vertex, b: Vertex, t: number): Vertex {
92
+ let nx = a.nx + (b.nx - a.nx) * t;
93
+ let ny = a.ny + (b.ny - a.ny) * t;
94
+ let nz = a.nz + (b.nz - a.nz) * t;
95
+ const len = Math.hypot(nx, ny, nz) || 1;
96
+ nx /= len;
97
+ ny /= len;
98
+ nz /= len;
99
+ return {
100
+ x: a.x + (b.x - a.x) * t,
101
+ y: a.y + (b.y - a.y) * t,
102
+ z: a.z + (b.z - a.z) * t,
103
+ nx,
104
+ ny,
105
+ nz,
106
+ u: a.u + (b.u - a.u) * t,
107
+ v: a.v + (b.v - a.v) * t,
108
+ };
109
+ }
110
+
111
+ /**
112
+ * Sort `polygon` against `plane` into the four lists, splitting it where it spans. A coplanar
113
+ * polygon goes to the front list when it faces the same way as the plane, the back one otherwise.
114
+ */
115
+ function split(
116
+ plane: Plane,
117
+ polygon: Polygon,
118
+ coplanarFront: Polygon[],
119
+ coplanarBack: Polygon[],
120
+ front: Polygon[],
121
+ back: Polygon[],
122
+ ): void {
123
+ let kind = 0;
124
+ const types: number[] = [];
125
+ for (const v of polygon.vertices) {
126
+ const t = plane.nx * v.x + plane.ny * v.y + plane.nz * v.z - plane.w;
127
+ const type = t < -EPSILON ? BACK : t > EPSILON ? FRONT : COPLANAR;
128
+ kind |= type;
129
+ types.push(type);
130
+ }
131
+ if (kind === COPLANAR) {
132
+ const facing =
133
+ plane.nx * polygon.plane.nx + plane.ny * polygon.plane.ny + plane.nz * polygon.plane.nz;
134
+ (facing > 0 ? coplanarFront : coplanarBack).push(polygon);
135
+ } else if (kind === FRONT) {
136
+ front.push(polygon);
137
+ } else if (kind === BACK) {
138
+ back.push(polygon);
139
+ } else if (kind === SPANNING) {
140
+ const f: Vertex[] = [];
141
+ const b: Vertex[] = [];
142
+ const n = polygon.vertices.length;
143
+ for (let i = 0; i < n; i++) {
144
+ const j = (i + 1) % n;
145
+ const ti = types[i] ?? COPLANAR;
146
+ const tj = types[j] ?? COPLANAR;
147
+ const vi = polygon.vertices[i];
148
+ const vj = polygon.vertices[j];
149
+ if (!vi || !vj) continue;
150
+ if (ti !== BACK) f.push(vi);
151
+ if (ti !== FRONT) b.push(vi);
152
+ if ((ti | tj) === SPANNING) {
153
+ const di = plane.nx * vi.x + plane.ny * vi.y + plane.nz * vi.z;
154
+ const dj = plane.nx * vj.x + plane.ny * vj.y + plane.nz * vj.z;
155
+ const t = (plane.w - di) / (dj - di);
156
+ const mid = lerp(vi, vj, t);
157
+ f.push(mid);
158
+ b.push({ ...mid });
159
+ }
160
+ }
161
+ if (f.length >= 3) front.push({ vertices: f, plane: polygon.plane });
162
+ if (b.length >= 3) back.push({ vertices: b, plane: polygon.plane });
163
+ }
164
+ }
165
+
166
+ /** A BSP tree of an operand's planes. Its polygons only choose the planes and are not kept. */
167
+ class Node {
168
+ plane: Plane | null = null;
169
+ front: Node | null = null;
170
+ back: Node | null = null;
171
+
172
+ /** Add `polygons`' planes to the tree, splitting down it, without recursion. */
173
+ build(polygons: Polygon[]): void {
174
+ const work: [Node, Polygon[]][] = [[this, polygons]];
175
+ const coplanar: Polygon[] = [];
176
+ while (work.length > 0) {
177
+ const item = work.pop();
178
+ if (!item) break;
179
+ const [node, list] = item;
180
+ if (list.length === 0) continue;
181
+ const first = list[0];
182
+ if (!node.plane && first) node.plane = { ...first.plane };
183
+ const plane = node.plane;
184
+ if (!plane) continue;
185
+ const f: Polygon[] = [];
186
+ const b: Polygon[] = [];
187
+ for (const p of list) split(plane, p, coplanar, coplanar, f, b);
188
+ coplanar.length = 0;
189
+ if (f.length > 0) {
190
+ node.front ??= new Node();
191
+ work.push([node.front, f]);
192
+ }
193
+ if (b.length > 0) {
194
+ node.back ??= new Node();
195
+ work.push([node.back, b]);
196
+ }
197
+ }
198
+ }
199
+
200
+ /** Turn the tree inside out: every plane reversed, front and back swapped. */
201
+ invert(): Node {
202
+ const stack: Node[] = [this];
203
+ while (stack.length > 0) {
204
+ const node = stack.pop();
205
+ if (!node) break;
206
+ if (node.plane) {
207
+ node.plane = {
208
+ nx: -node.plane.nx,
209
+ ny: -node.plane.ny,
210
+ nz: -node.plane.nz,
211
+ w: -node.plane.w,
212
+ };
213
+ }
214
+ const f = node.front;
215
+ node.front = node.back;
216
+ node.back = f;
217
+ if (node.front) stack.push(node.front);
218
+ if (node.back) stack.push(node.back);
219
+ }
220
+ return this;
221
+ }
222
+
223
+ /** The parts of `polygons` outside this tree's solid. */
224
+ clipPolygons(polygons: Polygon[]): Polygon[] {
225
+ if (!this.plane) return polygons.slice();
226
+ let f: Polygon[] = [];
227
+ let b: Polygon[] = [];
228
+ for (const p of polygons) split(this.plane, p, f, b, f, b);
229
+ if (this.front) f = this.front.clipPolygons(f);
230
+ b = this.back ? this.back.clipPolygons(b) : [];
231
+ return f.concat(b);
232
+ }
233
+ }
234
+
235
+ interface Bounds {
236
+ min: [number, number, number];
237
+ max: [number, number, number];
238
+ }
239
+
240
+ function boundsOf(polygons: readonly Polygon[]): Bounds {
241
+ const min: [number, number, number] = [Infinity, Infinity, Infinity];
242
+ const max: [number, number, number] = [-Infinity, -Infinity, -Infinity];
243
+ for (const p of polygons) {
244
+ for (const v of p.vertices) {
245
+ if (v.x < min[0]) min[0] = v.x;
246
+ if (v.y < min[1]) min[1] = v.y;
247
+ if (v.z < min[2]) min[2] = v.z;
248
+ if (v.x > max[0]) max[0] = v.x;
249
+ if (v.y > max[1]) max[1] = v.y;
250
+ if (v.z > max[2]) max[2] = v.z;
251
+ }
252
+ }
253
+ return { min, max };
254
+ }
255
+
256
+ /**
257
+ * `polygons` split into those that may meet `bounds` and those clear of it by more than the plane
258
+ * tolerance on some axis — which are outside the solid `bounds` encloses, with no tree to ask.
259
+ */
260
+ function partition(polygons: readonly Polygon[], bounds: Bounds): [Polygon[], Polygon[]] {
261
+ const near: Polygon[] = [];
262
+ const clear: Polygon[] = [];
263
+ const { min, max } = bounds;
264
+ for (const p of polygons) {
265
+ let lx = Infinity;
266
+ let ly = Infinity;
267
+ let lz = Infinity;
268
+ let hx = -Infinity;
269
+ let hy = -Infinity;
270
+ let hz = -Infinity;
271
+ for (const v of p.vertices) {
272
+ lx = Math.min(lx, v.x);
273
+ ly = Math.min(ly, v.y);
274
+ lz = Math.min(lz, v.z);
275
+ hx = Math.max(hx, v.x);
276
+ hy = Math.max(hy, v.y);
277
+ hz = Math.max(hz, v.z);
278
+ }
279
+ const apart =
280
+ hx < min[0] - EPSILON ||
281
+ hy < min[1] - EPSILON ||
282
+ hz < min[2] - EPSILON ||
283
+ lx > max[0] + EPSILON ||
284
+ ly > max[1] + EPSILON ||
285
+ lz > max[2] + EPSILON;
286
+ (apart ? clear : near).push(p);
287
+ }
288
+ return [near, clear];
289
+ }
290
+
291
+ const flipAll = (polygons: readonly Polygon[]): Polygon[] => polygons.map(flipPolygon);
292
+
293
+ function toPolygons(solid: Solid): Polygon[] {
294
+ const out: Polygon[] = [];
295
+ const p = solid.positions;
296
+ const n = solid.normals;
297
+ const uv = solid.uvs;
298
+ const vertex = (i: number): Vertex => ({
299
+ x: p[i * 3] ?? 0,
300
+ y: p[i * 3 + 1] ?? 0,
301
+ z: p[i * 3 + 2] ?? 0,
302
+ nx: n[i * 3] ?? 0,
303
+ ny: n[i * 3 + 1] ?? 0,
304
+ nz: n[i * 3 + 2] ?? 0,
305
+ u: uv[i * 2] ?? 0,
306
+ v: uv[i * 2 + 1] ?? 0,
307
+ });
308
+ for (let t = 0; t < solid.indices.length; t += 3) {
309
+ const a = vertex(solid.indices[t] ?? 0);
310
+ const b = vertex(solid.indices[t + 1] ?? 0);
311
+ const c = vertex(solid.indices[t + 2] ?? 0);
312
+ const plane = planeFrom(a, b, c);
313
+ if (plane) out.push({ vertices: [a, b, c], plane });
314
+ }
315
+ return out;
316
+ }
317
+
318
+ /** Polygons back to a solid, each fanned from its first corner — they are convex. */
319
+ function fromPolygons(polygons: Polygon[]): Solid {
320
+ const out = new SolidWriter();
321
+ for (const polygon of polygons) {
322
+ const base = out.vertexCount;
323
+ for (const v of polygon.vertices) out.vertex(v.x, v.y, v.z, v.nx, v.ny, v.nz, v.u, v.v);
324
+ for (let i = 1; i + 1 < polygon.vertices.length; i++)
325
+ out.triangle(base, base + i, base + i + 1);
326
+ }
327
+ return out.finish();
328
+ }
329
+
330
+ function tree(polygons: Polygon[]): Node {
331
+ const node = new Node();
332
+ node.build(polygons);
333
+ return node;
334
+ }
335
+
336
+ const isEmpty = (s: Solid): boolean => s.indices.length === 0;
337
+
338
+ /**
339
+ * Both operands' polygons, each split by the other's bounds. The three operations below are the
340
+ * published method's steps written over these lists, with the clear polygons settled by the rule
341
+ * each operation applies to everything outside the other operand.
342
+ */
343
+ function operands(a: Solid, b: Solid) {
344
+ const pa = toPolygons(a);
345
+ const pb = toPolygons(b);
346
+ const [aNear, aClear] = partition(pa, boundsOf(pb));
347
+ const [bNear, bClear] = partition(pb, boundsOf(pa));
348
+ return { pa, pb, aNear, aClear, bNear, bClear };
349
+ }
350
+
351
+ /** Everything inside either solid. */
352
+ export function solidUnion(a: Solid, b: Solid): Solid {
353
+ if (isEmpty(b)) return a;
354
+ if (isEmpty(a)) return b;
355
+ const { pa, pb, aNear, aClear, bNear, bClear } = operands(a, b);
356
+ const ta = tree(pa);
357
+ const aKept = tree(pb).clipPolygons(aNear);
358
+ /* B outside A, then without the faces lying on A's facing the same way, which A already has. */
359
+ const bKept = flipAll(ta.clipPolygons(flipAll(ta.clipPolygons(bNear))));
360
+ return fromPolygons([...aClear, ...aKept, ...bClear, ...bKept]);
361
+ }
362
+
363
+ /** Everything inside `a` and outside `b`. */
364
+ export function solidSubtract(a: Solid, b: Solid): Solid {
365
+ if (isEmpty(a)) return emptySolid();
366
+ if (isEmpty(b)) return a;
367
+ const { pa, pb, aNear, aClear, bNear } = operands(a, b);
368
+ const outside = tree(pa).invert();
369
+ const aKept = flipAll(tree(pb).clipPolygons(flipAll(aNear)));
370
+ /* B inside A, turned to face into the cavity it leaves. */
371
+ const bKept = outside.clipPolygons(flipAll(outside.clipPolygons(bNear)));
372
+ return fromPolygons([...aClear, ...aKept, ...bKept]);
373
+ }
374
+
375
+ /** Everything inside both solids. */
376
+ export function solidIntersect(a: Solid, b: Solid): Solid {
377
+ if (isEmpty(a) || isEmpty(b)) return emptySolid();
378
+ const { pa, pb, aNear, bNear } = operands(a, b);
379
+ const outsideA = tree(pa).invert();
380
+ const outsideB = tree(pb).invert();
381
+ const aKept = flipAll(outsideB.clipPolygons(flipAll(aNear)));
382
+ const bKept = flipAll(outsideA.clipPolygons(flipAll(outsideA.clipPolygons(bNear))));
383
+ return fromPolygons([...aKept, ...bKept]);
384
+ }
@@ -128,6 +128,7 @@ export class MeshBuilder {
128
128
  * inverse transpose, and nothing here has wanted that yet.
129
129
  */
130
130
  addMesh(mesh: MeshData, x: number, y: number, z: number, scale = 1): this {
131
+ refuseLayers(mesh);
131
132
  const base = this.vertexCount;
132
133
  const count = mesh.positions.length / 3;
133
134
  for (let i = 0; i < count; i++) {
@@ -199,6 +200,7 @@ export class MeshBuilder {
199
200
  forward: Vec3,
200
201
  scale = 1,
201
202
  ): this {
203
+ refuseLayers(mesh);
202
204
  assertOrthonormal(right, up, forward);
203
205
  const base = this.vertexCount;
204
206
  const count = mesh.positions.length / 3;
@@ -1399,3 +1401,16 @@ const CORNERS = [
1399
1401
  [1, 1],
1400
1402
  [-1, 1],
1401
1403
  ] as const;
1404
+
1405
+ /**
1406
+ * A builder has no layer channel, so merging a layered mesh would keep its texture coordinates and
1407
+ * silently put every face on layer 0 — the first image of the array, which reads as the wrong
1408
+ * facade everywhere rather than as an error. Refused instead; `concatMeshes` joins layered meshes.
1409
+ */
1410
+ function refuseLayers(mesh: MeshData): void {
1411
+ if (mesh.layers === undefined) return;
1412
+ throw new Error(
1413
+ 'MeshBuilder: this mesh names texture-array layers, which a builder does not carry — merging ' +
1414
+ 'it here would put every face on layer 0. Join layered meshes with concatMeshes instead.',
1415
+ );
1416
+ }
@@ -28,6 +28,7 @@ const OPTIONAL = [
28
28
  ['channel', 4],
29
29
  ['joints', 4],
30
30
  ['weights', 4],
31
+ ['layers', 1],
31
32
  ] as const;
32
33
 
33
34
  /**
@@ -67,6 +67,8 @@ const GLYPHS: Record<string, readonly number[]> = {
67
67
  "'": [0b00100, 0b00100, 0, 0, 0, 0, 0],
68
68
  '(': [0b00010, 0b00100, 0b01000, 0b01000, 0b01000, 0b00100, 0b00010],
69
69
  ')': [0b01000, 0b00100, 0b00010, 0b00010, 0b00010, 0b00100, 0b01000],
70
+ '[': [0b01110, 0b01000, 0b01000, 0b01000, 0b01000, 0b01000, 0b01110],
71
+ ']': [0b01110, 0b00010, 0b00010, 0b00010, 0b00010, 0b00010, 0b01110],
70
72
  '%': [0b11001, 0b11010, 0b00010, 0b00100, 0b01000, 0b01011, 0b10011],
71
73
  '·': [0, 0, 0, 0b01100, 0b01100, 0, 0],
72
74
  '×': [0, 0b10001, 0b01010, 0b00100, 0b01010, 0b10001, 0],
@@ -0,0 +1,189 @@
1
+ /**
2
+ * A solid: an indexed triangle surface with normals and texture coordinates, and the operations
3
+ * every procedural builder needs on one — place it, join several, measure it, and hand it to the
4
+ * renderer as a mesh.
5
+ *
6
+ * **Why a type of its own rather than `MeshData`.** A mesh carries a dozen per-vertex channels a
7
+ * renderer reads (colour, emissive, roughness, grain, joints…) and a solid is what exists before any
8
+ * of them are decided: shape alone. Booleans, smoothing and transforms are defined on shape, and
9
+ * dragging eleven channels through a boolean would mean deciding how each one interpolates across a
10
+ * cut. The caller paints a finished solid once, with `solidToMesh` or its own writer.
11
+ *
12
+ * **Named `Solid` because `*Shape` is taken**: the physics colliders the core barrel re-exports are
13
+ * `boxShape`, `cylinderShape` and the rest, and a second meaning for the word would read as the
14
+ * same thing. Every primitive here is closed except `solidQuad`, a single-sided plane.
15
+ *
16
+ * Built at load or bake time, never per frame, so these functions allocate their results.
17
+ */
18
+ import type { MeshData } from '../render/mesh.ts';
19
+ import type { Vec3 } from '../math/color.ts';
20
+
21
+ export interface Solid {
22
+ /** Three floats a vertex. */
23
+ readonly positions: Float32Array;
24
+ /** Three floats a vertex, unit length, outward. */
25
+ readonly normals: Float32Array;
26
+ /**
27
+ * Two floats a vertex, parametric: 0..1 across each face of a box, around and along a round
28
+ * primitive. Tiling in metres is the caller's, who knows the sizes.
29
+ */
30
+ readonly uvs: Float32Array;
31
+ /** Counter-clockwise triangles seen from outside. */
32
+ readonly indices: Uint32Array;
33
+ }
34
+
35
+ export function emptySolid(): Solid {
36
+ return {
37
+ positions: new Float32Array(0),
38
+ normals: new Float32Array(0),
39
+ uvs: new Float32Array(0),
40
+ indices: new Uint32Array(0),
41
+ };
42
+ }
43
+
44
+ /**
45
+ * `solid` moved by a column-major 4×4 model matrix.
46
+ *
47
+ * **Normals go through the inverse transpose of the upper 3×3**, then are renormalised. Rotating
48
+ * them by the matrix itself is right only for rotations and uniform scales; under a non-uniform one
49
+ * — a unit cylinder scaled to a post, a sphere to an ellipsoid — it leaves every normal leaning
50
+ * toward the stretched axis, and the surface is lit as if it faced somewhere else, which reads as a
51
+ * shading bug rather than a maths one.
52
+ *
53
+ * **A mirror reverses the winding**, because a negative determinant turns every triangle inside
54
+ * out; without the reversal a mirrored solid is culled from outside and its volume goes negative.
55
+ */
56
+ export function transformSolid(solid: Solid, model: ArrayLike<number>): Solid {
57
+ const m = (i: number): number => model[i] ?? 0;
58
+ const a = m(0);
59
+ const b = m(4);
60
+ const c = m(8);
61
+ const d = m(1);
62
+ const e = m(5);
63
+ const f = m(9);
64
+ const g = m(2);
65
+ const h = m(6);
66
+ const k = m(10);
67
+ /* Cofactors of the upper 3×3 in row-major (a b c / d e f / g h k): the inverse transpose up to
68
+ the determinant's scale, which renormalising removes — except its sign, kept below. */
69
+ const c00 = e * k - f * h;
70
+ const c01 = f * g - d * k;
71
+ const c02 = d * h - e * g;
72
+ const c10 = c * h - b * k;
73
+ const c11 = a * k - c * g;
74
+ const c12 = b * g - a * h;
75
+ const c20 = b * f - c * e;
76
+ const c21 = c * d - a * f;
77
+ const c22 = a * e - b * d;
78
+ const det = a * c00 + b * c01 + c * c02;
79
+ const sign = det < 0 ? -1 : 1;
80
+
81
+ const count = solid.positions.length / 3;
82
+ const positions = new Float32Array(count * 3);
83
+ const normals = new Float32Array(count * 3);
84
+ for (let v = 0; v < count; v++) {
85
+ const x = solid.positions[v * 3] ?? 0;
86
+ const y = solid.positions[v * 3 + 1] ?? 0;
87
+ const z = solid.positions[v * 3 + 2] ?? 0;
88
+ positions[v * 3] = a * x + b * y + c * z + m(12);
89
+ positions[v * 3 + 1] = d * x + e * y + f * z + m(13);
90
+ positions[v * 3 + 2] = g * x + h * y + k * z + m(14);
91
+ const nx = solid.normals[v * 3] ?? 0;
92
+ const ny = solid.normals[v * 3 + 1] ?? 0;
93
+ const nz = solid.normals[v * 3 + 2] ?? 0;
94
+ const tx = (c00 * nx + c01 * ny + c02 * nz) * sign;
95
+ const ty = (c10 * nx + c11 * ny + c12 * nz) * sign;
96
+ const tz = (c20 * nx + c21 * ny + c22 * nz) * sign;
97
+ const len = Math.hypot(tx, ty, tz) || 1;
98
+ normals[v * 3] = tx / len;
99
+ normals[v * 3 + 1] = ty / len;
100
+ normals[v * 3 + 2] = tz / len;
101
+ }
102
+ let indices = solid.indices;
103
+ if (det < 0) {
104
+ indices = new Uint32Array(solid.indices.length);
105
+ for (let t = 0; t < indices.length; t += 3) {
106
+ indices[t] = solid.indices[t] ?? 0;
107
+ indices[t + 1] = solid.indices[t + 2] ?? 0;
108
+ indices[t + 2] = solid.indices[t + 1] ?? 0;
109
+ }
110
+ }
111
+ return { positions, normals, uvs: solid.uvs, indices };
112
+ }
113
+
114
+ /** Several solids as one, each one's indices offset past the vertices before it. */
115
+ export function mergeSolids(solids: readonly Solid[]): Solid {
116
+ if (solids.length === 1 && solids[0] !== undefined) return solids[0];
117
+ let vertices = 0;
118
+ let triangles = 0;
119
+ for (const s of solids) {
120
+ vertices += s.positions.length / 3;
121
+ triangles += s.indices.length;
122
+ }
123
+ const positions = new Float32Array(vertices * 3);
124
+ const normals = new Float32Array(vertices * 3);
125
+ const uvs = new Float32Array(vertices * 2);
126
+ const indices = new Uint32Array(triangles);
127
+ let v = 0;
128
+ let t = 0;
129
+ for (const s of solids) {
130
+ positions.set(s.positions, v * 3);
131
+ normals.set(s.normals, v * 3);
132
+ uvs.set(s.uvs, v * 2);
133
+ for (let i = 0; i < s.indices.length; i++) indices[t + i] = (s.indices[i] ?? 0) + v;
134
+ v += s.positions.length / 3;
135
+ t += s.indices.length;
136
+ }
137
+ return { positions, normals, uvs, indices };
138
+ }
139
+
140
+ /**
141
+ * The enclosed volume by the divergence theorem: the sum of each triangle's signed tetrahedron
142
+ * with the origin. Positive for a closed, outward-wound solid; meaningless for an open one.
143
+ *
144
+ * Exact for any closed surface, T-junctions included, which is why the CSG tests can use it on a
145
+ * boolean's output where an edge-pairing check cannot.
146
+ */
147
+ export function solidVolume(solid: Solid): number {
148
+ const p = solid.positions;
149
+ const idx = solid.indices;
150
+ let sum = 0;
151
+ for (let t = 0; t < idx.length; t += 3) {
152
+ const a = (idx[t] ?? 0) * 3;
153
+ const b = (idx[t + 1] ?? 0) * 3;
154
+ const c = (idx[t + 2] ?? 0) * 3;
155
+ const ax = p[a] ?? 0;
156
+ const ay = p[a + 1] ?? 0;
157
+ const az = p[a + 2] ?? 0;
158
+ const bx = p[b] ?? 0;
159
+ const by = p[b + 1] ?? 0;
160
+ const bz = p[b + 2] ?? 0;
161
+ const cx = p[c] ?? 0;
162
+ const cy = p[c + 1] ?? 0;
163
+ const cz = p[c + 2] ?? 0;
164
+ sum += ax * (by * cz - bz * cy) - ay * (bx * cz - bz * cx) + az * (bx * cy - by * cx);
165
+ }
166
+ return sum / 6;
167
+ }
168
+
169
+ /**
170
+ * A solid painted one colour, as a mesh the renderer draws. `emissive` is the usual scalar: the
171
+ * surface's own colour scaled, gated by the environment's night factor like every emissive.
172
+ */
173
+ export function solidToMesh(solid: Solid, color: Vec3, emissive = 0): MeshData {
174
+ const count = solid.positions.length / 3;
175
+ const colors = new Float32Array(count * 3);
176
+ for (let v = 0; v < count; v++) {
177
+ colors[v * 3] = color[0];
178
+ colors[v * 3 + 1] = color[1];
179
+ colors[v * 3 + 2] = color[2];
180
+ }
181
+ return {
182
+ positions: solid.positions,
183
+ normals: solid.normals,
184
+ colors,
185
+ emissive: new Float32Array(count).fill(emissive),
186
+ uvs: solid.uvs,
187
+ indices: solid.indices,
188
+ };
189
+ }