Preparando nuestro lienzo

Programando un motor 3D desde cero - Brakeza3D

Preparando nuestro lienzo

Pantallas y píxeles

Cualquier cosa que ves en tu pantalla está formada por píxeles. Todos hemos escuchado esta palabra y tenemos una idea clara de que representa y para no complicarnos demasiado diremos que un píxel es un punto en pantalla.

Si queremos programar nuestro propio motor3d será inevitable disponer de la capacidad de dibujar píxeles en pantalla a nuestro antojo, suena evidente.

Con cualquier tecnología capaz de dibujar pixeles en pantalla podréis programar vuestro motor3d, ya sea un Canvas HTML5 mediante JS o como será nuestro caso, a través de la librería 2D SDL2.0 con C++.

En el caso de Brakeza3D se ha elegido C++ por muchas razones de rendimiento que podría mencionar, pero sin duda el elemento nostalgia ha jugado un papel fundamental. Sin ir mas lejos la librería SDL está disponible en multitud de lenguajes y tampoco es la única disponible para este fin. Respecto a la elección de SDL como librería 2D, es también una elección personal, existiendo múltiples alternativas que servirían para nuestros objetivos.

Las ventajas de mas peso serían la gestión de ventanas con el sistema operativo, carga de imágenes y una capa de abstracción para I/O y sonido que aunque no se incluyen en la idea original de Brakeza3D, me permiten soñar en incluir estas características al motor algún día sin cambiar de tecnología. De esta forma podemos despreocuparnos de la programación relativa a gestionar nuestra ventana principal contra el sistema operativo anfitrión independientemente de la plataforma en la que se ejecute.

 

Lo primero que necesitamos para programar nuestro motor 3D es dibujar un píxel en pantalla

Primeros pasos con SDL

Si estás interesado en profundizar en las funcionalidades que ofrece SDL, te recomiendo por encima de todos, la serie de tutoriales de Lazy Foo’ Productions.

El esquema de una aplicación basada en SDL es muy común:

//... iniciar ventana
while(!finish) {
    while (SDL_PollEvent(&e)) {
        //... procesado de eventos
    }
    //... actualizar ventana
}
//... cerrar ventana

Se puede observar como este código se ejecutaría sin parar hasta que la variable finish tenga valor false.  Este bucle hará gran parte del trabajo en nuestro engine y siempre es un buen punto de partida para analizar otros proyectos hechos con SDL. Llamaremos a este bucle main loop y podemos imaginar que cada iteración en él equivale a un frame en nuestro engine.

La ventana

Inicializar una ventana con SDL es un proceso muy sencillo, deberemos indicarle la resolución y cumplimentar unos flags en función de nuestras necesidades.

Veámos un ejemplo:
//Create window
SDL_window *window = SDL_CreateWindow(
"Hola mundo!",
SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED,
640,
480,
SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_MAXIMIZED
);
En este ejemplo se puede observar que especificamos la <strong>posicion inicial</strong>, su <strong>resolución</strong> y ademas le indicamos opciones extra como que esté <strong>maximizada</strong>, sea <strong>redimensionable</strong> y que se renderize en un contexto <strong>OpenGL</strong>. Podréis encontrar mas información de los flags disponibles en la <a href="https://wiki.libsdl.org/SDL_CreateWindow">documentación oficial de&nbsp;SDL y CreateWindow</a>

Este método es el mas formal, existen múltiples formas de inicializar una ventana en SDL según nuestras necesidades. Mas adelante veremos algunas de ellas.

Puedes observar el código de la creación de una ventana SDL:
Watch

¿Como dibujar un pixel en nuestra ventana?

Hasta aquí habremos conseguido un lienzo en blanco (en negro en nuestro caso), sobre el que ya podemos dibujar un pixel en pantalla. Antes de continuar, cabe destacar que la forma en la que operaremos con los píxeles en pantalla será a través de un simple array lineal de [ancho*alto] de longitud con el que poder manipular cada pixel mediante sus elementos. Llamaremos a este array nuestro buffer de video.

El tamaño de dicho buffer variará según la resolución, pues no será lo mismo el que necesitamos para una ventana de  320×240, que 640×480, etc. En resumidas cuentas, nos encontraremos constantemente accediendo a nuestro buffer de video y manipulando cada pixel con un color determinado de la siguiente forma:

bufferVideo[posicion_y * ANCHO_VENTANA + posicion_x] = 0xFFFFFF;

El contenido de cada píxel es un UInt32. El tipo de dato Uint32 nos permite almacenar 4 bytes de información, uno por cada componente RGBA de un color. Este será el formato utilizado para los colores en nuestro motor

Además del concepto de ventana , en SDL existen dos estructuras fundamentales para pintar gráficos en pantalla una es SDL_Render y la otra SDL_Surface. Por ahora nos servirá con entender que actuan como el video buffer que utiliza nuestra ventana. Una aplicación gráfica con SDL necesita al menos o un SDL_Render o una SDL_Surface para mostrar algo en pantalla a través de nuestra ventana.

Explicación sobre código

Analizamos el código que tenéis disponible en el repositorio XX con el que conseguiremos los objetivos de este primer artículo, que es iniciar una ventana en la que poder pintar nuestros píxeles:

#include <iostream>
#include <SDL.h>
 
#define WINDOW_WIDTH 640
#define WINDOW_HEIGHT 480
 
int main() {
SDL_Event event;
SDL_Renderer *renderer;
SDL_Window *window;
 
// Iniciamos la ventana
SDL_Init(SDL_INIT_VIDEO);
SDL_CreateWindowAndRenderer(WINDOW_WIDTH, WINDOW_HEIGHT, 0, &window, &renderer);
 
// Limpiamos la pantalla
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0);
SDL_RenderClear(renderer);
 
SDL_SetRenderDrawColor(renderer, 255, 0, 0, 255);
SDL_RenderDrawPoint(renderer, 10, 10);
SDL_RenderPresent(renderer);
 
while (1) {
    if (SDL_PollEvent(&event) &event.type == SDL_QUIT)
    break;
}
 
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
SDL_Quit();
return 0;
}

Antes de nada aclarar que por la simplicidad del ejemplo nos ahorramos la utilización de un buffer de video y el método de acceso a cada pixel mencionado anteriormente. En su lugar trabajamos directamente sobre el elemento SDL_Render asociado a la ventana lo que produce el efecto deseado de dibujar el pixel y por ahora es suficiente.
Como veremos en capítulos posteriores, Brakeza3D trabaja con varios buffers ademas del de video, que se combinan al finalizar la renderización y se vuelca al SDL_Render asociado a la ventana de un golpe.

Vamos con el código:

SDL_Event event;
SDL_Renderer *renderer;
SDL_Window *window;

Lo mínimo en una aplicación SDL: El objeto SDL_Window, el objeto SDL_Render que asociaremos a nuestra ventana y un SDL_Event que representa el sistema de eventos de SDL. Serán variables existentes en toda la aplicación.

SDL_Init(SDL_INIT_VIDEO);SDL_CreateWindowAndRenderer(WINDOW_WIDTH, WINDOW_HEIGHT, 0, &window, &renderer);

Se levanta el sistema de video y se crea una ventana que automáticamente se vincula a nuestro renderer. Cabe destacar que no hemos escrito ningún control de errores en éste ejemplo.

SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0);
SDL_RenderClear(renderer);

Activamos el color RGB = 0x000000 cuyo canal ALPHA es también 0 y limpiamos la pantalla con el color activo.

SDL_SetRenderDrawColor(renderer, 255, 0, 0, 255);
SDL_RenderDrawPoint(renderer, 10, 10);
SDL_RenderPresent(renderer);

Activamos el color RGB = 0xFF0000 cuyo canal ALPHA es 255. Pintamos un pixel en nuestro renderer en la posixión x=10, y=10 y finalmente hacemos SDL_RenderPresent con nuestro render, lo que llevará los cambios a nuestra ventana.

while (1) {
    if (SDL_PollEvent(&event) &event.type == SDL_QUIT)
    break;
}

Nuestro main loop por ahora no hace mas que evaluar si se activa el evento SDL_QUIT en cuyo caso haría break. En la práctica mantendrá la ventana activa hasta que la cierres
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
SDL_Quit();

Terminamos destruyendo los objetos creados anteriormente y cerramos el sistema de video con SDL.

Podéis descargar el ejemplo al completo donde se dibuja un pixel en la ventana:
Watch 

Resumen

Ya podemos pintar pixeles en pantalla a nuestro antojo. Se trata de nuestra primitiva fundamental, el punto. Con ello dibujaremos todo un entorno 3D en próximos capítulos.

Además hemos visto que nuestra aplicación tiene un main loop en el que a través de un renderer actualizaremos el contenido de nuestra ventana cuando deseemos.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *