#OPENGL学习笔记之十四
2019/3/12
这节的主要内容是增加光源的属性及相应的效果变化。
现实世界中有很多种类的光照,我们把将光投射(Cast)到物体的光源叫做投光物(Light Caster)。投光物我们分为以下三种:
- 定向光(Directional Light)
- 点光源(Point Light)
- 聚光(Spotlight)
马上我们还会讨论如何将这些不同种类的光照类型整合到一个场景之中。
平行光(Directional Light)
如图所示,一组平行的光束投射到物体表面。当我们使用一个假设光源处于无限远处的模型时,它就被称为定向光,因为它的所有光线都有着相同的方向,它与光源的位置是没有关系的。定向光非常好的一个例子就是太阳,由于光的位置向量保持一致,场景中每个物体的光照计算将会是类似的。
我们在片段着色器中的light结构体基main函数中添加如下代码:
//片段着色器中
struct Light {
// vec3 position; // 使用定向光就不再需要了
vec3 direction;
vec3 ambient;
vec3 diffuse;
vec3 specular;
};
...
void main()
{
//我们目前使用的光照计算需求一个从片段至光源的光线方向,但人们更习惯定义定向光为一个从光源出发的全局方向。
//所以我们需要对全局光照方向向量取反来改变它的方向,它现在是一个指向光源的方向向量了。
//而且,记得对向量进行标准化,假设输入向量为一个单位向量是很不明智的。
vec3 lightDir = normalize(-light.direction);//对light.direction向量取反
...
}
同时,为了方便观察光照的效果,我们增加立方体的个数,在主程序中添加:
//主程序中
for(unsigned int i = 0; i < 10; i++)
{
glm::mat4 model;
model = glm::translate(model, cubePositions[i]);
float angle = 20.0f * i;
model = glm::rotate(model, glm::radians(angle), glm::vec3(1.0f, 0.3f, 0.5f));
lightingShader.setMat4("model", model);
glDrawArrays(GL_TRIANGLES, 0, 36);
}
//注意提前设置好光的方向,(注意我们将方向定义为从光源出发的方向)。
lightingShader.setVec3("light.direction", -0.2f, -1.0f, -0.3f);
定向光的参考代码在这里
点光源(Point Light)
类似于灯泡,与太阳不同的是,点光源是处于世界中某一个位置的光源,它会朝着所有方向发光,但光线会随着距离逐渐衰减。
衰减
随着光线传播距离的增长逐渐削减光的强度通常叫做衰减(Attenuation)。随距离减少光强度的一种方式是使用一个线性方程,虽然线性方程方程能够随着距离的增长线性地减少光的强度,从而让远处的物体更暗。然而,这样的线性方程通常会看起来比较假。
为了逼真的效果,我们需要一个随着距离的增加光源的亮度一开始会下降非常快,但在远处时剩余的光强度就会下降的非常缓慢的规律,有人总结了如下公式。
d代表了片段距光源的距离。接下来为了计算衰减值,我们定义3个(可配置的)项:常数项Kc、一次项Kl和二次项Kq。
- 常数项Kc通常保持为1.0,它的主要作用是保证分母永远不会比1小
- 一次项Kl会与距离值相乘,以线性的方式减少强度。
- 二次项Kq会与距离的平方相乘,让光源以二次递减的方式减少强度。二次项在距离比较小的时候影响会比一次项小很多,但当距离值比较大的时候它就会比一次项更大了。
三个数值之间的数学关系及结果我们查阅一下网站,来选择效果较好的值。
实现衰减
//在片段着色器中我们还需要三个额外的值
struct Light {
vec3 position;
vec3 ambient;
vec3 diffuse;
vec3 specular;
float constant;//常数项
float linear;//一次项
float quadratic;//二次项
};
注意我们现在不采用刚刚学的定向光部分的vec3 direction了
//主程序我们还要设置一下属性值
lightingShader.setFloat("light.constant", 1.0f);
lightingShader.setFloat("light.linear", 0.09f);
lightingShader.setFloat("light.quadratic", 0.032f);
//片段着色器中我们可以使用GLSL内建的length函数来完成计算物距离体光源的距离
float distance = length(light.position - FragPos);
float attenuation = 1.0 / (light.constant + light.linear * distance +
light.quadratic * (distance * distance));
//衰减值到光照计算中,将它分别乘以环境光、漫反射和镜面光颜色
ambient *= attenuation;
diffuse *= attenuation;
specular *= attenuation;
我们可以将环境光分量保持不变,让环境光照不会随着距离减少,但是如果我们使用多于一个的光源,所有的环境光分量将会开始叠加,所以在这种情况下我们也希望衰减环境光照。简单实验一下,看看什么才能在你的环境中效果最好。
点光源的参考代码在这里
聚光(Spotlight)
- LightDir:从片段指向光源的向量。
- SpotDir:聚光所指向的方向。
- Phiϕ:指定了聚光半径的切光角。落在这个角度之外的物体都不会被这个聚光所照亮。
- Thetaθ:LightDir向量和SpotDir向量之间的夹角。在聚光内部的话θ值应该比ϕ值小。
聚光是位于环境中某个位置的光源,它只朝一个特定方向而不是所有方向照射光线。这样的结果就是只有在聚光方向的特定半径内的物体才会被照亮,其它的物体都会保持黑暗。聚光很好的例子就是路灯或手电筒。
OpenGL中聚光是用一个世界空间位置、一个方向和一个切光角(Cutoff Angle)来表示的,切光角指定了聚光的半径(译注:是圆锥的半径不是距光源距离那个半径)。对于每个片段,我们会计算片段是否位于聚光的切光方向之间(也就是在锥形内),如果是的话,我们就会相应地照亮片段。
手电筒
//片段着色器中,我们给light结构体添加属性
struct Light {
vec3 position;//聚光的位置向量(来计算光的方向向量
vec3 direction;//聚光的方向向量
float cutOff;//切光角
...
};
//主程序中传入属性值到着色器
lightingShader.setVec3("light.position", camera.Position);
lightingShader.setVec3("light.direction", camera.Front);
//用余弦值简化角的计算
lightingShader.setFloat("light.cutOff", glm::cos(glm::radians(12.5f)));
//片段着色器main函数中
float theta = dot(lightDir, normalize(-light.direction));//取反的是因为我们想让向量指向光源而不是从光源出发
if(theta > light.cutOff) //想想为什么是大于号不是小于号?(余弦值有关)
{
// 执行光照计算
}
else // 否则,使用环境光,让场景在聚光之外时不至于完全黑暗
color = vec4(light.ambient * vec3(texture(material.diffuse, TexCoords)), 1.0);
聚光的参考代码在这里
平滑/软化边缘
上面的结果出来后,我们发现一个问题,聚光的边缘效果太锐利了,与现实效果不符。
为了创建一种看起来边缘平滑的聚光,我们需要模拟聚光有一个内圆锥(Inner Cone)和一个外圆锥(Outer Cone)。我们可以将内圆锥设置为上一部分中的那个圆锥,但我们也需要一个外圆锥,来让光从内圆锥逐渐减暗,直到外圆锥的边界。
为了创建一个外圆锥,我们只需要再定义一个余弦值来代表聚光方向向量和外圆锥向量(等于它的半径)的夹角。然后,如果一个片段处于内外圆锥之间,将会给它计算出一个0.0到1.0之间的强度值。如果片段在内圆锥之内,它的强度就是1.0,如果在外圆锥之外强度值就是0.0。
- ϵ(Epsilon)是内(ϕ)和外圆锥(γ)之间的余弦值差(ϵ=ϕ−γ)
- I值就是在当前片段聚光的强度。
我们现在有了一个在聚光外是负的,在内圆锥内大于1.0的,在边缘处于两者之间的强度值了。如果我们正确地约束(Clamp)这个值,在片段着色器中就不再需要if-else了,我们能够使用计算出来的强度值直接乘以光照分量:
//片段着色器中
float theta = dot(lightDir, normalize(-light.direction));
float epsilon = light.cutOff - light.outerCutOff;
//clamp函数,注意它的参数值
float intensity = clamp((theta - light.outerCutOff) / epsilon, 0.0, 1.0);
...
// 将不对环境光做出影响,让它总是能有一点光
diffuse *= intensity;
specular *= intensity;
...
//主程序中设置它的uniform值,别忘了在light0结构体中也要定义
lightShader.setFloat("light.outerCutOff", glm::cos(glm::radians(17.5f)));
平滑边缘的参考代码在这里