
--------------------------------------------
      Tutorial de programacin grfica
                  Por: FAC
     -----------------------------------
        #8 - Bases de los grficos 3D
--------------------------------------------


Qu tal?  Despus de un largo receso sin tutoriales, aqu les presento
el octavo ejemplar cuyo objetivo principal es establecer las bases
de los grficos tridimensionales y presentar algunas tcnicas que
nos ayudarn a acelerar nuestros programas.

Para cualquier duda, sugerencia o comentario, pueden dirigirse a:

               Alfonso Alba Cadena
               ganzo@galia.fc.uaslp.mx
               (48) 13-34-71


Los tutoriales se han movido a la siguiente direccin en Internet:

        http://galia.fc.uaslp.mx/~ganzo/prog/tutorial.html

donde podrn encontrar tambin otros documentos y herramientas de
programacin.


ACLARACION:
-----------

     La programacin de grficos tridimensionales abarca muchos aspectos
     de todo tipo: matemticas, fsica, algoritmos, optimizacin, etc...,
     por lo tanto, es imposible que en un documento como ste se presenten
     todas las tcnicas de 3D y todas las aplicaciones que tienen.

     Aqu vamos a concentrarnos en algo un poco ms especfico: grficos
     en tiempo real, es decir, aquellos objetos que se "renderizan" al
     mismo tiempo que se ejecuta el programa principal. Muchos de los
     programas de diseo en 3D, como son el 3D Studio, trueSpace,
     POV-Ray, y otros, despliegan sus grficos con una calidad impresionante,
     todo eso a cambio del tiempo que tardan en renderizarse.

     Los grficos en tiempo real no poseen tanta calidad ni son tan
     complejos, pero tienen la ventaja de que podemos desplazarlos,
     rotarlos y transformarlos de la forma en que nosotros queramos
     y veremos los resultados inmediatamente. Los tutoriales tambin
     estarn orientados a cierto tipo de aplicaciones: demos y juegos.

     Tambin tendremos que ver un poco de matemticas, pero para simplificar
     las explicaciones y no hacer el tutorial demasiado largo (como los
     ltimos dos), en algunas ocasiones simplemente presentar las ecuaciones
     o el cdigo para realizar alguna cosa.


     En este tutorial veremos cmo proyectar una figura tridimensional
     en la pantalla (la cual es bidimensional), cmo desplazar y
     cmo rotar una figura tridimensional, pero antes veremos cmo
     acelerar las operaciones algebricas, ya que la computadora tendr
     que hacer MUCHAS operaciones por segundo.



Desplazamiento de bits:
-----------------------

     No se si debera escribir esta parte, puesto que son conocimientos
     de nivel PRIMARIA, pero de todos modos...


     Recuerdan que en el tutorial anterior vimos una forma de acelerar
     las divisiones?  Bueno, vamos a profundizar un poco en eso:

     Digamos que tenemos un nmero en decimal, por ejemplo: 1234,
     vamos a ver qu posicin ocupa cada dgito de nuestro nmero:


     dec. de millar      millares     centenas      decenas      unidades
           0                1            2             3             4


     Bueno, ahora imagnense qu pasara si desplazamos todos los dgitos
     una posicin hacia la izquierda. El resultado sera:

     dec. de millar      millares     centenas      decenas      unidades
          1                 2            3             4            0


     Obviamente, no tenemos nada que desplazar hacia las unidades, por lo
     tanto, insertamos un cero.

     Pero qu ha sucedido? Mgicamente hemos multiplicado el nmero por 10
     con solo desplazar cada dgito hacia la izquierda.

     Si ahora desplazamos una posicin hacia la derecha, obtendramos el
     nmero original, pero si desplazamos DOS posiciones, obtendramos
     lo siguiente:

     dec. de millar      millares     centenas      decenas      unidades
          0                 0            1            2             3

     Ahora hemos dividido el nmero entre 100, ya que desplazamos DOS
     posiciones y 10 elevado a la 2 es igual a 100. Ntese que en este
     caso solo hemos obtenido el COCIENTE de la divisin.

     Todo esto es muy fcil, pero ahora vamos a verlo en binario:

     * Si desplazamos un nmero binario hacia la IZQUIERDA, el resultado
       equivale a multiplicar el nmero por DOS.

     * Si desplazamos un nmero binario hacia la DERECHA, el resultado
       es el cociente de ese nmero dividido entre DOS.

     Vamos a ver rpidamente unos ejemplos:


     Desplazamiento a la izquierda         Desplazamiento a la derecha
     ------------------------------        ---------------------------
               00101  = 5                         11110000 = 240
               01010  = 10                        01111000 = 120
               10100  = 20                        00111100 = 60


     En Pascal y ensamblador, nosotros podemos hacer desplazamientos
     binarios utilizando SHR y SHL para multiplicar y dividir por potencias
     de 2. Esto es ms rpido que hacer la multiplicacin o divisin.

     En Pascal, la sintaxis es:

       a SHR n  : devuelve el resultado de desplazar un nmero (a)
                  n posiciones hacia la derecha.

       a SHL n  : devuelve el resultado de desplazar un nmero (a)
                  n posiciones hacia la izquierda.


     En ensamblador es as:

       SHR reg, n  : desplaza el registro reg hacia la derecha n posiciones.

       SHL reg, n  : desplaza el registro reg hacia la izquierda n posiciones.


     Existe un pequeo problema cuando hacemos un desplazamiento a la
     derecha sobre un nmero negativo. En la representacin binaria de
     un nmero negativo, el bit ms significativo es el que determina el
     signo. Si este bit es 1, entonces el nmero es negativo, pero cuando
     hacemos un desplazamiento binario hacia la derecha, siempre se
     coloca un cero en el ltimo bit, lo que hara que el resultado
     fuera positivo, adems de inesperado.

     El ensamblador incluye la instruccin SAR que desplaza un nmero
     con signo hacia la derecha, conservando el bit de signo. El Pascal
     no cuenta con esta instruccin, por lo que habra que usar el
     operador DIV para obtener el resultado correcto.



Nmeros de Punto Fijo:
----------------------

     Muchas veces, en la programacin de grficos, necesitamos hacer
     operaciones con nmeros reales (nmeros de punto flotante, para
     los usuarios de C++), y tambin, muchas veces, necesitamos hacer
     las operaciones en el menor tiempo posible.

     Pero las operaciones de nmeros reales son muy lentas, y eso es
     un problema cuando hablamos de grficos en tiempo real. La
     solucin es muy simple: no utilizar los nmeros reales.

     Existe una forma de realizar operaciones de punto flotante
     utilizando nicamente nmeros enteros. La idea principal es
     usar un nmero entero para la parte entera y otro nmero para
     la parte fraccionaria. Vamos a ver un ejemplo:


                            / Parte entera: 3
        Nmero:  3.14   ---|
                            \ Parte fraccionaria: 14


     Si multiplicamos nuestro nmero por 100, entonces tendremos
     la parte entera y la parte fraccionaria en un solo nmero entero:

                         3.14 * 100 = 3 1 4
                                      |  |
                       Parte entera --|  |-- Parte fraccionaria


     Si multiplicamos todos nuestros nmeros reales por 100 y tomamos
     solamente la parte entera del resultado, entonces tendremos una
     representacin de nmeros reales con una precisin de 2 dgitos
     despus del punto. Esta representacin se conoce como un nmero
     de PUNTO FIJO, debido a que el nmero de dgitos permitidos
     despus del punto decimal es constante.

     La ventaja de los nmeros de punto fijo es que podemos representar
     nmeros reales utilizando solamente enteros, por lo tanto, todas
     las operaciones algebricas entre nmeros de punto fijo se reducen
     simplemente a operaciones de enteros.

     Para convertir un nmero real a punto fijo solamente hay que
     multiplicarlo por un valor constante que determina la precisin
     del nmero. En el caso amterior usamos el 100, pero en realidad
     puede ser cualquier otro nmero. Para convertir un nmero de punto
     fijo a real, simplemente lo dividimos entre nuestro valor constante.

     Debido a que las multiplicaciones y la divisiones se aceleran si
     las sustitumos por desplazamientos binarios, entonces usaremos
     como valor constante un mltiplo de dos.

     En Pascal tenemos el tipo de datos longint que tiene una longitud
     de 32 bits, con lo que podemos usar 16 bits para la parte entera
     y los otros 16 bits para la parte decimal. Eso significa que
     para convertir un nmero entero a punto fijo, debemos desplazarlo
     16 posiciones hacia la izquierda, lo que equivale a multiplicarlo
     por 65536.  (65536 = 2 elevado a la 16).

     Cuando un nmero de punto fijo utiliza 16 bits para la parte entera
     y 16 bits para la parte fraccionaria, se dice que est en el formato
     de 16.16 . Existen otros formatos como 8.8, 8.24, 24.8, y cada uno
     tiene su uso apropiado.

     Adems, el bit superior se usar como bit de signo, por lo que
     tendremos nicamente 15 bits para la parte entera. Eso nos reduce
     a un rango de -32768 a 32767, el cual es el rango del tipo integer.


Conversin a punto fijo:
------------------------

     Primero debemos definir el tipo de datos para nmeros de punto fijo:

         type Fixed = longint;  { nmeros de punto fijo de 16.16 }


     Para convertir un entero a punto fijo, simplemente lo desplazamos
     16 posiciones hacia la izquierda:

         function Int2Fixed(n : integer) : Fixed;
         begin
              Int2Fixed := Fixed(n) shl 16;
         end;

     Ntese que ANTES de desplazar el nmero hacemos una conversin de
     tipo.

     Para convertir un nmero real a punto fijo, lo multiplicamos por
     65536. Esto es debido a que NO podemos hacer un desplazamiento
     binario sobre un nmero real.

         function Real2Fixed(n : real) : Fixed;
         begin
              Real2Fixed := round(n * 65536);
         end;


Conversin desde punto fijo:
----------------------------

     Para convertir un nmero de punto fijo a entero, simplemente lo
     desplazamos 16 posiciones a la derecha. El problema es que debemos
     hacerlo en ensamblador usando la instruccin SAR, debido al caso
     en que el nmero sea negativo.

     Debido a que estamos usando nmeros de 32 bits (tipo longint), lo
     conveniente es usar instrucciones de 32 bits del 80386:

         function Fixed2Int(n : Fixed) : integer; assembler;
         asm
            mov eax, n
            sar eax, 16
         end;

     En Turbo Pascal, el resultado de una funcin escrita en ensamblador,
     se regresa siempre en AX o en DX:AX.


     La funcin anterior convierte un nmero de punto fijo a entero,
     truncando la parte decimal. He aqu otra funcin que convierte
     a entero, pero en lugar de truncar el nmero, lo redondea:

         function RoundFixed2Int(n : Fixed) : integer; assembler;
         asm
            mov eax, n
            add eax, 8000h
            sar eax, 16
         end;

     Lo que se hace es sumar 32768 (8000h) al nmero y luego convertirlo.
     Ntese que 32768 es el equivalente en punto fijo a 0.5, ya que
     0.5 * 65536 = 32768, y recuerden que para redondear un nmero, hay
     que sumarle 0.5 y tomar la parte entera.


     Para convertir un nmero de punto fijo a real, solamente lo dividimos
     entre 65536:

         function Fixed2Real(n : Fixed) : real;
         begin
              Fixed2Real := n / 65536.0;
         end;


Suma y resta:
-------------

     Para sumar y/o restar nmeros de punto fijo, simplemente lo hacemos
     como si fueran simples enteros (porque eso es lo que son). El resultado
     queda tambin en formato de punto fijo:

             var a, b, c : Fixed;
             begin
                  a := Int2Fixed(30);
                  b := Real2Fixed(20.75);
                  c := a + b;
             end.

     El valor final de c debe ser el equivalente en punto fijo de 50.75,
     es decir: 50.75 * 65536 = 3325952.

     Esto se puede demostrar fcilmente con la ley distributiva:

          (30 * 65536) + (20.75 * 65536) = (30 + 20.75) * 65536


Multiplicacin y divisin:
--------------------------

     La multiplicacin de un nmero de punto fijo por un nmero entero
     o real se hace simplemente multiplicando los dos factores, siendo
     el resultado un nmero de punto fijo.

            var a, c : Fixed;
                b : real;
            begin
                 a := Int2Fixed(10);
                 b := 2.5;
                 c := round(a * b);  { c = 2.5 * 10 * 65536 = 25 * 65536 }
            end;

     Esto se demuestra con la ley asociativa:

          (a * 65536) * b  =  (a * b) * 65536


     La divisin de un nmero de punto fijo entre un nmero entero o real
     se hace de la misma manera:

          (a * 65536) / b  =  (a / b) * 65536


     La multiplicacin de dos nmeros de punto fijo necesita un tratamiento
     especial, debido a lo siguiente:

            (a * 65536) * (b * 65536) = (a * b) * 65536 * 65536

     Como podemos ver, el resultado deseado queda multiplicado por 65536,
     por lo tanto, debemos hacer una divisin extra (entre 65536) para
     obtener la respuesta correcta:

             var a, b, c : Fixed;
             begin
                  a := Int2Fixed(10);
                  b := Int2Fixed(20);
                  c := a * b / 65536;  { c = (10 * 20) * 65536 }
             end;

     El cdigo anterior no se puede usar en la realidad, debido a que
     la multiplicacin de dos nmeros de 32 bits produce un resultado
     de hasta 64 bits. Por lo tanto debemos usar ensamblador, el cual
     nos permite hacer multiplicaciones de 32 bits y almacenar el
     resultado en EDX:EAX (64 bits).

          function FMul(n1, n2 : Fixed) : Fixed; assembler;
          asm
             mov eax, n1
             mov edx, n2
             imul edx            { eax := eax * edx }
             add eax, 8000h      { sumar 0.5 para redondeo }
             adc edx, 0          { compensar si hay acarreo }
             shr eax, 16         { dividir entre 65536 }
          end;


     La divisin entre dos nmeros de punto fijo tambin es un poco
     compleja:

            (a * 65536) / (b * 65536)  =  a / b


     Ahora debemos multiplicar por 65536 para obtener el resultado
     correcto, pero debemos hacer la multiplicacin ANTES de la divisin
     para obtener la mayor precisin posible:

          function FDiv(n1, n2 : Fixed) : Fixed; assembler;
          asm
             mov edx, n1
             mov ebx, n2
             xor eax, eax       { EAX := 0 }
             shrd eax, edx, 16  { Se mueve el valor de EDX a EDX:EAX  }
             sar edx, 16        { y se desplaza 16 bits a la izquierda }
             idiv ebx           { se hace la divisin }
             shld edx, eax, 16  { Se pasa EAX a DX:AX }
          end;

     Bueno, la funcin anterior est un poco extraa, pero funciona. No
     se preocupen por el ensamblador y solamente usen la funcin.


     Todas las funciones anteriores estn en la unidad FMATH.PAS includa
     en el tutorial. Pero debido a que el Turbo Pascal no reconoce las
     instrucciones del 386, fu necesario usar un truco e insertar algunas
     instrucciones manualmente.

     El programa TESTFIJO.PAS sirve para comparar la velocidad  y la
     precisin de los nmeros reales con los nmeros de punto fijo.

     La diferencia es an ms pronunciada si no se cuenta con un
     coprocesador matemtico. Adems, otra ventaja de los nmeros de
     punto fijo, es que son muy fciles de utilizar en ensamblador, cosa
     que no se puede decir de los nmeros reales.


Tablas trigonomtricas con nmeros de punto fijo:
-------------------------------------------------

     Tambin podemos generar nuestras tablas trigonomtricas usando
     nmeros de punto fijo, por ejemplo:

             var Seno : array[0..359] of Fixed;
                 i : word;

             begin
                  for i := 0 to 359 do
                      Seno[i] := Real2Fixed(sin(i * Pi / 180));
                  ...
                  ...
             end.


     Esto ser muy til a la hora de hacer rotaciones de vrtices.

     Bueno, despus de que hayan masticado y digerido todo esto sobre
     desplazamientos binarios y nmeros de punto fijo, pueden pasar
     a la siguiente seccin:


LA TERCERA DIMENSION:
---------------------

     Lo primero que debemos establecer es un sistema de coordenadas
     tridimensionales. La forma ms comn (y la que vamos a utilizar
     en el tutorial) es la siguiente:

                                   Y
                                   |
                                   |   /
                                   |  /
                                   | /
                                   |/
                    ---------------|----------------- X
                                  /|
                                 / |
                                /  |
                               /   |
                              Z    |


     El origen coincide con el centro de la pantalla
     El eje X va de izquierda a derecha, siendo positivo hacia la derecha
     El eje Y va de abajo hacia arriba, siendo positivo la parte de arriba
     El eje Z va desde adentro de la pantalla hacia afuera, siendo positivo
     hacia afuera.


     En algunos otros sistemas se intercambian los ejes Y y Z o se cambia
     el sentido del eje Z, pero ste sistema es el ms comn.


Cmo definir un objeto tridimensional:
--------------------------------------

     Todos nuestros objetos estarn formados por polgonos. Por ejemplo,
     un cubo estara formado por 4 cuadrados, una pirmide est formada
     por 4 o ms tringulos, etc. Incluso podemos obtener objetos como
     esferas, toroides (donas) y otras figuras complejas, todas ellas
     formadas por polgonos.

     Por simplicidad, estos polgonos DEBEN de ser CONVEXOS. Si no
     recuerdan la diferencia entre cncavo y convexo, busquen en
     cualquier libro de geometra. SI su objeto tridimensional tiene
     alguna cara cncava, subdivdan esa cara en varios polgonos
     convexos.


     Para poder trabajar con un objeto tridimensional, hay 4 cosas
     importantes que debemos conocer de l:

                 - El nmero de vrtices del objeto
                 - El nmero de polgonos que forman el objeto
                 - Las coordenadas de cada vrtice
                 - Los vrtices que forman cada polgono


     Vamos a ver todo esto con un ejemplo simple: un cubo

                                C        D
                                 o-------o
                                /|      /|
                               / |     / |
                           A  /  |  B /  |
                             o---|---o   |
                             |   o---|---o
                             |  / E  |  / F
                             | /     | /
                             |/      |/
                             o-------o
                             G       H


     Hay ocho vrtices en nuestro cubo: A, B, C, D, E, F, G, H

     Las coordenadas de los vrtices (suponiendo que cada lado mide 20
     y que el cubo est centrado) son:

           A = (-10, 10, 10)                    E = (-10, -10, -10)
           B = (10, 10, 10)                     F = (10, -10, -10)
           C = (-10, 10, -10)                   G = (-10, -10, 10)
           D = (10, 10, -10)                    H = (10, -10, 10)


     Bien, ahora solo nos falta la informacin de los polgonos. Nuestro
     cubo (y todos los cubos) est formado por 6 cuadrados. Cada cara
     del cubo est determinada por 4 de sus vrtices:

         Cara 1: B - A - G - H
         Cara 2: B - H - F - D
         Cara 3: D - F - E - C
         Cara 4: C - E - A - G
         Cara 5: A - B - D - C
         Cara 6: H - G - E - F

     Es MUY importante tomar los vrtices en el orden correcto. Obviamente,
     los vrtices deben de ser adyacentes. Por ejemplo, en la cara 1:

                  Cara 1: B - A - G - H

     hubiera sido incorrecto si la definiramos como:

                  Cara 1: B - G - A - H

     ya que B y G NO son adyacentes, lo mismo que A y H.

     Tambin es importante que los vrtices se especifiquen en SENTIDO
     INVERSO a las manecillas del reloj cuando el polgono es visible
     para el observador. Esto parece un poco extrao pero es necesario.

     Por ejemplo, la cara 1 la pudimos haber definido tambin como:

                  Cara 1: A - G - H - B
                  Cara 1: G - H - B - A
                  Cara 1: H - B - A - G

     puesto que en todos esos casos el sentido de los vrtices es inverso
     al de las manecillas del reloj. En el caso de la cara 3 (la cual
     no debe de ser visible), el sentido es inverso:

                  Cara 3: D - F - E - C

     Noten que si rotramos el cubo sobre el eje Y de forma que pudiramos
     ver la cara 3, el orden de los vrtices quedara contrario al de las
     manecillas del reloj. De esta forma podemos saber si una cara es
     visible o no, pero eso corresponde al siguiente tutorial.


COMO PROYECTAR EL OBJETO A DOS DIMENSIONES:
-------------------------------------------

     Desgraciadamente, los monitores convencionales no fueron diseados
     para trabajar en tres dimensiones, lo que significa que debemos
     encontrar la manera de convertir nuestras coordenadas 3D a un
     espacio bidimensional.

     Esto es relativamente simple. Solamente hay que recordar un poco
     de geometra de tringulos. Supongamos que queremos calcular la
     proyeccin en dos dimensiones del punto (X, Y, Z). Las coordenadas
     del punto en el plano de la pantalla sern (X', Y'). Por simplicidad
     solamente calcularemos Y', ya que X' se puede obtener de la misma
     manera:


                                        (X, Y, Z)
                                            * <---- Punto en 3D
                                    |     . |
                      Pantalla ---> |   .   |
                                    | .     |
                                    .
                                  . |       Y
                                .   |Y'
                              .     |       |
                            .       |       |
                       ojo ------------------
                           |- Zoff -|
                                    |-- Z --|


          - (X, Y, Z) son las coordenadas del punto en 3D
          - (X', Y') son las coordenadas del punto proyectado a 2D
          - Zoff es la distancia del ojo a la pantalla (constante)


                                                Y'         Y
          En el dibujo se puede apreciar que: ------ = ----------
                                               Zoff     Z + Zoff

                                                 Y * Zoff
          y despejando Y', obtenemos que:  Y' = ----------
                                                 Z + Zoff

     Como les dije, es muy simple, pero an no terminamos. El valor de
     Zoff es constante y puede ser lo que nosotros queramos. Un buen
     valor para Zoff es 1024. Cambiando este valor se puede modificar
     la "perspectiva" con que vemos el objeto, pero conviene que sea
     una potencia de 2, ya que as podemos cambiar la multiplicacin
     por un desplazamiento binario a la izquierda.

     El valor de Z no siempre es la coordenada Z del punto, ya que si
     el objeto es trasladado a un nuevo origen, obtenemos lo siguiente:

       Valor de Z = Coord. Z del punto + Coord. Z del nuevo origen

     Adems, para asegurar que las coordenadas X' y Y' queden dentro
     de la zona de la pantalla (0, 0, 320, 200), puede ser necesario
     tener que multiplicar o dividir por un factor constante. En el
     caso de nuestro ejemplo, el dividir entre dos produce buenos
     resultados. Una vez ms, una divisin entre 2 puede ser cambiada
     por un desplazamiento binario a la derecha.

     As que, hasta ahora, las frmulas para la proyeccin a 2D quedan as:

         X' = ((X * 1024) / (1024 + Z + oZ)) / 2
         Y' = ((Y * 1024) / (1024 + Z + oZ)) / 2

     donde oZ es la coordenada Z del origen del objeto respecto a (0, 0, 0).

     Las ecuaciones anteriores an no funcionan como quisiramos, debido
     a que en nuestro sistema de coordenadas, entre mayor es el valor de Z,
     ms cerca est el punto del observador, pero aqu sucede lo contrario,
     entre ms grande es Z, los valores de X' y Y' son ms pequeos,
     sugiriendo que el punto se aleja (en lugar de acercarse).

     La solucin es simplemente cambiar el signo de Z (y de oZ):

        X' = ((X * 1024) / (1024 - Z - oZ)) / 2
        Y' = ((Y * 1024) / (1024 - Z - oZ)) / 2

     Adems, como podrn darse cuenta, lo anterior representa calcular
     dos veces la siguiente cantidad (una para X' y otra para Y'):

                           (1024 - Z - oZ)

     Podemos cambiar lo anterior y ahorrar algunos clculos:

             P := 1024 - Z - oZ
             X':= (X / P) * 512
             Y':= (Y / P) * 512

     An faltan algunos ajustes. Para empezar, el centro de la pantalla
     NO est en (0, 0), sino en (160, 100), por lo tanto, debemos hacer
     una traslacin para obtener las coordenadas correctas:

                    X' := (X / P) * 512 + 160
                    Y' := (Y / P) * 512 + 100

     Y por ltimo, en nuestro sistema de coordenadas, la coordenada Y
     "aumenta" hacia arriba, pero en la pantalla, la coordenada Y
     aumenta hacia abajo, por lo tanto, debemos realizar un cambio de signo:

                    Y' := -(Y / P) * 512 + 100

     Ms adelante veremos cmo llevar esto a la prctica.


COMO ROTAR UN VERTICE:
----------------------

     Esto es tambin muy fcil. Para hacerlo ms simple, vamos a ver
     primero cmo hacerlo en dos dimensiones. Supongamos que queremos
     rotar un vector en el plano XY:

                       (X', Y')  Y     (X, Y)
                          *      |      *
                            \    |    /
                              \  A  /
                                \|/ B
                     ------------|-------------X

     Bueno, ahora voy a descifrar el "dibujo" anterior. Tenemos el
     punto (X, Y) y queremos rotarlo un ngulo A hacia la izquierda,
     con lo que quedar en las coordenadas (X', Y'). El punto (X, Y)
     se encuentra originalmente a un ngulo B con respecto al eje X.

     Obviamente, el punto (X', Y') se encuentra a un ngulo A+B con
     respecto al eje X, y utilizando coordenadas polares tenemos que:

              X = R * cos(B)
              Y = R * sen(B)

              X' = R * cos(A+B) = R * cos(A) * cos(B) - R * sen(A) * sen(B)
              Y' = R * sen(A+B) = R * sen(A) * cos(B) + R * cos(A) * sen(B)

     donde R es la magnitud del vector (X, Y) (y (X', Y')).

     Ahora simplemente sustitumos las dos primeras ecuaciones en las
     dos ltimas y obtenemos que:

              X' := X * cos(A) - Y * sen(A)
              Y' := X * sen(A) + Y * cos(A)

     Y de esa forma hemos obtenido la rotacin de un vector sobre
     el plano XY, o lo que es lo mismo, la rotacin de un vector
     alrededor del EJE Z. Lo cual significa que si rotamos un vector
     en 3 dimensiones alrededor del eje Z, entonces su coordenada Z
     NO se modifica, ya que la rotacin se hace sobre un plano paralelo
     al plano XY.

     De esta misma forma podemos calcular las rotaciones alrededor de
     los ejes X y Y, obteniendo las siguientes ecuaciones:


         Rotacin alrededor del eje X:

                  X' := X
                  Y' := Y * cos(Ax) - Z * sen(Ax)
                  Z' := Y * sen(Ax) + Z * cos(Ax)


         Rotacin alrededor del eje Y:

                  X' := X * cos(Ay) - Z * sen(Ay)
                  Y' := Y
                  Z' := X * sen(Ay) + Z * cos(Ay)

         Rotacin alrededor del eje Z:

                  X' := X * cos(Az) - Y * sen(Az)
                  Y' := X * sen(Az) + Y * cos(Az)
                  Z' := Z


         Bueno, esto es lo bsico sobre rotaciones. Tambin se pueden
         hacer rotaciones con respecto a algn punto fijo y otras
         transformaciones como traslaciones y escalado, pero stas
         son muy sencillas y estoy seguro de que ustedes sabrn cmo
         implementarlas.



PROGRAMANDO UNA LIBRERIA DE 3D:
-------------------------------

     Cuando programamos plasmas, fuego, y otros efectos, debamos estar
     buscando y cambiando funciones y valores constantes hasta que el
     efecto resultara agradable. Pero ese conjunto de funciones y valores
     utilizados solamente funcionaba para ESE efecto, por lo que si
     queramos realizar OTRO plasma, tenamos que volver a deducir nuevos
     valores y funciones, aunque la estructura del programa fuera la misma.

     Con las rutinas de 3D no podemos hacer eso. En parte, porque es fcil
     generalizarlas y en parte porque cuesta tanto trabajo hacerlas que
     no podemos permitirnos el tener que modificar un programa entero
     solamente para desplegar un objeto diferente.

     Debemos construr nuestras rutinas de forma que podamos generar
     tanto cubos como esferas, toros, naves espaciales, o cualquier otro
     objeto imaginable. Adems, las rutinas deben ser fcilmente "expandibles"
     para aadir sombreado o mapeado de texturas.

     Una buena forma de lograr esto es utilizar programacin orientada
     a objetos. Desgraciadamente, no todos la conocen, y est muy fuera
     del objetivo de este tutorial el ensear este tipo de programacin.

     En cambio, vamos a definir varios tipos de datos y procedimientos
     para trabajar con ellos. Estos procedimientos estarn generalizados
     y no dependern en gran parte del objeto con el que se trabaje.

     Los tipos de datos principales son TVertice, TCara y TObjeto3D.


El tipo TVertice:
-----------------

     El tipo TVertice es el ms sencillo de todos. Simplemente representa
     un punto en el espacio tridimensional y su proyeccin a 2D:

              type TVertice = record
                              x, y, z : Fixed;
                              x2d, y2d : integer;
                              end;


     (x, y, z) son las coordenadas del punto (o vrtice) y estn en
     formato de punto fijo para lograr una mayor rapidez en los clculos.

     (x2d, y2d) son las coordenadas de la proyeccin en la pantalla. Estas
     coordenadas se calculan de la forma en que vimos, pero utilizando
     aritmtica de punto fijo.


Procedimientos para manejo de vrtices:

     procedure VerticeCalcula2D(var v : TVertice; Zdist : integer);

               Este procedimiento calcula x2d y y2d para un vrtice
               especfico. El parmetro Zdist es la coordenada Z
               del origen del objeto 3D, anteriormente oZ.

               La implementacin est de esta forma:

                  p := Int2Fixed(1024 - Zdist) - v.z;
                  v.x2d := Fixed2Int(FDiv(v.x, p) shl 9) + 160;
                  v.y2d := Fixed2Int(FDiv(v.y, -p) shl 9) + 100;

               Esto corresponde a las frmulas obtenidas anteriormente,
               ya que SHL 9 equivale a DIV 512, y la divisin se hace en
               punto fijo.


     procedure VerticeRota(var v : TVertice; ax, ay, az : integer);

               Este procedimiento rota un vrtice con respecto a cada
               uno de los ejes. AX, AY y AZ son los ngulos de rotacin
               y deben valer entre -360 y 359. El procedimiento se basa
               en lo que vimos anteriormente sobre cmo rotar un vrtice.


El tipo TCara:
--------------

     Este tipo contiene la informacin sobre una cara del objeto 3D.
     Una cara es simplemente un polgono convexo, el cual est determinado
     por tres o ms vrtices, por lo tanto, el tipo TCara se define as:

              type TCara = record
                           NVertices : byte; { Nmero de vrtices }
                           Vertice : array[1..MaxVerticesCara] of integer;
                           end;

     Bueno, aqu hay que tener algo muy claro. El arreglo Vertice NO
     contiene elementos del tipo TVertice, es decir que una cara, en
     realidad NO contiene la informacin de sus vrtices. Lo que contiene
     es un ARREGLO DE INDICES, en el que cada ndice se utiliza para
     referenciar a un vrtice.

     Esto se hace porque en general, varias caras pueden contener al mismo
     vrtice, as que en vez de guardar la informacin de ese vrtice
     en cada una de esas caras, simplemente guardamos un ndice que
     usaremos despus en un arreglo de vrtices. (Qu mala explicacin :( ).

     Bueno, debido a que tenemos que especificar un ndice superior para
     el arreglo, he declarado una constante global MaxVerticesCara que
     indica el nmero mximo de vrtices que una cara puede tener.

     En nuestro ejemplo, el objeto es un cubo, por lo tanto, la constante
     MaxVerticesCara vale 4, (las caras tienen 4 vrtices), y difcilmente
     necesitaremos alguna vez algn valor mayor que 8 o 10. De hecho,
     muchos programas de diseo en 3D generan solamente tringulos, con
     lo que este valor es casi siempre 3.


Procedimientos para manejo de caras:

     procedure CaraReinicia(var cara : TCara);

               Este procedimiento simplemente restaura las variables de
               una cara a sus valores iniciales. Por ahora, el nico valor
               que hay que restaurar es NVertices, dndole un valor de cero.


     procedure CaraAgregaVertice(var cara : TCara; vertice : integer);

               Este procedimiento aade un vrtice a una cara. Aunque
               lo que en realidad se aade es EL INDICE de un vrtice.
               Adems hay que RECORDAR que los vrtices DEBEN ser
               aadidos en orden INVERSO al sentido de las manecillas...

               Por ejemplo, recuerdan el cubo que definimos en una de
               las secciones anteriores? Bueno, ese cubo tena ocho
               vrtices, llamados: A, B, C, D, E, F, G, H.

               Pues ahora en lugar de letras, vamos a referenciar a
               los vrtices con un NUMERO: 0, 1, 2, 3, 4, 5, 6 y 7.

               Una de las caras estaba definida como: B - A - G - H.

               Pues ahora, la cara est definida como: 1, 0, 6, 7.

               Por lo tanto, para generar esa cara, hacemos lo siguiente:

                   CaraAgregaVertice(cara, 1);
                   CaraAgregaVertice(cara, 0);
                   CaraAgregaVertice(cara, 6);
                   CaraAgregaVertice(cara, 7);

               Entienden cmo funciona? OK, ya pueden continuar...


El tipo TObjeto3D:
------------------

     Este tipo contiene toda la informacin referente a un objeto
     tridimensional, incluyendo sus vrtices y polgonos.

     De hecho, rara vez utilizaremos alguno de los otros tipos,
     excepto tal vez, el tipo TCara a la hora de generar los
     objetos, pero una vez que tenemos un objeto definido,
     no deber haber necesidad de modificar sus vrtices o caras
     por separado.

     El tipo TObjeto3D est definido de esta forma:

             type TObjeto3D = record
                              NVertices : integer;
                              NCaras : integer;
                              ox, oy, oz : integer;
                              Vertice : array[1..MaxVertices] of TVertice;
                              Cara : array[1..MaxCaras] of TCara;
                              end;


     NVertices y NCaras son respectivamente el nmero de vrtices y caras
     que componen al objeto.

     (ox, oy, oz) son las coordenadas del origen del objeto con respecto
     a la posicin (0, 0, 0), que es el centro de la pantalla. Esta
     informacin no es realmente necesaria pero simplifica mucho las
     traslaciones, puesto que en vez de modificar todos los vrtices
     para hacer la translacin, solamente modificamos el origen del objeto.

     Vertice[] es el arreglo que contiene a los vrtices. En esta
     ocasin, los elementos de este arreglo SI son del tipo TVertice,
     y los "ndices" que mencionaba antes, (a la hora de definir las
     caras), se usan como ndices dentro de este arreglo.

     Cara[] es un arreglo que contiene la informacin de todas las caras
     del objeto.

     MaxVertices es el nmero mximo de vrtices que un objeto puede
     tener. En nuestro ejemplo, este valor es 8, ya que un cubo tiene
     ocho vrtices, pero se pueden generar figuras con cientos o
     miles de vrtices.

     MaxCaras es el nmero mximo de caras que puede tener un objeto.
     En el ejemplo, este valor es 6.


Procedimientos para el manejo de objetos tridimensionales:

     procedure Objeto3DReinicia(var obj : TObjeto3D);

               Fija los valores iniciales de un objeto tridimensional.
               Ms especficamente, hace el nmero de vrtices y caras
               igual a cero y fija el origen en (0, 0, 0).


     procedure Objeto3DAgregaVertice(var obj : TObjeto3D; nx, ny, nz : integer);

               Aade un vrtice al objeto. El nuevo vrtice es (nx, ny, nz).
               Hay que recordar el orden en que se aaden los vrtices,
               ya que necesitamos sus ndices para generar las caras.


     procedure Objeto3DAgregaCara(var obj : TObjeto3D; cara : TCara);

               Aade una cara al objeto. No importa el orden en que se
               aaden las caras.

               Se necesita una variable temporal de tipo TCara para
               agregar una cara al objeto. Primero hay que definir
               la cara y luego agregarla al objeto 3D. Por ejemplo:

                       var MiObjeto : TObjeto3D;
                           MiCara : TCara;

                       begin
                            { Aqu agregamos los vrtices del objeto }

                            CaraReinicia(MiCara);
                            CaraAgregaVertice(MiCara, 1);
                            CaraAgregaVertice(MiCara, 2);
                            CaraAgregaVertice(MiCara, 3);
                            Objeto3DAgregaCara(MiObjeto, MiCara);

                            CaraReinicia(MiCara);
                            CaraAgregaVertice(MiCara, 4);
                            ... { etc }
                       end;


     procedure Objeto3DRota(var obj : TObjeto3D; ax, ay, az : integer);

               Este procedimiento rota un objeto alrededor de cada eje
               segn los ngulos AX, AY y AZ.

               La manera de rotar un objeto es rotando todos sus vrtices:

                         for i := 1 to obj.NVertices do
                             VerticeRota(obj.Vertice[i], ax, ay, az);


     procedure Objeto3DMueve(var obj : TObjeto3D; nx, ny, nz : integer);

               Este procedimiento simplemente fija las nuevas coordenadas
               del origen de un objeto. La implementacin es muy sencilla:

                          begin
                               obj.ox := nx;
                               obj.oy := ny;
                               obj.oz := nz;
                          end;


     procedure Objeto3DCalcula2D(var obj : TObjeto3D);

               Calcula las proyecciones en dos dimensiones de todos los
               vrtices de un objeto, trasladndolos tambin con respecto
               al origen del objeto. HAY QUE LLAMAR a este procedimiento
               ANTES de dibujar el objeto.


Cmo DIBUJAR un objeto tridimensional:
--------------------------------------


     Hay muchas formas de dibujar un objeto y algunas de ellas son muy
     complicadas, como por ejemplo, un objeto con mapeo de textura,
     mapeo de entorno, sombreado Phong y mapeo de protuberancia, todo
     al mismo tiempo. Bueno, probablemente no lleguemos a hacer algo
     como eso aqu, puesto que es muy difcil hacerlo en tiempo real
     (excepto con un Pentium II a 266 Mhz), pero poco a poco iremos
     viendo cmo aadir sombreado a nuestros objetos (sombreado plano,
     Gouraud y Phong) y un poco de mapeo de texturas y entorno.

     Pero eso ser para los siguientes tutoriales. En este ejemplar,
     tendremos que conformarnos con los modelos de alambre sin
     supresin de caras ocultas.

     Un "modelo de alambre" es simplemente la representacin de un
     objeto tridimensional que se obtiene al dibujar nicamente sus
     aristas, lo cual produce una ligera idea de la "forma" que tiene
     nuestro objeto. Adems, las caras que no deberan ser visibles
     tambin son dibujadas, lo cual produce extraas distorsiones
     segn el ojo humano. Si en el programa de ejemplo ustedes empiezan
     a ver (o mejor dicho: a creer) que el objeto se deforma, simplemente
     parpadeen una o dos veces. Como deca Hunter: "Funciona para m".

     La forma de dibujar un modelo de alambre es muy sencilla:

              1 .- Se obtiene la proyeccin a 2D de todos los vrtices
                   del objeto. (Procedimiento Objeto3DCalcula2D).

              2 .- Se dibuja cada cara del objeto de la siguiente forma:

                   a) Se traza una lnea que va del primer vrtice de
                      la cara al segundo, luego del segundo al tercero,
                      y as hasta llegar al ltimo vrtice.

                   b) Se traza una lnea del ltimo vrtice al primero.


     Y eso es todo. En el programa de ejemplo se comprueba que las caras
     tengan al menos tres vrtices antes de dibujarlas, ya que no existen
     polgonos con menos de tres vrtices :)

     El ejemplo no es muy complicado, pero pueden encontrar algunas
     construcciones extraas como:

               x2 := obj.Vertice[obj.Cara[j].Vertice[i]].x2d

     Si lo anterior les parece un poco confuso, les sugiero que repasen
     la forma en que estn declarados los tipos TVertice, TCara y TObjeto3D
     y sus elementos. Poco a poco iremos aadiendo ms datos a cada uno
     de esos tipos, y no ser difcil confundirse de vez en cuando.



CONCLUSIONES:
-------------

     Bueno, este tutorial (tambin) estuvo un poco largo, pero fu debido
     a la explicacin de desplazamientos y punto fijo. El siguiente ser
     mucho ms corto, pues solamente veremos la supresin de caras ocultas,
     el relleno de polgonos y el sombreado plano.

     Hay algunas cosas que hay que tener en cuenta con respecto a los
     ejemplos:

          1) No se usaron apuntadores debido a que la informacin para
             un cubo no es considerable, pero si quieren disear objetos
             ms interesantes, como un carro o un avin, probablemente
             tendrn que usar la memoria del "heap", es decir, tendrn
             que usar apuntadores.

          2) El programa solamente dibuja 24 lneas por cuadro (el cubo
             tiene 6 caras de 4 lados cada una) y 192 lneas cuando
             aparece la estela de movimiento (formada por 8 cubos),
             pero cuando hagamos sombreado, lo que dibujaremos sern
             polgonos rellenos, no lneas, y tendremos que dibujar
             MUCHOS pxels por cuadro. Y despus tendremos que calcular
             el color DE CADA PIXEL. Eso significa que necesitaremos
             la ayuda del Oh Grandsimo Master de Todos Los Tiempos,
             el seor Ensamblador. Ni modo, chavos, es la UNICA forma
             de obtener figuras tridimensionales decentes.


EJERCICIOS:
-----------

     El nico ejercicio es tratar de entender lo que se dijo en las
     anteriores 1100 lneas de texto. Adems de eso, no hagan nada.

     Lo que pasa es que les conviene esperarse al siguiente tutorial,
     en donde las figuras tridimensionales empiezan a parecer REALMENTE
     tridimensionales.



FINAL:
------

     "With no dinero
      you're zero"

             - KRS-One

<----------------------------- cortar aqu --------------------------------->
