参考资料:
https://learnopengl.com/
https://learnopengl-cn.github.io/
这次要做的是将两张纹理叠加映射到图像上。为了方便读取纹理数据,可以用 这里 提供的库文件,导入后使用如下语句进行图片数据的读取:
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
int width, height, nrChannel;
unsigned char* data = stbi_load("container.jpg", &width, &height, &nrChannel, 0);
使用纹理需要使用uv值,接下来需要调整顶点着色器使其能够接受顶点坐标为一个顶点属性,并把坐标传给片元着色器。现在顶点着色器将接收位置、颜色、uv三种信息,输出顶点颜色和uv信息:
//顶点着色器编码
#version 330 core
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec3 aColor;
layout(location = 2) in vec2 aTexCoord;
out vec4 vertexColor;
out vec2 TexCoord;
void main() {
gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
vertexColor = vec4(aColor, 1.0f);
TexCoord = aTexCoord;
}
片元着色器则接收顶点颜色和uv信息,并声明两张sampler2D纹理,用于实现两张图片叠加效果。计算颜色时,使用texture采样纹理颜色,使用mix函数将两张图片的采样结果混合:
//片元着色器编码
#version 330 core
in vec4 vertexColor;
in vec2 TexCoord;
uniform sampler2D ourTexture;
uniform sampler2D ourFace;
out vec4 FragColor;
void main() {
FragColor = mix(texture(ourTexture, TexCoord), texture(ourFace, TexCoord), 0.3);
}
为此,所有顶点的信息也要对应修改,添加相应的纹理坐标:
//顶点数据
float vertices[] = {
// ---- 位置 ---- ---- 颜色 ---- - 纹理坐标 -
0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // 右上
0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // 右下
-0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // 左下
-0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f // 左上
};
以及除了告诉OpenGL解析顶点的位置颜色属性外,也要解析uv属性,也因添加了uv这个二维属性,“在连续的顶点属性组之间的间隔”这一项大小也变为了8,其中位置信息占3,颜色信息占3,纹理坐标信息占2:
//位置属性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0); //告诉OpenGL该如何解析顶点数据(应用到逐个顶点属性上)。参数1:要配置的顶点属性(layout(location = 0),故0),参数2:指定顶点属性的大小(vec3,故3),参数3:指定数据的类型,参数4:是否希望数据被标准化,参数5:在连续的顶点属性组之间的间隔,参数6:表示位置数据在缓冲中起始位置的偏移量(Offset)。
glEnableVertexAttribArray(0); //以顶点属性位置值作为参数,启用顶点属性,由于前面声明了layout(location = 0),故为0
//颜色属性
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
//UV属性
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
glEnableVertexAttribArray(2);
现在的顶点信息结构如下图所示:
OpenGL至少保证有16个纹理单元供你使用,也就是说你可以激活从GL_TEXTURE0到GL_TEXTRUE15。我们需要两张贴图,因此使用两个纹理单元,保证不冲突和单元号小于16就行。准备完成后,在main函数中需要做的是,绑定纹理到各自的纹理单元,可使用glActiveTexture和glBindTexture来完成,然后读取纹理,之前已提到。还需告诉OpenGL每个着色器采样器属于哪个纹理单元,使用glUniform1i实现,最后绘制。另外,由于OpenGL要求y轴0.0坐标是在图片的底部的,也就是原点在左下角,输出时有可能会出现图片颠倒问题,可在加载任何图像前加入stbi_set_flip_vertically_on_load(true)语句对y轴进行翻转。带注释的主文件代码如下:
#define GLEW_STATIC
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <iostream>
#include "Shader.h"
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
//顶点数据
float vertices[] = {
// ---- 位置 ---- ---- 颜色 ---- - 纹理坐标 -
0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // 右上
0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // 右下
-0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // 左下
-0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f // 左上
};
//顶点索引
unsigned int indices[] = {
0, 1, 2, //第一个三角形使用的顶点
2, 3, 0 //第二个三角形使用的顶点
};
//检查输入函数
void processInput(GLFWwindow* window)
{
//按下ESC键时,将WindowShouldClose设为true,循环绘制将停止
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
{
glfwSetWindowShouldClose(window, true);
}
}
//视口改变时的回调函数
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
glViewport(0, 0, width, height); //OpenGL渲染窗口的尺寸大小
}
int main()
{
glfwInit(); //初始化GLFW
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); //告诉GLFW要使用OpenGL的版本号
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); //主版本号、次版本号都为3,即3.3版本
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); //告诉GLFW使用核心模式(Core-profile)
//打开 GLFW Window
GLFWwindow* window = glfwCreateWindow(1600, 1200, "My OpenGL Game", nullptr, nullptr);
if (window == nullptr) //若窗口创建失败,打印错误信息,终止GLFW并return -1
{
printf("Open window failed.");
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window); //创建OpenGL上下文
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback); //用户改变窗口大小的时候,视口调用回调函数
//初始化GLEW
glewExperimental = true;
if (glewInit() != GLEW_OK) //若GLEW初始化失败,打印错误信息并终止GLFW窗口
{
printf("Init GLEW failed.");
glfwTerminate();
return -1;
}
Shader* myShader = new Shader("vertexSource.txt", "fragmentSource.txt");
//创建VAO(顶点数组对象)
unsigned int VAO;
glGenVertexArrays(1, &VAO); //生成一个顶点数组对象
glBindVertexArray(VAO); //绑定VAO
//创建VBO(顶点缓冲对象)
unsigned int VBO;
glGenBuffers(1, &VBO); //生成缓冲区对象。第一个参数是要生成的缓冲对象的数量,第二个是要输入用来存储缓冲对象名称的数组,由于只需创建一个VBO,因此不需要用数组形式
glBindBuffer(GL_ARRAY_BUFFER, VBO); //把新创建的缓冲绑定到GL_ARRAY_BUFFER目标上,GL_ARRAY_BUFFER是一种顶点缓冲对象的缓冲类型。OpenGL允许同时绑定多个缓冲,只要它们是不同的缓冲类型。
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); //把定义的数据复制到当前绑定缓冲的函数。参数1:目标缓冲类型,参数2:指定传输数据的大小(以字节为单位),参数3:我们希望发送的实际数据,参数4:指定显卡如何管理给定的数据,GL_STATIC_DRAW表示数据不会或几乎不会改变。
//创建EBO(元素缓冲对象/索引缓冲对象)
unsigned int EBO;
glGenBuffers(1, &EBO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
//位置属性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0); //告诉OpenGL该如何解析顶点数据(应用到逐个顶点属性上)。参数1:要配置的顶点属性(layout(location = 0),故0),参数2:指定顶点属性的大小(vec3,故3),参数3:指定数据的类型,参数4:是否希望数据被标准化,参数5:在连续的顶点属性组之间的间隔,参数6:表示位置数据在缓冲中起始位置的偏移量(Offset)。
glEnableVertexAttribArray(0); //以顶点属性位置值作为参数,启用顶点属性,由于前面声明了layout(location = 0),故为0
//颜色属性
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
//UV属性
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
glEnableVertexAttribArray(2);
//使用纹理单元0绑定TexBufferA
unsigned int TexBufferA;
glGenTextures(1, &TexBufferA);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, TexBufferA);
int width, height, nrChannel;
stbi_set_flip_vertically_on_load(true); //翻转图像
//加载并生成第一张纹理
unsigned char* data = stbi_load("container.jpg", &width, &height, &nrChannel, 0);
if (data)
{
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
}
else
{
std::cout << "图片加载失败" << std::endl;
}
stbi_image_free(data); //释放
//使用纹理单元3绑定TexBufferB
unsigned int TexBufferB;
glGenTextures(1, &TexBufferB);
glActiveTexture(GL_TEXTURE3);
glBindTexture(GL_TEXTURE_2D, TexBufferB);
//加载并生成第二张纹理
unsigned char* data2 = stbi_load("awesomeface1.png", &width, &height, &nrChannel, 0);
if (data2)
{
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data2);
glGenerateMipmap(GL_TEXTURE_2D);
}
else
{
std::cout << "图片加载失败" << std::endl;
}
stbi_image_free(data2);
//让程序在手动关闭之前不断绘制图像
while (!glfwWindowShouldClose(window))
{
//检测输入
processInput(window);
//渲染指令
glClearColor(0.f, 0.5f, 0.5f, 1.0f); //设置清空屏幕所用的颜色
glClear(GL_COLOR_BUFFER_BIT); //清空屏幕的颜色缓冲区
//绑定纹理到对应的纹理单元
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, TexBufferA);
glActiveTexture(GL_TEXTURE3);
glBindTexture(GL_TEXTURE_2D, TexBufferB);
glBindVertexArray(VAO); //绘制物体的时候就拿出相应的VAO,绑定它
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); //绑定EBO
myShader->use();
//告诉OpenGL每个着色器采样器属于哪个纹理单元
glUniform1i(glGetUniformLocation(myShader->ID, "ourTexture"), 0);
glUniform1i(glGetUniformLocation(myShader->ID, "ourFace"), 3);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); //绘制,参数1:绘制模式(三角形),参数2:绘制顶点数(两个三角形6个顶点),参数3:索引的类型,参数4:指定EBO中的偏移量。
//检查并调用事件,交换缓冲区
glfwSwapBuffers(window); //交换颜色缓冲区,前缓冲区保存最终输出的图像,后缓冲区负责绘制渲染指令,当渲染指令执行完毕后,交换前后缓冲区,使完整图像呈现出来,避免逐像素绘制图案时的割裂感
glfwPollEvents(); //检查触发事件,如键盘输入、鼠标移动等
}
glfwTerminate(); //关闭GLFW并退出
return 0;
}
运行可生成以下结果: