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

lib3ds加载3ds文件通过opengl显示

慕容聪
2023-12-01

基本原理

写代码之前,我们先要了解一些关于3DS格式的理论。一个3DS模型通常是从结点建立的。结点通常有一个类树的结构,有一个根结点,根结点又有两个分枝(子结点),然而,这些分枝又有自己的分枝等等。在lib3ds中,结点可以是很多东西,比如有几何结点、光源结点、相机结点。

每个几何结点都有一个相应的网格。你或许很好奇,什么是网格?简单说:就是面。它是一串带有相应材质的多边形。每个网格由好几个面(多边形)组成,这些面在lib3ds里就是一个带有自己坐标和法矢的三角形。本教程里,为了避免结点的困扰,我们只处理网格。

 

代码

好了,现在我们已经粗略的浏览了基本原理,接下来是写代码的时候了。我的代码用C++些的(但想转回C也不太难),并且使用OpenGL渲染(lib3ds并不依赖图形库,所有你可以用DX写渲染代码)以及QT4Winodws代码。之所以选择了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;

        }

}

 

我们首先创建一个名为meshLib3dsMesh指针。在循环中mesh先被赋值为m_model->meshes指向模型中第一个网格。然后一直循环下去,直到meshNULL(这意味着我们没有下一个网格了)。

我们的网格包含多样的种类或变量(包含下一网格的指针和网格的面数)。我们从此来计算模型中面数的总和。我们要依次来分配足够的内存来存储所有的顶点和法矢。

 

好了,我们现在到了最有趣的部分,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,你要创建自己的控件,继承自QGLWidgetQGLWidget有一些特定的函数可以被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;

};

 

这是个类最最基础的,唯一有趣的是有一个叫做monkeyCModel3DS对象。

在我们的构造函数中,要加载这个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调用,所有,我们重置了视场并调整了MODELVIEWPROJECTION矩阵。

 

// 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()就可以开启一个处理所有窗口事件的主循环。

 类似资料: