/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the Qt Data Visualization module of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:GPL$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 or (at your option) any later version
** approved by the KDE Free Qt Foundation. The licenses are as published by
** the Free Software Foundation and appearing in the file LICENSE.GPL3
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/

#include "surface3drenderer_p.h"
#include "q3dcamera_p.h"
#include "shaderhelper_p.h"
#include "texturehelper_p.h"
#include "utils_p.h"

#include <QtCore/qmath.h>

static const int ID_TO_RGBA_MASK = 0xff;

QT_BEGIN_NAMESPACE_DATAVISUALIZATION

//#define SHOW_DEPTH_TEXTURE_SCENE

const GLfloat sliceZScale = 0.1f;
const GLfloat sliceUnits = 2.5f;
const uint greenMultiplier = 256;
const uint blueMultiplier = 65536;
const uint alphaMultiplier = 16777216;

Surface3DRenderer::Surface3DRenderer(Surface3DController *controller)
    : Abstract3DRenderer(controller),
      m_cachedIsSlicingActivated(false),
      m_depthShader(0),
      m_backgroundShader(0),
      m_surfaceFlatShader(0),
      m_surfaceSmoothShader(0),
      m_surfaceTexturedSmoothShader(0),
      m_surfaceTexturedFlatShader(0),
      m_surfaceGridShader(0),
      m_surfaceSliceFlatShader(0),
      m_surfaceSliceSmoothShader(0),
      m_selectionShader(0),
      m_heightNormalizer(0.0f),
      m_scaleX(0.0f),
      m_scaleY(0.0f),
      m_scaleZ(0.0f),
      m_depthFrameBuffer(0),
      m_selectionFrameBuffer(0),
      m_selectionDepthBuffer(0),
      m_selectionResultTexture(0),
      m_shadowQualityToShader(33.3f),
      m_flatSupported(true),
      m_selectionActive(false),
      m_shadowQualityMultiplier(3),
      m_selectedPoint(Surface3DController::invalidSelectionPosition()),
      m_selectedSeries(0),
      m_clickedPosition(Surface3DController::invalidSelectionPosition()),
      m_selectionTexturesDirty(false),
      m_noShadowTexture(0)
{
    // Check if flat feature is supported
    ShaderHelper tester(this, QStringLiteral(":/shaders/vertexSurfaceFlat"),
                        QStringLiteral(":/shaders/fragmentSurfaceFlat"));
    if (!tester.testCompile()) {
        m_flatSupported = false;
        connect(this, &Surface3DRenderer::flatShadingSupportedChanged,
                controller, &Surface3DController::handleFlatShadingSupportedChange);
        emit flatShadingSupportedChanged(m_flatSupported);
        qWarning() << "Warning: Flat qualifier not supported on your platform's GLSL language."
                      " Requires at least GLSL version 1.2 with GL_EXT_gpu_shader4 extension.";
    }

    initializeOpenGL();
}

Surface3DRenderer::~Surface3DRenderer()
{
    contextCleanup();
    delete m_depthShader;
    delete m_backgroundShader;
    delete m_selectionShader;
    delete m_surfaceFlatShader;
    delete m_surfaceSmoothShader;
    delete m_surfaceTexturedSmoothShader;
    delete m_surfaceTexturedFlatShader;
    delete m_surfaceGridShader;
    delete m_surfaceSliceFlatShader;
    delete m_surfaceSliceSmoothShader;
}

void Surface3DRenderer::contextCleanup()
{
    if (QOpenGLContext::currentContext()) {
        m_textureHelper->glDeleteFramebuffers(1, &m_depthFrameBuffer);
        m_textureHelper->glDeleteRenderbuffers(1, &m_selectionDepthBuffer);
        m_textureHelper->glDeleteFramebuffers(1, &m_selectionFrameBuffer);

        m_textureHelper->deleteTexture(&m_noShadowTexture);
        m_textureHelper->deleteTexture(&m_depthTexture);
        m_textureHelper->deleteTexture(&m_selectionResultTexture);
    }
}

void Surface3DRenderer::initializeOpenGL()
{
    Abstract3DRenderer::initializeOpenGL();

    // Initialize shaders
    initSurfaceShaders();

    if (!m_isOpenGLES) {
        initDepthShader(); // For shadows
        loadGridLineMesh();
    }

    // Init selection shader
    initSelectionShaders();

    // Resize in case we've missed resize events
    // Resize calls initSelectionBuffer and initDepthBuffer, so they don't need to be called here
    handleResize();

    // Load background mesh (we need to be initialized first)
    loadBackgroundMesh();

    // Create texture for no shadows
    QImage image(2, 2, QImage::Format_RGB32);
    image.fill(Qt::white);
    m_noShadowTexture = m_textureHelper->create2DTexture(image, false, true, false, true);
}

void Surface3DRenderer::fixCameraTarget(QVector3D &target)
{
    target.setX(target.x() * m_scaleX);
    target.setY(target.y() * m_scaleY);
    target.setZ(target.z() * -m_scaleZ);
}

void Surface3DRenderer::getVisibleItemBounds(QVector3D &minBounds, QVector3D &maxBounds)
{
    // The inputs are the item bounds in OpenGL coordinates.
    // The outputs limit these bounds to visible ranges, normalized to range [-1, 1]
    // Volume shader flips the Y and Z axes, so we need to set negatives of actual values to those
    float itemRangeX = (maxBounds.x() - minBounds.x());
    float itemRangeY = (maxBounds.y() - minBounds.y());
    float itemRangeZ = (maxBounds.z() - minBounds.z());

    if (minBounds.x() < -m_scaleX)
        minBounds.setX(-1.0f + (2.0f * qAbs(minBounds.x() + m_scaleX) / itemRangeX));
    else
        minBounds.setX(-1.0f);

    if (minBounds.y() < -m_scaleY)
        minBounds.setY(-(-1.0f + (2.0f * qAbs(minBounds.y() + m_scaleY) / itemRangeY)));
    else
        minBounds.setY(1.0f);

    if (minBounds.z() < -m_scaleZ)
        minBounds.setZ(-(-1.0f + (2.0f * qAbs(minBounds.z() + m_scaleZ) / itemRangeZ)));
    else
        minBounds.setZ(1.0f);

    if (maxBounds.x() > m_scaleX)
        maxBounds.setX(1.0f - (2.0f * qAbs(maxBounds.x() - m_scaleX) / itemRangeX));
    else
        maxBounds.setX(1.0f);

    if (maxBounds.y() > m_scaleY)
        maxBounds.setY(-(1.0f - (2.0f * qAbs(maxBounds.y() - m_scaleY) / itemRangeY)));
    else
        maxBounds.setY(-1.0f);

    if (maxBounds.z() > m_scaleZ)
        maxBounds.setZ(-(1.0f - (2.0f * qAbs(maxBounds.z() - m_scaleZ) / itemRangeZ)));
    else
        maxBounds.setZ(-1.0f);
}

void Surface3DRenderer::updateData()
{
    calculateSceneScalingFactors();

    foreach (SeriesRenderCache *baseCache, m_renderCacheList) {
        SurfaceSeriesRenderCache *cache = static_cast<SurfaceSeriesRenderCache *>(baseCache);
        if (cache->isVisible() && cache->dataDirty()) {
            const QSurface3DSeries *currentSeries = cache->series();
            QSurfaceDataProxy *dataProxy = currentSeries->dataProxy();
            const QSurfaceDataArray &array = *dataProxy->array();
            QSurfaceDataArray &dataArray = cache->dataArray();
            QRect sampleSpace;

            // Need minimum of 2x2 array to draw a surface
            if (array.size() >= 2 && array.at(0)->size() >= 2)
                sampleSpace = calculateSampleRect(array);

            bool dimensionsChanged = false;
            if (cache->sampleSpace() != sampleSpace) {
                if (sampleSpace.width() >= 2)
                    m_selectionTexturesDirty = true;

                dimensionsChanged = true;
                cache->setSampleSpace(sampleSpace);

                for (int i = 0; i < dataArray.size(); i++)
                    delete dataArray.at(i);
                dataArray.clear();
            }

            if (sampleSpace.width() >= 2 && sampleSpace.height() >= 2) {
                if (dimensionsChanged) {
                    dataArray.reserve(sampleSpace.height());
                    for (int i = 0; i < sampleSpace.height(); i++)
                        dataArray << new QSurfaceDataRow(sampleSpace.width());
                }
                for (int i = 0; i < sampleSpace.height(); i++) {
                    for (int j = 0; j < sampleSpace.width(); j++) {
                        (*(dataArray.at(i)))[j] = array.at(i + sampleSpace.y())->at(
                                    j + sampleSpace.x());
                    }
                }

                checkFlatSupport(cache);
                updateObjects(cache, dimensionsChanged);
                cache->setFlatStatusDirty(false);
            } else {
                cache->surfaceObject()->clear();
            }
            cache->setDataDirty(false);
        }
    }

    if (m_selectionTexturesDirty && m_cachedSelectionMode > QAbstract3DGraph::SelectionNone)
        updateSelectionTextures();

    updateSelectedPoint(m_selectedPoint, m_selectedSeries);
}

void Surface3DRenderer::updateSeries(const QList<QAbstract3DSeries *> &seriesList)
{
    Abstract3DRenderer::updateSeries(seriesList);

    bool noSelection = true;
    foreach (QAbstract3DSeries *series, seriesList) {
        QSurface3DSeries *surfaceSeries = static_cast<QSurface3DSeries *>(series);
        SurfaceSeriesRenderCache *cache =
                static_cast<SurfaceSeriesRenderCache *>( m_renderCacheList.value(series));
        if (noSelection
                && surfaceSeries->selectedPoint() != QSurface3DSeries::invalidSelectionPosition()) {
            if (selectionLabel() != cache->itemLabel())
                m_selectionLabelDirty = true;
            noSelection = false;
        }

        if (cache->isFlatStatusDirty() && cache->sampleSpace().width()) {
            checkFlatSupport(cache);
            updateObjects(cache, true);
            cache->setFlatStatusDirty(false);
        }
    }

    if (noSelection && !selectionLabel().isEmpty()) {
        m_selectionLabelDirty = true;
        updateSelectedPoint(Surface3DController::invalidSelectionPosition(), 0);
    }

    // Selection pointer issues
    if (m_selectedSeries) {
        foreach (SeriesRenderCache *baseCache, m_renderCacheList) {
            SurfaceSeriesRenderCache *cache = static_cast<SurfaceSeriesRenderCache *>(baseCache);
            QVector4D highlightColor =
                    Utils::vectorFromColor(cache->series()->singleHighlightColor());
            SelectionPointer *slicePointer = cache->sliceSelectionPointer();
            if (slicePointer) {
                slicePointer->setHighlightColor(highlightColor);
                slicePointer->setPointerObject(cache->object());
                slicePointer->setRotation(cache->meshRotation());
            }
            SelectionPointer *mainPointer = cache->mainSelectionPointer();
            if (mainPointer) {
                mainPointer->setHighlightColor(highlightColor);
                mainPointer->setPointerObject(cache->object());
                mainPointer->setRotation(cache->meshRotation());
            }
        }
    }
}

void Surface3DRenderer::updateSurfaceTextures(QVector<QSurface3DSeries *> seriesList)
{
    foreach (QSurface3DSeries *series, seriesList) {
        SurfaceSeriesRenderCache *cache =
                static_cast<SurfaceSeriesRenderCache *>(m_renderCacheList.value(series));
        if (cache) {
            GLuint oldTexture = cache->surfaceTexture();
            m_textureHelper->deleteTexture(&oldTexture);
            cache->setSurfaceTexture(0);

            const QSurface3DSeries *currentSeries = cache->series();
            QSurfaceDataProxy *dataProxy = currentSeries->dataProxy();
            const QSurfaceDataArray &array = *dataProxy->array();

            if (!series->texture().isNull()) {
                GLuint texId = m_textureHelper->create2DTexture(series->texture(),
                                                                true, true, true, true);
                glBindTexture(GL_TEXTURE_2D, texId);
                glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
                glBindTexture(GL_TEXTURE_2D, 0);
                cache->setSurfaceTexture(texId);

                if (cache->isFlatShadingEnabled())
                    cache->surfaceObject()->coarseUVs(array, cache->dataArray());
                else
                    cache->surfaceObject()->smoothUVs(array, cache->dataArray());
            }
        }
    }
}

SeriesRenderCache *Surface3DRenderer::createNewCache(QAbstract3DSeries *series)
{
    m_selectionTexturesDirty = true;
    return new SurfaceSeriesRenderCache(series, this);
}

void Surface3DRenderer::cleanCache(SeriesRenderCache *cache)
{
    Abstract3DRenderer::cleanCache(cache);
    m_selectionTexturesDirty = true;
}

void Surface3DRenderer::updateRows(const QVector<Surface3DController::ChangeRow> &rows)
{
    foreach (Surface3DController::ChangeRow item, rows) {
        SurfaceSeriesRenderCache *cache =
                static_cast<SurfaceSeriesRenderCache *>(m_renderCacheList.value(item.series));
        QSurfaceDataArray &dstArray = cache->dataArray();
        const QRect &sampleSpace = cache->sampleSpace();

        const QSurfaceDataArray *srcArray = 0;
        QSurfaceDataProxy *dataProxy = item.series->dataProxy();
        if (dataProxy)
            srcArray = dataProxy->array();

        if (cache && srcArray->size() >= 2 && srcArray->at(0)->size() >= 2 &&
                sampleSpace.width() >= 2 && sampleSpace.height() >= 2) {
            bool updateBuffers = false;
            int sampleSpaceTop = sampleSpace.y() + sampleSpace.height();
            int row = item.row;
            if (row >= sampleSpace.y() && row <= sampleSpaceTop) {
                updateBuffers = true;
                for (int j = 0; j < sampleSpace.width(); j++) {
                    (*(dstArray.at(row - sampleSpace.y())))[j] =
                            srcArray->at(row)->at(j + sampleSpace.x());
                }

                if (cache->isFlatShadingEnabled()) {
                    cache->surfaceObject()->updateCoarseRow(dstArray, row - sampleSpace.y(),
                                                            m_polarGraph);
                } else {
                    cache->surfaceObject()->updateSmoothRow(dstArray, row - sampleSpace.y(),
                                                            m_polarGraph);
                }
            }
            if (updateBuffers)
                cache->surfaceObject()->uploadBuffers();
        }
    }

    updateSelectedPoint(m_selectedPoint, m_selectedSeries);
}

void Surface3DRenderer::updateItems(const QVector<Surface3DController::ChangeItem> &points)
{
    foreach (Surface3DController::ChangeItem item, points) {
        SurfaceSeriesRenderCache *cache =
                static_cast<SurfaceSeriesRenderCache *>(m_renderCacheList.value(item.series));
        QSurfaceDataArray &dstArray = cache->dataArray();
        const QRect &sampleSpace = cache->sampleSpace();

        const QSurfaceDataArray *srcArray = 0;
        QSurfaceDataProxy *dataProxy = item.series->dataProxy();
        if (dataProxy)
            srcArray = dataProxy->array();

        if (cache && srcArray->size() >= 2 && srcArray->at(0)->size() >= 2 &&
                sampleSpace.width() >= 2 && sampleSpace.height() >= 2) {
            int sampleSpaceTop = sampleSpace.y() + sampleSpace.height();
            int sampleSpaceRight = sampleSpace.x() + sampleSpace.width();
            bool updateBuffers = false;
            // Note: Point is (row, column), samplespace is (columns x rows)
            QPoint point = item.point;

            if (point.x() <= sampleSpaceTop && point.x() >= sampleSpace.y() &&
                    point.y() <= sampleSpaceRight && point.y() >= sampleSpace.x()) {
                updateBuffers = true;
                int x = point.y() - sampleSpace.x();
                int y = point.x() - sampleSpace.y();
                (*(dstArray.at(y)))[x] = srcArray->at(point.x())->at(point.y());

                if (cache->isFlatShadingEnabled())
                    cache->surfaceObject()->updateCoarseItem(dstArray, y, x, m_polarGraph);
                else
                    cache->surfaceObject()->updateSmoothItem(dstArray, y, x, m_polarGraph);
            }
            if (updateBuffers)
                cache->surfaceObject()->uploadBuffers();
        }

    }

    updateSelectedPoint(m_selectedPoint, m_selectedSeries);
}

void Surface3DRenderer::updateSliceDataModel(const QPoint &point)
{
    foreach (SeriesRenderCache *baseCache, m_renderCacheList)
        static_cast<SurfaceSeriesRenderCache *>(baseCache)->sliceSurfaceObject()->clear();

    if (m_cachedSelectionMode.testFlag(QAbstract3DGraph::SelectionMultiSeries)) {
        // Find axis coordinates for the selected point
        SeriesRenderCache *selectedCache =
                m_renderCacheList.value(const_cast<QSurface3DSeries *>(m_selectedSeries));
        QSurfaceDataArray &dataArray =
                static_cast<SurfaceSeriesRenderCache *>(selectedCache)->dataArray();
        QSurfaceDataItem item = dataArray.at(point.x())->at(point.y());
        QPointF coords(item.x(), item.z());

        foreach (SeriesRenderCache *baseCache, m_renderCacheList) {
            SurfaceSeriesRenderCache *cache = static_cast<SurfaceSeriesRenderCache *>(baseCache);
            if (cache->series() != m_selectedSeries) {
                QPoint mappedPoint = mapCoordsToSampleSpace(cache, coords);
                updateSliceObject(cache, mappedPoint);
            } else {
                updateSliceObject(cache, point);
            }
        }
    } else {
        if (m_selectedSeries) {
            SurfaceSeriesRenderCache *cache =
                    static_cast<SurfaceSeriesRenderCache *>(
                        m_renderCacheList.value(m_selectedSeries));
            if (cache)
                updateSliceObject(static_cast<SurfaceSeriesRenderCache *>(cache), point);
        }
    }
}

QPoint Surface3DRenderer::mapCoordsToSampleSpace(SurfaceSeriesRenderCache *cache,
                                                 const QPointF &coords)
{
    QPoint point(-1, -1);

    QSurfaceDataArray &dataArray = cache->dataArray();
    int top = dataArray.size() - 1;
    int right = dataArray.at(top)->size() - 1;
    QSurfaceDataItem itemBottomLeft = dataArray.at(0)->at(0);
    QSurfaceDataItem itemTopRight = dataArray.at(top)->at(right);

    if (itemBottomLeft.x() <= coords.x() && itemTopRight.x() >= coords.x()) {
        float modelX = coords.x() - itemBottomLeft.x();
        float spanX = itemTopRight.x() - itemBottomLeft.x();
        float stepX = spanX / float(right);
        int sampleX = int((modelX + (stepX / 2.0f)) / stepX);

        QSurfaceDataItem item = dataArray.at(0)->at(sampleX);
        if (!::qFuzzyCompare(float(coords.x()), item.x())) {
            int direction = 1;
            if (item.x() > coords.x())
                direction = -1;

            findMatchingColumn(coords.x(), sampleX, direction, dataArray);
        }

        if (sampleX >= 0 && sampleX <= right)
            point.setY(sampleX);
    }

    if (itemBottomLeft.z() <= coords.y() && itemTopRight.z() >= coords.y()) {
        float modelY = coords.y() - itemBottomLeft.z();
        float spanY = itemTopRight.z() - itemBottomLeft.z();
        float stepY = spanY / float(top);
        int sampleY = int((modelY + (stepY / 2.0f)) / stepY);

        QSurfaceDataItem item = dataArray.at(sampleY)->at(0);
        if (!::qFuzzyCompare(float(coords.y()), item.z())) {
            int direction = 1;
            if (item.z() > coords.y())
                direction = -1;

            findMatchingRow(coords.y(), sampleY, direction, dataArray);
        }

        if (sampleY >= 0 && sampleY <= top)
            point.setX(sampleY);
    }

    return point;
}

void Surface3DRenderer::findMatchingRow(float z, int &sample, int direction,
                                        QSurfaceDataArray &dataArray)
{
    int maxZ = dataArray.size() - 1;
    QSurfaceDataItem item = dataArray.at(sample)->at(0);
    float distance = qAbs(z - item.z());
    int newSample = sample + direction;
    while (newSample >= 0 && newSample <= maxZ) {
        item = dataArray.at(newSample)->at(0);
        float newDist = qAbs(z - item.z());
        if (newDist < distance) {
            sample = newSample;
            distance = newDist;
        } else {
            break;
        }
        newSample = sample + direction;
    }
}

void Surface3DRenderer::findMatchingColumn(float x, int &sample, int direction,
                                           QSurfaceDataArray &dataArray)
{
    int maxX = dataArray.at(0)->size() - 1;
    QSurfaceDataItem item = dataArray.at(0)->at(sample);
    float distance = qAbs(x - item.x());
    int newSample = sample + direction;
    while (newSample >= 0 && newSample <= maxX) {
        item = dataArray.at(0)->at(newSample);
        float newDist = qAbs(x - item.x());
        if (newDist < distance) {
            sample = newSample;
            distance = newDist;
        } else {
            break;
        }
        newSample = sample + direction;
    }
}

void Surface3DRenderer::updateSliceObject(SurfaceSeriesRenderCache *cache, const QPoint &point)
{
    int column = point.y();
    int row = point.x();

    if ((m_cachedSelectionMode.testFlag(QAbstract3DGraph::SelectionRow) && row == -1) ||
            (m_cachedSelectionMode.testFlag(QAbstract3DGraph::SelectionColumn) && column == -1)) {
        cache->sliceSurfaceObject()->clear();
        return;
    }

    QSurfaceDataArray &sliceDataArray = cache->sliceDataArray();
    for (int i = 0; i < sliceDataArray.size(); i++)
        delete sliceDataArray.at(i);
    sliceDataArray.clear();
    sliceDataArray.reserve(2);

    QSurfaceDataRow *sliceRow;
    QSurfaceDataArray &dataArray = cache->dataArray();
    float adjust = (0.025f * m_heightNormalizer) / 2.0f;
    float doubleAdjust = 2.0f * adjust;
    bool flipZX = false;
    float zBack;
    float zFront;
    if (m_cachedSelectionMode.testFlag(QAbstract3DGraph::SelectionRow)) {
        QSurfaceDataRow *src = dataArray.at(row);
        sliceRow = new QSurfaceDataRow(src->size());
        zBack = m_axisCacheZ.min();
        zFront = m_axisCacheZ.max();
        for (int i = 0; i < sliceRow->size(); i++)
            (*sliceRow)[i].setPosition(QVector3D(src->at(i).x(), src->at(i).y() + adjust, zFront));
    } else {
        flipZX = true;
        const QRect &sampleSpace = cache->sampleSpace();
        sliceRow = new QSurfaceDataRow(sampleSpace.height());
        zBack = m_axisCacheX.min();
        zFront = m_axisCacheX.max();
        for (int i = 0; i < sampleSpace.height(); i++) {
            (*sliceRow)[i].setPosition(QVector3D(dataArray.at(i)->at(column).z(),
                                                 dataArray.at(i)->at(column).y() + adjust,
                                                 zFront));
        }
    }
    sliceDataArray << sliceRow;

    // Make a duplicate, so that we get a little bit depth
    QSurfaceDataRow *duplicateRow = new QSurfaceDataRow(*sliceRow);
    for (int i = 0; i < sliceRow->size(); i++) {
        (*sliceRow)[i].setPosition(QVector3D(sliceRow->at(i).x(),
                                             sliceRow->at(i).y() - doubleAdjust,
                                             zBack));
    }
    sliceDataArray << duplicateRow;

    QRect sliceRect(0, 0, sliceRow->size(), 2);
    if (sliceRow->size() > 0) {
        if (cache->isFlatShadingEnabled()) {
            cache->sliceSurfaceObject()->setUpData(sliceDataArray, sliceRect, true, false, flipZX);
        } else {
            cache->sliceSurfaceObject()->setUpSmoothData(sliceDataArray, sliceRect, true, false,
                                                         flipZX);
        }
    }
}

inline static float getDataValue(const QSurfaceDataArray &array, bool searchRow, int index)
{
    if (searchRow)
        return array.at(0)->at(index).x();
    else
        return array.at(index)->at(0).z();
}

inline static int binarySearchArray(const QSurfaceDataArray &array, int maxIdx, float limitValue,
                                    bool searchRow, bool lowBound, bool ascending)
{
    int min = 0;
    int max = maxIdx;
    int mid = 0;
    int retVal;
    while (max >= min) {
        mid = (min + max) / 2;
        float arrayValue = getDataValue(array, searchRow, mid);
        if (arrayValue == limitValue)
            return mid;
        if (ascending) {
            if (arrayValue < limitValue)
                min = mid + 1;
            else
                max = mid - 1;
        } else {
            if (arrayValue > limitValue)
                min = mid + 1;
            else
                max = mid - 1;
        }
    }

    // Exact match not found, return closest depending on bound.
    // The boundary is between last mid and min/max.
    if (lowBound == ascending) {
        if (mid > max)
            retVal = mid;
        else
            retVal = min;
    } else {
        if (mid > max)
            retVal = max;
        else
            retVal = mid;
    }
    if (retVal < 0 || retVal > maxIdx) {
        retVal = -1;
    } else if (lowBound) {
        if (getDataValue(array, searchRow, retVal) < limitValue)
            retVal = -1;
    } else {
        if (getDataValue(array, searchRow, retVal) > limitValue)
            retVal = -1;
    }
    return retVal;
}

QRect Surface3DRenderer::calculateSampleRect(const QSurfaceDataArray &array)
{
    QRect sampleSpace;

    const int maxRow = array.size() - 1;
    const int maxColumn = array.at(0)->size() - 1;

    // We assume data is ordered sequentially in rows for X-value and in columns for Z-value.
    // Determine if data is ascending or descending in each case.
    const bool ascendingX = array.at(0)->at(0).x() < array.at(0)->at(maxColumn).x();
    const bool ascendingZ = array.at(0)->at(0).z() < array.at(maxRow)->at(0).z();

    int idx = binarySearchArray(array, maxColumn, m_axisCacheX.min(), true, true, ascendingX);
    if (idx != -1) {
        if (ascendingX)
            sampleSpace.setLeft(idx);
        else
            sampleSpace.setRight(idx);
    } else {
        sampleSpace.setWidth(-1); // to indicate nothing needs to be shown
        return sampleSpace;
    }

    idx = binarySearchArray(array, maxColumn, m_axisCacheX.max(), true, false, ascendingX);
    if (idx != -1) {
        if (ascendingX)
            sampleSpace.setRight(idx);
        else
            sampleSpace.setLeft(idx);
    } else {
        sampleSpace.setWidth(-1); // to indicate nothing needs to be shown
        return sampleSpace;
    }

    idx = binarySearchArray(array, maxRow, m_axisCacheZ.min(), false, true, ascendingZ);
    if (idx != -1) {
        if (ascendingZ)
            sampleSpace.setTop(idx);
        else
            sampleSpace.setBottom(idx);
    } else {
        sampleSpace.setWidth(-1); // to indicate nothing needs to be shown
        return sampleSpace;
    }

    idx = binarySearchArray(array, maxRow, m_axisCacheZ.max(), false, false, ascendingZ);
    if (idx != -1) {
        if (ascendingZ)
            sampleSpace.setBottom(idx);
        else
            sampleSpace.setTop(idx);
    } else {
        sampleSpace.setWidth(-1); // to indicate nothing needs to be shown
        return sampleSpace;
    }

    return sampleSpace;
}

void Surface3DRenderer::updateScene(Q3DScene *scene)
{
    Abstract3DRenderer::updateScene(scene);

    if (m_selectionActive
            && m_cachedSelectionMode.testFlag(QAbstract3DGraph::SelectionItem)) {
        m_selectionDirty = true; // Ball may need repositioning if scene changes
    }

    updateSlicingActive(scene->isSlicingActive());
}

void Surface3DRenderer::render(GLuint defaultFboHandle)
{
    // Handle GL state setup for FBO buffers and clearing of the render surface
    Abstract3DRenderer::render(defaultFboHandle);

    if (m_axisCacheX.positionsDirty())
        m_axisCacheX.updateAllPositions();
    if (m_axisCacheY.positionsDirty())
        m_axisCacheY.updateAllPositions();
    if (m_axisCacheZ.positionsDirty())
        m_axisCacheZ.updateAllPositions();

    drawScene(defaultFboHandle);
    if (m_cachedIsSlicingActivated)
        drawSlicedScene();

    // Render selection label
    if (m_selectionActive
            && m_cachedSelectionMode.testFlag(QAbstract3DGraph::SelectionItem)) {
        for (SeriesRenderCache *baseCache: m_renderCacheList) {
            const SurfaceSeriesRenderCache *cache = static_cast<SurfaceSeriesRenderCache *>(baseCache);
            if (cache->slicePointerActive() && cache->renderable() &&
                    m_cachedIsSlicingActivated ) {
                cache->sliceSelectionPointer()->renderSelectionLabel(defaultFboHandle);
            }
            if (cache->mainPointerActive() && cache->renderable()) {
                cache->mainSelectionPointer()->renderSelectionLabel(defaultFboHandle,
                                                                    m_useOrthoProjection);
            }
        }
    }
}

void Surface3DRenderer::drawSlicedScene()
{
    if (m_cachedSelectionMode.testFlag(QAbstract3DGraph::SelectionRow)
            == m_cachedSelectionMode.testFlag(QAbstract3DGraph::SelectionColumn)) {
        qWarning("Invalid selection mode. Either QAbstract3DGraph::SelectionRow or"
                 " QAbstract3DGraph::SelectionColumn must be set before calling"
                 " setSlicingActive(true).");
        return;
    }

    QVector3D lightPos;

    QVector4D lightColor = Utils::vectorFromColor(m_cachedTheme->lightColor());

    // Specify viewport
    glViewport(m_secondarySubViewport.x(),
               m_secondarySubViewport.y(),
               m_secondarySubViewport.width(),
               m_secondarySubViewport.height());

    // Set up projection matrix
    QMatrix4x4 projectionMatrix;

    GLfloat aspect = (GLfloat)m_secondarySubViewport.width()
            / (GLfloat)m_secondarySubViewport.height();
    GLfloat sliceUnitsScaled = sliceUnits / m_autoScaleAdjustment;
    projectionMatrix.ortho(-sliceUnitsScaled * aspect, sliceUnitsScaled * aspect,
                           -sliceUnitsScaled, sliceUnitsScaled,
                           -1.0f, 4.0f);

    // Set view matrix
    QMatrix4x4 viewMatrix;
    viewMatrix.lookAt(QVector3D(0.0f, 0.0f, 1.0f), zeroVector, upVector);

    // Set light position
    lightPos = QVector3D(0.0f, 0.0f, 2.0f);

    QMatrix4x4 projectionViewMatrix = projectionMatrix * viewMatrix;

    const Q3DCamera *activeCamera = m_cachedScene->activeCamera();

    bool rowMode = m_cachedSelectionMode.testFlag(QAbstract3DGraph::SelectionRow);
    AxisRenderCache &sliceCache = rowMode ? m_axisCacheX : m_axisCacheZ;

    GLfloat scaleXBackground = 0.0f;
    if (rowMode) {
        // Don't use the regular margin for polar, as the graph is not going to be to scale anyway,
        // and polar graphs often have quite a bit of margin, resulting in ugly slices.
        if (m_polarGraph)
            scaleXBackground = m_scaleX + 0.1f;
        else
            scaleXBackground = m_scaleXWithBackground;
    } else {
        if (m_polarGraph)
            scaleXBackground = m_scaleZ + 0.1f;
        else
            scaleXBackground = m_scaleZWithBackground;
    }

    // Disable culling to avoid ugly conditionals with reversed axes and data
    glDisable(GL_CULL_FACE);

    if (!m_renderCacheList.isEmpty()) {
        bool drawGrid = false;

        foreach (SeriesRenderCache *baseCache, m_renderCacheList) {
            SurfaceSeriesRenderCache *cache = static_cast<SurfaceSeriesRenderCache *>(baseCache);
            if (cache->sliceSurfaceObject()->indexCount() && cache->renderable()) {
                if (!drawGrid && cache->surfaceGridVisible()) {
                    glEnable(GL_POLYGON_OFFSET_FILL);
                    glPolygonOffset(0.5f, 1.0f);
                    drawGrid = true;
                }

                QMatrix4x4 MVPMatrix;
                QMatrix4x4 modelMatrix;
                QMatrix4x4 itModelMatrix;

                QVector3D scaling(1.0f, 1.0f, sliceZScale);
                modelMatrix.scale(scaling);
                itModelMatrix.scale(scaling);

                MVPMatrix = projectionViewMatrix * modelMatrix;
                cache->setMVPMatrix(MVPMatrix);

                if (cache->surfaceVisible()) {
                    ShaderHelper *surfaceShader = m_surfaceSliceSmoothShader;
                    if (cache->isFlatShadingEnabled())
                        surfaceShader = m_surfaceSliceFlatShader;

                    surfaceShader->bind();

                    GLuint colorTexture = cache->baseUniformTexture();
                    if (cache->colorStyle() == Q3DTheme::ColorStyleUniform) {
                        colorTexture = cache->baseUniformTexture();
                        surfaceShader->setUniformValue(surfaceShader->gradientMin(), 0.0f);
                        surfaceShader->setUniformValue(surfaceShader->gradientHeight(), 0.0f);
                    } else {
                        colorTexture = cache->baseGradientTexture();
                        if (cache->colorStyle() == Q3DTheme::ColorStyleObjectGradient) {
                            float objMin = cache->surfaceObject()->minYValue();
                            float objMax = cache->surfaceObject()->maxYValue();
                            float objRange = objMax - objMin;
                            surfaceShader->setUniformValue(surfaceShader->gradientMin(),
                                                           -(objMin / objRange));
                            surfaceShader->setUniformValue(surfaceShader->gradientHeight(),
                                                           1.0f / objRange);
                        } else {
                            surfaceShader->setUniformValue(surfaceShader->gradientMin(), 0.5f);
                            surfaceShader->setUniformValue(surfaceShader->gradientHeight(),
                                                           1.0f / (m_scaleY * 2.0f));
                        }
                    }

                    // Set shader bindings
                    surfaceShader->setUniformValue(surfaceShader->lightP(), lightPos);
                    surfaceShader->setUniformValue(surfaceShader->view(), viewMatrix);
                    surfaceShader->setUniformValue(surfaceShader->model(), modelMatrix);
                    surfaceShader->setUniformValue(surfaceShader->nModel(),
                                                   itModelMatrix.inverted().transposed());
                    surfaceShader->setUniformValue(surfaceShader->MVP(), MVPMatrix);
                    surfaceShader->setUniformValue(surfaceShader->lightS(), 0.0f);
                    surfaceShader->setUniformValue(surfaceShader->ambientS(),
                                                   m_cachedTheme->ambientLightStrength()
                                                   + m_cachedTheme->lightStrength() / 10.0f);
                    surfaceShader->setUniformValue(surfaceShader->lightColor(), lightColor);

                    m_drawer->drawObject(surfaceShader, cache->sliceSurfaceObject(), colorTexture);
                }
            }
        }

        // Draw surface grid
        if (drawGrid) {
            glDisable(GL_POLYGON_OFFSET_FILL);
            m_surfaceGridShader->bind();
            m_surfaceGridShader->setUniformValue(m_surfaceGridShader->color(),
                                                 Utils::vectorFromColor(m_cachedTheme->gridLineColor()));
            foreach (SeriesRenderCache *baseCache, m_renderCacheList) {
                SurfaceSeriesRenderCache *cache =
                        static_cast<SurfaceSeriesRenderCache *>(baseCache);
                if (cache->sliceSurfaceObject()->indexCount() && cache->isVisible() &&
                        cache->surfaceGridVisible()) {
                    m_surfaceGridShader->setUniformValue(m_surfaceGridShader->MVP(),
                                                         cache->MVPMatrix());
                    m_drawer->drawSurfaceGrid(m_surfaceGridShader, cache->sliceSurfaceObject());
                }
            }
        }
    }

    glEnable(GL_CULL_FACE);
    glCullFace(GL_BACK);

    // Grid lines
    if (m_cachedTheme->isGridEnabled()) {
        ShaderHelper *lineShader;
        if (m_isOpenGLES)
            lineShader = m_selectionShader; // Plain color shader for GL_LINES
        else
            lineShader = m_backgroundShader;

        // Bind line shader
        lineShader->bind();

        // Set unchanging shader bindings
        QVector4D lineColor = Utils::vectorFromColor(m_cachedTheme->gridLineColor());
        lineShader->setUniformValue(lineShader->lightP(), lightPos);
        lineShader->setUniformValue(lineShader->view(), viewMatrix);
        lineShader->setUniformValue(lineShader->color(), lineColor);
        lineShader->setUniformValue(lineShader->ambientS(),
                                    m_cachedTheme->ambientLightStrength()
                                    + m_cachedTheme->lightStrength() / 10.0f);
        lineShader->setUniformValue(lineShader->lightS(), 0.0f);
        lineShader->setUniformValue(lineShader->lightColor(), lightColor);

        // Horizontal lines
        int gridLineCount = m_axisCacheY.gridLineCount();
        if (m_axisCacheY.segmentCount() > 0) {
            QVector3D gridLineScaleX(scaleXBackground, gridLineWidth, gridLineWidth);

            for (int line = 0; line < gridLineCount; line++) {
                QMatrix4x4 modelMatrix;
                QMatrix4x4 MVPMatrix;
                QMatrix4x4 itModelMatrix;

                modelMatrix.translate(0.0f, m_axisCacheY.gridLinePosition(line), -1.0f);

                modelMatrix.scale(gridLineScaleX);
                itModelMatrix.scale(gridLineScaleX);

                MVPMatrix = projectionViewMatrix * modelMatrix;

                // Set the rest of the shader bindings
                lineShader->setUniformValue(lineShader->model(), modelMatrix);
                lineShader->setUniformValue(lineShader->nModel(),
                                            itModelMatrix.inverted().transposed());
                lineShader->setUniformValue(lineShader->MVP(), MVPMatrix);

                // Draw the object
                if (m_isOpenGLES)
                    m_drawer->drawLine(lineShader);
                else
                    m_drawer->drawObject(lineShader, m_gridLineObj);
            }
        }

        // Vertical lines
        QVector3D gridLineScaleY(gridLineWidth, m_scaleYWithBackground, gridLineWidth);

        gridLineCount = sliceCache.gridLineCount();
        for (int line = 0; line < gridLineCount; line++) {
            QMatrix4x4 modelMatrix;
            QMatrix4x4 MVPMatrix;
            QMatrix4x4 itModelMatrix;

            modelMatrix.translate(sliceCache.gridLinePosition(line), 0.0f, -1.0f);
            modelMatrix.scale(gridLineScaleY);
            itModelMatrix.scale(gridLineScaleY);

            if (m_isOpenGLES) {
                modelMatrix.rotate(m_zRightAngleRotation);
                itModelMatrix.rotate(m_zRightAngleRotation);
            }

            MVPMatrix = projectionViewMatrix * modelMatrix;

            // Set the rest of the shader bindings
            lineShader->setUniformValue(lineShader->model(), modelMatrix);
            lineShader->setUniformValue(lineShader->nModel(),
                                        itModelMatrix.inverted().transposed());
            lineShader->setUniformValue(lineShader->MVP(), MVPMatrix);

            // Draw the object
            if (m_isOpenGLES)
                m_drawer->drawLine(lineShader);
            else
                m_drawer->drawObject(lineShader, m_gridLineObj);
        }
    }

    // Draw labels
    m_labelShader->bind();
    glDisable(GL_DEPTH_TEST);
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

    // Y Labels to back wall
    int labelNbr = 0;

    QVector3D positionComp(0.0f, 0.0f, 0.0f);
    QVector3D labelTrans = QVector3D(scaleXBackground + labelMargin, 0.0f, 0.0f);
    int labelCount = m_axisCacheY.labelCount();
    for (int label = 0; label < labelCount; label++) {
        if (m_axisCacheY.labelItems().size() > labelNbr) {
            labelTrans.setY(m_axisCacheY.labelPosition(label));
            const LabelItem &axisLabelItem = *m_axisCacheY.labelItems().at(labelNbr);

            // Draw the label here
            m_dummyRenderItem.setTranslation(labelTrans);
            m_drawer->drawLabel(m_dummyRenderItem, axisLabelItem, viewMatrix, projectionMatrix,
                                positionComp, identityQuaternion, 0, m_cachedSelectionMode,
                                m_labelShader, m_labelObj, activeCamera, true, true,
                                Drawer::LabelMid, Qt::AlignLeft, true);
        }
        labelNbr++;
    }

    // X Labels to ground
    int countLabelItems = sliceCache.labelItems().size();

    QVector3D rotation(0.0f, 0.0f, -45.0f);
    QQuaternion totalRotation = Utils::calculateRotation(rotation);

    labelNbr = 0;
    positionComp.setY(-0.1f);
    labelTrans.setY(-m_scaleYWithBackground);
    labelCount = sliceCache.labelCount();
    for (int label = 0; label < labelCount; label++) {
        if (countLabelItems > labelNbr) {
            // Draw the label here
            if (rowMode)
                labelTrans.setX(sliceCache.labelPosition(label));
            else
                labelTrans.setX(-sliceCache.labelPosition(label));

            m_dummyRenderItem.setTranslation(labelTrans);

            LabelItem *axisLabelItem;
            axisLabelItem = sliceCache.labelItems().at(labelNbr);

            m_drawer->drawLabel(m_dummyRenderItem, *axisLabelItem, viewMatrix, projectionMatrix,
                                positionComp, totalRotation, 0, QAbstract3DGraph::SelectionRow,
                                m_labelShader, m_labelObj, activeCamera,
                                false, false, Drawer::LabelBelow,
                                Qt::AlignLeft | Qt::AlignTop, true);
        }
        labelNbr++;
    }

    // Draw labels for axes
    AbstractRenderItem *dummyItem(0);
    positionComp.setY(m_autoScaleAdjustment);
    m_drawer->drawLabel(*dummyItem, sliceCache.titleItem(), viewMatrix, projectionMatrix,
                        positionComp, identityQuaternion, 0, m_cachedSelectionMode, m_labelShader,
                        m_labelObj, activeCamera, false, false, Drawer::LabelBottom,
                        Qt::AlignCenter, true);

    // Y-axis label
    rotation = QVector3D(0.0f, 0.0f, 90.0f);
    totalRotation = Utils::calculateRotation(rotation);
    labelTrans = QVector3D(-scaleXBackground - labelMargin, 0.0f, 0.0f);
    m_dummyRenderItem.setTranslation(labelTrans);
    m_drawer->drawLabel(m_dummyRenderItem, m_axisCacheY.titleItem(), viewMatrix,
                        projectionMatrix, zeroVector, totalRotation, 0,
                        m_cachedSelectionMode, m_labelShader, m_labelObj, activeCamera,
                        false, false, Drawer::LabelMid, Qt::AlignBottom);

    glEnable(GL_DEPTH_TEST);
    glDisable(GL_BLEND);

    // Release shader
    glUseProgram(0);
}

void Surface3DRenderer::drawScene(GLuint defaultFboHandle)
{
    bool noShadows = true;

    GLfloat backgroundRotation = 0;
    QVector4D lightColor = Utils::vectorFromColor(m_cachedTheme->lightColor());

    glViewport(m_primarySubViewport.x(),
               m_primarySubViewport.y(),
               m_primarySubViewport.width(),
               m_primarySubViewport.height());

    // Set up projection matrix
    QMatrix4x4 projectionMatrix;
    GLfloat viewPortRatio = (GLfloat)m_primarySubViewport.width()
            / (GLfloat)m_primarySubViewport.height();
    if (m_useOrthoProjection) {
        GLfloat orthoRatio = 2.0f;
        projectionMatrix.ortho(-viewPortRatio * orthoRatio, viewPortRatio * orthoRatio,
                               -orthoRatio, orthoRatio,
                               0.0f, 100.0f);
    } else {
        projectionMatrix.perspective(45.0f, viewPortRatio, 0.1f, 100.0f);
    }

    const Q3DCamera *activeCamera = m_cachedScene->activeCamera();

    // Calculate view matrix
    QMatrix4x4 viewMatrix = activeCamera->d_ptr->viewMatrix();

    QMatrix4x4 projectionViewMatrix = projectionMatrix * viewMatrix;

    // Calculate flipping indicators
    if (viewMatrix.row(0).x() > 0)
        m_zFlipped = false;
    else
        m_zFlipped = true;
    if (viewMatrix.row(0).z() <= 0)
        m_xFlipped = false;
    else
        m_xFlipped = true;

    m_yFlippedForGrid = m_yFlipped;
    if (m_flipHorizontalGrid) {
        if (!m_useOrthoProjection) {
            // Need to determine if camera is below graph top
            float distanceToCenter = activeCamera->position().length()
                    / activeCamera->zoomLevel() / m_autoScaleAdjustment * 100.0f;
            qreal cameraAngle = qDegreesToRadians(qreal(activeCamera->yRotation()));
            float cameraYPos = float(qSin(cameraAngle)) * distanceToCenter;
            m_yFlippedForGrid = cameraYPos < (m_scaleYWithBackground - m_oldCameraTarget.y());
        } else if (m_useOrthoProjection && activeCamera->yRotation() == 0.0f) {
            // With ortho we only need to flip at angle zero, to fix label autorotation angles
            m_yFlippedForGrid = !m_yFlipped;
        }
    }

    // calculate background rotation based on view matrix rotation
    if (viewMatrix.row(0).x() > 0 && viewMatrix.row(0).z() <= 0)
        backgroundRotation = 270.0f;
    else if (viewMatrix.row(0).x() > 0 && viewMatrix.row(0).z() > 0)
        backgroundRotation = 180.0f;
    else if (viewMatrix.row(0).x() <= 0 && viewMatrix.row(0).z() > 0)
        backgroundRotation = 90.0f;
    else if (viewMatrix.row(0).x() <= 0 && viewMatrix.row(0).z() <= 0)
        backgroundRotation = 0.0f;

    QVector3D lightPos = m_cachedScene->activeLight()->position();

    QMatrix4x4 depthViewMatrix;
    QMatrix4x4 depthProjectionMatrix;
    QMatrix4x4 depthProjectionViewMatrix;

    // Draw depth buffer
    GLfloat adjustedLightStrength = m_cachedTheme->lightStrength() / 10.0f;
    if (!m_isOpenGLES && m_cachedShadowQuality > QAbstract3DGraph::ShadowQualityNone &&
            (!m_renderCacheList.isEmpty() || !m_customRenderCache.isEmpty())) {
        // Render scene into a depth texture for using with shadow mapping
        // Enable drawing to depth framebuffer
        glBindFramebuffer(GL_FRAMEBUFFER, m_depthFrameBuffer);

        // Attach texture to depth attachment
        glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D,
                               m_depthTexture, 0);
        glClear(GL_DEPTH_BUFFER_BIT);

        // Bind depth shader
        m_depthShader->bind();

        // Set viewport for depth map rendering. Must match texture size. Larger values give smoother shadows.
        glViewport(0, 0,
                   m_primarySubViewport.width() * m_shadowQualityMultiplier,
                   m_primarySubViewport.height() * m_shadowQualityMultiplier);

        // Get the depth view matrix
        // It may be possible to hack lightPos here if we want to make some tweaks to shadow
        QVector3D depthLightPos = activeCamera->d_ptr->calculatePositionRelativeToCamera(
                    zeroVector, 0.0f, 4.0f / m_autoScaleAdjustment);
        depthViewMatrix.lookAt(depthLightPos, zeroVector, upVector);

        // Set the depth projection matrix
        depthProjectionMatrix.perspective(10.0f, (GLfloat)m_primarySubViewport.width()
                                          / (GLfloat)m_primarySubViewport.height(), 3.0f, 100.0f);
        depthProjectionViewMatrix = depthProjectionMatrix * depthViewMatrix;

        // Surface is not closed, so don't cull anything
        glDisable(GL_CULL_FACE);

        foreach (SeriesRenderCache *baseCache, m_renderCacheList) {
            SurfaceSeriesRenderCache *cache = static_cast<SurfaceSeriesRenderCache *>(baseCache);
            SurfaceObject *object = cache->surfaceObject();
            if (object->indexCount() && cache->surfaceVisible() && cache->isVisible()
                    && cache->sampleSpace().width() >= 2 && cache->sampleSpace().height() >= 2) {
                // No translation nor scaling for surfaces, therefore no modelMatrix
                // Use directly projectionViewMatrix
                m_depthShader->setUniformValue(m_depthShader->MVP(), depthProjectionViewMatrix);

                // 1st attribute buffer : vertices
                glEnableVertexAttribArray(m_depthShader->posAtt());
                glBindBuffer(GL_ARRAY_BUFFER, object->vertexBuf());
                glVertexAttribPointer(m_depthShader->posAtt(), 3, GL_FLOAT, GL_FALSE, 0,
                                      (void *)0);

                // Index buffer
                glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, object->elementBuf());

                // Draw the triangles
                glDrawElements(GL_TRIANGLES, object->indexCount(), GL_UNSIGNED_INT, (void *)0);
            }
        }

        // Free buffers
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
        glBindBuffer(GL_ARRAY_BUFFER, 0);

        glDisableVertexAttribArray(m_depthShader->posAtt());

        glEnable(GL_CULL_FACE);
        glCullFace(GL_FRONT);

        Abstract3DRenderer::drawCustomItems(RenderingDepth, m_depthShader, viewMatrix,
                                            projectionViewMatrix,
                                            depthProjectionViewMatrix, m_depthTexture,
                                            m_shadowQualityToShader);

        // Disable drawing to depth framebuffer (= enable drawing to screen)
        glBindFramebuffer(GL_FRAMEBUFFER, defaultFboHandle);

        // Revert to original viewport
        glViewport(m_primarySubViewport.x(),
                   m_primarySubViewport.y(),
                   m_primarySubViewport.width(),
                   m_primarySubViewport.height());

        // Reset culling to normal
        glEnable(GL_CULL_FACE);
        glCullFace(GL_BACK);
    }

    // Do position mapping when necessary
    if (m_graphPositionQueryPending) {
        QVector3D graphDimensions(m_scaleX, m_scaleY, m_scaleZ);
        queriedGraphPosition(projectionViewMatrix, graphDimensions, defaultFboHandle);
        emit needRender();
    }

    // Draw selection buffer
    if (!m_cachedIsSlicingActivated && (!m_renderCacheList.isEmpty()
                                        || !m_customRenderCache.isEmpty())
            && m_selectionState == SelectOnScene
            && m_cachedSelectionMode > QAbstract3DGraph::SelectionNone
            && m_selectionResultTexture) {
        m_selectionShader->bind();
        glBindFramebuffer(GL_FRAMEBUFFER, m_selectionFrameBuffer);
        glViewport(0,
                   0,
                   m_primarySubViewport.width(),
                   m_primarySubViewport.height());

        glEnable(GL_DEPTH_TEST); // Needed, otherwise the depth render buffer is not used
        glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Needed for clearing the frame buffer
        glDisable(GL_DITHER); // disable dithering, it may affect colors if enabled

        glDisable(GL_CULL_FACE);

        foreach (SeriesRenderCache *baseCache, m_renderCacheList) {
            SurfaceSeriesRenderCache *cache = static_cast<SurfaceSeriesRenderCache *>(baseCache);
            if (cache->surfaceObject()->indexCount() && cache->renderable()) {
                m_selectionShader->setUniformValue(m_selectionShader->MVP(), projectionViewMatrix);

                cache->surfaceObject()->activateSurfaceTexture(false);

                m_drawer->drawObject(m_selectionShader, cache->surfaceObject(),
                                     cache->selectionTexture());
            }
        }
        m_surfaceGridShader->bind();
        Abstract3DRenderer::drawCustomItems(RenderingSelection, m_surfaceGridShader,
                                            viewMatrix,
                                            projectionViewMatrix, depthProjectionViewMatrix,
                                            m_depthTexture, m_shadowQualityToShader);
        drawLabels(true, activeCamera, viewMatrix, projectionMatrix);

        glEnable(GL_DITHER);

        QVector4D clickedColor = Utils::getSelection(m_inputPosition, m_viewport.height());

        glBindFramebuffer(GL_FRAMEBUFFER, defaultFboHandle);

        // Put the RGBA value back to uint
        uint selectionId = uint(clickedColor.x())
                + uint(clickedColor.y()) * greenMultiplier
                + uint(clickedColor.z()) * blueMultiplier
                + uint(clickedColor.w()) * alphaMultiplier;

        m_clickedPosition = selectionIdToSurfacePoint(selectionId);
        m_clickResolved = true;

        emit needRender();

        // Revert to original viewport
        glViewport(m_primarySubViewport.x(),
                   m_primarySubViewport.y(),
                   m_primarySubViewport.width(),
                   m_primarySubViewport.height());
    }

    // Selection handling
    if (m_selectionDirty || m_selectionLabelDirty) {
        QPoint visiblePoint = Surface3DController::invalidSelectionPosition();
        if (m_selectedSeries) {
            SurfaceSeriesRenderCache *cache =
                    static_cast<SurfaceSeriesRenderCache *>(
                        m_renderCacheList.value(const_cast<QSurface3DSeries *>(m_selectedSeries)));
            if (cache && m_selectedPoint != Surface3DController::invalidSelectionPosition()) {
                const QRect &sampleSpace = cache->sampleSpace();
                int x = m_selectedPoint.x() - sampleSpace.y();
                int y = m_selectedPoint.y() - sampleSpace.x();
                if (x >= 0 && y >= 0 && x < sampleSpace.height() && y < sampleSpace.width()
                        && cache->dataArray().size()) {
                    visiblePoint = QPoint(x, y);
                }
            }
        }

        if (m_cachedSelectionMode == QAbstract3DGraph::SelectionNone
                || visiblePoint == Surface3DController::invalidSelectionPosition()) {
            m_selectionActive = false;
        } else {
            if (m_cachedIsSlicingActivated)
                updateSliceDataModel(visiblePoint);
            if (m_cachedSelectionMode.testFlag(QAbstract3DGraph::SelectionItem))
                surfacePointSelected(visiblePoint);
            m_selectionActive = true;
        }

        m_selectionDirty = false;
    }

    // Draw the surface
    if (!m_renderCacheList.isEmpty()) {
        // For surface we can see glimpses from underneath
        glDisable(GL_CULL_FACE);

        bool drawGrid = false;

        foreach (SeriesRenderCache *baseCache, m_renderCacheList) {
            SurfaceSeriesRenderCache *cache = static_cast<SurfaceSeriesRenderCache *>(baseCache);
            QMatrix4x4 modelMatrix;
            QMatrix4x4 MVPMatrix;
            QMatrix4x4 itModelMatrix;

#ifdef SHOW_DEPTH_TEXTURE_SCENE
            MVPMatrix = depthProjectionViewMatrix;
#else
            MVPMatrix = projectionViewMatrix;
#endif
            cache->setMVPMatrix(MVPMatrix);

            const QRect &sampleSpace = cache->sampleSpace();
            if (cache->surfaceObject()->indexCount() && cache->isVisible() &&
                    sampleSpace.width() >= 2 && sampleSpace.height() >= 2) {
                noShadows = false;
                if (!drawGrid && cache->surfaceGridVisible()) {
                    glEnable(GL_POLYGON_OFFSET_FILL);
                    glPolygonOffset(0.5f, 1.0f);
                    drawGrid = true;
                }

                if (cache->surfaceVisible()) {
                    ShaderHelper *shader = m_surfaceFlatShader;
                    if (cache->surfaceTexture())
                        shader = m_surfaceTexturedFlatShader;
                    if (!cache->isFlatShadingEnabled()) {
                        shader = m_surfaceSmoothShader;
                        if (cache->surfaceTexture())
                            shader = m_surfaceTexturedSmoothShader;
                    }
                    shader->bind();

                    // Set shader bindings
                    shader->setUniformValue(shader->lightP(), lightPos);
                    shader->setUniformValue(shader->view(), viewMatrix);
                    shader->setUniformValue(shader->model(), modelMatrix);
                    shader->setUniformValue(shader->nModel(),
                                            itModelMatrix.inverted().transposed());
                    shader->setUniformValue(shader->MVP(), MVPMatrix);
                    shader->setUniformValue(shader->ambientS(),
                                            m_cachedTheme->ambientLightStrength());
                    shader->setUniformValue(shader->lightColor(), lightColor);

                    // Set the surface texturing
                    cache->surfaceObject()->activateSurfaceTexture(false);
                    GLuint texture;
                    if (cache->surfaceTexture()) {
                        texture = cache->surfaceTexture();
                        cache->surfaceObject()->activateSurfaceTexture(true);
                    } else {
                        if (cache->colorStyle() == Q3DTheme::ColorStyleUniform) {
                            texture = cache->baseUniformTexture();
                            shader->setUniformValue(shader->gradientMin(), 0.0f);
                            shader->setUniformValue(shader->gradientHeight(), 0.0f);
                        } else {
                            texture = cache->baseGradientTexture();
                            if (cache->colorStyle() == Q3DTheme::ColorStyleObjectGradient) {
                                float objMin = cache->surfaceObject()->minYValue();
                                float objMax = cache->surfaceObject()->maxYValue();
                                float objRange = objMax - objMin;
                                shader->setUniformValue(shader->gradientMin(), -(objMin / objRange));
                                shader->setUniformValue(shader->gradientHeight(), 1.0f / objRange);
                            } else {
                                shader->setUniformValue(shader->gradientMin(), 0.5f);
                                shader->setUniformValue(shader->gradientHeight(),
                                                        1.0f / (m_scaleY * 2.0f));
                            }
                        }
                    }

                    if (!m_isOpenGLES &&
                            m_cachedShadowQuality > QAbstract3DGraph::ShadowQualityNone) {
                        // Set shadow shader bindings
                        QMatrix4x4 depthMVPMatrix = depthProjectionViewMatrix * modelMatrix;
                        shader->setUniformValue(shader->shadowQ(), m_shadowQualityToShader);
                        shader->setUniformValue(shader->depth(), depthMVPMatrix);
                        shader->setUniformValue(shader->lightS(), adjustedLightStrength);

                        // Draw the objects
                        m_drawer->drawObject(shader, cache->surfaceObject(), texture,
                                             m_depthTexture);
                    } else {
                        // Set shadowless shader bindings
                        shader->setUniformValue(shader->lightS(), m_cachedTheme->lightStrength());
                        // Draw the objects
                        m_drawer->drawObject(shader, cache->surfaceObject(), texture);
                    }
                }
            }
        }
        glEnable(GL_CULL_FACE);

        // Draw surface grid
        if (drawGrid) {
            glDisable(GL_POLYGON_OFFSET_FILL);
            m_surfaceGridShader->bind();
            m_surfaceGridShader->setUniformValue(m_surfaceGridShader->color(),
                                                 Utils::vectorFromColor(
                                                     m_cachedTheme->gridLineColor()));
            foreach (SeriesRenderCache *baseCache, m_renderCacheList) {
                SurfaceSeriesRenderCache *cache =
                        static_cast<SurfaceSeriesRenderCache *>(baseCache);
                m_surfaceGridShader->setUniformValue(m_surfaceGridShader->MVP(),
                                                     cache->MVPMatrix());

                const QRect &sampleSpace = cache->sampleSpace();
                if (cache->surfaceObject()->indexCount() && cache->surfaceGridVisible()
                        && cache->isVisible() && sampleSpace.width() >= 2
                        && sampleSpace.height() >= 2) {
                    m_drawer->drawSurfaceGrid(m_surfaceGridShader, cache->surfaceObject());
                }
            }
        }
    }

    // Render selection ball
    if (m_selectionActive
            && m_cachedSelectionMode.testFlag(QAbstract3DGraph::SelectionItem)) {
        for (SeriesRenderCache *baseCache: m_renderCacheList) {
            const SurfaceSeriesRenderCache *cache = static_cast<SurfaceSeriesRenderCache *>(baseCache);
            if (cache->slicePointerActive() && cache->renderable() &&
                    m_cachedIsSlicingActivated ) {
                cache->sliceSelectionPointer()->renderSelectionPointer(defaultFboHandle);
            }
            if (cache->mainPointerActive() && cache->renderable()) {
                cache->mainSelectionPointer()->renderSelectionPointer(defaultFboHandle,
                                                                      m_useOrthoProjection);
            }
        }
    }

    // Bind background shader
    m_backgroundShader->bind();
    glCullFace(GL_BACK);

    // Draw background
    if (m_cachedTheme->isBackgroundEnabled() && m_backgroundObj) {
        QMatrix4x4 modelMatrix;
        QMatrix4x4 MVPMatrix;
        QMatrix4x4 itModelMatrix;

        QVector3D bgScale(m_scaleXWithBackground, m_scaleYWithBackground, m_scaleZWithBackground);
        modelMatrix.scale(bgScale);

        // If we're viewing from below, background object must be flipped
        if (m_yFlipped) {
            modelMatrix.rotate(m_xFlipRotation);
            modelMatrix.rotate(270.0f - backgroundRotation, 0.0f, 1.0f, 0.0f);
        } else {
            modelMatrix.rotate(backgroundRotation, 0.0f, 1.0f, 0.0f);
        }

        itModelMatrix = modelMatrix; // Only scaling and rotations, can be used directly

#ifdef SHOW_DEPTH_TEXTURE_SCENE
        MVPMatrix = depthProjectionViewMatrix * modelMatrix;
#else
        MVPMatrix = projectionViewMatrix * modelMatrix;
#endif

        bool blendEnabled = false;
        QVector4D backgroundColor = Utils::vectorFromColor(m_cachedTheme->backgroundColor());
        if (backgroundColor.w() < 1.0f) {
            blendEnabled = true;
            glEnable(GL_BLEND);
            glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
        }

        // Set shader bindings
        m_backgroundShader->setUniformValue(m_backgroundShader->lightP(), lightPos);
        m_backgroundShader->setUniformValue(m_backgroundShader->view(), viewMatrix);
        m_backgroundShader->setUniformValue(m_backgroundShader->model(), modelMatrix);
        m_backgroundShader->setUniformValue(m_backgroundShader->nModel(),
                                            itModelMatrix.inverted().transposed());
        m_backgroundShader->setUniformValue(m_backgroundShader->MVP(), MVPMatrix);
        m_backgroundShader->setUniformValue(m_backgroundShader->color(), backgroundColor);
        m_backgroundShader->setUniformValue(m_backgroundShader->ambientS(),
                                            m_cachedTheme->ambientLightStrength() * 2.0f);
        m_backgroundShader->setUniformValue(m_backgroundShader->lightColor(), lightColor);

        if (m_cachedShadowQuality > QAbstract3DGraph::ShadowQualityNone && !m_isOpenGLES) {
            // Set shadow shader bindings
            QMatrix4x4 depthMVPMatrix = depthProjectionViewMatrix * modelMatrix;
            m_backgroundShader->setUniformValue(m_backgroundShader->shadowQ(),
                                                m_shadowQualityToShader);
            m_backgroundShader->setUniformValue(m_backgroundShader->depth(), depthMVPMatrix);
            m_backgroundShader->setUniformValue(m_backgroundShader->lightS(),
                                                adjustedLightStrength);
            // Draw the object
            if (noShadows && m_customRenderCache.isEmpty())
                m_drawer->drawObject(m_backgroundShader, m_backgroundObj, 0, m_noShadowTexture);
            else
                m_drawer->drawObject(m_backgroundShader, m_backgroundObj, 0, m_depthTexture);
        } else {
            // Set shadowless shader bindings
            m_backgroundShader->setUniformValue(m_backgroundShader->lightS(),
                                                m_cachedTheme->lightStrength());

            // Draw the object
            m_drawer->drawObject(m_backgroundShader, m_backgroundObj);
        }

        if (blendEnabled)
            glDisable(GL_BLEND);
    }

    // Draw grid lines
    QVector3D gridLineScaleX(m_scaleXWithBackground, gridLineWidth, gridLineWidth);
    QVector3D gridLineScaleZ(gridLineWidth, gridLineWidth, m_scaleZWithBackground);
    QVector3D gridLineScaleY(gridLineWidth, m_scaleYWithBackground, gridLineWidth);

    if (m_cachedTheme->isGridEnabled()) {
        ShaderHelper *lineShader;
        if (m_isOpenGLES)
            lineShader = m_surfaceGridShader; // Plain color shader for GL_LINES
        else
            lineShader = m_backgroundShader;

        // Bind line shader
        lineShader->bind();

        // Set unchanging shader bindings
        QVector4D lineColor = Utils::vectorFromColor(m_cachedTheme->gridLineColor());
        lineShader->setUniformValue(lineShader->lightP(), lightPos);
        lineShader->setUniformValue(lineShader->view(), viewMatrix);
        lineShader->setUniformValue(lineShader->color(), lineColor);
        lineShader->setUniformValue(lineShader->ambientS(), m_cachedTheme->ambientLightStrength());
        lineShader->setUniformValue(lineShader->lightColor(), lightColor);
        if (m_cachedShadowQuality > QAbstract3DGraph::ShadowQualityNone && !m_isOpenGLES) {
            // Set shadowed shader bindings
            lineShader->setUniformValue(lineShader->shadowQ(), m_shadowQualityToShader);
            lineShader->setUniformValue(lineShader->lightS(),
                                        m_cachedTheme->lightStrength() / 20.0f);
        } else {
            // Set shadowless shader bindings
            lineShader->setUniformValue(lineShader->lightS(),
                                        m_cachedTheme->lightStrength() / 2.5f);
        }

        QQuaternion lineYRotation;
        QQuaternion lineXRotation;

        if (m_xFlipped)
            lineYRotation = m_yRightAngleRotationNeg;
        else
            lineYRotation = m_yRightAngleRotation;

        if (m_yFlippedForGrid)
            lineXRotation = m_xRightAngleRotation;
        else
            lineXRotation = m_xRightAngleRotationNeg;

        float yFloorLinePosition = -m_scaleYWithBackground + gridLineOffset;
        if (m_yFlipped != m_flipHorizontalGrid)
            yFloorLinePosition = -yFloorLinePosition;

        // Rows (= Z)
        if (m_axisCacheZ.segmentCount() > 0) {
            int gridLineCount = m_axisCacheZ.gridLineCount();
            // Floor lines
            if (m_polarGraph) {
                drawRadialGrid(lineShader, yFloorLinePosition, projectionViewMatrix,
                               depthProjectionViewMatrix);
            } else {
                for (int line = 0; line < gridLineCount; line++) {
                    QMatrix4x4 modelMatrix;
                    QMatrix4x4 MVPMatrix;
                    QMatrix4x4 itModelMatrix;

                    modelMatrix.translate(0.0f, yFloorLinePosition,
                                          m_axisCacheZ.gridLinePosition(line));

                    modelMatrix.scale(gridLineScaleX);
                    itModelMatrix.scale(gridLineScaleX);

                    modelMatrix.rotate(lineXRotation);
                    itModelMatrix.rotate(lineXRotation);

                    MVPMatrix = projectionViewMatrix * modelMatrix;

                    // Set the rest of the shader bindings
                    lineShader->setUniformValue(lineShader->model(), modelMatrix);
                    lineShader->setUniformValue(lineShader->nModel(),
                                                itModelMatrix.inverted().transposed());
                    lineShader->setUniformValue(lineShader->MVP(), MVPMatrix);

                    if (!m_isOpenGLES) {
                        if (m_cachedShadowQuality > QAbstract3DGraph::ShadowQualityNone) {
                            // Set shadow shader bindings
                            QMatrix4x4 depthMVPMatrix = depthProjectionViewMatrix * modelMatrix;
                            lineShader->setUniformValue(lineShader->depth(), depthMVPMatrix);
                            // Draw the object
                            m_drawer->drawObject(lineShader, m_gridLineObj, 0, m_depthTexture);
                        } else {
                            // Draw the object
                            m_drawer->drawObject(lineShader, m_gridLineObj);
                        }
                    } else {
                        m_drawer->drawLine(lineShader);
                    }
                }
                // Side wall lines
                GLfloat lineXTrans = m_scaleXWithBackground - gridLineOffset;

                if (!m_xFlipped)
                    lineXTrans = -lineXTrans;

                for (int line = 0; line < gridLineCount; line++) {
                    QMatrix4x4 modelMatrix;
                    QMatrix4x4 MVPMatrix;
                    QMatrix4x4 itModelMatrix;

                    modelMatrix.translate(lineXTrans, 0.0f, m_axisCacheZ.gridLinePosition(line));

                    modelMatrix.scale(gridLineScaleY);
                    itModelMatrix.scale(gridLineScaleY);

                    if (m_isOpenGLES) {
                        modelMatrix.rotate(m_zRightAngleRotation);
                        itModelMatrix.rotate(m_zRightAngleRotation);
                    } else {
                        modelMatrix.rotate(lineYRotation);
                        itModelMatrix.rotate(lineYRotation);
                    }

                    MVPMatrix = projectionViewMatrix * modelMatrix;

                    // Set the rest of the shader bindings
                    lineShader->setUniformValue(lineShader->model(), modelMatrix);
                    lineShader->setUniformValue(lineShader->nModel(),
                                                itModelMatrix.inverted().transposed());
                    lineShader->setUniformValue(lineShader->MVP(), MVPMatrix);

                    if (!m_isOpenGLES) {
                        if (m_cachedShadowQuality > QAbstract3DGraph::ShadowQualityNone) {
                            // Set shadow shader bindings
                            QMatrix4x4 depthMVPMatrix = depthProjectionViewMatrix * modelMatrix;
                            lineShader->setUniformValue(lineShader->depth(), depthMVPMatrix);
                            // Draw the object
                            m_drawer->drawObject(lineShader, m_gridLineObj, 0, m_depthTexture);
                        } else {
                            // Draw the object
                            m_drawer->drawObject(lineShader, m_gridLineObj);
                        }
                    } else {
                        m_drawer->drawLine(lineShader);
                    }
                }
            }
        }

        // Columns (= X)
        if (m_axisCacheX.segmentCount() > 0) {
            if (m_isOpenGLES)
                lineXRotation = m_yRightAngleRotation;

            // Floor lines
            int gridLineCount = m_axisCacheX.gridLineCount();

            if (m_polarGraph) {
                drawAngularGrid(lineShader, yFloorLinePosition, projectionViewMatrix,
                                depthProjectionViewMatrix);
            } else {
                for (int line = 0; line < gridLineCount; line++) {
                    QMatrix4x4 modelMatrix;
                    QMatrix4x4 MVPMatrix;
                    QMatrix4x4 itModelMatrix;

                    modelMatrix.translate(m_axisCacheX.gridLinePosition(line), yFloorLinePosition,
                                          0.0f);

                    modelMatrix.scale(gridLineScaleZ);
                    itModelMatrix.scale(gridLineScaleZ);

                    modelMatrix.rotate(lineXRotation);
                    itModelMatrix.rotate(lineXRotation);

                    MVPMatrix = projectionViewMatrix * modelMatrix;

                    // Set the rest of the shader bindings
                    lineShader->setUniformValue(lineShader->model(), modelMatrix);
                    lineShader->setUniformValue(lineShader->nModel(),
                                                itModelMatrix.inverted().transposed());
                    lineShader->setUniformValue(lineShader->MVP(), MVPMatrix);

                    if (!m_isOpenGLES) {
                        if (m_cachedShadowQuality > QAbstract3DGraph::ShadowQualityNone) {
                            // Set shadow shader bindings
                            QMatrix4x4 depthMVPMatrix = depthProjectionViewMatrix * modelMatrix;
                            lineShader->setUniformValue(lineShader->depth(), depthMVPMatrix);
                            // Draw the object
                            m_drawer->drawObject(lineShader, m_gridLineObj, 0, m_depthTexture);
                        } else {
                            // Draw the object
                            m_drawer->drawObject(lineShader, m_gridLineObj);
                        }
                    } else {
                        m_drawer->drawLine(lineShader);
                    }
                }

                // Back wall lines
                GLfloat lineZTrans = m_scaleZWithBackground - gridLineOffset;

                if (!m_zFlipped)
                    lineZTrans = -lineZTrans;

                for (int line = 0; line < gridLineCount; line++) {
                    QMatrix4x4 modelMatrix;
                    QMatrix4x4 MVPMatrix;
                    QMatrix4x4 itModelMatrix;

                    modelMatrix.translate(m_axisCacheX.gridLinePosition(line), 0.0f, lineZTrans);

                    modelMatrix.scale(gridLineScaleY);
                    itModelMatrix.scale(gridLineScaleY);

                    if (m_isOpenGLES) {
                        modelMatrix.rotate(m_zRightAngleRotation);
                        itModelMatrix.rotate(m_zRightAngleRotation);
                    } else if (m_zFlipped) {
                        modelMatrix.rotate(m_xFlipRotation);
                        itModelMatrix.rotate(m_xFlipRotation);
                    }

                    MVPMatrix = projectionViewMatrix * modelMatrix;

                    // Set the rest of the shader bindings
                    lineShader->setUniformValue(lineShader->model(), modelMatrix);
                    lineShader->setUniformValue(lineShader->nModel(),
                                                itModelMatrix.inverted().transposed());
                    lineShader->setUniformValue(lineShader->MVP(), MVPMatrix);

                    if (!m_isOpenGLES) {
                        if (m_cachedShadowQuality > QAbstract3DGraph::ShadowQualityNone) {
                            // Set shadow shader bindings
                            QMatrix4x4 depthMVPMatrix = depthProjectionViewMatrix * modelMatrix;
                            lineShader->setUniformValue(lineShader->depth(), depthMVPMatrix);
                            // Draw the object
                            m_drawer->drawObject(lineShader, m_gridLineObj, 0, m_depthTexture);
                        } else {
                            // Draw the object
                            m_drawer->drawObject(lineShader, m_gridLineObj);
                        }
                    } else {
                        m_drawer->drawLine(lineShader);
                    }
                }
            }
        }

        // Horizontal wall lines
        if (m_axisCacheY.segmentCount() > 0) {
            // Back wall
            int gridLineCount = m_axisCacheY.gridLineCount();

            GLfloat lineZTrans = m_scaleZWithBackground - gridLineOffset;

            if (!m_zFlipped)
                lineZTrans = -lineZTrans;

            for (int line = 0; line < gridLineCount; line++) {
                QMatrix4x4 modelMatrix;
                QMatrix4x4 MVPMatrix;
                QMatrix4x4 itModelMatrix;

                modelMatrix.translate(0.0f, m_axisCacheY.gridLinePosition(line), lineZTrans);

                modelMatrix.scale(gridLineScaleX);
                itModelMatrix.scale(gridLineScaleX);

                if (m_zFlipped) {
                    modelMatrix.rotate(m_xFlipRotation);
                    itModelMatrix.rotate(m_xFlipRotation);
                }

                MVPMatrix = projectionViewMatrix * modelMatrix;

                // Set the rest of the shader bindings
                lineShader->setUniformValue(lineShader->model(), modelMatrix);
                lineShader->setUniformValue(lineShader->nModel(),
                                            itModelMatrix.inverted().transposed());
                lineShader->setUniformValue(lineShader->MVP(), MVPMatrix);

                if (!m_isOpenGLES) {
                    if (m_cachedShadowQuality > QAbstract3DGraph::ShadowQualityNone) {
                        // Set shadow shader bindings
                        QMatrix4x4 depthMVPMatrix = depthProjectionViewMatrix * modelMatrix;
                        lineShader->setUniformValue(lineShader->depth(), depthMVPMatrix);
                        // Draw the object
                        m_drawer->drawObject(lineShader, m_gridLineObj, 0, m_depthTexture);
                    } else {
                        // Draw the object
                        m_drawer->drawObject(lineShader, m_gridLineObj);
                    }
                } else {
                    m_drawer->drawLine(lineShader);
                }
            }

            // Side wall
            GLfloat lineXTrans = m_scaleXWithBackground - gridLineOffset;

            if (!m_xFlipped)
                lineXTrans = -lineXTrans;

            for (int line = 0; line < gridLineCount; line++) {
                QMatrix4x4 modelMatrix;
                QMatrix4x4 MVPMatrix;
                QMatrix4x4 itModelMatrix;

                modelMatrix.translate(lineXTrans, m_axisCacheY.gridLinePosition(line), 0.0f);

                modelMatrix.scale(gridLineScaleZ);
                itModelMatrix.scale(gridLineScaleZ);

                modelMatrix.rotate(lineYRotation);
                itModelMatrix.rotate(lineYRotation);

                MVPMatrix = projectionViewMatrix * modelMatrix;

                // Set the rest of the shader bindings
                lineShader->setUniformValue(lineShader->model(), modelMatrix);
                lineShader->setUniformValue(lineShader->nModel(),
                                            itModelMatrix.inverted().transposed());
                lineShader->setUniformValue(lineShader->MVP(), MVPMatrix);

                if (!m_isOpenGLES) {
                    if (m_cachedShadowQuality > QAbstract3DGraph::ShadowQualityNone) {
                        // Set shadow shader bindings
                        QMatrix4x4 depthMVPMatrix = depthProjectionViewMatrix * modelMatrix;
                        lineShader->setUniformValue(lineShader->depth(), depthMVPMatrix);
                        // Draw the object
                        m_drawer->drawObject(lineShader, m_gridLineObj, 0, m_depthTexture);
                    } else {
                        // Draw the object
                        m_drawer->drawObject(lineShader, m_gridLineObj);
                    }
                } else {
                    m_drawer->drawLine(lineShader);
                }
            }
        }
    }

    Abstract3DRenderer::drawCustomItems(RenderingNormal, m_customItemShader, viewMatrix,
                                        projectionViewMatrix, depthProjectionViewMatrix,
                                        m_depthTexture, m_shadowQualityToShader);

    drawLabels(false, activeCamera, viewMatrix, projectionMatrix);

    // Release shader
    glUseProgram(0);
}

void Surface3DRenderer::drawLabels(bool drawSelection, const Q3DCamera *activeCamera,
                                   const QMatrix4x4 &viewMatrix,
                                   const QMatrix4x4 &projectionMatrix)
{
    ShaderHelper *shader = 0;
    GLfloat alphaForValueSelection = labelValueAlpha / 255.0f;
    GLfloat alphaForRowSelection = labelRowAlpha / 255.0f;
    GLfloat alphaForColumnSelection = labelColumnAlpha / 255.0f;
    if (drawSelection) {
        shader = m_surfaceGridShader;
    } else {
        shader = m_labelShader;
        shader->bind();

        glEnable(GL_BLEND);
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    }

    glEnable(GL_POLYGON_OFFSET_FILL);

    float labelAutoAngle = m_axisCacheZ.labelAutoRotation();
    float labelAngleFraction = labelAutoAngle / 90.0f;
    float fractionCamY = activeCamera->yRotation() * labelAngleFraction;
    float fractionCamX = activeCamera->xRotation() * labelAngleFraction;
    float labelsMaxWidth = 0.0f;

    int startIndex;
    int endIndex;
    int indexStep;

    // Z Labels
    QVector3D positionZComp(0.0f, 0.0f, 0.0f);
    if (m_axisCacheZ.segmentCount() > 0) {
        int labelCount = m_axisCacheZ.labelCount();
        float labelXTrans = m_scaleXWithBackground + labelMargin;
        float labelYTrans = m_flipHorizontalGrid ? m_scaleYWithBackground : -m_scaleYWithBackground;
        if (m_polarGraph) {
            labelXTrans *= m_radialLabelOffset;
            // YTrans up only if over background
            if (m_radialLabelOffset < 1.0f)
                labelYTrans += gridLineOffset + gridLineWidth;
        }
        Qt::Alignment alignment = (m_xFlipped == m_zFlipped) ? Qt::AlignLeft : Qt::AlignRight;
        QVector3D labelRotation;
        if (m_xFlipped)
            labelXTrans = -labelXTrans;
        if (m_yFlipped)
            labelYTrans = -labelYTrans;
        if (labelAutoAngle == 0.0f) {
            if (m_zFlipped)
                labelRotation.setY(180.0f);
            if (m_yFlippedForGrid) {
                if (m_zFlipped)
                    labelRotation.setY(180.0f);
                else
                    labelRotation.setY(0.0f);
                labelRotation.setX(90.0f);
            } else {
                labelRotation.setX(-90.0f);
            }
        } else {
            if (m_zFlipped)
                labelRotation.setY(180.0f);
            if (m_yFlippedForGrid) {
                if (m_zFlipped) {
                    if (m_xFlipped) {
                        labelRotation.setX(90.0f - (labelAutoAngle - fractionCamX)
                                           * (-labelAutoAngle - fractionCamY) / labelAutoAngle);
                        labelRotation.setZ(labelAutoAngle + fractionCamY);
                    } else {
                        labelRotation.setX(90.0f + (labelAutoAngle + fractionCamX)
                                           * (labelAutoAngle + fractionCamY) / labelAutoAngle);
                        labelRotation.setZ(-labelAutoAngle - fractionCamY);
                    }
                } else {
                    if (m_xFlipped) {
                        labelRotation.setX(90.0f + (labelAutoAngle - fractionCamX)
                                           * -(labelAutoAngle + fractionCamY) / labelAutoAngle);
                        labelRotation.setZ(-labelAutoAngle - fractionCamY);
                    } else {
                        labelRotation.setX(90.0f - (labelAutoAngle + fractionCamX)
                                           * (labelAutoAngle + fractionCamY) / labelAutoAngle);
                        labelRotation.setZ(labelAutoAngle + fractionCamY);
                    }
                }
            } else {
                if (m_zFlipped) {
                    if (m_xFlipped) {
                        labelRotation.setX(-90.0f + (labelAutoAngle - fractionCamX)
                                           * (-labelAutoAngle + fractionCamY) / labelAutoAngle);
                        labelRotation.setZ(-labelAutoAngle + fractionCamY);
                    } else {
                        labelRotation.setX(-90.0f - (labelAutoAngle + fractionCamX)
                                           * (labelAutoAngle - fractionCamY) / labelAutoAngle);
                        labelRotation.setZ(labelAutoAngle - fractionCamY);
                    }
                } else {
                    if (m_xFlipped) {
                        labelRotation.setX(-90.0f - (labelAutoAngle - fractionCamX)
                                           * (-labelAutoAngle + fractionCamY) / labelAutoAngle);
                        labelRotation.setZ(labelAutoAngle - fractionCamY);
                    } else {
                        labelRotation.setX(-90.0f + (labelAutoAngle + fractionCamX)
                                           * (labelAutoAngle - fractionCamY) / labelAutoAngle);
                        labelRotation.setZ(-labelAutoAngle + fractionCamY);
                    }
                }
            }
        }

        QQuaternion totalRotation = Utils::calculateRotation(labelRotation);

        QVector3D labelTrans = QVector3D(labelXTrans,
                                         labelYTrans,
                                         0.0f);

        if (m_zFlipped) {
            startIndex = 0;
            endIndex = labelCount;
            indexStep = 1;
        } else {
            startIndex = labelCount - 1;
            endIndex = -1;
            indexStep = -1;
        }
        float offsetValue = 0.0f;
        for (int label = startIndex; label != endIndex; label = label + indexStep) {
            glPolygonOffset(offsetValue++ / -10.0f, 1.0f);
            const LabelItem &axisLabelItem = *m_axisCacheZ.labelItems().at(label);
            // Draw the label here
            if (m_polarGraph) {
                float direction = m_zFlipped ? -1.0f : 1.0f;
                labelTrans.setZ((m_axisCacheZ.formatter()->labelPositions().at(label)
                                 * -m_polarRadius
                                 + m_drawer->scaledFontSize() + gridLineWidth) * direction);
            } else {
                labelTrans.setZ(m_axisCacheZ.labelPosition(label));
            }
            if (label == 0 || label == (labelCount - 1)) {
                // If the margin is small, adjust the position of the edge labels to avoid overlapping
                // with labels of the other axes.
                float scaleFactor = m_drawer->scaledFontSize() / axisLabelItem.size().height();
                float labelOverlap = qAbs(labelTrans.z())
                        + (scaleFactor * axisLabelItem.size().height() / 2.0f)
                        - m_scaleZWithBackground + labelMargin;
                // No need to adjust quite as much on the front edges
                if (label != startIndex)
                    labelOverlap /= 2.0f;
                if (labelOverlap > 0.0f) {
                    if (label == 0)
                        labelTrans.setZ(labelTrans.z() - labelOverlap);
                    else
                        labelTrans.setZ(labelTrans.z() + labelOverlap);
                }
            }
            m_dummyRenderItem.setTranslation(labelTrans);

            if (drawSelection) {
                QVector4D labelColor = QVector4D(label / 255.0f, 0.0f, 0.0f,
                                                 alphaForRowSelection);
                shader->setUniformValue(shader->color(), labelColor);
            }

            m_drawer->drawLabel(m_dummyRenderItem, axisLabelItem, viewMatrix, projectionMatrix,
                                positionZComp, totalRotation, 0, m_cachedSelectionMode,
                                shader, m_labelObj, activeCamera,
                                true, true, Drawer::LabelMid, alignment, false, drawSelection);
            labelsMaxWidth = qMax(labelsMaxWidth, float(axisLabelItem.size().width()));
        }
        if (!drawSelection && m_axisCacheZ.isTitleVisible()) {
            if (m_polarGraph) {
                float titleZ = -m_polarRadius / 2.0f;
                if (m_zFlipped)
                    titleZ = -titleZ;
                labelTrans.setZ(titleZ);
            } else {
                labelTrans.setZ(0.0f);
            }
            drawAxisTitleZ(labelRotation, labelTrans, totalRotation, m_dummyRenderItem,
                           activeCamera, labelsMaxWidth, viewMatrix, projectionMatrix, shader);
        }
    }
    // X Labels
    if (m_axisCacheX.segmentCount() > 0) {
        labelsMaxWidth = 0.0f;
        labelAutoAngle = m_axisCacheX.labelAutoRotation();
        labelAngleFraction = labelAutoAngle / 90.0f;
        fractionCamY = activeCamera->yRotation() * labelAngleFraction;
        fractionCamX = activeCamera->xRotation() * labelAngleFraction;
        int labelCount = m_axisCacheX.labelCount();

        float labelZTrans = 0.0f;
        float labelYTrans = m_flipHorizontalGrid ? m_scaleYWithBackground : -m_scaleYWithBackground;
        if (m_polarGraph)
            labelYTrans += gridLineOffset + gridLineWidth;
        else
            labelZTrans = m_scaleZWithBackground + labelMargin;

        Qt::Alignment alignment = (m_xFlipped != m_zFlipped) ? Qt::AlignLeft : Qt::AlignRight;
        QVector3D labelRotation;
        if (m_zFlipped)
            labelZTrans = -labelZTrans;
        if (m_yFlipped)
            labelYTrans = -labelYTrans;
        if (labelAutoAngle == 0.0f) {
            labelRotation = QVector3D(-90.0f, 90.0f, 0.0f);
            if (m_xFlipped)
                labelRotation.setY(-90.0f);
            if (m_yFlippedForGrid) {
                if (m_xFlipped)
                    labelRotation.setY(-90.0f);
                else
                    labelRotation.setY(90.0f);
                labelRotation.setX(90.0f);
            }
        } else {
            if (m_xFlipped)
                labelRotation.setY(-90.0f);
            else
                labelRotation.setY(90.0f);
            if (m_yFlippedForGrid) {
                if (m_zFlipped) {
                    if (m_xFlipped) {
                        labelRotation.setX(90.0f - (2.0f * labelAutoAngle - fractionCamX)
                                           * (labelAutoAngle + fractionCamY) / labelAutoAngle);
                        labelRotation.setZ(-labelAutoAngle - fractionCamY);
                    } else {
                        labelRotation.setX(90.0f - (2.0f * labelAutoAngle + fractionCamX)
                                           * (labelAutoAngle + fractionCamY) / labelAutoAngle);
                        labelRotation.setZ(labelAutoAngle + fractionCamY);
                    }
                } else {
                    if (m_xFlipped) {
                        labelRotation.setX(90.0f + fractionCamX
                                           * -(labelAutoAngle + fractionCamY) / labelAutoAngle);
                        labelRotation.setZ(labelAutoAngle + fractionCamY);
                    } else {
                        labelRotation.setX(90.0f - fractionCamX
                                           * (-labelAutoAngle - fractionCamY) / labelAutoAngle);
                        labelRotation.setZ(-labelAutoAngle - fractionCamY);
                    }
                }
            } else {
                if (m_zFlipped) {
                    if (m_xFlipped) {
                        labelRotation.setX(-90.0f + (2.0f * labelAutoAngle - fractionCamX)
                                           * (labelAutoAngle - fractionCamY) / labelAutoAngle);
                        labelRotation.setZ(labelAutoAngle - fractionCamY);
                    } else {
                        labelRotation.setX(-90.0f + (2.0f * labelAutoAngle + fractionCamX)
                                           * (labelAutoAngle - fractionCamY) / labelAutoAngle);
                        labelRotation.setZ(-labelAutoAngle + fractionCamY);
                    }
                } else {
                    if (m_xFlipped) {
                        labelRotation.setX(-90.0f - fractionCamX
                                           * (-labelAutoAngle + fractionCamY) / labelAutoAngle);
                        labelRotation.setZ(-labelAutoAngle + fractionCamY);
                    } else {
                        labelRotation.setX(-90.0f + fractionCamX
                                           * -(labelAutoAngle - fractionCamY) / labelAutoAngle);
                        labelRotation.setZ(labelAutoAngle - fractionCamY);
                    }
                }
            }
        }

        QQuaternion totalRotation = Utils::calculateRotation(labelRotation);
        if (m_polarGraph) {
            if ((!m_yFlippedForGrid && (m_zFlipped != m_xFlipped))
                    || (m_yFlippedForGrid && (m_zFlipped == m_xFlipped))) {
                totalRotation *= m_zRightAngleRotation;
            } else {
                totalRotation *= m_zRightAngleRotationNeg;
            }
        }

        QVector3D labelTrans = QVector3D(0.0f,
                                         labelYTrans,
                                         labelZTrans);

        if (m_xFlipped) {
            startIndex = labelCount - 1;
            endIndex = -1;
            indexStep = -1;
        } else {
            startIndex = 0;
            endIndex = labelCount;
            indexStep = 1;
        }
        float offsetValue = 0.0f;
        bool showLastLabel = false;
        QVector<float> &labelPositions = m_axisCacheX.formatter()->labelPositions();
        int lastLabelPosIndex = labelPositions.size() - 1;
        if (labelPositions.size()
                && (labelPositions.at(lastLabelPosIndex) != 1.0f || labelPositions.at(0) != 0.0f)) {
            // Avoid overlapping first and last label if they would get on same position
            showLastLabel = true;
        }

        for (int label = startIndex; label != endIndex; label = label + indexStep) {
            glPolygonOffset(offsetValue++ / -10.0f, 1.0f);
            // Draw the label here
            if (m_polarGraph) {
                // Calculate angular position
                if (label == lastLabelPosIndex && !showLastLabel)
                    continue;
                float labelPosition = labelPositions.at(label);
                qreal angle = labelPosition * M_PI * 2.0;
                labelTrans.setX((m_polarRadius + labelMargin) * float(qSin(angle)));
                labelTrans.setZ(-(m_polarRadius + labelMargin) * float(qCos(angle)));
                // Alignment depends on label angular position, as well as flips
                Qt::AlignmentFlag vAlignment = Qt::AlignCenter;
                Qt::AlignmentFlag hAlignment = Qt::AlignCenter;
                const float centerMargin = 0.005f;
                if (labelPosition < 0.25f - centerMargin || labelPosition > 0.75f + centerMargin)
                    vAlignment = m_zFlipped ? Qt::AlignTop : Qt::AlignBottom;
                else if (labelPosition > 0.25f + centerMargin && labelPosition < 0.75f - centerMargin)
                    vAlignment = m_zFlipped ? Qt::AlignBottom : Qt::AlignTop;

                if (labelPosition < 0.50f - centerMargin && labelPosition > centerMargin)
                    hAlignment = m_zFlipped ? Qt::AlignRight : Qt::AlignLeft;
                else if (labelPosition < 1.0f - centerMargin && labelPosition > 0.5f + centerMargin)
                    hAlignment = m_zFlipped ? Qt::AlignLeft : Qt::AlignRight;
                if (m_yFlippedForGrid && vAlignment != Qt::AlignCenter)
                    vAlignment = (vAlignment == Qt::AlignTop) ? Qt::AlignBottom : Qt::AlignTop;
                alignment = vAlignment | hAlignment;
            } else {
                labelTrans.setX(m_axisCacheX.labelPosition(label));
            }
            const LabelItem &axisLabelItem = *m_axisCacheX.labelItems().at(label);
            if (label == 0 || label == (labelCount - 1)) {
                // If the margin is small, adjust the position of the edge labels to avoid overlapping
                // with labels of the other axes.
                float scaleFactor = m_drawer->scaledFontSize() / axisLabelItem.size().height();
                float labelOverlap = qAbs(labelTrans.x())
                        + (scaleFactor * axisLabelItem.size().height() / 2.0f)
                        - m_scaleXWithBackground + labelMargin;
                // No need to adjust quite as much on the front edges
                if (label != startIndex)
                    labelOverlap /= 2.0f;
                if (labelOverlap > 0.0f) {
                    if (label == 0)
                        labelTrans.setX(labelTrans.x() + labelOverlap);
                    else
                        labelTrans.setX(labelTrans.x() - labelOverlap);
                }
            }
            m_dummyRenderItem.setTranslation(labelTrans);

            if (drawSelection) {
                QVector4D labelColor = QVector4D(0.0f, label / 255.0f, 0.0f,
                                                 alphaForColumnSelection);
                shader->setUniformValue(shader->color(), labelColor);
            }

            m_drawer->drawLabel(m_dummyRenderItem, axisLabelItem, viewMatrix, projectionMatrix,
                                positionZComp, totalRotation, 0, m_cachedSelectionMode,
                                shader, m_labelObj, activeCamera,
                                true, true, Drawer::LabelMid, alignment, false, drawSelection);
            labelsMaxWidth = qMax(labelsMaxWidth, float(axisLabelItem.size().width()));
        }
        if (!drawSelection && m_axisCacheX.isTitleVisible()) {
            labelTrans.setX(0.0f);
            bool radial = false;
            if (m_polarGraph) {
                if (m_xFlipped == m_zFlipped)
                    totalRotation *= m_zRightAngleRotation;
                else
                    totalRotation *= m_zRightAngleRotationNeg;
                if (m_yFlippedForGrid)
                    totalRotation *= QQuaternion::fromAxisAndAngle(0.0f, 0.0f, 1.0f, -180.0f);
                labelTrans.setZ(-m_polarRadius);
                radial = true;
            }
            drawAxisTitleX(labelRotation, labelTrans, totalRotation, m_dummyRenderItem,
                           activeCamera, labelsMaxWidth, viewMatrix, projectionMatrix, shader,
                           radial);
        }
    }
    // Y Labels
    if (m_axisCacheY.segmentCount() > 0) {
        labelsMaxWidth = 0.0f;
        labelAutoAngle = m_axisCacheY.labelAutoRotation();
        labelAngleFraction = labelAutoAngle / 90.0f;
        fractionCamY = activeCamera->yRotation() * labelAngleFraction;
        fractionCamX = activeCamera->xRotation() * labelAngleFraction;
        int labelCount = m_axisCacheY.labelCount();

        float labelXTrans = m_scaleXWithBackground;
        float labelZTrans = m_scaleZWithBackground;

        // Back & side wall
        float labelMarginXTrans = labelMargin;
        float labelMarginZTrans = labelMargin;
        QVector3D backLabelRotation(0.0f, -90.0f, 0.0f);
        QVector3D sideLabelRotation(0.0f, 0.0f, 0.0f);
        Qt::AlignmentFlag backAlignment =
                (m_xFlipped != m_zFlipped) ? Qt::AlignLeft : Qt::AlignRight;
        Qt::AlignmentFlag sideAlignment =
                (m_xFlipped == m_zFlipped) ? Qt::AlignLeft : Qt::AlignRight;
        if (!m_xFlipped) {
            labelXTrans = -labelXTrans;
            labelMarginXTrans = -labelMargin;
        }
        if (m_zFlipped) {
            labelZTrans = -labelZTrans;
            labelMarginZTrans = -labelMargin;
        }
        if (labelAutoAngle == 0.0f) {
            if (!m_xFlipped)
                backLabelRotation.setY(90.0f);
            if (m_zFlipped)
                sideLabelRotation.setY(180.f);
        } else {
            // Orient side labels somewhat towards the camera
            if (m_xFlipped) {
                if (m_zFlipped)
                    sideLabelRotation.setY(180.0f + (2.0f * labelAutoAngle) - fractionCamX);
                else
                    sideLabelRotation.setY(-fractionCamX);
                backLabelRotation.setY(-90.0f + labelAutoAngle - fractionCamX);
            } else {
                if (m_zFlipped)
                    sideLabelRotation.setY(180.0f - (2.0f * labelAutoAngle) - fractionCamX);
                else
                    sideLabelRotation.setY(-fractionCamX);
                backLabelRotation.setY(90.0f - labelAutoAngle - fractionCamX);
            }
        }
        sideLabelRotation.setX(-fractionCamY);
        backLabelRotation.setX(-fractionCamY);

        QQuaternion totalSideRotation = Utils::calculateRotation(sideLabelRotation);
        QQuaternion totalBackRotation = Utils::calculateRotation(backLabelRotation);

        QVector3D labelTransBack = QVector3D(labelXTrans, 0.0f, labelZTrans + labelMarginZTrans);
        QVector3D labelTransSide(-labelXTrans - labelMarginXTrans, 0.0f, -labelZTrans);

        if (m_yFlipped) {
            startIndex = labelCount - 1;
            endIndex = -1;
            indexStep = -1;
        } else {
            startIndex = 0;
            endIndex = labelCount;
            indexStep = 1;
        }
        float offsetValue = 0.0f;
        for (int label = startIndex; label != endIndex; label = label + indexStep) {
            const LabelItem &axisLabelItem = *m_axisCacheY.labelItems().at(label);
            float labelYTrans = m_axisCacheY.labelPosition(label);

            glPolygonOffset(offsetValue++ / -10.0f, 1.0f);

            if (drawSelection) {
                QVector4D labelColor = QVector4D(0.0f, 0.0f, label / 255.0f,
                                                 alphaForValueSelection);
                shader->setUniformValue(shader->color(), labelColor);
            }

            if (label == startIndex) {
                // If the margin is small, adjust the position of the edge label to avoid
                // overlapping with labels of the other axes.
                float scaleFactor = m_drawer->scaledFontSize() / axisLabelItem.size().height();
                float labelOverlap = qAbs(labelYTrans)
                        + (scaleFactor * axisLabelItem.size().height() / 2.0f)
                        - m_scaleYWithBackground + labelMargin;
                if (labelOverlap > 0.0f) {
                    if (label == 0)
                        labelYTrans += labelOverlap;
                    else
                        labelYTrans -= labelOverlap;
                }
            }

            // Back wall
            labelTransBack.setY(labelYTrans);
            m_dummyRenderItem.setTranslation(labelTransBack);
            m_drawer->drawLabel(m_dummyRenderItem, axisLabelItem, viewMatrix, projectionMatrix,
                                positionZComp, totalBackRotation, 0, m_cachedSelectionMode,
                                shader, m_labelObj, activeCamera,
                                true, true, Drawer::LabelMid, backAlignment, false,
                                drawSelection);

            // Side wall
            labelTransSide.setY(labelYTrans);
            m_dummyRenderItem.setTranslation(labelTransSide);
            m_drawer->drawLabel(m_dummyRenderItem, axisLabelItem, viewMatrix, projectionMatrix,
                                positionZComp, totalSideRotation, 0, m_cachedSelectionMode,
                                shader, m_labelObj, activeCamera,
                                true, true, Drawer::LabelMid, sideAlignment, false,
                                drawSelection);
            labelsMaxWidth = qMax(labelsMaxWidth, float(axisLabelItem.size().width()));
        }
        if (!drawSelection && m_axisCacheY.isTitleVisible()) {
            labelTransSide.setY(0.0f);
            labelTransBack.setY(0.0f);
            drawAxisTitleY(sideLabelRotation, backLabelRotation, labelTransSide, labelTransBack,
                           totalSideRotation, totalBackRotation, m_dummyRenderItem, activeCamera,
                           labelsMaxWidth, viewMatrix, projectionMatrix,
                           shader);
        }
    }
    glDisable(GL_POLYGON_OFFSET_FILL);

    if (!drawSelection)
        glDisable(GL_BLEND);
}

void Surface3DRenderer::updateSelectionMode(QAbstract3DGraph::SelectionFlags mode)
{
    Abstract3DRenderer::updateSelectionMode(mode);

    if (m_cachedSelectionMode > QAbstract3DGraph::SelectionNone)
        updateSelectionTextures();
}

void Surface3DRenderer::updateSelectionTextures()
{
    uint lastSelectionId = 1;

    foreach (SeriesRenderCache *baseCache, m_renderCacheList) {
        SurfaceSeriesRenderCache *cache =
                static_cast<SurfaceSeriesRenderCache *>(baseCache);
        GLuint texture = cache->selectionTexture();
        m_textureHelper->deleteTexture(&texture);
        createSelectionTexture(cache, lastSelectionId);
    }
    m_selectionTexturesDirty = false;
}

void Surface3DRenderer::createSelectionTexture(SurfaceSeriesRenderCache *cache,
                                               uint &lastSelectionId)
{
    // Create the selection ID image. Each grid corner gets 1 pixel area of
    // ID color so that each vertex (data point) has 2x2 pixel area of ID color,
    // except the vertices on the edges.
    const QRect &sampleSpace = cache->sampleSpace();
    int idImageWidth = (sampleSpace.width() - 1) * 2;
    int idImageHeight = (sampleSpace.height() - 1) * 2;

    if (idImageHeight <= 0 || idImageWidth <= 0) {
        cache->setSelectionIdRange(~0U, ~0U);
        cache->setSelectionTexture(0);
        return;
    }

    int stride = idImageWidth * 4 * sizeof(uchar); // 4 = number of color components (rgba)

    uint idStart = lastSelectionId;
    uchar *bits = new uchar[idImageWidth * idImageHeight * 4 * sizeof(uchar)];
    for (int i = 0; i < idImageHeight; i += 2) {
        for (int j = 0; j < idImageWidth; j += 2) {
            int p = (i * idImageWidth + j) * 4;
            uchar r, g, b, a;
            idToRGBA(lastSelectionId, &r, &g, &b, &a);
            fillIdCorner(&bits[p], r, g, b, a);

            idToRGBA(lastSelectionId + 1, &r, &g, &b, &a);
            fillIdCorner(&bits[p + 4], r, g, b, a);

            idToRGBA(lastSelectionId + sampleSpace.width(), &r, &g, &b, &a);
            fillIdCorner(&bits[p + stride], r, g, b, a);

            idToRGBA(lastSelectionId + sampleSpace.width() + 1, &r, &g, &b, &a);
            fillIdCorner(&bits[p + stride + 4], r, g, b, a);

            lastSelectionId++;
        }
        lastSelectionId++;
    }
    lastSelectionId += sampleSpace.width();
    cache->setSelectionIdRange(idStart, lastSelectionId - 1);

    // Move the ID image (bits) to the texture
    QImage image = QImage(bits, idImageWidth, idImageHeight, QImage::Format_RGB32);
    GLuint selectionTexture = m_textureHelper->create2DTexture(image, false, false, false);
    cache->setSelectionTexture(selectionTexture);

    // Release the temp bits allocation
    delete[] bits;
}

void Surface3DRenderer::initSelectionBuffer()
{
    // Create the result selection texture and buffers
    m_textureHelper->deleteTexture(&m_selectionResultTexture);

    m_selectionResultTexture = m_textureHelper->createSelectionTexture(m_primarySubViewport.size(),
                                                                       m_selectionFrameBuffer,
                                                                       m_selectionDepthBuffer);
}

void Surface3DRenderer::fillIdCorner(uchar *p, uchar r, uchar g, uchar b, uchar a)
{
    p[0] = r;
    p[1] = g;
    p[2] = b;
    p[3] = a;
}

void Surface3DRenderer::idToRGBA(uint id, uchar *r, uchar *g, uchar *b, uchar *a)
{
    *r = id & ID_TO_RGBA_MASK;
    *g = (id >> 8) & ID_TO_RGBA_MASK;
    *b = (id >> 16) & ID_TO_RGBA_MASK;
    *a = (id >> 24) & ID_TO_RGBA_MASK;
}

void Surface3DRenderer::calculateSceneScalingFactors()
{
    // Margin for background (the default 0.10 makes it 10% larger to avoid
    // selection ball being drawn inside background)
    if (m_requestedMargin < 0.0f) {
        m_hBackgroundMargin = 0.1f;
        m_vBackgroundMargin = 0.1f;
    } else {
        m_hBackgroundMargin = m_requestedMargin;
        m_vBackgroundMargin = m_requestedMargin;
    }
    if (m_polarGraph) {
        float polarMargin = calculatePolarBackgroundMargin();
        m_hBackgroundMargin = qMax(m_hBackgroundMargin, polarMargin);
    }

    // Calculate scene scaling and translation factors
    m_heightNormalizer = GLfloat(m_axisCacheY.max() - m_axisCacheY.min());

    float horizontalAspectRatio;
    if (m_polarGraph)
        horizontalAspectRatio = 1.0f;
    else
        horizontalAspectRatio = m_graphHorizontalAspectRatio;

    QSizeF areaSize;
    if (horizontalAspectRatio == 0.0f) {
        areaSize.setHeight(m_axisCacheZ.max() -  m_axisCacheZ.min());
        areaSize.setWidth(m_axisCacheX.max() - m_axisCacheX.min());
    } else {
        areaSize.setHeight(1.0f);
        areaSize.setWidth(horizontalAspectRatio);
    }

    float horizontalMaxDimension;
    if (m_graphAspectRatio > 2.0f) {
        horizontalMaxDimension = 2.0f;
        m_scaleY = 2.0f / m_graphAspectRatio;
    } else {
        horizontalMaxDimension = m_graphAspectRatio;
        m_scaleY = 1.0f;
    }
    if (m_polarGraph)
        m_polarRadius = horizontalMaxDimension;

    float scaleFactor = qMax(areaSize.width(), areaSize.height());
    m_scaleX = horizontalMaxDimension * areaSize.width() / scaleFactor;
    m_scaleZ = horizontalMaxDimension * areaSize.height() / scaleFactor;

    m_scaleXWithBackground = m_scaleX + m_hBackgroundMargin;
    m_scaleYWithBackground = m_scaleY + m_vBackgroundMargin;
    m_scaleZWithBackground = m_scaleZ + m_hBackgroundMargin;

    m_axisCacheX.setScale(m_scaleX * 2.0f);
    m_axisCacheY.setScale(m_scaleY * 2.0f);
    m_axisCacheZ.setScale(-m_scaleZ * 2.0f);
    m_axisCacheX.setTranslate(-m_scaleX);
    m_axisCacheY.setTranslate(-m_scaleY);
    m_axisCacheZ.setTranslate(m_scaleZ);

    updateCameraViewport();
    updateCustomItemPositions();
}

void Surface3DRenderer::checkFlatSupport(SurfaceSeriesRenderCache *cache)
{
    bool flatEnable = cache->isFlatShadingEnabled();
    if (flatEnable && !m_flatSupported) {
        qWarning() << "Warning: Flat qualifier not supported on your platform's GLSL language."
                      " Requires at least GLSL version 1.2 with GL_EXT_gpu_shader4 extension.";
        cache->setFlatShadingEnabled(false);
        cache->setFlatChangeAllowed(false);
    }
}

void Surface3DRenderer::updateObjects(SurfaceSeriesRenderCache *cache, bool dimensionChanged)
{
    QSurfaceDataArray &dataArray = cache->dataArray();
    const QRect &sampleSpace = cache->sampleSpace();

    const QSurface3DSeries *currentSeries = cache->series();
    QSurfaceDataProxy *dataProxy = currentSeries->dataProxy();
    const QSurfaceDataArray &array = *dataProxy->array();

    if (cache->isFlatShadingEnabled()) {
        cache->surfaceObject()->setUpData(dataArray, sampleSpace, dimensionChanged, m_polarGraph);
        if (cache->surfaceTexture())
            cache->surfaceObject()->coarseUVs(array, dataArray);
    } else {
        cache->surfaceObject()->setUpSmoothData(dataArray, sampleSpace, dimensionChanged,
                                                m_polarGraph);
        if (cache->surfaceTexture())
            cache->surfaceObject()->smoothUVs(array, dataArray);
    }
}

void Surface3DRenderer::updateSelectedPoint(const QPoint &position, QSurface3DSeries *series)
{
    m_selectedPoint = position;
    m_selectedSeries = series;
    m_selectionDirty = true;
}

void Surface3DRenderer::updateFlipHorizontalGrid(bool flip)
{
    m_flipHorizontalGrid = flip;
}

void Surface3DRenderer::resetClickedStatus()
{
    m_clickedPosition = Surface3DController::invalidSelectionPosition();
    m_clickedSeries = 0;
}

void Surface3DRenderer::loadBackgroundMesh()
{
    ObjectHelper::resetObjectHelper(this, m_backgroundObj,
                                    QStringLiteral(":/defaultMeshes/background"));
}

void Surface3DRenderer::surfacePointSelected(const QPoint &point)
{
    foreach (SeriesRenderCache *baseCache, m_renderCacheList) {
        SurfaceSeriesRenderCache *cache =
                static_cast<SurfaceSeriesRenderCache *>(baseCache);
        cache->setSlicePointerActivity(false);
        cache->setMainPointerActivity(false);
    }

    if (m_cachedSelectionMode.testFlag(QAbstract3DGraph::SelectionMultiSeries)) {
        // Find axis coordinates for the selected point
        SurfaceSeriesRenderCache *selectedCache =
                static_cast<SurfaceSeriesRenderCache *>(
                    m_renderCacheList.value(const_cast<QSurface3DSeries *>(m_selectedSeries)));
        QSurfaceDataArray &dataArray = selectedCache->dataArray();
        QSurfaceDataItem item = dataArray.at(point.x())->at(point.y());
        QPointF coords(item.x(), item.z());

        foreach (SeriesRenderCache *baseCache, m_renderCacheList) {
            SurfaceSeriesRenderCache *cache =
                    static_cast<SurfaceSeriesRenderCache *>(baseCache);
            if (cache->series() != m_selectedSeries) {
                QPoint mappedPoint = mapCoordsToSampleSpace(cache, coords);
                updateSelectionPoint(cache, mappedPoint, false);
            } else {
                updateSelectionPoint(cache, point, true);
            }
        }
    } else {
        if (m_selectedSeries) {
            SurfaceSeriesRenderCache *cache =
                    static_cast<SurfaceSeriesRenderCache *>(
                        m_renderCacheList.value(const_cast<QSurface3DSeries *>(m_selectedSeries)));
            if (cache)
                updateSelectionPoint(cache, point, true);
        }
    }
}

void Surface3DRenderer::updateSelectionPoint(SurfaceSeriesRenderCache *cache, const QPoint &point,
                                             bool label)
{
    int row = point.x();
    int column = point.y();

    if (column < 0 || row < 0)
        return;

    SelectionPointer *slicePointer = cache->sliceSelectionPointer();
    if (!slicePointer && m_cachedIsSlicingActivated) {
        slicePointer = new SelectionPointer(m_drawer);
        cache->setSliceSelectionPointer(slicePointer);
    }
    SelectionPointer *mainPointer = cache->mainSelectionPointer();
    if (!mainPointer) {
        mainPointer = new SelectionPointer(m_drawer);
        cache->setMainSelectionPointer(mainPointer);
    }

    QString selectionLabel;
    if (label) {
        m_selectionLabelDirty = false;
        selectionLabel = cache->itemLabel();
    }

    if (m_cachedIsSlicingActivated) {
        QVector3D subPosFront;
        QVector3D subPosBack;
        if (m_cachedSelectionMode.testFlag(QAbstract3DGraph::SelectionRow)) {
            subPosFront = cache->sliceSurfaceObject()->vertexAt(column, 0);
            subPosBack = cache->sliceSurfaceObject()->vertexAt(column, 1);
        } else if (m_cachedSelectionMode.testFlag(QAbstract3DGraph::SelectionColumn)) {
            subPosFront = cache->sliceSurfaceObject()->vertexAt(row, 0);
            subPosBack = cache->sliceSurfaceObject()->vertexAt(row, 1);
        }
        slicePointer->updateBoundingRect(m_secondarySubViewport);
        slicePointer->updateSliceData(true, m_autoScaleAdjustment);
        slicePointer->setPosition((subPosFront + subPosBack) / 2.0f);
        slicePointer->setLabel(selectionLabel);
        slicePointer->setPointerObject(cache->object());
        slicePointer->setLabelObject(m_labelObj);
        slicePointer->setHighlightColor(cache->singleHighlightColor());
        slicePointer->updateScene(m_cachedScene);
        slicePointer->setRotation(cache->meshRotation());
        cache->setSlicePointerActivity(true);
    }

    QVector3D mainPos;
    mainPos = cache->surfaceObject()->vertexAt(column, row);
    mainPointer->updateBoundingRect(m_primarySubViewport);
    mainPointer->updateSliceData(false, m_autoScaleAdjustment);
    mainPointer->setPosition(mainPos);
    mainPointer->setLabel(selectionLabel);
    mainPointer->setPointerObject(cache->object());
    mainPointer->setLabelObject(m_labelObj);
    mainPointer->setHighlightColor(cache->singleHighlightColor());
    mainPointer->updateScene(m_cachedScene);
    mainPointer->setRotation(cache->meshRotation());
    cache->setMainPointerActivity(true);
}

// Maps selection Id to surface point in data array
QPoint Surface3DRenderer::selectionIdToSurfacePoint(uint id)
{
    m_clickedType = QAbstract3DGraph::ElementNone;
    m_selectedLabelIndex = -1;
    m_selectedCustomItemIndex = -1;
    // Check for label and custom item selection
    if (id / alphaMultiplier == labelRowAlpha) {
        m_selectedLabelIndex = id - (alphaMultiplier * uint(labelRowAlpha));
        m_clickedType = QAbstract3DGraph::ElementAxisZLabel;
        return Surface3DController::invalidSelectionPosition();
    } else if (id / alphaMultiplier == labelColumnAlpha) {
        m_selectedLabelIndex = (id - (alphaMultiplier * uint(labelColumnAlpha))) / greenMultiplier;
        m_clickedType = QAbstract3DGraph::ElementAxisXLabel;
        return Surface3DController::invalidSelectionPosition();
    } else if (id / alphaMultiplier == labelValueAlpha) {
        m_selectedLabelIndex = (id - (alphaMultiplier * uint(labelValueAlpha))) / blueMultiplier;
        m_clickedType = QAbstract3DGraph::ElementAxisYLabel;
        return Surface3DController::invalidSelectionPosition();
    } else if (id / alphaMultiplier == customItemAlpha) {
        // Custom item selection
        m_clickedType = QAbstract3DGraph::ElementCustomItem;
        m_selectedCustomItemIndex = id - (alphaMultiplier * uint(customItemAlpha));
        return Surface3DController::invalidSelectionPosition();
    }

    // Not a label selection
    SurfaceSeriesRenderCache *selectedCache = 0;
    foreach (SeriesRenderCache *baseCache, m_renderCacheList) {
        SurfaceSeriesRenderCache *cache = static_cast<SurfaceSeriesRenderCache *>(baseCache);
        if (cache->isWithinIdRange(id)) {
            selectedCache = cache;
            break;
        }
    }
    if (!selectedCache) {
        m_clickedSeries = 0;
        return Surface3DController::invalidSelectionPosition();
    }

    uint idInSeries = id - selectedCache->selectionIdStart() + 1;
    const QRect &sampleSpace = selectedCache->sampleSpace();
    int column = ((idInSeries - 1) % sampleSpace.width()) + sampleSpace.x();
    int row = ((idInSeries - 1) / sampleSpace.width()) +  sampleSpace.y();

    m_clickedSeries = selectedCache->series();
    m_clickedType = QAbstract3DGraph::ElementSeries;
    return QPoint(row, column);
}

void Surface3DRenderer::updateShadowQuality(QAbstract3DGraph::ShadowQuality quality)
{
    m_cachedShadowQuality = quality;

    switch (quality) {
    case QAbstract3DGraph::ShadowQualityLow:
        m_shadowQualityToShader = 33.3f;
        m_shadowQualityMultiplier = 1;
        break;
    case QAbstract3DGraph::ShadowQualityMedium:
        m_shadowQualityToShader = 100.0f;
        m_shadowQualityMultiplier = 3;
        break;
    case QAbstract3DGraph::ShadowQualityHigh:
        m_shadowQualityToShader = 200.0f;
        m_shadowQualityMultiplier = 5;
        break;
    case QAbstract3DGraph::ShadowQualitySoftLow:
        m_shadowQualityToShader = 5.0f;
        m_shadowQualityMultiplier = 1;
        break;
    case QAbstract3DGraph::ShadowQualitySoftMedium:
        m_shadowQualityToShader = 10.0f;
        m_shadowQualityMultiplier = 3;
        break;
    case QAbstract3DGraph::ShadowQualitySoftHigh:
        m_shadowQualityToShader = 15.0f;
        m_shadowQualityMultiplier = 4;
        break;
    default:
        m_shadowQualityToShader = 0.0f;
        m_shadowQualityMultiplier = 1;
        break;
    }

    handleShadowQualityChange();

    updateDepthBuffer();
}

void Surface3DRenderer::updateTextures()
{
    Abstract3DRenderer::updateTextures();

    if (m_polarGraph)
        calculateSceneScalingFactors();
}

void Surface3DRenderer::updateSlicingActive(bool isSlicing)
{
    if (m_cachedIsSlicingActivated == isSlicing)
        return;

    m_cachedIsSlicingActivated = isSlicing;

    if (!m_cachedIsSlicingActivated) {
        // We need to re-init selection buffer in case there has been a resize
        initSelectionBuffer();
        initCursorPositionBuffer();
    }

    updateDepthBuffer(); // Re-init depth buffer as well

    m_selectionDirty = true;

    foreach (SeriesRenderCache *baseCache, m_renderCacheList) {
        SurfaceSeriesRenderCache *cache = static_cast<SurfaceSeriesRenderCache *>(baseCache);
        if (cache->mainSelectionPointer())
            cache->mainSelectionPointer()->updateBoundingRect(m_primarySubViewport);
    }
}

void Surface3DRenderer::initShaders(const QString &vertexShader, const QString &fragmentShader)
{
    Q_UNUSED(vertexShader);
    Q_UNUSED(fragmentShader);

    delete m_surfaceFlatShader;
    delete m_surfaceSmoothShader;
    delete m_surfaceTexturedSmoothShader;
    delete m_surfaceTexturedFlatShader;
    delete m_surfaceSliceFlatShader;
    delete m_surfaceSliceSmoothShader;

    if (!m_isOpenGLES) {
        if (m_cachedShadowQuality > QAbstract3DGraph::ShadowQualityNone) {
            m_surfaceSmoothShader = new ShaderHelper(this, QStringLiteral(":/shaders/vertexShadow"),
                                                     QStringLiteral(":/shaders/fragmentSurfaceShadowNoTex"));
            m_surfaceTexturedSmoothShader = new ShaderHelper(this, QStringLiteral(":/shaders/vertexShadow"),
                                                             QStringLiteral(":/shaders/fragmentTexturedSurfaceShadow"));
        } else {
            m_surfaceSmoothShader = new ShaderHelper(this, QStringLiteral(":/shaders/vertex"),
                                                     QStringLiteral(":/shaders/fragmentSurface"));
            m_surfaceTexturedSmoothShader = new ShaderHelper(this, QStringLiteral(":/shaders/vertexTexture"),
                                                             QStringLiteral(":/shaders/fragmentTexture"));
        }
        m_surfaceSliceSmoothShader = new ShaderHelper(this, QStringLiteral(":/shaders/vertex"),
                                                      QStringLiteral(":/shaders/fragmentSurface"));
        if (m_flatSupported) {
            if (m_cachedShadowQuality > QAbstract3DGraph::ShadowQualityNone) {
                m_surfaceFlatShader = new ShaderHelper(this, QStringLiteral(":/shaders/vertexSurfaceShadowFlat"),
                                                       QStringLiteral(":/shaders/fragmentSurfaceShadowFlat"));
                m_surfaceTexturedFlatShader = new ShaderHelper(this, QStringLiteral(":/shaders/vertexSurfaceShadowFlat"),
                                                               QStringLiteral(":/shaders/fragmentTexturedSurfaceShadowFlat"));
            } else {
                m_surfaceFlatShader = new ShaderHelper(this, QStringLiteral(":/shaders/vertexSurfaceFlat"),
                                                       QStringLiteral(":/shaders/fragmentSurfaceFlat"));
                m_surfaceTexturedFlatShader = new ShaderHelper(this, QStringLiteral(":/shaders/vertexSurfaceFlat"),
                                                               QStringLiteral(":/shaders/fragmentSurfaceTexturedFlat"));
            }
            m_surfaceSliceFlatShader = new ShaderHelper(this, QStringLiteral(":/shaders/vertexSurfaceFlat"),
                                                        QStringLiteral(":/shaders/fragmentSurfaceFlat"));
        } else {
            m_surfaceFlatShader = 0;
            m_surfaceSliceFlatShader = 0;
            m_surfaceTexturedFlatShader = 0;
        }
    } else {
        m_surfaceSmoothShader = new ShaderHelper(this, QStringLiteral(":/shaders/vertex"),
                                                 QStringLiteral(":/shaders/fragmentSurfaceES2"));
        m_surfaceFlatShader = new ShaderHelper(this, QStringLiteral(":/shaders/vertex"),
                                               QStringLiteral(":/shaders/fragmentSurfaceES2"));
        m_surfaceTexturedSmoothShader = new ShaderHelper(this, QStringLiteral(":/shaders/vertexTexture"),
                                                         QStringLiteral(":/shaders/fragmentTextureES2"));
        m_surfaceTexturedFlatShader = new ShaderHelper(this, QStringLiteral(":/shaders/vertexTexture"),
                                                       QStringLiteral(":/shaders/fragmentTextureES2"));
        m_surfaceSliceSmoothShader = new ShaderHelper(this, QStringLiteral(":/shaders/vertex"),
                                                      QStringLiteral(":/shaders/fragmentSurfaceES2"));
        m_surfaceSliceFlatShader = new ShaderHelper(this, QStringLiteral(":/shaders/vertex"),
                                                    QStringLiteral(":/shaders/fragmentSurfaceES2"));
    }

    m_surfaceSmoothShader->initialize();
    m_surfaceSliceSmoothShader->initialize();
    m_surfaceTexturedSmoothShader->initialize();
    if (m_flatSupported) {
        m_surfaceFlatShader->initialize();
        m_surfaceSliceFlatShader->initialize();
        m_surfaceTexturedFlatShader->initialize();
    }
}

void Surface3DRenderer::initBackgroundShaders(const QString &vertexShader,
                                              const QString &fragmentShader)
{
    if (m_backgroundShader)
        delete m_backgroundShader;
    m_backgroundShader = new ShaderHelper(this, vertexShader, fragmentShader);
    m_backgroundShader->initialize();
}

void Surface3DRenderer::initSelectionShaders()
{
    if (m_selectionShader)
        delete m_selectionShader;
    m_selectionShader = new ShaderHelper(this, QStringLiteral(":/shaders/vertexLabel"),
                                         QStringLiteral(":/shaders/fragmentLabel"));
    m_selectionShader->initialize();
}

void Surface3DRenderer::initSurfaceShaders()
{
    // Gridline shader
    if (m_surfaceGridShader)
        delete m_surfaceGridShader;
    m_surfaceGridShader = new ShaderHelper(this, QStringLiteral(":/shaders/vertexPlainColor"),
                                           QStringLiteral(":/shaders/fragmentPlainColor"));
    m_surfaceGridShader->initialize();

    // Triggers surface shader selection by shadow setting
    handleShadowQualityChange();
}

void Surface3DRenderer::initDepthShader()
{
    if (!m_isOpenGLES) {
        delete m_depthShader;
        m_depthShader = new ShaderHelper(this, QStringLiteral(":/shaders/vertexDepth"),
                                         QStringLiteral(":/shaders/fragmentDepth"));
        m_depthShader->initialize();
    }
}

void Surface3DRenderer::updateDepthBuffer()
{
    if (!m_isOpenGLES) {
        m_textureHelper->deleteTexture(&m_depthTexture);

        if (m_primarySubViewport.size().isEmpty())
            return;

        if (m_cachedShadowQuality > QAbstract3DGraph::ShadowQualityNone) {
            m_depthTexture = m_textureHelper->createDepthTextureFrameBuffer(m_primarySubViewport.size(),
                                                                            m_depthFrameBuffer,
                                                                            m_shadowQualityMultiplier);
            if (!m_depthTexture)
                lowerShadowQuality();
        }
    }
}

QVector3D Surface3DRenderer::convertPositionToTranslation(const QVector3D &position,
                                                          bool isAbsolute)
{
    float xTrans = 0.0f;
    float yTrans = 0.0f;
    float zTrans = 0.0f;
    if (!isAbsolute) {
        if (m_polarGraph) {
            calculatePolarXZ(position, xTrans, zTrans);
        } else {
            xTrans = m_axisCacheX.positionAt(position.x());
            zTrans = m_axisCacheZ.positionAt(position.z());
        }
        yTrans = m_axisCacheY.positionAt(position.y());
    } else {
        xTrans = position.x() * m_scaleX;
        yTrans = position.y() * m_scaleY;
        zTrans = position.z() * -m_scaleZ;
    }
    return QVector3D(xTrans, yTrans, zTrans);
}

void Surface3DRenderer::updateAxisLabels(QAbstract3DAxis::AxisOrientation orientation,
                                         const QStringList &labels)
{
    Abstract3DRenderer::updateAxisLabels(orientation, labels);

    // Angular axis label dimensions affect the chart dimensions
    if (m_polarGraph && orientation == QAbstract3DAxis::AxisOrientationX)
        calculateSceneScalingFactors();
}

void Surface3DRenderer::updateAxisTitleVisibility(QAbstract3DAxis::AxisOrientation orientation, bool visible)
{
    Abstract3DRenderer::updateAxisTitleVisibility(orientation, visible);

    // Angular axis title existence affects the chart dimensions
    if (m_polarGraph && orientation == QAbstract3DAxis::AxisOrientationX)
        calculateSceneScalingFactors();
}

void Surface3DRenderer::updateMargin(float margin)
{
    Abstract3DRenderer::updateMargin(margin);
    calculateSceneScalingFactors();
}

QT_END_NAMESPACE_DATAVISUALIZATION
