Reibung in abhängigkeit der framerate simulieren

Hey Leute.

Im Prinzip eine einfache Frage, auf die ich meinte eine Antwort gefunden zu haben…
aber so ganz bin ich mir nicht sicher, ob das so richtig ist.

Man nehme einen simplen vom boden abtitschenden Ball.
Wenn ich diesen nun anstupse fängt der an rumzuhüpfen, und bekommt auch
ne bewegung auf der x achse verpasst.

Letztere muss ja mit der Zeit langsamer werden - (Luft-) Reibung halt.
Mir fallen folgende Lösungen ein:

sx *= 1.0f - reibung * deltaInSeconds
sx -= sx * reibung * AGLRenderController.getDeltaS()

  1. Funktioniert zwar, aber “richtig” kann das ja nicht sein. Funktionieren tut es nur, weil deltaInSeconds durchschnittlich irgendwie 1/10000 bei mir ist,
    was wäre zB wenn delta > 1 ? Dann würde der Ball sich auf einmal in die falsche richtung bewegen…

  2. Naja… das sieht irgendwie nicht… realistisch aus. Und auch hier wäre bei nem zu großen delta wert alles kaputt.

Wie macht man das nun also? wenn ich zB eine geschwindigkeitsabnahme von 0.9 pro sekunde haben will? Was muss ich dann rechnen?
Bin da irgendwie zu dumm für gerade…

[QUOTE=mymaksimus]
Wie macht man das nun also? wenn ich zB eine geschwindigkeitsabnahme von 0.9 pro sekunde haben will? Was muss ich dann rechnen?[/QUOTE]

velocity *= 0.9;; !?

(Das ist gar nicht so unüblich, siehe z.B. HOWTO damping - ODE Wiki - und sollte auch nicht von der Framerate abhängen…)

ja marco, und das pro frame, wenn du bspw 100 fps hast? Dann ist innerhalb von null komma nix die geschwindigkeit wieder 0…

*** Edit ***

sondern von was? Ich meine ja auch nicht „von der framerate“ abhängen… sondern eben
„nicht von der framerate abhängen“… also unabhängig von der fr gleiche abnahme… also muss
delta irgendwie mit einberechnet werden…

Nun, wie berechnest du denn die Geschwindigkeit, bzw. das “Delta V”? (Auch wenn das bei mir nicht gaaanz frisch im Gedächtnis ist: Sollte der konstante Faktor nicht wirklich nur ein konstanter Faktor sein, also unabhängig davon, wie groß der Zeitschritt ist? (Kann aber auch grober Unfug sein, ggf. probier ich es bei Gelegenheit mal aus…))

vermeinlich üblich, nicht dass ich dazu je viel ernsthaft gemacht hätte, ist Messen der real vergangenen Zeit,
darauf die Berechnungen von Zaunpfahl-benannten Echtzeitprozessen ausrichten

ob das Programm zwischendurch Anzeige ausgeschaltet hat (aber weiterläuft), böse laggt oder 10 FPS oder 100 FPS hat, ist dafür egal


bei der Art der Abbremsung ist es ein vielleicht üblich aber auch gefährlicher Fehler, nur nach Berechnungen in Java zu gehen,
überlege doch erstmal grob, wie es in der realen Welt bzw. als Wunschergebnis aussehen soll,

das kann man sich natürlich nicht unbedingt gut vorstellen, hilfreich wenn das Programm es zeigt,
vielleicht Richtung Prozente gehen: je Delta Zeit den Wert um x% ändern, am Anfang stärker abgebremst als später,

irgendwann aber auf eine Mindestbremsung gehen, soll nicht die Halbwertszeit von Atommüll werden…,
und bei 0 natürlich evtl. aufhören, was auch immer da gerade modelliert wird

Überlege doch einfach mal, was “fps” bedeutet: Das “s” steht für Sekunde, und daran solltest Du Dich orientieren. So ein Bewegungsablauf darf auch gerne mal etwas flinker stattfinden, je nach Realitätsgrad, es soll ja ein Spiel werden - Basis bleibt allerdings die reale Zeit. In einer 3D-Welt versucht man ja auch, möglichst maßstabsgetreu zu arbeiten. Dir bleibt jetzt nur eigentlich nur, die zurückzulegende Strecke pro Frame festzulegen, d.h. diese wirst Du bei jedem update neu berechnen müssen. Dazu musst Du natürlich die Zeit messen, die zwischen zwei Frames liegt.

Ja, das einfache Multiplizieren war natürlich Unfug, bzw. funktioniert nur in Spezialfällen. (Notiz an mich: Nach Mitternacht keine Gremlins füttern UND keine Fragen mehr beantworten :o ). Dabei wird ja z.B. die Masse gar nicht berücksichtigt (im Gegensatz zu dem, was auch auf der verlinkten Seite steht). Eine Bleikugel wird weniger durch die („Luft“) Dämpfung beeinflusst, als eine Plastikkugel.

Richtig wäre also, eine Kraft auf das Objekt anzuwenden, die von der aktuellen Geschwindigkeit abhängt (und die dann - abhängig von der Masse des Objekts - die Beschleunigung und damit die zukünftige Geschwindigkeit verändert).

Das wichtige dabei ist: Das sind alles rein physikalische Berechnungen. Eine „Framerate“ gibt es gar nicht (d.h. man kann das auch ablaufen lassen, ohne überhaupt was auf dem Bildschirm anzuzeigen). Aaaaaber: Natürlich gibt es eine Zeitschrittgröße, die (bei einem „Echtzeitspiel“) von der Framerate abhängt. Je größer der Zeitschritt ist, desto „ungenauer“ werden die Berechnungen. Aber das ist ein allgemeines Problem, und gilt auch ohne Dämpfung. Die Dämpfung an sich schmiegt sich in die übrigen Berechnungen ein (und hängt damit genauso viel oder wenig von der Framerate ab, wie alle anderen).

Hab’ da mal schnell ein altes Beispiel so umgehackt, dass es eine „DampingForce“ gibt, die man mit einem Slider ein bißchen verändern kann.

ACHTUNG: Das ist wirklich nur ein altes, umgebautes Beispiel, und einiges davon ist inzwischen schon recht häßlich.

Aber dass die Dämpfung (Frameratenunabhängig) direkt in die physikalische Berechnung einfließt ist eben DerSpringendePunkt :smiley:

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.Path2D;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.SwingUtilities;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

public class DerSpringendePunkt
{
    public static void main(String[] args)
    {
        SwingUtilities.invokeLater(new Runnable()
        {
            @Override
            public void run()
            {
                createAndShowGUI();
            }
        });
    }

    private static void createAndShowGUI()
    {
        JFrame f = new JFrame();
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.getContentPane().setLayout(new BorderLayout());

        PointPhysicsRunner pointPhysicsRunner = new PointPhysicsRunner();

        f.getContentPane().add(
            pointPhysicsRunner.getPointPhysicsPanel(), BorderLayout.CENTER);
        
        JPanel controlPanel = createControlPanel(pointPhysicsRunner);
        
        f.getContentPane().add(controlPanel, BorderLayout.NORTH);
        f.setSize(800,800);
        f.setLocationRelativeTo(null);
        f.setVisible(true);
    }
    
    
    private static JPanel createControlPanel(
        final PointPhysicsRunner pointPhysicsRunner)
    {
        JPanel controlPanel = new JPanel(new GridLayout(0,1));
        
        JButton emitButton = new JButton("Emit");
        emitButton.addActionListener(new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                PointPhysics pointPhysics = 
                    pointPhysicsRunner.getPointPhysics();
                pointPhysics.emitPoint(0.0, 0.9, 0.25, 0.0);
            }
        });
        JPanel p0 = new JPanel();
        p0.add(emitButton);
        controlPanel.add(p0);
        
        
        final JSlider updateDelayMsSlider = new JSlider(1, 250, 10);
        final JLabel updateDelayMsLabel = new JLabel("Delay: 10");
        updateDelayMsLabel.setPreferredSize(
            new Dimension(150, 30));
        updateDelayMsSlider.addChangeListener(new ChangeListener()
        {
            @Override
            public void stateChanged(ChangeEvent e)
            {
                int updateDelayMs = updateDelayMsSlider.getValue();
                updateDelayMsLabel.setText("Delay: "+updateDelayMs);
                pointPhysicsRunner.setUpdateDelayMs(updateDelayMs);
            }
        });
        
        JPanel p1 = new JPanel(new BorderLayout());
        p1.add(updateDelayMsLabel, BorderLayout.WEST);
        p1.add(updateDelayMsSlider, BorderLayout.CENTER);
        controlPanel.add(p1);


        final JSlider dampingSlider = new JSlider(0, 100, 0);
        final JLabel dampingLabel = new JLabel("Damping: 0");
        dampingLabel.setPreferredSize(
            new Dimension(150, 30));
        dampingSlider.addChangeListener(new ChangeListener()
        {
            @Override
            public void stateChanged(ChangeEvent e)
            {
                double damping = dampingSlider.getValue() / 100.0;
                dampingLabel.setText("Damping: "+damping);
                PointPhysics pointPhysics = 
                    pointPhysicsRunner.getPointPhysics();
                pointPhysics.setDamping(damping);
            }
        });
        
        JPanel p2 = new JPanel(new BorderLayout());
        p2.add(dampingLabel, BorderLayout.WEST);
        p2.add(dampingSlider, BorderLayout.CENTER);
        controlPanel.add(p2);

        
        return controlPanel;
    }
}


class PointPhysicsRunner
{
    private final PointPhysics pointPhysics;
    private final PointPhysicsPanel pointPhysicsPanel;
    private int updateDelayMs = 10;
    
    PointPhysicsRunner()
    {
        pointPhysics = new PointPhysics();
        pointPhysicsPanel = new PointPhysicsPanel(pointPhysics);
        
        Thread thread = new Thread(new Runnable()
        {
            @Override
            public void run()
            {
                runPhysics();
            }
        }, "PointPhysicsRunner");
        thread.setDaemon(true);
        thread.start();
        
    }

    private void runPhysics()
    {
        long previousNS = System.nanoTime();
        while (true)
        {
            long currentNS = System.nanoTime();
            double seconds = (currentNS - previousNS) / 1e9;
            previousNS = currentNS;
            pointPhysics.doStep(seconds);
            pointPhysicsPanel.repaint();
            try
            {
                Thread.sleep(updateDelayMs);
            }
            catch (InterruptedException e)
            {
                Thread.currentThread().interrupt();
                return;
            }
        }
    }
    
    public void setUpdateDelayMs(int updateDelayMs)
    {
        this.updateDelayMs = updateDelayMs;
    }
    
    public PointPhysics getPointPhysics()
    {
        return pointPhysics;
    }
    
    public PointPhysicsPanel getPointPhysicsPanel()
    {
        return pointPhysicsPanel;
    }
}


class PointPhysicsPanel extends JPanel
{
    private final PointPhysics pointPhysics;
    private Path2D currentPath;
    private Path2D previousPath;
    private PhysicalPoint previousPhysicalPoint;

    public PointPhysicsPanel(PointPhysics pointPhysics)
    {
        this.pointPhysics = pointPhysics;
    }
    

    @Override
    protected void paintComponent(Graphics gr)
    {
        super.paintComponent(gr);
        Graphics2D g = (Graphics2D)gr;
        
        List<PhysicalPoint> physicalPoints = pointPhysics.getPhysicalPoints();
        if (!physicalPoints.isEmpty())
        {
            PhysicalPoint pp = physicalPoints.get(0);
            if (pp != previousPhysicalPoint)
            {
                previousPhysicalPoint = pp;
                previousPath = currentPath;
                currentPath = new Path2D.Double();
                
                Point2D position = pp.getPosition();
                int x = (int)(position.getX()*getWidth());
                int y = getHeight() - (int)(position.getY()*getHeight());
                currentPath.moveTo(x, y);
            }
            else
            {
                Point2D position = pp.getPosition();
                int x = (int)(position.getX()*getWidth());
                int y = getHeight() - (int)(position.getY()*getHeight());
                currentPath.lineTo(x, y);
            }
        
            g.setColor(Color.RED);
            for (PhysicalPoint physicalPoint : physicalPoints)
            {
                Point2D position = physicalPoint.getPosition();
                int x = (int)(position.getX()*getWidth());
                int y = getHeight() - (int)(position.getY()*getHeight());
                g.fillOval(x-5, y-5, 10, 10);
            }
        }

        g.setColor(Color.BLACK);
        int y = getHeight() - (int)(pointPhysics.getFloorHeight()*getHeight());
        g.drawLine(0, y, getWidth(), y);
        
        if (previousPath  != null)
        {
            g.setColor(Color.LIGHT_GRAY);
            g.draw(previousPath);
        }
        if (currentPath  != null)
        {
            g.setColor(Color.DARK_GRAY);
            g.draw(currentPath);
        }
    }
}


interface Force
{
    Point2D computeAcceleration(PhysicalPoint physicalPoint);
}

class Gravity implements Force
{
    @Override
    public Point2D computeAcceleration(PhysicalPoint physicalPoint)
    {
        return new Point2D.Double(0, -9.81*0.1);
    }
}

class DampingForce implements Force
{
    private double coefficient = 0.0;
    
    public void setCoefficient(double coefficient)
    {
        this.coefficient = coefficient;
    }
    
    @Override
    public Point2D computeAcceleration(PhysicalPoint physicalPoint)
    {
        Point2D velocity = physicalPoint.getVelocity();
        double ax = - coefficient * velocity.getX() / physicalPoint.getMass();
        double ay = - coefficient * velocity.getY() / physicalPoint.getMass();
        return new Point2D.Double(ax, ay);
    }
}



class PointPhysics
{
    private final List<PhysicalPoint> physicalPoints;
    private double totalTime = 0;
    private final List<Force> forces;
    private DampingForce dampingForce;
    private double floorHeight = 0.1;
    
    public PointPhysics()
    {
        physicalPoints = new CopyOnWriteArrayList<PhysicalPoint>();
        forces = new ArrayList<Force>();

        forces.add(new Gravity());
        
        dampingForce = new DampingForce();
        forces.add(dampingForce);
    }
    

    public void setDamping(double damping)
    {
        dampingForce.setCoefficient(damping);
    }

    double getFloorHeight()
    {
        return floorHeight;
    }

    public List<PhysicalPoint> getPhysicalPoints()
    {
        return Collections.unmodifiableList(physicalPoints);
    }

    public void doStep(double t)
    {
        removeOld();
        updateAccelerations();
        updateVelocities(t);
        updatePositions(t);
        handleFloorCollisions();
        totalTime += t;
        
        //System.out.println("After "+totalTime+": "+physicalPoints);
    }

    public void emitPoint(double px, double py, double vx, double vy)
    {
        physicalPoints.clear();
        
        PhysicalPoint physicalPoint = new PhysicalPoint();
        physicalPoint.setPosition(new Point2D.Double(px, py));
        physicalPoint.setVelocity(new Point2D.Double(vx, vy));
        physicalPoints.add(physicalPoint);
    }
    
    private void removeOld()
    {
        List<PhysicalPoint> toRemove = new ArrayList<PhysicalPoint>();
        for (PhysicalPoint physicalPoint : physicalPoints)
        {
            Point2D position = physicalPoint.getPosition();
            if (position.getX() < 0 || position.getX() > 1)
            {
                toRemove.add(physicalPoint);
            }
        }
        physicalPoints.removeAll(toRemove);
    }

    private void updateAccelerations()
    {
        for (PhysicalPoint physicalPoint : physicalPoints)
        {
            Point2D totalAcceleration = new Point2D.Double();
            for (Force force : forces)
            {
                Point2D acceleration = force.computeAcceleration(physicalPoint);
                addAssign(totalAcceleration, acceleration);
            }
            physicalPoint.setAcceleration(totalAcceleration);
            //System.out.println("acc "+physicalPoint.getAcceleration());
        }
    }

    private void updateVelocities(double t)
    {
        for (PhysicalPoint physicalPoint : physicalPoints)
        {
            Point2D velocity = physicalPoint.getVelocity();
            Point2D acceleration = physicalPoint.getAcceleration();
            scaleAddAssign(velocity, t, acceleration);
            physicalPoint.setVelocity(velocity);
            //System.out.println("vel "+physicalPoint.getVelocity());
        }
    }

    
    private void updatePositions(double t)
    {
        for (PhysicalPoint physicalPoint : physicalPoints)
        {
            Point2D position = physicalPoint.getPosition();
            Point2D velocity = physicalPoint.getVelocity();
            scaleAddAssign(position, t, velocity);
            physicalPoint.setPosition(position);
            //System.out.println("pos "+physicalPoint.getPosition());
        }
    }

    private void handleFloorCollisions()
    {
        for (PhysicalPoint physicalPoint : physicalPoints)
        {
            Point2D position = physicalPoint.getPosition();
            if (position.getY() < floorHeight)
            {
                position.setLocation(position.getX(), 
                    floorHeight + (floorHeight - position.getY()));
                physicalPoint.setPosition(position);
                
                Point2D velocity = physicalPoint.getVelocity();
                physicalPoint.setVelocity(
                    new Point2D.Double(velocity.getX(), -velocity.getY()));
            }
            //System.out.println("pos "+physicalPoint.getPosition());
        }
    }
    
    private static void addAssign(Point2D result, Point2D addend)
    {
        double x = result.getX() + addend.getX();
        double y = result.getY() + addend.getY();
        result.setLocation(x, y);
    }

    private static void scaleAddAssign(Point2D result, double factor, Point2D addend)
    {
        double x = result.getX() + factor * addend.getX();
        double y = result.getY() + factor * addend.getY();
        result.setLocation(x, y);
    }

}


class PhysicalPoint
{
    private final Point2D position;
    private final Point2D velocity;
    private final Point2D acceleration;
    private final double mass;

    public PhysicalPoint()
    {
        position = new Point2D.Double();
        velocity = new Point2D.Double();
        acceleration = new Point2D.Double();
        mass = 1;
    }

    public Point2D getPosition()
    {
        return new Point2D.Double(position.getX(), position.getY());
    }
    public void setPosition(Point2D point)
    {
        position.setLocation(point);
    }

    public Point2D getVelocity()
    {
        return new Point2D.Double(velocity.getX(), velocity.getY());
    }
    public void setVelocity(Point2D point)
    {
        velocity.setLocation(point);
    }

    public Point2D getAcceleration()
    {
        return new Point2D.Double(acceleration.getX(), acceleration.getY());
    }
    public void setAcceleration(Point2D point)
    {
        acceleration.setLocation(point);
    }

    public double getMass()
    {
        return mass;
    }

    @Override
    public String toString()
    {
        return String.format("PhysicalPoint[p=(%.2f,%.2f),v=(%.2f,%.2f),a=(%.2f,%.2f)]",
            position.getX(), position.getY(),
            velocity.getY(), velocity.getY(),
            acceleration.getX(), acceleration.getY());
    }

}

Hm danke Leute.

Alles sinnvoll was ihr schreibt, danke… ich muss mal schauen
wie ich es jetzt genau mache

Aber zu dir, @Marco13 : ^^
Ich hab mir dein Programm mal angesehen… und den reibungsfaktor mal auf größer als 1
gemacht… was mich wundert ist: Wieso fliegt der Ball dann nicht “rückwärts” ?

Beispiel:

x = 0
vx = 1
const damping = 100
const time = 1 //angenommen…
const mass = 1

so. Jetzt damping force anwenden:

ax = - damping * vx / mass // -> -100 * 1 / 1 -> -100

und jetzt vx = ax

dann müsste der ball doch theoretisch rückwärts fliegen??

Ja, mit “dt=1” macht er das auch (einen Schritt lang - dann ist er aus dem Bild…). Im nächsten Schritt ist die Dämpfung ja wieder so hoch, d.h. jede Geschwindigkeit, die er hat, geht innerhalb kürzester Zeit (d.h. innerhalb von 1-2 Schritten) auf “praktisch 0” runter.

Naja, ist dieses verhalten denn richtig? “Dämpfung” sollte doch nicht dazu führen das ein Objekt seine Richtung negiert…

Wenn die Dämpfung ein Faktor zwischen 0 und 1 sein sollte, dann ist 100 eben einfach falsch. Für “damping=1” wäre
ax = - damping * vx / mass // -> -1 * 1 / 1 -> -1
d.h. im nächsten Zeitschritt wäre die Geschwindigkeit 0…

Hmm, mathematisch ist es schon möglich dass sich etwas “rückwärts” bewegt, aber physikalisch eben nicht da die Naturgesetze den möglichen Wertebereich vorgeben. Dies muss halt vorher geprüft werden ob die Werte “realistisch” sind bzw in einem definierten Wertebereich liegen.

das

und das wäre meine nächste frage gewesen… ob es korrekt wäre einen wertebereich zu definieren, oder man
rein physikalisch auf alle werte vorbereitet sein müsste.

Okay, dann danke euch, ich schau mal was sich machen lässt :slight_smile:

Also ich würde jetzt bei sowas vorher den Wertebereich prüfen ob dieser in einem “realistischem” und “sinnvollem” Bereich liegt.
“realistisch” im Sinne von - Ist das physikalisch überhaupt so möglich?
“sinnvoll” im Sinne von - Stimmen Richtung und Winkel?

Mag sein das es für bestimmte Anwendungsfälle andere “best practice” gibt, aber man kann ja erstmal einfach anfangen.