参考文章---learnopengl-cn --- 模型加载
在之前的文章中,我们用的都是箱子模型,但是现实世界中,有很多不同的模型,例如车子模型,机器人模型,但是这些模型通常都非常复杂,不太能够通过自己手写设置顶点,纹理,法线向量这些数据,然而,和箱子对象不同。实现加载模型的方法是 3D艺术家在Blender、3DS Max或者Maya这样的工具中制作3D模型。
我们所要做的就是将这些模型文件解析,从中提取所有需要的数据(例如 顶点,法线向量,纹理,贴图等),但是3D模型文件有几十种不同的格式,单纯手写解析数据的话无疑是个庞大的工作量,好在我们可以使用第三方库Assimp,Assimp能够导入很多种不同的模型文件格式(并也能够导出部分的格式),它会将所有的模型数据加载至Assimp的通用数据结构中。当Assimp加载完模型之后,我们就能够从Assimp的数据结构中提取我们所需的所有数据了。由于Assimp的数据结构保持不变,不论导入的是什么种类的文件格式,它都能够将我们从这些不同的文件格式中抽象出来,用同一种方式访问我们需要的数据。
当使用Assimp导入一个模型的时候,它通常会将整个模型加载进一个场景(Scene)对象,它会包含导入的模型/场景中的所有数据。Assimp会将场景载入为一系列的节点(Node),每个节点包含了场景对象中所储存数据的索引,每个节点都可以有任意数量的子节点。Assimp数据结构的(简化)模型如下:
- 所有的场景/模型数据都包含在 Scene 对象中, Scene对象也包含了对场景根节点的引用。
- 场景的 Root node(根节点)可能包含子节点(和其它的节点一样),它会有一系列指向场景对象中mMeshes数组中储存的网格数据的索引。Scene下的mMeshes数组储存了真正的Mesh对象,节点中的mMeshes数组保存的只是场景中网格数组的索引。
- 一个Mesh对象本身包含了渲染所需要的所有相关数据,像是顶点位置、法向量、纹理坐标、面(Face)和物体的材质
- 一个网格包含了多个面。Face 代表的是物体的渲染图元(Primitive)(三角形、方形、点)。一个面包含了组成图元的顶点的索引。由于顶点和索引是分开的,使用一个索引缓冲来渲染是非常简单的
- 最后,一个网格也包含了一个Material对象,它包含了一些函数能让我们获取物体的材质属性,比如说颜色和纹理贴图(比如漫反射和镜面光贴图)。
接下来,我们将使用这个库来加载模型,并且解析,绘制3D模型。
assimp编译iOS静态库
这里我们为了节省体积,只编译了arm64架构的静态库
1.首先去github上下载assimp 5.1.3-release版本(写文章时最新的版本),
解压后,进入到port文件夹,如下图所示:
然后进入iOS文件下,修改build.sh文件如下图所示:
然后打开终端,cd到iOS文件夹,然后 ./build.sh,脚本就回执行编译工作,最后生成的静态库会在lib文件夹下,如下图:
生成libassimp.a导入项目就可以了,然后把include文件夹(包含头文件)导入项目,运行项目,会报错(大坑)找到原因是缺少两个静态库(libIrrXML-fat.a 和 libzlibstatic-fat.a)。
2.在assimp目录下下载 5.0.0版本的代码,然后按照上述步骤静态编译,这时候可以在lib文件夹中看到 libIrrXML-fat.a 和 libzlibstatic-fat.a,导入项目即可。
这里为啥不直接使用5.0.0版本呢?是因为5.0.0编译出来的 libassimp.a有几百兆那么大,具体还不知道原因,以后发现了再修改。
-
在项目中build settings 添加头文件路径
代码实现:
首先我们定义两个类 Mesh和ModelLoader,Mesh类用来管理网格数据和具体的数据解析绘制过程,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];
}
}
//递归调用
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句柄等数据)。
- setup操作:通过Assimp加载模型文件,判断加载是否成功,然后就是加工节点
- 加工节点数据:遍历节点和字节点,取出aimesh对象,通过这个对象生成Mesh对象,保存在meshes数组中。
- 4.加工网格数据: 这里就是Mesh对象通过aimesh对象,解析顶点,法线向量等数据
- 5.绘制,遍历meshes数组,绘制其中每一个网格对象(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();
//每个像素点四个字节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类的主要操作如上图代码,就是绑定数据,加载贴图等。
这里我们着重注意loadMaterialTexturesWithMaterial这个函数,这里相当于做了一个优化,因为在每一次绘制中,有可能有的纹理已经生成加载过了,这时候我们可以通过vector保存纹理对象,加载纹理的时候先判断是否已经加载过,如果加载过就不用重新加载,直接取出,没有加载过就加载,这样可以提高性能。
最后看一下实现的效果,如下图:
代码已上传至github.这里添加了一个点光源照明,有兴趣的读者可以结合上篇文章投光物的知识,添加聚光灯等照明效果。