Render Characters At Lower Framerates

So recently I saw a video by ByteBlox about creating a system for rendering character movements at a lower framerate for stylized reasons.

Such as running them at 10 FPS for more of a Spider-verse type feel.

Immediately I saw an opportunity to use an infinitely better method.

Here is a LocalScript that you can drop in StarterPlayerScripts. It will render EVERY character in the game at the framerate you set using the variable CHARACTER_FRAMERATE. It doesn’t use viewport frames, it doesn’t create any new parts or models, it just does everything automatically. I’ve also already optimized it pretty well for performance.

Should work on every avatar and every type of animation out of the box.

ENJOY :smiley:

Character_Renderer.lua (2.6 KB)

34 Likes

Was wondering how people did this. I tried putting it in a localscript in the starterplayerscripts but nothing seemed to happen, no console errors either which is weird.

2 Likes

Try walking around, your character animations should be at 10 FPS by default.

Super neat!

You needed a video showcasing it… so here you go. :upside_down_face:

10 Likes

ik this is for stylistic reasons, but do u think this has any effect on performance?

to be more clear: do u think this could improve perf because youre rendering chars at a lower framerate? or is it all for show? i couldnt tell by looking at the src code

2 Likes

Oh my god I feel so dumb now, my silly brain told me to make a whole system to render the lower frames by replicating a fake character over the original and adding the low frame rate on the fake character-

I genuinely need to learn more about Motor6Ds omg, but really good job on this btw!!

And for anyone who wants this to work on R6, you can just do these few changes I did myself and it works perfectly fine.

Replace the entire Character_Joint_References table with:

local Character_Joint_References = {
	-- R15
	UpperTorso = 'Waist',
	LowerTorso = 'Root',
	RightUpperArm = 'RightShoulder',
	RightLowerArm = 'RightElbow',
	RightHand = 'RightWrist',
	Head = 'Neck',
	LeftUpperArm = 'LeftShoulder',
	LeftLowerArm = 'LeftElbow',
	LeftHand = 'LeftWrist',
	LeftFoot = 'LeftAnkle',
	LeftLowerLeg = 'LeftKnee',
	LeftUpperLeg = 'LeftHip',
	RightFoot = 'RightAnkle',
	RightLowerLeg = 'RightKnee',
	RightUpperLeg = 'RightHip',
	
	-- R6
	HumanoidRootPart = 'RootJoint',
	Torso = {'Left Hip', 'Left Shoulder', 'Neck', 'Right Hip', 'Right Shoulder'}
}

(Or you can just add the R6 part, same thing lol)
And replace the function readTransforms with:

function readTransforms(Character)
	Character_Transform_Cache[Character] = {}
	for JointParent,JointName in pairs(Character_Joint_References) do
		if typeof(JointName) == 'table' then
			for _, v in JointName do
				if Character:FindFirstChild(JointParent) and Character[JointParent]:FindFirstChild(v) then
					local Joint = Character[JointParent][v]
					Character_Transform_Cache[Character][Joint] = Joint.Transform
				end
			end
		else
			if Character:FindFirstChild(JointParent) and Character[JointParent]:FindFirstChild(JointName) then
				local Joint = Character[JointParent][JointName]
				Character_Transform_Cache[Character][Joint] = Joint.Transform
			end
		end
	end
end

The code makes checks if the joints do exist so it was really simple to adapt

It will have an impact on performance over baseline. This is simply because you can’t make changes to every character in the game every frame without some level of impact. That can’t really be optimized out.

However, for most Roblox games it should be negligible.

But no, this won’t improve performance at all. It’s another layer on top of all of Roblox’s default rendering.

2 Likes

This is super awesome!! Now I feel so stupid for wasting so much time on making a whole set of animations in low fps :sob:

1 Like

Is there any way to make this work for R6 Characters?

This one works on R6 Characters

local RunService = game:GetService("RunService")


local CHARACTER_FRAMERATE    = 11
local CHARACTER_CHECK_DELAY  = 1

local Characters = {}
local Character_Transform_Cache = {}
local Last_Character_Get = os.clock() - 10
local Last_Read_Frame    = os.clock()

local Character_Joint_References = {
	{ Parent = "Torso", JointName = "Neck"           }, -- Head ↔ Torso
	{ Parent = "Torso", JointName = "Right Shoulder" }, -- Right Arm ↔ Torso
	{ Parent = "Torso", JointName = "Left Shoulder"  }, -- Left Arm ↔ Torso
	{ Parent = "Torso", JointName = "Right Hip"      }, -- Right Leg ↔ Torso
	{ Parent = "Torso", JointName = "Left Hip"       }, -- Left Leg ↔ Torso
}

local function getPrimaryPart(character)
	return character:FindFirstChild("Torso") 
		or character:FindFirstChild("HumanoidRootPart")
end

local function onScreen(cf)
	local cam = workspace.CurrentCamera
	local LV      = cam.CFrame.LookVector
	local toPart  = CFrame.new(cam.CFrame.p, cf.p).LookVector
	local dot     = LV:Dot(toPart)
	local fov     = cam.FieldOfView
	local solve   = ((125.787 - fov) / 0.0000279832) ^ 0.3022 * 0.01
	return dot > 0 and dot >= solve
end

local function getCharacters()
	Last_Character_Get = os.clock()
	Characters = {}
	for _, player in ipairs(game.Players:GetPlayers()) do
		local char     = player.Character
		local humanoid = char and char:FindFirstChild("Humanoid")
		local rootPart = char and getPrimaryPart(char)
		if humanoid and humanoid.Health > 0 and rootPart then
			table.insert(Characters, char)
		end
	end
end


local function readTransforms(character)
	Character_Transform_Cache[character] = {}
	for _, ref in ipairs(Character_Joint_References) do
		local part = character:FindFirstChild(ref.Parent)
		if part then
			local joint = part:FindFirstChild(ref.JointName)
			if joint then
				Character_Transform_Cache[character][joint] = joint.Transform
			end
		end
	end
end

local function writeTransforms()
	for character, jointTable in pairs(Character_Transform_Cache) do
		local rootPart = getPrimaryPart(character)
		if rootPart and onScreen(rootPart.CFrame) then
			for joint, savedTransform in pairs(jointTable) do
				if joint and joint.Parent then
					joint.Transform = savedTransform
				end
			end
		end
	end
end


local function Operate()
	if os.clock() - Last_Character_Get >= CHARACTER_CHECK_DELAY then
		getCharacters()
	end

	if os.clock() - Last_Read_Frame >= (1 / CHARACTER_FRAMERATE) then
		Last_Read_Frame = os.clock()
		for _, char in ipairs(Characters) do
			local rootPart = getPrimaryPart(char)
			if rootPart and onScreen(rootPart.CFrame) then
				readTransforms(char)
			end
		end
	else
		writeTransforms()
	end
end

RunService.Stepped:Connect(Operate)