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.
There already are a bunch of heightmap scripts around, so what's the advantage of these scripts?
Well, first of all, this script uses trianglelists instead of trianglestrips, which increases speed.
Second, this script defines the normals correctly, which allows you to use lighting.
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 cell.
- 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: http://easy-upload.nl/f/1o4n9NPt (I can't use the IMG tag: 'You are not allowed to use that image extension on this board.' Wut? It's PNG :[ )
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 heightmap
The 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 heightmap
So 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:
http://easy-upload.nl/f/2eHk63Rk
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 calclulate 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: de sprite van de heightmap die gebruikt moet worden
// maxheight: de maximale hoogte van het 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: de sprite van de heightmap die gebruikt moet worden
// maxheight: de maximale hoogte van het 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 terrain
All 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 model 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 terrain
But 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*gridy
Then 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)/gridsize
The 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 cell.
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);
}
}Examples
Here are two examples how to use this ingame:
Example 1
Download
This example shows the basics.
Example 2
Download
This example shows how to make the player walk on the terrain, and how to deal with gravity.
FAQ
A 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 Notes
Heightmap-based terrain can be darn nice, but also darn slow.
Enjoy, but use with care.
Sincerly,
BlueMoon











