# AI 꾸러미 — 사진 2.5D 시차 (깊이 지도 · 셰이더) — 2.5D parallax / depth parallax (3D photo)
> 그림 한 장과 흑백 깊이 지도(흰색 = 가까움)로 가까운 것은 많이, 먼 것은 조금 밀어 정지 그림 속으로 카메라가 들어간 듯 — 셰이더가 깊이 층을 훑어 가림까지 맞추고, 가장자리 찢김은 깊이를 넓혀 막는다.  
> 견본: https://ai-techstudio.web.app/#t/i558

이 문서를 코드 도우미(Claude Code · Cursor · ChatGPT 등)에 그대로 주면 돼요. 「## 주문서」 가 할 일, 나머지는 참고 자료예요.

## 주문서

### 만들어 줘: 사진 2.5D 시차 (깊이 지도 · 셰이더) — 2.5D parallax / depth parallax (3D photo)

#### 1. 목표
이야기 장면 그림 (노을 들판)을(를) 그림 한 장과 깊이 지도로 2.5D 시차 효과를 만들어 줘 — 카메라가 옆으로 흔들리고 앞으로 다가가면 가까운 것은 많이, 먼 것은 조금 움직인다. 분위기는 천천히 숨쉬듯 (켄 번스처럼).

#### 2. 핵심 기술 용어
- **2.5D parallax / depth parallax (3D photo)** — 그림 한 장 + 깊이 지도로 만드는 입체 카메라 움직임
- **Depth map** — 픽셀마다 가까운 정도를 밝기로 — AI 깊이 추정(Depth Anything 등)으로도 얻음
- **Parallax occlusion mapping (ray march)** — 가까운 층부터 훑어 「이 깊이의 점이 여기로 밀려왔나」를 찾아 가림까지 맞추기
- **Depth dilation** — 깊이 지도의 가까운 쪽을 몇 픽셀 넓혀, 경계에서 배경이 늘어나게
- **Dolly zoom** — 앞으로 다가갈 때 가까운 층이 더 크게 커지는 원근

#### 3. 환경
- 플랫폼: three.js r186 (ES 모듈 · TypeScript, `import * as THREE from "three"`), WebGL2, 외부 라이브러리 추가 없이
- 화면: 2D · 브라우저 — PC 와 폰(가로 844×390 · 세로 390×844) 모두, 60fps 목표
- 화면을 덮는 평면 하나 + ShaderMaterial. 그림 · 깊이를 텍스처로, 카메라 이동은 uniform 하나. 깊이 지도는 그림과 같은 크기 · 같은 자리로.

#### 4. 조건
- 깊이 지도는 그림과 같은 크기 · 같은 자리 (흰색 = 가까움)
- 셰이더는 가까운 층부터 먼 층으로 훑고, 층 사이는 선형 보간
- 깊이 지도의 가까운 쪽을 3~5px 넓혀서 쓸 것 (경계 찢김 막기)
- 그림을 10~16% 잘라 쓰기 — 밀려도 그림 밖이 안 보이게
- 움직임은 작게: 옆 이동은 화면 폭의 4~9%, 오래 보는 화면이면 더 작게

#### 5. 완성 기준 (이게 보이면 성공)
- 카메라가 옆으로 흔들리면 앞 바위 · 사람은 많이, 나무 · 집은 덜, 산 · 하늘은 거의 안 움직인다
- 「깊이 지도 보기」를 켜면 흰 앞쪽 · 검은 하늘이 그림과 같은 모양으로 보인다
- 「깊이 가장자리 넓히기」를 끄면 사람 · 나무 가장자리가 찢어져 늘어나고, 켜면 대신 뒤 배경이 살짝 늘어난다
- 「앞으로 다가가기」를 켜면 가까운 것이 더 크게 다가온다
- 초점 깊이를 바꾸면 안 움직이는 층이 바뀐다 (사람 고정 → 뒤가 반대로 흐름)

#### 6. 진행 방식
- 핵심 코드 위주로, 설명은 짧게. 내 프로젝트에 끼워 넣기 쉬운 함수 · 클래스로 나눠 줘.
- 처음 화면에 바로 결과가 보이게, 그리고 켬/끔(또는 전/후) 비교를 할 수 있게 만들어 줘.
- 그림 · 소리 · 모델 파일이 필요하면 코드로 만든 임시 대체물로 먼저 돌아가게 하고, 진짜 파일로 바꿀 자리를 표시해 줘.
- 마지막에 「확인 방법」(무엇을 보면 성공인지)과 「조절할 값」 목록을 짧게 정리해 줘.
- 답변과 코드 주석은 한국어로 해 줘.

## 원리
- 깊이 d(0 먼 ~ 1 가까움)인 점은 화면에서 off × (d − 초점) 만큼 밀린다. 초점 깊이의 층은 제자리, 그보다 가까우면 카메라 반대로, 멀면 같은 쪽으로 — 이 차이가 입체감이다.
- 셰이더는 거꾸로 푼다: 화면 자리 uv 에서 가까운 층(1)부터 먼 층(0)으로 훑으며 p = uv − off × (층 − 초점) 의 깊이를 읽고, 깊이 ≥ 층 인 첫 자리를 쓴다. 가까운 것부터 보므로 앞 물체가 뒤를 가린다.
- 층 사이를 그냥 끊으면 계단이 보인다 — 이전 층과 이 층의 차이로 선형 보간해 만나는 점을 찾는다 (패럴랙스 오클루전 매핑과 같다).
- 앞으로 다가가기(돌리): 자리를 가운데 기준 1 + 세기 × (층 − 초점) 으로 나눠 가까운 층일수록 크게 키운다.
- 경계 찢김: 앞 물체 가장자리의 깊이가 흐리면 물체가 늘어나 찢어져 보인다. 깊이 지도의 밝은 쪽을 3~5px 넓히면(최댓값 필터) 대신 뒤 배경이 늘어나 눈에 덜 띈다. 그림 가장자리는 10~16% 잘라 밀려도 밖이 안 보이게.

## 핵심 코드 — 깊이 층 훑기 + 층 사이 보간 (조각 셰이더)
(발췌: demos/demosMotionE.ts 의 FRAG558 을 정리)
```glsl
uniform sampler2D uCol, uDep;   // 그림 · 깊이 (흰색 = 가까움)
uniform vec2 uOff;               // 카메라 옆 이동
uniform float uFocus, uDolly;    // 안 움직이는 깊이 · 다가가기 세기
varying vec2 vUv;
void main() {
  vec2 uv = (vUv - 0.5) * 0.86 + 0.5;          // 가장자리 여유
  vec2 hit = uv, prevP = uv;
  float prevD = -1.0;
  const int N = 40;
  for (int i = 0; i < N; i++) {
    float layer = 1.0 - float(i) / float(N - 1);  // 가까운 층부터
    float k = layer - uFocus;
    vec2 p = 0.5 + (uv - 0.5) / (1.0 + uDolly * k) - uOff * k;
    float diff = texture2D(uDep, p).r - layer;
    hit = p;
    if (diff >= 0.0) {                            // 이 깊이의 점이 여기로 왔다
      if (i > 0) hit = mix(p, prevP, diff / (diff - prevD));
      break;
    }
    prevP = p; prevD = diff;
  }
  gl_FragColor = vec4(texture2D(uCol, hit).rgb, 1.0);
}
```

## 흔한 실수 · 확인 목록
- [ ] **앞 물체 가장자리가 찢어져 늘어난다** — 깊이 지도 경계가 흐리거나 그림보다 안쪽이면 물체 테두리 픽셀이 뒤 깊이로 읽힌다. 깊이의 밝은 쪽을 몇 px 넓혀(최댓값 필터) 경계를 물체 바깥으로 민다.
- [ ] **경계에 계단 같은 줄무늬가 보인다** — 층 수가 적으면 층마다 끊긴다. 이전 층과의 차이로 보간하면 층 32 ~ 40개로도 매끈하다.
- [ ] **그림 가장자리에 늘어난 띠 · 빈 곳이 보인다** — uv 를 0.84 ~ 0.9 로 줄여 여유를 두고, ClampToEdge 로 둔다.
- [ ] **움직임이 너무 작아 정지 그림처럼 보인다** — 옆 이동을 화면 폭의 3% 안으로 두면 거의 안 보인다. 견본은 8.5% + 위아래 2% 의 8자 길.
- [ ] **색이 탁하거나 너무 밝다** — ShaderMaterial 은 톤 매핑 · 색 공간 변환을 안 한다. 캔버스 텍스처를 색 공간 지정 없이 넣고 그대로 내보내면 원래 색이 나온다 (toneMapped: false).

## 완성 기준 체크리스트
- [ ] 카메라가 옆으로 흔들리면 앞 바위 · 사람은 많이, 나무 · 집은 덜, 산 · 하늘은 거의 안 움직인다
- [ ] 「깊이 지도 보기」를 켜면 흰 앞쪽 · 검은 하늘이 그림과 같은 모양으로 보인다
- [ ] 「깊이 가장자리 넓히기」를 끄면 사람 · 나무 가장자리가 찢어져 늘어나고, 켜면 대신 뒤 배경이 살짝 늘어난다
- [ ] 「앞으로 다가가기」를 켜면 가까운 것이 더 크게 다가온다
- [ ] 초점 깊이를 바꾸면 안 움직이는 층이 바뀐다 (사람 고정 → 뒤가 반대로 흐름)

## 이 기술 정보
- id: `i558` · 분류: 이펙트 · 연출 › 모션 그래픽 · 2D · 난이도 보통 · 폰 부담 가벼움 (폰 OK) — 화면 픽셀마다 텍스처 40번 읽기 — 폰에서도 전체 화면 60fps. 무거우면 층 수를 20으로.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i558
- 쓰면 좋을 때: 정지 그림 · 사진을 영상처럼 살릴 때 (이야기 장면 · 회상 · 썸네일 영상) / 3D 모델 없이 시작 화면에 입체감을 줄 때
- 쓰지 말 때: 카메라가 크게 움직여야 할 때 — 가려졌던 뒤가 없어서 늘어난 자국이 보인다. 그땐 층을 따로 그린 시차(i104)나 진짜 3D / 얇고 복잡한 경계(머리카락 · 나뭇잎)가 많은 그림 — 깊이 지도가 틀리면 바로 티 난다

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### parallax558 — `src/demos/demosMotionE.ts:567`
```ts
function parallax558(T: typeof THREE): ReturnType<Demo3D['make']> {
  const o = { s: 1, amt: 1, focus: 0.45, dolly: false, show: false, wide: true };
  const { col, dep, depWide } = paint558();
  const mkTex = (c: HTMLCanvasElement): THREE.CanvasTexture => {
    const t = new T.CanvasTexture(c);
    t.minFilter = T.LinearFilter;
    t.generateMipmaps = false;
    t.wrapS = t.wrapT = T.ClampToEdgeWrapping;
    return t;
  };
  const tCol = mkTex(col);
  const tDep = mkTex(dep);
  const tWide = mkTex(depWide);
  const scene = new T.Scene();
  const camera = new T.OrthographicCamera(-1, 1, 1, -1, 0, 1);
  const geo = new T.PlaneGeometry(2, 2);
  const mat = new T.ShaderMaterial({
    uniforms: {
      uCol: { value: tCol },
      uDep: { value: tWide },
      uOff: { value: new T.Vector2() },
      uFocus: { value: o.focus },
      uDolly: { value: 0 },
      uAsp: { value: 1 },
      uShow: { value: 0 },
      uCrop: { value: 0.84 },
    },
    vertexShader: 'varying vec2 vUv; void main(){ vUv = uv; gl_Position = vec4(position.xy, 0.0, 1.0); }',
    fragmentShader: FRAG558,
    depthTest: false,
    depthWrite: false,
    toneMapped: false,
  });
  const mesh = new T.Mesh(geo, mat);
  mesh.frustumCulled = false;
  scene.add(mesh);
  const u = mat.uniforms as Record<string, THREE.IUniform>;
  let clock = 0;
  return {
    scene,
    camera,
    tone: T.NoToneMapping,
    update(_t, dt) {
      clock += dt * o.s;
      // 카메라 길: 옆으로 크게 · 위아래로 조금 (8자) — 돌리는 앞으로 다가갔다 물러남
      const a = clock * 0.9;
      (u.uOff!.value as THREE.Vector2).set(Math.sin(a) * 0.085 * o.amt, Math.sin(a * 2) * 0.02 * o.amt);
      u.uDolly!.value = o.dolly ? (0.5 - 0.5 * Math.cos(a)) * 0.35 * o.amt : 0;
      u.uFocus!.value = o.focus;
      u.uShow!.value = o.show ? 1 : 0;
      u.uDep!.value = o.wide ? tWide : tDep;
    },
    resize(w, h) {
      u.uAsp!.value = w / h / (IW / IH);
    },
    controls: [
      speedCtl(o),
      { type: 'range', label: '시차 세기', min: 0, max: 2, step: 0.05, value: o.amt, on: (v) => (o.amt = v) },
      { type: 'range', label: '초점 깊이 (안 움직이는 층)', min: 0, max: 1, step: 0.05, value: o.focus, on: (v) => (o.focus = v) },
      { type: 'toggle', label: '앞으로 다가가기 (돌리)', value: o.dolly, on: (v) => (o.dolly = v) },
      { type: 'toggle', label: '깊이 지도 보기', value: o.show, on: (v) => (o.show = v) },
      { type: 'toggle', label: '깊이 가장자리 넓히기', value: o.wide, on: (v) => (o.wide = v) },
    ],
    dispose() {
      geo.dispose();
      mat.dispose();
      tCol.dispose();
      tDep.dispose();
      tWide.dispose();
    },
  };
}
```

### i558 견본 항목 — `src/demos/demosMotionE.ts:803`
```ts
  i558: { kind: '3d', caption: '그림 한 장 + 깊이 지도 — 셰이더가 깊이만큼 다르게 밀어 사진 속으로 카메라가 들어간 듯', make: parallax558 }
```

## 관련 기술
- 먼저 알면 좋은 기술: [2D 시차 층 · 무한 확대](https://ai-techstudio.web.app/ai/t/i104.md) `i104`
- 다음에 해 볼 기술: [영상으로 굽기 (헤드리스 브라우저 → mp4)](https://ai-techstudio.web.app/ai/t/i82.md) `i82`
- 참고 문서: [Depthy — 3D photo viewer (깊이 지도 시차)](https://depthy.stamina.pl/) · [Depth Anything (깊이 지도 추정)](https://github.com/LiheYoung/Depth-Anything) · [LearnOpenGL — Parallax Mapping](https://learnopengl.com/Advanced-Lighting/Parallax-Mapping)
