3#include <QElapsedTimer>
19#include <glm/gtc/matrix_transform.hpp>
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;
41 initializeOpenGLFunctions();
43 glEnable(GL_DEPTH_TEST);
44 glClipControl(GL_LOWER_LEFT, GL_ZERO_TO_ONE);
46 glDepthFunc(GL_GREATER);
54 glLineWidth(kDefaultGlLineWidth);
56 lastFrame = std::chrono::steady_clock::now();
62 glViewport(0, 0, w, h);
63 scene->getCamera()->setAspectRatio(
static_cast<float>(w) / h);
67 auto currentFrame = std::chrono::steady_clock::now();
68 std::chrono::duration<double> deltaDuration = currentFrame - lastFrame;
69 double deltaTime = deltaDuration.count();
70 lastFrame = currentFrame;
73 renderSimTime += deltaTime * simSpeed;
78 auto snaps = sceneManager->
physicsSystem->fetchLatestSnapshot(renderSimTime);
97 scene->getCamera()->position,
99 mousePos.x(), mousePos.y(),
100 fbSize.width(), fbSize.height(),
101 scene->getCamera()->getViewMatrix(), scene->getCamera()->getProjMatrix())
106void OpenGLWindow::calculateFPS() {
107 static int frameCount = 0;
108 static double fps = 0.0f;
109 static std::chrono::steady_clock::time_point lastFpsUpdate = lastFrame;
113 std::chrono::duration<double> fpsDuration = lastFrame - lastFpsUpdate;
114 if (fpsDuration.count() >= kFpsUpdateIntervalSeconds) {
115 fps = frameCount / fpsDuration.count();
117 lastFpsUpdate = lastFrame;
124 pressedKeys.insert(event->key());
128 pressedKeys.remove(event->key());
132 pressedMouseButtons.insert(event->button());
135 sceneManager->
handleMouseButton(event->button(), event->type(), event->modifiers());
140 pressedMouseButtons.remove(event->button());
143 sceneManager->
handleMouseButton(event->button(), QEvent::MouseButtonRelease, event->modifiers());
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);
158 mouseCaptured = captured;
160 mouseLastPosBeforeCapture = QCursor::pos();
161 setCursor(Qt::BlankCursor);
163 setCursor(Qt::ArrowCursor);
164 QCursor::setPos(mouseLastPosBeforeCapture);
170 QCursor::setPos(mouseLastPosBeforeCapture);
171 scene->getCamera()->processMouseMovement(dx, -dy);
175void OpenGLWindow::hideObjectLabels() {
176 for (QPushButton* button : objectLabelButtons) {
177 if (button) button->hide();
181void OpenGLWindow::updateObjectLabels() {
182 if (!scene || !sceneManager) {
188 if (!dbg.showObjectLabels) {
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();
202 sceneManager->focusObject(obj);
206 objectLabelButtons.push_back(button);
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());
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);
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);
226 for (
int yCell = top; yCell <= bottom; ++yCell) {
227 for (
int xCell = left; xCell <= right; ++xCell) {
228 if (visitor(xCell, yCell))
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;
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;
250 for (
size_t i = 0; i < objectLabelButtons.size(); ++i) {
251 QPushButton* button = objectLabelButtons[i];
252 if (i >= objects.size()) {
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)) {
266 const bool behindCamera = clip.w < 0.0f;
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)) {
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;
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);
287 cameraZ > kClipWMin ? cameraX / cameraZ : cameraX,
288 cameraZ > kClipWMin ? cameraY / cameraZ : cameraY
290 if (glm::dot(edgeDir, edgeDir) < kOffscreenDirectionEpsilonSq) {
291 edgeDir = glm::vec2(1.0f, 0.0f);
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;
300 ndc.x = std::clamp(ndc.x, -kEdgeClampX, kEdgeClampX);
301 ndc.y = std::clamp(ndc.y, -kEdgeClampY, kEdgeClampY);
304 bool metricsDirty =
false;
305 QString labelText = QString::fromStdString(obj->
getName());
307 if (std::abs(ndc.x) > std::abs(ndc.y)) {
308 labelText = ndc.x < 0.0f
312 labelText = ndc.y < 0.0f
317 if (button->text() != labelText) {
318 button->setText(labelText);
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
327 " background: rgba(20, 22, 24, 120);"
328 " border: 1px dashed rgba(20, 22, 24, 120);"
329 " color: rgba(210, 210, 210, 170);"
332 " border-radius: 2px;"
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);"
342 " background: rgba(28, 32, 35, 190);"
343 " border: 1px solid rgba(115, 130, 135, 150);"
344 " color: rgba(235, 235, 235, 235);"
347 " border-radius: 2px;"
350 "QPushButton:hover {"
351 " background: rgba(44, 50, 54, 225);"
352 " border-color: rgba(150, 170, 175, 190);"
357 const QVariant metricsReady = button->property(
"metricsReady");
358 if (metricsDirty || !metricsReady.isValid() || !metricsReady.toBool()) {
359 button->adjustSize();
360 button->setProperty(
"metricsReady",
true);
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);
371 int bestScore = std::numeric_limits<int>::max();
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;
383 const int score = (candidateX - baseX) * (candidateX - baseX) + (candidateY - baseY) * (candidateY - baseY);
384 if (score < bestScore) {
391 if (bestScore != std::numeric_limits<int>::max())
break;
394 const QRect placed(bestX, bestY, button->width(), button->height());
396 button->move(bestX, bestY);
Interactive 3D manipulation gizmo for scene objects.
static AppSettings & getInstance()
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 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()
const std::string & getName() const
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.
Represents a ray in 3D space.
Hash specialization for Vertex to enable use in unordered containers.