Make contents of a physical ViewportFrame in the workspace match the screen

This is confusing to explain, but I’ll try my best.

I want to display a model through a ViewportFrame, and make it look normal, just like how any other object in the workspace would be rendered. I could use a ViewportFrame in a ScreenGui, set the size to fill the screen and then set the CurrentCamera property to the workspace camera, HOWEVER this ViewportFrame will draw over everything else in the workspace (since it’s a 2d element), which is not what I want.

So, I decided to place a physical viewport in the workspace with a SurfaceGui so that the ViewportFrame won’t render over objects in the workspace.

The grey cube is a Part inside a ViewportFrame which is inside a SurfaceGui, which is adornee’d to a Flat Plane Mesh in workspace. The flat mesh is always rotated to face the camera (basically emulating a billboardgui)

The purple cube is a Part inside the workspace which serves as a frame of reference for what the ViewportFrame cube should look like.

The problem is that the cube inside the viewport doesn’t always match the one in workspace, like the perspective seems off. What maths would I need to use to make the model inside the viewport match the real one in workspace?

The code I have so far, which works but is still off.

RunService.RenderStepped:Connect(function()
	local X, Y, Z = Camera.CFrame:ToEulerAnglesXYZ()
	
	-- this is to rotate the plane to constantly face the camera (emulate billboardgui)
	Plane.CFrame = CFrame.new(Cube.Position) * CFrame.Angles(X, Y, Z)
	Plane.CFrame *= CFrame.Angles(math.pi/2, math.pi/2, 0)
	
	-- calculate the CFrame of the Viewport Camera
	local VirtCFrame = VPF.Part.CFrame * CFrame.Angles(X, -Y, Z) * CFrame.new(0,0,-9)
	VirtualCam.CFrame = CFrame.lookAt(VirtCFrame.Position, VPF.Part.Position)
end)

Right now I’m just setting the rotation of the viewport camera to the same as the actual workspace camera, and the position is the position of the cube + an arbitrary offset. This obviously isn’t right, but I’m not mathematically knowledgable enough to know how to calculate the correct solution.

This is another problem which is the same as mine, however was left unanswered

Bumping this. It’s hard to figure out the maths.

You can use EgoMoose’s ViewportFrame portal math. Here’s an example of how it can be used to realistically transform the ViewportFrame and its camera to match up with the cube in workspace:

local RunService = game:GetService("RunService")

local cam = workspace.CurrentCamera
local origin = -- Attachment which is the origin of the flat plane (can also be a Vector3 with minor edits to code)
local plane = -- Flat plane mesh
local surfaceGui = -- SurfaceGui
local VPF = -- ViewportFrame
local VPFCam = Instance.new("Camera", VPF)
VPF.CurrentCamera = VPFCam

-- Edited version of EgoMoose's portal ModuleScript

local FOV120 = math.rad(120)

local function UpdatePlane()
	local camCF = cam.CFrame
	local surfaceCF = plane.CFrame
	local surfaceSize = Vector2.new(plane.Size.X, plane.Size.Y)

	local rPoint = surfaceCF:PointToObjectSpace(camCF.Position)
	local sX, sY = rPoint.X / surfaceSize.X, rPoint.Y / surfaceSize.Y

	local scale = 1 + math.max(
		surfaceSize.Y / surfaceSize.X, 
		surfaceSize.X / surfaceSize.Y, 
		math.max(math.abs(sX), math.abs(sY))*2
	)

	local height = surfaceSize.Y/2
	local rDist = (camCF.Position - surfaceCF.Position):Dot(surfaceCF.LookVector)
	local newFov = 2 * math.atan2(height, rDist)
	local clampedFov = math.clamp(math.deg(newFov), 1, 120)
	local pDist = height / math.tan(math.rad(clampedFov) / 2)
	local adjust = rDist / pDist

	local factor = (newFov > FOV120 and adjust or 1) / scale
	local scaleCF = CFrame.new(0, 0, 0, factor, 0, 0, 0, factor, 0, 0, 0, 1)

	VPF.Position = UDim2.new(VPF.AnchorPoint.X - sX, 0, VPF.AnchorPoint.Y - sY, 0)
	VPF.Size = UDim2.new(scale, 0, scale, 0)

	VPFCam.FieldOfView = clampedFov
	VPFCam.CFrame = CFrame.new(camCF.Position) * (surfaceCF - surfaceCF.Position) * CFrame.Angles(0, math.pi, 0) * scaleCF
end

RunService.RenderStepped:Connect(function()
	plane.CFrame = CFrame.lookAlong(origin.WorldPosition, -cam.CFrame.LookVector)
	UpdatePlane()
end)
2 Likes

This is exactly the piece of maths I needed, thank you so much.

Note for anyone in the future: Use BindToRenderStep with a priority before the camera instead of RunService.RenderStepped

1 Like

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