參考文章---learnopengl-cn --- 模型加載
在之前的文章中,我們用的都是箱子模型寄猩,但是現(xiàn)實(shí)世界中找岖,有很多不同的模型,例如車子模型敛滋,機(jī)器人模型许布,但是這些模型通常都非常復(fù)雜,不太能夠通過自己手寫設(shè)置頂點(diǎn)绎晃,紋理蜜唾,法線向量這些數(shù)據(jù),然而庶艾,和箱子對象不同袁余。實(shí)現(xiàn)加載模型的方法是 3D藝術(shù)家在Blender、3DS Max或者M(jìn)aya這樣的工具中制作3D模型咱揍。
我們所要做的就是將這些模型文件解析颖榜,從中提取所有需要的數(shù)據(jù)(例如 頂點(diǎn),法線向量煤裙,紋理掩完,貼圖等),但是3D模型文件有幾十種不同的格式硼砰,單純手寫解析數(shù)據(jù)的話無疑是個龐大的工作量且蓬,好在我們可以使用第三方庫Assimp,Assimp能夠?qū)牒芏喾N不同的模型文件格式(并也能夠?qū)С霾糠值母袷剑鼤⑺械哪P蛿?shù)據(jù)加載至Assimp的通用數(shù)據(jù)結(jié)構(gòu)中题翰。當(dāng)Assimp加載完模型之后恶阴,我們就能夠從Assimp的數(shù)據(jù)結(jié)構(gòu)中提取我們所需的所有數(shù)據(jù)了。由于Assimp的數(shù)據(jù)結(jié)構(gòu)保持不變豹障,不論導(dǎo)入的是什么種類的文件格式冯事,它都能夠?qū)⑽覀儚倪@些不同的文件格式中抽象出來,用同一種方式訪問我們需要的數(shù)據(jù)血公。
當(dāng)使用Assimp導(dǎo)入一個模型的時候桅咆,它通常會將整個模型加載進(jìn)一個場景(Scene)對象,它會包含導(dǎo)入的模型/場景中的所有數(shù)據(jù)坞笙。Assimp會將場景載入為一系列的節(jié)點(diǎn)(Node)岩饼,每個節(jié)點(diǎn)包含了場景對象中所儲存數(shù)據(jù)的索引,每個節(jié)點(diǎn)都可以有任意數(shù)量的子節(jié)點(diǎn)薛夜。Assimp數(shù)據(jù)結(jié)構(gòu)的(簡化)模型如下:
- 所有的場景/模型數(shù)據(jù)都包含在 Scene 對象中籍茧, Scene對象也包含了對場景根節(jié)點(diǎn)的引用。
- 場景的 Root node(根節(jié)點(diǎn))可能包含子節(jié)點(diǎn)(和其它的節(jié)點(diǎn)一樣)梯澜,它會有一系列指向場景對象中mMeshes數(shù)組中儲存的網(wǎng)格數(shù)據(jù)的索引寞冯。Scene下的mMeshes數(shù)組儲存了真正的Mesh對象,節(jié)點(diǎn)中的mMeshes數(shù)組保存的只是場景中網(wǎng)格數(shù)組的索引。
- 一個Mesh對象本身包含了渲染所需要的所有相關(guān)數(shù)據(jù)吮龄,像是頂點(diǎn)位置俭茧、法向量、紋理坐標(biāo)漓帚、面(Face)和物體的材質(zhì)
- 一個網(wǎng)格包含了多個面母债。Face 代表的是物體的渲染圖元(Primitive)(三角形、方形尝抖、點(diǎn))毡们。一個面包含了組成圖元的頂點(diǎn)的索引。由于頂點(diǎn)和索引是分開的昧辽,使用一個索引緩沖來渲染是非常簡單的
- 最后衙熔,一個網(wǎng)格也包含了一個Material對象,它包含了一些函數(shù)能讓我們獲取物體的材質(zhì)屬性搅荞,比如說顏色和紋理貼圖(比如漫反射和鏡面光貼圖)红氯。
接下來,我們將使用這個庫來加載模型咕痛,并且解析脖隶,繪制3D模型。
assimp編譯iOS靜態(tài)庫
這里我們?yōu)榱斯?jié)省體積暇检,只編譯了arm64架構(gòu)的靜態(tài)庫
1.首先去github上下載assimp 5.1.3-release版本(寫文章時最新的版本),
解壓后产阱,進(jìn)入到port文件夾,如下圖所示:
然后進(jìn)入iOS文件下块仆,修改build.sh文件如下圖所示:
然后打開終端,cd到iOS文件夾构蹬,然后 ./build.sh,腳本就回執(zhí)行編譯工作,最后生成的靜態(tài)庫會在lib文件夾下悔据,如下圖:
生成libassimp.a導(dǎo)入項(xiàng)目就可以了庄敛,然后把include文件夾(包含頭文件)導(dǎo)入項(xiàng)目,運(yùn)行項(xiàng)目科汗,會報錯(大坑)找到原因是缺少兩個靜態(tài)庫(libIrrXML-fat.a 和 libzlibstatic-fat.a)藻烤。
2.在assimp目錄下下載 5.0.0版本的代碼,然后按照上述步驟靜態(tài)編譯头滔,這時候可以在lib文件夾中看到 libIrrXML-fat.a 和 libzlibstatic-fat.a,導(dǎo)入項(xiàng)目即可怖亭。
這里為啥不直接使用5.0.0版本呢?是因?yàn)?.0.0編譯出來的 libassimp.a有幾百兆那么大坤检,具體還不知道原因兴猩,以后發(fā)現(xiàn)了再修改。
-
在項(xiàng)目中build settings 添加頭文件路徑
image.png
代碼實(shí)現(xiàn):
首先我們定義兩個類 Mesh和ModelLoader,Mesh類用來管理網(wǎng)格數(shù)據(jù)和具體的數(shù)據(jù)解析繪制過程早歇,ModelLoader類用來處理加載模型文件倾芝,解析模型文件讨勤。
由于assimp庫是基于C++,所以這兩個類的后綴都要改為.mm晨另。
ModelLoader
- (instancetype)initWithFilePath:(NSString *)filepath andContext:(nonnull ESContext *)eglContext {
if (self = [super init]) {
self.filePath = filepath;
self.meshes = [NSMutableArray new];
self.eglContext = eglContext;
[self setup];
}
return self;
}
- (void)setup {
Assimp::Importer importer;
const aiScene *scene = importer.ReadFile(_filePath.cString, aiProcess_FlipUVs | aiProcess_Triangulate);
if (!scene || (scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE) || !scene->mRootNode) {
NSLog(@"Error: Assimp failed to open obj file: %@",_filePath);
return;
}
NSString *directory = [_filePath stringByDeletingLastPathComponent];
self.filePath = directory;
self.scene = scene;
[self processNode:self.scene->mRootNode];
}
- (void)processNode:(aiNode *)node {
for (unsigned int i = 0; i < node->mNumMeshes; i++) {
aiMesh *mesh = self.scene->mMeshes[node->mMeshes[i]];
Mesh *oneMesh = [self processMesh:mesh];
if (oneMesh != nil) {
[self.meshes addObject:oneMesh];
}
}
//遞歸調(diào)用
for (unsigned int i = 0; i < node->mNumChildren; i++) {
[self processNode:node->mChildren[i]];
}
}
- (Mesh *)processMesh:(aiMesh *)mesh {
Mesh *one = [Mesh new];
one.eglContext = self.eglContext;
one.directory = string([self.filePath cStringUsingEncoding:NSUTF8StringEncoding]);
[one parseWithMesh:mesh andAIScnene:self.scene];
return one;
}
- (void)draw {
for (Mesh *one in self.meshes) {
[one draw];
}
}
我們看一下具體步驟:
- 1.初始化傳入模型文件路徑和ESContext對象(包含glprogram句柄等數(shù)據(jù))潭千。
- setup操作:通過Assimp加載模型文件,判斷加載是否成功借尿,然后就是加工節(jié)點(diǎn)
- 加工節(jié)點(diǎn)數(shù)據(jù):遍歷節(jié)點(diǎn)和字節(jié)點(diǎn)刨晴,取出aimesh對象,通過這個對象生成Mesh對象垛玻,保存在meshes數(shù)組中。
- 4.加工網(wǎng)格數(shù)據(jù): 這里就是Mesh對象通過aimesh對象奶躯,解析頂點(diǎn)帚桩,法線向量等數(shù)據(jù)
- 5.繪制,遍歷meshes數(shù)組嘹黔,繪制其中每一個網(wǎng)格對象(Mesh).
Mesh
- (void)parseWithMesh:(aiMesh *)mesh andAIScnene:(const aiScene *)scene {
for (unsigned int i = 0; i < mesh->mNumVertices; i++) {
Vertex vertex;
Vector3 vector;
//position
vector.x = mesh->mVertices[i].x;
vector.y = mesh->mVertices[i].y;
vector.z = mesh->mVertices[i].z;
vertex.position = vector;
//normals
if (mesh->HasNormals()) {
vector.x = mesh->mNormals[i].x;
vector.y = mesh->mNormals[i].y;
vector.z = mesh->mNormals[i].z;
vertex.normal = vector;
}
//texture coordinates
if (mesh->mTextureCoords[0]) {
Vector2 vec;
vec.x = mesh->mTextureCoords[0][i].x;
vec.y = mesh->mTextureCoords[0][i].y;
vertex.textCoord = vec;
}
else {
vertex.textCoord = {{0.0f,0.0f}};
}
vertices.push_back(vertex);
}
// now wak through each of the mesh's faces (a face is a mesh its triangle) and retrieve the corresponding vertex indices.
for(unsigned int i = 0; i < mesh->mNumFaces; i++)
{
aiFace face = mesh->mFaces[i];
// retrieve all indices of the face and store them in the indices vector
for(unsigned int j = 0; j < face.mNumIndices; j++)
indices.push_back(face.mIndices[j]);
}
// process materials
aiMaterial* material = scene->mMaterials[mesh->mMaterialIndex];
// we assume a convention for sampler names in the shaders. Each diffuse texture should be named
// as 'texture_diffuseN' where N is a sequential number ranging from 1 to MAX_SAMPLER_NUMBER.
// Same applies to other texture as the following list summarizes:
// diffuse: texture_diffuseN
// specular: texture_specularN
// normal: texture_normalN
// 1. diffuse maps
vector<Texture> diffuseMaps = [self loadMaterialTexturesWithMaterial:material andTextureType:aiTextureType_DIFFUSE andTypeName:"texture_diffuse"];
textures.insert(textures.end(), diffuseMaps.begin(), diffuseMaps.end());
// 2. specular maps
vector<Texture> specularMaps = [self loadMaterialTexturesWithMaterial:material andTextureType:aiTextureType_SPECULAR andTypeName:"texture_specular"];
textures.insert(textures.end(), specularMaps.begin(), specularMaps.end());
// 3. normal maps
vector<Texture> normalMaps = [self loadMaterialTexturesWithMaterial:material andTextureType:aiTextureType_NORMALS andTypeName:"texture_normal"];
textures.insert(textures.end(), normalMaps.begin(), normalMaps.end());
// 4. height maps
std::vector<Texture> heightMaps = [self loadMaterialTexturesWithMaterial:material andTextureType:aiTextureType_HEIGHT andTypeName:"texture_height"];
textures.insert(textures.end(), heightMaps.begin(), heightMaps.end());
[self setupMesh];
}
- (vector<Texture>)loadMaterialTexturesWithMaterial:(aiMaterial *)mat andTextureType:(aiTextureType)type andTypeName:(string)typeName {
{
vector<Texture> textures;
for(unsigned int i = 0; i < mat->GetTextureCount(type); i++)
{
aiString str;
mat->GetTexture(type, i, &str);
// check if texture was loaded before and if so, continue to next iteration: skip loading a new texture
bool skip = false;
for(unsigned int j = 0; j < textures_loaded.size(); j++)
{
char *texturePath = textures_loaded[j].path.data();
if(std::strcmp(texturePath, str.C_Str()) == 0)
{
textures.push_back(textures_loaded[j]);
skip = true; // a texture with the same filepath has already been loaded, continue to next one. (optimization)
break;
}
}
if(!skip)
{ // if texture hasn't been loaded already, load it
Texture texture;
texture.id = TextureFromFile(str.C_Str(), self.directory);
texture.type = typeName;
texture.path = str.C_Str();
textures.push_back(texture);
textures_loaded.push_back(texture); // store it as texture loaded for entire model, to ensure we won't unnecesery load duplicate textures.
}
}
return textures;
}
}
unsigned int TextureFromFile(const char *path, const string &directory)
{
string filename = string(path);
filename = directory + '/' + filename;
unsigned int textureID;
glGenTextures(1, &textureID);
int width, height, nrComponents;
UIImage *img = [UIImage imageWithContentsOfFile:[[NSString alloc] initWithCString:filename.data() encoding:NSUTF8StringEncoding]];
CGImageRef imageref = [img CGImage];
width = CGImageGetWidth(imageref);
height = CGImageGetHeight(imageref);
GLubyte *textureData = (GLubyte *)malloc(width * height * 4);
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
//每個像素點(diǎn)四個字節(jié)RGBA
NSUInteger bytesperPixel = 4;
NSUInteger bytesperRow = bytesperPixel * width;
NSUInteger bitsperComponent = 8;
CGContextRef context = CGBitmapContextCreate(textureData, width, height, bitsperComponent, bytesperRow, colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageref);
CGColorSpaceRelease(colorSpace);
CGContextRelease(context);
NSData *_imageData = [NSData dataWithBytes:textureData length:(width * height * 4)];
if (_imageData)
{
glBindTexture(GL_TEXTURE_2D, textureID);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, (unsigned char *)_imageData.bytes);
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);
}
else
{
printf("Texture failed to load at path: %s",path);
}
return textureID;
}
- (void)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);
/**
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
layout (location = 2) in vec2 aTexCoords;
*/
GLuint positionIndex = glGetAttribLocation(_eglContext->program, "aPos");
GLuint texCoordIndex = glGetAttribLocation(_eglContext->program, "aTexCoords");
GLuint normalIndex = glGetAttribLocation(_eglContext->program, "aNormal");
// vertex Positions
glEnableVertexAttribArray(positionIndex);
glVertexAttribPointer(positionIndex, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)0);
// vertex normals
glEnableVertexAttribArray(normalIndex);
glVertexAttribPointer(normalIndex, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, normal));
// vertex texture coords
glEnableVertexAttribArray(texCoordIndex);
glVertexAttribPointer(texCoordIndex, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, textCoord));
glBindVertexArray(0);
}
-(void)draw {
unsigned int diffuseNr = 1;
unsigned int specularNr = 1;
unsigned int normalNr = 1;
unsigned int heightNr = 1;
for (unsigned int i = 0; i < textures.size(); i++)
{
glActiveTexture(GL_TEXTURE0 + i); // active proper texture unit before binding
// retrieve texture number (the N in diffuse_textureN)
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
else if (name == "texture_normal")
number = std::to_string(normalNr++); // transfer unsigned int to stream
else if (name == "texture_height")
number = std::to_string(heightNr++); // transfer unsigned int to stream
const char * one = (name + number).c_str();
GLuint index = glGetUniformLocation(_eglContext->program, one);
glUniform1i(index, i);
glBindTexture(GL_TEXTURE_2D, textures[i].id);
}
glActiveTexture(GL_TEXTURE0);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, indices.size(), GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
}
mesh類的主要操作如上圖代碼,就是綁定數(shù)據(jù),加載貼圖等熄驼。
這里我們著重注意loadMaterialTexturesWithMaterial這個函數(shù)蛤售,這里相當(dāng)于做了一個優(yōu)化,因?yàn)樵诿恳淮卫L制中喂江,有可能有的紋理已經(jīng)生成加載過了召锈,這時候我們可以通過vector保存紋理對象,加載紋理的時候先判斷是否已經(jīng)加載過获询,如果加載過就不用重新加載涨岁,直接取出,沒有加載過就加載吉嚣,這樣可以提高性能梢薪。
最后看一下實(shí)現(xiàn)的效果,如下圖:
代碼已上傳至github.這里添加了一個點(diǎn)光源照明尝哆,有興趣的讀者可以結(jié)合上篇文章投光物的知識秉撇,添加聚光燈等照明效果。