1. What do you want to achieve? I’m trying to get my mirror system (which uses viewport frame and a custom projection matrix) to work on horizontal surfaces like floors, just like it does for vertical walls.
2. What is the issue? The wall mirrors work seemingly fine, but when I try applying the current math to a floor part (top or bottom face), the projection just glitches or turn blank. Since looking down makes the camera’s LookVector run parallel to the floor’s normal, the calculations and cross-products seem to collapse.
3. What solutions have you tried so far? I’ve tried swapping out the look vector for camera-to-center vectors and tweaking the skew/scale variables, but every adjustment either warps the aspect ratio completely or inverts the viewport camera. (I’ve also found other posts trying to replicate this effect but none of them came to a solid solution.)
Here is the current script:
`Lua
local RunService = game:GetService(“RunService”)
local Players = game:GetService(“Players”)
local camera = workspace.CurrentCamera
local localPlayer = Players.LocalPlayer
local playerGui = localPlayer:WaitForChild(“PlayerGui”)
local UNIT_Y = Vector3.new(0, 1, 0)
local UNIT_NZ = Vector3.new(0, 0, -1)
local XZ = Vector3.new(1, 0, 1)
local YZ = Vector3.new(0, 1, 1)
local YSPIN = CFrame.fromEulerAnglesXYZ(0, math.pi, 0)
local PI2 = math.pi / 2
local portals = {
{part = workspace:WaitForChild(“PortalPart”), gui = playerGui:WaitForChild(“SurfaceGui”)},
{part = workspace:WaitForChild(“PP2”), gui = playerGui:WaitForChild(“SurfaceGui2”)},
{part = workspace:WaitForChild(“PP3”), gui = playerGui:WaitForChild(“SurfaceGui3”)},
{part = workspace:WaitForChild(“PP4”), gui = playerGui:WaitForChild(“SurfaceGui4”)},
}
local activeCharParts = {}
local activeCloneMap = {}
local function getSurfaceInfo(part, face)
local partCF, partSize = part.CFrame, part.Size
local back = -Vector3.FromNormalId(face)
local axis = (math.abs(back.Y) == 1) and Vector3.new(back.Y, 0, 0) or UNIT_Y
local right = CFrame.fromAxisAngle(axis, PI2) * back
local top = back:Cross(right).Unit
local cf = partCF * CFrame.fromMatrix(-back * partSize / 2, right, top, back)
local size = Vector3.new((partSize * right).Magnitude, (partSize * top).Magnitude, (partSize * back).Magnitude)
return cf, size
end
local function renderFrame(vpCamera, camCF, surfaceCF, surfaceSize)
local tc = surfaceCF * Vector3.new(0, surfaceSize.Y / 2, 0)
local bc = surfaceCF * Vector3.new(0, -surfaceSize.Y / 2, 0)
local cross = camCF.LookVector:Cross(surfaceCF.UpVector)
local right = cross:Dot(cross) > 0 and cross.Unit or camCF.RightVector
local levelCamCF = CFrame.fromMatrix(camCF.p, right, surfaceCF.UpVector, right:Cross(surfaceCF.UpVector))
local levelCamCFInv = levelCamCF:Inverse()
local csbc = levelCamCFInv * bc
local cstc = levelCamCFInv * tc
local v1 = (csbc * YZ).Unit
local v2 = (cstc * YZ).Unit
local alpha = math.sign(v1.Y) * math.acos(math.clamp(v1:Dot(UNIT_NZ), -1, 1))
local beta = math.sign(v2.Y) * math.acos(math.clamp(v2:Dot(UNIT_NZ), -1, 1))
local fh = 2 * math.tan(math.rad(camera.FieldOfView) / 2)
local hPrime = math.tan(beta) - math.tan(alpha)
local refHeight = hPrime / fh
local c2p = surfaceCF:VectorToObjectSpace(surfaceCF.p - camCF.p)
local c2pXZ = c2p * XZ
local c2pYZ = c2p * YZ
local dpX = c2pXZ.Unit:Dot(UNIT_NZ)
if math.abs(dpX) < 0.0001 then dpX = math.sign(dpX) * 0.0001 end
if dpX == 0 then dpX = 0.0001 end
local scale = (surfaceCF:VectorToObjectSpace(camCF.LookVector) * XZ).Unit:Dot(c2pXZ.Unit) / UNIT_NZ:Dot(c2pXZ.Unit)
local tanArcCos = math.sqrt(math.max(0, 1 - dpX * dpX)) / dpX
local w, h = 1, (surfaceSize.X / surfaceSize.Y)
local dx = math.sign(c2p.X * c2p.Z) * tanArcCos
local dy = c2pYZ.Y / c2pYZ.Z * h
local d = math.abs(scale * refHeight * h)
local ncf = (surfaceCF - surfaceCF.p) * YSPIN * CFrame.new(0, 0, 0, w, 0, 0, 0, h, 0, dx, dy, d)
local c = {ncf:GetComponents()}
local maxVal = 0
for i = 4, 12 do
maxVal = math.max(maxVal, math.abs(c[i]))
end
if maxVal > 1 then
for i = 4, 12 do
c[i] = c[i] / maxVal
end
ncf = CFrame.new(c[1], c[2], c[3], c[4], c[5], c[6], c[7], c[8], c[9], c[10], c[11], c[12])
end
vpCamera.FieldOfView = camera.FieldOfView
vpCamera.CFrame = ncf + camCF.p
end
local function getRelativePath(descendant, ancestor)
local path = {}
local current = descendant
while current and current ~= ancestor do
table.insert(path, 1, current.Name)
current = current.Parent
end
return path
end
local function setupCharacter(char)
table.clear(activeCharParts)
table.clear(activeCloneMap)
for _, part in ipairs(char:GetDescendants()) do
if part:IsA("BasePart") then
table.insert(activeCharParts, part)
end
end
for _, portal in ipairs(portals) do
portal.gui.CanvasSize = Vector2.new(1000, 1000)
portal.gui.ClipsDescendants = true
local viewportFrame = portal.gui:WaitForChild("ViewportFrame")
viewportFrame.Size = UDim2.new(1, 0, 1, 0)
viewportFrame.ClipsDescendants = true
local worldModel = viewportFrame:WaitForChild("WorldModel")
worldModel:ClearAllChildren()
local vpCamera = viewportFrame:FindFirstChildOfClass("Camera") or Instance.new("Camera")
vpCamera.Parent = viewportFrame
viewportFrame.CurrentCamera = vpCamera
char.Archivable = true
local charClone = char:Clone()
charClone.Parent = worldModel
for _, obj in ipairs(charClone:GetDescendants()) do
if obj:IsA("JointInstance") or obj:IsA("WeldConstraint") then
obj:Destroy()
end
end
activeCloneMap[portal] = {}
for _, clonePart in ipairs(charClone:GetDescendants()) do
if clonePart:IsA("BasePart") then
clonePart.Anchored = true
local path = getRelativePath(clonePart, charClone)
local realPart = char
for _, name in ipairs(path) do
if realPart then
realPart = realPart:FindFirstChild(name)
end
end
if realPart and realPart:IsA("BasePart") then
activeCloneMap[portal][realPart] = clonePart
end
end
end
portal.vpCamera = vpCamera
end
end
local function onPlayerAdded(targetPlayer)
if targetPlayer == localPlayer then
targetPlayer.CharacterAdded:Connect(function(newChar)
newChar:WaitForChild(“HumanoidRootPart”)
setupCharacter(newChar)
end)
if targetPlayer.Character and targetPlayer.Character:FindFirstChild("HumanoidRootPart") then
setupCharacter(targetPlayer.Character)
end
end
end
Players.PlayerAdded:Connect(onPlayerAdded)
for _, p in ipairs(Players:GetPlayers()) do
onPlayerAdded(p)
end
RunService.RenderStepped:Connect(function()
if not localPlayer.Character or not localPlayer.Character:FindFirstChild(“HumanoidRootPart”) then return end
local camCF = camera.CFrame
for _, portal in ipairs(portals) do
if portal.vpCamera then
portal.surfaceCF, portal.surfaceSize = getSurfaceInfo(portal.part, Enum.NormalId.Front)
renderFrame(portal.vpCamera, camCF, portal.surfaceCF, portal.surfaceSize)
local cloneParts = activeCloneMap[portal]
if cloneParts then
for _, realPart in ipairs(activeCharParts) do
local clonePart = cloneParts[realPart]
if clonePart then
clonePart.CFrame = realPart.CFrame
end
end
end
end
end
for _, part in ipairs(activeCharParts) do
part.LocalTransparencyModifier = 1
end
end)
`
Any ideas on how to tweak this math to properly support floor/horizontal surfaces without breaking the walls? ANY HELP WILL BE GREATLY APPRECIATED
Link to sample video: https://www.youtube.com/watch?v=dr8E9QRpMmQ
Link to sample game: Flat Characters | Play on Roblox

