Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to handle a huge amout of tiles in QML?

Tags:

graphics

qt

qml

Imagine a huge rectangular grid filled with tiles. The individual tiles are not very complicated, they are svg images containing a low amount of shapes.

The number of different types of tiles in not very large, I estimate in the low hundreds. However, the grid can become very large, so the number of total tiles is huge (at least tens of thousands, maybe more).

I have to be able to smoothly scroll the grid both horizontally and vertically, as well as smoothly zoom it in and out. I also have to be able to jump to a specific position.

It would also be nice if I could populate it asynchronously, first the elements which are actually visible, and then the rest. This means that a table-handling class where I first have to add rows and columns in a loop would not be the best solution, because the starting position is not necessarily the upper left corner.

Zooming is simply achieved by having all the width and height properties of the items within a tile specified as a multiple of a scaling factor. The svg shouldn't be a problem as the number of different images is not high, it should be able to be cached. In the unlikely case svg became the bottleneck, I could just use sets of different pngs in different resolutions.

I tried (or considered) the following approaches:

  1. Using the methods of the SameGame example, creating QML objects dynamically (Component.createObject). This works if the number of objects is small, but is very slow with a large number of objects. Even if the objects are completely empty, this method takes a very long time.

  2. Using a Repeater inside a Flickable. The Flickable contains a Grid, which is populated by a Repater. The Grid, of course, is immense. This method is faster than creating the objects dynamically, but still inefficient as the number of tiles grows. The QML engine keeps track of every item, even those which are not visible. Zooming is also quite slow, as the properties of every item are recalculated, not just the visible ones.

  3. Using a GridView. This looks like the perfect solution at a first glance. The GridView inherits Flickable, and it also takes care to only render contents which are within the bounds of the view. Even a test case with millions of svg images runs reasonably fast, and it scrolls and resizes smoothly. There is only one problem: The GridView is only flickable either horizontally or vertically, but not both. There has been a feature request about this since 2012, but it still seems to be ignored.

  4. Using a QGraphicsView directly. It is capable of displaying, scrolling and zooming the needed amount of elements, but it's not QML-based. The rest of my GUI is in QML, and I've only read horror stories about combining QML and QGraphicsView. I've never seen any reasonable examples of it.

What other solutions are there? Some horrible hack of using Javascript to add and remove rows and columns of a simple GridLayout (which is only a couple rows and columns larger than the visible area) while it is moved around in a Flickable? Or just embedding an OpenGL window and drawing everything manually?

I hope this shouldn't be an impossible task. There were strategy games written more than 20 years ago for DOS and Windows 95 which could handle this amount of tiles, while additionally having textures and animations.

like image 322
vsz Avatar asked Aug 31 '16 08:08

vsz


1 Answers

Yeah, Qt is very good at ignoring community suggestions for years, even if they would be extremely useful, considered important, and happen to be the most up-voted, such as zip support.

I personally wouldn't bother "fixing" GridView, but rather implement something from scratch that suits my specific requirements, in C++ so that it is fast and efficient. And it will be very easy if your tiles are uniform squares, and it sounds like you could get away with that, even if the actual images inside are not square. This will make it very easy to determined their positions programmatically, and also determine the top left corner tile, how much tiles per line and the stride for the subsequent lines. Then as the visibility rectangle moves you iterate your container and signal to create QML elements for those which enter visibility. Easy peasy.

You don't need anything fancy, just inherit QObject, register the type to QML, then go and populate it's internal "model". You definitely do not want to have all the objects in memory, even if the scene graph is smart enough to not render them, it will still be processing them, I suspect your drop in FPS is not the product of a GPU but a CPU bottleneck.

The actual grid object can emit creation and destruction signals with their data Q_SIGNAL void create(x, y, imgPath);, so you bind custom handlers on the QML side, which will give you flexibility and ease of use, such as easily specifying the "delegate" object, it will be more elegant than doing the actual creation/destruction in C++. You can use bindings on the QML side for the few items that are visible to track when they go out of screen to self-destruct, that would minimize complexity, as you won't have to track all the "living" objects.

Component {
   id: objComponent
   Image {
      property bool isVisible: { is in grid.visibleRect ??? }
      onIsVisibleChanged: if (!isVisible) destroy()
   }
}

MyGrid {
  id: grid
  contentX: flickable.contentX
  contentY: flickable.contentY

  onCreate: objComponent.createObject(flickable.contentItem, {"x" : x, "y" : y, "source" : imgPath})
}

Flickable {
   id: flickable
   contentWidth: grid.contentWidth
   contentHeight: grid.contentHeight
}

Normally, when a user has a question, important enough to offer a bounty I'd produce working code, but unfortunately I am currently too busy. The concept is pretty simple though and should not be too problematic to implement.

like image 84
dtech Avatar answered Sep 24 '22 02:09

dtech