5. 전시장에서 계속 켜놓을 수 있게, 자동으로 시스템이 계속 리셋되게 구현
2025년 11월 2일
To Infinity
화면 전환 효과와 시스템 리셋
폭발 효과 후 부드러운 화면 전환을 구현하고, 모든 상태를 초기화해 새로운 사이클을 준비하는 시스템을 구현하고자 합니다.
전체 시퀀스 완성
1. 사람 감지 → 실루엣 파티클 생성
2. 2초 대기
3. 카메라 확대 시작
4. 폭발 트리거 (Z ≤ 0.3)
5. 폭발 효과 (5초)
6. 화면 어두워짐 (1초 페이드)
7. 검은 화면 유지 (2초)
8. 시스템 리셋 (3초 대기)
9. 새로운 사이클 준비 완료
1단계: 검은 화면 오버레이 생성
HTML 요소로 오버레이 구현
createBlackScreen() { // HTML 요소로 검은 화면 생성 this.blackScreenElement = document.createElement('div'); this.blackScreenElement.style.position = 'fixed'; this.blackScreenElement.style.top = '0'; this.blackScreenElement.style.left = '0'; this.blackScreenElement.style.width = '100%'; this.blackScreenElement.style.height = '100%'; this.blackScreenElement.style.backgroundColor = 'black'; this.blackScreenElement.style.zIndex = '9999'; this.blackScreenElement.style.opacity = '0'; this.blackScreenElement.style.pointerEvents = 'none'; this.blackScreenElement.style.display = 'none'; document.body.appendChild(this.blackScreenElement); }
왜 HTML 요소를?
- Three.js 렌더러 위에 오버레이
- CSS로 부드러운 전환 제어
- WebGL과 독립적으로 동작
스타일 설명:
position: fixed: 화면 고정zIndex: 9999: 최상위 레이어opacity: 0: 초기 투명display: none: 초기 숨김pointerEvents: none: 클릭 이벤트 차단
초기화
init() { // 초기 카메라 위치 설정 this.camera.position.set(0, 0, this.initialZ); // 검은 화면 요소 생성 this.createBlackScreen(); }
2단계: 폭발 후 화면 전환 시작
폭발 종료 감지
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초 (
DURATION_MS: 5000) - 그 후 화면 어두워지기 시작
3단계: 부드러운 페이드 효과
페이드 인 구현
handleDarkening() { const elapsed = Date.now() - this.darkeningStartTime; // 검은 화면을 서서히 나타나게 하기 if (this.blackScreenElement) { this.blackScreenElement.style.display = 'block'; // 서서히 불투명하게 만들기 const fadeProgress = Math.min(elapsed / APP_CONFIG.SCREEN_EFFECTS.BLACK_SCREEN_FADE_MS, 1); this.blackScreenElement.style.opacity = fadeProgress; } // 설정된 시간 후 초기화면으로 복귀 if (elapsed >= APP_CONFIG.SCREEN_EFFECTS.BLACK_SCREEN_DURATION_MS) { this.isDarkening = false; this.isResetting = true; this.resetStartTime = Date.now(); } }
페이드 진행도 계산:
const fadeProgress = Math.min(elapsed / BLACK_SCREEN_FADE_MS, 1); // elapsed: 경과 시간 (0 ~ 1000ms) // BLACK_SCREEN_FADE_MS: 1000ms // fadeProgress: 0.0 ~ 1.0
시각적 효과:
0ms: opacity = 0.0 (완전 투명)
250ms: opacity = 0.25 (25% 불투명)
500ms: opacity = 0.5 (50% 불투명)
750ms: opacity = 0.75 (75% 불투명)
1000ms: opacity = 1.0 (완전 불투명)
Math.min()을 사용하는 이유
// Math.min() 없이 const fadeProgress = elapsed / BLACK_SCREEN_FADE_MS; // 문제: 1.0을 초과할 수 있음 (1.2, 1.5 등) // Math.min() 사용 const fadeProgress = Math.min(elapsed / BLACK_SCREEN_FADE_MS, 1); // 해결: 최대 1.0으로 제한
4단계: 검은 화면 유지
지속 시간 관리
// 설정된 시간 후 초기화면으로 복귀 if (elapsed >= APP_CONFIG.SCREEN_EFFECTS.BLACK_SCREEN_DURATION_MS) { this.isDarkening = false; this.isResetting = true; this.resetStartTime = Date.now(); }
타이밍:
- 페이드 시간: 1초 (
BLACK_SCREEN_FADE_MS: 1000) - 검은 화면 유지: 2초 (
BLACK_SCREEN_DURATION_MS: 2000) - 총 어두워짐 시간: 3초
왜 검은 화면을 유지하는가?
- 폭발 효과의 여운 유지
- 전환의 명확한 구분
- 다음 사이클 준비 시간 확보
5단계: 리셋 대기
리셋 시작
handleReset() { const elapsed = Date.now() - this.resetStartTime; // 설정된 시간(3초) 후 시스템 완전 리셋 if (elapsed >= APP_CONFIG.SCREEN_EFFECTS.RESET_DELAY_MS) { this.resetSystem(); } }
리셋 대기 시간:
RESET_DELAY_MS: 3000- 검은 화면 유지 후 추가 대기
- 시스템 안정화 시간
6단계: 완전한 시스템 리셋
리셋 메서드 구현
resetSystem() { // 1. 카메라를 초기 위치로 즉시 리셋 this.camera.position.set(0, 0, this.initialZ); // 2. 배경색을 원래대로 복원 this.renderer.setClearColor(this.originalBackgroundColor, 1.0); // 3. 검은 화면 숨기기 if (this.blackScreenElement) { this.blackScreenElement.style.display = 'none'; this.blackScreenElement.style.opacity = '0'; } // 4. 모든 상태 리셋 this.isMoving = false; this.isExploding = false; this.isDarkening = false; this.isResetting = false; this.personDetected = false; this.explosionTriggered = false; // 5. 폭발 파티클 완전 리셋 this.explosionParticles.reset(); // 6. 타이밍 리셋 this.detectionTime = 0; this.explosionStartTime = 0; this.darkeningStartTime = 0; this.resetStartTime = 0; }
카메라 위치 리셋
this.camera.position.set(0, 0, this.initialZ); // Z = 50으로 즉시 복귀
왜 즉시 리셋?
- 검은 화면 중이므로 부드러운 이동 불필요
- 다음 사이클을 빠르게 준비
- ux 영향 없음
배경색 복원
this.renderer.setClearColor(this.originalBackgroundColor, 1.0);
배경색 관리:
- 초기 저장:
this.originalBackgroundColor = new THREE.Color(0x000000) - 리셋 시 복원: 검은색으로 복귀
검은 화면 숨기기
if (this.blackScreenElement) { this.blackScreenElement.style.display = 'none'; this.blackScreenElement.style.opacity = '0'; }
왜 둘 다 설정하는가?
display: none: DOM에서 완전히 제거opacity: 0: 다음 페이드 준비
상태 변수 리셋
// 모든 상태 플래그 리셋 this.isMoving = false; this.isExploding = false; this.isDarkening = false; this.isResetting = false; this.personDetected = false; this.explosionTriggered = false;
상태 리셋의 중요성:
- 다음 사이클을 깨끗하게 시작
- 메모리 누수 방지
- 예상치 못한 동작 방지
폭발 파티클 리셋
this.explosionParticles.reset();
폭발 파티클 리셋 내용:
reset() { this.isActive = false; this.explosionSystem.visible = false; // 모든 파티클을 중앙으로 리셋 const positions = this.explosionGeometry.attributes.position.array; for (let i = 0; i < this.particleCount; i++) { const i3 = i * 3; positions[i3] = 0; positions[i3 + 1] = 0; positions[i3 + 2] = 0; this.lifetimes[i] = 0; } this.explosionGeometry.attributes.position.needsUpdate = true; }
리셋 내용:
- 활성 상태 비활성화
- 파티클 시스템 숨김
- 모든 파티클 위치를 (0, 0, 0)으로
- 수명 초기화
타이밍 변수 리셋
this.detectionTime = 0; this.explosionStartTime = 0; this.darkeningStartTime = 0; this.resetStartTime = 0;
타이밍 리셋의 중요성:
- 다음 사이클의 정확한 타이밍 계산
- 오래된 타임스탬프로 인한 버그 방지
7단계: 새로운 사이클 준비
자동 재시작
리셋 후 시스템은 자동으로 다음 사이클을 준비합니다:
update() { // 사람 감지 확인 (리셋 후 자동으로 다시 시작) this.checkPersonDetection(); // ... 다른 상태 처리 }
새로운 사이클:
- 사람 감지 대기
- 실루엣 파티클 생성
- 카메라 확대
- 폭발 효과
- 화면 전환
- 리셋
무한 루프:
- 사용자가 계속 웹캠 앞에 서면 반복
- "To Infinity" 컨셉 구현
문제 해결 과정
문제 1: 검은 화면이 나타나지 않음
증상: 페이드 효과가 작동하지 않음
원인: display: none 상태에서 opacity 변경
해결:
// 먼저 display를 block으로 설정 this.blackScreenElement.style.display = 'block'; // 그 다음 opacity를 변경 this.blackScreenElement.style.opacity = fadeProgress;
문제 2: 리셋 후 검은 화면이 남아있음
증상: 리셋 후에도 검은 화면이 보임
원인: display와 opacity 모두 리셋하지 않음
해결:
// 둘 다 리셋 this.blackScreenElement.style.display = 'none'; this.blackScreenElement.style.opacity = '0';
문제 3: 리셋 후 즉시 다음 사이클 시작
증상: 리셋 직후 바로 사람 감지됨
원인: 상태 리셋 순서 문제
해결:
// 상태를 먼저 리셋 this.personDetected = false; this.explosionTriggered = false; // 그 다음 타이밍 리셋 this.detectionTime = 0;
문제 4: 폭발 파티클이 리셋 후에도 보임
증상: 리셋 후에도 파티클이 화면에 남아있음
원인: reset() 메서드에서 visible 설정 누락
해결:
reset() { this.isActive = false; this.explosionSystem.visible = false; // 넣어야됨 // ... 나머지 리셋 }
전체 시퀀스 타이밍 정리
0초: 사람 감지
2초: 카메라 확대 시작
~10초: 폭발 트리거 (Z ≤ 0.3)
~15초: 폭발 종료, 화면 어두워지기 시작
~16초: 검은 화면 완전히 나타남
~18초: 리셋 시작
~21초: 시스템 완전 리셋, 새로운 사이클 준비
최종 구현 결과
- 부드러운 페이드 전환 효과
- 검은 화면 오버레이 시스템
- 완전한 시스템 리셋
- 자동 재시작 사이클
- 모든 상태 및 타이밍 관리
프로젝트 완성
구현된 전체 기능
-
배경 파티클 시스템
- 20,000개 파티클
- 4D 노이즈 기반 자연스러운 움직임
-
실루엣 파티클 시스템
- MediaPipe 실시간 사람 인식
- 2D 마스크를 3D 파티클로 변환
-
카메라 확대 시스템
- 부드러운 Z축 이동
- 실루엣 확대 효과
-
폭발 효과 시스템
- 3D 구면 좌표계 기반 균등한 폭발
- 물리 효과 (중력, 페이드 아웃)
-
화면 전환 시스템
- 부드러운 페이드 효과
- 검은 화면 오버레이
-
시스템 리셋
- 완전한 상태 초기화
- 자동 재시작 사이클
핵심 개념
- 상태 머신: 시퀀스 제어 및 전이
- 타이밍 관리: 정확한 시간 기반 이벤트
- 리소스 관리: 메모리 누수 방지
- 사용자 경험: 부드러운 전환과 피드백
마무리
- 실시간 사람 인식 및 파티클 변환
- 부드러운 카메라 애니메이션
- 시각적으로 인상적인 폭발 효과
- 완전한 인터랙티브 시퀀스
이 프로젝트를 통해 WebGL, MediaPipe, Three.js를 활용한 인터랙티브 웹 작품 - 무한한 확대 , To Infinity 작품 구현을 마칠 수 있었습니다.
고생했다.. 지옥의 상태관리..