开篇

  最早写 Ditto 渲染时没什么包袱,OpenGL 能画就行。毕竟引擎起步阶段需要的是“屏幕上有些什么东西”,只要能把 mesh、sprite、UI 画出来,Editor 看着像那么回事,其他都可以往后放。但这种写法很快就会到头。刚开始一个 Renderer 管全部还挺爽,后面一是Game View、Scene View、RenderTarget,二是Shader 和 Material 也要变成资源,再加上 DX12、Vulkan 这些更为现代化后端,原来那套 OpenGL 直连就有点撑不住了,再继续往里塞只会使代码臃肿而难以修改维护。

渲染抽象

RHI 边界

  所以后面就把渲染层抽象成了 RHI(Rendering Hardware Interface)。名字听着挺高大上的,但实际想解决的问题很简单:上层无需在意底层到底是 OpenGL、Vulkan 还是 DirectX 12。RHI 并不是为了显得架构高级,毕竟这次抽象确实是被需求推出来的。上层真正需要的是创建,绑定,绘制这些高层的指令,这些需求如果全写在 OpenGL 里,后面接 DX12 或 Vulkan 时就只能复制一大份逻辑。而RHI 做的事就是把“我要画什么”和“这个 API 具体怎么画”拆开。

1
2
3
4
5
Scene / UI / Editor
-> IRenderer
-> DirectX12Renderer
-> VulkanRenderer
-> GLRenderer

  这里也没有追求把三个 API 抹平成一模一样。DX12、Vulkan、OpenGL 的思路差别很大,硬装统一反而会很别扭。当前只抽引擎用得到的那部分能力,够用就行。

后端选择

  目前运行时后端顺序是:

1
DirectX 12 -> Vulkan -> OpenGL

  也可以用环境变量强制指定:

1
2
3
$env:DITTO_RHI = "dx12"
$env:DITTO_RHI = "vulkan"
$env:DITTO_RHI = "opengl"

  DX12 是 Windows 下的主要目标,Vulkan 需要 Vulkan SDK,OpenGL 则更像保底和调试后端。多后端最大的好处感觉是为了后续可能的多平台构建以及渲染性能优化,但从其他方面而言,它也倒逼了我把渲染边界理清楚。

Shader 资源化

Shader 与 Material

  早期 shader 基本就是 GLSL 文件加载器:读文件、编译 program、找 uniform、绘制时往里塞参数。能用,但它和 OpenGL 绑得太紧了。现在 Ditto 里 Shader 和 Material 都是资源。Material 引用 Shader,并保存纹理、颜色、float 等参数。渲染时上层只拿 Material 描述“我要用什么效果”,底层后端再决定创建什么 pipeline、怎么绑定资源。Shader 文件这边则做成了偏 ShaderLab 的形式,.shader 里可以写 HLSLPROGRAM 或 CGPROGRAM。ShaderAsset 负责解析属性和代码块,再生成引擎内部 HLSL。统一用 HLSL 作为上层写法主要是为了少维护几份 shader。否则 DX12 一份 HLSL,OpenGL 一份 GLSL,Vulkan 又一份 SPIR-V 逻辑,个人项目很快就会被同步成本拖死。

1
2
3
4
.shader
-> ShaderAsset 解析
-> engine HLSL
-> 后端 Pipeline

编译链路

  Shader 编译链路现在大概是:

1
2
3
DX12    : HLSL -> dxc -> DXIL
Vulkan : HLSL -> dxc -spirv -> SPIR-V
OpenGL : HLSL -> SPIR-V -> spirv-cross -> GLSL

  看起来绕,但实际比维护多语言 shader 要好。代价就是工具链依赖变多,特别是 dxc.exe 和 spirv-cross.exe。所以 ShaderCompiler 里会检查 Vulkan SDK 下这些工具是否存在,编译失败也会把错误返回给上层。

  目前Vulkan 和 OpenGL 主要走这套 ShaderCompiler,DX12 后端则在自己的 renderer 里直接调 dxc 生成字节码,失败时还会尝试回退到 fxc。这在架构上算不上最佳,只是现阶段最省事,也能让三个后端先稳定跑起来。

编辑器渲染

RenderTarget 视图

  游戏运行时通常只关心其游戏视角,但编辑器不能这样。Editor 里有 Scene View、Game View,还要把渲染结果塞进 ImGui 窗口里显示。窗口一 resize,RenderTarget 尺寸也得跟着变;Scene View 还要画 gizmo 和选中框;Game View 则尽量接近最终运行效果。

  这也是我后来觉得 RenderTarget 必须早些抽出来的原因。它不是高级特性,而是编辑器正常工作的基础。如果所有东西都直接画到主窗口 framebuffer,后面做多视图会很难受。

ImGui 兼容

  Ditto 的 Editor 还是基于 ImGui。RHI 抽完后,ImGui 也不能继续默认自己跑在 OpenGL 后端上,必须接到当前 Renderer。否则场景走 DX12,UI 还走另一套上下文,迟早出问题。

  这部分写起来没有 Shader 那么有成就感,但很影响稳定性。Editor 每帧都要画 UI,framebuffer、texture、resize、command 提交任何一处没对上,表现出来就是闪烁、黑屏或者窗口大小一变全乱。

小结

  渲染改进做完后,Ditto 才不像一个 OpenGL Demo 外面套了层 Editor,而更像一个有渲染层边界的引擎。虽然距离商业引擎还有些差距,但至少上层已经不需要知道底层 API,Shader 和 Material 也能作为资源被项目管理。

  虽然和Unity、UE这些商业引擎比还差得远,但对个人项目来说大概能力都有了一些。后续如果继续填坑——罢了应该也不是最近的事了,虽然论文通过了,但春招还得继续,到头来还是不得停歇。