版权归原作者 我决定写这个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 |