首先光照分为实时光照和烘焙光照,这里讨论的是对动态物体采用的实时光照。
1.为何提前光照计算在多光源下需要多个drawcall?
因为光照计算主要涉及点光源,聚光灯,平行光,环境光;主要的光照计算是
漫反色光照模型:Cd = max( dot(l,n), 0) * Sdiff + Mdiff.
镜面反色模型:blinn:Cs = pow( max( dot( normalize( l+v ), n), 0), shinnes * 128) * Sdiff + Mspec.
phong:Cs = pow( max( dot( -l + 2*dot(n,l)*n , v), 0), shinnes * 128) * Sdiff + Mspec.
环境光:Ca = Ga + Ma
光照合成:
C = Cd + Cs + Ca
不同类型的光源,只是在衰减上有区别:
点光源和聚光灯有衰减。
平行光和环境光无衰减。
点光源衰减系数:
d < dmin:
i(d) = 1
d > dmin && d < dmax:
i(d) = (dmax - d) / (dmax - dmin);
d > dmax:
i(d) = 0
聚光灯还多出一个辐射衰减半径。
平行光的衰减系数是i= 1
单光源的光照合成:
C = i*(Cd + Cs) + Ca
多光源的合成:
C = Sum( i*(Cd + Cs) ) + Ca
多光源时候:
光照方程中计算的光源颜色,光源入射向量是不一样的,其它都是一样的。
所以在顶点光照中,是可以一个drawcall 计算一个顶点可以进行多个光源光照的计算(因为每个光源可以设置他们的位置类型到shader中),使用相同的顶点法线,使用一份物体的顶点和光源的漫反色,高光,自发光颜色。
但是如果在片段着色器中计算,特点:
1)已经在光栅化后,基于光照计算的点是像素片段点
也可以将像素片段点的法线保留下来,在像素中用顶点插值过来的法线,漫反色颜色,高光系数,自发光颜色。
2)可以使用法线贴图,漫反色贴图,高光系数,自发光贴图
如果多光源时每个光源都是像素光照(为了利用法线贴图,物体表面纹理达到更好的效果),都使用物体法线/漫反色/高光/自发光贴图,那么一个物体的渲染在向前渲染路径下,在片段着色器处,
对每个光源都要遍历一次物体在屏幕坐标系区域的所有像素光照计算,这样是非常消耗片段着色器性能,很耗GPU。
因此很多向前光照算法,采用一个主平行光或一个最亮的主光源作为像素着色,其它光源采用顶点光照计算,像素着色器部分自是输出光照颜色用于和前面物体主颜色blend,因此渲染一个物体就增加了drawcall。
2.延期光照计算是如何解决提前光照计算的多光源下多drawcall问题的,细节过程如何?
支持
Multiple Render Targets(MRT)的图形卡,才支持
延迟着色法渲染。
延迟着色法基于我们
延迟(Defer)
或
推迟(Postpone)
大部分计算量非常大的渲染(像是光照)到后期进行处理的想法。它包含两个处理阶段(Pass):
第一个几何处理阶段(Geometry Pass)中,我们先渲染场景一次,之后获取对象的各种几何信息,并储存在一系列叫做G缓冲(G-buffer)的纹理中,包括位置向量(Position Vector)、颜色向量(Color Vector)、法向量(Normal Vector)和/或镜面值(Specular Value)。场景中这些储存在G缓冲中的几何信息将会在之后用来做(更复杂的)光照计算。
第二个光照处理阶段(Lighting Pass)中使用G缓冲内的纹理数据。在光照处理阶段中,我们渲染一个屏幕大小的方形,并使用G缓冲中的几何数据对每一个片段计算场景的光照;在每个像素中我们都会对G缓冲进行迭代。我们对于渲染过程进行解耦,
将它高级的片段处理挪到后期进行(低级的还是前面第一阶段进行)
,而不是直接将每个对象从顶点着色器带到片段着色器。光照计算过程还是和我们以前一样,但是现在我们需要从对应的G缓冲而不是顶点着色器(和一些uniform变量)那里获取输入变量了。
整个过程在伪代码中会是这样的:
while
(...)
// 游戏循环
{
// 1. 几何处理阶段:渲染所有的几何/颜色数据到G缓冲
glBindFramebuffer(GL_FRAMEBUFFER, gBuffer); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); gBufferShader.Use();
for
(Object obj : Objects) { ConfigureShaderTransformsAndUniforms(); obj.Draw(); }
// 2. 光照处理阶段:使用G缓冲计算场景的光照
glBindFramebuffer(GL_FRAMEBUFFER,
0
); glClear(GL_COLOR_BUFFER_BIT); lightingPassShader.Use(); BindAllGBufferTextures(); SetLightingUniforms(); RenderQuad();}
对于每一个片段我们需要储存的数据有:一个
位置
向量、一个
法
向量,一个
颜色
向量,一个镜面强度值。所以我们在几何处理阶段中需要渲染场景中所有的对象并储存这些数据分量到G缓冲中。我们可以使用
多渲染目标(Multiple Render Targets)
来在
一个drawcall内渲染多个MRT纹理缓冲区,利用相同的深度和模板缓冲区,主要进行顶点shader和简单的片段shader
。
对于几何渲染处理阶段,我们首先需要初始化一个帧缓冲对象,我们很直观的称它为gBuffer,它包含了多个颜色缓冲和一个单独的深度渲染缓冲对象(Depth Renderbuffer Object)。对于位置和法向量的纹理,我们希望使用高精度的纹理(每分量16或32位的浮点数),而对于反照率和镜面值,使用默认的纹理(每分量8位浮点数)就够了。
1)几何处理和简单片段着色器处理阶段:
初始化和绑定MRT缓冲区:
GLuint gBuffer;glGenFramebuffers(
1
, &gBuffer);glBindFramebuffer(GL_FRAMEBUFFER, gBuffer);GLuint gPosition, gNormal, gColorSpec;
// - 位置颜色缓冲
glGenTextures(
1
, &gPosition);glBindTexture(GL_TEXTURE_2D, gPosition);glTexImage2D(GL_TEXTURE_2D,
0
, GL_RGB16F, SCR_WIDTH, SCR_HEIGHT,
0
, GL_RGB, GL_FLOAT, NULL);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, gPosition,
0
// - 法线颜色缓冲
glGenTextures(
1
, &gNormal);glBindTexture(GL_TEXTURE_2D, gNormal);glTexImage2D(GL_TEXTURE_2D,
0
, GL_RGB16F, SCR_WIDTH, SCR_HEIGHT,
0
, GL_RGB, GL_FLOAT, NULL);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, gNormal,
0
);
// - 颜色 + 镜面颜色缓冲
glGenTextures(
1
, &gAlbedoSpec);glBindTexture(GL_TEXTURE_2D, gAlbedoSpec);glTexImage2D(GL_TEXTURE_2D,
0
, GL_RGBA, SCR_WIDTH, SCR_HEIGHT,
0
, GL_RGBA, GL_FLOAT, NULL);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT2, GL_TEXTURE_2D, gAlbedoSpec,
0
);
// - 告诉OpenGL我们将要使用(帧缓冲的)哪种颜色附件来进行渲染
GLuint attachments[
3
] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1, GL_COLOR_ATTACHMENT2 };glDrawBuffers(
3
, attachments);
// 之后同样添加渲染缓冲对象(Render Buffer Object)为深度缓冲(Depth Buffer),并检查完整性
[...]
片段着色器,渲染片段信息到输出目标缓冲区中:
目标缓冲区的绑定在
layout location指定,会采用CPU的glDrawBuffers(
3
, attachments);下标得到该RT, 别名为:
gPosition,gNormal, gAlbedoSpec;
#version 330 core
layout (location =
0
) out vec3 gPosition;layout (location =
1
) out vec3 gNormal;layout (location =
2
) out vec4 gAlbedoSpec;in vec2 TexCoords;in vec3 FragPos;in vec3 Normal;uniform sampler2D texture_diffuse1;uniform sampler2D texture_specular1;
void
main
()
{
// 存储第一个G缓冲纹理中的片段位置向量
gPosition = FragPos;
// 同样存储对每个逐片段法线到G缓冲中
gNormal = normalize(Normal);
// 和漫反射对每个逐片段颜色
gAlbedoSpec.rgb = texture(texture_diffuse1, TexCoords).rgb;
// 存储镜面强度到gAlbedoSpec的alpha分量
gAlbedoSpec.a = texture(texture_specular1, TexCoords).r;}
2)全部物体延后光照处理阶段
现在我们已经有了一大堆的片段数据储存在G缓冲中供我们处置,我们可以选择通过一个像素一个像素地遍历各个G缓冲纹理,并将储存在它们里面的内容作为光照算法的输入,来完全计算场景最终的光照颜色。由于所有的G缓冲纹理都代表的是最终变换的片段值,我们只需要对每一个像素执行一次昂贵的光照运算就行了。这使得延迟光照非常高效,特别是在需要调用大量重型片段着色器的复杂场景中。
对于这个光照处理阶段,我们将会渲染一个2D全屏的方形(有一点像后期处理效果)并且在每个像素上运行一个昂贵的光照片段着色器。
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);shaderLightingPass.Use();glActiveTexture(GL_TEXTURE0);glBindTexture(GL_TEXTURE_2D, gPosition); // 绑定更新后的MTR纹理对象gPosition到GL_TEXTURE0glActiveTexture(GL_TEXTURE1);glBindTexture(GL_TEXTURE_2D, gNormal);glActiveTexture(GL_TEXTURE2);glBindTexture(GL_TEXTURE_2D, gAlbedoSpec);
// 同样发送光照相关的uniform
SendAllLightUniformsToShader(shaderLightingPass);glUniform3fv(glGetUniformLocation(shaderLightingPass.Program,
"viewPos"
),
1
, &camera.Position[
0
]);RenderQuad(); //
CPU中绘制一个全屏的方形
我们在渲染之前绑定了G缓冲中所有相关的纹理,并且发送光照相关的uniform变量到着色器中。
光照处理阶段的片段着色器和我们之前一直在用的光照教程着色器是非常相似的,除了我们添加了一个新的方法,从而使我们能够获取光照的输入变量,当然这些变量我们会从G缓冲中直接采样。
片段着色器处理延后的光照:
#version 330 core
out vec4 FragColor;in vec2 TexCoords;uniform sampler2D gPosition;uniform sampler2D gNormal;uniform sampler2D gAlbedoSpec;
struct
Light { vec3 Position; vec3 Color;};
const
int
NR_LIGHTS =
32
;uniform Light lights[NR_LIGHTS];uniform vec3 viewPos;
void
main
(){
// 从G缓冲中获取数据
vec3 FragPos = texture(gPosition, TexCoords).rgb; vec3 Normal = texture(gNormal, TexCoords).rgb; vec3 Albedo = texture(gAlbedoSpec, TexCoords).rgb;
float
Specular = texture(gAlbedoSpec, TexCoords).a; // 取出高光分量
// 然后和往常一样地计算光照
vec3 lighting = Albedo *
0.1
;
// 硬编码环境光照分量
vec3 viewDir = normalize(viewPos - FragPos); // FragPos本来存放的就是世界坐标系位置,viewPos也是世界坐标系中位置,直接计算光照即可。
for
(
int
i =
0
; i < NR_LIGHTS; ++i)
// 这里也进行了多光源的光照计算,但是原来n个物体,需要n*NR_LIGHTS次片段着色器计算,现在只需要NR_LIGHTS次片段着色计算
{
// 漫反射
vec3 lightDir = normalize(lights[i].Position - FragPos); // 漫反色颜色,如果是点光源或聚光灯,要进行衰减计算。 vec3 diffuse = max(dot(Normal, lightDir),
0.0
) * Albedo * lights[i].Color; lighting += diffuse; } FragColor = vec4(lighting,
1.0
);}
光照处理阶段着色器接受三个uniform纹理,代表G缓冲,它们包含了我们在几何处理阶段储存的所有数据。如果我们现在再使用当前片段的纹理坐标采样这些数据,我们将会获得和之前完全一样的片段值,这就像我们在直接渲染几何体。在片段着色器的一开始,我们通过一个简单的纹理查找从G缓冲纹理中获取了光照相关的变量。注意我们从
gAlbedoSpec
纹理中同时获取了
Albedo
颜色和
Spqcular
强度。
因为我们现在已经有了必要的逐片段变量(和相关的uniform变量)来计算布林-冯氏光照(Blinn-Phong Lighting),我们不需要对光照代码做任何修改了。我们在延迟着色法中唯一需要改的就是获取光照输入变量的方法。
3.延期光照计算为什么不支持抗锯齿,半透明物体渲染?
On the downside, deferred shading has no real support for anti-aliasing and can’t handle semi-transparent GameObjects (these are rendered using
forward
rendering). There is also no support for the Mesh Renderer’s Receive Shadows flag and culling masks are only supported in a limited way. You can only use up to four culling masks. That is, your culling layer mask must at least contain all layers minus four arbitrary layers, so 28 of the 32 layers must be set. Otherwise you get graphical artefacts.
延迟着色法的其中一个缺点就是它不能进行
混合
(Blending),因为G缓冲中所有的数据都是从一个单独的片段中来的,而混合需要对多个片段的组合进行操作。
延迟着色法另外一个缺点就是它迫使你对大部分场景的光照使用相同的光照算法,你可以通过包含更多关于材质的数据到G缓冲中来减轻这一缺点。
锯齿产生的原因:
因为原来非抗锯齿是一个像素点的中心被三角形覆盖则选用该像素,又得被选中,又得没有被选中就会导致锯齿效果。
Opengl内置的
MSAA
多重采样抗锯齿原理:抗锯齿是基于几何顶点数据的,在光栅化时候用硬件对每个像素分割为多个采样点(采样点被几何图元覆盖则为1,没有覆盖采样点为0),像素着色器还是对每个像素进行着色器,但是在像素内部的采样点是否被几何覆盖,全部覆盖就是1个像素的颜色,有一半本覆盖本像素点就是0.5的颜色,这样边缘的颜色就会比较淡了(类似引入alpha透明融合的方式降低了亮度), 这样可以做到抗锯齿。
延后光照,因为最终颜色是Lightting阶段片段着色器中着色的,如果之前渲染物体时候就采用了多重采样抗锯齿,后面光照阶段片段着色也可以基于多重采样纹理进行,对锯齿边缘的颜色进行模糊淡化。
理论上,可以支持。
GLSL中有语法,可以基于MRT中输出的多重采样纹理进行片段着色器着色,使用
sampler2DMS
关键字即可。
uniform sampler2DMS screenTextureMS;
使用texelFetch函数,就可以获取每个样本的颜色值了:
vec4 colorSample = texelFetch(screenTextureMS, TexCoords,
3
);
// 4th subsample
但是应该是延迟光照在后期,之前基于每个物体图元的多重采样点像素缓存数据无法存储转换到延后光照渲染时还保留,所以在延后光照基于像素着色,但是没有多重采样点权重数据,不能进行整体的多重采样插值,所以暂时不支持MSAA.
故unity最新版也暂时不支持
Deferred Lighting
抗锯齿,半透明混合,及网格阴影。
4.图形渲染器,可以同时启用向前光照和延后光照,提供好的解决方案
为了克服向前光照渲染中的大量片段着色器运算或一个片段着色器光照多个顶点光照多drawcall的情况,对于大量需要像素光照物体还是可以使用延后光照的。
但是对于延后光照不能解决的抗锯齿和半透明物体光照后正确渲染的问题,对特殊物体还是需要向前光照渲染。
为了克服这些缺点(特别是混合),我们通常分割我们的渲染器为两个部分:
一个是延迟渲染的部分,另一个是专门为了混合或者其他不适合延迟渲染管线的着色器效果而设计的的正向渲染的部分。
为了展示这是如何工作的,我们将会使用正向渲染器渲染光源为一个小立方体,因为光照立方体会需要一个特殊的着色器(会输出一个光照颜色)。
先复制延后光照FBO中的深度缓冲区,模板缓冲区到系统默认的深度和模板缓冲区(颜色缓冲区可以不用复制,因为就是现在的效果)。
CPU中的代码,如下:
// 延迟渲染光照渲染阶段
[...]RenderQuad();
// 复制深度和模板缓冲区到默认缓冲区,以用向前渲染得到正确的之前图像的深度和模板值
glBindFramebuffer(GL_READ_FRAMEBUFFER, gBuffer);glBindFramebuffer(GL_DRAW_FRAMEBUFFER,
0
);
// 写入到默认帧缓冲
glBlitFramebuffer(
0
,
0
, SCR_WIDTH, SCR_HEIGHT,
0
,
0
, SCR_WIDTH, SCR_HEIGHT, GL_DEPTH_BUFFER_BIT, GL_NEAREST);glBindFramebuffer(GL_FRAMEBUFFER,
0
);
// 现在像正常情况一样正向渲染所有光立方体
shaderLightBox.Use();glUniformMatrix4fv(locProjection,
1
, GL_FALSE, glm::value_ptr(projection));glUniformMatrix4fv(locView,
1
, GL_FALSE, glm::value_ptr(view));
for
(GLuint i =
0
; i < lightPositions.size(); i++){ model = glm::mat4(); model = glm::translate(model, lightPositions[i]); model = glm::scale(model, glm::vec3(
0.25f
)); glUniformMatrix4fv(locModel,
1
, GL_FALSE, glm::value_ptr(model)); glUniform3fv(locLightcolor,
1
, &lightColors[i][
0
]); RenderCube();}
仅仅是延迟着色法它本身(没有光体积)已经是一个很大的优化了,每个像素仅仅运行一个单独的片段着色器,然而对于正向渲染,我们通常会对一个像素运行多次片段着色器。当然,延迟渲染确实带来一些缺点:大内存开销,没有MSAA和混合(仍需要正向渲染的配合)。
当你有一个很小的场景并且没有很多的光源时候,延迟渲染并不一定会更快一点,甚至有些时候由于开销超过了它的优点还会更慢。然而在一个更复杂的场景中,延迟渲染会快速变成一个重要的优化,特别是有了更先进的优化拓展的时候。
最后我仍然想指出,基本上所有能通过正向渲染完成的效果能够同样在延迟渲染场景中实现,这通常需要一些小的翻译步骤。举个例子,如果我们想要在延迟渲染器中使用法线贴图(Normal Mapping),我们需要改变几何渲染阶段着色器来输出一个世界空间法线(World-space Normal),它从法线贴图中提取出来(使用一个TBN矩阵)而不是表面法线,光照渲染阶段中的光照运算一点都不需要变。如果你想要让视差贴图工作,首先你需要在采样一个物体的漫反射,镜面,和法线纹理之前首先置换几何渲染阶段中的纹理坐标。一旦你了解了延迟渲染背后的理念,变得有创造力并不是什么难事。
使用光体积,对于没有光照找到的空间没有必要进行光照计算因此,引入光体积,光体积一般是以光源为中心的球体或锥体(对于点光源和聚光灯才有衰减):
struct
Light { [...]
float
Radius;};
void
main
()
{ [...]
for
(
int
i =
0
; i < NR_LIGHTS; ++i) {
// 计算光源和该片段间距离
float
distance = length(lights[i].Position - FragPos);
if
(distance < lights[i].Radius) //
这里GPU不能工作,因为会被GPU并行化优化掉。
{
// 执行大开销光照
[...] } } }
渲染光体积更好的方法是借用一个球体模型,对球体进行绘制,用模板缓存进行过滤,在球体区域的才进行光照计算,否则不进行。
结合延后光照,对所有物体进行一次光照计算,原来n*NR_LIGHTS次片段着色器计算或顶点光照计算,就变成了NR_LIGHTS次片段着色器光照计算。
在结合光体积去掉没有必要的像素光照计算会得到更好的光照性能,但是光体积并
不是最好的优化。另外两个基于延迟渲染的更流行(并且更高效)的拓展叫做
延迟光照(Deferred Lighting)
和
切片式延迟着色法(Tile-based Deferred Shading)
。这些方法会很大程度上提高大量光源渲染的效率,并且也能允许一个相对高效的多重采样抗锯齿(MSAA)。
ref:
http://learnopengl-cn.readthedocs.io/zh/latest/05%20Advanced%20Lighting/08%20Deferred%20Shading/
http://blog.csdn.net/kongfuxionghao/article/details/17656529
http://learnopengl-cn.readthedocs.io/zh/latest/04%20Advanced%20OpenGL/11%20Anti%20Aliasing/