# AI 꾸러미 — 3점 조명 (반구 + 해 + 테두리) — Three-point lighting (key · fill · rim)
> 반구 빛 · 해 · 뒤쪽 테두리 빛 셋을 겹쳐, 납작하던 캐릭터가 입체로 서고 윤곽이 배경에서 떠오르게 한다.  
> 견본: https://ai-techstudio.web.app/#t/u14

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

## 주문서

### 만들어 줘: 3점 조명 (반구 + 해 + 테두리) — Three-point lighting (key · fill · rim)

#### 1. 목표
동글동글한 캐릭터에 3점 조명(반구 빛 + 해 + 뒤 테두리 빛)을 넣어 밝은 장난감 사진 느낌으로 입체감과 윤곽을 살려 줘.

#### 2. 핵심 기술 용어
- **Three-point lighting (key · fill · rim)** — 주광 · 보조광 · 테두리 빛 세 가지 조명
- **HemisphereLight** — 하늘색 위 · 땅색 아래로 고르게 비추는 바탕 빛
- **DirectionalLight (key / rim)** — 해처럼 한 방향으로 오는 빛 — 앞 위에서 주광, 뒤에서 테두리

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

#### 4. 조건
- 반구 빛 하늘색 · 땅색을 다르게 (견본 0xbfe3ff 위 · 0x5a4630 아래) — 같은 색이면 바탕이 납작하다
- 해를 더하면 반구 세기를 낮춘다 (견본 1.4 → 0.9) — 합쳐서 하얗게 날아가지 않게
- 테두리 빛은 물체 뒤쪽(z 음수)에서, 주광과 다른 색(하늘색 · 분홍)으로 두 개
- 그림자는 주광 하나에만 (테두리 빛은 그림자 끔)

#### 5. 완성 기준 (이게 보이면 성공)
- 세 칸을 나란히 — 반구만 · + 해 · + 테두리 — 두면 칸마다 입체감이 한 단계씩 는다
- 세 번째 칸에서 캐릭터 가장자리에 하늘색 · 분홍 빛 테가 보인다
- 캐릭터가 돌아도 테두리는 늘 윤곽 쪽에 남는다
- 폰에서도 60fps

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

## 원리
- 반구 빛(바탕)만 있으면 어디서나 밝기가 같아 그림자 없는 스티커처럼 보인다.
- 해(주광)를 앞 위 비스듬히 더하면 한쪽은 밝고 반대쪽은 어두워져 둥근 모양이 드러난다.
- 뒤에서 오는 빛(테두리)은 물체 가장자리만 스친다. 어두운 배경에서 윤곽이 반짝 떠오른다.
- 테두리 빛은 가장자리 몇 픽셀만 밝히므로 세기를 주광보다 훨씬 세게(견본 7 · 4) 둔다.

## 핵심 코드 — 반구 + 해 + 테두리 두 개
(발췌: demos/demosLight.ts u14 build(n) 을 정리)
```ts
import * as THREE from 'three';

// 1) 바탕 — 위는 하늘색, 아래는 흙빛 (해가 있으니 0.9 로 낮춤, 반구만이면 1.4)
scene.add(new THREE.HemisphereLight(0xbfe3ff, 0x5a4630, 0.9));

// 2) 주광 — 앞 왼쪽 위에서 따뜻하게
const sun = new THREE.DirectionalLight(0xfff1d6, 2.4);
sun.position.set(-2.2, 3, 2.5);
scene.add(sun);

// 3) 테두리 빛 — 뒤(z 음수)에서 가장자리만 스친다. 몇 픽셀만 밝히니 세게
const rim = new THREE.DirectionalLight(0x9fe0ff, 7);
rim.position.set(2.6, 1.2, -3.5);
const rim2 = new THREE.DirectionalLight(0xffd0f0, 4);
rim2.position.set(-2.6, 1.0, -3.5);
scene.add(rim, rim2);
```

## 흔한 실수 · 확인 목록
- [ ] **해를 더하고 반구 세기를 그대로 두면 하얗게 날아간다** — 빛은 더해진다. 해를 넣으면 반구를 낮춘다 (견본 1.4 → 0.9).
- [ ] **테두리 빛을 카메라 쪽에 두면 앞면이 납작하게 밝아진다** — 테두리는 물체 뒤에서 카메라 쪽을 향해 — 견본은 z = -3.5.
- [ ] **빛마다 그림자를 켜면 느려지고 그림자가 겹친다** — 그림자는 주광 하나만. 방향 빛 하나 = 그림자 지도 한 장.

## 완성 기준 체크리스트
- [ ] 세 칸을 나란히 — 반구만 · + 해 · + 테두리 — 두면 칸마다 입체감이 한 단계씩 는다
- [ ] 세 번째 칸에서 캐릭터 가장자리에 하늘색 · 분홍 빛 테가 보인다
- [ ] 캐릭터가 돌아도 테두리는 늘 윤곽 쪽에 남는다
- [ ] 폰에서도 60fps

## 이 기술 정보
- id: `u14` · 분류: 그래픽 · 셰이더 › 빛 · 환경 · 3D · 난이도 쉬움 · 폰 부담 가벼움 (폰 OK) — 빛 4개(반구 1 + 방향 3)는 그림자를 끄면 폰도 가볍다. 그림자는 주광 하나에만 켠다.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/u14
- 쓰면 좋을 때: 캐릭터 · 말이 배경에 묻혀 보일 때 / 어두운 배경에 밝은 물체를 세울 때 / 어느 3D 장면이든 처음 조명을 잡을 때 — 기본 틀
- 쓰지 말 때: 빛이 하나뿐이어야 하는 장면(달밤 · 촛불) — 대신 밤 장면(i432) / 테두리를 셰이더로 그려야 할 때(툰) — 대신 테두리 빛 셰이더(i10)

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### fit — `src/demos/demosLight.ts:80`
```ts
function fit(cam: THREE.PerspectiveCamera, aspect: number, target: THREE.Vector3, radius: number, dir: THREE.Vector3): THREE.PerspectiveCamera {
  cam.aspect = aspect;
  cam.updateProjectionMatrix();
  const v = THREE.MathUtils.degToRad(cam.fov) / 2;
  const hf = Math.atan(Math.tan(v) * aspect);
  const d = radius / Math.sin(Math.min(v, hf));
  cam.position.copy(target).addScaledVector(dir.clone().normalize(), d);
  cam.lookAt(target);
  return cam;
}
```

### u14 견본 항목 — `src/demos/demosLight.ts:886`
```ts
  u14: {
    kind: '3d',
    caption: '반구 빛만 → 해(주광)를 더하면 입체 → 뒤에서 테두리 빛을 더하면 윤곽이 반짝 떠올라요',
    make() {
      const build = (n: number): { scene: THREE.Scene; ch: THREE.Group } => {
        const scene = new THREE.Scene();
        scene.background = gradTex('#1f2b5a', '#0c1230');
        scene.add(new THREE.HemisphereLight(0xbfe3ff, 0x5a4630, n === 0 ? 1.4 : 0.9));
        if (n >= 1) {
          const sun = new THREE.DirectionalLight(0xfff1d6, 2.4);
          sun.position.set(-2.2, 3, 2.5);
          scene.add(sun);
        }
        if (n >= 2) {
          const rim = new THREE.DirectionalLight(0x9fe0ff, 7);
          rim.position.set(2.6, 1.2, -3.5);
          const rim2 = new THREE.DirectionalLight(0xffd0f0, 4);
          rim2.position.set(-2.6, 1.0, -3.5);
          scene.add(rim2);
          scene.add(rim);
        }
        const ch = cutie(0xffa94d);
        const base = mesh(new THREE.CylinderGeometry(0.85, 0.9, 0.12, 48), std(0xdde6f5, 0.7), 0, -0.06, 0);
        scene.add(ch, base);
        return { scene, ch };
      };
      const S = [build(0), build(1), build(2)];
      const cam = new THREE.PerspectiveCamera(32, 1, 0.1, 50);
      const hud = new Hud();
      hud.vline(1 / 3);
      hud.vline(2 / 3);
      ['반구', '+ 해', '+ 테두리'].forEach((s, i) => hud.label(s, (i + 0.5) / 3, 0.95, 0.5, 1, i === 2 ? 'on' : 'tag'));
      return {
        scene: S[0]!.scene,
        camera: cam,
        update(t) {
          for (const s of S) {
            s.ch.rotation.y = Math.sin(t * 0.6) * 0.8;
            s.ch.position.y = Math.abs(Math.sin(t * 2)) * 0.06;
          }
        },
        render(r, w, h) {
          panels(r, w, h, S.map((s) => ({ scene: s.scene })), (a) => fit(cam, a, V3(0, 0.5, 0), 0.82, V3(0, 0.25, 1)));
          hud.draw(r, w, h);
        },
        dispose() {
          hud.dispose();
          disposeTree(...S.map((s) => s.scene));
        },
      };
    },
  }
```

## 관련 기술
- 먼저 알면 좋은 기술: [그림자 맵 (PCF · PCF Soft)](https://ai-techstudio.web.app/ai/t/u17.md) `u17`
- 다음에 해 볼 기술: [테두리 빛 (rim light, 프레넬)](https://ai-techstudio.web.app/ai/t/i10.md) `i10` · [환경 반사 (RoomEnvironment + PMREM)](https://ai-techstudio.web.app/ai/t/u12.md) `u12` · [스포트라이트 · 움직이는 점광](https://ai-techstudio.web.app/ai/t/u15.md) `u15`
- 참고 문서: [three.js 문서 — HemisphereLight](https://threejs.org/docs/#api/en/lights/HemisphereLight) · [Wikipedia — Three-point lighting](https://en.wikipedia.org/wiki/Three-point_lighting)
