英文原文:https://learnopengl.com/Lighting/Multiple-lights
在前面的章节中,我们学到了很多关于 OpenGL 光照的知识。 我们了解了 Phong 着色、材质、光照贴图和不同类型的光投射器。 在本章中,我们将通过创建一个具有 6 个活动光源的全光照场景来结合之前获得的所有知识。 我们将模拟一个类似太阳的光作为定向光源,4 个点光源散布在整个场景中,我们还将添加一个手电筒。
为了在场景中使用多个光源,我们希望将光照计算封装到 GLSL 函数中。 这样做的原因是,当我们使用多种光源类型进行光照计算时,代码很快就会变得令人讨厌,每种光源类型都需要不同的计算。 如果我们只在主函数中进行所有这些计算,代码很快就会变得难以理解。
GLSL 中的函数就像 C 函数。 我们有一个函数名,一个返回类型,如果在 main 函数之前还没有声明函数,我们需要在代码文件的顶部声明一个原型。 我们将为每种光类型创建不同的功能:平行光、点光和聚光灯。
当在一个场景中使用多个光源时,方法通常如下:我们有一个单一的颜色向量来表示片段的输出颜色。 对于每个光,光对片段的贡献被添加到这个输出颜色向量。 因此场景中的每盏灯都会计算其单独的影响并将其贡献给最终输出颜色。 一般结构看起来像这样:
out vec4 FragColor;
void main()
{
// 定义输出颜色值
vec3 output = vec3(0.0);
// 添加定向光对输出的贡献
output += someFunctionToCalculateDirectionalLight();
// 对所有点光源做同样的事情
for(int i = 0; i < nr_of_point_lights; i++)
output += someFunctionToCalculatePointLight();
// 并添加其他灯(如聚光灯)
output += someFunctionToCalculateSpotLight();
FragColor = vec4(output, 1.0);
}
实际代码可能会因实现而异,但总体结构保持不变。 我们定义了几个函数来计算每个光源的影响并将其结果颜色添加到输出颜色向量中。 例如,如果两个光源靠近片段,则与由单个光源照亮的片段相比,它们的组合贡献将导致更明亮的片段。
定向光
我们想在片段着色器中定义一个函数来计算定向光对相应片段的贡献:一个接受一些参数并返回计算出的定向照明颜色的函数。
首先,我们需要设置定向光源所需的最低限度的变量。 我们可以将变量存储在一个名为 DirLight 的结构中,并将其定义为一个 uniform。 该结构的变量应该在上一章中很熟悉:
struct DirLight {
vec3 direction;
vec3 ambient;
vec3 diffuse;
vec3 specular;
};
uniform DirLight dirLight;
然后我们可以将 dirLight uniform 传递给具有以下原型的函数:
vec3 CalcDirLight(DirLight light, vec3 normal, vec3 viewDir);
就像 C 和 C++ 一样,当我们想要调用一个函数时(在本例中是在 main 函数中),该函数应该在调用者的行号之前的某处定义。 在这种情况下,我们更愿意在 main 函数下面定义函数,这样这个要求就不成立了。 因此我们在 main 函数之上的某处声明函数的原型,就像我们在 C 中所做的那样。
您可以看到该函数需要一个 DirLight 结构和其计算所需的另外两个向量。 如果你成功完成了上一章,那么这个函数的内容就不足为奇了:
vec3 CalcDirLight(DirLight light, vec3 normal, vec3 viewDir)
{
vec3 lightDir = normalize(-light.direction);
// diffuse shading
float diff = max(dot(normal, lightDir), 0.0);
// specular shading
vec3 reflectDir = reflect(-lightDir, normal);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
// combine results
vec3 ambient = light.ambient * vec3(texture(material.diffuse, TexCoords));
vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, TexCoords));
vec3 specular = light.specular * spec * vec3(texture(material.specular, TexCoords));
return (ambient + diffuse + specular);
}
我们基本上复制了上一章的代码,并使用作为函数参数给出的向量来计算定向光的贡献向量。 所产生的环境、漫反射和镜面反射贡献然后作为单个颜色向量返回。
点光源
与定向光类似,我们还想定义一个函数来计算点光源对给定片段的贡献,包括它的衰减。 就像方向灯一样,我们想要定义一个结构体来指定点光源所需的所有变量:
struct PointLight {
vec3 position;
float constant;
float linear;
float quadratic;
vec3 ambient;
vec3 diffuse;
vec3 specular;
};
#define NR_POINT_LIGHTS 4
uniform PointLight pointLights[NR_POINT_LIGHTS];
如您所见,我们在 GLSL 中使用了预处理器指令来定义我们希望在场景中拥有的点光源数量。 然后我们使用这个 NR_POINT_LIGHTS 常量来创建一个 PointLight 结构数组。 GLSL 中的数组就像 C 数组一样,可以通过使用两个方括号来创建。 现在我们有 4 个 PointLight 结构来填充数据。
点光源的函数原型如下:
vec3 CalcPointLight(PointLight light, vec3 normal, vec3 fragPos, vec3 viewDir);
该函数将它需要的所有数据作为参数,并返回一个 vec3,表示该特定点光源对片段的颜色贡献。 同样,一些智能复制和粘贴会产生以下功能:
vec3 CalcPointLight(PointLight light, vec3 normal, vec3 fragPos, vec3 viewDir)
{
vec3 lightDir = normalize(light.position - fragPos);
// diffuse shading
float diff = max(dot(normal, lightDir), 0.0);
// specular shading
vec3 reflectDir = reflect(-lightDir, normal);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
// attenuation
float distance = length(light.position - fragPos);
float attenuation = 1.0 / (light.constant + light.linear * distance +
light.quadratic * (distance * distance));
// combine results
vec3 ambient = light.ambient * vec3(texture(material.diffuse, TexCoords));
vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, TexCoords));
vec3 specular = light.specular * spec * vec3(texture(material.specular, TexCoords));
ambient *= attenuation;
diffuse *= attenuation;
specular *= attenuation;
return (ambient + diffuse + specular);
}
在这样的函数中抽象出此功能的优点是我们可以轻松计算多个点光源的照明,而无需重复代码。 在 main 函数中,我们简单地创建一个循环遍历点光源阵列,为每个点光源调用 CalcPointLight。
把它们放在一起
现在我们已经定义了平行光的函数和点光源的函数,我们可以将它们放在主函数中。
void main()
{
// properties
vec3 norm = normalize(Normal);
vec3 viewDir = normalize(viewPos - FragPos);
// phase 1: Directional lighting
vec3 result = CalcDirLight(dirLight, norm, viewDir);
// phase 2: Point lights
for(int i = 0; i < NR_POINT_LIGHTS; i++)
result += CalcPointLight(pointLights[i], norm, FragPos, viewDir);
// phase 3: Spot light
//result += CalcSpotLight(spotLight, norm, FragPos, viewDir);
FragColor = vec4(result, 1.0);
}
每种光源类型都会将其贡献添加到生成的输出颜色中,直到处理完所有光源。 生成的颜色包含场景中所有光源组合的颜色影响。 我们将 CalcSpotLight 函数作为练习留给读者。
这种方法中有很多重复的计算分布在光类型函数上(例如计算反射向量、漫反射和镜面反射项,以及对材质纹理进行采样),因此这里有优化的空间。
为定向光结构设置 uniform 应该不会太陌生,但是您可能想知道如何设置点光源的 uniform 值,因为点光源 uniform 实际上是一个 PointLight 结构数组。 这不是我们之前讨论过的事情。
对我们来说幸运的是,它并不太复杂。 设置结构数组的 uniform 值就像设置单个结构的 uniform 一样,尽管这次我们还必须在查询 uniform 的位置时定义适当的索引:
lightingShader.setFloat("pointLights[0].constant", 1.0f);
这里我们索引 pointLights 数组中的第一个 PointLight 结构,并在内部检索其常量变量的位置,我们将其设置为 1.0。
别忘了我们还需要为 4 个点光源中的每一个定义一个位置向量,所以让我们将它们散布在场景周围。 我们将定义另一个包含点光源位置的 glm::vec3 数组:
glm::vec3 pointLightPositions[] = {
glm::vec3( 0.7f, 0.2f, 2.0f),
glm::vec3( 2.3f, -3.3f, -4.0f),
glm::vec3(-4.0f, 2.0f, -12.0f),
glm::vec3( 0.0f, 0.0f, -3.0f)
};
然后我们从 pointLights 数组中索引相应的 PointLight 结构,并将其位置属性设置为我们刚刚定义的位置之一。 还要确保现在绘制 4 个光立方体而不是 1 个。只需为每个光对象创建一个不同的模型矩阵,就像我们对容器所做的那样。
如果您还使用手电筒,所有组合灯的结果如下所示:
如您所见,天空某处似乎有某种形式的全局光(如太阳),我们有 4 盏灯散布在整个场景中,并且从玩家的角度可以看到手电筒。 看起来很整洁不是吗?
您可以在此处找到最终应用程序的完整源代码。
该图像显示了我们在前面章节中使用的默认灯光属性设置的所有光源,但是如果您尝试使用这些值,您会得到非常有趣的结果。 美术师和关卡设计师通常会在大型编辑器中调整所有这些照明变量,以确保照明与环境相匹配。 使用我们简单的环境,您已经可以通过调整灯光的属性来创建一些非常有趣的视觉效果:
我们还更改了透明颜色以更好地反映光照。 可以看到,通过简单地调整一些光照参数就可以营造出完全不同的氛围。
到目前为止,您应该对 OpenGL 中的光照有了很好的理解。 凭借目前的知识,我们已经可以创造有趣且视觉丰富的环境和氛围。 尝试使用所有不同的值来营造您自己的氛围。