Getting More out of Seamless Tiles 2


We have used tiles to decorate our living spaces for more than 4000 years. Tiles have several properties that make them attractive for use:

  • they can be mass-produced;
  • they are easy to build with (because of their geometric properties); and
  • combinations of tiles lead to a huge number of decorative options.

Early game makers recognised that these advantages of tiles also apply to tiles in computer graphics, and using tiles was (and still is) a popular way to make game graphics.

In computer terms, here are the advantages of a tile system:

  • it is efficient,
  • it is easy to program,
  • and a few tiles give you a huge number of possible game maps.

The last point is really the one that makes tiles shine, and it really comes to its full with procedural content generation. We can create vast worlds, and have them beautifully drawn with handcrafted tiles.

But tiled games can also suffer from some defects:

  • the geometric regularity of tiles might be less appealing than organic design; and
  • the repetition can be jarring.

This tutorial addresses these two problems, with heavy focus on seamless tiles.

Tiles

Tiles

Some Tile Basics

Grids

Square grids are the easiest to work with, and are by far the most common. Choosing a different grid will have a considerable impact on the game, and can go a long way to make your world more organic. Two other tile-systems are commonly used: hexagonal tiles, and triangular tiles. All three systems can be scaled, sheared or rotated without affecting the basic principles.

The algorithms in this tutorial are described for square tiles, but they can all be modified for triangular or hexagonal tiles.

Other tilings are possible, but are complicated to implement – perhaps too complicated for a game. For instance, Penrose tiling uses two simple tiles, but it leads to some seemingly irregular patterns. I know of no computer game that uses this tiling for map layout, but several puzzle games use the intricacies of Penrose tiling as a play mechanic.

Penrose tiles

Penrose tiles

A Few Definitions

region: In this article, we will use the term “region” to describe a contiguous area that is covered with the same “texture”, such as grass, sand or wall. Regions might be made up of several tiles, and, as will be discussed below, need not cover all tiles fully.

logical grid: This is the grid used to store information about the world. For instance, this grid might contain information on whether a character may move into this cell or not, or the type of cells.

tile grid: This grid contains information about the tiles used to represent the cells in the logical grid.

It is possible to combine the two grids, but it is often not advisable. For instance, a cell might be a grass cell in the logical grid, but can be one of three variations of tiles. Using two separate grids allows you to “paint” the tile grid (based on the info in the logical grid) according to some rule, without the cells in the world grid having to know anything about the surrounding cells. Some algorithms require separate grids.

Symmetries

It is important to understand the symmetries of your tiling system. You can take advantage of symmetry to optimise algorithms and add variety. You should carefully consider (and then specify) the symmetries that your tiles will have before you start making them.

There are three symmetries to consider.

Symmetries

Symmetries

  • Tile shape symmetry. Ignoring any image on the tile, how can it be rotated or reflected?
  • Tile match symmetry. This is the symmetry of tiles as they can be aligned. For example, you can make a tile that is asymmetric, but can still tile seamlessly with a copy of itself rotated through any multiple of 90 degrees. A tile cannot have more match symmetries than shape symmetries.
  • Tile image symmetry. This is the actual symmetry of the image of the tile. A tiles cannot have more image symmetries than tile match symmetries.

For example, the tile inset has:

  • no image symmetry;
  • 5 match symmetries (4 axes of reflection, and rotation through 180 degrees); and
  • 7 shape symmetries (4 axes of reflection, and rotation through 90, 180 and 270 degrees)

Simple Techniques

Reflection and Rotation

If your tile set allows this, you can reflect and rotate tiles randomly. Although you can store transformed tiles explicitly in memory, you can get these transformations for free (well, almost) by just changing the way you access pixels. For example, the following code will draw a tile reflected about the vertical access

for (i = 0; i < tile_width, i++)
  for (j=0; j < tile_height; j++)
    draw_pixel(tile[tile_width – i – 1][j], i + tile_x, j + tile_y)

Of course, you will typically not draw tiles pixel-by-pixel like this, but similar techniques apply. For example, if you use shaders to draw your tiles, a transformation can be done at the vertex level.

Tile Variations

Adding variations of tiles can considerably make a tiled world more visually appealing.

A common approach is to have two versions – one is a “filler” tile, the other a tile with an interesting feature. The feature tiles are sprinkled among a set of fillers.

An alternative approach is to have a few tiles that can be interchanged. These are then placed randomly or with some pattern. Both techniques can also be combined.

Above with prominent tiles, below with random tile alternates

Tile Layers

Using layers can be a great way to soften the gridiness of a tiled world. For example, stones and flowers can be put over a layer of tiled grass, perhaps using a Poisson distribution.

Transformations that preserve seamlessness

A strategy for creating seamless tiles is to start of with seamless tiles, and then transform them using transformations that keep the seamlessness intact. For example, a seamless tile of Perlin noise is created by this process:

  1. Start with 8 tiles of white noise with frequencies 1, 1/2, 1/4, 1/8, …
  2. Blur each tile (wrapping over edges). The blur radius is proportional to the inverse of the frequencies.
  3. Add the blurred textures, using the frequency for weights.
  4. Normalise.

Below is a list of some transformations (it is not a complete list). Many of these operations are trivial, but they form important primitives of more complicated procedures.

Structural Trsnsformations

Reflection: Flip the tile vertically, horizontally, or diagonally.

Rotation: Rotate the tile through 90, 180, or 270 degrees.

Wrapped translation: Tiles can be translated in any direction by any amount.

Flipped, rotated, and translated tiles

Flipped, rotated, and translated tiles

Colour Transformations

Colour transform

Colour transform

A uniform colour transformation. Adjusting the hue, saturation, contrast, levels, etc.

The following three transformations are especially useful as primitive operations:

  • gradient mapping
  • thresholding, and
  • normalisation.
Gradient mapping, thresholding and colour (or tone) normalisation

Gradient mapping, thresholding and colour (or tone) normalisation

Wrapped Filters

Blurring, sharpening, etc. can be done with convolution. Convolution can be done in Gimp [Filters | Generic | Convolution Matrix…] or Photoshop [Filters | Other | Custom]. Only Gimp allows you to set the filter to wrap. To make it work in Photoshop properly, tile duplicates of the texture in a 3×3 square. Apply the texture, and crop to the original size around the centre (make sure this is exact).

Blur

Blur

For some examples on image convolution, see here. (The numbers below the images represent a 3×3 matrix around the centre of the matrix that you can enter into Gimp or Photoshop.) If you are interested in implementing convolution, see Digital Image Filtering (PDF) for a readable introduction.

Combining More Than One Tile

If two tiles are seamless, the following operations will yield a seamless tile:

  • a weighted sum
  • a weighted multiplication

If three tiles are seamless, one can be use to weigh a sum of the others, to yield a tile that is also seamless. This is very useful for creating more complicated effects using the simpler transforms described above.

Blending with masks

Blending with masks

Smooth Transitions between Regions

Using seamless tiles can go a long way towards hiding the underlying grid.

What you do between regions is an important consideration. There are two basic options:

  • Borders between regions coincide with borders between tiles. Thus, each tile can be of only one type of region.
  • Borders between regions are inside tiles. Thus, a single tile may have many different region types.

The main advantages of the first scheme are that it is straightforward to implement and requires fewer tiles (only n tiles for n regions).

The second scheme allows you to blend between regions, and can therefore make the game look more organic. However, for the same number of regions it uses a lot more tiles – for n regions, you will need n4tiles (if your tiles are square and asymmetric). Also, the implementation is more difficult. It is discussed in more detail in the section below.

Implementation

When using this scheme, you should not map logical cells with tile cells one-to-one. Instead, let the corners of your tiles map to the logical cells. This approach makes it very easy to select tiles that match up properly. Below is pseudo code for the algorithm. The logical grid runs from 0..m, 0..n, and the tile grid from 0..m-1, 0..n-1. Note that the 1 cell border of the logical grid should not be “part of the game” – it is only used for choosing tiles.

for (i = 0; i < m – 1; i++)
  for (j = 0; j < n – 1; j++)
    tile_grid = select_tile(
      logic_grid[i, j], //top left
      logic_grid[i + 1, j], //top right
      logic_grid[i, j + 1], //bottom left
      logic_grid[i + 1, j + 1]) //bottom right

The select_tile function takes four cells from the logic grid – these cells corresponds to the four corners of the tile – and computes the tile to use.

The easiest way to compute the tile is to use a multidimensional array or hash table. (In this case, the function call to select_tile can be replaced by a lookup in the table or array). The trick is then to set up this table to begin with.

By hand

For a small number of tiles, this is a valid approach. But it becomes unwieldy for even moderate tile sizes. Typically, it would look something like this:

tiles[0, 0, 0, 0] = tile_gggg.png
tiles[0, 0, 0, 1] = tile_gggs.png
…

Use a naming convention to load the array

For larger tile sets, this approach is much more manageable. Typically, you will put all the tiles in a folder. The program will then read in all the file names in that folder, and parse in the essential information. It will then put the filename (or loaded image) in the lookup.

In the pseudo-code below, the function parse_tile_info returns an array of integers that corresponds to the four tile corners. For example, the tile of all grass tile_gggg will yield the array [0, 0, 0, 0], and tile_gsgs (half grass and half stone) will yield [0, 2, 0, 2].

tile_file_list = get_tile_list_from_directory(tile_dir)
foreach(tile_file in tile_file_list)
{
  tile_type_corners = parse_tile_info(tile_file)
  tiles[tile_type_key
    tile_type_corners [0],
    tile_type_corners[1],
    tile_type_corners[2],
    tile_type_corners[3]] = tile_file
}

This approach requires some discipline in naming files correctly.

Use automation and serialisation

For tile sets with many regions, painting them by hand is not feasible. It is, however, possible to create them from source images (the algorithm is discussed in the next section). When you create tiles in this way, you can fill a lookup table as you create your tiles. This table can then be serialised to be loaded by your game. If your language does not have built-in serialisation support, it is easy to create an XML file, which can be easily be read in by an XML parsing library available for almost any language. You may even consider writing out a file that can be compiled (or interpreted) directly.

Automated Tile Blending

When you want to use tiles with nice transitions between regions, hand-painting all the tiles will be a chore. Ignoring symmetries, you will have to paint 256 tiles for 4 regions, 625 tiles for 5 regions, and so on.

But it is possible to the blending procedurally, so that the full set can be created from only one tile per region.

Let’s look at a very simple scheme first. We will use the blend masks below in the images below.

Horizontal and vertical blending masks

Horizontal and vertical blending masks.

Now assume that our source images (seamless tiles, each of only one region) are in a list source_tiles.

Then the algorithm for generating a full set is as follows:

foreach (tile1 in source_tiles)
  foreach (tile2 in source_tiles)
  {
    new_tile = blend(tile1, tile2, horizontal_blend)
    horizontal_set.append(new_tile)
  }
foreach (tile1 in horizontal_set)
  foreach(tile2 in horizontal_set)
  {
    new_tile = blend(tile1, tile2, vertical_blend)
    full.append(new_tile)
  }

The blend function constructs the new tile by performing a weighted sum of the pixels, using the blend mask for weights. Here is how it looks in pseudo code:

blend(tile1, tile2, blend)
{
  for(i = 0; i < tile1.width; i++)
    for(j = 0; j < height(tile1); j++)
    {
      color1 = tile1[i, j]
      color2 = tile2[i, j]
      weight1 = blend[i, j]
      weight2 = 1 – blend[i, j]
      new_color = color1 * weight1 + color2 * weight2
      new_tile[i, j] = new_color
    }
  return new_tile
}

It is that simple! To create the lookup table as described in the previous section, you have to keep track of indices. Here is the modified algorithm:

k = 0
index_array[][2]
for (i = 0; i < source_tiles_count; i  )
{
  tile1 = source_tiles[i]
  for (j = 0; j < source_tiles_count; j  )
  {
    tile2 = source_tiles[j]
    new_tile = blend(tile1, tile2, horizontal_blend)
    horizontal_set.append(new_tile)
    index_array[k][0] = i
    index_array[k][1] = j
    k
  }

}

for (i = 0; i < k; i  )
{
  tile1 = source_tiles[i]
  for (j = 0; j < k; j  )
  {
    tile2 = source_tiles[j]
    new_tile = blend(tile1, tile2, vertical_blend)
    index_0 = index_array[i][0]
    index_1 = index_array[i][1]
    index_2 = index_array[j][0]
    index_3 = index_array[j][2]
    lookup_table[index_0, index_1, index_2, index_3] = new_tile
  }

}

This algorithm looks a lot more complicated than it really is. Here is what is going on:

When we create the horizontally blended tiles, we store two values (that corresponds with the two tiles blended together) in an array. For example, a tile composed from the first (0th) and second (1st) tiles from the list, will have the values 0 and 1 stored in the index array.

When we combine the horizontal tile set, we look up the two values index values for each of these tiles. The four values together forms the indices for the lookup table.

For more information about generating combinatorial structures, see Donald Knuth’s Generating all n-tuples.

Different Blend Schemes

The blending approach above is very powerful: to get a different way of combining tiles, we only need to change the blend mask. The basic rule is:

  • your source tiles should be seamless,
  • your vertical blend mask should be horizontally seamless,
  • your horizontal blend mask should be vertically seamless.

Here are a few ideas for different blend images:

Blendmasks

Blendmasks

You can use variations in blend masks to get more tiles. As long as the blend tiles are seamless and interchangeable, the resulting tiles will also be seamless.

You can use variations in blend masks to get more tiles. As long as the blend tiles are seamless and interchangeable, the resulting tiles will also be seamless.

You can use variations in blend masks to get more tiles. As long as the blend tiles are seamless and interchangeable, the resulting tiles will also be seamless.

As you can see, the area where four regions meet looks a bit artificial.

As you can see, the area where four regions meet looks a bit artificial.

Even with smoother blending, the result is not totally satisfactory.

Even with smoother blending, the result is not totally satisfactory.

To solve this problem, you might need to modify our algorithm to use different masks for tiles with corners. Classify tiles by the following scheme:

From top-left: Centre tiles, Straight-edge tiles, Corner tiles, Cross tiles, Diagonal-line tiles, Half-edge tiles

From top-left: Centre tiles, Straight-edge tiles, Corner tiles, Cross tiles, Diagonal-line tiles, Half-edge tiles

Each of these classes are treated differently, using specific blend masks. The exact format of your algorithm will depend on how you design the masks. In general each set of tiles will be generated with a separate algorithm. Here, for example, is what you would use to generate the cross tiles:

foreach (tile1 in tile_set)
  foreach (tile2 in tile_set)
  {
    new_tile = blend (tile1, tile2, blend_mask_cross)
    lookup_table[i, j, j, i] = new_tile
  }

A very attractive (but more complicated) method of solving the transition problem can be found in the GameDev.net article Tile/Map-Based Game Techniques: Handling Terrain Transitions.

Notes on Region Blending for Hexagonal and Triangular Tiles

The first important point of applying the algorithms above to hexagonal and triangular tiles is that your logical and tile grids will have different shapes: if your tiles are triangular, the grid will be hexagonal, and vice versa. It also has the effect that hexagonal tiles look triangular, and vice versa.

The blending algorithm depends on how your tiles are orientated. In my (square) image processing code, i use a data structure that will give me an index iterator that I can use to iterate over all indices of a grid. This allows me to not ever have to worry about boundaries. With triangular and hexagonal tiles, this is even more useful. It will allow you to write code for blending like this:

foreach (index in tile1.index_iter())
{
   new_tile[index] = mix_color(tile1[index], tile2[index], blend_mask[index])
}

Using this approach also allows you to pack your tiles more efficiently without complicating higher level algorithms (such as blending) if that is a concern. (Packing a regular triangle in a rectangle wastes 50% space. This can be totally eliminated).

Dynamic Colouring

A simple way to introduce variation is to change the colours of tiles slightly. When done in a controlled way, a large region can be made less homogeneous.

The general procedure is as follows:

  1. Use plain, uniform tiles as source tiles and create a blended set.
  2. Generate the tile set of blended tiles (without any colour modifications).

In your program, you maintain two tile grids. The one is for the colour variations, the other for the texture tiles. When you draw the tiles, you can combine the two tiles by adding, multiplying, or blending colours. (This is easy to implement in a shader, for example).

Smooth colour set

Smooth colour set

An extreme approach is to use no colour in your texture tiles, and produce all colour from your colour tiles. This allows you to have more variety with fewer tiles. Adding a bit of noise to your colour tiles often enhance the apparent detail in the tiles.

Noisy colour set

Noisy colour set

Adding, multiplying or blending colours can reduce the colour range of coloured textures, and therefore they are less attractive than you would imaging. There are simple ways to increase the colour range.

Of course, the colour and texture tiles need not coincide. Here is the effect applied to a texture:

Original

Original

Colour Tiles

Colour Tiles

Colour Addition (The colour tiles have been darkened before addition)

Colour Addition (The colour tiles have been darkened before addition)

Colour multiplication (The colour tiles have been lightened before multiplication)

Colour multiplication (The colour tiles have been lightened before multiplication)

Blending

Blending

You can often get better results by using a gradient map. Thus you use black and white colour tiles, map it to a gradient, and blend, add or multiply with the texture tiles.

Gradient map

Gradient map

Tile Tools

Except for standard image editing software, there are hundreds of tools specially suited for processing tiles. These below are my favourite ones.

TextureMaker

A powerful tool for making seamless tiles. The best thing about it is the interface for painting tiles: tiles are automatically wrapped, so while you paint, you can see that it is seamless. It has more filters than Photoshop.

MapZone

This tool is a procedural texture generator. It is extremely powerful and has built-in support for making seamless tiles. It is actually built for 3D texturing, so it also comes with support for bump maps and so on. The free version is full-featured, except that it does not come with real-time support.

TuDee

One of the guys at Luma developed this extremely useful 2D level editor. It is geared for working with tiles, and has a rich feature set. Its greatest feature is its exporting system: it allows you to write your own exporter in a very straightforward manner, which means that you can support your own custom file formats.

A Few Tips

  • You must carefully consider whether the repetitiveness and geometric symmetry listed as “problems” of tiled games are indeed problems for your game, before proceeding to implement some of the techniques here. The simplicity of vanilla tiles is in some cases the charm of a game.
  • Calculate the number of tiles that will be required for a specific system before you make them (whether it is by hand or machine). This number might be much bigger than you think; you might be compelled to constrain the system a bit.
  • Procedural techniques do not replace the artist; they multiply what an artist can do. To remind yourself of how awful procedurals can look without proper artistic input, look here. Always consult with an artist to make sure that your procedural techniques come to their best. If you work alone, consult the artist within you.
  • A tile system can become arbitrarily complex, and you must guard against it becoming too complex. If your tile engine is more complicated than a 3D game engine, you are (probably) on the wrong track.
  • When using dynamic colouring, it is better to use bright, saturated colours for the colour tiles, and control blending with parameters. This allows you to use the same colour set for all textures. For the same reason, it is better to use high contrast imagery for the texture tiles.
  • When you build levels by hand, and your tile sets with smooth transitions between regions, do not build the tile grid – build the logical grid, and construct tile grid algorithmically. Because of the number of tiles, building with full-blend sets can be cumbersome.
  • Microsoft Excel or Open office Calc can serve as crude level builders. You can use strings or codes to define a region type, and use conditional formatting to get visual feedback. The file can be saved in some XML format, and be imported with simple-to-write code.

More Resources

This article looks at many key issues for tile programming, including layered maps, see-through roofs and some special effects.

A 104-page PDF tutorial about using tiles with Flash.

Lots of short tutorials for creating tile based games.

This article looks at some of the issues you need to consider when using tile sets for terrains.

For the mathematically inclined: Wang tiles formalise tile matching. See the paper Wang Tiles for Image and Texture Generation (PDF).

Download Source Code

You can download a Python implementation of some of these blending techniques from here. The file blend_demo.py has the function relevant to this tutorial.


About Herman Tulleken

Herman Tulleken is a game developer co-founder of Plinq. He thinks computers are a necessary evil to make games, and sees frying a CPU or two in the process as a worthy sacrifice.

2 thoughts on “Getting More out of Seamless Tiles

Comments are closed.