# AI 꾸러미 — 행성 대기 (림 산란 · 낮밤 경계 · 구름 · 도시 불빛) — Atmospheric scattering (single scattering)
> 대기 껍질 구에서 빛줄을 12걸음 지나며 레일리(파랑) · 미(해 쪽 빛무리) 산란을 모아 행성 테두리가 빛나게 하고, 땅 · 구름 · 밤쪽 도시 불빛은 3D 잡음으로 그린다.  
> 견본: https://ai-techstudio.web.app/#t/i526

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

## 주문서

### 만들어 줘: 행성 대기 (림 산란 · 낮밤 경계 · 구름 · 도시 불빛) — Atmospheric scattering (single scattering)

#### 1. 목표
지구 같은 행성을 대기가 있는 행성으로 그려 줘 — 테두리 산란 빛 · 노을빛 낮밤 경계 · 구름 · 밤쪽 도시 불빛, 느낌은 우주에서 본 사진처럼.

#### 2. 핵심 기술 용어
- **Atmospheric scattering (single scattering)** — 빛줄을 따라 산란 빛을 모으는 대기
- **Rayleigh / Mie phase function** — 방향에 따른 산란 세기 (파랑 · 해 둘레 빛무리)
- **Ray-sphere intersection** — 빛줄이 구를 들어가고 나오는 거리
- **Day-night terminator + city lights** — 낮밤 경계 · 밤쪽 도시 불빛

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

#### 4. 조건
- 대기 구: AdditiveBlending · depthWrite false · depthTest false · renderOrder 2, 반지름 1.06
- 빛줄 구간은 대기 구 교차 [t0, t1], 땅에 닿으면 t1 = 땅 교차 — 12걸음
- 산란 = 레일리(0.16, 0.4, 1.0) × 0.75(1 + μ²) + 미 (g 0.78) × 해 쪽 색, 투과율은 exp(-β × 공기 두께)
- 땅 무늬 · 구름 · 도시는 3D 잡음 텍스처를 물체 법선으로 읽는다 (UV 이음매 없음)
- 구름 그림자는 해 쪽으로 조금(0.025) 옮긴 자리의 구름으로

#### 5. 완성 기준 (이게 보이면 성공)
- 행성 테두리가 파랗게 빛나고, 낮밤 경계 쪽 대기는 주황 노을빛이다
- 밤쪽에 도시 불빛 점이 보이고 구름이 그 위를 가린다
- 바다에 해 반짝임, 땅에 구름 그림자가 보인다
- 「대기 짙기」 · 「구름 양」 · 「도시 불빛」 · 「자전 빠르기」가 바로 바뀐다

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

## 원리
- 행성(반지름 1) 바깥에 조금 큰 대기 구(1.06)를 더하기로 겹친다. 대기 구 픽셀마다 빛줄이 대기를 지나는 구간을 구 교차로 구한다 (땅에 닿으면 거기까지).
- 그 구간을 12걸음 나눠, 각 점의 공기 밀도(높이에 따라 exp(-5h))와 해까지 · 눈까지 지나는 공기 두께로 빛을 모은다.
- 레일리는 파장별 세기(0.16, 0.4, 1.0)로 파랑이 세고, 해까지 공기가 두꺼우면 파랑이 먼저 사라져 노을빛이 된다. 미는 해 쪽으로 몰린 빛무리(g 0.78).
- 땅 · 바다 · 얼음 · 사막 · 구름 · 도시 불빛은 법선 방향으로 3D 잡음을 읽어 이음매 없이 칠한다. 밤쪽(N·L < 0)에만 도시 불빛.

## 핵심 코드 — 대기 구 — 12걸음 레일리 · 미 산란
(발췌: demos/demosSpace.ts ATMO_FS 를 정리)
```glsl
uniform vec3 uSun; uniform float uRA, uDens;   // uRA = 대기 반지름 1.06
varying vec3 vWP;
vec2 sph(vec3 ro, vec3 rd, float r){
  float b = dot(ro, rd), c = dot(ro, ro) - r * r, h = b * b - c;
  if (h < 0.0) return vec2(1e9, -1e9);
  h = sqrt(h);
  return vec2(-b - h, -b + h);
}
void main(){
  vec3 ro = cameraPosition, rd = normalize(vWP - cameraPosition);
  vec2 a = sph(ro, rd, uRA), p = sph(ro, rd, 1.0);
  float t0 = max(a.x, 0.0), t1 = a.y;
  if (p.x > 0.0) t1 = min(t1, p.x);             // 땅에 닿으면 거기까지
  if (t1 <= t0) discard;
  float ds = (t1 - t0) / 12.0;
  vec3 betaR = vec3(0.16, 0.4, 1.0);            // 파랑이 가장 많이 흩어진다
  float mu = dot(rd, uSun);
  float phR = 0.75 * (1.0 + mu * mu);
  float g = 0.78;
  float phM = (1.0 - g * g) / pow(1.0 + g * g - 2.0 * g * mu, 1.5) * 0.05;
  vec3 sum = vec3(0.0), sumM = vec3(0.0); float od = 0.0;
  for (int i = 0; i < 12; i++) {
    vec3 q = ro + rd * (t0 + ds * (float(i) + 0.5));
    float dn = exp(-(length(q) - 1.0) / (uRA - 1.0) * 5.0);   // 높을수록 옅은 공기
    od += dn * ds;
    float cs = dot(normalize(q), uSun);
    float lit = smoothstep(-0.22, 0.08, cs);                  // 밤쪽은 빛을 못 받음
    float air = 1.0 / (max(cs, 0.0) * 0.9 + 0.1);             // 해까지 공기 두께
    vec3 Tsun = exp(-betaR * air * 0.5 * uDens - vec3(0.02) * air);
    vec3 Tview = exp(-betaR * od * 7.0 * uDens);
    sum += dn * lit * Tsun * Tview * ds;
    sumM += dn * dn * lit * Tsun * Tview * ds;
  }
  vec3 col = (sum * betaR * phR * 20.0 + sumM * phM * 34.0 * vec3(1.0, 0.85, 0.7)) * uDens;
  gl_FragColor = vec4(col, 1.0);
}
```

## 흔한 실수 · 확인 목록
- [ ] **대기 구에 깊이 검사를 켜면 행성 뒤쪽 테두리가 잘린다** — 대기 구는 depthTest false 로 행성 위에 더하고, 땅에 닿는 곳은 셰이더의 구 교차로 자른다.
- [ ] **구 UV 로 땅 무늬를 그리면 이음매 · 극 찌그러짐이 보인다** — 물체 법선으로 3D 잡음을 읽으면 이음매가 없다.
- [ ] **해 방향을 세계 좌표로만 넘기면 행성이 돌 때 구름 그림자가 어긋난다** — 행성 역행렬로 바꾼 해 방향(uSunObj)을 따로 넘긴다.

## 완성 기준 체크리스트
- [ ] 행성 테두리가 파랗게 빛나고, 낮밤 경계 쪽 대기는 주황 노을빛이다
- [ ] 밤쪽에 도시 불빛 점이 보이고 구름이 그 위를 가린다
- [ ] 바다에 해 반짝임, 땅에 구름 그림자가 보인다
- [ ] 「대기 짙기」 · 「구름 양」 · 「도시 불빛」 · 「자전 빠르기」가 바로 바뀐다

## 이 기술 정보
- id: `i526` · 분류: 그래픽 · 셰이더 › 우주 표현 · 3D · 난이도 어려움 · 폰 부담 보통 (폰 주의) — 대기 구 픽셀마다 12걸음(exp 몇 번), 행성 픽셀마다 3D 텍스처 약 12번. 행성이 화면을 크게 덮으면 폰에서 무겁다.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i526
- 쓰면 좋을 때: 우주에서 본 지구 · 행성 / 낮밤 경계 · 노을이 보이는 구도 / 구름 그림자 · 바다 반짝임까지 필요한 행성
- 쓰지 말 때: 땅 위에서 올려다본 하늘 — 대신 대기 산란 하늘(i224) / 작게 보이는 행성 여러 개 — 텍스처 행성 + 테두리 빛(i10) 이 가볍다

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### i526 견본 항목 — `src/demos/demosSpace.ts:2043`
```ts
  i526: {
    kind: '3d',
    caption: '행성 대기 — 빛줄을 12걸음 지나며 레일리(파랑) · 미(해 쪽 빛무리) 산란을 모아 테두리가 빛나고 낮밤 경계는 노을빛, 밤쪽엔 도시 불빛 · 구름 그림자',
    make(): Scene3D {
      const bin = new Bin();
      const scene = new THREE.Scene();
      scene.background = new THREE.Color(0x000000);
      const cam = new THREE.PerspectiveCamera(38, 1.6, 0.01, 1000);
      cam.position.set(-0.5, 0.55, 3.1);
      cam.lookAt(-0.62, 0.05, 0);
      const S = makeStars(12000, 300, 44, { magMax: 7, tw: 0.1, gain: 0.8 });
      scene.add(S.pts);
      const sun = new THREE.Vector3(-1, 0.28, -0.55).normalize();
      const U = { uN3: { value: noise3D() }, uSun: { value: sun }, uSunObj: { value: new THREE.Vector3() }, uTime: { value: 0 }, uCloudRot: { value: 0 }, uCloud: { value: 0.5 }, uLights: { value: 1 } };
      const planet = new THREE.Mesh(new THREE.SphereGeometry(1, 160, 96), new THREE.ShaderMaterial({ uniforms: U, vertexShader: PLANET_VS, fragmentShader: EARTH_FS }));
      planet.rotation.z = 0.35;
      scene.add(planet);
      const UA = { uSun: { value: sun }, uRA: { value: 1.06 }, uDens: { value: 1 } };
      const atmo = new THREE.Mesh(new THREE.SphereGeometry(1.06, 128, 64), new THREE.ShaderMaterial({ uniforms: UA, vertexShader: ATMO_VS, fragmentShader: ATMO_FS, transparent: true, depthWrite: false, depthTest: false, blending: THREE.AdditiveBlending }));
      atmo.renderOrder = 2;
      scene.add(atmo);
      bin.scene(scene);
      let spin = 1;
      let rot = 0;
      let last = 0;
      const inv = new THREE.Matrix4();
      return {
        scene,
        camera: cam,
        tone: THREE.ACESFilmicToneMapping,
        update(t) {
          const dt = Math.min(0.05, Math.max(0, t - last));
          last = t;
          rot += dt * 0.03 * spin;
          planet.rotation.y = rot + 2.2;
          U.uTime.value = t;
          U.uCloudRot.value = rot * 0.35;
          S.U.uTime.value = t;
          planet.updateMatrixWorld();
          inv.copy(planet.matrixWorld).invert();
          U.uSunObj.value.copy(sun).transformDirection(inv);
        },
        resize: (_w, h) => (S.U.uScale.value = h / 900),
        controls: [
          range('대기 짙기', 0, 2.5, 0.05, 1, (v) => (UA.uDens.value = v)),
          range('구름 양', 0, 1, 0.01, 0.5, (v) => (U.uCloud.value = v)),
          range('도시 불빛', 0, 3, 0.05, 1, (v) => (U.uLights.value = v)),
          range('자전 빠르기', 0, 6, 0.1, 1, (v) => (spin = v)),
        ],
        dispose: () => {
          bin.dispose();
          S.dispose();
        },
      };
    },
  }
```

## 관련 기술
- 먼저 알면 좋은 기술: [대기 산란 하늘](https://ai-techstudio.web.app/ai/t/i224.md) `i224` · [테두리 빛 (rim light, 프레넬)](https://ai-techstudio.web.app/ai/t/i10.md) `i10`
- 다음에 해 볼 기술: [고리 행성 (고리 그림자 · 행성 그림자)](https://ai-techstudio.web.app/ai/t/i527.md) `i527` · [태양 표면 · 코로나 · 홍염](https://ai-techstudio.web.app/ai/t/i528.md) `i528`
- 참고 문서: [Wikipedia — Rayleigh scattering](https://en.wikipedia.org/wiki/Rayleigh_scattering)
