Portal Cropping Using ViewportFrames (ClipsDescendants issue)

I am trying to crop out a ViewportFrame by the front face of a part using a bunch of frames parented to each other and rotating each to cut from each edge of the part.

I have the rotation bit down but I ran into a problem with ClipsDescendants.
If the rotation of a Frame is NOT equal to 0 then ClipsDescendants does nothing. This includes a rotation of 360 degrees which probably means that this is intentional.

Here is my script. I know it’s a complete mess but I wanted to first get the script done before writing and finalizing my final product:

Script
local scrGui = script.Parent.ScreenGui
local bbGui = script.Parent.BillboardGui
local function scrSize()
	return scrGui.AbsoluteSize
end
local function get3DBounds(part)
	local ctr = part.CFrame*(workspace.CurrentCamera.CFrame-workspace.CurrentCamera.CFrame.p)
	
	local box = {}
	for xm=-1, 1, 2 do
		for ym=-1, 1, 2 do
			for zm=-1, 0, 1 do
				local v = Vector3.new(0.5, 0.5, 0.5)*Vector3.new(xm, ym, zm)*part.Size
				local cf = ctr*CFrame.new(v.X, v.Y, v.Z)
				local pos = workspace.CurrentCamera:WorldToScreenPoint(cf.p)
				table.insert(box, pos)
			end
		end
	end
	return unpack(box)
end
local function abolutize(frm)
	local frm2 = Instance.new("Frame")
	frm2.BackgroundTransparency = 1
	frm2.Size = UDim2.new(1, 0, 1, 0)
	local par = frm.Parent
	frm2.Rotation = -par.AbsoluteRotation
	frm.Parent = frm2
	frm2.Parent = par
end
local function getFramesForBounds(a, b, c, d)
	--[[local yA = math.min(a.Y, b.Y, c.Y, d.Y)
	local yB = math.max(a.Y, b.Y, c.Y, d.Y)
	local xA = math.min(a.X, b.X, c.X, d.X)
	local xB = math.max(a.X, b.X, c.X, d.X)
	
	local y2A = math.max(yA-a.Y, yA-b.Y, yA-c.Y, yA-d.Y)]]
	--[[local p1 = Vector2.new(xA, yA)
	local p2 = Vector2.new(xB, yA)
	local p3 = Vector2.new(xA, yB)
	local p4 = Vector2.new(xB, yB)]]
	
	--[[local fr1 = Instance.new("Frame")
	fr1.Size = UDim2.new(0, xB-xA, 0, yB-yA)
	fr1.Position = UDim2.new(0, xA, 0, yA)]]
	
	local topLeft
	local topRight
	local bottomLeft
	local bottomRight
	local pts = {a, b, c, d}
	
	local leftMost
	local rightMost
	local topMost
	local bottomMost
	
	local leftMost2
	local rightMost2
	local topMost2
	local bottomMost2
	for _, pt in ipairs(pts) do
		if not leftMost then
			leftMost = pt
		end
		if not rightMost then
			rightMost = pt
		end
		if not topMost then
			topMost = pt
		end
		if not bottomMost then
			bottomMost = pt
		end
		if pt.X <= leftMost.X then
			leftMost2 = leftMost
			leftMost = pt
		end
		if pt.X >= rightMost.X then
			rightMost2 = rightMost
			rightMost = pt
		end
		if pt.Y <= topMost.Y then
			topMost2 = topMost
			topMost = pt
		end
		if pt.Y >= bottomMost.Y then
			bottomMost2 = bottomMost
			bottomMost = pt
		end
	end
	if topMost.X < topMost2.X then
		topLeft = topMost
		topRight = topMost2
	else
		topRight = topMost
		topLeft = topMost2
	end
	if bottomMost.X < bottomMost2.X then
		bottomLeft = bottomMost
		bottomRight = bottomMost2
	else
		bottomRight = bottomMost
		bottomLeft = bottomMost2
	end
	--[[for _, pt in ipairs(pts) do
		if not topLeft then
			topLeft = pt
		end
		if not topRight then
			topRight = pt
		end
		if not bottomLeft then
			bottomLeft = pt
		end
		if not bottomRight then
			bottomRight = pt
		end
		if pt.X < topLeft.X and pt.Y < topLeft.Y then
			topLeft = pt
		end
		if pt.X > topRight.X and pt.Y < topRight.Y then
			topRight = pt
		end
		if pt.X < bottomLeft.X and pt.Y > bottomLeft.Y then
			bottomLeft = pt
		end
		if pt.X > bottomRight.X and pt.Y > bottomRight.Y then
			bottomRight = pt
		end
	end]]
	
	local top = Vector2.new(topLeft.X/2+topRight.X/2, topLeft.Y/2+topRight.Y/2)
	local bottom = Vector2.new(bottomLeft.X/2+bottomRight.X/2, bottomLeft.Y/2+bottomRight.Y/2)
	local left = Vector2.new(topLeft.X/2+bottomLeft.X/2, topLeft.Y/2+bottomLeft.Y/2)
	local right = Vector2.new(topRight.X/2+bottomRight.X/2, topRight.Y/2+bottomRight.Y/2)
	
	
	
	local angleLeft = math.atan2(topLeft.Y-left.Y, left.X-topLeft.X)--math.atan2(bottomLeft.Y-topLeft.Y, topLeft.X-bottomLeft.X)
	local angleRight = math.atan2(topLeft.Y-right.Y, right.X-topLeft.X)--math.atan2(bottomRight.Y-topRight.Y, topRight.X-bottomRight.X)
	local angleTop = math.atan2(topLeft.Y-top.Y, top.X-topLeft.X)--math.atan2(topLeft.Y-topRight.Y, topLeft.X-topRight.X)
	local angleBottom = math.atan2(topLeft.Y-bottom.Y, bottom.X-topLeft.X)--math.atan2(bottomLeft.Y-bottomRight.Y, bottomLeft.X-bottomRight.X)
	
	local trans = 1
	local frmLeft = Instance.new("Frame")
	frmLeft.Size = UDim2.new(1, 0, 1, 0)
	frmLeft.Rotation = math.deg(angleLeft)
	frmLeft.Position = UDim2.new(0, topLeft.X, 0, topLeft.Y)
	frmLeft.ClipsDescendants = true
	frmLeft.BackgroundTransparency = trans
	
	local frmRight = Instance.new("Frame")
	frmRight.Size = UDim2.new(1, 0, 1, 0)
	frmRight.Parent = frmLeft
	abolutize(frmRight)
	frmRight.Rotation = math.deg(angleRight)
	frmRight.Position = UDim2.new(0, topRight.X-topLeft.X, 0, topRight.Y-topLeft.Y)
	frmRight.ClipsDescendants = true
	frmRight.BackgroundTransparency = trans
	frmRight.BackgroundColor3 = Color3.new(1, 0, 0)
	
	local frmTop = Instance.new("Frame")
	frmTop.Size = UDim2.new(1, 0, 1, 0)
	frmTop.Parent = frmRight
	abolutize(frmTop)
	frmTop.Rotation = math.deg(angleTop)
	frmTop.Position = UDim2.new(0, topLeft.X-topRight.X, 0, topLeft.Y-topRight.Y)
	frmTop.ClipsDescendants = true
	frmTop.BackgroundTransparency = trans
	frmTop.BackgroundColor3 = Color3.new(0, 1, 0)
	
	local frmBottom = Instance.new("Frame")
	frmBottom.Size = UDim2.new(1, 0, 1, 0)
	frmBottom.Parent = frmTop
	abolutize(frmBottom)
	frmBottom.Rotation = math.deg(angleBottom)
	frmBottom.Position = UDim2.new(0, 0, 0, 0)
	frmBottom.ClipsDescendants = true
	frmBottom.BackgroundTransparency = trans
	frmBottom.BackgroundColor3 = Color3.new(0, 0, 1)
	
	return frmLeft, frmBottom
end

local frame = scrGui.ViewportFrame
local cropF = bbGui.Frame
local crop = cropF:Clone()
local subj = bbGui.Adornee
local crop2, btm
game:GetService("RunService").RenderStepped:Connect(function()
	if crop2 then
		frame.Position = UDim2.new()
		frame.Rotation = 0
		frame.Parent = nil
		crop2:Destroy()
	end
	crop2, btm = getFramesForBounds(get3DBounds(subj))
	
	frame.Size = UDim2.new(0, scrSize().X, 0, scrSize().Y)
	frame.CurrentCamera = workspace.CurrentCamera
	crop.Parent = scrGui
	crop.Size = UDim2.new(0, cropF.AbsoluteSize.X, 0, cropF.AbsoluteSize.Y)
	local pos = workspace.CurrentCamera:WorldToScreenPoint(subj.CFrame.p)
	--crop.Position = UDim2.new(0, pos.X-cropF.AbsoluteSize.X/2, 0, pos.Y-cropF.AbsoluteSize.Y/2)
	--frame.Position = UDim2.new(0, -crop.AbsolutePosition.X, 0, -crop.AbsolutePosition.Y)
	crop2.Parent = crop
	frame.Parent = btm
	abolutize(frame)
	frame.Position = UDim2.new(0, -btm.AbsolutePosition.X, 0, -btm.AbsolutePosition.Y)
	--frame.Position = UDim2.new(0, -frame.AbsolutePosition.X, 0, -frame.AbsolutePosition.Y)
end)

I have no idea how to achieve the cropping effect I’d like because of this issue… If anyone has any ideas or knows how I can achieve this effect, please let me know!

Edit 2:
The red box is the area that is incorrect… The green box is the desired area
image
I essentially want to create four boxes which all cover the green area but my current script gives me the red box.

Edit: ClipsDescendants should work with rotated UI elements

5 Likes

Glass parts could serve as an alternative for cropping.

Most in-world based gui instances won’t render the portion where the glass overlaps.

I was hoping for a solution other than glass since Roblox stated they intend to fix this later… I may end up having to rely on this for now though since ClipsDescendants isn’t an option and I can’t even get the rotations right for now.

I am having the exact same problem over here!
I need to make it so that only the area of the BillboardGUI that is over a part, is visible.

Like that, only the portion of the Billboard GUI that appears to be over the RED part should be visible.

Oh also, cropping the GUI using frames? I would have never thought about that tbh! I’ve been trying to use glass blocks.

Are you sure a surface gui couldn’t work if you moved the camera?

Yep, it won’t work.

1 Like

Do you mean something like this?

9 Likes

Yup, not possible using surface GUI’s.

1 Like

Yes, this is the effect I want… I’m trying to not use glass since Roblox said not to rely on this.

2 Likes

How did you do this? I would love to know.

1 Like

In the video, this is accomplished by making the billboardGui cover the entire screen, and setting it a few studs in front of the camera.

Then, the viewport contains the part of the world you want to render, with the viewport camera being constantly updated to be relative to the player’s camera

After that, the glass (2000 x 2000 x 0.1) is arranged in a pattern (like shown in the video), and relocated to a plane right in front of the camera, parallel to the normal of the glass.

4 Likes

This is really cool! Mind explaining a little bit more?

1 Like

Sure. What would you like more info on?

1 Like

Before you use the glass trick, just remember that glass also blocks particles, water, billboard/surface guis, neon glow, and semi-transparent parts from being rendered.

3 Likes

How did you update it to be relative to the player’s camera, I am not very good at maths.

Are you talking about the billboardGui, the glass, or the viewport camera?

1 Like

The viewport camera

Example
So lets say that we have the workspace Camera, A, B, and we want the viewport Camera’s CFrame to be like in the picture.

First have to find what the CFrame of the workspace Camera is, relative to the CFrame of part A, or simply “line1” as shown in the picture.

Roblox already has built in CFrame functions to accomplish this, specifically :toObjectSpace(). So, line1 would be partA.CFrame:toObjectSpace(workspaceCamera.CFrame).

Next, we want to transform it to in terms of B. Line 2 is going to be the same as line 1 since we want the viewport Camera in terms of B to be the same as the workspace Camera in terms of A. So all you really need to do next is to apply line1 (the value we got from before) to partB’s CFrame, or basically partB.CFrame * line1

In summary, to get the viewport Camera thats needed, it would be the following:
viewportCamera.CFrame = partB.CFrame * partA.CFrame:toObjectSpace(workspaceCamera.CFrame)

(Yes, you could do viewportCamera.CFrame = partB.CFrame * (partA.CFrame:Inverse() * workspaceCamera.CFrame). The former was just easier to explain.)

10 Likes

Thank you!

So part A is the part the viewportframe is on?