# AI 꾸러미 — 두 뼈 IK (팔 · 다리가 목표를 잡기) — Two-bone IK
> 위팔 · 아래팔 길이와 목표 위치만으로 어깨 · 팔꿈치 각도를 계산해, 손이 목표를 따라가게 한다.  
> 견본: https://ai-techstudio.web.app/#t/i459

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

## 주문서

### 만들어 줘: 두 뼈 IK (팔 · 다리가 목표를 잡기) — Two-bone IK

#### 1. 목표
캐릭터 팔이 공을 잡기에 두 뼈 IK 를 넣어 줘 — 목표 점을 움직이면 손(발)이 따라가고, 팔꿈치(무릎) 방향은 pole 로 정해. 화면은 뼈와 관절이 보이는 설명 화면.

#### 2. 핵심 기술 용어
- **Two-bone IK** — 뼈 두 개짜리 역운동학 (팔 · 다리)
- **Law of cosines** — 코사인 법칙 — 세 변 길이로 각 구하기
- **Pole vector** — 팔꿈치 · 무릎이 향할 쪽
- **Quaternion.setFromUnitVectors** — 방향 → 방향 회전

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

#### 4. 조건
- 닿지 않을 때 d 를 |a−b|+ε ~ a+b−ε 로 묶어 팔이 떨리지 않게
- pole 은 목표 방향에 수직인 성분만 써서 팔꿈치가 뒤집히지 않게
- 부모 뼈 좌표로 바꿔 계산 (부모가 움직여도 맞게)
- 목표 · 팔꿈치 · pole 을 작은 공 · 선으로 보여 주는 보기 단추

#### 5. 완성 기준 (이게 보이면 성공)
- 목표 공을 움직이면 손이 정확히 공에 닿고, 팔 길이는 변하지 않는다
- 공이 너무 멀면 팔을 쭉 뻗어 공 쪽을 가리킨다 (떨림 없음)
- pole 을 바꾸면 손은 그대로, 팔꿈치만 다른 쪽으로 꺾인다

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

## 원리
- 어깨 A, 위팔 길이 a, 아래팔 길이 b, 목표 T 가 있으면 삼각형 세 변(a, b, |T−A|)을 안다.
- 코사인 법칙 cos∠A = (a² + d² − b²) / (2ad) 로 어깨에서 꺾을 각이 나온다.
- 그 각만큼 목표 방향에서 pole 쪽으로 돌린 곳이 팔꿈치 자리다.
- 닿지 않는 거리면 d 를 a+b 로 줄여 팔을 쭉 뻗는다 (떨림 방지로 아주 조금 덜).
- 뼈에는 「쉼 자세 방향 → 새 방향」 회전(setFromUnitVectors)을 넣는다.

## 핵심 코드 — 코사인 법칙으로 팔꿈치 자리 구하기 + 뼈에 넣기
(발췌: demos/demosRigA.ts twoBoneIK() 를 세계 좌표 판으로 정리)
```ts
/** A 어깨 · a 위팔 · b 아래팔 · T 목표 · pole 팔꿈치가 향할 쪽 (모두 같은 좌표계) */
function solveTwoBone(A: THREE.Vector3, a: number, b: number, T: THREE.Vector3, pole: THREE.Vector3) {
  const d = T.clone().sub(A);
  const raw = d.length();
  const len = THREE.MathUtils.clamp(raw, Math.abs(a - b) + 1e-4, a + b - 1e-4); // 닿지 않으면 쭉 뻗기
  d.normalize();
  const cosA = THREE.MathUtils.clamp((a * a + len * len - b * b) / (2 * a * len), -1, 1); // 코사인 법칙
  const sinA = Math.sqrt(1 - cosA * cosA);
  const P = pole.clone().addScaledVector(d, -pole.dot(d)).normalize(); // pole 을 d 에 수직으로
  const elbow = A.clone().addScaledVector(d, a * cosA).addScaledVector(P, a * sinA);
  const hand = A.clone().addScaledVector(d, len);
  return { elbow, hand, reached: raw <= a + b };
}

// 뼈에 넣기 (upper · mid 의 쉼 자세 회전이 단위일 때, 좌표는 upper 의 부모 기준)
const { elbow, hand } = solveTwoBone(upper.position, mid.position.length(), handLocal.length(), targetLocal, poleLocal);
const qU = new THREE.Quaternion().setFromUnitVectors(mid.position.clone().normalize(), elbow.clone().sub(upper.position).normalize());
upper.quaternion.copy(qU);
const dirInUpper = hand.clone().sub(elbow).normalize().applyQuaternion(qU.clone().invert());
mid.quaternion.setFromUnitVectors(handLocal.clone().normalize(), dirInUpper);
```

## 흔한 실수 · 확인 목록
- [ ] **쭉 뻗을 때 팔이 부르르 떤다** — 거리를 a+b 바로 아래(−1e-4)로 묶는다. 정확히 a+b 면 sin 이 0 근처라 방향이 흔들린다.
- [ ] **팔꿈치가 갑자기 반대로 꺾인다** — pole 이 목표 방향과 거의 나란하면 수직 성분이 0 이 된다. 그럴 땐 다른 기본 축으로 대체.
- [ ] **쉼 자세 회전이 있는 뼈(불러온 모델)에선 각도가 틀어진다** — 견본은 코드로 세운 뼈라 쉼 회전이 단위다. 불러온 모델은 쉼 회전을 곱해 두고 차이만 돌린다.
- [ ] **부모가 움직였는데 세계 좌표로 계산** — 목표 · pole 을 upper 의 부모 좌표로 바꿔서 계산한다 (matrixWorld 역행렬).

## 완성 기준 체크리스트
- [ ] 목표 공을 움직이면 손이 정확히 공에 닿고, 팔 길이는 변하지 않는다
- [ ] 공이 너무 멀면 팔을 쭉 뻗어 공 쪽을 가리킨다 (떨림 없음)
- [ ] pole 을 바꾸면 손은 그대로, 팔꿈치만 다른 쪽으로 꺾인다

## 이 기술 정보
- id: `i459` · 분류: 3D 모델 · 캐릭터 › 캐릭터 · 리깅 · 3D · 난이도 보통 · 폰 부담 가벼움 (폰 OK) — 팔 하나에 제곱근 · 벡터 몇 번 — 수백 개를 돌려도 가볍다.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i459
- 쓰면 좋을 때: 손으로 물건 잡기 · 문고리 · 발 딛기처럼 끝점이 정확해야 할 때 / 애니메이션 파일 없이 캐릭터 동작을 만들 때
- 쓰지 말 때: 관절이 셋 이상인 사슬(꼬리 · 촉수) — FABRIK · CCD 가 맞다

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### twoBoneIK — `src/demos/demosRigA.ts:1003`
```ts
function twoBoneIK(upper: THREE.Bone, mid: THREE.Bone, endLocal: THREE.Vector3, target: THREE.Vector3, pole: THREE.Vector3, elbowOut?: THREE.Vector3): boolean {
  const parent = upper.parent!;
  parent.updateWorldMatrix(true, false);
  const inv = _m4.copy(parent.matrixWorld).invert();
  const A = _va.copy(upper.position);
  const T = _vb.copy(target).applyMatrix4(inv);
  const pq = parent.getWorldQuaternion(_q1).invert();
  const P = _vc.copy(pole).applyQuaternion(pq);
  const a = mid.position.length();
  const b = endLocal.length();
  const d = _vd.copy(T).sub(A);
  const raw = d.length();
  const len = clamp(raw, Math.abs(a - b) + 1e-4, a + b - 1e-4);
  d.normalize();
  const cosA = clamp((a * a + len * len - b * b) / (2 * a * len), -1, 1);
  const sinA = Math.sqrt(1 - cosA * cosA);
  P.addScaledVector(d, -P.dot(d));
  if (P.lengthSq() < 1e-8) P.set(0, 0, 1).addScaledVector(d, -d.z);
  P.normalize();
  const E = new THREE.Vector3().copy(A).addScaledVector(d, a * cosA).addScaledVector(P, a * sinA);
  const Tc = new THREE.Vector3().copy(A).addScaledVector(d, len);
  const uRest = new THREE.Vector3().copy(mid.position).normalize();
  const qU = _q2.setFromUnitVectors(uRest, new THREE.Vector3().subVectors(E, A).normalize());
  upper.quaternion.copy(qU);
  const ld = new THREE.Vector3().subVectors(Tc, E).normalize().applyQuaternion(qU.clone().invert());
  mid.quaternion.setFromUnitVectors(new THREE.Vector3().copy(endLocal).normalize(), ld);
  if (elbowOut) elbowOut.copy(E).applyMatrix4(parent.matrixWorld);
  return raw <= a + b;
}
```

## 관련 기술
- 먼저 알면 좋은 기술: [코드로 뼈대 만들기 (Bone · SkinnedMesh)](https://ai-techstudio.web.app/ai/t/i456.md) `i456`
- 다음에 해 볼 기술: [절차 걷기 · 뛰기 (애니메이션 파일 없이)](https://ai-techstudio.web.app/ai/t/i458.md) `i458` · [흔들리는 뼈 (귀 · 머리카락 · 꼬리 2차 움직임)](https://ai-techstudio.web.app/ai/t/i462.md) `i462`
- 참고 문서: [three.js 예제 — webgl_animation_skinning_ik](https://threejs.org/examples/#webgl_animation_skinning_ik) · [Wikipedia — Law of cosines](https://en.wikipedia.org/wiki/Law_of_cosines)
