QPainter 旋转阻止正确的 QPixmap 渲染

QPainter rotation prevents correct QPixmap rendering

作为错误报告给 Qt: https://bugreports.qt.io/browse/QTBUG-93475


我正在不同的位置重新绘制 QPixmap 多次,通过变换 QPainter 进行不同的旋转。在某些情况下 QPixmap 绘制不正确。下面的 GIF 显示了我最初发现此问题的 QPixmap 包含一个绿色圆柱体,请注意 GIF 左侧的渲染行为如何与预期的一样,但存在渲染不正确的边界。 QPixmap 内容似乎固定在原地,边缘的像素似乎在像素图的其余部分模糊不清。在 GIF 中 QPixmap 有一个洋红色背景,这是因为 QPainter::drawPixmap() 使用的 targetRect 也被用来单独填充像素图下方的矩形,这是因为我想检查目标矩形是否被正确计算。

最小可重现示例:

为了简单起见,我只是简单地用品红色像素填充 QPixmap,具有 1 像素宽的透明边缘,这样涂抹会导致像素图完全消失。它没有显示图像“固定”在适当的位置,但它清楚地显示了边界,因为边界之外的像素图似乎消失了。

我自己一直在试验,我相信这完全是由于 QPainter.

的旋转造成的

旋转的角度似乎有影响,如果所有的像素图都旋转到相同的角度,那么边界就会从一条模糊的对角线(模糊意味着消失的边界对于每个像素图不同)变为一个尖锐的 90 度角(其中尖锐意味着消失的边界对于所有像素图都是相同的)。

不同角度的范围似乎也有影响,如果随机生成的角度在 10 度的小范围内,那么边界只是一个稍微模糊的直角,有一个斜角。随着不同旋转次数的应用,似乎有从锐角到模糊对角线的进展。

代码

QtTestBed/pro:

QT += widgets

CONFIG += c++17
CONFIG -= app_bundle

# The following define makes your compiler emit warnings if you use
# any feature of Qt which as been marked deprecated (the exact warnings
# depend on your compiler). Please consult the documentation of the
# deprecated API in order to know how to port your code away from it.
DEFINES += QT_DEPRECATED_WARNINGS

# You can also make your code fail to compile if you use deprecated APIs.
# In order to do so, uncomment the following line.
# You can also select to disable deprecated APIs only up to a certain version of Qt.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000    # disables all the APIs deprecated before Qt 6.0.0

SOURCES += \
        MainWindow.cpp \
        main.cpp

# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target

HEADERS += \
    MainWindow.h

main.cpp:

#include "MainWindow.h"

#include <QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;
    w.show();
    return a.exec();
}

MainWindow.h:

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QWidget>
#include <QMouseEvent>
#include <QWheelEvent>
#include <QPainter>

#include <random>

class MainWindow : public QWidget {
    Q_OBJECT
public:
    MainWindow();

    void wheelEvent(QWheelEvent* event) override;
    void mouseReleaseEvent(QMouseEvent* /*event*/) override;
    void mousePressEvent(QMouseEvent* event) override;
    void mouseMoveEvent(QMouseEvent* event) override;
    void resizeEvent(QResizeEvent* /*event*/) override;
    void paintEvent(QPaintEvent* event) override;

private:
    struct PixmapLocation {
        QPointF location_;
        qreal rotation_;
        qreal radius_;
    };

    QPixmap pixmap_;
    std::vector<PixmapLocation> drawLocations_;

    qreal panX_ = 0.0;
    qreal panY_ = 0.0;
    qreal scale_ = 1.0;

    bool dragging_ = false;
    qreal dragX_ = 0.0;
    qreal dragY_ = 0.0;

    QPointF transformWindowToSimCoords(const QPointF& local) const;
    QPointF transformSimToWindowCoords(const QPointF& sim) const;

    static qreal randomNumber(qreal min, qreal max);
};

#endif // MAINWINDOW_H

MainWindow.cpp:

    #include "MainWindow.h"

MainWindow::MainWindow()
    : pixmap_(30, 50)
{
    setAutoFillBackground(true);

    constexpr int count = 10000;
    constexpr qreal area = 10000.0;
    constexpr qreal size = 44.0;

    for (int i = 0; i < count; ++i) {
        // qreal rotation = 0.0; // No rotation fixes the issue
        // qreal rotation = 360.0; // No rotation fixes the issue
        // qreal rotation = 180.0; // Mirroring also fixes the issue
        // qreal rotation = 90.0; // The boundary is now a corner, and has a sharp edge (i.e. all images dissapear at the same point)
        // qreal rotation = 0.1; // The boundary is now a corner, and has a sharp edge (i.e. all images dissapear at the same point)
        // qreal rotation = randomNumber(0.0, 10.0); // The boundary is still a corner, with a bevel, with a fuzzy edge (i.e. not all images dissapear at the same point)
        qreal rotation = randomNumber(0.0, 360.0); // The boundary appears to be a diagonal line with a fuzzy edge (i.e. not all images dissapear at the same point)

        drawLocations_.push_back(PixmapLocation{ QPointF(randomNumber(-area, area), randomNumber(-area, area)), rotation, size });
    }

    // Make edges transparent (the middle will be drawn over)
    pixmap_.fill(QColor::fromRgba(0x000000FF));

    /*
     * Fill with magenta almost up to the edge
     *
     * The transparent edge is required to see the effect, the misdrawn pixmaps
     * appear to be a smear of the edge closest to the boundary between proper
     * rendering and misrendering. If the pixmap is a solid block of colour then
     * the effect is masked by the fact that the smeared edge looks the same as
     * the correctly drawn pixmap.
     */
    QPainter p(&pixmap_);
    p.setPen(Qt::NoPen);
    constexpr int inset = 1;
    p.fillRect(pixmap_.rect().adjusted(inset, inset, -inset, -inset), Qt::magenta);

    update();
}

void MainWindow::wheelEvent(QWheelEvent* event)
{
    double d = 1.0 + (0.001 * double(event->angleDelta().y()));
    scale_ *= d;
    update();
}

void MainWindow::mouseReleaseEvent(QMouseEvent*)
{
    dragging_ = false;
}

void MainWindow::mousePressEvent(QMouseEvent* event)
{
    dragging_ = true;
    dragX_ = event->pos().x();
    dragY_ = event->pos().y();
}

void MainWindow::mouseMoveEvent(QMouseEvent* event)
{
    if (dragging_) {
        panX_ += ((event->pos().x() - dragX_) / scale_);
        panY_ += ((event->pos().y() - dragY_) / scale_);
        dragX_ = event->pos().x();
        dragY_ = event->pos().y();
        update();
    }
}

void MainWindow::resizeEvent(QResizeEvent*)
{
    update();
}

void MainWindow::paintEvent(QPaintEvent* event)
{
    QPainter paint(this);
    paint.setClipRegion(event->region());
    paint.translate(width() / 2, height() / 2);
    paint.scale(scale_, scale_);
    paint.translate(panX_, panY_);

    for (const PixmapLocation& entity : drawLocations_) {
        paint.save();
        QPointF centre = entity.location_;
        const qreal scale = (entity.radius_ * 2) / std::max(pixmap_.width(), pixmap_.height());

        QRectF targetRect(QPointF(0, 0), pixmap_.size() * scale);
        targetRect.translate(centre - QPointF(targetRect.width() / 2, targetRect.height() / 2));

        // Rotate our pixmap
        paint.translate(centre);
        paint.rotate(entity.rotation_);
        paint.translate(-centre);

        // paint.setClipping(false); // This doesn't fix it so it isn't clipping
        paint.drawPixmap(targetRect, pixmap_, QRectF(pixmap_.rect()));
        // paint.setClipping(true); // This doesn't fix it so it isn't clipping
        paint.restore();
    }
}

QPointF MainWindow::transformWindowToSimCoords(const QPointF& local) const
{
    qreal x = local.x();
    qreal y = local.y();
    // Sim is centred on screen
    x -= (width() / 2);
    y -= (height() / 2);
    // Sim is scaled
    x /= scale_;
    y /= scale_;
    // Sim is transformed
    x -= panX_;
    y -= panY_;
    return { x, y };
}

QPointF MainWindow::transformSimToWindowCoords(const QPointF& sim) const
{
    qreal x = sim.x();
    qreal y = sim.y();
    // Sim is transformed
    x += panX_;
    y += panY_;
    // Sim is scaled
    x *= scale_;
    y *= scale_;
    // Sim is centred on screen
    x += (width() / 2);
    y += (height() / 2);
    return { x, y };
}

qreal MainWindow::randomNumber(qreal min, qreal max)
{
    static std::mt19937 entropy = std::mt19937();

    std::uniform_real_distribution<qreal> distribution{ min, max };
    //    distribution.param(typename decltype(distribution)::param_type(min, max));
    return distribution(entropy);
}

我对这个问题的研究

左上角显示了所有以随机角度正确绘制的像素图

右上角显示了与左上角相同的实例,平移后像素图在模糊边界上,最右下角的像素图没有被绘制(或者更准确地说,被绘制为完全透明的像素,由于透明边缘被涂抹在整个图像上)

左下角显示所有像素图都旋转了 0.1 度,这导致了一个锐利的边界,当平移正方形使其重叠时,将正方形裁剪为矩形。

右下角显示了一个小范围的随机角度,介于 0.0 和 10.0 之间,这导致边缘稍微模糊,但仍然是垂直边缘,这看起来与左下角相似,但还有锐利的剪裁边缘,有还有轻微的渐变效果,因为靠近边缘的一些像素图也没有正确渲染。

我曾尝试在绘制像素图时在 QPainter 中关闭裁剪,但没有效果。

我曾尝试单独保存 QPainters 转换的副本,然后再将其设置回去,但没有任何效果。

我已经尝试升级到 Qt 6.0.3(声称已经解决了一些图形错误),问题仍然存在。

绝对坐标无关紧要,我可以将所有位置偏移 QPointF(-10000, 10000),平移​​到它们,消失点在 window 中的相同相对位置。

要查看正在运行的错误,请向外滚动,然后在 window 中单击并拖动以将像素图移动到屏幕的右下方,具体取决于您缩放的距离,一些将不再绘制像素图。

更新

我还发现,将原始 QPixmap 变大会使问题变得更糟,即边界在放大的级别上变得明显,并且会出现进一步的渲染偏差。请注意,它们仍被缩小到与以前相同的大小,只是源像素更多。

我把pixmap_(30, 50)改成了pixmap_(300, 500)

上图显示当平移以将像素图向右下方移动时,它们比以前消失得更快(即,当进一步放大并向左上方进一步放大时),弯曲的箭头表示绘制的像素图的移动在消失边界之外的弧中,它们的移动速度似乎比在移动时绘制的正确像素图快。

编辑:仔细检查表明明显的圆周运动不是真实的,像素图出现和消失的顺序只是让它看起来那样。通过下面的更新,您可以看到有同心环,其中已经消失的像素图重新出现(非常短暂地)在正确的位置,但重新出现只是针对一个看起来很薄的 window比像素图的尺寸窄,所以绘制的内容再次出现卡在原地,但显示的部分被剪裁了。

更新

要看到像素图“粘”在边界处,您可以将 MainWindow::MainWindow() 的内容调整为

MainWindow::MainWindow()
    : pixmap_(500, 500)
{
    setAutoFillBackground(true);
    
    constexpr int count = 10000;
    constexpr qreal area = 10000.0;
    constexpr qreal size = 44.0;
    
    for (int i = 0; i < count; ++i) {
        qreal rotation = randomNumber(0.0, 360.0);
        drawLocations_.push_back(PixmapLocation{ QPointF(randomNumber(-area, area), randomNumber(-area, area)), rotation, size });
    }
    
    // Make edges transparent (the middle will be drawn over)
    pixmap_.fill(QColor::fromRgba(0x000000FF));
    
    /*
     * Fill with magenta almost up to the edge
     *
     * The transparent edge is required to see the effect, the misdrawn pixmaps
     * appear to be a smear of the edge closest to the boundary between proper
     * rendering and misrendering. If the pixmap is a solid block of colour then
     * the effect is masked by the fact that the smeared edge looks the same as
     * the correctly drawn pixmap.
     */
    QPainter p(&pixmap_);
    p.setPen(Qt::NoPen);
    constexpr int smallInset = 1;
    const int bigInset = std::min(pixmap_.width(), pixmap_.height()) / 5;
    p.fillRect(pixmap_.rect().adjusted(smallInset, smallInset, -smallInset, -smallInset), Qt::magenta);
    p.fillRect(pixmap_.rect().adjusted(bigInset, bigInset, -bigInset, -bigInset), Qt::green);
    
    update();
}

这导致 看到似乎已被剪裁的正方形的右边缘。当四处移动它们时,正方形似乎卡在原地,而不是边界处的边缘消失,离边界最远的边缘首先消失。

这个问题很有意思。据我测试,你的代码看起来不错,我觉得这是一个 Qt 错误,我认为你需要向 Qt 报告它:https://bugreports.qt.io/。您应该 post 一段代码来说明问题,“更新”编辑中的第二段代码很好:它可以轻松重现问题。也许您还应该 post 一个小视频来说明当您缩放 in/out 或使用鼠标移动区域时出现的问题。

我尝试了一些替代方法希望找到解决方法,但我发现 none:

  • 尝试使用 QImage 而不是 QPixmap,同样的问题
  • 试图从冻结的 png/qrc 文件加载像素图,同样的问题
  • 尝试使用 QTransform 与 scale/translation/rotation 一起玩,同样的问题
  • 已尝试 Linux 和 Windows 10:观察到相同的问题

注意:

  • 如果不旋转(评论paint.rotate(entity.rotation_);),问题不可见
  • 如果您的像素图是一个简单的单色正方形(使用 pixmap_.fill(QColor::fromRgba(0x12345600)); 简单地用单一颜色填充您的像素图),则问题不再可见。这是最令人惊讶的,看起来图像中的一个像素被重新用作背景并弄乱了一切,但如果所有图像像素都相同,则不会导致任何显示问题。

Qt 团队提出的解决方法

"The issue can easily be worked around by enabling the SmoothPixmapTransform render hint on the painter"