作者:位元組流動
來源:
https://blog.csdn.net/Kennethdroid/article/details/103825593OpenGL ES 3D 模型加載和渲染

上一節簡單介紹了
常用的 3D 模型檔案 Obj 的資料結構和模型加載庫 Assimp 的編譯,本節主要介紹如何使用 Assimp 加載 3D 模型檔案和渲染 3D 模型。
3D 模型的設計一般是由許多小模型拼接組合成一個完整的大模型,一個小模型作為一個獨立的渲染單元,我們稱這些小模型為網格(Mesh)。
網格作為獨立的渲染單元至少需要包含一組頂點資料,每個頂點資料包含一個位置向量,一個法向量和一個紋理坐标,有了紋理坐标也需要為網格指定紋理對應的材質,還有繪制時頂點的索引。
這樣我們可以為 Mesh 定義一個頂點:
struct Vertex {
// 位置向量
glm::vec3 Position;
// 法向量
glm::vec3 Normal;
// 紋理坐标
glm::vec2 TexCoords;
};
還需要一個描述紋理資訊的結構體:
struct Texture
{
GLuint id;//紋理 id ,OpenGL 環境下建立
String type; //紋理類型(diffuse紋理或者specular紋理)
};
網格作為獨立的渲染單元至少需要包含一組頂點資料以及頂點的索引和紋理,可以定義如下:
class Mesh
{
Public:
vector<Vertex> vertices;//一組頂點
vector<GLuint> indices;//頂點對應的索引
vector<Texture> textures;//紋理
Mesh(vector<Vertex> vertices, vector<GLuint> indices, vector<Texture> texture);
Void Draw(Shader shader);
private:
GLuint VAO, VBO, EBO;
void initMesh();
void Destroy();
}
我們通過
initMesh
方法建立相應的
VAO、VBO、EBO,初始化緩沖,設定着色器程式的 uniform 變量。
Mesh(vector<Vertex> vertices, vector<GLuint> indices, vector<Texture> textures)
{
this->vertices = vertices;
this->indices = indices;
this->textures = textures;
this->initMesh();
}
void initMesh()
{
//生成 VAO、VBO、EBO
glGenVertexArrays(1, &this->VAO);
glGenBuffers(1, &this->VBO);
glGenBuffers(1, &this->EBO);
//初始化緩沖區
glBindVertexArray(this->VAO);
glBindBuffer(GL_ARRAY_BUFFER, this->VBO);
glBufferData(GL_ARRAY_BUFFER, this->vertices.size() * sizeof(Vertex),
&this->vertices[0], GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, this->EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, this->indices.size() * sizeof(GLuint),
&this->indices[0], GL_STATIC_DRAW);
// 設定頂點坐标指針
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex),
(GLvoid*)0);
// 設定法線指針
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex),
(GLvoid*)offsetof(Vertex, Normal));
// 設定頂點的紋理坐标
glEnableVertexAttribArray(2);
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex),
(GLvoid*)offsetof(Vertex, TexCoords));
glBindVertexArray(0);
}
//銷毀紋理和緩沖區對象
void Destroy()
{
for (int i = 0; i < textures.size(); ++i) {
glDeleteTextures(1, &textures[i].id);
}
glDeleteBuffers(1, &EBO);
glDeleteBuffers(1, &VBO);
glDeleteVertexArrays(1, &VAO);
VAO = EBO = VBO = GL_NONE;
}
預處理指令
offsetof
用于計算結構體屬性的偏移量,把結構體作為它的第一個參數,第二個參數是這個結構體名字的變量,函數傳回這個變量從結構體開始的位元組偏移量(offset)。如:
offsetof(Vertex, Normal)
傳回 12 個位元組,即 3 * sizeof(float) 。
我們用到的頂點着色器(簡化後):
#version 300 es
layout (location = 0) in vec3 a_position;
layout (location = 1) in vec3 a_normal;
layout (location = 2) in vec2 a_texCoord;
out vec2 v_texCoord;
uniform mat4 u_MVPMatrix;
void main()
{
v_texCoord = a_texCoord;
vec4 position = vec4(a_position, 1.0);
gl_Position = u_MVPMatrix * position;
}
而使用的片段着色器需要根據使用到的紋理數量和類型的不同做不同的調整。如隻有一個 diffuse 紋理的片段着色器如下:
#version 300 es
out vec4 outColor;
in vec2 v_texCoord;
uniform sampler2D texture_diffuse1;
void main()
{
outColor = texture(texture_diffuse1, v_texCoord);
}
假如在一個網格中我們有 3 個 diffuse 紋理和 3 個 specular 紋理,那麼對應的片段着色器中采樣器的聲明如下:
uniform sampler2D texture_diffuse1;
uniform sampler2D texture_diffuse2;
uniform sampler2D texture_diffuse3;
uniform sampler2D texture_specular1;
uniform sampler2D texture_specular2;
uniform sampler2D texture_specular3;
總結起來就是我們需要根據 Mesh 中紋理的數量和類型以及模型光照需求來使用不同的片段着色器和頂點着色器。
Mesh 的渲染的邏輯:
//渲染網格
void Draw(Shader shader)
{
unsigned int diffuseNr = 1;
unsigned int specularNr = 1;
//周遊各個紋理,根據紋理的數量和類型确定采樣器變量名
for(unsigned int i = 0; i < textures.size(); i++)
{
glActiveTexture(GL_TEXTURE0 + i); // active proper texture unit before binding
string number;
string name = textures[i].type;
if(name == "texture_diffuse")
number = std::to_string(diffuseNr++);
else if(name == "texture_specular")
number = std::to_string(specularNr++); // transfer unsigned int to stream
glUniform1i(glGetUniformLocation(shader.ID, (name + number).c_str()), i);
// and finally bind the texture
glBindTexture(GL_TEXTURE_2D, textures[i].id);
}
//繪制網格
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, indices.size(), GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
glActiveTexture(GL_TEXTURE0);
}
Shader 類的邏輯簡單包含了着色器程式的建立銷毀和 uniform 類型變量的設定。
class Shader
{
public:
unsigned int ID;//着色器程式的 ID
Shader(const char* vertexStr, const char* fragmentStr)
{
//建立着色器程式
ID = GLUtils::CreateProgram(vertexStr, fragmentStr);
}
void Destroy()
{
//銷毀着色器程式
GLUtils::DeleteProgram(ID);
}
void use()
{
glUseProgram(ID);
}
void setFloat(const std::string &name, float value) const
{
glUniform1f(glGetUniformLocation(ID, name.c_str()), value);
}
void setVec3(const std::string &name, float x, float y, float z) const
{
glUniform3f(glGetUniformLocation(ID, name.c_str()), x, y, z);
}
void setMat4(const std::string &name, const glm::mat4 &mat) const
{
glUniformMatrix4fv(glGetUniformLocation(ID, name.c_str()), 1, GL_FALSE, &mat[0][0]);
}
};
前面我們知道了一個模型(Model)包含許多個網格(Mesh),各個 Mesh 獨立渲染共同組成整個 Model。Model 類可定義如下:
class Model
{
public:
Model(GLchar* path)
{
loadModel(path);
}
//渲染模型,即依次渲染各個網格
void Draw(Shader shader);
//銷毀模型的所有網格
void Destroy();
private:
//模型所包含的網格
vector<Mesh> meshes;
//模型檔案所在目錄
string directory;
//加載模型
void loadModel(string path);
//處理 aiScene 對象包含的節點和子節點
void processNode(aiNode* node, const aiScene* scene);
//生成網格
Mesh processMesh(aiMesh* mesh, const aiScene* scene);
//建立紋理并加載圖像資料
vector<Texture> loadMaterialTextures(aiMaterial* mat, aiTextureType type, string typeName);
};
使用 Assimp 加載 3D 模型比較簡單,最終模型被加載到一個 Assimp 中定義的
aiScene
對象中,
aiScene
對象除了包含網格和材質,還包含一個
aiNode
對象(根節點),然後我們還需要周遊各個子節點的網格。
#include "assimp/Importer.hpp"
#include "assimp/scene.h"
#include "assimp/postprocess.h"
Assimp::Importer importer;
const aiScene* scene = importer.ReadFile(path, aiProcess_Triangulate | aiProcess_FlipUVs);
參數
aiProcess_Triangulate
表示如果模型不是(全部)由三角形組成,應該轉換所有的模型的原始幾何形狀為三角形;
aiProcess_FlipUVs
表示基于 y 軸翻轉紋理坐标。
Model 類中加載模型的函數:
void loadModel(string const &path)
{
Assimp::Importer importer;
const aiScene* scene = importer.ReadFile(path, aiProcess_Triangulate | aiProcess_FlipUVs);
if(!scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode) // if is Not Zero
{
LOGCATE("Model::loadModel path=%s, assimpError=%s", path, importer.GetErrorString());
return;
}
directory = path.substr(0, path.find_last_of('/'));
//處理節點
processNode(scene->mRootNode, scene);
}
//遞歸處理所有節點
void processNode(aiNode *node, const aiScene *scene)
{
for(unsigned int i = 0; i < node->mNumMeshes; i++)
{
aiMesh* mesh = scene->mMeshes[node->mMeshes[i]];
if(mesh != nullptr)
meshes.push_back(processMesh(mesh, scene));
}
for(unsigned int i = 0; i < node->mNumChildren; i++)
{
processNode(node->mChildren[i], scene);
}
}
//生成網格 Mesh
Mesh processMesh(aiMesh* mesh, const aiScene* scene)
{
vector<Vertex> vertices;
vector<unsigned int> indices;
vector<Texture> textures;
for(GLuint i = 0; i < mesh->mNumVertices; i++)
{
Vertex vertex;
// 處理頂點坐标、法線和紋理坐标
...
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];
vector<Texture> diffuseMaps = loadMaterialTextures(material, aiTextureType_DIFFUSE, "texture_diffuse");
textures.insert(textures.end(), diffuseMaps.begin(), diffuseMaps.end());
...
}
return Mesh(vertices, indices, textures);
}
在 native 層加載紋理的時候,我們使用 OpenCV 對圖檔進行解碼,然後生成紋理對象:
unsigned int TextureFromFile(const char *path, const string &directory)
{
string filename = string(path);
filename = directory + '/' + filename;
unsigned int textureID;
glGenTextures(1, &textureID);
unsigned char *data = nullptr;
LOGCATE("TextureFromFile Loading texture %s", filename.c_str());
//使用 OpenCV 對圖檔進行解碼
cv::Mat textureImage = cv::imread(filename);
if (!textureImage.empty())
{
// OpenCV 預設解碼成 BGR 格式,這裡轉換為 RGB
cv::cvtColor(textureImage, textureImage, CV_BGR2RGB);
glBindTexture(GL_TEXTURE_2D, textureID);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, textureImage.cols,
textureImage.rows, 0, GL_RGB, GL_UNSIGNED_BYTE,
textureImage.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);
GO_CHECK_GL_ERROR();
} else {
LOGCATE("TextureFromFile Texture failed to load at path: %s", path);
}
return textureID;
}
繪制模型就是周遊每個 Mesh 進行繪制:
void Draw(Shader shader)
{
for(unsigned int i = 0; i < meshes.size(); i++)
meshes[i].Draw(shader);
}
最後就是這個 Model 類的使用示例:
//初始化,加載模型
m_pModel = new Model("/sdcard/model/poly/Apricot_02_hi_poly.obj");
m_pShader = new Shader(vShaderStr, fShaderStr);
//繪制模型
glClearColor(0.5f, 0.5f, 0.5f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glEnable(GL_DEPTH_TEST);
UpdateMVPMatrix(m_MVPMatrix, m_AngleX, m_AngleY, (float)screenW / screenH);
m_pShader->use();
m_pShader->setMat4("u_MVPMatrix", m_MVPMatrix);
m_pModel->Draw((*m_pShader));
//銷毀對象
if (m_pModel != nullptr) {
m_pModel->Destroy();
delete m_pModel;
m_pModel = nullptr;
}
if (m_pShader != nullptr) {
m_pShader->Destroy();
delete m_pShader;
m_pShader = nullptr;
}
實作代碼路徑:
NDK_OpenGLES_3_0「視訊雲技術」你最值得關注的音視訊技術公衆号,每周推送來自阿裡雲一線的實踐技術文章,在這裡與音視訊領域一流工程師交流切磋。