Architecture & Engineering

비유클리드 공간 아키텍처: 뷰포트 포탈 워핑, 상대 속도 보존 및 무지연 통과

By DopaBrain 공간 기하학 및 엔진 아키텍처 팀 • Published 2026-10-01

포탈(Portal), 안티챔버(Antichamber), 스플릿게이트(Splitgate) 같은 혁신적인 게임에서 비유클리드 포탈은 플레이어에게 경이로운 공간 경험을 선사합니다. 맵의 한쪽 벽에 열린 문 너머로 수 킬로미터 떨어진 완전히 다른 공간이 실시간 3D 창문처럼 보이고, 그 문을 통과하는 순간 화면 암전이나 로딩, 속도 끊김 없이 자연스럽게 이어진다면 환상적인 퍼즐과 전투가 가능해집니다.

그러나 로블록스에서 단순히 터치 감지로 캐릭터 CFrame을 바꾸는 주먹구구식 텔레포트는 극심한 카메라 튐, 운동량 소멸, 벽 끼임 현상을 유발합니다. 본 실전 가이드에서는 Luau 기반의 상용급 심리스 포탈 프레임워크를 구축합니다. 상대 CFrame 좌표 변환 행렬 도출, ViewportFrame과 EditableImage를 활용한 실시간 시점 렌더링, 입구에서 출구로의 순간 선속도 및 각속도 벡터 전이, 그리고 프레임 단위 무충돌 관통 기법을 다룹니다.

1. 단순 순간이동의 결함: 왜 일반 텔레포트는 공간감을 깨뜨리는가

로블록스의 기본 좌표 이동 방식은 연속적인 물리적 시각적 법칙을 위배합니다:

2. 수학적 기반: 상대 CFrame 변환 행렬과 속도 전이 공식

포탈은 입구 A와 출구 B 사이의 좌표계를 180도 반전 연결하는 좌표 변환기입니다:

3. Luau 심리스 포탈 엔진 구현체

RunService.RenderStepped에서 카메라 시점을 매 프레임 동기화하고 초고속 통과를 감지하는 고성능 Luau 스크립트입니다:

SeamlessPortalEngine.luau (공간 변환 및 통과 코어)
--!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
    -- 출구 포탈 바깥쪽을 향하도록 Y축 기준 180도 회전 반전
    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)
    -- 180도 반전 출구에 맞춰 X, Z 속도 반전
    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)

    -- 전면(양수)에서 후면(음수)으로의 평면 교차 감지
    if prevDist > 0 and currDist <= 0 then
        -- 포탈 사각 개구부 영역 내부인지 검증
        local localPos = portalCF:PointToObjectSpace(currentPos)
        if math.abs(localPos.X) <= self.Width * 0.5 and math.abs(localPos.Y) <= self.Height * 0.5 then
            -- 위치, 속도, 카메라의 원자적 일괄 전이
            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. 실시간 포탈 렌더링 파이프라인: ViewportFrame과 EditableImage

포탈 표면에 반대편 세계를 거울처럼 투영하는 고성능 렌더링 아키텍처입니다:

5. 충돌체 인계, 공간 음향 래핑 및 상용 최적화

플레이어와 투사체가 포탈 모서리에 걸리지 않고 매끄럽게 통과하도록 만드는 엔지니어링 규칙입니다:

Frequently Asked Questions

포탈을 통과할 때 캐릭터의 어깨나 팔이 벽에 걸려 튕겨 나오는 현상은 어떻게 해결하나요?

Humanoid 충돌 볼륨의 두께 때문입니다. 통과가 감지된 순간부터 0.1초 동안 플레이어와 포탈 벽 파트 간의 충돌을 CollisionGroup 또는 CanCollide = false로 일시 비활성화해 주면 벽 끼임 없이 100% 매끄럽게 빠져나갈 수 있습니다.

포탈을 통과하는 순간 화면이 180도 홱 도는 멀미 현상은 어떻게 방지하나요?

카메라에도 캐릭터와 동일한 상대 변환 행렬을 적용해야 합니다. 입구 기준 카메라 CFrame을 180도 반전하여 출구 기준 CFrame으로 연속 변환하면, 플레이어가 바라보던 시선 그대로 부드럽게 반대편 공간으로 시야가 이어집니다.

ViewportFrame 내부에서 반대편의 달리는 캐릭터나 몬스터 애니메이션도 보이나요?

보입니다. ViewportFrame 내부에 Humanoid와 Animator가 장착된 복제 리그를 넣고 애니메이션 트랙을 원본과 동기화하여 재생하면 적의 실시간 움직임이 완벽하게 렌더링됩니다.

두 개의 포탈을 서로 마주 보게 설치하여 무한 낙하 루프를 만들면 어떻게 되나요?

물리적으로는 중력 가속도가 계속 누적되어 종단 속도에 도달하며 무한히 떨어집니다. 시각 렌더링 측면에서는 무한 재귀 호출로 인한 크래시를 방지하기 위해 뷰포트 내부의 재귀 렌더링 깊이를 최대 2~3회로 제한해야 합니다.

Explore More Interactive Tests & Guides

Discover personalized cognitive assessments, stress evaluations, and game psychology tools on DopaBrain.

Go to Portal Hub