PS2 Programming Log

PS2 Logo

My working log for learning programming for the Playstation 2. The goal is to have an abstraction/library layer that will be cross platform for the Playstation 1 as well, to leverage my existing psyq_lib stuff, hence the repository name is 'GameLib' currently

The source code is currently here: https://bitbucket.org/williamblair/gamelib

08/22/2020

The ps2sdk is set up, and have a basic Render/Model/Mesh setup. It was easier to set up a virtual machine with Linux to install the SDK, which you can then install via a single script from here: https://github.com/ps2dev/ps2dev. (I really should just re-install Linux on my machine to dual boot!) The code is adapted from the teapot example included with the ps2sdk.

The main loop is currently quite simple:

/*
 * GameLib test program
 */

#include "PS2Includes.h"
#include "PS2Renderer.h"
#include "PS2Model.h"

#include "mesh_data.c"

int main(int argc, char **argv)
{
    PS2Renderer Renderer;

    MeshData TeapotMeshData;
    PS2Model TeapotModel;

    Renderer.Init();
    
    TeapotMeshData.Normals = normals;
    TeapotMeshData.Colors = colours;
    TeapotMeshData.Vertices = vertices;
    TeapotMeshData.Indices = points;
    TeapotMeshData.VertexCount = vertex_count;
    TeapotMeshData.IndexCount = points_count;
    TeapotModel.SetMeshData( TeapotMeshData );

    for (;;)
    {
        TeapotModel.Rotate( 0.0f, 0.01f, 0.0f );
        Renderer.DrawModel( TeapotModel );

        Renderer.Update();
    }

    // Sleep
    SleepThread();

    // End program.
    return 0;

}

Which is shown running in pcsx2 and on a PS2 Slim:

The PS2 is running the ELF file over ethernet via ps2link and ps2client. ps2link is running on the PS2 off of the memory card (thanks to Free McBoot), which in turn is started via uLaunchElf. The IP configuration in order to find the PS2 over TCP from your computer is set in the file IPCONFIG.DAT

On the pc side, ps2client runs by specifying the PS2's IP address, then specifying to run your ELF file on the Emotion Engine (the PS2 main CPU) via execee

Current TODOs include lighting, camera, and controller management. The lighting and camera are currently hard coded in the renderer.

The following links were helpful for getting started:

08/23/2020

Added Camera and Controller classes (for the gamepad), as well as a simple floor mesh/model. The gamepad code I copied from another PS2 C++ class I had made when I was experimenting with PS2SDK years ago: https://github.com/williamblair/PS2Pad. Additionally, the Camera class I adapted from my learnopengl code for FPS camera movement: https://github.com/williamblair/learnopengl/tree/master/07_fps_camera. It felt good to see the fruits of previous labor ripen into use!

The camera movement/rotation calculation isn't optimized and probably could be using VU0 vector math; currently each x/y/z camera point is calculated individually.

Interestingly, moving around the world you can see the geometry freak out in places; I think this is because I need to manually do clipping and back face culling, which I read in Glampert's PS2 page: https://glampert.com/2015/03-23/ps2-homebrew-hardware-and-ps2dev-sdk/

The resulting main code is a little longer now but still not totally complicated:

/*
 * GameLib test program
 */

#include "PS2Includes.h"
#include "PS2Renderer.h"
#include "PS2Model.h"
#include "PS2Camera.h"
#include "PS2Controller.h"

#include "mesh_data.c"

... // Floor class impl is here

static void RotateCamera( Controller& Pad, Camera& Cam )
{
    int RightStickX = Pad.GetRightJoyX();
    int RightStickY = Pad.GetRightJoyY();
    const float Scale = 0.0001f;
    
    if (RightStickX > 150 || RightStickX < 100)
    {
        RightStickX -= 127; // scale from 0..255 to -128, 127
        Cam.Rotate( 0.0f, -Scale * (float)RightStickX, 0.0f );
    }
    
    if (RightStickY > 150 || RightStickY < 100)
    {
        RightStickY -= 127;
        Cam.Rotate( -Scale * (float)RightStickY, 0.0f, 0.0f );
    }
}

static void TranslateCamera( Controller& Pad, Camera& Cam )
{
    int LeftStickX = Pad.GetLeftJoyX();
    int LeftStickY = Pad.GetLeftJoyY();
    const float Scale = 0.01f;
    
    if (LeftStickX > 150 || LeftStickX < 100)
    {
        LeftStickX -= 127; // scale from 0..255 to -128, 127
        Cam.Translate( Scale * (float)LeftStickX, 0.0, 0.0f );
    }
    
    if (LeftStickY > 150 || LeftStickY < 100)
    {
        LeftStickY -= 127;
        // negative is forwards
        Cam.Translate( 0.0f, 0.0f, Scale * (float)LeftStickY );
    }
}

int main(int argc, char **argv)
{
    PS2Renderer Render;
    PS2Camera Cam;
    MeshData TeapotMeshData;
    PS2Model TeapotModel;
    PS2Floor Floor;
    PS2Controller Pad;

    SifInitRpc(0);

    Render.Init();
    Render.AttachCamera( Cam );
    
    TeapotMeshData.Normals = normals;
    TeapotMeshData.Colors = colours;
    TeapotMeshData.Vertices = vertices;
    TeapotMeshData.Indices = points;
    TeapotMeshData.VertexCount = vertex_count;
    TeapotMeshData.IndexCount = points_count;
    TeapotModel.SetMeshData( TeapotMeshData );

    Floor.Init();
    
    Pad.Init(0);

    for (;;)
    {
        Pad.Update();
        RotateCamera( Pad, Cam );
        TranslateCamera( Pad, Cam );
        
        TeapotModel.Rotate( 0.0f, 0.01f, 0.0f );
        Render.DrawModel( TeapotModel );
        
        //Floor.Rotate( 0.01f, 0.0f, 0.0f );
        Render.DrawModel( Floor );

        Render.Update();
    }

    // Sleep
    SleepThread();

    // End program.
    return 0;

}

Here's the result in PCSX2 and via PS2Client:

08/24/2020

Added a Light class. The renderer could use some more work - currently the lights are fixed/unchangeable at runtime. When the light is attached to the renderer, its data is copied into the internal renderer data array. Removing lights would require moving data within the renderer array. Additionaly, the renderer should use its own index for adding to the array instead of relying on the light's auto-assigned number to use as an index, otherwise the first array enter may not be filled but instead could start with an offset, which would break lighting calculation.

The main code is still straightforward, at least:

/*
 * GameLib test program
 */

#include "PS2Includes.h"
#include "PS2Renderer.h"
#include "PS2Model.h"
#include "PS2Camera.h"
#include "PS2Controller.h"

#include "mesh_data.c"

...

int main(int argc, char **argv)
{
    PS2Renderer Render;
    PS2Camera Cam;
    MeshData TeapotMeshData;
    PS2Model TeapotModel;
    PS2Floor Floor;
    PS2Controller Pad;
    PS2Light Lights[4];

    Lights[0].SetDirection(  0.00f,  0.00f,  0.00f );
    Lights[1].SetDirection(  1.00f,  0.00f, -1.00f );
    Lights[2].SetDirection(  0.00f,  1.00f, -1.00f );
    Lights[3].SetDirection( -1.00f, -1.00f, -1.00f );
    Lights[0].SetColor( 0.00f, 0.00f, 0.00f );
    Lights[1].SetColor( 1.00f, 0.00f, 0.00f );
    Lights[2].SetColor( 0.30f, 0.30f, 0.30f );
    Lights[3].SetColor( 0.50f, 0.50f, 0.50f );
    Lights[0].SetType( Light::AMBIENT );
    Lights[1].SetType( Light::DIRECTIONAL );
    Lights[2].SetType( Light::DIRECTIONAL );
    Lights[3].SetType( Light::DIRECTIONAL );

    SifInitRpc(0);

    Render.Init();
    Render.AttachCamera( Cam );
    Render.AttachLight( Lights[0] );
    Render.AttachLight( Lights[1] );
    Render.AttachLight( Lights[2] );
    Render.AttachLight( Lights[3] );
    
    TeapotMeshData.Normals = normals;
    TeapotMeshData.Colors = colours;
    TeapotMeshData.Vertices = vertices;
    TeapotMeshData.Indices = points;
    TeapotMeshData.VertexCount = vertex_count;
    TeapotMeshData.IndexCount = points_count;
    TeapotModel.SetMeshData( TeapotMeshData );

    Floor.Init();
    
    Pad.Init(0);

    for (;;)
    {
        Pad.Update();
        RotateCamera( Pad, Cam );
        TranslateCamera( Pad, Cam );
        
        TeapotModel.Rotate( 0.0f, 0.01f, 0.0f );
        Render.DrawModel( TeapotModel );
        
        //Floor.Rotate( 0.01f, 0.0f, 0.0f );
        Render.DrawModel( Floor );

        Render.Update();
    }

    // Sleep
    SleepThread();

    // End program.
    return 0;

}

11/11/2020

Investigating Glampert's PS2Dev programs (repo here: https://bitbucket.org/glampert/ps2dev-tests/src/master/) to see how they're implemented. Got the cubes application to compile after a few changes (presumably this was needed due to updates/more modern ps2sdk changes)

First, replaced fio* IO functions with c standard library ones (fioOpen() with open(), fioLseek with lseek(), etc.)

Second, removed the virtual CameraBase deconstructor and deconstructor from the thirdperson camera classes, due to the following error: Undefined reference to '__cxa_pure_virtual' with regards to the third person camera deconstructor

Third, removed `-lmf` from the compiler flags as mf wasn't found

After building, the result running in pcsx2 was having controller issues:

So I replaced the game controller pad implementation with my own I had created a while back: https://github.com/williamblair/PS2Pad

Success! The result:

My forked version of Glampert's repo is here: https://bitbucket.org/williamblair/ps2dev-tests/src/master/

11/15/2020

Finished the main setup/system initialization re-writing of Glampert's cube demo. This only required part of the Renderer class to be implemented along with the RenderPacket class.

I had an issue where would never pass 'draw_wait_finish()', however after removing some printf statements it worked. I assume then that the PS2/library are sensitive to timing with regards to hardware status, which I guess makes sense:

dma_wait_fast();
dma_channel_send_chain(DMA_CHANNEL_GIF, currentFramePacket->GetQwordPtr(), currentFramePacket->GetDisplacement(currentFrameQwPtr), 0, 0);

// V-Sync wait
graph_wait_vsync();
draw_wait_finish();

The above worked, while below did not:

printf("end frame dma wait fast\n");
dma_wait_fast();
printf("end frame dma wait channel send chain\n");
dma_channel_send_chain(DMA_CHANNEL_GIF, currentFramePacket->GetQwordPtr(), currentFramePacket->GetDisplacement(currentFrameQwPtr), 0, 0);

// V-Sync wait
printf("end frame wait vsync\n");
graph_wait_vsync();
printf("end frame draw wait finish\n");
draw_wait_finish();

The result isnt very exciting yet, just a brown screen:

The code can be found here: link

11/21/2020

Got a triangle drawn, continuing the glampert code re-implementation. This required addition of basic geometry renderering and the vector/matrix library. Originally I was stuck on thinking there was an error and the triangle wasn't getting drawn; however, it turned out to be that the triangle needed to be rotated 180 degrees about the Y axis due to the original code's camera placement. Without this rotation the triangle was back-facing and thus culled. My current re-write does not cull any triangles, so you can see when it spins you can see both sides

The code to render the triangle looks like this:

renderer.SetViewProjMatrix(gViewMatrix * gProjectionMatrix);

PS2Matrix trans, rotX, rotY;
trans.makeTranslation(0.0f, 0.0f, -3.0f);
rotX.makeRotationX(deg2rad(0.0f));
rotY.makeRotationY(deg2rad(triRotation));
triRotation += 1.0f;
if ( triRotation >= 360.0f )
{
    triRotation -= 360.0f;
}

renderer.SetModelMatrix(rotX * rotY * trans);
renderer.DrawIndexedTriangles(triIndi, 3, triVertices, 3);

11/22/2020

A group of cubes is now drawn instead of a single triangle. The cube data is stored in a Cube object (the vertices, texture coords, colors, and faces/indices). Each cube is drawn 3 times in a different location:

Cube gRedCube;
Cube gWhiteCube;
Cube gGreenCube;
float gCubeRotation = 0.0f;

static inline void DrawCubeGroup(const Cube & cube, const float y)
{
	for (u32 z = 0; z < 3; ++z)
	{
		for (u32 x = 0; x < 3; ++x)
		{
			PS2Matrix trans, rotX, rotY;
			trans.makeTranslation((x * 5.0f) - 5.0f, y, (z * -5.0f) - 20.0f);
			rotX.makeRotationX(deg2rad(gCubeRotation));
			rotY.makeRotationY(deg2rad(gCubeRotation));

			renderer.SetModelMatrix(rotX * rotY * trans);
			renderer.DrawIndexedTriangles(cube.indexes, Cube::INDEX_COUNT, cube.vertexes, Cube::VERT_COUNT);
		}
	}
}

Which looks like:

12/13/2020

Added texture support. With the way the renderering is currently set up, there is usually only room for a single texture at a time. Meaning, each time a different texture is used, it must be re-uploaded to vram and take the current texture's place.

Despite this, a texture atlas class is included to determine the available areas in VRam to upload the texture/keep track of the current vram use, however it isn't used (at least for this example). (I had to look up what a texture atlas was, but it's just a different name for a sprite sheet). This texture atlas in turn uses a custom/simple std::vector implementation called Array<>, which is interesting.

Besides the addition of the Texture and TextureAtlas classes, the renderer changes are mostly:

void SetTexture( const Texture& texture )
{
    // no need to change
    if ( &texture == currentTex )
    {
        return;
    }

    currentTex = (Texture*)&texture;
    // assert(currentTex->getPixels() != NULL);
    // assert(currentTex->getTexBuffer().address == uint(vramUserTextureStart));

    flushPipeline();
    //texSwitches++;

    // upload to GS ram
    const u32 width = currentTex->GetWidth();
    const u32 height = currentTex->GetHeight();
    const u32 psm = currentTex->GetPixelFormat();
    u8* pixels = (u8*)currentTex->GetPixels();

    qword_t* q = textureUploadPacket[frameIndex].GetQwordPtr();
    q = draw_texture_transfer( q, 
                               pixels, 
                               width, 
                               height, 
                               psm, 
                               vramUserTextureStart, 
                               width );
    q = draw_texture_flush( q );
    dma_channel_send_chain( DMA_CHANNEL_GIF, 
                            textureUploadPacket[frameIndex].GetQwordPtr(),
                            textureUploadPacket[frameIndex].GetDisplacement(q), 
                            0, 
                            0 );
    dma_wait_fast();
}

...

#define DRAW3D_PROLOGUE() \
BEGIN_DMA_TAG(currentFrameQwPtr); \
if (currentTex != nullptr) \
{ \
	setTextureBufferSampling(); \
} \
u64 * /*restrict*/ packetPtr = (u64 *)(draw_prim_start(currentFrameQwPtr, 0, &primDesc, &primColor))

...

void setTextureBufferSampling()
{
    // assert(currentTex != NULL);
    // assert(currentFrameQwPtr != NULL)
    // assert(currentTex->GetTexBuffer().address == u32(vramUserTextureStart));

    currentFrameQwPtr = draw_texture_sampling( currentFrameQwPtr, 
                                               0, 
                                               &currentTex->GetTexLod() );
    currentFrameQwPtr = draw_texturebuffer( currentFrameQwPtr, 
                                            0, 
                                            &currentTex->GetTexBuffer(), 
                                            &currentTex->GetTexClut() );
}

Then in main.cpp, you add:

// TODO - move this into CPP file or something
u32 Texture::vramUserTextureStart = 0;
...
renderer.SetPrimTextureMapping( true );
...
renderer.SetTexture( gTexture );
...

12/20/2020

Added basic 2D drawing, font drawing, and the ingame console. Now the TextureAtlas previously is used as the texture to store the font in. I think the builtin fonts are actual font files, as opposed to just images. There initially was a bug where I had made some small mistakes in the TextureAtlas code. Also, the 2D drawing seems to cause an error if you don't have enough commands sent or time spent or similar. I confirmed this in the original cubes demo code as well. Having the renderer draw "pre" doesn't work but anything at or above "pres" does

renderer.Begin2D();
{
    // doesn't work
	Vec2f pos = { 5.0f, 5.0f };
	Color4b textCol = { 255, 255, 255, 255 };
	renderer.DrawText(pos, textCol, FontManager::FONT_CONSOLAS_24, "Pre");

	rect.x = 10;
	rect.y = 50;
	rect.width = 100;
	rect.height = 100;
	color.r = 255;
	color.b = 0;
	color.g = 0;
	color.a = 255;
	renderer.DrawRectFilled( rect, color );
}
renderer.End2D();

vs.

renderer.Begin2D();
{
    // works
	Vec2f pos = { 5.0f, 5.0f };
	Color4b textCol = { 255, 255, 255, 255 };
	renderer.DrawText(pos, textCol, FontManager::FONT_CONSOLAS_24, "Pres");

	rect.x = 10;
	rect.y = 50;
	rect.width = 100;
	rect.height = 100;
	color.r = 255;
	color.b = 0;
	color.g = 0;
	color.a = 255;
	renderer.DrawRectFilled( rect, color );
}
renderer.End2D();

There is code to decompress the font and generate a bitmap image from their data. I should look at glampert's website to see if he discusses this. The code directs you via a comment to https://www.silverspaceship.com/inner/imgui/grlib/gr_extra.c, where a similar font decompression function is called grDecompressFont(). That however also doesn't document the expected input format. There is, however, a different font file in the same directory called "font_21", when, when added to the code does indeed work as well.


12/21/2020

Added the first person camera (minus the joystick integration). It's interesting to see the coordinate axis choices. In the current code, the camera looks towards positive z (the forward vector), the cubes are positioned around +20 z (they were positioned around -20 z previously, due to not having a camera and using an identity matrix as the view matrix), and moving the camera right takes a negative x direction (e.g. x -= 10 moves the camera right by 10)

The camera uses a system time counting object - GameTime - to calculate the delta and how much to move, via the clock() function. I'm curious to find out if the PS1 has something similar. I think the clock() function would be kernel/OS dependant, which I don't believe the PSX has.

Rotation is supported around the X axis (Pitch()) or the Y axis (Rotate()). The rotate function works by rotating each of the eye coordinate vectors (up, forward, right) by the input angle via basically 2D rotation matrix calculation. Pitch() is calculated by using a 3D rotation matrix around a generic axis vector, with in this case the axis being the right coordinate vector.

This time I ran on the actual PS2 just for fun:

12/26/2020

Untextured 3D model drawn. The model format is MD2, used Quake. Glampert lists that he obtained the models from here. Adding the textures should be pretty easy as well, just wanted to confirm the non-textured version worked first. Loading and drawing a model looks like this:

// generated via bin2c
#include "models/player_model.h"
#include "models/player_model_texture.h"
Md2Model playerMd2Model;
Md2AnimState playerAnimState; // not animated for now
//Texture playerTexture; // TODO
PS2Matrix playerModelMatrix;
// This pool of temporary vertexes is shared by all MD2 model instances.
// They are used to store the vertexes generated by animation blending.
static DrawVertex * tempMd2Verts = NULL;
static const u32 TEMP_MD2_VERT_COUNT   = 4096; // Default Quake MD2 max was 2048

static inline void initModel()
{
    const float scale = 0.025f;
    if (!playerMd2Model.InitFromMemory(player_model,
                                       size_player_model,
                                       scale))
    {
        printf("Failed to init model from memory\n");
        while (1);
    }
    // TODO
    // loadPermanentTexture(playerTexture, player_model_texture, size_player_model_texture, TEXTURE_FUNCTION_MODULATE);
    
    tempMd2Verts = (DrawVertex*)memalign(16, TEMP_MD2_VERT_COUNT*sizeof(DrawVertex));
    memset(tempMd2Verts, 0, TEMP_MD2_VERT_COUNT*sizeof(DrawVertex));
    
    playerAnimState.Clear();
    
    playerModelMatrix.makeIdentity();
}
static inline void drawModel()
{
    static Color4f colorTint = { 1.0f, 1.0f, 1.0f, 1.0f };
    Md2Model* model = &playerMd2Model;
    const u32 mdlVertCount = model->GetTriangleCount() * 3;
    if (mdlVertCount >= TEMP_MD2_VERT_COUNT)
    {
        printf("ERror = model vert count >= temp vert count\n");
        while (1);
    }
    
    model->AssembleFrame(/*playerAnimState.endFrame*/0,
                        256, 256, // player texture size width, height
                        colorTint,
                        tempMd2Verts);
    renderer.SetModelMatrix( playerModelMatrix );
    renderer.DrawUnindexedTriangles( tempMd2Verts, mdlVertCount );
}

Which gives us the result:

Another step is to try and reproduce the result on the PS1 as well

12/27/2020

Model drawn with textures as well now. This required loading the image (which was stored and compiled in memory as an array of bytes), then setting the renderer texture to said image before drawing:

static void loadPermanentTexture(Texture& texture,
                                 const u8* data,
                                 const u32 sizeBytes,
                                 const u8 textureFunction)
{
    ImageData img;
    if (!loadImageFromMemory(data,
                             sizeBytes,
                             img,
                             true)) // true = force rgba
    {
        printf("Failed to load texture from location %p\n", data);
        while (1);
    }
    // image should be r,g,b,a
    if (img.comps != 4)
    {
        printf("Converted image should be RGBA, but comps != 4\n");
        while (1);
    }

    // Texture isn't mipmapped
    lod_t lod;
    lod.calculation = LOD_USE_K;
    lod.max_level = 0;
    lod.mag_filter = LOD_MAG_LINEAR;
    lod.min_filter = LOD_MIN_LINEAR;
    lod.l = 0;
    lod.k = 0.0f;
    if (!texture.InitFromMemory(img.pixels,
                                TEXTURE_COMPONENTS_RGBA,
                                img.width, img.height,
                                GS_PSM_32,
                                textureFunction,
                                &lod))
    {
        printf("Failed to init image from memory!\n");
        while (1);
    }
    printf("Successfully loaded permanent texture\n");
}
...
Texture playerTexture;
...
static inline void initModel()
{
    const float scale = 0.025f;
    if (!playerMd2Model.InitFromMemory(player_model,
                                       size_player_model,
                                       scale))
    {
        printf("Failed to init model from memory\n");
        while (1);
    }
    loadPermanentTexture(playerTexture,
                         player_model_texture,
                         size_player_model_texture,
                         TEXTURE_FUNCTION_MODULATE);

    tempMd2Verts = (DrawVertex*)memalign(16, TEMP_MD2_VERT_COUNT*sizeof(DrawVertex));
    memset(tempMd2Verts, 0, TEMP_MD2_VERT_COUNT*sizeof(DrawVertex));

    playerAnimState.Clear();

    playerModelMatrix.makeIdentity();
}
static inline void drawModel()
{
    static Color4f colorTint = { 1.0f, 1.0f, 1.0f, 1.0f };
    Md2Model* model = &playerMd2Model;
    const u32 mdlVertCount = model->GetTriangleCount() * 3;
    if (mdlVertCount >= TEMP_MD2_VERT_COUNT)
    {
        printf("ERror = model vert count >= temp vert count\n");
        while (1);
    }

    model->AssembleFrame(/*playerAnimState.endFrame*/0,
                        256, 256, // player texture size width, height
                        colorTint,
                        tempMd2Verts);
    renderer.SetTexture( playerTexture );
    renderer.SetModelMatrix( playerModelMatrix );
    renderer.DrawUnindexedTriangles( tempMd2Verts, mdlVertCount );
}

01/01/2021

Animation with the MD2 model now working. It works by interpolating between the current model keyframe and the next model keyframe based on a floating point value between 0 and 1 (the percentage of the way through the anim). The current and next keyframe are updated based on the GameTime counter and the animation's set FPS (for the below image I set FPS to glampert's default MD2 fps of 7)

static void initAnimation(const StdMd2AnimId animId)
{
    u32 start, end, frameRate;

    Md2Model::GetStdAnim( animId, start, end, frameRate );
    playerAnimState.startFrame = start;
    playerAnimState.endFrame = end;
    playerAnimState.nextFrame = start + 1;
    playerAnimState.fps = /*frameRate*/MD2ANIM_DEFAULT_FPS;
}
static inline void drawModel()
{
    ...
    playerAnimState.Update(gTime.currentTimeSeconds, 
                           model->GetKeyframeCount());
    model->AssembleFrameInterpolated(playerAnimState.curFrame,
                                     playerAnimState.nextFrame,
                                     playerAnimState.interp,
                                     256, 256, // texture width, height
                                     colorTint,
                                     tempMd2Verts );
    renderer.SetTexture( playerTexture );
    renderer.DrawUnindexedTriangles( tempMd2Verts, mdlVertCount );
}

There are quite a few default MD2 animations available; so far I've only tried the first two:

// Standard MD2 animations, with predefined frame numbers and FPS.
enum StdMd2AnimId
{
    MD2ANIM_STAND,
    MD2ANIM_RUN,
    MD2ANIM_ATTACK,
    MD2ANIM_PAIN_A,
    MD2ANIM_PAIN_B,
    MD2ANIM_PAIN_C,
    MD2ANIM_JUMP,
    MD2ANIM_FLIP,
    MD2ANIM_SALUTE,
    MD2ANIM_FALL_BACK,
    MD2ANIM_WAVE,
    MD2ANIM_POINT,
    MD2ANIM_CROUCH_STAND,
    MD2ANIM_CROUCH_WALK,
    MD2ANIM_CROUCH_ATTACK,
    MD2ANIM_CROUCH_PAIN,
    MD2ANIM_CROUCH_DEATH,
    MD2ANIM_DEATH_FALL_BACK,
    MD2ANIM_DEATH_FALL_FORWARD,
    MD2ANIM_DEATH_FALL_BACK_SLOW,
    MD2ANIM_BOOM,

    // Number of entries in this enum. Internal use.
    MD2ANIM_COUNT
};

01/27/2021

Using third person camera. Ran into the issue where for some reason the program was crashing when the 2D console wasn't drawn. TODO - add a floor so we have some point of reference so it's not as confusing when we look and move around.

<-- Back to Home