当前位置: 首页 > 工具软件 > lib3ds > 使用案例 >

Lib3DS教程

贺俊材
2023-12-01
版权归原作者


我决定写这个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 faces;cur_face++)
                {
                        Lib3dsFace * face = &mesh->faceL[cur_face];
                        for(unsigned int i = 0;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提供了顶点数组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 
                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


本文来自ChinaUnix博客,如果查看原文请点:http://blog.chinaunix.net/u1/56532/showart_1361371.html

 类似资料: