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

Alpha-Arts » Blog » Tips

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

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



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

[ 1 2 3 ] Suivante »