游戏引擎Godot系统架构分析:渲染管线

Posted by 熊耀华 on 26 December 2023

边沁主义渲染管线:用尽量小的成本为尽量多的用户提供尽量好的视觉效果。

概况

渲染管线是游戏引擎中生成图像效果的子系统,而图像效果的表现力是决定游戏体验的重要因素。因此渲染管线可能是游戏引擎最重要的子系统。渲染管线相当复杂,在设计中需要做出不同的取舍。Unreal引擎的渲染管线采用最新的技术,适配最新的硬件,最求最真实的图像效果。Unity引擎的渲染管线像一个大杂烩,包含了过去十多年遗留下来的几套完全不同的方案。

Godot渲染管线的设计基于非常独特的考虑,可以总结为:用尽量小的成本为尽量多的用户提供尽量好的视觉效果。首先Godot作为依靠贡献者的开源项目,开发者时间有限,没有能力维持几套平行的管线。其次Godot需要适配大多数用户,很多任然在使用手机、笔记本、浏览器等图形性能很弱的平台。因此Godot的渲染管线设计的首要目标是用一套管线适配各种平台,然后才是在条件允许时尽量提高画质。

Godot的渲染管线分为2D和3D两部分。理论上2D渲染是3D渲染的子集,可以用3D管线实现2D游戏,例如Unity的做法。但是2D渲染作为单独的管线有独特的优势。首先在实现像素风2D游戏时更容易做到像素对齐(Pixel Perfect);其次单独的2D管线可以方便GUI的渲染;然后2D管线可以针对2D图像元素进行特殊的优化实现更高性能;最后对纯2D游戏可以剔除3D管线,缩小程序体积,方便浏览器部署。

总体架构

渲染管线是一个三层结构,分为RenderingServerRenderingDeviceRenderingDeviceDriver。上层抽象,下层具体,上层依赖下层实现。

Server层

渲染管线的总入口是RenderingServer,上面的API分别提供了2D和3D渲染管线的功能。这个层次的API提供了高层次图像元素抽象,不用考虑具体的图形编程接口和硬件实现。

其中2D管线最重要的概念是CanvasCanvasItemCanvas可以想象成平面空白画布,CanvasItem是画布上的贴花。2D管线允许多个Canvas存在,构成“图层”效果,每个Canvas可以有不同的Transform决定相对位置和Z-order决定重叠顺序。每个Canvas上的CanvasItem组织成树状结构,树中的父子关系决定了相对变换和重叠顺序。

3D管线最重要的概念是SceneGeometryInstanceScene可以想象为空白的世界,GeometryInstance是世界中的物体。3D管线也允许多个Scene,但不同的Scene可以看作互不关联的平行世界。GeometryInstance同样组成树状结构,父子关系也决定相对变换。

2D管线和3D管线的结合点是Framebuffer,两者的渲染结果都要写入某个Framebuffer。然后用Compositor组合不同的Framebuffer形成最终画面。

Device层和Driver层

RenderingDevice的API面向现代硬件,最常见的概念是BufferShaderPipeline等。RenderingDeviceRenderingDeviceDriver为不同的硬件和图形编程接口提供统一的抽象。其中RenderingDevice与具体的图形编程接口无关,提供共性的功能,比如参数合理性检查。RenderingDeviceDriver直接与图形编程接口相关,提供最小化的统一API,当前已经有了Vulkan和DX12的实现。Metal实现在开发中,未来有计划开发OpenGL和WebGPU的实现。

本质上Device层和Driver层的功能和WebGPU类似,都是提供硬件无关的图形编程接口。Godot开发者不直接使用WebGPU的原因是认为WebGPU针对浏览器设计,为了网络安全牺牲了太多的性能和功能,不适合游戏开发场景。

Server层逻辑

Server层提供硬件无关的高层渲染逻辑,可以总结成三个部分:Storage、Instance和Culling、Shading。Storage中存储的共享数据块,典型的数据块类型有Mesh、Texture、Material、Sekleton、Particle Setting、Camera Setting、Environment Setting等。Instance代表独一无二的实例,例如2D管线中的CanvasItem,3D管线中的GeometryInstance。多个实例可以直接、间接共享一个数据块。游戏中的典型场景是某个实例一般会引用一个Mesh,而Mesh又会引用Materia、Skeleton;Material又会引用Texture、Shader。

每个实例会占据空间中的一定范围,用坐标轴对齐方块(简称AABB)表示。Culling是根据摄像机的视野快速剔除AABB不可见的实例,避免大量无效的Shading计算。Shading是将视野中的Instance实际画到FrameBuffer上的过程。2D管线渲染相对简单,基本上就是按照Z顺序渲染CanvasItem,这样Z顺序小的先画,Z顺序大的后画。这样Z顺序大的CanvasItem覆盖Z顺序小的CanvasItem,实现正确的图层遮盖效果。

局部光照模型

3D渲染比较复杂,有不同类型的方法适合不同情况。最粗略的分类是Forward Shading和Deffered Shading。其中Forward Shading依次完成各个实例的渲染,通过Z Buffer实现正确的遮挡,缺点是实例中被遮挡的部分也做了昂贵的光照计算,浪费算力。Deffered Shading是将渲染拆成两个Pass,第一个Pass只考虑遮挡,不进行光照计算,而是将光照计算需要的材质数据写入G Buffer;第二个Pass根据G Buffer中的数据进行光照计算。这样的好处是只对可见像素进行昂贵的光照计算,避免算力浪费;缺点则是G Buffer需要占用更大的现存和带宽,不适合手机和较旧的硬件。

Godot虽然传统上为了照顾弱鸡硬件只支持Foward Shading,但进行了两种特殊优化。一种称为Forward+,实质是对光源和实例进行了Clustering,省略了计算遥远光源对某个实例的影响,一定程度上节省了算力。另外一种称为Mobile Foward,实质是将Framebuffer划分成Tile,依次渲染每个Tile,这种方式节省GPU-CPU带宽,适合手机。添加Deffered Shading的计划已经提上了日程。

全局光照模型

上节中的光照计算只正对光源对物体的直射,称为局部关照模型。要实现真实的图像效果还需要考虑物体之间光线的相互反射,称为全局光照模型。传统上Godot支持三种全局光照技术:Lightmap、Voxel Probe、SDFGI。其中Lightmap是将全局光照预烘培到贴图上,优点是对硬件要求低,缺点是不支持运动物体的光照。Voxel Probe也是预烘培,但保存的结果是空间中的光场,支持运动物体光照但不支持光源运动。

SDFGI是一种全动态的全局光照方法,支持运动物体、运动光源、甚至是镜面反射。基本思路是将Mesh场景动态转化为低分辨率的又向距离函数(简称SDF)表示,然后在SDF上进行光锥跟踪计算(Cone Tracing)。SDF的的场景表示有一些缺陷,4.3版本的Godot准备替换为一种基于层次化体素(简称HDD)的表示方法,新的全局光照系统代号HDDGI。

总结

本文对Godot的渲染架构进行了粗略的计算。当前Godot的渲染架构由其是3D管线部分处于一个快速开发期,马上会合并的重要功能还有新的Compositor架构,定制化的渲染管线、基于有向无环图(DAG)的渲染资源依赖管理等等。