Développement et info sur l'avancée des projets par les développeurs

Alpha-Arts » Blog



Un shader de blur est un post-traitement de l'image afin de lui donner un effet de flou.

Je vais vous expliquer deux techniques dans ce tips, une première permettant de faire des "petits flous" rapidement et une deuxième permettant des rendus de bien meilleure qualité, mais beaucoup plus lente.

Connaissances requises :

- Faire[...]

http://www.alpha-arts.net/programmes/galerie/Tips/TipsCode.png


Un shader de blur est un post-traitement de l'image afin de lui donner un effet de flou.

Je vais vous expliquer deux techniques dans ce tips, une première permettant de faire des "petits flous" rapidement et une deuxième permettant des rendus de bien meilleure qualité, mais beaucoup plus lente.

Connaissances requises :
  • Faire une fenêtre, afficher une image, utiliser les RenderImage et les shaders avec la SFML.
  • Connaissances de base en GLSL.


Première méthode

La première méthode consiste à prendre chaque pixel, regarder les quelques pixels aux alentours, et mélanger la couleur de ceux-ci.
On regarde un pixel en haut, un en bas, un à gauche, un à droite et un dans chaque coin. On mixe le tout en augmentant l'importance du pixel du milieu et en diminuant celle des coin.

Cette technique est rapide car elle ne nécessite qu'une seule passe. Il suffit d'appliquer une fois le shader sur l'image et c'est dans la poche.

uniform sampler2D texture;
uniform float offset;

void main()
{
	vec2 offx = vec2(offset, 0.0);
	vec2 offy = vec2(0.0, offset);

	gl_FragColor =  gl_Color * (
		 texture2D(texture, gl_TexCoord[0].xy)               * 0.2  +
                 texture2D(texture, gl_TexCoord[0].xy - offx)        * 0.13 +
                 texture2D(texture, gl_TexCoord[0].xy + offx)        * 0.13 +
                 texture2D(texture, gl_TexCoord[0].xy - offy)        * 0.13 +
                 texture2D(texture, gl_TexCoord[0].xy + offy)        * 0.13 +
                 texture2D(texture, gl_TexCoord[0].xy - offx - offy) * 0.07  +
                 texture2D(texture, gl_TexCoord[0].xy - offx + offy) * 0.07  +
                 texture2D(texture, gl_TexCoord[0].xy + offx - offy) * 0.07  +
                 texture2D(texture, gl_TexCoord[0].xy + offx + offy) * 0.07);
}

Shader repris du shader de blur dans les exemples de la SFML.

Résultat :

http://www.holyspirit.fr/Autres/shader_blur_2.png
http://www.holyspirit.fr/Autres/shader_blur_1.png

Ce shader rend donc très bien pour des blurs légers, mais donc qu'on doit "flouter" plus intensivement, le rendu devient très médiocre.

Deuxième méthode

La deuxième méthode consiste à appliquer deux fois le shader. La première fois en regardant les pixels à gauche et à droite, sur plusieurs pixels et de donner un effet de dégradé en diminuant l'importance suivant la distance.
Une fois ce premier rendu effectué, on applique le même shader, mais cette fois en regardant les pixels en haut et en bas.

Ce qui nous donne en images :

Image de base :
http://www.holyspirit.fr/Autres/shader_blur_5.png

Première passe horizontale :
http://www.holyspirit.fr/Autres/shader_blur_6.png

Deuxième passe verticale :
http://www.holyspirit.fr/Autres/shader_blur_7.png

Cette technique consomme donc beaucoup plus que l'autre. En effet, nous devons appliquer un premier shader, copier le rendu, lui appliquer à nouveau un shader et enfin l'afficher à l'écran.

uniform sampler2D texture;
uniform float offset;
uniform float direction_x;

void main()
{
	vec2 off;
	
	if(direction_x == 1.0)
		off = vec2(offset, 0.0);
	else
		off = vec2(0.0, offset);

	gl_FragColor =  gl_Color * (
			texture2D(texture, gl_TexCoord[0].xy + off * 1.0)	* 0.06 + 
			texture2D(texture, gl_TexCoord[0].xy + off * 0.75)	* 0.09 +
			texture2D(texture, gl_TexCoord[0].xy + off * 0.5)	* 0.12 +
			texture2D(texture, gl_TexCoord[0].xy + off * 0.25)	* 0.15 +
				
			texture2D(texture, gl_TexCoord[0].xy)	* 0.16 +
				
			texture2D(texture, gl_TexCoord[0].xy - off * 1.0) 	* 0.06 +
			texture2D(texture, gl_TexCoord[0].xy - off * 0.75)	* 0.09 +
			texture2D(texture, gl_TexCoord[0].xy - off * 0.5)	* 0.12 +
			texture2D(texture, gl_TexCoord[0].xy - off * 0.25)	* 0.15 );
}


sf::RenderWindow App;

sf::Sprite      background;
sf::Shader      blur;
sf::RenderImage img_render,
sf::Sprite      render;

...

App.Clear();

blur.SetParameter("direction_x", 1.0);
img_render.Draw(background, blur);
img_render.Display();

render.SetImage(img_render.GetImage());

blur.SetParameter("direction_x", 0.0);
App.Draw(render, blur);

App.Display();


Résultat final :

http://www.holyspirit.fr/Autres/shader_blur_3.png
http://www.holyspirit.fr/Autres/shader_blur_4.png

C'est quand même beaucoup mieux que l'autre, non ?

Conclusion

Il vaut mieux privilégier l'utilisation du premier shader quand on le peut, mais le deuxième rend quand même beaucoup mieux.

Vous pourrez trouver le code d'exemple avec les shaders ici.



Par Gregouar - 05/02/2011



Je vais vous expliquer dans ce tips comment j'ai réalisé mon système de cadre dynamique.

L'idée est de partir de là :



Et faire ce genre de choses :



Le principe est de prendre l'image de bordure, et la découper en 9 sous images : une pour chaque coin, une pour chaque bord et enfin une pour le centre.

http://www.alpha-arts.net/programmes/galerie/Tips/TipsCode.png


Je vais vous expliquer dans ce tips comment j'ai réalisé mon système de cadre dynamique.

L'idée est de partir de là :

http://holyspirit.alpha-arts.net/Autres/blog_border/Border.png

Et faire ce genre de choses :

http://holyspirit.alpha-arts.net/Autres/blog_interface/screenshot9.png

Le principe est de prendre l'image de bordure, et la découper en 9 sous images : une pour chaque coin, une pour chaque bord et enfin une pour le centre.

Regardez ce zoom :

http://holyspirit.alpha-arts.net/Autres/blog_border/border_1.png

Il nous suffit alors de faire une petite class qui va s'occuper de rendre tout ça.

struct Border
{
    Image_interface image_l;
    Image_interface image_r;
    Image_interface image_u;
    Image_interface image_d;

    Image_interface image_lu;
    Image_interface image_ru;
    Image_interface image_ld;
    Image_interface image_rd;

    Image_interface image_c;

    void Draw(coordonnee pos, coordonnee size, sf::Color color = sf::Color (255,255,255));
};

Image_interface est une autre petite structure qui contient une image ainsi que les coordonnées dans cette image.


Nous faisons donc une sous-image par partie du cadre.

Il nous suffit ensuite plus tard de faire myBorder.Draw(position, size, color) à chaque fois que nous voulons afficher un cadre autour de quelque chose.

void Border:raw(coordonnee pos, coordonnee size, sf::Color color)
{
    sf::Sprite sprite;
    sprite.SetColor(color);
    
    sprite.SetImage(*moteurGraphique->getImage(image_lu.image));
    sprite.SetSubRect(sf::IntRect(  image_lu.position.x, image_lu.position.y,
                                    image_lu.position.w, image_lu.position.h));
    sprite.SetPosition(pos.x - image_lu.position.w,
                       pos.y - image_lu.position.h);
    moteurGraphique->Draw(sprite);

    sprite.SetImage(*moteurGraphique->getImage(image_ru.image));
    sprite.SetSubRect(sf::IntRect(  image_ru.position.x, image_ru.position.y,
                                    image_ru.position.w, image_ru.position.h));
    sprite.SetPosition(pos.x + size.x ,
                       pos.y - image_ru.position.h);
    moteurGraphique->Draw(sprite);

    sprite.SetImage(*moteurGraphique->getImage(image_ld.image));
    sprite.SetSubRect(sf::IntRect(  image_ld.position.x, image_ld.position.y,
                                    image_ld.position.w, image_ld.position.h));
    sprite.SetPosition(pos.x - image_ld.position.w,
                       pos.y + size.y);
    moteurGraphique->Draw(sprite);

    sprite.SetImage(*moteurGraphique->getImage(image_rd.image));
    sprite.SetSubRect(sf::IntRect(  image_rd.position.x, image_rd.position.y,
                                    image_rd.position.w, image_rd.position.h));
    sprite.SetPosition(pos.x + size.x,
                       pos.y + size.y);
    moteurGraphique->Draw(sprite);



    sprite.SetImage(*moteurGraphique->getImage(image_u.image));
    sprite.SetSubRect(sf::IntRect(  image_u.position.x, image_u.position.y,
                                    image_u.position.w, image_u.position.h));
    sprite.SetPosition(pos.x,pos.y - image_u.position.h);
    sprite.Resize(size.x,image_u.position.h);
    moteurGraphique->Draw(sprite);

    sprite.SetImage(*moteurGraphique->getImage(image_d.image));
    sprite.SetSubRect(sf::IntRect(  image_d.position.x, image_d.position.y,
                                    image_d.position.w, image_d.position.h));
    sprite.SetPosition(pos.x,pos.y + size.y);
    sprite.Resize(size.x,image_u.position.h);
    moteurGraphique->Draw(sprite);

    sprite.SetImage(*moteurGraphique->getImage(image_l.image));
    sprite.SetSubRect(sf::IntRect(  image_l.position.x, image_l.position.y,
                                    image_l.position.w, image_l.position.h));
    sprite.SetPosition(pos.x - image_l.position.w,pos.y);
    sprite.Resize(image_l.position.w,size.y);
    moteurGraphique->Draw(sprite);

    sprite.SetImage(*moteurGraphique->getImage(image_r.image));
    sprite.SetSubRect(sf::IntRect(  image_r.position.x, image_r.position.y,
                                    image_r.position.w, image_r.position.h));
    sprite.SetPosition(pos.x + size.x,pos.y);
    sprite.Resize(image_r.position.w,size.y);
    moteurGraphique->Draw(sprite);



    sprite.SetImage(*moteurGraphique->getImage(image_c.image));
    sprite.SetSubRect(sf::IntRect(  image_c.position.x, image_c.position.y,
                                    image_c.position.w, image_c.position.h));
    sprite.SetPosition(pos.x,pos.y);
    sprite.Resize(size.x,size.y);
    moteurGraphique->Draw(sprite);
}



Concrètement, par exemple, si nous voulons afficher un cadre autour d'un texte :

sf::Text text;
text.SetString("Mon super texte !" );
text.SetPosition(pos.x, pos.y);

pos.x -= 6;
pos.y -= 2;

coordonnee s;
s.x = (int)temp.GetRect().Width + 13;
s.y = (int)temp.GetRect().Height + 6;
border.Draw(pos, s);

App.Draw(text);

Par Gregouar - 20/02/2011

Holyspirit
4

Aujourd'hui, je vais vous apprendre comment je gère mes tilesets, du moins d'un point de vue ressources. Je pourrai vous expliquer ça d'un point de vue programmation dans un futur billet si ça vous intéresse.

Tout d'abord, une petite définition personnel : un tileset est un ensemble de tiles, qui se traduit par tuile en français. Holyspirit est composé de "tuiles",[...]

Aujourd'hui, je vais vous apprendre comment je gère mes tilesets, du moins d'un point de vue ressources. Je pourrai vous expliquer ça d'un point de vue programmation dans un futur billet si ça vous intéresse.

Tout d'abord, une petite définition personnel : un tileset est un ensemble de tiles, qui se traduit par tuile en français. Holyspirit est composé de "tuiles", correspondant en fait aux petites images qui composent la partie graphique du jeu.
Nous pouvons avoir toutes sortes de tiles : des tiles pour des décors (sols, murs, arbres, ...), pour des personnages, pour des effets de sorts (ou miracles dans Holyspirit), ...

Dans cet article, je vais principalement détailler le fonctionnement des tilesets de décors, puis je vous expliquerai rapidement comment ça marche pour les personnages et miracles.

Un tileset de décor est composé simplement d'un fichier *.ts.hs, ainsi que d'une ou plusieurs images de ce genre :

http://holyspirit.alpha-arts.net/Fichiers_jeu/Holyspirit/Data/Landscapes/Misc/smallCart.png

Exemple de fichier *.ts.hs, celui qui va avec l'image ci-dessus.

*Data/Landscapes/Misc/smallCart.png
$

$
*x0 y0 w192 h128 ex126 ey64 i0 c1 z1 vPetitChariotDetruit $
*x192 y0 w192 h128 ex126 ey64 i0 c1 z1 vPetitChariot $
$


Les plus malins d'entre vous l'auront tout de suite compris, le fichier est décomposé en 3 parties.
La première contenant la liste des chemins des images, la seconde, même si l'on ne le voit pas ici, la liste des chemins des sons et la dernière, la liste des tiles.

Je ne penses pas devoir donner des informations sur les deux premières parties, on a simplement "*Chemin". En programmation, je récupère le chemin et ajoute à mon objet tileset l'ID de l'image dans le moteur graphique, idem pour le son.

Pour la liste des tiles, vous pouvez retrouver le même genre de structure que dans les miracles, donc des blocs ouverts par '*' et fermés par '$', avec des lettres pour indiquer le type de l'information, chacun de ces blocs correspondant à un tile, à un décor.

'x' et 'y' pour la position en pixels dans l'image du tile et 'w' et 'h' pour la largeur et la hauteur du tile.

'ex' et 'ey' c'est la position du centre du tile. En gros, si le décor est placé à la position 32 en x et 48 en y, ce sera le pixel de l'image en 'ex' et 'ey' qui s'y trouvera. C'est donc l'origine du tile. C'est aussi utile pour placer le centre de rotation du tile, pour l'ombre.

'i' pour le numéro de l'image dans le tileset. Ici, nous avons qu'une seule image qui compose ce tileset, donc c'est l'image 0. (On commence à 0).

'c' pour dire qu'il y a collision sur l'élément. Le personnage ne peut donc pas le traverser. Pour ne pas mettre de collision, on aurait eut 'c0' ou rien du tout.

'z' ça n'a aucune utilité pour le jeu lui-même, mais c'est pour l'éditeur. Ça veut dire que si, dans l'éditeur, je sélectionne ce tile, il me le place en par défaut sur la couche n°1, donc celle à hauteur des personnages.
'v' n'a aussi aucune utilité pour le jeu, mais permet de mieux s'y retrouver dans l'éditeur. C'est le nom associé au tile.

C'est tout pour ce tileset-ci, mais il existe encore de nombreux autres types d'informations.

Passons à un autre exemple de tileset :

http://holyspirit.alpha-arts.net/Fichiers_jeu/Holyspirit/Data/Landscapes/Misc/Lantern.png

*Data/Landscapes/Misc/Lantern.png
$

$
*x0 y0 w192 h192 i0 o1 c1 z1 ex83 ey168 lr255 lv200 lb81 li128 $
$


Vous avez comme nouvel élément : lr255 lv200 lb81 li128

Ça, c'est simplement pour indiquer que le tile émet une lumière. Les composantes RGB sont données par lr, lv et lb, ainsi que l'intensité, qui définit le rayon de la lumière, par li.

http://holyspirit.alpha-arts.net/Autres/screenshot0_blog_tilesets.png

Tile tiré d'un tileset d'arbres :

*Data/Landscapes/Trees/Trees.png
*Data/Menus/MinimapIcones.png
$
$
*x0 y0 w384 h384 i0 c1 o1 f1 ey342 z1 vBouleau m i1 x48 y32 w16 h16 $ $
$


Vous avez le 'f' qui signifie que le tile est réfléchi dans l'eau.

Le 'o' permet de dire que le tile possède une ombre.

Voyez le m i1 x48 y32 w16 h16 $, c'est pour dire que le tile possède aussi une image sur la minimap.
Niveau fonctionnement, c'est comme avoir un tile dans un tile, si ce n'est que le bloc est délimité par 'm' et '$'.
NB : Le 'i1', on est donc sur la seconde image du tileset, soit :

http://holyspirit.alpha-arts.net/Fichiers_jeu/Holyspirit/Data/Menus/MinimapIcones.png

En jeu, nous obtenons ceci :

http://holyspirit.alpha-arts.net/Autres/screenshot1_blog_tilesets.png


Maintenant, un tileset de feu de camp :


*Data/Landscapes/Campfire/Campfire.png
*Data/Landscapes/Fire/Campfire_distortion.png
$
* mData/Sounds/Feu.wav $
$
Feu de camp
*x0 y0 w128 h128 i0 c1 a1 s0 lr236 lv136 lb36 li255 lh0 j x0 y0 ey160 w128 h128 i1 $ $
*x128 y0 w128 h128 i0 c1 a2 s0 lr236 lv136 lb36 li245 lh0 j x128 y0 ey160 w128 h128 i1 $ $
*x256 y0 w128 h128 i0 c1 a3 s0 lr236 lv136 lb36 li235 lh0 j x256 y0 ey160 w128 h128 i1 $ $
*x384 y0 w128 h128 i0 c1 a4 s0 lr236 lv136 lb36 li245 lh0 j x384 y0 ey160 w128 h128 i1 $ $
*x512 y0 w128 h128 i0 c1 a5 s0 lr236 lv136 lb36 li255 lh0 j x512 y0 ey160 w128 h128 i1 $ $
*x640 y0 w128 h128 i0 c1 a6 s0 lr236 lv136 lb36 li245 lh0 j x640 y0 ey160 w128 h128 i1 $ $
*x768 y0 w128 h128 i0 c1 a7 s0 lr236 lv136 lb36 li255 lh0 j x768 y0 ey160 w128 h128 i1 $ $
*x896 y0 w128 h128 i0 c1 a0 s0 lr236 lv136 lb36 li255 lh0 j x896 y0 ey160 w128 h128 i1 $ $
$


Notez le 'a', c'est simplement le n° de la prochaine image dans l'animation du tile. Le tile devient donc un autre tile parmi la liste quand le temps est écoulé. Ce temps est par défaut de 0.075 secondes. Mais il peut être modifié par 'n'.
Vous pouvez aussi remarquer le 's' pour le n° du son joué.
Enfin, vous avez le j x768 y0 ey160 w128 h128 i1 $
En fait, c'est une image qui est utilisée pour donner un effet de distorsion sur l'écran via un shader, afin de donner une impression de chaleur. Je ne vais pas trop approfondir ce sujet, ce n'est pas le but de ce billet.
Il est définit de la même manière que pour la minimap, mais avec un 'j' au lieu d'un 'm'.

Je ne vais plus donner d'exemple, mais vous pouvez encore avoir :
't' : le tile devient transparent si le héros passe derrière, utile pour des tiles qui prennent beaucoup de place à l'écran ou des murs dans des donjons.
'p' : permet de modifier l'opacité du tile. Surtout utilisé pour des effets de miracles qui disparaissent petit à petit.
'd' : cette information n'est utilisée que dans les personnages. Elle peut valoir -1, 0, 1 ou 2. En gros, elle permet de définir à quel moment dans l'animation le personnage inflige des dégâts, quand il a fini de frapper, etc.
'g' : cette information n'est utilisée que pour le héros. Je ne vais rien expliquer ici car ça fera l'objet d'un prochain billet qui expliquera comment je fais pour que, suivant les objets que le héros porte, ses graphismes changent.
'r' : il peut valoir x, y, g, d, b, h, ... En gros, tout ce que vous avez besoin de savoir c'est que ça définit l'orientation des murs pour bloquer la lumière. Si je fais un billet sur mon moteur de lumières, je vous expliquerai tout ça plus en détails.
'k' : dans le même genre que 'm' pour la minimap et 'j' pour les distortions, 'k' est l'image de la shadowmap du tile. En gros, c'est utilisé pour assombrir des morceaux du tile suivant l'orientation du soleil.
Pour ça, j'utilise une autre information, 'b' qui est l'angle. Je compare cet angle avec l'angle du soleil et en fonction de ça, je modifie l'opacité de la shadowmap que j'applique sur le décor. Je peux avoir plusieurs shadowmap pour un même tile.

Exemple :
http://holyspirit.alpha-arts.net/Fichiers_jeu/Holyspirit/Data/Landscapes/Stone_wall/Stone_wall.png
http://holyspirit.alpha-arts.net/Fichiers_jeu/Holyspirit/Data/Landscapes/Stone_wall/Stone_wall_shadow


Enfin, si vous vous souvenez, dans le billet sur les miracles, nous avions des séquences. Vous pourrez remarquer que ces séquences ne sont autre que des tilesets, possédant exactement la même structure.

Pour les personnages, c'est un petit peu différent. En effet, vous avez juste un fichier .rs.hs qui contient le .ts.hs et les images. C'est donc une forme de fichier *.dat.
Par ailleurs, le .ts.hs est un petit peu différent.
Vous avez toujours la liste des images, liste des sons et puis des tiles, mais avec quelques nuances.

Tout d'abord, la liste des sons est maintenant comme ceci :

Liste des chemins de tous les sons
* mData/Sounds/9906__Snoman__grass3.wav $
* mData/Sounds/14609__man__swosh.wav $
* mData/Sounds/punch.wav n0 $
$


En effet, vous pouvez avoir une information 'n', à 0, cela signifie que le son est joué quand le personnage est touché par une attaque.

Quant aux tiles, ils vont par blocs de 8 blocs. En gros, ça veut dire que vous avez une liste de tile par direction, donc 8 listes. Et ces 8 listes sont elles même séparées en différents blocs qui représentent les états du personnage, donc à l'arrêt, qui marche, qui frappe, qui meurt, qui lance un miracle, ...
Le code permet de mettre autant d'états qu'on le souhaite, si on veut faire plusieurs animations différentes suivant les miracles lancés.


C'est tout pour aujourd'hui. Si vous avez des questions ou remarques, n'hésitez pas.

Par Gregouar - 06/06/2010


L'Occlusion Ambiente ou Ambient Occlusion (AO) en anglais est une technique d'illumination globale quelque peu particulière que l'on utilise conjointement avec la GI (Global Illumination). Dans ce tips, nous verrons donc rapidement comment utiliser l'AO créée sous 3DS Max pour la compositer avec Photoshop. Si vous utilisez The Gimp, cela fonctionne de la même manière.

http://www.alpha-arts.net/programmes/galerie/Tips/Tips3DS.png

L'Occlusion Ambiente ou Ambient Occlusion (AO) en anglais est une technique d'illumination globale quelque peu particulière que l'on utilise conjointement avec la GI (Global Illumination). Dans ce tips, nous verrons donc rapidement comment utiliser l'AO créée sous 3DS Max pour la compositer avec Photoshop. Si vous utilisez The Gimp, cela fonctionne de la même manière.

Voici un bref rappelle de la série de tips consacré à l'AO.
  • Mise en place de l'AO
  • Post-prod passe d'AO
  • Render to texture pour L'AO
  • L'AO et Blender


http://nsm05.casimages.com/img/2010/12/27/1012271030291114117374939.png
Nous venons donc de faire une passe d'Ambiant Occlusion pour notre rendu final. Cette passe est donc en Noir et Blanc uniquement, on va donc ouvrir notre rendu final sous Photohop ainsi que la passe d'AO.

Commencez par coller l'AO comme nouveau calque dans le même fichier que votre rendu brut (Étape 1).
Pour compositer votre passe d'AO, il vous suffit tout simplement de placer le calque en haut de la pile et d'utiliser un mode de fusion (Étape 2) puis de choisir le mode Produit qui permet de faire disparaitre les pixels blancs de l'image.
Si votre AO est trop forte n'hésitez pas à utiliser les niveaux pour calmer les noirs ou, inversement, pour renforcer ces derniers. Vous pouvez aussi jouer avec l'opacité du calque si besoin.

Tadadam !! C'est fini ! Ce n'était pas sorcier n'est-ce pas ?
Vous allez me dire, quoi c'est tout ?
Presque ! Il n'y a pas vraiment beaucoup plus de choses à dire, cependant cette technique à quelques avantages et inconvénients.
    Avantages :
  • C'est simple.
  • C'est rapide.
  • On peut mixer plusieurs couches d'AO pour rajouter de l'ombre.
  • Simple et rapide à modifier si un objet change de forme.
  • Travail plus souple pour les modifications en post-production.

    Inconvénient :
  • Fichier image supplémentaire.
  • Travail supplémentaire pour la post-production.
  • ... même avec beaucoup de mauvaise foie dur de trouver d'autre point négatif à cette technique.

De plus il s'agit d'une technique qui fonctionne dans beaucoup d'utilisation, notamment pour des rendu studio (parfum par exemple) ou encore pour des voitures avec incrustation qui permet de contrôler facilement l'ombre porté.

Voila, il nous reste un dernier tips dédié à 3DS Max qui arrivera prochainement afin d'utiliser l'AO pour du temps réel, tout un programme !
Pour finir je vous laisse avec un rendu de la taverne avec le version Post-prod, la version brut et la brut + AO.
http://nsm05.casimages.com/img/2010/12/28/1012281133091114117381523.png

Par stilobique - 15/01/2011

Holyspirit
3

Bonjour à toutes et à tous !


Aujourd'hui, je vais vous parler du parser d'équations que j'ai codé pour ma gestion des miracles.




Introduction

Tout d'abord, pour ceux qui ne le savent pas, un parser est, en gros, un code qui permet de découper un texte afin de le séparer en éléments clés.

Notre objectif ici est de[...]

Bonjour à toutes et à tous !


Aujourd'hui, je vais vous parler du parser d'équations que j'ai codé pour ma gestion des miracles.




Introduction

Tout d'abord, pour ceux qui ne le savent pas, un parser est, en gros, un code qui permet de découper un texte afin de le séparer en éléments clés.

Notre objectif ici est de pouvoir lire une équation contenant des additions, soustractions, multiplications, divisions, exposants et parenthèses et de calculer la valeur finale.

Le prototype de notre fonction ressemble à ceci :

float ChargerEquation(ifstream &fichier, const Caracteristique &caract, int level, char priorite, bool *continuer);



La fonction retourne un float, c'est notre valeur finale calculée.
Elle prend en paramètre le fichier, placé à la bonne ligne au bon endroit ; les caractéristiques du héros, afin de pouvoir donner des paramètres aux équations (par exemple, donner les dégâts de base du héros) ; le niveau du miracle, donc le nombre de points dépensés dedans, afin de faire varier les effets du miracle en fonction ; priorité, j'y reviendrai plus tard ; continuer est une bool qui permet de vérifier si on est à la fin d'un bloc.





Mise en place des outils

Tout d'abord, la fonction ChargerEquation() va tout lire jusqu'à ce qu'on rencontre le traditionnel signe $ qui représente une fin d'information.

Exemple d'équation : i0 * ( 40 + l * 10 ) / 100 $

En programmation, nous avons donc :

float ChargerEquation(ifstream &fichier, const Caracteristique &caract, int level, char priorite, bool *continuer)
{
    float valeur = 0;
    char caractere;

    do
    {
        fichier.get(caractere);
    }
    while (caractere!='$');
    return valeur;
}



NB : float valeur représente la valeur finale calculée qui est retournée à la fin.

Maintenant, on va tout lire caractère par caractère et regarder si l'on tombe sur un signe d'opération, une parenthèse, un nombre ou une valeur spéciale (niveau du miracle, dégâts du héros, etc)

float ChargerEquation(ifstream &fichier, const Caracteristique &caract, int level, char priorite, bool *continuer)
{
    float valeur = 0;
    char caractere;

    do
    {
        fichier.get(caractere);

        if (caractere == '+')
        {
        }
        else if (caractere == '-')
        {
        }
        else if (caractere == '*')
        {
        }
        else if (caractere == '/')
        {
        }
        else if (caractere == '^')
        {
        }

        else if (caractere == 'l')
            valeur = level;

        else if (caractere == 'i')
        {
            int no = 0;
            fichier>>no;
            if(no >=0 && no < 4)
                valeur = caract.degatsMin[no];
        }
        else if (caractere == 'a')
        {
            int no = 0;
            fichier>>no;
            if(no >=0 && no < 4)
                valeur = caract.degatsMax[no];
        }
        else if (caractere == 'b')
            valeur = caract.armure[PHYSIQUE];

        else if (caractere == '(')
            valeur = ChargerEquation(fichier, caract, level, '+', continuer);

        else if (caractere >= '0' && caractere <= '9')
            valeur = getValeur(fichier, caractere);

    }
    while (caractere!='$' && caractere!=')');
    return valeur;
}



NB : 'i', c'est pour les dégâts minimum, il est suivis d'un no de 0 à 3 qui représente le type de dégâts (physique, feu, foi, corrosion). Idem avec 'a' qui représente les dégâts max. 'b' représente l'armure. 'l' le niveau du miracle.

Si on rencontre une parenthèse ouverte, on considère ça comme une nouvelle équations qu'on va charger.

NB : while (caractere!='$'); est devenu while (caractere!='$' && caractere!=')');. En effet, on s'arrête si on arrive à la fin d'un bloc de parenthèses.

Si l'on rencontre un chiffre, c'est que nous sommes en présence d'un nombre, on va donc charger ce nombre avec cette petite fonction toute simple :

float getValeur(ifstream &fichier, char caractere)
{
    float temp = 0;
    
    fichier.putback(caractere);
    
    fichier>>temp;
  
    return temp;
}



Je suis obligé de passer par là car on a déjà lu le premier chiffre du nombre ; on doit donc revenir d'un caractère en arrière avant de lire le nombre.





Premiers opérateurs

Maintenant, occupons-nous du '+'.
Si nous avons un '+', nous devons ajouter ce qui suit.

if (caractere == '+')
{
    valeur += ChargerEquation(fichier, caract, level, '+', continuer);
}



En effet, si l'on refait un ChargerEquation(), il va lire le prochain nombre et l'ajouter ; si nous sommes en présence d'une parenthèse, il va lire le bloc de parenthèse et ajouter sa valeur finale.

NB : N'oubliez pas que le fichier est passé en référence, il va donc à chaque fois garder sa position.

Mais, si nous ajoutons le signe '-', ce système ne va pas tout à fait marcher :

if (caractere == '+')
{
    valeur += ChargerEquation(fichier, caract, level, '+', continuer);
}
else if (caractere == '-')
{
    valeur -= ChargerEquation(fichier, caract, level, '+', continuer);
}



En effet, imaginons que nous ayons :
6 + 10 - 5 - 3 + 8

Notre parser va charger 6, lire + et donc ajouter ce qui suit ;
soit : lire 10 puis retirer ce qui suit ;
soit : lire 5 puis retirer ce qui suit ;
soit : lire 3 et ajouter 8 ;

Ça nous fait donc 3 + 8 = 11
5 - 11 = -6
10 - (-6) = 16
6 + 16 = 22

Ça ne vous semble pas bizarre ? Moi si en tout cas !
En effet : 6 + 10 - 5 - 3 + 8 = 16 !
Il y a un problème au niveau de la priorité des opérations.
C'est pourquoi ne devons passer '-' en paramètre à la fonction ChargerEquation() après un '-', et nous arrêter après pour revenir en mode '+'.

if (caractere == '+')
{
     if(priorite == '+')
     {
        valeur += ChargerEquation(fichier, caract, level, '+', continuer);

        if(!*continuer)
            return valeur;
        else
            *continuer = false;
     }
     else
     {
         fichier.putback(caractere);
        *continuer = true;
         return valeur;
     }
}
else if (caractere == '-')
{
     if(priorite == '+')
     {
        valeur -= ChargerEquation(fichier, caract, level, '-', continuer);

        if(!*continuer)
            return valeur;
        else
            *continuer = false;
     }
     else
     {
         fichier.putback(caractere);
        *continuer = true;
         return valeur;
     }
}



Donc, on continue à ajouter et retirer normalement tant que nous avons une priorité de type '+'. Si nous passons en '-', nous le faisons que pour une seule valeur : la suivante.

C'est donc ici qu'interviennent les paramètres continuer et priorite.
Priorite permet de savoir de quel type d'opération il était question juste avant, et continuer permet de savoir si on doit continuer dans le bloc d'opération courant ou si on revient au précédent.

Concrètement, imaginez l'équation : 7 - 3 - 2 + 1
Il va donc charger 7, il voit le '-' et retire le bloc qui suit, soit '3', mais il doit stopper ce bloc juste après.
Il va trouver le second '-', mais dans un bloc avec une priorité '-'. Il stoppe donc le bloc en revenant un caractère en arrière pour que le bloc précédent continue.





Seconds opérateurs

Maintenant, nous pouvons rajouter le '*' et le '/' sans oublier leur priorité des opérations.

else if (caractere == '*')
{
     if(priorite == '+' || priorite == '-')
     {
        valeur *= ChargerEquation(fichier, caract, level, '*', continuer);
        if(!*continuer)
            return valeur;
        else
            *continuer = false;
     }
     else
     {
         fichier.putback(caractere);
        *continuer = true;
         return valeur;
     }
}
else if (caractere == '/')
{
     if(priorite == '+' || priorite == '-')
     {
        valeur /= ChargerEquation(fichier, caract, level, '*', continuer);
        if(!*continuer)
            return valeur;
        else
            *continuer = false;
     }
     else
     {
         fichier.putback(caractere);
        *continuer = true;
         return valeur;
     }
}




Et enfin l'exposant :

else if (caractere == '^')
{
     if( priorite == '*' || priorite == '+' || priorite == '-')
     {
        float temp = ChargerEquation(fichier, caract, level, '^', continuer);
        float buf = valeur;

        for(int i = 1 ; i < (int)temp ; ++i )
            valeur *= buf;

        if(!*continuer)
            return valeur;
        else
            *continuer = false;
     }
     else
     {
         fichier.putback(caractere);
        *continuer = true;
         return valeur;
     }
}



Je penses que ça se passe d'explications.





Code final

float ChargerEquation(ifstream &fichier, const Caracteristique &caract, int level, char priorite, bool *continuer)
{
    float valeur = 0;
    char caractere;

    do
    {
        fichier.get(caractere);

        if (caractere == '+')
        {
             if(priorite == '+')
             {
                valeur += ChargerEquation(fichier, caract, level, '+', continuer);

                if(!*continuer)
                    return valeur;
                else
                    *continuer = false;
             }
             else
             {
                 fichier.putback(caractere);
                *continuer = true;
                 return valeur;
             }
        }
        else if (caractere == '-')
        {
             if(priorite == '+')
             {
                valeur -= ChargerEquation(fichier, caract, level, '-', continuer);

                if(!*continuer)
                    return valeur;
                else
                    *continuer = false;
             }
             else
             {
                 fichier.putback(caractere);
                *continuer = true;
                 return valeur;
             }
        }
        else if (caractere == '*')
        {
             if(priorite == '+' || priorite == '-')
             {
                valeur *= ChargerEquation(fichier, caract, level, '*', continuer);
                if(!*continuer)
                    return valeur;
                else
                    *continuer = false;
             }
             else
             {
                 fichier.putback(caractere);
                *continuer = true;
                 return valeur;
             }
        }
        else if (caractere == '/')
        {
             if(priorite == '+' || priorite == '-')
             {
                valeur /= ChargerEquation(fichier, caract, level, '*', continuer);
                if(!*continuer)
                    return valeur;
                else
                    *continuer = false;
             }
             else
             {
                 fichier.putback(caractere);
                *continuer = true;
                 return valeur;
             }
        }
        else if (caractere == '^')
        {
             if( priorite == '*' || priorite == '+' || priorite == '-')
             {
                float temp = ChargerEquation(fichier, caract, level, '^', continuer);
                float buf = valeur;

                for(int i = 1 ; i < (int)temp ; ++i )
                    valeur *= buf;

                if(!*continuer)
                    return valeur;
                else
                    *continuer = false;
             }
             else
             {
                 fichier.putback(caractere);
                *continuer = true;
                 return valeur;
             }
        }

        else if (caractere == 'l')
            valeur = level;

        else if (caractere == 'i')
        {
            int no = 0;
            fichier>>no;
            if(no >=0 && no < 4)
                valeur = caract.degatsMin[no];
        }
        else if (caractere == 'a')
        {
            int no = 0;
            fichier>>no;
            if(no >=0 && no < 4)
                valeur = caract.degatsMax[no];
        }
        else if (caractere == 'b')
            valeur = caract.armure[PHYSIQUE];

        else if (caractere == '(')
            valeur = ChargerEquation(fichier, caract, level, '+', continuer);

        else if (caractere >= '0' && caractere <= '9')
            valeur = getValeur(fichier, caractere);

    }
    while (caractere!='$' && caractere!=')');
    return valeur;
}



Avez-vous des questions ?

Par Gregouar - 07/07/2010

Holyspirit
49

Aujourd'hui, je vais vous parler de mon moteur de lumières dynamiques 2D que j'ai réalisé spécialement pour Holyspirit.

Tout d'abord, il m'a fallu trouver un moyen de représenter une lumière. J'ai assez rapidement trouvé que la meilleure solution, à mes yeux, est de la représenter sous forme de triangles. Une lumière serait donc un ensemble de triangle partant d'un centre.

Aujourd'hui, je vais vous parler de mon moteur de lumières dynamiques 2D que j'ai réalisé spécialement pour Holyspirit.

Tout d'abord, il m'a fallu trouver un moyen de représenter une lumière. J'ai assez rapidement trouvé que la meilleure solution, à mes yeux, est de la représenter sous forme de triangles. Une lumière serait donc un ensemble de triangle partant d'un centre.
Pour ça, j'ai utilisé les sf::Shape de la SFML. J'ai aussi profité du système de dégradé inclus.

J'ai codé un petit manager de lumières, permettant d'en ajouter, les déplacer, etc.

Pour ajouter une lampe, ça donne par exemple (position, intensité, rayon, qualité, couleur) :

Light_Entity light;
light=
Manager->Add_Dynamic_Light(sf::Vector2f(600,200),255,160,16,sf::Color(0,255,0));

Je peux accéder à mes lampes en faisant par exemple :
Manager->SetRadius(light,128 );

Pour générer la lumière de base, il suffit simplement de prendre 2 fois pi qu'on divise par le nombre de triangles souhaités. On boucle sur le nombre de triangles et on les ajoutes à chaque fois via une fonction AddTriangle() qui prend en paramètre les deux points extrémités du triangle, le troisième étant le centre de la lumière, donc 0,0.
Dans cette fonction, j'ajoute un sf::Shape à ma liste, lui place un point en 0,0 et les deux entre aux points donnés en paramètre à la fonction.

Voici un exemple de lumière verte, de qualité 16 (Donc composée à la base de 16 triangles).
http://holyspirit.alpha-arts.net/Autres/Lampe01.png

Maintenant, il me faut pouvoir adapter ça à l'environnement.
Tout d'abord, les lumières sont affichées à l'écran en mode de rendu Add, ce qui signifie que la couleur des pixels s'additionnent. Ainsi, les lumières s'additionnent entre elles.

http://holyspirit.alpha-arts.net/Autres/Lampe02.png

Et je les dessinent toutes sur une RenderImage. En gros, c'est un écran virtuel sur lequel on peut dessiner, mais dont on peut ensuite récupérer son rendu pour l'afficher à l'écran réel.
Je vais afficher ce rendu en mode Multiply, la couleur des pixels est donc multipliée par la couleur du pixel de mon objet rendu divisé par 255.
On obtient ceci, avec un fond d'herbes :

http://holyspirit.alpha-arts.net/Autres/Lampe03.png

Il me suffit de changer la couleur de fond de ma RenderImage pour faire une lumière ambiante. La nuit, on prend une couleur bleu foncé, le soir et le matin orangée et la journée grise clair.
Vu que les lumières sont affichées en Add, tout se fond parfaitement.

Maintenant, passons aux choses sérieuses : les murs.

Je vais donc ajouter à mon gestionnaire de lumière la possibilité d'ajouter des murs qui empêchent la lumière de passer.

Pour voir le résultat et faire les tests, je vais aussi ajouter une séries de lignes qui les représentent. Cela donne ceci :

http://holyspirit.alpha-arts.net/Autres/Lampe04.png

On donne en paramètre à notre fonction AddTriangle la liste de ces murs.

Il va maintenant nous falloir trouver les intersections entre ces murs et ces triangles, afin de déplaces les points extrémités de ces triangles, voir même en ajouter d'autres.

J'ai trouvé trois cas de figure d'intersections mur/triangle :

a) Soit le mur traverse complétement le triangle. Dans ce cas, je déplace les deux points extrémités du triangle jusqu'aux points d'intersections.

http://holyspirit.alpha-arts.net/Autres/Lampe05.png

b) Soit le mur traverse l'un des deux côté du triangle et le côté extrémité. Dans ce cas de figure, je déplace le point extrémité en dehors de la lumière et le replace à l'intersection entre le côté extrémité et le mur. J'ajoute un nouveau triangle avec comme points extrémités les deux intersections.

http://holyspirit.alpha-arts.net/Autres/Lampe06.png

c) Soit l'extrémité du mur est contenu dans le triangle. Là, je coupe simplement le triangle en deux. Vu que je fais mes vérifications dans l'ordre c,a,b : on se retrouvera dans le cas de figure a et tout ira bien.

http://holyspirit.alpha-arts.net/Autres/Lampe07.png

Et c'est tout. En pratique, ça donne ce rendu :

http://holyspirit.alpha-arts.net/Autres/Lampe08.png

Mais voilà, on obtient un transition fort marqué, c'est un peu pixéllaire. Je vais donc appliquer un shader de flou afin d'avoir un rendu plus agréable.

On obtient alors un gentil petit rendu comme ça :

http://holyspirit.alpha-arts.net/Autres/Lampe09.png

Et dans Holyspirit :

http://holyspirit.alpha-arts.net/Screenshots/Screen%20cdn%20138.png

Maintenant, pour ceux qui sont plus motivés, je vais vous donner mes algorithmes.

Pour calculer le point d'intersection entre deux droites à partir de 4 points, i étant le point résultant. p1 et p2 définissent la première droite et q1 et q2 la deuxième. Pour calculer le point, je calcule leur équation cartésienne, donc y = ax + b et y = cx + d.
Pour l'intersection, je les égales :
ax + b = cx + d
x = (d-b)/(a-c)
y = ax + b
C'est une simple résolution de système de deux équations à deux inconnues.

float a = (p2.y - p1.y) / (p2.x - p1.x);
        float b = p1.y - p1.x * a;

        float c = (q2.y - q1.y) / (q2.x - q1.x);
        float d = q1.y - q1.x * c;

        i.x = (d-b)/(a-c);
        i.y = a * i.x + b;


PS : N'oubliez pas de gérer les exceptions. Je ne les mets pas ici, mais c'est assez simple. Vous pourrez regardé dans le code donné en fin de billet si vous voulez.

Pour calculer si le point d'intersection est compris dans les segments :
Je calcule le point d'intersection grâce à la fonction définie ci-dessus, ensuite je vérifie simplement s'il fait partie des rectangles formés par les extrémités des segments. Je rajoute une petite marge d'erreur de 0,1.

sf::Vector2f i;
    i = Intersect(p1, p2, q1, q2);

    if(((i.x >= p1.x - 0.1 && i.x <= p2.x + 0.1) 
        || (i.x >= p2.x - 0.1 && i.x <= p1.x + 0.1))
    && ((i.x >= q1.x - 0.1 && i.x <= q2.x + 0.1) 
        || (i.x >= q2.x - 0.1 && i.x <= q1.x + 0.1))
    && ((i.y >= p1.y - 0.1 && i.y <= p2.y + 0.1) 
        || (i.y >= p2.y - 0.1 && i.y <= p1.y + 0.1))
    && ((i.y >= q1.y - 0.1 && i.y <= q2.y + 0.1) 
        || (i.y >= q2.y - 0.1 && i.y <= q1.y + 0.1)))
        return i;
    else
        return sf::Vector2f (0,0);


Si la collision n'est pas vérifiée, le point d'intersection vaut 0,0.

Pour vérifier si il y a une extrémité du mur qui est dans le triangle :

Les deux points de mon mur sont l1 et l2, ici, je ne vais faire qu'avec l1 mais c'est la même chose avec l2.

Je regarde d'abord s'il est dans le cercle de la lampe, avec une simple comparaison de distances au carré.

Je calcule le point d'intersection entre le côté extrémité du triangle et la droite passant par l'origine et le point du mur.
Je vérifie ensuite si cette intersection fait partie du segment représentant le côté extrémité du triangle. Si oui, j'ajoute un nouveau triangle et déplace l'un des points extrémité du triangle jusqu'à l'intersection.
Je vérifie aussi si on est du bon côté et non pas à l'opposé.

if(l1.x * l1.x + l1.y * l1.y < m_radius * m_radius)
        {
            sf::Vector2f i = Intersect(pt1,pt2,sf::Vector2f (0,0),l1);

            if((pt1.x > i.x && pt2.x < i.x) || (pt1.x < i.x && pt2.x > i.x))
            if((pt1.y > i.y && pt2.y < i.y) || (pt1.y < i.y && pt2.y > i.y))
                if(l1.y > 0 && i.y > 0 || l1.y < 0 && i.y < 0)
                if(l1.x > 0 && i.x > 0 || l1.x < 0 && i.x < 0)
                AddTriangle(i, pt2, w, m_wall), pt2 = i;
        }


Je calcule ensuite les 3 points d'intersections possibles avec les trois côté du triangle :

sf::Vector2f m = Collision(l1, l2, sf::Vector2f(0,0), pt1);
        sf::Vector2f n = Collision(l1, l2, sf::Vector2f(0,0), pt2);
        sf::Vector2f o = Collision(l1, l2, pt1, pt2);


Enfin, si j'ai une intersection en m et n, c'est que je suis dans le cas de figure a, je peux donc simplement déplacer les points.

Sinon, je regarde quel côté à l'intersection et j'ajoute le triangle et déplace le point.

if((m.x != 0 || m.y != 0) && (n.x != 0 || n.y != 0))
            pt1 = m, pt2 = n;
        else
        {
            if((m.x != 0 || m.y != 0) && (o.x != 0 || o.y != 0))
                AddTriangle(m ,o , w, m_wall), pt1 = o;

            if((n.x != 0 || n.y != 0) && (o.x != 0 || o.y != 0))
                AddTriangle(o ,n , w, m_wall), pt2 = o;
        }


Et c'est fini !

Vous pouvez trouver un programme d'exemple avec les sources ici : Holyspirit : Moteur de lumières dynamiques
Si vous avez des questions, remarques ou idées d'optimisation, je vous suis tout ouï.


Par Gregouar - 10/06/2010


L'Occlusion Ambiente ou Ambient Occlusion en anglais est une technique d'illumination globale quelques peut particulière que l'on utilise conjointement avec la GI (Global Illumination). Dans ce tips, nous verrons donc rapidement comment mettre en place une AO dans 3DS Max. Un tips fera suite afin d'exporter cette AO dans notre dépliage, mais nous y reviendrons en détails plus[...]

http://www.alpha-arts.net/programmes/galerie/Tips/Tips3DS.png

L'Occlusion Ambiente ou Ambient Occlusion en anglais est une technique d'illumination globale quelques peut particulière que l'on utilise conjointement avec la GI (Global Illumination). Dans ce tips, nous verrons donc rapidement comment mettre en place une AO dans 3DS Max. Un tips fera suite afin d'exporter cette AO dans notre dépliage, mais nous y reviendrons en détails plus tard.

Wikipedia
L'algorithme est un principe d'illumination globale, à savoir que la lumière est émise de partout dans l'espace 3D, et non pas d'un point tel une lampe. Le principe de l'AO est que plus deux faces sont rapprochées, plus la quantité de lumière diminue entre ces deux faces. En définissant avant le rendu le taux de lumière globale, on influence cette lumière, pouvant même lui donner une couleur particulière.


Ce tips ferra l'objet de 4 tips en tout qui sont :
  • Mise en place de l'AO
  • Post prod passe d'AO
  • Render to texture pour L'AO
  • L'AO et Blender


Pour ce tips, je vais illustrer mon exemple avec la Taverne qui est actuellement en cours de refonte. Voici donc actuellement un rendu de cette dernière avec et sans AO et juste la passe d'AO.
http://www.alpha-arts.net/programmes/galerie/Tips/Tuto%20AO/Taverne-Comparaison.jpg

On remarques donc que l'AO permet de donner beaucoup plus de volume, en corrigeant les défauts de la solution de GI utilisée ici.

Maintenant, nous allons voir comment mettre ça en place.
Tout d'abord, rendez-vous dans le panneau Render Setup (Raccourcis F10) puis dans l'onglet Common . Allez dans les options Assign Renderer afin de passer sur Mental Ray, si ce n'est déjà fait. (Fig 1)
Il est essentiel d'utiliser Mental Ray comme moteur de rendu car ce dernier est de type "Raytracing", en d'autre terme c'est un lanceur de rayon et si vous avez compris le principe de l'AO il s'agit d'une technique utilisant le lancer de rayon, d'ou l'importance de Mental Ray ! D'autre moteur peuvent faire l'affaire t'elle que V-ray, cependant puisque mental ray est inclut à 3DS max on ne va pas quand même le bouder !

http://www.alpha-arts.net/programmes/galerie/Tips/Tuto%20AO/Fig-1.jpg


On passe maintenant dans le panneau des matériaux (1, Raccourcis M). Choisissez un matériaux vide. Nous allons le changer (2) et prendre un shader Mental Ray (3). Votre nouveau matériau s'affiche pour nous donner de nouvelles options. (Fig. 2)
http://www.alpha-arts.net/programmes/galerie/Tips/Tuto%20AO/Fig-2.jpg


Dans le slot Surface (1), nous allons mettre en place un nouveau matériau, à savoir Ambient/Reflective Occlusion (2). Pour expliquer rapidement, nous utilisons un matériau de Mental Ray pour y inclure un shader spécial AO. Mental Ray ne peut pas calculer cette solution autrement que via ce shader. (Fig 3)
http://www.alpha-arts.net/programmes/galerie/Tips/Tuto%20AO/Fig-3.jpg


Enfin, pour pouvoir calculer votre passe d'AO, Mental Ray demande de n'avoir aucune lumière dans la scène. Nous allons donc devoir toutes les désactiver. Pour ce faire aller dans Tools > > Light Lister (1 et 2). Un nouveau panneau s'ouvre, contenant toutes vos lumières. Nous allons tout simplement décocher nos lumières dans la colonne On (3). Vos lumière sont maintenant totalement désactivées mais toujours présentes dans votre scène. Nous pourrons donc les remettre en places très rapidement si besoin. (Fig 4)
http://www.alpha-arts.net/programmes/galerie/Tips/Tuto%20AO/Fig-4.jpg


Si votre scène contient des paramètres d'exposition, il vous faudra là aussi les désactiver.. Rendez-vous donc dans le panneau Rendering > > Exposure Control, un nouveau panneau s'ouvre, décochez activ si ce dernier est coché.
http://www.alpha-arts.net/programmes/galerie/Tips/Tuto%20AO/Fig-5.jpg


Maintenant, il nous reste à désactiver notre GI et FG. Retournez dans le panneau Render Setup (1, raccourcis F10), l'onglet Indirect Illumination (2). Il vous suffit de décocher Enable pour votre GI et FG (3 et 4).
http://www.alpha-arts.net/programmes/galerie/Tips/Tuto%20AO/Fig-6.jpg


Nous touchons enfin au but ! Maintenant, nous allons activer notre shader d'AO sur toute la scène via le Material Overide dans le render setup. Dans l'onglet Processing (1) nous activons la ligne Material Overide (2) puis nous faisons glisser notre shader d'AO dessus (3), nous pouvons enfin lancer le rendu.
http://www.alpha-arts.net/programmes/galerie/Tips/Tuto%20AO/Fig-7.jpg


Si votre modélisation est à l'échelle, il n'y a pas besoin de toucher au paramètre d'AO. Mais comme je suis gentil, je vais tout de même vous donner quelques détails sur les réglages du shader. Il faut savoir que vous devrez toucher généralement à un seul des paramètres du shader, à savoir Max Distance, parfois le sample mais très rarement. A quoi servent donc ces réglages ?
  • Sample : Il s'agit de la précision de votre AO : une précision élevée permet d'obtenir moins de grain mais augmente son temps de calcul, le réglage à 16 reste généralement suffisant.
  • Max Distance : Votre AO est calculée sur une distance, cette distance se paramètre via cette options. Comme je vous l'ai dit, si votre scène est à l'échelle, normalement le rendu sera bon.


Si vous avez tout suivez et tout compris tout devrez ce passer sans aucun problème. Si besoin laisser un commentaire ou contacter nous via le forum.
La suite la semaine prochaine

Par stilobique - 08/01/2011

Suite aux nombreuses questions que vous m'avez posées à propos de ma gestion des skins des héros, j'ai décidé d'en faire un article.

Tout d'abord, il faut savoir que nous avons décidé de mettre 4 parties changeables aux héros : le corps, la tête, la main gauche et la main droite.

Nous faisons les rendus de tout le corps pour l'amure, mais nous ne rendons que[...]

Suite aux nombreuses questions que vous m'avez posées à propos de ma gestion des skins des héros, j'ai décidé d'en faire un article.

Tout d'abord, il faut savoir que nous avons décidé de mettre 4 parties changeables aux héros : le corps, la tête, la main gauche et la main droite.

Nous faisons les rendus de tout le corps pour l'amure, mais nous ne rendons que l'élément concerné pour les trois autres (tête et casque pour l'un, main(s) et arme/bouclier pour les autres).

Skin en fonction de l'objet

Chaque objet contient les noms des skins qui lui sont attribués (ou pas, si c'est un objet qui ne modifie par l'apparence, comme un anneau).

Il faut savoir que suivant l'équipement du joueur, l'animation, et donc le skin, peut changer. Si le héros se bat avec une arme à deux mains, ce n'est pas le même skin d'armure que s'il se bat avec une arme dans chaque mains.
Nous sommes donc obligés d'attribuer plusieurs skins à chaque objet.

Voici un exemple d'armure :

* e0 mArmors/Chain_mail/OneHand.rs.hs e0 mArmors/Chain_mail/TwoWeapons.rs.hs e0 mArmors/Chain_mail/TwoHands.rs.hs $


Il faut donner les skins dans l'ordre : combat avec une arme à une main, combat avec une arme dans chaque main et combat avec une arme à deux mains.

L'animation de toutes les armes à une mains et de toutes les armes à deux mains sont les même. Donc nous sommes parti du principe que l'animation d'une hache à deux mains est la même qu'une épée à deux mains.


Ensuite, le e[X] représente le numéro de l'emplacement du skin :
  • 0 = corps
  • 1 = tête
  • 2 = main droite (utilisé aussi pour les deux mains en même temps)
  • 3 = main gauche


Voici un exemple de casque :
* e1 mHelmets/Iron_cap_1h.rs.hs e1 mHelmets/Iron_cap_1h.rs.hs e1 mHelmets/Iron_cap_2h.rs.hs $


Un bouclier :
* e3 mShields/Great_shield.rs.hs $

Dans le cas où nous avons un bouclier, le joueur ne peut pas se battre avec une arme dans chaque main ou une arme à deux mains.


Une épée à une main :
* e2 mWeapons_OneHand/Long_sword_r.rs.hs e3 mWeapons_OneHand/Long_sword_l.rs.hs $

Remarquez que le numéro de l'emplacement change suivant si le joueur se bat avec une arme dans chaque main ou non.


Une épée à deux mains :
* e2 mWeapons_TwoHands/Two_hands_sword.rs.hs $

Vous dites qu'il devrait se trouver en troisième position ? Et bien non, si le jeu ne trouve qu'un cas, il le force sur celui-là.


Ordre d'affichage

Bien, maintenant que nous savons comment ça marche du côté des objets, attaquons-nous à la structure de ces fichiers *.rs.hs qui contiennent le skin de l'objet.

Chaque fichier *.rs.hs est en fait un fichier *.dat (je vous conseille ce document pour gérer les *.dat : http://www.sfml-dev.org/wiki/fr/tutoriels/formatdat )

Ils contiennent un fichier info.txt, contenant toutes les infos sur les sprites (position dans l'image, centre, taille et j'en passe) ainsi que les images associées au skin.

Ces fichiers *.rs.hs sont aussi utilisés pour les autres entités (monstres, pnj, ...).

Ils sont structurés comme ceci :
*Crusader_LongSword_R_Wait0.png
*Crusader_LongSword_R_Walk0.png
*Crusader_LongSword_R_Attack0.png
$
//Ici, ce sont normalement les sons, mais je ne les mets que sur l'armure
$
* x0 y0 w45 h56 ex2 ey83 i0 a1 n0.5 g2 $
* x45 y0 w46 h56 ex1 ey85 i0 a2 n0.16 g2 $
* x91 y0 w45 h58 ex0 ey89 i0 a3 n0.16 g2 $
* x136 y0 w46 h57 ex2 ey89 i0 a4 n0.16 g2 $
* x182 y0 w46 h54 ex3 ey85 i0 a5 n0.16 g2 $
* x228 y0 w46 h52 ex4 ey82 i0 a6 n0.16 g2 $
* x274 y0 w46 h53 ex4 ey81 i0 a7 n0.16 g2 $
* x320 y0 w45 h53 ex3 ey81 i0 a0 n0.16 g2 $
$
* x365 y0 w33 h70 ex-12 ey100 i0 a1 n0.5 g2 $
* x398 y0 w30 h71 ex-11 ey103 i0 a2 n0.16 g2 $
* x428 y0 w28 h72 ex-7 ey105 i0 a3 n0.16 g2 $
* x456 y0 w30 h71 ex-5 ey105 i0 a4 n0.16 g2 $
* x486 y0 w35 h68 ex-5 ey101 i0 a5 n0.16 g2 $
* x521 y0 w37 h67 ex-6 ey98 i0 a6 n0.16 g2 $
* x558 y0 w37 h68 ex-8 ey98 i0 a7 n0.16 g2 $
* x595 y0 w35 h69 ex-11 ey99 i0 a0 n0.16 g2 $
$
* x630 y0 w8 h75 ex-17 ey113 i0 a1 n0.5 g-2 $
* x638 y0 w9 h76 ex-12 ey114 i0 a2 n0.16 g-2 $
* x647 y0 w12 h75 ex-3 ey113 i0 a3 n0.16 g-2 $
* x659 y0 w10 h76 ex-4 ey113 i0 a4 n0.16 g-2 $
* x669 y0 w8 h76 ex-8 ey112 i0 a5 n0.16 g-2 $
* x677 y0 w9 h75 ex-11 ey111 i0 a6 n0.16 g-2 $
* x686 y0 w9 h75 ex-14 ey111 i0 a7 n0.16 g-2 $
* x695 y0 w8 h75 ex-17 ey112 i0 a0 n0.16 g-2 $
$
* x703 y0 w33 h69 ex16 ey114 i0 a1 n0.5 g-2 $
* x736 y0 w36 h68 ex23 ey112 i0 a2 n0.16 g-2 $
* x772 y0 w38 h67 ex29 ey108 i0 a3 n0.16 g-2 $
* x810 y0 w37 h68 ex28 ey108 i0 a4 n0.16 g-2 $
* x847 y0 w33 h70 ex22 ey110 i0 a5 n0.16 g-2 $
* x880 y0 w31 h71 ex17 ey112 i0 a6 n0.16 g-2 $
* x911 y0 w31 h71 ex15 ey113 i0 a7 n0.16 g-2 $
* x942 y0 w31 h70 ex14 ey114 i0 a0 n0.16 g-2 $
$
* x973 y0 w45 h55 ex43 ey103 i0 a1 n0.5 g-2 $
* x0 y75 w45 h53 ex45 ey98 i0 a2 n0.16 g-2 $
* x45 y75 w45 h52 ex45 ey94 i0 a3 n0.16 g-2 $
* x90 y75 w46 h53 ex44 ey94 i0 a4 n0.16 g-2 $
* x136 y75 w46 h56 ex43 ey98 i0 a5 n0.16 g-2 $
* x182 y75 w46 h56 ex42 ey100 i0 a6 n0.16 g-2 $
* x228 y75 w46 h57 ex42 ey102 i0 a7 n0.16 g-2 $
* x274 y75 w45 h56 ex42 ey103 i0 a0 n0.16 g-2 $
$
* x319 y75 w33 h40 ex45 ey85 i0 a1 n0.5 g2 $
* x352 y75 w30 h39 ex41 ey81 i0 a2 n0.16 g2 $
* x382 y75 w28 h38 ex35 ey78 i0 a3 n0.16 g2 $
* x410 y75 w30 h39 ex35 ey79 i0 a4 n0.16 g2 $
* x440 y75 w35 h40 ex40 ey81 i0 a5 n0.16 g2 $
* x475 y75 w38 h41 ex44 ey83 i0 a6 n0.16 g2 $
* x513 y75 w37 h41 ex45 ey85 i0 a7 n0.16 g2 $
* x550 y75 w35 h41 ex46 ey86 i0 a0 n0.16 g2 $
$
* x585 y75 w8 h36 ex25 ey72 i0 a1 n0.5 g2 $
* x593 y75 w9 h35 ex21 ey70 i0 a2 n0.16 g2 $
* x602 y75 w12 h35 ex15 ey70 i0 a3 n0.16 g2 $
* x614 y75 w10 h35 ex14 ey71 i0 a4 n0.16 g2 $
* x624 y75 w8 h34 ex16 ey70 i0 a5 n0.16 g2 $
* x632 y75 w9 h35 ex20 ey71 i0 a6 n0.16 g2 $
* x641 y75 w9 h34 ex23 ey71 i0 a7 n0.16 g2 $
* x650 y75 w8 h35 ex25 ey72 i0 a0 n0.16 g2 $
$
* x658 y75 w33 h41 ex17 ey71 i0 a1 n0.5 g2 $
* x691 y75 w36 h42 ex13 ey72 i0 a2 n0.16 g2 $
* x727 y75 w38 h43 ex9 ey75 i0 a3 n0.16 g2 $
* x765 y75 w37 h42 ex9 ey75 i0 a4 n0.16 g2 $
* x802 y75 w33 h39 ex11 ey72 i0 a5 n0.16 g2 $
* x835 y75 w31 h37 ex14 ey70 i0 a6 n0.16 g2 $
* x866 y75 w32 h38 ex16 ey70 i0 a7 n0.16 g2 $
* x898 y75 w33 h39 ex17 ey70 i0 a0 n0.16 g2 $
$
* x0 y0 w45 h55 ex3 ey82 i1 a1 n0.04 g2 $
* x45 y0 w54 h48 ex12 ey75 i1 a2 n0.04 g2 $
* x99 y0 w67 h32 ex28 ey62 i1 a3 n0.04 g2 $
* x166 y0 w72 h24 ex36 ey55 i1 a4 n0.04 g2 $
* x238 y0 w70 h27 ex33 ey57 i1 a5 n0.04 g2 $
* x308 y0 w67 h35 ex27 ey64 i1 a6 n0.04 g2 $
* x375 y0 w58 h47 ex16 ey73 i1 a7 n0.04 g2 $
* x433 y0 w46 h57 ex4 ey83 i1 a8 n0.04 g2 $
* x479 y0 w33 h65 ex-7 ey93 i1 a9 n0.04 g2 $
* x512 y0 w21 h67 ex-14 ey100 i1 a10 n0.04 g2 $
* x533 y0 w19 h67 ex-18 ey104 i1 a11 n0.04 g2 $
* x552 y0 w19 h66 ex-20 ey105 i1 a12 n0.04 g2 $
* x571 y0 w19 h67 ex-17 ey103 i1 a13 n0.04 g2 $
* x590 y0 w28 h65 ex-10 ey95 i1 a14 n0.04 g2 $
* x618 y0 w40 h59 ex-1 ey86 i1 a0 n0.04 g2 $
* x658 y0 w45 h55 ex3 ey82 i1 a0 n0.04 g2 $
$
* x703 y0 w33 h70 ex-11 ey99 i1 a1 n0.04 g2 $
* x736 y0 w39 h67 ex-6 ey93 i1 a2 n0.04 g2 $
* x775 y0 w50 h55 ex6 ey79 i1 a3 n0.04 g2 $
* x825 y0 w56 h46 ex13 ey71 i1 a4 n0.04 g2 $
* x881 y0 w55 h50 ex11 ey74 i1 a5 n0.04 g2 $
* x936 y0 w48 h56 ex4 ey80 i1 a6 n0.04 g2 $
* x984 y0 w40 h65 ex-4 ey90 i1 a7 n0.04 g2 $
* x0 y69 w30 h71 ex-13 ey100 i1 a8 n0.04 g2 $
* x30 y69 w21 h74 ex-19 ey109 i1 a9 n0.04 g2 $
* x51 y69 w14 h71 ex-23 ey114 i1 a10 n0.04 g2 $
* x65 y69 w12 h64 ex-26 ey114 i1 a11 n0.04 g2 $
* x77 y69 w13 h62 ex-26 ey114 i1 a12 n0.04 g2 $
* x90 y69 w13 h67 ex-25 ey114 i1 a13 n0.04 g2 $
* x103 y69 w20 h72 ex-20 ey111 i1 a14 n0.04 g2 $
* x123 y69 w29 h71 ex-14 ey103 i1 a0 n0.04 g2 $
* x152 y69 w33 h70 ex-11 ey99 i1 a0 n0.04 g2 $
$
* x185 y69 w8 h75 ex-17 ey112 i1 a1 n0.04 g-2 $
* x193 y69 w8 h74 ex-18 ey106 i1 a2 n0.04 g-2 $
* x201 y69 w8 h65 ex-17 ey92 i1 a3 n0.04 g-2 $
* x209 y69 w10 h59 ex-15 ey85 i1 a4 n0.04 g-2 $
* x219 y69 w9 h60 ex-16 ey87 i1 a5 n0.04 g-2 $
* x228 y69 w8 h66 ex-18 ey94 i1 a6 n0.04 g-2 $
* x236 y69 w9 h72 ex-19 ey103 i1 a7 n0.04 g-2 $
* x245 y69 w12 h75 ex-16 ey112 i1 a8 n0.04 g-2 $
* x257 y69 w10 h69 ex-16 ey114 i1 a9 n0.04 g-2 $
* x267 y69 w8 h61 ex-16 ey114 i1 a10 n0.04 g-2 $
* x275 y69 w8 h54 ex-14 ey114 i1 a11 n0.04 g-2 $
* x283 y69 w8 h52 ex-14 ey114 i1 a12 n0.04 g-2 $
* x291 y69 w8 h56 ex-15 ey114 i1 a13 n0.04 g-2 $
* x299 y69 w9 h66 ex-16 ey114 i1 a14 n0.04 g-2 $
* x308 y69 w8 h74 ex-17 ey114 i1 a0 n0.04 g-2 $
* x316 y69 w8 h75 ex-17 ey112 i1 a0 n0.04 g-2 $
$
* x324 y69 w33 h70 ex15 ey114 i1 a1 n0.04 g-2 $
* x357 y69 w39 h66 ex15 ey107 i1 a2 n0.04 g-2 $
* x396 y69 w47 h57 ex12 ey95 i1 a3 n0.04 g-2 $
* x443 y69 w47 h50 ex8 ey88 i1 a4 n0.04 g-2 $
* x490 y69 w48 h53 ex10 ey91 i1 a5 n0.04 g-2 $
* x538 y69 w48 h57 ex13 ey96 i1 a6 n0.04 g-2 $
* x586 y69 w46 h63 ex17 ey104 i1 a7 n0.04 g-2 $
* x632 y69 w38 h68 ex18 ey113 i1 a8 n0.04 g-2 $
* x670 y69 w28 h63 ex16 ey114 i1 a9 n0.04 g-2 $
* x698 y69 w18 h57 ex13 ey114 i1 a10 n0.04 g-2 $
* x716 y69 w15 h52 ex15 ey114 i1 a11 n0.04 g-2 $
* x731 y69 w15 h50 ex16 ey114 i1 a12 n0.04 g-2 $
* x746 y69 w14 h54 ex13 ey114 i1 a13 n0.04 g-2 $
* x760 y69 w21 h61 ex13 ey114 i1 a14 n0.04 g-2 $
* x781 y69 w30 h68 ex15 ey114 i1 a0 n0.04 g-2 $
* x811 y69 w33 h70 ex15 ey114 i1 a0 n0.04 g-2 $
$
* x844 y69 w45 h56 ex42 ey103 i1 a1 n0.04 g-2 $
* x889 y69 w54 h48 ex42 ey96 i1 a2 n0.04 g-2 $
* x943 y69 w67 h35 ex39 ey85 i1 a3 n0.04 g-2 $
* x0 y143 w72 h29 ex36 ey80 i1 a4 n0.04 g-2 $
* x72 y143 w70 h30 ex37 ey81 i1 a5 n0.04 g-2 $
* x142 y143 w67 h35 ex40 ey86 i1 a6 n0.04 g-2 $
* x209 y143 w58 h43 ex42 ey93 i1 a7 n0.04 g-2 $
* x267 y143 w46 h51 ex42 ey101 i1 a8 n0.04 g-2 $
* x313 y143 w33 h58 ex40 ey109 i1 a9 n0.04 g-2 $
* x346 y143 w21 h61 ex35 ey114 i1 a10 n0.04 g-2 $
* x367 y143 w19 h59 ex37 ey114 i1 a11 n0.04 g-2 $
* x386 y143 w19 h58 ex39 ey114 i1 a12 n0.04 g-2 $
* x405 y143 w19 h60 ex36 ey114 i1 a13 n0.04 g-2 $
* x424 y143 w28 h62 ex38 ey113 i1 a14 n0.04 g-2 $
* x452 y143 w40 h58 ex41 ey106 i1 a0 n0.04 g-2 $
* x492 y143 w45 h56 ex42 ey103 i1 a0 n0.04 g-2 $
$
* x537 y143 w33 h41 ex44 ey86 i1 a1 n0.04 g-2 $
* x570 y143 w39 h32 ex45 ey79 i1 a2 n0.04 g-2 $
* x609 y143 w50 h18 ex44 ey69 i1 a3 n0.04 g-2 $
* x659 y143 w56 h18 ex43 ey72 i1 a4 n0.04 g-2 $
* x715 y143 w55 h18 ex44 ey71 i1 a5 n0.04 g-2 $
* x770 y143 w48 h18 ex44 ey69 i1 a6 n0.04 g-2 $
* x818 y143 w40 h26 ex44 ey75 i1 a7 n0.04 g-2 $
* x858 y143 w30 h37 ex43 ey84 i1 a8 n0.04 g-2 $
* x888 y143 w21 h49 ex40 ey93 i1 a9 n0.04 g-2 $
* x909 y143 w14 h59 ex37 ey102 i1 a10 n0.04 g-2 $
* x923 y143 w12 h65 ex38 ey108 i1 a11 n0.04 g-2 $
* x935 y143 w13 h67 ex39 ey110 i1 a12 n0.04 g-2 $
* x948 y143 w13 h64 ex38 ey107 i1 a13 n0.04 g-2 $
* x961 y143 w20 h56 ex40 ey98 i1 a14 n0.04 g-2 $
* x981 y143 w29 h45 ex43 ey89 i1 a0 n0.04 g-2 $
* x0 y209 w33 h41 ex44 ey86 i1 a0 n0.04 g-2 $
$
* x33 y209 w8 h35 ex25 ey72 i1 a1 n0.04 g2 $
* x41 y209 w8 h26 ex26 ey65 i1 a2 n0.04 g2 $
* x49 y209 w8 h19 ex25 ey64 i1 a3 n0.04 g2 $
* x57 y209 w10 h21 ex25 ey68 i1 a4 n0.04 g2 $
* x67 y209 w9 h19 ex25 ey67 i1 a5 n0.04 g2 $
* x76 y209 w8 h18 ex26 ey63 i1 a6 n0.04 g2 $
* x84 y209 w9 h22 ex28 ey62 i1 a7 n0.04 g2 $
* x93 y209 w12 h35 ex28 ey72 i1 a8 n0.04 g2 $
* x105 y209 w11 h48 ex26 ey82 i1 a9 n0.04 g2 $
* x116 y209 w9 h58 ex24 ey91 i1 a10 n0.04 g2 $
* x125 y209 w8 h65 ex22 ey98 i1 a11 n0.04 g2 $
* x133 y209 w8 h67 ex22 ey100 i1 a12 n0.04 g2 $
* x141 y209 w8 h63 ex23 ey96 i1 a13 n0.04 g2 $
* x149 y209 w9 h53 ex25 ey86 i1 a14 n0.04 g2 $
* x158 y209 w8 h41 ex25 ey77 i1 a0 n0.04 g2 $
* x166 y209 w8 h35 ex25 ey72 i1 a0 n0.04 g2 $
$
* x174 y209 w33 h41 ex18 ey71 i1 a1 n0.04 g2 $
* x207 y209 w39 h32 ex24 ey64 i1 a2 n0.04 g2 $
* x246 y209 w47 h18 ex35 ey54 i1 a3 n0.04 g2 $
* x293 y209 w47 h17 ex39 ey58 i1 a4 n0.04 g2 $
* x340 y209 w48 h18 ex38 ey57 i1 a5 n0.04 g2 $
* x388 y209 w48 h18 ex35 ey54 i1 a6 n0.04 g2 $
* x436 y209 w45 h29 ex29 ey61 i1 a7 n0.04 g2 $
* x481 y209 w38 h42 ex20 ey72 i1 a8 n0.04 g2 $
* x519 y209 w29 h54 ex12 ey82 i1 a9 n0.04 g2 $
* x548 y209 w19 h62 ex5 ey91 i1 a10 n0.04 g2 $
* x567 y209 w16 h66 ex1 ey96 i1 a11 n0.04 g2 $
* x583 y209 w15 h67 ex-1 ey98 i1 a12 n0.04 g2 $
* x598 y209 w14 h64 ex1 ey94 i1 a13 n0.04 g2 $
* x612 y209 w22 h57 ex8 ey85 i1 a14 n0.04 g2 $
* x634 y209 w30 h46 ex15 ey75 i1 a0 n0.04 g2 $
* x664 y209 w33 h41 ex18 ey71 i1 a0 n0.04 g2 $
$
* x0 y0 w45 h55 ex2 ey78 i2 a1 n0.04 g2 $
* x45 y0 w43 h46 ex8 ey68 i2 a2 n0.04 g2 $
* x88 y0 w8 h68 ex16 ey105 i2 a3 n0.04 g2 $
* x96 y0 w33 h65 ex42 ey131 i2 a4 n0.04 g2 $
* x129 y0 w69 h31 ex38 ey116 i2 a5 n0.04 g2 $
* x198 y0 w30 h53 ex-48 ey139 i2 a6 n0.04 g2 $
* x228 y0 w39 h67 ex-24 ey139 i2 a7 n0.04 g-2 $
* x267 y0 w21 h71 ex-19 ey148 i2 a8 n0.04 g-2 $
* x288 y0 w13 h52 ex-19 ey135 i2 a9 n0.04 g-2 $
* x301 y0 w22 h32 ex-9 ey117 i2 a10 n0.04 g-2 $
* x323 y0 w70 h34 ex17 ey125 i2 a11 n0.04 g-2 $
* x393 y0 w22 h71 ex-48 ey124 i2 a12 n0.04 g2 $
* x415 y0 w33 h65 ex42 ey131 i2 a13 n0.04 g2 $
* x448 y0 w12 h73 ex21 ey118 i2 a14 n0.04 g2 $
* x460 y0 w41 h49 ex9 ey72 i2 a15 n0.04 g2 $
* x501 y0 w45 h55 ex2 ey78 i2 a0 n0.04 g2 $
$
* x546 y0 w34 h70 ex-17 ey97 i2 a1 n0.04 g2 $
* x580 y0 w44 h62 ex-21 ey88 i2 a2 n0.04 g2 $
* x624 y0 w15 h67 ex-21 ey108 i2 a3 n0.04 g2 $
* x639 y0 w61 h48 ex32 ey117 i2 a4 n0.04 g2 $
* x700 y0 w52 h15 ex1 ey112 i2 a5 n0.04 g2 $
* x752 y0 w33 h64 ex-23 ey166 i2 a6 n0.04 g2 $
* x785 y0 w18 h74 ex8 ey152 i2 a7 n0.04 g-2 $
* x803 y0 w19 h74 ex11 ey156 i2 a8 n0.04 g-2 $
* x822 y0 w26 h56 ex5 ey145 i2 a9 n0.04 g-2 $
* x848 y0 w25 h31 ex1 ey124 i2 a10 n0.04 g-2 $
* x873 y0 w57 h13 ex30 ey116 i2 a11 n0.04 g-2 $
* x930 y0 w31 h68 ex-14 ey141 i2 a12 n0.04 g-2 $
* x961 y0 w61 h48 ex32 ey117 i2 a13 n0.04 g2 $
* x0 y73 w23 h68 ex-10 ey116 i2 a14 n0.04 g2 $
* x23 y73 w40 h64 ex-23 ey91 i2 a15 n0.04 g2 $
* x63 y73 w34 h70 ex-17 ey97 i2 a0 n0.04 g2 $
$
* x97 y73 w9 h75 ex-24 ey113 i2 a1 n0.04 g-2 $
* x106 y73 w23 h74 ex-34 ey113 i2 a2 n0.04 g-2 $
* x129 y73 w19 h67 ex-43 ey124 i2 a3 n0.04 g-2 $
* x148 y73 w55 h25 ex4 ey110 i2 a4 n0.04 g-2 $
* x203 y73 w10 h18 ex-34 ey129 i2 a5 n0.04 g-2 $
* x213 y73 w24 h73 ex22 ey177 i2 a6 n0.04 g-2 $
* x237 y73 w37 h67 ex60 ey140 i2 a7 n0.04 g-2 $
* x274 y73 w20 h72 ex41 ey148 i2 a8 n0.04 g-2 $
* x294 y73 w28 h64 ex27 ey148 i2 a9 n0.04 g-2 $
* x322 y73 w45 h44 ex23 ey133 i2 a10 n0.04 g-2 $
* x367 y73 w18 h13 ex27 ey115 i2 a11 n0.04 g-2 $
* x385 y73 w57 h51 ex47 ey135 i2 a12 n0.04 g-2 $
* x442 y73 w55 h25 ex4 ey110 i2 a13 n0.04 g-2 $
* x497 y73 w27 h60 ex-34 ey126 i2 a14 n0.04 g-2 $
* x524 y73 w21 h74 ex-38 ey115 i2 a15 n0.04 g-2 $
* x545 y73 w9 h75 ex-24 ey113 i2 a0 n0.04 g-2 $
$
* x554 y73 w33 h69 ex11 ey117 i2 a1 n0.04 g-2 $
* x587 y73 w20 h74 ex-14 ey127 i2 a2 n0.04 g-2 $
* x607 y73 w13 h66 ex-39 ey144 i2 a3 n0.04 g-2 $
* x620 y73 w22 h14 ex-25 ey115 i2 a4 n0.04 g-2 $
* x642 y73 w48 h19 ex-6 ey141 i2 a5 n0.04 g-2 $
* x690 y73 w17 h75 ex57 ey166 i2 a6 n0.04 g-2 $
* x707 y73 w52 h51 ex86 ey111 i2 a7 n0.04 g-2 $
* x759 y73 w24 h64 ex56 ey128 i2 a8 n0.04 g-2 $
* x783 y73 w18 h72 ex37 ey144 i2 a9 n0.04 g-2 $
* x801 y73 w45 h61 ex37 ey139 i2 a10 n0.04 g-2 $
* x846 y73 w44 h9 ex49 ey102 i2 a11 n0.04 g-2 $
* x890 y73 w52 h30 ex82 ey109 i2 a12 n0.04 g-2 $
* x942 y73 w22 h14 ex-25 ey115 i2 a13 n0.04 g-2 $
* x964 y73 w15 h55 ex-38 ey142 i2 a14 n0.04 g-2 $
* x979 y73 w19 h74 ex-18 ey131 i2 a15 n0.04 g-2 $
* x0 y147 w33 h69 ex11 ey117 i2 a0 n0.04 g-2 $
$
* x33 y147 w45 h55 ex43 ey107 i2 a1 n0.04 g-2 $
* x78 y147 w43 h63 ex35 ey124 i2 a2 n0.04 g-2 $
* x121 y147 w8 h65 ex-8 ey155 i2 a3 n0.04 g-2 $
* x129 y147 w33 h18 ex-9 ey129 i2 a4 n0.04 g-2 $
* x162 y147 w69 h31 ex31 ey154 i2 a5 n0.04 g2 $
* x231 y147 w30 h69 ex78 ey139 i2 a6 n0.04 g2 $
* x261 y147 w39 h33 ex63 ey80 i2 a7 n0.04 g2 $
* x300 y147 w21 h57 ex40 ey109 i2 a8 n0.04 g2 $
* x321 y147 w13 h74 ex32 ey134 i2 a9 n0.04 g2 $
* x334 y147 w22 h73 ex31 ey138 i2 a10 n0.04 g2 $
* x356 y147 w70 h24 ex53 ey99 i2 a11 n0.04 g2 $
* x426 y147 w22 h18 ex70 ey78 i2 a12 n0.04 g2 $
* x448 y147 w33 h18 ex-9 ey129 i2 a13 n0.04 g-2 $
* x481 y147 w12 h54 ex-9 ey154 i2 a14 n0.04 g-2 $
* x493 y147 w40 h63 ex31 ey129 i2 a15 n0.04 g-2 $
* x533 y147 w45 h55 ex43 ey107 i2 a0 n0.04 g-2 $
$
* x578 y147 w34 h40 ex51 ey88 i2 a1 n0.04 g2 $
* x612 y147 w44 h46 ex65 ey104 i2 a2 n0.04 g2 $
* x656 y147 w15 h66 ex36 ey152 i2 a3 n0.04 g2 $
* x671 y147 w61 h33 ex29 ey144 i2 a4 n0.04 g2 $
* x732 y147 w51 h54 ex51 ey162 i2 a5 n0.04 g2 $
* x783 y147 w32 h58 ex55 ey112 i2 a6 n0.04 g2 $
* x815 y147 w18 h25 ex10 ey67 i2 a7 n0.04 g2 $
* x833 y147 w19 h53 ex8 ey100 i2 a8 n0.04 g2 $
* x852 y147 w26 h71 ex21 ey125 i2 a9 n0.04 g2 $
* x878 y147 w25 h73 ex24 ey131 i2 a10 n0.04 g2 $
* x903 y147 w57 h49 ex27 ey109 i2 a11 n0.04 g2 $
* x960 y147 w31 h22 ex45 ey61 i2 a12 n0.04 g2 $
* x0 y220 w61 h33 ex29 ey144 i2 a13 n0.04 g2 $
* x61 y220 w23 h60 ex33 ey156 i2 a14 n0.04 g2 $
* x84 y220 w41 h48 ex63 ey110 i2 a15 n0.04 g2 $
* x125 y220 w34 h40 ex51 ey88 i2 a0 n0.04 g2 $
$
* x159 y220 w9 h35 ex33 ey72 i2 a1 n0.04 g2 $
* x168 y220 w23 h35 ex57 ey79 i2 a2 n0.04 g2 $
* x191 y220 w19 h67 ex62 ey136 i2 a3 n0.04 g2 $
* x210 y220 w55 h54 ex51 ey151 i2 a4 n0.04 g2 $
* x265 y220 w10 h64 ex44 ey154 i2 a5 n0.04 g2 $
* x275 y220 w24 h47 ex2 ey100 i2 a6 n0.04 g2 $
* x299 y220 w37 h31 ex-23 ey78 i2 a7 n0.04 g2 $
* x336 y220 w20 h56 ex-21 ey109 i2 a8 n0.04 g2 $
* x356 y220 w28 h62 ex1 ey121 i2 a9 n0.04 g2 $
* x384 y220 w46 h62 ex22 ey123 i2 a10 n0.04 g2 $
* x430 y220 w18 h62 ex-9 ey120 i2 a11 n0.04 g2 $
* x448 y220 w57 h36 ex10 ey67 i2 a12 n0.04 g2 $
* x505 y220 w55 h54 ex51 ey151 i2 a13 n0.04 g2 $
* x560 y220 w27 h68 ex61 ey146 i2 a14 n0.04 g2 $
* x587 y220 w21 h38 ex59 ey86 i2 a15 n0.04 g2 $
* x608 y220 w9 h35 ex33 ey72 i2 a0 n0.04 g2 $
$
* x617 y220 w32 h41 ex22 ey68 i2 a1 n0.04 g2 $
* x649 y220 w20 h35 ex34 ey64 i2 a2 n0.04 g2 $
* x669 y220 w13 h67 ex52 ey116 i2 a3 n0.04 g2 $
* x682 y220 w21 h68 ex46 ey145 i2 a4 n0.04 g2 $
* x703 y220 w48 h55 ex54 ey135 i2 a5 n0.04 g2 $
* x751 y220 w17 h46 ex-40 ey112 i2 a6 n0.04 g2 $
* x768 y220 w52 h49 ex-34 ey108 i2 a7 n0.04 g-2 $
* x820 y220 w24 h64 ex-32 ey129 i2 a8 n0.04 g-2 $
* x844 y220 w18 h54 ex-19 ey125 i2 a9 n0.04 g-2 $
* x862 y220 w45 h44 ex8 ey117 i2 a10 n0.04 g-2 $
* x907 y220 w44 h55 ex-5 ey126 i2 a11 n0.04 g-2 $
* x951 y220 w52 h57 ex-30 ey93 i2 a12 n0.04 g2 $
* x1003 y220 w21 h68 ex46 ey145 i2 a13 n0.04 g2 $
* x0 y287 w15 h73 ex53 ey130 i2 a14 n0.04 g2 $
* x15 y287 w19 h38 ex37 ey70 i2 a15 n0.04 g2 $
* x34 y287 w32 h41 ex22 ey68 i2 a0 n0.04 g2 $
$
$


Donc tout d'abord les noms des images, les sons et enfin les différents états du personnage : attente, marche et frappe, chacun contenant 8 séquences pour les 8 directions et chaque * $ représentant une image.

Au niveau des paramètres : x et y pour la position dans l'image, w et h pour la hauteur, ex et ey pour le centre, permettant de situer le morceau d'image par rapport aux pieds du héros, i le numéro de l'image, a le numéro de la prochaine image dans l'animation, n le temps avant de passer à la prochaine image et enfin le plus important g : l'index-z de l'image.

Il permet de savoir dans quel ordre afficher les éléments du personnage : z0 étant le corps.
Ici, nous avons un bouclier, vous pouvez voir que suivant le moment de l'animation, il passe tout devant ou tout derrière.

En général, je réserve 1 pour le casque, -2 et 2 pour la main droite, -3 et 3 pour la main gauche.

Il existe d'autres paramètres, comme d0 et d1, me permettant de savoir quand le héros est censé pourvoir toucher un ennemi. Je ne les place que sur l'amure de corps, qui reste l'élément principal du héros.



J'utilise alors cette sympathique petite fonction pour calculer l'ordre d'affichage de mes éléments :

Les cheminModele[] étant les emplacements de tout à l'heure (0 pour le corps, 1 pour la tête, etc).

void Hero::CalculerOrdreAffichage()
{
    for (int i=0;i<NOMBRE_MORCEAU_PERSONNAGE;++i)
        m_ordreAffichage[i]=-1;
        
    for (int i=0;i<NOMBRE_MORCEAU_PERSONNAGE;++i)
    {
        if (m_cheminModele[i] != ""
        {
            int ordre = m_personnage.getOrdre(&m_modelePersonnage[i]);
            if (ordre!=-10)
                m_ordreAffichage[(int)(NOMBRE_MORCEAU_PERSONNAGE/2+ordre)]=i;
        }
    }
}


Ensuite, pour afficher, je boucle sur m_ordreAffichage et j'affiche l'élément qui lui a été attribué.


Génération du skin

Intéressons-nous à la génération des images des skins.

Il faut faire ABSOLUMENT attention à TOUJOURS garder les mêmes animations et la même position des caméras dans la création des éléments d'équipement. Sinon, tout est foutu.


Donc l'infographiste nous produit nos images, il les fait dans toutes les directions pour les différents états, en prenant garde de ne rendre que ce qu'il faut.
En général, la simple gestion du z-index suffit, mais il faut parfois re-découper légèrement les rendus.

Nous obtenons pleins d'images comme ceci :

http://img96.imageshack.us/img96/8936/croisecamailfrappe1epee.pnghttp://img25.imageshack.us/img25/5310/croisematelassefrappesa.pnghttp://img856.imageshack.us/img856/1967/katzbalgerfrappe0105.png

Une fois qu'on a nos centaines d'images, j'utilise un petit logiciel que j'ai conçus, le TilesetCreator 2.0, qui vient les stocker dans des tilesets, en essayant de découper le plus possible les parties vides qui prennent de la place pour rien.

Il me sort un fichier texte contenant donc déjà tous les paramètres x,y,w,h,ex,ey,a et je lui passe en plus i et n en paramètre.

Ce n'est pas magique ?

On obtient ce genre de choses :

http://holyspirit.fr/img/Contenu/Rendus/CroiseMailleMarche1Epee0.png

http://holyspirit.fr/img/Contenu/Rendus/EpeeLongueMarcheDroite0.png

J'ai aussi conçu un logiciel, l'EntityCopier, qui me permet de prendre un info.txt source et un cible, il vient directement me placer où il faut les infos du z-index sur le fichier cible (il vient aussi chercher d'autres infos, comme les d0 et d1 qui se trouve sur l'armure et me permettent de savoir quand le héros touche), je ne dois donc même plus le faire pour les nouveaux objets ! =D

Voici quelques exemples en jeu :
http://img854.imageshack.us/img854/6681/equipements.png

Ceci conclut cet article, si vous avez des questions, comme d'habitude, n'hésitez-pas, les com's sont faits pour ça.

Par Gregouar - 13/03/2011

Salut à tous !

Aujourd'hui je ne vais pas parler d'Holyspirit ou de quelque chose qui s'y rapporte directement, mais je vais vous parler de la technologie que je m'amuse à développer : un moteur de lumières gérant un éclairage 3D par pixel pour jeu 2D avec en bonus un zbuffer. Oui oui, grâce à ça vous pouvez afficher vos décors dans l'ordre que vous voulez, il s'arrangera[...]

Salut à tous !

Aujourd'hui je ne vais pas parler d'Holyspirit ou de quelque chose qui s'y rapporte directement, mais je vais vous parler de la technologie que je m'amuse à développer : un moteur de lumières gérant un éclairage 3D par pixel pour jeu 2D avec en bonus un zbuffer. Oui oui, grâce à ça vous pouvez afficher vos décors dans l'ordre que vous voulez, il s'arrangera toujours pour afficher en dessous ce qui doit l'être.

Mais attention, quand je dis 2D, je parle de 2D comme Holyspirit, c'est-à-dire que tous les personnages et décors sont modélisés en 3D mais que le jeu utilise des rendus pré-générés.
C'est important parce qu'il est extrêmement difficile de produire directement en dessin 2D les ressources nécessaires au fonctionnement du moteur.


Voici un petit aperçu de mes travaux actuels :

http://www.holyspirit.fr/Autres/pixel_lighting/resultat.png

Je vais sans doute partager ça en 3 articles pour commencer, d'autres viendront peut-être par après pour présenter mes avancées et corrections.
Dans un premier temps je vous expliquerai la gestion de l'éclairage, ensuite la gestion du zbuffer et enfin comment produire les ressources nécessaires avec 3DS max (et je sais qu'il y a moyen de le faire aussi en Blender, donc je vous expliquerai peut-être pour les deux logiciels).

C'est encore fortement expérimental, il y a sûrement moyen d'améliorer certaines choses afin de les rendre plus naturelles ou plus optimisées. N'hésitez pas à commenter avec vos idées.


C'est parti !

Tout d'abord, il faut savoir que nous allons utiliser 3 images pour chaque sprite, au lieu d'une comme nous faisions d'habitude. Oui c'est un peu lourd, mais il faut ce qu'il faut.

Nous allons nommer ces images :
  • color-map : cette image contient la couleur de chaque pixel du décor sans éclairage
    http://www.holyspirit.fr/Autres/pixel_lighting/abbaye_color.png
  • normal-map : cette image contient la direction de la normale de chaque pixel, j'expliquerai ça en détail plus loin, mais en gros ça permet de connaitre l'orientation de la surface représentée par le pixel
    http://www.holyspirit.fr/Autres/pixel_lighting/abbaye_normal.png
  • heightmap : cette image contient la hauteur de chaque pixel, elle permet donc de connaitre la coordonnée en z du pixel
    http://www.holyspirit.fr/Autres/pixel_lighting/abbaye_heightmap.png




Le système est très simple : quand on affiche un sprite, on affiche sa color-map en utilisant un shader, ce shader va éclairer plus ou moins fortement chaque pixel selon sa distance avec la lumière et son orientation par rapport à celle-ci.


Tout d'abord, on va trouver la position en 3D du pixel.

Pour ce faire, je conçois un petit vertex shader qui me permet de récupérer la position du pixel à l'écran et de la multiplier par deux en y, ainsi que d'ajouter la hauteur du sprite qui est envoyé comme attribut uniform.

Je pars du principe qu'on a une caméra pour rendu dimétrique, c'est-à-dire avec un angle de vue de 60°. Ca permet d'avoir deux pixels en x pour un pixel en y.
C'est pour ça que je dois multiplier ma valeur en y par deux pour trouver ma véritable valeur en y dans le "monde".


varying vec3 vertex;
uniform float z_pos;

void main()
{
	vertex = gl_Vertex.xyz;
	vertex.y *= 2.0;
	vertex.z = z_pos;
	
	gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
	gl_TexCoord[0] = gl_TextureMatrix[0] * gl_MultiTexCoord0;
	gl_FrontColor = gl_Color;
}



Mais ce n'est pas tout, je dois encore modifier cette valeur en y puisque la coordonnée en z modifie aussi la position du pixel.
Puisqu'on a un angle de 60 degrés, 1 en z diminue de racine carré de 3 (tan 60°) en y réel. Il nous faut donc modifier les y réels qu'on avait calculé avant directement à partir des y écrans.

En effet, regardez sur le shéma qui suit : deux points avec une même composante y réel mais avec une composante z qui change modifie leur position en y sur l'écran. Si vous chipotez un petit peu avec la trigonométrie, vous obtiendrez que les y réels sont modifié de tan(60°) des z réels.
J'utilise simplement la formule :
tan alpha = côté opposé / côté adjacent
avec
tan 60° = delta y/delta z


http://www.holyspirit.fr/Autres/pixel_lighting/shema.png

Dans mon fragment shader, je récupère donc la valeur calculée dans le vertex shader et je lui ajoute une certaine coordonnée en z définie par sa heightmap (je considère que au plus le pixel de la heightmap a une valeur bleue élevée, au plus je suis haut) multipliée par la hauteur de l'objet (passée en attribut uniform). Puis je retire le décalage du à la hauteur du pixel à sa coordonnée en y.

vec3 v = vertex;
v[2] += texture2D(heightmap, gl_TexCoord[0].xy)[2] * height_factor;
v[1] += sqrt(3) * v[2];


Maintenant qu'on a la position, on peut connaitre la distance avec la lumière ainsi que la direction du rayon lumineux.

vec3 light_direction = gl_LightSource[i].position.xyz - v;
float dist = length(light_direction);


Vous noterez que j'utilise les lumières par défaut d'OpenGL mais rien ne vous empêche de coder votre propre système, le tout étant de passer les attributs de la lumière au shader.


Les lumières d'OpenGL permettent de définir une atténuation constante, linéaire et quadratique, on peut donc en déduire comment calculer l'atténuation de la luminosité en fonction de la distance :

float attenuation = 1.0/( gl_LightSource[i].constantAttenuation +
				dist*gl_LightSource[i].linearAttenuation +
				dist*dist*gl_LightSource[i].quadraticAttenuation);



Il nous faut maintenant calculer l'orientation du pixel pour calculer son exposition à la lumière. Pour ce faire, on utilise la normal-map. Chacune des trois couleur RGB permet de connaitre la composante en x, y et z de la normale du pixel. Par convention, le rouge est utilisé pour les x, le vert pour les y et le bleu pour les z.

Pour ceux qui ne le savent pas, la normale est le vecteur perpendiculaire au plan tangent de la surface au point, c'est le vecteur qui pointe vers l'extérieur. Elle permet donc de connaitre l'orientation de la surface.


Il est dès lors aisé de calculer la normale :

vec3 direction;
direction[0] = -1.0 + 2.0 * texture2D(normal, gl_TexCoord[0].xy)[0];
direction[1] = -1.0 + 2.0 * texture2D(normal, gl_TexCoord[0].xy)[1];
direction[2] = -1.0 + 2.0 * texture2D(normal, gl_TexCoord[0].xy)[2];


On fait " -1.0 + 2.0 * " car la couleur donne une orientation de -1 à 1 avec 0 qui représente -1, 0.5 représente 0 et 1 représente 1.

Hélàs la technique que j'utilise pour générer la normal-map me donne l'orientation en fonction de la position de la caméra, donc avec un décalage d'angle de 60°. Il me faut dès lors modifier un petit peu mes vecteurs avec une rotation de 60° :

vec3 temp = direction;
direction[1] = temp[1]*0.5 + temp[2]*0.5*sqrt(3);
direction[2] = temp[2]*0.5 - temp[1]*0.5*sqrt(3);


On y est presque, il ne nous reste plus qu'une chose à régler : comment allons-nous éclairer le pixel en fonction de la direction ?
Facile ! Avec un simple petit produit scalaire !

Le produit scalaire usuel nous donne un nombre plus au moins grand suivant si les vecteurs multipliés entre eux sont plus ou moins alignés. Ainsi, si nos vecteurs sont fortement alignés, c'est que la surface est orientée vers la lumière et donc on doit fortement éclairer.

Le produit scalaire est obtenu en additionnant le produit de chacune des composantes des deux vecteurs :
((x,y,z) | (a,b,c)) = xa + yb + zc


On obtient donc que l'éclairage du pixel est donné par :

lighting = max(0.0, dot(direction,normalize(light_direction))) * attenuation;


Je normalise le vecteur direction car suivant la distance avec la lumière, il est plus ou moins grand. Or ici on veut juste sa direction.
Vous remarquerez aussi le max qui me permet d'éviter d'avoir un éclairage négatif, ce qui n'aurait aucun sens.

Je peux donc maintenant calculer la valeur finale de mon pixel :

vec4 color =  gl_Color * texture2D(diffuse, gl_TexCoord[0].xy) * gl_LightSource[i].diffuse  * lighting;
color[3] = gl_Color[3] * texture2D(diffuse, gl_TexCoord[0].xy)[3];
gl_FragColor =  color;


Si vous voulez gérer plusieurs lumières à la fois, il vous suffit de boucler sur les différentes lumières et de faire :

vec4 color = gl_Color * texture2D(diffuse, gl_TexCoord[0].xy) * ambient_light;

// On boucle sur les lumières
color +=  gl_Color * texture2D(diffuse, gl_TexCoord[0].xy) * gl_LightSource[i].diffuse  * lighting;
// Fin de la boucle

color[3] = gl_Color[3] * texture2D(diffuse, gl_TexCoord[0].xy)[3];
gl_FragColor =  color;


Vous pouvez remarquer qu'ici j'ai ajouté en plus une lumière ambiante permettant de donner un éclairage de base aux pixels.


Vous pouvez aussi vous amuser à concevoir des lumières directionnelles, c'est-à-dire des lumières qui n'ont pas de position mais qui éclairent toute la scène suivant une certaine direction. On définit alors que les attributs de position de la lumière sont la direction vers laquelle elle pointe.
On obtient directement la luminosité par :

vec3 light_direction = -gl_LightSource[i].position.xyz;
				light_direction.y *= 2.0;
				lighting = max(0.0, dot(direction,normalize(light_direction)));


Si vous voulez tester le résultat (avec zbuffer en prime) vous pouvez télécharger ma démo ici : ShadersTest_demo.rar

A la prochaine pour la création du zbuffer !

Par Gregouar - 05/03/2012

Alpha Arts
1

Comme promis, voici la suite de mon article sur mon moteur d'éclairage 2.5D (si vous n'avez pas lu la partie une, c'est par ici).

On va voir aujourd'hui pour développer un zbuffer permettant de définir l'ordre d'affichage des pixels de nos sprites 3D afin de pouvoir les afficher dans l'ordre qu'on veut et de les faire s'intersecter en 3D.



On va[...]

Comme promis, voici la suite de mon article sur mon moteur d'éclairage 2.5D (si vous n'avez pas lu la partie une, c'est par ici).

On va voir aujourd'hui pour développer un zbuffer permettant de définir l'ordre d'affichage des pixels de nos sprites 3D afin de pouvoir les afficher dans l'ordre qu'on veut et de les faire s'intersecter en 3D.

http://www.holyspirit.fr/Autres/pixel_lighting/demo_zbuffer_2.png

On va utiliser la heightmap du sprite 3D pour connaitre la hauteur du pixel. Un pixel plus haut sera forcément plus proche de la caméra qu'un plus bas. Si vous ne me croyez pas, regardez ce shéma qui devrait vous en convaincre :

http://www.holyspirit.fr/Autres/pixel_lighting/shema2.jpg
P1 et P2 représentent le même pixel à l'écran, mais P2 est plus haut et donc plus proche de la caméra. Dès lors c'est P2 qui sera affiché et non P1.

Il nous suffira donc d'afficher le pixel uniquement si sa heightmap est plus clair que celui précédent. Pour ce faire, on travaillera avec deux écrans de rendus, l'un principal que l'utilisateur verra et l'autre permettant de stocker la hauteur des pixels dessinés à l'écran pour le moment, ainsi que leur niveau d'opacité (j'y reviendrai plus loin, c'est pour la gestion du blending et de l’anticrénelage).

Pour mettre tout ça en place, j'ai codé deux shaders, un qui est notre normal shader du précédent article auquel je rajoute une gestion de la profondeur. En gros je n'affiche le pixel que si sa heightmap est plus clair que l'actuel affiché à l'écran. L'autre shader permet de mettre à jour la heighmap, je dessine donc la heightmap de mon sprite avec ce shader sur l'écran virtuel contenant la heightmap actuelle de l'écran.

J'utilise uniquement la composante bleue des pixels pour stocker la hauteur de ceux-ci.


Voici le code de mon shader de heightmap :

uniform sampler2D diffuse;
uniform sampler2D heightmap;
uniform sampler2D heightmap_screen;
uniform vec2 screen_ratio;
uniform float height_factor;
uniform float z_pos;

void main()
{			
	gl_FragColor =  gl_Color * texture2D(heightmap, gl_TexCoord[0].xy);
	gl_FragColor[3] = 1.0;
	
	if( texture2D(heightmap, gl_TexCoord[0].xy)[3] > 0.2)
		gl_FragColor[2] /= texture2D(heightmap, gl_TexCoord[0].xy)[3];
	
	gl_FragColor[2] *=  height_factor/500.0;
	
	gl_FragColor[2] += z_pos/500.0;
	
	gl_FragColor[0] = texture2D(diffuse, gl_TexCoord[0].xy)[3];
	
	vec4 screen_color = texture2D(heightmap_screen, gl_FragCoord.xy*screen_ratio);
	
	if(gl_FragColor[2] <= screen_color[2])
	{
		gl_FragColor[2] = screen_color[2];
		gl_FragColor[0] = screen_color[0]
	}
}



Les trois textures passées en paramètre sont :
  • Diffuse : la color-map du sprite, permettant de récupérer le niveau d'opacité du pixel pour la gestion du blending.
  • Heightmap : la heightmap du sprite, permettant donc de récupérer la hauteur des pixels du sprite.
  • Heightmap-screen : la hauteur des pixels affichés à l'écran actuellement.


Je passe en outre comme paramètre screen_ratio, permettant d'inverser la normalisation de la position de mes pixels à l'écran afin de récupérer la couleur du pixel correspondant à l'écran via un : gl_FragCoord.xy*screen_ratio.

Enfin, les deux derniers paramètres me permettent de définir la position en z du sprite 3D (décalant vers le bleu plus clair la couleur des pixels de la heightmap) et son facteur de hauteur. Comme dans l'article précédent.

Au niveau du code, je mets à jour la composante bleu et rouge de l'écran uniquement si le nouveau pixel est plus bleu et donc plus haut. La composante rouge me permet de stocker l'opacité du pixel afin de gérer le blending par après dans le shader qui affiche le sprite à l'écran.

Remarquez le : "if( texture2D(heightmap, gl_TexCoord[0].xy)[3] > 0.2)
gl_FragColor[2] /= texture2D(heightmap, gl_TexCoord[0].xy)[3];"
C'est juste une petite correction du au fait que les logiciels 3D, quand ils font un rendu avec anticrénelage, affichent des pixels plus sombres en plus de plus transparents sur les côtés. Dès lors on divise par la transparence du pixel pour ré-obtenir la couleur d'origine et donc la hauteur d'origine. Mais on ne fait cela que pour des pixels avec un minimum de transparence (ici 1/5).


Attaquons nous justement à ce shader-ci maintenant !

Premier changement, pour définir la hauteur du pixel :
v.z += texture2D(heightmap, gl_TexCoord[0].xy)[2] * height_factor;

est devenu
if( texture2D(heightmap, gl_TexCoord[0].xy)[3] > 0.2)
	v.z += texture2D(heightmap, gl_TexCoord[0].xy)[2] * height_factor  / texture2D(heightmap, gl_TexCoord[0].xy)[3];
else
	v.z += texture2D(heightmap, gl_TexCoord[0].xy)[2] * height_factor;


C'est exactement comme on a fait juste au dessus.

Ensuite, si jamais le pixel du sprite est en dessous du pixel déjà affiché, on récupère l'opacité du pixel déjà affiché :
if(v.z/500.0 < texture2D(heightmap_screen, vec2(gl_FragCoord.xy*screen_ratio)).b)
	alpha_old = texture2D(heightmap_screen, vec2(gl_FragCoord.xy*screen_ratio)).r;


Avant de faire quoi que ce soit, on ajoute juste une petite condition : si l'opacité du pixel précédent est de 1, ça sert à rien d'afficher quoi que ce soit et des faire des calculs, donc on retourne un pixel transparent.

Ensuite on a tous nos calcul pour connaitre l'illumination et la couleur du pixel qu'on stocke dans un vec4 color.

Sauf que maintenant, avant de la retourner dans gl_FragColor, on ajoute une petite étape :
if(alpha_old != 0.0)
{
	color *= a * min(1.0,max(0.0,1.0 - alpha_old));
	color += texture2D(screen, vec2(gl_FragCoord*screen_ratio)) * alpha_old;
	color /= alpha_old + a * min(1.0,max(0.0,1.0 - alpha_old));
	color.a = alpha_old + a * min(1.0,max(0.0,1.0 - alpha_old));
}
else
	color.a = gl_Color.a * texture2D(diffuse, gl_TexCoord[0].xy).a;


Si l'opacité du pixel précédent est plus grande que 0 et s'il est au dessus (s'il était en dessous on aurait considéré qu'il avait une opacité de 0), alors on doit mixer les deux pixels, sinon on l'affiche avec sa transparence normale.
Pour mixer les pixels, on utilise la formule de mixage habituelle, celle de l'algorithme du peintre (plus d'infos ici) :
color_final = (color_a * alpha_a + color_b * alpha_b / (1 - alpha_a))/(alpha_a + alpha_b / (1 - alpha_a))

Où a est le pixel du dessus et b le pixel du dessous.

On sait que l'écran actuelle (écran des heightmap passé en paramètre sous le nom screen) contient déjà color_a et color contient color_b , on doit donc juste multiplié color par alpha_b * (1-alpha_a) et lui ajouter le pixel actuel de l'écran * alpha_old. On divise après le tout par l'alpha total afin d'obtenir le pixel final.

Et voilà ! On a notre z-buffer !

Revoici un aperçu de la bête (cliquez pour agrandir) :

http://www.holyspirit.fr/Autres/pixel_lighting/demo_zbuffer_2.png

Où on affiche d'abord l'arbre, puis une abbaye, puis l'herbe, puis l'autre abbaye. Chacune des abbaye est, bien évidemment, un seul sprite 3D.
Vous pouvez aussi remarquer les ombres qui feront l'objet d'un prochain billet.

C'est toujours la même adresse pour tester : ShadersTest_demo.rar

Par Gregouar - 08/04/2012

J'ai mis un peu de temps, mais voici enfin l'article sur ma gestion des ombres projetées en 2.5D. Accrochez-vous, c'est parti ! 8)

Re-voici un petit aperçu du rendu qu'on veut obtenir :



Pour le moment, je n'ai trouvé de solution "efficace" que pour faire des ombres en fonction d'une lumière directionnelle et dirigée pas trop à[...]

J'ai mis un peu de temps, mais voici enfin l'article sur ma gestion des ombres projetées en 2.5D. Accrochez-vous, c'est parti !

Re-voici un petit aperçu du rendu qu'on veut obtenir :

http://www.holyspirit.fr/Autres/pixel_lighting/demo_zbuffer_2.png

Pour le moment, je n'ai trouvé de solution "efficace" que pour faire des ombres en fonction d'une lumière directionnelle et dirigée pas trop à l'horizontale.

L'idée est de projeter les voxels du sprite 3D sur une image plane, représentant le sol, dans la direction de la lumière. On enregistre la hauteur du voxel qui a été projeté le plus haut (oui, on va réutiliser notre z-buffer pour ça). A la fin, au moment du rendu des sprites 3D eux-même, on saura si le pixel doit être éclairé par la lumière ou non suivant si, une fois projeté au sol, il est plus haut ou non que le voxel le plus haut projeté.
En effet, regardez ce shéma :

http://www.holyspirit.fr/Autres/pixel_lighting/shema_ombres.jpg

Sur la projection au sol, on retiendra une ombre de hauteur du point P2, donc le point P1 sera ombré et pas le point P2.

On ajoute en outre un petit facteur d'adoucissement pour un rendu plus agréable à l’œil.

Mais tout cela donne des ombres approximatives (les voxels ne remplissent pas toujours une forme sans trou au sol, on perd de l'information, etc). On applique donc un shader de flou sur l'image des ombres projetées et on peut aussi ajouter un petit anti-crénelage au moment du rendu en regardant la projection des voxels voisins et en faisant une moyenne pondérée.

Afin d'optimiser la chose, je calcul à l'avance la projection de chaque modèle de sprite 3D et il suffit après de just la dessiner avec le z-buffer sur l'écran de rendu.

Puisque la projection des ombres peut sortir de l'écran, il faut créer un écran de rendu des ombres légèrement plus grand que l'écran réel.


Maintenant que vous voyez l'idée, on va regarder le code un peu plus en détails.

Ombres locales

On commence par la méthode de Sprite3D qui permet de calculer la projection de l'ombre au sol en local (donc juste pour le sprite).

    void GenerateAmbientShadow(sf::RenderTarget *a, Shader_pack *s, sf::Vector3f light)
    {
        const int W = heightmap.getSize().x  + (int)((height+m_3Dpos.z) * fabs(light.x/light.z) + 1);
        const int H = heightmap.getSize().y + (int)((height+m_3Dpos.z) * fabs(light.y/2.0/light.z) + 1)
                                            + (int)((height+m_3Dpos.z)*sqrt(3)/2.0 + 1);

        const int X = std::min(0.f, - light.x * (height+m_3Dpos.z) / light.z);
        const int Y = std::min(0.f, - light.y * (height+m_3Dpos.z) / light.z);

        sf::Uint8 *shadow_map_pix = new sf::Uint8[W*H*4];

        const sf::Uint8* local_heightmap = img_heightmap.getPixelsPtr();

        for(unsigned int x = 0 ; x < heightmap.getSize().x ; x ++)
        for(unsigned int y = 0 ; y < heightmap.getSize().y ; y ++)
        {
            unsigned int x_p = x;
            if(getScale().x < 0)
                x_p = heightmap.getSize().x - x - 1;

            float c = local_heightmap[y*heightmap.getSize().x*4+x_p*4+2];

            if(local_heightmap[y*heightmap.getSize().x*4+x_p*4+3] > 192)
            {
                c *= 255/(float)local_heightmap[y*heightmap.getSize().x*4+x_p*4+3];

                float h = height * (float)c/255  + m_3Dpos.z;

                sf::Vector2f p(x-X,y-Y);

                p.y += h*sqrt(3)/2;

                p.x -= light.x * h / light.z;
                p.y -= light.y * h / 2.f / light.z;

                p.x = (int)p.x;
                p.y = (int)p.y;

                if(p.x >= 0 && p.y >= 0 && p.x < W && p.y < H)
                {
                    unsigned int n = p.y*W*4+p.x*4;
                    shadow_map_pix[n] = 0;
                    shadow_map_pix[n+1] = 0;
                    if(c > shadow_map_pix[n+2])
                        shadow_map_pix[n+2] = c;
                    shadow_map_pix[n+3] = 255;
                }

            }
        }
        shadow_map_img.create(W,H);
        shadow_map_img.update(shadow_map_pix);
        shadow_map.setTexture(shadow_map_img);
        shadow_map.setPosition(sf::Vector2f(X,Y));

        delete[] shadow_map_pix;
    }


La première étape :

        const int W = heightmap.getSize().x  + (int)((height+m_3Dpos.z) * fabs(light.x/light.z) + 1);
        const int H = heightmap.getSize().y + (int)((height+m_3Dpos.z) * fabs(light.y/2.0/light.z) + 1)
                                            + (int)((height+m_3Dpos.z)*sqrt(3)/2.0 + 1);

        const int X = std::min(0.f, - light.x * (height+m_3Dpos.z) / light.z);
        const int Y = std::min(0.f, - light.y * (height+m_3Dpos.z) / light.z);


Consiste simplement en le calcul de la largeur, de la hauteur et du décalage des coordonnées de la texture qui sera créée pour contenir la projection de l'ombre. On considère donc le pire des cas, en augmentant les dimensions de la texture de base avec la projection du voxel le plus haut possible dans la direction de la lumière.
On calcul en plus le décalage qui permettra de savoir où dessiner l'ombre par rapport au sprite 3D en fonction de sa nouvelle largeur et hauteur.

 
        sf::Uint8 *shadow_map_pix = new sf::Uint8[W*H*4];

        const sf::Uint8* local_heightmap = img_heightmap.getPixelsPtr();


Je crée un tableau de pixels qui contiendra les valeurs d'ombrage. Dans les faits, on enregistrera juste dans la composante rouge la hauteur du voxel projeté mais le fait d'avoir un tableau de pixels permet de créer rapidement une sf::Texture à la fin.
Je récupère en outre la liste des pixels de la heightmap du sprite 3D.

        for(unsigned int x = 0 ; x < heightmap.getSize().x ; x ++)
        for(unsigned int y = 0 ; y < heightmap.getSize().y ; y ++)
        {
            unsigned int x_p = x;
            if(getScale().x < 0)
                x_p = heightmap.getSize().x - x - 1;

            float c = local_heightmap[y*heightmap.getSize().x*4+x_p*4+2];
            [...]
        }


On boucle sur la heightmap du sprite 3D afin de récupérer la hauteur de chaque voxel.
On vérifie si le sprite 3D a été retourné en x, si oui on doit parcourir la heightmap dans le sens contraire pour la largeur.
On récupère et on stocke dans c la couleur représentant la hauteur du voxel considéré.

            if(local_heightmap[y*heightmap.getSize().x*4+x_p*4+3] > 192)
            {
                c *= 255/(float)local_heightmap[y*heightmap.getSize().x*4+x_p*4+3];

                float h = height * (float)c/255  + m_3Dpos.z;

                sf::Vector2f p(x-X,y-Y);

                p.y += h*sqrt(3)/2;

                p.x -= light.x * h / light.z;
                p.y -= light.y * h / 2.f / light.z;

                p.x = (int)p.x;
                p.y = (int)p.y;

                [...]
            }


On vérifie d'abord si le voxel possède suffisant d'opacité, sinon il est considéré comme inexistant car il sert juste à l'anticrénelage du décor.
Ensuite on compense la hauteur du voxel qui peut être trompée de par sa semi-opacité dans la heightmap (si le pixel de la heightmap est en partie transparent à cause de l'anti-crénelage, il perd en plus en luminosité ; or on s'intéresse ici à la couleur originelle du pixel).
Et on calcul la véritable hauteur du voxel en fonction des paramètres du sprite 3D.
Après, on décale sa position en y qui est trompée par sa position en z comme je l'avais déjà expliqué dans mon précédent article.
Enfin, on projette le voxel sur le sol dans la direction de la lumière et on arrondi car un pixel a une position entière.
Pour la projection, j'utilise simplement la conservation des rapports entre les côtés de triangles semblables.

                if(p.x >= 0 && p.y >= 0 && p.x < W && p.y < H)
                {
                    unsigned int n = p.y*W*4+p.x*4;
                    shadow_map_pix[n] = 0;
                    shadow_map_pix[n+1] = 0;
                    if(c > shadow_map_pix[n+2])
                        shadow_map_pix[n+2] = c;
                    shadow_map_pix[n+3] = 255;
                }


On s'assure que le voxel n'es pas projeté en dehors de l'image de l'ombre (ça ne devrait pas arriver, mais sait-on jamais).
Après, on calcule la position du pixel dans le tableau de l'image de l'ombre en fonction de la position de la projection et si le nouveau voxel projeté est plus haut que le précédent, on met à jour la couleur et donc la hauteur de la projection.

        shadow_map_img.create(W,H);
        shadow_map_img.update(shadow_map_pix);
        shadow_map.setTexture(shadow_map_img);
        shadow_map.setPosition(sf::Vector2f(X,Y));

        delete[] shadow_map_pix;


Pour terminer, on crée la texture, on la remplit avec notre tableau de pixels et on met à jour le sprite qui l'affichera ; sans oublier de libérer la mémoire.

Ombres globales

Le calcul des ombres de façon globale se fait après aisément, il suffit de faire appel à une méthode comme suit pour chacun des sprites 3D affichés avant de faire les autres rendus :

    void drawAmbientShadow(sf::RenderTarget *a, Shader_pack *s)
    {
        sf::RenderTexture &t = s->shadow_screen;
        sf::View v = a->getView();
        v.setSize(t.getSize().x,t.getSize().y);
        t.setView(v);

        s->heightmap_shader.setParameter("height_factor", height*fabs(getScale().y));
        s->heightmap_shader.setParameter("z_pos", m_3Dpos.z);
        s->heightmap_shader.setParameter("heightmap_screen", t.getTexture());
        s->heightmap_shader.setParameter("screen_ratio", sf::Vector2f(1.f/t.getSize().x, 1.f/t.getSize().y));

        sf::Sprite temp = shadow_map;
        temp.move(getPosition());
        t.draw(temp,&s->heightmap_shader);
        t.display();

        s->heightmap_shader.setParameter("heightmap_screen", s->heightmap_screen.getTexture());
        s->heightmap_shader.setParameter("screen_ratio", sf::Vector2f(1.f/a->getSize().x, 1.f/a->getSize().y));
    }


En gros, on affiche notre projection d'ombres sur l'écran des ombres avec un shader de z-buffer pour s'assurer d'avoir à chaque fois les voxels les plus hauts qui ombrent.

Une fois qu'on a affiché comme ça toutes nos ombres, on peut passer un petit shader de blur (voir mon autre billet ici pour plus d'explications).
Ça nous donne quelque chose comme :

    void drawAmbientShadow()
    {
         sf::View old_cam = screen.getView();
         screen.setView(screen.getDefaultView());
         shadow_screen.setView(shadow_screen.getDefaultView());

         blur_shader.setParameter("direction_x", 1.0);
         shadow_screen.draw(sf::Sprite (shadow_screen.getTexture()), &blur_shader);
         shadow_screen.display();
         blur_shader.setParameter("direction_x", 0);
         shadow_screen.draw(sf::Sprite (shadow_screen.getTexture()), &blur_shader);
         shadow_screen.display();

         screen.setView(old_cam);
    }


Shader d'éclairage

Dans le code C++, il n'y a pas de changement pour le rendu des sprites 3D si ce n'est que maintenant on passe en plus en paramètre au shader d'éclairage l'écran des ombres et son ratio.

Par contre, il y a du neuf dans le shader d'éclairage qui ressemble maintenant à ça :

uniform sampler2D diffuse;
uniform sampler2D normal;
uniform sampler2D heightmap;
uniform sampler2D heightmap_screen;
uniform sampler2D screen;
uniform sampler2D shadow_map;

uniform float NBR_LIGHTS;
uniform vec4 ambient_light;
uniform vec2 screen_ratio;
uniform vec2 shadow_ratio;
uniform vec2 image_ratio;

uniform float height_factor;
uniform float flipx;

varying vec3 vertex;

float GetShadow(vec2 decal, vec3 light_direction)
{
	float h = (vertex.z+texture2D(heightmap, gl_TexCoord[0].xy + decal*image_ratio).b * height_factor);
	vec2 pos_screen = gl_FragCoord.xy + decal;
	pos_screen.x -= h*light_direction.x/light_direction.z;
	pos_screen.y += h*light_direction.y/4.0/light_direction.z;
	pos_screen.y -= h*sqrt(3.0)/2.0;
	float shadow_color = texture2D(shadow_map, pos_screen*shadow_ratio + vec2(0.5,0.5) - shadow_ratio/screen_ratio/2.0).b;
				  
	return 1.0 - min(1.0,max(0.0, 1.0 -(h/500.0 - shadow_color+0.025)/0.025));
}

void main()
{	
	float alpha_old = 0.0;
	
	vec3 v = vertex;
	
	if( texture2D(heightmap, gl_TexCoord[0].xy)[3] > 0.2)
		v.z += texture2D(heightmap, gl_TexCoord[0].xy)[2] * height_factor  / texture2D(heightmap, gl_TexCoord[0].xy)[3];
	else
		v.z += texture2D(heightmap, gl_TexCoord[0].xy)[2] * height_factor;
	
	v.y += sqrt(3.0) * v.z;
	
	if(v.z/500.0 < texture2D(heightmap_screen, vec2(gl_FragCoord.xy*screen_ratio)).b)
		alpha_old = texture2D(heightmap_screen, vec2(gl_FragCoord.xy*screen_ratio)).r;
	
	if(alpha_old < 1.0)
	{
		vec3 direction = -1.0 + 2.0 * texture2D(normal, gl_TexCoord[0].xy).rgb;
		direction.x *= flipx;
		
		direction.yz = vec2(direction.y*0.5 + direction.z*0.5*sqrt(3.0),
							direction.z*0.5 - direction.y*0.5*sqrt(3.0));
		
		vec4 color = gl_Color * texture2D(diffuse, gl_TexCoord[0].xy) * ambient_light;
		
		int i;
		for(i = 0 ; i < int(NBR_LIGHTS) ; i = i+1)
		{
			float lighting = 0.0;
			
			if(gl_LightSource[i].position.w == 0.0)
			{		
				vec3 light_direction = -gl_LightSource[i].position.xyz;
				light_direction.y *= 2.0;
				lighting = max(0.0, dot(direction,normalize(light_direction)));
				
				float f = 1.0;
				
				lighting *= (GetShadow(vec2( 0, 0)*f,light_direction) * 4.0 +
							 GetShadow(vec2( 1, 0)*f,light_direction) * 2.0 +
							 GetShadow(vec2(-1, 0)*f,light_direction) * 2.0 +
							 GetShadow(vec2( 0, 1)*f,light_direction) * 2.0 +
							 GetShadow(vec2( 0,-1)*f,light_direction) * 2.0 +
							 GetShadow(vec2( 1, 1)*f,light_direction) * 1.0 +
							 GetShadow(vec2(-1, 1)*f,light_direction) * 1.0 +
							 GetShadow(vec2(-1,-1)*f,light_direction) * 1.0 +
							 GetShadow(vec2( 1,-1)*f,light_direction) * 1.0 )/16.0;
			}
			else
			{
				vec3 light_direction = gl_LightSource[i].position.xyz - v;
				float dist = length(light_direction);
				float attenuation = 1.0/( gl_LightSource[i].constantAttenuation +
										  dist*gl_LightSource[i].linearAttenuation +
										  dist*dist*gl_LightSource[i].quadraticAttenuation);
				
				
				lighting = max(0.0, dot(direction,normalize(light_direction))) * attenuation;
			}
			
			lighting *= gl_LightSource[i].diffuse.a;
			
			color.rgb +=  gl_Color.rgb * texture2D(diffuse, gl_TexCoord[0].xy).rgb * gl_LightSource[i].diffuse.rgb  * lighting;
		}
		
		float a = gl_Color.a * texture2D(diffuse, gl_TexCoord[0].xy).a;
			
		if(alpha_old != 0.0)
		{
			color *= a * min(1.0,max(0.0,1.0 - alpha_old));
			color += texture2D(screen, vec2(gl_FragCoord.xy*screen_ratio)) * alpha_old;
			color /= alpha_old + a * min(1.0,max(0.0,1.0 - alpha_old));
			color.a = alpha_old + a * min(1.0,max(0.0,1.0 - alpha_old));
		}
		else
			color.a = gl_Color.a * texture2D(diffuse, gl_TexCoord[0].xy).a;
		
		gl_FragColor = color;
	}
	else
		gl_FragColor = vec4(0.0,0.0,0.0,0.0);
}



Alors quoi de neuf ?
Pas grand chose si ce n'est l'apparition de la fonction float GetShadow(vec2 decal, vec3 light_direction) et :

float f = 1.0;

lighting *= (GetShadow(vec2( 0, 0)*f,light_direction) * 4.0 +
             GetShadow(vec2( 1, 0)*f,light_direction) * 2.0 +
             GetShadow(vec2(-1, 0)*f,light_direction) * 2.0 +
             GetShadow(vec2( 0, 1)*f,light_direction) * 2.0 +
             GetShadow(vec2( 0,-1)*f,light_direction) * 2.0 +
             GetShadow(vec2( 1, 1)*f,light_direction) * 1.0 +
             GetShadow(vec2(-1, 1)*f,light_direction) * 1.0 +
             GetShadow(vec2(-1,-1)*f,light_direction) * 1.0 +
             GetShadow(vec2( 1,-1)*f,light_direction) * 1.0 )/16.0;


En fait, GetShadow me permet de récupérer le taux d'éclairage du pixel en fonction de l'écran des ombres et je l'applique plusieurs fois en faisant une moyenne pondérée pour donner un effet de flou/anticrénelage.
Le f est juste là pour facilement manipuler ce taux de flou.

L'anticrénelage donne beaucoup plus beau, mais consomme énormément. Vous pouvez le diminuer en le réduisant à 5 points voir même le retirer.


Intéressons-nous à cette fonction GetShadow :

float GetShadow(vec2 decal, vec3 light_direction)
{
	float h = (vertex.z+texture2D(heightmap, gl_TexCoord[0].xy + decal*image_ratio).b * height_factor);
	vec2 pos_screen = gl_FragCoord.xy + decal;
	pos_screen.x -= h*light_direction.x/light_direction.z;
	pos_screen.y += h*light_direction.y/4.0/light_direction.z;
	pos_screen.y -= h*sqrt(3.0)/2.0;
	float shadow_color = texture2D(shadow_map, pos_screen*shadow_ratio + vec2(0.5,0.5) - shadow_ratio/screen_ratio/2.0).b;
				  
	return 1.0 - min(1.0,max(0.0, 1.0 -(h/500.0 - shadow_color+0.025)/0.025));
}


Je récupère la hauteur du voxel considéré ou du voxel voisin s'il y a décalage, je le projette sur le sol comme j'ai fait dans la génération de l'ombre et je récupère la valeur d'ombrage à cette position.
Ensuite j'assombris ou non suivant le voxel considéré s'il est plus haut ou non que l'ombre avec un petit adoucissement linéaire.

Et voilà, c'est tout !
Je rapelle le lien pour tester : ShadersTest_demo.rar

Comme d'hab, n'hésitez pas à poser vos questions dans les commentaires.

Par Gregouar - 12/05/2012