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:
D

Dawn DeerButt

Guest
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 by a moderator:
D

Dawn DeerButt

Guest
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)
 
D

Dawn DeerButt

Guest
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
 

wSanity

Member
thanks, I was using window_set and window_get funcyions instead but after changing them it made it work how I wanted
 
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
The exact same thing just happened to me, as in I put it back to <var f_x = sqrt(power(91,2) - power(pitch,2));> and I stopped getting the negative value crash.
 

Evanski

Raccoon Lord
Forum Staff
Moderator
This tutorial is from 2018 with the user having been deleted

You may want to look at an up to date tutorial
 
This tutorial is from 2018 with the user having been deleted

You may want to look at an up to date tutorial
Thanks, yeah this is the series I originally followed, and it's fantastic, but the camera can't look fully up or down, and for the game I'm making I need that.
 

Evanski

Raccoon Lord
Forum Staff
Moderator
Thanks, yeah this is the series I originally followed, and it's fantastic, but the camera can't look fully up or down, and for the game I'm making I need that.
so just edit the clamp on the camera pitch to allow the full up or down angles
 

Evanski

Raccoon Lord
Forum Staff
Moderator
Yes, but 90 is not directly up in this case.
Its like 0 and 360 being the same thing
when you get to 90, it will flip rotation as you've completed the angles, you're playing by gamemakers logic of 0 being right (forward in GM3D) 90 being up (up/down in GM3D pitch)
 

kburkhart84

Firehammer Games
I don't know how the rotation is happening, but you should look up "Gimbal Lock" and make sure that this is not related to what is happening to you.
 
Its like 0 and 360 being the same thing
when you get to 90, it will flip rotation as you've completed the angles, you're playing by gamemakers logic of 0 being right (forward in GM3D) 90 being up (up/down in GM3D pitch)
I understand this, but in the actual game, when I get to 90 or -90, it is not looking straight up or down, even though the look pitch says it is. If you read that first post it explains it. If you use dragonite spam's first person controller code, and multiply the zto:
GML:
zto = zfrom - (dsin(o_Player.look_pitch));
to
GML:
zto = zfrom - (dsin(o_Player.look_pitch)) * 50;
This will let you look straight up or down, but the sensitivity is all wrong, as explained in OPs post and video.
 
Top