写在前面
上一节光照中使用材质和lighting maps介绍了使用材质属性和lighting maps使物体的光照效果能反映物体的材料特性,看起来更逼真。在前面的章节中使用的实际上都是一个点光源,本节将学习其他几种光源类型,以及在场景中使用多个光源。本节代码均可以在我的github下载。
本节内容整理自:
1.www.learnopengl.com light casters
2.www.learnopengl.com Multiple lights
通过本节可以了解到
在前面章节中,我们通过位置和成分分量大小来指定光源属性,这个光源实际上是一个点光源。除了点光源外,还包括方向光源,聚光灯光源等其他类型的光源,他们的特点如下图所示(来自Simple Lighting):
使用不同的光源主要是为了模拟现实环境中不同类型的光,使场景光照效果更能满足需求。每种类型的光源各有其特点,下面予以介绍。
方向光源的特点就是光的方向几乎都平行,只有一个方向,这是为了模拟光源在无限远处的情景,例如太阳光。方向光源一般不考虑光的衰减,它与光源具体位置无关,我们只需要为它指定方向即可。注意一般我们指定方向光源的方向时,习惯从光源指向物体,而在计算光照时,又需要从物体指向光源的方向,因此需要做一个翻转。指定方向光源的结构体在着色器中定义为:
// 方向光源属性结构体
struct DirLightAttr
{
vec3 direction; // 方向光源
vec3 ambient;
vec3 diffuse;
vec3 specular;
};
在计算光照时,我们不必利用物体的位置和光源位置计算光源方向了,
// 不再需要
vec3 lightDir = normalize(light.position - FragPos);
直接使用这个direction即可:
vec3 lightDir = normalize(-light.direction); // 翻转方向光源的方向
在着色器中计算光照的效果与上一节的光照计算是相同的:
void main()
{
// 环境光成分
vec3 ambient = light.ambient * vec3(texture(material.diffuseMap, TextCoord));
// 漫反射光成分 此时需要光线方向为指向光源
vec3 lightDir = normalize(-light.direction); // 翻转方向光源的方向
vec3 normal = normalize(FragNormal);
float diffFactor = max(dot(lightDir, normal), 0.0);
vec3 diffuse = diffFactor * light.diffuse * vec3(texture(material.diffuseMap, TextCoord));
// 镜面反射成分 此时需要光线方向为由光源指出
float specularStrength = 0.5f;
vec3 reflectDir = normalize(reflect(-lightDir, normal));
vec3 viewDir = normalize(viewPos - FragPos);
float specFactor = pow(max(dot(reflectDir, viewDir), 0.0), material.shininess);
vec3 specular = specFactor * light.specular * vec3(texture(material.specularMap, TextCoord));
vec3 result = ambient + diffuse + specular;
color = vec4(result , 1.0f);
}
另外还需要在主程序中指定光源的方向如下:
GLint lightDirLoc = glGetUniformLocation(shader.programId, "light.direction");
glm::vec3 lampDir(0.5f, 0.8f, 0.0f);
glUniform3f(lightDirLoc, lampDir.x, lampDir.y, lampDir.z); // 方向光源
使用方向光源的效果就是,场景中无论远近,物体得到的光照都一样,光从同一个平行的方向射向物体表面,效果如下图所示:
在前面两节使用的光源都是一个简单的点光源,场景中物体不管离光源位置远近得到的光照强度都相同,这一点与实际不相符合。实际中的点光源向各个方向发射光,但是物体与光源的距离 d 增大时光照的强度将会减弱。我们需要模拟这个特点来是点光源更加逼真。光照强度的衰减系数 Fatt 与距离 d 之间的关系如何确定呢? 如果简单的使用线性函数,距离稍微远点的物体光照强度减小得太过于明显,不符合实际情况,因此一般要考虑使用二次函数。可以定义光照强度的衰减系数 Fatt 与距离 d 之间的关系如下式:
其中 Kc 表示常系数,当 d=0 时, Fatt=1 表示没有衰减,这时光照强度最大; Kl 表示线性衰减系数, Kq 表示二次衰减系数。使用上述公式计算光照的衰减时,大致的走势如下图所示(来自www.learnopengl.com):
可以看出当距离较近时光照强度较大,当距离超过一定范围后光照强度就很弱了,光照强度的较小不是直线型的,而是曲线型的,这样更符合实际情形。
调整上述衰减系数以模拟逼真的光照效果,需要仔细玩耍这些参数,是一件需要经验的工作,Point Light Attenuation给出了一些参考系数,如下:
Range | Constant | Linear | Quadratic |
---|---|---|---|
3250 | 1.0 | 0.0014 | 0.000007 |
600 | 1.0 | 0.007 | 0.0002 |
325 | 1.0 | 0.014 | 0.0007 |
200 | 1.0 | 0.022 | 0.0019 |
160 | 1.0 | 0.027 | 0.0028 |
100 | 1.0 | 0.045 | 0.0075 |
65 | 1.0 | 0.07 | 0.017 |
50 | 1.0 | 0.09 | 0.032 |
32 | 1.0 | 0.14 | 0.07 |
20 | 1.0 | 0.22 | 0.20 |
13 | 1.0 | 0.35 | 0.44 |
7 | 1.0 | 0.7 | 1.8 |
上面的表格中Range表示光照范围,后面三列表示使用Range时需要设定的光照衰减系数。我们这里取用50对应的数值就好了。在主程序中设置衰减系数如下:
GLint attConstant = glGetUniformLocation(shader.programId, "light.constant");
GLint attLinear = glGetUniformLocation(shader.programId, "light.linear");
GLint attQuadratic = glGetUniformLocation(shader.programId, "light.quadratic");
// 设置衰减系数
glUniform1f(attConstant, 1.0f);
glUniform1f(attLinear, 0.09f);
glUniform1f(attQuadratic, 0.032f);
在着色器中对应的点光源结构体更新为:
// 点光源属性结构体
struct PointLightAttr
{
vec3 position;
vec3 ambient;
vec3 diffuse;
vec3 specular;
float constant; // 衰减常数
float linear; // 衰减一次系数
float quadratic; // 衰减二次系数
};
计算光照强度后乘以使用衰减系数:
// 计算衰减因子
float distance = length(light.position - FragPos); // 在世界坐标系中计算距离 float attenuation = 1.0f / (light.constant + light.linear * distance + light.quadratic * distance * distance); vec3 result = (ambient + diffuse + specular) * attenuation;
color = vec4(result , 1.0f);
使用有衰减的点光源效果如下图所示:
这里我们看到红色箭头指向的物体随着与光源距离不同光照有明暗不同的效果。
聚光灯光源的特点是光只在一个指定的范围内发散,如下图所示(来自www.learnopengl.com):
注意指定聚光灯指定了3个方面:
在计算聚光灯的光照效果时需要计算的量包括:
聚光灯的特点是,当夹角 θ <= ϕ 时物体接受到光照,当 θ > ϕ 物体落在聚光灯的照明范围外,将得不到光照。
在着色器中定义聚光灯的结构体为:
// 聚光灯光源属性结构体
struct SpotLightAttr
{
vec3 position; // 聚光灯的位置
vec3 direction; // 聚光灯的spot direction
float cutoff; // 聚光灯张角的余弦值
vec3 ambient;
vec3 diffuse;
vec3 specular;
float constant; // 衰减常数
float linear; // 衰减一次系数
float quadratic; // 衰减二次系数
};
注意聚光灯在传递张角时,使用了一个技巧,即传递夹角的余弦值而不是角度值。对于 cos 函数,在 [0,π2] 时函数递减,如下图左边部分所示:
那么当 θ <= ϕ 时,有 cos(θ)>=cos(ϕ) ,这一点在着色器中将会利用到。
在主程序中设置参数的方法同上述方向光源和点光源一样,这里不再赘述。在着色器中计算聚光灯效果的实现思路为:
void main()
{
// 环境光成分
vec3 lightDir = normalize(light.position - FragPos);
// 光线与聚光灯spotDir夹角余弦值
float theta = dot(lightDir,normalize(-light.direction));
if(theta > light.cutoff)
{
// 在聚光灯张角范围内 计算漫反射光成分 镜面反射成分
}
else
{
// 不在张角范围内时只有环境光成分
}
}
其中计算漫反射和镜面反射同点光源的计算是一样的,也需要考虑光照强度衰减的因素。
如果将聚光灯的位置定义为观察者所在位置,将聚光灯的光轴方向定义为观察者的观察正向,那么就可以实现手电筒的效果。当观察者在场景中移动时,手电筒发出光位置也随着改变,如下图所示:
上述十年的手电筒效果,存在一个缺陷,就是当物体超过手电筒这个聚光灯模型的张角时,场景立马变暗了,这个与实际情形不符合。实际拿着手电筒时,物体不再张角范围内时是逐渐变暗的,而这里实现的手电筒的边缘部分带有很明显的变暗的感觉。可以为聚光灯模型指定两个张角, ϕ 用于内张角余弦值, r 用于外张角余弦值,定义光照强度为:
我们需要定义一个函数来实现这个光照强度的计算,如下图中右半部分所示:
并结合OpenGL提供的clamp函数来实现,clamp函数定义如下:
API genType clamp( genType x,
genType minVal,
genType maxVal);
这个函数将值x截断到minVal和maxVal之间。
这里我们使用一个形如如下形式的函数:
// 计算内外张角范围内的强度
float theta = dot(lightDir, normalize(-light.direction));// 光线与聚光灯spotDir夹角余弦值
float epsilon = light.cutoff - light.outerCutoff;
float intensity = clamp((theta - light.outerCutoff) / epsilon, 0.0, 1.0); // 引入聚光灯内张角和外张角后的强度值
diffuse *= intensity;
specular *= intensity;
改进时,对漫反射光和镜面光成分乘以通过内外张角计算出来的强度系数,其余部分不变。通过增加的这个intensity来计算光照后,手电筒效果如下:
从上图可以看出,改进之后从内张角到外张角之间这部分逐渐变暗,这样的效果更佳符合实际手电筒的效果。
上面学习了方向光源、点光源以及聚光灯光源,我们可以将他们应用到一个场景中。在着色器中,定义计算场景中各个不同类型光照效果的函数如下:
// 计算光源效果的函数声明 包括方向、点、聚光灯光源3种实现
vec3 calculateDirLight(DirLightAttr light, vec3 fragNormal, vec3 fragPos,vec3 viewPos);
vec3 calculatePointLight(PointLightAttr light, vec3 fragNormal, vec3 fragPos,vec3 viewPos);
vec3 calculateSpotLight(SpotLightAttr light,vec3 fragNormal, vec3 fragPos, vec3 viewPos);
定义多个光源:
uniform DirLightAttr dirLight; // 方向光源
#define POINT_LIGHT_NUM 4
uniform PointLightAttr pointLights[POINT_LIGHT_NUM]; // 定义点光源数组
uniform SpotLightAttr spotLight; // 聚光灯光源
则计算总的光照效果的函数实现为:
void main()
{
vec3 result = calculateDirLight(
dirLight, FragNormal,
FragPos, viewPos);
for(int i = 0; i < POINT_LIGHT_NUM; ++i)
{
result += calculatePointLight(
pointLights[i],FragNormal,
FragPos, viewPos);
}
result += calculateSpotLight(
spotLight, FragNormal,
FragPos, viewPos);
color = vec4(result , 1.0f);
}
实现函数calculatexxxLight的方法同上面讲到的一样,是对相关类型光源光照计算的一个函数封装,这里不再赘述。需要注意的是点光源包含一个数组,设置数组中每个光源时,需要在主程序中使用数组的索引方式,例如:
GLint lightAmbientLoc = glGetUniformLocation(
shader.programId,"pointLights[0].ambient");
glUniform3f(lightAmbientLoc, 0.0f, 0.1f, 0.4f);
根据需要调整各个多个光源的参数值后,实现的一个蓝色调效果为:
本节学习了三种光源类型,以及如何实现光照的计算,并在最后将多个类型的光源应用到了同一个场景中。到目前为止,我们已经学习了基本的光照,能够实现一些理想的效果了,后面还会继续学习光照的高级技巧。不过下一节,我们打算学习加载模型的方法,使场景中的物体更丰富。