New Viewport Camera System

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
6 Likes

i’m trying to use it - even with your base model it’s returning me an error ‘Players.cl0vermead0w.PlayerGui.Cam1View1.Frame.LocalScript:112: attempt to index nil with ‘Parent’’

i’m not the best scripter but im assuming this is a problem with the model and not me.

i have placed the model in a surfacegui in startergui with the surface gui having the adornee of a part in workspace - i’m assuming this is correct? i don’t know man it’s just returning an error

1 Like

Have you considered taking advantage of WorldModels for animated characters? Using them removes a ton of CFrame assignments, and it’s the one thing I see many ViewportFrame resources lack.

1 Like

Sorry for the late response, it was a pretty simple error where it wasn’t able to access models that had Archivable off (i.e. default humanoid models). I fixed this in a new update.

The main issue I’ve found with WorldModels is actually replicating all the animations over properly so that they match what the user is seeing. However, getting that working is currently the next major improvement.