Suivez-nous sur X
|
|
|
0,
A,
B,
C,
D,
E,
F,
G,
H,
I,
J,
K,
L,
M,
N,
O,
P,
Q,
R,
S,
T,
U,
V,
W,
X,
Y,
Z,
ALL
|
|
0,
A,
B,
C,
D,
E,
F,
G,
H,
I,
J,
K,
L,
M,
N,
O,
P,
Q,
R,
S,
T,
U,
V,
W,
X,
Y,
Z
|
|
0,
A,
B,
C,
D,
E,
F,
G,
H,
I,
J,
K,
L,
M,
N,
O,
P,
Q,
R,
S,
T,
U,
V,
W,
X,
Y,
Z
|
|
A propos d'Obligement
|
|
David Brunet
|
|
|
|
Programmation : C - Graphismes 3D en C (les projections perspectives)
(Article écrit par Pascal Amiable et extrait d'Amiga News Tech - juillet 1990)
|
|
Bonjour à toutes et à tous ! Ce mois-ci, nous allons terminer l'étude des différentes projections planes
par les projections perspectives. Et puis, comme il me restait un peu de temps, j'ai modifié le petit programme
d'affichage d'objet 3D du numéro 23 pour permettre le calcul de la
perspective et l'animation temps réel de cet objet (vous verrez cela se réalise très simplement).
Projections perspectives
Alors que les projections parallèles suivent les règles de la géométrie affine classique, les projections
perspectives suivent leur propre géométrie. Dans une perspective, deux lignes parallèles quelconques ne
sont plus parallèles. La longueur d'un segment est réduite par un facteur proportionnel à sa distance par
rapport à l'observateur. Ceci implique deux choses :
- Aucune information dimensionnelle ne peut être extraite d'une projection perspective.
- Cette compression des distances produit un excellent effet de profondeur, très utile pour le
rendu réaliste de scènes, car elle correspond à notre vision naturelle de l'espace.
En utilisant les coordonnées homogènes, la perspective peut être assimilée à une transformation
classique de l'espace 3D vers l'espace 3D. Dès lors, il ne reste plus qu'à réaliser une projection
orthographique et la projection perspective est réalisée (simple, non ?).
Lorsque la direction de vision est confondue avec un des axes principaux du repère objet, cette
projection perspective est des plus simple. La figure 1 vous montre, de manière intuitive, le
principe de modélisation de la perspective.
On peut s'imaginer que le plan de projection, c'est-à-dire l'écran de votre Amiga, est une vitre
placée entre l'observateur et les objets. La projection perspective est alors simplement l'image
des objets perçus par l'observateur à travers la vitre.
Comme on désire toujours obtenir les coordonnées de projections dans le plan de projection (écran),
il est nécessaire de décrire l'ensemble des données utiles à une perspective, par rapport au plan
de projection. Une perspective simple est définie par trois éléments :
- Un centre de projection [0, 0, L, 1].
- Une direction de vision [0, 0, -1, 0].
- Un plan de projection Z=0.
Un point P [x y z w] se transforme en perspective en P' [x' y' z'w'] à l'aide de la matrice suivante :
A partir de cette matrice de transformation perspective, on obtient la matrice de projection perspective
en multipliant cette matrice par une matrice de projection orthographique suivant z :
A partir de cette matrice de projection perspective, on peut calculer les coordonnées du point projeté :
Soit :
- x'=x
- y'=y
- z'=0
- w'=-a*z+w avec toujours a=-1/L.
On revient en coordonnées unitaires non homogène 2D pour affichage à l'écran. C'est-à-dire :
- x"=x'/w'
- y"=y'/w'
- Soit : x"=x*(1/(-z/L+1))
- y"=y"(1/(-z/L+1))
Ces équations correspondent à une perspective avec seul point de fuite.
Pour une direction de vision quelconque, c'est-à-dire d'une perspective générale on passe simplement
le cas général au cas particulier ci-dessus en utilisant un changement de repère.
La matrice de transformation M Persp (phi,teta) est égale à la multiplication de la matrice M
Persp(z) ci-dessus par une matrice de changement de repère M-1.
M-1 est composée de quatre matrices :
- T-1 : translation à l'observateur.
- R-1(z) : rotation d'axe z.
- R-1(y) : rotation d'axe y.
- E-1 : échange de coordonnées.
M-1 se calcule de la manière suivante :
M-1=E-1.R-1(y).R-1(z).T-1
|
La matrice de transformation générale se calcule donc assez simplement.
Mtranspersp(phi,teta)=Mtranspersp.M-1
|
On peut alors déterminer la matrice de projection perspective de la même manière en
remplaçant Mtranspersp par la matrice Mprojpersp.
Mprojpersp(phi,teta)=Mprojpersp.M-1
|
Ce qui donne en calculant P'=P.Mprojpersp(phi,teta) :
- x'=-x.sin(phi)+y.cos(phi)-Tx.w
- y=-x.sin(teta).cos(phi)-y.sin(teta).cos(phi)
- z'=0
- w'=-(x.cos(teta).cos(phi))/L-(y.cos(teta).sin(phi))/L-(z.sin(teta))/L+((Tz+L).w)/L
On ramène l'ensemble en coordonnées unitaires 2D non homogènes à l'aide des formules
x"=x'/w' et y"=y'/w' avec pour hypothèse w=1.
A partir des données de base :
Centre de projection `obsv' et Point vise 'vise' nous avons la direction de vision :
Direction=[xobsv-xvise,yobsv-yvise,zobsv-zvise,1]
|
...et sa norme vaut :
|Direction|=sqrt((xobsv-xvise)^2+(yobsv-yvise)^2+(zobsv-zvise)^2)
|
La projection sur le plan z=0 donne :
Dirproj=[xobsv-xvise,yobsv-yvise,0,1]
|
Ce vecteur peut être normalisé :
|Dirproj|=sqrt((xobsv-xvise)^2+(yobsv-yvise)^2)
|
Dès lors, nous avons directement les deux cosinus et sinus de la projection :
- cos(phi)=(xobsv-xvise)/|Dirproj|
- sin(phi)=(yobsv-yvise)/|Dirproj|
- cos(teta)=|Dirproj|/|Direction|
- sin(teta)=(zobsv-zvise)/|Direction|
Cette astuce évite le calcul des cosinus et sinus très gourmands en temps de calcul.
Affichage objet 3D
Bien, sur ces fortes paroles je vais arrêter la théorie et passer à la pratique en vous
proposant une version remaniée du petit programme d'affichage d'objet en 3D du numéro 23.
En prime et sans supplément, j'ai modifié le programme pour qu'il y est une petite animation
de l'objet. Bien sûr, cette animation n'est pas parfaite mais ne désespérez pas, nous étudierons
l'animation un peu plus tard.
/*
Programmation : C - Graphismes 3D en C (les projections perspectives)
Article écrit par Pascal Amiable et extrait d'Amiga News Tech - juillet 1990
http://obligement.free.fr/articles/c_3denc_projections_perspectives.php
Adapté à VBCC par Tygre, 2020/09/20
*/
/* Déclaration des fichiers inclus. */
#include <stdio.h>
#include <math.h>
#include <intuition/intuition.h>
#include <intuition/intuitionbase.h>
#include <graphics/rastport.h>
#include <graphics/gfxbase.h>
#include <graphics/gfxmacros.h>
#include <exec/types.h>
/* Déclaration des constantses. */
#define INTUITION_REV 0L
#define GFX_REV 0L
#define CIAAPRA 0xBFE001 /* Adresse du port CIA A */
/* Déclaration des variables et structures globales */
extern double atof(); /* Mandatory with VBCC */
SHORT pointE1[100][2], pointE2[100][2];
float pointH3D[100][4];
SHORT *ligneE1[100][2], *ligneE2[100][2];
struct Screen *OpenScreen();
struct Screen *ecran = NULL;
struct IntuitionBase *IntuitionBase = NULL;
struct GfxBase *GfxBase = NULL;
struct RastPort *rp = NULL;
SHORT nbrpoint = 0, nbrligne = 0;
float obs[4], vise[4], matrice[4][4];
/* Programme principal (ne fait pas grand chose) */
main()
{
char *test;
void chargepoints(), chargelignes(), init(), ouvreecran(), referme();
void transforme(), affiche();
test = (char *)CIAAPRA;
chargepoints();
chargelignes();
init();
ouvreecran();
vise[0] = 0;
vise[1] = 0; /* Point visé par l'observateur */
vise[2] = 0;
vise[3] = 1;
obs[0] = 250;
obs[1] = 250; /* Position de l'observateur */
obs[2] = 0;
obs[3] = 1;
transforme(pointE1);
WaitTOF();
while(!((*test & 0x40) - 64)) /* Test du bouton de la souris */
{
affiche(1, ligneE1);
obs[0]--;
obs[1]--; /* Trajectoire linéaire */
obs[2]++;
transforme(pointE2);
WaitTOF();
affiche(0, ligneE1); /* Attention, il n'y a pas de clipping */
affiche(1, ligneE2); /* ce qui explique des effects bizarres dans les points extrêmes */
obs[0]--;
obs[1]--;
obs[2]++;
transforme(pointE1);
WaitTOF();
affiche(0, ligneE2);
}
referme();
}
/* Fonction ouvrant les bibliothèques Intuition et Graphics */
void init()
{
IntuitionBase = (struct IntuitionBase *)OpenLibrary("intuition.library", INTUITION_REV);
if (IntuitionBase == NULL)
{
printf("Mais où est donc passé intuition ?\n");
exit(FALSE);
}
GfxBase = (struct GfxBase *)OpenLibrary("graphics.library", GFX_REV);
if (GfxBase == NULL)
{
printf("Mais où est donc passé graphics ?\n");
exit(FALSE);
}
}
/* Fonction ouvrant un simple écran et initialisant le Rastport */
void ouvreecran()
{
struct NewScreen nouvecran;
nouvecran.LeftEdge = 0;
nouvecran.TopEdge = 0;
nouvecran.Width = 320;
nouvecran.Height = 256;
nouvecran.Depth = 2;
nouvecran.DetailPen = 0;
nouvecran.BlockPen = 0;
nouvecran.ViewModes = 0;
nouvecran.Type = CUSTOMSCREEN;
nouvecran.Font = NULL;
nouvecran.DefaultTitle = NULL;
nouvecran.Gadgets = NULL;
nouvecran.CustomBitMap = NULL;
ecran = OpenScreen(&nouvecran);
if (ecran == NULL)
{
printf("L'écran ne veut pas apparaître\n");
if (GfxBase) CloseLibrary(GfxBase);
if (IntuitionBase) CloseLibrary(IntuitionBase);
exit(FALSE);
}
rp = &(ecran->RastPort);
}
/* Fonction chargeant le fichier de points 3D */
void chargepoints()
{
FILE *fich, *fopen();
int i, j;
char tab[100];
fich = fopen("point.dat", "r");
if (fich == NULL)
{
printf("Problème d'ouverture du fichier point.dat\n");
exit(FALSE);
}
else
{
fgets(tab, 100, fich);
nbrpoint = atoi(tab);
printf("Chargement de la structure POINT en cours pour %d points...\n", nbrpoint);
for(i = 0; i < nbrpoint; i++)
{
for(j = 0; j < 4; j++)
{
fgets(tab, 100, fich);
pointH3D[i][j] = atof(tab);
}
}
}
fclose(fich);
}
/* Fonction chargeant le fichier de segments */
void chargelignes()
{
FILE *fich, *fopen();
int i, j;
char tab[100];
fich = fopen("ligne.dat", "r");
if (fich == NULL)
{
printf("Problème d'ouverture du fichier ligne.dat\n");
exit(FALSE);
}
else
{
fgets(tab, 100, fich);
nbrligne = atoi(tab);
printf("Chargement de la structure LIGNE en cours pour %d lignes...\n", nbrligne);
for(i = 0; i < nbrligne; i++)
{
for(j = 0; j < 2; j++)
{
fgets(tab, 100, fich);
ligneE1[i][j] = &pointE1[atoi(tab)][0];
ligneE2[i][j] = &pointE2[atoi(tab)][0];
}
}
}
fclose(fich);
}
/* Fonction appliquant une projection perspective sur l'objet */
void transforme(pointE)
SHORT pointE[100][2];
{
int i;
float pointinter[4];
void calculepersp();
calculepersp();
for(i = 0; i < nbrpoint; i++)
{
pointinter[0] = pointH3D[i][0] * matrice[0][0] + pointH3D[i][1] * matrice[1][0] + matrice[3][0];
pointinter[1] = pointH3D[i][0] * matrice[0][1] + pointH3D[i][1] * matrice[1][1] + pointH3D[i][2] * matrice[2][1] + matrice[3][1];
pointinter[3] = pointH3D[i][0] * matrice[0][3] + pointH3D[i][1] * matrice[1][3] + pointH3D[i][2] * matrice[2][3] + matrice[3][0];
pointE[i][0] = (SHORT)(320 * (pointinter[0] / pointinter[3])) - 160;
pointE[i][1] = (SHORT)(256 * (pointinter[1] / pointinter[3])) - 128;
}
}
/* Fonction calculant la matrice de projection perspective */
void calculepersp()
{
float distance, distproj, costheta, cosphi, sintheta, sinphi;
float d[4];
int i, j;
/* Calcul du vecteur direction */
for(i = 0; i < 3; i++)
d[i] = obs[i] - vise[i];
/* Calcul des cosinus, sinus et distances */
distance = (float)sqrt(d[0]*d[0] + d[1]*d[1] + d[2]*d[2]);
distproj = (float)sqrt(d[0]*d[0] + d[1]*d[1]);
costheta = distproj / distance;
sintheta = d[2] / distance;
cosphi = d[0] / distproj;
sinphi = d[1] / distproj;
/* Calcul de la matrice de projection perspective */
matrice[0][0] = -sinphi;
matrice[1][0] = cosphi;
matrice[2][0] = 0;
matrice[3][0] = -d[0];
matrice[0][1] = -sintheta * cosphi;
matrice[1][1] = -sintheta * sinphi;
matrice[2][1] = costheta;
matrice[3][1] = -d[1];
matrice[0][2] = 0;
matrice[1][2] = 0;
matrice[2][2] = 0;
matrice[3][2] = 0;
matrice[0][3] = -(costheta * cosphi) / distance;
matrice[1][3] = -(costheta * sinphi) / distance;
matrice[2][3] = -sintheta / distance;
matrice[3][3] = -(d[2] + distance) / distance;
}
/* Fonction affichant l'objet à l'écran */
void affiche(allume, ligne)
SHORT allume;
SHORT *ligne[100][2];
{
int i;
SetAPen(rp, allume);
for(i = 0; i < nbrligne; i++)
{
/* Quelques tests pour éviter les débordements */
if (ligne[i][0][0] < 0) ligne[i][0][0] = 0;
if (ligne[i][0][0] > 319) ligne[i][0][0] = 319;
if (ligne[i][1][0] < 0) ligne[i][1][0] = 0;
if (ligne[i][1][0] > 319) ligne[i][1][0] = 319;
if (ligne[i][0][1] < 0) ligne[i][0][1] = 0;
if (ligne[i][0][1] > 255) ligne[i][0][1] = 255;
if (ligne[i][1][1] < 0) ligne[i][1][1] = 0;
if (ligne[i][1][1] > 255) ligne[i][1][1] = 255;
Move(rp, ligne[i][0][0], ligne[i][0][1]);
Draw(rp, ligne[i][1][0], ligne[i][1][1]);
}
}
/* Fonction fermant l'écran et les bibliothèques */
void referme()
{
if (ecran) CloseScreen(ecran);
if (IntuitionBase) CloseLibrary(IntuitionBase);
if (GfxBase) CloseLibrary(GfxBase);
}
|
Mise à jour de septembre 2020 : une archive contenant le listing adapté à vbcc, et avec l'exécutable
compilé par VBCC, a été réalisée par Yann-Gaël Guéhéneuc et est disponible sur
obligement.free.fr/files/ant3dgraphicsdrawlinesmoving.lha.
Annexe : bibliothèque de fonction
En examinant l'important courrier suscité par les articles sur le 3D en C et d'autres publications de
mes confrères, les questions qui reviennent le plus souvent concernent l'utilité de passer par les
bibliothèques de fonctions (library) disponibles dans l'environnement Amiga pour accéder aux routines
systèmes. Je vais donc faire le point sur ce sujet.
Une bibliothèque de fonction est un ensemble de routines, que votre application peut utiliser et qui
vous déchargent d'un certain travail, en général la programmation de fonctions de bas niveau
(par exemple la gestion des ressources). Ces bibliothèques résident soit en ROM (exemple : graphics.library,
intuition.library), soit sur disque (mathtrans.library, icon.library). A ce sujet, rien ne vous empêche de
créer les vôtres.
Étudions maintenant l'accès à ces routines et plus particulièrement, celles intégrées en ROM. Les
différentes routines système sont positionnées de manière séquentielle dans la ROM. On peut donc
accéder directement à une fonction, quelle qu'elle soit, pour peu que l'on connaisse l'adresse de début de la
routine (instruction "JSR" en assembleur ou par un pointeur de fonction en C). Mais cette méthode, utilisée
(malheureusement) par certains programmeurs peu scrupuleux, est très dangereuse. En effet, l'adresse physique
de cette routine peut évoluer d'une révision de système à l'autre (même si les concepteurs de l'Amiga
font tout pour éviter cela, c'est parfois obligatoire) et votre programme qui fonctionnait très bien va aller
rendre visite au Guru parce qu'il pointe désormais n'importe où dans la ROM et non plus sur la routine prévue.
Pour éviter cela, on va utiliser un adressage indirect et c'est là que la notion de bibliothèque va servir.
Chaque série de routines est organisée en bibliothèque suivant un thème précis (exemple :
graphics.library contient les routines graphiques). La bibliothèque principale est l'exec.library. En tête
de cette bibliothèque, positionnée à une adresse absolue immuable, on trouve une table de pointeur. Il
s'agit de pointeurs sur les fonctions d'Exec et sur les autres bibliothèques de la ROM. L'accès aux fonctions
d'Exec se fait via cette table.
Par contre, l'accès aux fonctions des autres bibliothèques nécessite une étape supplémentaire, le chargement de
la table de pointeurs lié à cette bibliothèque. Pour ce faire on utilise la fonction OpenLibrary() en lui passant
comme paramètres le nom de la bibliothèque et son numéro de version. Une fois cette table chargée on accède aux
fonctions, comme pour Exec. Dès lors, il n'y a plus de problème de compatibilité car à chaque révision de système
la table des pointeurs est aussi remise à jour.
Une fois votre application terminée, il ne faut pas oublier de refermer les bibliothèques ouvertes à l'aide de
CloseLibrary() avec comme paramètre l'adresse de la table des pointeurs de la bibliothèque.
Dans les différents programmes de la série le "3D C facile" des exemples d'ouverture et de fermeture des
bibliothèques sont fournis. J'espère que cela répond à vos questions.
|