topazcube 0.1.30 → 0.1.33

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 (96) hide show
  1. package/LICENSE.txt +0 -0
  2. package/README.md +0 -0
  3. package/dist/Renderer.cjs +18200 -0
  4. package/dist/Renderer.cjs.map +1 -0
  5. package/dist/Renderer.js +18183 -0
  6. package/dist/Renderer.js.map +1 -0
  7. package/dist/client.cjs +94 -260
  8. package/dist/client.cjs.map +1 -1
  9. package/dist/client.js +71 -215
  10. package/dist/client.js.map +1 -1
  11. package/dist/server.cjs +165 -432
  12. package/dist/server.cjs.map +1 -1
  13. package/dist/server.js +117 -370
  14. package/dist/server.js.map +1 -1
  15. package/dist/terminal.cjs +113 -200
  16. package/dist/terminal.cjs.map +1 -1
  17. package/dist/terminal.js +50 -51
  18. package/dist/terminal.js.map +1 -1
  19. package/dist/utils-CRhi1BDa.cjs +259 -0
  20. package/dist/utils-CRhi1BDa.cjs.map +1 -0
  21. package/dist/utils-D7tXt6-2.js +260 -0
  22. package/dist/utils-D7tXt6-2.js.map +1 -0
  23. package/package.json +19 -15
  24. package/src/{client.ts → network/client.js} +173 -403
  25. package/src/{compress-browser.ts → network/compress-browser.js} +2 -4
  26. package/src/{compress-node.ts → network/compress-node.js} +8 -14
  27. package/src/{server.ts → network/server.js} +229 -317
  28. package/src/{terminal.js → network/terminal.js} +0 -0
  29. package/src/{topazcube.ts → network/topazcube.js} +2 -2
  30. package/src/network/utils.js +375 -0
  31. package/src/renderer/Camera.js +191 -0
  32. package/src/renderer/DebugUI.js +572 -0
  33. package/src/renderer/Geometry.js +1049 -0
  34. package/src/renderer/Material.js +61 -0
  35. package/src/renderer/Mesh.js +211 -0
  36. package/src/renderer/Node.js +112 -0
  37. package/src/renderer/Pipeline.js +643 -0
  38. package/src/renderer/Renderer.js +1324 -0
  39. package/src/renderer/Skin.js +792 -0
  40. package/src/renderer/Texture.js +584 -0
  41. package/src/renderer/core/AssetManager.js +359 -0
  42. package/src/renderer/core/CullingSystem.js +307 -0
  43. package/src/renderer/core/EntityManager.js +541 -0
  44. package/src/renderer/core/InstanceManager.js +343 -0
  45. package/src/renderer/core/ParticleEmitter.js +358 -0
  46. package/src/renderer/core/ParticleSystem.js +564 -0
  47. package/src/renderer/core/SpriteSystem.js +349 -0
  48. package/src/renderer/gltf.js +546 -0
  49. package/src/renderer/math.js +161 -0
  50. package/src/renderer/rendering/HistoryBufferManager.js +333 -0
  51. package/src/renderer/rendering/ProbeCapture.js +1495 -0
  52. package/src/renderer/rendering/ReflectionProbeManager.js +352 -0
  53. package/src/renderer/rendering/RenderGraph.js +2064 -0
  54. package/src/renderer/rendering/passes/AOPass.js +308 -0
  55. package/src/renderer/rendering/passes/AmbientCapturePass.js +593 -0
  56. package/src/renderer/rendering/passes/BasePass.js +101 -0
  57. package/src/renderer/rendering/passes/BloomPass.js +417 -0
  58. package/src/renderer/rendering/passes/FogPass.js +419 -0
  59. package/src/renderer/rendering/passes/GBufferPass.js +706 -0
  60. package/src/renderer/rendering/passes/HiZPass.js +714 -0
  61. package/src/renderer/rendering/passes/LightingPass.js +739 -0
  62. package/src/renderer/rendering/passes/ParticlePass.js +835 -0
  63. package/src/renderer/rendering/passes/PlanarReflectionPass.js +456 -0
  64. package/src/renderer/rendering/passes/PostProcessPass.js +282 -0
  65. package/src/renderer/rendering/passes/ReflectionPass.js +157 -0
  66. package/src/renderer/rendering/passes/RenderPostPass.js +364 -0
  67. package/src/renderer/rendering/passes/SSGIPass.js +265 -0
  68. package/src/renderer/rendering/passes/SSGITilePass.js +296 -0
  69. package/src/renderer/rendering/passes/ShadowPass.js +1822 -0
  70. package/src/renderer/rendering/passes/TransparentPass.js +831 -0
  71. package/src/renderer/rendering/shaders/ao.wgsl +182 -0
  72. package/src/renderer/rendering/shaders/bloom.wgsl +97 -0
  73. package/src/renderer/rendering/shaders/bloom_blur.wgsl +80 -0
  74. package/src/renderer/rendering/shaders/depth_copy.wgsl +17 -0
  75. package/src/renderer/rendering/shaders/geometry.wgsl +550 -0
  76. package/src/renderer/rendering/shaders/hiz_reduce.wgsl +114 -0
  77. package/src/renderer/rendering/shaders/light_culling.wgsl +204 -0
  78. package/src/renderer/rendering/shaders/lighting.wgsl +932 -0
  79. package/src/renderer/rendering/shaders/lighting_common.wgsl +143 -0
  80. package/src/renderer/rendering/shaders/particle_render.wgsl +525 -0
  81. package/src/renderer/rendering/shaders/particle_simulate.wgsl +440 -0
  82. package/src/renderer/rendering/shaders/postproc.wgsl +272 -0
  83. package/src/renderer/rendering/shaders/render_post.wgsl +289 -0
  84. package/src/renderer/rendering/shaders/shadow.wgsl +76 -0
  85. package/src/renderer/rendering/shaders/ssgi.wgsl +266 -0
  86. package/src/renderer/rendering/shaders/ssgi_accumulate.wgsl +114 -0
  87. package/src/renderer/rendering/shaders/ssgi_propagate.wgsl +132 -0
  88. package/src/renderer/utils/BoundingSphere.js +439 -0
  89. package/src/renderer/utils/Frustum.js +281 -0
  90. package/dist/client.d.cts +0 -211
  91. package/dist/client.d.ts +0 -211
  92. package/dist/server.d.cts +0 -120
  93. package/dist/server.d.ts +0 -120
  94. package/dist/terminal.d.cts +0 -64
  95. package/dist/terminal.d.ts +0 -64
  96. package/src/utils.ts +0 -403
@@ -0,0 +1,1049 @@
1
+ import { calculateBoundingSphere } from "./utils/BoundingSphere.js"
2
+
3
+ var _UID = 30001
4
+
5
+ class Geometry {
6
+
7
+ constructor(engine, attributes) {
8
+ this.uid = _UID++
9
+ this.engine = engine
10
+ const { device } = engine
11
+
12
+ // Bounding sphere (lazy calculated)
13
+ this._bsphere = null
14
+
15
+ this.vertexBufferLayout = {
16
+ arrayStride: 80, // 20 floats * 4 bytes each
17
+ attributes: [
18
+ { format: "float32x3", offset: 0, shaderLocation: 0 }, // position
19
+ { format: "float32x2", offset: 12, shaderLocation: 1 }, // uv
20
+ { format: "float32x3", offset: 20, shaderLocation: 2 }, // normal
21
+ { format: "float32x4", offset: 32, shaderLocation: 3 }, // color
22
+ { format: "float32x4", offset: 48, shaderLocation: 4 }, // weights
23
+ { format: "uint32x4", offset: 64, shaderLocation: 5 }, // joints
24
+ ],
25
+ stepMode: 'vertex'
26
+ };
27
+
28
+ // Instance buffer layout for model matrix + sprite data
29
+ this.instanceBufferLayout = {
30
+ arrayStride: 112, // 28 floats * 4 bytes each
31
+ stepMode: 'instance',
32
+ attributes: [
33
+ { format: "float32x4", offset: 0, shaderLocation: 6 }, // matrix column 0
34
+ { format: "float32x4", offset: 16, shaderLocation: 7 }, // matrix column 1
35
+ { format: "float32x4", offset: 32, shaderLocation: 8 }, // matrix column 2
36
+ { format: "float32x4", offset: 48, shaderLocation: 9 }, // matrix column 3
37
+ { format: "float32x4", offset: 64, shaderLocation: 10 }, // position + radius
38
+ { format: "float32x4", offset: 80, shaderLocation: 11 }, // uvTransform (offset.xy, scale.xy)
39
+ { format: "float32x4", offset: 96, shaderLocation: 12 }, // color (r, g, b, a)
40
+ ]
41
+ };
42
+
43
+ const vertexCount = attributes.position.length / 3
44
+
45
+ if (attributes.indices == false) {
46
+ // Generate indices for non-indexed geometry
47
+ const positions = attributes.position;
48
+ const vertexCount = positions.length / 3; // 3 components per vertex
49
+ const indices = new Uint32Array(vertexCount);
50
+ for (let i = 0; i < vertexCount; i++) {
51
+ indices[i] = i;
52
+ }
53
+ attributes.indices = indices
54
+ }
55
+
56
+ if (attributes.normal == false) {
57
+ // Generate flat normals for non-normal geometry
58
+ const positions = attributes.position;
59
+ const indices = attributes.indices;
60
+ const normals = new Float32Array(positions.length);
61
+
62
+ // Calculate normals for each triangle
63
+ for (let i = 0; i < indices.length; i += 3) {
64
+ const i0 = indices[i] * 3;
65
+ const i1 = indices[i + 1] * 3;
66
+ const i2 = indices[i + 2] * 3;
67
+
68
+ // Get vertices of triangle
69
+ const v0 = [positions[i0], positions[i0 + 1], positions[i0 + 2]];
70
+ const v1 = [positions[i1], positions[i1 + 1], positions[i1 + 2]];
71
+ const v2 = [positions[i2], positions[i2 + 1], positions[i2 + 2]];
72
+
73
+ // Calculate vectors for cross product
74
+ const vec1 = [v1[0] - v0[0], v1[1] - v0[1], v1[2] - v0[2]];
75
+ const vec2 = [v2[0] - v0[0], v2[1] - v0[1], v2[2] - v0[2]];
76
+
77
+ // Calculate cross product
78
+ const normal = [
79
+ vec1[1] * vec2[2] - vec1[2] * vec2[1],
80
+ vec1[2] * vec2[0] - vec1[0] * vec2[2],
81
+ vec1[0] * vec2[1] - vec1[1] * vec2[0]
82
+ ];
83
+
84
+ // Normalize
85
+ const length = Math.sqrt(normal[0] * normal[0] + normal[1] * normal[1] + normal[2] * normal[2]);
86
+ normal[0] /= length;
87
+ normal[1] /= length;
88
+ normal[2] /= length;
89
+
90
+ // Assign same normal to all vertices of triangle
91
+ normals[i0] = normal[0];
92
+ normals[i0 + 1] = normal[1];
93
+ normals[i0 + 2] = normal[2];
94
+ normals[i1] = normal[0];
95
+ normals[i1 + 1] = normal[1];
96
+ normals[i1 + 2] = normal[2];
97
+ normals[i2] = normal[0];
98
+ normals[i2 + 1] = normal[1];
99
+ normals[i2 + 2] = normal[2];
100
+ }
101
+
102
+ attributes.normal = normals
103
+ }
104
+
105
+
106
+ const vertexArray = new Float32Array(vertexCount * 20);
107
+ this.vertexCount = vertexCount;
108
+ const vertexArrayUint32 = new Uint32Array(vertexArray.buffer);
109
+ const vertexBuffer = device.createBuffer({
110
+ size: vertexArray.byteLength,
111
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
112
+ })
113
+
114
+ // Create instance buffer (start small, grows dynamically)
115
+ const initialMaxInstances = 16;
116
+ const instanceBuffer = device.createBuffer({
117
+ size: 112 * initialMaxInstances, // 28 floats per instance
118
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
119
+ });
120
+ this.instanceBuffer = instanceBuffer;
121
+ this.maxInstances = initialMaxInstances;
122
+ this.instanceCount = 0;
123
+ this.instanceData = new Float32Array(28 * initialMaxInstances);
124
+ this._instanceDataDirty = true
125
+
126
+ const indexArray = new Uint32Array(attributes.indices.length);
127
+ const indexBuffer = device.createBuffer({
128
+ size: indexArray.byteLength,
129
+ usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
130
+ })
131
+
132
+ this.attributes = attributes
133
+ this.vertexArray = vertexArray
134
+ this.vertexArrayUint32 = vertexArrayUint32
135
+ this.indexArray = indexArray
136
+
137
+ this.vertexBuffer = vertexBuffer
138
+ this.indexBuffer = indexBuffer
139
+
140
+ this.updateVertexBuffer()
141
+ }
142
+
143
+
144
+ /**
145
+ * Grow instance buffer capacity by doubling it
146
+ * @param {number} minCapacity - Minimum required capacity (optional)
147
+ */
148
+ growInstanceBuffer(minCapacity = 0) {
149
+ const { device } = this.engine;
150
+
151
+ // Calculate new size: double current, or enough for minCapacity
152
+ let newMaxInstances = this.maxInstances * 2;
153
+ while (newMaxInstances < minCapacity) {
154
+ newMaxInstances *= 2;
155
+ }
156
+
157
+ // Create new larger buffer
158
+ const newInstanceBuffer = device.createBuffer({
159
+ size: 112 * newMaxInstances, // 28 floats per instance
160
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
161
+ });
162
+
163
+ // Create new larger data array and copy existing data
164
+ const newInstanceData = new Float32Array(28 * newMaxInstances);
165
+ newInstanceData.set(this.instanceData);
166
+
167
+ // Destroy old buffer
168
+ this.instanceBuffer.destroy();
169
+
170
+ // Update references
171
+ this.instanceBuffer = newInstanceBuffer;
172
+ this.instanceData = newInstanceData;
173
+ this.maxInstances = newMaxInstances;
174
+ this._instanceDataDirty = true;
175
+ }
176
+
177
+ addInstance(position, radius = 1, uvTransform = [0, 0, 1, 1], color = [1, 1, 1, 1]) {
178
+ if (this.instanceCount >= this.maxInstances) {
179
+ this.growInstanceBuffer();
180
+ }
181
+
182
+ // Create a 4x4 transform matrix for this instance
183
+ const matrix = mat4.create();
184
+ mat4.translate(matrix, matrix, position);
185
+
186
+ // Add the matrix to instance data at the next available slot
187
+ const offset = this.instanceCount * 28;
188
+ this.instanceData.set(matrix, offset);
189
+ // Position + radius (would normally be set from bsphere)
190
+ this.instanceData[offset + 16] = position[0];
191
+ this.instanceData[offset + 17] = position[1];
192
+ this.instanceData[offset + 18] = position[2];
193
+ this.instanceData[offset + 19] = radius;
194
+ // UV transform
195
+ this.instanceData[offset + 20] = uvTransform[0];
196
+ this.instanceData[offset + 21] = uvTransform[1];
197
+ this.instanceData[offset + 22] = uvTransform[2];
198
+ this.instanceData[offset + 23] = uvTransform[3];
199
+ // Color
200
+ this.instanceData[offset + 24] = color[0];
201
+ this.instanceData[offset + 25] = color[1];
202
+ this.instanceData[offset + 26] = color[2];
203
+ this.instanceData[offset + 27] = color[3];
204
+
205
+ this.instanceCount++;
206
+ this._instanceDataDirty = true
207
+ }
208
+
209
+ updateAllInstances(matrices) {
210
+ const { device } = this.engine;
211
+ this.instanceCount = matrices.length;
212
+ if (this.instanceCount > this.maxInstances) {
213
+ this.growInstanceBuffer(this.instanceCount);
214
+ }
215
+
216
+ // Copy all matrices into instance data (28 floats per instance)
217
+ for (let i = 0; i < matrices.length; i++) {
218
+ const offset = i * 28;
219
+ this.instanceData.set(matrices[i], offset);
220
+ // Set default UV transform and color if not provided
221
+ this.instanceData[offset + 20] = 0; // uvOffset.x
222
+ this.instanceData[offset + 21] = 0; // uvOffset.y
223
+ this.instanceData[offset + 22] = 1; // uvScale.x
224
+ this.instanceData[offset + 23] = 1; // uvScale.y
225
+ this.instanceData[offset + 24] = 1; // color.r
226
+ this.instanceData[offset + 25] = 1; // color.g
227
+ this.instanceData[offset + 26] = 1; // color.b
228
+ this.instanceData[offset + 27] = 1; // color.a
229
+ }
230
+
231
+ this._instanceDataDirty = true
232
+ }
233
+
234
+ updateInstance(index, matrix) {
235
+ this.instanceData.set(matrix, index * 28);
236
+ this._instanceDataDirty = true
237
+ }
238
+
239
+ writeInstanceBuffer() {
240
+ const { device } = this.engine;
241
+
242
+ device.queue.writeBuffer(this.instanceBuffer, 0, this.instanceData);
243
+ }
244
+
245
+ updateVertexBuffer() {
246
+ const { device } = this.engine
247
+ const { attributes, vertexArray, vertexArrayUint32 } = this
248
+ // Interleave vertex attributes into a single array
249
+ for (let i = 0; i < this.vertexCount; i++) {
250
+ const vertexOffset = i * 20; // 20 floats per vertex
251
+
252
+ // Position (xyz)
253
+ vertexArray[vertexOffset] = attributes.position[i * 3];
254
+ vertexArray[vertexOffset + 1] = attributes.position[i * 3 + 1];
255
+ vertexArray[vertexOffset + 2] = attributes.position[i * 3 + 2];
256
+
257
+ // UV (xy)
258
+ vertexArray[vertexOffset + 3] = attributes.uv ? attributes.uv[i * 2] : 0;
259
+ vertexArray[vertexOffset + 4] = attributes.uv ? attributes.uv[i * 2 + 1] : 0;
260
+
261
+ // Normal (xyz)
262
+ vertexArray[vertexOffset + 5] = attributes.normal[i * 3]
263
+ vertexArray[vertexOffset + 6] = attributes.normal[i * 3 + 1]
264
+ vertexArray[vertexOffset + 7] = attributes.normal[i * 3 + 2]
265
+
266
+ // Color (rgba)
267
+ vertexArray[vertexOffset + 8] = attributes.color ? attributes.color[i * 4] : 1;
268
+ vertexArray[vertexOffset + 9] = attributes.color ? attributes.color[i * 4 + 1] : 1;
269
+ vertexArray[vertexOffset + 10] = attributes.color ? attributes.color[i * 4 + 2] : 1;
270
+ vertexArray[vertexOffset + 11] = attributes.color ? attributes.color[i * 4 + 3] : 1;
271
+
272
+ // Weights (xyzw)
273
+ vertexArray[vertexOffset + 12] = attributes.weights ? attributes.weights[i * 4] : 0;
274
+ vertexArray[vertexOffset + 13] = attributes.weights ? attributes.weights[i * 4 + 1] : 0;
275
+ vertexArray[vertexOffset + 14] = attributes.weights ? attributes.weights[i * 4 + 2] : 0;
276
+ vertexArray[vertexOffset + 15] = attributes.weights ? attributes.weights[i * 4 + 3] : 0;
277
+
278
+ // Joints (xyzw) - using Float32Array view to write uint32 values
279
+ const jointsOffset = vertexOffset + 16;
280
+ vertexArrayUint32[jointsOffset] = attributes.joints ? attributes.joints[i * 4] : 0;
281
+ vertexArrayUint32[jointsOffset + 1] = attributes.joints ? attributes.joints[i * 4 + 1] : 0;
282
+ vertexArrayUint32[jointsOffset + 2] = attributes.joints ? attributes.joints[i * 4 + 2] : 0;
283
+ vertexArrayUint32[jointsOffset + 3] = attributes.joints ? attributes.joints[i * 4 + 3] : 0;
284
+ }
285
+
286
+ // Copy indices
287
+ for (let i = 0; i < attributes.indices.length; i++) {
288
+ this.indexArray[i] = attributes.indices[i];
289
+ }
290
+
291
+ device.queue.writeBuffer(this.vertexBuffer, 0, this.vertexArray)
292
+ device.queue.writeBuffer(this.indexBuffer, 0, this.indexArray)
293
+ }
294
+
295
+ update() {
296
+ if (this._instanceDataDirty) {
297
+ this.writeInstanceBuffer()
298
+ this._instanceDataDirty = false
299
+ }
300
+ }
301
+
302
+ /**
303
+ * Get bounding sphere for this geometry (lazy calculated)
304
+ * @returns {{ center: [number, number, number], radius: number }}
305
+ */
306
+ getBoundingSphere() {
307
+ if (!this._bsphere) {
308
+ this._bsphere = calculateBoundingSphere(this.attributes.position)
309
+ }
310
+ return this._bsphere
311
+ }
312
+
313
+ /**
314
+ * Invalidate cached bounding sphere (call after modifying positions)
315
+ */
316
+ invalidateBoundingSphere() {
317
+ this._bsphere = null
318
+ }
319
+
320
+ static createCullingBuffers(device, maxInstances) {
321
+ // Input buffer containing instance data (position + radius)
322
+ const instanceDataBuffer = device.createBuffer({
323
+ size: maxInstances * 16, // vec3 position + float radius
324
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
325
+ });
326
+
327
+ // Output buffer containing active instance indices and count
328
+ const culledInstancesBuffer = device.createBuffer({
329
+ size: (maxInstances + 1) * 4, // indices + count at start
330
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
331
+ });
332
+
333
+ // Uniform buffer for camera frustum planes
334
+ const frustumBuffer = device.createBuffer({
335
+ size: 96, // 6 planes * vec4
336
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
337
+ });
338
+
339
+ const computePipeline = device.createComputePipeline({
340
+ layout: 'auto',
341
+ compute: {
342
+ module: device.createShaderModule({
343
+ code: `
344
+ struct Instance {
345
+ position: vec3f,
346
+ radius: f32
347
+ }
348
+
349
+ struct FrustumPlanes {
350
+ planes: array<vec4f, 6>
351
+ }
352
+
353
+ @group(0) @binding(0) var<storage, read> instances: array<Instance>;
354
+ @group(0) @binding(1) var<storage, read_write> culledInstances: array<u32>;
355
+ @group(0) @binding(2) var<uniform> frustum: FrustumPlanes;
356
+
357
+ fn sphereAgainstPlane(center: vec3f, radius: f32, plane: vec4f) -> bool {
358
+ let dist = dot(vec4f(center, 1.0), plane);
359
+ return dist > -radius;
360
+ }
361
+
362
+ @compute @workgroup_size(64)
363
+ fn main(@builtin(global_invocation_id) global_id: vec3u) {
364
+ let index = global_id.x;
365
+ if (index >= arrayLength(&instances)) {
366
+ return;
367
+ }
368
+
369
+ let instance = instances[index];
370
+
371
+ // Test against all frustum planes
372
+ var visible = true;
373
+ for (var i = 0u; i < 6u; i++) {
374
+ if (!sphereAgainstPlane(instance.position, instance.radius, frustum.planes[i])) {
375
+ visible = false;
376
+ break;
377
+ }
378
+ }
379
+
380
+ if (visible) {
381
+ let oldCount = atomicAdd(&culledInstances[0], 1);
382
+ culledInstances[oldCount + 1] = index;
383
+ }
384
+ }
385
+ `
386
+ }),
387
+ entryPoint: 'main'
388
+ }
389
+ });
390
+
391
+ const bindGroup = device.createBindGroup({
392
+ layout: computePipeline.getBindGroupLayout(0),
393
+ entries: [
394
+ {
395
+ binding: 0,
396
+ resource: { buffer: instanceDataBuffer }
397
+ },
398
+ {
399
+ binding: 1,
400
+ resource: { buffer: culledInstancesBuffer }
401
+ },
402
+ {
403
+ binding: 2,
404
+ resource: { buffer: frustumBuffer }
405
+ }
406
+ ]
407
+ });
408
+
409
+ return {
410
+ pipeline: computePipeline,
411
+ bindGroup: bindGroup,
412
+ instanceDataBuffer,
413
+ culledInstancesBuffer,
414
+ frustumBuffer
415
+ };
416
+ }
417
+
418
+ static cullInstances(engine, commandEncoder, cullingData, instances, frustumPlanes) {
419
+ const { device } = engine;
420
+ const { pipeline, bindGroup, instanceDataBuffer, culledInstancesBuffer, frustumBuffer } = cullingData;
421
+
422
+ // Write instance data
423
+ device.queue.writeBuffer(instanceDataBuffer, 0, instances);
424
+
425
+ // Write frustum planes
426
+ device.queue.writeBuffer(frustumBuffer, 0, frustumPlanes);
427
+
428
+ // Clear count to 0
429
+ const zeros = new Uint32Array(1);
430
+ device.queue.writeBuffer(culledInstancesBuffer, 0, zeros);
431
+
432
+ // Dispatch compute shader
433
+ const computePass = commandEncoder.beginComputePass();
434
+ computePass.setPipeline(pipeline);
435
+ computePass.setBindGroup(0, bindGroup);
436
+ computePass.dispatchWorkgroups(Math.ceil(instances.length / 64));
437
+ computePass.end();
438
+
439
+ return culledInstancesBuffer;
440
+ }
441
+
442
+ static cube(engine) {
443
+ return Geometry.box(engine, 1, 1, 1, 1)
444
+ }
445
+
446
+ static box(engine, width = 1, height = 1, depth = 1, uvSize = 1) {
447
+ const w = width / 2
448
+ const h = height / 2
449
+ const d = depth / 2
450
+
451
+ // UV scale based on dimensions and uvSize
452
+ const uw = width / uvSize
453
+ const uh = height / uvSize
454
+ const ud = depth / uvSize
455
+
456
+ return new Geometry(engine, {
457
+ position: new Float32Array([
458
+ // Front face
459
+ -w, -h, d,
460
+ w, -h, d,
461
+ w, h, d,
462
+ -w, h, d,
463
+ // Back face
464
+ -w, -h, -d,
465
+ -w, h, -d,
466
+ w, h, -d,
467
+ w, -h, -d,
468
+ // Top face
469
+ -w, h, -d,
470
+ -w, h, d,
471
+ w, h, d,
472
+ w, h, -d,
473
+ // Bottom face
474
+ -w, -h, -d,
475
+ w, -h, -d,
476
+ w, -h, d,
477
+ -w, -h, d,
478
+ // Right face
479
+ w, -h, -d,
480
+ w, h, -d,
481
+ w, h, d,
482
+ w, -h, d,
483
+ // Left face
484
+ -w, -h, -d,
485
+ -w, -h, d,
486
+ -w, h, d,
487
+ -w, h, -d,
488
+ ]),
489
+ uv: new Float32Array([
490
+ // Front face
491
+ 0, 0,
492
+ uw, 0,
493
+ uw, uh,
494
+ 0, uh,
495
+ // Back face
496
+ 0, 0,
497
+ 0, uh,
498
+ uw, uh,
499
+ uw, 0,
500
+ // Top face
501
+ 0, 0,
502
+ 0, ud,
503
+ uw, ud,
504
+ uw, 0,
505
+ // Bottom face
506
+ 0, 0,
507
+ uw, 0,
508
+ uw, ud,
509
+ 0, ud,
510
+ // Right face
511
+ 0, 0,
512
+ 0, uh,
513
+ ud, uh,
514
+ ud, 0,
515
+ // Left face
516
+ 0, 0,
517
+ ud, 0,
518
+ ud, uh,
519
+ 0, uh,
520
+ ]),
521
+ normal: new Float32Array([
522
+ // Front face
523
+ 0, 0, 1,
524
+ 0, 0, 1,
525
+ 0, 0, 1,
526
+ 0, 0, 1,
527
+ // Back face
528
+ 0, 0, -1,
529
+ 0, 0, -1,
530
+ 0, 0, -1,
531
+ 0, 0, -1,
532
+ // Top face
533
+ 0, 1, 0,
534
+ 0, 1, 0,
535
+ 0, 1, 0,
536
+ 0, 1, 0,
537
+ // Bottom face
538
+ 0, -1, 0,
539
+ 0, -1, 0,
540
+ 0, -1, 0,
541
+ 0, -1, 0,
542
+ // Right face
543
+ 1, 0, 0,
544
+ 1, 0, 0,
545
+ 1, 0, 0,
546
+ 1, 0, 0,
547
+ // Left face
548
+ -1, 0, 0,
549
+ -1, 0, 0,
550
+ -1, 0, 0,
551
+ -1, 0, 0,
552
+ ]),
553
+ indices: new Uint32Array([
554
+ 0, 1, 2, 2, 3, 0, // Front face
555
+ 4, 5, 6, 6, 7, 4, // Back face
556
+ 8, 9, 10, 10, 11, 8, // Top face
557
+ 12, 13, 14, 14, 15, 12, // Bottom face
558
+ 16, 17, 18, 18, 19, 16, // Right face
559
+ 20, 21, 22, 22, 23, 20 // Left face
560
+ ])
561
+ })
562
+ }
563
+ static sphere(engine, radius = 1, widthSegments = 32, heightSegments = 16) {
564
+ const positions = []
565
+ const normals = []
566
+ const uvs = []
567
+ const indices = []
568
+
569
+ // Generate vertices
570
+ for (let y = 0; y <= heightSegments; y++) {
571
+ const v = y / heightSegments
572
+ const phi = v * Math.PI
573
+
574
+ for (let x = 0; x <= widthSegments; x++) {
575
+ const u = x / widthSegments
576
+ const theta = u * Math.PI * 2
577
+
578
+ // Calculate vertex position
579
+ const px = -radius * Math.cos(theta) * Math.sin(phi)
580
+ const py = radius * Math.cos(phi)
581
+ const pz = radius * Math.sin(theta) * Math.sin(phi)
582
+
583
+ positions.push(px, py, pz)
584
+
585
+ // Normal is just the normalized position for a sphere
586
+ const length = Math.sqrt(px * px + py * py + pz * pz)
587
+ normals.push(px / length, py / length, pz / length)
588
+
589
+ // UV coordinates
590
+ uvs.push(u, 1 - v)
591
+ }
592
+ }
593
+
594
+ // Generate indices
595
+ for (let y = 0; y < heightSegments; y++) {
596
+ for (let x = 0; x < widthSegments; x++) {
597
+ const a = y * (widthSegments + 1) + x
598
+ const b = a + 1
599
+ const c = a + widthSegments + 1
600
+ const d = c + 1
601
+
602
+ // Generate two triangles for each quad (counter-clockwise winding)
603
+ indices.push(a, d, b)
604
+ indices.push(a, c, d)
605
+ }
606
+ }
607
+
608
+ return new Geometry(engine, {
609
+ position: new Float32Array(positions),
610
+ normal: new Float32Array(normals),
611
+ uv: new Float32Array(uvs),
612
+ indices: new Uint32Array(indices)
613
+ })
614
+ }
615
+
616
+ static quad(engine) {
617
+ return new Geometry(engine, {
618
+ position: [
619
+ -1, -1, 0,
620
+ 1, -1, 0,
621
+ 1, 1, 0,
622
+ -1, 1, 0,
623
+ ],
624
+ uv: [
625
+ 0, 0,
626
+ 1, 0,
627
+ 1, 1,
628
+ 0, 1,
629
+ ],
630
+ normal: [
631
+ // Normals up
632
+ 0, 1, 0,
633
+ 0, 1, 0,
634
+ 0, 1, 0,
635
+ 0, 1, 0,
636
+ ],
637
+ index: [
638
+ 0, 1, 2,
639
+ 2, 3, 0
640
+ ]
641
+
642
+ })
643
+ }
644
+
645
+ /**
646
+ * Create a quad geometry for billboard/sprite rendering
647
+ * @param {Engine} engine - Engine instance
648
+ * @param {string} pivot - Pivot mode: 'center', 'bottom', or 'horizontal'
649
+ * @returns {Geometry} Billboard quad geometry
650
+ */
651
+ static billboardQuad(engine, pivot = 'center') {
652
+ let position, normal
653
+
654
+ if (pivot === 'center') {
655
+ // Center pivot: quad centered at origin, facing +Z
656
+ position = new Float32Array([
657
+ -0.5, -0.5, 0,
658
+ 0.5, -0.5, 0,
659
+ 0.5, 0.5, 0,
660
+ -0.5, 0.5, 0,
661
+ ])
662
+ normal = new Float32Array([
663
+ 0, 0, 1,
664
+ 0, 0, 1,
665
+ 0, 0, 1,
666
+ 0, 0, 1,
667
+ ])
668
+ } else if (pivot === 'bottom') {
669
+ // Bottom pivot: quad with bottom edge at origin, facing +Z
670
+ position = new Float32Array([
671
+ -0.5, 0, 0,
672
+ 0.5, 0, 0,
673
+ 0.5, 1, 0,
674
+ -0.5, 1, 0,
675
+ ])
676
+ normal = new Float32Array([
677
+ 0, 0, 1,
678
+ 0, 0, 1,
679
+ 0, 0, 1,
680
+ 0, 0, 1,
681
+ ])
682
+ } else if (pivot === 'horizontal') {
683
+ // Horizontal pivot: quad flat on XZ plane (ground decal)
684
+ // Front face points UP (+Y) with CCW winding when viewed from above
685
+ position = new Float32Array([
686
+ -0.5, 0, -0.5, // 0: back left
687
+ 0.5, 0, -0.5, // 1: back right
688
+ 0.5, 0, 0.5, // 2: front right
689
+ -0.5, 0, 0.5, // 3: front left
690
+ ])
691
+ normal = new Float32Array([
692
+ 0, 1, 0,
693
+ 0, 1, 0,
694
+ 0, 1, 0,
695
+ 0, 1, 0,
696
+ ])
697
+ // Use reversed winding for horizontal (CCW from above)
698
+ return new Geometry(engine, {
699
+ position,
700
+ uv: new Float32Array([
701
+ 0, 1, // back left -> top-left of texture
702
+ 1, 1, // back right -> top-right
703
+ 1, 0, // front right -> bottom-right
704
+ 0, 0, // front left -> bottom-left
705
+ ]),
706
+ normal,
707
+ indices: new Uint32Array([
708
+ 0, 3, 2, // CCW: back-left, front-left, front-right
709
+ 2, 1, 0 // CCW: front-right, back-right, back-left
710
+ ])
711
+ })
712
+ } else {
713
+ // Default to center pivot
714
+ return Geometry.billboardQuad(engine, 'center')
715
+ }
716
+
717
+ return new Geometry(engine, {
718
+ position,
719
+ uv: new Float32Array([
720
+ 0, 0, // Bottom-left: sample from v=0 (bottom of texture)
721
+ 1, 0, // Bottom-right
722
+ 1, 1, // Top-right: sample from v=1 (top of texture)
723
+ 0, 1, // Top-left
724
+ ]),
725
+ normal,
726
+ indices: new Uint32Array([
727
+ 0, 1, 2,
728
+ 2, 3, 0
729
+ ])
730
+ })
731
+ }
732
+
733
+ simplify(angleThreshold = 0.1) {
734
+ // Early return if no UV coordinates or not enough vertices
735
+ if (!this.attributes.uv || this.attributes.indices.length < 6) {
736
+ return this;
737
+ }
738
+
739
+ const positions = this.attributes.position;
740
+ const normals = this.attributes.normal;
741
+ const uvs = this.attributes.uv;
742
+ const indices = this.attributes.indices;
743
+ const colors = this.attributes.color;
744
+ const joints = this.attributes.joints;
745
+ const weights = this.attributes.weights;
746
+
747
+ let newPositions = [];
748
+ let newNormals = [];
749
+ let newUVs = [];
750
+ let newIndices = [];
751
+ let newColors = colors ? [] : null;
752
+ let newJoints = joints ? [] : null;
753
+ let newWeights = weights ? [] : null;
754
+
755
+ // Process triangles in pairs
756
+ for (let i = 0; i < indices.length; i += 6) {
757
+ if (i + 5 >= indices.length) {
758
+ // Add remaining triangle if we can't make a pair
759
+ for (let j = 0; j < 3; j++) {
760
+ const idx = indices[i + j];
761
+ newPositions.push(positions[idx * 3], positions[idx * 3 + 1], positions[idx * 3 + 2]);
762
+ newNormals.push(normals[idx * 3], normals[idx * 3 + 1], normals[idx * 3 + 2]);
763
+ newUVs.push(uvs[idx * 2], uvs[idx * 2 + 1]);
764
+ if (colors) {
765
+ newColors.push(colors[idx * 4], colors[idx * 4 + 1], colors[idx * 4 + 2], colors[idx * 4 + 3]);
766
+ }
767
+ if (joints) {
768
+ newJoints.push(joints[idx * 4], joints[idx * 4 + 1], joints[idx * 4 + 2], joints[idx * 4 + 3]);
769
+ }
770
+ if (weights) {
771
+ newWeights.push(weights[idx * 4], weights[idx * 4 + 1], weights[idx * 4 + 2], weights[idx * 4 + 3]);
772
+ }
773
+ }
774
+ newIndices.push(newPositions.length / 3 - 3, newPositions.length / 3 - 2, newPositions.length / 3 - 1);
775
+ continue;
776
+ }
777
+
778
+ // Get the two triangles
779
+ const tri1 = [indices[i], indices[i + 1], indices[i + 2]];
780
+ const tri2 = [indices[i + 3], indices[i + 4], indices[i + 5]];
781
+
782
+ // Calculate normals of both triangles
783
+ const normal1 = new Vector3(
784
+ normals[tri1[0] * 3], normals[tri1[0] * 3 + 1], normals[tri1[0] * 3 + 2]
785
+ );
786
+ const normal2 = new Vector3(
787
+ normals[tri2[0] * 3], normals[tri2[0] * 3 + 1], normals[tri2[0] * 3 + 2]
788
+ );
789
+
790
+ // Check if triangles are nearly coplanar
791
+ const angle = Math.acos(normal1.dot(normal2));
792
+
793
+ if (angle < angleThreshold) {
794
+ // Find shared vertices
795
+ const sharedVertices = tri1.filter(v => tri2.includes(v));
796
+
797
+ if (sharedVertices.length === 2) {
798
+ // Get unique vertices
799
+ const uniqueFromTri1 = tri1.find(v => !tri2.includes(v));
800
+ const uniqueFromTri2 = tri2.find(v => !tri1.includes(v));
801
+
802
+ // Create new quad from the four points
803
+ const quadIndices = [uniqueFromTri1, ...sharedVertices, uniqueFromTri2];
804
+
805
+ // Add vertices to new arrays
806
+ for (const idx of quadIndices) {
807
+ newPositions.push(positions[idx * 3], positions[idx * 3 + 1], positions[idx * 3 + 2]);
808
+ newNormals.push(normals[idx * 3], normals[idx * 3 + 1], normals[idx * 3 + 2]);
809
+ newUVs.push(uvs[idx * 2], uvs[idx * 2 + 1]);
810
+ if (colors) {
811
+ newColors.push(colors[idx * 4], colors[idx * 4 + 1], colors[idx * 4 + 2], colors[idx * 4 + 3]);
812
+ }
813
+ if (joints) {
814
+ newJoints.push(joints[idx * 4], joints[idx * 4 + 1], joints[idx * 4 + 2], joints[idx * 4 + 3]);
815
+ }
816
+ if (weights) {
817
+ newWeights.push(weights[idx * 4], weights[idx * 4 + 1], weights[idx * 4 + 2], weights[idx * 4 + 3]);
818
+ }
819
+ }
820
+
821
+ // Add indices for the simplified quad (as two triangles)
822
+ const baseIndex = newPositions.length / 3 - 4;
823
+ newIndices.push(
824
+ baseIndex, baseIndex + 1, baseIndex + 2,
825
+ baseIndex + 2, baseIndex + 3, baseIndex
826
+ );
827
+ continue;
828
+ }
829
+ }
830
+
831
+ // If we can't simplify, add original triangles
832
+ for (let j = 0; j < 6; j++) {
833
+ const idx = indices[i + j];
834
+ newPositions.push(positions[idx * 3], positions[idx * 3 + 1], positions[idx * 3 + 2]);
835
+ newNormals.push(normals[idx * 3], normals[idx * 3 + 1], normals[idx * 3 + 2]);
836
+ newUVs.push(uvs[idx * 2], uvs[idx * 2 + 1]);
837
+ if (colors) {
838
+ newColors.push(colors[idx * 4], colors[idx * 4 + 1], colors[idx * 4 + 2], colors[idx * 4 + 3]);
839
+ }
840
+ if (joints) {
841
+ newJoints.push(joints[idx * 4], joints[idx * 4 + 1], joints[idx * 4 + 2], joints[idx * 4 + 3]);
842
+ }
843
+ if (weights) {
844
+ newWeights.push(weights[idx * 4], weights[idx * 4 + 1], weights[idx * 4 + 2], weights[idx * 4 + 3]);
845
+ }
846
+ newIndices.push(newPositions.length / 3 - 1);
847
+ }
848
+ }
849
+
850
+ const geometry = {
851
+ position: new Float32Array(newPositions),
852
+ normal: new Float32Array(newNormals),
853
+ uv: new Float32Array(newUVs),
854
+ index: new Uint32Array(newIndices)
855
+ };
856
+
857
+ if (colors) {
858
+ geometry.color = new Float32Array(newColors);
859
+ }
860
+ if (joints) {
861
+ geometry.joints = new Uint32Array(newJoints);
862
+ }
863
+ if (weights) {
864
+ geometry.weights = new Float32Array(newWeights);
865
+ }
866
+
867
+ return new Geometry(geometry);
868
+ }
869
+
870
+ static unrepetizeUVs(geometry) {
871
+ const positions = geometry.attributes.position;
872
+ const normals = geometry.attributes.normal;
873
+ const uvs = geometry.attributes.uv;
874
+ const indices = geometry.attributes.indices;
875
+ const colors = geometry.attributes.color;
876
+ const joints = geometry.attributes.joints;
877
+ const weights = geometry.attributes.weights;
878
+
879
+ const newPositions = [];
880
+ const newNormals = [];
881
+ const newUVs = [];
882
+ const newIndices = [];
883
+ const newColors = colors ? [] : null;
884
+ const newJoints = joints ? [] : null;
885
+ const newWeights = weights ? [] : null;
886
+
887
+ // Process each triangle
888
+ for (let i = 0; i < indices.length; i += 3) {
889
+ const tri = [indices[i], indices[i + 1], indices[i + 2]];
890
+
891
+ // Get UV coordinates for the triangle
892
+ const uvCoords = tri.map(idx => [uvs[idx * 2], uvs[idx * 2 + 1]]);
893
+
894
+ // Get integer and fractional parts of UVs
895
+ const uvInts = uvCoords.map(uv => [
896
+ Math.floor(Math.abs(uv[0])),
897
+ Math.floor(Math.abs(uv[1]))
898
+ ]);
899
+ const uvFracs = uvCoords.map(uv => [
900
+ Math.abs(uv[0]) % 1,
901
+ Math.abs(uv[1]) % 1
902
+ ]);
903
+
904
+ // Find max UV integer values to determine grid size
905
+ const maxU = Math.max(...uvInts.map(uv => uv[0]));
906
+ const maxV = Math.max(...uvInts.map(uv => uv[1]));
907
+
908
+ if (maxU > 0 || maxV > 0) {
909
+ // Need to split into grid
910
+ for (let u = 0; u <= maxU; u++) {
911
+ for (let v = 0; v <= maxV; v++) {
912
+ // Generate vertices for this grid cell
913
+ const cellVertices = [];
914
+
915
+ // For each vertex of original triangle
916
+ for (let j = 0; j < 3; j++) {
917
+ const origPos = [
918
+ positions[tri[j] * 3],
919
+ positions[tri[j] * 3 + 1],
920
+ positions[tri[j] * 3 + 2]
921
+ ];
922
+ const origNorm = [
923
+ normals[tri[j] * 3],
924
+ normals[tri[j] * 3 + 1],
925
+ normals[tri[j] * 3 + 2]
926
+ ];
927
+
928
+ // Calculate UV for this cell
929
+ const cellUV = [
930
+ (uvFracs[j][0] + u) / (maxU + 1),
931
+ (uvFracs[j][1] + v) / (maxV + 1)
932
+ ];
933
+
934
+ const idx = newPositions.length / 3;
935
+ newPositions.push(...origPos);
936
+ newNormals.push(...origNorm);
937
+ newUVs.push(...cellUV);
938
+
939
+ if (colors) {
940
+ newColors.push(
941
+ colors[tri[j] * 4],
942
+ colors[tri[j] * 4 + 1],
943
+ colors[tri[j] * 4 + 2],
944
+ colors[tri[j] * 4 + 3]
945
+ );
946
+ }
947
+ if (joints) {
948
+ newJoints.push(
949
+ joints[tri[j] * 4],
950
+ joints[tri[j] * 4 + 1],
951
+ joints[tri[j] * 4 + 2],
952
+ joints[tri[j] * 4 + 3]
953
+ );
954
+ }
955
+ if (weights) {
956
+ newWeights.push(
957
+ weights[tri[j] * 4],
958
+ weights[tri[j] * 4 + 1],
959
+ weights[tri[j] * 4 + 2],
960
+ weights[tri[j] * 4 + 3]
961
+ );
962
+ }
963
+
964
+ cellVertices.push(idx);
965
+ }
966
+
967
+ // Add triangle indices for this cell
968
+ newIndices.push(
969
+ cellVertices[0],
970
+ cellVertices[1],
971
+ cellVertices[2]
972
+ );
973
+ }
974
+ }
975
+
976
+ } else {
977
+ // Triangle doesn't need splitting, add as-is with normalized UVs
978
+ for (let j = 0; j < 3; j++) {
979
+ const idx = tri[j];
980
+ newPositions.push(
981
+ positions[idx * 3],
982
+ positions[idx * 3 + 1],
983
+ positions[idx * 3 + 2]
984
+ );
985
+ newNormals.push(
986
+ normals[idx * 3],
987
+ normals[idx * 3 + 1],
988
+ normals[idx * 3 + 2]
989
+ );
990
+ newUVs.push(
991
+ uvFracs[j][0],
992
+ uvFracs[j][1]
993
+ );
994
+
995
+ if (colors) {
996
+ newColors.push(
997
+ colors[idx * 4],
998
+ colors[idx * 4 + 1],
999
+ colors[idx * 4 + 2],
1000
+ colors[idx * 4 + 3]
1001
+ );
1002
+ }
1003
+ if (joints) {
1004
+ newJoints.push(
1005
+ joints[idx * 4],
1006
+ joints[idx * 4 + 1],
1007
+ joints[idx * 4 + 2],
1008
+ joints[idx * 4 + 3]
1009
+ );
1010
+ }
1011
+ if (weights) {
1012
+ newWeights.push(
1013
+ weights[idx * 4],
1014
+ weights[idx * 4 + 1],
1015
+ weights[idx * 4 + 2],
1016
+ weights[idx * 4 + 3]
1017
+ );
1018
+ }
1019
+ }
1020
+
1021
+ const baseIdx = newPositions.length / 3 - 3;
1022
+ newIndices.push(baseIdx, baseIdx + 1, baseIdx + 2);
1023
+ }
1024
+ }
1025
+
1026
+ const result = {
1027
+ position: new Float32Array(newPositions),
1028
+ normal: new Float32Array(newNormals),
1029
+ uv: new Float32Array(newUVs),
1030
+ index: new Uint32Array(newIndices)
1031
+ };
1032
+
1033
+ if (colors) {
1034
+ result.color = new Float32Array(newColors);
1035
+ }
1036
+ if (joints) {
1037
+ result.joints = new Uint32Array(newJoints);
1038
+ }
1039
+ if (weights) {
1040
+ result.weights = new Float32Array(newWeights);
1041
+ }
1042
+
1043
+ return new Geometry(result);
1044
+ }
1045
+
1046
+ }
1047
+
1048
+
1049
+ export { Geometry }