From 7d50ded7b7e9c143959ede0832fcddf94128f9bb Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 8 Aug 2026 18:31:47 +0900 Subject: [PATCH] =?UTF-8?q?fix(B04,B05):=20=ED=83=91=EB=B7=B0=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=82=B4=EC=A7=9D=EB=A7=8C=20=EB=81=8C=EC=96=B4?= =?UTF-8?q?=EB=8F=84=20=ED=99=94=EB=A9=B4=EC=9D=B4=20=EB=92=A4=EC=A7=91?= =?UTF-8?q?=ED=9E=88=EB=8D=98=20=EB=AC=B8=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 기본 탑뷰는 시선이 up과 거의 나란하다(극각 1.9e-6 rad). 이때 회전축으로 쓰던 시선×up이 0에 수렴해 축 방향이 잡음으로 정해졌고, 살짝만 끌어도 엉뚱한 축으로 돌아 화면이 뒤집혔다. 앞 커밋의 극점 클램프도 각도를 여유각(0.02 rad) 경계까지 끌어올려, 작은 드래그가 여유각만큼 튀게 만들었다. - 시선×up이 시선 길이 대비 무시할 만큼 짧으면 카메라 자신의 가로축으로 대체한다. 두 축은 모든 각도에서 방향이 같아(내적 1.000000) 전환 순간에도 조작감이 안 바뀐다. - 클램프 경계를 현재 극각과 여유각 중 극점에서 먼 쪽으로 잡는다. 이미 여유각 안쪽인 탑뷰에서는 경계로 끌어올리지 않고, 극점에서 멀어지는 쪽만 열어 준다. 검증(탑뷰 재현): 0.005 요청 -> 0.005 적용, 0.05 -> 0.05, 극점 쪽 -0.005 -> 0(정지). typecheck·prettier 통과, 정적 번들 재빌드. Co-Authored-By: Claude Opus 5 (1M context) --- B04_PreProcess/B04_PreProcess_UI_Camera.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) 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) {