Poll

Ik ben...:


Tags for Lib3ds tutorial: My first model









Lib3ds tutorial: My first model
opengl animated
I've decided to write this Lib3ds tutorial, because there is very little information available about lib3ds on the internet. Only an example player file and some doxygen documentation.
In this first lib3ds tutorial you'Il be making a very simple program that uses lib3ds to render a 3ds model. This first example only uses a very small portion of the lib3ds library to introduce you to lib3ds

, so it won't support textures, materials etc etc(that's for the following tutorials).

The theory
But before we get to the code, first some theory about the 3DS format. A 3DS model is usually built up from nodes. Nodes usually have a tree like structure, there's a root node, and that node can have a couple of branches(childs), and those branch in their turn can also have branches et cetera. In lib3ds, nodes can be several things, for example there are geometry-nodes, light-nodes and camera-nodes.
Every geometry-node has a corresponding mesh. You might ask, what exactly is a mesh? To simply put it: it's a surface. It's a bunch of polygons with corresponding material properties. Every mesh is made up of several faces(polygons), which in lib3ds's case is just a triangle, each with it's own data such as coordinates and normals. In this first tutorial I won't bother you with nodes, we will only deal with meshes.

The code
Ok, now that we've gone through the theory it's time for the code. My code is written in C++(but it shouldn't be too difficult to port the code back to C) and uses OpenGL for rendering(lib3ds is not depended on a graphics library so you could also write the rendering code for DirectX) and QT 4 for the windowing code. I chose this combination for the sake of portability(I am a linux user after all), and I chose QT because it also contains functionality for loading images(because we'Il be using textures in the up-following tutorials).

A model class
The first thing i'm gonna do is create a simple 3DS model class(that we can extend later):

// 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;
};


As you can see this class has the following functions in it's public section: A constructor, a (virtual) destructor, a drawing function and a function that generates vbo's. The CreateVBO function will copy data from lib3ds and store it in a variable that we can pass to our Vertex Buffer Objects. We use Vertex Buffer Objects to increase rendering performance.
In our protected section we have a function that will count the number of faces and stores it in m_TotalFaces. Ok, now we've come to our first lib3ds code: the declaration of m_model. This is the most important variable of this class because it's the key to all the information in our model. I'Il get back on m_model later.
The last two variables in this class are identifiers for our vertex buffer objects.

Before we can do any rendering, we need lib3ds to load our model, this is done in our constructor:

// 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());
        }
}


The constructor takes the filename of the model as it's first argument and passes it to the function lib3ds_file_load() which will load the model to the memory and returns a Lib3dsFile pointer, which we will store in m_model. Obviously there could have gone something wrong while loading the model, so we need to check whether we actually got a pointer, if that's not the case we throw an exception.

Ok, let's get to some more exciting code, our GetFaces function which will count the total number of faces of our model.

// 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;
        }
}


We first create a Lib3dsMesh pointer called mesh. In our for loop the variable mesh gets set to m_model->meshes which points to the first mesh of the model. After that we keep looping until mesh equals to NULL(which means we don't have a next mesh).
Our mesh contains a wide variety or variables(http://lib3ds.sourceforge.net/a00030.html) including the next mesh(which we use in our for loop) and the number of faces (mesh->faces). We use this information to calculate the total number of faces in our model. We need this information, to allocate enough memory to store all the vertices and normals.

Ok, we've now come to the most interesting part of all, the CreateVBO() function. This function creates two vertex buffer objects: One to store the normals and one to store the vertices. But before we can pass the data to our vbo's, we need to have the vertices and normals in a continuous array. Unfortunately, lib3ds doesn't have the data the way we want it, because the geometry is stored per mesh.

// 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;
}


We first call GetFaces to calculate the number of faces, after that we allocate memory to store our vertices and normals. As you can see i'm using Lib3dsVector: It's a nice utility from lib3ds which is really just a typedef for an array of 3 floats.
Because a face consists of 3 vertices(it's a triangle remember , i'm allocating memory for m_TotalFaces * 3 vectors.
After that I create two variables, mesh(to hold the current mesh, just like in the function GetFaces) and FinishedFaces. I use FinishedFaces to keep track of the number of faces i've processed, so I know to where in the array I should copy the data. You probably recognize the for loop from GetFaces. In the mesh loop I use the lib3ds function lib3ds_mesh_calculate_normals() to get the normals for the current mesh, and since it produces a continuous array of normals, we can pass our variable normals to lib3ds_mesh_calculate_normals(). This way the calculated normals are automaticely copied in our normals variable.
The vertices are stored in mesh->pointL, however the indices for mesh->pointL are stored in the faces of the mesh, so we need to loop through every face of our mesh. As you can see I use mesh->faces to loop through the faces, and then I temporarily store a pointer to the current face from faceL (which is an array of faces). I then create a loop to copy the vertices to the variable vertices (remember that there are 3 vertices per triangle). face->points[ i ] gives me the position of the ith vertex of the big vertex array pointL. The element that I got from that is a Lib3dsPoint. A Lib3dsPoint only has one field: pos, which is a Lib3dsVector. That's exactly what we needed, so we copy it to our vertices array.
Now that we have the vertices and normals the way we want it to be, we need to pass those arrays to OpenGL. I won't get into much details, since this is not an OpenGL tutorial. But what it does is: It generates and binds a vbo, and then the array gets passed to OpenGL.
After we've done that, we remove our arrays, since we no longer need it (because the data is now stored in the GPU). I also free the Lib3dsFile, because I won't be using it anymore(everything we need is now in the memory of the GPU).

CModel3DS's last function is Draw. It's not very interesting and it's code is just standard 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);
}


What it does is it binds the vbo, and then it tells OpenGL to look for data in the current VBO. After that we render our arrays using glDrawArrays.

The QT code
The following code is QT specific and contains some OpenGL code, you can skip this section if you don't think this is very interesting(this is a lib3ds tutorial after all).

QT has a special class for OpenGL rendering: QGLWidget. It contains some prefabricated functions such as renderText to render characters to your OpenGL scene.To use OpenGL in QT, you need to create your own widget, that inherits from QGLWidget. QGLWidget has certain functions that are automaticly called by QT's mainloop. You can override these functions to react on certain events. I'm going to override three functions: paintGL() which is called whenever the window needs to be repainted, resizeGL which is called when the widget got resized and initializeGL which you can use for all your OpenGL initializing code.

// 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;
};


As you can see this class is pretty basic, and the only interesting thing is that we have a CModel3DS object called monkey.
In our constructor we try to load the model monkey.3ds. If we receive an exception we print a message to stderr and exit:

// 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);
        }
}


In initializeGL we initialize some OpenGL code and we create our vbo's:

// 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();
}


We first set our clear color(if we clear the screen, the screen gets reset to this color) to black and then we set our shade model to smooth(that means that we can have multiple colors per geometric primitive). After that we enable lighting and set the position of the light somewhere to the back of our viewing volume. Lastly we generate the Vertex Buffer Objects for our 3ds model by calling CreateVBO().

The following function resizeGL gets called by QT when the widget gets resized, so that we can reset the viewport and adjust the modelview and projection matrix.

// 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 changes the viewport(the area to which OpenGL renders). After that I change the current matrix to PROJECTION and reset the matrix to the standard identity matrix. With glOrtho() I set the viewing volume to a 10x10x10 cube and then I switch back to the modelview matrix, which I also reset to the identity matrix.

The render function is pretty simple:

// 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
}


It first clears the screen, and then it calls the Draw function of our 3ds model, which takes care or all the rendering. We don't need to swap the front and back buffer because QT does that automaticely for us.

And lastly, the main function.

int main(int argc, char **argv)
{
        QApplication app(argc, argv);
        CRender * window = new CRender();
        window->show();
        return app.exec();
}


We first create a QT application, and after that we create our widget which we will make visible with the show function. To make our application start, we only have to call app.exec() which will start a mainloop that takes care of all the windowing stuff.

Downloading and compiling the code
I've made a tar.gz file with the code, the 3DS model and a qmake-project file.
Download code
To compile the code, you need to issue the following commands:
qmake # generate Makefile
make # compile code
Sun, 11/11/2007 - 21:14 — Hylk0r | Tags:





Comments

wel een pro tutorial, allen wellicht volgende keer ook wat voorbeeldrenders?

(als in: wat voor extra nut heeft dit boven een al bestaande, en externe, renderer?)
Thu, 01/01/1970 - 01:00 — laurens (not verified)



Quote:
wel een pro tutorial, allen wellicht volgende keer ook wat voorbeeldrenders?

(als in: wat voor extra nut heeft dit boven een al bestaande, en externe, renderer?)

There are several reasons for using lib3ds instead of an existing engine:

  • You want to write your own engine
  • You have much more control over the rendering process: You can choose what material properties you want to use and you can decide how things are rendered.
  • Using a complete engine is often overkill (for example, if you write a map-editor and you want 3DS support you don't want to use a 100 mb engine just to support 3DS).
Thu, 01/01/1970 - 01:00 — Hylke (not verified)



This is really a nice job..
Thu, 01/01/1970 - 01:00 — KD (not verified)



Very nice tutorial, no need to bang head to the wall due to lack of good instructions.. (banged my head on the wall for 3days with freetype2)

Thanks

you might want to change
delete vertices;
delete normals;

to
delete[] vertices;
delete[] normals;
Sun, 10/08/2008 - 19:39 — Anonymous (not verified)



Excellently written article, if only all bloggers offered the same content as you, the internet would be a much better place. Please keep it up! Cheers.
Mon, 09/03/2009 - 03:05 — Zoran (not verified)



Very nice tutorial, no need to bang head to the wall due to lack of good instructions banged my head on the wall for 3days with freetype2.
Fri, 19/06/2009 - 07:15 — club penguin (not verified)



This one helped a lot! THX!
Mon, 31/08/2009 - 23:47 — Chris (not verified)



THX very much for this tutorial!!
Sun, 13/12/2009 - 18:39 — nelu (not verified)



I hope it will be a fine tutorial. But I cant try this code because i donot have QApplication and QGLWidget. Can any one tell how do get these header files. thanks
Mon, 28/12/2009 - 12:43 — kash (not verified)



Thank a lot for this great tutorial. Code was neat, and your explanation following of code snippets were perfect.
Fri, 22/01/2010 - 22:23 — Shantanu (not verified)



This is a very good tutorial indeed. It heped me a lot. Thank you very much. I look forward to seeing the up-coming ones.
Wed, 03/02/2010 - 14:26 — Anonymous (not verified)



Awesome tutorial, congrats! Just one problem: nothing is showing up in when I launch the app... do you have any idea why? When I draw with a QWidget instead of a QGLWidget, I see a window, but not with QGLWidget...
Tue, 02/03/2010 - 16:02 — Sebastien (not verified)



THX!
Sun, 31/10/2010 - 17:38 — povik (not verified)



So very good stuff
Tue, 16/11/2010 - 11:23 — OrionXL (not verified)



I've codded this using the wxWidgets API instead of QT.
Problem is.. I get a black window. All other renering to the opengl canvas works fine. I can't think of why the .3ds model is not getting drawn.
(The .3ds file is good and opens in autodesk. It also runs with lib3ds's 3dsplay.c example program perfectly, so I know the lib3ds library is working)

One thought is that I have to use the glew library to get glGenBuffers(). And I must glewInit() first as well. I don't think that's the issue yet because all other opengl stuff works. But I'm not ruleing it out just yet.

http://pastebin.com/CZ82VYWT

Any ideas would be a great help.
Sun, 09/01/2011 - 02:34 — aquawicket (not verified)



I think I have the same problem as you do. Couldn't compile it without glew in Windows, but I see no object. I think there is some problem reading 3ds file. First mesh has no pointer to the next one.
Did you find a solution?
Mon, 31/01/2011 - 20:39 — nasmork (not verified)



Great info!
I found your web page on google and it seems to have what I've been looking for. Here's another source that worth a look about this also. Thanks for sharing!

Patricia
Sat, 15/01/2011 - 08:45 — Latest Tech News (not verified)



I suppose, I have the same problem as aquawicket. I couldn't make tutorial code compile without glew. Now I get some crap in m_model - first mesh has NULL in next pointer. Looks like I have some problems reading 3DS file, though file is good (opens in 3d max and some 3d viewer I downloaded from internet).
What could be wrong? My lib3ds lib? Or dll? Or use of glew (I'm pretty sure it's not)?
How to compile tutorial code in windows? What extensions to use?

Thanks in advance.
Mon, 31/01/2011 - 20:35 — nasmork (not verified)