Monster Graphics

From FF5 Hacking Wiki
Jump to navigation Jump to search

By Squall (aka Squall_FF8)

I've been asked to add monster image in my Viewer, that is taken directly from the ROM. At first I thought that shouldn't be a hard work. Well, I was WRONG - nothing is simple in Final Fantasy.

Prerequisites

In order to understand or ever reproduce this tutorial you will need some knowledge on

  • SNES graphic hardware (PPU)
  • Tiles, tile engine
  • Bit-planes, how a 2d tile is stored in 1d linear memory
  • Palettes, Bit-Per-Pixel (BPP)
  • How a palette color can be reproduced on contemporary video cards


Master Table

The whole info needed to render an image of a monster is held by a table, lets call it MasterTable that starts at address $14B180. The table has 384 entries - exactly the same as numbers of different monsters FF5 defines. Each entry (or record) is 5 bytes long. In order to show an image of a monster with ID you need to access the memory that is

   MonsterImageData = $14B180 + 5 * MonsterID.

So lets see what is the meaning of these 5 bytes:

   +0 bit7:   Compression flag
      bit6-0: Tileset ID (high)
   +1 bit7-0: Tileset ID (low)
   +2 bit7:   Size flag
      bit6:   Shadow flag
      bit5-2: unused
      bit1-0: Palette ID (high)
   +3 bit7-0: Palette ID (low)
   +4 bit7-0: Form ID

In other words, if we use a contemporary program language terms, we have a record with following fields:

    1b Compression
   15b Tileset ID
    1b Size
    1b Shadow
   10b Palette ID
    8b Form ID


Compression

TBH that was the hardest part for me to understand. I expected some LZ77 or RLE compression. As it turns out, its well know to SNES trick - using 3bpp tiles displayed in 4bpp video mode. So what compression flag actually tells us?

   0: Tiles are in 4BPP format
   1: Tiles are in 3BPP format


Tileset

Since we have 4BPP & 3BPP tiles with 32/24 bytes per tile we should multiply TilesetID * 32(or 24). FF5 uses another trick. It puts all tiles in tilesets literary in the memory one after another. That's why TilesetID is more like an index, rather ID, but at the cost of larger index size (15 bits). So in order to access first byte in the tileset we must multiply by 8. The tiles are stored on address $150000.

   Tileset address = $150000 + 8 * TilesetID

At that address we have the bytes for all tiles that makes the image of the monster. Keep in mind that next tile start +32 bytes (4BPP) or +24 bytes (3BPP)


Size

That was another puzzle to solve. As you may think the size will be points Width X Height in pixels or tiles. But its only 1 bit. Internally FF5 uses only 2 logical sizes:

   0: Small - 64x64 pixels or 8x8 tiles
   1: Large - 128x128 pixels or 16x16 tiles


Shadow

Many of the monsters have a shadow under it. Some have pre-rendered one in the tileset. Others needs extra work to add.

   0: The monster have pre-rendered shadow. No extra drawing.
   1: The monster doesn't have pre-rendered shadow. Extra drawing is required.

So far I wasn't able to find the subroutine that draws the shadow. I have found the shadow tiles used in the routine.


Palette

As with Tileses, palettes are stored linearly in the memory one after another. For the same reason PaletteID is more of index rather ID.

    Palette address = $0ED000 + 8 * PalettetID

From there you read 16/32 bytes to make 8/16 color palette (3BPP/4BPP).


Form

Usually monsters are much smaller then their Sprites with dimension of 64x64 or 128x128. What is the reason of doing that I dont know, but one thing that comes in my mind could be achieving pixel-precision. Most likely pictures were drawn by artists, then rasterized on computers that have pixel precision and then rendered in tiles for SNES.

Regardless of the reasons a question raises - how to fill these huge Sprites? A natural answer will be - make 8x8 tiles that fills 64x64 Sprite. If we do that we will see that many tiles are the Zero tile (8x8 filled with 0 color - transparent). So what FF5 does is to store sequentially the tiles that are not Zero, starting from Top-Left corner left-to-right line by top-to-down line. That is what actually the tileset is. That's exactly how tilesets are stored in the memory. By not including the Zero tile we will save 32(or 24)*NumberOfZeros bytes and believe me NumberOfZeros is Huge compare to total (64) tiles. For example the Goblin uses only 17 non-Zero tiles, so we saved (64-17) * 32 = 1504 bytes - WOOAHH!!!

There is downside of this method - how to recreate the image from this pile of tiles? These is where the Form or maybe better terms will be Map or even Pattern.

   Form/Map/Pattern is 8x8 or 16x16 bit array
   1: place the next tile
   0: place the Zero tile
   * 8x8 bit array takes only 8 bytes to be stored. So for each Monster we have extra 8 bytes.
   * In case of 128x128 Sprite the From is 16x16 bit array, which uses 32 bytes to be stored

Now that we know how internal structures looks like, lets find them:

   $10D000 contain offsets of two tables - For Small and Large (2 bytes each), usually D004, D334
   FormSmall = $10D000 + ($10D000), usually $10D004
   FormAddress = FormSmall + 8 *FormID
   FormLarge = $10D000 + ($10D002), usually $10D334
   FormAddress = FormLarge + 32 * FormID


Examples

Preparing an image for FF5

Let see how and what steps are required to prepare an image for FF5.

In this example I will use a picture of SteelBat that is zoomed 4x (Graphic addresses go from $10D024 to $10D02B):
FF5%20Tutorial%200.png
Lets slice it with horizontal and vertical lines at every 8 pixels - this will show our Tiles. I have colored Zero tiles with white and the result is:
FF5%20Tutorial%201.png

If we number non-Zero tiles as described earlier, we will have 16 tiles:
FF5%20Tutorial%202.png If we put the tiles by their numbers, this will be our Tileset for Monster image.


The Form will look like like:

  Hex    Binary                 ASCII
   E8 =  1 1 1 0 1 0 0 0  ->    █ █ █ _ █ _ _ _
   78 =  0 1 1 1 1 0 0 0  ->    _ █ █ █ █ _ _ _
   78 =  0 1 1 1 1 0 0 0  ->    _ █ █ █ █ _ _ _
   30 =  0 0 1 1 0 0 0 0  ->    _ _ █ █ _ _ _ _
   30 =  0 0 1 1 0 0 0 0  ->    _ _ █ █ _ _ _ _
   00 =  0 0 0 0 0 0 0 0  ->    _ _ _ _ _ _ _ _
   00 =  0 0 0 0 0 0 0 0  ->    _ _ _ _ _ _ _ _
   00 =  0 0 0 0 0 0 0 0  ->    _ _ _ _ _ _ _ _