# AI 꾸러미 — 초신성 · 충격파 고리 — Supernova explosion sequence
> 부풀던 별이 번쩍 → 테두리가 밝은 충격파 껍질과 3D 잡음 필라멘트가 감속하며 퍼지고, 충격파가 적도 고리에 닿으면 구슬 빛이 켜지며 남은 펄서가 빛줄을 돌린다.  
> 견본: https://ai-techstudio.web.app/#t/i534

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

## 주문서

### 만들어 줘: 초신성 · 충격파 고리 — Supernova explosion sequence

#### 1. 목표
큰 폭발 · 보스 처치 연출에 초신성 폭발 연출을 넣어 줘 — 번쩍 · 충격파 껍질 · 필라멘트 · 고리 구슬 · 펄서 빛줄까지, 느낌은 장엄한 우주 사진.

#### 2. 핵심 기술 용어
- **Supernova explosion sequence** — 초신성 폭발 연출 (부풂 → 번쩍 → 껍질 → 잔해)
- **Fresnel rim shell (shock front)** — 가장자리가 밝은 구 껍질 — 충격파 면
- **Ease-out expansion R = 1 - exp(-k t)** — 감속하며 퍼지는 반지름
- **Light echo ring** — 뒤 먼지에 비친 빛 메아리 고리

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

#### 4. 조건
- 시간표는 한 변수(tau)로 — 부풂(0 ~ 2.5초) · 폭발 뒤(age = tau - 2.5) 로 나눠 모든 값이 age 의 함수
- 반지름은 1 - exp(-k·age) 꼴로 감속 (곧은 선형으로 퍼지지 않게)
- 껍질은 AdditiveBlending · DoubleSide · depthWrite false, 밝기는 0.3 + 1.1·exp(-0.5 age) 로 서서히 어둡게
- 고리 구슬 · 펄서는 미리 만들어 두고 visible · opacity 로만
- 다시 폭발 단추로 tau = 0 · 파편 방향 다시 뽑기

#### 5. 완성 기준 (이게 보이면 성공)
- 푸른 별이 부풀며 떨다가 번쩍! 하고 빛이 화면을 덮었다 사라진다
- 테두리가 밝은 껍질이 퍼지며 느려지고, 붉은 · 청록 필라멘트 결이 보인다
- 껍질이 고리에 닿을 때 구슬 빛이 차례로 켜지고, 가운데 펄서 빛줄이 돈다
- 뒤 먼지에 빛 메아리 고리가 퍼지고, 「빠르기」 · 「필라멘트」 · 「펄서」 조절이 바로 바뀐다

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

## 원리
- 한 바퀴 13초 시간표: 처음 2.5초는 별이 부풀며 떨리고 밝아진다. 그 순간 별을 숨기고 큰 빛 스프라이트가 번쩍(exp(-2.2 t)) 퍼진다.
- 충격파 껍질 반지름은 R = 0.25 + 3.3·(1 - exp(-0.55 t)) — 처음엔 빠르고 점점 느려진다. 안쪽 껍질은 0.62배.
- 껍질 셰이더: 테두리일수록 밝은 파란 충격파(pow(1 - μ, 2.2)³) + 3D 잡음 필라멘트(붉은 수소 · 청록 산소). 정점도 잡음으로 울퉁불퉁하게.
- 적도 고리 구슬 28개는 충격파 반지름이 고리(2.6)에 닿을 때 smoothstep 으로 하나씩 켜진다 (구슬마다 조금씩 늦게).
- 1.2초 뒤 펄서 빛줄(원뿔 둘)이 빠르게 돌고, 뒤 먼지 판에 빛 메아리 고리가 2.6 × t 로 퍼진다. 파편 입자 2,600개가 바깥으로.

## 핵심 코드 — 폭발 시간표 — 부풂 · 번쩍 · 감속 껍질 · 고리 구슬 · 펄서
(발췌: demos/demosSpace.ts i534 make() update 를 정리)
```ts
const smooth = (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); };
let tau = 0;
const T0 = 2.5; // 부푸는 시간

function update(t: number, dt: number, speed = 1): void {
  tau += dt * speed;
  if (tau > 13) tau = 0;                                // 한 바퀴 13초
  const age = tau - T0;
  if (age < 0) {                                        // 부풂 — 떨며 밝아진다
    const k = tau / T0;
    star.visible = true;
    star.scale.setScalar(1.0 + k * 0.8 + Math.sin(tau * 9) * 0.06 * k);
    star.material.color.setRGB(0.68, 0.8, 1).multiplyScalar(2.4 + k * 2);
    flash.visible = outer.visible = inner.visible = pulsar.visible = false;
    return;
  }
  star.visible = false;
  const f = Math.exp(-age * 2.2);                       // 번쩍
  flash.visible = f > 0.01;
  flash.scale.setScalar(4 + 26 * (1 - Math.exp(-age * 6)));
  flash.material.opacity = f;
  const R = 0.25 + 3.3 * (1 - Math.exp(-age * 0.55));   // 감속하며 퍼지는 충격파
  outer.visible = inner.visible = true;
  outer.scale.setScalar(R);
  inner.scale.setScalar(R * 0.62);
  const bright = 0.3 + 1.1 * Math.exp(-age * 0.5);
  outerU.uI.value = bright * Math.min(1, age * 3);
  innerU.uI.value = bright * 0.8 * Math.min(1, age * 2);
  echoU.uEchoR.value = age * 2.6;                        // 빛 메아리 고리
  echoU.uEchoI.value = Math.exp(-age * 0.25) * 1.2;
  beads.forEach((b) => {                                // 충격파가 고리(2.6)에 닿으면 켜진다
    const on = smooth(2.2, 2.7, R + b.userData.k * 0.25);
    b.visible = on > 0.01;
    b.material.opacity = on * (0.8 + 0.2 * Math.sin(t * 3 + b.userData.k * 20));
  });
  pulsar.visible = age > 1.2;
  spinner.rotation.y = t * 9;                           // 펄서 빛줄 회전
}
```

## 흔한 실수 · 확인 목록
- [ ] **반지름을 선형으로 키우면 폭발이 아니라 풍선처럼 보인다** — 1 - exp(-k t) 로 처음 빠르고 점점 느리게.
- [ ] **연출 물체를 그때그때 만들면 첫 폭발에서 멈칫한다** — 껍질 · 구슬 · 펄서를 처음부터 만들어 숨겨 두고 visible 로만. 첫 셰이더 컴파일은 로딩 중에 미리.
- [ ] **번쩍이 너무 세고 길면 눈이 아프다** — 번쩍은 exp(-2.2 t) 로 1초 안에 빠르게 꺼지게.

## 완성 기준 체크리스트
- [ ] 푸른 별이 부풀며 떨다가 번쩍! 하고 빛이 화면을 덮었다 사라진다
- [ ] 테두리가 밝은 껍질이 퍼지며 느려지고, 붉은 · 청록 필라멘트 결이 보인다
- [ ] 껍질이 고리에 닿을 때 구슬 빛이 차례로 켜지고, 가운데 펄서 빛줄이 돈다
- [ ] 뒤 먼지에 빛 메아리 고리가 퍼지고, 「빠르기」 · 「필라멘트」 · 「펄서」 조절이 바로 바뀐다

## 이 기술 정보
- id: `i534` · 분류: 그래픽 · 셰이더 › 우주 표현 · 3D · 난이도 어려움 · 폰 부담 보통 (폰 주의) — 구 껍질 2장(96 × 64, 3D 텍스처 3번) + 파편 2,600개(CPU 위치, 버퍼 올리기) + 스프라이트 30여 개. 껍질이 화면을 덮을 때 픽셀 부담이 크다.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i534
- 쓰면 좋을 때: 큰 사건을 알리는 폭발 연출 / 별의 일생 · 우주 학습 / 보스를 쓰러뜨린 마무리
- 쓰지 말 때: 자주 터지는 작은 폭발 — 대신 입자 방출기(i170) / 번쩍이 아이에게 부담될 수 있는 화면 — 번쩍 세기를 낮춘다

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### i534 견본 항목 — `src/demos/demosSpace.ts:2729`
```ts
  i534: {
    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(45, 1.6, 0.1, 1000);
      const S = makeStars(12000, 300, 141, { magMax: 7, tw: 0.15, gain: 0.8 });
      scene.add(S.pts);
      const N3 = noise3D();
      const UE = { uN2: { value: noise2D() }, uEchoR: { value: 0 }, uEchoI: { value: 0 } };
      const echo = new THREE.Mesh(new THREE.PlaneGeometry(40, 40), new THREE.ShaderMaterial({ uniforms: UE, vertexShader: UV_VS, fragmentShader: ECHO_FS, transparent: true, depthWrite: false, blending: THREE.AdditiveBlending }));
      echo.position.z = -14;
      scene.add(echo);
      const mkShell = (inner: number): { m: THREE.Mesh; U: { uN3: { value: THREE.Data3DTexture }; uAge: { value: number }; uFil: { value: number }; uI: { value: number }; uInner: { value: number } } } => {
        const U = { uN3: { value: N3 }, uAge: { value: 0 }, uFil: { value: 1 }, uI: { value: 1 }, uInner: { value: inner } };
        const m = new THREE.Mesh(new THREE.SphereGeometry(1, 96, 64), new THREE.ShaderMaterial({ uniforms: U, vertexShader: SHELL_VS, fragmentShader: SHELL_FS, side: THREE.DoubleSide, transparent: true, depthWrite: false, blending: THREE.AdditiveBlending }));
        m.renderOrder = 2;
        scene.add(m);
        return { m, U };
      };
      const outer = mkShell(0);
      const inner = mkShell(1);
      const gt = bin.add(texOf(glowCanvas()));
      const st = bin.add(texOf(spikeCanvas()));
      const star = glowSprite(st, 0xaecbff, 2.4, 1.2);
      star.renderOrder = 3;
      scene.add(star);
      const flash = glowSprite(gt, 0xeef4ff, 6, 4);
      flash.renderOrder = 4;
      scene.add(flash);
      // 적도 고리 (구슬 빛)
      const ringG = new THREE.Group();
      ringG.rotation.set(1.15, 0.3, 0);
      scene.add(ringG);
      const beads: THREE.Sprite[] = [];
      const rr = rng(9);
      for (let i = 0; i < 28; i++) {
        const a = (i / 28) * TAU + rr() * 0.12;
        const s = glowSprite(gt, 0xffd6b0, 1.6, 0.25);
        s.position.set(Math.cos(a) * 2.6, 0, Math.sin(a) * 2.6);
        s.userData.k = rr();
        ringG.add(s);
        beads.push(s);
      }
      // 펄서 빛줄
      const pulsar = new THREE.Group();
      pulsar.rotation.z = 0.5;
      scene.add(pulsar);
      const UB = { uI: { value: 0 } };
      const cone = new THREE.ConeGeometry(0.07, 3.0, 24, 1, true);
      cone.translate(0, -1.6, 0);
      const beamMat = new THREE.ShaderMaterial({ uniforms: UB, vertexShader: UV_VS, fragmentShader: BEAM_FS, side: THREE.DoubleSide, transparent: true, depthWrite: false, blending: THREE.AdditiveBlending });
      const spinner = new THREE.Group();
      pulsar.add(spinner);
      for (const sgn of [1, -1]) {
        const b = new THREE.Mesh(cone, beamMat);
        b.rotation.z = sgn > 0 ? Math.PI : 0;
        b.rotation.x = 0.5;
        spinner.add(b);
      }
      const ps = glowSprite(st, 0xbfe0ff, 2, 0.4);
      pulsar.add(ps);
      // 날아가는 조각
      const NP = 2600;
      const P = new Parts(NP);
      scene.add(P.pts);
      bin.add(P);
      const pv = new Float32Array(NP * 4);
      const seedParts = (): void => {
        const r = rng(Math.floor(Math.random() * 1e6));
        for (let i = 0; i < NP; i++) {
          const u = r() * 2 - 1;
          const a = r() * TAU;
          const s = Math.sqrt(1 - u * u);
          pv[i * 4] = s * Math.cos(a);
          pv[i * 4 + 1] = u;
          pv[i * 4 + 2] = s * Math.sin(a);
          pv[i * 4 + 3] = 0.55 + r() * 0.5;
        }
      };
      seedParts();
      bin.scene(scene);
      let restart = false;
      let speed = 1;
      let pulsarOn = true;
      let fil = 1;
      let last = 0;
      let tau = 0;
      return {
        scene,
        camera: cam,
        tone: THREE.ACESFilmicToneMapping,
        update(t) {
          const dt = Math.min(0.05, Math.max(0, t - last));
          last = t;
          S.U.uTime.value = t;
          tau += dt * speed;
          if (restart || tau > 13) {
            restart = false;
            tau = 0;
            seedParts();
          }
          const T0 = 2.5;
          const age = tau - T0;
          cam.position.set(Math.sin(t * 0.05) * 1.2, 0.8 + Math.sin(t * 0.04) * 0.4, 10.5);
          cam.lookAt(0, 0, 0);
          if (age < 0) {
            const k = tau / T0;
            star.visible = true;
            star.scale.setScalar(1.0 + k * 0.8 + Math.sin(tau * 9) * 0.06 * k);
            (star.material as THREE.SpriteMaterial).color.setRGB(0.68, 0.8, 1).multiplyScalar(2.4 + k * 2);
            flash.visible = false;
            outer.m.visible = inner.m.visible = false;
            pulsar.visible = false;
            UE.uEchoI.value = 0;
            beads.forEach((b) => (b.visible = false));
            P.upload(0);
            return;
          }
          star.visible = false;
          const f = Math.exp(-age * 2.2);
          flash.visible = f > 0.01;
          flash.scale.setScalar(4 + 26 * (1 - Math.exp(-age * 6)));
          (flash.material as THREE.SpriteMaterial).opacity = f;
          const R = 0.25 + 3.3 * (1 - Math.exp(-age * 0.55));
          outer.m.visible = inner.m.visible = true;
          outer.m.scale.setScalar(R);
          inner.m.scale.setScalar(R * 0.62);
          outer.m.rotation.y = age * 0.05;
          inner.m.rotation.y = -age * 0.07;
          const bright = 0.3 + 1.1 * Math.exp(-age * 0.5);
          outer.U.uI.value = bright * Math.min(1, age * 3);
          inner.U.uI.value = bright * 0.8 * Math.min(1, age * 2);
          outer.U.uAge.value = inner.U.uAge.value = age;
          outer.U.uFil.value = inner.U.uFil.value = fil;
          UE.uEchoR.value = age * 2.6;
          UE.uEchoI.value = Math.exp(-age * 0.25) * 1.2;
          // 충격파가 고리(2.6)에 닿으면 구슬이 하나씩 켜진다
          beads.forEach((b) => {
            const on = smooth(2.2, 2.7, R + (b.userData.k as number) * 0.25);
            b.visible = on > 0.01;
            (b.material as THREE.SpriteMaterial).opacity = on * (0.8 + 0.2 * Math.sin(t * 3 + (b.userData.k as number) * 20));
          });
          pulsar.visible = pulsarOn && age > 1.2;
          spinner.rotation.y = t * 9;
          UB.uI.value = Math.min(1, (age - 1.2) * 0.8) * 0.12;
          let n = 0;
          for (let i = 0; i < NP; i++) {
            const sp = pv[i * 4 + 3]!;
            const d = sp * 3.6 * (1 - Math.exp(-age * 0.6)) + 0.1;
            const k = clamp(age / 8, 0, 1);
            P.put(n++, pv[i * 4]! * d, pv[i * 4 + 1]! * d, pv[i * 4 + 2]! * d, lerp(1, 1, k), lerp(0.85, 0.35, k), lerp(0.6, 0.2, k), 0.6 * Math.exp(-age * 0.35), 0.006 + sp * 0.006);
          }
          P.upload(n);
        },
        resize: (_w, h) => {
          S.U.uScale.value = h / 900;
          P.U.uScale.value = h / 900;
        },
        controls: [
          { type: 'button', label: '다시 폭발', on: () => (restart = true) },
          range('빠르기', 0.2, 3, 0.05, 1, (v) => (speed = v)),
          range('필라멘트', 0, 2.5, 0.05, 1, (v) => (fil = v)),
          tog('펄서', true, (v) => (pulsarOn = v)),
        ],
        dispose: () => {
          bin.dispose();
          S.dispose();
        },
      };
    },
  }
```

## 관련 기술
- 먼저 알면 좋은 기술: [태양 표면 · 코로나 · 홍염](https://ai-techstudio.web.app/ai/t/i528.md) `i528` · [입자 방출기 (수명 곡선)](https://ai-techstudio.web.app/ai/t/i170.md) `i170`
- 다음에 해 볼 기술: [블랙홀 (중력 렌즈 · 강착 원반 도플러)](https://ai-techstudio.web.app/ai/t/i529.md) `i529` · [빛 번짐 (UnrealBloom)](https://ai-techstudio.web.app/ai/t/u20.md) `u20`
- 참고 문서: [Wikipedia — Supernova](https://en.wikipedia.org/wiki/Supernova)
