上接:https://blog.csdn.net/weixin_44506615/article/details/150465200?spm=1001.2014.3001.5501
完整代码:https://gitee.com/Duo1J/learn-open-gl
接下来我们通过加载模型文件的方式来导入我们要渲染的模型,取代之前的硬编码顶点的箱子
一、Assimp库
像图片、音视频等文件,模型文件也有各种各样的格式,像是 .obj、.fbx 等,要加载并解析它们的逻辑也各不相同,接下来我们就要用到Assimp库(Open Asset Import Library)
Assimp库在不同的模型格式上进行抽象,我们使用统一的接口就可以加载不同格式的模型
当Assimp导入一个模型的时候,它会将整个模型加载进一个场景对象(aiScene),其中还包含根节点和任意数量的子节点,下图是Assimp数据结构的简化模型 (图片来自于LearnOpenGL)
接下来我们去github上拉取代码 https://github.com/assimp/assimp,这里我使用了和LearnOpenGL一样的3.1.1版本
拉取完毕后,使用cmake创建sln编译,并在我们的项目中配置include目录和lib目录以及链接器输入即可
二、网格 (Mesh)
接下来我们来创建一个网格类,用于存储加载后的网格数据
首先创建顶点结构体
Vertex.h 新建
#pragma once#include <glm.hpp>/**
* 顶点
*/
struct Vertex
{/*** 位置*/glm::vec3 position;/*** 法向量*/glm::vec3 normal;/*** UV*/glm::vec2 texCoord;
};
接下来是网格类
Mesh.h 新建
#pragma once#include <vector>#include "Vertex.h"
#include "Texture.h"
#include "Shader.h"/**
* 网格
*/
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(const Shader& shader);private:/*** 缓冲*/unsigned int VAO, VBO, EBO;/*** 创建缓冲*/void CreateBuffer();
};
Mesh.cpp 新建
#include "Mesh.h"
#include <glad/glad.h>
#include <assert.h>Mesh::Mesh(std::vector<Vertex> _vertices, std::vector<unsigned int> _indices, std::vector<Texture> _textures)
{vertices = _vertices;indices = _indices;textures = _textures;// 创建三缓冲CreateBuffer();
}void Mesh::Draw(const Shader& shader)
{// 纹理索引计数unsigned int diffuseNum = 0;unsigned int specularNum = 0;for (int i = 0; i < textures.size(); ++i){glActiveTexture(GL_TEXTURE0 + i);// 这里计算应该用到第几张纹理了std::string number;Texture texture = textures[i];if (texture.type == TextureType::DIFFUSE){number = std::to_string(++diffuseNum);}else if (texture.type == TextureType::SPECULAR){number = std::to_string(++specularNum);}else{assert(false);}// 着色器中的纹理名std::string name = "material." + texture.GetTypeName() + number;shader.SetInt(name, i);glBindTexture(GL_TEXTURE_2D, texture.GetTextureID());}// 绑定缓冲glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);glBindVertexArray(VAO);//绘制glDrawElements(GL_TRIANGLES, indices.size(), GL_UNSIGNED_INT, 0);// 解绑glBindVertexArray(0);glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
}void Mesh::CreateBuffer()
{// 创建缓冲glGenVertexArrays(1, &VAO);glGenBuffers(1, &VBO);glGenBuffers(1, &EBO);// 绑定VAOglBindVertexArray(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, false, sizeof(Vertex), (void*)0);glEnableVertexAttribArray(1);glVertexAttribPointer(1, 3, GL_FLOAT, false, sizeof(Vertex), (void*)offsetof(Vertex, normal));glEnableVertexAttribArray(2);glVertexAttribPointer(2, 2, GL_FLOAT, false, sizeof(Vertex), (void*)offsetof(Vertex, texCoord));// 解绑glBindVertexArray(0);glBindBuffer(GL_ARRAY_BUFFER, 0);glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
}
这里我们需要对着色器中纹理采样器命名进行修改,由于在渲染前我们并不知道到底需要多少个sampler2D,所以我们会进行预定义,如下
struct Material {sampler2D tex_diffuse1;sampler2D tex_diffuse2;// ...sampler2D tex_specular1;sampler2D tex_specular2;// ...float shininess;
};
然后在绑定纹理的时候统计diffuseNum和specularNum来设置到对应的槽位上
对此,Texture类也增加了一个方法来根据类型返回不同的前缀名
Texture.h
public:/*** 获取纹理类型的字段名称*/std::string GetTypeName();
Texture.cpp
std::string Texture::GetTypeName()
{if (type == TextureType::DIFFUSE){return "tex_diffuse";}else if (type == TextureType::SPECULAR){return "tex_specular";}else{assert(false);return "";}
}
网格类创建好之后,接下来我们就该创建模型类并加载模型来渲染了
三、模型 (Model)
首先我们需要下载一个模型资源供后续使用,可以在这里下载到一个来自于Berk Gedik设计的吉他生存背包模型,如果以上链接点开没有反应的话,可以右键复制到迅雷中下载,或是在顶部的git仓库中的Resource目录中找到
解压后我们可以得到一个obj文件和几张纹理
接下来我们确定一下模型类的结构
Model.h 新建
#pragma once#include <vector>#include <assimp/Importer.hpp>
#include <assimp/scene.h>
#include <assimp/postprocess.h>#include "Shader.h"
#include "Mesh.h"
#include "Texture.h"/**
* 模型类
*/
class Model
{
public:Model(const char* 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 aiTexType, TextureType texType);
};
在实现之前,我们先捋一下加载的流程
首先我们使用Assimp::Importer的ReadFile方法加载我们的obj文件得到场景aiScene,接着获取到了 aiNode根节点(mRootNode)
接下来使用 ProcessNode 方法来递归解析aiNode,遍历其中的模型aiMesh
使用 ProcessMesh 方法来解析aiMesh,获取其中的顶点mVertices,法向量mNormals,UV坐标 mTextureCoords,索引数组 mIndices
最后,获取aiMesh中的 aiMaterial 并进行纹理的加载
#include "Model.h"Model::Model(const char* path)
{LoadModel(path);
}void Model::Draw(Shader shader)
{for (unsigned int i = 0; i < meshes.size(); ++i){meshes[i].Draw(shader);}
}void Model::LoadModel(std::string path)
{Assimp::Importer importer;// aiProcess_Triangulate 如果模型不完全是三角面组成,需要转化为三角面// aiProcess_FlipUVs OpenGL中需要翻转UVconst aiScene* scene = importer.ReadFile(path, aiProcess_Triangulate | aiProcess_FlipUVs);// AI_SCENE_FLAGS_INCOMPLETE 是否未加载完全if (!scene || scene->mFlags * AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode){std::cout << "[Error] Assimp: " << importer.GetErrorString() << std::endl;return;}// 获取纹理文件的根路径directory = path.substr(0, path.find_last_of('/'));// 处理根节点ProcessNode(scene->mRootNode, scene);
}void Model::ProcessNode(aiNode* node, const aiScene* scene)
{// 遍历Meshfor (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;// 顶点位置vertex.position = glm::vec3(mesh->mVertices[i].x, mesh->mVertices[i].y, mesh->mVertices[i].z);// 法向量if (mesh->HasNormals()){vertex.normal = glm::vec3(mesh->mNormals[i].x, mesh->mNormals[i].y, mesh->mNormals[i].z);}// UVif (mesh->mTextureCoords[0]){vertex.texCoord = glm::vec2(mesh->mTextureCoords[0][i].x, mesh->mTextureCoords[0][i].y);}else{vertex.texCoord = glm::vec2(0.0f, 0.0f);}vertices.push_back(vertex);}for (unsigned int i = 0; i < mesh->mNumFaces; ++i){// Face即为图元PrimitiveaiFace 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, TextureType::DIFFUSE);textures.insert(textures.end(), diffuseMaps.begin(), diffuseMaps.end());std::vector<Texture> specularMaps = LoadMaterialTextures(material, aiTextureType_SPECULAR, TextureType::SPECULAR);textures.insert(textures.end(), specularMaps.begin(), specularMaps.end());}return Mesh(vertices, indices, textures);
}std::vector<Texture> Model::LoadMaterialTextures(aiMaterial* mat, aiTextureType aiTexType, TextureType texType)
{std::vector<Texture> texturesOutput;for (unsigned int i = 0; i < mat->GetTextureCount(aiTexType); ++i){// 获取纹理名称aiString str;mat->GetTexture(aiTexType, i, &str);std::string path = directory + '/' + std::string(str.C_Str());// 判断是否已加载,避免重复加载bool skip = false;for (unsigned int j = 0; j < textures_loaded.size(); ++j){if (std::strcmp(textures_loaded[j].path.data(), path.data()) == 0){skip = true;texturesOutput.push_back(textures_loaded[j]);break;}}// 加载纹理if (!skip){Texture texture(path.data(), texType);texturesOutput.push_back(texture);textures_loaded.push_back(texture);}}return texturesOutput;
}
加载后处理指令 const aiScene* scene = importer.ReadFile(path, aiProcess_Triangulate | aiProcess_FlipUVs);
常用的还有
指令 | 描述 |
---|---|
aiProcess_GenNormals | 如果模型不包含法向量的话,就为每个顶点创建法线 |
aiProcess_SplitLargeMeshes | 将比较大的网格分割成更小的子网格 |
aiProcess_OptimizeMeshes | 将多个小网格拼接为一个大的网格 |
模型类写好了,接下来调整一下以前的main和着色器代码
fragmentshader.glsl (我修改了后缀以供GLSL插件识别)
我们现在只使用平行光,去掉了点光源和聚光灯
#version 330 corestruct Material {sampler2D tex_diffuse1;sampler2D tex_diffuse2;// ...sampler2D tex_specular1;sampler2D tex_specular2;// ...float shininess;
};struct DirLight {vec3 ambient;vec3 diffuse;vec3 specular;vec3 direction;
};in vec3 FragPos;
in vec3 Normal;
in vec2 TexCoords;out vec4 FragColor;uniform vec3 viewPos;uniform Material material;uniform DirLight dirLight;// 计算平行光
vec3 CalcDirectionalLight(DirLight light, vec3 normal, vec3 viewDir)
{vec3 lightDir = normalize(-light.direction);// 环境光vec3 ambient = light.ambient * vec3(texture(material.tex_diffuse1, TexCoords));// 漫反射float diff = max(dot(normal, lightDir), 0);vec3 diffuse = light.diffuse * diff * vec3(texture(material.tex_diffuse1, TexCoords));// 镜面反射vec3 reflectDir = reflect(-lightDir, normal);float spec = pow(max(dot(reflectDir, viewDir), 0), material.shininess);vec3 specular = light.specular * spec * vec3(texture(material.tex_specular1, TexCoords));return ambient + diffuse + specular;
}void main()
{vec3 normal = normalize(Normal);vec3 viewDir = normalize(viewPos - FragPos);// 平行光vec3 result = CalcDirectionalLight(dirLight, normal, viewDir);FragColor = vec4(result, 1.0f);
}
main.cpp
// ...int main()
{glfwInit();glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);GLFWwindow* window = glfwCreateWindow(screenWidth, screenHeight, "OpenGLRenderer", NULL, NULL);if (window == NULL){std::cout << "Failed to create GLFW window" << std::endl;EXIT}glfwMakeContextCurrent(window);if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)){std::cout << "Failed to initialize GLAD" << std::endl;EXIT}glfwSetFramebufferSizeCallback(window, OnSetFrameBufferSize);glfwSetCursorPosCallback(window, ProcessMouseInput);glfwSetScrollCallback(window, ProcessMouseWheelInput);// 别忘了翻转stbi_set_flip_vertically_on_load(true);Transform cameraTransform;cameraTransform.position = glm::vec3(0, 0, 3);cameraTransform.front = glm::vec3(0, 0, -1);cameraTransform.up = glm::vec3(0, 1, 0);cameraTransform.rotate.yaw = -90;camera = Camera(cameraTransform);glEnable(GL_DEPTH_TEST);// 修改了文件后缀,便于GLSL插件识别Shader shader("VertexShader.glsl", "FragmentShader.glsl");// 创建模型,加载backpack.objModel model("F:/Scripts/Cpp/LearnOpenGL/learn-open-gl/Resource/backpack/backpack.obj");while (!glfwWindowShouldClose(window)){float currentFrame = glfwGetTime();deltaTime = currentFrame - lastFrame;lastFrame = currentFrame;glClearColor(0.1f, 0.1f, 0.1f, 1.0f);glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);ProcessKeyboardInput(window);glm::mat4 view = camera.GetViewMatrix();glm::mat4 projection = glm::mat4(1);projection = glm::perspective(glm::radians(camera.fov), screenWidth / screenHeight, 0.1f, 100.0f);shader.Use();shader.SetMat4("view", view);shader.SetMat4("projection", projection);shader.SetVec3("lightColor", glm::vec3(1.0f, 1.0f, 1.0f));shader.SetVec3("viewPos", camera.transform.position);// 平行光参数shader.SetVec3("dirLight.ambient", glm::vec3(0.1f));shader.SetVec3("dirLight.diffuse", glm::vec3(0.9f));shader.SetVec3("dirLight.specular", glm::vec3(0.6f));shader.SetVec3("dirLight.direction", glm::vec3(-0.2f, -1.0f, -0.3f));shader.SetFloat("material.shininess", 32.0f);glm::mat4 modelMatrix = glm::mat4(1.0f);modelMatrix = glm::translate(modelMatrix, glm::vec3(0.0f, 0.0f, 0.0f));shader.SetMat4("model", modelMatrix);// 绘制model.Draw(shader);glfwSwapBuffers(window);glfwPollEvents();}shader.Delete();glfwTerminate();return 0;
}
编译运行,顺利的话可以看见以下图像
直接将漫反射纹理输出会是这样
FragColor = vec4(vec3(texture(material.tex_diffuse1, TexCoords)), 1.0f);
我们还可以加上之前的点光源和聚光灯来获得更好的效果
加上聚光灯
加上点光源
完整代码可在顶部git仓库找到
下接:https://blog.csdn.net/weixin_44506615/article/details/150584832?spm=1001.2014.3001.5502