# AI 꾸러미 — 2D 불 셰이더 — Procedural 2D fire shader
> 위로 흐르는 잡음을 불꽃 모양으로 깎고 밝기에 따라 색 띠를 입혀, 판 하나로 일렁이는 불을 만든다.  
> 견본: https://ai-techstudio.web.app/#t/i58

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

## 주문서

### 만들어 줘: 2D 불 셰이더 — Procedural 2D fire shader

#### 1. 목표
모닥불 · 촛불에 잡음을 위로 흘려 불꽃 모양으로 깎은 2D 불 셰이더를 넣어 줘. 분위기는 따뜻하고 아늑한 밤.

#### 2. 핵심 기술 용어
- **Procedural 2D fire shader** — 그림 없이 코드로 만드는 불
- **Scrolling fbm noise** — 시간에 따라 위로 흘러가는 여러 겹 잡음
- **Color ramp (gradient map)** — 밝기 0~1 을 검정 → 빨강 → 주황 → 노랑 → 흰색으로

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

#### 4. 조건
- 잡음 · 마스크 · 색 띠 세 단계를 나눠서 (설명 화면이면 셋을 나란히)
- 빠르기 · 불꽃 높이를 uniform 으로 조절
- 판에 쓸 땐 가산 섞기(AdditiveBlending) · depthWrite 끔
- 잡음은 반복문 없이 정해진 겹 수로 (또는 잡음 텍스처)

#### 5. 완성 기준 (이게 보이면 성공)
- 잡음 무늬가 아래에서 위로 흐르는 것이 보인다
- 불꽃 끝이 날름거리며 끊어지고, 가운데는 노랗고 가장자리는 빨갛다
- 「흐르는 빠르기」 · 「불꽃 높이」 슬라이더가 바로 반영된다
- 판 10개를 띄워도 폰에서 60fps

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

## 원리
- ① 잡음: fbm 잡음의 y 좌표에서 시간을 빼면 무늬가 위로 흘러간다 (빠른 잔잡음 30% 를 섞어 날름거림).
- ② 모양: 아래는 넓고 위로 갈수록 좁은 마스크(폭 = 0.26 × (1 − 높이)^0.6)를 만들고, 잡음이 낮은 곳은 깎아 낸다.
- ③ 색: 남은 세기 I(0~1)를 색 띠에 넣는다 — 0 검정, 0.3 짙은 빨강, 0.6 주황, 0.85 노랑, 1 흰색.
- 높이에 비례해 좌우로 살짝 흔들면(sin) 바람에 휘는 불꽃이 된다.

## 핵심 코드 — 위로 흐르는 잡음 → 불꽃 모양 → 색 띠
(발췌: demos/demosShader.ts fire2dDemo() 의 셰이더를 정리)
```glsl
uniform float uTime; uniform float uSpeed; uniform float uH; // uH = 불꽃 높이 0.62
vec3 ramp(float x){
  vec3 c = mix(vec3(0.0), vec3(0.7, 0.06, 0.02), smoothstep(0.0, 0.3, x));
  c = mix(c, vec3(1.0, 0.45, 0.05), smoothstep(0.3, 0.6, x));
  c = mix(c, vec3(1.0, 0.85, 0.3), smoothstep(0.6, 0.85, x));
  return mix(c, vec3(1.0, 1.0, 0.92), smoothstep(0.85, 1.0, x));
}
vec3 fire(vec2 p){ // p.x = 가운데 0, p.y = 아래 0 ~ 위 1
  float tt = uTime * uSpeed;
  // ① 잡음을 위로 흘린다
  float n = fbm(vec2(p.x * 5.0, p.y * 3.5 - tt * 1.8)) * 0.7 + noise(vec2(p.x * 11.0, p.y * 8.0 - tt * 3.2)) * 0.3;
  // ② 아래 넓고 위로 좁은 불꽃 모양으로 깎는다
  float base = 0.12;
  float vv = (p.y - base) / uH;
  float wd = 0.26 * pow(1.0 - clamp(vv, 0.0, 1.0), 0.6) + 0.005;
  float sway = 0.03 * sin(p.y * 6.0 - tt * 4.0) * clamp(vv, 0.0, 1.0);
  float mask = clamp(1.0 - abs(p.x + sway) / wd, 0.0, 1.0) * smoothstep(-0.1, 0.05, vv) * smoothstep(1.05, 0.8, vv);
  float I = clamp(mask * 1.8 - (1.0 - smoothstep(0.2, 0.8, n)) * 1.15 - max(vv, 0.0) * 0.5, 0.0, 1.0);
  // ③ 밝기 → 색 띠
  return ramp(clamp(I * 1.6, 0.0, 1.0));
}
```

## 흔한 실수 · 확인 목록
- [ ] **잡음을 그냥 위로만 흘리면 연기처럼 보인다** — 불꽃 모양 마스크에서 잡음이 낮은 곳을 「빼야」 끝이 날름거린다. 곱하기만 하면 뭉개진다.
- [ ] **색 띠 대신 한 색에 밝기만 바꾸면 밋밋하다** — 세기별로 빨강 → 주황 → 노랑 → 흰색을 지나야 진짜 불처럼 뜨거워 보인다.
- [ ] **판 가장자리에서 불이 잘린다** — 마스크를 판 안쪽(폭 0.26, 높이 uH 까지)으로 줄이고, 위 끝은 smoothstep 으로 사라지게.

## 완성 기준 체크리스트
- [ ] 잡음 무늬가 아래에서 위로 흐르는 것이 보인다
- [ ] 불꽃 끝이 날름거리며 끊어지고, 가운데는 노랗고 가장자리는 빨갛다
- [ ] 「흐르는 빠르기」 · 「불꽃 높이」 슬라이더가 바로 반영된다
- [ ] 판 10개를 띄워도 폰에서 60fps

## 이 기술 정보
- id: `i58` · 분류: 이펙트 · 연출 › 화려한 효과 (VFX) · 공통 · 난이도 쉬움 · 폰 부담 가벼움 (폰 OK) — 픽셀마다 잡음 몇 번이면 끝. 판 크기만큼만 계산하니 폰에서도 여러 개 OK.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i58
- 쓰면 좋을 때: 횃불 · 촛불처럼 작은 불이 여러 개일 때 / 정면에서만 보는 2D 화면 · 카메라를 보는 판(빌보드)
- 쓰지 말 때: 돌아가며 봐도 덩어리여야 하는 큰 불 — 부피 불(i57) / 터지는 한순간 — 플립북(i59)이나 입자(i170)가 더 쉽다

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### fire2dDemo — `src/demos/demosShader.ts:1171`
```ts
function fire2dDemo(): Scene3D {
  const u = { uRes: { value: new THREE.Vector2(1, 1) }, uTime: { value: 0 }, uSpeed: { value: 1 }, uH: { value: 0.62 } };
  const q = fsq(
    /* glsl */ `uniform vec2 uRes; uniform float uTime; uniform float uSpeed; uniform float uH; varying vec2 vUv;
    ${NOISE2}
    vec3 ramp(float x){
      vec3 c = mix(vec3(0.0), vec3(0.7, 0.06, 0.02), smoothstep(0.0, 0.3, x));
      c = mix(c, vec3(1.0, 0.45, 0.05), smoothstep(0.3, 0.6, x));
      c = mix(c, vec3(1.0, 0.85, 0.3), smoothstep(0.6, 0.85, x));
      return mix(c, vec3(1.0, 1.0, 0.92), smoothstep(0.85, 1.0, x));
    }
    void main(){
      float px = vUv.x * 3.0; float idx = floor(px); float u = fract(px);
      float asp = (uRes.x / 3.0) / uRes.y;
      vec2 p = vec2((u - 0.5) * asp, vUv.y);
      float tt = uTime * uSpeed;
      // ① 잡음을 위로 흘린다
      float n = fbm(vec2(p.x * 5.0, p.y * 3.5 - tt * 1.8)) * 0.7 + noise(vec2(p.x * 11.0, p.y * 8.0 - tt * 3.2)) * 0.3;
      // ② 불꽃 모양(아래 넓고 위로 좁게)으로 깎는다
      float base = 0.12;
      float vv = (p.y - base) / uH;
      float wd = 0.26 * pow(1.0 - clamp(vv, 0.0, 1.0), 0.6) + 0.005;
      float sway = 0.03 * sin(p.y * 6.0 - tt * 4.0) * clamp(vv, 0.0, 1.0);
      float mask = clamp(1.0 - abs(p.x + sway) / wd, 0.0, 1.0) * smoothstep(-0.1, 0.05, vv) * smoothstep(1.05, 0.8, vv);
      float n2 = smoothstep(0.2, 0.8, n);
      float I = clamp(mask * 1.8 - (1.0 - n2) * 1.15 - max(vv, 0.0) * 0.5, 0.0, 1.0);
      I = clamp(I * 1.6, 0.0, 1.0);
      vec3 bg = mix(vec3(0.05, 0.05, 0.12), vec3(0.1, 0.07, 0.2), vUv.y);
      vec3 col;
      if (idx < 0.5) col = vec3(n) * vec3(0.95, 0.95, 1.0);
      else if (idx < 1.5) col = mix(bg, vec3(1.0), I);
      else {
        col = bg + vec3(1.0, 0.35, 0.08) * 0.3 * exp(-length((p - vec2(0.0, base)) * vec2(1.0, 1.4)) * 5.0);
        col = max(col, ramp(I));
        // 장작
        float logY = abs(p.y - base + 0.04);
        if (logY < 0.022 && abs(p.x) < 0.17) col = mix(vec3(0.42, 0.24, 0.12), vec3(0.6, 0.36, 0.18), step(0.5, fract((p.x + p.y) * 30.0)));
      }
      float edge = min(u, 1.0 - u) * uRes.x / 3.0;
      if (edge < 1.0 && idx > 0.5 && u < 0.5) col = vec3(1.0);
      gl_FragColor = vec4(col, 1.0);
    }`,
    u,
  );
  const tags = new Tags(['① 잡음이 위로', '② 불꽃 모양', '③ 색 띠']);
  return {
    scene: q.scene,
    camera: q.camera,
    tone: THREE.NoToneMapping,
    update(t) {
      u.uTime.value = t;
    },
    render(r, w, h) {
      u.uRes.value.set(w, h);
      r.render(q.scene, q.camera);
      tags.draw(r, w, h, [
        [1 / 6, 0.08],
        [0.5, 0.08],
        [5 / 6, 0.08],
      ]);
    },
    controls: [
      { type: 'range', label: '흐르는 빠르기', min: 0.2, max: 3, step: 0.05, value: 1, on: (v) => (u.uSpeed.value = v) },
      { type: 'range', label: '불꽃 높이', min: 0.3, max: 0.9, step: 0.02, value: 0.62, on: (v) => (u.uH.value = v) },
    ],
    dispose() {
      q.dispose();
      tags.dispose();
    },
  };
}
```

## 관련 기술
- 먼저 알면 좋은 기술: [잡음으로 그린 성운 (fbm)](https://ai-techstudio.web.app/ai/t/u54.md) `u54`
- 다음에 해 볼 기술: [부피 불 · 연기 · 구름](https://ai-techstudio.web.app/ai/t/i57.md) `i57` · [플립북 (스프라이트 시트)](https://ai-techstudio.web.app/ai/t/i59.md) `i59` · [입자 방출기 (수명 곡선)](https://ai-techstudio.web.app/ai/t/i170.md) `i170`
- 참고 문서: [greentec — Shadertoy fire shader 설명](https://greentec.github.io/shadertoy-fire-shader-en/)
