这两天在整理LearnOpenGL教程中延迟着色部分的内容,在3月份看Unity Shader入门精要这本书,涉及到这个内容,当时仅仅是一扫而过,没有注意,这两天的学习让自己对前向渲染和延迟渲染有了一个直观的认识.
教程中提及了光体积(Light Volumn)的概念和使用技巧,但是并未给出示例代码,自己在查阅资料整理出了该部分的代码,作为记录,同时希望可以帮助在这里卡壳的童鞋.
参考的链接:
1.LearnOpenGL教程
该篇博客的结构如下:
思路
渲染一个实际的球体,并根据该光源的半径缩放.该球体的中心凡在光源的位置,由于它是根据光体积半径缩放的,这个球体正好覆盖了光的可视体积.我们使用大体相同的延迟片段着色器来渲染球体,因为球体产生了完全匹配于受影响像素的着色器调用,我们只渲染了受影响的像素而跳过其它的像素.
相比于教程中使用伪光体积(其所提及的方法由于GPU和GLSL在优化循环和分支上的不足,实际上和没有光体积的效率一样)的代码,光照渲染阶段的片段着色器的代码没有较大改动,仅仅是删除了分支和计算距离的语句
#version 330 core
in VS_OUT
{
vec2 texcoord;
}fs_in;
uniform sampler2D gPosition;
uniform sampler2D gNormal;
uniform sampler2D gAlbedoSpec;
struct Light
{
vec3 position;
vec3 color;
float radius;
float linear;
float quadratic;
};
const int NR_LIGHTS = 32;
uniform Light lights[NR_LIGHTS];
uniform vec3 viewPos;
out vec4 color;
void main()
{
//从G缓冲中获取数据
vec3 position = texture2D(gPosition, fs_in.texcoord).rgb;
vec3 normal = texture2D(gNormal, fs_in.texcoord).rgb;
vec3 albedo = texture2D(gAlbedoSpec, fs_in.texcoord).rgb;
//和往常一样进行光照计算
vec3 lighting = vec3(0.0f);
vec3 viewDir = normalize(viewPos - position);
for(int i = 0; i < NR_LIGHTS; ++i)
{
float distance = length(lights[i].position - position);
float attenuation = 1.0f / (1.0f + lights[i].linear * distance +
lights[i].quadratic * distance * distance);
lighting += attenuation * albedo * vec3(0.1) * lights[i].color; //环境光
//漫反射
vec3 lightDir = normalize(lights[i].position - position);
float diff = max(dot(lightDir, normal), 0.0f);
vec3 diffuse = attenuation * diff * albedo * lights[i].color;
lighting += diffuse;
//镜面高光
vec3 halfwayDir = normalize(viewDir + lightDir);
float spec = pow(max(dot(halfwayDir, normal), 0.0f), 32.0f);
vec3 specular = attenuation * spec * lights[i].color;
lighting += specular;
}
color = vec4(vec3(lighting), 1.0f);
}
光照渲染阶段的顶点着色器有一点改动,
#version 330 core
layout (location = 0) in vec3 position;
layout (location = 1) in vec2 texcoord;
layout (std140) uniform Camera
{
mat4 view;
mat4 projection;
};
uniform mat4 model;
out VS_OUT
{
vec2 texcoord;
}vs_out;
void main()
{
vec4 position = projection * view * model * vec4(position, 1.0f);
gl_Position = position;
vs_out.texcoord = vec2(position.x / position.w, position.y / position.w) * 0.5 + 0.5;
}
如上代码,相比于教程中的顶点着色器,它将顶点的纹理坐标直接传给片段着色器,由于我们要限定渲染的区域为球体所包含的区域,所以这个区域必定是和球体的顶点坐标相关的,于是我们将经过MVP变换后的顶点坐标position.x,position.y除以position.w得到标准化的顶点坐标,其坐标范围是在[-1, 1]区间,并以此坐标来对几何处理后获取的位置,颜色,法向量等纹理进行采样.需要注意的是,纹理的坐标是[0,1]区间,所以需要” * 0.5 + 0.5”来将坐标从[-1, 1]变换到[0, 1].
由于各个光源的光体积可能有重合的地方,对于重合的部分,如果只是简单的采用后渲染的光源的数据,可能会出现错误,毕竟各个光源的属性可能是不同的,以上粗暴的方式明显不理想.
这里,我使用融合来解决这个问题,在绘制光体积的时候,开启融合,且配置融合的参数如下:
glBlendFuncSeparate(GL_ONE, GL_ONE, GL_ONE, GL_ONE);
glBlendEquation(GL_MAX);
这样,对于重叠的区域,渲染管线会选择更亮的光照结果作为渲染结果,这是符合我们的期望的.
该部分的细代码可见文末的全部代码的链接.
问题
教程中提到,”该方法需要开启面剔除(不然会渲染一个光效果两次),但开启后,用户可能进入一个光源的光体积,然后这样之后这个体积就不再被渲染(由于背面剔除),这会使光源的影响消失”,后半句所说的光源的影响会消失,我没有想明白,运行的结果好像也不存在这个问题,还望有大大赐教.
2017.6.4更新
昨日和今日参加了网易游戏的线下探营活动,这次活动干货满满,更加想加入网易游戏了~
废话说完了~
针对之前教程中提到的”该方法需要开启面剔除(不然会渲染一个光效果两次),但开启后,用户可能进入一个光源的光体积,然后这样之后这个体积就不再被渲染(由于背面剔除),这会使光源的影响消失”,后半句本来是无法理解的,但在探营活动中,和一位其它实验室的同学交流后,弄明白了后半句的意思,如下图,
上图是摄像机在球体外时的一个侧面图,此时黑色半圆为front面,红色半圆为back面,如果不开启面剔除,那么两个半圆均需渲染,这样的结果是,对于两个半圆投影在摄像机视角平面P会被渲染两次(即黑色半圆投影的片段(P平面)会被渲染一次,红色半圆投影的片段(P平面)也会被渲染一次)造成P平面上的每个片段会被执行两次片段着色器;开启面剔除后,红色半圆会被剔除,只有黑色半圆执行投影的片段会执行片段着色器代码,这在第一个图中是正确的
但如果摄像机的位置进入球体内,那么由于渲染管线会将摄像机视角外的物体执行裁剪操作,结果是,黑色半圆此时不可见,而红色半圆由于面剔除(可以看看面剔除的原理)也不会被渲染,结果是,P平面本身在摄像机位置是可见的,但是由于进入球体后,黑红半圆都不会被渲染了,P平面也不会被渲染,由此导致错误!
在实际的代码中,将摄像机位置放置在球体内后,上文中P平面的内容确实没有再渲染,证明了上述的猜测!
之前没有理解这句话的原因,还是对渲染管线中的一些细节知识掌握不足.
效果
渲染的区域(光源的光体积):
思路
glEnable(GL_STENCIL_TEST); //开启模板测试
glDisable(GL_DEPTH_TEST); //关闭深度测试
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE); //配置模板测试
glStencilFunc(GL_ALWAYS, 1, 0xFF);
glStencilMask(0xFF); //设置模板缓冲为可写状态
RenderLightSphere //渲染光源对应的球体
glEnable(GL_DEPTH_TEST); //恢复深度测试
glStencilFunc(GL_EQUAL, 1, 0xFF); //配置模板测试,此时仅有先前球体绘制的区域才能通过模板测试
glStencilMask(0x00); //禁止修改模板缓冲
渲染几何阶段获取的纹理至一个平面上(和教程中的绘制代码相同)
glStencilMask(0xFF); //恢复模板缓冲可写,如果没有这行,运行结果会和没有清空缓冲区类似,不明白其中原因
基本上,就是模板技术的简单应用,详细代码可见文末的全部代码的链接.
思考
我们知道,模板测试是在片段着色器执行完毕后进行的,也就是说,其实即使是光源照不到的片段,也进行了费时的光照计算,这明显不是光体积的目的.
我不知道,是不是自己对教程中提到的模板测试的方法的理解有问题,还望指出,在此非常感谢.
1.帧缓冲由颜色缓冲,深度缓冲和模板缓冲构成,在未涉及自定义的帧缓冲时,窗口库(例如glfw)会为我们创建好深度缓冲和模板缓冲,这样我们在使用的时候,只要直接启动深度缓冲或者模板缓冲即可,但是在我们自己创建帧缓冲时,如果还是简单的开启深度缓冲或模板缓冲,运行的结果将和未开启深度缓冲一样,其中的原因是,我们此时必须显式地去创建深度缓冲和模板缓冲,并将他们绑定到创建的帧缓冲对象上,这样深度信息或者模板信息才有存放的媒介;
2.深度缓冲和模板缓冲的创建,如下:
//深度缓冲,如果没有会无法进行GL_DEPTH_TEST
_depthStencilTexture.setInternalFormat(GL_DEPTH_COMPONENT);
_depthStencilTexture.setImageFormat(GL_DEPTH_COMPONENT);
_depthStencilTexture.setDataType(GL_FLOAT);
_depthStencilTexture.setWrapType(GL_CLAMP_TO_BORDER);
_depthStencilTexture.generate(SCREEN_WIDTH, SCREEN_HEIGHT, nullptr);
上述代码和创建深度缓冲的代码无异,但是我们要开启模板缓冲时,并不需要创建模板缓冲区,实际上,我在按照OpenGL上的API去创建模板缓冲后,在将模板缓冲绑定到帧缓冲对象时,反倒会出现问题.我想,可能是OpenGL自动将GL_FLOAT中的24位分配给深度缓冲,剩余的8位分配给了模板缓冲,所以不需要自己再额外去创建模板缓冲.
全部代码链接,DeferedRender类