在前面的教程中,我们学习了很多关于OpenGL中光照的只是——Phong光照模型,材质,光照映射和不同的光照效果。在本次的教程中,我们将会学到如何将前面学到的各种光源组合到一起——将六种光源放置在同一个场景中。它们分别是一个类似于太阳的平行光源,四个分散在场景中的点光源,还有一个聚光灯(上一个教程中讲的聚光灯)。
为了在场景中使用多个光源,我们能要先写我们自己的GLSL功能函数。这样做的原因不说自明:如果所有光照效果的计算函数都在main函数中来写,会很乱,而且复杂难懂。
GLSL中的函数和C函数十分相似。函数名、返回值等,同样地,在使用之前需要首先声明。在下面的代码中,我们将会为每种光源创建一个实现其功能的函数。
在一个场景中使用不同的光源的方法大致可以描述如下:我们有一个颜色向量代表片段的输出颜色。对每种光源,它对这个片段的贡献值都会叠加在最后的颜色向量上。所以,每种光源都需要首先单独计算出其颜色分量,然后再将它们整合产生最后的整体结果。下面的代码展示了整体的流程:
out vec4 color;
void main()
{
// define an output color value
vec3 output;
// 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();
color = vec4(output, 1.0);
}
实际的代码可能和上面的过程不太一致,但是本质上还是相同的。我们为上面的每种光源定义了计算的函数并且将每种光源的计算结果叠加到最终输出的颜色向量上。所以场景中的每个片段的最终颜色效果都是场景中所有光源共同作用产生的效果。
我们想要做的是在片段处理程序中定义一个对给定片段计算平行光对其产生的作用效果的函数:它根据相关参数的输入计算最终产生的平行光的作用效果的颜色向量。
首先,我们需要设定为了完成平行光作用效果计算所需要的参数。我们把它们组织成如下所示的DirLight结构体中。并且声明一个这个结构体类型的uniform类型的变量:
struct DirLight {
vec3 direction;
vec3 ambient;
vec3 diffuse;
vec3 specular;
};
uniform DirLight dirLight;
然后我们可以将一个DirLight类型的变量当做一个函数的参数,像下面这种形式:
vec3 CalcDirLight(DirLight light, vec3 normal, vec3 viewDir);
上面声明的函数中,函数名是CalcDirLight返回一个vec3类型的向量,它需要传递三个参数,一个是DirLight类型的结构体变量,另外两个是vec3类型的向量。下面是它的函数体实现:
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);
}
上面的代码和前面教程中实现平行光效果的代码一致,只是这里我们将其封装成了一个函数,具体的过程是:分别计算环境光、漫反射光和镜面反射光,并将其叠加后返回。
对于点光源的实现过程和上面平行光源的实现过程相类似,我们首先也为其定义一个计算点光源作用效果所需参数的结构体PoinLight,其中还包括了实现衰减效果所需要的参数。如下所示:
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];
如你所见,我们利用一条预编译指令定义了场景中我们想要实现的点光源的数量。然后利用这个数值创建了一个PointLight数组,这和C中的代码风格还是几乎一致的。现在,我们有四个点光源数据了。
点光源的计算函数声明如下:
vec3 CalcPointLight(PointLight light, vec3 normal, vec3 fragPos, vec3 viewDir);
而其具体的实现也是和之前的教程大致相同:
// Calculates the color when using a point light.
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.0f / (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);
}
这样的抽象和以点光源数组形式的实现能够做到充分的代码复用和解耦。
上面我们已经定义了平行光和点光源,现在让我们按照之前介绍的流程将它们组合起来吧:
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);
color = vec4(result, 1.0);
}
正如前面所说,每种光源都对最终的效果有自己的贡献值。场景中每个片段的最终效果是所有光源共同作用产生的效果。上面还说到可以添加一个聚光灯,就是上次教程中实现的手电筒,这个作者留作练习了。
接下来就是为片段处理程序中我们定义的那些uniform类型的变量(一个DirLight类型的结构体和一个PointLight数组)赋值了,这是在OpenGL程序中完成的,就像之前使用的当时一样,这次唯一的不同就是,我们现在要为数组中的元素进行赋值。这该怎么做?
幸运的是,这不是太繁。只需要在指定要被赋值的uniform名称的时候按照具体的数组形式来指定就好了,像下面这样:
glUniform1f(glGetUniformLocation(lightingShader.Program, "pointLights[0].constant"), 1.0f);
只不过这行代码类似的代码行很多,因为我们要为我们设定的每一个uniform光源变量的每个属性都进行相应的赋值操作。但好像也只能这么做。
另外,我们还需要指定这四个点光源的位置,不能让它们在同一个位置,否则设置一个就够了。所以我们定义了一个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)
};
还有就是,我们需要在场景中绘出4个代表点光源的小立方体,这在模型矩阵中类似前面的做法就好了。
最终的效果是这样的:
如你所见,场景中有类似于太阳的平行光光源,并且有四个点光源还有一个从观察者照向场景的聚光灯(手电筒)。看上去还行吧?
源代码在这儿,包括 , 和 。
作者建议可以将代码进行属性值的修改以得到不同的效果。下面是一些效果:
到目前为止,我们已经学习了光宇OpenGL光照的很多内容,并且对其有了很好的理解。具备了这些知识,我们已经可以创建出非常有趣的场景了。