# AI 꾸러미 — 후처리 사슬 (EffectComposer → OutputPass) — Post-processing chain (EffectComposer)
> 장면을 바로 화면에 그리지 않고 그림으로 받아 빛 번짐 · 색 보정 같은 단계를 차례로 거친 뒤 마지막에 sRGB 로 바꿔 내보내는 후처리의 기본 틀.  
> 견본: https://ai-techstudio.web.app/#t/u21

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

## 주문서

### 만들어 줘: 후처리 사슬 (EffectComposer → OutputPass) — Post-processing chain (EffectComposer)

#### 1. 목표
3D 게임 장면 전체에 후처리 사슬을 짜 줘 — 그리기 → 빛 번짐 → 색 보정 → 출력(sRGB) 순서로, 단계마다 켜고 끌 수 있게. 분위기는 따뜻하고 화사한.

#### 2. 핵심 기술 용어
- **Post-processing chain (EffectComposer)** — 후처리 사슬 — 그림을 단계별로 손보기
- **RenderPass · ShaderPass · OutputPass** — 그리기 · 셰이더 한 단계 · 출력(톤 매핑 + sRGB)
- **Linear vs sRGB color** — 계산은 선형 색, 화면에 낼 때 sRGB

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

#### 4. 조건
- 순서: RenderPass → 효과들 → OutputPass (OutputPass 는 반드시 맨 끝, 하나만)
- composer.render(dt) 로 그리기 (renderer.render 를 따로 부르지 않기)
- 렌더러가 바뀌면 composer 를 새로, 화면 크기가 바뀌면 setSize
- 직접 만든 색 보정은 ShaderPass(tDiffuse 를 받는 ShaderMaterial)로
- 단계를 하나씩 켜 보이며 무엇이 바뀌는지 글로 보여 주기

#### 5. 완성 기준 (이게 보이면 성공)
- ① 그리기만: 어둡고 칙칙 → ② 빛 번짐: 별 · 볼이 빛남 → ③ 색 보정: 따뜻하고 가장자리 어둡게 → ④ 출력: 화면에 알맞은 밝기 — 1.8초마다 차례로 바뀐다
- 「단계 고정」 슬라이더로 한 단계에 멈춰 볼 수 있다
- 사슬을 다 켠 결과가 렌더러로 바로 그린 것보다 색이 어긋나지 않는다

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

## 원리
- RenderPass 가 장면을 화면 대신 그림(렌더 타깃)에 그린다. 이 그림은 아직 선형 색이라 어둡고 칙칙해 보인다.
- 다음 단계들은 앞 그림(tDiffuse)을 받아 손본 그림을 넘긴다 — 빛 번짐, 직접 짠 색 보정(ShaderPass) 등.
- 맨 끝 OutputPass 가 톤 매핑과 sRGB 변환을 해서 화면에 알맞은 색으로 낸다.
- 단계마다 pass.enabled 로 켜고 끌 수 있다.

## 핵심 코드 — 그리기 → 빛 번짐 → 색 보정 → 출력
(발췌: demos/demosLight.ts u21 make() 를 정리)
```ts
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';
import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js';
import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js';
import { OutputPass } from 'three/examples/jsm/postprocessing/OutputPass.js';

// 색 보정: 따뜻하게 · 채도 1.25 · 가장자리 어둡게
const GRADE = new THREE.ShaderMaterial({
  uniforms: { tDiffuse: { value: null } },
  vertexShader: 'varying vec2 vUv; void main(){ vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }',
  fragmentShader: 'uniform sampler2D tDiffuse; varying vec2 vUv; void main(){ vec4 c = texture2D(tDiffuse, vUv);' +
    ' c.rgb *= vec3(1.12, 1.0, 0.86); float l = dot(c.rgb, vec3(0.3,0.59,0.11)); c.rgb = mix(vec3(l), c.rgb, 1.25);' +
    ' float v = smoothstep(0.85, 0.3, length(vUv - 0.5)); c.rgb *= mix(0.35, 1.0, v); gl_FragColor = c; }',
});

const comp = new EffectComposer(renderer);
comp.addPass(new RenderPass(scene, camera));                 // ① 그리기 (선형 색)
const bloom = new UnrealBloomPass(new THREE.Vector2(256, 256), 0.8, 0.35, 1.1);
comp.addPass(bloom);                                         // ② 빛 번짐
const grade = new ShaderPass(GRADE);
comp.addPass(grade);                                         // ③ 색 보정
comp.addPass(new OutputPass());                              // ④ 톤 매핑 + sRGB — 맨 끝

function frame(dt: number) {
  bloom.enabled = true;   // 단계마다 켜고 끄기
  grade.enabled = true;
  comp.render(dt);
}
window.addEventListener('resize', () => comp.setSize(innerWidth, innerHeight));
```

## 흔한 실수 · 확인 목록
- [ ] **OutputPass 를 빼거나 중간에 두면 색이 어둡거나 두 번 밝아진다** — 중간 단계는 선형 색이다. sRGB 변환은 사슬 맨 끝 한 번만.
- [ ] **composer 를 쓰면 기본 안티앨리어싱(MSAA)이 꺼진다** — 렌더 타깃 samples 를 주거나 SMAA 패스(i04)를 더한다.
- [ ] **깊이를 다시 그리는 단계(구석 그늘 등)에서 반투명 · 스프라이트가 검은 네모가 된다** — 이 사이트 공용 후처리(shared/post.ts)는 깊이 패스 동안 반투명 · 스프라이트를 숨긴다.
- [ ] **가장자리 어둡게(비네트) · 심도로 얼버무리면 답답해 보인다** — 사용자 결정 — 비네트 · 심도 · 가장자리 흐림은 빼고, 빛 배치로 해결한다.

## 완성 기준 체크리스트
- [ ] ① 그리기만: 어둡고 칙칙 → ② 빛 번짐: 별 · 볼이 빛남 → ③ 색 보정: 따뜻하고 가장자리 어둡게 → ④ 출력: 화면에 알맞은 밝기 — 1.8초마다 차례로 바뀐다
- [ ] 「단계 고정」 슬라이더로 한 단계에 멈춰 볼 수 있다
- [ ] 사슬을 다 켠 결과가 렌더러로 바로 그린 것보다 색이 어긋나지 않는다

## 이 기술 정보
- id: `u21` · 분류: 그래픽 · 셰이더 › 후처리 · 3D · 난이도 쉬움 · 폰 부담 보통 (폰 주의) — 단계마다 화면 전체 그리기 한 번. 3~4단계는 PC OK, 폰은 단계를 줄이거나 해상도 배율을 낮춘다.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/u21
- 쓰면 좋을 때: 빛 번짐 · 구석 그늘 · 색 보정을 둘 이상 겹쳐 쓸 때 / 게임마다 같은 틀에 세기만 바꿔 쓰고 싶을 때 (이 사이트 shared/post.ts)
- 쓰지 말 때: 효과가 하나도 없는 장면 — renderer.render 가 더 가볍다 / 폰에서 단계를 많이 — 단계마다 화면 전체를 한 번씩 더 그린다

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### u21 견본 항목 — `src/demos/demosLight.ts:1460`
```ts
  u21: {
    kind: '3d',
    caption: '그리기 → 빛 번짐 → 색 보정 → 출력(sRGB) — 단계가 하나씩 켜지며 화면이 손질돼요 (출력 전엔 선형 색이라 어둡게)',
    make() {
      const scene = new THREE.Scene();
      scene.background = gradTex('#5b7bd8', '#1a2350');
      basicLights(scene, 0.8, 1.8, V3(-2, 4, 3));
      const ch = cutie(0xff8fb1);
      const base = mesh(new THREE.CylinderGeometry(1.2, 1.25, 0.15, 48), std(0x9fd77a, 0.8), 0, -0.075, 0);
      const stars: THREE.Mesh[] = [];
      const sg = starGeo(0.12, 0.055, 0.05);
      const sm = new THREE.MeshStandardMaterial({ color: 0xffe066, emissive: 0xffc020, emissiveIntensity: 2.2 });
      for (let i = 0; i < 5; i++) {
        const s = new THREE.Mesh(sg, sm);
        stars.push(s);
        scene.add(s);
      }
      scene.add(ch, base);
      const cam = new THREE.PerspectiveCamera(34, 1, 0.1, 50);
      const GRADE = new THREE.ShaderMaterial({
        uniforms: { tDiffuse: { value: null } },
        vertexShader: VERT,
        fragmentShader:
          'uniform sampler2D tDiffuse; varying vec2 vUv; void main(){ vec4 c = texture2D(tDiffuse, vUv); c.rgb *= vec3(1.12, 1.0, 0.86); float l = dot(c.rgb, vec3(0.3,0.59,0.11)); c.rgb = mix(vec3(l), c.rgb, 1.25); float v = smoothstep(0.85, 0.3, length(vUv - 0.5)); c.rgb *= mix(0.35, 1.0, v); gl_FragColor = c; }',
      });
      const kit = postKit((r) => {
        const comp = new EffectComposer(r);
        comp.setPixelRatio(1);
        comp.addPass(new RenderPass(scene, cam));
        const bloom = new UnrealBloomPass(new THREE.Vector2(256, 256), 0.8, 0.35, 1.1);
        const grade = new ShaderPass(GRADE.clone());
        const output = new OutputPass();
        const copy = new ShaderPass(CopyShader);
        comp.addPass(bloom);
        comp.addPass(grade);
        comp.addPass(output);
        comp.addPass(copy);
        return { comp, bloom, grade, output, copy };
      });
      const hud = new Hud();
      const names = ['① 그리기', '② 빛 번짐', '③ 색 보정', '④ 출력'];
      const steps = names.map((s, i) => hud.label(s, (i + 0.5) / 4, 0.04, 0.5, 0, 'off', 0.85));
      const what = hud.label('', 0.5, 0.96, 0.5, 1, 'tag');
      const notes = ['장면만 그린 선형 색 — 어둡고 칙칙', '밝은 별 · 볼이 빛나요', '따뜻한 색 · 가장자리 어둡게', 'sRGB 로 바꿔 화면에 알맞게'];
      let stage = -1;
      let manual = -1;
      let dt = 0.016;
      return {
        scene,
        camera: cam,
        update(t, d) {
          dt = d;
          const st = manual >= 0 ? manual : Math.floor(t / 1.8) % 4;
          if (st !== stage) {
            stage = st;
            steps.forEach((l, i) => l.set(names[i]!, i <= st ? (i === st ? 'on' : 'blue') : 'off'));
            what.set(notes[st]!);
          }
          ch.rotation.y = Math.sin(t * 0.8) * 0.7;
          ch.position.y = Math.abs(Math.sin(t * 2.2)) * 0.1;
          stars.forEach((s, i) => {
            const a = t * 0.8 + (i / 5) * Math.PI * 2;
            s.position.set(Math.cos(a) * 1.05, 1.15 + Math.sin(a * 2) * 0.12, Math.sin(a) * 0.8 - 0.2);
            s.rotation.y = t * 2 + i;
          });
        },
        render(r, w, h) {
          fit(cam, w / h, V3(0, 0.5, 0), 1.55, V3(0, 0.35, 1));
          const k = kit.get(r, w, h);
          k.bloom.enabled = stage >= 1;
          k.grade.enabled = stage >= 2;
          k.output.enabled = stage >= 3;
          k.copy.enabled = stage < 3;
          k.comp.render(dt);
          hud.draw(r, w, h);
        },
        controls: [{ type: 'range', label: '단계 고정 (0 = 자동)', min: 0, max: 4, step: 1, value: 0, on: (v) => (manual = v - 1) }],
        dispose() {
          kit.dispose();
          GRADE.dispose();
          hud.dispose();
          disposeTree(scene);
        },
      };
    },
  }
```

## 관련 기술
- 다음에 해 볼 기술: [빛 번짐 (UnrealBloom)](https://ai-techstudio.web.app/ai/t/u20.md) `u20` · [구석 그늘 (SSAO / GTAO)](https://ai-techstudio.web.app/ai/t/i02.md) `i02` · [계단 줄이기 (SMAA · FXAA · 다중 샘플)](https://ai-techstudio.web.app/ai/t/i04.md) `i04` · [색 보정 LUT · 색수차 · 필름 결](https://ai-techstudio.web.app/ai/t/i07.md) `i07`
- 참고 문서: [three.js 매뉴얼 — How to use post-processing](https://threejs.org/manual/#en/how-to-use-post-processing) · [three.js 소스 — EffectComposer](https://github.com/mrdoob/three.js/blob/dev/examples/jsm/postprocessing/EffectComposer.js)
