================================ 前序===============================
AndroidLearnOpenGL是本博主自己实现的LearnOpenGL练习集合:
Github地址:https://github.com/wangyongyao1989/AndroidLearnOpenGL
系列文章:
5、LearnOpenGL之3D模型加载
============================== 显示效果 ===============================
GL3D模型显示
=============================================================
想要的是将这些模型(Model)导入(Import)到程序当中。模型通常都由3D艺术家在Blender、3DS Max或者Maya这样的工具中精心制作。
这些所谓的3D建模工具(3D Modeling Tool)可以让艺术家创建复杂的形状,并使用一种叫做UV映射(uv-mapping)的手段来应用贴图。这些工具将会在导出到模型文件的时候自动生成所有的顶点坐标、顶点法线以及纹理坐标。这样子艺术家们即使不了解图形技术细节的情况下,也能拥有一套强大的工具来构建高品质的模型了。所有的技术细节都隐藏在了导出的模型文件中。但是,作为图形开发者,我们就必须要了解这些技术细节了。
我们的工作就是解析这些导出的模型文件以及提取所有有用的信息,将它们储存为OpenGL能够理解的格式。一个很常见的问题是,模型的文件格式有很多种,每一种都会以它们自己的方式来导出模型数据。像是Wavefront的.obj这样的模型格式,只包含了模型数据以及材质信息,像是模型颜色和漫反射/镜面光贴图。而以XML为基础的Collada文件格式则非常的丰富,包含模型、光照、多种材质、动画数据、摄像机、完整的场景信息等等。Wavefront的.obj格式通常被认为是一个易于解析的模型格式。建议至少去Wavefront的wiki页面上看看文件格式的信息是如何封装的。这应该能让你认识到模型文件的基本结构。
一、模型加载库:
一个非常流行的模型导入库是Assimp,它是Open Asset Import Library(开放的资产导入库)的缩写。Assimp能够导入很多种不同的模型文件格式(并也能够导出部分的格式),它会将所有的模型数据加载至Assimp的通用数据结构中。当Assimp加载完模型之后,我们就能够从Assimp的数据结构中提取我们所需的所有数据了。由于Assimp的数据结构保持不变,不论导入的是什么种类的文件格式,它都能够将我们从这些不同的文件格式中抽象出来,用同一种方式访问我们需要的数据。
Assimp的github项目地址:https://github.com/assimp/assimp?tab=readme-ov-file
另有JAVA代码实现的3D模型加载开源代码地址:android-3D-model-viewer
当使用Assimp导入一个模型的时候,它通常会将整个模型加载进一个场景(Scene)对象,它会包含导入的模型/场景中的所有数据。Assimp会将场景载入为一系列的节点(Node),每个节点包含了场景对象中所储存数据的索引,每个节点都可以有任意数量的子节点。Assimp数据结构的(简化)模型如下:
- 和材质和网格(Mesh)一样,所有的场景/模型数据都包含在Scene对象中。Scene对象也包含了场景根节点的引用。
- 场景的Root node(根节点)可能包含子节点(和其它的节点一样),它会有一系列指向场景对象中mMeshes数组中储存的网格数据的索引。Scene下的mMeshes数组储存了真正的Mesh对象,节点中的mMeshes数组保存的只是场景中网格数组的索引。
- 一个Mesh对象本身包含了渲染所需要的所有相关数据,像是顶点位置、法向量、纹理坐标、面(Face)和物体的材质。
- 一个网格包含了多个面。Face代表的是物体的渲染图元(Primitive)(三角形、方形、点)。一个面包含了组成图元的顶点的索引。由于顶点和索引是分开的,使用一个索引缓冲来渲染是非常简单的。
- 最后,一个网格也包含了一个Material对象,它包含了一些函数能让我们获取物体的材质属性,比如说颜色和纹理贴图(比如漫反射和镜面光贴图)
我们需要做的第一件事是将一个物体加载到Scene对象中,遍历节点,获取对应的Mesh对象(我们需要递归搜索每个节点的子节点),并处理每个Mesh对象来获取顶点数据、索引以及它的材质属性。最终的结果是一系列的网格数据,我们会将它们包含在一个Model
对象中。
二、网格(Mesh):
一个网格是我们在OpenGL中绘制物体所需的最小单位(顶点数据、索引和材质属性)。一个模型(通常)会包括多个网格。
一个网格应该至少需要一系列的顶点,每个顶点包含一个位置向量、一个法向量和一个纹理坐标向量。一个网格还应该包含用于索引绘制的索引以及纹理形式的材质数据(漫反射/镜面光贴图)。
1、顶点定义:
将所有需要的向量储存到一个叫做Vertex的结构体中,我们可以用它来索引每个顶点属性。
struct Vertex {
glm::vec3 Position;
glm::vec3 Normal;
glm::vec2 TexCoords;
};
2、纹理定义:
将纹理数据整理到一个Texture结构体中。储存了纹理的id以及它的类型,比如是漫反射贴图或者是镜面光贴图。
struct Texture {
unsigned int id;
string type;
};
3、定义网络类的结构:
在构造函数中,我们将所有必须的数据赋予了网格,我们在setupMesh函数中初始化缓冲,并最终使用Draw函数来绘制网格。注意我们将一个着色器传入了Draw函数中,将着色器传入网格类中可以让我们在绘制之前设置一些uniform(像是链接采样器到纹理单元)。
class Mesh {
public:
/* 网格数据 */
vector<Vertex> vertices;
vector<unsigned int> indices;
vector<Texture> textures;
/* 函数 */
Mesh(vector<Vertex> vertices, vector<unsigned int> indices
, vector<Texture> textures);
void Draw(Shader shader);
private:
/* 渲染数据 */
unsigned int VAO, VBO, EBO;
/* 函数 */
void setupMesh();
};
Mesh(vector<Vertex> vertices, vector<unsigned int> indices
, vector<Texture> textures)
{
this->vertices = vertices;
this->indices = indices;
this->textures = textures;
setupMesh();
}
4、Mesh网络类setupMesh函数:
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);
// 顶点位置
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)0);
// 顶点法线
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex)
, (void*)offsetof(Vertex, Normal));
// 顶点纹理坐标
glEnableVertexAttribArray(2);
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex)
, (void*)offsetof(Vertex, TexCoords));
glBindVertexArray(0);
}
5、Mesh网络类Draw函数:
在真正渲染这个网格之前,我们需要在调用glDrawElements函数之前先绑定相应的纹理。然而,这实际上有些困难,我们一开始并不知道这个网格(如果有的话)有多少纹理、纹理是什么类型的。所以我们该如何在着色器中设置纹理单元和采样器呢?
我们需要设定一个命名标准:每个漫反射纹理被命名为texture_diffuseN
,每个镜面光纹理应该被命名为texture_specularN
,其中N
的范围是1到纹理采样器最大允许的数字。
首先计算了每个纹理类型的N-分量,并将其拼接到纹理类型字符串上,来获取对应的uniform名称。接下来我们查找对应的采样器,将它的位置值设置为当前激活的纹理单元,并绑定纹理。这也是我们在Draw函数中需要着色器的原因。我们也将"material."
添加到了最终的uniform名称中,因为我们希望将纹理储存在一个材质结构体中(这在每个实现中可能都不同)。
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); // 在绑定之前激活相应的纹理单元
// 获取纹理序号(diffuse_textureN 中的 N)
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++);
shader.setInt(("material." + name + number).c_str(), i);
glBindTexture(GL_TEXTURE_2D, textures[i].id);
}
glActiveTexture(GL_TEXTURE0);
// 绘制网格
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, indices.size(), GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
}
三、模型:
Model类包含了一个Mesh对象的vector,构造函数需要我们给它一个文件路径。在构造函数中,它会直接通过loadModel来加载文件。
class Model
{
public:
/* 函数 */
Model(char *path)
{
loadModel(path);
}
void Draw(Shader shader);
private:
/* 模型数据 */
vector<Mesh> meshes;
string directory;
/* 函数 */
void loadModel(string path);
void processNode(aiNode *node, const aiScene *scene);
Mesh processMesh(aiMesh *mesh, const aiScene *scene);
vector<Texture> loadMaterialTextures(aiMaterial *mat, aiTextureType type,
string typeName);
};
Draw函数遍历所有网络,并调用各自的Draw函数。
void Draw(Shader &shader)
{
for(unsigned int i = 0; i < meshes.size(); i++)
meshes[i].Draw(shader);
}
四、导入3D模型到OpenGL:
1、头文件导入:
要想导入一个模型,并将它转换到我们自己的数据结构中的话,首先我们需要包含Assimp对应的头文件:
#include <assimp/Importer.hpp>
#include <assimp/scene.h>
#include <assimp/postprocess.h>
2、文件读取:
首先需要调用的函数是loadModel,它会从构造函数中直接调用。在loadModel中,我们使用Assimp来加载模型至Assimp的一个叫做scene的数据结构中。
Assimp::Importer importer;
const aiScene *scene = importer.ReadFile(path
, aiProcess_Triangulate | aiProcess_FlipUVs);
除了加载文件之外,Assimp允许我们设定一些选项来强制它对导入的数据做一些额外的计算或操作。
- aiProcess_Triangulate:告诉Assimp,如果模型不是(全部)由三角形组成,它需要将模型所有的图元形状变换为三角形。
- aiProcess_FlipUVs:将在处理的时候翻转y轴的纹理坐标。
- aiProcess_GenNormals:如果模型不包含法向量的话,就为每个顶点创建法线。
- aiProcess_SplitLargeMeshes:将比较大的网格分割成更小的子网格,如果你的渲染有最大顶点数限制,只能渲染较小的网格,那么它会非常有用。
- aiProcess_OptimizeMeshes:和上个选项相反,它会将多个小网格拼接为一个大的网格,减少绘制调用从而进行优化。
3、loadModel函数:
加载了模型之后,我们会检查场景和其根节点不为null,并且检查了它的一个标记(Flag),来查看返回的数据是不是不完整的。如果遇到了任何错误,我们都会通过导入器的GetErrorString函数来报告错误并返回。我们也获取了文件路径的目录路径。
void loadModel(string path)
{
Assimp::Importer import;
const aiScene *scene = import.ReadFile(path, aiProcess_Triangulate
|aiProcess_FlipUVs);
if(!scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE
|| !scene->mRootNode)
{
cout << "ERROR::ASSIMP::" << import.GetErrorString() << endl;
return;
}
directory = path.substr(0, path.find_last_of('/'));
processNode(scene->mRootNode, scene);
}
4、processNode函数:
在Assimp的结构中,每个节点包含了一系列的网格索引,每个索引指向场景对象中的那个特定网格。我们接下来就想去获取这些网格索引,获取每个网格,处理每个网格,接着对每个节点的子节点重复这一过程。processNode函数的内容如下:
void processNode(aiNode *node, const aiScene *scene)
{
// 处理节点所有的网格(如果有的话)
for(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);
}
}
5、processMesh函数:
将一个aiMesh
对象转化为我们自己的网格对象不是那么困难。我们要做的只是访问网格的相关属性并将它们储存到我们自己的对象中。processMesh函数的大体结构如下:
Mesh processMesh(aiMesh *mesh, const aiScene *scene)
{
vector<Vertex> vertices;
vector<unsigned int> indices;
vector<Texture> textures;
for(unsigned int i = 0; i < mesh->mNumVertices; i++)
{
Vertex vertex;
// 处理顶点位置、法线和纹理坐标
...
vertices.push_back(vertex);
}
// 处理索引
...
// 处理材质
if(mesh->mMaterialIndex >= 0)
{
...
}
return Mesh(vertices, indices, textures);
}
处理网格的过程主要有三部分:获取所有的顶点数据,获取它们的网格索引,并获取相关的材质数据。处理后的数据将会储存在三个vector当中,我们会利用它们构建一个Mesh对象,并返回它到函数的调用者那里。
6、顶点获取:
定义了一个Vertex结构体,我们将在每个迭代之后将它加到vertices数组中。我们会遍历网格中的所有顶点(使用mesh->mNumVertices
来获取)。在每个迭代中,我们希望使用所有的相关数据填充这个结构体。顶点的位置是这样处理的:
glm::vec3 vector;
vector.x = mesh->mVertices[i].x;
vector.y = mesh->mVertices[i].y;
vector.z = mesh->mVertices[i].z;
vertex.Position = vector;
7、法线处理:
vector.x = mesh->mNormals[i].x;
vector.y = mesh->mNormals[i].y;
vector.z = mesh->mNormals[i].z;
vertex.Normal = vector;
8、纹理坐标处理:
纹理坐标的处理也大体相似,但Assimp允许一个模型在一个顶点上有最多8个不同的纹理坐标,我们不会用到那么多,我们只关心第一组纹理坐标。我们同样也想检查网格是否真的包含了纹理坐标(可能并不会一直如此):
if(mesh->mTextureCoords[0]) // 网格是否有纹理坐标?
{
glm::vec2 vec;
vec.x = mesh->mTextureCoords[0][i].x;
vec.y = mesh->mTextureCoords[0][i].y;
vertex.TexCoords = vec;
}
else
vertex.TexCoords = glm::vec2(0.0f, 0.0f);
9、索引:
Assimp的接口定义了每个网格都有一个面(Face)数组,每个面代表了一个图元,在我们的例子中(由于使用了aiProcess_Triangulate选项)它总是三角形。一个面包含了多个索引,它们定义了在每个图元中,我们应该绘制哪个顶点,并以什么顺序绘制,所以如果我们遍历了所有的面,并储存了面的索引到indices这个vector中就可以了。
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]);
}
10、材质:
和节点一样,一个网格只包含了一个指向材质对象的索引。如果想要获取网格真正的材质,我们还需要索引场景的mMaterials数组。网格材质索引位于它的mMaterialIndex属性中,我们同样可以用它来检测一个网格是否包含有材质:
首先从场景的mMaterials数组中获取aiMaterial
对象。接下来我们希望加载网格的漫反射和/或镜面光贴图。一个材质对象的内部对每种纹理类型都存储了一个纹理位置数组。不同的纹理类型都以aiTextureType_
为前缀。我们使用一个叫做loadMaterialTextures的工具函数来从材质中获取纹理。这个函数将会返回一个Texture结构体的vector,我们将在模型的textures vector的尾部之后存储它。
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());
vector<Texture> specularMaps = loadMaterialTextures(material,
aiTextureType_SPECULAR, "texture_specular");
textures.insert(textures.end(), specularMaps.begin(), specularMaps.end());
}
11、loadMaterialTextures函数:
loadMaterialTextures函数遍历了给定纹理类型的所有纹理位置,获取了纹理的文件位置,并加载并和生成了纹理,将信息储存在了一个Vertex结构体中。它看起来会像这样:
vector<Texture> loadMaterialTextures(aiMaterial *mat
, aiTextureType type, string typeName)
{
vector<Texture> textures;
for(unsigned int i = 0; i < mat->GetTextureCount(type); i++)
{
aiString str;
mat->GetTexture(type, i, &str);
Texture texture;
texture.id = TextureFromFile(str.C_Str(), directory);
texture.type = typeName;
texture.path = str;
textures.push_back(texture);
}
return textures;
}
首先通过GetTextureCount函数检查储存在材质中纹理的数量,这个函数需要一个纹理类型。我们会使用GetTexture获取每个纹理的文件位置,它会将结果储存在一个aiString
中。我们接下来使用另外一个叫做TextureFromFile的工具函数,它将会(用stb_image.h
)加载一个纹理并返回该纹理的ID。
五、重大优化:
这还没有完全结束,因为我们还想做出一个重大的(但不是完全必须的)优化。大多数场景都会在多个网格中重用部分纹理。还是想想一个房子,它的墙壁有着花岗岩的纹理。这个纹理也可以被应用到地板、天花板、楼梯、桌子,甚至是附近的一口井上。加载纹理并不是一个低开销的操作,在我们当前的实现中,即便同样的纹理已经被加载过很多遍了,对每个网格仍会加载并生成一个新的纹理。这很快就会变成模型加载实现的性能瓶颈。
1、纹理的存储:
所以我们会对模型的代码进行微调,将所有加载过的纹理全局储存,每当我们想加载一个纹理的时候,首先去检查它有没有被加载过。如果有的话,我们会直接使用那个纹理,并跳过整个加载流程,来为我们省下很多处理能力。为了能够比较纹理,我们还需要储存它们的路径:
struct Texture {
unsigned int id;
string type;
aiString path; // 我们储存纹理的路径用于与其它纹理进行比较
};
2、纹理的加载:
接下来我们将所有加载过的纹理储存在另一个vector中,在模型类的顶部声明为一个私有变量:
vector<Texture> textures_loaded;
3、纹理存储、加载后载入loadMaterialTextures函数:
之后,在loadMaterialTextures函数中,我们希望将纹理的路径与储存在textures_loaded这个vector中的所有纹理进行比较,看看当前纹理的路径是否与其中的一个相同。如果是的话,则跳过纹理加载/生成的部分,直接使用定位到的纹理结构体为网格的纹理。更新后的函数如下:
vector<Texture> loadMaterialTextures(aiMaterial *mat
, aiTextureType type, string typeName)
{
vector<Texture> textures;
for(unsigned int i = 0; i < mat->GetTextureCount(type); i++)
{
aiString str;
mat->GetTexture(type, i, &str);
bool skip = false;
for(unsigned int j = 0; j < textures_loaded.size(); j++)
{
if(std::strcmp(textures_loaded[j].path.data(), str.C_Str()) == 0)
{
textures.push_back(textures_loaded[j]);
skip = true;
break;
}
}
if(!skip)
{ // 如果纹理还没有被加载,则加载它
Texture texture;
texture.id = TextureFromFile(str.C_Str(), directory);
texture.type = typeName;
texture.path = str.C_Str();
textures.push_back(texture);
textures_loaded.push_back(texture); // 添加到已加载的纹理中
}
}
return textures;
}
六、Model类:
// Author : wangyongyao https://github.com/wangyongyao1989
// Created by MMM on 2024/10/25.
//
#include "GL3DModel.h"
GL3DModel::GL3DModel(const string &path, bool gamma) {
loadModel(path);
}
void GL3DModel::Draw(GLShader &shader) {
for (unsigned int i = 0; i < meshes.size(); i++)
meshes[i].Draw(shader);
}
void GL3DModel::loadModel(const string &path) {
// read file via ASSIMP
Assimp::Importer importer;
const aiScene *scene = importer.ReadFile(path, aiProcess_Triangulate
| aiProcess_GenSmoothNormals
| aiProcess_FlipUVs
| aiProcess_CalcTangentSpace);
// check for errors
if (!scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode) // if is Not Zero
{
LOGE("ERROR::ASSIMP: %s", importer.GetErrorString());
return;
}
// retrieve the directory path of the filepath
directory = path.substr(0, path.find_last_of('/'));
// process ASSIMP's root node recursively
processNode(scene->mRootNode, scene);
}
void GL3DModel::processNode(aiNode *node, const aiScene *scene) {
// process each mesh located at the current node
for (unsigned int i = 0; i < node->mNumMeshes; i++) {
// the node object only contains indices to index the actual objects in the scene.
// the scene contains all the data, node is just to keep stuff organized (like relations between nodes).
aiMesh *mesh = scene->mMeshes[node->mMeshes[i]];
meshes.push_back(processMesh(mesh, scene));
}
// after we've processed all of the meshes (if any) we then recursively process each of the children nodes
for (unsigned int i = 0; i < node->mNumChildren; i++) {
processNode(node->mChildren[i], scene);
}
}
GL3DMesh GL3DModel::processMesh(aiMesh *mesh, const aiScene *scene) {
// data to fill
vector <Vertex> vertices;
vector<unsigned int> indices;
vector <Texture> textures;
// walk through each of the mesh's vertices
for (unsigned int i = 0; i < mesh->mNumVertices; i++) {
Vertex vertex;
// we declare a placeholder vector since assimp uses its own vector class that doesn't
// directly convert to glm's vec3 class so we transfer the data to this placeholder glm::vec3 first.
glm::vec3 vector;
// positions
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
// does the mesh contain texture coordinates?
if (mesh->mTextureCoords[0]) {
glm::vec2 vec;
// a vertex can contain up to 8 different texture coordinates. We thus make the assumption that we won't
// use models where a vertex can have multiple texture coordinates so we always take the first set (0).
vec.x = mesh->mTextureCoords[0][i].x;
vec.y = mesh->mTextureCoords[0][i].y;
vertex.TexCoords = vec;
// tangent
vector.x = mesh->mTangents[i].x;
vector.y = mesh->mTangents[i].y;
vector.z = mesh->mTangents[i].z;
vertex.Tangent = vector;
// bitangent
vector.x = mesh->mBitangents[i].x;
vector.y = mesh->mBitangents[i].y;
vector.z = mesh->mBitangents[i].z;
vertex.Bitangent = vector;
} else
vertex.TexCoords = glm::vec2(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 = loadMaterialTextures(material, aiTextureType_DIFFUSE,
"texture_diffuse");
textures.insert(textures.end(), diffuseMaps.begin(), diffuseMaps.end());
// 2. specular maps
vector <Texture> specularMaps = loadMaterialTextures(material, aiTextureType_SPECULAR,
"texture_specular");
textures.insert(textures.end(), specularMaps.begin(), specularMaps.end());
// 3. normal maps
std::vector <Texture> normalMaps = loadMaterialTextures(material, aiTextureType_HEIGHT,
"texture_normal");
textures.insert(textures.end(), normalMaps.begin(), normalMaps.end());
// 4. height maps
std::vector <Texture> heightMaps = loadMaterialTextures(material, aiTextureType_AMBIENT,
"texture_height");
textures.insert(textures.end(), heightMaps.begin(), heightMaps.end());
// return a mesh object created from the extracted mesh data
return GL3DMesh(vertices, indices, textures);
}
vector <Texture>
GL3DModel::loadMaterialTextures(aiMaterial *mat, aiTextureType type, 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++) {
if (std::strcmp(textures_loaded[j].path.data(), str.C_Str()) == 0) {
textures.push_back(textures_loaded[j]);
// a texture with the same filepath has
// already been loaded, continue to next one. (optimization)
skip = true;
break;
}
}
if (!skip) { // if texture hasn't been loaded already, load it
Texture texture;
texture.id = TextureFromFile(str.C_Str(), this->directory, false);
texture.type = typeName;
texture.path = str.C_Str();
textures.push_back(texture);
// store it as texture loaded for entire model,
// to ensure we won't unnecessary load duplicate textures.
textures_loaded.push_back(texture);
}
}
return textures;
}
unsigned int GL3DModel::TextureFromFile(const char *path, const string &directory, bool gamma) {
string filename = string(path);
filename = directory + '/' + filename;
unsigned int textureID;
glGenTextures(1, &textureID);
int width, height, nrComponents;
unsigned char *data = stbi_load(filename.c_str(), &width, &height, &nrComponents, 0);
if (data) {
GLenum format;
if (nrComponents == 1)
format = GL_RED;
else if (nrComponents == 3)
format = GL_RGB;
else if (nrComponents == 4)
format = GL_RGBA;
glBindTexture(GL_TEXTURE_2D, textureID);
glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, 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);
stbi_image_free(data);
} else {
LOGD("Texture failed to load at path: %s", path);
stbi_image_free(data);
}
return textureID;
}
七、Android平台上代码实现:
依据本系列文章的前几篇的基础上,OpenGL实现相关的流程。接下来在Android平台上实现3D模型的显示操作。
1.基于GLSurfaceView构建OpenGL的运行环境。
OpenGL运行环境构建及监听view的触摸及缩放事件,获取顶点/片段程序及3D模型文件的路径传入JNI层,把事件传入给OpenGL的GLCamera3D.cpp进行3D模型的缩放及移动。
package com.wangyongyao.gl3d.view;
import android.content.Context;
import android.opengl.GLSurfaceView;
import android.util.AttributeSet;
import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import com.wangyongyao.gl3d.GL3DCallJni;
import com.wangyongyao.gl3d.utils.GL3DShowUtil;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
public class GL3DShowView extends GLSurfaceView implements GLSurfaceView.Renderer {
private static String TAG = GL3DShowView.class.getSimpleName();
private GL3DCallJni mJniCall;
private Context mContext;
private boolean isScaleGesture;
private GestureDetector gestureDetector;
private ScaleGestureDetector scaleGestureDetector;
private float downX;
private float downY;
public GL3DShowView(Context context, GL3DCallJni jniCall) {
super(context);
mContext = context;
mJniCall = jniCall;
init();
}
public GL3DShowView(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
init();
}
private void init() {
getHolder().addCallback(this);
setEGLContextClientVersion(3);
setEGLConfigChooser(8, 8, 8, 8, 16, 0);
String fragPath = GL3DShowUtil.getModelFilePath(mContext
, "gl_3d_show_fragment.glsl");
String vertexPath = GL3DShowUtil.getModelFilePath(mContext
, "gl_3d_show_vertex.glsl");
if (mJniCall != null) {
mJniCall.setGL3DSLPath(fragPath, vertexPath);
}
setRenderer(this);
gestureDetector = new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener());
scaleGestureDetector = new ScaleGestureDetector(getContext()
, new ScaleGestureDetector.OnScaleGestureListener() {
@Override
public boolean onScale(ScaleGestureDetector detector) {
// 处理缩放事件
float scaleFactor = detector.getScaleFactor();
// Log.e(TAG, "onScale scaleFactor: " + scaleFactor
// + "==getFocusX:" + detector.getFocusX()
// + "===getFocusY" + detector.getFocusY());
mJniCall.gl3DShowOnScale(scaleFactor, detector.getFocusX()
, detector.getFocusY(), 2);
return true;
}
@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
// 开始缩放事件
// Log.e(TAG, "onScaleBegin: " + detector);
mJniCall.gl3DShowOnScale(detector.getScaleFactor(), detector.getFocusX()
, detector.getFocusY(), 1);
return true;
}
@Override
public void onScaleEnd(ScaleGestureDetector detector) {
// 结束缩放事件
// Log.e(TAG, "onScaleEnd: " + detector);
mJniCall.gl3DShowOnScale(detector.getScaleFactor(), detector.getFocusX()
, detector.getFocusY(), 3);
isScaleGesture = false;
}
});
}
public void onDrawFrame(GL10 gl) {
// Log.e(TAG, "onDrawFrame: ");
if (mJniCall != null)
mJniCall.gl3DRenderFrame();
}
public void onSurfaceChanged(GL10 gl, int width, int height) {
Log.e(TAG, "onSurfaceChanged: ");
if (mJniCall != null)
mJniCall.init3D(width, height);
}
@Override
public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {
// String modelPath = GL3DShowUtil.getModelFilePath(mContext
// , "nanosuit.obj");
// String modelPath = GL3DShowUtil.getModelFilePath(mContext
// , "nanosuit.mtl");
String modelPath = GL3DShowUtil.getModelFilePath(mContext
, "nanosuit.blend");
if (mJniCall != null)
mJniCall.setGL3DModelPath(modelPath);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (isScaleGesture) {
gestureDetector.onTouchEvent(event);
scaleGestureDetector.onTouchEvent(event);
return true;
}
switch (event.getAction()) {
case MotionEvent.ACTION_POINTER_2_DOWN: {
isScaleGesture = true;
}
break;
case MotionEvent.ACTION_POINTER_2_UP: {
isScaleGesture = false;
}
break;
case MotionEvent.ACTION_DOWN: {
// Log.e(TAG, "onTouchEvent: " + event.getAction());
downX = event.getX();
downY = event.getY();
mJniCall.gl3DShowMoveXY(0, 0, 1);
}
break;
case MotionEvent.ACTION_MOVE: {
// Log.e(TAG, "onTouchEvent: " + event.getAction());
float dx = event.getX() - downX;
float dy = event.getY() - downY;
Log.e(TAG, "ACTION_MOVE:dx= "
+ dx + "==dy:" + dy);
mJniCall.gl3DShowMoveXY(dx, dy, 2);
}
break;
case MotionEvent.ACTION_UP: {
// Log.e(TAG, "onTouchEvent: " + event.getAction());
downX = 0;
downY = 0;
mJniCall.gl3DShowMoveXY(0, 0, 3);
}
break;
}
return true;
}
}
2. JNI层数据传递:
把Java层OpenGL所需的运行环境构建成C++的一个桥接。
#include <jni.h>
#include <string>
#include <android/log.h>
#include "GL3DLogUtils.h"
#include "GL3DFlashLight.h"
#include "GL3DShow.h"
//包名+类名字符串定义:
const char *gl3d_class_name = "com/wangyongyao/gl3d/GL3DCallJni";
GL3DFlashLight *flashLight;
GL3DShow *gl3DShow;
/*********************** GL 聚光手电筒********************/
extern "C"
JNIEXPORT jboolean JNICALL
cpp_flash_light_init_opengl(JNIEnv *env, jobject thiz, jint width, jint height) {
if (flashLight == nullptr)
flashLight = new GL3DFlashLight();
flashLight->setupGraphics(width, height);
return 0;
}
extern "C"
JNIEXPORT void JNICALL
cpp_flash_light_render_frame(JNIEnv *env, jobject thiz) {
if (flashLight == nullptr) return;
flashLight->renderFrame();
}
extern "C"
JNIEXPORT void JNICALL
cpp_flash_light_frag_vertex_path(JNIEnv *env, jobject thiz, jstring frag, jstring vertex,
jstring picsrc1, jstring picsrc2) {
const char *fragPath = env->GetStringUTFChars(frag, nullptr);
const char *vertexPath = env->GetStringUTFChars(vertex, nullptr);
const char *picsrc1Path = env->GetStringUTFChars(picsrc1, nullptr);
const char *picsrc2Path = env->GetStringUTFChars(picsrc2, nullptr);
if (flashLight == nullptr) {
flashLight = new GL3DFlashLight();
}
flashLight->setSharderPath(vertexPath, fragPath);
flashLight->setPicPath(picsrc1Path, picsrc2Path);
env->ReleaseStringUTFChars(frag, fragPath);
env->ReleaseStringUTFChars(vertex, vertexPath);
env->ReleaseStringUTFChars(picsrc1, picsrc1Path);
env->ReleaseStringUTFChars(picsrc2, picsrc2Path);
}
extern "C"
JNIEXPORT void JNICALL
cpp_flash_light_color_frag_vertex_path(JNIEnv *env, jobject thiz, jstring frag,
jstring vertex) {
const char *fragPath = env->GetStringUTFChars(frag, nullptr);
const char *vertexPath = env->GetStringUTFChars(vertex, nullptr);
if (flashLight == nullptr) {
flashLight = new GL3DFlashLight();
}
flashLight->setColorSharderPath(vertexPath, fragPath);
env->ReleaseStringUTFChars(frag, fragPath);
env->ReleaseStringUTFChars(vertex, vertexPath);
}
extern "C"
JNIEXPORT void JNICALL
cpp_flash_light_move_xy(JNIEnv *env, jobject thiz, jfloat dx, jfloat dy, jint actionMode) {
if (flashLight == nullptr) return;
flashLight->setMoveXY(dx, dy, actionMode);
}
extern "C"
JNIEXPORT void JNICALL
cpp_flash_light_on_scale(JNIEnv *env, jobject thiz, jfloat scaleFactor, jfloat focusX,
jfloat focusY,
jint actionMode) {
if (flashLight == nullptr) return;
flashLight->setOnScale(scaleFactor, focusX, focusY, actionMode);
}
/*********************** GL 3d模型显示********************/
extern "C"
JNIEXPORT jboolean JNICALL
cpp_3dshow_init_opengl(JNIEnv *env, jobject thiz, jint width, jint height) {
if (gl3DShow == nullptr)
gl3DShow = new GL3DShow();
gl3DShow->setupGraphics(width, height);
return 0;
}
extern "C"
JNIEXPORT void JNICALL
cpp_3dshow_render_frame(JNIEnv *env, jobject thiz) {
if (gl3DShow == nullptr) return;
gl3DShow->renderFrame();
}
extern "C"
JNIEXPORT void JNICALL
cpp_3dshow_frag_vertex_path(JNIEnv *env, jobject thiz, jstring frag, jstring vertex) {
const char *fragPath = env->GetStringUTFChars(frag, nullptr);
const char *vertexPath = env->GetStringUTFChars(vertex, nullptr);
if (gl3DShow == nullptr) {
gl3DShow = new GL3DShow();
}
gl3DShow->setSharderPath(vertexPath, fragPath);
env->ReleaseStringUTFChars(frag, fragPath);
env->ReleaseStringUTFChars(vertex, vertexPath);
}
extern "C"
JNIEXPORT void JNICALL
cpp_3dshow_model_path(JNIEnv *env, jobject thiz, jstring model) {
const char *modelPath = env->GetStringUTFChars(model, nullptr);
if (gl3DShow == nullptr) {
gl3DShow = new GL3DShow();
}
gl3DShow->setModelPath(modelPath);
env->ReleaseStringUTFChars(model, modelPath);
}
extern "C"
JNIEXPORT void JNICALL
cpp_3d_show_move_xy(JNIEnv *env, jobject thiz, jfloat dx, jfloat dy, jint actionMode) {
if (gl3DShow == nullptr) return;
gl3DShow->setMoveXY(dx, dy, actionMode);
}
extern "C"
JNIEXPORT void JNICALL
cpp_3d_show_on_scale(JNIEnv *env, jobject thiz, jfloat scaleFactor, jfloat focusX,
jfloat focusY,
jint actionMode) {
if (gl3DShow == nullptr) return;
gl3DShow->setOnScale(scaleFactor, focusX, focusY, actionMode);
}
// 重点:定义类名和函数签名,如果有多个方法要动态注册,在数组里面定义即可
static const JNINativeMethod methods[] = {
/*********************** GL 3d模型显示********************/
{"native_3d_init_opengl", "(II)Z", (void *) cpp_3dshow_init_opengl},
{"native_3d_render_frame", "()V", (void *) cpp_3dshow_render_frame},
{"native_3d_set_glsl_path", "(Ljava/lang/String;"
"Ljava/lang/String;"
")V",
(void *) cpp_3dshow_frag_vertex_path},
{"native_3d_set_model_path", "(Ljava/lang/String;)V",
(void *) cpp_3dshow_model_path},
{"native_3d_move_xy", "(FFI)V", (void *) cpp_3d_show_move_xy},
{"native_3d_on_scale", "(FFFI)V", (void *) cpp_3d_show_on_scale},
/*********************** GL 聚光手电筒********************/
{"native_flash_light_init_opengl", "(II)Z", (void *) cpp_flash_light_init_opengl},
{"native_flash_light_render_frame", "()V", (void *) cpp_flash_light_render_frame},
{"native_flash_light_color_set_glsl_path", "(Ljava/lang/String"
";Ljava/lang/String;)V", (void *) cpp_flash_light_color_frag_vertex_path},
{"native_flash_light_set_glsl_path", "(Ljava/lang/String"
";Ljava/lang/String"
";Ljava/lang/String"
";Ljava/lang/String;)V", (void *) cpp_flash_light_frag_vertex_path},
{"native_flash_light_move_xy", "(FFI)V", (void *) cpp_flash_light_move_xy},
{"native_flash_light_on_scale", "(FFFI)V", (void *) cpp_flash_light_on_scale},
};
/**
* 定义注册方法
* @param vm
* @param reserved
* @return
*/
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
LOGD("动态注册");
JNIEnv *env;
if ((vm)->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
LOGD("动态注册GetEnv fail");
return JNI_ERR;
}
// 获取类引用
jclass clazz = env->FindClass(gl3d_class_name);
// 注册native方法
jint regist_result = env->RegisterNatives(clazz, methods,
sizeof(methods) / sizeof(methods[0]));
if (regist_result) { // 非零true 进if
LOGE("动态注册 fail regist_result = %d", regist_result);
} else {
LOGI("动态注册 success result = %d", regist_result);
}
return JNI_VERSION_1_6;
}
3.assmip库的导入及model类的初始化及使用。
由文章上半部分可知,GL3DModel.cpp类封装了文件导入之后loadModel -> processModel -> processMesh ->顶点获取 -> 法线处理 -> 纹理坐标处理 -> 索引的读取 -> 材质的添加,经过这一系列的处理最终在Draw方法中进行渲染绘制。
// Author : wangyongyao https://github.com/wangyongyao1989
// Created by MMM on 2024/10/23.
//
#include "GL3DShow.h"
bool GL3DShow::setupGraphics(int w, int h) {
screenW = w;
screenH = h;
GLuint modelProgram = modelShader->createProgram();
if (!modelProgram) {
LOGE("Could not create shaderId.");
return false;
}
return false;
}
void GL3DShow::renderFrame() {
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // also clear the depth buffer now!
//开启深度测试
glEnable(GL_DEPTH_TEST);
// be sure to activate shader when setting uniforms/drawing objects
modelShader->use();
// view/projection transformations
glm::mat4 projection = glm::perspective(glm::radians(mCamera.Zoom),
(float) screenW / (float) screenH, 0.1f, 100.0f);
vec3 cameraMove(0.0f, 0.0f, 6.0f);
mCamera.Position = cameraMove;
glm::mat4 view = mCamera.GetViewMatrix();
modelShader->setMat4("projection", projection);
modelShader->setMat4("view", view);
// render the loaded model
glm::mat4 model = glm::mat4(0.4f);
// translate it down so it's at the center of the scene
model = glm::translate(model, glm::vec3(0.0f, 0.0f, 0.0f));
// it's a bit too big for our scene, so scale it down
model = glm::scale(model, glm::vec3(0.1f, 0.1f, 0.1f));
modelShader->setMat4("model", model);
gl3DModel->Draw(*modelShader);
}
bool GL3DShow::setSharderPath(const char *vertexPath, const char *fragmentPath) {
modelShader->getSharderPath(vertexPath, fragmentPath);
return false;
}
bool GL3DShow::setModelPath(const char *modelPath) {
LOGI("setMosetModelPath :%s", modelPath);
string model(modelPath);
gl3DModel = new GL3DModel(model, false);
return false;
}
void GL3DShow::setMoveXY(float dx, float dy, int actionMode) {
LOGI("setMoveXY dx:%f,dy:%f,actionMode:%d", dy, dy, actionMode);
float xoffset = dx - lastX;
float yoffset = lastY - dy;
lastX = dx;
lastY = dy;
mActionMode = actionMode;
mCamera.ProcessXYMovement(xoffset, yoffset);
}
void GL3DShow::setOnScale(float scaleFactor, float focusX
, float focusY, int actionMode) {
float scale;
if (actionMode == 1 || actionMode == 3) {
scale = 45.0f;
} else {
if (scaleFactor > 1) {
scale = (scaleFactor - 1) * 1000 + 45;
} else {
scale = 50 - (1 - scaleFactor) * 1000;
}
}
LOGI("setOnScale scale:%f", scale);
mCamera.ProcessScroll(scale);
}
void GL3DShow::printGLString(const char *name, GLenum s) {
const char *v = (const char *) glGetString(s);
LOGI("OpenGL %s = %s\n", name, v);
}
void GL3DShow::checkGlError(const char *op) {
for (GLint error = glGetError(); error; error = glGetError()) {
LOGI("after %s() glError (0x%x)\n", op, error);
}
}
GL3DShow::GL3DShow() {
modelShader = new GL3DShader();
}
GL3DShow::~GL3DShow() {
if (modelShader) {
delete modelShader;
modelShader = nullptr;
}
if (gl3DModel) {
delete gl3DModel;
gl3DModel = nullptr;
}
lastX = 0;
lastY = 0;
mActionMode = 0;
screenW = 0;
screenH = 0;
}