# AI 꾸러미 — 잡음으로 그린 성운 (fbm) — fBm (fractal Brownian motion)
> 도메인 워프 잡음을 64×40 작은 캔버스에 화소로 계산한 뒤 부드럽게 크게 키우고 별을 얹어, 가볍게 흐르는 성운 하늘을 만든다.  
> 견본: https://ai-techstudio.web.app/#t/u54

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

## 주문서

### 만들어 줘: 잡음으로 그린 성운 (fbm) — fBm (fractal Brownian motion)

#### 1. 목표
게임 배경 밤하늘을 도메인 워프 fBm 잡음 성운으로 그려 줘 — 작은 캔버스(64×40)에 계산하고 부드럽게 키운 뒤 반짝이는 별을 얹는다. 색은 남색 → 보라 → 분홍 → 하늘.

#### 2. 핵심 기술 용어
- **fBm (fractal Brownian motion)** — 잡음을 크기 반씩 여러 겹 더한 것 — 구름 · 성운 결
- **Domain warping** — 잡음 좌표를 다른 잡음으로 휘게 해 소용돌이 무늬
- **Value noise** — 격자점 난수를 부드럽게 이은 잡음
- **Low-res render + upscale** — 작게 계산하고 크게 키우기

#### 3. 환경
- 플랫폼: HTML Canvas 2D (TypeScript), requestAnimationFrame 루프, 라이브러리 없이
- 화면: 2D · 브라우저 — PC 와 폰(390px 폭) 모두, devicePixelRatio 맞춰 또렷하게, 60fps 목표

#### 4. 조건
- 잡음 계산은 작은 캔버스(64×40 정도)에서만, 다시 칠하기는 0.06초마다
- 도메인 워프 세기(warp)를 슬라이더로 (0 이면 그냥 fbm)
- 크게 그릴 때 imageSmoothingEnabled = true · quality high (도트로 보이지 않게)
- 별은 큰 캔버스 위에 따로 — 작은 캔버스에 찍으면 키울 때 뭉개진다

#### 5. 완성 기준 (이게 보이면 성공)
- 왼쪽 64×40 원본 도트와 오른쪽 크게 키운 성운이 같은 무늬로 보인다
- 워프 0 → 5 로 올리면 덩어리가 소용돌이처럼 휘어진다
- 성운이 천천히 흐르고, 별은 따로 반짝인다
- 폰에서 60fps

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

## 원리
- fBm = 잡음을 크기 1/2 · 좌표 2배씩 4겹 더한 것. 큰 덩어리 위에 잔결이 얹힌다.
- 도메인 워프: 먼저 잡음 두 개로 q = (fbm(p), fbm(p + 5.2)) 를 만들고, 최종 값은 fbm(p + warp·q). 좌표가 휘어 소용돌이 성운이 된다.
- 이 계산은 화소마다 fbm 세 번이라 무겁다 — 64×40 = 2,560 화소만 계산하고 크게 키운다. 0.06초마다만 다시 칠한다.
- 키울 때 imageSmoothingQuality = high + 살짝 흐림(1.2px)으로 도트 티를 지우고, 별은 lighter 로 얹어 반짝이게.

## 핵심 코드 — 도메인 워프 성운을 작게 계산 → 크게 키우기
(발췌: demos/demosDraw2d.ts u54 paint() · fbm() · vnoise() 를 정리)
```ts
function hash2(x: number, y: number) {
  let n = Math.imul(x, 374761393) + Math.imul(y, 668265263);
  n = Math.imul(n ^ (n >>> 13), 1274126177);
  return ((n ^ (n >>> 16)) >>> 0) / 4294967295;
}
function vnoise(x: number, y: number) {
  const xi = Math.floor(x), yi = Math.floor(y), xf = x - xi, yf = y - yi;
  const u = xf * xf * (3 - 2 * xf), v = yf * yf * (3 - 2 * yf);
  const a = hash2(xi, yi), b = hash2(xi + 1, yi), c = hash2(xi, yi + 1), d = hash2(xi + 1, yi + 1);
  return a + (b - a) * u + (c - a) * v + (a - b - c + d) * u * v;
}
function fbm(x: number, y: number, oct = 4) {
  let s = 0, a = 0.5;
  for (let i = 0; i < oct; i++) { s += a * vnoise(x, y); x = x * 2.03 + 17.1; y = y * 2.01 + 3.7; a *= 0.5; }
  return s;
}
const SW = 64, SH = 40;
const small = document.createElement('canvas');
small.width = SW; small.height = SH;
const sg = small.getContext('2d')!;
const img = sg.createImageData(SW, SH);
function paint(t: number, warp = 2) {
  const d = img.data, tt = t * 0.06;
  for (let y = 0; y < SH; y++)
    for (let x = 0; x < SW; x++) {
      const px = x / 22, py = y / 22;
      const qx = fbm(px + tt, py, 3), qy = fbm(px + 5.2, py + 1.3 - tt, 3);
      const k = Math.min(1, Math.max(0, (fbm(px + warp * qx, py + warp * qy, 4) - 0.25) * 1.9));
      const i = (y * SW + x) * 4;
      d[i] = Math.min(255, 12 + 230 * k * k * (0.4 + qx) + 40 * qy * k);
      d[i + 1] = Math.min(255, 14 + 120 * k * k * qy * 1.6 + 30 * k);
      d[i + 2] = Math.min(255, 40 + 190 * k * (0.5 + qy * 0.6));
      d[i + 3] = 255;
    }
  sg.putImageData(img, 0, 0);
}
// 그리기: 0.06초마다 paint(t) → g.imageSmoothingQuality = 'high'; g.filter = 'blur(1.2px)';
// g.drawImage(small, x - 4, y - 4, w + 8, h + 8); g.filter = 'none'; 그 위에 별은 'lighter' 로
```

## 흔한 실수 · 확인 목록
- [ ] **큰 캔버스에서 바로 잡음을 계산하면 프레임이 무너진다** — 64×40 처럼 작게 계산하고 키운다. 다시 칠하기도 0.06초마다면 충분하다.
- [ ] **키울 때 흐림 없이 그리면 가장자리에 도트 계단이 보인다** — 살짝 흐림(1.2px)을 주고 4px 바깥까지 넘치게 그려 테두리 번짐을 숨긴다.
- [ ] **같은 잡음을 셰이더에 반복문으로 옮기면 윈도에서 컴파일이 멈춘다** — 보물 동굴에서 17~20초 멈췄다. 셰이더에서는 미리 구운 잡음 텍스처를 읽기만 한다.
- [ ] **별을 작은 캔버스에 찍으면 키울 때 번진 얼룩이 된다** — 별은 큰 화면에 따로 그린다.

## 완성 기준 체크리스트
- [ ] 왼쪽 64×40 원본 도트와 오른쪽 크게 키운 성운이 같은 무늬로 보인다
- [ ] 워프 0 → 5 로 올리면 덩어리가 소용돌이처럼 휘어진다
- [ ] 성운이 천천히 흐르고, 별은 따로 반짝인다
- [ ] 폰에서 60fps

## 이 기술 정보
- id: `u54` · 분류: 2D · 화면 › 2D 그리기 · 2D · 난이도 보통 · 폰 부담 가벼움 (폰 OK) — 64×40 × fbm 3번(4겹) ≈ 3만 번 잡음 계산을 0.06초마다. 폰에서도 1ms 안쪽.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/u54
- 쓰면 좋을 때: 2D 배경 하늘에 움직이는 성운 · 구름 결이 필요할 때 / 그림 파일 없이 코드로 매번 다른 하늘을 만들 때
- 쓰지 말 때: 큰 해상도에서 매 프레임 계산 — 1920×1080 이면 200만 화소 × fbm 3번. 꼭 작게 계산 / 셰이더 안에서 반복문 잡음을 여러 번 부르기 — 컴파일 멈춤. 대신 잡음 텍스처

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### u54 견본 항목 — `src/demos/demosDraw2d.ts:422`
```ts
  u54: {
    kind: '2d',
    caption: '왼쪽 작은 캔버스(64×40)에 도메인 워프 잡음을 그리고 → 오른쪽처럼 크게 키워 별을 얹으면 성운',
    make() {
      const SW = 64;
      const SH = 40;
      const small = mkCanvas(SW, SH);
      const sg = ctx(small);
      const img = sg.createImageData(SW, SH);
      let warp = 2;
      let acc = 1;
      const stars: [number, number, number, number][] = [];
      const r = rng(7);
      for (let i = 0; i < 90; i++) stars.push([r(), r(), r() * 1.2 + 0.3, r() * 6]);
      const paint = (t: number): void => {
        const d = img.data;
        const tt = t * 0.06;
        for (let y = 0; y < SH; y++)
          for (let x = 0; x < SW; x++) {
            const px = x / 22;
            const py = y / 22;
            const qx = fbm(px + tt, py, 3);
            const qy = fbm(px + 5.2, py + 1.3 - tt, 3);
            const v = fbm(px + warp * qx, py + warp * qy, 4);
            const k = clamp01((v - 0.25) * 1.9);
            const i = (y * SW + x) * 4;
            // 남색 → 보라 → 분홍 → 하늘 (q 로 색이 갈림)
            const R = 12 + 230 * k * k * (0.4 + qx) + 40 * qy * k;
            const Gc = 14 + 120 * k * k * qy * 1.6 + 30 * k;
            const B = 40 + 190 * k * (0.5 + qy * 0.6);
            d[i] = Math.min(255, R);
            d[i + 1] = Math.min(255, Gc);
            d[i + 2] = Math.min(255, B);
            d[i + 3] = 255;
          }
        sg.putImageData(img, 0, 0);
      };
      return {
        draw(g, w, h, t, dt) {
          const u = scaleOf(w, h);
          acc += dt;
          if (acc > 0.06) {
            acc = 0;
            paint(t);
          }
          g.fillStyle = '#05060f';
          g.fillRect(0, 0, w, h);
          // 왼쪽: 그대로(도트)
          const lw = w * 0.3;
          const lh = (lw * SH) / SW;
          const lx = w * 0.04;
          const ly = (h - lh) / 2;
          g.imageSmoothingEnabled = false;
          g.drawImage(small, lx, ly, lw, lh);
          g.strokeStyle = 'rgba(255,255,255,.7)';
          g.lineWidth = 1;
          g.strokeRect(lx, ly, lw, lh);
          txt(g, '64 × 40', lx + lw / 2, ly - 9 * u, 8.5 * u, '#cfd6ff');
          txt(g, '작게 계산', lx + lw / 2, ly + lh + 10 * u, 8.5 * u, '#9aa6e0');
          txt(g, '→', w * 0.385, h / 2, 16 * u, '#ffd36b', 'center', F, 900);
          // 오른쪽: 부드럽게 키우기 + 별
          const rx = w * 0.43;
          const ry = h * 0.08;
          const rw = w * 0.54;
          const rh = h * 0.84;
          g.save();
          rr(g, rx, ry, rw, rh, 8 * u);
          g.clip();
          g.imageSmoothingEnabled = true;
          g.imageSmoothingQuality = 'high';
          g.filter = `blur(${1.2 * u}px)`;
          g.drawImage(small, rx - 4, ry - 4, rw + 8, rh + 8);
          g.filter = 'none';
          g.globalCompositeOperation = 'lighter';
          for (const [sx, sy, sz, ph] of stars) {
            const a = 0.45 + 0.55 * Math.sin(t * 2 + ph);
            g.fillStyle = `rgba(255,255,255,${a})`;
            g.beginPath();
            g.arc(rx + sx * rw, ry + sy * rh, sz * u, 0, Math.PI * 2);
            g.fill();
          }
          g.globalCompositeOperation = 'source-over';
          g.restore();
          g.strokeStyle = 'rgba(255,255,255,.35)';
          rr(g, rx, ry, rw, rh, 8 * u);
          g.stroke();
          pill(g, '크게 키움 + 별', rx + rw / 2, ry + rh - 11 * u, 8 * u, 'rgba(0,0,0,.5)');
        },
        controls: [{ type: 'range', label: '휘어짐 (워프)', min: 0, max: 5, step: 0.1, value: 2, on: (v) => (warp = v) }] as Control[],
      };
    },
  }
```

## 관련 기술
- 먼저 알면 좋은 기술: [캔버스 그러데이션 · 흐림 · 빛 섞기](https://ai-techstudio.web.app/ai/t/u52.md) `u52`
- 다음에 해 볼 기술: [팔레트 색 순환 (그림은 그대로, 색만 흐름)](https://ai-techstudio.web.app/ai/t/i236.md) `i236` · [화면 밖 캔버스 (OffscreenCanvas)](https://ai-techstudio.web.app/ai/t/i32.md) `i32`
- 참고 문서: [Inigo Quilez — Domain Warping](https://iquilezles.org/articles/warp/) · [Inigo Quilez — fBM](https://iquilezles.org/articles/fbm/)
