Onde a Bloom está: escolhas de design, progresso e o que vem a seguir
A Bloom completou cinco semanas esta semana. Trezentos e onze commits depois, queríamos registrar o que temos, por que construímos do jeito que construímos e quais peças ainda estão ásperas.
O pitch, em uma frase
Escreva seu jogo em TypeScript, compile-o ahead-of-time e publique um binário nativo de verdade no macOS, Windows, Linux, iOS e tvOS — ou um bundle WASM para a web. Sem Electron, sem WebView, sem runtime de JavaScript embutido no jogo que você publica.
Essa frase fez muito trabalho. Ela também é a razão pela qual a maioria das nossas decisões iniciais de design tem a cara que tem.
Por que TypeScript
Não escolhemos TypeScript porque amamos JavaScript. Escolhemos porque é a linguagem estaticamente tipada mais usada, com sistema de tipos estrutural, uma enorme história de ferramentas e uma sintaxe que não assusta ninguém. A maioria das pessoas com quem queremos fazer jogos já publicou TypeScript antes. Pouquíssimas já publicaram C++.
Também queríamos uma linguagem que pudesse ser compilada ahead-of-time de forma limpa, sem arrastar um garbage collector e um interpretador de bytecode para dentro de cada binário publicado. Isso descartou qualquer coisa que exigisse um runtime pesado (Python, C#, JS-a-linguagem). TypeScript — menos as partes dinâmicas por padrão — acabou sendo um ponto ideal.
Por que Perry
Perry é o compilador ahead-of-time que transforma seu TypeScript em código nativo. O caminho TS-para-binário é a parte que nos permite prometer “zero overhead de runtime.” Seu jogo se torna um único binário que faz chamadas para um core em Rust através de uma ABI C estável. Não há V8, não há Bun, não há JIT.
Escolher Perry nos permitiu ser implacáveis quanto à superfície da linguagem que expomos. A API da Bloom é composta de funções e interfaces simples — sem classes, sem decorators, sem proxies, sem eval. Se um recurso de TypeScript não compila de forma limpa para uma chamada nativa, não o usamos na API. O resultado é uma API que cabe em um cheatsheet e um build que cabe na sua cabeça.
Por que wgpu e não quatro renderers sob medida
Nosso primeiro instinto foi escrever um renderer Metal, depois um renderer DirectX 12, depois um renderer Vulkan. Conversamos para sair dessa em um fim de semana. Quatro backends significam quatro linguagens de shader, quatro modelos de recursos, quatro superfícies de bug e quatro lugares para esquecer do HiDPI.
Em vez disso, o renderer inteiro é escrito uma única vez sobre o wgpu. Os shaders são WGSL. Temos Metal nas plataformas Apple, DirectX 12 no Windows, Vulkan no Linux e Android, e WebGPU (com fallback WebGL) no navegador — a partir de uma única base de código. O custo é estarmos atrelados ao conjunto de recursos do wgpu, e algumas coisas exóticas (mesh shaders, ray tracing) estão fora de cogitação por ora. Achamos que é uma troca justa para um time pequeno.
O que de fato está incluso hoje
Tentamos não colocar nada no site de marketing que não funcione. Eis o que é real, hoje:
- Nove módulos importáveis —
bloom/core,bloom/shapes,bloom/textures,bloom/text,bloom/audio,bloom/models,bloom/math,bloom/physicsebloom/scene. - Um renderer PBR de verdade — materiais em camadas estilo substrate, shadow maps em cascata com cache para geometria estática, tone mapping ACES/AgX, exposição automática, bloom, depth of field, motion blur, SSGI, SSAO, TAA, e um passe de CAS sharpen para escalas de renderização fracionárias.
- Animação esqueletal na GPU — importação de glTF 2.0, linear blend skinning de quatro ossos na GPU, até 128 juntas por esqueleto.
- Física com Jolt — corpos rígidos e moles, controladores de personagem, veículos, raycasts, restrições, callbacks de contato. O alvo web usa o fallback JoltPhysics.js para que o mesmo código rode no navegador.
- Seis plataformas-alvo — macOS, Windows, Linux, iOS, tvOS e Web. O Android está parcialmente conectado, mas ainda não é publicável.
- Hot reload — salve um shader WGSL ou um JSON de material e a mudança aparece na tela em menos de um segundo durante o desenvolvimento. O código de file-watching é removido nos builds de release.
- Dezessete projetos de exemplo — desde um Pong de 170 linhas até o carregamento das cenas Intel Sponza e Bistro.
Algumas coisas recentes das quais nos orgulhamos
O renderer teve um bom mês. Alguns destaques:
- Auto-DRS. O renderer ajusta sua escala de renderização sozinho para atingir seu framerate-alvo, e então roda um passe de upscale mais RCAS sharpen para manter a imagem nítida. Você define um FPS-alvo; a engine faz o resto.
- Reflexões planares. Capturas reais de plano espelhado com oblique-clip e fallback IBL quando o orçamento de reflexão se esgota. Útil para água, pisos polidos e vitrines.
- Splat mapping com texture array. Camadas de terreno e detalhe agora podem vincular um texture array com mips adequados, então você pode pintar vários materiais por tile sem pagar uma draw call por camada.
- Imposter baker. Uma pequena CLI que faz bake de atlas de impostores octaédricos para LODs distantes. Precisamos disso assim que começamos a soltar florestas nas cenas.
- HiDPI multiplataforma. Windows, Linux e Web finalmente compartilham o mesmo tratamento de HiDPI que macOS e iOS já tinham. A UI permanece nítida em qualquer display.
Coisas que não estamos fingindo estar prontas
Devemos a você a lista pouco lisonjeira também:
- Android. O crate da plataforma existe e a maior parte da superfície FFI está conectada, mas a cola de native-activity não está finalizada. Ainda não estamos dizendo a ninguém para publicar Android com a Bloom.
- watchOS. Os shaders compilam, o stub da plataforma está lá, mas estamos bloqueados pelo suporte a watchOS no Perry antes que isso se torne real.
- Geometria virtualizada. Nada equivalente ao Nanite, sem mesh shaders, sem ray tracing por hardware. Isso está no roadmap, não no binário.
- Limite da animação esqueletal. 128 juntas por esqueleto, limite duro, por causa do tamanho do UBO. Rigs grandes precisam de gambiarras hoje.
- O scene graph é jovem. Transformações, visibilidade, sombras e vinculação de materiais funcionam. Sistemas de query e passes de otimização mais amplos ainda não existem.
- Shaders de usuário. Você pode escrever WGSL à mão e carregá-lo pelo pipeline de materiais, mas não há um shader graph na engine nem compilação de shaders em runtime a partir de TypeScript.
O formato da API
Tomamos bastante emprestado do Raylib para as partes de modo imediato da API. Se você já escreveu um jogo em Raylib, a memória muscular se transfere:
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();
} Para trabalho 3D e de física, a mesma filosofia se aplica: interfaces simples, funções puras, sem máquinas de estado escondidas. Uma câmera é um literal de objeto. Um corpo rígido é um handle. O pipeline de renderização é configurado chamando funções em uma ordem específica, não registrando listeners em um orquestrador invisível.
O que vem a seguir
Nossa lista curta para as próximas semanas:
- Finalizar a cola de native-activity do Android e considerar o Android publicável.
- Empurrar o trabalho do scene-graph para além do “básico” até algo realmente útil: queries, refinamento de frustum culling, helpers de instancing.
- Lançar uma primeira versão da integração com o editor para que você possa iterar em uma cena sem reiniciar seu jogo.
- Abrir o código dos jogos de exemplo, incluindo um demo um pouco maior que o Pong.
- Documentar o pipeline de compilação do Perry em detalhes. Várias pessoas pediram.
Experimente
A Bloom é código aberto sob a licença MIT. O repositório inteiro está no GitHub. Se você quer acompanhar sem fazer fork, a documentação tem um quick start de 12 linhas, e a página de vitrine lista o que estamos construindo com ela nós mesmos.
De qualquer forma: obrigado por dar uma olhada. Continuaremos postando aqui à medida que peças maiores forem chegando.