# AI 꾸러미 — 계단 줄이기 (SMAA · FXAA · 다중 샘플) — Anti-aliasing (SMAA · FXAA · MSAA)
> 후처리를 쓰면 꺼지는 기본 안티앨리어싱 대신 SMAA 단계를 더해, 가는 바퀴살 · 상자 모서리의 계단을 다시 매끈하게 다듬는다.  
> 견본: https://ai-techstudio.web.app/#t/i04

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

## 주문서

### 만들어 줘: 계단 줄이기 (SMAA · FXAA · 다중 샘플) — Anti-aliasing (SMAA · FXAA · MSAA)

#### 1. 목표
가는 바퀴살 · 상자 모서리에 SMAA 계단 줄이기를 넣어 줘 — 후처리 사슬에서 꺼진 안티앨리어싱을 되살리기. 분위기는 선명하고 깔끔한.

#### 2. 핵심 기술 용어
- **Anti-aliasing (SMAA · FXAA · MSAA)** — 계단 줄이기 — 경계 다듬기 · 여러 번 샘플
- **SMAAPass** — three.js 형태 인식 경계 다듬기 후처리
- **Aliasing / jaggies** — 대각선 · 가는 선의 톱니 계단

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

#### 4. 조건
- 후처리 사슬에 SMAAPass 를 OutputPass 근처 끝에 (경계 찾기는 색이 다 정해진 뒤가 정확)
- composer.setSize 때 SMAA 도 같이 크기가 바뀌는지 확인 (composer 가 해 준다)
- 확대 보기로 전/후를 비교 — 왼쪽 AA 없음(계단) · 오른쪽 SMAA
- 「확대 배율」 1~6 슬라이더

#### 5. 완성 기준 (이게 보이면 성공)
- ×3 확대에서 왼쪽 바퀴살 · 상자 모서리는 톱니 계단, 오른쪽은 경계가 매끈하다
- 바퀴가 천천히 돌 때 왼쪽은 살이 지글거리고 오른쪽은 덜 지글거린다
- 확대 1 에서도 차이가 보인다

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

## 원리
- WebGLRenderer({ antialias: true }) 는 화면에 바로 그릴 때만 MSAA 가 된다. EffectComposer 의 렌더 타깃에 그리면 꺼진다.
- SMAA 는 다 그린 그림에서 경계 모양(가로 · 세로 · 대각선)을 찾아, 그 방향으로 이웃 색을 조금 섞어 계단을 지운다.
- 세 단계(경계 찾기 → 섞을 비율 → 섞기)라 FXAA 보다 조금 비싸지만 글씨 · 가는 선이 덜 뭉개진다.
- 견본은 확대해서(×3) 차이를 보이게 한다: 낮은 해상도로 그린 뒤 가까운 픽셀 그대로 키운다.

## 핵심 코드 — 후처리 사슬에 SMAA 더하기
(발췌: demos/demosLight.ts i04 make() 를 정리)
```ts
import { SMAAPass } from 'three/examples/jsm/postprocessing/SMAAPass.js';

const comp = new EffectComposer(renderer);   // 렌더 타깃에 그리므로 antialias:true 의 MSAA 가 꺼진다
comp.addPass(new RenderPass(scene, cam));
// ... 빛 번짐 · 색 보정 같은 다른 단계
comp.addPass(new SMAAPass());                // 경계 찾기 → 비율 → 섞기
comp.addPass(new OutputPass());

window.addEventListener('resize', () => comp.setSize(innerWidth, innerHeight));
function frame(dt: number) { comp.render(dt); }

// 견본의 확대 보기: 낮은 해상도 lw × lh 로 그린 뒤 가까운 픽셀 그대로 키우기
// 'ivec2 p = ivec2(clamp(floor(vUv * lowRes), vec2(0.0), lowRes - 1.0)); gl_FragColor = texelFetch(tDiffuse, p, 0);'
```

## 흔한 실수 · 확인 목록
- [ ] **composer 를 쓰는 순간 renderer 의 antialias 가 소용없어진다** — 후처리 16 게임 공통 문제. SMAA 를 더하거나, 렌더 타깃을 samples: 4 로 만들어 composer 에 넘긴다 (공용 후처리는 MSAA 4).
- [ ] **SMAA 를 빛 번짐 앞에 두면 번짐이 다시 계단을 만든다** — 사슬 끝 쪽(OutputPass 근처)에 둔다.
- [ ] **FXAA 는 글씨 · 가는 선까지 뭉갠다** — 글씨가 많은 화면은 SMAA 나 MSAA 가 낫다.

## 완성 기준 체크리스트
- [ ] ×3 확대에서 왼쪽 바퀴살 · 상자 모서리는 톱니 계단, 오른쪽은 경계가 매끈하다
- [ ] 바퀴가 천천히 돌 때 왼쪽은 살이 지글거리고 오른쪽은 덜 지글거린다
- [ ] 확대 1 에서도 차이가 보인다

## 이 기술 정보
- id: `i04` · 분류: 그래픽 · 셰이더 › 후처리 · 3D · 난이도 쉬움 · 폰 부담 가벼움 (폰 OK) — 화면 전체 3단계 가벼운 패스. MSAA(렌더 타깃 samples 4)보다 메모리가 적다.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i04
- 쓰면 좋을 때: EffectComposer 로 후처리를 하나라도 쓸 때 (16 게임) / 툰 외곽선 · 가는 선이 계단져 보일 때
- 쓰지 말 때: 후처리 없이 renderer 로 바로 그리는 장면 — antialias: true 면 이미 MSAA 다 / 폰에서 고해상도 + MSAA 와 겹쳐 — 둘 중 하나만

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### i04 견본 항목 — `src/demos/demosLight.ts:1847`
```ts
  i04: {
    kind: '3d',
    caption: '확대해서 본 가장자리 — 왼쪽은 안티앨리어싱이 꺼진 계단, 오른쪽 SMAA 는 경계를 매끈하게 다듬어요',
    make() {
      const scene = new THREE.Scene();
      scene.background = new THREE.Color(0x10163a);
      basicLights(scene, 0.8, 1.8, V3(-2, 3, 4));
      const wheel = new THREE.Group();
      const spokeM = std(0xffffff, 0.4);
      for (let i = 0; i < 10; i++) {
        const s = mesh(new THREE.BoxGeometry(0.035, 2.0, 0.035), spokeM);
        s.rotation.z = (i / 10) * Math.PI;
        wheel.add(s);
      }
      wheel.add(new THREE.Mesh(new THREE.TorusGeometry(1.0, 0.03, 8, 80), std(0xffd23f, 0.4)));
      const cube = mesh(new THREE.BoxGeometry(0.7, 0.7, 0.7), std(0xff5d73, 0.5));
      scene.add(wheel, cube);
      const cam = new THREE.PerspectiveCamera(36, 1, 0.1, 30);
      let zoom = 3;
      let lw = 1;
      let lh = 1;
      const UP = new THREE.ShaderMaterial({
        uniforms: { tDiffuse: { value: null }, lowRes: { value: new THREE.Vector2(1, 1) } },
        vertexShader: VERT,
        fragmentShader:
          'uniform sampler2D tDiffuse; uniform vec2 lowRes; varying vec2 vUv; void main(){ ivec2 p = ivec2(clamp(floor(vUv * lowRes), vec2(0.0), lowRes - 1.0)); gl_FragColor = texelFetch(tDiffuse, p, 0); }',
      });
      const kit = postKit((r) => {
        const comp = new EffectComposer(r);
        comp.setPixelRatio(1);
        comp.addPass(new RenderPass(scene, cam));
        const save = new SavePass();
        comp.addPass(save);
        const smaa = new SMAAPass();
        comp.addPass(smaa);
        comp.addPass(splitPass(save.renderTarget.texture));
        comp.addPass(new OutputPass());
        const up = new ShaderPass(UP);
        comp.addPass(up);
        return { comp, up };
      });
      const hud = new Hud();
      hud.vline(0.5);
      hud.label('AA 없음 (계단)', 0.25, 0.95, 0.5, 1);
      hud.label('SMAA', 0.75, 0.95, 0.5, 1, 'on');
      const zl = hud.label('×3 확대', 0.5, 0.04, 0.5, 0, 'tag', 0.8);
      let dt = 0.016;
      return {
        scene,
        camera: cam,
        update(t, d) {
          dt = d;
          wheel.rotation.z = t * 0.25;
          wheel.rotation.y = Math.sin(t * 0.3) * 0.4;
          cube.rotation.set(t * 0.35, t * 0.5, 0.3);
        },
        render(r, w, h) {
          fit(cam, w / h, V3(0, 0, 0), 1.15, V3(0, 0, 1));
          const z = Math.max(1, zoom * Math.max(0.6, Math.min(1.6, h / 350)));
          lw = Math.max(16, Math.ceil(w / z));
          lh = Math.max(16, Math.ceil(h / z));
          const k = kit.get(r, lw, lh);
          (k.up.uniforms['lowRes']!.value as THREE.Vector2).set(lw, lh);
          zl.set(`×${Math.round(z * 10) / 10} 확대`);
          k.comp.render(dt);
          r.setViewport(0, 0, w, h);
          hud.draw(r, w, h);
        },
        controls: [{ type: 'range', label: '확대 배율', min: 1, max: 6, step: 0.5, value: 3, on: (v) => (zoom = v) }],
        dispose() {
          kit.dispose();
          hud.dispose();
          disposeTree(scene);
        },
      };
    },
  }
```

## 관련 기술
- 먼저 알면 좋은 기술: [후처리 사슬 (EffectComposer → OutputPass)](https://ai-techstudio.web.app/ai/t/u21.md) `u21`
- 다음에 해 볼 기술: [후처리 외곽선 (깊이 · 법선 경계)](https://ai-techstudio.web.app/ai/t/i03.md) `i03`
- 참고 문서: [three.js 예제 — webgl_postprocessing_smaa](https://threejs.org/examples/#webgl_postprocessing_smaa) · [three.js 소스 — SMAAPass](https://github.com/mrdoob/three.js/blob/dev/examples/jsm/postprocessing/SMAAPass.js)
