logo

DowanKim

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(); // ... 다른 상태 처리 }

새로운 사이클:

  1. 사람 감지 대기
  2. 실루엣 파티클 생성
  3. 카메라 확대
  4. 폭발 효과
  5. 화면 전환
  6. 리셋

무한 루프:

  • 사용자가 계속 웹캠 앞에 서면 반복
  • "To Infinity" 컨셉 구현

문제 해결 과정

문제 1: 검은 화면이 나타나지 않음

증상: 페이드 효과가 작동하지 않음

원인: display: none 상태에서 opacity 변경

해결:

// 먼저 display를 block으로 설정 this.blackScreenElement.style.display = 'block'; // 그 다음 opacity를 변경 this.blackScreenElement.style.opacity = fadeProgress;

문제 2: 리셋 후 검은 화면이 남아있음

증상: 리셋 후에도 검은 화면이 보임

원인: displayopacity 모두 리셋하지 않음

해결:

// 둘 다 리셋 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초:   시스템 완전 리셋, 새로운 사이클 준비

최종 구현 결과

  • 부드러운 페이드 전환 효과
  • 검은 화면 오버레이 시스템
  • 완전한 시스템 리셋
  • 자동 재시작 사이클
  • 모든 상태 및 타이밍 관리

프로젝트 완성

구현된 전체 기능

  1. 배경 파티클 시스템

    • 20,000개 파티클
    • 4D 노이즈 기반 자연스러운 움직임
  2. 실루엣 파티클 시스템

    • MediaPipe 실시간 사람 인식
    • 2D 마스크를 3D 파티클로 변환
  3. 카메라 확대 시스템

    • 부드러운 Z축 이동
    • 실루엣 확대 효과
  4. 폭발 효과 시스템

    • 3D 구면 좌표계 기반 균등한 폭발
    • 물리 효과 (중력, 페이드 아웃)
  5. 화면 전환 시스템

    • 부드러운 페이드 효과
    • 검은 화면 오버레이
  6. 시스템 리셋

    • 완전한 상태 초기화
    • 자동 재시작 사이클

핵심 개념

  1. 상태 머신: 시퀀스 제어 및 전이
  2. 타이밍 관리: 정확한 시간 기반 이벤트
  3. 리소스 관리: 메모리 누수 방지
  4. 사용자 경험: 부드러운 전환과 피드백

마무리

  • 실시간 사람 인식 및 파티클 변환
  • 부드러운 카메라 애니메이션
  • 시각적으로 인상적인 폭발 효과
  • 완전한 인터랙티브 시퀀스

이 프로젝트를 통해 WebGL, MediaPipe, Three.js를 활용한 인터랙티브 웹 작품 - 무한한 확대 , To Infinity 작품 구현을 마칠 수 있었습니다.

고생했다.. 지옥의 상태관리..