Search code examples
swiftarkitrealitykitarquicklook

How to show a grounding shadow as in AR Quick Look?


While showing a .USDZ model using a QLPreviewController I get a grounding shadow out of the box. However, when I load the same model in the ARView there is no such shadow. Why is it hidden and is it possible to show it ? Or do I have to create a custom shadow ?

enter image description here


Solution

  • (VR-like) AR mode with Grounding Shadow component

    The answer is quite obvious: since ARQuickLook doesn't allow you customize anything in your scene (it's a ready-to-use solution), then if you want to implement everything that is activated in ARQuickLook by default (be it collision shapes, gestures, grounding shadows, playing animations, etc) in ARView or in RealityView you'll have to implement this from scratch.

    RealityKit 4.0 (iOS 18.0+) allows you generate grounding shadows from the perspective of another entity that receives the first entity's shadow. In the previous version of RealityKit you would have to use a directional or spot lights for this.

    import SwiftUI
    import RealityKit
    
    struct ContentView : View {
        var body: some View {
            ARViewContainer().ignoresSafeArea()
        }
    }
    

    struct ARViewContainer : UIViewRepresentable {
        let arView = ARView(frame: .zero)
        let anchor = AnchorEntity()
        
        init() {
            arView.environment.lighting.intensityExponent = 1.001
            arView.environment.background = .color(.white)
        }
        func makeUIView(context: Context) -> ARView {
            // Biplane's Entity
            let entity = try! Entity.load(named: "biplane")
            entity.position.y = 0.05
            anchor.addChild(entity)
    
            // Biplane's ModelEntity
            let model = entity.findEntity(named: "toy_biplane_bind") as! ModelEntity
            model.components[GroundingShadowComponent.self] = 
                                                     .init(castsShadow: true, 
                                                           receivesShadow: false)
            // Shadow plane
            let mesh = MeshResource.generatePlane(width: 0.5,
                                                  depth: 0.5,
                                           cornerRadius: 0.25)
            let material = SimpleMaterial()
            let shadowPlane = ModelEntity(mesh: mesh, materials: [material])
            shadowPlane.components[GroundingShadowComponent.self] = 
                                                           .init(castsShadow: false, 
                                                                 receivesShadow: true)
            anchor.addChild(shadowPlane)            
            arView.scene.anchors.append(anchor)
            return arView
        }
        func updateUIView(_ view: ARView, context: Context) { }
    }
    

    Here's some kind of tracked "VR mode" (camera feed is obscured by ARView's white BG).


    AR mode with Occlusion material

    For AR mode, you could use DirectionalLight object with a non-opaque material catching a shadow, but for some reason OcclusionMaterial causes the surface to be "overexposed". I hope that this bug will be fixed in future versions. It's quite possible that there's some parameter that can remove the "overexposed surface" effect of the plane, but I have not found it.

    struct ARViewContainer : UIViewRepresentable {
        let arView = ARView(frame: .zero)
        let anchor = AnchorEntity()
    
        func makeUIView(context: Context) -> ARView {
            // Biplane's Entity
            let entity = try! Entity.load(named: "biplane")
            entity.position.y = 0.05
            anchor.addChild(entity)
            
            // Light
            let sun = DirectionalLight()
            sun.shadow = .init()
            sun.light.intensity = 4000
            sun.light.color = .white
            sun.orientation = .init(angle: -.pi/2, axis: [1,0,0])
            anchor.addChild(sun)
            
            // Shadow plane
            let mesh = MeshResource.generatePlane(width: 0.5,
                                                  depth: 0.5,
                                           cornerRadius: 0.25)
    
            let material = OcclusionMaterial(receivesDynamicLighting: true) // !!!
            
            let shadowPlane = ModelEntity(mesh: mesh, materials: [material])
            anchor.addChild(shadowPlane)
            
            arView.scene.anchors.append(anchor)
            return arView
        }
        func updateUIView(_ view: ARView, context: Context) { }
    }
    

    VR mode (.nonAR)

    import SwiftUI
    import RealityKit
    
    struct ContentView : View {
        var body: some View {
            ARViewContainer().ignoresSafeArea()
        }
    }
    
    struct ARViewContainer : UIViewRepresentable {
        let arView = ARView(frame: .zero)
        let anchor = AnchorEntity()
        
        init() {
            arView.cameraMode = .nonAR
            arView.environment.background = .color(UIColor(white: 0.975, alpha: 1))
        }
        func makeUIView(context: Context) -> ARView {
            // Biplane's Entity
            let entity = try! Entity.load(named: "biplane")
            entity.position.y = 0.05
            anchor.addChild(entity)
            print(entity)
            
            // Perspective Camera
            let camera = PerspectiveCamera()
            camera.position = [-0.13, 0.2, 0.7]
            camera.orientation = .init(angle: -.pi/16, axis: [1,1,0])
            anchor.addChild(camera)
    
            // Biplane's ModelEntity
            let model = entity.findEntity(named: "toy_biplane_bind") as! ModelEntity
            model.components[GroundingShadowComponent.self] =
                                                     .init(castsShadow: true,
                                                           receivesShadow: false)
            // Shadow plane
            let mesh = MeshResource.generatePlane(width: 0.5,
                                                  depth: 0.5,
                                           cornerRadius: 0.25)
            let material = SimpleMaterial()
            let shadowPlane = ModelEntity(mesh: mesh, materials: [material])
            shadowPlane.components[OpacityComponent.self] = .init(opacity: 0.9)
            shadowPlane.components[GroundingShadowComponent.self] =
                                                           .init(castsShadow: false,
                                                                 receivesShadow: true)
            anchor.addChild(shadowPlane)
            arView.scene.anchors.append(anchor)
            return arView
        }
        func updateUIView(_ view: ARView, context: Context) { }
    }
    

    enter image description here