重构MIAW

内容纲要

我已经忍不了辣!

积重难返

之前在边学边写一个实时渲染器MIAW,到现在为止已经解决了不少问题,学到了很多东西,但同时也探明了更多严重的问题。而目前MIAW的代码量已经接近两万行了,我在制定继续扩建它的下一步计划时遇到了比之前都更加严重的问题——无论是代码结构上,还是性能上,或是算法上——原本的MIAW的框架已经很难扩建并继续容纳新的内容了,然而MIAW目前的性能和功能还远没有达到我的期望。

经过一段时间的权衡,我认为相比于痛苦地继续魔改强行续命,不如趁着代码还没那么多,把走到目前为止的经验以及从网络上调研得到的经验进行融合,并完全重构MIAW。

目前已经探明的问题/需要新增的功能主要有:

  • 重新制定目标并缩减不必要的结构

    到大学毕业还有一年的时间,目前来看,这一年时间内比较合理的预期是实现一套渲染管线(配套简单的动画引擎、简单的场景编辑器、还有一点创新),而脚本、场景树、Component等其它功能是完全不必要的。我可以将这部分削减,并再重新分为渲染、动画、编辑三个模块。重头应该在渲染上,动画和编辑功能的定位只是用来辅助实现渲染效果的工具。

  • 丑陋的UI布局

    虽然功能(尤其是排版功能)上差一些,但我决定续用ImGUI,其优点在于轻量,且绘制的主导权在我手上,而不像Qt或者其它框架一样——它们既是UI绘制库,也限定了整个程序的框架。而且我也应该不需要绘制过于复杂的UI。

    不过UI布局必不可能再那么丑陋了,我决定将UI的各个窗口合并成一个大面板,并将绘制逻辑集中到编辑模块里。

  • 奇怪的宏定义

    MIAW模仿Mitsuba的设计定义了namespace miaw {的宏(让IDE少一个缩进),结果发现CLion根本不认账还有奇奇怪怪的Bug,不如不用。

  • 渲染模块的问题

    • 无状态渲染模块

    原来的MIAW的渲染模块被设计为无状态(或状态极少)的,网格、变换、材质渲染管线等状态都被保存在其他模块里,在每一帧用所有的这些资源组装成一系列“渲染目标”并提供给渲染模块进行绘制。

    这样的好处是自由度高、代码简单逻辑明了,帧与帧之间的状态变换在渲染中不会引入额外开销,坏处是渲染模块的功能和效率都受到了非常严重的限制(比如不能做高级的Clipping,不兼容光追,点选功能实现困难等等)。

    • 过多的DrawCall

    现在每种材质都有自己的管线和DescriptorSet,好处是泛用性极强,坏处自然是数量爆炸的Device Call。

    • 各种色彩数值混为一谈

    SRGB和UNORM格式混用,Gamma是多少?我又干了什么?运算中的是Radiance数值还是Perceptural Color?我又输出了什么?完全不知道呢!

    • Vulkan版本和正确用法

    一年过去,Vulkan 1.3已经推出并被硬件厂商的驱动广泛支持了,然而MIAW还在使用Vulkan 1.0。而使用vulkan.h的代码实在是太臭太长了!那为什么MIAW没有使用C++ API呢?因为我当时还不知道有vulkan.hpp这玩意!

  • 简化编辑器

    现在编辑器不需要太复杂的功能,由于渲染外功能的简化,能变得更加简洁。

  • 更合理的存档系统和资源管理系统

    好歹得把资源文件复制到工程目录下吧...之前的MIAW甚至没有“工程目录”这一概念!

  • 使用PRE_INITmain函数启动前执行代码)宏会导致一系列恶心的问题。

    • 比若链接姿势不当会导致这些代码部分失效
    • 同时程序结束时创建的资源不能很好地按顺序回收,导致每次关闭都会爆一大堆错误出来。

新的工程:渲染

  • 在保证渲染质量和一定编码简易度的前提下尽我所能地做到高效

  • vulkan.hpp

    不说了,C艹赛高。

  • HDRP

    受够LDR了,HDR在更Coooool的情况下好像在现代图形硬件下也不会损失太多性能(所有FrameBuffer用16位浮点保存Radiance)。

  • 渲染模块保存状态

    实践已经证实了无状态渲染模块绝对是做不到高性能的,经过仔细考虑,我决定在相反的方向All in。

    我决定将全部渲染状态都放入渲染模块之中,并受其维护,而其他模块(编辑器、动画模块)能做的只是调用其给出的接口来修改其内在状态,或者修改一些同步的缓冲区(比如骨骼)。

    为了性能,接口调用的Validation也不会由渲染模块负责。

    “全部”渲染状态当然也包括网格、纹理、材质参数。

    • 场景树的设计

    渲染模块将保存一个简易的场景树。每个树节点有两种儿子,一种儿子是树节点,另一种儿子是RenderTarget

    同时,每个节点会有三个布尔属性,staticdisabled以及RT_disableddisabled树节点的子树不会生成任何图形,而static属性的树节点会在第一次渲染时生成render command并缓存下来,之后渲染时不会再深入遍历它。同样的,任何static属性的树节点的子树内使用的材质、纹理和网格都不能被删除或修改,其本身transform也不会变化。而RT_disabled的子树将不会被加入光追的AS中(比如巨大的动态几何)。

    每一次static的切换都会产生大量开销,因为整个Static部分的场景是一个(或几个,基于材质数量)Draw Call解决的,如果切换的话要重新生成一遍这个大Draw Call的输入。

    而当static部分场景不变化时,渲染中,这部分数据产生的CPU时间是常数,即便你有几千几万个静态物品,也不会在CPU端造成任何开销。

  • GPU Driven Rendering

    当然,要做到这一点不可避免地会失去一些泛用性,详细解决方案在后文描述。

    同时决定(为了方便)将所有Buffer(Vertex、Index、GI等)的尺寸从头到尾都固定分配,溢出了直接爆炸。虽然听起来很垃圾,但有时简单的做法比复杂的做法效果更好。

  • Mesh Shader

    ...说实话,这玩意在我的使用场景下蛮鸡肋的(没有超高精度大模型、顶点复用强、...等)。但好歹也能减少VRAM带宽消耗,同时也可以用来做比Compute Culling更好的剪裁。

    • Descriptor Set数量将骤减,目前(不考虑Foward Rendering)只使用三个Descriptor Set:全局与物体、材质、纹理与光照

    • 其中全局包含一些全局配置信息,包含相机投影矩阵

    • 物体包含所有物体的Transform,用于Draw Indirect

    • 材质包含当前渲染管线兼容的所有材质(用相同管线、不同参数的材质)的全部数据。

    • 纹理包含所有(可能用到的)纹理

    • 光照包含计算光照所需的信息,包括阴影贴图,GI Probe等。

  • 为了节约内存和简化实现,现在不会有大于一帧On-the-fly,CPU和GPU是互锁的。

    file
    旧版MIAW:完全异步,可以有2到3帧同时被GPU渲染(基于硬件能力),上图为同时渲染2帧的情况
    file
    现在

    这样做肯定会损失一点性能,但它带来的程序复杂度的降低和Framebuffer以及VRAM带宽上的开销降低(尤其是多份Descriptor和同步的问题!)是非常令人愉悦的。同时也能节约内存,现在这个管线光一个高清Framebuffer就几百兆了(阴影贴图、超巨G-Buffer以及HDRP带来的),而之前的设计是同时渲染几帧就得开几个FrameBuffer。

    而且还有一点比较重要:虽然帧率会稍微降低,但延迟也会相应地降低。曾经的MIAW在帧率下降到60时就已经有明显的延迟感,我想改善这一点。同时,开销巨大的GI部分本身和整条传统渲染管线是完全异步的(详见下文中的渲染管线设计图),所以在传统管线中并不需要太过分的优化。

    为了方便易用,每个帧自带一份主存缓冲区(与对应的STL Allocator)用于缓存直到这一帧结束都保证不会变化的数据,大小默认为64MB,主要包含上传的渲染命令等。

  • 将UI渲染(后端)和画面渲染从代码上彻底分离。UI不需要太多性能,所以随便写就好,这样也不会污染主要的渲染代码。

  • 承接之前的设计的一点是,渲染模块不会支持RTTI、半反射指针等功能(为了追求最好的性能)。而对于渲染模块的管理和使用代码(编辑器、动画)则需要全部接入RTTI与半反射指针等来保证易用和安全。同样的,操作的合法性也要由编辑器和动画保证。

  • 渲染Canvas整帧的渲染管线(不包含UI)
    file
    紫色代表一个Subpass,总共三个大RenderPass。其中“PBRMaterial”就是UberShader。

    其中各个黄框为各种FrameBuffer。

  • 阴影的处理

    阴影分为三种,平行光产生的阴影、点光源产生的阴影、体光源产生的阴影。首先,我不准备支持点光源(嘿嘿),所以不予考虑。体光源产生的阴影目前计划将直接由GI实现,因此也不予考虑。而平行光产生的阴影准备使用Cascaded MSM(128bit)加Contact Shadow完成。

    在GI中的阴影直接使用一张覆盖整个场景(或者说近景)的阴影贴图加PCFSM计算,毕竟GI对于精度要求并不高,速度够快就好。

  • GI:高级光照

    这一部分是整个渲染模块的重中之重,也是一个大话题。

    我调研了不少历史和现在流行的GI的方法,也权衡有一段时间了,实现过Sparse Voxel Cone Tracing,看过SSDO、RSM、DDGI的论文,看过虚幻5的技术报告视频和博客(非常复杂的混合GI方法,其中几何简化部分已经超出了图形学中渲染分支的范畴,一个人是不要想实现)。

    最终我决定实现一种类似DDGI的Light Probe Based与基于Near Field的混合的方法。与DDGI的不同点在于我将实现所谓的动态“Probe Allocation”,经过一些调研,我已经有了一些相关想法(9月25日更新:这些想发已经暴毙了,不过我又有了一些新想法)。

    我准备写一篇专门的博客,一边实践一边探讨这个问题。

  • 毛绒绒渲染

    出于一些个人兴趣我也很想做毛绒绒渲染,目前也已经有了一些想法,不过没多少创新,也仍需要更多调研,由于该部分融入主要渲染管线不是很难,准备在GI实现完毕后再实现。

其它与后记

  • 关于显卡

    我这块RTX 2060是2020年初买的,当时矿潮刚起,花了4300巨款买了一个丐版2060(有RT核心的底端显卡)+噪音巨大还接触不良的垃圾显卡坞,平时除了调程序也不用什么高负载应用(联盟不算吧?),结果从今年年初就开始不对劲了,调程序高负载的时候会闪屏,然后到今年暑假病情更严重,在Windows下4K输出办公写代码屏幕上就出现噪点,高清分辨率玩联盟都会掉到30、40帧(不如核显),有明显的间歇卡顿感,用NVIDIA测试套件跑了一遍,告诉我它显存炸了。不过Ubuntu日常使用目前还没问题,感觉应该是买到矿卡了,电脑店老板表示没有维修价值,看看还能撑多久,撑不住的时候就再买新的,淦**矿老板。

    或许我的Sparse Octree的GI卡慢也是因为显卡原因?

此条目发表在小项目分类目录,贴了标签。将固定链接加入收藏夹。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注