Sun, 09 Apr 2023 16:30:33 +0300
Also connect up the "Delete" action
/* * LDForge: LDraw parts authoring CAD * Copyright (C) 2013 - 2020 Teemu Piippo * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ #include <GL/glew.h> #include <glm/ext/matrix_transform.hpp> #include <glm/ext/matrix_clip_space.hpp> #include <QOpenGLFramebufferObject> #include <QPainter> #include <GL/glu.h> #include <QMouseEvent> #include <QMessageBox> #include <QAbstractButton> #include "src/geometry.h" #include "src/model.h" #include "src/settings.h" #include "src/gl/partrenderer.h" #include "src/gl/compiler.h" static constexpr double MIN_ZOOM = -3.0; static constexpr double MAX_ZOOM = 3.0; PartRenderer::PartRenderer( QTextDocument* model, DocumentManager* documents, const ColorTable& colorTable, QWidget* parent) : QOpenGLWidget{parent}, model{model}, documents{documents}, colorTable{colorTable} { this->setMouseTracking(true); this->setFocusPolicy(Qt::WheelFocus); QSurfaceFormat surfaceFormat; surfaceFormat.setSamples(8); this->setFormat(surfaceFormat); const auto setNeedBuild = [&]{this->needBuild = true;}; connect(model, &QTextDocument::contentsChange, setNeedBuild); const auto updateLayerMvpMatrix = [this]{ const glm::mat4 newMvpMatrix = this->projectionMatrix * this->viewMatrix * this->modelMatrix; for (RenderLayer* layer : this->activeRenderLayers) { layer->mvpMatrixChanged(newMvpMatrix); } for (RenderLayer* layer : this->inactiveRenderLayers) { layer->mvpMatrixChanged(newMvpMatrix); } }; connect(this, &PartRenderer::modelMatrixChanged, updateLayerMvpMatrix); connect(this, &PartRenderer::viewMatrixChanged, updateLayerMvpMatrix); connect(this, &PartRenderer::projectionMatrixChanged, updateLayerMvpMatrix); } PartRenderer::~PartRenderer() { } void PartRenderer::initializeGL() { glewInit(); gl::initializeModelShaders(&this->shaders); for (RenderLayer* layer : this->activeRenderLayers) { layer->initializeGL(); } for (RenderLayer* layer : this->inactiveRenderLayers) { layer->initializeGL(); } connect(this->model, &QTextDocument::contentsChanged, this, &PartRenderer::build); this->initialized = true; this->modelQuaternion = glm::angleAxis(glm::radians(30.0f), glm::vec3{-1, 0, 0}); this->modelQuaternion *= glm::angleAxis(glm::radians(225.0f), glm::vec3{-0, 1, 0}); this->updateModelMatrix(); this->updateViewMatrix(); this->update(); } void PartRenderer::resizeGL(int width, int height) { this->viewportVector = {0, 0, width, height}; glViewport(0, 0, width, height); this->projectionMatrix = glm::perspective( glm::radians(45.0f), static_cast<float>(width) / static_cast<float>(height), 0.1f, 10000.f); gl::setShaderUniformMatrix(&this->shaders, "projectionMatrix", this->projectionMatrix); Q_EMIT projectionMatrixChanged(this->projectionMatrix); } void PartRenderer::paintGL() { glEnable(GL_DEPTH_TEST); glShadeModel(GL_SMOOTH); this->renderScene(); for (RenderLayer* layer : this->activeRenderLayers) { layer->paintGL(); } QPainter painter{this}; painter.setRenderHint(QPainter::Antialiasing); for (RenderLayer* layer : this->activeRenderLayers) { layer->overpaint(&painter); } } void PartRenderer::renderScene() { if (this->needBuild) { gl::build(&this->shaders, this->model, this->colorTable, this->documents, this->renderPreferences); this->boundingBox = gl::boundingBoxForModel(this->model, this->documents); this->needBuild = false; } this->checkForGLErrors(); if (true and this->renderPreferences.lineAntiAliasing and this->renderPreferences.style != gl::RenderStyle::PickScene ) { glEnable(GL_LINE_SMOOTH); glHint(GL_LINE_SMOOTH_HINT, GL_NICEST); } else { glDisable(GL_LINE_SMOOTH); } if (this->renderPreferences.style != gl::RenderStyle::PickScene) { const QColor& backgroundColor = this->renderPreferences.backgroundColor; glClearColor( static_cast<float>(backgroundColor.redF()), static_cast<float>(backgroundColor.greenF()), static_cast<float>(backgroundColor.blueF()), 1.0f); gl::setShaderUniform(&this->shaders, "useLighting", GL_TRUE); } else { glClearColor(1.0f, 1.0f, 1.0f, 1.0f); gl::setShaderUniform(&this->shaders, "useLighting", GL_FALSE); } this->checkForGLErrors(); const QColor qs = this->renderPreferences.selectedColor; const glm::vec4 selectedColor{qs.redF(), qs.greenF(), qs.blueF(), 1.0f}; gl::setShaderUniformVector(&this->shaders, "selectedColor", selectedColor); gl::setShaderUniform(&this->shaders, "highlighted", this->highlighted.value); this->checkForGLErrors(); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glEnable(GL_DEPTH_TEST); glEnable(GL_POLYGON_OFFSET_FILL); glPolygonOffset(1.0f, 1.0f); glLineWidth(this->renderPreferences.lineThickness); const auto renderAllArrays = [this](){ // Lines need to be rendered last so that anti-aliasing does not interfere with polygon rendering. this->renderVao<gl::ArrayClass::Triangles>(); this->renderVao<gl::ArrayClass::Quads>(); this->renderVao<gl::ArrayClass::Lines>(); }; if (this->renderPreferences.wireframe and this->renderPreferences.style != gl::RenderStyle::PickScene) { glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); } switch (this->renderPreferences.style) { case gl::RenderStyle::Normal: this->setFragmentStyle(gl::FragmentStyle::Normal); renderAllArrays(); break; case gl::RenderStyle::BfcRedGreen: glEnable(GL_CULL_FACE); glCullFace(GL_BACK); this->setFragmentStyle(gl::FragmentStyle::BfcGreen); renderVao<gl::ArrayClass::Triangles>(); renderVao<gl::ArrayClass::Quads>(); glCullFace(GL_FRONT); this->setFragmentStyle(gl::FragmentStyle::BfcRed); renderVao<gl::ArrayClass::Triangles>(); renderVao<gl::ArrayClass::Quads>(); glDisable(GL_CULL_FACE); this->setFragmentStyle(gl::FragmentStyle::Normal); renderVao<gl::ArrayClass::Lines>(); break; case gl::RenderStyle::RandomColors: this->setFragmentStyle(gl::FragmentStyle::RandomColors); renderAllArrays(); break; case gl::RenderStyle::PickScene: glLineWidth(3.0f); this->setFragmentStyle(gl::FragmentStyle::Id); renderAllArrays(); break; case gl::RenderStyle::VertexPickScene: glLineWidth(1.0f); this->setFragmentStyle(gl::FragmentStyle::Black); renderAllArrays(); break; } glDisable(GL_POLYGON_OFFSET_FILL); glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); } void PartRenderer::updateViewMatrix() { // I'm not quite sure why using the exponent function on the zoom factor causes linear zoom behavior const float modelDistance = longestMeasure(this->boundingBox); const double z = 2.0 * std::exp(this->zoom) * (1 + static_cast<double>(modelDistance)); this->viewMatrix = glm::lookAt(glm::vec3{0, 0, z}, {0, 0, 0}, {0, -1, 0}); gl::setShaderUniformMatrix(&this->shaders, "viewMatrix", this->viewMatrix); Q_EMIT this->viewMatrixChanged(this->viewMatrix); } void PartRenderer::updateModelMatrix() { this->modelMatrix = glm::translate(glm::mat4_cast(this->modelQuaternion), -this->modelViewOrigin); gl::setShaderUniformMatrix(&this->shaders, "modelMatrix", modelMatrix); Q_EMIT this->modelMatrixChanged(this->modelMatrix); this->update(); } void PartRenderer::build() { this->needBuild = true; } void PartRenderer::renderVao(const gl::ArrayClass arrayClass, const GLenum glType) { gl::bindModelShaderVertexArray(&this->shaders, arrayClass); const std::size_t vertexCount = gl::vertexCount(&this->shaders, arrayClass); this->checkForGLErrors(); glDrawArrays(glType, 0, static_cast<GLsizei>(vertexCount)); this->checkForGLErrors(); gl::releaseModelShaderVertexArray(&this->shaders, arrayClass); this->checkForGLErrors(); } void PartRenderer::checkForGLErrors() { gl::checkForGLErrors(this); } void gl::checkForGLErrors(QWidget* parent) { GLenum glError; QStringList errors; while ((glError = glGetError()) != GL_NO_ERROR) { const QString glErrorString = QString::fromLatin1(reinterpret_cast<const char*>(gluErrorString(glError))); errors.append(glErrorString); } if (not errors.isEmpty()) { QMessageBox box{parent}; box.setIcon(QMessageBox::Critical); box.setText(QObject::tr("OpenGL error: %1").arg(errors.join("\n"))); box.setWindowTitle(QObject::tr("OpenGL error")); box.setStandardButtons(QMessageBox::Close); box.button(QMessageBox::Close)->setText(QObject::tr("Damn it")); box.exec(); } } void PartRenderer::mouseMoveEvent(QMouseEvent* event) { if (not this->frozen) { const bool left = event->buttons() & Qt::LeftButton; const QPoint move = event->pos() - this->lastMousePosition; this->totalMouseMove += move.manhattanLength(); if (left and not move.isNull()) { // q_x is the rotation of the brick along the vertical y-axis, because turning the // vertical axis causes horizontal (=x) rotation. Likewise q_y is the rotation of the // brick along the horizontal x-axis, which causes vertical rotation. const auto scalar = 0.006f; const float move_x = static_cast<float>(move.x()); const float move_y = static_cast<float>(move.y()); const glm::quat q_x = glm::angleAxis(scalar * move_x, glm::vec3{0, -1, 0}); const glm::quat q_y = glm::angleAxis(scalar * move_y, glm::vec3{-1, 0, 0}); this->modelQuaternion = q_x * q_y * this->modelQuaternion; this->updateModelMatrix(); } this->lastMousePosition = event->pos(); for (RenderLayer* layer : this->activeRenderLayers) { layer->mouseMoved(event); } this->update(); } } void PartRenderer::mousePressEvent(QMouseEvent* event) { if (not this->frozen) { this->totalMouseMove = 0; this->lastMousePosition = event->pos(); this->lastClickTime = steady_clock::now(); } } void PartRenderer::mouseReleaseEvent(QMouseEvent* event) { using namespace std::chrono_literals; if (true and not frozen and this->totalMouseMove < 50.0 / sqrt(2) and (steady_clock::now() - this->lastClickTime) < 0.5s ) { for (RenderLayer* layer : this->activeRenderLayers) { layer->mouseClick(event); } this->update(); } } void PartRenderer::keyReleaseEvent(QKeyEvent* event) { if (event->key() == Qt::Key_Pause) { this->frozen = not this->frozen; } } void PartRenderer::wheelEvent(QWheelEvent* event) { if (not this->frozen) { static constexpr double WHEEL_STEP = 1 / 1000.0; const double move = (-event->angleDelta().y()) * WHEEL_STEP; this->zoom = std::clamp(this->zoom + move, MIN_ZOOM, MAX_ZOOM); this->updateViewMatrix(); this->update(); } } void PartRenderer::addRenderLayer(RenderLayer* layer) { this->activeRenderLayers.push_back(layer); layer->setRendererPointer(this); this->update(); } void PartRenderer::setLayerEnabled(RenderLayer* layer, bool enabled) { auto& from = enabled ? this->inactiveRenderLayers : this->activeRenderLayers; auto& to = enabled ? this->activeRenderLayers : this->inactiveRenderLayers; auto it = std::find(from.begin(), from.end(), layer); if (it != from.end()) { from.erase(it); to.push_back(layer); } } /** * @brief Converts the specified on the screen into the 3D world. The point is unprojected twice into 3D and the * intersection of the resulting line with the specified plane is returned. If the intersection point lies behind * the camera, no value is returned. * @param point 2D window co-ordinates to convert. * @param plane Plane to raycast against * @return world co-ordinates, or no value if the point is behind the camera. */ std::optional<glm::vec3> PartRenderer::screenToModelCoordinates(const QPointF& point, const Plane& plane) const { const Line line = this->cameraLine(point); std::optional<glm::vec3> result; result = linePlaneIntersection(line, plane, 0.01f); // If the point lies behind the camera, do not return a result. if (result.has_value() and glm::dot(line.direction, *result - line.anchor) < 0) { result.reset(); } return result; } /** * @brief Converts the specified point to 2D window coordinates, with Y-coordinate inverted for Qt * @param point Point to unproject * @return screen coordinates */ QPointF PartRenderer::modelToScreenCoordinates(const glm::vec3& point) const { const glm::vec3 projected = glm::project( point, this->viewMatrix * this->modelMatrix, this->projectionMatrix, this->viewportVector); return toQPointF(glm::vec2{projected.x, static_cast<float>(this->height()) - projected.y}); } bool PartRenderer::isDark() const { return luma(this->renderPreferences.backgroundColor) < 0.25; } Line<3> PartRenderer::cameraLine(const QPointF& point) const { const glm::vec3 p1 = this->unproject({point.x(), point.y(), 0}); const glm::vec3 p2 = this->unproject({point.x(), point.y(), 1}); return lineFromPoints(p1, p2); } void PartRenderer::setModelViewOrigin(const glm::vec3& newViewOrigin) { this->modelViewOrigin = newViewOrigin; this->updateModelMatrix(); } /** * @brief Unprojects the specified window coordinates to model coordinates * @param win Window coordinates to project. Z-coordinate indicates depth * @return model coordinates */ glm::vec3 PartRenderer::unproject(const glm::vec3& win) const { return glm::unProject( glm::vec3{win.x, static_cast<float>(this->height()) - win.y, win.z}, this->viewMatrix * this->modelMatrix, this->projectionMatrix, viewportVector); } int32_t PartRenderer::pick(QPoint where) { // y is flipped, take that into account where.setY(this->height() - where.y()); // Since we are dealing with pixel data right from the framebuffer, its size // will be affected by High DPI scaling. We need to take this into account // and multiply the pixel positions by the screen pixel scaling factor. where *= this->devicePixelRatioF(); const gl::RenderStyle oldRenderStyle = this->renderPreferences.style; this->renderPreferences.style = gl::RenderStyle::PickScene; this->makeCurrent(); QOpenGLFramebufferObject fbo{this->width(), this->height(), QOpenGLFramebufferObject::CombinedDepthStencil}; fbo.bind(); this->renderScene(); std::array<GLubyte, 3> data; this->checkForGLErrors(); glReadPixels(where.x(), where.y(), 1, 1, GL_RGB, GL_UNSIGNED_BYTE, &data[0]); this->checkForGLErrors(); fbo.release(); this->renderPreferences.style = oldRenderStyle; return gl::idFromUcharColor(data); } /** * @brief Changes the color of rendered fragments * @param newFragmentStyle new fragment style to use */ void PartRenderer::setFragmentStyle(gl::FragmentStyle newFragmentStyle) { gl::setShaderUniform(&this->shaders, "fragmentStyle", static_cast<int>(newFragmentStyle)); } /** * @brief Changes the way the scene is rendered * @param newStyle new render style to use */ void PartRenderer::setRenderPreferences(const gl::RenderPreferences& newPreferences) { bool mainColorChanged = this->renderPreferences.mainColor != newPreferences.mainColor; bool backgroundColorChanged = this->renderPreferences.backgroundColor != newPreferences.backgroundColor; this->renderPreferences = newPreferences; if (mainColorChanged or backgroundColorChanged) { this->build(); } Q_EMIT this->renderPreferencesChanged(); this->update(); } /** * @return the currently highlighted object */ ModelId PartRenderer::getHighlightedObject() const { return this->highlighted; } void PartRenderer::setSelection(const QSet<int32_t>& selection) { #if 0 Q_ASSERT(not selection.contains({0})); gl::setModelShaderSelectedObjects(&this->shaders, selection); this->update(); #endif } glm::vec3 PartRenderer::cameraVector(const QPointF& point) const { const glm::vec3 p1 = this->unproject({point.x(), point.y(), 0}); const glm::vec3 p2 = this->unproject({point.x(), point.y(), 1}); return p2 - p1; }