延迟渲染中光源的体积光(Light Volumn)

这两天在整理LearnOpenGL教程中延迟着色部分的内容,在3月份看Unity Shader入门精要这本书,涉及到这个内容,当时仅仅是一扫而过,没有注意,这两天的学习让自己对前向渲染延迟渲染有了一个直观的认识.

教程中提及了光体积(Light Volumn)的概念和使用技巧,但是并未给出示例代码,自己在查阅资料整理出了该部分的代码,作为记录,同时希望可以帮助在这里卡壳的童鞋.

参考的链接:
1.LearnOpenGL教程

该篇博客的结构如下:

  1. 渲染球体实现光体积
  2. 模板缓冲实现光体积
  3. 遇到的一些问题

渲染球体实现光体积

效果
渲染的球体:
这里写图片描述
渲染结果:
这里写图片描述

思路

渲染一个实际的球体,并根据该光源的半径缩放.该球体的中心凡在光源的位置,由于它是根据光体积半径缩放的,这个球体正好覆盖了光的可视体积.我们使用大体相同的延迟片段着色器来渲染球体,因为球体产生了完全匹配于受影响像素的着色器调用,我们只渲染了受影响的像素而跳过其它的像素.

相比于教程中使用伪光体积(其所提及的方法由于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类

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值