Lab 041 – Impossible Doorway
More fun with portals and occlusion material.
I didn’t really have anything in mind for this lab, other than to have a bit of fun. We start in passthrough, with a red scene in the doorway. If we look at the doorway from the back, we’ll see a blue scene. We can tap any of the scenes to cycle through passthrough (represented with exclusion material in the doorway), the red scene, and blue scene.
Video demo of a scene changing from passthrough, to a red dome, to a blue dome, with lots of variation.
Full Lab Code
The code is a bit of a mess, but here it is!
struct Lab041: View {
@State var outerContent = Entity()
@State var portalContentFront = Entity()
@State var portalContentBack = Entity()
@State var portalEntityFront = ModelEntity(
mesh: .generatePlane(width: 0.8, height: 2, cornerRadius: 0.01),
materials: [PortalMaterial()]
)
@State var portalEntityBack = ModelEntity(
mesh: .generatePlane(width: 0.8, height: 2, cornerRadius: 0.01),
materials: [PortalMaterial()]
)
@State private var sceneRed: Entity?
@State private var sceneBlue: Entity?
@State private var currentScene: SceneType = .passthrough
// Add enum to track scene types
private enum SceneType {
case passthrough
case red
case blue
}
private let portalMaterial = PortalMaterial()
private let occlusionMaterial = OcclusionMaterial()
private let disabledMaterial = SimpleMaterial(color: .clear, isMetallic: false) // debugging only
// Add constants for portal positioning
private let frontPosition: SIMD3<Float> = [0, 1, -0.99]
private let backPosition: SIMD3<Float> = [0, 1, -1.01]
var body: some View {
RealityView { content in
guard let doorway = try? await Entity(named: "PortalDoorway", in: realityKitContentBundle) else { return }
guard let redScene = try? await Entity(named: "PortalSwapRed", in: realityKitContentBundle) else { return }
guard let blueScene = try? await Entity(named: "PortalSwapBlue", in: realityKitContentBundle) else { return }
// Store references to scenes
sceneRed = redScene
sceneBlue = blueScene
let rootEntity = Entity()
content.add(rootEntity)
rootEntity.addChild(doorway)
rootEntity.addChild(outerContent)
rootEntity.addChild(portalContentFront)
rootEntity.addChild(portalContentBack)
// Front portal - RED SCENE
portalContentFront.components.set(WorldComponent())
portalContentFront.children.removeAll()
portalContentFront.addChild(redScene) // RED in front
portalEntityFront.name = "Front"
portalEntityFront.position = frontPosition
portalEntityFront.transform.rotation = simd_quatf(angle: 0, axis: [0, 1, 0]) // Face forward
portalEntityFront.components.set(PortalComponent(target: portalContentFront))
portalEntityFront.components.set(CollisionComponent(shapes: [.generateBox(width: 0.8, height: 2, depth: 0.05)]))
portalEntityFront.components.set(InputTargetComponent())
rootEntity.addChild(portalEntityFront)
// Back portal - BLUE SCENE
portalContentBack.components.set(WorldComponent())
portalContentBack.children.removeAll()
portalContentBack.addChild(blueScene) // BLUE in back
portalEntityBack.name = "Back"
portalEntityBack.position = backPosition
portalEntityBack.transform.rotation = simd_quatf(angle: .pi, axis: [0, 1, 0]) // Rotate 180 degrees to face back
portalEntityBack.components.set(PortalComponent(target: portalContentBack))
portalEntityBack.components.set(CollisionComponent(shapes: [.generateBox(width: 0.8, height: 2, depth: 0.05)]))
portalEntityBack.components.set(InputTargetComponent())
rootEntity.addChild(portalEntityBack)
}
.gesture(tap)
}
var tap: some Gesture {
TapGesture()
.targetedToAnyEntity()
.onEnded { value in
guard let redScene = sceneRed, let blueScene = sceneBlue else { return }
let tappedEntity = value.entity
switch currentScene {
case .passthrough:
if tappedEntity == portalEntityFront {
// Tapped front portal - enter red scene
currentScene = .red
outerContent.children.removeAll()
outerContent.addChild(redScene)
} else if tappedEntity == portalEntityBack {
// Tapped back portal - enter blue scene
currentScene = .blue
outerContent.children.removeAll()
outerContent.addChild(blueScene)
}
case .red:
if tappedEntity == portalEntityFront {
// Tapped front portal - enter blue scene
currentScene = .blue
outerContent.children.removeAll()
outerContent.addChild(blueScene)
} else if tappedEntity == portalEntityBack {
// Tapped back occlusion - return to passthrough
currentScene = .passthrough
outerContent.children.removeAll()
}
case .blue:
if tappedEntity == portalEntityFront {
// Tapped front occlusion - return to passthrough
currentScene = .passthrough
outerContent.children.removeAll()
} else if tappedEntity == portalEntityBack {
// Tapped back portal - enter red scene
currentScene = .red
outerContent.children.removeAll()
outerContent.addChild(redScene)
}
}
// Update portal states after scene change
updatePortalStates()
}
}
// Helper function to update portal states and positions
private func updatePortalStates() {
switch currentScene {
case .passthrough:
// Front portal to red scene
portalEntityFront.transform.rotation = simd_quatf(angle: 0, axis: [0, 1, 0]) // Face forward
portalEntityFront.position = frontPosition
portalEntityFront.components.set(PortalComponent(target: portalContentFront))
portalEntityFront.model?.materials = [portalMaterial]
portalEntityFront.components.set(InputTargetComponent())
portalContentFront.children.removeAll()
portalContentFront.addChild(sceneRed!)
// Back portal to blue scene
portalEntityBack.transform.rotation = simd_quatf(angle: .pi, axis: [0, 1, 0]) // Face backward
portalEntityBack.position = backPosition
portalEntityBack.components.set(PortalComponent(target: portalContentBack))
portalEntityBack.model?.materials = [portalMaterial]
portalEntityBack.components.set(InputTargetComponent())
portalContentBack.children.removeAll()
portalContentBack.addChild(sceneBlue!)
case .red:
// Front portal to blue scene
portalEntityFront.transform.rotation = simd_quatf(angle: 0, axis: [0, 1, 0]) // Face forward
portalEntityFront.position = frontPosition
portalEntityFront.components.set(PortalComponent(target: portalContentFront))
portalEntityFront.model?.materials = [portalMaterial]
portalEntityFront.components.set(InputTargetComponent())
portalContentFront.children.removeAll()
portalContentFront.addChild(sceneBlue!)
// Back portal as occlusion
portalEntityBack.transform.rotation = simd_quatf(angle: .pi, axis: [0, 1, 0]) // Face backward
portalEntityBack.position = backPosition
portalEntityBack.components.remove(PortalComponent.self)
portalEntityBack.model?.materials = [occlusionMaterial]
portalEntityBack.components.set(InputTargetComponent())
portalContentBack.children.removeAll()
case .blue:
// Front portal as occlusion
portalEntityFront.transform.rotation = simd_quatf(angle: 0, axis: [0, 1, 0]) // Face forward
portalEntityFront.position = frontPosition
portalEntityFront.components.remove(PortalComponent.self)
portalEntityFront.model?.materials = [occlusionMaterial]
portalEntityFront.components.set(InputTargetComponent())
portalContentFront.children.removeAll()
// Back portal to red scene
portalEntityBack.transform.rotation = simd_quatf(angle: .pi, axis: [0, 1, 0]) // Face backward
portalEntityBack.position = backPosition
portalEntityBack.components.set(PortalComponent(target: portalContentBack))
portalEntityBack.model?.materials = [portalMaterial]
portalEntityBack.components.set(InputTargetComponent())
portalContentBack.children.removeAll()
portalContentBack.addChild(sceneRed!)
}
}
}Credits:
Architrave (Pencil Round) from The Base Mesh
Support our work so we can continue to bring you new examples and articles.
Download the Xcode project with this and many more labs from Step Into Vision.

Follow Step Into Vision