From efe44a35a635b2736e40916011cf6ffb3aba81ab Mon Sep 17 00:00:00 2001 From: tongtongstudio Date: Fri, 18 Sep 2026 09:37:36 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=9C=BA=E4=BD=8D?= =?UTF-8?q?=E8=A7=86=E8=A7=92=E5=88=87=E5=9B=9E=E5=AF=BC=E6=BC=94=E8=A7=86?= =?UTF-8?q?=E8=A7=92=E5=8F=AF=E8=83=BD=E5=87=BA=E7=8E=B0=E6=8B=89=E4=BC=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/editor/canvas/DirectorCanvas.tsx | 33 +++++++++++++++++--- src/editor/canvas/syncCameraAspect.test.ts | 35 ++++++++++++++++++++++ 2 files changed, 64 insertions(+), 4 deletions(-) create mode 100644 src/editor/canvas/syncCameraAspect.test.ts diff --git a/src/editor/canvas/DirectorCanvas.tsx b/src/editor/canvas/DirectorCanvas.tsx index c016483..2d353c7 100644 --- a/src/editor/canvas/DirectorCanvas.tsx +++ b/src/editor/canvas/DirectorCanvas.tsx @@ -184,6 +184,24 @@ function applySnapshotToCamera(camera: ThreePerspectiveCamera, snapshot: CameraS camera.updateMatrixWorld(); } +/** + * 把透视相机的 aspect 对齐到当前画布尺寸。 + * + * 为什么需要它:机位视角用的是 drei ``,导演视角用的是画布自带相机。 + * 切回导演视角时,R3F 把默认相机从「机位相机」换回「画布相机」,但画布相机的 aspect 停留在旧值; + * 机位视角期间发生过的画布缩放(窗口缩放 / 侧栏收起展开)只更新了那颗 makeDefault 相机的 aspect。 + * 于是切回后投影矩阵用了错误的 aspect —— 画面被横向拉伸、纵向压扁,看起来角色「变矮」。 + * 这里在每次进入导演视角(以及画布尺寸变化时)强制把 aspect 对齐到真实尺寸。 + */ +export function syncCameraAspectToViewport(camera: ThreePerspectiveCamera, width: number, height: number) { + if (width <= 0 || height <= 0) return; + const nextAspect = width / height; + if (Math.abs(camera.aspect - nextAspect) > 1e-4) { + camera.aspect = nextAspect; + camera.updateProjectionMatrix(); + } +} + /** * 把「机位视角」写到渲染相机上:位置 + 注视朝向角直接合成姿态。 * 与导演视角的 applySnapshotToCamera 分开 —— 那边是 OrbitControls 的轨道中心点(世界坐标), @@ -697,7 +715,7 @@ function DirectorViewCameraSync({ snapshotSourceRef: MutableRefObject; viewMode: "director" | "camera"; }) { - const { camera } = useThree(); + const { camera, size } = useThree(); // 上一次渲染时的视角模式:"切回导演视角"必须无条件恢复快照,见下面的判断 const previousViewModeRef = useRef(viewMode); @@ -707,6 +725,15 @@ function DirectorViewCameraSync({ if (viewMode !== "director") return; + const perspectiveCamera = camera as ThreePerspectiveCamera; + + // 切回导演视角时,默认相机从「机位视角的 PerspectiveCamera」换回画布自带相机。 + // 机位视角期间画布尺寸变化只更新了那颗 makeDefault 相机,画布自带相机的 aspect 仍停在旧值, + // 切回后画面被横向拉伸 / 纵向压扁(表现为角色「变矮」)。这里把 aspect 对齐到当前画布尺寸 + // (size 变化时也重算,双保险)。 + const sizeObj = size ?? { width: 0, height: 0 }; + syncCameraAspectToViewport(perspectiveCamera, sizeObj.width, sizeObj.height); + // 用户正在操作 OrbitControls 时,相机每一帧都在动,而滚轮 / 拖拽事件比 React 渲染更快: // 等这次渲染的 layout effect 跑起来时,回调里那份快照已经是"上一帧"的位置了。 // 把它写回相机 = 把镜头往回退一帧,下一帧又被控件推出去 —— 表现就是缩放过程中 @@ -714,8 +741,6 @@ function DirectorViewCameraSync({ // 不再写回相机;只有外部改视角(重置视角 / 坐标控件切轴 / 切回导演视角)才写回。 if (!viewModeChanged && snapshotSourceRef.current === "controls") return; - const perspectiveCamera = camera as ThreePerspectiveCamera; - if (controlsRef.current) { // 连 OrbitControls 一起更新(含先把残余惯性结算掉) applySnapshotToControls(controlsRef.current, perspectiveCamera, snapshot); @@ -723,7 +748,7 @@ function DirectorViewCameraSync({ } applySnapshotToCamera(perspectiveCamera, snapshot); - }, [camera, controlsRef, snapshot, snapshotSourceRef, viewMode]); + }, [camera, controlsRef, size, snapshot, snapshotSourceRef, viewMode]); return null; } diff --git a/src/editor/canvas/syncCameraAspect.test.ts b/src/editor/canvas/syncCameraAspect.test.ts new file mode 100644 index 0000000..b3aabc5 --- /dev/null +++ b/src/editor/canvas/syncCameraAspect.test.ts @@ -0,0 +1,35 @@ +import { PerspectiveCamera } from "three"; +import { describe, expect, it, vi } from "vitest"; +import { syncCameraAspectToViewport } from "./DirectorCanvas"; + +describe("syncCameraAspectToViewport", () => { + it("aligns a stale camera aspect to the real viewport size", () => { + const camera = new PerspectiveCamera(50, 1, 0.1, 100); // 初始 aspect 停在 1 + const update = vi.spyOn(camera, "updateProjectionMatrix"); + + syncCameraAspectToViewport(camera, 1600, 900); + + expect(camera.aspect).toBeCloseTo(1600 / 900, 6); + expect(update).toHaveBeenCalledTimes(1); + }); + + it("leaves the aspect alone when it already matches", () => { + const camera = new PerspectiveCamera(50, 16 / 9, 0.1, 100); + const update = vi.spyOn(camera, "updateProjectionMatrix"); + + syncCameraAspectToViewport(camera, 1600, 900); + + expect(camera.aspect).toBeCloseTo(16 / 9, 6); + expect(update).not.toHaveBeenCalled(); + }); + + it("ignores invalid viewport sizes", () => { + const camera = new PerspectiveCamera(50, 1, 0.1, 100); + const update = vi.spyOn(camera, "updateProjectionMatrix"); + + syncCameraAspectToViewport(camera, 0, 900); + + expect(camera.aspect).toBe(1); + expect(update).not.toHaveBeenCalled(); + }); +});