feat: implemented simulation v2 and glue
This commit is contained in:
parent
fc74361bb3
commit
5c67286175
43
src/Benchmarker.cpp
Normal file
43
src/Benchmarker.cpp
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
#include "Benchmarker.h"
|
||||
#include <SFML/System/Time.hpp>
|
||||
#include <print>
|
||||
|
||||
void BenchMark::bench() {
|
||||
this->clock.restart();
|
||||
}
|
||||
|
||||
double BenchMark::mark() {
|
||||
this->last = this->clock.getElapsedTime().asSeconds();
|
||||
if (this->count != 0) {
|
||||
double weight{1.0 / double(this->count + 1zu)};
|
||||
this->average = this->last * weight + this->average * (1.0 - weight);
|
||||
this->highest = std::max(this->last, this->highest);
|
||||
this->lowest = std::min(this->last, this->lowest);
|
||||
} else {
|
||||
this->average = this->highest = this->lowest = this->last;
|
||||
}
|
||||
this->count += 1;
|
||||
//std::println("MARK: {}", this->last);
|
||||
return this->last;
|
||||
}
|
||||
|
||||
void BenchMark::reset() {
|
||||
this->average = this->highest = this->lowest = this->last = 0.0;
|
||||
this->count = 0zu;
|
||||
}
|
||||
|
||||
double BenchMark::getLast() const {
|
||||
return this->last;
|
||||
}
|
||||
|
||||
double BenchMark::getLowest() const {
|
||||
return this->lowest;
|
||||
}
|
||||
|
||||
double BenchMark::getHighest() const {
|
||||
return this->highest;
|
||||
}
|
||||
|
||||
double BenchMark::getAverage() const {
|
||||
return this->average;
|
||||
}
|
||||
26
src/Benchmarker.h
Normal file
26
src/Benchmarker.h
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
#ifndef BENCHMARKER_H
|
||||
#define BENCHMARKER_H
|
||||
|
||||
#include <SFML/System/Clock.hpp>
|
||||
|
||||
class BenchMark {
|
||||
private:
|
||||
sf::Clock clock{};
|
||||
double last{0.0};
|
||||
double lowest{0.0};
|
||||
double highest{0.0};
|
||||
double average{0.0};
|
||||
size_t count{0};
|
||||
|
||||
public:
|
||||
void bench();
|
||||
double mark();
|
||||
|
||||
void reset();
|
||||
double getLast() const;
|
||||
double getLowest() const;
|
||||
double getHighest() const;
|
||||
double getAverage() const;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
|
@ -1,4 +1,7 @@
|
|||
#include "Balls.hpp"
|
||||
#include "Benchmarker.h"
|
||||
#include "defs.h"
|
||||
#include "simulation_v2.h"
|
||||
#include <SFML/Graphics.hpp>
|
||||
#include <SFML/Graphics/Rect.hpp>
|
||||
#include <SFML/Graphics/RectangleShape.hpp>
|
||||
|
|
@ -9,15 +12,18 @@
|
|||
#include <imgui.h>
|
||||
#include <print>
|
||||
|
||||
#include "Balls.hpp"
|
||||
static bool showV1{false};
|
||||
static bool showV2{true};
|
||||
static bool tick{true};
|
||||
namespace v1 {
|
||||
static BallGame sim;
|
||||
}
|
||||
namespace v2 {
|
||||
}
|
||||
|
||||
BenchMark v1BenchMark{};
|
||||
BenchMark v2BenchMark{};
|
||||
|
||||
void configure(AppConfig &config) {
|
||||
config.window_title = "CHANGEME";
|
||||
config.window_title = "Orbs!!!!";
|
||||
config.frame_rate_limit = std::nullopt;
|
||||
config.vsync = true;
|
||||
}
|
||||
|
|
@ -26,6 +32,7 @@ void setup() {
|
|||
ImGui::GetIO().ConfigFlags |= (ImGuiConfigFlags_NavEnableKeyboard | ImGuiConfigFlags_NavEnableGamepad | ImGuiConfigFlags_DockingEnable);
|
||||
sf::View view{get_window().getView()};
|
||||
set_render_view(view);
|
||||
v2::initialize(2500, {{50,50}, {1000, 1000}});
|
||||
}
|
||||
|
||||
void handle_input_event(sf::Event const &event) {
|
||||
|
|
@ -41,36 +48,42 @@ void handle_window_event(sf::Event const &event) {
|
|||
}
|
||||
|
||||
void loop(double delta) {
|
||||
v1::sim.updateBalls(get_window().getSize(), delta);
|
||||
if (showV1 && tick) {
|
||||
v1BenchMark.bench();
|
||||
v1::sim.updateBalls(get_window().getSize(), delta);
|
||||
v1BenchMark.mark();
|
||||
}
|
||||
if (showV2 && tick) {
|
||||
v2BenchMark.bench();
|
||||
v2::tick(delta);
|
||||
v2BenchMark.mark();
|
||||
}
|
||||
}
|
||||
|
||||
void draw_scene(sf::RenderTarget &target, sf::RenderStates const &states) {
|
||||
v1::sim.drawBalls(get_window());
|
||||
}
|
||||
|
||||
void draw_main_menu_bar() {
|
||||
if (ImGui::BeginMenu("Edit")) {
|
||||
if (ImGui::MenuItem("Menu item!")) {
|
||||
std::println("Wahooo!!!");
|
||||
}
|
||||
ImGui::EndMenu();
|
||||
if (showV1) {
|
||||
v1::sim.drawBalls(get_window());
|
||||
}
|
||||
if (showV2) {
|
||||
v2::draw(&get_window());
|
||||
}
|
||||
}
|
||||
|
||||
void draw_main_menu_bar() { }
|
||||
|
||||
void draw_gui() {
|
||||
// draw your GUI
|
||||
ImGui::DockSpaceOverViewport(0, NULL, ImGuiDockNodeFlags_PassthruCentralNode);
|
||||
if (ImGui::Begin("My Window")) {
|
||||
ImGui::Text("A window with text and a button!!");
|
||||
if (ImGui::Button("My Button")) {
|
||||
std::println("Yipeeee");
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
if (ImGui::Begin("Second Window :O")) {
|
||||
ImGui::Text("A window with text!");
|
||||
ImGui::End();
|
||||
if (ImGui::Begin("Tools")) {
|
||||
ImGui::Checkbox("Simulation V1 (reference implementation)", &showV1);
|
||||
ImGui::Text("frametime: %f", v1BenchMark.getLast());
|
||||
ImGui::Text("avg: %f", v1BenchMark.getAverage());
|
||||
ImGui::Checkbox("Simulation V2 (reimplementation)", &showV2);
|
||||
ImGui::Text("frametime: %f", v2BenchMark.getLast());
|
||||
ImGui::Text("avg: %f", v2BenchMark.getAverage());
|
||||
ImGui::Checkbox("Tick", &tick);
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
void shutdown() {
|
||||
|
|
|
|||
181
src/simulation_v2.cpp
Normal file
181
src/simulation_v2.cpp
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
#include "simulation_v2.h"
|
||||
#include "SFML/Graphics/RectangleShape.hpp"
|
||||
#include "SFML/Graphics/Text.hpp"
|
||||
#include "SFML/Graphics/Texture.hpp"
|
||||
#include "SFML/Graphics/Vertex.hpp"
|
||||
#include "SFML/Graphics/VertexArray.hpp"
|
||||
#include <SFML/Graphics/CircleShape.hpp>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <mutex>
|
||||
#include <print>
|
||||
#include <random>
|
||||
#include <ranges>
|
||||
#include <set>
|
||||
|
||||
#define HASH_BUCKETS 10
|
||||
#define HASH_GRID_DIVISIONS 2
|
||||
|
||||
namespace v2 {
|
||||
typedef uint8_t PointHash;
|
||||
|
||||
struct Particle {
|
||||
bool active{true};
|
||||
sf::CircleShape shape;
|
||||
sf::Vector2f velocity;
|
||||
sf::Vector2f solveMotion;
|
||||
sf::Vector2f acceleration;
|
||||
PointHash spatialHash;
|
||||
};
|
||||
|
||||
sf::FloatRect worldBounds{};
|
||||
std::array<std::set<Particle *>, HASH_BUCKETS> buckets{}; //!< Hash buckets; grid of 16x16 cells stretched over the bounding area
|
||||
std::vector<Particle> particles{};
|
||||
std::mutex mtx{};
|
||||
|
||||
// hashing strategy:
|
||||
// A 2 grid of rectangles divided evenly over the world's bounding box up to 16 * 16 buckets.
|
||||
// A point is hashed by converting from world space coordinates to floored grid coordinate.
|
||||
// To look it up, simply use the pointhash to index into the `buckets` array.
|
||||
PointHash hashPoint(sf::Vector2f vec, sf::FloatRect bounds) {
|
||||
// least- and most- significant four bits
|
||||
uint8_t ls4(uint8_t(std::floor(((vec.y - bounds.position.y) / bounds.size.y) * HASH_GRID_DIVISIONS)) & 0xF);
|
||||
uint8_t ms4(uint8_t(std::floor(((vec.x - bounds.position.x) / bounds.size.x) * HASH_GRID_DIVISIONS)) & 0xF);
|
||||
return (ls4 | (ms4 << 4u));
|
||||
}
|
||||
|
||||
sf::Vector2f particleTopLeft(Particle &particle) {
|
||||
return particle.shape.getPosition() - sf::Vector2f{particle.shape.getRadius(), particle.shape.getRadius()};
|
||||
}
|
||||
|
||||
PointHash hashParticlePosition(Particle &particle) {
|
||||
return hashPoint(particleTopLeft(particle), worldBounds);
|
||||
}
|
||||
|
||||
void updateParticleHash(Particle &particle) {
|
||||
PointHash previous{particle.spatialHash};
|
||||
particle.spatialHash = hashParticlePosition(particle);
|
||||
if (particle.spatialHash != previous) {
|
||||
buckets.at(previous % HASH_BUCKETS).erase(&particle);
|
||||
buckets.at(particle.spatialHash % HASH_BUCKETS).emplace(&particle);
|
||||
}
|
||||
}
|
||||
|
||||
void initialize(size_t particleCount, sf::FloatRect bounds) {
|
||||
worldBounds = bounds;
|
||||
|
||||
std::mt19937 rng{std::mt19937()};
|
||||
std::uniform_real_distribution<float> positionDist{std::uniform_real_distribution<float>(5.0f, 795.0f)};
|
||||
std::uniform_real_distribution<float> velocityDist{std::uniform_real_distribution<float>(-200.0f, 200.0f)};
|
||||
std::uniform_int_distribution<uint8_t> colorDist{std::uniform_int_distribution<uint8_t>(0, 255)};
|
||||
std::uniform_real_distribution<float> radiusDist{std::uniform_real_distribution<float>(2.5f, 2.5f)};
|
||||
particles.reserve(particleCount);
|
||||
for (size_t _ : std::ranges::views::iota(0zu, particleCount)) {
|
||||
particles.emplace_back(true,
|
||||
sf::CircleShape(radiusDist(rng)),
|
||||
sf::Vector2f{0, 0});
|
||||
Particle &particle{particles[particles.size() - 1]};
|
||||
particle.shape.setPosition({positionDist(rng), positionDist(rng)});
|
||||
particle.velocity = {velocityDist(rng), velocityDist(rng)};
|
||||
particle.spatialHash = hashParticlePosition(particle);
|
||||
particle.shape.setFillColor({colorDist(rng), colorDist(rng), colorDist(rng), 255});
|
||||
particle.shape.setOrigin({particle.shape.getRadius(), particle.shape.getRadius()});
|
||||
}
|
||||
}
|
||||
|
||||
sf::Vector2f reflect(sf::Vector2f x, sf::Vector2f normal, float bounce = 1.0) {
|
||||
float const dot{x.dot(normal)};
|
||||
if (dot > 0)
|
||||
return x;
|
||||
return x - (normal * -std::abs(dot) * (1 + bounce));
|
||||
}
|
||||
|
||||
void particleSolveOverlap(Particle &self, Particle const &other) {
|
||||
sf::Vector2f const difference{self.shape.getPosition() - other.shape.getPosition()};
|
||||
float const targetDistance{self.shape.getRadius() + other.shape.getRadius()};
|
||||
float const currentDistance{difference.length()};
|
||||
if (currentDistance < targetDistance && currentDistance > 0) {
|
||||
sf::Vector2f const collisionNormal{difference.normalized()};
|
||||
float const requiredMotion{targetDistance - currentDistance};
|
||||
self.solveMotion += requiredMotion * collisionNormal * 0.5f;
|
||||
self.acceleration += reflect(self.velocity, collisionNormal) - self.velocity;
|
||||
}
|
||||
}
|
||||
|
||||
void particleSolveOverlaps(Particle &self) {
|
||||
sf::Vector2f position{self.shape.getPosition()};
|
||||
sf::Vector2f offset{worldBounds.size.x / HASH_GRID_DIVISIONS, worldBounds.size.y / HASH_GRID_DIVISIONS};
|
||||
PointHash neighbours[4]{
|
||||
self.spatialHash,
|
||||
hashPoint(particleTopLeft(self) + sf::Vector2f{offset.x, 0}, worldBounds),
|
||||
hashPoint(particleTopLeft(self) + sf::Vector2f{offset.x, offset.y}, worldBounds),
|
||||
hashPoint(particleTopLeft(self) + sf::Vector2f{0, offset.y}, worldBounds)};
|
||||
for (PointHash neighbour : neighbours) {
|
||||
for (Particle *possible : buckets[neighbour % HASH_BUCKETS]) {
|
||||
if (possible == &self) {
|
||||
continue;
|
||||
} else {
|
||||
particleSolveOverlap(self, *possible);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (position.x - self.shape.getRadius() < worldBounds.position.x) {
|
||||
self.solveMotion.x += worldBounds.position.x - (position.x - self.shape.getRadius());
|
||||
self.acceleration += reflect(self.velocity, {1, 0}) - self.velocity;
|
||||
}
|
||||
if (position.x + self.shape.getRadius() > worldBounds.position.x + worldBounds.size.x) {
|
||||
self.solveMotion.x += (worldBounds.position.x + worldBounds.size.x) - (position.x + self.shape.getRadius());
|
||||
self.acceleration += reflect(self.velocity, {-1, 0}) - self.velocity;
|
||||
}
|
||||
if (position.y - self.shape.getRadius() < worldBounds.position.y) {
|
||||
self.solveMotion.y += worldBounds.position.y - (position.y - self.shape.getRadius());
|
||||
self.acceleration += reflect(self.velocity, {0, 1}) - self.velocity;
|
||||
}
|
||||
if (position.y + self.shape.getRadius() > worldBounds.position.y + worldBounds.size.y) {
|
||||
self.solveMotion.y += (worldBounds.position.y + worldBounds.size.y) - (position.y + self.shape.getRadius());
|
||||
self.acceleration += reflect(self.velocity, {0, -1}) - self.velocity;
|
||||
}
|
||||
}
|
||||
|
||||
void tick(double delta) {
|
||||
for (Particle &particle : particles) {
|
||||
if (!particle.active) {
|
||||
return;
|
||||
}
|
||||
particleSolveOverlaps(particle);
|
||||
}
|
||||
|
||||
for (Particle &particle : particles) {
|
||||
if (particle.active) {
|
||||
// apply velocity and combined solve motion
|
||||
particle.shape.move(particle.solveMotion);
|
||||
particle.shape.move(particle.velocity * float(delta));
|
||||
particle.velocity += particle.acceleration;
|
||||
particle.acceleration = particle.solveMotion = {0, 0}; // reset solve motion and acceleration
|
||||
updateParticleHash(particle); // update spatial hash
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void draw(sf::RenderTarget *target) {
|
||||
sf::RectangleShape rect{worldBounds.size};
|
||||
rect.setPosition(worldBounds.position);
|
||||
rect.setFillColor(sf::Color::Transparent);
|
||||
rect.setOutlineColor(sf::Color::White);
|
||||
rect.setOutlineThickness(3);
|
||||
target->draw(rect);
|
||||
sf::Vertex polygon[2];
|
||||
for (Particle &particle : particles) {
|
||||
if (particle.active) {
|
||||
target->draw(particle.shape);
|
||||
#if 0
|
||||
polygon[0].position = particle.shape.getPosition();
|
||||
polygon[1].position = particle.shape.getPosition() + particle.velocity;
|
||||
target->draw(polygon, 2, sf::PrimitiveType::Lines);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace v2
|
||||
11
src/simulation_v2.h
Normal file
11
src/simulation_v2.h
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
#pragma once
|
||||
|
||||
#include "SFML/Graphics/Rect.hpp"
|
||||
#include <SFML/Graphics.hpp>
|
||||
#include <SFML/System/Vector2.hpp>
|
||||
|
||||
namespace v2 {
|
||||
extern void initialize(size_t particleCount, sf::FloatRect bounds);
|
||||
extern void tick(double delta);
|
||||
extern void draw(sf::RenderTarget *window);
|
||||
} // namespace v2
|
||||
Loading…
Reference in a new issue