CODINGDEMONIAK ★ COURS d'ASSEMBLEUR par DEMONIAK (Dimanche 9 Janvier 2010 ) ★

Cours d'assembleur par Demoniak - Partie 8: La 3D en faces pleines sur CPCCoding Demoniak

La 3D en faces pleines sur CPC

J'ai eu envie d'ouvrir ce sujet parce que ça me plairai bien d'animer des objets en 3D face pleine sur CPC.
Je vais donc ici vous faire part de mes réflexions sur le sujet...

En fait la majorité des logiciels de 3D utilisent des objets définis sous la forme de "triangles".
Par exemple, un cube, composé de 6 faces carrées, peut être représenté par 12 triangles (2 triangles par faces).
Il en va ainsi pour n'importe quel objet.
Evidemment, si l'on veut des objets 'fin", on définira beaucoup plus de triangles et plus petits.

Donc l'idée m'est venue de créer une routine de tracé de triangles sur CPC. Cela permettrai ensuite de représenter (en théorie) n'importe quel objet 3D sur CPC.

J'ai donc fait quelques recherches sur le net pour trouver un algorithme de tracé de triangles rapide, pour pouvoir ensuite l'adapter au CPC.
Celui que j'ai retenu est le suivant :
On peut décomposer chaque triangle en 2 triangles, le premier ayant une base horizontale, le second ayant un côté horizontal (le suivant du premier). En fait, c'est comme si l'on "coupait" le triangle avec une ligne horizontale passant par l'un de ses sommets.
Pour cela, il faut tout d'abord "trier" les coordonnées des 3 points du triangle suivant leur ordonnées (Y)
En supposant que l'on nome les coordonnées du triangle (X1,Y1), (X2,Y2), (X3,Y3), on "coupera" donc le triangle avec une droite horizontale passant par Y2.
Ensuite, c'est assez simple. Nous avons donc deux triangles avec un côté horizontal.
Il suffit de remplir ces deux triangles avec des lignes horizontales pour obtenir le résultat voulu.

L'algorithme de remplissage de triangles

Voici l'algorithme en language C. C'est ce que je préfère, ça permet une traduction vers tous les autres langages je trouve.
Pour fonctionner, il est nécessaire que les couples de coordonnées soient triées du y le plus petit au y le plus grand.

void FillTriangle(int x1, int y1, int x2, int y2, int x3, int y3, int color) {
   int dx1 = x3 - x1;
   int dy1 = y3 - y1;
   int sgn1 = Sign(dx1);
   dx1 = Abs(dx1);
   int err1 = 0;
   int dx2 = x2 - x1;
   int dy2 = y2 - y1;
   int sgn2 = Sign(dx2);
   dx2 = Abs(dx2);
   int err2 = 0;
   int xl = x1;
   int xr = x1;
   if (y1 == y2)
      xr = x2;
   for (int y = y1; y < y3; y++) {
      DrawLine(xl, y, xr, y, color);
      err1 += dx1;
      while (err1 > dy1) {
         xl += sgn1;
         err1 -= dy1;
      }
      if (y == y2) { // On passe au tracé du second "demi-triangle"
         dx2 = x3 - x2;
         dy2 = y3 - y2;
         sgn2 = Sign(dx2);
         dx2 = Abs(dx2);
         err2 = 0;
      }
      err2 += dx2;
      while (err2 > dy2) {
         xr += sgn2;
         err2 -= dy2;
      }
   }
}

Nous verrons plus tard qu'il est facilement transciptable en assembleur Z80.

Avec cet algorithme, on peut maintenant tracer des triangles remplis avec une couleur déterminée.
Pour pouvoir tracer des objets un peu plus complexe, il suffira de tracer tous les triangles définissant ces objets.

Ensuite, seconde partie: animer un objet.
Pour cela, j'ai ajouté une option à Make3DFrame qui me permet de générer une suite de coordonnées de triangles,
pour définir un objet, et ce pour chaque image (que je nomerai "frame" par la suite).

Ensuite s'est posé un problème:
pour animer un objet, il faut donc créer une suite d'images définissant la position et/ou la forme de cet objet.
Ce pour quoi Make3DFrame est prévu, jusque là pas de problème.
Mais côté CPC, il faudra ensuite:

  • Afficher la frame 0,
  • Effacer la frame 0,
  • Afficher la frame 1,
  • Effacer la frame 1,
  • Afficher la frame 2,
  • Effacer la frame 2...

Et ainsi de suite jusqu'à la dernière frame, puis reboucler sur la première.
Dans l'idéal, On pourrait dire que l'algorithme d'animation est le suivant:

  • Effacer frame n-1
  • Afficher frame n
  • Incrémenter n
  • Si n > dernière frame, alors n=0 (pour rebouclage)

Ce qui nous fait en gros une boucle.
Sur CPC, on ajouterai même un "wait vbl" au début de la boucle, pour synchroniser notre animation avec le balayage écran.
Oui mais voilà, si la routine d'effacement + d'affichage dépasse le temps d'affichage d'une image (environ 20 milli secondes sur CPC), on obtient un clignottement très désagréable...
La routine de tracé de triangle est rapide, mais malheureusement pas assez pour permettre ce genre de chose.
Il a donc fallu utiliser une "astuce" : le double buffering.
Cette technique consiste en gros à utiliser 2 pages écran, à dessiner dans une page pendant que l'autre est affichée, puis faire un "échange" de la page affichée.
Ceci fonctionne très bien sur CPC, grace au CRTC, qui permet de définir justement l'adresse de la page écran (qui par défaut est en #C000).
Ensuite, plutôt que d'effacer "bêtement" tout l'écran, il suffit d'effacer que ce qui a été dessiné.
Pour cela j'ai donc utilisé au début la même routine de tracé de triangles, simplement en lui passant en paramètre une couleur égale à zéro.
Ceci semble logique, vu que je connais les coordonnées des triangles à effacer (ils sont dans la liste).
Mais la routine de tracé de triangles utilise pas mal le processeur, et je me suis dit qu'il serait plus rapide d'effacer un "rectangle" dans lequel serait contenus tous les triangles affichés.
Facile également, vu que l'on connait les coordonnées de tous les triangles de la "frame".
Il n'y a qu'à déterminer les bornes xmin, ymin et xmax, ymax de la frame en parcourant les coordonnées de tous les triangles.

L'algorithme de tracé de lignes horizontales

L'algorithme précédent se résumait à quelques instruction arithmétiques simples (additions, soustractions, comparaisons), mais utilisait la fonction DrawLine(x1,y1,x2,y2,color).

Cette fonction doit nous permettre de tracer une ligne verticale d'ordonnée Y1 (Y2=Y1) depuis l'abscisse X1 jusqu'à l'abscisse X2.

J'ai choisi de travailler en mode 1, car c'est le mode qui nous donnes des pixels plus ou moins "carrés".
A priori tracer une ligne horizontale en mode 1 en assembleur peut sembler facile, mais on doit quand même se poser quelques questions :

1) Déterminer l'adresse de début de la mémoire vidéo
Ceci peut se faire facilement, à l'aide de la formule suivante :

adresse écran = (X>>2)+(Y>>3)*NbCols+(Y&7)*#800


avec X l'abscisse de départ, Y l'ordonnée, NbCols le nombre de colonnes caractères (équivalent à la valeur du registre 1 du CRTC multiplié par 2). A cette adresse il faut ajouter biensur l'adresse de base de la mémoire vidéo, qui est #C000 par défaut sur le CPC.

2) Déterminer quel est le pixel de départ
Oui, car en faisant X>>2 (ou X/4), on obtient une adresse écran, mais un octet en mode 1 peut contenir 4 pixels. Notre pixel de départ correspond en fait au reste de la division entière de X/4.

3) Déterminer quel est le pixel de fin
Pareil que pour le pixel de départ, on doit savoir quand on s'arrète.

4) Choisir en fonction de la couleur les valeurs à écrire en mémoire écran
Evidemment, tracer une ligne avec le stylo 1 sera différent de tracer une ligne avec le stylo 2 ou 3.

Prenons un exemple concret:
Si je veux exécuter la fonction suivante : DrawLine( 2, 100, 10, 100, 1 )

1) Je calcule l'adresse de départ.

adresse écran = (X>>2)+(Y>>3)*NbCols+(Y&7)*#800


= (2>>2)+(100>>3)*80+(100&7)*#800
= 0 + 12*80 + 4*#800
= 9152, ou #23C0 en hexa.
Soit #23C0+#C000 = #E3C0 sur le cpc.
2) Je détermine le pixel de départ
X MOD 4 = 2 MOD 4 = 2. Le pixel de départ sera le numéro 2 (en numérotant les pixels de 0 à 3).

3) Je détermine le pixel d'arrivée
Là c'est un peu plus complexe. Je dois tracer au total 10-2 (X2-X1) soit 8 pixels.
Je commence au pixel numéro 2 sur le premier octet. Je vais donc dessiner 2 pixels (le 2 et le 3) sur le premier octet.
Octet suivant, pas de soucis, je remplis les 4 pixels.
Octet suivant, j'ai déjà tracé 6 pixels, il m'en reste donc 2.

4) Choix des octets à écrire en fonction de la couleur déterminée.
Je ne ferai pas ici un cours sur l'organisation de la mémoire vidéo du CPC en mode 1, mais un petit rappel:

  • 4 pixels en stylo 1 = #F0 (octet de mémoire vidéo)
  • 4 pixels en stylo 2 = #0F (octet de mémoire vidéo)
  • 4 pixels en stylo 3 = #FF (octet de mémoire video)

Pas facile donc de faire une fonction "universelle" de tracé de lignes en mode 1.
Sauf en utilisant une "table" de couleurs.
En gros, une table indexée par le numéro de stylo, qui contiendrai les octets à écrire en mémoire vidéo.

Voilà le principe général de cette fonction de tracé de lignes horizontales.
Pour des questions de rapidité, plutôt que de calculer l'adresse mémoire écran, j'ai préféré passer par une table précalculée.
Ceci à quand même l'inconvénient d'utiliser 400 octets (200 mots de 16 bits).

DEMONIAK

Page précédente : Cours d'assembleur par Demoniak - Partie 7 : les vecteurs système II

CPCrulez[Content Management System] v8.75-desktop/c
Page créée en 054 millisecondes et consultée 1354 fois

L'Amstrad CPC est une machine 8 bits à base d'un Z80 à 4MHz. Le premier de la gamme fut le CPC 464 en 1984, équipé d'un lecteur de cassettes intégré il se plaçait en concurrent  du Commodore C64 beaucoup plus compliqué à utiliser et plus cher. Ce fut un réel succès et sorti cette même années le CPC 664 équipé d'un lecteur de disquettes trois pouces intégré. Sa vie fut de courte durée puisqu'en 1985 il fut remplacé par le CPC 6128 qui était plus compact, plus soigné et surtout qui avait 128Ko de RAM au lieu de 64Ko.