pmndrs / cannon-es

đź’Ł A lightweight 3D physics engine written in JavaScript.
https://pmndrs.github.io/cannon-es/
MIT License
1.75k stars 130 forks source link

Please, add Debug Drawer #152

Open 8Observer8 opened 2 years ago

8Observer8 commented 2 years ago

I know cannon-es-debugger exists that uses Three.js but I am using pure WebGL API. It is very complicated to develop without collider drawing:

jill-cannon-es-collider-problem

Please, add generic collider drawing like in Ammo.js with the DebugDrawer class. I am rewriting my project in Ammo.js to see problems with colliders:

jill-colliders-ammojs

jill-colliders-of-obstacle-ammojs

Dannie226 commented 2 years ago

Here is a quick question. Why aren't you using threejs? Why are you writing the 3D rendering from scratch when a library like threejs exists and does it for you? I am certain that whatever you are doing with the WebGL you wrote, you could use it with threejs with a bit of refactoring.

8Observer8 commented 2 years ago

I love to create interactive animations from scratch and use computer graphics in pure WebGL and OpenGL. When you create it from scratch again and again you can automate it. Frameworks has many things that I do not understand and I do not need. Sometimes I do not use a render loop. I do not need PBR and skinning. Skinning and PBR is hard for mobile browser games and laptops. I use skeleton animation above for RE1 but without skinning. I extracted original animation Jill's parts and animations using RE1MVP:

https://www.youtube.com/watch?v=zN0iA0b2k7s

IMAGE ALT TEXT HERE

I wrote a script in Blender Python API to load Jill's parts and animations. All Jill's parts are separated objects that does not require skinning:

840a6c3f1fb063fd6dc02826db7db454bb52a830

It is very important for me to have Desktop and Mobile versions written in Qt6 C++ and OpenGL ES. At first I write my applications in Python and PyQt6 + OpenGL. Python is very good language for prototyping. I often write Desktop applications in PyQt6 because they do not require to build EXE, copy Qt DLL's or run http-server (and open browser). I can run PyQt6 scripts by double click and scripts do not require a big space on my laptop. I use Python instead of calculator for a long time. It is easy to rewrite from PyQt6/OpenGL to Qt6 C++ (for creating EXE or Android APK). I do not like PyInstaller or Electron because they create a very big EXE and require a lot of time to study them.

WebGL and QtOpenGL allows to make 2D and 3D games with Physics (with Box2D/Planck.js and with BulletPhysics/Ammo.js/CannonES) with multiplayer (with client and server side physics loops for client predictions) in one workspace. I just need simple graphics and understanding how it works. I do not want to study Pixi,js, Mellon.js or Phaser for 2D games. I know how to make sprite animations and how to use Free Texture Packer and Tiled Map Editor (Tiled | Flexible level editor). I already know enough from WebGL and QtOpenGL to make 2D/3D games and non game interactive applications with 3D graphics and GUI (with Handlebars/Bootstrap and QtGui).

You cannot change my opinion. Please, do not try to do it. Let's do not spend our time. Thanks.

Dannie226 commented 2 years ago

I wasn’t going to try to convince you to use three js. I was just kind of wondering why you aren’t. Three js does have a wide variety of things it can do, but there are some things it can’t. If you have hit one of those, perfectly fine by me. Would you be looking for something like Ammojs’ debug drawer where you just create a massive array for line points and colors, and it draws everything for you?

8Observer8 commented 2 years ago

Would you be looking for something like Ammojs’ debug drawer where you just create a massive array for line points and colors, and it draws everything for you?

I used the Ammo.js DebugDrawer class in my gif-animation in the first post --> demo

69c378c1c1c54f3889198db476b5dab4 jill-movement

Ammo.js is very good because I can write Qt C++ Desktop/Mobile clients with Bullet Physics for multiplayer with Ammo.js on server side. I use the free Heroku server with WebSockets. But Cannon-ES is better for lightweight apps. My laptop works better with Cannon-ES. I will just draw colliders using cube and sphere objects. I am writing a demo based on Battle City but in 3D in Python, PyQt6, OpenGL3 and Bullet Physics (from Panda3D module) It is a demo to start: https://github.com/8Observer8/falling-collada-cube-bullet-physics-opengl33-pyqt6

falling-tank-bullet-opengl3-pyqt6

Dannie226 commented 2 years ago

Ok... that didn’t answer my question. So, how do you want the debug drawer to function. Like, obviously, it can’t use three.js, so it has to have some other way to interact with your environment. How do you want that to look? Do you want it to just take a massive float32 array like the ammojs debug drawer would, or do you want to do something else. I am trying to gauge what you are asking for so that way I might be able to accommodate it.

8Observer8 commented 2 years ago

I just need one drawLine function that I will override and that I will use to draw segments, like I do with Ammo.js in my DebugDrawer class:

debug-drawer.ts

import Ammojs from "ammojs-typed";
import { Ammo, physicsWorld } from "./ammojs-init";
import { mat4, quat, vec3 } from "gl-matrix";
import ColliderEdge from "./collider-edge";

export default class DebugDrawer
{
    public projMatrix: mat4;
    public viewMatrix: mat4;

    private _edgeObject: ColliderEdge;
    private _debugDrawer: Ammojs.DebugDrawer;
    private _unitX = vec3.fromValues(1, 0, 0);
    private _tempVec = vec3.create();
    private _projViewMatrix = mat4.create();

    public constructor(edgeObject: ColliderEdge)
    {
        this._edgeObject = edgeObject;
        this._debugDrawer = new Ammo.DebugDrawer();

        this._debugDrawer.drawLine = (from: any, to: any, color: any): void =>
        {
            const heap = Ammo.HEAPF32;
            const r = heap[(parseInt(color) + 0) / 4];
            const g = heap[(parseInt(color) + 4) / 4];
            const b = heap[(parseInt(color) + 8) / 4];

            const fromX = heap[(parseInt(from) + 0) / 4];
            const fromY = heap[(parseInt(from) + 4) / 4];
            const fromZ = heap[(parseInt(from) + 8) / 4];

            const toX = heap[(parseInt(to) + 0) / 4];
            const toY = heap[(parseInt(to) + 4) / 4];
            const toZ = heap[(parseInt(to) + 8) / 4];

            let centerX: number;
            let centerY: number;
            let centerZ: number;

            if (fromX > toX)
            {
                centerX = toX + Math.abs(fromX - toX) / 2;
            }
            else
            {
                centerX = fromX + Math.abs(toX - fromX) / 2;
            }

            if (fromY > toY)
            {
                centerY = toY + Math.abs(fromY - toY) / 2;
            }
            else
            {
                centerY = fromY + Math.abs(toY - fromY) / 2;
            }

            if (fromZ > toZ)
            {
                centerZ = toZ + Math.abs(fromZ - toZ) / 2;
            }
            else
            {
                centerZ = fromZ + Math.abs(toZ - fromZ) / 2;
            }

            this._tempVec[0] = toX - fromX;
            this._tempVec[1] = toY - fromY;
            this._tempVec[2] = toZ - fromZ;
            const length = vec3.length(this._tempVec);
            vec3.normalize(this._tempVec, this._tempVec);

            quat.rotationTo(this._edgeObject.rotation, this._unitX, this._tempVec);
            this._edgeObject.scale[0] = length;
            this._edgeObject.scale[1] = 0.1;
            this._edgeObject.scale[2] = 0.1;
            this._edgeObject.position[0] = centerX;
            this._edgeObject.position[1] = centerY;
            this._edgeObject.position[2] = centerZ;

            mat4.mul(this._projViewMatrix, this.projMatrix, this.viewMatrix);
            this._edgeObject.draw(this._projViewMatrix);
        };

        this._debugDrawer.setDebugMode = (debugMode: number) => { }

        let debugMode = 1; // 0 - 0ff, 1 - on
        this._debugDrawer.getDebugMode = (): number =>
        {
            return debugMode;
        }

        this._debugDrawer.drawContactPoint = (pointOnB: Ammojs.btVector3, normalOnB: Ammojs.btVector3,
            distance: number, lifeTime: number, color: Ammojs.btVector3): void => { }

        this._debugDrawer.reportErrorWarning = (warningString: string): void => { };
        this._debugDrawer.draw3dText = (location: Ammojs.btVector3, textString: string): void => { };

        this._debugDrawer.setDebugMode(0);
        physicsWorld.setDebugDrawer(this._debugDrawer);
    }
}
8Observer8 commented 2 years ago
    public constructor(edgeObject: ColliderEdge)
    {
        this._edgeObject = edgeObject;

edgeObject is a cube with the size 1x1x1 which I load from .dae

Dannie226 commented 2 years ago

So... all you want is some class, with a draw line function that you will override. I should be able to do that.

Dannie226 commented 2 years ago

Now, obviously this isn't complete, I will add cylinder and convex poly later, but I just wish to know, is this near what you are looking for. In the gif you posted, there were only boxes and spheres, so this should work for that, but, idk. I only did minimal testing to ensure that stuff rendered and updated. Anyway, if this is what you are looking for, I will continue to work on it.

import * as CANNON from "cannon-es";
import * as THREE from "three";

type ShapeInfo = {
    points:[number, number, number][],
    indices:[number, number][]
};

export class CannonDebugger {
    private physicsWorld:CANNON.World;
    public drawLine:(from:CANNON.Vec3, to:CANNON.Vec3, color:THREE.Color) => void;
    private sphereInfo:ShapeInfo;
    constructor(world:CANNON.World){
        this.physicsWorld = world;
        this.drawLine = () => {};
        this.sphereInfo = {
            points:[
                [0, 1, 0]
            ],
            indices:[
               [ 0, 1],
               [ 0, 2],
               [ 0, 3],
               [ 0, 4],
               [ 0, 5],
               [ 0, 6],
               [ 0, 7],
               [ 0, 8],
               [ 0, 9],
               [ 0,10],
               [ 0,11],
               [ 0,12],
               [ 1, 2],
               [ 2, 3],
               [ 3, 4],
               [ 4, 5],
               [ 5, 6],
               [ 6, 7],
               [ 7, 8],
               [ 8, 9],
               [ 9,10],
               [10,11],
               [11,12],
               [12, 1],
               [ 1,13],
               [ 2,14],
               [ 3,15],
               [ 4,16],
               [ 5,17],
               [ 6,18],
               [ 7,19],
               [ 8,20],
               [ 9,21],
               [10,22],
               [11,23],
               [12,24],
               [13,14],
               [14,15],
               [15,16],
               [16,17],
               [17,18],
               [18,19],
               [19,20],
               [20,21],
               [21,22],
               [22,23],
               [23,24],
               [24,13],
               [13,25],
               [14,26],
               [15,27],
               [16,28],
               [17,29],
               [18,30],
               [19,31],
               [20,32],
               [21,33],
               [22,34],
               [23,35],
               [24,36],
               [25,26],
               [26,27],
               [27,28],
               [28,29],
               [29,30],
               [30,31],
               [31,32],
               [32,33],
               [33,34],
               [34,35],
               [35,36],
               [36,25],
               [25,37],
               [26,38],
               [27,39],
               [28,40],
               [29,41],
               [30,42],
               [31,43],
               [32,44],
               [33,45],
               [34,46],
               [35,47],
               [36,48],
               [37,38],
               [38,39],
               [39,40],
               [40,41],
               [41,42],
               [42,43],
               [43,44],
               [44,45],
               [45,46],
               [46,47],
               [47,48],
               [48,37],
               [37,49],
               [38,50],
               [39,51],
               [40,52],
               [41,53],
               [42,54],
               [43,55],
               [44,56],
               [45,57],
               [46,58],
               [47,59],
               [48,60],
               [49,50],
               [50,51],
               [51,52],
               [52,53],
               [53,54],
               [54,55],
               [55,56],
               [56,57],
               [57,58],
               [58,59],
               [59,60],
               [60,49],
               [49,61],
               [50,61],
               [51,61],
               [52,61],
               [53,61],
               [54,61],
               [55,61],
               [56,61],
               [57,61],
               [58,61],
               [59,61],
               [60,61]
           ]
        };
        for(let i = 1; i < 6; i++){
            const phi = i * Math.PI / 6, sp = Math.sin(phi);
            for(let j = 0; j < 12; j++){
                const theta = j * Math.PI / 6;
                this.sphereInfo.points.push([Math.sin(theta) * sp, Math.cos(phi), Math.cos(theta) * sp]);
            }
        }
        this.sphereInfo.points.push([0, -1, 0]);
    }

    private boxInfo:ShapeInfo = {
        points:[
            [ 1, 1, 1],
            [ 1, 1,-1],
            [ 1,-1, 1],
            [ 1,-1,-1],
            [-1, 1, 1],
            [-1, 1,-1],
            [-1,-1, 1],
            [-1,-1,-1]
        ],
        indices:[
            [0, 1],
            [1, 3],
            [3, 2],
            [2, 0],
            [4, 5],
            [5, 7],
            [7, 6],
            [6, 4],
            [0, 4],
            [1, 5],
            [2, 6],
            [3, 7]
        ]
    };

    private from = new CANNON.Vec3;
    private to = new CANNON.Vec3;
    private color = new THREE.Color(0x0000FF);

    private drawBox(body:CANNON.Body){
        const shape = body.shapes[0] as CANNON.Box;

        const info = this.boxInfo;
        for(const index of info.indices){
            let a = index[0], b = index[1];
            this.from.set(...info.points[a]);
            this.to.set(...info.points[b]);
            this.from.vmul(shape.halfExtents, this.from);
            this.to.vmul(shape.halfExtents, this.to);
            body.quaternion.vmult(this.from, this.from);
            body.quaternion.vmult(this.to, this.to);
            this.from.vadd(body.position, this.from);
            this.to.vadd(body.position, this.to);

            this.drawLine(this.from, this.to, this.color);
        }
    }

    private drawSphere(body:CANNON.Body){
        const shape = body.shapes[0] as CANNON.Sphere;

        const info = this.sphereInfo;
        for(const index of info.indices){
            let a = index[0], b = index[1];
            this.from.set(...info.points[a]);
            this.to.set(...info.points[b]);
            this.from.scale(shape.radius, this.from);
            this.to.scale(shape.radius, this.to);
            body.quaternion.vmult(this.from, this.from);
            body.quaternion.vmult(this.to, this.to);
            this.from.vadd(body.position, this.from);
            this.to.vadd(body.position, this.to);

            this.drawLine(this.from, this.to, this.color);
        }
    }

    public update(){
        for(const body of this.physicsWorld.bodies){
            switch(body.shapes[0].type){
                case CANNON.SHAPE_TYPES.BOX:
                    this.drawBox(body);
                    break;

                case CANNON.SHAPE_TYPES.SPHERE:
                    this.drawSphere(body);
                    break;
            }
        }
    }
}
8Observer8 commented 2 years ago

I just need to know the beginning and end of each segment for collider like it made in Ammo.js. I can draw the segments by myself using a cube with lightless fragment shader like here:

image

Every collider segment on the screenshot above is a green cube that I transformed using only from and to points for each segments. What if engine developers (Babylon.js or PlayCanvas) will release drawing of collider segments in they engines? Should the developer of Babylon.js install Three.js? It will be sad if Cannon-ES is for Three.js only. Ammo.js (and Bullet Physics) developers made an universal method: we give you the from and to points of segment and you can draw a segment as you like.

I already made what you want: drawing a whole collider with box or sphere. It is my temporary solution (I have a problem with size of ground collider):

tank-colliders-with-ground-problem

Dannie226 commented 2 years ago

The code above doesn’t draw the entire collider. It uses the draw line function that you override to draw a bunch of lines. And it is also for cannon js... not ammo js. I thought that that was what you wanted

8Observer8 commented 2 years ago

Could you remove Three.js dependency?

Dannie226 commented 2 years ago

I don’t think I even use three anywhere. Just open it up in an editor, and see whether it is used. It shouldn’t be.

Dannie226 commented 2 years ago

Oh. Color. That can be easily changed to a number triple or cannon.vec3, or even removed. You can decide what is best for your scenario.

8Observer8 commented 2 years ago

Thank you very much! It looks like it will work. I will write some simple examples and I will post links on playground here for beginners. If I have problems I will write about them here.

Dannie226 commented 2 years ago

I would still need to add like, convex polyhedron rendering, cylinder rendering, and trimesh rendering, but, I will get that done and hopefully get back to you.

8Observer8 commented 2 years ago

Drawing of a box and sphere colliders work! Thanks very much! I will make interesting examples and I will test your code.

https://codesandbox.io/s/cannon-es-box-sphere-debugger-webgl-ts-9co3qr (toggle the preview to see the result: Ctrl+Shift+D)

Source: https://github.com/8Observer8/cannon-es-box-sphere-debugger-webgl-ts

image image

8Observer8 commented 2 years ago

It will be good if you add axes to understand orientation like it is in Bullet Physics and Ammo.js:

CollidersIsDrawn

8Observer8 commented 1 year ago

The custom debug drawer is needed because I could draw cuboids instead of lines like in cannon-es-debugger. It's hard to see these lines because they are too thin:

image

8Observer8 commented 1 year ago

You can get ideas from OimoPhysics:

export default class DebugDrawer extends OIMO.DebugDraw {
    line(from, to, color) {
    }
}

I use this class to draw cuboids instead of lines with pure WebGL:

debug-drawer.js

import { mat4, quat, vec3 } from "gl-matrix";

export default class DebugDrawer extends OIMO.DebugDraw {
    constructor(edge) {
        super();
        this.edge = edge;
        this.projViewMatrix = null;

        this.centerX = 0;
        this.centerY = 0;
        this.centerZ = 0;

        this.length = 0;
        this.vec = vec3.create();
        this.x = 0;
        this.y = 0;
        this.z = 0;
        this.unitX = vec3.fromValues(1, 0, 0);
    }

    point(v, color) {
        console.log("point");
    }

    triangle(v1, v2, v3, n1, n2, n3, color) {
        console.log("triangle");
    }

    line(from, to, color) {
        this.edge.color[0] = color.x;
        this.edge.color[1] = color.y;
        this.edge.color[2] = color.z;

        if (from.x > to.x) {
            this.centerX = to.x + Math.abs(from.x - to.x) / 2;
        } else {
            this.centerX = from.x + Math.abs(to.x - from.x) / 2;
        }

        if (from.y > to.y) {
            this.centerY = to.y + Math.abs(from.y - to.y) / 2;
        } else {
            this.centerY = from.y + Math.abs(to.y - from.y) / 2;
        }

        if (from.z > to.z) {
            this.centerZ = to.z + Math.abs(from.z - to.z) / 2;
        } else {
            this.centerZ = from.z + Math.abs(to.z - from.z) / 2;
        }

        this.vec[0] = to.x - from.x;
        this.vec[1] = to.y - from.y;
        this.vec[2] = to.z - from.z;
        this.length = vec3.length(this.vec);

        vec3.normalize(this.vec, this.vec);
        quat.rotationTo(this.edge.rotation, this.unitX, this.vec);

        this.edge.scale = [this.length, 0.2, 0.2];
        this.edge.position = [this.centerX, this.centerY, this.centerZ];
        this.edge.draw(this.projViewMatrix);
    }
}