실제 고객사에서는 여러 시점을 부드럽게 전환해야 하는 경우가 많았었습니다.
예를 들어 제품 상세 페이지에서 각 부분을 클릭하면 해당 부분으로 카메라가 자연스럽게 이동하는 기능을 구현해보겠습니다.
class CameraTransitionManager {
private camera: THREE.PerspectiveCamera;
private controls: OrbitControls;
private isTransitioning: boolean = false;
// 미리 정의된 뷰포인트들
private viewpoints: Record<string, CameraPosition> = {
'front': {
position: new THREE.Vector3(0, 0, 5),
target: new THREE.Vector3(0, 0, 0),
zoom: 1
},
'top': {
position: new THREE.Vector3(0, 5, 0),
target: new THREE.Vector3(0, 0, 0),
zoom: 1.5
},
'detail': {
position: new THREE.Vector3(1, 1, 1),
target: new THREE.Vector3(0.5, 0.5, 0.5),
zoom: 2
}
};
constructor(camera: THREE.PerspectiveCamera, controls: OrbitControls) {
this.camera = camera;
this.controls = controls;
// 컨트롤 조작 중 전환 방지
this.controls.addEventListener('start', () => {
this.isTransitioning = false;
});
}
public async transitionToView(viewName: string, duration: number = 1.5): Promise<void> {
if (this.isTransitioning) return;
const targetView = this.viewpoints[viewName];
if (!targetView) return;
this.isTransitioning = true;
this.controls.enabled = false;
// GSAP를 사용한 부드러운 전환
await gsap.to(this.camera.position, {
x: targetView.position.x,
y: targetView.position.y,
z: targetView.position.z,
duration: duration,
ease: 'power2.inOut'
});
await gsap.to(this.controls.target, {
x: targetView.target.x,
y: targetView.target.y,
z: targetView.target.z,
duration: duration,
ease: 'power2.inOut',
onUpdate: () => this.controls.update()
});
this.controls.enabled = true;
this.isTransitioning = false;
}
// 현재 카메라 위치 저장
public saveCurrentViewpoint(name: string): void {
this.viewpoints[name] = {
position: this.camera.position.clone(),
target: this.controls.target.clone(),
zoom: this.camera.zoom
};
}
}
실제 프로젝트에서는 특정 상황에서 카메라 조작을 제한해야 하는 경우가 많습니다.
class AdvancedOrbitControls {
private controls: OrbitControls;
private restrictions: ControlRestrictions;
constructor(camera: THREE.PerspectiveCamera, domElement: HTMLElement) {
this.controls = new OrbitControls(camera, domElement);
this.restrictions = {
rotationSpeed: 1.0,
zoomSpeed: 1.0,
minDistance: 2,
maxDistance: 10,
minPolarAngle: 0,
maxPolarAngle: Math.PI * 0.5,
minAzimuthAngle: -Math.PI * 0.25,
maxAzimuthAngle: Math.PI * 0.25
};
this.applyRestrictions();
}
private applyRestrictions(): void {
// 기본 제한 설정
this.controls.minDistance = this.restrictions.minDistance;
this.controls.maxDistance = this.restrictions.maxDistance;
this.controls.minPolarAngle = this.restrictions.minPolarAngle;
this.controls.maxPolarAngle = this.restrictions.maxPolarAngle;
this.controls.minAzimuthAngle = this.restrictions.minAzimuthAngle;
this.controls.maxAzimuthAngle = this.restrictions.maxAzimuthAngle;
// 부드러운 감속 효과
this.controls.enableDamping = true;
this.controls.dampingFactor = 0.05;
}
// 특정 영역에서만 줌 허용
public setZoomableAreas(areas: THREE.Box3[]): void {
this.controls.addEventListener('change', () => {
const cameraPosition = this.controls.object.position;
let isInZoomableArea = false;
for (const area of areas) {
if (area.containsPoint(cameraPosition)) {
isInZoomableArea = true;
break;
}
}
this.controls.enableZoom = isInZoomableArea;
});
}
// 동적 제한 설정
public setDynamicRestrictions(distance: number): void {
// 거리에 따른 동적 제한 조정
const factor = Math.max(0, Math.min(1, distance / this.restrictions.maxDistance));
this.controls.minPolarAngle = this.restrictions.minPolarAngle * factor;
this.controls.maxPolarAngle = this.restrictions.maxPolarAngle * factor;
this.controls.rotateSpeed = this.restrictions.rotationSpeed * (1 - factor * 0.5);
}
}
OrbitControls의 성능을 최적화하기 위한 프레임 제어 시스템을 구현해보겠습니다.
class PerformanceOptimizedControls {
private controls: OrbitControls;
private camera: THREE.PerspectiveCamera;
private lastUpdateTime: number = 0;
private readonly updateInterval: number = 1000 / 60; // 60fps
private frameCount: number = 0;
private fpsHistory: number[] = [];
constructor(camera: THREE.PerspectiveCamera, domElement: HTMLElement) {
this.camera = camera;
this.controls = new OrbitControls(camera, domElement);
// 성능 모니터링 설정
this.setupPerformanceMonitoring();
}
private setupPerformanceMonitoring(): void {
// FPS 모니터링
setInterval(() => {
const fps = this.frameCount;
this.fpsHistory.push(fps);
if (this.fpsHistory.length > 10) {
this.fpsHistory.shift();
const avgFps = this.fpsHistory.reduce((a, b) => a + b) / this.fpsHistory.length;
// FPS가 낮을 경우 자동으로 품질 조정
if (avgFps < 30) {
this.reduceDampingQuality();
}
}
this.frameCount = 0;
}, 1000);
}
public update(): void {
const currentTime = performance.now();
// 프레임 제한
if (currentTime - this.lastUpdateTime < this.updateInterval) {
return;
}
this.controls.update();
this.frameCount++;
this.lastUpdateTime = currentTime;
}
private reduceDampingQuality(): void {
// 성능이 좋지 않을 때 댐핑 효과 줄이기
this.controls.enableDamping = false;
setTimeout(() => {
this.controls.enableDamping = true;
this.controls.dampingFactor = 0.05;
}, 1000);
}
}
모바일 환경에서의 사용성을 개선하기 위한 터치 최적화를 구현해보겠습니다.
class TouchOptimizedControls {
private controls: OrbitControls;
private touchStartTime: number = 0;
private lastTapTime: number = 0;
private initialPinchDistance: number = 0;
constructor(camera: THREE.PerspectiveCamera, domElement: HTMLElement) {
this.controls = new OrbitControls(camera, domElement);
this.setupTouchControls();
}
private setupTouchControls(): void {
// 터치 감도 최적화
this.controls.touches = {
ONE: THREE.TOUCH.ROTATE,
TWO: THREE.TOUCH.DOLLY_PAN
};
this.controls.touchStart = (event: TouchEvent) => {
this.touchStartTime = performance.now();
if (event.touches.length === 2) {
const dx = event.touches[0].pageX - event.touches[1].pageX;
const dy = event.touches[0].pageY - event.touches[1].pageY;
this.initialPinchDistance = Math.sqrt(dx * dx + dy * dy);
}
};
this.controls.touchEnd = (event: TouchEvent) => {
const touchDuration = performance.now() - this.touchStartTime;
// 더블 탭 감지
if (touchDuration < 200) {
const currentTime = performance.now();
if (currentTime - this.lastTapTime < 300) {
this.handleDoubleTap(event);
}
this.lastTapTime = currentTime;
}
};
}
private handleDoubleTap(event: TouchEvent): void {
// 더블 탭으로 원점으로 복귀
gsap.to(this.controls.target, {
x: 0,
y: 0,
z: 0,
duration: 1,
ease: 'power2.out'
});
gsap.to(this.controls.object.position, {
x: 0,
y: 0,
z: 5,
duration: 1,
ease: 'power2.out'
});
}
}
개발 과정에서 OrbitControls의 상태를 모니터링하고 디버깅하기 위한 도구를 구현해보겠습니다.
class OrbitControlsDebugger {
private controls: OrbitControls;
private stats: {
position: THREE.Vector3;
target: THREE.Vector3;
polar: number;
azimuth: number;
distance: number;
};
private debugElement: HTMLElement;
constructor(controls: OrbitControls) {
this.controls = controls;
this.setupDebugUI();
this.createDebugVisuals();
}
private setupDebugUI(): void {
this.debugElement = document.createElement('div');
this.debugElement.style.cssText = `
position: fixed;
top: 10px;
right: 10px;
background: rgba(0, 0, 0, 0.7);
color: white;
padding: 10px;
font-family: monospace;
font-size: 12px;
border-radius: 4px;
z-index: 1000;
`;
document.body.appendChild(this.debugElement);
}
private createDebugVisuals(): void {
// 카메라 방향 표시기
const directionHelper = new THREE.ArrowHelper(
this.controls.object.getWorldDirection(new THREE.Vector3()),
this.controls.object.position,
2,
0xffff00
);
this.controls.object.parent.add(directionHelper);
}
public update(): void {
// 상태 업데이트
this.stats = {
position: this.controls.object.position.clone(),
target: this.controls.target.clone(),
polar: this.controls.getPolarAngle(),
azimuth: this.controls.getAzimuthalAngle(),
distance: this.controls.getDistance()
};
// UI 업데이트
this.debugElement.innerHTML = `
Camera Position: (${this.stats.position.x.toFixed(2)}, ${this.stats.position.y.toFixed(2)}, ${this.stats.position.z.toFixed(2)})
Target: (${this.stats.target.x.toFixed(2)}, ${this.stats.target.y.toFixed(2)}, ${this.stats.target.z.toFixed(2)})
Polar Angle: ${(this.stats.polar * 180 / Math.PI).toFixed(2)}°
Azimuth Angle: ${(this.stats.azimuth * 180 / Math.PI).toFixed(2)}°
Distance: ${this.stats.distance.toFixed(2)}
`;
}
}
이러한 심화 기능들은 실제 고객사 환경에서 자주 필요로 해서 적용된 기능들입니다