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,792 @@
1
+ /**
2
+ * Skin class for skeletal animation
3
+ * Manages joint hierarchy, animations, and joint matrices texture for GPU skinning
4
+ *
5
+ * Supports animation blending for smooth transitions between animations.
6
+ */
7
+ class Skin {
8
+ engine = null
9
+
10
+ constructor(engine = null, options = {}) {
11
+ this.engine = engine
12
+ this.joints = [] // Array of joint nodes
13
+ this.inverseBindMatrices = [] // Inverse bind matrices for each joint
14
+ this.jointMatrices = [] // Final joint matrices (worldMatrix * inverseBindMatrix)
15
+ this.jointData = null // Float32Array for texture upload
16
+ this.jointTexture = null // GPU texture storing joint matrices
17
+ this.animations = {} // Named animations { name: Animation }
18
+ this.currentAnimation = null // Currently playing animation name
19
+ this.time = 0 // Current animation time
20
+ this.speed = 1.0 // Playback speed multiplier
21
+ this.loop = true // Whether to loop animation
22
+ this.rootNode = null // Root node of skeleton hierarchy
23
+
24
+ // Animation blending state
25
+ this.blendFromAnimation = null // Previous animation name (during blend)
26
+ this.blendFromTime = 0 // Time in previous animation
27
+ this.blendWeight = 1.0 // Blend weight (0 = previous, 1 = current)
28
+ this.blendDuration = 0.3 // Default blend duration in seconds
29
+ this.blendElapsed = 0 // Elapsed time in current blend
30
+ this.isBlending = false // Whether currently blending
31
+
32
+ // Per-skin local transforms (for individual skins that don't share joint state)
33
+ this.localTransforms = null // Array of { position, rotation, scale } per joint
34
+ this.worldMatrices = null // Array of mat4 for world transforms
35
+ this.useLocalTransforms = false // Whether to use per-skin transforms instead of shared joints
36
+
37
+ // Previous frame joint matrices for motion vectors
38
+ this.prevJointData = null // Float32Array for previous frame
39
+ this.prevJointTexture = null // GPU texture storing previous joint matrices
40
+ this.prevJointTextureView = null // View for binding
41
+ }
42
+
43
+ /**
44
+ * Initialize the skin with joints and inverse bind matrices
45
+ * @param {Array} joints - Array of joint nodes
46
+ * @param {Float32Array} inverseBindMatrixData - Flat array of inverse bind matrices
47
+ * @param {Object} rootNode - Root node of the skeleton
48
+ */
49
+ init(joints, inverseBindMatrixData, rootNode) {
50
+ const { device } = this.engine
51
+
52
+ this.joints = joints
53
+ this.rootNode = rootNode
54
+ this.inverseBindMatrices = []
55
+ this.jointMatrices = []
56
+
57
+ // Allocate joint data array (16 floats per joint for mat4)
58
+ this.jointData = new Float32Array(joints.length * 16)
59
+
60
+ // Parse inverse bind matrices and create views for joint matrices
61
+ for (let i = 0; i < joints.length; i++) {
62
+ // Create view into inverse bind matrix data
63
+ const ibm = new Float32Array(
64
+ inverseBindMatrixData.buffer,
65
+ inverseBindMatrixData.byteOffset + i * 16 * 4,
66
+ 16
67
+ )
68
+ this.inverseBindMatrices.push(ibm)
69
+
70
+ // Create view into joint data for this joint's matrix
71
+ const jointMatrix = new Float32Array(this.jointData.buffer, i * 16 * 4, 16)
72
+ mat4.identity(jointMatrix)
73
+ this.jointMatrices.push(jointMatrix)
74
+
75
+ // Link joint to this skin
76
+ joints[i].skin = this
77
+ }
78
+
79
+ // Create GPU texture for joint matrices (4 pixels wide x numJoints high, RGBA32F)
80
+ this.jointTexture = device.createTexture({
81
+ size: [4, joints.length, 1],
82
+ format: 'rgba32float',
83
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
84
+ })
85
+
86
+ this.jointTextureView = this.jointTexture.createView()
87
+
88
+ // Create previous frame joint texture for motion vectors
89
+ this.prevJointData = new Float32Array(joints.length * 16)
90
+ this.prevJointTexture = device.createTexture({
91
+ size: [4, joints.length, 1],
92
+ format: 'rgba32float',
93
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
94
+ })
95
+ this.prevJointTextureView = this.prevJointTexture.createView()
96
+
97
+ // Initialize prevJointData with identity matrices
98
+ for (let i = 0; i < joints.length; i++) {
99
+ const offset = i * 16
100
+ // Identity matrix column-major
101
+ this.prevJointData[offset + 0] = 1; this.prevJointData[offset + 1] = 0; this.prevJointData[offset + 2] = 0; this.prevJointData[offset + 3] = 0
102
+ this.prevJointData[offset + 4] = 0; this.prevJointData[offset + 5] = 1; this.prevJointData[offset + 6] = 0; this.prevJointData[offset + 7] = 0
103
+ this.prevJointData[offset + 8] = 0; this.prevJointData[offset + 9] = 0; this.prevJointData[offset + 10] = 1; this.prevJointData[offset + 11] = 0
104
+ this.prevJointData[offset + 12] = 0; this.prevJointData[offset + 13] = 0; this.prevJointData[offset + 14] = 0; this.prevJointData[offset + 15] = 1
105
+ }
106
+
107
+ // Create sampler for joint texture (nearest filtering, no interpolation)
108
+ this.jointSampler = device.createSampler({
109
+ magFilter: 'nearest',
110
+ minFilter: 'nearest',
111
+ })
112
+ }
113
+
114
+ /**
115
+ * Add an animation to this skin
116
+ * @param {string} name - Animation name
117
+ * @param {Object} animation - Animation data { duration, channels }
118
+ */
119
+ addAnimation(name, animation) {
120
+ this.animations[name] = animation
121
+ if (!this.currentAnimation) {
122
+ this.currentAnimation = name
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Play a named animation with optional blending from current animation
128
+ * @param {string} name - Animation name
129
+ * @param {boolean} loop - Whether to loop
130
+ * @param {number} phase - Starting phase (0-1)
131
+ * @param {number} blendTime - Blend duration in seconds (0 = instant switch)
132
+ */
133
+ play(name, loop = true, phase = 0.0, blendTime = 0) {
134
+ if (!this.animations[name]) return
135
+
136
+ const anim = this.animations[name]
137
+ phase = Math.max(Math.min(phase, 1.0), 0.0)
138
+
139
+ // If we have a current animation and blend time > 0, start blending
140
+ if (blendTime > 0 && this.currentAnimation && this.currentAnimation !== name) {
141
+ this.blendFromAnimation = this.currentAnimation
142
+ this.blendFromTime = this.time
143
+ this.blendDuration = blendTime
144
+ this.blendElapsed = 0
145
+ this.blendWeight = 0 // Start fully on previous animation
146
+ this.isBlending = true
147
+ } else {
148
+ this.isBlending = false
149
+ this.blendFromAnimation = null
150
+ }
151
+
152
+ this.currentAnimation = name
153
+ this.loop = loop
154
+ this.time = phase * anim.duration
155
+ }
156
+
157
+ /**
158
+ * Transition to a new animation with blending
159
+ * @param {string} name - Target animation name
160
+ * @param {number} blendTime - Blend duration (default 0.3s)
161
+ */
162
+ blendTo(name, blendTime = 0.3) {
163
+ this.play(name, this.loop, 0, blendTime)
164
+ }
165
+
166
+ /**
167
+ * Update animation and joint matrices
168
+ * @param {number} dt - Delta time in seconds
169
+ */
170
+ update(dt) {
171
+ const { device } = this.engine
172
+
173
+ // Copy current joint data to previous BEFORE updating (for motion vectors)
174
+ if (this.prevJointData && this.jointData) {
175
+ this.prevJointData.set(this.jointData)
176
+ }
177
+
178
+ // Update animation time
179
+ this.time += dt * this.speed
180
+
181
+ // Update blend progress (only if dt > 0, otherwise assume externally managed)
182
+ if (this.isBlending && dt > 0) {
183
+ this.blendElapsed += dt
184
+ this.blendWeight = Math.min(this.blendElapsed / this.blendDuration, 1.0)
185
+
186
+ // Also advance the "from" animation time
187
+ this.blendFromTime += dt * this.speed
188
+
189
+ // Blend complete
190
+ if (this.blendWeight >= 1.0) {
191
+ this.isBlending = false
192
+ this.blendFromAnimation = null
193
+ this.blendWeight = 1.0
194
+ }
195
+ }
196
+
197
+ // Apply animations (with blending if active)
198
+ if (this.useLocalTransforms) {
199
+ this._applyAnimationToLocalTransforms(dt)
200
+ } else {
201
+ this._applyAnimationToSharedJoints()
202
+ }
203
+
204
+ // Update world matrices
205
+ if (this.useLocalTransforms) {
206
+ this._updateWorldMatricesFromLocal()
207
+ } else if (this.rootNode) {
208
+ this.rootNode.updateMatrix()
209
+ }
210
+
211
+ // Calculate final joint matrices: jointMatrix = worldMatrix * inverseBindMatrix
212
+ for (let i = 0; i < this.joints.length; i++) {
213
+ const worldMat = this.useLocalTransforms ? this.worldMatrices[i] : this.joints[i].world
214
+ const dst = this.jointMatrices[i]
215
+ mat4.multiply(dst, worldMat, this.inverseBindMatrices[i])
216
+ }
217
+
218
+ // Upload previous joint matrices to GPU (for motion vectors)
219
+ if (this.prevJointTexture && this.prevJointData) {
220
+ device.queue.writeTexture(
221
+ { texture: this.prevJointTexture },
222
+ this.prevJointData,
223
+ { bytesPerRow: 4 * 4 * 4, rowsPerImage: this.joints.length },
224
+ [4, this.joints.length, 1]
225
+ )
226
+ }
227
+
228
+ // Upload current joint matrices to GPU
229
+ device.queue.writeTexture(
230
+ { texture: this.jointTexture },
231
+ this.jointData,
232
+ { bytesPerRow: 4 * 4 * 4, rowsPerImage: this.joints.length },
233
+ [4, this.joints.length, 1]
234
+ )
235
+ }
236
+
237
+ /**
238
+ * Apply animation to shared joint objects (original behavior)
239
+ */
240
+ _applyAnimationToSharedJoints() {
241
+ if (!this.currentAnimation || !this.animations[this.currentAnimation]) return
242
+
243
+ const currentAnim = this.animations[this.currentAnimation]
244
+
245
+ // Handle looping for current animation
246
+ let currentTime = this.time
247
+ if (this.loop && currentAnim.duration > 0) {
248
+ currentTime = currentTime % currentAnim.duration
249
+ } else {
250
+ currentTime = Math.min(currentTime, currentAnim.duration)
251
+ }
252
+
253
+ if (this.isBlending && this.blendFromAnimation && this.animations[this.blendFromAnimation]) {
254
+ // Blending between two animations
255
+ const fromAnim = this.animations[this.blendFromAnimation]
256
+
257
+ // Handle looping for from animation
258
+ let fromTime = this.blendFromTime
259
+ if (this.loop && fromAnim.duration > 0) {
260
+ fromTime = fromTime % fromAnim.duration
261
+ } else {
262
+ fromTime = Math.min(fromTime, fromAnim.duration)
263
+ }
264
+
265
+ // Apply blended animation
266
+ this._applyBlendedAnimation(fromAnim, fromTime, currentAnim, currentTime, this.blendWeight)
267
+ } else {
268
+ // Single animation
269
+ this._applyAnimation(currentAnim, currentTime)
270
+ }
271
+ }
272
+
273
+ /**
274
+ * Apply animation with blending to local transforms (for individual skins)
275
+ */
276
+ _applyAnimationToLocalTransforms(dt) {
277
+ if (!this.localTransforms) return
278
+ if (!this.currentAnimation || !this.animations[this.currentAnimation]) return
279
+
280
+ const currentAnim = this.animations[this.currentAnimation]
281
+
282
+ // Handle looping for current animation
283
+ let currentTime = this.time
284
+ if (this.loop && currentAnim.duration > 0) {
285
+ currentTime = currentTime % currentAnim.duration
286
+ } else {
287
+ currentTime = Math.min(currentTime, currentAnim.duration)
288
+ }
289
+
290
+ if (this.isBlending && this.blendFromAnimation && this.animations[this.blendFromAnimation]) {
291
+ const fromAnim = this.animations[this.blendFromAnimation]
292
+
293
+ let fromTime = this.blendFromTime
294
+ if (this.loop && fromAnim.duration > 0) {
295
+ fromTime = fromTime % fromAnim.duration
296
+ } else {
297
+ fromTime = Math.min(fromTime, fromAnim.duration)
298
+ }
299
+
300
+ this._applyBlendedAnimationToLocal(fromAnim, fromTime, currentAnim, currentTime, this.blendWeight)
301
+ } else {
302
+ this._applyAnimationToLocal(currentAnim, currentTime)
303
+ }
304
+ }
305
+
306
+ /**
307
+ * Apply blended animation between two animations to shared joints
308
+ */
309
+ _applyBlendedAnimation(fromAnim, fromTime, toAnim, toTime, weight) {
310
+ // Temporary storage for blended values
311
+ const tempPos = vec3.create()
312
+ const tempRot = quat.create()
313
+ const tempScale = vec3.fromValues(1, 1, 1)
314
+
315
+ // First apply "from" animation to get base pose
316
+ this._applyAnimation(fromAnim, fromTime)
317
+
318
+ // Store the "from" pose for each joint
319
+ const fromPoses = this.joints.map(joint => ({
320
+ position: vec3.clone(joint.position),
321
+ rotation: quat.clone(joint.rotation),
322
+ scale: vec3.clone(joint.scale)
323
+ }))
324
+
325
+ // Apply "to" animation
326
+ this._applyAnimation(toAnim, toTime)
327
+
328
+ // Blend between stored "from" pose and current "to" pose
329
+ for (let i = 0; i < this.joints.length; i++) {
330
+ const joint = this.joints[i]
331
+ const fromPose = fromPoses[i]
332
+
333
+ // Lerp position
334
+ vec3.lerp(joint.position, fromPose.position, joint.position, weight)
335
+
336
+ // Slerp rotation
337
+ quat.slerp(joint.rotation, fromPose.rotation, joint.rotation, weight)
338
+
339
+ // Lerp scale
340
+ vec3.lerp(joint.scale, fromPose.scale, joint.scale, weight)
341
+ }
342
+ }
343
+
344
+ /**
345
+ * Apply animation keyframes to joints
346
+ * @param {Object} anim - Animation data
347
+ * @param {number} time - Current time
348
+ */
349
+ _applyAnimation(anim, time) {
350
+ for (const channel of anim.channels) {
351
+ const joint = channel.target
352
+ const sampler = channel.sampler
353
+
354
+ // Find keyframes
355
+ const times = sampler.input
356
+ const values = sampler.output
357
+
358
+ // Find the two keyframes to interpolate between
359
+ let prevIndex = 0
360
+ let nextIndex = 0
361
+
362
+ for (let i = 0; i < times.length - 1; i++) {
363
+ if (time >= times[i] && time < times[i + 1]) {
364
+ prevIndex = i
365
+ nextIndex = i + 1
366
+ break
367
+ }
368
+ if (i === times.length - 2) {
369
+ prevIndex = i + 1
370
+ nextIndex = i + 1
371
+ }
372
+ }
373
+
374
+ // Calculate interpolation factor
375
+ let t = 0
376
+ if (nextIndex !== prevIndex) {
377
+ t = (time - times[prevIndex]) / (times[nextIndex] - times[prevIndex])
378
+ }
379
+
380
+ // Apply based on path type
381
+ const path = channel.path
382
+ const numComponents = path === 'rotation' ? 4 : 3
383
+
384
+ if (path === 'translation') {
385
+ const prev = [
386
+ values[prevIndex * 3],
387
+ values[prevIndex * 3 + 1],
388
+ values[prevIndex * 3 + 2]
389
+ ]
390
+ const next = [
391
+ values[nextIndex * 3],
392
+ values[nextIndex * 3 + 1],
393
+ values[nextIndex * 3 + 2]
394
+ ]
395
+ vec3.lerp(joint.position, prev, next, t)
396
+ } else if (path === 'rotation') {
397
+ const prev = quat.fromValues(
398
+ values[prevIndex * 4],
399
+ values[prevIndex * 4 + 1],
400
+ values[prevIndex * 4 + 2],
401
+ values[prevIndex * 4 + 3]
402
+ )
403
+ const next = quat.fromValues(
404
+ values[nextIndex * 4],
405
+ values[nextIndex * 4 + 1],
406
+ values[nextIndex * 4 + 2],
407
+ values[nextIndex * 4 + 3]
408
+ )
409
+ quat.slerp(joint.rotation, prev, next, t)
410
+ } else if (path === 'scale') {
411
+ const prev = [
412
+ values[prevIndex * 3],
413
+ values[prevIndex * 3 + 1],
414
+ values[prevIndex * 3 + 2]
415
+ ]
416
+ const next = [
417
+ values[nextIndex * 3],
418
+ values[nextIndex * 3 + 1],
419
+ values[nextIndex * 3 + 2]
420
+ ]
421
+ vec3.lerp(joint.scale, prev, next, t)
422
+ }
423
+ }
424
+ }
425
+
426
+ /**
427
+ * Initialize local transforms for individual skin (enables per-skin animation state)
428
+ * Call this to make the skin independent from the shared joint hierarchy
429
+ */
430
+ initLocalTransforms() {
431
+ this.useLocalTransforms = true
432
+ this.localTransforms = []
433
+ this.worldMatrices = []
434
+
435
+ // Create local transform storage for each joint
436
+ for (let i = 0; i < this.joints.length; i++) {
437
+ const joint = this.joints[i]
438
+ this.localTransforms.push({
439
+ position: vec3.clone(joint.position),
440
+ rotation: quat.clone(joint.rotation),
441
+ scale: vec3.clone(joint.scale)
442
+ })
443
+ this.worldMatrices.push(mat4.create())
444
+ }
445
+ }
446
+
447
+ /**
448
+ * Apply animation to local transforms (for individual skins)
449
+ */
450
+ _applyAnimationToLocal(anim, time) {
451
+ for (const channel of anim.channels) {
452
+ const joint = channel.target
453
+ const jointIndex = this.joints.indexOf(joint)
454
+ if (jointIndex === -1) continue
455
+
456
+ const localTrans = this.localTransforms[jointIndex]
457
+ const sampler = channel.sampler
458
+ const times = sampler.input
459
+ const values = sampler.output
460
+
461
+ // Find keyframes
462
+ let prevIndex = 0
463
+ let nextIndex = 0
464
+ for (let i = 0; i < times.length - 1; i++) {
465
+ if (time >= times[i] && time < times[i + 1]) {
466
+ prevIndex = i
467
+ nextIndex = i + 1
468
+ break
469
+ }
470
+ if (i === times.length - 2) {
471
+ prevIndex = i + 1
472
+ nextIndex = i + 1
473
+ }
474
+ }
475
+
476
+ let t = 0
477
+ if (nextIndex !== prevIndex) {
478
+ t = (time - times[prevIndex]) / (times[nextIndex] - times[prevIndex])
479
+ }
480
+
481
+ const path = channel.path
482
+ if (path === 'translation') {
483
+ const prev = [values[prevIndex * 3], values[prevIndex * 3 + 1], values[prevIndex * 3 + 2]]
484
+ const next = [values[nextIndex * 3], values[nextIndex * 3 + 1], values[nextIndex * 3 + 2]]
485
+ vec3.lerp(localTrans.position, prev, next, t)
486
+ } else if (path === 'rotation') {
487
+ const prev = quat.fromValues(values[prevIndex * 4], values[prevIndex * 4 + 1], values[prevIndex * 4 + 2], values[prevIndex * 4 + 3])
488
+ const next = quat.fromValues(values[nextIndex * 4], values[nextIndex * 4 + 1], values[nextIndex * 4 + 2], values[nextIndex * 4 + 3])
489
+ quat.slerp(localTrans.rotation, prev, next, t)
490
+ } else if (path === 'scale') {
491
+ const prev = [values[prevIndex * 3], values[prevIndex * 3 + 1], values[prevIndex * 3 + 2]]
492
+ const next = [values[nextIndex * 3], values[nextIndex * 3 + 1], values[nextIndex * 3 + 2]]
493
+ vec3.lerp(localTrans.scale, prev, next, t)
494
+ }
495
+ }
496
+ }
497
+
498
+ /**
499
+ * Apply blended animation to local transforms
500
+ */
501
+ _applyBlendedAnimationToLocal(fromAnim, fromTime, toAnim, toTime, weight) {
502
+ // First apply "from" animation
503
+ this._applyAnimationToLocal(fromAnim, fromTime)
504
+
505
+ // Store from pose
506
+ const fromPoses = this.localTransforms.map(lt => ({
507
+ position: vec3.clone(lt.position),
508
+ rotation: quat.clone(lt.rotation),
509
+ scale: vec3.clone(lt.scale)
510
+ }))
511
+
512
+ // Apply "to" animation
513
+ this._applyAnimationToLocal(toAnim, toTime)
514
+
515
+ // Blend
516
+ for (let i = 0; i < this.localTransforms.length; i++) {
517
+ const lt = this.localTransforms[i]
518
+ const from = fromPoses[i]
519
+
520
+ vec3.lerp(lt.position, from.position, lt.position, weight)
521
+ quat.slerp(lt.rotation, from.rotation, lt.rotation, weight)
522
+ vec3.lerp(lt.scale, from.scale, lt.scale, weight)
523
+ }
524
+ }
525
+
526
+ /**
527
+ * Update world matrices from local transforms (replicates joint hierarchy traversal)
528
+ */
529
+ _updateWorldMatricesFromLocal() {
530
+ // Build parent-child mapping from joints
531
+ const parentIndices = this.joints.map(joint => {
532
+ if (joint.parent) {
533
+ return this.joints.indexOf(joint.parent)
534
+ }
535
+ return -1
536
+ })
537
+
538
+ // Process joints in order (assuming they're topologically sorted - parents before children)
539
+ for (let i = 0; i < this.joints.length; i++) {
540
+ const lt = this.localTransforms[i]
541
+ const worldMat = this.worldMatrices[i]
542
+ const parentIndex = parentIndices[i]
543
+
544
+ // Build local matrix
545
+ const localMat = mat4.create()
546
+ mat4.fromRotationTranslationScale(localMat, lt.rotation, lt.position, lt.scale)
547
+
548
+ // Combine with parent
549
+ if (parentIndex >= 0) {
550
+ mat4.multiply(worldMat, this.worldMatrices[parentIndex], localMat)
551
+ } else {
552
+ mat4.copy(worldMat, localMat)
553
+ }
554
+ }
555
+ }
556
+
557
+ /**
558
+ * Get the number of joints
559
+ */
560
+ get numJoints() {
561
+ return this.joints.length
562
+ }
563
+
564
+ /**
565
+ * Get available animation names
566
+ */
567
+ getAnimationNames() {
568
+ return Object.keys(this.animations)
569
+ }
570
+
571
+ /**
572
+ * Clone this skin with its own joint texture but sharing animation data
573
+ * This allows multiple instances to play at different phases independently
574
+ * @param {boolean} individual - If true, create local transforms for independent animation/blending
575
+ * @returns {Skin} A new Skin instance
576
+ */
577
+ clone(individual = false) {
578
+ const { device } = this.engine
579
+
580
+ const clonedSkin = new Skin(this.engine)
581
+
582
+ // Share references to joints hierarchy and animations (read-only during playback)
583
+ clonedSkin.joints = this.joints
584
+ clonedSkin.inverseBindMatrices = this.inverseBindMatrices
585
+ clonedSkin.animations = this.animations
586
+ clonedSkin.rootNode = this.rootNode
587
+ clonedSkin.speed = this.speed
588
+ clonedSkin.loop = this.loop
589
+ clonedSkin.currentAnimation = this.currentAnimation
590
+ clonedSkin.blendDuration = this.blendDuration
591
+
592
+ // Mark as externally managed - GBufferPass should skip calling update()
593
+ clonedSkin.externallyManaged = true
594
+
595
+ // Create own joint matrices array (these get modified during update)
596
+ clonedSkin.jointMatrices = []
597
+ clonedSkin.jointData = new Float32Array(this.joints.length * 16)
598
+
599
+ for (let i = 0; i < this.joints.length; i++) {
600
+ const jointMatrix = new Float32Array(clonedSkin.jointData.buffer, i * 16 * 4, 16)
601
+ mat4.identity(jointMatrix)
602
+ clonedSkin.jointMatrices.push(jointMatrix)
603
+ }
604
+
605
+ // Create own GPU texture for joint matrices
606
+ clonedSkin.jointTexture = device.createTexture({
607
+ size: [4, this.joints.length, 1],
608
+ format: 'rgba32float',
609
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
610
+ })
611
+
612
+ clonedSkin.jointTextureView = clonedSkin.jointTexture.createView()
613
+
614
+ // Create previous frame joint texture for motion vectors
615
+ clonedSkin.prevJointData = new Float32Array(this.joints.length * 16)
616
+ clonedSkin.prevJointTexture = device.createTexture({
617
+ size: [4, this.joints.length, 1],
618
+ format: 'rgba32float',
619
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
620
+ })
621
+ clonedSkin.prevJointTextureView = clonedSkin.prevJointTexture.createView()
622
+
623
+ // Create own sampler
624
+ clonedSkin.jointSampler = device.createSampler({
625
+ magFilter: 'nearest',
626
+ minFilter: 'nearest',
627
+ })
628
+
629
+ clonedSkin.time = this.time
630
+
631
+ // For individual skins, initialize local transforms for independent animation/blending
632
+ if (individual) {
633
+ clonedSkin.initLocalTransforms()
634
+ }
635
+
636
+ return clonedSkin
637
+ }
638
+
639
+ /**
640
+ * Create an individual clone with its own animation state and blending support
641
+ * Use this for entities that need smooth animation transitions (close to camera)
642
+ * @returns {Skin} A new independent Skin instance
643
+ */
644
+ cloneForIndividual() {
645
+ return this.clone(true)
646
+ }
647
+
648
+ /**
649
+ * Update animation at a specific time (absolute, not delta)
650
+ * Used for phase-offset playback where multiple skins share the same animation
651
+ * @param {number} absoluteTime - Absolute time in the animation
652
+ */
653
+ updateAtTime(absoluteTime) {
654
+ const { device } = this.engine
655
+
656
+ // Copy current joint data to previous BEFORE updating (for motion vectors)
657
+ if (this.prevJointData && this.jointData) {
658
+ this.prevJointData.set(this.jointData)
659
+ }
660
+
661
+ // Apply current animation if any
662
+ if (this.currentAnimation && this.animations[this.currentAnimation]) {
663
+ const anim = this.animations[this.currentAnimation]
664
+
665
+ // Handle looping
666
+ let t = absoluteTime
667
+ if (this.loop && anim.duration > 0) {
668
+ t = t % anim.duration
669
+ } else {
670
+ t = Math.min(t, anim.duration)
671
+ }
672
+
673
+ // Apply animation to joints
674
+ this._applyAnimation(anim, t)
675
+ }
676
+
677
+ // Update world matrices starting from root
678
+ if (this.rootNode) {
679
+ this.rootNode.updateMatrix()
680
+ }
681
+
682
+ // Calculate final joint matrices: jointMatrix = worldMatrix * inverseBindMatrix
683
+ for (let i = 0; i < this.joints.length; i++) {
684
+ const joint = this.joints[i]
685
+ const dst = this.jointMatrices[i]
686
+
687
+ // dst = joint.world * inverseBindMatrix
688
+ mat4.multiply(dst, joint.world, this.inverseBindMatrices[i])
689
+ }
690
+
691
+ // Upload previous joint matrices to GPU (for motion vectors)
692
+ if (this.prevJointTexture && this.prevJointData) {
693
+ device.queue.writeTexture(
694
+ { texture: this.prevJointTexture },
695
+ this.prevJointData,
696
+ { bytesPerRow: 4 * 4 * 4, rowsPerImage: this.joints.length },
697
+ [4, this.joints.length, 1]
698
+ )
699
+ }
700
+
701
+ // Upload current joint matrices to GPU
702
+ device.queue.writeTexture(
703
+ { texture: this.jointTexture },
704
+ this.jointData,
705
+ { bytesPerRow: 4 * 4 * 4, rowsPerImage: this.joints.length },
706
+ [4, this.joints.length, 1]
707
+ )
708
+ }
709
+ }
710
+
711
+ /**
712
+ * Joint node for skeletal animation
713
+ * Represents a bone in the skeleton hierarchy
714
+ */
715
+ class Joint {
716
+ constructor(name = 'joint') {
717
+ this.name = name
718
+ this.position = vec3.create()
719
+ this.rotation = quat.create()
720
+ this.scale = vec3.fromValues(1, 1, 1)
721
+ this.children = []
722
+ this.parent = null
723
+ this.skin = null
724
+
725
+ // Matrices
726
+ this.matrix = mat4.create() // Local transform
727
+ this.world = mat4.create() // World transform (accumulated from parents)
728
+
729
+ // Original pose (bind pose)
730
+ this.bindPosition = vec3.create()
731
+ this.bindRotation = quat.create()
732
+ this.bindScale = vec3.fromValues(1, 1, 1)
733
+ }
734
+
735
+ /**
736
+ * Set the local transform from a matrix
737
+ */
738
+ setMatrix(m) {
739
+ mat4.getTranslation(this.position, m)
740
+ mat4.getRotation(this.rotation, m)
741
+ mat4.getScaling(this.scale, m)
742
+ }
743
+
744
+ /**
745
+ * Save current pose as bind pose
746
+ */
747
+ saveBindPose() {
748
+ vec3.copy(this.bindPosition, this.position)
749
+ quat.copy(this.bindRotation, this.rotation)
750
+ vec3.copy(this.bindScale, this.scale)
751
+ }
752
+
753
+ /**
754
+ * Reset to bind pose
755
+ */
756
+ resetToBindPose() {
757
+ vec3.copy(this.position, this.bindPosition)
758
+ quat.copy(this.rotation, this.bindRotation)
759
+ vec3.copy(this.scale, this.bindScale)
760
+ }
761
+
762
+ /**
763
+ * Add a child joint
764
+ */
765
+ addChild(child) {
766
+ child.parent = this
767
+ this.children.push(child)
768
+ }
769
+
770
+ /**
771
+ * Update local and world matrices
772
+ * @param {mat4} parentWorld - Parent's world matrix (optional)
773
+ */
774
+ updateMatrix(parentWorld = null) {
775
+ // Build local matrix from TRS
776
+ mat4.fromRotationTranslationScale(this.matrix, this.rotation, this.position, this.scale)
777
+
778
+ // Combine with parent world matrix
779
+ if (parentWorld) {
780
+ mat4.multiply(this.world, parentWorld, this.matrix)
781
+ } else {
782
+ mat4.copy(this.world, this.matrix)
783
+ }
784
+
785
+ // Update children
786
+ for (const child of this.children) {
787
+ child.updateMatrix(this.world)
788
+ }
789
+ }
790
+ }
791
+
792
+ export { Skin, Joint }