1. 성능 최적화: Throttling과 Debouncing 적용

3D 씬에서 레이캐스팅은 꽤 무거운 연산입니다.
특히 복잡한 모델이나 많은 오브젝트가 있는 경우, 매 프레임마다 수행되는 레이캐스팅은 성능 저하의 원인이 될 수 있습니다.

import _ from 'lodash';

class OptimizedRaycaster {
    private raycaster: THREE.Raycaster;
    private mouse: THREE.Vector2;
    private camera: THREE.PerspectiveCamera;
    private pickables: THREE.Mesh[];
    
    constructor(camera: THREE.PerspectiveCamera) {
        this.raycaster = new THREE.Raycaster();
        this.mouse = new THREE.Vector2();
        this.camera = camera;
        this.pickables = [];
        
        // mousemove 이벤트를 16ms(약 60fps)로 제한
        this.handleMouseMove = _.throttle(this.handleMouseMove.bind(this), 16);
        
        // 더블클릭 이벤트를 300ms 디바운스
        this.handleDoubleClick = _.debounce(this.handleDoubleClick.bind(this), 300);
    }

    private handleMouseMove(event: MouseEvent): void {
        // 성능 측정 시작
        const startTime = performance.now();
        
        this.updateMousePosition(event);
        this.raycaster.setFromCamera(this.mouse, this.camera);
        const intersects = this.raycaster.intersectObjects(this.pickables);
        
        if (intersects.length > 0) {
            this.handleIntersection(intersects[0]);
        }
        
        // 성능 측정 종료 및 로깅
        const endTime = performance.now();
        if (endTime - startTime > 16.67) { // 60fps 기준 초과시 경고
            console.warn(`Raycasting took ${endTime - startTime}ms - Consider optimization`);
        }
    }

    private updateMousePosition(event: MouseEvent): void {
        const rect = (event.target as HTMLElement).getBoundingClientRect();
        this.mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
        this.mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
    }
}

2. 동적 LOD(Level of Detail) 시스템 구현

복잡한 3D 모델의 경우, 레이캐스팅 연산을 최적화하기 위해 LOD 시스템을 구현할 수 있습니다.

class DynamicLODSystem {
    private highDetailGeometry: THREE.BufferGeometry;
    private mediumDetailGeometry: THREE.BufferGeometry;
    private lowDetailGeometry: THREE.BufferGeometry;
    private currentMesh: THREE.Mesh;
    
    constructor(originalGeometry: THREE.BufferGeometry) {
        this.highDetailGeometry = originalGeometry;
        this.mediumDetailGeometry = this.simplifyGeometry(originalGeometry, 0.5);
        this.lowDetailGeometry = this.simplifyGeometry(originalGeometry, 0.2);
        
        this.currentMesh = new THREE.Mesh(
            this.highDetailGeometry,
            new THREE.MeshStandardMaterial()
        );
    }
    
    private simplifyGeometry(geometry: THREE.BufferGeometry, detail: number): THREE.BufferGeometry {
        // 실제 프로젝트에서는 SimplifyModifier 사용
        const modifier = new THREE.SimplifyModifier();
        const vertexCount = Math.floor(geometry.attributes.position.count * detail);
        return modifier.modify(geometry, vertexCount);
    }
    
    public updateLOD(camera: THREE.Camera): void {
        const distance = camera.position.distanceTo(this.currentMesh.position);
        
        if (distance < 5) {
            this.currentMesh.geometry = this.highDetailGeometry;
        } else if (distance < 15) {
            this.currentMesh.geometry = this.mediumDetailGeometry;
        } else {
            this.currentMesh.geometry = this.lowDetailGeometry;
        }
    }
}

3. 복잡한 인터랙션: 드래그 & 드롭 시스템

레이캐스터를 활용한 드래그 & 드롭 시스템을 구현해보겠습니다.

class DragDropSystem {
    private raycaster: THREE.Raycaster;
    private selected: THREE.Mesh | null = null;
    private dragPlane: THREE.Plane;
    private intersection: THREE.Vector3 = new THREE.Vector3();
    private offset: THREE.Vector3 = new THREE.Vector3();
    
    constructor(camera: THREE.PerspectiveCamera, scene: THREE.Scene) {
        this.raycaster = new THREE.Raycaster();
        this.dragPlane = new THREE.Plane();
        
        // 그리드 헬퍼 추가 (드래그 가이드)
        const gridHelper = new THREE.GridHelper(20, 20);
        scene.add(gridHelper);
        
        // 이벤트 리스너 설정
        document.addEventListener('mousedown', this.onMouseDown.bind(this));
        document.addEventListener('mousemove', this.onMouseMove.bind(this));
        document.addEventListener('mouseup', this.onMouseUp.bind(this));
    }
    
    private onMouseDown(event: MouseEvent): void {
        // 레이캐스트로 오브젝트 선택
        const intersects = this.raycaster.intersectObjects(this.pickables);
        
        if (intersects.length > 0) {
            this.selected = intersects[0].object as THREE.Mesh;
            
            // 드래그 평면 설정
            this.dragPlane.setFromNormalAndCoplanarPoint(
                camera.getWorldDirection(new THREE.Vector3()),
                this.selected.position
            );
            
            // 오프셋 계산
            this.raycaster.ray.intersectPlane(this.dragPlane, this.intersection);
            this.offset.copy(this.selected.position).sub(this.intersection);
        }
    }
    
    private onMouseMove(event: MouseEvent): void {
        if (this.selected) {
            // 드래그 중인 오브젝트 위치 업데이트
            this.raycaster.ray.intersectPlane(this.dragPlane, this.intersection);
            this.selected.position.copy(this.intersection.add(this.offset));
            
            // 그리드에 스냅
            this.selected.position.x = Math.round(this.selected.position.x);
            this.selected.position.z = Math.round(this.selected.position.z);
        }
    }
}

4. 커스텀 셰이더를 활용한 하이라이팅

레이캐스터로 선택된 오브젝트에 커스텀 하이라이트 효과를 적용해보겠습니다.

// 버텍스 셰이더
const vertexShader = `
    varying vec3 vNormal;
    varying vec3 vPosition;
    
    void main() {
        vNormal = normalize(normalMatrix * normal);
        vPosition = (modelViewMatrix * vec4(position, 1.0)).xyz;
        gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
`;

// 프래그먼트 셰이더
const fragmentShader = `
    varying vec3 vNormal;
    varying vec3 vPosition;
    uniform vec3 highlightColor;
    uniform float highlightIntensity;
    
    void main() {
        float rim = 1.0 - max(dot(normalize(-vPosition), vNormal), 0.0);
        vec3 finalColor = mix(vec3(1.0), highlightColor, rim * highlightIntensity);
        gl_FragColor = vec4(finalColor, 1.0);
    }
`;

class HighlightSystem {
    private highlightMaterial: THREE.ShaderMaterial;
    private originalMaterials: Map<THREE.Mesh, THREE.Material> = new Map();
    
    constructor() {
        this.highlightMaterial = new THREE.ShaderMaterial({
            vertexShader,
            fragmentShader,
            uniforms: {
                highlightColor: { value: new THREE.Color(0x00ff00) },
                highlightIntensity: { value: 1.0 }
            }
        });
    }
    
    public highlightObject(mesh: THREE.Mesh): void {
        if (!this.originalMaterials.has(mesh)) {
            this.originalMaterials.set(mesh, mesh.material);
            mesh.material = this.highlightMaterial;
            
            // 하이라이트 애니메이션
            gsap.to(this.highlightMaterial.uniforms.highlightIntensity, {
                value: 0.5,
                duration: 0.3,
                yoyo: true,
                repeat: -1
            });
        }
    }
}

5. 오클루전(Occlusion) 처리

레이캐스터가 오브젝트를 통과하지 못하도록 오클루전 처리를 구현합니다.

class OcclusionSystem {
    private raycaster: THREE.Raycaster;
    private camera: THREE.PerspectiveCamera;
    private occluders: THREE.Mesh[] = [];
    
    constructor(camera: THREE.PerspectiveCamera) {
        this.raycaster = new THREE.Raycaster();
        this.camera = camera;
    }
    
    public addOccluder(mesh: THREE.Mesh): void {
        this.occluders.push(mesh);
    }
    
    public checkVisibility(target: THREE.Vector3): boolean {
        const direction = target.clone().sub(this.camera.position).normalize();
        this.raycaster.set(this.camera.position, direction);
        
        const intersects = this.raycaster.intersectObjects(this.occluders);
        if (intersects.length > 0) {
            const distanceToTarget = this.camera.position.distanceTo(target);
            return intersects[0].distance > distanceToTarget;
        }
        
        return true;
    }
    
    public update(): void {
        // 모든 인터랙티브 오브젝트에 대해 가시성 체크
        this.pickables.forEach(object => {
            const visible = this.checkVisibility(object.position);
            object.visible = visible;
            
            if (visible) {
                // 반투명 효과 적용
                const material = object.material as THREE.Material;
                material.opacity = 0.5;
                material.transparent = true;
            }
        });
    }
}

성능 모니터링 및 디버깅

실제 프로덕션 환경에서는 성능 모니터링이 매우 중요합니다.

class PerformanceMonitor {
    private frameCount: number = 0;
    private lastTime: number = performance.now();
    private raycasterTimes: number[] = [];
    
    public logRaycasterPerformance(duration: number): void {
        this.raycasterTimes.push(duration);
        
        if (this.raycasterTimes.length > 100) {
            const avgTime = this.raycasterTimes.reduce((a, b) => a + b) / this.raycasterTimes.length;
            console.log(`Average raycaster time: ${avgTime.toFixed(2)}ms`);
            this.raycasterTimes = [];
        }
    }
    
    public update(): void {
        this.frameCount++;
        const currentTime = performance.now();
        
        if (currentTime - this.lastTime >= 1000) {
            const fps = (this.frameCount * 1000) / (currentTime - this.lastTime);
            console.log(`Current FPS: ${fps.toFixed(2)}`);
            
            this.frameCount = 0;
            this.lastTime = currentTime;
        }
    }
}
profile
꾸준히, 의미있는 사이드 프로젝트 경험과 문제해결 과정을 기록하기 위한 공간입니다.

0개의 댓글