3D [Solved] Trying to use triangle normals to make an object follow the terrain.

Emilie_Blue

Member
Hello everyone !

I recently had the idea to use normals to make , for example a car follow the 3D ground and rotating accordingly.
Since I'm bad at maths , I decided to learn from scratch how I could implement it in GML. Fortunately , I found some
ressources online to help me :D

I managed to make something that works by finding the average normal of the 2 normals found in these 2 triangles :

screenshot102.png

So here's the issue I have :
Depending of the slope I'm on , there is a slight offset from what the object is supposed to have :

screenshot101.png

But on the other slopes , it seems to work well :

screenshot100.png

These are the differents scripts used related to this problem :

GML:
///math_triangle_normal_x(x1,y1,z1,x2,y2,z2,x3,y3,y3,axis,invert)
//Returns the specified axis normal of the specified triangle

//var axv,ayv,azv,axn,ayn,azn,bxv,byv,bzv,cxv,cyv,czv,cxn,cyn,czn,dista,distb,distc,nx,ny,nz;


axv = argument3-argument0
ayv = argument4-argument1
azv = argument5-argument2
dista = point_distance_3d(argument0,argument1,argument2,argument3,argument4,argument5)
axn = axv/dista         //Normals of a
ayn = ayv/dista
azn = azv/dista


bxv = argument6-argument3
byv = argument7-argument4
bzv = argument8-argument5
distb = point_distance_3d(argument3,argument4,argument5,argument6,argument7,argument8)
bxn = bxv/distb         //Normals of b
byn = byv/distb
bzn = bzv/distb

cxv = argument0-argument6
cyv = argument1-argument7
czv = argument2-argument8
distc = point_distance_3d(argument6,argument7,argument8,argument0,argument1,argument2)
cxn = cxv/distc         //Normals of c
cyn = cyv/distc
czn = czv/distc
 
if argument10 = 1
    {
    if argument9 = 0 {return (ayn*bzn)-(azn*byn)}
    if argument9 = 1 {return (azn*bxn)-(axn*bzn)}
    if argument9 = 2 {return (axn*byn)-(ayn*bxn)}
    }
if argument10 = -1
    {
    if argument9 = 0 {return (ayn*czn)-(azn*cyn)}
    if argument9 = 1 {return (azn*cxn)-(axn*czn)}
    if argument9 = 2 {return (axn*cyn)-(ayn*cxn)}
    }
GML:
///math_get_normal(x,y,axis,triangle size)
/*
return surface normal along the specified axis(x , y or z)
argument0 = x
argument1 = y
argument2 = 0,1 or 2 for x,y or z
argument3 = size of surface on
*/

var x1,y1,z1,x2,y2,z2,x3,y3,z3,x4,y4,z4;

x1 = argument0-argument3
y1 = argument1-argument3
z1 = terrain_get_z(argument0-argument3,argument1-argument3)

x2 = argument0+argument3
y2 = argument1-argument3
z2 = terrain_get_z(argument0+argument3,argument1-argument3)

x3 = argument0-argument3
y3 = argument1+argument3
z3 = terrain_get_z(argument0-argument3,argument1+argument3)

x4 = argument0+argument3
y4 = argument1+argument3
z4 = terrain_get_z(argument0+argument3,argument1+argument3)

var normalX,normalY,normalZ;
normalX = math_triangle_normal(x1,y1,z1,x2,y2,z2,x3,y3,z3,0,1)
normalY = math_triangle_normal(x1,y1,z1,x2,y2,z2,x3,y3,z3,1,1)
normalZ = math_triangle_normal(x1,y1,z1,x2,y2,z2,x3,y3,z3,2,1)

var normalX2,normalY2,normalZ2;
normalX2 = math_triangle_normal(x3,y3,z3,x2,y2,z4,x4,y4,z4,0,1)
normalY2 = math_triangle_normal(x3,y3,z3,x2,y2,z4,x4,y4,z4,1,1)
normalZ2 = math_triangle_normal(x3,y3,z3,x2,y2,z4,x4,y4,z4,2,1)

//Sum of the two vectors
var normalX3,normalY3,normalZ3;
normalX3 = normalX+normalX2
normalY3 = normalY+normalY2
normalZ3 = normalZ+normalZ2

//Normalize it
var normal3Magnitude;
normal3Magnitude = sqrt(normalX3*normalX3+normalY3*normalY3+normalZ3*normalZ3)
normalX3 /= normal3Magnitude
normalY3 /= normal3Magnitude
normalZ3 /= normal3Magnitude

if argument2 = 0 {return normalX3}
if argument2 = 1 {return normalY3}
if argument2 = 2 {return normalZ3}
And this is used in the draw event of the object :

GML:
var rotx,roty,rotz;

rotx=radtodeg(arccos(math_get_normal(x,y,0,.1)))
roty=radtodeg(arcsin(math_get_normal(x,y,1,.1)))
rotz=radtodeg(arcsin(math_get_normal(x,y,2,.1)))

d3d_transform_add_scaling(.3,.3,.3);
d3d_transform_add_rotation_y(-90)
d3d_transform_add_rotation_x(direction)
d3d_transform_add_rotation_axis(0,rotz,-roty,rotx)

d3d_transform_add_translation(x,y,z)
d3d_draw_block(-2,-2,4,2,2,0,-1,1,1)
d3d_set_lighting(false)
d3d_model_draw(mod_lines,0,0,.01,-1)
d3d_set_lighting(true)
d3d_transform_set_identity()


I don't know if this is the better way to achieve it so I'm open for your suggestions.
Is the issue related to maths ? Or is it related to something else ?
I'm out of ideas...
 

Attachments

Last edited:

Joe Ellis

Member
You need this little beauty here:
GML:
z_pos = z + (dot_product_3d(tri.nx, tri.ny, tri.nz, tri.x1 - x, tri.y1 - y, _tri.z1 - z) / _tri.nz)
It ends up getting the exact place the instance will land on the triangle based on the it's x & y, so directly below it.
It's cus the dot product of the triangle's normal and the vector from one of the vertices of the triangle to the instance's position gives the exact distance that the instance is away from the nearest point on the triangle's plane. But if the triangle is sloped, the point directly below it will be further away than the nearest point, so then you need to increase the value by a certain amount based on how sloped the triangle is, which you can do by simply dividing the value by the triangle's z normal (which is basically how sloped the triangle is, if it's 1\-1 it's completely flat, if it's 0 it's completely sideways, in which case it's impossible to land on it from above or below, but doing the simple point_in_triangle test first will make sure this doesn't happen)
Cus it's always less than 1, negative or positive, it increases the value by exactly the right amount so that it gives the z value directly below(or above) the x & y.
It's pretty amazing how simple the solution is.
If you want a slightly more detailed explanation I wrote the solution here aswell:
https://forum.yoyogames.com/index.p...rrain-z-value-calculations.73295/#post-432720
 
Last edited:

Emilie_Blue

Member
So thats why ! I think I understand but I'm not sure how I could put in in the code... I would need to know the normals in advance ?
Is this where I need to do it :

GML:
///math_get_normal(x,y,axis,triangle size)
/*
return surface normal along the specified axis(x , y or z)
argument0 = x
argument1 = y
argument2 = 0,1 or 2 for x,y or z
argument3 = size of surface on
*/

var x1,y1,z1,x2,y2,z2,x3,y3,z3,x4,y4,z4;

x1 = argument0-argument3
y1 = argument1-argument3
z1 = terrain_get_z(argument0-argument3,argument1-argument3)

x2 = argument0+argument3
y2 = argument1-argument3
z2 = terrain_get_z(argument0+argument3,argument1-argument3)

x3 = argument0-argument3
y3 = argument1+argument3
z3 = terrain_get_z(argument0-argument3,argument1+argument3)

x4 = argument0+argument3
y4 = argument1+argument3
z4 = terrain_get_z(argument0+argument3,argument1+argument3)

var normalX,normalY,normalZ;
normalX = math_triangle_normal(x1,y1,z1,x2,y2,z2,x3,y3,z3,0,1)
normalY = math_triangle_normal(x1,y1,z1,x2,y2,z2,x3,y3,z3,1,1)
normalZ = math_triangle_normal(x1,y1,z1,x2,y2,z2,x3,y3,z3,2,1)

var normalX2,normalY2,normalZ2;
normalX2 = math_triangle_normal(x3,y3,z3,x2,y2,z4,x4,y4,z4,0,1)
normalY2 = math_triangle_normal(x3,y3,z3,x2,y2,z4,x4,y4,z4,1,1)
normalZ2 = math_triangle_normal(x3,y3,z3,x2,y2,z4,x4,y4,z4,2,1)

//Sum of the two vectors
var normalX3,normalY3,normalZ3;
normalX3 = normalX+normalX2
normalY3 = normalY+normalY2
normalZ3 = normalZ+normalZ2

//Normalize it
var normal3Magnitude;
normal3Magnitude = sqrt(normalX3*normalX3+normalY3*normalY3+normalZ3*normalZ3)
normalX3 /= normal3Magnitude
normalY3 /= normal3Magnitude
normalZ3 /= normal3Magnitude

if argument2 = 0 {return normalX3}
if argument2 = 1 {return normalY3}
if argument2 = 2 {return normalZ3}
 

Joe Ellis

Member
Hmm, I'm not sure, I don't know about that method you're using but it seems to work well for rotating the object based on the surface angle, but I've never done anything for this so I don't know.

I recommend precalculating the normals of each triangle cus it could end up really slow with a lot of instances. You would have to create the triangles as structs or arrays and put them into the terrain grid.

To use the method I was talking about, it should be fine to just use it somewhere in the instance's step event with the triangle the instance is over, and set the instance's z to this, or offset it based on the instance's bounding box.
Do you already have a method for getting which triangle the instance is over?

Btw here some code to get a triangle's normal if you need it:
GML:
///face_get_normal(f)
var f = argument0;

//Edge vectors (V1 - V2) & (V1 - V3)
var
vx1 = f[_face.x2] - f[_face.x1],
vy1 = f[_face.y2] - f[_face.y1],
vz1 = f[_face.z2] - f[_face.z1],
vx2 = f[_face.x3] - f[_face.x1],
vy2 = f[_face.y3] - f[_face.y1],
vz2 = f[_face.z3] - f[_face.z1];

//Cross product of the two vectors
var
cx = vy1 * vz2 - vz1 * vy2,
cy = vz1 * vx2 - vx1 * vz2,
cz = vx1 * vy2 - vy1 * vx2;

//Normalize the cross product and set the values to the face's normal
var l = 1 / point_distance_3d(0, 0, 0, cx, cy, cz);
f[@ _face.nx] = cx * l
f[@ _face.ny] = cy * l
f[@ _face.nz] = cz * l
 

Emilie_Blue

Member
Thank you for this 😄

I managed to find what caused my issue : it was maths , I needed to add a rotation in my object :
GML:
d3d_transform_add_rotation_x(direction+roty+(sign(rotx-rotz)/2))
And also have 4 normals forming a square to have a correct average normal

It took me way too long to find the solution but now it works 😌
You're right , it would be better to already have precalculed normals. I already have a ds_grid for the terrain , might as well add one for the normals 🤔
 

Yal

🐧 *penguin noises*
GMC Elder
I already have a ds_grid for the terrain , might as well add one for the normals
Or you could have a grid of structs, so you can have all the terrain data in a single grid and access them by name? (e.g. global.terrain_data[#xx,yy].normal_x)
 

Joe Ellis

Member
Yeah it's a lot easier to manage if each triangle\face is an individual struct or array, cus you only need one grid and once you've got it from the grid you can access all of it's data without having to keep reading from a grid for every variable.
The variables each face would typically hold are: x1, y1, z1, x2, y2, z2, x3, y3, z3, nx, ny, nz, the texture\material it uses or the mesh\vertex buffer it's part of, precalculated edge vectors if instances would otherwise have to calculate them every time they interact with a face, also any other vectors that are made only from the triangle's coordinates, just cus it saves having to calculate them over and over, and makes the instances' code cleaner\simpler.
 
Top