Python et la 3D avec vispy

Python et la 3D avec vispy #

Tutoriel réalisé avec la version 0.6 de vispy

vispy est un module Python en cours de développement qui apporte les fonctionnalités avancées des dernières versions d’OpenGL ainsi qu’une API de plus haut niveau pour la visualisation scientifique.

Nous allons nous concentrer ici sur les fonctionnalité de plus haut niveau et laisser de côté ce qui fait la puissance et la difficulté d’OpenGL (shaders etc…).

Documentations #

Architecture des modules #

Vispy est divisé en plusieurs modules :

  • vispy.app - Application, event loops, canvas, backends
  • vispy.color - Handling colors
  • vispy.geometry - Visualization-related geometry routines
  • vispy.gloo - User-friendly, Pythonic, object-oriented interface to OpenGL
  • vispy.io - Data IO
  • vispy.plot - OpenGL backend for matplotlib [experimental]
  • vispy.scene - The system underlying the upcoming high-level visualization interfaces [experimental]
  • vispy.visuals - The visuals that are used for high-level plotting
  • vispy.util - Miscellaneous utilities

Dans la suite, nous allons nous intéresser au sous module vispy.scene. Dans les programmes d’exemple, les importations seront toujours faites ainsi :

from vispy import app, scene
...
scene....

Structure du code #

Le code que nous allons réaliser suit cette trame :

  • création d’un canvas, une zone graphique dans laquelle vispy pourra dessiner
  • création d’une vue (view) dans ce canvas
  • mise en place de la caméra relative à la vue
  • peuplement de la vue par des objets
  • affichage du canvas
  • boucle des événements
from vispy import app, scene, geometry

# Création du canvas
canvas = scene.SceneCanvas(title="Vis3D", size=(800, 600), keys='interactive')

# Ajout de la vue dans le canvas (nous aurons toujours une seule vue)
view = canvas.central_widget.add_view()

# Caméra
# turntable est une caméra qui permet de tourner autour de la scène
view.camera = 'turntable'

Il reste à ajouter les objets à la scène, puis à :

  • afficher le canvas : canvas.show()
  • démarrer l’application app.run() (étape inutile si on utilise un shell interactif qui gère déjà les boucles d’événements)

Ajout d’objets dans la scène #

Les objets 2D ou 3D sont ajoutés dans une vue ainsi :

view.add(objet)

Toutefois, avant d’ajouter objet il faut le créer.

Les objets qu’il est possible d’ajouter à une vue sont de la classe Node ou d’une classe héritant de Node. C’est le cas des objets du sous module scene.visuals qui héritent entre autres de Node. C’est donc dans ce module qu’on trouvera les objets de haut-niveau qu’il est possible d’ajouter à une scène comme :

  • Cube
  • Ellipse
  • GridLine
  • Image
  • Line
  • Mesh
  • Polygon
  • RegularPolygon

Voyons comment ajouter un «cube» à la scène :

# Création du parallélépipède (dimensions 2, 1, 5), faces cyan, et arêtes rouges
c = scene.visuals.Cube((2.0, 1.0, 5.0), color=(0, 1, 1, 1), edge_color='red')
# Ajout du cube à la scène 
view.add(c)

Après avoir fait canvas.show() et éventuellement app.run(), le parallélépipède apparaît :

src/test_vispy_0.py  
from vispy import app, scene, geometry

# Création du canvas
canvas = scene.SceneCanvas(title="Vis3D", size=(800, 600), keys='interactive')

# Ajout de la vue dans le canvas (nous aurons toujours une seule vue)
view = canvas.central_widget.add_view()

# Caméra
# turntable est une caméra qui permet de tourner autour de la scène
view.camera = 'turntable'

# Création du parallélépipède (dimensions 2, 1, 5), faces cyan, et arêtes rouges
c = scene.visuals.Cube((2.0, 1.0, 5.0), color=(0, 1, 1, 1), edge_color='red')
# Ajout du cube à la scène
view.add(c)

canvas.show()

app.run()

On peut le faire tourner à la souris et zoomer ou dézoomer avec la molette (n’essayez pas sur l’image… 😀)

Notez que les couleurs peuvent être données sous la forme de triplets RGBA de nombres entre 0 et 1 ou sous la forme d’une chaîne de caractères.

Création d’autres objets #

visuals.Mesh permet de créer des objets quelconques à partir de données sur les sommets, les arêtes et les faces. Ces donnes peuvent être construites manuellement, ou bien à l’aide des fonction du module geometry.

La fonction create_sphere renvoie par exemple les données des triangles matérialisant une sphère de manière plus ou moins fine (les deux entiers passés en paramètres indiquent le nombre de subdivisions de la sphère… avec 2 et 3 on obtient 2 tétraèdres accolés). Puis ces données sont utilisées pour créer un nouvel objet, qui est enfin ajouté.

mdata = geometry.create_sphere(2, 3,  radius=3)
mesh = scene.visuals.Mesh(meshdata=mdata, shading='flat')
view.add(mesh)

L’objet a ici une couleur par défaut. Sans l’option shading='flat', toutes les faces auront exactement la même couleur, rendant délicate l’interprétation de la 3D.

Pour donner une couleur à cet objet, on peut procéder de plusieurs manières :

  • donner une couleur au moment de la création du nœud
mesh = scene.visuals.Mesh(meshdata=mdata, shading='flat', color='red')
  • donner une couleur à chacune des faces (if faut donner un tableau de 6 couleurs dans ce cas) directement sur les données géométriques
mdata = geometry.create_sphere(2, 3,  radius=5)
mdata.set_face_colors([[1,0,0,1],[0,1,0,1], [0,0,1,1], [1,1,0,1], [1,0,1,1], [0,1,1,1]], indexed=None)
mesh = scene.visuals.Mesh(...)

Le module contient d’autres fonctions qui peuvent être utilisées sur le même principe. En particulier, MeshData permet de décrire un objet en donnant la liste des sommets, des arêtes et des faces. Voici un exemple complet avec un tétraèdre ayant ses 4 faces colorées d’une couleur différente.

src/test_vispy_1.py  
from vispy import app, scene, geometry
import numpy as np # utilisé pour les listes d'arêtes et de faces

# Création du canvas
canvas = scene.SceneCanvas(title="Vis3D", size=(800, 600), keys='interactive')

# Ajout de la vue dans le canvas (nous aurons toujours une seule vue)
view = canvas.central_widget.add_view()

# Caméra
# turntable est une caméra qui permet de tourner autour de la scène
view.camera = 'turntable'

# Position des sommets du tétraèdre
pos = [[0,0,1], [1,0,0], [-0.5, 0.806,0], [-0.5, -0.806, 0]]

# Création de l'objet
mdata=geometry.MeshData(vertices = np.array(pos), 
                        edges=np.array([[0, 1],[0, 2],[0, 3],[1, 2],[1, 3],[2, 3]], dtype=np.uint32),
                        faces=np.array([[0, 1, 3],[1, 2, 3],[2, 0, 3],[0,1,2]], dtype=np.uint32))
mdata.set_face_colors([[1,0,0,1],[0,1,0,1], [0,0,1,1], [1,1,0,1]], indexed= None)
mesh = scene.visuals.Mesh(meshdata=mdata, shading='flat')

view.add(mesh)

canvas.show()

app.run()

Transformations appliquées aux objets #

Les objets créés sont souvent centrées en (0, 0, 0) avec des directions privilégiées (sur les axes). Il est naturellement possible de leur appliquer translations et rotations…

L’idée est d’affecter une transformation à l’objet avant de l’ajouter à la scène. Supposons que nous ayons un objet de type Mesh :

mesh = scene.visuals.Mesh(...)

Nous créons une transformation affine :

tr = scene.transforms.MatrixTransform()

L’objet tr renvoyé contient la matrice de transformation (pour l’instant c’est l’identité).

Puis on peut ajouter des transformations élémentaires (ce qui correspond à multiplier la matrice de transformation) :

import math
tr.rotate(math.pi/3, (0.0, 0.0, 1.0))
tr.translate((-1.0, 0.0, 2.0))

La multiplication est faite à gauche, ce qui signifie que la transformation est une rotation puis une translation et non l’inverse.

On affecte enfin la transformation à l’objet, qu’on ajoute ensuite à la vue :

mesh.transform = tr
view.add(mesh)

Gestion des événements #

Une solution pour gérer les événements (clavier, souris) est de créer un Canvas personnalisé qui hérite du Canvas standard. Ainsi, à la place de :

canvas = scene.SceneCanvas(title="Vis3D", size=(800, 600), keys='interactive')

on écrira :

class MonCanvas(scene.SceneCanvas):
    def on_key_press(self, event):
        print("You pressed '{}'".format(event.text))
    def on_mouse_press(self, event):
        print("You clicked button {}, pos {}".format(event.button, event.pos))

canvas = MonCanvas(title="Vis3D", size=(800, 600), keys='interactive')

Toutefois, la définition précise des callbacks semble dépendre du backend employé, de la plate-forme…

Un exemple complet #

Voici un programme complet qui illustre les éléments présentés dans ce document. Il est fonctionnel sour Linux, avec Python 3.8 et vispy 0.6…

src/test_vispy.py  
from vispy import app, scene, geometry
import numpy as np # utilisé pour les listes d'arêtes et de faces

class MonCanvas(scene.SceneCanvas):

    def on_key_press(self, event):
        print("You pressed '{}'".format(event.text))
    def on_mouse_press(self, event):
        print("You clicked button {}, pos {}".format(event.button, event.pos))


def translate(obj, vect):
    t = scene.transforms.MatrixTransform()
    t.translate(vect)
    obj.transform = t

def create_scene(view):
    # Création du parallélépipède faces cyan, et arêtes rouges
    c = scene.visuals.Cube((.5, 1.0, 1.0), color=(0,1,1,1), edge_color='red')
    translate(c, (-2,0,0))
    view.add(c)

    # Création de la sphère
    mdata = geometry.create_sphere(32, 32,  radius=1)
    mesh = scene.visuals.Mesh(meshdata=mdata, shading='flat')
    translate(mesh,(0,0,2))
    view.add(mesh)

    # Création du tétraèdre
    pos = [[0,0,2], [2,0,0], [-1, 1.6,0], [-1, -1.6, 0]]
    mdata=geometry.MeshData(vertices = np.array(pos),
                            edges=np.array([[0, 1],[0, 2],[0, 3],[1, 2],[1, 3],[2, 3]], dtype=np.uint32),
                            faces=np.array([[0, 1, 3],[1, 2, 3],[2, 0, 3],[0,1,2]], dtype=np.uint32))
    mdata.set_face_colors([[1,0,0,1],[0,1,0,1], [0,0,1,1], [1,1,0,1]], indexed= None)

    mesh = scene.visuals.Mesh(meshdata=mdata, shading='smooth') # Try 'flat'
    translate(mesh, (2,1,0) )
    view.add(mesh)

# Création du canvas
canvas = MonCanvas(title="Vis3D", size=(800,600), keys='interactive')
# Ajout de la vue dans le canvas (nous aurons toujours une seule vue)
view = canvas.central_widget.add_view()
# Réglage de la caméra
view.camera = "turntable"
view.camera.distance=10
create_scene(view)
#view.update()
canvas.show()
app.run()

Quelques réalisations #

Visualisation de molécules à partir de fichiers PDB (ici la caféine) :

Génération et visualisation de terrains fractals (utilisation de SurfacePlot)