Legacy GM Targeting UI in 3D

hijong park

Member


I'm making a 3D game that has the targeting system, So I want to display a target UI on the target.

The targeting UI should look like the picture adove, qualifying these conditions :

1. It should be a 2D picture displayed on the 3D object.

2. It should never be covered by other 3D objects.

3. the size of the UI should always be same, ignoring the distance between the camera and target.

I have no idea how to make this UI. It should always be visible and keep the same size ignoring the distance of the target, so obviously I shouldn't use d3d_draw_wall (Like when making Doom-style Bill boarding sprites) but draw it in draw_GUI event instead. But If I draw the UI in draw_GUI event, There's no way to get the correct location of it to draw on the screen as the target is in different dimension.
 

kraifpatrik

(edited)
GameMaker Dev.
But If I draw the UI in draw_GUI event, There's no way to get the correct location of it to draw on the screen as the target is in different dimension.
Hey there, actually there is a way! When you draw a vertex in 3D space, it goes through series of matrix multiplications to calculate its position on screen, and this can be replicated!

The formula is simple:

1)
Multiply your matrix_world, matrix_view and matrix_projection matrices. The result of this multiplication is generally called an MVP matrix (model, view, projection).

Note: This has to be done in the Draw event where you draw your 3D objects, after you set up a 3D projection (using d3d_set_projection_ext / camera_apply / ...). If you use matrix_get in a Draw GUI event, then it won't return the required matrices!

Code:
var _world = matrix_get(matrix_world);
var _view = matrix_get(matrix_view);
var _projection = matrix_get(matrix_projection);
var _mvp = matrix_multiply(matrix_multiply(_world, _view), _projection);
2) Multiply the MVP matrix with a vector [x, y, z, w], where x,y,z is the position of the enemy in 3D space and w is 1. The result of this is a new vector, containing the enemy's position in projection space.
Code:
var _enemyX = ...;
var _enemyY = ...;
var _enemyZ = ...;
var _enemyW = 1;

var _x = _mvp[ 0]*_enemyX + _mvp[ 4]*_enemyY + _mvp[ 8]*_enemyZ + _mvp[12]*_enemyW;
var _y = _mvp[ 1]*_enemyX + _mvp[ 5]*_enemyY + _mvp[ 9]*_enemyZ + _mvp[13]*_enemyW;
var _z = _mvp[ 2]*_enemyX + _mvp[ 6]*_enemyY + _mvp[10]*_enemyZ + _mvp[14]*_enemyW;
var _w = _mvp[ 3]*_enemyX + _mvp[ 7]*_enemyY + _mvp[11]*_enemyZ + _mvp[15]*_enemyW;
3) Divide x,y,z of the resulting vector by w of the resulting vector - this is called perspective division. After this division, x,y,z will be in an NDC (normalized device coordinates) space. In this space, all values are in range [-1, 1].
Code:
_x /= _w;
_y /= _w;
_z /= _w;
4) We will now want to get the x and y into range [0, 1].
Code:
_x = _x * 0.5 + 0.5;
_y = _y * 0.5 + 0.5;
5) The coordinates are also flipped vertically. Fix that with:
Code:
_y = 1 - _y;
6) We can now scale the x and y by our screen size to get the enemy's position in screen space.
Code:
_x *= window_get_width();
_y *= window_get_height();
And now, if z is greater than 0, then x and y are the enemy's position on screen (otherwise it's somewhere outside of the camera's view)! It seems like a lot of code, but it can be squished into two scripts really (transform, project). I don't remember any good learning resources on this topic, but you can try googling "world view projection matrix", "perspective projection", "clip space", "NDC space" and "linear algebra" in general. Good luck!
 
Last edited:

hijong park

Member
Hey there, actually there is a way! When you draw a vertex in 3D space, it goes through series of matrix multiplications to calculate its position on screen, and this can be replicated!

The formula is simple:

Multiply your matrix_world, matrix_view and matrix_projection matrices (= MVP matrix):
Code:
var _world = matrix_get(matrix_world);
var _view = matrix_get(matrix_view);
var _projection = matrix_get(matrix_projection);
var _mvp = matrix_multiply(matrix_multiply(_world, _view), _projection);
Multiply MVP with vector [x, y, z, w], where x,y,z is position of the enemy in 3D space and w is 1:
Code:
var _enemyX = ...;
var _enemyY = ...;
var _enemyZ = ...;
var _enemyW = 1;

var _x = _mvp[ 1]*_enemyX + _mvp[ 5]*_enemyY + _mvp[ 9]*_enemyZ + _mvp[13]*_enemyW;
var _y = _mvp[ 2]*_enemyX + _mvp[ 6]*_enemyY + _mvp[10]*_enemyZ + _mvp[14]*_enemyW;
var _z = _mvp[ 3]*_enemyX + _mvp[ 7]*_enemyY + _mvp[11]*_enemyZ + _mvp[15]*_enemyW;
var _w = _mvp[ 0]*_enemyX + _mvp[ 4]*_enemyY + _mvp[ 8]*_enemyZ + _mvp[12]*_enemyW;
Divide x, y (and z) of the resulting vector by w of the resulting vector - this is called perspective division:
Code:
_x /= _w;
_y /= _w;
// _z =/ _w; // Not necessary for our purposes
X and Y are now values in range [-1,1], you want to get them into [0, 1]:
Code:
_x = _x * 0.5 + 0.5;
_y = _y * 0.5 + 0.5;
Also the coordinates are flipped vertically, fix that with:
Code:
_y = 1 - _y;
Now you can scale the x coordinate from [0,1] range to [0,screenWidth] and y from [0,1] range to [0, screenHeight]:
Code:
_x *= window_get_width();
_y *= window_get_height();
And now x and y are the enemy's position on screen! Seems like a lot of code, but can be squished into two scripts really (transform, project). I don't remember any good learning resources on this topic, but you can try googling "world view projection matrix", "perspective projection", "clip space", "NDC space" and "linear algebra" in general. Good luck!

EDIT: Wrote this without any tests, if there are some errors, please let me know!
Where am I suppossed to put these code ?

I tried it in both draw and draw_GUI event but none of them worked.

When I tried to draw the value of _x and _y, They displayed insane amount of value (202003,452168) when the target's location is (0,0,0).
 

kraifpatrik

(edited)
GameMaker Dev.
You can calculate the MVP matrix in the draw event, right after you use d3d_set_projection_ext, and store it into a variable. Then use that variable in the draw GUI event in the rest of the code. There's also a chance that I've put the matrix multiplications in an opposite order and it should be matrix_multiply(matrix_multiply(_projection, _view), _world), but I'm not sure about that, so please try both variants. Nope, it's correct, I have tested it.
 
Last edited:
Top