pret / pokeplatinum

Decompilation of Pokémon Platinum
164 stars 56 forks source link

Document 3D Abstraction Layer #238

Closed Fexty12573 closed 1 month ago

Fexty12573 commented 1 month ago

Documents the 2 levels of 3D abstraction layers in the game. Below a "quick" write-up about the system.

TL;DR: Easy 3D model loading and drawing API.

Abstraction Layers

The lowest level 3D is interacting directly with the hardware via NitroSDK. NitroSystem provides a slight abstraction over this with G3D.

The game has 2 additional layers of abstraction over this:

1 - Easy3D

The first layer of abstraction is the Easy3D System (easy3d.h). Which provides basic functionality for:

To elaborate on the last point: Before using any 3D graphics, a "3D Graphics State" must be initialized. The struct for this is currently called GenericPointerData. The Easy3D system provides an easy method to set up a simple gfx state via Easy3D_Init (With Easy3D_Shutdown as its counterpart), which works for most scenarios.

2 - Easy3DObject

The second layer of abstraction is Easy3DObject (easy3d_object.h). This API provides a streamlined interface for:

One thing to keep in mind is that this system by itself does not establish a 3D GFX State. This system is also not used everywhere. In a lot of places Easy3D is used directly instead of this object oriented interface.

Example

The following is an example for using the Easy3DObject API for:

  1. Loading a model with textures, animation, and texture animation
  2. Binding them all together
  3. Updating the animations
  4. Drawing the model

I will be using the Giratina model from the title screen for this.

Loading the data

The first thing needed is some static storage to hold our data:

Easy3DObject giratinaObj;
Easy3DModel giratinaModel;
Easy3DAnim giratinaAnim; // Model Animation
Easy3DAnim giratinaTexAnim; // Texture Animation
NNSFndAllocator allocator; // Needed for Animations

Next we load all of the data:

// Open the title NARC
NARC *narc = NARC_ctor(NARC_INDEX_DEMO__TITLE__TITLEDEMO, HEAP_ID_FIELD);

// Load the model from the title screen NARC. Member index 1 is the model data.
// There is also Easy3DModel_Load which takes a NARC index and a member index.
Easy3DModel_LoadFrom(&giratinaModel, narc, 1, HEAP_ID_FIELD);
Easy3DObject_Init(&giratinaObj, &giratinaModel);

// Initialize the Allocator used by the animations
Heap_FndInitAllocatorForExpHeap(&allocator, HEAP_ID_FIELD, 4);

// Load the model animation with member index 2.
Easy3DAnim_LoadFrom(&giratinaModelAnim, &giratinaModel, narc, 2, HEAP_ID_FIELD, &allocator);
// Bind the animation to the object
Easy3DObject_AddAnim(&giratinaObj, &giratinaModelAnim);

// Do the same for the texture animation
Easy3DAnim_LoadFrom(&giratinaTexAnim, &giratinaModel, narc, 0, HEAP_ID_FIELD, &allocator);
Easy3DObject_AddAnim(&giratinaObj, &giratinaTexAnim);

NARC_dtor(narc);

This is all that needs to be done in terms of setup, now the model is ready to draw (provided a 3D GFX State has been set up).

Drawing the Model

Before drawing there's a few things that should be configured on the object, one of them being the position of the model obviously. For simplicity's sake I will just set the models position to the player's position.

const VecFx32 *pos = PlayerAvatar_PosVector(fieldSystem->playerAvatar);
Easy3DObject_SetPosition(&giratinaObj, pos->x, pos->y, pos->z);

// Make sure the model actually gets rendered
Easy3DObject_SetVisibility(&giratinaObj, TRUE); 

// The model is pretty big so scale it to half its size
Easy3DObject_SetScale(&giratinaObj, FX32_CONST(0.5), FX32_CONST(0.5), FX32_CONST(0.5)); 

Now we can actually render the model:

// Update the animations
// Here we advance the animation by one frame.
// There is also Easy3DAnim_Update which does not loop the animation
Easy3DAnim_UpdateLooped(&giratinaModelAnim, FX32_ONE);
Easy3DAnim_UpdateLooped(&giratinaTexAnim, FX32_ONE);

// Draw the model
Easy3DObject_Draw(&giratinaObj);

All of that results in the following:

https://github.com/pret/pokeplatinum/assets/60443001/ba162c62-e64a-4cd6-850f-414774f19bfd

Obviously the rotation and resizing is not outlined above. The code for this is the following:

u16 angle = Easy3DObject_GetRotation(&giratinaObj, ROTATION_AXIS_Y);
angle = (angle + 100) % 0xFFFF;
Easy3DObject_SetRotation(&giratinaObj, angle, ROTATION_AXIS_Y);

// Resize using L and R
if (gCoreSys.heldKeys & PAD_BUTTON_R) {
    VecFx32 scale;
    Easy3DObject_GetScale(&giratinaObj, &scale.x, &scale.y, &scale.z);
    scale.x = FX_Mul(scale.x, FX32_CONST(1.01));
    scale.y = FX_Mul(scale.y, FX32_CONST(1.01));
    scale.z = FX_Mul(scale.z, FX32_CONST(1.01));
    Easy3DObject_SetScale(&giratinaObj, scale.x, scale.y, scale.z);
} else if (gCoreSys.heldKeys & PAD_BUTTON_L) {
    VecFx32 scale;
    Easy3DObject_GetScale(&giratinaObj, &scale.x, &scale.y, &scale.z);
    scale.x = FX_Mul(scale.x, FX32_CONST(0.99));
    scale.y = FX_Mul(scale.y, FX32_CONST(0.99));
    scale.z = FX_Mul(scale.z, FX32_CONST(0.99));
    Easy3DObject_SetScale(&giratinaObj, scale.x, scale.y, scale.z);
}