# AI 꾸러미 — 블랙홀 (중력 렌즈 · 강착 원반 도플러) — Gravitational lensing (ray bending)
> 픽셀마다 빛줄을 슈바르츠실트 근사 가속도로 휘며 96걸음 따라가, 원반 면을 지날 때 색을 모으고 남은 빛은 휜 방향의 하늘을 읽어 블랙홀을 그린다.  
> 견본: https://ai-techstudio.web.app/#t/i529

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

## 주문서

### 만들어 줘: 블랙홀 (중력 렌즈 · 강착 원반 도플러) — Gravitational lensing (ray bending)

#### 1. 목표
우주 컷신에 블랙홀을 그려 줘 — 빛줄을 휘며 따라가 뒤 하늘이 휘고 원반 뒤쪽이 위아래로 보이게, 느낌은 영화 같은 사실감.

#### 2. 핵심 기술 용어
- **Gravitational lensing (ray bending)** — 중력으로 빛줄이 휘는 것
- **Schwarzschild approximation a = -1.5 h² r / r⁵** — 빛줄 가속도 근사식 (h = 각운동량)
- **Accretion disk Doppler beaming** — 다가오는 쪽 원반이 밝고 파래지는 도플러
- **Gravitational redshift** — 블랙홀 가까울수록 빛이 붉어짐

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

#### 4. 조건
- 카메라를 감싸는 구(반지름 50, BackSide, depthWrite false)를 매 프레임 카메라 자리로
- 반복 상한은 상수(96), 블랙홀 안(r < 1) · 멀리 빠짐(r > 46 이고 멀어짐) · 투과율 < 0.01 이면 break
- 걸음 크기는 거리에 비례 clamp(0.07r, 0.025, 1.6) — 가까이서만 촘촘하게
- 원반은 빛줄이 y 부호를 바꿀 때 그 사이를 이어 교차점을 구한다 (y = 0 면)
- 뒤 하늘은 미리 그린 등거리 원통 지도(2048 × 1024)를 휜 방향으로 읽는다

#### 5. 완성 기준 (이게 보이면 성공)
- 블랙홀 둘레로 뒤 하늘 별 · 은하수가 휘어 고리처럼 보인다
- 원반의 뒤쪽이 블랙홀 위 · 아래로 휘어 올라와 보인다
- 원반의 한쪽(다가오는 쪽)이 더 밝고 파랗다
- 「중력 렌즈」를 0 으로 하면 하늘이 곧게 펴지고, 「보는 높이」에 따라 원반 모양이 바뀐다

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

## 원리
- 카메라를 감싸는 큰 구(뒷면)를 그려 픽셀마다 빛줄 하나를 만든다. 시작은 카메라, 방향은 그 픽셀 쪽.
- 한 걸음마다 블랙홀 쪽으로 가속도 -1.5 h² p / r⁵ 를 더해 방향을 휜다 (h² = |p × v|², 처음 한 번). 걸음 크기는 멀면 크게(0.07r, 0.025 ~ 1.6).
- 빛줄이 원반 면(y = 0)을 건너면 그 반지름(2.4 ~ 15)의 원반 색 · 밀도를 모으고 투과율을 줄인다. 그래서 원반 뒤쪽이 블랙홀 위 · 아래로 휘어 보인다.
- 원반이 도는 속도로 도플러 계수를 구해 다가오는 쪽을 g³ 만큼 밝고 파랗게, 중력 적색편이 √(1 - 1/r) 로 안쪽을 붉게.
- 반지름 1 안에 들어가면 검정, 멀리 빠져나가면 휜 방향으로 하늘 그림을 읽는다 — 아인슈타인 고리가 생긴다.

## 핵심 코드 — 빛줄 휘기 + 원반 교차 + 도플러
(발췌: demos/demosSpace.ts BH_FS 를 정리)
```glsl
uniform sampler2D uSky, uN2; uniform float uTime, uLens, uDopp, uDisk;
varying vec3 vWP;
void main(){
  vec3 p = cameraPosition, v = normalize(vWP - cameraPosition);
  vec3 cr = cross(p, v); float h2 = dot(cr, cr);   // 각운동량² (처음 한 번)
  vec3 col = vec3(0.0); float T = 1.0;
  for (int i = 0; i < 96; i++) {
    float r2 = dot(p, p), r = sqrt(r2);
    float dt = clamp(0.07 * r, 0.025, 1.6);        // 가까울수록 촘촘하게
    v += -1.5 * h2 * p / (r2 * r2 * r) * uLens * dt; // 슈바르츠실트 근사 가속도
    vec3 pn = p + v * dt;
    if (p.y * pn.y < 0.0) {                          // 원반 면(y = 0)을 건넜다
      vec3 q = mix(p, pn, p.y / (p.y - pn.y));
      float rq = length(q.xz);
      if (rq > 2.4 && rq < 15.0) {
        float n1 = texture(uN2, vec2(atan(q.z, q.x) / 6.2831853 * 2.0 - uTime * 0.5 / pow(rq, 1.5) * 0.9, rq * 0.11)).r;
        float dens = smoothstep(2.4, 3.3, rq) * smoothstep(15.0, 6.5, rq) * clamp(0.25 + 0.9 * n1, 0.0, 1.3);
        float temp = pow(3.0 / rq, 0.75) * 1.05;
        vec3 vel = vec3(-q.z, 0.0, q.x) / rq * sqrt(0.5 / max(rq - 1.0, 0.5));
        float bta = length(vel), gam = 1.0 / sqrt(1.0 - bta * bta);
        float gd = 1.0 / (gam * (1.0 - dot(vel, -normalize(v))));   // 도플러
        float g = mix(1.0, gd * sqrt(max(1.0 - 1.0 / rq, 0.05)), uDopp); // × 중력 적색편이
        vec3 ec = diskCol(temp * g) * pow(g, 3.0) * dens * temp * temp * 4.2 * uDisk;
        float a = clamp(dens * 0.92, 0.0, 1.0);
        col += T * ec * a; T *= 1.0 - a;
      }
    }
    p = pn;
    float rn = length(p);
    if (rn < 1.0) { T = 0.0; break; }                 // 사건의 지평선
    if (rn > 46.0 && dot(p, v) > 0.0) break;           // 빠져나감
    if (T < 0.01) break;
  }
  col += T * sky(normalize(v)) * 0.9;                // 휜 방향의 하늘
  gl_FragColor = vec4(col, 1.0);
}
```

## 흔한 실수 · 확인 목록
- [ ] **걸음을 고르게 잘게 하면 너무 느리고, 크게 하면 원반이 빠진다** — 걸음을 거리에 비례하게 — 블랙홀 가까이서만 촘촘하다.
- [ ] **원반을 걸음 위치에서만 검사하면 얇은 원반을 건너뛴다** — 두 걸음 사이 y 부호가 바뀌면 그 사이를 이어 교차점을 구한다.
- [ ] **감싸는 구를 카메라에 붙이지 않으면 멀어질 때 하늘이 잘린다** — 매 프레임 ball.position = 카메라 위치.

## 완성 기준 체크리스트
- [ ] 블랙홀 둘레로 뒤 하늘 별 · 은하수가 휘어 고리처럼 보인다
- [ ] 원반의 뒤쪽이 블랙홀 위 · 아래로 휘어 올라와 보인다
- [ ] 원반의 한쪽(다가오는 쪽)이 더 밝고 파랗다
- [ ] 「중력 렌즈」를 0 으로 하면 하늘이 곧게 펴지고, 「보는 높이」에 따라 원반 모양이 바뀐다

## 이 기술 정보
- id: `i529` · 분류: 그래픽 · 셰이더 › 우주 표현 · 3D · 난이도 어려움 · 폰 부담 무거움 — 픽셀마다 최대 96걸음 (걸음마다 원반 교차 · 잡음 2번). 화면 전체를 덮으므로 폰에서는 렌더 해상도를 낮춰야 한다.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i529
- 쓰면 좋을 때: 블랙홀이 주인공인 컷신 · 보스 / 중력 렌즈를 보여 주는 학습 / 뒤 하늘이 휘는 신비한 연출
- 쓰지 말 때: 폰 전체 화면 — 픽셀마다 96걸음은 무겁다. 해상도를 반으로 / 작게 보이는 블랙홀 — 검은 원 + 빛 고리 스프라이트로 충분

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### i529 견본 항목 — `src/demos/demosSpace.ts:2207`
```ts
  i529: {
    kind: '3d',
    caption: '블랙홀 — 픽셀마다 빛줄을 휘게 따라가(슈바르츠실트 근사) 뒤 하늘이 휘고 원반 뒤쪽이 위아래로 보인다. 다가오는 쪽 원반은 도플러로 더 밝고 파랗다',
    make(): Scene3D {
      const bin = new Bin();
      const scene = new THREE.Scene();
      const cam = new THREE.PerspectiveCamera(48, 1.6, 0.1, 1000);
      const sky = bin.add(texOf(bhSkyCanvas(), true));
      sky.generateMipmaps = false;
      sky.minFilter = THREE.LinearFilter;
      const U = { uSky: { value: sky as THREE.Texture }, uN2: { value: noise2D() }, uTime: { value: 0 }, uLens: { value: 1 }, uDopp: { value: 1 }, uDisk: { value: 1 } };
      const ball = new THREE.Mesh(new THREE.SphereGeometry(50, 64, 32), new THREE.ShaderMaterial({ uniforms: U, vertexShader: BH_VS, fragmentShader: BH_FS, side: THREE.BackSide, depthWrite: false }));
      scene.add(ball);
      bin.scene(scene);
      let elev = 9;
      return {
        scene,
        camera: cam,
        tone: THREE.ACESFilmicToneMapping,
        update(t) {
          U.uTime.value = t;
          const a = t * 0.05;
          const e = THREE.MathUtils.degToRad(elev + Math.sin(t * 0.13) * 3);
          const R = 26;
          cam.position.set(Math.sin(a) * Math.cos(e) * R, Math.sin(e) * R, Math.cos(a) * Math.cos(e) * R);
          cam.lookAt(0, 0, 0);
          ball.position.copy(cam.position);
        },
        controls: [
          range('중력 렌즈 (빛 휘기)', 0, 1, 0.01, 1, (v) => (U.uLens.value = v)),
          range('도플러 · 중력 적색편이', 0, 1, 0.01, 1, (v) => (U.uDopp.value = v)),
          range('원반 밝기', 0, 2.5, 0.05, 1, (v) => (U.uDisk.value = v)),
          range('보는 높이 (°)', -30, 60, 1, 9, (v) => (elev = v)),
        ],
        dispose: () => bin.dispose(),
      };
    },
  }
```

## 관련 기술
- 같은 방법으로 만든 효과 예시: [블랙홀 (강착 원반 · 중력 렌즈)](https://ai-techstudio.web.app/ai/t/i517.md) `i517`
- 먼저 알면 좋은 기술: [나선 은하 (입자 20만 · 밀도파 팔)](https://ai-techstudio.web.app/ai/t/i525.md) `i525` · [부피 성운 (레이마칭 · 별빛 산란)](https://ai-techstudio.web.app/ai/t/i524.md) `i524`
- 다음에 해 볼 기술: [워프 · 초공간 이동](https://ai-techstudio.web.app/ai/t/i530.md) `i530`
- 참고 문서: [Wikipedia — Gravitational lens](https://en.wikipedia.org/wiki/Gravitational_lens) · [Wikipedia — Schwarzschild metric](https://en.wikipedia.org/wiki/Schwarzschild_metric)
