From 42dc9def7daa0fdcf7630c88adda04e06d54c3c6 Mon Sep 17 00:00:00 2001 From: Evan Black Date: Thu, 7 Jul 2022 00:01:26 -0400 Subject: [PATCH] Add clicking to select Nodes See: https://github.com/usnistgov/NetSimulyzer/issues/15 --- resources.qrc | 2 + shaders/picking.frag | 14 +++ shaders/picking.vert | 11 +++ src/CMakeLists.txt | 1 + src/render/framebuffer/PickingFramebuffer.cpp | 92 +++++++++++++++++++ src/render/framebuffer/PickingFramebuffer.h | 72 +++++++++++++++ src/render/model/ModelCache.cpp | 8 ++ src/render/model/ModelCache.h | 2 + src/render/renderer/Renderer.cpp | 22 +++++ src/render/renderer/Renderer.h | 3 + src/window/scene/SceneWidget.cpp | 38 +++++++- src/window/scene/SceneWidget.h | 4 + 12 files changed, 268 insertions(+), 1 deletion(-) create mode 100644 shaders/picking.frag create mode 100644 shaders/picking.vert create mode 100644 src/render/framebuffer/PickingFramebuffer.cpp create mode 100644 src/render/framebuffer/PickingFramebuffer.h diff --git a/resources.qrc b/resources.qrc index 576efe2..699b710 100644 --- a/resources.qrc +++ b/resources.qrc @@ -21,5 +21,7 @@ shaders/model.frag shaders/skybox.vert shaders/skybox.frag + shaders/picking.frag + shaders/picking.vert diff --git a/shaders/picking.frag b/shaders/picking.frag new file mode 100644 index 0000000..e8a70f3 --- /dev/null +++ b/shaders/picking.frag @@ -0,0 +1,14 @@ +#version 330 + +uniform uint object_type; +uniform uint object_id; + +out uvec3 picking_fragment; + +void main() { + // The leading 1.0 is to indicate the presence of an object + // in the texture, since by default, OpenGL will, clear to 0 + // so, we need some way to tell an object was rendered at this + // fragment + picking_fragment = uvec3(1.0, object_type, object_id); +} diff --git a/shaders/picking.vert b/shaders/picking.vert new file mode 100644 index 0000000..314edc4 --- /dev/null +++ b/shaders/picking.vert @@ -0,0 +1,11 @@ +#version 330 + +layout (location = 0) in vec3 in_position; + +uniform mat4 model; +uniform mat4 view; +uniform mat4 projection; + +void main() { + gl_Position = projection * view * model * vec4(in_position, 1.0); +} \ No newline at end of file diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 0c4c2e1..b5a6151 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -37,6 +37,7 @@ target_sources(netsimulyzer PRIVATE group/node/Node.h group/node/Node.cpp group/node/TrailBuffer.h group/node/TrailBuffer.cpp render/camera/Camera.h render/camera/Camera.cpp + render/framebuffer/PickingFramebuffer.h render/framebuffer/PickingFramebuffer.cpp render/helper/Floor.h render/helper/Floor.cpp render/Light.h render/material/material.h diff --git a/src/render/framebuffer/PickingFramebuffer.cpp b/src/render/framebuffer/PickingFramebuffer.cpp new file mode 100644 index 0000000..cb974a2 --- /dev/null +++ b/src/render/framebuffer/PickingFramebuffer.cpp @@ -0,0 +1,92 @@ +/* + * NIST-developed software is provided by NIST as a public service. You may use, + * copy and distribute copies of the software in any medium, provided that you + * keep intact this entire notice. You may improve,modify and create derivative + * works of the software or any portion of the software, and you may copy and + * distribute such modifications or works. Modified works should carry a notice + * stating that you changed the software and should note the date and nature of + * any such change. Please explicitly acknowledge the National Institute of + * Standards and Technology as the source of the software. + * + * NIST-developed software is expressly provided "AS IS." NIST MAKES NO + * WARRANTY OF ANY KIND, EXPRESS, IMPLIED, IN FACT OR ARISING BY OPERATION OF + * LAW, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTY OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT + * AND DATA ACCURACY. NIST NEITHER REPRESENTS NOR WARRANTS THAT THE + * OPERATION OF THE SOFTWARE WILL BE UNINTERRUPTED OR ERROR-FREE, OR THAT + * ANY DEFECTS WILL BE CORRECTED. NIST DOES NOT WARRANT OR MAKE ANY + * REPRESENTATIONS REGARDING THE USE OF THE SOFTWARE OR THE RESULTS THEREOF, + * INCLUDING BUT NOT LIMITED TO THE CORRECTNESS, ACCURACY, RELIABILITY, + * OR USEFULNESS OF THE SOFTWARE. + * + * You are solely responsible for determining the appropriateness of using and + * distributing the software and you assume all risks associated with its use, + * including but not limited to the risks and costs of program errors, + * compliance with applicable laws, damage to or loss of data, programs or + * equipment, and the unavailability or interruption of operation. This + * software is not intended to be used in any situation where a failure could + * cause risk of injury or damage to property. The software developed by NIST + * employees is not subject to copyright protection within the United States. + * + * Author: Evan Black + */ + +#include "PickingFramebuffer.h" +#include +namespace netsimulyzer { + +void PickingFramebuffer::generate(int width, int height) { + openGl.glGenFramebuffers(1, &fbo); + bind(GL_FRAMEBUFFER); + + openGl.glGenTextures(1, &idTexture); + openGl.glBindTexture(GL_TEXTURE_2D, idTexture); + openGl.glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB32UI, width, height, 0, GL_RGB_INTEGER, GL_UNSIGNED_INT, nullptr); + openGl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + openGl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + openGl.glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, idTexture, 0); + + openGl.glGenTextures(1, &depthTexture); + openGl.glBindTexture(GL_TEXTURE_2D, depthTexture); + openGl.glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, width, height, 0, GL_DEPTH_COMPONENT, GL_FLOAT, nullptr); + openGl.glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depthTexture, 0); +} + +// NOLINT(cppcoreguidelines-pro-type-member-init) +PickingFramebuffer::PickingFramebuffer(QOpenGLFunctions_3_3_Core &openGl, int width, int height) + : openGl(openGl) { // NOLINT(cppcoreguidelines-pro-type-member-init) + generate(width, height); +} + +PickingFramebuffer::~PickingFramebuffer() { + openGl.glDeleteTextures(1, &idTexture); + openGl.glDeleteTextures(1, &depthTexture); + openGl.glDeleteFramebuffers(1, &fbo); +} + +void PickingFramebuffer::bind(GLenum mode) const { + openGl.glBindFramebuffer(mode, fbo); +} + +void PickingFramebuffer::unbind(GLenum mode, unsigned int defaultFbo) const { + openGl.glBindFramebuffer(mode, defaultFbo); +} + +PickingFramebuffer::PixelInfo PickingFramebuffer::read(int x, int y) const { + bind(GL_READ_FRAMEBUFFER); + openGl.glReadBuffer(GL_COLOR_ATTACHMENT0); + + PixelInfo pixelInfo; // NOLINT(cppcoreguidelines-pro-type-member-init) + openGl.glReadPixels(x, y, 1, 1, GL_RGB_INTEGER, GL_UNSIGNED_INT, &pixelInfo); + + return pixelInfo; +} + +void PickingFramebuffer::resize(int width, int height) { + openGl.glDeleteTextures(1, &idTexture); + openGl.glDeleteTextures(1, &depthTexture); + openGl.glDeleteFramebuffers(1, &fbo); + generate(width, height); +} + +} // namespace netsimulyzer diff --git a/src/render/framebuffer/PickingFramebuffer.h b/src/render/framebuffer/PickingFramebuffer.h new file mode 100644 index 0000000..9cda538 --- /dev/null +++ b/src/render/framebuffer/PickingFramebuffer.h @@ -0,0 +1,72 @@ +/* + * NIST-developed software is provided by NIST as a public service. You may use, + * copy and distribute copies of the software in any medium, provided that you + * keep intact this entire notice. You may improve,modify and create derivative + * works of the software or any portion of the software, and you may copy and + * distribute such modifications or works. Modified works should carry a notice + * stating that you changed the software and should note the date and nature of + * any such change. Please explicitly acknowledge the National Institute of + * Standards and Technology as the source of the software. + * + * NIST-developed software is expressly provided "AS IS." NIST MAKES NO + * WARRANTY OF ANY KIND, EXPRESS, IMPLIED, IN FACT OR ARISING BY OPERATION OF + * LAW, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTY OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT + * AND DATA ACCURACY. NIST NEITHER REPRESENTS NOR WARRANTS THAT THE + * OPERATION OF THE SOFTWARE WILL BE UNINTERRUPTED OR ERROR-FREE, OR THAT + * ANY DEFECTS WILL BE CORRECTED. NIST DOES NOT WARRANT OR MAKE ANY + * REPRESENTATIONS REGARDING THE USE OF THE SOFTWARE OR THE RESULTS THEREOF, + * INCLUDING BUT NOT LIMITED TO THE CORRECTNESS, ACCURACY, RELIABILITY, + * OR USEFULNESS OF THE SOFTWARE. + * + * You are solely responsible for determining the appropriateness of using and + * distributing the software and you assume all risks associated with its use, + * including but not limited to the risks and costs of program errors, + * compliance with applicable laws, damage to or loss of data, programs or + * equipment, and the unavailability or interruption of operation. This + * software is not intended to be used in any situation where a failure could + * cause risk of injury or damage to property. The software developed by NIST + * employees is not subject to copyright protection within the United States. + * + * Author: Evan Black + */ +#pragma once +#include + +namespace netsimulyzer { + +class PickingFramebuffer { + QOpenGLFunctions_3_3_Core &openGl; + unsigned int fbo; + unsigned int idTexture; + unsigned int depthTexture; + + void generate(int width, int height); + +public: + struct PixelInfo { + unsigned int object; + unsigned int type; + unsigned int id; + }; + + PickingFramebuffer(QOpenGLFunctions_3_3_Core &openGl, int width, int height); + ~PickingFramebuffer(); + + // Disallow copying + PickingFramebuffer(const PickingFramebuffer &) = delete; + PickingFramebuffer &operator=(const PickingFramebuffer &) = delete; + + void bind(GLenum mode) const; + void unbind(GLenum mode, unsigned int defaultFbo) const; + + [[nodiscard]] PixelInfo read(int x, int y) const; + + inline unsigned int getIds() { + return idTexture; + } + + void resize(int width, int height); +}; + +} // namespace netsimulyzer diff --git a/src/render/model/ModelCache.cpp b/src/render/model/ModelCache.cpp index e6d3c44..123e798 100644 --- a/src/render/model/ModelCache.cpp +++ b/src/render/model/ModelCache.cpp @@ -261,6 +261,14 @@ void ModelRenderInfo::clear() { meshes.clear(); } +std::vector &ModelRenderInfo::getMeshes() { + return meshes; +} + +std::vector &ModelRenderInfo::getTransparentMeshes() { + return transparentMeshes; +} + ModelCache::ModelCache(TextureCache &textureCache) : textureCache(textureCache) { } diff --git a/src/render/model/ModelCache.h b/src/render/model/ModelCache.h index 2f45b12..01a9213 100644 --- a/src/render/model/ModelCache.h +++ b/src/render/model/ModelCache.h @@ -92,6 +92,8 @@ class ModelRenderInfo : protected QOpenGLFunctions_3_3_Core { void render(Shader &s, const Model &model); void renderTransparent(Shader &s, const Model &model); + std::vector& getMeshes(); + std::vector& getTransparentMeshes(); void clear(); }; diff --git a/src/render/renderer/Renderer.cpp b/src/render/renderer/Renderer.cpp index 40472a4..4e5b888 100644 --- a/src/render/renderer/Renderer.cpp +++ b/src/render/renderer/Renderer.cpp @@ -80,6 +80,7 @@ void Renderer::init() { initShader(modelShader, ":shader/shaders/model.vert", ":shader/shaders/model.frag"); initShader(skyBoxShader, ":shader/shaders/skybox.vert", ":shader/shaders/skybox.frag"); + initShader(pickingShader, ":/shader/shaders/picking.vert", ":/shader/shaders/picking.frag"); } void Renderer::setPerspective(const glm::mat4 &perspective) { @@ -88,6 +89,7 @@ void Renderer::setPerspective(const glm::mat4 &perspective) { gridShader.uniform("projection", perspective); modelShader.uniform("projection", perspective); skyBoxShader.uniform("projection", perspective); + pickingShader.uniform("projection", perspective); } void Renderer::setPointLightCount(unsigned int count) { @@ -539,6 +541,8 @@ void Renderer::use(const Camera &cam) { auto noTranslationView = cam.view_matrix(); noTranslationView[3] = {0.0f, 0.0f, 0.0f, 1.0f}; skyBoxShader.uniform("view", noTranslationView); + + pickingShader.uniform("view", cam.view_matrix()); } void Renderer::render(const DirectionalLight &light) { @@ -712,4 +716,22 @@ void Renderer::render(const std::vector &wiredLinks) { glDisable(GL_LINE_SMOOTH); } +void Renderer::renderPickingNode(unsigned int nodeId, const Model &m) { + auto &model = modelCache.get(m.getModelId()); + + pickingShader.uniform("model", m.getModelMatrix()); + pickingShader.uniform("object_id", nodeId); + pickingShader.uniform("object_type", 1u); + + pickingShader.bind(); + auto &meshes = model.getMeshes(); + for (auto &mesh : meshes) { + mesh.render(); + } + auto &transparentMeshes = model.getTransparentMeshes(); + for (auto &mesh : transparentMeshes) { + mesh.render(); + } +} + } // namespace netsimulyzer diff --git a/src/render/renderer/Renderer.h b/src/render/renderer/Renderer.h index 7c780cc..79c5efa 100644 --- a/src/render/renderer/Renderer.h +++ b/src/render/renderer/Renderer.h @@ -62,6 +62,7 @@ class Renderer : protected QOpenGLFunctions_3_3_Core { Shader gridShader; Shader modelShader; Shader skyBoxShader; + Shader pickingShader; void initShader(Shader &s, const QString &vertexPath, const QString &fragmentPath); @@ -89,6 +90,8 @@ class Renderer : protected QOpenGLFunctions_3_3_Core { void startTransparent(); void endTransparent(); + void renderPickingNode(unsigned int nodeId, const Model &m); + void use(const Camera &cam); void render(const DirectionalLight &light); void render(const PointLight &light); diff --git a/src/window/scene/SceneWidget.cpp b/src/window/scene/SceneWidget.cpp index 6bc7bf2..e09b3b2 100644 --- a/src/window/scene/SceneWidget.cpp +++ b/src/window/scene/SceneWidget.cpp @@ -209,6 +209,10 @@ void SceneWidget::initializeGL() { updatePerspective(); + // picking FBO + pickingFbo = std::make_unique(openGl, width(), height()); + pickingFbo->unbind(GL_FRAMEBUFFER, defaultFramebufferObject()); + // Cheap hack to get Qt to repaint at a reasonable rate // Seems to only work with the old connect syntax QObject::connect(&timer, SIGNAL(timeout()), this, SLOT(update())); @@ -225,9 +229,25 @@ void SceneWidget::paintGL() { handleUndoEvents(); } - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // NOLINT(hicpp-signed-bitwise) + // Picking + pickingFbo->bind(GL_FRAMEBUFFER); + glClearColor(0.0f, 0.0f, 0.0f, 0.0f); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + for (auto &[key, node] : nodes) { + if (!node.visible()) + continue; + renderer.renderPickingNode(node.getNs3Model().id, node.getModel()); + } + + glBindFramebuffer(GL_FRAMEBUFFER, defaultFramebufferObject()); + // end Picking + camera.move(static_cast(frameTimer.elapsed())); renderer.use(camera); + + glClearColor(1.0f, 1.0f, 1.0f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); if (renderSkybox) renderer.render(*skyBox); @@ -318,6 +338,7 @@ void SceneWidget::paintGL() { void SceneWidget::resizeGL(int w, int h) { updatePerspective(); glViewport(0, 0, w, h); + pickingFbo->resize(w, h); } void SceneWidget::keyPressEvent(QKeyEvent *event) { @@ -332,6 +353,21 @@ void SceneWidget::keyReleaseEvent(QKeyEvent *event) { void SceneWidget::mousePressEvent(QMouseEvent *event) { QWidget::mousePressEvent(event); + + makeCurrent(); + + // OpenGL starts from the bottom left, + // Qt Starts at the top left, + // so adjust the Y coordinate accordingly + const auto selected = pickingFbo->read(event->x(), height() - event->y()); + pickingFbo->unbind(GL_READ_FRAMEBUFFER, defaultFramebufferObject()); + doneCurrent(); + + if (selected.object && selected.type == 1u) { + emit nodeSelected(selected.id); + return; + } + if (!camera.mouseControlsEnabled()) return; diff --git a/src/window/scene/SceneWidget.h b/src/window/scene/SceneWidget.h index 5e3ae7e..ce17a85 100644 --- a/src/window/scene/SceneWidget.h +++ b/src/window/scene/SceneWidget.h @@ -49,6 +49,7 @@ #include "../../settings/SettingsManager.h" #include "../../util/undo-events.h" #include "src/group/link/WiredLink.h" +#include "src/render/framebuffer/PickingFramebuffer.h" #include "src/render/helper/CoordinateGrid.h" #include "src/render/helper/SkyBox.h" #include @@ -93,6 +94,8 @@ class SceneWidget : public QOpenGLWidget, protected QOpenGLFunctions_3_3_Core { bool renderBuildingOutlines = settings.get(SettingsManager::Key::RenderBuildingOutlines).value(); bool renderMotionTrails = settings.get(SettingsManager::Key::RenderMotionTrails).value(); + std::unique_ptr pickingFbo; + DirectionalLight mainLight; std::unique_ptr skyBox; std::unique_ptr floor; @@ -242,5 +245,6 @@ class SceneWidget : public QOpenGLWidget, protected QOpenGLFunctions_3_3_Core { void timeChanged(parser::nanoseconds simulationTime, parser::nanoseconds increment); void paused(); void playing(); + void nodeSelected(unsigned int nodeId); }; } // namespace netsimulyzer