# AI 꾸러미 — 고리 행성 (고리 그림자 · 행성 그림자) — Analytic shadows (ray-plane / ray-sphere)
> 그림자 지도 없이 — 행성 픽셀에서 해 쪽으로 쏜 빛줄이 고리 면과 만나는 반지름의 밀도로 고리 그림자를, 고리 픽셀에서 쏜 빛줄이 행성에 닿으면 행성 그림자를 계산한다.  
> 견본: https://ai-techstudio.web.app/#t/i527

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

## 주문서

### 만들어 줘: 고리 행성 (고리 그림자 · 행성 그림자) — Analytic shadows (ray-plane / ray-sphere)

#### 1. 목표
토성 같은 고리 행성에 고리 행성을 만들어 줘 — 고리 그림자 · 행성 그림자를 그림자 지도 없이 식으로, 느낌은 탐사선 사진처럼.

#### 2. 핵심 기술 용어
- **Analytic shadows (ray-plane / ray-sphere)** — 식으로 푸는 그림자 — 빛줄이 면 · 구와 만나는지
- **Ring density function** — 반지름에 따른 고리 짙기 (카시니 틈 · 엥케 틈)
- **Oblate spheroid** — 납작한 회전 타원체 (y 0.9배)

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

#### 4. 조건
- 해 방향 · 카메라 위치를 그룹 역행렬로 바꿔(uSunG · uCamG) 행성 · 고리에 같이 넘긴다
- 고리 밀도 함수 ringD(r) 를 행성 · 고리 셰이더가 함께 쓴다 (같은 문자열)
- 행성이 납작(scale.y 0.9)하면 그림자 계산도 y 를 1/0.9 로 편 공간에서
- 고리는 DoubleSide · transparent · depthWrite false · renderOrder 1, 밀도 0.002 아래는 discard
- 잔 홈은 fwidth 로 멀리서는 줄여 깜빡임(모아레)을 막는다

#### 5. 완성 기준 (이게 보이면 성공)
- 행성 위에 고리 그림자 띠가 또렷하고, 카시니 틈 자리는 그림자도 비어 있다
- 고리 뒤쪽이 행성 그림자에 가려 어둡게 끊긴다
- 해가 움직이면 두 그림자가 함께 옮겨 간다 — 「해 방향」 · 「고리 기울기」 조절
- 멀리서 봐도 고리 잔 홈이 깜빡이지 않는다

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

## 원리
- 행성과 고리를 같은 묶음(그룹) 좌표에서 계산한다. 해 방향 · 카메라 위치를 그룹 역행렬로 바꿔 넘긴다.
- 행성 픽셀: 해 쪽으로 빛줄을 쏘아 고리 면(y = 0)과 만나는 지점까지 t = -y / 해.y. 그 점의 반지름으로 고리 밀도를 읽어 그만큼 어둡게.
- 고리 픽셀: 해 쪽으로 쏜 빛줄이 납작한 행성(y 를 1/0.9 배로 펴서 구로)과 앞에서 만나면 행성 그림자 (밝기 0.04).
- 고리 밀도는 반지름 함수 하나: C 고리 옅게 · B 고리 짙게 · 카시니 틈 · A 고리 · 엥케 틈 · 잔 홈. 같은 함수를 둘이 함께 쓴다.
- 고리를 해가 비추는 면에서 보면 밝고, 뒷면에서 보면 얇은 곳만 빛이 새어 옅게 밝다.

## 핵심 코드 — 고리 밀도 함수 + 행성 위 고리 그림자 + 고리 위 행성 그림자
(발췌: demos/demosSpace.ts RING_FN · SAT_FS · RING_FS 를 정리)
```glsl
float ringD(float r, float fw){
  float d = 0.0;
  d += smoothstep(1.24, 1.27, r) * (1.0 - smoothstep(1.50, 1.53, r)) * 0.16;                      // C 고리
  d += smoothstep(1.52, 1.55, r) * (1.0 - smoothstep(1.92, 1.95, r)) * (0.82 + 0.14 * sin(r * 47.0)); // B 고리
  d += smoothstep(2.02, 2.04, r) * (1.0 - smoothstep(2.26, 2.28, r)) * 0.62;                      // A 고리 (사이 = 카시니 틈)
  d *= 1.0 - 0.92 * exp(-pow((r - 2.215) / 0.006, 2.0));                                           // 엥케 틈
  float g = sin(r * 420.0) * sin(r * 173.0 + 1.0) * 0.5 + sin(r * 97.0 + 2.0) * 0.5;
  d *= 1.0 + 0.22 * g * (1.0 - smoothstep(0.004, 0.03, fw * 120.0));                                // 멀면 잔 홈 줄이기
  return clamp(d, 0.0, 1.0);
}
// 행성 (vGP = 그룹 좌표 점, uSunG = 그룹 좌표 해 방향)
float sh = 1.0;
if (abs(uSunG.y) > 1e-4) {
  float t = -vGP.y / uSunG.y;                      // 해 쪽 빛줄이 고리 면(y = 0)과 만나는 거리
  if (t > 0.0) sh = 1.0 - 0.9 * ringD(length(vGP.xz + uSunG.xz * t), 0.0) * uRing;
}
// 고리 — 해 쪽 빛줄이 납작한 행성(y 를 1/uFlat 로 펴서 구)에 앞에서 닿는가
vec3 ro = vGP * vec3(1.0, 1.0 / uFlat, 1.0);
vec3 rd = normalize(uSunG * vec3(1.0, 1.0 / uFlat, 1.0));
float b = dot(ro, rd), hh = b * b - (dot(ro, ro) - 1.0);
float shadow = (hh > 0.0 && -b - sqrt(hh) > 0.0) ? 0.04 : 1.0;
```

## 흔한 실수 · 확인 목록
- [ ] **고리에 그림자 지도를 쓰면 얇은 고리 그림자가 계단 지거나 사라진다** — 면 · 구 사이 그림자는 식으로 풀면 해상도와 상관없이 또렷하다.
- [ ] **납작한 행성을 구로 계산하면 그림자 끝이 어긋난다** — 고리 점 · 해 방향의 y 를 1/0.9 로 펴서 단위 구와 교차한다.
- [ ] **고리 잔 홈이 멀리서 무지개 줄무늬로 깜빡인다** — fwidth(r) 로 픽셀 하나에 홈이 여러 개 들어가면 홈 세기를 줄인다.

## 완성 기준 체크리스트
- [ ] 행성 위에 고리 그림자 띠가 또렷하고, 카시니 틈 자리는 그림자도 비어 있다
- [ ] 고리 뒤쪽이 행성 그림자에 가려 어둡게 끊긴다
- [ ] 해가 움직이면 두 그림자가 함께 옮겨 간다 — 「해 방향」 · 「고리 기울기」 조절
- [ ] 멀리서 봐도 고리 잔 홈이 깜빡이지 않는다

## 이 기술 정보
- id: `i527` · 분류: 그래픽 · 셰이더 › 우주 표현 · 3D · 난이도 어려움 · 폰 부담 보통 (폰 주의) — 그림자 지도 없음. 행성 픽셀마다 3D 텍스처 2번 + 고리 함수 1번, 고리 픽셀마다 구 교차 1번. 고리 RingGeometry 256 조각.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i527
- 쓰면 좋을 때: 고리 행성이 주인공인 장면 / 그림자 지도 없이 또렷한 그림자가 필요한 모양 (면 · 구) / 폰에서도 고리 그림자를 보여 주고 싶을 때
- 쓰지 말 때: 모양이 복잡한 물체의 그림자 — 대신 그림자 지도(u17) / 고리가 작은 점처럼 보이는 먼 행성 — 텍스처 한 장이면 충분

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### i527 견본 항목 — `src/demos/demosSpace.ts:2101`
```ts
  i527: {
    kind: '3d',
    caption: '고리 행성 — 행성 픽셀에서 해 쪽으로 쏜 빛줄이 고리 면을 지나면 고리 그림자, 고리 픽셀에서 쏜 빛줄이 행성에 닿으면 행성 그림자',
    make(): Scene3D {
      const bin = new Bin();
      const scene = new THREE.Scene();
      scene.background = new THREE.Color(0x000000);
      const cam = new THREE.PerspectiveCamera(36, 1.6, 0.1, 1000);
      cam.position.set(0.6, 1.5, 8.6);
      cam.lookAt(0, -0.1, 0);
      const S = makeStars(12000, 300, 55, { magMax: 7, tw: 0.1, gain: 0.8 });
      scene.add(S.pts);
      const grp = new THREE.Group();
      grp.rotation.set(0.12, 0, 0.42);
      scene.add(grp);
      const sun = new THREE.Vector3();
      const U = { uN3: { value: noise3D() }, uSun: { value: sun }, uSunG: { value: new THREE.Vector3() }, uCamG: { value: new THREE.Vector3() }, uTime: { value: 0 }, uFlat: { value: 0.9 }, uRing: { value: 1 } };
      const planet = new THREE.Mesh(new THREE.SphereGeometry(1, 128, 96), new THREE.ShaderMaterial({ uniforms: U, vertexShader: SAT_VS, fragmentShader: SAT_FS }));
      planet.scale.y = 0.9;
      grp.add(planet);
      const rg = new THREE.RingGeometry(1.22, 2.36, 256, 2);
      rg.rotateX(-Math.PI / 2);
      const ring = new THREE.Mesh(rg, new THREE.ShaderMaterial({ uniforms: U, vertexShader: RING_VS, fragmentShader: RING_FS, side: THREE.DoubleSide, transparent: true, depthWrite: false }));
      ring.renderOrder = 1;
      grp.add(ring);
      bin.scene(scene);
      let auto = true;
      let manual = 40;
      let tilt = 0.42;
      const inv = new THREE.Matrix4();
      return {
        scene,
        camera: cam,
        tone: THREE.ACESFilmicToneMapping,
        update(t) {
          S.U.uTime.value = t;
          U.uTime.value = t;
          grp.rotation.z = tilt;
          const az = THREE.MathUtils.degToRad(auto ? -10 + 60 * Math.sin(t * 0.12) : manual);
          sun.set(-Math.cos(az), 0.42, Math.sin(az) * 0.9 + 0.35).normalize();
          planet.rotation.y = t * 0.05;
          grp.updateMatrixWorld();
          inv.copy(grp.matrixWorld).invert();
          U.uSunG.value.copy(sun).transformDirection(inv);
          U.uCamG.value.copy(cam.position).applyMatrix4(inv);
        },
        resize: (_w, h) => (S.U.uScale.value = h / 900),
        controls: [
          tog('해가 저절로 움직이기', true, (v) => (auto = v)),
          range('해 방향 (자동 끄면)', -80, 80, 1, 40, (v) => (manual = v)),
          range('고리 기울기', -0.2, 1.0, 0.01, 0.42, (v) => (tilt = v)),
          range('고리 짙기', 0, 1.5, 0.05, 1, (v) => (U.uRing.value = v)),
        ],
        dispose: () => {
          bin.dispose();
          S.dispose();
        },
      };
    },
  }
```

## 관련 기술
- 먼저 알면 좋은 기술: [행성 대기 (림 산란 · 낮밤 경계 · 구름 · 도시 불빛)](https://ai-techstudio.web.app/ai/t/i526.md) `i526`
- 다음에 해 볼 기술: [소행성대 인스턴싱 + 렌즈 플레어](https://ai-techstudio.web.app/ai/t/i531.md) `i531` · [태양 표면 · 코로나 · 홍염](https://ai-techstudio.web.app/ai/t/i528.md) `i528`
- 참고 문서: [Wikipedia — Rings of Saturn](https://en.wikipedia.org/wiki/Rings_of_Saturn)
