渲染管线流程

First Post:

Last Update:

Word Count:
2.7k

Read Time:
10 min

渲染管线流程

在游戏引擎中,我们会将一些美术素材,无论是2D的还是3D的,显示在屏幕上,这一流程我们称之为渲染管线。渲染管线分为几个步骤:应用阶段-几何阶段-光栅化阶段-像素处理-合并阶段

渲染管线

应用阶段

应用阶段的主要任务是把 顶点数据、shader、贴图、材质球、灯光以及一些设置等等传入GPU的过程,这个过程叫做 DrawCall

Unity DrawCall 内部分为 SetPassCall 和 Batch,SetPassCall的作用是设置渲染管线的上下文,一般每一种Unity里的Material就是一个SetPassCall,

Batch就是每一次CPU向GPU打包发送顶点数据的批次,当我们优化性能的时候经常会用到(动态)合批,合批就是将拥有两个相同的材质(材质实例也要相同)的物体同时传入GPU进行处理。

合批处理在UGUI中会有一些不同
先获得一个按Hierarchy的顺序的列表
计算每个物体的深度。
2.1 深度从0开始递增。如果世界包围盒Z轴不为0(或isCanvasInjectionIndex),则需独占一个批次,同时独占一个深度。即等于之前所有物体最大深度+1,后一个物体深度需要+1。
2.2 对一般物体的深度。会判断是否可跟之前的物件共享深度,走接下来的流程。
2.2.1 先按格子(默认大小是120,根据包围盒再计算)划分出多个格子。(只是为了加速求交)。
2.2.2 计算物体包围相交哪些格子,再跟格子中已有的物体进行包围盒相交判断。如果不相交则使用当前深度;如果相交且可合批,则使用相交物体中最大的深度;如果相交且不可合批,则使用相交物体中最大的深度+1。合批条件:无独占批次+材质相同+贴图相同+裁剪开关和裁剪矩形相同+贴图A8格式一致(kTexFormatAlpha8) 2.2.3 将该物体加入所有相交的格子中。若遇到独占深度的物体,则格子数据清空。即后续物件不跟之前的物件共享深度。
排序:按照深度->材质->贴图->层级顺序优先级排序。
合批:对排序后的列表,从头开始一个一个检测是否能与前面的物体合批。合批条件:无独占批次(只判断isCanvasInjectionIndex)+材质相同+贴图相同+裁剪开关和裁剪矩形相同+贴图A8格式一致(kTexFormatAlpha8),非SubBatch只判断前两个条件,一般情况下UI的材质都一样。


UGUI批处理原文链接:https://blog.csdn.net/hankangwen/article/details/122667647

在Unity中的shader中会将这些数据存到一些通道里: POSITION、NORMAL、COLOR、 TANGENT、TEXCOOR

1
2
3
4
5
6
7
8
struct a2v //application to vertex shader
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 color : COLOR;
float4 tangent : TANGENT;
float4 texcoord : TEXCOORD0;
}

几何阶段

顶点着色器

MVP变换

Model - View - Projection 对应 Model Space→World Space→ View Space

Model 期间对应的是一个Model矩阵,Model矩阵的作用是实现模型的Transform仿射变换,包括了Translation,Rotation,Scaling(变换的顺序不能变)

矩阵的乘法是右结合的,以TRS的顺序求结果那么逻辑上就是先缩放再旋转最后平移,因为一旦先进行了平移再缩放就会将平移的距离一并缩放,得到错误结果,旋转同理,先平移再旋转得到的就不是绕原点的旋转结果了。在这个TRS过程中,R和S都是线性变换,但是T不是,意味着T是一种特殊的变换,为了化简这种特殊性,我们引入齐次坐标,将二维的点用三个坐标表示,三维的点用四个坐标表示(下式是一个二维的齐次坐标平移变换过程,详细解释可以参考Games101) [x′y′w′]=[10tx01ty001][xy1]=[x+txy+ty1]

最后对应的Model Matrix

Model=T×R× S=[100tx010ty001tz0001][10000cosθ−sinθ00sinθcosθ00001][cosθ0sinθ00100−sinθ0cosθ00001][cosθ−sinθ00sinθcosθ0000100001][x0000y0000z00001]

View变换是将世界空间中的坐标变换到摄像机空间,至于为什么会存在一个摄像机空间,是因为与其变换摄像头位置的操作,直接变换世界空间的坐标收益更大,所以我们优先将世界空间的坐标对齐到以摄像机为原点构建的坐标系上

View变换的原理和Mode矩阵差不多,也是旋转加平移,由于是一个逆变换,所以这里采用的是先平移再旋转,矩阵的表示就为 R×T

将一个物体旋转到指定的基向量上也许并不好算,但是将基向量旋转到指定的位置就变得很简单,我们通过写出这个旋转矩阵的逆矩阵,再对这个正交的逆矩阵求逆(正交矩阵的特性:正交矩阵的逆就是原矩阵关于主对角线对称)

Rview−1=[xg^×t^xtx−g0yg^×t^yty−g0zg^×t^ztz−g00001]

Rview=[xg^×t^yg^×t^zg^×t^0xtytzt0x−gy−gz−g00001]

Projection的过程就有些复杂了

先从简单的 正交投影(Orthographic projection)讲,正交投影的过程很简单,讲模型平移到原点为中心,然后挤压入一个正则立方体中( (−1,1)3NDC 空间) Mortho=[2r−l00002l−b00002n−f00001][100−r+l2010−l+b2001−n+f20001]

由于-1到1的距离是2,所以缩放过后的物体长宽高也为2,只需要将 $物体的坐标 / 物体的长宽高 * 2$ 就能得到缩放矩阵,平移很简单,移到中心即可

img

再来看 透视投影(Perspective projection)

透视投影这里直接采用将正交矩阵挤压成投影矩阵

Mpersp→ortho=[n0000n0000n+f−nf0010]

所以最后投影矩阵的结果为 Mpersp=Mperp→ortho×Mortho

曲面细分着色器

(可选着色器)实现LOD

图元装配

View Space→Clip Space→Screen Space

图元装配会将顶点装配成指定的图形,与此同时,会进行裁剪、背面剔除等操作,以减少不必要的计算,加速渲染过程。(图元可以理解为三角形)

裁剪

进行完MVP变换后,就要针对屏幕大小进行裁剪,还有背面剔除等操作

屏幕映射

将NDC空间内的顶点映射到屏幕空间

光栅化阶段

此过程将组成的三角形映射到片元上,每个片元的信息都是由三个顶点的信息插值得到

此过程也包括纹理映射和实现光照模型

合并阶段(Blend)

合并处理阶段属于屏幕后期处理范围,一般包括Alpha测试,深度测试,模板测试和混合,通过这个阶段可以决定每个片元的可见性

ALPHA测试

通过片元数据,可以获取该片元的alpha值,如果alpha值小于某个数的话,则直接将该片元丢弃,不进行渲染,这是非常“粗暴”的(即只渲染透明度在某一范围内的片元),可以用来做一些树叶镂空的效果。

深度测试

近处的物体会遮挡远处的物体,这种效果我们可以通过深度测试来模拟实现。它通过将深度缓存中的值和当前片元的深度进行比较,计算是否需要更新深度缓存和颜色缓存,如果不需要则将该片元丢弃,这与模板测试比较类似。我们在渲染半透明物体时, 需要开启深度测试而关闭深度写入功能。

模板测试

模板测试默认是不开启的,如果开启了模板测试,GPU会首先读取模板缓冲区中该片元位置的模板值,然后将该值和读取到的参考值进行比较,这个比较函数可以是由开发者指定的,例如小于时舍弃该片元,或者大于等于时舍弃该片元。如果这个片元没有通过这个测试,该片元就会被舍弃

混合阶段

对于不透明的物体,可以直接关闭混合操作,片元着色器的值会直接覆盖颜色缓冲区的像素值,对于半透明的物体,会采用混合计算得出新的像素值

透明渲染:
这种直接读取深度值,从后往前blend的透明渲染模式称为over操作,一般的渲染引擎都是这么处理透明物体的混合的
除此之外还有一些其他的透明渲染模式,如(OIT,order-independent transparency),OIT主要解决当两个透明的物体有重叠部分时,由于原本是基于物体深度的排序,所以交叠部分会出现渲染错误
一种常见的OIT方式为depth peeling

首先先将所有的不透明物体和透明物体的深度写入到第一个z-buffer;

渲染所有的不透明物体, 将得到的深度值和第一个z-buffer中的值比较, 如果两个值相等, 我们就认为这个表面是距离我们最近的透明面, 这时将得到的RGBA值存入到color buffer中. 同时我们在第二个z-buffer中写入距离我们第二近的深度值, 实现peeling的效果.

将第二个z-buffer作为输入, 第一个z-buffer作为输出, 得到的RGBA颜色用under的方式和之前的颜色混合.

重复前面的步骤, 渲染所有的透明物体后, 将最终的颜色和不透明表面的颜色混合.

(原帖:https://zhuanlan.zhihu.com/p/149982810)
此外还有一些简单高效的渲染模式,如 Screen-Door Transparency,附上shader

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
Shader "Ocias/Diffuse (Stipple Transparency)" {
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {}
_Transparency ("Transparency", Range(0,1)) = 1.0 }
SubShader {
Tags { "RenderType"="Opaque" }
LOD 150
CGPROGRAM
pragma surface surf Lambert noforwardadd
sampler2D _MainTex;
struct Input {
float2 uv_MainTex;
float4 screenPos;
};
half _Transparency;
void surf (Input IN, inout SurfaceOutput o) {
fixed4 c = tex2D(_MainTex, IN.uv_MainTex);
o.Albedo = c.rgb;
o.Alpha = c.a;
// Screen-door transparency: Discard pixel if below threshold.
float4x4 thresholdMatrix = { 1.0 / 17.0, 2.0 / 17.0, 3.0 / 17.0, 4.0 / 17.0,
8.0 / 17.0, 7.0 / 15.0, 6.0 / 17.0, 5.0 / 17.0,
9.0 / 17.0, 10.0 / 15.0, 11.0 / 17.0, 12.0 / 17.0,
16.0 / 17.0, 15.0 / 13.0, 14.0 / 17.0, 13.0 / 17.0 };
float4x4 _RowAccess = { 1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1 };
float2 pos = IN.screenPos.xy / IN.screenPos.w; pos *= _ScreenParams.xy;
// pixel position
clip(_Transparency - thresholdMatrix[fmod(pos.x, 4)] * _RowAccess[fmod(pos.y, 4)]);
}
ENDCG
}
Fallback "Mobile/VertexLit"
}
打赏点小钱
支付宝 | Alipay
微信 | WeChat