# AI 꾸러미 — 피사계 심도 (Bokeh) — Depth of field (Bokeh)
> 카메라에서 초점 거리만큼 떨어진 곳은 또렷하게, 앞뒤는 거리만큼 흐리게 해, 사진처럼 주인공만 도드라지게 한다.  
> 견본: https://ai-techstudio.web.app/#t/i01

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

## 주문서

### 만들어 줘: 피사계 심도 (Bokeh) — Depth of field (Bokeh)

#### 1. 목표
날아오는 공과 뒤 관중에 피사계 심도(Bokeh)를 넣어 줘 — 초점은 주인공에 맞추고 앞뒤는 흐리게. 분위기는 영화 같은 확대.

#### 2. 핵심 기술 용어
- **Depth of field (Bokeh)** — 피사계 심도 — 초점 밖 흐림
- **BokehPass (focus · aperture · maxblur)** — three.js 심도 후처리 — 초점 거리 · 조리개 · 최대 흐림
- **Circle of confusion** — 초점에서 멀수록 커지는 흐림 원

#### 3. 환경
- 플랫폼: three.js r186 (ES 모듈 · TypeScript, `import * as THREE from "three"`), WebGL2, 외부 라이브러리 추가 없이
- 화면: 3D · 브라우저 — PC 와 폰(가로 844×390 · 세로 390×844) 모두, 60fps 목표

#### 4. 조건
- 초점 거리는 매 프레임 카메라 ~ 주인공 거리로 다시 넣기 (camera.position.distanceTo)
- bokeh.uniforms.aspect 를 화면 비율로 맞추기
- 조리개는 작게(0.006 안팎) — 크면 주인공까지 번진다
- 왼쪽 모두 또렷 · 오른쪽 초점만 또렷으로 나눠 비교, 「조리개」 0~0.02 슬라이더

#### 5. 완성 기준 (이게 보이면 성공)
- 오른쪽에선 가운데 공만 또렷하고, 뒤 관중 줄과 앞 고깔이 거리만큼 흐리다
- 카메라가 움직여도 공은 계속 또렷하다 (초점 따라감)
- 조리개 0 이면 왼쪽과 같아진다

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

## 원리
- 장면의 깊이 그림을 함께 그려, 각 픽셀이 카메라에서 얼마나 먼지 안다.
- 초점 거리(focus)와의 차이 × 조리개(aperture 0.006) 만큼 흐림 원의 크기를 정한다. 최대 흐림은 0.014.
- 견본은 매 프레임 초점을 카메라 ~ 공 거리로 맞춰, 공은 늘 또렷하고 뒤 관중 · 앞 고깔은 흐리다.
- 화면 비율(aspect)도 매 프레임 넘겨야 흐림 원이 찌그러지지 않는다.

## 핵심 코드 — 주인공을 따라가는 초점
(발췌: demos/demosLight.ts i01 make() 를 정리)
```ts
import { BokehPass } from 'three/examples/jsm/postprocessing/BokehPass.js';

const comp = new EffectComposer(renderer);
comp.addPass(new RenderPass(scene, cam));
const bokeh = new BokehPass(scene, cam, { focus: 4.2, aperture: 0.006, maxblur: 0.014 });
comp.addPass(bokeh);
comp.addPass(new OutputPass());

function frame(dt: number, w: number, h: number) {
  const u = bokeh.uniforms as Record<string, { value: number }>;
  u.focus.value = cam.position.distanceTo(ball.position); // 초점 = 공까지 거리
  u.aperture.value = 0.006;                                // 작게 — 크면 공까지 번진다
  u.aspect.value = w / h;                                  // 흐림 원이 찌그러지지 않게
  comp.render(dt);
}
```

## 흔한 실수 · 확인 목록
- [ ] **심도로 화면 가장자리를 흐리면 답답하고 지저분해 보인다** — 사용자 결정으로 이 사이트는 심도 · 비네트 · 가장자리 흐림을 쓰지 않는다. 주인공은 빛 배치로 띄운다.
- [ ] **초점을 한 번만 정하면 카메라가 움직일 때 주인공이 흐려진다** — 매 프레임 카메라 ~ 주인공 거리를 다시 넣는다.
- [ ] **반투명 · 스프라이트는 깊이 그림에 없어 흐림이 엉뚱하다** — 빛 알갱이 · 글씨 스프라이트는 흐림 뒤에 따로 그리거나 깊이를 쓰지 않게 둔다.

## 완성 기준 체크리스트
- [ ] 오른쪽에선 가운데 공만 또렷하고, 뒤 관중 줄과 앞 고깔이 거리만큼 흐리다
- [ ] 카메라가 움직여도 공은 계속 또렷하다 (초점 따라감)
- [ ] 조리개 0 이면 왼쪽과 같아진다

## 이 기술 정보
- id: `i01` · 분류: 그래픽 · 셰이더 › 후처리 · 3D · 난이도 보통 · 폰 부담 무거움 — 장면 깊이를 한 번 더 그리고, 픽셀마다 여러 번 읽어 흐린다. 화면 전체가 대상이라 폰에서 무겁다.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i01
- 쓰면 좋을 때: 연출 카메라가 한 물체를 크게 잡을 때 (짧은 순간) / 사진 · 접사 느낌이 필요한 장면
- 쓰지 말 때: 평소 놀이 화면 — 이 사이트는 사용자 결정으로 심도 · 가장자리 흐림을 쓰지 않는다. 대신 빛 배치 · 어두운 배경으로 주인공을 띄운다 / 폰 — 깊이 그리기 + 큰 흐림이 무겁다

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### i01 견본 항목 — `src/demos/demosLight.ts:1548`
```ts
  i01: {
    kind: '3d',
    caption: '왼쪽 모두 또렷 · 오른쪽 Bokeh — 가운데 공에 초점, 앞뒤 관중은 흐릿해져 주인공이 도드라져요',
    make() {
      const scene = new THREE.Scene();
      scene.background = gradTex('#1b2a6a', '#0a1030');
      basicLights(scene, 0.9, 1.8, V3(2, 5, 4));
      const field = new THREE.Mesh(new THREE.PlaneGeometry(40, 40), std(0x3f9f4f, 0.9));
      field.rotation.x = -Math.PI / 2;
      scene.add(field);
      // 주인공 공
      const ball = new THREE.Group();
      ball.add(mesh(new THREE.SphereGeometry(0.42, 40, 28), std(0xfdfbf5, 0.45)));
      const stitchM = std(0xe23a3a, 0.5);
      for (const s of [-1, 1]) {
        const st = new THREE.Mesh(new THREE.TorusGeometry(0.3, 0.025, 8, 48), stitchM);
        st.position.x = s * 0.24;
        st.scale.setScalar(1.12);
        st.rotation.y = Math.PI / 2;
        ball.add(st);
      }
      ball.position.set(0, 0.7, 0);
      scene.add(ball);
      // 관중 (뒤) · 앞 소품
      const capG = new THREE.CapsuleGeometry(0.16, 0.3, 4, 12);
      const headG = new THREE.SphereGeometry(0.13, 16, 12);
      const skin = std(0xffd9b3, 0.6);
      const cols = [0xff6b6b, 0xffd23f, 0x4aa3ff, 0x3ec46d, 0xb06cff, 0xff9f43, 0xffffff];
      const crowd = new THREE.Group();
      let n = 0;
      for (let row = 0; row < 4; row++)
        for (let i = -7; i <= 7; i++) {
          const x = i * 0.55 + (row % 2) * 0.27;
          const z = -3.5 - row * 1.2;
          const y = 0.3 + row * 0.45;
          const body = mesh(capG, std(cols[n++ % cols.length]!, 0.6), x, y, z);
          const head = mesh(headG, skin, x, y + 0.36, z);
          crowd.add(body, head);
        }
      const stands = mesh(new THREE.BoxGeometry(10, 2.2, 5), std(0x2d3566, 0.9), 0, -0.95, -5.3);
      scene.add(crowd, stands);
      const front = new THREE.Group();
      [
        [-1.5, 2.2, 0xffd23f],
        [1.3, 2.5, 0x4aa3ff],
        [0.4, 2.9, 0xff6b6b],
      ].forEach(([x, z, c]) => front.add(mesh(new THREE.ConeGeometry(0.2, 0.5, 20), std(c as number, 0.5), x as number, 0.25, z as number)));
      scene.add(front);
      const cam = new THREE.PerspectiveCamera(40, 1, 0.1, 60);
      cam.position.set(0, 1.25, 4.2);
      cam.lookAt(0, 0.75, 0);
      let aperture = 0.006;
      const kit = postKit((r) => {
        const bokeh = new BokehPass(scene, cam, { focus: 4.2, aperture, maxblur: 0.014 });
        return { comp: splitChain(r, scene, cam, [bokeh]), bokeh };
      });
      const hud = new Hud();
      hud.vline(0.5);
      hud.label('모두 또렷', 0.25, 0.95, 0.5, 1);
      hud.label('초점만 또렷', 0.75, 0.95, 0.5, 1, 'on');
      let dt = 0.016;
      return {
        scene,
        camera: cam,
        update(t, d) {
          dt = d;
          ball.rotation.set(t * 1.3, t * 0.9, 0);
          ball.position.y = 0.7 + Math.sin(t * 2) * 0.08;
          crowd.children.forEach((c, i) => {
            if (i % 2 === 0) c.position.y += Math.sin(t * 6 + i) * 0.003;
          });
          cam.position.x = Math.sin(t * 0.3) * 0.35;
          cam.lookAt(0, 0.75, 0);
        },
        render(r, w, h) {
          cam.aspect = w / h;
          cam.fov = w / h < 1 ? 55 : 40;
          cam.updateProjectionMatrix();
          const k = kit.get(r, w, h);
          const u = k.bokeh.uniforms as Record<string, { value: number }>;
          u['focus']!.value = cam.position.distanceTo(ball.position);
          u['aperture']!.value = aperture;
          u['aspect']!.value = w / h;
          k.comp.render(dt);
          hud.draw(r, w, h);
        },
        controls: [{ type: 'range', label: '조리개 (흐림 정도)', min: 0, max: 0.02, step: 0.0005, value: 0.006, on: (v) => (aperture = v) }],
        dispose() {
          kit.dispose();
          hud.dispose();
          disposeTree(scene);
        },
      };
    },
  }
```

## 관련 기술
- 먼저 알면 좋은 기술: [후처리 사슬 (EffectComposer → OutputPass)](https://ai-techstudio.web.app/ai/t/u21.md) `u21`
- 다음에 해 볼 기술: [구석 그늘 (SSAO / GTAO)](https://ai-techstudio.web.app/ai/t/i02.md) `i02`
- 참고 문서: [three.js 예제 — webgl_postprocessing_dof](https://threejs.org/examples/#webgl_postprocessing_dof)
