Inner Rage - intro 256 octets

(there is also an english version of this article).

Trouvant les graphismes de Mana Fortress un peu trop simplistes à mon goût, je voulais essayer de créer une scène plus complexe, qui mélangerait un ou deux sprites et des graphismes créés par le code.

Après avoir terminé l’intro, j’ai eu l’idée d’une alternative pour disposer les éléments à l’écran sans passer par une “grille” explicite, permettant (en théorie) d’économiser suffisamment d’octets pour créer une scène plus intéressante.

La Revision arrivant début Avril cette année, je voulais également tenter d’éviter le développement en mode “panique de dernière minute” de l’année précédente.

Au final, le développement m’aura pris environ un mois (fin Février -> début Avril), tout en continuant à hésiter sur les dernières décisions quelques heures avant la deadline, comme d’habitude :)

Et comme d’habitude, étant fermement dans le camp des programmeurs sorciers, j’ai fini avec quelque chose de complètement différent de ce que j’avais en tête à l’origine :)

~

L’intro et son code source sont disponibles ici et sur pouët.net, elle nécessite DOSBox avec la configuration “cycles=60000” (et un CPU relativement puissant) pour tourner avec un framerate suffisant.

Le code source complet est également disponible ici pour référence.

Inspiration

Mon idée d’origine est de créer une scène combinant un ou deux sprites relativement simple, avec des effets générés procéduralement (i.e. via le code).

En fouillant dans mes vieux favoris deviantArt, je tombe sur quelques scènes de pixel art représentant des rochers et cristaux de quartz flottant dans l’espace :

Je pense essayer de représenter une scène combinant des rochers ou cristaux (rendus avec des sprites), sur un arrière-plan relativement simple comme l’espace ou le ciel.

~

J’étudie également des scènes de Chrono Trigger et Secret of Mana (est-ce que j’ai déjà dit aimer les RPGs sur Super NES ?), en particulier le sol, les cristaux, et les cascades tombant dans le vide :

Royaume de Zeal, dans Chrono Trigger

Pays de la glace, dans Secret of Mana

Temple de l’eau, dans Secret of Mana

En étudiant les cascades de Secret of Mana, je remarque qu’il ne s’agit au final que de deux sprites de 16x16 pixels affichés l’un au dessus de l’autre, pour un rendu plutôt joli et réaliste :

À ce stade mon plan est donc :

  • essayer d’implémenter un meilleur système de grille pour disposer les éléments à l’écran,
  • créer un effet de cascade comme celui du dessus, avec du code plutôt qu’un sprite,
  • ajouter un ou plusieurs sprites à la scène si possible.

Système de grille

Contrairement à mon intro précédente, je veux coder un système qui ne nécessite pas de calculer un “identifiant de case” ni un index explicite de 0 à 255 pour adresser les pixels de chaque tuile de 16x16.

Pour je ne sais quelle raison, j’implémente directement le Rrrola’s trick, qui calcule une coordonnée X à l’écran approximative et une coordonnée Y exacte, à partir du pointeur d’écran DI :

mov ax, 0xCCCD ; constante de Rrrola
mul di         ; multiplie cette constante avec DI (0-64 000)
; DL : coordonnée X à l'écran approximée (0-255 au lieu de 0-320)
; DH : coordonnée Y à l'écran (0-200)

Ce code n’utilise que 5 octets (3 pour la constante, 2 pour le MUL), comparé à l’approche classique dans Mana Fortress, qui utilise 9 octets :

mov ax, di ; copie DI dans AX
mov bx, 320
xor dx, dx
div bx ; divise le pointeur d'écran stocké dans AX par 320
; DX : coordonnée X à l'écran (0 - 320)
; AX : cordonnée Y à l'écran (0 - 200)

Cela pose juste un ‘petit’ problème : l’intro utilise une résolution de 320x200 pixels ; comme X est approximé de 320 à 255 pixels, certaines valeurs seront fausses ou dupliquées. Cela rends impossible l’affichage correct d’un sprite, mais je ne m’en suis pas encore rendu compte à ce stade :)

Coordonnées de tuile

Pour calculer les coordonnées X/Y à l’intérieur de chaque tuile, on peut utiliser le fait qu’un ET logique (AND) est équivalent à un modulo, tant que le diviseur est une puissance de deux :

; exemple avec une tuile de 8x8 pixels

mov al, dl ; copie la coordonnée X de l'écran dans AL
and al, 0x07 ; calcule X % 8 : x = [0-7]

; y % 8 : [0 - 7]
mov bl, dh ; copie la coordonnée Y de l'écran dans BL
and bl, 0x07 ; calcule Y % 8 : y = [0-7]

shl bl, 3 ; décalage binaire à gauche de 3 (i.e. multiplie Y by 8), puisque chaque ligne fait 8 pixels
add al, bl ; ajoute Y à X

; génère une tuile de 8x8 tile avec des indices allant de 0 à 63

Au lieu d’utiliser l’instruction classique DIV pour calculer un modulo, comme dans mon intro précédente :

mov al, dl ; copie la coordonnée X de l'écran dans AL
mov bl, 8
xor dx, dx
dix bx ; divise X par 8 et récupère le reste dans DX

Cette technique permet d’économiser beaucoup d’octets comparé à mon intro précédente (qui utilisait beaucoup trop de DIV à mon goût).

Cascade ?

Le système de tuiles terminé, je tente de coder une cascade, notamment inspiré par l’animation du feu dans DOOM … et j’échoue largement :

Le bruit pseudo-aléatoire est visuellement affreux, et je n’arrive pas à coder un dégradé suffisamment convaincant. Je ne veux pas utiliser un sprite à ce stade, puisque cela consommerait trop d’espace pour ajouter quelque chose d’intéressant ensuite.

Dark Portal

Je tente de jouer avec les coordonnées, modifier les couleurs, ajouter des symétries, jusqu’à tomber là-dessus :

Pas vraiment un effet d’eau, mais en le retournant cela semble créer un effet de portail dans la partie haute de l’écran :

L’effet me rappelant l’excellent The Maw of Darkness de Julian Faylona, je me demande s’il serait possible d’ajouter une sorte de pont ou structure sur la partie basse de l’écran pour compléter la scène.

Cependant je n’arrive pas à trouver d’idée convaincante, d’autant que la transition avec la partie haute de l’écran semble difficile à “lisser”, et après quelques essais je décide de tenter d’autres variations à la place.

Bulles

J’essaie de calculer une fonction de distance à partir du centre de l’écran (i.e. en créant un cercle via la FPU), en ajoutant le résultat au calcul de couleur de chaque pixel dans une tuile.

Cela finit par créer des dégradés intéressants :

Puis des motifs ressemblants un peu à des sortes de bulles :

Puis finalement, avec de meilleurs couleurs, ce motif pas tout à fait symétrique :

… qui ressemble plus ou moins à une cascade, je suppose ? :’)

Plus d’expérimentations

Puisque l’effet précédent n’a qu’une symétrie horizontale, j’essaie d’ajouter une symmétrie verticale, puis de tester toutes les variations de couleurs, en pensant garder la meilleure version pour créer une scène statique :

C’est à ce moment que je remarque que ma palette a une erreur off-by-one, une erreur déjà faite pendant la création de la palette de mon intro Hellgate :

La palette utilise 16 couleurs, puisque les tuiles sont de 16x32 pixels, avec deux dégradés théoriquement identiques de 8 couleurs.

Le noir aurait donc dû être à la 8 et 16e position, au lieu de 7e et 14e. Et comme pour Hellgate, le rendu est plus intéressant au final, je garde donc cette palette.

~

À ce stade, l’effet est relativement intéressant, et l’intro utilise ~150 octets, donc encore pas mal d’espace ; je pense utiliser cet effet comme arrière-plan, et ajouter un sprite par-dessus.

En testant toutes les variations une par une, je remarque celle-ci, qui crée un effet intéressant et un peu “organique” :

Je réalise enfin à ce moment que c’est l’approximation du Rrrola’s trick qui crée cette discontinuité un peu étrange au milieu de l’écran, et que cela m’empêchera très probablement d’afficher un sprite correctement, puisque les calculs de pixels seront faux.

J’essaie de remplacer le calcul des coordonnées par la division exacte à la place, mais cela ruine complètement l’effet.

Modifier la palette

Réalisant que je ne pourrais probablement pas utiliser de sprite, j’essaie de créer un arrière-plan plus intéressant en utilisant une palette complète de 256 couleurs avec des dégradés, plutôt que de rester sur la palette bleue/verte de 16 couleurs.

L’idée étant d’utiliser 16 couleurs différentes d’une tuile à l’autre en fonction de la position de l’écran, et du temps.

Je passe pas mal de temps à essayer de trouver des couleurs intéressantes, échoue plusieurs fois, et plusieurs tests et maux de tête plus tard finis avec ce code :

mov dx, 0x3c8 ; 
xor ax, ax ; commence à la couleur 0
out dx, al ; sélectionne les couleurs de palette
inc dx

mov ax, 58 ; commence avec AX à 58
push ax ; sauvegarde AX pour plus tard
xor bp, bp ; utiliser BP comme compteur d'itération (0 -> 255)

color:
	out dx, al ; envoie le rouge
	
	mov bx, bp ; copie BP dans BX
	shr bx, 4 ; divise BX par 16
	shl bx, 2 ; multiplie BX par 4
	sub ax, bx ; soustrait BX à AX
	
	; si AX est négatif, mets AX (et donc le vert et bleu) à 0 à la place
	jns next2
		xor ax, ax ; AX = 0
	next2:
	
	out dx, al ; envoie le vert
	out dx, al ; envoie le bleu
	
	pop ax
	sub ax, 9 ; soustrait 9 à AX
	and ax, 0x3F ; calcule AX modulo 63, puisque 63 est l'indice de couleur maximum
	push ax ; sauvegarde AX pour la prochaine itération
	
	inc bp
loop color ; recommence pour les 256 couleurs

Ceci produit un dégradé du blanc au noir, qui progresse vers un degradé de rouge au noir plus loin dans la palette :

Vague de sang

Je remplace la palette bleue/verte de l’effet original par la nouvelle, en appliquant un décalage de couleur en fonction de la position à l’écran et du temps, créant cet effet :

J’aime immédiatement cette combinaison de l’aspect quasi-organique des bulles, de l’effet de ‘bouillonnement’ créé par l’animation, et de la transition rouge/blanc qui crée un contraste fort entre les différentes parties de l’écran.

Comme chaque pixel a une couleur différente, c’est un exemple d’intro avec un bien meilleur affichage dans DOSBox, puisque la compression vidéo a tendance à flouter tous ces détails.

J’essaie de trouver la meilleure variation en jouant avec la symmétrie et le calcul de palette :

(NOTE: les aperçus vidéos peuvent être flous les premières secondes, il est possible de revenir au début une fois le rendu net)

  • Premier test avec un défilement horizontal :
  • Défilement diagonal du coin haut/gauche vers le coin bas/droit (mon préféré) :
  • Défilement et couleurs inversés. J’aime le rendu, mais ce dernier rendrait probablement mieux avec un défilement plus lent, ou en tant que scène statique :
  • En modifiant une multiplication dans le calcul de couleurs :

Ce dernier code crée un effet organique un peu horrifique et plutôt original, mais n’ayant pas vraiment d’idée d’amélioration, je reste sur mon préféré.

Scène sous-marine

J’essaie évidemment aussi de modifier la palette elle-même, en supprimant la composante rouge et en utilisant le dégardé pour les composantes bleues et vertes à la place.

Ceci crée un effet de vague d’eau assez joli (du moins pour moi ; d’autres y ont vu quelque chose de plus végétal ou “alien”) :

Pour amplifier l’effet sous-marin, j’ajoute une couche de bleu en appliquant un XOR sur la composante bleue, donnant cette palette :

color:
	xor ax, ax ; => mets le rouge à 0 dans tous les cas
	out dx, al ; envoie le rouge
	
	pop ax ; => récupère la valeur de dégradé plus tôt que dans la palette rouge/blanc
	sub ax, bx ; => soustrait le compteur d'itération
	
	jns next2
		xor ax, ax ; => empêche le vert et le bleu de passer sous 0
	next2:
	
	out dx, al ; envoie le vert
	
	xor al, 0x09 ; => XOR sur le vert avec une constante
	out dx, al ; envoie le bleu
	
	pop ax
	sub ax, 9
	and ax, 0x3F
	push ax
	push ax
	
	inc bp
loop color

Ce qui donne ceci :

Combiner les deux scènes

La première scène (la rouge/blanche) utilise 171 octets après une passe d’optimisation, mais j’aime également la seconde scène. J’ai donc l’idée d’essayer de combiner les deux scènes avec une transition de couleurs entre les deux.

Malheureusement cela pose pas mal de problèmes :

  • la seconde scène utilise une palette différent (évidemment),
  • l’effet de ‘vague’ diagonale bouge dans le sens inverse : de bas en haut au lieu de haut en bas,
  • les ‘bulles’ qui créent cet effet d’eau bougent également dans le sens inverse,
  • la ‘vague’ diagonale est plus fine que dans l’autre scène.

Je savais que je ne pourrais pas corriger tous ces problèmes : il aurait été bizarre de changer brutalement la direction de la vague, et je ne voyais pas vraiment comment le coder de manière optimisée.

Je me concentre donc sur les couleurs, en mettant les deux palettes côte à côte et en essayant de passer progressivement de l’une à l’autre.

Je savais que ce serait difficile, mais je finis par trouver une solution au bout de quelques jours :

Cette transition crée de nouvelles variations de couleur intéressantes, je ralentit donc l’effet, puisque les intro 256 octets à la Revision peuvent être affichées pendant 1 minute environ.

J’ajuste ensuite la taille de la ‘vague’ pour qu’elle commence fine, puis s’élargisse au fil du temps :

N’ayant pas trouvé de moyen d’alterner le défilement de bas en haut vers haut en bas, je garde celui qui rends le mieux pour la palette rouge/blanc.

L’effet bleu/vert défile donc “à l’envers” comparé à sa version d’origine.

Avec ~215 octets, je suis alors complètement satisfait des graphismes, il ne me reste plus qu’un problème familier … pas de son :)

Musique

Ajouter du son fût aussi douloureux que l’année précédente : pas assez de temps, pas assez d’octets pour un motif intéressant.

Cependant j’ai cette fois une transition visuelle, donc je décide d’utiliser les même valeurs de progression à la fois pour les graphismes et le son.

Comme mon intro précédente, j’ai stocké les commandes MIDI à la fin du code :

music:
	db 0x3F ; mode UART
    db 0x99 ; NOTE ON sur le channel 9
	db 44 ; note
	db 80 ; volume
    db 0xc0 ; modifie l'instrument sur le channel 0
	db 101 ; instrument
	db 0x90 ; NOTE ON sur le channel 0
    db 35 ; note
	db 80 ; volume	

Puis je modifie le volume à chaque frame, avec le même registre utilisé pour modifier les couleurs (i.e. ajouter plus de rouge et retirer du vert et du bleu) :

; augmente progressivement le volume de la batterie
mov [si - 6], byte bl

; ajoute du rouge
mov al, 64
sub al, bl ; 64 -> 0
mov [brighten_red + 2], byte al

; go_green:
shr bl, 5 ; réduit le vert toutes les 256 frames
mov [darken_green + 2], byte bl

mov cx, 0xFF ; réinitialise la boucle de génération de palette
jmp palette ; retourne à la génération de palette, puis dessine une nouvelle frame
  • SI pointe quelque part dans les commandes MIDI (SI +/- un offset peut être moins cher qu’utiliser un label dans le code)
  • “brighten_red” et “darken_green” sont two labels dans le code de palette, qui contrôlent la quantité de couleur ajoutée,
  • j’utilise le compteur de frame BL à la fois pour augmenter le volume et changer progressivement les couleurs

Notons également que je dois regénérer la palette entière à chaque frame, ce qui coûte évidemment assez cher en temps CPU.

Composition

J’utilise deux canaux MIDI, comme d’habitude le canal 0 (instrument) et le canal 9 (batterie).

Je n’arrive pas à trouver rapidement de notes de basse satisfaisantes, donc à la place je repère et utilise un instrument de type “effet sonore”, qui boucle indéfiniment lors du déclenchement d’une note.

Puisqu’une note est jouée sur les deux canaux à chaque frame, l’astuce est simplement de mettre à 0 le volume des notes suivantes sur le canal 0, dès la première frame, pour éviter “d’empiler” les notes.

Pour la batterie, j’ai déjà un volume augmentant progressivement à ce stade, il ne me fallait plus qu’une rythmique intéressante.

J’ai cette fois testé d’utiliser des AND logiques basés sur la compteur de frame, pour déclencher soit un snare, soit un kick, soit du silence :

mov bx, bp ; copie dans BX le compteuer de temps stocké dans BP
	
cmp bx, 256
jle kick ; ne commence pas avec le snare avant la 256e frame

mov al, 40 ; la note MIDI 40 correspond au snare
test bx, 0x0F ; joue le snare toutes les fois où le compteur de frame AND 0x0F = 0, i.e. toutes les 16 frames
jz drum_note

kick:
mov al, 36 ; le note MIDI 36 correspond au kick
test bx, 0x03 ; joue le kick toutes les 4 frames
jz drum_note

salc ; xor al, al : set the MIDI note to 0 (silence)

drum_note:
mov [si + 1], al ; modifie la note dans la liste des commandes MIDI

; envoie les 8 commandes MIDI
mov cl, 8
rep outsb 

J’essaie probablement près d’une centaine de combinaisons de kick/snare à la main jusqu’à la veille de la Revision, avant de me décider quelques heures avant la deadline.

Cette technique est plus simple et moins coûteuse que l’approche avec plusieurs branches essayée dans Mana Fortress, mais beaucoup plus fastidieuse à coder car obligeant à réassembler et relancer le programme pour écouter à chaque tentative.

Je ne suis pas entièrement satisfait au final, j’aurais aimé avoir plus de place pour proposer une meilleure rythmique ou d’autres variations pour mieux aller avec les graphismes.

J’ai également remarqué que les notes MIDI sonnent largement mieux lors de la projection à la Revision par rapport à mes propres PC, ce qui m’a un peu réconcilié avec la rythmique simpliste. Cette batterie sonne définitivement metal et le DOOM original à mes oreilles :)

Optimisations de dernière minute

Comme d’habitude, l’intro pèse toujours plus de 256 octets quelques heures avant la deadline ; cette fois l’une des optimisations qui l’a sauvée a un rapport avec le calcul de symétrie :

mov cl, 7 
sub cl, bl
mov bl, cl ; bl = 7 - bl

Ce code est similaire à celui utilisé dans Mana Fortress : on veut remplacer une série d’indices comme ceci : 0 1 2 3 4 5 6 7 par une série symétrique : 7 6 5 4 3 2 1 0 ; on calcule donc 7 - X à la place.

Il se trouve que l’on peut utiliser NEG à la place de SUB, ce qui permet d’économiser UN octet entier, et un registre :

neg bl
add bl, 7

Ceci calcule en pratique -X + 7, plutôt que 7 - X.

L’optimisation avec ce NEG, combinée à d’autres techniques, finissent par réduire l’intro à une taille satisfaisante de 256 octets.

Je la nomme “Inner Rage”, puisque j’aime le contraste entre les vagues calmes du début progressant vers l’effet de “sang bouillonnant”.

Glitch de dernière minute

Après avoir fusionné les deux scènes, je me rends compte que le XOR du bleu dans la première scène crée des acents subtils de jaune dans la seconde (rouge/blanc), je décide de les conserver.

En revanche, ce dont je ne me suis pas rendu compte avant la fin du développement, c’est qu’après environ 50s, le calcul de palette passe au-dessus d’un seuil et le dégradé rouge/blanc disparaît, remplacé par des couleurs complètement étranges, qui continuent à changer jusqu’à rester coincées sur un dégradé bleu/vert similaire au début du développement (?).

Je n’ai pas vraiment le temps d’investiguer la source du comportement ni, et l’effet en lui-même est cool et inattendu, je décide donc évidemment de le conserver en prétendant qu’il était prévu depuis le début.

Le résultat final :

Conclusion

Au final je suis bien plus satisfait de cette intro comparé à l’année dernière, même si j’ai eu de la chance de tomber sur cet effet de ‘bulles’ tôt dans le développement, sans suivre le plan initial.

J’ai beaucoup aimé le rendu visuel cette fois-ci, et amélioré massivement le code comparé à Mana Fortress, je la considère donc comme ma meilleure intro actuelle au moins sur le plan technique.

Comme d’habitude, je ne suis pas vraiment sûr d’arriver à égaler ou surpasser cette intro dans le futur :)

Revision 2026

Les intros 256 octets présentées cette année sont visibles sur Youtube et les archives correspondantes sur Pouët.

Je ne savais pas vraiment à quoi m’attendre, mais le niveau global était vraiment impressionnant, probablement le plus élevé depuis la réintroduction de la compo 256 octets (Revision 2019). Plusieurs intros présentées cette année auraient gagnées sans problèmes les compétitions des années précédentes.

J’ai vraiment eu l’impression que tout le monde essayait de se surpasser cette année (même en dehors de cette compo), cela restera un très bon souvenir.

Remerciements

Remerciements à :

  • Gub pour ses optimisations salvatrices de dernière minute (comme d’habitude)
  • Coralune pour avoir vu bien trop de variations de ces effets sans être morte d’ennui
  • toutes les personnes n’ayant pas trop idée de ce qu’est une “intro 256 octets” mais ayant malgré tout donné des conseils et encouragements
  • sizecoding.org et ses optimisations fort utiles quand il est minuit passé et qu’on ne voit toujours pas comment gratter ces foutus octets
  • Ctrl-Alt-Test, Razor1911, tous les sizecoders