# AI 꾸러미 — 표정 모프 (눈 감기 · 웃음 · 놀람) — Morph targets (blend shapes · shape keys)
> 같은 얼굴의 변형 모양 6가지(깜빡 · 웃음 · 놀람 · 화남 · 아 · 오)를 모프 타깃으로 만들어 비율로 섞어, 표정과 말하는 입을 움직인다.  
> 견본: https://ai-techstudio.web.app/#t/i463

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

## 주문서

### 만들어 줘: 표정 모프 (눈 감기 · 웃음 · 놀람) — Morph targets (blend shapes · shape keys)

#### 1. 목표
점토 토끼 얼굴에 표정 모프를 넣어 줘 — 깜빡 · 웃음 · 놀람 · 화남 · 아 · 오 변형을 비율로 섞고, 말할 때는 음절마다 입이 열리게. 분위기는 귀엽고 아기자기.

#### 2. 핵심 기술 용어
- **Morph targets (blend shapes · shape keys)** — 같은 정점 수의 변형 모양을 비율로 섞기
- **geometry.morphAttributes.position** — three.js 모프 모양 목록
- **mesh.morphTargetInfluences** — 모프마다 섞는 비율 (0 ~ 1)
- **Procedural lip flap (viseme A · O)** — 음절마다 아 · 오 입 모양

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

#### 4. 조건
- 모든 모프 모양은 기본 모양과 정점 수 · 순서가 같게 (같은 함수에서 뽑기)
- 표정 값은 바로 바꾸지 말고 부드럽게 따라가기
- 깜빡임은 웃음과 겹치면 줄인다 (blink × (1 − smile))
- 표정마다 몸짓(머리 기울기 · 팔 · 귀)도 함께 — 얼굴만 바뀌면 어색하다
- 모프 비율을 막대로 보여 주고, 웃음 · 놀람 · 화남 슬라이더로 직접 섞을 수 있게

#### 5. 완성 기준 (이게 보이면 성공)
- 기본 → 웃음 → 놀람 → 화남 → 말하기 → 웃으며 말하기가 2.2초마다 부드럽게 바뀐다
- 말할 때 음절마다 입이 아 · 오로 열렸다 닫힌다
- 슬라이더로 웃음 0.5 + 놀람 0.5 처럼 섞으면 중간 표정이 나온다
- 화면 막대 6개가 지금 섞는 비율을 보여 준다

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

## 원리
- 모든 모양(기본 + 변형 6)은 정점 수와 순서가 같아야 한다 — 같은 정점이 다른 자리로 갈 뿐이다.
- 견본은 눈 · 눈썹 · 입 · 혀를 위 · 아래 테 한 쌍의 「띠」로 만들고, 표정마다 테 모양 함수만 바꿔 같은 수의 점을 뽑는다.
- 점은 머리 표면에 투영해 붙이고(이분 탐색으로 표면 찾기), 머리 뼈에 매단다.
- 변형 모양을 geometry.morphAttributes.position 에 넣고, mesh.morphTargetInfluences[i] 로 섞는 비율을 준다.
- 표정 값은 목표로 부드럽게(1 − e^(−9dt)), 말할 땐 0.17초 음절마다 아 · 오를 번갈아 연다. 깜빡임은 웃을 때 줄인다.

## 핵심 코드 — 모프 모양 넣기 + 표정 섞기 + 말하는 입
(발췌: demos/demosRigB.ts stripGeo() · i463 update() 를 정리)
```ts
const EXPR = ['blink', 'smile', 'surprise', 'angry', 'talkA', 'talkO'];

// 기본 모양 pos 와, 같은 정점 수의 변형 모양 6개 morph[k]
const geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.BufferAttribute(pos, 3));
geo.setAttribute('normal', new THREE.BufferAttribute(nrm, 3));
geo.setIndex(idx);
geo.morphAttributes.position = morph.map((m, k) => {
  const a = new THREE.BufferAttribute(m, 3);
  a.name = EXPR[k]!;
  return a;
});
const face = new THREE.Mesh(geo, mat); // morphTargetInfluences 가 6칸 생긴다
headBone.add(face);

// 매 프레임: 목표 표정으로 부드럽게
const w = [0, 0, 0, 0, 0, 0];
const k = 1 - Math.exp(-dt * 9);
for (let i = 1; i < 4; i++) w[i] += (target[i] - w[i]) * k;  // 웃음 · 놀람 · 화남
// 말하기: 0.17초 음절마다 아 또는 오
const syl = Math.floor(t / 0.17);
const open = talk * Math.max(0, Math.sin(((t % 0.17) / 0.17) * Math.PI));
const isO = (syl * 7919) % 3 === 0;
const kt = 1 - Math.exp(-dt * 25);
w[4] += ((isO ? 0 : open * 0.9) - w[4]) * kt;
w[5] += ((isO ? open : 0) - w[5]) * kt;
// 깜빡: 3.3초마다 0.16초, 웃을 땐 줄임
const p = t % 3.3;
w[0] = (p < 0.16 ? Math.sin((p / 0.16) * Math.PI) : 0) * (1 - w[1]);
for (let i = 0; i < 6; i++) face.morphTargetInfluences![i] = w[i];
```

## 흔한 실수 · 확인 목록
- [ ] **변형 모양의 정점 수가 하나라도 다르면 얼굴이 깨진다** — 기본과 변형을 같은 함수 · 같은 점 수(M)로 뽑는다. 견본은 띠의 위 · 아래 테 함수만 바꾼다.
- [ ] **모프에 절대 위치를 넣었는데 상대로 읽히면(또는 반대) 얼굴이 두 배로 밀린다** — three.js 기본은 절대 위치(morphTargetsRelative = false). 차이값을 넣었다면 geo.morphTargetsRelative = true.
- [ ] **깜빡임과 웃음(눈 감은 웃음)을 둘 다 1로 섞으면 눈이 파고든다** — 깜빡임에 (1 − 웃음)을 곱한다.

## 완성 기준 체크리스트
- [ ] 기본 → 웃음 → 놀람 → 화남 → 말하기 → 웃으며 말하기가 2.2초마다 부드럽게 바뀐다
- [ ] 말할 때 음절마다 입이 아 · 오로 열렸다 닫힌다
- [ ] 슬라이더로 웃음 0.5 + 놀람 0.5 처럼 섞으면 중간 표정이 나온다
- [ ] 화면 막대 6개가 지금 섞는 비율을 보여 준다

## 이 기술 정보
- id: `i463` · 분류: 3D 모델 · 캐릭터 › 캐릭터 · 리깅 · 3D · 난이도 보통 · 폰 부담 가벼움 (폰 OK) — 모프 6개 × 얼굴 띠 정점 수백 — GPU 에서 섞는다. 모프 수가 아주 많으면(수십) 정점 셰이더가 무거워진다.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i463
- 쓰면 좋을 때: 얼굴 표정 · 입 모양처럼 뼈로 만들기 힘든 작은 변형 / 대사에 맞춰 입을 움직일 때
- 쓰지 말 때: 팔 · 다리처럼 크게 움직이는 곳 — 뼈(i456)가 맞다 / 텍스처 한 장으로 되는 2D 얼굴 — 얼굴 그림 바꾸기가 훨씬 싸다

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### i463 — `src/demos/demosRigB.ts:1098`
```ts
const i463: DemoMap[string] = {
  kind: '3d',
  caption: '같은 얼굴의 변형 6가지(깜빡 · 웃음 · 놀람 · 화남 · 아 · 오)를 비율로 섞어 표정 — morphTargetInfluences',
  make() {
    const st = makeStage([0.25, 1.4, 3.7], [0.12, 1.24, 0], 30, ['#3a3f72', '#171b36']);
    const ped = pedestal(1.0, '#f2c6d3', '#9a6f86');
    st.scene.add(ped);
    const bn = makeBunny();
    bn.group.rotation.y = -0.12;
    bn.group.position.x = 0.32;
    st.scene.add(bn.group);
    const springs = new Springs(bn, ['earL1', 'earL2', 'earR1', 'earR2', 'hair']);
    let auto = true;
    const manual = [0, 0, 0];
    const w = [0, 0, 0, 0, 0, 0];
    const COLS = ['#9fb4ff', '#ffb3c6', '#ffd36e', '#ff7a6b', '#8ee3c2', '#c4a6ff'];
    const NAMES = ['기본', '웃음', '놀람', '화남', '말하기', '웃음'];
    let label = '기본';
    st.hud.add(104, 98, 'tl', (g, lw, lh) => {
      plate(g, lw, lh);
      txt(g, auto ? '표정 · ' + label : '표정 · 직접 섞기', 8, 11, 8.5, '#fff', 'left', 800);
      for (let i = 0; i < 6; i++) bar(g, EXPR_KO[i]!, w[i]!, 8, 26 + i * 12.5, lw - 14, COLS[i]!, 24);
    }, 0.06);
    return {
      scene: st.scene,
      camera: st.camera,
      update(t, dt) {
        const d = Math.min(dt, 0.05);
        const tgt = [0, 0, 0, 0, 0, 0];
        let talk = 0;
        if (auto) {
          const seg = Math.floor(t / 2.2) % 6;
          label = NAMES[seg]!;
          if (seg === 1 || seg === 5) tgt[1] = 1;
          if (seg === 2) tgt[2] = 1;
          if (seg === 3) tgt[3] = 1;
          if (seg === 4 || seg === 5) talk = 1;
        } else {
          tgt[1] = manual[0]!;
          tgt[2] = manual[1]!;
          tgt[3] = manual[2]!;
        }
        const k = 1 - Math.exp(-d * 9);
        for (let i = 1; i < 4; i++) w[i] = lerp(w[i]!, tgt[i]!, k);
        // 말하기: 음절마다 아 · 오
        const syl = Math.floor(t / 0.17);
        const open = talk * Math.max(0, Math.sin(((t % 0.17) / 0.17) * Math.PI));
        const isO = (syl * 7919) % 3 === 0;
        w[4] = lerp(w[4]!, isO ? 0 : open * 0.9, 1 - Math.exp(-d * 25));
        w[5] = lerp(w[5]!, isO ? open : 0, 1 - Math.exp(-d * 25));
        w[0] = blinkAt(t) * (1 - w[1]!);
        for (let i = 0; i < 6; i++) bn.expr[i] = w[i]!;
        bn.applyFace();
        const b = bn.b;
        b.head!.rotation.set(-0.08 * w[2]! + 0.1 * w[3]! + 0.02 * Math.sin(t * 1.3), 0.08 * Math.sin(t * 0.7), 0.13 * w[1]! * Math.sin(t * 2.2) + 0.03 * Math.sin(t));
        b.spine!.rotation.x = 0.02 * Math.sin(t * 2);
        b.armL!.rotation.z = -0.6 * w[2]! + 0.3 * w[3]! - 0.25 * w[1]! * (0.5 + 0.5 * Math.sin(t * 6));
        b.armR!.rotation.z = 0.6 * w[2]! - 0.3 * w[3]! + 0.25 * w[1]! * (0.5 + 0.5 * Math.sin(t * 6 + 1));
        const earX = -0.55 * w[3]! + 0.05 * w[2]!;
        const earZ = 0.12 * w[2]! + 0.35 * w[3]!;
        b.earL1!.rotation.set(earX, 0, earZ);
        b.earR1!.rotation.set(earX, 0, -earZ);
        b.earL2!.quaternion.identity();
        b.earR2!.quaternion.identity();
        b.hair!.quaternion.identity();
        bn.group.position.y = 0.015 * Math.sin(t * 2);
        bn.group.updateMatrixWorld(true);
        springs.step(d, 0.035, 0.05, true);
      },
      render: st.render,
      controls: [
        { type: 'toggle', label: '자동 표정', value: true, on: (v) => (auto = v) },
        { type: 'range', label: '웃음', min: 0, max: 1, step: 0.01, value: 0, on: (v) => ((manual[0] = v), (auto = false)) },
        { type: 'range', label: '놀람', min: 0, max: 1, step: 0.01, value: 0, on: (v) => ((manual[1] = v), (auto = false)) },
        { type: 'range', label: '화남', min: 0, max: 1, step: 0.01, value: 0, on: (v) => ((manual[2] = v), (auto = false)) },
      ] as Control[],
      dispose() {
        bn.dispose();
        st.disposeAll();
      },
    };
  },
};
```

## 관련 기술
- 먼저 알면 좋은 기술: [코드로 뼈대 만들기 (Bone · SkinnedMesh)](https://ai-techstudio.web.app/ai/t/i456.md) `i456`
- 다음에 해 볼 기술: [시선 따라가기 (머리 · 눈 돌리기 제한)](https://ai-techstudio.web.app/ai/t/i461.md) `i461` · [애니메이션 섞기 (가만히 ↔ 걷기 ↔ 뛰기)](https://ai-techstudio.web.app/ai/t/i464.md) `i464`
- 참고 문서: [three.js 예제 — webgl_morphtargets_face](https://threejs.org/examples/#webgl_morphtargets_face)
