feat(3d): FBX/GLTF/OBJ 加载器与骨骼动画支持 (#315)
* feat(3d): FBX/GLTF/OBJ 加载器与骨骼动画支持 * chore: 更新 pnpm-lock.yaml * fix: 移除未使用的变量和方法 * fix: 修复 mesh-3d-editor tsconfig 引用路径 * fix: 修复正则表达式 ReDoS 漏洞
This commit is contained in:
341
packages/mesh-3d/src/Animation3DComponent.ts
Normal file
341
packages/mesh-3d/src/Animation3DComponent.ts
Normal file
@@ -0,0 +1,341 @@
|
||||
/**
|
||||
* Animation3DComponent - 3D animation playback component.
|
||||
* Animation3DComponent - 3D 动画播放组件。
|
||||
*/
|
||||
|
||||
import { Component, ECSComponent, Property, Serializable, Serialize } from '@esengine/ecs-framework';
|
||||
import type { IGLTFAnimationClip } from '@esengine/asset-system';
|
||||
|
||||
/**
|
||||
* Animation play state.
|
||||
* 动画播放状态。
|
||||
*/
|
||||
export enum AnimationPlayState {
|
||||
/** Stopped - not playing. | 停止 - 未播放。 */
|
||||
Stopped = 'stopped',
|
||||
/** Playing forward. | 正向播放。 */
|
||||
Playing = 'playing',
|
||||
/** Paused. | 暂停。 */
|
||||
Paused = 'paused'
|
||||
}
|
||||
|
||||
/**
|
||||
* Animation wrap mode.
|
||||
* 动画循环模式。
|
||||
*/
|
||||
export enum AnimationWrapMode {
|
||||
/** Play once and stop. | 播放一次后停止。 */
|
||||
Once = 'once',
|
||||
/** Loop continuously. | 连续循环。 */
|
||||
Loop = 'loop',
|
||||
/** Play forward then backward (ping-pong). | 往返播放。 */
|
||||
PingPong = 'pingpong',
|
||||
/** Clamp to last frame. | 停在最后一帧。 */
|
||||
ClampForever = 'clampForever'
|
||||
}
|
||||
|
||||
/**
|
||||
* 3D Animation component for playing skeletal/node animations.
|
||||
* 用于播放骨骼/节点动画的 3D 动画组件。
|
||||
*
|
||||
* Requires MeshComponent for animation data source.
|
||||
* 需要 MeshComponent 作为动画数据来源。
|
||||
*/
|
||||
@ECSComponent('Animation3D', { requires: ['Mesh'] })
|
||||
@Serializable({ version: 1, typeId: 'Animation3D' })
|
||||
export class Animation3DComponent extends Component {
|
||||
/**
|
||||
* 默认动画片段名称
|
||||
* Default animation clip name
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'string', label: 'Default Clip' })
|
||||
public defaultClip: string = '';
|
||||
|
||||
/**
|
||||
* 播放速度(1.0 = 正常速度)
|
||||
* Playback speed (1.0 = normal speed)
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Speed', min: 0, max: 10 })
|
||||
public speed: number = 1.0;
|
||||
|
||||
/**
|
||||
* 循环模式
|
||||
* Wrap mode
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({
|
||||
type: 'enum',
|
||||
label: 'Wrap Mode',
|
||||
options: ['once', 'loop', 'pingpong', 'clampForever']
|
||||
})
|
||||
public wrapMode: AnimationWrapMode = AnimationWrapMode.Loop;
|
||||
|
||||
/**
|
||||
* 是否启动时自动播放
|
||||
* Whether to auto-play on start
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'boolean', label: 'Play On Awake' })
|
||||
public playOnAwake: boolean = true;
|
||||
|
||||
// ===== Runtime State | 运行时状态 =====
|
||||
|
||||
/**
|
||||
* 当前播放状态
|
||||
* Current play state
|
||||
*/
|
||||
private _playState: AnimationPlayState = AnimationPlayState.Stopped;
|
||||
|
||||
/**
|
||||
* 当前播放的动画片段
|
||||
* Currently playing animation clip
|
||||
*/
|
||||
private _currentClip: IGLTFAnimationClip | null = null;
|
||||
|
||||
/**
|
||||
* 当前播放时间(秒)
|
||||
* Current playback time (seconds)
|
||||
*/
|
||||
private _currentTime: number = 0;
|
||||
|
||||
/**
|
||||
* 播放方向(1 = 正向,-1 = 反向)
|
||||
* Playback direction (1 = forward, -1 = backward)
|
||||
*/
|
||||
private _direction: number = 1;
|
||||
|
||||
/**
|
||||
* 可用的动画片段列表
|
||||
* Available animation clips
|
||||
*/
|
||||
private _clips: IGLTFAnimationClip[] = [];
|
||||
|
||||
/**
|
||||
* 当前片段名称到索引的映射
|
||||
* Map of clip name to index
|
||||
*/
|
||||
private _clipNameToIndex: Map<string, number> = new Map();
|
||||
|
||||
// ===== Public Getters | 公共获取器 =====
|
||||
|
||||
/**
|
||||
* 获取当前播放状态
|
||||
* Get current play state
|
||||
*/
|
||||
public get playState(): AnimationPlayState {
|
||||
return this._playState;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前播放的动画片段
|
||||
* Get currently playing clip
|
||||
*/
|
||||
public get currentClip(): IGLTFAnimationClip | null {
|
||||
return this._currentClip;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前播放时间
|
||||
* Get current playback time
|
||||
*/
|
||||
public get currentTime(): number {
|
||||
return this._currentTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前片段的持续时间
|
||||
* Get duration of current clip
|
||||
*/
|
||||
public get duration(): number {
|
||||
return this._currentClip?.duration ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取归一化时间(0-1)
|
||||
* Get normalized time (0-1)
|
||||
*/
|
||||
public get normalizedTime(): number {
|
||||
if (!this._currentClip || this._currentClip.duration <= 0) return 0;
|
||||
return this._currentTime / this._currentClip.duration;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否正在播放
|
||||
* Whether playing
|
||||
*/
|
||||
public get isPlaying(): boolean {
|
||||
return this._playState === AnimationPlayState.Playing;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有可用的动画片段
|
||||
* Get all available clips
|
||||
*/
|
||||
public get clips(): readonly IGLTFAnimationClip[] {
|
||||
return this._clips;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有动画片段名称
|
||||
* Get all clip names
|
||||
*/
|
||||
public get clipNames(): string[] {
|
||||
return this._clips.map(c => c.name);
|
||||
}
|
||||
|
||||
// ===== Public Methods | 公共方法 =====
|
||||
|
||||
/**
|
||||
* 设置动画片段列表(由 Animation3DSystem 调用)
|
||||
* Set animation clips (called by Animation3DSystem)
|
||||
*/
|
||||
public setClips(clips: IGLTFAnimationClip[]): void {
|
||||
this._clips = clips;
|
||||
this._clipNameToIndex.clear();
|
||||
clips.forEach((clip, index) => {
|
||||
this._clipNameToIndex.set(clip.name, index);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 播放动画
|
||||
* Play animation
|
||||
*
|
||||
* @param clipName - 动画片段名称,不指定则播放当前/默认片段
|
||||
*/
|
||||
public play(clipName?: string): void {
|
||||
const name = clipName ?? this.defaultClip ?? (this._clips[0]?.name ?? '');
|
||||
|
||||
if (!name) {
|
||||
console.warn('[Animation3DComponent] No clip to play');
|
||||
return;
|
||||
}
|
||||
|
||||
const index = this._clipNameToIndex.get(name);
|
||||
if (index === undefined) {
|
||||
console.warn(`[Animation3DComponent] Clip not found: ${name}`);
|
||||
return;
|
||||
}
|
||||
|
||||
this._currentClip = this._clips[index];
|
||||
this._currentTime = 0;
|
||||
this._direction = 1;
|
||||
this._playState = AnimationPlayState.Playing;
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止动画
|
||||
* Stop animation
|
||||
*/
|
||||
public stop(): void {
|
||||
this._playState = AnimationPlayState.Stopped;
|
||||
this._currentTime = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 暂停动画
|
||||
* Pause animation
|
||||
*/
|
||||
public pause(): void {
|
||||
if (this._playState === AnimationPlayState.Playing) {
|
||||
this._playState = AnimationPlayState.Paused;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复播放
|
||||
* Resume playback
|
||||
*/
|
||||
public resume(): void {
|
||||
if (this._playState === AnimationPlayState.Paused) {
|
||||
this._playState = AnimationPlayState.Playing;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置播放时间
|
||||
* Set playback time
|
||||
*/
|
||||
public setTime(time: number): void {
|
||||
this._currentTime = Math.max(0, Math.min(time, this.duration));
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置归一化时间
|
||||
* Set normalized time
|
||||
*/
|
||||
public setNormalizedTime(t: number): void {
|
||||
this._currentTime = Math.max(0, Math.min(t, 1)) * this.duration;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新播放时间(由 Animation3DSystem 调用)
|
||||
* Update playback time (called by Animation3DSystem)
|
||||
*
|
||||
* @param deltaTime - 时间增量(秒)
|
||||
*/
|
||||
public updateTime(deltaTime: number): void {
|
||||
if (this._playState !== AnimationPlayState.Playing || !this._currentClip) {
|
||||
return;
|
||||
}
|
||||
|
||||
const scaledDelta = deltaTime * this.speed * this._direction;
|
||||
this._currentTime += scaledDelta;
|
||||
|
||||
const duration = this._currentClip.duration;
|
||||
if (duration <= 0) return;
|
||||
|
||||
// Handle wrap mode
|
||||
// 处理循环模式
|
||||
switch (this.wrapMode) {
|
||||
case AnimationWrapMode.Once:
|
||||
if (this._currentTime >= duration || this._currentTime < 0) {
|
||||
this._currentTime = Math.max(0, Math.min(this._currentTime, duration));
|
||||
this._playState = AnimationPlayState.Stopped;
|
||||
}
|
||||
break;
|
||||
|
||||
case AnimationWrapMode.Loop:
|
||||
while (this._currentTime >= duration) {
|
||||
this._currentTime -= duration;
|
||||
}
|
||||
while (this._currentTime < 0) {
|
||||
this._currentTime += duration;
|
||||
}
|
||||
break;
|
||||
|
||||
case AnimationWrapMode.PingPong:
|
||||
if (this._currentTime >= duration) {
|
||||
this._currentTime = duration - (this._currentTime - duration);
|
||||
this._direction = -1;
|
||||
} else if (this._currentTime < 0) {
|
||||
this._currentTime = -this._currentTime;
|
||||
this._direction = 1;
|
||||
}
|
||||
break;
|
||||
|
||||
case AnimationWrapMode.ClampForever:
|
||||
this._currentTime = Math.max(0, Math.min(this._currentTime, duration));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置组件
|
||||
* Reset component
|
||||
*/
|
||||
reset(): void {
|
||||
this.defaultClip = '';
|
||||
this.speed = 1.0;
|
||||
this.wrapMode = AnimationWrapMode.Loop;
|
||||
this.playOnAwake = true;
|
||||
this._playState = AnimationPlayState.Stopped;
|
||||
this._currentClip = null;
|
||||
this._currentTime = 0;
|
||||
this._direction = 1;
|
||||
this._clips = [];
|
||||
this._clipNameToIndex.clear();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user