src/gl/partrenderer.cpp

Wed, 25 May 2022 20:36:34 +0300

author
Teemu Piippo <teemu@hecknology.net>
date
Wed, 25 May 2022 20:36:34 +0300
changeset 199
6988973515d2
parent 189
815fbaae9cb2
child 200
ca23936b455b
permissions
-rw-r--r--

Fix pick() picking from weird places on the screen with high DPI scaling

glReadPixels reads data from the frame buffer, which contains data after
high DPI scaling, so any reads to that need to take this scaling into account

/*
 *  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 <glm/ext/matrix_transform.hpp>
#include <glm/ext/matrix_clip_space.hpp>
#include <GL/glu.h>
#include <QMouseEvent>
#include <QMessageBox>
#include <QAbstractButton>
#include "geometry.h"
#include "partrenderer.h"
#include "model.h"

static constexpr double MIN_ZOOM = -3.0;
static constexpr double MAX_ZOOM = 3.0;
QOpenGLFunctions glfunc;

PartRenderer::PartRenderer(
	Model* model,
	DocumentManager* documents,
	const ldraw::ColorTable& colorTable,
	QWidget* parent) :
	QOpenGLWidget{parent},
	model{model},
	documents{documents},
	colorTable{colorTable}
{
	this->setMouseTracking(true);
	connect(model, &Model::rowsInserted, [&]{
		this->needBuild = true;
	});
	connect(model, &Model::rowsRemoved, [&]{ this->needBuild = true; });
}

PartRenderer::~PartRenderer()
{
}

static QVector3D calcQVector3DFromQColor(const QColor& color)
{
	return {
		toFloat(color.redF()),
		toFloat(color.greenF()),
		toFloat(color.blueF()),
	};
}

void PartRenderer::initializeGL()
{
	::glfunc.initializeOpenGLFunctions();
	if (glGetError() != GL_NO_ERROR)
	{
		abort();
	}
	gl::initializeModelShaders(&this->shaders);
	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();
}

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(0.0f, 0.0f, 0.0f, 1.0f);
		gl::setShaderUniform(&this->shaders, "useLighting", GL_FALSE);
	}
	this->checkForGLErrors();
	const QVector3D color = calcQVector3DFromQColor(this->renderPreferences.selectedColor);
	gl::setShaderUniform(&this->shaders, "selectedColor", color);
	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);
	};
	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;
	case gl::RenderStyle::Wireframe:
		glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
		this->setFragmentStyle(gl::FragmentStyle::Normal);
		renderAllArrays();
		break;
	}
	glDisable(GL_POLYGON_OFFSET_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)
{
	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(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();
	this->renderScene();
	std::array<GLubyte, 3> data;
	this->checkForGLErrors();
	glfunc.glReadPixels(where.x(), where.y(), 1, 1, GL_RGB, GL_UNSIGNED_BYTE, &data[0]);
	this->checkForGLErrors();
	this->renderPreferences.style = oldRenderStyle;
	this->update();
	return gl::idFromColor(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
 */
ldraw::id_t PartRenderer::getHighlightedObject() const
{
	return this->highlighted;
}

mercurial