Affichage des articles dont le libellé est glsl. Afficher tous les articles
Affichage des articles dont le libellé est glsl. Afficher tous les articles

vendredi 29 janvier 2021

Afficher un quad plein écran avec OpenGL

Je suis en train de m'intéresser aux "skyboxes", c'est à dire l'affichage d'un environement tout autour de la caméra via une texture. En gros, l'idée est de créer un cube texturé qui suive la position de la caméra (voyez les explications techniques ici) pour afficher le "fond" du paysage, comme un ciel, une galaxie, un horizon urbain, qui remplisse le monde à peu de frais avant d'afficher les vrais objets à proximité. Manifestement, juste afficher un gros cube est maintenant dépassé, et bien plus recommandé est l'idée d'afficher juste un quad formé de deux triangles qui couvrent complètement l´écran, et de projeter la texture correctement. Je passe sur les détails de la projection avec lesquels je suis encore en train de me battre, pour vous parler plutôt de l'affichage du quad.

Comment afficher ce polygone ? La première idée qui vient, évidemment, c'est de créer les vertex buffers, les element buffers, de faire tous les appels à "bind" qui vont bien, et tout le toutim. Mais il semblerait qu'il y ait des solutions bien plus intelligentes...

En effet, pourquoi ne pas mettre toute la géométrie directement dans le vertex shader, puisqu'elle est fixe ? On sait que l'on veut des triangles étirés entres les coordonnées (-1, -1 ) et (1, 1), et l'on sait également quelles seront les coordonnées de textures voulues.

Le truc, c'est qu'un vertex shader a accès à l'index du vertex qui est en train d'être calculé. Donc, en incluant dans le shader un tableau qui indique pour chaque index quelles sont ses coordonnés de position et de texture, il n'y a plus qu'à retrouver les données à la volée, et c'est bon. Regardez moi ça, je vous le fais en simplifié :

Dans le code C++, on a un simple appel à draw, à poil, sans rien autour (à part la mise en oeuvre du shader)

glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

Et dans le vertex shader, on utilise l'index gl_VertexID pour accéder aux coordonnées voulues dans les tableaux positions et coords, et on fixe notre z à 0.99 pour l'avoir au fond :

out vec2 textureCoords;

void main() {
    const vec2 positions[4] = vec2[](
        vec2(-1, -1),
        vec2(+1, -1),
        vec2(-1, +1),
        vec2(+1, +1)
    );
    const vec2 coords[4] = vec2[](
        vec2(0, 0),
        vec2(1, 0),
        vec2(0, 1),
        vec2(1, 1)
    );

    textureCoords = coords[gl_VertexID];
    gl_Position = vec4(positions[gl_VertexID], 0.99, 1.0);
}

Voilà, c'est incroyablement simple. Avec un seul appel OpenGL (1 ! UN !) on arrive à afficher le Quad qui va bien. Magique, non ?

(Plus d'infos, et plus d'approches supplémentaires, VBO, geometry shaders et tout le toutim, sur la sur la question StackOverflow qui va bien)

lundi 23 septembre 2019

Et on rebondit !

Voilà les principaux éléments mis en place: l'on a la balle, et la surface sur laquelle elle rebondit. Je lui ai donné un moment (c'est à dire une vitesse de rotation, pas une petite pause, hein...), histoire de rendre son mouvement plus intéressant.

J'ai modélisé la balle et la surface marbrée dans Blender, puis exporté au format GLTF. Ensuite, j'ai intégré les modèles au sein d'une architecture entité-composant-système où chaque entité (la balle et la surface) a un composant physique, utilisant le moteur physique Bullet, et un composant graphique, qui référence le modèle GLTF et l'affiche via mes primitives de rendu OpenGL.

L'on notera les bugs graphiques sur la balle. Je ne sais pas encore s'ils sont dus à mon modèle, à l'export, ou à mon lecteur de GLTF, il faudra que je creuse la question.

mardi 16 avril 2019

GLTF et worflow métallique

Je continue à explorer le format GLTF, et j'obtiens enfin quelque chose de potable avec des modèles exportés depuis Blender. La procédure est la suivante:

  • Créer un modèle dans Blender avec un matériau se basant sur le "Principled shader" dans lequel on aura fourni la couleur, la rugosité, l'aspect métallique ou non, et le déplacement des normales
  • Ajouter au matériau, mais sans les connecter à quoi que ce soit, des textures images pour la couleur, la rugosité, le métal, et les normales
  • Sélectionner chaque image à son tour, et générer la texture via l'option "bake". Attention, pour de meilleurs résultats, utiliser plutôt Cycles que Eevee. Attention également, il n'est pas possible de générer directement la texture métallique, il faut ruser en connectant temporairement l'arrivée métallique au nœud de rugosité et en générant celui-ci vers l'image métallique.
  • Appliquer à l'objet un nouveau matériau qui utilise un principled shader, et connecter chaque texture à son nœud. Astuces: utiliser le mode "non-color data" sur les textures autre que la couleur principale, et passer par un nœud "normal map" pour les normales
  • Exporter le modèle !

J'ai écrit un petit programme en C++ qui tente d'afficher le modèle en utilisant un shader PBR qui tente de se rapprocher de ce que l'on verrait dans Blender. Voici le résultat avec un tore "rouillé", où la partie principale du modèle est métallique et lisse, mais présente un aspect abîmée par endroits via un matériau non-métallique, non-lisse, et présentant un fort bruit dans les normales.

Voici également les textures utilisées: couleur, combinaison de métal / rugosité, et normales.

jeudi 28 mars 2019

Format GLTF

Je me lance dans l'écriture d'une petite bibliothèque pour charger un fichier au format gltf. Le format gltf a été inventé par Khronos Group pour permettre l'échange de scènes 3D, et pour l'instant je trouve que la spec se tient très bien, et (mais c'était l'idée) est particulièrement adaptée au chargement via une API OpenGL. Si l'on rajoute en plus que le plugin Blender pour exporter dans ce format est de qualité et supporte très bien les workflows PBR (rendu basé sur la physique) et les animations, alors ça donne vraiment envie.

mercredi 14 décembre 2016

C'était facile, finalement

Une fois trouvé le petit bug qui faisait foirer le lissage des couleurs, je me suis rendu compte que c'était finalement très simple à implémenter (et nettement moins simple à comprendre). Voici le résultat:

Laisse moi zoom zoom zang

Dans ta fractale, fractale, fractale!

Après le contrôle de la palette, j'ai rajouté la navigation via la souris pour le déplacement, et la molette pour le zoom. L'on peut enfin explorer la fractale à volonté. C'est toujours d'une rapidité surprenante à calculer, au point que tout semble trop facile: je me sens presque floué de ne pas avoir à attendre plusieurs minutes pour voir une belle image. Mais pouvoir juste regarder un peu à côté de l'image, ou approcher un détail, est tellement confortable.

Maintenant, j'ai une application qui se tient à peu près. Quelques améliorations seront cependant les bienvenues:

  • Le lissage des couleurs en passant par des logarithmes
  • Une amélioration de la palette, peut-être avec du noir pour le centre
  • Une amélioration de la précision

Je développe un peu le dernier point. Mon visualiseur utilise des flottants simple précision, qui sont ce que le GPU préfère. C'est super pour les graphismes en général, mais sur du calcul de fractales, cette précision créé très rapidement une limite dans le zoom, au delà duquel l'image se retrouve composée de blocs disgracieux. La solution la plus simple pour franchir cette limite consiste à passer en double précision, ce qui est trivial pour un CPU, mais nettement plus périlleux pour un GPU, où l'augmentation de précision est une fonctionnalité très récente qui est encore mal supportée par le logiciel et le matériel.

L'autre solution est d'utiliser des nombres décimaux à précision illimitée. Avec un CPU, le temps de calcul devient rapidement exécrable, tandis que l'utilisation mémoire explose. Avec un GPU, je ne sais même pas si c'est possible...

vendredi 9 décembre 2016

Partageons

C'est toujours mieux quand on partage, donc j'ai mis une version très basique de mon explorateur de l'ensemble de Mandelbrot, version ordinateur de bureau, sur Github. C'est par ici que ça se passe.

Notez que j'ai trouvé une fonction pour transformer du HSV en RGB, histoire de fournir une coloration continue. Wikipedia l'explique mieux que moi, mais l'idée est que la composante H (la teinte) correspond à une position sur un cercle, l'ensemble HSV étant un cylindre, par opposition au RGB qui est un cube, et que la faire varier permet donc de changer de teinte aussi rapidement que souhaité sans jamais avoir de "saut" peu esthétique dans les couleurs.

lundi 28 novembre 2016

Une fractale sur mon téléphone

L'obstination a fini par payer, et j'arrive maintenant à afficher un bout de l'ensemble de Mandelbrot sur mon Ubuntu Touch ! Sans plus attendre, la démonstration en photo :

Les écueils furent nombreux, mais j'ai fini par à peu près comprendre comment approcher le problème. En particulier, le port vers OpenGL ES fut un poil tordu, mais j'ai maintenant une base qui fonctionne.

En termes de performances, mon téléphone calcule une image en 500ms. À comparer avec ma machine de bureau qui fait tourner (presque) le même programme en 1ms, ce qui est plutôt raisonnable si l'on considère que la carte graphique à elle seule coûte 2 fois le prix du téléphone.

Il va donc falloir revoir les performances d'une part (réduction de la précision, rafraîchir l'image le moins possible), et les fonctionnalités d'autre part: afficher l'ensemble de Mandelbrot, c'est bien, mais s'assurer que l'on a pas de déformations, que l'on peut zoomer, et qu'il y a un joli dégradé de couleurs, c'est mieux.

Je compte donc d'abord tenter d'unifier autant que possible mes programmes OpenGL et OpenGL ES, puis de développer sur le bureau, ce qui est quand même rudement plus confortable, avant de revenir sur le téléphone, et peut-être un jour publier l'application...

mercredi 31 août 2011

Normal maps et Blender 2.5 - Créer un astéroïde

Le nouveau Blender ayant changé pas mal de choses, voici la version mise à jour de mon tutoriel sur les normal map, cette foi-ci pour créer un astéroïde.



Voici l'écran initial de Blender, après être passé à 4 vues (Ctrl-Alt-Q). Virons le cube (sélection avec bouton droit, puis DEL), et ajoutons une icosphère (Shift-A, Mesh => Icosphere).



L'on peut maintenant tortiller la sphère dans tous les sens: s + déplacements de souris pour changer sa taille, et s suivi de x, y ou z pour changer la taille dans une dimension seulement. L'on peut sélectionner les points individuellement en passant en mode vertex (TAB), et en sélectionnant les points avec le bouton droit. La touche g permet de déplacer les vertex, et l'on peut une fois de plus restreindre la dimension avec les touches x, y ou z. Formons donc un patatoïde.



Lequel patatoïde nous pouvons maintenant donner une forme plus douce, en y ajoutant le modifier "subdivisions". Dans le menu de droite, allez chercher le tab "Modifiers" (la petite clé à molette), et choisissez "Subdivision Surface". Mettons View et Render à 2.



L'on va ensuite rendre notre patatoïde tout doux: passez en mode vertex (TAB), selectionnez tout (a), et choisissez dans le menu "Mesh Tools" à gauche le shading "Smooth".



Allez chercher le tab "Material", et ajoutez un nouveau matériau.



Allez ensuite chercher le tab "Texture", et ajoutez une nouvelle texture. Pour donner un aspect rugueux à notre astéroïde, nous allons choisir une texture de type "Cloud", choisir un "noise" à "Hard", et changer son influence en désactivant "Colo" (pour couleur) et en activant "Nor" pour normales.



Ajustez vos couleurs, et admirez!



Maintenant, passons à la création de l'objet à exporter. D'abord, dupliquez l'astéroïde (Shift-D). Il est possible de renommer les objets en "high poly" et "low poly", par exemple, dans le menu "Object", histoire de ne pas se perdre. Enlevez le modifier de subdivisions afin de retrouver notre objet initial.



Ensuite, générez la texture UV (mode vertex, u, "Smart UV project"). Vous pouvez maintenant effectuer le rendu de la texture, en allant dans le tab "Render" (le petit appareil photo), et en allant tout en bas dans "Bake". Choisissez le bake mode "Normals", avec normal space "Object". Lancez le rendu avec "Bake".



Tout en haut, passez de la vue "Default" à la vue "UV editing" pour admirer votre œuvre. Sauvez l'image, puis exportez l'objet!



Avec OSG, je transforme l'objet exporté en Wavefront (.obj) vers un .osg, puis j'édite manuellement le .osg afin de dupliquer le noeud de texture, pour ajouter une deuxième texture avec un identifiant à 1. Je change ensuite les noms pour avoir ma texture de couleurs en 0, et ma texture de normales en 1, comme l'attend mon programme.

Et voilà le travail:


mercredi 15 septembre 2010

Tutorial - Gérer le passage high-poly à low-poly avec Blender pour OSG

Ce petit tutorial décrit la marche à suivre pour créer un objet high-poly dans Blender, puis d'en sauver les normales dans une texture pour un objet low-poly.

Commençons par créer un objet high-poly. J'ai démarré du cube de base, subdivisé, puis ajouté quelques reliefs intéressants.





Ensuite, créons le même cube, mais sans subdivisions, à l'endroit exact du premier cube. Pour éviter la confusion, appelons le cube détaillé "HighCube" et le cube simple "LowCube".

Créons ensuite la texture du LowCube. Sélectionnez en mode edit, tapez "u", et faites un unwrap (j'aime bien la smart projection, sans l'option "stretch to bound"). Créez l'image, et sauvez. Blender vous montre à peu près ça:



Remarquez le rendu moche du cube, dû au Z fighting des deux cubes superposés.

Maintenant, sélectionnez les deux cubes, et allez faire un coup de "bake" pour les normales, en object space, avec l'option "selected to active".



Le rendu de la texture donne ceci, qui correspond aux normales aux faces:



Ajoutez maintenant un material par défaut à LowCube, et exportez le. En le passant dans Shadedit avec notre shader standard (à peine modifié pour ne pas s'embêter avec la texture de couleur), l'on a bien le cube simple, mais avec les normales déplacées comme pour le cube détaillé!

lundi 6 septembre 2010

Oh le beau bruit!



Une texture de bruit utilisée comme normal map. J'aime beaucoup les reflets obtenus, et c'est encore mieux quand ça bouge. Je posterais bien une vidéo si seulement kdenlive acceptait de les exporter sans planter lamentablement...

samedi 4 septembre 2010

Un shader plus complet


varying vec3 v;
varying vec2 t0;

void main()
{
gl_Position = ftransform();
v = vec3(gl_ModelViewMatrix * gl_Vertex);
t0 = gl_MultiTexCoord0.xy;
}



varying vec3 v;
varying vec2 t0;

uniform sampler2D textureMap;
uniform sampler2D normalMap;

void main()
{
vec3 lightVec = normalize(gl_LightSource[0].position.xyz - v);

vec3 normal =
gl_NormalMatrix *
(vec3(-1, 1, -1) *
normalize(texture2D(normalMap, t0).rgb * 2 - 1));

vec4 base = texture2D(textureMap, t0);

vec4 ambiantTerm =
gl_LightSource[0].ambient *
gl_FrontMaterial.ambient;

float diffuse = max(dot(normal, lightVec), 0.0);
vec4 diffuseTerm =
diffuse *
gl_LightSource[0].diffuse *
gl_FrontMaterial.diffuse;

float specular = pow(max(dot(reflect(lightVec, normal * 0.8), v), 0.0),
gl_FrontMaterial.shininess);
vec4 specularTerm =
specular *
gl_FrontMaterial.specular *
gl_LightSource[0].specular;

gl_FragColor =
ambiantTerm * base +
diffuseTerm * base +
specularTerm;
}


Un shader pour les normal maps de Blender

Après une après-midi à me battre avec mon shader, j'ai enfin réussi à maîtriser la bête. Il reste encore beaucoup pour obtenir le shader ultime, mais au vu de la complexité du processus, je vais commencer par un petit tutorial simple sur la manière d'obtenir des normal maps dans OpenSceneGraph.

La plupart des shaders que l'on trouve sur le net sont des shaders en tangent space, est à dire qu'ils correspondent aux normales, ou au déplacement de normales, tangentiellement à la surface au point appliqué. L'intérêt de ces shaders est multiple, entre autres, ils fonctionnent sur des objets déformables, et ils peuvent être appliqués localement pour perturber les normales, permettant ainsi d'avoir par exemple une petite texture de sable appliquée à l'ensemble de la plage.

Je n'ai rien contre le tangent space, mais malheureusement, OSG et Blender si. Blender ne sait pas exporter les tangentes, et OSG ne sait pas les importer. Je pourrais en théorie les calculer pour chaque objet, nécessitant soit de créer mon propre format au sein d'OSG, ou d'aller méchamment tripatouiller les modèles. Mon plan est donc d'utiliser une alternative en attendant qu'une bonne âme fournisse un worflow Blender OSG complet.

En attendant, je m'intéresse aux normales en object space, qui sont très proches des normales aux faces. La différence, c'est qu'au lieu de définir une normale par face ou par vertex, on définit la normale dans la texture.

Démarrons donc Blender, et tentons de créer un objet qui ait une surface intéressante.



Supprimons le cube de départ, et ajoutons une bête UV sphère, que l'on étirera dans la direction z. Un petit coup de "Set smooth" pour faire joli. Et c'est maintenant que tout démarre. Ajoutons un material et sa texture. Je choisis par exemple du Voronoï:



De retour dans les matériaux, sélectionnons notre texture comme étant appliquée aux normales. Renforçons également la valeur "Nor" à 5, afin d'avoir un relief plus profond.



Il est maintenant temps de créer la texture. Sélectionnons l'objet, et déroulons sa texture: (Tab) pour passer en mode édition, (u) pour le dérouler, choisir "Unwrap - Smart projection", déselectionner "Stretch to boun" pour éviter les distorsions.



En changeant la fenêtre à "UV/Image editor", l'on peut voir notre modèle tout aplati. L'on peut maintenant créer l'image qui va contenir la texture, en choisissant dans ce menu (Image) -> (New). Allons y franchement pour ce modèle, avec une texture en 1024x1024.



Effectuons le rendu: dans le menu de rendu, choisir (Bake), sélectionner (Normals) pour rendre uniquement les normales, et changez le normal space vers (Object). Clic sur Bake, et voilà, la texture est complétée! Sauvez là, et admirez:



L'on remarque que les couleurs dominantes changent, car elles correspondent aux directions des normales depuis l'objet. Par comparaison, la texture en espace tangent ressemble à ça:



Ensuite, l'on peut exporter l'objet, par exemple au format Open Inventor (.iv).

J'ai créé une petite application qui me permet d'ajuster l'objet et les shaders en temps réel. Je charge l'objet, ajuste mes shaders, et voilà, un bel œuf de dinosaure!



Voici le code pour le vertex shader:


varying vec3 v;
varying vec3 n;
varying vec2 t0;

void main()
{
gl_Position = ftransform();
v = vec3(gl_ModelViewMatrix * gl_Vertex);
n = normalize(gl_Normal);
t0 = gl_MultiTexCoord0.xy;
}

et le fragment shader:

varying vec3 v;
varying vec3 n;
varying vec2 t0;

uniform sampler2D normalMap;

void main()
{
vec3 lightVec = normalize(gl_LightSource[0].position.xyz - v);
vec3 normal =
gl_NormalMatrix *
(vec3(-1, 1, -1) * normalize(texture2D(normalMap, t0).rgb * 2 - 1));
float diffuseTerm = max(dot(normal, lightVec), 0.0);
gl_FragColor = vec4(1, 1, 1, 1) * diffuseTerm;
}


Je vais maintenant affiner le shader:

  • Passage au multitexturing, pour avoir à la fois les normales et les couleurs

  • Gestion de la lumière spéculaire, pour permettre par exemple un rendu "mouillé"

  • Amélioration du pipeline, et résolution des quelques points bizarres, telle la multiplication par (-1, 1, -1)

dimanche 16 mai 2010

Textures combinées

J'ai souvent eu des soucis à rendre mes textures de terrain jolies. Mon plus gros problème, généralement, est l'effet de répétition. Surtout avec les textures moches du genre de celles que je créé moi-même. Prenons par exemple cette simple texture d'herbe.



On applique la texture à ce terrain, et hop, l'effet de répétition prend toute sa mesure. C'est moche!



En même temps, si l'on réduit la répétition de la texture, c'est moche aussi, car la texture semble grossière et peu détaillée.



Mais si l'on calcule la moyenne des deux, avec un facteur qui évite la répétition, comme par exemple 16 et 3.44 (!), tout d'un coup, ça rend beaucoup mieux! Pas d'artefacts de répétition, mais la texture maintient l'impression de détail.



En plus, ce genre de calcul est trivial à effectuer avec le petit shader:


gl_FragColor =
diffuse
* (texture2D(grassTex, t * 16)
+ texture2D(grassTex, t * 3.44)) / 2;


L'on multiplie la couleur diffuse calculée à partir de l'éclairement par la moyenne des textures, en multipliant les coordonnées de la texture par nos facteurs.

samedi 14 novembre 2009

Enfin, le bump mapping!

C'est avec l'aide de cette page, de nombreuses simplifications, et beaucoup de bidouillages, que j'ai enfin réussi à faire du bump mapping qui ressemble à quelque chose.

L'ensemble de la scène est assez complexe.


  • Tout d'abord, il y a l'élévation, générée à partir d'une texture, au niveau du vertex shader

  • Ensuite, il y a l'ombrage général de la scène, généré également avec une texture, mais au niveau du fragment

  • Ensuite, il y a le calcul de la tangente, au niveau du vertex shader, qui est ensuite passée au fragment pour générer le bump mapping, à la fois en diffus, et en reflets, pour donner les effets "mouillés"



L'ensemble tourne à un très correct 1700 images par secondes en 1280x1024 plein écran.






Dans la dernière image, la couleur diffuse est en rose, afin que les reflets soient encore plus visibles.

lundi 13 avril 2009

Océan - Une vidéo

Finalement, je suis tombé sur "gtk-recordmydesktop", lequel a été capable de me sortir une vidéo potable. Voilà donc l'animation de l'océan, basée sur C++, SDL, OpenSceneGraph(OSG), et une épaisse couche de shaders GLSL:

dimanche 12 avril 2009

La couleur de l'eau

J'ai tout d'abord réessayé le bump mapping, ce qui a donné des résultats tout à fait ridicules. Je me suis ensuite basé sur une texture de flotte assez standard, pour des résultats qui m'ont immédiatement plus plu. Cependant, restait le problème de savoir comment mélanger le reflet et la texture d'eau.

- Addition -> Joli, mais les reflets apparaissent beaucoup plus clairs que l'objet original
- Multiplication -> L'eau est presque noire!
- Max -> A peu près potable si la couleur de l'eau est sombre, fait disparaître des éléments qui devraient être reflétés sinon
- Min -> Situation inverse de max, la couleur de l'eau doit être claire, sinon on perd des reflets

En fait, la couleur de l'eau "sans rien" est vraiment la couleur du ciel réfléchie. Ce que l'on veut vraiment, c'est la couleur du reflet si il existe, et l'océan (de fait, le ciel!) sinon. En modifiant ma caméra de reflet pour qu'elle retourne une couleur de fond transparente, j'ai ensuite combiné mon reflet à mon océan en utilisant la valeur alpha dans mon shader:

gl_FragColor = ocean * (1. - refl.a) + refl * refl.a;

Ainsi, mon océan n'est visible que si mon reflet est transparent (c'est à dire correspond à la couleur de fond), et disparaît lorsqu'un reflet existe.

J'ai même la sourde impression que ce code devrait fonctionner même le jour où je voudrais réfléchir des objets semi-transparents, puisqu'effectivement c'est le ciel qui apparaîtrait au travers.

Et voilà le résultat!

Ocean

Probablement les premières captures qui ressemblent vraiment à quelque chose! J'ai beaucoup diminué l'intensité du mouvement, ce qui semble mieux marcher.