Tutorial de Ray casting 3: Falso 3D

Escrito por el 12 noviembre, 2017

En los tutoriales anteriores habíamos creado un juego basado en tiles, lo habíamos renderizado en un mini mapa, y le habíamos metido un primitivo pero funcional sistema de casteo de rayos. Es el momento de usarlo, para simular el render 3D.

Podeis ver el código en github aquí, y probar el juego con este enlace.

La idea general es la siguiente: En cada frame del juego lanzamos los rayos cubriendo proporcionalmente el fov del jugador, calculando por cada rayo en qué coordenada exacta impacta con una pared, y a la distancia a que lo hace con respecto al jugador. La idea es dibujar tantos rectángulos como rayos he lanzado, repartiéndose el ancho del canvas a partes iguales entre ellos, y el alto es la relación del alto del canvas con la distancia a la que colisiona el rayo que vamos a dibujar.

Las ideas

Si has estado siguiendo los tutoriales previos, ya te podrás imaginar lo que vamos a hacer. Sí, un nuevo render, el Render25D. Para programarlo, hay que enfrentarse a varios problemas. Veámoslos uno a uno.

El primero es ¿Cómo empiezo a dibujar el mundo? Pues bien, lo primero que se suele dibujar en los juegos 3D, es el entorno que rodea el escenario de juego, muchas veces dibujado “de forma diferente” al propio mapa. Es conocido como skybox. Los juegos modernos con mapeados enormes, casi todo el skybox forma parte del propio escenario,así que éste casi se reduce exclusívamente a renderizar el cielo. Antes esto no era así, y el skybox eran una o varias texturas que rodeaban el mapa jugable, y que el jugador nunca podía alcanzar, pero sí ver a “lo lejos”. El Wolfenstein discurre en los calabozos de un castillo nazi, siempre en interiores. ¿Tiene skybox? Pues podríamos decir que sí. Si miras con atención el vídeo del primer tutorial, verás que el techo y el suelo no cambian nunca. Serio candidato a ser el skybox de este experimento. Y si te paras a pensar, llegarás a la conclusión que no son más que dos rectángulos que dividen la pantalla horizontalmente por la mitad. El de arriba, es el techo, y el de abajo, el suelo.

¿Cómo dibujar los rayos? La idea es dibujar un rectángulo por cada rayo. Para dibujar cada rectángulo necesito su coordenada superior izquierda, un ancho, un alto, y un color.

El más fáci del calcular es el ancho. Si tiro N_RAYOS rayos, y mi pantalla mide ANCHO_CANVAS, entonces cada rectángulo debe tener de ancho ANCHO_CANVAS/ N_RAYOS.

El alto es un poco más sofisticado. Hay que obtenerlo relacionando el alto del canvas, con la distancia a la que ha chocado el rayo.La fórmula más simple es la siguiente: ALTO_CANVAS / DISTANCIA_RAYO: parece que funciona bien. Si rayo choca cerca, la distancia es poca y el resultado de la división sera grande. Si la distancia es lejana, la división me devolverá valores más pequeños cuanto más lejos. Funciona bien la mayoría de las veces, pero estarás deformando la dimensión vertical. Si tu canvas es muy alto en proporción a su ancho, tendrás la sensación de que estás estirando el mundo en su eje vertical, y si es al contrario, lo encogerás. Para solucionarlo, hay que complicar un poco la fórmula, teniendo en cuenta el FOV del jugador y solamente el ancho del canvas. La dimensión vertical estará en directa relación a ello, consiguiendo que tenga un aspecto similar sin importar la relación que haya entre el ancho y el alto de tu canvas.
La fórmula es la siguiente: (ANCHO_CANVAS / 2) / tangente(FOV / 2). Yo me la creí y la usé, y vivo feliz sin meterme en la trigonometría que hay detrás.

La coordenada, con los datos anteriores, es trivial. Estoy pintando rayos, asi que tendré un bucle FOR que los va procesando uno a uno. La primera coordenada X es 0, la segunda es el ANCHO_RAYO calculado antes, la tercera es ANCHO_RAYO * 2… pues INDICE_BUCLE * ANCHO_RAYO es la coordenada X de cada rayo. La coordenada Y es poco más. La idea es que la mitad vertical de cada rectángulo coincida con la mitad del alto del canvas. Así que la coordenada Y es ALTO_CANVAS / 2 – ALTO_RAYO / 2.
En cuanto al color, para no meter demasiada complicación, he puesto que todos los rectángulos sean amarillos. Pero esto producía un extraño efecto, pues costaba distinguir donde empieza una pared y acaba otra. Así que he optado por “oscurecer” el color del rayo según la distancia, con una fórmula matemática que pondré después. Así que cuanto más lejos estuviera el rectángulo, más oscurecía el color.

El código

Empezaré con la clase de matemáticas, aunque sólo las funciones que vamos a usar en este juego.

Todos los ángulos en un videojuego debe estar en radianes, que es la unidad con la que operan las funciones trigonométricas de javascript. Así que con una pequeña función, transformo los grados a los que yo estoy acostumbrado, a radianes.

La función interpolate es la que le dices un mínimo, un máximo, y un porcentaje entre (entre 0 y 1) y te calcula el valor del rango que corresponde al porcentage. Por ejemplo, en un rango de 10 a 20, el 0.5 es el 15.

La función limit es la típica utilidad que te devuelve un valor que está entre un máximo y un mínimo. Si el valor es mayor que el máximo, te devuelve el máximo. Idem con el mínimo.

Y la última función, es la de oscurecer colores. Tiene como argumentos el color en RGB en formato #XXXXXX, obligatoriamente, y un porcentage positico o negativo, entre 100 y -100, para abrillantar u oscurecer el color dado. Si te pasas de brillo, lo pone blanco, y si te pasas de oscurecer, negro.

No sufráis si tenéis dudas con alguna. Todas las fórmulas matemáticas se usarán en nuestro juego exclusivamente para oscurecer el color, a excepción de la que convierte grados a radianes, obviamente.

Ya estamos listos para ver la versión del render 2.5D:

El código es la implementación de las ideas que puse arriba. Divido el renderizador en dos, skybox por un lado, y paredes (rayos) por el otro. El skybox son dos rectángulos, aunque como voya oscurecer paredes, al color del suelo le he metido un degradado para que también se oscurezca. Pero la forma de dibujar el suelo y el techo son siempre igual, constantes.

En cuanto al código de las paredes, es tal cual dije arriba. Lo mas raro es el código para calcular el oscurecimiento del color y algún fix para que quede mejor.

Dicha clase render la meto en el cosntructor del juego principal, y llamo a su render en la etapa de renderizado. En el html, solo es meter un div más. Podeis ver le resultado mejor en github o en el código fuente de la demo, no creo que haga falta comentarlo por aquí.

Y con ésto, ya lo tenemos todo. Bueno, no. Hay un comportamiento inesperado que convendría solucionar.

El efecto ojo de pez

El el mundo de la óptica, hay un tipo de lente que produce un efecto llamado “fish eye”, en el que las imágenes se aplastan por los lados, y se agrandan por el centro.

Nuestro renderizado pseudo 3D presenta ese problema, aunque la solución es un realizar un truco matemático muy sencillo. A la hora de calcular la distancia, después de obtenerla, hay que multiplicarla por el coseno del ángulo que forma el rayo con la dirección a la que mira el jugador. La siguiente imagen lo deja más claro:

A nuestra clase RayCasting, en _castRay, este pequeño fix arregla el problema:

Aunque ahora, nuestra clase está devolviendo una distancia que no es real; está manipulada para que el renderizado 3D no tenga el problema del ojo de pez; pero si por lo que sea, en el futuro, alguna otra funcionalidad desea usar el casteador de rayos, no debería de falsear dicha distancia. De momento no lo voy a cambiar, pero es algo que probablemente tenga que mejorar en próximos tutoriales si quiero usar la clase RayCasting para otros usos diferentes, como por ejemplo, detectar si un disparo golpea en enemigos…

Y con ésto, ya está tó el pescao vendío, nunca mejor dicho.

Comentarios (2)

  • Hoy he entrado de nuevo en tu web para revisar el concepto de clases en jascript y vaya sorpreson me he llevado al ver que subes nuevos tutoriales!!
    Graciaaaas!!!

  • Javascript* ^^U

¿Tienes algo que decir?

Gestionado con Wordpress y Stripes Theme Entradas (RSS) | Comentarios (RSS)