草庐IT

Android OpenGL ES 3.0 3D模型介绍以及加载和渲染

若之灵动 2023-07-30 原文

1.OpenGLES 3D 模型

OpenGLES 3D 模型本质上是由一系列三角形在 3D 空间(OpenGL 坐标系)中构建而成,另外还包含了用于描述三角形表面的纹理、光照、材质等信息。

利用 3D 建模软件,设计师可以构建一些复杂的形状,并将贴图应用到形状上去,不需要去关注图像技术细节。最后在导出模型文件时,建模工具会自己生成所有的顶点坐标、顶点法线和纹理坐标

常用的模型文件格式有 .obj、.max、.fbx .3ds 等,其中.obj是 Wavefront 科技开发的一种几何体图形文件格式,包含每个顶点的位置、纹理坐标、法线,以及组成面(多边形)的顶点列表等数据,应用较为广泛。

2.OBJ 文件的结构

# Blender v2.83.15 OBJ File: 'monkey.blend'
# www.blender.org
mtllib monkey.mtl
o face_Plane
v 0.156846 -0.166171 1.299656
v 0.040169 -0.151694 1.361770
v 0.167955 -0.148006 1.292944
...
vt 0.211637 0.867755
vt 0.220409 0.881854
vt 0.205920 0.875706
vt 0.225888 0.873355
vt 0.235953 0.888705
vt 0.197610 0.862036
...
vn 0.7356 -0.5550 0.3885
vn 0.8016 -0.5069 0.3170
vn 0.7547 -0.5032 0.4209
vn 0.7271 -0.6192 0.2964
vn 0.7657 -0.5795 0.2791
vn 0.6553 -0.4363 0.6166
...
f 313/720/711 1850/727/718 316/722/713
f 1623/337/337 317/728/719 1873/729/720
f 311/719/710 1851/730/721 313/720/711
f 317/728/719 1839/717/708 1871/704/695
f 1847/721/712 1874/726/717 1873/729/720
f 1875/725/716 1625/327/327 1874/726/717
f 1850/727/718 1877/731/722 1876/732/723

模型的数据量是非常大的,这里做了部分截取

OBJ 文件数据结构的简单说明:

  • # 开头的行表示注释行
  • mtllib 表示指定该 OBJ 文件所使用的 mtl 文件(材质文件)
  • v 开头的行表示存放的是顶点坐标,后面三个数分别表示一个顶点的(x,y,z)坐标值
  • vn 开头的行表示存放的是顶点法向量,后面三个数分别表示一个顶点法向量的三维(x,y,z)分量值
  • vt 开头的行表示存放的是纹理坐标,后面三个数分别表示一个纹理坐标的(s,t,p)分量值,其中 p 分量一般用于 3D 纹理
  • f 开头的行表示存放的是一个三角面的信息,后面有三组数据分别表示组成三角面的三个顶点的信息,每个顶点信息的格式为:顶点位置索引/纹理坐标索引/法向量索引。

3.模型加载

模型加载可以使用模型加载库 Assimp,Assimp 全称为 Open Asset Import Library,可以支持几十种不同格式的模型文件的解析(同样也可以导出部分模型格式),Assimp 本身是 C++ 库,可以跨平台使用。

Assimp 可以将几十种模型文件都转换为一个统一的数据结构,所有无论我们导入何种格式的模型文件,都可以用同一个方式去访问我们需要的模型数据。

由于知道了模型的数据格式,通过C++代码的方式也可以方便的解析,本文就通过C++的方式进行解析,暂时先不使用Assimp这个库。

std::string fileName = dirPath + "/monkey.obj";

std::ifstream inputStream(fileName, std::ifstream::in | std::ifstream::binary);

if (!inputStream.is_open()){
    std::cerr << "Error opening file:"<<fileName<<std::endl;
}


std::vector<glm::vec3> coords;     //顶点数据
std::vector<glm::vec2> texturCoords;   //uv贴图顶点
std::vector<glm::vec3> normals;     //法线数据

std::vector<MSVertexData> vertexes;
std::vector<GLuint> indexes;

MSMesh *mesh = nullptr;

std::string mtlName;
std::string lineString;

while (std::getline(inputStream,lineString))
{

    std::vector<std::string> list = ccStringSplit(lineString," ");
    if (list[0]  == "#") {
        std::cout<< "This is comment:" << lineString;
        continue;
    } else if (list[0]  == "mtllib") {
        std::cout<< "File with materials:" << list[1];
        continue;
    } else if (list[0]  == "v") { //顶点
        coords.emplace_back(glm::vec3(atof(list[1].c_str()), atof(list[2].c_str()), atof(list[3].c_str())));
        continue;
    } else if (list[0]  == "vt") {  //纹理坐标
        texturCoords.emplace_back(glm::vec2(atof(list[1].c_str()), atof(list[2].c_str())));
        continue;
    } else if (list[0]  == "vn") { //顶点法向量  Normal vector
        normals.emplace_back(glm::vec3(atof(list[1].c_str()), atof(list[2].c_str()), atof(list[3].c_str())));
        continue;
    } else if (list[0]  == "f") { // 顶点位置索引/纹理坐标索引/法向量索引
        for (int i = 1; i <= 3; ++i){
            std::vector<std::string> vert = ccStringSplit(list[i],"/");
            vertexes.emplace_back(VertexData(
                    coords[static_cast<int>(atol(vert[0].c_str())) - 1],
                    texturCoords[static_cast<int>(atol(vert[1].c_str())) - 1],
                    normals[static_cast<int>(atol(vert[2].c_str())) -1 ])
                    );
            indexes.emplace_back(static_cast<unsigned>(indexes.size()));
        }
        continue;
    } else if (list[0] == "usemtl") {
        mtlName = list[1];
        std::cout<< "This is used naterial:" << mtlName;

    }
}

通过不同的字段就能解析3d模型文件,将解析好的数据保存到vertexes 和 indexes中

4.进行TBN空间计算

void MSModelLoader::calculateTBN(std::vector<MSVertexData> &vertData)
{
    for (int i = 0; i < (int)vertData.size(); i += 3) {

        glm::vec3 &v1 = vertData[i].position;
        glm::vec3 &v2 = vertData[i + 1].position;
        glm::vec3 &v3 = vertData[i + 2].position;

        glm::vec2 &uv1 = vertData[i].textCoord;
        glm::vec2 &uv2 = vertData[i + 1].textCoord;
        glm::vec2 &uv3 = vertData[i + 2].textCoord;

        // https://youtu.be/ef3XR0ZttDU?t=1097
        // deltaPos1 = deltaUV1.x * T + deltaUV1.y * B;
        // deltaPos2 = deltaUV2.x * T + deltaUV2.y * B;

        glm::vec3 deltaPos1 = v2 - v1;
        glm::vec3 deltaPos2 = v3 - v1;

        glm::vec2 deltaUV1 = uv2 - uv1;
        glm::vec2 deltaUV2 = uv3 - uv1;

        float r = 1.0f / (deltaUV1.x * deltaUV2.y - deltaUV1.y * deltaUV2.x);
        glm::vec3 tangent = (deltaPos1 * deltaUV2.y - deltaPos2 * deltaUV1.y) * r;
        glm::vec3 bitangent = (deltaPos2 * deltaUV1.x - deltaPos1 * deltaUV2.x) * r;

        vertData[i].tangent = tangent;
        vertData[i + 1].tangent = tangent;
        vertData[i + 2].tangent = tangent;

        vertData[i].bitangent = bitangent;
        vertData[i + 1].bitangent = bitangent;
        vertData[i + 2].bitangent = bitangent;

    }
}

由于模型中只保存了,顶点数据、材质顶点数据、法线数据以及顶点的index,由于要进行法线纹理贴图需要切线和二维切线数据,

所以通过TBN空间计算得到切线以及二维切线来加载法线纹理贴图。

5.加载顶点以及顶点序列数据

void MSMesh::InitRenderResources(AAssetManager *pManager, const std::vector<MSVertexData> &vertData, const std::vector<GLuint> &indexes)
{
    if(pManager == NULL){
        return;
    }

    loadTextureResources(pManager);
    loadShaderResources(pManager);

    m_indexBuffSize = indexes.size() ;


    m_pVAO->Create();
    m_pVAO->Bind();
    /*给顶点数据赋值*/
    m_pVBO->Create();
    m_pVBO->Bind();
    m_pVBO->SetBufferData(vertData.data(),vertData.size() * sizeof (MSVertexData));

    /*给Index 数据赋值 */
    m_pEBO->Create();
    m_pEBO->Bind();
    m_pEBO->SetBufferData(indexes.data(), indexes.size() * sizeof (GLuint));

    int offset = 0;
    /*给a_position传值 */
    m_pOpenGLShader->SetAttributeBuffer(0, GL_FLOAT, (void *)offset, 3, sizeof(MSVertexData));
    m_pOpenGLShader->EnableAttributeArray(0);

    offset += sizeof (glm::vec3);
    /*给shader材质顶点 a_texturCoord 赋值*/
    m_pOpenGLShader->SetAttributeBuffer(1, GL_FLOAT, (void *)offset, 2, sizeof(MSVertexData));
    m_pOpenGLShader->EnableAttributeArray(1);

    offset += sizeof (glm::vec2);

    /*给shader a_normal赋值 法向量*/
    m_pOpenGLShader->SetAttributeBuffer(2, GL_FLOAT, (void *)offset, 3, sizeof (MSVertexData));
    m_pOpenGLShader->EnableAttributeArray(2);

    offset += sizeof (glm::vec3);
    /*给切线赋值 a_tangent */
    m_pOpenGLShader->SetAttributeBuffer(3, GL_FLOAT, (void *)offset, 3, sizeof (MSVertexData));
    m_pOpenGLShader->EnableAttributeArray(3);

    offset += sizeof (glm::vec3);

    /*给双切线赋值*/
    m_pOpenGLShader->SetAttributeBuffer(4, GL_FLOAT, (void *)offset, 3, sizeof (MSVertexData));
    m_pOpenGLShader->EnableAttributeArray(4);


    m_pVAO->Release();
    m_pVBO->Release();
    m_pEBO->Release();
}

由于3D模型顶点数量非常多,复杂的模型可能达到百万顶点或者更多,所以需要用到VBO、EBO以及VAO等,否则频繁的从CPU传递数据到GPU会卡死。

这一步就是将解析到的模型的顶点数据以及顶点序列数据加载到shader中。

6.进行渲染操作

void MSMesh::Render(MSGLCamera* pCamera)
{


    glm::mat4x4  modelMatrix = glm::mat4x4(1.0);

    glm::mat4x4  objectTransMat = glm::translate(glm::mat4(1.0f), glm::vec3(m_Objx, m_Objy, m_Objz));
    glm::mat4x4  objectScaleMat = glm::scale(glm::mat4(1.0f),glm::vec3(0.25f*m_ObjScale, 0.25f*0.6*m_ObjScale, 0.25f*m_ObjScale) );

    modelMatrix = objectTransMat * objectScaleMat ;  //模型矩阵


    m_pOpenGLShader->Bind();  //使用程序

    //对shader的三个矩阵传值
    m_pOpenGLShader->SetUniformValue("u_modelMatrix", modelMatrix);
    m_pOpenGLShader->SetUniformValue("u_viewMatrix", pCamera->viewMatrix);
    m_pOpenGLShader->SetUniformValue("u_projectionMatrix", pCamera->projectionMatrix);

    //给shader的散射光采样器赋值
    m_pOpenGLShader->SetUniformValue("texture_diffuse", 0);
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D,m_diffuseId);

    //给环境光 采样器赋值
    m_pOpenGLShader->SetUniformValue("texture_normal", 1);
    glActiveTexture(GL_TEXTURE1);
    glBindTexture(GL_TEXTURE_2D,m_normalId);

    //给镜面光 采样器赋值
    m_pOpenGLShader->SetUniformValue("texture_specular", 2);
    glActiveTexture(GL_TEXTURE2);
    glBindTexture(GL_TEXTURE_2D,m_specularId);

    m_pOpenGLShader->SetUniformValue("m_shiness", 32.0f);
    /*观察者的位置*/
    m_pOpenGLShader->SetUniformValue("u_viewPos", pCamera->GetEyePosition());

    /*给光线的位置赋值*/
    m_pOpenGLShader->SetUniformValue("myLight.m_ambient", glm::vec3(0.5,0.5,0.5));
    m_pOpenGLShader->SetUniformValue("myLight.m_diffuse", glm::vec3(0.8,0.8,0.8));
    m_pOpenGLShader->SetUniformValue("myLight.m_specular", glm::vec3(0.9,0.9,0.9));

    m_pOpenGLShader->SetUniformValue("myLight.m_pos", glm::vec3(5.0,5.0,5.0));
    m_pOpenGLShader->SetUniformValue("myLight.m_c", 1.0f);
    m_pOpenGLShader->SetUniformValue("myLight.m_l", 0.09f);
    m_pOpenGLShader->SetUniformValue("myLight.m_q", 0.032f);


    m_pVAO->Bind();

    const short* indices =(const short *) 0;
    glDrawElements(GL_TRIANGLES, m_indexBuffSize,  GL_UNSIGNED_INT, indices);

    glBindTexture(GL_TEXTURE_2D,0);

    m_pOpenGLShader->Release();
    m_pVAO->Release();

}

渲染的操作大部分都是在通过uniform进行给sahder传数据

由于使用VBO、EBO、VAO,渲染操作只需要执行glDrawElements就可以了。

7.顶点着色器

#version 300 es

layout(location = 0) in   vec3 a_position;      //顶点
layout(location = 1) in   vec2 a_texturCoord;   //材质顶点
layout(location = 2) in   vec3 a_normal;        //法线
layout(location = 3) in   vec3 a_tangent;       //切线
layout(location = 4) in   vec3 a_bitangent;     //双切线

uniform  mat4 u_projectionMatrix;           //透视矩阵
uniform  mat4 u_viewMatrix;                 //观察者矩阵
uniform mat4 u_modelMatrix;                 //模型矩阵

out  vec4 vary_pos;
out  vec2 vary_texCoord;
out  mat3 vary_tbnMatrix;


void main(void)
{
    mat4 mv_matrix = u_viewMatrix * u_modelMatrix;    //观察者矩阵和模型矩阵相乘
    gl_Position = u_projectionMatrix * mv_matrix * vec4(a_position,1.0);

    vary_texCoord = a_texturCoord;

    vary_pos = u_modelMatrix *  vec4(a_position,1.0);
    //transpose 求转置矩阵  inverse 逆矩阵   normalize 归一化
    vec3 normal = normalize(mat3(transpose(inverse(u_modelMatrix))) * a_normal);
    vec3 tangent = normalize(mat3(transpose(inverse(u_modelMatrix))) * a_tangent);
    vec3 bitangent = normalize(mat3(transpose(inverse(u_modelMatrix))) * a_bitangent);

    vary_tbnMatrix = mat3(tangent, bitangent, normal);  //得到的是TBN空间矩阵  传给片元着色器

}
  • MVP矩阵*顶点坐标得到的是屏幕坐标
  • transpose 求转置矩阵
  • inverse 逆矩阵
  • normalize 归一化

8.片元着色器

#version 300 es
precision highp float;


struct Light
{
    vec3 m_pos;
    vec3 m_ambient;     //环境光
    vec3 m_diffuse;     //散射光
    vec3 m_specular;    //镜面光

    float m_c;          
    float m_l;
    float m_q;
};

uniform Light myLight;

uniform sampler2D   texture_diffuse;
uniform sampler2D   texture_normal;       //环境光采样器
uniform sampler2D   texture_specular;

uniform float       m_shiness;

uniform vec3       u_viewPos;

in vec4 vary_pos;
in vec2 vary_texCoord;
in  mat3 vary_tbnMatrix;

out vec4 fragColor;    //GUP 本质上只要这个4分量的颜色

void main(void)
{
    vec3 normal = texture(texture_normal,vary_texCoord).rgb;  //texture(texture_normal,vary_texCoord)  //材质顶点  采样器
    normal = normalize(normal * 2.0 - 1.0);
    normal = normalize(vary_tbnMatrix * normal);

    float dist = length(myLight.m_pos - vary_pos.xyz);

    float attenuation = 1.0f / (myLight.m_c + myLight.m_l * dist + myLight.m_q *dist * dist);

    //ambient  环境光
    vec3 ambient = myLight.m_ambient * vec3(texture(texture_diffuse , vary_texCoord).rgb);
    //diffuse  散射光
    vec3 lightDir = normalize(myLight.m_pos - vary_pos.xyz);
    float diff = max(dot(normal , lightDir) , 0.0f);
    vec3 diffuse = myLight.m_diffuse * diff * vec3(texture(texture_diffuse , vary_texCoord).rgb);

    //mirror reflect  镜面光
    float specular_strength = 0.5;
    vec3 viewDir = normalize(u_viewPos - vary_pos.xyz);
    vec3 reflectDir = reflect(-lightDir , normal);

    float spec =  pow(max(dot(viewDir , reflectDir) , 0.0f) , m_shiness);

    vec3 sepcular = specular_strength* myLight.m_specular * spec;

    vec3 result = ambient  + diffuse + sepcular ;   //三个光的强度 加在一起
    fragColor = vec4(result,1.0f) ;

}
  • dot:点乘得到的是两个向量的夹角
  • reflect:进行反射,镜面光用到
  • 片元着色器的本质就是给GPU传递一个颜色数据

有关Android OpenGL ES 3.0 3D模型介绍以及加载和渲染的更多相关文章

  1. ruby-on-rails - Rails - 子类化模型的设计模式是什么? - 2

    我有一个模型:classItem项目有一个属性“商店”基于存储的值,我希望Item对象对特定方法具有不同的行为。Rails中是否有针对此的通用设计模式?如果方法中没有大的if-else语句,这是如何干净利落地完成的? 最佳答案 通常通过Single-TableInheritance. 关于ruby-on-rails-Rails-子类化模型的设计模式是什么?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.co

  2. ruby - 什么是填充的 Base64 编码字符串以及如何在 ruby​​ 中生成它们? - 2

    我正在使用的第三方API的文档状态:"[O]urAPIonlyacceptspaddedBase64encodedstrings."什么是“填充的Base64编码字符串”以及如何在Ruby中生成它们。下面的代码是我第一次尝试创建转换为Base64的JSON格式数据。xa=Base64.encode64(a.to_json) 最佳答案 他们说的padding其实就是Base64本身的一部分。它是末尾的“=”和“==”。Base64将3个字节的数据包编码为4个编码字符。所以如果你的输入数据有长度n和n%3=1=>"=="末尾用于填充n%

  3. ruby-on-rails - Rails - 一个 View 中的多个模型 - 2

    我需要从一个View访问多个模型。以前,我的links_controller仅用于提供以不同方式排序的链接资源。现在我想包括一个部分(我假设)显示按分数排序的顶级用户(@users=User.all.sort_by(&:score))我知道我可以将此代码插入每个链接操作并从View访问它,但这似乎不是“ruby方式”,我将需要在不久的将来访问更多模型。这可能会变得很脏,是否有针对这种情况的任何技术?注意事项:我认为我的应用程序正朝着单一格式和动态页面内容的方向发展,本质上是一个典型的网络应用程序。我知道before_filter但考虑到我希望应用程序进入的方向,这似乎很麻烦。最终从任何

  4. ruby-on-rails - 渲染另一个 Controller 的 View - 2

    我想要做的是有2个不同的Controller,client和test_client。客户端Controller已经构建,我想创建一个test_clientController,我可以使用它来玩弄客户端的UI并根据需要进行调整。我主要是想绕过我在客户端中内置的验证及其对加载数据的管理Controller的依赖。所以我希望test_clientController加载示例数据集,然后呈现客户端Controller的索引View,以便我可以调整客户端UI。就是这样。我在test_clients索引方法中试过这个:classTestClientdefindexrender:template=>

  5. ruby-on-rails - 在混合/模块中覆盖模型的属性访问器 - 2

    我有一个包含模块的模型。我想在模块中覆盖模型的访问器方法。例如:classBlah这显然行不通。有什么想法可以实现吗? 最佳答案 您的代码看起来是正确的。我们正在毫无困难地使用这个确切的模式。如果我没记错的话,Rails使用#method_missing作为属性setter,因此您的模块将优先,阻止ActiveRecord的setter。如果您正在使用ActiveSupport::Concern(参见thisblogpost),那么您的实例方法需要进入一个特殊的模块:classBlah

  6. ruby - 如何在续集中重新加载表模式? - 2

    鉴于我有以下迁移:Sequel.migrationdoupdoalter_table:usersdoadd_column:is_admin,:default=>falseend#SequelrunsaDESCRIBEtablestatement,whenthemodelisloaded.#Atthispoint,itdoesnotknowthatusershaveais_adminflag.#Soitfails.@user=User.find(:email=>"admin@fancy-startup.example")@user.is_admin=true@user.save!ende

  7. ruby-on-rails - 如何验证非模型(甚至非对象)字段 - 2

    我有一个表单,其中有很多字段取自数组(而不是模型或对象)。我如何验证这些字段的存在?solve_problem_pathdo|f|%>... 最佳答案 创建一个简单的类来包装请求参数并使用ActiveModel::Validations。#definedsomewhere,atthesimplest:require'ostruct'classSolvetrue#youcouldevencheckthesolutionwithavalidatorvalidatedoerrors.add(:base,"WRONG!!!")unlesss

  8. ruby-on-rails - form_for 中不在模型中的自定义字段 - 2

    我想向我的Controller传递一个参数,它是一个简单的复选框,但我不知道如何在模型的form_for中引入它,这是我的观点:{:id=>'go_finance'}do|f|%>Transferirde:para:Entrada:"input",:placeholder=>"Quantofoiganho?"%>Saída:"output",:placeholder=>"Quantofoigasto?"%>Nota:我想做一个额外的复选框,但我该怎么做,模型中没有一个对象,而是一个要检查的对象,以便在Controller中创建一个ifelse,如果没有检查,请帮助我,非常感谢,谢谢

  9. ruby-on-rails - Rails HTML 请求渲染 JSON - 2

    在我的Controller中,我通过以下方式在我的index方法中支持HTML和JSON:respond_todo|format|format.htmlformat.json{renderjson:@user}end在浏览器中拉起它时,它会自然地以HTML呈现。但是,当我对/user资源进行内容类型为application/json的curl调用时(因为它是索引方法),我仍然将HTML作为响应。如何获取JSON作为响应?我还需要说明什么? 最佳答案 您应该将.json附加到请求的url,提供的格式在routes.rb的路径中定义。这

  10. ruby-on-rails - 如何将验证与模型分开 - 2

    我有一些非常大的模型,我必须将它们迁移到最新版本的Rails。这些模型有相当多的验证(User有大约50个验证)。是否可以将所有这些验证移动到另一个文件中?说app/models/validations/user_validations.rb。如果可以,有人可以提供示例吗? 最佳答案 您可以为此使用关注点:#app/models/validations/user_validations.rbrequire'active_support/concern'moduleUserValidationsextendActiveSupport:

随机推荐