我们为什么自研 WebGPU 引擎,而不是 fork PlayCanvas
作者:Oleg Sidorkin,Cinevva CTO 和联合创始人

每个看到 Cinevva World 的技术人,五分钟之内都会问同一个问题。你们是个小团队。成熟的 web 3D 引擎已经有了。你们为什么要自己写 renderer、自己写角色物理、自己写动画系统,而不是站在 PlayCanvas 或 Babylon 上更快地发版?
这是个公道的问题,而"不是我们发明的所以不用"是个错误的答案。我们做了功课。我们跑过现有引擎、读过它们的源码,并在其中两个之上发过 spike,然后才做决定。这篇文章是那个决定的诚实版本:现成方案真正擅长什么、我们的需求在哪四个地方分歧大到值得自己造,以及哪些情况下你该选它们而不是照搬我们。这次自研的过程我们记录在一个持续更新的浏览器里的开放世界工程系列里,所以下面凡是有可运行日志支撑的论断,我都会附上链接。
我们真正评估过的几个选项
有四样东西都被叫做"web 游戏引擎",但它们根本不是同一类东西。
Three.js 是个渲染库,不是游戏引擎。它给你一个 scene graph、材质、loader 和一个 renderer,然后就让开了路。没有编辑器、没有物理、没有实体系统,也不对你的游戏该怎么组织发表任何意见。这既是它的吸引力,也是它的代价。renderer 之上的一切都得你自己造,但没有任何东西跟你较劲。它是 MIT 协议,拥有这个领域里最大的生态,而且当你凌晨两点撞上一个冷门的 shader bug 时,论坛上总有现成的答案等着你。
Babylon.js 是个完整引擎,带 scene graph、物理集成(Havok)、资源管线和一个 web 编辑器。它是 MIT 协议,背后有微软的团队,它的 WebGPU 工作一直推进得很快。如果你想要开箱即用,又乐于待在引擎的结构里,它是个不错的默认选择。
PlayCanvas 是最接近"web 版 Unity"的东西。写这篇文章时,引擎版本是 v2.19.6,于 2026 年 6 月 5 日发布,引擎本身在 MIT 协议下开源。不过大多数人真正用的,是它那个托管的可视化编辑器,而那个编辑器是个商业产品,不是开源的。PlayCanvas 用的是实体-组件系统,你用 TypeScript 或 JavaScript 写脚本,资源经由服务端的 GLB 管线流转,而且真有商业游戏在它上面发过版(比如 Snap 就在 PlayCanvas 上跑生产级的项目)。它的 renderer 跑在 WebGL2 上,WebGPU 路径还在成熟中,而不是默认选项。最后这个细节比听上去要重要,我后面会回头讲。
Unity WebGL 根本不是 web 引擎。它是个导出目标。你在 Unity 桌面编辑器里做,然后编译成一个 WebGL 包。当你已经有一个 Unity 游戏、想把它搬进浏览器时,它是对的工具;而当"在中端手机的标签页里秒开"是个硬性要求时,它就是错的工具,因为运行时和下载体积都得一起背上。
这里面任何一个,拿来做一个普通 web 游戏的地基都很合理。问题是我们做的不是普通的 web 游戏。
决定一:WebGPU 是我们的地板,不是我们的天花板
把我们推离每一个现成方案的那道分歧是这样的。对我们来说,WebGPU 是个要求,不是一个我们会慢慢长进去的功能。
我们的地形不是静态的高度图。它是流式高度图场和由符号距离场支撑的 marching-cubes chunk 的混合体,所以世界可以有真正的洞穴和悬挑,创作者也能现场雕刻它。雕刻笔刷、植被散布、地形网格化全都跑成 compute shader。把 compute 拿走,世界不是降级,而是根本跑不起来。
这跟通用引擎今天所处的位置正好相反。它们的 WebGPU 支持被设计成在一个 WebGL2 优先的 renderer 之上做渐进增强,并为缺少它的浏览器留一条回退路径。PlayCanvas 尤其如此,它是 WebGL2 优先,WebGPU 还在 beta 阶段。对它们而言这是对的选择,因为它们的工作是让尽可能多样的游戏跑在尽可能多样的设备上。我们的工作更窄也更深,所以我们做了相反的选择:我们在 spike 13 就走了 WebGPU-only,再没回头。没有 WebGPU 的浏览器不是被降级,而是不被支持,我们把这个当作一个触达数字来追踪,而不是假装一个 WebGL2 回退离我们只差一个配置开关。事实并非如此。那会是对我们地形和植被阶段的一次局部重写。
在一个 WebGL2 优先的引擎上构建,意味着要么在每个 compute 功能上跟它的回退假设较劲,要么永远维护两条渲染路径。自己拥有 renderer 让我们能把 compute 当成基线。
决定二:一个角色解算器,而不是一个物理引擎
教科书做法是塞进一个物理引擎。我们试过。早期有一个 spike 验证了 Rapier 在 worker 里跑,感觉还不错。
我们还是自己写了一个,发布出去的构建里哪儿都没有 Rapier、Cannon 或 Ammo。原因在于范围。我们不需要刚体、关节、布娃娃或约束解算器。我们需要一个胶囊体能正确地相对地形移动,需要行走、滑行、滑翔、攀爬和游泳对"我是否着地、踩在什么表面上、什么角度"给出同一个共享答案。一个通用物理引擎会让这件事更难而不是更容易,因为那些模式最后都在跟它内部的弹簧和阻尼较劲。
所以我们的角色控制器是一个可插拔的多通道状态机。每个模式都是一个小单元,它声明本帧是否想要控制权,如果它赢了,就写入速度和朝向。它们跨三个通道按优先级仲裁,资源、然后姿态、然后移动,而且它们都读同一个地形查询。碰撞是一个胶囊体探针,根据 chunk 不同去探高度图或符号距离场。
我最得意的细节挺不起眼。着地检测会扫描你脚下垂直列里的每一个表面,挑出在胶囊体之处或之下最高的那个,而不是信任 SDF 梯度。在悬挑的边缘,侧向最近的表面是崖壁,所以基于梯度的法线会在"地板"和"墙"之间闪烁,你就会得到幽灵般的滑动。列查询让站在凸出的台沿上变得很无聊,而这正是你想要的。一个现成的高度场碰撞器不会白送你这个,因为它压根看不见我们的地形。地形以我们的格式存在 GPU 上。任何外部引擎都没法跟它碰撞,除非我们每次编辑都把整个场拷进它的碰撞器格式里。
决定三:能跨骨架迁移的动画
Avatar 用的是 Synty POLYGON 骨架,我们的动作片段来自一个大型开源动画库。因为骨架和片段共享骨骼名,常见情况在运行时不需要重定向,轨道直接按名字绑定。完整的角色管线我们写在了通用角色里。
有意思的活儿是它们对不上的情况。我们做了一个重定向流程,把一套骨骼映射到另一套上,而要做对它,意味着要解决三个具体问题。你得先对齐绑定姿势,因为 A-pose 对上 T-pose 会悄悄给每个关节加上大约三十度。你得用两个独立的比例去缩放根运动和步幅,垂直方向用髋到地,水平方向用整体比例。还有,你得在一个固定的采样率上拿重定向后的片段去对照它的源,这样回归在玩家发现之前就先暴露出来。正是这条管线让我们能加入新的动画来源,并最终加入创作者提供的角色,而不必手动修每一个片段。
在这之上坐着一个动画状态机,它每帧从运动状态里挑出对的片段族:按速度选跳跃变体、按冲击选落地的剧烈程度、从速度对地形梯度的点积里判断坡向、相位正确的脚步停止让你不会"太空步"般刹住,还有一个一百毫秒的去抖,这样蹭一下墙不会让你在行走和坠落之间频闪。这些都不奇异,但只有自己拥有这一层你才能得到。
决定四:没有任何引擎会附带的那部分
另外三个决定讲的是世界怎么跑。这一个讲的是产品到底是什么,也是为什么拿它跟 PlayCanvas 比较归根结底是个类别错误。
PlayCanvas、Babylon、Unity,以及一个手搓的 Three.js 应用,都假设同一种形态:你坐在桌前造你的游戏,在一个 2D 编辑器里,从外面看着这个世界,然后你点播放、发一个独立的运行时。哪怕它们的引擎内编辑,也是你坐在工作站前去摆弄一个你正看着的场景。
Cinevva World 把这反了过来。你从世界内部创作,作为一个 avatar 站在你的玩家将来要站的同一个空间里,靠描述你想要什么、看着它出现。创作和游玩是一段连续的会话,而不是一个被桥接到运行时的构建步骤。最接近的参照点是 Roblox、Rec Room 和 Horizon Worlds,而不是 web 3D 引擎,而且就连那几个也把大部分创作留在桌前。让这件事在没有鼠标驱动的编辑器的情况下也能搞定的,是那个 AI 构建器,它把"在这儿放一个灯笼照亮的码头"变成几何体和摆放。
你没法把这个东西拴到一个通用引擎上,因为它不是个渲染功能。它是整套技术栈的前提,从地形如何在运行时可编辑,到网络如何把每个对象都当作刚刚由一个站在附近的人造出来的东西。
什么时候你不该照我们这么干
下面这部分是"我们造了自己的引擎"这个文体通常会跳过的。如果你想这个季度就发一个浏览器游戏,别照搬我们。想要 Unity 风格的编辑器和一条托管管线就用 PlayCanvas。想要一个自带物理的完整引擎、又乐于待在它的结构里就用 Babylon。想要最大的控制权和最少的意见、而且你有团队在它之上构建就用 Three.js。已经有一个 Unity 游戏就从 Unity 移植。
自己写 renderer、物理和动画,只有在你产品的前提跟现成方案不兼容时才是对的选择,也就是当你造的不是一个跑在引擎上的游戏、而是一个恰好由引擎构成的地方时。这对我们来说是真的。它对你来说大概不是,而那没关系。诚实地做这次评估,意义就在于在你写下第一行代码之前,先知道自己处在哪一种情况里。