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