How to Create Chromatic Aberration Effect

This method is not optimized, I am only giving you the main idea so, so you know how it is possible, so you do it on your own


Introduction

Visual effects can greatly enhance the atmosphere of a game. Chromatic aberration is one such effect that can give your game a unique, stylistic edge by adding a subtle distortion of colors, often seen in vintage photography or to create a disorienting effect. In this tutorial, I’ll guide you through the steps to create a chromatic aberration effect, adding a professional touch to your game’s visuals.

Before we start, make sure you have a basic understanding of Roblox Studio and scripting

Please note that the use of continuous updates and frequent object management operations can impact game performance, particularly on lower-end devices.

Step 1: Set Up Your Environment

  1. Insert a Screengui.
    image
  2. Insert 3 Viewportframes.
    image
  3. Name them Red, Green and Blue
    image
  4. Insert WorldModel in each frame:
    image
    WorldModel basically provides some physics features to the viewportframe
    Read more about it here

Step 2: Adjust the Viewportframes Transparency and Colors

Settings for Red Viewportframe:
image
Settings for Green Viewportframe:
image
Settings for Blue Viewportframe:
image

Now time to start scripting!

Step 3: Scripting the Chromatic Aberration Effect

  1. Insert a local script inside the Screengui
    image
  2. Create a table for viewportframes
    image
  3. Cloning function:
    We first loop through the children of the workspace. If the object is not a terrain or a camera, we loop through the table of the ViewportFrames, finding the WorldModel inside each one. If a WorldModel exists and the object is not already inside the WorldModel (to prevent duplication), we clone the object, check if the cloned part exists, and then finally parent it to the WorldModel.
  4. Cleaning Function:
    We first loop through the viewportFrames. For each viewport, we find the WorldModel inside it. If a WorldModel exists, we then loop through its children. For each child, we destroy it
  5. SetCamera Function:
    The setCamera function updates the CurrentCamera property of each ViewportFrame in the viewportFrames table to match the CurrentCamera of the workspace. After defining the function, it is immediately called to apply the changes.
    image
  6. Updating Function:
    This function ensures that the cloneObjects and cleanUpObjects functions run continuously and match with the game’s rendering process. By using RenderStepped, the update occurs every frame, providing a consistent and smooth experience. The task.wait() adds a small delay to potentially balance the load, preventing the cleanUpObjects function from executing immediately after cloneObjects.
    image

The End

Final Code:

Result:

2024-08-1515-12-31-ezgif.com-cut

How Would You Rate This Tutorial?

  • 5 - Excellent: The tutorial was clear, comprehensive, and very helpful.
  • 4 - Good: The tutorial was helpful and well-explained, with minor areas for improvement.
  • 3 - Average: The tutorial was okay but could use improvements.
  • 2 - Poor: The tutorial had some useful information but was lacking in key areas.
  • 1 - Very Poor: The tutorial was unclear or unhelpful.

0 voters

40 Likes

Your way seems to be extremely unoptimized, to be honest. As well as providing the code as an image? Not really convenient.

I would suggest using this module to help fix performance.

2 Likes

I’m aiming to keep things simple and accessible while ensuring that readers understand the concepts.

Including the code as an image is intentional; it helps to encourage learning rather than just copying

1 Like

Keeping things simple should also include properly optimizing things. Plus, that module is already simpler to use than your implementation and optimized. If you don’t consider optimization than this tutorial is simply useless.

It just makes the tutorial annoying.

2 Likes

great tutorial, this is something that will come useful in the future for my game. straight to the bookmark

3 Likes

I understand the importance of optimization and will definitely take that into account for future tutorials. My goal with this one was to balance simplicity with educational value, but I appreciate your perspective. If you have any suggestions for optimizing the approach or improving the tutorial, I’d be grateful!

1 Like

It’s a tutorial - not necessarily a drag and drop resource (it’s not in #resources:community-resources for a reason). As long as OP is displaying functional proof of concept of how to create a Chromatic Aberration effect (which they are), I’m not sure where the critique is coming from. The method is clear and replicable. If people want to take it in different directions (including more optimized directions) they are able to do that - OP is just here to point them in the direction of it.

7 Likes

Adding a module in s tutorial doesn’t make it into a resource. I keep saying to optimise because the current implementation seems to lag the game so much that it would not work for low end devices and therefore using what I said, that is the module, would first, make things easier as it’s already easy to use and second, make things optimised.

This tutorial would be useless for low end devices. I know people can just disable it but it would still render it useless.

Op could also make an alternative section with optimised code.

This looks great .I remember trying to accomplish something similar some time ago and I ended up making this: An effect similar to stereoscopic 3d effect/chromatic aberration/anaglyph I am posting this just because I noticed that you are using viewport frames and I know that they have some problems when it comes to lighting. Feel free to take elements from this if they might be useful for your approach though.

1 Like

Thank you, I will try that and i will also look into optimizing the system better

1 Like

Why dont you give us an optimized version then? You’re coming for this guy with all these reponses and no solution.

1 Like

Alright. I’ll do that. I’ll let you know when I make one.

2 Likes

Would love to see your implementation on an effect like this. (As far as I am aware) this is really the only method to achieve chromatic aberration on roblox.


Also, I want to point out that I have the code… It just doesn’t work.

That’s what I got. Probably the logic is ugly:
(And here too much bugs)

local RunService = game:GetService("RunService")
local TweenService = game:GetService("TweenService")

local screenGui = script.Parent
local viewports = {
	Red = screenGui.Red,
	Green = screenGui.Green, 
	Blue = screenGui.Blue
}

local CONFIG = {
	CAMERA_OFFSET = {
		Red = Vector3.new(0.3, 0.2, 0.2),
		Green = Vector3.new(0.15, 0.1, 0.1),
		Blue = Vector3.new(-0.3, -0.2, -0.2)
	},
	MOTION_MULTIPLIER = {
		Red = 3,
		Green = 2,
		Blue = 1
	},
	BASE_INTENSITY = 1,
	MOTION_SENSITIVITY = 0.3,
	RANDOM_INTENSITY = 0.02
}

local clones = {}
local previousCameraPos = Vector3.new()
local currentIntensity = CONFIG.BASE_INTENSITY

for name, viewport in (viewports) do
	local worldModel = viewport:FindFirstChildOfClass("WorldModel") or Instance.new("WorldModel")
	worldModel.Name = "WorldModel"
	worldModel.Parent = viewport

	local camera = Instance.new("Camera")
	camera.FieldOfView = 70
	camera.CameraType = Enum.CameraType.Scriptable
	viewport.CurrentCamera = camera

	clones[viewport] = {}
end

local function updateIntensity(delta)
	local velocity = (workspace.CurrentCamera.CFrame.Position - previousCameraPos).Magnitude
	local targetIntensity = CONFIG.BASE_INTENSITY + (velocity * CONFIG.MOTION_SENSITIVITY)
	currentIntensity = currentIntensity + (targetIntensity - currentIntensity) * delta * 5
end

local function updateClones()
	for _, obj in ipairs(workspace:GetChildren()) do
		if not obj:IsA("Terrain") and not obj:IsA("Camera") then
			for name, viewport in pairs(viewports) do
				local viewportClones = clones[viewport]

				if not viewportClones[obj] then
					obj.Archivable = true
					local clone = obj:Clone()
					clone.Parent = viewport.WorldModel
					viewportClones[obj] = clone
				end
				
				local clone = viewportClones[obj]
				if obj:IsA("BasePart") then
					clone.CFrame = obj.CFrame
				elseif obj:IsA("Model") and obj.PrimaryPart then
					if obj ~= game.Players.LocalPlayer.Character then
						clone:SetPrimaryPartCFrame(obj.PrimaryPart.CFrame)
					end
				end
			end
		end
	end

	for _, viewport in pairs(viewports) do
		for original, clone in pairs(clones[viewport]) do
			if not original:IsDescendantOf(workspace) then
				clone:Destroy()
				clones[viewport][original] = nil
			end
		end
	end
end

local function updateCameras(delta)
	local baseCFrame = workspace.CurrentCamera.CFrame
	local basePos = baseCFrame.Position

	updateIntensity(delta)

	local randomOffset = Vector3.new(
		math.random() * CONFIG.RANDOM_INTENSITY,
		math.random() * CONFIG.RANDOM_INTENSITY,
		math.random() * CONFIG.RANDOM_INTENSITY
	)

	for name, viewport in pairs(viewports) do
		local offset = CONFIG.CAMERA_OFFSET[name] * currentIntensity
		local motionOffset = (basePos - previousCameraPos) * CONFIG.MOTION_MULTIPLIER[name]
		viewport.CurrentCamera.CFrame = baseCFrame * CFrame.new(offset + motionOffset + randomOffset)
	end

	previousCameraPos = basePos
end

RunService.RenderStepped:Connect(function(delta)
	updateClones()
	updateCameras(delta)
end)

Updated
local RunService = game:GetService("RunService")

local screenGui = script.Parent
local viewports = {
	Red = screenGui.Red,
	Green = screenGui.Green,
	Blue = screenGui.Blue
}

local CONFIG = {
	CAMERA_OFFSET = {
		Red = Vector3.new(0.12, 0.4, 0.2),
		Green = Vector3.new(0.2, 0.2, 0.1),
		Blue = Vector3.new(-0.145, -0.3, 0.15)
	},
	MOTION_MULTIPLIER = {
		Red = 1.0,
		Green = 0.8,
		Blue = 0.6
	},
	BASE_INTENSITY = 0.1,
	MOTION_SENSITIVITY = 0.15,
	RANDOM_INTENSITY = 0.005,
	SMOOTHING = 0.9,
	DISTANCE_FADE = 1,
	MAX_DISTANCE_MULTIPLIER = 2.0,
	COLOR_TRAIL_LENGTH = 75
}


local clones = {}
local previousCameraPos = Vector3.new()
local currentIntensity = CONFIG.BASE_INTENSITY

local cameraPosHistory = { }

local function updateCameraPositionsHistory(newPosition)
	table.insert(cameraPosHistory, newPosition)
	while #cameraPosHistory > CONFIG.COLOR_TRAIL_LENGTH do
		table.remove(cameraPosHistory, 1)
	end
end

local function getAveragePositionInHistory()
	local sum = Vector3.new(0, 0, 0)
	for _, pos in ipairs(cameraPosHistory) do
		sum = sum + pos
	end
	return sum / #cameraPosHistory
end

for name, viewport in pairs(viewports) do
	local worldModel = viewport:FindFirstChildOfClass("WorldModel") or Instance.new("WorldModel")
	worldModel.Name = "WorldModel"
	worldModel.Parent = viewport

	local camera = Instance.new("Camera")
	camera.FieldOfView = 70
	camera.CameraType = Enum.CameraType.Scriptable
	viewport.CurrentCamera = camera

	clones[viewport] = {}
end

local function updateIntensity(delta)
	local velocity = (workspace.CurrentCamera.CFrame.Position - previousCameraPos).Magnitude
	local targetIntensity = CONFIG.BASE_INTENSITY + (velocity * CONFIG.MOTION_SENSITIVITY)
	currentIntensity = currentIntensity + (targetIntensity - currentIntensity) * delta * 5
end

local function updateClones()
	for _, obj in ipairs(workspace:GetChildren()) do
		if not obj:IsA("Terrain") and not obj:IsA("Camera") then
			for name, viewport in pairs(viewports) do
				local viewportClones = clones[viewport]

				if not viewportClones[obj] then
					obj.Archivable = true
					local clone = obj:Clone()
					clone.Parent = viewport.WorldModel
					viewportClones[obj] = clone
				end

				local clone = viewportClones[obj]
				if obj:IsA("BasePart") then
					clone.CFrame = obj.CFrame
				elseif obj:IsA("Model") and obj.PrimaryPart then
					if obj ~= game.Players.LocalPlayer.Character then
						clone:SetPrimaryPartCFrame(obj.PrimaryPart.CFrame)
					end
				end
			end
		end
	end

	for _, viewport in pairs(viewports) do
		for original, clone in pairs(clones[viewport]) do
			if not original:IsDescendantOf(workspace) then
				clone:Destroy()
				clones[viewport][original] = nil
			end
		end
	end
end

local function getLookDirection()
	return workspace.CurrentCamera.CFrame.LookVector
end

local function getDistanceMultiplier(position)
	local camera = workspace.CurrentCamera
	local distance = (position - camera.CFrame.Position).Magnitude
	local fade = math.clamp((distance - CONFIG.DISTANCE_FADE) / CONFIG.DISTANCE_FADE, 0, 1)
	return 1 + (fade * (CONFIG.MAX_DISTANCE_MULTIPLIER - 1))
end

local smoothedIntensity = CONFIG.BASE_INTENSITY
local smoothedOffset = Vector3.new()

local function updateCameras(delta)
	local baseCFrame = workspace.CurrentCamera.CFrame or CFrame.new()
	local basePos = baseCFrame.Position

	updateCameraPositionsHistory(basePos)

	local lookDir = getLookDirection()

	local velocity = (basePos - previousCameraPos).Magnitude
	local targetIntensity = CONFIG.BASE_INTENSITY + (velocity * CONFIG.MOTION_SENSITIVITY)
	smoothedIntensity = smoothedIntensity + (targetIntensity - smoothedIntensity) * (1 - CONFIG.SMOOTHING)

	local randomOffset = Vector3.new(
		math.random() * CONFIG.RANDOM_INTENSITY,
		math.random() * CONFIG.RANDOM_INTENSITY,
		math.random() * CONFIG.RANDOM_INTENSITY
	)

	smoothedOffset = smoothedOffset:Lerp(randomOffset, 1 - CONFIG.SMOOTHING)

	local averagePosition = getAveragePositionInHistory()

	for name, viewport in pairs(viewports) do
		local baseOffset = CONFIG.CAMERA_OFFSET[name]
		local motionOffset = (averagePosition - previousCameraPos) * CONFIG.MOTION_MULTIPLIER[name]

		local rightVector = baseCFrame.RightVector
		local upVector = baseCFrame.UpVector
		local forwardVector = baseCFrame.LookVector

		local adjustedOffset = (rightVector * baseOffset.X + upVector * baseOffset.Y + forwardVector * baseOffset.Z) * smoothedIntensity

		local distanceMultiplier = getDistanceMultiplier(basePos)
		adjustedOffset = adjustedOffset * distanceMultiplier

		viewport.CurrentCamera.CFrame = baseCFrame * CFrame.new(
			adjustedOffset +
				(motionOffset * smoothedIntensity) +
				(smoothedOffset * distanceMultiplier)
		)
	end

	previousCameraPos = basePos
end

RunService.RenderStepped:Connect(function(delta)
	updateClones()
	updateCameras(delta)
end)

Well forget about it. I am busy with my own life now because I got college exams.

Can i Have the place file?