Lusito / box2d.ts

Full blown Box2D Ecosystem for the web, written in TypeScript
https://lusito.github.io/box2d.ts
60 stars 6 forks source link

Drawing colliders for box2d-core using Pixi.js #40

Closed 8Observer8 closed 2 months ago

8Observer8 commented 1 year ago

This issue is similar to this one: Rotation problem with box2d-core and WebGL

box2d-core has a method to draw vertices without translation and rotation: DrawSolidPolygon(vertices, vertexCount, color) and separate method to set translations and rotations:

    PushTransform(xf) {
        this.translationX = xf.p.x;
        this.translationY = xf.p.y;
        this.angle = xf.q.s * 180 / Math.PI;
    }

I created the lines object in the DebugDrawer class:

    constructor(stage, pixelsPerMeter) {
        this.lines = new PIXI.Graphics();
        stage.addChild(this.lines);

I draw lines but transforms applied on whole lines object:

    DrawSolidPolygon(vertices, vertexCount, color) {
        const c = new PIXI.Color([color.r, color.g, color.b]).toHex();
        this.lines.lineStyle(1, c, 1, 0.5, true);

        this.lines.moveTo((vertices[0].x + this.translationX) * this.pixelsPerMeter,
            (vertices[0].y + this.translationY) * this.pixelsPerMeter);
        this.lines.lineTo((vertices[1].x + this.translationX) * this.pixelsPerMeter,
            (vertices[1].y + this.translationY) * this.pixelsPerMeter);
        this.lines.lineTo((vertices[2].x + this.translationX) * this.pixelsPerMeter,
            (vertices[2].y + this.translationY) * this.pixelsPerMeter);
        this.lines.lineTo((vertices[3].x + this.translationX) * this.pixelsPerMeter,
            (vertices[3].y + this.translationY) * this.pixelsPerMeter);
        this.lines.lineTo((vertices[0].x + this.translationX) * this.pixelsPerMeter,
            (vertices[0].y + this.translationY) * this.pixelsPerMeter);

        this.lines.angle = this.angle;
    }

falling-box-debug-drawer-box2dcore-pixijs-js

8Observer8 commented 1 year ago

Box2D-WASM has a better solution. It just pass vertices directly and I can draw the lines:

    drawLines(vertices, color) {
        const c = new PIXI.Color([color.r, color.g, color.b]).toHex();
        this.lines.lineStyle(1, c, 1, 0.5, true);
        this.lines.moveTo(vertices[0].x * this.pixelsPerMeter, vertices[0].y * this.pixelsPerMeter);
        this.lines.lineTo(vertices[1].x * this.pixelsPerMeter, vertices[1].y * this.pixelsPerMeter);
        this.lines.lineTo(vertices[2].x * this.pixelsPerMeter, vertices[2].y * this.pixelsPerMeter);
        this.lines.lineTo(vertices[3].x * this.pixelsPerMeter, vertices[3].y * this.pixelsPerMeter);
        this.lines.lineTo(vertices[0].x * this.pixelsPerMeter, vertices[0].y * this.pixelsPerMeter);
    }

falling-box-debug-drawer-box2dwasm-pixijs-js

8Observer8 commented 1 year ago

This is the full code of the example above:

The example is small: 62 lines in index.js and 50 lines in debug-drawer.js:

index.html

<!DOCTYPE html>

<html>

<head>
    <meta charset="utf-8">
    <title>Example</title>
    <link rel="stylesheet" type="text/css" href="./css/style.css">
</head>

<body>
    <!-- Since importmap is not yet supported by all browsers, it is
        necessary to add the polyfill es-module-shims.min.js -->
    <script async src="https://unpkg.com/es-module-shims@0.1.7/dist/es-module-shims.min.js"></script>

    <script type="importmap">
        {
            "imports": {
                "@box2d/core": "https://cdn.skypack.dev/@box2d/core@0.10.0",
                "pixi.js": "https://cdn.jsdelivr.net/npm/pixi.js@7.2.4/+esm"
            }
        }
    </script>

    <script type="module" src="./js/index.js"></script>
</body>

</html>

index.js

import { b2BodyType, b2PolygonShape, b2Vec2, b2World, DrawShapes } from "@box2d/core";
import * as PIXI from "pixi.js";
import DebugDrawer from "./debug-drawer.js";

async function init() {
    const renderer = PIXI.autoDetectRenderer(300, 300, {
        backgroundColor: 0x000000,
        antialias: true,
        resolution: 1
    });
    renderer.view.width = 300;
    renderer.view.height = 300;
    document.body.appendChild(renderer.view);

    // Create the main stage for your display objects
    const stage = new PIXI.Container();

    const world = b2World.Create({ x: 0, y: 9.8 });
    const pixelsPerMeter = 30;
    const debugDrawer = new DebugDrawer(stage, pixelsPerMeter);

    // Box
    const boxShape = new b2PolygonShape();
    boxShape.SetAsBox(30 / pixelsPerMeter, 30 / pixelsPerMeter);
    const boxBody = world.CreateBody({
        type: b2BodyType.b2_dynamicBody,
        position: { x: 100 / pixelsPerMeter, y: 30 / pixelsPerMeter },
        angle: 30 * Math.PI / 180
    });
    boxBody.CreateFixture({ shape: boxShape, density: 1 });

    // Ground
    const groundShape = new b2PolygonShape();
    groundShape.SetAsBox(130 / pixelsPerMeter, 20 / pixelsPerMeter);
    const groundBody = world.CreateBody({
        type: b2BodyType.b2_staticBody,
        position: { x: 150 / pixelsPerMeter, y: 270 / pixelsPerMeter }
    });
    groundBody.CreateFixture({ shape: groundShape });

    let currentTime, lastTime, dt;

    function render() {
        requestAnimationFrame(render);

        currentTime = Date.now();
        dt = (currentTime - lastTime) / 1000;
        lastTime = currentTime;

        world.Step(dt, { velocityIterations: 3, positionIterations: 2 });
        DrawShapes(debugDrawer, world);

        // Render the stage
        renderer.render(stage);
        debugDrawer.clear();
    }

    lastTime = Date.now();
    render();
}

init();

debug-drawer.js

import * as PIXI from "pixi.js";

export default class DebugDrawer {

    constructor(stage, pixelsPerMeter) {
        this.lines = new PIXI.Graphics();
        stage.addChild(this.lines);
        this.pixelsPerMeter = pixelsPerMeter;

        this.translationX = 0;
        this.translationY = 0;
        this.angle = 0;
    }

    DrawSolidPolygon(vertices, vertexCount, color) {
        const c = new PIXI.Color([color.r, color.g, color.b]).toHex();
        this.lines.lineStyle(1, c, 1, 0.5, true);

        this.lines.moveTo((vertices[0].x + this.translationX) * this.pixelsPerMeter,
            (vertices[0].y + this.translationY) * this.pixelsPerMeter);
        this.lines.lineTo((vertices[1].x + this.translationX) * this.pixelsPerMeter,
            (vertices[1].y + this.translationY) * this.pixelsPerMeter);
        this.lines.lineTo((vertices[2].x + this.translationX) * this.pixelsPerMeter,
            (vertices[2].y + this.translationY) * this.pixelsPerMeter);
        this.lines.lineTo((vertices[3].x + this.translationX) * this.pixelsPerMeter,
            (vertices[3].y + this.translationY) * this.pixelsPerMeter);
        this.lines.lineTo((vertices[0].x + this.translationX) * this.pixelsPerMeter,
            (vertices[0].y + this.translationY) * this.pixelsPerMeter);

        this.lines.angle = this.angle;
    }

    clear() {
        this.lines.clear();
    }

    PushTransform(xf) {
        this.translationX = xf.p.x;
        this.translationY = xf.p.y;
        this.angle = xf.q.s * 180 / Math.PI;
    }

    PopTransform(xf) {}
    DrawPolygon(vertices, vertexCount, color) {}
    DrawCircle(center, radius, color) {}
    DrawSolidCircle(center, radius, axis, color) {}
    DrawSegment(p1, p2, color) {}
    DrawTransform(xf) {}
    DrawPoint(p, size, color) {}
}
8Observer8 commented 1 year ago

I got the idea with drawing colliders with Pixi.js from this rapier2d topic: https://github.com/dimforge/rapier.js/pull/119 (Add basic debug-render support). It shows how to draw colliders for rapier2d/3d with Pixi.js and Three.js:

if (!this.lines) {
    this.lines = new PIXI.Graphics();
    this.viewport.addChild(this.lines);
}
let buffers = world.debugRender();
let vtx = buffers.vertices;
let cls = buffers.colors;

this.lines.clear();

for (let i = 0; i < vtx.length / 4; i += 1) {
    let color = PIXI.utils.rgb2hex([cls[i * 8], cls[i * 8 + 1], cls[i * 8 + 2]]);
    this.lines.lineStyle(1.0, color, cls[i * 8 + 3], 0.5, true);
    this.lines.moveTo(vtx[i * 4], -vtx[i * 4 + 1]);
    this.lines.lineTo(vtx[i * 4 + 2], -vtx[i * 4 + 3]);
}
8Observer8 commented 1 year ago

I created another example using the Melon.js game engine: https://plnkr.co/edit/QVRPb8spIyVwLvtF?preview I draw a ground like this;

export default class DebugDrawer {
    constructor(renderer, pixelsPerMeter) {
        this.renderer = renderer;
        this.pixelsPerMeter = pixelsPerMeter;

        this.translationX = 0;
        this.translationY = 0;
        this.angle = 0;
    }

    DrawSolidPolygon(vertices, vertexCount, color) {
        this.renderer.setLineWidth(3);
        this.renderer.beginPath();
        const c = new me.Color().setFloat(color.r, color.g, color.b, 1);
        this.renderer.setColor(c);
        this.renderer.moveTo((vertices[0].x + this.translationX) * this.pixelsPerMeter,
            (vertices[0].y + this.translationY) * this.pixelsPerMeter);
        this.renderer.lineTo((vertices[1].x + this.translationX) * this.pixelsPerMeter,
            (vertices[1].y + this.translationY) * this.pixelsPerMeter);
        this.renderer.lineTo((vertices[2].x + this.translationX) * this.pixelsPerMeter,
            (vertices[2].y + this.translationY) * this.pixelsPerMeter);
        this.renderer.lineTo((vertices[3].x + this.translationX) * this.pixelsPerMeter,
            (vertices[3].y + this.translationY) * this.pixelsPerMeter);
        this.renderer.lineTo((vertices[0].x + this.translationX) * this.pixelsPerMeter,
            (vertices[0].y + this.translationY) * this.pixelsPerMeter);
        this.renderer.stroke();
    }

    PushTransform(xf) {
        this.translationX = xf.p.x;
        this.translationY = xf.p.y;
        this.angle = xf.q.s * 180 / Math.PI;
    }

image

I want to draw a rotated platform:

    // Platform
    const platformBodyDef = new b2BodyDef();
    platformBodyDef.set_position(new b2Vec2(220 / this.pixelsPerMeter,
        200 / this.pixelsPerMeter));
    platformBodyDef.angle = -20 * Math.PI / 180;
    const platformBody = this.world.CreateBody(platformBodyDef);
    const platformShape = new b2PolygonShape();
    platformShape.SetAsBox(50 / this.pixelsPerMeter, 5 / this.pixelsPerMeter);
    platformBody.CreateFixture(platformShape, 0);

For a while it looks like this because I don't apply a rotation:

image

I see these two methods:

    PushTransform(xf) {
        this.translationX = xf.p.x;
        this.translationY = xf.p.y;
        this.angle = xf.q.s * 180 / Math.PI;
    }

    PopTransform(xf) {}

What is the difference between them? It looks like they have translations and angles. But how to apply this angle to the lines?

8Observer8 commented 1 year ago

I solved a problem above with Melon.js using this example from this message.

    PushTransform(xf) {
        this.renderer.save();
        this.renderer.translate(xf.p.x * this.pixelsPerMeter, xf.p.y * this.pixelsPerMeter);
        this.renderer.rotate(xf.q.GetAngle());
    }

    PopTransform(xf) {
        this.renderer.restore();
    }
8Observer8 commented 4 months ago

Phaser 3 and Melon.js have the save() and restore() methods even in WEBGL mode:

8Observer8 commented 2 months ago

Pixi.js 8 has save() and restore methods in the GraphicsContext class. How to solve the rotation problem: https://plnkr.co/edit/1RizUv5yknLrb9w4?preview

debug-drawer-pixijs8-js

index.html

<!DOCTYPE html>

<html>

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Set up Pixi.js 8 as ES6-module in JavaScript</title>
</head>

<body>
    <script type="importmap">
        {
            "imports": {
                "@box2d/core": "https://8observer8.github.io/libs/box2d-core@0.10.0/box2d-core.min.js",
                "pixi.js": "https://8observer8.github.io/libs/pixi.js@8.1.6/pixi.min.mjs"
            }
        }
    </script>

    <script type="module" src="./js/index.js"></script>
</body>

</html>

index.js

import { b2BodyType, b2PolygonShape, b2World } from "@box2d/core";
import { DrawShapes } from "@box2d/core";
import { Application, Graphics } from "pixi.js";
import DebugDrawer from "./debug-drawer.js";

// Create the main application and the canvas
const app = new Application();
await app.init({ width: 400, height: 400 });
document.body.appendChild(app.canvas);

const world = b2World.Create({ x: 0, y: 10 });
const pixelsPerMeter = 30;

// Ground
const groundShape = new b2PolygonShape();
groundShape.SetAsBox(130 / pixelsPerMeter, 20 / pixelsPerMeter);
const groundBody = world.CreateBody({
    type: b2BodyType.b2_staticBody,
    position: { x: 150 / pixelsPerMeter, y: 270 / pixelsPerMeter }
});
groundBody.CreateFixture({ shape: groundShape });

// Box
const boxShape = new b2PolygonShape();
boxShape.SetAsBox(30 / pixelsPerMeter, 30 / pixelsPerMeter);
const boxBody = world.CreateBody({
    type: b2BodyType.b2_dynamicBody,
    angle: 10 * Math.PI / 180,
    position: { x: 200 / pixelsPerMeter, y: 30 / pixelsPerMeter }
});
boxBody.CreateFixture({ shape: boxShape, density: 1 });

const debugGraphics = new Graphics();
const debugDrawer = new DebugDrawer(debugGraphics, pixelsPerMeter);
app.stage.addChild(debugGraphics);

draw();

function draw() {
    world.Step(0.016, { velocityIterations: 3, positionIterations: 2 });

    debugGraphics.clear();
    DrawShapes(debugDrawer, world);

    requestAnimationFrame(draw);
}

debug-drawer.js

import { Color } from "pixi.js";

export default class DebugDrawer {

    constructor(graphics, pixelsPerMeter, lineWidth = 5) {
        this.graphics = graphics;
        this.ppm = pixelsPerMeter;
        this.lineWidth = lineWidth;
    }

    DrawSolidPolygon(vertices, vertexCount, color) {
        const c = new Color([color.r, color.g, color.b]).toHex();
        this.graphics.moveTo(vertices[0].x * this.ppm, vertices[0].y * this.ppm);
        this.graphics.lineTo(vertices[1].x * this.ppm, vertices[1].y * this.ppm);
        this.graphics.lineTo(vertices[2].x * this.ppm, vertices[3].y * this.ppm);
        this.graphics.lineTo(vertices[3].x * this.ppm, vertices[3].y * this.ppm);
        this.graphics.lineTo(vertices[0].x * this.ppm, vertices[0].y * this.ppm);
        this.graphics.stroke({ color: c, width: this.lineWidth });
    }

    DrawSolidCircle(center, radius, axis, color) {

    }

    PushTransform(xf) {
        this.graphics.context.save();
        this.graphics.context.translate(xf.p.x * this.ppm, xf.p.y * this.ppm);
        this.graphics.context.rotate(xf.q.GetAngle());
    }

    PopTransform(xf) {
        this.graphics.context.restore();
    }

    DrawCircle(center, radius, color) {}
    DrawPolygon(vertices, vertexCount, color) {}
    DrawSegment(p1, p2, color) {}
    DrawTransform(xf) {}
    DrawPoint(p, size, color) {}
}
8Observer8 commented 2 months ago

I have created the issue for Pixi.js 8: https://github.com/pixijs/pixijs/issues/10632

8Observer8 commented 2 months ago

It is very good that @box2d/core, @box2d/particles, and Pixi.js v8 can be loaded from Skypack:

    <script type="importmap">
        {
            "imports": {
                "@box2d/core": "https://cdn.skypack.dev/@box2d/core@0.10.0",
                "@box2d/particles": "https://cdn.skypack.dev/@box2d/particles@0.10.0",
                "pixi.js": "https://cdn.skypack.dev/pixi.js@8.1.6"
            }
        }
    </script>

    <script type="module" src="./js/index.js"></script>

index.js

import { b2CircleShape, b2World } from "@box2d/core";
import { b2ParticleGroupDef, b2ParticleSystemDef }  from "@box2d/particles";
import { Application, Graphics } from "pixi.js";
8Observer8 commented 2 months ago

I have swapped the order of the rotation and translation and it works now: https://plnkr.co/edit/1RizUv5yknLrb9w4?preview

    PushTransform(xf) {
        this.graphics.context.save();
        this.graphics.context.rotate(xf.q.GetAngle());
        this.graphics.context.translate(xf.p.x * this.ppm, xf.p.y * this.ppm);
    }

But Phaser 3 works in the other order: https://plnkr.co/edit/QZ6ce2LwdIExLdBf?preview

    PushTransform(xf) {
        this.graphics.save();
        this.graphics.translateCanvas(xf.p.x * this.pixelsPerMeter,
            xf.p.y * this.pixelsPerMeter);
        this.graphics.rotateCanvas(xf.q.GetAngle());
    }

321203764-53315605-54d7-4169-b820-b708271d8bd8