0

I'm trying to render individual tiles from a tileset. For example, I want to display the grey tile in the tileset below:

tileset image

In the real use case, these would be e.g. water, grass, etc. tiles in a game. There are some requirements for rendering these tiles:

  • They are 32x32 pixels and will be rendered fullscreen, so performance is important.
  • They should not be smoothed when scaled.

None of the built-in Qt Quick types meet these requirements (rendering a section of an image that's not smoothed), as far as I can tell. I've tried QQuickPaintedItem with various QPainter render hints (such as SmoothPixmapTransform set to false) without success; the image is "blurry" when upscaled. AnimatedSprite supports rendering sections of an image, but has no API to disable smoothing.

My idea was to implement a custom QQuickItem using the scene graph API.

main.cpp:

#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQuickItem>
#include <QQuickWindow>
#include <QSGImageNode>

static QImage image;
static const int tileSize = 32;
static const int tilesetSize = 8;

class Tile : public QQuickItem
{
    Q_OBJECT
    Q_PROPERTY(int index READ index WRITE setIndex NOTIFY indexChanged)

public:
    Tile() :
        mIndex(-1) {
        setWidth(tileSize);
        setHeight(tileSize);

        setFlag(QQuickItem::ItemHasContents);
    }

    QSGNode *updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *)
    {
        if (!oldNode) {
            oldNode = window()->createImageNode();
        }

        if (mIndex == -1)
            return oldNode;

        if (image.isNull()) {
            image = QImage("C:/tileset.png");
            if (image.isNull())
                return oldNode;
        }

        QSGTexture *texture = window()->createTextureFromImage(image);
        qDebug() << "textureSize:" << texture->textureSize();
        if (!texture)
            return oldNode;

        QSGImageNode *imageNode = static_cast<QSGImageNode*>(oldNode);
//        imageNode->setOwnsTexture(true);
        imageNode->setTexture(texture);
        qDebug() << "source rect:" << (mIndex % tileSize) * tileSize << (mIndex / tileSize) * tileSize << tileSize << tileSize;
        imageNode->setSourceRect((mIndex % tileSize) * tileSize, (mIndex / tileSize) * tileSize, tileSize, tileSize);

        return oldNode;

    }

    int index() const {
        return mIndex;
    }

    void setIndex(int index) {
        if (index == mIndex)
            return;

        mIndex = index;
        emit indexChanged();
    }

signals:
    void indexChanged();

private:
    int mIndex;
};

int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);

    qmlRegisterType<Tile>("App", 1, 0, "Tile");

    QQmlApplicationEngine engine;
    engine.load(QUrl(QStringLiteral("qrc:/main.qml")));

    return app.exec();
}

#include "main.moc"

main.qml:

import QtQuick 2.9
import QtQuick.Controls 2.2

import App 1.0

ApplicationWindow {
    id: window
    width: 800
    height: 800
    visible: true

    Slider {
        id: slider
        from: 1
        to: 10
    }

    Tile {
        scale: slider.value
        index: 1
        anchors.centerIn: parent

        Rectangle {
            anchors.fill: parent
            color: "transparent"
            border.color: "darkorange"
        }
    }
}

The output from this application looks fine, but nothing is rendered within the rectangle:

textureSize: QSize(256, 256)
source rect: 32 0 32 32

Judging from the minimal docs, my implementation (in terms of how I create nodes) seems OK. Where am I going wrong?

Mitch
  • 23,716
  • 9
  • 83
  • 122

3 Answers3

2

A year late to the party, but I was running into the same problem as you. For the sake of anyone else who is trying to subclass a QQuickItem and has come across this thread, there's a little nugget that's in the documentation in regards to updatePaintNode:

The function is called as a result of QQuickItem::update(), if the user has set the QQuickItem::ItemHasContents flag on the item.

When I set that flag, everything rendered.

And I considered myself a detail-oriented person...

EDIT:

After the OP pointed out they had already set the ItemHasContents flag, I looked at the code again and saw that while the OP had set the sourceRect on the node, the OP hadn't set the rect of the node, and that indeed was the problem the OP was running into.

  • My code already called that function though, so this is not an answer to the question unfortunately. – Mitch May 12 '18 at 06:19
  • You're right--see...I'm not so detail oriented. Ugh. But I did look at your code again, and a difference that I found is that while you are setting the sourceRect of the node, you aren't setting the rect of the node. I have a call to `QSGImageNode::setRect()` that I removed, and nothing showed up. Perhaps that's what you're missing? – DigitalExegete May 13 '18 at 01:15
  • Nice, calling `imageNode->setRect(0, 0, 32, 32);` fixes it. :) You should include that in your answer. – Mitch May 13 '18 at 06:29
  • I'll definitely add it--I wanted to make sure it /actually/ solved your problem first. – DigitalExegete May 13 '18 at 22:28
1

I ended up going with a friend's idea of using QQuickImageProvider:

tileimageprovider.h:

#ifndef TILEIMAGEPROVIDER_H
#define TILEIMAGEPROVIDER_H

#include <QQuickImageProvider>
#include <QHash>
#include <QString>
#include <QImage>

class TileImageProvider : public QQuickImageProvider
{
public:
    TileImageProvider();
    QImage requestImage(const QString &id, QSize *size, const QSize &requestedSize) override;

private:
    QHash<QString, QImage> mTiles;
};

#endif // TILEIMAGEPROVIDER_H

tileimageprovider.cpp:

#include "tileimageprovider.h"

#include <QImage>
#include <QDebug>

TileImageProvider::TileImageProvider() :
    QQuickImageProvider(QQmlImageProviderBase::Image)
{
    QImage tilesetImage(":/sprites/tiles/tileset.png");
    if (tilesetImage.isNull()) {
        qWarning() << "Failed to load tileset image";
        return;
    }

    int index = 0;
    for (int row = 0; row < 8; ++row) {
        for (int col = 0; col < 8; ++col) {
            int sourceX = col * 32;
            int sourceY = row * 32;
            QImage subTile = tilesetImage.copy(sourceX, sourceY, 32, 32);
            if (tilesetImage.isNull()) {
                qWarning() << "Tile image at" << sourceX << sourceY << "is null";
                return;
            }
            mTiles.insert(QString::number(index++), subTile);
        }
    }
}

QImage TileImageProvider::requestImage(const QString &id, QSize *size, const QSize &)
{
    Q_ASSERT(mTiles.find(id) != mTiles.end());
    *size = QSize(32, 32);
    return mTiles.value(id);
}

I then create tile instances from the following Component:

Image {
    width: 32
    height: 32
    smooth: false
    asynchronous: true
    source: "image://tile/" + index

    property int index
}

There's also Tiled's tile rendering code, which uses scenegraph nodes and is likely more efficient.

Mitch
  • 23,716
  • 9
  • 83
  • 122
0

It does seem like you can have it both easier and more efficient.

Instead of implementing a custom QQuickItem you can just use a trivial ShaderEffect. You can pass an offset and control the sampling.

Additionally, you would only need one single black and white plus, and you can have the color dynamic by passing it as a parameter to the shader.

Lastly, doing an atlas yourself is likely redundant, as the scenegraph will likely put small textures in atlases anyway. And of course, there is the advantage of not having to write a single line of C++, while still getting an efficient and flexible solution.

dtech
  • 47,916
  • 17
  • 112
  • 190
  • Damn, I forgot to mention another point, which is that these tiles are 32x32 pixels and will fill an entire fullscreen window. I had considered using shaders, but was worried about how it would perform. Would this be an issue in practice? The colour and plus icon were examples; the actual tiles are e.g. grass, water, etc. for a game. It's funny that you should mention the atlas - I was using individual tiles before which was working fine, but I switched to a tileset to make it easier to edit the tiles in the bitmap editor I'm using. – Mitch Apr 03 '17 at 06:10
  • You will end up executing a fragment shader for every pixel no matter what. That's just how opengl works. Thus a shader only solution could not possibly be worse. It might be beneficial to arrange images in an atlas yourself, since you might end up doing a better job that the scenegraph. I would not bother unless I run into VRAM usage issues. You can still keep it as a single image for easier editing, and use the tile offsets to write a simple script that will slice it down to individual images. – dtech Apr 03 '17 at 06:18
  • BTW, for what is essentially a bitmap (only 2 color values) you can expect for it to look crisp when upscaled without sampling, but for "grass, water, etc" I doubt it will be that pretty. You might want to investigate signed distance fields. – dtech Apr 03 '17 at 06:20
  • Hmmm, good point about the script. That could work quite well. The smooth property does what I need; see: http://stackoverflow.com/q/23579857/904422 By the way, can you provide an example of a shader that would do this? – Mitch Apr 03 '17 at 06:31
  • It is basically the good old "nearest neighbor", which is achieved by `floor()`-ing the default sampler value: https://csantosbh.wordpress.com/tag/nearest-neighbor-sampling/ – dtech Apr 03 '17 at 06:46
  • Oh, I meant with regards to the offset, but I guess I would need that too. :) – Mitch Apr 03 '17 at 06:53