refactor(io): 机位视角导出改用监视器画布渲染
This commit is contained in:
26
src/editor/io/monitorRenderBridge.ts
Normal file
26
src/editor/io/monitorRenderBridge.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { PerspectiveCamera, Scene, WebGLRenderer } from "three";
|
||||
|
||||
/**
|
||||
* 把「镜头监视器」那块独立 Canvas 的渲染三件套(renderer / scene / camera)暴露给视频导出。
|
||||
*
|
||||
* 机位视角导出直接在这块画布的 gl 上、用它自己的 scene / camera、按导出分辨率逐帧渲染 ——
|
||||
* 与监视器物理上就是同一份渲染(同一颗 gl、同一个场景、同一套景深管线),
|
||||
* 天然逐像素一致,也省得把主视口的场景辅助元素再隐藏一遍(监视器场景本就只有成片内容)。
|
||||
*
|
||||
* 模式与 videoExportBridge / captureBridge 一致:Canvas 内的桥组件注册,导出方读取。
|
||||
*/
|
||||
export interface MonitorRenderContext {
|
||||
gl: WebGLRenderer;
|
||||
scene: Scene;
|
||||
camera: PerspectiveCamera;
|
||||
}
|
||||
|
||||
let monitorRenderContext: MonitorRenderContext | null = null;
|
||||
|
||||
export function setMonitorRenderContext(context: MonitorRenderContext | null) {
|
||||
monitorRenderContext = context;
|
||||
}
|
||||
|
||||
export function getMonitorRenderContext(): MonitorRenderContext | null {
|
||||
return monitorRenderContext;
|
||||
}
|
||||
@@ -22,6 +22,12 @@ vi.mock("./videoExportBridge", () => ({
|
||||
getVideoExportContext: () => contextRef.current,
|
||||
}));
|
||||
|
||||
// 机位视角导出改为「从监视器那块画布出片」,这里让监视器渲染上下文同样指向这套假上下文:
|
||||
// 于是机位视角导出用的就是同一颗(被记录的)gl / scene / camera。
|
||||
vi.mock("./monitorRenderBridge", () => ({
|
||||
getMonitorRenderContext: () => contextRef.current,
|
||||
}));
|
||||
|
||||
// 相机视角导出会走 renderSceneWithBokeh(真实 EffectComposer / BokehPass 在 jsdom 下无 WebGL)。
|
||||
// 桩里直接委托给 gl.render,既能避开 WebGL,又保留「记录每帧相机位姿」的测试意图
|
||||
// (createFakeBridge 只在 gl.render 时把相机位姿写入 bridge.rendered)。
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useDirectorStore } from "../store/directorStore";
|
||||
import { usePlaybackStore } from "../runtime/playbackStore";
|
||||
import type { Scene } from "three";
|
||||
import { getVideoExportContext } from "./videoExportBridge";
|
||||
import { getMonitorRenderContext, type MonitorRenderContext } from "./monitorRenderBridge";
|
||||
import { hideViewportCaptureHelpers, withViewportCaptureHelpersHidden } from "../canvas/viewportCapture";
|
||||
import { applyActiveCameraViewAtTime, applyTimelineTransformsAtTime, resolveLensAtTime } from "../runtime/playbackTransforms";
|
||||
import { getCameraRenderQuaternion, getCameraViewSnapshotFromShot, getWorldCameraViewSnapshot, resolveLens, type CameraViewSnapshot } from "../schema/cameraGeometry";
|
||||
@@ -56,6 +57,21 @@ function waitNextFrame(): Promise<void> {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 机位视角导出从「镜头监视器」那块画布出片:需要等它进入自渲染模式并注册好渲染上下文。
|
||||
* 监视器的自渲染 Canvas 是条件挂载的(机位视角平时显示的是主视口镜像),
|
||||
* 导出标志置位 + React 重渲染 + WebGL 上下文就绪需要几帧,这里轮询等待。
|
||||
*/
|
||||
async function waitForMonitorRenderContext(timeoutMs = 4000): Promise<MonitorRenderContext | null> {
|
||||
const deadline = performance.now() + timeoutMs;
|
||||
while (performance.now() < deadline) {
|
||||
const context = getMonitorRenderContext();
|
||||
if (context) return context;
|
||||
await waitNextFrame();
|
||||
}
|
||||
return getMonitorRenderContext();
|
||||
}
|
||||
|
||||
/**
|
||||
* 机位没有运镜轨道时的兜底:把「这个机位自己的视角」直接写到导出相机上。
|
||||
*
|
||||
@@ -149,10 +165,17 @@ async function pickVideoEncoderConfig(
|
||||
* - H.264 编码进 MP4(Chrome / Electron 均支持)
|
||||
*/
|
||||
async function exportVideoFrameByFrame(view: VideoExportView): Promise<void> {
|
||||
const bridge = getVideoExportContext();
|
||||
if (!bridge) throw new Error("找不到视口渲染上下文");
|
||||
// 机位视角「直接用监视器那块画布出片」:同一颗 gl / 同一个场景(本就只有成片内容、没有编辑器辅助)/
|
||||
// 同一颗相机 / 同一套景深管线 —— 导出的画面与监视器所见就是同一份渲染,天然逐像素一致。
|
||||
// 上帝视角仍走主视口(监视器只显示机位画面)。
|
||||
const bridge = view === "camera" ? await waitForMonitorRenderContext() : getVideoExportContext();
|
||||
if (!bridge) {
|
||||
throw new Error(view === "camera" ? "找不到镜头监视器的渲染上下文" : "找不到视口渲染上下文");
|
||||
}
|
||||
const { gl, scene, camera } = bridge;
|
||||
const canvas = gl.domElement;
|
||||
// 机位视角的渲染源是监视器自己那块画布,不必去动主视口的视角
|
||||
const switchesViewportView = view !== "camera";
|
||||
|
||||
const directorStore = useDirectorStore.getState();
|
||||
const exportSettings = directorStore.project.exportSettings;
|
||||
@@ -213,7 +236,7 @@ async function exportVideoFrameByFrame(view: VideoExportView): Promise<void> {
|
||||
|
||||
// 切到导出状态:固定分辨率渲染(不改 CSS 尺寸,canvas 加 object-fit: contain 保持显示不变形)
|
||||
usePlaybackStore.getState().setPlaying(false);
|
||||
directorStore.setViewMode(view);
|
||||
if (switchesViewportView) directorStore.setViewMode(view);
|
||||
canvas.classList.add("is-video-exporting");
|
||||
gl.setPixelRatio(1);
|
||||
gl.setSize(width, height, false);
|
||||
@@ -235,7 +258,8 @@ async function exportVideoFrameByFrame(view: VideoExportView): Promise<void> {
|
||||
if (encodeError) throw encodeError;
|
||||
const time = Math.min(frameIndex / fps, duration);
|
||||
// 相机可能因视角切换被替换,每帧从桥取最新;并保证导出画面比例
|
||||
const frameCamera = getVideoExportContext()?.camera ?? camera;
|
||||
const frameCamera =
|
||||
(view === "camera" ? getMonitorRenderContext()?.camera : getVideoExportContext()?.camera) ?? camera;
|
||||
frameCamera.aspect = width / height;
|
||||
frameCamera.updateProjectionMatrix();
|
||||
|
||||
@@ -349,11 +373,14 @@ async function exportVideoFrameByFrame(view: VideoExportView): Promise<void> {
|
||||
// - aspect 两颗都要还,否则切回导演视角后画面比例还停在导出的 16:9;
|
||||
// - 位姿只在「导出用的就是导出前那颗相机」时还,否则等于把导演相机的位姿
|
||||
// 写到机位相机上,机位视角就会被带歪。
|
||||
const restoreCamera = getVideoExportContext()?.camera ?? camera;
|
||||
const restoreCamera =
|
||||
(view === "camera" ? getMonitorRenderContext()?.camera : getVideoExportContext()?.camera) ?? camera;
|
||||
restoreCamera.aspect = previousAspect;
|
||||
restoreCamera.updateProjectionMatrix();
|
||||
camera.aspect = previousAspect;
|
||||
camera.updateProjectionMatrix();
|
||||
if (restoreCamera !== camera) {
|
||||
camera.aspect = previousAspect;
|
||||
camera.updateProjectionMatrix();
|
||||
}
|
||||
|
||||
if (restoreCamera === camera) {
|
||||
restoreCamera.position.copy(previousCameraPosition);
|
||||
@@ -362,7 +389,7 @@ async function exportVideoFrameByFrame(view: VideoExportView): Promise<void> {
|
||||
restoreCamera.updateProjectionMatrix();
|
||||
}
|
||||
|
||||
directorStore.setViewMode(previousViewMode);
|
||||
if (switchesViewportView) directorStore.setViewMode(previousViewMode);
|
||||
usePlaybackStore.getState().setPlayhead(previousPlayhead);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user