上接: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