Textures是LearnOpenGL CN教程中的经典项目,里面包含了完整的OpenGL渲染流程,着色器的编写,着色器类的应用,纹理的加载等一系列经典的应用场景,是OpenGL入门过程中必须掌握的。最近在复习之前学习的OpenGL时,发现有些基本的概念比较模糊,于是对照着重新写了一遍,把每个比较重要的地方都加上了中文注释,方便以后自己查阅。此代码基于Linux下的Clion编写,项目目录如下:
- Shaders文件夹:包含顶点着色器和片段着色器。着色器的后缀可以任意定义,有辨识度即可,例如可以将着色器命名为shader.vert,shader.frag;
- Textures文件夹:包含需要加载的两张纹理图片。注意,png后缀的图片在加载时需要将参数由GL_RGB改为GL_RGBA,因为png图片包含alpha通道,是可以显示透明效果的,普通的jpg图片为rgb色彩空间,无透明显示功能;
- CmakeLists.txt:clion自动生成的cmake文件,需要手动更改一下参数;
- glad.c:glad库文件,编写OpenGL必须要加上;
- main.cpp:主要的代码文件;
- shader_s.cpp:着色器类,可直接用着色器类去编译着色器文件,更加快捷方便;
- stb_image.h / stb_image.cpp:一个非常流行的图像加载库,这里用来加载纹理图片(stb_image.h为下载的库文件,stb_image.cpp需要自己编写)。
以下部分为添加了注释的代码段,仅供参考。
CmakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(Textures)
set(CMAKE_CXX_STANDARD 14)
set(SOURCE_FILES main.cpp glad.c stb_image.cpp)
add_executable(Textures ${SOURCE_FILES})
target_link_libraries(Textures glfw3 GL m Xrandr Xi X11 Xxf86vm pthread dl Xinerama Xcursor)
stb_image.cpp
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
Shader.fs
#version 330 core
out vec4 FragColor; // 表示最终的输出颜色
in vec3 ourColor; // 从顶点着色器传来的输入变量(名称相同、类型相同)
in vec2 TexCoord;
// Sampler(采样器), GLSL中供纹理对象使用的内建数据类型, 以纹理类型作为后缀, 比如sampler1D、sampler3D,
uniform sampler2D texture1;
uniform sampler2D texture2;
void main()
{
// 在两个纹理之间线性插值(80%的箱子,20%的笑脸)
FragColor = mix(texture(texture1, TexCoord), texture(texture2, TexCoord), 0.2);
}
Shader.vs
#version 330 core
layout (location = 0) in vec3 aPos; // 位置变量的属性位置值为0
layout (location = 1) in vec3 aColor; // 颜色变量的属性位置值为1
layout (location = 2) in vec2 aTexCoord; // 纹理变量的属性位置值为2
out vec3 ourColor; // 向片段着色器输出一个颜色
out vec2 TexCoord; // 向片段着色器输出一个纹理
void main()
{
gl_Position = vec4(aPos, 1.0); // 归一化的裁剪空间坐标(xyz各个维度的范围为-1到1), 第四个分量w是1.由于透视投影,该分量假定另一个值大于1。
ourColor = aColor; // 将ourColor设置为我们从顶点数据那里得到的输入颜色
TexCoord = vec2(aTexCoord.x, aTexCoord.y);
}
main.cpp
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include "stb_image.h"
#include "shader_s.h"
#include <iostream>
void processInput(GLFWwindow *window); // 处理键盘/鼠标的输入
// 设置窗口大小
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;
int main() {
// glfw的初始化和配置
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); // 设置主版本
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); // 设置次版本
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // 使用OpenGL核心模式
// 如果是苹果电脑,需要加上这个
#ifdef __APPLE__
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // uncomment this statement to fix compilation on OS X
#endif
// 创建OpenGL窗口
// -------------------------------------------------------------------------------------------------------
GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "Textures", NULL, NULL);
if (window == NULL)
{
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window); // 将窗口的上下文设置为当前线程的主上下文
// 初始化glad,加载OpenGL函数指针地址的函数
// -----------------------------------------------------
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
// 指定当前视口尺寸(前两个参数为左下角位置,后两个参数是渲染窗口宽、高)
glViewport(0, 0, SCR_WIDTH, SCR_HEIGHT);
// 创建和编译着色器
Shader ourShader("../Shaders/shader.vs", "../Shaders/shader.fs");
// 设置VAO,VBO,EBO,顶点属性等
// -------------------------------------------------------------------------
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[] = { // EBO的索引
0, 1, 3, // 第一个三角形
1, 2, 3 // 第二个三角形
};
// 生成并绑定VAO,VBO,EBO(绑定顺序为VAO,VBO,EBO)
GLuint VAO;
glGenVertexArrays(1, &VAO);
glBindVertexArray(VAO);
GLuint VBO;
glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
GLuint EBO;
glGenBuffers(1, &EBO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
// 将顶点数据和索引数据绑定至当前默认的缓冲中
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices ,GL_STATIC_DRAW);
// 顶点属性, 大小3, 步长32(4 * 8), float长度为4, 向右移动8个float; 位置顶点属性在前, 故偏移量为0
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// 颜色属性, 大小3, 步长32; 因位置属性值为3, 故偏移量为3
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
// 纹理属性, 大小2, 步长32; 因颜色属性值为3, 故偏移量为3 + 3 = 6
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
glEnableVertexAttribArray(2);
// 创建并加载纹理
// ------------------------
// 纹理1
GLuint texture1;
glGenTextures(1, &texture1);
glBindTexture(GL_TEXTURE_2D, texture1);
// 设置纹理环绕方式为GL_REPEAT
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
// 设置纹理过滤方式为GL_LINEAR(线性过滤)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// 加载并生成纹理
int width, height, nrChannels;
unsigned char *data = stbi_load("../Textures/container.jpg", &width, &height, &nrChannels, 0);
stbi_set_flip_vertically_on_load(true); // 告诉stb_image.h在y轴上翻转加载的纹理
if (data)
{
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data); // 生成纹理
glGenerateMipmap(GL_TEXTURE_2D); // 为纹理对象生成一组完整的mipmap
}
else
{
std::cout << "Failed to load texture" << std::endl;
}
stbi_image_free(data); // 释放图像的内存
// ------------------------
// 纹理2
GLuint texture2;
glGenTextures(1, &texture2);
glBindTexture(GL_TEXTURE_2D, texture2);
// 设置纹理环绕方式为GL_REPEAT
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
// 设置纹理过滤方式为GL_LINEAR(线性过滤)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
data = stbi_load("../Textures/awesomeface.png", &width, &height, &nrChannels, 0);
if (data)
{
// 此处png格式的awesomeface是GL_RGBA, A表示的是透明度
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
}
else
{
std::cout << "Failed to load texture" << std::endl;
}
stbi_image_free(data); // 释放图像的内存
// 告诉opengl每个采样器分别属于哪个纹理单元(只需执行一次)
ourShader.use(); // 设置uniform之前需要激活/使用着色器
// 通过纹理类进行设置(建议的方法)
ourShader.setInt("texture1", 0);
ourShader.setInt("texture2", 1);
// 也可以这样手动设置
// glUniform1i(glGetUniformLocation(ourShader.ID, "texture1"), 0);
// glUniform1i(glGetUniformLocation(ourShader.ID, "texture2"), 1);
// 解绑VAO, VBO和EBO(解绑顺序不能乱)
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
// 正常模式和线框模式
//glPolygonMode(GL_FRONT_AND_BACK, GL_FULL);
//glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
// 渲染循环
// -----------------------------------
while (!glfwWindowShouldClose(window))
{
processInput(window); // 处理输入
// 渲染
glClearColor(0.2f, 0.3f, 0.3f, 1.0f); // 设置清空屏幕所用的颜色
glClear(GL_COLOR_BUFFER_BIT); // 清空屏幕的颜色缓冲, 它接受一个缓冲位(Buffer Bit)来指定要清空的缓冲
// 在相应的纹理单元上绑定纹理
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture1); // 绑定纹理1箱子
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, texture2); // 绑定纹理2笑脸
// 绘制图形
ourShader.use();
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
// glBindVertexArray(0);
// 交换缓冲并且检查是否有触发事件(比如键盘输入、鼠标移动等)
glfwSwapBuffers(window);
glfwPollEvents();
}
// 删除VAO和VBO
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
glDeleteBuffers(1, &EBO);
// 释放/删除之前的分配的所有资源,退出程序
glfwTerminate();
return 0;
}
void processInput(GLFWwindow *window)
{
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
}