Composante horizontale du champ de vitesse d'un écoulement (gauche vers droite) autour du logo LBM. La couleur bleue désigne des zones de recirculation.
Présentation
L'objectif du projet LBM est d'aboutir à la simulation d'un écoulement fluide en deux dimensions sur mobile (Android uniquement). J'ai choisi d'employer la Méthode Lattice Boltzmann (LBM) qui permet de simuler un écoulement compressible et visqueux en prenant en compte des conditions limites type mur en tout point du domaine.
Contrairement à la résolution directe des équations de Navier-Stokes à l'aide d'une méthode type différence finie, la méthode LBM est principalement locale et son traitement peut donc être facilement parallélisé.
Actuellement, deux implémentations ont été réalisées :
La première, réalisée en Java en 2014-2016, est disponible sur le Google play store ici. En plus d'un mode "bac à sable", permettant l'étude d'un fluide autour d'obstacles quelconques, cette version dispose d'un mode jeu de type "tower defense" (c.f. vidéo ci-contre). Afin d'obtenir des performances correctes, la simulation peut être réalisée sur plusieurs cœurs (jusque 4) en parallèle. Il est cependant difficile d'obtenir un résultat fluide pour des grilles dépassant les 150x75 nœuds (environ 160 it/s sur un Galaxy s8 en désactivant le rendu).
La seconde, réalisée en Kotlin en 2019 m'a permis de découvrir le monde OpenGL afin de gérer à la fois l'aspect graphique (affichage du champ de vitesse), mais aussi l'aspect GPGPU à l'aide des compute shaders permettant de réaliser la simulation directement sur GPU. L'utilisation du GPU permet d'obtenir de bien meilleures performances (environ 200 it/s sur un Galaxy s8 pour une grille de 384*728 nœuds avec rendu graphique à 50 fps).
Pour finir, voici un article détaillé (en anglais) décrivant le fonctionnement de la méthode LBM sur un réseau d2q9 (2D, 9 vecteurs vitesse).
Pour aller plus loin
Brève introduction à OpenGL
OpenGL est une librairie graphique permettant de gérer l'affichage d'une scène (2D ou 3D) directement partir de la carte graphique (GPU).
L'affichage d'une scène se déroule en plusieurs étapes :
Envoi des données (ou modification de celles-ci) dans la mémoire du GPU. Ces données représentent des coordonnées de points dans l'espace physique ainsi que des attributs associés, par exemple une couleur, une coordonnée dans une texture, etc. On parle ainsi de vertex. L'élément de base en OpenGL étant le triangle, il faut donc fournir 3 vertex pour décrire complétement un triangle et 6 vertex pour définir un carré.
Appel des shaders (codes écrits dans le langage glsl et exécutés en parallèles sur le GPU) :
Le vertex shader est un programme exécuté pour chaque vertex. Ce dernier prend en entrée les coordonnées du vertex doit obligatoirement renvoyer en sortie les coordonnées du vertex une fois projetée sur l'écran. Le point en bas à gauche a pour coordonnées [-1,-1] et le point en haut à droite [1,1]
Le fragment shader est un programme exécuté pour chaque pixel. Ce dernier prend en entrée les coordonnées à l'écran ainsi que d'éventuelles informations facultatives transmises par le vertex shader et interpolées au point considéré. Le fragment shader doit obligatoirement renvoyer en sortie la couleur du pixel.
D'autres types de shaders peuvent être utilisés pour générer une image mais leur utilisation n'est pas obligatoire. On peut ici citer pour l'exemple le geometry shader qui est appelé après le vertex shader et permet d'ajouter dynamiquement d'autres vertex à la géométrie.
En plus des shaders graphique, il est dorénavant possible d'appeler un compute shader qui n'est lié au pipeline graphique. Le compute shader peut être exécuté (en parallèle) directement sur le GPU et peut disposer de plusieurs types d'entrées (en plus de son numéro d'invocation) et de sorties. On peut envoyer un nombre (entier, réel, ...) au compute shader à l'aide d'une variable dite Uniform. De même, on peut aussi envoyer un tableau de données quelconque appelé Shader Storage Buffer Object (SSBO) au compute shader qui pourra ainsi le lire et éventuellement le modifier.
Le SSBO permet aussi de faire le lien entre le compute shader et le pipeline graphique. En effet, ce dernier peut être lu par les vertex/fragment shaders
Conseil pour réaliser une simulation physique avec OpenGL
De nombreux tutoriels existent pour l'utilisation d'OpenGL. Ces derniers supposent le plus souvent que le langage utilisé côté application est du C/C++
Ainsi, il est plus difficile d'obtenir des informations pour d'autres langages (comme Java ou Kotlin). N'hésitez donc pas à partir d'un projet existant dans le langage souhaité. Une fois le projet compris, vous pourrez alors l'adapter selon vos besoins. De mon côté, je me suis inspiré de l'application mandelGL dont le code source est disponible sous github.
Il est impossible de prédire l'ordre d'exécution des shaders. Il faut donc faire particulièrement attention lors de la modification d'une donnée (dans un SSBO) qui est susceptible d'être utilisée par un autre shader. Pour éviter cet écueil, j'ai choisi d'utiliser deux SSBOs distincts pour le stockage des données. Lors de la première itération, les anciennes données sont lues dans le premier et les nouvelles sont écrites dans le second. Les deux SSBOs sont ensuite permutés avant l'itération suivante et ainsi de suite.
La simulation est réalisée sur une grille de Nx par Ny nœuds. Chaque nœuds contient 12 données. La première indique si le nœud est dans une zone type fluide ou parois, les 9 suivants sont utilisées par la méthode LBM et les deux dernières sont utilisées pour les composantes horizontales et verticales du champ de vitesse. Un SSBO d'une taille de 12*Nx*Ny est donc créé depuis l'application et envoyé sur le GPU sous la forme d'un tableau unidimensionnel.
Ayant utilisé un Samsung Galaxy s8 pour le développement de mon application, j'ai observé que le GPU mali ne permet pas l'utilisation d'un SSBO à partir du vertex shader. J'ai donc choisi d'utilisé l'information simulée dans le compute shader directement dans le fragment shader. À partir des coordonnées du pixel et des entiers Nx et Ny, on peut retrouver l'indice correspondant à la composante verticale ou horizontale du champ de vitesse dans le SSBO puis renvoyer une couleur liée à cette valeur.