kripken / ammo.js

Direct port of the Bullet physics engine to JavaScript using Emscripten
Other
4.1k stars 556 forks source link

btRaycastVehicle has major jitters when using a chase camera #310

Open hayden2114 opened 4 years ago

hayden2114 commented 4 years ago

Hey Alon -

I've setup a simple test scene with a btRaycastVehicle and a camera that follows the vehicle around (follow_board). The issue is that the follow_board component creates a visual jitter on the chassis of the vehicle.

I'm wondering if you have any insight on how to remedy this issue as it results in an unpleasant experience and the experience I'm building relies upon a chase camera. One semi-solution I've found is to drop the fixedTimeStep to 0.005 or lower but it doesn't solve it on all devices. I've also tested multiple different implementations of the chase camera but none have solved it.

Here's a link to the test scene: https://loving-franklin-009677.netlify.app/. Control the vehicle with wasd. There's also a button to toggle the follow_board component so you can see that it's the main reason for the jitters.

This is built in a-frame and is using your most recent build of ammo.js. Here's the two components of importance:

const AFRAME = window.AFRAME;
const THREE = AFRAME.THREE;

// camera follows board
AFRAME.registerComponent('follow_board', {
    schema: {
        posOffset: {type: 'vec3', default: '0 1 -3'},
        rotation: {type: 'vec3', default: '45 180 0'},
        speed: {type: 'number', default: 5},
        active: {type: 'bool', default: true}
    },

    init() {
        if (!this.data.active) return;

        // init vars
        this.boardLoaded = false;
        this.targetVec = new THREE.Vector3();
        this.positionOffset = this.data.posOffset;
        this.cameraSpeed = this.data.speed;

        // init camera rotation
        this.cameraRig3D = this.el.object3D;
        this.cameraRig3D.rotation.x = THREE.Math.degToRad(this.data.rotation.x);
        this.cameraRig3D.rotation.y = THREE.Math.degToRad(this.data.rotation.y);
        this.originalXRotation = this.cameraRig3D.rotation.x;

        document.addEventListener('skateboardLoaded', (e) => {
            this.skateboard = e.detail;
            this.boardLoaded = true;
        });

    },

    tick: function (time, timeDelta) {
        if (!this.boardLoaded || !this.data.active) return;

        var targetVec = this.targetVec;
        var targetPos = this.skateboard.position.clone().add(this.positionOffset);
        var currentPos = this.cameraRig3D.position;

        targetVec.copy(targetPos).sub(currentPos);

        var distance = targetVec.length();
        if (distance < .05) return;

        var factor = distance * this.cameraSpeed;
        ['x', 'y', 'z'].forEach(function (axis) {
            targetVec[axis] *= factor * (timeDelta / 1000);
        });

        // make x and z position of camera match board position + offset
        targetPos.set(currentPos.x + targetVec.x, currentPos.y + targetVec.y, currentPos.z + targetVec.z);
        this.cameraRig3D.position.lerp(targetPos, 0.75); // this helps the jitters some

    }

});

const AFRAME = window.AFRAME;
const THREE = AFRAME.THREE;
const Ammo = window.Ammo;

AFRAME.registerComponent('skateboard', {

    init() {

        // - Global variables -
        var DISABLE_DEACTIVATION = 4;
        var TRANSFORM_AUX = new Ammo.btTransform();
        var ZERO_QUATERNION = new THREE.Quaternion(0, 0, 0, 1);

        // Graphics variables
        var scene;
        var clock = new THREE.Clock();
        var materialDynamic, materialStatic, materialInteractive;

        // Physics variables
        var collisionConfiguration;
        var dispatcher;
        var broadphase;
        var solver;
        var physicsWorld;

        var syncList = [];
        var time = 0;
        var objectTimePeriod = 3;

        // Keybord actions
        var actions = {};
        var keysActions = {
            "KeyW":'acceleration',
            "KeyS":'braking',
            "KeyA":'left',
            "KeyD":'right'
        };

        // - Functions -

        function initGraphics() {

            scene = document.getElementById("scene").object3D;

            window.addEventListener( 'keydown', keydown);
            window.addEventListener( 'keyup', keyup);
        }

        function initPhysics() {

            // Physics configuration
            collisionConfiguration = new Ammo.btDefaultCollisionConfiguration();
            dispatcher = new Ammo.btCollisionDispatcher( collisionConfiguration );
            broadphase = new Ammo.btDbvtBroadphase();
            solver = new Ammo.btSequentialImpulseConstraintSolver();
            physicsWorld = new Ammo.btDiscreteDynamicsWorld( dispatcher, broadphase, solver, collisionConfiguration );
            physicsWorld.setGravity( new Ammo.btVector3( 0, -9.82, 0 ) );
        }

        function tick() {
            requestAnimationFrame( tick );
            var dt = clock.getDelta();
            for (var i = 0; i < syncList.length; i++)
                syncList[i](dt);
            physicsWorld.stepSimulation( dt, 10 );
            time += dt;
        }

        function keyup(e) {
            if(keysActions[e.code]) {
                actions[keysActions[e.code]] = false;
                e.preventDefault();
                e.stopPropagation();
                return false;
            }
        }
        function keydown(e) {
            if(keysActions[e.code]) {
                actions[keysActions[e.code]] = true;
                e.preventDefault();
                e.stopPropagation();
                return false;
            }
        }

        function createBox(pos, quat, w, l, h, mass, friction) {
            var material = mass > 0 ? materialDynamic : materialStatic;
            var shape = new THREE.BoxGeometry(w, l, h, 1, 1, 1);
            var geometry = new Ammo.btBoxShape(new Ammo.btVector3(w * 0.5, l * 0.5, h * 0.5));

            if(!mass) mass = 0;
            if(!friction) friction = 1;

            var mesh = new THREE.Mesh(shape, material);
            mesh.position.copy(pos);
            mesh.quaternion.copy(quat);
            scene.add( mesh );

            var transform = new Ammo.btTransform();
            transform.setIdentity();
            transform.setOrigin(new Ammo.btVector3(pos.x, pos.y, pos.z));
            transform.setRotation(new Ammo.btQuaternion(quat.x, quat.y, quat.z, quat.w));
            var motionState = new Ammo.btDefaultMotionState(transform);

            var localInertia = new Ammo.btVector3(0, 0, 0);
            geometry.calculateLocalInertia(mass, localInertia);

            var rbInfo = new Ammo.btRigidBodyConstructionInfo(mass, motionState, geometry, localInertia);
            var body = new Ammo.btRigidBody(rbInfo);

            body.setFriction(friction);
            //body.setRestitution(.9);
            //body.setDamping(0.2, 0.2);

            physicsWorld.addRigidBody( body );

            if (mass > 0) {
                body.setActivationState(DISABLE_DEACTIVATION);
                // Sync physics and graphics
                function sync(dt) {
                    var ms = body.getMotionState();
                    if (ms) {
                        ms.getWorldTransform(TRANSFORM_AUX);
                        var p = TRANSFORM_AUX.getOrigin();
                        var q = TRANSFORM_AUX.getRotation();
                        mesh.position.set(p.x(), p.y(), p.z());
                        mesh.quaternion.set(q.x(), q.y(), q.z(), q.w());
                    }
                }

                syncList.push(sync);
            }
        }

        function createWheelMesh(radius, width) {
            var t = new THREE.CylinderGeometry(radius, radius, width, 24, 1);
            t.rotateZ(Math.PI / 2);
            var mesh = new THREE.Mesh(t, materialInteractive);
            mesh.add(new THREE.Mesh(new THREE.BoxGeometry(width * 1.5, radius * 1.75, radius*.25, 1, 1, 1), materialInteractive));
            scene.add(mesh);
            return mesh;
        }

        function createChassisMesh(w, l, h) {
            var shape = new THREE.BoxGeometry(w, l, h, 1, 1, 1);
            var mesh = new THREE.Mesh(shape, materialInteractive);
            scene.add(mesh);
            return mesh;
        }

        function createVehicle(pos, quat) {

            // Vehicle contants

            var chassisWidth = 1.8 / 4;
            var chassisHeight = .6 / 4;
            var chassisLength = 4 / 4;
            var massVehicle = 800 / 4;

            var wheelAxisPositionBack = -1 / 4;
            var wheelRadiusBack = .4 / 4;
            var wheelWidthBack = .3 / 4;
            var wheelHalfTrackBack = 1 / 4;
            var wheelAxisHeightBack = .3 / 4;

            var wheelAxisFrontPosition = 1.7 / 4;
            var wheelHalfTrackFront = 1 / 4;
            var wheelAxisHeightFront = .3 / 4;
            var wheelRadiusFront = .35 / 4;
            var wheelWidthFront = .2 / 4;

            var friction = 1000;
            var suspensionStiffness = 20.0 / 4;
            var suspensionDamping = 2.3 / 4;
            var suspensionCompression = 4.4 / 4;
            var suspensionRestLength = 0.6 / 4;
            var rollInfluence = 0.2 / 4;

            var steeringIncrement = .04;
            var steeringClamp = .5;
            var maxEngineForce = 2000;
            var maxBreakingForce = 100;

            // Chassis
            var geometry = new Ammo.btBoxShape(new Ammo.btVector3(chassisWidth * .5, chassisHeight * .5, chassisLength * .5));
            var transform = new Ammo.btTransform();
            transform.setIdentity();
            transform.setOrigin(new Ammo.btVector3(pos.x, pos.y, pos.z));
            transform.setRotation(new Ammo.btQuaternion(quat.x, quat.y, quat.z, quat.w));
            var motionState = new Ammo.btDefaultMotionState(transform);
            var localInertia = new Ammo.btVector3(0, 0, 0);
            geometry.calculateLocalInertia(massVehicle, localInertia);
            var body = new Ammo.btRigidBody(new Ammo.btRigidBodyConstructionInfo(massVehicle, motionState, geometry, localInertia));
            body.setActivationState(DISABLE_DEACTIVATION);
            physicsWorld.addRigidBody(body);
            var chassisMesh = createChassisMesh(chassisWidth, chassisHeight, chassisLength);
            setTimeout(function() {
                document.dispatchEvent(new CustomEvent('skateboardLoaded', {'detail': chassisMesh}));
            }, 1000);

            // Raycast Vehicle
            var engineForce = 0;
            var vehicleSteering = 0;
            var breakingForce = 0;
            var tuning = new Ammo.btVehicleTuning();
            var rayCaster = new Ammo.btDefaultVehicleRaycaster(physicsWorld);
            var vehicle = new Ammo.btRaycastVehicle(tuning, body, rayCaster);
            vehicle.setCoordinateSystem(0, 1, 2);
            physicsWorld.addAction(vehicle);

            // Wheels
            var FRONT_LEFT = 0;
            var FRONT_RIGHT = 1;
            var BACK_LEFT = 2;
            var BACK_RIGHT = 3;
            var wheelMeshes = [];
            var wheelDirectionCS0 = new Ammo.btVector3(0, -1, 0);
            var wheelAxleCS = new Ammo.btVector3(-1, 0, 0);

            function addWheel(isFront, pos, radius, width, index) {

                var wheelInfo = vehicle.addWheel(
                        pos,
                        wheelDirectionCS0,
                        wheelAxleCS,
                        suspensionRestLength,
                        radius,
                        tuning,
                        isFront);

                wheelInfo.set_m_suspensionStiffness(suspensionStiffness);
                wheelInfo.set_m_wheelsDampingRelaxation(suspensionDamping);
                wheelInfo.set_m_wheelsDampingCompression(suspensionCompression);
                wheelInfo.set_m_frictionSlip(friction);
                wheelInfo.set_m_rollInfluence(rollInfluence);

                wheelMeshes[index] = createWheelMesh(radius, width);
            }

            addWheel(true, new Ammo.btVector3(wheelHalfTrackFront, wheelAxisHeightFront, wheelAxisFrontPosition), wheelRadiusFront, wheelWidthFront, FRONT_LEFT);
            addWheel(true, new Ammo.btVector3(-wheelHalfTrackFront, wheelAxisHeightFront, wheelAxisFrontPosition), wheelRadiusFront, wheelWidthFront, FRONT_RIGHT);
            addWheel(false, new Ammo.btVector3(-wheelHalfTrackBack, wheelAxisHeightBack, wheelAxisPositionBack), wheelRadiusBack, wheelWidthBack, BACK_LEFT);
            addWheel(false, new Ammo.btVector3(wheelHalfTrackBack, wheelAxisHeightBack, wheelAxisPositionBack), wheelRadiusBack, wheelWidthBack, BACK_RIGHT);

            // Sync keybord actions and physics and graphics
            function sync(dt) {

                var speed = vehicle.getCurrentSpeedKmHour();

                breakingForce = 0;
                engineForce = 0;

                if (actions.acceleration) {
                    if (speed < -1)
                        breakingForce = maxBreakingForce;
                    else engineForce = maxEngineForce;
                }
                if (actions.braking) {
                    if (speed > 1)
                        breakingForce = maxBreakingForce;
                    else engineForce = -maxEngineForce / 2;
                }
                if (actions.left) {
                    if (vehicleSteering < steeringClamp)
                        vehicleSteering += steeringIncrement;
                }
                else {
                    if (actions.right) {
                        if (vehicleSteering > -steeringClamp)
                            vehicleSteering -= steeringIncrement;
                    }
                    else {
                        if (vehicleSteering < -steeringIncrement)
                            vehicleSteering += steeringIncrement;
                        else {
                            if (vehicleSteering > steeringIncrement)
                                vehicleSteering -= steeringIncrement;
                            else {
                                vehicleSteering = 0;
                            }
                        }
                    }
                }

                vehicle.applyEngineForce(engineForce, BACK_LEFT);
                vehicle.applyEngineForce(engineForce, BACK_RIGHT);

                vehicle.setBrake(breakingForce / 2, FRONT_LEFT);
                vehicle.setBrake(breakingForce / 2, FRONT_RIGHT);
                vehicle.setBrake(breakingForce, BACK_LEFT);
                vehicle.setBrake(breakingForce, BACK_RIGHT);

                vehicle.setSteeringValue(vehicleSteering, FRONT_LEFT);
                vehicle.setSteeringValue(vehicleSteering, FRONT_RIGHT);

                var tm, p, q, i;
                var n = vehicle.getNumWheels();
                for (i = 0; i < n; i++) {
                    vehicle.updateWheelTransform(i, true);
                    tm = vehicle.getWheelTransformWS(i);
                    p = tm.getOrigin();
                    q = tm.getRotation();
                    wheelMeshes[i].position.set(p.x(), p.y(), p.z());
                    wheelMeshes[i].quaternion.set(q.x(), q.y(), q.z(), q.w());
                }

                tm = vehicle.getChassisWorldTransform();
                p = tm.getOrigin();
                q = tm.getRotation();
                chassisMesh.position.set(p.x(), p.y(), p.z());
                chassisMesh.quaternion.set(q.x(), q.y(), q.z(), q.w());
            }

            syncList.push(sync);
        }

        function createObjects() {
            createBox(new THREE.Vector3(0, -0.5, 0), ZERO_QUATERNION, 75, 1, 75, 0, 2);
            createVehicle(new THREE.Vector3(0, 4, -2), ZERO_QUATERNION);
        }

        // - Init -
        initGraphics();
        initPhysics();
        createObjects();
        tick();

    }
});

Thanks in advance for any help or guidance you can offer! Please let me know if you'd like me to provide other code or info.

All the best, Hayden Greer

willeastcott commented 4 years ago

Hi @haydeng21. I can only advise on how PlayCanvas does things. But for my raycast vehicle project, I just ensure that the camera tracks the vehicle after the simulation has stepped. In PlayCanvas, you do that by updating the camera in a postUpdate function instead of the vanilla update function. Otherwise, yep, you get jitters. Maybe a similar solution works for you.

hayden2114 commented 4 years ago

Thanks for replying @willeastcott! In the actual project we're using aframe-physics-system with the ammo.js driver. The vehicle updates in step and I tried hooking the camera into the afterStep call in the physics system but unfortunately there was no noticeable difference.

arpu commented 4 years ago

@haydeng21 maybe make a codesandbox from your example and post it in the aframe-physics-system Issue Tracker

hayden2114 commented 4 years ago

Hey @arpu! I have submitted an issue on the aframe-physics-system repo (https://github.com/donmccurdy/aframe-physics-system/issues/153#issue-642045495)

n5ro commented 3 years ago

Hi haydeng21, can you share a working sample so I can look at it and attempt to figure out what is causing the jitter?

It looks like what you shared before is no longer operational ( https://loving-franklin-009677.netlify.app/ ) is that because you solved the issue?

hayden2114 commented 3 years ago

I solved or at least patched the issue by setting the fixedTimeStep to 0.005. The scene was still lagging and jittering on some devices, which seemed to be caused by a separate issue that was fixed by reducing texture count + sizes and adding a ceiling to the fps at 72.

With that being said, the jitter issues will still occur when the fixedTimeStep is set back to it's default, which I believe is 0.016. But in this case the 0.005 value is the solution I needed!

n5ro commented 3 years ago

I will go ahead and close the issue for now. By the way have you seen how to improve page performance by using web workers in javascript? "3D World Generation #7: Speeding it up via Threading (JavaScript Web Workers & Three.js)" If you are not already using workers for multi-threading it might reduce your lag & jitter on multi-core devices. https://www.youtube.com/watch?v=a1L7k35EHIc&ab_channel=SimonDev (I meant I will close the issue on the Aframe Physics page, not here)

hayden2114 commented 3 years ago

I've never seen that before, thanks for sharing the knowledge!

seojoon-y commented 1 year ago

Hi, I came across this and thought it might help

image

https://doc.babylonjs.com/features/featuresDeepDive/physics/usingPhysicsEngine

seojoon-y commented 1 year ago

I think you should also instantiate scene after await Ammo() is complete.

ID-Emmett commented 2 months ago

I have also encountered this issue. I tried using getMotionState().getWorldTransform to get the vehicle's transformation and update it to the graphical object, which solved the problem partly. However, it seems that the rigid body of the vehicle still jitters, which prevents me from attaching cloth or soft body physics to the vehicle stably.