Welcome, Guest. Please login or register.
Did you miss your activation email?

Login with username, password and session length

 
Advanced search

1367996 Posts in 64188 Topics- by 56117 Members - Latest Member: Leoyarms

October 19, 2019, 11:03:13 AM

Need hosting? Check out Digital Ocean
(more details in this thread)
TIGSource ForumsDeveloperTechnical (Moderator: ThemsAllTook)Basic 3D 3rd (+ 1st) person camera tutorial
Pages: [1]
Print
Author Topic: Basic 3D 3rd (+ 1st) person camera tutorial  (Read 1182 times)
Prinsessa
Level 10
*****


Ava Skoog


View Profile WWW
« on: January 26, 2019, 06:18:30 AM »

Since the old tutorial forum is gone, I've been told this should go here~

Rather than being an expert on 3D cameras, I've just learned how to do this properly for the first time after spending many days on finding existing resources and learning from those, so the intent of this post is to pass that information on in a distilled format to whomever may find it useful due to being in the same spot as I was just a week or so ago!

Since I do not have a blog of my own I decided just to post it here! Smiley

My own code has been based mainly on this (YT) video tutorial by ThinMatrix and this camera collision script by Unity forums user ChadrickEvans, but does not copy either solution verbatim. I actually recommend watching the video tutorial before reading any further since it visually explains the maths that I'll be providing in text, so might be easier to follow.

The code in this post will be pseudocode based on C-style syntax with as little unnecessary fluff as possible. It's about the fundamental concepts and not tied to any specific framework or engine. I will be treating the y axis as up; if your coördinate system works differently you may have to swap axes or invert numbers.



Overview

Here's what we'll be making (excuse the stray crying babies):



  • Third person camera following a character.
  • Free camera rotation controlled by player.
  • Collision detection, sliding camera out of obstacles.
  • Resetting camera to locked mode behind character.
  • Locked camera rotation not controlled by player.



Following the character

Basic constants

To begin with we will need to know the camera's maximum (default) distance from the character (you'll have to tweak this and see what feels good!) and what point relative to the character should be treated as the pivot of the camera; in your game that might just be the character's position as is, but in my game the character's pivot is at their feet, and so I needed to add a vertical offset off the ground.

Code:
const float _maxdist = 4.0;
const float _offset  = 2.0;

The distance from the character does not refer to any specific axis, but the three-dimensional distance from the pivot on the character (i.e. its position plus the offset), i.e. the length or magnitude of the vector resulting from subtracting the pivot of the character from the position of the camera, as shown by the line drawn between the pivot and the camera in the picture below. The line between the ground and the pivot represents the offset.



Pitch and angle, yaw and roll

We will also be needing two mutable values: the angle by which the camera has been rotated around the character's pivot on the y axis, and the pitch of the camera (how much it is looking up or down on its x axis).

Code:
float _angle = 0.0;
float _pitch = 0.0;

You might not want to default your pitch to zero (since that means the camera is looking straight ahead rather than for example slightly downward) so that's something to play around with. Both of these angles, and indeed all angles throughout this codebase, will be in radians and not in degrees.

The pitch:



The angle:



If you know your terminology you might be wondering why the angle isn't referred to as the yaw—we will be calculating that based on the angle as well, but it's not going to be the same value, as you'll see. But for that reason we will also declare a yaw variable, and while we're at we might also declare a roll (the z axis rotation) even though we won't actually be using it in this tutorial, but at least you'll know where in the code to put it if you do want to play with it as well.

Code:
float _roll = 0.0;
float _yaw; // needs to be calculated so no initial value

Since we don't want to be able to turn the camera upside down, we need to set a pair of minimum and maximum pitch constants. You can use a conversion function if you want to specify these values in degrees, but since we're working with radians anyway, I just played around till I found a pair of values that suited me and used those:

Code:
const float _minpitch = -0.45;
const float _maxpitch = 1.25;

Finally since we want smooth camera movements, we will be using variables to hold the actual current pitch and angle of the camera, whereas the previously declared variables are for the intended final values that the current values are going to be interpolated toward over time.

Code:
float _currpitch = _pitch;
float _currangle = _angle;

Calculating the position

This is where having watched that video by ThinMatrix is really going to pay off!

We want to write a function to calculate the position where the camera should be based on the origin of the target character and the distance from the character. These will be passed in as arguments. The basic outline of the function looks like so:

Code:
vec3 calculatePosition(vec3 origin, float distance)
{
vec3 result;

// … calculate values here

return result;
}

As demonstrated in the video, the calculations will be based around a right-angled triangle formed from the origin of the character and the intended position of the camera:



The length of the diagonal side (or hypotenuse) of this triangle is supposed to be equal to the previously declared maximum or default distance of the camera from the origin. The angle of the triangle is the current pitch of the camera. Based on that we are able to calculate the length of the horizontal and vertical sides of the triangle as follows:

Code:
float hordist = distance * cos(_currpitch);
float verdist = distance * sin(_currpitch);

If you've ever calculated a directional vector using the cosine and sine of the angle, you'll understand why this works. If not, that's a recommended concept for you to look up, but not needed to understand to progress further from here, so let's get on with the calculations!

Now that we have the vertical distance, calculating the final y position is as simple as adding this vertical distance to the origin's y:

Code:
result.y = origin.y + verdist;

Calculating x and z requires us to basically perform the same kind of directional vector calculation yet again but from the top view—this time the horizontal distance from the previous calculation is the length of the hypotenuse, and the angle is the current angle around the target:



As such, x and z are calculated like so:

Code:
result.x = origin.x - hordist * sin(_currangle);
result.z = origin.z - hordist * cos(_currangle);

And so the final function is this:

Code:
vec3 calculatePosition(vec3 origin, float distance)
{
vec3 result;

float hordist = distance * cos(_currpitch);
float verdist = distance * sin(_currpitch);

result.x = origin.x - hordist * sin(_currangle);
result.y = origin.y + verdist;
result.z = origin.z - hordist * cos(_currangle);

return result;
}

So now in our camera update loop we will have to actually set the camera's position based on the values that should be used to calculate it:

Code:
vec3 origin = _targetpos + vec3(0.0, _offset, 0.0);
_position = calculatePosition(origin, _maxdist);

Calculating the rotation

The camera's rotation is based on the current pitch, yaw and roll. We already have the pitch, and the roll like I said before isn't actually really used in this tutorial, so we've just set it to zero. But, like I said earlier, we need to calculate the yaw, which isn't simply equal to the angle around the character. The reason for this is that this angle is pivoting around the character, facing away from it, like so:



This means that if we were to use this angle, the camera would be facing away from the character and not toward. We simply fix this by turning it around 180°, which corresponds to π radians:

Code:
_yaw = PI - _currangle;

We want to calculate our final rotation as a quaternion since that's our actual representation of a rotation, despite working with pitch, yaw and roll separately for the purposes of manipulating our camera. This means we need to use a function to convert Euler angles to a quaternion, something I'm going to assume is present in your maths library of choice, and not implementing in this tutorial. We also need to create a separate quaternion for each axis and multiply them one by one or we will not get the desired result:

Code:
quat a = eulerquat(_currpitch, 0.0, 0.0);
quat b = eulerquat(0.0, _yaw, 0.0);
quat c = eulerquat(0.0, 0.0, _roll);

_rotation = a * b * c;

Interpolating and clamping the values

At the top of our update function we want to linearly interpolate our current pitch and current angle to approach the desired values. We also want to clamp the pitch between the minimum and maximum defined earlier so that we can't turn the camera upside down or below the ground. This is also a good place to put the yaw calculation from before.

Code:
_pitch = clamp(_pitch, _minpitch, _maxpitch);
_currpitch = clerp(_currpitch, _pitch, deltatime * _turnspeed);
_currangle = clerp(_currangle, _angle, deltatime * _turnspeed);

I've introduced a new constant here which is the rate at which the camera will lerp (linearly interpolate) toward the desired pitch and angle, i.e. its turn speed. The deltatime is of course the time since the last frame or update.

The clerp refers to a circular linear interpolation which is different from a regular lerp in that it wraps the radian around, more like a slerp (spherical linear interpolation) but only for a single angle value and not an entire quaternion. As this is not super common to maths libraries, here's an implementation:

Code:
float clerp(float a, float b, float t)
{
float max = PI * 2.0; // or TAU
float da = (b - a) % max;
float length = (da * 2.0) % max - da;
return a + length * t;
}

Note that if you're using C/C++, the built-in modulo operator % doesn't work on floating point values and you'll have to use (std::)fmod() from the C maths library instead.
« Last Edit: March 10, 2019, 07:33:31 AM by Prinsessa » Logged

Prinsessa
Level 10
*****


Ava Skoog


View Profile WWW
« Reply #1 on: January 26, 2019, 06:19:08 AM »

This post got too long for the forum limit! Embarrassed So I'm splitting it up! Here's the rest.



Controlling the camera

This will mostly be left to you to implement as you see fit depending on your input method(s), but I will briefly explain what values you need to transfer from your inputs and how. You'll want to get your motion input on the x axis and on the y axis separately, whether it comes from the mouse or a joystick or the arrows on the keyboard and you'll want these values to be in the range from negative to positive but they need not necessarily be from -1 to 1 as higher values can be suitable for higher sensitivity, especially for mouse movement. Then calculating the camera movement is as easy as this:

Code:
_angle += input.x * deltatime;
_pitch += input.y * deltatime;

You may need to invert one or both of these values to get the desired result. I'm personally dealing with this on a lower level in my code where the input gets processed based on user settings in the first place and so my camera code does not need to deal with this.

Remember to always allow your users to manually configure camera axis inversion because people are used to different configurations and might not be able to play comfortably at all with one or the other—also differentiate between third and first person inversion as people may have different preferences for each of those too in case you're planning on having a first person mode in your game as well!

Note that we are updating the desired angle and pitch values and not updating the current pitch and angle values directly since these will get lerped toward the desired values in the update code that we just wrote.



Handling collision

There are many ways to fine-tune this, but we will be going for a very simple but effective solution to preventing the camera from clipping through walls that's a solid foundation to work from, if not even enough for many games.

All this requires is a way to cast a line or ray from the target character's origin to the camera's position and see if it intersects with any geometry, and at what point the first (closest to the character) intersection happens and calculating a new camera position and distance based on that, bringing the camera closer to the character as necessary.

In some instances this means the camera will actually clip through the character instead—you can either try to tack on more failsafes to prevent this, or you can do something as simple as just fading the character out, which is done even in AAA games such as The Legend of Zelda: Breath of the Wild:



Footage from ~3:26:34 into this gameplay video on YT from channel RabidRetrospectGames.

I've not yet done this in my own game, as you can see in the GIF at the beginning of this post, but I plan on doing so!

Line or ray cast

So the basic premise is this:



We cast a line from the character to the camera. Not the other way around. This is important. We do this so that the first thing that the line intersects with will be the closest to the character, so that we know for sure that if we move the camera there, that's where the character will be visible. If the camera were all the way behind the house and we did it the other way around we would instead have found the wall on the other side of the building, and moving the camera there wouldn't have brought the character back into view!

So for this I'm assuming that you already have some sort of library in place that you're already using to check collisions between the player and the level geometry, and that this library provides a line or ray cast function. The only difference between the two is API: a line cast specifies a start and an end point whereas a ray cast merely specifies the starting point and a direction as well as an optional length. Since we want to be checking between the character's origin and the camera's position I'll be using a line cast between the two.

Code:
Hitinfo info = linecast(origin, _position);

The origin here is the vector we calculated earlier by adding the offset to the target's position. I'm assuming here that your line cast function returns some kind of struct with information about the result, perhaps like so:

Code:
struct Hitinfo
{
bool hit; // did the line cast actually intersect with anything?
vec3 point; // the world position where the intersection happened
}

There might also be things like a normal direction in there, but these are the only two pieces of information we are interested in: did we hit anything at all, and if so where? Then we can simply calculate a new distance for the camera from the origin based on the distance between the origin and this hit point. The naïve calculation would look like so:

Code:
float dist = length(origin - info.point);

However we want to bear some things in mind. First, we probably want to have some offset from the obstacle so that the camera isn't licking the wall and risking clipping on the sides. An additional factor to bear in mind here is the value of your camera's near clip plane as well, but that's outside the scope of this codebase. Let's add another constant:

Code:
const float _hitoffset = 0.25;

Fiddle with this number as you wish. Now we need the directional vector of the line between the hit point or camera and the character's origin so that we can offset in the right direction.

Code:
vec3 dir = vec3(0.0, 0.0, 1.0); // forward vector; you might have a library built-in
dir = _rotation * dir;          // align with the character's facing direction
dir = normalise(dir);

That's it! Multiplying the camera's rotation quaternion by the forward vector turns that vector into a directional vector matching the quaternion. Now we can use this to calculate the actual point:

Code:
vec3 point = hit.point - (dir * _hitoffset);

Then we can update the previous calculation like so:

Code:
float dist = length(origin - point);

Here we are subtracting the hit point position with the added offset from the origin position and getting the length, also known as magnitude depending on your maths library, of the resulting vector.

Finally we also want to clamp the distance to make sure it's between zero and the permitted maximum as defined by our constant:

Code:
dist = clamp(dist, 0.0, _maxdist);

Then we can recalculate the position using this distance instead of the maximum distance we defined earlier:

Code:
_position = calculatePosition(origin, dist);

Since we drew the line between the origin and the camera we know that the direction of the vector is equal to the one that the camera is already pointing, and so what this code effectively does is to just kind of slide the camera in its own forward direction along this line closer toward the character.

Smoothing the movement

Now, this might get choppy if the camera just warps from one distance to the other and we might want to lerp this as well. I've found that it's best to warp directly if the new distance is shorter than the old one because otherwise we risk seeing the camera clipped through the obstacle for a moment, whereas I certainly do prefer a lerp when the new distance is longer since that means the camera now has room to comfortably back up again with nothing in the way.

This means that the we, much like we differentiate between current and desired angle or pitch, now need to also have a variable for the current distance that can be lerped toward the desired one:

Code:
float _currdist = _maxdist;

The old position calculation (not the one we just performed based on the distance to the line cast hit point, but the one at the beginning of this tutorial) thus needs to be updated to use the current instead of the maximum distance:

Code:
_position = calculatePosition(origin, _currdist);

And then of course we need to actually do the lerping somewhere. If we've detected a hit we want to lerp toward the new distance if it is greater than the old one, but otherwise warp directly to it. If we did not detect a hit at all we want to lerp toward the maximum distance.

Putting it all together

So all in all the position calculation logic, taking into account line casting and lerping, would look something like this:

Code:
Hitinfo info = linecast(origin, _position);

if (info.hit)
{
vec3 dir = vec3(0.0, 0.0, 1.0);
dir = _rotation * dir;
dir = normalise(dir);

vec3 point = hit.point - (dir * _hitoffset);

float dist = length(origin - point);

if (dist < _currdist)
_currdist = dist;
else
_currdist = lerp(_currdist, dist, deltatime * _movespeed);
}
else
_currdist = lerp(_currdist, _maxdist, deltatime * _movespeed);

_position = calculatePosition(origin, _currdist);

I've introduced yet another constant here for the speed of this movement along the line between the character and the camera that you can fiddle with to get your desired results. Of course you're probably also going to want to filter somehow what geometry the line should actually detect so the camera doesn't try to avoid minor things that aren't actually really in the way, but that's very API-specific, so I left this out here.



Resetting the camera

If you've ever played a 3D Zelda game, you'll know that they let you use something like a shoulder button (or the Z trigger on a Nintendo 64 controller) to reset the camera behind Link. This is also done in my game as evidenced by the GIF at the top. In fact, this is the only way to control the camera in some of these games, as many of them happen to be on consoles that lack a secondary stick. Even when you have one, I think it's a nice feature to have, so let's look at this too!



Footage from ~13:43 into this gameplay video on YT from channel TheRealSonicFan.

Calculating the angle

This is fairly easy. We need to calculate the angle that the camera should have around the character according to their current facing direction in order to end up behind their back. We also need to reset the pitch to whatever default value you decided on earlier. Let's whip up a function for this, like we did for calculating position. This time we want to pass in the rotation of the character as a quaternion.

Code:
float calculateAngle(quat targetrot)
{
vec3 dir = vec3(0.0, 0.0, 1.0);

dir   = targetrot * dir;
dir.y = 0.0;                // we do not want to use this axis
dir   = normalise(dir);

return atan2(dir.x, dir.z); // turn direction vector into a radian angle
}

The calculation is much the same as for calculating the direction for the hit point offset before, but this time we don't care about the y axis, and then using atan2 on the resulting vector gives us the angle. We need to use a 3D vector initially since the quaternion is 3D, but since we're really only calculating an angle for a 2D top view rotation, we end up only using the x and z axes after that, effectively a 2D vector.

Now all you need to do, when the user gives you the input you want to assign to this action, is to set the desired angle (not the current angle—we still want to make use of our nice clerp from before to make the current angle interpolate nicely to the new angle, not just warp there) to the result of this calculation, and likewise reset the desired (not current) pitch to the default value.

Code:
_angle = calculateAngle(_targetrot);
_pitch = _startpitch;



Free and locked mode

Once again I shall bring up an example from the Zelda series. You may or may not want this functionality!

While Breath of the Wild relies entirely on the player to manually control the camera using the secondary stick and games like Ocarina of Time rely on a combination of resetting and also automatically following the turning motions of character, The Wind Waker offers both: if the player manually takes control, it works like BotW, but if they reset the camera it starts working like OoT instead, somewhat following the motion of the character.

I personally like this a lot, so it's what I've done for my game as you can see in the GIF at the top—let's look at this as well as the grand finale!



Footage from ~1:26 into this gameplay video on YT from channel GuffyTheWeird.

Here the camera is in locked or automatic mode, and as you can see it follows Link as he turns to an extent, but not if he does a full 180 and starts running toward the camera. I've achieved something similar in my game, so let's have a peek at the code!

Swapping states

So we already have two important mechanisms in place by now: free camera control, and camera resetting. What we want now is a variable that controls whether the camera is in free mode or not, and to enter free mode when the player turns the camera and exit free mode when they reset it. So we add a boolean for this, starting out in locked mode:

Code:
bool _free = false;

After updating our input management code to modify this variable, we can add to our update function a section to make the camera follow the player's heading to the desired extent with some more maths.

Code:
if (!_free)
{
// … calculations go in here
}

First we need the heading direction of the camera as well as that of the character. We calculate these just like we did before in the angle calculation function but stop once we've got to the directional vectors since we don't need the radian angles for this:

Code:
// camera direction

vec3 dir = vec3(0.0, 0.0, 1.0);

dir   = _rotation * dir;
dir.y = 0.0;
dir   = normalise(dir);

// character direction

vec3 targetdir = vec3(0.0, 0.0, 1.0);

targetdir   = _targetrot * targetdir;
targetdir.y = 0.0;
targetdir   = normalise(targetdir);

Now we want the dot product between these two directional vectors to calculate how parallel or perpendicular they are to each other. If you're completely clueless on dot products, this page provides an excellent visual guide to how it works. Basically, if they are more or less parallel, we know the camera is looking at the character's back or their face and we don't want to turn the camera, but if they're more perpendicular (forming a right angle) we are looking at the character's side, so they are turning to the left or the right relative to the camera.

Parallel:



Nearly perpendicular:



Code:
float dp = dot(targetdir, dir);

Now comes some magic number fiddling again. We need to check whether the resulting dot product is within a certain range of acceptable (non-)perpendicularity where the camera will actually turn with the character and only do so if this is the case. The dot product ranges from -1.0 to 1.0 and for my purposes I found the range -1.0 to 0.25 to be suitable. Adjusted for sign, this means that 1.25 is the size of the total range. I chose to do the maths like so:

Code:
float range = 1.25;
if (dp < (range - 1.0))
_angle -= eulervec(inverse(_rotation) * _targetrot).y * deltatime;

The Euler vector function turns a quaternion into a 3D vector containing the x, y and z angles separately as radians and is something I expect you to have in your maths library.

The quaternion in question is the result of multiplying the inverse rotation quaternion of the camera by the rotation of the character in order to adjust for the camera's rotation relative to the character and get a more absolute rotation for the character. After this we are only interested in the y axis rotation angle which will be a negative or positive number depending on which way (their left or right) the character is turning relative to the camera.

You might want to normalise this into an absolute sign that's either -1.0 or 1.0 but having the angle scale with the intensity of the rotation seemed suitable to me, so I didn't.

Finally we multiply this by the deltatime for smooth goodness and update our desired angle by subtracting the result of this whole expression.

Detecting motion

But wait! We forgot something in the last if statement. It gets very annoying if the camera always keeps turning even if the player isn't doing anything, so we want to make it only do so if the character is actually moving. We do this simply by keeping track of their position from the last frame and comparing the magnitude of their difference on the x and z axes (like so many times before, we don't care about the y axis here). So we need a persistent variable for this:

Code:
vec3 _targetposOld;

Then at the very end of each update step we need to refresh it with the current position:

Code:
_targetposOld = _targetpos;

Now we can calculate the difference like so:

Code:
vec3 a = _targetpos;
vec3 b = _targetposOld;

a.y = 0.0;
b.y = 0.0;

float diff = length(a - b);

And then update our if statement something like the following:

Code:
if (dp < (range - 1.0) && diff > (epsilon * deltatime))

Epsilon is a magic small number of your choosing to determine when the character's movement is no longer significant; I went for 0.01. Multiplied by deltatime to make it consistent regardless of frame rate.
« Last Edit: January 26, 2019, 09:21:12 AM by Prinsessa » Logged

Prinsessa
Level 10
*****


Ava Skoog


View Profile WWW
« Reply #2 on: January 26, 2019, 06:19:24 AM »

Too long still! Final part now.



Just give me the source already!!

Perhaps you don't care all too much how this actually works (which is perfectly valid; sometimes you just want to get on with your dang game!) or you find it difficult to fully grasp everything when it's scattered throughout the post and not in one coherent place. So I'll leave you here with the entire source code in one big chunk! I hope you enjoyed the tutorial and found it useful! Gomez

The code here is written basically as if it were all in some global namespace, but depending on your language and conventions you might want to move this into something like a class. I'm assuming there to be external mechanisms to invoke the input callbacks and the update function, and I do not attempt to explain where the information about the target character's position and rotation variables come from; you'll need to plug those in as you see fit~

Code:
// you need to tune all these constants to suit your game!

const float _maxdist    = 4.0;
const float _offset     = 2.0;
const float _minpitch   = -0.45;
const float _maxpitch   = 1.25;
const float _turnspeed  = 10.0;
const float _movespeed  = 2.5;
const float _startpitch = 0.0;
const float _hitoffset  = 0.25;

float _angle = 0.0;
float _pitch = _startpitch;
float _roll  = 0.0;
float _yaw;

float _currpitch = _pitch;
float _currangle = _angle;
float _currdist  = _maxdist;

bool _free = false;

// init not included in tutorial, but to (re)set default values

void init()
{
_targetposOld = _targetpos;
_currdist     = _maxdist;
_currpitch    = _startpitch;

// set these both to the same value

_currangle =
_angle     = calculateAngle(_targetrot);
}

void update(float deltatime)
{
// calculate origin relative to character posititon

vec3 origin = _targetpos + vec3(0.0, _offset, 0.0);

// interpolate the changing values

_pitch = clamp(_pitch, _minpitch, _maxpitch);
_currpitch = clerp(_currpitch, _pitch, deltatime * _turnspeed);
_currangle = clerp(_currangle, _angle, deltatime * _turnspeed);

// calculate yaw based on angle

_yaw = PI - _currangle;

// automatic camera movement

if (!_free)
{
// camera direction

vec3 dir = vec3(0.0, 0.0, 1.0);

dir   = _rotation * dir;
dir.y = 0.0;
dir   = normalise(dir);

// character direction

vec3 targetdir = vec3(0.0, 0.0, 1.0);

targetdir   = _targetrot * targetdir;
targetdir.y = 0.0;
targetdir   = normalise(targetdir);

// perpendicularity between the two

float dp = dot(targetdir, dir);

// how much did the character move?

vec3 a = _targetpos;
vec3 b = _targetposOld;

a.y = 0.0;
b.y = 0.0;

float diff = length(a - b);

// turn with the character

float range = 1.25;
float epsilon = 0.01;
if (dp < (range - 1.0) && diff > (epsilon * deltatime))
_angle -= eulervec(inverse(_rotation) * _targetrot).y * deltatime;
}

// collision detection

Hitinfo info = linecast(origin, _position);

if (info.hit)
{
vec3 dir = vec3(0.0, 0.0, 1.0);
dir = _rotation * dir;
dir = normalise(dir);

vec3 point = hit.point - (dir * _hitoffset);

float dist = length(origin - point);

if (dist < _currdist)
_currdist = dist;
else
_currdist = lerp(_currdist, dist, deltatime * _movespeed);
}
else
_currdist = lerp(_currdist, _maxdist, deltatime * _movespeed);

_position = calculatePosition(origin, _currdist);

// update rotation

quat a = eulerquat(_currpitch, 0.0, 0.0);
quat b = eulerquat(0.0, _yaw, 0.0);
quat c = eulerquat(0.0, 0.0, _roll);

_rotation = a * b * c;

// keep track of character movement

_targetposOld = _targetpos;
}

void onInputForMovement(float x, float y, float deltatime)
{
_angle += x * deltatime;
_pitch += y * deltatime;

_free = true;
}

void onInputForReset()
{
_angle = calculateAngle(_targetrot);
_pitch = _startpitch;

_free = false;
}

vec3 calculatePosition(vec3 origin, float distance)
{
vec3 result;

float hordist = distance * cos(_currpitch);
float verdist = distance * sin(_currpitch);

result.x = origin.x - hordist * sin(_currangle);
result.y = origin.y + verdist;
result.z = origin.z - hordist * cos(_currangle);

return result;
}

float calculateAngle(quat targetrot)
{
vec3 dir = vec3(0.0, 0.0, 1.0);

dir   = targetrot * dir;
dir.y = 0.0;                // we do not want to use this axis
dir   = normalise(dir);

return atan2(dir.x, dir.z); // turn direction vector into a radian angle
}

float clerp(float a, float b, float t)
{
float max = PI * 2.0; // or TAU
float da = (b - a) % max;
float length = (da * 2.0) % max - da;
return a + length * t;
}

Wow! Lengthy tutorial to explain everything, but all in all the code turns out to be quite concise! A simple but effective base for you to build on top of. And me, too!



Further reading/watching

There is also this article on Gamasutra by Yoann Pignole—I haven't really made use of the additional techniques described here yet and so they were not covered here, but it's still an interesting article for those interested in going further!

For more video material, there is this GDC talk on camera mistakes by John Nesky who worked on Journey.
« Last Edit: January 26, 2019, 07:22:05 AM by Prinsessa » Logged

Prinsessa
Level 10
*****


Ava Skoog


View Profile WWW
« Reply #3 on: January 26, 2019, 07:24:03 AM »

Read through the whole thing and found some typos, including in the code—I think it should be good now! Hand Thumbs Up Right

Also, small addition…



Addendum: controlling the character

Completely forgot about this, and I'll make it brief, but I suppose that it's also worth saying a few words about integrating the character controller with the camera, especially if you've never done it before.

Since the camera can be looking at the character from almost any angle in 3D space in a sphere around them, transforming player input on two axes isn't as easy as mapping the two directions to the character's x and z positions—however, it's almost that easy!

All you really need to do is to multiply the camera's rotation quaternion by the input direction vector, much like we've seen quaternions multiplied by several directional vectors in the camera code as well in the above tutorial. Then as usual you need to throw away the y axis and normalise the vector, and you're good to go!

Code:
vec3 movement = vec3(input.x, 0.0, input.y);

movement   = _camrot * movement;
movement.y = 0.0;
movement   = normalise(movement);

_direction = vec2(movement.x, movement.z);

There you have it!



Addendum 2: adding a first person mode

I just added a first person camera mode to my game featuring the third person mode described here, and learnt that it required very few additions to the previous code, so I thought I'd cover that here as well!



As you can see here, the first person mode matches the camera angle and pitch of the third person mode rather than turning around to match the facing direction of the character; this is somewhat odd, but a lot less disorienting, and how modern games tend to do it. I'm thinking instead I might add a short animation of the character turning toward the facing direction of the camera before swapping modes.

Variables and constants

To begin with, we'll need a variable to keep track of the current mode. It could be a fancy enumeration with named values, but I'll just use a boolean that decides whether to use first person mode or not.

Code:
bool _first = false;

We also probably want a different vertical offset from the character's origin than the one specified for third person mode by _offset that matches the position of the character's eyes, or in the case of my game, their polaroid camera used to take photographs in first person mode:

Code:
float _offsetFirst = 1.75;

Separate input

We also want to handle the camera control (whether using a mouse or a joystick or otherwise) separately from the third person mode, because a lot of people need different inversion settings for first and third person depending on what they are used to and might not be able to comfortably control the camera otherwise.

Again this will depend on how exactly you deal with input, and perhaps you will have already applied inversion settings to your input direction by the time it is passed into the camera input calculations, but otherwise the code remains the same as for third person:

Code:
_angle += input.x * deltatime;
_pitch += input.y * deltatime;

Additionally you will of course also have to decide on an input that tells the camera to alternate between the first and third person modes. For the simple implementation covered here, you need only change the value of the _first boolean to do so.

Updating the main loop

Now we need to jump into the update() function that for the third person code deals with all of the per-frame calculations such as interpolation, line casting and finally setting the new rotation and position of the camera.

We will wrap an else block around everything starting from if (!_free) down to but not including the code near the bottom of the function that updates the rotation. This will be preceded by the following if clause:

Code:
if (_first)
_position = _targetpos + vec3(0.0, _offsetFirst, 0.0);

That's it! For first person we really only need to glue the camera to the character's head and deal with none of the distance or collision stuff now wrapped up in that else we just added. The character's own collider that prevents them from walking through walls will also prevent the camera from doing so. The same camera and character controllers used for third person, as well as the pitch limit, will still work just fine, possibly adjusted for those differing inversion preferences.

Hiding the character

Finally, since the camera now probably is inside of your character model, you probably want do deäctivate rendering them when going into first person mode, so that their geometry doesn't get in the way, or perhaps switch to a version without the head if you still want the player to be able to look down on their body. Up to you!
« Last Edit: March 10, 2019, 05:50:35 AM by Prinsessa » Logged

ProgramGamer
Administrator
Level 10
******


The programmer of games


View Profile
« Reply #4 on: January 26, 2019, 08:23:40 AM »

Impressive writeup!
Logged

Prinsessa
Level 10
*****


Ava Skoog


View Profile WWW
« Reply #5 on: January 26, 2019, 08:48:29 AM »

Thank you! <3 Slapped on a little addendum in the post above, too!

EDIT: oops, just noticed I've been writing 'perpendicular' where I meant 'parallel', fixed!
« Last Edit: January 26, 2019, 09:22:36 AM by Prinsessa » Logged

subliminalman
Level 0
**



View Profile WWW
« Reply #6 on: January 31, 2019, 01:01:15 PM »

Ah this is such an awesome camera tutorial! [_]<|  Cave Story
Logged

Prinsessa
Level 10
*****


Ava Skoog


View Profile WWW
« Reply #7 on: January 31, 2019, 01:07:11 PM »

Ohhh, heyy, I didn't even know you're on TIGS! You even have a Button City devlog on here! Blink Thank you!! <3
Logged

Prinsessa
Level 10
*****


Ava Skoog


View Profile WWW
« Reply #8 on: March 10, 2019, 02:48:09 AM »

Addendum for first person mode added above!
Logged

Pages: [1]
Print
Jump to:  

Theme orange-lt created by panic