高性能二维绘图

Performant 2D plotting

我正在寻找绘图库(目前使用 Qt 作为 UI),但我的问题如下:我需要绘制 数百万 个非常小的矩形canvas 并能够放大和缩小 canvas。要求:它必须非常快,最好是跨平台的。 "fast" 我的意思是在一秒钟内绘制数百万(或更多)个矩形。

到目前为止我尝试的一切都太慢了。我一直在使用 Qt+OpenGL,但是它有点太低级了,而且速度很慢(可能是因为缺乏 OpenGL 经验)。还有qcustomplot,可惜太迟钝了。甚至尝试 HTML+Canvas 作为实验。

对于那些想知道为什么我有这样的要求以及为了更好地理解我需要什么的人,请参阅此演示文稿:Heap Visualisation Tools(请参阅幻灯片 10 到 17 和 28-29)。

你有什么建议?什么 tool/library/framework 能够绘制数百万个矩形而没有明显的性能问题?如果有足够快的 JavaScript 库,我什至可以使用网络。也许在引擎盖下使用 LOD。

TL;DR:在 QImage.

上只使用 QPainter::drawRect,您应该能够在 4 核系统上一秒钟内绘制约 1000 万个小矩形

绘制一百万个矩形没什么大不了的,典型的 1920x1080 屏幕大约有 200 万像素,因此绘制 "small" 个矩形就像单独写入每一对像素,或者每个矩形写入 8 个字节,平均。

在我的特定系统 i5 iMac 运行 Qt 5.6 上,绘制 100 万个小矩形需要大约 1/3 秒:

#include <QtWidgets>
int main(int argc, char ** argv) {
  QApplication app{argc, argv};
  QImage image(1920, 1080, QImage::Format_ARGB32_Premultiplied);
  QPainter p(&image);
  QElapsedTimer timer;
  timer.start();
  int n = 0;
  for (int y = 0; y < image.height(); ++y)
    for (int x = 0; x < image.width(); x+=2) {
      ++ n;
      p.drawRect(x, y, 2, 1);
    }
  p.end();
  qDebug() << n << timer.elapsed();
}

如果您愿意,可以跨多个线程并行绘图。

// https://github.com/KubaO/Whosebugn/tree/master/questions/qimage-rectangles-37510435
#include <QtWidgets>
#include <QtConcurrent>

QVector<QRect> rects(const QSize & size) {
  QVector<QRect> rs;
  for (int y = 0; y < size.height(); ++y)
    for (int x = 0; x < size.width(); x+=2)
      rs.append(QRect(x, y, 2, 1));
  return rs;
}

QImage render(const QVector<QRect> & rects, const QSize & size, const QPair<int,int> range)
{
  QImage image(size, QImage::Format_ARGB32_Premultiplied);
  image.fill(Qt::transparent);
  QPainter p(&image);
  QElapsedTimer timer;
  timer.start();
  const int n = range.second-range.first;
  for (int i = range.first; i < range.second; ++i)
    p.drawRect(rects[i]);
  p.end();
  qDebug() << n << timer.elapsed();
  return image;
}

struct Render {
  const QVector<QRect> & rects;
  const QSize & size;
  typedef QImage result_type;
  QImage operator()(const QPair<int,int> range) { return render(rects, size, range); }
  Render(QVector<QRect>& rects, const QSize& size) : rects(rects), size(size) {}
};

template <typename Seq>
QVector<QPair<int,int>> partition(const Seq & s, int n)
{
  QVector<QPair<int,int>> ps;
  ps.reserve(n);
  int begin = 0;
  for (int i = 0; i < n; ++i) {
    int end = (s.count() * (i+1))/n;
    ps.append(qMakePair(begin, end));
    begin = end;
  }
  return ps;
}

void combine(QImage & result, const QImage & source)
{
  if (result.isNull()) {
    result = source;
    return;
  }
  QPainter p(&result);
  p.drawImage(0, 0, source);
}

int main(int argc, char ** argv) {
  QApplication app{argc, argv};
  QSize size{1920, 1080};
  auto rs = rects(size);
  auto ranges = partition(rs, QThread::idealThreadCount());
  QElapsedTimer t;
  t.start();
  QtConcurrent::blockingMappedReduced(ranges, Render(rs, size), combine);
  qDebug() << "parallel time" << t.elapsed() << "ms";
  t.restart();
  render(rs, size, qMakePair(0, rs.count()));
  qDebug() << "serial time" << t.elapsed() << "ms";
}

输出:

259200 94
259200 97
259200 102
259200 102
parallel time 112 ms
1036800 360
serial time 362 ms

如果您可以将矩形分组到具有相同 pen/brush 的组中,那么请充分利用 drawRects 比重复调用 drawRect 更快的事实(速度快约 20%我的机器)。

您也可以自己实现一个未转换的 drawRect 并使其更快。