fix: 优化空格播放和截图角色标签对不上
This commit is contained in:
@@ -1,7 +1,26 @@
|
||||
import { beforeEach, expect, it } from "vitest";
|
||||
import { beforeEach, expect, it, vi } from "vitest";
|
||||
import { Object3D, PerspectiveCamera, Scene, Vector2 } from "three";
|
||||
import { usePlaybackStore } from "../runtime/playbackStore";
|
||||
import { useDirectorStore } from "../store/directorStore";
|
||||
import { exportVideo } from "./videoExport";
|
||||
|
||||
vi.mock("mp4-muxer", () => ({
|
||||
Muxer: class {
|
||||
target = { buffer: new ArrayBuffer(8) };
|
||||
addVideoChunk() {}
|
||||
finalize() {}
|
||||
},
|
||||
ArrayBufferTarget: class {},
|
||||
}));
|
||||
|
||||
const contextRef = vi.hoisted(() => ({
|
||||
current: null as null | { gl: any; scene: Scene; camera: PerspectiveCamera },
|
||||
}));
|
||||
|
||||
vi.mock("./videoExportBridge", () => ({
|
||||
getVideoExportContext: () => contextRef.current,
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
usePlaybackStore.setState({ isPlaying: false, playhead: 0, duration: 5, loop: false, editingObjectId: null });
|
||||
});
|
||||
@@ -44,3 +63,68 @@ it("keeps the playhead where the export found it", async () => {
|
||||
|
||||
expect(usePlaybackStore.getState().playhead).toBe(2.5);
|
||||
});
|
||||
|
||||
it("hides editor helpers (gizmo / grid / axes) while rendering exported frames, then restores them", async () => {
|
||||
// 模拟场景里有一个「导出时隐藏」的辅助物体(也就是选中对象拖拽 gizmo 的那三条红绿蓝轴)
|
||||
const scene = new Scene();
|
||||
const helper = new Object3D();
|
||||
helper.userData.hideFromViewportCapture = true;
|
||||
const solidObject = new Object3D();
|
||||
scene.add(helper, solidObject);
|
||||
|
||||
const visibilityDuringRender: boolean[] = [];
|
||||
const gl = {
|
||||
domElement: document.createElement("canvas"),
|
||||
render: vi.fn(() => {
|
||||
// 在渲染这一帧的瞬间记录辅助物体的可见性
|
||||
visibilityDuringRender.push(helper.visible);
|
||||
}),
|
||||
getSize: (size: Vector2) => size.set(300, 150),
|
||||
getPixelRatio: () => 1,
|
||||
setPixelRatio: () => {},
|
||||
setSize: () => {},
|
||||
};
|
||||
const camera = new PerspectiveCamera();
|
||||
contextRef.current = { gl, scene, camera };
|
||||
|
||||
// 短时长,缩小导出帧数,加速测试
|
||||
useDirectorStore.getState().setTimelineDuration(0.1);
|
||||
|
||||
// jsdom 没有 WebCodecs,这里把逐帧导出需要的几个全局桩好
|
||||
const VideoEncoderStub = class {
|
||||
static isConfigSupported = () => Promise.resolve({ supported: true });
|
||||
output = () => {};
|
||||
error = () => {};
|
||||
encodeQueueSize = 0;
|
||||
configure() {}
|
||||
encode() {}
|
||||
flush() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
close() {}
|
||||
};
|
||||
const VideoFrameStub = class {
|
||||
close() {}
|
||||
};
|
||||
vi.stubGlobal("VideoEncoder", VideoEncoderStub);
|
||||
vi.stubGlobal("VideoFrame", VideoFrameStub);
|
||||
vi.stubGlobal("URL", {
|
||||
...URL,
|
||||
createObjectURL: () => "blob:fake",
|
||||
revokeObjectURL: () => {},
|
||||
});
|
||||
|
||||
try {
|
||||
await exportVideo("director");
|
||||
} finally {
|
||||
vi.unstubAllGlobals();
|
||||
}
|
||||
|
||||
expect(gl.render).toHaveBeenCalled();
|
||||
// 每一帧渲染时,辅助物体都必须藏起来,否则导出的视频里会带着 gizmo 红绿蓝轴
|
||||
expect(visibilityDuringRender.length).toBeGreaterThan(0);
|
||||
expect(visibilityDuringRender.every((v) => v === false)).toBe(true);
|
||||
// 导出结束后原样恢复
|
||||
expect(helper.visible).toBe(true);
|
||||
expect(solidObject.visible).toBe(true);
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Vector2 } from "three";
|
||||
import { useDirectorStore } from "../store/directorStore";
|
||||
import { usePlaybackStore } from "../runtime/playbackStore";
|
||||
import { getVideoExportContext } from "./videoExportBridge";
|
||||
import { hideViewportCaptureHelpers, withViewportCaptureHelpersHidden } from "../canvas/viewportCapture";
|
||||
import { applyActiveCameraViewAtTime, applyTimelineTransformsAtTime } from "../runtime/playbackTransforms";
|
||||
import { getViewportAspectRatioValue } from "../schema/viewportAspectRatio";
|
||||
import { normalizeTimelineFps } from "../schema/timeline";
|
||||
@@ -190,7 +191,9 @@ async function exportVideoFrameByFrame(view: VideoExportView): Promise<void> {
|
||||
editingObjectId: usePlaybackStore.getState().editingObjectId,
|
||||
});
|
||||
|
||||
gl.render(scene, frameCamera);
|
||||
// 和截图同一套:渲染这一帧前先把编辑器辅助元素(gizmo 红绿蓝轴 / 网格 / 坐标轴 / 轨迹)藏起来,
|
||||
// 否则导出的视频里会带着选中的 gizmo 三条轴。渲完 withViewportCaptureHelpersHidden 会原样恢复。
|
||||
withViewportCaptureHelpersHidden(scene, () => gl.render(scene, frameCamera));
|
||||
|
||||
const frame = new VideoFrame(canvas, {
|
||||
timestamp: frameIndex * frameDurationUs,
|
||||
@@ -250,12 +253,20 @@ async function exportVideoWithMediaRecorder(view: VideoExportView): Promise<void
|
||||
const directorStore = useDirectorStore.getState();
|
||||
const duration = directorStore.project.timeline.duration;
|
||||
|
||||
// 实时录屏兜底路径录的是「实时画面」,整段录制期间都要藏住编辑器辅助元素
|
||||
//(gizmo 红绿蓝轴 / 网格 / 坐标轴等)—— 不能像逐帧路径那样只在某一帧渲染时隐藏。
|
||||
const captureBridge = getVideoExportContext();
|
||||
const restoreCaptureHelpers = captureBridge ? hideViewportCaptureHelpers(captureBridge.scene) : () => {};
|
||||
|
||||
// 切视角 + 复位指针(时间轴本身就是单次播放,到末尾会自己停住)
|
||||
directorStore.setViewMode(view);
|
||||
usePlaybackStore.getState().setPlayhead(0);
|
||||
|
||||
const stopped = new Promise<void>((resolve) => {
|
||||
recorder.onstop = () => resolve();
|
||||
recorder.onstop = () => {
|
||||
restoreCaptureHelpers();
|
||||
resolve();
|
||||
};
|
||||
});
|
||||
|
||||
recorder.start();
|
||||
|
||||
Reference in New Issue
Block a user