一、简介
本文介绍了如何使用OpenGL加载obj模型,绘制简单纹理。
在加载obj模型时,使用glm
库确定对顶点进行坐标变换的MVP(model, view, projection)
矩阵。
在绘制纹理时,使用stb_image.h
头文件加载纹理图片,生成纹理。
按照本文代码实现后,理论上可以得到以下结果:
二、使用OpenGL加载obj模型、绘制纹理
0. 环境需要
- Linux,或者 windos下使用wsl2。
- 安装GLFW和GLAD。请参考[OpenGL] wsl2上安装使用cmake+OpenGL教程。
- 安装glm。glm是个可以只使用头文件的库,因此可以直接下载release的压缩文件,然后解压到
include
目录下。例如,假设下载的release版本的压缩文件为glm-1.0.1-light.zip
。将glm-1.0.1-light.zip
复制include
目录下,然后执行以下命令即可解压glm源代码:unzip glm-1.0.1-light.zip
- 需要下载 stb_image.h 作为加载
.png
图像的库。将 stb_image.h 下载后放入include/
目录下。
1. 项目目录
其中:
Mesh.hpp
包含了自定义的 Vertex, Texture, 和 Mesh 类,用于加载 obj 模型、加载图片生成纹理。Shader.hpp
用于创建 shader 程序。vertexShader.glsl
和fragmentShader.glsl
是用于编译 shader 程序的 顶点着色器 和 片段着色器 代码。
下面介绍各部分的代码:
2. CMakeLists.txt代码
cmake_minimum_required(VERSION 3.10)
set(CMAKE_CXX_STANDARD 14)
project(OpenGL_Load_Model)
include_directories(include)
find_package(glfw3 REQUIRED)
file(GLOB project_file main.cpp glad.c)
add_executable(${PROJECT_NAME} ${project_file})
target_link_libraries(${PROJECT_NAME} glfw)
3. Mesh.hpp 代码
#pragma once
#include <glad/glad.h> // holds all OpenGL type declarations
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
#include "Shader.hpp"
#include <string>
#include <vector>
using namespace std;
struct Vertex
{
// position
glm::vec3 Position;
// normal
glm::vec3 Normal;
// texCoords
glm::vec2 TexCoords;
};
struct Texture
{
string path;
GLuint Id; // 纹理 id
};
class Mesh
{
public:
// mesh Data
vector<Vertex> vertices;// vertex 数据,一个顶点包括 position, normal 和 texture coord 三个信息
vector<unsigned int> indices;// index 数据,用于拷贝到 EBO 中
Texture texture;
unsigned int VAO;
Mesh(string obj_path, string texture_path = "")
{
// load obj
ifstream obj_file(obj_path, std::ios::in);
if (obj_file.is_open() == false)
{
std::cerr << "Failed to load obj: " << obj_path << "\n";
return;
}
int position_id = 0;
int normal_id = 0;
int texture_coord_id = 0;
string line;
while (getline(obj_file, line))
{
std::istringstream iss(line);
std::string prefix;
iss >> prefix;
if (prefix == "v") // vertex
{
if (vertices.size() <= position_id)
{
vertices.push_back(Vertex());
}
iss >> vertices[position_id].Position.x;
iss >> vertices[position_id].Position.y;
iss >> vertices[position_id].Position.z;
position_id++;
}
else if (prefix == "vn") // normal
{
if (vertices.size() <= normal_id)
{
vertices.push_back(Vertex());
}
iss >> vertices[normal_id].Normal.x;
iss >> vertices[normal_id].Normal.y;
iss >> vertices[normal_id].Normal.z;
normal_id++;
}
else if (prefix == "vt") // texture coordinate
{
if (vertices.size() <= texture_coord_id)
{
vertices.push_back(Vertex());
}
iss >> vertices[texture_coord_id].TexCoords.x;
iss >> vertices[texture_coord_id].TexCoords.y;
texture_coord_id++;
}
else if (prefix == "f") // face
{
for (int i = 0; i < 3; ++i)
{
std::string vertexData;
iss >> vertexData;
unsigned int ver, tex, nor;
sscanf(vertexData.c_str(), "%d/%d/%d", &ver, &tex, &nor);
indices.push_back(ver - 1);
}
}
}
obj_file.close();
// load texture
GLuint textureID;
glGenTextures(1, &textureID); // 生成纹理 ID
glBindTexture(GL_TEXTURE_2D, textureID); // 绑定纹理,说明接下来对纹理的操作都应用于对象 textureID 上
// 设置纹理参数
// 设置纹理在 S 方向(水平方向)的包裹方式为 GL_REPEAT
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
// 设置纹理在 T 方向(垂直方向)的包裹方式为 GL_REPEAT
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
// 设置纹理的缩小过滤方式,当纹理变小时,使用 GL_LINEAR (线性过滤)方式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
// 设置纹理的放大过滤方式,当纹理变大时,使用 GL_LINEAR (线性过滤)方式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// 加载纹理图像
int width, height, nrChannels;
stbi_set_flip_vertically_on_load(true);
unsigned char *data = stbi_load(texture_path.c_str(), &width, &height, &nrChannels, 0);
if (data)
{
GLenum format;
if (nrChannels == 1)
format = GL_RED;
else if (nrChannels == 3)
format = GL_RGB;
else if (nrChannels == 4)
format = GL_RGBA;
// 生成纹理
glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D); // 生成 Mipmaps
}
else
{
std::cerr << "Failed to load texture: " << texture_path << "\n";
}
stbi_image_free(data); // 释放图像内存
glBindTexture(GL_TEXTURE_2D, 0); // 解绑纹理
texture.Id = textureID;
texture.path = texture_path;
setupMesh();
}
// render the mesh
void Draw(Shader &shader)
{
// draw mesh
glActiveTexture(GL_TEXTURE0); // 激活 纹理单元0
glBindTexture(GL_TEXTURE_2D, texture.Id); // 绑定纹理,将纹理texture.id 绑定到 纹理单元0 上
glUniform1i(glGetUniformLocation(shader.ID, "texture1"), 0); // 将 shader 中的 texture1 绑定到 纹理单元0
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, static_cast<unsigned int>(indices.size()), GL_UNSIGNED_INT, 0);
glBindTexture(GL_TEXTURE_2D, 0);
glBindVertexArray(0);
}
private:
// render data
unsigned int VBO, EBO;
// initializes all the buffer objects/arrays
void setupMesh()
{
// create buffers/arrays
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glGenBuffers(1, &EBO);
glBindVertexArray(VAO);
// load data into vertex buffers
glBindBuffer(GL_ARRAY_BUFFER, VBO);
// A great thing about structs is that their memory layout is sequential for all its items.
// The effect is that we can simply pass a pointer to the struct and it translates perfectly to a glm::vec3/2
// array which again translates to 3/2 floats which translates to a byte array.
glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex), &vertices[0], GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(unsigned int), &indices[0], GL_STATIC_DRAW);
// set the vertex attribute pointers
// vertex Positions
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void *)0);
// vertex normals
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void *)offsetof(Vertex, Normal));
// vertex texture coords
glEnableVertexAttribArray(2);
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void *)offsetof(Vertex, TexCoords));
glBindVertexArray(0);
}
};
4. Shader.hpp 代码
Shader.hpp
即 LearnOpenGL-入门-着色器 中的 由于管理 shader 的头文件。
#ifndef SHADER_H
#define SHADER_H
#include <glad/glad.h>
#include <glm/glm.hpp>
#include <string>
#include <fstream>
#include <sstream>
#include <iostream>
class Shader
{
public:
unsigned int ID;
// constructor generates the shader on the fly
// ------------------------------------------------------------------------
Shader(const char *vertexPath, const char *fragmentPath)
{
// 1. retrieve the vertex/fragment source code from filePath
std::string vertexCode;
std::string fragmentCode;
std::ifstream vShaderFile;
std::ifstream fShaderFile;
// ensure ifstream objects can throw exceptions:
vShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);
fShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);
try
{
// open files
vShaderFile.open(vertexPath);
fShaderFile.open(fragmentPath);
std::stringstream vShaderStream, fShaderStream;
// read file's buffer contents into streams
vShaderStream << vShaderFile.rdbuf();
fShaderStream << fShaderFile.rdbuf();
// close file handlers
vShaderFile.close();
fShaderFile.close();
// convert stream into string
vertexCode = vShaderStream.str();
fragmentCode = fShaderStream.str();
}
catch (std::ifstream::failure &e)
{
std::cout << "ERROR::SHADER::FILE_NOT_SUCCESSFULLY_READ: " << e.what() << std::endl;
}
const char *vShaderCode = vertexCode.c_str();
const char *fShaderCode = fragmentCode.c_str();
// 2. compile shaders
unsigned int vertex, fragment;
// vertex shader
vertex = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertex, 1, &vShaderCode, NULL);
glCompileShader(vertex);
checkCompileErrors(vertex, "VERTEX");
// fragment Shader
fragment = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragment, 1, &fShaderCode, NULL);
glCompileShader(fragment);
checkCompileErrors(fragment, "FRAGMENT");
// shader Program
ID = glCreateProgram();
glAttachShader(ID, vertex);
glAttachShader(ID, fragment);
glLinkProgram(ID);
checkCompileErrors(ID, "PROGRAM");
// delete the shaders as they're linked into our program now and no longer necessary
glDeleteShader(vertex);
glDeleteShader(fragment);
}
// activate the shader
// ------------------------------------------------------------------------
void use() const
{
glUseProgram(ID);
}
// utility uniform functions
// ------------------------------------------------------------------------
void setBool(const std::string &name, bool value) const
{
glUniform1i(glGetUniformLocation(ID, name.c_str()), (int)value);
}
// ------------------------------------------------------------------------
void setInt(const std::string &name, int value) const
{
glUniform1i(glGetUniformLocation(ID, name.c_str()), value);
}
// ------------------------------------------------------------------------
void setFloat(const std::string &name, float value) const
{
glUniform1f(glGetUniformLocation(ID, name.c_str()), value);
}
// ------------------------------------------------------------------------
void setVec2(const std::string &name, const glm::vec2 &value) const
{
glUniform2fv(glGetUniformLocation(ID, name.c_str()), 1, &value[0]);
}
void setVec2(const std::string &name, float x, float y) const
{
glUniform2f(glGetUniformLocation(ID, name.c_str()), x, y);
}
// ------------------------------------------------------------------------
void setVec3(const std::string &name, const glm::vec3 &value) const
{
glUniform3fv(glGetUniformLocation(ID, name.c_str()), 1, &value[0]);
}
void setVec3(const std::string &name, float x, float y, float z) const
{
glUniform3f(glGetUniformLocation(ID, name.c_str()), x, y, z);
}
// ------------------------------------------------------------------------
void setVec4(const std::string &name, const glm::vec4 &value) const
{
glUniform4fv(glGetUniformLocation(ID, name.c_str()), 1, &value[0]);
}
void setVec4(const std::string &name, float x, float y, float z, float w) const
{
glUniform4f(glGetUniformLocation(ID, name.c_str()), x, y, z, w);
}
// ------------------------------------------------------------------------
void setMat2(const std::string &name, const glm::mat2 &mat) const
{
glUniformMatrix2fv(glGetUniformLocation(ID, name.c_str()), 1, GL_FALSE, &mat[0][0]);
}
// ------------------------------------------------------------------------
void setMat3(const std::string &name, const glm::mat3 &mat) const
{
glUniformMatrix3fv(glGetUniformLocation(ID, name.c_str()), 1, GL_FALSE, &mat[0][0]);
}
// ------------------------------------------------------------------------
void setMat4(const std::string &name, const glm::mat4 &mat) const
{
glUniformMatrix4fv(glGetUniformLocation(ID, name.c_str()), 1, GL_FALSE, &mat[0][0]);
}
private:
// utility function for checking shader compilation/linking errors.
// ------------------------------------------------------------------------
void checkCompileErrors(GLuint shader, std::string type)
{
GLint success;
GLchar infoLog[1024];
if (type != "PROGRAM")
{
glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(shader, 1024, NULL, infoLog);
std::cout << "ERROR::SHADER_COMPILATION_ERROR of type: " << type << "\n"
<< infoLog << "\n -- --------------------------------------------------- -- " << std::endl;
}
}
else
{
glGetProgramiv(shader, GL_LINK_STATUS, &success);
if (!success)
{
glGetProgramInfoLog(shader, 1024, NULL, infoLog);
std::cout << "ERROR::PROGRAM_LINKING_ERROR of type: " << type << "\n"
<< infoLog << "\n -- --------------------------------------------------- -- " << std::endl;
}
}
}
};
#endif
5. vertexShader.glsl 和 fragmentShader.glsl 代码
vertexShader.glsl
:
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNor;
layout (location = 2) in vec2 aTexCoord;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
out vec2 textureCoord;
void main()
{
textureCoord = aTexCoord;
gl_Position = projection * view * model * vec4(aPos, 1.0f);
}
fragmentShader.glsl
:
#version 330 core
out vec4 FragColor;
in vec2 textureCoord;
uniform sampler2D texture1;
void main()
{
FragColor = texture(texture1, textureCoord);
}
6. main.cpp代码
1). 代码整体流程
- 初始化glfw,glad,窗口
- 编译 shader 程序
- 加载obj模型、纹理图片
- 设置MVP变换矩阵
- 开始绘制
- 释放资源
2). main.cpp代码
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include "Shader.hpp"
#include "Mesh.hpp"
#include "glm/ext.hpp"
#include "glm/mat4x4.hpp"
#include <random>
#include <iostream>
// 用于处理窗口大小改变的回调函数
void framebuffer_size_callback(GLFWwindow *window, int width, int height);
// 用于处理用户输入的函数
void processInput(GLFWwindow *window);
// 指定窗口默认width和height像素大小
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;
/************************************/
int main()
{
/****** 1.初始化glfw, glad, 窗口 *******/
// glfw 初始化 + 配置 glfw 参数
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
// 在创建窗口之前
glfwWindowHint(GLFW_SAMPLES, 4); // 设置多重采样级别为4
// glfw 生成窗口
GLFWwindow *window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
if (window == NULL)
{
// 检查是否成功生成窗口,如果没有成功打印出错信息并且退出
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
// 设置窗口window的上下文
glfwMakeContextCurrent(window);
// 配置window变化时的回调函数
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
// 使用 glad 加载 OpenGL 中的各种函数
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
// 启用 深度测试
glEnable(GL_DEPTH_TEST);
// 启用 多重采样抗锯齿
glEnable(GL_MULTISAMPLE);
// glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); // 使用线框模式,绘制时只绘制 三角形 的轮廓
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); // 使用填充模式,绘制时对 三角形 内部进行填充
/************************************/
/****** 2.编译 shader 程序 ******/
Shader ourShader("../resources/vertexShader.glsl", "../resources/fragmentShader.glsl");
/************************************/
/****** 3.加载obj模型、纹理图片 ******/
// Mesh ourModel("../resources/models/backpack/backpack.obj", "../resources/models/backpack/backpack.jpg"); //
// backpack
Mesh ourModel("../resources/models/spot/spot.obj", "../resources/models/spot/spot.png"); // dairy cow
// Mesh ourModel("../resources/models/rock/rock.obj", "../resources/models/rock/rock.png"); // rock
/************************************/
/****** 4.设置 MVP 矩阵 ******/
// model 矩阵
glm::mat4 model = glm::mat4(1.0f);
model = glm::translate(model, glm::vec3(0.0f, 0.0f, 0.0f));
model = glm::rotate(model, glm::radians(0.0f), glm::vec3(1.0f, 0.0f, 0.0f));
model = glm::rotate(model, glm::radians(0.0f), glm::vec3(0.0f, 1.0f, 0.0f));
model = glm::rotate(model, glm::radians(0.0f), glm::vec3(0.0f, 0.0f, 1.0f));
model = glm::scale(model, glm::vec3(0.5f, 0.5f, 0.5f));
// view 矩阵
glm::mat4 view = glm::mat4(1.0f);
view = glm::lookAt(glm::vec3(0.0f, 0.0f, -1.5f), glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 1.0f, 0.0f));
// projection 矩阵
glm::mat4 projection = glm::mat4(1.0f);
projection = glm::perspective(glm::radians(60.0f), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);
/************************************/
/****** 5.开始绘制 ******/
float rotate = 0.0f;
while (!glfwWindowShouldClose(window))
{
rotate += 0.00005f;
// input
// -----
processInput(window);
// render
// ------
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
// 清除颜色缓冲区 并且 清楚深度缓冲区
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// 使用我们之前编译连接好的 shader 程序
// 必须先使用 shaderProgram 然后才能操作 shaderProgram 中的 uniform 变量
ourShader.use();
model = glm::rotate(model, glm::radians(rotate), glm::vec3(0.0f, 1.0f, 0.0f));
ourShader.setMat4("model", model);
ourShader.setMat4("view", view);
ourShader.setMat4("projection", projection);
ourModel.Draw(ourShader);
glfwSwapBuffers(window); // 在gfw中启用双缓冲,确保绘制的平滑和无缝切换
glfwPollEvents(); // 用于处理所有挂起的事件,例如键盘输入、鼠标移动、窗口大小变化等事件
}
/************************************/
/****** 6.释放资源 ******/
// glfw 释放 glfw使用的所有资源
glfwTerminate();
/************************************/
return 0;
}
// 用于处理用户输入的函数
void processInput(GLFWwindow *window)
{
// 当按下 Esc 按键时调用 glfwSetWindowShouldClose() 函数,关闭窗口
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
{
glfwSetWindowShouldClose(window, true);
}
}
// 在使用 OpenGL 和 GLFW 库时,处理窗口大小改变的回调函数
// 当窗口大小发生变化时,确保 OpenGL 渲染的内容能够适应新的窗口大小,避免图像被拉伸、压缩或出现其他比例失真的问题
void framebuffer_size_callback(GLFWwindow *window, int width, int height)
{
glViewport(0, 0, width, height);
}
4. 编译运行及结果
编译运行:
cd ./build
cmake ..
make
./OpenGL_Load_Model
绘制结果:
三、全部代码及模型文件
全部代码以及模型文件可以在使用OpenGL加载obj模型、绘制纹理中下载。
四、参考
[1].[OpenGL]使用OpenGL绘制三维立方体
[2].[OpenGL]使用OpenGL绘制带纹理三角形