← 블로그로 돌아가기

Bloom의 현재 위치: 디자인 선택, 진행 상황, 그리고 다음 단계

Bloom이 이번 주로 5주가 되었습니다. 311개의 커밋을 쌓은 시점에서, 우리가 가진 것, 왜 이렇게 만들었는지, 그리고 어떤 부분이 아직 거친지 정리해두고 싶었습니다.

한 문장으로 요약하는 핵심

TypeScript로 게임을 작성하고, 사전 컴파일하여 macOS, Windows, Linux, iOS, tvOS에서 진짜 네이티브 바이너리로 출시하거나 — 웹용 WASM 번들로 빌드합니다. 출시되는 게임에 Electron도, WebView도, 임베디드 자바스크립트 런타임도 들어가지 않습니다.

이 한 문장이 많은 일을 해냈습니다. 그리고 초기 디자인 결정 대부분이 지금과 같은 모습인 이유이기도 합니다.

왜 TypeScript인가

JavaScript를 좋아해서 TypeScript를 고른 것은 아닙니다. 구조적 타입 시스템을 가진 정적 타입 언어 중 가장 널리 쓰이고, 방대한 도구 생태계를 가지며, 사람들이 거부감 없이 받아들일 수 있는 문법을 가졌기 때문에 골랐습니다. 우리가 함께 게임을 만들고 싶은 사람들 대부분은 이미 TypeScript로 무언가를 출시해본 경험이 있습니다. C++를 출시해본 사람은 거의 없습니다.

또한 가비지 컬렉터와 바이트코드 인터프리터를 모든 출시 바이너리에 끌고 다니지 않으면서도 깔끔하게 사전 컴파일이 가능한 언어를 원했습니다. 그래서 무거운 런타임이 필요한 것들(Python, C#, JS-그-언어 자체)은 제외되었습니다. TypeScript는 — 기본적으로 동적인 부분들을 빼고 나면 — 딱 알맞은 균형점이었습니다.

왜 Perry인가

Perry는 TypeScript를 네이티브 코드로 변환하는 사전 컴파일러입니다. TS-바이너리 경로가 바로 우리가 “런타임 오버헤드 없음”을 약속할 수 있는 이유입니다. 게임은 안정적인 C ABI를 통해 Rust 코어를 호출하는 단일 바이너리가 됩니다. V8도, Bun도, JIT도 없습니다.

Perry를 선택한 덕분에 노출하는 언어 표면을 가차없이 줄일 수 있었습니다. Bloom API는 함수와 평범한 인터페이스로 이루어져 있습니다 — 클래스도, 데코레이터도, 프록시도, eval도 없습니다. 어떤 TypeScript 기능이 네이티브 호출로 깔끔하게 컴파일되지 않는다면, API에서 사용하지 않습니다. 그 결과로 치트시트 한 장에 들어가는 API와 머릿속에 그려지는 빌드를 얻었습니다.

왜 wgpu이고, 왜 네 개의 전용 렌더러가 아닌가

처음에는 Metal 렌더러를 만들고, 그다음 DirectX 12 렌더러, 그다음 Vulkan 렌더러를 따로 만들 생각이었습니다. 한 주말 만에 그 생각을 접었습니다. 백엔드가 네 개라는 건 셰이더 언어가 네 개, 리소스 모델이 네 개, 버그가 발생할 표면이 네 개, HiDPI를 잊어버릴 곳이 네 개라는 뜻입니다.

대신 전체 렌더러를 wgpu 위에서 한 번만 작성했습니다. 셰이더는 WGSL입니다. 그 결과 Apple 플랫폼에서는 Metal, Windows에서는 DirectX 12, Linux와 Android에서는 Vulkan, 브라우저에서는 WebGPU(WebGL 폴백 포함)를 — 하나의 코드베이스로 얻습니다. 비용은 wgpu의 기능 집합에 묶인다는 점이고, 일부 특수한 기능들(메시 셰이더, 레이 트레이싱)은 당분간 사용할 수 없습니다. 작은 팀에게는 충분히 합리적인 거래라고 생각합니다.

오늘 실제로 들어 있는 것

마케팅 사이트에 동작하지 않는 것은 올리지 않으려고 합니다. 오늘 실제로 동작하는 것은 다음과 같습니다:

  • 임포트 가능한 9개의 모듈bloom/core, bloom/shapes, bloom/textures, bloom/text, bloom/audio, bloom/models, bloom/math, bloom/physics, 그리고 bloom/scene.
  • 진짜 PBR 렌더러 — substrate 스타일의 레이어드 머티리얼, 정적 지오메트리에 대한 캐싱이 적용된 캐스케이드 섀도 맵, ACES/AgX 톤 매핑, 자동 노출, 블룸, 피사계 심도, 모션 블러, SSGI, SSAO, TAA, 그리고 분수 렌더 스케일을 위한 CAS 샤픈 패스.
  • GPU 스켈레탈 애니메이션 — glTF 2.0 임포트, GPU 위에서의 4본 선형 블렌드 스키닝, 스켈레톤당 최대 128개의 조인트.
  • Jolt 물리 — 강체 및 연체, 캐릭터 컨트롤러, 차량, 레이캐스트, 컨스트레인트, 컨택트 콜백. 웹 타깃은 JoltPhysics.js 폴백을 사용하여 동일한 코드가 브라우저에서도 실행됩니다.
  • 6개의 타깃 플랫폼 — macOS, Windows, Linux, iOS, tvOS, 웹. Android는 일부 연결되어 있지만 아직 출시 가능한 상태는 아닙니다.
  • 핫 리로드 — WGSL 셰이더나 머티리얼 JSON을 저장하면 개발 중 1초 안에 화면에 반영됩니다. 파일 감시 코드는 릴리스 빌드에서 제거됩니다.
  • 17개의 예제 프로젝트 — 170줄짜리 Pong부터 Intel Sponza와 Bistro 씬을 로드하는 것까지.

최근 우리가 자랑하고 싶은 것들

이번 달 렌더러는 좋은 한 달을 보냈습니다. 몇 가지 하이라이트:

  • Auto-DRS. 렌더러가 타깃 프레임레이트에 맞춰 렌더 스케일을 스스로 조정하고, 업스케일에 RCAS 샤픈 패스를 더해 이미지가 선명하게 유지되도록 합니다. 타깃 FPS만 설정하면 나머지는 엔진이 알아서 합니다.
  • 평면 반사. 오블리크 클립과 함께 진짜 거울면 캡처를 수행하며, 반사 예산이 소진되면 IBL로 폴백합니다. 물, 광택 바닥, 상점 진열창 등에 유용합니다.
  • 텍스처 어레이 스플랫 매핑. 지형과 디테일 레이어를 이제 적절한 밉을 가진 텍스처 어레이로 바인딩할 수 있어, 레이어마다 드로우콜을 지불하지 않고도 타일당 여러 머티리얼을 칠할 수 있습니다.
  • 임포스터 베이커. 원거리 LOD를 위한 옥타헤드럴 임포스터 아틀라스를 굽는 작은 CLI입니다. 씬에 숲을 떨구기 시작한 순간부터 필요했던 도구입니다.
  • 크로스 플랫폼 HiDPI. Windows, Linux, 웹이 마침내 macOS와 iOS가 이미 가지고 있던 동일한 HiDPI 처리를 공유합니다. 모든 디스플레이에서 UI가 선명하게 유지됩니다.

아직 끝났다고 말할 수 없는 것들

마음에 들지 않는 목록도 솔직히 공유해야겠지요:

  • Android. 플랫폼 크레이트는 존재하고 FFI 표면 대부분이 연결되어 있지만, 네이티브 액티비티 글루가 아직 끝나지 않았습니다. 아직 누구에게도 Bloom으로 Android에 출시하라고 권하지는 않습니다.
  • watchOS. 셰이더는 컴파일되고 플랫폼 스텁도 있지만, Perry의 watchOS 지원이 갖춰지기 전까지는 막혀 있습니다.
  • 버추얼라이즈드 지오메트리. Nanite에 해당하는 것도 없고, 메시 셰이더도, 하드웨어 레이 트레이싱도 없습니다. 이는 로드맵에 있을 뿐, 바이너리에 있는 것은 아닙니다.
  • 스켈레탈 애니메이션 한계. UBO 크기 때문에 스켈레톤당 128 조인트가 하드 리밋입니다. 큰 리그는 오늘 우회 방법이 필요합니다.
  • 씬 그래프는 아직 어립니다. 트랜스폼, 가시성, 그림자, 머티리얼 바인딩은 모두 동작합니다. 쿼리 시스템과 더 폭넓은 최적화 패스는 아직 존재하지 않습니다.
  • 사용자 셰이더. WGSL을 직접 작성해 머티리얼 파이프라인을 통해 로드할 수 있지만, 엔진 내장 셰이더 그래프나 TypeScript에서의 런타임 셰이더 컴파일은 없습니다.

API의 모양

이미디어트 모드 부분의 API는 Raylib에서 많이 차용했습니다. Raylib으로 게임을 만들어본 적이 있다면 손에 익은 감각이 그대로 옮겨집니다:

import { initWindow, windowShouldClose, beginDrawing,
         endDrawing, clearBackground, drawText, Colors } from "bloom";

initWindow(800, 450, "Hello Bloom");

while (!windowShouldClose()) {
  beginDrawing();
  clearBackground(Colors.RAYWHITE);
  drawText("Hello, Bloom!", 190, 200, 20, Colors.DARKGRAY);
  endDrawing();
}

3D와 물리 작업도 같은 철학을 따릅니다: 평범한 인터페이스, 순수 함수, 숨겨진 상태 머신 없음. 카메라는 객체 리터럴입니다. 강체는 핸들입니다. 렌더링 파이프라인은 보이지 않는 오케스트레이터에 리스너를 등록하는 것이 아니라, 함수를 특정 순서로 호출하여 구성합니다.

다음 단계

앞으로 몇 주 동안의 짧은 우선순위 목록:

  • Android 네이티브 액티비티 글루를 마무리하고 Android를 출시 가능 상태로 만들기.
  • 씬 그래프 작업을 “기본” 단계 너머로 끌어올려 실제로 유용하게 만들기: 쿼리, 절두체 컬링 정교화, 인스턴싱 헬퍼.
  • 에디터 통합의 첫 번째 버전을 안착시켜, 게임을 재시작하지 않고도 씬을 반복 작업할 수 있도록 하기.
  • 예제 게임들을 오픈소스화하기. Pong보다 약간 더 큰 데모도 포함합니다.
  • Perry 컴파일 파이프라인을 자세히 정리하기. 여러 분이 물어봤습니다.

사용해보기

Bloom은 MIT 라이선스 기반 오픈 소스입니다. 전체 저장소는 GitHub에 있습니다. 포크 없이 따라가고 싶다면 문서에 12줄짜리 빠른 시작 가이드가 있고, 쇼케이스 페이지에는 우리가 직접 Bloom으로 만들고 있는 것들이 정리되어 있습니다.

어느 쪽이든: 살펴봐 주셔서 감사합니다. 더 큰 조각들이 자리잡으면 이곳에 계속 글을 올리겠습니다.