# AI 꾸러미 — 셰이더 하늘로 만든 환경 반사
> 분위기 색 하늘을 구워 반사 지도로 (금빛 동굴 · 밤 보라 등)  
> 견본: https://ai-techstudio.web.app/#t/u13

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

## 주문서

### 만들어 줘: 셰이더 하늘로 만든 환경 반사

#### 1. 목표
3D 장면의 물체에 「셰이더 하늘로 만든 환경 반사」 기술을 깔끔한 기본 느낌으로 넣어 줘. — 분위기 색 하늘을 구워 반사 지도로 (금빛 동굴 · 밤 보라 등)

#### 2. 핵심 기술 용어
- 셰이더 하늘로 만든 환경 반사 — 분위기 색 하늘을 구워 반사 지도로 (금빛 동굴 · 밤 보라 등)

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

#### 4. 조건
- 한 프레임 16ms 안 (폰 포함) — 무거운 계산은 처음에 한 번 또는 여러 프레임에 나눠서
- 새 재질 · 셰이더는 첫 화면 전에 미리 컴파일 (renderer.compileAsync)
- 화면 가장자리 흐림 · 비네트로 얼버무리지 않기
- 외부 라이브러리 추가 없이 three.js 와 three/examples 만

#### 5. 완성 기준 (이게 보이면 성공)
- 견본처럼 「분위기 색 하늘을 구워 반사 지도로 (금빛 동굴 · 밤 보라 등)」 이(가) 첫 화면에서 바로 보인다
- 켬/끔(또는 전/후) 비교 단추로 이 기술이 만드는 차이를 확인할 수 있다
- 중요한 값 1~3개를 슬라이더로 바꿔 볼 수 있다
- 콘솔 오류 0, 폰 폭(390px)에서도 동작한다

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

## 원리
- 분위기 색 하늘을 구워 반사 지도로 (금빛 동굴 · 밤 보라 등)


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

## 완성 기준 체크리스트
- [ ] 견본처럼 「분위기 색 하늘을 구워 반사 지도로 (금빛 동굴 · 밤 보라 등)」 이(가) 첫 화면에서 바로 보인다
- [ ] 켬/끔(또는 전/후) 비교 단추로 이 기술이 만드는 차이를 확인할 수 있다
- [ ] 중요한 값 1~3개를 슬라이더로 바꿔 볼 수 있다
- [ ] 콘솔 오류 0, 폰 폭(390px)에서도 동작한다

## 이 기술 정보
- id: `u13` · 분류: 그래픽 · 셰이더 › 빛 · 환경 · 3D · 난이도 보통 · 폰 부담 보통 (폰 주의)
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/u13

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### u13 견본 항목 — `src/demos/demosLight.ts:780`
```ts
  u13: {
    kind: '3d',
    caption: '셰이더로 그린 하늘(금빛 동굴 → 밤 보라 → 얼음 하늘)을 구워 반사 지도로 — 금화 · 수정 빛깔이 하늘 따라 바뀌어요',
    make() {
      const scene = new THREE.Scene();
      scene.add(new THREE.HemisphereLight(0xffffff, 0x404050, 0.25));
      const key = new THREE.DirectionalLight(0xffffff, 0.6);
      key.position.set(2, 4, 3);
      scene.add(key);
      const sky = new THREE.Mesh(new THREE.SphereGeometry(30, 48, 24), skyMat(MOODS[0]!));
      scene.add(sky);
      const root = new THREE.Group();
      const coinM = std(0xffc24a, 0.22, 1);
      const coinG = new THREE.CylinderGeometry(0.36, 0.36, 0.09, 40);
      for (let i = 0; i < 6; i++) {
        const c = mesh(coinG, coinM, -0.75 + Math.sin(i * 2.1) * 0.04, -0.6 + i * 0.095, Math.cos(i * 1.7) * 0.04);
        root.add(c);
      }
      const ball = mesh(new THREE.SphereGeometry(0.42, 48, 32), std(0xffffff, 0.04, 1), 0.72, 0.35, -0.1);
      const crystal = mesh(
        new THREE.OctahedronGeometry(0.42, 0),
        new THREE.MeshPhysicalMaterial({ color: 0xcdf3ff, roughness: 0.04, metalness: 0.25, clearcoat: 1, flatShading: true }),
        0,
        0.2,
        0.35,
      );
      crystal.scale.set(0.8, 1.5, 0.8);
      const ring = mesh(new THREE.TorusGeometry(0.32, 0.09, 24, 64), std(0xffffff, 0.15, 1), 0.6, -0.5, 0.3);
      root.add(ball, crystal, ring);
      scene.add(root);
      const cam = new THREE.PerspectiveCamera(36, 1, 0.1, 100);
      const hud = new Hud();
      const name = hud.label(MOODS[0]!.name, 0.04, 0.95, 0, 1, 'on');
      hud.label('하늘 셰이더 → 반사 지도', 0.5, 0.05, 0.5, 0, 'tag', 0.85);
      let envs: { r: R; tex: THREE.Texture[] } | null = null;
      let shift = 0;
      let t0 = 0;
      let mood = -1;
      const apply = (i: number): void => {
        const m = MOODS[i]!;
        const u = sky.material.uniforms;
        (u['top']!.value as THREE.Color).setRGB(...m.top);
        (u['bot']!.value as THREE.Color).setRGB(...m.bot);
        (u['sun']!.value as THREE.Color).setRGB(...m.sun);
        u['sunK']!.value = m.k;
        name.set(m.name);
        if (envs) scene.environment = envs.tex[i]!;
      };
      let now = 0;
      return {
        scene,
        camera: cam,
        update(t) {
          now = t;
          const i = (Math.floor((t - t0) / 3.5) + shift) % MOODS.length;
          if (i !== mood) {
            mood = i;
            apply(i);
          }
          root.rotation.y = t * 0.35;
          crystal.rotation.y = -t * 0.8;
          ring.rotation.x = t * 0.9;
        },
        render(r, w, h) {
          if (!envs || envs.r !== r) {
            envs?.tex.forEach((x) => x.dispose());
            const pm = new THREE.PMREMGenerator(r);
            const geo = new THREE.SphereGeometry(10, 48, 24);
            const tex = MOODS.map((m) => {
              const es = new THREE.Scene();
              const mat = skyMat(m);
              es.add(new THREE.Mesh(geo, mat));
              const tx = pm.fromScene(es, 0.02).texture;
              mat.dispose();
              return tx;
            });
            geo.dispose();
            pm.dispose();
            envs = { r, tex };
            scene.environment = tex[Math.max(0, mood)]!;
          }
          const a = Math.sin(now * 0.25) * 0.5;
          fit(cam, w / h, V3(0, 0, 0), 1.75, V3(Math.sin(a), 0.25, Math.cos(a)));
          r.render(scene, cam);
          hud.draw(r, w, h);
        },
        controls: [
          {
            type: 'button',
            label: '다음 하늘',
            on: () => {
              shift += 1;
            },
          },
          { type: 'range', label: '반사 세기', min: 0, max: 2.5, step: 0.05, value: 1, on: (v) => (scene.environmentIntensity = v) },
        ],
        dispose() {
          envs?.tex.forEach((x) => x.dispose());
          hud.dispose();
          disposeTree(scene);
        },
      };
    },
  }
```

## 관련 기술
- 이 효과를 만든 기술: [환경 반사 (RoomEnvironment + PMREM)](https://ai-techstudio.web.app/ai/t/u12.md) `u12`
