Está en la página 1de 116

3

Volumen

50% Teoría
50% Práctica

Nivel
Avanzado

Manual de
Entornos 3D
INTRODUCCIÓN

Manual de Entornos 3D

 InteractiveStation
Panamá

Escrito por Ricardo Santamaría


Teléfono 6718-8721

i
Tabla de contenido
PARTE 1 –
CREACIÓN DEL VÉRTICE 3D
FUNDAMENTOS DE
POLÍGONOS 3D CREACIÓN DE LA ARISTA

CREACIÓN DE LA CARA
I N T R O D U C C I Ó N A L M U N D O
3 D
CÁLCULO DEL PUNTO MEDIO DE LA CARA

INTRODUCCIÓN CÁLCULO DEL PUNTO NORMAL DE LA CARA

EL MOTOR 3D ROTANDO NUESTRO OBJETO 3D

LOS VERTICES ELIMINACIÓN DE LA CARA POSTERIOR

VÉRTICE 2D ORDENAR LAS CARAS

VÉRTICE 3D R E N D E R I Z A N D O O B J E T O S

COLOR DEL VÉRTICE INTRODUCCIÓN

ÁNGULOS DEL VÉRTICE DIBUJANDO LA CARA

ARISTAS DIBUJANDO NUESTRO OBJETO 3D

CARAS RENDERIZANDO LOS VÉRTICES

OBJETO 3D RENDERIZANDO LAS ARISTAS

CÁMARA 3D RENDERIZANDO LAS CARAS

D E S A R R O L L O D EL M O T O R
RENDERIZANDO LA ILUMINACIÓN
3 D
MODO DE INTENSIDAD ADITIVO
INTRODUCCIÓN MODO DE ATENUACIÓN PARA EL MODO DE
ILUMINACIÓN ADITIVO
REDEFINIENDO LAS ESTRUCTURAS
MODO DE INTENSIDAD COMPARATIVO
BÚFER Z
MODO DE ATENUACIÓN PARA EL MODO DE
ILUMINACIÓN INTENSIDAD COMPARATIVO
VARIABLES AUXILIARES EXPORTANDO LAS FUNCIONES
INICIO DEL MOTOR PROBANDO NUESTRO MOTOR 3D
CREACIÓN Y ELIMINACIÓN DE LA CÁMARA 3D

FILMANDO A NUESTRO OBJETO 3D PARTE 2 –


FUNDAMENTOS DE
AVANZANDO LA CÁMARA 3D VOXELS 3D
CREACIÓN Y ELIMINACIÓN DEL OBJETO 3D
D E S A R R O L L O D E V I D E O-
V O X E L J U E G O S D E T I P O M O D O 8
E X T E N D I D O
INTRODUCCIÓN
INTRODUCCIÓN
PAISAJES DE VOXELS

MODELOS DE VOXELS PARTE 4 – APÉNDICE


COMPARANDO LOS VOXELS CON LOS
POLÍGONOS
R E D E S E N F E N I X
INICIALIZANDO EL MOTOR DE VOXEL
INTRODUCCIÓN
CREANDO UN NUEVO VOXELSPACE
LA LIBRERÍA NET.DLL
ELIMINANDO UN VOXELSPACE EXISTENTE
LA LIBRERÍA TCPSOCK.DLL
GENERANDO TABLAS PRECALCULADAS
CONSIDERACIONES FINALES
CONFIGURANDO EL VOXELSPACE

CONFIGURANDO LOS MAPAS PARA EL R E F E R E N C I A S


VOXELSPACE
ENLACES FENIX
POSICIONANDO NUESTRO VOXELSPACE
DESARROLLO DE MOTORES 3D
GIRANDO NUESTRO VOXELSPACE
PROGRAMAS GRATUITOS
MANEJANDO LA CÁMARA DEL VOXELSPACE
EPÍLOGO
OBTENIENDO LA ALTURA DEL VOXELSPACE

LIBERANDO EL VOXELSPACE

R E N D E R I Z A N D O V O X E L S

INTRODUCCIÓN

EL PROCESO DE RENDERIZACIÓN

EXPORTANDO LA LIBRERÍA A FENIX

U S O D E L V S E

INTRODUCCIÓN

PARTE 3 – SOBRE EL
MODO 8 EXTENDIDO

L A L I B R E R Í A M 8 E E

INTRODUCCIÓN

H E R R A M I E N T A S P A R A E L
M O D O 8 E X T E N D I D O

INTRODUCCIÓN
1
Parte

Fundamentos de
Polígonos 3D

1
1
Capítulo

Introducción al Mundo 3D
I N T R O D U C C I Ó N

Bienvenidos al Manual de Entornos 3D para desarrollar cualquier tipo de juego que


requiera de entornos 3D en DIV Games Studio, Fénix y cDiv. En este capítulo
hablaremos acerca de los conceptos básicos que debe tener todo un entorno en tercera
dimensión. Se asume que usted debe haber dominado por completo los conceptos
básicos de cómo funciona el entorno de programación en Div Games Studio o en Fénix
a través de los dos volúmenes del “Manual de Video Juegos”. Ahora vamos a hacer algo
más que eso. Crear nuestras propias librerías de gráficos 3D para nuestro entorno Div
Games Studio o Fénix.

Para preparar nuestro motor 3D debemos tener dos archivos fundamentales:

ETD.C – En donde estará nuestra librería del motor 3D.


ETD.INC – En donde se definirán las variables del motor 3D en Fénix.

E L M O T O R 3 D

En primer lugar, es necesario explicar el concepto básico... ¿qué es un motor 3d?

La respuesta es relativamente simple:

Un motor 3D es un colección de estructuras, funciones y algoritmos utilizados


para visualizar, después de muchos cálculos y transformaciones, objetos
tridimensionales en una pantalla bidimensional.

El motor 3D es en realidad el lenguaje de las matemáticas. La geometría 3D se compone


de formas fundamentales que constituyen bloques de construcción.

Las secciones principales de un motor 3d (ya sea para desarrollar objetos 3D, como
Voxels, como Entornos 3D) son:

1.- La ubicación de los datos de los objetos en estructuras.

2
2.- Las transformaciones para posicionar los objetos en el mundo.
3.- Renderizar la escena en la pantalla bidimensional.
4.- Exportar nuestro motor a una librería para su posterior utilización (En nuestro caso
sería en el lenguaje Fenix).

Esto es lo que vamos a desarrollar a partir de este capítulo en adelante… así que manos a
la obra.

L O S V É R T I C E S

El átomo del mundo 3D es el vértice, que es la primera dimensión, sin anchura ni


longitud, un punto del espacio.

V É R T I C E 2 D

Los vértices 2D se utilizan para crear líneas y formas. Los vértices de textura se utilizan
en la creación de coordenadas de mapeado.

Como las coordenadas se basan en un producto catesiano, los tipos de datos son de tipo
enteros, y lo definiremos en la siguiente estructura:

type _ETD_2DPOINT
int x,y;
end

V É R T I C E 3 D

Supón que tienes un objeto y quieres mostrarlo en una pantalla 2d. Para poder hacer
esto, es necesario obtener información acerca de su estructura. ¿Cómo podemos hacer
esto?. Primero, debemos definir algunos puntos clave: los vértices del objeto. Cada
vértice se compone de tres coordenadas las cuales llamaremos x, y, z. Cada coordenada
debe ser expresada con una variable de tipo FLOAT o DOUBLE , necesario pues
siempre necesitamos tener la mejor resolución para renderizar la escena. Esta solución es
utilizada en casi todos los motores 3d pero no está libre de errores y, de hecho, pierde
precisión cuando el valor numérico es muy alto. Esta particularidad limita el espacio en
el cual la simulación puede trabajar. En un simulador espacial, deberíamos ser capaces de
movernos a través de un espacio infinito así que nos tendremos que enfrentar con este
problema eventualmente.

3
Para definir un Vértice 3D, utilizaremos una estructura compuesta de tres variables x, y,
z.

type _ETD_3DPOINT
float x,y,z;
end

C O L O R D E L V É R T I C E

Felicidades, hemos creado satisfactoriamente el núcleo de nuestro motor 3D, pero


nuestro punto en el espacio sería vacío o bien dicho invisible. Una vez que hayamos
definidos los dos tipos de vértices, ahora es necesario darle color a nuestro vértice en
base a la estructura de color RGB, que consta de un conjunto de 255 combinaciones
tanto de Rojo, como de Verde y de Azul dando un total de 16777216 colores posibles.

type _ETD_COLOR
int r,g,b;
end

Es muy importante tener en cuenta que todos los cálculos relacionados con el
posicionamiento y rotación del objeto son aplicados a los vértices, ya que ellos son las
unidades que hacen la estructura básica.

Á N G U L O S D E L V É R T I C E

Una vez que tenemos nuestro vértice definido, es necesario crearle una estructura para que la
misma pueda rotarse. Las rotaciones se basan a través de los ángulos:

• alfa: Rotación en torno al eje x


• beta: Rotación en torno al eje y
• gamma: Rotación en torno al eje z

4
type _ETD_3DANGLE
int alfa,beta,gamma;
end

A R I S T A S

Consiste en dos vértices v1 y v2 que al unirse forman una línea de un color especificado

type _ETD_EDGE
int v1,v2,color;
end

C A R A S

Conjunto de tres aristas que al unir los vértices forman un triángulo. Comúnmente está
compuesto por 3 vértices y un color especificado. También debe poseer un color para el
relleno y dos tipos de vértices:

• Normal: Para los procesos de dibujo de la cara posterior.


• Punto Medio: Para ordena r las caras.

type _ETD_FACE

5
int v1,v2,v3;
int color;
_ETD_COLOR color2;
_ETD_3DPOINT normal;
_ETD_3DPOINT pmed;
end

O B J E T O 3 D

Es nuestro objeto en el espacio que va a ser graficado en 3D. Debe contener como
subestructuras:

• Un puntero representando el conjunto de puntos en 3D.


• Un puntero representando el conjunto de aristas.
• Un puntero representando el conjunto de caras.

También debemos conocer cuántos:

• Número de vértices tiene el objeto


• Número de aristas tiene el objeto
• Número de caras tiene el objeto

Nuestro objeto 3D debe tener como propiedades:

• Tamaño del objeto, que su valor predeterminado es 1.0.


• Un Punto 3D indicando la posición del objeto en el espacio 3D.
• El ángulo inicial de rotación del objeto respecto a sus ejes.
• Los modos de renderizado. Se puede tomar uno de los siguientes valores o la suma
de varios:
o 1: Dibuja los vértices
o 2: Dibuja las aristas independientes del objeto (las introducidas con
is_new_edge)
o 4: Dibuja las aristas de las caras
o 8: Dibuja las caras rellenas
o 16: Hace back face culling
o 32: Ordena las caras antes de dibujaralas
o 64: El objeto es afectado por focos de luz

6
• Foco de luz que afecta al objeto

El valor predeterminado de la variable es 56

Un Objeto 3D puede estar constituído por una o múltiples caras. Por ejemplo el objeto 3D
cubo está constituído de 8 vértices, 18 aristas y 12 caras.

type _ETD_3DOBJECT
_ETD_3DPOINT pointer vertex;
_ETD_EDGE pointer edge;
_ETD_FACE pointer face;
int NVertex;
int NEdges;
int NFaces;
float scale;
float x,y,z;
int alfa,beta,gamma;
int RenderMode;
int LTable;
end

C Á M A R A

Una cámara nos define desde donde vamos a realizar el renderizado, en que dirección, la
distancia que hay del observador a la cámara (para efectos de tipo ojo de pez), las distancias
máxima y mínima para ignorar un objeto, y en que map vamos a renderizar todo lo que se le
indique.

Las cámaras sirven para poder dibujar una misma escena (o escenas distintas) desde distintos
ángulos. Nuestro motor se ha orientado de manera que es absolutamente necesario el uso de
camaras, en cualquier caso la cámara 0 se crea automáticamente al iniciar el motor, estando
situada en el punto 0,0,0. Una cosa IMPORTANTE a saber es que las cámaras de primeras
APUNTAN EN LA DIRECCIÓN DEL EJE Z. Es decir, un ángulo beta igual a 0 implica
que apuntamos hacia el eje Z y no el ángulo de 90 grados. Esto se hace así porque es más
intuitivo a la hora de programar, ya que los ejes x e y forman el ancho y alto de la pantalla es
lógico que la camara apunte hacia dentro de esta.

7
Los atributos que debe tener una cámara son:

• Un Fichero y gráfico donde renderizará la escena.


• Un Punto 3D indicando la posición de la cámara en el espacio.
• Los ángulos de rotación de la cámara que indican la orientación de esta, es decir,
hacia donde apunta la cámara. Al rotar una cámara se realiza primero la rotación en el
ángulo beta y después alfa, la rotación de gamma se aplica después de calcular la
posición 2D de un punto. Esto se hace así porque es más sencillo a la hora de
utilizarla. Un aspecto importante es que las cámaras con un ángulo beta 0 apuntan
hacia la parte positiva del eje z.
• Una variable de estado que se utiliza para realizar los cálculos internos de nuestro
motor 3D.
• La distancia del observador a la pantalla. Lo normal es que se iguale al ancho de la
pantalla de render. Jugando con esta variable se pueden conseguir efectos del tipo ojo
de pez.
• Si un objeto está a una distancia menor de la cámara de la indicada en esta variable
no será dibujado.
• Si un objeto está a una distancia mayor de la cámara de la indicada en esta variable
no será dibujado.

type _ETD_3DCAMERA
int file,graph;
float x,y,z;
int alfa,beta,gamma;
int state;
int distance;
int min_distance;
int max_distance;
end

8
Teniedo definido estas variables ya podemos crear el código necesario para desarrollar
nuestro entorno 3D.

9
2
Capítulo

Desarrollo del Motor 3D


I N T R O D U C C I Ó N

En este capítulo, una vez que hayamos definido las variables principales de Fenix para el
Motor 3D, ahora es necesario generar nuestro código en el lenguaje C para adaptar
nuestro motor a un conjunto de funciones y estructuras compatibles con Fenix. Todo
código escrito en C tiene que incluir las librerías estándares para los procesos de
memoria y del lenguaje. Además se incluye la librería fxdll.h para efectos de implementar
todas las estructuras y variables de Fénix en nuestro librería 3D.

#include <fxdll.h>
#include <malloc.h>
#include <stdlib.h>

Luego de ello, es necesario crear unas variables globales que se puedan implementar
cuando estemos desarrollando nuestro motor 3d. Estas variables poseen valores
limitados, por motivos de prueba de nuestro motor, pero cuando nuestro proyecto esté
robusto, lo podemos incrementar en valores más altos…

# define _PI 3.14159265358979323846


# define CARAS_MAX 5000
# define VERTEX_MAX 5000

En este caso, nuestro motor 3D está limitado por ahora a crear 5000 caras y 5000
vértices.

R E D E F I N I E N D O L A S E S TR U C T U R A S

En el capítulo anterior creamos las estructuras primitivas en Fenix para nuestro motor
3D, ahora lo que sigue es redefinir estas estructuras en nuestra librería 3D para
implementar la que hicimos en el archivo include en nuestros juegos.

typedef struct etd_p2d{


int x;
int y;
} etd_punto2d;

typedef struct etd_p3d{


float x;

10
float y;
float z;
} etd_punto3d;

typedef struct etd_col{


int r;
int g;
int b;
} etd_color;

typedef struct etd_a3d{


int alfa;
int beta;
int gamma;
} etd_angulo3d;

typedef struct etd_lin{


int v1;
int v2;
int color;
} etd_linea;

typedef struct etd_tri{


int v1;
int v2;
int v3;
int color;
is_color color2;
is_punto3d normal;
is_punto3d pmed;
} etd_cara;

typedef struct etd_obj3d{


//Punteros a los datos de el objeto
etd_punto3d * vertice; //Puntero al array de vertices
etd_linea * arista; //Puntero al array de aristas
etd_cara * cara; //Puntero al array de caras
//Indices
int NVertices; //Numero de vertices
int NAristas; //Numero de aristas
int NCaras; //Numero de caras
//Propiedades
float escalado;
etd_punto3d posicion;
etd_angulo3d giro; //Rotacion respecto a si mismo
int ModoVision; //Modo como se verá el objeto
//valores o la suma de varios:
// 1.- Se muestran los vertices
// 2.- Se muestran las aristas introducidas a mano
// 4.- Se muestran las aristas de las caras
// 8.- Se muestran las caras en filled
// 16.- BFC
// 32.- Ordena las caras
int FocoLuz; //Foco de luz que afecta al objeto
} etd_objeto3d;

typedef struct etd_c{


int file,graph;
etd_punto3d posicion;

11
etd_angulo3d angulo;
int estado;
int distancia;
int distancia_minima;
int distancia_maxima;
} etd_camara;

B Ú F E R Z

Las estructuras que vamos a crear a continuación son para crear una sensación de que
nuestro objeto se está alejando a acercándose a nuestra cámara 3D. Para ello debemos
guardar esas estructuras en un búfer especial llamado Búfer Z. Los objetos a crear para
nuestro búfer Z son:

• La pieza Z que nos indica a qué objeto 3D vamos a implementar, y la


distancia entre la cámara y el objeto.
• La Tabla Z que es un conjunto de piezas Z para N cantidad de Objetos Z.
• La Cara Z que corresponde al número de cara y la distancia entre la cámara y
la cara.

//Tipos para los ZObjs


typedef struct etd_zt{
int objeto;
int dist;
} etd_z_token;

typedef struct etd_zo{


etd_z_token * zobj;
int NZObjs;
char estado;
} etd_z_tabla;

//Ordenacion de caras
typedef struct etd_zf{
unsigned short cara;
int dist;
} etd_z_face;

I L U M I N A C I Ó N

La iluminación consiste en otro vértice del espacio que apunta a una posición específica
del entorno, con el objetivo de iluminar al vértice objetivo. La iluminación depende de la
intensidad, su modo de atenuarse al objeto, y la distancia mínima y máxima.

Y las Tablas de Luz son listas de focos de luz que se asignan a los objetos, cada objeto
puede usar una única Tablas de Luz pero ésta puede ser usada por cualquier número de
objetos. Antes que nada quiero explicar como funciona el sistema de iluminación en
nuestro motor 3D, el algoritmo usado es un Lambert, que, todo hay que decirlo, no es
nada real, pero al menos da un aspecto de iluminación a las escenas. Se basa en el ángulo

12
entre el vector de dirección del foco y la normal de cada cara, por lo que todas las caras
que sean paralelas se verán iluminadas de la misma forma (si tienen el mismo color base
el color destino será el mismo).

typedef struct etd_l{


etd_punto3d posicion;
etd_punto3d objetivo;
int intensidad;
char modo_atenuacion; //Modo de atenuacion
// 0 = no atenua
// 1 = mide dist a objeto
// 2 = distancia a cara
int distancia_minima; //Para la atenuacion (está al cuadrado)
int distancia_maxima; //Para la atenuacion (está al cuadrado)
double fact_at;
} etd_luz;

typedef struct _etd_tl{


int *luz; //Vector de luces de la tabla
char NLuces; //Numero de luces de la tabla
char modo; // -1 = indica que la tabla ha sido borrada
//0 = Se suman las luces
//1 = Sólo se usa la mayor
} etd_l_tabla;

V A R I A B L E S A U X I L I A R E S

Antes de desarrollar nuestro motor es necesario definer ciertas variables auxiliaries que
nos ayudarán a realizar todos los cálculos necesarios para desarrollar el motor 3D.

//Variables Auxiliares
int NObjetos; //Numero de objetos creados
etd_objeto3d *objeto; //Array de objetos
etd_objeto3d **fx_obj; //Puntero para controlar los objetos desde Fénix
int NCamaras; //Numero de camaras creadas
etd_camara *camara; //Array de camaras
etd_camara **fx_camara; //Puntero para controlar las camaras desde Fénix
int NLuces; //Numero de luces
etd_luz *luz; //Array de luces
int NTablasZ ; //Numero de tablas creadas
etd_z_tabla *tabla_z; //Array de tablas
int NTablasL; //Numero de tabla de luz creadas
etd_l_tabla *tabla_l; //Array de tablas de luz

//Variables de render
int ancho_scr = 320;
int alto_scr = 200;
int x_scr = 160;
int y_scr = 100;
//Opciones de render de luz
char op_luz_16=1; //0 = 8 bits, 1 = 16 bits

etd_punto3d pos; //Posición auxiliar del objeto


etd_punto3d p3d[VERTEX_MAX]; //Aqui metemos los vertices aunxiliares de una
rotacion
etd_punto3d pm3d[CARAS_MAX]; //Aqui metemos los puntos medios auxiliares

13
etd_punto3d n3d[CARAS_MAX]; //Aqui las normales auxiliares de la rotacion
etd_punto3d l3d[CARAS_MAX]; //Aqui las normales auxiliares de la rotacion
(para luces)
etd_punto2d p2d[10]; //Puntos 2d auxiliares
unsigned short NcarasP;
etd_z_face caraP[CARAS_MAX];

etd_luz luz_test = {50.0,50.0,50.0,50,10000};

float coseno[36000];
float seno[36000];

I N I C I O D E L M O T O R

Ahora una vez que hemos desarrollado todas las estructuras y variables iniciales, ya
podemos comenzar a crear las funciones necesarias para inicializar nuestro motor.

El primero que vamos a crear se va a llamar ETD_init, que va a ser nuestra primera
función que podemos convocar desde Fenix para inicializar nuestro Motor 3D. Hasta
ahora no hemos hecho nada. Simplemente hemos definido una variable Fénix Objeto
que apunta a todos los Objetos 3D que se crean en el código fuente, y se ha definido una
variable Fénix Cámara que apunta a todas las Cámaras 3D que se crean en el código
fuente.

static int ETD_init (INSTANCE * my, int * params) {


int i;
double ang;

//Apuntamos al puntero desde Div controla todos los objetos


fx_obj = (etd_objeto3d **)params[0];
//Apuntamos al puntero desde Div controla todas las camaras
fx_camara = (etd_camara **)params[1];
//Calcula tablas de senos y cosenos
for (i=0;i<=36000;i++){ //Calcula hasta centesimas de grado
ang = (i-18000.0)*_PI/18000.0;
coseno[i] = (float)cos(ang);
seno[i] = (float)sin(ang);
}
//Creamos la camara 0
return _Nueva_Camara();
}

Una vez que hemos apuntado tanto los Objetos 3D como las Cámaras 3D, es necesario
realizar operaciones trigonométricas invocando a las funciones de Fenix cos(ang) y
sin(ang), pero usar estos métodos consume demasiado tiempo, reduciendo el
rendimiento. Para evitar ese problema es necesario hacer un ciclo repetitivo desde el
ángulo 0º hasta el ángulo 360º (36000) para guardar los valores respectivos de senos y
cosenos en sus respectivos arreglos, declarados en el artículo anterior.

Y por último se invoca al método _Nueva_Camara para crear la primera cámara que
apunta al espacio 3D vacío.

14
C R E A C I Ó N Y E L I M I N A C I O N D E L A C Á M A R A 3 D

El siguiente paso consiste en crear la cámara en 3D a través de la función


_Nueva_Camara.

La misma consiste en buscar si hay un hueco disponible para crear la cámara 3D. En
caso que no exista el hueco, se procede a crear uno, asignandole un espacio de memoria
a la Cámara 3D, y éste puede incrementarse dependiendo del número de cámaras que se
crean en el código fuente.

Luego la estructura de la cámara asignada es inicializada, y la variable Fénix Camara es


asignada con las Cámaras 3D que se crean en el código fuente. Y por último el número
de cámaras aumenta retornando la cámara actual.

int _Nueva_Camara () {
int i;
char encontrado=0;

//Primero le buscamos un hueco


if (NCamaras){
for (i=0;i<NCamaras;i++){
if (camara[i].estado == -1) {encontrado=1; break;}
}
}

//Si no encontramos hueco lo hacemos


if (!encontrado){
if (NCamaras == 0)
camara = (etd_camara *)malloc(sizeof (etd_camara));
else
camara = (etd_camara *)realloc(camara,
(NCamaras+1)*sizeof(etd_camara));
i = NCamaras;
}

//Iniciamos las variables


camara[i].file = 0;
camara[i].graph = 0;
camara[i].posicion.x = 0;
camara[i].posicion.y = 0;
camara[i].posicion.z = 0;
camara[i].angulo.alfa = 0;
camara[i].angulo.beta = 0;
camara[i].angulo.gamma = 0;
camara[i].estado = 0;
camara[i].distancia = ancho_scr;
camara[i].distancia_minima = 10;
camara[i].distancia_maxima = 1000;

//Reapuntamos el puntero de Div por si ha habido realocación


*fx_camara = camara;
NCamaras++;
return (i);

15
}

Una vez que hemos creado la función para inicializar la cámara es necesario crear una
función en Fenix que nos permita inicializar la cámara en los objetos Cámara 3D que se
crean en el código fuente:

static int ETD_Nueva_Camara (INSTANCE * my, int * params) {


return _Nueva_Camara ();
}

Y por último, es necesario crearle una función que elimine la cámara liberando memoria.

static int ETD_Elimina_Camara (INSTANCE * my, int * params) {


camara[params[0]] .estado = -1;
return 0;
}

F I L M A N D O A N U E S T R O OB J E T O 3 D

Aunque no es necesario, la Cámara 3D es capáz de apuntar a un Objeto 3D en


particular. Por consiguiente se ha creado la función ETD_Dirige_Camara, en donde
recibimos los siguientes parámetros:

• Identificador de cámara a apuntar


• Coordenada X del punto a apuntar
• Coordenada Y del punto a apuntar
• Coordenada Z del punto a apuntar

Una vez recolectada esa información, se procede a calcular el ángulo alfa y beta en
centésimas de grados:

Coordenada Y Cámara - Coordenada Y del Objeto


Alfa = A tan 2
Coordenada Z Cámara - Coordenada Z del Objeto

Coordenada Z Cámara - Coordenada Z del Objeto


Beta = A tan 2
Coordenada X Cámara - Coordenada X del Objeto

Una vez que se realiza ese cálculo, se procede a actualizar esos datos a la Cámara 3D
actual, y se realizan las correcciones de sus ángulos si es necesario.

static int ETD_Dirige_Camara (INSTANCE * my, int * params) {


double alfa,beta;
alfa = atan2(camara[params[0]].posicion.y - params[2],
camara[params[0]].posicion.z - params[3]);
beta = atan2(camara[params[0]].posicion.z - params[3],
camara[params[0]].posicion.x - params[1]);
camara[params[0]].angulo.alfa = alfa*5729.57795131-18000;

16
camara[params[0]].angulo.beta = beta*5729.57795131+9000;
if (camara[params[0]].angulo.alfa>18000) camara[params[0]].angulo.alfa-
=36000;
if (camara[params[0]].angulo.alfa<-18000)
camara[params[0]].angulo.alfa+=36000;
if (camara[params[0]].angulo.alfa>9000) camara[params[0]].angulo.alfa =
18000 - camara[params[0]].angulo.alfa;
if (camara[params[0]].angulo.alfa<-9000) camara[params[0]].angulo.alfa =
-camara[params[0]].angulo.alfa - 18000;
return 0;
}

A V A N Z A N D O N U E S T R A C ÁM A R A 3 D

Para que nuestra cámara avance, es necesario tener a disposición:

• El identificador de cámara a mover


• La distancia que se moverá la cámara
• El ángulo alfa en que se moverá la cámara
• El ángulo beta en que se moverá la cámara

Una vez recolectada esa información, ahora se procede a normalizar los ángulos y
procedemos a avanzar la cámara desplazándonos en coordenadas X sumando la
multiplicación de la distancia por el coseno del ángulo beta, en coordenadas Y sumando
la multiplicación de la distancia por el seno del ángulo alfa, y en coordenadas Z sumando
la multiplicación de la distancia por el seno del ángulo beta.

static int ETD_Avanza_Camara (INSTANCE * my, int * params) {


int id = params[0];
int dist = params[1];
int alfa = params[2]+18000;
int beta = params[3]+18000;

//Normalizamos los angulos


while(alfa<0) alfa+=36000; while(alfa>36000) alfa-=36000;
while(beta<0) beta+=36000; while(beta>36000) beta-=36000;

//Avanzamos la camara
camara[id].posicion.x += dist*coseno[beta];
camara[id].posicion.y += dist*seno[alfa];
camara[id].posicion.z += dist*seno[beta];

return 0;
}

C R E A C I Ó N Y E L I M I N A C I Ó N D E L O B J E T O 3 D

Para crear un Objeto 3D, se hace referencia a la función _Nuevo_Objeto, que consiste
en crear el Objeto si no encuentra un hueco disponible, asignándole al Objeto 3D un
espacio de memoria, y éste al igual que la Cámara 3D puede incrementar su espacio de

17
memoria dependiendo de la cantidad de Objetos 3D que se crean cuando se codifica en
el código fuente.

Luego al Objeto 3D especificado se le inicializan las variables y se incrementa el número


de objetos. Y por último la variable Fénix Objeto es asignada por el conjunto de Objetos
3D que se crean cuando se invoca esta función.

int _Nuevo_Objeto () {
int i;
char encontrado=0;

//Primero le buscamos un hueco


//Si hay algún objeto con ModoVision a -100 significa que ha sido
eliminado
if (NObjetos){
for (i=0;i<NObjetos;i++){
if (objeto[i].ModoVision == -100) {encontrado=1; break;}
}
}

//Si no encontramos hueco lo hacemos


if (!encontrado){
if (NObjetos == 0)
objeto = (etd_objeto3d *)malloc(sizeof(etd_objeto3d));
else
objeto = (etd_objeto3d *)realloc(objeto,
(NObjetos+1)*sizeof(etd_objeto3d));
i = NObjetos;
}

//Iniciamos las variables


objeto[i].posicion.x = 0;
objeto[i].posicion.y = 0;
objeto[i].posicion.z = 0;
objeto[i].vertice = 0;
objeto[i].arista = 0;
objeto[i].cara = 0;
objeto[i].NVertices = 0;
objeto[i].NAristas = 0;
objeto[i].NCaras = 0;
objeto[i].giro.alfa = 0;
objeto[i].giro.beta = 0;
objeto[i].giro.gamma = 0;
objeto[i].ModoVision = 56;
objeto[i].escalado = 1.0;
objeto[i].FocoLuz = 0;

NObjetos++;

//Redirigimos el puntero Div si hemos cambiado de zona de memoria


*fx_obj = objeto;
return i;
}

Y para implementar este llamado de función en nuestro lenguaje de programación Fenix,


es necesario crear una función de referencia llamada ETD_Nuevo_Objeto:

18
static int ETD_Nuevo_Objeto (INSTANCE * my, int * params) {
return _Nuevo_Objeto ();
}

Y para borrar el Objeto 3D que hemos creado, debemos implementar la función


ETD_Elimina_Objeto, en donde eliminamos el objeto que especifiquemos,
liberandolo de la memoria y definimos el Modo de Visión a -100 para que volvamos a
utilizar ese hueco para otro Objeto cuando lo volvamos a crear.

static int ETD_Elimina_Objeto (INSTANCE * my, int * params) {


//Primero hay que comprobar que exista ese objeto
if (params[0]<NObjetos){
//También hay que comprobar que no haya sido borrado ya
if (objeto[params[0]].ModoVision != -100){
//En este caso ya podemos borrar la tabla
//Liberamos la memoria de vertices aristas y caras
free(objeto[params[0]].vertice);
free(objeto[params[0]].arista);
free(objeto[params[0]].cara);
//Los contadores a 0 para que no se produzcan errores
objeto[params[0]].NVertices = 0;
objeto[params[0]].NAristas = 0;
objeto[params[0]].NCaras = 0;
//ModoVision a -100 para marcar el objeto como reusable
objeto[params[0]].ModoVision = -100;
}
return 0;
}else
//No existe el objeto
return -1;
}

C R E A C I Ó N D E L V É R T I C E 3D

Para la creación del vértice 3D, es necesario referenciar la función _Nuevo_Vertice, que
en caso de ser la primera vez que se invoca esta función, se reserva un espacio de
memoria para almacenar nuestro Vértice 3D en el Objeto 3D y se asignan las
coordenadas x, y, z al vértice.

De lo contrario se actualiza esa zona de memoria para crear las nuevas coordenadas del
Vértice 3D al Objeto 3D.

int _Nuevo_Vertice (int p0,float p1,float p2,float p3) {


//Si es el primer vertice
if (objeto[p0].NVertices == 0){
objeto[ p0].vertice = (etd_punto3d *)malloc(sizeof(etd_punto3d));
objeto[p0].vertice[objeto[p0].NVertices].x = (float )p1;
objeto[p0].vertice[objeto[p0].NVertices].y = (float )p2;
objeto[p0].vertice[objeto[p0].NVertices].z = (float )p3;
}
else{

19
objeto[p0].vertice =
realloc(objeto[p0].vertice,(objeto[p0].NVertices+1)*sizeof(etd_punto3d));
objeto[p0].vertice[objeto[p0].NVertices].x = (float)p1;
objeto[p0].vertice[objeto[p0].NVertices].y = (float)p2;
objeto[p0].vertice[objeto[p0].NVertices].z = (float)p3;
}
objeto[p0].NVertices++;
return (objeto[p0].NVertices-1);
}

Y para implementar este llamado de función en nuestro lenguaje de programación Fenix,


es necesario crear una función de referencia llamada ETD_Nuevo_Vertice:

static int ETD_Nuevo_Vertice (INSTANCE * my, int * params) {


return
_Nuevo_Vertice(params[0],(float)params[1],(float)params[2],(float)params[3])
;
}

C R E A C I Ó N D E L A A R I S TA

Para la creación de la Arista, es necesario referenciar la función ETD_Nueva_Arista,


que en caso de ser la primera vez que se invoca esta función, se reserva un espacio de
memoria para almacenar nuestra Arista en el Objeto 3D y se asignan los vértices v1, v2 y
el color a la Arista.

De lo contrario se actualiza esa zona de memoria para crear las nuevas Aristas al Objeto
3D.

static int ETD_Nueva_Arista (INSTANCE * my, int * params) {


//Si es la primera cara
if (objeto[params[0]].NAristas == 0){
objeto[params[0]].arista = (etd_linea *)malloc(sizeof(etd_linea));
objeto[params[0]].arista->v1 = (int)param s[1];
objeto[params[0]].arista->v2 = (int)params[2];
objeto[params[0]].arista->color = (int)params[3];
}
else {
objeto[params[0]].arista =
realloc(objeto[params[0]].arista,(objeto[params[0]].NAristas+1)*sizeof(etd_l
inea));
objeto[params[0]].arista[objeto[params[0]].NAristas].v1 =
(int)params[1];
objeto[params[0]].arista[objeto[params[0]].NAristas].v2 =
(int)params[2];
objeto[params[0]].arista[objeto[params[0]].NAristas].color =
(int)params[3];
}
objeto[params[0]].NAristas++;
return (objeto[params[0]].NAristas-1);
}

20
C R E A C I Ó N D E L A C A R A

Para culminar con las rutinas de inicialización de nuestro Objeto 3D, es necesario
referenciar la función Nueva_Cara, que en caso de ser la primera vez que se invoca esta
función, se reserva un espacio de memoria para almacenar nuestra Cara en el Objeto 3D
y se asignan los vértices v1, v2 y el color a la Cara.

De lo contrario se actualiza esa zona de memoria para crear las nuevas Caras al Objeto
3D.

Como último procedimiento se procede a calcular el Punto Medio y el Punto Normal de


la Cara y se aumenta el número de Caras en nuestro Objeto 3D, retornando la Cara
actual.

static int _Nueva_Cara (int p0,int p1,int p2,int p3,int p4) {


//Si es la primera cara
if (objeto[p0].NCaras == 0){
objeto[p0].cara = (etd_cara *)malloc(sizeof(etd_cara));
objeto[p0].cara->v1 = (int)p1;
objeto[p0].cara->v2 = (int)p2;
objeto[p0].cara->v3 = (int)p3;
}
else {
objeto[p0].cara = realloc(objeto[p0].cara,
(objeto[p0].NCaras+1) * sizeof(etd_cara));
objeto[p0].cara[objeto[p0].NCaras].v1 = (int)p1;
objeto[p0].cara[objeto[p0].NCaras].v2 = (int)p2;
objeto[p0].cara[objeto[p0].NCaras].v3 = (int)p3;
}
objeto[p0].cara[objeto[p0].NCaras].color = (int)p4;
_Calcula_Pmed(p0, objeto[p0].NCaras);
_Calcula_Normal(p0, objeto[p0].NCaras);
objeto[p0].NCaras++;
return (objeto[p0].NCaras-1);
}

Y para implementar este llamado de función en nuestro lenguaje de programación Fenix,


es necesario crear una función de referencia llamada ETD_Nueva_Cara:

static int ETD_Nueva_Cara (INSTANCE * my, int * params) {


return _Nueva_Cara((int)params[0],(int)params[1],(int)params[2],
(int)params[3],(int)params[4]);
}

C Á L C U L O D E L P U N T O M E D I O D E L A C A R A

El cálculo del Punto Medio (Pm) de la cara en un Objeto 3D se basa en sumar las
coordenadas X, Y, Z de los tres vértices de la cara y ese resultado se divide entre 3.

21
void _Calcula_Pmed (int id, int cara) {
objeto[id].cara[cara].pmed.x =
(objeto[id].vertice[objeto[id].cara[cara].v1].x +
objeto[id].vertice[objeto[id].cara[cara].v2].x +
objeto[id].vertice[objeto[id].cara[cara].v3].x)/3;
objeto[id].cara[cara].pmed.y =
(objeto[id].vertice[objeto[id].cara[cara].v1].y +
objeto[id].vertice[objeto[id].cara[cara].v2].y +
objeto[id].vertice[objeto[id].cara[cara].v3].y)/3;
objeto[id].cara[cara].pmed.z =
(objeto[id].vertice[objeto[id].cara[cara].v1].z +
objeto[id].vertice[objeto[id].cara[cara].v2].z +
objeto[id].vertice[objeto[id].cara[cara].v3].z)/3;
}

C Á L C U L O D E L P U N T O N O R M A L D E L A C A R A

El vector unitario normal a la superficie del plano que contiene al triángulo, N = (nx,
ny, nz), se obtiene mediante el producto vectorial de dos vectores construidos con los
vértices del polígono.

x1 = v2.x - pmed.x
y1 = v2.y - pmed.y
z1 = v2.z - pmed.z
x2 = v3.x - pmed.x
y2 = v3.y - pmed.y
z2 = v3.z - pmed.z

normal.x = y1 * z2 - y2 * z1
normal.y = x2 * z1 - x1 * z2
normal.z = x1 * y2 - y1 * x2

Y por último para sacar el Vector Unitario Normal se calcula la raíz de las sumas al
cuadrado de las tres normales:

tam = normal.x 2 + normal.y 2 + normal.z 2

normal.x /= tam;
normal.y /= tam;
normal.z /= tam;

22
El orden en que se definen los vértices es importante, ya que la dirección de la normal
varía en función de ello. Para nuestros propósitos consideraremos los vértices
ordenados, sobre el plano, en el mismo sentido del avance de las agujas del reloj.

Teóricamente al implementar la función _Calcula_Normal no debería ser necesaria


usarla nunca pues las normales se recalculan automáticamente, pero es posible que el
usuario la modifique actualmente o que la acumulación de errores por rotaciones sea
significativa y se necesite actualizarlas.

void _Calcula_Normal (int id, int cara) {


float _x1,_y1,_z1,_x2,_y2,_z2;
double tam;
//Mejor que el punto común sea el centro de la cara
_x1 = objeto[id].vertice[objeto[id].cara[cara].v2].x -
objeto[id].cara[cara].pmed.x;
_y1 = objeto[id].vertice[objeto[id].cara[cara].v2].y -
objeto[id].cara[cara].pmed.y;
_z1 = objeto[id].vertice[objeto[id].cara[cara].v2].z -
objeto[id].cara[cara].pmed.z;
_x2 = objeto[id].vertice[objeto[id].cara[cara].v3].x -
objeto[id].cara[cara].pmed.x;
_y2 = objeto[id].vertice[objeto[id].cara[cara].v3].y -
objeto[id].cara[cara].pmed.y;
_z2 = objeto[id].vertice[objeto[id].cara[cara].v3].z -
objeto[id].cara[cara].pmed.z;

// Calculamos la normal
objeto[id].cara[cara].normal.x = _y1*_z2 - _y2*_z1;
objeto[id].cara[cara].normal.y = _x2*_z1 - _x1*_z2;
objeto[id].cara[cara].normal.z = _x1*_y2 - _y1*_x2;

//Ahora hacemos el vector unitario


tam = sqrt(objeto[id].cara[cara].normal.x*objeto[id].cara[cara].normal.x+
objeto[id].cara[cara].normal.y*objeto[id].cara[cara].normal.y+
objeto[id].cara[cara].normal.z*objeto[id].cara[cara].normal.z);

objeto[id].cara[cara].normal.x /= tam;
objeto[id].cara[cara].normal.y /= tam;
objeto[id].cara[cara].normal.z /= tam;
}

Hasta ahora hemos creado las rutinas necesarias para crear nuestro Objeto 3D en su
totalidad. Ahora sólo falta renderizarlo a nuestra pantalla 2D. Para realizar eso, es
necesario crear estas técnicas antes de renderizar nuestro objeto 3D a la Pantalla:

• Rotar el Objeto
• Eliminación de la Cara Posterior (Back Face Culling)

23
• Ordenar las Caras

R O T A N D O N U E S T R O O B J E T O 3 D

Para rotar nuestro Objeto 3D, se hace referencia a la función _Rotar_Objeto, y como
verá el código fuente es muy complejo, y es necesario que sepamos para qué sirven los
siguientes Puntos 3D auxiliares:

• pos - Posición auxiliar del objeto


• p3d - Los vertices aunxiliares de una rotacion.
• pm3d - Los puntos medios auxiliares.
• n3d - Las normales auxiliares de la rotacion.
• l3d - Las normales auxiliares de la rotacion (para luces).
• _DiSt – Distancia entre dos puntos.

void _Rotar_Objeto (int id, char modo, char OC, int id_camara) {
float xb,yb,zb; //Coords provixionales
int alfa, beta, gamma;
int i;
etd_punto3d _DiSt;

Existen tres tipos de rotaciones de objetos:

• modo 0 = normal
• modo 1 = permanente
• modo 2 = posiciones (Referido a camara)

Para el modo rotación de posiciones, es necesario calcular los ángulos alfa, beta y gama a
través de los ángulos de la camara a excepción de gamma que equivale a 180º. Luego se
saca la distancia _DiSt desde la Cámara hasta el Objeto. Para los otros modos, los
ángulos alfa, beta y gamma se calculan a través de los ángulos de giro del objeto.

if (modo == 2) {
alfa = camara[id_camara].angulo.alfa+18000;
beta = camara[id_camara].angulo.beta+18000;
gamma = 18000;
_DiSt.x = objeto[id].posicion.x - camara[id_camara].posicion.x;
_DiSt.y = objeto[id].posicion.y - camara[id_camara].posicion.y;
_DiSt.z = objeto[id].posicion.z - camara[id_camara].posicion.z;
}
else {
alfa = -objeto[id].giro.alfa+18000;
beta = objeto[id].giro.beta+18000;
gamma = objeto[id].giro.gamma+18000;
}

Luego se procede a normalizar los ángulos en caso tal de que los mismos sean menor
que 0º o mayor a 360º.

24
//Normalizamos angulos
while(alfa<0) alfa+=36000;
while(alfa>36000) alfa-=36000;

while(beta<0) beta+=36000;
while(beta>36000) beta-=36000;

while(gamma<0)gamma+=36000;
while(gamma>36000)gamma-=36000;

Antes de que véa el código fuente, es necesario que sepa las fórmulas de rotación de los
ejes:

(1) Rotación del eje X:

yb = yini;
zb = zini;
yrot = (coseno[alfa] * yb) - (seno[alfa] * zb);
zrot = (seno[alfa] * yb) + (coseno[alfa] * zb);

(2) Rotación del eje Y:

xb = xini;
zb = zrot;
xrot = (coseno[beta] * xb) + (seno[beta] * zb);
zrot = -(seno[beta] * xb) + (coseno[beta] * zb);

(3) Rotación del eje Z:

xb = xrot;
yb = yrot;
xrot = (coseno[gamma] * xb) - (seno[gamma] * yb);
yrot = (seno[gamma] * xb) + (coseno[gamma] * yb);

En donde:
• Hay que tener en cuenta que los signos pueden variar según los ejes de
coordenadas.
• xini, yini y zini son los valores iniciales del punto (antes de rotarlo)
• xb, yb y zb son variables auxiliares.
• xrot, yrot y zrot son los puntos ya rotados.

Aquí les explico el algoritmo de rotación:

• Si alfa-18000 == 0
o Si beta-18000 == 0
§ Si gamma-18000 == 0 (Cuando no hay rotación)
• Si se está rotando con respecto a posiciones de la cámara

25
o La posición de X, Y, Z es igual a la distancia de X, Y,
Z.
o Los Puntos 3D de los vértices se desplazan con
respecto a la Distancia.
o Los Puntos Normales y los Puntos Medios de las
Caras se desplazan con respecto a la Distancia.
• De lo contrario
o Los Puntos 3D de los vértices son equivalentes a los
puntos de los vértices del Objeto 3D.
o Los Puntos Normales son equivalentes a los Puntos
Normales del Objeto, Los Puntos Normales de Luz
son equivalentes a los Puntos Normales y los Puntos
Medios de las Caras son equivalentes a los Puntos
Medios del Objeto.
§ Si gamma-18000 es distinto de 0 (Cuando se rota el objeto con
respecto al eje Z)
• Si el modo de rotación no es con respecto a posiciones de la
cámara
o Rotamos los vértices con respecto a Z (Fórmula 3).
o Rotamos las caras con respecto a Z (Fórmula 3) .
o Si beta-18000 es distinto de 0 (Cuando se rota tanto la cámara como el
objeto con respecto al eje Y)
§ Si gamma-18000 == 0
• Si el modo de rotación es con respecto a posiciones de la
cámara
o Rotamos la cámara con respecto a Y (Fórmula 2).
o Rotamos los vértices y las caras con respecto a Y
(Fórmula 2).
• De lo contrario
o Rotamos los vértices y las caras con respecto a Y
(Fórmula 2).
§ Si gamma-18000 es distinto de 0 (Cuando se rota el objeto con
respecto al eje Y, Z)
• Si el modo de rotación no es con respecto a posiciones de la
cámara
o Rotamos los vértices y las caras con respecto a Y
(Fórmula 2).
o Rotamos los vértices y las caras con respecto a Z
(Fórmula 3).
• Si alfa-18000 es distinto de 0
o Si beta-18000 == 0
§ Si gamma-18000 == 0 (Cuando se rota la cámara y el objeto con
respecto al eje X)

26
• Si el modo de rotación es con respecto a posiciones de la
cámara
o Rotamos la cámara con respecto a X (Fórmula 1).
o Rotamos los vértices y las caras con respecto a X
(Fórmula 1).
• De lo contrario
o Rotamos los vértices y las caras con respecto a X
(Fórmula 1).
§ Si gamma-18000 es distinto de 0 (Cuando se rota el objeto con
respecto al eje X, Z)
• Si el modo de rotación no es con respecto a posiciones de la
cámara
o Rotamos los vértices y las caras con respecto a X
(Fórmula 1).
o Rotamos los vértices y las caras con respecto a Z
(Fórmula 3).
o Si beta-18000 es distinto de 0
§ Si gamma-18000 == 0 (Cuando se rota la cámara y el objeto con
respecto al eje X, Y)
• Si el modo de rotación es con respecto a posiciones de la
cámara
o Rotamos la cámara con respecto a X (Fórmula 1).
o Rotamos la cámara con respecto a Y (Fórmula 2).
o Rotamos los vértices y las caras con respecto a X
(Fórmula 1).
o Rotamos los vértices y las caras con respecto a Y
(Fórmula 2).
• De lo contrario
o Rotamos los vértices y las caras con respecto a X
(Fórmula 1).
o Rotamos los vértices y las caras con respecto a Y
(Fór mula 2).
§ Si gamma-18000 es distinto de 0 (Cuando se rota el objeto con
respecto al eje X, Y, Z)
• Si el modo de rotación no es con respecto a posiciones de la
cámara
o Rotamos los vértices y las caras con respecto a X
(Fórmula 1).
o Rotamos los vértices y las caras con respecto a Y
(Fórmula 2).
o Rotamos los vértices y las caras con respecto a Z
(Fórmula 3).
• Si el modo de rotación es Permanente
o Los Vértices del Objeto son equivalentes a los Puntos 3D Auxiliares

27
o Los Puntos Medios y Normal de las Caras son equivalentes a los Puntos
Medios y Normal Auxiliares.
o Se resetean los ángulos de giro del Objeto 3D.

Una vez visto este complejo algoritmo, podemos ver cómo queda el código fuente:

if (alfa-18000 == 0) {
if (beta-18000 == 0) {
if (gamma-18000 == 0){//Han metido 3 ceros...
if (modo==2){
pos.x = _DiSt.x;
pos.y = _DiSt.y;
pos.z = _DiSt.z;
for (i=0;i<objeto[id].NVertices;i++) {
p3d[i].x = p3d[i].x + _DiSt.x;
p3d[i].y = p3d[i].y + _DiSt.y;
p3d[i].z = p3d[i].z + _DiSt.z;
}

for (i=0;i<objeto[id].NCaras;i++) {
n3d[i].x = n3d[i].x + _DiSt.x;
n3d[i].y = n3d[i].y + _DiSt.y;
n3d[i].z = n3d[i].z + _DiSt.z;
if (!OC) continue ;
pm3d[i].x = pm3d[i].x + _DiSt.x;
pm3d[i].y = pm3d[i].y + _DiSt.y;
pm3d[i].z = pm3d[i].z + _DiSt.z;
}
}
else {
for (i=0;i<objeto[id].NVertices;i++) {
p3d[i].x = objeto[id].vertice[i].x;
p3d[i].y = objeto[id].vertice[i].y;
p3d[i].z = objeto[id].vertice[i].z;
}

for (i=0;i<objeto[id].NCaras;i++) {
n3d[i].x = objeto[id].cara[i].normal.x;
n3d[i].y = objeto[id].cara[i].normal.y;
n3d[i].z = objeto[id].cara[i].normal.z;
l3d[i].x = n3d[i].x;
l3d[i].y = n3d[i].y;
l3d[i].z = n3d[i].z;
if (!OC) continue ;
pm3d[i].x = objeto[id].cara[i].pmed.x;
pm3d[i].y = objeto[id].cara[i].pmed.y;
pm3d[i].z = objeto[id].cara[i].pmed.z;
}
}
}
else {
//Solo cuenta gamma
if (modo!=2){
for (i=0;i<objeto[id].NVertices;i++) {
xb = objeto[id].vertice[i].x;
yb = objeto[id].vertice[i].y;
p3d[i].x = (coseno[gamma] * xb) - (seno[gamma] * yb);

28
p3d[i].y = (seno[gamma] * xb) + (coseno[gamma] * yb);
p3d[i].z = objeto[id].vertice[i].z;
}

//Ahora rotamos las normales y los pmed


for (i=0;i<objeto[id].NCaras;i++) {
xb = objeto[id].cara[i].normal.x;
yb = objeto[id].cara[i].normal.y;
n3d[i].x = (coseno[gamma] * xb) - (seno[gamma] * yb);
n3d[i].y = (seno[gamma] * xb) + (coseno[gamma] * yb);
n3d[i].z = objeto[id].cara[i].normal.z;
l3d[i].x = n3d[i].x;
l3d[i].y = n3d[i].y;
l3d[i].z = n3d[i].z;
if (!OC) continue;
xb = objeto[id].cara[i].pmed.x;
yb = objeto[id].cara[i].pmed.y;
pm3d[i].x = (coseno[gamma] * xb) - (seno[gamma] * yb);
pm3d[i].y = (seno[gamma] * xb) + (coseno[gamma] * yb);
pm3d[i].z = objeto[id].cara[i].pmed.z;
}
}
}
}
else {
if (gamma-18000 == 0){
//Solo cuenta beta
if (modo==2){
xb = _DiSt.x;
zb = _DiSt.z;
pos.x = (coseno[beta] * xb) + (seno[beta] * zb);
pos.z = -(seno[beta] * xb) + (coseno[beta] * zb);
pos.y = _DiSt.y;

for (i=0;i<objeto[id].NVertices;i++) {
xb = p3d[i].x + _DiSt.x;
zb = p3d[i].z + _DiSt.z;
p3d[i].x = (coseno[beta] * xb) + (seno[beta] * zb);
p3d[i].z = -(seno[beta] * xb) + (coseno[beta] * zb);
p3d[i].y = p3d[i].y + _DiSt.y;
}

for (i=0;i<objeto[id].NCaras;i++) {
xb = n3d[i].x + _DiSt.x;
zb = n3d[i].z + _DiSt.z;
n3d[i].x = (coseno[beta] * xb) + (seno[beta] * zb);
n3d[i].z = -(seno[beta] * xb) + (coseno[beta] * zb);
n3d[i].y = n3d[i].y + _DiSt.y;
if (!OC) continue;
xb = pm3d[i].x + _DiSt.x;
zb = pm3d[i].z + _DiSt.z;
pm3d[i].x = (coseno[beta] * xb) + (seno[beta] * zb);
pm3d[i].z = -(seno[beta] * xb) + (coseno[beta] * zb);
pm3d[i].y = pm3d[i].y + _DiSt.y;
}
}
else {
for (i=0;i<objeto[id].NVertices;i++) {
xb = objeto[id].vertice[i].x;

29
zb = objeto[id].vertice[i].z;
p3d[i].x = (coseno[beta] * xb) + (seno[beta] * zb);
p3d[i].z = -(seno[beta] * xb) + (coseno[beta] * zb);
p3d[i].y = objeto[id].vertice[i].y;
}

for (i=0;i<objeto[id].NCaras;i++) {
xb = objeto[id].cara[i].normal.x;
zb = objeto[id].cara[i].normal.z;
n3d[i].x = (coseno[beta] * xb) + (seno[beta] * zb);
n3d[i].z = -(seno[beta] * xb) + (coseno[beta] * zb);
n3d[i].y = objeto[id].cara[i].normal.y;
l3d[i].x = n3d[i].x;
l3d[i].y = n3d[i].y;
l3d[i].z = n3d[i].z;
if (!OC) continue ;
xb = objeto[id].cara[i].pmed.x;
zb = objeto[id].cara[i].pmed.z;
pm3d[i].x = (coseno[beta] * xb) + (seno[beta] * zb);
pm3d[i].z = -(seno[beta] * xb) + (coseno[beta] * zb);
pm3d[i].y = objeto[id].cara[i].pmed.y;
}
}
}
else{
//Cuentan beta y gamma
if (modo!=2){
for (i=0;i<objeto[id].NVertices;i++) {
xb = objeto[id].vertice[i].x;
zb = objeto[id].vertice[i].z;
p3d[i].x = (coseno[beta] * xb) + (seno[beta] * zb);
p3d[i].z = -(seno[beta] * xb) + (coseno[beta] * zb);
xb = p3d[i].x;
yb = objeto[id].vertice[i].y;
p3d[i].x = (coseno[gamma] * xb) - (seno[gamma] * yb);
p3d[i].y = (seno[gamma] * xb) + (coseno[gamma] * yb);
}

for (i=0;i<objeto[id].NCaras;i++) {
xb = objeto[id].cara[i].normal.x;
zb = objeto[id].cara[i].normal.z;
n3d[i].x = (coseno[beta] * xb) + (seno[beta] * zb);
n3d[i].z = -(seno[beta] * xb) + (coseno[beta] * zb);
xb = n3d[i].x;
yb = objeto[id].cara[i].normal.y;
n3d[i].x = (coseno[gamma] * xb) - (seno[gamma] * yb);
n3d[i].y = (seno[gamma] * xb) + (coseno[gamma] * yb);
l3d[i].x = n3d[i].x;
l3d[i].y = n3d[i].y;
l3d[i].z = n3d[i].z;
if (!OC) continue ;
xb = objeto[id].cara[i].pmed.x;
zb = objeto[id].cara[i].pmed.z;
pm3d[i].x = (coseno[beta] * xb) + (seno[beta] * zb);
pm3d[i].z = -(seno[beta] * xb) + (coseno[beta] * zb);
xb = pm3d[i].x;
yb = objeto[id].cara[i].pmed.y;
pm3d[i].x = (coseno[gamma] * xb) - (seno[gamma] * yb);
pm3d[i].y = (seno[gamma] * xb) + (coseno[gamma] * yb);

30
}
}
}
}
}
else{
if (beta-18000 == 0){
if (gamma-18000 == 0){
//Solo cuenta alfa
if (modo==2){
yb = _DiSt.y;
zb = _DiSt.z;
pos.y = (coseno[alfa] * yb) - (seno[alfa] * zb);
pos.z = (seno[alfa] * yb) + (coseno[alfa] * zb);
pos.x = _DiSt.x;

for (i=0;i<objeto[id].NVertices;i++) {
yb = p3d[i].y + _DiSt.y;
zb = p3d[i].z + _DiSt.z;
p3d[i].y = (coseno[alfa] * yb) - (seno[alfa] * zb);
p3d[i].z = (seno[alfa] * yb) + (coseno[alfa] * zb);
p3d[i].x = p3d[i].x + _DiSt.x;
}

for (i=0;i<objeto[id].NCaras;i++) {
yb = n3d[i].y + _DiSt.y;
zb = n3d[i].z + _DiSt.z;
n3d[i].y = (coseno[alfa] * yb) - (seno[alfa] * zb);
n3d[i].z = (seno[alfa] * yb) + (coseno[alfa] * zb);
n3d[i].x = n3d[i].x + _DiSt.x;
if (!OC) continue;
yb = pm3d[i].y + _DiSt.y;
zb = pm3d[i].z + _DiSt.z;
pm3d[i].y = (coseno[alfa] * yb) - (seno[alfa] * zb);
pm3d[i].z = (seno[alfa] * yb) + (coseno[alfa] * zb);
pm3d[i].x = pm3d[i].x + _DiSt.x;
}
}
else {
for (i=0;i<objeto[id].NVertices;i++) {
yb = objeto[id].vertice[i].y;
zb = objeto[id].vertice[i].z;
p3d[i].y = (coseno[alfa] * yb) - (seno[alfa] * zb);
p3d[i].z = (seno[alfa] * yb) + (coseno[alfa] * zb);
p3d[i].x = objeto[id].vertice[i].x;
}

for (i=0;i<objeto[id].NCaras;i++) {
yb = objeto[id].cara[i].normal.y;
zb = objeto[id].cara[i].normal.z;
n3d[i].y = (coseno[alfa] * yb) - (seno[alfa] * zb);
n3d[i].z = (seno[alfa] * yb) + (coseno[alfa] * zb);
n3d[i].x = objeto[id].cara[i].normal.x;
l3d[i].x = n3d[i].x;
l3d[i].y = n3d[i].y;
l3d[i].z = n3d[i].z;
if (!OC) continue;
yb = objeto[id].cara[i].pmed.y;
zb = objeto[id].cara[i].pmed.z;

31
pm3d[i].y = (coseno[alfa] * yb) - (seno[alfa] * zb);
pm3d[i].z = (seno[alfa] * yb) + (coseno[alfa] * zb);
pm3d[i].x = objeto[id].cara[i].pmed.x;
}
}
}
else{
//Cuentan alfa y gamma
if (modo!=2){
for (i=0;i<objeto[id].NVertices;i++) {
yb = objeto[id].vertice[i].y;
zb = objeto[id].vertice[i].z;
p3d[i].y = (coseno[alfa] * yb) - (seno[alfa] * zb);
p3d[i].z = (seno[alfa] * yb) + (coseno[alfa] * zb);
xb = objeto[id].vertice[i].x;
yb = p3d[i].y;
p3d[i].x = (coseno[gamma] * xb) - (seno[gamma] * yb);
p3d[i].y = (seno[gamma] * xb) + (coseno[gamma] * yb);
}

for (i=0;i<objeto[id].NCaras;i++) {
yb = objeto[id].cara[i].normal.y;
zb = objeto[id].cara[i].normal.z;
n3d[i].y = (coseno[alfa] * yb) - (seno[alfa] * zb);
n3d[i].z = (seno[alfa] * yb) + (coseno[alfa] * zb);
xb = objeto[id].cara[i].normal.x;
yb = n3d[i].y;
n3d[i].x = (coseno[gamma] * xb) - (seno[gamma] * yb);
n3d[i].y = (seno[gamma] * xb) + (coseno[gamma] * yb);
l3d[i].x = n3d[i].x;
l3d[i].y = n3d[i].y;
l3d[i].z = n3d[i].z;
if (!OC) continue ;
yb = objeto[id].cara[i].pmed.y;
zb = objeto[id].cara[i].pmed.z;
pm3d[i].y = (coseno[alfa] * yb) - (seno[alfa] * zb);
pm3d[i].z = (seno[alfa] * yb) + (coseno[alfa] * zb);
xb = objeto[id].cara[i].pmed.x;
yb = pm3d[i].y;
pm3d[i].x = (coseno[gamma] * xb) - (seno[gamma] * yb);
pm3d[i].y = (seno[gamma] * xb) + (coseno[gamma] * yb);
}
}
}
}
else{
if (gamma-18000 == 0){
//Cuentan alfa y beta
if (modo==2){
xb = _DiSt.x;
zb = _DiSt.z;
pos.x = (coseno[beta] * xb) + (seno[beta] * zb);
pos.z = -(seno[beta] * xb) + (coseno[beta] * zb);
yb = _DiSt.y;
zb = pos.z;
pos.y = (coseno[alfa] * yb) - (seno[alfa] * zb);
pos.z = (seno[alfa] * yb) + (coseno[alfa] * zb);

for (i=0;i<objeto[id].NVertices;i++) {

32
xb = p3d[i].x + _DiSt.x;
zb = p3d[i].z + _DiSt.z;
p3d[i].x = (coseno[beta] * xb) + (seno[beta] * zb);
p3d[i].z = -(seno[beta] * xb) + (coseno[beta] * zb);
yb = p3d[i].y + _DiSt.y;
zb = p3d[i].z;
p3d[i].y = (coseno[alfa] * yb) - (seno[alfa] * zb);
p3d[i].z = (seno[alfa] * yb) + (coseno[alfa] * zb);
}

for (i=0;i<objeto[id].NCaras;i++) {
xb = n3d[i].x + _DiSt.x;
zb = n3d[i].z + _DiSt.z;
n3d[i].x = (coseno[beta] * xb) + (seno[beta] * zb);
n3d[i].z = -(seno[beta] * xb) + (coseno[beta] * zb);
yb = n3d[i].y + _DiSt.y;
zb = n3d[i].z;
n3d[i].y = (coseno[alfa] * yb) - (seno[alfa] * zb);
n3d[i].z = (seno[alfa] * yb) + (coseno[alfa] * zb);
if (!OC) continue;
xb = pm3d[i].x + _DiSt.x;
zb = pm3d[i].z + _DiSt.z;
pm3d[i].x = (coseno[beta] * xb) + (seno[beta] * zb);
pm3d[i].z = -(seno[beta] * xb) + (coseno[beta] * zb);
yb = pm3d[i].y + _DiSt.y;
zb = pm3d[i].z;
pm3d[i].y = (coseno[alfa] * yb) - (seno[alfa] * zb);
pm3d[i].z = (seno[alfa] * yb) + (coseno[alfa] * zb);
}
}
else {
for (i=0;i<objeto[id].NVertices;i++) {
yb = objeto[id].vertice[i].y;
zb = objeto[id].vertice[i].z;
p3d[i].y = (coseno[alfa] * yb) - (seno[alfa] * zb);
p3d[i].z = (seno[alfa] * yb) + (coseno[alfa] * zb);
xb = objeto[id].vertice[i].x;
zb = p3d[i].z;
p3d[i].x = (coseno[beta] * xb) + (seno[beta] * zb);
p3d[i].z = -(seno[beta] * xb) + (coseno[beta] * zb);
}

for (i=0;i<objeto[id].NCaras;i++) {
yb = objeto[id].cara[i].normal.y;
zb = objeto[id].cara[i].normal.z;
n3d[i].y = (coseno[alfa] * yb) - (seno[alfa] * zb);
n3d[i].z = (seno[alfa] * yb) + (coseno[alfa] * zb);
xb = objeto[id].cara[i].normal.x;
zb = n3d[i].z;
n3d[i].x = (coseno[beta] * xb) + (seno[beta] * zb);
n3d[i].z = -(seno[beta] * xb) + (coseno[beta] * zb);
l3d[i].x = n3d[i].x;
l3d[i].y = n3d[i].y;
l3d[i].z = n3d[i].z;
if (!OC) continue;
yb = objeto[id].cara[i].pmed.y;
zb = objeto[id].cara[i].pmed.z;
pm3d[i].y = (coseno[alfa] * yb) - (seno[alfa] * zb);
pm3d[i].z = (seno[alfa] * yb) + (coseno[alfa] * zb);

33
xb = objeto[id].cara[i].pmed.x;
zb = pm3d[i].z;
pm3d[i].x = (coseno[beta] * xb) + (seno[beta] * zb);
pm3d[i].z = -(seno[beta] * xb) + (coseno[beta] * zb);
}
}
}
else{
//Cuentan alfa, beta y gamma
if (modo!=2){
for (i=0;i<objeto[id].NVertices;i++) {
yb = objeto[id].vertice[i].y;
zb = objeto[id].vertice[i].z;
p3d[i].y = (coseno[alfa] * yb) - (seno[alfa] * zb);
p3d[i].z = (seno[alfa] * yb) + (coseno[alfa] * zb);
xb = objeto[id].vertice[i].x;
zb = p3d[i].z;
p3d[i].x = (coseno[beta] * xb) + (seno[beta] * zb);
p3d[i].z = -(seno[beta] * xb) + (coseno[beta] * zb);
xb = p3d[i].x;
yb = p3d[i].y;
p3d[i].x = (coseno[gamma] * xb) - (seno[gamma] * yb);
p3d[i].y = (seno[gamma] * xb) + (coseno[gamma] * yb);
}

for (i=0;i<objeto[id].NCaras;i++) {
yb = objeto[id].cara[i].normal.y;
zb = objeto[id].cara[i].normal.z;
n3d[i].y = (coseno[alfa] * yb) - (seno[alfa] * zb);
n3d[i].z = (seno[alfa] * yb) + (coseno[alfa] * zb);
xb = objeto[id].cara[i].normal.x;
zb = n3d[i].z;
n3d[i].x = (coseno[beta] * xb) + (seno[beta] * zb);
n3d[i].z = -(seno[beta] * xb) + (coseno[beta] * zb);
xb = n3d[i].x;
yb = n3d[i].y;
n3d[i].x = (coseno[gamma] * xb) - (seno[gamma] * yb);
n3d[i].y = (seno[gamma] * xb) + (coseno[gamma] * yb);
l3d[i].x = n3d[i].x;
l3d[i].y = n3d[i].y;
l3d[i].z = n3d[i].z;
if (!OC) continue ;
yb = objeto[id].cara[i].pmed.y;
zb = objeto[id].cara[i].pmed.z;
pm3d[i].y = (coseno[alfa] * yb) - (seno[alfa] * zb);
pm3d[i].z = (seno[alfa] * yb) + (coseno[alfa] * zb);
xb = objeto[id].cara[i].pmed.x;
zb = pm3d[i].z;
pm3d[i].x = (coseno[beta] * xb) + (seno[beta] * zb);
pm3d[i].z = -(seno[beta] * xb) + (coseno[beta] * zb);
xb = pm3d[i].x;
yb = pm3d[i].y;
pm3d[i].x = (coseno[gamma] * xb) - (seno[gamma] * yb);
pm3d[i].y = (seno[gamma] * xb) + (coseno[gamma] * yb);
}
}
}
}
}

34
if (modo==1) { //Hacemos la rotacion permanente
for (i=0;i<objeto[id].NVertices;i++) {
objeto[id].vertice[i].x = p3d[i].x;
objeto[id].vertice[i].y = p3d[i].y;
objeto[id].vertice[i].z = p3d[i].z;
}

for (i=0;i<objeto[id].NCaras;i++) {
objeto[id].cara[i].normal.x = n3d[i].x;
objeto[id].cara[i].normal.y = n3d[i].y;
objeto[id].cara[i].normal.z = n3d[i].z;
objeto[id].cara[i].pmed.x = pm3d[i].x;
objeto[id].cara[i].pmed.y = pm3d[i].y;
objeto[id].cara[i].pmed.z = pm3d[i].z;
}

//Reseteamos angulos de giro


objeto[id].giro.alfa = 0;
objeto[id].giro.beta = 0;
objeto[id].giro.gamma = 0;
}
}

Y para implementar este llamado de función en nuestro lenguaje de programación Fenix,


es necesario crear una función de referencia llamada ETD_Rotate_Object:

static int ETD_Rotate_Object (INSTANCE * my, int * params) {


_Rotar_Objeto((int)params[0], 1, 0, 0);
return 0;
}

E L I M I N A C I Ó N D E L A C A R A P O S T E R I O R

El BFC es una técnica por la cual se calculan ¿qué caras miran hacia el observador? y
¿cuáles no?, pensemos por ejemplo en un cubo con sus 6 caras (si tienes un cubo a mano
y lo coges), si vamos girando el cubo, siempre veremos como máximo tres caras y hay
otras tres que no se ven. Esas tres caras que no se ven son las que no "miran" al
observador.

Como verás, dibujar caras que no se ven es perder tiempo de proceso, por ello hay que
descubrir cuales son las caras que no se ven, y para ello se necesitan ciertos
conocimientos matemáticos, pero básicamente el sistema se basa en calcular el coseno

35
del ángulo formado entre el punto de observación y la normal de la cara en cuestión. Así
pues, lo que realmente nos importa es hacia dónde apunta la normal (la normal es un
vector perpendicular al plano que apunta en uno u otro sentido) ya que hacia donde
apunte es hacia donde mira la cara. Esto se controla a la hora de definir la cara, ya que el
cálculo de la normal se basa en unas ecuaciones fijas y no son independiente del orden
en que se indican los vértices de la cara, es decir, si tenemos el triángulo de la figura, no
es lo mismo indicar el orden de vértices 1-2-3 que el 1-3-2, ya que varía completamente
el sentido del vector normal.

Ahora bien, es importante saber como hay que definir los vértices, si quisiésemos que
el triángulo "mirase" hacia fuera de la pantalla, es decir, que al meter en nuestro
motor 3D las coordenadas de ese triángulo se dibujase directamente en pantalla sin
ninguna rotación, tendríamos que definir sus vértices en sentido anti- horario (en
sentido contrario al de las agujas de un relo j), es decir, cualquiera de estas órdenes
nos valdría: 1-2-3, 2-3-1 ó 3-1-2, pero en el momento que giramos el triángulo sobre
su eje y o sobre su eje x, se vería la parte de atrás del triángulo, y visto desde el otro
lado el vector normal no apunta hacia nosotros, por lo que no se verá nada, así pues,
si lo que quisiésemos es que el triángulo se viese desde el otro lado, lo que haríamos
es definir uno de estos sentidos: 1-3-2, 2-1-3 ó 3-2-1.

Como verás, esto complica bastante el hacer modelos complejos introduciéndolos


vértice a vértice y cara a cara.

void _BFC(int id, int id_camara){


int i=0;
int j=0;
double dot;
NcarasP = 0;
for (i=0;i<objeto[id].NCaras;i++) {
dot = (n3d[i].x-pos.x)*p3d[objeto[id].cara[i].v1].x +
(n3d[i].y-pos.y)*p3d[objeto[id].cara[i].v1].y +
(n3d[i].z-pos.z)*p3d[objeto[id].cara[i].v1].z;

if (dot>0.0){ //Si es una cara que hay que pintar


caraP[j].cara = i; //La añadimos a la lista de caras a pintar
j++;
}
}
NcarasP = j;
}

36
O R D E N A C I Ó N D E L A S C A R A S

Una vez que hayamos optimizado la forma en como vamos a visualizar nuestro objeto
3D, ahora es necesario ordenar las Caras para renderizar de forma rápida nuestro Objeto
3D que explicaremos en el siguiente capítulo.

Este método consiste en almacenar la distancia auxiliar de la cara haciendo una suma de
las coordenadas de las Puntos Medios al cuadrado. Luego comenzamos a ordenar las
caras preguntándonos si la distancia de la Cara siguiente de la lista es menor que la
distancia de la Cara actual, y si es así se realiza el intercambio, con el objetivo de ordenar
las caras de menor a mayor.

void _OrdenaCaras(int id, int id_camara){


int i,j;
etd_z_face aux;

//Calculamos las distancias


for (i=0;i<NcarasP;i++){
//Calculamos las distancias al cuadrado
caraP[i].dist =
(pm3d[caraP[i].cara].x)*(pm3d[caraP[i].cara].x)+
(pm3d[caraP[i].cara].y)*(pm3d[caraP[i].cara].y)+
(pm3d[caraP[i].cara].z)*(pm3d[caraP[i].cara].z);
}

//Ordenamos las caras


for(i=0;i<NcarasP-1;i++) {// Hacer N -1 pasadas.
for(j=0;j<NcarasP-i-1;j++) {// Mirar los N-i-1 pares.
// Si el elemento j+1 es menor que el elemento j:
if(caraP[j+1].dist<caraP[j].dist){
// Se intercambian los elementos
aux.dist = caraP[j+1].dist;
aux.cara = caraP[j+1].cara;
caraP[j+1].dist = caraP[j].dist;
caraP[j+1].cara = caraP[j].cara;
caraP[j].dist = aux.dist;
caraP[j].cara = aux.cara;
}
}
}
}

37
3
Capítulo

Renderizando Objetos 3D
I N T R O D U C C I Ó N

En este capítulo, vamos a comenzar a crear las rutinas para renderizar nuestro Objeto
3D. Como verás, hemos creado las rutinas para darle vida tanto nuestro Objeto como a
la Cámara, sólo falta interpretarlo en la pantalla en coordenadas 2D.

Existen diferentes técnicas para renderizar nuestro objeto a la pantalla.

Puntos 3D: Dibuja los vértices en la pantalla. Podrá ver que no se pueden ver con
facilidad debido a que son representados gráficamente como un un simple pixel en la
pantalla.

Alambrado (Wireframe): Dibuja las aristas de las caras:

Relleno Plano sin eliminar la cara posterior (Flat Shading): Dibuja todas las caras
rellenas:

38
Relleno Plano eliminando la cara posterior (BFC Flat Shading): Elimina la cara
posterior para mejorar el rendimiento, y Ordena las caras antes de dibujar todas las caras
rellenas:

En el siguiente capítulo explicaremos técnicas más avanzadas de renderizar nuestro


objeto 3D tales como el de incluir la iluminación, el Búfer Z y mejores técnicas de
renderizar nuestro objeto.

D I B U J A N D O L A C A R A

Antes de renderizar nuestro Objeto 3D, primero hay que asegurarnos de poder dibujar la
cara correctamente. Por ello hemos implementado esta función para dibujar nuestro
triángulo en base a líneas.

Para ello debemos ordenar los puntos de menor a mayor con respecto a las coordenadas
Y del Vértice, para dibujar el triángulo de arriba hacia abajo.

Y luego se calcula la distancia del primer vértice y segundo vértice ordenado, y en base a
eso, se dibujan las líneas horizontales hasta que llegue a la arista límite destino del
triángulo.

Y por último se calcula la distancia del segundo vértice y tercer vértice ordenado, y en
base a eso, se dibujan las líneas horizontales hasta que llegue a la arista límite destino del
triángulo.

39
void _Dibuja_Triangulo (GRAPH *drawing_graph, etd_punto2d * v1, etd_punto2d
* v2, etd_punto2d * v3) {
int x1,y1,x2,y2,x3,y3;
int xd1,yd1,xd2,yd2,i;
int Lx,Rx;

//Ordenamos los puntos


if (v1->y > v2->y){
if (v1->y > v3->y){
if (v2->y>v3->y){
x3=v1->x;y3=v1->y;
x2=v2->x;y2=v2->y;
x1=v3->x;y1=v3->y;
}
else{
x3=v1->x;y3=v1->y;
x2=v3->x;y2=v3->y;
x1=v2->x;y1=v2->y;
}
}
else{
x3=v3->x;y3=v3->y;
x2=v1->x;y2=v1->y;
x1=v2->x;y1=v2->y;
}
}
else{
if (v2->y > v3->y){
if (v1->y>v3->y){
x3=v2->x;y3=v2->y;
x2=v1->x;y2=v1->y;
x1=v3->x;y1=v3->y;
}
else{
x3=v2->x;y3=v2->y;
x2=v3->x;y2=v3->y;
x1=v1->x;y1=v1->y;
}
}
else{
x3=v3->x;y3=v3->y;
x2=v2->x;y2=v2->y;
x1=v1->x;y1=v1->y;
}
}

xd1=x2-x1; //10
yd1=y2-y1; //10
xd2=x3-x1; //15
yd2=y3-y1; //15

40
if(yd1){
for(i=y1;i<=y2;i++){
Lx = x1 + ((i - y1) * xd1) / yd1; // 10+((5-10)*10)/10 = -15
Rx = x1 + ((i - y1) * xd2) / yd2; // 10+((5-10)*15)/15 = 5
if(Lx<Rx){
gr_hline (drawing_graph, 0, Lx, i, Rx-Lx);
}
else{
if(Lx>Rx)
gr_hline (drawing_graph, 0, Rx, i, Lx-Rx);
}
}
}

xd1=x3-x2; // 5
yd1=y3-y2; // 5

if(yd1){
for(i=y2;i<=y3;i++){
Lx = x1 + ((i - y1) * xd2) / yd2; // 10+((24-10)*15)/15 = 24
Rx = x2 + ((i - y2) * xd1) / yd1; // 20+((24-20)*5)/5 = 24
if(Lx<Rx){
gr_hline (drawing_graph, 0, Lx, i, Rx-Lx);
}
else{
if(Lx>Rx)
gr_hline (drawing_graph, 0, Rx, i, Lx-Rx);
}
}
}
return;
}

D I B U J A N D O N U E S T R O O B J E T O 3 D

Para dibujar nuestro objeto tuvimos que haber definido los métodos básicos escenciales
para dibujar nuestro Objeto 3D. El método que se encarga de renderizar nuestro Objeto
3D se llama _Dibuja_Objeto().

void _Dibuja_Objeto(int id,int id_camara){


int x_inc = 0;
int y_inc = 0;
int _x1,_x2,_x3;
int _y1,_y2,_y3;
int x1,x2,x3;
int y1,y2,y3;
int color,i,j,atc;
int ModoVision=objeto[id].ModoVision;
GRAPH *map;
int r,g,b;
int r2,g2,b2;

Luego definimos las variables de renderizado:

//Opciones de dibujado
char FL = 0; //Iluminacion

41
char BFC = 0; //Back Face Culling
char OC = 0; //Ordenado de caras
char DV = 0; //Dibujar vertices
char DA = 0; //Dibujar aristas metidas a mano
char DAC = 0; //Dibujar aristas de las caras
char DC = 0; //Dibujar caras (FILLED)

Y otras variables auxiliares:

double tam=0;
double tam2=0;
double tam3=0;
double *int_at;
double at_aux1;
double at_aux2;
double at_aux3;

Hay que normalizar el ángulo Gamma de la cámara para evitar que se salga de los
ángulos normales entre 0º y 360º:

//Rotación gamma de la camara


int gamma = -camara[id_camara].angulo.gamma+18000;
while(gamma<0)gamma+=36000;
while(gamma>36000)gamma-=36000;

Las opciones de dibujado nos permite decidir cómo vamos a dibujar nuestro objeto, y el
valor máximo permitido es 64 y el mínimo es 1. Por ejemplo, si el Modo de Renderizado
es 64, entonces se establecen las variables DV, DA, DAC, DC, BFC, OC, FL a 1:

//Sacamos las opciones de dibujado


if (ModoVision <= 0 || id<0) return;
if (ModoVision >= 64){FL=1;ModoVision-=64;}
if (ModoVision >= 32){OC=1;ModoVision-=32;}
if (ModoVision >= 16){BFC=1;ModoVision-=16;}
if (ModoVision >= 8){DC=1;ModoVision-=8;}
if (ModoVision >= 4){DAC=1;ModoVision-=4;}
if (ModoVision >= 2){DA=1;ModoVision-=2;}
if (ModoVision >= 1){DV=1;}

Luego sacamos la información del mapa dividiendo el mismo entre dos para poder
colocar nuestro objeto en ese punto. Por ejemplo, si al ejecutar nuestro programa,
definimos la resolución a 320x200, entonces nuestra posición incremental en donde se
dibujará nuestro Objeto 3D sería 160x100:

//Sacamos la información del mapa


map = bitmap_get(camara[id_camara].file, camara[id_camara].graph);
map->width = screen->w;
map->height = screen->h;

x_inc = map->width>>1; //Sacamos incrementos dividiendo entre 2


y_inc = map->height>>1; //el ancho y alto del mapa

42
Rotamos el Objeto 3D con los valores iniciales:

//Calculamos la nueva posicion del objeto


_Rotar_Objeto(id, 0, OC, id_camara); //Rotamos el objeto

Se reescala el objeto en caso tal que es distinto de 1.0 a los Puntos 3D del vértice y a los
Puntos Medios 3D en caso que esté establecido el Ordenamiento de Caras:

//Reescalamos el objeto si es necesario


if (objeto[id].escalado != 1.0){
for (i=0;i<objeto[id].NVertices;i++) {
p3d[i].y *= objeto[id].escalado;
p3d[i].z *= objeto[id].escalado;
p3d[i].x *= objeto[id].escalado;
}
if (OC){
for (i=0;i<objeto[id].NCaras;i++) {
pm3d[i].y *= objeto[id].escalado;
pm3d[i].z *= objeto[id].escalado;
pm3d[i].x *= objeto[id].escalado;
}
}
}

Rotamos la Cámara con los valores iniciales:

_Rotar_Objeto(id, 2, OC, id_camara);

Si un objeto está detrás o demasiado cerca/lejos de la camara no lo dibujamos:

if (pos.z<camara[id_camara].distancia_minima ||
pos.z>camara[id_camara].distancia_maxima) return;

Eliminamos la Cara Posterior en caso de estar activa esta opción, de lo contrario, se


dibuja todas las caras:

if (BFC)
_BFC(id, id_camara);
else {
for (i=0;i<objeto[id].NCaras;i++) caraP[i].cara = i; NcarasP = i;
}

Ordenamos las caras:

if (OC) {_OrdenaCaras(id, id_camara);}

R E N E R I Z A N D O V É R T I C E S

Para ello los puntos X, Y de nuestro Vértice se posiciónan de la siguiente forma:

43
X1 = (X * Distancia de la Cámara) / (Z + IncX)
Y1 = (Y * Distancia de la Cámara) / (Z + IncY)

Por ejemplo, tenemos un vértice con las coordenadas (0,50,0), en una pantalla con una
resolución de 320x200, con IncX = 160 e IncY = 100 y la cámara está a una distancia de
320, tenemos que:

X1 = 0 * 320 / 0 + 160 = 0
Y1 = 50 * 320 / 0 + 100 = 160

Y cuando se dibuja en la pantalla a través de gr_put_pixel:

X = 160 + 0 = 0
Y = 200 – 160 = 40

//Dibujamos los vertices


if (DV) {
for(i=0;i<objeto[id].NVertices;i++){
_x1 = (p3d[i].x)*camara[id_camara].distancia/(p3d[i].z)+x_inc;
_y1 = (p3d[i].y)*camara[id_camara].distancia/(p3d[i].z)+y_inc;
if (gamma-18000 != 0) {
x1 = (_x1-x_inc)*coseno[gamma]-(_y1-y_inc)*seno[gamma]+x_inc;
y1 = (_x1-x_inc)*seno[gamma]+(_y1-y_inc)*coseno[gamma]+y_inc;
color = objeto[id].cara[i].color;
gr_put_pixel(map, x1, map->height-y1, color);
}
else
color = objeto[id].cara[i].color;
gr_put_pixel(map, _x1, map->height-_y1, color);
}
}

R E N E R I Z A N D O A R I S T A S

Se dibujan ahora las aristas que hemos creado…

X1 = (X del Vector 1 * Distancia de la Cámara) / (Z del Vector 1 + IncX)


Y1 = (Y del Vector 1 * Distancia de la Cámara) / (Z del Vector 1 + IncY)
X2 = (X del Vector 2 * Distancia de la Cámara) / (Z del Vector 2 + IncX)
Y2 = (Y del Vector 2 * Distancia de la Cámara) / (Z del Vector 2 + IncY)

Y1, Y2 es restado con el alto de la pantalla:

Y1 = Alto de la Pantalla – Y1
Y2 = Alto de la Pantalla – Y2

Para que al final se dibuje una línea que une los dos vértices formando la arista:

44
if (DA) {
for(i=0;i<objeto[id].NAristas;i++){
_x1 =
(p3d[objeto[id].arista[i].v1].x)*camara[id_camara].distancia/
(p3d[objeto[id].arista[i].v1].z)+x_inc;
_y1 =
(p3d[objeto[id].arista[i].v1].y)*camara[id_camara].distancia/
(p3d[objeto[id].arista[i].v1].z)+y_inc;
_x2 =
(p3d[objeto[id].arista[i].v2].x)*camara[id_camara].distancia/
(p3d[objeto[id].arista[i].v2].z)+x_inc;
_y2 =
(p3d[objeto[id].arista[i].v2].y)*camara[id_camara].distancia/
(p3d[objeto[id].arista[i].v2].z)+y_inc;

_y1 = map->height-_y1;
_y2 = map->height-_y2;

color = objeto[id].arista[i].color;

gr_setcolor (color) ;

if (gamma-18000 != 0) {
x1 = (_x1-x_inc)*coseno[gamma]-(_y1-y_inc)*seno[gamma]+x_inc;
y1 = (_x1-x_inc)*seno[gamma]+(_y1-y_inc)*coseno[gamma]+y_inc;
x2 = (_x2-x_inc)*coseno[gamma]-(_y2-y_inc)*seno[gamma]+x_inc;
y2 = (_x2-x_inc)*seno[gamma]+(_y2-y_inc)*coseno[gamma]+y_inc;
gr_line (map, 0, x1, y1, x2-x1, y2-y1) ;
}
else
gr_line (map, 0, _x1, _y1, _x2-_x1, _y2-_y1) ;
}
}

R E N E R I Z A N D O C A R A S

Ahora, se comienza a dibujar las caras y aristas rellenas:

if (DAC || DC){

En donde procedemos a calcular la distancia que hay entre la luz y el objeto para
realización su respectiva atenuación:

Distancia X = Posición X Luz – Posición X Objeto


Distancia Y = Posición Y Luz – Posición Y Objeto
Distancia Z = Posición Z Luz – Posición Z Objeto

Distancia Total = (Distancia X + Distancia Y + Distancia Z)2

Entonces si la distancia Total es mayor que la distancia máxima, entonces no hay


atencuación. Y si es menor que la distancia mínima, hay atenuación. Y si no cumple
ninguna de las dos condiciones, la atenuación equivale a:

45
Atenuación = 1– ((Distancia Total – Distancia Mínima)*Factor Atenuación)/1000000000

if (FL && NTablasL>objeto[id].FocoLuz){


int_at = (double *)malloc(tabla_l[objeto[id].FocoLuz].Nluces *
sizeof(double));

for (j=0;j<tabla_l[objeto[id].FocoLuz].NLuces;j++){
if (luz[tabla_l[objeto[id].FocoLuz].luz[j]].modo_atenuacion!=1){
int_at[j] = 1;
continue;
}

//Calculamos la distancia al objeto


at_aux1 = (luz[tabla_l[objeto[id].FocoLuz].luz[j]].posicion.x –
objeto[id].posicion.x);
at_aux2= (luz[tabla_l[objeto[id].FocoLuz].luz[j]].posicion.y –
objeto[id].posicion.y);
at_aux3= (luz[tabla_l[objeto[id].FocoLuz].luz[j]].posicion.z –
objeto[id].posicion.z);
at_aux1 = at_aux1*at_aux1 + at_aux2*at_aux2 + at_aux3*at_aux3;

//Calculamos la atenuacion
if (at_aux1 > luz[tabla_l[objeto[id].FocoLuz].luz[j]].distancia_maxima)
int_at[j] = 0;
else if (at_aux1 <
luz[tabla_l[objeto[id].FocoLuz].luz[j]].distancia_minima)
int_at[j] = 1.0;
else int_at[j] = 1-((at_aux1 -
luz[tabla_l[objeto[id].FocoLuz].luz[j]].distancia_minima) *
luz[tabla_l[objeto[id].FocoLuz].luz[j]].fact_at)/1000000000;
}
}

Se comienzan a dibujar las caras con orden:

X1 = (Punto X del Vértice 1 * Distancia de la Cámara ) / (Punto Z del Vértice 1 + X_Inc)


Y1 = (Punto Y del Vértice 1 * Distancia de la Cámara ) / (Punto Z del Vértice 1 + Y_Inc)

X2 = (Punto X del Vértice 2 * Distancia de la Cámara ) / (Punto Z del Vértice 2 + X_Inc)


Y2 = (Punto Y del Vértice 2 * Distancia de la Cámara ) / (Punto Z del Vértice 2 + Y_Inc)

X3 = (Punto X del Vértice 3 * Distancia de la Cámara ) / (Punto Z del Vértice 3 + X_Inc)


Y3 = (Punto Y del Vértice 3 * Distancia de la Cámara ) / (Punto Z del Vértice 3 + Y_Inc)

for(i=NcarasP-1;i>-1;i--){ //Dibujamos las caras por orden


_x1 = (p3d[objeto[id].cara[caraP[i].cara].v1].x) *
camara[id_camara].distancia /
(p3d[objeto[id].cara[caraP[i].cara].v1].z) + x_inc;
_y1 = (p3d[objeto[id].cara[caraP[i].cara].v1].y) *
camara[id_camara].distancia /
(p3d[objeto[id].cara[caraP[i].cara].v1].z) + y_inc;
_x2 = (p3d[objeto[id].cara[caraP[i].cara].v2].x) *
camara[id_camara].distancia /

46
(p3d[objeto[id].cara[caraP[i].cara].v2].z) + x_inc;
_y2 = (p3d[objeto[id].cara[caraP[i].cara].v2].y) *
camara[id_camara].distancia /
(p3d[objeto[id].cara[caraP[i].cara].v2].z) + y_inc;
_x3 = (p3d[objeto[id].cara[caraP[i].cara].v3].x) *
camara[id_camara].distancia /
(p3d[objeto[id].cara[caraP[i].cara].v3].z) + x_inc;
_y3 = (p3d[objeto[id].cara[caraP[i].cara].v3].y) *
camara[id_camara].distancia /
(p3d[objeto[id].cara[caraP[i].cara].v3].z) + y_inc;

R E N E R I Z A N D O L A I L U M I N A C I Ó N

Cuando el Número de tablas de Luz es mayor que el Foco de Luz del Objeto, se
inicializan las variables para el cálculo de intensidades de Luz:

if (FL && NTablasL>objeto[id].FocoLuz){


tam2=atc=r2=g2=b2=0;
gr_get_rgb (objeto[id].cara[caraP[i].cara].color, &r, &g, &b);

Ahora se calcula el modo con que se interpretarán las luces en la Tabla de Luz.

switch (tabla_l[objeto[id].FocoLuz].modo) {
case -1: break;

Sólo hay dos modos posibles.

M O D O D E I N T E N S I D A D A D I T I V O

El modo 0 (modo aditivo, que viene por defecto), en el que para cada cara se suman las
intensidades de los distintos focos para obtener una intensidad total, éste modo
seguramente es físicamente más correcto, pero queda peor al representarlo.

case 0://Suma de intensidades


for (j=0;j<tabla_l[objeto[id].FocoLuz].NLuces;j++){
tam = l3d[caraP[i].cara].x *
luz[tabla_l[objeto[id].FocoLuz].luz[j]].objetivo.x +
l3d[caraP[i].car a].y *
luz[tabla_l[objeto[id].FocoLuz].luz[j]].objetivo.y +
l3d[caraP[i].cara].z *
luz[tabla_l[objeto[id].FocoLuz].luz[j]].objetivo.z;

if (tam<0) continue;

M O D O D E A T E N U A C I Ó N PA R A E L M O D O D E I N T E NS I D A D
A D I T I V O

Se calcula entonces el modo de atenuación. El modo puede ser 0, 1 ó 2.

switch (luz[tabla_l[objeto[id].FocoLuz].luz[j]].modo_atenuacion){

47
En modo 0 no hay atenuación y es el modo por defecto, en donde el tamaño de la lúz es
equivalente a la suma de la intensidad de la luz por el tamaño de la intensidad total:

case 0:
tam2 += luz[tabla_l[objeto[id].FocoLuz].luz[j]].intensidad * tam;
break;

El modo 1 es atenuación respecto al objeto completo, es decir, que la distancia se mide


respecto al objeto y todas sus caras se atenúan en el mismo grado.

case 1:
if (int_at[j]==0) ;//tam2=tam2;
else if (int_at[j]==1)
tam2 += luz[tabla_l[objeto[id].FocoLuz].luz[j]].intensidad*tam;
else
tam2 += luz[tabla_l[objeto[id].FocoLuz].luz[j]].intensidad *
int_at[j]*tam;
break;

El modo 2 es atenuación respecto a las caras de un objeto, éste sirve para por ejemplo
hacer una tabla con muchas caras y ponerle un foco atenuado, de forma que se vea un
circulo de luz que se va difuminando (éste método es bastante lento)

case 2:
//Calculamos la distancia a la cara
at_aux1 = (luz[tabla_l[objeto[id].FocoLuz].luz[j]].posicion.x –
objeto[id].cara[caraP[i].cara].pmed.x);
at_aux2 = (luz[tabla_l[objeto[id].FocoLuz].luz[j]].posicion.y –
objeto[id].cara[caraP[i].cara].pmed.y);
at_aux3 = (luz[tabla_l[objeto[id].FocoLuz].luz[j]].posicion.z –
objeto[id].cara[caraP[i].cara].pmed.z);
at_aux1 = at_aux1*at_aux1 + at_aux2*at_aux2 + at_aux3*at_aux3;

//Calculamos la atenuacion
if (at_aux1 > luz[tabla_l[objeto[id].FocoLuz].luz[j]].distancia_maxima)
int_at[j] = 0;
else if (at_aux1 <
luz[tabla_l[objeto[id].FocoLuz].luz[j]].distancia_minima) int_at[j] = 1.0;
else int_at[j] = 1-((at_aux1 -
luz[tabla_l[objeto[id].FocoLuz].luz[j]].distancia_minima) *
luz[tabla_l[objeto[id].FocoLuz].luz[j]].fact_at) / 1000000000;

if (int_at[j]==0) ;//tam2=tam2;
else if (int_at[j]==1) tam2 +=
luz[tabla_l[objeto[id].FocoLuz].luz[j]].intensidad * tam;
else tam2 += luz[tabla_l[objeto[id].FocoLuz].luz[j]].intensidad *
int_at[j]*tam;
break;
}
}

Y a través del tamaño de la atenuación de la luz podemos calcular las intensidades de los
colores RGB:

48
if (tam2>100) tam2=100;

tam2/=100;
r=(255-r)*tam2+r;
g=(255-g)*tam2+g;
b=(255-b)*tam2+b;
break;

M O D O D E I N T E N S I D A D C O M P A R A T I V O

Y el modo 1 (modo comparativo), en él, para cada cara se calcula la intensidad que
produce cada foco de luz y sólo nos quedamos con la mayor, éste modo, en mi opinión,
da resultados más realistas.

case 1://Comparacion de intensidades


for (j=0;j<tabla_l[objeto[id].FocoLuz].NLuces;j++){
tam = l3d[caraP[i].cara].x *
luz[tabla_l[objeto[id].FocoLuz].luz[j]].objetivo.x +
l3d[caraP[i].cara].y *
luz[tabla_l[objeto[id].FocoLuz].luz[j]].objetivo.y +
l3d[caraP[i].cara].z *
luz[tabla_l[objeto[id].FocoLuz].luz[j]].objetivo.z;

if (tam<0) continue;

M O D O D E A T E N U A C I Ó N PA R A E L M O D O D E I N T E NS I D A D
C O M P A R A T I V O

Luego se calcula la Atenuación de la Luz en el modo Comparativo. La intensidad será un


valor comprendido entre 0 y 100, si el valor se sale de éste rango (es tarea del
programador evitar esto) los colores tendrán valores inesperados. Una intensidad 0
implica que el color de las caras será el color base que se les asignó en cualquier caso, un
valor 100 significa que cuando una cara está a 90º respecto al vector unitario del foco,
ésta cara se verá completamente blanca (iluminación máxima)

En modo 0 no hay atenuación y es el modo por defecto:

switch (luz[tabla_l[objeto[id].FocoLuz].luz[j]].modo_atenuacion){
case 0:
r2=(((255-r) * luz[tabla_l[objeto[id].FocoLuz].luz[j]].intensidad) /
100) * tam+r;
g2=(((255-g) * luz[tabla_l[objeto[id].FocoLuz].luz[j]].intensidad) /
100) * tam+g;
b2=(((255-b) * luz[tabla_l[objeto[id].FocoLuz].luz[j]].intensidad)/100)
* tam+b;

//Guardamos solo el de máxima intensidad


if (r2>r && g2>g && b2>b){
r=r2;
g=g2;
b=b2;

49
}
break;

El modo 1 es atenuación respecto al objeto completo, es decir, que la distancia se mide


respecto al objeto y todas sus caras se atenúan en el mismo grado.

case 1:
if (int_at[j] == 0) {
r2 = r;
g2 = g;
b2 = b;
}
else if(int_at[j] == 1){
r2=(((255-r) * luz[tabla_l[objeto[id].FocoLuz].luz[j]].intensidad) /
100) * tam+r;
g2=(((255-g) * luz[tabla_l[objeto[id].FocoLuz].luz[j]].intensidad) /
100) * tam+g;
b2=(((255-b) * luz[tabla_l[objeto[id].FocoLuz].luz[j]].intensidad) /
100) * tam+b;
}
else {
r2=(((255-r) *
luz[tabla_l[objeto[id].FocoLuz].luz[j]].intensidad*int_at[j])/100)*tam+r;
g2=(((255-g) *
luz[tabla_l[objeto[id].FocoLuz].luz[j]].intensidad*int_at[j])/100)*tam+g;
b2=(((255-b) *
luz[tabla_l[objeto[id].FocoLuz].luz[j]].intensidad*int_at[j])/100)*tam+b;
}

//Guardamos solo el de máxima intensidad


if (r2>r && g2>g && b2>b){
r=r2;
g=g2;
b=b2;
}
break;

El modo 2 es atenuación respecto a las caras de un objeto, éste sirve para por ejemplo
hacer una tabla con muchas caras y ponerle un foco atenuado, de forma que se vea un
circulo de luz que se va difuminando (éste método es bastante lento)

case 2:
//Calculamos la distancia a la cara
at_aux1 = (luz[tabla_l[objeto[id].FocoLuz].luz[j]].posicion.x –
objeto[id].cara[caraP[i].cara].pmed.x);
at_aux2 = (luz[tabla_l[objeto[id].FocoLuz].luz[j]].posicion.y –
objeto[id].cara[caraP[i].cara].pmed.y);
at_aux3 = (luz[tabla_l[objeto[id].FocoLuz].luz[j]].posicion.z –
objeto[id].cara[caraP[i].cara].pmed.z);
at_aux1 = at_aux1*at_aux1 + at_aux2*at_aux2 + at_aux3*at_aux3;

//Calculamos la atenuacion
if (at_aux1 > luz[tabla_l[objeto[id].FocoLuz].luz[j]].distancia_maxima)
int_at[j] = 0;
else if (at_aux1 <
luz[tabla_l[objeto[id].FocoLuz].luz[j]].distancia_minima)

50
int_at[j] = 1.0;
else int_at[j] = 1-((at_aux1 -
luz[tabla_l[objeto[id].FocoLuz].luz[j]].distancia_minima) *
luz[tabla_l[objeto[id].FocoLuz].luz[j]].fact_at) / 1000000000;

if (int_at[j] == 0) {
r2 = r;
g2 = g;
b2 = b;
}
else if(int_at[j] == 1){
r2=(((255-r) *
luz[tabla_l[objeto[id].FocoLuz].luz[j]].intensidad) /
100) * tam + r;
g2=(((255-g) *
luz[tabla_l[objeto[id].FocoLuz].luz[j]].intensidad) /
100) * tam + g;
b2=(((255-b) *
luz[tabla_l[objeto[id].FocoLuz].luz[j]].intensidad) /
100) * tam + b;
}
else {
r2=(((255-r) *
luz[tabla_l[objeto[id].FocoLuz].luz[j]].intensidad*int_at[j])
/ 100)*tam+r;
g2=(((255-g) *
luz[tabla_l[objeto[id].FocoLuz].luz[j]].intensidad*int_at[j])
/ 100)*tam+g;
b2=(((255-b) *
luz[tabla_l[objeto[id].FocoLuz].luz[j]].intensidad*int_at[j])
/ 100)*tam+b;
}

//Guardamos solo el de máxima intensidad


if (r2>r && g2>g && b2>b){
r=r2;
g=g2;
b=b2;
}
break;
}
}
}

Una vez que se hayan calculado las intensidades de luz, se procede a calcular el nuevo
color, dependiendo del modo de video que haya seleccionado, ya que si se trara de un
modo de 16 bits de color, se especifica la misma a través del valor RGB. De lo contrario,
se busca el color más cercano al que se pide…

//Calculamos un nuevo color en función del ángulo


if (op_luz_16)
color = gr_rgb(r, g, b);
else
color = gr_find_nearest_color(r, g, b);

51
Pero ahora si el Número de Tablas de Luz es menor que la intensidad del Foco de Luz,
entonces el color es equivalente al que se asignaron en la cara:

}
else
color = objeto[id].cara[caraP[i].cara].color;

Ahora se especifica el color que vamos a utilizar para rellenar la cara:

gr_setcolor (color) ;

Una vez hecho esto, se procede a convertir los puntos 3D de las caras a puntos 2D para
que a través del método _Dibujar_Triangulo se puedan dibujar de manera correcta en
la pantalla 2D:

//Caras rellenadas
if (DC){
if (gamma-18000 != 0) {
x1 = (_x1-x_inc)*coseno[gamma]-(_y1-y_inc)*seno[gamma]+x_inc;
y1 = (_x1-x_inc)*seno[gamma]+(_y1-y_inc)*coseno[gamma]+y_inc;

x2 = (_x2-x_inc)*coseno[gamma]-(_y2-y_inc)*seno[gamma]+x_inc;
y2 = (_x2-x_inc)*seno[gamma]+(_y2-y_inc)*coseno[gamma]+y_inc;

x3 = (_x3-x_inc)*coseno[gamma]-(_y3-y_inc)*seno[gamma]+x_inc;
y3 = (_x3-x_inc)*seno[gamma]+(_y3-y_inc)*coseno[gamma]+y_inc;

p2d[0].x=x1;p2d[0].y=map->height-y1;
p2d[1].x=x2;p2d[1].y=map->height-y2;
p2d[2].x=x3;p2d[2].y=map->height-y3;
}
else{
p2d[0].x=_x1;p2d[0].y=map->height-_y1;
p2d[1].x=_x2;p2d[1].y=map->height-_y2;
p2d[2].x=_x3;p2d[2].y=map->height-_y3;
}

_Dibuja_Triangulo (map, &p2d[0], &p2d[1], &p2d[2]);


}

Y por último se dibujan las aristas en caso de que no se dibuje con relleno el triángulo:

//Aristas
else{
_y1 = map->height-_y1;
_y2 = map->height-_y2;
_y3 = map->height-_y3;

if (gamma-18000 != 0) {
x1 = (_x1-x_inc) * coseno[gamma] –
(_y1-y_inc) * seno[gamma] + x_inc;
y1 = (_x1-x_inc) * seno[gamma] +
(_y1-y_inc) * coseno[gamma] + y_inc;
x2 = (_x2-x_inc) * coseno[gamma] –

52
(_y2-y_inc) * seno[gamma] + x_inc;
y2 = (_x2-x_inc) * seno[gamma] +
(_y2-y_inc) * coseno[gamma] + y_inc;
x3 = (_x3-x_inc) * coseno[gamma] –
(_y3-y_inc) * seno[gamma] + x_inc;
y3 = (_x3-x_inc) * seno[gamma] +
(_y3-y_inc) * coseno[gamma] + y_inc;

gr_line (map, 0, x1, y1, x2-x1, y2-y1) ;


gr_line (map, 0, x2, y2, x3-x2, y3-y2) ;
gr_line (map, 0, x3, y3, x1-x3, y1-y3) ;
}
else{
gr_line (map, 0, _x1, _y1, _x2-_x1, _y2-_y1) ;
gr_line (map, 0, _x2, _y2, _x3-_x2, _y3-_y2) ;
gr_line (map, 0, _x3, _y3, _x1-_x3, _y1-_y3) ;
}
}
}
}
}

Bueno, hasta aquí podemos renderizar de forma normal nuestro objeto 3d a la pantalla.
Y para implementar este llamado de función en nuestro lenguaje de programación Fenix,
es necesario crear una función de referencia llamada ETD_Dibuja_Objeto:

static int ETD_Dibuja_Objeto (INSTANCE * my, int * params) {


_Dibuja_Objeto(params[0],params[1]);
return 0;
}

**OJO** Este tipo de función es usado solamente cuando está dentro de un bucle
infinito o un bule con condición (Loop, While, Until y For).

E X P O R T A N D O N U E S T R A S F U N C I O N E S

Y Como último paso para implementar nuestro motor 3D en nuesros juegos hechos en
Fenix debemos crear una cabecera principal para la DLL en donde debemos de exportar
nuestras funciones que hemos creado a lo largo de los capítulos 2 y 3:

FENIX_MainDLL RegisterFunctions (COMMON_PARAMS)


{
FENIX_DLLImport

//FUNCIONES generales
FENIX_export("ETD_DRAW_OBJECT","II",TYPE_DWORD, ETD_Dibuja_Objeto);
FENIX_export("ETD_START","PP",TYPE_DWORD, ETD_init);

//Funciones de cámaras
FENIX_export("ETD_NEW_CAMERA","",TYPE_DWORD, ETD_Nueva_Camara);
FENIX_export("ETD_DELETE_CAMERA","I",TYPE_DWORD, ETD_Elimina_Camara);
FENIX_export("ETD_ADVANCE_CAMERA","IIII",TYPE_DWORD, ETD_Avanza_Camara);
FENIX_export("ETD_TARGET_CAMERA","IIII",TYPE_DWORD, ETD_Dirige_Camara);

53
//Funciones de objetos y ZTables
FENIX_export("ETD _NEW_VERTEX","IIII",TYPE_DWORD, ETD_Nuevo_Vertice);
FENIX_export ("ETD _NEW_EDGE","IIII",TYPE_DWORD, ETD_Nueva_Arista);
FENIX_export("ETD _NEW_FACE","IIIII",TYPE_DWORD, ETD_Nueva_Cara);
FENIX_export("ETD_NEW_OBJECT","",TYPE_DWORD, ETD_Nuevo_Objeto);
FENIX_export("ETD _DELETE_OBJECT","I",TYPE_DWORD, ETD_Elimina_Objeto);
FENIX_export ("ETD _ROTATE_OBJECT","I",TYPE_DWORD, ETD_Rotate_Object);
}

Nada más es cuestión de compilar nuestro código fuente a una DLL, pero el nombre de
nuestra DLL para el proyecto se debe llamar ETD.DLL

P R O B A N D O N U E S T R O M O T OR 3D

Ahora es momento de implementar el Motor 3D al lenguaje Fénix. Y para ello debemos


de crear un nuevo archivo .PRG llamado Ejemplo1.prg y escribimos el siguiente
código:

include "ETD.inc"; //Tipos de variables

Global
_ETD_3DOBJECT pointer objeto; //Puntero que controlará el array de objetos
_ETD_3DCAMERA pointer camara; //Puntero que controlará el array de cámaras
id_objeto; //ID de el objeto

import "ETD.dll"

private
int v1,v2,v3;//IDs de los 3 vertices
int c1;//ID de la cara

begin
full_screen = false;
set_fps(60,0);
set_mode(320,200,16);
write_int(0,0,0,0,&fps);
frame;

//Iniciamos el Motor 3D
etd_start(&objeto,&camara);

//Creamos el objeto
id_objeto = etd_new_object( );

//Ahora creamos los 3 vértices


v1 = etd_new_vertex ( id_objeto, 0, 50, 0 );
v2 = etd_new_vertex ( id_objeto, -50, -50, 0 );
v3 = etd_new_vertex ( id_objeto, 50, -50, 0);

//Ahora creamos la cara


c1 = etd_new_face ( id_objeto, v1, v2, v3, rgb(255,45,56) );

//Movemos el objeto
objeto[id_objeto].z = 200;

//Quitamos el ordenamiento de caras y el BFC

54
objeto[id_objeto].RenderMode = 8;

//Bucle general de redibujado


loop
//Rotamos el objeto
objeto[id_objeto].alfa += 100;

//Borramos el mapa y lo dibujamos


map_clea r ( 0, 0, 0 );
etd _draw_object ( id_objeto, 0 );

//Permitimos la salida
if (key(_esc)) exit(0,0); end
frame;
end
end

Ahora es momento de ejecutar nuestro código fuente:

¿Qué hemos hecho?

Para empezar, hemos incluído el archivo ETD.INC visto en el Capítulo 1 para


incorporar todas las estructuras de datos de nuestros elementos del Motor 3D, es decir el
Objeto 3D, la Cámara 3D, el Punto 3D, la Arista 3D y la Cara 3D. Luego definimos
como datos globales el puntero objeto y al igual que camara para almacenar el conjunto
de Objetos 3D y de Cámara 3D que se implementarán en nuestro motor a lo largo de
nuestro programa.

Definimos una variable entera llamada id_objeto que es el que vamos a identificar al
Objeto 3D en nuestra pantalla y lo vamos a usar para dibujar nuestros objetos y para ser
manipulados por la cámara.

Seguido, inicializamos el Motor 3D a través de la función exportada etd_start en donde


los argumentos deben ser los OFFSETS de los punteros que declaramos antes. Ésta

55
función genera las tablas precalculadas de senos y cosenos, dirige los punteros internos
de ETD y crea una cámara por defecto (la 0).

Así pues, para hacer un triángulo o un mundo entero, éste debe ser un objeto, por lo
tanto vamos a comenzar creando un objeto Y de ahí creamos el primer objeto a través
del método etd_new_object que nos devuelve el código identificador (ID a partir de
ahora) del nuevo objeto creado.

A través del método etd_new_vertex, creamos los 3 vértices, para unirlos forma ndo
una cara a través de etd_new_face. El último parámetro de ésta función es el color de la
cara, y dependerá según estemos en modo 8bits o 16bits.

Una vez creado el objeto, lo alejamos un poco de la cámara con respecto al eje de las Z,
y el modo de renderizado (RenderMode) es 8, es decir que se dibuja la cara rellena con
las aristas.

Dentro del bucle infinito rotamos el objeto 1 grado (100 centésimas) con respecto al eje
de las X.

Hay que borrar el mapa antes de dibujar el Objeto 3D en él, por lo que hay que hacerlo a
mano usando la instrucción de Fénix map_clear, en donde los argumentos file y graph
hay que poner los file y graph de la cámara, y el color lo normal es que sea 0 para borrar
el mapa.

Y por último verás que al final dentro del bucle infinito se implementó el método
etd_draw_object para actualizar las nuevas coordenadas de los vértices en nuestro
Objeto 3d, y es por eso que vemos nuestro objeto moviéndose una y otra véz y así
sucesivamente. Esta función necesita un ID de objeto y otro de cámara, en el de cámara
por ahora pondremos 0, que es la cámara que se crea por defecto y está en 0,0,0
apuntando a lo largo del eje z.

El siguiente ejemplo básico consiste en utilizar varias cámaras en vez de una.

include "etd.inc"; //Tipos de variables

Global
_ETD_3DOBJECT pointer objeto; //Puntero que controlará el array de objetos
_ETD_3DCAMERA pointer camara; //Puntero que controlará el array de cámaras
id_objeto; //ID de el objeto
id_camara[3];
import "ETD.dll"

private
int v1,v2,v3;//IDs de los 3 vertices
int c1;//ID de la cara
i;
begin
full_screen = false;
set_fps(60,0); //Limitamos a 60 fps porke va demasiado rapido

56
write_int(0,0,0,0,&fps); //Chivato
frame;
//Iniciamos ETD
etd_start(&objeto,&camara);

//Creamos el objeto
id_objeto = etd_new_object( );

//Ahora creamos los 3 vértices


v1 = etd_new_vertex ( id_objeto, 0, 50, 0 );
v2 = etd_new_vertex ( id_objeto, -50, -50, 0 );
v3 = etd_new_vertex ( id_objeto, 50, -50, 0);

//Ahora creamos la cara


c1 = etd_new_face ( id_objeto, v1, v2, v3, rgb(45,24,240) );

//Movemos el objeto
objeto[id_objeto].z = 200;

//Quitamos el ordenamiento de caras y el BFC


objeto[id_objeto].RenderMode = 8;

//Creamos las 3 cámaras


from i=1 to 3; id_camara[i] = etd_new_camera ( ); end

//Empezamos por la 1 porque la 0 se va a quedar donde está


camara[id_camara[1]].x = -50;
camara[id_camara[1]].y = -50;
camara[id_camara[1]].z = -50;
etd_target_camera ( id_camara[1],
objeto[id_objeto].x,
objeto[id_objeto].y,
objeto[id_objeto].z ); //Apuntamos al triángulo

//Ajustamos la segunda cámara


camara[id_camara[2]].x = 50;
camara[id_camara[2]].y = 50;
camara[id_camara[2]].z = 50;
etd_target_camera ( id_camara[2],
objeto[id_objeto].x,
objeto[id_objeto].y,
objeto[id_objeto].z ); //Apuntamos al triángulo

//En esta cámara modificaremos su variable


//al observador para observar las diferencias en el render
camara[id_camara[3]].distance = 50;

i=0;

//Bucle general de redibujado


loop
//Elegimos las distintas cámaras con los F's
if (key(_f1)) i=0; end
if (key(_f2)) i=1; end
if (key(_f3)) i=2; end
if (key(_f4)) i=3; end

objeto[id_objeto].beta += 100;

57
if (key(_e))
camara[id_camara[i]].alfa += 50;
end
if (key(_r))
camara[id_camara[i]].alfa -= 50;
end

if (key(_right))
camara[id_camara[i]].beta += 50;
end
if (key(_left))
camara[id_camara[i]].beta -= 50;
end

if (key(_up))
camara[id_camara[i]].z += 50;
end
if (key(_down))
camara[id_camara[i]].z -= 50;
end

if (key(_c))
camara[id_camara[i]].gamma += 50;
end
if (key(_v))
camara[id_camara[i]].gamma -= 50;
end

//Borramos el mapa y lo dibujamos


map_clear ( 0, 0, 0 );
etd_draw_object ( id_objeto, id_camara[i] );

//Permitimos la salida
if (key(_esc)) exit(0,0); end
frame;
end
end

Ahora al comenzar a ejecutar el programa verás que se inicializa el Motor 3D con la


cámara 0 en la posición 0,0,0 y apuntando hacia el eje z positivo (por hacerlo más similar
a Fénix), pero para apuntar la cámara lo que se hace es girarla, es decir, usar sus variables
alfa, beta gamma. Esto complica bastante si queremos apuntar a algún sitio en específico,
por lo que también hay un función que se encarga de apuntar a ese objeto,
etd_target_camera, que es muy útil y sencilla de utilizar.

Las teclas para manejar el objeto dentro de la cámara son:

Abajo – Alejamos el Objeto.


Arriba – Acercamos el Objeto.
Izquierda y Derecha – Rotamos el Objeto con respecto al Eje de las Y
E y R – Rotamos el Objeto con respecto al Eje de las X.
C y V – Rotamos el Objeto con respecto al Eje de las Z.

58
Y para acceder a cada una de las cámaras hay que presionar F1, F2, F3 y F4:

Ejemplo con Cámara 0 (F1)

Ejemplo con Cámara 1 (F2)

Ejemplo con Cámara 2 (F3)

59
Ejemplo con Cámara 3 (F4)

El tercer y último ejemplo que vamos a explicar consiste en un cubo que gira
constantemente y que lo podemos alejarlo y acercarlo a través de las coordenadas Z al
igual que lo podemos mover con respecto a las coordenadas X, Y del Objeto 3D.

include "etd.inc"; //Tipos de variables

Global
_ETD_3DOBJECT pointer objeto; //Puntero que controlará el array de objetos
_ETD_3DCAMERA pointer camara; //Puntero que controlará el array de cámaras
id_cubo; //ID de el objeto

import "ETD.dll"
begin
full_screen = false;
set_fps(60,0); //Limitamos a 60 fps porque va demasiado rapido
write_int(0,0,0,0,&fps);
frame;

//Iniciamos el Motor
etd_start(&objeto,&camara);

//Creamos un objetos
id_cubo = etd_new_object();
etd_new_vertex(id_cubo,-10,-10,-10);
etd_new_vertex(id_cubo,10,-10,-10);
etd_new_vertex(id_cubo,10,-10,10);
etd_new_vertex(id_cubo,-10,-10,10);
etd_new_vertex(id_cubo,-10,10,-10);
etd_new_vertex(id_cubo,10,10,-10);
etd_new_vertex(id_cubo,10,10,10);
etd_new_vertex(id_cubo,-10,10,10);
etd_new_face(id_cubo, 3, 2, 1, rgb(45,35,200));
etd_new_face(id_cubo, 3, 1, 0, rgb(200,35,45));
etd_new_face(id_cubo, 4, 5, 6, rgb(60,200,35));
etd_new_face(id_cubo, 4, 6, 7, rgb(200,240,60));
etd_new_face(id_cubo, 4, 7, 3, rgb(20,200,60));
etd_new_face(id_cubo, 4, 3, 0, rgb(100,200,60));
etd_new_face(id_cubo, 5, 4, 0, rgb(40,240,200));
etd_new_face(id_cubo, 5, 0, 1, rgb(140,60,140));

60
etd_new_face(id_cubo, 6, 5, 1, rgb(190,60,25));
etd_new_face(id_cubo, 6, 1, 2, rgb(250,60,230));
etd_new_face(id_cubo, 7, 6, 2, rgb(230,110,25));
etd_new_face(id_cubo, 7, 2, 3, rgb(250,60,25));

//Cambiamos su posicion
objeto[id_cubo].z = 100;

write(0,160,190,4,"US AR CURSORES, A Y Z");


loop
//Manejamos el objeto con los cursores y "a"-"z"
if (key(_left)) objeto[id_cubo].x--; end
if (key(_right)) objeto[id_cubo].x++; end
if (key(_up)) objeto[id_cubo].y++; end
if (key( _down)) objeto[id_cubo].y--; end
if (key(_a)) objeto[id_cubo].z++; end
if (key(_z)) objeto[id_cubo].z--; end

if (key(_1)) objeto[id_cubo].RenderMode = 1; end


if (key(_2)) objeto[id_cubo].RenderMode = 2; end
if (key(_3)) objeto[id_cubo].RenderMode = 4; end
if (key(_4)) objeto[id_cubo].RenderMode = 8; end
if (key(_5)) objeto[id_cubo].RenderMode = 16; end
if (key(_6)) objeto[id_cubo].RenderMode = 32; end
if (key(_7)) objeto[id_cubo].RenderMode = 56; end

//Rotamos el objeto
objeto[id_cubo].alfa += 200;
objeto[id_cubo].beta += 300;
objeto[id_cubo].gamma -= 200;

//Dibujamos el objeto con la camara 0


map_clear(0,graph,0);
etd _draw_object ( id_cubo, 0 );
if (key(_esc)) break; end
frame;
end
end

Al ejecutar el programa, veremos el cubo creado, y las teclas de este programa son:

1 - Sólo dibuja los vértices del objeto


2 - Sólo dibuja las aristas independientes del objeto (las introducidas con
etd_new_edge )
3 - Sólo se dibujan las aristas de las caras (caras sin relleno)
4 - Sólo se dibujan las caras rellenas
5 - Hace BFC con éste objeto
6 - Ordena las caras por profundidad antes de dibujarlas
7 - El objeto es afectado por focos de luz

A – Alejamos el Objeto
Z – Acercamos el Objeto
Arriba y Abajo – Se desplaza el objeto con respecto a las coordenadas Y.
Izquierda y Derecha – Se desplaza el objeto con respecto a las coordenadas X.

61
62
2
Parte

Fundamentos de
Voxels 3D

63
4
Capítulo

Voxel
I N T R O D U C C I Ó N

En este capítulo hablaremos sobre los voxeles. La palabra voxel viene de volumetric pixel
(pixel volumétrico). Idealmente, un voxel tiene forma cúbica (igual que un pixel tiene
forma cuadrada). En la siguiente figura veremos cómo se representa un voxel en pantalla
a partir de un plano 2D. El primero, de color verde, posee en sí una altura de 1 pixel, en
cambio el segundo, de color rojo, posee en sí una altura de 2 pixeles, notando como
diferencia sus volúmenes. Más adelante veremos cómo se implementa la misma con la
librería VSE:

Sin embargo, representar una escena de voxels con su forma exacta (cada voxel como un
cubo, con sus 6 lados) normalmente requiere mucho tiempo de proceso y resulta
ineficiente para dibujar escenas en tiempo real, por lo que la gran mayoría de juegos
simplifican el algoritmo representando cada voxel como un cuadrado. Se gana velocidad
y se pierde fidelidad a la forma original, pero no por ello empeora la imagen final, ya que

64
si usamos cubos en vez de cuadrados los objetos dan la sensación de estar hechos con
bloques de Lego.

El uso de voxels se hizo popular en la segunda mitad de los años 90 gracias a juegos de
Novalogic como Comanche 3 y la serie Delta Force. Sin embargo, una vez se extendió el
uso de tarjetas aceleradoras 3D, que mejoraban el rendimiento y la calidad de los
polígonos pero no de los voxels, hizo que su uso cayera en picado en el mundo de los
videojuegos comerciales.

P A I S A J E S D E V O X E L S

La principal aplicación de los voxels en el mundo de los videojuegos ha sido la


representación de terrenos y paisajes (voxelscapes). La gran ventaja de los voxels en este
terreno (al menos en la época en la que eran utilizados) es que permitían un gran nivel de
detalle y distancia de visión, lo cual permitía mostrar paisajes muy realistas (a veces
incluso con efectos como hierba, agua transparente, deformación del terreno en tiempo
real, etc).

En el caso de los paisajes los voxels no se suelen dibujar como cuadrados sino como
barras verticales, obteniendo la altura de cada "barra" a partir de un mapa de alturas y su
correspondiente color a partir de la textura. Esto permite simplificar mucho el algoritmo
y disminuir el consumo de memoria, a cambio de dos pequeños sacrificios:

• No pueden existir dos alturas de terreno en el mismo punto, es decir, no se


pueden representar "techos", cuevas o formaciones rocosas complicadas. Una
solución a esto sería completar el terreno con modelos de voxels o bien
poligonales, como se hizo en Battlezone (1998).
• No se puede balancear la cámara (rotarla sobre su eje Z), es decir, el horizonte
debe ser siempre horizontal (valga la redundancia). Además, la caída o rotación
de la misma sobre el eje X sólo se podía hacer dentro de ciertos ángulos. Esto
limitaba bastante el uso de voxelscapes a la hora de hacer simuladores de vuelo.

M O D E L O S D E V O X E L S

Aunque se han usado bastante poco, hay juegos muy conocidos que utilizaron modelos
3D representados mediante voxels, como Shadow Warrior, Blood, Blade Runner o Red
Alert 2. En una época en la que los modelos poligonales no siempre daban el realismo o
el rendimiento deseados, los modelos de voxels permitían solucionar el problema de
mostrar objetos 3D con formas complejas o en gran número. La desventaja es que a
veces daban la sensación de ser sprites con un desagradable efecto de pixelado.

65
C O M P A R A N D O L O S V O X E L S C O N L O S P O L Í G O N O S

Mucho se ha discutido a la hora de comparar ambas tecnologías. Lo cierto es que cada


una tiene sus ventajas y sus inconvenientes, si bien la industria de las tarjetas gráficas
finalmente inclinó la balanza a favor de los polígonos.

Ventajas
o Una mayor resolución de pantalla disminuye el rendimiento, pero no un mayor
nivel de detalle. El algoritmo para representar los voxels funciona a igual
velocidad si usamos 100 voxels que si usamos 10000. Esta es la principal ventaja
a la hora de representar paisajes: no son necesarios mecanismos de LOD para
dibujar las formas lejanas, ya que el número de pixels que va a ocupar la imagen
en pantalla es el mismo. Simplemente dibuja el voxel que "cae" en cada pixel.
o En su época, permitían mejorar mucho ciertos efectos tales como hierba, agua,
sombras, ruido Perlin, etc. Posiblemente el juego que más explotó estos efectos
fue Outcast.
o La libertad a la hora de diseñar formas con voxels permite hacer modelos que
finalmente tienen el acabado suave de los sprites tradicionales, y no muestran
superficies anormalmente planas o vértices afilados.

Desventajas

Aparte de las arriba mencionadas para los paisajes de v oxels, existen otras:

o El inconveniente más criticado de los voxels es el efecto de pixelado que


muestran a corta distancia. En los paisajes se puede suavizar el efecto mediante
interpolación bilineal, tal como hace Outcast. En los modelos, en cambio, el
efecto es difícil de ocultar.
o A larga distancia se pueden "perder detalles". Por ejemplo, si en un paisaje hay
un pico muy pronunciado, al verlo de lejos ese pico puede "caer" entre dos
columnas de pixels, con lo cual no se dibuja y da la sensación de que
"desaparece". Normalmente esto se evita haciendo que los paisajes tengan
formas suaves, pero si realmente son necesarias formas pronunciadas se puede
recurrir al oversampling. En los modelos, en cambio, puede ser necesario recurrir
al mipmapping (generando modelos de escala reducida para ser usados a mayor
distancia).
o El consumo de memoria. Por lo general un voxelscape no consume más memoria
que una simple textura un poco grande (y a pesar de ello se puede conseguir
mucha calidad a corta distancia si se usa ruido Perlin y texturas de detalle), pero
los modelos voxel o los grandes escenarios de voxels al estilo Voxlap requieren
ciertos algoritmos de compresión para no consumir gigas de memoria. El tema
se complica, además, si se quiere que el terreno sea modificable en tiempo real.

66
I N I C I A L I Z A N D O E L M O T O R D E V O X E L

Habíamos dicho anteriormente que usaríamos VSE para desarrollar nuestro mundo en
3ra Dimensión, ya que a través de él posee esa característica peculiar de usar un modo 7
con un efecto realista de pixeles voluminosos. Esta librería tiene como nombre de
código fuente VSE.C que es en donde se lleva la mayor parte de la creación o
inicialización y renderización del VoxelSpace que es lo que vamos a estudiar a
continuación.

Primero tenemos que definir varias variables y estructuras para representar el Voxel en
pantalla y estas son los valores FIXP_SHIFT, FIXP_MUL, PIE, y DPIE que se
usarán más adelante. Esto se debe hacer debido a que realmente al definir tipos de datos
de esas magnitudes, no es necesario declararlo como una variable, sino que debemos de
definirlo como contantes dándole un valor “específico” y así no se consume tiempo de
CPU para realizar los cálculos de dibujo del Voxel.

Tenemos también los UCHAR para representar valores de carateres sin signo con un
valor de 256 bytes, y al igual que USHRT para representar valores cortos sin signo con
un valor de 65535 bytes, y luego está la estructura _color8 para representar la tabla de
colores RGB en valores enteros:

/* Definiciones para Fixed Point*/


#define FIXP_SHIFT 12 // 20.12
#define FIXP_MUL 4096 // 2^12, para convertir reales

/* Definiciones para ángulos*/


#define PIE 3.14159265358979323846 // El que usa Fénix
#define DPIE 6.28318530717958647692 //Pi*2

typedef unsigned char UCHAR;


typedef unsigned short USHRT;

typedef struct _color8{


int r,g,b;
} color8;

Luego procedemos a analizar la estructura _VoxSpace que es la estructura que va a


generar nuestro entorno en 3ra Dimensión a través de voxeles y es la que vamos a
estudiar en detalle:

typedef struct _VoxSpace{


int x,y,z; //Posición del observador
int a,b,c; //Ángulo del observador en unidad interna(actualmente la c
no se usa)
GRAPH *RMap; //Puntero al buffer de render
GRAPH *TMap; //Puntero al buffer del mapa de textura
GRAPH *HMap; //Puntero al buffer del mapa de alturas
GRAPH *BRMap; //Puntero al buffer de render del bilineal

color8 paleta[256];
int *cos_look,*sin_look; //Tablas de senos y cosenos

67
int a360; //numero de angulos totales
int a360m2; //La mitad de los ángulos totales y en negativo para evitar
expansiones gigantes
int ini_angle; //El valor de cam_angle/2 en la base del casting
//Variables varias
int cam_angle;
int dslope;
char jumplin; //Numero de columnas que pasamos cada vez (1,2 ó 4)
char scroll; //Si se scrollea el mapa o no (1,0)
char height_bit_shift; //logaritmo del ancho del mapa (textura o altura)
int max_steps; //Número máximo de pasos (lo lejos que se renderiza)
int scale; //Escalado de las montañas
} VoxSpace;

int NVoxSpaceIdents=0;
VoxSpace ** VoxSpaceIdents=NULL;

¿Qué tipos de datos se está definiendo en la estructura?. Primero, estamos creando las
variables x, y, z que es nuestra posición en 3D que vamos a reperesentar en pantalla,
luego están las variables a, b, c que es el ángulo del observador (cámara) representado en
alfa, beta y gamma (similar a lo que vimos en ETD). Luego están los Gráficos que nos
permitirán cargar primero el búfer del render que es lo que vamos a presentar en pantalla
aunque también se permite de la forma bilineal que hablaremos más adelante; luego
están los TMap que es el búfer en donde almacenaremos nuestro mapa de textura (el
terreno 3D) y está el HMap que es el mapa de altura, y como verás este mapa de altura
se comporta de manera algo similar a los Mapas de Durezas que se usan para generar
colisiones con un color en especial, solo que en este caso lo usaremos para realizar el
efecto Voxel en nuestro mapa de textura ya creado. Ya con la estructura color8 definida
podemos crear nuestra tabla de paletas de 256 colores que se usará mas adelante. Y lo
más importante, para evitar estar haciendo tantos cálculos de seno y coseno que pone
lento los procesos de dibujado de pantalla, entonces se ha definido las variables
*cos_look y *sin_look para generar tablas predefinidas de senos y cosenos.

Luego estamos definiendo el número de ángulos y la mitad de ellos para evitar


problemas de ángulos cuando se sobrepasa de los 360 grados. También se tienen
definida las variables para definir el ángulo de la cámara inicial, el número de columnas
que se deben de pasar (recuerda que un valor de 1 nuestro terreno se verá con muy alta
calidad a una velocidad razonable y un valor de 4 nos permite tener más velocidad a
cambio de una peor calidad), luego está definida la variable scroll por si necesita hacer
desplazamientos cíclicos, la variable height_bit_shift sirve para definir la altura que
tendrá el voxel space, luego sigue max_step para definir cuantos terrenos a la vez
veremos en el horizonte, scale representa el escalado de las montañas y por último fbil
nos dirá si se activa o no el filtro bilineado.

Una vez hecho esto generamos un vector de VoxelSpace (por los punteros **) llamada
VoxSpaceIdents que van a ser los identificadores de nuestro terreno voxel que vamos a
usar a lo largo de este capítulo y todos ellos están asignados con valor NULL.

68
C R E A N D O U N N U E V O V O X E L S P A C E

Una vez definido la estructura del VoxelSpace que estaremos usando a lo largo del
desarrollo de Voxeles, ahora es tiempo de crear un nuevo VoxelSpace a través de la
función vse_new_voxelspace. Esta función lo que hace es cuando no hay ningún
VoxelSpace que se haya creado anteriormente, entonces se procede a crear una zona de
memoria para almacenar nuestro voxelspace a través de malloc en C, pero si ya se había
creado una estructura voxelspace y hace mención a ese mismo voxelspace, entonces se
procede a buscar un hueco, es decir el identificador de voxelspace que creamos
anteriormente y si lo encuentra, no hacemos nada, pero de lo contrario se vuelve a
reasignar el espacio de memoria para el nuevo voxelspace a través de la función de C
realloc. Y por último se procede a iniclizar las variables establecidas en el parámetro al
llamar esta función en la estructura VoxSpaceIdents y nos devuelve el número de
identificador. Ese número de identificador es el que nos va a ayudar a utilizarlo en el
resto de las funciones que veremos más adelante en este capítulo:

static int vse_new_voxelspace (INSTANCE * my, int * params) {


int i=0;
char found=0;
if (!NVoxSpaceIdents){
VoxSpaceIdents = (VoxSpace **)malloc(sizeof(VoxSpace *));
NVoxSpaceIdents++;
} else {
//Buscamos un hueco
for (i=0;i<NVoxSpaceIdents;i++){
if (VoxSpaceIdents[i] == NULL) {found=1; break;}
}
//Si no hay hueco reallocamos
if (!found) {
VoxSpaceIdents = (VoxSpace **)realloc(VoxSpaceIdents,
(NVoxSpaceIdents+1)*sizeof (VoxSpace *));
i = NVoxSpaceIdents;
NVoxSpaceIdents++;
}
}
//Iniciamos el VoxelSpace
VoxSpaceIdents[i] = (VoxSpace *)malloc(sizeof(VoxSpace));
VoxSpaceIdents[i]->x = 0;
VoxSpaceIdents[i]->y = 0;
VoxSpaceIdents[i]->z = 0;
VoxSpaceIdents[i]->a = 0;
VoxSpaceIdents[i]->b = 0;
VoxSpaceIdents[i]->c = 0;
VoxSpaceIdents[i]->TMap = NULL;
VoxSpaceIdents[i]->HMap = NULL;
VoxSpaceIdents[i]->RMap = NULL;
VoxSpaceIdents[i]->BRMap = NULL;
VoxSpaceIdents[i]->cos_look = NULL;
VoxSpaceIdents[i]->sin_look = NULL;
VoxSpaceIdents[i]->a360 = 0;
VoxSpaceIdents[i]->a360m2 = 0;
VoxSpaceIdents[i]->ini_angle = 0;
VoxSpaceIdents[i]->cam_angle = 0;
VoxSpaceIdents[i]->dslope = 0;

69
VoxSpaceIdents[i] ->jumplin = 1;
VoxSpaceIdents[i] ->scroll = 0;
VoxSpaceIdents[i] ->height_bit_shift = 0;
VoxSpaceIdents[i] ->max_steps = 200;
VoxSpaceIdents[i] ->scale = 3;
//Devolvemos el identificador
return i;
}

E L I M I N A N D O U N V O X E L S P A C E E X I S T E N T E

Una vez definida la función para crear voxelspace ahora procedemos a eliminarlos para
ahorrar memoria a través de la función free y así darle oportunidad a que nuevos
voxelspaces puedan asignarse a través de la estructura VoxSpaceIdents. Recuerden que
para esta función se requiere como único parámetro el número de identificador que
había devuelto con la función vse_new_voxelspace.

static int vse_delete_voxelspace (INSTANCE * my, int * params) {


int j;
//Comprobamos que existe el VoxelSpace
if(params[0]>=NVoxSpaceIdents || VoxSpaceIdents[params[0]]==NULL)
return -1;

//Descargamos sus tablas


free(VoxSpaceIdents[params[0]]->cos_look);
free(VoxSpaceIdents[params[0]]->sin_look);

//Descargamos los sprites


free(VoxSpaceIdents[params[0]]->VoxSpriteIdents);

//Descargamos el mapa bilineal


free(VoxSpaceIdents[params[0]]->BRMap);

//Descargamos los objetos


free(VoxSpaceIdents[params[0]]->ObjRenderIdents);
//Hay que descargar vértices, aristas y caras de todos los objetos
for (j=0;j<VoxSpaceIdents[params[0]]->NObjetos;j++){
free(VoxSpaceIdents[params[0]]->objeto[j].vertex);
free(VoxSpaceIdents[params[0]]->objeto[j].edge);
free(VoxSpaceIdents[params[0]]->objeto[j].face);
}
free(VoxSpaceIdents[params[0]]->objeto);

//Descargamos el voxelspace
free(VoxSpaceIdents[params[0]]);

//Lo marcamos como libre


VoxSpaceIdents[params[0]] = NULL;

return 1;
}

70
G E N E R A N D O T A B L A S P R EC A L C U L A D A S

¿Te acuerdas de las variables *cos_look y *sin_look vistas en la estructura


VoxelSpace?. Bueno ahora es momento de usar estas variables para generar las
respectivas tablas precalculadas tanto para seno como para coseno por 4096 que era el
valor arreglado de 2^12 FIXP_MUL. Se recomienda que angle (usado en la función
vse_set_voxelspace que veremos más adelante) sea de valor 60, y que el mínimo sea 10
ya que de lo contrario se define como valor a 60.

void build_lookup_tables( int id, int angle ){


int a360,i,w;
double a;
if (VoxSpaceIdents[id]->RMap == NULL)
w = 320;
else
w = VoxSpaceIdents[id]->RMap->pitch;

if (angle<10) angle=60;
a360 = (360*w)/angle;
if (VoxSpaceIdents[id]->cos_look == NULL) {
VoxSpaceIdents[id]->cos_look = (int *)malloc(a360*sizeof (int));
VoxSpaceIdents[id]->sin_look = (int *)malloc(a360*sizeof (int));
}else{
VoxSpaceIdents[id]->cos_look = (int *)realloc(VoxSpaceIdents[id]-
>cos_look, a360*sizeof(int ));
VoxSpaceIdents[id]->sin_look = (int *)realloc(VoxSpaceIdents[id]-
>sin_look, a360*sizeof(int ));
}
for (i=0;i<a360;i++){
a = (i*2*PIE)/a360;
VoxSpaceIdents[id]->cos_look[i] = (int)(cos(a) * FIXP_MUL);
VoxSpac eIdents[id]->sin_look[i] = (int)(sin(a) * FIXP_MUL);
}
//Creamos tbn los demás datos relacionados con la cámara
i = 720/angle;
VoxSpaceIdents[id]->a360 = a360;
VoxSpaceIdents[id]->a360m2 = a360/2;
VoxSpaceIdents[id]->ini_angle = a360/i;
VoxSpaceIdents[id]->dslope = (FIXP_MUL*64)/w;
VoxSpaceIdents[id]->cam_angle = angle;
}

C O N F I G U R A N D O E L V O X EL S P A C E

Luego se procede a configurar el VoxelSpace. Para ellos se procede a verificar de que el


identificador del VoxelSpace existe en la estructura VoxSpaceIdents o de que el valor
del identificador no exceda del número de identificadores de VoxelSpace permitido.

Una vez comprobado pasamos a setear el VoxelSpace, para ello vamos a necesitar los
siguientes Parametros:

0.- ID de VoxelSpace
1.- Ángulo de la camara

71
2.- Líneas que saltar
3.- Scroll
4.- MaxSteps
5.- Factor de Es calado
6.- Filtro Bilineal

Se procede ahora a sacar el cam_angle que debería ser entre 60 y 10, pero si es distinto
del que teniamos, entonces se rehacen las tablas de lookup llamando a la función
build_lookup_tables vista en la sección anterior. Ahora se ajustan las líneas de salto,
para ello verifica si la misma no es igual a 1, ni a 2, ni a 4 que son los valores que permite
el motor de voxel, y si eso ocurre entonces se pone como valor de defecto 1, en caso
contrario es asignado en el miembro jumplin de nuestra estructura de VoxelSpace.
Luego verificamos que el modo de scroll esté entre 0 y 1 y si es un valor distinto
entonces se asigna por defecto 0. Luego ponemos los maxsteps que debe ser igual o
mayor a 10. Introducir un 0 no modifica los maxSteps. Y Fijamos el escalado del
terreno a través de la variable i que se calcula a través del logaritmo a base 2.

Si todo sale correcto nos devolverá 1, en caso contrario nos devolverá -1:

int log_b2(int num) {


int resto;
int l=0;
if (!num) return -1;
if (num==1) return 0;
while (1){
resto = num%2;
if (resto) return -1;
num /= 2;
l++;
if (num==1) return l;
}
}

static int vse_set_voxelspace (INSTANCE * my, int * params) {


int i;
if(params[0]>=NVoxSpaceIdents || VoxSpaceIdents[params[0]]==NULL)
return -1;

if (params[1] < 10) params[1] = 60;


if (params[1] != VoxSpaceIdents[params[0]]->cam_angle)
build_lookup_tables( params[0], params[1] );
if (params[2]!=1 && params[2]!=2 && params[2]!=4) params[2]=1;
VoxSpaceIdents[params[0]]->jumplin = (char)params[2];
if (params[3]!=0 && params[3]!=1) params[3]=0;
VoxSpaceIdents[params[0]]->scroll = (char)params[3];
if (params[4]) {
if (params[4]<10) params[4]=10;
VoxSpaceIdents[params[0]]->max_steps = params[4];
}
i = log_b2(params[5]);
if (i>=0) VoxSpaceIdents[params[0]]->scale = i;

return 1;

72
}

C O N F I G U R A N D O L O S M A P A S P A R A E L V O X E L S P A C E

Ahora una vez que hayamos configurado el VoxelSpace, es necesario configurar los
mapas que vamos a necesitar para generar el terreno para así asegurarnos que todo el
Render nos saldrá como debería de ser. Primero, de igual forma como hicimos en
vse_set_voxelspace(), verificamos que el identificador del VoxelSpace existe en la
estructura VoxSpaceIdents o que el valor del identificador no exceda del número de
identificadores de VoxelSpace permitido.

Si todo nos sale bien (que no retorne -1), entonces procedemos a utilizar los siguientes
parámetros del puntero *params que están enumeradas de la siguiente forma:

0.- ID de VoxelSpace
1.- Fichero de textura
2.- Mapa de textura
3.- Fichero de altura
4.- Mapa de altura
5.- Fichero de render_map
6.- Mapa de render_map

Recuerda que sin el Fichero cargado, no podemos seleccionar el Mapa o Grá fico, así
como hacemos en los procesos en Fenix.

Luego sacamos los buffers de los mapas de texturas, alturas y render en los parámetros 1
al 6 y lo guardamos en los búferes TMap, HMap y RMap.

Ejemplo de Mapa de Textura (TMap)

Ejemplo de Mapa de Altura (HMap)

73
El Mapa de Render Resultante (RMap)

Luego sacamos el logaritmo en base 2 del ancho del mapa (para hacer desplazamiento) y
lo guardamos en una variable auxiliar. Esto nos ayuda a saber si nuestro mapa de
Texturas o de Altura es potencia de 2.

Pero hay que tener mucho cuidado a la hora de cargar los mapas. Primero debemos
cerciorarse de varias cosas:

o Si el ancho del Mapa de Texturas no es igual al ancho del Mapa de Alturaa


entonces devuelve -2. De igual forma ocurre si el alto del Mapa de Texturas no
es igual al alto del Mapa de Altura.

o Si la profundidad de colores del Mapa de Altura no es 8 bits, entonces retorna -3.

o Si aux (la variable para indicar si nuestro mapa de Texturas o de Altura es


potencia de 2) es igual a -1, entonces retorna -4

o Si la profundidad de colores del Mapa de Texturas no es igual a la profundidad


de colores del Mapa de Render entonces retorna -5.

Una vez que todo nos sale bien, ahora procedemos a apuntar a la estructura
VoxSpaceIdents los mapas TMap, HMap y RMap y nos retorna 1 para indicar que
todo salió bien.

static int vse_set_map (INSTANCE * my, int * params) {


GRAPH *TMap, *HMap, *RMap;
int aux;

if(params[0]>=NVoxSpaceIdents || VoxSpaceIdents[params[0]]==NULL)
return -1;

TMap = bitmap_get (params[1], params[2]) ;


HMap = bitmap_get (params[3], params[4]) ;
RMap = bitmap_get (params[5], params[6]) ;

aux = log_b2(HMap ->width);

//Posibles errores en los mapas


if (TMap->width!=HMap ->width) return -2;
if (TMap->height!=HMap->height) return -2;
if (HMap->depth != 8) return -3;
if (aux == -1) return -4;
if (TMap->depth != RMap->depth) return -5;

//Apuntamos los buffers

74
VoxSpaceIdents[params[0]]->TMap = TMap;
VoxSpaceIdents[params[0]]->HMap = HMap;
VoxSpaceIdents[params[0]]->RMap = RMap;

build_lookup_tables( params[0], VoxSpaceIdents[params[0]]->cam_angle );


VoxSpaceIdents[params[0]]->height_bit_shift = (char)aux;

return 1;
}

P O S I C I O N A N D O N U E S T R O V O X E L S P A C E

Una vez definido el VoxelSpace al igual que los mapas, es momento de proceder con
darle movimientos y obtener información importante para el VoxelSpace.

Empezamos con posicionar el VoxelSpace con los métodos vse_set_position y


vse_get_position. Con vse_set_position al comprobar que nuestro VoxelSpace existe,
establecemos los valores x, y, z del VoxSpaceIdents con los valores que se reciben en
los parámetros al llamar a este método.

static int vse_set_position (INSTANCE * my, int * params) {


if(params[0]>=NVoxSpaceIdents || VoxSpaceIdents[params[0]]==NULL)
return -1;

VoxSpaceIdents[params[0]]->x = params[1];
VoxSpaceIdents[params[0]]->y = params[2];
VoxSpaceIdents[params[0]]->z = params[3];
return 1;
}

Y con vse_get_position , obtenemos los valores x, y, z del VoxSpaceIdents siempre y


cuando el identificador del VoxelSpace exista.

75
static int vse_get_position (INSTANCE * my, int * params) {
int *x,*y,*z;
if(params[0]>=NVoxSpaceIdents || VoxSpaceIdents[params[0]]==NULL)
return -1;

x = (int *)params[1];
y = (int *)params[2];
z = (int *)params[3];
if (x) *x = VoxSpaceIdents[params[0]]->x;
if (y) *y = VoxSpaceIdents[params[0]]->y;
if (z) *z = VoxSpaceIdents[params[0]]->z;
return 1;
}

**OJO** Este tipo de funciones son usados solamente cuando está dentro de un bucle
infinito o un bule con condición (Loop, While, Until y For).

G I R A N D O N U E S T R O V O X E L S P A C E

Luego de saber cómo vamos a posicionar nuestro VoxelSpace, ahora podemos girarlo.
Primero con vse_set_angle , que al saber que nuestro VoxelSpace existe, se procede a
ajustar los ángulos dentro de sus rangos, primero el ángulo alfa, debe estar entre los
ángulos -90000º a 90000º, y luego está el ángulo beta que debe estar entre los ángulos 0º
y 360000º; los valores fuera de este rango se recalculan a su equivalente dentro de él
(p.e. -90000 se iguala a 270000). Los ángulos se indican en milésimas de grado.

Y de esas milésimas la pasamos en centésimas de grados en los miembros a, b de la


estructura VoxSpaceIdents.

static int vse_set_angle (INSTANCE * my, int * params) {


if(params[0]>=NVoxSpaceIdents || VoxSpaceIdents[params[0]]==NULL)
return -1;

//Ajustamos los ángulos dentro de sus rangos


if (params[1] > 90000) params[1] = 90000;

76
else if (params[1] < -90000) params[1] = -90000;
while(params[2]>=360000) params[2]-=360000;
while(params[2]<0) params[2]+=360000;

//Recibimos el ángulo en grados y hay que pasarlo a base a360


VoxSpaceIdents[params[0]]->a = (params[1]*VoxSpaceIdents[params[0]]-
>a360)/360000;
VoxSpaceIdents[params[0]]->b = (params[2]*VoxSpaceIdents[params[0]]-
>a360)/360000;
return 1;
}

Y por último, tenemos a vse_get_angle que con ella obtenemos los ángulos en que
mira la cámara del voxelspace id_voxelspace.

static int vse_get_angle (INSTANCE * my, int * params) {


int *a, *b;
//Comprobamos que existe el VoxelSpace
if(params[0]>=NVoxSpaceIdents || VoxSpaceIdents[params[0]]==NULL)
return -1;

a = (int *)params[1];
b = (int *)params[2];
if (a) *a = (VoxSpaceIdents[params[0]]-
>a*360000)/VoxSpaceIdents[params[0]]->a360;
if (b) *b = (VoxSpaceIdents[params[0]]-
>b*360000)/VoxSpaceIdents[params[0]]->a360;
return 1;
}

**OJO** Este tipo de funciones son usados solamente cuando está dentro de un bucle
infinito o un bule con condición (Loop, While, Until y For).

M A N E J A N D O L A C Á M A R A D E L V O X E L S P A C E

Existen dos funciones que nos van a ayudar a manejar la cámara en el VoxelSpace. El
primero es vse_target que modifica los ángulos de la cámara del voxelspace indicado
para apuntar al punto que se le diga. Al hacer el siguiente render el punto que se pasó en
parámetros quedará exactamente en el centro del mapa de render. Los parámetros que
necesita esta función son:

0.- ID del voxelspace


1.- X de la coordenada donde apuntaremos
2.- Y de la coordenada donde apuntaremos
3.- Z de la coordenada donde apuntaremos

Una vez establecido las coordena das se procede a verificar que no sean iguales a las
coordenadas de la cámara, es decir, el de VoxSpaceIdents, ya que de lo contrario
devuelve -2 como error.

77
Luego sacamos el ángulo base angSB a través de la función vse_fget_angle , pasando las
coordenadas X,Y del VoxelSpace y las coordenadas X,Y a la cual apuntaremos (De ahí
se calcula la distancia entre ellos y a través del arco tangente nos debe devolver el
ángulo). Una vez hecho esto, se procede a verificar que el ángulo base esté ente los 0 y
360000 grados, y de ahí se pasa el ángulo a los sistemas de cámara (en centésimas de
grados).

Luego sacamos la distancia x, y a través del coseno y seno (precalculado) del ángulo
angSB. Y una vez hecho se calcula la distancia final en base a las distancias de x,y. Al
final veremos que se calcula el ángulo alfa que es que se apunta y se sobrescribe los
ángulos en los miembros a, b de VoxSpaceIdents.

int vse_fget_angle (params0, params1, params2, params3)


{
double dx = params2 - params0 ;
double dy = params3 - params1 ;
int angle ;

if (dx == 0) return dy > 0 ? 270000 : 90000 ;

angle = (int ) (atan(dy / dx) * 180000.0 / PIE) ;

return dx > 0 ? -angle:-angle+180000 ;


}

static int vse_target (INSTANCE * my, int * params) {


int angSB,
dx,dy,dz,
dist,
zr;

if(params[0]>=NVoxSpaceIdents || VoxSpaceIdents[params[0]]==NULL) return


-1;

if(params[1]==VoxSpaceIdents[params[0]]->x &&
params[2]==VoxSpaceIdents[params[0]] ->y &&
params[3]==VoxSpaceIdents[params[0]] ->z) return -2;

angSB = vse_fget_angle ( VoxSpaceIdents[params[0]]->x,


VoxSpaceIdents[params[0]]->y,
params[1],
params[2]);
if (angSB<0) angSB+= 360000;
angSB = (angSB * VoxSpaceIdents[params[0]]->a360)/360000;

dx = VoxSpaceIdents[params[0]]->cos_look[angSB] << 1;
dy = -VoxSpaceIdents[params[0]]->sin_look[angSB] << 1;

if (dx>dy)
if (dx!=0)
dist = ((params[1]-VoxSpaceIdents[params[0]]->x) << FIXP_SHIFT)/dx;
else dist=0;
else if (dy!=0)
dist = ((params[2]-VoxSpaceIdents[params[0]]->y) << FIXP_SHIFT)/dy;
else dist=0;

78
zr = (params[3] << (FIXP_SHIFT+VoxSpaceIdents[params[0]]->scale))-
(VoxSpaceIdents[params[0]] ->dslope*dist*VoxSpaceIdents[params[0]]->RMap-
>height>>1);
dz = (zr - (VoxSpaceIdents[params[0]]->z << FIXP_SHIFT))/dist;

VoxSpaceIdents[params[0]]->a = (dz/VoxSpaceIdents[params[0]]->dslope) +
VoxSpaceIdents[params[0]]->RMap->height;
VoxSpaceIdents[params[0]]->b = angSB;
return 1;
}

Y el segundo es vse_advance, que avanza la cámara del voxelspace id_voxelspace la


distancia que se le indica en los ángulos que tiene actualmente ésta.

static int vse_advance (INSTANCE * my, int * params) {


if(params[0]>=NVoxSpaceIdents || VoxSpaceIdents[params[0]]==NULL)
return -1;

VoxSpaceIdents[params[0]]->x += ((params[1]*VoxSpaceIdents[params[0]]-
>cos_look[VoxSpaceIdents[params[0]]->b]) >> FIXP_SHIFT);
VoxSpaceIdents[params[0]]->y -= ((params[1]*VoxSpaceIdents[params[0]]-
>sin_look[VoxSpaceIdents[params[0]]->b]) >> FIXP_SHIFT);
return 1;
}

**OJO** Este tipo de funciones son usados solamente cuando está dentro de un bucle
infinito o un bule con condición (Loop, While, Until y For).

O B T E N I E N D O L A A L T U R A D E L V O X E L S P A C E

Esta última función para el manejo de VoxelSpace nos ayuda a obtener la altura de un
punto (x,y) del voxelspace id_voxelspace. La altura que se devuelve se verá afectada
por el factor de escalado del voxelspace. Para obtener la altura sin escalado habrá que
usar la función map_get_pixel de Fénix.

static int vse_get_height (INSTANCE * my, int * params) {


if(params[0]>=NVoxSpaceIdents || VoxSpaceIdents[params[0]]==NULL) return
-1;

return (((UCHAR *)VoxSpaceIdents[params[0]]->HMap-


>data)[params[1]+(params[2]<<VoxSpaceIdents[params[0]]-
>height_bit_shift)])<<VoxSpaceIdents[params[0]]->scale;
}

**OJO** Este tipo de función es usado solamente cuando está dentro de un bucle
infinito o un bule con condición (Loop, While, Until y For).

79
L I B E R A N D O E L V O X E L S PA C E

Para liberar el VoxelSpace debemos llamar a la función vse_quit() que descarga de


memoria todos los voxelspaces que usase la dll. Debe ser usada antes de salir de
Fénix ya que este por si mismo no descarga las variables de las dll.

static int vse_quit (INSTANCE * my, int * params) {


int i,j;
//Descargamos todos los voxspaces
for (i=0;i<NVoxSpaceIdents;i++) {
//Si existe el voxspace lo descargamos
if (VoxSpaceIdents[i]) {
//Descargamos sus tablas
free(VoxSpaceIdents[i]->cos_look);
free(VoxSpaceIdents[i]->sin_look);

//Descargamos el mapa bilineal


free(VoxSpaceIdents[i]->BRMap);

//Descargamos el voxelspace en sí
free(VoxSpaceIdents[i]);
}
}
//Descargamos el array de VoxSpaces
free(VoxSpaceIdents);

//Reseteamos
NVoxSpaceIdents = 0;
return i;
}

Listo, ya tenemos las funciones necesarias para manejar por completo nuestro
VoxelSpace, ahora solo nos queda renderizar el VoxelSpace.

80
5
Capítulo

Renderizando Voxels 3D
I N T R O D U C C I Ó N

En esta sección veremos las funciones que nos ayudará a renderizar nuestro Voxel 3D.
Antes de comenzar a explicar el renderizado de los Voxeles, debemos tener claro los
siguientes conceptos:

Angle nos permite indicar el ángulo de apertura de la cámara, así el ángulo


recomendado es 60º y el mínimo es 10º, es tarea del programador elegir el que mejor
le convenga.

El salto (jump) nos permite renderizar más rápido o lento a costa de perder o ganar
calidad, así que este valor por defecto vale 1, que significa que se renderizan todas las
columnas de pantalla, un salto de valor 2 indica que se renderizará una de cada dos
columnas, y la que queda libre se copia de la anterior, y un salto de valor 4 copia 3
columnas de cada 4.

Como se puede comprender un salto 4 es el más rápido al no tener que hacer apenas
cálculos, pero también es el que tendrá peor calidad, una buena opción es el salto 2,
que mantiene una calidad aceptable así como velocidad.

En la siguiente imagen vemos nuestro VoxelSpace con un salto de 1 píxels por


columna, renderizando todas las columnas:

81
Mientras que en esta imagen vemos el mismo VoxelSpace pero con saltos de 4 pixeles
por columna:

El parámetro scroll sirve para indicar si el mapa será tratado cíclicamente, es decir, si
cuando acaba el mapa se repite éste otra vez y así en todas las direcciones del espacio,
por defecto vale 0 que indica que no se usará terreno cíclico, un valor de 1 hace que sí
se use.

En esta imagen vemos nuestro VoxelSpace que es nuestro Mapa de Textura limitado
al tamaño que posee:

Mientras que en la otra imagen con scroll en 1, se vé que la misma es cíclica:

82
maxStep nos permite indicar hasta que distancia se renderizará, por supuesto un
mayor valor implica una cantidad mayor de cálculo, por defecto vale 200 e indicar un
valor 0 implica mantener el valor actual.

En esta imagen vemos el VoxelSpace con un valor de distancia de 200:

Y estra otra imagen vemos el mismo VoxelSpace con un valor de distancia de 600:

Y con scale indicamos por cuanto se amplifican las alturas del mapa de alturas, es
decir, si usamos un scale 8 (el que hay por defecto) la altura que tendrá la punta más
alta de un mapa de alturas será 255*8 = 2040 (referido a la pos ición del observador),
es decir, que cuanto mayor sea scale las montañas serán más "puntiagudas", scale
debe ser una potencia de 2 (1, 2, 4, 8, 16, ...), si se introduce un factor que no sea con
éstas características se dejará el anterior.

83
En la siguiente imagen veremos el VoxelSpace casi plano ya que tiene como valor de
escala 1:

Pero si cambiamos el valor de escala del VoxelSpace a 4 veremos una gran diferencia en
el renderizado ya que las montañas están ahora más crecidas:

Y por último si establecemos el valor de escala a 16 nuestra montaña quedaría muy


puntiaguda:

E L P R O C E S O D E R E N D E R I Z A C I Ó N

Ya una vez comprendido los términos que vamos a implementar a la hora del render, es
momento de llamar a la función vse_render_voxelspace() que recibe como único
parámetro el identificador del VoxelSpace.

Ahora es momento de entrar a la parte inicial del renderizado y en ella vamos a necesitar
varias variables de posiciones para aplicar lo que se llama el trazado de rayos que no es
más que una técnica en donde si en una posición en particular hay un valor de altura

84
especificada, entonces el píxel genera una especie de línea vertical hacia arriba y el
conjunto de ellos desde lejos se denotaría como si hubieran “rayos” y esa es la sensación
que se dá para renderizar un vóxel en pantalla.

static int vse_render_voxelspace (INSTANCE * my, int * params) {


int xr,yr, // posición del rayo actual
curr_column, // columna actual de la pantalla
curr_step, // paso del rayo
raycast_ang, // angulo actual del rayo
dx,dy,dz, // deltas
curr_voxel_scale, // tamaño de voxel
column_height, // altura de la columna
curr_row, // numero de filas renderizadas en la columna
x_ray,y_ray,z_ray, // posicion de inicio del rayo
map_addr; // posicion en los mapas(buffer)

Ahora procedemos a definir las estructuras que vamos a usar para pintar la pantalla. Una
de ellas es que vamos a utilizar el tipo de datos color que ocupa 256 bytes (UCHAR), al
igual que los tipos de datos *dest_column_ptr y *aux_column_ptr que serán los
búferes que vamos a usar para pintar las columnas o “rayos” a la pantalla:

UCHAR color, // color del pixel a pintar


*dest_column_ptr, // posición de memoria donde pintar
*aux_column_ptr, //puntero auxiliar para hacer backup de dest_column_ptr

Una vez obtenido el parámetro del identificador del VoxelSpace ahora procedemos a
definir las variables para posicionar nuestro VoxelSpace, y estas son las posiciones x,y,z
al igual que los ángulos a,b,c que son alfa, beta y gamma. Luego con el búfer
*dest_buffer de 256 bytes de tamaño (UCHAR) obtenemos el mapa de Renderizado
que es el que vamos a usar para dar el resultado final a la pantalla una vez que se hayan
pintado las columnas o rayos de los búferes *dest_column_ptr y *aux_column_ptr
mencionados anteriormente:

int i = params[0];
int vp_x;
int vp_y;
int vp_z;
int vp_ang_x;
int vp_ang_y;
int vp_ang_z;
UCHAR *dest_buffer;
int dslope;

vp_x = VoxSpaceIdents[i]->x;
vp_y = VoxSpaceIdents[i]->y;
vp_z = VoxSpaceIdents[i]->z;
vp_ang_x = VoxSpaceIdents[i]->a;
vp_ang_y = VoxSpaceIdents[i]->b;
vp_ang_z = VoxSpaceIdents[i]->c;
dest_buffer = (UCHAR *)VoxSpaceIdents[i]->RMap->data;
dslope = VoxSpaceIdents[i]->dslope;

85
Luego de haber definido las variables, es momento primero de comprobar si exite el
VoxelSpace que nos mandó el primer parámetro, ya que en caso contrario, se retorna -1.
Luego debemos de comprobar si el Mapa de Renderizado (Definido por la función
set_mode() en Fenix) es de 16 bits; en caso de ser así se procede a llamar al método
vse_render16 que veremos más adelante.

En caso contrario entonces vamos a comprobar si los Mapas y las tablas precalculadas de
coseno y seno estén correctamente inicializadas (Que no estén en NULL), ya que en caso
contrario nos retornará -2, -3, -4, y -5 como error.

Luego es momento de redefinir las variables x, y, z con multiplicaciones con valores


arreglados. Recordando la variable FIXP_SHIFT que valía 12, entonces:

vp_x = vp_x * 212 = vp _ x * 4096


vp_y = vp_y * 212 = vp _ y * 4096
vp_z = vp_z * 212 = vp _ z * 4096

Y así tenemos ya definidos las variables respectivas sin necesidad de usar complejos
cálculos exponenciales como se vió en el primer resultado de la fórmula.

//Comprobamos que existe el VoxelSpace


if(i>=NVoxSpaceIdents || VoxSpaceIdents[i]==NULL) return -1;

//Comprobamos si es de 16 bits, y si es así renderizamos con la otra


if (VoxSpaceIdents[i] ->RMap->depth != 8) return vse_render16(i);

//Comprobamos que esté bien inicializado


if ( VoxSpaceIdents[i]->TMap == NULL) return -2;
if ( VoxSpaceIdents[i]->HMap == NULL) return -3;
if ( VoxSpaceIdents[i]->RMap == NULL) return -4;
if ( VoxSpaceIdents[i]->cos_look == NULL) return -5;
if ( VoxSpaceIdents[i]->sin_look == NULL) return -5;

// Convertimos a fixed
vp_x = (vp_x << FIXP_SHIFT);
vp_y = (vp_y << FIXP_SHIFT);
vp_z = (vp_z << FIXP_SHIFT);

Ahora procedemos a colocar el búfer de destino hacia abajo a la izquierda ya que a partir
de ahí es que va a comenzar a realizar el pintado de los rayos o columnas en la pantalla, y
luego de colocar el ángulo del rayo inicial entre los grados 0 y 360 (Como lo indica a360),
entonces es momento de lanzar el rayo por cada columna que pase:

// Ponemos el buffer de destino abajo izqda de la pantalla


dest_buffer += ((VoxSpaceIdents[i]->RMap->pitch) * (VoxSpaceIdents[i]-
>RMap->height-1));

// Ajustamos vp_ang_y dentro del rango 0..360º


// Angulo del rayo inicial

86
raycast_ang = vp_ang_y + VoxSpaceIdents[i]->ini_angle;
if (raycast_ang>VoxSpaceIdents[i]->a360)
raycast_ang -= VoxSpaceIdents[i]->a360;

// Ahora lanzamos un rayo por cada columna


for (curr_column=0; curr_column < VoxSpaceIdents[i]->RMap->pitch;
curr_column+=VoxSpaceIdents[i]->jumplin){

Ahora pasamos los valores de x,y,z del VoxelSpace actual hacia las corrdenadas x,y,z del
rayo inicial y calculamos la distancia de las coordenadas x,y a partir del coseno y seno
precalculado del ángulo del rayo multiplicado por 2. Mientras que la distancia de z es
correspondiente a la altura que hay entre la altura total (dslope) multiplicado por el
ángulo alfa del Voxel restado por la altura del mapa de Renderizado:

// Punto inicial (el del observador)


x_ray = vp_x;
y_ray = vp_y;
z_ray = vp_z;

// Calculamos las deltas


dx = VoxSpaceIdents[i]->cos_look[raycast_ang] << 1;
dy = -VoxSpaceIdents[i]->sin_look[raycast_ang] << 1; //< -NEGATIVO
PORQUE EN SISTEMA DE COORDENADAS DE ORDENADOR LA "Y" VA PARA ABAJO!!
dz = dslope * (vp_ang_x - VoxSpaceIdents[i]->RMap->height);

Ahora reseteamos el escalado del voxel y la fila (que se pasa por el alto del mapa) a cero
y con el puntero de la columna lo apuntamos al punto inicial o en donde esté del búfer
de destino.

// Reseteamos las variables de control


curr_voxel_scale = 0;
curr_row = 0;

// Apuntamos el puntero de dibujo en la columna que toque


dest_column_ptr = dest_buffer;

Ahora entramos al ciclo For para calcular los pasos (steps) de cada rayo y esto se hace
hasta que llegue a maxSteps, es decir las porciones extras que se dibujarán en el Vóxel y
esta parte es la que lleva la mayor parte del código de renderizado. Así que de ahí
procedemos a calcular la posición que vamos a iniciar en nuestro mapa así:

xr = xdelrayo / 4096
yr = ydelrayo / 4096

// Calculamos los pasos de cada rayo


for (curr_step = 0;
curr_step < VoxSpaceIdents[i]->max_steps;
curr_step++){
//Calculamos la posición del mapa (real, no fixed)
xr = (x_ray >> FIXP_SHIFT);
yr = (y_ray >> FIXP_SHIFT);

87
Luego debemos de comprobar que en caso de que no haya scroll, se dibuje solamente el
Mapa con el ancho y alto correspondiente. Y en caso contrario añadimos las
coordenadas con el ancho y alto del mapa ya sea cuando nos movemos hacia delante o
atrás o cuando nos movemos de derecha a izquierda.

//Si no hay scroll comprobamos que no nos hayamos salido


if (!VoxSpaceIdents[params[0]]->scroll){
if ( xr<0 || xr>VoxSpaceIdents[i]->HMap->pitch ||
yr<0 || yr>VoxSpaceIdents[i]->HMap->height) {
x_ray+=dx;
y_ray+=dy;
z_ray+=dz;
curr_voxel_scale+=dslope;
continue;
}
} else { //Si hay scroll metemos coordenadas en el rango
xr = (xr & (VoxSpaceIdents[i]->HMap->pitch-1));
yr = (yr & (VoxSpaceIdents[i]->HMap->height-1));
}

Luego haremos dos cálculos, primero, la dirección del mapa, que es la que nos ayudará a
posicionar el punto en las coordenadas del Mapa de Alturas, y la altura de la columna
que es el resultado de la dirección del mapa por el factor de escala en el VoxelSpace. Y
una vez hecho esto obtenemos el color a partir del Mapa de Texturas en la dirección del
Mapa, pero ahora nos pregutamos ¿Qué lineas verticales dibujaremos?. La respuesta es
relativamente simple, dibujaremos solamente líneas verticales aquellos en donde la altura
de la columna sea mayor que la altura de la columna anterior (zr). Eso nos ayudaría a
ahorrar tiempo a la hora de dibujar Voxels en pantalla.

Representación del renderizado de un VoxelSpace

Representación en otro ángulo del renderizado del VoxelSpace

//Calculamos la posición en los buffers(altura y textura)


map_addr = (xr + (yr << VoxSpaceIdents[i]->height_bit_shift));

//Cogemos la altura de la columna


column_height = (((UCHAR *)VoxSpaceIdents[i]->HMap->data)[map_addr]
<< (FIXP_SHIFT+VoxSpaceIdents[params[0]]->scale));

//Comprobamos si la altura es mayor que la anterior


if (column_height > z_ray){
// color de la columna
color = ((UCHAR *)VoxSpaceIdents[i]->TMap->data)[map_addr];

88
Ahora procedemos a entrar en un bucle infinito para hacer el dibujo de las líneas
verticales. Y dependiendo del modo de salto (establecidas en jumplin del VoxelSpace)
tenemos tres valores. Si vale 1, entonces dibujamos en el búfer de columnas de destinos
1 píxel. Pero si vale 2, entonces dibujaremos dos píxeles en vez de 1 y realizamos un
salto de dos píxeles. Así de esta forma se hace con 4.

// dibujamos una linea vertical(o varias en modos de salto)


while(1){
// pintamos uno o varios p íxeles
switch(VoxSpaceIdents[i]->jumplin){
case 1:
if (!*dest_column_ptr)
*dest_column_ptr = color;
break;
case 2:
if (!*dest_column_ptr)
*dest_column_ptr = color;
if (!*(dest_ column_ptr+1))
*(dest_column_ptr+1) = color;
break;
case 4:
if (!*dest_column_ptr)
*dest_column_ptr = color;
if (!*(dest_column_ptr+1))
*(dest_column_ptr+1) = color;
if (!*(dest_column_ptr+2))
*(dest_column_ptr+2) = color;
if (!*(dest_column_ptr+3))
*(dest_column_ptr+3) = color;
break;
}

Ahora al estar dentro del bucle infinito nos preguntaremos ¿Cuándo salimos de este
bucle?. La respuesta es la misma que la anterior. Cuando la posición z del rayo (z_ray)
sea mayor a la altura de la columna obtenida por el Mapa de Alturas. Aquí hay que tener
en cuenta lo siguiente. ¿Se acuerdan que habíamos mostrado un ejemplo de un Mapa de
Texturas y de un Mapa de Alturas?. Bien, al tener un Mapa de Alturas como este:

Tenemos una paleta como esta:

89
Viendo esta paleta, la altura empieza desde 1 con el color azul (que es el más llano) hasta
254 con el color rojo (que es el más alto), y al momento de calcular la altura de la
columna, el obtiene cualquiera de esos valores. Así por ejemplo, si se obtuvo como altura
de columna 240, entonces nuestro puntero de dibujo de columnas apuntará al píxel
superior y seguirá dibujando el rayo (dependiendo del número de saltos) hasta que llegue
a 240 la altura máxima. Y este seguirá dibujando el rayo también hasta que llegue al topo
de maxSteps comentado anteriormente. Y una salido del ciclo While, avanzamos el rayo
en base a las coordenadas x,y,z al igual que aumentamos en vóxel en base a la escala
establecida (dslope).

// aumentamos la delta
dz+=dslope;

//Avanzamos el rayo
z_ray+=curr_voxel_scale;

// apuntamos al pixel inmediatamente superior


dest_column_ptr-=VoxSpaceIdents[i]->RMap->pitch;

// comprobamos que no nos hayamos salido


if (++curr_row >= VoxSpaceIdents[i]->RMap ->height){
//Forzamos la salida del bucle
curr_step = VoxSpaceIdents[i]->max_steps;
break;
} // end if

// comprobamos si podemos salir


if (z_ray > column_height) break;
} // end while
} // end if

// avanzamos el rayo
x_ray+=dx;
y_ray+=dy;
z_ray+=dz;

//Aumentamos el tamaño del voxel


curr_voxel_scale+=dslope;
} // end for curr_step

90
Finalmente, para terminar de renderizar nuestro Vóxel, pasamos el búfer de dibujo de
destino al siguiente píxel de columnas y aumentamos el ángulo del rayo y tiene que estar
entre 0º y 360º y volvemos nuevamente a dibujar los rayos tal y como lo vimos en el
bucle anterior hasta que llegue a la última columna (que obviamente debe ser el ancho
del mapa de Texturas) y si es cíclico nuestro VoxelSpace, seguimos dibujando de nuevo
el VoxelSpace, etc. Y todo eso nos debe retornar 1 indicando que todo el render se
desarrolló de manera satisfactoria.

// avanzamos el puntero de dibujo a la


// siguiente columna (o siguientes en caso de salto)
dest_buffer+=VoxSpaceIdents[i]->jumplin;

// aumentamos el angulo (el correspondiente


// a la siguiente columna)
raycast_ang-=VoxSpaceIdents[i]->jumplin;
if (raycast_ang<0)
raycast_ang += VoxSpaceIdents[params[0]]->a360;
}// end Ciclo For curr_col

return 1;
} // end Render_Terrain

**OJO** Este tipo de función es usado solamente cuando está dentro de un bucle
infinito o un bule con condición (Loop, While, Until y For).

Ahora en caso de que estamos en una pantalla de 16 bits de colores, entonces el método
anterior no nos servirá, así que llamamos al método vse_render16(). Hace las mismas
operaciones que hace la función anterior, solo que la misma al dibujarse en 16 bits la
pantalla necesita pintarse en otro Buffer de Destino que ocupa 65535 Bytes (USHRT) en
vez del otro Buffer de Destino que ocupaba 256 bytes de la función anterior (UCHAR):

int vse_render16 (int i ) {


int xr,yr, // posición del rayo actual
curr_column, // columna actual de la pantalla
curr_step, // paso del rayo
raycast_ang, // angulo actual del rayo
dx,dy,dz, // deltas
curr_voxel_scale, // tamaño de voxel
column_height, // altura de la columna
curr_row, // numero de filas renderizadas en la columna
x_ray,y_ray,z_ray, // posicion de inicio del rayo
map_addr; // posicion en los mapas(buffer)
USHRT color16, // color del pixel a pintar
*dest_column_ptr16, // posición de memoria donde pintar
*aux_column_ptr16, //puntero auxiliar para hacer backup de
dest_column_ptr
int vp_x = VoxSpaceIdents[i]->x;
int vp_y = VoxSpaceIdents[i]->y;
int vp_z = VoxSpaceIdents[i]->z;
int vp_ang_x = VoxSpaceIdents[i]->a;
int vp_ang_y = VoxSpaceIdents[i]->b;
int vp_ang_z = VoxSpaceIdents[i]->c;
USHRT *dest_buffer16 = (USHRT *)VoxSpaceIdents[i]->RMap->data;

91
int dslope = VoxSpaceIdents[i]->dslope;

//Comprobamos que existe el VoxelSpace


if(i>=NVoxSpaceIdents || VoxSpaceIdents[i]==NULL) return -1;

//Comprobamos que esté bien inicializado


if ( VoxSpaceIdents[i]->TMap == NULL) return -2;
if ( VoxSpaceIdents[i]->HMap == NULL) return -3;
if ( VoxSpaceIdents[i]->RMap == NULL) return -4;
if ( VoxSpaceIdents[i]->cos_look == NULL) return -5;
if ( VoxSpaceIdents[i]->sin_look == NULL) return -5;

// Convertimos a fixed
vp_x = (vp_x << FIXP_SHIFT);
vp_y = (vp_y << FIXP_SHIFT);
vp_z = (vp_z << FIXP_SHIFT);

// Ponemos el buffer de destino abajo izqda de la pantalla


dest_buffer16 += ((VoxSpaceIdents[i]->RMap->pitch) * (VoxSpaceIdents[i]-
>RMap->height-1));

//Ajustamos vp_ang_y dentro del rango 0..360º


// Angulo del rayo inicial
raycast_ang = vp_ang_y + VoxSpaceIdents[i]->ini_angle;
if (raycast_ang>VoxSpaceIdents[i]->a360)
raycast_ang -= VoxSpaceIdents[i]->a360;

// Ahora lanzamos un rayo por cada columna


for (curr_column=0; curr_column < VoxSpaceIdents[i]->RMap->pitch;
curr_column+=VoxSpaceIdents[i]->jumplin){
// Punto inicial (el del observador)
x_ray = vp_x;
y_ray = vp_y;
z_ray = vp_z;

// Calculamos las deltas


dx = VoxSpaceIdents[i]->cos_look[raycast_ang] << 1;
dy = -VoxSpaceIdents[i]->sin_look[raycast_ang] << 1; //<-NEGATIVO
PORQUE EN SISTEMA DE COORDENADAS DE ORDENADOR LA "Y" VA PABAJO!!
dz = dslope * (vp_ang_x - VoxSpaceIdents[i]->RMap->height);

// Reseteamos las variables de control


curr_voxel_scale = 0;
curr_row = 0;

// Apuntamos el puntero de dibujo en la columna que toque


dest_column_ptr16 = dest_buffer16;

// Calculamos los pasos de cada rayo


for (curr_step = 0; curr_step < VoxSpaceIdents[i]->max_steps;
curr_step++){
//Calculamos la posición del mapa (real, no fixed)
xr = (x_ray >> FIXP_SHIFT);
yr = (y_ray >> FIXP_SHIFT);

//Si no hay scroll comprobamos que no nos hayamos salido


if (!VoxSpaceIdents[i]->scroll){
if (xr<0 || xr>VoxSpaceIdents[i]->HMap->pitch || yr<0 ||
yr>VoxSpaceIdents[i]->HMap->height) {

92
x_ray+=dx;
y_ray+=dy;
z_ray+=dz;
curr_voxel_scale+=dslope;
continue;
}
} else { //Si hay scroll metemos coordenadas en el rango
xr = (xr & (VoxSpaceIdents[i]->HMap->pitch-1));
yr = (yr & (VoxSpaceIdents[i]->HMap->height-1));
}
//Calculamos la posición en los buffers(altura y textura)
map_addr = (xr + (yr << VoxSpaceIdents[i]->height_bit_shift));

//Cogemos la altura de la columna


column_height = (((UCHAR *)VoxSpaceIdents[i]->HMap->data)[map_addr]
<< (FIXP_SHIFT+VoxSpaceIdents[i]->scale));

//Comprobamos si la altura es mayor que la anterior


if (column_height > z_ray){
// color de la columna
color16 = ((USHRT *)VoxSpaceIdents[i]->TMap->data)[map_addr];

// dibujamos una linea vertical(o varias en modos de salto)


while(1){
// pintamos uno o varios pixels
switch(VoxSpaceIdents[i]->jumplin){
case 1:
if (!*dest_column_ptr16)
*dest_column_ptr16 = color16;
break;
case 2:
if (!*dest_column_ptr16)
*dest_column_ptr16 = color16;
if (!*(dest_column_ptr16+1))
*(dest_column_ptr16+1) = color16;
break;
case 4:
if (!*dest_column_ptr16)
*dest_column_ptr16 = color16;
if (!*(dest_column_ptr16+1))
*(dest_column_ptr16+1) = color16;
if (!*(dest_column_ptr16+2))
*(dest_column_ptr16+2) = color16;
if (!*(dest_column_ptr16+3))
*(dest_column_ptr16+3) = color16;
break;
}
// aumentamos la delta
dz+=dslope;

//Avanzamos el rayo
z_ray+=curr_voxel_scale;

// apuntamos al pixel inmediatamente superior


dest_column_ptr16 -=VoxSpaceIdents[i]->RMap->pitch;

// comprobamos que no nos hayamos salido


if (++curr_row >= VoxSpaceIdents[i]->RMap->height){
//Forzamos la salida del bucle

93
curr_step = VoxSpaceIdents[i]->max_steps;
break;
} // end if

// comprobamos si pdemos salir


if (z_ray > column_height) break;
} // end while
} // end if

// avanzamos el rayo
x_ray+=dx;
y_ray+=dy;
z_ray+=dz;

//Aumentamos el tamaño del voxel


curr_voxel_scale+=dslope;
} // end curr_step

// avanzamos el puntero de dibujo a la siguiente


// columna (o siguientes en caso de salto)
dest_buffer16+=VoxSpaceIdents[i]->jumplin;

// aumentamos el angulo (el correspondiente


// a la siguiente columna)
raycast_ang-=VoxSpaceIdents[i]->jumplin;

if (raycast_ang<0) raycast_ang += VoxSpaceIdents[i]->a360;


}// end curr_col

return 1;
} // end Render_Terrain

Listo, eso es todo. Recuerda que esta función se usa en conjunto con
vse_render_voxelspace() y no es necesaria incluirla en las funciones que vamos a
exportar en Fénix.

E X P O R T A N D O L A L I B R E R Í A A F É N I X

Una vez definida todas las funciones para renderizar nuestro VoxelSpace, ahora es
momento de exportar todas las funciones a una DLL para después compilarlas, para así
poder usarlo en Fénix:

FENIX_MainDLL RegisterFunctions (COMMON_PARAMS)


{
FENIX_DLLImport

FENIX_export ("VSE_ADVANCE", "II", TYPE_DWORD, vse_advance ) ;


FENIX_export ("VSE_DELETE_VOXELSPACE", "I", TYPE_DWORD,
vse_delete_voxelspace ) ;
FENIX_export ("VSE_GET_ANGLE", "IPP", TYPE_DWORD, vse_get_angle ) ;
FENIX_export ("VSE_GET_HEIGHT", "III", TYPE_DWORD, vse_get_height ) ;
FENIX_export ("VSE_GET_POSITION", "IPPP", TYPE_DWORD, vse_get_position )
;
FENIX_export ("VSE_NEW_VOXELSPACE", "", TYPE_DWORD, vse_new_voxelspace )
;

94
FENIX_export ("VSE_RENDER_VOXELSPACE", "I", TYPE_DWORD,
vse_render_voxelspace ) ;
FENIX_export ("VSE_SET_ANGLE", "III", TYPE_DWORD, vse_set_angle ) ;
FENIX_export ("VSE_SET_MAP", "IIIIIII", TYPE_DWORD, vse_set_map ) ;
FENIX_export ("VSE_SET_POSITION", "IIII", TYPE_DWORD, vse_set_position )
;
FENIX_export ("VSE_SET_VOXELSPACE", "IIIIII", TYPE_DWORD,
vse_set_voxelspace ) ;
FENIX_export ("VSE_TARGET", "IIII", TYPE_DWORD, vse_target ) ;
FENIX_export ("VSE_QUIT", "", TYPE_DWORD, vse_quit ) ;
}

Así este al final debe generarte una librería llamada VSE.DLL que usaremos en el
siguiente capítulo.

95
6
Capítulo

Uso del VSE


I N T R O D U C C I Ó N

Ya hemos visto en los dos capítulos anteriores sobre qué es un Vóxel, qué características
implementa, qué funciones debemos de usar y cómo se renderiza la misma a la pantalla.
Ahora es momento de probar nuestro Voxel 3D en pantalla con este ejemplo.

import "vse.dll"

private

id_voxelspace; //ID de la escena


GTMap,GHMap; //IDs de el mapa de texturas y el de alturas

//Posición de la camara
xb = 256;
yb = 256;
zb = 150;

//Angulos de la camara
alfa = 15000;
beta = 90000;

//Variables para configurar el voxelspace


angulo = 60;
salto = 1;
scrol = 1;
pasoMax = 200;
scale = 8;

//Variables de control
byte set_p = 1; //Para indicar si hay que setear posicion
byte set_a = 1; //Para indicar si hay que setear angulo
byte set_v = 0; //Para indicar si hay que setear el voxelspace
velocidad = 5; //Velocidad a la que nos movemos

begin
set_mode(320, 200, 16);
full_screen = false;
set_fps(30,0); //Le damos alegria
write_int(0,0,0,0,&fps); //Metemos el chivato

//Creamos un voxelspace
id_voxelspace = vse_new_voxelspace( );

96
//Lo configuramos
vse_set_voxelspace( id_voxelspace, angulo, salto, scrol, pasoMax,
scale);

//Cargamos los mapas


GHMap = load_map("test512h.map");
GTMap = load_map("test512t.map");

//Creamos el rendermap
graph = new_map(320,200,8);
set_center(0, graph, 0, 0); //Por comodidad ponemos el centro en 0,0

//Configuramos los mapas


vse_set_map( id_voxelspace, 0, GTMap, 0, GHMap, 0, graph );

loop
//Controles de la camara
if (key(_a)) alfa+=1000; set_a = 1; end
if (key(_z)) alfa -=1000; set_a = 1; end
if (key(_left)) beta+=1000; set_a = 1; end
if (key(_right)) beta-=1000; set_a = 1; end
if (key(_up)) velocidad++; end
if (key(_down)) velocidad--; end

//Controlamos que la velocidad no se desmadre


if (velocidad>25) velocidad=25; end
if (velocidad<-25) velocidad=-25; end

//Control de posición
if (key(_s))
vse_get_position( id_voxelspace, &xb, &yb, &zb );
zb+=10;
set_p = 1;
end
if (key(_x))
vse_get_position( id_voxelspace, &xb, &yb, &zb );
zb-=10;
set_p = 1;
end
if (key(_d))
vse_get_position( id_voxelspace, &xb, &yb, &zb );
yb+=10;
set_p = 1;
end
if (key(_c))
vse_get_position( id_voxelspace, &xb, &yb, &zb );
yb-=10;
set_p = 1;
end
if (key(_f))
vse_get_position( id_voxelspace, &xb, &yb, &zb );
xb+=10;
set_p = 1;
end
if (key(_v))
vse_get_position( id_voxelspace, &xb, &yb, &zb );
xb-=10;
set_p = 1;

97
end

//Controles del voxelspace


if (key(_f1)) salto = 1; set_v = 1; end
if (key(_f2)) salto = 2; set_v = 1; end
if (key(_f3)) salto = 4; set_v = 1; end
if (key(_f5)) pasoMax = 100; set_v = 1; end
if (key(_f6)) pasoMax = 200; set_v = 1; end
if (key(_f7)) pasoMax = 400; set_v = 1; end
if (key(_f9)) scale = 1; set_v = 1; end
if (key(_f10)) scale = 4; set_v = 1; end
if (key(_f11)) scale = 8; set_v = 1; end
if (key(_f12)) scale = 16; set_v = 1; end

if (key(_q)) angulo += 5; set_v = 1; end


if (key(_w)) angulo -= 5; set_v = 1; end

//Si hay que setear lo hacemos


if (set_p)
vse_set_position( id_voxelspace, xb, yb, zb ); set_p = 0;
end
if (set_a)
vse_set_angle( id_voxelspace, alfa, beta ); set_a = 0;
end
if (set_v)
vse_set_voxelspace( id_voxelspace, angulo, salto, scrol,
pasoMax, scale);
set_v = 0;
end

//Avanzamos la camara
vse_advance( id_voxelspace, velocidad );

//Renderizamos la escena
map_clear(0, graph, 0);
vse_render_voxelspace( id_voxelspace );

if (key(_esc)) break; end


frame;
end
vse_quit();
end

¿Qué estamos haciendo en este ejemplo?

Primero ante todo estamos cargando la librería que acabamos de crear, el VSE.DLL a
través del import. Luego estamos declarando las variables privadas que vamos a usar a lo
largo del ejemplo. De ahí definimos la resolución a 320x200 a 16 bits. Ahora es
momento de crear el VoxelSpace con la función vse_new_voxelspace() cuyo valor
devuelto (que debe ser positivo) se almacenará en la variable id_voxelspace . Luego lo
seteamos a través de vse_set_voxelspace() con el identificador del VoxelSpace, el
ángulo de la cámara a 60º, que use saltos de 1 píxel por columna, que sea cíclico, de paso
máximo de 200 píxeles y con un factor de escala de 8.

98
Ahora es necesario cargar los respectivos mapas para representar nuestro VoxelSpace. El
primero que debemos de cargar es el Mapa de Alturas (test512h.map) y lo cargamos con
load_map que esta a su vez se guarda en GHMap:

y luego el Mapa de Texturas (test512t.map) y lo cargamos con load_map que esta a su


vez se guarda en GTMap:

Una vez listo, es momento de crear un nuevo mapa para el Render a través de new_map,
y lo llamamos graph. Ahora lo podemos setear con vse_set_map().

Ahora entramos al bucle repetitivo en donde controlaremos la cámara a través de los


ángulos alfa y beta al igual que los métodos que se renderízará en pantalla el VoxelSpace
como son los saltos, el maxStep, la escala, el ángulo de cámara y teniendo esto listo lo
seteamos a través de vse_set_position(), vse_set_angle (), y vse_set_voxelspace().

99
Se supone que tiene como valores de x en 255 y en y en 255, por consiguiente, nuestra
cámara que se sitúa en el VoxelSpace estará en la mitad de ese mapa al igual que tenemos
un valor de ángulo de alfa de 15º (un poco levantado hacia arriba) y el ángulo beta de 90º
(hacia el frente).

También tenemos establecida otra variable llamada velocidad que surge efecto al usarlo
en la función vse_advance() que es el reemplazo de la función advance() del modo 7. Y
por último renderizamos la escena con vse_render _voxelspace() pero antes debemos
hacer un map_clear() para actualizar la pantalla.

Eso es todo. Ahora si lo vemos en la salida quedaría algo como:

Y con los controles del teclado podemos modificar el renderizado de nuestro


VoxelSpace para que veamos los resultados que se efectúan a la hora de usar el
VoxelSpace.

100
3
Parte

Sobre el Modo 8
Extendido

101
7
Capítulo

La librería M8EE
I N T R O D U C C I Ó N

En esta sección veremos las funciones que nos ayudará a usar esta librería M8EE.

102
8
Capítulo

Herramientas para el Modo 8


Extendido
I N T R O D U C C I Ó N

En este capítulo veremos las herramientas que nos ayudará a usar esta librería M8EE.

103
9
Capítulo

Desarrollo de Video-Juegos
del tipo Modo 8 Extendido
I N T R O D U C C I Ó N

En este capítulo desarrollaremos un juego del tipo M8EE.

104
4
Parte

Apéndice

105
A
Apéndice

Redes en Fenix
I N T R O D U C C I Ó N

En este apéndice M8EE.

106
B
Apéndice

Referencias
E N L A C E S F É N I X

Official Site(Win32 ,MacOS, and Linux Port )

https://sourceforge.net/projects/fenix/

La página oficial de Fenix en donde encontrarás la última actualización de este lenguaje.

Fénix Pack

http://fenixpack.blogspot.com

A través de este paquete para novatos podemos descargar además de la última versión de
Fénix, las librerías que hemos usado a lo largo de este manual las cuales son el ETD, VSE y el
Modo 8 Extendido.

Foro DIVSITE

http://forum.divsite.net

Por supuesto la página del foro de Fénix, actualizada cada día y de visita obligatoria. Entérate
de las últimas noticias y proyectos sobre este fantástico proyecto de Fénix.

D E S A R R O L L O D E M O T O R ES 3 D

Good Looking Textured Light Sourced Bouncy Fun Smart and Stretchy Page.

http://freespace.virgin.net/hugo.elias

A través de esta página (En Inglés), nos adentraremos a tópicos bien interesantes que
hablan sobre el desarrollo de excelente motores 3D. La mayoría de ellos poseen
pseudocódigos que ayudan al programador a comprender mejor la implementación de
distintas técnicas de renderizado en 3D en diferentes lenguajes de programación. De
visita obligatoria.

107
Graficos 3d por ordenador

http://www.rastersoft.com/articulo/articulos.html

Esta página (Aunque el código esté escrito en Basic) nos explica sobre cómo diseñar
gráficos en 3ra Dimensión a través de diferentes técnicas como lo es el Flat Shading,
Gouraud Shading y entre otros.

Métodos de sombreado en 3D

http://rnasa.tic.udc.es/gc/trabajos%202002-03/sombreado/Selec_metodo.htm

Esta página en español nos explica sobre los diferentes tipos de sombreados que se
pueden aplicar en diferentes lenguajes de programación como lo es el C o C++. Es un
excelente punto de referencia por si quiere conocer a profundidad sobre estos tipo de
sombreados.

P R O G R A M A S G R A T U I T O S

Game Development Toolkit

http://gdt.sourceforge.net/
http://portalxuri.dyndns.org/gdt/index.html

Como lo dice su página web: GDT es una librería para hacer juegos 3D y 2D en C++,
basada en otras librerías de más bajo nivel: OpenGL, Irrlicht, OpenAL, OGG Vorbis.
Con GDT lo que se ha buscado es una simplicidad extrema, haciendo posible que un
usuario medio logre crear sus propios videojuegos con el poder y rendimiento de C++.
Además en esta página encontrarás los ejemplos de Demos ya compiladas en ejecutables,
un manual de referencia de funciones, foros y las últimas noticias de este entorno en 3D.

108
Epílogo
Carta abierta de Emilio de Paz (http://www.alcachofasoft.com)

A los desarrolladores independientes:

¿Por qué hacemos videojuegos?

No se trata de “Hacer el mejor motor del mercado”.Ni se trata de “Estamos haciendo


un juego que va a vender un montón”. No se trata de “Sorprender a los demás con la
idea que a nadie se le ha ocurrido”.

Aquí se trata de “¿Qué hay que usar con la polea para subir el cubo?”.Aquí hablamos de
“¡Mierda! ¡Me han matado! Voy a intentarlo otra vez, ahora no me va a pasar”.

En muchas ocasiones, nosotros mismos, los creadores de videojuegos, no somos


conscientes de lo que estamos haciendo. El proceso de desarrollo nos abarca y, estamos
tan inmersos en él, que quizá no nos damos cuenta del aspecto global

¿Por qué hacemos videojuegos? ¿De dónde sale ese impulso de CREAR?

Miremos hacia atrás, a nuestra infancia, adolescencia o juventud, y pensemos en los


primeros juegos que cayeron en nuestras manos. Los cargamos en nuestros viejos
ordenadores y nos pusimos a jugar con ellos... ¿Qué sentíamos?

No era “Qué bien optimizada está la memoria RAM”. No era “¿Cómo habrán
conseguido el naranja en un Spectrum?”.

Era el paso a un mundo mágico. Era Alicia a través del espejo. Es el mundo de las
sensaciones, de las emociones. De vernos atrapados por un juego. Sin más.

Sin “El personaje tiene más polígonos que el de Tomb Raider”. Sin “Las sombras molan
un mazo porque se arrojan a tiempo real”.Sin “Vamos a distribuirlo en Francia,
Alemania y Djibuti”.

109
Sí, los videojuegos son un medio, cada día más cercano al cine. Nos cuentan una historia,
nos atrapan en su mundo con sus gráficos, su sonido, su ambientación...

Pero hay algo que ningún otro medio tiene, al menos en tan gran medida: la
INTERACCIÓN. Y es que en aquellos videojuegos, sin importar el tipo de historia, o el
género del juego... NOSOTROS éramos el protagonista.

Y la historia podía terminar bien o mal, todo dependía de nosotros. No había un final
pre-fijado, no éramos simples espectadores: vivíamos una aventura. Era JUGAR, con
mayúsculas.

¿Cuántas veces en estos años nos hemos sentido un científico metido a Rambo, un
aprendiz de pirata o de mago, un piloto de carreras, o incluso una gran boca amarilla que
cuando se come unos puntos se encabrona y se zampa al primer fantasma que pilla? ¿Y
acaso no hemos sentido alegría cuando hemos triunfado? ¿Tristeza cuando hemos
fracasado? Júbilo, Odio, Rencor, Frustración, Humor, Amor...

Cuando éramos “profanos”, cuando no teníamos ni idea de lo que era un píxel, un


polígono o un puntero, un juego era para nosotros, conscientemente o no, una fuente de
EMOCIONES.

¿Por qué hacemos videojuegos? ¿De dónde sale ese impulso de CREAR?

En el fondo todos sabemos la respuesta: queremos hacer sentir al resto de la gente las
emociones que nosotros vivimos cuando jugamos a un videojuego. Porque, hora es ya de
decirlo, la creación de videojuegos es un ARTE, y por lo tanto nosotros somos unos
ARTISTAS. Y como artistas que somos llevamos a la gente, en forma de CD-ROM,
emociones fantásticas y maravillosas.

¿Qué es la “adicción”, la “jugabilidad”? ¿Acaso el que una persona no quiera dejar de


jugar a nuestro juego, o no deje de pensar en el puzzle que le hemos puesto, no evidencia
que esas sensaciones le llegan de la forma más directa posible?

No es “ver a un piloto de carreras” es SER un piloto de carreras. No es “¿encajarán las


piezas?” es ENCAJAR las piezas. No es “A ver cómo sale el bueno de ésta” es ¡¡¿Cómo
SALGO de ésta?!!

Y nuestra profesión por tanto, de programadores, diseñadores, músicos, grafistas,


directores... de ARTISTAS al fin y al cabo, es transmitir esas emociones.

No es, cómo muchos creen, “Soy un freaky que quiere demostrar que es el más listo”.
No es, cómo nosotros quisiéramos, “Voy a hacerme rico con este negocio tan moderno”.

110
Hacemos felices a los jugadores creando emociones para ellos. Lo que nosotros hacemos
es algo MUY IMPORTANTE. Somos ARTISTAS, haciendo un ARTE. Un arte nuevo,
que tiene un mercado que crece, que tiene un futuro espléndido, que muy poca gente es
capaz de realizar...

Desde aquí mi más sincero agradecimiento a todos los creadores por haberme
EMOCIONADO con sus juegos. El cine ha evolucionado mucho desde los hermanos
Lumiére (1895, año que se inventó el cinematógrafo). Nuestro arte nació hace poco más
de 30 años. Tenemos mucho camino por delante y será un placer recorrerlo juntos.

111

También podría gustarte