# AI 꾸러미 — 따라가는 카메라 (부드러운 추적) — Smooth follow camera (SmoothDamp)
> 카메라와 바라보는 곳을 SmoothDamp 스프링으로 따라가게 하고 목표를 가는 쪽으로 조금 앞질러 두어서, 흔들림 없이 따라가며 앞길을 더 보여 준다.  
> 견본: https://ai-techstudio.web.app/#t/i426

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

## 주문서

### 만들어 줘: 따라가는 카메라 (부드러운 추적) — Smooth follow camera (SmoothDamp)

#### 1. 목표
길을 걷는 병아리를 따라가는 카메라를 만들어 줘 — 스프링으로 부드럽게, 가는 쪽을 조금 앞질러 보여 주고, 폴짝 뛰어도 화면이 같이 흔들리지 않게. 분위기는 맑은 마을.

#### 2. 핵심 기술 용어
- **Smooth follow camera (SmoothDamp)** — 스프링으로 부드럽게 따라가는 카메라
- **Look-ahead (camera lead)** — 앞질러 보기 — 가는 쪽을 더 보여 주기
- **Critically damped spring** — 넘치지 않고 가장 빨리 붙는 용수철
- **Velocity smoothing** — 속도를 걸러 덜컹거림 없애기

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

#### 4. 조건
- 카메라 자리 · 바라보는 곳 둘 다 SmoothDamp(속도 상태를 따로 들고)로
- 앞질러 보기 = 걸러 낸 속도 × 배율(0.75) 를 목표에 더하기
- 폴짝 높이는 목표에 넣지 않는다 (바닥 자리만 따라가기)
- dt 는 0.05초로 자르고, 처음 한 번은 바로 놓기
- 앞질러 보기 켬/끔 · 부드러움(0.05 ~ 1초) · 「딱 붙기」 비교

#### 5. 완성 기준 (이게 보이면 성공)
- 병아리가 길을 따라 걷다 멈췄다 출발해도 화면이 부드럽게 따라가고 덜컹거리지 않는다
- 앞질러 보기를 켜면 가는 쪽이 더 보인다 (작은 지도에서 분홍 점이 병아리보다 앞)
- 「딱 붙기」 비교에서는 폴짝마다 화면이 흔들린다
- 「부드러움」을 키우면 더 느긋하게 따라온다

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

## 원리
- 딱 붙여 따라가면 폴짝 · 멈춤마다 화면이 덜컹거린다.
- SmoothDamp: 지금 값 · 목표 · 속도를 들고 「smoothTime 초쯤 걸려 넘치지 않게」 붙게 하는 식. ω = 2 / smoothTime, 감쇠 = 1 / (1 + x + 0.48x² + 0.235x³), x = ω·dt.
- 목표 = 자리 + 걸러 낸 속도 × 0.75 (앞질러 보기). 속도는 1 − e^(−6dt) 로 걸러 폴짝 높이 변화가 섞이지 않게.
- 바라보는 곳은 smoothTime 0.35, 카메라 자리는 그 1.25배(0.44)로 조금 더 늦게 — 고정 거리 (0, 6.8, 7.4) 뒤에서 따라간다.
- 처음 한 번은 스프링 없이 목표에 바로 놓는다 (시작부터 날아오지 않게).

## 핵심 코드 — SmoothDamp + 앞질러 보기
(발췌: demos/demosCamLight.ts smoothDamp() · demoFollow() 를 정리)
```ts
// Unity Vector3.SmoothDamp 와 같은 식 — 넘치지 않고 smoothTime 쯤에 붙는다
function smoothDamp(cur: THREE.Vector3, target: THREE.Vector3, vel: THREE.Vector3, smoothTime: number, dt: number): void {
  const om = 2 / Math.max(0.0001, smoothTime);
  const x = om * dt;
  const ex = 1 / (1 + x + 0.48 * x * x + 0.235 * x * x * x);
  const cx = cur.x - target.x, cy = cur.y - target.y, cz = cur.z - target.z;
  const tx = (vel.x + om * cx) * dt, ty = (vel.y + om * cy) * dt, tz = (vel.z + om * cz) * dt;
  vel.set((vel.x - om * tx) * ex, (vel.y - om * ty) * ex, (vel.z - om * tz) * ex);
  cur.set(target.x + (cx + tx) * ex, target.y + (cy + ty) * ex, target.z + (cz + tz) * ex);
}

const OFF = new THREE.Vector3(0, 6.8, 7.4);
const vel = new THREE.Vector3(), prev = new THREE.Vector3(), tmp = new THREE.Vector3();
const look = new THREE.Vector3(), lookV = new THREE.Vector3(), camP = new THREE.Vector3(), camV = new THREE.Vector3(), goal = new THREE.Vector3();
let first = true, lead = true, smooth = 0.35;

function follow(cam: THREE.PerspectiveCamera, pos: THREE.Vector3, dt0: number): void { // pos = 바닥 자리 (폴짝 높이 뺌)
  const dt = Math.min(dt0, 0.05);
  if (dt > 0) {
    tmp.copy(pos).sub(prev).divideScalar(dt);
    if (tmp.length() < 20) vel.lerp(tmp, 1 - Math.exp(-dt * 6)); // 속도 거르기
  }
  prev.copy(pos);
  goal.copy(pos);
  if (lead) goal.addScaledVector(vel, 0.75);                      // 앞질러 보기
  if (first) { look.copy(goal); camP.copy(goal).add(OFF); first = false; }
  smoothDamp(look, goal, lookV, smooth, dt);
  smoothDamp(camP, tmp.copy(goal).add(OFF), camV, smooth * 1.25, dt);
  cam.position.copy(camP);
  cam.lookAt(look);
}
```

## 흔한 실수 · 확인 목록
- [ ] **lerp(목표, 0.1) 로 따라가면 프레임 빠르기에 따라 속도가 달라진다** — dt 를 쓰는 SmoothDamp(또는 1 − e^(−k·dt))로. 고정 0.1 은 30fps 에서 두 배 느리다.
- [ ] **폴짝 높이까지 따라가면 화면이 위아래로 출렁인다** — 목표에는 바닥 자리만 넣는다.
- [ ] **앞질러 보기에 날 속도를 쓰면 방향이 바뀔 때 화면이 휙 돈다** — 속도를 1 − e^(−6dt) 로 걸러 쓰고, 순간 이동(20 넘음)은 버린다.

## 완성 기준 체크리스트
- [ ] 병아리가 길을 따라 걷다 멈췄다 출발해도 화면이 부드럽게 따라가고 덜컹거리지 않는다
- [ ] 앞질러 보기를 켜면 가는 쪽이 더 보인다 (작은 지도에서 분홍 점이 병아리보다 앞)
- [ ] 「딱 붙기」 비교에서는 폴짝마다 화면이 흔들린다
- [ ] 「부드러움」을 키우면 더 느긋하게 따라온다

## 이 기술 정보
- id: `i426` · 분류: 이펙트 · 연출 › 카메라 · 3D · 난이도 보통 · 폰 부담 가벼움 (폰 OK) — 벡터 계산 몇 줄. 비용 없음.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i426
- 쓰면 좋을 때: 캐릭터 · 공이 넓은 판을 돌아다닐 때 / 갑자기 멈췄다 출발하는 움직임이 있을 때
- 쓰지 말 때: 판 전체가 늘 보여야 하는 퍼즐 — 고정 카메라(u41)가 맞다 / 아주 빠른 경주 — 앞질러 보기 배율을 속도에 맞춰 키우지 않으면 화면 밖으로 나간다

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### demoFollow — `src/demos/demosCamLight.ts:921`
```ts
function demoFollow(): Scene3D {
  const { scene, cam, vil } = villageWorld();
  const chick = makeChick();
  scene.add(chick);
  const curve = makePath();
  const L = curve.getLength();
  const mm = mapFor(vil);
  const pathLine = mm.line(0xffffff, 0.35, 96);
  pathLine.set(curve.getSpacedPoints(95));
  const aheadDot = mm.dot(0xff6fae, 0.22);
  const chickDot = mm.dot(0xffd84a, 0.3);
  mm.camera();
  const hud = new Hud();
  const st = { lead: true, smooth: 0.35, rigid: false };
  const OFF = new THREE.Vector3(0, 6.8, 7.4);
  let dist = 0;
  const pos = new THREE.Vector3();
  const prev = curve.getPointAt(0);
  const vel = new THREE.Vector3();
  const look = new THREE.Vector3();
  const lookV = new THREE.Vector3();
  const camP = new THREE.Vector3();
  const camV = new THREE.Vector3();
  const goal = new THREE.Vector3();
  const tmp = new THREE.Vector3();
  let first = true;
  let speedK = 0;
  return {
    scene,
    camera: cam,
    update(t, dt) {
      dt = Math.min(dt, 0.05);
      const ph = t % 4;
      const want = ph < 2.9 ? 1 : 0;
      speedK += (want - speedK) * (1 - Math.exp(-dt * (want ? 4 : 7)));
      dist = (dist + dt * 2.3 * speedK) % L;
      pos.copy(curve.getPointAt(dist / L));
      const tan = curve.getTangentAt(dist / L);
      const hop = Math.abs(Math.sin(dist * 4.2)) * 0.22 * speedK;
      chick.position.set(pos.x, hop, pos.z);
      chick.rotation.y = Math.atan2(tan.x, tan.z);
      chick.rotation.z = Math.sin(dist * 4.2) * 0.12 * speedK;
      if (dt > 0) {
        tmp.copy(pos).sub(prev).divideScalar(dt);
        if (tmp.length() < 20) vel.lerp(tmp, 1 - Math.exp(-dt * 6));
      }
      prev.copy(pos);
      if (st.rigid) {
        look.copy(chick.position);
        camP.copy(chick.position).add(OFF);
        lookV.set(0, 0, 0);
        camV.set(0, 0, 0);
      } else {
        goal.copy(pos);
        if (st.lead) goal.addScaledVector(vel, 0.75);
        if (first) {
          look.copy(goal);
          camP.copy(goal).add(OFF);
          first = false;
        }
        smoothDamp(look, goal, lookV, st.smooth, dt);
        tmp.copy(goal).add(OFF);
        smoothDamp(camP, tmp, camV, st.smooth * 1.25, dt);
      }
      cam.position.copy(camP);
      cam.lookAt(look);
      chickDot.position.set(pos.x, 0, pos.z);
      aheadDot.position.set(look.x, 0, look.z);
      mm.setCamera(cam);
    },
    render(r, w, h) {
      withState(r, () => {
        r.render(scene, cam);
        mm.draw(r, w, h);
        const u = Math.min(1.9, w / 280, h / 175);
        hud.label('a', st.rigid ? '딱 붙기 (비교)' : st.lead ? '부드럽게 + 앞질러 보기' : '부드럽게만', 10 * u, 10 * u, 17 * u, { bg: st.rigid ? 'rgba(200,60,80,0.85)' : 'rgba(14,20,44,0.78)' });
        hud.draw(r, w, h);
      });
    },
    controls: [
      { type: 'toggle', label: '앞질러 보기 (가는 쪽을 더 보여 주기)', value: true, on: (v) => (st.lead = v) },
      { type: 'range', label: '부드러움 (스프링 시간, 초)', min: 0.05, max: 1, step: 0.05, value: 0.35, on: (v) => (st.smooth = v) },
      { type: 'toggle', label: '비교: 딱 붙기 (스프링 끔 · 폴짝도 따라 흔들림)', value: false, on: (v) => (st.rigid = v) },
    ] as Control[],
    dispose() {
      disposeTree(scene);
      mm.dispose();
      hud.dispose();
    },
  };
}
```

### i426 견본 항목 — `src/demos/demosCamLight.ts:2476`
```ts
  i426: { kind: '3d', caption: '병아리를 스프링으로 따라가며 가는 쪽을 앞질러 보여 줌 — 지도: 노랑 = 카메라 시야, 분홍 = 바라보는 곳', make: () => demoFollow() }
```

## 관련 기술
- 다음에 해 볼 기술: [장면 전환 카메라 (궤도 이동)](https://ai-techstudio.web.app/ai/t/i430.md) `i430` · [카메라 흔들림 (잡음 기반 · 감쇠)](https://ai-techstudio.web.app/ai/t/i429.md) `i429`
- 참고 문서: [Unity 문서 — Vector3.SmoothDamp](https://docs.unity3d.com/ScriptReference/Vector3.SmoothDamp.html)
