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 #
- Documentation officielle de vispy ou api.vispy.org
- Galerie d’exemples
- Page du projet vispy
- Version de développement de vispy sur GitHub
Architecture des modules #
Vispy est divisé en plusieurs modules :
vispy.app
- Application, event loops, canvas, backendsvispy.color
- Handling colorsvispy.geometry
- Visualization-related geometry routinesvispy.gloo
- User-friendly, Pythonic, object-oriented interface to OpenGLvispy.io
- Data IOvispy.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 plottingvispy.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
)