Why Did They Tile Their Images?
Sometimes images in your game are tiled. This is done to help the graphics processor render the graphics faster.
The graphics processor is divided into subunits and each of these is able to draw graphics. To obtain the best performance, it's necessary to keep all of these units busy. In a tiled image, the image is divided into tiles and each is assigned to a subunit, so the image can be drawn quickly.
Note: For standard images this happens too. However, it's more work for the graphics processor's main unit, because for standard images, the image has to first be cleaved into tiles and then assigned to each subunit.
What is a Tiled Image?
Here is what our example image looks like in standard format. I've rendered the images in text format so you can more easily see them. Our image is a 4 x 4 pixel image. This is what it looks like as a standard image:
0123 4567 89AB CDEF
As a simple string of pixels, our image looks like: 0123456789ABCDEF
When game developers made tools to make their game, they decided on a tile size that's based on the properties of the graphics processor of the hardware their game was supposed to run on. The tile size will be different depending on the graphics processing hardware. A general rule of thumb is tile sizes tended to get larger as time moved forward.
We're going to divide our image into four tiles: 0145, 2367, 89CD and ABEF. As a string of pixels, our tiled image looks like 0145236789CDABEF. Now the GPU needs to hand them out to subunits, enough for one tile at a time. It does not need to apply an offset to get the next row of pixel data- the data is said to be tiled- ordered ahead of time for efficient rendering.
If we try to render tiled pixels in standard order, we get garbage- the pixels are out of order for standard rendering, such as in an image viewer program. So we need to conduct the untiling process.
- First we make an empty buffer of 16 pixels. Then we go by tiled pixels.
- When starting each tile we move the buffer position counter to the top-left of the tile.
- We copy tile_width worth of pixels, then move the buffer position counter down one row. We do this once for each pixel in the tile height.
Untiling Sample Code
def chunks(l, n): """Yield successive n-sized chunks from l.""" for i in range(0, len(l), n): yield l[i:i + n] def off_gen(tile_w, tile_h, width, stop=1000000, bpp=8): '''Generate offsets within untiled image for successive tiles''' for tile_row_start in range(0, stop, width * bpp // 8 * tile_h): for tile_col_start in range(0, width * bpp // 8, tile_w * bpp // 8): yield tile_row_start + tile_col_start def untile(bitmap, tile_w, tile_h, width, bpp): '''Support only bpp=4 or bpp=8 Power-of-two tile dimensions strongly recommended. ''' if not (width / tile_w).is_integer(): raise ValueError('Width must be a multiple of tile width') if not (len(bitmap) / (tile_w * tile_h)).is_integer(): raise ValueError('Last tile is incomplete') bitmap_new = bytearray(len(bitmap)) #Loop over tiles for tile_data, bitmap_offset in zip( chunks(bitmap, tile_w * tile_h * bpp // 8), off_gen( tile_w, tile_h, width, len(bitmap), bpp)): #Loop over rows within a tile for row_data, bitmap_offset2 in zip( chunks(tile_data, tile_w * bpp // 8), range( bitmap_offset, bitmap_offset + tile_h * width * bpp // 8, width * bpp // 8)): bitmap_new[bitmap_offset2: bitmap_offset2 + tile_w * bpp // 8] = row_data return bitmap_new #Example call ##with open('input_bitmap.bin', 'rb') as f: ## bitmap = f.read() ##bitmap = untile(bitmap, tile_w = 32, tile_h = 8, width = 512, bpp = 4) ##with open('output_bitmap.bin', 'wb') as f: ## f.write(bitmap)