Multiple lights
Lighting/Multiple-lights
在前几章中,我们学习了很多关于OpenGL中的照明的知识。我们学习了Phong阴影,材料,照明地图和不同类型的光源。在本章中,我们将结合所有之前获得的知识,通过创建一个有6个主动光源的完全照明的场景。我们将模拟一个类似太阳的光源作为方向光源,4个分散在整个场景中的点光源,我们还将添加一个手电筒。
为了在场景中使用多个光源,我们希望将照明计算封装到GLSL函数中。这样做的原因是,当我们使用多种光照类型进行光照计算时,代码很快就变得令人讨厌,每种光照类型都需要不同的计算。如果我们只在main函数中进行所有这些计算,代码很快就会变得难以理解。
GLSL中的函数就像c函数一样。我们有一个函数名,一个返回类型,如果函数还没有在main函数之前声明,我们需要在代码文件的顶部声明一个原型。我们将为每种光线类型创建一个不同的函数:方向光、点光和聚光灯。
当在一个场景中使用多个灯光时,方法通常如下:我们有一个代表片段输出颜色的单一颜色向量。对于每个光,光对片段的贡献被添加到这个输出颜色向量中。所以场景中的每一盏灯都将计算其各自的影响,并将其贡献给最终的输出颜色。一般的结构是这样的:
out vec4 FragColor;
void main()
{
// define an output color value
vec3 output = vec3(0.0);
// add the directional light's contribution to the output
output += someFunctionToCalculateDirectionalLight();
// do the same for all point lights
for(int i = 0; i < nr_of_point_lights; i++)
output += someFunctionToCalculatePointLight();
// and add others lights as well (like spotlights)
output += someFunctionToCalculateSpotLight();
FragColor = vec4(output, 1.0);
}
每个实现的实际代码可能不同,但总体结构是相同的。我们定义了几个函数来计算每个光源的影响,并将其结果颜色添加到输出颜色向量中。例如,如果两个光源靠近碎片,他们的共同贡献将导致一个更明亮的碎片被一个单一的光源点燃。
Directional light
我们想要在片段着色器中定义一个函数来计算一个方向光对相应片段的贡献:一个函数需要一些参数并返回计算出的方向照明颜色。
首先,我们需要设置一个方向光源所需的最小变量。我们可以将变量存储在一个名为DirLight的结构中,并将其定义为uniform。结构体的变量应该与前一章很相似:
struct DirLight {
vec3 direction;
vec3 ambient;
vec3 diffuse;
vec3 specular;
};
uniform DirLight dirLight;
然后我们可以将dirLight制服传递给一个函数,其原型如下:
vec3 CalcDirLight(DirLight light, vec3 normal, vec3 viewDir);
就像C和c++一样,当我们想要调用一个函数(在本例中是在主函数内部)时,函数应该在调用者的行号之前定义。在这种情况下,我们更喜欢在主函数下面定义函数,这样就不满足这个需求。因此,我们在主函数的上方声明函数的原型,就像在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);
}
我们基本上复制了前一章的代码,并使用给出的向量作为函数参数来计算方向光的贡献向量。由此产生的环境、漫反射和高光贡献,然后作为一个单一的颜色矢量返回。
Point light
与定向光类似,我们也想定义一个函数来计算一个点光对给定片段的贡献,包括它的衰减。就像方向灯一样,我们需要定义一个结构来指定点灯所需的所有变量:
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表示该特定点光源在片段上的颜色贡献。同样,一些智能复制粘贴的结果如下:
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。
Putting it all together
现在我们已经定义了方向光的函数和点光的函数,我们可以把它们放在main函数中。
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一样,尽管这一次我们在查询统一的位置时也需要定义适当的索引:
lightingShader.setFloat("pointLights[0].constant", 1.0f);
:这里,我们在pointLights数组中索引第一个PointLight结构体,并在内部检索其常量变量的位置,我们将其设置为1.0。
我们不要忘记,我们还需要为每个点光源定义一个位置向量,所以让我们在场景中展开它们。我们将定义另一个包含点光源位置的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结构体,并将其position属性设置为我们刚刚定义的位置之一。还要确保现在绘制4个轻的立方体,而不是仅仅1个。简单地为每个轻对象创建一个不同的模型矩阵,就像我们对容器所做的那样。
如果你也使用手电筒,所有灯的组合结果是这样的
正如你所看到的,天空中似乎有某种形式的全局光(比如太阳),我们在场景中有4个分散的光,从玩家的角度可以看到一个手电筒。看起来很整洁,不是吗?
您可以在这里找到最终应用程序的完整源代码here. 。
这张图片显示了我们在前几章中使用的默认灯光属性设置的所有光源,但是如果你使用这些值,你会得到非常有趣的结果。美工和关卡设计师通常会在一个大型编辑器中调整所有这些灯光变量,以确保灯光与环境相匹配。使用我们的简单环境,你已经可以创建一些非常有趣的视觉效果,只需调整灯光的属性:
我们还改变了清晰的颜色,以更好地反射光线。您可以看到,通过简单地调整一些照明参数,您可以创建完全不同的氛围。
到目前为止,您应该对OpenGL中的照明有了很好的理解。凭借目前的知识,我们已经可以创造出有趣的、视觉丰富的环境和氛围。试着尝试各种不同的价值观来创造你自己的氛围
Exercises
- Can you (sort of) re-create the different atmospheres of the last image by tweaking the light's attribute values? solution.