fix: 修复机位视角切回导演视角可能出现拉伸
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
35
src/editor/canvas/syncCameraAspect.test.ts
Normal file
35
src/editor/canvas/syncCameraAspect.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user