Text Rendering

From dreamcast.wiki
Jump to navigation Jump to search

Now that we can display our font texture with transparency correctly, let’s build our function to render text. To do this, we’ll have to manipulate the UV coordinates of the texture we render with.

We start by making a clone of our SubmitQuad function, which we’ll call SubmitText. This will take some different parameters. First, it’ll take a const char* Input string. This is a c-style string, which is just an array of bytes on the stack. We want to take a font color, since our font.png texture has 4 different font colors. Each font in our texture atlus has 2 sizes, so we’ll take a flag to switch between them. Finally, we’ll take an X and Y coordinate to place the text on screen. Our Function should look like this:

void GameState::SubmitText(const char* Input_String, uint8_t FontColor, uint8_t Small, uint32_t X_In, uint32_t Y_In)
{
			/* Function code in here */
}

Don’t forget to add this function declaration in Gamestate.h. This function is only for rendering text, so we can assume some things about it that we didn’t have to assume in the Quad function. We don’t want our text to have an opaque background, only transparent, so we’ll add a check to ensure we’re in the PVR_LIST_TR_POLY list:

   if(OpenPolyList == PVR_LIST_TR_POLY)
   {
		/* Transparent Polygon Bin code in here */
   }

Our font atlus follows some rules that makes it organized well for our task. I drew the font so that every cell is 16x16 pixels big on the texture. There is an 8x8 font included on every other row, but they stick to the 16x16 grid. When the font is 8x8, it simply is stuck in the upper left corner of the 16x16 pixel grid. That makes the math involved in selecting which character to render a lot easier.

We want to avoid if statements as much as possible when programming, they make our code unpredictable. It is better to use weighted math when possible to avoid branching conditions. This is a perfect example of when to use this. We have two font sizes we want to choose from, and how we choose them is by altering the vertical axis of our texture mapping coordinate by 16 pixels. Either our texture mapping coordinate will be X, or it’ll be X + 16. We can think of that as X + (0)*16, and X + (1)*16. By changing the variable from 0 to 1, our answer changes by 16 pixels each time, without needing an if branch. We can accomplish something like this in our code by using our SizeFlag in the parameters. Let’s create a final FontSize scaling factor:

       uint32_t Font_Scale = 8 * SizeFlag;

This variable, Font_Scale, represents how many pixels downward our bottom UV coordinate is, from the top of our texture map. Our sizeFlag parameter can either be 1 or 2. If it’s 1, then we use 8 pixel tall fonts. If it’s 2, then we use 16 pixel tall fonts.

After that, we create our texture mapping context header. This will use font_png exclusively, since it renders text, so we don’t need a variable for the texture enum:

       /* Texture Mapping */
           Texture_t* Texture = m_Textures.GetTexture(font_png);
           pvr_prim(&Texture->m_Header, sizeof(Texture->m_Header));

We can now start working on our vertices. We still use a temporary vertex, T_Vertex, to work on, and a pointer, Vertex_Ptr, to hold the mesh vertex we’re working on, as convinence labels for readability. We also introduce a couple of new variables. One is a uint8_t flag called Input_String_EOL. It is used to control the loop that we use to read the character array string containing our text to render. We also have a uint8_t variable called FontSelector. This variable chooses which vertical value to use for our uv texture map, to select between one of the four font types. Since each font has 2 sizes, and each font row is always 16 pixels tall, we can multiply our FontSelector_Input parameter, which goes from 0-3, by 2, so it skips both font lines. Lastly, we add the result of the SizeFlag parameter with modulus division by 2. This gives us the remainder of division. Our SizeFlag choices are 1 or 2, with 2 being big font (16) and 1 being small font (8). When we use the big font, we’re using the first line in our font atlus, when we use the small font, we’re using the second line. Thus, when we pass 2 by our SizeFlag parameter, we need it to turn into 0 for our Line Selector. And if we pass 1 by our SizeFlag parameter, we need it to turn into 1 for our Line Selector. Modulus division does this. 2%2 = 0, since we have a remainder of 0 when we do 2/2. Likewise, 2%1 = 1, because we have a remainder of 1 when we do 2/1. We add this modulus division to our FontSelector value, which shifts the row selected by 0 or 1.

       /* Vertex Submission */
           pvr_vertex_t  T_vertex;
           pvr_vertex_t* Vertex_ptr;
           uint8_t Input_String_EOL = 0;
           uint8_t FontSelector = 2*FontSelector_Input;
           FontSelector+=SizeFlag%2;

With our LineSelector determining which font row we’re using, we’re ready to start rendering the text. We’ll create a for-loop to iterate through the Input_String array, byte by byte:

       for(int Idx = 0; !Input_String_EOL; Idx++)
       {
	 }

The condition for the loop to terminate is that Input_String_EOL is something other than 0, so that flag is our loop terminator. Input_String[Idx] is a byte containing a single character of our text string:

if(Input_String[Idx] == '\0')
{
	Input_String_EOL = 1;
}

The first condition to consider is if that character is ‘\0’. This is a special character called a “NULL Terminator.” It’s a character which means “nothing,” that is used to mark special conditions. When encountered in a C-Style String, it means that is the end of the String, called EOL. So we set our Input_String_EOL flag to 1 to end the loop.

else if(Input_String[Idx] < 33 || Input_String[Idx] > 122)
           {
               /* We only print the error if it's 31 or below, because 32 is space */
               if(Input_String[Idx] < 32)
                   printf("Invalid Character to Render: %c\n", Input_String[Idx]);
           }

The first if conditional is appended with an else if statement. This condition considers that our character that we’re examining isn’t EOL, but also isn’t valid. According to the ASCII map, the only inputs we are considering is 32 to 122, with 32 being a space character. We don’t render anything for space, so we check to make sure our value is between 33 and 122. If it’s not, skip rendering and give a small message letting us know we encountered an unknown character.

else /* Render the character to the screen as a quad */
           {

}

We append one last else condition. This is what happens if this is a valid character between 33 and 122. We need to do some more math on it. Characters after the value of 96 are actually lower case characters, and our font set does not cover those. So we can subtract 32 from our value if it’s after 96, which will turn lower case characters into upper case characters:

               int32_t CharIdx = (uint8_t)Input_String[Idx];
               if(Input_String[Idx] > 96)
               {
                   CharIdx = CharIdx - 32;
               }

This ensures our index is now between 33 and 96. We want to turn this into an index from the range of 0-63, with each number being a horizontal cell index in our font texture. 64 x 16 = 1024: CharIdx = CharIdx - 33; /* Turn our CharIdx into a index for our array, we use 33 to turn space into -1 */

With our Index now correctly configured, we can begin drawing the character. We need to call to get a vertex from our quad mesh, and we need to do it for 4 times as this is a quad. m_Meshes.Get(e_Quad)→m_VtxCount returns 4 programmatically:

for(int i = 0; i < m_Meshes.Get(e_Quad)->m_VtxCount; i++)
{

}

Now we’re ready to prime and submit our vertex. We use Vertex_Ptr to hold our vertex in our Quad Mesh:

/* Map all the information */
                   T_vertex.flags = Vertex_ptr->flags;
                   T_vertex.x = X_In + (Idx * Font_Scale)  + (Vertex_ptr->x * Font_Scale);
                   T_vertex.y = Y_In + (Vertex_ptr->y * Font_Scale);
                   T_vertex.z = Vertex_ptr->z + RenderDepth;
                   

The main bit of this text rendering algorithm comes from how we set T_Vertex.x and T_Vertex.y, as well as T_Vertex.u and T_Vertex.v.

T_Vertex.x is what determines the horizontal position of the quad being drawn. We start with our base X_In parameter, which becomes the origin of our Text Renderer. We add to this X_In origin the result of multiplication of the Index by the Font_Scale. Font_Scale will be either 8 or 16, and the Idx is choosing how many of these 8 or 16 pixel steps to move over. This makes the spot where we render our next character quad to shift over so we’re not rendering on top of ourselves.

This same formula will be run on all 4 vertices of the quad. 2 of the vertices are on the left side of the edge of the quad, which is equal to our origin at X_Int. However, the other 2 vertices are on the right side of the quad, which is Font_Scale pixels to the right (either 8 or 16). When we made our Quad Mesh, the vertices on the left had a 0 in their x variable, and the vertices on the right had a 1. We can use this variable, Vertex_ptr->x, as an on/off switch for our Font_Scale variable. This creates the XY coordinates for the quad that will make up our text character on screen, next we need to select it from the texture.

When we do texture mapping, we are assigning an X,Y coordinate to each vertex that tells it where that corner should map to on our texture. Because X,Y are already used in our vertex to describe the position, we use UV instead. Anytime we see UV, we are likely talking about texture coordinates. In addition to knowing their use intuitively, the choice of UV also tells us that our values are normalized. Recall that normalizing a value restricts it between a range of 0.00 to 1.00. This turns our value into a percentage. That means when we describe our texture mapping coordinates, we do not do so in terms of pixel values. Rather, we describe our coordinates as “percentage to the right of our texture” and “percentage to the bottom of our texture.”

You might think this method of storing coordinates is silly, but it’s very useful for textures. This is because we don’t always use the same size for our texture. A method to reduce computation for texture scaling is mipmapping, which involves creating the same texture multiple times in memory at different pre-scaled resolutions. If we described our texture mapping in terms of pixel coordinates, this wouldn’t work. If our texture at full resolution is 640 pixels horizontally, and we are at 50% mipmap level, and we tell our vertex to map to texel 640, it wouldn’t know what to do. At 50% mipmap level, our horizontal span is only 320. By describing our coordinate as a percentage, it remains the same no matter what size the texture.

For example, if our right edge corner of our quad needs to map to the far right edge of the texture, we can say it’s 100% to the right. That way, if we are using 100% mipmap level, that’s pixel 640. And if we are using 50% mipmap level, then that’s pixel 320. Using UV coordinates as percentages, and thus describing them as normalized floats, keeps everything working as intended.

Luckily the formula to translate from pixel space to normalized percentage is easy. U = X/Width, and V= Y/Height. To give an example, if we wanted to get a texel on the far right edge of a 640 pixel texture, then U = 640/640 = 1 = 100%. If we wanted to get a texel on the dead center of the texture, then U = 320/640 = .5 = 50%. This works for the V coordinate as well. We are doing exactly this in our T_Vertex.u calculation, just with more variables worked in:

T_vertex.u = ((16.0f * (float)CharIdx) + (Vertex_ptr->u * Font_Scale))/1024.0f;
                   T_vertex.v = ((16.0f * (float)FontSelector) + (Vertex_ptr->v * Font_Scale))/128.0f;
                   T_vertex.argb = Vertex_ptr->argb;
                   T_vertex.oargb = Vertex_ptr→oargb;

The left edge of our texture map moves over by 16 pixels times our character index. This selects the character on the texture map. Our vertex was created with UV coordinates that change between 0 and 1, just like our position coordinates, so we can use them as a flag for turning off an offset in our calculation. In this case, the UV flag adds our Font_Scale variable (8 or 16 pixels). We then divide all of this by 1024.0f, our font.png width. That gives us the left and right edges of our texture map.

We do the same for V using the Y coordinates. Instead of 1024.0f as our width, we use 128.0f as our height.

With our Vertices calculated correctly for the text, we can submit them as we finish each loop:

/* Submit data */
                   pvr_prim(&T_vertex, sizeof(T_vertex));
               }

Everytime we submit a quad, we need to increment RenderDepth so we avoid z-fighting:

RenderDepth += 0.1;
           }
       }
  } else /* if(OpenPolyList != PVR_LIST_TR_POLY) */

We attach an else condition at the end of our close braces. At the very beginning of this function, we checked if we were on the PVR_LIST_TR_POLY bin list. If we’re not, we print a little error:

{
       if(m_Textures.GetTexture(font_png)->RenderWarning == 0)
       {
           printf("Could not render text, font texture format is not PVR_LIST_TR_POLY\n");
           m_Textures.GetTexture(font_png)->RenderWarning++;
       }
   }
}

With all this done, let’s draw some text to the screen. In our gamestate.cpp render() function:

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;
               SubmitText("Hello World!!", FONT_BLUE, FONT_SMALL, 15, 15);
	     OpenPolyList = 0;
       pvr_list_finish();
   pvr_scene_finish();
}

Run our program, and we should see “HELLO WORLD!” rendered over the background!

We can do other things with our SubmitText function. It can take a std::string if we use the c_str() function:

/* Translucent Polygons */
       pvr_list_begin(PVR_LIST_TR_POLY);
           OpenPolyList = PVR_LIST_TR_POLY;
               std::string Test = "Hello World!!";

               SubmitText(Test.c_str(), FONT_BLUE, FONT_SMALL, 15, 15);
           OpenPolyList = 0;
       pvr_list_finish();

We can also use variables in our string, with the built-in to_string function from <string>:

/* Translucent Polygons */
       pvr_list_begin(PVR_LIST_TR_POLY);
           OpenPolyList = PVR_LIST_TR_POLY;
               std::string Test = "Counter: ";
               static int a = 0;
               Test += std::to_string(a);
               a++;
               SubmitText(Test.c_str(), FONT_BLUE, FONT_SMALL, 15, 15);
           OpenPolyList = 0;
       pvr_list_finish();

You can use to_string to directly print a variable in our SubmitText function by using c_str() as well:

       pvr_list_begin(PVR_LIST_TR_POLY);
           OpenPolyList = PVR_LIST_TR_POLY;
               static int a = 0;
               a++;
               SubmitText(std::to_string(a).c_str(), FONT_BLUE, FONT_SMALL, 15, 15);
           OpenPolyList = 0;
       pvr_list_finish();

The 8x8 font actually looks pretty small on a 640x480 resolution, it was made for a 320x240 resolution in mind. The Dreamcast supports 320x240, but it’s sort of a hack. We can fake a 320x240 resolution by applying a scaling factor to all of our video functions. Let’s do that, in our gamestate class in gamestate.h, add the following private variable:

   uint32_t VideoScale;
};
#endif // GAMESTATE_H

This variable will scale everything by a factor. We’ll set it to 2 to start in our constructor:

GameState::GameState()
{
   Done = 0;
   PrevControllerState.buttons = 0;
   RenderDepth = 0;
   VideoScale = 2;
}

Now, in our SubmitText function, let’s apply the scale:

/* Map all the information */
T_vertex.flags = Vertex_ptr->flags;
T_vertex.x = X_In + (Idx * Font_Scale * VideoScale)  + (Vertex_ptr->x * Font_Scale) * VideoScale;
T_vertex.y = Y_In + (Vertex_ptr->y * Font_Scale * VideoScale);
T_vertex.z = Vertex_ptr->z + RenderDepth;
T_vertex.u = ((16.0f * (float)CharIdx) + (Vertex_ptr->u * Font_Scale))/1024.0f;
T_vertex.v = ((16.0f * (float)FontSelector) + (Vertex_ptr->v * Font_Scale))/128.0f;
T_vertex.argb = Vertex_ptr->argb;
T_vertex.oargb = Vertex_ptr->oargb;

With our VideoScale set to 2, our 640x480 screen is treated like a 320x240 screen, or rather our text is rendered at double resolution!

It would be very useful if our text rendering code could handle carriage returns. These are characters which starts a new line. We could pass a parameter to indicate the number of NewLines to start rendering at, and every time we encounter a ‘\n’ character we could add to it. Let’s build this out. In our Gamestate.h header, let’s clone our SubmitText function to overload it with an extra parameter for new lines:

   void SubmitText(const char* Input_String, uint8_t FontColor, uint8_t Small, uint32_t X_In, uint32_t Y_In);
   void SubmitText(const char* Input_String, uint8_t FontColor, uint8_t Small, uint32_t X_In, uint32_t Y_In, uint32_t NewLines);
   mesh_manager m_Meshes;
   pvr_list_t OpenPolyList;
   float VideoScale;
};
#endif // GAMESTATE_H

We need to create these functions. We’ll turn our existing function into the new function with the added parameter, then create a version of the original that calls our new function with new lines set to 0:

void GameState::SubmitText(const char* Input_String, uint8_t FontSelector_Input, uint8_t SizeFlag, uint32_t X_In, uint32_t Y_In)
{
   SubmitText(Input_String, FontSelector_Input, SizeFlag, X_In, Y_In, 0);
}
void GameState::SubmitText(const char* Input_String, uint8_t FontSelector_Input, uint8_t SizeFlag, uint32_t X_In, uint32_t Y_In, uint32_t NewLines)
{
   if(OpenPolyList == PVR_LIST_TR_POLY)
   {

We are going to have a new variable called X_Position in our function to keep track of where we are drawing, so when we do a newline, we start at the beginning of the line. Previously we were using Idx as our value for the x position, but that wouldn’t reset with a new line character:

     uint32_t X_Position = 0;
       for(int Idx = 0; !Input_String_EOL; Idx++)
       {
           if(Input_String[Idx] == '\n')
           {
               NewLines++;
               X_Position = (-1);
           } else if(Input_String[Idx] == '\0')
           {

We modify our if condition to add a check before our NULL Terminator check. This time, we’re looking for ‘\n’, which is the new line character. If we encounter it, increase our NewLines counter and reset our X_Position to -1. We set it to -1 instead of 0, because at the end of our loop we increase X_Position:

           }
           X_Position++;
       }
   } else /* if(OpenPolyList != PVR_LIST_TR_POLY) */
   {

Now, we need to change our vertex setting code to use X_Position for the character position:

/* Map all the information */
                   T_vertex.flags = Vertex_ptr->flags;
                   T_vertex.x = X_In + ((X_Position * Font_Scale) * VideoScale)  + ((Vertex_ptr->x * Font_Scale) * VideoScale);
                   T_vertex.y = Y_In + ((NewLines * Font_Scale) * VideoScale) + ((Vertex_ptr->y * Font_Scale) * VideoScale);
                   T_vertex.z = Vertex_ptr->z + RenderDepth;

With all this, we can now render newlines. Let’s test it in our Render function:

/* Translucent Polygons */
       pvr_list_begin(PVR_LIST_TR_POLY);
           OpenPolyList = PVR_LIST_TR_POLY;
           std::string Test = "Hello World!! My name is gabriel\n"
                              "and I am typing this on my Sega \n"
                              "Dreamcast! \n\n"
                              "My email is support@aol.com!";
               SubmitText(Test.c_str(), FONT_BLUE, FONT_BIG, 15, 15);
           OpenPolyList = 0;
       pvr_list_finish();

Every time we type \n, we get a new line in our text renderer!