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.
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 :
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) :
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