first commit
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
# 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 셰이더 정상 동작을 주장하지 않는다.
|
||||
- 결과 보고에는 생성/수정 파일, 구현 기능, 미완료 또는 미검증 항목, 실행 방법, 셰이더 원리와 다음 개선점을 포함한다.
|
||||
Reference in New Issue
Block a user