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;
}
}
복잡한 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;
}
}
}
레이캐스터를 활용한 드래그 & 드롭 시스템을 구현해보겠습니다.
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);
}
}
}
레이캐스터로 선택된 오브젝트에 커스텀 하이라이트 효과를 적용해보겠습니다.
// 버텍스 셰이더
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
});
}
}
}
레이캐스터가 오브젝트를 통과하지 못하도록 오클루전 처리를 구현합니다.
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;
}
}
}