Syncing a GLTF car model with Cannon-es

Hi all,

I’m trying to load a GLTF model of a car and sync it with a rigid vehicle from Cannon-es in order to create a drivable car. I started by setting up the physics world and that went extremely smoothly:

/**
 * Physics
 */

// World

const world = new CANNON.World()
world.broadphase = new CANNON.SAPBroadphase(world)
//world.allowSleep = true
world.gravity.set(0, -29.81, 0)

// Ground

const groundBody = new CANNON.Body({
    type: CANNON.Body.STATIC,
    shape: new CANNON.Plane(),
})

groundBody.quaternion.setFromEuler(-Math.PI * 0.5, -Math.PI * 0.00, 0)
world.addBody(groundBody)

// Default material

const defaultMaterial = new CANNON.Material('ground')
const defaultContactMaterial = new CANNON.ContactMaterial(
    defaultMaterial,
    defaultMaterial,
    {
        friction: 0.2,
        restitution: 0.0
    }
)
world.defaultContactMaterial = defaultContactMaterial

// Debugger

const cannonDebugger = new CannonDebugger(scene, world)

// Build the car chassis
const chassisShape = new CANNON.Box(new CANNON.Vec3(5, 0.5, 2))
const chassisBody = new CANNON.Body({ mass: 1 })
const centerOfMassAdjust = new CANNON.Vec3(0, -1, 0)
chassisBody.addShape(chassisShape, centerOfMassAdjust)

// Create the vehicle
const vehicle = new CANNON.RigidVehicle({
    chassisBody,
})

const mass = 1
const axisWidth = 3
const wheelShape = new CANNON.Sphere(1.5)
const wheelMaterial = new CANNON.Material('wheel')
const down = new CANNON.Vec3(0, -1, 0)

const wheelBody1 = new CANNON.Body({ mass, material: wheelMaterial })
wheelBody1.addShape(wheelShape)
vehicle.addWheel({
    body: wheelBody1,
    position: new CANNON.Vec3(-5, 0, axisWidth / 2).vadd(centerOfMassAdjust),
    axis: new CANNON.Vec3(0, 0, 1),
    direction: down,
})

const wheelBody2 = new CANNON.Body({ mass, material: wheelMaterial })
wheelBody2.addShape(wheelShape)
vehicle.addWheel({
    body: wheelBody2,
    position: new CANNON.Vec3(-5, 0, -axisWidth / 2).vadd(centerOfMassAdjust),
    axis: new CANNON.Vec3(0, 0, -1),
    direction: down,
})

const wheelBody3 = new CANNON.Body({ mass, material: wheelMaterial })
wheelBody3.addShape(wheelShape)
vehicle.addWheel({
    body: wheelBody3,
    position: new CANNON.Vec3(5, 0, axisWidth / 2).vadd(centerOfMassAdjust),
    axis: new CANNON.Vec3(0, 0, 1),
    direction: down,
})

const wheelBody4 = new CANNON.Body({ mass, material: wheelMaterial })
wheelBody4.addShape(wheelShape)
vehicle.addWheel({
    body: wheelBody4,
    position: new CANNON.Vec3(5, 0, -axisWidth / 2).vadd(centerOfMassAdjust),
    axis: new CANNON.Vec3(0, 0, -1),
    direction: down,
})

vehicle.wheelBodies.forEach((wheelBody) => {
    // Some damping to not spin wheels too fast
    wheelBody.angularDamping = 0.4

})

vehicle.addToWorld(world)

I then loaded the GLTF model and defined independent variables for the wheels I selected by traversing the model. I also selected the body:

/**
 * GLTF Model
 */

const dracoLoader = new DRACOLoader()
dracoLoader.setDecoderPath('/draco/')

const gltfLoader = new GLTFLoader()
gltfLoader.setDRACOLoader(dracoLoader)

let model;
let wheel1;
let wheel2;
let wheel3;
let wheel4;
let whell5; // Spare wheel
let test_body;

gltfLoader.load(
    '/models/fw190_airplanecar/scene.gltf',
    (gltf) => {
        model = gltf.scene
        model.scale.set(0.025, 0.025, 0.025)

        model.traverse(function (node) {
          
            if (node.isMesh) {
                node.castShadow = true;
                node.receiveShadow = true;
                node.material.wireframe = true;
            }

            if (node.isMesh && node.name === "Circle043_rear_wheel_0") {
                wheel1 = node;
            }

            if (node.isMesh && node.name === "Circle001_front_wheel_0") {
                wheel2 = node;
            }

            if (node.isMesh && node.name === "Tyre-Left-Front_spare_wheel_0") {
                wheel3 = node;
            }

            if (node.isMesh && node.name === "Circle015_rear_wheel_0") {
                wheel4 = node;
            }

            if (node.isMesh && node.name === "Circle023_front_wheel_0") {
                //console.log("Nai re file")
                wheel5 = node;
            }

            if (node.isMesh && node.name === "Body_body_0") {
                test_body = node;
            }
        });

        scene.add(model)

        tick()
    }
)

I then tried to sync the rigid vehicle with the three.js GLTF model and strange things started happening. Here is the code:

const tick = () => {
    // Update controls
    controls.update()

    // Render
    renderer.render(scene, camera)

    // Physics

    world.fixedStep()
    cannonDebugger.update()

    model.position.copy(chassisBody.position)
    model.quaternion.copy(chassisBody.quaternion)

    wheel1.position.copy(wheelBody1.position)
    wheel1.quaternion.copy(wheelBody1.quaternion)

    // Call tick again on the next frame
    window.requestAnimationFrame(tick)
}

Specifically, the rigid vehicle and the GLTF model form a 90 degrees angle, the wheel appears in a different location than the GLTF, and when I use the keys I can see the wheels of the rigid vehicle (CANNON) working just fine, the GLTF model is kind of following the motion in a strange 90-degree angle, and the one wheel I show in the code wanders off the screen (it does spin though). So what is going on? Why is the wheel, selected from the tree nodes at a different location? And why is the model at a 90-degree angle, and a few meters up in the y-direction?
Relevant pics attached



Hi, you must noticed that wheel1 is a child node of the model. When you set its position like this, three.js will set the position of wheel1 relative to the model.

Something like this might help you understand more:

model.position.set(0, 5, 0)
wheel1.position.set(0, 10, 0) // actual position in scene: (0, 15, 0)

You could try add separate node to the scene instead of adding the entire model to the scene and try to modify its position independently.

As well as the rotation problem, it seems that the model is set up a different quaternion in the first place. You could try to adjust its rotation dynamically to fix this problem or modify the model itself.

model.rotation.set(chassisBody.rotation.x + Math.PI / 2, chassisBody.rotation.y, chassisBody.rotation.z)

Hi! Thank you very much for your reply. Your suggestions inspired me to try a few different things and I did make some progress but the car is still not working as expected. First of all, I changed the wheels from spheres to cylinders in Cannon-es:

const radiusTop = 1.5
const radiusBottom = 1.5
const height = 2
const numSegments = 24
const wheelShape = new CANNON.Cylinder(radiusTop, radiusBottom, height, numSegments)
const quaternion = new CANNON.Quaternion().setFromEuler(0, 0, -Math.PI / 2)

wheelBody1.addShape(wheelShape, new CANNON.Vec3(), quaternion)

Notice that in Cannon-es there are no rotations, only quaternions can be used.

I then synced the whole model to the Cannon-es chassis (I think this might be problematic) and then synced just the quaternions of the wheels (which I had to modify, in the sense that the z Cannon quaternion corresponded to the x Three.js quaternion, etc):

    model.position.copy(chassisBody.position)
    model.quaternion.x = chassisBody.quaternion.x
    model.quaternion.y = chassisBody.quaternion.y
    model.quaternion.z = chassisBody.quaternion.z
    model.quaternion.w = chassisBody.quaternion.w

    wheel1.quaternion.x = wheelBody2.quaternion.z
    wheel1.quaternion.y = wheelBody2.quaternion.y
    wheel1.quaternion.z = wheelBody2.quaternion.x
    wheel1.quaternion.w = wheelBody2.quaternion.w

    wheel4.quaternion.x = wheelBody1.quaternion.z
    wheel4.quaternion.y = wheelBody1.quaternion.y
    wheel4.quaternion.z = wheelBody1.quaternion.x
    wheel4.quaternion.w = wheelBody1.quaternion.w

    wheel2.quaternion.x = wheelBody3.quaternion.z
    wheel2.quaternion.y = wheelBody3.quaternion.x
    wheel2.quaternion.z = wheelBody3.quaternion.y
    wheel2.quaternion.w = wheelBody3.quaternion.w

    wheel5.quaternion.x = wheelBody3.quaternion.z
    wheel5.quaternion.y = wheelBody3.quaternion.x
    wheel5.quaternion.z = wheelBody3.quaternion.y
    wheel5.quaternion.w = wheelBody3.quaternion.w

Now the whole thing appears to be working for a few milliseconds before the wheels start spinning in a very weird way as the picture suggests.

Thank you very much for your help again!

Screenshot 2022-08-06 at 3.56.20 PM

Glad to see your progress!
Now you could try separate the car body into another group, or an Object3D object. Using this group to replace your previous model object to let the quaternion affect its own part.

Furthermore, I would recommend you modify 1 wheel at a time to find out which quaternion matches the right goal.

let carBody = new THREE.Object3D();
gltfLoader.load(
    '/models/fw190_airplanecar/scene.gltf',
    (gltf) => {
        model = gltf.scene
        model.scale.set(0.025, 0.025, 0.025)

        model.traverse(function (node) {
          
            if (node.isMesh) {
                node.castShadow = true;
                node.receiveShadow = true;
                node.material.wireframe = true;

                if (node.name === "Circle043_rear_wheel_0") {
                    wheel1 = node;
                    scene.add(wheel1);
                    return;
                }

                if (node.name === "Circle001_front_wheel_0") {
                    wheel2 = node;
                    scene.add(wheel2);
                    return;
                }

                if (node.name === "Tyre-Left-Front_spare_wheel_0") {
                    wheel3 = node;
                    scene.add(wheel3);
                    return;
                }

                if (node.name === "Circle015_rear_wheel_0") {
                    wheel4 = node;
                    scene.add(wheel4);
                    return;
                }

                if (node.name === "Circle023_front_wheel_0") {
                    wheel5 = node;
                    scene.add(wheel5);
                    return;
                }
                
                carBody.add(node); // rest of the car body part
            }
        });

        scene.add(carBody);

        tick()
    }
)

Hi again!

I finally managed to make some progress thanks to your help! I first synced a simple three.js car I created using primitive shapes (BoxGeometry, Cylinder, etc) to the Cannon-es car.

The reason the wheels were spinning in a crazy fashion was this piece of code in the Cannon part:

position: new CANNON.Vec3(-5, 0, axisWidth / 2).vadd(centerOfMassAdjust),

By removing the centerOfMassAdjust things started looking more normal. I then created my experimental Three.js car:

/**
 * Experimental three.js car
 */

const carGeometry = new THREE.BoxGeometry(4, 1.5, 10);
const carMaterial = new THREE.MeshBasicMaterial({ color: "white" });
carMaterial.wireframe = true
const chassisMesh = new THREE.Mesh(carGeometry, carMaterial);
scene.add(chassisMesh);

const rearWheelGeometry1 = new THREE.CylinderGeometry(rearRadiusTop, rearRadiusBottom, rearHeight, rearNumSegments, rearNumSegments)
const rearWheelMesh1 = new THREE.Mesh(rearWheelGeometry1, carMaterial)
scene.add(rearWheelMesh1)

const rearWheelGeometry2 = new THREE.CylinderGeometry(rearRadiusTop, rearRadiusBottom, rearHeight, rearNumSegments, rearNumSegments)
const rearWheelMesh2 = new THREE.Mesh(rearWheelGeometry2, carMaterial)
scene.add(rearWheelMesh2)

const frontWheelGeometry1 = new THREE.CylinderGeometry(frontRadiusTop, frontRadiusBottom, frontHeight, frontNumSegments, frontNumSegments)
const frontWheelMesh1 = new THREE.Mesh(frontWheelGeometry1, carMaterial)
scene.add(frontWheelMesh1)

const frontWheelGeometry2 = new THREE.CylinderGeometry(frontRadiusTop, frontRadiusBottom, frontHeight, frontNumSegments, frontNumSegments)
const frontWheelMesh2 = new THREE.Mesh(frontWheelGeometry2, carMaterial)
scene.add(frontWheelMesh2)

I then synced the Cannon-es.js and Three.js cars inside the tick() function:

function tick = () => {
 // Experimental three.js car

    chassisMesh.position.copy(chassisBody.position)
    chassisMesh.quaternion.copy(chassisBody.quaternion)

    frontWheelMesh2.position.copy(wheelBody4.position)
    frontWheelMesh2.quaternion.copy(wheelBody4.quaternion)
    frontWheelMesh2.rotateOnAxis(new THREE.Vector3(0, 0, 1), Math.PI / 2)

    frontWheelMesh1.position.copy(wheelBody3.position)
    frontWheelMesh1.quaternion.copy(wheelBody3.quaternion)
    frontWheelMesh1.rotateOnAxis(new THREE.Vector3(0, 0, 1), Math.PI / 2)

    rearWheelMesh1.position.copy(wheelBody1.position)
    rearWheelMesh1.quaternion.copy(wheelBody1.quaternion)
    rearWheelMesh1.rotateOnAxis(new THREE.Vector3(0, 0, 1), Math.PI / 2)


    rearWheelMesh2.position.copy(wheelBody2.position)
    rearWheelMesh2.quaternion.copy(wheelBody2.quaternion)
    rearWheelMesh2.rotateOnAxis(new THREE.Vector3(0, 0, 1), Math.PI / 2)

    // Call tick again on the next frame
    window.requestAnimationFrame(tick)
}

Notice how I had to use a rotation over the z-axis: frontWheelMesh2.rotateOnAxis(new THREE.Vector3(0, 0, 1), Math.PI / 2), because the three.js wheels were making a 90-degree angle with the cannon.js wheels.

Then I went back to the GLTF model. I loaded as follows:

gltfLoader.load(
    '/models/fw190_airplanecar/scene.gltf',
    (gltf) => {
        model = gltf.scene
   
        console.log(model)

        model.traverse(function (node) {        
            if (node.isMesh) {
                node.castShadow = true;
                node.receiveShadow = true;
                node.material.wireframe = true;  
                console.log(node.name)
            }

            if (node.isMesh && node.name === "Circle043_rear_wheel_0") {
                wheel1 = node;
            }

            // if (node.isMesh && node.name === "Circle214_disc_brake_0") {
            //     let disc1 = node;
            //     scene.add(disc1)
            // }

            // if (node.isMesh && node.name === "chevycamarobremsenfright001_disc_brake_0") {
            //     let disc2 = node;
            //     scene.add(disc2)
            // }

            if (node.isMesh && node.name === "Circle015_rear_wheel_0") {
                wheel4 = node;
            }

            if (node.isMesh && node.name === "Circle001_front_wheel_0") {
                wheel2 = node;
            }

            // if (node.isMesh && node.name === "Tyre-Left-Front_spare_wheel_0") {
            //     wheel3 = node;
            // }

            if (node.isMesh && node.name === "Circle023_front_wheel_0") {
                wheel5 = node;
            }

        });
       
        model.scale.set(0.023, 0.023, 0.023)

        scene.add(model)
        scene.add(wheel1)
        scene.add(wheel4)

        scene.add(wheel2)
        scene.add(wheel5)

        tick()

        // Animation
        //mixer = new THREE.AnimationMixer(gltf.scene)
        //const action = mixer.clipAction(gltf.animations[2])
        // action.play()
    }
)

Then inside the tick() function I did the following:

function tick() = {
// GLTF car

     model.position.copy(chassisBody.position)
     model.quaternion.copy(chassisBody.quaternion)
     model.position.y = model.position.y + debugPosition.amount
 
     wheel1.position.copy(wheelBody1.position)
     wheel1.quaternion.copy(wheelBody1.quaternion)
     wheel1.rotateOnAxis(new THREE.Vector3(0, 1, 0), -Math.PI / 2)

     wheel4.position.copy(wheelBody2.position)
     wheel4.quaternion.copy(wheelBody2.quaternion)
     wheel4.rotateOnAxis(new THREE.Vector3(0, 1, 0), -Math.PI / 2)

     wheel2.position.copy(wheelBody3.position)
     wheel2.quaternion.copy(wheelBody3.quaternion)
     wheel2.rotateOnAxis(new THREE.Vector3(0, 0, 1), -Math.PI / 2)

     wheel5.position.copy(wheelBody4.position)
     wheel5.quaternion.copy(wheelBody4.quaternion)
     wheel5.rotateOnAxis(new THREE.Vector3(0, 0, 1), -Math.PI / 2)
 
    wheel1.scale.set(2,2, 2)
    wheel4.scale.set(2,2,2)

    wheel2.scale.set(2,2, 2)
    wheel5.scale.set(2,2,2)

}

And it’s now almost there. I have a few more questions if you want to bother:

  1. When I set wheel1 = node, wheel2 = node, etc, and then add the wheels to the scene, it seems like they are removed from the model, i.e., somehow when I do scene.add(model ), only the chassis without the wheels is added to the scene

  2. I needed to scale the wheels up because they were smaller than the model. Why did this happen?

  3. Is there a smarter way of doing this? I.e., I had to spend a lot of time trying things out!

  4. I tried the carBody idea and added nodes to it but everything appears like a pile on top of each other on the screen if I do that. So I went with using the model

Thank you very much again for all your help!!

I was able to figure this out. It turns out that the model itself is an Object3D, but Cannon only works with meshes (THREE.Mesh) objects. Thus all I had to do was to extract the car body (THREE.Mesh) and sync it to the Cannon body. In fact, I ended up using a whole different model than the one above, and you can find the final result here. A link to the source code is also provided in the lower right corner of the scene, however, here is the code anyhow