I. Introduction

Notre objectif est de réaliser un moteur de lumières dynamiques en dimension 2.
Il nous faudra donc d'abord trouver un moyen de représenter une lumière et son impact sur l'environnement, et trouver ensuite un moyen de l'empêcher de traverser les murs.

Nous allons le concevoir dans un premier temps pour un monde purement en dimension 2, je vous expliquerai ensuite comment, moi, je l'ai adapté pour un jeu en 2D dimétrique (souvent appelé isométrique), donnant une impression de profondeur/hauteur.

Je précise qu'il n'est pas forcément optimisé et toujours à 100% efficace, n'hésitez pas à me faire part de vos remarques.
Mon moteur est conçu avec la SFMLSFML, mais il est aisément adaptable à OpenGL.

Pour comprendre l'intégralité de cet article, il faut avoir des petites notions de mathématiques. Mais rassurez-vous, rien de bien méchant, il vous suffit juste de comprendre l'équation d'une droite de type y = ax + b et de savoir résoudre des simples systèmes d'équations à deux inconnues. Il faut aussi savoir ce qu'est le sinus et le cosinus d'un angle.

II. Représentation d'une lumière

Une lumière est définie par une position (abscisse et ordonnée), un rayon (nous ne ferons dans un premier temps que des lumières de type omnidirectionnelles, nous verrons plus tard pour faire des lumières directionnelles) et une couleur.

Une lampe omnidirectionnelle est une source de lumière qui projette de la lumière dans toutes les directions à la fois.

Une lumière est donc un point qui projette une couleur dans toutes les directions sous forme de cercle, avec un dégradé jusqu'au noir en dehors du rayon.

La meilleure solution que j'ai trouvée est de découper cette lumière en triangles. Chacun de ces triangles possède un sommet en commun : le centre de la lumière.
Gérer des triangles est très simple et est une fonctionnalité de base d'OpenGL.

Nous pouvons donc encore ajouter une option aux lumières : leur "qualité", c'est-à-dire le nombre de triangles qui les composent à la base.

Voici un exemple de lumière de couleur verte, dont les triangles sont contourés juste pour que vous puissiez visionner le fonctionnement du moteur :

Image non disponible


Pour représenter l'impact de ces lumières, j'ai décidé de partir sur un système d'écran virtuel qui servira de tampon. Cet écran permettra de générer une image qui pourra modifier les pixels à l'écran, pour donner une impression d'éclairage.

Nous allons donc afficher ces triangles en mode de rendu additif, c'est-à-dire que la couleur des pixels est ajoutée à la couleur des pixels déjà rendu à l'écran. Je dessine tout cela sur un fond de la couleur de la lumière ambiante voulue. Ainsi, les lumières s'additionnent entre elles, permettant des fondus.


Image non disponible

Ce rendu final est ensuite copié en mémoire pour être finalement redessiné une fois les rendus du jeu faits, en mode de rendu multiplicatif. Cela signifie que la couleur des pixels de l'écran est multipliée par la couleur des pixels de la texture rendue. En prenant compte, bien évidemment, que la couleur des pixels est définie par une valeur de 0 à 1 pour le rouge, le vert et le bleu. (Et non pas de 0 à 255)

Voici une image d'exemple pour bien comprendre la différence entre les différents modes de rendu :


Image non disponible

Tout à gauche, nous avons notre image de fond, ensuite vient en deuxième lieu l'image que nous allons dessiner.
En troisième position, notre image est rendue en mode multiplicatif, la couleur des pixels de notre image de fond est multipliée par la couleur des pixels de notre image rendue.
En quatrième position, nous avons notre image qui est rendue en mode additif, la couleur de ses pixels est ajoutée aux pixels déjà existants.


Tout cela pour permettre d'assombrir tout le rendu, sauf où il y a des lumières !
Et il suffit de changer la couleur de fond pour avoir des ambiance bien sympa. Prenons par exemple une couleur orangée pour donner un effet de lever de soleil, ou un bleu foncé pour la nuit.

Image non disponible

Ne faites pas attention aux lignes plus claires, ce sont toujours les contours de nos triangles.

Mes lumières sont gérées via un gestionnaire de lumières que j'ai conçu. Il permet d'en ajouter, d'en supprimer, de les modifier, de les déplacer, etc. C'est aussi lui qui s'occupe de faire les rendus.
Je vous conseille de coder le vôtre afin qu'il soit adapté aux technologies que vous utilisez, mais sinon, vous pouvez avoir le mien grâce au code d'exemple donné dans la partie Liens et conclusion.


Par exemple, ajouter une lumière donne ceci :

 
Sélectionnez

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

Les paramètres de la méthode sont la position, l'intensité, le rayon, la qualité et la couleur.

Remarquez que je précise Dynamic, en effet, mon gestionnaire gère les lumières dynamiques et statiques.
La seule différence vient du fait que les lumières dynamiques sont générées à chaque tour de boucle, tandis que les lumières statiques ne le sont que lors de leur création, ceci afin d'éviter de consommer des performances pour des lumières qui ne sont jamais modifiées.
Par exemple, un joueur qui lance une boule de feu va créer une lumière dynamique, en effet, celle-ci se déplace.
Tandis qu'une torche sur un mur ne bouge pas, sa lumière peut donc être statique et donc son rendu calculé uniquement au chargement de la carte.

Je peux accéder à mes lampes en faisant par exemple :

 
Sélectionnez

Manager->SetRadius(light,128 ); 

Le moteur de lumières possède une méthode Generate() qui permet de générer toutes les lumières et de les rendre à l'écran. Elle va donc boucler sur l'ensemble des lumières contenues dans le moteur et faire appel à leur propre méthode Generate(). Le code de génération d'une lumière :

 
Sélectionnez

void Light::Generate()
{
    m_shape.clear();

    float buf=(M_PI*2)/(float)m_quality;

    for(int i=0;i < m_quality;++i)
    {
        AddTriangle(sf::Vector2f((float)((float)m_radius*cos((float)i*buf))
                                ,(float)((float)m_radius*sin((float)i*buf))) ,
                    sf::Vector2f((float)((float)m_radius*cos((float)(i+1)*buf))
                                ,(float)((float)m_radius*sin((float)(i+1)*buf))));
    }
}
				

m_shape est un tableau dynamique de polygones. C'est une fonction par défaut de la SFML, vous pouvez facilement créer une classe ou une structure utilisant des GLTriangles pour faire cela.

En gros, on divise 2pi (la circonférence d'un cercle de rayon 1) par la qualité, ainsi on obtient l'angle entre les deux côtés du triangle adjacents au centre de la lumière. Ceci nous permet de projeter ensuite les deux points extrémités de chaque triangle qui compose la lampe, grâce à la trigonométrie.
Ces points sont passés en paramètres à la fonction AddTriangle() qui va s'occuper d'ajouter le triangle à m_shape.

 
Sélectionnez

void Light::AddTriangle(sf::Vector2f pt1,sf::Vector2f pt2)
{
    float intensity;
    
    m_shape.push_back(sf::Shape ());

    m_shape.back().AddPoint(0, 0,  sf::Color((int)(m_intensity*m_color.r/255),
                                             (int)(m_intensity*m_color.g/255),
                                             (int)(m_intensity*m_color.b/255)),sf::Color(255,255,255));

    intensity = m_intensity-sqrt(pt1.x*pt1.x + pt1.y*pt1.y)*m_intensity/m_radius;
    m_shape.back().AddPoint(pt1.x, pt1.y,  sf::Color((int)(intensity*m_color.r/255),
                                                     (int)(intensity*m_color.g/255),
                                                     (int)(intensity*m_color.b/255)),sf::Color(255,255,255));

    intensity = m_intensity-sqrt(pt2.x*pt2.x + pt2.y*pt2.y)*m_intensity/m_radius;
    m_shape.back().AddPoint(pt2.x, pt2.y,  sf::Color((int)(intensity*m_color.r/255),
                                                     (int)(intensity*m_color.g/255),
                                                     (int)(intensity*m_color.b/255)),sf::Color(255,255,255));

    m_shape.back().SetBlendMode(sf::Blend::Add);
    m_shape.back().SetPosition(m_position);
}
				

Je ne sais pas si OpenGL gère par défaut les modes de rendus (Add et Multiply), mais je penses que oui, étant donné que la SFML le fait nativement, via SetBlendMode().

J'ajoute les trois points qui composent le triangle à mon m_shape, avec leur couleur respective.
Je calcule déjà leur intensité en fonction de leurs distances avec le centre, vous verrez pourquoi je fais ça dans la suite du tutoriel.

III. Gestion des murs

Maintenant, nous allons voir comment gérer des murs. Ces murs sont des segments de droites que la lumière ne peut pas traverser.
Par soucis de lisibilité, je vais représenter graphiquement ces murs par des lignes blanches (devenues grises à cause de la lumière ambiante).
Voici les murs que nous allons utiliser pour nos tests :


Image non disponible

Mon gestionnaire de lumières s'occupe aussi de gérer mes murs.

Un mur est juste composé de ses deux points extrémités. La liste des murs est envoyée à la méthode AddTriangle() de nos lumières.
Il nous suffit maintenant de calculer les intersections entre les murs et les triangles qui composent la lumière, afin de "couper" celle-ci et l'empêcher de traverser ceux-ci.

Il existe trois façons dont le mur peut couper un triangle (si l'on ne prend pas en compte des exceptions où des points se confondent).

Premièrement, le mur peut couper de part en part le triangle, comme ceci :

Image non disponible

Les lignes noires représentent le triangle, la ligne bleue le mur et les points rouges les intersections.
La partie grisée est la partie du triangle qui disparaît.



Cette situation est la plus simple, il nous suffit de déplacer les deux sommets extrémités du triangle afin de les ramener au niveau des intersections entre les côtés adjacents et le mur.

Je nommerai à l'avenir ces deux côtés qui partent du centre côtés adjacents.
Le côté du triangle le plus éloigné du centre sera nommé côté extrémité.


Pour trouver ces deux points, je calcule les équations cartésiennes des quatre droites sur lesquelles sont alignés nos trois côtés du triangle et notre mur. Je calcule l'intersection entre le mur et chacun des trois côtés grâce à cette fonction :

 
Sélectionnez

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

    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;

    return i;
}
				

Mes deux droites sont représentées sous forme d'équations y = ax + b et y = cx + d.
Je calcule a et c, le coefficient de la pente, en prenant le rapport entre la hauteur et la largeur qui séparent les deux points.
Je calcule ensuite b et d en manipulant l'équation :
y = ax + b => b = y - ax

Je calcule mon point d'intersection grâce à ce tout petit système d'équations :
y = ax + b
y = cx + d

ax + b = cx + d
ax - cx = d - b
x = (d - b)/(a - c)

y = ax + b (x étant calculé juste au-dessus)

Et voilà, nous avons les coordonnées en abscisse et ordonnée de notre point d'intersection !

Ce code n'est pas "sécurisé", il faut maintenant rajouter la gestion des erreurs.
Les habitués auront tout de suite remarqué les divisions par 0.

 
Sélectionnez

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

    if((p2.x - p1.x) == 0 && (q2.x - q1.x) == 0)
        i.x = 0, i.y = 0;
    else if((p2.x - p1.x) == 0)
    {
        i.x = p1.x;

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

        i.y = c * i.x + d;
    }
    else if((q2.x - q1.x) == 0)
    {
        i.x = q1.x;

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

        i.y = a * i.x + b;
    }
    else
    {
        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;
    }

    return i;
}

Nous calculerons toujours ces trois points d'intersection.
C'est en vérifiant s'ils appartiennent aux segments de droites que nous saurons dans quel cas de coupe nous sommes.
Ici, avec le mur qui coupe le triangle de part en part, les deux points d'intersection des côtés adjacents appartiendront aux deux segments.

Si le point d'intersection n'appartient pas au mur, c'est que celui-ci ne coupe pas ce triangle de la lumière.


Pour vérifier cela, j'utilise ce code :

 
Sélectionnez

sf::Vector2f Collision(sf::Vector2f p1, sf::Vector2f p2, sf::Vector2f q1, sf::Vector2f q2)
{
    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);
}

Dans un cas parfait, il nous faudrait juste vérifier en x, hélas il y a parfois des décimales qui se perdent, je prends donc aussi une marge d'erreur de 0.1.

Cette fonction retourne 0,0 si il n'y a pas de point d'intersection entre les deux segments de droites.

Notre méthode AddTriangle() donne donc ceci :

 
Sélectionnez

void Light::AddTriangle(sf::Vector2f pt1,sf::Vector2f pt2,std::vector <Wall> &m_wall)
{
    for(std::vector<Wall>::iterator IterWall=m_wall.begin()+minimum_wall ; IterWall!=m_wall.end() ; ++IterWall)
    {
        // l1 et l2 sont les positions relatives au centre de la lumière des deux extrémités du mur
        sf::Vector2f l1(IterWall->pt1.x-m_position.x, IterWall->pt1.y-m_position.y);
        sf::Vector2f l2(IterWall->pt2.x-m_position.x, IterWall->pt2.y-m_position.y);
        
        //Point d'intersection entre le mur et le côté qui part du centre jusqu'à l'extrémité 1, c'est donc un des deux côtés adjacents.
        sf::Vector2f m = Collision(l1, l2, sf::Vector2f(0,0), pt1);
        //Idem que juste au dessus, mais avec l'autre extrémité.
        sf::Vector2f n = Collision(l1, l2, sf::Vector2f(0,0), pt2);
        //Segment qui relie les deux extrémités, c'est le côté extrémité.
        sf::Vector2f o = Collision(l1, l2, pt1, pt2);
        
        //Vérification que les deux points d'intesections appartiennent bien aux deux côtés adjacents du triangle.
        if((m.x != 0 || m.y != 0) && (n.x != 0 || n.y != 0))
            pt1 = m, pt2 = n;
    }
    
    // METHODE DE RENDU LA MÊME QUE PLUS HAUT
    ...
    // FIN DE LA METHODE DE RENDU
}

Dans un jeu complexe, il est conseillé de partitionner l'espace (ou le plutôt le plan, dans notre cas) afin d'éviter de calculer les intersections murs/lumières si les deux objets sont trop éloignés.

Maintenant, attaquons-nous au deuxième cas de figure :

Image non disponible
Le mur coupe un des côtés adjacents et le côté extrémité.

Dans ce cas, il nous faut découper le triangle en deux, donc modifier le triangle en cours et en ajouter un autre (en vert).
La méthode AddTriangle() devient donc une méthode inclusive.

D'un point de vue programmation, il suffit de vérifier que l'un de nos deux points d'intersection n'appartient pas à un côté adjacent. Sinon, on regarde s'il y a une intersection entre le côté extrémité et le mur.
On ajoute alors un triangle et on déplace l'extrémité du triangle actuel pour la ramener à l'intersection entre le mur et le côté extrémité. N'hésitez pas à regarder à nouveau le schéma pour comprendre.

 
Sélectionnez

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;
}

Enfin, troisième et dernière possibilité :

Image non disponible
L'extrémité du mur arrive dans le triangle.

Si cela arrive, nous divisons le triangle en deux.
Pour vérifier cela, il suffit de regarder d'abord si l'une des deux extrémités est plus proche du centre que la taille du rayon.
Et de regarder ensuite si le point d'intersection entre la droite projetée depuis le centre jusqu'à l'extrémité du mur et le côté extrémité du triangle se trouve entre les deux côtés adjacents du triangle.

On coupe le triangle au niveau du point d'intersection entre la droite projetée et le côté extrémité.
Nous nous retrouvons ensuite dans le premier cas pour l'un des deux triangles qui sera alors coupé de part en part par le mur.

Il faut donc impérativement tester ce premier cas avant les autres.

 
Sélectionnez

void Light::AddTriangle(sf::Vector2f pt1,sf::Vector2f pt2, int minimum_wall,std::vector <Wall> &m_wall)
{
    int w = minimum_wall;
    
    for(std::vector<Wall>::iterator IterWall=m_wall.begin()+minimum_wall;IterWall!=m_wall.end();++IterWall,++w)
    {
        // l1 et l2 sont les positions relatives au centre de la lumière des deux extrémités du mur
        sf::Vector2f l1(IterWall->pt1.x-m_position.x, IterWall->pt1.y-m_position.y);
        sf::Vector2f l2(IterWall->pt2.x-m_position.x, IterWall->pt2.y-m_position.y);

        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 != i && pt2 != i)
            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;
        }
        if(l2.x * l2.x + l2.y * l2.y < m_radius * m_radius)
        {
            sf::Vector2f i = Intersect(pt1,pt2,sf::Vector2f (0,0),l2);

            if (pt1 != i && pt2 != i)
            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((l2.y >= 0 && i.y >= 0) || (l2.y <= 0 && i.y <= 0))
                if((l2.x >= 0 && i.x >= 0) || (l2.x <= 0 && i.x <= 0))
                AddTriangle(pt1, i, w, m_wall), pt1 = i;
        }

        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);

        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;
        }
    }

    float intensity;

    m_shape.push_back(sf::Shape ());

    m_shape.back().AddPoint(0, 0,  sf::Color((int)(m_intensity*m_color.r/255),
                                             (int)(m_intensity*m_color.g/255),
                                             (int)(m_intensity*m_color.b/255)),sf::Color(255,255,255));

    intensity=m_intensity-sqrt(pt1.x*pt1.x + pt1.y*pt1.y)*m_intensity/m_radius;
    m_shape.back().AddPoint(pt1.x, pt1.y,  sf::Color((int)(intensity*m_color.r/255),
                                                     (int)(intensity*m_color.g/255),
                                                     (int)(intensity*m_color.b/255)),sf::Color(255,255,255));

    intensity=m_intensity-sqrt(pt2.x*pt2.x + pt2.y*pt2.y)*m_intensity/m_radius;
    m_shape.back().AddPoint(pt2.x, pt2.y,  sf::Color((int)(intensity*m_color.r/255),
                                                     (int)(intensity*m_color.g/255),
                                                     (int)(intensity*m_color.b/255)),sf::Color(255,255,255));

    m_shape.back().SetBlendMode(sf::Blend::Add);
    m_shape.back().SetPosition(m_position);
}

Remarquez l'ajout d'un paramètre minimum_wall, celui-ci permet de revenir où l'on était arrivé dans la liste des murs lors de l'ajout d'un triangle supplémentaire. En effet, on peut considérer que le nouveau triangle a déjà été vérifié avec les mêmes murs que l'ancien triangle qui le contenait.

Et voici ce que cela donne au final :

Image non disponible

C'est-y pas magnifique ?

Pour ma part, j'ajoute un filtre de flou sur le rendu des lumières, je trouve cela plus esthétique.

Ce filtre flou est en fait un shader appliqué lors du rendu final en mode multiplicatif.
Voici le code de ce shader :

 
Sélectionnez

uniform sampler2D texture;
uniform float offset;

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

	vec4 pixel = texture2D(texture, gl_TexCoord[0].xy)               * 1 +
	             texture2D(texture, gl_TexCoord[0].xy - offx)        * 2 +
	             texture2D(texture, gl_TexCoord[0].xy + offx)        * 2 +
	             texture2D(texture, gl_TexCoord[0].xy - offy)        * 2 +
	             texture2D(texture, gl_TexCoord[0].xy + offy)        * 2 +
	             texture2D(texture, gl_TexCoord[0].xy - offx - offy) * 1 +
	             texture2D(texture, gl_TexCoord[0].xy - offx + offy) * 1 +
	             texture2D(texture, gl_TexCoord[0].xy + offx - offy) * 1 +
	             texture2D(texture, gl_TexCoord[0].xy + offx + offy) * 1;

	gl_FragColor =  gl_Color * (pixel / 13.0);
}


Image non disponible


IV. Les lumières directionnelles

Les lumières directionnelles, au contraire des lumières de type omnidirectionnel, ne projettent de la lumière que dans une seule direction.
Elles seront donc composées par un seul triangle.

Nos Directional_light héritent de Light. On a juste besoin de leur rajouter deux paramètres : leur angle et leur angle d'ouverture.
Nous devons aussi surcharger la méthode Generate(), comme ceci, afin de ne plus générer qu'un seul triangle :

 
Sélectionnez

void Directional_light::Generate(std::vector<Wall> &m_wall)
{
    m_shape.clear();

    float angle     = m_angle * M_PI / 180;
    float o_angle   = m_opening_angle * M_PI / 180;

    AddTriangle(sf::Vector2f((m_radius*cos(angle + o_angle * 0.5))
                            ,(m_radius*sin(angle + o_angle * 0.5))) ,
                sf::Vector2f((m_radius*cos(angle - o_angle * 0.5))
                            ,(m_radius*sin(angle - o_angle * 0.5))),0,m_wall);
}

Mes angles sont en degrés, je dois donc les convertir en gradients pour pouvoir les utiliser avec les fonctions sinus et cosinus.

J'ai dû aussi faire pas mal de modifications dans mon gestionnaire de lumières, comme passer les Light en pointeur afin de profiter du polymorphisme dans mes tableaux dynamiques.

Voici un exemple pour ajouter une lumière directionnelle :

 
Sélectionnez

directional_light = Manager->Add_Dynamic_Directional_Light(sf::Vector2f(750,310),255,384,180,45,sf::Color(255,128,255));

Les paramètres sont la position du projecteur, l'intensité, la taille du triangle, l'angle, l'angle d'ouverture et la couleur.

Ça ne demande pas vraiment plus de travail que ça d'un point de vue conception, le tout étant de créer le gestionnaire.

Image non disponible


Et juste pour le plaisir, un petit gif animé du résultat final :


Image non disponible

V. Annexe : intégration à Holyspirit

Maintenant que notre moteur de lumières est créé, il faut l'implanter dans le jeu.
Je ne vais pas expliquer ici comment je gère ça dans mon code, ni comment c'est intégré par rapport aux ressources, mais je vais surtout expliquer comment l'utiliser pour le rendre compatible avec une méthode de rendu en 2D dimétrique (souvent appelé 2D isométrique car ça y ressemble fort)

La première grosse différence vient de la profondeur. En effet, nous n'avons plus une vue du sol d'en haut, mais légèrement de côté. Étant donné que nous sommes en dimétrique, nous savons que l'effet de profondeur est créé par des tiles écrasés de manière à avoir deux fois leur hauteur dans leur longueur.
Il nous suffit donc "d'écraser" aussi nos lumières, en divisant leurs coordonnées de rendu par deux.
Nous obtenons ceci :

Image non disponible


La deuxième différence vient de la gestion de la hauteur des murs.
Je dois donc ajouter une hauteur à mes murs et projeter la lumière dessus.

La première étape consiste à vérifier que la lumière est devant le mur.

 
Sélectionnez

bool devant = false;
if ( 0>=(pt1.y) - (pt1.x)*(((pt1.y)-(pt2.y))/((pt1.x)-(pt2.x))))
   devant = true;

Il me suffit de vérifier si l'ordonnée à l'origine (b) est plus petite que 0 dans l'équation de ma droite y = ax + b.
Soit b = y - ax
y étant la coordonnée en y d'un de mes points, x la coordonnée en x d'un de mes points et je retrouve a en prenant le rapport de la différence en y sur la différence en x de mes deux points.
L'ordonnée à l'origine doit être négative car l'axe des y va vers le bas dans la SFML.

Maintenant que nous savons si le mur est devant, il nous suffit de rajouter un parallélépipède à m_shape.

 
Sélectionnez

if (devant)
    if (intensity>1||intensity2>1)
    {
        m_shape.push_back(sf::Shape ());

        m_shape.back().AddPoint(pt1.x,pt1.y/2,  sf::Color((int)(intensity*m_color.r/255),(int)(intensity*m_color.g/255),(int)(intensity*m_color.b/255)));
        m_shape.back().AddPoint(pt1.x,pt1.y/2-96 * sin(intensity /m_intensity*M_PI_2),  sf::Color(0,0,0));
        m_shape.back().AddPoint(pt2.x,pt2.y/2-96 * sin(intensity2/m_intensity*M_PI_2),  sf::Color(0,0,0));
        m_shape.back().AddPoint(pt2.x,pt2.y/2,  sf::Color((int)(intensity2*m_color.r/255),(int)(intensity2*m_color.g/255),(int)(intensity2*m_color.b/255)));

        m_shape.back().SetBlendMode(sf::Blend::Add);

        m_shape.back().SetPosition(m_position.x,m_position.y/2);
    }

Remarquez que je projette la hauteur avec un sinus, cela me permet de donner une forme de demi-ellipse à ma projection.

J'applique ensuite mon rendu par dessus tout le jeu. Cela donne parfois des incohérences, comme juste le dessus d'un personnage qui est éclairé au lieu de tout son corps, mais cela est plus performant que de calculer deux rendus (un pour le sol et un pour les entités et murs).

Et voici le résultat final :

Image non disponible


Si vous voulez le voir en mouvement, je vous invite à regarder cette vidéo :



VI. Liens et conclusion

J'espère que cet article vous aura été utile, et, au risque de me répéter, n'hésitez pas à me faire part de vos remarques !
Si un jour vous concevez un jeu ou autre avec mon moteur, ou en vous inspirant de celui-ci, n'hésitez pas à m'envoyer un mail, ça me fera plaisir de voir que j'ai pu être utile.

Et maintenant, voici quelques liens utiles :
Voir l'article original sur mon blog
Télécharger le programme d'exemple
Télécharger Holyspirit (Pour ceux qui veulent tester pour voir ce que cela donne dans un jeu.)

Et un tout tout grand merci à NarcissX et à jacques_jean pour leur relecture.