一个简单的Vulkan渲染器管线组织

延迟渲染管线核心在于先记录一遍每个像素上的信息,如Material、Normal、Albedo等,即G-Buffer,世界空间坐标可通过MVP矩阵和深度逆向算出。使用这些信息进行屏幕空间的计算,对于每个像素进行光照或阴影等计算,主要都是屏幕空间算法(即基于每个像素处理,而不是前向渲染中的每个物体处理)。

渲染器架构基于Piccolo引擎,项目地址MWEngine,Shader及算法相关实现参考Examples and demos for the new Vulkan API,只实现了不透明物体的相关光栅化。

博客中不关注具体的实现细节以及在渲染管线之外的操作,如具体资源的管理及窗口操作等。(语文不好,碰到语句不顺请见谅)

主要分成预计算管线和逐帧管线

  • 其中预计算相关管线只需渲染器初始化时执行一次
  • 逐帧管线每帧都需要执行一次

大致流程如下:

目前在延迟渲染中实现了Shadow、AO、Lighting,并希望其并行处理后进行Composite。

同时希望可以实现不同的算法,因此尝试实现一个良好的架构(目前尚未重构),架构如下:

预计算管线

计算在渲染器中需要的数据,目前只有IBL所需数据,在IBL Pass初始化时即进行预计算生成图中所需的三个数据,三个数据分别走三个VkRenderPass,执行一次后相关资源直接销毁,保存所需数据。

逐帧管线

分为预计算部分和计算部分

  • 其中预计算部分主要生成每一帧中的所需资源,例如shadow map等。
  • 计算部分主要目的是在窗口中画出一张好的图(图中主要体现在RenderPass Begin和RenderPass End之中),使用Vulkan的subpass机制,尝试做出好的优化。

逐帧预计算部分

目前只有Shadow Depth Pass,生成ShadowMap。

Shadow Depth Pass

从光源角度生成一张深度图,即ShadowMap,这里不讲解ShadowMap原理,详情可参照实时阴影技术(1)Shadow Mapping。这里只有一个平行光,使用CSM技术,所以需要生成多张ShadowMap。

img

在架构上的是被Shadow Mask Pass相关Pass所管理(不同的Shadow算法可能需要不同的Depth算法),在Main Camera Pass执行之前获取。

逐帧计算部分

这里主要指一个完整的延迟渲染管线流程,即G-Buffer、AO、Shadow、Lighting、Composite等。其中Composite Pass中可以混合数据和后处理。

屏幕空间算法使用一个覆盖整个屏幕空间的三角形进行操作,利用UV确定像素坐标对G-Buffer采样。

// Vulkan Draw
vkCmdDraw(commandBuffer, 3, 1, 0, 0);
// Vertex Shader
layout(location = 0) out vec2 outUV;
void main()
{
    outUV = vec2((gl_VertexIndex << 1) & 2, gl_VertexIndex & 2);
    gl_Position = vec4(outUV * 2.0f - 1.0f, 0.0f, 1.0f);
}

由于某个Pass可能需要其他Pass的Descriptor相关信息,会生成全局变量+extern以供其他Pass使用,在代码中就是在Framebuffer中创建多个ImageView,同时在VkRenderPass的建立中设置好Dependcy。

G-Buffer Pass

类似于前向渲染,每个物体都进行一次Draw Call,但不进行复杂计算,进行MVP变换后,记录屏幕空间所需要的信息,生成多张屏幕大小的图(如1920*1080)。

优化:Piccolo提前用AABB和屏幕视锥体在CPU端进行相交判断,只将出现在视锥体的物体进行Draw Call,不过Piccolo是在CPU端实现这一过程,再优化可以在GPU端完成。

AO Pass

环境光遮蔽(Ambient Occulusion,AO),就是某个shading point因为被其它几何表面所遮挡 ,从而降低了接受环境光的比例(这种遮蔽常常发生在凹处表面),个人理解就是光打在几何表面的概率,如果这个表面被其他表面遮挡,概率便下降了:

img

目前只实现了SSAO算法,有待实现其他即插即用的AO算法。

SSAO

具体算法可参照基于屏幕空间的实时全局光照(Real-time Global Illumination Based On Screen Space),基本思路是利用像素的位置及法线,在着色点的法线方向上形成一个半球,在半球上进行重要性采样,如果采样点对应的G-Buffer中的深度如果小于采样点深度,即表示采样点被遮挡,光流入该表面的概率降低。

Shadow Mask Pass

生成屏幕空间的ShadowMask,Shadow Mask Pass所需的ShadowMap在逐帧预计算的Shadow Depth Pass中生成,这里直接使用生成好的ShadowMap进行阴影计算,目前实现了平行光的CSM算法。

有待实现其他光源的对应阴影算法。

CSM

CSM技术同样可参照上述的博客,主要用于平行光,将摄像机视锥体分成若干个Cascade,对于光源来说,每个视锥体生成一张ShadowMap,具体阴影计算仍然可参考实时阴影技术(1)Shadow Mapping,在生成阴影过程中实现了PCF算法,即同时比较ShadowMap对应的位置附近纹素,这里可以使用低差异序列进行采样,也可以直接使用一个卷积核。

Cascade设计参考原神设计:分为8级Cascade,前4级每帧更新,后4级第2/4/6/8帧更新(保证8帧将所有Cascade更新完)。本渲染器的后四级根据当前帧数对2/4/6/8取模更新(其实是一开始理解错意思实现错了,然后懒得改了)。

Lighting Pass

主要实现光照算法,目前实现了PBR-IBL。

PBR-IBL Pass

IBL原理可参考浅谈IBL(Image-based Lighting) - 知乎 (zhihu.com)](https://zhuanlan.zhihu.com/p/659128090),PBR原理也可参考这位大佬的其他文章。

IBL在预计算中需要算三项——irradiance map、prefilter environment map、LUT,将IBL公式分为漫反射和镜面反射两部分,其中漫反射项需要预计算irradiance map,镜面反射项需要计算prefilter environment map、LUT。

IBL只算出了环境光,具体的每个光源的PBR计算也同时结合在Shader中。

Composite Pass

主要将AO、Shadow、Lighting三项合并起来,代码中实现上就是直接做乘积。

layout (input_attachment_index = 0, set = 0, binding = 0) uniform subpassInput inputShadow;
layout (input_attachment_index = 1, set = 1, binding = 0) uniform subpassInput inputAO;
layout (input_attachment_index = 2, set = 2, binding = 0) uniform subpassInput inputLighting;
//
vec3 shadow = subpassLoad(inputShadow).rgb;
float ao = subpassLoad(inputAO).r;
vec3 lighting = subpassLoad(inputLighting).rgb;
vec3 color = shadow * ao * lighting;

然后做一些后处理,目前只实现了ToneMapping,有待实现Bloom等后处理,但可能得更改管线,只需要在Composite Pass中之后继续对Framebuffer中的attachment进行操作即可。

// Tone mapping
color = Uncharted2Tonemap(color * uboParams.exposure);
color = color * (1.0f / Uncharted2Tonemap(vec3(11.2f)));
// Gamma correction
color = pow(color, vec3(1.0f / uboParams.gamma));

outFragColor = vec4(color, 1.0);

总结

预计算结果可以在渲染器完成前就处理完存到一张图中进行采样,而非如此每打开一次生成一次。

管线其实组织起来并不困难,理解到我们所处理的不过也是一张图,只不过这张图是呈现在窗口中的,逐像素单独处理出所需要的数据再按需结合即可。

窗口中的图也是Framebuffer的attachment之一,只不过可以绑定在swapchain中,所以可以呈现在窗口中。

每个Pass获取到的数据都是attachment中的一张图,保存并在所需要时拿出即可。

如果需要后处理,之前处理出的图需要再单独存到attachment,不能直接对swapchain中的图(即窗口图本身)进行操作,因为如果牵涉到临近像素采样时,无法保证各个像素的计算顺序一致。

问题

如果G-Buffer某一点的Depth为1,即该点不是所需要绘制的点,即采样天空盒。而天空盒只需要在一个Pass中采样即可,其他的Pass怎么处理。

SSAO在半球上采样并与G-Buffer中的深度比较,那么如果像素在屏幕边界,采样点在屏幕空间之外,如何处理。

如果想要实现即插即用的各个算法,以及优化Vulkan代码量,怎么实现架构及更好的RHI层封装,例如将目前架构中的三个Pass抽象出接口。


未实现管线相关知识

V-Buffer(Visibility Buffer)

G-Buffer很大,读取和写入所需的带宽会非常的费,而且在Base Pass中仍然需要纹理采样,比如法线贴图等信息,浪费带宽和资源,分开并不彻底。

V-Buffer的缓冲区只记录三角形索引和实例ID,一个至少四字节的信息。

管线主要分为三个阶段:

  • Visibility Passes: 对场景进行光栅化,将Primitive ID和Instance ID(或Material ID)保存到ID Texture里(顺手做个Depth Prepass),也就是说只有可见的Primitive才会进入后续的阶段。
  • Worklist Pass:构建并Worklist,这一步是为了驱动下一步,将屏幕划分成很多tile,根据使用到某个Material ID的tile加到该Material ID的Worklist里,作为下一步的索引。
  • Shading Passes : 使用Compute Shader对每个Material ID进行软光栅,获取顶点属性并插值,然后再进行表面着色。

一般Visibility Buffer通常需要这些信息:

  • Instance ID,表示当前像素属于哪个Instance(16~24 bits)
  • Primitive ID,表示当前像素属于Instance的哪个三角形(8~16 bits)
  • Barycentric Coord,代表当前像素位于三角形内的位置,用重心坐标表示(16 bits)
  • Depth Buffer,代表当前像素的深度(16~24 bits)
  • Material ID,表示当前像素属于哪个材质(8~16 bits)

每种材质会认领屏幕空间中的一块是否需要进行执行Shader,因此每种材质并不会执行一个全屏Pass,而且这样对Cache友好。

优势:

  • 实际Shading了更少的像素
  • 可以Batch,提升Cache效率
  • 可以使用compute shader等提升GPU的利用率

参考资料

F-Mu/MWEngine (github.com)

BoomingTech/Piccolo: Piccolo (formerly Pilot) – mini game engine for games104 (github.com)

SaschaWillems/Vulkan: Examples and demos for the new Vulkan API (github.com)

实时阴影技术(1)Shadow Mapping - KillerAery - 博客园 (cnblogs.com)

基于屏幕空间的实时全局光照(Real-time Global Illumination Based On Screen Space) - KillerAery - 博客园 (cnblogs.com)

[摸着原神学图形]平行光的cascade和软阴影 - 知乎 (zhihu.com)

浅谈IBL(Image-based Lighting) - 知乎 (zhihu.com)

Visibility Buffer学习笔记

《地平线:西部禁域》的Deferred Texture技术

UE_Visibility Buffer & Deferred Material

posted @ 2024-02-01 10:55  幕无  阅读(161)  评论(0编辑  收藏  举报
1 博文导航目录