Files
behind_glass/docs/implementation-plan.md
T
2026-10-06 12:16:46 +09:00

9.5 KiB

Pet Glass MVP 상세 구현 지시

원문은 task-brief.txt이며 이 문서는 구현 방법과 검증 기준을 구체화한다.

범위와 가정

  • Vite + strict TypeScript + Three.js + GLSL, Vanilla TS. 백엔드와 React는 사용하지 않는다.
  • 업로드 미디어는 브라우저 내부에서만 처리한다. 외부 샘플 이미지가 없어도 안내 placeholder로 실행된다.
  • 실제 PetPlane과 GlassPlane 사이 거리를 갖는 단일 표면 굴절 근사를 구현한다. 물리적으로 정확한 두꺼운 유리 시뮬레이션이나 여러 렌더 패스는 MVP 범위가 아니다.
  • 구조는 원문 예상 폴더를 참고하되, 빈 관리자나 한 번만 쓰는 기능을 위한 추가 추상화를 만들지 않는다.

구현 순서

  1. Scene, PerspectiveCamera, renderer와 이미지 업로드, PetPlane을 구현하고 비율 보존을 확인한다.
  2. 실제 GlassPlane, pointer drag, Mosaic 셰이더를 구현하여 시점 변화에 따른 서로 다른 셀 굴절을 눈으로 검증한다.
  3. VideoTexture와 미디어 수명 관리를 추가한다.
  4. Reeded, Frosted, gyro와 반응형 컨트롤을 완성한다.
  5. 타입 검사와 빌드, 브라우저 동작을 검증하고 제한사항을 기록한다.

Scene과 굴절

  • PerspectiveCamera는 대략 fov 40도, (0,0,3)에서 시작하고 원점을 바라본다.
  • PetPlane은 z=0, GlassPlane은 z=0.2에 배치한다. 실제 PlaneGeometry 두 개를 scene에 추가한다. 셀이나 ridge마다 mesh를 만들지 않는다.
  • 미디어 texture는 두 plane에서 공유하되 MediaLoader만 texture의 수명을 소유한다.
  • vertex shader에서 유리 표면 world position을 전달한다. fragment shader는 cameraPosition에서 표면으로 향하는 입사 ray를 계산하고, 셀 내부 위치에 따라 변화하는 +z 방향 normal로 굴절시킨다.
  • 유리 위치에서 굴절 ray와 pet z=0의 교차점을 구한 뒤 pet의 실제 width/height로 UV를 계산한다. 이렇게 해야 유리와 미디어 사이 거리, 카메라 시점, 셀 normal이 함께 샘플 위치를 결정한다.
  • strength=0은 기본 직진 ray, strength가 증가하면 굴절 ray가 반영되도록 한다. ray.z는 교차점 계산에서 0이 되지 않도록 제한한다.
  • 원문의 uTexture, uGridSize, uStrength, uViewOffset, uResolution, uTime uniform을 유지한다. uViewOffset는 작은 셀별 normal 또는 굴절 응답 변화에 사용할 수 있지만 모든 UV에 동일한 이동만 주는 구현은 허용하지 않는다.
  • 컬러 텍스처 색공간과 fragment 출력 색공간 처리를 맞추고 이미지·비디오에서 방향과 색상이 일치하는지 확인한다.

Contain과 겹침 처리

  • 원본 가로세로 비율을 항상 보존한다. 캔버스의 실제 컨테이너 크기와 카메라 frustum으로 크기를 계산한다.
  • 유리 평면이 카메라에 더 가까우므로 앞쪽 GlassPlane의 가시 영역을 기준으로 두 평면 크기를 정한다. 약 15% 여백과 최대 약 0.15 world-unit의 카메라 이동 제한을 시작값으로 사용한다.
  • resize 시 두 plane 크기와 shader의 pet 크기, 해상도 uniform을 함께 갱신한다. 드래그 중 이미지 크기가 수시로 변하는 자동 확대는 피한다.
  • 같은 크기의 앞쪽 유리는 정면 투영에서 뒤쪽 PetPlane을 덮는다. 카메라 이동 극단과 portrait/landscape viewport에서도 뒤쪽 원본 사진이 가장자리에 중복 노출되지 않는지 반드시 확인한다. 필요하면 유리의 면적을 소폭 넓히되 pet 크기 기반 sampling 좌표는 유지한다.
  • GlassPlane에서 texture를 샘플하여 최종 색상을 출력할 때 opaque 출력으로 시작한다. 원본 사진과 반투명 합성하여 얼굴이 이중으로 보이는 결과를 만들지 않는다.
  • 굴절된 UV가 미디어 범위를 벗어나면 scene과 맞는 배경색 또는 부드러운 가장자리 fade를 사용한다. clamp만으로 사진 가장자리를 길게 늘이지 않는다.
  • contain은 배치상 원본 미디어가 잘리지 않는다는 의미다. 굴절 때문에 경계의 일부가 다른 샘플로 바뀌는 것은 효과에 포함되지만, 드래그 시 평면 자체가 viewport 밖으로 잘리면 안 된다.

효과별 요구사항

Mosaic

  • vUv를 grid로 나누고 cell=floor(scaled), local=fract(scaled)-0.5를 계산한다.
  • local에 따라 변화하는 볼록/오목 렌즈 normal과 cell hash 기반 약 ±15% 강도 차이를 사용한다.
  • 카메라 이동 시 서로 다른 cell에서 얼굴의 눈·코 등 feature가 다르게 움직여야 한다. cell 중심의 단일 texel로 양자화하는 pixelation은 금지한다.
  • 셀 경계는 은은하게 표현한다. 종횡비가 극단적인 사진에서도 사각형 렌즈 셀 형태가 과도하게 찌그러지지 않도록 grid의 축별 비율을 고려한다.

Reeded

  • x 방향으로 반복되는 원통 렌즈 normal을 만든다. y 방향 변형은 거의 없도록 한다.
  • local ridge 위치와 약한 ridge별 차이가 굴절에 반영되어야 한다. 좌우 시점 변화가 전체 사진 이동에 그치면 안 된다.

Frosted

  • 안정된 공간 noise로 normal 또는 UV를 왜곡하고 5~9개의 주변 texture 샘플을 평균한다.
  • 대략적인 형태는 남기고 디테일을 흐리게 한다. 시점 변화에 굴절 방향이 반응하게 하되 시간만으로 noise가 끓는 효과는 피한다.

입력과 애니메이션

  • pointerdown/move/up/cancel, pointer capture를 사용하고 canvas에만 touch-action:none을 설정한다.
  • drag delta를 canvas 크기로 정규화하여 target에 누적한 뒤 [-1,1]로 clamp한다.
  • pointer와 gyro는 하나의 target/current 상태를 공유한다. sensitivity는 한 곳에서만 적용한다.
  • requestAnimationFrame에서 시간 기반 보간으로 카메라와 shader 상태를 갱신한다. VideoTexture를 매 프레임 새로 만들지 않는다.
  • Reset은 target/current를 중앙으로 돌리고 gyro 사용 중이면 기준 자세도 재설정한다.
  • ResizeObserver로 실제 canvas container 크기를 반영하고 DPR은 최대 2로 제한한다.

미디어 수명과 오류

  • jpg/jpeg/png/webp 이미지와 브라우저에서 재생할 수 있는 mp4/webm 영상을 지원한다.
  • 새 미디어가 준비된 다음 기존 미디어를 교체한다. 로드 실패 시 기존 화면을 보존하고 읽을 수 있는 오류를 표시한다.
  • 증가하는 요청 ID를 사용하여 연속 업로드 중 늦게 준비된 이전 파일이 최신 파일을 덮지 않게 한다. stale 결과도 dispose/revoke한다.
  • 비디오는 muted, loop, playsInline을 설정하고 metadata와 첫 frame을 준비한 뒤 VideoTexture를 적용한다. play() 실패 시 사용자용 재생 버튼과 상태 메시지를 제공한다.
  • 교체/실패/해제 시 texture.dispose, video.pause, video src 해제와 load, URL.revokeObjectURL을 빠뜨리지 않는다. 활성 미디어와 로드 중 미디어의 정리를 구분한다.
  • scene 해제 시 RAF, observer, DOM/sensor 이벤트, material, geometry, renderer와 미디어를 정리한다. material 교체 시 이전 material을 dispose한다.

Gyroscope

  • 기본 OFF이며 사용자 버튼 클릭에서 권한을 요청한다. iOS requestPermission은 클릭 핸들러에서 다른 비동기 대기 전에 호출한다.
  • 권한 거절, 미지원, 비보안 컨텍스트를 표시하고 drag는 계속 사용 가능하게 한다.
  • 권한 API가 없는 환경에서도 orientation 이벤트를 연결하되 null beta/gamma는 무시한다.
  • 첫 유효 reading을 기준 자세로 삼고 약 30도 기울기를 [-1,1]로 변환한다. 화면 회전 방향을 보정한다.
  • 이벤트가 없으면 센서값 대기 상태로 표시한다. OFF 또는 해제 시 listener를 제거한다. 권한 허용만으로 실제 센서 동작이 검증됐다고 보고하지 않는다.

UI

  • Dark UI, 큰 canvas, 모바일에서 접근 가능한 반투명 컨트롤 패널을 사용한다.
  • 사진/영상 업로드, Mosaic/Reeded/Frosted, strength, sensitivity, Reset, gyro toggle, 현재 효과와 상태 표시를 제공한다.
  • 초기에는 “반려동물 사진 또는 영상을 선택하세요”, 미디어 준비 후 약 2초간 drag 안내를 표시한다.
  • 패널 스크롤과 canvas drag가 충돌하지 않도록 한다. WebGL 초기화 실패 시 화면에 설명한다.

완료 검증

  • npm install, npm run build, npm run dev가 동작하고 TypeScript 오류가 없다.
  • 가로/세로 이미지 업로드, canvas resize, 모바일 viewport에서 비율과 contain이 유지된다.
  • 드래그 전후 Mosaic의 서로 다른 셀 내부 feature가 서로 다르게 움직인다. Reeded는 x 중심 굴절, Frosted는 흐림과 시점 반응을 보인다.
  • strength 0부터 최대값, sensitivity, Reset, pointer cancel을 확인한다.
  • 영상 loop 및 재생 실패 UI, 영상→이미지→영상 교체, 빠른 연속 업로드를 확인한다.
  • 카메라 이동 극단에서 원본 사진이 유리 옆에 중복 노출되지 않고, 유리/미디어 평면이 viewport 밖으로 잘리지 않는다.
  • gyro 미지원/거절 시 drag가 유지된다. 실제 센서와 성능 수치를 검증하지 못하면 미검증으로 표시한다.
  • 브라우저에 지속적인 콘솔 오류나 GLSL 컴파일 오류가 없다. 빌드 성공만으로 WebGL 셰이더 정상 동작을 주장하지 않는다.
  • 결과 보고에는 생성/수정 파일, 구현 기능, 미완료 또는 미검증 항목, 실행 방법, 셰이더 원리와 다음 개선점을 포함한다.