liabru / matter-js

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

Unexpected loss of energy in frictionless perfectly elastic setting #256

Open mbchang opened 8 years ago

mbchang commented 8 years ago

I'm using a modified version of the beachBalls example, with no friction, no air friction, and restitution of 1. When I let it run in the demo window, the balls eventually stop bouncing and come to a stop. I would expect them to keep bouncing indefinitely.

(function() {

    var World = Matter.World,
        Bodies = Matter.Bodies,
        Composites = Matter.Composites;

    Example.frictionlessBeachBalls = function(demo) {

        var engine = demo.engine,
            world = engine.world;

        // need random initialization
        var stack = Composites.stack(0, 100, 3, 1, 20, 0, function(x, y) {
            return Bodies.circle(x, y, 75, { restitution: 1, friction: 0, frictionAir: 0, frictionStatic: 0 });
        });
        World.add(world, stack);
    };

})();

I also tried setting the restitution of the world boundaries to 1 (the code below is modified from the Demo.reset, and that did not help.

        World.add(world, [
            Bodies.rectangle(400, -offset, 800.5 + 2 * offset, 50.5, { isStatic: true, restitution: 1 }),
            Bodies.rectangle(400, 600 + offset, 800.5 + 2 * offset, 50.5, { isStatic: true, restitution: 1 }),
            Bodies.rectangle(800 + offset, 300, 50.5, 600.5 + 2 * offset, { isStatic: true, restitution: 1 }),
            Bodies.rectangle(-offset, 300, 50.5, 600.5 + 2 * offset, { isStatic: true, restitution: 1 })
        ]);

I thought this might be an approximation issue when computing the effects of collisions and set high numbers for positionIterations, velocityIterations, and constraintIterations.

        demo.engine.positionIterations = 1000
        demo.engine.velocityIterations = 1000
        demo.engine.constraintIterations = 1000

This did not help either.

Is this loss of energy due to numerical approximation errors, or can this energy loss be explicitly controlled? At the moment beyond adjusting restitution and friction I am unable to find ways to make energy conservation perfect.

liabru commented 8 years ago

See #21. Basically the linear momentum is being converted to angular momentum on collision (because circles are approximated at the moment).

liabru commented 8 years ago

Hopefully my last post helped you resolve your issue, so closing this. Feel free to reply if you need more help.

wmike1987 commented 8 years ago

I was trying something similar to the original poster. Just a ball bouncing on a static body.

var ball = Bodies.circle(100, 250, 50, { inertia: Infinity, restitution: 1, friction: 0, frictionAir: 0, frictionStatic: 0});

Even with inertia set to Infinity, the ball loses energy and bounces closer and closer to the ground, although interestingly never fully stopping. It seemingly bounces forever very close to the ground. Am I missing something?

liabru commented 8 years ago

How quickly does it lose energy? I realise conservation isn't exactly perfect at the moment, which is a bit of a problem, but hopefully it takes a while? Can you show a jsfiddle?

wmike1987 commented 8 years ago

It loses it noticeably quick.

https://jsfiddle.net/vd7d25pu/7/

liabru commented 8 years ago

Thanks for the example. It's pretty quick indeed, I will investigate this. I expect it may be due to floating point errors but it does seem a little quick even for that.

wmike1987 commented 8 years ago

Take a look at this jsfiddle:

https://jsfiddle.net/vd7d25pu/10/

Notice that the ball does appear to bounce forever here. The only difference is that the 'ground' rectangle's height is set to 1. I stumbled upon this accidentally.

kujma10-zz commented 7 years ago

I have the same problem with many balls (gravity turned off) Here is my jsfiddle example: https://jsfiddle.net/vd7d25pu/11/

I considered @wmike1987 's comment above and set 1 height to borders. However balls are still losing the force and not moving forever, they stop moving after some time. Probably an energy loss is after ball collisions.

kujma10-zz commented 7 years ago

One thing I noticed: if I increase mass of the balls (and force proportionally) then it lasts longer but still not infinite.

liabru commented 7 years ago

Thanks for the test case. My guess is that at least some of the loss is due to floating point errors (it seems that even box2d has the same issue). There's an article on energy drift which suggests the same.

My suggestion is to artificially add energy (through velocity or forces) back in over time, which you may be able to do yourself using collision or tick events. But a generic solution for any situation might be difficult.

wmike1987 commented 7 years ago

Any idea why this is happening?

https://jsfiddle.net/vd7d25pu/7/ - loses energy https://jsfiddle.net/vd7d25pu/10/ - appears to not lose energy

Hardly any difference in the code, just the size of one of the objects.

danielsvane commented 7 years ago

After some testing, it seems like the radius of the circle (in my case) had an influence on how long it bounced. This is related to the mass, which I suspect is the deciding factor. My temporary workaround is to set a restitution of 1.01.

liabru commented 7 years ago

Interesting. Maybe try using Bodies.polygon with a high number of sides (like 50) to see if that helps.

vincenzoml commented 5 years ago

I am facing the same issues. I want a very slow body (ball or rectangle does not change what I get) that bounces forever with no friction. I get a body that stops at a wall, if the initial force is very low. I've used { inertia: Infinity, inverseInertia: 0, restitution: 1, frictionStatic: 1, frictionAir: 0, friction: 0 }

Any suggestion?

Menowa1709 commented 4 years ago

in beforeUpdate event:

if(ball.speed != 0){
    let speedMultiplier = 11.241098900509593/ball.speed   // 11.241098900509593 == initial (starting) ball speed
    Body.setVelocity(
      ball,
      { x: ball.velocity.x*speedMultiplier, y: ball.velocity.y*speedMultiplier }
    );
  }
Menowa1709 commented 4 years ago

in beforeUpdate event:

if(ball.speed != 0){
    let speedMultiplier = 11.241098900509593/ball.speed   // 11.241098900509593 == initial (starting) ball speed
    Body.setVelocity(
      ball,
      { x: ball.velocity.x*speedMultiplier, y: ball.velocity.y*speedMultiplier }
    );
  }

or afterCollision

Dediggefedde commented 4 years ago

Hi! Sorry for gravedigging.

This still seems to be an issue. I put multiple balls in a box (no friction, infinite inertia) and calculate the total kinetic energy over time. I initialize all of them by applyForce. There are 3 major problems. 1. Energy loss over time, 2. Sticky walls, 3. reflection angle on walls not always correct (tendency towards the wall). If the E-loss would just be a small loss per collision it would be fine, but it seems to distort the collision angle as well. In case a wall is sticky, I checked on the body.velocity x/y ratio and it was still the same (ignoring that one component was 0 since it stuck in the wall).

With the Workaround of @Menowa1709 the energy is conserved, walls are not sticky and the angle looks fine. Here is a fiddle where the fix can be switched on/off on line 77 https://jsfiddle.net/dediggefedde/3n6whja5/22/

The problem of the workaround is ignoring inertia and friction if it is switched on. also momentum transfer based on mass is not working. (In my code I made a small error setting all speeds to the first ones, but even splitting them does not fix this. One would need to calculate the e-transfer by hand)

michael-garofalo commented 3 years ago

This looks like it's still an issue, so I'm posting up what I found.

The momentum loss is quite significant. I can't just increase the bounce above 1, because then it will eventually gain too much energy. (Without CCD, the bouncing element will just fly out of the scene.)

Based on previous conversations, I tried two things to fix this problem.

You should check the slop property. https://github.com/liabru/matter-js/issues/862#issuecomment-635012278

I didn't even know that setting existed. 😄 The initial value is 0.05. I set it to 0. I didn't notice a difference.

Setting the Inertia to Infinity was another suggestion.

Here there's also energy being converted to torque, so try also setting the ball's inertia to infinity so the ball can not rotate, for me it then bounces for much longer - is this what you need? https://github.com/liabru/matter-js/issues/21#issuecomment-42775549

This didn't solve the problem, but it did change the results. The bouncing element will still lose momentum quickly, but not totally. When the distance between the two colliding elements is really close, the bouncing element no longer seems to lose momentum. That's not much new information, but that seemed like really strange behavior to me. Maybe that helps to track down this problem.

bounce-test

michael-garofalo commented 3 years ago

Here's some HTML to quickly test this problem... (It's a slightly modified version of the "Getting Started" Tutorial.)

<!DOCTYPE html>
<html lang="en">

<head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>Bounce Test</title>
     <script src="matter.min.js"></script>
</head>

<body>
     <script>
          // module aliases
          var Engine = Matter.Engine,
               Render = Matter.Render,
               World = Matter.World,
               Bodies = Matter.Bodies;

          // create an engine
          var engine = Engine.create();

          // create a renderer
          var render = Render.create({
               element: document.body,
               engine: engine
          });

          // create two boxes and a ground
          var circle = Bodies.circle(400, 200, 80, { restitution: 1, friction: 0, frictionAir: 0, frictionStatic: 0 });
          var square = Bodies.rectangle(200, 200, 40, 40, { restitution: 1, friction: 0, frictionAir: 0, frictionStatic: 0 });
          var polygon = Bodies.polygon(600, 200, 1000, 80, { restitution: 1, friction: 0, frictionAir: 0, frictionStatic: 0 })
          var ground = Bodies.rectangle(400, 610, 810, 60, { isStatic: true, restitution: 1, friction: 0, frictionAir: 0, frictionStatic: 0 });

          // add all of the bodies to the world
          World.add(engine.world, [circle, square, polygon, ground]);

          // run the engine
          Engine.run(engine);

          // run the renderer
          Render.run(render);
     </script>

</body>

</html>

I saw the 0.15.0 update, and I thought that I saw this issue listed as closed, so I decided to test this issue once again. It seems that the problem still exists.

Through some experimentation, I realized that squares don't bounce indefinitely. They don't bounce very well at all actually. Also, liabru made an interesting suggestion...

Maybe try using Bodies.polygon with a high number of sides (like 50) to see if that helps.

That's why I looked more closely at the circle body.

circle-close-up

It doesn't look like a circle. It looks like a 26-sided polygon. So, I added a 1000 sided polygon next to the circle. Surprisingly, the 1000-sided polygon bounces similarly. I thought it might be better at preserving energy. It is, but only towards the end. While the circle eventually stops, the polygon keeps bouncing — in a similar manner to the previous animated GIF.

karimshalapy commented 3 years ago

Any updates on this issue?

karimshalapy commented 3 years ago

I wanted to do a collision simulation, and a Brownian motion simulation but the energy loss in a fully elastic world is recognizable and the system becomes idle fairly quickly especially when a ball hits a stationary wall. In order to get the desired outcome, I had to implement the physics logic in the "collisionStart" event on the engine.

And here's what I did to achieve the Brownian motion...

Events.on(engine, "collisionStart", e => {
        e.pairs.forEach(pair => {
            const { bodyA, bodyB } = pair
            if (bodyA.label === "wallH") Body.setVelocity(bodyB, { x: bodyB.velocity.x, y: -bodyB.velocity.y }) //Horizontal wall collision
            else if (bodyB.label === "wallH") Body.setVelocity(bodyA, { x: bodyA.velocity.x, y: -bodyA.velocity.y }) //Horizontal wall collision
            else if (bodyA.label === "wallV") Body.setVelocity(bodyB, { x: -bodyB.velocity.x, y: bodyB.velocity.y }) //Vertical wall collision
            else if (bodyB.label === "wallV") Body.setVelocity(bodyA, { x: -bodyA.velocity.x, y: bodyA.velocity.y }) //Vertical wall collision
            else { //Two balls collision
                const vAXBefore = bodyA.velocity.x;
                const vBXBefore = bodyB.velocity.x;
                const vAYBefore = bodyA.velocity.y;
                const vBYBefore = bodyB.velocity.y;
                const mA = bodyA.mass
                const mB = bodyB.mass
                const { vAFinal: vAXFinal, vBFinal: vBXFinal } = calcElasticCollision(mA, mB, vAXBefore, vBXBefore)
                const { vAFinal: vAYFinal, vBFinal: vBYFinal } = calcElasticCollision(mA, mB, vAYBefore, vBYBefore)
                if (bodyA.label !== "wall") Body.setVelocity(bodyA, { x: vAXFinal, y: vBXFinal })
                if (bodyB.label !== "wall") Body.setVelocity(bodyB, { x: vAYFinal, y: vBYFinal })
            }
        })
    })

Add some walls

Composite.add(world, [
        Bodies.rectangle(-50, canvasHeight / 2, 50, canvasHeight + 200, {
            label: "wallV",
            isStatic: true,
            friction: 0,
            frictionAir: 0,
            frictionStatic: 0,
            restitution: 1,
            render: { visible: false }
        }),
        Bodies.rectangle(canvasWidth + 50, canvasHeight / 2, 50, canvasHeight + 200, {
            label: "wallV",
            isStatic: true,
            friction: 0,
            frictionAir: 0,
            frictionStatic: 0,
            restitution: 1,
            render: { visible: false }
        }),
        Bodies.rectangle(canvasWidth / 2, -50, canvasWidth + 200, 50, {
            label: "wallH",
            isStatic: true,
            friction: 0,
            frictionAir: 0,
            frictionStatic: 0,
            restitution: 1,
            render: { visible: false }
        }),
        Bodies.rectangle(canvasWidth / 2, canvasHeight + 50, canvasWidth + 200, 50, {
            label: "wallH",
            isStatic: true,
            friction: 0,
            frictionAir: 0,
            frictionStatic: 0,
            restitution: 1,
            render: { visible: false }
        }),
    ])

Then I add balls at random locations and set a random initial velocity to each one.

const balls = Array.from({ length: 100 }, () => Bodies.circle(getRandomNumber(5, canvasWidth - 5), getRandomNumber(5, canvasHeight - 5), 5, {
        render: { fillStyle: "blue" },
        frictionAir: 0,
        friction: 0,
        frictionStatic: 0,
        inertia: Infinity,
        restitution: 1,
    }))
 Composite.add(world, balls)
 balls.forEach(ball => Body.setVelocity(ball, { x: getRandomNumber(-3, 3), y: getRandomNumber(-3, 3) }))

And here are the Helper functions:

const getRandomNumber = (min, max) => Math.random() * (max - min) + min
const calcElasticCollision = (mA, mB, vAInitial, vBInitial) => ({
    vAFinal: (((mA - mB) / (mA + mB)) * vAInitial) + (((2 * mB) / (mA + mB)) * vBInitial),
    vBFinal: (((2 * mA) / (mA + mB)) * vAInitial) + (((mB - mA) / (mA + mB)) * vBInitial)
})

The function calcElasticCollision calculates the final velocity after an elastic collision according to the equations: One-dimensional Newtonian For more information search Elastic collisions or check the equations for a brief explanation on the wiki here

Doing this will provide you with a fully elastic collision system that never loses energy, this would work with one-dimensional collision too, but too complicated stuff I have no guarantees it'll work. Anyway, this is my workaround and I hope this helps someone who needed the same thing I did, and I hope this issue is solved and closed soon enough.

Regards,

liabru commented 3 years ago

@karimshalapy thanks for sharing!

This is still on my list of improvements I'd like to make (quite a long list though!)

For now my suggestions are the following as discussed earlier in the thread:

lout33 commented 3 years ago

Adding this line of code seems resolve the issue.

Matter.Resolver._restingThresh = 0.001;

liabru commented 3 years ago

@lout33 good point I think that could help! Only thing is it will apply globally to all bodies, but probably reasonable if you don't need resting collisions or if they still work fine with that setting. This is good info though for when I get around to visiting this topic properly, it may need some tuning or a new body property.

Shilo commented 1 year ago

Adding this line of code seems resolve the issue.

Matter.Resolver._restingThresh = 0.001;

Thank you so much. I spent hours attempting every possible solution and this was the only solution that worked for me. What a nightmare.

In Phaser, you can use MatterWorldConfig.restingThresh property: https://newdocs.phaser.io/docs/3.55.2/Phaser.Types.Physics.Matter.MatterWorldConfig

jeroenheijmans commented 1 year ago

Thanks for all the info and interaction in this thread! I ran into the same issue while using MatterJS with Phaser, and could not resolve it so far. Figured I'd add my reproducible scenario in case it's helpful in tweaking this.

For context, Phaser has a "bounce test" for its other (simpler) "Arcade" Physcis engine which at first seems to perfectly keep bouncing back to the same height, until you increase the gravity from y: 150 to y: 151: then it slowly starts loosing energy (though a lot slower than MatterJS, see below).

My repro is a fork from above test, where I've tried to apply really all of the fixes from this thread....

physics: {
    default: 'matter',
    matter: {
        debug: true,
        setBounds: true,
        restingThresh: 0.00001,
        positionIterations: 30,
        velocityIterations: 25,
        constraintIterations: 18,
        runner: {
            isFixed: true,
            fps: 240,
            correction: 10,
        },
        gravity: { y: 3 }
    }
},
this.logo = this.matter.add.rectangle(300, 100, 40, 10, {
    inertia: Infinity,
    restitution: 1,
    friction: 0,
    frictionAir: 0,
    frictionStatic: 0,
    slop: 0.5,
});

A runnable example can be found at https://jsfiddle.net/jeroenheijmans/agncp79q/ and here's a screen recording of how quickly energy is lost from the system:

matterjs-energy-loss animated gif

Most likely I incorrectly applied one of the abovementioned workarounds, if that's the case a suggested fix or better fork of the jsfiddle would be much appreciated.

Regardless, thank you for a wonderful piece of open source software! Much appreciated that we can all play around with this tool 💛

morekid commented 1 year ago
  • prevent friction body.friction = 0 and body.frictionAir = 0
  • prevent rotation body.inertia = Infinity
  • increase or decrease body.slop
  • use a small fixed timestep Engine.update(engine, 1) and call multiple updates per frame
  • use more iterations engine.positionIterations = 20 and engine.velocityIterations = 20
  • manually set a constant velocity on every frame Body.setVelocity(body, Vector.mult(Vector.normalise(body.velocity), speed))

All of the above helped a little in my case, but this is what considerably reduced the energy loss:

  • use a small fixed timestep Engine.update(engine, 1) and call multiple updates per frame

I've replaced:

Engine.update(engine, 1000 / fps)

with:

for (let i = 0; i < 1000 / fps; i++) {
  Engine.update(engine, 1);
}