Skip to content

5) Sample library tutorial

Buu342 edited this page Jun 7, 2024 · 32 revisions

This page contains a small tutorial on how to use the sample library. It assumes you have already converted your model to a format the library can use with a parsing tool, such as Arabiki.

Useful prerequisite knowledge

The parser makes use of a bunch of data structs, of note are the s64ModelData type and the s64ModelHelper type. The former contains the mesh and animation data for each model, and should be treated as read only. The latter is a helper struct that contains information that is relevant to each active model in the scene. If you wish to have lots of different objects, each with a copy of the model+animation but working completely independently from each other, then you should have a s64ModelHelper for each object (which share the same s64ModelData).

A detailed description of the struct is as follows:

typedef struct {
    u8 interpolate;                // Boolean value that describes frame interpolation behaviour
    u8 loop;                       // Boolean value that describes animation looping behaviour
    u32 rendercount;               // Internal render counter. Don't touch!
    Mtx* matrix;                   // An array of matricies, one for each mesh object (Libultra only)
    u8   (*predraw)(u16);          // The pre draw function
    void (*postdraw)(u16);         // The post draw function
    void (*animcallback)(u16);     // The animation callback function
    s64ModelData* mdldata;         // The model data
    s64FrameTransform* transforms; // An array with the final transforms of each mesh for the current animation
    s64AnimPlay curanim;           // The current animation playback
    s64AnimPlay blendanim;         // The blending animation playback
    f32 blendticks;                // The amount of ticks to blend the animation over
    f32 blendticks_left;           // The amount of ticks left to do the blending
} s64ModelHelper;

To make accessing data as fast as possible, the parser will output macros with numbers that represent different meshes or animations. This is because the data is stored in arrays to make iteration and acessing much faster. The macros are formatted like this:

// Note, X is the name of the model, Y is the name of the mesh/bone, and Z is the animation name. 
#define MODEL_X       // A pointer to a model
#define MESH_X_Y      // A mesh index (example: s64ModelData->meshes[i])
#define ANIMATION_X_Z // An animation index (example: s64ModelData->anims[i])

A macro named MESHCOUNT_X and ANIMATIONCOUNT_X is also generated that tells you the size of each modeldata's meshes and anims array without needing to spend CPU time accessing the pointers for s64ModelData->meshcount and s64ModelData->animcount respectively.

Basic library usage

Sausage64 models come in two flavours: Static and Binary. Static models are typically generated as C structs into a .h file, while Binary models are stored in a custom binary format. By default, Arabiki will generate Binary models, along with a helper header file which you can use in your project if you want.

Binary Models

To include binary models in your project, you need to do the following:

  • Libultra - Add the model binary to the spec file, with the segment name matching your model name from Arabiki. If you wish to give a different segment name, you need to change the extern definitions in the exported header file.
  • Libultra (Modern SDK) - Generate an assembly file with your asset, and then include it in the linker script. The glabel should match the extern definition in the exported header file.
  • Libdragon - Simply include the model in your filesystem like any other asset.

To load the model from ROM, simply call the sausage64_load_binarymodel function to load it into RAM. You can unload models by calling sausage64_unload_binarymodel. You need to provide a list of textures to use during the model loading, the order of which is described in the exported header file.

Static Models

With Libultra, static models are initialized from the get-go, so you can skip to the next section of this chapter. Libdragon users unfortunately need to do a little bit more work.

In order to use your model in Libdragon, you must first initialize all of its textures using sausage64_load_texture. You only need to do this for textured materials, not for primitive colors. Afterwards, you can call sausage64_load_staticmodel to generate a static display list for your model.

Once textured materials are loaded and the static model is generated, you are good to go.

If you wish to free the memory used by the GL textures or the display list, simply call sausage64_unload_texture and sausage64_unload_staticmodel respectively.

Simple models without the Model Helper

If your model does not have any animations, Arabiki will generate simplified structures without a s64ModelHelper struct (since there is no need for them without animations). To render these models in Libultra, simply call gSPDisplayList. With Libdragon, use glCallList with the ID being s64Mesh.dl->guid_mdl.

Using the Model Helper

Let's say you want to have a player character that uses a model+animation set called MyCharacter (which will probably have a macro along the lines of MODEL_MyCharacter in the header file generated by the parser). For this player object, you'd allocate memory for a s64ModelHelper struct, and then you'd initialize it properly by calling sausage64_initmodel like so:

s64ModelHelper* modeldata;

void create_playerobject()
{
    modeldata = sausage64_initmodel(MODEL_MyCharacter);
    
    // Initialize the rest of the player 
}

⚠️ Libultra users: sausage64_initmodel uses malloc internally. Be sure that your heap is initialized with InitHeap at your ROM's boot stage.

The default animation is set to the one who's ANIMATION_X_Z index is 0, so if you want it to be set to something else it's probably a good idea to call sausage64_set_anim after initializing the model. Example, with an animation called Idle:

sausage64_set_anim(modeldata, ANIMATION_MyModel_Idle);

Of course, any time you want to change an animation, you call this function again. To actually make the animation advance, however, you must call the sausage64_advance_anim function as often as possible (like in your update or game tick function). The second argument of this function is the tick to advance the animation by (the larger the number, the faster it animates forwards/backwards).

If you want to have multiple player objects using the same model+animation set, but operating individually from one another, you'd allocate a different s64ModelHelper for each object (but obviously, you give it the same MODEL_X macro in the s64ModelData argument of sausage64_initmodel).

Drawing the model, and draw callbacks

To draw the model, simply call the sausage64_drawmodel function, which takes the display list pointer and the model helper:

sausage64_drawmodel(glistp, modeldata);

⚠️ Libdragon users: you omit the glistp argument as OpenGL stores this information internally.

Sometimes, you'll want to perform dynamic operations on models before or after they are drawn. For instance, you might want to load a different face texture depending on the context. For these situations, you can provide a pre draw and post draw function to the model helper struct (using the sausage64_set_predrawfunc and sausage64_set_postdrawfunc functions respectively). These functions take a single argument, which is the current mesh index that is being drawn (hint, use the MESH_X_Y macro for this). Here's an example:

u8 handleplayerface(u16 meshpart)
{
    if (meshpart == MESH_MyCharacter_Head)
    {
        #ifndef LIBDRAGON
            gDPLoadTextureBlock(...); // Load a texture in Libultra
        #else
            sausage64_loadmaterial(...); // Load a texture in Libdragon
        #endif
    }
    return 1; // You can return 0 to disable drawing of a specific meshpart
}

void create_playerobject()
{
    modeldata = sausage64_initmodel(MODEL_MyCharacter);
    sausage64_set_anim(modeldata, ANIMATION_MyModel_Idle);
    
    // Set the pre draw function to the above one
    sausage64_set_predrawfunc(modeldata, handleplayerface);
}

void update_playerobject()
{
    sausage64_advance_anim(modeldata, 1.0);
}

void draw_playerobject()
{
    sausage64_drawmodel(glistp, modeldata);
    // Now, before drawing any mesh, it'll call the handleplayerface function. 
    // If it's going to load the head mesh next, it'll load the face texture first
}

For this to work, of course, you'll need to make sure that the display list for Head does not load another face texture afterwards (hence overwriting our custom load). In the parser program, you can prevent this texture load from happening by appending a texture definition with the DONTLOAD flag.

If you wish to manipulate the mesh's positional/rotational data, this is covered in Manipulating the model at runtime.

To free the memory used by the s64ModelHelper struct, simply call sausage64_freehelper.

Animation callbacks

Similar to drawing callbacks, you can attach a callback function that executes when the animation finishes. This is useful, for instance, if you want to go back to the idle animation/state when a character finishes their attack animation. Similar to the predraw and postdraw callbacks, the function takes a single argument (which is the currently playing ANIMATION_X_Z value).

Example usage:

void handleanimfinish(u16 anim)
{
    if (anim == ANIMATION_MyCharacter_Attack1)
        sausage64_set_anim(modeldata, ANIMATION_MyModel_Idle);
}

void create_playerobject()
{
    modeldata = sausage64_initmodel(MODEL_MyCharacter);
    sausage64_set_anim(modeldata, ANIMATION_MyModel_Attack1);
    
    // Set the animation callback function to the above one
    sausage64_set_animcallback(modeldata, handleanimfinish);
}

void update_playerobject()
{
    sausage64_advance_anim(modeldata, 1.0);
}

Billboarding

Sausage64 supports billboarding, but it is a little tricky to setup. This is due to me attempting to keep the billboarding math as lightweight as possible.

Blender side

On your mesh, you need the flat of the billboard surface (AKA what you want to face the camera) to be with its front pointing to the positive Z axis. You should have the root of the bone on the center of the billboard (as that's the center of rotation).

Next, to mark the bone as a billboard, select the bone in Pose Mode, go to the bone tab, and scroll to the bottom. You should see a section called "Custom Properties". Add a new custom property and name it Billboard, its value doesn't matter, only the name. Your bone will now be marked as a billboard on export.

During animation, you are free to rotate the billboard bone to face the camera to help you visualize what is going on, just be aware that rotations will be ignored in the ROM (since it's going to always face the camera).

ROM side

All you need to do is to call the following function in your scene draw code:

sausage64_set_camera(view, projection);

Where view is your view matrix and projection is your projection matrix.

Once you've done that, you're all set! Sausage64 will automatically billboard any bones which were marked as such.

Manipulating the model at runtime

It is possible to manipulate the meshes of the model at runtime for custom effects, such as jiggle physics or IK. However, to do this, it is important to understand some design decisions about the library.

For a bunch of optimization reasons, despite Sausage64 using a hierarchical modeling system, the actual implementation doesn't. Every single animation value (position, rotation, scale) is stored in model space relative to the root of the model, as opposed to being an offset of the previous bone in the chain. Furthermore, the meshes themselves are not stored in any hierarchical order, they are stored in whatever order the parser deemed optimal.

Before drawing, Sausage64 calculates the position of all the meshes for a given animation and stores them in the transforms array of the model helper. This step is performed in sausage64_drawmodel or in sausage64_get_meshtransform. To prevent the calculation from being done multiple times, the transform value of the helper struct contains a counter value that only increments when sausage64_drawmodel is called. In this example, we are going to translate the mesh with bone SomeBone on the X axis by 100 units.

s64Transform* mesh_trans;

// mesh_trans contains a pointer to the transform data of MESH_SomeBone
// Calling it causes the transform data to be calculated for the given frame
mesh_trans = sausage64_get_meshtransform(modeldata, MESH_SomeBone);

// Perform manipulations on mesh_trans
mesh_trans->pos[0] += 100;

// When drawing, the X value of mesh 0's translation has been moved by 100 units.
sausage64_drawmodel(modeldata);

// If you call sausage64_get_meshtransform on modeldata now, it will increment the internal counter. Don't do this unless you know what you're doing!

As was highlighted previously, the transforms are not applied hierarchically, so if you wish to also apply the manipulations on the children, you need to recursively iterate through them and manually calculate the new positions based on the previous one:

for (int i=0; i<modeldata->mdldata->meshcount; i++)
{
    if (modeldata->mdldata->meshes[i].parent == MESH_SomeBone)
    {
        s64Transform* child_trans = sausage64_get_meshtransform(modeldata, i);
        child_trans->pos[0] += 100;
        // If this child also has children, you might wanna do this recursively instead of using a for loop
        // Obviously, for rotations and scaling, these calculations are much trickier!
    }
}

You need to do this iteratively for each bone in the chain.

The s64Transform structure is defined as:

typedef struct {
    f32 pos[3];   // X Y Z position
    f32 rot[4];   // W X Y Z Quaternion
    f32 scale[3]; // X Y Z scale
} s64Transform;

⚠️ You might ask, "is it possible to perform the manipulations in a pre-draw function?". The answer is yes, but you must be very careful because if want to manipulate the transforms for the children nodes, it might not work. The pre-draw function runs just before the actual rendering of the mesh is performed, remember that there is no guarantee that the children meshes haven't already been rendered by the time you modify the parent!

LookAt

Sausage64 provides the sausage64_lookat function to allow the user to make meshes "lookat" something else. For instance, this can be used to make a character's head turn to face an object in the world. There are some caveats regarding how the mesh data is stored, so it is recommended that you read through the Manipulating the model at runtime section first.

The Libultra and Libdragon sample ROM provides a pretty extensively documented example of how to perform the lookat for a character, shown here:

float w;
s64Transform* headtrans;
float targetpos[3], targetdir[3];
float eyepos[3] = {0, -25.27329f, 18.116f}; // This is the eye position, offset from the head mesh's root

// First, we need the head's transform in model space
headtrans = sausage64_get_meshtransform(catherine, MESH_Catherine_Head);

// Now we can calculate the eye's position from the model's space
// To make the code simpler, I am ignoring rotation and scaling of the head's transform
// If you want to take the head's rotation into account, check the s64vec_rotate function in sausage64.c
eyepos[0] = headtrans->pos[0] + eyepos[0];
eyepos[1] = headtrans->pos[1] + eyepos[1];
eyepos[2] = headtrans->pos[2] + eyepos[2];

// Take the camera's position in world space and convert it to the model's space
// In the sample ROM, the model is always at (0,0,0), so nothing magical here :P
targetpos[0] = campos[0] - 0;
targetpos[1] = campos[1] - 0;
targetpos[2] = campos[2] - 0;

// Calculate the direction vector and normalize it
targetdir[0] = targetpos[0] - eyepos[0];
targetdir[1] = targetpos[1] - eyepos[1];
targetdir[2] = targetpos[2] - eyepos[2];
w = 1/sqrtf(targetdir[0]*targetdir[0] + targetdir[1]*targetdir[1] + targetdir[2]*targetdir[2]);
targetdir[0] *= w;
targetdir[1] *= w;
targetdir[2] *= w;

// Put a limit on how much Catherine can turn her head
// I'm just gonna check the angle between the target direction and the forward axis ((0, -1, 0) in Catherine's case)
// instead of doing it properly by limiting pitch yaw and roll specifically.
// Remember, if the head mesh rotates, the axis to check against must be properly calculated. 
w = 1/((targetdir[0]*targetdir[0] + targetdir[1]*targetdir[1] + targetdir[2]*targetdir[2]) * (0*0 + (-1)*(-1) + 0*0));
w = (targetdir[0]*0 + targetdir[1]*(-1) + targetdir[2]*0)*w;
lookat_canseecam = (w >= 0.6);

// Perform the lookat
sausage64_lookat(catherine, MESH_Catherine_Head, targetdir, lookat_amount, true);