feat(编辑器): 支持导入 GLB/GLTF 模型并驱动角色动画

- 新增 GLB/GLTF 模型加载与渲染,支持内嵌动画播放
- 通过 IndexedDB 持久化大体积模型二进制,规避 localStorage 限制
- 新增动作片段序列,时间轴可添加、编辑与编排多段动作
- 优化机位视角相机同步,修复角度修改不生效问题
- 扩展骨骼识别,兼容 Mixamo 前缀及 UE4 骨架
This commit is contained in:
tongtong committed 2026-09-18 18:23:13 +08:00
1 parent 9d3a5b9bea
commit 38dc27501a
36 files changed
+1899 -427

No files matched your search

+42 -6
View File
@@ -1,8 +1,10 @@
import { Component, type ReactNode } from "react";
import type { CharacterRigState } from "../schema/directorProject";
import type { CharacterRigState, MotionClip } from "../schema/directorProject";
import { PrimitiveMannequin } from "./PrimitiveMannequin";
import { UE4MannequinModel } from "./UE4MannequinModel";
import { GltfCharacterModel } from "./GltfCharacterModel";
import type { CharacterBodyType } from "./mannequin/bodyTypes";
import { getActiveMotionClipAtTime, resolveMotionClipEnd } from "./mannequin/motion";
interface CharacterModelProps {
bodyType?: CharacterBodyType;
@@ -15,6 +17,12 @@ interface CharacterModelProps {
motionStartTime?: number;
/** 动作片段终点(秒):缺省视为时间轴末尾 */
motionEndTime?: number;
/** 时间轴上的动作片段序列(多段动作)。存在时按播放时间取当前片段覆盖上面的单一 motion。 */
motionClips?: MotionClip[];
/** 时间轴总时长(秒),用于解析到末尾的动作片段。 */
duration: number;
/** 当 rigState.modelAssetId 指向一个导入的 GLB/GLTF 资产时,其可访问的 url(用于渲染该角色几何)。 */
modelAssetUrl?: string;
}
class CharacterModelBoundary extends Component<
@@ -37,8 +45,36 @@ class CharacterModelBoundary extends Component<
}
}
export function CharacterModel({ bodyType, color, onLabelAnchorYChange, rigState, motionTime, characterId, motionStartTime, motionEndTime }: CharacterModelProps) {
const fallback = <PrimitiveMannequin bodyType={bodyType} color={color} rigState={rigState} motionTime={motionTime} characterId={characterId} motionStartTime={motionStartTime} motionEndTime={motionEndTime} />;
export function CharacterModel({ bodyType, color, onLabelAnchorYChange, rigState, motionTime, characterId, motionStartTime, motionEndTime, modelAssetUrl, motionClips, duration }: CharacterModelProps) {
// 动作序列:按当前播放时间取出应生效的片段(后段覆盖前段),用它覆盖单一 motion 字段。
// 无片段 / 时间不在任何片段内 → 回退到旧的单一段落(rigState.motion + motionStartTime/EndTime)。
const activeClip = getActiveMotionClipAtTime(motionClips, motionTime ?? 0, duration);
const resolvedRigState: CharacterRigState | undefined = activeClip && rigState
? { ...rigState, motion: activeClip }
: rigState;
const resolvedStart = activeClip ? activeClip.start : motionStartTime;
const resolvedEnd = activeClip ? resolveMotionClipEnd(activeClip, duration) : motionEndTime;
const fallback = <PrimitiveMannequin bodyType={bodyType} color={color} rigState={resolvedRigState} motionTime={motionTime} characterId={characterId} motionStartTime={resolvedStart} motionEndTime={resolvedEnd} />;
// 导入的 GLB/GLTF 人形角色:几何来自资产,用通用人形骨骼映射驱动程序化动作。
if (rigState?.modelAssetId && modelAssetUrl) {
return (
<CharacterModelBoundary fallback={fallback}>
<GltfCharacterModel
url={modelAssetUrl}
bodyType={bodyType}
color={color}
onLabelAnchorYChange={onLabelAnchorYChange}
rigState={resolvedRigState}
motionTime={motionTime}
characterId={characterId}
motionStartTime={resolvedStart}
motionEndTime={resolvedEnd}
/>
</CharacterModelBoundary>
);
}
if (rigState?.rigType !== "ue4-mannequin") {
return fallback;
@@ -50,11 +86,11 @@ export function CharacterModel({ bodyType, color, onLabelAnchorYChange, rigState
bodyType={bodyType}
color={color}
onLabelAnchorYChange={onLabelAnchorYChange}
rigState={rigState}
rigState={resolvedRigState}
motionTime={motionTime}
characterId={characterId}
motionStartTime={motionStartTime}
motionEndTime={motionEndTime}
motionStartTime={resolvedStart}
motionEndTime={resolvedEnd}
/>
</CharacterModelBoundary>
);