VTK is slow when I have a lot of actors (>10000)

I’m trying to display a lot of actors in VTK but the rendering is very slow.
I’m wondering if I used VTK correctly to display that much actors.
In the code I have a mouse interactor which change the color of the selected actor. I would like to keep this faculty.
In the below example, I have one mapper per actor but it might not be the correct way to do it.
Any suggestion to speed up the rendering?
Here is a minimal example :

import vtk
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from vtk.qt.QVTKRenderWindowInteractor import QVTKRenderWindowInteractor
import sys
import numpy as np


NUMBER_OF_OBJECTS = 10000

class Tetrahedre(vtk.vtkUnstructuredGrid):
    def __init__(self, d=None, pos = None, scale = [1,1,1]):
        vtk.vtkUnstructuredGrid.__init__(self, )
        # Make a tetrahedron.
        points = vtk.vtkPoints()
        points.InsertNextPoint(pos[0]*scale[0]+0   ,pos[1]*scale[1]+0      ,pos[2]*scale[2]+2*d/3)
        points.InsertNextPoint(pos[0]*scale[0]-d/2 ,pos[1]*scale[1]-d/3  ,pos[2]*scale[2]-d/3)
        points.InsertNextPoint(pos[0]*scale[0]+d/2 ,pos[1]*scale[1]-d/3  ,pos[2]*scale[2]-d/3)
        points.InsertNextPoint(pos[0]*scale[0]+0   ,pos[1]*scale[1]+2*d/3  ,pos[2]*scale[2]-d/3)
        points.InsertNextPoint(pos[0]*scale[0]+0   ,pos[1]*scale[1]+0      ,pos[2]*scale[2]+2*d/3)

        self.SetPoints(points)

        ids = vtk.vtkIdList()
        ids.SetNumberOfIds(5)
        for i  in range(5) :
          ids.SetId(i, i)

        cellArray =vtk.vtkCellArray()
        cellArray.InsertNextCell(ids)

        self.SetCells(vtk.VTK_TETRA, cellArray)


class MouseInteractorHighLightActor(vtk.vtkInteractorStyleTrackballCamera):

    def __init__(self, parent=None):
        self.AddObserver("LeftButtonPressEvent", self.leftButtonPressEvent)

        self.LastPickedActor = None
        self.LastPickedProperty = vtk.vtkProperty()

    def leftButtonPressEvent(self, obj, event):
        clickPos = self.GetInteractor().GetEventPosition()
        print(clickPos)
        picker = vtk.vtkPropPicker()
        picker.Pick(clickPos[0], clickPos[1], 0, self.GetDefaultRenderer())

        # get the new
        self.NewPickedActor = picker.GetActor()

        # If something was selected
        if self.NewPickedActor:
            # If we picked something before, reset its property
            if self.LastPickedActor:
                self.LastPickedActor.GetProperty().DeepCopy(self.LastPickedProperty)

            # Save the property of the picked actor so that we can
            # restore it next time
            self.LastPickedProperty.DeepCopy(self.NewPickedActor.GetProperty())
            # Highlight the picked actor by changing its properties
            self.NewPickedActor.GetProperty().SetColor(1.0, 0.0, 0.0)

            # save the last picked actor
            self.LastPickedActor = self.NewPickedActor

        self.OnLeftButtonDown()
        return





class Graph_viewer3D_VTK(QMainWindow):
    def __init__(self, parent=None):
        super(Graph_viewer3D_VTK, self).__init__(parent)
        import platform
        if platform.system() == 'Darwin':
            self.facteurzoom = 1.05
        else:
            self.facteurzoom = 1.25

        self.parent=parent


        # self.scene = QGraphicsScene(self)
        # self.setScene(self.scene)

        self.frame = QFrame()

        self.vl = QVBoxLayout()
        self.vtkWidget = QVTKRenderWindowInteractor(self.frame)
        self.vl.addWidget(self.vtkWidget)

        self.ren = vtk.vtkRenderer()
        self.ren.SetBackground(.1, .1, .1)
        self.vtkWidget.GetRenderWindow().AddRenderer(self.ren)


        self.iren = self.vtkWidget.GetRenderWindow().GetInteractor()
        style = MouseInteractorHighLightActor(self)
        style.SetDefaultRenderer(self.ren)
        self.iren.SetInteractorStyle(style)


        self.ren.ResetCamera()

        self.frame.setLayout(self.vl)
        self.setCentralWidget(self.frame)
        self.show()
        self.iren.Initialize()
        self.iren.Start()

        self.selected_cells = []
    #

    def set_center(self):
        self.ren.ResetCamera()

    def draw_Graph(self,):
        self.ren.RemoveAllViewProps()

        # Add spheres to play with
        for i in range(NUMBER_OF_OBJECTS):
            # random position and radius
            x = vtk.vtkMath.Random(-500, 500)
            y = vtk.vtkMath.Random(-500, 500)
            z = vtk.vtkMath.Random(-500, 500)
            radius = vtk.vtkMath.Random(.5, 20.0)
            if np.random.randint(0,2):
                source = vtk.vtkSphereSource()



                source.SetRadius(radius)
                source.SetCenter(x, y, z)
                source.SetPhiResolution(11)
                source.SetThetaResolution(21)

                mapper = vtk.vtkPolyDataMapper()
                mapper.SetInputConnection(source.GetOutputPort())
                actor = vtk.vtkActor()
                actor.SetMapper(mapper)
            else:
                source = Tetrahedre(d=vtk.vtkMath.Random(.5, 20.0)*2, pos=[x,y,z])
                mapper = vtk.vtkDataSetMapper()
                mapper.SetInputData(source)

                actor = vtk.vtkActor()
                actor.SetMapper(mapper)

            r = vtk.vtkMath.Random(.4, 1.0)
            g = vtk.vtkMath.Random(.4, 1.0)
            b = vtk.vtkMath.Random(.4, 1.0)
            actor.GetProperty().SetDiffuseColor(r, g, b)
            actor.GetProperty().SetDiffuse(.8)
            actor.GetProperty().SetSpecular(.5)
            actor.GetProperty().SetSpecularColor(1.0, 1.0, 1.0)
            actor.GetProperty().SetSpecularPower(30.0)

            self.ren.AddActor(actor)


        self.iren.GetRenderWindow().Render()
        self.set_center()

class Window(QMainWindow):
    def __init__(self, parent=None):
        super(Window, self).__init__()
        self.centralWidget = QWidget()
        self.setCentralWidget(self.centralWidget)

        self.Graph_viewer = Graph_viewer3D_VTK(self)
        self.Graph_viewer.draw_Graph()
        layout = QVBoxLayout()
        layout.addWidget(self.Graph_viewer)
        self.centralWidget.setLayout(layout)


def main():
    app = QApplication(sys.argv)
    app.setStyle('Windows')
    ex = Window(app)
    ex.setWindowTitle('Model micro GUI')
    # ex.showMaximized()
    ex.show()
    sys.exit(app.exec())
if __name__ == '__main__':
    main()

Rendering is expected to be very slow if you have tens of thousands of actors. You would normally display this many objects using a single vtkGlyph3DMapper.

Currently, there is no efficient interaction/picking mechanism in VTK that would allow you to interact with tens of thousands of points. We reimplemented widgets and interactions in 3D Slicer from almost the ground up (keeping performance and multi-view synchronization in mind) and managed to get interactive performance with tens of thousands of points. See more information in this post: picking glyphs vtkGlyph3DMapper.

Okay.
I’m wondering if it is possible to have only one mapper for all the actors.
Like that I could still select the actor independently?

The GlyphMapper is indeed a better solution along with Hardware Selector.

You can even see an example of it in the web with vtk.js. Using VTK/C++ should even be simpler and provide better performances and capabilities.