{%hackmd theme-dark %}
# Ocarina of Time: Skeletons, Animations, and Bunny Hood
## Note: frames
There are many coordinate systems that OoT uses, in several different spaces. Today we are concerned primarily with world space, and to a certain extent with embeddings of model space into world space. Even restricting to world space there are a number of different coordinate systems we may use, with different origins and orientations. We call any such coordinate system a *frame*. The following are the most common frames:
global frame
: The overall coordinate system used by the scene. You may have heard of the 0,0,0 glitch for attacking yet-to-load colliders with bombs, that point is referring to this frame. In this system, the defaults are
- x is westward
- y is upward
- z is southward
although the relationship of these to the minimap do vary by area and are not completely consistent.
(actor's) world frame
: Each actor has a *world frame*, defined by
- the origin is at `world.pos`
- the axes are oriented with `world.rot`
- x is leftward
- y is upward
- z is forward
This frame is used for movement: `Actor_MoveWithGravity` and friends will move the actor in the z direction.
(actor's) shape frame
: Similarly, each actor has a *shape frame*, defined by
- the origin is at `world.pos`
- the axes are oriented with `shape.rot` (note `shape` has no `pos`)
- x is leftward
- y is upward
- z is forward
This frame is used for orienting the model: z is the direction the actor is facing.
Shape and world are most often the same, but will frequently differ: backwalking, for example, will have the shape frame with z pointing in the direction of movement, opposite from the facing direction that the model will take. Another example is a Poe like Meg circling the Player: `shape` forward will be radial (looking at Player), while `world` forward is transverse (tangential to the circle on which it moves).
limb frame
: Every single limb in an actor has its own frame, origin at the joint, rotation usually determined by the actor's current animation. If the limb has a displaylist, SkelAnime drawing will assign a matrix to it that you can use to draw extra stuff on the limb. Because limb frames are inherited from embedding objects from model space in world space, they are subject to the same weird convention that the rest of the model space calculations are in.
## Structure of skeletons
A *skeleton* is structured as a [left-child right-sibling binary tree](https://en.wikipedia.org/wiki/Left-child_right-sibling_binary_tree), which is beneficial for both storage size and for drawing it, which is done recursively depth-first.
There are several different sorts, but we will concentrate on Flex skeletons. Flex skeletons usually have rather fewer limbs than ordinary skeletons, but fewer limbs is more useful for us simply because it is easier to follow. Another good reason is that most "human" characters use flex skeletons, and in particular, Player. (Flex skeletons were originally called "SV", which probably stands for something like "soft vertex" (?).)
Skeletons are made of three components: an overall header, a list of limbs, and a collection of individual limb information. For a flex skeleton with standard limbs, these look like
```c!
typedef struct {
/* 0x00 */ Vec3s jointPos;
/* 0x06 */ u8 child;
/* 0x07 */ u8 sibling;
/* 0x08 */ Gfx* dList;
} StandardLimb; // size = 0xC
typedef struct {
/* 0x00 */ void** segment;
/* 0x04 */ u8 limbCount;
} SkeletonHeader; // size = 0x8
// Model has limbs with flexible meshes
typedef struct {
/* 0x00 */ SkeletonHeader sh;
/* 0x08 */ u8 dListCount;
} FlexSkeletonHeader; // size = 0xC
```
Player's skeleton looks like this:
```
pos.x,pos.y,pos.z,sibling,child,dList[0], dList[1], structure
( 0, 2376, 0), 1, -1, 0, 0 0__
( -4, -104, 0), 2, 9, 0x060202A8, 0x0601AEC8 └─ 1**
( 607, 0, 0), 3, -1, 0, 0 ├─ 2__
( -172, 50, -190), 4, 6, 0x060204F0, 0x0601B0F8 │ └─ 3**
( 697, 0, 0), 5, -1, 0x060206E8, 0x0601B2B8 │ ├─ 4**
( 825, 5, 11), -1, -1, 0x06020978, 0x0601B510 │ │ └─ 5**
( -170, 57, 192), 7, -1, 0x06020AD8, 0x0601B638 │ 6**
( 695, 0, 0), 8, -1, 0x06020CD0, 0x0601B7F8 │ └─ 7**
( 817, 8, 4), -1, -1, 0x06020F60, 0x0601BA50 │ └─ 8**
( 0, -103, -7), 10, -1, 0, 0 9__
( 996, -201, -1), 11, 12, 0x06021360, 0x0601BDA0 └─ 10**
( -365, -670, 0), -1, -1, 0x060219B0, 0x0601C2A8 ├─ 11**
( 0, 0, 0), -1, 13, 0x060210C0, 0x0601BB78 12**
( 696, -175, 466), 14, 16, 0x06021E18, 0x0601C688 13**
( 581, 0, 0), 15, -1, 0x06021FE8, 0x0601C848 ├─ 14**
( 514, 0, 0), -1, -1, 0x06013CB0, 0x06016280 │ └─ 15**
( 696, -175, -466), 17, 19, 0x06021AE8, 0x0601C398 16**
( 577, 0, 0), 18, -1, 0x06021CB8, 0x0601C558 ├─ 17**
( 525, 0, 0), -1, -1, 0x060141C0, 0x060164E0 │ └─ 18**
( 657, -523, 367), -1, 20, 0x06015248, 0x06017360 19**
( 0, 0, 0), -1, -1, 0x06021130, 0x0601BBD8 20**
```
- The positions are stored as `s16`s to save space. This is also why model space uses much larger scale than world space: you can get more detail out an `s16` for the same "size" if you scale it down when putting it in world space. They are relative to the previous node, or the actor's shape frame for root.
- `sibling` and `child` are indexes into the limb table, `-1` standing for no limb.
- The two displaylists are present because Player has LOD limbs instead of standard ones: it will draw a less detailed version if the camera is farther away. (This is easy to spot if you look at the ears, which stick out more on the close model.)
## Drawing a skeleton
A skeleton, no matter what the type, is drawn recursively starting at the root limb, then working through children and siblings depth-first:
0. Set up pointers and allocate RSP matrices on segment 13, add a new matrix to the stack
1. Push a new copy of the current matrix onto the top of the matrix stack.
2. copy joint position from the limb data and joint rotation from the actor's `jointTable` (this is the only place that the animation data applies to drawing).
3. Drawing:
i. if there is no OverrideLimbDraw, or running it returns false, apply the transformation to the limb. Otherwise go straight to Post limb drawing.
ii. If there is a displaylist, draw it using the current matrix.
iii. For a flex skeleton, an RSP matrix is added if there was a displaylist before the OverrideLimbDraw ran, to keep the segment 13 matrices in the same place either way. **Note:** this means that you should never return `true` from a Flex OverrideLimbDraw, it will mess up the positions of all subsequently-drawn limbs at best, and probably produce some quite funky graphics artefacts.
4. Post limb drawing: if PostLimbDraw exists, apply its effects.
5. Draw limb's child, starting from step 1.
6. Pop top matrix off the matrix stack (to return to the one used before drawing the child). For flex, this does *not* affect the segment 13 matrices (which will always increment if the limb had a displaylist).
7. Draw limb's sibling, starting from step 1.
Notice that the main purprose of this approach is to create a chain of matrix transformations that cause each limb to be drawn based on the position of its parent. The flex skeleton produces a guaranteed stack of other matrices along the way that can be used in the LimbDraw functions or the displaylists themselves.
### OverrideLimbDraw
This allows for changing the position, colour, etc. of a limb before it is drawn, or even skipping the limb all together.
```c!
s32 EnGe3_OverrideLimbDraw(PlayState* play, s32 limbIndex, Gfx** dList, Vec3f* pos, Vec3s* rot, void* thisx) {
EnGe3* this = (EnGe3*)thisx;
switch (limbIndex) {
// Hide swords and veil from object_geldb
case GELDB_LIMB_VEIL:
case GELDB_LIMB_R_SWORD:
case GELDB_LIMB_L_SWORD:
*dList = NULL;
return false;
// Turn head
case GELDB_LIMB_HEAD:
rot->x += this->headRot.y;
FALLTHROUGH;
default:
// This is a hack to fix the color-changing clothes this Gerudo has on most N64 versions
OPEN_DISPS(play->state.gfxCtx, "../z_en_ge3.c", 547);
switch (limbIndex) {
case GELDB_LIMB_NECK:
break;
case GELDB_LIMB_HEAD:
gDPPipeSync(POLY_OPA_DISP++);
gDPSetEnvColor(POLY_OPA_DISP++, 80, 60, 10, 255);
break;
case GELDB_LIMB_R_SWORD:
case GELDB_LIMB_L_SWORD:
gDPPipeSync(POLY_OPA_DISP++);
gDPSetEnvColor(POLY_OPA_DISP++, 140, 170, 230, 255);
gDPSetPrimColor(POLY_OPA_DISP++, 0, 0, 255, 255, 255, 255);
break;
default:
gDPPipeSync(POLY_OPA_DISP++);
gDPSetEnvColor(POLY_OPA_DISP++, 140, 0, 0, 255);
break;
}
CLOSE_DISPS(play->state.gfxCtx, "../z_en_ge3.c", 566);
break;
}
return false;
}
```
To a certain extent this is a reasonable example: it shows two common uses of the OverrideLimbDraw, namely rotating the head according to outside influence (in this case, the code elsewhere uses it for facing the Player), and nulling displaylists on limbs we do not want to draw. This makes sense here since EnGe3 shares a model with EnGeldB, the fighter Gerudo.
This causes a problem, though: some of the displaylists for GeldB do not contain colour information, which is set in `EnGeldB_OverrideLimbDraw` (possibly they planned for further colours as MM got?). Leaving this information out, it will instead draw with stale colour data from the previous actor that set colour, usually either Player or a bomb. To patch this, they copied over the colour commands from `EnGeldB_OverrideLimbDraw`; this was was done for the final N64 version and carries over to all GameCube versions.
### PostLimbDraw
The other main type of callback. It allows us to do stuff onto the limb after it draws (hence "Post"): all the matrices and so on are already set up correctly. An example of the limits of what a PostLimbDraw can do is `EnFirefly_PostLimbDraw`, which uses it for drawing the eyes, drawing the aura, and setting the `bodyPartsPos`.
Setting world positions from limb positions is a common use of the PostLimbDraw.
### (MM-only) TransformLimbDraw
### Other ways of drawing on a skeleton
Flex skeletons use a dynamically allocated array of matrices on segment 13. This allows each limb's drawing to know about the frame of the previous limb's joint.
Player is sufficiently complex that it has yet another way of drawing objects
#### Example: Goron bracelet
The Goron bracelet is meant to draw on Player's left arm as child when any strength upgrade is obtained (it is assumed that you cannot get to adult without Goron bracelet). `Player_DrawImpl` contains this block to draw it:
```c!
SkelAnime_DrawFlexLod(play, skeleton, jointTable, dListCount, overrideLimbDraw, postLimbDraw, data, lod);
if ((overrideLimbDraw != Player_OverrideLimbDrawGameplayFirstPerson) &&
(overrideLimbDraw != Player_OverrideLimbDrawGameplay_80090440) &&
(gSaveContext.gameMode != GAMEMODE_END_CREDITS)) {
if (LINK_IS_ADULT) {
[...]
else {
if (Player_GetStrength() > PLAYER_STR_NONE) {
gSPDisplayList(POLY_OPA_DISP++, gLinkChildGoronBraceletDL);
}
}
}
```
There is no specification on the limb in this function, so it must know from somewhere else. Looking in the displaylist, we see:
<details>
<summary>Large displaylist, click to show</summary>
```c!
Gfx gLinkChildGoronBraceletDL[] = {
gsSPMatrix(0x0D0002C0, G_MTX_NOPUSH | G_MTX_LOAD | G_MTX_MODELVIEW),
gsDPPipeSync(),
gsDPSetTextureLUT(G_TT_NONE),
gsSPTexture(0x012C, 0x0190, 0, G_TX_RENDERTILE, G_ON),
gsDPLoadTextureBlock(gLinkChildGoronBraceletTex, G_IM_FMT_RGBA, G_IM_SIZ_16b, 8, 8, 0, G_TX_MIRROR | G_TX_WRAP,
G_TX_MIRROR | G_TX_WRAP, 3, 3, G_TX_NOLOD, 15),
gsDPSetCombineLERP(TEXEL0, 0, SHADE, 0,
0, 0, 0, 1,
COMBINED, 0, PRIMITIVE, 0,
0, 0, 0, COMBINED),
gsDPSetRenderMode(G_RM_FOG_SHADE_A, G_RM_AA_ZB_OPA_SURF2),
gsSPClearGeometryMode(G_CULL_BACK),
gsSPSetGeometryMode(G_FOG | G_LIGHTING | G_TEXTURE_GEN | G_TEXTURE_GEN_LINEAR),
gsDPSetPrimColor(0, 0, 255, 255, 255, 255),
gsSPVertex(&object_link_childVtx_00ABF0[1106], 3, 0),
gsSP1Triangle(0, 1, 2, 0),
gsSPVertex(&object_link_childVtx_00ABF0[1109], 14, 0),
[...]
gsDPPipeSync(),
gsDPSetTextureLUT(G_TT_NONE),
gsSPTexture(0xFFFF, 0xFFFF, 0, G_TX_RENDERTILE, G_ON),
gsDPLoadTextureBlock(gLinkChildGoronSymbolTex, G_IM_FMT_IA, G_IM_SIZ_16b, 16, 32, 0, G_TX_NOMIRROR | G_TX_CLAMP,
G_TX_NOMIRROR | G_TX_CLAMP, 4, 5, G_TX_NOLOD, G_TX_NOLOD),
gsDPSetCombineLERP(TEXEL0, 0, SHADE, 0, 0, 0, 0, TEXEL0, COMBINED, 0, PRIMITIVE, 0, 0, 0, 0, COMBINED),
gsDPSetRenderMode(G_RM_FOG_SHADE_A, G_RM_AA_ZB_XLU_DECAL2),
gsSPClearGeometryMode(G_CULL_BACK | G_TEXTURE_GEN | G_TEXTURE_GEN_LINEAR),
gsSPSetGeometryMode(G_FOG | G_LIGHTING),
gsDPSetPrimColor(0, 0, 255, 255, 255, 255),
gsSPVertex(&object_link_childVtx_00ABF0[2238], 8, 0),
[...]
gsSPEndDisplayList(),
};
```
</details>
and in particular
```c!
gsSPMatrix(0x0D0002C0, G_MTX_NOPUSH | G_MTX_LOAD | G_MTX_MODELVIEW),
```
so it is using a limb frame matrix. Which one? Looking back up at the limbs, we can work out where each limb's frame matrix is:
```c!
typedef enum {
skeleton limb DL/mtx? 0x0D000000 +
/* 0x00 */ PLAYER_LIMB_NONE, | No | -
0__ /* 0x01 */ PLAYER_LIMB_ROOT, | No | -
└─ 1** /* 0x02 */ PLAYER_LIMB_WAIST, | Yes | 0
├─ 2__ /* 0x03 */ PLAYER_LIMB_LOWER, | No | -
│ └─ 3** /* 0x04 */ PLAYER_LIMB_R_THIGH, | Yes | 0x40
│ ├─ 4** /* 0x05 */ PLAYER_LIMB_R_SHIN, | Yes | 0x80
│ │ └─ 5** /* 0x06 */ PLAYER_LIMB_R_FOOT, | Yes | 0xC0
│ 6** /* 0x07 */ PLAYER_LIMB_L_THIGH, | Yes | 0x100
│ └─ 7** /* 0x08 */ PLAYER_LIMB_L_SHIN, | Yes | 0x140
│ └─ 8** /* 0x09 */ PLAYER_LIMB_L_FOOT, | Yes | 0x180
9__ /* 0x0A */ PLAYER_LIMB_UPPER, | No | -
└─ 10** /* 0x0B */ PLAYER_LIMB_HEAD, | Yes | 0x1C0
├─ 11** /* 0x0C */ PLAYER_LIMB_HAT, | Yes | 0x200
12** /* 0x0D */ PLAYER_LIMB_COLLAR, | Yes | 0x240
13** /* 0x0E */ PLAYER_LIMB_L_SHOULDER, | Yes | 0x280
├─ 14** /* 0x0F */ PLAYER_LIMB_L_FOREARM, | Yes | 0x2C0
│ └─ 15** /* 0x10 */ PLAYER_LIMB_L_HAND, | Yes | 0x300
16** /* 0x11 */ PLAYER_LIMB_R_SHOULDER, | Yes | 0x340
├─ 17** /* 0x12 */ PLAYER_LIMB_R_FOREARM, | Yes | 0x380
│ └─ 18** /* 0x13 */ PLAYER_LIMB_R_HAND, | Yes | 0x3C0
19** /* 0x14 */ PLAYER_LIMB_SHEATH, | Yes | 0x400
20** /* 0x15 */ PLAYER_LIMB_TORSO, | Yes | 0x440
/* 0x16 */ PLAYER_LIMB_MAX | - | -
} PlayerLimb;
```
Hence, as expected, `0x0D0001C0` is the matrix that converts to the limb frame of the left forearm.
#### Example: Triforce in the ending cutscene
```c!
void EndTitle_DrawFull(Actor* thisx, PlayState* play) {
MtxF* mf;
EndTitle* this = (EndTitle*)thisx;
s32 frameCount = play->csCtx.frames;
Player* player = GET_PLAYER(play);
mf = &player->leftHandMf;
OPEN_DISPS(play->state.gfxCtx, "../z_end_title.c", 403);
// Draw the Triforce on Link's left hand
Gfx_SetupDL_25Xlu(play->state.gfxCtx);
Matrix_Mult(mf, MTXMODE_NEW);
Matrix_Translate(0.0f, 150.0f, 170.0f, MTXMODE_APPLY);
Matrix_Scale(0.13f, 0.13f, 0.13f, MTXMODE_APPLY);
Matrix_RotateX(BINANG_TO_RAD(0xBB8), MTXMODE_APPLY);
Matrix_RotateY(0.0f, MTXMODE_APPLY);
Matrix_RotateZ(0.0f, MTXMODE_APPLY);
gSPMatrix(POLY_XLU_DISP++, Matrix_NewMtx(play->state.gfxCtx, "../z_end_title.c", 412), G_MTX_LOAD);
gSPDisplayList(POLY_XLU_DISP++, sTriforceDL);
CLOSE_DISPS(play->state.gfxCtx, "../z_end_title.c", 417);
```
This demonstrates yet another way to draw: use a predetermined matrix or position, in this case for the left hand.
## Example: Bunny Hood
The Bunny Hood is famous for its floppy ears. This is very useful for our purposes, because it uses all the rotation and translation components already, so we have a recognisable, general example we can examine.
A look at the displaylist shows that it uses 2 segments:
<details>
<summary>Large displaylist, click to show</summary>
```c!
Gfx gLinkChildBunnyHoodDL[] = {
gsSPMatrix(0x0D0001C0, G_MTX_NOPUSH | G_MTX_LOAD | G_MTX_MODELVIEW),
gsSPTexture(0xFFFF, 0xFFFF, 0, G_TX_RENDERTILE, G_ON),
gsDPPipeSync(),
gsDPSetRenderMode(G_RM_FOG_SHADE_A, G_RM_AA_ZB_OPA_SURF2),
gsDPSetCombineLERP(TEXEL0, 0, SHADE, 0,
0, 0, 0, TEXEL0,
PRIMITIVE, 0, COMBINED, 0,
0, 0, 0, COMBINED),
gsDPSetTextureLUT(G_TT_NONE),
gsDPLoadTextureBlock(gLinkChildBunnyHoodEyeTex, G_IM_FMT_RGBA, G_IM_SIZ_16b, 16, 16, 0, G_TX_NOMIRROR | G_TX_CLAMP,
G_TX_NOMIRROR | G_TX_CLAMP, 4, 4, G_TX_NOLOD, G_TX_NOLOD),
gsDPSetTileSize(G_TX_RENDERTILE, 0, 0, 0x003C, 0x003C),
gsDPSetPrimColor(0, 0x80, 255, 255, 255, 255),
gsSPSetGeometryMode(G_LIGHTING),
gsSPClearGeometryMode(G_CULL_BACK),
gsSPClearGeometryMode(G_TEXTURE_GEN | G_TEXTURE_GEN_LINEAR),
gsSPVertex(object_link_childVtx_02C428, 25, 0),
[...]
gsDPPipeSync(),
gsDPSetRenderMode(G_RM_FOG_SHADE_A, G_RM_AA_ZB_TEX_EDGE2),
gsDPSetTextureLUT(G_TT_NONE),
gsDPLoadTextureBlock(gLinkChildBunnyHoodTex, G_IM_FMT_RGBA, G_IM_SIZ_16b, 16, 32, 0, G_TX_NOMIRROR | G_TX_CLAMP,
G_TX_NOMIRROR | G_TX_CLAMP, 4, 5, G_TX_NOLOD, G_TX_NOLOD),
gsDPSetTileSize(G_TX_RENDERTILE, 0, 0, 0x003C, 0x007C),
gsSPClearGeometryMode(G_TEXTURE_GEN | G_TEXTURE_GEN_LINEAR),
gsSPVertex(&object_link_childVtx_02C428[25], 10, 0),
[...]
gsSPMatrix(0x0D0001C0, G_MTX_NOPUSH | G_MTX_LOAD | G_MTX_MODELVIEW),
gsSPTexture(0xFFFF, 0xFFFF, 0, G_TX_RENDERTILE, G_ON),
gsSPSetGeometryMode(G_LIGHTING),
gsSPClearGeometryMode(G_TEXTURE_GEN | G_TEXTURE_GEN_LINEAR),
gsSPVertex(&object_link_childVtx_02C428[35], 5, 0),
gsSPMatrix(0x0B000000, G_MTX_NOPUSH | G_MTX_MUL | G_MTX_MODELVIEW),
gsSPTexture(0xFFFF, 0xFFFF, 0, G_TX_RENDERTILE, G_ON),
gsDPPipeSync(),
gsDPSetRenderMode(G_RM_FOG_SHADE_A, G_RM_AA_ZB_OPA_SURF2),
gsDPSetCombineLERP(TEXEL0, 0, SHADE, 0,
0, 0, 0, TEXEL0,
PRIMITIVE, 0, COMBINED, 0,
0, 0, 0, COMBINED),
gsDPSetTextureLUT(G_TT_NONE),
gsDPLoadTextureBlock(gLinkChildBunnyHoodEarTex, G_IM_FMT_RGBA, G_IM_SIZ_16b, 16, 32, 0, G_TX_NOMIRROR | G_TX_CLAMP,
G_TX_NOMIRROR | G_TX_CLAMP, 4, 5, G_TX_NOLOD, G_TX_NOLOD),
gsDPSetTileSize(G_TX_RENDERTILE, 0, 0, 0x003C, 0x007C),
gsDPSetPrimColor(0, 0x80, 255, 255, 255, 255),
gsSPSetGeometryMode(G_CULL_BACK),
gsSPClearGeometryMode(G_TEXTURE_GEN | G_TEXTURE_GEN_LINEAR),
gsSPVertex(&object_link_childVtx_02C428[40], 26, 5),
[...]
gsSPMatrix(0x0D0001C0, G_MTX_NOPUSH | G_MTX_LOAD | G_MTX_MODELVIEW),
gsSPTexture(0xFFFF, 0xFFFF, 0, G_TX_RENDERTILE, G_ON),
gsSPSetGeometryMode(G_LIGHTING),
gsSPClearGeometryMode(G_TEXTURE_GEN | G_TEXTURE_GEN_LINEAR),
gsSPVertex(&object_link_childVtx_02C428[66], 5, 0),
gsSPMatrix(0x0B000040, G_MTX_NOPUSH | G_MTX_MUL | G_MTX_MODELVIEW),
gsSPTexture(0xFFFF, 0xFFFF, 0, G_TX_RENDERTILE, G_ON),
gsDPPipeSync(),
gsDPSetRenderMode(G_RM_FOG_SHADE_A, G_RM_AA_ZB_OPA_SURF2),
gsDPSetCombineLERP(TEXEL0, 0, SHADE, 0,
0, 0, 0, TEXEL0,
PRIMITIVE, 0, COMBINED, 0,
0, 0, 0, COMBINED),
gsDPSetTextureLUT(G_TT_NONE),
gsDPLoadTextureBlock(gLinkChildBunnyHoodEarTex, G_IM_FMT_RGBA, G_IM_SIZ_16b, 16, 32, 0, G_TX_NOMIRROR | G_TX_CLAMP,
G_TX_NOMIRROR | G_TX_CLAMP, 4, 5, G_TX_NOLOD, G_TX_NOLOD),
gsDPSetTileSize(G_TX_RENDERTILE, 0, 0, 0x003C, 0x007C),
gsDPSetPrimColor(0, 0x80, 255, 255, 255, 255),
gsSPSetGeometryMode(G_CULL_BACK),
gsSPClearGeometryMode(G_TEXTURE_GEN | G_TEXTURE_GEN_LINEAR),
gsSPVertex(&object_link_childVtx_02C428[71], 26, 5),
[...]
gsSPEndDisplayList(),
};
```
</details>
namely `0x0D00001C0` and `0x0B000000`/`0x0B000040` As we know, segment 13 is used for the limbdraw matrices, so these parts are drawing on some limb; to find out which, we do the same calculation as above and find that the limb is indeed `PLAYER_LIMB_HEAD`. This is the "head" part of Bunny hood, including the eyes.
The rest uses two matrices on segment 11, which have to be set separately. We see the code to do this in `Player_DrawGameplay`:
```clike!
if ((overrideLimbDraw == Player_OverrideLimbDrawGameplayDefault) && (this->currentMask != PLAYER_MASK_NONE)) {
// Matrices for use with Bunny Hood (no other masks use segment 11, so this could have gone inside the next if)
Mtx* matrices = Graph_Alloc(play->state.gfxCtx, 2 * sizeof(Mtx));
if (this->currentMask == PLAYER_MASK_BUNNY) {
Vec3s earRot;
// Apply the floppiness to Bunny Hood's ears
gSPSegment(POLY_OPA_DISP++, 0x0B, matrices);
// Right ear
earRot.x = sBunnyEarKinematics.rot.y + 0x3E2;
earRot.y = sBunnyEarKinematics.rot.z + 0xDBE;
earRot.z = sBunnyEarKinematics.rot.x - 0x348A;
Matrix_SetTranslateRotateYXZ(97.0f, -1203.0f, -240.0f, &earRot);
Matrix_ToMtx(matrices++, "../z_player.c", 19273);
// Left ear
earRot.x = sBunnyEarKinematics.rot.y - 0x3E2;
earRot.y = -sBunnyEarKinematics.rot.z - 0xDBE;
earRot.z = sBunnyEarKinematics.rot.x - 0x348A;
Matrix_SetTranslateRotateYXZ(97.0f, -1203.0f, 240.0f, &earRot);
Matrix_ToMtx(matrices, "../z_player.c", 19279);
// No final front ear
}
gSPDisplayList(POLY_OPA_DISP++, sMaskDlists[this->currentMask - 1]);
}
```
Each ear is translated and rotated by its own matrix to set it into position, right by `0x0B000000` and left by `0x0B000040`. The translation is fixed: it just shifts the ears to start in the correct places on top of the head rather than on the limb joint itself, which is more around the top of the spine. The rotations are the tricky part: we can see from the translation that the `z` components are what differ between the ears, which breaks with our expectation that `z` is forward, as it is in the actor's world frame.
### Rotations
If we draw the axes for the head limb, we see that unlike what we are used to in global coordinates,
- x is forward
- y is downward
- z is leftward.
This explains the strange translations we saw earlier. This is further confused by two things
- The copy to the vector of angles to use in the rotation is not a like-to-like copy, instead it goes
- x -> z
- z -> y
- y -> x
- The order of rotations is not the same: it goes X, then Z, then Y (here the capital letters refer to the axes rather than the angles).
We assume this is a consequence of the settings in the modelling software that Nintendo used for Ocarina of Time.
Therefore the rotational component can be summarised as
- Rotate around the limb Z (leftward) axis by `rot.x`
- Rotate around the limb X (forward) axis by `rot.y`
- Rotate around the limb Y (downward) axis by `rot.z`
### Kinematics
The kinematic (position and velocity) information of the ears is contained in this struct:
```c
typedef struct {
/* 0x0 */ Vec3s rot;
/* 0x6 */ Vec3s angVel;
} BunnyEarKinematics; // size = 0xC
```
where as usual, for each angle $\theta$, the angular velocity $\omega$ is related by $\Delta \theta = \omega \Delta t$, $\Delta t$ here being the time of a single update cycle. Thus approximately, $\omega = \dot{\theta}$.
### Dynamics
The angles and angular velocities in the struct are updated by the following function:
<details>
<summary>Large function, click to show</summary>
```c!
void Player_UpdateBunnyEars(Player* this) {
Vec3s force;
s16 angle;
// Damping: decay to 7/8 the previous value each frame
sBunnyEarKinematics.angVel.x -= sBunnyEarKinematics.angVel.x >> 3;
sBunnyEarKinematics.angVel.y -= sBunnyEarKinematics.angVel.y >> 3;
// Elastic restorative force
sBunnyEarKinematics.angVel.x += -sBunnyEarKinematics.rot.x >> 2;
sBunnyEarKinematics.angVel.y += -sBunnyEarKinematics.rot.y >> 2;
// Forcing from motion relative to shape frame
angle = this->actor.world.rot.y - this->actor.shape.rot.y;
force.x = (s32)(this->actor.speedXZ * -200.0f * Math_CosS(angle) * (Rand_CenteredFloat(2.0f) + 10.0f)) & 0xFFFF;
force.y = (s32)(this->actor.speedXZ * 100.0f * Math_SinS(angle) * (Rand_CenteredFloat(2.0f) + 10.0f)) & 0xFFFF;
sBunnyEarKinematics.angVel.x += force.x >> 2;
sBunnyEarKinematics.angVel.y += force.y >> 2;
// Here the new \f$\omega\f$ is an approximation of the solution to the differential equation,
// \f$ \dot{\omega} = -\omega/8 - \theta/4 + F(t) \f$
// Clamp both angular velocities to [-6000, 6000]
if (sBunnyEarKinematics.angVel.x > 6000) {
sBunnyEarKinematics.angVel.x = 6000;
} else if (sBunnyEarKinematics.angVel.x < -6000) {
sBunnyEarKinematics.angVel.x = -6000;
}
if (sBunnyEarKinematics.angVel.y > 6000) {
sBunnyEarKinematics.angVel.y = 6000;
} else if (sBunnyEarKinematics.angVel.y < -6000) {
sBunnyEarKinematics.angVel.y = -6000;
}
// Add angular velocity to rotations
sBunnyEarKinematics.rot.x += sBunnyEarKinematics.angVel.x;
sBunnyEarKinematics.rot.y += sBunnyEarKinematics.angVel.y;
// swivel ears outwards if bending backwards
if (sBunnyEarKinematics.rot.x < 0) {
sBunnyEarKinematics.rot.z = sBunnyEarKinematics.rot.x >> 1;
} else {
sBunnyEarKinematics.rot.z = 0;
}
}
```
</details>
This code implies that for `rot.x` and `rot.y`,
$$ \Delta \omega = \left( - \frac{1}{8} \omega - \frac{1}{4} \theta + F(t) \right) \Delta t . $$
Here the driving force is different for each direction:
$$\begin{align}
F_x(t) &= (-200 s (10 + r_x) \cos \phi) \frac{1}{4} , \\
F_y(t) &= (100 s (10 + r_y) \sin \phi) \frac{1}{4} ,
\end{align}$$
where $s$ is the horizontal speed, $\phi$ is the horizontal angle between forwards and the velocity vector, and $r_x,r_y$ are random numbers in $[-1,1]$ that are picked every frame. $(s \cos \phi, s \sin \phi)$ is the resolution of the horizontal speed perpendicular and parallel to the `shape.z` axis.
`rot.z` is not dynamical: it is instead tied to `rot.x`, and serves only to rotate the ears outwards if they are bent backwards.
To find the continuous approximation, divide and take the limit:
$$ \dot{\omega} = - \frac{1}{8} \omega - \frac{1}{4} \theta + F(t) . $$
Replacing $\omega$ by $\dot{\theta}$, we obtain the single ordinary differential equation
$$ \ddot{\theta} + \frac{1}{8} \dot{\theta} + \frac{1}{4} \theta = F(t) , $$
i.e. the `rot.x` and `rot.y` are damped harmonic oscillators, driven by the horizontal velocity.
#### Limitations of the program as a model of reality
- We are probably meant to assume that the bunny ears act like they are elastically attached, but a linear restoring force is not going to be appropriate for large angles, even before the issues we note below caused by the angles looping round the circle.
- The driving force is probably supposed to be derived from (linear) air resistance impinging on the ears' cross section. This explains the lesser sideways force: the bunny ears are wider than they are deep. However, this does not take into account that the force should decrease as the cross-sectional area exposed to the oncoming air is decreased.
- Strictly speaking there should also be an induced force from Player's acceleration, but since acceleration is not stored anywhere convenient, we would have to remember Player's speed from the previous update cycle and subtract to obtain it. This would also require imposing consistent mass on the ears.
- Nothing is done to account for vertical speed, or changes thereof.
#### Limitations of the mathematical model
- The angular velocity is also clamped to the interval $[-6000, 6000]$, but this generally only matters for short-term effects: long-term, the forcing is the most relevant term.
- This model also does not account for the periodicity in $\theta$: large displacements will suddenly feel the restorative force in the opposite direction, which will encourage the ears to rotate in full circles rather than to oscillate about the neutral position; this can be seen when supersliding or HESSing while wearing Bunny Hood, for example.
- Finally, the random variations per frame are not taken into account in the continuous model; this can be mitigated by considering a range of values to propagate uncertainty.