How to draw 10000+ 2d graphical objects quickly?

I need to draw tens of thousands, in the future probably hundreds of thousands of simple 2d objects (circles, rectangles, some filled, labeled...) to a widget.

In the same program I need GUI widgets (buttons, text input, checkboxes).

I tried Gtk, Qt and SDL with C++ and Python. First result was to be expected: C++ and Python show the same performance, as they call the same C or C++ routines in the backend.

Second result is that none of the libraries made a big difference. In numbers: 22500 rectangles (150*150) needed approximately a second to update. As there will be constant upadating due to (a) new data, i.e. more rectangles, and (b) user interaction, i.e. zooming, panning etc., a second is way to long!

What would be a faster way. Small examples are very much appreciated. Python and C++ is good. Other libraries should be easily accessible and installable on Linux.

Maybe I am just doing it wrong.

ps. I am not posting my test codes, because I don't want to bias answers. And I don't want my code to be corrected, I want to the fastest way to do it...

edit:

Alright, I will add my gtk test:

#!/bin/env python2

import gtk
import gobject
import gtk.gdk

class GraphWidget(gtk.DrawingArea):
    __gsignals__ = {
        'expose-event': 'override',
        'clicked' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_STRING, gtk.gdk.Event))
        }

    def __init__(self,window):
        gtk.DrawingArea.__init__(self)
        #self.win = window
        self.zoom_ratio = 1.0
        self.dx = 40
        self.dy = 40

    def do_expose_event(self, event):
        cr = self.window.cairo_create()
        cr.set_source_rgba(1.0, 0.9, 0.8, 1.0)
        cr.paint()

        cr.translate(self.dx, self.dy)
        cr.scale(self.zoom_ratio,self.zoom_ratio)

        self.draw(cr)

    def draw(self, cr):
        n = 150
        cr.set_source_rgba(0.,1.,1.,1.0)
        for i in range(n):
            for j in range(n):
                cr.arc(i*30, j*30, 10, 0, 6.2832)

                cr.close_path()
                cr.fill()

        cr.set_source_rgba(0.,0.,1.,1.0)
        for i in range(n):
            for j in range(n):
                cr.arc(i*30, j*30, 10, 0, 6.2832)
                cr.move_to(i*30-10, j*30)
                cr.show_text("hu")
                cr.stroke()

    def on_zoom(self, zoom_factor):
        self.zoom_ratio *= zoom_factor
        self.queue_draw()

    def on_translate(self,dx,dy):
        self.dx += dx
        self.dy += dy
        self.queue_draw()

class TestWindow(gtk.Window):
    def __init__(self):
        gtk.Window.__init__(self)

        self.widget = GraphWidget(self)

        self.add(self.widget)

        self.show_all()

        # connect key press events

        self.connect('key-press-event', self.on_key_press_event)
        self.connect('destroy', gtk.main_quit)

        self.widget.queue_draw()

    def on_key_press_event(self, widget, event):

        if event.keyval == gtk.keysyms.space and not (event.state & gtk.gdk.CONTROL_MASK):
            self.on_run(widget)
            return True
        elif event.keyval == gtk.keysyms.r:
            self.on_refresh(widget)
            return True
        elif event.keyval == gtk.keysyms.Left:
            self.widget.on_translate(-100, 0)
        elif event.keyval == gtk.keysyms.Right:
            self.widget.on_translate(100, 0)
        elif event.keyval == gtk.keysyms.Up:
            self.widget.on_translate(0, -100)
        elif event.keyval == gtk.keysyms.Down:
            self.widget.on_translate(0, 100)
        elif event.keyval == gtk.keysyms.Page_Down:
            self.widget.on_zoom(0.7)
        elif event.keyval == gtk.keysyms.Page_Up:
            self.widget.on_zoom(1.3)

if __name__ == '__main__':
    win = TestWindow()
    gtk.main()

And the SDL experiment:

#!/usr/bin/env python

import sdl2
import sdl2.ext as sdl2ext

dx = 0
dy = 0
zoom_factor = 1.
n_objects = 150

sdl2ext.init()

window = sdl2ext.Window('hallo', 
                        size=(800, 600), 
                        flags= sdl2.SDL_WINDOW_RESIZABLE)
window.show()

renderer = sdl2ext.RenderContext(window)
renderer.color = sdl2ext.Color(255,155,25)

def draw():
    renderer.clear()
    for i in xrange(n_objects):
        for j in xrange(n_objects):
            renderer.fill([int((i*30+dx)*zoom_factor), 
                           int((j*30+dy)*zoom_factor), 
                           int(20*zoom_factor), 
                           int(20*zoom_factor)], 
                          sdl2ext.Color(255,25,55))
            renderer.draw_rect([int((i*30+dx)*zoom_factor), 
                                int((j*30+dy)*zoom_factor), 
                                int(20*zoom_factor), 
                                int(20*zoom_factor)], 
                               sdl2ext.Color(255,255,255))

    renderer.present()

draw()

running = True
while running:
    for e in sdl2ext.get_events():
        if e.type == sdl2.SDL_QUIT:
            running = False
            break
        if e.type == sdl2.SDL_KEYDOWN:
            if e.key.keysym.sym == sdl2.SDLK_ESCAPE:
                running = False
                break
            elif e.key.keysym.sym == sdl2.SDLK_RIGHT:
                dx += 50
                draw()
            elif e.key.keysym.sym == sdl2.SDLK_LEFT:
                dx -= 50
                draw()
            elif e.key.keysym.sym == sdl2.SDLK_UP:
                dy += 50
                draw()
            elif e.key.keysym.sym == sdl2.SDLK_DOWN:
                dy -= 50
                draw()
            elif e.key.keysym.sym == sdl2.SDLK_PAGEUP:
                zoom_factor *= 1.2
                draw()
            elif e.key.keysym.sym == sdl2.SDLK_PAGEDOWN:
                zoom_factor /= 1.2
                draw()

The Qt test was done by my colleague so I don't have the code right now...

Answers


Looks like you're drawing every single object regardless of whether or not it's in the viewable area. Have you considered storing an array of the positions of each object, and then test each to determine if it's in the screen's viewable area before drawing it?

I did a quick and dirty test to try it out (the code could be much cleaner, I'm sure), and it's much more responsive drawing only what's visible:

First I created some variables to determine the visible boundary (you would update the boundaries every time move and zoom, window resize, etc happen):

boundX = [-60, 160]
boundY = [-60, 160]

In the draw event, I'm checking if the position is within the boundry before drawing. I also consolidated your loop to increase efficiency, you can draw and add text in the same iteration. Even testing this with n = 50000 is more responsive than what was there before.

def draw(self, cr):
    n = 150

    for i in range(n):
        if min(boundX) < i * 30 < max(boundX):
            for j in range(n):
                if min(boundY) < j * 30 < max(boundY):
                    cr.set_source_rgba(0.,1.,1.,1.0)
                    cr.arc(i*30, j*30, 10, 0, 6.2832)

                    cr.close_path()
                    cr.fill()

                    cr.set_source_rgba(0.,0.,1.,1.0)
                    cr.arc(i*30, j*30, 10, 0, 6.2832)
                    cr.move_to(i*30-10, j*30)
                    cr.show_text("hu")
                    cr.stroke()

And in the keypress events, just increment and decrement the boundry by the amount the widget is being translated:

def on_key_press_event(self, widget, event):

    if event.keyval == gtk.keysyms.space and not (event.state & gtk.gdk.CONTROL_MASK):
        self.on_run(widget)
        return True
    elif event.keyval == gtk.keysyms.r:
        self.on_refresh(widget)
        return True
    elif event.keyval == gtk.keysyms.Left:
        boundX[0] += 100
        boundX[1] += 100
        self.widget.on_translate(-100, 0)
    elif event.keyval == gtk.keysyms.Right:
        boundX[0] -= 100
        boundX[1] -= 100
        self.widget.on_translate(100, 0)
    elif event.keyval == gtk.keysyms.Up:
        boundY[0] += 100
        boundY[1] += 100
        self.widget.on_translate(0, -100)
    elif event.keyval == gtk.keysyms.Down:
        boundY[0] -= 100
        boundY[1] -= 100
        self.widget.on_translate(0, 100)
    elif event.keyval == gtk.keysyms.Page_Down:
        self.widget.on_zoom(0.7)
    elif event.keyval == gtk.keysyms.Page_Up:
        self.widget.on_zoom(1.3)

Need Your Help

read a binary file (python)

python file io

I cant read a file, and I dont understand why:

Is it possible to get the file name and line number of the clojurescript file using a macro?

clojurescript

I am looking for a way to grab the file name and line number information of a particular piece of code from the cljs analyser using a macro. Is this possible?