OpenGL #08 Lighting maps
在上一节中,我们学习到了每一个物体都应该有属于自己的材质(Materials),但这远远不够,一个物体的任意一部分对光的反射方式都是相同的吗?显然不是,一个物体本身也会包含各种材质,例如一个边缘被镶了铁框的木箱,铁制部分的镜面反射肯定比木制部分要强很多。不同的部分对光的处理方式不一,这使得我们需要扩展之前的系统,引入漫反射贴图和镜面光贴图(map)。这允许我们对物体的漫反射分量(以及间接地对环境光分量,它们几乎总是一样的)和镜面光分量有着更精确的控制。
漫反射贴图
要想实现一个物体不同部分能有不同的漫反射光照处理,即对物体的漫反射分量有着精确的控制,我们应该需要对这个物体的每一个片段都单独设置其漫反射颜色,那么如何根据片段在物体上的位置来确定这个位置的漫反射颜色呢?这其实跟我们之前学过的纹理那一节有很大的相似之处,纹理是为了获取该片段所在位置的颜色,而漫反射贴图获取的是漫反射颜色。原理上是类似的,且操作起来也是类似的。
为此,我们需要一张物体表面图来进行试验,就拿刚才举的例子,一个有钢边框的木箱:
上一节中,我们为物体定义了一个材质结构体,里面存放的是进行三种光照处理时应该表现的颜色值,现在我们要让物体在不同的部分表现不同的漫反射颜色,所以需要漫反射这个变量的类型,为
sampler2D
:
struct Material{
sampler2D diffuse;
vec3 specular;
float shininess;
};
由于漫反射光照和环境光照的要表现的颜色其实是一样的,只是光照来源不一样,所以把原本的diffuse
和ambient
合并为sampler2D diffuse;
。sampler2D
是一种存储纹理数据的类型。
片段着色器除了要获得纹理数据意外,还要获得纹理坐标,然后使用函数从纹理采样片段的漫反射颜色值:
#version 330 core
...
struct Material{
sampler2D diffuse;
vec3 specular;
float shininess;
};
...
in vec2 texCoords;
void main()
{
float ambientStrength = 0.1;
vec3 ambient = light.ambient * texture(material.diffuse, texCoords).rgb;
...
vec3 diffuse = light.diffuse * diff * texture(material.diffuse, texCoords).rgb;
...
}
in vec2 texCoords;
,我们需要在从顶点着色器中把纹理坐标传递给片段着色器,而顶点着色器的纹理坐标,可以通过顶点链接方式传递,最重要的,就是要在主函数中提供纹理坐标,更新后的顶点数据可以在这里找到。在顶点着色器里定义一个接收纹理坐标的顶点属性:
#version 330 core
layout (location = 6) in vec3 aPos;
layout (location = 7) in vec3 aNormal;
layout (location = 8) in vec2 aTextCoord;
...
out vec2 texCoords;
void main()
{
...
texCoords = aTextCoord;
}
然后顶点链接方式要做出一定的调整:
//链接顶点属性
glVertexAttribPointer(6, 3, GL_FLOAT, GL_FALSE, sizeof(float) * 8, (void*)0);
glEnableVertexAttribArray(6);
glVertexAttribPointer(7, 3, GL_FLOAT, GL_FALSE, sizeof(float) * 8, (void*)(sizeof(float) * 3));
glEnableVertexAttribArray(7);
glVertexAttribPointer(8, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 8, (void*)(sizeof(float) * 6));
glEnableVertexAttribArray(8);
纹理坐标有了,还差纹理数据,从之前学习纹理流程来看,现在需要加载纹理图,然后把数据传递给sampler2D diffuse;
。
//加载纹理图
unsigned int textureBuffer3;
textureBuffer3 = LoadImageToGPU("container2.png", GL_RGBA, GL_RGBA, 6);
LoadImageToGPU
只是把之前在学习纹理时的加载图片流程封装成一个函数而已:
unsigned int LoadImageToGPU(const char* imageName, GLint internalFormat, GLenum format, int textureSlot) {
unsigned int textureBuffer;
glGenTextures(1, &textureBuffer);
glActiveTexture(GL_TEXTURE0 + textureSlot);
glBindTexture(GL_TEXTURE_2D, textureBuffer);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
int width, height, nrChannels;
unsigned char *data = stbi_load(imageName, &width, &height, &nrChannels, 0);
glTexImage2D(GL_TEXTURE_2D, 0, internalFormat, width, height, 0, format, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
stbi_image_free(data);
return textureBuffer;
}
由于我在加载纹理图时,把数据放在了6号通道上,所以要指引sampler2D diffuse;
去6号管道里拿纹理数据:shader.setInt("material.diffuse", 6);
,然后记得在渲染之前激活通道,并把数据灌进通道里:
//在渲染循环里
// bind diffuse map
glActiveTexture(GL_TEXTURE6);
glBindTexture(GL_TEXTURE_2D, textureBuffer3);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 36);
现在我们就可以看看效果了:
可以看到,纹理图上的颜色已经很好地展现在箱子的每一个顶点上,但镜面光照有点奇怪,它不应在木制材料上有如此强烈的镜面高光,回想一下,我们只是在漫反射上按纹理图做出了不一致的处理,但镜面光照仍在按照上节的思路。我们也应该为镜面光照提供镜面光贴图。
镜面光贴图
在处理的思路上,应与漫反射贴图别无二致,我们可以自行再走一遍,提升熟练度。
我们同样可以使用一个专门用于镜面高光的纹理贴图。这也就意味着我们需要生成一个黑白的(如果你想得话也可以是彩色的)纹理,来定义物体每部分的镜面光强度。下面是一个镜面光贴图(Specular Map)的例子:
由于箱子大部分都由木头所组成,而且木头材质应该没有镜面高光,所以漫反射纹理的整个木头部分全部都转换成了黑色。箱子钢制边框的镜面光强度是有细微变化的,钢铁本身会比较容易受到镜面高光的影响,而裂缝则不会。
在片段着色器上,修改材质结构体里的镜面高光变量为
sampler2D
类型,并通过函数完成采样:
#version 330 core
out vec4 fragColor;
struct Material{
sampler2D diffuse;
sampler2D specular;
float shininess;
};
...
in vec2 texCoords;
void main()
{
...
vec3 viewDir = normalize(viewPos - fragPos);
vec3 reflectDir = reflect(-lightDir, normalize(Normal));
float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
vec3 specular = light.specular * spec * texture(material.specular, texCoords).rgb;
vec3 result = ambient + diffuse + specular;
fragColor = vec4(result, 1.0);
}
顶点着色器提供纹理坐标,在学习漫反射贴图时就已经提供了,不再赘述。
加载纹理图,并把纹理数据传递给sampler2D specular;
:
unsigned int textureBuffer4;
textureBuffer4 = LoadImageToGPU("container2_specular.png", GL_RGBA, GL_RGBA, 7);
贴图会放在7号通道上:shader.setInt("material.specular", 7);
,在渲染前绑定好纹理图:
// bind diffuse map
glActiveTexture(GL_TEXTURE6);
glBindTexture(GL_TEXTURE_2D, textureBuffer3);
glActiveTexture(GL_TEXTURE7);
glBindTexture(GL_TEXTURE_2D, textureBuffer4);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 36);
现在我们可以看看最终效果如何:
可以看到现在只在铁制边框处有明显的镜面高光,效果较为真实。