Transparent Textures

From dreamcast.wiki
Jump to navigation Jump to search

Up till now, we’ve been relying on our attached console to have our dreamcast talk back to us. Now that we can map textures to the screen, let’s use that to create a function that can print text through the Dreamcast. We will learn how to use translucent textures in the process as well.

Our font will take the form of a Texture Atlus. A texture atlus is a giant texture with lots of smaller textures in it. You select which texture you use by changing the UV coordinates of the vertices.

The general idea behind our text rendering code will rely on how ASCII works. Our text will be a c-style string, which is an array of bytes, where each byte is a single character. These bytes map to characters using an agreed-upon code called ASCII. We will take the numeric value of the characters in our C-style string and map them to positions on our font.

Our font will come in two sizes – 16x16 pixel ‘Large’ font, and 8x8 pixel ‘small’ font. We will map all characters A through Z, in caps, plus 0-9, plus the symbols mapped to 0-9, as well as a few punctuation marks and brackets. We will draw the fonts in a row in a png. Calculated out, our font will be 1024 pixels wide, which is the maximum amount the Dreamcast can have. Each font character needs to be at least 16x16 pixels big. I settled on my final font texture size being 1024x128 pixels big. The dimensions of the texture need to be a power of 2. 128 pixels big gives me eight rows of 16 pixel tall cells for font characters, where each row is a different font.

By looking up the ASCII character codes, we can find the order our characters come in. We are mapping 64 characters, from ASCII code 32 to 96.

I have provided a sample font.png, you can use it or make your own. The 8x8 fonts map to the upper left corner of a 16x16 grid cell. When you have your font made, place it in our romdisk folder so we can use it.

Now let’s try drawing our font texture first. In our Texture_manager constructor, let’s have it build our font texture:

   /* Setup background.png */
   CompileContext("/rd/font.png", font_png, &TempTxr, 1024, 128, PNG_NO_ALPHA, PVR_FILTER_NONE);
   Textures[font_png] = TempTxr;

Make sure we add font_png to our TextureEnums.h:

#ifndef TEXTUREENUMS_H
#define TEXTUREENUMS_H
enum TextureEnum {
   background_png = 0,
   font_png,   /* = 1 */
   TX_NULL
};
#endif // TEXTUREENUMS_H

Now in our GameState.Render() function, let’s submit a quad to draw with font_png. Our Font texture size is 1024x128:

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(font_png, 0, 0, 1024, 128);
       pvr_list_finish();
       pvr_scene_finish();
}

Let’s run it and see if it works. We should see our background with our font texture drawn in front of it. It is much larger than the screen, so it goes to the right. Currently, we are drawing the font with no transparency, let’s change that. To do that, we will need to change our definition in texture_manager constructor:

/* Setup font.png */
   CompileContext("/rd/font.png", font_png, font_png, 1024, 128, PNG_FULL_ALPHA, PVR_FILTER_NONE);

We need to edit our font.png texture to have transparent pixels. Open it up in gimp, and create a new layer with a transparent background. Put it below our font.png layer. Select our font.png layer to work in. Now, on the menu bar in gimp, click “select” -> “By Color” and click on the pink color in the font image. Press delete, this will turn the pixels that used to be pink into transparent pixels.

Now, we need to make sure we have 16-bit precision. Click Image-> Precision -> 16 bit integer. Now we need to export our image. Click File -> Export As, and in the dialog box, name it font_transparent.png in our romdisk folder. In the png export dialog, make sure “save color values from transparent pixels” is checked.

We need to add a new value for our new texture in our TextureEnum.h file:

#ifndef TEXTUREENUMS_H
#define TEXTUREENUMS_H
enum TextureEnum {
   background_png = 0,
   font_png,   /* = 1 */
   font_transparent_png,
   TX_NULL
};
#endif // TEXTUREENUMS_H

Now, we need to make our texture_manager() constructor call our new transparent font texture:

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, 512, 512, PNG_NO_ALPHA, PVR_FILTER_BILINEAR);

   /* Setup font.png */
   CompileContext("/rd/font.png", font_png, 1024, 128, PNG_FULL_ALPHA, PVR_FILTER_NONE);

/* Setup font_transparent.png */
   CompileContext("/rd/font_transparent.png", font_transparent_png, 1024, 128, PNG_FULL_ALPHA, PVR_FILTER_NONE);

}

Since this png has transparent pixels, we change our flag to PNG_FULL_ALPHA. This is a font texture, so we don’t really want any filtering, so we use PVR_FILTER_NONE. PNG_FULL_ALPHA means it needs to be drawn in our Translucent poly bin, currently we only draw in our opaque one. Let’s change it so it draws with the right bin, to do that we need to change our submission code:

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);
       pvr_list_finish();
       pvr_list_begin(PVR_LIST_TR_POLY);
           SubmitQuad(font_png, 0, 0, 1024, 128);
       pvr_list_finish();
       pvr_scene_finish();
}

We now have two render lists. We first open our PVR_LIST_OP_POLY bin for Opaque polygons, then submit our quad for the background_png texture. Once we’re done submitting it, we close our PVR_LIST_OP_POLY bin, then open our PVR_LIST_TR_POLY bin list. This list holds vertices for transparent polygons. We need to change our ContextCompile code in our Texture_manager.cpp class to use the right Poly list when compiling the texture header:

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 */
   uint32_t Texture_Fmt;
   pvr_list_t Poly_List;
   switch(PngMode)
   {
       /* Our PNG has 4-bit alpha channel, Translucent */
       case PNG_FULL_ALPHA:
       {
           Texture_Fmt = PVR_TXRFMT_ARGB4444;
           Poly_List = PVR_LIST_TR_POLY;
       }
       break;
       /* Our PNG has 1-bit alpha channel, Punch-thru */
       case PNG_MASK_ALPHA:
       {
           Texture_Fmt = PVR_TXRFMT_ARGB1555;
           Poly_List = PVR_LIST_PT_POLY;
       }
       break;
       /* Our PNG has no alpha channel */
       default:   /* PNG_NO_ALPHA, Opaque */
       {
           Texture_Fmt = PVR_TXRFMT_RGB565;
           Poly_List = PVR_LIST_OP_POLY;
       }
       break;
   }
   pvr_poly_cxt_txr(&TempTxr->m_Context,        /* Dest Context to write to */
                      Poly_List,        /* Polygon Bin Type */
                      Texture_Fmt,       /* 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;

}

We use a variable called Poly_list of type pvr_list_t. We set it to PVR_LIST_TR_POLY if we have PNG_FULL_ALPHA using PVR_TXRFMT_ARGB4444. We use PVR_LIST_PT_POLY if we have PNG_MASK_ALPHA using PVR_TXRFMT_ARGB1555. We use PVR_LIST_OP_POLY if we have PNG_NO_ALPHA using PVR_TXR_FMT_RGB565. We substitute this variable in our pvr_poly_cxt_txr function. Now, when we render our font texture, our transparent pixels will actually be transparent.

Let’s add a guard so our Render() function won’t draw a mesh with vertices in the wrong list, just in case. In our gamestate.h header file, let’s add a new private variable called OpenPolyList of type pvr_list_t:

private:
   cont_state_t PrevControllerState;
   texture_manager m_Textures;
   void SubmitPolygon(TextureEnum Tex_In, MeshEnum Mesh_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;
   pvr_list_t OpenPolyList;
};
#endif // GAMESTATE_H

This variable will hold a state indicating when PVR poly bin we are submitting to. Let’s change our Render code to indicate this:

void GameState::Render()
{
   pvr_wait_ready();
   RenderDepth = 0;
   pvr_scene_begin();
   
       /* Opaque Polygons */
       pvr_list_begin(PVR_LIST_OP_POLY);
           OpenPolyList = PVR_LIST_OP_POLY
           SubmitQuad(background_png, 0, 0, 640, 480);
           OpenPolyList = 0;
       pvr_list_finish();
       
       /* Translucent Polygons */
       pvr_list_begin(PVR_LIST_TR_POLY);
           OpenPolyList = PVR_LIST_TR_POLY
           SubmitQuad(font_png, 0, 0, 1024, 128);
                       OpenPolyList = 0;
       pvr_list_finish();
       
   pvr_scene_finish();
}

Now, when we open a list, it’ll set OpenPolyList, and when we close it, we’ll clear it with 0. Our SubmitQuad function can now check if the OpenPolyList matches the Context in our Texture:

void GameState::SubmitQuad(TextureEnum Tex_In, uint32_t X_In, uint32_t Y_In, uint32_t W_In, uint32_t H_In)
{
   if(OpenPolyList == m_Textures.GetTexture(Tex_In)->m_Context.list_type)
   {
       /* 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.Get(e_Quad)->m_VtxCount; i++)
           {
               /* Get the vertex */
               Vertex_ptr = m_Meshes.Get(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.1;
   }
}

Our Texture now won’t draw if it’s in the wrong bin. Let’s test this by intentionally putting our font texture in the opaque bin despite it being a transparent texture:

void GameState::Render()
{
   pvr_wait_ready();
   RenderDepth = 0;
   pvr_scene_begin();
   
       /* Opaque Polygons */
       pvr_list_begin(PVR_LIST_OP_POLY);
           OpenPolyList = PVR_LIST_OP_POLY
               SubmitQuad(background_png, 0, 0, 640, 480);
               SubmitQuad(font_png, 0, 0, 1024, 128);
           OpenPolyList = 0;
       pvr_list_finish();
       
       /* Translucent Polygons */
       pvr_list_begin(PVR_LIST_TR_POLY);
           OpenPolyList = PVR_LIST_TR_POLY
               //SubmitQuad(font_png, 0, 0, 1024, 128);
           OpenPolyList = 0;
       pvr_list_finish();
       
   pvr_scene_finish();
}

If we do this, then our font_png quad won’t render, and our console will output an error message letting us know. Let’s create a second font texture, this time fully transparent. In our texture_manager constructor:

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, 512, 512, PNG_NO_ALPHA, PVR_FILTER_BILINEAR);

   /* Setup font.png */
   CompileContext("/rd/font.png", font_png, 1024, 128, PNG_FULL_ALPHA, PVR_FILTER_NONE);
   
   /* Setup font_transparent.png */
   CompileContext("/rd/font_transparent.png", font_transparent_png, 1024, 128, PNG_FULL_ALPHA, PVR_FILTER_NONE);

}

make sure we have a TextureEnum for font_transparent_png:

#ifndef TEXTUREENUMS_H
#define TEXTUREENUMS_H
enum TextureEnum {
   background_png = 0,
   font_png,   /* = 1 */
   font_transparent_png,
   TX_NULL
};
const char* GetTextureEnumString(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();
}
#endif // TEXTUREENUMS_H

Let’s have SubmitQuads print out an error if we submit a vertex to the wrong bin list. We want it to be able to tell us which texture we called that was incompatible, along with the format of the texture and the OpenPolyList we are working on. We already have a function to turn our TextureEnums into a string, let’s create a quick function to turn our PolyBinList into a string. In TextureEnums.h, add:

#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();
   }
   const char* GetPolyBinString(pvr_list_t Input)
   {
       std::string TxFmt;
       switch(Input)
       {
           case PVR_LIST_OP_MOD : TxFmt = "Opaque Modifier";
           break;
           case PVR_LIST_TR_POLY: TxFmt = "Transparent";
           break;
           case PVR_LIST_TR_MOD : TxFmt = "Transparent Modifier";
           break;
           case PVR_LIST_PT_POLY: TxFmt = "Punch-Thru";
           break;
           case PVR_LIST_OP_POLY: TxFmt = "Opaque";
           break;
       }
       return TxFmt.c_str();
   }
#else
   extern const char* GetTextureString(TextureEnum In);
   extern const char* GetPolyBinString(pvr_list_t Input);
#endif

We now have a function called GetPolyBinString which can turn a pvr_list_t into a string to print out. Now, let’s create a string to print out our error in our submitquad function. We need to store a flag in our Texture file to let us know if the error has been printed once before, otherwise our error will print out every loop when we render. We’ll call this Warning in our texture_t struct in texture.h:

#ifndef TEXTURE_H
#define TEXTURE_H
#include "kos.h"
#include "stdint.h"
#include "TextureEnums.h"
   typedef struct _Texture_t
   {
       pvr_ptr_t m_Ptr;                  /* Pointer to VRAM */
       pvr_poly_cxt_t m_Context;       /* Texture Context settings */
       pvr_poly_hdr_t m_Header;         /* Texture PVR Header */
       uint32_t Width;                 /* Texture Width */
       uint32_t Height;                /* Texture Height */
       TextureEnum TextureID;
       uint8_t RenderWarning;                /* Flag letting us know if an error msg has printed once */
   } Texture_t;
#endif // TEXTUREENUMS_H

Now let’s set it so our Texture_t has a warning of 0 when created in our Texture_manager compilerContext function:

void texture_manager::CompileContext(char* Path, TextureEnum TxrID, uint32_t W, uint32_t H, uint32_t PngMode, uint32_t Filtering)
{
   Texture_t TempTxr;  /* This is a dummy Texture_t object we
                           reuse to build out our Texture map */
   TempTxr.RenderWarning = 0;

Now, when we print our error when Rendering, we check if RenderWarning is 0 or not, and set it after printing the error, add this in our gamestate::SubmitQuad function:

else
   {
       if(m_Textures.GetTexture(Tex_In)->RenderWarning == 0)
       {
           std::string Txr = GetPolyBinType(m_Textures.GetTexture(Tex_In)->m_Context.list_type);
           std::string Ply = GetPolyBinType(OpenPolyList);
           std::string TxrID = GetTextureEnumString(m_Textures.GetTexture(Tex_In)->TextureID);
           printf("Could not render %s, incorrect Texture Format for this Polygon Bin. Format: %s, Bin: %s\n", TxrID.c_str(), Txr.c_str(), Ply.c_str());
           m_Textures.GetTexture(Tex_In)->RenderWarning++;
       }
   }
}

Now let’s modify the SubmitQuad function in gamestate.cpp:

else
   {
       if(m_Textures.GetTexture(Tex_In)->RenderWarning == 0)
       {
           std::string Txr = GetPolyBinString(m_Textures.GetTexture(Tex_In)->m_Context.list_type);
           std::string Ply = GetPolyBinString(OpenPolyList);
           std::string TxrID = GetTextureString(m_Textures.GetTexture(Tex_In)->TextureID);
           printf("Could not render %s, incorrect Texture Format for this Polygon Bin. Format: %s, Bin: %s\n", TxrID.c_str(), Txr.c_str(), Ply.c_str());
           m_Textures.GetTexture(Tex_In)->RenderWarning++;
       }

Run our program, and we should see an error message telling us we used the wrong texture in the wrong list.