Wed, 18 Mar 2020 15:54:30 +0200
fix
/* * 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/glut.h> #include <glm/ext/matrix_transform.hpp> #include <glm/ext/matrix_clip_space.hpp> #include <QMouseEvent> #include <QMessageBox> #include <QAbstractButton> #include "geometry.h" #include "partrenderer.h" #include "model.h" PartRenderer::PartRenderer( Model* model, DocumentManager* documents, const ldraw::ColorTable& colorTable, QWidget* parent) : QOpenGLWidget{parent}, model{model}, documents{documents}, colorTable{colorTable}, compiler{new gl::Compiler{this->colorTable, this}} { this->setMouseTracking(true); } PartRenderer::~PartRenderer() { } static QVector3D vec3FromQColor(const QColor& color) { return { toFloat(color.redF()), toFloat(color.greenF()), toFloat(color.blueF()), }; } void PartRenderer::initializeGL() { this->initializeOpenGLFunctions(); if (glGetError() != GL_NO_ERROR) { abort(); } this->compiler->initialize(); this->compiler->build(this->model, this->documents, this->renderPreferences); connect(this->model, &Model::dataChanged, [&]() { this->compiler->build(this->model, this->documents, this->renderPreferences); }); 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->setupBackgroundColor(); 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); this->compiler->setUniformMatrix("projectionMatrix", this->projectionMatrix); emit projectionMatrixChanged(this->projectionMatrix); } static GLenum getGlTypeForArrayClass(const gl::ArrayClass vboClass) { switch (vboClass) { case gl::ArrayClass::Lines: case gl::ArrayClass::ConditionalLines: return GL_LINES; case gl::ArrayClass::Triangles: return GL_TRIANGLES; case gl::ArrayClass::Quads: return GL_QUADS; } throw std::runtime_error{"Bad vbo class passed to getGlTypeForVboClass"}; } void PartRenderer::paintGL() { glEnable(GL_DEPTH_TEST); glShadeModel(GL_SMOOTH); this->renderScene(); } void PartRenderer::renderScene() { this->checkForGLErrors(); if (this->renderPreferences.lineAntiAliasing && 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); this->compiler->setUniform("useLighting", GL_TRUE); } else { glClearColor(0.0f, 0.0f, 0.0f, 1.0f); this->compiler->setUniform("useLighting", GL_FALSE); } this->checkForGLErrors(); this->compiler->setUniform("selectedColor", vec3FromQColor(this->renderPreferences.selectedColor)); this->compiler->setUniform("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); switch (this->renderPreferences.style) { case gl::RenderStyle::Normal: this->setFragmentStyle(gl::FragmentStyle::Normal); this->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); case gl::RenderStyle::RandomColors: this->setFragmentStyle(gl::FragmentStyle::RandomColors); this->renderAllArrays(); break; case gl::RenderStyle::PickScene: glLineWidth(3.0f); this->setFragmentStyle(gl::FragmentStyle::Id); this->renderAllArrays(); break; case gl::RenderStyle::Wireframe: glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); this->setFragmentStyle(gl::FragmentStyle::Normal); this->renderAllArrays(); break; } glDisable(GL_POLYGON_OFFSET_FILL); } void PartRenderer::renderAllArrays() { // Lines need to be rendered last so that anti-aliasing does not interfere with polygon rendering. renderVao(gl::ArrayClass::Triangles); renderVao(gl::ArrayClass::Quads); renderVao(gl::ArrayClass::Lines); } void PartRenderer::updateViewMatrix() { // I'm not quite sure why using the exponent function on the zoom factor causes linear zoom behavior const double z = 2 * std::exp(this->zoom) * (1 + this->compiler->modelDistance()); this->viewMatrix = glm::lookAt(glm::vec3{0, 0, z}, {0, 0, 0}, {0, -1, 0}); this->compiler->setUniformMatrix("viewMatrix", this->viewMatrix); emit this->viewMatrixChanged(this->viewMatrix); } void PartRenderer::updateModelMatrix() { this->modelMatrix = glm::mat4_cast(this->modelQuaternion); this->compiler->setUniformMatrix("modelMatrix", modelMatrix); emit this->modelMatrixChanged(this->modelMatrix); this->update(); } void PartRenderer::setupBackgroundColor() { } void PartRenderer::renderVao(const gl::ArrayClass arrayClass) { this->compiler->bindVertexArray(arrayClass); const std::size_t vertexCount = this->compiler->vertexCount(arrayClass); this->checkForGLErrors(); glDrawArrays(getGlTypeForArrayClass(arrayClass), 0, static_cast<GLsizei>(vertexCount)); this->checkForGLErrors(); this->compiler->releaseVertexArray(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) { const bool left = event->buttons() & Qt::LeftButton; const QPoint move = event->pos() - this->lastMousePosition; 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(); } void PartRenderer::wheelEvent(QWheelEvent* event) { 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(); } /** * @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 QPoint& point, const geom::Plane& plane) const { const geom::Line line = this->cameraLine(point); std::optional<glm::vec3> result; result = geom::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 * glm::mat4_cast(this->modelQuaternion), this->projectionMatrix, this->viewportVector); return toQPointF(glm::vec2{projected.x, this->height() - projected.y}); } geom::Line<3> PartRenderer::cameraLine(const QPoint& 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 geom::lineFromPoints(p1, p2); } /** * @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, this->height() - win.y, win.z}, this->viewMatrix * glm::mat4_cast(this->modelQuaternion), this->projectionMatrix, viewportVector); } ldraw::id_t PartRenderer::pick(const QPoint& where) { const gl::RenderStyle oldRenderStyle = this->renderPreferences.style; this->renderPreferences.style = gl::RenderStyle::PickScene; this->makeCurrent(); this->renderScene(); std::array<GLubyte, 3> data; this->checkForGLErrors(); glReadPixels(where.x(), this->height() - where.y(), 1, 1, GL_RGB, GL_UNSIGNED_BYTE, &data[0]); this->checkForGLErrors(); this->renderPreferences.style = oldRenderStyle; this->update(); return gl::Compiler::idFromColor(data); } /** * @brief Changes the color of rendered fragments * @param newFragmentStyle new fragment style to use */ void PartRenderer::setFragmentStyle(gl::FragmentStyle newFragmentStyle) { this->compiler->setUniform("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->compiler->build(this->model, this->documents, this->renderPreferences); this->setupBackgroundColor(); } emit this->renderPreferencesChanged(); this->update(); }