属于年轻人的第一个Vulkan Forward+与OIT

目录

心念所动,哪怕只是梦境的余温,亦可突破门扉,将刀刃铸就。
——神里绫华

又是一个多月过去了,又在实验室摸了一个多月,这个月真实发生了很多事,现在都还没回家,感觉有点难受。

这个月主要就是把上一篇文章摸出来的一个Vulkan简单框架整理了一下,窗口换成QT的了,加了点命名空间,重写了部分模块,并且新加了Forward+渲染模式和A-buffer、Depth-Peeling两种半透明物体渲染模式。本来还想等之后加了音频模块和控制模块在整理文章,但想想还是算了,封模块着实没啥意思,能用就行了,不如先把这两个涉及到以前没仔细学过的东西整摸个文章出来。

上旬摸了一两周鱼,导致现在才刚写完这俩模块,其实工作量并没有那么大。

仓库在这里:

GitHub - FREEstriker/Air_TileBasedForward​github.com/FREEstriker/Air_TileBasedForward正在上传…重新上传取消icon-default.png?t=M666https://link.zhihu.com/?target=https%3A//github.com/FREEstriker/Air_TileBasedForward

现在效果就是这么个情况:

Forward+与OIT54 播放 · 0 赞同视频正在上传…重新上传取消​

反射球是Forward渲染,中间的球是Forward+渲染,那个复杂的透明体用的是Deepth-Peeling/A-Buffer和Forward+渲染的,上面两个破碎玻璃一个是Forward的顺序相关的半透明渲染,一个是Forward+的顺序相关的半透明渲染。

大言不惭的说,我可能摸出来了一个还算能用的简单的srp了,虽然效率贼低,但是用来验证学习应该来说是足够了。

窗口系统

原来窗口使用GLFW写的,有点太底层了,并且初始化Vulkan非常的麻烦,我记得我刚开始学的时候光看着那个初始化就看了一两周。

考虑到之后还会加输入控制,可能用个比较完善的窗口系统会比较方便,因为再原来写的那个软渲染器就是用的QT,所以就直接用了QT,而且比较爽的是,QT的窗口系统自带了一个VulaknWindow的东西,他会帮你初始化Vulkan,并且给你提供一个循环接口,挺舒服的。

原来写了几百行的初始化,这里只要寥寥数行就搞完了。

硬要说不太好的地方,就是网上几乎就是没有QT使用Vulkan的相关教程指南,遇到错误会非常的难搞,只能去抠QT的文档,我现在就是怎么也搞不懂如何把QT输出的Vulkan相关消息截下来用自己的Log输出。

我上周还发现自己忘了开“VK_LAYER_KHRONOS_validation”验证层了,之前还以为自己写的代码贼对,其实疯狂报错,用了好几天才把报的错全给解决了,我真傻真的。

Asset Manager

上一版的AssetManager写的还是比较拉的,我再检查的时候有非常显眼的bug,这次重写了一遍,但好像还是有点隐藏问题,之后再仔细查吧,反正现在接口用起来非常方便了。

之前的,全都是分开的

现在的,全用一个接口

多摄像机支持

之前那写的,Attachment都是和RenderPass绑死了的,根本不支持多摄像机。

之前RenderPass和FrameBufferManager直接绑死

当时其实就没有搞明白Attachment和RenderPass的关系,封了RenderPass,还封了个FrameBufferManager出来,整的挺离谱的。

指定RenderPass和Attachment

现在我直接在创建摄像机的时候,就指定好这个摄像机所使用的的RenderPass和拥有的作为Attachment的Image,这样就可以直接用这两个设置,创建出对应RenderPass的FrameBuffer,并且将该摄像机的所有的FrameBuffer合成一个RenderTarget,感觉好像就和Unity里的RenderTexture差不多了(其实我RenderTexture用得少,我也不是很懂),大概的创建流程如下图所示:

有了这个RenderPassTarget,在渲染循环的时候就可以对每个摄像机都进行一遍渲染,最后再把主摄像机的画面拷贝至交换链的图像上。

大概是这个感觉

GLSL

之前写的glsl虽然也抽成了几个glsl头文件,不过头文件里还包含了Descriptor Set的定义,根本不可能拆开复用,这回重写了一下,并且把函数名也都重改了。

现在的可以共用的头文件包括:Camera.glsl、Collision.glsl、Light.glsl和Object.glsl,里面只包含结构体定义、函数体和其他定义的一些预处理参数,可以随意地被引用。

只有结构体与函数

还有一些必须定义Descriptor Set的头文件,比如:TileBasedForwardLighting.glsl、TBF_OIT_DepthPeelingLighting.glsl等,这里面包含了资源的定义。并使用预处理命令,让同类型的头文件只能使用一个,这样就不能即使用Forward光照又使用Forward+光照了。

包括了资源的定义

我现在居然还觉得预处理挺好用的,哈哈哈哈,好怪。

哔哔了前面这么多,其实这部分才是比较重要的,下面分成Tile-Based-Forward、Depth-Peeling和A-Buffer三个部分来说。

Tile-Based-Forward

Forward+就是Tile-Based-Forward,它的本质就是将屏幕分为多个Tile,通过ComputeShader计算每个灯光是否对这个Tile造成影响,对每个Tile构造一个灯光的索引表,之后在渲染中就可以根据Fragment的实际位置定位Tile,从而定位对其造成影响的灯光,从而避免每个Fragment对每个灯光都计算一遍。

计算灯光是否造成影响的算法就是检测Tile的包围盒与灯光的包围盒是否相交,显然灯光的包围盒我们是可以很方便的得到的,而Tile我们是对屏幕切割而来的,这就涉及到一个重建世界坐标的过程了:需要将屏幕上的Tile转换为世界坐标系内的一个六面体。

直接构造Tile包围盒

但实际上,性能是可以提高一些的,如果我们有一张深度图,我们就可以在ComputeShader中计算每个Tile的最小深度和最大深度了,通过计算的深度来缩小这个Tile包围盒的尺寸,这样我们就可以过滤掉更多的灯光,从而获得更好的性能。

使用深度图大幅缩小Tile包围盒尺寸

原理可以说是非常简单,基本一看就能明白,主要的难点在于集成到已有的代码里,并且不出错误。

首先来个Pre-Z Pass,搞个深度图出来:

我是把视椎体剔除放在RenderPass里了,就是正常的画。不过有一个问题是,D32SFLOAT格式的深度图很少有设备支持StorageImage访问,具体的支持情况可以在这里找到。

那我们要在之后的阶段使用深度图的画,最好是不用D32SFLOAT格式,最好把它拷贝到R32SFLOAT,颜色的格式支持的情况就比较好了。

但是,好像没有办法从D32直接拷贝到R32,CopyImage只能同种类型的拷贝,BlitImage也是类似的情况,我只在网上找到一种方法就是先把D32拷贝到Buffer,再把Buffer拷贝到R32(真实复杂呢),这就很离谱了,必然非常消耗时间,但是没有办法,所幸的是只用拷贝这么一次,怪不得URP里单独的需要指定最早的使用深度图的阶段。

深度图

深度图有了,就可以直接构造一个光照索引表出来,我是把这些计算写到了BuildTBFLightListsShader.comp文件里,里面用barrier()函数搞了这么一些个阶段:初始化参数、计算本地工作组的最大最小深度值、构造包围盒、相交计算、写入结果。

我是把Tile的大小设置为了32*32,全局工作组根据Attachment的尺寸计算,本地工作组大小为8*8,具体怎么算直接看代码吧,我现在半夜看着屏幕想了半天没想出来怎么说。。。

计算Tile最大最小深度值

由于Tile是32*32,本地工作组是8*8,所以显然你只能先算出来每个本地线程的最大最小的深度,再遍历每个线程算出来的深度值,得出这个Tile的最大最小深度值。本来想用shared float和aotmicMax方法来直接算,根本不需要这样的两步,但是显卡好像并不支持这个拓展,所以只能用这种略麻烦的办法。

构造Tile包围盒

这步就是直接把屏幕坐标转换为ndc二维坐标,合上之前算的最小最大深度值,构成三维ndc坐标,然后把它转换到观察坐标系,然后在观察坐标系下直接构造包围盒就好了,非常简单,我是卡在ndc到view挺长时间的,网上的教程好像都是抄的同一个,都是直接把Unity的那个函数直接抄上了,看也看不懂,还没有讲解,挺离谱的。

实际上原理是非常简单的:

当然这个计算是和我选用的坐标系有关的,看看大概意思就行,上面这个就可以计算出来透视摄像机的观察坐标系下的Z,想要计算线性深度就直接在用f和n再搞一下就行,很简单。正交相机的Depth就直接是线性深度,直接就能用。

接下来我们就让每个本地线程取一个灯光的包围盒对本地工作组的Tile的包围盒做相交检测就可以了。

中间那三行就是原子操作递增表长的。

最后再写点数据进去,方便之后的读取。在Fragment阶段,我们直接根据gl_FragCoord得到Fragment的像素位置,再根据Tile大小得出所属的Tile光照表在光照表Buffer中的位置,使用光照表中的索引就可以获得灯光数据了,是比较容易理解的。

看起来好像是没有问题的。

半透明与Forward+

再来看一下这个图,如果我们需要渲染半透明的话,这个光照表肯定是不对的,需要适当扩大一下这个包围盒,如果要获得精确地包围盒,那么就必须再来一个半透明物体的Pre-Z,在拷贝出来深度图,挺麻烦的:

我是没有选择这么做,我是直接把包围盒的其中一个面改成了摄像机的近裁面,免掉了一些麻烦的操作,大点就大点把:

这个在计算半透明光照表的时候可以稍微的优化一点,因为半透明的Tile包围盒大,我们就先构造半透明的光照表,不透明物体的光照表就可以从半透明的光照表中构造,减少一点计算。我这个小优化其实原来是写了的,不过我调半透明Forward+的时候发现哪个地方有个bug,造成了一些离谱的渲染结果,找半天不知道哪错,把这个优化去掉就通了。。。

下面的碎玻璃是Forward的顺序相关半透明

左面这个就是Forward+的顺序无关的半透明

Depth-Peeling

一般来说渲染半透明物体的话直接对深度排个序,再一个一个渲染就好了,我之前场景中的破碎玻璃就是这样选染的。不过这种方式只对简单物体有用,如果物体自身形状比较复杂,会挡住自己,或者厚度较大造成排序错误,渲染出来的东西就是很差的。

OIT就是Order-Independent-Transparent的缩写,就是说这一类方法可以不对物体排序,直接渲染就可以得到正确的效果。Depth-Peeling是英伟达提出的一种方法,它是通过每次渲染透明体的最外层,并把它剥掉,最后把渲染的多层合成到最后的结果上,大概就是这么个原理:

原理是非常简单的,就是做起来有点麻烦,剥层的原理就是使用上一层渲染的深度图作为阈值,比这个阈值大的就说明在上一层的后面,这样循环起来,就可以一层一层的把它剥掉,不过还需要一张之前不透明物体的深度图用来剔除被挡住的透明体,但是由于我们需要上一层的深度图,所以肯定是要写Depth-Attachemnt的,所以Z-Test只能手动discard来实现,过程就是这样的:

这样就可以渲染出N层,我是把N设置成了4,就已经有比较好的效果了。

第一层

第二层

第三层

第四层

最后再通过一个Pass,把这四层合成一下画到相机的Color-Attachment上,一般来说想的就是有N层,就画N个全屏网格,把每一个网格当做是半透明渲染,使用经典的混合公式进行混合。

但这样需要画四遍,且必须等上一个网格画完之后才能画下一个有点麻烦,但这就是经典混合公式的缺点,他只能从远往近依次渲染,被称为over操作。但还有另一种混合公式,它是从近往远渲染,称为under操作:

原理就是通过经典混合公式展开重新组合得到的:

这样我们就可以在一次渲染全屏网格的过程中,采样这四个层,最后再混合到相机的Color-Attachment上:

效果是没有任何问题的:

不过需要注意的是,我第一次写的时候是把这四层当做是Storage-Image采样的,不过发现R8G8B8A8格式的图片很少有支持Storage-Image,所以最后就改成使用Sampler采样了,其实是一样的。

Per-Pixel-Linked-List

这个A-Buffer的方法感觉算是OIT的比较快方法了,它的本质就是对每一个像素位构建一个像素链表,这样就可以把对该像素有影响的半透明Fragment结果存下来,接着再用一个单独的Pass进行一个排序和合成就可以了,避免了在Depth-Peeling方法中反复的拷贝深度图且需要画N遍的缺点,理论上稍微快一点。

大概就是这种感觉

显然,首先需要一张R32UINT的图作为头结点的存储图,还有一张较大的Buffer存储颜色、深度以及下一个Fragment的索引,我是把这个Buffer的大小设定为了四倍像素数量,应该也差不多够用了。

我好像并没有处理好Buffer溢出的情况

我是直接把这个写成了头文件,在Fragment阶段算完颜色之后,直接调用这个方法把它塞到链表中就可以了。接着把这个链表在额外的混合Pass里对这个链表做一个排序,再按照under操作混合就好了,还挺方便的。

看起来好像也是没有什么问题的。而且这个方法的帧数比Depth-Peeling高一点,毕竟减少了复制的操作。

其他OIT

但其实还有一种略微有损的方法,也是基于A-Buffer的,叫做Adaptive-Transparent,他与基本的A-Buffer相比,只有最后的混合阶段不同,它并没有对像素进行排序,而是通过一个叫做visiblity的参数来直接对颜色进行混合,而这个visiblity参数是一个单调递减的近似函数,原理非常简单,直接看这里就好。

不过因为他这个构建近似函数的过程我实在是写不出来,看了一下午,还是放弃了,这个方法比排序的A-Buffer会快一点。

还有一种比较玄学的OIT方法:Weighted Blended OIT,这里是论文,不过太长,直接看这个也可以,本质就是根本连A-Buffer也不要了,直接结合深度和透明度,用算法算一个因子乘到颜色上就好了,必然是非常快的,效果看起来也还行。

几种效果较好的因子的算法

我现在其实非常迷茫,越写越不知道写的这些东西到底有没有用,我现在总是感觉我写的这些个东西的原理也不是非常困难,随便看看就能学会,总感觉唯一性还不太够,经常有这样的感觉。还总害怕明年这时候找不到工作,难顶,一直都感觉自己挺彩笔的,ε=(´ο`*)))唉。

原神2.8刚开我就把万叶抽出来了,不得不说,用的真爽,这聚怪,从未体验过,马上就把砂糖的一套圣遗物拔下来凑合用上了。

现在感觉打游戏也在坐牢,天天就上了游戏清清日常然后开刷圣遗物,而且圣遗物怎么也刷不出来,难顶。

就是说,想去上海,想去mhy。

md今天刚用实验室的项目的设备就把设备搞坏了,又可以开摆了,就是放假不知道啥时候了。。。

下面应该会加个音频模块和输入控制模块,再往后向摸个Shadow-Map和SSAO出来,还准备搞搞球谐光照,感觉我现在也有Tick-Tock节奏了,一段时间搞点周边的,一段时间搞点渲染的。

希望一切顺利吧!

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值