Docs → Étape 5

Étape 5 — Validation du moteur local

Comparaison pixel à pixel avec l'API officielle. Pas de « ça a l'air bon » : des écarts chiffrés, canal par canal.

1. Protocole

Pour chaque cas : le gabarit archivé est recolorisé localement, la même combinaison est demandée à l'API, et les deux PNG sont comparés pixel par pixel et canal par canal. Six cas — cinq couleurs réalistes, plus un cas volontairement pathologique.

Les 8 pixels de la bande de calibration sont comptés à part : ce sont des métadonnées du moteur, pas des pixels de vêtement.

2. Résultats

CasCouleursPxÉcart moyenÉcart maxAlpha
top/basic#3366cc / #ffcc004600,076110
top/chef_shirt#2e8b57 / #daa5208220,007310
bottom/jeans#4682b4 / #b222224000,158310
headwear/cap#7f3fbf / #cc66331900,182510
outerwear/alien_force#b0c4de / #556b2f8300,306810
top/basic dégénéré#ff0000 / #0000ff4600,055810
Conclusion

6 cas sur 6 conformes à 1 LSB près, soit 1/255 — l'écart d'arrondi irréductible entre deux implémentations. Aucun désaccord sur le canal alpha, sur aucun cas. Le moteur reconstruit reproduit fidèlement celui de l'API.

3. Le cas dégénéré, et où l'écart se loge

Le sixième cas utilise #ff0000 et #0000ff — deux couleurs ayant exactement deux canaux à zéro. Le moteur officiel y applique parfois une rampe plus resserrée. Décomposition de l'écart :

RégionPixelsÉcart max
Vêtement4601
Bande de calibration819
Résultat rassurant

La totalité de l'écart se concentre sur les 8 pixels de la bande de calibration, dans le coin inutilisé du skin. Le vêtement lui-même reste exact à 1 LSB même avec une couleur saturée. Un utilisateur ne verrait jamais la différence.

4. Ce que j'ai refusé de coder

En caractérisant la rampe, j'ai relevé une asymétrie que je n'ai pas su expliquer. Le ratio du slot le plus sombre, mesuré par canal et par intensité :

Canal64128192255
Rouge seul0,7500,7500,8230,824
Vert seul0,7500,8200,8230,824
Bleu seul0,7500,7500,750
Gris0,8280,8200,8230,824

Le vert à 128 suit la rampe standard, le rouge et le bleu non ; à 192 le bleu diverge toujours mais plus le rouge. Ni HSL, ni HSV, ni une pondération de luma, ni une multiplication en lumière linéaire ne rendent compte de ce comportement — j'ai testé les quatre, aucun ne colle.

Décision

J'avais d'abord codé une heuristique « si deux canaux sont nuls, rampe resserrée ». Elle aggravait le résultat : l'écart moyen passait de 0,08 à 2,21, parce qu'elle se déclenchait sur #ff0000 qui, lui, suit la rampe standard. Je l'ai retirée. Une heuristique dont on ne comprend pas le domaine de validité est pire qu'une limite documentée.

5. Questions ouvertes

  • La règle exacte qui déclenche la rampe resserrée. Elle dépend à la fois du canal et de l'intensité, ce qui évoque un artefact d'implémentation — peut-être un accrochage sur une palette pour les couleurs très saturées.
  • Le saut de +35 entre le niveau de base et le premier niveau clair, alors que tous les autres pas côté clair valent +18 : un neuvième slot existe probablement dans l'outillage de l'auteur sans être utilisé dans les textures publiées.
  • Aucun layer à plus de deux zones n'a été trouvé sur les 374 variantes. La structure du manifest ne prévoit d'ailleurs que _color et _color_secondary.

6. Le moteur, en entier

Une fois le gabarit extrait, la recolorisation tient en quelques lignes. C'est tout ce que fait la page de démo :

const RATIOS = [0.823516, 0.874117, 0.929377, 1.0,
                1.136881, 1.206932, 1.277318, 1.347848];

for (chaque pixel non transparent) {
  const zone = template.R;        // 0 fixe, 1 primaire, 2 secondaire
  const slot = template.G;        // 0..7
  if (zone === 0) { recopier la couleur fixe; continue; }
  const c = (zone === 1) ? couleurPrimaire : couleurSecondaire;
  for (canal de 0 à 2)
    sortie[canal] = clamp(round(c[canal] * RATIOS[slot]), 0, 255);
  sortie.alpha = template.alpha;  // jamais modifié
}

Huit multiplications et un arrondi. Toute la difficulté était de reconstituer le gabarit, pas d'appliquer la couleur.