Extending Quake Limits - Part 3: Alias Models
Posted: Fri Jan 08, 2010 11:13 pm
This one's for GLQuake only I'm afraid; I might study the software Quake code and see if something can be done there too at a later stage.
Some true evil lives in the alias model code: the format is horrible, the loader is disgusting, the renderer is awkward to work with. Every time you use the alias model code a homeless baby puppy dog dies somewhere.
Concept
We're going to accomplish a few things here. Firstly we're going to remove all limits on the number of vertexes and triangles in an alias model (unfortunately the number of frames requires a protocol change). Secondly we're going to give ourselves a much cleaner in-memory format (the on-disk format is beyond redemption I'm afraid). Thirdly we're going to have cleaner (and therefore more maintainable) elements in the loader. Fourthly we'll be doing the same for the renderer. Fifthly we're going to save on memory by only storing a single copy of each vertex.
For the sake of clarity and keeping things short I've omitted bbox correction, anything to do with coloured light or interpolation, and shadows. It should be obvious what's needed from reading this code.
I also need to give a heads-up that this is not the most efficient way to render an alias model (although it might be more efficient than GLQuake, depending on your hardware). The most efficient way involves vertex arrays and we might get a look at that as an addendum to this tutorial at some time. There's enough going on here as it is already!
The Code
Quite a bit of code here, so let's take it one step at a time. I might comment briefly after each one if the changes made require it.
Firstly we're going to gl_model.h and replacing our entire aliashdr_t struct with these two:
This is our new in-memory alias model format. Before we can get to use it we need to load the alias model, so in gl_model.c we go to the "ALIAS MODELS" comment header and replace all the horrible global variables with this:
All this is doing is loading in each vertex individually and one-time only. We're also setting some info in the header - and note the trap I fell into while writing this!
Here's our new Mod_LoadAliasFrame:
And our new Mod_LoadAliasGroup:
Nothing much different here aside from calling Mod_LoadFrameVerts where required. Now we need to load the model itself, and as the format has changed the old Mod_LoadAliasModel also needs a few small changes. Rather than going through it line-by-line which would take forever, I'll just give the full function:
The real changes here are that instead of filling temporary buffers with stverts and triangles we're taking them directly from the disk version. Note also that we're feeding them into GL_MakeAliasModelDisplayLists, and setting up a few extra alias header members along the way.
Speaking of GL_MakeAliasModelDisplayLists, here's the entirety of our new gl_mesh.c file:
At this stage we're definitely moving far away from the original code and getting a lot nicer. The in-memory format is now complete, so it's time to put them on-screen. This is all you need:
And that is roundabout it. If anyone has any specific questions feel free to ask.
Cleaning Up
Nothing really to be done this time, although you may wish to get rid of the defines for the old maximums.
As I mentioned, popping this data into a vertex array would be more efficient, and I'm going to post the code required for that later on.
Some true evil lives in the alias model code: the format is horrible, the loader is disgusting, the renderer is awkward to work with. Every time you use the alias model code a homeless baby puppy dog dies somewhere.
Concept
We're going to accomplish a few things here. Firstly we're going to remove all limits on the number of vertexes and triangles in an alias model (unfortunately the number of frames requires a protocol change). Secondly we're going to give ourselves a much cleaner in-memory format (the on-disk format is beyond redemption I'm afraid). Thirdly we're going to have cleaner (and therefore more maintainable) elements in the loader. Fourthly we'll be doing the same for the renderer. Fifthly we're going to save on memory by only storing a single copy of each vertex.
For the sake of clarity and keeping things short I've omitted bbox correction, anything to do with coloured light or interpolation, and shadows. It should be obvious what's needed from reading this code.
I also need to give a heads-up that this is not the most efficient way to render an alias model (although it might be more efficient than GLQuake, depending on your hardware). The most efficient way involves vertex arrays and we might get a look at that as an addendum to this tutorial at some time. There's enough going on here as it is already!
The Code
Quite a bit of code here, so let's take it one step at a time. I might comment briefly after each one if the changes made require it.
Firstly we're going to gl_model.h and replacing our entire aliashdr_t struct with these two:
Code: Select all
typedef struct aliasmesh_s
{
float s;
float t;
unsigned short vertindex;
} aliasmesh_t;
typedef struct aliashdr_s
{
vec3_t scale;
vec3_t scale_origin;
float boundingradius;
synctype_t synctype;
int flags;
float size;
int nummeshframes;
int numtris;
int numverts;
int vertsperframe;
int numframes;
int meshverts;
int vertexes;
int framevertexsize;
int skinwidth;
int skinheight;
int numskins;
int gl_texturenum[MAX_SKINS][4];
int texels[MAX_SKINS]; // only for player skins
maliasframedesc_t frames[1];
} aliashdr_t;
Code: Select all
aliashdr_t *pheader;
void Mod_LoadFrameVerts (trivertx_t *verts)
{
int i;
int mark = Hunk_LowMark ();
trivertx_t *vertexes = (trivertx_t *) Hunk_Alloc (pheader->vertsperframe * sizeof (trivertx_t));
// this should only be done once and stores an offset to the first set of verts for the frame
if (!pheader->vertexes) pheader->vertexes = (int) vertexes - (int) pheader;
for (i = 0; i < pheader->vertsperframe; i++, vertexes++, verts++)
{
vertexes->lightnormalindex = verts->lightnormalindex;
vertexes->v[0] = verts->v[0];
vertexes->v[1] = verts->v[1];
vertexes->v[2] = verts->v[2];
}
// hunk tags will mean that pheader->vertsperframe * sizeof (trivertx_t) is invalid for this
// we only need to do this once but no harm in doing it every frame
pheader->framevertexsize = Hunk_LowMark () - mark;
pheader->nummeshframes++;
}
Here's our new Mod_LoadAliasFrame:
Code: Select all
void *Mod_LoadAliasFrame (void *pin, maliasframedesc_t *frame)
{
int i;
trivertx_t *verts;
daliasframe_t *pdaliasframe;
pdaliasframe = (daliasframe_t *) pin;
strncpy (frame->name, pdaliasframe->name, 16);
frame->firstpose = pheader->nummeshframes;
frame->numposes = 1;
for (i = 0; i < 3; i++)
{
frame->bboxmin.v[i] = pdaliasframe->bboxmin.v[i];
frame->bboxmax.v[i] = pdaliasframe->bboxmax.v[i];
}
verts = (trivertx_t *) (pdaliasframe + 1);
// load the frame vertexes
Mod_LoadFrameVerts (verts);
verts += pheader->vertsperframe;
return (void *) verts;
}
Code: Select all
void *Mod_LoadAliasGroup (void *pin, maliasframedesc_t *frame)
{
daliasgroup_t *pingroup;
int i, numframes;
daliasinterval_t *pin_intervals;
void *ptemp;
pingroup = (daliasgroup_t *) pin;
numframes = LittleLong (pingroup->numframes);
frame->firstpose = pheader->nummeshframes;
frame->numposes = numframes;
for (i = 0; i < 3; i++)
{
frame->bboxmin.v[i] = pingroup->bboxmin.v[i];
frame->bboxmax.v[i] = pingroup->bboxmax.v[i];
}
pin_intervals = (daliasinterval_t *) (pingroup + 1);
frame->interval = LittleFloat (pin_intervals->interval);
pin_intervals += numframes;
ptemp = (void *) pin_intervals;
for (i = 0; i < numframes; i++)
{
Mod_LoadFrameVerts ((trivertx_t *) ((daliasframe_t *) ptemp + 1));
ptemp = (trivertx_t *) ((daliasframe_t *) ptemp + 1) + pheader->vertsperframe;
}
return ptemp;
}
Code: Select all
void Mod_LoadAliasModel (model_t *mod, void *buffer)
{
int i, j;
mdl_t *pinmodel;
stvert_t *pinstverts;
dtriangle_t *pintriangles;
int version, numframes, numskins;
int size;
daliasframetype_t *pframetype;
daliasskintype_t *pskintype;
int start, end, total;
start = Hunk_LowMark ();
pinmodel = (mdl_t *) buffer;
version = LittleLong (pinmodel->version);
if (version != ALIAS_VERSION) Sys_Error ("%s has wrong version number (%i should be %i)", mod->name, version, ALIAS_VERSION);
// allocate space for a working header
pheader = Hunk_AllocName (sizeof (aliashdr_t) + LittleLong (pinmodel->numframes) * sizeof (maliasframedesc_t), loadname);
mod->flags = LittleLong (pinmodel->flags);
mod->type = mod_alias;
// endian-adjust and copy the data, starting with the alias model header
pheader->boundingradius = LittleFloat (pinmodel->boundingradius);
pheader->numskins = LittleLong (pinmodel->numskins);
pheader->skinwidth = LittleLong (pinmodel->skinwidth);
pheader->skinheight = LittleLong (pinmodel->skinheight);
pheader->vertsperframe = LittleLong (pinmodel->numverts);
pheader->numtris = LittleLong (pinmodel->numtris);
pheader->numframes = LittleLong (pinmodel->numframes);
pheader->numverts = pheader->numtris * 3;
// validate the setup
// Sys_Error seems a little harsh here...
if (pheader->numframes < 1) Host_Error ("Mod_LoadAliasModel: Invalid # of frames: %d\n", pheader->numframes);
if (pheader->numtris <= 0) Host_Error ("model %s has no triangles", mod->name);
if (pheader->vertsperframe <= 0) Host_Error ("model %s has no vertices", mod->name);
pheader->size = LittleFloat (pinmodel->size) * ALIAS_BASE_SIZE_RATIO;
mod->synctype = (synctype_t) LittleLong (pinmodel->synctype);
mod->numframes = pheader->numframes;
for (i = 0; i < 3; i++)
{
pheader->scale[i] = LittleFloat (pinmodel->scale[i]);
pheader->scale_origin[i] = LittleFloat (pinmodel->scale_origin[i]);
}
// load the skins
pskintype = (daliasskintype_t *) &pinmodel[1];
pskintype = Mod_LoadAllSkins (pheader->numskins, pskintype);
// load base s and t vertices
pinstverts = (stvert_t *) pskintype;
for (i = 0; i < pheader->vertsperframe; i++)
{
pinstverts[i].onseam = LittleLong (pinstverts[i].onseam);
pinstverts[i].s = LittleLong (pinstverts[i].s);
pinstverts[i].t = LittleLong (pinstverts[i].t);
}
// load triangle lists
pintriangles = (dtriangle_t *) &pinstverts[pheader->vertsperframe];
for (i = 0; i < pheader->numtris; i++)
{
pintriangles[i].facesfront = LittleLong (pintriangles[i].facesfront);
pintriangles[i].vertindex[0] = LittleLong (pintriangles[i].vertindex[0]);
pintriangles[i].vertindex[1] = LittleLong (pintriangles[i].vertindex[1]);
pintriangles[i].vertindex[2] = LittleLong (pintriangles[i].vertindex[2]);
}
// load the frames
pheader->nummeshframes = 0;
pheader->vertexes = 0;
pheader->framevertexsize = 0;
pframetype = (daliasframetype_t *) &pintriangles[pheader->numtris];
for (i = 0; i < pheader->numframes; i++)
{
aliasframetype_t frametype = LittleLong (pframetype->type);
if (frametype == ALIAS_SINGLE)
pframetype = (daliasframetype_t *) Mod_LoadAliasFrame (pframetype + 1, &pheader->frames[i]);
else pframetype = (daliasframetype_t *) Mod_LoadAliasGroup (pframetype + 1, &pheader->frames[i]);
}
// FIXME: do this right
mod->mins[0] = mod->mins[1] = mod->mins[2] = -16;
mod->maxs[0] = mod->maxs[1] = mod->maxs[2] = 16;
// build the draw lists
GL_MakeAliasModelDisplayLists (pinstverts, pintriangles);
// move the complete, relocatable alias model to the cache
end = Hunk_LowMark ();
total = end - start;
Cache_Alloc (&mod->cache, total, loadname);
if (!mod->cache.data) return;
memcpy (mod->cache.data, pheader, total);
Hunk_FreeToLowMark (start);
}
Speaking of GL_MakeAliasModelDisplayLists, here's the entirety of our new gl_mesh.c file:
Code: Select all
#include "quakedef.h"
void GL_MakeAliasModelDisplayLists (stvert_t *stverts, dtriangle_t *triangles)
{
int i, j;
aliasmesh_t *mesh = (aliasmesh_t *) Hunk_Alloc (sizeof (aliasmesh_t) * pheader->numverts);
pheader->meshverts = (int) mesh - (int) pheader;
for (i = 0; i < pheader->numtris; i++)
{
for (j = 0; j < 3; j++, mesh++)
{
// emit s/t coords into the vert
mesh->s = stverts[triangles[i].vertindex[j]].s;
mesh->t = stverts[triangles[i].vertindex[j]].t;
// check for back side and adjust texcoord s
if (!triangles[i].facesfront && stverts[triangles[i].vertindex[j]].onseam) mesh->s += pheader->skinwidth / 2;
// final s and t
mesh->s = (mesh->s + 0.5) / pheader->skinwidth;
mesh->t = (mesh->t + 0.5) / pheader->skinheight;
// index into hdr->vertexes
mesh->vertindex = triangles[i].vertindex[j];
}
}
}
Code: Select all
void GL_DrawAliasFrame (aliashdr_t *paliashdr, int posenum)
{
int i;
float l;
aliasmesh_t *mesh = (aliasmesh_t *) ((byte *) paliashdr + paliashdr->meshverts);
trivertx_t *vert = (trivertx_t *) ((byte *) paliashdr + paliashdr->vertexes + paliashdr->framevertexsize * posenum);
lastposenum = posenum;
glBegin (GL_TRIANGLES);
for (i = 0; i < paliashdr->numverts; i++, mesh++)
{
trivertx_t *trivert = &vert[mesh->vertindex];
float l = shadedots[trivert->lightnormalindex] * shadelight;
glTexCoord2f (mesh->s, mesh->t);
glColor3f (l, l, l);
glVertex3f (trivert->v[0], trivert->v[1], trivert->v[2]);
}
glEnd ();
}
Cleaning Up
Nothing really to be done this time, although you may wish to get rid of the defines for the old maximums.
As I mentioned, popping this data into a vertex array would be more efficient, and I'm going to post the code required for that later on.