diff --git a/B04_PreProcess/B04_PreProcess_UI_Camera.ts b/B04_PreProcess/B04_PreProcess_UI_Camera.ts index e1db3c38..c2161342 100644 --- a/B04_PreProcess/B04_PreProcess_UI_Camera.ts +++ b/B04_PreProcess/B04_PreProcess_UI_Camera.ts @@ -251,8 +251,15 @@ export function bindCursorPivotControls(options: CursorPivotOptions): () => void // 수직 회전 — 시선의 오른쪽 축 기준. 마우스를 내리면 위에서 내려다보는 방향(2026-08-01 // 사용자 지시로 상하 반전). 극점을 넘으면 상하 성분만 버린다. const eyeDirection = cameraOffset.clone().sub(targetOffset); + // 회전축은 화면의 가로축이다. 보통은 시선×up으로 얻지만, 탑뷰처럼 시선이 up과 거의 + // 나란하면 이 외적이 0에 수렴해 축 방향이 잡음으로 정해진다 — 살짝만 끌어도 엉뚱한 + // 축으로 돌아 화면이 뒤집혔다(2026-08-08 사용자 보고). 그럴 때는 카메라 자신의 + // 가로축을 쓴다(시선×up과 같은 방향이라 조작감이 달라지지 않는다). const right = eyeDirection.clone().cross(up); - if (right.lengthSq() > 1e-8) { + if (right.lengthSq() < 1e-6 * Math.max(eyeDirection.lengthSq(), 1e-12)) { + right.setFromMatrixColumn(camera.matrix, 0).negate(); + } + if (right.lengthSq() > 1e-12) { right.normalize(); // 극점을 넘길 회전은 **버리지 않고 극점 직전까지만 잘라서** 적용한다. 통째로 버리면 // 끄는 동안 화면이 멈춰 있다가 각도가 범위 안으로 돌아오는 순간 한꺼번에 움직여 @@ -262,10 +269,12 @@ export function bindCursorPivotControls(options: CursorPivotOptions): () => void // 실제로 재 본다 — 카메라가 극점 어느 쪽에 있느냐에 따라 뒤집힌다. const probe = eyeDirection.clone().applyAxisAngle(right, 1e-3).angleTo(up); const direction = probe >= polar ? 1 : -1; + // 기본 탑뷰는 극각이 이미 여유각 안쪽(거의 0)이다. 경계로 끌어올리면 살짝만 끌어도 + // 여유각만큼 튄다 — 현재 각도를 경계로 삼아, 극점에서 **멀어지는 쪽만** 열어 준다. const clamped = THREE.MathUtils.clamp( polar + direction * pitch, - POLAR_EPSILON, - Math.PI - POLAR_EPSILON, + Math.min(polar, POLAR_EPSILON), + Math.max(polar, Math.PI - POLAR_EPSILON), ); const applied = direction * (clamped - polar); if (Math.abs(applied) > 1e-9) {