PROJET AUTOBLOG


blog.fevrierdorian.com

source: blog.fevrierdorian.com

⇐ retour index

Faire un AOV de translucence avec Guerilla

dimanche 19 septembre 2021 à 18:25

La translucence est un effet couramment utilisé en rendu pour les surfaces fines ; feuilles, papiers, etc. Il permet de récupérer l’illumination et les ombres projetées d’un côté pour les projeter de l’autre.

Si vous utilisez Guerilla (ce que vous devriez faire… :siffle: ) vous avez peut-être remarqué que la translucence n’est présente dans aucun AOV de base.

Réponse courte

Vous avez des plans à sortir, pas l’temps d’niaiser. :grenadelauncher:

Réponse illustrée

Si vous regardez du côté de la documentation des Light Path Expression (LPE), vous verrez qu’il est explicitement stipulé que la translucence ne fait pas partie de l’AOV de Diffuse :

Note that this expresion does not include the translucence, as it is categorized as <TD> for Transmitted Diffuse.

Ça me fait penser qu’il faudra vraiment que je fasse un vrai billet sur les LPE… :pasClasse:

Si votre boulot c’est de sortir des images, je vous invite à regarder et vous familiariser avec ce tableau qui liste ce que les AOVs de base représentent en termes de LPE.

Je vais prendre la scène d’exemple livré avec Guerilla dans : Help/Samples/Surface.

Créez l’AOV. Nommez-le comme vous voulez (Utilisez « Translucence » si vous n’êtes pas inspiré… :jdicajdirien: ) et mettez le light path expression C<TD>.+# dans Accept Expression :

Note : Vous devriez pouvoir mettre « Caustics » dans Ignore Expression pour être cohérent avec les AOVs de base, mais je ne suis pas sûr de moi, à vérifier. :reflexionIntense:

Arrêtons-nous un peu sur cette expression. :reflechi:

Les LPE sont un peu au lancer de rayon ce que les expressions régulières (regex) peuvent être aux chaînes de caractères. :redface:

Oui, donc il faut savoir ce que sont les regexs, mais si vous faites du lighting, je pars du principe que vous en avez déjà entendu parler. :youplaBoum:

Il y a trois types de propagation de rayons, chacune représenté par une lettre :

Et quatre types de rayons :

Note : On pourrait caler un cinquième type pour les rayons de caméras « C », mais ce dernier ne peut pas se combiner à un type de propagation (c’est un rayon de caméra, point barre… :redface: ).

Et tout ça peut se mélanger :

Pareil pour VS, VG, VD, etc. :hehe:

L’expression C<TD>.+# prend tous les rayons partant de la caméra (C), de type transmission diffuse (TD), quel que soit le type de rayon « après » <TD>.

Le « .+ » est un caractère (le point) et un quantificateur (le +) :

Le dernier caractère « # » bloque l’expression qui ne peut aller plus loin (ça ne veut pas dire que le moteur s’arrête, juste que c’est cette partie du rayon qui sera stocké en AOV).

En gros, l’expression C<TD>.+# prends la translucence indirect.

On a donc :

La Translucence direct : C<TD>#
La Translucence indirect : C<TD>.+#
Les deux : C<TD>.*#

Je ne suis pas sûr qu’une translucence direct ait le moindre sens, car la translucence est justement caractérisé par le fait qu’il s’agit d’un rayon traversant une surface.

Mettez une couleur de translucence à l’une des sphères (un vert pur, pour montrer que vous avez du goût :vomit: ) :

Puis rendez.

La Beauty :

L’AOV Translucence :

Et voilà ! En espérant que ça serve à quelqu’un !

À très bientôt !

:marioCours:

Les foules de personnages en volumétriques de Soul chez Pixar

dimanche 13 juin 2021 à 22:56

N’ayant plus beaucoup de temps pour lire des publications, je suis passé à côté d’un papier de Pixar sorti en juillet 2020 : Rasterizing Volumes and Surfaces for Crowds on Soul. De la rastérisation dans un papier de 2020 ? Intéressant… :reflechi:

Quand j’ai ouvert le papier, je m’attendais à trouver des équations mathématiques sur une nouvelle méthode ou que sais-je, mais c’est en fait la description détaillée d’un problème précis sur un plan. Tout ce que j’aime ! :baffed:

Vous allez voir que la situation que Pixar a rencontré sur ce plan a pas mal de similitude avec les problèmes qu’on peut rencontrer, dès qu’un plan un peu complexe se pointe. La différence c’est que Pixar n’est pas aussi limité que nous dans ses méthodes. Et c’est là où ce genre de document prend de la valeur : Quand on se retrouve face à des choses difficiles à sortir, on peut parfois se laisser aller à de la pensée magique comme : « À Pixar, ils auraient tout envoyé en farm ! » (mais bien sûr…). Ce papier nous prouve que non, et surtout, qu’ils n’ont pas peur de revenir sur des vieux paradigmes pour sortir leurs plans quand la contrainte (ici technique) l’impose. :redface:

Ce billet sera donc l’occasion de râler comme un vieux con, puis on va essayer de comprendre comment Pixar a géré ce plan. :banaeyouhou:

Perception de la technique

Ceux qui me connaissent savent que je ne suis pas super dogmatique vis-à-vis de la technologie et que chaque paradigme contenant le mot « moderne » oublies souvent de lister les avantages et marges de manœuvre perdues du paradigme qu’il cherche à remplacer. De plus, le fait que ces paradigmes soient poussés par des intérêts économiques rend difficile les discussions et la recherche de ce qui est perdu (c’est un peu comme si, dans une discussion sur la colorimétrie, une personne parle de « smart color », parce que c’est ce qui est écrit sur la boite de son nouveau moniteur…) :

Vous voyez l’idée…

Bien souvent ces propositions ne sortent pas de nulle-part. Elles sont cohérentes et résolvent beaucoup des problèmes des paradigmes précédents. C’est la raison pour laquelle il faut les prendre au sérieux. :hehe:

Mais comme n’importe quel paradigme, ils viennent avec leur lot de situations difficile à gérer. Comme personne ne vous en parlera, il faut souvent aller les chercher sous le capot, dans la documentation des logiciels, dans des interviews, des VFX breakdown, par la pratique, dans les bars (quand vous avez la chance de tomber sur une personne qui s’en est servi), parfois en regardant les images.

Vous pourriez découvrir que :

En gros, à l’exception de quelques-uns qui arrivent à s’imposer, les autres ne deviennent que options dans la boite à outil dont on dispose pour faire nos productions, et certains disparaissent tout simplement. C’est très bien d’avoir plus d’options pour résoudre un problème, mais ce n’est pas la même chose que sauter aveuglément dedans. Si on est incapable de lister clairement ce qu’on gagne et ce qu’on perd quand on amène une solution pour en remplacer une autre, c’est risqué. Et tout le discourt dogmatique qui peut accompagner ses solutions vise à faire sauter la question du risque, à ne pas répondre à la question. Quand tu pars sur 1 an et demi de prod comme ça, ça peut mal se passer…

Et la fameuse, répétée à outrance :

« Le temps humain, c’est plus cher que le temps machine. »

Waow… Je n’y avais vraiment pas pensé, merci d’éclairer mon chemin, je m’en vais méditer là-dessus et penser à mon avenir. :bete:

Après ce énième vomit de tonton grincheux, vous pouvez rebrancher votre cerveau, on peut y aller ! :papi:

Le plan

Le papier tourne autour d’un plan précis du film, le voici :

Regardez bien les personnages de la foule. On pourrait faire quelques remarques artistiques, mais ce n’est pas le sujet. Mettez de côté, et gardez cette image sous les yeux durant la lecture de ce billet.

Avant-propos

J’aimerais préciser que le terme « pose » utilisé dans le papier est assez difficile à traduire. Littéralement, il veut dire « prendre la pose ». Et dans le cas du rendu, il s’approche d’une idée de pré-calcul, un « bake d’information » qui semble être un process important de Pixar. J’ai pensé à utiliser le terme bake, mais il aurait prêté à confusion. Dans le cas du papier, il s’agit de baker les choses au moment du rendu (pose at render-time) de sorte que le moteur puisse s’éviter de lourds calculs au moment de tracer les rayons ; c’est très courant en temps-réel et pas du tout path-tracing, dans l’idée. Je vais conserver l’utilisation du terme pose, en italique, mais je voudrais que vous gardiez cette idée de « bake au moment du rendu » en tête. :sourit:

Autre point que je dois expliquer. Le terme « hero » fait référence à tout ce qui à trait aux personnages principaux. « Hero shader » et « Hero volume » sont respectivement des shader et des volumétriques de personnage principaux, souvent lourd et prévu pour les gros plans. Sur les grosses productions, il n’est pas rare d’avoir des morceaux de pipeline dédiée à des personnages dans le but d’augmenter sa qualité finale, mais qui ne sont pas appliqués au reste des personnages pour des raisons de coût.

Si vous n’aimez ni le mot « rasterize », ni les anglicismes, vous allez vomir en lisant ce billet, et j’en suis le premier désolé. J’ai vraiment fait ce que j’ai pu… :vomit:

Autre chose : J’alternerai entre ma traduction des paragraphes du papier (qui sera préfixée d’un gros « Traduction : ») et mes remarques.

Pour finir, gardez à l’esprit que je ne suis pas un expert de RenderMan, en particulier sur certaines notions pointues, comme les Implicit Field Plugin, les modifications de scène « at render-time », et le mécanisme de moteur embarqué (dont je soupçonne qu’aucune intégration publique n’y fait référence). Bref, je fais au mieux pour expliquer ce que je comprends des notions utilisées, mais il y aura pas mal de spéculations, pas taper… :seSentCon:

Traduction

Et on commence avec la partie abstract :

Traduction : Pour sortir les plans de Soul, contenant une foule de centaines de personnages composés de plusieurs volumes, on ne pouvait pas s’appuyer sur le pipeline de cache de pose utilisé sur les personnages principaux [Coleman et al.2020], trop lourd en IO. Nos équipes rendues et systèmes ont évalué le stockage total nécessaire au « monde des âmes » à plus de 100 To, pour une moyenne de deux personnages principaux par plans. Pour pouvoir gérer les larges foules de personnages ayant le même aspect volumétrique que les personnages principaux, tout en évitant d’atteindre cette limite d’IO, deux nouvelles techniques « at render-time » furent développées. La première s’appuie sur un rasterizer de volumétrique existant pour « poser » les volumes au moment du rendu en utilisant leur déformeur lattice. La seconde technique permettait aux primvars rasterizées de la surface d’être utilisé par le shader de volumétrique.

Voilà pour la partie abstract. :bravo:

En gros, il y a des limites/quota d’IO à respecter, et ce plan partait pour les dépasser éclater, donc il fallait trouver une solution. C’est le genre de problèmes qui doit vous parler. Il est probable que vous n’ayez pas de quotas imposés (« segfault is the limit » :smileFou: ), mais si vous avez un minimum de conscience professionnelle, anticiper des plans complexes est un classique des débuts de productions.

La première partie concerne l’économie de la bande passante des fichiers de volumétrique :

Traduction : En plus des limitations d’IO qu’aurait généré l’utilisation du pipeline de cache de pose, le temps de génération des « hero volumes » dans Houdini appliqué à la foule aurait eut un coût important en farm. Avec notre librairie maison de rasterization de volume REVES (une implémentation de l’algorithme REYES pour les volumétriques) [Wrenninge 2016], nous avons fait un pipeline autour de la gestion des volumes en rest pose et des lattices déformées. Cela nous permit de générer des volumes de façon implicite au moment du rendu pour ne pas atteindre la limite de stockage réseaux.

En gros, seuls les fichiers de rest pose des volumétriques ainsi que les lattices de déformation étaient envoyés au moteur, et c’est ce dernier qui s’occupait de générer le volume final. :petrus:

Un peu comme si, dans le cas de la géométrie, on envoyait le personnage en T-pose avec ses joints et les informations d’influence et que le moteur s’occupait de faire le skinning au moment du rendu (au passage, c’était la technique utilisée par Massive, et il me semble que c’est encore la méthode utilisée par les différents moteurs de foule, mais je ne veux pas m’avancer :jdicajdirien: ).

Notez aussi que REYES (ou du moins, son principe) semble toujours de la partie, ce qui ne me surprend qu’à moitié. :reflechi:

Ce passage sous-entend aussi que la génération des personnages principaux au rendu est faite dans Houdini.

Traduction : Au moment du build de personnage des foules, un lattice est généré pour chaque volume d’un personnage ; body, hair, face, details, et accessoires (La résolution de chaque lattice était de 15 × 15 × 15, permettant d’équilibrer la préservation des détails générés lors de la rastérization, et les performances). Le lattice est déformé par le pipeline de foules UsdSkel [Yen et al.2018], qui génère les motion samples des points de la lattice et les envois à un plugin Implicit Field de RenderMan. Muni de la lattice déformée et du volume en rest pose stoqué sur le disque, le plugin voxelise chaque field au moment du rendu, dont le field de vélocité calculée depuis les vecteurs de vélocité des points. Le field de vélocité est ensuite samplé par le moteur pour générer le motion blur des volumes. À cela s’ajoute la génération de fields min-max afin d’optimiser le tracé de rayon après la rasterization, via les algorithmes de volume de RenderMan [Fong et al. 2017]. Rasterizer la rest pose d’entrée à sa densité de voxel maximum avait un haut coût de calcul pré-pixel. Cela ralentissait le temps d’itération des lighters, nous obligeant à utiliser la technologie de LOD stochastique pour ajuster la résolution du field utilisée [Cook et al. 2007].

Il y a beaucoup de choses là-dedans. Je pense que vous avez compris l’idée. Ce qui m’a vraiment surpris, c’est la présence d’une référence à un papier de 2007 de simplification stochastique. J’ai le souvenir que ce type de mécanisme n’était pas très pratique en path-tracing, car il cassait les instances (chaque objet devient unique), et était utilisé sur Cars, à l’époque ou RenderMan était encore full REYES. On peut donc spéculer… :youplaBoum:

Soit le REYES est encore énormément ancré dans le workflow de Pixar, ce qui encore une fois, ne serait pas étonnant : Quand un pipeline est tourné vers du bake à outrance, il est difficile d’obtenir plus efficace. Comprenez que baker des PTC à la main est un problème de paysan du rendu et qu’un pipeline optimisé fait ça en sorti d’anim sans intervention humaine.

La dernière option est que cette simplification stochastique (et tous les mécanismes « at render-time ») soient fait lors d’une première passe de rendu. À titre personnel, j’ai déjà eux à implémenter un mécanisme pour déterminer la densité optimale de poils. Je passais par Maya, j’importais l’Alembic du perso et de caméra. Ensuite, pour chaque image (et motion step) je déterminais le vertex le plus proche et dans le frustrum, y plaçait une sphère de taille 1 et calculait le ratio de sa projection (sa taille) sur l’axe Y de la caméra. Je suppose qu’à Pixar, ce genre de chose est fait via des plugins du moteur, au moment du rendu. Dans le cas de la simplification stochastique, ça permettrait de simplifier une fois, pour lancer le rendu ensuite (mais ça casse quand même les instances, donc je ne sais pas… :pasClasse: ).

Vient ensuite la partie geometry rasterizer… Mais ! De la géométrie ? N’est-on pas supposé calculer des volumétriques ? Nous aurait-on menti ? :reflechi:

C’est le moment de vous resservir du thé, car on entame la partie qui saigne, celle qui fait qu’à mes yeux, les ingés de Pixar ont bien plus de classe que ceux qui affirment sans sourciller qu’ils envoient tout en farm.

Détendez-vous, prenez une inspiration, ça va chier ! :enerve:

Traduction : Bien que les personnages de la foule étaient principalement composés de volumes, les yeux étaient en meshs. L’opacité des faces volumétriques n’étant pas géré de la même manière que les surfaces, on avait l’impression que les yeux « flottaient ». L’occlusion des yeux pouvait facilement se faire via un Z-buffer, mais comme nos personnages n’étaient pas opaques et se chevauchaient en screen space, il fallait générer un buffer unique par personnage. Cette exigence nous empêchait d’utiliser RenderMan pour générer ces signaux, ce dernier n’ayant qu’un unique contexte de rendu par process, et non des centaines. Nous avons développé un rasterizer CPU (en non GPU, car notre farm est principalement en CPU) afin de générer des textures en mémoire (In-Memory Textures). Muni de ce moteur de rendu embarqué (sic), la Z-depth est rasterizé pour ensuite pouvoir tester la présence de la surface des yeux.

Je ne suis pas un expert RenderMan, mais la question de l’occlusion d’une surface à l’intérieur d’un voxel volumétrique est en effet un problème assez pointu si on veut conserver de bonnes performances. Je soupçonne que l’utilisation d’un Implicit Field Plugin empêche le moteur de pouvoir tracer l’occlusion correctement (ou bien ils sont en REYES pûr, ce qui, encore une fois, reste une option). Dans tous les cas, les yeux semblent ne pas s’intégrer correctement dans les volumétriques et ils ont dû coder un rasterizer CPU embarqué à l’intérieur de RenderMan (un moteur dans le moteur) pour générer, par personnage (et at render-time, Monsieur !), une image permettant de savoir s’il faut afficher l’œil ou non.

Le fait qu’ils aient utilisé un rasterizer donne une information intéressante, notamment le fait qu’ils ne rendent pas avec un lens shader. :reflechi:

Vous allez me dire : « Putain, mais Dorian ! Personne rends avec un lens shader, et puis c’est quoi le rapport avec le papier ?!!! :injures:  ». Et bien, comme il m’est arrivé de me retrouver dans une réunion très sérieuse (avec prodex et tout le bordel) sur le fait qu’il fallait aaaabsolument rendre avec un lens shader, je pose ça là… Si un jour ça vous arrive, l’argument Pixar avec une petite démonstration via ce papier peut vous sauver les miches, et celles de votre équipe compo…

Bref, on ne doit pas être nombreux à avoir codé un moteur dans un moteur pour générer puis injecter une texture en attribut géométrique pour ensuite la sampler dans le shader… :smileFou:

Traduction : De plus, certains fields du shading des volumes de rest pose ne pouvaient pas être facilement rasterizé par REVES. Les fields indépendants de la caméra (c.à.d la densité ou les fields de mask) ne posaient pas de problèmes, mais certains signaux (comme les gradient fields) étaient alignés sur le frustrum. À cela s’ajoutait des inquiétudes montantes concernant le temps d’attente du premier pixel (time-to-first-pixel) généré par le coût de rastérization REVES de ces fields. Nous avons utilisé le geometry rasterizer pour rendre, en texture, les propriétés de surface de chaque mesh représentant un volume. Ces textures pouvaient ensuite être connectées aux shaders de volume, comme si elles provenaient des fields. Cela a permis à nos shading artists de construire un shading network qui pouvait facilement être utilisé sur les personnages hero et foule, quelle que soit la provenance des signaux. De plus, un shader de surface séparé émulant le look de la volumétrique tirait parti d’une texture d’alpha généré par le rasterizer.

Ici, on aborde les effets sur la production. Leur système de rest pose + lattices permettait de rester sous la barre des quotas d’IO, mais intégrait des fields dont le contenu dépendait de la caméra et que REVES avait du mal à les évaluer (trop lent, je suppose). Ceci allongeait la durée de démarrage du rendu, ce qui gênait les équipes qui devait attendre avant de voir le résultat, chaque fois qu’ils modifiaient un potard. Pour résoudre le problème, ils ont dû baker les informations de volumétrique en se servant d’un mesh qu’ils ont rasterizé. De ce que j’en comprends, les volumétriques avaient une représentation en mesh (un peu comme un « volume to mesh ») et c’est ce mesh qui a été utilisé (c.à.d, rendu via le geometry rasterizer) pour générer un bake d’information (apparemment, les dégradés de couleur) qui ont été baké dans des textures (screen-space ? brickmap ? Ce papier ne le précise pas). Le shader, au lieu d’évaluer le fields, allait directement sampler la texture en question. :neutral:

Traduction : Le fait de rasterizer à la volée évitait les problèmes de synchronisation du pipeline de foule. De plus, ces signaux étaient nécessaires au light shaping, d’où le besoin de rasterizer avant le rendu, plutôt que de laisser cette responsabilité au compo. Ce geometry rasterizer dispose d’un ensemble limité de features et n’a pas pour but de remplacer les possibilités d’un moteur REYES ; C’est un rasterizer à un sample, simpliste et pas cher, conçu pour fournir des entrées à d’autres partis de la scène.

Je crois que tout est dit… :baffed:

Je comptais sauter la section suivante, « In-Memory Textures », car elle était très spécifique, mais il n’aurait manqué que ça pour que tout le papier soit traduit, donc on va essayer de faire ça bien :

Traduction : Le système (In-Texture Memory) devait gérer plusieurs textures en 512 × 512 par personnage, par frame (Il s’agit d’une résolution moyenne pour des personnages à mi-distance. Nous avons utilisé la taille du personnage en screen-space pour déterminer procéduralement la résolution nécessaire, sur un écart partant de 2048 × 2048 à 256 × 256). Afin d’éviter le coût d’écriture et de lecture de plusieurs centaines de textures par plan, les textures rasterizées étaient gardées en mémoire sous la forme d’attributs de géométries, qui pouvaient être passés à l’interface de texture Rtx de RenderMan. Nous avons écrit un plugin Rtx pour répondre aux requêtes de tile fill des buffers. Il implémentait aussi le support de la génération de mipmaps pour les accès aux textures floutées. Le sampling de texture sur les bords du volume nécessitait une dilatation du signal rasterizé. Nous compositions des mips de plus haute résolution au-dessus des mips de plus basses résolutions pour les faire « déborder ». Ce mécanisme avait l’avantage d’être rapide et temporellement plus cohérent que l’algorithme de push-pull d’OpenImageIO.

Comme vous pouvez le voir, c’est très spécifique, si vous êtes arrivé jusqu’ici, je ne vois pas trop quoi développer. :hehe:

Et arrive la conclusion du papier ! :banaeyouhou:

Conclusion

Traduction : À travers l’intégration des techniques de rasterization à l’initiation de scène de notre moteur de rendu path-tracer, nous avons été en mesure de gérer une grande quantité de personnages de foule en volumétrique dans Soul. REVES nous a permis de poser des foules de volumes at render-time, tandis que le geometry rasterizer générait les entrées de shader permettant de matcher le look d’un hero, à une fraction de son coût.

Conclusion 2, le retour du fils de la première conclusion que tu croyais qu’il y en avait qu’une seule alors qu’en fait non

On y est arrivé ! Si vous êtes encore à lire ces lignes, félicitation ! :bravo:

J’espère avoir réussi à rendre ça le plus clair possible et vous avoir motivé à jeter un œil par vous-même dans leurs publications, certaines devraient forcément vous intéresser ! :joue:

Un gros merci à Pixar de sortir ce genre de papier qui a le mérite d’attaquer un problème précis et de donner pas mal de détails en deux pages. Ça peut aussi rassurer pas mal de personnes de voir qu’il faut parfois contorsionner nos outils pour en sortir ce qu’on veut. :sourit:

Bon, je sais pas pour vous, mais quand je lis ça, je me dis qu’il est loin le path-tracing qui résout tous les problèmes de rendu et la faim dans le monde… :nannan:

On voit à quel point la réalité de Pixar est très loin de ce qu’on pourrait appeler « l’idéal RenderMan », tout de XPU path-tracing vêtu, qui ne fait pourtant que suivre et tenter de dépasser la tendance. D’un côté on a un RenderMan qui suit les tendances de ses concurrents (car c’est sur ce marché qu’il évolue) et de l’autre, un RenderMan qui sort des longs. :reflexionIntense:

C’est à la lecture de ce genre de papiers que je me dis qu’il serait vraiment intéressant que les moteurs de rendu commerciaux proposent un paradigme pré-rendu (ou préanalyse). En pratique, on le fait déjà avec les moyens du bord :

Ces mécanismes sont souvent lourds à mettre en place à la main. :reflexionIntense:

Un moteur capable de lancer un sample (ou plus) par pixel et me sortir un JSON (dixit le mec qui écrit la moitié d’un billet sur l’inefficacité des technologies modernes) de ce qui est le plus, et le moins samplé (objet ET composants de shaders), avec un ray differential moyen (ou médian), serait déjà énorme en termes d’optimisation possible. :hehe:

On arrive dans un monde sous contrainte. La question de la sur-optimisation (et son automatisation) va revenir à grands pas. On a une industrie de l’animation, on est capable de profiter de ces paradigmes, pour peu qu’ils soient exposés par les moteurs. Un tel papier ne fait que prouver qu’en pratique, le contrôle reste la mère des belles images. (Putain, c’est beau ! :neutral: )

Je reste persuadé que l’avenir du path-tracing passe par l’analyse de scène par le moteur lui-même. L’analyse de scène est le pilier du temps-réel : Plus il dispose d’informations, plus il peut optimiser et baker de choses. Si le temps réel est l’avenir, les moteurs path-tracing devraient s’en inspirer, au moins pour résoudre les problèmes les plus récurrents et évidents. :redface:

Oui, bon, à essayer de faire une conclusion un peu sérieuse je commence à dire vraiment n’importe quoi, je m’arrête là… :slow_clap:

À bientôt !

:marioCours:

Guerilla, Hair, et Back specular

dimanche 6 juin 2021 à 14:34

Aujourd’hui un billet inutile et vraiment spécifique pour vous parler d’un problème que vous ne rencontrerez sûrement jamais. :laClasse: En fait il est probable que vous ne rencontriez ce problème que sur de la série, où la contrainte de la puissance de calcul est importante. :pafOrdi:

Quand on rend des poils avec le shader Hair de Guerilla (et je suppose avec n’importe quel autre moteur de rendu) il peut être intéressant de supprimer les-dit poils du trace set « Diffuse » pour économiser leur coût de calcul dans l’indirect (BIM ! Direct dans le bain sans politesse ni respect pour son lecteur :grenadelauncher: ), mais dans ce cas précis, on risque d’obtenir un effet bizarre, presque esthétique, et je vous propose de voir ce dont il s’agit. :hehe:

Le back dans le shader Hair

Dans la vraie vie vivante qu’on partage avec ces foutus moustiques si propice à ruiner cette si chouette période de l’année :fuck: , les poils ont des propriétés physiques difficiles à modeler. Les shaders de poils ne sont pas les trucs les plus simples à comprendre et à coder :perplex: . Un petit coup d’œil sur le papier Light Scattering from Human Hair Fibers devrait vous convaincre. Et si vous êtes motivé

Au passage, je pense que le rendu temps-réel est complètement à la ramasse sur ce sujet. « Le temps-réel, c’est le futur », sauf si on a encore des cheveux, sinon : :vomit:

Si vous utilisez le shader Hair de Guerilla, dont la documentation se trouve ici (avec des potards à bouger comme dans vos logiciels préférés ! :bravo: ), vous savez qu’il se compose de trois composants de spéculaires ; primary, secondary et back (nous n’aborderons pas les paramètres Multiple Scatter et Diffuse :nannan: ).

Quand on regarde les images de la documentation officielle, on pourrait être tenté de confondre le back avec un effet de rim de light (une light, souvent forte, venant quasiment de derrière un modèle) :

Nous allons voir que le back n’a rien à voir avec une rim.

Cette image dérobée sur la page de documentation du shader Curve permet de simplifier le comportement qu’on cherche à reproduire avec ces trois composants :

Nous avons donc trois composants :

Notez à que le back est dans l’alignement de la light.

C’est ici qu’on comprend quelle différence il y a avec une rim light et que ça devient visuellement intéressant. Là où on s’imagine assez bien le comportement des deux premiers spéculaires, qui réagissent à peu près comme sur un shader de base, en miroir (c.à.d, réfléchissent la lumière en face d’eux), l’intensité du spéculaire back dépend de la lumière qui se trouve derrière. :reflechi:

En gros : Plus la lumière derrière le poil (donc devant vous) est importante, plus le spéculaire de back apparaît. C’est contre-intuitif, et c’est pourquoi je vous demande de bien observer ce schéma et de l’avoir en tête pour la suite de ce billet.

Vous l’aurez compris, c’est ce que produit le back qui va nous intéresser.

Rendu

Tout ça c’est bien joli, mais ça donne quoi en image ? :petrus:

Voici six AOV d’un rendu :

La scène est composée :

Dans l’ordre des AOVs, nous avons :

Notez que les poils cachent totalement la sphère rouge (son scalp).

On remarque que le back est sur l’AOV de Reflection et semble visuellement jouer son rôle.

Mais pourquoi l’AOV de Reflection ?

Rappelez-vous : Le back simule le spéculaire de la lumière venant de derrière le poil. Pour faire cela, le moteur va lancer un rayon comme il le ferait pour un spéculaire standard, mais il va le faire suivant la logique du spéculaire back, tout droit, devant lui (voir schéma). Le back est donc une réflexion qui part dans le sens du rayon original. Le moteur va simplement traverser le poil et récupérer ce qui se trouve derrière. S’il tombe sur d’autres poils il s’arrête, sinon, il touche le plan texturé (d’où la teinte colorée du back).

On comprend pourquoi cet effet n’est visible que sur les bords des poils : Sur les bords, un rayon a peu de chances de croiser un autre poil sur sa route lorsqu’il trace le back, mais quand on se situe au milieu et qu’on trace le back, on tombe rapidement sur un ou plus de poils, ce qui arrête son tracé. On comprend aussi que par extension, le back a un coup de calcul important, par rapport à ce qu’il apporte ; il est tracé partout, mais n’apparaît que sur les bords.

Donc si vous avez des poils très courts et très denses, il faut sérieusement se poser la question de son apport artistique avant de l’activer, car son coût est important. :redface:

En effet, le temps de rendu est de 1 min 43 sec (ça aura son importance dans la suite de ce billet).

Maintenant faisons notre super trick de la mort qui va nous permettre de sauver du temps de rendu, la production, notre salaire et draguer en discothèque : Supprimons les poils du trace set « Diffuse » (ici, via le nœud « -Diffuse ») :

(Au passage, je ne vais pas en discothèque, parce que tout le monde sait bien que)

Bref, on vire les poils du trace set « Diffuse » et on rend :

On remarque immédiatement que l’AOV de Reflection est « bizarre » et ressemble plus à une sorte de verre trouble qu’une réflexion. Si vous avez bien suivi ce qu’on a dit plus haut, vous avez peut-être compris ce qui se passe. :sauteJoie:

Je vous disais que pour calculer le back, le moteur lance un rayon de réflexion, mais orienté dans son sens original (c’est le spéculaire de back qui veut ça, voir le schéma). S’il touche un autre poil, il s’arrête, s’il ne touche pas de poil, il touche le plan (ou parfois ici, la sphère faisant office de scalp). C’est pour ça qu’en principe, ça ne fonctionne que sur les bords, le rayon ayant peu de chances de taper d’autres poils.

Sauf qu’ici, il semble que tout le spéculaire de back se soit comporté comme s’il n’avait rencontré aucun poil… Comme si, le trace set utilisé pour tracer le back ne contenait pas les poils… D’ailleurs, il utilise quoi comme trace set Guerilla pour tracer ses poils ? :petrus:

Allez dans « Hair / Advanced / Trace Set » :

Mais, les bras m’en tombent ! :bete:

Alors forcément, là vous êtes pépouze, le cul posé dans votre siège à lire un billet de blog écrit avec amour et vous comprenez peut-être ce qui se passe, mais je vous assure que quand un rendu comme ça tombe dans votre boite mail avec pour objet : « UrGeNt ! A rEnDrE pOuR aVaNt Yèr !!11! », vous envisagez sérieusement une reconversion dans le recyclage de mugs usagés au tréfonds de la Sibérie. :casseTeteMur:

Vous l’aurez compris, en virant les poils du trace set « Diffuse », vous avez purement et simplement supprimé les poils de leur propre réflexion… :slow_clap:

On peut se demander ce qu’on y gagne réellement : Après tout, il suffirait de remettre les poils dans le trace set « Diffuse » et on pourrait rendre sa production heureuse… :sourit:

C’est là qu’interviennent les temps de rendu. La dernière image, avec le poil supprimé du trace set « Diffuse » met 48 secondes à se calculer. Pour rappel, le rendu original met 1 min 43 sec, plus du double (ce n’est pas autant sur des rendus de production, mais pas loin). Ni l’un, ni l’autre n’est une solution acceptable pour la production.

Comment on fait ?

Donc je disais : La Sibérie… :seSentCon:

OK, à partir d’ici il faut commencer à manipuler des trace set :enerve: , donc si vous n’êtes pas à l’aise avec ce concept, vous pouvez lâcher ce billet en choisissant :

Et c’est gentil d’être passé ! :IFuckTheWorld:

Pour les autres, voici le moment que vous attendiez secrètement en commençant la lecture de ce billet, ce plaisir coupable qui vous prend quand vos temps de rendu foutent le camp, ce truc qui crée chez certains leads des envies de meurtre vis-à-vis de leurs graphistes partis en vacances, ce machin réfléchi et posé dans le wiki du studio avec plus grand sérieux du monde par le sup lighting en début de prod et qui n’a plus aucune réalité à la seconde où les premiers plans commencent à sortir. J’ai nommé : Les trace set. :youplaBoum:

En vrai, je taquine, car c’est loin d’être ingérable. :trollface:

En effet, comme le trace set par défaut, « Diffuse », est celui contenant tout ce qui est tracé par les rayons d’indirect (et c’est bien la raison pour laquelle on vire les poils de ce trace set), il suffit que les poils aient leur propre trace set. :idee:

On peut imaginer un trace set « Hair » (ou « Char_Hair », tant qu’à faire) contenant toutes les primitives de poil, et l’ajouter à la liste des trace set tracé par le (ou les) shader de Hair :

Avec une tel approche, les poils sont bien retirés du trace set « Diffuse » (c.à.d qu’ils ne sont pas pris en compte dans le calcul de l’indirect du reste de votre scène), mais ils sont pris en compte par le shader de Hair, et donc le back est calculé correctement. :papi:

Avec une telle approche le rendu est similaire à la première image (le back est joli tout pleins), à l’exception de l’AOV d’indirect qui devient totalement noir (les poils n’étant plus dans la diffuse) et le temps de rendu passe à 1 min 10 sec.

À vous de voir si c’est justifié. :sourit:

Conclusion

Comme le disait le poète :

Sauve ta tournette, créé un trace set.

À bientôt !

:marioCours:

Convertir des primaires sRGB en primaires ACES

jeudi 20 mai 2021 à 11:36

Bonjour à tous, dans ce petit billet nous allons voir comment convertir des primaires sRGB en primaire ACEScg. :reflechi:

Ce sera surtout un prétexte à un quelques explications sur ce qu’est une primaire, la différence entre celles de sRGB et ACEScg, dans quel cas précis on doit le faire et quand ne surtout pas le faire. :tuComprendRien:

Notez qu’il y aura du Python à la fin, toutefois le code est très simple et je pense que la première partie du billet est suffisamment intéressante pour être lu par tout le monde. :hehe:

Qu’est-ce qu’une primaire ?

Il y a plusieurs façons de décrire une couleur, mais celle qu’on utilise le plus dans notre boulot ce fait via trois composants : Rouge, vert, et bleu. :neutral:

Une primaire, c’est la relation qu’il existe entre chaque composant et la longueur d’onde de son rayonnement électromagnétique (qu’on va abréger REM, comme le groupe à succès des années 80… :slow_clap: ).

Le REM est un phénomène physique complexe (mais passionnant). Succinctement, il y a plusieurs façons de décrire un REM et on va simplement dire que c’est une onde électromagnétique qui a donc une longueur d’onde. Nos yeux sont réceptifs à certaines longueurs d’ondes (le spectre visible).

Une couleur est donc déterminée par la longueur d’onde de son REM.

En gros, « rouge pur » ça ne veut rien dire en physique optique. Le rouge du pinard n’a rien à voir avec le rouge-sang de mes yeux quand ils lisent du JS (Le monsieur dit qu’il code en Python et qu’il ne voit pas le problème :petrus: ).

Il faut donc déterminer à quelle longueur d’onde correspond chacun des composants :

Le standard sRGB spécifie que le rouge pur (la primaire rouge) à une longueur d’onde de 612 nm.

Ça veut donc dire que quand votre moniteur (qu’on suppose calibré en sRGB) affiche un pixel rouge pur (1, 0, 0), ce pixel émet en fait un REM d’une longueur d’onde de 612 nm.

Il en va de même pour le vert et le bleu, respectivement 547 et 464,5 nm.

Avec ses trois primaires, on peut donc connaître l’ensemble des couleurs (longueurs d’ondes) que peut afficher le standard. Cet ensemble s’appelle le gamut. :sourit:

Bien entendu, visualiser une longueur d’onde ce n’est pas pratique. Ainsi, il existe un système, qu’on appelle « diagramme de chromaticité » qui permet de visualiser les longueurs d’onde du spectre visible dans un plan 2D. Ce diagramme, vous l’avez sûrement déjà vu quelque part, on l’appelle parfois le « CIE xy » :

Image récupérée nonchalamment sur l’article Everything you need to know about ACEScg, du blog de Chaos Group. :gniarkgniark:

Sans rentrer dans les détails, la magie du truc réside dans la capacité à convertir une longueur d’onde en coordonnée 2D (en passant par CIE xyz, mais on va faire l’impasse là-dessus). Dans le cas du rouge pur sRGB, 612 nm se convertit en coordonnées (0,64, 0.33). Vous pouvez faire la conversion vous-même ici.

Le standard sRGB défini donc une longueur d’onde pour chaque composant, tout comme ACEScg, et le schéma ci-dessus superpose ces deux gamuts. :laClasse:

Il est maintenant temps d’expliquer le problème qu’on cherche à résoudre. :perplex:

Le problème

Comme expliqué précédemment, si vous prenez une photo de moi qui lis du JS (ça marche avec n’importe quel codeur ayant un minimum de bon goût), vous aurez donc des pixels en rouge pur (1, 0, 0) au niveau des yeux :

Si on compare le gamut sRGB à celui de ACEScg (cf. le diagramme CIE xy), on voit que les primaires rouges ne sont pas aux mêmes coordonnées :

Il ne s’agit donc pas de la même longueur d’onde, donc il ne s’agit pas du même REM, donc ce n’est pas le même rouge ! :papi:

Pour information les coordonnées CIE xy sont disponibles à la page 7 du document S-2014-004. :redface:

Pourquoi convertir manuellement ses primaires ?

Il est important de comprendre que ce besoin est très spécifique. En situation normale, vos logiciels (Guerilla, Nuke, Mari, RV, etc.) sont correctement configuré via OpenColorIO (viewer en « ACES - sRGB ») :

Et quand vous mettez une texture dans votre moteur de rendu, vous spécifiez l’espace colorimétrique en « Input - sRGB - Texture » :

En faisant ça, votre moteur va passer les valeurs de vos textures dans une LUT ACES de OpenColorIO pour, convertir les valeurs de l’espace sRGB vers l’espace ACES. D’autres informations sont disponibles ici.

Et quand vous n’utilisez pas de textures, mais directement les couleurs, le picker affiche correctement la couleur, mais surtout, c’est le rendu qui fait foi, comme quand on travaille en sRGB, sans ACES : On modifie un potard, on rend, on ajuste, etc. :hehe:

Le seul moment où vous aurez à vous poser cette question c’est quand vous récupérez un lookdev/lighting fait en sRGB et qu’il faut passer en ACEScg, en gardant le même résultat. Chose qui peut arriver en série. :mechantCrash:

Quand vous récupérez le lookdev d’un asset fait en sRGB (sans ACES, donc), la première chose à faire est de définir le bon espace colorimétrique d’entrée de vos textures (souvent « Input - sRGB - Texture »).

C’est plus compliqué dans le cas des couleurs, car il faut prendre en compte plusieurs choses : En effet, à l’inverse des textures, dans les logiciels, les attributs de couleurs n’ont pas d’espace qui leur sont assignées. C’est juste 3 valeurs (rouge, vert, bleu) sans espace pour les interpréter.

Les valeurs de cette couleur sont-elles en sRGB, ou ACEScg ? :reflexionIntense:

Les valeurs sont interprétées telles quelles : Le moteur prend les trois chiffres et met ça dans son shader. Mais comme nous l’avons vu plus haut, ces trois valeurs font références à une couleur (un REM) différente suivant l’espace colorimétrique utilisé. Il faut donc modifier ses valeurs pour qu’elle renvoie la même couleur qu’avant, le même REM.

En gros, les valeurs de notre rouge pur sRGB (1, 0, 0) vont devoir être modifiées pour s’afficher « correctement » (c.à.d, avec une longueur d’onde de 612 nm) en ACES.

J’ouvre une parenthèse : Vous pourriez être tenté de faire un script qui prends tous les attributs de couleurs et appliquer la conversion de primaire que nous allons voir plus bas, mais il reste un risque de convertir une couleur qui n’a pas vocation à être utilisé. :seSentCon:

Dès lors, c’est du bricolage, mais si vous connaissez les entrées de vos matériaux, vous pouvez vous appuyer dessus. Sur le Surface2 de Guerilla, l’attribut de couleur de la diffuse est DiffuseColor. Si vous ouvrez un lookdev d’asset fait en sRGB, vous pouvez en déduire que les primaires de la DiffuseColor doivent être convertis pour s’afficher correctement.

Fin de la parenthèse. :siffle:

Nous allons maintenant convertir un rouge pur sRGB (1, 0, 0) en ACEScg.

Comment on fait ?

Bien que ça y ressemble, il ne s’agit pas d’une simple interpolation de coordonnée dans le plan CIE xy. On doit passer par une matrice. Le site de Colour-Science propose un générateur de matrice de conversion de primaires. Allez-y et sélectionnez :

Notez que Chromatic Adaptation Transform change légerement la matrice, c’est la méthode utilisée pour « conserver » la chromaticité (la couleur) lors de la transformation. Mettez Bianco 2010, c’est suffisant. :redface:

Vous aurez cette matrice :

[[ 0.612494198536835  0.338737251923843  0.048855526064502]
 [ 0.070594251610915  0.917671483736251  0.011704306146428]
 [ 0.020727335004178  0.106882231793044  0.872338062223856]]

Je vous passe la leçon d’algèbre linéaire. Le code suivant « déroule » l’équation :

def srgb_to_acescg(r, g, b):
    return r * 0.612494198536835 + g * 0.338737251923843 + b * 0.048855526064502, \
           r * 0.070594251610915 + g * 0.917671483736251 + b * 0.011704306146428, \
           r * 0.020727335004178 + g * 0.106882231793044 + b * 0.872338062223856

La fonction srgb_to_acescg() prend les trois composant d’une couleur sRGB et renvoi cette couleur avec les primaires converties. On a donc :

>>> srgb_to_acescg(1.0, 0.0, 0.0)  # rouge pur sRGB
(0.612494198536835, 0.070594251610915, 0.020727335004178)  # equivalent ACEScg

Si vous prenez ces valeurs et que vous les mettez dans votre DiffuseColor, vous retombez sur vos pattes.

Voici trois images :

L’assombrissement des deux dernières images est dû à ACES qui garde une portion plage du moniteur (0.8 à 1.0) pour les hautes valeurs (1.0 à 16). C’est la couleur qui nous intéresse : Suivant votre moniteur, vous remarquerez que l’image du milieu tire vers le mauve. C’est encore plus flagrant quand on augmente la puissance de l’éclairage, et c’est ce que nous allons faire ! :grenadelauncher:

ACES étant fait pour gérer les dynamiques larges (de 0.0 à 16.0), il est courant d’augmenter la puissance de ses lumières. Si on le fait sur les rendus ACES (les deux derniers) pour se rapprocher de l’intensité du rendu original, on obtient ça (le premier rendu est le même qu’avant, sRGB sans ACES, il n’est mis qu’à titre de référence) :

On remarque immédiatement que le rouge non corrigé « éclate ». :baffed:

Conclusion

La colorimétrie est un sujet sans fin, ce qui le rend complexe aux non-initiés, mais comme on fait de l’image, il faut s’y coller. :pasClasse:

J’espère que ce billet vous aura plu et qu’il vous aura apporté un éclairage sur les conversions de primaires.

À bientôt !

:marioCours:

EDIT : Barrage du verbe « lire » dans les références au JS, car on ne lit pas du JS, on le… En fait, on le rien-du-tout, on le fout à la poubelle et on change de boulot. :vomit:

La ferme de rendu (seconde partie) : Les jobs

samedi 16 mai 2020 à 17:48

Dans la première partie, nous avons abordé les différents jobs qu’on peut retrouver sur une ferme de calcul. Ici nous allons parler de certains aspects tel que la notion de prédiction de données, relation aux versions, gestion du temps de chargement des scènes et des versions des logiciels et plugins.

Prédiction des données

Si vous ne deviez retenir qu’un point de ce billet ce serait celui-là ! :hehe:

Pas forcément connu, ce concept me semble crucial pour gérer les ressources de sa ferme correctement.

Le principe est de savoir exactement quelles données une chaîne de job va produire avant de générer les-dites données. Avant de vous expliquer ce que c’est, on va imaginer une gestion de ferme de calcul sans ce principe.

dessin_prediction

Si vous débutez en gestion de ferme, vous aurez tendance à exécuter des gros scripts qui s’occupe de tout faire à la volée, sans vraiment savoir sur quoi il va tomber :

Dans le cas de Guerilla :

Bim ! Comme ça, d’un coup. :grenadelauncher:

Quand tu as trois personnages légers, c’est cool et plutôt rapide, mais le gros souci de cette méthode c’est que la difficulté à gérer vos jobs augmentera de façon exponentielle au fil de la montée en complexité de vos scènes. Bah oui, si à chaque exécution du job ça recrée des versions, tu vas le sentir passer l’export qui plante et que tu dois relancer dix fois…

Vous l’aurez compris, c’est moisi. Mais qu’est-ce qu’on entend par « gérer vos jobs » ?

Déjà leur utilisation au quotidien : Plus un job est lourd et fait plein de choses, moins il est facile pour les équipes de savoir ce qui est sorti et ce qui ne l’est pas. Sans compter que la moindre chose qui pourrait faire planter le job l’oblige à recommencer entièrement (Et nous savons tous que c’est la dernière partie du job d’export du plan le plus long du projet qui plante, toujours…).

Ensuite le test par les TDs. La facilité à tester un job permet de le développer rapidement (mais ça on s’en fout, parce qu’on sait tous qu’un vrai TD ça fait des cubes et des sphères) et surtout, de le déboguer rapidement moins lentement (Et ça, on s’en fout pas…).

« C’est plus facile de faire un gros script monolithique » diront certains, et ils auront raison, mais je répondrais que « c’est plus facile à déboguer » d’en faire plusieurs petits. Le temps économisé à l’écriture du code se paie par la lourdeur des jobs. En gros, la question n’est pas d’aller vite, mais d’aller loin (C’est beau… :petrus: ).

Le pire, c’est si vos jobs créent des jobs à la volée (création dynamique). Encore une fois, tout ceci est pratique (et parfois nécessaire), mais il n’est pas toujours simple de déboguer de tels jobs.

Je le crie sur tous les toits mais l’argument « c’est plus facile à déboguer » est vraiment fondamental dans un pipeline. En particulier sur les jobs de fermes qu’il est difficile de garder rigide tant ils tendent à évoluer au fil des projets en ouvrant tout et n’importe quoi ; scènes d’animateurs™, scène de rendu, versions des logiciels différentes suivant les projets, etc. Sans compter qu’il faut, pour tester, que la ferme puisse exécuter votre code à vous (La méthode la plus simple consistant à faire passer sa machine pour un worker de la ferme).

Bref, pour pouvoir développer, tester et déboguer des jobs efficacement, il faut qu’ils soient le plus légers et rapide possible (à démarrer et à s’exécuter) et ne fasse qu’une seule et unique chose. La capacité de mettre votre ferme à l’échelle avec des projets de plus en plus gros dépends de ça.

Si vous gardez ça à l’esprit quand vous structurez vos jobs, vous allez vite vous rendre compte que le meilleur (seul ?) moyen d’avoir des jobs précis et granulaires, c’est de savoir à l’avance ce qu’il y a dans vos scènes, c’est-a-dire au moment où vous générez le graphe de dépendance de jobs.

En effet, si vous savez avant d’ouvrir une scène d’animation ce qu’il y a dedans, vous allez pouvoir faire autant de job d’export qu’il y a de personnages (ou prop) et ainsi gagner en temps.

Si vous avez trois personnages avec, pour chaque personnage, un export Alembic et une simulation automatique de cloth, vous aurez une chaîne de six jobs :

toto_001_abc -> toto_001_auto_cloth
tata_001_abc -> tata_001_auto_cloth
tutu_001_abc -> tutu_001_auto_cloth

Le petit malin du fond me fera remarquer qu’il suffit d’ouvrir la scène Maya et de générer la chaîne de job depuis cette dernière pour savoir ce qu’il y a à générer. Après lui avoir rappelé que les petits malins du fond dans son genre finissent seul, alcoolique et au chômage, je lui expliquerai qu’on a pas que ça à faire d’ouvrir des scènes Maya et que même si on le fait dans un job qui se charge d’ouvrir les scènes Maya pour générer les jobs, on ne sait pas, au moment où on génère ce fameux job ce qu’il va générer. En faisant ça, vous avez simplement décalé le problème. :reflechi:

Bref, pas le choix, il faut connaître à l’avance, au moment où on fait toute la chaîne de job.

Il y a plusieurs méthodes pour y arriver suivant votre pipeline. Voici quelques pistes :

Ces méthodes ont en commun de s’exécuter au moment de la publication afin d’avoir les informations nécessaires à disposition.

Cela implique aussi que ces informations doivent être accessible et lu au moment de la création du graph de job.

Le versionning

dessin_versioning

Comme je le disais, un des risques qu’il y a à relancer une chaîne de job non prédictible est de se retrouver avec autant de versions créées que de fois où la chaîne est relancé. Une manière d’éviter ça consiste à créer les versions au moment de la soumission du job. Ceci permet d’avoir une gestion synchrone des versions, mais c’est la ferme, asynchrone par nature, qui va les remplir, puis les « fermer ».

Un problème se pose toutefois rapidement : En effet, quand on crée une version, elle est souvent accessible immédiatement. Ainsi, si vous assemblez un plan dans la minute qui suit la publication d’un asset qui le compose (e.g. l’export d’une animation d’un personnage), le plan assemblé utilisera cette version, mais si la sauvegarde du fichier se fait en décalé (de façon asynchrone, par la ferme), vous risquez d’avoir des problèmes, car l’assembleur de scène ne comprendra pas pourquoi une version existe mais le fichier n’est pas présent et/ou invalide.

Une façon de résoudre ce problème passe par la possibilité d’activer ou de désactiver d’une version. Ainsi, une version crée mais non « fini » (le fichier n’est pas encore là), sera « désactivée » aux yeux du pipeline, et ne sera activé (et ses fichiers mis en lecture seule) qu’une fois la génération du fichier (export Alembic, rendu, playblast, etc.) terminé.

Toute version visant à être « remplis » sur la ferme est donc désactivée à sa création, et un job dédié s’occupe de l’activer quand la chaîne de job est terminé.

Dernier problème (promis) : Si on relance une chaîne de job qui s’est totalement terminé, et dont, implicitement, les fichiers sont « fermés » à la modification, il est donc nécessaire de les rouvrir. Une étape (un job) est donc également nécessaire avant la modification d’une version.

On a donc :

Il n’est pas nécessaire de passer par un job dédié pour l’ouverture et la fermeture des versions, cela peut se faire directement dans le code qui s’exécute sur la ferme. À titre personnel, je préfère des jobs dédiés, car la modification d’une version peut nécessiter que deux jobs (ou plus) lui passe dessus.

J’appelle l’ensemble de cette approche « l’allocation de versions » : À un moment t, vous demandez au pipeline des « espaces » (ici, des versions) qui vont être modifié, plus tard, par différentes machines de la ferme qui s’occuperont ensuite de les libérer. Si aucune erreur ne s’est produite lors de l’exécution de votre chaîne de job, les versions créées par la ferme sont remplis, disponibles pour le reste du pipeline et les permissions y sont correctement appliquées.

Durée d’ouverture des scènes d’animation

Je vous invite à garder à l’esprit que la durée d’ouverture des scènes d’animation peut être importante. Certains plans de certains projets sur lesquelles j’ai pu travailler mettaient quasiment 30 minutes à s’ouvrir. C’est souvent un problème à résoudre en amont (car c’est l’animateur qui perd du temps), mais comme souvent en production, ce n’est pas possible pour le projet en cours. Si vous exportez vos Alembics directement depuis la scène d’animation, ça veut dire qu’une mise à jour du rig d’un seul personnage nécessite la réouverture complète de la scène que ce soit par l’animateur ou un job de ferme, c’est du gâchis.

dessin_toutessoukontrolle

Une méthode pour éviter ça consiste à n’exporter que les courbes d’animation et de reconstruire l’ensemble en partant d’une scène vide.

Il y a plusieurs façons d’exporter des courbes d’animation, je vous en donne deux en vrac :

Une fois les courbes exportées, vous n’avez plus qu’à ouvrir une scène vide, importer la dernière version du rig et y coller vos courbes pour exporter l’Alembic.

Ces mécanismes, qui, mine de rien, ajoutent des étapes d’export, permettent de gérer des exports complexes car chaque asset est exporté individuellement.

Un dossier temporaire pour votre ferme

Il peut être pratique de disposer la ferme d’un système de fichier temporaire partagé par tous les worker. L’avantage immédiat étant de ne plus s’embêter avec des problématiques de pipeline et de version. Vous créez un dossier /farm/tmp/mon_id/ et vous y mettez ce que vous voulez. Les différents jobs liront et écriront à l’intérieur, avec, éventuellement, un job de nettoyage en fin de chaîne.

Un exemple d’utilisation que j’ai souvent observé tourne autour de l’assemblage de scènes :

On pourrait grossir cette liste d’étapes avec du précalcul de fichier (bake d’illumination, simulation et génération de poils) qui vient se faire avant le rendu. Bien que cela se fasse moins qu’il y a quelques années, le précalcul peut se révéler nécessaire pour rendre des plans qui sortent de l’ordinaire ; transitions bizarres, plans séquences, etc.

Les graphistes utilisant Houdini et ayant la responsabilité de plans complexes ainsi que les studios travaillant sur plusieurs projets de natures différentes (e.g. La publicité) s’appuient beaucoup sur ce système.

Plus un plan est complexe et spécifique, plus il est difficile de mettre en place un mécanisme de prédiction poussé. Le temps passé à généraliser une solution étant trop important et/ou le nombre de plan concerné étant trop faible.

Il y a un dernier cas où vous n’avez pas beaucoup le choix : Si la taille de votre ligne de commande est trop importante, vous devez sérialiser les arguments. Ça semble absurde, mais pour m’être déjà retrouvé dos au mur (une ligne de commande dont le nombre d’argument a augmenté au fil des semaines), écrire un JSON dans un dossier temporaire plutôt que d’avoir à repasser sur toute la gestion de la commande (on était en fin de production) peut vous sauver la mise.

Passer par un dossier partagé a le mérite de proposer une solution simple à comprendre pour tout le monde. On est clairement sur une approche artisanale de la ferme, tout en gardant une distinction claire entre ce qui est dans le pipeline et à l’extérieur.

Malgré ça, ne pas avoir de suivi granulaire des données générées par la ferme peut entraîner son lot de complication. Ainsi, une ferme multi-site devient un calvaire à gérer. Les dossiers temporaires n’étant pas au même endroit, soit vous faites un partage direct (ce qui implique une occupation continue de la bande passante), soit vous calez des jobs de synchro entre les jobs utilisant des machines de sites différents. Dans les deux cas, cela ajoute de la complexité à l’ensemble et il faut peser le pour et contre.

À titre personnel, j’ai pu remarquer que, dès lors qu’il faut synchroniser des choses, avoir un suivi précis est plus efficace : Gardez un suivi des choses à l’échelle du pipeline (asset, version, etc.) pour tout ce que vous pouvez anticiper et ne sortez la solution des fichiers temporaires de ferme que pour les exceptions.

D’un point de vue code, une petite API distribuée aux superviseurs leur évite d’avoir à gérer ça eux-mêmes : Une fonction qui crée le dossier temporaire, renvoi son chemin ainsi que le job chargé de supprimer de dossier en question est un bon début. Pour le nom du dossier temporaire généré par cette API, lui préfixer la date (2020_04_12) permet d’avoir rapidement un coup d’œil sur ce qui ne semble plus nécessaire.

Faites un suivi régulier de la taille de ces dossiers et impliquez les superviseurs qui l’utilisent, ils sont bien plus en mesure de savoir ce qui est nécessaire ou non.

Il va de soi qu’aucun autre script ne doit s’appuyer sur ce dossier. Certains pourraient avoir l’idée de s’en servir comme dossier d’échange dans le studio.

dessin_temp_farm

Nettoyage des jobs

On en a déjà un peu parlé, mais le nettoyage des jobs concerne plusieurs choses qui peuvent se faire à plusieurs moments du cycle de vie d’un job.

La plupart du temps, on aura un job dédié au nettoyage, mais certains jobs managers intègrent le concept de nettoyage en tant que paramètre du job. Cela peut prendre la forme de clef comme clean_dir, clean_file, clean_files, etc.

Des jobs génériques, ou spécifique ?

Au moment de l’écriture d’un job, une balance tend à apparaitre : Doit-on écrire un job qui fonctionnera pour tout type de tâche ? Ou doit-on écrire un job qui fonctionnera pour un besoin précis ? Doit-on gérer les choses de façon abstraite ou de façon concrète ? Ma réponse ne va pas énormément vous aider : Les deux mon capitaine !

Prenons un exemple : Vous souhaitez faire les jobs « d’export des personnages ». Exprimé de la sorte, c’est un besoin abstrait. En effet, « exporter un personnage » se fait, à priori, en plusieurs étapes :

J’ai donné une liste aussi exhaustive que possible, mais il est évidant que peu de projets cochent toutes les cases et qu’il n’est pas forcément optimal de faire un export de personnage aussi granulaire que ça (tout dépend de votre budget).

La question de savoir si ont fait des jobs pour « exporter un personnage » ou « exporter un Alembic » se pose. Si on choisit de faire un job « d’export de personnages », vous vous rendrez vite compte qu’il doit être séparé en petits jobs. Ensuite, si on choisit de faire des jobs qui « exportent un Alembic », on risque de buter, plus loin, sur des problématiques spécifiques : Est-ce qu’on exporte un Alembic de la même façon pour un personnage que pour une simulation ? Dès lors, un besoin de faire un job qui « export un Alembic FX » apparaît et il est probable qu’il n’ait pas été anticipé. Dans le pire des cas, vous vous retrouvez avec un département FX complet à qui on a dit que les jobs « d’export d’Alembic » étaient près et qu’il pouvait commencer le travail.

Si tout ceci peut sembler relever du domaine académiques, c’est parce qu’il s’agit rarement de questions qui se posent en début de production, quand on met notre ferme en place, mais plutôt quand la nature de ce que fait votre ferme évolue (milieux de production ou entre deux saisons d’une série). Ces questions, mise de côté, agissent comme un ressort.

La raison pour laquelle j’insiste sur cette distinction, c’est pour que vous soyez en mesure de la « détecter » en production, au risque de perdre du temps en enchevêtrement de paradigmes et/ou discussion avec la production.

Quand des graphistes ou des superviseurs vous parleront, ils ne feront pas cette distinction. C’est vous le codeur, c’est votre travail de la faire et de l’expliquer. Dans notre exemple, si un superviseur pense qu’il va pouvoir exporter des Alembic de FX parce que vous lui avez fait un job d’export d’Alembic sans en définir correctement le contour, vous êtes en parti responsable de cette confusion. En gros, ne « pensez » pas les jobs suivant leur nom, mais suivant ce qu’ils font, et assurez-vous que vous n’êtes pas le seul.

On peut, légitimement, se demander s’il est utile d’avoir des jobs génériques qui font des choses dont la définition n’est pas (encore ?) clair aux yeux de la production. Quand un besoin est « ballant » (comprenez, on sait qu’on en a besoin, mais on ne sait pas si c’est la bonne façon de faire), il me semble que faire un job qui fait « le truc » est la solution.

Gestion des différentes versions des logiciels sur la ferme

Suivant la nature des projets que vous avez à sortir, il est fort probable que vous ayez à gérer différentes versions de logiciels et des plugins qui les compose.

Vous pouvez définir des versions « à la machine », mais cela vous prive de puissance de calcul, car vous risquez de ne pas pouvoir utiliser une machine au simple prétexte qu’elle n’a pas la bonne version de Maya d’installée.

On serait donc tenté d’avoir plusieurs versions d’un logiciel, disponible sur chaque machine. Dès lors qu’une machine rend disponible plusieurs versions d’un logiciel, vos jobs doivent pouvoir appeler la bonne version.

Les méthodes pour y arriver sont nombreuses aucune n’est parfaite et leur utilisation dépend beaucoup de l’organisation de votre studio (rien que ça). La plus naïve consiste à appeler l’exécutable d’un logiciel par un nom précis. Dans le cas de Maya, on aurait donc une ligne de commande :

maya2019 -c "print 'toto'"

J’ai quelque à priori vis-à-vis de cette méthode. Le principal étant la gestion des versions des plugins. En effet, si vous utilisez un plugin (e.g. Yeti pour Maya), il est probable que vous ayez à glisser de version en cours de production (vous commencez les nouveaux plans avec la nouvelle version, vous finissez les anciens plans avec l’ancienne).

Je rappelle l’évidence : Il est dangereux de changer de version d’un logiciel en cours de production. En revanche, certains bugs peuvent être tellement bloquants qu’ils remettent en question ce dogme. Il peut s’agir de problèmes corrigés dans une version mineure (2.5.0 -> 2.5.1), comme de problèmes plus graves qui remettent en cause l’utilisation de l’outil. C’est une situation qui peut apparaître quand on utilise un tout nouvel outil en production et qu’on réalise, trop tard, que ce qui était prévu de faire avec n’est pas possible à cause d’un bug corrigé dans une version majeure (2.5.0 -> 3.2.0).

À cela vient s’ajouter un dernier point, pratique, qui est le test : Vous êtes en 2.5.0 et que la version 2.5.1 corrige le problème, vous passez tout le plancher en 2.5.1. Plus tard, en production, un problème, différent mais qui semble lié apparait. Vous avez un doute : Est-ce que ce problème apparaissait en 2.5.0 ?

Et là on parle d’un seul plugin, mais certains logiciels sont amenés à faire tourner pas mal de plugins différents. Dès lors, vous voyez une matrice se dessiner : Version du logiciel/Version du plugin A/Version du plugin B/etc. Et si vous utilisez des plugins compilés « maison », c’est encore pire, car vous pouvez être amené à livrer une version plusieurs fois par jour.

Il est tout à fait possible de ne s’appuyer que sur le nom de l’exécutable dans la ligne de commande :

maya2019-yeti2.5.0-rigNodeMaison3.6.5 -c "print 'toto'"
maya2019.sp1-yeti2.5.0-rigNodeMaison3.6.5 -c "print 'toto'"
maya2019.sp1-yeti2.5.1-rigNodeMaison3.6.4 -c "print 'toto'"
...

Notez que Ecosystem de PeregrinLabs fonctionne un peu suivant ce principe, mais je suis dubitatif quant à la taille de tels lignes de commandes quand vos graphistes deviennent mordu de plugins maisons. Je lui préfère une approche via variables d’environnement :

export MYENV_MAYA_VERSION=2019.sp1;
export MYENV_YETI_VERSION=2.5.0;
export MYENV_RIGNODEMAISON_VERSION=3.6.5;
myenv -- maya -c "print 'toto'"

Ici, myenv est un exécutable qui consomme les variables d’environnement commençant par MYENV_ pour en faire des environnements dans lequel la commande qui suit « -- » est exécuté.

Bien entendu, les commandes export ne sont pas exécutées par vous, mais envoyé avec le job au moment de la soumission :

my_env = {'MYENV_MAYA_VERSION': '2019.sp1',
          'MYENV_YETI_VERSION': '2.5.0',
          'MYENV_RIGNODEMAISON_VERSION': '3.6.5'}

job = {'command': 'myenv -- maya -c "print \'toto\'",
       'env': my_env}

farm.submit(job)

Je suis un adepte de cette méthode.

Arrivé ici, je ne peux pas ne pas vous parler de Rez. Je ne l’ai jamais utilisé personnellement, mais j’en ai entendu beaucoup de bien.

Rez résout des environnements via un système de dépendance. La charge cognitive initiale due à son apprentissage est importante, mais il est considéré comme une brique solide d’un pipeline avec une bonne documentation.

La gestion des environnements étant souvent central dans un studio, vous pouvez aussi être tenté d’écrire le vôtre. Ce n’est pas particulièrement difficile à faire (pour tout vous dire, je n’ai encore jamais fait autrement), mais c’est une maintenance en continue. L’avantage étant que ce gestionnaire s’en tiendra à faire exactement ce que vous voudrez, pas plus, pas moins.

Si vous choisissez de le faire en Python, gardez le module aussi isolé et indépendant que possible. S’il vous vient à l’idée de questionner la BDD sur les métadonnées d’un shot pour résoudre un environnement, vous fonctionnez peut-être à l’envers (et si vraiment vous n’avez pas le choix, passez par une variable d’environnement : MYENV_SHOT_NUMBER)

Data mining

C’est le sujet sexy du moment et il est difficile d’avoir un avis tranché tant le secteur « tâtonne » à déterminer si le temps de compréhension et d’interprétation des données justifie d’y dépenser de l’énergie.

La première erreur consisterait à utiliser des données de ferme pour répondre à des questions de production : Si vous voulez répondre à des questions de production (e.g. nombre de validation par semaine), analysez des données de production (e.g. Shotgun). En revanche il est très utile de comparer les données de ferme à des données de production, cette dernière influençant la première. Vous serez peut-être en mesure d’y trouver des corrélations intéressantes, voir, surprenantes.

Pour l’anecdote, sur un projet, on avait pu remarquer que plus on se rapproche de la fin du projet, plus le temps de rendu moyen par image diminuait, en parallèle de quelques plans qui voyaient leur temps de rendu augmenter drastiquement. On avait mis ça sur le compte du fait que, la deadline arrivant, les graphistes ne prenaient plus le risque d’augmenter les temps de rendu et forçait sur l’optimisation de leurs scènes pour éviter de les voir « revenir » de la ferme au motif d’un temps de rendu trop long, ce qui, par effet de débordement, diminuait la qualité générale. La pression qui peut apparaître en fin de production peut entraîner, à tous les niveaux de la hiérarchie, une diminution du zèle (que je crois) nécessaire à la sortie de belles images. Les superviseurs et les leads doivent alors se focaliser sur ce qui est essentiel à la qualité d’un plan. C’est d’ailleurs le moment ou le ratio qualité/temps de travail graphiste est le meilleur, les gens ayant pris l’habitude des erreurs à ne surtout pas faire et de ce qui plaît. Quant aux quelques rendus qui explosent ? C’est les laissés pour compte, les rendus qui ont un problème, mais qu’on a plus le temps de régler.

Mais je digresse…

Les données de ferme ne répondront qu’à des questions de gestion de ferme : Alimentation, charge, licence. Ces données sont très utiles pour élaborer correctement des budgets par juxtaposition. Il est, en effet, plus simple de définir les besoins d’une production quand on a déjà une base chiffrée de l’utilisation d’une ferme sur un projet.

Avoir des logs explicites

Isoler un job pour reproduire un bogue prend du temps, et ce, malgré le travail effectué pour simplifier cette démarche. Quand un problème apparaît, vous aurez rapidement le réflexe de lire le log du-dis job en le comparant avec les appels à print() de votre code. En faisant cela vous aurez rapidement une idée de votre script a planté. Et si vous avez affiché les valeurs des différentes variables, vous serez potentiellement en mesure de reproduire le problème avec une simple commande.

Il est donc important d’avoir des logs descriptif et suffisamment claires. Il ne faut pas hésiter à être verbeux, car si vous n’affichez que quelques informations, vous risquez de vous retrouver face à un gros bloc de code dans lequel le problème apparaît, sans que vous puissiez être en mesure de déterminer ce qu’il fait.

Je vous invite également à ajouter des informations de temps afin de déceler les écarts qui peuvent se produire entre deux lignes :

2018-01-22 10:53:00,500|CMD |INFO|...
2018-01-22 10:53:01,589|CMD |INFO|...
2018-01-22 10:53:01,589|CMD |INFO|...
2018-01-22 10:53:02,351|MAYA|INFO|...
2018-01-22 10:53:02,351|MAYA|INFO|...
2018-01-22 10:53:02,605|MAYA|WARN|...
2018-01-22 10:53:02,605|MAYA|INFO|...

Conclusion

J’aurais pu aborder le cloud, mais le sujet est très large et je ne pense pas avoir assez d’expérience pour donner des conseils pertinents.

J’espère que ces deux billets vous auront plus.

À bientôt !

:marioCours: