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.
- package/LICENSE.txt +0 -0
- package/README.md +0 -0
- package/dist/Renderer.cjs +18200 -0
- package/dist/Renderer.cjs.map +1 -0
- package/dist/Renderer.js +18183 -0
- package/dist/Renderer.js.map +1 -0
- package/dist/client.cjs +94 -260
- package/dist/client.cjs.map +1 -1
- package/dist/client.js +71 -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} +173 -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 +572 -0
- package/src/renderer/Geometry.js +1049 -0
- package/src/renderer/Material.js +61 -0
- package/src/renderer/Mesh.js +211 -0
- package/src/renderer/Node.js +112 -0
- package/src/renderer/Pipeline.js +643 -0
- package/src/renderer/Renderer.js +1324 -0
- package/src/renderer/Skin.js +792 -0
- package/src/renderer/Texture.js +584 -0
- package/src/renderer/core/AssetManager.js +359 -0
- package/src/renderer/core/CullingSystem.js +307 -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 +546 -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 +2064 -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 +417 -0
- package/src/renderer/rendering/passes/FogPass.js +419 -0
- package/src/renderer/rendering/passes/GBufferPass.js +706 -0
- package/src/renderer/rendering/passes/HiZPass.js +714 -0
- package/src/renderer/rendering/passes/LightingPass.js +739 -0
- package/src/renderer/rendering/passes/ParticlePass.js +835 -0
- package/src/renderer/rendering/passes/PlanarReflectionPass.js +456 -0
- package/src/renderer/rendering/passes/PostProcessPass.js +282 -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 +265 -0
- package/src/renderer/rendering/passes/SSGITilePass.js +296 -0
- package/src/renderer/rendering/passes/ShadowPass.js +1822 -0
- package/src/renderer/rendering/passes/TransparentPass.js +831 -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/depth_copy.wgsl +17 -0
- package/src/renderer/rendering/shaders/geometry.wgsl +550 -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 +525 -0
- package/src/renderer/rendering/shaders/particle_simulate.wgsl +440 -0
- package/src/renderer/rendering/shaders/postproc.wgsl +272 -0
- package/src/renderer/rendering/shaders/render_post.wgsl +289 -0
- package/src/renderer/rendering/shaders/shadow.wgsl +76 -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/utils/BoundingSphere.js +439 -0
- package/src/renderer/utils/Frustum.js +281 -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,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 }
|