Tools for preprocessing data files from Quake to make them suitable for use on PS1 hardware
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

919 lines
34 KiB

<html><head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" href="Chapter%201.3%20Textures,%20TPages%20and%20CLUTs_files/style.css">
<title>Chapter 1.3: Textures, TPages and CLUTs</title>
<meta http-equiv="Content-Type" content="text/html; charset=windows-1252">
</head>
<body>
<header>
<h1>Chapter 1.3: Textures, TPages and CLUTs</h1>
</header>
<p>
This tutorial will teach you how to draw graphics with textures, from
converting texture data to loading it onto VRAM, and finally drawing said
texture. This is yet another essential part in PS1 graphics programming,
as not having any textures would not always make for a pretty looking game.
</p>
<p>
This chapter will also cover some development tools to be used for
preparing texture data, such as <b>timtool</b> and <b>img2tim</b>.
</p>
<p>
<b>Compatible with PSn00bSDK:</b> Yes
</p>
<h2>Tutorial Index</h2>
<ul>
<li><a href="#textures">Textures</a></li>
<li><a href="#cluts">Color Look-up Tables (CLUT)</a></li>
<li><a href="#texaddr">Texture Addressing on the PS1</a></li>
<li><a href="#seltexpage">Defining and Selecting a Texture Page</a></li>
<li><a href="#maketexture">Creating a TIM Texture Image</a></li>
<li><a href="#inctexture">Including TIM Image Data</a></li>
<li><a href="#tipsandtricks">Tips, Tricks and Improvements</a></li>
<li><a href="#conclusion">Conclusion</a></li>
</ul>
<h2 id="textures">Textures</h2>
<p>
A texture in the context of graphics programming, is basically a bitmap
of pixels that can either be drawn to the screen as a 2D image or
mapped onto polygons to add textures on 3D models.
</p>
<p>
On the PS1 hardware, textures are naturally stored in VRAM alongside the
display and drawing buffers described in previous tutorials. Textures are
generally positioned outside of said visual areas so to not get overwritten
by graphics frames drawn by the GPU. Once texture images have been loaded,
it can then be drawn by textured sprite and/or polygon primitives.
</p>
<p>
The PS1 supports texture color depths of 4, 8 and 16 bits per pixel. 4-bit
and 8-bit textures are usually accompanied with CLUT or Color Lookup Table.
A CLUT is essentially the color palette of its associated texture image.
</p>
<p>
Because the VRAM addresses pixels in 16-bit words, 4-bit and 8-bit textures
are stored at quarter and half of the actual width of the texture respectively
as shown in the image below.
</p>
<center>
<img src="Chapter%201.3%20Textures,%20TPages%20and%20CLUTs_files/vram-texture-widths.png">
<i>4-bit, 8-bit and 16-bit texture images as they appear logically in VRAM</i>
</center>
<p>
This also means that 8-bit textures must have a width that is of a multiple
of 2 whilst 4-bit textures must be of a multiple of 4 to ensure that the pixel
data is word-aligned in VRAM. Failing to do so will often yield artifacts, or
corrupted texture images when trying to upload odd-width images into VRAM.
</p>
<p>
Texture images are usually handled on the PS1 in the TIM file format, which
is a very simple image format designed specifically for the PS1 as it can
hold X and Y coordinates for either image and CLUT data which are used as
target coordinates when uploading the texture to VRAM. TIM files are typically
created from regular image files (bmp, pcx, jpeg or bmp) with tools such as
<b>timtool</b> included in the PsyQ/Programmer Tool SDK,
<a href="https://github.com/lameguy64/img2tim">img2tim</a> which is a free
command-line driven TIM converter and finally,
<a href="https://github.com/lameguy64/timedit">timedit</a> which is essentially
a free albeit slightly dodgy equivalent of <b>timtool</b>.
</p>
<p>
For beginners it may be best to use <b>timedit</b> as its the only free tool
(or at the very least one that I'm aware of) that features a graphical preview
of the TIM image layout in VRAM or stick to <b>timtool</b> if you're using the
PsyQ/Programmers Tool SDK.
</p>
<h2 id="cluts">Color Look-up Tables (CLUT)</h2>
<p>
A CLUT is simply a 16x1 or 256x1 16-bit pixel image that normally accompanies
4-bit and 8-bit texture images respectively. If you haven't guessed by now, a
CLUT is essentially the color palette of the image it accompanies, where each
pixel of the CLUT represents a 16-bit color entry for the texture image
starting from the left (ie. the first pixel from the left represents the color
for index 0 while the very last pixel of the CLUT represents the color for
index 15 or index 255). CLUTs are normally used with 4-bit and 8-bit texture
images, though it can also be used for procedurally generated textures such as
animated plasma effects.
</p>
<p>
The TIM file format could support multiple CLUTs on a single TIM file,
as the CLUT headers in the file format allow for more than 1 pixel high
CLUTs. This is beyond the scope of this part of the tutorial series however.
</p>
<h2 id="texaddr">Texture Addressing on the PS1</h2>
<p>
You may have learned from the last tutorial that the PS1 accesses the VRAM
as a two-dimensional image rather than a more conventional linear framebuffer.
Well, this also applies on how textures in VRAM are 'selected' for drawing with
textured primitives.
</p>
<p>
The PS1 addresses the VRAM in pages, in which the VRAM is divided into a
grid of 16x2 cells of 64x256 16-bit pixels each. The actual texture page
coordinates are still X,Y but with a 64x256 granularity instead. The image
below visually illustrates the cells of each texture page.
</p>
<center>
<img src="Chapter%201.3%20Textures,%20TPages%20and%20CLUTs_files/vram-texture-pages.svg">
<i>Texture page grid of the VRAM</i>
</center>
<p>
Texture pages only applies to the selection of the area at which textures would
be read from by textured graphics primitives, as VRAM coordinates of display
and drawing environments do not adhere to this texture page granularity at all.
Setting a texture page serves as an anchor point at which textured primitives
would source texture data from starting from the top-left corner of the texture
page, with the U,V coordinates of the primitive serving as an offset relative to
the current texture page allowing small portions of a texture page to be read
and drawn, such as animation frames of one particular character in a large
sprite sheet.
</p>
<center>
<img src="Chapter%201.3%20Textures,%20TPages%20and%20CLUTs_files/page-texcoord-relation.svg">
<i>An example of using U,V coordinates to select a particular sprite (in this
case at 48,32) in a sprite sheet</i>
</center>
<p>
Of course this would mean that the texture images would have to be arranged
such that it is placed on the top-right corner of a texture page boundary for
the U,V coordinates to make the most sense. However, some simple offset
translation based on the texture image's position relative to the page boundary
should get around that issue and is going to be covered in this chapter.
</p>
<p>
One thing to consider about texture coordinates that may initially confuse
beginners is that while the width of a 4-bit and 8-bit texture on VRAM varies
from the actual width of the image itself, the texture coordinates does not
need to be adjusted to take that into account as long as the correct color
depth is specified in the texture page value. In this case, if you want to
draw a texture at 96,24 from a 256x256 4-bit texture sheet, you just simply
specify a U,V coordinate of 96,24 in your primitive as if the texture were
still 16-bit.
</p>
<center>
<img src="Chapter%201.3%20Textures,%20TPages%20and%20CLUTs_files/vram-texture-coordinates.svg">
<i>Texture coordinates are always true to the actual size of the
texture image</i>
</center>
<p>
And because the U,V coordinates only have a range of 0-255, it is not possible
to draw a texture image larger than 256x256 pixels regardless of color depth.
To draw larger textures one has to draw multiple primitives to span the entire
size of a larger image, trying to draw a SPRT primitive with a size greater
than 256x256 will only result in a wrap around.
</p>
<h2 id="seltexpage">Defining and Selecting a Texture Page</h2>
<p>
A texture page value is easily defined using the <b>getTPage()</b> macro.
A function version of this macro is present in the PsyQ/Programmers Tool SDK
as <b>GetTPage()</b>, but this is not available in PSn00bSDK as it would be
considered redundant.
</p>
<pre>tpage = getTPage( <i>tp</i>, <i>abr</i>, <i>x</i>, <i>y</i> );
</pre>
<p>
<i>tp</i> specifies the color depth for the texture page in the range of
0 to 2 (0:4-bit, 1:8-bit, 2:16-bit). <i>abr</i> specifies the blend operator
for both non-textured and textured semi-transparent primitives which can be
ignored for now and lastly, <i>x,y</i> specifies the X,Y coordinates of the
VRAM in 16-bit pixel units. Keep in mind that the coordinates will be rounded
down to the next lowest texture page.
</p>
<p>
Now that a texture page value has been defined, there are a number of ways to
set it to the GPU. One is through the <i>tpage</i> field of the <b>DRAWENV</b>
struct which specifies the initial texture page value whenever the drawing
environment is applied. Another is to use a <b>DR_TPAGE</b> primitive which
can be defined with the <b>setDrawTPage()</b> macro which follows the same
syntax as <b>getTPage()</b>. <b>DR_TPAGE</b> is needed for <b>SPRT</b>
primitives which lack a tpage field, though it is also useful for non-textured
primitives to set the blend operator for semi-transparent primitives.
</p>
<p>
Because the PS1 usually processes primitives that have been sorted last first,
<b>DR_TPAGE</b> primitives must be sorted after <b>SPRT</b> or other similar
primitives have been sorted.
</p>
<p>
The following table describes the bit fields of a texture page value which may
come in handy for the more crafty programmers.
</p>
<center>
<table class="bordered-table">
<tbody><tr>
<th>Bits</th>
<td>15-14</td>
<td>13</td>
<td>12</td>
<td>11</td>
<td>10</td>
<td>9</td>
<td>8-7</td>
<td>6-5</td>
<td>4</td>
<td>3-0</td>
</tr>
<tr>
<th>Description</th>
<td>Reserved</td>
<td>Y-flip*</td>
<td>X-flip*</td>
<td>Texture disable*</td>
<td>Draw on displayed area</td>
<td>Dither enable</td>
<td>Texture color depth</td>
<td>Blend operator</td>
<td>Texture page Y (n*256)</td>
<td>Texutre page X (n*64)</td>
</tr>
<tr>
<th colspan="11">*Does not work on <i>really</i> early units, not recommended
to use to maintain compatibility</th>
</tr>
</tbody></table>
</center>
<p>
Whilst on the topic of texture page selection, when working with 4-bit and
8-bit texture images you will also need to use <b>getClut()</b> to define a
CLUT value and is required for 4-bit and 8-bit texture images to draw properly.
Obviously this is not required for 16-bit texture images.
</p>
<pre>clut = getClut( <i>x</i>, <i>y</i> );
</pre>
<p>
<i>x,y</i> specifies the X,Y coordinate of the CLUT within the VRAM.
However, keep in mind that the X axis must be a multiple to 16 pixels for a
CLUT to be defined correctly as otherwise it will be rounded down to the
lowest CLUT value.
</p>
<p>
As for selecting the CLUT, all textured primitives usually have a <i>clut</i>
field in the struct in which the CLUT value can be set to. There is also a
<b>setClut()</b> macro for setting CLUT coordinates to a primitive directly.
</p>
<p>
Usually, the coordinates for texture page and CLUT values are typically derived
from the X,Y coordinates of the TIM image file to take into account TIM images
that are not placed on the top-left corner of the texture page boundary.
</p>
<p>
The following describes the bit fields of a CLUT value.
</p>
<center>
<table class="bordered-table">
<tbody><tr>
<th>Bits</th>
<td>15</td>
<td>14-6</td>
<td>5-0</td>
</tr>
<tr>
<th>Description</th>
<td>Reserved</td>
<td>Y coordinate</td>
<td>X coordinate (n*16)</td>
</tr>
</tbody></table>
</center>
<h2 id="maketexture">Creating a TIM Texture Image</h2>
<p>
There are numerous ways to convert image files into the TIM format, the most
common is by using <b>timtool</b> from the official PsyQ/Programmers Tool SDKs,
or at least those who chose to use that SDK. Another is to use my free but old
command-line converter called <b>img2tim</b>, which follows the same command
line syntax as <b>bmp2tim</b> but supports many more image formats supported
by the FreeImage library and has better conversion options related to
transparencies.
</p>
<p>
Perhaps the most preferable tool to use for those who've stuck to using
PSn00bSDK is <b>timedit</b>, as not only is it free but also features a
similar graphical interface to <b>timtool</b>, on top of extra features
useful for managing a large amount of TIM images, such as grouping.
</p>
<p>
Whichever tool you use, convert the image below into a 8-bit TIM file with
image coordinates of 640,0 and CLUT coordinates of 0,480. You will have to
convert it into a 4-bit or 8-bit TIM as this tutorial is also going to cover
how to work with CLUTs.
</p>
<center>
<img src="Chapter%201.3%20Textures,%20TPages%20and%20CLUTs_files/texture64.png">
</center>
<p>
When converting the image, always remember that texture image and CLUT
coordinates in the tools are always absolute coordinates to the VRAM
regardless of color depth. And in case you're wondering, the PS1 cannot
use 24-bit TIM images, as the GPU can only draw graphics in 16-bit color
depth but can display 24-bit images by entering 24-bit color mode, but
this is beyond the scope of this chapter.
</p>
<h2 id="inctexture">Including TIM Image Data</h2>
<p>
In the meantime, the easiest way to include a binary file into your program
is to include it as an object file by means of a very simple assembler file,
and access that file as an array by defining its symbol name with <h>extern</h>.
Its not ideal for larger projects, but this will do the job for testing purposes.
</p>
<h3>PsyQ/Programmers Tool SDK</h3>
<p>
Save the following as <h>my_image.asm</h>.
</p>
<pre> opt m+,l.,c+
section data ; Store the array in the data section
global tim_my_image ; Define label as global
tim_my_image:
incbin 'my_image.tim' ; Include file data (your TIM)
</pre>
<p>
The name of the data is defined by the label, in this case <h>tim_my_image</h>.
Make sure the label is also defined by a global directive so the linker can find
it when linked against your C program. When writing the assembler file, make sure
line indentations are created with real TAB characters as ASMPSX will throw an
error otherwise.
</p>
<p>
Assemble the file using <h>ASMPSX</h> to turn it into an object file.
</p>
<pre>asmpsx /l my_image.asm,my_image.obj
</pre>
<p>Then link it with your program by simply specifying the object file.</p>
<pre>ccpsx -O2 -Xm -Xo$80010000 my_image.obj program.c -o program.cpe
</pre>
<h3>PSn00bSDK</h3>
<p>
In PSn00bSDK, it works in much the same principle as in the PsyQ SDK,
but this one will be written in GNU assembler syntax. Save the assembler
file with a <h>.s</h> file extension.
</p>
<pre>.section .data
.global tim_my_image
.type tim_my_image, @object
tim_my_image:
.incbin "my_image.tim"
</pre>
<p>
It is recommended to use the makefile from one of the example programs included
with PSn00bSDK, as it has parameters for building with assembler files already
defined and should automatically pick your assembler file as long as it has
a <h>.s</h> file extension.
</p>
<h3>Accessing the Array</h3>
<p>
On both SDKs, the binary file can be accessed as an array by simply defining
it with an <h>extern</h>. Make sure the name matches with the label name defined
in the assembler file.
</p>
<pre>extern int tim_my_image[];
</pre>
<p>
If you get mismatching type errors when compiling, you can simply cast it with a
different pointer type with <h>(u_long*)</h> or <h>(u_int*)</h> depending on
the compiler warnings you're getting. <h>u_int</h> is typically used in PSn00bSDK
instead of <h>u_long</h> as modern GCC interprets <h>u_long</h> as a 64-bit integer
whereas it is a 32-bit integer in PsyQ.
</p>
<h2>Parsing and Uploading the TIM to VRAM</h2>
<p>
To parse a TIM image file, use <b>OpenTIM()</b> to set the TIM file data for
parsing and <b>ReadTIM()</b> to retrieve header information of the TIM file to
a <b>TIM_IMAGE</b> struct. The <b>TIM_IMAGE</b> would contain not only a pointer
to the X,Y and size coordinates of either texture image and CLUT but also
pointers to the image and CLUT data within the TIM file.
</p>
<p>
In PSn00bSDK, <b>GetTimInfo()</b> function instead. It still returns the same
<b>TIM_IMAGE</b> struct.
</p>
<p>
The TIM image coordinates are in the <i>*prect</i> field and CLUT in
the <i>*crect</i> field, both are of type RECT. The actual texture image data
is at <i>*paddr</i> and the CLUT data at <i>*caddr</i>. The color depth of
the TIM file are in bits 0-3 of the <i>mode</i> field and CLUT presence is
determined by testing bit 4.
</p>
<p>
Uploading either pixel or CLUT data is done using <b>LoadImage()</b>, followed
by a call to <b>DrawSync()</b> to wait for texture upload to complete. Whilst
you might be able to get away with not waiting for the texture upload to
finish processing, it is recommended to wait for upload completion as
unpredictable results may occur otherwise.
</p>
<p>
The TIM image upload function should go like this.
</p>
<pre>void LoadTexture(u_long *tim, TIM_IMAGE *tparam) {
// Read TIM information (PsyQ)
OpenTIM(tim);
ReadTIM(tparam);
// Read TIM information (PSn00bSDK)
//GetTimInfo(tim, tparam);
// Upload pixel data to framebuffer
LoadImage(tparam-&gt;prect, (u_long*)tparam-&gt;paddr);
DrawSync(0);
// Upload CLUT to framebuffer if present
if( tparam-&gt;mode &amp; 0x8 ) {
LoadImage(tparam-&gt;crect, (u_long*)tparam-&gt;caddr);
DrawSync(0);
}
}
</pre>
<p>
In PSn00bSDK, you may additionally replace u_long to u_int. They are actually
the same in PsyQ/Programmers Tool whereas in modern GCC/PSn00bSDK u_long
usually defines a 64-bit unsigned integer.
</p>
<p>
Now that you have a TIM upload function, the TIM loading sequence should go like so.
</p>
<pre>extern int tim_my_image[];
// To keep a copy of the TIM coordinates for later
int tim_mode;
RECT tim_prect,tim_crect;
..
void LoadStuff(void) {
// This can be defined locally, if you don't need the TIM coordinates
TIM_IMAGE my_image;
// Load the TIM
LoadTexture((u_long*)tim_my_image, &amp;my_image);
// Copy the TIM coordinates
tim_prect = *my_image.prect;
tim_crect = *my_image.crect;
tim_mode = my_image.mode;
}
</pre>
<p>
Reason you may want to keep a copy of the TIM coordinates separate from the
<b>TIM_IMAGE</b> variable is that once you've figured out how to load files
from CD and load the TIM file to a dynamically allocated buffer, relying on
the <b>TIM_IMAGE</b> struct may not be a good idea as the <i>*prect</i> and
<i>*crect</i> fields point to the TIM file data directly, and will most
likely become undefined data once you've finished uploading that TIM to
VRAM and deallocated its buffer in later parts of your program.
</p>
<h2>Drawing the TIM</h2>
<p>
Now that a texture image has been loaded. The next thing to do is to set
it's texture page to the drawing environment. Since there's only one texture
to deal with, the texture page can be set to the <b>DRAWENV</b> struct.
</p>
<pre>draw[0].tpage = getTPage( tim_mode&amp;0x3, 0, tim_prect.x, tim_prect.y );
draw[1].tpage = getTPage( tim_mode&amp;0x3, 0, tim_prect.x, tim_prect.y );
</pre>
<p>
To deal with TIMs that are not page aligned, the U,V offset relative to
the TIM's rounded down texture page can be determined with the following
and should work for texture images of any color depth.
</p>
<pre>int tim_uoffs,tim_voffs;
tim_uoffs = (tim_prect.x%64)&lt;&lt;(2-(tim_mode&amp;0x3));
tim_voffs = (tim_prect.y&amp;0xff);
</pre>
<p>
Whilst this would allow texture images to not require being aligned to the
top-left corner of the texture page boundary, texture images cannot cross
between the vertical texture page bounds and for 4-bit texture images,
should not cross the horizontal texture page bounds as otherwise trying to
draw the texture image with a <b>SPRT</b> will result in wrapping in
relation to the texture page.
</p>
<p>
Now to actually draw the texture image. This can be done with a <b>SPRT</b>
primitive which simply draws a textured sprite of a specified size. It doesn't
do fancy things such as rotation and scaling but its a useful primitive for
drawing simple sprites.
</p>
<pre>SPRT *sprt;
...
sprt = (SPRT*)nextpri;
setSprt(sprt); // Initialize the primitive (important)
setXY0(sprt, 48, 48); // Position the sprite at (48,48)
setWH(sprt, 64, 64); // Set sprite size to 64x64 pixels
setUV0(sprt, // Set UV coordinates from TIM offsets
tim_uoffs,
tim_voffs);
setClut(sprt, // Set CLUT coordinates from TIM to sprite
tim_crect.x,
tim_crect.y);
setRGB0(sprt, // Set color of sprite, 128 is neutral
128, 128, 128);
addPrim(ot[db], sprt); // Sort primitive to OT
nextpri += sizeof(SPRT); // Advance next primitive
</pre>
<p>
When dealing with multiple texture images that tend to reside on different
texture pages, you will have to sort a <b>DR_TPAGE</b> primitive right after
the <b>SPRT</b> primitive that requires a particular texture page value set,
as the PS1 processes primitives that are sorted last first.
</p>
<p>
Whilst not required in this chapter, this is how you sort a <b>DR_TPAGE</b>
for when you start playing around with more texture images.
</p>
<pre>DR_TPAGE *tpage;
...
tpage = (DR_TPAGE*)nextpri;
setDrawTPage(tpage, 0, 1, // Set TPage primitive
getTPage(my_image.mode&amp;0x3, 0,
my_image.prect-&gt;x, my_image.prect-&gt;y));
addPrim(ot[db], tpage); // Sort primitive to OT
nextpri += sizeof(DR_TPAGE); // Advance next primitive address
</pre>
<p>
A very simple optimization practice you may want to consider when you start
getting into larger projects is that you only need to sort one <b>DR_TPAGE</b>
primitive if all the sprites sorted prior share a common texture page.
</p>
<h2>Sample Code</h2>
<p>
Working from code in the last tutorial, here's what the code should
look like:
</p>
<pre>#include &lt;sys/types.h&gt; // This provides typedefs needed by libgte.h and libgpu.h
#include &lt;stdio.h&gt; // Not necessary but include it anyway
#include &lt;libetc.h&gt; // Includes some functions that controls the display
#include &lt;libgte.h&gt; // GTE header, not really used but libgpu.h depends on it
#include &lt;libgpu.h&gt; // GPU library header
#define OTLEN 8 // Ordering table length (recommended to set as a define
// so it can be changed easily)
DISPENV disp[2]; // Display/drawing buffer parameters
DRAWENV draw[2];
int db = 0;
// PSn00bSDK requires having all u_long types replaced with
// u_int, as u_long in modern GCC that PSn00bSDK uses defines it as a 64-bit integer.
u_long ot[2][OTLEN]; // Ordering table length
char pribuff[2][32768]; // Primitive buffer
char *nextpri; // Next primitive pointer
int tim_mode; // TIM image parameters
RECT tim_prect,tim_crect;
int tim_uoffs,tim_voffs;
void display() {
DrawSync(0); // Wait for any graphics processing to finish
VSync(0); // Wait for vertical retrace
PutDispEnv(&amp;disp[db]); // Apply the DISPENV/DRAWENVs
PutDrawEnv(&amp;draw[db]);
SetDispMask(1); // Enable the display
DrawOTag(ot[db]+OTLEN-1); // Draw the ordering table
db = !db; // Swap buffers on every pass (alternates between 1 and 0)
nextpri = pribuff[db]; // Reset next primitive pointer
}
// Texture upload function
void LoadTexture(u_long *tim, TIM_IMAGE *tparam) {
// Read TIM parameters (PsyQ)
OpenTIM(tim);
ReadTIM(tparam);
// Read TIM parameters (PSn00bSDK)
//GetTimInfo(tim, tparam);
// Upload pixel data to framebuffer
LoadImage(tparam-&gt;prect, (u_long*)tparam-&gt;paddr);
DrawSync(0);
// Upload CLUT to framebuffer if present
if( tparam-&gt;mode &amp; 0x8 ) {
LoadImage(tparam-&gt;crect, (u_long*)tparam-&gt;caddr);
DrawSync(0);
}
}
void loadstuff(void) {
TIM_IMAGE my_image; // TIM image parameters
extern u_long tim_my_image[];
// Load the texture
LoadTexture(tim_my_image, &amp;my_image);
// Copy the TIM coordinates
tim_prect = *my_image.prect;
tim_crect = *my_image.crect;
tim_mode = my_image.mode;
// Calculate U,V offset for TIMs that are not page aligned
tim_uoffs = (tim_prect.x%64)&lt;&lt;(2-(tim_mode&amp;0x3));
tim_voffs = (tim_prect.y&amp;0xff);
}
// To make main look tidy, init stuff has to be moved here
void init(void) {
// Reset graphics
ResetGraph(0);
// First buffer
SetDefDispEnv(&amp;disp[0], 0, 0, 320, 240);
SetDefDrawEnv(&amp;draw[0], 0, 240, 320, 240);
// Second buffer
SetDefDispEnv(&amp;disp[1], 0, 240, 320, 240);
SetDefDrawEnv(&amp;draw[1], 0, 0, 320, 240);
draw[0].isbg = 1; // Enable clear
setRGB0(&amp;draw[0], 63, 0, 127); // Set clear color (dark purple)
draw[1].isbg = 1;
setRGB0(&amp;draw[1], 63, 0, 127);
nextpri = pribuff[0]; // Set initial primitive pointer address
// load textures and possibly other stuff
loadstuff();
// set tpage of lone texture as initial tpage
draw[0].tpage = getTPage( tim_mode&amp;0x3, 0, tim_prect.x, tim_prect.y );
draw[1].tpage = getTPage( tim_mode&amp;0x3, 0, tim_prect.x, tim_prect.y );
// apply initial drawing environment
PutDrawEnv(&amp;draw[!db]);
}
int main() {
TILE *tile; // Pointer for TILE
SPRT *sprt; // Pointer for SPRT
// Init stuff
init();
while(1) {
ClearOTagR(ot[db], OTLEN); // Clear ordering table
// Sort textured sprite
sprt = (SPRT*)nextpri;
setSprt(sprt); // Initialize the primitive (very important)
setXY0(sprt, 48, 48); // Position the sprite at (48,48)
setWH(sprt, 64, 64); // Set size to 64x64 pixels
setUV0(sprt, // Set UV coordinates
tim_uoffs,
tim_voffs);
setClut(sprt, // Set CLUT coordinates to sprite
tim_crect.x,
tim_crect.y);
setRGB0(sprt, // Set primitive color
128, 128, 128);
addPrim(ot[db], sprt); // Sort primitive to OT
nextpri += sizeof(SPRT); // Advance next primitive address
// Sort untextured tile primitive from the last tutorial
tile = (TILE*)nextpri; // Cast next primitive
setTile(tile); // Initialize the primitive (very important)
setXY0(tile, 32, 32); // Set primitive (x,y) position
setWH(tile, 64, 64); // Set primitive size
setRGB0(tile, 255, 255, 0); // Set color yellow
addPrim(ot[db], tile); // Add primitive to the ordering table
nextpri += sizeof(TILE); // Advance the next primitive pointer
// Update the display
display();
}
return 0;
}
</pre>
<p>
Compile the example, run it and you should get a yellow square cascaded by
a sprite with your texture image.
</p>
<center>
<img src="Chapter%201.3%20Textures,%20TPages%20and%20CLUTs_files/3-textures.png">
</center>
<p>
You may notice that the untextured rectangle is drawn before the textured
sprite even though the textured sprite was sorted first and the untextured
rectangle last. That's because of the way how primitives are appended to
the ordering table in <b>addPrim()</b>, where the primitive being sorted
is linked right after a primitive that has been sorted to it previously,
and the PS1 hardware parses the ordering table from the top of the array
to the bottom with a reverse ordering table (created with <b>ClearOTagR()</b>).
</p>
<center>
<img src="Chapter%201.3%20Textures,%20TPages%20and%20CLUTs_files/ordertable-multiprims.svg">
<i>How primitives are <b>appended</b> to a preoccupied ordering table entry</i>
</center>
<p>
Primitive 1 would be the <b>SPRT</b> primitive, Primitive 2 would be the
<b>DR_TPAGE</b> primitive (if there was) and lastly, Primitive 3 would be
the untextured <b>TILE</b> primitive. You may think that a reverse
order ordering table makes no sense at first, but this technique is
integral to the PS1's ability to do on-the-fly sorting of polygons when
rendering 3D graphics.
</p>
<h2 id="tipsandtricks">Tips, Tricks and Improvements</h2>
<h3>Pre-calculate tpage, CLUT and UV offset in a struct</h3>
<p>
Using <b>getTPage()</b>, <b>getClut()</b> and <b>setClut()</b> macros on
every sprite sorted isn't exactly the most efficient way of doing things
especially when drawing lots of sprites, not to mention makes your code
look cluttered when managing several TIM images at once. This can be
addressed by defining a struct that contains all the parameters you
need precomputed for drawing a sprite.
</p>
<pre>typedef struct _SPRITE {
u_short tpage; // Tpage value
u_short clut; // CLUT value
u_char u,v; // UV offset (useful for non page aligned TIMs)
u_char w,h; // Size (primitives can only draw 256x256 anyway)
CVECTOR col;
} SPRITE;
...
// Sets parameters to a MYSPRITE using coordinates from TIM_INFO
void GetSprite(TIM_IMAGE *tim, SPRITE *sprite) {
// Get tpage value
sprite-&gt;tpage = getTPage(tim-&gt;mode&amp;0x3, 0,
tim-&gt;prect-&gt;x, tim-&gt;prect-&gt;y);
// Get CLUT value
if( tim-&gt;mode &amp; 0x8 ) {
sprite-&gt;clut = getClut(tim-&gt;crect-&gt;x, tim-&gt;crect-&gt;y);
}
// Set sprite size
sprite-&gt;w = tim-&gt;prect-&gt;w&lt;&lt;(2-tim-&gt;mode&amp;0x3);
sprite-&gt;h = tim-&gt;prect-&gt;h;
// Set UV offset
sprite-&gt;u = (tim-&gt;prect-&gt;x&amp;0x3f)&lt;&lt;(2-tim-&gt;mode&amp;0x3);
sprite-&gt;v = tim-&gt;prect-&gt;y&amp;0xff;
// Set neutral color
sprite-&gt;col.r = 128;
sprite-&gt;col.g = 128;
sprite-&gt;col.b = 128;
}
</pre>
<p>And to draw the sprite, you can write a simple function for it:</p>
<pre>char *SortSprite(int x, int y, u_long *ot, char *pri, SPRITE *sprite) {
SPRT *sprt;
DR_TPAGE *tpage;
sprt = (SPRT*)pri; // initialize the sprite
setSprt(sprt);
setXY0(sprt, x, y); // Set position
setWH(sprt, sprite-&gt;w, sprite-&gt;h); // Set size
setUV0(sprt, sprite-&gt;u, sprite-&gt;v); // Set UV coordinate of sprite
setRGB0(sprt, // Set the color
sprite-&gt;col.r,
sprite-&gt;col.g,
sprite-&gt;col.b);
sprt-&gt;clut = sprite-&gt;clut; // Set the CLUT value
addPrim(ot, sprt); // Sort the primitive and advance
pri += sizeof(SPRT);
tpage = (DR_TPAGE*)pri; // Sort the texture page value
setDrawTPage(tpage, 0, 1, sprite-&gt;tpage);
addPrim(ot, tpage);
return pri+sizeof(DR_TPAGE); // Return new primitive pointer
// (set to nextpri)
}
</pre>
<p>
This method will also work for sprite sheets by simply modifying the
GetSprite() function mentioned earlier such that you can specify an U,V
offset relative to the TIM's U,V offset and the size of the sprite.
</p>
<h3>Use Sprite Sheets</h3>
<p>
Instead of creating a single TIM image for each and every animation frame
of your character sprite, it is best to compile such small images as one
large image. This is called a sprite sheet and allows all sprite frames
to share a common palette if you use 4-bit or 8-bit color depth. If all
sprite frames have the same size, arrange the sprites in a grid where
each cell is the same size as the sprites. This way you can compute the
U,V coordinate of the sprites in the grid easily.
</p>
<h3>Lower color depth textures are faster</h3>
<p>
A 4-bit texture is faster to draw than a 8-bit texture, and a 8-bit
texture is faster to draw than a 16-bit texture, as the GPU has to read
less data from VRAM when drawing textures of lower color depth. Find a
balance that achieves best possible performance without degrading the
quality of your sprites or texture images too much.
</p>
<h3>SPRT_8 and SPRT_16 are faster than SPRT</h3>
<p>
The fixed size sprite primitives are a bit faster than <b>SPRT</b>
primitives, making them best suited for particle sprites of a fixed size
or when drawing tiles of a 2D map.
</p>
<h3>Minimize tpage primitive changes</h3>
<p>
This technique is most applicable when drawing tile maps. If your tile
sheet fits in a single texture page (less than 256x256), you only have
to sort a single <b>DR_TPAGE</b> primitive for all the tiles. This may
also help the GPU maintain texture cache coherency, as well as minimizing
redundant primitive packets.</p>
<h3>Textures don't need to be powers of two</h3>
<p>
Texutures of any size can be used by the GPU, though it is recommended to
make the width of 8-bit TIMs a multiples of 2 and multiples of 4 for 4-bit
TIMs as mentioned earlier in this chapter.
</p>
<h2 id="conclusion">Conclusion</h2>
<p>
This concludes Chapter 1.3. of Lameguy64's PSX Tutorial series. If you've
sifted though this chapter well enough, you should have figured out how to
handle TIM images, uploading them to VRAM and drawing them.
</p>
<p>A few things you may want to experiment with for further learning:</p>
<ul>
<li>Try loading more TIMs and drawing them as individual sprites. Make sure
the TIM image and CLUT do not overlap one another in the TIM editing tool
you're using.</li>
</ul>
<hr>
<table width="100%">
<tbody><tr>
<td width="40%" align="left"><a href="http://lameguy64.net/tutorials/pstutorials/chapter1/2-graphics.html">Previous</a></td>
<td width="20%" align="center"><a href="http://lameguy64.net/tutorials/pstutorials/index.html">Back to Index</a></td>
<td width="40%" align="right"><a href="http://lameguy64.net/tutorials/pstutorials/chapter1/4-controllers.html">Next</a></td>
</tr>
</tbody></table>
</body></html>