Physics Simulation & Visualization Tool 0.1
A C++ physics simulation engine with real-time 3D visualization
Loading...
Searching...
No Matches
OpenGLWindow.cpp
Go to the documentation of this file.
1#include "ui/OpenGLWindow.h"
2#include <QMouseEvent>
3#include <QElapsedTimer>
4#include <QRect>
8
12#include "ui/AppSettings.h"
14
15#include <algorithm>
16#include <cmath>
17#include <limits>
18#include <vector>
19#include <glm/gtc/matrix_transform.hpp>
20
21namespace {
22constexpr float kDefaultGlLineWidth = 10.0f;
23constexpr double kFpsUpdateIntervalSeconds = 0.1;
24constexpr int kWheelDegreesPerStep = 15;
25constexpr int kWheelDeltaDivisor = 8;
26constexpr int kLabelGridCellPx = 32;
27constexpr float kClipWMin = 0.000001f;
28constexpr float kOffscreenDirectionEpsilonSq = 1.0e-6f;
29constexpr float kEdgeClampX = 0.98f;
30constexpr float kEdgeClampY = 0.95f;
31constexpr int kLabelPaddingPx = 2;
32constexpr int kLabelOffsetAboveObjectPx = 8;
33constexpr int kLabelStepYPx = 4;
34constexpr int kLabelMinStepXPx = 36;
35constexpr int kLabelSearchRings = 12;
36}
37
38OpenGLWindow::OpenGLWindow(QWidget* parent) : QOpenGLWidget(parent) {}
39
41 initializeOpenGLFunctions();
42 ResourceManager::initialize(this); // inherits from QOpenGLFunctions so can be cast to it
43 glEnable(GL_DEPTH_TEST);
44 glClipControl(GL_LOWER_LEFT, GL_ZERO_TO_ONE);
45 glClearDepth(0.0);
46 glDepthFunc(GL_GREATER);
47
48 // glEnable(GL_DEBUG_OUTPUT);
49 // glDebugMessageCallback([](GLenum source, GLenum type, GLuint id, GLenum severity,
50 // GLsizei length, const GLchar* message, const void* userParam) {
51 // std::cerr << "OpenGL DEBUG: " << message << std::endl;
52 // }, nullptr);
53
54 glLineWidth(kDefaultGlLineWidth);
55
56 lastFrame = std::chrono::steady_clock::now();
57 emit glInitialized();
58}
59
60void OpenGLWindow::resizeGL(int w, int h) {
61 // TODO: camera should be in SceneManager
62 glViewport(0, 0, w, h);
63 scene->getCamera()->setAspectRatio(static_cast<float>(w) / h);
64}
65
67 auto currentFrame = std::chrono::steady_clock::now();
68 std::chrono::duration<double> deltaDuration = currentFrame - lastFrame;
69 double deltaTime = deltaDuration.count();
70 lastFrame = currentFrame;
71
72 if (simulating) {
73 renderSimTime += deltaTime * simSpeed;
74 }
75
76 //sceneManager->stepPhysics(deltaTime);
77 // 1) Acquire the latest batch of snapshots
78 auto snaps = sceneManager->physicsSystem->fetchLatestSnapshot(renderSimTime);
79
80 sceneManager->processHeldKeys(pressedKeys, deltaTime);
81
82 Math::Ray ray = getMouseRay();
83 sceneManager->updateHoverState(ray);
84 scene->draw(snaps, sceneManager->hoveredIDs, sceneManager->selectedIDs);
85 updateObjectLabels();
86
87 calculateFPS();
88
89 update();
90}
91
92Math::Ray OpenGLWindow::getMouseRay() {
93 QPointF mousePos = getMousePos();
94 QSize fbSize = getFramebufferSize();
95
96 return {
97 scene->getCamera()->position,
99 mousePos.x(), mousePos.y(),
100 fbSize.width(), fbSize.height(),
101 scene->getCamera()->getViewMatrix(), scene->getCamera()->getProjMatrix())
102 };
103}
104
105
106void OpenGLWindow::calculateFPS() {
107 static int frameCount = 0;
108 static double fps = 0.0f;
109 static std::chrono::steady_clock::time_point lastFpsUpdate = lastFrame;
110
111 frameCount++;
112
113 std::chrono::duration<double> fpsDuration = lastFrame - lastFpsUpdate;
114 if (fpsDuration.count() >= kFpsUpdateIntervalSeconds) {
115 fps = frameCount / fpsDuration.count();
116 frameCount = 0;
117 lastFpsUpdate = lastFrame;
118 emit fpsUpdated(fps);
119 }
120}
121
122
123void OpenGLWindow::keyPressEvent(QKeyEvent* event) {
124 pressedKeys.insert(event->key());
125}
126
127void OpenGLWindow::keyReleaseEvent(QKeyEvent* event) {
128 pressedKeys.remove(event->key());
129}
130
131void OpenGLWindow::mousePressEvent(QMouseEvent* event) {
132 pressedMouseButtons.insert(event->button());
133 setFocus();
134 if (sceneManager)
135 sceneManager->handleMouseButton(event->button(), event->type(), event->modifiers());
136 update();
137}
138
139void OpenGLWindow::mouseReleaseEvent(QMouseEvent* event) {
140 pressedMouseButtons.remove(event->button());
141
142 if (sceneManager)
143 sceneManager->handleMouseButton(event->button(), QEvent::MouseButtonRelease, event->modifiers());
144 update();
145}
146
147void OpenGLWindow::wheelEvent(QWheelEvent* event) {
148 if (!scene) return;
149
150 const QPoint numDegrees = event->angleDelta() / kWheelDeltaDivisor;
151 const float wheelSteps = static_cast<float>(numDegrees.y()) / static_cast<float>(kWheelDegreesPerStep);
152 scene->getCamera()->processScroll(wheelSteps);
153 event->accept();
154 update();
155}
156
158 mouseCaptured = captured;
159 if (captured) {
160 mouseLastPosBeforeCapture = QCursor::pos();
161 setCursor(Qt::BlankCursor);
162 } else {
163 setCursor(Qt::ArrowCursor);
164 QCursor::setPos(mouseLastPosBeforeCapture);
165 }
166}
167
169 if (mouseCaptured) {
170 QCursor::setPos(mouseLastPosBeforeCapture);
171 scene->getCamera()->processMouseMovement(dx, -dy);
172 }
173}
174
175void OpenGLWindow::hideObjectLabels() {
176 for (QPushButton* button : objectLabelButtons) {
177 if (button) button->hide();
178 }
179}
180
181void OpenGLWindow::updateObjectLabels() {
182 if (!scene || !sceneManager) {
183 hideObjectLabels();
184 return;
185 }
186
188 if (!dbg.showObjectLabels) {
189 hideObjectLabels();
190 return;
191 }
192
193 const auto& objects = sceneManager->getObjects();
194 while (objectLabelButtons.size() < objects.size()) {
195 auto* button = new QPushButton(this);
196 button->setFocusPolicy(Qt::NoFocus);
197 button->setCursor(Qt::PointingHandCursor);
198 connect(button, &QPushButton::clicked, this, [this, button]() {
199 if (!sceneManager) return;
200 const uint32_t objectID = button->property("objectID").toUInt();
201 if (SceneObject* obj = sceneManager->getObjectByID(objectID)) {
202 sceneManager->focusObject(obj);
203 setFocus();
204 }
205 });
206 objectLabelButtons.push_back(button);
207 }
208
209 const glm::vec3 renderOrigin = SceneObject::getRenderOrigin();
210 const bool useFloatingOrigin = std::max({std::abs(renderOrigin.x), std::abs(renderOrigin.y), std::abs(renderOrigin.z)}) > 0.0f;
211 const glm::mat4 view = useFloatingOrigin ? scene->getCamera()->getRenderViewMatrix() : scene->getCamera()->getViewMatrix();
212 const glm::mat4 proj = scene->getCamera()->getProjMatrix();
213 const float w = static_cast<float>(width());
214 const float h = static_cast<float>(height());
215
216 const int labelGridCols = std::max(1, (width() + kLabelGridCellPx - 1) / kLabelGridCellPx);
217 const int labelGridRows = std::max(1, (height() + kLabelGridCellPx - 1) / kLabelGridCellPx);
218 std::vector<unsigned char> occupiedLabelCells(static_cast<size_t>(labelGridCols * labelGridRows), 0);
219
220 auto visitLabelCells = [&](const QRect& rect, auto&& visitor) {
221 const int left = std::clamp(rect.left() / kLabelGridCellPx, 0, labelGridCols - 1);
222 const int right = std::clamp(rect.right() / kLabelGridCellPx, 0, labelGridCols - 1);
223 const int top = std::clamp(rect.top() / kLabelGridCellPx, 0, labelGridRows - 1);
224 const int bottom = std::clamp(rect.bottom() / kLabelGridCellPx, 0, labelGridRows - 1);
225
226 for (int yCell = top; yCell <= bottom; ++yCell) {
227 for (int xCell = left; xCell <= right; ++xCell) {
228 if (visitor(xCell, yCell))
229 return true;
230 }
231 }
232 return false;
233 };
234
235 auto overlapsOccupiedLabel = [&](const QRect& rect) {
236 const QRect padded = rect.adjusted(-kLabelPaddingPx, -kLabelPaddingPx, kLabelPaddingPx, kLabelPaddingPx);
237 return visitLabelCells(padded, [&](int xCell, int yCell) {
238 return occupiedLabelCells[static_cast<size_t>(yCell * labelGridCols + xCell)] != 0;
239 });
240 };
241
242 auto occupyLabel = [&](const QRect& rect) {
243 const QRect padded = rect.adjusted(-kLabelPaddingPx, -kLabelPaddingPx, kLabelPaddingPx, kLabelPaddingPx);
244 visitLabelCells(padded, [&](int xCell, int yCell) {
245 occupiedLabelCells[static_cast<size_t>(yCell * labelGridCols + xCell)] = 1;
246 return false;
247 });
248 };
249
250 for (size_t i = 0; i < objectLabelButtons.size(); ++i) {
251 QPushButton* button = objectLabelButtons[i];
252 if (i >= objects.size()) {
253 button->hide();
254 continue;
255 }
256
257 SceneObject* obj = objects[i].get();
258 const glm::vec3 objectPosition = glm::vec3(obj->getModelMatrix()[3]);
259 const glm::vec3 labelPosition = useFloatingOrigin ? objectPosition - renderOrigin : objectPosition;
260 glm::vec4 clip = proj * view * glm::vec4(labelPosition, 1.0f);
261 if (!std::isfinite(clip.x) || !std::isfinite(clip.y) || !std::isfinite(clip.z) || !std::isfinite(clip.w)) {
262 button->hide();
263 continue;
264 }
265
266 const bool behindCamera = clip.w < 0.0f;
267 if (behindCamera) {
268 clip.x = -clip.x;
269 clip.y = -clip.y;
270 clip.w = -clip.w;
271 }
272
273 glm::vec3 ndc = glm::vec3(clip) / std::max(clip.w, kClipWMin);
274 if (!std::isfinite(ndc.x) || !std::isfinite(ndc.y) || !std::isfinite(ndc.z)) {
275 button->hide();
276 continue;
277 }
278
279 const bool outsideNdc = ndc.x < -1.0f || ndc.x > 1.0f || ndc.y < -1.0f || ndc.y > 1.0f;
280 const bool offscreen = behindCamera || outsideNdc;
281 if (offscreen) {
282 const glm::vec3 toObject = objectPosition - scene->getCamera()->position;
283 const float cameraX = glm::dot(toObject, scene->getCamera()->right);
284 const float cameraY = glm::dot(toObject, scene->getCamera()->up);
285 const float cameraZ = glm::dot(toObject, scene->getCamera()->front);
286 glm::vec2 edgeDir(
287 cameraZ > kClipWMin ? cameraX / cameraZ : cameraX,
288 cameraZ > kClipWMin ? cameraY / cameraZ : cameraY
289 );
290 if (glm::dot(edgeDir, edgeDir) < kOffscreenDirectionEpsilonSq) {
291 edgeDir = glm::vec2(1.0f, 0.0f);
292 }
293
294 const float scaleX = edgeDir.x != 0.0f ? kEdgeClampX / std::abs(edgeDir.x) : std::numeric_limits<float>::infinity();
295 const float scaleY = edgeDir.y != 0.0f ? kEdgeClampY / std::abs(edgeDir.y) : std::numeric_limits<float>::infinity();
296 const float edgeScale = std::min(scaleX, scaleY);
297 ndc.x = edgeDir.x * edgeScale;
298 ndc.y = edgeDir.y * edgeScale;
299 } else {
300 ndc.x = std::clamp(ndc.x, -kEdgeClampX, kEdgeClampX);
301 ndc.y = std::clamp(ndc.y, -kEdgeClampY, kEdgeClampY);
302 }
303
304 bool metricsDirty = false;
305 QString labelText = QString::fromStdString(obj->getName());
306 if (offscreen) {
307 if (std::abs(ndc.x) > std::abs(ndc.y)) {
308 labelText = ndc.x < 0.0f
309 ? "← " + labelText
310 : labelText + " →";
311 } else {
312 labelText = ndc.y < 0.0f
313 ? labelText + " ↓"
314 : "↑ " + labelText;
315 }
316 }
317 if (button->text() != labelText) {
318 button->setText(labelText);
319 metricsDirty = true;
320 }
321 button->setProperty("objectID", obj->getObjectID());
322 const QVariant oldOffscreen = button->property("offscreen");
323 if (!oldOffscreen.isValid() || oldOffscreen.toBool() != offscreen) {
324 button->setProperty("offscreen", offscreen);
325 button->setStyleSheet(offscreen
326 ? "QPushButton {"
327 " background: rgba(20, 22, 24, 120);"
328 " border: 1px dashed rgba(20, 22, 24, 120);"
329 " color: rgba(210, 210, 210, 170);"
330 " padding: 1px 5px;"
331 " font-size: 12px;"
332 " border-radius: 2px;"
333 " min-height: 0px;"
334 "}"
335 "QPushButton:hover {"
336 " background: rgba(35, 38, 42, 170);"
337 " border-color: rgba(220, 220, 220, 180);"
338 " color: rgba(235, 235, 235, 220);"
339 "}"
340
341 : "QPushButton {"
342 " background: rgba(28, 32, 35, 190);"
343 " border: 1px solid rgba(115, 130, 135, 150);"
344 " color: rgba(235, 235, 235, 235);"
345 " padding: 1px 5px;"
346 " font-size: 12px;"
347 " border-radius: 2px;"
348 " min-height: 0px;"
349 "}"
350 "QPushButton:hover {"
351 " background: rgba(44, 50, 54, 225);"
352 " border-color: rgba(150, 170, 175, 190);"
353 "}"
354 );
355 metricsDirty = true;
356 }
357 const QVariant metricsReady = button->property("metricsReady");
358 if (metricsDirty || !metricsReady.isValid() || !metricsReady.toBool()) {
359 button->adjustSize();
360 button->setProperty("metricsReady", true);
361 }
362
363 const int maxX = std::max(0, width() - button->width());
364 const int maxY = std::max(0, height() - button->height());
365 const int baseX = std::clamp(static_cast<int>((ndc.x * 0.5f + 0.5f) * w) - button->width() / 2, 0, maxX);
366 const int baseY = std::clamp(static_cast<int>((1.0f - (ndc.y * 0.5f + 0.5f)) * h) - button->height() - kLabelOffsetAboveObjectPx, 0, maxY);
367 const int stepY = button->height() + kLabelStepYPx;
368 const int stepX = std::max(button->width() / 2, kLabelMinStepXPx);
369 int bestX = baseX;
370 int bestY = baseY;
371 int bestScore = std::numeric_limits<int>::max();
372
373 for (int ring = 0; ring <= kLabelSearchRings; ++ring) {
374 for (int dySign : {1, -1}) {
375 const int dy = ring == 0 ? 0 : dySign * ring * stepY;
376 for (int dxSign : {0, 1, -1}) {
377 const int dx = dxSign * ring * stepX;
378 const int candidateX = std::clamp(baseX + dx, 0, maxX);
379 const int candidateY = std::clamp(baseY + dy, 0, maxY);
380 const QRect candidate(candidateX, candidateY, button->width(), button->height());
381 if (overlapsOccupiedLabel(candidate)) continue;
382
383 const int score = (candidateX - baseX) * (candidateX - baseX) + (candidateY - baseY) * (candidateY - baseY);
384 if (score < bestScore) {
385 bestScore = score;
386 bestX = candidateX;
387 bestY = candidateY;
388 }
389 }
390 }
391 if (bestScore != std::numeric_limits<int>::max()) break;
392 }
393
394 const QRect placed(bestX, bestY, button->width(), button->height());
395 occupyLabel(placed);
396 button->move(bestX, bestY);
397 button->show();
398 button->raise();
399 }
400}
Interactive 3D manipulation gizmo for scene objects.
static AppSettings & getInstance()
Definition AppSettings.h:11
T & getGroup() const
Definition AppSettings.h:28
QSize getFramebufferSize() const
void mouseReleaseEvent(QMouseEvent *event) override
void resizeGL(int w, int h) override
void mousePressEvent(QMouseEvent *event) override
void keyReleaseEvent(QKeyEvent *event) override
void keyPressEvent(QKeyEvent *event) override
void setMouseCaptured(bool captured)
void fpsUpdated(double fps)
OpenGLWindow(QWidget *parent=nullptr)
QPointF getMousePos() const
void handleRawMouseDelta(int dx, int dy)
void glInitialized()
void paintGL() override
void initializeGL() override
void wheelEvent(QWheelEvent *event) override
static void initialize(QOpenGLFunctions_4_5_Core *funcs)
SceneObject * getObjectByID(uint32_t objectID) const
void processHeldKeys(const QSet< int > &heldKeys, float dt)
std::unordered_set< uint32_t > selectedIDs
void updateHoverState(const Math::Ray &mouseRay)
std::unique_ptr< Physics::PhysicsSystem > physicsSystem
void handleMouseButton(Qt::MouseButton button, QEvent::Type type, Qt::KeyboardModifiers mods)
std::unordered_set< uint32_t > hoveredIDs
const std::vector< std::unique_ptr< SceneObject > > & getObjects() const
glm::mat4 getModelMatrix() const override
Gets the model transformation matrix for this instance.
static glm::vec3 getRenderOrigin()
Definition SceneObject.h:64
const std::string & getName() const
Definition SceneObject.h:48
uint32_t getObjectID() const override
Gets the unique identifier for this object.
glm::vec3 screenToWorldRayDirection(double mouseX, double mouseY, int fbWidth, int fbHeight, const glm::mat4 &view, const glm::mat4 &projection)
Converts screen coordinates to a world-space ray direction.
Definition MathUtils.h:117
Represents a ray in 3D space.
Definition Ray.h:11
Hash specialization for Vertex to enable use in unordered containers.
Definition Mesh.h:33