Como eso de los gráficos es una de las cosas más vistosas que se pueden hacer con un ordenador, especialmente cuando se trata de 3 dimensiones, vamos a profundizar un poco más, y a poner un par de ejemplos.
Las rotaciones son sencillas cuando se tiene una cierta base de álgebra de matrices, y no tanto si no es el caso. De cualquier modo, podemos usar las formulitas "tal cual", sin saber cómo trabajan.
Para girar en torno al eje X, la matriz de rotación es:
¦ 1 0 0 ¦
donde sx es el seno del ángulo que se rota
¦ 0 cx sx ¦
y cx es su coseno.
¦ 0 -sx cx ¦
Si esto lo convertimos a formulitas
x = x
y = (y * cx) - (z * sx)
z = (y * sx) + (z * cx)
De forma similar, en torno al eje Y tenemos:
¦ cy 0 -sy ¦
igualmente, sy y cy son seno y coseno del
¦ 0 1 0 ¦
ángulo girado
¦ sy 0 cy ¦
que queda como
x = (x * cy) + (z * sx)
y = y
z = (z * cy) - (x * sy)
Y alrededor del eje Z:
¦ cz sz 0 ¦
cz y sz son... ¡ lo de siempre !
¦ -sz cz 0 ¦
¦ 0 0 1 ¦
que queda como
x = (x * cz) - (y * sz)
y = (x * sz) + (y * cz)
z = z
(esta última es la rotación en el plano que habíamos
visto).
Hay autores que usan estos 3 grupos de fórmulas de forma independiente, y hay quien prefiere multiplicar las tres matrices para obtener la que sería la "matriz de giro" de un punto cualquiera, que queda algo así
¦ (cz*cy)+(sz*sx*sy) (cy*-sz)+(cz*sx*sy)
(cx*sy) ¦
¦
¦
¦ (sz*cx)
(cz*cx)
(-sx) ¦
¦
¦
¦ (-sy*cz)+(sz*sx*cy) (sz*sy)+(cz*sx*cy)
(cx*cy) ¦
En cualquier caso, vamos a dejarnos de rollos y a ver un par de ejemplos
de aplicación de esto.
El primero está basado en un fuente de Peter M. Gruhn, que es
muy fácil de seguir, porque la parte encargada de las rotaciones
sigue claramente los 3 grupos de fórmulas anteriores. El resto
es la definición de la figura, las rutinas para dibujar un punto
o una línea (que ya hemos visto) y poco más. Allá
va:
{--------------------------} { Ejemplo en Pascal: } { } { Rotar un cubo en 3D } { ROTACUBO.PAS } { } { Este fuente procede de } { CUPAS, curso de Pascal } { por Nacho Cabanes } { } { Comprobado con: } { - Turbo Pascal 7.0 } {--------------------------} program RotaCubo; {$G+} { Basado en un fuente de Dominio Público, por Peter M. Gruhn 1993 El original se puede encontrar en los SWAG Modificaciones por Nacho Cabanes, 1996: - Modo 320x200x256, sin BGI (usa algoritmo de Bresenham para dibujar líneas y sincroniza con el barrido de la VGA). - Emplea algo de ensamblador (ver Ampliación 5) - El cubo se mueve sólo. Posibles mejoras (muchas, sólo pongo algunas como ejemplo): - Emplear aritmética entera, y tablas de senos y cosenos para mayor velocidad, aunque en este caso no es necesario, porque se rotan muy pocos puntos. - Que las rotaciones no sean aditivas (se vuelva a rotar a partir del original, no de la figura ya rotada, para que los errores no se vayan sumando). - Más flexibilidad: definir las líneas a partir de sus dos vértices en vez de crear las figuras "a pelo". - Definir caras 3D para dibujar figuras sólidas. } uses crt; const gradRad = 1 {grados} * 3.1415926535 {radianes} / 180 {por grado}; { Convierte un grado a radianes (sin y cos usan radianes) } type punto = record { Punto en 3d } x, y, z : real; end; var img : array [0..7] of punto; { Nuestra imagen tendrá 8 puntos } tecla: char; color: byte; procedure retrace; assembler; { Espera el barrido de la pantalla } asm mov dx,3dah @vert1: in al,dx test al,8 jz @vert1 @vert2: in al,dx test al,8 jnz @vert2 end; procedure init; { Inicializa } begin asm mov ax, $13 { Modo 320x200x256 } int $10 end; { Datos de la imagen } img[0].x := -35; img[0].y := -35; img[0].z := -35; img[1].x := 35; img[1].y := -35; img[1].z := -35; img[2].x := 35; img[2].y := 35; img[2].z := -35; img[3].x := -35; img[3].y := 35; img[3].z := -35; img[4].x := -35; img[4].y := -35; img[4].z := 35; img[5].x := 35; img[5].y := -35; img[5].z := 35; img[6].x := 35; img[6].y := 35; img[6].z := 35; img[7].x := -35; img[7].y := 35; img[7].z := 35; end; Procedure Ponpixel (X,Y : Integer; Col : Byte); assembler; { Dibuja un punto en la pantalla gráfica, en 320x200x256 } Asm mov ax,$A000 mov es,ax mov bx,[X] mov dx,[Y] mov di,bx mov bx, dx { bx = dx } shl dx, 8 { dx = dx * 256 } shl bx, 6 { bx = bx * 64 } add dx, bx { dx = dx + bx (= y*320) } add di, dx { Posición final } mov al, [Col] stosb end; procedure LineaB(x, y, x2, y2 : word; color: byte); { Dibuja una línea, basado en el algoritmo de Bresenham } { Original de Sean Palmer; una pequeña corrección por Nacho Cabanes } var d, dx, dy, { Salto total según x e y } ai, bi, xi, yi { Incrementos: +1 ó -1, según se recorra } : integer; begin if (x=x2) and (y=y2) then { Corrige un fallo: si es un sólo punto } begin { el algoritmo (tal y como era) falla } PonPixel(x,y,color); exit; end; if (x < x2) then { Si las componentes X están ordenadas } begin xi := 1; { Incremento +1 } dx := x2 - x; { Espacio total en x } end else { Si no están ordenadas } begin xi := - 1; { Increm. -1 (hacia atrás) } dx := x - x2; { y salto al revés (negativo) } end; if (y < y2) then { Análogo para las componentes Y } begin yi := 1; dy := y2 - y; end else begin yi := - 1; dy := y - y2; end; PonPixel(x, y,color); { Dibujamos el primer punto } if dx > dy then { Si hay más salto según x que según y } begin { (recta más cerca de la horizontal) } ai := (dy - dx) * 2; { Variables auxiliares del algoritmo } bi := dy * 2; { ai y bi no varían; d comprueba cuando } d := bi - dx; { debe cambiar la coordenada y } repeat if (d >= 0) then { Comprueba si hay que avanzar según y } begin y := y + yi; { Incrementamos Y (+1 ó -1) } d := d + ai; { y la variable de control } end else d := d + bi; { Si no varía y, d sí lo hace según bi } x := x + xi; { Incrementamos X como corresponda } PonPixel(x, y, color); { Dibujamos el punto } until (x = x2); { Se repite hasta alcanzar el final } end else { Si hay más salto según y que según x } begin { (más vertical), todo similar } ai := (dx - dy) * 2; bi := dx * 2; d := bi - dy; repeat if (d >= 0) then begin x := x + xi; d := d + ai; end else d := d + bi; y := y + yi; PonPixel(x, y, color); until (y = y2); end; end; procedure linea(x1, y1, z1, x2, y2, z2 : real); { Convierte las coordenadas de real a entero y muestra centrado en pantalla. La coordenada Z se desprecia en este ejemplo, pero se podría usar para dar una mayor sensación de perspectiva (cónica en vez de cilíndrica. } begin lineaB(round(x1) + 160, round(y1) + 100, round(x2) + 160, round(y2) + 100, color); end; procedure dibujaImg; { Dibuja la imagen (ésta en concreto -> poco versátil ) } begin linea(img[0].x, img[0].y, img[0].z, img[1].x, img[1].y, img[1].z); linea(img[1].x, img[1].y, img[1].z, img[2].x, img[2].y, img[2].z); linea(img[2].x, img[2].y, img[2].z, img[3].x, img[3].y, img[3].z); linea(img[3].x, img[3].y, img[3].z, img[0].x, img[0].y, img[0].z); linea(img[4].x, img[4].y, img[4].z, img[5].x, img[5].y, img[5].z); linea(img[5].x, img[5].y, img[5].z, img[6].x, img[6].y, img[6].z); linea(img[6].x, img[6].y, img[6].z, img[7].x, img[7].y, img[7].z); linea(img[7].x, img[7].y, img[7].z, img[4].x, img[4].y, img[4].z); linea(img[0].x, img[0].y, img[0].z, img[4].x, img[4].y, img[4].z); linea(img[1].x, img[1].y, img[1].z, img[5].x, img[5].y, img[5].z); linea(img[2].x, img[2].y, img[2].z, img[6].x, img[6].y, img[6].z); linea(img[3].x, img[3].y, img[3].z, img[7].x, img[7].y, img[7].z); linea(img[0].x, img[0].y, img[0].z, img[5].x, img[5].y, img[5].z); linea(img[1].x, img[1].y, img[1].z, img[4].x, img[4].y, img[4].z); end; procedure rotx; { Rotación en torno al eje X. Un poco de álgebra lineal... } var i : integer; begin color := 0; dibujaImg; for i := 0 to 7 do begin img[i].x := img[i].x; img[i].y := img[i].y * cos(gradRad) + img[i].z * sin(gradRad); img[i].z := -img[i].y * sin(gradRad) + img[i].z * cos(gradRad); end; color := 15; dibujaImg; end; procedure roty; { Rotación en torno al eje Y } var i : integer; begin color := 0; dibujaImg; for i := 0 to 7 do begin img[i].x := img[i].x * cos(gradRad) - img[i].z * sin(gradRad); img[i].y := img[i].y; img[i].z := img[i].x * sin(gradRad) + img[i].z * cos(gradRad); end; color := 15; dibujaImg; end; procedure rotz; { Rotación en torno al eje Z } var i : integer; begin color := 0; dibujaImg; for i := 0 to 7 do begin img[i].x := img[i].x * cos(gradRad) + img[i].y * sin(gradRad); img[i].y := -img[i].x * sin(gradRad) + img[i].y * cos(gradRad); img[i].z := img[i].z; end; color := 15; dibujaImg; end; begin init; { Inicializar } repeat retrace; rotx; { Rotar y dibujar } retrace; roty; retrace; rotz; until (keypressed) { Hasta pulsar ESC } and (readkey = #27); asm mov ax, 3 { Modo texto } int $10 end; end.
Ahora vamos a ver otro ejemplo bastante más elaborado. Este está basado en un fuente de Bas van Gaalen.
{--------------------------} { Ejemplo en Pascal: } { } { Rota una E sólida 3D } { ROTAE.PAS } { } { Este fuente procede de } { CUPAS, curso de Pascal } { por Nacho Cabanes } { } { Comprobado con: } { - Turbo Pascal 7.0 } {--------------------------} {------------------------------------------------} { E rotada en 3D } { Por Nacho Cabanes, 96 } { } { Basado (mucho) en 3DHEXASH, de Bas Van Gaalen } { (dominio público, recopilado en GFXFX) } { } { Modificaciones sobre el original: } { - Comentado, para que sea más fácil de seguir } { - Traducido a español :-) } { - Cambiadas sentencias Inline por Asm } { - Añadido un fondo al dibujo } { - La figura ahora es una E :-) } { } { Otras posibles mejoras: } { - Sombreado en función de la dirección de } { cada cara, no de su distancia. } {------------------------------------------------} program RotaE; {$G+} uses crt; const divd=128; { Para convertir de reales a enteros los senos/cosenos } dist=200; { Distancia del observador } segVideo:word=$a000; { Segmento de video: VGA modo gráfico } NumPuntos = 23; { Numero de puntos } NumPlanos = 19; { Número de caras } { Ahora van los puntos en sí } punto:array[0..NumPuntos,0..2] of integer=( (-40, 40, 20),( 40, 40, 20),( 40, 27, 20),(-27, 27, 20), { E superior } (-27, 7, 20),( 27, 7, 20),( 27, -7, 20),(-27, -7, 20), (-27,-27, 20),( 40,-27, 20),( 40,-40, 20),(-40,-40, 20), (-40, 40, 0),( 40, 40, 0),( 40, 27, 0),(-27, 27, 0), { E inferior } (-27, 7, 0),( 27, 7, 0),( 27, -7, 0),(-27, -7, 0), (-27,-27, 0),( 40,-27, 0),( 40,-40, 0),(-40,-40, 0)); { Y ahora los 4 puntos que forman cada plano } plano:array[0..NumPlanos,0..3] of byte=( (0,3,8,11),(0,1,2,3),(4,5,6,7),(8,9,10,11), { Superior } (12,15,20,23),(12,13,14,15),(16,17,18,19),(20,21,22,23), { Inferior } (1,2,14,13),(2,3,15,14),(3,4,16,15),(4,5,17,16), { Uniones } (6,7,19,18),(7,8,20,19),(8,9,21,20),(9,10,22,21), (10,11,23,22),(11,0,12,23),(0,1,13,12),(5,6,18,17) ); var { Coordenada "z" de cada plano, usada para sombrear: los más lejanos serán más oscuros } polyz:array[0..NumPlanos] of integer; pind:array[0..NumPlanos] of byte; { Tablas de senos y cosenos } ctab:array[0..255] of integer; stab:array[0..255] of integer; { La pantalla temporal en la que realmente se dibujará y el fondo } pantTemp, fondo:pointer; { Las direcciones en que empiezan ambos } segTemp, segFondo:word; { Límites de la pantalla, para no dibujar fuera } minx,miny,maxx,maxy:integer; { -------------------------------------------------------------------------- } procedure retrace; assembler; asm { Sincroniza con el barrido de la VGA } mov dx,3dah; @vert1: in al,dx; test al,8; jz @vert1 @vert2: in al,dx; test al,8; jnz @vert2; end; procedure copia(src,dst:word); assembler; asm { Copia 64K de una dirección de memoria a otra } push ds; mov ax,[dst]; mov es,ax; mov ax,[src]; mov ds,ax xor si,si; xor di,di; mov cx,320*200/2; rep movsw; pop ds; end; procedure setpal(c,r,g,b:byte); assembler; asm { Cambia un color de la paleta: fija la cantidad de rojo, verde y azul } mov dx,3c8h; mov al,[c]; out dx,al; inc dx; mov al,[r] out dx,al; mov al,[g]; out dx,al; mov al,[b]; out dx,al; end; function coseno(i:byte):integer; begin coseno:=ctab[i]; end; function seno(i:byte):integer; begin seno:=stab[i]; end; { Seno y coseno, a partir de tablas para mayor velocidad } { -------------------------------------------------------------------------- } procedure horline(xb,xe,y:integer; c:byte); assembler; { Dibuja una línea horizontal a una cierta altura y con un color dado } asm mov bx,xb mov cx,xe cmp bx,cx jb @skip xchg bx,cx @skip: inc cx sub cx,bx mov es,segTemp mov ax,y shl ax,6 mov di,ax shl ax,2 add di,ax add di,bx mov al,c shr cx,1 jnc @skip2 stosb @skip2: mov ah,al rep stosw @out: end; function MaxI(A,B:Integer):Integer; assembler; { Valor máximo de 2 dados } asm mov ax, a mov bx, b cmp ax,bx jg @maxax xchg ax, bx @maxax: end; function MinI(A,B:Integer):Integer; assembler; { Valor mínimo de 2 dados } asm mov ax, a mov bx, b cmp ax,bx jl @minax xchg ax, bx @minax: end; function EnRango(valor,min,max:integer):integer; assembler; { Comprueba si un valor está entre dos dados } asm mov ax, valor mov bx, min mov cx, max cmp ax,bx jg @maxAx xchg ax, bx @maxAx: cmp ax,cx jl @minAx xchg ax, cx @minAx: end; procedure polygon( x1,y1, x2,y2, x3,y3, x4,y4 :integer; c:byte); { Dibuja un polígono, dados sus 4 vértices y el color } { Este sí es el original de Bas van Gaalen intacto... O:-) } var pos:array[0..199,0..1] of integer; xdiv1,xdiv2,xdiv3,xdiv4:integer; ydiv1,ydiv2,ydiv3,ydiv4:integer; dir1,dir2,dir3,dir4:byte; ly,gy,y,tmp,paso:integer; begin { Determinar punto más alto y más bajo y ventana vertical } ly:=MaxI(MinI(MinI(MinI(y1,y2),y3),y4),miny); gy:=MinI(MaxI(MaxI(MaxI(y1,y2),y3),y4),maxy); if ly>maxy then exit; if gy<miny then exit; { Ver dirección (-1=arriba, 1=abajo) y calcular constantes } dir1:=byte(y1<y2); xdiv1:=x2-x1; ydiv1:=y2-y1; dir2:=byte(y2<y3); xdiv2:=x3-x2; ydiv2:=y3-y2; dir3:=byte(y3<y4); xdiv3:=x4-x3; ydiv3:=y4-y3; dir4:=byte(y4<y1); xdiv4:=x1-x4; ydiv4:=y1-y4; y:=y1; paso:=dir1*2-1; if y1<>y2 then begin repeat if EnRango(y,ly,gy)=y then begin tmp:=xdiv1*(y-y1) div ydiv1+x1; pos[y,dir1]:=EnRango(tmp,minx,maxx); end; inc(y,paso); until y=y2+paso; end else begin if (y>=ly) and (y<=gy) then begin pos[y,dir1]:=EnRango(x1,minx,maxx); end; end; y:=y2; paso:=dir2*2-1; if y2<>y3 then begin repeat if EnRango(y,ly,gy)=y then begin tmp:=xdiv2*(y-y2) div ydiv2+x2; pos[y,dir2]:=EnRango(tmp,minx,maxx); end; inc(y,paso); until y=y3+paso; end else begin if (y>=ly) and (y<=gy) then begin pos[y,dir2]:=EnRango(x2,minx,maxx); end; end; y:=y3; paso:=dir3*2-1; if y3<>y4 then begin repeat if EnRango(y,ly,gy)=y then begin tmp:=xdiv3*(y-y3) div ydiv3+x3; pos[y,dir3]:=EnRango(tmp,minx,maxx); end; inc(y,paso); until y=y4+paso; end else begin if (y>=ly) and (y<=gy) then begin pos[y,dir3]:=EnRango(x3,minx,maxx); end; end; y:=y4; paso:=dir4*2-1; if y4<>y1 then begin repeat if EnRango(y,ly,gy)=y then begin tmp:=xdiv4*(y-y4) div ydiv4+x4; pos[y,dir4]:=EnRango(tmp,minx,maxx); end; inc(y,paso); until y=y1+paso; end else begin if (y>=ly) and (y<=gy) then begin pos[y,dir4]:=EnRango(x4,minx,maxx); end; end; for y:=ly to gy do horline(pos[y,0],pos[y,1],y,c); end; { -------------------------------------------------------------------------- } procedure quicksort(lo,hi:integer); { Una de las rutinas de ordenación más habituales. Mucho mejor que burbuja (por ejemplo) cuando hay bastantes puntos } procedure sort(l,r:integer); var i,j,x,y:integer; begin i:=l; j:=r; x:=polyz[(l+r) div 2]; repeat while polyz[i]<x do inc(i); while x<polyz[j] do dec(j); if i<=j then begin y:=polyz[i]; polyz[i]:=polyz[j]; polyz[j]:=y; y:=pind[i]; pind[i]:=pind[j]; pind[j]:=y; inc(i); dec(j); end; until i>j; if l<j then sort(l,j); if i<r then sort(i,r); end; begin sort(lo,hi); end; { -------------------------------------------------------------------------- } procedure rotarImg; { Pues eso ;-) } const xst=1; yst=2; zst=-3; var xp,yp,z:array[0..NumPuntos] of integer; x,y,i,j,k: integer; n,Key,angx,angy,angz: byte; begin angx:=0; angy:=0; angz:=0; fillchar(xp,sizeof(xp),0); fillchar(yp,sizeof(yp),0); repeat copia(segFondo,segTemp); for n:=0 to NumPuntos do begin { Proyectamos las coordenadas en 3D y luego a 2D } i:=(coseno(angy)*punto[n,0]-seno(angy)*punto[n,2]) div divd; j:=(coseno(angz)*punto[n,1]-seno(angz)*i) div divd; k:=(coseno(angy)*punto[n,2]+seno(angy)*punto[n,0]) div divd; x:=(coseno(angz)*i+seno(angz)*punto[n,1]) div divd; y:=(coseno(angx)*j+seno(angx)*k) div divd; z[n]:=(coseno(angx)*k-seno(angx)*j) div divd+coseno(angx) div 3; xp[n]:=160+seno(angx)+(-x*dist) div (z[n]-dist); yp[n]:=100+coseno(angx) div 2+(-y*dist) div (z[n]-dist); end; for n:=0 to NumPlanos do begin { Coordenada Z asignada al plano para sombrearlo: en función de la distancia al observador (media de las Z de las esquinas). Está dividido entre 5 y no entre 4 para limitar un poco más el rango de valores que puede tomar } polyz[n]:=(z[plano[n,0]]+z[plano[n,1]]+z[plano[n,2]]+z[plano[n,3]]) div 5; pind[n]:=n; end; quicksort(0,NumPlanos); { Ordenamos los planos } for n:=0 to NumPlanos do { Dibujamos los planos por orden } polygon(xp[plano[pind[n],0]],yp[plano[pind[n],0]], xp[plano[pind[n],1]],yp[plano[pind[n],1]], xp[plano[pind[n],2]],yp[plano[pind[n],2]], xp[plano[pind[n],3]],yp[plano[pind[n],3]],polyz[n]+55); inc(angx,xst); inc(angy,yst); inc(angz,zst); copia(segTemp,segVideo); { Ponemos en la pantalla visible } until keypressed; end; { -------------------------------------------------------------------------- } var i,j:word; begin asm mov ax,13h; int 10h; end; { Modo 320x200, 256 colores } for i:=0 to 255 do ctab[i]:=round(-cos(i*pi/128)*divd); { Creo las tablas } for i:=0 to 255 do stab[i]:=round(sin(i*pi/128)*divd); minx:=0; miny:=0; maxx:=319; maxy:=199; { Límites de la pantalla } getmem(pantTemp,64000); { Reservo la pantalla temporal } segTemp := seg(pantTemp^); getmem(fondo,64000); { Y el fondo } segFondo := seg(fondo^); { Dibujo el fondo } for i:=0 to 319 do for j:=0 to 199 do mem[segFondo:j*320+i]:=(i+j) mod 102 +152; for i:=0 to 255 do stab[i]:=round(sin(i*pi/128)*divd); { Colores del rótulo } for i:=1 to 150 do setpal(i,30+i div 6,20+i div 7,10+i div 7); { Colores del fondo } for i:=151 to 255 do setpal(i,i div 7, i div 7,i div 5); rotarImg; { Se acabó -> liberamos la memoria reservada } freemem(pantTemp,64000); freemem(fondo,64000); { Y volvemos a modo texto } textmode(lastmode); end.
Venga, a experimentar... }:-)