Textures and Meshes

From dreamcast.wiki
Jump to navigation Jump to search

At this point, since we’re going to be using multiple textures from now on, it makes sense to make a texture manager class. This will hold and store our Textures for us, so we can easily look through them and delete them if necessary. Begin by making a new class in our folder, “texture_manager.cpp” and “texture_manager.h”. Don’t forget to add “texture_manager.o” to $(OBJS) in our Makefile.

Our Texture_Manager class will need to see KOS to get internal dreamcast types, such as pvr_ptr. We will begin by adding our standard KOS headers, as well as our standard IO and int headers. We will be dealing with png files, so we need to include png/png.h. We will also need to access maps for our texture container, and since we will be dealing with c-strings, we should include cstring. Our Texture_manager.h should look like this:

#ifndef TEXTURE_MANAGER_H
#define TEXTURE_MANAGER_H
#include <kos.h>                /* KalistiOS Dreamcast SDK */
#include <png/png.h>            /* Library to load png files */
#include <stdio.h>              /* Standard Input-Output */
#include <stdint.h>             /* Standard Integer types (uint_t, etc) */
#include <map>
#include <cstring>
class texture_manager
{ 
public:
    texture_manager();
};
#endif // TEXTURE_MANAGER_H

Internally, our class will need a map that pairs our texture ptrs and polygon contexts and other data objects to an enum. We will create and maintain an enum type that can be used to identify Textures as we use them in our source code. We will also need to create a Texture type that bundles the parts of a texture needed to render it together. Let’s create a new blank text file called Texture.h, and let’s create our TextureEnum and fill it with our background texture, and our font texture that we’ll use:

#ifndef TEXTUREENUMS_H
#define TEXTUREENUMS_H
#include "kos.h"
#include "stdint.h"
    enum TextureEnum {
        Background_png = 0,
    };


We want a global function that can provide the name of the Enum as a string when called. We’ll do this with a little trick. In C, if you want to use a function that is in another translation unit object, you can do so by telling that source that, when linked, there is an “external” function in another object by that name. We do this with the Extern keyname. We need that function declaration to actually exist as compiled code somewhere, too. We can accomplish both the forward declaration and the extern declaration at the same time. To do this, we create a “TextureEnums.cpp” file. This file defines a macro variable named TEXTUREENUM_CPP at the beginning of the file, and then undefs it at the end. This lets anything included within these brackets know they’re in that file. We can include our header within these brackets:

#define TEXTUREENUMS_CPP
#include "TextureEnums.h"
#undef TEXTUREENUMS_CPP /* TEXTUREENUMS_CPP */

Because we defined TEXTUREENUMS_CPP, our header file can behave different depending on if it’s in TEXTUREENUMS_CPP or not:

#ifdef TEXTUREENUMS_CPP
    const char* GetTextureString(TextureEnum In)
    {
        std::string TxEnum;
        switch(In)
        {
            case background_png : TxEnum = "background_png";
            break;
            case font_png: TxEnum = "font_png";
            break;
            case font_transparent_png : TxEnum = "font_transparent_png";
            break;
        }
    return TxEnum.c_str();
    }
#else
    extern const char* GetTextureString(TextureEnum In);
#endif
#endif // TEXTUREENUMS_H

This will define the function in TextureEnums.cpp, but in every other file, it’ll instead forward declare the external function. This function lets us map a const c-string return to a TextureEnum. Whenever we add a new TextureEnum, we should modify this part of the code to update the strings.


Let’s create our Texture container. When we rendered our texture in Main.cpp, every Texture needed a pvr_ptr, which pointed to the texture in VRAM. Each also needed a pvr_poly_ctx_t, which holds their polygon context for the texture. We can also store our pvr_ply_hdr_t which is our context compiled into a PVR object. It would be wise to also store the width and height of the Texture. We will also store our TextureEnum as TextureID, so our Texture can be reflexive, meaning it can identify itself. Our Texture container should be the following:

#ifndef TEXTURE_H
#define TEXTURE_H
#include "kos.h"
#include "stdint.h"
#include "TextureEnums.h"

    typedef struct _Texture_t
    {
        pvr_ptr m_Ptr;                  /* Pointer to VRAM */
        pvr_poly_ctx_t m_Context;       /* Texture Context settings */
        pvr_ply_hdr_t m_Header;         /* Texture PVR Header */
        uint32_t Width;                 /* Texture Width */
        uint32_t Height;                /* Texture Height */
        TextureEnum TextureID;
    } Texture_t;
#endif // TEXTUREENUMS_H


Now, include this file in our Texture_Manager.h:

#ifndef TEXTURE_MANAGER_H
#define TEXTURE_MANAGER_H
#include <kos.h>                /* KalistiOS Dreamcast SDK */
#include <png/png.h>            /* Library to load png files */
#include <stdio.h>              /* Standard Input-Output */
#include <stdint.h>             /* Standard Integer types (uint_t, etc) */
#include <map>
#include <cstring>
#include "TextureEnums.h"

Now in our class, let’s use a map container to hold our Texture_t. A map container lets you pair a value, our Texture_t, with a key, in this case a TextureEnum value. This will be our internal storage, so we’ll make it private: private:

    std::map<TextureEnum, Texture_t> Textures;

Now, let’s create a function to get a texture_t pointer from our internal texture map: public:

    texture_manager();
    Texture_t* GetTexture(TextureEnum In);

Let’s fill our these functions in our texture_manager.cpp. Our constructor will load our Textures into Map and assign them to our Enums:

texture_manager::texture_manager()
{
    Texture_t TempTxr;  /* This is a dummy Texture_t object we
                            reuse to build out our Texture map */
    /* Setup background.png */
    CompileContext("/rd/background.png", background_png, &TempTxr, 512, 512, PNG_NO_ALPHA, PVR_FILTER_BILINEAR);
    Textures[background_png] = TempTxr;
    /* Setup background.png */
    CompileContext("/rd/font.png", font_png, &TempTxr, 1024, 128, PNG_FULL_ALPHA, PVR_FILTER_NONE);
    Textures[font_png] = TempTxr;
}

Our GetTexture Function returns a pointer to the Texture according to the TextureEnum passed to it:

Texture_t *texture_manager::GetTexture(TextureEnum In)
{
    return &Textures[In];
}

We need to move our Render function to our GameState. In gamestate.h change the headers to:

#include "stdint.h"
#include <vector>
#include "EventEnums.h"
#include <kos.h>                /* KalistiOS Dreamcast SDK */
#include "texture_manager.h"

and in the class, under public, add:

public:
    GameState();
    ~GameState();
    void HandleEvents();
    void Render();
    uint32_t Done;                /* Is the game finished running? */   
    std::vector<Event_ControllerAction> ControllerEvents;
    const cont_state_t &GetPrevController() const;
    void SetPrevControllerButtons(uint32_t In);

Under Private, we need to add our Texture manager object and a function to make our GameState submit polygons to the Dreamcast PVR. Make our Private section look like this: private:

   cont_state_t PrevControllerState;
   texture_manager m_Textures;
   void SubmitPolygon(TextureEnum In);

Our Gamestate now holds our Textures and can draw to the screen. It loads textures into it’s texture atlus at object creation. Lets build out the Render() function in gamestate.cpp:

void GameState::Render()
{
   pvr_wait_ready();
   pvr_scene_begin();
       pvr_list_begin(PVR_LIST_OP_POLY);
           SubmitPolygon(background_png);
       pvr_list_finish();
       pvr_scene_finish();
}

This is our Draw function from Main.cpp transplanted over to the Gamestate class. It calls a new function called SubmitPolygon, which we’ll create next.

void GameState::SubmitPolygon(TextureEnum In)
{
       Texture_t* Texture = m_Textures.GetTexture(In);
       pvr_vertex_t vert;
       pvr_prim(&Texture->m_Header, sizeof(Texture->m_Header));
       vert.argb = PVR_PACK_COLOR(1.0f, 1.0f, 1.0f, 1.0f);
       vert.oargb = 0;
       vert.flags = PVR_CMD_VERTEX;
       vert.x = 0.0f;
       vert.y = 0.0f;
       vert.z = 1.0f;
       vert.u = 0.0f;
       vert.v = 0.0f;
       pvr_prim(&vert, sizeof(vert));
       vert.x = 640.0f;
       vert.y = 0.0f;
       vert.z = 1.0f;
       vert.u = 1.0f;
       vert.v = 0.0f;
       pvr_prim(&vert, sizeof(vert));
       vert.x = 0.0f;
       vert.y = 480.0f;
       vert.z = 1.0f;
       vert.u = 0.0f;
       vert.v = 1.0f;
       pvr_prim(&vert, sizeof(vert));
       vert.x = 640.0f;
       vert.y = 480.0f;
       vert.z = 1.0f;
       vert.u = 1.0f;
       vert.v = 1.0f;
       vert.flags = PVR_CMD_VERTEX_EOL;
       pvr_prim(&vert, sizeof(vert));
}

This is a function that submits vertices that fill the screen and have it draw a Texture specified by the input parameter. Our Gamestate object now can create and manage textures, as well as submit them to the PVR to draw. Let’s modify our Main.cpp to accommodate these changes. Firstly, we can remove the draw_back() and DrawFrame() functions from Main.cpp, as those are handled by our render() and submit_polygon() functions in gamestate. We can also remove “pvr_ptr_t back_tex” from the top of the file. We can remove the textures_init() function, as this is now handled by the gamestate.texture constructor.

This leaves our Main.cpp looking very bare-bones, which is what we want. Our Main.cpp should create an instance of our game object, then run it in a loop. This is our new Main.cpp:

#include <arch/gdb.h>           /* gdb debugger */
#include <kos.h>                /* KalistiOS Dreamcast SDK */
#include <kos/dbglog.h>         /* debug log */
#include "gamestate.h"
#include "maple_events.h"
/*** C Library forward declarations ***/
#ifdef __cplusplus
extern "C" {
#endif
    #include <zlib/zlib.h>                  /* Z-Lib library, used to decompress gzip files */
    extern int zlib_getlength(char*);
#ifdef __cplusplus
}
#endif
int main(int argc, char **argv) {
    /* init systems  */
    gdb_init();
    pvr_init_defaults();
    GameState Game;
    MapleEvents Maple;
    /* Game Loop */
    while(!Game.Done)
    {    
        Maple.PollControllers(&Game);
        Game.HandleEvents();
        Game.Render();
    }
   return 0;
}

Our game now handles inputs and can quit by pressing start. Run it and we should see our texture drawn to the screen. We now have our Texture rendering system in place, but our rendering code still manually hardcodes our vertex and their positiions. Just as we broke our textures out into a texture type and manager, so should we break out our vertex mesh shapes. Let’s start doing that by creating a mesh manager class, in the same style as our texture manager class. Create two blank text files called mesh_manager.cpp and mesh_manager.h. Inside mesh_manager.h, put the following:

#ifndef MESH_MANAGER_H
#define MESH_MANAGER_H

#include <stdio.h>              /* Standard Input-Output */
#include <stdint.h>             /* Standard Integer types (uint_t, etc) */
#include <map>
#include <cstring>

class mesh_manager
{
public:
    mesh_manager();

};
#endif // MESH_MANAGER_H

That is our standard class declaration. Now, just as we needed to create a texture_t type to hold all the information we need about a texture object for our texture manager, so will we create a pool of vertices that make up a mesh for our mesh manager to take care of. Let’s start by creating a mesh class. Create a blank text file for mesh.cpp and mesh.h. Inside of mesh.h:

#ifndef MESH_H
#define MESH_H
#include "Texture.h"
#include "cstring"
#include "stdio.h"
#include "stdint.h"
class Mesh
{
    public:
        Mesh();
    private:
        pvr_vertex_t* VertexArray;         /* pointer to our vertex pool that makes up this mesh */
};
#endif

We are going to need to know our texture enums to map to our mesh so we need Texture.h. We also will need our standard IO and integer types.

We have a public constructor and a private pvr_vertex_t* pointer called verts. We will allocate a pool on the heap in our class, and this will be the head of the array of vertices.

We are going to need to keep some other information about our Mesh. We want to know how many vertices are in our verts array, and we want to know the size of our array that we’ve allocated, so we don’t write outside our boundry. We’ll need a MeshEnum variable so the Mesh will be reflexive of it’s own type. Let’s add all this under public, as other parts of our program might need to change those variables:

       uint32_t m_VtxCount;           /* number of vertices in this mesh */
       uint32_t m_Size;              /* Amount of Space allocated */
       MeshEnum e_MeshID;             /* My own Mesh type */

We need some functions to allocate and get and set data in our vertex array. Let’s create a public Allocate function, along with public GetVertex and SetVertex:

       pvr_vertex_t* GetVertex(uint32_t Idx);
       void SetVertex(pvr_vertex_t Input, uint32_t Idx);
       void AllocateVertexArray(uint32_t Count);

SetVertex will change an already existing member of our Array. Let’s use another function, AddVertex, to actually add a new vertex to the array:

       void AddVertex(pvr_vertex_t Input)

The way our Mesh will work is that we’ll allocate space for X number of vertices. A variable in our container will keep track of how many vertices have actually been written to the array. We’ll call this variable Vtx_Idx, and keep it in private:

       uint32_t Vtx_Idx;

When our Mesh object is created, this Vtx_Idx will be set to 0. Every time AddVertex is called, this Vtx_Idx is increased. The value of Vtx_Idx is used to set and write the new vertex in the vertex array. That is where the name is derived from, “Vertex Index” which is the index we are writing at in the vertex array. This value also doubles as the number of vertices in the array. We will only allow a new vertex to be added to our mesh if Vtx_Idx is less than Size, which is how much we have allocated.

Let’s start by filling our our constructor in Mesh.cpp:

#include "mesh.h"
Mesh_t::Mesh_t()
{
        ptr_VertexArray = NULL;
        m_VtxCount = 0;
        Vtx_Idx = 0;
        e_MeshID = MESH_NULL;
}

We set our verts pointer to NULL, and then set our idx and num_vtx counters to 0. We set our Texture enum to TX_NULL, our special enum value for no-texture.

Let’s next set up our getter, setter, and adder:

       pvr_vertex_t* Mesh::GetVertex(uint32_t Idx)
       {
           if( m_VtxCount > 0)
           {
               if(Idx < m_VtxCount)
               {
                   return verts[Idx];
               }
           }
           return NULL;
       }
       
       void Mesh::SetVertex(pvr_vertex_t Input, uint32_t Idx)
       {
           if(Idx < m_VtxCount)
           {
               verts[Idx] = Input;
           }
       }
       void Mesh::AddVertex(pvr_vertex_t Input)
       {
           verts[Vtx_Idx] = Input;
           Vtx_Idx++;    
	     m_VtxCount = Vtx_Idx;
       }

Our Getter has a guard check to make sure we have a vertex added to the array, and that the asked index is less than the number of submitted vertices. The Setter has the same guard. The Add Vertex function adds to the map, the convention is to use the numeric variable Vtx_Idx as the input for the key via the brackets. You set the value with the equal sign. This creates a key-value pair in the map container. We then increase the Vtx_Idx counter, so it points to the next empty space in the Vertex array. m_VtxCount is set to reflect Vtx_Idx, because m_VtxCount is public-facing.

Now let’s write a function to allocate our Vertex array:

void Mesh::AllocateVertexArray(uint32_t Count)
{
   VertexArray = (pvr_vertex_t*)malloc(Count * sizeof(pvr_vertex_t))
   m_Size = Count;
}

This calls malloc, the function in c and c++ that reserves space. It takes in a number representing the number of bytes to reserve. We take in a number called count as a function parameter, and use that number times the dize of the pvr_vertex_t type, to figure out how many bytes we need to store our data. This allocated memory is pointed to using our VertexArray pointer. Our mesh m_Size member is set to Count, the number of pvr_vertex_t elements we have allocated space for.

With this, we have our Mesh object. A mesh is any array of Vertices we want to group together under one texture to render. When we rendered our texture to the screen before, the full screen image, we used 4 vertices in a strip of 2 polygon triangles to draw it. Those 4 vertices are our mesh, we could call that a Quad Mesh. In fact, let’s do just that. We’ll keep an enum of our Meshes, just like we do with our Textures. Let’s create a blank header file called MeshEnums.h and add the following:

#ifndef MESHENUMS_H
#define MESHENUMS_H
enum MeshEnum {
   e_Quad = 0,
   MESH_NULL
};
#endif // MESHENUMS_H

This sets us up with an enum type, MeshEnum, which we can use to distinguish between mesh objects we define. We already have a type called Quad in our list, let’s build it out. We will create it in our Mesh_Manager then have it ready for when we need to call it. Let’s add our MeshEnums.h file to our Mesh_Manager.h header file:

#ifndef MESH_MANAGER_H
#define MESH_MANAGER_H
#include "kos.h"
#include <stdio.h>              /* Standard Input-Output */
#include <stdint.h>             /* Standard Integer types (uint_t, etc) */
#include <map>
#include "mesh.h"
#include "MeshEnums.h"
class mesh_manager
{
public:
   mesh_manager();
private:
   std::map<MeshEnum, Mesh_t> Meshes;
};
#endif // MESH_MANAGER_H

Now, let’s have our Mesh_Manager constructor make our quad mesh for us, as a default one when our program begins. That way we can always have it on hand to easily render textures to the screen. Let’s allocate some vertex space and create our mesh object in the constructor in mesh_manager.cpp:

#include "mesh_manager.h"
mesh_manager::mesh_manager()
{
   Mesh_t T_Mesh;                   /* A temporary Mesh_t object we will fill out to add to our mesh vector */
   pvr_vertex_t T_Vertex;      /* A temporary vertex object, we will fill out to add to our T Mesh's vertex array */
   
   T_Mesh.AllocateVertexArray(4);   /* We can hold 4 vertices in this mesh */
   T_Vertex.argb = PVR_PACK_COLOR(1.0f, 1.0f, 1.0f, 1.0f);
   T_Vertex.oargb = 0;
   T_Vertex.flags = PVR_CMD_VERTEX;
   
}

We begin by creating a temporary T_Mesh object of Mesh_t type. This is the object we will interact with and configure, which will be submitted to the Mesh vector in our manager. We next create a temporary T_Vertex object, of pvr_vertex_t type. This is the vertex object that we will configure, which will be uploaded to the T_Mesh vertex array.

We begin by allocating 4 vertices in T_Mesh. Next, we configure our argb and oargb values in our T_Vertex, and set a flag indicating this is a PVR_CMD_VERTEX. That means this is just one in a strip of vertices, not the end of the strip.

   T_Vertex.x = 0.0f;
   T_Vertex.y = 0.0f;
   T_Vertex.z = 1.0f;
   T_Vertex.u = 0.0f;
   T_Vertex.v = 0.0f;
   T_Mesh.AddVertex(T_Vertex);

Our next step is to set the XYZ position and the UV coordinates of the vertex. This is the upper left corner of the quad. We use 1.0 as the constant Z value. After we set the vertex position and uv coordinates, we push it onto our T_Mesh Vertex Array. We can then do the same for the other 3 vertices:

   T_Vertex.x = 1.0f;
   T_Vertex.y = 0.0f;
   T_Vertex.z = 1.0f;
   T_Vertex.u = 1.0f;
   T_Vertex.v = 0.0f;
   T_Mesh.AddVertex(T_Vertex);
   T_Vertex.x = 0.0f;
   T_Vertex.y = 1.0f;
   T_Vertex.z = 1.0f;
   T_Vertex.u = 0.0f;
   T_Vertex.v = 1.0f;
   T_Mesh.AddVertex(T_Vertex);
   T_Vertex.x = 1.0f;
   T_Vertex.y = 1.0f;
   T_Vertex.z = 1.0f;
   T_Vertex.u = 1.0f;
   T_Vertex.v = 1.0f;
   T_Vertex.T_Mesh.AddVertex = PVR_CMD_VERTEX_EOL;
   T_Mesh.AddVertex(T_Vertex);
   
   T_Mesh.e_MeshID = e_Quad;


A few things to note here. In our previous example where we drew the background texture to the screen, our vertex positions spanned from 0 to 640 in the X range, and 0 to 480 in the Y range. Yet in this mesh, our vertex spans from 0 to 1 in both ranges. This is because this will be a generic mesh. We will create a transformation matrix that will allow us to resize the mesh on the fly when we need it, that way we can reuse this mesh to draw any quad shape we want to the screen.

The second thing to note is that in the final vertex, we change the flag to PVR_CMD_VERTEX_EOL. This is a command to tell the PVR that this vertex is the tail end of the vertex chain, meaning this group of 4 Vertices are grouped as one Vertex Strip.

Finally, we set the enum inside this Mesh object to e_Quad, so our object can self-identify. With all this done, we are ready to add this to our Mesh_Manager object:

   Meshes[e_Quad] = T_Mesh;
}

This creates a hash item in our Meshes Map for e_Quad, which contains a copy of our T_Mesh object. We now have a Mesh in our Mesh_Manager object that we can easily call to draw quads with. Let’s put an instance of our Mesh_Manager in our Gamestate class. First, we need to add our header to gamestate.h:

#ifndef GAMESTATE_H
#define GAMESTATE_H
#include "stdint.h"
#include <vector>
#include "EventEnums.h"
#include <kos.h>                /* KalistiOS Dreamcast SDK */
#include "texture_manager.h"
#include "mesh_manager.h"

Next, in gamestate.h, inside the class Gamestate {} declaration, add this under private:

private:
    cont_state_t PrevControllerState;
    
    texture_manager m_Textures;
    void SubmitPolygon(TextureEnum In);
    mesh_manager m_Meshes;
};
#endif // GAMESTATE_H

Now let’s try using our mesh object from our mesh manager. In Gamestate.cpp, in the SubmitPolygon function, let’s have it call and use our Mesh_Manager:

void GameState::SubmitPolygon(TextureEnum Tex_In, MeshEnum Mesh_In)
{
   /* Texture Mapping */
       Texture_t* Texture = m_Textures.GetTexture(Tex_In);
       pvr_prim(&Texture->m_Header, sizeof(Texture->m_Header));
       
   /* Vertex Submission */        
       pvr_vertex_t  T_vertex;
       pvr_vertex_t* Vertex_ptr;
       for(int i = 0; i < m_Meshes.Meshes[Mesh_In].m_VtxCount; i++)
       {
           /* Get the vertex */
           Vertex_ptr = m_Meshes.Meshes[Mesh_In].GetVertex(i);
       
           /* Map all the information */
           T_vertex.flags = Vertex_ptr->flags;
           T_vertex.x = Vertex_ptr->x;
           T_vertex.y = Vertex_ptr->y;
           T_vertex.z = Vertex_ptr->z;
           T_vertex.u = Vertex_ptr->u;
           T_vertex.v = Vertex_ptr->v;
           T_vertex.argb = Vertex_ptr->argb;
           T_vertex.oargb = Vertex_ptr->oargb;
       
           /* Submit data */
           pvr_prim(&T_vertex, sizeof(T_vertex));
       }
}

This is basically the same logic as before, only now the vertex is being submitted in a loop. We set the for-loop to repeat for as many vertices are in our mesh, as called by m_Meshes.Meshes[Mesh_In].m_VtxCount. A temporary vertex T_Vertex is then mapped to the current vertex in the Vertex Array for that mesh, and then T_Vertex is submitted to the dreamcast PVR to render. We can select which Mesh to render when using the function by using a Mesh_In enum, which is set as a second parameter for “Submit Polygon.” There is one problem, however: Our XY positions for the corners of our quad are 1 pixel big. We need to do math on them to make them resize the way we want it. Let’s clone our SubmitPolygon function, and make a “SubmitQuad” function:

void GameState::SubmitQuad(TextureEnum Tex_In, uint32_t X_In, uint32_t Y_In, uint32_t W_In, uint32_t H_In)
{
   /* Texture Mapping */
       Texture_t* Texture = m_Textures.GetTexture(Tex_In);
       pvr_prim(&Texture->m_Header, sizeof(Texture->m_Header));
       
   /* Vertex Submission */        
       pvr_vertex_t  T_vertex;
       pvr_vertex_t* Vertex_ptr;
       
       for(int i = 0; i < m_Meshes.Meshes[e_Quad].m_VtxCount; i++)
       {
           /* Get the vertex */
           Vertex_ptr = m_Meshes.Meshes[e_Quad].GetVertex(i);
       
           /* Map all the information */
           T_vertex.flags = Vertex_ptr->flags;
           T_vertex.x = X_In + (Vertex_ptr->x * W_In);
           T_vertex.y = Y_In + (Vertex_ptr->y * H_In);
           T_vertex.z = Vertex_ptr->z + RenderDepth;
           T_vertex.u = Vertex_ptr->u;
           T_vertex.v = Vertex_ptr->v;
           T_vertex.argb = Vertex_ptr->argb;
           T_vertex.oargb = Vertex_ptr->oargb;
       
           /* Submit data */
           pvr_prim(&T_vertex, sizeof(T_vertex));
       }
           RenderDepth += 0.01;
}


This is similar to the old SubmitPolygon function, but the x and y positions ot T_Vertex are altered before drawing. This function is only for drawing Quads, so we don’t need it to take in a MeshEnum parameter, we already know it’ll use the e_Quad value. Instead, we take in an X, Y, W, and H parameter. This is the position and dimensions of the Quad we are drawing. When we submit our vertices, we do some math to change the value of T_Vertex before it’s submitted.

We are using a new variable called RenderDepth and adding it to the Z position in the Vertex. This is because rendering two Quads in the same position causes Z-Fighting, so we need to offset every call to this Quad function by a small amount each time. Let’s create this variable in Gamestate.h:

class GameState
{
public:
   GameState();
   ~GameState();
   void HandleControllerEvents();
   void Render();
   void ClearEvents();
   uint32_t Done;                                              /* Is the game finished running? */
   std::vector<Event_ControllerAction> ControllerEvents;
   const cont_state_t &GetPrevController() const;
   void SetPrevControllerButtons(uint32_t In);
   
   float Render_Depth;

Every time we call the Render function in Gamestate, we need to reset our Render_Depth:

void GameState::Render()
{
   static uint8_t Timer = 0;
   pvr_wait_ready();
   RenderDepth = 0;
   pvr_scene_begin();
       pvr_list_begin(PVR_LIST_OP_POLY);
       Timer++;
           SubmitQuad(background_png, 0, 0, 640, 480);
           if(Timer & 1)
           {
               SubmitQuad(background_png, 20, 20, 200, 140);
               SubmitQuad(background_png, 500, 98, 90, 90);
           }
           SubmitQuad(background_png, 200, 20, 150, 150);
       pvr_list_finish();
       pvr_scene_finish();
}

First, we add our X_In and Y_In positions to our X and Y vertex positions respectively. This lets us scroll the quad with those two values. The Vertex X and Y positions are also added the Width and Height values provided, but only when the Vertex X and Y are 1 and not 0. This is why we set our Vertex positions to 1 instead of 640 and 480 in our mesh constructor, so they can be used as toggles for these additional height and width calculations.

Don’t forget to add a declaration for our SubmitQuad function in our gamestate header. We have also changed our SubmitPolygon function to take in a MeshEnum parameter so we can choose which mesh to render, so let’s make these changes:

class GameState
{
public:
   GameState();
   ~GameState();
   void HandleControllerEvents();
   void Render();
   void ClearEvents();
   uint32_t Done;                                              /* Is the game finished running? */
   std::vector<Event_ControllerAction> ControllerEvents;
   const cont_state_t &GetPrevController() const;
   void SetPrevControllerButtons(uint32_t In);
private:
   cont_state_t PrevControllerState;
   
   texture_manager m_Textures;
   void SubmitPolygon(TextureEnum In);
   void SubmitQuad(TextureEnum Tex_In, uint32_t X_In, uint32_t Y_In, uint32_t W_In, uint32_t H_In);
   mesh_manager m_Meshes;
};

We also need to add our new source files to our Makefile under $(OBJS): OBJS = game.o romdisk.o gamestate.o maple_events.o texture_manager.o mesh.o mesh_manager.o

With these changes set, we can now tell our program to draw a texture anywhere in any size on the screen, just by referring to the TextureEnum. Let’s do so in our Gamestate class. In our Render function, let’s call our SubmitQuad function instead. Let’s use custom X,Y,W, and H settings:

void GameState::Render()
{
   pvr_wait_ready();
   pvr_scene_begin();
       pvr_list_begin(PVR_LIST_OP_POLY);
           SubmitQuad(background_png, 20, 20, 200, 140);
       pvr_list_finish();
       pvr_scene_finish();
}

Let’s run and see what happens! We can now draw textures to the screen in any position or shape we want! Let’s go crazy and do it multiple times!

void GameState::Render()
{
   pvr_wait_ready();
   RenderDepth = 0;
   pvr_scene_begin();
       pvr_list_begin(PVR_LIST_OP_POLY);
           SubmitQuad(background_png, 0, 0, 640, 480);
           SubmitQuad(background_png, 20, 20, 200, 140);
           SubmitQuad(background_png, 500, 98, 90, 90);
           SubmitQuad(background_png, 200, 300, 950, 950);
       pvr_list_finish();
       pvr_scene_finish();
}

We now have a pretty good base tool which we can modify to start rendering text. These mesh and texture classes and their respective managers will pay off greatly down the line. Just remember that for each mesh and texture we create, we need to modify their Enum header files to indicate their creation and enable their usage.

Let’s have our Texture_manager load our font texture. First, we need to create an entry in our TextureEnum.h:

enum TextureEnum {
   background_png = 0,
   font_png,   /* = 1 */
   TX_NULL
};

Let’s create a function to compile our Texture context for us:

void CompileContext(char* Path, TextureEnum TxrID, Texture_t* TempTxr, uint32_t W, uint32_t H, uint32_t PngMode, uint32_t Filtering)
{
TempTxr.Width = W;
   TempTxr.Height = H;
   TempTxr.TextureID = TxrID;
   TempTxr.m_Ptr = pvr_mem_malloc(W * H * 2);
   png_to_texture(Path, TempTxr.m_Ptr, PngMode);
   /* Set our Texture Type */

    pvr_poly_cxt_txr(&TempTxr->m_Context,        /* Dest Context to write to */
                      PVR_LIST_OP_POLY,        /* Polygon Bin Type */
                      PVR_TXRFMT_RGB565,       /* Texture Format */
                      W,                     /* Texture Width */
                      H,                     /* Texture Height */
                      TempTxr->m_Ptr,                /* Texture Pointer */
                      Filtering);    /* Filtering */
   pvr_poly_compile(&TempTxr.m_Header, &TempTxr.m_Context);
   Textures[TxrID] = TempTxr;
}

This is the same code we used to compile our Texture Context before. We set the dest, set the polygon list to use, set the texture format, set width, height, a texture pointer, and a filter flag.

Now, let’s have our Texture_manager constructor create a font texture for us:

texture_manager::texture_manager()
{
   /* Setup background.png */
   CompileContext("/rd/background.png", background_png, background_png, 512, 512, PNG_NO_ALPHA, PVR_FILTER_BILINEAR);
   /* Setup font.png */
   CompileContext("/rd/font.png", font_png, font_png, 1024, 128, PNG_FULL_ALPHA, PVR_FILTER_NONE);

}