# AI 꾸러미 — 빛 번짐 (UnrealBloom) — Bloom (UnrealBloomPass)
> 그린 화면에서 문턱보다 밝은 곳만 골라 여러 크기로 흐려 다시 더해, 네온 · 전구 · 빛나는 물체 둘레에 빛이 번지게 한다.  
> 견본: https://ai-techstudio.web.app/#t/u20

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

## 주문서

### 만들어 줘: 빛 번짐 (UnrealBloom) — Bloom (UnrealBloomPass)

#### 1. 목표
네온 고리 · 전구 · 빛나는 공에 빛 번짐(UnrealBloom)을 넣어 줘 — 문턱보다 밝은 곳만 번지게, 나머지는 또렷하게. 분위기는 어두운 밤 네온.

#### 2. 핵심 기술 용어
- **Bloom (UnrealBloomPass)** — 빛 번짐 후처리
- **Luminance threshold** — 문턱 — 이보다 밝은 곳만 번짐
- **Mip-chain blur** — 해상도를 반씩 줄여 가며 흐려 넓게 번지게
- **HDR emissive (emissiveIntensity > 1)** — 1 보다 밝은 빛 — 이것만 번지게

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

#### 4. 조건
- EffectComposer: RenderPass → UnrealBloomPass(크기, 세기 0.8, 반경 0.4, 문턱 1.4) → OutputPass
- 번질 물체만 emissive 를 1 보다 크게 — 보통 물체(흰 상자)는 문턱 아래
- composer 는 렌더러마다 하나, 화면 크기가 바뀌면 setSize
- 같은 장면 왼쪽 끔 · 오른쪽 켬 비교, 「번짐 세기」 0~3 · 「문턱」 0~4 슬라이더

#### 5. 완성 기준 (이게 보이면 성공)
- 왼쪽은 네온 고리 · 공이 또렷하기만 하고, 오른쪽은 그 둘레에 색 빛이 번진다
- 가운데 흰 상자는 양쪽 모두 번지지 않는다
- 문턱을 0 으로 내리면 상자까지 번지고, 4 로 올리면 번짐이 사라진다

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

## 원리
- 장면을 먼저 그림(렌더 타깃)으로 그린 뒤, 밝기가 문턱(1.4)을 넘는 곳만 따로 뽑는다.
- 그 밝은 그림을 해상도를 반씩 줄여 가며 여러 번 흐린 뒤 합치면 가까이는 진하고 멀리는 넓게 번진다.
- 원래 그림에 세기(0.8)만큼 더한다.
- 번질 물체는 emissiveIntensity 를 1 보다 크게(3.5~4) 해 문턱을 넘게, 보통 물체는 넘지 않게 둔다.

## 핵심 코드 — 번질 것만 밝게 + 빛 번짐 사슬
(발췌: demos/demosLight.ts u20 make() · postKit() 를 정리)
```ts
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js';
import { OutputPass } from 'three/examples/jsm/postprocessing/OutputPass.js';

// 번질 것: emissive 를 1 보다 크게 (문턱 1.4 를 넘도록)
const neon = (c: number, k = 4) => new THREE.MeshStandardMaterial({ color: c, emissive: c, emissiveIntensity: k });
scene.add(new THREE.Mesh(new THREE.TorusGeometry(1.15, 0.055, 16, 96), neon(0xff4fa8)));
// 번지지 않을 것: 보통 재질
scene.add(new THREE.Mesh(new RoundedBoxGeometry(0.55, 0.55, 0.55, 3, 0.08), new THREE.MeshStandardMaterial({ color: 0xdfe6ff, roughness: 0.5 })));

const comp = new EffectComposer(renderer);
comp.addPass(new RenderPass(scene, camera));
const bloom = new UnrealBloomPass(new THREE.Vector2(256, 256), 0.8, 0.4, 1.4); // 크기 · 세기 · 반경 · 문턱
comp.addPass(bloom);
comp.addPass(new OutputPass());                 // 톤 매핑 · sRGB 는 맨 끝에서

function resize(w: number, h: number) { comp.setSize(w, h); }
function frame(dt: number) {
  bloom.strength = 0.8;
  bloom.threshold = 1.4;
  comp.render(dt);                              // renderer.render 대신
}
```

## 흔한 실수 · 확인 목록
- [ ] **밝은 배경 장면에서 문턱을 낮게 두면 화면 전체가 하얗게 날아간다** — 장난감 상자 게임은 반구광을 낮추고 문턱을 1.25 로 맞췄다. 번질 것만 emissive 로 문턱을 넘게.
- [ ] **전광판 · 글씨가 번져 숫자가 안 읽힌다** — 숫자 야구는 전광판 화면 색을 문턱 아래(0xd8)로 두어 또렷하게 했다.
- [ ] **composer 를 쓰면 기본 안티앨리어싱이 꺼진다** — 렌더 타깃 samples 를 주거나 SMAA 패스(i04)를 더한다.
- [ ] **OutputPass 를 빼면 색이 어둡고 칙칙하다** — 후처리 중간은 선형 색이다. 사슬 맨 끝에 OutputPass(u21).

## 완성 기준 체크리스트
- [ ] 왼쪽은 네온 고리 · 공이 또렷하기만 하고, 오른쪽은 그 둘레에 색 빛이 번진다
- [ ] 가운데 흰 상자는 양쪽 모두 번지지 않는다
- [ ] 문턱을 0 으로 내리면 상자까지 번지고, 4 로 올리면 번짐이 사라진다

## 이 기술 정보
- id: `u20` · 분류: 그래픽 · 셰이더 › 후처리 · 3D · 난이도 쉬움 · 폰 부담 보통 (폰 주의) — 화면 크기 렌더 타깃 + 반씩 줄인 흐림 5단계. 폰에서는 해상도 배율을 낮추거나 끄는 선택지를 둔다.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/u20
- 쓰면 좋을 때: 네온 · 전구 · 마법처럼 「빛나는 것」이 있는 장면 / 정답 · 홈런 순간을 화려하게
- 쓰지 말 때: 밝은 낮 장면 전체 — 하얀 벽 · 하늘까지 번져 뿌옇다. 문턱을 높이거나 빼기 / 폰 저사양 모드 — 흐림 단계가 여러 번이라 채우기 비용이 크다

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### u20 견본 항목 — `src/demos/demosLight.ts:1390`
```ts
  u20: {
    kind: '3d',
    caption: '왼쪽 끔 · 오른쪽 UnrealBloom — 문턱보다 밝은 네온 · 전구만 빛이 번져요',
    make() {
      const scene = new THREE.Scene();
      scene.background = gradTex('#141a40', '#04050c');
      basicLights(scene, 0.35, 0.9, V3(2, 4, 3));
      const root = new THREE.Group();
      const neon = (c: number, k = 4): THREE.MeshStandardMaterial => new THREE.MeshStandardMaterial({ color: c, emissive: c, emissiveIntensity: k });
      const ring = new THREE.Mesh(new THREE.TorusGeometry(1.15, 0.055, 16, 96), neon(0xff4fa8));
      const ring2 = new THREE.Mesh(new THREE.TorusGeometry(0.9, 0.04, 16, 96), neon(0x38e1ff));
      root.add(ring, ring2);
      const balls: THREE.Mesh[] = [];
      [0xffd23f, 0x3ee07a, 0x38e1ff, 0xff7a3a].forEach((c, i) => {
        const b = mesh(new THREE.SphereGeometry(0.17, 24, 16), neon(c, 3.5), 0, 0, 0);
        balls.push(b);
        root.add(b);
        void i;
      });
      const plain = mesh(new RoundedBoxGeometry(0.55, 0.55, 0.55, 3, 0.08), std(0xdfe6ff, 0.5), 0, 0, 0);
      root.add(plain);
      const floor = mesh(new THREE.CylinderGeometry(1.6, 1.6, 0.06, 64), std(0x22284a, 0.8), 0, -1.35, 0);
      scene.add(root, floor);
      const cam = new THREE.PerspectiveCamera(36, 1, 0.1, 50);
      let strength = 0.8;
      let threshold = 1.4;
      const kit = postKit((r) => {
        const bloom = new UnrealBloomPass(new THREE.Vector2(256, 256), strength, 0.4, threshold);
        return { comp: splitChain(r, scene, cam, [bloom]), bloom };
      });
      const hud = new Hud();
      hud.vline(0.5);
      hud.label('끔', 0.25, 0.95, 0.5, 1);
      hud.label('빛 번짐', 0.75, 0.95, 0.5, 1, 'on');
      let dt = 0.016;
      return {
        scene,
        camera: cam,
        update(t, d) {
          dt = d;
          ring.rotation.set(Math.sin(t * 0.5) * 0.4, t * 0.6, 0);
          ring2.rotation.set(t * 0.7, 0, Math.sin(t * 0.4) * 0.5);
          balls.forEach((b, i) => {
            const a = t * 1.2 + (i * Math.PI) / 2;
            b.position.set(Math.cos(a) * 0.75, Math.sin(a * 1.3) * 0.25, Math.sin(a) * 0.75);
          });
          plain.rotation.set(t * 0.4, t * 0.6, 0);
        },
        render(r, w, h) {
          fit(cam, w / h, V3(0, -0.15, 0), 1.6, V3(0, 0.3, 1));
          const k = kit.get(r, w, h);
          k.bloom.strength = strength;
          k.bloom.threshold = threshold;
          k.comp.render(dt);
          hud.draw(r, w, h);
        },
        controls: [
          { type: 'range', label: '번짐 세기', min: 0, max: 3, step: 0.05, value: 0.8, on: (v) => (strength = v) },
          { type: 'range', label: '문턱 (이보다 밝으면 번짐)', min: 0, max: 4, step: 0.05, value: 1.4, on: (v) => (threshold = v) },
        ],
        dispose() {
          kit.dispose();
          hud.dispose();
          disposeTree(scene);
        },
      };
    },
  }
```

## 관련 기술
- 먼저 알면 좋은 기술: [후처리 사슬 (EffectComposer → OutputPass)](https://ai-techstudio.web.app/ai/t/u21.md) `u21`
- 다음에 해 볼 기술: [빛살 (God rays)](https://ai-techstudio.web.app/ai/t/i06.md) `i06` · [색 보정 LUT · 색수차 · 필름 결](https://ai-techstudio.web.app/ai/t/i07.md) `i07`
- 참고 문서: [three.js 예제 — webgl_postprocessing_unreal_bloom](https://threejs.org/examples/#webgl_postprocessing_unreal_bloom) · [three.js 소스 — UnrealBloomPass](https://github.com/mrdoob/three.js/blob/dev/examples/jsm/postprocessing/UnrealBloomPass.js)
