SCNPhysicsWorld Force Simulation Update

Viewed 161

I'm trying to get client-side prediction / reconciliation working for a client/server model. I have been using Scenekit for rendering the 3D world and handling physics calculations.

In order to get this working, I need the ability to force the physics simulation to "calculate" a given number of frames (apply forces, check for collisions, update node positions / orientations), without rendering these updates. This way, when I determine that the client has had a misprediction with what the server is sending back, I can re-orient all nodes in the physics simulation to match what the server sent (including setting physics properties like velocity / angular velocity), and then fast forward through the "events" that the server has not yet processed to get back to the client's current point in time.

I understand SceneKit does not use a deterministic physics engine, but I'm hoping I can get something that looks "good enough" (my misprediction system has a threshold for determining if a given prediction doesn't align with the server's).

The process looks roughly like this:

  1. Client runs physics simulation locally based on local inputs / current local simulation state
  2. The client generates it's own host packets each time the physics simulation runs, caching them for the current frame date.
  3. Client receives a host packet from server stating what the physics simulation should look like. This packet is based on a packet the client sent to the server in the past. The host packet contains an identifier (a date) that the client bundled with it's packet it sent to the server previously.
  4. The client looks up it's previous cached version of how it saw the physics world in the past for this date. If it differs too much from what the host has sent, we determine that we've had a misprediction and need to correct.
  5. The client re-orients all nodes in the scene to match what the host packet states (setting position, rotation, velocity, angular velocity, etc)
  6. The client then iterates over all of the packets it has sent to the server AFTER the host packet's date (client packets that the server has not processed yet as part of this host packet), and re-applies them to their corresponding nodes.
  7. The part I'm struggling with: Each time the client applies a set of historic packets to the nodes in part 6, I need to force the physics simulation to "process" these changes (apply the forces generated from these packets, check for collisions, update node positions) before moving on to the next set of packets that the server has not yet processed.

I have tried playing with the physics world's timeStep but it appears increasing this property also decreases the applied forces per physics "cycle" (we get more physics simulations, but the end result is just a more "accurate" simulation by moving physics bodies shorter distances before checking for collisions).

I have tried playing with the physics world's speed but a) increasing this value reduces the physics simulation's accuracy, b) for some reason scenes created in the scene editor have a default speed of 6 instead of 1 so determining the appropriate value here is a bit confusing, and c) it doesn't seem to have any affect until the next time the simulation attempts to run.

I have tried playing with the scene view's sceneTime, incrementing it by 1 each time I process a set of historical inputs, which I thought was working but upon closer investigation it seems this does nothing.

I have tried pausing the scene, applying my changes and then playing the scene, but pausing the scene pauses the physics simulation as well.

Is there any way to just have the SCNPhysicsWorld update it's physics simulation manually/repeatedly in a loop, without triggering any rendering calls?

1 Answers

[EDIT] PhysicsKit is now PhyKit due to namespace / linker issues with Apple's internal PhysicsKit framework. It is also now cross-platform for iOS / macOS, built as an xcframework available via SPM or Cocoapods. The repo has all of the updates.

After a lot of trial and error I was unable to get anything approximating a forced simulation update to work with SceneKit. Upon contacting Apple, I was also informed that this functionality is currently not supported.

I decided using a separate Physics engine to accomplish my goals, in place of SceneKit's, would allow me to orient my SCNNodes to the calculations of the external physics engine.

To that end I've created PhysicsKit, an open source wrapper around the popular Bullet physics engine, which can be found here: https://github.com/AdamEisfeld/PhyKit

The framework is available via Cocoapods and I will work on adding support for Carthage / SPM in the future. The README in the repo and the class documentation should help in getting things up and running, but in the interest of including some actual code in this post, here is a brief run through demonstrating how we can use the framework to trigger a physics simulation step, showing a bouncy ball colliding with a ground plane:

import UIKit
import SceneKit
import PhysicsKit

class ViewController: UIViewController {
    
    let sceneView = SCNView()
    let scene = SCNScene(named: "scenes.scnassets/world.scn")!
    let physicsWorld = PKPhysicsWorld()
    let physicsScene = PKPhysicsScene(isMotionStateEnabled: true)
    var sceneTime: TimeInterval? = nil
    let cameraNode = SCNNode()
    
    override func viewDidLoad() {
        
        super.viewDidLoad()
        
        setupSceneView()
        
        setupGroundPlane()
        setupBouncyBall()
        
    }
    
    private func setupSceneView() {
        // Embed the scene view into our view
        sceneView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(sceneView)
        NSLayoutConstraint.activate([
            sceneView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            sceneView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            sceneView.topAnchor.constraint(equalTo: view.topAnchor),
            sceneView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
        ])
        // Play the sceneview so we get constant calls to the delegate's update(atTime...) function
        sceneView.isPlaying = true
        // Set the delegate so we can update our physics world with the scene view
        sceneView.delegate = self
        // Set the scene
        sceneView.scene = scene
        
        // Configure the camera
        cameraNode.camera = SCNCamera()
        scene.rootNode.addChildNode(cameraNode)
        sceneView.pointOfView = cameraNode
        cameraNode.position = SCNVector3(0, 0, 20)
        
    }
    
    private func setupGroundPlane() {
        
        // Create a visual node representing a ground plane, and add it to our scene
        let groundGeometry = SCNFloor()
        let groundNode = SCNNode(geometry: groundGeometry)
        scene.rootNode.addChildNode(groundNode)
        
        // Create a physics body for the ground and add it to our physics world
        let groundShape = PKCollisionShapeStaticPlane()
        let groundBody = PKRigidBody(type: .static, shape: groundShape)
        physicsWorld.add(groundBody)
        
        // Shift the ground down 10 units
        groundBody.position = .vector(0, -10, 0)
        
        // Make the ground a little bouncy
        groundBody.restitution = 0.6
        
        // Wire the transform of the ground's rigid body to it's node
        physicsScene.attach(groundBody, to: groundNode)
    }
    
    private func setupBouncyBall() {
        
        // Create a visual node representing a bouncy ball, and add it to our scene
        let ballGeometry = SCNSphere(radius: 1.0)
        let ballNode = SCNNode(geometry: ballGeometry)
        scene.rootNode.addChildNode(ballNode)
        
        // Create a physics body for the bouncy ball and add it to our physics world
        let ballShape = PKCollisionShapeSphere(radius: 1.0)
        let ballBody = PKRigidBody(type: .dynamic(mass: 1.0), shape: ballShape)
        physicsWorld.add(ballBody)
        
        // Make the ball a little bouncy
        ballBody.restitution = 0.6
        
        // Wire the transform of the ball's rigid body to it's node
        physicsScene.attach(ballBody, to: ballNode)
        
    }
    
}

extension ViewController: SCNSceneRendererDelegate {
    
    func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
        sceneTime = sceneTime ?? time
        let physicsTime = time - sceneTime!
        
        // Here we get to the root of the SO question: We can now manually increment the physicsWorld's
        // simulation time, irrespective of the scene's render loop. We could iteratively increment
        // this simulationTime value as many times as we want in a single render cycle of the scene view.
        physicsWorld.simulationTime = physicsTime
        physicsScene.iterativelyOrientAllNodesToAttachedRigidBodies()
    }
    
}

I won't mark this as an accepted answer as it doesn't really answer the original question, but I wanted to include it here for future reference in the event it helps anyone. Maybe in the future SceneKit will be updated to support manual physics steps.

  • Adam
Related