liabru / matter-js

a 2D rigid body physics engine for the web ▲● ■
MIT License
16.62k stars 1.96k forks source link

launching ball upwards with setVelocity does not have expected height #767

Open mobileben opened 5 years ago

mobileben commented 5 years ago

I was following around with trying to predict trajectories. Right now I'm isolating just the vertical component using the basic formula

vy = - sqrt(2 * g * h)

where vy is the vertical component of the initial velocity, g is gravity, and h is the height to travel. I currently have frictionAir on the ball set to 0.

I have a basic setup where the ball is on the ground. A mouse button press will setVelocity on the ball, using the calculated vy.

What results is the ball going way higher than expected. Either my understanding of the correct way of using gravity is incorrect or I'm doing something else wrong.

The isolated code (full code will also be included at the end) for just setting the velocity is:

    Events.on(mouseConstraint, "mousedown", function(event) {
        // Calculate vy based on vy = - sqrt(2 * g * h)
        let g = world.gravity.y // Note gravity is positive since y increases downwards
        let h = ball.position.y // In theory, this should be the distance of the ball to the top of the screen
        let vy = - Math.sqrt(2 * g * h)

        console.log("vy = " + vy)
        console.log("Projected height = " + h)
        launchY = ball.position.y
        Body.setVelocity(ball, {x: 0, y: vy})
        upwards = true
    })

Clearly it seems trying to use world.gravity.y is the naive approach. It appears I'm missing some conversion or perhaps my math is off.

I've tried multiplying in the world.gravity.scale but in that case, the ball doesn't even launch.

    // module aliases
    let Engine = Matter.Engine,
        Render = Matter.Render,
        World = Matter.World,
        Body = Matter.Body,
        Composite = Matter.Composite,
        Constraint = Matter.Constraint,
        MouseConstraint = Matter.MouseConstraint,
        Mouse = Matter.Mouse,
        Runner = Matter.Runner,
        Events = Matter.Events,
        Vector = Matter.Vector,
        Bodies = Matter.Bodies;

    // create an engine
    let engine = Engine.create();
    let world = engine.world;

    // create a renderer
    let render = Render.create({
        element: document.body,
        engine: engine,
        options: {
            height: 1000
        }
    });

    // Build ground
    const ballRadius = 60
    let ground = Bodies.rectangle(render.bounds.max.x / 2.0, render.bounds.max.y, render.bounds.max.x, 60, { restitution: 1, isStatic: true } );
    let ball = Bodies.circle(render.bounds.max.x / 2.0, render.bounds.max.y - ballRadius * 2, ballRadius, { restitution: 1, friction: 0, frictionAir: 0, frictionStatic: 0, restitution: 0.5 })

    // Add bodies
    World.add(world, ground)
    World.add(world, ball)

    // Add mouse
    mouse = Mouse.create(render.canvas),
        mouseConstraint = MouseConstraint.create(engine, {
            mouse: mouse,
            constraint: {
                stiffness: 0.98,
                render: {
                    visible: false
                }
            }
        });

    // Ignore mouseConstraint for now
    mouseConstraint.collisionFilter.mask = 0
    World.add(world, mouseConstraint);

    let upwards = false
    let launchY = 0

    // On mousedown, add velocity to the ball
    Events.on(mouseConstraint, "mousedown", function(event) {
        // Calculate vy based on vy = - sqrt(2 * g * h)
        let g = world.gravity.y // Note gravity is positive since y increases downwards
        let h = ball.position.y // In theory, this should be the distance of the ball to the top of the screen
        let vy = - Math.sqrt(2 * g * h)

        console.log("vy = " + vy)
        console.log("Projected height = " + h)
        launchY = ball.position.y
        Body.setVelocity(ball, {x: 0, y: vy})
        upwards = true
    })    

    let useEngineRun = false

    if (useEngineRun) {
        // START - Code based on default Engine.run
        Engine.run(engine);

        // run the renderer
        Render.run(render);
        // END - Code based on default Engine.run
    } else {
        // START - Code based on using runner
        let runner =  Runner.create()
        Render.run(render);

        requestAnimationFrame(loop)
        function loop() {
            //console.log("Ball y " + ball.position.y)
            if (upwards && ball.velocity.y >= 0) {
                console.log("Distance traveled " + (launchY - ball.position.y))
                upwards = false
            }
            Engine.update(engine)
            requestAnimationFrame(loop)
        }
        // END - Code based on using runner
    }
mobileben commented 5 years ago

I've been doing some more investigation around this. Much like in this issue #603, I found I need roughly the same corrective values as that author. In that issue, he cites about how the value is derived to cancel out the delta time squared.

I was looking for a way to come up with deriving that number based on known information since it seems we should have a way to get a good estimate of what the engine would do (there actually doesn't see to be a great way to do it).

This code generates roughly the correct vy. The resulting value does need to be scaled by 1.01 (roughly) to reach the desired height. This value will be referenced again below.

  let g = world.gravity.y // Note gravity is positive since y increases downwards
  let correction = 1000 * (1/60) * (1/60)
  let h = ball.getPosition().y * correction // In theory, this should be the distance of the ball to the top of the screen
  let vy = - Math.sqrt(2 * g * h)

I should also note, that in my test, I added box2d support. I have a setup where I have side by side balls, where one ball is based on matter and the other is based on box2d.

The balls start from the top of the screen and drop to verify gravity is working the same. Using a box2d scale of 100, I can see the fall of the ball is pretty much identical. Until the hit of the ground ... here things deviate slightly. I have found a restitution of 1 with matter is approximately equal to a restitution of 0.49498995 in box2d. Both balls are close enough on the bounce.

Moreover, I have found when using iforce2d's formula (link below) to calculate the vertical velocity opposed to the formula I was using, the box2d results are pretty much as expected (I print out the actual height reached for each ball and compare it to the desired height). Here's some sample output to the console as an example:

Box2d Projected height = 6.092009544372559, actual distance 609.2009544372559
Matter Distance traveled 600.869167694045
Box2D Distance traveled 609.1531753540039

This is his formula (this is based on Box2D, hence b2Gravity).

function calculateVerticalVelocityForHeight(height) {
    if (height <= 0) {
        return 0.0
    }
    var t = 1 / 60.0
    var stepG = t * t * b2Gravity.y
    var a = 0.5 / stepG
    var b = 0.5
    var c = height
    var qSol1 = (-b - Math.sqrt(Math.abs(b * b - 4 * a * c))) / (2 * a)
    var qSol2 = (-b + Math.sqrt(Math.abs(b * b - 4 * a * c))) / (2 * a)
    var v = qSol1
    if (v < 0) {
        v = qSol2
    }
    return v * 60.0
}

I have not managed to make figure out how to create a version of calculateVerticalVelocityForHeight which will generate the correct velocity for matter.js.

Here is an example of a run. Box2d is on the right.

matter-v-box2d

Both engines use Verlet integration, so I would expect roughly the same results.

But I think one thing based on reviewing #179, #240, #584, and #603 is that there seems to be a need for a better explanation of converting coordinates as well as how to derive more accurate values like in this case.

Right now, matter.js is operating on a 1:1 for pixels. box2D is a scale of 100. box2D, at least from this case, seems more predictable/derivable and was actually quite easy to get expected results.

I'd love to better understand how correction in my code above is derived (the "why"), where the correction really needs to be applied (and "why"), as well as how to properly calculate the proper velocity. It just seems like there are some "magic numbers" that are needed to make it work, which I think is some of the basis of those issues I've cited.

As another note, when I used the

vy = sqrt(2 * g * h)

Method for deriving the initial velocity is applied to both matter and box2d, the results are nearly identical. I of course still have to use the correction value in the case of matter.

Based on https://www.iforce2d.net/b2dtut/projected-trajectory, I sort of get why you need to calculate based on time steps versus seconds. I need to go through the math a bit more, but it does make sense. Using his version, it was roughly 1.01 scaled up from when using the physics formula.

Another curious thing I noticed from my quick look at Verlet integration and how it works is your formulas seem to be slightly different. Note I didn't go through the math entirely. I'm sure you have your reasons. I'm just wondering is this also is a source for the weirdness.

Hope this isn't taken the wrong way. I think the engine is quite nice and you've done a great job. Just noticing some pain points in usage which seem valuable not just to me, but to others as well.

driescroons commented 4 years ago

Any updates on this? Also struggling with impulse / force required to reach certain height.