Figure 1. Un exemple de rendus volumétriques effectués par le rendu WebGL décrit dans l'article. À gauche: simulation de la distribution de probabilité spatiale des électrons dans une molécule de protéine à haut potentiel. À droite: tomographie d'un bonsaï. Les deux jeux de données sont extraits du référentiel Open SciVis Datasets .En visualisation scientifique, le rendu volumétrique est largement utilisé pour visualiser des champs scalaires tridimensionnels. Ces champs scalaires sont souvent des grilles de valeurs homogènes représentant, par exemple, la densité de charge autour de la molécule, une IRM ou un scanner, un flux d'air enveloppant l'avion, etc. Le rendu volumétrique est une méthode conceptuellement simple pour convertir ces données en images: en échantillonnant les données le long des rayons de l'œil et en attribuant de la couleur et de la transparence à chaque échantillon, nous pouvons créer des images utiles et belles de ces champs scalaires (voir figure 1). Dans le rendu GPU, ces champs scalaires tridimensionnels sont stockés sous forme de textures 3D; cependant, WebGL1 ne prend pas en charge les textures 3D, des hacks supplémentaires sont donc nécessaires pour les émuler dans le rendu de volume. WebGL2 a récemment ajouté la prise en charge des textures 3D, permettant au navigateur d'implémenter un rendu de volume élégant et rapide. Dans cet article, nous discuterons des fondements mathématiques du rendu volumétrique et expliquerons comment l'implémenter sur WebGL2 pour créer un rendu volumétrique interactif qui fonctionne complètement dans le navigateur! Avant de commencer, vous pouvez tester le moteur de rendu volumétrique en
ligne décrit dans cet article.
1. Introduction
Figure 2: rendu physique volumétrique, prenant en compte l'absorption et l'émission de lumière en volume, ainsi que les effets de diffusion.Pour créer une image physiquement réaliste à partir de données volumétriques, nous devons simuler la façon dont les rayons lumineux sont absorbés, émis et diffusés par le milieu (figure 2). Bien que la modélisation de la propagation de la lumière à travers un médium à ce niveau crée de beaux résultats et physiquement corrects, elle est trop coûteuse pour le rendu interactif, ce qui est l'objectif du logiciel de visualisation. Dans les visualisations scientifiques, le but ultime est de permettre aux scientifiques de rechercher de manière interactive leurs données, de poser des questions sur leur tâche de recherche et d'y répondre. Étant donné qu'un modèle de diffusion entièrement physique sera trop coûteux pour le rendu interactif, les applications de visualisation utilisent un modèle simplifié d'absorption des émissions, ignorant les effets de diffusion coûteux ou les rapprochant d'une manière ou d'une autre. Dans cet article, nous considérons uniquement le modèle émission-absorption.
Dans le modèle d'émission-absorption, nous calculons les effets d'éclairage qui apparaissent sur la figure 2 uniquement le long du rayon noir, et ignorons ceux qui proviennent des rayons gris en pointillés. Les rayons traversant le volume et atteignant les yeux accumulent la couleur émise par le volume et s'estompent progressivement jusqu'à ce qu'ils soient complètement absorbés par le volume. Si nous suivons les rayons de l'œil à travers le volume, nous pouvons calculer la lumière entrant dans l'œil en intégrant le faisceau sur le volume afin d'accumuler l'émission et l'absorption le long du faisceau. Prenez un rayon tombant dans le volume en un point
et hors de volume à un moment donné
. Nous pouvons calculer la lumière entrant dans l'œil en utilisant l'intégrale suivante:
Au fur et à mesure que le faisceau traverse le volume, nous intégrons la couleur émise
et l'absorption
à chaque point
le long de la poutre. La lumière émise à chaque point s'estompe et renvoie à l'œil l'absorption du volume jusqu'à ce point, qui est calculé par
.
Dans le cas général, cette intégrale ne peut pas être calculée analytiquement; par conséquent, une approximation numérique doit être utilisée. Nous effectuons l'approximation de l'intégrale en prélevant de nombreux échantillons
le long du rayon dans l'intervalle
dont chacun est situé à distance
séparément les uns des autres (figure 3), et en résumant tous ces échantillons. Le terme d'amortissement à chaque point d'échantillonnage devient le produit de la série accumulant l'absorption dans les échantillons précédents.
Pour simplifier encore plus cette somme, nous approchons le terme d'amortissement (
) lui près de Taylor. Pour plus de commodité, nous introduisons alpha
. Cela nous donne l'équation de la composition alpha effectuée d'avant en arrière:
Figure 3: Calcul de l'intégrale du rendu de l'émission-absorption en volume.L'équation ci-dessus se réduit à une boucle for, dans laquelle nous parcourons le faisceau étape par étape à travers le volume et accumulons de manière itérative la couleur et l'opacité. Ce cycle se poursuit jusqu'à ce que le faisceau quitte le volume ou que la couleur accumulée devienne opaque (
) Le calcul itératif de la somme ci-dessus est effectué en utilisant les équations de composition front-to-back connues:
Ces équations finales contiennent l'opacité précédemment multipliée pour un mélange correct,
.
Pour rendre une image de volume, tracez simplement le rayon de l'œil à chaque pixel, puis effectuez l'itération indiquée ci-dessus pour chaque rayon traversant le volume. Chaque rayon (ou pixel) traité est indépendant, donc si nous voulons rendre l'image rapidement, nous avons besoin d'un moyen de traiter un grand nombre de pixels en parallèle. C'est là que le GPU est utile. En implémentant le processus de raymarching dans le fragment shader, nous pouvons utiliser la puissance de l'informatique GPU parallèle pour implémenter un rendu de volume très rapide!
Figure 4: Raymarching sur une grille de volume.2. Implémentation du GPU sur WebGL2
Pour que le raymarching soit effectué dans le fragment shader, il est nécessaire de forcer le GPU à exécuter le fragment shader au niveau des pixels le long desquels nous voulons tracer le rayon. Cependant, le pipeline OpenGL fonctionne avec des primitives géométriques (figure 5) et n'a aucun moyen direct d'exécuter un fragment shader dans une zone spécifique de l'écran. Pour contourner ce problème, nous pouvons rendre une sorte de géométrie intermédiaire pour exécuter le fragment shader sur les pixels que nous devons rendre. Notre approche du rendu du volume sera similaire à celle de
Shader Toy et des rendus de
scène de démonstration , qui rendent deux triangles en plein écran pour effectuer un shader de fragment, puis il fait le vrai travail de rendu.
Figure 5: Le pipeline OpenGL dans WebGL se compose de deux étapes de shaders programmables: un vertex shader, qui est responsable de la conversion des sommets d'entrée en un espace de clip, et un fragment shader, qui est responsable de l'ombrage des pixels couverts par le triangle.Bien que le rendu de deux triangles plein écran à la manière de ShaderToy fonctionnera, il effectuera un traitement de fragment inutile lorsque le volume ne couvrira pas tout l'écran. Ce cas est assez courant: les utilisateurs éloignent la caméra du volume pour regarder un grand nombre de données en général ou pour étudier de grandes parties caractéristiques. Pour limiter le traitement des fragments aux seuls pixels affectés par le volume, nous pouvons pixelliser le parallélogramme de délimitation de la grille de volume, puis effectuer l'étape de raymarching dans le fragment shader. De plus, nous n'avons pas besoin de rendre les faces avant et arrière du parallélogramme, car avec un certain ordre de rendu des triangles, le fragment shader dans ce cas peut être effectué deux fois. De plus, si nous ne rendons que les faces avant, nous pouvons rencontrer des problèmes lorsque l'utilisateur effectue un zoom avant car les faces avant seront projetées derrière la caméra, ce qui signifie qu'elles seront écrêtées, c'est-à-dire que ces pixels ne seront pas rendus. Pour permettre aux utilisateurs de rapprocher complètement la caméra du volume, nous ne rendrons que les faces inverses du parallélogramme. Le pipeline de rendu résultant est illustré à la figure 6.

Figure 6: pipeline WebGL pour le volume de raymarching. Nous allons pixelliser les faces inverses du parallélogramme du volume englobant afin que le fragment shader soit exécuté pour les pixels touchant ce volume. À l'intérieur du shader de fragment, nous rendons les rayons à travers le volume étape par étape pour le rendu.Dans ce pipeline, la majeure partie du rendu réel est effectuée dans le shader de fragments; cependant, nous pouvons toujours utiliser le vertex shader et l'équipement pour l'interpolation fixe des fonctions pour effectuer des calculs utiles. Le vertex shader convertira le volume en fonction de la position de la caméra de l'utilisateur, calculera la direction du faisceau et la position des yeux dans l'espace de volume, puis les transférera vers le fragment shader. La direction du faisceau calculée à chaque sommet est ensuite interpolée en triangle par un équipement d'interpolation à fonction fixe dans le GPU, ce qui nous permet de calculer les directions des rayons pour chaque fragment un peu moins cher, cependant, lors du transfert vers le fragment shader, ces directions peuvent ne pas être normalisées, donc doivent encore les normaliser.
Nous allons rendre le parallélogramme englobant comme un seul cube [0, 1] et le mettre à l'échelle par les valeurs des axes de volume pour fournir un support pour les volumes de volume inégal. La position de l'œil est convertie en un seul cube et, dans cet espace, la direction du faisceau est calculée. Le raymarching dans un espace cubique unique nous permettra de simplifier les opérations d'échantillonnage de texture lors du raymarching dans un shader de fragments. car ils seront déjà dans l'espace des coordonnées de texture [0, 1] du volume tridimensionnel.
Le vertex shader utilisé par nous est illustré ci-dessus, les faces arrière tramées peintes dans la direction du faisceau de visibilité sont illustrées à la figure 7.
#version 300 es layout(location=0) in vec3 pos; uniform mat4 proj_view; uniform vec3 eye_pos; uniform vec3 volume_scale; out vec3 vray_dir; flat out vec3 transformed_eye; void main(void) {
Figure 7: faces inverses du parallélogramme de volume limite peintes dans la direction du faisceau.Maintenant que le fragment shader traite les pixels pour lesquels nous devons rendre le volume, nous pouvons raymarching le volume et calculer la couleur pour chaque pixel. En plus de la direction du faisceau et de la position de l'œil, calculées dans le vertex shader, pour rendre le volume, nous devons transférer d'autres données d'entrée au fragment shader. Bien sûr, pour commencer, nous avons besoin d'un échantillonneur de texture 3D pour échantillonner le volume. Cependant, le volume n'est qu'un bloc de valeurs scalaires, et si nous les utilisions directement comme valeurs de couleur (
) et l'opacité (
), alors une image rendue en niveaux de gris ne serait pas très utile pour l'utilisateur. Par exemple, il serait impossible de mettre en évidence des zones intéressantes avec des couleurs différentes, d'ajouter du bruit et de rendre les zones d'arrière-plan transparentes pour les masquer.
Pour donner à l'utilisateur le contrôle sur la couleur et l'opacité affectées à chaque valeur d'échantillon, une carte de couleurs supplémentaire appelée
fonction de transfert est utilisée dans les rendus de visualisations scientifiques. La fonction de transfert définit la couleur et l'opacité qui doivent être attribuées à une valeur spécifique échantillonnée à partir du volume. Bien qu'il existe des fonctions de transfert plus complexes, ces fonctions utilisent généralement des tables de recherche de couleurs simples, qui peuvent être représentées comme une texture unidimensionnelle de couleur et d'opacité (au format RGBA). Pour appliquer la fonction de transfert lors de l'exécution du raymarching de volume, nous pouvons échantillonner la texture de la fonction de transfert en fonction d'une valeur scalaire échantillonnée à partir de la texture de volume. Les valeurs de couleur de retour et l'opacité sont ensuite utilisées comme
et
échantillon.
Les dernières données d'entrée pour le shader de fragment sont les dimensions de volume que nous utilisons pour calculer la taille du pas de faisceau (
) pour échantillonner chaque voxel le long du faisceau au moins une fois. Étant donné que l'
équation de faisceau traditionnelle a la forme
, pour des raisons de conformité, nous modifierons la terminologie du code et indiquerons
comment
. De même, l'intervalle
le long du faisceau, recouvert de volume, nous désignons comme
.
Pour effectuer un raymarching volumique dans un fragment shader, nous procéderons comme suit:
- Nous normalisons la direction du faisceau de visibilité reçu en entrée du vertex shader;
- Traversez la ligne de visée avec les limites du volume pour déterminer l'intervalle effectuer un raymarching dans le but de rendre le volume;
- Nous calculons une telle longueur de pas de sorte que chaque voxel soit échantillonné au moins une fois;
- À partir du point d'entrée vers , parcourons le faisceau à travers le volume jusqu'à ce que nous atteignions le point final
- À chaque point, nous échantillonnons le volume et utilisons la fonction de transfert pour attribuer la couleur et l'opacité;
- Nous accumulerons la couleur et l'opacité le long du faisceau en utilisant l'équation de composition d'avant en arrière.
Comme optimisation supplémentaire, vous pouvez ajouter la condition pour quitter prématurément le cycle de raymarching pour le terminer lorsque la couleur accumulée devient presque opaque. Lorsque la couleur devient presque opaque, les échantillons suivants n'auront presque aucun effet sur le pixel, car leur couleur sera complètement absorbée par l'environnement et n'atteindra pas l'œil.
Le shader de fragment complet pour notre rendu de volume est illustré ci-dessous. Des commentaires y ont été ajoutés, marquant chaque étape du processus.
Figure 8: La visualisation du bonsaï prêt à l'emploi résulte du même point de vue que sur la figure 7.C'est tout!
Le moteur de rendu décrit dans cet article pourra créer des images similaires à celles illustrées dans les figures 8 et 1. Vous pouvez également le tester en
ligne . Par souci de concision, j'ai omis le code Javascript nécessaire à la préparation du contexte WebGL, au chargement des textures de volume et des fonctions de transfert, à la configuration des shaders et au rendu d'un cube pour le rendu du volume; Le code de rendu complet est disponible pour référence sur
Github .