topazcube 0.1.31 → 0.1.35

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 (103) hide show
  1. package/LICENSE.txt +0 -0
  2. package/README.md +0 -0
  3. package/dist/Renderer.cjs +20844 -0
  4. package/dist/Renderer.cjs.map +1 -0
  5. package/dist/Renderer.js +20827 -0
  6. package/dist/Renderer.js.map +1 -0
  7. package/dist/client.cjs +91 -260
  8. package/dist/client.cjs.map +1 -1
  9. package/dist/client.js +68 -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} +170 -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 +703 -0
  33. package/src/renderer/Geometry.js +1049 -0
  34. package/src/renderer/Material.js +64 -0
  35. package/src/renderer/Mesh.js +211 -0
  36. package/src/renderer/Node.js +112 -0
  37. package/src/renderer/Pipeline.js +645 -0
  38. package/src/renderer/Renderer.js +1496 -0
  39. package/src/renderer/Skin.js +792 -0
  40. package/src/renderer/Texture.js +584 -0
  41. package/src/renderer/core/AssetManager.js +394 -0
  42. package/src/renderer/core/CullingSystem.js +308 -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 +563 -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 +2258 -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 +420 -0
  58. package/src/renderer/rendering/passes/CRTPass.js +724 -0
  59. package/src/renderer/rendering/passes/FogPass.js +445 -0
  60. package/src/renderer/rendering/passes/GBufferPass.js +730 -0
  61. package/src/renderer/rendering/passes/HiZPass.js +744 -0
  62. package/src/renderer/rendering/passes/LightingPass.js +753 -0
  63. package/src/renderer/rendering/passes/ParticlePass.js +841 -0
  64. package/src/renderer/rendering/passes/PlanarReflectionPass.js +456 -0
  65. package/src/renderer/rendering/passes/PostProcessPass.js +405 -0
  66. package/src/renderer/rendering/passes/ReflectionPass.js +157 -0
  67. package/src/renderer/rendering/passes/RenderPostPass.js +364 -0
  68. package/src/renderer/rendering/passes/SSGIPass.js +266 -0
  69. package/src/renderer/rendering/passes/SSGITilePass.js +305 -0
  70. package/src/renderer/rendering/passes/ShadowPass.js +2072 -0
  71. package/src/renderer/rendering/passes/TransparentPass.js +831 -0
  72. package/src/renderer/rendering/passes/VolumetricFogPass.js +715 -0
  73. package/src/renderer/rendering/shaders/ao.wgsl +182 -0
  74. package/src/renderer/rendering/shaders/bloom.wgsl +97 -0
  75. package/src/renderer/rendering/shaders/bloom_blur.wgsl +80 -0
  76. package/src/renderer/rendering/shaders/crt.wgsl +455 -0
  77. package/src/renderer/rendering/shaders/depth_copy.wgsl +17 -0
  78. package/src/renderer/rendering/shaders/geometry.wgsl +580 -0
  79. package/src/renderer/rendering/shaders/hiz_reduce.wgsl +114 -0
  80. package/src/renderer/rendering/shaders/light_culling.wgsl +204 -0
  81. package/src/renderer/rendering/shaders/lighting.wgsl +932 -0
  82. package/src/renderer/rendering/shaders/lighting_common.wgsl +143 -0
  83. package/src/renderer/rendering/shaders/particle_render.wgsl +672 -0
  84. package/src/renderer/rendering/shaders/particle_simulate.wgsl +440 -0
  85. package/src/renderer/rendering/shaders/postproc.wgsl +293 -0
  86. package/src/renderer/rendering/shaders/render_post.wgsl +289 -0
  87. package/src/renderer/rendering/shaders/shadow.wgsl +117 -0
  88. package/src/renderer/rendering/shaders/ssgi.wgsl +266 -0
  89. package/src/renderer/rendering/shaders/ssgi_accumulate.wgsl +114 -0
  90. package/src/renderer/rendering/shaders/ssgi_propagate.wgsl +132 -0
  91. package/src/renderer/rendering/shaders/volumetric_blur.wgsl +80 -0
  92. package/src/renderer/rendering/shaders/volumetric_composite.wgsl +80 -0
  93. package/src/renderer/rendering/shaders/volumetric_raymarch.wgsl +634 -0
  94. package/src/renderer/utils/BoundingSphere.js +439 -0
  95. package/src/renderer/utils/Frustum.js +281 -0
  96. package/src/renderer/utils/Raycaster.js +761 -0
  97. package/dist/client.d.cts +0 -211
  98. package/dist/client.d.ts +0 -211
  99. package/dist/server.d.cts +0 -120
  100. package/dist/server.d.ts +0 -120
  101. package/dist/terminal.d.cts +0 -64
  102. package/dist/terminal.d.ts +0 -64
  103. package/src/utils.ts +0 -403
@@ -0,0 +1,439 @@
1
+ import { vec3 } from "../math.js"
2
+
3
+ /**
4
+ * Calculate a bounding sphere from position data
5
+ * Uses Ritter's bounding sphere algorithm for a good approximation
6
+ *
7
+ * @param {Float32Array|Array} positions - Position data (x, y, z, x, y, z, ...)
8
+ * @returns {{ center: [number, number, number], radius: number }}
9
+ */
10
+ function calculateBoundingSphere(positions) {
11
+ if (!positions || positions.length < 3) {
12
+ return { center: [0, 0, 0], radius: 0 }
13
+ }
14
+
15
+ const vertexCount = Math.floor(positions.length / 3)
16
+
17
+ // Step 1: Find the centroid (average of all points)
18
+ let cx = 0, cy = 0, cz = 0
19
+ for (let i = 0; i < vertexCount; i++) {
20
+ cx += positions[i * 3]
21
+ cy += positions[i * 3 + 1]
22
+ cz += positions[i * 3 + 2]
23
+ }
24
+ cx /= vertexCount
25
+ cy /= vertexCount
26
+ cz /= vertexCount
27
+
28
+ // Step 2: Find the point farthest from the centroid
29
+ let maxDistSq = 0
30
+ let farthestIdx = 0
31
+ for (let i = 0; i < vertexCount; i++) {
32
+ const dx = positions[i * 3] - cx
33
+ const dy = positions[i * 3 + 1] - cy
34
+ const dz = positions[i * 3 + 2] - cz
35
+ const distSq = dx * dx + dy * dy + dz * dz
36
+ if (distSq > maxDistSq) {
37
+ maxDistSq = distSq
38
+ farthestIdx = i
39
+ }
40
+ }
41
+
42
+ // Step 3: Find the point farthest from that point
43
+ let p1x = positions[farthestIdx * 3]
44
+ let p1y = positions[farthestIdx * 3 + 1]
45
+ let p1z = positions[farthestIdx * 3 + 2]
46
+
47
+ maxDistSq = 0
48
+ let oppositeIdx = 0
49
+ for (let i = 0; i < vertexCount; i++) {
50
+ const dx = positions[i * 3] - p1x
51
+ const dy = positions[i * 3 + 1] - p1y
52
+ const dz = positions[i * 3 + 2] - p1z
53
+ const distSq = dx * dx + dy * dy + dz * dz
54
+ if (distSq > maxDistSq) {
55
+ maxDistSq = distSq
56
+ oppositeIdx = i
57
+ }
58
+ }
59
+
60
+ let p2x = positions[oppositeIdx * 3]
61
+ let p2y = positions[oppositeIdx * 3 + 1]
62
+ let p2z = positions[oppositeIdx * 3 + 2]
63
+
64
+ // Step 4: Initial sphere from these two extreme points
65
+ let sphereCx = (p1x + p2x) * 0.5
66
+ let sphereCy = (p1y + p2y) * 0.5
67
+ let sphereCz = (p1z + p2z) * 0.5
68
+ let radius = Math.sqrt(maxDistSq) * 0.5
69
+
70
+ // Step 5: Ritter's expansion - grow sphere to include all points
71
+ for (let i = 0; i < vertexCount; i++) {
72
+ const px = positions[i * 3]
73
+ const py = positions[i * 3 + 1]
74
+ const pz = positions[i * 3 + 2]
75
+
76
+ const dx = px - sphereCx
77
+ const dy = py - sphereCy
78
+ const dz = pz - sphereCz
79
+ const dist = Math.sqrt(dx * dx + dy * dy + dz * dz)
80
+
81
+ if (dist > radius) {
82
+ // Point is outside sphere, expand to include it
83
+ const newRadius = (radius + dist) * 0.5
84
+ const ratio = (newRadius - radius) / dist
85
+
86
+ sphereCx += dx * ratio
87
+ sphereCy += dy * ratio
88
+ sphereCz += dz * ratio
89
+ radius = newRadius
90
+ }
91
+ }
92
+
93
+ // Add a small epsilon to avoid floating-point issues
94
+ radius *= 1.001
95
+
96
+ return {
97
+ center: [sphereCx, sphereCy, sphereCz],
98
+ radius: radius
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Calculate axis-aligned bounding box from positions
104
+ * @param {Float32Array|Array} positions - Position data
105
+ * @returns {{ min: [number, number, number], max: [number, number, number] }}
106
+ */
107
+ function calculateAABB(positions) {
108
+ if (!positions || positions.length < 3) {
109
+ return { min: [0, 0, 0], max: [0, 0, 0] }
110
+ }
111
+
112
+ const vertexCount = Math.floor(positions.length / 3)
113
+
114
+ let minX = Infinity, minY = Infinity, minZ = Infinity
115
+ let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity
116
+
117
+ for (let i = 0; i < vertexCount; i++) {
118
+ const x = positions[i * 3]
119
+ const y = positions[i * 3 + 1]
120
+ const z = positions[i * 3 + 2]
121
+
122
+ if (x < minX) minX = x
123
+ if (y < minY) minY = y
124
+ if (z < minZ) minZ = z
125
+ if (x > maxX) maxX = x
126
+ if (y > maxY) maxY = y
127
+ if (z > maxZ) maxZ = z
128
+ }
129
+
130
+ return {
131
+ min: [minX, minY, minZ],
132
+ max: [maxX, maxY, maxZ]
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Create bounding sphere from AABB
138
+ */
139
+ function boundingSphereFromAABB(aabb) {
140
+ const center = [
141
+ (aabb.min[0] + aabb.max[0]) * 0.5,
142
+ (aabb.min[1] + aabb.max[1]) * 0.5,
143
+ (aabb.min[2] + aabb.max[2]) * 0.5
144
+ ]
145
+
146
+ const dx = aabb.max[0] - aabb.min[0]
147
+ const dy = aabb.max[1] - aabb.min[1]
148
+ const dz = aabb.max[2] - aabb.min[2]
149
+ const radius = Math.sqrt(dx * dx + dy * dy + dz * dz) * 0.5
150
+
151
+ return { center, radius }
152
+ }
153
+
154
+ /**
155
+ * Merge two bounding spheres
156
+ */
157
+ function mergeBoundingSpheres(a, b) {
158
+ const dx = b.center[0] - a.center[0]
159
+ const dy = b.center[1] - a.center[1]
160
+ const dz = b.center[2] - a.center[2]
161
+ const dist = Math.sqrt(dx * dx + dy * dy + dz * dz)
162
+
163
+ // Check if one sphere contains the other
164
+ if (dist + b.radius <= a.radius) {
165
+ return { center: [...a.center], radius: a.radius }
166
+ }
167
+ if (dist + a.radius <= b.radius) {
168
+ return { center: [...b.center], radius: b.radius }
169
+ }
170
+
171
+ // Calculate new sphere that contains both
172
+ const newRadius = (dist + a.radius + b.radius) * 0.5
173
+ const ratio = (newRadius - a.radius) / dist
174
+
175
+ return {
176
+ center: [
177
+ a.center[0] + dx * ratio,
178
+ a.center[1] + dy * ratio,
179
+ a.center[2] + dz * ratio
180
+ ],
181
+ radius: newRadius
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Transform a bounding sphere by a matrix
187
+ * @param {Object} bsphere - Bounding sphere
188
+ * @param {mat4} matrix - Transform matrix
189
+ * @returns {Object} Transformed bounding sphere
190
+ */
191
+ function transformBoundingSphere(bsphere, matrix) {
192
+ // Transform center
193
+ const center = vec3.create()
194
+ vec3.transformMat4(center, bsphere.center, matrix)
195
+
196
+ // Get scale factor from matrix (approximate as max axis scale)
197
+ const sx = Math.sqrt(matrix[0] * matrix[0] + matrix[1] * matrix[1] + matrix[2] * matrix[2])
198
+ const sy = Math.sqrt(matrix[4] * matrix[4] + matrix[5] * matrix[5] + matrix[6] * matrix[6])
199
+ const sz = Math.sqrt(matrix[8] * matrix[8] + matrix[9] * matrix[9] + matrix[10] * matrix[10])
200
+ const maxScale = Math.max(sx, sy, sz)
201
+
202
+ return {
203
+ center: [center[0], center[1], center[2]],
204
+ radius: bsphere.radius * maxScale
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Test if a bounding sphere is visible in a frustum
210
+ * @param {Object} bsphere - Bounding sphere { center, radius }
211
+ * @param {Float32Array} planes - Frustum planes (6 vec4s)
212
+ * @returns {boolean} True if visible
213
+ */
214
+ function sphereInFrustum(bsphere, planes) {
215
+ for (let i = 0; i < 6; i++) {
216
+ const offset = i * 4
217
+ const nx = planes[offset]
218
+ const ny = planes[offset + 1]
219
+ const nz = planes[offset + 2]
220
+ const d = planes[offset + 3]
221
+
222
+ const dist = nx * bsphere.center[0] + ny * bsphere.center[1] + nz * bsphere.center[2] + d
223
+
224
+ if (dist < -bsphere.radius) {
225
+ return false // Completely outside this plane
226
+ }
227
+ }
228
+ return true
229
+ }
230
+
231
+ /**
232
+ * Test if a bounding sphere is within a distance from a point
233
+ * @param {Object} bsphere - Bounding sphere
234
+ * @param {Array} point - Test point [x, y, z]
235
+ * @param {number} maxDistance - Maximum distance
236
+ * @returns {boolean} True if within distance
237
+ */
238
+ function sphereWithinDistance(bsphere, point, maxDistance) {
239
+ const dx = bsphere.center[0] - point[0]
240
+ const dy = bsphere.center[1] - point[1]
241
+ const dz = bsphere.center[2] - point[2]
242
+ const dist = Math.sqrt(dx * dx + dy * dy + dz * dz) - bsphere.radius
243
+ return dist <= maxDistance
244
+ }
245
+
246
+ /**
247
+ * Calculate a "shadow bounding sphere" that encompasses both the object
248
+ * and its shadow projection on the ground plane.
249
+ *
250
+ * This is used for shadow map culling optimization - we can cull objects
251
+ * whose shadow spheres are not visible to the camera (frustum/occlusion).
252
+ *
253
+ * The algorithm:
254
+ * 1. Start with the object's world-space bounding sphere
255
+ * 2. Project the sphere onto the ground plane along the light direction
256
+ * - This creates an ellipse on the ground
257
+ * 3. Calculate a bounding sphere that contains both the object sphere
258
+ * and the shadow ellipse
259
+ *
260
+ * @param {Object} bsphere - Object's bounding sphere { center: [x,y,z], radius: r }
261
+ * @param {Array} lightDir - Normalized light direction vector [x,y,z] (pointing TO light)
262
+ * @param {number} groundLevel - Y coordinate of the ground/shadow receiver plane
263
+ * @returns {Object} Shadow bounding sphere { center: [x,y,z], radius: r }
264
+ */
265
+ function calculateShadowBoundingSphere(bsphere, lightDir, groundLevel = 0) {
266
+ // Light direction should point FROM object TO light (i.e., towards the sky for sun)
267
+ // If Y component is positive, light comes from above
268
+ const lx = lightDir[0]
269
+ const ly = lightDir[1]
270
+ const lz = lightDir[2]
271
+
272
+ // Object sphere properties
273
+ const cx = bsphere.center[0]
274
+ const cy = bsphere.center[1]
275
+ const cz = bsphere.center[2]
276
+ const r = bsphere.radius
277
+
278
+ // If object is below ground level, its shadow is above it (not on ground)
279
+ // In that case, just return the original sphere with some margin
280
+ if (cy + r < groundLevel) {
281
+ return {
282
+ center: [...bsphere.center],
283
+ radius: r * 1.1
284
+ }
285
+ }
286
+
287
+ // Calculate the height of sphere center above ground
288
+ const heightAboveGround = cy - groundLevel
289
+
290
+ // If light is nearly horizontal (ly close to 0), shadow extends very far
291
+ // Clamp to a reasonable maximum shadow distance
292
+ const maxShadowDistance = 100
293
+
294
+ // Calculate shadow center on ground:
295
+ // The center of the sphere projects along light direction to ground
296
+ // shadowPos = center + t * (-lightDir) where t = heightAboveGround / ly
297
+ let shadowCenterX, shadowCenterZ
298
+
299
+ if (Math.abs(ly) < 0.01) {
300
+ // Nearly horizontal light - shadow extends very far
301
+ // Use a reasonable estimate: shadow at maxShadowDistance in light direction
302
+ const horizLen = Math.sqrt(lx * lx + lz * lz)
303
+ if (horizLen > 0.001) {
304
+ shadowCenterX = cx - (lx / horizLen) * maxShadowDistance
305
+ shadowCenterZ = cz - (lz / horizLen) * maxShadowDistance
306
+ } else {
307
+ shadowCenterX = cx
308
+ shadowCenterZ = cz
309
+ }
310
+ } else {
311
+ // Normal case: calculate projection along light ray
312
+ const t = heightAboveGround / ly
313
+ // Clamp shadow distance
314
+ const clampedT = Math.min(Math.abs(t), maxShadowDistance) * Math.sign(t)
315
+ shadowCenterX = cx - lx * clampedT
316
+ shadowCenterZ = cz - lz * clampedT
317
+ }
318
+
319
+ // The shadow of a sphere is an ellipse when light is not vertical
320
+ // The ellipse's major axis extends along the light direction
321
+ // For simplicity, we approximate the shadow as a circle with radius
322
+ // that encompasses the ellipse
323
+
324
+ // Shadow radius depends on the angle of the light
325
+ // When light is vertical (ly=1), shadow radius = object radius
326
+ // When light is at an angle, shadow stretches along the light direction
327
+ const cosAngle = Math.abs(ly)
328
+ const sinAngle = Math.sqrt(1 - cosAngle * cosAngle)
329
+
330
+ // Shadow elongation factor (how much the shadow stretches)
331
+ // When light angle is 45°, shadow is ~1.4x longer
332
+ // When light angle is 30° (from horizon), shadow is ~2x longer
333
+ const elongation = cosAngle > 0.01 ? 1 / cosAngle : maxShadowDistance / r
334
+ const clampedElongation = Math.min(elongation, maxShadowDistance / Math.max(r, 0.1))
335
+
336
+ // Shadow "radius" (bounding circle of the ellipse)
337
+ const shadowRadius = r * Math.max(1, clampedElongation)
338
+
339
+ // Now we have two spheres to encompass:
340
+ // 1. Object sphere: center=(cx, cy, cz), radius=r
341
+ // 2. Shadow "sphere" on ground: center=(shadowCenterX, groundLevel, shadowCenterZ), radius=shadowRadius
342
+
343
+ // Create a sphere that contains both
344
+ // Method: find the enclosing sphere of two spheres
345
+ const s1 = {
346
+ center: [cx, cy, cz],
347
+ radius: r
348
+ }
349
+ const s2 = {
350
+ center: [shadowCenterX, groundLevel, shadowCenterZ],
351
+ radius: shadowRadius
352
+ }
353
+
354
+ // Distance between sphere centers
355
+ const dx = s2.center[0] - s1.center[0]
356
+ const dy = s2.center[1] - s1.center[1]
357
+ const dz = s2.center[2] - s1.center[2]
358
+ const dist = Math.sqrt(dx * dx + dy * dy + dz * dz)
359
+
360
+ // Check containment
361
+ if (dist + s2.radius <= s1.radius) {
362
+ // Shadow is inside object sphere (unlikely but handle it)
363
+ return { center: [...s1.center], radius: s1.radius * 1.01 }
364
+ }
365
+ if (dist + s1.radius <= s2.radius) {
366
+ // Object is inside shadow sphere
367
+ return { center: [...s2.center], radius: s2.radius * 1.01 }
368
+ }
369
+
370
+ // Calculate enclosing sphere
371
+ const newRadius = (dist + s1.radius + s2.radius) * 0.5
372
+
373
+ // Center is along the line between sphere centers
374
+ // Position it so both spheres fit
375
+ const ratio = dist > 0.001 ? (newRadius - s1.radius) / dist : 0.5
376
+
377
+ return {
378
+ center: [
379
+ s1.center[0] + dx * ratio,
380
+ s1.center[1] + dy * ratio,
381
+ s1.center[2] + dz * ratio
382
+ ],
383
+ radius: newRadius * 1.01 // Small margin
384
+ }
385
+ }
386
+
387
+ /**
388
+ * Test if a bounding sphere intersects a cascade's orthographic box
389
+ *
390
+ * @param {Object} bsphere - Bounding sphere { center: [x,y,z], radius: r }
391
+ * @param {mat4} cascadeMatrix - Cascade's light view-projection matrix
392
+ * @returns {boolean} True if sphere intersects the cascade box
393
+ */
394
+ function sphereInCascade(bsphere, cascadeMatrix) {
395
+ // Transform sphere center to cascade clip space
396
+ const cx = bsphere.center[0]
397
+ const cy = bsphere.center[1]
398
+ const cz = bsphere.center[2]
399
+
400
+ // Apply cascade view-projection matrix to center
401
+ // clipPos = cascadeMatrix * vec4(center, 1)
402
+ const m = cascadeMatrix
403
+ const w = m[3] * cx + m[7] * cy + m[11] * cz + m[15]
404
+
405
+ if (Math.abs(w) < 0.0001) return true // Degenerate case, include it
406
+
407
+ const invW = 1.0 / w
408
+ const clipX = (m[0] * cx + m[4] * cy + m[8] * cz + m[12]) * invW
409
+ const clipY = (m[1] * cx + m[5] * cy + m[9] * cz + m[13]) * invW
410
+ const clipZ = (m[2] * cx + m[6] * cy + m[10] * cz + m[14]) * invW
411
+
412
+ // Get scale factor for radius (approximate as max axis scale)
413
+ // For orthographic projections, this is simpler
414
+ const sx = Math.sqrt(m[0] * m[0] + m[1] * m[1] + m[2] * m[2])
415
+ const sy = Math.sqrt(m[4] * m[4] + m[5] * m[5] + m[6] * m[6])
416
+ const sz = Math.sqrt(m[8] * m[8] + m[9] * m[9] + m[10] * m[10])
417
+ const maxScale = Math.max(sx, sy, sz)
418
+ const clipRadius = bsphere.radius * maxScale * invW
419
+
420
+ // NDC box is [-1, 1] for X/Y, [0, 1] for Z in WebGPU
421
+ // Check if sphere (with radius) intersects this box
422
+ if (clipX + clipRadius < -1 || clipX - clipRadius > 1) return false
423
+ if (clipY + clipRadius < -1 || clipY - clipRadius > 1) return false
424
+ if (clipZ + clipRadius < 0 || clipZ - clipRadius > 1) return false
425
+
426
+ return true
427
+ }
428
+
429
+ export {
430
+ calculateBoundingSphere,
431
+ calculateAABB,
432
+ boundingSphereFromAABB,
433
+ mergeBoundingSpheres,
434
+ transformBoundingSphere,
435
+ sphereInFrustum,
436
+ sphereWithinDistance,
437
+ calculateShadowBoundingSphere,
438
+ sphereInCascade
439
+ }