L’obscurité naît de la lumière

Le rendu actuel est bien joli, mais ça manque d’effet de lumière et donc d’obscurité. Du coup j’ai tenté l’expérience, pleuré, cassé, et réussi à avoir un premier résultat.

D’abord c’est quoi cette notion de lumière/obscurité ? C’est tout simplement le fait d’avoir une image lumineuse ou assombrie sur base d’une source supposée de lumière et d’un calcul pour définir l’effet.

Une source ? N’importe quel Element peut être une source de lumière, j’ai créé un interface Illuminate pour qu’il se prenne pour un truc radioactif.

Un calcul ? J’ai déclaré,pour le test, que la lumière allait à 6 cases de distance en décroissant de 20% de manière arbitraire. Cependant j’ai prévu une interface (pas finie) dont le but sera de définir des paramètres pour l’éclairage, permettant ainsi des variables d’affichage (couleur, force, …).

Il s’agit donc de définir pour chaque Element de chaque Zones de tous les GridBlocks du subsetMap une valeur de « Brightness » (luminosité) sur base du calcul et des sources. Le tout sans faire de calculs inutiles.

J’ai opté pour le moment de la création du subsetMap pour agir. Ceci arrive uniquement lors d’un changement d’offset de la caméra (déplacement du héro en bord d’écran), on peut alors profiter de la boucle de création pour trouver les lumières de la Scene. Ensuite il faut parcourir à nouveau le subsetMap pour mettre le « Brightness » de tout le monde à 0 : le noir total. Oui deux fois, car nous agissons dans PlayerScene basé sur un IsoScene et la création du subsetMap est dans IsoScene.

Ensuite pour savoir ce que la lumière va affecter je me suis dit que le PathFinder ferait un bon système de propagation, respectant les murs et les objets. Ce n’est pas au point, et cette idée doit être poussée plus loin, mais le départ est pas mal. On imagine une source de 100% qui perd 20% à chaque changement de case, et on ajoute ceci à l’éclairage déjà présent.

La théorie est pas mal, on est en 2D isométrique quand même, on ne peut pas faire trop de folies et on est limité par l’architecture de TARS et des possibilités de « Canvas » (notre support HTML5 de dessin). En pratique : les performances s’effondre…

Mais que se passe-t-il ? Il faut savoir que pour avoir cet effet j’utilise le « filter » « brightness » de « Canvas », en gros une propriété au moment du dessin permettant d’affecter la luminosité de l’image que j’ajout pour composer la Scene. Je le fais déjà avec l’alpha sans soucis (cf titre daaboo). Bref, ça fonctionne mais prend un temps de dingue, ce qui rend le résultat non réactif et donne du mal au navigateur pour répondre.

Il n’y a pas d’autre façon, l’accès au données de l’image n’est permis qu’au niveau du rendu final et non à l’image que l’on va ajouter et on ne peut pas travailler sur l’image stockée en mémoire, d’une part car c’est pas permis et de l’autre car il nous faut l’original pour modifier les effets.

Une seule solution : la protestation ! À chanter comme une manif de métallo un jour de pont entre deux grèves…

Comment améliorer les performances ? Comment faire quand les outils manquent, les accès limités, les possibilités réduites… ? Il y a bien quelques solutions, mais l’idée n’est pas de demander plus de travail aux utilisateurs de TARS, enfin, le moins possible.

Je suis parti sur l’idée d’un cache, le soucis étant le temps de rendu, et on sait que les mises à jour ne sont que ponctuelles par comparaison. Mais comment ? Nous allons créer un « Canvas » par IsoElement (on va surement le descendre en SpriteElement par la suite (c’est fait :p) et on fait le rendu dans ce « Canvas » invisible. Au moment de composer la scène on copie le contenu à sa destination comme on le faisait avant, et on ne fait plus que ça jusqu’à ce que l’on doive régénérer le cache.

Pour vérifier que ça fonctionne bien avant de péter tous mes calculs, j’ai simulé un rendu dans mon cache, un carré rouge et au lieu de dessiner comme l’image ci-avant, on utilise le contenu du cache.

Je vous épargne le retour du bug de hauteur, le curseur qui fait bouger les Elements, les soucis liés à la division du calcul en 2 étapes, etc. On a déjà connu, déjà fixé, et j’ai dû le refaire encore une fois…

Une anecdote cependant, si vous ne dite pas à votre cache de se régénérer sur base des paramètres de votre Element (PlayerElement, TreasureElement, etc.), il restera tel quel depuis son premier rendu, en gros Squellettore ne marchait plus, il glissait, droit comme un pique sans changer d’orientation. Logique si vous avez lu. Du coup on doit penser à invalider le cache lors de nos mises à jour.

Vu qu’on parle d’animation, il faudra permettre de savoir quand une animation change afin de régénérer le cache à point nommé. Et hop un TODO de plus…

Une fois tout cela fait, on a un rendu avec des performances bien meilleures, cela demande un peu plus de manipulations car il faut penser au cache mais globalement c’est une nette différence.

Concernant le cache, certains automatismes pourront être mis en place en interne au lieu de demander à l’utilisateur de TARS d’y penser. Comme invalider le cache si vous changez d’orientation, l’alpha ou la luminosité. Ce qui est fait en même temps qu’écrire cet article :p et ajouter 3 TODOs… comme si j’en avais pas assez… ^^

Revenons à nos lumières. On a un rendu correct sur base du calcul déclaré plus avant, simple, efficace, même si pas fini (murs opposés, murs d’une case supérieure à la distance prévue, …). Du coup je décide d’en ajouter une seconde pour voir si le cumul fonctionne bien et c’est la fête du slip… Je n’ai pas fait de photo, mais en gros le point d’origine est assombri et génère de l’obscurité, les cases éclairées sont plus loin, bref ça ne va pas.

Mais pourquoi ? C’est une sacrée boucle à investiguer. Ce n’est pas la fonction d’action sur les Elements, ce n’est pas les préparatifs, ni les accès aux variables. C’est le propagateur (par extension le Pathfinder). En fait, par un malheureux hasard, il se peut que vous passiez plusieurs fois sur la même case au cours d’une boucle de propagation (ce qui peut arriver). Le truc c’est que je pensais que je les avais bien exclues, et c’est vrai, mais il faut aussi vérifier en préventif.

Mes tests montraient une quantité astronomique de calculs pour ma petite zone de 10×10, ce qui ne me semblait pas vraisemblable. C’est ce qui m’a mit la puce à l’oreille en gros. Un « If » de vérification et d’un coup la folie s’arrête, les lumières apparaissent et se cumulent proprement. En plus dans l’histoire on a augmenté les performances du Pathfinder et du propagateur pour les lumières !


Voici un résultat avec 2 lumières, le coffre statique et le héro mobile.

Il ne faut pas perdre de vue que ce n’est pas fini. Il y a un gros soucis de performance à la mise en place des lumières quand on s’occupe du subsetMap. Il faut considérer les lumières animées et s’occuper également des occlusions (ça serait top) et de l’orientations des objets, par exemple les murs, opposés ou de la case d’à côté nous tournant le dos. Là il va falloir creuser les théories les meilleures pour augmenter la qualité de l’effet sans défoncer les performances.

Une autre chose aussi, vu que nous travaillons en 2D isométrique, serait d’ombrer selon le caractère cubique de nos objets et de prévoir ainsi, soit une image South, une image East et pourquoi pas une image Top pour ombrer selon des calculs d’angle (les normales) et de distance. Le tout serait de fournir à ces objets des informations plus complète qu’actuellement.

Ceci dit, en l’état, c’est déjà chouette comme rendu !