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,761 @@
1
+ /**
2
+ * Raycaster - Async ray intersection testing
3
+ *
4
+ * Performs ray-geometry intersection tests without blocking rendering.
5
+ * Uses a Web Worker for heavy triangle intersection calculations.
6
+ *
7
+ * Usage:
8
+ * const raycaster = new Raycaster(engine)
9
+ * await raycaster.initialize()
10
+ *
11
+ * raycaster.cast(origin, direction, maxDistance, (result) => {
12
+ * if (result.hit) {
13
+ * console.log('Hit:', result.entity, 'at distance:', result.distance)
14
+ * }
15
+ * })
16
+ */
17
+
18
+ import { vec3, mat4 } from "../math.js"
19
+
20
+ class Raycaster {
21
+ constructor(engine) {
22
+ this.engine = engine
23
+ this.worker = null
24
+ this._pendingCallbacks = new Map()
25
+ this._nextRequestId = 0
26
+ this._initialized = false
27
+ }
28
+
29
+ async initialize() {
30
+ // Create worker from inline code (avoids separate file issues with bundlers)
31
+ const workerCode = this._getWorkerCode()
32
+ const blob = new Blob([workerCode], { type: 'application/javascript' })
33
+ const workerUrl = URL.createObjectURL(blob)
34
+
35
+ this.worker = new Worker(workerUrl)
36
+ this.worker.onmessage = this._handleWorkerMessage.bind(this)
37
+ this.worker.onerror = (e) => console.error('Raycaster worker error:', e)
38
+
39
+ this._initialized = true
40
+ URL.revokeObjectURL(workerUrl)
41
+ }
42
+
43
+ /**
44
+ * Cast a ray and get the closest intersection
45
+ * @param {Array|vec3} origin - Ray start point [x, y, z]
46
+ * @param {Array|vec3} direction - Ray direction (will be normalized)
47
+ * @param {number} maxDistance - Maximum ray length
48
+ * @param {Function} callback - Called with result: { hit, distance, point, normal, entity, mesh, triangleIndex }
49
+ * @param {Object} options - Optional settings
50
+ * @param {Array} options.entities - Specific entities to test (default: all scene entities)
51
+ * @param {Array} options.meshes - Specific meshes to test (default: all scene meshes)
52
+ * @param {boolean} options.backfaces - Test backfaces (default: false)
53
+ * @param {Array} options.exclude - Entities/meshes to exclude
54
+ */
55
+ cast(origin, direction, maxDistance, callback, options = {}) {
56
+ if (!this._initialized) {
57
+ console.warn('Raycaster not initialized')
58
+ callback({ hit: false, error: 'not initialized' })
59
+ return
60
+ }
61
+
62
+ const ray = {
63
+ origin: Array.from(origin),
64
+ direction: this._normalize(Array.from(direction)),
65
+ maxDistance
66
+ }
67
+
68
+ // Collect candidates from scene
69
+ const candidates = this._collectCandidates(ray, options)
70
+
71
+ if (candidates.length === 0) {
72
+ callback({ hit: false })
73
+ return
74
+ }
75
+
76
+ // Send to worker for triangle intersection
77
+ const requestId = this._nextRequestId++
78
+ this._pendingCallbacks.set(requestId, { callback, candidates })
79
+
80
+ this.worker.postMessage({
81
+ type: 'raycast',
82
+ requestId,
83
+ ray,
84
+ debug: options.debug ?? false,
85
+ candidates: candidates.map(c => ({
86
+ id: c.id,
87
+ vertices: c.vertices,
88
+ indices: c.indices,
89
+ matrix: c.matrix,
90
+ backfaces: options.backfaces ?? false
91
+ }))
92
+ })
93
+ }
94
+
95
+ /**
96
+ * Cast a ray upward from a position to check for sky visibility
97
+ * Useful for determining if camera is under cover
98
+ * @param {Array|vec3} position - Position to test from
99
+ * @param {number} maxDistance - How far to check (default: 100)
100
+ * @param {Function} callback - Called with { hitSky: boolean, distance?: number, entity?: object }
101
+ */
102
+ castToSky(position, maxDistance, callback) {
103
+ this.cast(
104
+ position,
105
+ [0, 1, 0], // Straight up
106
+ maxDistance ?? 100,
107
+ (result) => {
108
+ callback({
109
+ hitSky: !result.hit,
110
+ distance: result.distance,
111
+ entity: result.entity,
112
+ mesh: result.mesh
113
+ })
114
+ }
115
+ )
116
+ }
117
+
118
+ /**
119
+ * Cast a ray from screen coordinates (mouse picking)
120
+ * @param {number} screenX - Screen X coordinate
121
+ * @param {number} screenY - Screen Y coordinate
122
+ * @param {Object} camera - Camera with projection/view matrices
123
+ * @param {Function} callback - Called with intersection result
124
+ * @param {Object} options - Cast options
125
+ */
126
+ castFromScreen(screenX, screenY, camera, callback, options = {}) {
127
+ const { width, height } = this.engine.canvas
128
+
129
+ // Convert screen to NDC
130
+ const ndcX = (screenX / width) * 2 - 1
131
+ const ndcY = 1 - (screenY / height) * 2 // Flip Y
132
+
133
+ // Unproject to world space
134
+ const invViewProj = mat4.create()
135
+ mat4.multiply(invViewProj, camera.proj, camera.view)
136
+ mat4.invert(invViewProj, invViewProj)
137
+
138
+ // Near and far points
139
+ const nearPoint = this._unproject([ndcX, ndcY, 0], invViewProj)
140
+ const farPoint = this._unproject([ndcX, ndcY, 1], invViewProj)
141
+
142
+ // Ray direction
143
+ const direction = [
144
+ farPoint[0] - nearPoint[0],
145
+ farPoint[1] - nearPoint[1],
146
+ farPoint[2] - nearPoint[2]
147
+ ]
148
+
149
+ const maxDistance = options.maxDistance ?? camera.far ?? 1000
150
+
151
+ this.cast(nearPoint, direction, maxDistance, callback, options)
152
+ }
153
+
154
+ /**
155
+ * Collect candidate geometries that pass bounding sphere test
156
+ */
157
+ _collectCandidates(ray, options) {
158
+ const candidates = []
159
+ const exclude = new Set(options.exclude ?? [])
160
+ const debug = options.debug
161
+
162
+ // Test entities - entities reference models via string ID, geometry is in asset
163
+ const entities = options.entities ?? this._getAllEntities()
164
+ const assetManager = this.engine.assetManager
165
+
166
+ for (const entity of entities) {
167
+ if (exclude.has(entity)) continue
168
+ if (!entity.model) continue
169
+
170
+ // Get geometry from asset manager
171
+ const asset = assetManager?.get(entity.model)
172
+ if (!asset?.geometry) continue
173
+
174
+ // Entity has world-space bsphere in _bsphere
175
+ const bsphere = this._getEntityBoundingSphere(entity)
176
+ if (!bsphere) continue
177
+
178
+ if (this._raySphereIntersect(ray, bsphere)) {
179
+ const geometryData = this._extractGeometry(asset.geometry)
180
+ if (geometryData) {
181
+ const matrix = entity._matrix ?? mat4.create()
182
+ candidates.push({
183
+ id: entity.id ?? entity.name ?? `entity_${candidates.length}`,
184
+ type: 'entity',
185
+ entity,
186
+ asset,
187
+ vertices: geometryData.vertices,
188
+ indices: geometryData.indices,
189
+ matrix: Array.from(matrix),
190
+ bsphereDistance: this._raySphereDistance(ray, bsphere)
191
+ })
192
+ }
193
+ }
194
+ }
195
+
196
+ // Test standalone meshes
197
+ const meshes = options.meshes ?? this._getAllMeshes()
198
+ let debugStats = debug ? { total: 0, noGeom: 0, noBsphere: 0, noData: 0, sphereMiss: 0, candidates: 0 } : null
199
+
200
+ for (const [name, mesh] of Object.entries(meshes)) {
201
+ if (exclude.has(mesh)) continue
202
+ if (!mesh.geometry) {
203
+ if (debug) debugStats.noGeom++
204
+ continue
205
+ }
206
+
207
+ const bsphere = this._getMeshBoundingSphere(mesh)
208
+ if (!bsphere) {
209
+ if (debug) debugStats.noBsphere++
210
+ continue
211
+ }
212
+
213
+ const geometryData = this._extractGeometry(mesh.geometry)
214
+ if (!geometryData) {
215
+ if (debug) debugStats.noData++
216
+ continue
217
+ }
218
+
219
+ if (debug) debugStats.total++
220
+
221
+ // For instanced meshes, test each instance
222
+ // Static meshes keep their instanceCount; dynamic meshes may have it reset mid-frame
223
+ let instanceCount = mesh.geometry.instanceCount ?? 0
224
+
225
+ // For meshes with instanceCount=0, still test if they have instance data
226
+ // (transparent/static meshes may have valid transforms even when instanceCount is 0)
227
+ if (instanceCount === 0) {
228
+ if (mesh.geometry.instanceData) {
229
+ // Use maxInstances for static meshes (instance data persists)
230
+ // For dynamic meshes, test at least 1 instance
231
+ instanceCount = mesh.static ? (mesh.geometry.maxInstances ?? 1) : 1
232
+ } else {
233
+ // Non-instanced mesh - test with identity matrix
234
+ instanceCount = 1
235
+ }
236
+ }
237
+
238
+ for (let i = 0; i < instanceCount; i++) {
239
+ const matrix = this._getInstanceMatrix(mesh.geometry, i)
240
+ const instanceBsphere = this._transformBoundingSphere(bsphere, matrix)
241
+
242
+ if (this._raySphereIntersect(ray, instanceBsphere)) {
243
+ if (debug) debugStats.candidates++
244
+ candidates.push({
245
+ id: `${name}_${i}`,
246
+ type: 'mesh',
247
+ mesh,
248
+ meshName: name,
249
+ instanceIndex: i,
250
+ vertices: geometryData.vertices,
251
+ indices: geometryData.indices,
252
+ matrix: Array.from(matrix),
253
+ bsphereDistance: this._raySphereDistance(ray, instanceBsphere)
254
+ })
255
+ } else {
256
+ if (debug) debugStats.sphereMiss++
257
+ }
258
+ }
259
+ }
260
+
261
+ if (debug && debugStats) {
262
+ console.log(`Raycaster: meshes=${debugStats.total}, sphereHit=${debugStats.candidates}, sphereMiss=${debugStats.sphereMiss}`)
263
+ // Show which candidates passed bounding sphere test
264
+ if (candidates.length > 0 && candidates.length < 50) {
265
+ const candInfo = candidates.map(c => {
266
+ const m = c.matrix
267
+ const pos = [m[12], m[13], m[14]] // Translation from matrix
268
+ return `${c.id}@[${pos.map(v=>v.toFixed(1)).join(',')}]`
269
+ }).join(', ')
270
+ console.log(`Candidates: ${candInfo}`)
271
+ }
272
+ }
273
+
274
+ // Sort by bounding sphere distance (closest first) for early termination
275
+ candidates.sort((a, b) => a.bsphereDistance - b.bsphereDistance)
276
+
277
+ return candidates
278
+ }
279
+
280
+ _getAllEntities() {
281
+ // Get entities from engine's entity manager
282
+ // engine.entities is a plain object { id: entity }
283
+ const entities = this.engine.entities
284
+ if (!entities) return []
285
+ return Object.values(entities)
286
+ }
287
+
288
+ _getAllMeshes() {
289
+ // Get meshes from engine
290
+ return this.engine.meshes ?? {}
291
+ }
292
+
293
+ _getEntityBoundingSphere(entity) {
294
+ // Entities have pre-calculated _bsphere in world space
295
+ if (entity._bsphere && entity._bsphere.radius > 0) {
296
+ return {
297
+ center: Array.from(entity._bsphere.center),
298
+ radius: entity._bsphere.radius
299
+ }
300
+ }
301
+
302
+ // Fallback to mesh geometry bounding sphere
303
+ const geometry = entity.mesh?.geometry
304
+ if (!geometry) return null
305
+
306
+ const localBsphere = geometry.getBoundingSphere?.()
307
+ if (!localBsphere || localBsphere.radius <= 0) return null
308
+
309
+ // Transform by entity matrix
310
+ const matrix = entity._matrix ?? entity.matrix ?? mat4.create()
311
+ return this._transformBoundingSphere(localBsphere, matrix)
312
+ }
313
+
314
+ _getMeshBoundingSphere(mesh) {
315
+ const geometry = mesh.geometry
316
+ if (!geometry) return null
317
+
318
+ return geometry.getBoundingSphere?.() ?? null
319
+ }
320
+
321
+ _transformBoundingSphere(bsphere, matrix) {
322
+ // Transform center
323
+ const center = vec3.create()
324
+ vec3.transformMat4(center, bsphere.center, matrix)
325
+
326
+ // Scale radius by max scale factor
327
+ const scaleX = Math.sqrt(matrix[0]*matrix[0] + matrix[1]*matrix[1] + matrix[2]*matrix[2])
328
+ const scaleY = Math.sqrt(matrix[4]*matrix[4] + matrix[5]*matrix[5] + matrix[6]*matrix[6])
329
+ const scaleZ = Math.sqrt(matrix[8]*matrix[8] + matrix[9]*matrix[9] + matrix[10]*matrix[10])
330
+ const maxScale = Math.max(scaleX, scaleY, scaleZ)
331
+
332
+ return {
333
+ center: Array.from(center),
334
+ radius: bsphere.radius * maxScale
335
+ }
336
+ }
337
+
338
+ _getInstanceMatrix(geometry, instanceIndex) {
339
+ // Always try to read from instanceData - transforms are stored there even for single instances
340
+ if (!geometry.instanceData) {
341
+ return mat4.create()
342
+ }
343
+
344
+ const stride = 28 // floats per instance (matrix + posRadius + uvTransform + color)
345
+ const offset = instanceIndex * stride
346
+
347
+ // Check if we have data at this offset
348
+ if (offset + 16 > geometry.instanceData.length) {
349
+ return mat4.create()
350
+ }
351
+
352
+ const matrix = mat4.create()
353
+
354
+ // Copy 16 floats for matrix
355
+ for (let i = 0; i < 16; i++) {
356
+ matrix[i] = geometry.instanceData[offset + i]
357
+ }
358
+
359
+ return matrix
360
+ }
361
+
362
+ _extractGeometry(geometry) {
363
+ // Get vertex positions from CPU arrays if available
364
+ if (!geometry.vertexArray || !geometry.indexArray) {
365
+ return null
366
+ }
367
+
368
+ // Extract positions (assuming stride of 20 floats: pos(3) + uv(2) + normal(3) + color(4) + weights(4) + joints(4))
369
+ const stride = 20 // floats per vertex
370
+ const vertexCount = geometry.vertexArray.length / stride
371
+ const vertices = new Float32Array(vertexCount * 3)
372
+
373
+ for (let i = 0; i < vertexCount; i++) {
374
+ vertices[i * 3] = geometry.vertexArray[i * stride]
375
+ vertices[i * 3 + 1] = geometry.vertexArray[i * stride + 1]
376
+ vertices[i * 3 + 2] = geometry.vertexArray[i * stride + 2]
377
+ }
378
+
379
+ return {
380
+ vertices,
381
+ indices: geometry.indexArray
382
+ }
383
+ }
384
+
385
+ /**
386
+ * Ray-sphere intersection test
387
+ * Returns true if ray intersects sphere within maxDistance
388
+ * Handles case where ray origin is inside the sphere
389
+ */
390
+ _raySphereIntersect(ray, sphere) {
391
+ // Vector from sphere center to ray origin
392
+ const oc = [
393
+ ray.origin[0] - sphere.center[0],
394
+ ray.origin[1] - sphere.center[1],
395
+ ray.origin[2] - sphere.center[2]
396
+ ]
397
+
398
+ // Check if we're inside the sphere
399
+ const distToCenter = Math.sqrt(oc[0]*oc[0] + oc[1]*oc[1] + oc[2]*oc[2])
400
+ if (distToCenter < sphere.radius) {
401
+ // Inside sphere - ray will definitely exit through it
402
+ // Just check if exit point is within maxDistance
403
+ // Exit distance is approximately radius - distToCenter (simplified)
404
+ return true
405
+ }
406
+
407
+ const a = this._dot(ray.direction, ray.direction)
408
+ const b = 2.0 * this._dot(oc, ray.direction)
409
+ const c = this._dot(oc, oc) - sphere.radius * sphere.radius
410
+ const discriminant = b * b - 4 * a * c
411
+
412
+ if (discriminant < 0) return false
413
+
414
+ const sqrtDisc = Math.sqrt(discriminant)
415
+ const t1 = (-b - sqrtDisc) / (2.0 * a)
416
+ const t2 = (-b + sqrtDisc) / (2.0 * a)
417
+
418
+ // Check if either intersection is within valid range [0, maxDistance]
419
+ if (t1 >= 0 && t1 <= ray.maxDistance) return true
420
+ if (t2 >= 0 && t2 <= ray.maxDistance) return true
421
+
422
+ return false
423
+ }
424
+
425
+ /**
426
+ * Get distance to sphere along ray (for sorting)
427
+ */
428
+ _raySphereDistance(ray, sphere) {
429
+ const oc = [
430
+ ray.origin[0] - sphere.center[0],
431
+ ray.origin[1] - sphere.center[1],
432
+ ray.origin[2] - sphere.center[2]
433
+ ]
434
+
435
+ const a = this._dot(ray.direction, ray.direction)
436
+ const b = 2.0 * this._dot(oc, ray.direction)
437
+ const c = this._dot(oc, oc) - sphere.radius * sphere.radius
438
+ const discriminant = b * b - 4 * a * c
439
+
440
+ if (discriminant < 0) return Infinity
441
+
442
+ const t = (-b - Math.sqrt(discriminant)) / (2.0 * a)
443
+ return Math.max(0, t)
444
+ }
445
+
446
+ _handleWorkerMessage(event) {
447
+ const { type, requestId, result } = event.data
448
+
449
+ if (type === 'raycastResult') {
450
+ const pending = this._pendingCallbacks.get(requestId)
451
+ if (pending) {
452
+ this._pendingCallbacks.delete(requestId)
453
+
454
+ // Enrich result with original entity/mesh references
455
+ if (result.hit && pending.candidates) {
456
+ const candidate = pending.candidates.find(c => c.id === result.candidateId)
457
+ if (candidate) {
458
+ result.entity = candidate.entity
459
+ result.mesh = candidate.mesh
460
+ result.meshName = candidate.meshName
461
+ result.instanceIndex = candidate.instanceIndex
462
+ }
463
+ }
464
+
465
+ pending.callback(result)
466
+ }
467
+ }
468
+ }
469
+
470
+ _normalize(v) {
471
+ const len = Math.sqrt(v[0]*v[0] + v[1]*v[1] + v[2]*v[2])
472
+ if (len === 0) return [0, 0, 1]
473
+ return [v[0]/len, v[1]/len, v[2]/len]
474
+ }
475
+
476
+ _dot(a, b) {
477
+ return a[0]*b[0] + a[1]*b[1] + a[2]*b[2]
478
+ }
479
+
480
+ /**
481
+ * Get perpendicular distance from a point to a ray
482
+ */
483
+ _pointToRayDistance(point, ray) {
484
+ // Vector from ray origin to point
485
+ const op = [
486
+ point[0] - ray.origin[0],
487
+ point[1] - ray.origin[1],
488
+ point[2] - ray.origin[2]
489
+ ]
490
+
491
+ // Project onto ray direction
492
+ const t = this._dot(op, ray.direction)
493
+
494
+ // Closest point on ray
495
+ const closest = [
496
+ ray.origin[0] + ray.direction[0] * t,
497
+ ray.origin[1] + ray.direction[1] * t,
498
+ ray.origin[2] + ray.direction[2] * t
499
+ ]
500
+
501
+ // Distance from point to closest point on ray
502
+ const dx = point[0] - closest[0]
503
+ const dy = point[1] - closest[1]
504
+ const dz = point[2] - closest[2]
505
+
506
+ return Math.sqrt(dx*dx + dy*dy + dz*dz)
507
+ }
508
+
509
+ _unproject(ndc, invViewProj) {
510
+ const x = ndc[0]
511
+ const y = ndc[1]
512
+ const z = ndc[2]
513
+
514
+ // Multiply by inverse view-projection
515
+ const w = invViewProj[3]*x + invViewProj[7]*y + invViewProj[11]*z + invViewProj[15]
516
+
517
+ return [
518
+ (invViewProj[0]*x + invViewProj[4]*y + invViewProj[8]*z + invViewProj[12]) / w,
519
+ (invViewProj[1]*x + invViewProj[5]*y + invViewProj[9]*z + invViewProj[13]) / w,
520
+ (invViewProj[2]*x + invViewProj[6]*y + invViewProj[10]*z + invViewProj[14]) / w
521
+ ]
522
+ }
523
+
524
+ /**
525
+ * Generate Web Worker code as string
526
+ */
527
+ _getWorkerCode() {
528
+ return `
529
+ // Raycaster Web Worker
530
+ // Performs triangle intersection tests off the main thread
531
+
532
+ self.onmessage = function(event) {
533
+ const { type, requestId, ray, candidates, debug } = event.data
534
+
535
+ if (type === 'raycast') {
536
+ const result = raycastTriangles(ray, candidates, debug)
537
+ self.postMessage({ type: 'raycastResult', requestId, result })
538
+ }
539
+ }
540
+
541
+ function raycastTriangles(ray, candidates, debug) {
542
+ let closestHit = null
543
+ let closestDistance = ray.maxDistance
544
+ let debugInfo = debug ? { totalTris: 0, testedCandidates: 0, scales: [] } : null
545
+
546
+ for (const candidate of candidates) {
547
+ if (debug) debugInfo.testedCandidates++
548
+ const result = testCandidate(ray, candidate, closestDistance, debug ? debugInfo : null)
549
+
550
+ if (result && result.distance < closestDistance) {
551
+ closestDistance = result.distance
552
+ closestHit = {
553
+ hit: true,
554
+ distance: result.distance,
555
+ point: result.point,
556
+ normal: result.normal,
557
+ triangleIndex: result.triangleIndex,
558
+ candidateId: candidate.id,
559
+ localT: result.localT,
560
+ scale: result.scale
561
+ }
562
+ }
563
+ }
564
+
565
+ if (debug) {
566
+ let msg = 'Worker: candidates=' + debugInfo.testedCandidates + ', triangles=' + debugInfo.totalTris
567
+ if (closestHit) {
568
+ msg += ', hit=' + closestHit.distance.toFixed(2) + ' (localT=' + closestHit.localT.toFixed(2) + ', scale=' + closestHit.scale.toFixed(2) + ')'
569
+ } else {
570
+ msg += ', hit=none'
571
+ }
572
+ console.log(msg)
573
+ }
574
+
575
+ return closestHit ?? { hit: false }
576
+ }
577
+
578
+ function testCandidate(ray, candidate, maxDistance, debugInfo) {
579
+ const { vertices, indices, matrix, backfaces } = candidate
580
+
581
+ // Compute inverse matrix for transforming ray to local space
582
+ const invMatrix = invertMatrix4(matrix)
583
+
584
+ // Transform ray to local space
585
+ const localOrigin = transformPoint(ray.origin, invMatrix)
586
+ const localDir = transformDirection(ray.direction, invMatrix)
587
+
588
+ // Calculate the scale factor of the transformation (for correct distance)
589
+ const dirScale = Math.sqrt(localDir[0]*localDir[0] + localDir[1]*localDir[1] + localDir[2]*localDir[2])
590
+ const localDirNorm = [localDir[0]/dirScale, localDir[1]/dirScale, localDir[2]/dirScale]
591
+
592
+ let closestHit = null
593
+ let closestT = maxDistance
594
+
595
+ // Test each triangle
596
+ const triangleCount = indices.length / 3
597
+ if (debugInfo) debugInfo.totalTris += triangleCount
598
+ for (let i = 0; i < triangleCount; i++) {
599
+ const i0 = indices[i * 3]
600
+ const i1 = indices[i * 3 + 1]
601
+ const i2 = indices[i * 3 + 2]
602
+
603
+ const v0 = [vertices[i0 * 3], vertices[i0 * 3 + 1], vertices[i0 * 3 + 2]]
604
+ const v1 = [vertices[i1 * 3], vertices[i1 * 3 + 1], vertices[i1 * 3 + 2]]
605
+ const v2 = [vertices[i2 * 3], vertices[i2 * 3 + 1], vertices[i2 * 3 + 2]]
606
+
607
+ const hit = rayTriangleIntersect(localOrigin, localDirNorm, v0, v1, v2, backfaces)
608
+
609
+ if (hit && hit.t > 0) {
610
+ // Transform hit point and normal back to world space
611
+ const worldPoint = transformPoint(hit.point, matrix)
612
+
613
+ // Calculate world-space distance (local t may be wrong due to matrix scale)
614
+ const worldDist = Math.sqrt(
615
+ (worldPoint[0] - ray.origin[0]) ** 2 +
616
+ (worldPoint[1] - ray.origin[1]) ** 2 +
617
+ (worldPoint[2] - ray.origin[2]) ** 2
618
+ )
619
+
620
+ if (worldDist < closestT) {
621
+ closestT = worldDist
622
+ const worldNormal = transformDirection(hit.normal, matrix)
623
+
624
+ closestHit = {
625
+ distance: worldDist,
626
+ point: worldPoint,
627
+ normal: normalize(worldNormal),
628
+ triangleIndex: i,
629
+ localT: hit.t,
630
+ scale: dirScale
631
+ }
632
+ }
633
+ }
634
+ }
635
+
636
+ return closestHit
637
+ }
638
+
639
+ // Möller–Trumbore intersection algorithm
640
+ function rayTriangleIntersect(origin, dir, v0, v1, v2, backfaces) {
641
+ const EPSILON = 0.0000001
642
+
643
+ const edge1 = sub(v1, v0)
644
+ const edge2 = sub(v2, v0)
645
+ const h = cross(dir, edge2)
646
+ const a = dot(edge1, h)
647
+
648
+ // Check if ray is parallel to triangle
649
+ if (a > -EPSILON && a < EPSILON) return null
650
+
651
+ // Check backface
652
+ if (!backfaces && a < 0) return null
653
+
654
+ const f = 1.0 / a
655
+ const s = sub(origin, v0)
656
+ const u = f * dot(s, h)
657
+
658
+ if (u < 0.0 || u > 1.0) return null
659
+
660
+ const q = cross(s, edge1)
661
+ const v = f * dot(dir, q)
662
+
663
+ if (v < 0.0 || u + v > 1.0) return null
664
+
665
+ const t = f * dot(edge2, q)
666
+
667
+ if (t > EPSILON) {
668
+ const point = [
669
+ origin[0] + dir[0] * t,
670
+ origin[1] + dir[1] * t,
671
+ origin[2] + dir[2] * t
672
+ ]
673
+ const normal = normalize(cross(edge1, edge2))
674
+ return { t, point, normal, u, v }
675
+ }
676
+
677
+ return null
678
+ }
679
+
680
+ // Matrix and vector utilities
681
+ function invertMatrix4(m) {
682
+ const inv = new Array(16)
683
+
684
+ inv[0] = m[5]*m[10]*m[15] - m[5]*m[11]*m[14] - m[9]*m[6]*m[15] + m[9]*m[7]*m[14] + m[13]*m[6]*m[11] - m[13]*m[7]*m[10]
685
+ inv[4] = -m[4]*m[10]*m[15] + m[4]*m[11]*m[14] + m[8]*m[6]*m[15] - m[8]*m[7]*m[14] - m[12]*m[6]*m[11] + m[12]*m[7]*m[10]
686
+ inv[8] = m[4]*m[9]*m[15] - m[4]*m[11]*m[13] - m[8]*m[5]*m[15] + m[8]*m[7]*m[13] + m[12]*m[5]*m[11] - m[12]*m[7]*m[9]
687
+ inv[12] = -m[4]*m[9]*m[14] + m[4]*m[10]*m[13] + m[8]*m[5]*m[14] - m[8]*m[6]*m[13] - m[12]*m[5]*m[10] + m[12]*m[6]*m[9]
688
+ inv[1] = -m[1]*m[10]*m[15] + m[1]*m[11]*m[14] + m[9]*m[2]*m[15] - m[9]*m[3]*m[14] - m[13]*m[2]*m[11] + m[13]*m[3]*m[10]
689
+ inv[5] = m[0]*m[10]*m[15] - m[0]*m[11]*m[14] - m[8]*m[2]*m[15] + m[8]*m[3]*m[14] + m[12]*m[2]*m[11] - m[12]*m[3]*m[10]
690
+ inv[9] = -m[0]*m[9]*m[15] + m[0]*m[11]*m[13] + m[8]*m[1]*m[15] - m[8]*m[3]*m[13] - m[12]*m[1]*m[11] + m[12]*m[3]*m[9]
691
+ inv[13] = m[0]*m[9]*m[14] - m[0]*m[10]*m[13] - m[8]*m[1]*m[14] + m[8]*m[2]*m[13] + m[12]*m[1]*m[10] - m[12]*m[2]*m[9]
692
+ inv[2] = m[1]*m[6]*m[15] - m[1]*m[7]*m[14] - m[5]*m[2]*m[15] + m[5]*m[3]*m[14] + m[13]*m[2]*m[7] - m[13]*m[3]*m[6]
693
+ inv[6] = -m[0]*m[6]*m[15] + m[0]*m[7]*m[14] + m[4]*m[2]*m[15] - m[4]*m[3]*m[14] - m[12]*m[2]*m[7] + m[12]*m[3]*m[6]
694
+ inv[10] = m[0]*m[5]*m[15] - m[0]*m[7]*m[13] - m[4]*m[1]*m[15] + m[4]*m[3]*m[13] + m[12]*m[1]*m[7] - m[12]*m[3]*m[5]
695
+ inv[14] = -m[0]*m[5]*m[14] + m[0]*m[6]*m[13] + m[4]*m[1]*m[14] - m[4]*m[2]*m[13] - m[12]*m[1]*m[6] + m[12]*m[2]*m[5]
696
+ inv[3] = -m[1]*m[6]*m[11] + m[1]*m[7]*m[10] + m[5]*m[2]*m[11] - m[5]*m[3]*m[10] - m[9]*m[2]*m[7] + m[9]*m[3]*m[6]
697
+ inv[7] = m[0]*m[6]*m[11] - m[0]*m[7]*m[10] - m[4]*m[2]*m[11] + m[4]*m[3]*m[10] + m[8]*m[2]*m[7] - m[8]*m[3]*m[6]
698
+ inv[11] = -m[0]*m[5]*m[11] + m[0]*m[7]*m[9] + m[4]*m[1]*m[11] - m[4]*m[3]*m[9] - m[8]*m[1]*m[7] + m[8]*m[3]*m[5]
699
+ inv[15] = m[0]*m[5]*m[10] - m[0]*m[6]*m[9] - m[4]*m[1]*m[10] + m[4]*m[2]*m[9] + m[8]*m[1]*m[6] - m[8]*m[2]*m[5]
700
+
701
+ let det = m[0]*inv[0] + m[1]*inv[4] + m[2]*inv[8] + m[3]*inv[12]
702
+ if (det === 0) return m // Return original if singular
703
+
704
+ det = 1.0 / det
705
+ for (let i = 0; i < 16; i++) inv[i] *= det
706
+
707
+ return inv
708
+ }
709
+
710
+ function transformPoint(p, m) {
711
+ const w = m[3]*p[0] + m[7]*p[1] + m[11]*p[2] + m[15]
712
+ return [
713
+ (m[0]*p[0] + m[4]*p[1] + m[8]*p[2] + m[12]) / w,
714
+ (m[1]*p[0] + m[5]*p[1] + m[9]*p[2] + m[13]) / w,
715
+ (m[2]*p[0] + m[6]*p[1] + m[10]*p[2] + m[14]) / w
716
+ ]
717
+ }
718
+
719
+ function transformDirection(d, m) {
720
+ return [
721
+ m[0]*d[0] + m[4]*d[1] + m[8]*d[2],
722
+ m[1]*d[0] + m[5]*d[1] + m[9]*d[2],
723
+ m[2]*d[0] + m[6]*d[1] + m[10]*d[2]
724
+ ]
725
+ }
726
+
727
+ function normalize(v) {
728
+ const len = Math.sqrt(v[0]*v[0] + v[1]*v[1] + v[2]*v[2])
729
+ if (len === 0) return [0, 0, 1]
730
+ return [v[0]/len, v[1]/len, v[2]/len]
731
+ }
732
+
733
+ function dot(a, b) {
734
+ return a[0]*b[0] + a[1]*b[1] + a[2]*b[2]
735
+ }
736
+
737
+ function cross(a, b) {
738
+ return [
739
+ a[1]*b[2] - a[2]*b[1],
740
+ a[2]*b[0] - a[0]*b[2],
741
+ a[0]*b[1] - a[1]*b[0]
742
+ ]
743
+ }
744
+
745
+ function sub(a, b) {
746
+ return [a[0]-b[0], a[1]-b[1], a[2]-b[2]]
747
+ }
748
+ `
749
+ }
750
+
751
+ destroy() {
752
+ if (this.worker) {
753
+ this.worker.terminate()
754
+ this.worker = null
755
+ }
756
+ this._pendingCallbacks.clear()
757
+ this._initialized = false
758
+ }
759
+ }
760
+
761
+ export { Raycaster }