fix: 修复机位视角切回导演视角可能出现拉伸

This commit is contained in:
2026-09-18 09:37:36 +08:00
parent 2280775e8d
commit efe44a35a6
2 changed files with 64 additions and 4 deletions

View File

@@ -184,6 +184,24 @@ function applySnapshotToCamera(camera: ThreePerspectiveCamera, snapshot: CameraS
camera.updateMatrixWorld();
}
/**
* 把透视相机的 aspect 对齐到当前画布尺寸。
*
* 为什么需要它:机位视角用的是 drei `<PerspectiveCamera makeDefault>`,导演视角用的是画布自带相机。
* 切回导演视角时,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<ViewportSnapshotSource>;
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;
}

View File

@@ -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();
});
});