A few questions about Physics and Editor

Hi there.

I dive into physics part of PC and got some questions.

Mesh-to-Mesh collision.
Bullet engine doesn’t deal with it, right? Okay, what about hull and how to build it?

I guess divide my mesh to primitives for collisions is my best move, but how can I add more than one primitive to collision component?

Edit bounding box with Editor
What is Physics Edit Mode? The only difference I see is always visible collision zones of entities.

I thought it gives possibility of manipulating with bounding box, but no.
So, the only option for bounding box is it’s size. How can I move my bounding box, cuz by default its under than I need.

Thanks a lot for attention,
Mike.

So you need to put the collider in an Entity child of your object, so you can move it. But that won’t help with physics if you are doing real physics - no compound colliders have been implemented last time I looked. (Ended up writing my own collision detection, interpenetration prevention and raycasting solution for this and performance reasons).

I’ve got a quick hull implementation from that if you need it. That will output what I need for a collision surface (but might not be enough to make a PC mesh collider without work).

/*
     * QuickHull function to produce convex hulls and faces
     */
export default (function () {
    /*
     QuickHull
     ---------
     The MIT License
     Copyright © 2010-2014 three.js authors
     Permission is hereby granted, free of charge, to any person obtaining a copy
     of this software and associated documentation files (the "Software"), to deal
     in the Software without restriction, including without limitation the rights
     to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     copies of the Software, and to permit persons to whom the Software is
     furnished to do so, subject to the following conditions:
     The above copyright notice and this permission notice shall be included in
     all copies or substantial portions of the Software.
     THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
     IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
     FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
     AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
     LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
     OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
     THE SOFTWARE.
     @author mark lundin / http://mark-lundin.com
     This is a 3D implementation of the Quick Hull algorithm.
     It is a fast way of computing a convex hull with average complexity
     of O(n log(n)).
     It uses depends on three.js and is supposed to create THREE.Geometry.

     It's also very messy

     */
    var w = new pc.Vec3();
    var faces = [],
        faceStack = [],
        i, NUM_POINTS, extremes,
        max = 0,
        dCur, current, j, v0, v1, v2, v3,
        N = new pc.Vec3(), D;

    var ab, ac, ax,
        subA, subB, normal,
        diff, subAA, subBB, subC;

    function reset() {

        ab = new pc.Vec3();
        ac = new pc.Vec3();
        ax = new pc.Vec3();
        subA = new pc.Vec3();
        subB = new pc.Vec3();
        normal = new pc.Vec3();
        diff = new pc.Vec3();
        subAA = new pc.Vec3();
        subBB = new pc.Vec3();
        subC = new pc.Vec3();

    }

    //temporary vectors

    function process(points) {

        // Iterate through all the faces and remove
        while (faceStack.length > 0) {
            cull(faceStack.shift(), points);
        }
    }

    var ca = new pc.Vec3();
    var ba = new pc.Vec3();

    var norm = function () {

        return function (a, b, c) {

            ca.copy(c).sub(a);
            ba.copy(b).sub(a);

            N.cross(ca, ba);

            return N.normalize();
        }

    }();

    function getNormal(face, points) {

        if (face.normal !== undefined) return face.normal;

        var p0 = points[face[0]],
            p1 = points[face[1]],
            p2 = points[face[2]];

        ab.copy(p1).sub(p0);
        ac.copy(p2).sub(p0);
        normal.cross(ac, ab);
        normal.normalize();

        return face.normal = normal.clone();

    }

    function assignPoints(face, pointset, points) {

        var p0 = points[face[0]],
            dots = [], apex,
            norm = getNormal(face, points);

        pointset.sort(function (aItem, bItem) {

            dots[aItem.x / 3] =
                dots[aItem.x / 3] !== undefined ? dots[aItem.x / 3] : norm.dot(subA.copy(aItem).sub(p0));
            dots[bItem.x / 3] =
                dots[bItem.x / 3] !== undefined ? dots[bItem.x / 3] : norm.dot(subB.copy(bItem).sub(p0));

            return dots[aItem.x / 3] - dots[bItem.x / 3];
        });

        var index = pointset.length;

        if (index === 1) dots[pointset[0].x / 3] = norm.dot(subA.copy(pointset[0]).sub(p0));
        while (index-- > 0 && dots[pointset[index].x / 3] > 0) {
            var point;
        }
        if (index + 1 < pointset.length && dots[pointset[index + 1].x / 3] > 0) {

            face.visiblePoints = pointset.splice(index + 1);
        }
    }

    function cull(face, points) {

        var i = faces.length,
            dot, visibleFace, currentFace,
            visibleFaces = [face];

        var apex = points.indexOf(face.visiblePoints.pop());

        // Iterate through all other faces...
        while (i-- > 0) {
            currentFace = faces[i];
            if (currentFace !== face) {
                // ...and check if they're pointing in the same direction
                dot = getNormal(currentFace, points).dot(diff.copy(points[apex]).sub(points[currentFace[0]]));
                if (dot > 0) {
                    visibleFaces.push(currentFace);
                }
            }
        }

        var index, neighbouringIndex, vertex;

        var j = i = visibleFaces.length;
        var isDistinct = false,
            hasOneVisibleFace = i === 1,
            cull = [],
            perimeter = [],
            edgeIndex = 0, compareFace, nextIndex,
            a, b;

        var allPoints = [];
        var originFace = [
            visibleFaces[0][0],
            visibleFaces[0][1],
            visibleFaces[0][1],
            visibleFaces[0][2],
            visibleFaces[0][2],
            visibleFaces[0][0]
        ];

        if (visibleFaces.length === 1) {
            currentFace = visibleFaces[0];

            perimeter =
                [currentFace[0], currentFace[1], currentFace[1], currentFace[2], currentFace[2], currentFace[0]];
            // remove visible face from list of faces
            if (faceStack.indexOf(currentFace) > -1) {
                faceStack.splice(faceStack.indexOf(currentFace), 1);
            }

            if (currentFace.visiblePoints) allPoints = allPoints.concat(currentFace.visiblePoints);
            faces.splice(faces.indexOf(currentFace), 1);

        } else {

            while (i-- > 0) {	// for each visible face

                currentFace = visibleFaces[i];

                // remove visible face from list of faces
                if (faceStack.indexOf(currentFace) > -1) {
                    faceStack.splice(faceStack.indexOf(currentFace), 1);
                }

                if (currentFace.visiblePoints) allPoints = allPoints.concat(currentFace.visiblePoints);
                faces.splice(faces.indexOf(currentFace), 1);

                var isSharedEdge;
                var cEdgeIndex = 0;

                while (cEdgeIndex < 3) { // Iterate through it's edges

                    isSharedEdge = false;
                    j = visibleFaces.length;
                    a = currentFace[cEdgeIndex];
                    b = currentFace[(cEdgeIndex + 1) % 3];

                    while (j-- > 0 && !isSharedEdge) { // find another visible faces

                        compareFace = visibleFaces[j];
                        edgeIndex = 0;

                        // isSharedEdge = compareFace == currentFace;
                        if (compareFace !== currentFace) {

                            while (edgeIndex < 3 && !isSharedEdge) { //Check all it's indices

                                nextIndex = ( edgeIndex + 1 );
                                isSharedEdge =
                                    ( compareFace[edgeIndex] === a && compareFace[nextIndex % 3] === b ) ||
                                    ( compareFace[edgeIndex] === b && compareFace[nextIndex % 3] === a );

                                edgeIndex++;
                            }
                        }
                    }

                    if (!isSharedEdge || hasOneVisibleFace) {
                        perimeter.push(a);
                        perimeter.push(b);
                    }

                    cEdgeIndex++;
                }
            }
        }

        // create new face for all pairs around edge
        i = 0;
        var l = perimeter.length / 2;
        var f;

        while (i < l) {
            f = [perimeter[i * 2 + 1], apex, perimeter[i * 2]];
            assignPoints(f, allPoints, points);
            faces.push(f);
            if (f.visiblePoints !== undefined)faceStack.push(f);
            i++;
        }

    }

    var distSqPointSegment = function () {

        var ab = new pc.Vec3(),
            ac = new pc.Vec3(),
            bc = new pc.Vec3();

        return function (a, b, c) {

            ab.copy(b).sub(a);
            ac.copy(c).sub(a);
            bc.copy(c).sub(b);

            var e = ac.dot(ab);
            if (e < 0.0) return ac.dot(ac);
            var f = ab.dot(ab);
            if (e >= f) return bc.dot(bc);
            return ac.dot(ac) - e * e / f;

        }

    }();

    return function (points) {

        reset();

        faces = [];
        faceStack = [];
        i = NUM_POINTS = points.length;
        extremes = points.slice(0, 6);
        max = 0;

        /*
         * 	FIND EXTREMETIES
         */
        while (i-- > 0) {
            if (points[i].x < extremes[0].x) extremes[0] = points[i];
            if (points[i].x > extremes[1].x) extremes[1] = points[i];

            if (points[i].y < extremes[2].y) extremes[2] = points[i];
            if (points[i].y < extremes[3].y) extremes[3] = points[i];

            if (points[i].z < extremes[4].z) extremes[4] = points[i];
            if (points[i].z < extremes[5].z) extremes[5] = points[i];
        }

        /*
         *  Find the longest line between the extremeties
         */

        j = i = 6;
        while (i-- > 0) {
            j = i - 1;
            while (j-- > 0) {
                if (max < (dCur = w.copy(extremes[i]).sub(extremes[j]).lengthSq())) {
                    max = dCur;
                    v0 = extremes[i];
                    v1 = extremes[j];

                }
            }
        }

        // 3. Find the most distant point to the line segment, this creates a plane
        i = 6;
        max = 0;
        while (i-- > 0) {
            dCur = distSqPointSegment(v0, v1, extremes[i]);
            if (max < dCur) {
                max = dCur;
                v2 = extremes[i];
            }
        }

        // 4. Find the most distant point to the plane.

        N = norm(v0, v1, v2);
        D = N.dot(v0);

        max = 0;
        i = NUM_POINTS;
        while (i-- > 0) {
            dCur = Math.abs(points[i].dot(N) - D);
            if (max < dCur) {
                max = dCur;
                v3 = points[i];
            }
        }

        var v0Index = points.indexOf(v0),
            v1Index = points.indexOf(v1),
            v2Index = points.indexOf(v2),
            v3Index = points.indexOf(v3);

        //	We now have a tetrahedron as the base geometry.
        // 	Now we must subdivide the

        var tetrahedron = [
            [v2Index, v1Index, v0Index],
            [v1Index, v3Index, v0Index],
            [v2Index, v3Index, v1Index],
            [v0Index, v3Index, v2Index]
        ];

        subAA.copy(v1).sub(v0).normalize();
        subBB.copy(v2).sub(v0).normalize();
        subC.copy(v3).sub(v0).normalize();
        var sign = subC.dot(new pc.Vec3().cross(subBB, subAA));

        // Reverse the winding if negative sign
        if (sign < 0) {
            tetrahedron[0].reverse();
            tetrahedron[1].reverse();
            tetrahedron[2].reverse();
            tetrahedron[3].reverse();
        }

        //One for each face of the pyramid
        var pointsCloned = points.slice();
        pointsCloned.splice(pointsCloned.indexOf(v0), 1);
        pointsCloned.splice(pointsCloned.indexOf(v1), 1);
        pointsCloned.splice(pointsCloned.indexOf(v2), 1);
        pointsCloned.splice(pointsCloned.indexOf(v3), 1);

        var i = tetrahedron.length;
        while (i-- > 0) {
            assignPoints(tetrahedron[i], pointsCloned, points);
            if (tetrahedron[i].visiblePoints !== undefined) {
                faceStack.push(tetrahedron[i]);
            }
            faces.push(tetrahedron[i]);
        }

        process(points);

        //	Assign to our geometry object

        var ll = faces.length;

        var output = [];

        var dedupe = {};
        var id = 0;
        //Build vertex connectivity
        for (i = 0; i < ll; i++) {
            var face = faces[i];
            var v = dedupe[face[0]] || (dedupe[face[0]] = {
                id: id++,
                connections: [],
                directions: [],
                connected: {},
                point: points[face[0]]
            });
            v.connected[face[0]] = true;
            if (!v.connected[face[1]]) v.connections.push(face[1]);
            if (!v.connected[face[2]]) v.connections.push(face[2]);
            v.connected[face[1]] = true;
            v.connected[face[2]] = true;
            v = dedupe[face[1]] || (dedupe[face[1]] = {
                id: id++,
                connections: [],
                directions: [],
                connected: {},
                point: points[face[1]]
            });
            v.connected[face[1]] = true;
            if (!v.connected[face[0]]) v.connections.push(face[0]);
            if (!v.connected[face[2]]) v.connections.push(face[2]);
            v.connected[face[0]] = true;
            v.connected[face[2]] = true;
            v = dedupe[face[2]] || (dedupe[face[2]] = {
                id: id++,
                connections: [],
                directions: [],
                connected: {},
                point: points[face[2]]
            });
            v.connected[face[2]] = true;
            if (!v.connected[face[1]]) v.connections.push(face[1]);
            if (!v.connected[face[0]]) v.connections.push(face[0]);
            v.connected[face[1]] = true;
            v.connected[face[0]] = true;

        }

        for (i = 0; i < ll; i++) {
            var face = faces[i];
            for (var j = 0; j < 3; j++) {
                face[j] = dedupe[face[j]].id;
            }
        }

        for (i = 0; i < id; i++) {
            output.push(null);
        }

        //Create hill climb data
        for (var k in dedupe) {
            var vertex = dedupe[k];
            vertex.length = vertex.point.length();
            vertex.inverseLength = 1 / vertex.length;
            for (var c = 0, l = vertex.connections.length; c < l; c++) {
                var other = dedupe[vertex.connections[c]];
                vertex.directions.push(V(other.point).sub(vertex.point).normalize().clone());
                vertex.connections[c] = other.id;
            }
            output[vertex.id] = vertex;
        }
        return {
            points: output,
            faces: faces
        };

    }

}());

Actually that code needs my “V” and “Q” functions that allocate temporary pc.Vec3 and pc.Quat without garbage

Wow, great work you did!

So native physics is not the best solution, is it?
What about p2? PlayCancas has implementation, as I know.

Depends on what you need. The p2 physics integration by Will is for 2D physics only and would more performant than the ammo.js that is ‘native’ to PlayCanvas.

You can do joints in ammo.js which will allow to do something similar to compound colliders and ragdolls (as shown here: https://www.miniclip.com/games/virtual-voodoo/en/)

As for the ‘repair kit’ physics offset issue, create an entity with the rigidbody and collider and then create another entity as a child with the model. This will allow you to move the model in local space of the collider to the correct position. It’s a pain as you have to be careful on what you select to move things around later but it works.

Yeah I’m looking forward to trying out p2 when I need a 2d game, though we do mostly use Pixi for 2d stuff - I’d like to try it out. I started writing a 3d physics engine based on an idea I think is different from the standard ones (though perhaps more like bullet/ammo than PhysX) but really I’d have to have too much spare time to finish it. All the 3d stuff we do is fake physics and the library I wrote for CD and simple pushing etc.

I must update it shortly, found a couple of bugs in raycasting that need addressing but we’ve moved to building everything in ES6 and cross compiling so I have to get it back out of the modules to make it useful to folks again.

2 Likes

So I should do in programmatically, not in Editor, right?
Maybe there is an example of custom physics usage?

What about create joins of entities with rigid bodies? Is it possible?

Yes, it worked, thanks a lot.

But I still wonder, why bounding box hasn’t its own matrix? It would be much easier to work with by move\scale\rotate tools.

To work with joints, you have to dig through the Bullet documentation and what ammo.js exposes as well.

The Will’s ragdoll physics should get you started https://playcanvas.com/project/431888/overview/ragdoll

And you should look at the the fixed constraint for a compound physics object: http://bulletphysics.org/mediawiki-1.5.8/index.php/Constraints#Fixed

Oh yea, that’s exactly what I need!

Ahhh, yeah, I remember making that. It’s a cool project - but I should point out, it’s not quiiiiite finished. There are some remaining bugs controlling how joints are limited. I wish I had time to go back and fix everything. If you find any solutions to any bugs, let me know!! :smiley:

By the way, I saw you’ve been working on Ammo.js, right?

Bullet physics has gimpact algorithm for mesh-to-mesh collisitons, does Ammo implement it?

And about joints - is it possible to do it this way?

When I use your scripts, it doesn’t work properly, seems like primitives can’t be inside each other…

And I made hull (right?) for my mesh, it’s pretty simple, but still can’t work with other mesh elements.

What is the best move in my case? Use joints? Use btCompoundShape?

Okay, now it’s done

1 Like