多个箱子
创建多个箱子的过程与第一章中类似,下面是主程序中的代码
glm::vec3 cubePositions[] = {
...};
glBindVertexArray(cubeVAO);
for (int i = 0; i < 10; i++)
{
glm::mat4 model = glm::mat4(1.0f);
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);
}
值得注意的是,要记得改变顶点着色器中箱子的法向量,否则它的光照将会很诡异。
void main()
{
...
Normal = mat3(transpose(inverse(model))) * aNormal;
...
}
平行光
太阳光是最常见的一种平行光,平行光通常不需要设定光源的位置,只需要知道其方向,因此我们在light结构体中添加一个方向向量。
struct Light {
// vec3 position; // 使用定向光就不再需要了
vec3 direction;
vec3 ambient;
vec3 diffuse;
vec3 specular;
};
...
void main()
{
vec3 lightDir = normalize(-light.direction);
...
}
**注意我们首先对light.direction向量取反。**我们目前使用的光照计算需求一个从片段至光源的光线方向,但人们更习惯定义定向光为一个从光源出发的全局方向。
衰减
现实中,光照强度随距离衰减,公式如下
公式中的Kc固定为1,另外两个参数根据我们需要光覆盖的距离而变化,下面是部分对照表
为了实现衰减,我们在结构体中加入这3个参数,并依照公式进行计算
struct Light {
vec3 position;
vec3 ambient;
vec3 diffuse;
vec3 specular;
float constant;
float linear;
float quadratic;
};
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;
聚光
现实中光源通常不会照亮所有方向,而是照亮一个圆锥形的区域,如上图所示
对于这种光源,既要设定光源的位置light.position,也要设定它的方向light.direction。
注意,light.direction 是光源正对的方向,即上图中的红线。它是从光源指向片段的。
lightDir 是片段到光源的方向,即上图中的黑箱,它是从片段指向光源的。
上图中的蓝线是光照的边缘,称为切光(cutoff)。而现实中,光照范围的边缘往往是羽化模糊的。所以我们又设置了一个完全变暗的边界outercutoff。
如图,light.direction与lightDir夹角的cos值记为θ,注意这两个向量方向不同,计算时要将light.direction取反。
内切光与中心夹角的cos记为light.cutoff,外切光记为light.outercutoff。
利用以下公式就可实现内外切光的过渡
取点在内切之内时,I>1,在内外切之间,1>I>0,在外切以外,I<0。
我们可以利用clamp函数,让I>1时取1,I<0时取0.
float theta = dot(lightDir, normalize(-light.direction));
float epsilon = light.cutOff - light.outerCutOff;
float intensity = clamp((theta - light.outerCutOff) / epsilon, 0.0, 1.0);
...
// 将不对环境光做出影响,让它总是能有一点光
diffuse *= intensity;
specular *= intensity;
...