4. 확대되는 효과, 사람 실루엣을 보여주는 캔버스를 앞으로 당길까 아니면 카메라를 앞으로 보낼까.
2025년 10월 10일
To Infinity
카메라 확대 시스템과 폭발 효과
사람이 인식되면 실루엣 파티클이 나타나고, 카메라가 확대되며, 실루엣이 화면을 가득 채울 때 폭발 효과가 발생하는 인터랙티브 시퀀스를 구현합니다.
구현해야할 흐름
1. 사람 감지 → 실루엣 파티클 생성
2. 2초 대기
3. 카메라 확대 시작 (Z축 앞으로 이동)
4. 실루엣이 화면 가득 차게 됨
5. 폭발 트리거 (Z ≤ 0.3 ? 정도)
6. 폭발 효과 (5초?)
7. 화면 어두워짐
8. 시스템 리셋
1단계: 사람 감지 및 상태 관리
사람 감지 확인
checkPersonDetection() { // 이미 다른 상태에 있으면 무시 if (this.isMoving || this.isExploding || this.isDarkening || this.isResetting) return; // 사람 감지 확인 if (this.silhouetteParticles && this.silhouetteParticles.isPersonDetected()) { if (!this.personDetected) { this.personDetected = true; this.detectionTime = Date.now(); } // 설정된 시간(2초) 후 카메라 이동 시작 if (Date.now() - this.detectionTime >= APP_CONFIG.PERSON_DETECTION.DELAY_MS) { this.isMoving = true; } } else { this.personDetected = false; } }
상태 관리:
personDetected: 사람 감지 여부detectionTime: 감지 시점 기록isMoving: 카메라 이동 중 여부
대기 시간:
DELAY_MS: 2000: 감지 후 2초 대기- 사용자가 자리를 잡을 시간 확보
2단계: 카메라 확대 시스템
접근 방식: 카메라 Z축 이동
moveCamera() { // 카메라를 앞으로 이동 (Z축 감소) this.camera.position.z -= APP_CONFIG.CAMERA.MOVEMENT_SPEED; // 목표 위치에 도달하면 카메라 이동 정지 if (this.camera.position.z <= this.targetZ) { this.isMoving = false; } }
왜 카메라를 앞으로 이동시키는가?
- 실루엣 파티클은 Z=0에 고정
- 카메라가 앞으로 이동하면 상대적으로 실루엣이 커 보임
- 파티클 위치 변경 없이 확대 효과 구현
- 파티클 위치를 변경시키는 것보다, 카메라를 움직이는게 훨씬 쉽다고 판단
카메라 위치 설정
// 초기 위치 this.initialZ = 50; // 카메라가 뒤에 있음 (배경 파티클 보기) // 목표 위치 this.targetZ = -10; // 카메라가 앞으로 이동 (실루엣 확대) // 이동 속도 MOVEMENT_SPEED: 0.03 // 매 프레임마다 0.03씩 이동
시각적 효과:
초기: 카메라 Z=50, 실루엣 Z=0
→ 실루엣이 작게 보임
확대 중: 카메라 Z=25, 실루엣 Z=0
→ 실루엣이 점점 커짐
최종: 카메라 Z=0, 실루엣 Z=0
→ 실루엣이 화면 가득 참
-10까지 땡기는 이유는, 폭발 이후에 초기의 배경 우주를 사용자가 다시 보여주게 하고 싶어서..
부드러운 이동 구현
// 매 프레임마다 작은 값씩 이동 this.camera.position.z -= 0.03; // 결과: 부드러운 확대 애니메이션 // 50 → 49.97 → 49.94 → ... → -10
이동 속도 조절:
- 너무 빠르면:
0.1→ 급격한 확대 - 너무 느리면:
0.01→ 느린 확대 - 적절한 속도:
0.03→ 자연스러운 확대
3단계: 폭발 트리거 조건
언제 폭발을 시작할까?
// 카메라 Z 좌표가 설정된 값 이하가 되면 폭발 시작 if (this.camera.position.z <= APP_CONFIG.CAMERA.EXPLOSION_TRIGGER_Z && !this.explosionTriggered) { this.explosionTriggered = true; this.startExplosion(); }
폭발 트리거 조건:
EXPLOSION_TRIGGER_Z: 0.3- 카메라가 실루엣에 매우 가까워졌을 때
- 실루엣이 화면을 가득 채워 더 이상 확대가 무의미할 때
왜 0.3인가?
- Z=0은 실루엣 위치
- Z=0.3은 실루엣에 매우 가까운 위치
- 이 시점에서 실루엣이 화면을 거의 가득 채움
폭발 위치 계산
startExplosion() { this.isExploding = true; this.explosionStartTime = Date.now(); // 카메라 앞쪽에서 폭발 효과 시작 const explosionPosition = { x: this.camera.position.x, y: this.camera.position.y, z: this.camera.position.z - 20 // 카메라 앞쪽 20 거리 }; this.explosionParticles.explodeAt(explosionPosition); }
폭발 위치:
- 카메라 위치 기준
- 카메라 앞쪽 20 단위 지점
- 화면 중앙에서 폭발하도록 설정
4단계: 폭발 파티클 시스템 구현
폭발 파티클 초기화
init() { this.explosionGeometry = new THREE.BufferGeometry(); // 위치, 속도, 색상, 수명 배열 this.positions = new Float32Array(this.particleCount * 3); this.velocities = new Float32Array(this.particleCount * 3); this.colors = new Float32Array(this.particleCount * 3); this.lifetimes = new Float32Array(this.particleCount); // 초기에는 숨김 this.explosionSystem.visible = false; }
파티클 개수:
PARTICLE_COUNT: 2000- 충분한 밀도로 폭발 효과 표현
3D 구면 좌표계로 균등한 폭발
explodeAt(position) { for (let i = 0; i < this.particleCount; i++) { // 중심을 기준으로 사방으로 균등하게 폭발 const horizontalAngle = Math.random() * Math.PI * 2; // 수평 방향 (0~2π) const verticalAngle = Math.random() * Math.PI; // 수직 방향 (0~π) const speed = Math.random() * 8 + 3; // 속도 3~11 // 3D 구면 좌표계로 균등한 방향 생성 this.velocities[i3] = Math.sin(verticalAngle) * Math.cos(horizontalAngle) * speed; this.velocities[i3 + 1] = Math.cos(verticalAngle) * speed; this.velocities[i3 + 2] = Math.sin(verticalAngle) * Math.sin(horizontalAngle) * speed; } }
구면 좌표계 사용 이유:
- 모든 방향으로 균등하게 분산
- 자연스러운 구형 폭발
Math.random()만 사용하면 중심에 집중될 수 있음
horizontalAngle: 0 ~ 2π (수평 회전)
verticalAngle: 0 ~ π (수직 각도)
X = sin(vertical) * cos(horizontal) * speed
Y = cos(vertical) * speed
Z = sin(vertical) * sin(horizontal) * speed
불꽃놀이 색상 적용
// 불꽃놀이 색상 (빨강, 주황, 노랑, 흰색) const colorType = Math.random(); if (colorType < 0.3) { colors[i3] = 1; // 빨강 colors[i3 + 1] = 0.2; colors[i3 + 2] = 0; } else if (colorType < 0.6) { colors[i3] = 1; // 주황 colors[i3 + 1] = 0.5; colors[i3 + 2] = 0; } else if (colorType < 0.8) { colors[i3] = 1; // 노랑 colors[i3 + 1] = 1; colors[i3 + 2] = 0; } else { colors[i3] = 1; // 흰색 colors[i3 + 1] = 1; colors[i3 + 2] = 1; }
색상 확률:
- 빨강: 30%
- 주황: 30%
- 노랑: 20%
- 흰색: 20%
5단계: 폭발 파티클 애니메이션
파티클 이동 및 물리 효과
update() { if (!this.isActive) return; for (let i = 0; i < this.particleCount; i++) { // 파티클 이동 positions[i3] += this.velocities[i3] * 0.15; positions[i3 + 1] += this.velocities[i3 + 1] * 0.15; positions[i3 + 2] += this.velocities[i3 + 2] * 0.15; // 중력 효과 this.velocities[i3 + 1] -= 0.05; // 수명 증가 this.lifetimes[i] += 0.01; // 투명도 감소 (페이드 아웃) const alpha = Math.max(0, 1 - this.lifetimes[i] * 0.5); colors[i3] *= alpha; colors[i3 + 1] *= alpha; colors[i3 + 2] *= alpha; } }
물리 효과:
- 속도 기반 이동:
positions += velocities * 0.15 - 중력: Y축 속도 감소 (
velocities[Y] -= 0.05) - 페이드 아웃: 수명에 따라 투명도 감소
파티클 수명 관리
// 파티클이 너무 멀리 가거나 투명해지면 화면 밖으로 이동 if (this.lifetimes[i] > 3 || alpha <= 0) { positions[i3] = 1000; positions[i3 + 1] = 1000; positions[i3 + 2] = 1000; }
수명 관리:
- 최대 수명: 3초
- 투명도가 0이 되면 제거
- 화면 밖으로 이동시켜 렌더링 부하 감소
6단계: 시퀀스 제어 및 타이밍
상태 머신 구현
update() { // 사람 감지 확인 this.checkPersonDetection(); // 카메라 이동 처리 if (this.isMoving) { this.moveCamera(); } // 폭발 효과 처리 if (this.isExploding) { this.explosionParticles.update(); this.handleExplosion(); } // 화면 어두워지는 효과 처리 if (this.isDarkening) { this.handleDarkening(); } // 초기화면 복귀 처리 if (this.isResetting) { this.handleReset(); } }
상태 전이:
대기 → 사람 감지 → 카메라 이동 → 폭발 → 어두워짐 → 리셋
폭발 후 시퀀스
handleExplosion() { const elapsed = Date.now() - this.explosionStartTime; // 설정된 시간(5초) 후 화면 어두워지기 시작 if (elapsed >= APP_CONFIG.EXPLOSION.DURATION_MS) { this.isExploding = false; this.isDarkening = true; this.darkeningStartTime = Date.now(); } }
타이밍:
- 폭발 지속: 5초
- 어두워짐: 1초 페이드
- 검은 화면: 2초 유지
- 리셋 대기: 3초
문제 해결 과정
문제 1: 카메라가 너무 빠르게 이동
증상: 확대가 급격함
원인: MOVEMENT_SPEED가 너무 큼
해결:
// 초기 시도 MOVEMENT_SPEED: 0.1 // 너무 빠름 // 조정 후 MOVEMENT_SPEED: 0.03 // 적절한 속도
문제 2: 폭발이 여러 번 발생
증상: 폭발이 중복 트리거됨
원인: explosionTriggered 플래그 누락
해결:
if (this.camera.position.z <= EXPLOSION_TRIGGER_Z && !this.explosionTriggered) { this.explosionTriggered = true; // 플래그 설정 this.startExplosion(); }
문제 3: 폭발 파티클이 한쪽으로 치우침
증상: 파티클이 특정 방향으로만 날아감
원인: 랜덤 분산이 불균등
해결:
// 구면 좌표계 사용 const horizontalAngle = Math.random() * Math.PI * 2; const verticalAngle = Math.random() * Math.PI;
문제 4: 폭발 후 카메라가 계속 이동
증상: 폭발 중에도 카메라가 이동함
원인: 상태 체크 누락
해결:
moveCamera() { // 폭발 중에는 카메라 이동 계속 (의도된 동작) this.camera.position.z -= MOVEMENT_SPEED; // 목표 위치에 도달하면 정지 if (this.camera.position.z <= this.targetZ) { this.isMoving = false; } }
문제 5: 폭발 파티클이 너무 빨리 사라짐
증상: 파티클이 금방 사라짐
원인: 수명이 너무 짧음
해결:
// 초기 시도 const alpha = Math.max(0, 1 - this.lifetimes[i] * 1.0); // 너무 빠름 // 조정 후 const alpha = Math.max(0, 1 - this.lifetimes[i] * 0.5); // 적절함
최종 구현 결과
- 카메라 Z축 이동: 파티클 위치 변경 없이 확대 효과
- 폭발 트리거: 카메라 위치 기반 조건부 실행
- 구면 좌표계: 3D 공간에서 균등한 방향 분산
- 상태 머신: 시퀀스 제어 및 타이밍 관리
- 물리 시뮬레이션: 중력, 속도, 수명 기반 애니메이션
이를 통해 제가 처음에 의도한 시나리오는 완성이 되었습니다. 중간중간 최대한 리팩터링이 가능하게 코드를 작성한다고 조금 시간이 걸린거 같습니다.
이제 이 시나리오가 반복되는 시스템만 손봐주면(화면 암전 후 다시 시작) 완성이 될 것 같습니다. 오래 걸렸다...