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.
- package/LICENSE.txt +0 -0
- package/README.md +0 -0
- package/dist/Renderer.cjs +20844 -0
- package/dist/Renderer.cjs.map +1 -0
- package/dist/Renderer.js +20827 -0
- package/dist/Renderer.js.map +1 -0
- package/dist/client.cjs +91 -260
- package/dist/client.cjs.map +1 -1
- package/dist/client.js +68 -215
- package/dist/client.js.map +1 -1
- package/dist/server.cjs +165 -432
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +117 -370
- package/dist/server.js.map +1 -1
- package/dist/terminal.cjs +113 -200
- package/dist/terminal.cjs.map +1 -1
- package/dist/terminal.js +50 -51
- package/dist/terminal.js.map +1 -1
- package/dist/utils-CRhi1BDa.cjs +259 -0
- package/dist/utils-CRhi1BDa.cjs.map +1 -0
- package/dist/utils-D7tXt6-2.js +260 -0
- package/dist/utils-D7tXt6-2.js.map +1 -0
- package/package.json +19 -15
- package/src/{client.ts → network/client.js} +170 -403
- package/src/{compress-browser.ts → network/compress-browser.js} +2 -4
- package/src/{compress-node.ts → network/compress-node.js} +8 -14
- package/src/{server.ts → network/server.js} +229 -317
- package/src/{terminal.js → network/terminal.js} +0 -0
- package/src/{topazcube.ts → network/topazcube.js} +2 -2
- package/src/network/utils.js +375 -0
- package/src/renderer/Camera.js +191 -0
- package/src/renderer/DebugUI.js +703 -0
- package/src/renderer/Geometry.js +1049 -0
- package/src/renderer/Material.js +64 -0
- package/src/renderer/Mesh.js +211 -0
- package/src/renderer/Node.js +112 -0
- package/src/renderer/Pipeline.js +645 -0
- package/src/renderer/Renderer.js +1496 -0
- package/src/renderer/Skin.js +792 -0
- package/src/renderer/Texture.js +584 -0
- package/src/renderer/core/AssetManager.js +394 -0
- package/src/renderer/core/CullingSystem.js +308 -0
- package/src/renderer/core/EntityManager.js +541 -0
- package/src/renderer/core/InstanceManager.js +343 -0
- package/src/renderer/core/ParticleEmitter.js +358 -0
- package/src/renderer/core/ParticleSystem.js +564 -0
- package/src/renderer/core/SpriteSystem.js +349 -0
- package/src/renderer/gltf.js +563 -0
- package/src/renderer/math.js +161 -0
- package/src/renderer/rendering/HistoryBufferManager.js +333 -0
- package/src/renderer/rendering/ProbeCapture.js +1495 -0
- package/src/renderer/rendering/ReflectionProbeManager.js +352 -0
- package/src/renderer/rendering/RenderGraph.js +2258 -0
- package/src/renderer/rendering/passes/AOPass.js +308 -0
- package/src/renderer/rendering/passes/AmbientCapturePass.js +593 -0
- package/src/renderer/rendering/passes/BasePass.js +101 -0
- package/src/renderer/rendering/passes/BloomPass.js +420 -0
- package/src/renderer/rendering/passes/CRTPass.js +724 -0
- package/src/renderer/rendering/passes/FogPass.js +445 -0
- package/src/renderer/rendering/passes/GBufferPass.js +730 -0
- package/src/renderer/rendering/passes/HiZPass.js +744 -0
- package/src/renderer/rendering/passes/LightingPass.js +753 -0
- package/src/renderer/rendering/passes/ParticlePass.js +841 -0
- package/src/renderer/rendering/passes/PlanarReflectionPass.js +456 -0
- package/src/renderer/rendering/passes/PostProcessPass.js +405 -0
- package/src/renderer/rendering/passes/ReflectionPass.js +157 -0
- package/src/renderer/rendering/passes/RenderPostPass.js +364 -0
- package/src/renderer/rendering/passes/SSGIPass.js +266 -0
- package/src/renderer/rendering/passes/SSGITilePass.js +305 -0
- package/src/renderer/rendering/passes/ShadowPass.js +2072 -0
- package/src/renderer/rendering/passes/TransparentPass.js +831 -0
- package/src/renderer/rendering/passes/VolumetricFogPass.js +715 -0
- package/src/renderer/rendering/shaders/ao.wgsl +182 -0
- package/src/renderer/rendering/shaders/bloom.wgsl +97 -0
- package/src/renderer/rendering/shaders/bloom_blur.wgsl +80 -0
- package/src/renderer/rendering/shaders/crt.wgsl +455 -0
- package/src/renderer/rendering/shaders/depth_copy.wgsl +17 -0
- package/src/renderer/rendering/shaders/geometry.wgsl +580 -0
- package/src/renderer/rendering/shaders/hiz_reduce.wgsl +114 -0
- package/src/renderer/rendering/shaders/light_culling.wgsl +204 -0
- package/src/renderer/rendering/shaders/lighting.wgsl +932 -0
- package/src/renderer/rendering/shaders/lighting_common.wgsl +143 -0
- package/src/renderer/rendering/shaders/particle_render.wgsl +672 -0
- package/src/renderer/rendering/shaders/particle_simulate.wgsl +440 -0
- package/src/renderer/rendering/shaders/postproc.wgsl +293 -0
- package/src/renderer/rendering/shaders/render_post.wgsl +289 -0
- package/src/renderer/rendering/shaders/shadow.wgsl +117 -0
- package/src/renderer/rendering/shaders/ssgi.wgsl +266 -0
- package/src/renderer/rendering/shaders/ssgi_accumulate.wgsl +114 -0
- package/src/renderer/rendering/shaders/ssgi_propagate.wgsl +132 -0
- package/src/renderer/rendering/shaders/volumetric_blur.wgsl +80 -0
- package/src/renderer/rendering/shaders/volumetric_composite.wgsl +80 -0
- package/src/renderer/rendering/shaders/volumetric_raymarch.wgsl +634 -0
- package/src/renderer/utils/BoundingSphere.js +439 -0
- package/src/renderer/utils/Frustum.js +281 -0
- package/src/renderer/utils/Raycaster.js +761 -0
- package/dist/client.d.cts +0 -211
- package/dist/client.d.ts +0 -211
- package/dist/server.d.cts +0 -120
- package/dist/server.d.ts +0 -120
- package/dist/terminal.d.cts +0 -64
- package/dist/terminal.d.ts +0 -64
- 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 }
|