Documentos de Académico
Documentos de Profesional
Documentos de Cultura
Volumen
50% Teoría
50% Práctica
Nivel
Avanzado
Manual de
Entornos 3D
INTRODUCCIÓN
Manual de Entornos 3D
InteractiveStation
Panamá
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
VÉRTICE 3D R E N D E R I Z A N D O O B J E T O S
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
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
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
E L M O T O R 3 D
Las secciones principales de un motor 3d (ya sea para desarrollar objetos 3D, como
Voxels, como Entornos 3D) son:
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
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
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:
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:
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:
6
• Foco de luz que afecta al objeto
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:
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
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…
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.
10
float y;
float z;
} etd_punto3d;
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:
//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).
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
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];
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.
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
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.
int _Nueva_Camara () {
int i;
char encontrado=0;
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:
Y por último, es necesario crearle una función que elimine la cámara liberando memoria.
F I L M A N D O A N U E S T R O OB J E T O 3 D
Una vez recolectada esa información, se procede a calcular el ángulo alfa y beta en
centésimas de grados:
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.
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
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.
//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.
int _Nuevo_Objeto () {
int i;
char encontrado=0;
NObjetos++;
18
static int ETD_Nuevo_Objeto (INSTANCE * my, int * params) {
return _Nuevo_Objeto ();
}
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.
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);
}
C R E A C I Ó N D E L A A R I S TA
De lo contrario se actualiza esa zona de memoria para crear las nuevas Aristas al Objeto
3D.
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.
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:
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.
// 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;
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:
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;
• 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:
yb = yini;
zb = zini;
yrot = (coseno[alfa] * yb) - (seno[alfa] * zb);
zrot = (seno[alfa] * yb) + (coseno[alfa] * zb);
xb = xini;
zb = zrot;
xrot = (coseno[beta] * xb) + (seno[beta] * zb);
zrot = -(seno[beta] * xb) + (coseno[beta] * zb);
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.
• 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;
}
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;
}
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.
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.
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.
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.
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:
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;
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().
//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)
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º:
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:
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:
42
Rotamos el Objeto 3D con los valores iniciales:
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:
if (pos.z<camara[id_camara].distancia_minima ||
pos.z>camara[id_camara].distancia_maxima) return;
if (BFC)
_BFC(id, id_camara);
else {
for (i=0;i<objeto[id].NCaras;i++) caraP[i].cara = i; NcarasP = i;
}
R E N E R I Z A N D O V É R T I C E S
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
X = 160 + 0 = 0
Y = 200 – 160 = 40
R E N E R I Z A N D O A R I S T A S
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
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:
45
Atenuación = 1– ((Distancia Total – Distancia Mínima)*Factor Atenuación)/1000000000
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 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;
}
}
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:
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;
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.
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
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;
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.
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
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;
49
}
break;
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;
}
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;
}
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…
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;
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;
}
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;
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:
**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:
//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
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( );
//Movemos el objeto
objeto[id_objeto].z = 200;
54
objeto[id_objeto].RenderMode = 8;
//Permitimos la salida
if (key(_esc)) exit(0,0); end
frame;
end
end
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.
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.
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( );
//Movemos el objeto
objeto[id_objeto].z = 200;
i=0;
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
//Permitimos la salida
if (key(_esc)) exit(0,0); end
frame;
end
end
58
Y para acceder a cada una de las cámaras hay que presionar F1, F2, F3 y F4:
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.
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;
//Rotamos el objeto
objeto[id_cubo].alfa += 200;
objeto[id_cubo].beta += 300;
objeto[id_cubo].gamma -= 200;
Al ejecutar el programa, veremos el cubo creado, y las teclas de este programa son:
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
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:
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
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:
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:
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.
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:
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.
//Descargamos el voxelspace
free(VoxSpaceIdents[params[0]]);
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
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
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:
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.
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:
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.
if(params[0]>=NVoxSpaceIdents || VoxSpaceIdents[params[0]]==NULL)
return -1;
74
VoxSpaceIdents[params[0]]->TMap = TMap;
VoxSpaceIdents[params[0]]->HMap = HMap;
VoxSpaceIdents[params[0]]->RMap = RMap;
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.
VoxSpaceIdents[params[0]]->x = params[1];
VoxSpaceIdents[params[0]]->y = params[2];
VoxSpaceIdents[params[0]]->z = params[3];
return 1;
}
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.
76
else if (params[1] < -90000) params[1] = -90000;
while(params[2]>=360000) params[2]-=360000;
while(params[2]<0) params[2]+=360000;
Y por último, tenemos a vse_get_angle que con ella obtenemos los ángulos en que
mira la cámara del voxelspace id_voxelspace.
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:
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.
if(params[1]==VoxSpaceIdents[params[0]]->x &&
params[2]==VoxSpaceIdents[params[0]] ->y &&
params[3]==VoxSpaceIdents[params[0]] ->z) return -2;
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;
}
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.
**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
//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:
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.
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:
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.
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:
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.
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:
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.
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.
// 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:
86
raycast_ang = vp_ang_y + VoxSpaceIdents[i]->ini_angle;
if (raycast_ang>VoxSpaceIdents[i]->a360)
raycast_ang -= VoxSpaceIdents[i]->a360;
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:
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.
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
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.
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.
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.
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:
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;
// avanzamos el rayo
x_ray+=dx;
y_ray+=dy;
z_ray+=dz;
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.
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):
91
int dslope = VoxSpaceIdents[i]->dslope;
// Convertimos a fixed
vp_x = (vp_x << FIXP_SHIFT);
vp_y = (vp_y << FIXP_SHIFT);
vp_z = (vp_z << FIXP_SHIFT);
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));
//Avanzamos el rayo
z_ray+=curr_voxel_scale;
93
curr_step = VoxSpaceIdents[i]->max_steps;
break;
} // end if
// avanzamos el rayo
x_ray+=dx;
y_ray+=dy;
z_ray+=dz;
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:
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
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
//Posición de la camara
xb = 256;
yb = 256;
zb = 150;
//Angulos de la camara
alfa = 15000;
beta = 90000;
//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);
//Creamos el rendermap
graph = new_map(320,200,8);
set_center(0, graph, 0, 0); //Por comodidad ponemos el centro en 0,0
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
//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
//Avanzamos la camara
vse_advance( id_voxelspace, velocidad );
//Renderizamos la escena
map_clear(0, graph, 0);
vse_render_voxelspace( id_voxelspace );
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:
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().
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.
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
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
104
4
Parte
Apéndice
105
A
Apéndice
Redes en Fenix
I N T R O D U C C I Ó N
106
B
Apéndice
Referencias
E N L A C E S F É N I X
https://sourceforge.net/projects/fenix/
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
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)
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”.
¿Por qué hacemos videojuegos? ¿De dónde sale ese impulso de CREAR?
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...
¿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.
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