Revenir au plan du site

Animation avec les sprites hard

Comme évoqué dans le [ premier article sur les sprites hard ], les sprites hards possèdent leur propre mémoire qu'il faut écrire à chaque fois qu'on veut en changer le contenu. Ce ne sont pas des sprites hard "traditionnels" avec lesquels il n'y aurait que l'adresse des données à changer.

En conséquence de quoi, voyons un peu comment nous pouvons en changer le contenu :

Copie au LDIR

La copie au LDIR est la plus simple. Relativement rapide, cela prendra 1544 nops pour la copie d'un seul sprite. Avec cette méthode, il n'est pas possible de changer tous les sprites hard à l'écran dans la frame (50Hz).

Est-ce grave d'être lent? Pas obligatoirement, cette méthode peut être utilisée pour une première initialisation.
ld hl,source_de_donnees ; 3 nops
ld de,#4000 ; 3 nops
ld bc,256 ; 3 nops
ldir ; 255 x 6 + 5 nops

Copie au LDI

Même technique mais en déroulant le LDIR, on gagne quelques nops, c'est pas foufou
ld hl,source_de_donnees ; 3 nops
ld de,#4000 ; 3 nops
ld bc,256 ; 3 nops
copieSprite
ldi 32
jp p,copieSprite

Bien entendu, on ne va pas dérouler pour chaque copie, on se fera une petite fonction de copie dédiée à copier les 256 octets d'un sprite

Copie avec décompression à la volée

Dans les techniques lentes, il est intéressant de grouper les pixels par deux. En effet, les données d'un sprite n'ont que 16 valeurs possibles (transparence + 15 couleurs). Ces valeurs n'occupent que 4 bits, on peut donc avoir les informations de deux pixels dans un octet.

On descend à 2181 nops mais les données de nos sprites sont deux fois plus petites! À retenir ;)
ld hl,source_de_donnees ; 3 nops
ld de,#4000 ; 3 nops
decrunchSprite
ld a,(hl) : inc hl : ld (de),a : inc e ; 7 nops x 128
rrca : rrca : rrca : rrca : ld (de),a : inc e ; 7 nops x 128
jr nz,decrunchSprite ; 127 x 3 + 2 nops

Avec une petite table de conversion, contenant l'octet pré-shifté de 4, et en alignant les données sources sur 128, on peut gratter un peu (256 nops). Et si on déroule un peu, on gagne encore un peu plus de 300 nops.

La routine reste lente comme une copie au LDIR mais nos données sont deux fois plus petites, c'est excellent pour un usage peu exigeant.
startingindex 0
align 256
tableDecale4
repeat 256,x
defb x>>4
rend

decrunchSprite
; HL = source alignée sur 128 octets
; DE = destination dans l'ASIC
ld b,hi(tableDecale4)
.loop
repeat 8
ld a,(hl) : inc l : ld (de),a : inc e
ld c,a : ld a,(bc) : ld (de),a : inc e
rend
jr nz,.loop
ret

Générer du code avec HSPcompiler

J'ai créé en 2018 un outil qui produit du code à partir de données graphiques (utilisable sous interruption).i Cet outil est disponible sur [ mon Github ].

Il faut au préalable convertir des données au format CPC. Dans le cas des sprites hard de l'ASIC, cela correspond à des tableaux de 256 octets avec 16 valeurs possibles.

Alors j'ai bûché un peu une voiture sous photoshop, voici la planche complète qui est bien grosse. Nous avons 32k de sprites (16k avec la méthode qui groupe les pixels par deux). Vous me voyez venir avec cette planche? Vous avez une idée de ce qu'on va réaliser dans quelques articles? ^_^


Bon, d'abord, convertissons notre image en données CPC. Vous devez avoir l'habitude maintenant, on utilise convGeneric (ou Martine!)
convgeneric.exe tutureAlpha.png -hsp -meta 2x2 -c 999 -flat -g

Maintenant, nous allons travailler sur nos données avec l'outil de génération. Sur une animation comme celle-ci, on va avoir besoin de tourner à droite (passer au sprite d'à côté) ou à gauche (idem dans l'autre sens). On va indiquer à l'outil nos séquences, un label, la taille unitaire de nos données (quatre sprites pour une voiture) et c'est parti.

La première commande s'écrit ainsi, pour chaque index 0, 1, 2, 3, ... on va faire la différence avec les images d'index 1, 2, 3, 4, ... (et on termine par zéro pour faire le tour)
compiler.exe -d tutureAlpha.bin tutureAlpha.bin -idx 0-31 -idx 1-31,0 -meta 4 -l superCar -lidx > tutureAlpha.asm

Et dans l'autre sens (permuter les séquences suffit presque dans notre cas). Pour aller de 1, 2, 3, ... vers 0, 1, 2, ...
compiler.exe -d tutureAlpha.bin tutureAlpha.bin -idx 0-31 -idx 31,0-30 -meta 4 -l superCar -lidx -lstartidx 32 > tutureAlphaBack.asm

Alors les fichiers générés sont un peu gros (56k de code en tout) par contre en vitesse d'exécution, malgré des sprites non optimisés avec des aplats de couleur, une mesure de la vitesse avec Rasm donne 1231 nops pour la première fonction, soit 300 nops pour un sprite! Et on peut aller encore plus vite en retravaillant les sprites (moins de trame, moins de nuances).

Le code généré en pratique

Nous avons 64 routines qui s'appellent superCar0, superCar1, ... jusqu'à superCar63. Les 32 premières routines servent à tourner dans le sens horaire et les 32 autres dans le sens anti-horaire. Comme le point d'arrivée de chaque étape du sens horaire est celui de départ du sens anti-horaire, et vice-versa, il sera simple de naviguer dans nos routine. Charge à nous d'initialiser une fois pour toutes les deux sprites de notre voiture, et ensuite d'appeler les fonctions.

C'est trop gros!

Nos deux fichiers s'assemblent chacun en 28k et nos routines sont logiquement uniformes en taille car la rotation du véhicule est régulière. On va placer nos routines en ROM. Mais comme les ROM ne font que 16k, on va découper notre code en 4. On commence par déclarer une ROM pour snapshot avec la directive ROMBANK
ROMBANK 8 ; purement arbitraire!
org #C000 ; car connexion en tant que ROM haute!

Dans nos deux fichiers, on va ajouter devant les labels superCar16, superCar32 et superCar48 une directive 'ROMBANK NEXT' pour automatiquement occuper la ROM suivante Téléchargez les fichiers de code généré retouchés [tutureAlpha.asm] et [tutureAlphaBack.asm] puis assemblez ce simple source qui ne fait RIEN.
BUILDSNA : BANKSET 0 : ORG #38 : EI : RET
ORG #100 : RUN #100 : jr $
ROMBANK 8 : org #C000 ; purement arbitraire!
include 'tutureAlpha.asm' ; modifié pour avoir le ROMBANK NEXT
include 'tutureAlphaBack.asm' ; idem

La sortie sera la suivante
Pre-processing [hsp.asm]
Assembling
Write snapshot v3 file rasmoutput.sna
WriteSNA bank 0,1,2,3 packed
WriteSNA ROM 8 of 14210 bytes start at #C000
WriteSNA ROM 9 of 14191 bytes start at #C000
WriteSNA ROM 10 of 14162 bytes start at #C000
WriteSNA ROM 11 of 14202 bytes start at #C000
Total 4 banks (64K)
Write RASM symbol files rasmoutput.rasm

Nos 4 ROM sont bien remplies et ne débordent pas, reste à aller chercher ces routines pour les utiliser.

Ensuite, pour savoir où sont placées nos routines, le préfixe {bank} positionné devant un label nous retournera le numéro de BANK, charge à nous de connecter cette ROM avant un appel.

Par exemple, pour appeler la routine superCar13 :
ld bc,#DF00+{bank}superCar13
out (c),c
RMR ROM_UP|MODE_0 ; voir macro dans les annexes ;)
call superCar13

Bien entendu, nous n'allons pas écrire un morceau de code par étape d'animation, nous allons utiliser une table contenant le numéro de ROM et l'adresse de la routine
startingindex 0
rotation_horaire
repeat 32,x
defb {bank}supercar{x} : defw supercar{x}
rend
rotation_antihoraire
repeat 32,x
defb {bank}supercar{x+32} : defw supercar{x+32}
rend

Il vous faudra le [binaire des sprites] ainsi que les fichiers de code généré retouchés [tutureAlpha.asm] et [tutureAlphaBack.asm]
; initialisation, le grand classique, on commence à avoir l'habitude ;)
BUILDSNA : BANKSET 0 : ORG #38 : EI : RET
SNASET CPC_TYPE,4 ; modèle 6128+ conseillé
ORG #100 : RUN #100 : ld sp,#100
; macro RMR + RMR2
MODE_0 equ 0 : MODE_1 equ 1 : MODE_2 equ 2 : MODE_3 equ 3 : CLEAR_INT equ %10000
ROM_OFF equ %1100 : ROM_BOTH equ 0 : ROM_UP equ %100 : ROM_LOW equ %1000 : INTRESET equ %10000
macro RMR tags : ld a,{tags}+%10000000 : ld b,#7F : out (c),a : mend
ASICOFF equ 0 : ROM0000 equ 0 : ROM4000 equ %01000 : ROM8000 equ %10000 : ASICON equ %11000
ROM0 equ 0 : ROM1 equ 1 : ROM2 equ 2 : ROM3 equ 3 : ROM4 equ 4 : ROM5 equ 5 : ROM6 equ 6 : ROM7 equ 7
macro RMR2 tags : ld a,{tags}+%10100000 : ld b,#7F : out (c),a : mend

call UnlockAsic : RMR2 ASICON
ld hl,palette_voiture : ld de,#6422 : ld bc,30 : ldir
ld hl,superCarInit : ld de,#4000 : ld bc,1024 : ldir ; copier la première étape sur les 4 premiers sprites
; positionner les sprites
ld hl,288 : ld (#6000),hl : ld (#6010),hl ; position x
ld hl,320 : ld (#6008),hl : ld (#6018),hl ; position x
ld hl,84 : ld (#6002),hl : ld (#600A),hl ; position y
ld hl,100 : ld (#6012),hl : ld (#601A),hl ; position y
ld a,%1001 : ld (#6004),a : ld (#600C),a : ld (#6014),a : ld (#601C),a ; zoom type mode 1
RMR ROM_UP|MODE_0 ; connecter la rom HAUTE
ei
;*************************
    BouclePrincipale
;*************************
ld b,10
.attente halt : djnz .attente
call lectureMatriceClavier
ld a,(OCTET_CURSEUR_DROITE) : and BIT_CURSEUR_DROITE : jr nz,.pasDroite ; sens horaire
ld a,(position_angulaire) : ld b,0 : ld c,a : add a : add c : ld c,a ; BC = position x3
ld hl,rotation_horaire : add hl,bc : ld c,(hl) : ld b,#DF : out (c),c ; connexion ROM
inc hl : ld a,(hl) : inc hl : ld h,(hl) : ld l,a : ld (AdresseRoutine),hl
ld a,(position_angulaire) : inc a : and 31 : ld (position_angulaire),a
jp AppelCodGenSprite
.pasDroite
ld a,(OCTET_CURSEUR_GAUCHE) : and BIT_CURSEUR_GAUCHE : jr nz,.pasGauche
ld a,(position_angulaire) : ld b,0 : ld c,a : add a : add c : ld c,a ; BC = position x3
ld hl,rotation_antihoraire : add hl,bc : ld c,(hl) : ld b,#DF : out (c),c ; connexion ROM
inc hl : ld a,(hl) : inc hl : ld h,(hl) : ld l,a : ld (AdresseRoutine),hl
ld a,(position_angulaire) : dec a : and 31 : ld (position_angulaire),a
jp AppelCodGenSprite
.pasGauche
jp BouclePrincipale

AppelCodGenSprite
ld h,#40
call #1234 : AdresseRoutine=$-2
jp BouclePrincipale
position_angulaire defb 0 ; étape de départ ZERO

;---------------------------------------
OCTET_CURSEUR_HAUT equ matriceClavier : BIT_CURSEUR_HAUT equ 1
OCTET_CURSEUR_DROITE equ matriceClavier : BIT_CURSEUR_DROITE equ 2
OCTET_CURSEUR_BAS equ matriceClavier : BIT_CURSEUR_BAS equ 4
OCTET_CURSEUR_GAUCHE equ matriceClavier+1 : BIT_CURSEUR_GAUCHE equ 1
OCTET_TOUCHE_ESPACE equ matriceClavier+5 : BIT_TOUCHE_ESPACE equ 128
;---------------------------------------
lectureMatriceClavier
di ; Vous n'avez pas besoin de couper les interruptions
   ; si il n'y a pas de routine sonore sous interruption
ld hl,matriceClavier
ld bc,#f782
out (c),c
ld bc,#f40e
ld e,b
out (c),c
ld bc,#f6c0
ld d,b
out (c),c
out (c),0
ld bc,#f792
out (c),c
ld a,#40
ld c,d
.loop ld b,d
out (c),a ; sélectionner la ligne
ld b,e
ini ; lire et stocker dans notre tableau
inc a
inc c
jr nz,.loop
ld bc,#f782
out (c),c
ei ; pas besoin d'activer les interruptions si on ne les a pas coupées
ret

matriceClavier defs 10,#FF
;---------------------------------------
UnlockAsic
ld bc,#BCFF
out (c),c
out (c),0
ld hl,%1001000011101010
.loop
out (c),c
ld a,h:rlca:ld h,l:ld l,a
srl c:res 3,c
and #88
or c
ld c,a
cp #4D
jr nz,.loop
ld a,#CD
out (c),a : out (c),a
ret

startingindex 0
rotation_horaire
repeat 32,x
defb {bank}supercar{x} : defw supercar{x}
rend
rotation_antihoraire
repeat 32,x
defb {bank}supercar{x+32} : defw supercar{x+32}
rend
palette_voiture defw #000,#002,#300,#040,#440,#550,#090,#4B0,#9B0,#0F0
superCarInit incbin 'tutureAlpha.bin',0,1024 ; seulement 1024 octets soit 4 sprites ou une étape

ROMBANK 8 ; purement arbitraire!
org #C000 ; car on va la connecter en tant que ROM haute!
include 'tutureAlpha.asm' ; modifié pour avoir le ROMBANK NEXT
include 'tutureAlphaBack.asm' ; idem

Et voilà notre voiture qui peut tourner dans les deux sens avec le curseur.


L'exemple est un peu brut, on peut améliorer les appels et les retours avec les nombreuses options de compilation de l'outil (ne manque guère qu'un découpage de code en tranches de 16k hein? Promis, je m'y colle bientôt.)