# AI 꾸러미 — 절차 뭉게구름 — Procedural clouds (fbm threshold)
> 잡음(fbm)을 문턱으로 잘라 구름 모양을 만들고 해 쪽으로 한 번 더 읽어 명암을 넣어, 몽실몽실 흘러가는 구름 두 겹을 그린다.  
> 견본: https://ai-techstudio.web.app/#t/i225

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

## 주문서

### 만들어 줘: 절차 뭉게구름 — Procedural clouds (fbm threshold)

#### 1. 목표
메뉴 · 시작 화면 하늘에 fbm 문턱으로 만든 뭉게구름 두 겹을 그려 줘 — 해 쪽은 밝고 아래는 그늘, 바람에 흘러가게. 그림체는 부드러운 그림책.

#### 2. 핵심 기술 용어
- **Procedural clouds (fbm threshold)** — 겹친 잡음을 문턱으로 잘라 만든 구름
- **Fractal Brownian motion (fbm)** — 크기가 반씩 작은 잡음 5겹을 더한 것
- **Directional derivative lighting** — 해 쪽으로 조금 옮긴 밀도와의 차로 밝음 · 그늘
- **Full-screen quad shader** — 화면을 덮는 사각형 하나에 그리는 셰이더

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

#### 4. 조건
- 구름 양은 문턱 하나(uCover 0.35 ~ 0.75)로, 바람은 시간 × 빠르기로 잡음 좌표를 민다
- 명암은 해 쪽으로 옮긴 밀도와의 차: light = clamp(0.55 + (d - d2) * 5.0, 0, 1)
- 먼 겹 · 가까운 겹 두 장, 크기 · 빠르기를 다르게 (먼 겹은 위쪽에만)
- 화면 비율(uRes)을 넣어 구름이 가로로 늘어나지 않게
- 잡음 반복문 셰이더가 첫 컴파일에 오래 걸리면 잡음을 텍스처로 미리 굽는다

#### 5. 완성 기준 (이게 보이면 성공)
- 하늘에 크고 작은 뭉게구름이 흘러가고, 해 쪽 면은 밝고 반대쪽 · 아래는 푸르스름한 그늘이다
- 「구름 양」 슬라이더로 맑은 하늘 ↔ 거의 덮인 하늘이 된다
- 「툰 구름」을 켜면 명암이 3단으로 끊기고 테가 또렷해진다
- 먼 구름은 느리게, 가까운 구름은 빠르게 흘러 깊이가 느껴진다

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

## 원리
- 잡음 5겹(fbm)을 더하면 크고 작은 덩어리가 섞인 값이 나온다. 그 값에서 문턱(구름 양, 견본 0.56)을 빼고 0 보다 큰 곳만 구름이다.
- 해 쪽으로 조금(0.12) 옮긴 자리의 밀도를 한 번 더 읽어 차를 본다. 해 쪽이 옅으면 그 면은 빛을 받아 밝다.
- 가장자리 얇은 곳에 따뜻한 빛 테(rim)를 더해 햇살이 비치는 느낌을 준다.
- 먼 구름은 작게 · 느리게(0.05), 가까운 구름은 크게 · 빠르게(0.16) 흘려 깊이를 낸다. 툰 모드는 명암을 3단(1 · 0.6 · 0.25)으로 끊는다.

## 핵심 코드 — fbm 문턱 구름 한 겹 (해 쪽 명암 · 빛 테 · 툰 3단)
(발췌: demos/demosLook2.ts makeClouds() 의 조각 셰이더를 정리)
```glsl
// noise() · fbm(5겹) 은 이 위에 (값 잡음 + smoothstep 보간)
uniform float uTime, uCover, uSpeed, uToon;
float dens(vec2 p, float cover){
  float d = fbm(p) * 0.85 + noise(p * 0.5) * 0.35;
  return d - cover;                       // 0 보다 크면 구름
}
vec4 layer(vec2 p, float cover, vec2 sunDir, vec3 lit, vec3 shade, float soft){
  float d = dens(p, cover);
  float a = smoothstep(0.0, soft, d);     // 가장자리 부드럽게
  float d2 = dens(p + sunDir * 0.12, cover);
  float light = clamp(0.55 + (d - d2) * 5.0, 0.0, 1.0); // 해 쪽이 옅으면 밝다
  float rim = smoothstep(soft * 2.5, 0.0, d) * a;        // 얇은 가장자리 빛 테
  if (uToon > 0.5){
    light = light > 0.62 ? 1.0 : (light > 0.35 ? 0.6 : 0.25);
    a = step(0.02, d);
    rim = step(d, 0.045) * a;
  }
  vec3 c = mix(shade, lit, light) + rim * vec3(1.0, 0.92, 0.8) * 0.35;
  return vec4(c, a * 0.85);
}
// main(): t = uTime * uSpeed
//  먼 겹  layer(p * vec2(3.6, 6.0) + vec2(t * 0.05, 3.0), uCover + 0.06, …, soft 0.08)
//  가까운 layer(p * vec2(1.7, 2.6) + vec2(t * 0.16, 0.0), uCover + 0.08 - shapeY * 0.18, …, soft 0.06)
//  col = mix(sky, far.rgb, far.a);  col = mix(col, near.rgb, near.a);
```

## 흔한 실수 · 확인 목록
- [ ] **반복문 잡음을 셰이더 여러 곳에서 부르면 첫 컴파일에 수십 초 멈춘다** — 보물 동굴에서 윈도 D3D 컴파일이 17~20초 걸렸다. 잡음은 미리 구운 텍스처로 읽고, 값은 uniform 으로.
- [ ] **화면 비율을 안 넣으면 구름이 가로로 늘어난다** — p = vec2(uv.x * 가로/세로, uv.y) 로 좌표를 맞춘다.
- [ ] **문턱 없이 잡음을 그대로 칠하면 구름이 아니라 얼룩 안개가 된다** — 밀도 - 문턱 이 0 보다 큰 곳만, smoothstep 으로 가장자리를 다듬는다.

## 완성 기준 체크리스트
- [ ] 하늘에 크고 작은 뭉게구름이 흘러가고, 해 쪽 면은 밝고 반대쪽 · 아래는 푸르스름한 그늘이다
- [ ] 「구름 양」 슬라이더로 맑은 하늘 ↔ 거의 덮인 하늘이 된다
- [ ] 「툰 구름」을 켜면 명암이 3단으로 끊기고 테가 또렷해진다
- [ ] 먼 구름은 느리게, 가까운 구름은 빠르게 흘러 깊이가 느껴진다

## 이 기술 정보
- id: `i225` · 분류: 그래픽 · 셰이더 › 빛 · 환경 · 공통 · 난이도 보통 · 폰 부담 보통 (폰 주의) — 픽셀마다 fbm(잡음 5번)을 겹 하나에 두 번, 두 겹이라 잡음 약 20번. 폰 전체 화면이면 무겁다 — 미리 구운 잡음 텍스처로 바꾸면 가볍다.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i225
- 쓰면 좋을 때: 그림 파일 없이 움직이는 하늘이 필요할 때 / 메뉴 배경처럼 화면 전체를 덮는 하늘 / 툰 장면에 어울리는 또렷한 구름
- 쓰지 말 때: 구름 사이를 날아다니는 3D 장면 — 대신 부피 렌더(i524 같은 레이마칭) / 폰에서 화면 전체를 고해상도로 — 픽셀 비율을 낮추거나 잡음을 텍스처로

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### makeClouds — `src/demos/demosLook2.ts:1151`
```ts
function makeClouds(): Scene3D {
  const uni = { uTime: { value: 0 }, uRes: { value: new THREE.Vector2(1, 1) }, uCover: { value: 0.56 }, uSpeed: { value: 1 }, uToon: { value: 0 } };
  const q = quad(
    /* glsl */ `
    uniform float uTime, uCover, uSpeed, uToon; uniform vec2 uRes; varying vec2 vUv;
    ${NOISE}
    float dens(vec2 p, float cover){
      float d = fbm(p) * 0.85 + noise(p * 0.5) * 0.35;
      return d - cover;
    }
    vec4 layer(vec2 p, float cover, vec2 sunDir, vec3 lit, vec3 shade, float soft){
      float d = dens(p, cover);
      float flatBottom = smoothstep(-0.25, 0.25, fract(p.y * 0.18) - 0.2);
      float a = smoothstep(0.0, soft, d);
      float d2 = dens(p + sunDir * 0.12, cover);
      float light = clamp(0.55 + (d - d2) * 5.0, 0.0, 1.0);
      float rim = smoothstep(soft * 2.5, 0.0, d) * a;
      if (uToon > 0.5){
        light = light > 0.62 ? 1.0 : (light > 0.35 ? 0.6 : 0.25);
        a = step(0.02, d);
        rim = step(d, 0.045) * a;
      }
      vec3 c = mix(shade, lit, light) + rim * vec3(1.0, 0.92, 0.8) * 0.35;
      return vec4(c, a * (0.85 + flatBottom * 0.0));
    }
    void main(){
      vec2 uv = vUv; float asp = uRes.x / uRes.y;
      vec2 p = vec2(uv.x * asp, uv.y);
      vec3 sky = mix(vec3(0.98, 0.83, 0.70), vec3(0.32, 0.58, 0.95), smoothstep(0.0, 0.9, uv.y));
      sky = mix(sky, vec3(0.18, 0.40, 0.86), smoothstep(0.6, 1.0, uv.y));
      vec2 sun = vec2(asp * 0.82, 0.78);
      float sd = length(p - sun);
      sky += vec3(1.0, 0.85, 0.55) * (0.5 * exp(-sd * 6.0) + 0.9 * smoothstep(0.06, 0.05, sd));
      float t = uTime * uSpeed;
      vec2 sdir = normalize(sun - p);
      // 먼 구름 (작고 느림)
      vec4 far = layer(p * vec2(3.6, 6.0) + vec2(t * 0.05, 3.0), uCover + 0.06, sdir, vec3(1.0, 0.95, 0.92), vec3(0.62, 0.66, 0.82), 0.08);
      far.a *= smoothstep(0.25, 0.55, uv.y) * 0.85;
      vec3 col = mix(sky, far.rgb, far.a);
      // 가까운 뭉게구름 (크고 빠름, 아래가 평평)
      vec2 q = p * vec2(1.7, 2.6) + vec2(t * 0.16, 0.0);
      float shapeY = smoothstep(0.05, 0.3, uv.y) * (1.0 - smoothstep(0.62, 0.95, uv.y));
      vec4 near = layer(q, uCover + 0.08 - shapeY * 0.18, sdir, vec3(1.0, 0.98, 0.96), vec3(0.55, 0.6, 0.78), 0.06);
      near.a *= smoothstep(0.08, 0.2, uv.y);
      col = mix(col, near.rgb, near.a);
      // 땅 언덕
      float hill = 0.12 + 0.05 * sin(p.x * 3.0 + 1.0) + 0.03 * sin(p.x * 7.0);
      col = mix(col, vec3(0.30, 0.55, 0.30), smoothstep(hill + 0.004, hill - 0.004, uv.y));
      gl_FragColor = vec4(pow(col, vec3(2.2)), 1.0);
      ${OUT}
    }`,
    uni,
  );
  return {
    scene: q.scene,
    camera: q.cam,
    tone: THREE.NoToneMapping,
    update(t) {
      uni.uTime.value = t;
    },
    render(r, w, h) {
      uni.uRes.value.set(w, h);
      r.render(q.scene, q.cam);
    },
    controls: [
      { type: 'range', label: '구름 양 (문턱)', min: 0.35, max: 0.75, step: 0.01, value: 0.56, on: (v) => (uni.uCover.value = v) },
      { type: 'range', label: '바람 속도', min: 0, max: 4, step: 0.1, value: 1, on: (v) => (uni.uSpeed.value = v) },
      { type: 'toggle', label: '툰 구름 (3단 명암 · 또렷한 테)', value: false, on: (v) => (uni.uToon.value = v ? 1 : 0) },
    ],
    dispose() {
      q.dispose();
    },
  };
}
```

### i225 견본 항목 — `src/demos/demosLook2.ts:4509`
```ts
  i225: { kind: '3d', caption: 'fbm 잡음 문턱으로 만든 뭉게구름 두 겹 — 해 쪽 밝고 아래 그늘, 바람에 흘러감 (툰 3단도)', make: () => makeClouds() }
```

## 관련 기술
- 먼저 알면 좋은 기술: [잡음으로 그린 성운 (fbm)](https://ai-techstudio.web.app/ai/t/u54.md) `u54`
- 다음에 해 볼 기술: [대기 산란 하늘](https://ai-techstudio.web.app/ai/t/i224.md) `i224` · [하루 시간 흐름 (해 · 하늘색)](https://ai-techstudio.web.app/ai/t/i431.md) `i431`
- 참고 문서: [The Book of Shaders — Fractal Brownian Motion](https://thebookofshaders.com/13/) · [Inigo Quilez — fbm](https://iquilezles.org/articles/fbm/)
