# AI 꾸러미 — 환경 반사 (RoomEnvironment + PMREM) — Image-based lighting (environment map)
> 스튜디오 조명 장면을 한 번 구워 반사 지도로 깔아, 금속 · 유리 · 니스 칠한 물체에 빛이 비쳐 반짝이게 한다.  
> 견본: https://ai-techstudio.web.app/#t/u12

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

## 주문서

### 만들어 줘: 환경 반사 (RoomEnvironment + PMREM) — Image-based lighting (environment map)

#### 1. 목표
금속 공 · 금 고리 · 보석 · 주사위에 RoomEnvironment 반사 지도를 깔아 금속 · 유리가 깨끗한 스튜디오 사진 느낌으로 반짝이게 해 줘.

#### 2. 핵심 기술 용어
- **Image-based lighting (environment map)** — 둘레 그림으로 비침 · 은은한 빛 주기
- **RoomEnvironment** — three.js 가 주는 스튜디오 방 장면 (창 · 조명판)
- **PMREMGenerator** — 거칠기마다 흐린 반사 지도를 미리 구워 주는 도구
- **scene.environment / environmentIntensity** — 장면 전체 반사 지도 · 그 세기

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

#### 4. 조건
- PMREMGenerator 는 렌더러가 생긴 뒤 한 번만 굽고, 다 구우면 pm.dispose() · room.dispose()
- 반사는 scene.environment 에만 — scene.background 는 그대로 (스튜디오 방이 배경에 보이면 안 된다)
- environmentIntensity 로 세기를 조절 (견본 1, 게임은 0.5 ~ 0.7) — 빛을 끄고 반사로만 밝히지 말 것
- 반사가 보이는 재질은 roughness 0.05 ~ 0.2 의 금속(metalness 1) 또는 clearcoat 1 물리 재질
- 렌더러가 바뀌면(다시 만들면) 지도를 다시 굽는다

#### 5. 완성 기준 (이게 보이면 성공)
- 왼쪽 조명만 · 오른쪽 환경 반사로 나란히 두면, 오른쪽 크롬 공 · 금 고리에 하얀 창 모양 비침이 보인다
- 빨간 주사위 클리어코트에 또렷한 하이라이트, 하늘색 보석 면마다 다른 밝기가 보인다
- 「반사 세기」 0 ~ 2 슬라이더로 비침이 줄고 는다 (0 이면 왼쪽과 같아짐)
- 폰에서도 60fps — 시작 때 한 번 굽고 나면 프레임 비용이 거의 없다

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

## 원리
- 반들반들한 물체는 빛만 받는 것이 아니라 둘레 풍경을 비춘다. 둘레가 없으면 금속이 시커멓게 보인다.
- RoomEnvironment 는 하얀 조명판 · 창이 있는 작은 방 장면이다. 이것을 렌더러로 한 번 찍어 반사 지도로 쓴다.
- PMREMGenerator 가 거칠기마다 다르게 흐린 지도를 미리 만들어 둔다 — 매끈하면 또렷한 비침, 거칠면 뿌연 빛.
- scene.environment 에 넣으면 표준 · 물리 재질 모두가 같은 지도를 읽는다. 배경(background)은 따로라 그대로 둘 수 있다.

## 핵심 코드 — RoomEnvironment 를 구워 장면 반사 지도로
(발췌: demos/demosLight.ts u12 make() 의 render 첫 굽기 · build() 재질을 정리)
```ts
import * as THREE from 'three';
import { RoomEnvironment } from 'three/examples/jsm/environments/RoomEnvironment.js';

// 렌더러가 생긴 뒤 한 번만 굽는다
const pm = new THREE.PMREMGenerator(renderer);
const room = new RoomEnvironment();
const envTex = pm.fromScene(room, 0.04).texture; // 0.04 = 살짝 흐리게
room.dispose();
pm.dispose();

scene.environment = envTex;     // 배경은 그대로, 비침에만 쓰인다
scene.environmentIntensity = 1; // 게임은 0.5 ~ 0.7

// 조명은 그대로 둔다 — 환경은 빛을 대신하지 않고 비침을 준다
scene.add(new THREE.HemisphereLight(0xffffff, 0x7a8aa0, 0.5));
const sun = new THREE.DirectionalLight(0xfff0d6, 1.3);
sun.position.set(3, 5, 4);
scene.add(sun);

// 비침이 잘 보이는 재질 — 매끈한 금속 · 클리어코트
const chrome = new THREE.MeshStandardMaterial({ color: 0xffffff, roughness: 0.05, metalness: 1 });
const gold = new THREE.MeshStandardMaterial({ color: 0xffc94a, roughness: 0.2, metalness: 1 });
const dice = new THREE.MeshPhysicalMaterial({ color: 0xe8453c, roughness: 0.2, clearcoat: 1, clearcoatRoughness: 0.04 });
const gem = new THREE.MeshPhysicalMaterial({ color: 0x7fe6ff, roughness: 0.02, clearcoat: 1, transparent: true, opacity: 0.6, flatShading: true });
```

## 흔한 실수 · 확인 목록
- [ ] **반사 지도 없이 metalness 1 을 쓰면 금속이 검게 보인다** — 금속은 둘레를 비추는 재질이라 비출 것이 없으면 까맣다. environment 를 깔거나 metalness 를 낮춘다.
- [ ] **환경 세기를 1 그대로 두면 장면이 뿌옇게 밝아진다** — 빛과 반사가 겹친다. 이 사이트 게임들은 0.5 ~ 0.7 로 낮춰 썼다.
- [ ] **프레임마다 PMREMGenerator 로 다시 구우면 느려진다** — 지도는 렌더러마다 한 번만. 견본도 렌더러가 바뀔 때만 다시 굽는다.
- [ ] **툰 장면에 깔면 계단 명암이 흐려진다** — 툰 재질(MeshToonMaterial)은 환경을 쓰지 않지만 섞인 표준 재질이 튄다. 툰 장면은 환경을 끄고 빛으로.

## 완성 기준 체크리스트
- [ ] 왼쪽 조명만 · 오른쪽 환경 반사로 나란히 두면, 오른쪽 크롬 공 · 금 고리에 하얀 창 모양 비침이 보인다
- [ ] 빨간 주사위 클리어코트에 또렷한 하이라이트, 하늘색 보석 면마다 다른 밝기가 보인다
- [ ] 「반사 세기」 0 ~ 2 슬라이더로 비침이 줄고 는다 (0 이면 왼쪽과 같아짐)
- [ ] 폰에서도 60fps — 시작 때 한 번 굽고 나면 프레임 비용이 거의 없다

## 이 기술 정보
- id: `u12` · 분류: 그래픽 · 셰이더 › 빛 · 환경 · 3D · 난이도 쉬움 · 폰 부담 가벼움 (폰 OK) — 굽기는 시작할 때 한 번(수십 ms). 그 뒤에는 재질마다 지도 한 번 읽기라 폰도 가볍다. 지도는 렌더러마다 하나만 만든다.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/u12
- 쓰면 좋을 때: 금속 · 유리 · 클리어코트 재질이 조명만으로 밋밋할 때 / 보드게임 말 · 주사위처럼 작은 물체를 고급스럽게 / 빛을 여러 개 더하지 않고 은은한 둘레 빛을 주고 싶을 때
- 쓰지 말 때: 툰 그림체 — 계단 명암을 흐린다. 대신 테두리 빛(i10) · 매트캡(i09) / 장면 하늘색 · 분위기에 반사가 맞아야 할 때 — 대신 셰이더 하늘로 구운 반사(u13)

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — 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;
}
```

### u12 견본 항목 — `src/demos/demosLight.ts:694`
```ts
  u12: {
    kind: '3d',
    caption: '왼쪽 조명만 · 오른쪽 RoomEnvironment 반사 — 금속 · 유리에 스튜디오 빛이 비쳐 반짝여요',
    make() {
      const build = (): { scene: THREE.Scene; root: THREE.Group; items: THREE.Mesh[] } => {
        const scene = new THREE.Scene();
        scene.background = gradTex('#25346e', '#0b1026');
        basicLights(scene, 0.5, 1.3, V3(3, 5, 4));
        const root = new THREE.Group();
        const chrome = mesh(new THREE.SphereGeometry(0.5, 48, 32), std(0xffffff, 0.05, 1), -0.62, 0.55, 0);
        const gold = mesh(new THREE.TorusKnotGeometry(0.3, 0.1, 120, 16), std(0xffc94a, 0.2, 1), 0.66, 0.58, 0);
        const gem = mesh(
          new THREE.IcosahedronGeometry(0.44, 0),
          new THREE.MeshPhysicalMaterial({ color: 0x7fe6ff, roughness: 0.02, clearcoat: 1, transparent: true, opacity: 0.6, flatShading: true }),
          -0.62,
          -0.55,
          0,
        );
        const dice = mesh(
          new RoundedBoxGeometry(0.72, 0.72, 0.72, 4, 0.13),
          new THREE.MeshPhysicalMaterial({ color: 0xe8453c, roughness: 0.2, clearcoat: 1, clearcoatRoughness: 0.04 }),
          0.66,
          -0.55,
          0,
        );
        root.add(chrome, gold, gem, dice);
        scene.add(root);
        return { scene, root, items: [chrome, gold, gem, dice] };
      };
      const A = build();
      const B = build();
      const cam = new THREE.PerspectiveCamera(32, 1, 0.1, 50);
      const hud = new Hud();
      hud.vline(0.5);
      hud.label('조명만', 0.25, 0.95, 0.5, 1);
      hud.label('환경 반사', 0.75, 0.95, 0.5, 1, 'on');
      let env: { r: R; tex: THREE.Texture } | null = null;
      let k = 1;
      return {
        scene: A.scene,
        camera: cam,
        update(t) {
          for (const S of [A, B])
            S.items.forEach((m, i) => {
              m.rotation.y = t * (0.5 + i * 0.12);
              m.rotation.x = Math.sin(t * 0.7 + i) * 0.5;
            });
        },
        render(r, w, h) {
          if (!env || env.r !== r) {
            env?.tex.dispose();
            const pm = new THREE.PMREMGenerator(r);
            const room = new RoomEnvironment();
            env = { r, tex: pm.fromScene(room, 0.04).texture };
            room.dispose();
            pm.dispose();
            B.scene.environment = env.tex;
            B.scene.environmentIntensity = k;
          }
          panels(r, w, h, [{ scene: A.scene }, { scene: B.scene }], (a) => fit(cam, a, V3(0, 0, 0), 1.45, V3(0, 0.12, 1)));
          hud.draw(r, w, h);
        },
        controls: [
          {
            type: 'range',
            label: '반사 세기',
            min: 0,
            max: 2,
            step: 0.05,
            value: 1,
            on: (v) => {
              k = v;
              B.scene.environmentIntensity = v;
            },
          },
        ],
        dispose() {
          env?.tex.dispose();
          hud.dispose();
          disposeTree(A.scene, B.scene);
        },
      };
    },
  }
```

## 관련 기술
- 같은 방법으로 만든 효과 예시: [셰이더 하늘로 만든 환경 반사](https://ai-techstudio.web.app/ai/t/u13.md) `u13`
- 먼저 알면 좋은 기술: [3점 조명 (반구 + 해 + 테두리)](https://ai-techstudio.web.app/ai/t/u14.md) `u14` · [PBR + 클리어코트](https://ai-techstudio.web.app/ai/t/u04.md) `u04`
- 다음에 해 볼 기술: [셰이더 하늘로 만든 환경 반사](https://ai-techstudio.web.app/ai/t/u13.md) `u13` · [평면 반사 (Reflector)](https://ai-techstudio.web.app/ai/t/i14.md) `i14`
- 참고 문서: [three.js 소스 — RoomEnvironment](https://github.com/mrdoob/three.js/blob/dev/examples/jsm/environments/RoomEnvironment.js) · [three.js 문서 — PMREMGenerator](https://threejs.org/docs/#api/en/extras/PMREMGenerator)
