gbadev.org forum archive

This is a read-only mirror of the content originally found on forum.gbadev.org (now offline), salvaged from Wayback machine copies. A new forum can be found here.

DS development > Model format

#79722 - tciny - Sat Apr 15, 2006 5:05 pm

Hey,

I'm planning to create a model format for use on the DS that has a proper Maya exporter, nice importer and so forth...
I want the format to support skeletal animation for characters and the only thing I'm not sure of yet is wether to support soft skinning or not. (Meaning one vertex can be influenced by multiple joints).
The difference would be that with rigid binding it'd be possible to use the glTranslate and glRotate commands the devkit provides wheres soft skinning would mean that I'd have to do the matrix calculations in software.

Long story short: Does the DS handle matrix manipulation like in glTranslate and glRotate in hardware or is it software emulated anyway?!

#79723 - Mighty Max - Sat Apr 15, 2006 5:07 pm

Take a look here: http://www.bottledlight.com/ds/index.php/Video/GeometryEngine
_________________
GBAMP Multiboot

#79726 - tepples - Sat Apr 15, 2006 5:19 pm

Pretty much, you'll want to export to DS display list format. A lot of research into this format went into model viewers for the dump of Metroid Prime Hunters: First Hunt.
_________________
-- Where is he?
-- Who?
-- You know, the human.
-- I think he moved to Tilwick.

#79729 - tciny - Sat Apr 15, 2006 5:27 pm

Thanks for your quick replys.
Would you happen have a like for some docs on that display list format? :)

#79946 - Chris Holmes - Mon Apr 17, 2006 1:40 pm

Why don't you use the Maya GE2 exporter? It's not enabled by default, but it has been included since about Maya 2. GE2 is a _very_ simple to use/edit format as it's plain-text. If you need a parser for that, I've already got one written.

Chris

#80073 - tciny - Tue Apr 18, 2006 5:07 pm

Because I'd like the files to stay as small as possible. Plain text has the problem of being slow to parse and wastful in terms of storage...

I havent done any such low level proramming yet and I'm having a trouble figuring out what to do with the CPU registers listed in the docs. Could anyone please explain this to me real quick? :)

#80082 - ecurtz - Tue Apr 18, 2006 5:49 pm

tciny wrote:
I havent done any such low level proramming yet and I'm having a trouble figuring out what to do with the CPU registers listed in the docs. Could anyone please explain this to me real quick? :)


The registers correspond to the 3D commands that the DS is able to execute. So for example, to set the model transform matrix you would write to the MATRIX_SET (or whatever it is) 16 times for the values in your matrix. If you're not an experienced programmer trying to write an exporter is going to be NO FUN. If you're willing to wait I have one for the open source Misfit Model 3d program that is at about 80% functionality (hierarchical rotation keyframes are fubared, most other basic stuff works.)

#80084 - tciny - Tue Apr 18, 2006 6:33 pm

I've written several maya exporters, modeling tools, opengl apps etc. thats no problem. It's just that I have a rather artistic background so docs with only memory locations in them intimidate me ;)

My problem is I dont quite get yet how to assign something to a register... should I just assign a value to it?! Like
vfixed *blah = new vfixed();
*blah = 5;
MATRIX_MULT4x4 = blah;

#80107 - ecurtz - Tue Apr 18, 2006 8:58 pm

Yeah, pretty much, take a look at the methods in lidnds...

videoGL.c

Once you get the stuff figured out you can then compile all the commands into a stream and copy it all to a single register, that's the final format that Metroid et al. use. That's essentially an id for 4 operations crammed into a int32 followed by the data for those operations.

#80272 - tciny - Thu Apr 20, 2006 10:34 pm

Thanks! Works great :)

What bothers me tho, is that I cant read the current matrix :/
Is there a way to do this?

#80295 - ecurtz - Fri Apr 21, 2006 3:18 am

Code:
void glGetFixed(GL_GET_TYPE param, fixed* f) {
//---------------------------------------------------------------------------------
  int i;

  switch (param) {
    case GL_GET_MATRIX_ROTATION:
      for(i = 0; i < 9; i++)
        f[i] = MATRIX_READ_ROTATION[i];
      break;
    case GL_GET_MATRIX_PROJECTION:
      for(i = 0; i < 16; i++)
        f[i] = MATRIX_READ_PROJECTION[i];
      break;
    default:
      break;
  }
}


I'm not sure if these work, but have no reason to suspect they don't.

#80321 - tciny - Fri Apr 21, 2006 9:48 am

Just so I get this right:
MATRIX_READ_ROTATION will actually read the currently active world matrix? Because the "read rotation" makes it seem like the name was picked really odd...

#80585 - ecurtz - Sun Apr 23, 2006 4:57 pm

By the way this is what my current animation structures look like. They are still very much work in progress. Metroid was using Euler angles for its rotations (at least in base pose) so maybe that's a better way to go, I'm sure they thought about performance more than I have. One obvious change is that palette data and texture data should be split so that you can use different combinations.

Code:

typedef struct
{
   int parent;
   quat rot_quat;
   vector trans_vec;
} __attribute__((packed))
ModelBone;

typedef struct
{
   short texid;
   short palid;
   int   start_ofs;
   int   size;
} __attribute__((packed))
ModelObject;

typedef struct
{
   unsigned short frame;
   quat16 rot_quat;
} __attribute__((packed))
RotFrame;

typedef struct
{
   unsigned short frame;
   vector16 trans_vec;
} __attribute__((packed))
TransFrame;

typedef struct
{
   unsigned int frames;
   RotFrame*  rot_ofs;
   TransFrame*   trans_ofs;
   int*   scale_ofs;
}
ModelAnim;

typedef struct
{
   unsigned short      format;
   unsigned short      width;
   unsigned short      height;
   unsigned short      palette_cnt;
   unsigned int      palette_ofs;
   unsigned int      data_ofs;
}
PNGImage;

typedef struct
{
   int bone_cnt;
   ModelBone* bone_ofs;
   int object_cnt;
   ModelObject* object_ofs;
   int anim_cnt;
   ModelAnim* anim_ofs;
}
ModelHeader;

#80693 - tciny - Mon Apr 24, 2006 9:06 pm

Just some thoughts I had while reading:

In ModelBone, I think for the parent even a byte would be enough seeing how performance is... :)

Also, when you rig a model properly you dont need an X,Y and Z component for translation but only a distance, which is always measured along X. So one float/short/... would be sufficent here.

As for the keyframes: It depends on what you want to achieve, but generally I'd say squash n stretch isnt something you'll need very often so I think you dont need the translate keyframes... or at least give them an own array of offsets so you dont need to store them for every rotation keyframe.

Its nice to see other people had the same idea; sharing thoughts about how to implement things can be really helpful imho.

Am I correct when I read from your code that each body part is a single mesh? Because what I'd like to do is transform a single mesh on a vertex basis. It makes the rendering faster because there arent as much intersections between polys and it also looks a lot better...
Right now the only problem I have is the geo engine of the DS and how matrix operations are handeled.
What I intended to do was:

Give every bone a worldMatrix attribute.
Now for each frame:

Iterate over all joints and calculate their woldMatrices. So if you calculated joint Nr. 4 and it has Nr. 2 as parent you'd grab the matrix of #2, add your deformations and store that as worldMatrix for #4. etc.

After all bones have had their worldMatrix refreshed, iterate over all vertices and store their new position in a buffer for drawing later on. (this unfortunately cant be done immediately). If a vertex only has one bone as influence that transform it using that matrix. If it has multiple influences compute the solutions for all bones and then blend them respectively.

After all vertices have been transformed use the vertex buffer to draw the mesh, then free the memory.


The problem I'm having is retrieving the current matrix and transforming single vertices with a given matrix without drawing them... I dont want to apply the projection matrix at that point.

#80746 - ecurtz - Tue Apr 25, 2006 4:48 am

There's a 32 entry matrix stack, so you can do glRestoreMatrix() to get the parent transform for a joint back into the current matrix.

I'm not really a 3d guy, so I'll have to think about your translation comment - don't we need a full translation to offset the "beginning" of the bone from its parent?

The translation keyframes are already separated from the rotation ones on the theory that they'll be much rarer.

Each body is indeed a single mesh, the matrix stack means that the whole thing can be submitted in the condensed dlist form including switching the matrix on the fly. I'm pretty sure trying to do weighted vertices would totally kill your performance.

There's no reason to cut down the parent to a byte unless you want to use the other byte for flags or something. The rest of the fields need to get aligned for the reads anyway.

Here's the current (rough) demo code.

Code:

int DrawGLScene()                                 // Here's Where We Do All The Drawing
{
   if (modelPtr->bone_cnt > 0) {
      ModelBone* bonePtr = modelPtr->bone_ofs;
      for (int b = 0; b < modelPtr->bone_cnt; b++) {
         if ( bonePtr[b].parent >= 0 ) {
            glRestoreMatrix(bonePtr[b].parent);
         } else {
            setBaseMatrix();
         }   

         if (modelPtr->anim_cnt > 0) {
            ModelAnim* animPtr = modelPtr->anim_ofs;
           
            int currentFrame = animFrame >> 2;
            while (currentFrame >= animPtr->frames)
               currentFrame -= animPtr->frames;
           
            // Calculate the rotation
            if (animPtr->rot_ofs) {
               RotFrame* rotPtr = &animPtr->rot_ofs[b];
               // First frame has marker index rather than frame (which is 0)
               if (rotPtr->frame) {
                  // Not normalizing portions because of the quat normalize
                  int portion_prev;
                  int portion_next;
                  RotFrame* nextPtr = &animPtr->rot_ofs[rotPtr->frame];
                  if (nextPtr->frame > currentFrame) {
                     portion_prev = nextPtr->frame - currentFrame;
                     portion_next = currentFrame;
                  }
                  else {
                     do {
                        nextPtr++;
                     } while (nextPtr->frame < currentFrame);
                     rotPtr = nextPtr--;
                     
                     portion_prev = nextPtr->frame - currentFrame;
                     portion_next = currentFrame - rotPtr->frame;
                  }
                 
                  quat interp_quat;
                  interp_quat.w = (rotPtr->rot_quat.w * portion_prev) + (nextPtr->rot_quat.w * portion_next);
                  interp_quat.x = (rotPtr->rot_quat.x * portion_prev) + (nextPtr->rot_quat.x * portion_next);
                  interp_quat.y = (rotPtr->rot_quat.y * portion_prev) + (nextPtr->rot_quat.y * portion_next);
                  interp_quat.z = (rotPtr->rot_quat.z * portion_prev) + (nextPtr->rot_quat.z * portion_next);
                  quat_normalize(&interp_quat);
                 
                  glMultQuatAsM4x3(&interp_quat);
               }
               else {
                  glMultQuatAsM4x3(&rotPtr->rot_quat);
               }
            }
            else {
               glMultQuatAsM4x3(&bonePtr[b].rot_quat);
            }
           
            // Calculate the translation
            if (animPtr->trans_ofs) {
               TransFrame* transPtr = &animPtr->trans_ofs[b];
               // First frame has marker index rather than frame (which is 0)
               if (transPtr->frame) {
                  f32 portion_prev;
                  f32 portion_next;
                  TransFrame* nextPtr = &animPtr->trans_ofs[transPtr->frame];
                  if (nextPtr->frame > currentFrame) {
                     f32 inv_length = divf32(f32one, inttof32(nextPtr->frame));
                     portion_prev = (nextPtr->frame - currentFrame) * inv_length;
                     portion_next = currentFrame * inv_length;
                  }
                  else {
                     do {
                        nextPtr++;
                     } while (nextPtr->frame < currentFrame);
                     transPtr = nextPtr--;
                     
                     f32 inv_length = divf32(f32one, inttof32(nextPtr->frame - transPtr->frame));
                     portion_prev = (nextPtr->frame - currentFrame) * inv_length;
                     portion_next = (currentFrame - transPtr->frame) * inv_length;
                  }
                 
                  vector interp_vec;
                  interp_vec.x = mulf32(transPtr->trans_vec.x, portion_prev) + mulf32(nextPtr->trans_vec.x, portion_next);
                  interp_vec.y = mulf32(transPtr->trans_vec.y, portion_prev) + mulf32(nextPtr->trans_vec.y, portion_next);
                  interp_vec.z = mulf32(transPtr->trans_vec.z, portion_prev) + mulf32(nextPtr->trans_vec.z, portion_next);

                  // Using glMultQuatAsM4x3 above, complete the matrix here
                  MATRIX_MULT4x3 = interp_vec.x;
                  MATRIX_MULT4x3 = interp_vec.y;
                  MATRIX_MULT4x3 = interp_vec.z;
               }
               else {
                  MATRIX_MULT4x3 = transPtr->trans_vec.x;
                  MATRIX_MULT4x3 = transPtr->trans_vec.y;
                  MATRIX_MULT4x3 = transPtr->trans_vec.z;
               }
            }
            else {
               MATRIX_MULT4x3 = bonePtr[b].trans_vec.x;
               MATRIX_MULT4x3 = bonePtr[b].trans_vec.y;
               MATRIX_MULT4x3 = bonePtr[b].trans_vec.z;
            }
            /*
            if (animPtr->scale_ofs) {
            }
            */
         }
         else {
            glMultQuat(&bonePtr[b].rot_quat);
            glTranslatev((GLvector*) &bonePtr[b].trans_vec);
         }
         
         glStoreMatrix(b);
      }
   }
   else {
      setBaseMatrix();
      glPushMatrix();
   }

   ModelObject* objPtr = modelPtr->object_ofs;
   for (int obj = 0; obj < modelPtr->object_cnt; obj++ ) {
      glBindTexture( GL_TEXTURE_2D, texture[objPtr[obj].texid] );
      glColorTable( GL_RGB, palette[objPtr[obj].palid] );

      int count = objPtr[obj].size;
      int* dlist = (int*)((byte*) modelPtr + objPtr[obj].start_ofs);
     
      //glCallList((u32*) objPtr[obj]->dlist_ofs);
      while(count--)
         GFX_FIFO = *dlist++;
   }

   return TRUE;         
}

#81479 - tciny - Sat Apr 29, 2006 1:10 pm

Thanks for posting the code, its an interesting read.

About the translation: If you want to support "dirty" rigs, then yes, you'll need all translates. But if the character was set up properly it doesnt have any y and z translantes on any of the bones. Its like giving a normal vector (rotation) and the distance (x translate).

I'll do some benchmarking once I have some time on my hands (currently writing my diploma thesis)... but from what I've read so far I think performance should be ok... I dont expect it to be as fast as an object based system, but when you really want to show high-res characters its worth it... I hope :)
I'm thinking about switching between smooth-binding and per object binding on a LOD basis...

#86901 - Valmond - Sat Jun 10, 2006 10:20 am

Hi !

ecurtz pointed me to this discussion, and it's indeed an interesting read.
(thanks !)

what I have understood is that any kind of skinning should be done
along these lines :

Calculate (as usual) the matrices for the bones
Draw (with a displaylist probably) all Triangles who are affected by only one bone
Calculate and draw the other ones (multiple bones per tri, or even multiple bones per vertex) "by hand".

In the "by hand" part, vertexes could be stored sorted "by bone" and easily get
calculated (especially if not softskinning, ie. weight always is 1.0) by
the geometric engine (couldn't you do geometric-lists ?) as the matrices
are already calculated, then flush triangles (by hand again :) with these vertexes.


Right ?

/Valmond