Rendering a section of a non-smooth QImage using QSGImageNode

假装没事ソ 提交于 2020-01-17 05:51:12

问题


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

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?


回答1:


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.




回答2:


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.




回答3:


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.



来源:https://stackoverflow.com/questions/43172503/rendering-a-section-of-a-non-smooth-qimage-using-qsgimagenode

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!