Se connecter / S'enregistrer
Votre question

[Tuto BATCH+debug] métaprogrammation d'un moteur 3D logiciel

Tags :
  • Logiciels
  • Programmation
Dernière réponse : dans Programmation
a b L Programmation
18 Août 2009 22:03:38

Bonjour,

3 jours avec un PC sans internet m'a donné l'idée faire un tutoriel qui ne sert à rien mais qui montre qu'avec le batch et le programme debug.exe (présent par exemple sous windows XP pro), on peut vraiment tout faire.
Si c'est pour apprendre à programmer, autant vous dire d'abandonner le batch pour aller sur des langages plus propices à la programmation (tel que le python).

Dans l'ensemble des batchs (CRickyLib_*.bat) que je vais présenter, j'ai éviter d'imbriquer des commandes FOR et IF afin d'éviter d'activer l'expansion retardée des variables d'environnement (les !toto! au lieu des %toto%).
Je vais aussi utiliser debug.exe pour créer un programme en langage machine. J'expliquerai brièvement les instructions assembleur associées et leur fonctionnement.

Dans un premier temps, je commence par des choses simples, et je compliquerai rapidement. :) 
Voici le plan prévu:
1. conversion décimal <-> hexadécimal
2. calcul de valeur absolue
3. debug: calculer en hexadécimal
4. debug: calculer la taille d'une instruction assembleur
5. debug+metaprogrammation: génération de l'initialisation et de la finalisation d'un programme en langage machine
6. debug+metaprogrammation: génération de l'affichage d'un point 2D
7. génération de l'affichage d'un segment 2D
8. Projection d'un point 3D
9. Projection d'un segment 3D

Dans ce premier post, je vais présenter créer les fonctionnalités de bases dont j'aurais besoin.

Tout d'abord, je rappelle que la commande SET avec le commutateur /A permet de gérer des variable numériques en batch, mais l'on doit se contenter des opérations de bases pour les calculs.
Wikibooks > DOS > SET

Avec la commande SET, on peut convertir une valeur en hexadécimal en valeur décimale. Par exemple SET /A valeur=0x10 va affecteur la valeur 16 à la variable d'environnement %valeur%.
Cependant, l'opération inverse n'est pas faisable directement, alors il va falloir faire un ou deux script batch pour réaliser la conversion décimal vers hexadécimal.

Pour réaliser la conversion décimal vers hexadécimal, il faut déjà avoir écrire 1 caractère hexadécimal avec une valeur de 0 à 15 (soit 0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F).
J'ai donc créé un script prenanbt en paramètre la valeur (de 0 à 16) et qui permet d'obtenir le caractère hexadécimal associé (de 0 à F).

CRickyLib_GetNibbles.bat
  1. @ECHO off
  2. REM *** GPL License
  3. REM *** By CRicky
  4.  
  5. SET CRickyLibNibbles=
  6.  
  7. :loop
  8.  
  9. IF "%1" EQU "" GOTO end
  10.  
  11. IF "%1" EQU "0" SET CRickyLibNibbles=%CRickyLibNibbles%0
  12. IF "%1" EQU "1" SET CRickyLibNibbles=%CRickyLibNibbles%1
  13. IF "%1" EQU "2" SET CRickyLibNibbles=%CRickyLibNibbles%2
  14. IF "%1" EQU "3" SET CRickyLibNibbles=%CRickyLibNibbles%3
  15. IF "%1" EQU "4" SET CRickyLibNibbles=%CRickyLibNibbles%4
  16. IF "%1" EQU "5" SET CRickyLibNibbles=%CRickyLibNibbles%5
  17. IF "%1" EQU "6" SET CRickyLibNibbles=%CRickyLibNibbles%6
  18. IF "%1" EQU "7" SET CRickyLibNibbles=%CRickyLibNibbles%7
  19. IF "%1" EQU "8" SET CRickyLibNibbles=%CRickyLibNibbles%8
  20. IF "%1" EQU "9" SET CRickyLibNibbles=%CRickyLibNibbles%9
  21. IF "%1" EQU "10" SET CRickyLibNibbles=%CRickyLibNibbles%A
  22. IF "%1" EQU "11" SET CRickyLibNibbles=%CRickyLibNibbles%B
  23. IF "%1" EQU "12" SET CRickyLibNibbles=%CRickyLibNibbles%C
  24. IF "%1" EQU "13" SET CRickyLibNibbles=%CRickyLibNibbles%D
  25. IF "%1" EQU "14" SET CRickyLibNibbles=%CRickyLibNibbles%E
  26. IF "%1" EQU "15" SET CRickyLibNibbles=%CRickyLibNibbles%F
  27.  
  28. SHIFT
  29. GOTO loop
  30.  
  31. :end

Dans ce script, j'utilise en plus une boucle (GOTO loop) et la commande SHIFT qui permet de décaler les paramètres. Ainsi, cette boucle permet de boucler sur tous les paramètres du script, et s'arrête lorsque %1 est vide, c'est à dire que l'on a décalé tous les paramètres et qu'il n'y en a plus.

Voici un exemple d'utilisation:
  1. @echo off
  2.  
  3. CALL CRickyLib_GetNibbles 1 4 10 15
  4. ECHO (1, 4, 10, 15) = (%CRickyLibNibbles%)
  5.  
  6. pause

On lance le batch CRickyLib_GetNibbles.bat avec les paramètres 1 4 10 et 15, et le résultat est mis dans %CRickyLibNibbles%. Etant donné les conversions 1d=1h, 4d=4h, 10d=Ah et 15d=Fh, alors on aura %CRickyLibNibbles%=14AF.

Maintenant que l'on sait traduire les caractères hexadécimaux, il faut convertir un nombre décimal supérieur à 16. C'est un simple changement de base de 10 vers 16.
Pour me faciliter la vie, je vais me contenter d'un maximum de 4 caractères hexadécimaux (h1 h2 h3 h4), soit une limite décimale de 16^4 = 65536.
Il faut décomposer un nombre N en base 16: N=h4 + h3 * 16 + h2 * 16^2 + h1 * 16^3
N=h4 + h3*16 + h2*256 + h1*4096.
J'applique des division entier pour décomposer les valeurs.
Pour trouver h1, il suffit de faire le calculs en valeurs entières N / 4096. Ensuite, pour calculer h2, on prend N - h1*4096 = h4 + h3*16 + h2*256 et on fait la même division entière pour obtenir h2
h2 = (N - h1*4096) / 256
et ainsi de suite
h3 = (N - h1*4096 - h2*256) / 16
h4 = N - h1*4096 - h2*256 - h3*16

en suite lorsqu'on a calculé les h1, h2, h3 et h4, on a des valeurs de 0 à 16. Il suffit alors d'utiliser le script précédent.

Le script suivant prend en paramètre un nombre décimal compris entre 0 et 65535 inclus, le convertit en hexadécimal et place le résultat dans la variable d'environnement CRickyLibHexa
CRickyLib_Conv_Dec2Hexa.bat
  1. @ECHO off
  2. REM *** GPL License
  3. REM *** By CRicky
  4.  
  5. SET /A CRickyLibHexaN0=%1/4096
  6. SET /A CRickyLibHexaN1=(%1-%CRickyLibHexaN0%*4096)/256
  7. SET /A CRickyLibHexaN2=(%1-%CRickyLibHexaN1%*256-%CRickyLibHexaN0%*4096)/16
  8. SET /A CRickyLibHexaN3=%1-%CRickyLibHexaN2%*16-%CRickyLibHexaN1%*256-%CRickyLibHexaN0%*4096
  9.  
  10. CALL CRickyLib_GetNibbles %CRickyLibHexaN0% %CRickyLibHexaN1% %CRickyLibHexaN2% %CRickyLibHexaN3%
  11.  
  12. SET CRickyLibHexa=%CRickyLibNibbles%


Voici un exemple d'utilisation:
  1. CALL CRickyLib_Conv_Dec2Hexa 5243
  2. echo 5243 = %CRickyLibHexa%h
  3.  
  4. SET /A test3=0x%CRickyLibHexa%
  5. echo %CRickyLibHexa%h = %test3%
  6.  
  7. pause

Je montre aussi comment convertir simplement de l'hexadécimal vers du décimal en utilisant SET /A ...=0x...

Dans un prochain post, j'expliquerai une méthode simple de calcul de la valeur absolue d'un nombre: Abs(-x)=x, où x est un nombre.

Autres pages sur : tuto batch debug metaprogrammation moteur logiciel

a b L Programmation
19 Août 2009 20:59:11

Calcul de valeur absolue

Calculer une valeur absolue en batch est en fait relativement simple car il suffit d'agir sur la variable comme une chaine de caractères et non comme une valeur. Il suffit alors d'éliminer le caractère - de la variable.

CRickyLib_Abs.bat
  1. @ECHO off
  2. REM *** GPL License
  3. REM *** By CRicky
  4.  
  5. FOR /F "tokens=1* usebackq delims=-" %%A in (`echo %1`) do (
  6. SET CRickyLibAbs=%%A%%B
  7. )

J'utilise un FOR /F sur une commande batch (echo %1) pour découper la ligne. Il n'y a pas de boucle puisque la commande ECHO ne retourne qu'une seule ligne.
Dans les options, "delims=-" permet d'indiquer que le séparateur de champs est le caractère "-", et "tokens=1*" permet de prendre le premier champ (1) et le reste (*) dans, respectivement, %%A et %%B. Comme seules les partie découpées par le délimteur sont conserver, le délimiteur est perdu. Il suffit donc de reprendre %%A et %%B pour reformer la chaine sans le caractère "-".

Un exxemple d'utilisation:
  1. CALL CRickyLib_Abs 30
  2. echo ABS(30) = %CRickyLibAbs%
  3.  
  4. CALL CRickyLib_Abs -130
  5. echo ABS(-130) = %CRickyLibAbs%
  6.  
  7. pause


Dans un prochain post, j'expliquerai une méthode simple avec debug.exe pour faire un calcul d'addition et de soustraction de 2 nombres en hexadécimal.
a b L Programmation
20 Août 2009 20:44:03

Debug.exe: calculer en hexadécimal
debug est un programme qui permet d'analyser et faire un programme en assembleur ou directement en langage machine.

Debug.exe lance une sorte de console dans laquelle on entre les commandes de debug. Il suffit de taper ? pour avoir la liste des actions.

Pour commencer simplement, je vais utiliser la fonctionnalité H valeur1 valeur2. Celle-ci calcul la somme et la soustraction de valeurs hexadécimales. Par exemple, "H 1A 10" va donner comme résultat 002A 000A. Le premier est la somme des nombre, et le second et la soustraction.

Le problème de debug.exe est son utilisation dans le batch. Ce programme lance sa console qui est en fait un programme. Donc, si dans le batch, on met simplement la commande debug, les commandes batch suivantes ne seront exécutées que lorsque l'utilisateur quitte la console debug (commande q).
La technique pour utiliser les commandes debug est la redirection de l'entrée standard (le clavier) à partir d'un fichier avec la commande batch: debug < liste_de_commandes_debug.txt
Ainsi, dans le fichier liste_de_commandes_debug.txt, on y met tout ce que l'on taperais au clavier. C'est à dire que si l'on tapait dans debug à la main un espace suivi d'un appuie sur entrée, il suffit de mettre un espace dans le fichier suivi d'une nouvelle ligne.
Maintenant, il faut savoir comment créer le fichier liste_de_commandes_debug.txt. Pour cela, il suffit d'utiliser la redirection de la sortie standard (le texte écran à l'écran) vers un fichier.
Par exemple, si l'on tape la commande batch "DIR > resultat_dir.txt" alors on retrouvera l'affichage de DIR dans resultat_dir.txt. Si l'on tape "ECHO q > commandes.txt", le fichier contiendra le caractère "q" (la commande pour quitter la console debug).

CRickyLib_Hexa.bat
  1. @ECHO off
  2. REM *** GPL License
  3. REM *** By CRicky
  4.  
  5. ECHO h %1 %2 > CRickyLibHexa_temp.txt
  6. ECHO q >> CRickyLibHexa_temp.txt
  7.  
  8. debug < CRickyLibHexa_temp.txt > CRickyLibHexa_temp2.txt
  9.  
  10. FOR /F "eol=- skip=1 tokens=1,2" %%A IN (CRickyLibHexa_temp2.txt) DO (
  11. SET CRickyLibHexaAdd=%%A
  12. SET CRickyLibHexaSub=%%B
  13. )

Ce script prend 2 paramètres %1 et %2 qui contiennent les 2 valeurs hexadécimales à ajouter et soustraire.
CRickyLibHexa_temp.txt est construit pour contenir les commandes h _ _ (calcul hexa de debug) et q (pour quitter la console debug et reprendre la main dans le batch).
On redirige le clavier à partir de CRickyLibHexa_temp.txt, et on y met tout le résultat affiché dans CRickyLibHexa_temp2.txt.
Enfin, on utilise un FOR /F pour boucler sur les lignes du fichier et les analyser.

Pour expliquer l'utilisation du FOR /F ici, un exemple d'utilisation:
  1. SET X=10A
  2. SET Y=B
  3.  
  4. CALL CRickyLib_Hexa %X% %Y%
  5.  
  6. ECHO %X%h + %Y%h = %CRickyLibHexaAdd%
  7. ECHO %X%h - %Y%h = %CRickyLibHexaSub%
  8.  
  9. PAUSE

On appelle CRickyLib_Hexa.bat avec les valeurs hexadécimales 10A et B. Ainsi, on obtient le fichier de commandes debug suivant:
h 10A B
q


Ce qui donne pour résultat:
-h 10A B
0115 00FF
-q

le FOR /F boucle sur les lignes, mais seul un découpage de la ligne 2 est intéressant. L'option "skip=1" permet de ne commencer qu'à partir de la ligne 2, et l'option "eol=-" permet d'indiquer que le caractère "-" est aussi un caractère de fin de ligne, c'est un commentaire, c'est à dire que tout ce qui suit "-" ("-" compris) n'est pas analysé. Ainsi, seule la seconde ligne est découpée. Le délimiteur par défaut du FOR /F est l'espace et tabulation, et c'est ce que l'on va utiliser donc, on ne redéfinit pas l'option "delims".
On indique enfin l'option "tokens=1,2" pour prendre les 2 champs séparés par l'espace. On aurait pu aussi mettre ici "tokens=1*" puisqu'il n'y a que 2 champs.

Bien, on a maintenant notre script batch pour faire le calcul, mais ce script nous génère 2 fichiers temporaires, il faut les supprimer pour être propre.
CRickyLib_Clean.bat
  1. @ECHO off
  2. REM *** GPL License
  3. REM *** By CRicky
  4.  
  5. DEL CRickyLibHexa_temp.txt
  6. DEL CRickyLibHexa_temp2.txt
  7. EXIT


Il y a ici une petite particularité pour l'appel de ce script. On pourrait l'appeler avec un CALL CRickyLib_Clean.bat, mais ça ne fonctionnera pas bien car le processus debug.exe lancé dans l'interpréteur de commande verrouille encore le fichier, ce qui fait échouer la suppression du fichier CRickyLibHexa_temp.txt.
L'astuce pour le supprimer est de le lancer dans un autre interpréteur batch en parallèle avec la commande START. Cette commande, contrairement à la commande CALL, n'attend pas de retour, c'est-à-dire que lorsque le START est lancé, le script CRickyLib_Clean.bat est lancé, mais le script batch continue sont fonctionnement en même temps. Donc, si l'on place la commande START à la fin de notre fichier batch, alors un interpréteur de commande se lance pour exécuter CRickyLib_Clean.bat, et dans le même temps, l'interpréteur de commande actuel (qui a lancé debug) se termine. Du coup, comme celui-ci se termine, le processus déverrouille le fichier, et CRickyLib_Clean.bat peut effacer tranquillement nos fichiers.
Il est important, dans notre cas, de ne mettre aucune commande après le START, car ça ferait ralentir la fermeture du processus, et donc le fichier CRickyLibHexa_temp.txt risquerait d'être encore verrouillé lors de sa suppression dans l'autre processus. Et donc, si l'on veut mettre une commande batch PAUSE, elle est à mettre avec le START.

Exemple d'utilisation propre:
  1. SET X=10A
  2. SET Y=B
  3.  
  4. CALL CRickyLib_Hexa %X% %Y%
  5.  
  6. ECHO %X%h + %Y%h = %CRickyLibHexaAdd%
  7. ECHO %X%h - %Y%h = %CRickyLibHexaSub%
  8.  
  9. PAUSE
  10. START CRickyLib_Clean.bat


Dans un prochain post, j'expliquerai comment obtenir la taille d'une instruction binaire x86 à partir de l'instruction assembleur correspondante, en utilisant le principe d'utilisation de debug.
a b L Programmation
21 Août 2009 20:00:31

Une fonctionnalité (commande "a") de debug est de permettre d'entrer une instruction assembleur, directement décodée en langage machine, mis en mémoire et exécutable immédiatement.
Le batch que je vais présenter ici ne va pas tellement servir pour la suite, mais il permet de voir une manipulation plus avancée de debug, voir la manipulation des paramètres d'un batch et revoir l'analyse d'un fichier texte.
Avant de commencer, il faut savoir que dans debug, toutes les valeurs sont en hexadécimal, donc 11 est en fait 11h=16+1=17.

Je souhaite savoir la taille d'une instruction assembleur JMP 120. Cette instruction permet de sauter d'une position d'un programme à une autre (fonctionnellement, c'est l'équivalent du GOTO en batch). Lorsqu'on lance un programme, il est chargé en mémoire. La position d'un octet en mémoire est identifié par une adresse. Par exemple l'adresse 120 est le 120ème octet en mémoire. En fait, c'est un peu plus compliqué que ça parce que le 0 n'est pas forcément le début de la mémoire, mais passons.
L'instruction JMP 120 indique un saut vers l'instruction située à l'adresse 120h. En langage machine binaire, les adresses ne sont pas absolues mais relatives, c'est-à-dire que si l'on place cette instruction à l'adresse 100h, alors il faudra faire un saut vers l'avant de 20h octets. Donc en binaire, on indiquera 20 et non 100 comme valeur du saut.
En binaire tout est défini en octets, et pour optimiser la taille, les instructions x86 sont différentes selon la taille de la valeur du saut, c'est-à-dire que si l'on fait un saut trop loin, il va falloir changer d'instruction binaire.
il y a autre chose à savoir : le saut est relatif à l'adresse de l'instruction suivant le JMP et pas à l'adresse de l'instruction JMP. Dans notre cas, le JMP 120 situé à l'adresse 100 va générer une instruction de 2 octets. Donc, on ne fait pas un saut de 20h, mais de 20h-2h=1Eh.
L'instruction JMP pour des petits sauts (inférieur à 128) est EB, donc l'instruction binaire correspondante au JMP 120, si elle est située à l'adresse 100h est: EB 1E
Il est important de savoir où l'instruction JMP est placée, car si elle est placée trop loin (par ex JMP 200h situé à l'adresse 100h fait un saut de 100h=256), alors la taille de l'instruction peut être plus grande. Dans l'exemple du JMP 200, on obtient E9 FD 00 qui se traduit par E9 (saut relatif vers) 00FDh+3h=100h (l'architecture x86 étant little endian, il faut inverser les octets pour former le nombre), bref la taille de l'instruction est de 3.

Donc, notre script a besoin de 2 paramètres : l'adresse où placer l'instruction et l'instruction elle-même. Le problème, si l'on veut faire "CALL CRickyLib_Ins_Size 200 JMP 100", est que le batch va découper ça en 3 paramètres. Classiquement, on retrouve ce problème en batch lorsqu'on donne un répertoire en paramètre d'un batch (il suffit dans ce cas, de mettre tout le répertoire entre guillemets).
Dans mon script, je vais procéder autrement, je vais récupérer le premier paramètre qui va être l'adresse, et je vais boucler sur tous les autres paramètres pour reformer l'instruction à la main.

CRickyLib_Ins_Size.bat
  1. @ECHO off
  2. REM *** GPL License
  3. REM *** By CRicky
  4.  
  5. REM Lecture de l'adresse (1er paramètre)
  6. SET CRickyLibInsSizeAddrBegin=%1
  7. ECHO a%1 > CRickyLibInsSize_temp.txt
  8.  
  9. SET CRickyLibInsSizeIns=
  10.  
  11. REM formation de l'instruction en reprenant tous les autres paramètres
  12. :loop
  13. SHIFT
  14. IF "%1" EQU "" GOTO end
  15. IF "%CRickyLibInsSizeIns%" EQU "" (
  16. SET CRickyLibInsSizeIns=%1
  17. ) ELSE (
  18. SET CRickyLibInsSizeIns=%CRickyLibInsSizeIns% %1
  19. )
  20. GOTO loop
  21.  
  22. REM Utilisation de debug
  23. :end
  24. ECHO %CRickyLibInsSizeIns% >> CRickyLibInsSize_temp.txt
  25. ECHO. >> CRickyLibInsSize_temp.txt
  26. ECHO q >> CRickyLibInsSize_temp.txt
  27.  
  28. debug < CRickyLibInsSize_temp.txt > CRickyLibInsSize_temp2.txt
  29.  
  30. REM Analyse du résultat pour récupérer l'information des 2 adresses
  31. for /f "eol=- skip=1 tokens=1,2 delims=: " %%A in (CRickyLibInsSize_temp2.txt) do (
  32. set CRickyLibInsSizeAddrEnd=%%B
  33. )
  34.  
  35. REM Calcul de l'adresse relative
  36. CALL CRickyLib_Hexa %CRickyLibInsSizeAddrEnd% %CRickyLibInsSizeAddrBegin%
  37.  
  38. SET CRickyLibInsSize=%CRickyLibHexaSub%

Pour passer tous les paramètre, j'utilise la commande SHIFT qui enlève le premier paramètre et décale tout (le second paramètre passe dans %1, etc).

Un exemple d'utilisation pour expliquer le décodage:
  1. call CRickyLib_Ins_Size 200 jmp 100
  2.  
  3. echo Size of (JMP 100) from 200 = %CRickyLibInsSize%
  4.  
  5. pause
  6. START CRickyLib_Clean.bat

On obtient comme fichier de commande pour debug:
a500
jmp 100

q

Et comme affichage de debug redirigé dans le fichier:
-a200
1550:0200 jmp 100
1550:0203
-q

On écrit à l'adresse mémoire 1550:0200, et l'adresse suivante est 1550:0203. Il y a donc un décalage de 3 octets entre l'instruction JMP et la suivante. Il faut donc calculer dans le script le 203h-200h=3
Pour cela, on fait un FOR /F pour lire toutes les lignes du fichier, on utilise l'option "eol=-" pour considérer le "-" comme début de commentaire pour le pas prendre en compte la première et dernière ligne.
L'option "delims=: " (avec le caractère ':' et ' ' (espace)) indique que les séparateurs de champs sont ':' et ' '. Donc, on aura dans le FOR pour le cas de la ligne 3:
%%A contenant "1550"
%%B contenant "0203"
Il suffit donc de calculer 0203 - 0200 (le premier paramètre du script), mais attention, les valeurs sont en hexadécimal, on aurait pu avoir à calculer 020F - 020C si l'on avait mis l'instruction à l'adresse 020C.
On utilise alors notre script précédent CRickyLib_Hexa.bat pour faire le calcul.

Dans un prochain post, j'expliquerai comment programmer en batch un programme en langage binaire sous debug.
a b L Programmation
24 Août 2009 21:23:50

debug+metaprogrammation: génération de l'initialisation et de la finalisation d'un programme en langage machine
Bon, jusque là rien de bien compliqué. Maintenant, je vais utiliser debug pour faire un programme .COM binaire en mode réel.

D'abord, je vais changer d'affichage en passant du mode texte 80x25 caractères au mode graphique 320x200 en 256couleurs.

Pour cela, il y a des instructions en assembleur à utiliser: Il faut remplir le registre (un registre étant une petite mémoire intégrée dans le processeur) ES (Extra-Segment) du processeur avec la valeur A000, car il suffira d'écrire en mémoire à partir de l'adresse A000:0000 pour écrire directement sur la mémoire vidéo et donc afficher quelque chose à l'écran. Si on met 8 à l'adresse A000:0000, le premier pixel aura la couleur n°8 de la palette.
ceci se fait pas les instructions assembleur:
PUSH A000h
POP ES
Avec debug, on peut écrire ce code avec a100 et voir ce que ça donne avec u100.
Ainsi, on remarque la traduction:
PUSH A000h => 6800A0 (68=PUSH, 00A0=A000 car on inverse)
POP ES => 07 (07=POP ES)
Pour info, PUSH valeur place la valeur sur une pile, et POP registre dépile la valeur de la pile et la met dans le registre. On a donc mis A000h dans ES. L'affectation directe de ES avec la valeur n'est pas possible (mais possible avec d'autres registres, pas ES).
Bref, notre programme commencera donc par: 68 00 A0 07

Bon on a juste préparer notre mémoire pour l'affichage, on n'a toujours pas initialisé le mode graphique. Voici le code pour le mode graphique:
B81300 MOV AX, 0013h
CD10 INT 10h
On place 0013h dans le registre AX (registre composé des sous-registres AH=00h et AL=13h), et on lance l'interruption 10h (BIOS video). Pour résumer, on exécute la fonction 00h de l'interruption video 10h (qui correcpond à "changer le mode d'affichage") avec comme paramètre 10h qui le numéro du mode. 13h correspond au 320x200x8bits (1 octet par pixel).

Par la suite, on va utiliser le registre AL pour la couleur du pixel (la valeur est l'index dans la palette des 256 couleurs), et CX comme compteur qui va servir à passer sur tout les pixel (c'est CX qui va servir de boucle principale). Donc, on initialise tout ça, par exemple AL=1, et CX=0:
B001 MOV AL, 01h
31C9 XOR CX, CX
faire CX=CX xor CX revient à faire MOV CX, 0, mais l'instruction XOR CX, CX est plus courte et plus rapide.

Voilà nous avons le début du programme à générer: 68 00 A0 07 B8 13 00 CD 10 31 C9
Nous allons faire de la métaprogrammation, c'est-à-dire manipuler un programme dans un autre programme. La variable décrivant le programme à manipuler est un metaprogramme (description d'un programme).

CRickyLib_Init.bat
  1. REM *** GPL License
  2. REM *** By CRicky
  3. @echo off
  4.  
  5. SET CRickyLibPos=100
  6.  
  7. echo e%CRickyLibPos% > CRickyLib_temp.txt
  8. echo 68 00 A0 07 B8 13 00 CD 10 31 C9 >> CRickyLib_temp.txt
  9.  
  10. SET CRickyLibPos=10B
  11. SET CRickyLibBeginPos=10B
  12. SET CRickyLibPrgSize=0
  13.  
  14. SET CRickyLibInit=Start

Le metaprogramme sera en fait le contenu du fichier CRickyLib_temp.txt.
Pour savoir à quelle adresse se trouve les instructions, j'utilise la variable d'environnement CRickyLibPos. Le programme commence à une adresse xxxx:0100h. Le début du programme prend 11 octets (11=0Bh), donc les prochaines instructions se situeront à l'adresse xxxx:010Bh

Voilà, ensuite, on fera l'affichage des pixels en utilisant AL pour la couleur, et CX pour la position. En fait on va boucler sur tous les pixels, et suivant la position courant du pixel, on affichera ou non un pixel de couleur indiquée par AL.
La mémoire n'est qu'une ligne. On sera sur le premier pixel (pixel 0) lorsque CX=0, on sera sur le dernier pixel de la ligne lorsque CX=319 (puisque ça fait 320 pixels de large). On sera sur le premier pixel de la 2nde ligne lorsque CX=320.
De manière plus générale, on sera sur le point situé à l'écran en (X, Y) lorsque CX = Y * 320 + X. Il suffira alors d'écrire à l'adresse A000:[CX] la contenu de AL pour afficher le point (X, Y) avec la couleur indiqué, mais on reverra ça dans le prochain post.

D'aboird, il faut prévoir la fin du programme. Je récapitule: on initialise le programme, on fera l'affichage ou non du point situé en CX, et après l'affichage du pixel courant indiqué par CX, il faut:
1) préparer le passage au pixel suivant en augmentant CX
2) tester si on arrive au dernier pixel. Si c'est le cas, on doit remettre CX à 0 pour repartir au premier pixel
3) synchroniser l'affichage avec l'écran. En assembleur, si on ne synchronise pas, l'affichage se fera trop vite et on verra tout clignoter bizarrement.
4) tester si l'utilisateur a appuyé sur la touche ECHAP (il faut pouvoir quitter notre programme :)  ), si ce n'est pas le cas, alors on reboucle sur nos pixel à afficher (maintenant CX a changé donc on passe au pixel suivant).
5) dans le cas d'appui sur ECHAP, on remet le mode graphique en mode texte, et on quitte le programme.

Voici les instructions:
1)
41 INC CX
équivalent à CX := CX+1, ça augmente CX de 1.

2)
81F900FA CMP CX, FA00h
7502 JNZ +02h
31C9 XOR CX, CX
On compare CX avec FA00h = 320 * 200.
Si c'est différent (CMP Not Zero), on saute 2 octets plus loin (pour sauter le XOR).
Et donc, si c'est équal, l'instruction XOR CX,CX qui remet la partie haute de CX à 0000h

3)
BADA03 MOV DX, 03DAh
EC IN AL, DX
A808 TEST AL, 08h
75FB JNZ -05h
EC IN AL, DX
A808 TEST AL, 08h
74FB JZ -05h
Je détaille pas, en gros on attend l'activation de la retrace verticale, puis on attend sa désactivation, ce qui permet de s'assurer que l'on ne va pas changer la mémoire vidéo lorsque celle-ci est réellement copiée à l'écran. Si on affiche la moitié de l'écran, c'est là qu'on vera un clignotement (comme si l'écran était coupé en 2 avec un retard sur une moitié).

4) C'est là que les problèmes vont commencer. :) 
E460 IN AL, 60h
3C01 CMP AL, 01h
yyyy JNZ -xxh
On lit à l'adresse 60h (sortie du contrôleur clavier) et on met le résultat dans AL. En fait, on récupère la touche appuyé par l'utilisateur.
Si AL=01 (CMP Zero, inverse de JNZ), c'est que c'est la touche ECHAP qui est enfoncée. Dans ce cas, on continue les instruction pour terminer le programme.
Si AL est différent de 01, CMP donne Jump if Not Zero de xxh (xxh octets en arrière pour boucler sur notre affichage, et c'est juste après le début du programme (donc à l'adresse xxxx:010Bh).
Ici, j'ai remplacé la valeur par -xxh car on ne sait pas où se situe le début, car la valeur à indiquer, n'est pas 10Bh, mais une valeur relative (d'où le signe négatif). Donc, il faudra savoir à quelle adresse on se situe pour faire la soustraction en hexadécimal (adresse fin - 10Bh). Heureusement on a fait le script qui va bien. ;) 
J'ai aussi masqué l'instruction entière yyyy, car comme je l'avais indiqué dans un post précédent, les instructions de saut (JMP, JNZ, JZ...) dépend de la longueur du saut. Il faudra donc en tenir compte dans le batch et calculer tout ça!

5) D'abord on remet l'affichage en mode texte 80x25: c'est l'interruption video 10h, fonction 00h, mode 03h:
B80300 MOV AX, 0003h
CD10 INT 10h
Et on indique la fin du programme de type DOS (interruption DOS 21h, fonction 4Ch, sous fonction 00h)
B8004C MOV AX, 4C00h
CD21 INT 21h

Voilà, on a donc, pour terminer le programme:
... 41 81 F9 00 FA 75 02 31 C9 BA DA 03 EC A8 08 75 FB EC A8 08 74 (x...x) E4 60 3C 01 74 (y...y) B8 03 00 CD 10 B8 00 4C CD 21
Dans le batch, j'ai fait le calcul du saut de synchro d'affichage (qui n'a pas d'intérêt, mais ça m'a permis de tester sur quelque chose de fixe), puis le batch sur ce qui est variable.
Pour les sauts, en fait, je vais remplacer JNZ -xxh, par les 2 instructions JZ 02h ou JZ 03h selon la taille de l'instruction du saut, suivi d'un simple JMP -xxh (instruction EB xx) ou JMP -xxzzh (instruction E9 zz xx, en inversant bien xx et zz) selon la distance à sauter (de 0 à 128, ou de 128 à 65536 (notre programme est donc limité en taille, puisque je ne prévois pas plus grand :)  )).
Ce qui nous donne, le grand script suivant:
CRickyLib_Finish.bat
  1. REM *** GPL License
  2. REM *** By CRicky
  3. REM todo: bad offset in if...
  4. @ECHO off
  5.  
  6. ECHO e%CRickyLibPos% >> CRickyLib_temp.txt
  7. ECHO 41 81 F9 00 FA 75 02 31 C9 BA DA 03 EC A8 08 75 FB EC A8 08 74 >> CRickyLib_temp.txt
  8.  
  9. CALL CRickyLib_Hexa %CRickyLibPos% 15
  10. SET CRickyLibPosTemp=%CRickyLibHexaAdd%
  11.  
  12. CALL CRickyLib_Hexa %CRickyLibPos% 18
  13. SET CRickyLibPos=%CRickyLibHexaAdd%
  14.  
  15. CALL CRickyLib_Hexa %CRickyLibPos% %CRickyLibBeginPos%
  16. SET CRickyLibOffset=%CRickyLibHexaSub%
  17.  
  18. SET /A CRickyLibOffsetValue=0x%CRickyLibOffset%
  19.  
  20. rem reverse offset
  21. CALL CRickyLib_Hexa %CRickyLibBeginPos% %CRickyLibPos%
  22. SET CRickyLibOffset=%CRickyLibHexaSub%
  23.  
  24. CALL CRickyLib_Hexa %CRickyLibHexaSub% 1
  25. SET CRickyLibOffsetPlus=%CRickyLibHexaSub%
  26.  
  27. CALL CRickyLib_Hexa %CRickyLibPos% 1
  28. SET CRickyLibPosPlus=%CRickyLibHexaAdd%
  29.  
  30. IF %CRickyLibOffsetValue% LEQ 128 (
  31.  
  32. ECHO E %CRickyLibPosTemp% >> CRickyLib_temp.txt
  33. ECHO 02 EB %CRickyLibOffset:~2,2% >> CRickyLib_temp.txt
  34.  
  35. ) ELSE (
  36.  
  37. rem reverse offset
  38. ECHO E %CRickyLibPosTemp% >> CRickyLib_temp.txt
  39. ECHO 03 E9 %CRickyLibOffsetPlus:~2,2% %CRickyLibOffsetPlus:~0,2% >> CRickyLib_temp.txt
  40.  
  41. SET CRickyLibPos=%CRickyLibPosPlus%
  42. SET CRickyLibOffset=%CRickyLibOffsetPlus%
  43. )
  44.  
  45. ECHO E %CRickyLibPos% >> CRickyLib_temp.txt
  46. ECHO E4 60 3C 01 74 >> CRickyLib_temp.txt
  47.  
  48. CALL CRickyLib_Hexa %CRickyLibPos% 5
  49. SET CRickyLibPosTemp=%CRickyLibHexaAdd%
  50.  
  51. CALL CRickyLib_Hexa %CRickyLibPos% 8
  52. SET CRickyLibPos=%CRickyLibHexaAdd%
  53.  
  54. CALL CRickyLib_Hexa %CRickyLibPos% %CRickyLibBeginPos%
  55. SET CRickyLibOffset=%CRickyLibHexaSub%
  56.  
  57. SET /A CRickyLibOffsetValue=0x%CRickyLibOffset%
  58.  
  59. rem reverse offset
  60. CALL CRickyLib_Hexa %CRickyLibBeginPos% %CRickyLibPos%
  61. SET CRickyLibOffset=%CRickyLibHexaSub%
  62.  
  63. CALL CRickyLib_Hexa %CRickyLibHexaSub% 1
  64. SET CRickyLibOffsetPlus=%CRickyLibHexaSub%
  65.  
  66. CALL CRickyLib_Hexa %CRickyLibPos% 1
  67. SET CRickyLibPosPlus=%CRickyLibHexaAdd%
  68.  
  69. IF %CRickyLibOffsetValue% LEQ 128 (
  70.  
  71. ECHO E %CRickyLibPosTemp% >> CRickyLib_temp.txt
  72. ECHO 02 EB %CRickyLibOffset:~2,2% >> CRickyLib_temp.txt
  73.  
  74. ) ELSE (
  75.  
  76. rem reverse offset
  77. ECHO E %CRickyLibPosTemp% >> CRickyLib_temp.txt
  78. ECHO 03 E9 %CRickyLibOffsetPlus:~2,2% %CRickyLibOffsetPlus:~0,2% >> CRickyLib_temp.txt
  79.  
  80. SET CRickyLibPos=%CRickyLibPosPlus%
  81. SET CRickyLibOffset=%CRickyLibOffsetPlus%
  82. )
  83.  
  84. ECHO E %CRickyLibPos% >> CRickyLib_temp.txt
  85. ECHO B8 03 00 CD 10 B8 00 4C CD 21 >> CRickyLib_temp.txt
  86.  
  87. CALL CRickyLib_Hexa %CRickyLibPos% A
  88. SET CRickyLibPos=%CRickyLibHexaAdd%
  89.  
  90. REM save program (not used)
  91. echo nPROG.COM >> CRickyLib_temp.txt
  92. echo rBX >> CRickyLib_temp.txt
  93. echo 0 >> CRickyLib_temp.txt
  94. echo rCX >> CRickyLib_temp.txt
  95. CALL CRickyLib_Hexa %CRickyLibPos% 100
  96. SET CRickyLibSize=%CRickyLibHexaSub%
  97. echo %CRickyLibSize% >> CRickyLib_temp.txt
  98. echo w >> CRickyLib_temp.txt
  99.  
  100. ECHO g >> CRickyLib_temp.txt
  101. ECHO q >> CRickyLib_temp.txt
  102.  
  103. SET CRickyLibInit=End
  104.  
  105. REM RUN PROGRAM
  106. DEBUG < CRickyLib_temp.txt

A là fin, j'enregistre le programme dans PROG.COM (non obligatoire, car on n'est pas obliger de créer le fichier exécutable pour l'exécuter) pour garder notre programme généré. En suite j'exécute le programme (commande debug g).

J'en profite pour enrichir le script qui va nettoyer les fichiers (car on commence à en avoir quelques uns):

CRickyLib_Clean.bat
  1. @ECHO off
  2. REM *** GPL License
  3. REM *** By CRicky
  4.  
  5. DEL CRickyLibHexa_temp.txt
  6. DEL CRickyLibHexa_temp2.txt
  7.  
  8. DEL CRickyLibInsSize_temp.txt
  9. DEL CRickyLibInsSize_temp2.txt
  10.  
  11. DEL CRickyLib_temp.txt
  12.  
  13. EXIT


Voilà, ce post montre que, bien que la batch reste un langage très limité, on peut, en utilisant debug créer n'importe quel programme qui peut faire n'importe quoi. Il est donc préférable de toujours ouvrir un batch pour savoir ce qu'il fait réellement avant de l'exécuter.

Bon, je ne fais pas d'exemple d'utilisation, puisque notre programme ne fait rien (on n'a pas encore affiché de programme.

Dans le prochain post, je montrerai comment intégrer l'affichage d'un pixel dans le programme généré.
a b L Programmation
25 Août 2009 19:39:21

debug+metaprogrammation: génération de l'affichage d'un point 2D

Bien, maintenant que l'on a préparé l'initialisation de l'affichage graphique, la boucle sur tous les pixels de l'écran et la fin du programme, il ne reste plus qu'à afficher un point à l'écran au milieu de tout ça.

Pour rappel, on utilise les registres CX = x + 320 * y pour afficher le point (x, y) et AL pour la couleur.

Pour afficher un point, il suffit d'acrire à l'adresse [ES]:[CX] avec ES=A000h (adresse mémoire de l'écran):
89CF MOV DI, CX
26 8805 MOV ES:[DI],AL
on passe par le registre DI car on ne peut pas utiliser directement CX, et le MOV [adresse], valeur permet d'écrire en mémoire à l'adresse indiquée la valeur.

Avant d'écrire ça, il faut tester si CX est bien sur le pixel voulu (x, y).
Pour faire la comparaison, je distingue 2 cas: si la valeur x+320*y tiens sur 1 octet (<256):
83 F9 xx CMP CX, (x+320*y)
75 07 JNZ +07
B0 yy MOV AL, couleur
<écriture mémoire video: 89 CF 26 88 05>

sinon:
81 F9 xx xx CMP CX, (x+320*y)
75 07 JNZ +07
B0 yy MOV AL, couleur
<écriture mémoire video: 89 CF 26 88 05>

Voilà, donc si l'on veut afficher en (10, 10) la couleur 14 (0Eh), on aura:
position = 10 + 10 * 320 = 3210 = 0C8Ah
83 F9 8A 0C (on inverse le 0C 8A)
75 07
B0 0E
89 CF 26 88 05

En batch on donne x, y et couleur en parallèle, on calcule la position dans le batch, et on ajoute l'écriture en dur du pixel comme dans l'exemple:
CRickyLib_DrawPoint.bat
  1. REM *** GPL License
  2. REM *** By CRicky
  3. @ECHO off
  4.  
  5. SET /A CRickyLib_X=%1
  6. SET /A CRickyLib_Y=%2
  7. SET /A CRickyLib_COL=%3
  8.  
  9. SET /A CRickyLibOffsetDec=%CRickyLib_Y%*320+%CRickyLib_X%
  10. CALL CRickyLib_Conv_Dec2Hexa %CRickyLibOffsetDec%
  11. SET CRickyLibOffset=%CRickyLibHexa%
  12.  
  13. CALL CRickyLib_Conv_Dec2Hexa %CRickyLib_COL%
  14. SET CRickyLibCol=%CRickyLibHexa%
  15.  
  16. CALL CRickyLib_Hexa %CRickyLibPos% 1
  17.  
  18. IF %CRickyLibOffsetDec% LEQ 255 (
  19.  
  20. ECHO e%CRickyLibPos% >> CRickyLib_temp.txt
  21. ECHO 83 F9 %CRickyLibOffset:~2,2% 75 07 B0 %CRickyLibCol:~2,2% 89 CF 26 88 05 >> CRickyLib_temp.txt
  22.  
  23. ) ELSE (
  24.  
  25. ECHO e%CRickyLibPos% >> CRickyLib_temp.txt
  26. ECHO 81 F9 %CRickyLibOffset:~2,2% %CRickyLibOffset:~0,2% 75 07 B0 %CRickyLibCol:~2,2% 89 CF 26 88 05 >> CRickyLib_temp.txt
  27.  
  28. SET CRickyLibPos=%CRickyLibHexaAdd%
  29. )
  30.  
  31. CALL CRickyLib_Hexa %CRickyLibPos% 0C
  32. SET CRickyLibPos=%CRickyLibHexaAdd%

Comme on peut afficher plusieurs point, on utilise bien %CRickyLibPos% pour savoir où l'on se trouve dans le programme que l'on fabrique.
Dans l'exemple du (10, 10), le bout de code prend 13 (0Dh) octets, mais dans l'autre cas, on n'a plus que 12 (0Ch) octets. Il faut donc bien faire attention à l'endroit où l'on se trouve.

Exemple d'utilisation:
  1. CALL CRickyLib_Init
  2.  
  3. CALL CRickyLib_DrawPoint 5 50 14
  4. CALL CRickyLib_DrawPoint 100 100 1
  5.  
  6. CALL CRickyLib_Finish
  7.  
  8. START CRickyLib_Clean

D'abord on génère le début du programme, puise un point en (5, 50) couleur jaune, un point (100, 100) couleur bleu, puis on termine en mettant la synchro, la boucle, fin de programme et exécution du programme. Enfin, on supprime tous les fichiers temporaire en mettant bien un START et pas un CALL.

A partir du prochain post, je montrerai comment l'on trace un segment d'un point à un autre en utilisant ce batch d'affichage d'un point.
a b L Programmation
26 Août 2009 20:28:16

génération de l'affichage d'un segment 2D

Maintenant que l'on a tout pour générer un programme qui affiche des points, il ne reste plus qu'à faire évoluer les fonctionnalités en se basant sur les existantes.
On va afficher des traits en affichant tous les points du trait.
Si on veut tracer le trait entre 2 points A de coordonnées (x1, y1) et B de coordonnées (x2, y2), il va falloir calculer tous les points intermédiaires.
Je prends d'abord le cas où le segment fait un angle compris entre 0 et 45° par rapport à l'horizontale.
Si je prend le point M dont je ne connais que la position horizontale (x, ???), il me faut calculer la position verticale C'est très simple en utilisant le théorème de Thalès sur le triangle droit formé par les points (x1, y1), (x2, y2), (x2, y1) et le triangle (x1, y1), (x, ???), (x, y1)
Le théorème nous donne donc (x - x1) / (x2 - x1) = (??? - y1) / (y2 - y1)
Donc, M a pour coordonnées (x, (x - x1) * (y2 - y1) / (x2 - x1) + y1)

Et inversement, si on a M = (???, y), alors M = ((y - y1)*(x2 - x1)/(y2 - y1) + x1, y)

Pour tracer le segment, il faut boucler, soit sur les x soit sur les y des points entre A et B, on calcule les points M à afficher. Le choix (boucle sur x ou sur y) va se faire selon l'angle du segment. Si celui-ci est presque vertical, si on boucle sur les x, on ne va pas avoir beaucoup de points avec de grands trous, et inversement si le segment est presque horizontal.
Donc si c'est vertical, il faudra boucler sur tous les y, qui permettra de faire des marches sur la droite. En 320x200, les pixels sont gros, alors on voit bien l'effet escalier sur le segment (aliasing).

On sait si c'est plus horizontal que vertical lorsque Abs(x2-x1) > Abs(y2-y1), c'est-à-dire qu'on s'étend plus sur x que sur y.

CRickyLib_DrawLine.bat
  1. @ECHO off
  2. REM *** GPL License
  3. REM *** By CRicky
  4.  
  5. SET /A CRickyLib_X1=%1
  6. SET /A CRickyLib_Y1=%2
  7. SET /A CRickyLib_X2=%3
  8. SET /A CRickyLib_Y2=%4
  9. SET /A CRickyLib_COL=%5
  10.  
  11. SET /A CRickyLib_DiffX=%CRickyLib_X2%-%CRickyLib_X1%
  12. CALL CRickyLib_Abs %CRickyLib_DiffX%
  13. SET /A CRickyLib_DiffXAbs=%CRickyLibAbs%
  14.  
  15. SET /A CRickyLib_DiffY=%CRickyLib_Y2%-%CRickyLib_Y1%
  16. CALL CRickyLib_Abs %CRickyLib_DiffY%
  17. SET /A CRickyLib_DiffYAbs=%CRickyLibAbs%
  18.  
  19. SET /A CRickyLib_CurX=%CRickyLib_X1%
  20. SET /A CRickyLib_CurY=%CRickyLib_Y1%
  21.  
  22. Echo PREPARING LINE...
  23.  
  24. IF %CRickyLib_DiffXAbs% GEQ %CRickyLib_DiffYAbs% (
  25. GOTO loop1
  26. )
  27.  
  28. :loop2
  29.  
  30. SET /A CRickyLib_CurX=%CRickyLib_CurY%-%CRickyLib_Y1%
  31. SET /A CRickyLib_CurX*=%CRickyLib_DiffX%
  32. SET /A CRickyLib_CurX/=%CRickyLib_DiffY%
  33. SET /A CRickyLib_CurX+=%CRickyLib_X1%
  34.  
  35. CALL CRickyLib_DrawPoint %CRickyLib_CurX% %CRickyLib_CurY% %CRickyLib_COL%
  36.  
  37. IF %CRickyLib_Y1% LEQ %CRickyLib_Y2% (
  38. SET /A CRickyLib_CurY+=1
  39. ) ELSE (
  40. SET /A CRickyLib_CurY-=1
  41. )
  42.  
  43. IF %CRickyLib_CurY% EQU %CRickyLib_Y2% GOTO :eof
  44.  
  45. GOTO loop2
  46.  
  47. :loop1
  48.  
  49. SET /A CRickyLib_CurY=%CRickyLib_CurX%-%CRickyLib_X1%
  50. SET /A CRickyLib_CurY*=%CRickyLib_DiffY%
  51. SET /A CRickyLib_CurY/=%CRickyLib_DiffX%
  52. SET /A CRickyLib_CurY+=%CRickyLib_Y1%
  53.  
  54. CALL CRickyLib_DrawPoint %CRickyLib_CurX% %CRickyLib_CurY% %CRickyLib_COL%
  55.  
  56. IF %CRickyLib_X1% LEQ %CRickyLib_X2% (
  57. SET /A CRickyLib_CurX+=1
  58. ) ELSE (
  59. SET /A CRickyLib_CurX-=1
  60. )
  61.  
  62. IF %CRickyLib_CurX% EQU %CRickyLib_X2% GOTO :eof
  63.  
  64. GOTO loop1

En batch, lorsqu'on fait un IF, toute l'expression incluse dans le IF est evaluée avant d'être exécutée. Donc, si l'on modifie une variable dans un IF, elle ne sera réellement modifiée qu'en dehors. Pour éviter ce phénomène, on peut utiliser l'expansion retardée. Si l'on avait tout mis dans un IF, il aurait utilisé !CRickyLib_CurX! au lieu de %CRickyLib_CurX%. Ceci permet de n'évaluer le contenu de la variable que lorsque elle est utilisée, et pas au IF. Pour pouvoir utiliser cette fonctionnalité, il faut activer l'expansion retardée des variables (commutateur /N : ON lorsqu'on lance cmd, ou à activer dans la base de registre).
Afin d'éviter le problème d'activation de l'expansion retardé en batch, j'évite d'accumuler les instruction dans les IF, c'est pourquoi je suis passé par des goto un peu partout.
La boucle et le calcul des point est uniquement fait en batch, donc ça prend du temps, mais ce n'est pas un problème puisque l'on va générer un programme qui affichera tous points calculé. Plus on affiche des points, plus le programme sera long à construire, et plus le fichier résultant sera gros.
Si l'on voulait optimiser, il suffirait de créer une fonction d'affichage de ligne en assembleur en utilisant le même principe, mais mon but est d'en montrer le maximum en batch.

Exemple d'utilisation:
  1. CALL CRickyLib_Init
  2.  
  3. CALL CRickyLib_DrawPoint 5 50 14
  4. CALL CRickyLib_DrawPoint 100 100 1
  5. CALL CRickyLib_DrawLine 10 10 100 50 13
  6. CALL CRickyLib_DrawLine 10 200 19 50 12
  7.  
  8. CALL CRickyLib_Finish
  9. start CRickyLib_Clean


Dans le prochain post, je ferai dans le même principe une nouvelle fonctionnalité : afficher un point en 3D projeté sur l'écran (toujours en utilisant le théorème de Thalès entre l'oeil l'écran et le point).
a b L Programmation
27 Août 2009 20:50:32

Projection d'un point 3D

Je vais introduire de la 3D en mode raytracing (tracé de rayons) simple, et c'est finalement plus simple que le tracé d'un segment en 2D.

D'abord, je définit un environnement 3D fixe (on pourrait le faire paramétrer pour modifier l'angle de vision, mais je ne le ferai pas).
En 3D, je prends le repère (x, y, z), où x est le déplacement horizontal sur l'écran, y le déplacement vertical sur l'écran, et z la profondeur.
Je place l'oeil O en (0, 0, 0) au centre du monde. :) 
Je place le centre de l'écran en z=1, (x=0 et y=0). Donc attention, car pour z=1, avec x=0 et y=0, on ne se situe pas en haut à gauche de l'écran mais au milieu de l'écran, c'est à dire au point situé en 160x100 car la résolution et 320x200.
Je définis la largeur de l'écran à 2, et la hauteur à 2. C'est-à-dire que si je prends les coins de l'écran C1, C2, C3, C4, alors ces points seront mis dans l'espace aux coordonnées:
C1=(-1, -1, 1)
C2=(1, -1, 1)
C3=(-1, 1, 1)
C4=(1, 1, 1)
Avec l'oeil, ça définit une pyramide dont l'angle entre 2 cotés opposés est de 90°.

Bien, maintenant que tout est placé, on veut afficher un point M=(Xm, Ym, Zm) sur l'écran. Il faut imaginer la vision de ce point comme un trait en 3D qui part de M vers l'œil qui est notre rayon lumineux. Ce rayon, avant d'arriver à l'oeil, va toucher l'écran (au point I), et c'est à ce point d'intersection qu'il faut afficher l'image de M sur l'écran. Ainsi, pour projetter le point M situé derrière l'écran sur l'écran, il suffit d'afficher le point I sur l'écran.
Pour calculer l'intersection du rayon et de l'écran, on voir l'espace par dessus et par le coté.
D'abord par dessus, on a une vision sur x et sur z uniquement, alors on a
- l'œil O (x=0, z=0)
- M (x=Xm, z=Zm)
- I(x=Xi, z=Zi) le point d'intersection du rayon avec l'écran. Comme il est sur l'écran, on sait que Zi=1, donc I=(x=Xi, z=1)

Je définis les points qui vont me servir à faire 2 triangles rectangles pour appliquer le théorème de Thalès:
- M' (x=0, z=Zm) => sur l'axe du milieu de l'écran, vu de dessus
- I' (x=0, z=1) => milieu de l'écran vu de dessus
Le théorème de Thalès nous dis que: [ I I' ] / [ M M' ] = [ O I' ] / [ O M' ]
On a:
- [ I I' ] = Xi
- [ M M' ] = Xm
- [ O I' ] = 1
- [ O M' ] = Zm

On a Xm et Zm puisque ce sont les coordonnées du point M qui nous est donné, et on souhaite savoir Xi pour déterminer la position sur x sur l'écran.
Donc, si on remplace les vecteurs dans la formule, on obtient:
Xi / Xm = 1 / Zm

Donc, Xi = Xm / Zm.

Ensuite, on refait la même chose mais vu sur le côté, c'est-à-dire que l'on regarde sur le plan y et z. On fait le même calcul (enfin, moi je ne le fais pas :p  ) et on obtient Yi = Ym / Zm

On a trouvé les coordonées du point I en 3D.
Attention, car (Xi, Yi), n'est pas le point à afficher à l'écran, puisque j'ai dit que mon écran faisait 2 de largeur, 2 de hauteur et que le milieu en 3D (x=0, y=0, z=1), était le centre de l'écran (160, 100).
En fait, on a trouvé I en 3d: I=(Xi, Yi, 1) = (Xm / Zm, Ym / Zm, 1)

Pour convertir en coordonnées sur l'écran, on va élargir les Xi et Yi aux dimensions de l'écran: (X * 160, Y * 100).
Si on convertit les coins de l'écran donnés plus haut, on obtient:
C1 => (-160, -100)
C2 => (160, -100)
C3 => (-160, 100)
C4 => (160, 100)
On retrouve bien la largeur [ C1 C2 ] = 320 et [ C1 C3 ] = 160, la résolution de notre écran.

Maintenant, il faut déplacer le repère de l'écran à partir du milieu vers le coin haut-gauche. Pour cela, il suffit d'ajouter 160 horizontalement, et 100 verticalement: (X * 160 + 160, Y * 100 + 100).
Si on convertit les coins de l'écran donnés plus haut, on obtient:
C1 => (0, 0)
C2 => (320, 0)
C3 => (0, 200)
C4 => (320, 200)
Ce qui maintenant bien la position réelle des pixels à l'écran.
On applique donc la formule de transformation d'homothétie et translation (X * 160 + 160, Y * 100 + 100) à notre point I. ce qui donne les coordonnées du pixel du point I (qui est le projeté du point M sur l'écran):
(Xi * 160 + 160, Yi * 100 + 100) =
(Xm / Zm * 160 + 160, Ym / Zm * 100 + 100)

On a donc à partir du point M en coordonnée 3D (Xm, Ym, Zm), sa position une fois projeté sur l'écran les coordonées du pixel à l'écran (Xm / Zm * 160 + 160, Ym / Zm * 100 + 100).

Voilà, il ne nous reste plus qu'à faire un petit batch pour faire le calcul, et afficher le point projeté:
CRickyLib_DrawPoint3D.bat
  1. @ECHO off
  2. REM *** GPL License
  3. REM *** By CRicky
  4.  
  5. SET /A CRickyLib_X3D=%1
  6. SET /A CRickyLib_Y3D=%2
  7. SET /A CRickyLib_Z3D=%3
  8. SET /A CRickyLib_COL=%4
  9.  
  10. SET /A CRickyLib_X=%CRickyLib_X3D%
  11. SET /A CRickyLib_X*=160
  12. SET /A CRickyLib_X/=%CRickyLib_Z3D%
  13. SET /A CRickyLib_X+=160
  14.  
  15. SET /A CRickyLib_Y=%CRickyLib_Y3D%
  16. SET /A CRickyLib_Y*=100
  17. SET /A CRickyLib_Y/=%CRickyLib_Z3D%
  18. SET /A CRickyLib_Y+=100
  19.  
  20. CALL CRickyLib_DrawPoint %CRickyLib_X% %CRickyLib_Y% %CRickyLib_COL%


Et un petit exemple d'utilisation qui affiche les sommets d'un cube:
  1. CALL CRickyLib_Init
  2.  
  3. CALL CRickyLib_DrawPoint3D -1000 -1000 5000 14
  4. CALL CRickyLib_DrawPoint3D -1000 1000 5000 14
  5. CALL CRickyLib_DrawPoint3D 1000 1000 5000 14
  6. CALL CRickyLib_DrawPoint3D 1000 -1000 5000 14
  7. CALL CRickyLib_DrawPoint3D -1000 -1000 4000 14
  8. CALL CRickyLib_DrawPoint3D -1000 1000 4000 14
  9. CALL CRickyLib_DrawPoint3D 1000 1000 4000 14
  10. CALL CRickyLib_DrawPoint3D 1000 -1000 4000 14
  11.  
  12. CALL CRickyLib_Finish
  13. start CRickyLib_Clean

J'utilise les grandes valeurs car les calculs dans le batch ne se font pas avec les virgules, alors si j'avais mis -1 -1 -5, ça donnerait rapidement 0. C'est pourquoi, j'ai tout multiplié par 1000 (qui est donc mon unité de base). Par 1000, on peut voir 1.000 (on pourrait faire un batch pour convertir les 1.520 en 1520 et lancer notre script avec un petit FOR /F).

Dans le prochain post, j'expliquerai comment projeté un segment, et j'afficherai les arêtes de 2 cubes.
27 Août 2009 23:40:43

Hahaha, magnifique! Fondalement inutile, et donc naturellement indispensable. J'applaudis l'effort, et je conseille à tous ceux qui veulent se perfectionner en batch ou faire un script un peu complexe de s'inspirer des techniques présentées ici.
(Je leur conseillerais également de trouver un langage plus adapté, mais bon. Parfois, il faut coder, et on a pas le choix.)

Je n'ai pas pu tout lire, le batch me file des boutons, les projections en 3D me donnent des migraines, et le texte est vraiment très riche, mais c'est du post de qualité.
Il y a des infos utiles à toutes les lignes, alors à lire par petites doses en plusieurs fois, pour arriver à assimiler.

Citation :
Une fonctionnalité (commande "a" ) de debug est de permettre d'entrer une instruction assembleur, directement décodée en langage machine, mis en mémoire et exécutable immédiatement.

Tiens, celle là je ne connaissais pas. Pratique.

L'utilisation de scripts comme fonctions, excellent aussi.
Et les concepts 2D/3D, je me les garde pour quand j'en aurais de nouveau besoin (à chaque fois, je passe trois plombes à chercher dans ma tête comment faire).

Je suppose que les techniques données pour l'assembleur, etc. ne fonctionnent qu'en mode réel (et donc seulement sous DOS par exemple, et pas avec cmd.exe)? Sur les dernières versions de Windows il me semble que ça n'existe plus, mais sous Windows 95/98, est-ce que ça marcherait (par une émulation du mode réel, ou je ne sais trop quoi)?
a b L Programmation
28 Août 2009 19:33:38

Oui totalement inutile. :D 
Du coup je post un peu à l'arrach sans me relire même si j'essaie d'être un minimum clair (pas évident en batch et debug :D ), alors si quelqu'un veut des précisions, il faut demander.

Citation :
Je suppose que les techniques données pour l'assembleur, etc. ne fonctionnent qu'en mode réel (et donc seulement sous DOS par exemple, et pas avec cmd.exe)?

C'est effectivement du mode réel, mais ça fonctionne sans problème sous windows XP. En fait, il y a certaines interruptions qui ne fonctionnent plus, comme pour le clavier INT 16h (c'est pour ça que je vais lire directement le contrôleur).
Windows utilise le mode virtuel (qui date quand même des 386/486 avant les pentium) qui permet, pour simplifier, de gérer le mode réel dans un environnement en mode protégé. Ce mode avait été mis en place pour conserver la compatibilité et ne pas énerver les utilisateurs qui voudraient lancer une application ancienne. Ce sont des ruines encore utilisables ;) 

Bon allez, je fais le dernier petit post du tuto. :) 
a b L Programmation
28 Août 2009 19:47:13

Projection d'un segment 3D

La projection d'un segment 3D est très simple. En fait il suffit de projeter les extrémités du segment (la projection d'un point 3D en 2D, on a déjà vu), puis de faire le trait en 2D puisque qu'on a les 2 points projetés (et le dessin de trait, on a aussi déjà vu).

Donc, il suffit soit de réutiliser le script de projection 3D (en ajoutant en retour les coordonnées 2D du pixel), soit refaire la projection des 2 points (c'est la solution que je choisis parce qu'il n'y a pas grand chose à faire).
Donc, voilà le script:

CRickyLib_DrawLine3D.bat
  1. @ECHO off
  2. REM *** GPL License
  3. REM *** By CRicky
  4.  
  5. SET /A CRickyLib_X1_3D=%1
  6. SET /A CRickyLib_Y1_3D=%2
  7. SET /A CRickyLib_Z1_3D=%3
  8. SET /A CRickyLib_X2_3D=%4
  9. SET /A CRickyLib_Y2_3D=%5
  10. SET /A CRickyLib_Z2_3D=%6
  11. SET /A CRickyLib_COL=%7
  12.  
  13. SET /A CRickyLib_X1=%CRickyLib_X1_3D%
  14. SET /A CRickyLib_X1*=160
  15. SET /A CRickyLib_X1/=%CRickyLib_Z1_3D%
  16. SET /A CRickyLib_X1+=160
  17.  
  18. SET /A CRickyLib_Y1=%CRickyLib_Y1_3D%
  19. SET /A CRickyLib_Y1*=100
  20. SET /A CRickyLib_Y1/=%CRickyLib_Z1_3D%
  21. SET /A CRickyLib_Y1+=100
  22.  
  23. SET /A CRickyLib_X2=%CRickyLib_X2_3D%
  24. SET /A CRickyLib_X2*=160
  25. SET /A CRickyLib_X2/=%CRickyLib_Z2_3D%
  26. SET /A CRickyLib_X2+=160
  27.  
  28. SET /A CRickyLib_Y2=%CRickyLib_Y2_3D%
  29. SET /A CRickyLib_Y2*=100
  30. SET /A CRickyLib_Y2/=%CRickyLib_Z2_3D%
  31. SET /A CRickyLib_Y2+=100
  32.  
  33. CALL CRickyLib_DrawLine %CRickyLib_X1% %CRickyLib_Y1% %CRickyLib_X2% %CRickyLib_Y2% %CRickyLib_COL%


Et voilà un exemple d'utilisation qui affiche les arêtes de 2 cubes:
  1. CALL CRickyLib_Init
  2.  
  3. ECHO CUBE 1...
  4.  
  5. CALL CRickyLib_DrawLine3D -1000 -1000 8000 1000 -1000 8000 14
  6. CALL CRickyLib_DrawLine3D 1000 -1000 8000 1000 1000 8000 14
  7. CALL CRickyLib_DrawLine3D 1000 1000 8000 -1000 1000 8000 14
  8. CALL CRickyLib_DrawLine3D -1000 1000 8000 -1000 -1000 8000 14
  9.  
  10. CALL CRickyLib_DrawLine3D -1000 -1000 6000 1000 -1000 6000 14
  11. CALL CRickyLib_DrawLine3D 1000 -1000 6000 1000 1000 6000 14
  12. CALL CRickyLib_DrawLine3D 1000 1000 6000 -1000 1000 6000 14
  13. CALL CRickyLib_DrawLine3D -1000 1000 6000 -1000 -1000 6000 14
  14.  
  15. CALL CRickyLib_DrawLine3D -1000 -1000 8000 -1000 -1000 6000 14
  16. CALL CRickyLib_DrawLine3D 1000 -1000 8000 1000 -1000 6000 14
  17. CALL CRickyLib_DrawLine3D 1000 1000 8000 1000 1000 6000 14
  18. CALL CRickyLib_DrawLine3D -1000 1000 8000 -1000 1000 6000 14
  19.  
  20. ECHO CUBE 2...
  21.  
  22. CALL CRickyLib_DrawLine3D -1000 -1000 4000 1000 -1000 5000 2
  23. CALL CRickyLib_DrawLine3D 1000 -1000 5000 2000 -1000 3000 2
  24. CALL CRickyLib_DrawLine3D 2000 -1000 3000 0 -1000 2000 2
  25. CALL CRickyLib_DrawLine3D 0 -1000 2000 -1000 -1000 4000 2
  26.  
  27. CALL CRickyLib_DrawLine3D -1000 1000 4000 1000 1000 5000 2
  28. CALL CRickyLib_DrawLine3D 1000 1000 5000 2000 1000 3000 2
  29. CALL CRickyLib_DrawLine3D 2000 1000 3000 0 1000 2000 2
  30. CALL CRickyLib_DrawLine3D 0 1000 2000 -1000 1000 4000 2
  31.  
  32. CALL CRickyLib_DrawLine3D -1000 -1000 4000 -1000 1000 4000 2
  33. CALL CRickyLib_DrawLine3D 1000 -1000 5000 1000 1000 5000 2
  34. CALL CRickyLib_DrawLine3D 2000 -1000 3000 2000 1000 3000 2
  35. CALL CRickyLib_DrawLine3D 0 -1000 2000 0 1000 2000 2
  36.  
  37. CALL CRickyLib_Finish
  38. start CRickyLib_Clean

Une petite remarque: le calcul des cube est long (1 min) car il faut dessiner les traits qui se dessinent pixel par pixel, et en batch c'est long, c'est pour ça que je conserve le PROG.COM qui est le programme généré. :) 

J'aurais pu faire un script pour afficher un cube, une sphère, modifier la palette de couleur, afficher un triangle plein (pour ajouter des fonctionnalités), faire de l'animation, gestion clavier, gestion d'algorithmes (pour améliorer la métaprogrammation), mais je suis passé à d'autres occupations plus utiles, alors j'ai arrêté là. :D 

Fin du tutoriel
Tom's guide dans le monde
  • Allemagne
  • Italie
  • Irlande
  • Royaume Uni
  • Etats Unis
Suivre Tom's Guide
Inscrivez-vous à la Newsletter
  • ajouter à twitter
  • ajouter à facebook
  • ajouter un flux RSS