← DEVLOG
우주 과학2025.11.297 min read

별의 일생 — HR 다이어그램을 인터랙티브하게

원시별에서 백색왜성까지 — 별의 일생 각 단계를 직접 전환하며 관찰하는 인터랙티브 시뮬레이션 제작기

canvasastronomystellar-evolutioninteractive

시작 — 수십억 년을 몇 초로

밤하늘의 별들은 모두 진화하고 있습니다. 단지 너무 느려서 우리가 못 볼 뿐입니다. 태양도 지금으로부터 약 50억 년 후에는 적색거성으로 부풀어 올라 지구 궤도까지 집어삼킬 것입니다.

이 시뮬레이션은 그 수십억 년의 여정을 브라우저에서 직접 조작할 수 있게 만든 결과물입니다.


별 진화 단계 설계

각 단계의 색상과 물리적 특성은 실제 천문학 데이터를 기반으로 정의했습니다.

const STAR_STAGES = {
  protostar: { color: '#aa66ff', tempK: 3000, lumRatio: 0.01 },
  mainSequence: { color: '#ffdd44', tempK: 5778, lumRatio: 1.0 },
  redGiant: { color: '#ff7733', tempK: 3500, lumRatio: 200 },
  supernova: { color: '#ff4422', tempK: 30000, lumRatio: 1e9 },
  neutronStar: { color: '#88aaff', tempK: 600000, lumRatio: 0.001 },
};

원시별은 보라빛 — 아직 핵융합이 시작되지 않은 먼지 구름이 중력으로 수축하는 단계입니다. 주계열성은 태양과 같이 수소 핵융합이 안정적으로 유지되는 상태이며, 적색거성은 수소가 고갈되어 외곽이 팽창한 형태입니다. 초신성 폭발은 가장 짧고 가장 밝은 순간입니다.


H-R 다이어그램 Canvas 렌더링

헤르츠스프룽-러셀(H-R) 다이어그램은 별의 온도(x축)와 광도(y축) 관계를 나타냅니다. 현재 단계의 별이 다이어그램 어디에 위치하는지 실시간으로 표시합니다.

// 온도 → x좌표 변환 (고온이 왼쪽)
const tempToX = (tempK: number) => {
  const logT = Math.log10(tempK);
  return W - ((logT - LOG_T_MIN) / (LOG_T_MAX - LOG_T_MIN)) * W;
};

// 광도 → y좌표 변환 (고광도가 위)
const lumToY = (lumRatio: number) => {
  const logL = Math.log10(lumRatio);
  return H - ((logL - LOG_L_MIN) / (LOG_L_MAX - LOG_L_MIN)) * H;
};

주계열성 밴드는 반투명 흰색 사선으로, 거성 영역은 옅은 주황 영역으로 배경에 깔아두었습니다. 현재 단계의 별은 글로우 효과와 함께 해당 위치로 부드럽게 이동합니다.


Canvas 별 렌더링 — 코로나와 흑점

별 자체의 시각 표현은 세 레이어로 구성됩니다.

레이어 1 — 코로나 글로우: radialGradient로 바깥에서 안쪽으로 투명하게 페이드인.

레이어 2 — 별 표면: 구체감을 위해 하이라이트 포인트를 오프셋 설정.

const grad = ctx.createRadialGradient(cx - r * 0.25, cy - r * 0.25, 0, cx, cy, r);
grad.addColorStop(0, lightenColor(stage.color, 0.6));
grad.addColorStop(0.5, stage.color);
grad.addColorStop(1, darkenColor(stage.color, 0.4));

레이어 3 — 흑점 효과: 주계열성과 적색거성 단계에서만 표면에 작은 어두운 원들을 랜덤 배치합니다. seededRandom으로 매 프레임 위치가 흔들리지 않도록 고정했습니다.


오디오 — 별마다 다른 목소리

각 단계는 서로 다른 주파수와 음색을 가집니다. Web Audio API로 순수 합성합니다.

  • 원시별: 40Hz 저음 드론 — 중력 수축의 무게감
  • 주계열성: 220Hz 안정적인 정현파 + 고조파 — 항성의 고요한 연소
  • 적색거성: 80Hz + 130Hz 베이스 — 팽창의 낮고 울리는 진동
  • 초신성: 화이트 노이즈 버스트 + 고주파 스파이크 — 폭발의 혼돈
  • 중성자별: 1200Hz 펄스 — 밀리초 단위 회전하는 펄사 리듬
const startStageAudio = (stage: StarStage, ctx: AudioContext) => {
  const osc = ctx.createOscillator();
  const gain = ctx.createGain();
  osc.frequency.value = STAGE_AUDIO[stage].freq;
  osc.type = STAGE_AUDIO[stage].type;
  gain.gain.setValueAtTime(0, ctx.currentTime);
  gain.gain.linearRampToValueAtTime(0.3, ctx.currentTime + 0.5);
  osc.connect(gain).connect(ctx.destination);
  osc.start();
};

타이머 자동 진행 + 클릭 수동 전환

자동 모드에서는 각 단계가 일정 시간 후 다음 단계로 전환됩니다. 원시별과 주계열성은 길게, 초신성은 짧게 머뭅니다.

const STAGE_DURATION_MS = {
  protostar: 4000,
  mainSequence: 6000,
  redGiant: 5000,
  supernova: 1500, // 찰나의 순간
  neutronStar: 4000,
};

클릭하면 현재 단계를 건너뛰고 즉시 다음 단계로 이동합니다. 진화 전환 시 별의 크기와 색상은 lerp로 보간하여 부드럽게 변합니다.


성능 최적화 — OffscreenCanvas blur 배칭

별의 코로나 글로우와 플레어 효과에는 ctx.filter = 'blur(...)'를 사용합니다. 문제는 blur 연산의 비용이 픽셀 수 × blur 반경²에 비례한다는 점입니다. 초신성 폭발 장면에서는 프레임당 438회의 blur draw call이 발생해 프레임 드롭이 심각했습니다.

해결 방법은 OffscreenCanvas 배칭 패턴이었습니다. 다수의 blur가 필요한 요소들을 먼저 OffscreenCanvas에 blur 없이 모두 그린 뒤, 마지막에 ctx.filter = 'blur(N)'; ctx.drawImage(offscreen, 0, 0) 한 번으로 일괄 처리합니다.

// Before: 438 blur draw calls per frame
elements.forEach(el => {
  ctx.filter = `blur(${el.blur}px)`;
  drawElement(ctx, el);
});

// After: 7 blur draw calls per frame
drawAllElements(offscreenCtx, elements); // blur 없이 일괄 렌더
ctx.filter = `blur(${radius}px)`;
ctx.drawImage(offscreenCanvas, 0, 0); // 1회 blur 적용

438회의 blur 연산이 7회로 줄었습니다. 초신성 폭발 같은 시각적으로 화려한 장면에서 4K 디스플레이에서도 안정적인 프레임을 유지합니다.


마치며

태양의 수명은 약 100억 년입니다. 지금 태양은 그 절반쯤 왔습니다. 그 숫자를 처음 접했을 때는 피부에 와닿지 않았는데, 이 시뮬레이션을 만들고 나서 버튼 하나로 단계를 건너뛸 때마다 왠지 모를 무게감이 생겼습니다.

수십억 년을 1초로 압축하면 — 별의 일생은 놀랍도록 짧습니다.

이 포스트와 연결된 콘텐츠

직접 체험하기