Viewport camera systems have been a bit of an obsession of mine, as they can be really useful in games, but usually don’t end up being performant enough.
However, I don’t think this needs to remain the case. Recently, I made a viewport camera system that seems to not affect the player’s FPS at all in-game. In addition, it’s pretty straightforward in concept. It definitely can be fine-tuned a lot more, but from what I can tell from my tests, it’s definitely the best camera system I’ve seen yet.
Performance-wise, with 15 of these running simultaneously on the user, each cameras’ FPS drops to 4-5 FPS on my machine. The actual game, however, remained pinned at 60 FPS, meaning the user feels essentially no lag from this.
This camera system also fixes a lot of issues I noticed with previous systems I have played around with, especially around character models, accessories, and 3d clothing. This new system has essentially no issues with any of these, except a couple of slightly off CFrame placements in more complex Accessories that use Weld Constraints.
Link to the model:
Issues I'm aware of
I’m going to ignore any issues that are generally inherent with all Viewport systems
- Accessories with multiple parts using Weld Constraints sometimes have a weird offset applied to them (though not drastic)
- Any changes in part size, color, or other properties besides position and orientation are not reflected (though this would be a fairly easy addition)
- Parts that move visually without actually updating their CFrame will not be updated in the camera
- Parts that are destroyed won’t be destroyed in the camera (this is also likely a relatively easy addition)
- Parts won’t be updated if they are outside the range of the player
- It’s all client-sided, so there may be replication issues. In addition, if StreamingEnabled is being used, and the player hasn’t loaded the area the camera is in, nothing will be shown.
This is a pretty early stage project, so it’s not going to be fully fleshed out.
Feel free to critique the code; I’m well aware it’s not very clean.
Feel free to make different versions of this system or use it however you want. Just credit me somewhere in the code.
Code
resolution = 100 -- Size in studs of the cube to be rendered and updated around the player
fps = 30
fov = 90
static_map = false
static_camera = false
camera = game.Players.LocalPlayer.Character:WaitForChild("Head") -- The camera. Must have a valid CFrame
offset = -5 -- Offset. Default is -5, and places the actual camera 5 studs behind the camera part.
viewport = script.Parent.ViewportFrame
loaded_items = {}
frame_items = {}
humanoids = {}
frame_humanoids = {}
creation_time = math.round(tick()*1000000000) -- Give a unique identifier to each camera display.
function check_include(item)
if item.ClassName == "Part"
or item.ClassName == "BasePart"
or item.ClassName == "Attatchment"
or item.ClassName == "SurfaceAppearance"
or item.ClassName == "Weld"
or item.ClassName == "WeldConstraint"
or item.ClassName == "MeshPart"
or item.ClassName == "UnionOperation"
or item.ClassName == "Texture"
or item.ClassName == "Decal"
or item.ClassName == "Mesh"
or item.ClassName == "Accessory" then
return true
end
return false
end
function has_cframe(item)
if item.ClassName == "Part"
or item.ClassName == "BasePart"
or item.ClassName == "MeshPart"
or item.ClassName == "UnionOperation" then
return true
end
return false
end
function clean(object)
for _, item in object:GetDescendants() do
if check_include(item) == false then
item:Destroy()
end
end
return object
end
function update_cframes(model)
for _, item in model.clone:GetChildren() do
local corresponding_item = model.original:FindFirstChild(item.Name)
if corresponding_item == nil then
item:Destroy()
end
if has_cframe(item) then
item.CFrame = corresponding_item.CFrame
else
if item.ClassName == "Accessory" then
for _, item2 in item:GetChildren() do
if item2.ClassName == "Part" or item2.ClassName == "BasePart" or item2.ClassName == "MeshPart" or item2.ClassName == "UnionOperation" then
item2.CFrame = corresponding_item:FindFirstChild(item2.Name).CFrame
item2.Transparency = corresponding_item:FindFirstChild(item2.Name).Transparency
end
end
end
end
end
for _, item in model.original:GetChildren() do
local corresponding_item = model.clone:FindFirstChild(item.Name)
if corresponding_item == nil then
if check_include(item) then
item:Clone().Parent = model.clone
end
end
end
end
function castBox()
local parts = workspace:GetPartBoundsInBox(camera.CFrame, Vector3.new(resolution,resolution,resolution))
for _, part in parts do
if part:GetAttribute("ViewportCloned"..creation_time) then
frame_items[#frame_items + 1] = loaded_items[part:GetAttribute("ViewportCloned"..creation_time)]
else
local model = part:FindFirstAncestorWhichIsA("Model")
local archivable = model.Archivable
model.Archivable = true
if model then
if model:FindFirstChildWhichIsA("Humanoid") ~= nil then
if model:GetAttribute("ViewportCloned"..creation_time) then
update_cframes(humanoids[model:GetAttribute("ViewportCloned"..creation_time)])
frame_humanoids[#frame_humanoids + 1] = humanoids[model:GetAttribute("ViewportCloned"..creation_time)]
else
model:SetAttribute("ViewportCloned"..creation_time,#humanoids+1)
humanoids[#humanoids+1] = {clone = model:Clone(), original = model}
print(model:Clone())
print(humanoids)
humanoids[#humanoids].clone.Parent = viewport
frame_humanoids[#frame_humanoids + 1] = humanoids[#humanoids]
end
continue
end
end
part:SetAttribute("ViewportCloned"..creation_time,#loaded_items+1)
loaded_items[#loaded_items+1] = {clone = clean(part:Clone()), original = part}
loaded_items[#loaded_items].clone.Parent = viewport
frame_items[#frame_items + 1] = loaded_items[#loaded_items]
model.Archivable = archivable
end
if #frame_items > 0 then
frame_items[#frame_items].clone.CFrame = frame_items[#frame_items].original.CFrame
end
end
end
-- Mainloop
viewport.CurrentCamera = Instance.new("Camera", viewport)
viewport.CurrentCamera.FieldOfView = fov
while true do
local start = tick()
frame_items = {}
frame_humanoids = {}
viewport.CurrentCamera.CFrame = camera.CFrame + camera.CFrame.LookVector * offset
castBox()
wait()
local frame_time = (tick()-start)
if frame_time > 1/fps then
script.Parent.TextLabel.Text = "FPS: "..math.round((1/frame_time))
else
script.Parent.TextLabel.Text = "FPS: "..fps
wait((1/fps)-frame_time)
end
end