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:

Ce qu’on appelle une « primaire » (ou « couleur primaire »), c’est la relation qu’il existe entre la valeur extrême de chaque composant (ici, rouge, vert, bleu) 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 numériques :

  • Primaire rouge (1, 0, 0)
  • Primaire verte (0, 1, 0)
  • Primaire bleue (0, 0, 1)

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 où l’on peut voir que le décalage est important. :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 :

  • Un rouge pur (1, 0, 0) en sRGB est aux coordonnées (0,64, 0,33)
  • Un rouge pur (1, 0, 0) en ACEScg est aux coordonnées (0,713, 0,293).

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:

Convertir des primaires revient simplement à changer de gamut.

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 y a un risque de convertir une couleur qui n’a pas vocation à être utilisée comme telle ; les attributs de type « couleur », c.à.d, trois valeurs, mais qui sont utilisés sur des masques (oui, c’est moche, mais la vie c’est d’la merde, il va falloir s’y habituer)… :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 :

  • Input Colourspace : sRGB
  • Output Colourspace : ACEScg
  • Chromatic Adaptation Transform : Bianco 2010
  • Formatter : str
  • Decimals : Autant que vous voulez.

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.

Vérification

Comme on parle de couleurs et d’images, il est dommage de s’arrêter là… C’est parti pour un petit rendu dans notre logiciel favoris ! :banaeyouhou:

J’ai fais une simple sphère, éclairée par une square light blanche. Seul la DiffuseColor de sa sphère change.

Voici trois images :

  • La première est la DiffuseColor rouge pur (1.0, 0.0, 0.0) en sRGB (sans ACES, Guerilla par défaut).
  • La seconde est la DiffuseColor rouge pur (1.0, 0.0, 0.0) en ACES 1.2 (le profil OpenColorIO disponible ici).
  • La troisième est la DiffuseColor rouge corrigée (0.6124, 0.0705, 0.02072) en ACES 1.2.

Sur le second rendu, on remarque que le spéculaire se fait absorber par la profondeur du rouge. :reflechi:

L’assombrissement des deux dernières images est dû à ACES qui garde une portion de la 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 dépasser la dynamique « de base » (0.0 à 1.0) et gérer les dynamiques larges (de 0.0 à 16.0), il est courant d’augmenter la puissance de ses lumières. Si on augmente la puissance des lumières sur les rendus ACES (les deux derniers) pour se rapprocher de l’intensité du rendu original, on obtient les trois images suivantes (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: