src/gl/partrenderer.cpp

Fri, 01 Jul 2022 16:46:43 +0300

author
Teemu Piippo <teemu.s.piippo@gmail.com>
date
Fri, 01 Jul 2022 16:46:43 +0300
changeset 312
2637134bc37c
parent 311
fab454611f9b
child 313
c24d87f64bed
permissions
-rw-r--r--

Fix right click to delete not really working properly
Instead of removing the point that had been added, it would remove
the point that is being drawn, which would cause it to overwrite the
previous point using the new point, causing a bit of a delay

/*
 *  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(
	Model* 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);
	connect(model, &Model::rowsInserted, [&]{
		this->needBuild = true;
	});
	connect(model, &Model::rowsRemoved, [&]{ this->needBuild = true; });
	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, &Model::dataChanged, 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);
}

static constexpr 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 value for vboClass"};
}

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::mat4_cast(this->modelQuaternion);
	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)
{
	gl::bindModelShaderVertexArray(&this->shaders, arrayClass);
	const std::size_t vertexCount = gl::vertexCount(&this->shaders, arrayClass);
	this->checkForGLErrors();
	glDrawArrays(getGlTypeForArrayClass(arrayClass), 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();
	}
}

void PartRenderer::mouseReleaseEvent(QMouseEvent* event)
{
	if (not frozen and this->totalMouseMove < (2.0 / sqrt(2)) * 5.0)
	{
		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);
}

/**
 * @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);
}

ElementId 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<ElementId>& selection)
{
	Q_ASSERT(not selection.contains({0}));
	gl::setModelShaderSelectedObjects(&this->shaders, selection);
	this->update();
}

mercurial