Skip to main content
GameDev.net gamedev.net
🔒 Locked

Putting MS3D Models in Vertex Arrays

Started by Shamino Mar 24, 2006 at 10:38 AM 16 replies 8.5k views
Original Post
Shamino
Shamino
Okay, so lately I've been rewriting my MS3D class to fit itself nicely for vertex arrays, I'm having a few issues in doing this, let me post some code..

	struct Vertex
	{
		float vx;
		float vy;
		float vz;
		float nx;
		float ny;
		float nz;
		float s;
		float t;
		unsigned char r,g,b,a;
		int BoneID;
	};

	int NumVertices;
	Vertex * Vertices;

This is the structure I will be holding the vertex information, I want to hold the xyz vertices, the xyz normals, the st tex coords, the rgba colors, and the BoneID for skeletal animation... The MS3D file structure is setup like this

// File header
struct MS3DHeader
{
	char m_ID[10];
	int m_version;
} PACK_STRUCT;

// Vertex information
struct MS3DVertex
{
	byte m_flags;
	float m_vertex[3];
	char m_boneID;
	byte m_refCount;
} PACK_STRUCT;

// Triangle information
struct MS3DTriangle
{
	word m_flags;
	word m_vertexIndices[3];
	float m_vertexNormals[3][3];
	float m_s[3], m_t[3];
	byte m_smoothingGroup;
	byte m_groupIndex;
} PACK_STRUCT;

// Material information
struct MS3DMaterial
{
    char m_name[32];
    float m_ambient[4];
    float m_diffuse[4];
    float m_specular[4];
    float m_emissive[4];
    float m_shininess;	// 0.0f - 128.0f
    float m_transparency;	// 0.0f - 1.0f
    byte m_mode;	// 0, 1, 2 is unused now
    char m_texture[128];
    char m_alphamap[128];
} PACK_STRUCT;

//	Joint information
struct MS3DJoint
{
	byte m_flags;
	char m_name[32];
	char m_parentName[32];
	float m_rotation[3];
	float m_translation[3];
	word m_numRotationKeyframes;
	word m_numTranslationKeyframes;
} PACK_STRUCT;

// Keyframe data
struct MS3DKeyframe
{
	float m_time;
	float m_parameter[3];
} PACK_STRUCT;

Right now I'm just worried about getting vertex info, normal info, and tex coord info.. I've managed to copy the vertex information successfully..

	int nVertices = *( word* )pPtr; 
	NumVertices = nVertices;
	Vertices = new Vertex[nVertices];
	pPtr += sizeof( word );


	int i;
	for ( i = 0; i < nVertices; i++ )
	{
		MS3DVertex * pVertex = ( MS3DVertex* )pPtr;
		MS3DTriangle *pTriangle = ( MS3DTriangle* )pPtr;
		Vertices.BoneID = pVertex->m_boneID;
		Vertices.vx = pVertex->m_vertex[0];
		Vertices.vy = pVertex->m_vertex[1];
		Vertices.vz = pVertex->m_vertex[2];
		pPtr += sizeof( MS3DVertex );
	}

The normals and texture coordinates are a little more iffy, they are set up per triangle, not per vertex, I've thought of how I could throw everything into that one vertex loop, but somehow I'm not sure if that will be able to happen, here is how ms3d converts the information, (using identical structures)..

	int nTriangles = *( word* )pPtr;
	NumTriangles = nTriangles;
	Triangles = new Triangle[nTriangles];
	pPtr += sizeof( word );

	for ( i = 0; i < nTriangles; i++ )
	{
		MS3DTriangle *pTriangle = ( MS3DTriangle* )pPtr;
		int vertexIndices[3] = { pTriangle->m_vertexIndices[0], pTriangle->m_vertexIndices[1], pTriangle->m_vertexIndices[2] };
		float t[3] = { 1.0f-pTriangle->m_t[0], 1.0f-pTriangle->m_t[1], 1.0f-pTriangle->m_t[2] };
		
		memcpy( Triangles.Textures1, pTriangle->m_s, sizeof( float )*3 );
		memcpy( Triangles.Textures2, t, sizeof( float )*3 );
		memcpy( Triangles.VertexIndices, vertexIndices, sizeof( int )*3 );
		pPtr += sizeof( MS3DTriangle );
	}

How am I supposed to break these things down to being copied per vertex instead of copying them per triangle? Can anyone gimme a push in the right direction?
----------------------------------------------------------Rating me down will only make me stronger.----------------------------------------------------------
Rasmadrak
Rasmadrak
write your own loader for the ms3d format, and make it load all vertexes in a row..?

"Game Maker For Life, probably never professional thou." =)
Shamino
Shamino
That is what I'm doing right now, and I'm asking for help.
----------------------------------------------------------Rating me down will only make me stronger.----------------------------------------------------------
CGameProgrammer
CGameProgrammer
It's been a while since I wrote my MS3D loader, so my memory is really fuzzy, but I think you might have to have more vertices than you think. MS3D vertices represent only unique coordinates, right? Because you can have a few triangles sharing the same vertex, but with different textures or texture coordinates at that vertex, and that means you actually need several vertices for that one location. So they'd have exactly the same position, but different texture coordinates, or normals, or textures, etc.

This sounds vaguely familiar to me so I assume it's what I did. Maybe when I get home later tonight I can see how my code handled it.
~CGameProgrammer( ); Developer Image Exchange -- New Features: Upload screenshots of your games (size is unlimited) and upload the game itself (up to 10MB). Free. No registration needed.
Shamino
Shamino
I thought the Index would handle for all the vertices that aren't re used, I didn't think I would have to actually duplicate all the skipped vertices to put them in a vertex array..
----------------------------------------------------------Rating me down will only make me stronger.----------------------------------------------------------
mandrav
mandrav
// Triangle informationstruct MS3DTriangle{	word m_flags;	word m_vertexIndices[3];	float m_vertexNormals[3][3];	float m_s[3], m_t[3];	byte m_smoothingGroup;	byte m_groupIndex;} PACK_STRUCT;


m_vertexNormals holds the normals for the three vertices of the face.

Quote:
The normals and texture coordinates are a little more iffy, they are set up per triangle, not per vertex


I 'm not sure I follow you...
Everything is per-vertex. On the contrary, if you want to have face normals, you have to calculate them yourself.

Or did I misunderstand something?

Yiannis.
Arsenal
Arsenal
CGameProgrammer is right, you do need to "unshare" the vertices because you could have two different materials sharing the same vertex and the S/T's and normals are different.

Here's what I did:

void TriangleMesh::UnshareVertices(){  int verx, trix, i;  int nTriangles;  int _numVertices = this->numVertices;  float s, t;  int material;  for (verx=0; verx<this->numVertices; verx++)  {    nTriangles = 0;    for (trix=0; trix<this->numTriangles; trix++)    {      for (i=0; i<3; i++)      {        if (verx == triangles [trix].vIndices )        {          nTriangles++;          if (nTriangles == 1)          {            arrayBone [verx] = vertices [verx].boneID;            arrayVertex [verx] = vertices [verx].v;            arrayVertexSkel [verx] = vertices [verx].vSkel;            // Save s/t and material# for later reference            s = triangles [trix].s ;            t = triangles [trix].t ;            material = groups [triangles [trix].groupIndex].materialIndex;            // Set s/t, normal and skeleton normal from triangle to this vertex array element            arrayTexCoord [(verx*2)] = triangles [trix].s ;            arrayTexCoord [(verx*2)+1] = triangles [trix].t ;            arrayNormal [verx] = triangles [trix].vNormals ;            arrayNormalSkel [verx] = triangles [trix].vNormalsSkel ;          }          else          {            if ((triangles [trix].s  != s || triangles [trix].t  != t) &&                (groups [triangles [trix].groupIndex].materialIndex == material))            {              // Create new Vertex:               // * Update vertex index for this triangle              triangles [trix].vIndices  = _numVertices;              // * Add bone ID, x/y/z, skeleton x/y/z, s/t, normal and skeletion normal to vertex array              arrayBone [_numVertices] = vertices [verx].boneID;              arrayVertex [_numVertices] = vertices [verx].v;              arrayVertexSkel [_numVertices] = vertices [verx].vSkel;              arrayTexCoord [(_numVertices*2)] = triangles [trix].s ;              arrayTexCoord [(_numVertices*2)+1] = triangles [trix].t ;              arrayNormal [_numVertices] = triangles [trix].vNormals ;              arrayNormalSkel [_numVertices] = triangles [trix].vNormalsSkel ;              // * Increment max vertex index               _numVertices++;            }          }        }      }    }  }  this->numVertices = _numVertices;}


For each group in the MS3D file, I create an array of indexes, with the size of the # of triangles in that group * 3, and fill those with the list of vertices I just created in the code above.

  for (int i=0; i<numGroups; i++)  {    groups.indexArray = new unsigned short[groups.numTriangles * 3];    for (int j=0; j<groups .numTriangles; j++)    {      TriangleMesh::Triangle *tri = &triangles[groups.triangleIndices[j]];      groups.indexArray[(j*3)] = tri->vIndices[0];      groups.indexArray[(j*3)+1] = tri->vIndices[1];      groups.indexArray[(j*3)+2] = tri->vIndices[2];    }  }


Then, before I start rendering, I:

    glTexCoordPointer(2, GL_FLOAT, 0, arrayTexCoord);    glNormalPointer(GL_FLOAT, 0, (GLfloat *)arrayNormal);    glVertexPointer(3, GL_FLOAT, 0, (GLfloat *)arrayVertex);


THEN, for each group, I render with:

    glEnableClientState(GL_TEXTURE_COORD_ARRAY);    glEnableClientState(GL_VERTEX_ARRAY);    glEnableClientState(GL_NORMAL_ARRAY);    glDrawElements(GL_TRIANGLES, groups[grpx].numTriangles * 3, GL_UNSIGNED_SHORT, groups[grpx].indexArray);    glDisableClientState(GL_NORMAL_ARRAY);    glDisableClientState(GL_VERTEX_ARRAY);    glDisableClientState(GL_TEXTURE_COORD_ARRAY);


I'm giving away all my secrets, but hope it helps :)

Are the "+" appearing fine in the code? The preview is making them disappear.
Mark Grocki
Shamino
Shamino
Thats really cool Arsenal, I'll try that code out, there are a few things I want to change though, and I'd like to know if they're possible..

I'd like to change it so normals/vertex/texcoords come out of one struct, I want to make the final rendering function centralized, I want to pass a Vertex array to the render function, and do all the stuff you said, only working out of one super vertex struct, would this be possible?
----------------------------------------------------------Rating me down will only make me stronger.----------------------------------------------------------
Arsenal
Arsenal
I think the "stride" parameter in the gl*Pointer functions will help you there. I didn't think to put them all in the same struct, but I guess that would be better for memory management.

So, if you had

typedef struct
{
float vertex[3];
float normal[3];
float st[2];
} MegaVertex;

You could say

MegaVertex *mega;
// Blah blah, allocation, initialization

glTexCoordPointer(2, GL_FLOAT, 32, (GLfloat *)mega+24);
glNormalPointer(GL_FLOAT, 32, (GLfloat *)mega+12);
glVertexPointer(3, GL_FLOAT, 32, (GLfloat *)mega);

I think my math is right. For the glTexCoordPointer, we know that the st[] array starts 24 bytes in from the start of the struct (assuming tightly packed 4 bytes per float) and the "stride" to the next item in the array is 32 bytes. For glNormalPointer, the initial value starts 12 bytes from the beginning, but the next value is 32 bytes away from that.
Mark Grocki
Shamino
Shamino
Sweet, so in that code you're creating a straight up array of vertex information?

Not worrying about breaking them down into triangles in the MS3D structure right?

Maybe I'm missing something..

What kind of index to I need to give my vertex array?

the MS3D Loading code looks like this in the beginning...

bool MS3DModel::Load(const std::string & name){	const char *ptr1 = 0;	ptr1= name.data ( );	ifstream inputFile( ptr1, ios::in | ios::binary);	if ( inputFile.fail())	{		MessageBox(NULL,"Model file not found.","Model Error",MB_OK);		return false;	// "Couldn't open the model file."	}	inputFile.seekg( 0, ios::end );	long fileSize = inputFile.tellg();	inputFile.seekg( 0, ios::beg );	byte *pBuffer = new byte[fileSize];	inputFile.read((char *)pBuffer, fileSize );	inputFile.close();	const byte *pPtr = pBuffer;	MS3DHeader *pHeader = ( MS3DHeader* )pPtr;	pPtr += sizeof( MS3DHeader );	if ( strncmp( pHeader->m_ID, "MS3D000000", 10 ) != 0 )	{		MessageBox(NULL,"Not a valid Milkshape3D model file.", "Model Error",MB_OK);		return false; // "Not a valid Milkshape3D model file."	}	if ( pHeader->m_version < 3 || pHeader->m_version > 7 )	{		MessageBox(NULL,"Not a valid Milkshape3D file version.","Model Error",MB_OK);		return false; // "Unhandled file version.  Milkshape3D Version 1.3 through 1.7 is supported." :)	}	int nVertices = *( word* )pPtr; 	NumVertices = nVertices;	Vertices = new Vertex[nVertices];	pPtr += sizeof( word );	int i;	for ( i = 0; i < nVertices; i++ )	{		MS3DVertex *pVertex = ( MS3DVertex* )pPtr;		Vertices.BoneID = pVertex->m_boneID;		memcpy( Vertices.Location, pVertex->m_vertex, sizeof( float )*3 );		pPtr += sizeof( MS3DVertex );	}	int nTriangles = *( word* )pPtr;	NumTriangles = nTriangles;	Triangles = new Triangle[nTriangles];	pPtr += sizeof( word );	for ( i = 0; i < nTriangles; i++ )	{		MS3DTriangle *pTriangle = ( MS3DTriangle* )pPtr;		int vertexIndices[3] = { pTriangle->m_vertexIndices[0], pTriangle->m_vertexIndices[1], pTriangle->m_vertexIndices[2] };		float t[3] = { 1.0f-pTriangle->m_t[0], 1.0f-pTriangle->m_t[1], 1.0f-pTriangle->m_t[2] };		memcpy( Triangles.VertexNormals, pTriangle->m_vertexNormals, sizeof( float )*3*3 );		memcpy( Triangles.Textures1, pTriangle->m_s, sizeof( float )*3 );		memcpy( Triangles.Textures2, t, sizeof( float )*3 );		memcpy( Triangles.VertexIndices, vertexIndices, sizeof( int )*3 );		pPtr += sizeof( MS3DTriangle );	}	int nGroups = *( word* )pPtr;	NumMeshes = nGroups;	Meshes = new Mesh[nGroups];	pPtr += sizeof( word );	for ( i = 0; i < nGroups; i++ )	{		pPtr += sizeof( byte );	// flags		pPtr += 32;				// name		word nTriangles = *( word* )pPtr;		pPtr += sizeof( word );		int *pTriangleIndices = new int[nTriangles];		for ( int j = 0; j < nTriangles; j++ )		{			pTriangleIndices[j] = *( word* )pPtr;			pPtr += sizeof( word );		}		char materialIndex = *( char* )pPtr;		pPtr += sizeof( char );			Meshes.MaterialIndex = materialIndex;		Meshes.NumTriangles = nTriangles;		Meshes.TriangleIndices = pTriangleIndices;	}	int nMaterials = *( word* )pPtr;	NumMaterials = nMaterials;	Materials = new Material[nMaterials];	pPtr += sizeof( word );	for ( i = 0; i < nMaterials; i++ )	{		MS3DMaterial *pMaterial = ( MS3DMaterial* )pPtr;		memcpy( Materials.Ambient, pMaterial->m_ambient, sizeof( float )*4 );		memcpy( Materials.Diffuse, pMaterial->m_diffuse, sizeof( float )*4 );		memcpy( Materials.Specular, pMaterial->m_specular, sizeof( float )*4 );		memcpy( Materials.Emissive, pMaterial->m_emissive, sizeof( float )*4 );		Materials.Shininess = pMaterial->m_shininess;		Materials.TextureFilename = new char[strlen( pMaterial->m_texture )+1];		strcpy( Materials.TextureFilename, pMaterial->m_texture );		pPtr += sizeof( MS3DMaterial );	}


I'm under the impression that you have to arrange everything in that way, but I know this can't be the case..

I don't care about defining triangles, the vertex array can pick out triangles with an index right? This is so weird, I'm not sure how to convert the normals (which are created for every triangle), as well as tex coords, to be created per vertex??

Am I thinking wrong, missing something?
----------------------------------------------------------Rating me down will only make me stronger.----------------------------------------------------------
Arsenal
Arsenal
Those big arrays (or the MegaVertex) holds all the information you need to draw all the groups. For each group, you have an array of indices that points to the vertices within the big array. You pass THAT array into glDrawElements for each group to render. This array is populated in my "UnshareVertices()" method.

So, after setting all your vertex pointers, like in the previous post, you then:

  // Render by group  for (int grpx=0; grpx<numGroups; grpx++)  {    int matx = groups[grpx].materialIndex;    if (matx != -1)    {      glColor4fv(materials[matx].diffuse);      if (materials[matx].decalmap)      {        Texture *tex = (Texture *)materials[matx].decalmap;        tex->Select();      }      else        glDisable(GL_TEXTURE_2D);    }    glEnableClientState(GL_TEXTURE_COORD_ARRAY);    glEnableClientState(GL_VERTEX_ARRAY);    glEnableClientState(GL_NORMAL_ARRAY);    glDrawElements(GL_TRIANGLES, groups[grpx].numTriangles * 3, GL_UNSIGNED_SHORT, groups[grpx].indexArray);    glDisableClientState(GL_NORMAL_ARRAY);    glDisableClientState(GL_VERTEX_ARRAY);    glDisableClientState(GL_TEXTURE_COORD_ARRAY);    // Deselect texture, if any    if (matx != -1)      if (materials[matx].decalmap)      {        Texture *tex = (Texture *)materials[matx].decalmap;        tex->Deselect();      }  }


You probably don't have to enable and disable the arrays for each group, though.

---

As far as your question about what index you gave the vertex array, look at my second block of code from my first post. What I do is create a big array for each group, basically a list of vertex indices with size "# of triangles in group times 3", and then populate that array with vertex index information from each triangle (which I get from the MS3D file and later modify in UnshareVertices). What glDrawElements will do is take the first 3 elements of that array, reference the vertex pointers to find the right vertices, and render a triangle. Then it goes to the next 3, and so on.
Mark Grocki
Shamino
Shamino
Oh I see, when we unshare vertices, we're creating an array of vertices (as well as creating more vertices we need to draw the models with the VA)..

Then we create an index for every group..

After that, in our rendering function...

for every group
drawelements (group index)

----------------------------------------------------------Rating me down will only make me stronger.----------------------------------------------------------
Arsenal
Arsenal
Yep, that's it. I'm sure there are different ways to manage the large structures, how they're created and such, but that's the idea.
Mark Grocki
Shamino
Shamino
Thanks alot, so just doing this:

	int nVertices = *( word* )pPtr; 	NumVertices = nVertices;	Vertices = new Vertex[nVertices];	pPtr += sizeof( word );	int i;	for ( i = 0; i < nVertices; i++ )	{		MS3DVertex * pVertex = ( MS3DVertex* )pPtr;	//	Vertices.BoneID = pVertex->m_boneID;		memcpy( Vertices.vx, pVertex->m_vertex[0], sizeof(float));		memcpy( Vertices.vy, pVertex->m_vertex[1], sizeof(float));		memcpy( Vertices.vz, pVertex->m_vertex[2], sizeof(float));		pPtr += sizeof( MS3DVertex );	}


Isn't creating enough vertices to draw the model?

Never woulda guessed...
----------------------------------------------------------Rating me down will only make me stronger.----------------------------------------------------------
Arsenal
Arsenal
It is enough, yes... but it's not enough if you want correct lighting and texturing using vertex arrays.
Mark Grocki
Shamino
Shamino
Ahh, I see..

Who would have though switching from the ms3d file def to my own would be such a hassle, I gotta like, completely rewrite the loader..

NeHe should really cover this topic :d
----------------------------------------------------------Rating me down will only make me stronger.----------------------------------------------------------
CGameProgrammer
CGameProgrammer
Yeah it's kind of pointless for MS3D to store only unique vertex positions. That kind of thing is useful for a software renderer since it can greatly reduce the amount of transformations to be done, but there's no way to get 3D cards to take advantage of something like that; Direct3D and OpenGL don't have such mechanisms in place, as far as I'm aware. It probably would be an extremely tiny improvement anyway, since matrix multiplication is very fast on any hardware T&L card.
~CGameProgrammer( ); Developer Image Exchange -- New Features: Upload screenshots of your games (size is unlimited) and upload the game itself (up to 10MB). Free. No registration needed.

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.