Hey y’all!
It’s the peak of summer, so you should be wearing sunglasses whenever you’re in direct sunlight! Yep, that’s right, it’s National Sunglasses Day, I’ve already started:
Wanna hear a story? That was a rhetorical question because you’re going to have to listen anyways. So, I was going to post this tutorial yesterday June 26 because it was National Chocolate Pudding Day, but I was so busy that I started this tutorial in the morning, and couldn’t even finish near midnight. It was like 11 PM (1 hour from the day changing!) and I had only half the tutorial done. It was like so bad and like—
Oh, yeah I’m supposed to be doing a tutorial…
Introduction
The normal Roblox camera is not the most interesting sight, it just stays there in one place until you zoom in or out (or drag around). Not very realistic, is it?
Speaking of realism, when you walk in real life, your head bobs up and down and causes your vision to do the same. We’re so used to it since it happens every day, but it’s there. Also, when you quickly turn your head, there’s a quick blur between the initial position and the next one.
Well, that’s what we’ll add to our little camera friend:
- a camera bobbing system
- dynamic blur
- and sprinting
Please note: this is best suited for first-person camera. Although it still functions with third person, it’s not preferred.
Video
As you guys requested below, I have finally been able to create a non-laggy video (it was a hassle to go through, but you don’t need to know). Just make sure you watch it in full screen so that you can see me chatting:
Several things to note about the video that I didn’t mention in it:
- look at the wall as reference to notice the bobbing
- the wall may look like it’s shaking but that’s not because of the camera, it’s because it’s unanchored
- I had to exaggerate the dynamic blur for it to be noticeable (and it’s still very subtle)
- the default footsteps sound will not match up with the bobbing (you can implement your own footsteps for that)
Quick Tips
-
With effects like these, you’ve got to be careful not to make them too extreme. For example, the bobbing shouldn’t be a 10 stud difference; that can easily cause players to leave your game.
-
Always consider UX and user preferences. Add a settings menu (which is part of the kit provided way below, but won’t be going in-depth with it) that can toggle each of the three effects on or off.
-
Among the three, camera bobbing is the most difficult to get right. There are many configurations of it, each resulting in different “bobs”. This tutorial will only show one type, so you may change the conditions to your desire. When testing this out, don’t be overly attentive on small details like the bobbing being too noticeable. Chances are, the more you try to notice something, the more it becomes emphasized to you. So, take breaks while developing this and look naturally through the eyes of a player.
Prerequisites
This is supposed to be realistic, and therefore will use come next-level fundamental concepts, so you’ll need to understand these topics:
- some trignomonetry (check out my tutorial)
- some Vector Mathematics (read this tutorial)
- Quadratic Bézier Curves (see below)
- the basics of Perlin Noise (see below)
Quadratic Bézier Curves
When tweening in a curved motion, it’s not the traditional way of plugging in the end position as it will just tween linearly.
You may recall that a function such as f(x) = x
has exactly one output for each input, the same goes for curves like parabolas, hyperbolas, and yes our dear Bézier Curves. Any value you plug in should result in another value being returned. There are many types of Bézier Curves, but we’ll be using the quadratic one that looks like a U.
How a Bézier Curve is Created
Linear interpolation is simply going from one point to another with a specified number. This number (conventionally called “time”) is between 0-1 inclusive. It indicates how far you’re from the start point to the end point proportionally.
If you want to go 1/4 of the way from the first to the next point, you’ll have to use 0.25 as the “time”.
Sound a lot like a linear function, right? It’s exactly that: plug in an input value to get an output value. It’s different in the way that the domain (input) is not infinite, but rather restricted from 0-1. But, we’ll need a formula for this.
To represent the segment between the two points (not the entire infinite line), we have to subtract the first point from the second:
segment = point2 - point1 --order matters!
Now to get the proportion of the segment, you can simply multiply the segment by the “time”. So, putting 0.25 will return 1/4 of the segment.
number = 0.25
proportion = segment * number --1/4 of the segment
Now, to actually get an absolute position between the two points, we need to add the first point to this equation. That makes sense because 0.25 would be travelling 1/4 the distance from the first point.
point3 = point1 + proportion
--[[entire equation
B is in between A and C
t is the "time" value (the input)
]]
B = A + t * (C - A)
But there is the conventional way to put this:
B = A + t * (C - A)
B = A + tC - tA
B = A - tA + tC
B = (1 - t)A + tC --final; just remember the first point comes first with the (1 - t)
So, that’s the formula for linear interpolation. How do Bézier Curves use this, you may ask? Well, if you look at this diagram, all a Bézier Curve is is a set of points derived from three linear interpolations:
Image from the DevHub.
There is the left green point being interpolated between P0 and P1, then there’s the right green point going from P1 to P2. And then, there is the black point interpolating between the two green points, creating a curve.
P0 and P2 are endpoints of the curve, while P1 actually shapes the curve. If P1 was exactly between P0 and P2, then it’d just be a line.
See the “t” value increasing at the bottom? That’s the input. This is the value that drives the three interpolations. As the two green points reach the middle, so does the black point on the green line. Pretty interesting, but there is an equation you must know to use this as a function.
It may seem complicated at first, so I’m going to write it step by step.
--[[Bezier Curves are just three linear interpolations
I'll call the left and right green points L1 and L2, respectively;
and the black point as L3.
]]
L1 = (1 - t)P0 + tP1 --from P0 to P1
L2 = (1 - t)P1+ tP2 --from P1 to P2
L3 = (1 - t)L1+ tL2 --from the Left Green Point to the Right Green Point
If you notice, we can actually shorten that down to one line by substituting for L1 and L2, and then simplifying:
It’s a little messy to put it into Lua, however. (I’m using extra parentheses just in case).
local function curve(t, p0, p1, p2)
--skip the extra calculations if t is 0 or 1
if t == 0 then return p0 elseif t == 1 then return p2 end
return ((1 - t)^2 * p0) + (2 * (1 - t) * t * p1) + (t^2 * p2)
end
Perlin Noise
The loudest mathematical function:
Perlin Noise
Perlin Noise is a way to get random numbers as an output, but still having a relation to the previous result. Take a totally random function (math.random(x, y)
), which can be ridiculously inconsistent.
Say the number of uses of the function is the X-axis and the result is the Y-axis. Its graph can look very jagged and sharp:
There is no relation between one point and another point, they’re purely random.
But, Perlin Noise; huh, that puts random noise to shame! Because the output values are related to the previous one in some way, it’s partially random, but also not at the same time. Its graph is quite smooth:
Imagine a person walking in a 2D plane and the graph is their trail. Naturally, no one abruptly changes their path, so their new direction is usually close to the previous direction.
Perlin Noise is useful for procedural terrain generation that can have smooth, randomly generated terrain or it can be used to script an AI to walk in semi-random directions.
math.noise(x, y, z)
This is the Lua implementation of Perlin Noise; it takes in up to three input numbers and spits out a value. This value is just a single number “x” where -1 < x < 1. Only one argument is required (“X”), the others assumed to be zero if left out.
But, what does it mean exactly? How does this function spit out a value between - 1 and 1 by taking in up to three values? Well, I’ve tried to learn how Perlin Noise is calculated, and from what I understand, it uses vectors (“gradient vectors” and “distance vectors”) and takes their dot product (these are also restricted between -1 and 1). Depending on the number of arguments you put in, it either uses a 1D, 2D, or 3D grid to place the vectors in.
I won’t be going too much in-depth with how it calculates the result (as I’m still trying to get my head around it). However, you don’t have to know the inner workings of it to get its general behavior. In this tutorial, we’ll only provide one argument.
Several behaviors for 1D Perlin Noise:
- The arguments aren’t like the traditional
min
andmax
ofmath.random()
, but instead, just any value to get back a single result between -1 and 1. - Inputting an integer such as 9 will always result in 0.
- A cool thing is that inputting the same number(s) will always, always result in the same output (thus, controlled randomness).
- The greater the increment between the last “X” value and the current one, the more random the results can be; they’ll usually be farther apart as well. Go back up to the graph of Perlin Noise and you’ll see that moving horizontally just ever so slightly will result in a small change in the output value, and moving a lot will return a farther number.
So, how do we use this in Lua, you may ask? Well, to get different results, you’ll need to input different numbers. Remember that you should try to avoid using integers, but in the end, it’s inevitable. Here’s what I mean:
local inc = 0.05 --increment
local counter = 1 + inc --you can also just start at inc, but I prefer to do this
0.05, or any other consistent increment, will eventually make the counter be an integer, thus resulting in a 0. But, that’s fine as long as it’s not too frequent.
Now, to actually use the function over and over again, we’ll need to use a loop or a function that runs repeatedly like RenderStepped
:
game.RunService.RenderStepped:Connect(function()
print(math.noise(counter))
counter = counter + inc
end)
That’s basically how we will use Perlin Noise in this tutorial, nothing too complicated.
Uses of Realistic Camera
There are perfect times when you can use camera bobbing, dynamic blur, and sprinting. For example:
- horror games
- adventure games
- exploration games
Structure
Basically, this is what the explorer should look like if you want to follow the tutorial. The selected objects are required, and you can disregard the rest:
Because they look the same, I want to clarify: “Sine” is a NumberValue and “IsSprinting” is a BoolValue.
You can just ignore “run” if you want. It’s basically a controlled way of disabling the scripts while not actually setting their Disabled property to true; it’ll turn off various parts of the script. Why? Because of the settings UI I mentioned earlier. At the end of the tutorial, there’ll be a whole kit with the UI, so these “run” values will be there, too. Plus, there’s not much to learn about it and we’re not covering the UI aspect of it today, sadly.
Dynamic Blur
~38 LINES
Dynamic blur is the easiest to create and the shortest in terms of scripting, so this will be first on the list!
PLAN OF ACTION We’ll be tweening the blur based on how much the camera turns. To check in a loop, we’ll use RenderStepped as a reliable and performant method.
Start off with variables:
local rs, ts = game:GetService("RunService"), game:GetService("TweenService")
local blur, camera = game.Lighting:WaitForChild("Blur"), workspace.CurrentCamera
--we'll use these two to make the loop run less frequent (if counter % dampener == 0 then)
local counter, dampener = 1, 5
--[[the faster they turn, the more blurry it gets; so we'll be comparing the
previous look vector of the camera to the current one (dot product), and depending
on the result, the blur will be applied (you'll see later on)
]]
local prevLookVector = camera.CFrame.LookVector
--[[allowedRange is in degrees & maxBlur is in terms of the Blur object's Size property;
allowedRange will be an angle in which the blur will not initiate, anything beyond
will, however
]]
local allowedRange, maxBlur = 10, 25
local ti = TweenInfo.new(0.25, Enum.EasingStyle.Sine, Enum.EasingDirection.InOut)
--we'll create the tweens later as their values will change
I just used degrees for allowedRange
because’s it’s better to visualize and get exact than just a decimal value between -1 and 1 (dot product range).
Next, we’ll need a small function to return the dot product based on the allowedRange
degrees. There is a formula for calculating the dot product this way:
A ⋅ B = |A| * |B| * cos(theta)
But, remember: look vectors are always unit vectors, and thus putting in 1 as A and B is redundant. So, it’ll simply be the cosine of theta:
--Lua trig only accepts radians, so picky geez!
local function degreesToDot() return math.cos(math.rad(allowedRange)) end
Now, onto the last part: the RenderStepped function:
rs.RenderStepped:Connect(function()
if counter % dampener == 0 then --only run every so often
--remember: order doesn't matter for the dot product
local dot = prevLookVector:Dot(camera.CFrame.LookVector)
--[[anything within 10 degrees of the previous one is not blur-worthy!
also, 10 degrees will be ~0.98, the closer the dot is to 1, the closer
the look vector is to the previous one; we want the dot product to be
higher than 0.98]]
if dot >= degreesToDot() then
--just fade out the blur; won't be noticeable if already 0
ts:Create(blur, ti, {Size = 0}):Play()
--outside of the allowedRange
elseif dot < degreesToDot() then
--[[you don't need to use this specific equation; just make the blur
related to the dot product in some reasonable way]]
local equation = (1 - dot) * (maxBlur / 2)
ts:Create(blur, ti, {Size = equation}):Play()
end
end
prevLookVector = camera.CFrame.LookVector --update for the next cycle
counter = counter + 1 --keep the modulus working
end)
With the equation I have, the blur never reaches the maxBlur
value of 25 I defined, I thought 25 was a decent amount and not too crazy. Remember: don’t overdo!
Dynamic blur is completed!
Sprint
~72 LINES
This one has a ton of variables, only because of the number of tweens there are!
PLAN OF ACTION Upon pressing the desired key (“LeftShift”), the player’s FOV and walk speed will tween up. However, to make it realistic, we won’t allow the player to sprint backwards, for instance (dot product again!). So, while sprinting, if you do try to run in such a way, their FOV and walk speed will reset (same goes for when the key is released). Just in case if they already have the key pressed before running, we’ll use RenderStepped again (as a loop) and :IsKeyPressed()
to see if the key is being held down.
Variable time! (remember, I won’t repeat variables like the TweenService)
local player = game.Players.LocalPlayer
local uis = game:GetService("UserInputService")
local char = script.Parent
local hum = char:WaitForChild("Humanoid")
local hrp = hum.RootPart
local normalFOV, sprintFOV = 70, 100 --self-explanatory
--[[you can use absolute numbers if you want; I set default speed to 5 as slower
speeds are more suited for camera bobbing]]
local normalSpeed, sprintSpeed = hum.WalkSpeed, hum.WalkSpeed + 5
--I'm obssed with Sine
local ti = TweenInfo.new(0.5, Enum.EasingStyle.Sine, Enum.EasingDirection.Out)
local normalTween = ts:Create(camera, ti, {FieldOfView = normalFOV})
local sprintTween = ts:Create(camera, ti, {FieldOfView = sprintFOV})
local playing = Enum.PlaybackState.Playing --shorten it
--accelerate/decelerate humanoid walk speed (no one abruptly goes from 0 to 70mph)
local normalTweenHum = ts:Create(hum, ti, {WalkSpeed = normalSpeed})
local sprintTweenHum = ts:Create(hum, ti, {WalkSpeed = sprintSpeed})
local allowedRange = 45 --degrees; allow sprinting only inside this angle
local key = "LeftShift" --easy to change later on
--we'll need our friend again to convert degrees into the dot product
local function degreesToDot() return math.cos(math.rad(allowedRange)) end
I created a function for this simply to keep it from overflowing horizontally, but it’s not needed:
local function controlledTween(tween)
--to keep the tween from playing again and interrupting itself over and over again
if tween.PlaybackState ~= playing then tween:Play() end
end
Now, this isn’t very long at all, it just looks so massive because of all them tweens!
rs.RenderStepped:Connect(function()
if uis:IsKeyDown(Enum.KeyCode[key]) then --stays true until released
--[[check if within allowedRange; remember, when they walk sideways
their move direction is going to be more different than their camera look vector
(only for first-person)]]
if hum.MoveDirection:Dot(hrp.CFrame.LookVector) >= degreesToDot() then
isSprinting.Value = true --set this to true for the bobbing script to see
controlledTween(sprintTweenHum) --walk speed
controlledTween(sprintTween) --FOV
end
else --if it's released and not pressed down
isSprinting.Value = false
controlledTween(normalTweenHum) --walk speed
controlledTween(normalTween) --FOV
end
--if players try to go beyond 45 degree angle, they won't be sprinting no more!
if hum.MoveDirection:Dot(hrp.CFrame.LookVector) < degreesToDot() then
isSprinting.Value = false
controlledTween(normalTweenHum) --walk pseed
controlledTween(normalTween) --FOV
end
end)
And sprinting’s all done! Now, onto the final part of this:
Bob
~87 LINES
This will be the main script in the way that it actually creates something more noticeable and prominent: view bobbing.
PLAN OF ACTION Using Bézier Curves, we’ll create a U shaped path for the camera to travel back and forth on. To keep the motion from looking too linear, we’ll use TweenService to apply an EasingStyle to it. Also, to add a bit of randomness (no one’s perfect in real life), the middle point of the curve will use Perlin Noise and move to semi-random spots. Then, whenever the player stops running, the camera will tween back to the original position.
This one has the most variables, without a doubt!
--shortening purposes
local playing, completed = Enum.PlaybackState.Playing, Enum.PlaybackState.Completed
local style, dir = Enum.EasingStyle.Sine, Enum.EasingDirection.InOut
local sin = script:WaitForChild("Sine") --remember, the NumberValue; it will act as the "time" for our curve
local isSprinting = char.Sprint:WaitForChild("IsSprinting") --aah, finally it came in use
local dur, durSprint = 0.675, 0.5 --tween duration while walking & sprint time, respectively
--the tweens for walking and sprinting (I made them reverse, but running only once)
local ti, tiSprint = TweenInfo.new(dur, style, dir, 0, true), TweenInfo.new(durSprint, style, dir, 0, true)
local oneTween, oneSprintTween = ts:Create(sin, ti, {Value = 1}), ts:Create(sin, tiSprint, {Value = 1})
--offset = camera offset; maxRandomBounds = for clamping the Perlin Noise value for point0
local offset, maxRandomBounds = 0.25, 0.5
--point0 and point2 are on the right and left side, respectively
local point0, point2 = Vector3.new(offset, 0, 0), Vector3.new(-offset, 0, 0)
--remember, point1 is in the middle; we'll be using Perlin Noise for the X position
local point1 = Vector3.new(0, -offset, 0)
local ti2 = TweenInfo.new(dur / 2, style, dir)
--tween back to the origin
local tweenBack = ts:Create(hum, ti2, {CameraOffset = Vector3.new(0, 0, 0)})
--tween to point0 so it can bob back and forth
local begTween = ts:Create(hum, ti2, {CameraOffset = point0}) --no, I'm not making the tween beg!
--this will be for our Perlin Noise, the higher the inc, the more random
local counter, counterInc = 1, 0.1
--this will be true once begTween starts playing (to make it not play again)
local running = false
Yeah, that wasn’t so bad when you break it down and explain each one. Alright, alright, let’s move onto the functions before getting to RenderStepped:
local function curve(t, p0, p1, p2) --curve creation
--skip calculations when t is 0 or 1
if t == 0 then return p0 elseif t == 1 then return p2 end
--curve forumla
return (((1 - t)^2) * p0) + (2 * (1 - t) * t * p1) + (t^2 * p2)
--[[do note: we're going to input vector3 values in p0, p1, and p2
(point0, point1, and point2); but vector3 can be multiplied! so, the formula
still works]]
end
local function sprint() --run different tweens depending on if sprinting or not
--just make sure the tween's not playing so we don't interrupt it
if oneTween.PlaybackState ~= playing and isSprinting.Value == false then
oneTween:Play() --run the slower tween when sprinting is false
end
if oneSprintTween.PlaybackState ~= playing and isSprinting.Value == true then
--i notice that we don't need to pause/cancel the tweens, it happens automatically
oneSprintTween:Play() --run the faster tween when sprinting is true
end
end
Lastly, we have the RenderStepped function. You can use the deltaTime
parameter is you want, but I don’t think you should rely on something that unstable (frame rate is not always 60).
rs.RenderStepped:Connect(function(deltaTime)
--[[move direction's magnitude is either 0 (not running) or 1 (running), nothing else!
use the dot product instead of magnitude to avoid unnecessary square root
calculations (which are expensive). a vector's dot product w/ itself is
its magnitude squared, so 0 squared is still 0]]
if hum.MoveDirection:Dot(hum.MoveDirection) > 0 then
--tween to point0, and run it only once
if (not running) then begTween:Play() running = true end
--after the tween reaches point0, continue
if begTween.PlaybackState == completed then
if sin.Value == 0 then --aka when it's point0, set some things
counter = counter + counterInc --for different Perlin Noise
--[[Perlin Noise is from -1 to 1, which can be too big of a range
so clamp it down with the maxRandomBounds variable]]
point1 = Vector3.new(math.clamp(math.noise(counter), -maxRandomBounds, maxRandomBounds), -offset, 0)
--[[separate thread to not delay the function further
I noticed that if it does delay, the tween becomes lopsided
where it goes fast to point2 then slower towards point0;
with coroutines, this will only happen at the beginning
and it will balance out shortly after (IDK the reason!)]]
local coro = coroutine.wrap(sprint)
coro()
end
--[[we actually need to set the cam offset w/ the number value
remember: sin.Value will act as the "time" in the curve function]]
hum.CameraOffset = curve(sin.Value, point0, point1, point2)
end
else --if the player is not running
tweenBack:Play() --tween back to the beginning (if already there, it's not noticeable)
oneTween:Cancel() --stop the walk/sprint tweens
oneSprintTween:Cancel()
sin.Value = 0 --reset the "time" value to 0
running = false --make this false to allow begTween to play again
end
end)
Woah, did that even seem like 87 lines? Them variables be hoggin’ up 27, so it was really 60 lines! Fun fact: this script was almost 156 lines before. Why? Because I had two tweens for going to point2 and coming back to point0, but then I realized, you can just make the tween reverse! That made things much more efficient.
Do remember that this is for first-person, so in third person, the bobbing may not sync up with the footsteps! If you want actual footsteps in first-person, disable the normal walking sound in the sound scripts, then add your own footsteps that match the bobbing.
Also, this is a simple script, so you may add more “jazz” in if you would like to. Plus, various variables like dur
and durSprint
need to be adjusted manually to somewhat match the walk speed. Like I said before, there is so much customization for this!
Full Kit
Recall how I mentioned that having a GUI that allows players to turn off these effects is recommended? Well, although not covered in this tutorial, that UI is in this kit below along with the scripts we just covered. To actually make the GUI work with the scripts, the code may look a teeny bit different, but it’s 99% the same (nothing’s removed from what we covered).
I couldn’t do the thumbnail properly on the model itself, so I made a custom link box.
If you want to use the same UI I provided with the kit, please do not change the names of the scripts or anything within the UI itself! If you don’t want the UI, you can just delete it, then you can safely modify the objects. I’m saying this because my UI scripts are very name-based so, they can break easily.
Instructions
- The ScreenGui – put it in StarterGui
- And the three LocalScripts – parent them to StarterPlayer > StarterCharacterScripts
Closing Remarks & Feedback
This was one of those longer tutorials, so there may be some flaws that I have missed. Along with the polls, feel free to DM me with any mistakes in this tutorial, though I will reread after posting and edit if needed.
Rate this tutorial.
How well did you understand the topic? Are the diagrams/images clear? Etc.
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
0 voters
Did you learn anything new?
Any concepts as well (like Perlin Noise and Bézier Curves).
- Yes
- No
0 voters
On a scale of 1-10, how likely are you to use this (or just a part) in your games/projects?
Is this useful to you in any way? Are you going to implement this into your games? Etc.
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
0 voters
That’s it for today! I know it’s been a while since my last tutorial, but man I spent days trying to perfect the scripts and the UI.
Thank you all for your time and feedback,
And wear shades, but don’t be too shade-y!