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.

Questions or feedback?