
Big Tuto SFML 2 : Rabidja v. 3.0
Annexe 2 : Ajoutons des lumières !
Tutoriel présenté par : Skrool
Relecture et corrections : Jérémie F. Bellanger (Jay81)
Date d'écriture : 29 mars 2015
Date de révision : 20 mars 2016
Introduction
Dans le chapitre précédent, nous avons créé notre gestionnaire de lumières, ajouté des ombres et des ambiances lumineuses, et nous les avons implantées dans notre jeu grâce aux tiles.
Maintenant, nous allons (enfin) créer nos lumières ! ![]()
Accrochez-vous, car le niveau monte d'un cran ! ![]()
Théorie
De quoi allons-nous avoir besoin ? ![]()
Tout d'abord, nous allons avoir besoin d'une nouvelle class lightEntity. Dans celle-ci, nous allons stocker les informations telles que le rayon de la lumière, sa couleur, sa position, etc...
Nous allons aussi stocker sa forme. Mais la question est : quelle forme a une lumière ? ![]()
Vous avez probablement pensé à un cercle pour représenter une lumière, dont l'opacité au centre serait plus grande que celle à ses côtés.
Bien que techniquement la SFML gère les cercles, et que ce soit possible, nous allons voir que ce n'est pas pratique du tout ! ![]()
En effet, un cercle ne nous permettra ni de gérer les collisions mur / lumière, ni de créer des lumières directionnelles (des lumières qui, contrairement aux lumières omnidirectionnelles, n'éclairent pas tout autour d'elles. Un projecteur, si vous voulez
).
Nous allons donc utiliser... des triangles ! ![]()
Des triangles, mais pour quoi faire ? ![]()
Nous allons créer plusieurs triangles partant du centre de la lumière pour donner l'impression que la lumière est un cercle et qu'elle est exponentielle. ![]()
Demonstration :

Comme vous pouvez le voir sur l'image 1, nos lumières ne seront pas réellement rondes, mais plus "polygonales". Ce n'est cependant pas grave, puisque cela ne se verra pas (image 2). De plus, ces lumières se subdiviseront en de plus petits triangles quand nous aborderons la question des murs et des collisions. ![]()
Sur l'image 3, nous pouvons voir une lumière directionnelle, il s'agit en fait du même système que les lumières omnidirectionnelles, sauf que les triangles ne se bouclent pas sur eux mêmes.
Enfin, sur la quatrième image, nous pouvons voir de manière simplifiée comment la lumière réagira face à un mur : les triangles se redimensionneront pour "longer le mur". ![]()
Vous avez assimilé la théorie ?
Parce qu'on va entrer dans le vif du sujet : j'ai bien sûr nommé notre class lightEntity !
Ajoutons notre class lightEntity !
Et on est parti ! Tout d'abord, nous allons créer une nouvelle class lightEntity avec les fichiers lightEntity.cpp et lightEntity.h.
Voilà ce à quoi va maintenant ressembler notre header :
Fichier : Créer un nouveau fichier lightEntity.h et y copier :
|
//Rabidja 3 - nouvelle version convertie en SFML 2
//Copyright / Droits d'auteur du tuto : www.meruvia.fr - Jérémie F. Bellanger
//Copyright / Droits d'auteur du fichier : Skrool, 2015.
//Nous allons avoir besoin d'inclure ces bibliothèques
#include <SFML/Graphics.hpp>
#include <iostream>
#include <math.h>
//Ainsi que la class Map
class Map;
class lightEntity // La lumière en elle-même
{
public:
// Constructeur et destructeur
lightEntity();
~lightEntity();
// Mutateurs
void SetIntensity(float);
void SetRadius(float);
void SetQuality(int);
void SetColor(sf::Color);
void SetPosition(sf::Vector2f);
void SetAbsolutePosition(sf::Vector2f);
// Accesseurs
float GetIntensity() const;
float GetRadius() const;
int GetQuality() const;
sf::Color GetColor() const;
sf::Vector2f getPosition(void) const;
sf::Vector2f getAbsolutePosition(void) const;
int getSourceX(void) const;
int getSourceY(void) const;
int getSourceID(void) const;
//Fonctions
//Affichage
void Draw(sf::RenderTarget &window) const;
//Fonction pour générer les triangles
void Generate(Map &map);
//Fonction pour ajouter un triangle (sera plus tard couplée avec une autre fonction)
void AddTriangle(sf::Vector2f pt1, sf::Vector2f pt2, Map &map);
//Création d'une lumière omnidirectionnelle
void create(int quality, sf::Color color, float radius, float intensity,
sf::Vector2f position, int type, Map &map, int sourceX, int sourceY);
//Création d'une lumière directionnelle
void createDirectionalLight(sf::Color color, float radius, float intensity,
sf::Vector2f position, int type, float angle, float o_angle, Map &map,
int sourceX, int sourceY);
//Suppression des triangles de subdivision pour nettoyer la class
void clear(void);
//Mise à jour de la lumière (on gèrera des animations ainsi que des déplacement et
//plein d'autres petites choses ici
void update(Map &map);
//Modification de la position de chacun des points de chaque triangle de la lumière
void move(sf::Vector2f mouvement);
private:
//Intensité de la lumière
float m_intensity;
//Position de la lumière dans la map
int m_absoluteX, m_absoluteY;
// Tableau dynamique de triangles de subdivision
std::vector <sf::VertexArray> m_triangle;
//Position à l'écran
sf::Vector2f m_position;
//Couleur
sf::Color m_color;
//Qualité : nombre de triangle de subdivision de base (lumière omnidirectionnelle seulement)
int m_quality;
//Rayon
float m_radius;
//Booleen pour savoir si la lumière est directionnelle
bool m_directionnal;
//Angle et angle d'ouverture pour les lumières directionnelles
float m_angle, m_opening_angle;
//Le type de la lumière, pour créer des animations dans la fonction update
int m_type;
//Timer interne pour gérer certaines lumières dans la fonction update
int m_timer;
//Variable pour gérer l'apparition de la lumière de manière plus esthétique
float m_targetIntensity;
//Tile source, pour replacer la tile de lumière à sa place quand elle sortira de l'écran pour économiser du CPU
int m_sourceX, m_sourceY, m_sourceID;
//Constante
const float M_PI = 3.14;
};
|
Fichier : Créer un nouveau fichier lightEntity.cpp et y copier :
|
//Rabidja 3 - nouvelle version convertie en SFML 2
//Copyright / Droits d'auteur du tuto : www.meruvia.fr - Jérémie F. Bellanger
//Copyright / Droits d'auteur du fichier : Skrool, 2015.
#include "lightEntity.h"
#include "map.h"
/**********************************************************************************/
/********************************* lightEntity **********************************/
/**********************************************************************************/
lightEntity::lightEntity()
{
}
lightEntity::~lightEntity()
{
m_triangle.clear();
}
void lightEntity::SetIntensity(float value){ m_intensity = value; }
void lightEntity::SetRadius(float value){ m_radius = value; }
void lightEntity::SetQuality(int value){ m_quality = value; }
void lightEntity::SetColor(sf::Color color){ m_color = color; }
void lightEntity::SetPosition(sf::Vector2f value){ m_position = value; }
float lightEntity::GetIntensity() const { return (m_intensity); }
float lightEntity::GetRadius() const { return (m_radius); }
int lightEntity::GetQuality() const { return (m_quality); }
sf::Color lightEntity::GetColor() const { return (m_color); }
sf::Vector2f lightEntity::getPosition(void) const { return (m_position); }
sf::Vector2f lightEntity::getAbsolutePosition(void) const { return
(sf::Vector2f(m_absoluteX, m_absoluteY)); }
int lightEntity::getSourceX(void) const { return m_sourceX; }
int lightEntity::getSourceY(void) const { return m_sourceY; }
int lightEntity::getSourceID(void) const { return m_sourceID; }
void lightEntity::clear(void){ m_triangle.clear(); }
void lightEntity::move(sf::Vector2f mouvement)
{
//Cette fonction déplace tous les points d'une lumière, triangle par triangle
m_position += mouvement;//On modifie la position du centre de la lumière
//Pour chaque triangle de la lumière nous ajoutons aux trois points le vecteur de mouvement
for (unsigned int i = 0; i < m_triangle.size(); i++)
{
//Nous utilisons ici un opérateur pour ajouter le vecteur de mouvement aux points
m_triangle[i].operator[](0).position += mouvement;
m_triangle[i].operator[](1).position += mouvement;
m_triangle[i].operator[](2).position += mouvement;
}
}
void lightEntity::create(int quality, sf::Color color, float radius, float intensity,
sf::Vector2f position, int type, Map &map, int sourceX, int sourceY)
{
//Initialisation de variables
m_quality = quality;
m_color = color;
m_radius = radius;
m_intensity = intensity;
m_absoluteX = (int)position.x;
m_absoluteY = (int)position.y;
m_position = position; // Par défaut. Sera modifié au prochain update
m_type = type;
//Récupération des données de tiles
m_sourceX = sourceX;
m_sourceY = sourceY;
m_sourceID = map.getTile(sourceY, sourceX);
m_directionnal = false;
Generate(map);
}
void lightEntity::createDirectionalLight(sf::Color color, float radius, float intensity,
sf::Vector2f position, int type, float angle, float o_angle, Map &map, int sourceX, int sourceY)
{
//Initialisation de variables
m_color = color;
m_radius = radius;
m_intensity = intensity;
m_type = type;
m_angle = angle;
m_opening_angle = o_angle;
m_absoluteX = (int)position.x;
m_absoluteY = (int)position.y;
m_position = position; // Par défaut. Sera modifié au prochain update
//Récupération des données de tiles
m_sourceX = sourceX;
m_sourceY = sourceY;
m_sourceID = map.getTile(sourceY, sourceX);
m_directionnal = true;
Generate(map);
}
void lightEntity::Draw(sf::RenderTarget &window) const
{
// On boucle sur m_triangle pour afficher tous les triangles si la lumière
//n'est pas hors de l'écran.
if (m_position.x > 0 - m_radius && m_position.x < 800 + m_radius
&& m_position.y > 0 - m_radius && m_position.y < 600 + m_radius)
{
for (int i = 0; i < (int)m_triangle.size(); i++)
{
window.draw(m_triangle[i], sf::BlendAdd);
}
}
}
void lightEntity::update(Map &map)
{
//Pour l'instant vide, mais plus tard, on y ajoutera des animations
Generate(map);
}
|
Fichier : lightEntity.cpp : Rajouter :
|
void lightEntity::Generate(Map &map)
{
//On libère tout les triangles pour les recalculer ensuite
m_triangle.clear();
//Si la lumière n'est pas directionnelle, on calcule la taille des triangles
//par rapport à sa qualité et Pi
if (!m_directionnal)
{
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))), map);
}
}
else
{
//Tant que l'angle d'ouverture est > 360, on lui retire 360° pour qu'il corresponde
//à un angle normal
while (m_opening_angle > 360)
m_opening_angle -= 360;
//Si la lumière est directionnelle et que l'angle est inférieur à 180°, on crée
//un triangle de la taille de la lumière, ce qui donne l'effet "spot"
if (m_opening_angle < 179)
{
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))), map);
}
//Si l'angle est supérieur à 180°, on divise le triangle en plusieurs triangles
//de 30° max, pour donner un effet rond à la lumière
else if (m_opening_angle < 361)
{
int sub = m_opening_angle / 30;
float angle = m_angle * M_PI / 180;
float o_angle = (m_opening_angle / sub) * M_PI / 180;
for (int i = 0; i < sub; i++)
{
AddTriangle(sf::Vector2f((m_radius*cos(angle + (i*o_angle) + o_angle * 0.5))
, (m_radius*sin(angle + (i*o_angle) + o_angle * 0.5))),
sf::Vector2f((m_radius*cos(angle + (i*o_angle) - o_angle * 0.5))
, (m_radius*sin(angle + (i*o_angle) - o_angle * 0.5))), map);
}
}
}
}
|
Dans le premier cas, on détermine l'angle de chaque sous-triangle grâce au calcul : buf = (M_PI * 2) / (float)m_quality;
Fichier : lightEntity.cpp : Rajouter :
|
// Ajout d'un triangle
void lightEntity::AddTriangle(sf::Vector2f pt1, sf::Vector2f pt2, Map &map)
{
// Variable qui contiendra l'intensité calculée, pour le dégradé de lumière
float intensity;
// On ajoute une shape
m_triangle.push_back(sf::VertexArray());
m_triangle.back().setPrimitiveType(sf::Triangles);
// On lui donne comme point de départ (0,0), le centre de la lumière, avec
//la couleur et intensité maximales
m_triangle.back().append(sf::Vertex(m_position,
sf::Color((int)(m_intensity*m_color.r / 255),
(int)(m_intensity*m_color.g / 255),
(int)(m_intensity*m_color.b / 255))));
// On calcul où l'on se trouve par rapport au centre, pour savoir à quelle intensité on est
intensity = m_intensity - sqrt(pt1.x*pt1.x + pt1.y*pt1.y)*m_intensity / m_radius;
// Et on ajoute un point au triangle
m_triangle.back().append(sf::Vertex(m_position + pt1,
sf::Color((int)(intensity*m_color.r / 255),
(int)(intensity*m_color.g / 255),
(int)(intensity*m_color.b / 255))));
// Pareil que précédemment
intensity = m_intensity - sqrt(pt2.x*pt2.x + pt2.y*pt2.y)*m_intensity / m_radius;
m_triangle.back().append(sf::Vertex(m_position + pt2,
sf::Color((int)(intensity*m_color.r / 255),
(int)(intensity*m_color.g / 255),
(int)(intensity*m_color.b / 255))));
}
|
Note : le contenu de cette fonction sera plus tard déplacé dans une sous fonction du nom de AddShape(), quand on gérera les collisions, pour éviter des calculs interminables. ![]()
Ici, on utilise m_triangle.push_back(sf::VertexArray()); pour ajouter une case à notre tableau dynamique, comprenant les triangles. Du coup, on ajoute un triangle. ![]()
Avec m_triangle.back().setPrimitiveType(sf::Triangles); on indique que la dernière case contient un triangle (il faut tout lui dire à cet ordinateur
).
Ensuite, on place les trois points du triangle en prenant en compte l'intensité pour générer une couleur. Souvenez-vous, nous allons appliquer sur les lumières en elles-même un mode de rendu "add", ce qui nous permet de mélanger la couleur au noir. Puisque le noir ne sera pas affiché, les cotés paraitront transparents, et il y aura un dégradé par rapport au centre. ![]()
Mais pourquoi avoir mis map en paramètre alors qu'on ne l'utilise pas ? ![]()
Pour l'instant, on ne l'utilise pas, mais on l'utilisera quand on devra gérer les collisions, et plutôt que de changer toutes les déclarations de fonctions plus tard, je préfère mettre map en paramètre tout de suite
.
Affichons notre première lumière !
S’il y a une chose qu'il ne faut pas oublier de faire, c'est de mettre notre gestionnaire de lumière (dans la classe Light
) à jour. Allons-y, donc :
Fichier : light.h : Remplacer le code précédent par :
|
//Rabidja 3 - nouvelle version convertie en SFML 2
//Copyright / Droits d'auteur du tuto : www.meruvia.fr - Jérémie F. Bellanger
//Copyright / Droits d'auteur du fichier : Skrool, 2015.
//Nous allons avoir besoin d'inclure ces bibliothèques
#include <SFML/Graphics.hpp>
#include <iostream>
#include <math.h>
//Ainsi que les classes
class Map;
class lightEntity;
class Light // Class permettant de gérer les lumière
{
public:
//Constructeur et destructeur
Light(void);
~Light(void);
//Accesseurs
sf::Color getShadowColor(void) const;
sf::Color getTargetShadowColor(void) const;
//Mutateurs
void setShadowColor(sf::Color value);
void setTargetShadowColor(sf::Color value);
//Fonctions
void draw(sf::RenderWindow &window);
void update(Map &map);
//Ajout de lumière
void AddLight(int quality, sf::Color color, float radius, float intensity,
sf::Vector2f position, int type, Map &map, int sourceX, int sourceY);
void AddDirectionalLight(sf::Color color, float radius, float intensity, sf::Vector2f position,
int type, float angle, float o_angle, Map &map, int sourceX, int sourceY);
//On vide les lumières
void clear(void);
//Cette fonction retourne "true" uniquement si le timer pour recalculer les lumières est arrivé à 0
bool decrementeTimer(void);
private:
//Tesxture de l'ombre
sf::RenderTexture m_shadowTexture;
//Notre couleur d'ombre
sf::Color m_shadowColor, m_targetShadowColor;
bool m_shadow;
//Les lumières vont être déclarées dans un tableau dynamique.
//Cela nous permettra de les supprimer beaucoup plus simplement qu'en utilisant
//le même système qu'avec les autre entités.
std::vector<lightEntity> m_light;
//Le timer pour ne pas avoir à mettre à jour les lumières toutes les frames
int m_refreshTimer;
/*****************/
/* Constantes */
/*****************/
//Nombre max de lumières
const unsigned int MAX_LIGHT = 15;
//Temps en deux générations de lumière
const int TIME_BETWEN_TWO_UPDATES = 3;
};
|
|
Light::~Light(void)
{
clear();
};
|
Fichier : light.cpp : Faire les modifications nécessaires :
|
void Light::draw(sf::RenderWindow &window)
{
// On crée un RenderStates avec le mode de rendu multiply, il nous sera indispensable pour la suite
sf::RenderStates render(sf::BlendMultiply);
// On vide notre render texture et la remplit de la couleur de l'ombre
m_shadowTexture.clear(m_shadowColor);
for (unsigned int i = 0; i < m_light.size(); i++) //On boucle sur nos lumières pour les afficher une à une
{
//Attention ! La fonction lightEntity::draw prend comme paramètre un render target.
//Nous allons en effet envoyer notre render texture d'ombre pour afficher les lumières dessus, et
//ensuite les afficher sur notre fenêtre
m_light[i].Draw(m_shadowTexture);
}
//On affiche les lumières se trouvant sur notre render texture
m_shadowTexture.display();
// On affiche notre render texture avec le mode multiply
window.draw(sf::Sprite(m_shadowTexture.getTexture()), render);
}
void Light::update(Map &map)
{
// On rajoute une partie de la différence entre les valeurs r, g et b des
//deux variables pour avoir un effet de transition en douceur
if (m_shadowColor.r < m_targetShadowColor.r)
m_shadowColor.r++;
else if (m_shadowColor.r > m_targetShadowColor.r)
m_shadowColor.r--;
if (m_shadowColor.g < m_targetShadowColor.g)
m_shadowColor.g++;
else if (m_shadowColor.g > m_targetShadowColor.g)
m_shadowColor.g--;
if (m_shadowColor.b < m_targetShadowColor.b)
m_shadowColor.b++;
else if (m_shadowColor.b > m_targetShadowColor.b)
m_shadowColor.b--;
// Si le timer est arrivé à 0, on met la variable timerOk à true.
bool timerOk = decrementeTimer();
//On boucle sur toutes les lumières
for (unsigned int i = 0; i<m_light.size(); i++)
{
//On met à jour les coordonnées m_position de la lumière par
//rapport à sa position sur la carte, et à map.startX et startY
m_light[i].move(sf::Vector2f((m_light[i].getAbsolutePosition().x
- m_light[i].getPosition().x) - map.getStartX(),
(m_light[i].getAbsolutePosition().y - m_light[i].getPosition().y)
- map.getStartY()));
if (timerOk) //Si le timer s'est écoulé on met à jour la lumière
m_light[i].update(map);
}
}
bool Light::decrementeTimer(void)
{
m_refreshTimer--;
if (m_refreshTimer < 0)
{
m_refreshTimer = TIME_BETWEN_TWO_UPDATES;
return true;
}
return false;
}
void Light::AddLight(int quality, sf::Color color, float radius, float intensity,
sf::Vector2f position, int type, Map &map, int sourceX, int sourceY)
{
if (m_light.size() < MAX_LIGHT)
{
m_light.push_back(lightEntity());
m_light.back().create(quality, color, radius, intensity, position, type,
map, sourceX, sourceY);
}
}
void Light::AddDirectionalLight(sf::Color color, float radius, float intensity,
sf::Vector2f position, int type, float angle, float o_angle, Map &map, int sourceX, int sourceY)
{
if (m_light.size() < MAX_LIGHT)
{
m_light.push_back(lightEntity());
m_light.back().createDirectionalLight(color, radius, intensity, position,
type, angle, o_angle, map, sourceX, sourceY);
}
}
void Light::clear()
{
//On vide tout
for (unsigned int i = 0; i < m_light.size(); i++)
{
m_light[i].clear();
}
m_light.clear();
}
|
Et on rajoute le paramètre map à l'appel de la fonction d'update() dans notre boucle principale de main.cpp, et on vérifie aussi que la fonction draw() est correctement appelée
:
Fichier : main.cpp : Ajouter / changer les éléments suivants :
|
//On met à jour le Gestionaire de lumière
manager.update(map);
//Code coupé...
//Gestion des lumières dynamiques par Skrool
manager.draw(window);
|
On compile et... rien !!... ![]()
Bah oui, on n'a pas initialisé de lumière !
Avant de gérer ça avec notre map et nos tiles de lumières dans le chapitre suivant, nous pouvons en générer une directement dans le main() pour tester :
Fichier : main.cpp : Ajouter juste avant la boucle principale :
|
//On crée des lumières pour tester
manager.AddLight(15, sf::Color::White, 200, 250, sf::Vector2f(200, 500), 0, map, 0, 0);
manager.AddDirectionalLight(sf::Color::Magenta, 300, 200, sf::Vector2f(400, 200), 0, 40, 70, map, 0, 0);
// Boucle infinie, principale, du jeu
while (window.isOpen())
{
|
On compile, et ... MAGNIFIQUE !!
Cela marche ! ![]()
Essayez de changer les valeurs des lumières ci-dessus pour voir ce que cela fait, puis, quand vous aurez terminé d'expérimenter, supprimez ces lignes
. Eh oui, on va raccorder notre système avec nos tiles, comme on a fait avec les tiles d'obscurité.
Mais, ça, ce sera pour le prochain chapitre ! ![]()

@ bientôt pour l'annexe 3 ! ![]()
Skrool

English
Français 