-
Notifications
You must be signed in to change notification settings - Fork 7
5) Sample library tutorial
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.
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.
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.
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.
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.
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
.
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
}
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
).
To draw the model, simply call the sausage64_drawmodel
function, which takes the display list pointer and the model helper:
sausage64_drawmodel(glistp, modeldata);
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
.
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);
}
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.
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).
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.
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;
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);