1. Assimp类库
-
Assimp是一个流行的模型载入类库,全称为Open Asset Import Library。Assimp通过将模型数据载入Assimp的通用数据结构实现多种不同3D模型文件格式的数据载入和解析。
-
当我们使用Assimp导入一个模型,Assimp将整个模型载入到一个场景(scene)对象。一个简单的Assimp结构如下所示:(图片取自书中)
Assimp数据结构
- 场景/模型的所有数据都包含在
Scene
对象中。 - 场景根节点可能包含子节点,并保存指向场景对象网格数组的索引。场景的
mMeshes
数组包含实际的网格对象,而节点的mMeshes
数组只是包含索引。 - 一个网格(Mesh) 对象本身包含渲染所需要的所有相关的数据,如顶点位置、法向量、纹理坐标、面片和物体材质。
- 一个面片(face) 代表对象的一个渲染基元(如三角形,四方形和点)。一个面片包含组成基元图形的顶点的索引。
- 最后,一个网格对象同时链接到一个材质对象,通过材质对象的函数可以检索一个物体的材质,如颜色和/或纹理图(扩散光和镜面光图)。
- 场景/模型的所有数据都包含在
-
使用Assimp类库载入3D模型的一般过程是:首先将模型数据载入到
Scene
对象,然后递归访问节点获取相应的网格对象,处理每个网格对象检索顶点数据,索引和相应的材质属性。结果就是我们获得了一个网格数据的集合。下面我们会将该数据集合包含到我们定义的Model
对象当中。 -
在OpenGL中,一个网格是绘制一个对象的最小单元。一个模型一般包含多个网格。
-
Assimp下载
Assimp类库 -
我们参照前面编译GLFW类库的过程对Assimp进行编译。我下载的是5.0.1版本,使用的是VS 2019,最终编译的Debug和Release内容如下:
Assimp Debug编译内容
Assimp Release编译内容 -
配置Assimp类库
- 将下载解压后的include文件和编译产生的include文件的头文件拷贝到我们项目头文件的包含路径下(如我的是D:\3Lib\Include\assimp)。
- 将Assimp的静态库文件拷贝到我们的库目录下(如我的是D:\3Lib\Libs\Assimp),Debug和Release都拷贝进去。
- 在项目的【属性】-【VC++目录】-【库目录】添加静态库文件的目录(头文件路径原先的已经包含了)。
- 在项目的【属性】-【链接器】-【输入】-【附加依赖项】根据是Debug或Release添加相应的静态库文件名(注意分号分隔)。
- 根据Debug或Release将相应的dll文件拷贝到程序执行目录。
2. 网格(Mesh)
从上一小节我们知道,一个网格代表一个可绘制的实体,下面我们来定义一个自己的网格类。
- 首先,我们定义一个代表顶点数据的结构。
struct Vertex {
glm::vec3 Position;
glm::vec3 Normal;
glm::vec2 TexCoords;
};
- 其次,我们定义一个代表纹理数据的结构。
struct Texture {
unsigned int id;
std::string type;
std::string path;
};
- 接下来,我们定义网格类的结构。
class Mesh {
public:
// 网格数据
std::vector<Vertex> vertices;
std::vector<unsigned int> indices;
std::vector<Texture> textures;
Mesh(std::vector<Vertex> vertices, std::vector<unsigned int> indices, std::vector<Texture> textures);
// 绘制网格
void Draw(Shader& shader);
private:
// 渲染所需的OpenGL对象
unsigned int VAO, VBO, EBO;
// 网格初始化:初始化缓冲区
void setupMesh();
};
- 网格类构造函数实现:设置所需成员变量,调用初始化函数。
Mesh::Mesh(std::vector<Vertex> vertices, std::vector<unsigned int> indices, std::vector<Texture> textures)
{
this->vertices = vertices;
this->indices = indices;
this->textures = textures;
setupMesh();
}
- 实现网格类的初始化函数:设置合适的缓冲区对象,指定顶点数据的属性布局。
void Mesh::setupMesh()
{
// 创建顶点数组、顶点缓冲区和元素缓冲区
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glGenBuffers(1, &EBO);
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
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);
// 顶点位置
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)0);
// 法向量
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Normal));
// 纹理坐标
glEnableVertexAttribArray(2);
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, TexCoords));
// 恢复上下文
glBindVertexArray(0);
}
- 从前面章节我们知道,在绘制之前,我们需要激活相应的纹理单元并绑定纹理数据。但是,在网格对象绘制的时候,我们并不知道网格拥有多少纹理,纹理的类型是什么?要解决这个问题,我们设定一种命名约定:扩散光图纹理我们命名为
texture_diffuseN
,镜面光图纹理我们命名为texture_specularN
,其中N
代表从1到纹理取样器允许的最大值。假设我们的网格对象有3个扩散光图纹理和2个镜面光图纹理,那么着色器中的定义应类似下面这样:
uniform sample2D texture_diffuse1;
uniform sample2D texture_diffuse2;
uniform sample2D texture_diffuse3;
uniform sample2D texture_specular1;
uniform sample2D texture_specular2;
- 最终网格类绘制函数的实现。
void Mesh::Draw(Shader& shader)
{
unsigned int diffuseNr = 1;
unsigned int specularNr = 1;
for (unsigned int i = 0; i < textures.size(); i++)
{
glActiveTexture(GL_TEXTURE0 + i); // 激活纹理单元
std::string number;
std::string name = textures[i].type;
if (name == "texture_diffuse")
number = std::to_string(diffuseNr++);
else if (name == "texture_specular")
number = std::to_string(specularNr++);
shader.setFloat(("material." + name + number).c_str(), i);
glBindTexture(GL_TEXTURE_2D, textures[i].id);
}
// 这里默认启动一个纹理单元
glActiveTexture(0);
// 绘制网格
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, indices.size(), GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
}
3. 模型(Model)
下面我们来创建一个自定义的模型类来代表我们整个场景模型,并通过Assimp类库读入3D模型数据并解析出需要绘制的网格对象。
- 首先,我们给出模型类的结构定义。
// 注意引入Assimp相应的头文件
#include <assimp/Importer.hpp>
#include <assimp/scene.h>
#include <assimp/postprocess.h>
// 辅助函数,读取纹理文件
unsigned int TextureFromFile(const char* path, const std::string& directory);
class Model
{
public:
Model(char* path)
{
loadModel(path);
}
void Draw(Shader& shader);
private:
// 模型数据
std::vector<Mesh> meshes;
std::string directory;
std::vector<Texture> textures_loaded; // 用于优化纹理载入
void loadModel(std::string path);
void processNode(aiNode* node, const aiScene* scene);
Mesh processMesh(aiMesh* mesh, const aiScene* scene);
std::vector<Texture> loadMaterialTextures(aiMaterial* mat,
aiTextureType type, std::string typeName);
}
- 因为前面我们已经封装了网格类,因此对于模型的绘制函数,则只需调用所包含网格的绘制函数,循环将所有网格绘制出来即可。
void Model::Draw(Shader& shader)
{
for (unsigned int i = 0; i < meshes.size(); i++)
{
meshes[i].Draw(shader);
}
}
- 对于3D模型导入,Assimp抽象了加载所有不同格式文件的技术细节,我们只需调用
Importer
对象的ReadFile
函数即可。
Assimp::Importer import;
const aiScene* scene = import.ReadFile(path, aiProcess_Triangulate | aiProcess_FlipUVs);
1. 第一个参数是我们需要加载的3D模型的文件路径。
2. 第二个参数是一些后处理选项。
- aiProcess_GenNormals:如果模型没有法向量则为每个顶点创建法向量。
- aiProcess_SplitLargeMeshes:将大网格分割成多个子网格,如果你的计算机只能处理一定数量的顶点,该选项十分有用。
- aiProcess_OptimizeMeshes:将多个网格组合成一个大的网格,降低绘制调用次数,优化渲染。
- 我们的模型载入函数实现如下。
void Model::loadModel(std::string path)
{
Assimp::Importer import;
const aiScene* scene = import.ReadFile(path, aiProcess_Triangulate | aiProcess_FlipUVs);
if (!scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode)
{
std::cout << "ERROR::ASSIMP::" << import.GetErrorString() << std::endl;
return;
}
directory = path.substr(0, path.find_last_of('/'));
processNode(scene->mRootNode, scene);
}
- 实现处理节点函数:递归处理所有节点,解析网格数据。
void Model::processNode(aiNode* node, const aiScene* scene)
{
// 处理节点的所有网格
for (unsigned int i = 0; i < node->mNumMeshes; i++)
{
aiMesh* mesh = scene->mMeshes[node->mMeshes[i]];
meshes.push_back(processMesh(mesh, scene));
}
// 递归处理所有子节点
for (unsigned int i = 0; i < node->mNumChildren; i++)
{
processNode(node->mChildren[i], scene);
}
}
- 注意:由于节点保存指向网格数据的索引,所以根据节点父-子的关系,我们也可以为网格数据创建类似树形结构,但是这里我们只是简单遍历节点解析网格数据。对于实际开发,建议采用树形结构来对网格数据进行处理。
- 处理网格数据函数的实现。
Mesh Model::processMesh(aiMesh* mesh, const aiScene* scene)
{
std::vector<Vertex> vertices;
std::vector<unsigned int> indices;
std::vector<Texture> textures;
// 处理顶点数据
for (unsigned int i = 0; i < mesh->mNumVertices; i++)
{
Vertex vertex;
glm::vec3 vector;
// 顶点位置
vector.x = mesh->mVertices[i].x;
vector.y = mesh->mVertices[i].y;
vector.z = mesh->mVertices[i].z;
vertex.Position = vector;
// 法向量(有些模型没有法向量,读取的时候记得设置生成法向量的选项)
vector.x = mesh->mNormals[i].x;
vector.y = mesh->mNormals[i].y;
vector.z = mesh->mNormals[i].z;
vertex.Normal = vector;
// 纹理坐标
if (mesh->mTextureCoords[0])
{
glm::vec2 vec;
vec.x = mesh->mTextureCoords[0][i].x;
vec.y = mesh->mTextureCoords[0][i].y;
vertex.TexCoords = vec;
}
else
vertex.TexCoords = glm::vec2(0.0f, 0.0f);
vertices.push_back(vertex);
}
// 处理顶点索引
for (unsigned int i = 0; i < mesh->mNumFaces; i++)
{
aiFace face = mesh->mFaces[i];
for (unsigned int j = 0; j < face.mNumIndices; j++)
indices.push_back(face.mIndices[j]);
}
// 处理物体材质
if (mesh->mMaterialIndex >= 0)
{
aiMaterial* material = scene->mMaterials[mesh->mMaterialIndex];
std::vector<Texture> diffuseMaps = loadMaterialTextures(material, aiTextureType_DIFFUSE, "texture_diffuse");
textures.insert(textures.end(), diffuseMaps.begin(), diffuseMaps.end());
std::vector<Texture> specularMaps = loadMaterialTextures(material, aiTextureType_SPECULAR, "texture_specular");
textures.insert(textures.end(), specularMaps.begin(), specularMaps.end());
}
return Mesh(vertices, indices, textures);
}
- 载入物体材质纹理函数的实现。
std::vector<Texture> Model::loadMaterialTextures(aiMaterial* mat, aiTextureType type, std::string typeName)
{
std::vector<Texture> textures;
for (unsigned int i = 0; i < mat->GetTextureCount(type); i++)
{
aiString str;
mat->GetTexture(type, i, &str);
// 判断纹理是否载入,防止载入重复的纹理数据
bool skip = false;
for (unsigned int j = 0; j < textures_loaded.size(); j++)
{
if (std::strcmp(textures_loaded[j].path.data(), str.C_Str()) == 0)
{
textures.push_back(textures_loaded[j]);
skip = true;
break;
}
}
if (!skip)
{
Texture texture;
texture.id = TextureFromFile(str.C_Str(), directory);
texture.type = typeName;
texture.path = str.C_Str();
textures.push_back(texture);
textures_loaded.push_back(texture);
}
}
return textures;
}
- 实现载入纹理数据的辅助函数。
unsigned int TextureFromFile(const char* path, const std::string& directory)
{
std::string filename = std::string(path);
filename = directory + '/' + filename;
unsigned int textureID;
glGenTextures(1, &textureID);
int width, height, nrComponents;
unsigned char* data = stbi_load(filename.c_str(), &width, &height, &nrComponents, 0);
if (data)
{
GLenum format;
if (nrComponents == 1)
format = GL_RED;
else if (nrComponents == 3)
format = GL_RGB;
else if (nrComponents == 4)
format = GL_RGBA;
glBindTexture(GL_TEXTURE_2D, textureID);
glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
stbi_image_free(data);
}
else
{
std::cout << "Texture failed to load at path: " << path << std::endl;
stbi_image_free(data);
}
return textureID;
}
4. 模型渲染
4.1 直接渲染模型
- 编写顶点着色器
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
layout (location = 2) in vec2 aTexCoords;
out vec2 TexCoords;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main()
{
TexCoords = aTexCoords;
gl_Position = projection * view * model * vec4(aPos, 1.0);
}
- 编写片元着色器:注意这里我们直接声明取样器的uniform变量,但是网格类绘制的时候我们默认指定了
material.
前缀,因为默认会启用一个纹理单元,所以是可行的。
#version 330 core
out vec4 FragColor;
in vec2 TexCoords;
uniform sampler2D texture_diffuse1;
void main()
{
FragColor = texture(texture_diffuse1, TexCoords);
}
- 声明着色器和模型(3D模型数据来源书中代码包中的资源)。
// 翻转纹理图像
stbi_set_flip_vertically_on_load(true);
Shader objectShader("./VertexShader.vs", "./FragmentShader.fs");
char path[] = "./backpack/backpack.obj";
Model ourModel(path);
- 渲染模型
objectShader.use();
glm::mat4 view = camera.GetViewMatrix();
glm::mat4 projection = glm::perspective(glm::radians(camera.Zoom), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);
objectShader.setMat4("view", view);
objectShader.setMat4("projection", projection);
glm::mat4 model = glm::mat4(1.0f);
model = glm::translate(model, glm::vec3(0.0f, 0.0f, 0.0f));
model = glm::scale(model, glm::vec3(1.0f, 1.0f, 1.0f));
objectShader.setMat4("model", model);
ourModel.Draw(objectShader);
-
渲染效果
背包模型1
背包模型2
4.2 添加一个点光源
- 编写顶点着色器:根据点光源计算需要的参数,我们将片元位置和法向量输出到片元着色器。
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
layout (location = 2) in vec2 aTexCoords;
out vec3 FragPos;
out vec3 Normal;
out vec2 TexCoords;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main()
{
FragPos = vec3(model * vec4(aPos, 1.0));
Normal = mat3(transpose(inverse(model))) * aNormal;
TexCoords = aTexCoords;
gl_Position = projection * view * model * vec4(aPos, 1.0);
}
- 编写片元着色器:直接抽取多光源章节中点光源的计算函数。
#version 330 core
out vec4 FragColor;
struct Material{
sampler2D texture_diffuse1;
sampler2D texture_specular1;
float shininess;
};
struct PointLight
{
vec3 position;
float constant;
float linear;
float quadratic;
vec3 ambient;
vec3 diffuse;
vec3 specular;
};
in vec3 Normal;
in vec3 FragPos;
in vec2 TexCoords;
uniform vec3 viewPos;
uniform Material material;
uniform PointLight pointLight;
vec3 CalcPointLight(PointLight light, vec3 normal, vec3 fragPos, vec3 viewDir);
void main()
{
vec3 norm = normalize(Normal);
vec3 viewDir = normalize(viewPos - FragPos);
vec3 result = CalcPointLight(pointLight, norm, FragPos, viewDir);
FragColor = vec4(result, 1.0);
}
// 点光源计算
vec3 CalcPointLight(PointLight light, vec3 normal, vec3 fragPos, vec3 viewDir)
{
vec3 lightDir = normalize(light.position - fragPos);
// diffuse
float diff = max(dot(normal, lightDir), 0.0);
// specular
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.0 / (light.constant + light.linear * distance + light.quadratic * (distance * distance));
// combine
vec3 ambient = light.ambient * vec3(texture(material.texture_diffuse1, TexCoords));
vec3 diffuse = light.diffuse * diff * vec3(texture(material.texture_diffuse1, TexCoords));
vec3 specular = light.specular * spec * vec3(texture(material.texture_specular1, TexCoords));
ambient *= attenuation;
diffuse *= attenuation;
specular *= attenuation;
return (ambient + diffuse + specular);
}
- 渲染循环中设置点光源的参数,这里直接将光源位置设置为相机的位置,光颜色偏绿。
objectShader.setVec3("pointLight.position", camera.Position);
objectShader.setVec3("viewPos", camera.Position);
// 光属性
objectShader.setVec3("pointLight.ambient", 0.1f, 0.1f, 0.1f);
objectShader.setVec3("pointLight.diffuse", 0.5f, 0.8f, 0.6f);
objectShader.setVec3("pointLight.specular", 1.0f, 1.0f, 1.0f);
// 衰减参数
objectShader.setFloat("pointLight.constant", 1.0f);
objectShader.setFloat("pointLight.linear", 0.09f);
objectShader.setFloat("pointLight.quadratic", 0.032f);
// 光斑半径参数
objectShader.setFloat("material.shininess", 32.0f);
-
渲染效果
背包模型+点光源1
背包模型+点光源2
网友评论