How would I create a blur drag effect like Amnesia: TDD

I’m trying to recreate the effect in this video at 3:39 where the camera blurs but the blur effect has this drag on it when the player looks around. I have seen this effect does in this game but I would like to recreate it.

4 Likes

From what I noticed playing it with peed pants I can say it uses Glass material parts to do the “not-refreshing” screen pixels, don’t know how though. Or with viewport frames, because changing my graphics settings seem to affect it

Directional motion blur like this is not yet possible in Roblox; games that do this have an extra velocity buffer computed during the render pass that makes this sort of thing possible to do with a post-process shader. The best you can do right now is probably to use just a regular Blur effect whose strength increases with movement (rotational and/or linear) of the camera. It won’t look the same, it probably won’t even look good. I’d focus on something else, tbh, a lot of people feel sick from motion blur and turn it off, or turn it off just to get better framerate.

Haven’t tried this before but I had the same interest as you for a while.
I noticed some games simply use what’s rendered to the player’s screen as a low resolution texture.

I would personally try to utilize CaptureService for this.
The idea is simple:

  1. Capture the player’s screen at a low frequency (be generous, e.g. between 10 and 30 hertz).
  2. Instance the resulting texture as a UI taking up the player’s entire screen, but highly transparent.
  3. Move the individual texture in the opposite direction the player’s cursor moves towards (to simulate texture movement), while the texture is fading out quickly.
  4. Do this rapidly for every screen capture to simulate a “ghosting” effect.

Again, just a very rough idea based on my own observations.

2 Likes

I did try that and ur right it looks mid, im just trying to recreate this effect for flashback cutscenes in my game that last like 10 seconds

ill see what I can do with captureservice

If it’s fast enough, that could be an interesting effect. Traditionally though, when this was done in 2D games (or replicated in a 3D game, using a shader), the frame stacking is done with just a couple of buffers, and blitting semi-transparent copies onto what’s already in the buffer.

I don’t think even EditableImages have a way to do this quickly enough for a realtime effect, and I’d kind expect that if you had enough CaptureService images stacked, that the effect is really only going to be viable on gaming PCs due to the huge amount of texture memory this would use. But definitely try it. Worst case maybe it works on PC but you have to do something else on mobile or lower-end machines.

Frame-stacking blur is also visually a very different effect from motion blur; but if you’re just after something disorienting, it could still work.

i managed to make it work but the problem is captureservice cant update faster than 60 fps so it works but its too slow

Could you show a clip of how it looks?
I didn’t think you’d need anywhere near up to 60hz for this, but my speculation could be off.

i cant record but heres the code i used, js put a localscript in starterplayerscripts

--// Directional Capture Blur / Amnesia Low Health Vision
--// Place in StarterPlayerScripts

local Players = game:GetService("Players")
local CaptureService = game:GetService("CaptureService")
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")

local player = Players.LocalPlayer
local playerGui = player:WaitForChild("PlayerGui")

--==================================================
-- SETTINGS
--==================================================

--==================================================
-- HEALTH
--==================================================

-- Effect begins below this health.
local HEALTH_THRESHOLD = 15

-- Maximum effect strength.
-- 1 = full effect.
local MAX_HEALTH_EFFECT = 9999

--==================================================
-- CAPTURE
--==================================================

-- Desired capture request rate.
--
-- CaptureService may physically capture slower than this,
-- but the script will request captures as frequently as
-- the service allows.
local CAPTURE_RATE = 9999

--==================================================
-- DIRECTIONAL SMEAR
--==================================================

-- How strongly the captured image follows behind
-- the player's mouse.
local DRAG_MULTIPLIER = 4

-- Maximum distance a ghost can travel.
local MAX_DRAG = 500

--==================================================
-- GHOST APPEARANCE
--==================================================

-- Lower = stronger.
local START_TRANSPARENCY = 0.1

-- How long each ghost exists.
local FADE_TIME = 2

-- Maximum ghosts at once.
local MAX_GHOSTS = 999

-- Ignore extremely tiny mouse movement.
local MOVEMENT_DEADZONE = 0.05

--==================================================
-- CONTINUOUS SMEAR
--==================================================

-- How quickly the smear reacts to mouse movement.
local DRAG_RESPONSE = 45

-- How quickly the smear settles when movement stops.
local TRAIL_DECAY = 2

--==================================================
-- GUI
--==================================================

local screenGui = Instance.new("ScreenGui")

screenGui.Name = "DirectionalCaptureBlur"

screenGui.IgnoreGuiInset = true

screenGui.ResetOnSpawn = false

screenGui.DisplayOrder = 999999

screenGui.ZIndexBehavior =
	Enum.ZIndexBehavior.Sibling

screenGui.Parent = playerGui

--==================================================
-- STATE
--==================================================

local humanoid = nil

local healthEffect = 0

local lastMousePosition =
	UserInputService:GetMouseLocation()

local mouseVelocity =
	Vector2.zero

local smoothMouseVelocity =
	Vector2.zero

local smearVelocity =
	Vector2.zero

local smearOffset =
	Vector2.zero

local ghosts = {}

local capturing = false

local accumulator = 0

--==================================================
-- CHARACTER
--==================================================

local function setupCharacter(character)

	humanoid =
		character:WaitForChild("Humanoid")

	-- Reset effect when respawning.
	healthEffect = 0

end

if player.Character then

	setupCharacter(
		player.Character
	)

end

player.CharacterAdded:Connect(
	setupCharacter
)

--==================================================
-- HEALTH + MOUSE VELOCITY
--==================================================

RunService.RenderStepped:Connect(function(deltaTime)

	--==================================================
	-- HEALTH INTENSITY
	--==================================================

	if humanoid then

		local health =
			humanoid.Health

		if health < HEALTH_THRESHOLD then

			-- 15 HP = 0%
			-- 0 HP = 100%

			healthEffect =
				math.clamp(
					(
						HEALTH_THRESHOLD
						-
						health
					)
					/
					HEALTH_THRESHOLD,

					0,

					MAX_HEALTH_EFFECT
				)

		else

			healthEffect = 0

		end

	end

	--==================================================
	-- MOUSE VELOCITY
	--==================================================

	local mousePosition =
		UserInputService:GetMouseLocation()

	local movement =
		mousePosition
	-
		lastMousePosition

	if deltaTime > 0 then

		mouseVelocity =
			movement
			/
			deltaTime

	end

	lastMousePosition =
		mousePosition

	-- Smooth mouse movement.
	smoothMouseVelocity =
		smoothMouseVelocity:Lerp(
			mouseVelocity,
			math.clamp(
				deltaTime * 30,
				0,
				1
			)
		)

end)

--==================================================
-- REMOVE OLDEST GHOST
--==================================================

local function removeOldestGhost()

	if #ghosts == 0 then
		return
	end

	local oldest =
		table.remove(
			ghosts,
			1
		)

	if oldest
		and oldest.image
		and oldest.image.Parent then

		oldest.image:Destroy()

	end

end

--==================================================
-- CREATE GHOST
--==================================================

local function createGhost(contentId)

	-- Don't create anything if the effect is inactive.
	if healthEffect <= 0 then
		return
	end

	if not contentId then
		return
	end

	-- Keep ghost count under control.
	while #ghosts >= MAX_GHOSTS do

		removeOldestGhost()

	end

	local ghost =
		Instance.new("ImageLabel")

	ghost.Name =
		"CaptureGhost"

	ghost.AnchorPoint =
		Vector2.new(
			0.5,
			0.5
		)

	-- Overscan prevents edges from appearing.
	ghost.Size =
		UDim2.new(
			1,
			800,
			1,
			800
		)

	ghost.Position =
		UDim2.fromScale(
			0.5,
			0.5
		)

	ghost.BackgroundTransparency =
		1

	ghost.BorderSizePixel =
		0

	ghost.Image =
		contentId

	ghost.ImageTransparency =
		1 -
		(
			(
				1
				-
				START_TRANSPARENCY
			)
			*
			healthEffect
		)

	ghost.ScaleType =
		Enum.ScaleType.Stretch

	ghost.ZIndex =
		1

	ghost.Parent =
		screenGui

	--==================================================
	-- GHOST DATA
	--==================================================

	table.insert(
		ghosts,
		{
			image = ghost,

			age = 0,

			strength =
				0.5
				+
				(
					#ghosts
					/
					MAX_GHOSTS
				)
				*
				1.5,

			healthAtCreation =
				healthEffect,
		}
	)

end

--==================================================
-- CONTINUOUS SMEAR
--==================================================

RunService.RenderStepped:Connect(function(deltaTime)

	--==================================================
	-- TARGET VELOCITY
	--==================================================

	local velocity =
		smoothMouseVelocity

	if velocity.Magnitude <
		MOVEMENT_DEADZONE then

		velocity =
			Vector2.zero

	end

	-- Old vision moves opposite mouse movement.
	local targetVelocity =
		-velocity
		*
		DRAG_MULTIPLIER
		*
		healthEffect

	-- Limit.
	if targetVelocity.Magnitude >
		MAX_DRAG then

		targetVelocity =
			targetVelocity.Unit
			*
			MAX_DRAG

	end

	--==================================================
	-- FOLLOW
	--==================================================

	smearVelocity =
		smearVelocity:Lerp(
			targetVelocity,
			math.clamp(
				deltaTime
				*
				DRAG_RESPONSE,
				0,
				1
			)
		)

	--==================================================
	-- MOVE
	--==================================================

	smearOffset +=
		smearVelocity
		*
		deltaTime
		*
		healthEffect

	--==================================================
	-- DECAY
	--==================================================

	smearOffset *=
		math.exp(
			-TRAIL_DECAY
			*
			deltaTime
		)

	--==================================================
	-- LIMIT
	--==================================================

	if smearOffset.Magnitude >
		MAX_DRAG then

		smearOffset =
			smearOffset.Unit
			*
			MAX_DRAG

	end

	--==================================================
	-- UPDATE GHOSTS
	--==================================================

	for i = #ghosts, 1, -1 do

		local ghostData =
			ghosts[i]

		local ghost =
			ghostData.image

		if not ghost
			or not ghost.Parent then

			table.remove(
				ghosts,
				i
			)

			continue
		end

		ghostData.age +=
			deltaTime

		--==================================================
		-- LIFE
		--==================================================

		local alpha =
			math.clamp(
				ghostData.age
				/
				FADE_TIME,
				0,
				1
			)

		--==================================================
		-- POSITION
		--==================================================

		local offset =
			smearOffset
			*
			ghostData.strength

		ghost.Position =
			UDim2.new(
				0.5,
				offset.X,
				0.5,
				offset.Y
			)

		--==================================================
		-- TRANSPARENCY
		--==================================================

		if healthEffect <= 0 then

			ghost.ImageTransparency = 1

		else

			local baseTransparency =
				1
			-
				(
					(
						1
						-
						START_TRANSPARENCY
					)
					*
					healthEffect
				)

			ghost.ImageTransparency =
				baseTransparency
				+
				(
					1
					-
					baseTransparency
				)
				*
				alpha

		end

		--==================================================
		-- REMOVE
		--==================================================

		if alpha >= 1
			or healthEffect <= 0 then

			ghost:Destroy()

			table.remove(
				ghosts,
				i
			)

		end

	end

	--==================================================
	-- RESET WHEN EFFECT IS OFF
	--==================================================

	if healthEffect <= 0 then

		smearOffset =
			smearOffset:Lerp(
				Vector2.zero,
				math.clamp(
					deltaTime * 10,
					0,
					1
				)
			)

		smearVelocity =
			smearVelocity:Lerp(
				Vector2.zero,
				math.clamp(
					deltaTime * 10,
					0,
					1
				)
			)

	end

end)

--==================================================
-- CAPTURE
--==================================================

local function captureScreen()

	-- Don't capture when the effect isn't needed.
	if healthEffect <= 0 then
		return
	end

	if capturing then
		return
	end

	capturing = true

	local success =
		pcall(function()

			CaptureService:CaptureScreenshot(
				function(contentId)

					if contentId
						and healthEffect > 0 then

						createGhost(
							contentId
						)

					end

					capturing = false

				end
			)

		end)

	if not success then

		capturing = false

	end

end

--==================================================
-- CAPTURE LOOP
--==================================================

RunService.RenderStepped:Connect(function(deltaTime)

	-- Completely stop capture above 15 HP.
	if healthEffect <= 0 then

		accumulator = 0

		return

	end

	accumulator +=
		deltaTime

	local interval =
		1
		/
		CAPTURE_RATE

	if accumulator >= interval then

		accumulator -=
			interval

		if not capturing then

			captureScreen()

		end

	end

end)
1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.