lib3ds java_Lib3DS教程:我的第一个模型
//声明:本文只是本人学习时的翻译,如有不当之处非常希望得到各位高手的指点,在下感激不尽。版权归原作者。
我决定写这个lib3ds教程,那是因为现在网上关于lib3ds的信息还非常少。仅仅是一个例子演示文件和一些毫无生机文档。在这第一个lib3ds教程里,你将会使用lib3ds写一个非常简单的程序来渲染你的3ds模型。第一个例子仅用了lib3ds库中很小的一部分接口,以便向你介绍lib3ds。因此,它不支持纹理,材质等等(那是接下来教程的事)。
基本原理
写代码之前,我们先要了解一些关于3DS格式的理论。一个3DS模型通常是从结点建立的。结点通常有一个类树的结构,有一个根结点,根结点又有两个分枝(子结点),然而,这些分枝又有自己的分枝等等。在lib3ds中,结点可以是很多东西,比如有几何结点、光源结点、相机结点。
每个几何结点都有一个相应的网格。你或许很好奇,什么是网格?简单说:就是面。它是一串带有相应材质的多边形。每个网格由好几个面(多边形)组成,这些面在lib3ds里就是一个带有自己坐标和法矢的三角形。本教程里,为了避免结点的困扰,我们只处理网格。
代码
好了,现在我们已经粗略的浏览了基本原理,接下来是写代码的时候了。我的代码用C++些的(但想转回C也不太难),并且使用OpenGL渲染(lib3ds并不依赖图形库,所有你可以用DX写渲染代码)以及QT4写Winodws代码。之所以选择了QT,那是因为它还包含加载图像的功能(以后我们还会用到纹理贴图)。
一个模型类
我们要做的第一件事就是创建一个简单的3DS模型类(以后可做扩展):
// Our 3DS model class
class CModel3DS
{
public:
CModel3DS(std:: string filename);
virtual void Draw() const;
virtual void CreateVBO();
virtual ~CModel3DS();
protected:
void GetFaces();
unsigned int m_TotalFaces;
Lib3dsFile * m_model;
GLuint m_VertexVBO, m_NormalVBO;
};
就像你看到的,这个类中的公共区域有如下函数:一个构造函数,一个(虚)析构函数,一个绘画函数和一个计算vbo的函数。这个CreateVBO函数将从lib3ds拷贝数据并存储到一个变量。可以传递此变量到我们的顶点缓冲对象中去的。我们使用顶点缓冲对象来增加渲染性能。
在保护区域,有一个计算面数的函数并将其值存到m_TotalFaces。好了,现在要到我们的第一个lib3ds代码了:m_model的声明。这是我们这个类中最重要的变量,因为它是获取我们的模型所有信息的关键。稍后再回到m_model上来。
类中最后两个变量,是我们的顶点缓冲对象的标识符。
在我们做任何渲染之前,我们要用lib3ds去加载我们的模型,这将在我们的构造函数中完成:
// Load 3DS model
CModel3DS::CModel3DS(std:: string filename)
{
m_TotalFaces = 0;
m_model = lib3ds_file_load(filename.c_str());
// If loading the model failed, we throw an exception
if(!m_model)
{
throw strcat("Unable to load ", filename.c_str());
}
}
构造函数以模型的文件名为第一个参数,并把它传递给lib3ds_file_load()这个函数。从此加载模型到内存中,并返回一个Lib3dsFile的指针。我们将把这个指针存储到m_model里。加载出错是时有可能的,所有有必要判断是否成功,出错就抛出异常。
下面让我们来电更有激情的代码,计算我们模型中面数总和的GetFaces函数。
// Count the total number of faces this model has
void CModel3DS::GetFaces()
{
assert(m_model != NULL);
m_TotalFaces = 0;
Lib3dsMesh * mesh;
// Loop through every mesh
for(mesh = m_model->meshes;mesh != NULL;mesh = mesh->next)
{
// Add the number of faces this mesh has to the total faces
m_TotalFaces += mesh->faces;
}
}
我们首先创建一个名为mesh的Lib3dsMesh指针。在循环中mesh先被赋值为m_model->meshes指向模型中第一个网格。然后一直循环下去,直到mesh为NULL(这意味着我们没有下一个网格了)。
我们的网格包含多样的种类或变量(包含下一网格的指针和网格的面数)。我们从此来计算模型中面数的总和。我们要依次来分配足够的内存来存储所有的顶点和法矢。
好了,我们现在到了最有趣的部分,CreateVBO()函数。此函数创建两个顶点缓冲对象:一个存储法矢,一个存储顶点。但是在传递数据到我们的vbo之前,我们需要在一个连续的数组中保存这些顶点和法矢。不幸的是,lib3ds并没有以我们想要的方式给出数据,因为这些几何量存储在每一个网格中。
// Copy vertices and normals to the memory of the GPU
void CModel3DS::CreateVBO()
{
assert(m_model != NULL);
// Calculate the number of faces we have in total
GetFaces();
// Allocate memory for our vertices and normals
Lib3dsVector * vertices = new Lib3dsVector[m_TotalFaces * 3];
Lib3dsVector * normals = new Lib3dsVector[m_TotalFaces * 3];
Lib3dsMesh * mesh;
unsigned int FinishedFaces = 0;
// Loop through all the meshes
for(mesh = m_model->meshes;mesh != NULL;mesh = mesh->next)
{
lib3ds_mesh_calculate_normals(mesh, &normals[FinishedFaces*3]);
// Loop through every face
for(unsigned int cur_face = 0; cur_face < mesh->faces;cur_face++)
{
Lib3dsFace * face = &mesh->faceL[cur_face];
for(unsigned int i = 0;i < 3;i++)
{
memcpy(&vertices[FinishedFaces*3+i], mesh->pointL[face->points[ i ]].pos, sizeof(Lib3dsVector));
}
FinishedFaces++;
}
}
// Generate a Vertex Buffer Object and store it with our vertices
glGenBuffers(1, &m_VertexVBO);
glBindBuffer(GL_ARRAY_BUFFER, m_VertexVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(Lib3dsVector) * 3 * m_TotalFaces, vertices, GL_STATIC_DRAW);
// Generate another Vertex Buffer Object and store the normals in it
glGenBuffers(1, &m_NormalVBO);
glBindBuffer(GL_ARRAY_BUFFER, m_NormalVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(Lib3dsVector) * 3 * m_TotalFaces, normals, GL_STATIC_DRAW);
// Clean up our allocated memory
delete vertices;
delete normals;
// We no longer need lib3ds
lib3ds_file_free(m_model);
m_model = NULL;
}
我们首先调用GetFaces来计算面数,然后为存储我们的顶点和法矢分配内存空间。如你所见,我在使用Lib3dsVector:一个来自lib3ds很棒的工具,其实它只是一个包含3个浮点数值数组的类型定义。
由于一个面包含3个顶点(要记得那是一个三角形^_^),我在为m_TotalFaces*3个顶点分配内存空间。
然后,创建两个变量,mesh(保存当前网格,就像在函数GetFaces中那样)和FinishedFaces。使用FinishedFaces来记录我们已经处理过的面数,因此我知道该从数组中何处开始拷贝数据。你或许已经从GetFaces中认出这个for循环了。在mesh循环中我使用了lib3ds的函数lib3ds_mesh_caculate_normals()获取当前网格的法矢,由于它处理一个连续的法矢数组,我们就可以传递我们的法矢变量到lib3ds_mesh_calculate_normals()。这样计算得到的法矢会自动拷贝到我们的法矢变量中去。
顶点存储在mesh->pointL中,但是mesh->pointL的索引却存在网格的面中。所有我们要循环经过网格中的每一个面。如上,我使用了mesh->faces来循环遍历这些面,然后把一个指针临时地存储到faceL(一个面数组)中的当前面。然后,创建一个循环来复制这些顶点到变量vertices(记着一个三角形三个顶点)。face->points[i]提供了顶点数组pointL中第i个顶点的位置。从中得到的元素是一个Lib3dsPoint。一个Lib3dsPoint仅有一个域:pos,这是一个Lib3dsVector。这正是我们想要的,因此,把它拷贝到我们的顶点数组中。
现在我们有了自己想要格式的顶点和法矢,我们要把这些数组传递给OpenGL。我不会叙述过多的细节,因为这不是个OpenGL教程。但它要做的是:生成并绑定一个vbo,然后把数组传递给OpenGL。
做完这些之后,我们要移除我们的数组,因为不再需要(因为数据已经存储在GPU了)。同样释放Lib3dsFile,因为不再需要(我们所要的一切都在GPU的内存里了)。
CModel3DS的最后一个函数就是绘图了。没什么有趣的,都是些标准的OpenGL代码:
// Render the model using Vertex Buffer Objects
void CModel3DS:: Draw() const
{
assert(m_TotalFaces != 0);
// Enable vertex and normal arrays
glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_NORMAL_ARRAY);
// Bind the vbo with the normals
glBindBuffer(GL_ARRAY_BUFFER, m_NormalVBO);
// The pointer for the normals is NULL which means that OpenGL will use the currently bound vbo
glNormalPointer(GL_FLOAT, 0, NULL);
glBindBuffer(GL_ARRAY_BUFFER, m_VertexVBO);
glVertexPointer(3, GL_FLOAT, 0, NULL);
// Render the triangles
glDrawArrays(GL_TRIANGLES, 0, m_TotalFaces * 3);
glDisableClientState(GL_VERTEX_ARRAY);
glDisableClientState(GL_NORMAL_ARRAY);
}
这些代码只是绑定vbo,然后告诉OpenGL在当前VBO中寻找数据。之后,就是使用glDrawArrays来渲染我们的数组了。
Qt代码
下面是一些特定的QT代码并包含了一些OGL代码,如果没兴趣,你可以跳过这一部分(毕竟这是一个lib3ds教程)。
QT有特定的类来处理OGL渲染:QGLWidget。它包含一些预制函数,比如renderText可以渲染字体到OGL场景。在QT中使用OGL,你要创建自己的控件,继承自QGLWidget。QGLWidget有一些特定的函数可以被QT的主循环调用。你可以重写这些函数来响应特定的事件。我要重写的3个函数是:paintGL(),每次窗口需要重绘的时候被调用;resizeGL,当调整了控件大小时会被调用;和initializeGL,你可以用来写所有的OGL初始化代码。
// A render widget for QT
class CRender : public QGLWidget
{
public:
CRender(QWidget *parent = 0);
protected:
virtual void initializeGL();
virtual void resizeGL(int width, int height);
virtual void paintGL();
private:
CModel3DS * monkey;
};
这是个类最最基础的,唯一有趣的是有一个叫做monkey的CModel3DS对象。
在我们的构造函数中,要加载这个monkey,3ds模型。如果得到一个异常,我们就打印一条消息到标准错误,并退出。
// Constructor, initialize our model-object
CRender::CRender(QWidget *parent) : QGLWidget(parent)
{
try
{
monkey = new CModel3DS("monkey.3ds");
}
catch(std::
string error_str)
{
std::cerr << "Error: " << error_str << std::endl;
exit(1);
}
}
在initializeGL里,是我们初始化OGL的一些代码并且创建了我们的vbo:
// Initialize some OpenGL settings
void CRender::initializeGL()
{
glClearColor(0.0, 0.0, 0.0, 0.0);
glShadeModel(GL_SMOOTH);
// Enable lighting and set the position of the light
glEnable(GL_LIGHT0);
glEnable(GL_LIGHTING);
GLfloat pos[] = { 0.0, 4.0, 4.0 };
glLightfv(GL_LIGHT0, GL_POSITION, pos);
// Generate Vertex Buffer Objects
monkey->CreateVBO();
}
首先设置我们的清屏颜色(如果我们清除屏幕,屏幕就会被重置为此颜色)为黑色,然后设置阴影模型为光滑(这意味着我们的一个几何单元可以有多重颜色)。之后,我们启用光照并把光源设置到视场后面的某个地方。最后,我们调用CreateVBO()为我们的3DS模型生成顶点缓冲单元(vbo)。
下面的函数resizeGL在调整控件大小的时候会被QT调用,所有,我们重置了视场并调整了MODELVIEW和PROJECTION矩阵。
// Reset viewport and projection matrix after the window was resized
void CRender::resizeGL(int width, int height)
{
// Reset the viewport
glViewport(0, 0, width, height);
// Reset the projection and modelview matrix
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
// 10 x 10 x 10 viewing volume
glOrtho(-5.0, 5.0, -5.0, 5.0, -5.0, 5.0);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
}
glViewport改变了视场(OGL渲染的区域)。然后,改变当前矩阵操作为PROJECTION并重置为标准单位阵。使用glOrtho()设置视景体为一个10*10*10的立方体,然后切换回模型视点矩阵操作,同样重置了当前阵。
渲染函数委实简单:
// Do all the OpenGL rendering
void CRender:: paintGL()
{
glClear(GL_COLOR_BUFFER_BIT);
// Draw our model
monkey->Draw();
// We don't need to swap the buffers, because QT does that automaticly for us
}
首先清屏,然后调用3ds模型的绘制函数,由它完成所有的渲染。我们不必交换前后缓冲区,QT会为我们自动完成这些。
最后,是主函数了。
int main(int argc, char **argv)
{
QApplication app(argc, argv);
CRender * window = new CRender();
window->show();
return app.exec();
}
首先我们第创建了一个QT程序,然后我们创建了自己的控件,通过show函数使之可视。要开始我们的程序,只要调用app.exe()就可以开启一个处理所有窗口事件的主循环。
下载并编译代码
我已经做好了这些代码的tar.gz压缩包,包括3DS模型和一个qmake工程文件。
要编译代码,可以运行以下命令:
qmake # generate Makefile
make # compile code
lib3ds java_Lib3DS教程:我的第一个模型相关推荐
- 模型部署入门教程(一):模型部署简介
前言 OpenMMLab 的算法如何部署?是很多社区用户的困惑.而模型部署工具箱 MMDeploy 的开源,强势打通了从算法模型到应用程序这 "最后一公里"! 今天我们将开启模型部 ...
- vim入门教程(实践第一)
2019独角兽企业重金招聘Python工程师标准>>> vim 是一个具有很多命令的功能非常强大的编辑器.限于篇幅,在本教程当中 就不详细介绍了.本教程的设计目标是讲述一些 ...
- sklearn快速入门教程:补充内容 -- sklearn模型评价指标汇总(聚类、分类、回归)
sklearn集成了大多数模型评价指标,这可以很大程度上方便我们的使用,尤其在对进行进行自动调参时可以方便我们进行选择. 做下这个笔记主要是为了补充之前的内容:sklearn快速入门教程:(四)模型自 ...
- sklearn快速入门教程:(四)模型自动调参
上个教程中我们已经看到在sklearn中调用机器学习模型其实非常简单.但要获得较好的预测效果则需要选取合适的超参数.在实际的项目中其实也有不少参数是由工程师借助其经验手动调整的,但在许多场景下这种方式 ...
- 机器学习:从入门到第一个模型
欢迎大家前往腾讯云技术社区,获取更多腾讯海量技术实践干货哦~ 作者:李春晓 导语: "从入门到第一个模型"差点就成了"从入门到放弃".本文是机器学习在运维场景下 ...
- (转)Django ==== 实战学习篇二 需求分析及设计,创建第一个模型---购物车的应用...
#####购物车应用: 角色:买方,卖方 用例:买方----浏览产品,创建订单 卖方-----管理查品,管理订单,管理发货 界面设计: 买方: 目录页明显查品信息,可以选择一个产品,打开购物车界面,同 ...
- 【sklearn-cookbook-zh】第一章 模型预处理
第一章 模型预处理 作者:Trent Hauck 译者:muxuezi 协议:CC BY-NC-SA 4.0 本章包括以下主题: 目录 第一章 模型预处理 简介 1.1 从外部源获取样本数据 Gett ...
- [LLVM教程]LLVM之第一个语言前端
目录: [LLVM教程]LLVM之第一个语言前端 [LLVM教程]LLVM之第一个语言前端/"万花筒":介绍与词法分析器 LLVM Tutorial: Table of Conte ...
- 李含光《C语言程序设计教程》答案第一——四章
李含光<C语言程序设计教程>答案第一--四章 第一章 单选 ACDCB 填空 函数 主函数或main函数 scanf printf 第二章 单选 CBCCC CDCDC D 填空 1 26 ...
最新文章
- 使用 IntraWeb (8) - 系统模板
- 重新编译php又出错了。undefined reference to `php_xx
- linux c 消息队列简介
- Invalid character escape '\o'.
- electron 打包把node代理服务打包进去_专题:让C++给node做技术加持(三)编译electron本地模块踩坑记
- python selenium下载,在python中通过Selenium Webdriver下载文件
- 动态实例分割SOLOv2,更快更强更精准!
- java异步处理_Java编程开发好入门吗 消息队列的用途有哪些
- pgp 私钥需要什么样的保护措施_参与以太坊 2.0 存款合约前需要了解的相应风险...
- 提前11秒,AI让神经科学家预知了你的决定
- Android笔记:触摸事件的分析与总结----TouchEvent处理机制
- 四川2021年5月计算机二级报名时间,四川2021年3月计算机等级考试什么时候报名...
- c语言函数求圆面积,C语言编写函数,计算圆面积.
- Lua调试:getinfo详解
- 一元云购系统接入手机短信功能说明【V3版】
- 如何提高c语言编程能力,如何快速提高自己的编程能力
- pytest单元测试
- get 传值 是params
- Topic 17. 临床预测模型之缺失值识别及可视化
- SuperMap Objects组件式开发
热门文章
- Explainability in Graph Neural Networks:A Taxonomic Survey
- 给小铄做的围棋入门思道导图
- 创新科技,只为尊重音乐原声:dFiM睿妙深度剖析
- 串′ababaaababaa′的next数组(求next数组思路与实例)
- 微信3D小游戏系列一:在微信小游戏中使用threejs
- Mysql迁移Postgresql
- Mac配置Iterm2+oh-my-zsh+powerlevel10k打造炫酷终端
- Android 性能优化总结
- 【面试集锦 - 嵌入式软件工程师 - MCU篇】
- 【安徽老乡鸡】签约企企通,打造国内餐饮行业采购数字化标杆