perf(editor): 优化播放头拖动性能
This commit is contained in:
1 parent
5cc2e26a29
commit
07d9e5e798
7 files changed
+141
-56
No files matched your search
@@ -1,6 +1,6 @@
|
||||
import { Canvas, useFrame, useThree } from "@react-three/fiber";
|
||||
import { PerspectiveCamera as DreiPerspectiveCamera } from "@react-three/drei";
|
||||
import { useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||
import { memo, 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";
|
||||
@@ -45,14 +45,12 @@ function LensMonitorCamera({
|
||||
sceneSettings,
|
||||
sceneRef,
|
||||
camRef,
|
||||
playhead,
|
||||
cameraTracks,
|
||||
}: {
|
||||
camera: DirectorCameraShot;
|
||||
sceneSettings: SceneSettings;
|
||||
sceneRef: React.MutableRefObject<Scene | null>;
|
||||
camRef: React.MutableRefObject<ThreePerspectiveCamera | null>;
|
||||
playhead: number;
|
||||
cameraTracks: CameraTimelineTrack[];
|
||||
}) {
|
||||
const { gl, scene } = useThree();
|
||||
@@ -63,6 +61,11 @@ function LensMonitorCamera({
|
||||
const transitionClips = useDirectorStore((state) => state.project.timeline.transitionClips);
|
||||
const globalTransition = useDirectorStore((state) => state.project.timeline.cameraTransition);
|
||||
const isExporting = usePlaybackStore((state) => state.isExporting);
|
||||
// playhead 自己订阅(而不是从外层当 prop 传进来):外壳组件不必因为这个每秒变几十次的值
|
||||
// 去 reconcile 整棵场景树(见 LensMonitorCanvas 的 memo)。
|
||||
const playhead = usePlaybackStore((state) => state.playhead);
|
||||
// 拖动播放头期间走草稿档:跳过景深整套后处理,把帧预算留给「跟手」
|
||||
const isScrubbing = usePlaybackStore((state) => state.isScrubbing);
|
||||
// 关键帧感知:拖播放头时,监视器上的对焦距离 / 光圈 / 焦段 / 景深强度都按关键帧插值,
|
||||
// 让「移焦 / 渐入渐出虚化 / 变焦」在监视器上所见即所得(与导出管线一致的 resolveLensAtTime)。
|
||||
const lens = useMemo(
|
||||
@@ -125,7 +128,8 @@ function LensMonitorCamera({
|
||||
// 与主视口 / 视频导出共用同一份景深渲染(renderSceneWithBokeh):监视器不再自建一套
|
||||
// EffectComposer —— 两套实现很容易漂移,导出与监视器的取景 / 虚化就会对不上。
|
||||
// 合成器按 gl 缓存、画布尺寸变化会自动重建;不支持后处理时回退普通渲染,监视器永远可用。
|
||||
renderSceneWithBokeh(gl, scene, renderCamera, lens);
|
||||
// 拖动播放头时传 draft:跳过整套景深后处理(松手立刻恢复),拖动才跟手。
|
||||
renderSceneWithBokeh(gl, scene, renderCamera, lens, undefined, { draft: isScrubbing });
|
||||
}, 1);
|
||||
|
||||
return (
|
||||
@@ -139,6 +143,71 @@ function LensMonitorCamera({
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 监视器那块 3D 画布:整棵场景树(SceneRoot 会把当前工程所有物体铺进来)。
|
||||
*
|
||||
* 单独抽成 memo 组件的原因:拖动播放头时 playhead 每秒变几十次,如果这棵树长在外壳组件里,
|
||||
* 每动一下都要把整棵场景树 reconcile 一遍 —— 那是拖动卡顿的一大来源。这里 props 只在
|
||||
* 「换机位 / 改场景 / 换全景图」这类真实编辑时变,播放头由 LensMonitorCamera 自己订阅。
|
||||
*/
|
||||
const LensMonitorCanvas = memo(function LensMonitorCanvas({
|
||||
camera,
|
||||
sceneSettings,
|
||||
panoramaAsset,
|
||||
cameraTracks,
|
||||
sceneRef,
|
||||
camRef,
|
||||
initialCamera,
|
||||
}: {
|
||||
camera: DirectorCameraShot;
|
||||
sceneSettings: SceneSettings;
|
||||
panoramaAsset: React.ComponentProps<typeof ViewportBackground>["panoramaAsset"];
|
||||
cameraTracks: CameraTimelineTrack[];
|
||||
sceneRef: React.MutableRefObject<Scene | null>;
|
||||
camRef: React.MutableRefObject<ThreePerspectiveCamera | null>;
|
||||
/** 画布的初始相机参数:只取首次渲染那一刻的值(位姿 / fov 由 LensMonitorCamera 每帧写) */
|
||||
initialCamera: { fov: number; position: [number, number, number] };
|
||||
}) {
|
||||
return (
|
||||
<Canvas
|
||||
className="lens-monitor-canvas"
|
||||
// 这块画布在机位视角下就是主视口那块画幅(尺寸跟着画幅走),所以 dpr 上限收一档:
|
||||
// 大画布上跑到 2x 会让「景深 composer + 深度 pass」的像素量翻倍,是之前发卡的主因;
|
||||
// 1.5x 在高分屏上肉眼几乎无差别,开销却少 ~45%。导出不受影响(导出自己设 1x + 导出分辨率)。
|
||||
dpr={[1, 1.5]}
|
||||
// preserveDrawingBuffer:机位视角导出就是把这块画布的内容逐帧读进 VideoFrame,
|
||||
// 不保留绘画缓冲会读到空帧。
|
||||
gl={{ antialias: true, preserveDrawingBuffer: true }}
|
||||
camera={initialCamera}
|
||||
>
|
||||
<color attach="background" args={[sceneSettings.backgroundColor]} />
|
||||
{/* 与主视口 / 导出共用同一份背景实现:有全景图时渲染全景(equirect 走 scene.background、
|
||||
backdrop 走穹顶网格),否则用背景色。少了它,监视器会和导出画面差一个背景。 */}
|
||||
<ViewportBackground
|
||||
backgroundColor={sceneSettings.backgroundColor}
|
||||
panoramaAsset={panoramaAsset}
|
||||
panoramaRadius={sceneSettings.panoramaRadius}
|
||||
panoramaYaw={sceneSettings.panoramaYaw}
|
||||
/>
|
||||
<ambientLight intensity={1.15} />
|
||||
<directionalLight intensity={1.2} position={[8, 10, 6]} />
|
||||
{/* 与主视口 / 视频导出共用同一份时间轴变换:把物体 / 角色关键帧的插值写进监视器场景。
|
||||
SceneRoot 自身只取 item.transform 和人物轨迹,不应用时间轴轨道 —— 少了这一层,
|
||||
监视器里的物体就不会随关键帧运动,物体到相机的深度和导出不同,
|
||||
清晰平面落在别处,看起来就是「对焦距离和导出对不上」。 */}
|
||||
<PlaybackTransformSync />
|
||||
<LensMonitorCamera
|
||||
camera={camera}
|
||||
sceneSettings={sceneSettings}
|
||||
sceneRef={sceneRef}
|
||||
camRef={camRef}
|
||||
cameraTracks={cameraTracks}
|
||||
/>
|
||||
<SceneRoot editorHelpers={false} />
|
||||
</Canvas>
|
||||
);
|
||||
});
|
||||
|
||||
/** 鼠标每像素对应的角度增量(度) */
|
||||
const LOOK_DRAG_SENSITIVITY = 0.3;
|
||||
/** 俯仰角夹到 ±89°,避免越过天顶后镜头翻转 */
|
||||
@@ -230,6 +299,11 @@ export function LensMonitor({ rightOffset = 12 }: { rightOffset?: number }) {
|
||||
}, [sensorAspect]);
|
||||
|
||||
const viewportCameraFrame = viewMode === "camera" && camera ? viewportFrame : null;
|
||||
// 画布初始相机参数只在首次渲染那一刻取一次:位姿 / fov 每帧都由 LensMonitorCamera 写,
|
||||
// 保持这个对象引用稳定,<Canvas> 才不会因为播放头变化被重新应用 camera props。
|
||||
const initialCanvasCameraRef = useRef<{ fov: number; position: [number, number, number] } | null>(null);
|
||||
initialCanvasCameraRef.current ??= { fov: lens?.fov ?? 50, position: [0, 1, 5] };
|
||||
const initialCanvasCamera = initialCanvasCameraRef.current;
|
||||
const [dragging, setDragging] = useState(false);
|
||||
const dragRef = useRef<{
|
||||
startX: number;
|
||||
@@ -339,43 +413,15 @@ export function LensMonitor({ rightOffset = 12 }: { rightOffset?: number }) {
|
||||
onPointerCancel={endDrag}
|
||||
>
|
||||
{camera && lens ? (
|
||||
<Canvas
|
||||
className="lens-monitor-canvas"
|
||||
// 这块画布在机位视角下就是主视口那块画幅(尺寸跟着画幅走),所以 dpr 上限收一档:
|
||||
// 大画布上跑到 2x 会让「景深 composer + 深度 pass」的像素量翻倍,是之前发卡的主因;
|
||||
// 1.5x 在高分屏上肉眼几乎无差别,开销却少 ~45%。导出不受影响(导出自己设 1x + 导出分辨率)。
|
||||
dpr={[1, 1.5]}
|
||||
// preserveDrawingBuffer:机位视角导出就是把这块画布的内容逐帧读进 VideoFrame,
|
||||
// 不保留绘画缓冲会读到空帧。
|
||||
gl={{ antialias: true, preserveDrawingBuffer: true }}
|
||||
camera={{ fov: lens.fov, position: [0, 1, 5] }}
|
||||
>
|
||||
<color attach="background" args={[sceneSettings.backgroundColor]} />
|
||||
{/* 与主视口 / 导出共用同一份背景实现:有全景图时渲染全景(equirect 走 scene.background、
|
||||
backdrop 走穹顶网格),否则用背景色。少了它,监视器会和导出画面差一个背景。 */}
|
||||
<ViewportBackground
|
||||
backgroundColor={sceneSettings.backgroundColor}
|
||||
panoramaAsset={panoramaAsset}
|
||||
panoramaRadius={sceneSettings.panoramaRadius}
|
||||
panoramaYaw={sceneSettings.panoramaYaw}
|
||||
/>
|
||||
<ambientLight intensity={1.15} />
|
||||
<directionalLight intensity={1.2} position={[8, 10, 6]} />
|
||||
{/* 与主视口 / 视频导出共用同一份时间轴变换:把物体 / 角色关键帧的插值写进监视器场景。
|
||||
SceneRoot 自身只取 item.transform 和人物轨迹,不应用时间轴轨道 —— 少了这一层,
|
||||
监视器里的物体就不会随关键帧运动,物体到相机的深度和导出不同,
|
||||
清晰平面落在别处,看起来就是「对焦距离和导出对不上」。 */}
|
||||
<PlaybackTransformSync />
|
||||
<LensMonitorCamera
|
||||
camera={camera}
|
||||
sceneSettings={sceneSettings}
|
||||
sceneRef={monitorSceneRef}
|
||||
camRef={monitorCamRef}
|
||||
playhead={playhead}
|
||||
cameraTracks={cameraTracks}
|
||||
/>
|
||||
<SceneRoot editorHelpers={false} />
|
||||
</Canvas>
|
||||
<LensMonitorCanvas
|
||||
camera={camera}
|
||||
cameraTracks={cameraTracks}
|
||||
camRef={monitorCamRef}
|
||||
initialCamera={initialCanvasCamera}
|
||||
panoramaAsset={panoramaAsset}
|
||||
sceneRef={monitorSceneRef}
|
||||
sceneSettings={sceneSettings}
|
||||
/>
|
||||
) : (
|
||||
<div className="lens-monitor-empty">无激活机位</div>
|
||||
)}
|
||||
|
||||
Reference in new issue
Block a user