[3D] Generating terrain by using a heightmap
This is a translated version of my
Dutch tutorial. If you are Dutch, you might prefer to read that one, because it is probably much better formulated than this one.
While Game Maker is mainly a 2D engine, a lot of cool things can be done in 3D. This tutorial learns you how to generate 3D terrain, based on a heightmap. The scripts are pretty fast, since I use trianglestrips instead of trianglelists.
It also defines the normals correctly, which allows you to use lighting.
An example of 3D terrain Some specifications:
- GM6+
- Requires the Pro edition.
Table of contents- Table of contents
- What is a heightmap(and how does it work?)?
- Making a heightmap
- Reading a heightmap
- Drawing the terrain
- Finding the z-value of a position on the terrain
- Drawing a texture over the entire terrain, instead of repeating it every cel.
- Examples
- FAQ
- Final Notes
What is a heightmap(and how does it work?)?A heightmap is a black and white(usually), topdown print of a landscape. This is a simple example:
The heightmap is being read, and converted to an array containing the z-value of each gridpoint. These z-value's are based on the 'value'(Like in Hue, Saturation, Value) of the gridpoint(pixel).
In pseudocode it works like this:
for each pixel
{
Height[ x, y] = Value;
Height[ x, y] /= 255; // 255 == white, the maximum
Height[ x, y] *= max height; // The maximal height you want
}The maximum and minimum are now 0 and 'max height'.
A black pixel represents 0: 0/255*max height = 0.
A white pixel represents 'max height': 255/255*max height = max height.
Making a heightmapThe more creative part. You can make a heightmap by making a black sprite and enthusiasticly start drawing grey pixels on it, but that makes it really hard to get a nice result.
Instead, use a Terrain Editor, such as
EarthSculptor(free version is good enough). You can export a heightmap by clicking Export>Something-with-Heightmap.
Reading a heightmapSo how are we going to read the heightmap?
First of all, we have to define two variables:
gridsize and
gridparts.
gridsize will be the size of a cel in our heightmap. The height/width of a triangle. Just like the gridsize in GM's room editor.
gridparts is the number of cels, horizontal and vertical.
Remember: your heightmap image should always be 1 pixel bigger then the number of cels you want, since a pixel represents the height of a corner of a triangle, not a cel itself.So if you want a 50x50 terrain, use a heightmap of 51x51 pixels.
Here is an image for those of you who didn't understand:
The total size of the terrain is equal to
gridsize*gridparts, but the size of the heightmap is
gridparts+1.
In my script, the value of
gridparts is based on the size of the sprite(instead of manually defining it), and
gridsize is based on
room_width and
gridparts.
So, this is the first part of our script:
// terrain_create(heightmap)
//
// heightmap: the sprite to be used as heightmap
globalvar gridparts, gridsize;
gridparts = sprite_get_width(argument0)-1;
gridsize = room_width/gridparts;
Now we should read the height value's. First we draw the sprite using
draw_sprite(which means you'll have to execute this script
before initialising D3D(by calling
d3d_start()). Then we'll read the value's and calculate the height.
draw_sprite(argument0,-1,0,0);
var i, j;
globalvar height;
for ( i=0; i<=gridparts; i+=1) {
for ( j=0; j<=gridparts; j+=1) {
height[ i, j] = color_get_value( draw_getpixel( i, j) )/255 * argument1;
}
}
As you can see, I added
argument1. This is the maximum height('white'). Our script is now:
// terrain_create(heightmap,maxheight)
//
// heightmap: the sprite to be used as heightmap
// maxheight: the maximal height of the terrain
globalvar gridparts, gridsize;
gridparts = sprite_get_width(argument0)-1;
gridsize = room_width/gridparts;
draw_sprite(argument0,-1,0,0);
var i, j;
globalvar height;
for ( i=0; i<=gridparts; i+=1) {
for ( j=0; j<=gridparts; j+=1) {
height[ i, j] = color_get_value( draw_getpixel( i, j) )/255 * argument1;
}
}
Now we have the height value's. But we still don't have a model to draw. So now we are going to construct a model using trianglelists:
globalvar terrain;
terrain = d3d_model_create();
for ( j=0; j<gridparts; j+=1)
{
d3d_model_primitive_begin(terrain,pr_trianglestrip)
for ( i=0; i<=gridparts; i+=1)
{
terrain_get_normal(i*gridsize,j*gridsize+gridsize);
d3d_model_vertex_normal_texture(terrain,i*gridsize,j*gridsize+gridsize,height[i,j+1],global.xx,global.yy,global.zz,i,j+1);
terrain_get_normal(i*gridsize,j*gridsize);
d3d_model_vertex_normal_texture(terrain,i*gridsize,j*gridsize,height[i,j],global.xx,global.yy,global.zz,i,j);
}
d3d_model_primitive_end(terrain);
}
I used another script,
terrain_get_normal. This scripts calculates the normals, so your models get shaded correctly when Lighting is enabled. Here it is:
// terrain_get_normal(x,y)
// the result is saved in global.xx, global.yy and global.zz
globalvar gridsize;
var d;
global.xx = terrain_get_z(argument0-gridsize,argument1)-terrain_get_z(argument0+gridsize,argument1);
global.yy = terrain_get_z(argument0,argument1-gridsize)-terrain_get_z(argument0,argument1+gridsize);
global.zz = gridsize*2;
d = sqrt(sqr(global.xx)+sqr(global.yy)+sqr(global.zz));
global.xx /= d;
global.yy /= d;
global.zz /= d;
This script also uses another scipt,
terrain_get_z. This will be explained later in this tutorial.
The final script:
// terrain_create(heightmap,maxheight)
//
// heightmap: the sprite to be used as heightmap
// maxheight: the maximal height of the terrain
globalvar gridparts, gridsize;
gridparts = sprite_get_width(argument0)-1;
gridsize = room_width/gridparts;
draw_sprite(argument0,-1,0,0);
var i, j;
globalvar height;
for ( i=0; i<=gridparts; i+=1) {
for ( j=0; j<=gridparts; j+=1) {
height[ i, j] = color_get_value( draw_getpixel( i, j) )/255 * argument1;
}
}
globalvar terrain;
terrain = d3d_model_create();
for ( j=0; j<gridparts; j+=1)
{
d3d_model_primitive_begin(terrain,pr_trianglestrip)
for ( i=0; i<=gridparts; i+=1)
{
terrain_get_normal(i*gridsize,j*gridsize+gridsize);
d3d_model_vertex_normal_texture(terrain,i*gridsize,j*gridsize+gridsize,height[i,j+1],global.xx,global.yy,global.zz,i,j+1);
terrain_get_normal(i*gridsize,j*gridsize);
d3d_model_vertex_normal_texture(terrain,i*gridsize,j*gridsize,height[i,j],global.xx,global.yy,global.zz,i,j);
}
d3d_model_primitive_end(terrain);
}
Drawing the terrainAll what's left is drawing the terrain:
// terrain_draw(tex)
//
// tex: the texture to be applied to the model
//
// Note: use background_get_texture(tex) on the texture first.
texture_set_repeat(true);
d3d_model_draw(terrain,0,0,0,argument0);
(this is probably the most complicated script in the entire tutorial)
Finding the z-value of a position on the terrainBut we're not finished yet. It would be nice if you were able to make your character(or whatever) stand 'on' the heightmap/
We will need another script for that. Maybe you remember we made a global array
height. I did not var(var height;) it on purpose, because we'll be using it now.
First we determine on which cel we are:
var gridx, gridy;
gridx=floor(argument0/gridsize)
gridy=floor(argument1/gridsize)Then we look
where on the cel the position is:
var offsetx, offsety;
offsetx= argument0-gridsize*gridx
offsety= argument1-gridsize*gridyThen we request the z-positions of the corners of the cel:
var z1, z2, z3, z4;
z1=height[gridx,gridy]
z2=height[gridx+1,gridy]
z3=height[gridx+1,gridy+1]
z4=height[gridx,gridy+1]And using these variables we calculate the z:
var zz;
if offsetx>offsety
zz=z1 - offsetx*(z1-z2)/gridsize - offsety*(z2-z3)/gridsize
else
zz=z1 - offsetx*(z4-z3)/gridsize - offsety*(z1-z4)/gridsizeThe final script:
// terrain_get_z(x,y)
//
// x: the x-position
// y: err..
globalvar gridparts, gridsize, height;
var gridx, gridy, offsetx, offsety, z1, z2, z3, z4, zz;
gridx = max(0,min(gridparts-1,floor(argument0/gridsize))); //min/max stuff so you won't get errors if the position is outside the terrain
gridy = max(0,min(gridparts-1,floor(argument1/gridsize)));
offsetx = argument0-gridsize*gridx;
offsety = argument1-gridsize*gridy;
z1=height[gridx,gridy];
z2=height[gridx+1,gridy];
z3=height[gridx+1,gridy+1];
z4=height[gridx,gridy+1];
if offsetx>offsety
zz=z1 - offsetx*(z1-z2)/gridsize - offsety*(z2-z3)/gridsize;
else
zz=z1 - offsetx*(z4-z3)/gridsize - offsety*(z1-z4)/gridsize;
return zz;
Drawing a texture over the entire terrain, instead of repeating it every cel.To do this, replace the lines with the
d3d_model_vertex_normal_texture stuff by:
for ( j=0; j<gridparts; j+=1)
{
d3d_model_primitive_begin(terrain,pr_trianglestrip)
for ( i=0; i<=gridparts; i+=1)
{
terrain_get_normal(i*gridsize,j*gridsize+gridsize);
d3d_model_vertex_normal_texture(terrain,i*gridsize,j*gridsize+gridsize,height[i,j+1],global.xx,global.yy,global.zz,i/gridparts,(j+1)/gridparts);
terrain_get_normal(i*gridsize,j*gridsize);
d3d_model_vertex_normal_texture(terrain,i*gridsize,j*gridsize,height[i,j],global.xx,global.yy,global.zz,i/gridparts,j/gridparts);
}
d3d_model_primitive_end(terrain);
}
}
ExamplesHere are two examples how to use this ingame:
Example 1DownloadThis example shows the basics.
Example 2DownloadThis example shows how to make the player walk on the terrain, and how to deal with gravity.
FAQA short FAQ.
(got a question? Feel free to ask)
Q: My game crashes when loading the heightmap.
A: Your heightmap is probably to big. The bigger the better of course, but loading time increases exponentially. Use a size of maximal 51*51, 31*31 preferred.
Q: My game lags when drawing the terrain.
A: Your texture is probably to big. Use maximal 1024*1024.
Q: My terrain isn't drawn.
A: Well, the scripts work. Download one of the example's, and look for differences between your and my code.
Final NotesHeightmap-based terrain can be darn nice, but also darn slow.
Enjoy, but use with care.
Sincerly,
BlueMoon