Dynamic Depth of Field

A quick tutorial on how to make a simple Dynamic DOF effect

by Foxxive

Author Avatar

Hi, this tutorial will be teaching you, the reader, how to make a Dynamic Depth of Field effect


To start things off...

Depth of field is an instance that can be a child of Lighting under the explorer. Additional elements can be added such as Bloom, Blur, Color Correction, and a couple of others.

Depth of Field on it's own is a lighting instance that, depending on how it's configured, will blur surroundings at a distance or up close. Both of these use cases can be great for things like cutscenes or general ambient blending.

image|250x250


What do I mean by "Dynamic"?

When I say "Dynamic Depth of Field", I mean it adjusts to the player's camera view and it's surroundings. For example, if I zoomed into an object- as if to examine it, the rest of the world around the object within a specified radius would swiftly but smoothly blur out. It's a simple effect, but can really make a difference. The script is quite simple too, so let's continue with that!


Scripting our effect

To start scripting insert a LocalScript into StarterGui. You can also insert it into StarterCharacterScripts.

First, we'll need to define our DOF effect, so let's do:

local DOF = Instance.new("DepthOfFieldEffect", game.Lighting)
DOF.InFocusRadius = 0
DOF.NearIntensity = 0

Then, we have to define our Camera, I'll explain why in a bit:

local DOF = Instance.new("DepthOfFieldEffect", game.Lighting)
DOF.InFocusRadius = 0
DOF.NearIntensity = 0

local camera = workspace.CurrentCamera

Now, let's define our player and our character:

local DOF = Instance.new("DepthOfFieldEffect", game.Lighting)
DOF.InFocusRadius = 0
DOF.NearIntensity = 0

local camera = workspace.CurrentCamera
local player = game.Players.LocalPlayer
local character = player.Character or player.CharacterAdded:Wait()

Our player is us, the player sitting at our computers or using our phones to play games. Character is the character we are controlling in the environment. The reason we add or player.CharacterAdded:Wait() is as a failsafe option of sorts. In case the game or script doesn't react within a timeframe, instead of exhausting itself the script will also consider to wait for a player's character to load into the game

We can continue by doing:

local DOF = Instance.new("DepthOfFieldEffect", game.Lighting)
DOF.InFocusRadius = 0
DOF.NearIntensity = 0

local camera = workspace.CurrentCamera
local player = game.Players.LocalPlayer
local character = player.Character or player.CharacterAdded:Wait()
local head = char:WaitForChild("Head")
local HumanoidRootPart = char:WaitForChild("HumanoidRootPart")

local focusRadius = 6

So, you'll notice at the bottom there's a variable defined as focusRadius at the bottom. We're setting this as an intValue within the script, and with this intValue we're basically asking the script: "How far can an object be before we focus on it?"

Keep in mind that this will be measured in studs. You're free to change the value, but I recommend keeping it somewhere between 5-10 studs. Setting it to something like 2 would be WAY too close, and setting it to something over 20 would make it behave oddly.

Next, we'll write a very simple function:

function Lerp(a, b, t)
    return a + (b - a) * t
end

You might be familiar with the term Lerp It stands for "Linear Interpolation" and, in a way, can be compared to Tweening. The reason we won't be using tweening in this case is because it can get messy, lerp is easier to write down.

In this lerp function, we're using parameters a, b, and t. Parameters a and b are 2 different points we will be interpolating between that we can define later in the code, whereas t is the time it'll take to interpolate something from point a to b or vice-versa.

Then to add, we'll add another very simple function:

function isFirstPerson()
	local cameraDistance = (camera.CFrame.Position - camera.Focus.Position).Magnitude
	return (cameraDistance < 1)
end

We'll be using this function because Roblox has no built-in way to check if the player is in first person or not without any hacky methods for some reason.

Next, we'll be scripting the function that makes the whole thing come together! To start, we'll write:

game:GetService("RunService").RenderStepped:Connect(function(dt)

RenderStepped is a function of "RunService" that fires every frime prior to the current one. So let's say we're sitting at frame 7, RenderStepped will run every frame from 1-6. We move on to frame 8, it'll render frame 1-7, so on and so forth.

You'll also notice between parenthesis at the end of the first line of the function, we wrote (dt). That DT stands for Delta time, which is essentially the time between events. This is an extremely useful thing to use. We mostly use dt to make sure frame rate makes little to no difference on what happens in-game.

On the next line, we'll write:

game:GetService("RunService").RenderStepped:Connect(function(dt)
	local ignoreList = isFirstPerson() and {character} or {}

Our variable named ignoreList is exactly what it sounds like: an ignore list. Within this list, we're adding our isFirstPerson() function that is also paired with {character} or {}. Between the last pair of brackets, we can add objects in the workspace we don't want to activate the effect. Like grass parts, or really small details that aren't worth activating the effect with. We can assign these objects to a variable and put them in the table, or by other means you may wish to use

Next, we'll do:

game:GetService("RunService").RenderStepped:Connect(function(dt)
	local ignoreList = isFirstPerson() and {character} or {}
	
	local camRay = Ray.new(cam.CFrame.Position, cam.CFrame.LookVector * focusRadius)
	local hit, hitPosition = workspace:FindPartOnRayWithIgnoreList(ray, ignoreList)

Our camRay will be a ray we'll generate from our camera's position that will be looking at the camera's LookVector and multiplying it by the focusRadius variable we set earlier. In other words, we're gonna be firing our ray from the position of our camera, towards the direction it's looking in, within the radius we set. Which in this case would be 6 studs away

Underneath the camRay variable, we're setting a variable hit and hitPosition. With that, we'll tell the script to check if there's a part within the radius that isn't on an ignore list, and we're also telling the script where the position of said part is.

Now, we'll wrap up our code by doing:

game:GetService("RunService").RenderStepped:Connect(function(dt)
	local ignoreList = isFirstPerson() and {character} or {}
	
	local camRay = Ray.new(cam.CFrame.Position, cam.CFrame.LookVector * focusRadius)
	local hit, hitPosition = workspace:FindPartOnRayWithIgnoreList(ray, ignoreList)
	
	if hit and hit.Transparency < 0.3 then
		local distanceFromCamera = (hitPosition - camera.CFrame.Position).Magnitude
		local intensity = focusRadius / distanceFromCamera * 0.25
		DOF.FocusDistance = distanceFromCamera
		DOF.FarIntensity = Lerp(DOF.FarIntensity, intensity, dt * 8)
	else
		DOF.FarIntensity = Lerp(DOF.FarIntensity, 0, dt * 8)
	end
end)

If we hit a part, and the part's transparency is less than < 0.3, then run the stuff that comes after. This is written to prevent transparent stuff from breaking the script.

With our variable distanceFromCamera, we're doing exactly what it says. Checking distance from our camera to the position of the part we're hitting with our ray. intensity is also just what it is, however with intensity, we divide our focusRadius (6 in this case) by the distance of the part our ray is hitting from the camera and multiplying it by 0.25. Let's say I hit a part within 4 studs of the radius. If we took 6 and divided it by 4, we get 1.5, and if we multiply it by 0.25, we get 0.375. 0.375 will be the intensity of the effect while we focus on a part or other character.

Continuing, if the part does meet the criteria we mentioned previously, then Lerp the intensity from FarIntensity to Intensity and make it take 8 seconds together with DT so FPS does not affect it. Else, if the part's transparency is greater than > 0.3, or if we look at nothing, then lerp the intensity back 0 and keep it like that until we look at something again.

Congratulations! You made a Dynamic Depth of Field effect inside of your game!

That's the tutorial. I also made a Youtube video on this that showcases the final effect in action. Sadly I cannot link videos here, so you'd have to go to my Roblox profile and check out my Youtube channel to find it Anyways, thank you for reading my first (proper) written tutorial on Roblox scripting! If you'd like more tutorials please do let me know, any constructive feedback/criticism is also appreciated to help me improve something!

See you all on the flipside - 🦊

View in-game to comment, award, and more!