# AI 꾸러미 — 아스키 렌더 — ASCII art shader
> 3D 장면을 글자 칸 크기로 작게 그려 칸마다 밝기를 글자 사다리( . : - = + * # % @ )로 바꾸고, 밝기 경계는 기울기 방향에 맞춘 사선 글자로 그려 장면을 글자로 보여 준다.  
> 견본: https://ai-techstudio.web.app/#t/i238

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

## 주문서

### 만들어 줘: 아스키 렌더 — ASCII art shader

#### 1. 목표
등불 비친 방 속 도형들을 아스키 렌더로 그려 줘 — 칸마다 밝기를 글자로, 밝기 경계는 방향에 맞는 사선 글자로. 분위기는 초록 단말기.

#### 2. 핵심 기술 용어
- **ASCII art shader** — 아스키 렌더 — 밝기를 글자로
- **Glyph atlas** — 글자 그림 한 줄 (16칸)
- **Sobel gradient** — 이웃 밝기 차이로 경계 방향 찾기

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

#### 4. 조건
- 장면 렌더 타깃 크기 = ceil(화면 / 칸 크기), Nearest 필터
- 글자 아틀라스는 밉맵 끄고 LinearFilter, 밝기 순서로 글자 배치
- 칸 크기는 화면 높이에 비례 (max(5, round(칸 × max(1, 높이/400))))
- 경계 글자 켜고 끄기 · 장면 색 / 초록 단말기 · 「글자 칸 크기」 5~18

#### 5. 완성 기준 (이게 보이면 성공)
- 나눔 줄 오른쪽에서 도형들이 글자로 그려지고, 밝은 면은 # % @, 어두운 곳은 . : 로 보인다
- 도형 윤곽에는 기울기 방향에 맞는 / \ | - 가 이어져 선처럼 보인다
- 「장면 색」을 끄면 초록 단말기 글자가 된다

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

## 원리
- 장면을 화면 ÷ 칸 크기(10px) 해상도로 작게 그린다 — 한 픽셀이 글자 한 칸이다.
- 글자 그림(아틀라스)에 빈칸부터 @ 까지 밝기 순서로 10글자, 그 뒤에 | / - \ 경계 글자 4개를 한 줄로 그려 둔다.
- 칸 밝기 L × 1.15 를 10단계로 끊어 글자 번호를 고르고, 칸 안 위치로 그 글자 그림을 읽는다.
- 이웃 8칸의 소벨 기울기가 0.55 를 넘으면 경계 — 기울기 각도를 4방향으로 끊어 경계 글자로 바꾼다.
- 글자 색은 장면 색을 밝게 하거나 초록 단말기 색으로.

## 핵심 코드 — 밝기 → 글자, 경계 → 사선 글자
(발췌: demos/demosLook2.ts glyphAtlas() · makeAscii() 의 셰이더)
```glsl
// 아틀라스 한 줄 16칸: ' ' . : - = + * # % @ | / - (역슬래시)  (0~9 밝기, 10~13 경계)
uniform sampler2D tScene, tGlyph; uniform vec2 uLow; uniform float uCell, uEdge, uColor;
vec3 S(vec2 cell){ vec3 c = texture2D(tScene, (cell + 0.5) / uLow).rgb; return pow(1.0 - exp(-c * 0.8), vec3(1.0 / 2.2)); }
float Lm(vec2 cell){ return dot(S(cell), vec3(0.299, 0.587, 0.114)); }
void main(){
  vec2 cell = floor(gl_FragCoord.xy / uCell);
  vec2 inC = fract(gl_FragCoord.xy / uCell);            // 칸 안 위치
  vec3 c = S(cell);
  float L = dot(c, vec3(0.299, 0.587, 0.114));
  float gi = floor(clamp(L * 1.15, 0.0, 0.999) * 10.0); // 밝기 → 글자 번호 0~9
  if (uEdge > 0.5){
    float tl = Lm(cell + vec2(-1, 1)), t = Lm(cell + vec2(0, 1)), tr = Lm(cell + vec2(1, 1));
    float l = Lm(cell + vec2(-1, 0)), r = Lm(cell + vec2(1, 0));
    float bl = Lm(cell + vec2(-1, -1)), b = Lm(cell + vec2(0, -1)), br = Lm(cell + vec2(1, -1));
    float gx = (tr + 2.0 * r + br) - (tl + 2.0 * l + bl);   // 소벨
    float gy = (tl + 2.0 * t + tr) - (bl + 2.0 * b + br);
    if (length(vec2(gx, gy)) > 0.55){
      float a = mod(atan(gy, gx) + 3.14159265 / 8.0, 3.14159265);
      float k = floor(a / (3.14159265 / 4.0));                // 4 방향
      gi = k < 0.5 ? 10.0 : k < 1.5 ? 13.0 : k < 2.5 ? 12.0 : 11.0;
    }
  }
  float gl = texture2D(tGlyph, vec2((gi + inC.x) / 16.0, inC.y)).r;
  vec3 tint = uColor > 0.5 ? clamp(c * 1.7 + 0.18, 0.0, 1.0) : vec3(0.35, 1.0, 0.55);
  gl_FragColor = vec4(vec3(0.02, 0.03, 0.04) + c * 0.16 + tint * gl, 1.0);
}
```

## 흔한 실수 · 확인 목록
- [ ] **글자 아틀라스에 밉맵을 켜면 글자가 흐려진다** — generateMipmaps = false · minFilter = LinearFilter.
- [ ] **글꼴마다 글자 굵기가 달라 밝기 순서가 어긋난다** — 굵은 고정폭 글꼴(900 Consolas 류)로 그리고, 순서는 눈으로 확인해 맞춘다.
- [ ] **경계 문턱이 낮으면 화면 전체가 사선 글자로 덮인다** — 견본 문턱 0.55. 장면 대비가 약하면 올린다.

## 완성 기준 체크리스트
- [ ] 나눔 줄 오른쪽에서 도형들이 글자로 그려지고, 밝은 면은 # % @, 어두운 곳은 . : 로 보인다
- [ ] 도형 윤곽에는 기울기 방향에 맞는 / \ | - 가 이어져 선처럼 보인다
- [ ] 「장면 색」을 끄면 초록 단말기 글자가 된다

## 이 기술 정보
- id: `i238` · 분류: 그래픽 · 셰이더 › 후처리 · 공통 · 난이도 보통 · 폰 부담 가벼움 (폰 OK) — 작은 해상도로 그리고, 화면 셰이더가 칸마다 장면 9번 · 글자 1번 읽기.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i238
- 쓰면 좋을 때: 해독 · 비밀 연출 · 레트로 단말기 화면 / 숫자 · 글자로만 이루어진 세계 같은 그림체
- 쓰지 말 때: 작은 물체 · 세밀한 장면 — 한 칸에 뭉개진다. 크고 단순한 도형 장면에 / 폰 작은 화면 — 칸이 작으면 글자가 안 읽힌다 (칸 크기를 화면 높이에 비례)

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### makeAscii — `src/demos/demosLook2.ts:3290`
```ts
function makeAscii(): Scene3D {
  const st = postStage();
  const low = new LowRT({ minFilter: THREE.NearestFilter, magFilter: THREE.NearestFilter });
  const atlas = glyphAtlas();
  const uni = { tScene: { value: null as THREE.Texture | null }, tGlyph: { value: atlas }, uLow: { value: new THREE.Vector2() }, uCell: { value: 10 }, uEdge: { value: 1 }, uColor: { value: 1 } };
  const q = quad(
    /* glsl */ `
    uniform sampler2D tScene, tGlyph; uniform vec2 uLow; uniform float uCell, uEdge, uColor;
    vec3 S(vec2 cell){ vec3 c = texture2D(tScene, (cell + 0.5) / uLow).rgb; return pow(1.0 - exp(-c * 0.8), vec3(1.0 / 2.2)); }
    float Lm(vec2 cell){ return dot(S(cell), vec3(0.299, 0.587, 0.114)); }
    void main(){
      vec2 cell = floor(gl_FragCoord.xy / uCell);
      vec2 inC = fract(gl_FragCoord.xy / uCell);
      vec3 c = S(cell);
      float L = dot(c, vec3(0.299, 0.587, 0.114));
      float gi = floor(clamp(L * 1.15, 0.0, 0.999) * 10.0);
      if (uEdge > 0.5){
        float tl = Lm(cell + vec2(-1, 1)), t = Lm(cell + vec2(0, 1)), tr = Lm(cell + vec2(1, 1));
        float l = Lm(cell + vec2(-1, 0)), r = Lm(cell + vec2(1, 0));
        float bl = Lm(cell + vec2(-1, -1)), b = Lm(cell + vec2(0, -1)), br = Lm(cell + vec2(1, -1));
        float gx = (tr + 2.0 * r + br) - (tl + 2.0 * l + bl);
        float gy = (tl + 2.0 * t + tr) - (bl + 2.0 * b + br);
        if (length(vec2(gx, gy)) > 0.55){
          float a = mod(atan(gy, gx) + 3.14159265 / 8.0, 3.14159265);
          float k = floor(a / (3.14159265 / 4.0));
          gi = k < 0.5 ? 10.0 : k < 1.5 ? 13.0 : k < 2.5 ? 12.0 : 11.0;
        }
      }
      float gl = texture2D(tGlyph, vec2((gi + inC.x) / 16.0, inC.y)).r;
      vec3 tint = uColor > 0.5 ? clamp(c * 1.7 + 0.18, 0.0, 1.0) : vec3(0.35, 1.0, 0.55);
      vec3 o = vec3(0.02, 0.03, 0.04) + c * 0.16 + tint * gl;
      gl_FragColor = vec4(o, 1.0);
    }`,
    uni,
  );
  const hud = new Hud();
  const a = hud.add('원래 장면');
  const b = hud.add('아스키 — 밝기 → 글자, 윤곽 → / \\ | -', 'rgba(10,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 cs = Math.max(5, Math.round(uni.uCell.value * Math.max(1, h / 400)));
      const lw = Math.ceil(w / cs);
      const lh = Math.ceil(h / cs);
      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 save = uni.uCell.value;
      uni.uCell.value = cs;
      const split = compare ? 0.32 + Math.sin(time * 0.4) * 0.12 : 0;
      splitPost(
        r,
        w,
        h,
        split,
        () => r.render(st.scene, st.cam),
        () => r.render(q.scene, q.cam),
      );
      uni.uCell.value = save;
      if (compare) hud.at(a, 0.03, 0.05, 0, 0);
      else hud.hide(a);
      hud.draw(r, w, h);
    },
    controls: [
      { type: 'range', label: '글자 칸 크기', min: 5, max: 18, step: 1, value: 10, on: (v) => (uni.uCell.value = v) },
      { type: 'toggle', label: '윤곽 글자 (/ \\ | -)', value: true, on: (v) => (uni.uEdge.value = v ? 1 : 0) },
      { type: 'toggle', label: '장면 색 (끄면 초록 단말기)', value: true, on: (v) => (uni.uColor.value = v ? 1 : 0) },
      { type: 'toggle', label: '원래 장면과 나눠 보기', value: true, on: (v) => (compare = v) },
    ],
    dispose() {
      st.dispose();
      low.dispose();
      q.dispose();
      atlas.dispose();
      hud.dispose();
    },
  };
}
```

### i238 견본 항목 — `src/demos/demosLook2.ts:4522`
```ts
  i238: { kind: '3d', caption: '칸마다 밝기를 글자 사다리(. : = + # @)로, 밝기 경계는 기울기 방향 따라 / \\ | - 로', make: () => makeAscii() }
```

## 관련 기술
- 먼저 알면 좋은 기술: [디더링 화풍 (Bayer · 1비트)](https://ai-techstudio.web.app/ai/t/i237.md) `i237`
- 다음에 해 볼 기술: [저해상 3D 도트 + 1px 외곽선](https://ai-techstudio.web.app/ai/t/i239.md) `i239`
- 참고 문서: [Wikipedia — ASCII art](https://en.wikipedia.org/wiki/ASCII_art)
