延迟渲染

前向渲染(Forward Rendering)

它是我们渲染物体的一种非常直接的方式,在场景中我们根据所有光源照亮一个物体,之后再渲染下一个物体,以此类推。

前向渲染伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//伪代码
Pass
{
for(each primitive in model)
{
//所有被这个三角形包围的片元
for(each fragment covered by this primitive)
{
if(failed in depth test)
{
//如果深度测试失败,说明该片元不可见,不应该被着色
discard;
}
else{
            //对每一个片元计算着色
               float4 color = Shading(materialInfo,pos,normal,lightDir,viewDir);
               //写入帧缓存
               writeFrameBuffer(fragment,color);
}
}
}
}

总的来说在渲染的时候,会遍历每一个物体的每一个三角面来一次的进行光照计算。计算量十分的庞大!大部分片段着色器的输出都会被之后的输出覆盖(深度测试),前向渲染还会在场景中因为高深的复杂度(多个物体重合在一个像素上)浪费大量的片段着色器运行时间。

延迟渲染(Deferred Rendering)

为了解决上述问题而诞生了,它大幅度地改变了我们渲染物体的方式。这给我们优化拥有大量光源的场景提供了很多的选择,因为它能够在渲染上百甚至上千光源的同时还能够保持能让人接受的帧率。

下面这张图片包含了一共1874个点光源,它是使用延迟着色法来完成的,而这对于正向渲染几乎是不可能的

图片来源:Hannes Nevalainen

延迟着色法基于我们延迟(Defer)或推迟(Postpone)大部分计算量非常大的渲染(像是光照)到后期进行处理的想法。它包含两个处理阶段(Pass):在第几何处理阶段(Geometry Pass)中,我们先渲染场景一次,之后获取对象的各种几何信息,并储存在一系列叫做G缓冲(G-buffer)的纹理中;想想位置向量(Position Vector)、颜色向量(Color Vector)、法向量(Normal Vector)和/或镜面值(Specular Value)。场景中这些储存在G缓冲中的几何信息将会在之后用来做(更复杂的)光照计算。下面是一帧中G缓冲的内容:

我们会在第二个光照处理阶段(Lighting Pass)中使用G缓冲内的纹理数据。在光照处理阶段中,我们渲染一个屏幕大小的方形,并使用G缓冲中的几何数据对每一个片段计算场景的光照;在每个像素中我们都会对G缓冲进行迭代。我们对于渲染过程进行解耦,将它高级的片段处理挪到后期进行,而不是直接将每个对象从顶点着色器带到片段着色器。光照计算过程还是和我们以前一样,但是现在我们需要从对应的G缓冲而不是顶点着色器(和一些uniform变量)那里获取输入变量了。

下面这幅图片很好地展示了延迟着色法的整个过程:

延迟渲染伪代码:

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
34
35
36
37
38
39
//伪代码
Pass1
{
//第一个Pass不进行光照,只根据深度把能写入G-Buffer的片元挑选出来
for(each primitive in model)
{
//所有被这个三角形包围的片元
for(each fragment covered by this primitive)
{
if(failed in depth test)
{
//如果深度测试失败,说明该片元不可见,不应该被写入G-Buffer
discard;
}
else{
//如果可见就就要把相关信息存储到G-Buffer中
WriteGBuffer(materialInfo,pos,normal,lightDir,viewDir);
}
}
}
}

Pass2
{
//第二个Pass会根据G-Buffer中的片元信息进行光照
for(each pixel in screen)
{
if(the pixel is valid)
{
//如果该像素是有效的
//读取它对应的G缓冲中的信息
readGBuffer(pixel,materialInfo,pos,normal,lightDir,viewDir);

//根据读取的信息进行光照
float4 color = Shading(materialInfo,pos,normal,lightDir,viewDir);
WriteFrameBuffer(pixel,color);
}
}
}

第一个Pass中不进行任何光照,只根据深度信息把符合条件的片元信息存储到G-Buffer中,这样剔除了那些不会被画在屏幕上的片元,这样保证了对于在光照处理阶段中处理的每一个像素都只处理一次,所以我们能够省下很多无用的渲染调用。第二个Pass,会根据G-Buffer中的各个片元的信息,如法线,视角方向,漫反射系数等进行光照计算。

在几何处理阶段中填充G缓冲非常高效,因为我们直接储存像素位置,颜色或者是法线等对象信息到帧缓冲中,而这几乎不会消耗处理时间。在此基础上使用多渲染目标(Multiple Render Targets, MRT)技术,我们甚至可以在一个渲染处理之内完成这所有的工作。

G缓冲

G缓冲(G-buffer)是对所有用来储存光照相关的数据,并在最后光照处理阶段中使用的所有纹理的总称。

对于每一个片段我们需要储存的数据有:一个位置向量、一个法向量,一个颜色向量,一个镜面强度值。所以我们在几何处理阶段中需要渲染场景中所有的对象并储存这些数据分量到G缓冲中。

unity中的延迟渲染

逐像素光源&逐顶点光源

在片元着色器中计算光照的是逐像素光照(per-pixel lighting);在顶点着色器中计算光照的是逐顶点光照(per-vertex lighting)。

在逐像素光照中,会以每个像素为基础,得到它的法线(由顶点差值得到或者从法线贴图中得到),然后进行光照模型的计算。这种插值顶点法线从而计算每个像素颜色的技术称之为Phong shading(注意这个不是Blinn-Phong光照模型,而是一种shading frequency)。

逐顶点光照,也被称为高洛德着色(Gouraud shaing,也是一种shading frequency)。逐顶点光照在每个顶点上计算光照,之后每个像素的颜色根据顶点的颜色插值得到。

逐像素光照的计算量大效果好,逐顶点光照计算量较小效果较差。

在unity中使用延迟渲染,不会限制你灯光的个数并且所有的灯光都是逐像素光源的效果

MRT(Multiple Render Targets)

要想使用延迟渲染,那么硬件必须支持MRT。

多重渲染目标(MRT)允许程序同时渲染到多个颜色缓冲,向不同的颜色缓冲中送入渲染结果(如不同RGBA色彩通道的值、深度值等)。不少高级特效渲染时需要使用多重渲染目标技术,例如延迟着色、屏幕空间环境光遮蔽等。

延迟渲染的缺点

延迟渲染缺点:

  • 1)不支持真正的抗锯齿功能。
  • 2)不能处理半透明物体,因此假如你要使用透明度混合(blend)那么请使用前向渲染;前向渲染可以和延迟渲染同时使用,在一个subshader块里面,你可以既有前向渲染的pass也可以有延迟渲染的pass。
  • 3)对显卡有一定要求。如果要使用延迟渲染的话,显卡必须支持MRT、Shader Mode 3.0及以上(即OpenGL,DX的版本不能低于某个版本,具体的版本对应情况见手册)、深度渲染纹理以及双面的模板缓冲。

unity延迟渲染例子

我们知道在OpenGL中实现延迟渲染技术是利用了帧缓冲来实现的,把纹理绑定到一个帧缓冲中,然后先渲染一遍相关的深度,法线等等的数据都写到相关的纹理上,之后在做后面的渲染的时候我们直接使用这些数据即可。

在unity中我们使用延迟渲染的步骤也大致类似:

  • 编写第一个shader,渲染第一遍把对应的数据保存到4张图里面去
  • 编写第二个shader,拿到这些数据计算光照,把相应的图片数据还原到世界坐标下进行光照计算,值得注意的是如果你并没有特殊的需求其实编写第一个shader已经足够了,第二个shader在unity中已经自动的帮你写好了,并且还是基于PBR的,效果很不错!如果你自己写一个类似的效果工作量是比较大的!

下面给出第一个shader的列子代码,供日后做参考:

Unlit Shader
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
Shader "Unlit/002"
{
Properties
{
_MainTex("Texture",2D) = "white" {}
_Diffuse("Diffuse",Color) = (1,1,1,1)
_Specular("Specular", Color) = (1,1,1,1)
_Gloss("Gloss", Range(8.0, 50)) = 20
}

SubShader
{
Tags { "RenderType" = "Opaque" }
LOD 100

Pass
{
Tags{"LightMode" = "Deferred"}

CGPROGRAM
#pragma target 3.0
#pragma vertex vert
#pragma fragment frag
#pragma exclude_renderers norm
#pragma multi_compile __ UNITY_HDR_ON

#include "UnityCG.cginc"

sampler2D _MainTex;
float4 _MainTex_ST;
fixed4 _Diffuse;
fixed4 _Specular;
float _Gloss;

struct appdata
{
float4 vertex :POSITION;
float2 uv : TEXCOORD0;
float3 normal:NORMAL;
};

struct v2f
{
float2 uv: TEXCOORD0;
float4 vertex : SV_POSITION;
float3 worldNormal : TEXCOORD1;
float3 worldPos : TEXCOORD2;
};

//输出到GBuffer的结构体
struct DeferredOutput
{
//4个输出
float4 gBuffer0 : SV_TARGET0;
float4 gBuffer1 : SV_TARGET1;
float4 gBuffer2 : SV_TARGET2;
float4 gBuffer3 : SV_TARGET3;
};
//顶点着色器
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
//将模型顶点的uv和Tiling、Offset两个变量进行运算,计算出实际显示用的定点uv
o.uv = TRANSFORM_TEX(v.uv,_MainTex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
return o;
}


//片段着色器,这里是延迟渲染不再渲染到默认的帧缓冲了
DeferredOutput frag(v2f i)
{
DeferredOutput o;
//对_MainTex进行采样;获取到漫反射的颜色值
fixed3 color = tex2D(_MainTex, i.uv).rgb * _Diffuse.rgb;
o.gBuffer0.rgb = color;
//不管遮罩,直接设置为1
o.gBuffer0.a = 1;
//高光
o.gBuffer1.rgb = _Specular.rgb;
//高光系数,归一化
o.gBuffer1.a = _Gloss/50.0;
//归一化到[0,1]
o.gBuffer2 = float4(i.worldNormal * 0.5 + 0.5,1);
#if !defined(UNITY_HDR_ON)
color.rgb = exp2(-color.rgb);
#endif

//存储自发光+lightmap+反射探针深度缓冲和模板缓冲
o.gBuffer3 = fixed4(color,1);
return o;
}

ENDCG
}
}
}

评论