# AI 꾸러미 — SDF 글자 빛 · 테
> 아무리 키워도 매끈한 글자 + 테 · 빛무리  
> 견본: https://ai-techstudio.web.app/#t/i242

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

## 주문서

### 만들어 줘: SDF 글자 빛 · 테

#### 1. 목표
3D 위 큰 숫자에 「SDF 글자 빛 · 테」 기술을 깔끔한 기본 느낌으로 넣어 줘. — 아무리 키워도 매끈한 글자 + 테 · 빛무리

#### 2. 핵심 기술 용어
- **SDF**

#### 3. 환경
- 플랫폼: TypeScript (브라우저), 라이브러리 없이 — 화면과 떨어진 순수 함수로
- 화면: 브라우저 — PC · 폰 모두

#### 4. 조건
- 화면 코드와 계산 코드를 나눠서 (계산은 순수 함수)
- 한 프레임 16ms 안 — 오래 걸리는 계산은 나눠서

#### 5. 완성 기준 (이게 보이면 성공)
- 견본처럼 「아무리 키워도 매끈한 글자 + 테 · 빛무리」 이(가) 첫 화면에서 바로 보인다
- 켬/끔(또는 전/후) 비교 단추로 이 기술이 만드는 차이를 확인할 수 있다
- 중요한 값 1~3개를 슬라이더로 바꿔 볼 수 있다
- 콘솔 오류 0, 폰 폭(390px)에서도 동작한다

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

## 원리
- 아무리 키워도 매끈한 글자 + 테 · 빛무리


## 흔한 실수 · 확인 목록
- (아직 정리된 것이 없어요)

## 완성 기준 체크리스트
- [ ] 견본처럼 「아무리 키워도 매끈한 글자 + 테 · 빛무리」 이(가) 첫 화면에서 바로 보인다
- [ ] 켬/끔(또는 전/후) 비교 단추로 이 기술이 만드는 차이를 확인할 수 있다
- [ ] 중요한 값 1~3개를 슬라이더로 바꿔 볼 수 있다
- [ ] 콘솔 오류 0, 폰 폭(390px)에서도 동작한다

## 이 기술 정보
- id: `i242` · 분류: 그래픽 · 셰이더 › 재질 · 그림체 · 공통 · 난이도 보통 · 폰 부담 보통 (폰 주의)
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i242
- 쓰면 좋을 때: 쓰면 좋을 곳 — 3D 위 큰 숫자 · 점수판

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### makeSdf — `src/demos/demosLook2.ts:4058`
```ts
function makeSdf(): Scene3D {
  let tex = makeSdfTex('123');
  const uni = {
    tS: { value: tex.sdf as THREE.Texture },
    tB: { value: tex.bmp as THREE.Texture },
    uRes: { value: new THREE.Vector2(1, 1) },
    uZoom: { value: 1 },
    uOut: { value: 0.09 },
    uGlow: { value: 1 },
    uTime: { value: 0 },
  };
  let alive = true;
  void document.fonts?.ready.then(() => {
    if (!alive) return;
    tex.sdf.dispose();
    tex.bmp.dispose();
    tex = makeSdfTex('123');
    uni.tS.value = tex.sdf;
    uni.tB.value = tex.bmp;
  });
  const q = quad(
    /* glsl */ `
    uniform sampler2D tS, tB; uniform vec2 uRes; uniform float uZoom, uOut, uGlow, uTime;
    varying vec2 vUv;
    void main(){
      bool right = vUv.x > 0.5;
      vec2 lp = vec2(right ? (vUv.x - 0.5) * 2.0 : vUv.x * 2.0, vUv.y);
      float hasp = (uRes.x * 0.5) / uRes.y;
      vec2 p = (lp - 0.5) * vec2(hasp, 1.0);
      vec2 S = vec2(hasp * 0.92, hasp * 0.46);
      vec2 focus = vec2(0.47, 0.62);
      vec2 tuv = mix(vec2(0.5), focus, 1.0 - 1.0 / uZoom) + p / (S * uZoom);
      vec3 bg = mix(vec3(0.05, 0.06, 0.16), vec3(0.12, 0.08, 0.24), vUv.y);
      vec3 col = bg;
      float inside = step(0.0, tuv.x) * step(tuv.x, 1.0) * step(0.0, tuv.y) * step(tuv.y, 1.0);
      vec3 gold = mix(vec3(1.0, 0.55, 0.12), vec3(1.0, 0.92, 0.4), clamp(tuv.y * 1.6 - 0.3, 0.0, 1.0));
      if (!right){
        float a = texture2D(tB, tuv).a * inside;
        col = mix(col, gold, a);
      } else {
        float d = texture2D(tS, tuv).r;
        if (inside < 0.5) d = 0.0;
        float fw = max(fwidth(d) * 0.75, 1e-4);
        float fill = smoothstep(0.5 - fw, 0.5 + fw, d);
        float o = smoothstep(0.5 - uOut - fw, 0.5 - uOut + fw, d);
        float pulse = 0.75 + 0.25 * sin(uTime * 3.0);
        float glow = exp(-max(0.5 - uOut - d, 0.0) * 9.0) * uGlow * pulse * step(0.001, d);
        col = mix(col, vec3(0.3, 0.85, 1.0), clamp(glow, 0.0, 1.0) * (1.0 - o));
        col = mix(col, vec3(0.08, 0.1, 0.35), o);
        col = mix(col, gold, fill);
      }
      gl_FragColor = vec4(pow(col, vec3(1.0)), 1.0);
    }`,
    uni,
  );
  const hud = new Hud();
  const a = hud.add('보통 그림 글자 (128×64)');
  const b = hud.add('SDF 글자 (같은 128×64)', 'rgba(20,90,140,0.85)');
  hud.at(a, 0.03, 0.05, 0, 0);
  hud.at(b, 0.97, 0.05, 1, 0);
  const zl = hud.add('');
  hud.at(zl, 0.5, 0.95, 0.5, 1);
  let auto = true;
  let manual = 4;
  return {
    scene: q.scene,
    camera: q.cam,
    tone: THREE.NoToneMapping,
    update(t) {
      uni.uTime.value = t;
      const k = 0.5 - 0.5 * Math.cos(t * 0.9);
      uni.uZoom.value = auto ? 1 + 9 * k * k : manual;
      hud.set(zl, `× ${uni.uZoom.value.toFixed(1)} 확대`);
    },
    render(r, w, h) {
      uni.uRes.value.set(w, h);
      r.render(q.scene, q.cam);
      drawDivider(r, w / 2, h, w);
      hud.draw(r, w, h);
    },
    controls: [
      { type: 'toggle', label: '저절로 확대', value: true, on: (v) => (auto = v) },
      { type: 'range', label: '확대 (저절로 끄면)', min: 1, max: 12, step: 0.1, value: 4, on: (v) => (manual = v) },
      { type: 'range', label: '테 두께', min: 0, max: 0.3, step: 0.01, value: 0.09, on: (v) => (uni.uOut.value = v) },
      { type: 'range', label: '빛무리', min: 0, max: 2, step: 0.05, value: 1, on: (v) => (uni.uGlow.value = v) },
    ],
    dispose() {
      alive = false;
      q.dispose();
      tex.sdf.dispose();
      tex.bmp.dispose();
      hud.dispose();
    },
  };
}
```

### i242 견본 항목 — `src/demos/demosLook2.ts:4526`
```ts
  i242: { kind: '3d', caption: '같은 128×64 크기 — 왼쪽 그림 글자는 확대하면 뭉개지고, 오른쪽 거리장(SDF)은 매끈 + 테 + 빛무리', make: () => makeSdf() }
```

## 관련 기술
- 이 효과를 만든 기술: [3D 입체 글씨 (글꼴 윤곽 돌출 · 모서리 깎기)](https://ai-techstudio.web.app/ai/t/i17.md) `i17`
