LearnOpenGL 笔记(三)-高级光照

目录

十五、Blinn-Phong

十六、Gamma校正

重校

光照衰减

 十七、阴影映射

 Shadow Mapping

深度贴图

光源空间的变换

渲染阴影

改进阴影贴图

十八、点光源阴影

生成深度立方体贴图

十九、法线贴图

法线贴图

切线空间

二十、视差贴图

陡峭视差映射

二十一、HDR-高动态范围

浮点帧缓冲

色调映射

 二十二、泛光-光晕

提取亮色

高斯模糊

把两个纹理混合

二十三、延迟着色

G缓冲

延迟光照处理阶段

结合延迟渲染与正向渲染

 二十四、SSAO-屏幕空间环境光遮蔽

样本缓冲

法向半球

随机核心转动

SSAO着色器

环境遮蔽模糊

应用环境遮蔽


十五、Blinn-Phong

冯氏光照不仅对真实光照有很好的近似,而且性能也很高。但是它的镜面反射会在一些情况下出现问题,特别是物体反光度很低时,会导致大片(粗糙的)高光区域。

出现这个问题的原因是观察向量和反射向量间的夹角不能大于90度。

左图中是我们熟悉的冯氏光照中的反射向量,而右图中,视线与反射方向之间的夹角明显大于90度,这种情况下镜面光分量会变为0.0。引入了Blinn-Phong着色模型它对镜面光模型的处理上有一些不同。

Blinn-Phong模型不再依赖于反射向量,而是采用了所谓的半程向量(Halfway Vector),即光线与视线夹角一半方向上的一个单位向量。当半程向量与法线向量越接近时,镜面光分量就越大。

//将光线的方向向量和观察向量加到一起,并将结果正规化(Normalize)
vec3 lightDir   = normalize(lightPos - FragPos);
vec3 viewDir    = normalize(viewPos - FragPos);
vec3 halfwayDir = normalize(lightDir + viewDir);

//镜面光分量的实际计算只不过是对表面法线和半程向量进行一次约束点乘

float spec = pow(max(dot(normal, halfwayDir), 0.0), shininess);
vec3 specular = lightColor * spec;

Blinn-Phong与冯氏模型唯一的区别就是,Blinn-Phong测量的是法线与半程向量之间的夹角,而冯氏模型测量的是观察方向与反射向量间的夹角。

半程向量与表面法线的夹角通常会小于观察与反射向量的夹角。想获得和冯氏着色类似的效果,就必须在使用Blinn-Phong模型时将镜面反光度设置更高一点。

 if(blinn)//blinn 和 phong 差别
    {
        vec3 halfwayDir = normalize(lightDir + viewDir);  
        spec = pow(max(dot(normal, halfwayDir), 0.0), 16.0);
    }
    else
    {
        vec3 reflectDir = reflect(-lightDir, normal);
        spec = pow(max(dot(viewDir, reflectDir), 0.0), 8.0);
    }

十六、Gamma校正

监视器有一个物理特性就是两倍的输入电压产生的不是两倍的亮度。输入电压产生约为输入电压的2.2次幂的亮度,这叫做监视器Gamma。Gamma也叫灰度系数,每种显示设备都有自己的Gamma值,都不相同。

设备输出亮度 = 电压的Gamma次幂

直到现在,我们还一直假设我们所有的工作都是在线性空间中进行的(译注:Gamma为1),但最终还是要把所有的颜色输出到监视器上,所以我们配置的所有颜色和光照变量从物理角度来看都是不正确的,在我们的监视器上很少能够正确地显示。

 Gamma校正(Gamma Correction)的思路是在最终的颜色输出上应用监视器Gamma的倒数。

有两种在你的场景中应用gamma校正的方式:

使用OpenGL内建的sRGB帧缓冲。开启GL_FRAMEBUFFER_SRGB,可以告诉OpenGL每个后续的绘制命令里,在颜色储存到颜色缓冲之前先校正sRGB颜色。sRGB这个颜色空间大致对应于gamma2.2。

glEnable(GL_FRAMEBUFFER_SRGB);

gamma校正将把线性颜色空间转变为非线性空间所以在最后一步进行gamma校正是极其重要的。

第二个方法稍微复杂点,但同时也是我们对gamma操作有完全的控制权。我们在每个相关像素着色器运行的最后应用gamma校正,所以在发送到帧缓冲前,颜色就被校正了。

float gamma = 2.2;
    fragColor.rgb = pow(fragColor.rgb, vec3(1.0/gamma));
//将fragColor的每个颜色元素应用有一个1.0/gamma的幂运算,校正像素着色器的颜色输出。

重校

当我们基于监视器上看到的情况创建一个图像,我们就已经对颜色值进行了gamma校正,所以再次显示在监视器上就没错。由于我们在渲染中又进行了一次gamma校正,图片就实在太亮了。

把这些sRGB纹理在进行任何颜色值的计算前变回线性空间

float gamma = 2.2;
vec3 diffuseColor = pow(texture(diffuse, texCoords).rgb, vec3(gamma));

,在OpenGL中创建了一个纹理,GL_SRGB和GL_SRGB_ALPHA内部纹理格式,OpenGL将自动把颜色校正到线性空间中,这样我们所使用的所有颜色值都是在线性空间中的了。

glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, image);

diffuse纹理,这种为物体上色的纹理几乎都是在sRGB空间中的。而为了获取光照参数的纹理,像specular贴图和法线贴图几乎都在线性空间中.

光照衰减

真实的物理世界中,光照的衰减和光源的距离的平方成反比。-平方衰减

float attenuation = 1.0 / (distance * distance);

然而,当我们使用这个衰减公式的时候,衰减效果总是过于强烈,光只能照亮一小圈,看起来并不真实。我们还可以使用双曲线函数-线性衰减

float attenuation = 1.0 / distance;

 十七、阴影映射

阴影是光线被阻挡的结果;当一个光源的光线由于其他物体的阻挡不能够达到一个物体的表面的时候,那么这个物体就在阴影中了。有阴影的时候你能更容易地区分出物体之间的位置关系。

 Shadow Mapping

我们以光的位置为视角进行渲染,我们能看到的东西都将被点亮,看不见的一定是在阴影之中了。

 这里的所有蓝线代表光源可以看到的fragment。黑线代表被遮挡的fragment:它们应该渲染为带阴影的。

深度贴图

//为渲染的深度贴图创建一个帧缓冲对象
GLuint depthMapFBO;
glGenFramebuffers(1, &depthMapFBO);

//创建一个2D纹理,提供给帧缓冲的深度缓冲使用
const GLuint SHADOW_WIDTH = 1024, SHADOW_HEIGHT = 1024;//纹理的高宽设置为1024:这是深度贴图的分辨率。

GLuint depthMap;
glGenTextures(1, &depthMap);
glBindTexture(GL_TEXTURE_2D, depthMap);
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, 
             SHADOW_WIDTH, SHADOW_HEIGHT, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); 
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);

//把生成的深度纹理作为帧缓冲的深度缓冲
glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depthMap, 0);
glDrawBuffer(GL_NONE);
glReadBuffer(GL_NONE);//读和绘制缓冲设置为GL_NONE
glBindFramebuffer(GL_FRAMEBUFFER, 0);

生成深度贴图

// 1. 首选渲染深度贴图
glViewport(0, 0, SHADOW_WIDTH, SHADOW_HEIGHT);
//阴影贴图经常和我们原来渲染的场景(通常是窗口分辨率)有着不同的分辨率,我们需要改变视口
glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);
glClear(GL_DEPTH_BUFFER_BIT);
ConfigureShaderAndMatrices();
RenderScene();
glBindFramebuffer(GL_FRAMEBUFFER, 0);
// 2. 像往常一样渲染场景,但这次使用深度贴图
glViewport(0, 0, SCR_WIDTH, SCR_HEIGHT);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
ConfigureShaderAndMatrices();
glBindTexture(GL_TEXTURE_2D, depthMap);
RenderScene();

光源空间的变换

我们将为光源使用正交投影矩阵,透视图将没有任何变形,使用glm::lookAt函数;这次从光源的位置看向场景中央

glm::mat4 lightSpaceMatrix = lightProjection * lightView;

 顶点着色器将一个单独模型的一个顶点,使用lightSpaceMatrix变换到光空间中。

#version 330 core
layout (location = 0) in vec3 position;

uniform mat4 lightSpaceMatrix;
uniform mat4 model;

void main()
{
    gl_Position = lightSpaceMatrix * model * vec4(position, 1.0f);
}

//渲染深度缓冲
simpleDepthShader.Use();
glUniformMatrix4fv(lightSpaceMatrixLocation, 1, GL_FALSE, glm::value_ptr(lightSpaceMatrix));

glViewport(0, 0, SHADOW_WIDTH, SHADOW_HEIGHT);
glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);//帧缓冲
glClear(GL_DEPTH_BUFFER_BIT);
RenderScene(simpleDepthShader);
glBindFramebuffer(GL_FRAMEBUFFER, 0);

渲染阴影

正确地生成深度贴图以后我们就可以开始生成阴影了。在顶点着色器中进行光空间的变换

#version 330 core
layout (location = 0) in vec3 position;
layout (location = 1) in vec3 normal;
layout (location = 2) in vec2 texCoords;

out vec2 TexCoords;

out VS_OUT {
    vec3 FragPos;
    vec3 Normal;
    vec2 TexCoords;
    vec4 FragPosLightSpace;//光空间
} vs_out;

uniform mat4 projection;
uniform mat4 view;
uniform mat4 model;
uniform mat4 lightSpaceMatrix;

void main()
{
    gl_Position = projection * view * model * vec4(position, 1.0f);
    vs_out.FragPos = vec3(model * vec4(position, 1.0));
    vs_out.Normal = transpose(inverse(mat3(model))) * normal;
    vs_out.TexCoords = texCoords;
    vs_out.FragPosLightSpace = lightSpaceMatrix * vec4(vs_out.FragPos, 1.0);
}

片段着色器使用Blinn-Phong光照模型渲染场景。

#version 330 core
out vec4 FragColor;

in VS_OUT {
    vec3 FragPos;
    vec3 Normal;
    vec2 TexCoords;
    vec4 FragPosLightSpace;//光空间的片段位置
} fs_in;

uniform sampler2D diffuseTexture;
uniform sampler2D shadowMap;//第一个渲染阶段得到的深度贴图

uniform vec3 lightPos;
uniform vec3 viewPos;

float ShadowCalculation(vec4 fragPosLightSpace)
{
    [...]
 // 执行透视除法将裁切空间坐标的范围-w到w转为-1到1
    vec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w;

//整个projCoords向量都需要变换到[0,1]范围
    projCoords = projCoords * 0.5 + 0.5;
//得到光的位置视野下最近的深度,获取投影向量的z坐标
    float closestDepth = texture(shadowMap, projCoords.xy).r;
    float currentDepth = projCoords.z;
//简单检查currentDepth是否高于closetDepth
    float shadow = currentDepth > closestDepth  ? 1.0 : 0.0;
}

void main()
{           
    vec3 color = texture(diffuseTexture, fs_in.TexCoords).rgb;
    vec3 normal = normalize(fs_in.Normal);
    vec3 lightColor = vec3(1.0);
    // Ambient
    vec3 ambient = 0.15 * color;
    // Diffuse
    vec3 lightDir = normalize(lightPos - fs_in.FragPos);
    float diff = max(dot(lightDir, normal), 0.0);
    vec3 diffuse = diff * lightColor;
    // Specular
    vec3 viewDir = normalize(viewPos - fs_in.FragPos);
    vec3 reflectDir = reflect(-lightDir, normal);
    float spec = 0.0;
    vec3 halfwayDir = normalize(lightDir + viewDir);  
    spec = pow(max(dot(normal, halfwayDir), 0.0), 64.0);
    vec3 specular = spec * lightColor;    
    // 计算阴影
    float shadow = ShadowCalculation(fs_in.FragPosLightSpace);     
//计算出一个shadow值,当fragment在阴影中时是1.0,在阴影外是0.0  
    vec3 lighting = (ambient + (1.0 - shadow) * (diffuse + specular)) * color;    

    FragColor = vec4(lighting, 1.0f);
}

改进阴影贴图

阴影失真:明显的线条样式,地板四边形渲染出很大一块交替黑线。这种阴影贴图的不真实感叫做阴影失真(Shadow Acne).

       

 因为阴影贴图受限于分辨率,在距离光源比较远的情况下,多个片段可能从深度贴图的同一个值中去采样。我们可以用一个叫做阴影偏移(shadow bias)的技巧来解决这个问题,我们简单的对表面的深度(或深度贴图)应用一个偏移量,这样片段就不会被错误地认为在表面之下了。

float bias = 0.005;
float shadow = currentDepth - bias > closestDepth  ? 1.0 : 0.0;

//使用点乘
float bias = max(0.05 * (1.0 - dot(normal, lightDir)), 0.005);

悬浮:对物体的实际深度应用了平移。偏移有可能足够大,以至于可以看出阴影相对实际物体位置的偏移。

当渲染深度贴图时候使用正面剔除

glCullFace(GL_FRONT);
RenderSceneToDepthMap();
glCullFace(GL_BACK); // 不要忘记设回原先的culling face

采样过多:光的视锥不可见的区域一律被认为是处于阴影中,不管它真的处于阴影之中。因为超出光的视锥的投影坐标比1.0大,这样采样的深度纹理就会超出他默认的0到1的范围。根据纹理环绕方式,我们将会得到不正确的深度结果。

让所有超出深度贴图的坐标的深度范围是1.0,这样超出的坐标将永远不在阴影之中。

//把深度贴图的纹理环绕选项设置为GL_CLAMP_TO_BORDER
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
GLfloat borderColor[] = { 1.0, 1.0, 1.0, 1.0 };
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);

PCF

因为深度贴图有一个固定的分辨率,多个片段对应于一个纹理像素。结果就是多个片段会从深度贴图的同一个深度值进行采样,这几个片段便得到的是同一个阴影,这就会产生锯齿边。

可以通过增加深度贴图的分辨率的方式来降低锯齿块,也可以尝试尽可能的让光的视锥接近场景。另一个(并不完整的)解决方案叫做PCF,这是一种多个不同过滤方式的组合,它产生柔和阴影,使它们出现更少的锯齿块和硬边。

float shadow = 0.0;
vec2 texelSize = 1.0 / textureSize(shadowMap, 0);
//textureSize返回一个给定采样器纹理的0级mipmap的vec2类型的宽和高。
//用1除以它返回一个单独纹理像素的大小
for(int x = -1; x <= 1; ++x)
{
    for(int y = -1; y <= 1; ++y)
    {//从纹理像素四周对深度贴图采样
        float pcfDepth = texture(shadowMap, projCoords.xy + vec2(x, y) * texelSize).r; //从深度贴图中多次采样
        shadow += currentDepth - bias > pcfDepth ? 1.0 : 0.0;        
    }    
}
shadow /= 9.0;//结合在一起,进行平均化

十八、点光源阴影

在各种方向生成动态阴影,立方体贴图可以储存6个面的环境数据,它可以将整个场景渲染到立方体贴图的每个面上,把它们当作点光源四周的深度值来采样。

 生成后的深度立方体贴图被传递到光照像素着色器,它会用一个方向向量来采样立方体贴图,从而得到当前的fragment的深度(从光的透视图)。

生成深度立方体贴图

为创建一个光周围的深度值的立方体贴图,我们必须渲染场景6次:每次一个面。

//创建一个立方体贴图
GLuint depthCubemap;
glGenTextures(1, &depthCubemap);

//然后生成立方体贴图的每个面,将它们作为2D深度值纹理图像
const GLuint SHADOW_WIDTH = 1024, SHADOW_HEIGHT = 1024;
glBindTexture(GL_TEXTURE_CUBE_MAP, depthCubemap);
for (GLuint i = 0; i < 6; ++i)
        glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_DEPTH_COMPONENT, 
                     SHADOW_WIDTH, SHADOW_HEIGHT, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL);

//设置合适的纹理参数
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);

glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);
glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, depthCubemap, 0);
//把立方体贴图附加成帧缓冲
glDrawBuffer(GL_NONE);
glReadBuffer(GL_NONE);//只关心深度值
glBindFramebuffer(GL_FRAMEBUFFER, 0);

正常情况下,我们把立方体贴图纹理的一个面附加到帧缓冲对象上,渲染场景6次,每次将帧缓冲的深度缓冲目标改成不同立方体贴图面。由于我们将使用一个几何着色器,它允许我们把所有面在一个过程渲染。

// 1. first render to depth cubemap
glViewport(0, 0, SHADOW_WIDTH, SHADOW_HEIGHT);
glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);
glClear(GL_DEPTH_BUFFER_BIT);
ConfigureShaderAndMatrices();
RenderScene();
glBindFramebuffer(GL_FRAMEBUFFER, 0);
// 2. then render scene as normal with shadow mapping (using depth cubemap)
glViewport(0, 0, SCR_WIDTH, SCR_HEIGHT);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
ConfigureShaderAndMatrices();
glBindTexture(GL_TEXTURE_CUBE_MAP, depthCubemap);
RenderScene();

十九、法线贴图

如果我们以光的视角来看这个问题:是什么使表面被视为完全平坦的表面来照亮?答案会是表面的法线向量。砖块表面只有一个法线向量,表面完全根据这个法线向量被以一致的方式照亮。

如果每个fragment都是用自己的不同的法线,我们就可以根据表面细微的细节对法线向量进行改变;这样就会获得一种表面看起来要复杂得多的幻觉。

 这种每个fragment使用各自的法线,替代一个面上所有fragment使用同一个法线的技术叫做法线贴图(normal mapping)或凹凸贴图(bump mapping)。

法线贴图

为使法线贴图工作,我们需要为每个fragment提供一个法线,使用一个2D纹理来储存法线数据。

2D纹理不仅可以储存颜色和光照数据,还可以储存法线向量。

vec3 rgb_normal = normal * 0.5 + 0.5; // 从 [-1,1] 转换至 [0,1]

将法线向量变换为像这样的RGB颜色元素,我们就能把根据表面的形状的fragment的法线保存在2D纹理中。这会是一种偏蓝色调的纹理这是因为所有法线的指向都偏向z轴(0, 0, 1)这是一种偏蓝的颜色。可以看到在每个砖块的顶部,颜色倾向于偏绿,这是因为砖块的顶部的法线偏向于指向正y轴方向(0, 1, 0),这样它就是绿色的了。

uniform sampler2D normalMap;  

void main()
{           
    // 从法线贴图范围[0,1]获取法线
    normal = texture(normalMap, fs_in.TexCoords).rgb;
    // 将法线向量转换为范围[-1,1]
    normal = normalize(normal * 2.0 - 1.0);   

    [...]
    // 像往常那样处理光照
}

切线空间

法线贴图中的法线向量定义在切线空间中,在切线空间中,法线永远指着正z方向。无论最终变换到什么方向。使用一个特定的矩阵我们就能将本地/切线空间中的法线向量转成世界或视图空间下,使它们转向到最终的贴图表面的方向。

TBN矩阵:tangent、bitangent和normal向量。上向量是表面的法线向量。右和前向量是切线(Tagent)和副切线(Bitangent)向量。

//为让法线贴图工作,我们先得在着色器中创建一个TBN矩阵
layout (location = 1) in vec3 normal;
layout (location = 3) in vec3 tangent;
layout (location = 4) in vec3 bitangent;

void main()
{
   [...]
//先将所有TBN向量变换到我们所操作的坐标系中,现在是世界空间,我们可以乘以model矩阵
   vec3 T = normalize(vec3(model * vec4(tangent,   0.0)));
   vec3 B = normalize(vec3(model * vec4(bitangent, 0.0)));
//vec3 B = cross(T, N);
   vec3 N = normalize(vec3(model * vec4(normal,    0.0)));
   mat3 TBN = mat3(T, B, N)
}

1.我们直接使用TBN矩阵,这个矩阵可以把切线坐标空间的向量转换到世界坐标空间。我们把它传给片段着色器中,把通过采样得到的法线坐标左乘上TBN矩阵,转换到世界坐标空间中,这样所有法线和其他光照变量就在同一个坐标系中了。

//把TBN矩阵发给像素着色器
out VS_OUT {
    vec3 FragPos;
    vec2 TexCoords;
    mat3 TBN;
} vs_out;  

void main()
{
    [...]
    vs_out.TBN = mat3(T, B, N);
}
//在像素着色器中我们用mat3作为输入变量
in VS_OUT {
    vec3 FragPos;
    vec2 TexCoords;
    mat3 TBN;
} fs_in;

//更新法线贴图代码引入切线到世界空间变换
normal = texture(normalMap, fs_in.TexCoords).rgb;
normal = normalize(normal * 2.0 - 1.0);   
normal = normalize(fs_in.TBN * normal);

2.我们也可以使用TBN矩阵的逆矩阵,这个矩阵可以把世界坐标空间的向量转换到切线坐标空间。

二十、视差贴图

视差贴图(Parallax Mapping)技术和法线贴图差不多,但它有着不同的原则。视差贴图和光照无关。视差贴图属于位移贴图(Displacement Mapping)技术的一种,它对根据储存在纹理中的几何信息对顶点进行位移或偏移。

 视差贴图背后的思想是修改纹理坐标使一个fragment的表面看起来比实际的更高或者更低,所有这些都根据观察方向和高度贴图。

 红线代表高度贴图中的数值的立体表达,向量V代表观察方向。如果平面进行实际位移,观察者会在点B看到表面。在A位置上的fragment不再使用点A的纹理坐标而是使用点B的.

如何从点AA得到点BB的纹理坐标:通过对从fragment到观察者的方向向量V进行缩放的方式解决这个问题,缩放的大小是A处fragment的高度。即P与H(A)等长。然后选取P这个向量与平面对齐的坐标作为纹理坐标偏移量。即H(P)。

在大多数时候都没问题,但点B是粗略估算得到的。当表面的高度变化很快的时候,看起来就不会真实,因为向量P最终不会和B接近。

使用反色高度贴图(也叫深度贴图)去模拟深度比模拟高度更容易。下图反映了这个轻微的改变。

 我们再次获得A和B,但是这次我们用向量V减去点A的纹理坐标得到P。我们通过在着色器中用1.0减去采样得到的高度贴图中的值来取得深度值,而不再是高度值。

位移贴图是在像素着色器中实现的,因为三角形表面的所有位移效果都不同。法线贴图教程中我们已经有了一个顶点着色器。

#version 330 core
layout (location = 0) in vec3 position;
layout (location = 1) in vec3 normal;
layout (location = 2) in vec2 texCoords;
layout (location = 3) in vec3 tangent;
layout (location = 4) in vec3 bitangent;

out VS_OUT {
    vec3 FragPos;
    vec2 TexCoords;
    vec3 TangentLightPos;
    vec3 TangentViewPos;
    vec3 TangentFragPos;
} vs_out;

uniform mat4 projection;
uniform mat4 view;
uniform mat4 model;

uniform vec3 lightPos;
uniform vec3 viewPos;

void main()
{
    gl_Position      = projection * view * model * vec4(position, 1.0f);
    vs_out.FragPos   = vec3(model * vec4(position, 1.0));   
    vs_out.TexCoords = texCoords;    

    vec3 T   = normalize(mat3(model) * tangent);
    vec3 B   = normalize(mat3(model) * bitangent);
    vec3 N   = normalize(mat3(model) * normal);
    mat3 TBN = transpose(mat3(T, B, N));

    vs_out.TangentLightPos = TBN * lightPos;
    vs_out.TangentViewPos  = TBN * viewPos;
    vs_out.TangentFragPos  = TBN * vs_out.FragPos;
}

把position和在切线空间中的观察者的位置viewPos发送给像素着色器。在像素着色器中,我们实现视差贴图的逻辑。

#version 330 core
out vec4 FragColor;

in VS_OUT {
    vec3 FragPos;
    vec2 TexCoords;
    vec3 TangentLightPos;
    vec3 TangentViewPos;
    vec3 TangentFragPos;
} fs_in;

uniform sampler2D diffuseMap;
uniform sampler2D normalMap;
uniform sampler2D depthMap;

uniform float height_scale;

vec2 ParallaxMapping(vec2 texCoords, vec3 viewDir);//把fragment的纹理坐标和切线空间中的fragment到观察者的方向向量为输入。这个函数返回经位移的纹理坐标。
vec2 ParallaxMapping(vec2 texCoords, vec3 viewDir)
{ 
    float height =  texture(depthMap, texCoords).r;    
    vec2 p = viewDir.xy / viewDir.z * (height * height_scale);
    return texCoords - p;    
}

void main()
{           
    // Offset texture coordinates with Parallax Mapping
    vec3 viewDir   = normalize(fs_in.TangentViewPos - fs_in.TangentFragPos);
    vec2 texCoords = ParallaxMapping(fs_in.TexCoords,  viewDir);

    // then sample textures with new texture coords
    vec3 diffuse = texture(diffuseMap, texCoords);
    vec3 normal  = texture(normalMap, texCoords);
    normal = normalize(normal * 2.0 - 1.0);
    // proceed with lighting code
    [...]    
}

 viewDir向量是经过了标准化的,viewDir.z会在0.0到1.0之间的某处。当viewDir大致平行于表面时,它的z元素接近于0.0,除法会返回比viewDir垂直于表面的时候更大的P。

 只用法线贴图和与视差贴图相结合的法线贴图的不同之处。因为视差贴图尝试模拟深度,它实际上能够根据你观察它们的方向使砖块叠加到其他砖块上。在平面的边缘上,纹理坐标超出了0到1的范围进行采样,根据纹理的环绕方式导致了不真实的结果。

texCoords = ParallaxMapping(fs_in.TexCoords,  viewDir);
if(texCoords.x > 1.0 || texCoords.y > 1.0 || texCoords.x < 0.0 || texCoords.y < 0.0)
    discard;
//超出默认纹理坐标范围进行采样的时候就丢弃这个fragment

陡峭视差映射

陡峭视差映射(Steep Parallax Mapping)是视差映射的扩展,原则是一样的,但不是使用一个样本而是多个样本来确定向量P,B。在陡峭的高度变化的情况下,它也能得到更好的结果.

陡峭视差映射的基本思想是将总深度范围划分为同一个深度/高度的多个层。从每个层中我们沿着P方向移动采样纹理坐标,直到我们找到一个采样低于当前层的深度值。

 我们从上到下遍历深度层,我们把每个深度层和储存在深度贴图中的它的深度值进行对比。如果这个层的深度值小于深度贴图的值,就意味着这一层的P向量部分在表面之下。这个例子中我们可以看到第二层(D(2) = 0.73)的深度贴图的值仍低于第二层的深度值0.4,所以我们继续。下一次迭代,这一层的深度值0.6大于深度贴图中采样的深度值(D(3) = 0.37)。

vec2 ParallaxMapping(vec2 texCoords, vec3 viewDir)
{ 
    // number of depth layers
    const float numLayers = 10;
    // calculate the size of each layer
    float layerDepth = 1.0 / numLayers;
    // depth of current layer
    float currentLayerDepth = 0.0;
    // the amount to shift the texture coordinates per layer (from vector P)
    vec2 P = viewDir.xy * height_scale; 
    float deltaTexCoords = P / numLayers;

    [...]     
// get initial values
vec2  currentTexCoords     = texCoords;
float currentDepthMapValue = texture(depthMap, currentTexCoords).r;
//循环每一层深度,直到沿着P¯向量找到第一个返回低于(位移)表面的深度的纹理坐标偏移量
while(currentLayerDepth < currentDepthMapValue)
{
    // shift texture coordinates along direction of P
    currentTexCoords -= deltaTexCoords;
    // get depthmap value at current texture coordinates
    currentDepthMapValue = texture(depthMap, currentTexCoords).r;  
    // get depth of next layer
    currentLayerDepth += layerDepth;  
}

return texCoords - currentTexCoords;
}

二十一、HDR-高动态范围

一般来说,当存储在帧缓冲(Framebuffer)中时,亮度和颜色的值是默认被限制在0.0到1.0之间的。但是如果我们遇上了一个特定的区域,其中有多个亮光源使这些数值总和超过了1.0。这些片段中超过1.0的亮度或者颜色值会被约束在1.0,从而导致场景混成一片

 这是由于大量片段的颜色值都非常接近1.0,在很大一个区域内每一个亮的片段都有相同的白色。这损失了很多的细节,使场景看起来非常假。

  1. 减小光源的强度从而保证场景内没有一个片段亮于1.0。
  2. 让颜色暂时超过1.0,然后将其转换至0.0到1.0的区间内,从而防止损失细节。

显示器被限制为只能显示值为0.0到1.0间的颜色,但是在光照方程中却没有这个限制。通过使片段的颜色超过1.0,我们有了一个更大的颜色范围,这也被称作HDR(High Dynamic Range, 高动态范围)亮的东西可以变得非常亮,暗的东西可以变得非常暗。

HDR渲染和其很相似,我们允许用更大范围的颜色值渲染从而获取大范围的黑暗与明亮的场景细节,最后将所有HDR值转换成在[0.0, 1.0]范围的LDR(Low Dynamic Range,低动态范围)。转换HDR值到LDR值得过程叫做色调映射(Tone Mapping)

浮点帧缓冲

当帧缓冲使用了一个标准化的定点格式(像GL_RGB)为其颜色缓冲的内部格式,OpenGL会在将这些值存入帧缓冲前自动将其约束到0.0到1.0之间。

当一个帧缓冲的颜色缓冲的内部格式被设定成了GL_RGB16FGL_RGBA16FGL_RGB32F 或者GL_RGBA32F时,这些帧缓冲被叫做浮点帧缓冲(Floating Point Framebuffer),浮点帧缓冲可以存储超过0.0到1.0范围的浮点值,所以非常适合HDR渲染。

glBindTexture(GL_TEXTURE_2D, colorBuffer);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGB, GL_FLOAT, NULL);  //只需要改变颜色缓冲的内部格式参数

有了一个带有浮点颜色缓冲的帧缓冲,我们可以放心渲染场景到这个帧缓冲中。

glBindFramebuffer(GL_FRAMEBUFFER, hdrFBO);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);  
    // [...] 渲染(光照的)场景
glBindFramebuffer(GL_FRAMEBUFFER, 0);

// 现在使用一个不同的着色器将HDR颜色缓冲渲染至2D铺屏四边形上
hdrShader.Use();
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, hdrColorBufferTexture);
RenderQuad();

色调映射

色调映射(Tone Mapping)是一个损失很小的转换浮点颜色值至我们所需的LDR[0.0, 1.0]范围内的过程,通常会伴有特定的风格的色平衡(Stylistic Color Balance)。最简单的色调映射算法是Reinhard色调映射,它涉及到分散整个HDR颜色值到LDR颜色值上,所有的值都有对应。

uniform float exposure;//曝光参数

void main()
{             
    const float gamma = 2.2;
    vec3 hdrColor = texture(hdrBuffer, TexCoords).rgb;

    // Reinhard色调映射
    vec3 mapped = hdrColor / (hdrColor + vec3(1.0));
//
 // 曝光色调映射
    vec3 mapped = vec3(1.0) - exp(-hdrColor * exposure);

    // Gamma校正
    mapped = pow(mapped, vec3(1.0 / gamma));

    color = vec4(mapped, 1.0);
}   

高曝光值会使隧道的黑暗部分显示更多的细节,然而低曝光值会显著减少黑暗区域的细节,但允许我们看到更多明亮区域的细节。

 二十二、泛光-光晕

明亮的光源和区域经常很难向观察者表达出来,因为监视器的亮度范围是有限的。一种区分明亮光源的方式是使它们在监视器上发出光芒,光源的光芒向四周发散。这样观察者就会产生光源或亮区的确是强光区。光晕效果可以使用一个后处理特效泛光来实现。泛光使所有明亮区域产生光晕效果。

  1.  我们得到这个HDR颜色缓冲纹理,提取所有超出一定亮度的fragment。
  2. 我们将这个超过一定亮度阈限的纹理进行模糊。泛光效果的强度很大程度上是由被模糊过滤器的范围和强度所决定。
  3. 最终的被模糊化的纹理就是我们用来获得发出光晕效果的东西。

      

 泛光本身并不是个复杂的技术,但很难获得正确的效果。它的品质很大程度上取决于所用的模糊过滤器的质量和类型。

 

提取亮色

 第一步我们要从渲染出来的场景中提取两张图片。我们可以渲染场景两次,每次使用一个不同的着色器渲染到不同的帧缓冲中,但我们可以使用一个叫做MRT(Multiple Render Targets,多渲染目标)的小技巧。

layout (location = 0) out vec4 FragColor;
layout (location = 1) out vec4 BrightColor;

void main()
{            
    [...] // first do normal lighting calculations and output results正常计算光照
//将其传递给第一个像素着色器的输出变量FragColor
    FragColor = vec4(lighting, 1.0f);
    // Check whether fragment output is higher than threshold, if so output as brightness color使用当前储存在FragColor的东西来决定它的亮度是否超过了一定阈限
    float brightness = dot(FragColor.rgb, vec3(0.2126, 0.7152, 0.0722));
    if(brightness > 1.0)//超过了一定阈限,我们就把颜色输出到第二个颜色缓冲
        BrightColor = vec4(FragColor.rgb, 1.0);
}
// Set up floating point framebuffer to render scene to
//一个附加了两个颜色缓冲的帧缓冲对象
GLuint hdrFBO;
glGenFramebuffers(1, &hdrFBO);
glBindFramebuffer(GL_FRAMEBUFFER, hdrFBO);
GLuint colorBuffers[2];
glGenTextures(2, colorBuffers);
for (GLuint i = 0; i < 2; i++)
{
    glBindTexture(GL_TEXTURE_2D, colorBuffers[i]);
    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_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    // attach texture to framebuffer
    glFramebufferTexture2D(
        GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + i, GL_TEXTURE_2D, colorBuffers[i], 0
    );
}

//显式告知OpenGL我们正在通过glDrawBuffers渲染到多个颜色缓冲,否则OpenGL只会渲染到帧缓冲的第一个颜色附件
GLuint attachments[2] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1 };
glDrawBuffers(2, attachments);


为什么泛光在HDR基础上能够运行得很好: 因为HDR中,我们可以将颜色值指定超过1.0这个默认的范围,我们能够得到对一个图像中的亮度的更好的控制权。

有了两个颜色缓冲,我们就有了一个正常场景的图像和一个提取出的亮区的图像;这些都在一个渲染步骤中完成。

高斯模糊

 高斯曲线在它的中间处的面积最大,使用它的值作为权重使得近处的样本拥有最大的优先权。

两步高斯模糊:它允许我们把二维方程分解为两个更小的方程:一个描述水平权重,另一个描述垂直权重。我们首先用水平权重在整个纹理上进行水平模糊,然后在经改变的纹理上进行垂直模糊。

把两个纹理混合

#version 330 core
out vec4 FragColor;
in vec2 TexCoords;

uniform sampler2D scene;
uniform sampler2D bloomBlur;
uniform float exposure;

void main()
{             
    const float gamma = 2.2;
    vec3 hdrColor = texture(scene, TexCoords).rgb;      
    vec3 bloomColor = texture(bloomBlur, TexCoords).rgb;
    hdrColor += bloomColor; // additive blending
    // tone mapping
    vec3 result = vec3(1.0) - exp(-hdrColor * exposure);
    // also gamma correct while we're at it       
    result = pow(result, vec3(1.0 / gamma));
    FragColor = vec4(result, 1.0f);
}

要注意的是我们要在应用色调映射之前添加泛光效果。这样添加的亮区的泛光,也会柔和转换为LDR,光照效果相对会更好。

二十三、延迟着色

现在一直使用的光照方式叫做正向渲染(Forward Rendering)或者正向着色法(Forward Shading),它是我们渲染物体的一种非常直接的方式,在场景中我们根据所有光源照亮一个物体,之后再渲染下一个物体,以此类推。

大部分片段着色器的输出都会被之后的输出覆盖,正向渲染还会在场景中因为高深的复杂度(多个物体重合在一个像素上)浪费大量的片段着色器运行时间。

延迟着色法(Deferred Shading)或者说是延迟渲染(Deferred Rendering),为了解决上述问题而诞生了,它大幅度地改变了我们渲染物体的方式。

延迟着色法基于我们延迟(Defer)推迟(Postpone)大部分计算量非常大的渲染(像是光照)到后期进行处理的想法。

  1. 在第一个几何处理阶段(Geometry Pass)中,我们先渲染场景一次,之后获取对象的各种几何信息,并储存在一系列叫做G缓冲(G-buffer)的纹理中。
  2. 在第二个光照处理阶段(Lighting Pass)中使用G缓冲内的纹理数据。

我们对于渲染过程进行解耦,将它高级的片段处理挪到后期进行,而不是直接将每个对象从顶点着色器带到片段着色器。光照计算过程还是和我们以前一样,但是现在我们需要从对应的G缓冲而不是顶点着色器(和一些uniform变量)那里获取输入变量了。

多渲染目标(Multiple Render Targets, MRT)技术

G缓冲

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

  • 一个3D位置向量来计算(插值)片段位置变量供lightDirviewDir使用
  • 一个RGB漫反射颜色向量,也就是反照率(Albedo)
  • 一个3D向量来判断平面的斜率
  • 一个镜面强度(Specular Intensity)浮点值
  • 所有光源的位置和颜色向量,通过uniform变量来设置
  • 玩家或者观察者的位置向量,通过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();
}

对于几何渲染处理阶段,我们首先需要初始化一个帧缓冲对象,我们很直观的称它为gBuffer,它包含了多个颜色缓冲和一个单独的深度渲染缓冲对象(Depth Renderbuffer Object)。

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);
//将颜色和镜面强度数据合并到一起,存储到一个单独的RGBA纹理里面
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),并检查完整性
[...]
#version 330 core//这个布局指示符(Layout Specifier)告诉了OpenGL我们需要渲染到当前的活跃帧缓冲中的哪一个颜色缓冲。
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;
}  

延迟光照处理阶段

我们可以选择通过一个像素一个像素地遍历各个G缓冲纹理,并将储存在它们里面的内容作为光照算法的输入.

out vec4 FragColor;
in vec2 TexCoords;

uniform sampler2D gPosition;
uniform sampler2D gNormal;
uniform sampler2D gAlbedoSpec;//光照处理阶段着色器接受三个uniform纹理,代表G缓冲

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 Diffuse = texture(gAlbedoSpec, TexCoords).rgb;
    float Specular = texture(gAlbedoSpec, TexCoords).a;

    // 然后和往常一样地计算光照
    vec3 lighting = Diffuse * 0.1; // 硬编码环境光照分量
    vec3 viewDir = normalize(viewPos - FragPos);
    for(int i = 0; i < NR_LIGHTS; ++i)
    {
        // 漫反射
        vec3 lightDir = normalize(lights[i].Position - FragPos);
        vec3 diffuse = max(dot(Normal, lightDir), 0.0) * Diffuse * lights[i].Color;

        // specular
        vec3 halfwayDir = normalize(lightDir + viewDir);  
        float spec = pow(max(dot(Normal, halfwayDir), 0.0), 16.0);
        vec3 specular = lights[i].Color * spec * Specular;

        // attenuation光照衰减
        float distance = length(lights[i].Position - FragPos);
        float attenuation = 1.0 / (1.0 + lights[i].Linear * distance + lights[i].Quadratic * distance * distance);
        diffuse *= attenuation;
        specular *= attenuation;
        lighting += diffuse + specular;        
    }
    }

    FragColor = vec4(lighting, 1.0);
}  

结合延迟渲染与正向渲染

      

 

// create and attach depth buffer (renderbuffer)
    unsigned int rboDepth;
    glGenRenderbuffers(1, &rboDepth);
    glBindRenderbuffer(GL_RENDERBUFFER, rboDepth);
    glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT, SCR_WIDTH, SCR_HEIGHT);
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, rboDepth);
    // finally check if framebuffer is complete
    if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
        std::cout << "Framebuffer not complete!" << std::endl;
    glBindFramebuffer(GL_FRAMEBUFFER, 0);


while:
 2.5. copy content of geometry's depth buffer to default framebuffer's depth buffer
  glBindFramebuffer(GL_READ_FRAMEBUFFER, gBuffer);
        glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); // write to default framebuffer
        
        glBlitFramebuffer(0, 0, SCR_WIDTH, SCR_HEIGHT, 0, 0, SCR_WIDTH, SCR_HEIGHT, GL_DEPTH_BUFFER_BIT, GL_NEAREST);
        glBindFramebuffer(GL_FRAMEBUFFER, 0);

 // 3. render lights on top of scene
        // --------------------------------
        shaderLightBox.use();
        shaderLightBox.setMat4("projection", projection);
        shaderLightBox.setMat4("view", view);
        for (unsigned int i = 0; i < lightPositions.size(); i++)
        {
            model = glm::mat4(1.0f);
            model = glm::translate(model, lightPositions[i]);
            model = glm::scale(model, glm::vec3(0.125f));
            shaderLightBox.setMat4("model", model);
            shaderLightBox.setVec3("lightColor", lightColors[i]);
            renderCube();
        }

 二十四、SSAO-屏幕空间环境光遮蔽

在现实中,光线会以任意方向散射,它的强度是会一直改变的,所以间接被照到的那部分场景也应该有变化的强度,而不是一成不变的环境光。间接光照的模拟叫做环境光遮蔽(Ambient Occlusion),它的原理是通过将褶皱、孔洞和非常靠近的墙面变暗的方法近似模拟出间接光照。

 这一技术使用了屏幕空间场景的深度而不是真实的几何体数据来确定遮蔽量。

SSAO原理:对于铺屏四边形(Screen-filled Quad)上的每一个片段,我们都会根据周边深度值计算一个遮蔽因子(Occlusion Factor)。这个遮蔽因子之后会被用来减少或者抵消片段的环境光照分量。遮蔽因子是通过采集片段周围球型核心(Kernel)的多个深度样本,并和当前片段深度值对比而得到的。高于片段深度值样本的个数就是我们想要的遮蔽因子。

 上图中在几何体内灰色的深度样本都是高于片段深度值的,他们会增加遮蔽因子;几何体内样本个数越多,片段获得的环境光照也就越少。渲染效果的质量和精度与我们采样的样本数量有直接关系。

  1. 如果样本数量太低,渲染的精度会急剧减少,我们会得到一种叫做波纹(Banding)的效果。
  2. 如果它太高了,反而会影响性能。通过引入随机性到采样核心(Sample Kernel)的采样中从而减少样本的数目。因为随机性引入了一个很明显的噪声图案,需要通过模糊结果来修复。

 SSAO技术会产生一种特殊的视觉风格。因为使用的采样核心是一个球体,它导致平整的墙面也会显得灰蒙蒙的,因为核心中一半的样本都会在墙这个几何体上。由于这个原因,我们将不会使用球体的采样核心,而使用一个沿着表面法向量的半球体采样核心。

 通过在法向半球体(Normal-oriented Hemisphere)周围采样,我们将不会考虑到片段底部的几何体.它消除了环境光遮蔽灰蒙蒙的感觉,从而产生更真实的结果。

样本缓冲

SSAO需要获取几何体的信息:

  • 逐片段位置向量
  • 逐片段的法线向量
  • 逐片段的反射颜色
  • 采样核心
  • 用来旋转采样核心的随机旋转矢量

通过使用一个逐片段观察空间位置,我们可以将一个采样半球核心对准片段的观察空间表面法线。对于每一个核心样本我们会采样线性深度纹理来比较结果。SSAO和延迟渲染能完美地兼容,因为我们已经存位置和法线向量到G缓冲中了。

layout (location = 0) out vec4 gPositionDepth;
layout (location = 1) out vec3 gNormal;
layout (location = 2) out vec4 gAlbedoSpec;

in vec2 TexCoords;
in vec3 FragPos;
in vec3 Normal;

const float NEAR = 0.1; // 投影矩阵的近平面
const float FAR = 50.0f; // 投影矩阵的远平面
float LinearizeDepth(float depth)
{//线性深度是在观察空间中的,所以之后的运算也是在观察空间中
    float z = depth * 2.0 - 1.0; // 回到NDC标准化设备坐标,最后我们所能看到的图像展示
    return (2.0 * NEAR * FAR) / (FAR + NEAR - z * (FAR - NEAR));    
}

void main()
{    
    // 储存片段的位置矢量到第一个G缓冲纹理
    gPositionDepth.xyz = FragPos;
    // 储存线性深度到gPositionDepth的alpha分量
    gPositionDepth.a = LinearizeDepth(gl_FragCoord.z); 
    // 储存法线信息到G缓冲
    gNormal = normalize(Normal);
    // 和漫反射颜色
    gAlbedoSpec.rgb = vec3(0.95);
}

//gPositionDepth颜色缓冲纹理被设置成了下面这样,可以用它来对每一个核心样本获取深度值

glGenTextures(1, &gPositionDepth);
glBindTexture(GL_TEXTURE_2D, gPositionDepth);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, 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);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

法向半球

我们需要沿着表面法线方向生成大量的样本。在切线空间(Tangent Space)内生成采样核心,法向量将指向正z方向.

std::uniform_real_distribution<GLfloat> randomFloats(0.0, 1.0); // 随机浮点数,范围0.0 - 1.0
//如果以-1.0到1.0为范围,取样核心就变成球型了
std::default_random_engine generator;
std::vector<glm::vec3> ssaoKernel;
for (GLuint i = 0; i < 64; ++i)
{//最大64样本值的采样核心
    glm::vec3 sample(
        randomFloats(generator) * 2.0 - 1.0, 
        randomFloats(generator) * 2.0 - 1.0, 
        randomFloats(generator)
    );
    sample = glm::normalize(sample);
    sample *= randomFloats(generator);
    GLfloat scale = GLfloat(i) / 64.0; 
    ssaoKernel.push_back(sample);  
}
//将更多的注意放在靠近真正片段的遮蔽上,也就是将核心样本靠近原点分布
scale = lerp(0.1f, 1.0f, scale * scale);//加速插值函数
/*lerp被定义为
GLfloat lerp(GLfloat a, GLfloat b, GLfloat f)
{
    return a + f * (b - a);
}
*/
   sample *= scale;
   ssaoKernel.push_back(sample);  
}

这就给了我们一个大部分样本靠近原点的核心分布。每个核心样本将会被用来偏移观察空间片段位置从而采样周围的几何体。

随机核心转动

通过引入一些随机性到采样核心上,我们可以大大减少获得不错结果所需的样本数量。创建一个小的随机旋转向量纹理平铺在屏幕上.

//创建一个4x4朝向切线空间平面法线的随机旋转向量数组
std::vector<glm::vec3> ssaoNoise;
for (GLuint i = 0; i < 16; i++)
{
    glm::vec3 noise(
        randomFloats(generator) * 2.0 - 1.0, 
        randomFloats(generator) * 2.0 - 1.0, 
        0.0f); //由于采样核心是沿着正z方向在切线空间内旋转,我们设定z分量为0.0,从而围绕z轴旋转。
    ssaoNoise.push_back(noise);
}
//创建一个包含随机旋转向量的4x4纹理
GLuint noiseTexture; 
glGenTextures(1, &noiseTexture);
glBindTexture(GL_TEXTURE_2D, noiseTexture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, 4, 4, 0, GL_RGB, GL_FLOAT, &ssaoNoise[0]);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
//GL_REPEAT,从而保证它合适地平铺在屏幕上

SSAO着色器

SSAO着色器在2D的铺屏四边形上运行,它对于每一个生成的片段计算遮蔽值(为了在最终的光照着色器中使用)。

GLuint ssaoFBO;
glGenFramebuffers(1, &ssaoFBO);  
glBindFramebuffer(GL_FRAMEBUFFER, ssaoFBO);

GLuint ssaoColorBuffer;//我们需要存储SSAO阶段的结果,我们还需要在创建一个帧缓冲对象

glGenTextures(1, &ssaoColorBuffer);
glBindTexture(GL_TEXTURE_2D, ssaoColorBuffer);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGB, GL_FLOAT, NULL);//由于环境遮蔽的结果是一个灰度值,我们将只需要纹理的红色分量,所以我们将颜色缓冲的内部格式设置为GL_RED。
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, ssaoColorBuffer, 0);

渲染SSAO完整的过程会像这样:

// 几何处理阶段: 渲染到G缓冲中
glBindFramebuffer(GL_FRAMEBUFFER, gBuffer);
    [...]
glBindFramebuffer(GL_FRAMEBUFFER, 0);  

// 使用G缓冲渲染SSAO纹理
glBindFramebuffer(GL_FRAMEBUFFER, ssaoFBO);
    glClear(GL_COLOR_BUFFER_BIT);
    shaderSSAO.Use();//shaderSSAO这个着色器将对应G缓冲纹理(包括线性深度)
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, gPositionDepth);
    glActiveTexture(GL_TEXTURE1);
    glBindTexture(GL_TEXTURE_2D, gNormal);
    glActiveTexture(GL_TEXTURE2);
    glBindTexture(GL_TEXTURE_2D, noiseTexture);
    SendKernelSamplesToShader();
    glUniformMatrix4fv(projLocation, 1, GL_FALSE, glm::value_ptr(projection));
    RenderQuad();
glBindFramebuffer(GL_FRAMEBUFFER, 0);

// 光照处理阶段: 渲染场景光照
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
shaderLightingPass.Use();
[...]
glActiveTexture(GL_TEXTURE3);
glBindTexture(GL_TEXTURE_2D, ssaoColorBuffer);
[...]
RenderQuad();

FS:

#version 330 core//噪声纹理和法向半球核心样本作为输入参数
out float FragColor;
in vec2 TexCoords;

uniform sampler2D gPositionDepth;
uniform sampler2D gNormal;
uniform sampler2D texNoise;

uniform vec3 samples[64];
uniform mat4 projection;

// 屏幕的平铺噪声纹理会根据屏幕分辨率除以噪声大小的值来决定,缩放大小
//将噪声纹理平铺(Tile)在屏幕上
const vec2 noiseScale = vec2(800.0/4.0, 600.0/4.0); // 屏幕 = 800x600

void main()
{
    [...]
    vec3 fragPos = texture(gPositionDepth, TexCoords).xyz;
    vec3 normal = texture(gNormal, TexCoords).rgb;
    vec3 randomVec = texture(texNoise, TexCoords * noiseScale).xyz;
//创建一个TBN矩阵,将向量从切线空间变换到观察空间
    vec3 tangent = normalize(randomVec - normal * dot(randomVec, normal));
    vec3 bitangent = cross(normal, tangent);
    mat3 TBN = mat3(tangent, bitangent, normal);

    float occlusion = 0.0;
    for(int i = 0; i < kernelSize; ++i)
    {
        // 获取样本位置
        vec3 sample = TBN * samples[i]; // 切线->观察空间
        sample = fragPos + sample * radius; //将它们加到当前像素位置上
//变换sample到屏幕空间,这个向量目前在观察空间,我们将首先使用projection矩阵uniform变换它到裁剪空间
    vec4 offset = vec4(sample, 1.0);
    offset = projection * offset; // 观察->裁剪空间
    offset.xyz /= offset.w; // 透视划分得标准化设备坐标
    offset.xyz = offset.xyz * 0.5 + 0.5; // 变换到0.0 - 1.0的值域
//使用offset向量的x和y分量采样线性深度纹理从而获取样本位置从观察者视角的深度值(第一个不被遮蔽的可见片段)。
    float sampleDepth = -texture(gPositionDepth, offset.xy).w;
    //检查样本的当前深度值是否大于存储的深度值,是的,添加到最终的贡献因子上
//occlusion += (sampleDepth >= sample.z ? 1.0 : 0.0);
//引入一个范围测试从而保证我们只当被测深度值在取样半径内时影响遮蔽因子

float rangeCheck = smoothstep(0.0, 1.0, radius / abs(fragPos.z - sampleDepth));
//smoothstep函数,它非常光滑地在第一和第二个参数范围内插值了第三个参数
occlusion += (sampleDepth >= sample.z ? 1.0 : 0.0) * rangeCheck;
    [...]
    }
     occlusion = 1.0 - (occlusion / kernelSize);
    
    FragColor = occlusion;
}
/

环境遮蔽模糊

在SSAO阶段和光照阶段之间,我们想要进行模糊SSAO纹理的处理,所以我们又创建了一个帧缓冲对象来储存模糊结果。

GLuint ssaoBlurFBO, ssaoColorBufferBlur;
glGenFramebuffers(1, &ssaoBlurFBO);
glBindFramebuffer(GL_FRAMEBUFFER, ssaoBlurFBO);
glGenTextures(1, &ssaoColorBufferBlur);
glBindTexture(GL_TEXTURE_2D, ssaoColorBufferBlur);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, 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, ssaoColorBufferBlur, 0);

FS:

#version 330 core
out float FragColor;

in vec2 TexCoords;

uniform sampler2D ssaoInput;

void main() 
{
    vec2 texelSize = 1.0 / vec2(textureSize(ssaoInput, 0));
    float result = 0.0;
    for (int x = -2; x < 2; ++x) 
    {
        for (int y = -2; y < 2; ++y) //遍历了周围在-2.0和2.0之间的SSAO纹理单元(Texel)
        {
            vec2 offset = vec2(float(x), float(y)) * texelSize;
            result += texture(ssaoInput, TexCoords + offset).r;
        }
    }
    FragColor = result / (4.0 * 4.0);
}  

应用环境遮蔽

应用遮蔽因子到光照方程中极其简单:我们要做的只是将逐片段环境遮蔽因子乘到光照环境分量上。

uniform sampler2D ssao;

main:
    float AmbientOcclusion = texture(ssao, TexCoords).r;
// Blinn-Phong (观察空间中)
    vec3 ambient = vec3(0.3 * AmbientOcclusion); // 这里我们加上遮蔽因子

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值