基本原理
写代码之前,我们先要了解一些关于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()就可以开启一个处理所有窗口事件的主循环。