Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rewrite FlatMesh #47

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions src/controls/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,11 @@ set(SRC
src/controls_plugin.cpp
src/application_p.cpp
src/flatmesh.cpp
src/flatmeshnode.cpp
src/icon.cpp)
set(HEADERS
src/controls_plugin.h
src/application_p.h
src/flatmesh.h
src/flatmeshnode.h
src/icon.h)

add_library(asteroidcontrolsplugin ${SRC} ${HEADERS} resources.qrc)
Expand Down
247 changes: 217 additions & 30 deletions src/controls/src/flatmesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,49 +27,220 @@
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

#include <QOpenGLShaderProgram>
#include <QOpenGLFunctions>
#include <QSettings>

#include "flatmesh.h"
#include "flatmeshnode.h"
#include "flatmeshgeometry.h"

// Our Adreno drivers fail to load shaders that are too long so we have to be concise and skip
// every unnecessary character such as spaces, \n, etc... This is effectively one long line!
static const char *vertexShaderSource =
// Qt dynamically injects an "attribute" before main. With GLES3, this should be "in"
"#define attribute in\n"

// Attributes are per-vertex information, they give base coordinates and colors
"in vec4 coord;"
"in vec4 color;"

// Uniforms are FlatMesh-wide, they give scaling information, the animation state or shifts
"uniform mat4 matrix;"
"uniform float shiftMix;"
"uniform int loopNb;"
"uniform vec2 shifts[" FLATMESH_SHIFTS_NB_STR "];"

// This is the color vector outputted here and forwarded to the fragment shaders
// The flat keyword enables flat shading (no interpolation between the vertices of a triangle)
"flat out vec4 fragColor;"

"void main()"
"{"
// Two vertices can have the same coordinate (if they give different colors to 2 triangles)
// However, they need to move in sync, so we hash their coordinates as an index for shifts
"int shiftIndex = loopNb+floatBitsToInt(coord.x)+floatBitsToInt(coord.y);"

// Interpolate between (coord + shiftA) and (coord + shiftB) in the [-0.5, 0.5] domain
"vec2 pos = coord.xy + mix(shifts[(shiftIndex)%" FLATMESH_SHIFTS_NB_STR "],"
"shifts[(shiftIndex+1)%" FLATMESH_SHIFTS_NB_STR "],"
"shiftMix);"

FlatMesh::FlatMesh(QQuickItem *parent) : QQuickItem(parent)
// Apply scene graph transformations (FlatMesh position and size) to get the final coords
"gl_Position = matrix * vec4(pos, 0, 1);"

// Forward the color in the vertex attribute to the fragment shaders
"fragColor = color;"
"}";

static const char *fragmentShaderSource =
"#ifdef GL_ES\n"
"precision mediump float;"
"\n#endif\n"

// The flat keyword disables interpolation in triangles
// Each pixel gets the color of the last vertex of the triangle it belongs to
"flat in vec4 fragColor;"
"out vec4 color;"

// Just keep the provided color
"void main()"
"{"
"color = fragColor;"
"}";

static QByteArray versionedShaderCode(const char *src)
{
m_timer.setInterval(90);
m_timer.setSingleShot(false);
connect(&m_timer, SIGNAL(timeout()), this, SLOT(update()));
m_timer.start();
return (QOpenGLContext::currentContext()->isOpenGLES()
? QByteArrayLiteral("#version 300 es\n")
: QByteArrayLiteral("#version 330\n"))
+ src;
}

m_centerColor = QColor("#ffaa39");
m_outerColor = QColor("#df4829");
// This class wraps the FlatMesh vertex and fragment shaders
class SGFlatMeshMaterialShader : public QSGMaterialShader
{
public:
SGFlatMeshMaterialShader() {}
const char *vertexShader() const override {
return versionedShaderCode(vertexShaderSource);
}
const char *fragmentShader() const override {
return versionedShaderCode(fragmentShaderSource);
}
void updateState(const RenderState &state, QSGMaterial *newEffect, QSGMaterial *oldEffect) override {
// On every run, update the animation state uniforms
SGFlatMeshMaterial *material = static_cast<SGFlatMeshMaterial *>(newEffect);
program()->setUniformValue(m_shiftMix_id, material->shiftMix());
program()->setUniformValue(m_loopNb_id, material->loopNb());

if (state.isMatrixDirty()) {
// Vertices coordinates are always in the [-0.5, 0.5] range, modify QtQuick's projection matrix to do the scaling for us
QMatrix4x4 combinedMatrix = state.combinedMatrix();
combinedMatrix.scale(material->width(), material->height());
combinedMatrix.translate(0.5, 0.5);
combinedMatrix.scale(material->screenScaleFactor());
program()->setUniformValue(m_matrix_id, combinedMatrix);
}
// Enable a mode such that 0xFF indices mean "restart a strip"
m_glFuncs->glEnable(GL_PRIMITIVE_RESTART_FIXED_INDEX);
}
char const *const *attributeNames() const override {
// Map attribute numbers to attribute names in the vertex shader
static const char *const attr[] = { "coord", "color", nullptr };
return attr;
}
void deactivate() override {
m_glFuncs->glDisable(GL_PRIMITIVE_RESTART_FIXED_INDEX);
}
private:
void initialize() override {
// Seed the array of shifts with pre-randomized shifts
program()->setUniformValueArray("shifts", flatmesh_shifts, flatmesh_shifts_nb, 2);
// Get the ids of the uniforms we regularly update
m_matrix_id = program()->uniformLocation("matrix");
m_shiftMix_id = program()->uniformLocation("shiftMix");
m_loopNb_id = program()->uniformLocation("loopNb");
// Retrieve OpenGL functions available on all platforms
m_glFuncs = QOpenGLContext::currentContext()->functions();
}
int m_matrix_id;
int m_shiftMix_id;
int m_loopNb_id;
QOpenGLFunctions *m_glFuncs;
};

QSGMaterialShader *SGFlatMeshMaterial::createShader() const
{
return new SGFlatMeshMaterialShader;
}

FlatMesh::FlatMesh(QQuickItem *parent) : QQuickItem(parent), m_geometry(QSGGeometry::defaultAttributes_ColoredPoint2D(), flatmesh_vertices_sz, flatmesh_indices_sz)
{
// Dilate the FlatMesh more or less on squared or round screens
QSettings machineConf("/etc/asteroid/machine.conf", QSettings::IniFormat);
m_material.setScreenScaleFactor(machineConf.value("Display/ROUND", false).toBool() ? 1.2 : 1.7);

// Iterate over all vertices and assign them the coordinates of their base point from flatmesh_vertices
QSGGeometry::ColoredPoint2D *vertices = m_geometry.vertexDataAsColoredPoint2D();
for (int i = 0; i < flatmesh_vertices_sz; i++) {
vertices[i].x = flatmesh_vertices[i].x();
vertices[i].y = flatmesh_vertices[i].y();
}
// Copy the indices buffer (already in the right format)
memcpy(m_geometry.indexData(), flatmesh_indices, sizeof(flatmesh_indices));

// Give initial colors to the vertices
setColors(QColor("#ffaa39"), QColor("#df4829"));


// m_animation interpolates the shiftMix, a float between 0.0 and 1.0
// This is used by the vertex shader as the mix ratio between two shifts
m_animation.setStartValue(0.0);
m_animation.setEndValue(1.0);
m_animation.setDuration(4000);
m_animation.setLoopCount(-1);
m_animation.setEasingCurve(QEasingCurve::InOutQuad);
QObject::connect(&m_animation, &QVariantAnimation::currentLoopChanged, [this]() {
m_material.incrementLoopNb();
});
QObject::connect(&m_animation, &QVariantAnimation::valueChanged, [this](const QVariant& value) {
m_material.setShiftMix(value.toFloat());
update();
});

// Run m_animation depending on the item's visibility
connect(this, SIGNAL(visibleChanged()), this, SLOT(maybeEnableAnimation()));
setAnimated(true);

// Tell QtQuick we have graphic content and that updatePaintNode() needs to run
setFlag(ItemHasContents);
setAnimated(true);
}

void FlatMesh::setCenterColor(QColor c)
void FlatMesh::updateColors()
{
if (c == m_centerColor)
// Iterate over all vertices and give them the rgb values of the triangle they represent
// In the flat shading model we use, each triangle is colored by its last vertex
QSGGeometry::ColoredPoint2D *vertices = m_geometry.vertexDataAsColoredPoint2D();
for (int i = 0; i < flatmesh_vertices_sz; i++) {
// Ratios are pre-calculated to save some computation, we just need to do the mix
// We do the color blending on the CPU because center and outer colors change rarely
// and it would be a waste of GPU time to re-calculate that in every vertex shader
float ratio = flatmesh_vertices[i].z();
float inverse_ratio = 1-ratio;
vertices[i].r = m_centerColor.red()*inverse_ratio + m_outerColor.red()*ratio;
vertices[i].g = m_centerColor.green()*inverse_ratio + m_outerColor.green()*ratio;
vertices[i].b = m_centerColor.blue()*inverse_ratio + m_outerColor.blue()*ratio;
}
m_geometryDirty = true;
}

void FlatMesh::setColors(QColor center, QColor outer)
{
if (center == m_centerColor && outer == m_outerColor)
return;
m_centerColor = c;
m_centerColor = center;
m_outerColor = outer;
updateColors();
update();
}

void FlatMesh::setCenterColor(QColor c)
{
setColors(c, m_outerColor);
}

void FlatMesh::setOuterColor(QColor c)
{
if (c == m_outerColor)
return;
m_outerColor = c;
update();
setColors(m_centerColor, c);
}

void FlatMesh::maybeEnableAnimation()
{
if (isVisible() && m_animated) {
m_timer.start();
} else {
m_timer.stop();
}
update();
// Only run the animation if the item is visible. No point running the shaders if this is hidden
if (isVisible() && m_animated)
m_animation.start();
else
m_animation.pause();
}

void FlatMesh::setAnimated(bool animated)
Expand All @@ -81,16 +252,32 @@ void FlatMesh::setAnimated(bool animated)
maybeEnableAnimation();
}

void FlatMesh::geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry)
{
// On resizes, tell the vertex shader about the new size so the transformation matrix compensates it
m_material.setSize(newGeometry.width(), newGeometry.height());

QQuickItem::geometryChanged(newGeometry, oldGeometry);
}

// Called by the SceneGraph on every update()
QSGNode *FlatMesh::updatePaintNode(QSGNode *old, UpdatePaintNodeData *)
{
FlatMeshNode *n = static_cast<FlatMeshNode *>(old);
if (!n)
n = new FlatMeshNode(window(), boundingRect());

n->setAnimated(m_animated);
n->setRect(boundingRect());
n->setCenterColor(m_centerColor);
n->setOuterColor(m_outerColor);
// On the first update(), create a scene graph node for the mesh
QSGGeometryNode *n = static_cast<QSGGeometryNode *>(old);
if (!n) {
n = new QSGGeometryNode;
n->setOpaqueMaterial(&m_material);
n->setGeometry(&m_geometry);
}

// On every update(), mark the material dirty so the shaders run again
n->markDirty(QSGNode::DirtyMaterial);
// And if colors changed, mark the geometry dirty so the new vertex attributes are sent to the GPU
if (m_geometryDirty) {
n->markDirty(QSGNode::DirtyGeometry);
m_geometryDirty = false;
}

return n;
}
50 changes: 48 additions & 2 deletions src/controls/src/flatmesh.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,39 @@
#include <QQuickItem>
#include <QSGNode>
#include <QColor>
#include <QTimer>
#include <QVariantAnimation>
#include <QSGMaterial>

// This is the scene graph material used by FlatMesh. It just creates the Shader object and holds values for some of the uniforms
class SGFlatMeshMaterial : public QSGMaterial
{
public:
// Start the animation at a random point. Disable SceneGraph optimizations that assume our vertex coordinates to be in pixels
SGFlatMeshMaterial() : m_loopNb(random()) { setFlag(QSGMaterial::RequiresFullMatrix); }
int compare(const QSGMaterial *other) const override { return 0; }
void setScreenScaleFactor(float screenScaleFactor) { m_screenScaleFactor = screenScaleFactor; }
float screenScaleFactor() { return m_screenScaleFactor; }
void setShiftMix(float shiftMix) { m_shiftMix = shiftMix; }
float shiftMix() { return m_shiftMix; }
void setSize(float width, float height) { m_width = width; m_height = height; }
float width() { return m_width; }
float height() { return m_height; }
void incrementLoopNb() { m_loopNb++; }
int loopNb() { return m_loopNb; }
protected:
QSGMaterialType *type() const override { static QSGMaterialType type; return &type; }
QSGMaterialShader *createShader() const override;
private:
float m_screenScaleFactor;
float m_shiftMix;
float m_width;
float m_height;
int m_loopNb;
};

struct FlatMeshVertex;

// The QtQuick item per-se, this is the highest level construct that exposes properties to QML
class FlatMesh : public QQuickItem
{
Q_OBJECT
Expand All @@ -48,6 +79,10 @@ class FlatMesh : public QQuickItem
bool getAnimated() const { return m_animated; }
void setAnimated(bool animated);

// As an optimization for color animations, make it possible to change the two colors
// on one call. Then, updateColors() will only run once saving some CPU cycles
Q_INVOKABLE void setColors(QColor center, QColor outer);

QColor getCenterColor() const { return m_centerColor; }
void setCenterColor(QColor c);

Expand All @@ -59,14 +94,25 @@ class FlatMesh : public QQuickItem

protected:
QSGNode *updatePaintNode(QSGNode *node, UpdatePaintNodeData *data);
void geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) override;

private slots:
void maybeEnableAnimation();
void updateColors();

private:
QColor m_centerColor, m_outerColor;
bool m_animated;
QTimer m_timer;
QVariantAnimation m_animation;
// Note: the Qt documentation says "It is crucial that [...] interaction with the scene
// graph happens exclusively on the render thread, primarily during the updatePaintNode()
// call. The rule of thumb is to only use classes with the "QSG" prefix inside the
// QQuickItem::updatePaintNode() function."
// However, no hell broke loose for instantiating these in the FlatMesh constructor so...
// It doesn't look like we're doing anything nasty here but let's keep an eye on it.
SGFlatMeshMaterial m_material;
QSGGeometry m_geometry;
bool m_geometryDirty;
};

#endif // FLATMESH_H
Loading