# AI 꾸러미 — 도트화 (Pixelated pass)
> 3D 를 도트 그림처럼  
> 견본: https://ai-techstudio.web.app/#t/i08

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

## 주문서

### 만들어 줘: 도트화 (Pixelated pass)

#### 1. 목표
꼬마 창고지기 레트로 모드에 「도트화 (Pixelated pass)」 기술을 깔끔한 기본 느낌으로 넣어 줘. — 3D 를 도트 그림처럼

#### 2. 핵심 기술 용어
- **Pixelated pass**

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

#### 4. 조건
- 한 프레임 16ms 안 (폰 포함) — 무거운 계산은 처음에 한 번 또는 여러 프레임에 나눠서
- 새 재질 · 셰이더는 첫 화면 전에 미리 컴파일 (renderer.compileAsync)
- 화면 가장자리 흐림 · 비네트로 얼버무리지 않기
- 외부 라이브러리 추가 없이 three.js 와 three/examples 만

#### 5. 완성 기준 (이게 보이면 성공)
- 견본처럼 「3D 를 도트 그림처럼」 이(가) 첫 화면에서 바로 보인다
- 켬/끔(또는 전/후) 비교 단추로 이 기술이 만드는 차이를 확인할 수 있다
- 중요한 값 1~3개를 슬라이더로 바꿔 볼 수 있다
- 콘솔 오류 0, 폰 폭(390px)에서도 동작한다

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

## 원리
- 3D 를 도트 그림처럼


## 흔한 실수 · 확인 목록
- (아직 정리된 것이 없어요)

## 완성 기준 체크리스트
- [ ] 견본처럼 「3D 를 도트 그림처럼」 이(가) 첫 화면에서 바로 보인다
- [ ] 켬/끔(또는 전/후) 비교 단추로 이 기술이 만드는 차이를 확인할 수 있다
- [ ] 중요한 값 1~3개를 슬라이더로 바꿔 볼 수 있다
- [ ] 콘솔 오류 0, 폰 폭(390px)에서도 동작한다

## 이 기술 정보
- id: `i08` · 분류: 그래픽 · 셰이더 › 후처리 · 3D · 난이도 보통 · 폰 부담 보통 (폰 주의)
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i08
- 쓰면 좋을 때: 쓰면 좋을 곳 — 꼬마 창고지기 레트로 모드

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### i08 견본 항목 — `src/demos/demosLight.ts:2240`
```ts
  i08: {
    kind: '3d',
    caption: '왼쪽 그냥 3D · 오른쪽 도트화 — 낮은 해상도로 그리고 가장자리에 선을 넣어 도트 그림처럼 (꼬마 창고지기 레트로 모드)',
    make() {
      const scene = new THREE.Scene();
      scene.background = new THREE.Color(0x7ec8ff);
      basicLights(scene, 0.9, 1.8, V3(-3, 6, 4));
      const root = new THREE.Group();
      const tileA = std(0xe8d3a8, 0.9);
      const tileB = std(0xd8bf8e, 0.9);
      const tileG = new THREE.BoxGeometry(0.98, 0.2, 0.98);
      for (let x = -2; x <= 2; x++)
        for (let z = -2; z <= 2; z++) root.add(mesh(tileG, (x + z) & 1 ? tileA : tileB, x, -0.1, z));
      const wallM = std(0x8a8fa3, 0.85);
      const wallG = new THREE.BoxGeometry(1, 1, 1);
      for (let i = -3; i <= 3; i++) {
        root.add(mesh(wallG, wallM, i, 0.5, -3));
        if (i > -3 && i < 3) {
          root.add(mesh(wallG, wallM, -3, 0.5, i));
          root.add(mesh(wallG, wallM, 3, 0.5, i));
        }
      }
      const crateM = std(0xc07a3a, 0.8);
      const crateE = std(0x8a5530, 0.8);
      const crate = (x: number, z: number): THREE.Group => {
        const g = new THREE.Group();
        g.add(mesh(new THREE.BoxGeometry(0.8, 0.8, 0.8), crateM, 0, 0.4, 0));
        g.add(mesh(new THREE.BoxGeometry(0.84, 0.12, 0.84), crateE, 0, 0.74, 0));
        g.add(mesh(new THREE.BoxGeometry(0.84, 0.12, 0.84), crateE, 0, 0.06, 0));
        g.position.set(x, 0, z);
        return g;
      };
      const crates = [crate(-1, -1), crate(1, 0), crate(0, 1)];
      root.add(...crates);
      const goalM = std(0xff7aa8, 0.6);
      for (const [x, z] of [
        [-2, -2],
        [2, -1],
        [-1, 2],
      ] as const) {
        const gm = mesh(new THREE.CylinderGeometry(0.3, 0.3, 0.04, 20), goalM, x, 0.02, z);
        root.add(gm);
      }
      const kid = new THREE.Group();
      kid.add(mesh(new THREE.BoxGeometry(0.4, 0.45, 0.3), std(0x3a6cf0, 0.7), 0, 0.42, 0));
      kid.add(mesh(new THREE.BoxGeometry(0.36, 0.34, 0.34), std(0xffd9b3, 0.7), 0, 0.82, 0));
      kid.add(mesh(new THREE.BoxGeometry(0.4, 0.12, 0.4), std(0xe8453c, 0.6), 0, 1.03, 0.02));
      kid.add(mesh(new THREE.BoxGeometry(0.4, 0.04, 0.2), std(0xe8453c, 0.6), 0, 0.98, 0.25));
      const legs: THREE.Mesh[] = [];
      for (const s of [-1, 1]) {
        const l = mesh(new THREE.BoxGeometry(0.15, 0.22, 0.15), std(0x2a2a3a), s * 0.1, 0.11, 0);
        legs.push(l);
        kid.add(l);
      }
      root.add(kid);
      scene.add(root);
      const cam = new THREE.PerspectiveCamera(34, 1, 0.1, 50);
      let mul = 1;
      let px = 0;
      const kit = postKit((r) => {
        const pix = new RenderPixelatedPass(4, scene, cam, { normalEdgeStrength: 0.35, depthEdgeStrength: 0.5 });
        return { comp: splitChain(r, scene, cam, [pix]), pix };
      });
      const hud = new Hud();
      hud.vline(0.5);
      hud.label('그냥 3D', 0.25, 0.95, 0.5, 1);
      hud.label('도트화', 0.75, 0.95, 0.5, 1, 'on');
      let dt = 0.016;
      let now = 0;
      return {
        scene,
        camera: cam,
        update(t, d) {
          dt = d;
          now = t;
          const p = (t * 0.35) % 2;
          const e = p < 1 ? p : 2 - p;
          kid.position.set(-2 + e * 4, 0, -2 + 0 * e);
          kid.rotation.y = p < 1 ? Math.PI / 2 : -Math.PI / 2;
          legs.forEach((l, i) => (l.position.z = Math.sin(t * 10 + i * Math.PI) * 0.08));
          kid.position.z = -2 + Math.sin(t * 0.2) * 0.02;
          crates[1]!.position.y = 0;
        },
        render(r, w, h) {
          root.rotation.y = Math.sin(now * 0.25) * 0.45;
          fit(cam, w / h, V3(0, 0.3, 0), 4.3, V3(0, 1.25, 1));
          const k = kit.get(r, w, h);
          const want = Math.max(2, Math.round((h / 75) * mul));
          if (want !== px) {
            px = want;
            k.pix.setPixelSize(px);
          }
          k.comp.render(dt);
          hud.draw(r, w, h);
        },
        controls: [
          {
            type: 'range',
            label: '도트 크기',
            min: 0.4,
            max: 3,
            step: 0.1,
            value: 1,
            on: (v) => {
              mul = v;
            },
          },
        ],
        dispose() {
          kit.dispose();
          hud.dispose();
          disposeTree(scene);
        },
      };
    },
  }
```

## 관련 기술
- 이 효과를 만든 기술: [저해상 3D 도트 + 1px 외곽선](https://ai-techstudio.web.app/ai/t/i239.md) `i239`
