refactor(io): 机位视角导出改用监视器画布渲染
This commit is contained in:
1 parent
7e28c73643
commit
f778f05957
7 files changed
+191
-101
No files matched your search
@@ -1,9 +1,10 @@
|
||||
import { Canvas, useFrame, useThree } from "@react-three/fiber";
|
||||
import { PerspectiveCamera as DreiPerspectiveCamera } from "@react-three/drei";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||
import { PerspectiveCamera as ThreePerspectiveCamera, Raycaster, Scene, Vector2 } from "three";
|
||||
import { useDirectorStore } from "../store/directorStore";
|
||||
import { usePlaybackStore } from "../runtime/playbackStore";
|
||||
import { setMonitorRenderContext } from "../io/monitorRenderBridge";
|
||||
import { SceneRoot } from "./SceneRoot";
|
||||
import { ViewportBackground } from "./ViewportBackground";
|
||||
import { PlaybackTransformSync } from "./PlaybackTransformSync";
|
||||
@@ -54,6 +55,7 @@ function LensMonitorCamera({
|
||||
const cameras = useDirectorStore((state) => state.project.cameras);
|
||||
const transitionClips = useDirectorStore((state) => state.project.timeline.transitionClips);
|
||||
const globalTransition = useDirectorStore((state) => state.project.timeline.cameraTransition);
|
||||
const isExporting = usePlaybackStore((state) => state.isExporting);
|
||||
// 关键帧感知:拖播放头时,监视器上的对焦距离 / 光圈 / 焦段 / 景深强度都按关键帧插值,
|
||||
// 让「移焦 / 渐入渐出虚化 / 变焦」在监视器上所见即所得(与导出管线一致的 resolveLensAtTime)。
|
||||
const lens = useMemo(
|
||||
@@ -84,6 +86,15 @@ function LensMonitorCamera({
|
||||
}, [camera, cameras, cameraTracks, transitionClips, globalTransition, playhead, sceneSettings]);
|
||||
const sensorAspect = lens.sensor.widthMm / lens.sensor.heightMm;
|
||||
|
||||
// 把监视器的渲染三件套注册给视频导出:机位视角导出直接用这块画布 / 这颗相机 / 这个场景出片,
|
||||
// 因此导出的画面与监视器所见是同一份渲染,逐像素一致。
|
||||
useLayoutEffect(() => {
|
||||
const renderCamera = cameraRef.current;
|
||||
if (!renderCamera) return undefined;
|
||||
setMonitorRenderContext({ gl, scene, camera: renderCamera });
|
||||
return () => setMonitorRenderContext(null);
|
||||
}, [gl, scene]);
|
||||
|
||||
useFrame(() => {
|
||||
const renderCamera = cameraRef.current;
|
||||
if (!renderCamera || !worldView) return;
|
||||
@@ -100,6 +111,10 @@ function LensMonitorCamera({
|
||||
renderCamera.updateProjectionMatrix();
|
||||
camRef.current = renderCamera;
|
||||
|
||||
// 导出期间由 videoExport 在这块画布上逐帧渲染(同一颗 gl / 同一个场景 / 同一颗相机),
|
||||
// 这里跳过,避免和导出循环双重渲染、也避免争用同一份景深合成器缓存。
|
||||
if (isExporting) return;
|
||||
|
||||
// 与主视口 / 视频导出共用同一份景深渲染(renderSceneWithBokeh):监视器不再自建一套
|
||||
// EffectComposer —— 两套实现很容易漂移,导出与监视器的取景 / 虚化就会对不上。
|
||||
// 合成器按 gl 缓存、画布尺寸变化会自动重建;不支持后处理时回退普通渲染,监视器永远可用。
|
||||
@@ -124,70 +139,8 @@ const PITCH_LIMIT = 89;
|
||||
/** 区分「单击对焦」与「拖拽瞄准」的位移阈值(像素) */
|
||||
const CLICK_THRESHOLD_PX = 4;
|
||||
|
||||
/**
|
||||
* 机位视角下监视器的「镜像」:主视口此刻渲染的就是机位画面,监视器没必要再开一个 Canvas
|
||||
* 把整个场景重渲一遍(第二份场景 + 第二套景深后处理,既卡,又容易和主视口取景 / 虚化漂移)。
|
||||
* 这里直接把主视口画布按「传感器画幅」居中裁一块拷过来,逐像素与主视口一致。
|
||||
*
|
||||
* fov 是垂直的:视口比传感器画幅更宽时按宽度裁,垂直取景保持不变 ——
|
||||
* 结果与监视器自己用传感器画幅渲染完全一致。
|
||||
*/
|
||||
function MonitorMirror({ sensorAspect }: { sensorAspect: number }) {
|
||||
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let frame = 0;
|
||||
const draw = () => {
|
||||
frame = requestAnimationFrame(draw);
|
||||
const target = canvasRef.current;
|
||||
const source = document.querySelector<HTMLCanvasElement>(".director-canvas canvas");
|
||||
if (!target || !source) return;
|
||||
|
||||
// 输出尺寸跟随容器(CSS 像素)
|
||||
const cssWidth = target.clientWidth;
|
||||
const cssHeight = target.clientHeight;
|
||||
if (!cssWidth || !cssHeight) return;
|
||||
if (target.width !== cssWidth || target.height !== cssHeight) {
|
||||
target.width = cssWidth;
|
||||
target.height = cssHeight;
|
||||
}
|
||||
|
||||
const sourceWidth = source.width;
|
||||
const sourceHeight = source.height;
|
||||
if (!sourceWidth || !sourceHeight) return;
|
||||
|
||||
let cropWidth = sourceWidth;
|
||||
let cropHeight = Math.round(sourceWidth / sensorAspect);
|
||||
if (cropHeight > sourceHeight) {
|
||||
cropHeight = sourceHeight;
|
||||
cropWidth = Math.round(sourceHeight * sensorAspect);
|
||||
}
|
||||
|
||||
const context = target.getContext("2d");
|
||||
if (!context) return;
|
||||
context.drawImage(
|
||||
source,
|
||||
Math.round((sourceWidth - cropWidth) / 2),
|
||||
Math.round((sourceHeight - cropHeight) / 2),
|
||||
cropWidth,
|
||||
cropHeight,
|
||||
0,
|
||||
0,
|
||||
target.width,
|
||||
target.height
|
||||
);
|
||||
};
|
||||
|
||||
frame = requestAnimationFrame(draw);
|
||||
return () => cancelAnimationFrame(frame);
|
||||
}, [sensorAspect]);
|
||||
|
||||
return <canvas className="lens-monitor-canvas" ref={canvasRef} />;
|
||||
}
|
||||
|
||||
export function LensMonitor({ rightOffset = 12 }: { rightOffset?: number }) {
|
||||
const sceneSettings = useDirectorStore((state) => state.project.scene);
|
||||
const viewMode = useDirectorStore((state) => state.viewMode);
|
||||
const assets = useDirectorStore((state) => state.project.assets);
|
||||
const panoramaAssetId = useDirectorStore((state) => state.project.panoramaAssetId);
|
||||
const panoramaAsset = assets.find((item) => item.id === panoramaAssetId);
|
||||
@@ -199,16 +152,23 @@ export function LensMonitor({ rightOffset = 12 }: { rightOffset?: number }) {
|
||||
const beginUndoBatch = useDirectorStore((state) => state.beginUndoBatch);
|
||||
const endUndoBatch = useDirectorStore((state) => state.endUndoBatch);
|
||||
const playhead = usePlaybackStore((state) => state.playhead);
|
||||
const isPlaying = usePlaybackStore((state) => state.isPlaying);
|
||||
const isScrubbing = usePlaybackStore((state) => state.isScrubbing);
|
||||
const isExporting = usePlaybackStore((state) => state.isExporting);
|
||||
const cameras = useDirectorStore((state) => state.project.cameras);
|
||||
const transitionClips = useDirectorStore((state) => state.project.timeline.transitionClips);
|
||||
const cameraTracks = useDirectorStore((state) => state.project.timeline.cameraTracks);
|
||||
// 当前时刻「正在显示」的机位:与视频导出用同一个 getActiveCameraIdAtTime 解析。
|
||||
// 多机位自动切换 / 过渡时,监视器画面就是这台机位;它生效的镜头(焦段 / 光圈 / 对焦 / 景深强度)
|
||||
// 和画幅也必须取这台机位 —— 否则会出现「画面是 B 机位、虚化却用了激活的 A 机位」,
|
||||
// 导出(按显示机位解析镜头)就会和监视器对不上。
|
||||
// 当前时刻「正在显示」的机位:与机位视角(ActiveCameraPlaybackSync)用同一套选取逻辑 ——
|
||||
// 播放 / 导出 / scrub 时按时间轴关键帧自动切(也与导出的 getActiveCameraIdAtTime 一致),
|
||||
// 静止编辑时沿用手动选中的机位。机位视角和监视器因此永远显示同一台机位,不会出现
|
||||
// 「主视口是手选的 A 机位、监视器却是时间轴上的 B 机位」这种画面 / 虚化都对不上的情况。
|
||||
const autoCut = isPlaying || isExporting || isScrubbing;
|
||||
const displayCameraId = useMemo(
|
||||
() => getActiveCameraIdAtTime(transitionClips, cameraTracks, activeCamera?.id ?? null, playhead),
|
||||
[transitionClips, cameraTracks, activeCamera?.id, playhead]
|
||||
() =>
|
||||
autoCut
|
||||
? getActiveCameraIdAtTime(transitionClips, cameraTracks, activeCamera?.id ?? null, playhead)
|
||||
: (activeCamera?.id ?? null),
|
||||
[autoCut, transitionClips, cameraTracks, activeCamera?.id, playhead]
|
||||
);
|
||||
const camera = cameras.find((item) => item.id === displayCameraId) ?? activeCamera;
|
||||
// 小字镜头信息也按播放头解析(与画面渲染同一份 resolveLensAtTime),点击/拖到关键帧时数字同步刷新
|
||||
@@ -305,16 +265,14 @@ export function LensMonitor({ rightOffset = 12 }: { rightOffset?: number }) {
|
||||
onPointerCancel={endDrag}
|
||||
>
|
||||
{camera && lens ? (
|
||||
// 机位视角下主视口本身就是机位画面 → 监视器直接镜像它,不再开第二个 Canvas 重渲整个场景
|
||||
//(省掉一份场景渲染 + 一套景深后处理,而且与主视口逐像素一致)。
|
||||
// 其它视角(例如导演视角下想同时看机位画面)才需要监视器自己渲染。
|
||||
viewMode === "camera" ? (
|
||||
<MonitorMirror sensorAspect={sensorAspect} />
|
||||
) : (
|
||||
<Canvas
|
||||
className="lens-monitor-canvas"
|
||||
dpr={[1, 1.5]}
|
||||
gl={{ antialias: true }}
|
||||
// dpr 与主视口(机位视角)对齐到同一档 [1,2]:否则高分屏(dpr=2)上主视口按 2x 锐利渲染、
|
||||
// 监视器却只按 1.5x 渲染再放大显示,画面就发虚、和机位视角"显示不一样"。
|
||||
dpr={[1, 2]}
|
||||
// preserveDrawingBuffer:机位视角导出就是把这块画布的内容逐帧读进 VideoFrame,
|
||||
// 不保留绘画缓冲会读到空帧。
|
||||
gl={{ antialias: true, preserveDrawingBuffer: true }}
|
||||
camera={{ fov: lens.fov, position: [0, 1, 5] }}
|
||||
>
|
||||
<color attach="background" args={[sceneSettings.backgroundColor]} />
|
||||
@@ -343,7 +301,6 @@ export function LensMonitor({ rightOffset = 12 }: { rightOffset?: number }) {
|
||||
/>
|
||||
<SceneRoot editorHelpers={false} />
|
||||
</Canvas>
|
||||
)
|
||||
) : (
|
||||
<div className="lens-monitor-empty">无激活机位</div>
|
||||
)}
|
||||
|
||||
Reference in new issue
Block a user