Search code examples
swiftaugmented-realityarkitrealitykitreality-composer

Applying downward force to an object using RealityKit


Here is my previous question about in general apply force for a certain point of an AR object which had a perfect answer. I have managed to apply force to a given point with a little bit of tinkering to have a perfect effect for me. Let me show also some code.

I get the AR object from Experience like:

if let skateAnchor = try? Experience.loadSkateboard(), 
   let skateEntity = skateAnchor.skateboard {

    guard let entity = skateEntity as? HasPhysicsBody else { return }
    skateAnchor.generateCollisionShapes(recursive: true)
    entity.collision?.filter.mask = [.sceneUnderstanding]
    skateboard = entity
}

Afterwards I set up the plane and the LiDAR scanner and add some gestures to it like:

let arViewTap = UITapGestureRecognizer(target: self, 
                                       action: #selector(tapped(sender:)))
arView.addGestureRecognizer(arViewTap)
let arViewLongPress = UILongPressGestureRecognizer(target: self, 
                                                   action: #selector(longPressed(sender:)))
arView.addGestureRecognizer(arViewLongPress)

So far so good, on tap gesture I apply the logic from the previously linked answer and apply force impulse like:

if  let sk8 = skateboard as? HasPhysics {
    sk8.applyImpulse(direction, at: position, relativeTo: nil)
}

My issue comes with my "catching" logic, where I do want to use the long press, and apply downward force to my skateboard AR object like this:

@objc func longPressed(sender: UILongPressGestureRecognizer) {
    if sender.state == .began || sender.state == .changed {
        let location =  sender.location(in:arView)

        if arView.entity(at: location) is HasPhysics {

            if let ray = arView.ray(through: location) {

                let results = arView.scene.raycast(origin: ray.origin,
                                                direction: ray.direction,
                                                   length: 100.0,
                                                    query: .nearest,
                                                     mask: .all,
                                               relativeTo: nil)

                if let _ = results.first,
                   let position = results.first?.position,
                   let normal = results.first?.normal {

                    // test different kind of forces
                    let direction = SIMD3<Float>(0, -20, 0)
                        
                    if let sk8 = skateboard as? HasPhysics {
                        sk8.addForce(direction, at: position, relativeTo: nil)
                    }
                }
            }
        }
    }
}

Right now I know that I am ignoring the raycast results, but this is in pure development state, my issue is that when I apply positive/negative x/z the object responds well, it either slides back and forth or left or right, the positive y is also working by draging the board in the air, the only error prone force direction is the one I am striving to achieve is the downward facing negative y. The object just sits there with no effect at all.

Let also share how my object is defined inside the Reality Composer:

skateboard inside the Reality Composer


Solution

  • Ollie trick

    In real life, if you shift your entire body's weight to the nose of the skateboard's deck (like doing the Ollie Maneuver), the skateboard's center of mass shifts from the middle towards the point where the force is being applied. In RealityKit, if you need to tear the rear (front) wheels of the skateboard off the floor, move the model's center of mass towards the slope.

    enter image description here

    The repositioning of the center of mass occurs in a local coordinate system.

    import SwiftUI
    import RealityKit
    
    struct ContentView : View {
        var body: some View {
            ARViewContainer().ignoresSafeArea()
        }
    }
    
    struct ARViewContainer: UIViewRepresentable {
        
        func makeUIView(context: Context) -> ARView {
            
            let arView = ARView(frame: .zero)
            arView.debugOptions = .showPhysics         // shape visualization
            
            let scene = try! Experience.loadScene()
            let name = "skateboard_01_base_stylized_lod0"
            typealias ModelPack = ModelEntity & HasPhysicsBody & HasCollision
            
            let model = scene.findEntity(named: name) as! ModelPack
            model.physicsBody = .init()
            model.generateCollisionShapes(recursive: true)
            model.physicsBody?.massProperties.centerOfMass.position = [0, 0,-27]
    
            arView.scene.anchors.append(scene)
            return arView
        }
        
        func updateUIView(_ uiView: ARView, context: Context) { }
    }
    

    Physics shape

    The second problem that you need to solve is to replace the model's box shape of the physical body (RealityKit and Reality Composer generate this type of shape by default). Its shape cannot be in the form of a monolithic box, it's quite obvious, because the box-shaped form does not allow the force to be applied appropriately. You need a shape similar to the outline of the model.

    enter image description here

    So, you can use the following code to create a custom shape:

    (four spheres for wheels and box for deck)

    let shapes: [ShapeResource] = [ 
                                    .generateBox(size: [ 20, 4, 78])
                                        .offsetBy(translation: [ 0.0, 11, 0.0]),
                                    .generateSphere(radius: 3.1)
                                        .offsetBy(translation: [ 7.5, 3, 21.4]),
                                    .generateSphere(radius: 3.1)
                                        .offsetBy(translation: [ 7.5, 3,-21.4]),
                                    .generateSphere(radius: 3.1)
                                        .offsetBy(translation: [-7.5, 3, 21.4]),
                                    .generateSphere(radius: 3.1)
                                        .offsetBy(translation: [-7.5, 3,-21.4])
                                  ]
    
    // model.physicsBody = PhysicsBodyComponent(shapes: shapes, mass: 4.5)
    
    model.collision = CollisionComponent(shapes: shapes)
    

    enter image description here

    enter image description here

    enter image description here

    P.S.

    Reality Composer model's settings (I used Xcode 14.0 RC 1).

    enter image description here