• 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!

GameMaker 3D projection math

Hi all,

First off, I'm not coming into this from ground zero. I'm relatively savvy with the concepts behind 3D space, linear algebra, types of projection, and matrices. So, if your first compulsion is to link me with a common go-to online resource, odds are I've already spent much of my time looking at it. Don't let that stop you from re-linking, mind you, I just want to be clear that posting a thread isn't my first port of call here.

The Goal

A simple fly-through camera, as achievable with the built-in GM functions:
Code:
// Camera create
var cam = camera_create(),
    mat_proj = matrix_build_projection_perspective_fov(45,view_wport[0]/view_hport[0],1,32000);
camera_set_proj_mat(cam,mat_proj);
view_set_camera(0,cam);
camera_set_update_script(cam,scr_cam_update());

// scr_cam_update()
var matr_view = matrix_build_lookat(x,y,z,x+dcos(dir), y-dsin(dir),z-dsin(pitch),0,0,1);
The Problem
I'm trying to learn how to implement my own perspective 3D camera. Putting aside the fact that my project uses vbuffers and solely deals with the Draw GUI event, I think this stuff is pretty interesting and would like to figure out how to manually do it.

So, I think I'm pretty close, but am getting some weird results and think my math might be slightly off.

The Attempt
This part works as expected. Any functions in camelCase are just my own way of managing memory, and can be treated as any other object var attribution.
Code:
STEP
// Rotation
var _yaw = view_yaw,
    _pitch = view_pitch,
    _rotational_spd = 0.00001,  // Lower is slower.
    _mouse_x = mouse_x,
    _mouse_y = mouse_y,
    ;
_yaw -= (_mouse_x-window_width/2)*_rotational_spd;
_pitch -= clamp(_pitch-(_mouse_y-window_height/2)*_rotational_spd, -120, 120);
viewSetYaw(_yaw);
viewSetPitch(_pitch);
             
// Translation
var _move_spd = 10,
    _xaxis = keyboard_check(ord("D"))-keyboard_check(ord("A")),
    _yaxis = keyboard_check(ord("S"))-keyboard_check(ord("W")),
    _zaxis = keyboard_check(ord("C"))-keyboard_check(vk_space),
    _view_x = view_x,
    _view_y = view_y,
    _view_z = view_z,
    ;
_view_x += _xaxis*dsin(_yaw)*_move_spd-_yaxis*dcos(_yaw)*_move_spd;
_view_y += _xaxis*dcos(_yaw)*_move_spd-_yaxis*dsin(_yaw)*_move_spd;
_view_z += _zaxis*_move_spd;
viewSetX(_view_x);
viewSetY(_view_y);
viewSetZ(_view_z);
Same as before. This stuff works without the fly-through camera, i.e. with orthographic projection.
Code:
DRAW GUI
shader_set(gpu_dynamic_lighting);

// View 3D translation and rotation uniforms.
// Translation
shader_set_uniform_f(shader_get_uniform(gpu_dynamic_lighting, "u_fViewX"), view_x);
shader_set_uniform_f(shader_get_uniform(gpu_dynamic_lighting, "u_fViewY"), view_y);
shader_set_uniform_f(shader_get_uniform(gpu_dynamic_lighting, "u_fViewZ"), view_z);

// Scale
shader_set_uniform_f(shader_get_uniform(gpu_dynamic_lighting, "u_fViewScale"), view_scale);

// Rotation
shader_set_uniform_f(shader_get_uniform(gpu_dynamic_lighting, "u_fViewYaw"), view_yaw);
shader_set_uniform_f(shader_get_uniform(gpu_dynamic_lighting, "u_fViewPitch"), view_pitch);
shader_set_uniform_f(shader_get_uniform(gpu_dynamic_lighting, "u_fViewRoll"), view_roll);

// For each render target in the game,
for (var j=0; j<RENDER.height; j++) {
    var _render = j,
        _tex = renderGetVertexTexture(_render),
        _num_buffers = renderGetNumVertexBuffers(_render),
        ;
    // For each VB in the render target,
    for (var k=0; k<_num_buffers; k++) {
        var _vb = renderGetVertexBuffer(_render, k);
                         
        // Submit the buffer.
        vertex_submit_buffer(_vb, _tex);  // Submits with pr_trianglelist.
    };
};
 
shader_reset();
Code:
SHADER - VERTEX
// Only relevant code incl.
// DW - I've tested without irrelevant code and the problem persists.
attribute vec3 in_Position;        // (x,y,z)        [as seen in room editor]    sprite (x,y,depth) - corners not sprite origin!
uniform float u_fViewX;
uniform float u_fViewY;
uniform float u_fViewZ;
uniform float u_fViewScale;
uniform float u_fViewYaw;
uniform float u_fViewPitch;
uniform float u_fViewRoll;

void main()
{
    // Obtain 3D position of point to be projected, _a.
    vec3 _a = vec3( in_Position.x, in_Position.y, in_Position.z);
 
    // Obtain 3D position of view, _v.
    vec3 _v = vec3( u_fViewX, u_fViewY, u_fViewZ);
 
    // Obtain 3D rotation of view, _theta.
    vec3 _theta = vec3( u_fViewYaw, u_fViewPitch, u_fViewRoll);
 
    // Calculate sin and cos of theta, _s, _c.
    vec3 _s = sin(_theta);
    vec3 _c = cos(_theta);
 
    // Calculate display surface's position relative to view lens, _e.
    vec3 _e = _a-_v;
 
    // To apply 2D perspective, we first need to determine 3D transformation point, _d.
    // Directly from: https://en.wikipedia.org/wiki/3D_projection#Mathematical_formula
    vec3 _d = vec3(0.0, 0.0, 0.0);
    _d.x = _c.y * (_s.z*_e.y + _c.z*_e.x) - _s.y*_e.z;
    _d.y = _s.x * (_c.y*_e.z + _s.y * (_s.z*_e.y + _c.z*_e.x)) + _c.x * (_c.z*_e.y - _s.z*_e.x);
    _d.z = _c.x * (_c.y*_e.z + _s.y * (_s.z*_e.y + _c.z*_e.x)) - _s.x * (_c.z*_e.y - _s.z*_e.x);
 
    // Finally, determine 2D position of point to be projected, b.
    vec4 object_space_pos = vec4(0.0, 0.0, 0.0, 1.0/u_fViewScale);
    object_space_pos.x = (_e.z/_d.z)*_d.x + _v.x;
    object_space_pos.y = (_e.z/_d.z)*_d.y + _v.y;
 
    // Submit edits to screen.
    gl_Position = gm_Matrices[MATRIX_WORLD_VIEW_PROJECTION] * object_space_pos;
 
    // To fragment
    v_vPosition = vec3( in_Position.x, in_Position.y, in_Position.z);    // Send true 3D verts to fragment shader.
    v_vTexcoord = in_TextureCoord;
 
    // Haven't included fragment bc irrelevant.
}

x, y, z, yaw, pitch, roll == 0.


pitch == -0.2 (the rest are 0).


view_x == -0.03
view_y == -0.03
view_z == -339.88
view_yaw == -470.23
view_pitch == 0
view_roll == 0

I know these are relatively arbitrary, but I wanted to showcase what I'm looking at here.

If you're like me and have no idea, I'd be happy to share the vertex data writing code too, I just wanted to avoid dumping my entire pipeline here before anyone even replied lol. It is curious that the van doesn't move in the same direction as the background, though. The way the spotlight works is it dims anything not within a radius of the mouse position. This helps me to isolate a position in 3D space on the screen.

Thanks guys!
 
Last edited:
All right, maybe we can start by being more specific: I understand the maths on the Wikipedia page and how it's applying rotation and translation in order to determine the position of each vertex in world-space. I understand matrices and can see how they're affecting the vertex data. What I don't understand however, is how to do that with matrices in GM. I opted for the solution in the Wikipedia article that bypasses the matrix math in favour of longer equations, and I've managed to get results vaguely familiar to a 3D camera. However, it's got some pretty rough edges, and I think in order to get a better grasp of how this is all working, I need to work with matrices properly.

Anyone able to help me with this? I know of 'matrix_build()', but the arguments specify position, translation and rotation - what does this end up actually looking like? And how would I use it once passed into the shader with 'shader_set_uniform_shader()'? The maths in the Wikipedia article doesn't mix translation and rotation within the one matrix. Is this just the way GM has set it up? Super confused about this.

Finally - if someone could also help explain to me what the fourth column/row is doing in a GM matrix, I'd really appreciate that. Is it W, like with a vec4 in the shader? W is the scale component of xyz, so if that's the case, how do you have a W value in rotation? Or translation?
 
You're asking for a lot of information, so it's hard to know where to begin.

If you want to use the built in matrices that are sent to the gpu for rendering, they are the 'world', 'view' and 'projection' matrices.

The world matrix is for positioning, scaling, and orientation of individual "objects". The world matrix might actually be the product of several other matrices, that represent transformations done in a certain order.

The view matrix is for positioning and orientating the camera.

The projection matrix applies either a perspective or an orthographic projection to the camera. It is also used to calculate a depth value for each vertex for use with the depth buffer.

All of those matrices together determine the location of a vertex within the 2d view port. They are (automatically by gamemaker) combined into a single matrix by multiplying them in this order: projection * view * world. And then, if you want to transform a vertex with the resulting world_view_projection matrix, all you have to do is do a multiplication with the matrix on the left side, and a vec4 vector on the right side, and assign the value to gl_Position. The w component of that vector should have a value of 1.0. Example:

gl_Position = gm_Matrices[MATRIX_WORLD_VIEW_PROJECTION] * vec4( in_Position, 1.0 );

And if you'd like to know, the result of that calculation produces a new vec4 with these properties:

the "w" component, when using a perspective projeciton, is the depth of the vertex in view space... i.e., it's distance from the camera a long the camera's z (look) axis. When using an orthographic projection, the w component is always 1.

After divided by "w", the z component is the "depth" value of the vertex that will be written to the depth buffer (interpolated between vertices for each fragment). The range is 0 to 1 (and is not linear).

After divided by "w", the x and y components are the 2d position of the vertex within the view port. -1 x is the left side of the view port, 1 is the right. -1 y is the bottom of the view port, 1 is the top.
 
Last edited:
You're asking for a lot of information, so it's hard to know where to begin.
Yeah, I absolutely appreciate that. It's a large topic and unfortunately I haven't been able to get my head around all of it on my own. Happy to do baby steps!

The projection matrix applies either a perspective or an orthographic projection to the camera. It is also used to calculate a depth value for each vertex for use with the depth buffer.
Ah, I see. That explains why it seems 'W' scales my image inversely! I.e. In_Position.w = 1.0/view_scale results in a larger image proportionate with view_scale. But really, it's a measure of distance to the camera. That makes sense. It's what 'e' is doing in the Wikipedia projection maths. At the risk of sounding dumb, is this where I would be writing code to switch to a perspective projection? By default, games in GM are orthographic, and without using the built-in camera functions, would it make sense to apply a perspective projection outside of the shader to the projection matrix?

The projection matrix applies either a perspective or an orthographic projection to the camera. It is also used to calculate a depth value for each vertex for use with the depth buffer.
Yep, all of this I'm fine with :)

All of those matrices together determine the location of a vertex within the 2d view port. They are (automatically by gamemaker) combined into a single matrix by multiplying them in this order: projection * view * world. And then, if you want to transform a vertex with the resulting world_view_projection matrix, all you have to do is do a multiplication with the matrix on the left side, and a vec4 vector on the right side, and assign the value to gl_Position.
Nice. I didn't realize the position of the matrix relative to the vec4 was relevant, but other than that I think I'm pretty ok with all of this. It just comes down to how to put it in practice.

Essentially, all of these questions culminate in one primary goal: I'm trying to apply perspective projection to GameMaker, whether within the shader or outside, without using the camera functions. I want to know how the math in the Wikipedia article (which does work, it's just a bit rough), can function within the paradigms of GameMaker functions like matrix_build(). Thanks a tonne for replying! It surprises me that this isn't a more common topic of interest in this forum.
 
I can tell you a bit more about the view and projection matrices, and show you how they are constructed (at least in GMS1.4). I'm still not 100% sure whether you are just trying to learn how the matrices work, or if you are trying to reproduce the math in another way from some reason.

See in the spoiler.
I wrote this in a hurry, so verify by your own means anything that you want to be certain of.
Important note: By default in GMS1.4, orthographic projections are in a right-handed coordinate system, and perspective projections are in a left-handed coordinate system.

Everything below will reflect that same convention.

The handedness of the coordinate system determines the visual relationship between the orientation of the world's axes. And the coordinate system depends on the view and projection matrices as well as the display (although the display is usually constant). Switching from one coordinate system to the other will cause all geometry to appear mirrored, textures and text will appear flipped, angles will switch between clockwise and anti-clockwise, and vertex winding orders will be reversed (resulting in opposite face culling if culling is enabled).
----------------------------------------------------------------------------------
Also be aware that you may see discrepancies between what you see here and what you might find on google. In particular, the near and far clipping distances may be used differently. You may also find that the camera points in the opposite direciton in view space.

To the best of my knowledge, everything below accurately reflects the way view and projection matrices are constructed in gamemaker (GMS1.4 at least).
----------------------------------------------------------------------------------
4x4 matrices returned by built-in gamemaker functions are stored as 16 element 1D arrays.
The arrangement of array indices corresponds to the layout of matrix elements in the following way:
Code:
┌             ┐
│ 0  4  8  12 │
│ 1  5  9  13 │
│ 2  6  10 14 │
│ 3  7  11 15 │
└             ┘
----------------------------------------------------------------------------------
Orthographic projection matrix:
Code:
Legend
VW = view width
VH = view height
N = near clipping distance
F = far clipping distance
┌                           ┐┌ ┐ ┌                       ┐
│2/VW  0    0        0      ││x│ │ 2/VW*x                │
│0    -2/VH 0        0      ││y│=│-2/VH*y                │
│0     0    1/(F-N) -N/(F-N)││z│ │ 1/(F-N)*z + -N/(F-N)*w│
│0     0    0        1      ││w│ │ 1*w                   │
└                           ┘└ ┘ └                       ┘
Note: the above column vector (x,y,z,w) is presumed to be in view space.
The "w" component of the product is 1*w. And since the w component of the input vector should be 1, then 1*w is 1.
Since the w component of the product is 1, then the z,y,z, components will not change when divided by w.

----------------------------------------------------------------------------------
Perspective projection matrix:
Code:
Legend
T = tangent of (field_of_view/2)
A = aspect ratio of perspective projection
N = near clipping distance
F = far clipping distance
┌                               ┐┌ ┐ ┌                         ┐
│1/(T*A) 0   0       0          ││x│ │1/(T*A)*x                │
│0       1/T 0       0          ││y│=│1/T*y                    │
│0       0   F/(F-N) (F*N)/(N-F)││z│ │F/(F-N)*z + (F*N)/(N-F)*w│
│0       0   1       0          ││w│ │1*z                      │
└                               ┘└ ┘ └                         ┘
Note: the above column vector (x,y,z,w) is presumed to be in view space.
The "w" component of the product is 1*z.
So the "z,y,z" components of the product will all scale inversely to z when divied by "w".
The main difference between between an orthographic and a perspective projection matrix is the location of the "1" in the fourth row.
----------------------------------------------------------------------------------
Field of view with a perspective projection:


The above diagram is intended to explain the values "1/(T*A)" and "1/T" in the perspective projection matrix. Any point that is on the edge of the view, if you divide its x or y component by its depth (distance along z axis in view space), then multiply by 1/(T*A) (x axis) or 1/T (y axis), will result in a value of 1.0 (top or right side), or -1.0 (bottom or left side). Any result outside of the range -1.0 to 1.0 on the x or y axis puts that point outside of the view frustum.

Note: aspect = (width/2) / (height/2) = width/height
----------------------------------------------------------------------------------
A few notes on perspective projection and the depth buffer.

Legend
N = near clipping distance
F = far clipping distance
Z = depth in view space (w component after multiplying with perspective projection matrix)
D = value written to depth buffer (or z position in clip volume)

D = [F/(F-N)*Z + (F*N)/(N-F)]/Z = F/(F-N) + (F*N)/(N-F)/Z

when Z = N
D = F/(F-N) + (F*N)/(N-F)/N
D = F/(F-N) + F/(N-F) = F/(F-N) - F/(F-N) = 0.0

when Z = F
D = F/(F-N) + (F*N)/(N-F)/F
D = F/(F-N) + N/(N-F) = F/(F-N) - N/(F-N) = (F-N)/(F-N) = 1.0


Scale of both axes is linear in the chart on the left side. The larger F is in proportion to N, the more abruptly the curve rises toward the N side of F-N, and the less precisely the depth will be written for more distant fragments (z fighting may be visible with low precision).

Values of D outside of the range 0.0 to 1.0 are not within the near and far clipping planes. Geometry that extends outisde of this range will be clipped.
----------------------------------------------------------------------------------
"2D" view matrix:
Code:
Legend
C,S = cosine and sine of view_angle
X,Y,Z = camera location in world space
┌                   ┐
│  C  S  0 -X*C-Y*S │
│ -S  C  0  X*S-Y*C │
│  0  0  1 -Z       │
│  0  0  0  1       │
└                   ┘
----------------------------------------------------------------------------------
One method of constructing a "3d" view matrix:
Using, camera to and from positions, and up vector.
Note: Up vector MUST NOT be parallel to "zaxis", which is the direction the camera is pointing.
Code:
view axes in world space:
zaxis = normalize(cam_to - cam_from)    ← camera's look direction
xaxis = normalize(cross(cam_up, zaxis)) ← camera's right direction
yaxis = cross(zaxis, xaxis)             ← camera's up direction

eye = camera location in world space

  world axes in view space
  x        y        z         world origin in view space
  ↓        ↓        ↓         ↓
┌                                            ┐
│ xaxis_x  xaxis_y  xaxis_z  -dot(xaxis, eye)│
│ yaxis_x  yaxis_y  yaxis_z  -dot(yaxis, eye)│
│ zaxis_x  zaxis_y  zaxis_z  -dot(zaxis, eye)│
│ 0        0        0         1              │
└                                            ┘

EDIT: Added a few things.
 
Last edited:
Okay. Geez, dude. Let me just take a time-out to say, woah. You are awesome, and you really seem to know your stuff. Big props for all that effort, it has not gone unappreciated in the slightest...

Goddamn.

I'm just going to quote myself real quick here...
Happy to do baby steps!
I'm not gonna lie, I'm gonna need some back-and-forth before I can wrap my head around all of that. But don't get me wrong, I want to. That's exactly the kind of information I've been looking for, and I'm absolutely gobsmacked you had the patience to write it all out for me.

I'm still not 100% sure whether you are just trying to learn how the matrices work, or if you are trying to reproduce the math in another way from some reason.
Yep, I can see that :D I'll clarify: I'm trying to create a perspective camera in GMS: 2, without using the camera functions, and without the Draw Event (only Draw GUI). Please refer to each of my attempts to see exactly what I am asking help with :)

Code:
# VERTEX SHADER:
// The following is from 3D Projection in Wikipedia:
// https://en.wikipedia.org/wiki/3D_projection#Mathematical_formula

// Obtain 3D position of point to be projected, _a.
vec3 _a = in_Position;
 
// Obtain 3D position of view, _v.
vec3 _v = vec3( u_fViewX, u_fViewY, u_fViewZ);
 
// Obtain 3D rotation of view, _theta.
vec3 _theta = vec3( u_fViewPitch, u_fViewYaw, u_fViewRoll);
 
// Calculate sin and cos of theta, _s, _c.
vec3 _s = sin(_theta);
vec3 _c = cos(_theta);
 
// Calculate display surface's position relative to view lens, _e.
vec3 _e = _a-_v;
 
// To apply 2D perspective, we first need to determine our 3D transformation point, _d.
vec3 _d = vec3(0.0, 0.0, 0.0);
_d.x = _c.y * (_s.z*_e.y + _c.z*_e.x) - _s.y*_e.z;
_d.y = _s.x * (_c.y*_e.z + _s.y * (_s.z*_e.y + _c.z*_e.x)) + _c.x * (_c.z*_e.y - _s.z*_e.x);
_d.z = _c.x * (_c.y*_e.z + _s.y * (_s.z*_e.y + _c.z*_e.x)) - _s.x * (_c.z*_e.y - _s.z*_e.x);
 
// Finally, determine 2D position of point to be projected, b.
vec4 pos = vec4(0.0, 0.0, 0.0, 1.0/u_fViewScale);
pos.x = (_e.z/_d.z)*_d.x + _v.x;
pos.y = (_e.z/_d.z)*_d.y + _v.y;

// Submit edits to screen.
gl_Position = gm_Matrices[MATRIX_WORLD_VIEW_PROJECTION] * pos;
This all works, albeit roughly. The reason why I was asking about matrices is because I wanted to know how to use GameMaker functions to use the matrices from the Wikipedia maths, instead of having to use convoluted equations in this way.
Okay, let's start at ground zero. I've created a new project, with 1 room and 1 object in that room. The object's code is as follows:
Code:
# CREATE EVENT:
camera_x = room_width/2;
camera_y = room_height/2;
camera_z = 0;
view_x = room_width/2;
view_y = room_height/2;
view_z = 0;

gpu_set_zwriteenable(true);
gpu_set_ztestenable(true);
gpu_set_cullmode(cull_noculling);

# STEP EVENT:
// Camera pos --------- //
var _xaxis = keyboard_check(ord("D")) - keyboard_check(ord("A")),
   _yaxis = keyboard_check(ord("S")) - keyboard_check(ord("W")),
   _zaxis = keyboard_check(ord("V")) - keyboard_check(ord("C")),
   _spd = 10,
   ;
camera_x += _xaxis * _spd;
camera_y += _yaxis * _spd;
camera_z += _zaxis * _spd;

// View pos ----------- //
var _xaxis = ((display_mouse_get_x() - display_get_width()/2) / display_get_width()),
   _yaxis = ((display_mouse_get_y() - display_get_height()/2) / display_get_height()),
   _xaxis_alt = keyboard_check(vk_right) - keyboard_check(vk_left), // Alt controls.
   _yaxis_alt = keyboard_check(vk_down) - keyboard_check(vk_up),
   _xaxis = _xaxis_alt == 0? _xaxis : _xaxis_alt,
   _yaxis = _yaxis_alt == 0? _yaxis : _yaxis_alt,
   _zaxis = mouse_wheel_down() - mouse_wheel_up(),
   _spd = 10,
   _dir_x = point_direction(view_x, view_y, camera_x, camera_y) + 90,  // Travel in 90* tangent to camera.
   _dir_y = point_direction(view_y, view_z, camera_y, camera_z) + 90,
   ;
 
// Horizontal movement
view_x += lengthdir_x(_spd*_xaxis, _dir_x);
view_y += lengthdir_y(_spd*_xaxis, _dir_x);

// Vertical movement
view_y += lengthdir_x(_spd*_yaxis, _dir_y);
view_z += lengthdir_y(_spd*_yaxis, _dir_y);

// Depth movement (scroll wheel)
view_z += _zaxis*_spd;

# DRAW_GUI EVENT:
// Set the perspective
var _mat_world = matrix_build_identity(),
   _mat_proj = matrix_build_projection_perspective(room_width, room_height, 1, 32000),
   _mat_view = matrix_build_lookat(camera_x, camera_y, camera_z, view_x, view_y, view_z, 0, 0, 0),
   _fov = 0.0005,
   ;
_mat_world[8] = camera_x * _fov;  // Apply 'field of view'.
_mat_world[9] = camera_y * _fov;
_mat_world[11] = _fov;
matrix_set(matrix_world, _mat_world);
matrix_set(matrix_projection, _mat_proj); // <-
matrix_set(matrix_view, _mat_view);       // <- Comment these out to see it working normally.

// Draw something
draw_set_colour(c_green);
draw_rectangle(0, 0, 50, 50, false);

// Debug
draw_set_colour(c_blue);
draw_circle(camera_x, camera_y, 8, false);
draw_set_colour(c_red);
draw_circle(view_x, view_y, 8, false);
From this, you can probably see that my knowledge of how the GameMaker functions operate is lacking. The rectangle isn't moving at all, and I'm at a complete loss as to why.
  1. Doesn't 'matrix_build_projection_perspective' and 'matrix_build_lookat' build a projection and view matrix (respectively) for use with 'matrix_build'? Setting the matrices in this way should be all we need to do, right? What's missing?
  2. I've observed the rule in the docs about setting projection before view.
  3. From my tests, I've found that setting matrices has to be in the Draw events. Is this right? Nothing in the Docs mentions it.
  4. Shouldn't the default vertex shader be applying the projection and view matrices to the world matrix in the way you said earlier? E.g. gm_Matrices[MATRIX_WORLD_VIEW_PROJECTION] * object_space_pos.
    They are (automatically by gamemaker) combined into a single matrix by multiplying them in this order: projection * view * world.
  5. The examples given in the Docs just use magic numbers, so it's hard to know what the variables are supposed to represent. For example, matrix_build_lookat says it's meant to be used to move the view around within the projection, but the arguments just say '(640, 240, -10, 640, 240, 0, 0, 1, 0)'. No idea what that means lol.
Hopefully you can see now what I'm trying to achieve.

Once I get the code working, I'd love to sit down with my whiteboard and your post, and really dig into what's going on behind the scenes - but unfortunately for now, I'm too visual of a learner to figure this stuff out without a working prototype to play with.

But dude, thanks again. This is mega.

EDIT: Fleshed out second attempt a bit.
 
Last edited:
Doesn't 'matrix_build_projection_perspective' and 'matrix_build_lookat' build a projection and view matrix (respectively) for use with 'matrix_build'? Setting the matrices in this way should be all we need to do, right? What's missing?
matrix_build_projection_perspective_fov(fov_y, aspect, znear, zfar);

fov_y = field of view across vertical axis.
note: field of view across horizontal axis is: 2*atan(tan(fov/2)*aspect)
aspect = width of port / height of port
znear and zfar = near and far clipping distances

matrix_build_lookat(xfrom, yfrom, zfrom, xto, yto, zto, xup, yup, zup);

from = position of camera
to = the point that camera is looking at
up = up direction of camera (will be made orthonormal with the "look" and "right" directions within this function, meaning all three vectors will be perpendicular to each other and unit length. But up vector must not be parallel to the look direction!)

I've observed the rule in the docs about setting projection before view.
I am not aware of this. I cannot think of any reason why that should be. It does not appear to be the case in GMS1.4 at the least.

From my tests, I've found that setting matrices has to be in the Draw events. Is this right? Nothing in the Docs mentions it.
You should do it in the draw events. And if you set a new render target, you will probably need to set the view and projection again.

Shouldn't the default vertex shader be applying the projection and view matrices to the world matrix in the way you said earlier? E.g. gm_Matrices[MATRIX_WORLD_VIEW_PROJECTION] * object_space_pos.
Yes it does.
 
matrix_build_projection_perspective_fov(fov_y, aspect, znear, zfar);
matrix_build_lookat(xfrom, yfrom, zfrom, xto, yto, zto, xup, yup, zup);
Ok, so I'm not entirely sure by your description what it was that I did wrong, but I've done my best to have another go. It still doesn't work! Would you mind trying this code to see if it works for you? Or is there's something glaringly obvious wrong with it? It's all self-contained, so it just needs a fresh room and object to get started.
Code:
// CREATE EVENT:
camera_x = room_width/2;
camera_y = room_height/2;
camera_z = 0;
view_x = room_width/2;
view_y = room_height/2;
view_z = 0;
view_yaw = 0;
view_pitch = 0;
view_roll = 0;
focal_length = 10;

gpu_set_zwriteenable(true);
gpu_set_ztestenable(true);
gpu_set_cullmode(cull_noculling); // Purely aesthetic, I know.
Code:
// STEP EVENT:
// Camera pos - FROM //
// This moves our FROM position in the view matrix.
var _xaxis = keyboard_check(ord("D")) - keyboard_check(ord("A")),
    _yaxis = keyboard_check(ord("W")) - keyboard_check(ord("S")),
    _zaxis = keyboard_check(ord("V")) - keyboard_check(ord("C")),
    _spd = 10,
    ;
camera_x += _xaxis * _spd;
camera_y += _yaxis * _spd;
camera_z += _zaxis * _spd;

// View pos - TO //
// This moves our TO position in the view matrix, around the camera.
var _xaxis = ((display_mouse_get_x() - display_get_width()/2) / display_get_width()),
    _yaxis = ((display_mouse_get_y() - display_get_height()/2) / display_get_height()),
    _zaxis = keyboard_check(ord("E")) - keyboard_check(ord("Q")),
    _xaxis_alt = keyboard_check(vk_right) - keyboard_check(vk_left), // Alt controls.
    _yaxis_alt = keyboard_check(vk_down) - keyboard_check(vk_up),
    _xaxis = _xaxis_alt == 0? _xaxis : _xaxis_alt,
    _yaxis = _yaxis_alt == 0? _yaxis : _yaxis_alt,
    _rotational_spd = 10,
    ;
// Rotations
view_yaw += _xaxis*_rotational_spd;  // xy
view_pitch += _yaxis*_rotational_spd;  // yz
view_roll += _zaxis*_rotational_spd;  // zx

// Focal length
focal_length += mouse_wheel_down()-mouse_wheel_up();

// Movement
view_x = camera_x + lengthdir_x(focal_length, view_yaw) + lengthdir_y(focal_length, view_roll);
view_y = camera_y + lengthdir_y(focal_length, view_yaw) + lengthdir_x(focal_length, view_pitch);
view_z = camera_z + lengthdir_y(focal_length, view_pitch) + lengthdir_x(focal_length, view_roll);
Code:
// DRAW_GUI EVENT:
// Set the perspective
var _fov = 1,
    _mat_proj = matrix_build_projection_perspective_fov(_fov, room_width/room_height, 1.0, 32000.0),
    _mat_view = matrix_build_lookat(camera_x, camera_y, camera_z, view_x, view_y, view_z, 0, 0, 0),
    ;
//matrix_set(matrix_projection, _mat_proj); // <-
//matrix_set(matrix_view, _mat_view);        // <- Uncomment these and watch it break.

// Draw something
draw_set_colour(c_green);
draw_rectangle(0, 0, 50, 50, false);

// Debug
draw_set_colour(c_blue);
draw_circle(camera_x, camera_y, 8, false);
draw_set_colour(c_red);
draw_circle(view_x, view_y, 8, false);
 
Last edited:
All right, I'm absolutely agonizing over this one, so I've continued bashing my friends Trial and Error together until I've finally gotten a little further... I realize now that it makes no sense leaving the Up vector at 0, although I still don't really understand what it's doing. I thought for a while it was yaw, pitch and roll, but after trying that, everything went really messy and I'm pretty sure rotational values are best left as the relationship between camera and view. I can understand how the Up vector is rotation around the axis toward the viewpoint, but how is this a vec3? The only component of that is roll, isn't it? So I've left these values at 1, and now have a project that allows you to view a cube with perspective. The controls are still quite wonky though, and I can't figure out exactly what's going on to fix it.
Code:
/// @function object0/Create
#macro room_depth 256

// The point in 3D space we're looking from.
camera_x = room_width/2;
camera_y = room_height/2;
camera_z = 0;

// The 3D rotation of the camera lens, relative to our viewpoint.
camera_yaw = 0;
camera_pitch = 0;
camera_roll = 0;

// Dist from camera to view.
focal_length = 10;

// The point in 3D space we're looking at.
view_x = room_width/2;
view_y = room_height/2;
view_z = 0;

// 3D vertex format
vertex_format_begin();
vertex_format_add_position_3d();
vertex_format_add_colour();
vertex_format_add_texcoord();
vformat = vertex_format_end();

// Vertex buffer
vbuffer = vertex_create_buffer();

// Draw a cube
vertex_begin(vbuffer, vformat);
draw_cube(room_width/2, room_height/2, room_depth/2, 256);
vertex_end(vbuffer);

// Graphics settings
gpu_set_zwriteenable(true);
gpu_set_ztestenable(true);
gpu_set_cullmode(cull_noculling); // Don't want to get bogged down with this.
Code:
/// @function object0/Step

// CAMERA - LOOKING FROM //
// Position
var _xaxis = keyboard_check(ord("D")) - keyboard_check(ord("A")),
    _yaxis = keyboard_check(ord("W")) - keyboard_check(ord("S")),
    _zaxis = keyboard_check(ord("V")) - keyboard_check(ord("C")),
    _spd = 10,
    ;
camera_x += lengthdir_x(_xaxis*_spd, camera_yaw) + lengthdir_y(_xaxis*_spd, camera_roll);
camera_y += lengthdir_y(_yaxis*_spd, camera_yaw) + lengthdir_x(_yaxis*_spd, camera_pitch);
camera_z += lengthdir_y(_zaxis*_spd, camera_pitch) + lengthdir_x(_zaxis*_spd, camera_roll);

// Rotation
// Mouse movement
var _xaxis = (display_mouse_get_x() / display_get_width()) - 0.5,
    _yaxis = (display_mouse_get_y() / display_get_height()) - 0.5,
    _zaxis = keyboard_check(ord("E")) - keyboard_check(ord("Q")),
    /* Arrow key movement
    _xaxis = keyboard_check(vk_right) - keyboard_check(vk_left),
    _yaxis = keyboard_check(vk_down) - keyboard_check(vk_up),
    _zaxis = keyboard_check(ord("E")) - keyboard_check(ord("Q")),
    */
    _spd = 0.5,
    ;
camera_yaw += _xaxis*_spd;  // XY - rotate along Z axis
camera_pitch += _yaxis*_spd;  // YZ - rotate along X axis
camera_roll += _zaxis*_spd;  // XZ - rotate along Y axis
display_mouse_set(display_get_width()/2, display_get_height()/2); // Keep cursor centered.

// VIEW - LOOKING AT //
// Focal length
// This is how far away the view is from the camera.
var _waxis = mouse_wheel_down()-mouse_wheel_up(),
    _spd = 10,
    ;
focal_length += _waxis * _spd; // i.e. scale?

// Position
// This is the point, relative to the camera, that we're looking at.
view_x = camera_x + lengthdir_x(focal_length, camera_yaw) + lengthdir_y(focal_length, camera_roll);
view_y = camera_y + lengthdir_y(focal_length, camera_yaw) + lengthdir_x(focal_length, camera_pitch);
view_z = camera_z + lengthdir_y(focal_length, camera_pitch) + lengthdir_x(focal_length, camera_roll);
Code:
/// @function object0/Draw_GUI
// Set the perspective
var _fov = 60,  // I've been setting this high so I can find the damn thing.
    _mat_proj = matrix_build_projection_perspective_fov(_fov, room_width/room_height, 1.0, 320000.0),
    _mat_view = matrix_build_lookat(camera_x, camera_y, camera_z, view_x, view_y, view_z, 1, 1, 1),
    ;
matrix_set(matrix_projection, _mat_proj); // <-
matrix_set(matrix_view, _mat_view); // <- Comment these out to see it working normally.

// Draw the cube.
vertex_submit(vbuffer, pr_trianglelist, -1);

// Debug
draw_set_colour(c_blue);
draw_circle(camera_x, camera_y, 8, false);
draw_set_colour(c_red);
draw_circle(view_x, view_y, 8, false);
Code:
///@function draw_cube(x, y, z)
var _cx = argument0,
    _cy = argument1,
    _cz = argument2,
    _len = argument3,
    _hlen = _len/2,
    _left = _cx-_hlen,
    _right = _cx+_hlen,
    _back = _cy-_hlen,
    _front = _cy+_hlen,
    _top = _cz-_hlen,
    _bot = _cz+_hlen,
    ;

// Draw left and right face (fixed X)
draw_set_colour(c_red);
draw_rect(_left, _left, _back, _front, _top, _bot);
draw_rect(_right, _right, _back, _front, _top, _bot);

// Draw front and back face (fixed Y)
draw_set_colour(c_green);
draw_rect(_left, _right, _back, _back, _top, _bot);
draw_rect(_left, _right, _front, _front, _top, _bot);

// Draw top and bottom face (fixed Z)
draw_set_colour(c_blue);
draw_rect(_left, _right, _back, _front, _top, _top);
draw_rect(_left, _right, _back, _front, _bot, _bot);
Code:
/// @function draw_rect(left, right, back, front, top, bot)
var _left = argument0,
    _right = argument1,
    _back = argument2,
    _front = argument3,
    _top = argument4,
    _bot = argument5,
    ;
// Tri #1
draw_vert(_left, _back, _top);
draw_vert(_right, _front, _top);
draw_vert(_left, _front, _bot);

// Tri #2
draw_vert(_right, _back, _top);
draw_vert(_right, _front, _bot);
draw_vert(_left, _back, _bot);
Code:
/// @function draw_vert(x, y, z)
var _x = argument0,
    _y = argument1,
    _z = argument2,
    _col = draw_get_colour(),
    _alpha = draw_get_alpha(),
    _vb = vbuffer;
vertex_position_3d(_vb, _x, _y, _z);
vertex_colour(_vb, _col, _alpha);
vertex_texcoord(_vb, 0, 0);
 
Last edited:
Can you describe exactly how you want your camera to move? There are a lot of different ways.

In the spoiler below, I show you how you can orient the camera using Eulur angles and a rotation order equal to yaw*pitch*roll... (i.e rotation is applied in order: roll, then pitch, then yaw). Translation of the camera is done relative to its current orientation. I.e., forward, right, and upward motion is relative to the camera, and not the world.

I think this type of motion is nearest to what you were trying to do with your code. Correct me if I'm wrong about that. The orientation and position of the view can be determined in a number of different ways. An alternative would be to use just what the matrix_build_lookat function asks for, which is to provide a from and to position, as well as an up vector. And those values can be chosen in almost any way and still produce some kind of a result. Another alternative is to make rotations relative to the present orientation of the view. Still another alternative is to set orientation using an axis of rotation and an angle.

Code:
cam_roll = clamp(cam_roll + 1 * ( keyboard_check(ord("O")) - keyboard_check(ord("U")) ),-15,15);
cam_pitch += 2 * ( keyboard_check(ord("I")) - keyboard_check(ord("K")) )
cam_yaw +=   2 * ( keyboard_check(ord("J")) - keyboard_check(ord("L")) )
var _cy = dcos(cam_pitch),
    _sy = dsin(cam_pitch),
    _cz = dcos(cam_yaw),
    _sz = dsin(cam_yaw),
    _cx = dcos(cam_roll),
    _sx = dsin(cam_roll);
    //look axis            //right axis                    //up axis
var _lookx =  _cz*_cy; var _rightx =  _cz*_sy*_sx+_sz*_cx; var _upx = -_cz*_sy*_cx+_sz*_sx;
var _looky = -_sz*_cy; var _rightx = -_sz*_sy*_sx+_cz*_cx; var _upx =  _sz*_sy*_cx+_cz*_sx;
var _lookz =  _sy;     var _rightx = -_cy*_sx;             var _upx =  _cy*_cx;
//move camera
var _x = 4 * ( keyboard_check(ord("D")) - keyboard_check(ord("A")) );
var _y = 4 * ( keyboard_check(ord("E")) - keyboard_check(ord("Q")) );
var _z = 4 * ( keyboard_check(ord("W")) - keyboard_check(ord("S")) );
cam_x += _x * _rightx + _y * _upx + _z * _lookx;
cam_y += _x * _righty + _y * _upy + _z * _looky;
cam_z += _x * _rightz + _y * _upz + _z * _lookz;
view_matrix = matrix_build_lookat(
    cam_x, cam_y, cam_z,
    cam_x + _lookx*some_dist, cam_y + _looky*some_dist, cam_z + _lookz*some_dist,
    up_x, up_y, up_z
),
Or skip using built in matrix build function.

Below, we set the view matrix elements directly.

Note: _v = view_matrix.
Code:
cam_roll = clamp(cam_roll + 1 * ( keyboard_check(ord("O")) - keyboard_check(ord("U")) ),-15,15);
cam_pitch += 2 * ( keyboard_check(ord("I")) - keyboard_check(ord("K")) )
cam_yaw +=   2 * ( keyboard_check(ord("J")) - keyboard_check(ord("L")) )
var _cy = dcos(cam_pitch),
    _sy = dsin(cam_pitch),
    _cz = dcos(cam_yaw),
    _sz = dsin(cam_yaw),
    _cx = dcos(cam_roll),
    _sx = dsin(cam_roll);
var _v = view_matrix;
_v[@0] =  _cz*_sy*_sx+_sz*_cx; _v[@4] = -_sz*_sy*_sx+_cz*_cx; _v[@8] =  -_cy*_sx; //right axis
_v[@1] = -_cz*_sy*_cx+_sz*_sx; _v[@5] =  _sz*_sy*_cx+_cz*_sx; _v[@9] =   _cy*_cx; //up axis
_v[@2] =  _cz*_cy;             _v[@6] = -_sz*_cy;             _v[@10] =  _sy;     //look axis
//move camera
var _x = 4 * ( keyboard_check(ord("D")) - keyboard_check(ord("A")) );
var _y = 4 * ( keyboard_check(ord("E")) - keyboard_check(ord("Q")) );
var _z = 4 * ( keyboard_check(ord("W")) - keyboard_check(ord("S")) );
//--------------------
//"camera_position" += _x * "right axis" + _y * "up axis" + _z * "look axis"
cam_x += _x * _v[0] + _y * _v[1] + _z * _v[2];
cam_y += _x * _v[4] + _y * _v[5] + _z * _v[6];
cam_z += _x * _v[8] + _y * _v[9] + _z * _v[10];
//--------------------
_v[@12] = -cam_x * _v[0] - cam_y * _v[4] - cam_z * _v[8];  //-dot("camera_position", "right axis")
_v[@13] = -cam_x * _v[1] - cam_y * _v[5] - cam_z * _v[9];  //-dot("camera_position", "up axis")
_v[@14] = -cam_x * _v[2] - cam_y * _v[6] - cam_z * _v[10];  //-dot("camera_position", "look axis")
//--------------------
_v[@15] = 1; //only needs to be set once at start.
 
Last edited:
In the spoiler below, I show you how you can orient the camera using Eulur angles and a rotation order equal to yaw*pitch*roll... (i.e rotation is applied in order: roll, then pitch, then yaw).
Ahh, I see! Yeah I was way off, wasn't I? But wow, thank you so much - I'm able to play around in a 3D sandbox now, and can really get a sense of how the maths is playing a part. (I'm using the first example btw).

Having said that, now that I have something to play around with, would you mind explaining a few aspects of this for me?
  1. What exactly is going on when you calculate the look, right and up vectors? I understand that you're applying Euler angles in order of yaw, pitch and roll, although I don't get why it's in that order? More than that, though, I really don't understand what's going on with dsin and dcos, and why that's being applied in such sporadic ways? I don't see any pattern to that madness.
  2. Just clarifying - but a simplified explanation of an Euler angle is that it's a scalar component of a vec3 orientation, right? E.g. yaw, pitch, and roll are each scalar components of a vec3 rotation. So multiplying vec3(roll, pitch, yaw) * vec3(x, y, z) will apply your rotational force to your position?
  3. Lastly, what is the 'some_dist' variable actually doing here? After setting it to 1, I played around with setting it to the FOV, although I don't think it really does anything as a measurement of distance (obviously the FOV applied to perspective proj does something).
I won't be offended at any point if you wanna bail out btw. You've really helped loosen some of the more stubborn parts of this puzzle - so you get big ups from me regardless.
 
What exactly is going on when you calculate the look, right and up vectors? I understand that you're applying Euler angles in order of yaw, pitch and roll, although I don't get why it's in that order? More than that, though, I really don't understand what's going on with dsin and dcos, and why that's being applied in such sporadic ways? I don't see any pattern to that madness.
Rotations with Euler angles can be done in any order. Roll then Pitch then Yaw tends to work well for things like first-person-shooters.

If you represent each rotation as a 3x3 matrix, and then multiply them in the order Yaw*Pitch*Roll, you get the following:
Code:
pitch       roll        pitch*roll
│cy  0 -sy││1  0   0 │ │cy  sy*sx -sy*cx│
│0   1  0 ││0  cx  sx│=│0   cx     sx   │
│sy  0  cy││0 -sx  cx│ │sy -cy*sx  cy*cx│

 yaw        (pitch*roll)       yaw*(pitch*roll)
│ cz  sz  0││y  sy*sx -sy*cx│ │ cz*cy  cz*sy*sx+sz*cx -cz*sy*cx+sz*sx│
│-sz  cz  0││ 0   cx   sx   │=│-sz*cy -sz*sy*sx+cz*cx  sz*sy*cx+cz*sx│
│ 0   0   1││sy -cy*sx cy*cx│ │ sy    -cy*sx           cy*cx         │
                                ↑      ↑               ↑
                                look   right           up
Just clarifying - but a simplified explanation of an Euler angle is that it's a scalar component of a vec3 orientation, right? E.g. yaw, pitch, and roll are each scalar components of a vec3 rotation. So multiplying vec3(roll, pitch, yaw) * vec3(x, y, z) will apply your rotational force to your position?
Not sure what you mean here by a vec3 rotation.

Lastly, what is the 'some_dist' variable actually doing here? After setting it to 1, I played around with setting it to the FOV, although I don't think it really does anything as a measurement of distance (obviously the FOV applied to perspective proj does something).
It's just putting some distance between the camera's from and to positions.
 
Rotations with Euler angles can be done in any order. Roll then Pitch then Yaw tends to work well for things like first-person-shooters.
Hm, do you know why this is? I'll play around with different orders and probably see for myself, but I'm curious as to why roll pitch yaw works best for FPS's.

If you represent each rotation as a 3x3 matrix, and then multiply them in the order Yaw*Pitch*Roll, you get the following:
Code:
pitch       roll        pitch*roll
│cy  0 -sy││1  0   0 │ │cy  sy*sx -sy*cx│
│0   1  0 ││0  cx  sx│=│0   cx     sx   │
│sy  0  cy││0 -sx  cx│ │sy -cy*sx  cy*cx│

 yaw        (pitch*roll)       yaw*(pitch*roll)
│ cz  sz  0││y  sy*sx -sy*cx│ │ cz*cy  cz*sy*sx+sz*cx -cz*sy*cx+sz*sx│
│-sz  cz  0││ 0   cx   sx   │=│-sz*cy -sz*sy*sx+cz*cx  sz*sy*cx+cz*sx│
│ 0   0   1││sy -cy*sx cy*cx│ │ sy    -cy*sx           cy*cx         │
                                ↑      ↑               ↑
                                look   right           up
Ahh, I see! That makes a lot more sense, thanks. So would it be viable then to do this within matrix functions in GM? Rather than writing all the math out as an equation?

Not sure what you mean here by a vec3 rotation.
Uh, hm. Well, we take the rotational values in xyz, and wrap it up in a 3-tuple - i.e. a vec3? I think I answered my own question though, because just multiplying that by the position won't do squat. That's why you're using dcos and dsin. Sorry, don't mind me.

It's just putting some distance between the camera's from and to positions.
Does that have any actual effect? Changing that value in-game doesn't seem to do anything.
 
Last edited:
@flyingsaucerinvasion
Hey mate, sorry to revive a dying thread (I mean it's just us in here anyway lol), but I was hoping you might be able to help me with getting the rotation input relative to the current angles, in the same vein as the position input. I've been trying to apply the same yaw * pitch * roll logic on the current angles, although it's just not working right. I feel like there needs to be a 90* offset in there or something, but I can't get it to work.
Code:
// ROTATE CAMERA //
var _dir_axis_x = 1.0 - (display_mouse_get_x()/display_get_width()+0.5),
    _dir_axis_y = keyboard_check(ord("Q")) - keyboard_check(ord("E")),
    _dir_axis_z = 1.0 - (display_mouse_get_y()/display_get_height()+0.5),
    /* - Alternate controls.
    _dir_axis_x = keyboard_check(vk_left) - keyboard_check(vk_right),
    _dir_axis_y = keyboard_check(ord("Q")) - keyboard_check(ord("E")),
    _dir_axis_z = keyboard_check(vk_up) - keyboard_check(vk_down),
    */
    _dir_spd = 1.0,
    _dir_vel_x = _dir_spd * _dir_axis_x,
    _dir_vel_y = _dir_spd * _dir_axis_y,
    _dir_vel_z = _dir_spd * _dir_axis_z,
                           
    // Trying to adapt this for camera rotation.
    _cz = dcos(camera_yaw),  // Update the new camera rotation
    _sz = dsin(camera_yaw),  // with reference to old rotations.
    _cy = dcos(camera_pitch),
    _sy = dsin(camera_pitch),
    _cx = dcos(camera_roll),
    _sx = dsin(camera_roll),
    _lookx =  _cz*_cy,
    _looky = -_sz*_cy,
    _lookz =  _sy,
    _rightx = _cz*_sy*_sx+_sz*_cx,
    _righty = -_sz*_sy*_sx+_cz*_cx,
    _rightz = -_cy*_sx,
    _upx = -_cz*_sy*_cx+_sz*_sx,
    _upy =  _sz*_sy*_cx+_cz*_sx,
    _upz =  _cy*_cx,
    ;
cameraSetYaw(camera_yaw + _dir_vel_x*_rightx + _dir_vel_y*_upx + _dir_vel_z*_lookx);
cameraSetRoll(camera_roll + _dir_vel_x*_righty + _dir_vel_y*_upy + _dir_vel_z*_looky);
cameraSetPitch(camera_pitch + _dir_vel_x*_rightz + _dir_vel_y*_upz + _dir_vel_z*_lookz);

/*
// The result is a camera that has mostly the same behaviour as:
cameraSetYaw(camera_yaw + _dir_vel_x );
cameraSetRoll(camera_roll + _dir_vel_y);
cameraSetPitch(camera_pitch + _dir_vel_z );
// Except it's just a bit... stranger? lol.
*/
If it wasn't clear - the goal is to have a camera that moves relative to the current view. So pressing the up arrow rotates the camera up relative to what we see, even if that would be affecting the yaw and not the pitch.
 
Well, here's one way to go about it.

notes:
_v = view matrix.
_zr, _yr, _xr = rotation around (view's) look, up, and right axes... corresponding to (roll, yaw, pitch) relative to view's current orientation.

Important: Due to cumalative error, it will probably eventually be necessary to re-orthonormalize the view matrix. This really doesn't need to be done very often (depending on needs), the error will be pretty small for a good length of time.

Code:
//rotation (no bottom row because using cross product to compute new view's look axis)
var _cx = cos(_xr); var _sx = sin(_xr);
var _cy = cos(_yr); var _sy = sin(_yr);
var _cz = cos(_zr); var _sz = sin(_zr);
var _b0 =  _cy*_cz-_sy*_sx*_sz; var _b3 = _cy*_sz+_sy*_sx*_cz; var _b6 = -_sy*_cx;
var _b1 = -_cx*_sz;             var _b4 = _cx*_cz;             var _b7 =  _sx;

//copy old view orientation
var _a0 = _v[0]; var _a3 = _v[4]; var _a6 = _v[8];
var _a1 = _v[1]; var _a4 = _v[5]; var _a7 = _v[9];
var _a2 = _v[2]; var _a5 = _v[6]; var _a8 = _v[10];

//new view orientation
_v[@0] = _b0*_a0 + _b3*_a1 + _b6*_a2;  _v[@4] = _b0*_a3 + _b3*_a4 + _b6*_a5;  _v[@8] =  _b0*_a6 + _b3*_a7 + _b6*_a8; //view's right axis
_v[@1] = _b1*_a0 + _b4*_a1 + _b7*_a2;  _v[@5] = _b1*_a3 + _b4*_a4 + _b7*_a5;  _v[@9] =  _b1*_a6 + _b4*_a7 + _b7*_a8; //view's up axis
_v[@2] = _v[4]*_v[9] - _v[5]*_v[8];    _v[@6] = _v[8]*_v[1] - _v[9]*_v[0];    _v[@10] = _v[0]*_v[5] - _v[1]*_v[4];  //view's look axis = cross(right,up)
 
Last edited:
Well, here's one way to go about it.
Gee, there's a lot to unpack here, and I'm realising that I don't understand the basic, underlying rationale for why this all works. I couldn't get your example to work even a little bit - so if you'll humour me, I'd like to take this journey to a code block:
Code:
Preface:
The big conceptual issue I am having with all of this, is what the 'look',
'right', and 'up' vectors actually represent.

For example, to achieve a 'roll', you look along the Y axis and apply a
vector with components in X and Z. More generally, to apply a Euler Angle,
you need to suspend the third dimension while you do it. That's why we
use the sine and cosine of these angles, because we're calculating the
two components along the third dimension. (E.g. roll calculates change to
XZ along Y; yaw calculates change to XY along Z; pitch calculates change
to ZY along X).

Thereotically, in order to effectuate each of these changes, we should
only have to add each of these components to our position in 3D space.
But instead, we're using look, right, and up vectors, which seem to
represent these angle components in no clear or discernible way.
At the crux of the matter, is that I don't understand why something
like 'look', which refers to the Z axis, could have a value -for- z.

Please, allow me to explain my understanding with a little pseudo-code.
Feel free to pull me up on any assumptions or errors with my working
that may explain my confusion.

---

Pitch shifts YZ in respect of X axis. Let's call pitch 'rot_around_x'.
Roll shifts XZ in respect of Y axis. Let's call roll 'rot_around_y'.
Yaw shifts XY in respect of Z axis. Let's call yaw 'rot_around_z'.

Remembering high school maths:

        /|
     H / |O
      /__|
    θ   A
   
It is important to note that 0* always starts at an eastern bearing.
Meaning the adjacent will always run along X (or horizontally), and
the opposite along Y (or vertically).

   cos(θ) = A / H;
   sin(θ) = O / H;

I like to look at H as a force vector. With a force of 1 and dir of θ,
we can calculate its 'X 'and 'Y' components - i.e. opposite and adjacents.

   cos(θ) = A / 1; cos(θ) = A;
   sin(θ) = O / 1; sin(θ) = O;

   ~ (therefore)
   rot_around_z = θ = 30;
   x_rot_around_z = cos(θ) = 0.866;
   y_rot_around_z = sin(θ) = 0.5;

By extracting these components, we effectively get a set of instructions we
can apply to our shape's vertices, to rotate our shape by any given angle.

   ~
   rot_around_x = 30;
   y_rot_around_x = 0.866;
   z_rot_around_x = 0.5;

   rot_around_y = 30;
   x_rot_around_y = 0.866;
   z_rot_around_y = 0.5;

But now is where I get fuzzy. If we were rotating the view frustum as if it
were any normal quad, I would say to run through each of our angles and
add their components to that quad's vertices.

   ~
   point_in_3d_space.x += x_rot_around_y + x_rot_around_z;
   point_in_3d_space.y += y_rot_around_x + y_rot_around_z;
   point_in_3d_space.x += z_rot_around_x + z_rot_around_y;

But that's not what we're doing, and it's this next step that I need
help understanding. For example,

1. What are the 'right', 'up' and 'look' variables referring to? Are they
   referring to the axes in x,y,z, respectively?
 
2. What are these variables doing exactly? For example, you could say that
   'x_rot_around_y' represents the X component of our desired rotation around Y.
   So what is 'lookx' doing? In your example from 5 Sept, you showed that:
   _lookx =  _cz*_cy;
   _looky = -_sz*_cy;
   _lookz =  _sy;

   Remembering my working-out earlier for cos and sin:

       rot_around_z = θ;
       x_rot_around_z = cos(θ);
       y_rot_around_z = sin(θ);

       rot_around_y = θ;
       x_rot_around_y = cos(θ);
       z_rot_around_y = sin(θ);

       ~
       _lookx = x_rot_around_z * x_rot_around_y;
       _looky = -y_rot_around_z * x_rot_around_y;
       _lookz = z_rot_around_y;
 
   We now have the XYZ of a point in 3D space that represents... what?
   -   Its X represents positive change in X around Z multiplied by positive
       change in X around Y.
   -   Its Y represents negative change in Y around Z multiplied by positive
       change in X around Y.
   -   Its Z represents positive change in Z around Y.

   a.  I don't understand how something measuring a change in Z, can also
       have a value -for- Z. In my mind, this breaks the rule about suspending
       the third dimension.
   b.  I also don't understand what the effect of multiplying these changes
       together is having. How does 0.866 * 0.866 (=0.75) do anything? I don't
       understand why we're not adding these changes together, but are multiplying
       them?
   c.  And I REALLY don't understand what GM is doing with these vectors once
       they're churned into the back-end.
   
       Although I know that it works, because of your example from 5 Sept, I want
       to know -why- it works -- and I think that is what's holding me back from
       understanding your latest code example.

Thanks for following me on this :)
Edit: Significant restructure to my question.
 
Last edited:
Top