OpenGLES 3D 模型本质上是由一系列三角形在 3D 空间(OpenGL 坐标系)中构建而成,另外还包含了用于描述三角形表面的纹理、光照、材质等信息。
利用 3D 建模软件,设计师可以构建一些复杂的形状,并将贴图应用到形状上去,不需要去关注图像技术细节。最后在导出模型文件时,建模工具会自己生成所有的顶点坐标、顶点法线和纹理坐标。
常用的模型文件格式有 .obj、.max、.fbx .3ds 等,其中.obj是 Wavefront 科技开发的一种几何体图形文件格式,包含每个顶点的位置、纹理坐标、法线,以及组成面(多边形)的顶点列表等数据,应用较为广泛。
# 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 开头的行表示存放的是一个三角面的信息,后面有三组数据分别表示组成三角面的三个顶点的信息,每个顶点信息的格式为:顶点位置索引/纹理坐标索引/法向量索引。模型加载可以使用模型加载库 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中
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空间计算得到切线以及二维切线来加载法线纹理贴图。
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中。
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就可以了。
#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空间矩阵 传给片元着色器
}
#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) ;
}
我有一个模型:classItem项目有一个属性“商店”基于存储的值,我希望Item对象对特定方法具有不同的行为。Rails中是否有针对此的通用设计模式?如果方法中没有大的if-else语句,这是如何干净利落地完成的? 最佳答案 通常通过Single-TableInheritance. 关于ruby-on-rails-Rails-子类化模型的设计模式是什么?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.co
我正在使用的第三方API的文档状态:"[O]urAPIonlyacceptspaddedBase64encodedstrings."什么是“填充的Base64编码字符串”以及如何在Ruby中生成它们。下面的代码是我第一次尝试创建转换为Base64的JSON格式数据。xa=Base64.encode64(a.to_json) 最佳答案 他们说的padding其实就是Base64本身的一部分。它是末尾的“=”和“==”。Base64将3个字节的数据包编码为4个编码字符。所以如果你的输入数据有长度n和n%3=1=>"=="末尾用于填充n%
我需要从一个View访问多个模型。以前,我的links_controller仅用于提供以不同方式排序的链接资源。现在我想包括一个部分(我假设)显示按分数排序的顶级用户(@users=User.all.sort_by(&:score))我知道我可以将此代码插入每个链接操作并从View访问它,但这似乎不是“ruby方式”,我将需要在不久的将来访问更多模型。这可能会变得很脏,是否有针对这种情况的任何技术?注意事项:我认为我的应用程序正朝着单一格式和动态页面内容的方向发展,本质上是一个典型的网络应用程序。我知道before_filter但考虑到我希望应用程序进入的方向,这似乎很麻烦。最终从任何
我想要做的是有2个不同的Controller,client和test_client。客户端Controller已经构建,我想创建一个test_clientController,我可以使用它来玩弄客户端的UI并根据需要进行调整。我主要是想绕过我在客户端中内置的验证及其对加载数据的管理Controller的依赖。所以我希望test_clientController加载示例数据集,然后呈现客户端Controller的索引View,以便我可以调整客户端UI。就是这样。我在test_clients索引方法中试过这个:classTestClientdefindexrender:template=>
我有一个包含模块的模型。我想在模块中覆盖模型的访问器方法。例如:classBlah这显然行不通。有什么想法可以实现吗? 最佳答案 您的代码看起来是正确的。我们正在毫无困难地使用这个确切的模式。如果我没记错的话,Rails使用#method_missing作为属性setter,因此您的模块将优先,阻止ActiveRecord的setter。如果您正在使用ActiveSupport::Concern(参见thisblogpost),那么您的实例方法需要进入一个特殊的模块:classBlah
鉴于我有以下迁移: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
我有一个表单,其中有很多字段取自数组(而不是模型或对象)。我如何验证这些字段的存在?solve_problem_pathdo|f|%>... 最佳答案 创建一个简单的类来包装请求参数并使用ActiveModel::Validations。#definedsomewhere,atthesimplest:require'ostruct'classSolvetrue#youcouldevencheckthesolutionwithavalidatorvalidatedoerrors.add(:base,"WRONG!!!")unlesss
我想向我的Controller传递一个参数,它是一个简单的复选框,但我不知道如何在模型的form_for中引入它,这是我的观点:{:id=>'go_finance'}do|f|%>Transferirde:para:Entrada:"input",:placeholder=>"Quantofoiganho?"%>Saída:"output",:placeholder=>"Quantofoigasto?"%>Nota:我想做一个额外的复选框,但我该怎么做,模型中没有一个对象,而是一个要检查的对象,以便在Controller中创建一个ifelse,如果没有检查,请帮助我,非常感谢,谢谢
在我的Controller中,我通过以下方式在我的index方法中支持HTML和JSON:respond_todo|format|format.htmlformat.json{renderjson:@user}end在浏览器中拉起它时,它会自然地以HTML呈现。但是,当我对/user资源进行内容类型为application/json的curl调用时(因为它是索引方法),我仍然将HTML作为响应。如何获取JSON作为响应?我还需要说明什么? 最佳答案 您应该将.json附加到请求的url,提供的格式在routes.rb的路径中定义。这
我有一些非常大的模型,我必须将它们迁移到最新版本的Rails。这些模型有相当多的验证(User有大约50个验证)。是否可以将所有这些验证移动到另一个文件中?说app/models/validations/user_validations.rb。如果可以,有人可以提供示例吗? 最佳答案 您可以为此使用关注点:#app/models/validations/user_validations.rbrequire'active_support/concern'moduleUserValidationsextendActiveSupport: