Collision Shapes Don't Match the 3D Model

Hello,

I’m having a collision issue. I have one thread for rendering and one thread for physics. When I load my two models into a worker (for physics) and create the collision shapes, then send them to the rendering thread, the shapes appear correctly as the ones corresponding to the models (as shown in the screenshots). However, the collision shapes being created don’t seem to match the ones sent to the rendering thread. They appear to be creating bounding boxes around my models, likely rectangular ones.

As a result, even when Model 1 doesn’t exactly touch Model 2, collisions are detected, as if a bounding box is being used. Can anyone help identify the problem in my code ?

I use Havok physics.

I’m not familiar with the “Playground” but i probably could upload my code into an archive if needed.

Thanks.

(when the shape of the spaceship is red on the screenshot, it means that a collision is detected)

function createPhysicsBody(meshes, name) {
    console.log(`Creating physics body for ${name}`);
    
    // Create a compound mesh to represent the physical body
    const compoundMesh = BABYLON.Mesh.MergeMeshes(meshes, true, true, undefined, false, true);
    compoundMesh.name = name;

    // Create a physics shape container
    const shapeContainer = new BABYLON.PhysicsShapeContainer(scene);

    // Add the mesh shape to the container
    shapeContainer.addChild(new BABYLON.PhysicsShapeMesh(compoundMesh, scene));

    // Create the physics body with the shape container
    const physicsBody = new BABYLON.PhysicsBody(compoundMesh, BABYLON.PhysicsMotionType.DYNAMIC, false, scene);
    physicsBody.shape = shapeContainer;

    physicsBody.setMassProperties({
        mass: 1000,
        inertia: new BABYLON.Vector3(100, 100, 100),
        centerOfMass: new BABYLON.Vector3(0, 0, 0),
    });

    // Store the physics body in the physicsBodies object
    physicsBodies[name] = { mesh: compoundMesh, body: physicsBody };

    // Send the mesh data to the main thread (for rendering/debuging)
    const positions = compoundMesh.getVerticesData(BABYLON.VertexBuffer.PositionKind);
    const indices = compoundMesh.getIndices();

    self.postMessage({
        type: 'meshCreated',
        name: name,
        meshData: {
            positions: positions,
            indices: indices
        }
    });

    return physicsBody;
}

function checkCollisions() {
    const bodies = Object.values(physicsBodies);
    const collisionStates = {};

    for (let i = 0; i < bodies.length; i++) {
        const body1 = bodies[i].mesh;
        collisionStates[body1.name] = false;
        
        for (let j = i + 1; j < bodies.length; j++) {
            const body2 = bodies[j].mesh;
            
            if (body1 instanceof BABYLON.Mesh && body2 instanceof BABYLON.Mesh) {
                if (body1.intersectsMesh(body2, true)) { // Use 'true' to check for precise intersections with polygons
                    collisionStates[body1.name] = true;
                    collisionStates[body2.name] = true;
                    console.log(`Collision detected between ${body1.name} and ${body2.name}`);
                }
            }
        }
    }

    self.postMessage({
        type: 'collisionUpdate',
        collisionStates: collisionStates
    });
}


Maybe @eoin can have an idea or @Cedric but please bear with us as it is vacation time. A playground would be great if you can isolate a minimal repro there. If not sharing a ready to go archive would be fine.

1 Like

@Marvin have you tried HavokPlugin.getCollisionObservable() instead of Mesh.intersectsMesh()?

1 Like

Or what HiGreg suggested may be better: V2 Physics Collision Detection - #3 by HiGreg

1 Like

Also, a couple of small suggestions:

Your array named physicsBodies contains a mesh and the physicsBody already associated with that mesh. I don’t think there will be a physics collision with a mesh, only with physics bodies. Neither of those objects has a .mesh property, so your line “const body1 = bodies[i].mesh;” I think will silently fail (with a message in the browser’s console log).

There’s no real need to keep a physicsBody variable around since you can access a mesh’s physicsBody with “mesh.physicsBody” and you can access a mesh from “physicsBody.transformNode”.

From within the plugin.onCollisionObservable, you could access the bodies as

  • collision.collider
  • collision.collidedAgainst

Or the meshes with

  • collision.collider.transformNode
  • collision.collidedAgainst.transformNode

Edit to add: I don’t think there’s a reason to have a shapeContainer that contains a single physics Shape. Just use the physics Shape directly.

3 Likes

You really think i didn’t check the console.log before posting my message here ? There is no error, and if it had failed, then why would it detect the collision? You think i used photoshop to paint the body in red and i came here with a lie ? The collision is detected, checkCollisions is informing my main thread (rendering thread) that the collision is detected and this is why the body is changing it’s color in red… it’s written on my message. I used shapeContainer because i had already tried many different methods and the result was exactly the same. In the end, i published the last code i tried. The problem is the shape of the body used for the collision.

By the way the documentation and examples are very poorly written.

Check my video.

(the movement of the spaceship is controlled by the main thread not the physics from the worker. For the moment, I’m just working on collision detection. The fact that the spaceship is passing through the other spaceship isn’t the problem. I will share an archive with my projet today)

I think people are just trying to help …

Babylon.js and its documentation are open source and accept contributions from anyone if you have some suggestions for improvements.

3 Likes

Suggestions for improvements? examples on how to use Havok, including how to load two OBJ models with their meshes and detect collisions between them. The most importants stuff when it comes to create a game.

I know it’s open source and that people are here to help. I’m not very good at English, and sometimes I might use the wrong pronunciation.

Thanks for clarifying. We’re all patient and understanding here, especially with so many users (maybe even the majority) for whom English is not their native language.

But please try not to accuse people of calling you a liar (they weren’t) and attacking the project and its contributors. We’re very welcoming as a community, but we do expect civility.

https://forum.babylonjs.com/faq

3 Likes

I missed this line in your code. No offense intended.

It was not clear in your message text that you checked console.log. Again, no offense intended. Not everyone who posts questions are as thorough as you, so it’s not always clear what steps and checks each person who posts actually go through.

I was volunteering my time to try to help. The other points I posted are still relevant, I think. Have a look at plugin.onCollisionObservable and see if that meets your needs.

1 Like

Okay, no problem :wink: .
I’ll try this today.

Thank you.

English or no english, I HIGHLY encourage you to be kinder and more patient in the future.

2 Likes

Ok, I improved some stuff about the physics, but the problem of the collision shapes is still here. It’s better, but still present (it’s not a rectangular box anymore, but the shapes are still incorrect).

I uploaded my project into an archive. I also translated some comments. The project uses “Vite.”
Link archive: Upload Files | Free File Upload and Transfer Up To 20 GB (you have to do “npm install” and “npm run dev” to run it)

You can press “G” or “F” to make the camera follow the main model, and use the arrows to move the model. You can also hold the left-click and then move your mouse to move the camera, or you can press CTRL + LEFT CLICK and then move the mouse to rotate the camera (this does not work if you have the “G” view activated because the camera is locked on the model).

The problem is probably coming from the function “createPhysicsBody” in the file “collisionWorker.js.” There is also a function “checkCollisions,” and I don’t know why but now it always says that the bodies are colliding even when they are not, while the function “setupCollisionDetection,” which uses “havokPlugin.onCollisionObservable.add,” does not detect anything.
(I haven’t finished synchronizing the model’s movements between both threads, so there’s no need to mention the unsmooth movements :slight_smile: )

There is a new version of my “createPhysicsBody.” If you have any new suggestions I could try, but honestly, I have tried a lot of different things over the last 4 days and nothing worked. I also tried ChatGPT and Perplexity AI, they gave me a bunch of different solutions that i tried, but nothing solved the problem. I reduced the number of polygons/meshes with Cinema4D, but it didn’t improve anything.

Thank you to everyone for your help.

You can reach me by PM with a BNB (Binance Smart Chain) address or ETH (Ethereum main net) address to receive compensation if you succeed in solving my problem.

function createPhysicsBody(meshes, name) {
    console.log(`Creating physics body for ${name}`);

    if (!meshes || meshes.length === 0) {
        console.error(`No valid meshes provided for ${name}`);
        return null;
    }

    let compoundMesh;

    // If there is only one mesh, no need to merge
    if (meshes.length === 1) {
        compoundMesh = meshes[0];
    } else {
        // Filter valid meshes
        const validMeshes = meshes.filter(mesh =>
            mesh && mesh.getVerticesData && mesh.getVerticesData(BABYLON.VertexBuffer.PositionKind)
        );

        if (validMeshes.length === 0) {
            console.error(`No valid meshes to merge for ${name}`);
            return null;
        }

        // Attempt the merge
        compoundMesh = BABYLON.Mesh.MergeMeshes(validMeshes, true, true, undefined, false, true);
    }

    if (!compoundMesh) {
        console.error(`Failed to create compound mesh for ${name}`);
        return null;
    }

    compoundMesh.name = name;

    // Define initial position and rotation
    let initialPosition, initialRotation, mass, motionType;
    if (name === "SF_CockpitG") {
        mass = 1; // A very low mass
        initialPosition = new BABYLON.Vector3(0, 0, 0);
        initialRotation = BABYLON.Quaternion.RotationYawPitchRoll(Math.PI, 0, 0);
        motionType = BABYLON.PhysicsMotionType.DYNAMIC;
    } else if (name === "Carrier_VX-8") {
        mass = 100000; // A very high mass
        initialPosition = new BABYLON.Vector3(3, 0, 0);
        initialRotation = BABYLON.Quaternion.RotationYawPitchRoll(Math.PI, 0, 0);
        motionType = BABYLON.PhysicsMotionType.DYNAMIC;
    } else {
        mass = 1000;
        initialPosition = new BABYLON.Vector3(0, 0, 0);
        initialRotation = BABYLON.Quaternion.Identity();
        motionType = BABYLON.PhysicsMotionType.DYNAMIC;
    }

    // Apply initial position and rotation to the mesh
    compoundMesh.position = initialPosition;
    compoundMesh.rotationQuaternion = initialRotation;

    console.log(compoundMesh)
    // Create the physics body
    const physicsBody = new BABYLON.PhysicsBody(compoundMesh, motionType, false, scene);

    // Create the physics shape
    const physicsShape = new BABYLON.PhysicsShapeMesh(compoundMesh, scene);

    // Set the shape of the physics body
    havokPlugin.setShape(physicsBody, physicsShape);

    // Apply initial position and rotation directly to the physics body
    physicsBody.transformNode.position = initialPosition;
    physicsBody.transformNode.rotationQuaternion = initialRotation;

    // Define mass properties
    havokPlugin.setMassProperties(physicsBody, {
        mass: mass,
        inertia: new BABYLON.Vector3(mass * 100, mass * 100, mass * 100),
        centerOfMass: new BABYLON.Vector3(0, 0, 0),
    });

    // Adjust friction and restitution properties
    let friction, restitution;
    if (name === "SF_CockpitG") {
        friction = 0.1; // Low friction
        restitution = 0.1; // Low bounce
    } else if (name === "Carrier_VX-8") {
        friction = 0.8; // High friction
        restitution = 0.2; // Moderate bounce
    } else {
        friction = 0.5;
        restitution = 0.5;
    }

    // Apply friction and restitution to the physics shape
    physicsShape.material = {
        friction: friction,
        restitution: restitution
    };

    // Store the physics body in the physicsBodies object
    physicsBodies[name] = physicsBody;

    // Send mesh information to the main thread
    const positions = compoundMesh.getVerticesData(BABYLON.VertexBuffer.PositionKind);
    const indices = compoundMesh.getIndices();

    self.postMessage({
        type: 'meshCreated',
        name: name,
        meshData: {
            positions: positions,
            indices: indices
        }
    });

    console.log(`Physics body created for ${name} at position:`, initialPosition);
}

function setupCollisionDetection() {
    havokPlugin.onCollisionObservable.add((collisionEvent) => {
        const bodyA = collisionEvent.collider;
        const bodyB = collisionEvent.collidedAgainst;

        // Filter specific collisions
        if ((bodyA.transformNode.name === "SF_CockpitG" && bodyB.transformNode.name === "Carrier_VX-8") ||
            (bodyA.transformNode.name === "Carrier_VX-8" && bodyB.transformNode.name === "SF_CockpitG")) {
            console.log("Collision detected between SF_CockpitG and Carrier_VX-8");
        }

    });
}
function simulatePhysics() {
    applyControls();
    
    // Convert the physicsBodies object into an array of physics bodies
    const physicsBodiesArray = Object.values(physicsBodies);

    // Simulate physics
    havokPlugin.executeStep(1 / fpsControl, physicsBodiesArray);

    // Correct positions and rotations after simulation
    const targetHeight = 1; // Use the same value as in applyControls
    for (const body of physicsBodiesArray) {
        // Maintain constant height
        body.transformNode.position.y = targetHeight;

        // Reset vertical velocity
        const currentVelocity = new BABYLON.Vector3();
        havokPlugin.getLinearVelocityToRef(body, currentVelocity);
        currentVelocity.y = 0;
        havokPlugin.setLinearVelocity(body, currentVelocity);

        // Maintain vertical rotation (prevent tipping)
        const currentRotation = body.transformNode.rotationQuaternion;
        const eulerAngles = currentRotation.toEulerAngles();
        const correctedRotation = BABYLON.Quaternion.RotationYawPitchRoll(
            eulerAngles.y, // Preserve rotation around the Y-axis (yaw)
            0,             // Reset rotation around the X-axis (pitch) to 0
            0              // Reset rotation around the Z-axis (roll) to 0
        );
        body.transformNode.rotationQuaternion = correctedRotation;

        // Reset angular velocity
        havokPlugin.setAngularVelocity(body, BABYLON.Vector3.Zero());
    }

    const positions = {};
    for (const [name, body] of Object.entries(physicsBodies)) {
        const position = body.transformNode.position;
        const rotation = body.transformNode.rotationQuaternion;
        positions[name] = {
            x: position.x,
            y: position.y,
            z: position.z,
            rotation: {
                x: rotation.x,
                y: rotation.y,
                z: rotation.z,
                w: rotation.w
            }
        };
    }

    self.postMessage({
        type: 'positionUpdate',
        positions: positions
    });
    
    checkCollisions();
}

function checkCollisions() {
    const bodies = Object.values(physicsBodies);
    const collisionStates = {};

    for (let i = 0; i < bodies.length; i++) {
        const body1 = bodies[i].transformNode;
        collisionStates[body1.name] = false;

        for (let j = i + 1; j < bodies.length; j++) {
            const body2 = bodies[j].transformNode;
            if (body1 instanceof BABYLON.Mesh && body2 instanceof BABYLON.Mesh) {
                if (body1.intersectsMesh(body2, true)) { // Use 'true' to check for precise polygon intersections
                    collisionStates[body1.name] = true;
                    collisionStates[body2.name] = true;
                    console.log(`Collision detected between ${body1.name} and ${body2.name}`);
                }
            }
        }
    }

    self.postMessage({
        type: 'collisionUpdate',
        collisionStates: collisionStates
    });
}

@Deltakosh Ok, i’m sorry. I will do my best now.

disablePreStep?

Be sure to note the first comment. Constantly resetting disablePreStep may not be necessary.

Edit: I know that changing shape.density requires a subsequent reassignment of shape to body, but not sure about shape.material. If friction or restitution seem not to have an effect in your code, try reassigning the shape to the body.

1 Like

@HiGreg thanks, I already tried to activate/deactivate disablePreStep and unfortunately, it doesn’t change anything. I tried so many different things that I don’t even know what to try anymore.

I simplified the code, but I still have the same result. I’m probably going to move to Godot as I have tried so many different techniques and nothing is working. Even here the meshes look good: https://playground.babylonjs.com/#WHBL0X#26

I can remove the rotation and stuff like that, it still the same problem.


async function loadGLTFFiles(paths) {
    try {
        console.log("Starting to load glTF files");
        const model1 = await loadGLTFModel(paths.model);
        const model2 = await loadGLTFModel(paths.model13);
        console.log("glTF models loaded in worker:", model1, model2);
        createPhysicsBody(model1, "SF_CockpitG");
        createPhysicsBody(model2, "Carrier_VX-8");
        setupCollisionDetection(); // Setup collision detection after loading models
        self.postMessage({ type: 'gltfModelsLoaded' });
    } catch (error) {
        console.error("Error in loading glTF files:", error);
        self.postMessage({ type: 'loadError', error: error.message });
    }
}

async function loadGLTFModel(gltfFilePath) {
    return new Promise((resolve, reject) => {
        BABYLON.SceneLoader.ImportMesh("", "", gltfFilePath, scene, (meshes, particleSystems, skeletons, animationGroups) => {
            console.log("Loaded meshes:", meshes);
            if (meshes.length === 0) {
                reject(new Error("No meshes loaded from glTF file"));
                return;
            }
            resolve(meshes);
        }, null, (scene, message) => {
            reject(new Error(`Failed to load glTF file: ${message}`));
        });
    });
}

function createPhysicsBody(meshes, name) {
    console.log(`Creating physics body for ${name}`);
    if (!meshes || meshes.length === 0) {
        console.error(`No valid meshes provided for ${name}`);
        return null;
    }

    let tempArrayOfMeshes = [];
    for (let i = 1; i < meshes.length; i++) {
        meshes[i].setParent(null);
        tempArrayOfMeshes.push(meshes[i]);
    }

    var compoundMesh = BABYLON.Mesh.MergeMeshes(tempArrayOfMeshes, true, true, undefined, false, true);
    meshes[0].dispose();
    compoundMesh.name = name;

    let initialPosition, initialRotation, mass;
    if (name === "SF_CockpitG") {
        mass = 1;
        initialPosition = new BABYLON.Vector3(0, 0, 0);
        initialRotation = BABYLON.Quaternion.RotationYawPitchRoll(Math.PI, 0, 0);
    } else if (name === "Carrier_VX-8") {
        mass = 10000;
        initialPosition = new BABYLON.Vector3(3, 0, 0);
        initialRotation = BABYLON.Quaternion.RotationYawPitchRoll(Math.PI, 0, 0);
    }

    compoundMesh.position = initialPosition;
    compoundMesh.rotationQuaternion = initialRotation;

    const meshAggregate = new BABYLON.PhysicsAggregate(compoundMesh, BABYLON.PhysicsShapeType.MESH, { mass: mass, restitution: 0 }, scene);
    meshAggregate.body.setMassProperties({
        mass: mass,
        inertia: new BABYLON.Vector3(0, 0, 0)
    });

    physicsBodies[name] = meshAggregate.body;

    const positions = compoundMesh.getVerticesData(BABYLON.VertexBuffer.PositionKind);
    const indices = compoundMesh.getIndices();
    self.postMessage({ type: 'meshCreated', name: name, meshData: { positions: positions, indices: indices } });
}

cc @RaananW to help if he has some cycles

You can change your code: await BABYLON.SceneLoader.ImportMeshAsync.

You need to get familiar with it to get the answers you are seeking for. It’s really simple.

My project uses different threads and scenes for rendering, collisions, and loading the models,
so it’s unfortunately not “really simple” to remake everything into a playground, at least not for me :wink: and not only it’s not that simple but i can’t have any garantee that my problem will be solved.
However, i’m 100% sure it will work if i remake it with Godot (in fact i’m worried to lose one more week to try to make everything working into a playground all that maybe for nothing good). I just wanted to give WebGPU/JS/BabylonJS a chance to create what i believe could be a good MMORTS, apart of this collision problem the benchmarks show pretty good results.

I have already used async in the other codes i shared before but anyway i don’t really need async because basically in the production code all the models will be loaded from another thread for performance reason (which is already the case but i simplified the code before sharing it here).
It’s better to focus solely on the collision problem. Thank anyway.

I added a y offset to put the mesh above the ground. I also added a body collision observer to print out every collision event.

Interestingly, I couldn’t get the worldwide collision observer to fire, just the body one.

2 Likes