# AI 꾸러미 — 테두리 빛 (rim light, 프레넬) — Rim light (Fresnel rim)
> 카메라에서 옆으로 비껴 보이는 가장자리일수록 빛을 더해, 어두운 배경에서도 캐릭터 윤곽이 또렷하게 떠 보이게 한다.  
> 견본: https://ai-techstudio.web.app/#t/i10

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

## 주문서

### 만들어 줘: 테두리 빛 (rim light, 프레넬) — Rim light (Fresnel rim)

#### 1. 목표
밤 배경 위 동글 캐릭터에 프레넬 테두리 빛을 넣어 줘 — 가장자리만 빛나 배경에서 떠 보이게. 느낌은 신비로운 밤.

#### 2. 핵심 기술 용어
- **Rim light (Fresnel rim)** — 테두리 빛 — 가장자리만 빛남
- **Fresnel term pow(1 − N·V, p)** — 보는 방향과 면이 비껴 있을수록 커지는 값
- **totalEmissiveRadiance** — three.js 셰이더의 스스로 빛나는 양

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

#### 4. 조건
- 표준 재질에 onBeforeCompile 로 끼워 넣기 (빛 · 그림자는 그대로)
- #include <emissivemap_fragment> 뒤에 totalEmissiveRadiance += 색 × fres × 세기
- 세기 · 좁기 · 색은 공유 uniform 으로 — 슬라이더로 바로 바뀌게
- 어두운 배경에서 왼쪽 없음 · 오른쪽 있음 비교

#### 5. 완성 기준 (이게 보이면 성공)
- 어두운 밤 배경에서 오른쪽 캐릭터만 가장자리에 하늘빛 테가 보인다
- 캐릭터가 돌아도 테는 늘 화면에서 본 윤곽을 따라간다
- 「빛 세기」 0~4 · 「가장자리 좁게」 0.8~6 · 「빛 색」 슬라이더가 바로 반영된다

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

## 원리
- 면의 법선 N 과 카메라 쪽 방향 V 가 같으면(정면) N·V = 1, 옆으로 비껴 있으면 0 에 가깝다.
- fres = pow(1 − N·V, uPow) 는 가장자리에서만 커진다. uPow(2.5)를 올리면 테가 더 좁아진다.
- 그 값에 색과 세기(1.6)를 곱해 스스로 빛나는 양에 더한다.
- 조명과 상관없이 늘 가장자리가 밝아, 어두운 배경과 경계가 생긴다.

## 핵심 코드 — 표준 재질에 프레넬 테두리 빛 끼워 넣기
(발췌: demos/demosMaterial.ts i10 make() 의 rimify() 를 정리)
```ts
const uRim = { value: 1.6 };                       // 세기
const uPow = { value: 2.5 };                       // 클수록 테가 좁다
const uCol = { value: new THREE.Color(0x7fd8ff) };

function rimify(m: THREE.MeshStandardMaterial): THREE.MeshStandardMaterial {
  const c = m.clone();
  c.onBeforeCompile = (sh) => {
    sh.uniforms.uRim = uRim;
    sh.uniforms.uPow = uPow;
    sh.uniforms.uRimCol = uCol;
    sh.fragmentShader = 'uniform float uRim;\nuniform float uPow;\nuniform vec3 uRimCol;\n' +
      sh.fragmentShader.replace(
        '#include <emissivemap_fragment>',
        '#include <emissivemap_fragment>\n' +
        // vViewPosition 은 카메라 → 점 반대 방향: 정면이면 N·V = 1
        'float fres = pow(1.0 - clamp(dot(normalize(normal), normalize(vViewPosition)), 0.0, 1.0), uPow);\n' +
        'totalEmissiveRadiance += uRimCol * fres * uRim;',
      );
  };
  return c;
}

const body = rimify(new THREE.MeshStandardMaterial({ color: 0x3a3a8a, roughness: 0.6 }));
scene.add(new THREE.Mesh(new THREE.SphereGeometry(0.6, 48, 32), body));

// 색 바꾸기 (색상 0~360)
function setHue(h: number) { uCol.value.setHSL(h / 360, 1, 0.75); }
```

## 흔한 실수 · 확인 목록
- [ ] **평평한 면이 많은 물체는 면 전체가 한꺼번에 번쩍인다** — 테두리 빛은 곡면에서 예쁘다. 상자는 모서리를 둥글게(RoundedBox) 하거나 uPow 를 크게.
- [ ] **빛 번짐(u20)과 같이 쓰면 테가 너무 번진다** — 테 세기를 낮추거나 번짐 문턱을 테 밝기보다 높게 둔다.
- [ ] **양면(DoubleSide) 재질은 뒷면에서 테가 뒤집힌다** — 뒷면에선 법선이 반대라 값이 틀린다. 앞면만 쓰거나 abs(dot) 로.

## 완성 기준 체크리스트
- [ ] 어두운 밤 배경에서 오른쪽 캐릭터만 가장자리에 하늘빛 테가 보인다
- [ ] 캐릭터가 돌아도 테는 늘 화면에서 본 윤곽을 따라간다
- [ ] 「빛 세기」 0~4 · 「가장자리 좁게」 0.8~6 · 「빛 색」 슬라이더가 바로 반영된다

## 이 기술 정보
- id: `i10` · 분류: 그래픽 · 셰이더 › 재질 · 그림체 · 3D · 난이도 쉬움 · 폰 부담 가벼움 (폰 OK) — 조각 셰이더에 내적 · pow 한 번. 폰 OK.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i10
- 쓰면 좋을 때: 어두운 배경 · 밤 장면에서 캐릭터가 묻힐 때 / 툰 캐릭터에 「뒤에서 비친 빛」 느낌을 싸게 줄 때
- 쓰지 말 때: 밝은 낮 배경 — 테가 하얗게 떠 보여 부자연스럽다 / 평평한 판 · 상자 — 면 전체가 한꺼번에 밝아진다. 대신 진짜 뒤쪽 빛(u14 3점 조명)

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### i10 견본 항목 — `src/demos/demosMaterial.ts:1077`
```ts
  i10: {
    kind: '3d',
    caption: '어두운 밤 배경 — 오른쪽만 가장자리가 빛나 배경에서 또렷하게 떠 보여요',
    make() {
      const B = base({ bg: gradBg('#0a0f2a', '#1a2450'), labels: ['테두리 빛 없음', '테두리 빛 (프레넬)'], fit: [2.5, 0.9], target: [0, 0.05, 0] });
      B.scene.add(new THREE.HemisphereLight(0x8090c0, 0x101020, 0.5));
      const key = new THREE.DirectionalLight(0xffffff, 1.1);
      key.position.set(-2, 3, 4);
      B.scene.add(key);
      const uRim = { value: 1.6 };
      const uPow = { value: 2.5 };
      const uCol = { value: new THREE.Color(0x7fd8ff) };
      const rimify = (m: THREE.MeshStandardMaterial): THREE.MeshStandardMaterial => {
        const c = m.clone();
        c.onBeforeCompile = (sh) => {
          sh.uniforms['uRim'] = uRim;
          sh.uniforms['uPow'] = uPow;
          sh.uniforms['uRimCol'] = uCol;
          sh.fragmentShader = 'uniform float uRim;\nuniform float uPow;\nuniform vec3 uRimCol;\n' + sh.fragmentShader.replace(
            '#include <emissivemap_fragment>',
            `#include <emissivemap_fragment>
            float fres = pow(1.0 - clamp(dot(normalize(normal), normalize(vViewPosition)), 0.0, 1.0), uPow);
            totalEmissiveRadiance += uRimCol * fres * uRim;`,
          );
        };
        return c;
      };
      const body = new THREE.MeshStandardMaterial({ color: 0x3a3a8a, roughness: 0.6 });
      const ball = new THREE.MeshStandardMaterial({ color: 0x7a2a5a, roughness: 0.5 });
      const eye = new THREE.MeshBasicMaterial({ color: 0xffffff });
      const pupil = new THREE.MeshBasicMaterial({ color: 0x101020 });
      const bodyR = rimify(body);
      const ballR = rimify(ball);
      const bg = new THREE.SphereGeometry(0.6, 48, 32);
      const sg = new THREE.SphereGeometry(0.3, 40, 24);
      const eg = new THREE.SphereGeometry(0.1, 16, 12);
      const pg = new THREE.SphereGeometry(0.05, 12, 8);
      const set = (bm: THREE.Material, sm: THREE.Material): { g: THREE.Group; c: THREE.Group; b: THREE.Mesh } => {
        const g = new THREE.Group();
        const c = new THREE.Group();
        const bd = new THREE.Mesh(bg, bm);
        bd.scale.set(1, 0.9, 0.95);
        c.add(bd);
        for (const sx of [-1, 1]) {
          const e = new THREE.Mesh(eg, eye);
          e.position.set(sx * 0.2, 0.1, 0.52);
          const p = new THREE.Mesh(pg, pupil);
          p.position.set(sx * 0.2, 0.1, 0.61);
          c.add(e, p);
        }
        c.position.set(-0.25, 0, 0);
        const b = new THREE.Mesh(sg, sm);
        b.position.set(0.6, -0.45, 0.3);
        g.add(c, b);
        return { g, c, b };
      };
      const L = set(body, ball);
      const R = set(bodyR, ballR);
      L.g.position.x = -1.2;
      R.g.position.x = 1.2;
      B.scene.add(L.g, R.g);
      return B.done({
        update(t) {
          for (const S of [L, R]) {
            S.c.rotation.y = Math.sin(t * 0.8) * 0.6;
            S.c.position.y = Math.abs(Math.sin(t * 1.8)) * 0.12;
            S.b.position.y = -0.45 + Math.abs(Math.sin(t * 2.4 + 1)) * 0.3;
          }
        },
        controls: [
          { type: 'range', label: '빛 세기', min: 0, max: 4, step: 0.1, value: 1.6, on: (v) => { uRim.value = v; } },
          { type: 'range', label: '가장자리 좁게', min: 0.8, max: 6, step: 0.1, value: 2.5, on: (v) => { uPow.value = v; } },
          { type: 'range', label: '빛 색 (색상)', min: 0, max: 360, step: 1, value: 197, on: (v) => { uCol.value.setHSL(v / 360, 1, 0.75); } },
        ],
        dispose() {
          body.dispose();
          ball.dispose();
        },
      });
    },
  }
```

## 관련 기술
- 같은 방법으로 만든 효과 예시: [엑스레이 · 반투명 (프레넬)](https://ai-techstudio.web.app/ai/t/i42.md) `i42`
- 먼저 알면 좋은 기술: [재질에 셰이더 끼워 넣기](https://ai-techstudio.web.app/ai/t/u08.md) `u08`
- 다음에 해 볼 기술: [선택 강조 맥박 테](https://ai-techstudio.web.app/ai/t/i226.md) `i226` · [홀로그램 · 주사선 셰이더](https://ai-techstudio.web.app/ai/t/i221.md) `i221`
- 참고 문서: [Wikipedia — Fresnel equations](https://en.wikipedia.org/wiki/Fresnel_equations)
