Programmer54
Programmer54

Reputation: 174

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

Upvotes: 2

Views: 91

Answers (1)

Andy Jazz
Andy Jazz

Reputation: 58093

(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

Upvotes: 2

Related Questions