[Three.js] OrbitControls 심화 활용과 프로젝트 최적화 학습하기

궁금하면 500원·2024년 12월 25일
0

1. 카메라 전환 시스템 구현

실제 고객사에서는 여러 시점을 부드럽게 전환해야 하는 경우가 많았었습니다.
예를 들어 제품 상세 페이지에서 각 부분을 클릭하면 해당 부분으로 카메라가 자연스럽게 이동하는 기능을 구현해보겠습니다.

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
        };
    }
}

2. 인터랙티브 제어 제한 구현

실제 프로젝트에서는 특정 상황에서 카메라 조작을 제한해야 하는 경우가 많습니다.

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);
    }
}

3. 성능 최적화: 프레임 제어 시스템

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);
    }
}

4. 터치 디바이스 최적화

모바일 환경에서의 사용성을 개선하기 위한 터치 최적화를 구현해보겠습니다.

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'
        });
    }
}

5. 디버깅 도구 구현

개발 과정에서 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)}
        `;
    }
}

이러한 심화 기능들은 실제 고객사 환경에서 자주 필요로 해서 적용된 기능들입니다

profile
꾸준히, 의미있는 사이드 프로젝트 경험과 문제해결 과정을 기록하기 위한 공간입니다.

0개의 댓글