Lab 037 – Portal in a Window

Learning the basics of how to use PortalComponent to render one scene inside another.

Overview

Is portal fever in the air? Recently both Tom K. and Todd H. shared some interesting exampled with portals. I think portals could be useful in Dark Spaces, so it’s time to learn the basics.

💡 3D Carousel Concept for visionOSYou can easily add depth to a visionOS app without using a Volume or Immersive Space. It’s a great way to spark curiosity before users commit to a full immersive experience—perfect for real estate, travel agencies, and more!

Tom Krikorian ᯅ (@tomkrikorian.com) 2025-02-21T18:06:11.567Z

visionOS portals work a little differently in an immersive environment as opposed to being attached to a window.You can move the portal around to see different areas of the world behind it.Might make a nice immersive story feature.

Todd Heberlein (@toddheberlein.bsky.social) 2025-03-03T01:50:51.556Z

Apple has a great example project and tutorial to cover this topic. This lab is a simplified version of that project. I wanted to reduce it to as few pieces as possible to see how it works.

I used a Window to render the portal in this lab, but we could also use these ideas in Volumes or Spaces. Let’s build the portal step by step.

We’ll start with a RealityView for our window, then add an entity that will serve as the root for content outside of the portal.

struct Lab037: View {
    var body: some View {
        RealityView { content in

            let rootEntity = Entity()
            content.add(rootEntity)

        }
    }
}

Next, we’ll add an entity that will act as the root for the content that will appear inside the portal. We’ll use the WorldComponent on this entity.

let portalContentRoot = Entity()
portalContentRoot.components.set(WorldComponent())
rootEntity.addChild(portalContentRoot)

Then we need something to render the portal. We’ll use a plane mesh and PortalMaterial. We also need to add a PortalComponent that targets the portalContentRoot.

let portalEntity = ModelEntity(
  mesh: .generatePlane(width: 0.4, height: 0.2, cornerRadius: 0.03),
  materials: [PortalMaterial()]
)
portalEntity.components.set(PortalComponent(target: portalContentRoot))
rootEntity.addChild(portalEntity)

Finally, we need some content for the portal. We’ll use one of the existing lab scenes from Reality Composer Pro.

guard let scene = try? await Entity(named: "TeleportLabs", in: realityKitContentBundle) else { return }
portalContentRoot.addChild(scene)

When we put it all together, we get a delightful result. We’ll explore some ideas for using portals in future labs.

Video Demo

visionOS Simulator demo of a portal in a window

Full Lab Code

struct Lab037: View {
    var body: some View {
        RealityView { content in

            // 1. The root for our scene *outside* of the portal
            let rootEntity = Entity()
            content.add(rootEntity)

            // 2. The root for the content that will appear *inside* the portal
            let portalContentRoot = Entity()
            portalContentRoot.components.set(WorldComponent())
            rootEntity.addChild(portalContentRoot)

            // 3. An entity that will render the portal
            // We need to use PortalMaterial
            let portalEntity = ModelEntity(
                mesh: .generatePlane(width: 0.4, height: 0.2, cornerRadius: 0.03),
                materials: [PortalMaterial()]
            )
            portalEntity.position.z = -0.175

            // We also need to add a PortalComponent that targets the portalContentRoot
            portalEntity.components.set(PortalComponent(target: portalContentRoot))
            rootEntity.addChild(portalEntity)


            // 4. We'll load some content to add to the portalContentRoot
            guard let scene = try? await Entity(named: "TeleportLabs", in: realityKitContentBundle) else { return }

            portalContentRoot.addChild(scene)
            scene.position.y = -1.4

        }
    }
}

Support our work so we can continue to bring you new examples and articles.

Questions or feedback?