aboutsummaryrefslogtreecommitdiff
#include "voronoisplat.h"

#include <algorithm>

#include "colormap.h"
#include "scatterplot.h"
#include "shader.h"
// #include "skelft.h"

static const float DEFAULT_ALPHA = 5.0f;
static const float DEFAULT_BETA = 20.0f;

static const char *programVoronoiVertexShader = R"EOF(#version 330
// Single triangle strip quad generated entirely on the vertex shader.
// Simply do glDrawArrays(GL_TRIANGLE_STRIP, 0, 4) and the shader
// generates 4 points from gl_VertexID. No Vertex Attributes are
// required.

precision mediump float;

uniform mat4 transform;

layout(location = 0) in vec2 site_pos;
// layout(location = 1) in vec4 site_color;

out vec2 site;
// out vec4 color;

void main(void)
{
    vec2 uv;
    uv.x = (gl_VertexID & 1);
    uv.y = ((gl_VertexID >> 1) & 1);
    gl_Position = vec4(uv * 2.0 - 1.0, 0.0, 1.0);
    vec4 site4d = transform * vec4(site_pos.xy, 0.0, 1.0);
    // 0.5 otherwise our maps are always scaled twice
    site = 0.5 * site4d.xy;
    // color = site_color;
}
)EOF";

static const char *programVoronoiFragmentShader = R"EOF(#version 330

precision mediump float;

uniform vec2 resolution;
uniform float rad_blur;
uniform float rad_max;

// in vec4 color;
in vec2 site;
out vec4 color;

void main(void) {
    float dt = length(gl_FragCoord.xy - site);
    float maxlen = length(resolution);
    float normalised_dt = dt / maxlen;
    // float radius = rad_max + rad_blur;
    // float normalised_radius = radius / maxlen;
    gl_FragDepth = normalised_dt;
    color = vec4(dt, 0.0, 0.0, 0.0);
}
)EOF";

static const char *program1VertexShader = R"EOF(#version 330

uniform float rad_blur;
uniform float rad_max;
uniform mat4 transform;

in vec2 vert;
in float scalar;

out float value;

void main() {
    gl_PointSize = 2.0 * (rad_max + rad_blur);
    gl_Position = transform * vec4(vert, 0.0, 1.0);
    value = scalar;
}
)EOF";

static const char *program1FragmentShader = R"EOF(#version 330

uniform float rad_blur;
uniform float rad_max;
uniform sampler2D siteDT;

in float value;

layout (location = 0) out vec4 fragColor;

void main() {
    float dt = texelFetch(siteDT, ivec2(gl_FragCoord.xy), 0).r;
    if (dt > rad_max) {
        discard;
    } else {
        vec2 point = gl_PointCoord - vec2(0.5, 0.5);
        float d2 = dot(point, point);
        float radius = rad_max + rad_blur;
        float r2 = 4.0 * d2 * radius * radius;
        float dt_blur = dt + rad_blur;
        float dt_blur2 = dt_blur * dt_blur;
        if (r2 > dt_blur2) {
            discard;
        } else {
            float w = exp(-5.0 * r2 / dt_blur2);
            fragColor = vec4(w * value, w, 0.0, 0.0);
        }
    }
}
)EOF";

static const char *program2VertexShader = R"EOF(#version 330

in vec2 vert;

void main() {
    gl_Position = vec4(vert, 0.0, 1.0);
}
)EOF";

static const char *program2FragmentShader = R"EOF(#version 330

uniform sampler2D siteDT;
uniform sampler2D accumTex;
uniform sampler2D colormap;
uniform float rad_max;

layout (location = 0) out vec4 fragColor;

vec3 getRGB(float value) {
    return texture(colormap, vec2(mix(0.005, 0.995, value), 0)).rgb;
}

void main() {
    float dt = texelFetch(siteDT, ivec2(gl_FragCoord.xy), 0).r;
    if (dt > rad_max) {
        discard;
    } else {
        vec4 accum = texelFetch(accumTex, ivec2(gl_FragCoord.xy), 0);
        // float value = accum.g > 0.0 ? accum.r / accum.g : 0.0;
        float value = (accum.g > 1.0) ? (accum.r - 1.0) / (accum.g - 1.0) : 0.0;
        fragColor = vec4(getRGB(value), 1.0 - dt / rad_max);
    }
}
)EOF";

static int nextPow2(int n)
{
    // TODO: check for overflows
    n--;
    for (int shift = 1; ((n + 1) & n); shift <<= 1) {
        n |= n >> shift;
    }
    return n + 1;
}

VoronoiSplat::VoronoiSplat()
    : m_sx(0.0f, 1.0f, 0.0f, 1.0f)
    , m_sy(0.0f, 1.0f, 0.0f, 1.0f)
    , m_alpha(DEFAULT_ALPHA)
    , m_beta(DEFAULT_BETA)
    , m_sitesChanged(false)
    , m_valuesChanged(false)
    , m_colorScaleChanged(false)
    , m_redraw(false)
{
    std::fill(&m_transform[0][0], &m_transform[0][0] + 16, 0.0f);
    m_transform[3][3] = 1.0f;

    glGenFramebuffers(1, &m_voronoiFBO);
    glGenFramebuffers(1, &m_preFBO);
    glGenFramebuffers(1, &m_FBO);

    setupShaders();
    setupVAOs();
    setupTextures();
}

VoronoiSplat::~VoronoiSplat()
{
    glDeleteFramebuffers(1, &m_FBO);
    glDeleteFramebuffers(1, &m_preFBO);
    glDeleteFramebuffers(1, &m_voronoiFBO);
}

void VoronoiSplat::update()
{
    m_redraw = true;

    if (m_sitesChanged) {
        updateSites();
    }
    if (m_valuesChanged) {
        updateValues();
    }
}

void VoronoiSplat::setSites(const arma::mat &points)
{
    if (points.n_rows < 1 || points.n_cols != 2) {
        return;
    }

    if (m_values.size() > 0 && m_values.size() != points.n_rows) {
        // Old values are no longer valid, clean up
        m_values.assign(points.n_rows, 0);
    }

    // Copy 'points' to internal data structure(s)
    m_sites.resize(points.n_rows);
    const double *col_x = points.colptr(0);
    const double *col_y = points.colptr(1);
    for (unsigned i = 0; i < points.n_rows; i++) {
        m_sites[i].set(col_x[i], col_y[i]);
    }

    setSitesChanged(true);
    update();
}

void VoronoiSplat::setValues(const arma::vec &values)
{
    if (values.n_elem == 0
        || (m_sites.size() != 0 && values.n_elem != m_sites.size())) {
        return;
    }

    m_values.resize(values.n_elem);
    LinearScale<float> scale(values.min(), values.max(), 0, 1.0f);
    std::transform(values.begin(), values.end(), m_values.begin(), scale);
    valuesChanged(values);

    setValuesChanged(true);
    update();
}

void VoronoiSplat::setScale(const LinearScale<float> &sx,
                            const LinearScale<float> &sy)
{
    m_sx = sx;
    m_sy = sy;
    scaleChanged(m_sx, m_sy);
    update();
}

void VoronoiSplat::setColormap(GLuint texture)
{
    m_colormapTex = texture;
    colormapChanged(texture);
    update();
}

void VoronoiSplat::setAlpha(float alpha)
{
    m_alpha = alpha;
    alphaChanged(m_alpha);
    update();
}

void VoronoiSplat::setBeta(float beta)
{
    m_beta = beta;
    betaChanged(m_beta);
    update();
}

void VoronoiSplat::setSize(size_t width, size_t height)
{
    m_width = width;
    m_height = height;

    resizeTextures();
}

void VoronoiSplat::setupShaders()
{
    m_program1 = std::make_unique<Shader>(
        program1VertexShader,
        program1FragmentShader);
    m_program2 = std::make_unique<Shader>(
        program2VertexShader,
        program2FragmentShader);
    m_programVoronoi = std::make_unique<Shader>(
        programVoronoiVertexShader,
        programVoronoiFragmentShader);
}

void VoronoiSplat::setupVAOs()
{
    // sitesVAO: VBOs 0 & 1 are for sites & their values (init'd later)
    glGenBuffers(3, m_VBOs);
    glGenVertexArrays(1, &m_sitesVAO);
    glBindVertexArray(m_sitesVAO);
    glBindBuffer(GL_ARRAY_BUFFER, m_VBOs[0]);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, nullptr);

    glBindBuffer(GL_ARRAY_BUFFER, m_VBOs[1]);
    glEnableVertexAttribArray(1);
    glVertexAttribPointer(1, 1, GL_FLOAT, GL_FALSE, 0, nullptr);
    glBindVertexArray(0);

    // 2ndPassVAO: VBO 2 is a quad mapping the final texture to the framebuffer
    glGenVertexArrays(1, &m_2ndPassVAO);
    glBindVertexArray(m_2ndPassVAO);
    GLfloat verts[] = { -1.0f, -1.0f, -1.0f,  1.0f,
                         1.0f, -1.0f,  1.0f,  1.0f };
    glBindBuffer(GL_ARRAY_BUFFER, m_VBOs[2]);
    glBufferData(GL_ARRAY_BUFFER, sizeof(verts), verts, GL_STATIC_DRAW);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, nullptr);
    glBindVertexArray(0);

    glGenVertexArrays(1, &m_voronoiVAO);
    glBindVertexArray(m_voronoiVAO);
    glGenBuffers(1, &m_dtVBO);
    glBindBuffer(GL_ARRAY_BUFFER, m_dtVBO);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, nullptr);
    glVertexAttribDivisor(0, 1);
    glBindVertexArray(0);
}

void VoronoiSplat::setupTextures()
{
    glGenTextures(2, m_textures);

    // Texture where output is drawn to
    glGenTextures(1, &m_outTex);

    // Voronoi diagram is built by relying on depth tests on GPU
    glGenTextures(1, &m_voronoiDepthTex);
}

void VoronoiSplat::resizeTextures()
{
    // textures[0] stores the DT values for each pixel
    glBindTexture(GL_TEXTURE_2D, m_textures[0]);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, m_width, m_height, 0, GL_RED,
                 GL_FLOAT, 0);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

    // textures[1] is the result of the first pass
    glBindTexture(GL_TEXTURE_2D, m_textures[1]);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, m_width, m_height, 0, GL_RGBA,
                 GL_FLOAT, 0);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glBindTexture(GL_TEXTURE_2D, 0);

    glBindTexture(GL_TEXTURE_2D, m_voronoiDepthTex);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, m_width, m_height, 0,
                 GL_DEPTH_COMPONENT, GL_UNSIGNED_BYTE, nullptr);
    glBindTexture(GL_TEXTURE_2D, 0);

    glBindTexture(GL_TEXTURE_2D, m_outTex);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, m_width, m_height, 0, GL_RGBA,
                 GL_FLOAT, 0);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glBindTexture(GL_TEXTURE_2D, 0);
}

void VoronoiSplat::updateSites()
{
    glBindBuffer(GL_ARRAY_BUFFER, m_VBOs[0]);
    glBufferData(GL_ARRAY_BUFFER, m_sites.size() * sizeof(vec2),
                 m_sites.data(), GL_DYNAMIC_DRAW);

    // Compute DT values for the new positions
    // computeDT();
    float padding = Scatterplot::PADDING;
    m_sx.setRange(static_cast<float>(padding),
                  static_cast<float>(m_width - padding));
    m_sy.setRange(static_cast<float>(m_height - padding),
                  static_cast<float>(padding));
    updateTransform4x4(m_sx, m_sy, m_transform);

    GLint originalFBO;
    glGetIntegerv(GL_FRAMEBUFFER_BINDING, &originalFBO);
    glBindFramebuffer(GL_FRAMEBUFFER, m_voronoiFBO);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
                           m_textures[0], 0);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D,
                           m_voronoiDepthTex, 0);

    glViewport(0, 0, m_width, m_height);
    m_programVoronoi->use();
    m_programVoronoi->setUniform("transform", m_transform);
    m_programVoronoi->setUniform("rad_max", m_beta);
    m_programVoronoi->setUniform("rad_blur", m_alpha);
    GLfloat resolution[] = {
        static_cast<GLfloat>(m_width),
        static_cast<GLfloat>(m_height),
    };
    m_programVoronoi->setUniform2dArray("resolution", resolution, 1);
    glEnable(GL_DEPTH_TEST);
    glEnable(GL_BLEND);
    // glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ZERO);
    glBlendFunc(GL_ONE, GL_ZERO);

    glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glBindVertexArray(m_voronoiVAO);
    glBindBuffer(GL_ARRAY_BUFFER, m_dtVBO);
    glBufferData(GL_ARRAY_BUFFER, m_sites.size() * sizeof(vec2),
                 m_sites.data(), GL_DYNAMIC_DRAW);
    glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, m_sites.size());
    glBindVertexArray(0);
    m_programVoronoi->release();
    glDisable(GL_DEPTH_TEST);
    glBindFramebuffer(GL_FRAMEBUFFER, originalFBO);

    // Update transform used when drawing sites
    updateTransform();

    m_sitesChanged = false;
}

void VoronoiSplat::updateValues()
{
    glBindBuffer(GL_ARRAY_BUFFER, m_VBOs[1]);
    glBufferData(GL_ARRAY_BUFFER, m_values.size() * sizeof(GLfloat),
                 m_values.data(), GL_DYNAMIC_DRAW);

    m_valuesChanged = false;
}

void VoronoiSplat::updateTransform()
{
    float padding = Scatterplot::PADDING;
    float offsetX = padding / m_width, offsetY = padding / m_height;
    updateTransform4x4(m_sx, m_sy, offsetX, offsetY, m_transform);
}

void VoronoiSplat::draw()
{
    if (!m_redraw) {
        return;
    }
    m_redraw = false;

    int originalFBO;
    glGetIntegerv(GL_FRAMEBUFFER_BINDING, &originalFBO);

    // First, we draw to an intermediate texture, which is used as input for the
    // second pass
    glBindFramebuffer(GL_FRAMEBUFFER, m_preFBO);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
                           GL_TEXTURE_2D, m_textures[1], 0);

    glViewport(0, 0, m_width, m_height);
    m_program1->use();
    m_program1->setUniform("rad_max", m_beta);
    m_program1->setUniform("rad_blur", m_alpha);
    m_program1->setUniform("transform", m_transform);

    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, m_textures[0]);
    m_program1->setUniform("siteDT", 0);

    // glEnable(GL_POINT_SPRITE);
    glEnable(GL_PROGRAM_POINT_SIZE);
    glEnable(GL_BLEND);
    glBlendFunc(GL_ONE, GL_ONE);

    glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);

    glBindVertexArray(m_sitesVAO);
    glDrawArrays(GL_POINTS, 0, m_sites.size());
    glBindVertexArray(0);
    m_program1->release();
    glDisable(GL_PROGRAM_POINT_SIZE);

    // Second pass
    m_program2->use();
    m_program2->setUniform("rad_max", m_beta);

    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, m_textures[0]);
    m_program2->setUniform("siteDT", 0);
    glActiveTexture(GL_TEXTURE1);
    glBindTexture(GL_TEXTURE_2D, m_textures[1]);
    m_program2->setUniform("accumTex", 1);
    glActiveTexture(GL_TEXTURE2);
    glBindTexture(GL_TEXTURE_2D, m_colormapTex);
    m_program2->setUniform("colormap", 2);

    glBindFramebuffer(GL_FRAMEBUFFER, m_FBO);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
                           GL_TEXTURE_2D, m_outTex, 0);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    // TODO: know beforehand which color to be used as transparent for
    // blending. We currently assume we always plot to a white
    // background.
    glClearColor(1.0f, 1.0f, 1.0f, 0.0f);
    glClear(GL_COLOR_BUFFER_BIT);

    glBindVertexArray(m_2ndPassVAO);
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
    glBindVertexArray(0);
    m_program2->release();

    /*
    GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
    if (status != GL_FRAMEBUFFER_COMPLETE) {
        checkGLError("draw():");
    }
    */

    glBindFramebuffer(GL_FRAMEBUFFER, originalFBO);
}

// ----------------------------------------------------------------------------
/*
class VoronoiSplatRenderer
    : public QQuickFramebufferObject::Renderer
{
public:
    // 'size' must be square (and power of 2)
    VoronoiSplatRenderer();
    virtual ~VoronoiSplatRenderer();

protected:
    QOpenGLFramebufferObject *createFramebufferObject(const QSize &size);
    void render();
    void synchronize(QQuickFramebufferObject *item);

private:
    void setupShaders();
    void setupVAOs();
    void setupTextures();
    void resizeTextures();

    void updateSites();
    void updateValues();
    void updateColormap();
    void updateTransform();
    void computeDT();

    QSize m_size;
    const std::vector<float> *m_sites, *m_values, *m_cmap;
    float m_alpha, m_beta;
    GLfloat m_transform[4][4];
    LinearScale<float> m_sx, m_sy;

    QQuickWindow *m_window; // used to reset OpenGL state (as per docs)
    QOpenGLFunctions gl;
    QOpenGLShaderProgram *m_program1, *m_program2;
    GLuint m_FBO;
    GLuint m_VBOs[3];
    GLuint m_textures[2], m_colormapTex;
    QOpenGLVertexArrayObject m_sitesVAO, m_2ndPassVAO;
    bool m_sitesChanged, m_valuesChanged, m_colormapChanged;
};

QQuickFramebufferObject::Renderer *VoronoiSplat::createRenderer() const
{
    return new VoronoiSplatRenderer;
}

VoronoiSplatRenderer::VoronoiSplatRenderer()
    : m_sx(0.0f, 1.0f, 0.0f, 1.0f)
    , m_sy(0.0f, 1.0f, 0.0f, 1.0f)
    , gl(QOpenGLContext::currentContext())
{
    std::fill(&m_transform[0][0], &m_transform[0][0] + 16, 0.0f);
    m_transform[3][3] = 1.0f;

    gl.glGenFramebuffers(1, &m_FBO);

    setupShaders();
    setupVAOs();
    setupTextures();
}

void VoronoiSplatRenderer::setupShaders()
{
    m_program1 = new QOpenGLShaderProgram;
    m_program1->addShaderFromSourceCode(QOpenGLShader::Vertex,
R"EOF(#version 440

uniform float rad_blur;
uniform float rad_max;
uniform mat4 transform;

in vec2 vert;
in float scalar;

out float value;

void main() {
  gl_PointSize = 2.0 * (rad_max + rad_blur);
  gl_Position = transform * vec4(vert, 0.0, 1.0);
  value = scalar;
}
)EOF");
    m_program1->addShaderFromSourceCode(QOpenGLShader::Fragment,
R"EOF(#version 440

uniform float rad_blur;
uniform float rad_max;
uniform sampler2D siteDT;

in float value;

layout (location = 0) out vec4 fragColor;

void main() {
  float dt = texelFetch(siteDT, ivec2(gl_FragCoord.xy), 0).r;
  if (dt > rad_max)
    discard;
  else {
    float r = 2.0 * distance(gl_PointCoord, vec2(0.5, 0.5)) * (rad_max + rad_blur);
    float r2 = r * r;
    float rad = dt + rad_blur;
    float rad2 = rad * rad;
    if (r2 > rad2)
      discard;
    else {
      float w = exp(-5.0 * r2 / rad2);
      fragColor = vec4(w * value, w, 0.0, 0.0);
    }
  }
}
)EOF");
    m_program1->link();

    m_program2 = new QOpenGLShaderProgram;
    m_program2->addShaderFromSourceCode(QOpenGLShader::Vertex,
R"EOF(#version 440

in vec2 vert;

void main() {
  gl_Position = vec4(vert, 0.0, 1.0);
}
)EOF");
    m_program2->addShaderFromSourceCode(QOpenGLShader::Fragment,
R"EOF(
#version 440

uniform sampler2D siteDT;
uniform sampler2D accumTex;
uniform sampler2D colormap;
uniform float rad_max;

layout (location = 0) out vec4 fragColor;

vec3 getRGB(float value) {
  return texture(colormap, vec2(mix(0.005, 0.995, value), 0)).rgb;
}

void main() {
  float dt = texelFetch(siteDT, ivec2(gl_FragCoord.xy), 0).r;
  if (dt > rad_max)
    discard;
  else {
    vec4 accum = texelFetch(accumTex, ivec2(gl_FragCoord.xy), 0);
    float value = (accum.g > 1.0) ? (accum.r - 1.0) / (accum.g - 1.0) : 0.0;
    fragColor = vec4(getRGB(value), 1.0 - dt / rad_max);
  }
}
)EOF");
    m_program2->link();
}

void VoronoiSplatRenderer::setupVAOs()
{
    gl.glGenBuffers(3, m_VBOs);

    // sitesVAO: VBOs 0 & 1 are for sites & their values (init'd later)
    m_sitesVAO.create();
    m_sitesVAO.bind();
    gl.glBindBuffer(GL_ARRAY_BUFFER, m_VBOs[0]);
    int vertAttrib = m_program1->attributeLocation("vert");
    gl.glVertexAttribPointer(vertAttrib, 2, GL_FLOAT, GL_FALSE, 0, 0);
    gl.glEnableVertexAttribArray(vertAttrib);

    gl.glBindBuffer(GL_ARRAY_BUFFER, m_VBOs[1]);
    int valueAttrib = m_program1->attributeLocation("scalar");
    gl.glVertexAttribPointer(valueAttrib, 1, GL_FLOAT, GL_FALSE, 0, 0);
    gl.glEnableVertexAttribArray(valueAttrib);
    m_sitesVAO.release();

    // 2ndPassVAO: VBO 2 is a quad mapping the final texture to the framebuffer
    m_2ndPassVAO.create();
    m_2ndPassVAO.bind();
    GLfloat verts[] = { -1.0f, -1.0f, -1.0f,  1.0f,
                         1.0f, -1.0f,  1.0f,  1.0f };
    gl.glBindBuffer(GL_ARRAY_BUFFER, m_VBOs[2]);
    gl.glBufferData(GL_ARRAY_BUFFER, sizeof(verts), verts, GL_STATIC_DRAW);
    vertAttrib = m_program2->attributeLocation("vert");
    gl.glVertexAttribPointer(vertAttrib, 2, GL_FLOAT, GL_FALSE, 0, 0);
    gl.glEnableVertexAttribArray(vertAttrib);
    m_2ndPassVAO.release();
}

void VoronoiSplatRenderer::setupTextures()
{
    gl.glGenTextures(2, m_textures);

    // Used for colorScale lookup in the frag shader
    // (2D texture for compatibility; used to be a 1D texture)
    gl.glGenTextures(1, &m_colormapTex);
    gl.glBindTexture(GL_TEXTURE_2D, m_colormapTex);
    gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
}

VoronoiSplatRenderer::~VoronoiSplatRenderer()
{
    gl.glDeleteBuffers(3, m_VBOs);
    gl.glDeleteTextures(2, m_textures);
    gl.glDeleteTextures(1, &m_colormapTex);

    gl.glDeleteFramebuffers(1, &m_FBO);

    delete m_program1;
    delete m_program2;

    skelft2DDeinitialization();
}

void VoronoiSplatRenderer::resizeTextures()
{
    // textures[0] stores the DT values for each pixel
    gl.glBindTexture(GL_TEXTURE_2D, m_textures[0]);
    gl.glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, m_size.width(),
            m_size.height(), 0, GL_RED, GL_FLOAT, 0);
    gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

    // textures[1] is the result of the first pass
    gl.glBindTexture(GL_TEXTURE_2D, m_textures[1]);
    gl.glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, m_size.width(),
            m_size.height(), 0, GL_RGBA, GL_FLOAT, 0);
    gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
}

QOpenGLFramebufferObject *VoronoiSplatRenderer::createFramebufferObject(const QSize &size)
{
    int baseSize = nextPow2(std::min(size.width(), size.height()));
    m_size.setWidth(baseSize);
    m_size.setHeight(baseSize);
    resizeTextures();

    skelft2DInitialization(m_size.width());

    return QQuickFramebufferObject::Renderer::createFramebufferObject(m_size);
}

void VoronoiSplatRenderer::render()
{
    if (!m_sitesChanged && !m_valuesChanged && !m_colormapChanged) {
        return;
    }

    // Update OpenGL buffers and textures as needed
    if (m_sitesChanged) {
        updateSites();
    }
    if (m_valuesChanged) {
        updateValues();
    }
    if (m_colormapChanged) {
        updateColormap();
    }

    int originalFBO;
    gl.glGetIntegerv(GL_FRAMEBUFFER_BINDING, &originalFBO);

    gl.glBindFramebuffer(GL_FRAMEBUFFER, m_FBO);

    // First pass
    m_program1->bind();
    m_program1->setUniformValue("rad_max", m_beta);
    m_program1->setUniformValue("rad_blur", m_alpha);
    m_program1->setUniformValue("transform", m_transform);

    gl.glActiveTexture(GL_TEXTURE0);
    gl.glBindTexture(GL_TEXTURE_2D, m_textures[0]);
    m_program1->setUniformValue("siteDT", 0);

    gl.glEnable(GL_POINT_SPRITE);
    gl.glEnable(GL_PROGRAM_POINT_SIZE);
    gl.glEnable(GL_BLEND);
    gl.glBlendFunc(GL_ONE, GL_ONE);

    // First, we draw to an intermediate texture, which is used as input for the
    // second pass
    gl.glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
            GL_TEXTURE_2D, m_textures[1], 0);

    gl.glClearColor(1, 1, 1, 1);
    gl.glClear(GL_COLOR_BUFFER_BIT);

    m_sitesVAO.bind();
    gl.glDrawArrays(GL_POINTS, 0, m_values->size());
    m_sitesVAO.release();

    m_program1->release();

    // For some reason this call makes the splat circle of the correct size
    //m_window->resetOpenGLState();

    // Second pass
    m_program2->bind();
    m_program2->setUniformValue("rad_max", m_beta);

    gl.glActiveTexture(GL_TEXTURE0);
    gl.glBindTexture(GL_TEXTURE_2D, m_textures[0]);
    m_program2->setUniformValue("siteDT", 0);
    gl.glActiveTexture(GL_TEXTURE1);
    gl.glBindTexture(GL_TEXTURE_2D, m_textures[1]);
    m_program2->setUniformValue("accumTex", 1);
    gl.glActiveTexture(GL_TEXTURE2);
    gl.glBindTexture(GL_TEXTURE_2D, m_colormapTex);
    m_program2->setUniformValue("colormap", 2);

    gl.glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

    // We now render to the QQuickFramebufferObject's FBO
    gl.glBindFramebuffer(GL_FRAMEBUFFER, originalFBO);

    gl.glClearColor(0, 0, 0, 0);
    gl.glClear(GL_COLOR_BUFFER_BIT);

    m_2ndPassVAO.bind();
    gl.glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
    m_2ndPassVAO.release();

    m_program2->release();

    m_window->resetOpenGLState();
}

void VoronoiSplatRenderer::synchronize(QQuickFramebufferObject *item)
{
    VoronoiSplat *splat = static_cast<VoronoiSplat *>(item);

    m_sitesChanged    = splat->sitesChanged();
    m_valuesChanged   = splat->valuesChanged();
    m_colormapChanged = splat->colorScaleChanged();

    m_sites  = &(splat->sites());
    m_values = &(splat->values());
    m_cmap   = &(splat->colorScale());
    m_sx     = splat->scaleX();
    m_sy     = splat->scaleY();
    m_alpha  = splat->alpha();
    m_beta   = splat->beta();
    m_window = splat->window();

    // Reset so that we have the correct values by the next synchronize()
    splat->setSitesChanged(false);
    splat->setValuesChanged(false);
    splat->setColorScaleChanged(false);
}

void VoronoiSplatRenderer::updateTransform()
{
    GLfloat w = m_size.width(), h = m_size.height();

    GLfloat rangeOffset = Scatterplot::PADDING / w;
    m_sx.setRange(rangeOffset, 1.0f - rangeOffset);
    GLfloat sx = 2.0f * m_sx.slope();
    GLfloat tx = 2.0f * m_sx.offset() - 1.0f;

    rangeOffset = Scatterplot::PADDING / h;
    m_sy.setRange(1.0f - rangeOffset, rangeOffset);
    GLfloat sy = 2.0f * m_sy.slope();
    GLfloat ty = 2.0f * m_sy.offset() - 1.0f;

    // The transform matrix should be this (but transposed -- column major):
    // [   sx  0.0f  0.0f    tx ]
    // [ 0.0f    sy  0.0f    ty ]
    // [ 0.0f  0.0f  0.0f  0.0f ]
    // [ 0.0f  0.0f  0.0f  1.0f ]
    m_transform[0][0] = sx;
    m_transform[1][1] = sy;
    m_transform[3][0] = tx;
    m_transform[3][1] = ty;
}

void VoronoiSplatRenderer::updateSites()
{
    gl.glBindBuffer(GL_ARRAY_BUFFER, m_VBOs[0]);
    gl.glBufferData(GL_ARRAY_BUFFER, m_sites->size() * sizeof(float),
            m_sites->data(), GL_DYNAMIC_DRAW);

    // Compute DT values for the new positions
    computeDT();

    // Update transform used when drawing sites
    updateTransform();

    m_sitesChanged = false;
}

void VoronoiSplatRenderer::updateValues()
{
    gl.glBindBuffer(GL_ARRAY_BUFFER, m_VBOs[1]);
    gl.glBufferData(GL_ARRAY_BUFFER, m_values->size() * sizeof(float),
            m_values->data(), GL_DYNAMIC_DRAW);

    m_valuesChanged = false;
}

void VoronoiSplatRenderer::updateColormap()
{
    gl.glBindTexture(GL_TEXTURE_2D, m_colormapTex);
    gl.glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, m_cmap->size() / 3, 1, 0, GL_RGB,
            GL_FLOAT, m_cmap->data());

    m_colormapChanged = false;
}

void VoronoiSplatRenderer::computeDT()
{
    int w = m_size.width(), h = m_size.height();

    // Compute FT of the sites
    m_sx.setRange(Scatterplot::PADDING, w - Scatterplot::PADDING);
    m_sy.setRange(h - Scatterplot::PADDING, Scatterplot::PADDING);
    const std::vector<float> &sites = *m_sites;
    std::vector<float> buf(w*h);
    for (unsigned i = 0; i < sites.size(); i += 2) {
        int x = int(m_sx(sites[i]));
        int y = int(m_sy(sites[i + 1]));
        if (x < 0 || x >= w || y < 0 || y >= h) {
            // point out of bounds
            continue;
        }

        buf[x + y*w] = i/2.0f + 1.0f;
    }
    skelft2DFT(0, buf.data(), 0, 0, w, h, w);

    // Compute DT of the sites (from the resident FT)
    skelft2DDT(buf.data(), 0, 0, w, h);

    // Upload result to lookup texture
    gl.glActiveTexture(GL_TEXTURE0);
    gl.glBindTexture(GL_TEXTURE_2D, m_textures[0]);
    gl.glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, w, h, GL_RED, GL_FLOAT,
            buf.data());
}
*/