Free and Open Source real time strategy game with a new take on micro-management

Making Tiles

From Globulation2

Jump to: navigation, search

This page briefly explains the procedure used in making Globulation 2's new tileset. This tileset was written by me (Andrew Sayers) around Christmas 2005. As of the time of writing, the tiles were still being polished up for inclusion in the game.

I am a programmer rather than an artist, and I've taken a programmer's approach to making tiles. The tiles were made with The GIMP, which I've never used for a significant project before. As such, this page should be used as an informative guide, rather than a strict script to follow.

Types of Terrain

There are three basic terrain types in Globulation 2 - water, sand and grass. Water is represented in the game by a large tile called "water0.png". I didn't create this tile, and don't know how it was done. Grass and sand are represented by 32x32 pixel tiles. There are 16 of each tile, starting at "terrain0.png" and "terrain128.png", respectively. There are also a large number of 32x32 tiles representing transitions between grass and sand, and between sand and water. Because water is represented by a larger tile, transitions between sand and water are represented by 32x32 pixels where the water part is transparent.

Grass

Before creating the grass tiles, I tried to think about what grass looks like, and how to express that in a graphics package. I eventually decided that grass is a mess of diffuse blobs of slightly differing shades of green. So to produce grass, I did the following:

  1. created a new 128x128 image (i.e. large enough to make 16 32x32 tiles)
  2. Filled it the with grey. The shade of grey affects brightness of the final results
  3. Used the tool filters->noise->scatter RGB, with "independent RGB" deselected
  4. Used layer->colours->curves. Set the maximum value on the red and blue channels to 50%. Left the green channel alone. On the value channel, added a 3rd point in the middle of the curve at about 30% height
  5. Used filters->blur->gaussian blur, with the blur radius set to 2 on both the horizontal and vertical axis
  6. Used a Scheme script to split the image into 16 segments. This was my first ever bit of GIMP scripting, so the script was pretty ugly. To use it, go to Xtns->Script Fu->Script Fu console. In the "current command" text box, paste the following lines
(define (my-copy-square x y from base) (gimp-rect-select 1 (* x 32) (* y 32) 32 32 2 0 0) (gimp-edit-copy (car (gimp-image-get-active-drawable 1))) (let* ((image (car (gimp-image-new 32 32 RGB))) (layer (car (gimp-layer-new image 32 32 RGB-IMAGE "foobar" 100 NORMAL-MODE))) (filename (string-append "terrain" (string-append (number->string (+ base (+ x (* 4 y)))) ".png")))) (gimp-image-add-layer image layer 0) (gimp-display-new image) (gimp-rect-select image 0 0 32 32 0 0 0) (gimp-edit-paste (car (gimp-image-get-active-drawable image)) 0) (gimp-image-flatten image) (file-png-save 1 image (car (gimp-image-get-active-drawable image)) filename filename 0 9 0 0 0 1 1) (gimp-display-delete image) (print filename) ))                                                                                                                                     
(define (my-copy-row y from base) (my-copy-square 0 y from base) (my-copy-square 1 y from base) (my-copy-square 2 y from base) (my-copy-square 3 y from base) )
(define (my-copy-all from base) (my-copy-row 0 from base) (my-copy-row 1 from base) (my-copy-row 2 from base) (my-copy-row 3 from base) )
(my-copy-all (aref (car (cdr (gimp-image-list))) 0) 128)

Sand

Whereas grass is a mass of blobs, sand is made up of rather grainier pebbles, so needs a different approach. After some thought (and some exploring around the menu system), I came up with the following:

  1. Created a new 672x672 image (the reason for this size will become apparent later)
  2. Filled it with yellow. The shade of yellow will be the basic colour of the eventual sand
  3. Filters->Distorts->Mosaic, with a low neatness and a tile size of 16
  4. If you look carefully, the mosaic tool messes up the right and bottom edges of the screen, with a white or black line and some shading by it. I don't know why it does this, but rather than fix it, I shrank the canvas

size down to 640x480 with an offset of 0, so as to remove the corrupt sections.

  1. Scaled the image down to 128x128 pixels. This obscures the mosaic nature of the image, and just leaves a nice gritty patch of yellow
  2. Cut the image up with slightly less ugly versions of the scripts from before:
(define (my-copy-square x y from base) (gimp-rect-select from (* x 32) (* y 32) 32 32 2 0 0) (gimp-edit-copy (car (gimp-image-get-active-drawable from))) (let* ((image (car (gimp-image-new 32 32 RGB))) (layer (car (gimp-layer-new image 32 32 RGB-IMAGE "foobar" 100 NORMAL-MODE))) (filename (string-append "terrain" (string-append (number->string (+ base (+ x (* 4 y)))) ".png")))) (gimp-image-add-layer image layer 0) (gimp-display-new image) (gimp-rect-select image 0 0 32 32 0 0 0) (gimp-edit-paste (car (gimp-image-get-active-drawable image)) 0) (gimp-image-flatten image) (file-png-save 1 image (car (gimp-image-get-active-drawable image)) filename filename 0 9 0 0 0 1 1) (gimp-display-delete image) ))
(define (my-copy-row y from base) (my-copy-square 0 y from base) (my-copy-square 1 y from base) (my-copy-square 2 y from base) (my-copy-square 3 y from base) )
(define (my-copy-all from base) (gimp-image-undo-disable from) (my-copy-row 0 from base) (my-copy-row 1 from base) (my-copy-row 2 from base) (my-copy-row 3 from base) (gimp-selection-none from) (gimp-image-undo-enable from) )
(my-copy-all (aref (car (cdr (gimp-image-list))) 0) 128)

Transitions

Transitions took much longer to do, because they are far more complex. In the end, I made a Perl script to do the whole job, but the basic process to do one is:

  1. Create an image with grass as a background layer (or no background for water), and sand as an over layer
  2. Select roughly the area of the screen you want cut out
  3. Script-Fu->Filters->Distress selection
  4. In order that all the tiles marry up in the game, the edges of the cut-out area have to be exactly halfway along the width/height of the tile:
    1. Create a new channel from the selection
    2. Use the "transform perspective" tool on the channel to get the edges in the right place
    3. Turn the channel back into a selection
  5. Delete the selection
  6. Save the image

The following script does this automatically:

#!/usr/bin/perl

use Gimp qw(:auto);
use Gimp::Fu;

sub tile_size() { 32 };
sub debug() { 0 };

use strict;
use warnings;

sub topleft() { 0 };
sub bottomleft() { 1 };
sub topright() { 2 };
sub bottomright() { 3 };

# $side1 
sub warp_image {
    my ($img, $selection_area, $side1, $side2, $threshold, $spread, $granularity, $smooth, $smooth_horizontally, $smooth_vertically) = @_;

    die "Diagonal cuts are not allowed" if ($side1 + $side2 == 3);

    ($side2, $side1) = ($side1, $side2) if ($side1 > $side2);

    my ($width, $height) = ( tile_size * tile_size, tile_size * tile_size );

    $width /= 2 unless ($side1 == topleft && $side2 == topright) || ($side1 == bottomleft && $side2 == bottomright);
    $height /= 2 unless ($side1 == topleft && $side1 == bottomleft) || ($side1 == topright && $side2 == bottomright);

    gimp_free_select($img, @$selection_area, 2, 0, 0, 0 );

    # Then distress the selection...
    script_fu_distress_selection(1, $img, gimp_image_get_active_layer($img), $threshold, $spread, $granularity, $smooth, $smooth_horizontally, $smooth_vertically);

    # Then warp the perspective such that the edges are always in the right place...
    my $channel = gimp_selection_save($img);
    gimp_selection_none($img);

    my ($x0, $y0) = (0, 0); # Co-ordinates of the transformed top-left corner
    if    ($side1 ==    topright) { while ($x0 < tile_size-1 && gimp_drawable_get_pixel($channel, $x0,           0) < 128) { ++$x0 }; $x0=tile_size-$width/(tile_size-$x0); }
    elsif ($side1 == bottomright) { while ($x0 < tile_size-1 && gimp_drawable_get_pixel($channel, $x0, tile_size-1) < 128) { ++$x0 }; $x0=tile_size-$width/(tile_size-$x0); };
    if    ($side1 == bottomleft ) { while ($y0 < tile_size-1 && gimp_drawable_get_pixel($channel,           0, $y0) < 128) { ++$y0 }; $y0=tile_size-$height/(tile_size-$y0); }
    elsif ($side1 == bottomright) { while ($y0 < tile_size-1 && gimp_drawable_get_pixel($channel, tile_size-1, $y0) < 128) { ++$y0 }; $y0=tile_size-$height/(tile_size-$y0); }

    print("X0=$x0	Y0=$y0\n") if debug;

    my ($x1, $y1) = (tile_size, 0); # Co-ordinates of the transformed top-right corner
    if    ($side1 ==    topleft) { while ($x1 > 0 && gimp_drawable_get_pixel($channel, $x1-1,           0) < 128) { --$x1 }; $x1=$width/$x1; }
    elsif ($side1 == bottomleft) { while ($x1 > 0 && gimp_drawable_get_pixel($channel, $x1-1, tile_size-1) < 128) { --$x1 }; $x1=$width/$x1; };
    if ($side1 == bottomleft && $side2 == bottomright) {
	while ($y1 < tile_size-1 && gimp_drawable_get_pixel($channel, tile_size-1, $y1) < 128) { ++$y1 }; $y1=tile_size-$height/(tile_size-$y1);
    } else {
	$y1 = $y0;
    };

    print("X1=$x1	Y1=$y1\n") if debug;

    my ($x2, $y2) = (0, tile_size); # Co-ordinates of the transformed bottom-left corner
    if    ($side1 ==    topright) { while ($x2 < tile_size-1 && gimp_drawable_get_pixel($channel, $x2, 0) < 128) { ++$x2 }; $x2=tile_size-$width/(tile_size-$x2); }
    elsif ($side1 == bottomright) { $x2 = $x0 };
    if ($side1 == topleft && ($side2 == topleft || $side2 == topright)) {
	while ($y2 > 0 && gimp_drawable_get_pixel($channel,           0, $y2-1) < 128) { --$y2 }; $y2=$height/$y2;
    } elsif ($side2 == topright) {
	while ($y2 > 0 && gimp_drawable_get_pixel($channel, tile_size-1, $y2-1) < 128) { --$y2 }; $y2=$height/$y2;
    };

    print("X2=$x2	Y2=$y2\n") if debug;

    my ($x3, $y3) = (tile_size, tile_size); # Co-ordinates of the transformed bottom-right corner
    if ($side1 == topleft && $side2 == bottomleft) {
	while ($x3 > 0 && gimp_drawable_get_pixel($channel, $x3-1, tile_size-1) < 128) { --$x3 }; $x3=$width/$x3;
    } else {
	$x3 = $x1;
    };
    if ($side1 == topleft && $side2 == topright) {
	while ($y3 > 0 && gimp_drawable_get_pixel($channel, tile_size-1, $y3-1) < 128) { --$y3 }; $y3=$height/$y3;
    } else {
	$y3 = $y2;
    };

    print("X3=$x3	Y3=$y3\n") if debug;

    gimp_drawable_transform_perspective($channel,
					$x0, $y0, $x1, $y1, $x2, $y2, $x3, $y3,
					0, 2, 1, 3, 1);

    return $channel;
};

sub make_image {
    my ($count, $background, $overlay, $quadrant1, $quadrant2, $terrain_no, $invert, $threshold, $spread, $granularity, $smooth, $smooth_horizontally, $smooth_vertically) = @_;
    # set tracing, disable this to get rid of the debugging output
    Gimp::set_trace(TRACE_CALL) if debug;

    ($quadrant2, $quadrant1) = ($quadrant1, $quadrant2) if $quadrant1 > $quadrant2;

    my @images;
    while ($count--) { 
	my $img = gimp_image_new(tile_size, tile_size, RGB);

	# Load the layers of the image from files
	my $img1;

	if (-e $background) {
	    $img1 = gimp_file_load(1, $background, $background);
	    gimp_image_add_layer($img, gimp_layer_new_from_drawable(gimp_image_get_active_layer($img1), $img), 0);
	    gimp_image_delete($img1);
	    $background =~ s|([0-9]+)([^/]*)$|($1+1) . $2|e;
	};

	$img1 = gimp_file_load(1, $overlay, $overlay);
	gimp_image_add_layer($img, gimp_layer_new_from_drawable(gimp_image_get_active_layer($img1), $img), 0);
	gimp_image_delete($img1);
	$overlay =~ s|([0-9]+)([^/]*)$|($1+1) . $2|e;

	my @quadrants = ([ [ 8, [ tile_size/2,         0, tile_size/2, tile_size/2,		0, tile_size/2,			        0, 0           ] ], topleft, topleft ],
			 [ [ 8, [         0, tile_size/2, tile_size/3, tile_size*2/3,		tile_size/2, tile_size,		0          , tile_size ] ], bottomleft, bottomleft ],
			 [ [ 8, [ tile_size/2,         0, tile_size/3, tile_size/3,		tile_size, tile_size/2,		tile_size, 0           ] ], topright, topright ],
			 [ [ 8, [ tile_size, tile_size/2, tile_size*2/3, tile_size*2/3,	tile_size/2, tile_size,		tile_size  , tile_size ] ], bottomright, bottomright ],
			 );

	my @halves = (
		      undef,
		      [ [ 8, [ 0,  0, tile_size/2,  0, tile_size/2, tile_size,  0, tile_size ] ], topleft, bottomleft ],
		      [ [ 8, [ 0,  0,  0, tile_size/2, tile_size, tile_size/2, tile_size,  0 ] ], topleft, topright ],
		      undef,
		      [ [ 8, [ 0, tile_size,	tile_size, tile_size,		tile_size, tile_size/2,		0, tile_size/2 ] ], bottomleft, bottomright ],
		      [ [ 8, [ tile_size/2, 0,	tile_size/2, tile_size,		tile_size, tile_size,		tile_size, 0   ] ], topright, bottomright ],
		      );

	if ($quadrant1 == $quadrant2) {
	    print "Creating single quadrant\n" if debug;
	    my $channel = warp_image($img, @{$quadrants[$quadrant1]}, $threshold, $spread, $granularity, $smooth, $smooth_horizontally, $smooth_vertically);
	    gimp_selection_combine($channel, 2);
	} elsif ($quadrant1 == topleft && $quadrant2 == bottomright || $quadrant1 == bottomleft && $quadrant2 == topright) {
	    print "creating two quadrants\n" if debug;
	    my $channel1 = warp_image($img, @{$quadrants[$quadrant1]}, $threshold, $spread, $granularity, $smooth, $smooth_horizontally, $smooth_vertically);
	    my $channel2 = warp_image($img, @{$quadrants[$quadrant2]}, $threshold, $spread, $granularity, $smooth, $smooth_horizontally, $smooth_vertically);
	    gimp_selection_combine($channel1, 2);
	    gimp_selection_combine($channel2, 0);
	} else {
	    print "creating one half\n" if debug;
	    my $channel = warp_image($img, @{$halves[$quadrant1 + $quadrant2]}, $threshold, $spread, $granularity, $smooth, $smooth_horizontally, $smooth_vertically);
	    gimp_selection_combine($channel, 2);
	};

	gimp_selection_invert($img) if $invert;
	gimp_image_set_active_layer($img, ${gimp_image_get_layers($img)});
	gimp_edit_clear(gimp_image_get_active_layer($img));
	gimp_selection_none($img);
	gimp_image_merge_visible_layers($img, 1) if -e $background;

	if ($terrain_no) {
	    print("Saving terrain$terrain_no.png\n");
	    file_png_save(1, $img, gimp_image_get_active_layer($img), "terrain$terrain_no.png", "terrain$terrain_no.png", 0, 9, 0, 0, 0, 1, 0);
	    gimp_image_delete($img);
	    ++$terrain_no;
	} else {
	    print "Storing $img\n" if debug;
	    push @images, $img;
	};
    };
    print("Returning ", $#images+1, " images\n") if debug;
    return @images;
};

sub make_all_images {
    our ($source, $overwrite) = @_;
    my (@backgrounds, @overlays);
    foreach ( 0 .. 15 ) {
	push @backgrounds, $source . '/terrain' . ($_) . '.png';
	push @overlays, $source . '/terrain' . ($_+128) . '.png';
    };

    gimp_progress_init('making images...');

    my @images;
    foreach ( 0 .. 7 ) {
	sub can_write { $overwrite || !-e './terrain' . $_[0] . '.png' };

	gimp_progress_update(1/(8-$_));

	# Tiles backed by grass

	# Sand in one quadrant
	push(@images, make_image(1, $backgrounds[  $_], $overlays[  $_], bottomright, bottomright, 16+$_, 0, 32+int(rand(196)), 12, 3, 1, 1, 1)) if can_write(16+$_);
	push(@images, make_image(1, $backgrounds[8+$_], $overlays[  $_], bottomleft , bottomleft , 24+$_, 0, 32+int(rand(196)), 12, 3, 1, 1, 1)) if can_write(24+$_);
	push(@images, make_image(1, $backgrounds[  $_], $overlays[8+$_], topleft    , topleft    , 32+$_, 0, 32+int(rand(196)), 12, 3, 1, 1, 1)) if can_write(32+$_);
	push(@images, make_image(1, $backgrounds[8+$_], $overlays[8+$_], topright   , topright   , 40+$_, 0, 32+int(rand(196)), 12, 3, 1, 1, 1)) if can_write(40+$_);

	# Sand in one half
	push(@images, make_image(1, $backgrounds[  $_], $overlays[  $_], topleft    , topright   , 48+$_, 0, 32+int(rand(196)), 12, 3, 1, 1, 1)) if can_write(48+$_);
	push(@images, make_image(1, $backgrounds[8+$_], $overlays[  $_], bottomleft , bottomright, 56+$_, 0, 32+int(rand(196)), 12, 3, 1, 1, 1)) if can_write(56+$_);
	push(@images, make_image(1, $backgrounds[  $_], $overlays[8+$_], bottomleft , topleft    , 64+$_, 0, 32+int(rand(196)), 12, 3, 1, 1, 1)) if can_write(64+$_);
	push(@images, make_image(1, $backgrounds[8+$_], $overlays[8+$_], bottomright, topright   , 72+$_, 0, 32+int(rand(196)), 12, 3, 1, 1, 1)) if can_write(72+$_);

	# Sand in one quadrant
	push(@images, make_image(1, $backgrounds[  $_], $overlays[  $_], bottomright, bottomright, 80+$_, 1, 32+int(rand(196)), 12, 3, 1, 1, 1)) if can_write(80+$_);
	push(@images, make_image(1, $backgrounds[8+$_], $overlays[  $_], bottomleft , bottomleft , 88+$_, 1, 32+int(rand(196)), 12, 3, 1, 1, 1)) if can_write(88+$_);
	push(@images, make_image(1, $backgrounds[  $_], $overlays[8+$_], topleft    , topleft    , 96+$_, 1, 32+int(rand(196)), 12, 3, 1, 1, 1)) if can_write(96+$_);
	push(@images, make_image(1, $backgrounds[8+$_], $overlays[8+$_], topright   , topright   , 104+$_, 1, 32+int(rand(196)), 12, 3, 1, 1, 1)) if can_write(104+$_);


	# Tiles with no backing

	# Sand in one quadrant
	push(@images, make_image(1, , $overlays[  $_], bottomright, bottomright, 144+$_, 1, 32+int(rand(196)), 12, 3, 1, 1, 1)) if can_write(144+$_);
	push(@images, make_image(1, , $overlays[  $_], bottomleft , bottomleft , 152+$_, 1, 32+int(rand(196)), 12, 3, 1, 1, 1)) if can_write(152+$_);
	push(@images, make_image(1, , $overlays[8+$_], topleft    , topleft    , 160+$_, 1, 32+int(rand(196)), 12, 3, 1, 1, 1)) if can_write(160+$_);
	push(@images, make_image(1, , $overlays[8+$_], topright   , topright   , 168+$_, 1, 32+int(rand(196)), 12, 3, 1, 1, 1)) if can_write(168+$_);

	# Sand in one half
	push(@images, make_image(1, , $overlays[  $_], bottomleft , bottomright, 176+$_, 0, 32+int(rand(196)), 12, 3, 1, 1, 1)) if can_write(176+$_);
	push(@images, make_image(1, , $overlays[  $_], topleft    , topright   , 184+$_, 0, 32+int(rand(196)), 12, 3, 1, 1, 1)) if can_write(184+$_);
	push(@images, make_image(1, , $overlays[8+$_], topright   , bottomright, 192+$_, 0, 32+int(rand(196)), 12, 3, 1, 1, 1)) if can_write(192+$_);
	push(@images, make_image(1, , $overlays[8+$_], topleft    , bottomleft , 200+$_, 0, 32+int(rand(196)), 12, 3, 1, 1, 1)) if can_write(200+$_);

	# Sand in three quadrants
	push(@images, make_image(1, , $overlays[  $_], bottomright, bottomright, 208+$_, 0, 32+int(rand(196)), 12, 3, 1, 1, 1)) if can_write(208+$_);
	push(@images, make_image(1, , $overlays[  $_], bottomleft , bottomleft , 216+$_, 0, 32+int(rand(196)), 12, 3, 1, 1, 1)) if can_write(216+$_);
	push(@images, make_image(1, , $overlays[8+$_], topleft    , topleft    , 224+$_, 0, 32+int(rand(196)), 12, 3, 1, 1, 1)) if can_write(224+$_);
	push(@images, make_image(1, , $overlays[8+$_], topright   , topright   , 232+$_, 0, 32+int(rand(196)), 12, 3, 1, 1, 1)) if can_write(232+$_);

	# Sand in two quadrants
	push(@images, make_image(1, , $overlays[$_], topright, bottomleft , 240+$_, 0, 32+int(rand(196)), 12, 3, 1, 1, 1)) if can_write(240+$_);
	push(@images, make_image(1, , $overlays[$_], topleft , bottomright, 248+$_, 0, 32+int(rand(196)), 12, 3, 1, 1, 1)) if can_write(248+$_);

    };

    # Sand in two quadrants, backed by grass
    push(@images, make_image(1, $backgrounds[0], $overlays[0], topright, bottomleft , 112, 0, 32+int(rand(196)), 12, 3, 1, 1, 1)) if can_write(112);
    push(@images, make_image(1, $backgrounds[1], $overlays[1], topright, bottomleft , 113, 0, 32+int(rand(196)), 12, 3, 1, 1, 1)) if can_write(113);
    push(@images, make_image(1, $backgrounds[2], $overlays[2], topleft , bottomright, 114, 1, 32+int(rand(196)), 12, 3, 1, 1, 1)) if can_write(114);
    push(@images, make_image(1, $backgrounds[3], $overlays[3], topleft , bottomright, 115, 1, 32+int(rand(196)), 12, 3, 1, 1, 1)) if can_write(115);
    push(@images, make_image(1, $backgrounds[4], $overlays[4], topright, bottomleft , 116, 0, 32+int(rand(196)), 12, 3, 1, 1, 1)) if can_write(116);
    push(@images, make_image(1, $backgrounds[5], $overlays[5], topright, bottomleft , 117, 0, 32+int(rand(196)), 12, 3, 1, 1, 1)) if can_write(117);
    push(@images, make_image(1, $backgrounds[6], $overlays[6], topright, bottomleft , 118, 0, 32+int(rand(196)), 12, 3, 1, 1, 1)) if can_write(118);
    push(@images, make_image(1, $backgrounds[7], $overlays[7], topright, bottomleft , 119, 0, 32+int(rand(196)), 12, 3, 1, 1, 1)) if can_write(119);

    push(@images, make_image(1, $backgrounds[0], $overlays[7], topleft , bottomright, 120, 0, 32+int(rand(196)), 12, 3, 1, 1, 1)) if can_write(120);
    push(@images, make_image(1, $backgrounds[1], $overlays[6], topleft , bottomright, 121, 0, 32+int(rand(196)), 12, 3, 1, 1, 1)) if can_write(121);
    push(@images, make_image(1, $backgrounds[2], $overlays[5], topright, bottomleft , 122, 1, 32+int(rand(196)), 12, 3, 1, 1, 1)) if can_write(122);
    push(@images, make_image(1, $backgrounds[3], $overlays[4], topright, bottomleft , 123, 1, 32+int(rand(196)), 12, 3, 1, 1, 1)) if can_write(123);
    push(@images, make_image(1, $backgrounds[4], $overlays[3], topleft , bottomright, 124, 0, 32+int(rand(196)), 12, 3, 1, 1, 1)) if can_write(124);
    push(@images, make_image(1, $backgrounds[5], $overlays[2], topleft , bottomright, 125, 0, 32+int(rand(196)), 12, 3, 1, 1, 1)) if can_write(125);
    push(@images, make_image(1, $backgrounds[6], $overlays[1], topright, bottomleft , 126, 1, 32+int(rand(196)), 12, 3, 1, 1, 1)) if can_write(126);
    push(@images, make_image(1, $backgrounds[7], $overlays[0], topright, bottomleft , 127, 1, 32+int(rand(196)), 12, 3, 1, 1, 1)) if can_write(127);



    # Sand in two quadrants
    push(@images, make_image(1, , $overlays[2], topleft , bottomright, 242, 1, 32+int(rand(196)), 12, 3, 1, 1, 1)) if can_write(242);
    push(@images, make_image(1, , $overlays[2], topright, bottomleft , 243, 1, 32+int(rand(196)), 12, 3, 1, 1, 1)) if can_write(243);

    return @images;
};

# register("glob_terrain_blend",
#          "Blends between two Glob2 tiles",
#          undef,
#          "Andrew Sayers",
#          "Andrew Sayers <glob2-terrain\@pileofstuff.org>",
#          "2005-12-30",
#          N_"<Toolbox>/Xtns/Glob2/Terrain Blend...",
#          "*",
#          [
# 	  [PF_INT, "count", "number of tiles", 1 ],
# 	  [PF_FILE, "background", "Background tile (leave this blank for a transparent background)", '/home/andrew/.glob2/data/gfx/terrain0.png' ],
# 	  [PF_FILE, "overlay", "Overlay tile", '/home/andrew/.glob2/data/gfx/terrain130.png' ],
# 	  [PF_RADIO, 'quadrant1', 'First quadrant the overlay will be displayed in', 0,
# 	   [ 'top-left' => topleft, 'bottom-left' => bottomleft, 'top-right' => topright, 'bottom-right' => bottomright ] ],
# 	  [PF_RADIO, 'quadrant2', 'First quadrant the overlay will be displayed in', 0,
# 	   [ 'top-left' => topleft, 'bottom-left' => bottomleft, 'top-right' => topright, 'bottom-right' => bottomright ] ],
# 	  [PF_INT, 'start', 'if non-zero, images will be saved as terrain$n.png, starting at $n=start' ],
# 	  [PF_BOOL, 'invert', 'invert the selection the overlay will appear in' ],
# 	  [PF_SLIDER, "threshold", "threshold (bigger 1 <--> 255 smaller)", 127, [ 1, 255, 1] ],
# 	  [PF_INT, "spread", "Spread:", 8 ],
# 	  [PF_INT, "granularity", "Granularity (1 is low):", 2],
# 	  [PF_INT, 'smooth', 'Smooth:', 1 ],
# 	  [PF_BOOL, 'smooth_horizontally', 'Smooth horizontally', 1 ],
# 	  [PF_BOOL, 'smooth_vertically', 'Smooth vertically', 1 ],
#          ],
#     \&make_image);

register("glob_terrain_blend_all",
         "Create several blends of tiles",
         undef,
         "Andrew Sayers",
         "Andrew Sayers <glob2-terrain\@pileofstuff.org>",
         "2005-12-30",
         N_"<Toolbox>/Xtns/Glob2/Multiple Terrain Blend...",
         "*",
         [
	  [PF_FILE, "source", "Directory containing the sand and grass tiles to be used", '/home/andrew/.glob2/data/gfx/' ],
	  [PF_BOOL, "overwrite", "Should we overwrite tiles that have already been created?", 0 ],
         ],
	 \&make_all_images
	 );

exit main;

To use this Perl script, first start The GIMP in non-interactive mode with the Perl server enabled:

gimp -i -b '(extension-perl-server 0 0 0)'

Then run the script. You will be prompted to enter the directory that your grass and sand images are located in.

Once I had created all of the tiles, I created a directory $HOME/.glob2/data/gfx and put the new tiles in there.