# AI 꾸러미 — 매트캡 (MatCap) — MatCap (material capture)
> 빛 계산 없이 둥근 그림 한 장에서 색을 읽어, 도자기 · 젤리 · 금처럼 보이는 아주 가벼운 재질을 만든다.  
> 견본: https://ai-techstudio.web.app/#t/i09

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

## 주문서

### 만들어 줘: 매트캡 (MatCap) — MatCap (material capture)

#### 1. 목표
도자기 · 젤리 · 금 장난감을 매트캡 재질로 그려 줘 — 조명 없이 동그란 그림 한 장으로. 느낌은 반들반들 장난감.

#### 2. 핵심 기술 용어
- **MatCap (material capture)** — 재질 사진 한 장 — 둥근 공에 빛이 비친 그림
- **MeshMatcapMaterial** — three.js 매트캡 재질
- **View-space normal lookup** — 화면 쪽 법선 방향으로 그림 좌표를 고르기

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

#### 4. 조건
- MeshMatcapMaterial({ matcap }) 로 — 조명 · 환경은 이 재질에 영향 없음
- 매트캡 그림은 동그란 원 안에 그리고, 위 왼쪽에 반짝임 점
- 재질마다 그림만 바꿔 도자기 · 젤리 · 금 셋을 보여 주고, 그림 자체도 물체 위에 작게 보여 주기
- 「매트캡 켜기」 단추로 조명 계산 재질과 바꿔 비교

#### 5. 완성 기준 (이게 보이면 성공)
- 같은 장면에 도자기 · 젤리 · 금 세 물체가 보이고, 위에 그 재질의 동그란 그림이 함께 보인다
- 조명을 바꿔도 매트캡 물체는 그대로다
- 단추로 끄면 조명 계산 재질로 바뀌어 차이를 비교할 수 있다
- 폰에서도 60fps

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

## 원리
- 매트캡 그림은 「빛을 받은 공」을 정면에서 찍은 동그란 그림이다.
- 물체의 각 점이 화면 쪽으로 어느 방향을 보는지(화면 공간 법선 xy)로 그 그림의 같은 자리 색을 읽는다.
- 그래서 조명 · 그림자 계산이 전혀 없고, 그림만 바꾸면 도자기 · 젤리 · 금이 된다.
- 빛이 카메라에 붙어 다니는 셈이라, 물체를 돌려도 하이라이트 자리는 늘 같다.

## 핵심 코드 — 캔버스로 매트캡 그림 그리기 + 재질
(발췌: demos/demosMaterial.ts i09 make() 의 matcap() 를 정리)
```ts
/** kind 0 도자기 · 1 젤리 · 2 금 */
function matcap(kind: number): THREE.CanvasTexture {
  const [c, g] = cv(256, 256);
  const C = 128;
  if (kind === 0) {
    const rg = g.createRadialGradient(100, 90, 10, C, C, 128);
    rg.addColorStop(0, '#ffffff'); rg.addColorStop(0.45, '#eef2f8');
    rg.addColorStop(0.8, '#9aa8c4'); rg.addColorStop(1, '#3a4664');
    g.fillStyle = rg;
  } else if (kind === 1) {
    const rg = g.createRadialGradient(C, 140, 10, C, C, 128);
    rg.addColorStop(0, '#2fbf5a'); rg.addColorStop(0.6, '#45d870');
    rg.addColorStop(0.88, '#a8ffb8'); rg.addColorStop(1, '#e8fff0'); // 가장자리가 밝으면 젤리
    g.fillStyle = rg;
  } else {
    const lg = g.createLinearGradient(0, 0, 0, 256);              // 금: 밝음-어둠 띠가 번갈아
    [['#fff6c8', 0], ['#e8b84a', 0.3], ['#7a4a12', 0.5], ['#f2c860', 0.62], ['#a06a1c', 0.85], ['#3a2006', 1]]
      .forEach(([col, k]) => lg.addColorStop(k as number, col as string));
    g.fillStyle = lg;
  }
  g.beginPath(); g.arc(C, C, 128, 0, Math.PI * 2); g.fill();
  const hl = g.createRadialGradient(92, 78, 0, 92, 78, kind === 2 ? 34 : 26); // 반짝임
  hl.addColorStop(0, 'rgba(255,255,255,1)'); hl.addColorStop(1, 'rgba(255,255,255,0)');
  g.fillStyle = hl;
  g.beginPath(); g.arc(92, 78, 40, 0, Math.PI * 2); g.fill();
  const t = new THREE.CanvasTexture(c);
  t.colorSpace = THREE.SRGBColorSpace;
  return t;
}

const jelly = new THREE.Mesh(new THREE.SphereGeometry(0.62, 48, 32), new THREE.MeshMatcapMaterial({ matcap: matcap(1) }));
scene.add(jelly); // 빛이 없어도 보인다
```

## 흔한 실수 · 확인 목록
- [ ] **물체를 돌려도 반짝임이 따라오지 않아 이상해 보일 수 있다** — 매트캡의 빛은 카메라에 붙어 있다. 카메라가 많이 도는 장면은 표준 재질이 낫다.
- [ ] **그림을 네모 끝까지 채우면 가장자리에 줄이 생긴다** — 원 안에만 그리고, 원 밖은 원 가장자리 색과 비슷하게.
- [ ] **평평한 넓은 면은 한 색으로 뭉개진다** — 법선이 같은 곳은 같은 색이다. 곡면이 많은 물체에 쓴다.

## 완성 기준 체크리스트
- [ ] 같은 장면에 도자기 · 젤리 · 금 세 물체가 보이고, 위에 그 재질의 동그란 그림이 함께 보인다
- [ ] 조명을 바꿔도 매트캡 물체는 그대로다
- [ ] 단추로 끄면 조명 계산 재질로 바뀌어 차이를 비교할 수 있다
- [ ] 폰에서도 60fps

## 이 기술 정보
- id: `i09` · 분류: 그래픽 · 셰이더 › 재질 · 그림체 · 3D · 난이도 쉬움 · 폰 부담 가벼움 (폰 OK) — 텍스처 한 번 읽기뿐이라 가장 가벼운 재질 중 하나. 조명 수와 상관없다.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i09
- 쓰면 좋을 때: 저사양 폰에서 빛 계산을 줄이고 싶을 때 / 젤리 · 도자기처럼 특정 재질감을 그림 한 장으로 / 모델 보기 · 조각 도구처럼 형태가 잘 보여야 할 때
- 쓰지 말 때: 장면 조명 · 그림자가 바뀌어야 할 때 (밤낮 · 횃불) — 대신 표준 재질 / 환경이 비쳐야 할 때 — 대신 환경 반사(u12)

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### i09 견본 항목 — `src/demos/demosMaterial.ts:973`
```ts
  i09: {
    kind: '3d',
    caption: '조명 없이 동그란 그림(위) 한 장씩만 — 도자기 · 젤리 · 금이 돼요',
    make() {
      const B = base({ bg: gradBg('#d8ecff', '#f6fbff'), labels: ['도자기', '젤리', '금'], fit: [3.1, 1.1], target: [0, 0.2, 0], dir: [0, 0.12, 1] });
      const matcap = (kind: number): THREE.CanvasTexture => {
        const [c, g] = cv(256, 256);
        const C = 128;
        if (kind === 0) {
          const rg = g.createRadialGradient(100, 90, 10, C, C, 128);
          rg.addColorStop(0, '#ffffff');
          rg.addColorStop(0.45, '#eef2f8');
          rg.addColorStop(0.8, '#9aa8c4');
          rg.addColorStop(1, '#3a4664');
          g.fillStyle = rg;
        } else if (kind === 1) {
          const rg = g.createRadialGradient(C, 140, 10, C, C, 128);
          rg.addColorStop(0, '#2fbf5a');
          rg.addColorStop(0.6, '#45d870');
          rg.addColorStop(0.88, '#a8ffb8');
          rg.addColorStop(1, '#e8fff0');
          g.fillStyle = rg;
        } else {
          const lg = g.createLinearGradient(0, 0, 0, 256);
          lg.addColorStop(0, '#fff6c8');
          lg.addColorStop(0.3, '#e8b84a');
          lg.addColorStop(0.5, '#7a4a12');
          lg.addColorStop(0.62, '#f2c860');
          lg.addColorStop(0.85, '#a06a1c');
          lg.addColorStop(1, '#3a2006');
          g.fillStyle = lg;
        }
        g.beginPath();
        g.arc(C, C, 128, 0, Math.PI * 2);
        g.fill();
        // 반짝임
        const hl = g.createRadialGradient(92, 78, 0, 92, 78, kind === 2 ? 34 : 26);
        hl.addColorStop(0, 'rgba(255,255,255,1)');
        hl.addColorStop(1, 'rgba(255,255,255,0)');
        g.fillStyle = hl;
        g.beginPath();
        g.arc(92, 78, 40, 0, Math.PI * 2);
        g.fill();
        if (kind === 1) {
          g.strokeStyle = 'rgba(255,255,255,0.6)';
          g.lineWidth = 6;
          g.beginPath();
          g.arc(C, C, 108, 0.6, 1.4);
          g.stroke();
        }
        return tex(c);
      };
      const caps = [matcap(0), matcap(1), matcap(2)];
      const mm = caps.map((m) => new THREE.MeshMatcapMaterial({ matcap: m }));
      const sm = [
        new THREE.MeshStandardMaterial({ color: 0xeef2f8, roughness: 0.3 }),
        new THREE.MeshStandardMaterial({ color: 0x3fd068, roughness: 0.2 }),
        new THREE.MeshStandardMaterial({ color: 0xe0b040, roughness: 0.3, metalness: 1 }),
      ];
      const geos = [new THREE.TorusKnotGeometry(0.42, 0.15, 120, 18), new THREE.SphereGeometry(0.62, 48, 32), new THREE.CapsuleGeometry(0.35, 0.5, 8, 24)];
      const objs: THREE.Mesh[] = [];
      const xs = [-2.15, 0, 2.15];
      xs.forEach((x, i) => {
        const o = new THREE.Mesh(geos[i]!, mm[i]!);
        o.position.set(x, -0.25, 0);
        if (i === 1) o.scale.set(1, 0.8, 1);
        B.scene.add(o);
        objs.push(o);
        const disc = new THREE.Mesh(new THREE.CircleGeometry(0.28, 40), new THREE.MeshBasicMaterial({ map: caps[i]!, toneMapped: false }));
        disc.position.set(x, 0.95, 0);
        const ring = new THREE.Mesh(new THREE.RingGeometry(0.28, 0.32, 40), new THREE.MeshBasicMaterial({ color: 0x2a3a6a, toneMapped: false }));
        ring.position.copy(disc.position);
        B.scene.add(disc, ring);
      });
      // 조명은 「비교」 때만 의미 있음 — 매트캡은 무시한다
      lights(B.scene, 0xffffff, 0x8090a0, 0.9, 1.8);
      B.scene.environment = studioEnv();
      return B.done({
        update(t) {
          objs[0]!.rotation.set(t * 0.4, t * 0.7, 0);
          const sq = 1 + Math.sin(t * 4) * 0.08;
          objs[1]!.scale.set(sq, 0.8 / sq, sq);
          objs[1]!.rotation.y = t * 0.5;
          objs[2]!.rotation.set(Math.sin(t) * 0.5, 0, t * 0.6);
        },
        controls: [
          {
            type: 'toggle',
            label: '매트캡 켜기 (끄면 조명 계산 재질)',
            value: true,
            on: (v) => {
              objs.forEach((o, i) => (o.material = v ? mm[i]! : sm[i]!));
              B.setLabels(v ? ['도자기', '젤리', '금'] : ['조명 계산 1', '조명 계산 2', '조명 계산 3']);
            },
          },
        ],
        dispose() {
          for (const m of [...mm, ...sm]) m.dispose();
        },
      });
    },
  }
```

## 관련 기술
- 먼저 알면 좋은 기술: [캔버스로 그린 무늬 (CanvasTexture)](https://ai-techstudio.web.app/ai/t/u09.md) `u09`
- 다음에 해 볼 기술: [테두리 빛 (rim light, 프레넬)](https://ai-techstudio.web.app/ai/t/i10.md) `i10` · [툰 셰이딩 (3단 명암)](https://ai-techstudio.web.app/ai/t/u01.md) `u01`
- 참고 문서: [three.js 예제 — webgl_materials_matcap](https://threejs.org/examples/#webgl_materials_matcap) · [three.js 문서 — MeshMatcapMaterial](https://threejs.org/docs/#api/en/materials/MeshMatcapMaterial)
