perf(editor): 优化播放头拖动性能

This commit is contained in:
tongtong committed 2026-09-21 00:04:25 +08:00
1 parent 5cc2e26a29
commit 07d9e5e798
7 files changed
+141 -56

No files matched your search

+87 -41
View File
@@ -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>
)}