@myned-ai/gsplat-flame-avatar-renderer 1.0.4 → 1.0.6
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/README.md +128 -40
- package/dist/gsplat-flame-avatar-renderer.cjs.js +3478 -722
- package/dist/gsplat-flame-avatar-renderer.cjs.min.js +2 -0
- package/dist/gsplat-flame-avatar-renderer.cjs.min.js.map +1 -0
- package/dist/gsplat-flame-avatar-renderer.esm.js +3439 -724
- package/dist/gsplat-flame-avatar-renderer.esm.min.js +2 -0
- package/dist/gsplat-flame-avatar-renderer.esm.min.js.map +1 -0
- package/package.json +11 -14
- package/src/core/SplatMesh.js +53 -46
- package/src/core/Viewer.js +47 -196
- package/src/errors/ApplicationError.js +185 -0
- package/src/errors/index.js +17 -0
- package/src/flame/FlameAnimator.js +282 -57
- package/src/loaders/PlyLoader.js +302 -44
- package/src/materials/SplatMaterial.js +13 -10
- package/src/materials/SplatMaterial3D.js +72 -27
- package/src/renderer/AnimationManager.js +8 -5
- package/src/renderer/GaussianSplatRenderer.js +668 -217
- package/src/utils/BlobUrlManager.js +294 -0
- package/src/utils/EventEmitter.js +349 -0
- package/src/utils/LoaderUtils.js +2 -1
- package/src/utils/Logger.js +171 -0
- package/src/utils/ObjectPool.js +248 -0
- package/src/utils/RenderLoop.js +306 -0
- package/src/utils/Util.js +59 -18
- package/src/utils/ValidationUtils.js +331 -0
- package/src/utils/index.js +10 -1
- package/dist/gsplat-flame-avatar-renderer.cjs.js.map +0 -1
- package/dist/gsplat-flame-avatar-renderer.esm.js.map +0 -1
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ApplicationError - Base error class for all application errors
|
|
3
|
+
*
|
|
4
|
+
* Provides structured error handling with error codes and cause tracking.
|
|
5
|
+
* All domain-specific errors should extend this class.
|
|
6
|
+
*
|
|
7
|
+
* @extends Error
|
|
8
|
+
*/
|
|
9
|
+
export class ApplicationError extends Error {
|
|
10
|
+
/**
|
|
11
|
+
* Create an ApplicationError
|
|
12
|
+
* @param {string} message - Human-readable error message
|
|
13
|
+
* @param {string} code - Machine-readable error code for programmatic handling
|
|
14
|
+
* @param {Error} [cause=null] - Original error that caused this error (for error chaining)
|
|
15
|
+
*/
|
|
16
|
+
constructor(message, code, cause = null) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = this.constructor.name;
|
|
19
|
+
this.code = code;
|
|
20
|
+
this.cause = cause;
|
|
21
|
+
|
|
22
|
+
// Capture stack trace, excluding constructor call from it
|
|
23
|
+
if (Error.captureStackTrace) {
|
|
24
|
+
Error.captureStackTrace(this, this.constructor);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Convert error to JSON for logging/transmission
|
|
30
|
+
* @returns {object} JSON representation of error
|
|
31
|
+
*/
|
|
32
|
+
toJSON() {
|
|
33
|
+
return {
|
|
34
|
+
name: this.name,
|
|
35
|
+
message: this.message,
|
|
36
|
+
code: this.code,
|
|
37
|
+
stack: this.stack,
|
|
38
|
+
cause: this.cause ? {
|
|
39
|
+
name: this.cause.name,
|
|
40
|
+
message: this.cause.message
|
|
41
|
+
} : null
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* ValidationError - Thrown when input validation fails
|
|
48
|
+
*
|
|
49
|
+
* Used for invalid parameters, out-of-range values, or malformed data.
|
|
50
|
+
*
|
|
51
|
+
* @extends ApplicationError
|
|
52
|
+
*/
|
|
53
|
+
export class ValidationError extends ApplicationError {
|
|
54
|
+
/**
|
|
55
|
+
* Create a ValidationError
|
|
56
|
+
* @param {string} message - Validation failure description
|
|
57
|
+
* @param {string} field - Name of the field that failed validation
|
|
58
|
+
* @param {Error} [cause=null] - Original error that caused this validation failure
|
|
59
|
+
*/
|
|
60
|
+
constructor(message, field, cause = null) {
|
|
61
|
+
super(message, 'VALIDATION_ERROR', cause);
|
|
62
|
+
this.field = field;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* NetworkError - Thrown when network operations fail
|
|
68
|
+
*
|
|
69
|
+
* Used for fetch failures, timeouts, or HTTP error responses.
|
|
70
|
+
*
|
|
71
|
+
* @extends ApplicationError
|
|
72
|
+
*/
|
|
73
|
+
export class NetworkError extends ApplicationError {
|
|
74
|
+
/**
|
|
75
|
+
* Create a NetworkError
|
|
76
|
+
* @param {string} message - Network error description
|
|
77
|
+
* @param {number} [statusCode=0] - HTTP status code (if applicable)
|
|
78
|
+
* @param {Error} [cause=null] - Original error that caused this network failure
|
|
79
|
+
*/
|
|
80
|
+
constructor(message, statusCode = 0, cause = null) {
|
|
81
|
+
super(message, 'NETWORK_ERROR', cause);
|
|
82
|
+
this.statusCode = statusCode;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* AssetLoadError - Thrown when asset loading fails
|
|
88
|
+
*
|
|
89
|
+
* Used for failures loading models, textures, or other asset files.
|
|
90
|
+
*
|
|
91
|
+
* @extends ApplicationError
|
|
92
|
+
*/
|
|
93
|
+
export class AssetLoadError extends ApplicationError {
|
|
94
|
+
/**
|
|
95
|
+
* Create an AssetLoadError
|
|
96
|
+
* @param {string} message - Asset load failure description
|
|
97
|
+
* @param {string} assetPath - Path to the asset that failed to load
|
|
98
|
+
* @param {Error} [cause=null] - Original error that caused this load failure
|
|
99
|
+
*/
|
|
100
|
+
constructor(message, assetPath, cause = null) {
|
|
101
|
+
super(message, 'ASSET_LOAD_ERROR', cause);
|
|
102
|
+
this.assetPath = assetPath;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* ResourceDisposedError - Thrown when attempting to use a disposed resource
|
|
108
|
+
*
|
|
109
|
+
* Used to prevent use-after-dispose bugs.
|
|
110
|
+
*
|
|
111
|
+
* @extends ApplicationError
|
|
112
|
+
*/
|
|
113
|
+
export class ResourceDisposedError extends ApplicationError {
|
|
114
|
+
/**
|
|
115
|
+
* Create a ResourceDisposedError
|
|
116
|
+
* @param {string} resourceName - Name of the disposed resource
|
|
117
|
+
*/
|
|
118
|
+
constructor(resourceName) {
|
|
119
|
+
super(
|
|
120
|
+
`Cannot use ${resourceName}: resource has been disposed`,
|
|
121
|
+
'RESOURCE_DISPOSED_ERROR'
|
|
122
|
+
);
|
|
123
|
+
this.resourceName = resourceName;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* InitializationError - Thrown when initialization fails
|
|
129
|
+
*
|
|
130
|
+
* Used when required setup steps fail or prerequisites are not met.
|
|
131
|
+
*
|
|
132
|
+
* @extends ApplicationError
|
|
133
|
+
*/
|
|
134
|
+
export class InitializationError extends ApplicationError {
|
|
135
|
+
/**
|
|
136
|
+
* Create an InitializationError
|
|
137
|
+
* @param {string} message - Initialization failure description
|
|
138
|
+
* @param {string} component - Name of component that failed to initialize
|
|
139
|
+
* @param {Error} [cause=null] - Original error that caused this initialization failure
|
|
140
|
+
*/
|
|
141
|
+
constructor(message, component, cause = null) {
|
|
142
|
+
super(message, 'INITIALIZATION_ERROR', cause);
|
|
143
|
+
this.component = component;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* ParseError - Thrown when parsing data fails
|
|
149
|
+
*
|
|
150
|
+
* Used for malformed file formats, invalid JSON, or corrupt data.
|
|
151
|
+
*
|
|
152
|
+
* @extends ApplicationError
|
|
153
|
+
*/
|
|
154
|
+
export class ParseError extends ApplicationError {
|
|
155
|
+
/**
|
|
156
|
+
* Create a ParseError
|
|
157
|
+
* @param {string} message - Parse failure description
|
|
158
|
+
* @param {string} dataType - Type of data being parsed (e.g., 'JSON', 'PLY', 'GLB')
|
|
159
|
+
* @param {Error} [cause=null] - Original error that caused this parse failure
|
|
160
|
+
*/
|
|
161
|
+
constructor(message, dataType, cause = null) {
|
|
162
|
+
super(message, 'PARSE_ERROR', cause);
|
|
163
|
+
this.dataType = dataType;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* ConfigurationError - Thrown when configuration is invalid
|
|
169
|
+
*
|
|
170
|
+
* Used for invalid settings, missing required configuration, or conflicting options.
|
|
171
|
+
*
|
|
172
|
+
* @extends ApplicationError
|
|
173
|
+
*/
|
|
174
|
+
export class ConfigurationError extends ApplicationError {
|
|
175
|
+
/**
|
|
176
|
+
* Create a ConfigurationError
|
|
177
|
+
* @param {string} message - Configuration error description
|
|
178
|
+
* @param {string} configKey - Configuration key that is invalid
|
|
179
|
+
* @param {Error} [cause=null] - Original error
|
|
180
|
+
*/
|
|
181
|
+
constructor(message, configKey, cause = null) {
|
|
182
|
+
super(message, 'CONFIGURATION_ERROR', cause);
|
|
183
|
+
this.configKey = configKey;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error classes for structured error handling
|
|
3
|
+
*
|
|
4
|
+
* This module exports all custom error classes used throughout the application.
|
|
5
|
+
* All errors extend ApplicationError for consistent error handling.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
ApplicationError,
|
|
10
|
+
ValidationError,
|
|
11
|
+
NetworkError,
|
|
12
|
+
AssetLoadError,
|
|
13
|
+
ResourceDisposedError,
|
|
14
|
+
InitializationError,
|
|
15
|
+
ParseError,
|
|
16
|
+
ConfigurationError
|
|
17
|
+
} from './ApplicationError.js';
|
|
@@ -1,60 +1,158 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* FlameAnimator
|
|
3
|
-
*
|
|
2
|
+
* FlameAnimator - FLAME Parametric Head Model Animation Controller
|
|
3
|
+
*
|
|
4
4
|
* Derived from gaussian-splat-renderer-for-lam
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* -
|
|
8
|
-
* - Blendshape weight updates
|
|
9
|
-
* - Skeleton synchronization
|
|
5
|
+
*
|
|
6
|
+
* Manages FLAME (Faces Learned with an Articulated Model and Expressions) model animation:
|
|
7
|
+
* - Skeletal bone rotation from FLAME parameters
|
|
8
|
+
* - Blendshape weight updates for facial expressions
|
|
9
|
+
* - Skeleton-mesh synchronization for gaussian splat rendering
|
|
10
|
+
* - Linear blend skinning (LBS) with bone weights
|
|
11
|
+
*
|
|
12
|
+
* FLAME uses 5 bones: root, neck, jaw, left eye, right eye
|
|
10
13
|
*/
|
|
11
14
|
|
|
12
15
|
import {
|
|
13
|
-
Vector3,
|
|
14
|
-
Quaternion,
|
|
15
16
|
Matrix4,
|
|
16
17
|
Bone,
|
|
17
18
|
Skeleton
|
|
18
19
|
} from 'three';
|
|
19
20
|
|
|
20
21
|
import { FlameTextureManager } from './FlameTextureManager.js';
|
|
22
|
+
import { getLogger } from '../utils/Logger.js';
|
|
23
|
+
import { ValidationError, ResourceDisposedError } from '../errors/index.js';
|
|
24
|
+
import { validateRequiredProperties } from '../utils/ValidationUtils.js';
|
|
25
|
+
import { vector3Pool, quaternionPool } from '../utils/ObjectPool.js';
|
|
26
|
+
|
|
27
|
+
const logger = getLogger('FlameAnimator');
|
|
21
28
|
|
|
22
29
|
/**
|
|
23
30
|
* FlameAnimator - Manages FLAME parametric head model animation
|
|
31
|
+
*
|
|
32
|
+
* Provides skeletal animation, blendshape morphing, and mesh deformation
|
|
33
|
+
* for FLAME-based avatar heads with gaussian splatting rendering.
|
|
24
34
|
*/
|
|
25
35
|
export class FlameAnimator {
|
|
36
|
+
/**
|
|
37
|
+
* Create a FlameAnimator instance
|
|
38
|
+
*
|
|
39
|
+
* @constructor
|
|
40
|
+
*/
|
|
26
41
|
constructor() {
|
|
42
|
+
/** @type {THREE.Skeleton|null} FLAME skeleton with 5 bones */
|
|
27
43
|
this.skeleton = null;
|
|
44
|
+
|
|
45
|
+
/** @type {THREE.Bone[]|null} Array of skeleton bones */
|
|
28
46
|
this.bones = null;
|
|
47
|
+
|
|
48
|
+
/** @type {Object|null} FLAME animation parameters (rotation, expr, neck_pose, jaw_pose, eyes_pose) */
|
|
29
49
|
this.flameParams = null;
|
|
50
|
+
|
|
51
|
+
/** @type {Array|null} Linear blend skinning weights */
|
|
30
52
|
this.lbsWeight = null;
|
|
53
|
+
|
|
54
|
+
/** @type {number} Current animation frame */
|
|
31
55
|
this.frame = 0;
|
|
56
|
+
|
|
57
|
+
/** @type {number} Total frames in animation sequence */
|
|
32
58
|
this.totalFrames = 0;
|
|
59
|
+
|
|
60
|
+
/** @type {boolean} Whether FLAME mode is enabled */
|
|
33
61
|
this.useFlame = true;
|
|
62
|
+
|
|
63
|
+
/** @type {THREE.SkinnedMesh|null} The FLAME avatar mesh */
|
|
34
64
|
this.avatarMesh = null;
|
|
65
|
+
|
|
66
|
+
/** @type {number} Number of gaussian splats in the mesh */
|
|
35
67
|
this.gaussianSplatCount = 0;
|
|
68
|
+
|
|
69
|
+
/** @type {boolean} Whether animator has been disposed */
|
|
70
|
+
this._disposed = false;
|
|
71
|
+
|
|
72
|
+
logger.debug('FlameAnimator instance created');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Assert animator is not disposed
|
|
77
|
+
* @private
|
|
78
|
+
* @throws {ResourceDisposedError} If animator has been disposed
|
|
79
|
+
*/
|
|
80
|
+
_assertNotDisposed() {
|
|
81
|
+
if (this._disposed) {
|
|
82
|
+
throw new ResourceDisposedError('FlameAnimator has been disposed');
|
|
83
|
+
}
|
|
36
84
|
}
|
|
37
85
|
|
|
38
86
|
/**
|
|
39
|
-
* Initialize with FLAME parameters and
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
87
|
+
* Initialize animator with FLAME parameters and skeleton data
|
|
88
|
+
*
|
|
89
|
+
* Sets up the FLAME animation system with parameters for bone rotations,
|
|
90
|
+
* expressions, and linear blend skinning weights.
|
|
91
|
+
*
|
|
92
|
+
* @param {Object} flameParams - FLAME animation parameters
|
|
93
|
+
* @param {Array} flameParams.rotation - Root bone rotations per frame (axis-angle)
|
|
94
|
+
* @param {Array} flameParams.expr - Expression blendshape weights per frame
|
|
95
|
+
* @param {Array} flameParams.neck_pose - Neck bone rotations per frame
|
|
96
|
+
* @param {Array} flameParams.jaw_pose - Jaw bone rotations per frame
|
|
97
|
+
* @param {Array} flameParams.eyes_pose - Eye bone rotations per frame (6 values: left 3, right 3)
|
|
98
|
+
* @param {Object|Array} boneTree - Skeleton hierarchy definition
|
|
99
|
+
* @param {Array} lbsWeight - Linear blend skinning weights for each vertex
|
|
43
100
|
* @param {THREE.SkinnedMesh} avatarMesh - The FLAME avatar mesh
|
|
101
|
+
* @throws {ValidationError} If required parameters are missing or invalid
|
|
102
|
+
* @returns {void}
|
|
44
103
|
*/
|
|
45
104
|
initialize(flameParams, boneTree, lbsWeight, avatarMesh) {
|
|
105
|
+
this._assertNotDisposed();
|
|
106
|
+
|
|
107
|
+
// Validate required parameters
|
|
108
|
+
try {
|
|
109
|
+
validateRequiredProperties(flameParams, ['rotation', 'expr', 'neck_pose', 'jaw_pose', 'eyes_pose'], 'flameParams');
|
|
110
|
+
} catch (error) {
|
|
111
|
+
logger.error('Invalid flameParams', error);
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!avatarMesh || !avatarMesh.geometry || !avatarMesh.geometry.attributes || !avatarMesh.geometry.attributes.position) {
|
|
116
|
+
const error = new ValidationError(
|
|
117
|
+
'avatarMesh must have geometry with position attribute',
|
|
118
|
+
'avatarMesh'
|
|
119
|
+
);
|
|
120
|
+
logger.error('Invalid avatarMesh', error);
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
logger.info('Initializing FlameAnimator', {
|
|
125
|
+
frameCount: flameParams.rotation?.length,
|
|
126
|
+
splatCount: avatarMesh.geometry.attributes.position.count
|
|
127
|
+
});
|
|
128
|
+
|
|
46
129
|
this.flameParams = flameParams;
|
|
47
130
|
this.lbsWeight = lbsWeight;
|
|
48
131
|
this.avatarMesh = avatarMesh;
|
|
49
|
-
|
|
50
|
-
|
|
132
|
+
|
|
133
|
+
// Calculate total frames from rotation data
|
|
134
|
+
if (flameParams.rotation && Array.isArray(flameParams.rotation)) {
|
|
51
135
|
this.totalFrames = flameParams.rotation.length;
|
|
136
|
+
logger.debug('Animation has frames', { totalFrames: this.totalFrames });
|
|
137
|
+
} else {
|
|
138
|
+
logger.warn('No rotation data found, totalFrames set to 0');
|
|
139
|
+
this.totalFrames = 0;
|
|
52
140
|
}
|
|
53
141
|
|
|
54
142
|
this.gaussianSplatCount = avatarMesh.geometry.attributes.position.count;
|
|
55
|
-
|
|
143
|
+
|
|
56
144
|
// Build skeleton from bone tree
|
|
57
|
-
|
|
145
|
+
try {
|
|
146
|
+
this.buildSkeleton(boneTree);
|
|
147
|
+
logger.debug('Skeleton built successfully', { boneCount: this.bones?.length });
|
|
148
|
+
} catch (error) {
|
|
149
|
+
logger.error('Failed to build skeleton', error);
|
|
150
|
+
throw new ValidationError(
|
|
151
|
+
`Failed to build skeleton: ${error.message}`,
|
|
152
|
+
'boneTree',
|
|
153
|
+
error
|
|
154
|
+
);
|
|
155
|
+
}
|
|
58
156
|
}
|
|
59
157
|
|
|
60
158
|
/**
|
|
@@ -139,58 +237,115 @@ export class FlameAnimator {
|
|
|
139
237
|
}
|
|
140
238
|
|
|
141
239
|
/**
|
|
142
|
-
* Set bone rotation from axis-angle or quaternion
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
240
|
+
* Set bone rotation from axis-angle or quaternion representation
|
|
241
|
+
*
|
|
242
|
+
* Converts rotation data to quaternion and applies it to the bone.
|
|
243
|
+
* Uses object pooling to avoid allocations in animation loop.
|
|
244
|
+
*
|
|
245
|
+
* @param {THREE.Bone} bone - Target bone to rotate
|
|
246
|
+
* @param {Array<number>} angles - Rotation values (3 for axis-angle, 4 for quaternion)
|
|
247
|
+
* @param {boolean} [isQuat=false] - Whether angles are quaternion [x, y, z, w]
|
|
248
|
+
* @throws {ValidationError} If bone or angles are invalid
|
|
249
|
+
* @returns {void}
|
|
146
250
|
*/
|
|
147
251
|
setBoneRotation(bone, angles, isQuat = false) {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
if (
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
252
|
+
this._assertNotDisposed();
|
|
253
|
+
|
|
254
|
+
if (!bone || !angles || !Array.isArray(angles)) {
|
|
255
|
+
throw new ValidationError(
|
|
256
|
+
'bone and angles array are required',
|
|
257
|
+
'bone/angles'
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Use object pooling for temp objects
|
|
262
|
+
const quaternion = quaternionPool.acquire();
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
if (isQuat) {
|
|
266
|
+
// Direct quaternion (XYZW format)
|
|
267
|
+
if (angles.length < 4) {
|
|
268
|
+
throw new ValidationError('Quaternion requires 4 values', 'angles');
|
|
269
|
+
}
|
|
270
|
+
quaternion.set(angles[0], angles[1], angles[2], angles[3]);
|
|
271
|
+
} else {
|
|
272
|
+
// Axis-angle representation: [x, y, z] where magnitude is angle
|
|
273
|
+
if (angles.length < 3) {
|
|
274
|
+
throw new ValidationError('Axis-angle requires 3 values', 'angles');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const axis = vector3Pool.acquire();
|
|
278
|
+
try {
|
|
279
|
+
axis.set(angles[0], angles[1], angles[2]);
|
|
280
|
+
const angleInRadians = axis.length();
|
|
281
|
+
axis.normalize();
|
|
282
|
+
quaternion.setFromAxisAngle(axis, angleInRadians);
|
|
283
|
+
} finally {
|
|
284
|
+
vector3Pool.release(axis);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Apply rotation to bone
|
|
289
|
+
bone.quaternion.copy(quaternion);
|
|
290
|
+
bone.updateMatrixWorld(true);
|
|
291
|
+
} finally {
|
|
292
|
+
quaternionPool.release(quaternion);
|
|
158
293
|
}
|
|
159
|
-
|
|
160
|
-
bone.quaternion.copy(quaternion);
|
|
161
|
-
bone.updateMatrixWorld(true);
|
|
162
294
|
}
|
|
163
295
|
|
|
164
296
|
/**
|
|
165
297
|
* Update FLAME bones from current frame parameters
|
|
298
|
+
*
|
|
299
|
+
* Applies bone rotations for all 5 FLAME bones (root, neck, jaw, left eye, right eye)
|
|
300
|
+
* from the current animation frame. Updates skeleton matrices and returns data
|
|
301
|
+
* needed for mesh deformation.
|
|
302
|
+
*
|
|
303
|
+
* @returns {Object} Bone and blendshape data for rendering
|
|
304
|
+
* @returns {Array} return.bsWeight - Expression blendshape weights for current frame
|
|
305
|
+
* @returns {Float32Array} return.bonesMatrix - Updated bone transformation matrices
|
|
306
|
+
* @returns {number} return.bonesNum - Number of bones (always 5 for FLAME)
|
|
307
|
+
* @returns {Array} return.bonesWeight - LBS weights for vertices
|
|
166
308
|
*/
|
|
167
309
|
updateFlameBones() {
|
|
168
|
-
|
|
310
|
+
this._assertNotDisposed();
|
|
169
311
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const rootAngle = this.flameParams['rotation'][frame];
|
|
175
|
-
this.setBoneRotation(this.skeleton.bones[0], rootAngle);
|
|
312
|
+
if (!this.flameParams || !this.skeleton) {
|
|
313
|
+
logger.warn('Cannot update bones: flameParams or skeleton not initialized');
|
|
314
|
+
return {};
|
|
315
|
+
}
|
|
176
316
|
|
|
177
|
-
|
|
178
|
-
const neckAngle = this.flameParams['neck_pose'][frame];
|
|
179
|
-
this.setBoneRotation(this.skeleton.bones[1], neckAngle);
|
|
317
|
+
const frame = this.frame;
|
|
180
318
|
|
|
181
|
-
//
|
|
182
|
-
const
|
|
183
|
-
this.setBoneRotation(this.skeleton.bones[2], jawAngle);
|
|
319
|
+
// Get blendshape weights for this frame
|
|
320
|
+
const bsWeight = this.flameParams['expr'][frame];
|
|
184
321
|
|
|
185
|
-
//
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
322
|
+
// Apply bone rotations for current frame
|
|
323
|
+
try {
|
|
324
|
+
// Root bone rotation (global head orientation)
|
|
325
|
+
const rootAngle = this.flameParams['rotation'][frame];
|
|
326
|
+
this.setBoneRotation(this.skeleton.bones[0], rootAngle);
|
|
327
|
+
|
|
328
|
+
// Neck rotation
|
|
329
|
+
const neckAngle = this.flameParams['neck_pose'][frame];
|
|
330
|
+
this.setBoneRotation(this.skeleton.bones[1], neckAngle);
|
|
331
|
+
|
|
332
|
+
// Jaw rotation
|
|
333
|
+
const jawAngle = this.flameParams['jaw_pose'][frame];
|
|
334
|
+
this.setBoneRotation(this.skeleton.bones[2], jawAngle);
|
|
335
|
+
|
|
336
|
+
// Eyes rotation (6 values: left eye xyz, right eye xyz)
|
|
337
|
+
const eyesAngle = this.flameParams['eyes_pose'][frame];
|
|
338
|
+
this.setBoneRotation(this.skeleton.bones[3], eyesAngle.slice(0, 3)); // Left eye
|
|
339
|
+
this.setBoneRotation(this.skeleton.bones[4], eyesAngle.slice(3, 6)); // Right eye
|
|
340
|
+
} catch (error) {
|
|
341
|
+
logger.error('Error setting bone rotations', { frame, error });
|
|
342
|
+
throw error;
|
|
343
|
+
}
|
|
189
344
|
|
|
190
|
-
// Update skeleton matrices
|
|
345
|
+
// Update skeleton matrices after all rotations are set
|
|
191
346
|
this.skeleton.update();
|
|
192
347
|
|
|
193
|
-
// Get updated bone matrices
|
|
348
|
+
// Get updated bone matrices for shader
|
|
194
349
|
const numBones = 5;
|
|
195
350
|
const bonesMatrix = FlameTextureManager.getUpdatedBoneMatrices(this.skeleton, numBones);
|
|
196
351
|
|
|
@@ -233,39 +388,109 @@ export class FlameAnimator {
|
|
|
233
388
|
|
|
234
389
|
/**
|
|
235
390
|
* Set current animation frame
|
|
236
|
-
*
|
|
391
|
+
*
|
|
392
|
+
* @param {number} frame - Frame number to set (will wrap if exceeds totalFrames)
|
|
393
|
+
* @throws {ValidationError} If frame is not a valid number
|
|
394
|
+
* @returns {void}
|
|
237
395
|
*/
|
|
238
396
|
setFrame(frame) {
|
|
239
|
-
this.
|
|
397
|
+
this._assertNotDisposed();
|
|
398
|
+
|
|
399
|
+
if (typeof frame !== 'number' || isNaN(frame)) {
|
|
400
|
+
throw new ValidationError('frame must be a valid number', 'frame');
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (this.totalFrames > 0) {
|
|
404
|
+
this.frame = ((frame % this.totalFrames) + this.totalFrames) % this.totalFrames; // Handle negative
|
|
405
|
+
} else {
|
|
406
|
+
this.frame = 0;
|
|
407
|
+
}
|
|
240
408
|
}
|
|
241
409
|
|
|
242
410
|
/**
|
|
243
|
-
* Advance to next frame
|
|
411
|
+
* Advance to next frame in animation sequence
|
|
412
|
+
*
|
|
413
|
+
* Automatically wraps to frame 0 after reaching totalFrames.
|
|
414
|
+
*
|
|
415
|
+
* @returns {void}
|
|
244
416
|
*/
|
|
245
417
|
nextFrame() {
|
|
246
|
-
this.
|
|
418
|
+
this._assertNotDisposed();
|
|
419
|
+
|
|
420
|
+
if (this.totalFrames > 0) {
|
|
421
|
+
this.frame = (this.frame + 1) % this.totalFrames;
|
|
422
|
+
}
|
|
247
423
|
}
|
|
248
424
|
|
|
249
425
|
/**
|
|
250
426
|
* Get skeleton for external use
|
|
427
|
+
*
|
|
428
|
+
* @returns {THREE.Skeleton|null} The FLAME skeleton or null if not initialized
|
|
251
429
|
*/
|
|
252
430
|
getSkeleton() {
|
|
431
|
+
this._assertNotDisposed();
|
|
253
432
|
return this.skeleton;
|
|
254
433
|
}
|
|
255
434
|
|
|
256
435
|
/**
|
|
257
436
|
* Get current frame number
|
|
437
|
+
*
|
|
438
|
+
* @returns {number} Current animation frame index
|
|
258
439
|
*/
|
|
259
440
|
getFrame() {
|
|
441
|
+
this._assertNotDisposed();
|
|
260
442
|
return this.frame;
|
|
261
443
|
}
|
|
262
444
|
|
|
263
445
|
/**
|
|
264
446
|
* Get total frame count
|
|
447
|
+
*
|
|
448
|
+
* @returns {number} Total number of animation frames
|
|
265
449
|
*/
|
|
266
450
|
getTotalFrames() {
|
|
451
|
+
this._assertNotDisposed();
|
|
267
452
|
return this.totalFrames;
|
|
268
453
|
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Dispose animator and free resources
|
|
457
|
+
*
|
|
458
|
+
* Properly cleans up:
|
|
459
|
+
* - Skeleton and bones
|
|
460
|
+
* - References to meshes and parameters
|
|
461
|
+
*
|
|
462
|
+
* @returns {void}
|
|
463
|
+
*/
|
|
464
|
+
dispose() {
|
|
465
|
+
if (this._disposed) {
|
|
466
|
+
logger.warn('FlameAnimator.dispose() called on already disposed instance');
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
logger.info('Disposing FlameAnimator');
|
|
471
|
+
|
|
472
|
+
// Dispose skeleton (bones are part of skeleton)
|
|
473
|
+
if (this.skeleton) {
|
|
474
|
+
this.skeleton.dispose();
|
|
475
|
+
this.skeleton = null;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Nullify all references to aid GC
|
|
479
|
+
this.bones = null;
|
|
480
|
+
this.flameParams = null;
|
|
481
|
+
this.lbsWeight = null;
|
|
482
|
+
this.avatarMesh = null;
|
|
483
|
+
|
|
484
|
+
// Reset state
|
|
485
|
+
this.frame = 0;
|
|
486
|
+
this.totalFrames = 0;
|
|
487
|
+
this.gaussianSplatCount = 0;
|
|
488
|
+
|
|
489
|
+
// Mark as disposed
|
|
490
|
+
this._disposed = true;
|
|
491
|
+
|
|
492
|
+
logger.debug('FlameAnimator disposed successfully');
|
|
493
|
+
}
|
|
269
494
|
}
|
|
270
495
|
|
|
271
496
|
export default FlameAnimator;
|