Perfect 3D Camera Rotation

M

MHBlacky

Guest
GM Version: GMS 1.4
Target Platform: All
Links: Basic First Person Camera Tutorial by The Sivart:

Summary
In this tutorial I will show you how to code a 3D camera rotation that can be done with the mouse. Compared to other 3D camera rotation codes with this one you can look anywhere from straight up in the air to straight down to the floor without any awkward changes in camera speed.

Tutorial
There are a lot of basic tutorials out there for 3D camera rotation. I've linked one in the header of this post. But one problem that they all have is that with the provided code it's impossible to look up or down completely in a 90 degree angle. But since you can do that in basically any other 3D game I'm sure you want to be able to do that in your game aswell. I was looking for a solution on the internet but didn't find one. If you know a post that already adresses this problem then this tutorial is unnecessary, my bad. But after some coding and testing I came up with this solution and wanted to share it with you guys.

First of this tutorial is about the camera rotation itself so I won't go into detail about setting up the views, the movement and all the other stuff needed for a functional 3D game. Also for simplicity reasons we assume that we're doing a first person camera. The provded steps also work for 3rd person cameras of course.

So this is what the usual 3D camera code looks like:
Code:
direction -= (display_mouse_get_x() - (display_get_width()/2));
pitch -= (display_mouse_get_y() - (display_get_height()/2));
pitch = clamp(pitch, -90, 90);

cam_x = x + lengthdir_x(100,direction);
cam_y = y + lengthdir_y(100,direction);
cam_z = 10 + pitch;

d3d_set_projection(x,y,z,cam_x,cam_y,cam_z,0,0,1);
x,y and z are the coordinates where the camera is placed. cam_x, cam_y and cam_z are the coordinates of the point the camera is looking at. This point - let's call it CP - changes as we move the mouse. With the commands

cam_x = x + lengthdir_x(100,direction);
cam_y = y + lengthdir_y(100,direction);
cam_z = 10 + pitch;

we basically define a cylinder with a radius of 100 and a height of +-90 (because pitch goes from -90 to 90 degrees) around the player. This cylinder is a model for all the coordinats CP can have. And this leads to the basic problem: A correct camera movement should not work with a cylindric model. That's also the reason we can't look up and down correctly because the cylinder is not infinitely high and if it was then the camera sensitivity would be really weird. It's difficult for me to describe but believe me, you don't want that!

The solution: We use a sphere as model for our CP not a cylinder. If CP moves on a spheric surface we can look up and down correctly without any change to the mouse sensitivity. And that's how we do it:

For the cylinder "cam_x = x + lengthdir_x(100,direction);" and "cam_y = y + lengthdir_y(100,direction);" define the circular base and "cam_z = 10 + pitch;" the height. If we want to reshape that into a sphere we have to change cam_x and cam_y dynamically as a function of the height (pitch) of our sphere. The picture compares the spherical with the cylindrical model and should clarify what I mean:

cylindric vs spheric model.png

So in order to change cam_x and cam_y dynamically we use the circle function f(x) = sqrt( r ²- x² ) with r being the radius of the circle:

Code:
var f_x = sqrt(power(91,2) - power(pitch,2)); // We define the radius as 91 so the sqrt doesn't get negative since pitch ranges from -90 to 90

cam_x = x + lengthdir_x(f_x, direction);
cam_y = y + lengthdir_y(f_x, direction);
cam_z = 10 + pitch;
With this code you should have a perfect camera movement over the whole pitch range.

If there's a built-in way to da that I feel stupid for posting this xD

Still hope it was helpful ;)
 
Last edited by a moderator:
i cant seem to figure out how this is meant to work, pitch or z_direction if you follow the video tutorial is a number between -90 or 90 after youve clamped it but you then define the radius as 91 as you said you stop it being a negative value but power(pitch,2) will still output negatives and give you this crash:

edit: forgot to change z_direction to pitch and i copy pasted a error accidentally saying that pitchj wasnt defined >_>!

Code:
___________________________________________
############################################################################################
ERROR in
action number 1
of  Step Event0
for object player:

Cannot apply sqrt to negative number.
 at gml_Object_player_StepNormalEvent_1 (line 6) - var f_x = sqrt(power(91,2) - power(pitch,2));
############################################################################################
--------------------------------------------------------------------------------------------
stack frame is
gml_Object_player_StepNormalEvent_1 (line 6)
 
Last edited:
ok i got it to stop erroring out due to negatives, issue i have now is my camera is backwards (all the controls inverted) and from about 14 to 90 and the same for a negative cam_z it barely moves the camera up or down and both negative and positive are backwards but center, but you go anywhere between -14 and 14 and suddenly it accellerates, flips the camera around 0 (where it actually ends up the right way around) and then flips it again as it goes in the other direction and ends up backwards again.

edit: as a note, the way i got it to stop erroring out due to negatives was change:
sqrt(power(90,2) - power(pitch,2)); to sqrt(power(90,2)) - power(pitch,2)

my setup for my camera is as follows:

create event:
Code:
d3d_start();
depth = 1000;
//fogcol = make_color_rgb(0,100,20)
z = 32;

skydist = 5000;

cam_x = 0
cam_y = 0
cam_z = 0

pitch = 32

direction = 0

lookdir = direction

d3d_set_fog(true,c_white, skydist-(skydist/4), skydist)
step event:
Code:
direction -= (display_mouse_get_x()-display_get_width()/2)/5;
pitch -= (display_mouse_get_y()-display_get_height()/2)/3;
display_mouse_set(display_get_width()/2,display_get_height()/2);
pitch = clamp(pitch, -90, 90);

var f_x = sqrt(power(90,2)) - power(pitch,2);

cam_x = x + lengthdir_x(f_x, direction);
cam_y = y + lengthdir_y(f_x, direction);
cam_z = 10 + pitch

lookdir = direction;
if (keyboard_check(ord("W"))+keyboard_check(ord("S"))+
    keyboard_check(ord("A"))+keyboard_check(ord("D"))) == 1{
    if keyboard_check(ord("A")){
                direction += 90;
                speed = 6;
       }
   
     if keyboard_check(ord("D")){
                direction -= 90;
                speed = 6;
        }
   
     if keyboard_check(ord("W")){
            direction = lookdir;
            speed = 6;
        }
       
     if keyboard_check(ord("S")){
            direction += 180;
            speed = 6;
        }
}
else
{

    if keyboard_check(ord("W")) && keyboard_check(ord("A"))
    {
        direction += 45;
        speed = 6;
    }
    else
    if keyboard_check(ord("W")) && keyboard_check(ord("D"))
    {
        direction -= 45;
        speed = 6;
    }
    else
    if keyboard_check(ord("S")) && keyboard_check(ord("A"))
    {
        direction += 135;
        speed = 6;
    }
    else
    if keyboard_check(ord("S")) && keyboard_check(ord("D"))
    {
        direction -= 135;
        speed = 6;
    }
    else
    {
            direction = lookdir;
            speed = 0;
    }
}
end step event:
Code:
direction = lookdir;
draw event:

Code:
///draw camera

d3d_set_projection(x,y,z,cam_x,cam_y,cam_z,0,0,1);
var tex = background_get_texture(bg_sky);
d3d_draw_ellipsoid(x-skydist,y-skydist,z-skydist,x+skydist,y+skydist,z+skydist,tex,1,1,10)
 
huh, i undid moving the bracket in sqrt(power(90,2)) - power(pitch,2) back to how you had it and it suddenly works despite the fact it previously just kept giving me negative number errors
 
Top