diff --git a/shaders/fragment.glsl b/shaders/fragment_pendula.glsl similarity index 100% rename from shaders/fragment.glsl rename to shaders/fragment_pendula.glsl diff --git a/shaders/fragment_quad.glsl b/shaders/fragment_quad.glsl new file mode 100644 index 0000000..3d32963 --- /dev/null +++ b/shaders/fragment_quad.glsl @@ -0,0 +1,11 @@ +#version 330 core + +out vec4 FragColor; + +in vec2 texCoord; + +uniform sampler2D quadTexture; + +void main() { + FragColor = texture(quadTexture, texCoord); +} diff --git a/shaders/shaders.qrc b/shaders/shaders.qrc index 65aede6..eb51953 100644 --- a/shaders/shaders.qrc +++ b/shaders/shaders.qrc @@ -1,6 +1,8 @@ - vertex.glsl - fragment.glsl + vertex_pendula.glsl + fragment_pendula.glsl + vertex_quad.glsl + fragment_quad.glsl \ No newline at end of file diff --git a/shaders/vertex.glsl b/shaders/vertex_pendula.glsl similarity index 60% rename from shaders/vertex.glsl rename to shaders/vertex_pendula.glsl index 544fdaa..ab4b822 100644 --- a/shaders/vertex.glsl +++ b/shaders/vertex_pendula.glsl @@ -7,8 +7,8 @@ layout (location = 2) in float vMassRadius; uniform mat3 VP; uniform bool drawPoints; -uniform float screenSizePixels; -uniform float screenSizeMeters; +uniform int simulationSizePixels; +uniform float simulationSizeMeters; uniform float depthOffset; out vec3 color; @@ -16,8 +16,11 @@ out vec3 color; void main() { gl_Position = vec4(VP * vPos, 1.0); if (drawPoints){ + // Points always on top of neighbouring lines gl_Position.z -= depthOffset * 0.5; - gl_PointSize = vMassRadius / screenSizeMeters * screenSizePixels * 2; + + // PointSize is diameter in Pixels + gl_PointSize = vMassRadius / simulationSizeMeters * simulationSizePixels * 2; } else { color = vColor; } diff --git a/shaders/vertex_quad.glsl b/shaders/vertex_quad.glsl new file mode 100644 index 0000000..76d0754 --- /dev/null +++ b/shaders/vertex_quad.glsl @@ -0,0 +1,11 @@ +#version 330 core + +layout (location = 0) in vec2 vPos; +layout (location = 1) in vec2 vTex; + +out vec2 texCoord; + +void main() { + gl_Position = vec4(vPos, 0, 1); + texCoord = vTex; +} diff --git a/src/GLWidget.cpp b/src/GLWidget.cpp index dca7c64..e5adb9d 100644 --- a/src/GLWidget.cpp +++ b/src/GLWidget.cpp @@ -6,24 +6,25 @@ #include "Simulation.h" #include #include -#include "FPS.h" +#include "Overlay.h" #include +#include "FPS.h" GLWidget::GLWidget(Simulation * simulation) : simulation(simulation) { - startTimer(1000 / 144); - fps = new FPS; - fps->setUpdateInterval(500); + startTimer(1000 / 60); + overlay = new Overlay(simulation); connect(simulation, &Simulation::layoutChanged, this, &GLWidget::uploadStaticDataToGPU); + connect(simulation, &Simulation::layoutChanged, overlay, &Overlay::fetchEnergyLimit); connect(simulation, &Simulation::positionChanged, this, &GLWidget::changePosition); } void GLWidget::initializeGL() { initializeOpenGLFunctions(); - program = new QOpenGLShaderProgram; - program->addShaderFromSourceFile(QOpenGLShader::Vertex, ":/shaders/vertex.glsl"); - program->addShaderFromSourceFile(QOpenGLShader::Fragment, ":/shaders/fragment.glsl"); - program->link(); + pendulaProgram = new QOpenGLShaderProgram; + pendulaProgram->addShaderFromSourceFile(QOpenGLShader::Vertex, ":/shaders/vertex_pendula.glsl"); + pendulaProgram->addShaderFromSourceFile(QOpenGLShader::Fragment, ":/shaders/fragment_pendula.glsl"); + pendulaProgram->link(); glGenVertexArrays(1, &VAO); glGenBuffers(1, &positionVBO); @@ -35,57 +36,53 @@ void GLWidget::initializeGL() { glEnable(GL_PROGRAM_POINT_SIZE); glEnable(GL_PRIMITIVE_RESTART); glEnable(GL_PRIMITIVE_RESTART_FIXED_INDEX); - glEnable(GL_DEPTH_TEST); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glDepthFunc(GL_LESS); glClearDepth(1); glClearColor(.15, .15, .15, 1); - std::vector empty; - uploadStaticDataToGPU(&empty); + uploadStaticDataToGPU(); + + overlay->init(); } void GLWidget::paintGL() { - // Native OpenGL - { - - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - program->bind(); - program->setUniformValue("VP", VP); + // Content + { + glViewport(0, 0, width(), height()); + glEnable(GL_DEPTH_TEST); + glDisable(GL_BLEND); + pendulaProgram->bind(); + pendulaProgram->setUniformValue("VP", VP); glBindVertexArray(VAO); // Lines { - program->setUniformValue("drawPoints", false); + pendulaProgram->setUniformValue("drawPoints", false); glDrawElements(GL_LINE_STRIP, indexCount, GL_UNSIGNED_INT, nullptr); } // Mass Circles if (showMasses) { - program->setUniformValue("drawPoints", true); - program->setUniformValue("depthOffset", depthOffset); - program->setUniformValue("screenSizePixels", screenSizePixels * 0.9f); - program->setUniformValue("screenSizeMeters", float(simulation->sizeMeters)); + pendulaProgram->setUniformValue("drawPoints", true); + pendulaProgram->setUniformValue("depthOffset", depthOffset); + pendulaProgram->setUniformValue("simulationSizePixels", simulationSizePixels); + pendulaProgram->setUniformValue("simulationSizeMeters", float(simulation->sizeMeters)); glDrawElements(GL_POINTS, indexCount, GL_UNSIGNED_INT, nullptr); } glBindVertexArray(0); } - QPixmap img(size()); - auto p = new QPainter(&img); - // FPS + // Overlay { - p->setPen(Qt::white); - auto font = p->font(); - font.setPixelSize(20); - p->setFont(font); - fps->newFrame(); - QString fpsString = "FPS: " + QString::fromStdString(std::to_string(fps->current)); - p->drawText(0, 0, 100, 100, Qt::AlignTop | Qt::AlignLeft, fpsString); + glDisable(GL_DEPTH_TEST); + glEnable(GL_BLEND); + overlay->draw(); } - p->end(); } void GLWidget::timerEvent(QTimerEvent *e) { @@ -101,7 +98,8 @@ bool GLWidget::AnyDialogOpen() { return false; } -void GLWidget::uploadStaticDataToGPU(const std::vector *pendula) { +void GLWidget::uploadStaticDataToGPU() { + auto pendula = &simulation->pendula; int pointCount = std::transform_reduce(pendula->begin(), pendula->end(), 0, [](int prev, int curr){ return prev + curr + 1; @@ -185,11 +183,11 @@ void GLWidget::uploadStaticDataToGPU(const std::vector *pendula) { simulation->pendulaMutex.unlock(); } -void GLWidget::changePosition(const std::vector *pendula) { +void GLWidget::changePosition() { glBindBuffer(GL_ARRAY_BUFFER, positionVBO); auto positions = (GLfloat*) glMapBufferRange(GL_ARRAY_BUFFER, 0, GLsizeiptr(positionCount * sizeof(float)), GL_MAP_WRITE_BIT); size_t index = 0; - for (const auto p : *pendula){ + for (const auto p : simulation->pendula){ index += 3; for (auto x : p->X){ positions[index++] = float(x.x); @@ -202,16 +200,15 @@ void GLWidget::changePosition(const std::vector *pendula) { } void GLWidget::resizeGL(int w, int h) { - screenSizePixels = std::min(float(w), float(h)); - float scale = screenSizePixels / float(simulation->sizeMeters) * 2 * 0.9f; + simulationSizePixels = std::min(w, h) - 100; + float scale = float(simulationSizePixels) / float(simulation->sizeMeters) * 2; VP.setToIdentity(); - VP(0, 0) = 1 / float(w); - VP(1, 1) = -1 / float(h); - VP *= scale; - VP(2, 2) = 1; + VP(0, 0) = scale / float(w); + VP(1, 1) = scale / float(h); - glViewport(0, 0, w, h); + int s = simulationSizePixels; + overlay->resize(QSize(w, h), QRect((w - s) / 2, (h - s) / 2, s, s)); } void GLWidget::showMassesChanged(int state) { diff --git a/src/GLWidget.h b/src/GLWidget.h index a440bb0..7b5da54 100644 --- a/src/GLWidget.h +++ b/src/GLWidget.h @@ -6,8 +6,8 @@ class Pendulum; class Simulation; -class FPS; class QOpenGLShaderProgram; +class Overlay; class GLWidget : public QOpenGLWidget, protected QOpenGLExtraFunctions { public: @@ -20,10 +20,10 @@ protected: public slots: void showMassesChanged(int state); private slots: - void uploadStaticDataToGPU(const std::vector *pendula); - void changePosition(const std::vector *pendula); + void uploadStaticDataToGPU(); + void changePosition(); private: - QOpenGLShaderProgram * program; + QOpenGLShaderProgram * pendulaProgram; GLuint VAO; GLuint positionVBO; GLuint colorVBO; @@ -32,11 +32,11 @@ private: GLsizei indexCount = 0; GLsizei positionCount = 0; QMatrix3x3 VP; - float screenSizePixels; + int simulationSizePixels; float depthOffset; bool showMasses; Simulation * simulation; - FPS * fps; + Overlay * overlay; static bool AnyDialogOpen(); }; \ No newline at end of file diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 30480fc..5db725b 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -30,6 +30,8 @@ MainWindow::MainWindow() { resetLengths(); buildUI(); + + normalizeLengths(); } void MainWindow::buildUI() { diff --git a/src/Overlay.cpp b/src/Overlay.cpp new file mode 100644 index 0000000..5db2bb6 --- /dev/null +++ b/src/Overlay.cpp @@ -0,0 +1,177 @@ +#include "Overlay.h" +#include "FPS.h" +#include +#include +#include "Simulation.h" +#include +#include +#include +#include + +Overlay::Overlay(Simulation * simulation) : simulation(simulation) { + fps = new FPS; + fps->setUpdateInterval(500); +} + +void Overlay::init() { + initializeOpenGLFunctions(); + + program = new QOpenGLShaderProgram; + program->addShaderFromSourceFile(QOpenGLShader::Vertex, ":/shaders/vertex_quad.glsl"); + program->addShaderFromSourceFile(QOpenGLShader::Fragment, ":/shaders/fragment_quad.glsl"); + program->link(); + + float quadVertices[] = { + -1, 1, 0, 1, // top left + -1, -1, 0, 0, // bottom left + 1, -1, 1, 0, // bottom right + 1, 1, 1, 1 // top right + }; + + GLuint indices[] = { + 0, 1, 2, 2, 3, 0 + }; + + glGenVertexArrays(1, &VAO); + glBindVertexArray(VAO); + + GLuint VBO; + glGenBuffers(1, &VBO); + glBindBuffer(GL_ARRAY_BUFFER, VBO); + glBufferData(GL_ARRAY_BUFFER, sizeof(quadVertices), quadVertices, GL_STATIC_DRAW); + + GLuint EBO; + glGenBuffers(1, &EBO); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); + + glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), nullptr); + glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (GLvoid*) (2 * sizeof(float))); + glEnableVertexAttribArray(0); + glEnableVertexAttribArray(1); + + glBindVertexArray(0); +} + +void Overlay::draw() { + int w = viewportSize.width(); + int h = (viewportSize.height() - simulationRect.height()) / 2; + + // Top HUD with Energy Plot + { + glViewport(0, viewportSize.height() - h, w, h); + + QImage hud(w, h, QImage::Format_RGBA8888); + hud.fill(Qt::transparent); + + QPainter p(&hud); + p.setRenderHints(QPainter::Antialiasing); + p.setPen(Qt::white); + auto font = p.font(); + font.setPixelSize(15); + p.setFont(font); + + auto m = p.fontMetrics(); + + double pot = simulation->potentialEnergy; + double kin = simulation->kineticEnergy; + double total = pot + kin; + + QColor empty(50, 50, 50); + QColor full(80, 80, 80); + + // Total energy bar + { + double lossPercentage = (1 - total / energyLimit) * 100; + + std::stringstream text; + text << std::fixed << std::setprecision(0) << "Total Energy: " << total << " J"; + text << " --- "; + text << "Loss: " << std::setprecision(1) << lossPercentage << " %"; + QString totalText = QString::fromStdString(text.str()); + + int x = int(total / energyLimit * w); + + p.fillRect(0, 0, w, h / 2, empty); + p.fillRect(0, 0, x, h / 2, full); + p.drawLine(x, 0, x, h / 2); + p.drawText(0, 0, w, h / 2, Qt::AlignCenter | Qt::AlignVCenter, totalText); + } + + // Split energy bar + { + int potX = total == 0 ? int(0.5 * w) : int(pot / total * w); + p.fillRect(0, h / 2, w, h / 2, empty); + p.fillRect(0, h / 2, potX, h / 2, full); + + std::stringstream text; + text << std::fixed << std::setprecision(0) << "Potential: " << pot << " J"; + QString potText = QString::fromStdString(text.str()); + + text = std::stringstream(); + text << std::fixed << std::setprecision(0) << "Kinetic: " << kin << " J"; + QString kinText = QString::fromStdString(text.str()); + + int textWidth1 = m.horizontalAdvance(potText); + int textWidth2 = m.horizontalAdvance(kinText); + int x1 = (w - textWidth1) / 4; + int x2 = (w - textWidth2) / 4 * 3; + + p.drawLine(potX, h / 2, potX, h); + p.drawText(x1, h / 2, textWidth1, h / 2, Qt::AlignCenter | Qt::AlignVCenter, potText); + p.drawText(x2, h / 2, textWidth2, h / 2, Qt::AlignCenter | Qt::AlignVCenter, kinText); + } + + p.drawLine(0, 0, w, 0); + p.drawLine(0, h / 2, w, h / 2); + p.drawLine(0, h, w, h); + + drawTexture(&hud); + } + + // Bottom HUD with FPS and UPS + { + glViewport(0, 0, w, h); + + QImage hud(w, h, QImage::Format_RGBA8888); + + // Background + hud.fill(Qt::transparent); + + QPainter p(&hud); + p.setRenderHints(QPainter::Antialiasing); + + // FPS + { + p.setPen(Qt::white); + auto font = p.font(); + font.setPixelSize(15); + p.setFont(font); + fps->newFrame(); + std::stringstream text; + text << "FPS: " << fps->current << "\n"; + text << "UPS: " << simulation->ups->current; + p.drawText(0, 0, 100, h, Qt::AlignBottom | Qt::AlignLeft, QString::fromStdString(text.str())); + } + drawTexture(&hud); + } +} + +void Overlay::resize(QSize newViewportSize, QRect newSimulationRect) { + viewportSize = newViewportSize; + simulationRect = newSimulationRect; +} + +void Overlay::drawTexture(QImage *image) { + glBindVertexArray(VAO); + program->bind(); + + QOpenGLTexture texture(image->mirrored()); + texture.bind(); + + glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr); +} + +void Overlay::fetchEnergyLimit() { + energyLimit = simulation->kineticEnergy + simulation->potentialEnergy; +} diff --git a/src/Overlay.h b/src/Overlay.h new file mode 100644 index 0000000..0e4af91 --- /dev/null +++ b/src/Overlay.h @@ -0,0 +1,29 @@ +#include + +#include + +class QOpenGLWidget; +class FPS; +class QOpenGLShaderProgram; +class QOpenGLTexture; +class Simulation; + +class Overlay : public QObject, protected QOpenGLExtraFunctions { + Q_OBJECT + + FPS * fps; + QSize viewportSize; + QRect simulationRect; + QOpenGLShaderProgram * program; + GLuint VAO; + void drawTexture(QImage *image); + Simulation * simulation; + double energyLimit = 0; +public slots: + void fetchEnergyLimit(); +public: + explicit Overlay(Simulation *); + void init(); + void draw(); + void resize(QSize newViewportSize, QRect newSimulationRect); +}; \ No newline at end of file diff --git a/src/Pendulum.cpp b/src/Pendulum.cpp index 352b5e5..791b12f 100644 --- a/src/Pendulum.cpp +++ b/src/Pendulum.cpp @@ -7,9 +7,9 @@ Pendulum::Pendulum(const std::vector &M, const std::vector &L, QColor color, double startAngle) : M(M), L(L), color(color){ - startAngle *= 3.141 / 180; + startAngle *= M_PI / 180; - Vector direction(-sin(startAngle), cos(startAngle)); + Vector direction(-sin(startAngle), -cos(startAngle)); Vector currentPosition(0, 0); for (int i = 0; i < M.size(); i++){ @@ -31,7 +31,7 @@ void Pendulum::update(double h, double g) { for (int i = 0; i < X.size(); i++){ // explicit integration with gravity as external force - V[i] = V[i] + Vector(0, g * h); + V[i] = V[i] + Vector(0, -g * h); Vector p2 = X[i] + V[i] * h; // solve distance constraint @@ -64,3 +64,20 @@ void Pendulum::update(double h, double g) { } + +double Pendulum::potentialEnergy(double gravity) const { + double total = 0; + double zeroLevel = 0; + for (int i = 0; i < X.size(); i++){ + zeroLevel -= L[i]; + total += M[i] * gravity * (X[i].y - zeroLevel); + } + return total; +} + +double Pendulum::kineticEnergy() const { + double total = 0; + for (int i = 0; i < V.size(); i++) + total += M[i] * V[i].squaredLength(); + return total / 2; +} diff --git a/src/Pendulum.h b/src/Pendulum.h index fa4e657..faf0cd7 100644 --- a/src/Pendulum.h +++ b/src/Pendulum.h @@ -12,6 +12,8 @@ public: const std::vector &L, QColor color, double startAngle); void update(double, double); + double potentialEnergy(double gravity) const; + double kineticEnergy() const; private: friend class GLWidget; std::vector X, V; diff --git a/src/Simulation.cpp b/src/Simulation.cpp index 608190d..89c1619 100644 --- a/src/Simulation.cpp +++ b/src/Simulation.cpp @@ -4,36 +4,47 @@ #include #include #include +#include "FPS.h" Simulation::Simulation() { + ups = new FPS; + ups->setUpdateInterval(100); + timer = new QTimer(this); QTimer::connect(timer, &QTimer::timeout, this, &Simulation::update); timer->setInterval(updateInterval); timer->start(); - lastUpdate = system_clock::now(); }; void Simulation::update() { - auto thisUpdate = system_clock::now(); - auto ms = (int)duration_cast(thisUpdate - lastUpdate).count(); - lastUpdate = thisUpdate; - if (!isPlaying) return; - double h = (timescale * updateInterval) / 1000; + ups->newFrame(); - h /= substeps; + double h = (timescale * updateInterval) / 1000 / substeps; pendulaMutex.lock(); + double newPotentialEnergy = 0; + double newKineticEnergy = 0; + #pragma omp parallel for for (int i = 0; i < pendula.size(); i++){ // NOLINT(*-loop-convert) // not ranged based for msvc for (int k = 0; k < substeps; k++) pendula[i]->update(h, gravity); + double localPotentialEnergy = pendula[i]->potentialEnergy(gravity); + double localKineticEnergy = pendula[i]->kineticEnergy(); + #pragma omp atomic + newPotentialEnergy += localPotentialEnergy; + #pragma omp atomic + newKineticEnergy += localKineticEnergy; } - emit positionChanged(&pendula); + potentialEnergy = newPotentialEnergy; + kineticEnergy = newKineticEnergy; + + emit positionChanged(); } void Simulation::clearPendula() { @@ -47,7 +58,9 @@ void Simulation::clearPendula() { pendula.clear(); pendula.shrink_to_fit(); - emit layoutChanged(&pendula); + updateEnergy(); + + emit layoutChanged(); } void Simulation::addPendula(const std::vector &add) { @@ -56,6 +69,16 @@ void Simulation::addPendula(const std::vector &add) { for (auto p : add) pendula.push_back(p); - emit layoutChanged(&pendula); + updateEnergy(); + + emit layoutChanged(); +} + +void Simulation::updateEnergy() { + potentialEnergy = kineticEnergy = 0; + for (auto p : pendula){ + potentialEnergy += p->potentialEnergy(gravity); + kineticEnergy += p->kineticEnergy(); + } } diff --git a/src/Simulation.h b/src/Simulation.h index 2814cac..bea0365 100644 --- a/src/Simulation.h +++ b/src/Simulation.h @@ -8,6 +8,7 @@ using namespace std::chrono; class QTimer; +class FPS; class Simulation : public QObject { Q_OBJECT @@ -20,20 +21,24 @@ public: double timescale {}; int substeps {}; + double potentialEnergy = 0, kineticEnergy = 0; + bool isPlaying = false; + std::vector pendula; + std::mutex pendulaMutex; + FPS * ups; signals: - void layoutChanged(const std::vector *newPendula); - void positionChanged(const std::vector *changedPendula); + void layoutChanged(); + void positionChanged(); public slots: void clearPendula(); void addPendula(const std::vector &add); private slots: void update(); private: + void updateEnergy(); QTimer * timer; - int updateInterval = 17; - std::vector pendula; - time_point lastUpdate; + int updateInterval = 16; }; \ No newline at end of file diff --git a/src/Vector.cpp b/src/Vector.cpp index 9378f92..247ba06 100644 --- a/src/Vector.cpp +++ b/src/Vector.cpp @@ -29,3 +29,7 @@ Vector operator/(Vector v, double divisor){ return {v.x / divisor, v.y / divisor}; } +double Vector::squaredLength() const { + return x * x + y * y; +} + diff --git a/src/Vector.h b/src/Vector.h index 334f913..ecaeef4 100644 --- a/src/Vector.h +++ b/src/Vector.h @@ -3,6 +3,7 @@ struct Vector { Vector(double x, double y); double length() const; + double squaredLength() const; void normalize(); friend Vector operator +(Vector lhs, Vector rhs); diff --git a/src/main.cpp b/src/main.cpp index 501f7aa..b2a5eda 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -11,6 +11,7 @@ int main(int argc, char* argv[]) { QSurfaceFormat fmt; fmt.setSamples(4); fmt.setDepthBufferSize(32); + fmt.setSwapInterval(1); fmt.setVersion(3, 3); fmt.setProfile(QSurfaceFormat::CoreProfile); QSurfaceFormat::setDefaultFormat(fmt);