Circle Physics Example (Code)



  • Mit ein wenig Mathematik zur Verfügung ist das gar nicht viel Code. Kennt Ihr das YouTube Video "Can Water solve a Maze?". Ich sollte mir auch so ein Maze erstellen und dann minimale Circles durchlaufen lassen.

    #pragma once
    #include "zpDotEngine.h"
    
    namespace physics
    {
    	struct SimulationState
    	{
    		int MaxSteps = 0;
    		int Updates = 0;
    		float ElapsedTime = 0;
    
    		int NumOfBalls = 0;
    		float BallRadius = 0;
    
    		int NumOfEdges = 0;
    		float EdgeRadius = 0;
    
    		float Grav = 0;
    		bool IsGrav = false;
    
    		constexpr zp::Vector2D<float> getGravity() const
    		{
    			if (IsGrav)
    				return zp::Vector2D<float>(0, Grav);
    			else
    				return zp::Vector2D<float>();
    		}
    	};
    
    	struct Ball
    	{
    		std::size_t ID = 0;
    
    		float Radius = 0;
    		float Mass = 0;
    		float Friction = 0;
    			
    		zp::Vector2D<float> Pos, Old;
    		zp::Vector2D<float> Vel;
    		zp::Vector2D<float> Acc;
    		zp::ColorDot Color;
    
    		float SimTimeRemaining = 0;
    	};
    
    	struct Edge
    	{
    		float HitRadius = 0;
    
    		float Radius = 0;
    		float MassFactor = 0;
    
    		zp::Vector2D<float> Start;
    		zp::Vector2D<float> End;
    		zp::ColorDot Color;
    	};
    
    	static constexpr bool checkBallCollision(const Ball& lhs, const Ball& rhs)
    	{
    		return zp::utils::doCirclesOverlap(lhs.Pos, lhs.Radius, rhs.Pos, rhs.Radius);
    	}
    }
    
    class CirclePhysics : public zpDotEngine
    {
    	physics::SimulationState Sim;
    
    	std::vector<physics::Ball> SimBalls;
    	physics::Ball* SelectedBall = nullptr;
    
    	std::vector<physics::Edge> SimEdges;
    	physics::Edge* SelectedEdge = nullptr;
    	bool IsSelectedStartEdge = false;
    
    	void createCanvas();
    	void createSimState();
    	void createBalls();
    	void createEdges();
    	void addBalls(const int N, const float radius);
    
    	void updateBallPositions();
    	void updateBallCollisions();
    	void updateSimulation(const float elapsedTime);
    
    	void drawEdges();
    	void drawBalls();
    	void drawBall(const physics::Ball& ball);
    
    	void drawTargetZone(const physics::Edge& edge);
    
    	virtual bool onUserCreate() override;
    	virtual bool onUserUpdate(const float elapsedTime) override;
    	virtual bool handleUserInput(const float elapsedTime) override;
    
    public:
    	CirclePhysics();
    };
    
    


  • #include "CirclePhysics.h"
    #include <memory>
    
    using namespace physics;
    
    CirclePhysics::CirclePhysics() = default;
    
    void CirclePhysics::createCanvas()
    {
        canvas().color() = zp::ColorDot::DGREY(); 
        canvas().scaleEnabled(false);
        canvas().inverseY(false);
    }
    
    void CirclePhysics::createSimState()
    {
        Sim.MaxSteps = 15;
        Sim.Updates = 4;
    
        Sim.NumOfBalls = 250;
        Sim.BallRadius = 4.f;
    
        Sim.NumOfEdges = 4;
        Sim.EdgeRadius = 2.5f;
    
        Sim.Grav = 0.8f;
        Sim.IsGrav = false;
    }
    
    void CirclePhysics::createBalls()
    {
        addBalls(Sim.NumOfBalls, Sim.BallRadius);
    }
    
    void CirclePhysics::createEdges()
    {
        constexpr auto hitRadius = 3.f;
        constexpr auto massFactor = 0.8f;
        constexpr auto gap = 3.f;
        const auto delta = Sim.EdgeRadius * 2.f + gap;
        const auto color = zp::ColorDot::BLACK();
    
    	for (auto i = 0; i < Sim.NumOfEdges; ++i)
    	{
                    const float posY = screenHeight() - (delta * i + delta);
    
    		Edge edge;
                    edge.HitRadius = hitRadius;
    		edge.Radius = Sim.EdgeRadius;   
                    edge.MassFactor = massFactor;
    		edge.Start = zp::Vector2D<float>(delta, posY);
    		edge.End = zp::Vector2D<float>(screenCenter().x * 0.5f, posY);
    		edge.Color = color;
    		SimEdges.push_back(edge);
    	}
    }
    
    void CirclePhysics::addBalls(const int N, const float radius)
    {
        constexpr auto massFactor = 10.f;
        //constexpr auto fricFactor = 1.f / -10.f;
        constexpr auto friction = -0.1f;
        const auto color = zp::ColorDot::GREY();
    
        for (auto i = 0; i < N; ++i)
        {
            Ball ball;
    
            ball.Radius = radius;
            ball.Mass = ball.Radius * massFactor;
            ball.Pos = zp::Vector2D<float>(
                random().uniformReal<float>(ball.Radius, screenWidth() - ball.Radius),
                random().uniformReal<float>(ball.Radius, screenCenter().y * 0.5f - ball.Radius));
    
            ball.Friction = friction;
            //ball.Friction = ball.Mass * fricFactor; // to do
            
            ball.ID = std::size(SimBalls);
            ball.Color = color;
    
            SimBalls.push_back(ball);
        }
    }
    
    bool CirclePhysics::onUserCreate()
    {
        createCanvas();
        createSimState();
        createEdges();
        createBalls();
    
        return true;
    }
    
    void CirclePhysics::updateBallPositions()
    {
        for (auto i = 0; i < Sim.MaxSteps; ++i)
        {
            for (auto& ball : SimBalls)
            {
                if (ball.SimTimeRemaining > 0.0f)
                {
                    ball.Old = ball.Pos;
    
                    // add drag to emulate friction and gravitation
                    ball.Acc = ball.Vel * ball.Friction + Sim.getGravity();
    
                    // update ball physics
                    ball.Vel += ball.Acc * ball.SimTimeRemaining;
                    ball.Pos += ball.Vel * ball.SimTimeRemaining;
    
                    // check screen borders 
                    if (ball.Pos.x < ball.Radius || ball.Pos.x > screenWidth() - ball.Radius)
                    {
                        ball.Pos.x = std::clamp(ball.Pos.x, ball.Radius, screenWidth() - ball.Radius);
                        ball.Vel.x *= -1.0f;
                    }
    
                    if (ball.Pos.y < 0) ball.Pos.y += screenHeight();
                    if (ball.Pos.y >= screenHeight()) ball.Pos.y -= screenHeight();
    
                    // Stop ball when velocity is neglible 
                    /*if (std::fabs(ball.Vel.mag2()) < 0.01f) // to do
                        ball.Vel.clear();*/
                }
            }
        }
    }
    
    void CirclePhysics::updateBallCollisions()
    {
        std::vector<Ball*> edgeBalls;
        std::vector<std::pair<Ball*, Ball*>> collisions;
    
        // static collision, i.e. overlap
        for (auto& ball : SimBalls)
        {
            // collision against other balls
            for (auto& oBall : SimBalls)
            {
                if (ball.ID != oBall.ID) 
                {
                    if (checkBallCollision(ball, oBall))
                    {
                        // collision has ocurred
                        collisions.push_back(std::make_pair(&ball, &oBall));
                        // distance between ball centers
                        const float distance = ball.Pos.dist(oBall.Pos);
                        const float overlap = 0.5f * (distance - ball.Radius - oBall.Radius);
                        // displace balls
                        ball.Pos -= overlap * (ball.Pos - oBall.Pos) / distance;
                        oBall.Pos += overlap * (ball.Pos - oBall.Pos) / distance;
                    }
                }
            }
    
            // collision against edges
            for (auto& edge : SimEdges)
            {
                const zp::Vector2D<float> edgeLine = edge.End - edge.Start;
                const zp::Vector2D<float> ballLine = ball.Pos - edge.Start;
                const float edgeLength = edgeLine.mag2();
                const float t = std::max(0.f, std::min(edgeLength, edgeLine.dot(ballLine))) / edgeLength;
    
                const zp::Vector2D<float> closestPoint = edge.Start + t * edgeLine;
                const float distance = ball.Pos.dist(closestPoint);
    
                if (distance <= ball.Radius + edge.Radius)
                {
                    // collision has ocurred
                    Ball* edgeBall = new Ball();
                    edgeBall->Radius = edge.Radius;
                    edgeBall->Mass = ball.Mass * edge.MassFactor;
                    edgeBall->Pos = closestPoint;
                    edgeBall->Vel = ball.Vel.negate();
    
                    edgeBalls.push_back(edgeBall);
                    collisions.push_back(std::make_pair(&ball, edgeBall));
    
                    // displace current ball away from collision
                    const float overlap = distance - ball.Radius - edgeBall->Radius;
                    ball.Pos -= overlap * (ball.Pos - edgeBall->Pos) / distance;
                }
            }
    
            // time displacement
            const float intendedSpeed = ball.Vel.mag();
            const float intendedDistance = intendedSpeed * ball.SimTimeRemaining;
            const float currentDistance = ball.Pos.dist(ball.Old);
            const float currentTime = currentDistance / intendedSpeed;
    
            ball.SimTimeRemaining = ball.SimTimeRemaining - currentTime;
        }
    
        // now work out dynamic collisions
        for (const auto& collided : collisions)
        {
            Ball* b1 = collided.first;
            Ball* b2 = collided.second;
            // distance between balls
            const float distance = b1->Pos.dist(b2->Pos);
            // nomal
            const zp::Vector2D<float> normal = (b2->Pos - b1->Pos) / distance;
            // tangent
            const zp::Vector2D<float> tangent = normal.perp();
            // dot products
            const float dpTan1 = b1->Vel.dot(tangent);
            const float dpTan2 = b2->Vel.dot(tangent);
            const float dpNorm1 = b1->Vel.dot(normal);
            const float dpNorm2 = b2->Vel.dot(normal);
            // conversation of momentum in 1D
            const float m1 = (dpNorm1 * (b1->Mass - b2->Mass) + 2.0f * b2->Mass * dpNorm2) / (b1->Mass + b2->Mass);
            const float m2 = (dpNorm2 * (b2->Mass - b1->Mass) + 2.0f * b1->Mass * dpNorm1) / (b1->Mass + b2->Mass);
            // update ball velocities
            b1->Vel = tangent * dpTan1 + normal * m1;
            b2->Vel = tangent * dpTan2 + normal * m2;
        }
    
        // remove edge balls
        for (auto& edgeBall : edgeBalls) delete edgeBall;
    }
    
    void CirclePhysics::updateSimulation(const float elapsedTime)
    {
        Sim.ElapsedTime = elapsedTime / static_cast<float>(Sim.Updates);
    
        for (auto i = 0; i < Sim.Updates; ++i)
        {
            // set all balls time to maximum for this epoch
            for (auto& ball : SimBalls)
                ball.SimTimeRemaining = Sim.ElapsedTime;
    
            updateBallPositions();
            updateBallCollisions();
        }
    }
    
    bool CirclePhysics::onUserUpdate(const float elapsedTime)
    {
        updateSimulation(elapsedTime);
        drawEdges();
        drawBalls();
        return true;
    }
    
    void CirclePhysics::drawEdges()
    {
        for (const auto& edge : SimEdges)
        {
            drawFillCircle(edge.Start, edge.Radius, edge.Color);
            drawFillCircle(edge.End, edge.Radius, edge.Color);
    
            float nx = -(edge.End.y - edge.Start.y);
            float ny = (edge.End.x - edge.Start.x);
            const float d = std::sqrtf(nx * nx + ny * ny);
            nx /= d;
            ny /= d;
    
            zp::Vector2D<float> start(edge.Start.x + nx * edge.Radius, edge.Start.y + ny * edge.Radius);
            zp::Vector2D<float> end(edge.End.x + nx * edge.Radius, edge.End.y + ny * edge.Radius);
            drawLine(start, end, edge.Color);
            start = zp::Vector2D<float>(edge.Start.x - nx * edge.Radius, edge.Start.y - ny * edge.Radius);
            end = zp::Vector2D<float>(edge.End.x - nx * edge.Radius, edge.End.y - ny * edge.Radius);
            drawLine(start, end, edge.Color);
    
            drawTargetZone(edge);
        }
    }
    
    void CirclePhysics::drawBalls()
    {
        // draw balls
        for (const auto& ball : SimBalls)
            drawBall(ball);
    
        // draw shoot line
        if (SelectedBall != nullptr)
            drawLine(SelectedBall->Pos, screen().mCursor().fpos(), zp::ColorDot::BLUE());
    }
    
    void CirclePhysics::drawBall(const Ball& ball)
    {
        drawCircle(ball.Pos, ball.Radius, ball.Color);
    
        // draw velocity direction
        if (ball.Vel.empty())
            drawPoint(ball.Pos, zp::ColorDot::YELLOW());
        else
            drawLine(ball.Pos, ball.Pos + ball.Vel.norm() * ball.Radius, zp::ColorDot::YELLOW());
    }
    
    void CirclePhysics::drawTargetZone(const Edge& edge)
    {
        const zp::Vector2D<float> mousePos = screen().mCursor().fpos();
    
        if (zp::utils::isPointInsideCircle(edge.Start, edge.Radius, mousePos))
            drawCircle(mousePos, edge.HitRadius, zp::ColorDot::RED());
    
        if (zp::utils::isPointInsideCircle(edge.End, edge.Radius, mousePos))
            drawCircle(mousePos, edge.HitRadius, zp::ColorDot::RED());
    }
    
    bool CirclePhysics::handleUserInput(const float elapsedTime)
    {
        // reset simulation
        if (getKey(VK_BACK).pressed)
        {
            SimBalls.clear();
            resetScreen();
            createBalls();
        }
    
        const zp::Vector2D<float> mousePos = screen().mCursor().fpos();
    
        // click to select object
        if (getMouse(zp::M_LEFT).pressed || getMouse(zp::M_RIGHT).pressed)
        {
            // select ball
            SelectedBall = nullptr;
            for (auto& ball : SimBalls)
            {
                if (zp::utils::isPointInsideCircle(ball.Pos, ball.Radius, mousePos))
                {
                    SelectedBall = &ball;
                    break;
                }
            }
    
            // select edge
            SelectedEdge = nullptr;
            for (auto& edge : SimEdges)
            {
                if (zp::utils::isPointInsideCircle(edge.Start, edge.HitRadius, mousePos))
                {
                    SelectedEdge = &edge;
                    IsSelectedStartEdge = true;
                    break;
                }
    
                if (zp::utils::isPointInsideCircle(edge.End, edge.HitRadius, mousePos))
                {
                    SelectedEdge = &edge;
                    IsSelectedStartEdge = false;
                    break;
                }
            }
        }
    
        // move object
        if (getMouse(zp::M_LEFT).held)
        {
            if (SelectedBall != nullptr)
            {
                SelectedBall->Pos = mousePos;
            }
    
            if (SelectedEdge != nullptr)
            {
                if (IsSelectedStartEdge)
                    SelectedEdge->Start = mousePos;
                else
                    SelectedEdge->End = mousePos;
            }
        }
    
        // release object
        if (getMouse(zp::M_LEFT).released)
        {
            SelectedBall = nullptr;
            SelectedEdge = nullptr;
        }
    
        // shoot ball
        if (getMouse(zp::M_RIGHT).released)
        {
            if (SelectedBall != nullptr)
            {
                // apply velocity
                constexpr auto velScale = 0.3f;
                SelectedBall->Vel = (SelectedBall->Pos - mousePos) * velScale;
            }
            SelectedBall = nullptr;
        }
    
        // switch gravity
        if (getKey(VK_SPACE).pressed)
            Sim.IsGrav = !Sim.IsGrav;
    
    
        // testing change dot size on fly
        if (getKey(VK_DOWN).pressed)
        {
            if (!changeResolution(consoleSize().pixel.width, consoleSize().pixel.height + 1))
                return false;
        }
    
        if (getKey(VK_UP).pressed)
        {
            if (!changeResolution(consoleSize().pixel.width, consoleSize().pixel.height - 1))
                return false;
        }
    
        if (getKey(VK_RIGHT).pressed)
        {
            if (!changeResolution(consoleSize().pixel.width + 1, consoleSize().pixel.height))
                return false;
        }
    
        if (getKey(VK_LEFT).pressed)
        {
            if (!changeResolution(consoleSize().pixel.width - 1, consoleSize().pixel.height))
                return false;
        }
       
        return true;
    }
    
    
    
    


  • Sorry, wenn ich das jetzt einfach so hinschmeisse. Bedürfnis mitzuteilen und so... 😉

    PS: Mindestens sollte ich zu Smart Pointer wechseln...



  • @zeropage sagte in Circle Physics Example (Code):

    Mit ein wenig Mathematik zur Verfügung ist das gar nicht viel Code.

    Eine durchweg interresante Idee, keine Frage.

    Aber wenig Mathematik glaube ich so nicht. Man hat es ja mit Simlation von Flüssigkeiten, Oberflächenspannungen, Luftdrücken, Adhäsion,... zu tun.



  • @Quiche-Lorraine sagte in Circle Physics Example (Code):

    @zeropage sagte in Circle Physics Example (Code):

    Mit ein wenig Mathematik zur Verfügung ist das gar nicht viel Code. 
    

    Eine durchweg interresante Idee, keine Frage.

    In der Computergrafik werden Wassereffekte mit frei fließendem Wasser (im Gegensatz zu einer Wasseroberfläche z.B. eines Sees, die man mit einem 2D-Grid simulieren kann) durchaus mit solchen Kugeln/Partikeln gemacht, die dann als Grundlage für die Berechnung der eigentlichen Wasseroberfläche dienen.

    Aber wenig Mathematik glaube ich so nicht. Man hat es ja mit Simlation von Flüssigkeiten, Oberflächenspannungen, Luftdrücken, Adhäsion,... zu tun.

    Diese anderen Eigenschaften von Wasser simuliert man dann durch Interaktion zwischen den Partikeln. Die Oberflächenspannung z.B. durch Simulation eines Federmechanismus, der benachbarte Partikeln mehr oder weniger stark zusammenhält, mit dessen Parametern sich dann auch die Viskosität der Flüssigkeit steuern lässt.

    Hier mal so ein Beispiel, wie das dann aussehen kann: https://www.youtube.com/watch?v=DhNt_A3k4B4

    Der nächste Schritt wäre dann ein geeignetes Triangulationsverfahren, mit dem man dann aus den äußeren Partikeln der Simulation die eigentliche Wasseroberfläche generiert.

    Das sind natürlich keine Simulationen, die den Anspruch physikalischer Korrektheit haben, aber für visuelle Effekte ist das durchaus ausreichend 😉

    Interessant an dem Ansatz finde ich, dass man den gut stufenweise umsetzen kann. Die erste Simulation der "kullernden Kugeln" ist mathematisch tatsächlich nicht so kompliziert. Das kommt erst, wenn man die Interaktion der Partikel physikalisch modelliert. Insofern kann man da schon früh "was sehen" und das dann sukzessive verbessern. Das finde ich deutlich motivierender, als direkt mit einer vollen Wassersimulation einzusteigen.

    Hier noch ein anderes Beispiel, auch partikelbasiert, was man aber nicht so direkt erkennt, da dort bereits die Wasseroberfläche generiert wurde: https://www.youtube.com/watch?v=3Y_QTY4C08I


Anmelden zum Antworten