← 返回博客

Bloom 现状:设计选择、进展与下一步计划

本周,Bloom 满五周了。在三百一十一次提交之后,我们想把现在拥有的一切、当初为何这样构建、以及哪些部分仍然粗糙记录下来。

一句话定位

用 TypeScript 编写你的游戏,提前编译,然后在 macOS、Windows、Linux、iOS 和 tvOS 上发布真正的原生二进制 —— 或为 Web 发布 WASM 包。无需 Electron、无需 WebView,你发布的游戏中也不嵌入 JavaScript 运行时。

这句话承担了很多分量。它也是我们早期大多数设计决策呈现这般面貌的原因。

为什么选择 TypeScript

我们选择 TypeScript 并非因为热爱 JavaScript,而是因为它是使用最广泛、具备结构化类型系统、拥有庞大工具生态、并且语法不会吓跑任何人的静态类型语言。我们想一起做游戏的大多数人都发布过 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 渲染器。我们在一个周末内说服了自己放弃这个想法。四个后端意味着四种着色器语言、四种资源模型、四个 Bug 暴露面,以及四处可能忘记 HiDPI 的地方。

取而代之的是,整个渲染器只在 wgpu 之上写了一遍。着色器使用 WGSL。我们在 Apple 平台获得 Metal,在 Windows 上获得 DirectX 12,在 Linux 与 Android 上获得 Vulkan,在浏览器中获得 WebGPU(并以 WebGL 作为回退) —— 全部来自一套代码库。代价是我们被绑定在 wgpu 的特性集上,一些奇特的功能(网格着色器、光线追踪)目前无法实现。对于一个小团队来说,我们认为这是一笔公平的交易。

今天盒子里实际有什么

我们尽量不在营销网站上放任何不能用的东西。以下是今天真正可用的内容:

  • 九个可导入模块 —— bloom/corebloom/shapesbloom/texturesbloom/textbloom/audiobloom/modelsbloom/mathbloom/physicsbloom/scene
  • 一套真正的 PBR 渲染器 —— substrate 风格的分层材质、为静态几何体缓存的级联阴影贴图、ACES/AgX 色调映射、自动曝光、bloom、景深、运动模糊、SSGI、SSAO、TAA,以及用于分数渲染缩放的 CAS 锐化通道。
  • GPU 骨骼动画 —— glTF 2.0 导入、GPU 上的四骨骼线性混合蒙皮,每副骨架最多 128 个关节。
  • Jolt 物理 —— 刚体与软体、角色控制器、载具、射线检测、约束、接触回调。Web 目标使用 JoltPhysics.js 回退,因此同一份代码也能在浏览器中运行。
  • 六个目标平台 —— macOS、Windows、Linux、iOS、tvOS 与 Web。Android 已部分接入但尚不可发布。
  • 热重载 —— 保存一个 WGSL 着色器或材质 JSON,开发期间一秒内即可看到屏幕上的变化。文件监听代码会从发布构建中剥离。
  • 十七个示例项目 —— 从 170 行的 Pong 到加载 Intel Sponza 与 Bistro 场景。

近期我们引以为傲的一些进展

渲染器度过了不错的一个月。一些亮点:

  • Auto-DRS。渲染器会自调节渲染缩放以达到目标帧率,然后运行升采样加 RCAS 锐化通道,使图像保持清晰。你设定目标 FPS,引擎搞定剩下的事。
  • 平面反射。真正的镜面平面捕获,带斜裁剪,在反射预算耗尽时回退到 IBL。适用于水面、抛光地板与店面。
  • 纹理数组 splat 贴图。地形与细节图层现在可以绑定带正确 mips 的纹理数组,因此可以在每个 tile 上绘制多种材质,而无需为每一层支付一次绘制调用。
  • Imposter baker。一个小型 CLI,为远距离 LOD 烘焙八面体 imposter 图集。我们一开始往场景里塞森林,就立刻需要它。
  • 跨平台 HiDPI。Windows、Linux 和 Web 终于和 macOS、iOS 一样共享相同的 HiDPI 处理方式。UI 在每种显示器上都保持锐利。

我们不假装已经完成的部分

我们也欠你一份不那么光鲜的清单:

  • Android。平台 crate 已存在,大部分 FFI 表面也已接入,但 native-activity 胶水代码尚未完成。我们目前还不会建议任何人用 Bloom 发布 Android 游戏。
  • watchOS。着色器可以编译,平台桩代码已就位,但在 Perry 支持 watchOS 之前,这部分还无法成真。
  • 虚拟化几何体。没有 Nanite 等价物,没有网格着色器,也没有硬件光线追踪。这些在路线图上,但不在二进制中。
  • 骨骼动画上限。每副骨架 128 个关节,这是受 UBO 大小限制的硬上限。大型骨骼当前需要变通方案。
  • 场景图还很年轻。变换、可见性、阴影与材质绑定都已工作。查询系统和更广泛的优化通道尚未存在。
  • 用户着色器。你可以手写 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 的 native-activity 胶水代码,并将 Android 标记为可发布。
  • 把场景图的工作从“基础”推进到真正实用:查询、视锥剔除细化、实例化辅助。
  • 落地编辑器集成的初版,让你无需重启游戏即可迭代场景。
  • 开源示例游戏,包括一个比 Pong 略大一些的演示。
  • 详细介绍 Perry 的编译流水线。已经有几位读者问过了。

试试看

Bloom 基于 MIT 开源。整个仓库托管在 GitHub 上。如果你想先关注而不分叉,文档里有 12 行的快速上手,案例展示页面列出了我们自己用它构建的项目。

无论怎样:感谢你来看一眼。随着更大的部分落地,我们会继续在这里更新。