I made this thing...

Posted by Astryl on May 2, 2013, 2:54 a.m.

As a challenge, yesterday, I decided to start writing an OBJ model loader again, and see how hard it would be to plug it in to Exile.

30 minutes later, and this:

Don't get too excited though, I'm probably not going to be using 3D models for items or weapons in Exile. Mostly because 'converting' or 'reimagining' all the existing items and weapons in 3D, not to mention the enemies (Because sprites and models won't look too good together) would be a pain in the neck.

Their actual use was intended as simple level decorations, like tables, lanterns, whatever. Basically, those floor/ceiling decals, but models.

The 3D, however, is now a core part of the engine, and I'm intending to make a game that fully utilizes it, using the existing engine. Specifically: A Doom/Quake inspired shooter. Because I can. :P

Animation considerations

Eventually, I'm going to want animations. I have a few ideas for this.

Solution one is to write an MD2/MD3 model loader. I have been wanting to do this for some time; but the coordinate system in MD2/MD3 models doesn't match mine, so it'd require a major core engine rewrite. So scratch this.

Solution two is to use Blender's OBJ animation export, which exports an animation as a series of separate baked models. (So something like sword_001.obj, sword_002.obj, etc). Of course, this would be a lot of little files.

So my idea is to use my PAK tool to pack them together. Problem solved.

Another plus here is that my engine is running on a fixed timestep. Makes it easier to 'play' the animations.

System requirements

I remember when whoever-it-was who did the RPG4D playthroughs laughed at my Readme, and the system requirements. Well guess what? I didn't just smoke them up. They're worse now though.

This being my first 'complete' 3D game and engine, it's inefficient. Like the IRC bot, it was 'grown', not designed from the ground up with any specific purpose or goal in mind. That I got it to where it is today is nothing short of miraculous (And it still hasn't crashed much… a bonus, I believe).

Of course, it'll run fine on anything that has a Dual Core CPU, and at least an nVidia 8 series graphics card. And I've managed to keep it's RAM imprint very low (40MB at average, up to 200MB with lots of enemies/models. And I mean a lot).

Anyway, I've managed to somewhat de-lobotomize the enemy AI, to the point where most of it functions correctly. 'cept for the Red Gargoyle. But that's a small fix.

Pardon me for rambling about Exile again :P

EDIT/UPDATE:

Here, have the source code.

// obj.h
// Loads OBJ models and renders them using VBOs or Immediate mode

#ifndef OBJ_H
#define OBJ_H

#include <cstdio>
#include <cstdlib>
#include <cmath>
#include <fstream>
#include <iostream>
#include <string>
#include <vector>
#include <sstream>
#include <MIO.h>

#include <SFML/Graphics.hpp>
#include <SFML/System.hpp>
#include <SFML/Window.hpp>

#include <gl/glew.h>
#include <gl/gl.h>
#include <gl/glext.h>

using namespace std;

typedef struct vert_s
{
    float x, y, z;

    vert_s(float ax, float ay, float az)
    {
        x = ax;
        y = ay;
        z = az;
    }
}vert_t;

typedef struct texco_s
{
    float s, t;

    texco_s(float as, float at)
    {
        s = as;
        t = at;
    }
}texco_t;

typedef struct face_vert_s
{
    int i1, i2, i3;

    face_vert_s(int a1, int a2, int a3)
    {
        i1 = a1;
        i2 = a2;
        i3 = a3;
    }
}face_vert_t;

typedef struct face_texco_s
{
    int i1, i2, i3;

    face_texco_s(int a1, int a2, int a3)
    {
        i1 = a1;
        i2 = a2;
        i3 = a3;
    }
}face_texco_t;

typedef struct model_obj_s
{
    string mtllib;
    string oname;
    vector<vert_t> ls_verts;
    vector<texco_t> ls_texco;
    vector<face_vert_t> ls_faceverts;
    vector<face_texco_t> ls_facetexco;

    int numverts;
    int numtexcos;
    int numfaces;
    GLuint vbo;
    GLuint tbo;
    int vboelements;
}model_obj_t;

/** Loads an OBJ model **/

void mobj_load(string filename, model_obj_t& obj);

/** Renders a previously loaded OBJ model **/
// Supply an SFML texture

void mobj_render(const model_obj_t& obj, sf::Texture tex);

/** Builds VBOs for a model **/
void mobj_build(model_obj_t& obj);

/** Render a VBO **/
void mobj_render_vbo(const model_obj_t& obj,sf::Texture& tex);

#endif

And the implementation:

// obj.cpp
#include "obj.h"
#include <gl/gl.h>

void mobj_load(string filename, model_obj_t& obj)
{
    vector<string> filedata;
    ifstream in(filename);
    if(!in.is_open())
    {
        cout << "Couldn't open " << filename << " for reading!" << endl;
        throw 1; 
    }

    string s = "";
    while(getline(in, s))
    {
        if(in.eof())break;
        filedata.push_back(s);
    }

    in.close();

    /// Clear any existing data
    while(obj.ls_facetexco.size() > 0)obj.ls_facetexco.pop_back();
    while(obj.ls_faceverts.size() > 0)obj.ls_faceverts.pop_back();
    while(obj.ls_texco.size() > 0)obj.ls_texco.pop_back();
    while(obj.ls_verts.size() > 0)obj.ls_verts.pop_back();

    obj.numverts = 0;
    obj.numtexcos = 0;
    obj.numfaces = 0;

    /// We have the raw data. Let's parse it and try to make sense of it.
    for(int i = 0; i < filedata.size(); i++)
    {
        vector<string> line = MIO::split(filedata[i_hate_bbcode_sometimes],' ');

        string lo = "";
        for(int j = 1; j < line.size(); j++)
        {
            lo += line[j];
        }

        stringstream linestream(lo);

        if(line[0] == "#")
        {
            cout << "Comment line..." << endl;
        }
        if(line[0] == "mtllib")
        {
            obj.mtllib = line[1];
            cout << "Material file: " << obj.mtllib << endl;
        }
        if(line[0] == "o")
        {
            obj.oname = line[1];
            cout << "Object Name (First only): " << obj.oname << endl;
        }
        if(line[0] == "v")
        {
            // Get rest of input from the stream
            float x, y, z;
            if(line.size() >= 4)
            {
                x = atof(line[1].c_str());
                y = atof(line[2].c_str());
                z = atof(line[3].c_str());
            }
            else
            {
                x = -99;
                y = -99;
                z = -99;
            }
            // Push vertices to model
            obj.ls_verts.push_back(vert_t(x,y,z));
        }
        if(line[0] == "vt")
        {
            // Get rest of input from the stream
            float s, t;
            if(line.size() >= 3)
            {
                s = atof(line[1].c_str());
                t = atof(line[2].c_str());
            }
            else
            {
                s = -99;
                t = -99;
            }
            // Push vertices to model
            obj.ls_texco.push_back(texco_t(s,t));
        }
        if(line[0] == "f")
        {
            // Face reading is going to prove annoying at best.
            // No normals read in this version.
            // Assume tris only

            int vi[3], vt[3];
            if(line.size() >= 4)
            {
                vector<string> ik1 = MIO::split(line[1],'/');
                vector<string> ik2 = MIO::split(line[2],'/');
                vector<string> ik3 = MIO::split(line[3],'/');

                vi[0] = atoi(ik1[0].c_str());
                vi[1] = atoi(ik2[0].c_str());
                vi[2] = atoi(ik3[0].c_str());

                vt[0] = atoi(ik1[1].c_str());
                vt[1] = atoi(ik2[1].c_str());
                vt[2] = atoi(ik3[1].c_str());
            }
            else
            {
                vi[0] = vi[1] = vi[2] = -1;
                vt[0] = vt[1] = vt[2] = -1;
            }

            obj.ls_facetexco.push_back(face_texco_t(vt[0],vt[1],vt[2]));
            obj.ls_faceverts.push_back(face_vert_t(vi[0],vi[1],vi[2]));
        }
    }

    cout << "Number of verts: " << obj.ls_verts.size() << endl;
    cout << "Number of texture coordinates: " << obj.ls_texco.size() << endl;
    cout << "Number of faces: " << obj.ls_faceverts.size() << endl;
}

/// Render code

void mobj_render(const model_obj_t& obj, sf::Texture tex)
{
    tex.bind();

    for(int i = 0; i < obj.ls_faceverts.size(); i++)
    {
        face_vert_t ft = obj.ls_faceverts[i_had_to_change_these];
        face_texco_t tt = obj.ls_facetexco[innocuous];
        glBegin(GL_TRIANGLES);
            glTexCoord2f(obj.ls_texco[tt.i1-1].s,obj.ls_texco[tt.i1-1].t);
            glVertex3f(obj.ls_verts[ft.i1-1].x,obj.ls_verts[ft.i1-1].y,obj.ls_verts[ft.i1-1].z);

            glTexCoord2f(obj.ls_texco[tt.i2-1].s,obj.ls_texco[tt.i2-1].t);
            glVertex3f(obj.ls_verts[ft.i2-1].x,obj.ls_verts[ft.i2-1].y,obj.ls_verts[ft.i2-1].z);

            glTexCoord2f(obj.ls_texco[tt.i3-1].s,obj.ls_texco[tt.i3-1].t);
            glVertex3f(obj.ls_verts[ft.i3-1].x,obj.ls_verts[ft.i3-1].y,obj.ls_verts[ft.i3-1].z);

            glTexCoord2f(obj.ls_texco[tt.i1-1].s,obj.ls_texco[tt.i1-1].t);
            glVertex3f(obj.ls_verts[ft.i1-1].x,obj.ls_verts[ft.i1-1].y,obj.ls_verts[ft.i1-1].z);
        glEnd();
    }
}

void mobj_build(model_obj_t& obj)
{
    // 'draw' the model into an array
    int ee = obj.ls_verts.size()*(sizeof(float)*11);
    cout << "NUM ELEMENTS: " << ee << endl;
    //getchar();
    float verts[ee];
    int j = 0;
    for(int i = 0; i < obj.ls_faceverts.size(); i++)
    {
        face_vert_t ft = obj.ls_faceverts[index_variables];
        face_texco_t tt = obj.ls_facetexco[into_this_bloat];
            cout << "E: " << j << endl;
            verts[j++] = obj.ls_verts[ft.i1-1].x;
            verts[j++] = obj.ls_verts[ft.i1-1].y;
            verts[j++] = obj.ls_verts[ft.i1-1].z;
            // Texcoord
            verts[j++] = obj.ls_texco[tt.i1-1].s;
            verts[j++] = obj.ls_texco[tt.i1-1].t;

            verts[j++] = obj.ls_verts[ft.i2-1].x;
            verts[j++] = obj.ls_verts[ft.i2-1].y;
            verts[j++] = obj.ls_verts[ft.i2-1].z;
            // Texcoord
            verts[j++] = obj.ls_texco[tt.i2-1].s;
            verts[j++] = obj.ls_texco[tt.i2-1].t;

            verts[j++] = obj.ls_verts[ft.i3-1].x;
            verts[j++] = obj.ls_verts[ft.i3-1].y;
            verts[j++] = obj.ls_verts[ft.i3-1].z;
            // Texcoord
            verts[j++] = obj.ls_texco[tt.i3-1].s;
            verts[j++] = obj.ls_texco[tt.i3-1].t;

            // For reference
            //glVertex3f(obj.ls_verts[ft.i1-1].x,obj.ls_verts[ft.i1-1].y,obj.ls_verts[ft.i1-1].z);
            //glVertex3f(obj.ls_verts[ft.i2-1].x,obj.ls_verts[ft.i2-1].y,obj.ls_verts[ft.i2-1].z);
            //glVertex3f(obj.ls_verts[ft.i3-1].x,obj.ls_verts[ft.i3-1].y,obj.ls_verts[ft.i3-1].z);
    }

    int elements = j;
    cout << "Number of VBO elements: " << elements << endl;
    cout << "Building VBO..." << endl;

    obj.vboelements = elements;

    glGenBuffers(1, &obj.vbo);
    glBindBuffer(GL_ARRAY_BUFFER,obj.vbo);
    glBufferData(GL_ARRAY_BUFFER,sizeof(verts),verts, GL_STATIC_DRAW);
}

void mobj_render_vbo(const model_obj_t& obj,sf::Texture& tex)
{
    tex.bind();
    glBindBuffer(GL_ARRAY_BUFFER, obj.vbo);
    glVertexPointer(3,GL_FLOAT,sizeof(float)*5,0L);
    glBindBuffer(GL_TEXTURE_2D_ARRAY, obj.tbo);
    glTexCoordPointer(2,GL_FLOAT,sizeof(float)*5,12L);
    glEnableClientState(GL_VERTEX_ARRAY);
    glEnableClientState(GL_TEXTURE_COORD_ARRAY);
    //glClientActiveTexture(GL_TEXTURE0);

    glDrawArrays(GL_TRIANGLES,0,obj.vboelements/5);
    glDisableClientState(GL_TEXTURE_COORD_ARRAY);
    glDisableClientState(GL_VERTEX_ARRAY);
    glBindBuffer(GL_ARRAY_BUFFER,0);
    glBindBuffer(GL_TEXTURE_2D_ARRAY,0);
}

EDIT the second:

There's still a good bit of ambiguous code in that lot. Scaffolding, mostly, such as the bits that tell you a comment has been read.

Also, using this in Immediate mode is slow with too many objects (Mostly because all the vertices, normals and indices are being stored in vectors, as floats). That's why the VBO code is there.

If you don't know what VBOs are…

Short and simplified explanation of VBOs

Vertex Buffer Object's allow one to store all the vertex, normal and texture coordinate data (Along with some other types of data) in a single array (Or multiple arrays, but single is more efficient), then offload this array straight to the graphics card, thus storing the verts, normals and texture coordinates in VRAM, which is very fast. This makes model rendering extremely fast.

With my vbo rendering code, only one loop through the model vectors is required, when the vbo is constructed. That only happens once, when the model is loaded in Exile.

A VBO can also be edited during runtime. So if I wanted to deform or animate my models, I can.

My next move is to build Exile's levels into VBO's at load time (Will take longer to load, slightly. Like about 0.5 seconds longer. And it's already only taking a few milliseconds). This will net a major performance increase.

Right. I'm done.

Comments

Cesque 11 years, 4 months ago

Good. Because with 3d models, it will look exactly like Hexen II.

Astryl 11 years, 4 months ago

Quote: Cesque
Good. Because with 3d models, it will look exactly like Hexen II.
Quote: Mega
I'm probably not going to be using 3D models for items or weapons in Exile.

But… all that extra work… Oh well. I need the practice, I guess…

Visor 11 years, 4 months ago

Nice. It looks like this is progressing very well. Are you going to implement traps like crushing walls, or pits of death into the final product?

Astryl 11 years, 4 months ago

The functionality is already there for that, actually. So yeah.

Toast 11 years, 4 months ago

You made something which can parse obj files, draw all the vertices into your game, texture them correctly and load them into a map in… 30 minutes?

Astryl 11 years, 4 months ago

Add another 10 minutes and you can add 'uses VBOs to render' to that. Yes.

Toast 11 years, 4 months ago

Well. Impressive. Of course I have no idea how all this OpenGL type stuff works. I can imagine making an FPS from scratch is very educational.

Astryl 11 years, 4 months ago

It gets easier the more you do it. I managed to pull this off based on what I remembered from my last four or so failed attempts at writing an OBJ loader.

The best thing about this code, though, is that it's completely independent of the Exile source; give me an OpenGL context, and I can load/render textured OBJ models using this code.

Of course, a few features aren't implemented. Material values, for instance (Though I don't usually use them when modeling for games), normal values (Easy enough to add), and separate object blocks (Not really worth it. I want a single mesh, not a group of individual meshes).

Cesque 11 years, 4 months ago

My comment was actually a response to the line you've quoted in response to my comment, Mega :P

(I guess I should have used 'would' rather than 'will')

Astryl 11 years, 4 months ago

Ah. Sense has been made. Right. As you were, gentlemen.