# AI 꾸러미 — 하루 시간 흐름 (해 · 하늘색) — Day-night cycle
> 시각(0~24시) 하나로 해 방향을 정하고, 빛 색 · 세기 · 그림자 길이 · 하늘색 · 창 불빛을 함께 바꿔 하루가 흐르게 한다.  
> 견본: https://ai-techstudio.web.app/#t/i431

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

## 주문서

### 만들어 줘: 하루 시간 흐름 (해 · 하늘색) — Day-night cycle

#### 1. 목표
작은 마을 장면에 하루 시간 흐름을 넣어 줘 — 시각 하나로 해 · 하늘색 · 그림자 · 불빛이 함께 아침 → 낮 → 노을 → 밤로 바뀌게.

#### 2. 핵심 기술 용어
- **Day-night cycle** — 하루 시간 흐름 (해가 뜨고 짐)
- **Sun elevation driven lighting** — 해 높이 하나로 모든 빛 값 정하기
- **Color keyframes (night · dusk · day)** — 밤 · 노을 · 낮 세 묶음 색 사이 섞기

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

#### 4. 조건
- 해 · 달은 같은 방향 빛 하나를 돌려 쓴다 (빛 개수가 바뀌면 셰이더를 다시 굽는다)
- 색은 묶음 셋(night · dusk · day) 사이 smoothstep 섞기 — 해 높이 -0.28 ~ 0 은 밤→노을, 0.02 ~ 0.32 는 노을→낮
- 해빛 세기 = 3.0 × smoothstep(-0.03, 0.22, 높이), 지평선 아래면 달빛 0.45
- 창 · 가로등은 발광 세기만 (night × 2.2 · 0.3 + night × 3), 빛무리 스프라이트 투명도도 night
- 시간은 dt 로 흐르게 (프레임 수가 아니라) — 느린 폰에서도 같은 빠르기

#### 5. 완성 기준 (이게 보이면 성공)
- 시계 표시가 흐르며 새벽 → 아침 → 낮 → 노을 → 밤 순서로 하늘 · 빛이 바뀐다
- 낮에는 그림자가 짧고, 노을 무렵 길어지며 주황빛이 된다
- 밤이 되면 하늘에 별 · 달이 보이고 창 · 가로등이 켜진다
- 「시각」 슬라이더로 원하는 시간에 멈추고, 「빠르기」로 흐름을 바꾼다

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

## 원리
- 시각을 각도로 바꿔 해 방향을 정한다: a = (시각 - 6) / 24 × 2π, 방향 = (cos a, sin a × 0.9, -1.1) 정규화. 6시에 뜨고 18시에 진다.
- 해 높이(방향의 y)로 색 묶음 세 개(밤 · 노을 · 낮) 사이를 섞는다 — 하늘 위 · 지평선 · 반구 빛 · 땅 색 모두.
- 해가 낮으면 빛이 주황으로, 높으면 하얗게. 지평선 아래로 가면 같은 빛을 반대편에 두고 달빛(푸른색, 세기 0.45)으로 바꾼다.
- 어두워질수록(night) 창 · 가로등 빛을 켠다 — 빛 하나 더 넣지 않고 발광 세기만 올린다.

## 핵심 코드 — 시각 하나 → 해 방향 · 색 · 세기 · 불빛
(발췌: demos/demosCamLight.ts demoDay() · SKY_KEYS · skyMix() 를 정리)
```ts
import * as THREE from 'three';

const C = (h: number): THREE.Color => new THREE.Color(h);
const KEYS = {
  night: { top: C(0x060b26), hor: C(0x1a2552), hemi: C(0x27335e), gnd: C(0x0c0f1c) },
  dusk: { top: C(0x3a3f80), hor: C(0xff9a5c), hemi: C(0xf2a88a), gnd: C(0x4a3a40) },
  day: { top: C(0x3b8ee6), hor: C(0xcbe7ff), hemi: C(0xcfe6ff), gnd: C(0x6a5a40) },
};
const sstep = (a: number, b: number, x: number): number => { const t = Math.min(1, Math.max(0, (x - a) / (b - a))); return t * t * (3 - 2 * t); };
function skyMix(el: number, key: 'top' | 'hor' | 'hemi' | 'gnd', out: THREE.Color): THREE.Color {
  if (el < 0) return out.copy(KEYS.night[key]).lerp(KEYS.dusk[key], sstep(-0.28, 0, el));
  return out.copy(KEYS.dusk[key]).lerp(KEYS.day[key], sstep(0.02, 0.32, el));
}
const warm = C(0xff8a4a), noon = C(0xfff4e0), moonC = C(0x8fa8ff);
const dir = new THREE.Vector3();
let hour = 5;

function update(dt: number, speed = 2): void {
  hour = (hour + Math.min(dt, 0.05) * speed) % 24;           // 1초에 2시간
  const a = ((hour - 6) / 24) * Math.PI * 2;                 // 6시 해돋이 · 18시 해넘이
  dir.set(Math.cos(a), Math.sin(a) * 0.9, -1.1).normalize();
  const el = dir.y;
  if (el > -0.03) {                                          // 해
    sun.position.copy(dir).multiplyScalar(20);
    sun.color.copy(warm).lerp(noon, sstep(0.0, 0.45, el));
    sun.intensity = 3.0 * sstep(-0.03, 0.22, el);
  } else {                                                   // 같은 빛을 달로
    sun.position.copy(dir).multiplyScalar(-20);
    sun.color.copy(moonC);
    sun.intensity = 0.45 * sstep(-0.03, -0.2, el);
  }
  skyMix(el, 'hemi', hemi.color);
  skyMix(el, 'gnd', hemi.groundColor);
  hemi.intensity = 0.3 + (1.1 - 0.3) * sstep(-0.2, 0.3, el);
  const night = sstep(0.08, -0.08, el);
  winMat.emissiveIntensity = night * 2.2;                    // 창 불빛
  lampMat.emissiveIntensity = 0.3 + night * 3;               // 가로등
}
```

## 흔한 실수 · 확인 목록
- [ ] **해 · 달을 따로 빛 두 개로 두고 켜고 끄면 넘어가는 순간 멈칫한다** — 빛 개수가 바뀌면 재질 셰이더가 다시 컴파일된다. 같은 방향 빛 하나를 반대편으로 옮겨 달로 쓴다.
- [ ] **프레임마다 고정 값만큼 시간을 더하면 느린 폰에서 하루가 느리게 간다** — 실제 dt 로 더한다 (견본은 0.05 로 잘라 튀는 프레임만 막음).
- [ ] **하늘색만 바꾸고 반구 빛 색을 그대로 두면 땅이 하늘과 따로 논다** — 하늘 위 · 지평선 · 반구 · 땅 색을 같은 해 높이로 함께 섞는다.

## 완성 기준 체크리스트
- [ ] 시계 표시가 흐르며 새벽 → 아침 → 낮 → 노을 → 밤 순서로 하늘 · 빛이 바뀐다
- [ ] 낮에는 그림자가 짧고, 노을 무렵 길어지며 주황빛이 된다
- [ ] 밤이 되면 하늘에 별 · 달이 보이고 창 · 가로등이 켜진다
- [ ] 「시각」 슬라이더로 원하는 시간에 멈추고, 「빠르기」로 흐름을 바꾼다

## 이 기술 정보
- id: `i431` · 분류: 그래픽 · 셰이더 › 빛 · 환경 · 3D · 난이도 보통 · 폰 부담 보통 (폰 주의) — 빛 값만 바꾸므로 셰이더는 그대로. 그림자 지도 1024² 한 장 (해 · 달 같은 빛을 돌려 씀) + 가로등 빛무리 스프라이트.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i431
- 쓰면 좋을 때: 시간 · 그림자 길이를 보여 주는 장면 / 하루가 흐르는 생활 · 농장 게임 / 같은 장면을 아침 · 밤 두 분위기로
- 쓰지 말 때: 실제 산란 하늘이 필요할 때 — 대신 대기 산란 하늘(i224) / 시간과 상관없는 퍼즐 판 — 고정 조명이 낫다

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### demoDay — `src/demos/demosCamLight.ts:1681`
```ts
function demoDay(): Scene3D {
  const { scene, cam, sky, vil, sun, hemi } = villageWorld();
  cam.fov = 50;
  cam.updateProjectionMatrix();
  sky.u.uSpan.value = 0.32;
  const glow = glowTex();
  const halos: THREE.Sprite[] = vil.lamps.map((p) => {
    const s = new THREE.Sprite(new THREE.SpriteMaterial({ map: glow, color: 0xffc46b, blending: THREE.AdditiveBlending, depthWrite: false, transparent: true }));
    s.position.copy(p);
    s.scale.setScalar(1.3);
    scene.add(s);
    return s;
  });
  const hud = new Hud();
  const st = { auto: true, hour: 9, speed: 2 };
  let hour = 5;
  const dir = new THREE.Vector3();
  const tc = new THREE.Color();
  const warm = C(0xff8a4a);
  const noon = C(0xfff4e0);
  const moonC = C(0x8fa8ff);
  return {
    scene,
    camera: cam,
    update(t, dt) {
      if (st.auto) hour = (hour + Math.min(dt, 0.05) * st.speed) % 24;
      else hour = st.hour;
      const a = ((hour - 6) / 24) * TAU;
      dir.set(Math.cos(a), Math.sin(a) * 0.9, -1.1).normalize();
      const el = dir.y;
      sky.u.uSun.value.copy(dir);
      skyMix(el, 'top', sky.u.uTop.value);
      skyMix(el, 'hor', sky.u.uHor.value);
      sky.u.uBot.value.copy(sky.u.uHor.value).multiplyScalar(0.7);
      sky.u.uSunK.value = sstep(-0.12, 0.05, el);
      sky.u.uSunCol.value.copy(warm).lerp(noon, sstep(0.0, 0.4, el));
      sky.u.uMoonK.value = sstep(0.0, -0.15, el);
      sky.u.uStars.value = sstep(-0.02, -0.25, el);
      sky.u.uTime.value = t;
      if (el > -0.03) {
        sun.position.copy(dir).multiplyScalar(20);
        sun.color.copy(warm).lerp(noon, sstep(0.0, 0.45, el));
        sun.intensity = 3.0 * sstep(-0.03, 0.22, el);
      } else {
        sun.position.copy(dir).multiplyScalar(-20);
        sun.color.copy(moonC);
        sun.intensity = 0.45 * sstep(-0.03, -0.2, el);
      }
      hemi.color.copy(skyMix(el, 'hemi', tc));
      hemi.groundColor.copy(skyMix(el, 'gnd', tc));
      hemi.intensity = lerp(0.3, 1.1, sstep(-0.2, 0.3, el));
      const night = sstep(0.08, -0.08, el);
      vil.winMat.emissiveIntensity = night * 2.2;
      vil.lampMat.emissiveIntensity = 0.3 + night * 3;
      halos.forEach((s) => ((s.material as THREE.SpriteMaterial).opacity = night));
      const yaw = Math.sin(t * 0.1) * 0.12;
      cam.position.set(Math.sin(yaw) * 13, 4.6, Math.cos(yaw) * 13);
      cam.lookAt(0, 1.4, 0);
    },
    render(r, w, h) {
      withState(r, () => {
        r.render(scene, cam);
        const u = Math.min(1.9, w / 280, h / 175);
        const hh = Math.floor(hour);
        const mi = Math.floor((hour - hh) * 60);
        hud.label('a', `${String(hh).padStart(2, '0')}:${String(Math.floor(mi / 10) * 10).padStart(2, '0')} ${timeWord(hour)}`, 10 * u, 10 * u, 19 * u, { bg: 'rgba(14,20,44,0.75)' });
        hud.draw(r, w, h);
      });
    },
    controls: [
      { type: 'toggle', label: '시간 저절로 흐르기', value: true, on: (v) => (st.auto = v) },
      { type: 'range', label: '시각 (0 ~ 24시)', min: 0, max: 24, step: 0.25, value: 9, on: (v) => ((st.hour = v), (st.auto = false)) },
      { type: 'range', label: '흐르는 빠르기 (시간/초)', min: 0.5, max: 6, step: 0.5, value: 2, on: (v) => (st.speed = v) },
    ] as Control[],
    dispose() {
      disposeTree(scene);
      hud.dispose();
    },
  };
}
```

### i431 견본 항목 — `src/demos/demosCamLight.ts:2481`
```ts
  i431: { kind: '3d', caption: '해 각도 하나로 빛 색 · 세기 · 그림자 길이 · 하늘색이 함께 — 아침 · 낮 · 노을 · 밤', make: () => demoDay() }
```

## 관련 기술
- 먼저 알면 좋은 기술: [그림자 맵 (PCF · PCF Soft)](https://ai-techstudio.web.app/ai/t/u17.md) `u17` · [3점 조명 (반구 + 해 + 테두리)](https://ai-techstudio.web.app/ai/t/u14.md) `u14`
- 다음에 해 볼 기술: [밤 장면 (어둠 속 빛나는 것만)](https://ai-techstudio.web.app/ai/t/i432.md) `i432` · [대기 산란 하늘](https://ai-techstudio.web.app/ai/t/i224.md) `i224`
