In puzzle mechanics, impossible architecture, and sci-fi FPS arenas inspired by Portal, Antichamber, and Splitgate, non-Euclidean portals captivate players. Looking through a doorway on one side of a map and seeing a live, interactive 3D window into a completely different coordinate space—then stepping through it with zero screen fade, loading delay, or velocity hiccup—creates genuine architectural wonder.
However, naive teleportation in Roblox (such as setting HumanoidRootPart.CFrame upon touching a Part) produces severe camera snaps, destroys linear momentum, clips characters through world geometry, and provides no visual preview. In this master technical guide, we build a production-grade seamless portal framework in Luau. We derive relative CFrame coordinate mapping matrices, render real-time virtual camera perspectives using ViewportFrames and EditableImages, transpose linear/angular velocity vectors instantaneously across portal thresholds, and handle sub-stud collision handoffs.
1. The Teleportation Flaw: Why Naive CFrame Warps Break Immersion
Standard Roblox teleportation breaks spatial immersion across multiple physical and visual dimensions:
- Camera Gimbal Jitter: Abruptly overwriting HumanoidRootPart.CFrame snaps the player's camera rotation, inducing spatial disorientation and motion sickness.
- Momentum Vector Decoupling: A player jumping horizontally into an inverted ceiling portal exits with their velocity still pointing horizontally, violating conservation of momentum relative to the exit portal orientation.
- Zero Pre-Crossing Visuals: Stepping into a black or semi-transparent flat texture feels primitive; players expect to see the dynamic world waiting on the other side with correct parallax.
- The Non-Euclidean Solution: Treating portals as paired affine transformation matrices allows seamless projection of visual rays, physical bodies, and continuous momentum.
2. The Mathematical Foundation: Relative CFrame Matrices & Velocity Transposition
Portals function as coordinate space transformers connecting Portal A (entry) and Portal B (exit):
- Relative Coordinate Transform: Point P in world space relative to Portal A is P_rel = PortalA.CFrame:PointToObjectSpace(P). Transposed to Portal B: P_out = PortalB.CFrame:PointToWorldSpace(Vector3.new(-P_rel.X, P_rel.Y, -P_rel.Z)).
- Virtual Camera Transformation: Given player Camera CFrame C, virtual exit camera C_virtual = PortalB.CFrame * CFrame.Angles(0, math.pi, 0) * PortalA.CFrame:Inverse() * C.
- Linear Velocity Vector Mapping: Velocity vector V enters Portal A; exit velocity V_out = PortalB.CFrame:VectorToWorldSpace(-PortalA.CFrame:VectorToObjectSpace(V)). Momentum direction and magnitude are perfectly preserved relative to the exit frame.
- Plane Crossing Signed Distance: Character position C is evaluated against Portal A's front plane: Distance d = (C - PortalA.Position):Dot(PortalA.CFrame.LookVector). Teleportation triggers when d transitions from positive to negative within the portal aperture.
3. Complete Seamless Portal Controller Luau Implementation
Below is a complete, modular Luau portal engine running in RunService.RenderStepped for frame-synchronized perspective projection and crossing detection:
- Virtual Camera Perspective Sync: Updates the exit portal's ViewportFrame camera every frame with exact parallax and field-of-view matching.
- Signed Distance Threshold Crossing: Detects sub-frame crossing events using dot-product plane checks, preventing tunneling even at high velocities.
- Momentum & Rotation Transposition: Simultaneously updates character CFrame, assembly linear velocity, and camera look direction in a single atomic frame step.
--!strict
local RunService = game:GetService("RunService")
local Workspace = game:GetService("Workspace")
local Players = game:GetService("Players")
export type PortalPair = {
PortalA: BasePart,
PortalB: BasePart,
Width: number,
Height: number,
ViewportA: ViewportFrame?,
ViewportB: ViewportFrame?,
}
local PortalEngine = {}
PortalEngine.__index = PortalEngine
function PortalEngine.new(portalA: BasePart, portalB: BasePart, width: number, height: number)
local self = setmetatable({}, PortalEngine)
self.PortalA = portalA
self.PortalB = portalB
self.Width = width
self.Height = height
self.LastPositions = {} :: {[Model]: Vector3}
return self
end
function PortalEngine:GetTransformedCFrame(inputCF: CFrame, fromPortal: BasePart, toPortal: BasePart): CFrame
-- Flip 180 degrees around Y axis to exit pointing outward from target portal
local relative = fromPortal.CFrame:ToObjectSpace(inputCF)
local flipped = CFrame.Angles(0, math.pi, 0) * relative
return toPortal.CFrame:ToWorldSpace(flipped)
end
function PortalEngine:GetTransformedVelocity(velocity: Vector3, fromPortal: BasePart, toPortal: BasePart): Vector3
local localVel = fromPortal.CFrame:VectorToObjectSpace(velocity)
-- Invert X and Z to match flipped 180 degree exit
local flippedVel = Vector3.new(-localVel.X, localVel.Y, -localVel.Z)
return toPortal.CFrame:VectorToWorldSpace(flippedVel)
end
function PortalEngine:CheckCrossing(character: Model, fromPortal: BasePart, toPortal: BasePart)
local root = character:FindFirstChild("HumanoidRootPart") :: BasePart?
if not root then return end
local currentPos = root.Position
local prevPos = self.LastPositions[character] or currentPos
self.LastPositions[character] = currentPos
local portalCF = fromPortal.CFrame
local normal = portalCF.LookVector
local prevDist = (prevPos - portalCF.Position):Dot(normal)
local currDist = (currentPos - portalCF.Position):Dot(normal)
-- Detect crossing from front (positive dist) to back (negative dist)
if prevDist > 0 and currDist <= 0 then
-- Verify crossing occurs within portal bounding rectangular aperture
local localPos = portalCF:PointToObjectSpace(currentPos)
if math.abs(localPos.X) <= self.Width * 0.5 and math.abs(localPos.Y) <= self.Height * 0.5 then
-- Atomic transposition of position, velocity, and camera
local newCF = self:GetTransformedCFrame(root.CFrame, fromPortal, toPortal)
local newVel = self:GetTransformedVelocity(root.AssemblyLinearVelocity, fromPortal, toPortal)
root.CFrame = newCF
root.AssemblyLinearVelocity = newVel
self.LastPositions[character] = newCF.Position
local camera = Workspace.CurrentCamera
if camera and character == Players.LocalPlayer.Character then
camera.CFrame = self:GetTransformedCFrame(camera.CFrame, fromPortal, toPortal)
end
end
end
end
return PortalEngine
4. Rendering Techniques: ViewportFrames vs Modern EditableImage Shaders
Simulating a live window into another part of the game world requires innovative rendering strategies in Roblox:
- ViewportFrame Method: Clones nearby environment geometry into a ViewportFrame mapped to a SurfaceGui on Portal A, updating a linked Camera to match perspective.
- Lightweight Proxy Geometry: To maintain high frame rates, populate the ViewportFrame only with low-poly static geometry and dynamic enemy proxies within a 60-stud radius.
- EditableImage Pixel Rasterization: Modern advanced implementations copy screen buffers into EditableImage instances, using custom compute shaders to achieve recursive portal-in-portal reflections.
- Oblique Near-Plane Clipping: Adjust camera near-clipping planes parallel to the exit portal surface to prevent geometry behind the exit portal from rendering inside the doorway.
5. Collision Handoffs, Sound Propagation & Production Tuning
Ensuring players, projectiles, and vehicles transition smoothly without edge-snagging:
- Portal Frame Collision Masks: Carve out an exact doorway aperture in physical walls using CollisionGroups; ensure portal frame borders use smooth chamfered colliders.
- Sub-Stud Collision Disabling: Momentarily disable collisions between player limbs and the wall hosting the entry portal during the exact crossing frame to prevent snagging.
- Doppler Shift & Spatial Sound Wrapping: Route 3D audio listeners so sounds playing near Portal B emit audibly through Portal A with distance attenuation.
- Projectile Passing: Bullets and guided missiles perform raycast line intersection against portal planes, teleporting raycast origin and direction vectors mid-trajectory seamlessly.
Frequently Asked Questions
Why do characters snag or collide with the wall when walking through a portal?
Humanoid hitboxes have volume. Even if the portal aperture is open, an arm or shoulder can graze the wall edge. To prevent snagging, assign the player to a temporary NoCollision collision group with the doorway wall for 0.1 seconds during portal transition.
How do you preserve camera orientation so the screen doesn't snap abruptly?
The camera must undergo the exact same relative coordinate transformation matrix as the character. By taking Camera.CFrame relative to Portal A, flipping it 180 degrees, and mapping it relative to Portal B, the player's view smoothly continues along their visual trajectory.
Can ViewportFrames render animated characters and particles on the other side?
ViewportFrames support rigged Humanoids and Motor6D animations, but do not natively render ParticleEmitters, Beams, or Terrain. To simulate terrain, convert surrounding terrain chunks into low-poly MeshParts placed inside the ViewportFrame.
What happens if a player places two portals facing each other to create an infinite loop?
In physics, falling between two opposing vertical portals creates continuous gravitational acceleration until terminal velocity is reached. For visual rendering, limit recursive rendering to a maximum recursion depth of 2 or 3 passes to prevent rendering stalls.