# AI 꾸러미 — 디더링 화풍 (Bayer · 1비트) — Ordered dithering (Bayer matrix)
> 3D 장면을 낮은 해상도로 그려 밝기를 구한 뒤 8×8 Bayer 문턱 표와 비교해 두 색 점으로 바꿔, 흑백 점 밀도로 명암을 그리는 1비트 그림체를 만든다.  
> 견본: https://ai-techstudio.web.app/#t/i237

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

## 주문서

### 만들어 줘: 디더링 화풍 (Bayer · 1비트) — Ordered dithering (Bayer matrix)

#### 1. 목표
등불 비친 방 속 도형들을 Bayer 디더링 화풍으로 그려 줘 — 낮은 해상도로 그리고 밝기를 8×8 문턱 표와 비교해 두 색 점으로. 분위기는 1비트 옛 컴퓨터.

#### 2. 핵심 기술 용어
- **Ordered dithering (Bayer matrix)** — 정해진 문턱 표로 점 찍기 — 밝을수록 점이 많음
- **1-bit rendering** — 두 색만으로 그리기
- **Low-res render target + NearestFilter** — 작게 그려 가까운 픽셀 그대로 키우기

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

#### 4. 조건
- 장면은 낮은 해상도 렌더 타깃(Nearest 필터)에 그리고, 화면 덮는 사각형 셰이더가 점 좌표로 읽기
- 점 좌표 = floor(gl_FragCoord.xy / 점 크기) — 문턱 표도 이 좌표로
- Bayer 표는 비트 섞기 식으로 계산 (텍스처 없이)
- 방식(Bayer · 문턱만 · 4색) · 「점 크기」 1~6 · 원래 장면과 나눠 보기

#### 5. 완성 기준 (이게 보이면 성공)
- 움직이는 나눔 줄 왼쪽은 원래 장면, 오른쪽은 두 색 점 무늬로 명암이 보인다
- 「문턱만」으로 바꾸면 그러데이션이 덩어리로 뭉개져 디더의 효과가 보인다
- 「4색」은 초록 네 단계 옛 게임기 화면이 된다

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

## 원리
- 장면을 화면의 1/2 크기(점 크기 2px) 렌더 타깃에 그린다.
- 각 점의 색을 간단히 톤 매핑(1 − e^(−c·0.75))한 뒤 밝기 L 을 구하고 0.08~0.92 를 0~1 로 늘린다.
- 8×8 Bayer 표는 0~63 을 고르게 흩어 둔 문턱이다. 점 좌표의 표 값 b 보다 L 이 크면 밝은 색, 아니면 어두운 색.
- 그래서 밝기 40% 인 넓은 면은 40% 의 점만 밝게 — 멀리서 보면 회색으로 보인다.
- 문턱만(0.5) 쓰면 계단처럼 뭉개지고, 4색 모드는 floor(L·3 + b)로 단계를 고른다.

## 핵심 코드 — 8×8 Bayer 문턱 디더 (화면 덮는 사각형)
(발췌: demos/demosLook2.ts BAYER8 · makeDither() 를 정리)
```glsl
uniform sampler2D tScene; uniform vec2 uLow; uniform float uPx, uMode;
// 0..63 을 고르게 흩은 8x8 문턱 — 비트 섞기로 계산
float bayer8(vec2 p){
  ivec2 ip = ivec2(mod(p, 8.0)); int x = ip.x; int xc = ip.x ^ ip.y;
  int v = ((xc & 1) << 5) | ((x & 1) << 4) | ((xc & 2) << 2) | ((x & 2) << 1) | ((xc & 4) >> 1) | ((x & 4) >> 2);
  return (float(v) + 0.5) / 64.0;
}
void main(){
  vec2 pix = floor(gl_FragCoord.xy / uPx);                 // 점 좌표
  vec3 c = texture2D(tScene, (pix + 0.5) / uLow).rgb;      // 낮은 해상도 장면 (선형 HDR)
  c = pow(1.0 - exp(-c * 0.75), vec3(1.0 / 2.2));          // 간단한 톤 매핑 + 감마
  float L = dot(c, vec3(0.299, 0.587, 0.114));
  L = smoothstep(0.08, 0.92, L);
  float b = bayer8(pix);
  vec3 o;
  if (uMode < 0.5)      o = L > b   ? vec3(0.90, 1.0, 1.0) : vec3(0.20, 0.20, 0.10); // Bayer
  else if (uMode < 1.5) o = L > 0.5 ? vec3(0.90, 1.0, 1.0) : vec3(0.20, 0.20, 0.10); // 문턱만
  else {
    float k = clamp(floor(L * 3.0 + b), 0.0, 3.0);                                  // 4색
    o = k < 0.5 ? vec3(0.06, 0.22, 0.06) : k < 1.5 ? vec3(0.19, 0.38, 0.19) : k < 2.5 ? vec3(0.55, 0.67, 0.06) : vec3(0.61, 0.74, 0.06);
  }
  gl_FragColor = vec4(o, 1.0);
}
// JS: 렌더 타깃 = new THREE.WebGLRenderTarget(ceil(w/px), ceil(h/px), { type: THREE.HalfFloatType, minFilter: NearestFilter, magFilter: NearestFilter })
//     renderer.setRenderTarget(rt); renderer.render(scene, cam); → 화면 덮는 사각형을 그린다
```

## 흔한 실수 · 확인 목록
- [ ] **점 좌표 대신 화면 픽셀로 문턱을 고르면 점 크기가 1px 로 깨진다** — 문턱 표도 floor(gl_FragCoord / 점 크기) 로 고른다.
- [ ] **장면 그림을 선형 색 그대로 비교하면 거의 다 어둡게 나온다** — 견본처럼 간단한 톤 매핑 · 감마를 먼저 하고 비교한다.
- [ ] **카메라가 움직이면 점 무늬가 화면에 붙어 지글거린다** — 디더의 본래 성질이다. 느린 카메라 · 회상 장면에 쓴다.

## 완성 기준 체크리스트
- [ ] 움직이는 나눔 줄 왼쪽은 원래 장면, 오른쪽은 두 색 점 무늬로 명암이 보인다
- [ ] 「문턱만」으로 바꾸면 그러데이션이 덩어리로 뭉개져 디더의 효과가 보인다
- [ ] 「4색」은 초록 네 단계 옛 게임기 화면이 된다

## 이 기술 정보
- id: `i237` · 분류: 그래픽 · 셰이더 › 후처리 · 공통 · 난이도 보통 · 폰 부담 가벼움 (폰 OK) — 오히려 작은 해상도로 그려 가볍다. 화면 덮는 사각형 하나의 짧은 셰이더.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i237
- 쓰면 좋을 때: 회상 · 추리 · 옛날 느낌의 연출 장면 / 「밝기 = 점 비율」을 눈으로 보여 줄 때
- 쓰지 말 때: 색이 정보인 화면 — 두 색으로 줄어든다 / 읽어야 할 작은 글씨 — 점 무늬에 묻힌다

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### makeDither — `src/demos/demosLook2.ts:3191`
```ts
function makeDither(): Scene3D {
  const st = postStage();
  const low = new LowRT({ minFilter: THREE.NearestFilter, magFilter: THREE.NearestFilter });
  const uni = { tScene: { value: null as THREE.Texture | null }, uLow: { value: new THREE.Vector2() }, uPx: { value: 2 }, uMode: { value: 0 } };
  const q = quad(
    /* glsl */ `
    uniform sampler2D tScene; uniform vec2 uLow; uniform float uPx, uMode;
    ${BAYER8}
    void main(){
      vec2 pix = floor(gl_FragCoord.xy / uPx);
      vec3 c = texture2D(tScene, (pix + 0.5) / uLow).rgb;
      c = pow(1.0 - exp(-c * 0.75), vec3(1.0 / 2.2));
      float L = dot(c, vec3(0.299, 0.587, 0.114));
      L = smoothstep(0.08, 0.92, L);
      float b = bayer8(pix);
      vec3 o;
      if (uMode < 0.5){ o = L > b ? vec3(0.90, 1.0, 1.0) : vec3(0.20, 0.20, 0.10); }
      else if (uMode < 1.5){ o = L > 0.5 ? vec3(0.90, 1.0, 1.0) : vec3(0.20, 0.20, 0.10); }
      else {
        float k = clamp(floor(L * 3.0 + b), 0.0, 3.0);
        o = k < 0.5 ? vec3(0.06, 0.22, 0.06) : k < 1.5 ? vec3(0.19, 0.38, 0.19) : k < 2.5 ? vec3(0.55, 0.67, 0.06) : vec3(0.61, 0.74, 0.06);
      }
      gl_FragColor = vec4(o, 1.0);
    }`,
    uni,
  );
  const hud = new Hud();
  const MODES = ['1비트 Bayer 디더', '1비트 문턱만 (디더 없음)', '4색 Bayer (옛 게임기)'];
  const a = hud.add('원래 장면');
  const b = hud.add(MODES[0]!, 'rgba(60,60,30,0.85)');
  hud.at(a, 0.03, 0.05, 0, 0);
  hud.at(b, 0.97, 0.05, 1, 0);
  let compare = true;
  let time = 0;
  return {
    scene: st.scene,
    camera: q.cam,
    update(t) {
      time = t;
      st.update(t);
    },
    render(r, w, h) {
      st.cam.aspect = w / h;
      st.cam.updateProjectionMatrix();
      const px = uni.uPx.value;
      const lw = Math.ceil(w / px);
      const lh = Math.ceil(h / px);
      const rt = low.get(lw, lh);
      r.setRenderTarget(rt);
      r.render(st.scene, st.cam);
      r.setRenderTarget(null);
      uni.tScene.value = rt.texture;
      uni.uLow.value.set(lw, lh);
      const split = compare ? 0.5 + Math.sin(time * 0.4) * 0.22 : 0;
      splitPost(
        r,
        w,
        h,
        split,
        () => r.render(st.scene, st.cam),
        () => r.render(q.scene, q.cam),
      );
      if (compare) hud.at(a, 0.03, 0.05, 0, 0);
      else hud.hide(a);
      hud.set(b, MODES[uni.uMode.value]!);
      hud.draw(r, w, h);
    },
    controls: [
      { type: 'range', label: '방식 (0 Bayer · 1 문턱 · 2 4색)', min: 0, max: 2, step: 1, value: 0, on: (v) => (uni.uMode.value = v) },
      { type: 'range', label: '점 크기 (px)', min: 1, max: 6, step: 1, value: 2, on: (v) => (uni.uPx.value = v) },
      { type: 'toggle', label: '원래 장면과 나눠 보기', value: true, on: (v) => (compare = v) },
    ],
    dispose() {
      st.dispose();
      low.dispose();
      q.dispose();
      hud.dispose();
    },
  };
}
```

### i237 견본 항목 — `src/demos/demosLook2.ts:4521`
```ts
  i237: { kind: '3d', caption: '3D 장면을 낮은 해상도로 그려 밝기 → 8×8 Bayer 문턱으로 흑백 점 (오브라 딘 느낌) · 4색 모드', make: () => makeDither() }
```

## 관련 기술
- 먼저 알면 좋은 기술: [후처리 사슬 (EffectComposer → OutputPass)](https://ai-techstudio.web.app/ai/t/u21.md) `u21` · [만화 망점 · 해칭 셰이더](https://ai-techstudio.web.app/ai/t/i11.md) `i11`
- 다음에 해 볼 기술: [아스키 렌더](https://ai-techstudio.web.app/ai/t/i238.md) `i238` · [저해상 3D 도트 + 1px 외곽선](https://ai-techstudio.web.app/ai/t/i239.md) `i239`
- 참고 문서: [Wikipedia — Ordered dithering](https://en.wikipedia.org/wiki/Ordered_dithering)
