- 「원근으로/직교로」 토글을 「원근」 「직교」 두 단추로 · 켜진 쪽 강조 - 카메라 기본 = 직교 - FRONT = 남에서 북 수평 · SIDE = 동에서 서 수평 · 앞 가장자리 반폭만큼 물려 화면맞춤 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EYJJRCfHfdCGNhmXTjQKV7
81 lines
3.0 KiB
TypeScript
81 lines
3.0 KiB
TypeScript
/* =============================================================================
|
|
* B05_Profile_UI_Viewer_Camera.ts
|
|
* B05 뷰어의 **원근/직교 두 카메라**와 그 사이 갈아 끼우기.
|
|
*
|
|
* 기본은 직교 — 원근(시야각 45°)은 단추로 고른다. 갈아 끼울 때 위치·시선·근평면·
|
|
* 먼평면과 **보이는 크기**를 그대로 옮기므로 화면이 튀지 않는다.
|
|
*
|
|
* 카메라 객체가 바뀌므로 쓰는 쪽은 붙잡아 두지 말고 `camera()`로 그때그때 읽을 것.
|
|
* ========================================================================== */
|
|
|
|
import * as THREE from "three";
|
|
import type { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
|
|
|
|
export type ProjectionKind = "perspective" | "ortho";
|
|
|
|
const FOV = 45;
|
|
/** 원근 45°의 반각 tan — 직교 반높이를 같은 크기감으로 맞출 때 쓴다. */
|
|
const HALF_TAN = Math.tan((FOV * Math.PI) / 360);
|
|
|
|
export function createCameraRig() {
|
|
const perspective = new THREE.PerspectiveCamera(FOV, 1, 0.1, 100000);
|
|
perspective.position.set(100, 120, 100);
|
|
const ortho = new THREE.OrthographicCamera(-100, 100, 100, -100, 0.1, 100000);
|
|
let kind: ProjectionKind = "ortho";
|
|
let aspect = 1;
|
|
let halfHeight = 100;
|
|
|
|
const active = (): THREE.PerspectiveCamera | THREE.OrthographicCamera =>
|
|
kind === "perspective" ? perspective : ortho;
|
|
|
|
function apply(): void {
|
|
perspective.aspect = aspect;
|
|
perspective.updateProjectionMatrix();
|
|
ortho.left = -halfHeight * aspect;
|
|
ortho.right = halfHeight * aspect;
|
|
ortho.top = halfHeight;
|
|
ortho.bottom = -halfHeight;
|
|
ortho.updateProjectionMatrix();
|
|
}
|
|
|
|
return {
|
|
camera: active,
|
|
kind: () => kind,
|
|
/** 뷰포트 종횡비(리사이즈 시). */
|
|
setAspect(value: number): void {
|
|
aspect = value;
|
|
apply();
|
|
},
|
|
/** 화면맞춤 — 시점까지 거리로 직교 반높이를 잡는다(원근과 같은 크기감). */
|
|
setFit(distance: number): void {
|
|
halfHeight = distance * HALF_TAN;
|
|
ortho.zoom = 1;
|
|
apply();
|
|
},
|
|
/** 투영 전환. 보이는 크기를 유지하며 OrbitControls의 대상 카메라도 갈아 끼운다. */
|
|
setKind(next: ProjectionKind, controls: OrbitControls): void {
|
|
if (next === kind) return;
|
|
const from = active();
|
|
kind = next;
|
|
const to = active();
|
|
to.quaternion.copy(from.quaternion);
|
|
to.near = from.near;
|
|
to.far = from.far;
|
|
const offset = from.position.clone().sub(controls.target);
|
|
if (next === "ortho") {
|
|
halfHeight = offset.length() * HALF_TAN;
|
|
ortho.zoom = 1;
|
|
to.position.copy(from.position);
|
|
} else {
|
|
// 직교는 배율(zoom)로도 커지므로, 같은 크기로 보이는 거리까지 카메라를 물린다.
|
|
to.position.copy(controls.target).add(offset.setLength(halfHeight / ortho.zoom / HALF_TAN));
|
|
}
|
|
apply();
|
|
controls.object = to;
|
|
controls.update();
|
|
},
|
|
};
|
|
}
|
|
|
|
export type CameraRig = ReturnType<typeof createCameraRig>;
|