Scene's origin relative to portal's window?

I am experimenting with RealityKit to set up a portal. Everything works, but I was wondering where the scene's origin is with respect to the front of the portal window?

From experiments, the origin's X and Y appear to be at the center of the portal window, while the origin's Z appearing to be about a meter behind the portal window.

Is this (at least roughly) correct? Is it documented anywhere?

PS. I began with the standard visionOS app and edited the Reality Composer Pro file to create the scene.

Answered by Vision Pro Engineer in 827220022

Hi @THeberlein

It sounds like you are asking for the origin of the world entity (with WorldComponent) relative to the plain entity (with the PortalComponent). I "believe" the plane and world origins will always intersect. By default, the origin of an entity is its center. So by default, the center of the world should intersect the center of the plane (in all dimensions).

I ran an experiment to confirm this. Note the comments.

struct PortalView: View {
    @Environment(\.physicalMetrics) private var physicalMetrics
    let portalSize:Float = 1.0
    var body: some View {
        RealityView { content in
            var worldMaterial = SimpleMaterial(color: .blue, isMetallic: false)
            worldMaterial.faceCulling = .front
            let world = ModelEntity(
                mesh: .generateBox(size: portalSize),
                materials: [worldMaterial])
            world.components.set(WorldComponent())
            
            // Since the world origin relative to the portal plane
            // is the center of the plane, pushing the world
            // back 1/2 the depth of the world plus 0.1m causes
            // a 0.1m gap between the portal opening and the world. 
            // Comment out the next line and observe the world move to the center.
            world.position.z = -portalSize / 2.0 - 0.1
            content.add(world)
            
            let portal = ModelEntity(
                mesh: .generatePlane(width: portalSize, height: portalSize),
                materials: [PortalMaterial()]
            )
            portal.components.set(PortalComponent(target: world))
            content.add(portal)
        }
        .frame(
            width: physicalMetrics.convert(CGFloat(portalSize), from: .meters),
            height: physicalMetrics.convert(CGFloat(portalSize), from: .meters)
        )
    }
}

I couldn't find any documentation on this. Please accept this answer if this answers your question. Otherwise please followup. I want to help you achieve your goal.

Accepted Answer

Hi @THeberlein

It sounds like you are asking for the origin of the world entity (with WorldComponent) relative to the plain entity (with the PortalComponent). I "believe" the plane and world origins will always intersect. By default, the origin of an entity is its center. So by default, the center of the world should intersect the center of the plane (in all dimensions).

I ran an experiment to confirm this. Note the comments.

struct PortalView: View {
    @Environment(\.physicalMetrics) private var physicalMetrics
    let portalSize:Float = 1.0
    var body: some View {
        RealityView { content in
            var worldMaterial = SimpleMaterial(color: .blue, isMetallic: false)
            worldMaterial.faceCulling = .front
            let world = ModelEntity(
                mesh: .generateBox(size: portalSize),
                materials: [worldMaterial])
            world.components.set(WorldComponent())
            
            // Since the world origin relative to the portal plane
            // is the center of the plane, pushing the world
            // back 1/2 the depth of the world plus 0.1m causes
            // a 0.1m gap between the portal opening and the world. 
            // Comment out the next line and observe the world move to the center.
            world.position.z = -portalSize / 2.0 - 0.1
            content.add(world)
            
            let portal = ModelEntity(
                mesh: .generatePlane(width: portalSize, height: portalSize),
                materials: [PortalMaterial()]
            )
            portal.components.set(PortalComponent(target: world))
            content.add(portal)
        }
        .frame(
            width: physicalMetrics.convert(CGFloat(portalSize), from: .meters),
            height: physicalMetrics.convert(CGFloat(portalSize), from: .meters)
        )
    }
}

I couldn't find any documentation on this. Please accept this answer if this answers your question. Otherwise please followup. I want to help you achieve your goal.

Edit, I just learned:

In general Portals/Worlds do not have their own coordinate space; rather they share the same coordinate space as the rest of the RealityKit entity hierarchy.

That means that you can move the world around to see the world as seen in the portal move, or move the portal around to see the window into the world move.

In the above example, the entities‘ origins intersect because they share a parent (content) and an entity‘s position defaults to 0,0,0.

I discovered part of my confusion. An entity (e.g., green cylinder) that is front of the portal is still visible as long as the portal's clipping plane isn't activated.

In screenshots below, initially Red cylinder is at Z = 0; Blue cylinder is at Z = -0.3 Green cylinder is at Z = 0.3 (in front of the portal plane).

Despite the green cylinder being in front of the window/portal plane, it is initially still visible as long as it is positioned between the camera and the portal window.

When I activate the clipping plane, the green cylinder disappears, and the red cylinder gets cut in half.

To have the green cylinder appear beyond the frame of the portal, (1) the portal has to have the PortalComponent's crossingMode set to .plane(.positiveZ) and (2) the green cylinder needs to have a PortalCrossingComponent.

I am still stuck on the fact that when I move the green cylinder's Z position to +0.5, it still gets clipped.

Still have lots to learn...

Example screenshots:

Green cylinder Z at 0.3, it appears in front of the portal but rendering is still bound by the portal.

Portal's clipping plane is enabled. Green cylinder is gone, and red cylinder is cut in half, verifying clipping plane is at Z = 0.

Portal's PortalComponent crossingMode = .plane(.positiveZ) and PortalCrossingComponent added to green cylinder.

When green cylinder is pushed out to Z = 0.5, part of it is still clipped. Is there a limit for how far things can come out of the portal?

Hi @THeberlein

Thanks so much for sharing your research. I'm certain your post will help others. I believe the green puck is being clipped by the window. Here's how to see the clipping:

  • In debug mode open "Debug Visualizations" by clicking the button to the right of the "step out" button in the "Debug Area" (bottom pane). It kind of looks like a box.
  • Select "Clipping".
  • You'll see the puck is clipped

I'll ask around to see if that can be changed.

Hi @THeberlein

The cause is indeed app clipping. The clipping behavior is not limited to content in a portal, it happens for any/all entities that exceed the app's bounds. I was able to get a little more room using a volume with.windowResizability(.contentMinSize):

WindowGroup {
    PortalView()
        .environment(appModel)
        // It doesn't seem to matter what depth is passed to frame.
        .frame(depth: 2)
}
.windowResizability(.contentMinSize)
.windowStyle(.volumetric)

If you want to avoid clipping entirely, use a plane in an immersive space.

Scene's origin relative to portal's window?
 
 
Q