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).
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.
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 :
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 :
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.
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.
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.
Et c'est tout. En pratique, ça donne ce rendu :
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 :
Et dans Holyspirit :
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ï.

49
Excellent billet, et très instructif ! Je vous encourage à continuer !
Merci à toi pour cette encouragement, on fait de notre mieux pour mettre en ligne d'autres billets sur d'autre thématique prochainement.
Nice
j'aime !
and so Cute(Qt)
Super article
Quel rapport avec Qt ?
Super article !

Très bien expliqué et c'est vrai que c'est une question que je me posais pendant longtemps sur la "collision" de la lumière avec un mur en 2D.
En fait je restait sur faire un dégradé circulaire donc automatiquement je coinçais mais la technique des triangles est très ingénieuse !
Donc merci pour ce super billet
Salut !
).
Article intéressant, démarche "originale" dans le sens qu'elle emprunte un chemin inverse de la démarche classique de génération d'ombre.
Dans ton cas, tu coupes tes rayons lumineux par rapport aux murs "occulteurs" (occluders).
Dans le cas classique (Shadow-Map, Shadow-Volumes), on utilise les objets occulteurs/occluders pour générer des bitmaps d'ombres (Depth-Shadow-Map), ou des volumes d'ombre (Shadow-Volumes).
Sur GameDev, tu peux trouver une méthode intéressante basée sur ces approches (Shadow-Volumes en l'occurrence): Dynamic 2D Soft Shadow. Le caractère 2D du problème permet même à l'auteur de s'amuser avec les pénombres (tu utilises un flou ScreenSpace dans ton cas pour atténuer les effets "dur"/hard des ombres/absence_de_lumière, c'est une très bonne idée, qui fonctionne bien dans le cas 2D (ou 3D-iso)).
En terme de perfs, je ne sais pas à priori quelle approche est la plus pertinente (split des triangles de lumières, ou génération de Shadow-Quads) ... à priori je dirais les Shadow-Quads (parce que je bosse pas mal avec ça en 3D
En tout cas,
très bon tutos,
démarche très très altruiste de ta part
Au plaisir de te lire prochainement sur d'autres aspects techniques de ton moteur !
YoYoOOoOOoOOOoo
N.B:
- Ca me fait penser que j'ai vu des travaux plus récents sur ce domaine => Catalina's XNA Experiments - Dynamic 2D Shadows (ya une p'tite explication et des sources en XNA à récupérer).
- Une vidéo de démonstration d'un moteur (indie) utilisant (surement) une méthode proche (voir identique) => XNA 2d Game with Lighting and Shadows (c'est très très joli !).
Voili, voilou,
YoYoOoOooOOo
En tout grand merci pour ces liens, je vais y jeter un oeil.
Sinon, pour les shadow map, certes c'est pas mal, mais le problème c'est que je vais devoir rendre chaque lumière sur une RenderImage différente, ensuite dessiner les shadow map dessus, rerendre le tout sur une RenderImage principal que je rendrai enfin sur le jeu... plutôt lourd hein ?
Ici, je n'utilise qu'une seule RenderImage.
Dans le profilage de mon code, je vois que c'est quasi que le fait de rendre la RenderImage qui me bouffe des perfs, le calcul des lumières se fait relativement très rapidement.
Yo !
Je ne recommandais pas l'utilisation d'une shadow-map: discrétisation de tes occulteurs (murs) peu pertinente au vu de leur complexité et nature très vectoriel (des segments 2D).
Mais plutôt un shadow-volume: projection (analytique) des occluders (segments de murs) => création de shadow-quads (2D)), plutôt que ton "split-volume-light" (découpe de ton cercle de lumière (discrètisé par des triangles) par des segments).
Après, vu le cas spécifique et très contraint de ton problème (vue 2D, occluders 'mur' (i.e segments), etc ...), ça revient surement au même niveau perf./résultat.
Mais si t'as le temps et la motivation, tu peux intégrer (facilement je pense) la seconde méthode dont je fais mention (méthode évoquée dans tous les liens).
Au final, ça ne change le nombre de RenderImage (toujours 1), mais change la génération des primitives d'ombres: tu crées des quadrilatères, au lieu d'intersecter des triangles (ça me semble, en l'écrivant, quand même beaucoup plus simple/performant).
Tu vois ce que je veux dire ?
YoYooOoOoOOooOO
Le problème, c'est qu'il faut que je trouve un moyen de rendre ces ombres de murs sans qu'ils interfèrent avec les autres lumières.
Donc je dois pouvoir appliquer ces ombres uniquement sur le rendu de la lumière, et sans RenderImage, je ne vois pas comment réaliser cela. (Sauf peut-être en passant directement par OpenGL ?)
En effet, ça semble compliquer (surtout que je connais pas la lib que tu utilises) de réaliser cette méthode sans un minimum de contrôle de l'API graphique (OpenGL dans ton cas).
).
Si tu as un contrôle, tu peux utiliser un Stencil-Buffer et ces fonctions de Stencil_Tests pour filtrer/sélectionner les occlusions pour chaque lumières.
Grosso modo,
pour chaque source de lumière:
- tu détermines l'ensemble des arêtes/segments de murs occluders
- tu les projètes avec le centre de la source de lumière (centre du cercle de lumière) sur les bords de la sources de lumière (tu peux utiliser directement le cercle ou la Bounding-Box).
- Avec les segments et leurs projections, tu obtiens des Shadow-Quad que tu rends dans le Stencil-Buffer (pas dans le Color-Buffer).
- Dans le stencil tu auras donc 1 aux endroits ou l'ombre des murs se projettent, et 0 partout ailleurs (zones illuminées)
- Tu rends ensuite (dans le Color-Buffer) ta lumière (avec le modèle que tu veux, les triangles en fan ou un quad + une texture de lumière (ligttmap)) en activant le test de stencil:
- Si 1: on n'écrit pas (ombré)
- Si 0: on rend la lumière (illuminé)
- Tu re-initialises la zone de la lumière avec un rendu dans le stencil en forçant l'écriture de 0 (zone illuminée)
- Tu boucles sur toute les lumières (en color additif par exemple).
Tu peux remplacer les valeurs 0 et 1, par deux valeurs distinctes que tu veux (l'inverse par exemple, question de gout après
Je ne sais pas si tu es familié avec la terminologie que j'utilise, mais ce n'est pas très compliqué normalement (mise à part jongler avec ta lib pour faire ce que tu désires).
Ton système est astucieux par rapport à tes contraintes, et si ça fonctionne, ce n'est pas la peine de te prendre plus la tête
Si un jour, tu passes à la 3D ou un système plus élaboré, tu pourras relire tout ça !
Certes, c'est ce que je ferai.

Je peux avoir un contrôle direct sur Opengl avec la SFML, mais je ne le fais pas car ça demande pas mal de changements.
Je crois que je vais rester sur mon système pour Holyspirit, mais je garde tes idées pour plus tard.
Un tout grand merci à toi.
Merci à toi pour tes partages !
Je continuerai à suivre ton blog
Tu peux si tu n'utilises pas ton canal alpha (transparence par exemple) utiliser les tests alpha via:
En reprenant ce que j'ai écrit au dernier post,
tu remplaces tout ce que je dis pour le stencil par les tests alpha (avec les fonctions: GL_EQUAL ou GL_NOTEQUAL, et valeurs de références: 1 ou 0).
Normalement ça devrait être direct ...
Ton jeu est en OpenSource,
il y a un moyen de récupérer des sources quelque part ?
Si oui, je peux te coder ça en une dizaine de minute je pense.
Les sources sont disponibles sur la SVN, mais je te préviens, c'est un vrai foutoire. J'ai commis de nombreuses erreurs dans le développement, mais qu'il serait fort lourd de corriger à l'heure actuelle. (Je travaille sur un nouveau moteur pour ça.)
Voici l'url :
https://lechemindeladam.svn.sourceforge.net/svnroot/lechemindeladam/Source/
Hé Hé j'avais pas lu jusqu'à la fin, ya des sources proposées, je regarde ça je te tiens au courant !
Bon finalement,
ce n'était pas si simple que cela
J'ai du modifier SFML à la main (la version 2.00) pour qu'il initialise correctement les surfaces de rendu (en F.B.O) pour prendre en compte la gestion du Stencil-Buffer (faudrait que j'envoie un mail au dev. de SFML pour leur signaler).
En tout cas, ce dont je parlais tourne !
Faut que je nettoie le code (surtout les modifs de la SFML),
optimise un poil,
et je te fais une archive !
Je lancerai des tests de perfs pour quantifier les perfs !
Okey, super. J'attends ça avec impatience.
Hello !
Pas mal de taffs ces derniers temps,
je n'ai pas trop pris le temps de finir le prog,
mais j'ai stabilisé ce que je voulais faire !
En gros j'utilise le cercle de lumière directement en définition paramétrique (x² + y² = r) et je résous une équation du second degré pour trouver les points d'intersections avec les wall (segments).
Le truc un peu plus chaud est de construire le polygone englobant la projection des intersections sur le cercle de lumière.
Mais j'avais déjà effectué ces calculs (assez simple en 2D) pour ma thèse (traitant d'un problème quasi similaire en 3D).
Donc ça fonctionne (normal), par contre c'est la foire niveau instruction, je ne suis pas sur de faire mieux que toi !
Mais ça sera pour l'amour de l'art comme qui dirait
En attendant les sources,
voici un screen debug de la méthode:
Avec:
- en gris: les walls (segments)
- en jaune: les droites supports des walls détectés comme walls-occulteurs pour une light (cercle de lumière)
- en rose: le segment/wall après clipping par le cercle de lumière
- les points:
- en vert: projection des sommets (formant le segment rose) sur le cercle (de lumière)
- en orange (petit): milieu des sommets en verts
- en orange/rouge (plus gros): sommets utilisés pour englober le shadow-volume lié au wall clippé par le cercle de lumière (faudrait plus de détails pour comprendre, mais pas maintenant :-))
Bon,
ça doit pas être très clair,
mais j'ai mis pas mal de commentaires dans le code,
et je ferai un tuto si t'es intéressé !
J'archive demain ou après demain les sources !
A bientôt,
YoYOOOOoOOOOOo
bon ... la balise html n'a pas fonctionné ...
Si tu peux modifier mon post pour l'activer je ne suis pas contre :-)
Le lien direct sur l'image: HolySpirit Light Test
C'est fait.

Ca à l'air pas mal, niveau performances, tu vois une grosse différence ?
Niveau mettre un flou sur les bords, pour faire des soft shadows, tu as une idée autre que passer par un shader qui consomme quand même assez bien ?
Merci.
Re man !
.
Niveau perf., je ne vois pas d'énormes différences avec cette configuration de scène (peu d'occulteurs, peu de lumières).
J'ai fait un test rapidosse en plaçant des murs (segments) aléatoires un peu partout sur la map.
Ma méthode est plutôt stable (perf. décroissante de manière linéaire (faible pente)).
Par contre, la tienne chute drastiquement (décroissance exponentielle) à partir d'un nombre relativement important d'occulteurs (aux alentours de 200 ou 250 murs aléatoires). Je n'ai pas essayé de tracker la source de la chute ... peut être une fuite mémoire ou un récursion qui ne finit pas ... je ne sais pas trop. Je ne suis pas sure que ce soit très intéressant d'ailleurs d'étudier ces cas peu réalistes ("nuage" de murs).
Sinon, je suis sur un système full GPU pour gérer les intersections.
Le système de calculs (des p'tits points) à la main est joli mathématiquement, mais ça ne sert pas forcément à grand chose dans un cas aussi simple que celui là (2D).
En gros, j'utilise les fonctions (natives) OpenGL de Viewport et Frustum-Culling (en 2D) pour clipper les projections (shadow-volume).
Ça fonctionne! Faut que je test les perfs (différence de perfs).
A priori, ça devrait aller plus vite, car il y a beaucoup moins d'instructions (coté CPU) pour le calcul des intersections.
Par contre, il y a plus de rasterisation des volumes d'ombres (une sorte d'équilibre CPU/GPU à trouver).
J'ai des idées pour les Soft-Shadow (en 2D):
- Une version 'hack' avec l'utilisation d'une texture de pénombre (qui donne de très bon résultat, c'est la méthode proposée dans les liens que j'ai fourni au début).
- Une seconde 'mathématique' qui calcul directement la pénombre "exacte" (jamais vu encore sur le net). Ça doit être réalisable assez facilement (je pense) vu la nature 2D du problème (qui se ramène au final à un problème 1D après projection).
Dans les 2 cas, il faut que j'update ma génération de volume d'ombre pour prendre en compte les zones de pénombre (penumbra-volume in/out).
Mais rien de (très) compliqué (ou déjà fait (en 3D)).
Pour ton blur,
je te filerai des liens et des méthodes plus optimales que ce tu fais.
En gros l'idée s'appuie sur la décorrélation horizontale/vertical d'un filtre gaussien (filtre dit "blur"
Plus concrètement, tu peux décomposer ton rendu de blur en deux passes indépendantes: une verticale et une horizontale.
Au final, au lieu d'avoir n*n accès textures (n taille du noyau), tu as 2*n accès (donc dans ton cas 6 au lieu de 9, pour un noyau supérieur 8 au lieu de 16, etc ...).
Le top moutte-moutte ensuite, c'est d'utiliser le multi-échelle pour projeter ta fonction gaussienne.
Plus concrètement, tu utilises un système de mipmap (plus ou moins grossier) pour blurer à différent niveau de résolution, et tu sommes "intelligemment" les mipmap pour obtenir ton blur final.
Pour que cette méthode fonctionne bien (sans trop d'artefact ou le moins possible), il faut la coupler avec des décalages d'offset pour exploiter le filtrage linéaire (natif) des textures par OpenGL.
Ya de très bons papiers qui traitent de la reconstruction de filtre (bilinear-cubic par exemple) en quelques accès textures (9 au lieu de 16 je crois). Les rendus sont vraiment impressionnant en qualité et le cout n'est pas super élevé.
J'ai retrouvé des exemples que j'avais codé, en images ça donne ça:
(1) Bitmap 16x16, en mode nearest:
(2) Bitmap 16x16, en mode bilinear (filtre hardware classique):
(2) Bitmap 16x16, en mode bi-cubic ("nouveau" filtre calculé par shader):
(ps: j'espère que ça passera cte fois l'ajout des images :lol
Bon j'arrête mon roman,
on en reparlera si t'es intéressé !
A bientôt!
ps: sous FireFox (v3.6.10), je n'arrive pas à utiliser le bouton 'Aperçu' pour justement voir si le formatage de mon texte est correct.
Raaahhh, je me suis raté sur la 1ère ... désolé ... si tu peux corriger (faut enlever un <br /> qui traine au milieu), merci d'avance ! :lol
Je vais regarder ça de plus prêt.
Pour la triche en prenant une texture, j'étais justement en train de faire des tests dessus, mais j'obtiens des résultats plutôt pas terrible pour l'instant.
Je serais curieux de voir le code du shader pour faire le blur. ^^
Mon problème reste toujours que pour partir sur une solution comme tu parle, il me faut passer outre de la SFMl, et je ne sais pas trop si c'est directement faisable, mes connaissances en OpenGL sont encore trop limitées. >_<
Tout ce que je dis est compatible avec l'utilisation de la SFML (qui est d'ailleurs une très bonne p'tite API, ravi de l'avoir découverte grâce à toi!) sans passer (forcément) par OpenGL directement.
L'API est assez bien faite, modulable, flexible, pour tout ça (technique d'ombre, d'ombre douce, et le filtrage des textures (c'est juste un shader)).
Pour le filtre BiCubic tu peux jeter un œil la dessus: High-Quality Filtering
C'est un peu technique mais ça se suit.
Bon j'ai finalisé (partiellement) le code, la compile, la lib (sfml2), etc ...
Je t'ai envoyé un mail avec l'adresse pour récupérer l'archive!
J'espère que tu arrivera à l'utiliser.
Tiens moi au courant !
Hey !
Première étape finalisée pour les Soft Shadow 2D en calculs analytiques: Caractérisation des zones de pénombres (inner/outer penumbra) et calcul des volumes englobant ces zones de pénombres (qui est au final, un peu différent des volumes englobants les volumes d'ombre).
Une p'tite image debug pour voir tout ça en action:
Il me reste "plus" qu'à calculer les coefficients de pénombre, mais normalement c'est pas compliqué (du moins sur le papier ca n'a pas l'air). Si ça fonctionne le cout devrait être vraiment très faible (le shader correspond semble être très léger!).
A confirmer
Je suis vraiment pas doué avec cette balise d'image ! (désolé)
Bon, dernier essai:
Sinon l'adresse de l'image: Debug pour les Softs Shadow en 2D
Yeah, ça a l'air pas mal.
Moi je n'ai pas encore trop pris le temps de regarder ton code, pour le moment je dois bloquer un peu car j'ai des interros samedi prochain.
J'espère donc avoir le temps un peu le weekend prochain, genre dimanche.
Hello !
).
Tu dois être à bloc sur les révisions j'imagine !
Je laisse une p'tite image sur mon avancement pour les SoftShadow:
Comme on peut le voir, ça avance, mais ça commence à devenir une boucherie niveau code, faut que je revois (encore) tout ça.
Je vais surement m'émanciper partiellement de tes classes lumière, peut être ré-integré le projet dans une autre lib de dev maison (Bablib, projet inria). Ça tourne sous QT et c'est assez pratique à utiliser (et je connais personnellement le dev
J'ai remplacé la construction de la source de lumière (avec des triangles) avec une utilisation d'une BoundingBox et un shader qui calcul directement la lumière (ratio lié à la distance du centre, et prises en compte des caractéristiques des ligths (ambient, diffuse color, etc ...)).
Au final, ça permet d'avoir un rectangle pour afficher la light (que j'utilise déjà pour mes phases de Shadow-Volume).
Pour le calcul de pénombre, j'utilise une retro-projection des arêtes sur la source de lumière et un calcul simple et précis d'occlusion. Faut que je mette en place une stratégie pour gérer les recouvrements des volumes de pénombres (surement une approximation du style prendre le minimum d'intensité lumineuse). Il me reste encore des bugs, mais globalement c'est stable.
Niveau perf, ca a l'air raisonnable. Faut que je stabilise le code et les algos pour voir si j'ai besoin d'une autre RenderImage ou si je peux m'en passer. La seconde RenderImage serait une sorte de LightBuffer, qui contiendrait uniquement les coefficients d'éclairage/visibilité/ombres/pénombres/etc ... (ça permet de pas fusionner les attributs liés à la couleur/matériaux et les coefficients de lumières/ombres).
C'est à peu près tout :-)
A bientôt !
Et pour changer, je me suis raté sur l'image
La voici:
Enjoy
Waaaw ! Ça a de la gueule. o_O
Hé Hé,
content que ça te plaise
Ya mon pote (dev de la BabLib dont je parlais) qui est passé jeter un œil au prog. On a pas mal discuté, et il m'a filé des pistes sur les recouvrements des volumes de pénombres (problème classique de fusion des occluders en Soft-Shadow).
Il m'a corrigé aussi des p'tites erreurs que j'avais fait.
En particulier, des approximations "naïves" de constructions des volumes englobant des recouvrements de pénombre. Les approximations au final sont utiles, mais je ne les contrôlais pas (du moins je ne savais pas que j'en faisais).
Ça m'a rajouté de la complexité dans le code, mais ça m'explique bien certains cas foireux (avec des sources de lumières trop grande, i.e. des occluders/walls trop proches de la source, etc ...).
Il me reste le (moyen-gros) problème de recouvrement des volumes de pénombres sur les volumes d'ombre.
Faut que je code une approximation avec l'idée du min. rapidement pour voir ce que ça donne (ça sera surement acceptable visuellement et surement suffisant pour une application comme ton jeu).
Quand j'aurais une version qui tourne de manière stable avec le recouvrement, je ferai un gros clean de code, et je me lancerai dans un (plus ou moins big) tutorial sur la méthode.
Ça commence à partir un peu dans tous les sens mon code, et la méthode devient de plus en plus riche (peut être pas complexe mais fournie). Un arrêt de code pour écrire une doc. aidera !
Voili voila mon programme !
Je te tiens au courant si ya des news !
Okey, super.
Yo !
Je pense avoir trouvé une stratégie (simpliste mais fonctionnelle) pour accumuler les lights et les occlusions (ombres dures et pénombres).
Voici un résultat avec les 3 lights dynamiques:
J'utilise une RenderImage en plus pour séparer les coefficients de visibilité (ou sont les ombres? pénombres ?) et j'utilise la seconde pour stocker les composantes ambiantes, diffuse color et je calcul la pass de lighting.
J'ai encore des gros soucys avec le StencilBuffer, il semblerait qu'il soit initialisé mais je n'arrive pas à utiliser plus d'un bit (2 valeurs) sur 8 (256 valeurs). Je ne sais pas d'où vient le bug ...
J'ai pu passer outre (l'utilisation du stencil) en calculant analytiquement l'influence des sources de lumière (un disque) et discard(annuler) les texels qui sont en dehors.
Mais il y a moyen de le faire avec le StencilBuffer pour moins chère normalement ... et cette technique ne fonctionne qu'avec des primitives de lumières simples (cercle, quad, triangle, etc...).
Je vais continuer un peu sur le debug du Stencil-Buffer, cleaner le code et avant le tuto !
A bientôt !
Re !
(j'y ai pensé hier lors d'un concert).
Bon, je n'arrive toujours pas à m'en sortir avec le Stencil-Buffer ... Il faut que je vérifie au niveau des drivers de ma carte graphique (Nvidia 9700GTM - Win7x64) si je peux utiliser un stencil-buffer via OpenGL. Normalement ça devrait être le cas (fonctionnalité de base d'une carte graphique), mais ça bug étrangement ...
Pour la fusion des lights, ce n'est pas aussi simple que je pensais. Faut que je passe par un shader pour accumuler de manière "intelligente" les coefficients de visibilité. J'ai lu quelques docs sur les penumbra-wedge: Game Developers Conference 2005 Advanced Stencil Shadow and Penumbral Wedge Rendering.
En gros, il faut soit utiliser des RenderBuffer avec des floats, et faire des calculs en float directement (plus ou moins précis et rapide), ou soit faire des tricks avec un buffer classique RGBA color (8 bits par composant, chaque canal est un "unsigned char" [0, 255]). Pas très simple mais jouable
Je suis parti sur une nouvelle version du code en quasi pur ray-tracing (pour les coefficients de lumière/shadow). Je vais utiliser ce code pour debugger éventuellement la version "optimisée" avec les shadow-volumes et penumbra-volumes englobants.
Il semblerait (visuellement) que j'ai des bugs de discontinuités entre les zones éclairées et ombrées ... mais je ne suis pas sure. Ça semble visible sur mon écran mais il faut que je vérifie analytiquement la continuité du champs de valeurs (coefficients de lumière)).
Ce qui me chagrine c'est que sur le papier ca semble exacte (intersection simple de droite, plan, segment, etc ... des calculs 2D très simples), mais il y a peut être des problèmes de précisions ... A approfondir.
Comme je m'en sortais pas avec le Stencil et les accumulations, j'ai implémenté rapidosse un Normal-Mapping pour (me remonter le moral) voir ce que ça donnait:
Ça rend pas mal
Je vais complexifier un peu le modèle d'éclairage, et la prochaine étape de ce coté sera de gérer une jolie lumière directionnelle (un spot), avec lighting correspondant et je vais essayer de rajouter un effet de "brouillard", un peu dans cet esprit:
Ça devrait pas être trop trop compliqué à mettre en place.
Voili voilou,
je vais m'atteler à faire un tuto cette semaine sur le début de la méthode, je te tiens au courant !
Sympa, mais il ne devrait pas être dans l'autre sens ton normal map ?
Dans l'autre sens ?
).
Tu veux dire l'orientation de la texture ?
Bonne question, j'ai eu une p'tite merde avec les coordonnées de texture justement ... j'ai effectué un flip vertical à la main (y' = 1 - y).
Étrangement la color map (le mur de brique) et la normal map étaient inversées dans le shader. Faut que je regarde en détail dans SFML comment il génère ses coordonnées de texture (et les transmet aux shaders).
Sinon,
j'ai trouvé le problème pour le Stencil.
Une grosse erreur de noobs (pourtant je ne le suis pas plus trop ... comme quoi
En gros, le piège était dans les modes de transmission des valeurs des masques.
J'utilise (assez) souvent les masques de couleur qui s'écrivent avec cette fonction:
idem pour le depthbuffer:
et naturellement, j'ai extrapolé, sans réfléchir pour le Stencil-Buffer, que la fonction est de ce type:
Mais que neni
Le stencil est nativement un buffer d'entier (integer) et son masque est logiquement un masque sur des entiers, donc son vrai prototype est:
Les joies et malheurs du C++ effectuent des cast auto sur les types, donc mon appel:
revenait à un
qui indique à OpenGL que je ne m'intéresse qu'au 1er bit des valeurs du Stencil-Buffer ... un vrai casse tête pour retrouver ce problème !
J'ai réussi à trouver une stratégie d'accumulation pour gérer les recouvrements des ombres simplement: mode additif sur les coefficients d'ombre (on rajoute de l'ombre, au lieu d'enlever de la lumière). C'est une approximation (assez classique) qui ne prend pas en compte les relations d'occlusions: on considère individuellement les occluders, et on ne prend pas en compte leur "fusion", faudrait pour comprendre le problème ...
Il me reste des p'tits soucys sur le clipping de la light par les segments-murs: la lumière (volumique, ou surfacique) peut être intersectée par les occluders.
Faut que je distingue bien aussi ce qui est fait (ou pourrait être fait) coté CPU, puis GPU (vertex processing, pixel processing). Les performances/équilibres dépendront des processeurs dispos (CPU, GPU).
Prochaines étapes:
- Cleanage du code
- Optims
- Tuto
- Spot + Poussières/Brouillard dans la lumière
- le bonus extra: Texture de Lumière <= Si ça fonctionne, ça pourrait être super jolie. Avec cette approche, je pourrais simuler des sources de lumières complexes (animées), tel un feu ou une télé/panneau_d_affichage par exemple qui diffuse un programme et éclaire une scène. Normalement c'est assez compliqué comme effet (soit lent à calculer, soit chère à stocker), mais la nature 2D (projeté en 1D pour les calculs) du problème permet de réduire drastiquement l'ordre de complexité !
Un bon p'tit programme en perspective !
Je te tiens au courant de mes avancées !
1er bilan en images !
).
Toujours la même scène,
avec 1 seule lumière de couleur bleuté (un peu kitch mais j'aime bien ce cyan
1er rendu => Juste la scène avec la zone d'influence de la lumière (ou la lumière se diffuse):
2nd rendu => ajout de l'atténuation de la lumière (modèle OpenGL: avec coefficients constant, lineaire et quadratique d'atténuation):
ps: ça sature un peu, mais c'est juste un jeu de coefficients à trouver/régler.
3ème rendu => On rajoute les ombres dures (à peu près équivalent à la méthode que tu proposes):
4ème rendu => On active le calcul d'ombres douces (en fait, c'était déjà actif dans le 3ème rendu, mais j'ai triché sur le résultat dans le shader pour n'avoir que les ombres dures):
5ème rendu (et dernier) => On ajoute l'effet de normal-mapping:
Le rendu de la 5ème est très contrastée, mais comme pour l'atténuation, c'est un jeu de coefficients à régler. En l'occurrence dans le cas du normal-mapping, je reconstruis virtuellement une position 3D pour la source de lumière et le point de réception. En jouant sur la hauteur (le Z) de ces points, on peut régler les caractéristiques visuelles du normal-mapping.
Voili voilou !
Me suis raté sur le 3ème rendu - Ombre dure:

Sympatoche tout ça.

Ce que je disais pour le normal map, regarde les arrêtes des briques, elles sont noires du côté de la lumière, et éclairée de l'autre. Donc j'ai l'impression que tu as inversé éclairage/assombrissement.
Oki je vois ce que tu veux dire.
C'est peut être un problème avec la normale map, ou juste une impression "d'optique".
La méthode est vraiment simple (pas de cas foireux ou autres discontinuités possibles) et j'ai l'impression qu'elle fonctionne.
Dans les images, la source de lumière est peut être placée sur une zone foireuse au niveau de la texture de normal.
Le truc pour l'illusion d'optique pourrait venir qu'il n'y a pas vraiment d'informations de profondeur/hauteur et on (nos yeux, notre cerveau) juge l'enfoncement ou l'élévation de la brique à l'aide des couleurs et du jeu de lumière ...
J'dis ça j'dis rien, j'en sais rien en fait
Mea Culpa
Tu avais raison, il y a effectivement un bug, c'est pas logique l'affichage.
Encore une fois, je me retrouve avec une inverse de repère (il doit avoir une histoire de repère direct, indirect, OpenGL versus DirectX) selon l'axe y.
J'ai inversé l'axe y (y' = -y) et ça donne ce résultat (plus convaincant il me semble):
Well done man !
En effet, là c'est vraiment parfait.
Yep, ça commence à être sympa !
Par contre, j'ai une augmentation niveau complexité de code et algorithmique pour régler (ou essayer de régler) le dernier gros problème de la méthode: Gestion du single-view (single-point-position), point de vue unique, de la source de lumière (surface/linéaire).
En gros, le problème est la caractérisation ponctuelle de la source de lumière (centre du cercle de lumière) pour déterminer la visibilité des murs par la lumière.
Il y a plusieurs façon de voir le problème (on parle pour ce type de problème de point de vue 'dual'(=2)).
(1) On peut soit considérer le point de vue de la source de lumière, et se demander du point de vue de la lumière, qu'est ce qu'elle perçoit de la scène (ses récepteurs).
(2) Ou inversement (dualité), on peut se mettre à la place des récepteurs, et se demander ce qu'ils perçoivent de la source de lumière.
Mon approche pour l'instant s'apparente plus au cas (1), et plus précisément, la relation source_de_lumière/segment_mur.
Pour faire court, la solution que je met en place est de clipper la source de lumière par les droites support des segment_mur (localement pour chaque mur_segment/lumière => il n'y a pas une explosion de complexité (sauf cas pathologiques)).
Avec des mots, ce n'est pas forcément très clair, mais ya moyen de faire de joli schéma explicatif ;-)
Par contre, ça commence à être la foire à la saucisse niveau code et les debugs deviennent de plus en plus dur à lire:
Bon ... ça fait des images sympa !
Sinon, j'ai mis en place une stratégie multi-light-pass pour accumuler des séries de lumières.
Avec trois sources de lumières, bleu, blanc et rouge:
Le fps n'est pas vraiment significatif sur cette image, car il n'y a quasiment aucunes optims (déjà en place partiellement) activées (mode debug).
Avec les optims, je devrais tourner (sur ma config) aux alentours de 800-850 fps.
J'évaluerai le coup de la méthode (coté CPU et GPU).
Voili voilou, encore un peu de taff pour le problème dont je parle au début, et après ça devrait être stable ((presque) plus de cas foireux).
Je te tiens au courant des avancées !
Yo !
J'arrive doucement à une solution clean pour le cas dont je parlais l'autre fois: la lumière (cercle) coupée par la droite support du segment.
Finalement, après discussion avec mon pote (Bab), j'ai laissé tombé l'idée de splitter la source de lumière. Ça complexifie le code (j'ai quand même implémenter la solution ...), et ce n'est pas "très juste" d'un point de vue théorique (un cercle ne peut pas être représenté (même en terme de visibilité) par deux plus petits cercles (inscrits)).
Par la suite, en y réfléchissant un peu plus, je me suis rendu compte que mes calculs d'occlusion cercle/segment étaient déjà robustes à ces cas, donc le problème venait d'ailleurs.
Après quelques heures de debug en tout genre, j'ai pisté le problème qui venait d'une "optimisation" introduite pour les calculs des volumes englobant (qui devenait faux dans le cas mentionné au départ).
Donc, ça commence à être bien stable, très smooth les transitions (passage en zone illuminée, ombrée, pénombres, etc ...) et presque plus de discontinuités:
Dans la foulée, j'ai implémenté un mini Ray-Tracer pour avoir des résultats de références (Ground-Truth). J'ai utilisé un sampling stratifié avec un jittering pseudo-aléatoire. Pour la même avec une position un peu différente de la source de lumière:
Ya du bruit, à cause de l'aléatoire, mais j'aime bien cet effet, ce type de grain
On peut voir que les comportements d'occlusions sont très proches sur les deux méthodes (calcul analytique direct, et par sampling massif (lancé de rayons)).
Le facteur de vitesse est naturellement incomparable (normal) mais ça m'aidera à avoir une comparaison qualitative.
Faut que je mette en place une interface et des systèmes de comparaison pour avoir quelques choses de plus 'scientifique' (ou tout simplement exploitable/utilisable).
Il faudrait aussi que je commence à faire des vidéos pour observer justement le caractère smooth des transitions quand les éléments de la scène bougent (relativement).
A priori pour finir cette "étape", il me faut traiter (ou pas) le cas ou les murs rentrent dans la lumière.
A ce niveau, c'est presque une considération personnelle de comment on considère la source de lumière:
- (1) Comme un objet physique, comme une ampoule, avec une enveloppe physique autour de l'émetteur de rayons lumineux
- (2) Ou un objet volatile, sans matière, juste de la lumière.
Dans le cas (1), il suffit de faire des tests pour être sur que la lumière ne soit pas coupée, voir même interdire ce cas (une sorte de moteur physique de collision).
Dans le cas (2), C'est plus fin et plus compliqué
Dans un 1er temps, sans trop réfléchir sur la question, je vais implémenter un simple clipping des segments-murs par le cercle de lumière (une sorte de mélange de la solution (1) et (2)).
Après, pour changer, grosse phase de clean du code.
Voili voilou,
ça avance doucement ... mais surement
A bientôt!
Chalut !!!
).
J'ai continué sur le projet, un peu moins intensivement mais de manière constante.
J'ai commencé à shooter des vidéos pour observer (ou faire observer) la continuité temporelle des ombres: YouTube - Dynamic Lighting 2D with Soft Shadow
La qualité de la vidéo n'est pas top, mais je fais encore des essais pour trouver un bon codec pour les vidéos sur YouTube. Sur le descriptif de la vidéo, il y a un lien vers la même vidéo de (bien) meilleur qualité.
J'ai pas mal avancé pour le problème d'intersection de la source de lumière par un segment-mur.
Finalement, je me suis ramené à une considération totalement surfacique de la source de lumière, c'est à dire à considérer directement le disque de lumière (et non le plus le cercle, ou ce que je faisais dans un 1er temps, la ligne de lumière). Je calcul directement les aires de recouvrement des arêtes sur le disque. Ça me permet (ou permettra) de gérer le cas des pénétrations de murs dans la source de lumière. Je suis à 70% de résolution du problème (j'ai la solution sur papier et partiellement codée).
Voici des screens qui montrent le problème et le début de solution:
En gros, j'ai calculé deux zones sur trois (d'où le 70%) pour ce cas, et j'ai le calcul pour la dernière zone en C++ que je dois le transposer en shader (mais il est un peu tard là tout de suite
J'ai bien cleané le code: ajout d'une nouvelle entité Light-Wall (indépendante) qui associe individuellement pour chaque mur, une lumière et un ensemble de données pertinentes (bounding shape shadow/penumbra volume, arête générant l'ombre, position de la lumière, etc ...).
Ça clarifie bien le code (encapsule mieux, factorise les calculs), mais faut que je pousse encore un peu dans la simplification/factorisation du modèle/des calculs.
Je dois aussi travailler le "GroundTruth" ou raytracer coté CPU, pour avoir de bonnes références de comparaisons.
Voili, voilou, c'est bientôt fini pour cette étape !
A bientôt !
Yo !
Bon c'était moins simple que prévu (pour changer).
J'ai du utiliser des outils plus techniques pour débugger mes calculs.
J'ai utilisé les occlusions query (on demande à l'API combien de pixels ont été dessiné, rendu, passé les tests, etc ...) pour avoir une base de résultats pour débugger mes routines. C'était un peu long à mettre en place mais ca a été efficace :-D
Voici une vidéo sur DailyMotion (moins de problèmes de compression qu'avec YouTube) des résultats pour des sources de lumières disques (c'est des disques maintenant, plus vraiment des cercles):
Dynamic Lighting 2D with (Analytic) SoftShadow - N°2
Ça commence à être sympathique visuellement,
faut que j'optimise les shaders et clarifie le code (comme toujours).
Je pourrais diffuser les sources après (c'est un peu une boucherie pour l'instant).
Voili voilou,
YoYoOOOoOOOOOo
Tout ce travail accompli est véritablement impressionnant.
Les différentes vidéos de Yoyooooo sont aussi très bluffantes.
Pour le travail de yoyo, je me demandais si les sources seraient à disposition prochainement ?