jeudi, août 04, 2016

Si j'étais graphiste ...

Si j'étais graphiste, mes sprites auraient plutôt cette trogne-là. Je saurais qu'une bonne animation, c'est une animation où on a pas peur de casser les cadres et j'arriverais à l'appliquer. Du coup, le meilleur environnement pour faire du bon travail, ce serait quelque-chose qui soit le plus souple possible. avoir suffisamment d'images disponibles simultanément pour les copier-coller et les comparaison. Bref, un outil comme SEDS ne me semblerait probablement jamais franchement pratique.

Let's be honest: I'm far from getting any close to what a real pixel artist produces. If I was, my spritesheets would look like those featured in Titus's Prehistorik II: I would not let canvas restrict myself and I would apply squash/stretch so that animations work fine. There would be significant pixels variation between frames, too. In a word, the SEDS+AnimEDS would be very unlikely to suit me. I'd need something more flexible, and in return, my game engine would need something to easily convert such flat sheets into usable assets.

Mais pour mettre des graphismes dans un jeu, faire des graphismes ne suffit pas: il faut aussi qu'ils puissent être intégré au jeu. Il faut que l'artiste puisse le plus librement possible retoucher son animation, y compris faire grandir ou rétrécir certaines images.

L'idéal pour ça, comme l'expliquait hier Eric Zmiro -- concepteur avec plus de 20 ans de métier -- c'est d'avoir le système le plus automatisé possible. Voici donc ce que j'ai compris du système qu'il décrit.

 I had the opportunity to compare the tools used in my own "School Rush" with the kind of tools and techniques that Eric Zmiro (now at Magic Pocket, and formerly developer of Prehistorik II at Titus). The core idea is to make the spritesheet-to-game data conversion as automated as possible. There cannot be any "great hotspot positioning tool" because there shouldn't be any human doing such task in first place.

From his explanations, I started to think about such automated framing tool, which is mostly a matter of separating lines that contain pixels from lines that contain none -- and having some conventions such as "an animation is bottom-aligned horizontally". Of course, there is still need to identify a reference position for each frame, so that e.g. the character's nose stays at the same position on screen while scrolling at constant speed, but a mere line of lone pixels could do the trick.


Sur base de la première planche de sprites, le premier algorithme à faire tourner est un système qui identifie les zones dans lesquelles il y a des images des zones vides. Une ou plusieurs lignes horizontale ne contenant aucun pixel (gris foncé): c'est une nouvelle animation. Une ou plusieurs lignes verticales ne contenant aucun pixels (gris clair) à l'intérieur d'une tranche d'animation: c'est une séparation entre deux étapes d'animation. On peut ensuite raffiner pour rétrécir verticalement la zone active d'un sprite.

1) on découpe les animations par planche (1 planche = 1 animation)
2) on a un outil qui capture les sprites, les convertie en bloc

3) quand un bloc existe déjà dans la banque des bloc, il ne le rajoute pas.
4) pour chaque sprite indique : [x, y, flipXY, color_index, bloc_number, size] (j'espère n'avoir rien oublié)


Si deux sprites sont identiques, ils seront partagés. Mieux, certains blocs peuvent être partagé même si ils appartiennent a des sprites différents
Encore mieux, des blocs peuvent être identique apres flip, ou être identique a une transposition de palette prêt..
Si elle me semble contre-productive, la décision "une animation par fichier" trouve tout son sens dans une équipe soumise à une date de sortie: sur une planche "prehistorik-man.png", on peut effectivement laisser le logiciel découper les bandes d'animations et faire référence à prun = pman[0][*]; pcrawl = pman[1][*]; pswim = pman[2][*]; pjump=pman[3][*]; etc. Mais imaginons qu'au terme de la production on décide finalement de laisser tomber le niveau aquatique. L'animation de nage tombe aussi et pjump devient maintenant pman[2][*]. Tous les autres animations doivent également être re-numérotée. On s'en passerait bien vu qu'il faut livrer la ROM fin de semaine pour que le jeu soit prêt début décembre...

But my understanding was yet one productivity-step below the Zmiro approach: by enforcing only one animation per sprites file, we now have a strong filename-to-game asset relationship. If the file is named pcrawl.png, then we can gather all the corresponding sprites as pcrawl[*]. When instead the 'crawl' animation is simply refered as the 5th one on pman.png, then we become vulnerable to the removal of e.g. 4th or 3rd animation, for instance because "well, the water level doesn't work, we drop the swim animation".

Yet, having the art split into multiple bitmaps doesn't mean that the tool importing them all cannot identify shared frames (or blocks) across the whole dataset. The final touch, for every frame the auto-cropper found, will be to isolate solid pixels into a minimum set of hardware-compatible blocks and pack the pixels of those blocks into tiles. The DS video chip, for instance, will only work with 8*W x 8*H blocks with W/H ratios of 0.5, 1 or 2 and a maximum size of 64*64 pixels. Even with a simple greedy algorithm, we can easily shrunk the amount of video memory required for a sprite to 1/3rd of the brute-force 64x64 sprite approach. Each batch of tiles has a companion set of OAM data that can configure hardware sprites' size, ratio position and indexes so that your 7 sprites actually look like a single, seamless cro-magnon protagonist to the player.


Reste à faire manger ça par le hardware d'une console comme la DS, qui ne fonctionne qu'avec des sprites hardware de 8x8, 16x8, 8x16, 16x16, 16x32, 32x16, 32x32, etc. jusqu'à 64x64. Sa mémoire est limitée, aussi donc avec un seul bloc de 64x64, le nombre de sprites affichables simultanément est sérieusement réduit. Ici, p.ex., on a un cro-magnon de 37x49 pixels qui prendrait 64x64. Avec un simpe algorithme de découpe "glouton", on peut le couvrir avec 4 blocs de 16x16 et 3 blocs de 8x16. Soit 6/16 de la mémoire nécessaire pour l'afficher d'un seul bloc.

AnimEDS travaille par petites éditions successives. Mais AnimEDS suppose que le contenu de la mémoire vidéo est constant. Ici, il n'y a aucun intérêt à garder un morceau de cro-magnon en mémoire si cette image-là n'est pas à l'écran. Du coup, tout ce qu'il faut c'est allouer un bloc de N tiles de sprites -- avec N assez grand pour la plus grosse étape d'animation -- et assez de blocs 'OAM' pour les contrôler. Pendant la synchronisation vidéo, c'est un nouvel ensemble "VRAM+OAM" qui est transféré simultanément pour changer les pixels que la console peut afficher et l'information sur quoi afficher où (les OAMs).

Then you need to have a game engine that can make the best use of these list of OAM datas. A real animator like those who work with Eric will easily saturate the 128K of sprite memory the DS has, so every new frame is likely to see new sprites tiles updated in VRAM together with the OAM updates. More on that in a future post.

Mais je ne suis pas graphiste. Il y aura encore pas mal d'eau coulant sous des ponts avant que je n'accouche d'un personnage pareil ... donc je vais garder SEDS et AnimEDS encore un petit moment. Par contre, si mon moteur de jeu évolue dans le bon sens, on pourrait envisager un outil d'importation des .png en .spr qui ferait ce genre de travail pour faciliter l'importation de graphismes non-SEDS dans un jeu tournant grâce à libgeds.

1 commentaire:

Sylvain Pypebros a dit…

chose amusante: le travail de la première passe ressemble en fait à un parser graphique et peut se construire sur base d'une machine d'états:

blank->data on non-transparent-pixel
data->blank on end-of-line [pixels == 0]
data->self on end-of-line [pixels >= 0] (pixels := 0)

sur base de ça, on peut imaginer utiliser p.ex. une ligne de "data" isolée comme "meta-data" (p.ex. pour les hotspots) alors qu'il faut 2 lignes ou plus pour que "data" devienne "sprite graphics"