视频导出、时间轴与动作姿势模块
- 视频导出:WebCodecs 逐帧快速导出 MP4(固定分辨率不随视口布局变形),MediaRecorder 兜底 - 时间轴:面板可拖动调高(持久化、固定上限)、关键帧轨道区内滚动、无横向滚动条 - 动作模块:走路/跑步步态曲线(对侧摆臂修复、跑步独立步频、跑姿屈膝曲线、参考校准参数) - 姿势调节:两级折叠分组、动作偏移联动 - 删除对象时同步清理时间轴轨道 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3b053c714c
commit
59e68e444a
29 files changed
+3564
-166
No files matched your search
@@ -14,6 +14,7 @@ import { Euler, Matrix4, PerspectiveCamera as ThreePerspectiveCamera, Quaternion
|
||||
import type { Object3D } from "three";
|
||||
import type { OrbitControls as OrbitControlsImpl } from "three-stdlib";
|
||||
import { clearViewportCaptureHandler, setViewportCaptureHandler } from "../io/captureBridge";
|
||||
import { setVideoExportContext } from "../io/videoExportBridge";
|
||||
import { buildScreenshotMeta, type ScreenshotResult } from "../io/screenshotExport";
|
||||
import { useDirectorStore, type CameraShotSnapshot } from "../store/directorStore";
|
||||
import { DEFAULT_DIRECTOR_CAMERA_VIEW_SNAPSHOT, getCameraViewSnapshotFromShot } from "../schema/cameraGeometry";
|
||||
@@ -481,6 +482,18 @@ function CanvasCaptureBridge({
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 把渲染三件套注册给视频导出逻辑(逐帧离线渲染用),模式仿 CanvasCaptureBridge */
|
||||
function VideoExportBridge() {
|
||||
const { camera, gl, scene } = useThree();
|
||||
|
||||
useEffect(() => {
|
||||
setVideoExportContext({ gl, scene, camera: camera as ThreePerspectiveCamera });
|
||||
return () => setVideoExportContext(null);
|
||||
}, [camera, gl, scene]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function DirectorViewCameraSync({
|
||||
controlsRef,
|
||||
snapshot,
|
||||
@@ -737,6 +750,7 @@ export function DirectorCanvas() {
|
||||
viewMode={viewMode}
|
||||
/>
|
||||
<PlaybackFrameDriver />
|
||||
<VideoExportBridge />
|
||||
<PlaybackTransformSync />
|
||||
<ActiveCameraPlaybackSync activeCameraId={activeCamera?.id ?? null} />
|
||||
<Suspense fallback={null}>
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import { useFrame, useThree } from "@react-three/fiber";
|
||||
import { useDirectorStore } from "../store/directorStore";
|
||||
import { evaluateCameraTrackAtTime, evaluateTrackAtTime } from "../schema/timeline";
|
||||
import { usePlaybackStore } from "../runtime/playbackStore";
|
||||
import { getViewportCameraQuaternion } from "./SceneRoot";
|
||||
import { applyTimelineTransformsAtTime } from "../runtime/playbackTransforms";
|
||||
|
||||
/**
|
||||
* 挂在 <Canvas> 内:每帧根据 playhead 把物体/相机线框的插值结果直接写到
|
||||
* 对应的 Three.js 对象。SceneRoot 的 group 通过 userData 标记身份:
|
||||
* - directorObjectId → 物体(覆盖 position/rotation/scale)
|
||||
* - directorCameraId → 导演视图相机线框(覆盖 position + 朝向 target)
|
||||
* 对应的 Three.js 对象(逻辑在 playbackTransforms.ts,与视频导出共用)。
|
||||
* 无轨道或无关键帧的对象保持静态,不被触碰。
|
||||
*/
|
||||
export function PlaybackTransformSync() {
|
||||
@@ -19,39 +16,8 @@ export function PlaybackTransformSync() {
|
||||
const editingObjectId = usePlaybackStore((state) => state.editingObjectId);
|
||||
|
||||
useFrame(() => {
|
||||
if (tracks.length === 0 && cameraTracks.length === 0) return;
|
||||
|
||||
const trackMap = new Map(tracks.map((track) => [track.objectId, track]));
|
||||
const cameraTrackMap = new Map(cameraTracks.map((track) => [track.cameraId, track]));
|
||||
|
||||
scene.traverse((object) => {
|
||||
// 播放、scrub 时都按 playhead 插值覆盖;但跳过正在被 gizmo 拖拽的对象(editingObjectId),
|
||||
// 否则 gizmo 改的值会被立刻覆盖回去,拖不动。
|
||||
const objectId = object.userData?.directorObjectId;
|
||||
if (objectId && objectId !== editingObjectId) {
|
||||
const track = trackMap.get(objectId);
|
||||
if (track && track.keyframes.length > 0) {
|
||||
const value = evaluateTrackAtTime(track, playhead);
|
||||
if (value) {
|
||||
object.position.set(value.position[0], value.position[1], value.position[2]);
|
||||
object.rotation.set(value.rotation[0], value.rotation[1], value.rotation[2]);
|
||||
object.scale.set(value.scale[0], value.scale[1], value.scale[2]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const cameraId = object.userData?.directorCameraId;
|
||||
if (cameraId && cameraId !== editingObjectId) {
|
||||
const track = cameraTrackMap.get(cameraId);
|
||||
if (track && track.keyframes.length > 0) {
|
||||
const value = evaluateCameraTrackAtTime(track, playhead);
|
||||
if (value) {
|
||||
object.position.set(value.position[0], value.position[1], value.position[2]);
|
||||
object.quaternion.copy(getViewportCameraQuaternion(value.position, value.target));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
// 播放、scrub 时都按 playhead 插值覆盖;editingObjectId 的跳过逻辑在共享函数里
|
||||
applyTimelineTransformsAtTime(scene, tracks, cameraTracks, playhead, editingObjectId);
|
||||
});
|
||||
|
||||
return null;
|
||||
|
||||
@@ -476,6 +476,7 @@ function ObjectSceneNode({
|
||||
} | null>(null);
|
||||
const updateObjectTransform = useDirectorStore((state) => state.updateObjectTransform);
|
||||
const isPlaying = usePlaybackStore((state) => state.isPlaying);
|
||||
const motionTime = usePlaybackStore((state) => state.playhead);
|
||||
const isImportedModel = asset?.sourceType === "model";
|
||||
const characterLabelKey = `${item.id}:${item.bodyType ?? ""}:${item.characterRig?.rigType ?? ""}`;
|
||||
const fallbackCharacterLabelY =
|
||||
@@ -539,6 +540,8 @@ function ObjectSceneNode({
|
||||
color={item.color}
|
||||
onLabelAnchorYChange={handleCharacterLabelAnchorYChange}
|
||||
rigState={item.characterRig}
|
||||
motionTime={motionTime}
|
||||
characterId={item.id}
|
||||
/>
|
||||
</Suspense>
|
||||
{showLabels ? (
|
||||
|
||||
Reference in new issue
Block a user