• Hey Guest! Ever feel like entering a Game Jam, but the time limit is always too much pressure? We get it... You lead a hectic life and dedicating 3 whole days to make a game just doesn't work for you! So, why not enter the GMC SLOW JAM? Take your time! Kick back and make your game over 4 months! Interested? Then just click here!

3D calculating normals for terrain [SOLVED]

Kentae

Member
Hello good people of the GMC!

I have recently added som 3D terrain to my project. The terrain is generated from a heightmap and it works perfectly. I have also added a script for finding the z value of any x/y-position that also works perfectly.

However, I also have a lighting and specular shader that I have added to the terrain and these does of course not work correctly as all the normals in the terrain are set to point directly upwards. This means that the light is just completely even all over the terrain and the specular looks like it is hitting a completley flat plane.

So I need to know how I should go about calculating the normals so that they point in the direction of the surfaces, wich in turn will make the lighting and specular effects work correctly.

Here is the code I use to create the terrain:
Code:
///terrain_create(heightmap,squaresize,height)

// argument0 = the sprite to use as heigtmap
// argument1 = the size of each square
// argument2 = the maximum height of the terrain

// get input
global.gridWidth = sprite_get_width( argument0 );
global.gridHeight = sprite_get_height( argument0 );
global.segmentSize = argument1;

// create a grid based on the heightmaps size and input the height values
global.terrainGrid = ds_grid_create( global.gridWidth, global.gridHeight );
draw_sprite( argument0, 0, 0, 0 );
for ( w = 0; w < global.gridWidth; w++ )
    {
    for ( h = 0; h < global.gridHeight; h++ )
        {
        col = draw_getpixel( w, h );
        val = colour_get_value( col );
        ds_grid_set( global.terrainGrid, w, h, (val / 255) * argument2 );
        }
    }

// create the terrain model using the information from the newly made grid
global.terrainModel = d3d_model_create();
for ( xx = 0; xx < global.gridWidth; xx++; )
    {
    d3d_model_primitive_begin( global.terrainModel, pr_trianglestrip );
    //drawSwitch = true;
    for ( yy = 0; yy < global.gridHeight; yy++ )
        {
        //drawSwitch = !drawSwitch;
        z1 = ds_grid_get( global.terrainGrid, xx, yy );
        z2 = ds_grid_get( global.terrainGrid, xx + 1, yy );
        xTex = xx * ( 1 / global.gridWidth );
        xTex2 = (xx + 1) * ( 1 / global.gridWidth );
        yTex = yy * ( 1 / global.gridHeight );
        d3d_model_vertex_normal_texture( global.terrainModel, xx * global.segmentSize, yy * global.segmentSize, z1, 0, 0, 1, xTex, yTex );
        d3d_model_vertex_normal_texture( global.terrainModel, (xx + 1)  * global.segmentSize, yy * global.segmentSize, z2, 0, 0, 1, xTex2, yTex );
        }
    d3d_model_primitive_end( global.terrainModel );
    }
depth = -1000;
Any help will be greatly appreciated :)
 

FrostyCat

Redemption Seeker
Take the cross product of two adjacent edges on each grid square, that will give you the normal.
 

chamaeleon

Member
The normal of a plane is the cross product of two vectors on it. These vectors are presumably most easily computed using the three points making up triangles as you iterate over your grid. Points A, B, and C in a triangle could form vectors AB and AC (computed as B minus A and C minus A). Then the cross product of AB and AC is your normal (or possibly pointing in the opposite of the desired direction depending on the order you use the points).
 

Kentae

Member
Hmm, okay, I believe I get some of that. I'm just a little unclear on how to get the cross product. Haven't used cross products much as you probably have guessed now.
When I've been searching for a solution for this on google I find a lot places where they say I need to use cross products but noone seems to explain in a simple way how to get it >.<

Thanks for the help so far though :)
 

chamaeleon

Member
Given (x1, y1, z1) and (x2, y2, z2) vectors (directions from one point to the two other points in a triangle)
Code:
xnormal = y1*z2 - z1*y2
ynormal = z1*x2 - x1*z2
znormal = x1*y2 - y1*x2
 

Kentae

Member
Okay, that doesn't seem to bad. Still not entirely sure how I would implement it though :p
I think I know how I would do it if i drew one triangle at a time in my terrain script.
But since I use pr_trianglestrip and sort of draw two points at a time, I'm thinking there might be some problems. I might be wrong of course xD
I'll be trying to wrap my head around it though.
 

Binsk

Member
In your code you have a grid of z values. You already know the x/y values so all you need to figure out is the vectors.

The easiest way is just to calculate vector one by using the right adjacent segment and vector two with the bottom adjacent segment.

For example, you have heightmap position x5, y7. Vector 1 would be:
X: x6 - x5
Y: y7 - y7
Z: z1 - z0
(Assuming z1 is the adjacent z and z0 is the z of x5,y7)

Vector 2 would be:
X: x6 - x6
Y: y7 - y8
Z: z0 - z1

Normalize these vectors, take their cross product and the resulting values for each axis define your normal vector. If you don't know how to normalize a vector, use Google. It's easy to find.

Cross product of v1 x v2 will give you the up vector and v2 x v1 the down vector. Obviously you can use different adjacent values but i was trying to stick closer to how you form the model in your code.
 
Last edited:

chamaeleon

Member
Given a triangle strip instead of triangles, I guess it means you'll have to decide whether a given vertex should have the normal of one or the other of the two triangles (not counting the first and last vertices which only belong to one), or if you want to average the two surface normals to get a possibly smoother display. Since your code is solely responsible for computing all vertex positions, you should be able to incorporate whatever strategy makes sense.
 

Kentae

Member
Ahh! With the information you guys gave me I was able to figure it out and write a script that calculates the normals for me ^^

Thanks a lot for all your help people, this will make my project look sooo much better :D

Here's the script I came up with by the way, if anyones interested :)
Code:
///terrain_get_normal(x,y)

// finds the normal of the vertex located at the specified grid position

// the size of the terrain segments
ss = global.segmentSize

// the vertex to find the normal of
xa = argument0 * ss;
ya = argument1 * ss;
za = ds_grid_get( global.terrainGrid, argument0, argument1 );

// the vertex to the right of the vertex to find the normal of
xb = (argument0 + 1) * ss;
yb = (argument1) * ss;
zb = ds_grid_get( global.terrainGrid, argument0 + 1, argument1 );

// the vertex below the vertex to find the normal of
xc = (argument0) * ss;
yc = (argument1 + 1) * ss;
zc = ds_grid_get( global.terrainGrid, argument0, argument1 + 1 );

// calculate the AB- and AC-vectors
v1x = xb - xa;
v1y = yb - ya;
v1z = zb - za;

v2x = xc - xa;
v2y = yc - ya;
v2z = zc - za;

// calculate the vertex-normal using the cross product of the two vectors
global.nx = v1y * v2z - v1z * v2y;
global.ny = v1z * v2x - v1x * v2z;
global.nz = v1x * v2y - v1y * v2x;

// normalize it
d = sqrt( sqr( global.nx ) + sqr( global.ny ) + sqr( global.nz ) );

global.nx /= d;
global.ny /= d;
global.nz /= d;
I had to adjust my terrain creation code slightly to make it work porperly though.
Here's the updated terrain creation code:
Code:
///terrain_create(heightmap,squaresize,height)

// argument0 = the sprite to use as heigtmap
// argument1 = the size of each square
// argument2 = the maximum height of the terrain

// get input
global.gridWidth = sprite_get_width( argument0 );
global.gridHeight = sprite_get_height( argument0 );
global.segmentSize = argument1;

// create a grid based on the heightmaps size and input the height values
global.terrainGrid = ds_grid_create( global.gridWidth, global.gridHeight );
draw_sprite( argument0, 0, 0, 0 );
for ( w = 0; w < global.gridWidth; w++ )
    {
    for ( h = 0; h < global.gridHeight; h++ )
        {
        col = draw_getpixel( w, h );
        val = colour_get_value( col );
        ds_grid_set( global.terrainGrid, w, h, (val / 255) * argument2 );
        }
    }

// create the terrain model using the information from the newly made grid
global.terrainModel = d3d_model_create();
for ( xx = 0; xx < global.gridWidth-2; xx++; )
    {
    d3d_model_primitive_begin( global.terrainModel, pr_trianglestrip );
    for ( yy = 0; yy < global.gridHeight-1; yy++ )
        {
        z1 = ds_grid_get( global.terrainGrid, xx, yy );
        z2 = ds_grid_get( global.terrainGrid, xx + 1, yy );
        xTex = xx * ( 1 / global.gridWidth );
        xTex2 = (xx + 1) * ( 1 / global.gridWidth );
        yTex = yy * ( 1 / global.gridHeight );
        terrain_get_normal( xx, yy );
        d3d_model_vertex_normal_texture( global.terrainModel, xx * global.segmentSize, yy * global.segmentSize, z1, global.nx, global.ny, global.nz, xTex, yTex );
        terrain_get_normal( xx + 1, yy );
        d3d_model_vertex_normal_texture( global.terrainModel, (xx + 1)  * global.segmentSize, yy * global.segmentSize, z2, global.nx, global.ny, global.nz, xTex2, yTex );
        }
    d3d_model_primitive_end( global.terrainModel );
    }
depth = -1000;
The obvious change is of course the implementation of the terrain_get_normal script, but I also had to reduce the xx loop by 2 and the yy loop by 1 otherwise the script tried to get normals from nonexisting gridpoints.

And again, thanks all ^^
 
Top