Interpolation in the physical world

I see that various physics engines are wrapped and not used by the engine to their full potential. When experimenting with Box2D - I wrote my own interpolation logic with autoSimulation turned off, but now I’m testing 3d (CannonJS) and even in the physics engine that has interpolation logic under the hood - it is not used because it is wrapped in its own accumulators for calling .step(…) and synchronizes bodies accordingly through position and not interpolatedPosition. And it seems like it will be more difficult to modify without breaking other functionality of the wrapper.

My target devices are weak TVs, so reducing the load by increasing the fixedTimeStep and restoring smoothness by interpolation is mandatory.

Are there any plans to update the approach to using physics engines?

This is working for me so far, but needs more testing. !!! cannon.js !!!

import { _decorator, PhysicsSystem, Component, director, DirectorEvent, Vec3, ERigidBodyType, Quat } from 'cc';
const { ccclass } = _decorator;

@ccclass('PhysicsManager')
export class PhysicsManager extends Component {
    public static instance: PhysicsManager;

    start() {
        PhysicsManager.instance = this;

        const physicsSystem = PhysicsSystem.instance;
        if(physicsSystem.autoSimulation) {
            console.log('[!!!] Disable auto physics simulation in project settings.');
            physicsSystem.autoSimulation = false;
        }
        
        const cannonWorld = physicsSystem.physicsWorld as any;
        cannonWorld._world.addEventListener('preStep', this.preStep.bind(this));
        cannonWorld._world.addEventListener('postStep', this.postStep.bind(this));
    }

    preStep(){
        //console.log('preStep');
        const physicsSystem = PhysicsSystem.instance;
        physicsSystem.physicsWorld.syncSceneToPhysics();
        director.emit(DirectorEvent.BEFORE_PHYSICS);
    }
    postStep(){
        //console.log('postStep');
        const physicsSystem = PhysicsSystem.instance;
        physicsSystem.physicsWorld.emitEvents();
        director.emit(DirectorEvent.AFTER_PHYSICS);
    }

    update (deltaTime: number) {
        const physicsSystem = PhysicsSystem.instance;

        const cannonWorld = physicsSystem.physicsWorld as any;
        cannonWorld._world.step(physicsSystem.fixedTimeStep, deltaTime, physicsSystem.maxSubSteps);
        
        for (let i = 0; i < cannonWorld.bodies.length; i++) {
            const sharedBody = cannonWorld.bodies[i];
            const node = sharedBody.node;
            const body = sharedBody.body;
            if (body.type === ERigidBodyType.DYNAMIC) {
                if (!body.isSleeping()) {
                    Vec3.copy(node.worldPosition, body.interpolatedPosition);
                    node.worldPosition = node.worldPosition;
                    Quat.copy(node.worldRotation, body.interpolatedQuaternion);
                    node.worldRotation = node.worldRotation;
                }
            }
        }
    }
}
import { _decorator, CCFloat, Component, RigidBody, input, KeyCode, Vec3, Input, DirectorEvent, director, PhysicsSystem } from 'cc';
const { ccclass, property } = _decorator;

@ccclass('BallController')
export class BallController extends Component {
    @property(CCFloat)
    speed: number = 10;

    body: RigidBody;

    private pressedKeys: Set<KeyCode> = new Set();

    start() {
        this.body = this.getComponent(RigidBody);
        
    }
    onEnable() {
        input.on(Input.EventType.KEY_DOWN, this.onKeyDown, this);
        input.on(Input.EventType.KEY_UP, this.onKeyUp, this);
        director.on(DirectorEvent.BEFORE_PHYSICS, this.fixedUpdate, this);
    }
    onDisable() {
        input.off(Input.EventType.KEY_DOWN, this.onKeyDown, this);
        input.off(Input.EventType.KEY_UP, this.onKeyUp, this);
        director.off(DirectorEvent.BEFORE_PHYSICS, this.fixedUpdate, this);
    }

    onKeyDown(event: any) {
        this.pressedKeys.add(event.keyCode);
    }

    onKeyUp(event: any) {
        this.pressedKeys.delete(event.keyCode);
    }

    fixedUpdate() {
        //console.log('fixedUpdate');
        const torque = new Vec3(0, 0, 0);
        
        if (this.pressedKeys.has(KeyCode.ARROW_LEFT)) {
            torque.z += 1;
        }
        else if (this.pressedKeys.has(KeyCode.ARROW_RIGHT)) {
            torque.z -= 1;
        }
        if (this.pressedKeys.has(KeyCode.ARROW_UP)) {
            torque.x -= 1;
        }
        else if (this.pressedKeys.has(KeyCode.ARROW_DOWN)) {
            torque.x += 1;
        }
        
        if (torque.x !== 0 || torque.y !== 0 || torque.z !== 0) {
            torque.normalize();
            this.body.applyTorque(torque.multiplyScalar(this.speed));
        }
    }
}
1 Like

Some fixes and polish

import { _decorator, PhysicsSystem, Component, director, DirectorEvent, Vec3, ERigidBodyType, Quat, Node } from 'cc';
const { ccclass, property } = _decorator;

@ccclass('PhysicsManager')
export class PhysicsManager extends Component {
    public static instance: PhysicsManager;

    @property iterations: number = 6;
    @property tolerance: number = 0.001;

    private _tempVec3 = new Vec3();
    private _tempQuat = new Quat();
    
    private _stepping: boolean = false;

    onLoad() {
        PhysicsManager.instance = this;

        const physicsSystem = PhysicsSystem.instance;
        if(physicsSystem.autoSimulation) {
            console.error('[!!!] Disable Physics -> Auto Simulation in project settings.');
            physicsSystem.autoSimulation = false; // this is somehow broken and must be defined in the config
            this.enabled = false; // to make it clear that something is wrong 
        }
        
        const cannonWorld = physicsSystem.physicsWorld as any;
        cannonWorld._world.solver.iterations = this.iterations;
        cannonWorld._world.solver.tolerance = this.tolerance;
        cannonWorld._world.default_dt = physicsSystem.fixedTimeStep;
        
        cannonWorld._world.addEventListener('preStep', this.preStep.bind(this));
    }

    preStep(){
        if(this._stepping) return; //only for first substep
        this._stepping = true;
        //console.log('preStep');

        const physicsSystem = PhysicsSystem.instance;
        physicsSystem.physicsWorld.syncSceneToPhysics();
        director.emit(DirectorEvent.BEFORE_PHYSICS);
    }
    postStep(){
        //console.log('postStep');
        const physicsSystem = PhysicsSystem.instance;
        physicsSystem.physicsWorld.emitEvents(); // needed?
        director.emit(DirectorEvent.AFTER_PHYSICS);
    }

    update (deltaTime: number) {
        if(this._stepping) {
            this._stepping = false;
        }

        const physicsSystem = PhysicsSystem.instance;
        const cannonWorld = physicsSystem.physicsWorld as any;
        cannonWorld._world.step(physicsSystem.fixedTimeStep, deltaTime, physicsSystem.maxSubSteps);
        
        for (let i = 0; i < cannonWorld.bodies.length; i++) {
            const sharedBody = cannonWorld.bodies[i];
            const body = sharedBody.body;
            const node: Node = sharedBody.node;
            if (body.type === ERigidBodyType.DYNAMIC && !body.isSleeping()) {
                node.worldPosition = Vec3.copy(this._tempVec3, body.interpolatedPosition);
                node.worldRotation = Quat.copy(this._tempQuat, body.interpolatedQuaternion);;
            }
        }
        if(this._stepping) {
            cannonWorld._world.accumulator %= physicsSystem.fixedTimeStep; //adjusting if accumulator > step
            this.postStep();
        }
    }
}
1 Like

Nice!