# AI 꾸러미 — 컬 잡음 소용돌이 (curl noise)
> 입자를 무작위로 흔드는 대신 컬 잡음(소용돌이만 있는 흐름) 위에 띄워서, 연기 · 치유 오라가 지글거리지 않고 부드럽게 휘감기며 흐른다.  
> 견본: https://ai-techstudio.web.app/#t/i174

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

## 주문서

### 만들어 줘: 컬 잡음 소용돌이 (curl noise)

#### 1. 목표
치유 오라 (초록 빛 알갱이)의 입자가 컬 잡음을 따라 부드럽게 휘감기며 흐르게 해 줘 — 무작위 흔들기와 켬/끔으로 비교할 수 있게. 분위기는 초록 치유 빛.

#### 2. 핵심 기술 용어
- **Curl noise** — 컬 잡음 — 소용돌이만 있고 모이거나 퍼지지 않는 흐름
- **Divergence-free velocity field** — 발산 없는 속도장 (입자가 한곳에 뭉치지 않음)
- **Vector potential (curl of sine field)** — 퍼텐셜의 회전(curl)으로 속도 만들기
- **Particle advection** — 흐름을 따라 입자 옮기기

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

#### 4. 조건
- 흐름은 퍼텐셜의 curl 로 — 그냥 잡음 세 개를 속도로 쓰면 발산이 생겨 입자가 뭉친다
- 두 겹(작은 소용돌이 + 큰 소용돌이)과 시간 흐름
- 「컬 잡음 / 무작위 흔들기」 켬/끔 비교와 「소용돌이 세기」(0 ~ 3) 조절
- 흐름 크기(curlScale)는 효과 크기에 맞게 (견본 0.85)

#### 5. 완성 기준 (이게 보이면 성공)
- 4.5초마다 컬 잡음 ↔ 무작위 흔들기가 바뀌고, 컬 쪽은 알갱이가 부드럽게 휘감기며 올라가고 무작위 쪽은 지글지글 떤다
- 알갱이가 한곳에 뭉치거나 구멍이 나지 않고 고르게 퍼진다
- 「소용돌이 세기」를 0 으로 두면 곧게 올라간다
- 폰에서도 끊김 없이

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

## 원리
- 무작위로 매 프레임 방향을 바꾸면 입자가 지글지글 떤다. 대신 공간 전체에 부드럽게 바뀌는 「흐름」을 깔고 입자를 그 위에 띄운다.
- 흐름은 세 개의 sin 퍼텐셜 A = (Ax, Ay, Az) 의 회전(curl): v = (∂Az/∂y − ∂Ay/∂z, ∂Ax/∂z − ∂Az/∂x, ∂Ay/∂x − ∂Ax/∂y).
- curl 로 만든 흐름은 발산이 0 이라 입자가 한 점에 모이거나 비지 않고 고르게 휘감긴다.
- 견본은 두 겹(크기 1배 + 2.3배, 세기 1 + 0.45)을 더하고, 시간 t 를 넣어 흐름 자체도 천천히 바뀐다. 입자 자리 += 흐름 × 세기 × dt.

## 핵심 코드 — 컬 잡음 흐름 (두 겹) 과 입자 옮기기
(발췌: demos/demosSkillA.ts curlOct() · curlAt() · Pool.step() 을 정리)
```ts
const CV = { x: 0, y: 0, z: 0 };
// sin 퍼텐셜의 회전(curl) — 미분을 직접 써서 발산 0
function curlOct(x: number, y: number, z: number, t: number, s: number, amp: number): void {
  x *= s; y *= s; z *= s;
  const a1 = 1.3 * y + 0.7 * t, a2 = 1.7 * z + 0.4 * t;  // Ax 의 재료
  const b1 = 1.5 * z + 0.6 * t, b2 = 1.1 * x - 0.5 * t;  // Ay
  const c1 = 1.2 * x + 0.8 * t, c2 = 1.9 * y + 0.3 * t;  // Az
  const dAxdy = 1.3 * Math.cos(a1), dAxdz = -1.7 * Math.sin(a2);
  const dAydz = 1.5 * Math.cos(b1), dAydx = -1.1 * Math.sin(b2);
  const dAzdx = 1.2 * Math.cos(c1), dAzdy = -1.9 * Math.sin(c2);
  CV.x += (dAzdy - dAydz) * amp;
  CV.y += (dAxdz - dAzdx) * amp;
  CV.z += (dAydx - dAxdy) * amp;
}
function curlAt(x: number, y: number, z: number, t: number, s: number): void {
  CV.x = CV.y = CV.z = 0;
  curlOct(x, y, z, t, s, 1);                                 // 큰 소용돌이
  curlOct(x + 3.1, y - 1.7, z + 5.3, t * 1.3, s * 2.3, 0.45); // 작은 소용돌이
}

// 입자 한 걸음: 보통 이동 뒤 흐름만큼 더 옮긴다
const curlScale = 0.85;
let nx = x[i] + vx[i] * dt, ny = y[i] + vy[i] * dt, nz = z[i] + vz[i] * dt;
if (curl[i]) {
  curlAt(nx, ny, nz, time, curlScale);
  nx += CV.x * curl[i] * dt;  // curl[i] = 소용돌이 세기 (견본 1.3)
  ny += CV.y * curl[i] * dt;
  nz += CV.z * curl[i] * dt;
}
```

## 흔한 실수 · 확인 목록
- [ ] **잡음 세 개를 그대로 속도로 쓰면 입자가 한곳에 뭉친다** — 그건 발산이 있는 흐름이다. 퍼텐셜을 만들고 그 curl 을 속도로 쓴다.
- [ ] **흐름을 속도에 더하면 점점 빨라져 날아간다** — 견본처럼 흐름은 속도가 아니라 자리에 직접 더한다(nx += CV·세기·dt). 속도는 따로 둔다.
- [ ] **흐름 크기가 효과보다 너무 크면 그냥 한쪽으로 쏠린다** — 효과 지름 안에 소용돌이가 2~3개 들어가게 curlScale 을 맞춘다 (반지름 1 정도 오라에 0.85).

## 완성 기준 체크리스트
- [ ] 4.5초마다 컬 잡음 ↔ 무작위 흔들기가 바뀌고, 컬 쪽은 알갱이가 부드럽게 휘감기며 올라가고 무작위 쪽은 지글지글 떤다
- [ ] 알갱이가 한곳에 뭉치거나 구멍이 나지 않고 고르게 퍼진다
- [ ] 「소용돌이 세기」를 0 으로 두면 곧게 올라간다
- [ ] 폰에서도 끊김 없이

## 이 기술 정보
- id: `i174` · 분류: 이펙트 · 연출 › 스킬 VFX (마법 · 미사일) · 3D · 난이도 보통 · 폰 부담 보통 (폰 주의) — 입자마다 sin · cos 12번 정도. 견본은 초당 190개 · 최대 2400개를 CPU 로 돌린다. 폰은 1000개 안쪽.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i174
- 쓰면 좋을 때: 연기 · 오라 · 마나처럼 「살아 움직이는」 흐름이 필요할 때 / 입자가 무작위로 떨려 싸구려처럼 보일 때
- 쓰지 말 때: 폭발 · 불티처럼 곧게 튀어야 하는 것 — 컬을 끄고 속도 · 중력만 / 입자 수만 개 — CPU 계산이 무거워진다. 그땐 GPU(셰이더) 에서 같은 식을 계산한다

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### i174 견본 항목 — `src/demos/demosSkillA.ts:1839`
```ts
  i174: {
    kind: '3d',
    caption: '치유 오라: 컬 잡음을 따라 부드럽게 휘감기는 입자 ↔ 그냥 무작위로 흔들리는 입자',
    make(): Scene3D {
      const S = makeStage({ cam: [0, 5.2, 7.2], look: [0, 1.1, 0] });
      const P = new Pool(2400, S.puff);
      P.mesh.renderOrder = 3;
      P.curlScale = 0.85;
      S.scene.add(P.mesh);
      const lab = S.hud.label('tl');
      const gd = glowDisc(0x40ffb0, 1.6, 5);
      S.scene.add(gd.mesh);
      const ringM = new THREE.Mesh(new THREE.RingGeometry(1.08, 1.16, 96), new THREE.MeshBasicMaterial({ color: new THREE.Color(0x60ffc0).multiplyScalar(2), transparent: true, opacity: 0.7, blending: THREE.AdditiveBlending, depthWrite: false }));
      ringM.rotation.x = -Math.PI / 2;
      ringM.position.y = 0.02;
      S.scene.add(ringM);
      const pl = new THREE.PointLight(0x50ffb0, 10, 8, 1.5);
      pl.position.set(0, 1, 0);
      S.scene.add(pl);
      let auto = true;
      let curlOn = true;
      let amt = 1.3;
      let rate = 1;
      let acc = 0;
      P.onStep = (p, i, dt) => {
        // 무작위 흔들기 (비교용): 매 프레임 마구 바뀌는 방향
        const j = 7 * dt;
        p.vx[i] = p.vx[i]! * 0.9 + R(-1, 1) * j * 6;
        p.vz[i] = p.vz[i]! * 0.9 + R(-1, 1) * j * 6;
        p.vy[i] = p.vy[i]! + R(-1, 1) * j;
      };
      const controls: Control[] = [
        ctrlTog('컬 잡음 (끄면 무작위)', true, (v) => {
          auto = false;
          curlOn = v;
        }),
        ctrlRange('소용돌이 세기', 0, 3, 0.1, 1.3, (v) => (amt = v)),
        ctrlRange('입자 수 (배)', 0.3, 2.5, 0.1, 1, (v) => (rate = v)),
      ];
      return {
        scene: S.scene,
        camera: S.cam,
        controls,
        update(t, dt0) {
          const dt = Math.min(dt0, 0.05);
          const c = auto ? Math.floor(t / 4.5) % 2 === 0 : curlOn;
          lab.set(c ? '컬 잡음 — 부드럽게 휘감김' : '무작위 흔들기 — 지글지글', c ? '#60ffc0' : '#888');
          acc += dt * 190 * rate;
          while (acc >= 1) {
            acc--;
            const a = Math.random() * TAU;
            const r = R(0.2, 1.1);
            const big = Math.random() < 0.06;
            P.spawn({
              x: Math.cos(a) * r,
              y: R(0.02, 0.3),
              z: Math.sin(a) * r,
              vy: R(0.5, 0.9),
              life: R(2.4, 3.4),
              s0: big ? 0.25 : 0.05,
              s1: big ? 0.5 : 0.1,
              br: big ? 0.3 : 1,
              ramp: RAMP.heal,
              shape: big ? 0 : Math.random() < 0.2 ? 4 : 2,
              curl: c ? amt : 0,
              tag: c ? 0 : 1,
            });
          }
          ringM.rotation.z = t * 0.4;
          pl.intensity = 10 + Math.sin(t * 3) * 2;
          P.step(dt);
          P.upload();
          S.tick(dt);
        },
        resize: (w, h) => S.resize(w, h),
        dispose: () => S.dispose(),
      };
    },
  }
```

## 관련 기술
- 먼저 알면 좋은 기술: [입자 방출기 (수명 곡선)](https://ai-techstudio.web.app/ai/t/i170.md) `i170`
- 다음에 해 볼 기술: [부드러운 입자 (soft particles)](https://ai-techstudio.web.app/ai/t/i175.md) `i175`
- 참고 문서: [Wikipedia — Curl (mathematics)](https://en.wikipedia.org/wiki/Curl_(mathematics))
