到目前为止,我们介绍的都是点光源。但是现实世界中,光源的类型却要相对复杂一些。大概会有这么几种形式:定向光
、点光源
、聚光
等等。
当一个光源处于很远的地方时,来自光源的每条光线就会近似于互相平行。这点很好理解,生活中我们的太阳光,就可以近似看做是平行光。定向光的特点是,光线的防线与光源的位置没有关系,他们的光线方向都是相同的。
在之前的例子中,我们都要通过光源的位置和物体上的顶点来计算光线射入的角度,从而计算漫反射分量和镜面光分量。如果是定向光,我们这直接可以拿到光线的射入角度。
这部分内容十分简单,只要将我们之前传入的用来计算光源方向的光源位置替换为光源方向直接使用即可。不过有一点需要注意的是:
我们首先对light.direction向量取反。我们目前使用的光照计算需求一个从片段至光源的光线方向,但人们更习惯定义定向光为一个从光源出发的全局方向。所以我们需要对全局光照方向向量取反来改变它的方向,它现在是一个指向光源的方向向量了。而且,记得对向量进行标准化,假设输入向量为一个单位向量是很不明智的。
这里在原教程中存在这样一个扩展:
我们一直将光的位置和位置向量定义为
vec3
,但一些人会喜欢将所有的向量都定义为vec4
。当我们将位置向量定义为一个vec4
时,很重要的一点是要将w分量设置为1.0,这样变换和投影才能正确应用。然而,当我们定义一个方向向量为vec4
的时候,我们不想让位移有任何的效果(因为它仅仅代表的是方向),所以我们将w分量设置为0.0。方向向量就会像这样来表示:
vec4(0.2f, 1.0f, 0.3f, 0.0f)
。这也可以作为一个快速检测光照类型的工具:你可以检测w分量是否等于1.0,来检测它是否是光的位置向量;w分量等于0.0,则它是光的方向向量,这样就能根据这个来调整光照计算了:if(lightVector.w == 0.0) // 注意浮点数据类型的误差
// 执行定向光照计算
else if(lightVector.w == 1.0)
// 根据光源的位置做光照计算(与上一节一样)你知道吗:这正是旧OpenGL(固定函数式)决定光源是定向光还是位置光源(Positional Light Source)的方法,并根据它来调整光照。
私以为,之所以要求w分量等于1
是因为在坐标系统这一章中,我们了解到,w分量
是用来做透视效果的。而平行光是不具备透视效果的,所以我们将其w分量设置为1。
这部分详细的内容,可以看下Demos/5.投光物/5-1.定向光中的相关代码。
点光源,就是用来描述向所有方向发光,但光线会随着距离逐渐衰减的光源。生活中,火把、灯泡等都可以看做是点光源。
之前的章节中,我们所模拟的光源都是一个简化的点光源,因为它不具备衰减的特性,这一节中,我们将着重完成光线的衰减这一部分。
随着光线传播距离的增长逐渐削弱光的强度通常叫做衰减
。现实世界中,等在近处通常会非常亮,但随着距离的增加光源的亮度一开始会下降非常快,但在远处时剩余的光强度就会下降的非常缓慢了。这里有一个公式,可以用它来就算光的衰减值:
这里d代表顶点至光源的距离,另外三个分别是系数。可以看到是一个二次函数的倒数,所以他的图像大概会是这个样子的:
你可以看到光在近距离的时候有着最高的强度,但随着距离增长,它的强度明显减弱,并缓慢地在距离大约100的时候强度接近0。这正是我们想要的。
那么如何配置这三个系数将是一个重点,影响他们的因素有很多:环境、覆盖距离、光源类型等。幸运的是Ogre3D的Wiki帮我们列出了一些合理范围的值:
常数项 | 一次项 | 二次项 | 覆盖距离 |
---|---|---|---|
1.0 | 0.7 | 1.8 | 7 |
1.0 | 0.35 | 0.44 | 13 |
1.0 | 0.22 | 0.20 | 20 |
1.0 | 0.14 | 0.07 | 32 |
1.0 | 0.09 | 0.032 | 50 |
1.0 | 0.07 | 0.017 | 65 |
1.0 | 0.045 | 0.0075 | 100 |
1.0 | 0.027 | 0.0028 | 160 |
1.0 | 0.022 | 0.0019 | 200 |
1.0 | 0.014 | 0.0007 | 325 |
1.0 | 0.007 | 0.0002 | 600 |
1.0 | 0.0014 | 0.000007 | 3250 |
在我们的环境中,32到100的距离对大多数的光源都足够了。
做了上述的准备工作,实际上衰减实现起来比较简单。只要先计算出衰减值后,在三个分量上各自乘以衰减值即可。这里的代码跟上节的定向光不同,而是我们上章中的点光源。实现起来比较简单,这里直接放代码:
///改造Light结构体
struct Light {
vec3 position;
vec3 ambient;
vec3 diffuse;
vec3 specular;
float constant;
float linear;
float quadratic;
};
///配置着色器程序
lightingShader.setFloat("light.constant", 1.0f);
lightingShader.setFloat("light.linear", 0.09f);
lightingShader.setFloat("light.quadratic", 0.032f);
///片段着色器
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;
你可以看到,只有前排的箱子被照亮的,距离最近的箱子是最亮的。后排的箱子一点都没有照亮,因为它们离光源实在是太远了。
这部分详细的内容,可以看下Demos/5.投光物/5-2.点光源中的相关代码。
我们来说一下我们将介绍的最后一种类型的光,聚光
。
聚光是位于环境中某个位置的光源,它只朝一个特定方向而不是所有方向照射光线。这样的结果就是只有在聚光方向的特定半径内的物体才会被照亮,其它的物体都会保持黑暗。聚光很好的例子就是路灯或手电筒。
根据上面的图片我们知道,我们只要确定设置顶点的光线与聚光方向的夹角是否在聚光夹角中即可。
LightDir
:从片段指向光源的向量。SpotDir
:聚光所指向的方向。Phiϕ
:指定了聚光半径的切光角。落在这个角度之外的物体都不会被这个聚光所照亮。Thetaθ
:LightDir向量和SpotDir向量之间的夹角。在聚光内部的话θ值应该比ϕ值小。
所以我们要做的就是计算LightDir向量和SpotDir向量之间的点积,它会返回两个单位向量夹角的余弦值,并将它与切光角ϕ值对比。你现在应该了解聚光究竟是什么了,下面我们将以手电筒的形式创建一个聚光。
手电筒(Flashlight)是一个位于观察者位置的聚光,通常它都会瞄准玩家视角的正前方。基本上说,手电筒就是普通的聚光,但它的位置和方向会随着玩家的位置和朝向不断更新。
这里思路很简单,之前我们封装过摄像机类,在摄像机类中,我们的Front
向量会随着玩家的位置和朝向不断的更新,他正是我们所需要的聚光方向。
我们改造我们的Light结构体:
struct Light {
vec3 position;
vec3 direction;
float cutOff;
...
};
接下来我们将合适的值传到着色器中:
ourShader.setFloat("light.cutOff", glm::cos(glm::radians(12.5f)));
我们看到这里我们传入的是我们内圆锥的角度余弦值,这是因为如果传角度的话,在片段着色器中我们是根据光照方向和顶点到光源的方向做点乘求向量的夹角余弦值的,所以我们直接将cutOff也转变为夹角余弦值来减少片段着色器中的计算量。
这时候,我们的片段着色器中大概是这个样子的:
float theta = dot(lightDir, normalize(-light.direction));
if (theta > light.cutOff) {
///做正常的渲染
} else {
///仅保留环境分量
}
这时,我们看大超出内圆锥的部分已经是灰暗的一片了,手电筒效果基本已经完成。但是我们还可以进一步完善它,一个真实的聚光将会在边缘处逐渐减少亮度。(当然这取决于你希望俊光边缘的展现方式)
为了创建逐渐变暗的效果,我们还要指定一个外圆锥,然后让光线的镜面光分量和漫反射分量的作用强度在这之间做插值运算即可。思路很简单,我们直接看下代码:
///片段着色器
struct Light {
vec3 direction;
vec3 position;
vec3 ambient;
vec3 diffuse;
vec3 specular;
float cutOff;
float outerCutOff;
float constant;
float linear;
float quadratic;
};
///环境光照
vec3 ambientColor = light.ambient * vec3(texture(material.diffuse, TexCoord));
///计算到顶点光线方向
vec3 norm = normalize(normal);
vec3 lightDir = normalize(light.position - fragPosition);
///计算衰减系数r
float distance = length(light.position - fragPosition);
float attenuationFactor = 1.0 / (light.constant + light.linear * distance +
light.quadratic * (distance * distance));
float theta = dot(lightDir, normalize(-light.direction));
float smoothFactor = clamp((theta - light.outerCutOff)/(light.cutOff - light.outerCutOff),0.0,1.0);
///漫反射光照
float factor = max(dot(norm,lightDir),0.0);
vec3 diffuseColor = light.diffuse * factor * vec3(texture(material.diffuse, TexCoord));
///镜面反射光照
vec3 reflectDir = reflect(-lightDir, norm);
vec3 viewDir = normalize(viewPosition - fragPosition);
float spec = pow(max(dot(viewDir, reflectDir), 0.0),material.shininess);
vec3 specularColor = light.specular * spec * vec3(texture(material.specular, TexCoord));
///颜色合成
FragColor = vec4((ambientColor + (diffuseColor + specularColor) * smoothFactor) * attenuationFactor , 1.0);
其中clamp函数是释义如下:
float clamp(a,b,c),将a转换为介于[b,c]之间的值。
现在你已经有了一个完美的手电筒了:
这部分详细的内容,可以看下Demos/5.投光物/5-3.手电筒中的相关代码。