n5ro / aframe-physics-system

Physics system for A-Frame VR, built on CANNON.js.

Home Page:https://n5ro.github.io/aframe-physics-system/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Ammo raycastVehicle jitters when using chase camera

hayden2114 opened this issue · comments

Hey Don -

I've setup a simple test scene (using your ammo driver with default settings) with a btRaycastVehicle and a camera that follows the vehicle around. The issue is that the chase camera 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://keen-lamarr-078385.netlify.app/. It may take 10+ seconds for the skateboard to load in. Control it 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.

I didn't see any implementation of a RaycastVehicle in your documentation but did find it in your code, although I couldn't figure out how to use it so I created my own btRaycastVehicle and injected it into the scene manually:

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



AFRAME.registerComponent('skateboard_physics', {
    
    init() {
        
        // - Global variables -

        this.Ammo = window.Ammo;
        var Ammo = this.Ammo;
        this.scene = this.el.sceneEl.object3D;

        // Helpers
        this.DISABLE_DEACTIVATION = 4;
        this.TRANSFORM_AUX = new Ammo.btTransform();
        this.VECTOR_AUX = new Ammo.btVector3();
        this.QUATERNION_AUX = new Ammo.btQuaternion();
        this.ZERO_VECTOR = new Ammo.btVector3(0, 0, 0);
        this.ZERO_QUATERNION = new THREE.Quaternion(0, 0, 0, 1);
        
        // Graphics variables
        this.boardGLTF = null; 
        this.body = null;
        this.GltfScale = new THREE.Vector3(0.01, 0.01, 0.01);
        this.startingPos = new THREE.Vector3(0, 4, -.5);
        
        // Physics variables
        this.system = this.el.sceneEl.systems.physics;
        this.physicsWorld = this.el.sceneEl.systems.physics.driver.physicsWorld;

        if (this.el.sceneEl.hasLoaded) {
            this.initGraphics();
        } else {
            this.el.sceneEl.addEventListener('loaded', this.initGraphics.bind(this));
        }

       
    },

    initGraphics: function () {

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

        this.loadBoardGLTF();
    },

    keyup: function (e) {
        if (window.keysActions[e.code]) {
            window.actions[window.keysActions[e.code]] = false;
            window.keysAlreadyPressed[e.code] = false;
            e.preventDefault();
            e.stopPropagation();
            return false;
        }
    },

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

    loadBoardGLTF: function () {
        var loader = new THREE.GLTFLoader();

        // Optional: Provide a DRACOLoader instance to decode compressed mesh data
        // var dracoLoader = new DRACOLoader();
        // dracoLoader.setDecoderPath( '/examples/js/libs/draco/' );
        // loader.setDRACOLoader( dracoLoader );

        loader.load(
            // resource URL
            '/models/newest-skateboard.glb',
            // loaded
            ( gltf ) => {
              
                this.boardGLTF = gltf.scene;
                this.scene.add( gltf.scene );

                this.createSkateboard(this.startingPos, this.ZERO_QUATERNION);

                document.dispatchEvent(new CustomEvent('skateboardGLTFLoaded', {'detail': gltf.scene}));
            },
            // progressing
            function ( xhr ) {
                // console.log( ( xhr.loaded / xhr.total * 100 ) + '% loaded' );
            },
            // errors
            function ( error ) {
                console.log( 'An error happened', error );
            }
        );
        
    },

    createSkateboard: function (pos, quat) {

        var Ammo = this.Ammo;

        var bbox = new THREE.Box3().setFromObject(this.boardGLTF);
        var bbHeight = bbox.max.y - bbox.min.y;
        var bbWidth = bbox.max.x - bbox.min.x;
        var bbLength = bbox.max.z - bbox.min.z;
    
        // - Vehicle vars -
    
        // back wheel
        var wheelAxisPositionBack = -0.4; // z position of wheels
        var wheelRadiusBack = 0.03;
        var wheelWidthBack = 0.05;
        var wheelHalfTrackBack = bbWidth; // spread between wheels (x pos)
        var wheelAxisHeightBack = 0.05; // y position of wheels compared with chassis
        
        // front wheel
        var wheelAxisFrontPosition = 0.4; // z position of wheels
        var wheelRadiusFront = 0.03;
        var wheelWidthFront = 0.05;
        var wheelHalfTrackFront = bbWidth; // spread between wheels (x pos)
        var wheelAxisHeightFront = 0.05; // y position of wheels compared with chassis

        // chassis
        var chassisWidth = bbWidth;
        var chassisHeight = bbHeight;
        var chassisLength = bbLength;
        var suspensionStiffness = 5; // 20
        var suspensionDamping = 0.575; // 2.3
        var suspensionCompression = 1.1; // 4.4
        var suspensionRestLength = 0.15;
        var rollInfluence = 0.05;

        // speed
        this.breakingForce = 0;
        this.engineForce = 0;
        var massVehicle = 6;
        var wheelFriction = 14;
    
        // steering
        this.vehicleSteering = 0;
        this.steeringIncrement = .5;
        this.steeringClamp = 1;

        // constraints
        this.maxEngineForce = 42;
        this.maxBreakingForce = 5;
        this.maxSpeed = 21;
        var linearDamping = 0.1;
        var angularDamping = 0.1;

        // tricks
        this.ollieImpulse = new Ammo.btVector3(0, 21, 0);

        // DAT.gui
        if (window.debug) {
            var folder = window.Gui.addFolder('Skateboard');
            
            
            folder.add({steeringIncrement: this.steeringIncrement}, 'steeringIncrement', .01, 5).onChange(function (e) {
                this.steeringIncrement = e;
            });
            folder.add({steeringClamp: this.steeringClamp}, 'steeringClamp', .01, 5).onChange(function (e) {
                this.steeringClamp = e;
            });
            folder.add({maxEngineForce: this.maxEngineForce}, 'maxEngineForce', 10, 2000).onChange(function (e) {
                this.maxEngineForce = e;
            });
            folder.add({maxBreakingForce: this.maxBreakingForce}, 'maxBreakingForce', 1, 200).onChange(function (e) {
                this.maxBreakingForce = e;
            });
            folder.add({maxSpeed: this.maxSpeed}, 'maxSpeed', 10, 100).onChange(function (e) {
                this.maxSpeed = e;
            });
            
            
            
            folder.add({ollieImpulse: 21}, 'ollieImpulse', 1, 500).onChange(function (e) {
                this.ollieImpulse = new Ammo.btVector3(0, e, 0);
            });
            // folder.add({massVehicle: massVehicle}, 'massVehicle', 10, 500).onChange(function (e) {
            //     massVehicle = e;
            // });
        }
    
        // Vehicle 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(this.DISABLE_DEACTIVATION);
        body.setDamping(linearDamping, angularDamping);

    
        // Raycast Vehicle
        var tuning = new Ammo.btVehicleTuning();
        var rayCaster = new Ammo.btDefaultVehicleRaycaster(this.physicsWorld);
        var vehicle = new Ammo.btRaycastVehicle(tuning, body, rayCaster);
        vehicle.setCoordinateSystem(0, 1, 2);
        this.vehicle = vehicle;
        this.body = vehicle.getRigidBody();
        document.dispatchEvent(new CustomEvent('vehicleLoaded', {'detail': this.vehicle}));
    
        // Wheels
        var FRONT_LEFT = 0; this.FRONT_LEFT = FRONT_LEFT;
        var FRONT_RIGHT = 1; this.FRONT_RIGHT = FRONT_RIGHT;
        var BACK_LEFT = 2; this.BACK_LEFT = BACK_LEFT;
        var BACK_RIGHT = 3; this.BACK_RIGHT = BACK_RIGHT;
        var wheelMeshes = [];
        var wheelDirectionCS0 = new Ammo.btVector3(0, -1, 0);
        var wheelAxleCS = new Ammo.btVector3(-1, 0, 0);
    
        const addWheel = (isFront, pos, radius, width, index) => {
    
            var wheelInfo = this.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(wheelFriction);
            wheelInfo.set_m_rollInfluence(rollInfluence);
    
            wheelMeshes[index] = this.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);
    
        this.addToSystem();
    },

    createWheelMesh: function (radius, width) {
        var t = new THREE.CylinderGeometry(radius, radius, width, 24, 1);
        t.rotateZ(Math.PI / 2);
        var wireframeMaterial = new THREE.MeshBasicMaterial({wireframe: true});
        var mesh = new THREE.Mesh(t, wireframeMaterial);
        // scene.add(mesh);
        return mesh;
    },

    addToSystem: function () {
        //   this.system.addBody(this.body, this.data.collisionFilterGroup, this.data.collisionFilterMask);
        // system.addBody wasn't working so I added the addCustomBody function to aframe-physics-system which accepts an el instead of using body.el
        this.system.driver.addCustomBody(this.body, this.el, 1, 1);
        
        
        this.system.driver.addEventListener(this.body);
        
        // TODO: handle collision events
        //   if (this.data.emitCollisionEvents) {
        //     this.system.driver.addEventListener(this.body);
        //   }

        this.system.addComponent(this);

        this.physicsWorld.addAction(this.vehicle);
    },

    step: function (t, dt) {
        const Ammo = this.Ammo;
        const speed = this.vehicle.getCurrentSpeedKmHour();
    
        this.breakingForce = 0;
        this.engineForce = 0;
        this.vehicleSteering = 0;

        if (window.actions.acceleration) {
            if (speed < -1)
                this.breakingForce = this.maxBreakingForce;
            else if (speed > this.maxSpeed) {
                this.breakingForce = this.maxBreakingForce;
                this.engineForce = this.maxEngineForce;
            }
            else 
                this.engineForce = this.maxEngineForce;
        }
        if (window.actions.braking) {
            if (speed > 1)
                this.breakingForce = this.maxBreakingForce;
            else if (speed < -this.maxSpeed) {
                this.breakingForce = this.maxBreakingForce;
                this.engineForce = this.maxEngineForce;
            }
            else 
                this.engineForce = -this.maxEngineForce / 1.2; // reverse a bit slower than forwards 
        }
        if (window.actions.left) {
            if (this.vehicleSteering < this.steeringClamp)
                this.vehicleSteering += this.steeringIncrement;
        }
        else {
            if (window.actions.right) {
                if (this.vehicleSteering > -this.steeringClamp)
                    this.vehicleSteering -= this.steeringIncrement;
            }
            else {
                if (this.vehicleSteering < -this.steeringIncrement)
                    this.vehicleSteering += this.steeringIncrement;
                else {
                    if (this.vehicleSteering > this.steeringIncrement)
                        this.vehicleSteering -= this.steeringIncrement;
                    else {
                        this.vehicleSteering = 0;
                    }
                }
            }
        }

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

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

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

        var transform, pos, quat, i;

        transform = this.vehicle.getChassisWorldTransform();
        pos = transform.getOrigin();
        quat = transform.getRotation();
        if (this.boardGLTF != null) {
            // graphically update the board
            this.boardGLTF.position.set(pos.x(), pos.y(), pos.z());
            this.boardGLTF.quaternion.set(quat.x(), quat.y(), quat.z(), quat.w());
        }
        // chassisMesh.position.set(pos.x(), pos.y(), pos.z());
        // chassisMesh.quaternion.set(quat.x(), quat.y(), quat.z(), quat.w());

        // var n = vehicle.getNumWheels();
        // for (i = 0; i < n; i++) {
        //     vehicle.updateWheelTransform(i, true);
        //     transform = vehicle.getWheelTransformWS(i);
        //     pos = transform.getOrigin();
            // // quat = tm.getRotation(); // using chassis quaternion so wheels don't appear to turn
            // wheelMeshes[i].position.set(pos.x(), pos.y(), pos.z());
            // wheelMeshes[i].quaternion.set(quat.x(), quat.y(), quat.z(), quat.w());
        // }

        // ollie feature
        if (!window.keysAlreadyPressed['Space'] && window.actions.ollie) {
            console.log('ollie!!'); 
            this.body.applyCentralImpulse(this.ollieImpulse);

            window.keysAlreadyPressed['Space'] = true; // make sure this only fires once
        }

        // rollover feature
        if (!window.keysAlreadyPressed['KeyR'] && window.actions.rollover) {
            console.log('rollover!!'); 
            transform = this.vehicle.getChassisWorldTransform();
            this.VECTOR_AUX = transform.getOrigin();
            this.QUATERNION_AUX = new Ammo.btQuaternion(0, 0, 0, 1);
            this.TRANSFORM_AUX = new Ammo.btTransform(this.QUATERNION_AUX, this.VECTOR_AUX);
            this.body.setCenterOfMassTransform(this.TRANSFORM_AUX);

            window.keysAlreadyPressed['KeyR'] = true; // make sure this only fires once
        }
        
    }
});

Here's the follow_board component (chase camera):

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 0 0' },
        speed: { type: 'number', default: 5 },
        active: { type: 'bool', default: true }
    },

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

        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.originalXRotation = this.cameraRig3D.rotation.x;

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

    },


    // update camera position to follow the board with a slightly delayed and smooth movement
    tick: function (time, timeDelta) {
        if (!this.boardLoaded || !this.data.active) return;

        var targetVec = this.targetVec;
        var boardPos = this.boardGltf.position.clone();
        var targetPos = boardPos.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;
        targetVec.x *= factor;
        targetVec.y *= factor;
        targetVec.z *= factor;

        // make x and z position of camera match board position + offset
        if (Number.isNaN(currentPos.x)) { // handles case when board starts out of the camera's FOV
            this.cameraRig3D.position.set(targetPos.x, targetPos.y, targetPos.z);
            console.warn("camera position unkown");
        } else {
            // 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.1); // this helps the jitters
        }
    }
});

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

FYI, I tried updating the camera position in the afterStep call but that didn't help it. I added an ammo-body to the camera rig and then changed the tick to afterStep and called addComponent

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 0 0' },
        speed: { type: 'number', default: 5 },
        active: { type: 'bool', default: true }
    },

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

        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.originalXRotation = this.cameraRig3D.rotation.x;

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

        this.el.sceneEl.systems.physics.addComponent(this);

    },


    // update camera position to follow the board with a slightly delayed and smooth movement
    afterStep: function (time, timeDelta) {
        if (!this.boardLoaded || !this.data.active) return;

        var targetVec = this.targetVec;
        var boardPos = this.boardGltf.position.clone();
        var targetPos = boardPos.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;
        targetVec.x *= factor;
        targetVec.y *= factor;
        targetVec.z *= factor;

        // make x and z position of camera match board position + offset
        if (Number.isNaN(currentPos.x)) { // handles case when board starts out of the camera's FOV
            this.cameraRig3D.position.set(targetPos.x, targetPos.y, targetPos.z);
            console.warn("camera position unkown");
        } else {
            // 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.1); // this helps the jitters
        }
    }
});
commented

hey @haydeng21 best would be a codesandbox.io sample and maybe @donmccurdy or @InfiniteLee could look at this

Micah here, I wrote to Haydeng21 as n5ro, I said something, he replied, and then I wrote a further reply. I didn't realize I was commenting on a different github at the time, so I am copying the exchange here before closing this issue:

n5ro commented 23 hours 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?

haydeng21 commented 15 minutes 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 2 minutes 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