The Fastest Camera System You'll Probably See Written For Roblox!

Fast-Camera-Systems

Probably the most optimized and fastest Camera you’ll find written for the Roblox engine. Pretty easy to use. Might write up an article explaining the math (if I’m not lazy)…

Get The Module Here!


Github

rbxm

FastCameraSystem.rbxm (13.5 KB)

Model

Fast-Camera-System - Roblox

Camera Modes

Over-The-Shoulder

image

Top-Down-Camera

Isometric-Camera

Side-Scrolling-Camera

Follow-Mouse

Usage

After downloading the rbxm file, drag it into StarterCharacterScripts.
Code is designed to be pretty self-documenting so documentation is pretty excessive.

local Module = require(script:WaitForChild("CameraSystem")) -- CameraModes should be placed under this.
Module.EnableTopDownCamera() -- Functions are pretty self-documenting
Module.EnableShiftLockCamera() -- etc

Camera mode settings can be adjusted in the Configs module if needed. Alternatively, if you have other ideas on ways of using my modules, then you can pass the needed configurations through function parameters.

Functions

EnableShiftLockCamera()
DisableShiftLockCamera()
EnableIsometricCamera()
DisableIsometricCamera()
EnableTopDownCamera()
DisableTopDownCamera()
EnableSideScrollingCamera()
DisableSideScrollingCamera()
FollowMouse()
StopFollowingMouse()
FaceCharacterToMouse()
StopFacingMouse()
HeadFollowCamera() -- (Experimental)
StopHeadFollowCamera()

Incentive

Over recent weeks, I’ve spent some time carefully deriving the math for faster cameras that I thought I would like to contribute to the community. As you should know from your game-dev adventures, camera design itself is essential to designing top-quality games. You want to do as much as you can to give your player that immersive feeling they deserve. The math behind cameras can be pretty math-heavy which is something important to consider as that math is coordinated per frame. This is pretty important to take into consideration for performance tolling games or tactical games where FPS is crucial. Currently, the current Roblox camera module doesn’t ideally achieve the speeds it should target. It’s often been looked at as a threshold to performance-needing games and from personal experience of my own performance-heavy mechanics. Optimization itself is compensation for expensive applications-- it’s the path to future-proofing. Thus, with the incentive of building a high-performant camera but also to provide practical camera functionality to the programmers who often find camera math to be challenging.

Design

Purely math-based cameras designed from scratch. Intended to be lightweight and flexible for future-proofing. All the math I wrote is carefully optimized in consideration of modern architectures and in consideration to lower-end architectures like ARM. For those who crave all the slightest optimizations possible, I made a separate CameraModes module (FastCameraModes) but I leave my cautions as readability is sacrificed considerably.

The challenge posed for deriving and writing a fast camera:

  • GC footprint: Luau’s GC (garbage collector) is notorious when it comes to cleaning up userdata creation or even table creations that may result in GC Assist instructions which will halt Luau scripts for this extra work needing to be done by the GC.
  • Consideration of Luau->C/C++ bridge invocations: Biggest bottleneck Roblox games face. It is extremely expensive to bridge data between C++ and Luau often making calculations in Luau much faster than they could be in C++.
  • Optimizing Expensive Calculations: Considering cases where components could cancel out other components and finding shortcut calculations.
  • Numerical Stability: Unfortunately, calculations in C++ have more precision than Luau but this is negligible, however, it is important to take into consideration of errors that may stem from this thus designing math to be more numerically robust is something that should always be noted.
  • Consideration of current compiler optimizations: It is important to make sure for example that upvalues should stay constant, otherwise, the compiler won’t be able to cache function closures which is a reasonable performance loss. If we can’t cache closures and avoid this, then it is best to cache function constants as upvalues otherwise.
  • Further target hardware considerations such as cache locality and branch prediction: Although from the perspective of Luau, the impact is minimal however we can still considerably influence this. When possible, division is avoided and multiplication is favored by multiplying ratios (1/x) instead as this is still faster on ARM but doesn’t hurt other architectures however bypass delays are also taken into consideration as inverse division leads to interleaving integer and floating operations.
191 Likes

Awesome, i will definitely be trying this out. Messing with the camera is a pain so this will really speed things up :+1:. A free model link would be useful though

2 Likes

Haha, good idea.
https://www.roblox.com/library/10130297552/Fast-Camera-System

11 Likes

This looks great, I was curious if each Camera was compatible with Controller & Mobile?

2 Likes

I believe it should work for mobile devices although not intentionally or specifically designed for any other platform other than PC. I designed the code to be pretty scalable so it may work for controllers…?

1 Like

A function for using a controllers right stick to change direction would make this module 10/10

4 Likes

This looks great! I recommend removing the “probably” from the topic title, it makes it more appealing! I also would recommend moving this to #resources:community-resources until the tutorial is complete!

2 Likes

Seems a bit silly to worry so much about how fast your camera is. Are there any cases where the performance of your camera is a major concern?

1 Like

On the contrary, camera calculations have to be ran usually at the beginning of every RenderStepped (Priority 200 to be exact), and while most camera systems usually don’t show any bottlenecking on most modern cpus, older mobile devices can struggle especially with hardware specific limitations

This resource accounts for small optimizations with SMID computing and generally quicker alternatives to bloaty math calculations (i.e. inverse multiplying vs normal division), which alleviates stress on user’s devices and overall lead to a more smoother experience

7 Likes

In terms of where performance for cameras can be a major concern is coding some expensive mechanic for a game that can be pretty tolling on FPS. A good example of this from experience was writing up some custom water. The calculations for the water were as optimized as they can get. The problem came down to some latency with userdata creation. Even when it comes to optimizing out userdatas, it was noted just how poor Roblox optimized their own scripts which turned out to be the true bottleneck. It’s actually pretty common for professional developers to rewrite custom game mechanics for Roblox. For example, it’s also common for some games to have their own physics engine specifically optimized for their games because the actual Roblox physics engine was that much of a bottleneck for them or even custom humanoids. Regardless, it doesn’t hurt to use a custom Camera that is more faster than Roblox’s camera as it just gives you more freedom of choice for mechanics you may want to add to your game. It’s the idea of compensating for resources that may be added in the future.

On a technical breakdown, a big problem relating to performance that was mentioned in the post under the design section was:

  • GC footprint: Luau’s GC (garbage collector) is notorious when it comes to cleaning up userdata creation or even table creations that may result in GC Assist instructions which will halt Luau scripts for this extra work needing to be done by the GC.
  • Consideration of Luau->C/C++ bridge invocations: Biggest bottleneck Roblox games face. It is extremely expensive to bridge data between C++ and Luau often making calculations in Luau much faster than they could be in C++.

As mentioned, userdata is somewhat often difficult for the GC to clean up which can have some performance impacts. On a frame-per-frame breakdown, most of your userdata creation will most likely stem from the default Roblox camera scripts. The math isn’t the best and many people actually despise the modules itself. There are plenty of calls to the C++ side to bridge back data such as CFrames for needed math or thereof. In the context of CFrame specifically, some people may argue it’s sort of a lazy way of working with matrix math which is true considering doing matrix math out by hand takes some effort, however, you can actually simplify most matrix calculations and only use a single CFrame object when needed to update an object’s coordinate frame. For example:

local _ = CFrame.new(...) * CFrame.Angles(...) -- Common practice

Alternatively, this can be simplified into just one CFrame. This was pretty much the basis of optimizations that were considered for the camera. In terms of raw-computing speed, some of these calculations that Roblox may do for their matrix calculations can sometimes be further simplified given certain cases so you can compute some math faster than what they could be in C++ but it is important to mention that Roblox does have some benefits from hardware-related optimizations such as inverting a CFrame so it’s important to benchmark when it comes to uncertainty.

It’s always good to consider expensive operations in your main loops and how you may optimize them. @maycoleee2231 Does a nice job explaining the rest.

5 Likes

I see. I’m guessing the reason why it’s difficult for me to track this performance penalty of using the default camera script is because most of the price is paid in GC, which is as opaque as it gets for performance tracking. If the proposed change to making CFrame a native type goes through eventually, this difference will become negligible if I’m not mistaken.

I suppose until then, your camera system is indeed a big boost. I don’t think most people are that concerned about performance, but as a proof of concept, it is well done. Nice!

I have no idea what any of this code means, but it looks awesome! Thanks for your contribution <3

I think this is something I’d look into for my own games.

1 Like

:raised_hand:
me

This is an awesome resource and really nicely written too.

Could you provide a little more clarification as to the difference between the CameraSystem and FastCameraSystem? Is it just that FastCameraSystem is more optimized, but less readable (or is it something more)?

1 Like

What function does the normal camera system use that is not present in Luau? I’m curious.

Also, you mention it’s faster, but you don’t exactly tell us the difference.

YEAH, this looks good, it’s certainly very useful. After activating your system can you move the camera to some object?

Yes just as you assumed, I probably forgot to explain the other file but it’s most likely explained in the GitHub. The faster version is considerably faster but less readable but the original version already achieves reasonable speeds which is why I decided to still keep a saner version.

Wow this module is really nice

Is there a way for you to implement head follow cursor much like head follow camera?

Very awesome resource, although is R6 compatibility planned anytime soon?

The normal camera system uses the C++ library that isn’t native to Luau, such as the CFrame library or needing to index C++ instances. This all requires bridging data between Lua and C++.

It’s hard to present accurate benchmarks with Roblox’s camera system and mine because of how constrained Roblox’s player module is but I can give you an analysis of the math they use to put the perspective of how much faster my module is.

Here is a somewhat rough yet most basic example: say I wanted to build a rotation matrix from only the x-axis and probably want to multiply some existing transformation by this rotation. Traditionally, we would do something like CFrame.angles(x, 0, 0) or equivalently fromEulerAnglesXYZ(x, 0, 0). However, given the case where we only need rotation off the X-Axis, we can simplify the actual calculations for the rotation matrix given that we have to take the cosine and sine of the other axes like so:

local function AnglesXYZ(x, y, z)
	local m11 = cos(y) * cos(z)
	local m12 = -cos(y) * sin(z)
	local m13 = sin(y)
	local m21 = cos(z) * sin(x) * sin(y) + cos(x) * sin(z)
	local m22 = cos(x) * cos(z) - sin(x) * sin(y) * sin(z) 
	local m23 = -cos(y) * sin(x)
	local m31 = sin(x) * sin(z) - cos(x) * cos(z) * sin(y)
	local m32 = cos(z) * sin(x) + cos(x) * sin(y) * sin(z)
	local m33 = cos(x) * cos(y)

	return CFrame.new(0, 0, 0, m11, m12, m13, m21, m22, m23, m31, m32, m33)
end

Because of our known axes (x, 0, 0) we don’t have to waste precious computations calculating their sin and cosign since we know sin(1) = 0 and cos(1) = 0 now we can start to simplify:

Simplifying
--local function AnglesX(x)
--	local m11 = cos(0) * cos(0)
--	local m12 = -cos(0) * sin(0)
--	local m13 = sin(0)
--	local m21 = cos(0) * sin(x) * sin(0) + cos(x) * sin(0)
--	local m22 = cos(x) * cos(0) - sin(x) * sin(0) * sin(0) 
--	local m23 = -cos(0) * sin(x)
--	local m31 = sin(x) * sin(0) - cos(x) * cos(0) * sin(0)
--	local m32 = cos(0) * sin(x) + cos(x) * sin(0) * sin(0)
--	local m33 = cos(x) * cos(0)

--	return CFrame.new(0, 0, 0, m11, m12, m13, m21, m22, m23, m31, m32, m33)
--end

--local function AnglesX(x)
--	local m11 = 1 * 1
--	local m12 = -1 * 0
--	local m13 = 0
--	local m21 = 1 * 0 * 0 + cos(x) * 0
--	local m22 = cos(x) * 1 - sin(x) * 0 * 0
--	local m23 = -1 * sin(x)
--	local m31 = sin(x) * 0 - cos(x) * 1 * 0
--	local m32 = 1 * sin(x) + cos(x) * 0 *0
--	local m33 = cos(x) * 1

--	return CFrame.new(0, 0, 0, m11, m12, m13, m21, m22, m23, m31, m32, m33)
--end

--local function AnglesX(x)
--	local m11 = 1
--	local m12 = 0
--	local m13 = 0
--	local m21 = 0
--	local m22 = cos(x)
--	local m23 = -sin(x)
--	local m31 = 0
--	local m32 = sin(x)
--	local m33 = cos(x)

--	return CFrame.new(0, 0, 0, m11, m12, m13, m21, m22, m23, m31, m32, m33)
--end

Yields us:

local function AnglesX(x)
	local Cosx = math.cos(x)
	local Sinx = math.sin(x)

	return CFrame.new(0, 0, 0, 1, 0, 0, 0, Cosx, -Sinx, 0 ,Sinx, Cosx)
end

In all it’s glory, we are left with just this however if you are a mathy person you could probably have derived this on your own without even trying to think of the rotation matrix by working in a 2D perspective. Building a full CFrame however isn’t as useful to us and barely gives us a speed up. As the example given:

want to multiply some existing transformations by this rotation

We are actually more interested in just Cosx and Sinx which we can actually directly multiply now with a transformation. The instinctive minds however here will notice that just a X-Axis rotation matrix is mostly composed of 0s and a 1 component meaning we have more cool shortcuts to do, the fun never ends!

Now, from all of this rotation matrix fun, we have eliminated a CFrame creation since we don’t have to do something like: Transformation*CFrame.Angles(...) we can just multiply only 4 components and cancel other components out. In just the rotation matrix alternative, we have actually just speed up our math by roughly 7467.8%!

See for yourself

Rotation Math Example Benchmark
local Clock = os.clock()
for i = 1, 100  do
	local Cosx = math.cos(i)
	local Sinx = math.sin(i)

	local m11 = 1
	local m12 = 0
	local m13 = 0
	local m21 = 0
	local m22 = Cosx
	local m23 = -Sinx
	local m31 = 0
	local m32 = Sinx
	local m33 = Cosx
	
end
print(os.clock()-Clock, "Faster Rotation")


local Clock2 = os.clock()
for i = 1, 100  do
	local _ = CFrame.Angles(i, 0, 0)
end
print(os.clock()-Clock2, "CFrame Rotation")
3 Likes

what one is over the shoulders?

2 Likes