Notxor tiene un blog

Defenestrando la vida

Un pequeño raytracer

Notxor
2020-10-21

Hoy vengo a hablaros de un pequeño proyecto, ─otro─, referido a una de mis ancestrales aficiones. Digo ancestrales porque viene de muy atrás, de hace muchos años, de cuando el mundo era joven y los ingenuos humanos no se conectaban con sus ordenadores a Internet nada más encenderlos: había que encender el módem y escuchar una serie de pitidos (y no otra) antes de ello. De cuando la información la compartíamos en disquetes de forma habitual y el conocimiento lo adquiríamos de los libros. Éste va a ser un proyecto de porque sí, porque me apetece y porque no es necesario hacerlo, ni sirve para nada: el típico proyecto que me gusta para aprender más.

Quien me conoce de hace tiempo sabe de mi pasión con los trazadores de rayos. Algunos recordarán algún artículo publicado junto con el autor de YafRay1, en alguna jornada de Blendiberia2, aunque mi afición a este tipo de herramientas es muy anterior.

libro-povray.png

En la imagen muestro un libro que aún conservo y que aún recuerdo cómo lo compré allá por el 1996 o el 19973. Recuerdo que fue en La Casa del Libro, yo estaba de visita en Madrid, y sólo existían dos Casa del Libro: la original de Gran Vía y otra de baratillo frente a Cortilandia. Lo recuerdo porque me llamaron, de alguna manera, la atención. Llegué a las estanterías con el ánimo de mirar libros de informática. En aquel entonces estaba interesado más en programación y trasteaba con C/C++ y ensamblador. Recuerdo también que mirando entre los libros me llamó la atención el subtítulo del libro de arriba. Había tocado algo de AutoCAD, aunque no mucho. En aquella época, dicha herramienta, era 2D, dibujabas planos para imprimirlos en papel con un plotter y poco más. Lo del 3D me resultó curioso. Lo cogí de la estantería, miré la portada y me pareció muy prometedora: Una imagen resultona (impresionante en aquella época), «código fuente para generar imágenes», «incluye disquete»... lo abrí. Comencé a hojearlo casi página a página, tanto rato le eché que terminé sentado en el suelo, mirando las imágenes, ─algunas impresionantes─, las fórmulas matemáticas, los listados de código y tanto tiempo estuve allí sentado que finalmente un empleado se acercó a ver qué estaba haciendo. «¿Vas a comprarlo o te lo quieres aprender ahí sentado?», me preguntó. A punto estuve de tirarle el libro a la cabeza por distraerme, pero sonrió con la expresión de quien sabe que tiene la venta hecha. Pagué con tarjeta, no llevaba efectivo suficiente para pagar el libro y me fui. Recuerdo que había quedado con unos amigos y me pasé el tiempo deseando que acabara el fin de semana para irme a casa donde estaba mi ordenador para enchufarle mi disquete de «Pov-Ray 2.2» y probarlo.

Desde entonces ha llovido mucho, pero siempre estas herramientas siempre me han parecido casi mágicas. Sé cómo funcionan, entiendo los entresijos del funcionamiento y las matemáticas que los soportan. Incluso años después participé en un intento de hacer un raytracer cuyo objetivo era simular más perfectamente el funcionamiento de una cámara fotográfica. Había llegado a NicoDigital, un foro sobre Blender3D ya desaparecido. Recuerdo que aquel intento lo promovió Javier Belanche y quería llamarlo como su pintor favorito: vermeer. Incluso me envió un paquetón de fotocopias con artículos y capítulos de libros que había recolectado sobre cómo hacer un raytracer, la mayoría en inglés.

Entre unas cosas y otras el tema se enfrió, los tres que estábamos no teníamos muy claro cómo empezar: uno estudió psicología, otro bellas artes y ningún informático en el equipo, acabó con un intento muy fallido.

No fue el último. De vez en cuando, encuentro algún tutorial de cómo hacer un raytracer donde encuentras mucho pseudocódigo y algo de código, pero el poco código que hay no funciona, está obsoleto o te faltan librerías; las explicaciones no son muy claras con introducciones largas y farragosas plagadas de teoría y matemáticas que yo puedo obviar, aunque las repase de todos modos. Cuando he conseguido algo ha sido casi de casualidad y obteniendo un código poco flexible y extensible, más digno de la basura que de comenzar un proyecto serio.

Nuevo intento

Hace poco llegué a otra página web: https://raytracing.github.io/ donde presenta tres libros4. Después de ojearlos los tres (son muy cortitos) comencé por el primero: Ray Tracing in One Weekend.

En el título promete hacer un raytracer en un fin de semana... muy poco tiempo me parece. No digo que no haya alguien capaz de hacerlo, pero no creo que esté a mi alcance. Llevo un par de ratos leyendo y picando código, pero estoy convencido que lo del fin de semana será si picas código sin parar para dormir, ni comer, ni cagar.

El caso es que ya tengo algo picado, no mucho. Pero ha sido prometedor: primera explicación, primer código metido y primera imagen conseguida. Vale que aún no he trazado un sólo rayo pero me anima el haber seguido las explicaciones y haber conseguido el resultado esperado. Teniendo en cuenta además, que el código que el libro propone es C++ y yo lo estoy haciendo en erlang. ¿Por qué? Porque al verlo me he imaginado lo que puede ser un proceso imagen lanzando procesos rayos que se distribuyen por varias máquinas en distintos nodos de una red y conseguir hacer un raytracer distribuido sin las complicaciones de otros lenguajes.

En contra me tropiezo con el problema de convertir el código POO de C++ a código funcional de erlang.

El ejemplo conseguido hasta ahora

prueba.png

Vale, es poca cosa pero funcionó a la primera, a pesar de que a las explicaciones que da el «libro» le he hecho algunos cambios. Concretamente he separado la generación del gráfico, escribiéndolo directamente a un fichero, del contador que muestra el avance del render. En el libro, utiliza el flujo normal de salida para escribir el fichero y el flujo de error para indicar el avance5. Además, le he añadido un lanzador de nombre rerl, que viene a ser raytracerl más corto... y que lo he acortado también porque me imaginaba a Joaquín Reyes presentándolo al grito de raytraceeeerrllll imitando a cualquier informático famoso6 con acento manchego y no me resultaba serio.

El primer problema que me encontré es que al hacerlo en erlang había que lanzar la máquina virtual, cargar el módulo y llamar a la función que dibuja la imagen. La solución: hacer un lanzador que lo haga por nosotros en forma de escript:

#!/usr/bin/env escript
%% -*- erlang -*-
%%! -smp enable -sname lanzador
main([Nombre_Fichero]) ->
    imagen:dibujar(Nombre_Fichero);
main([]) ->
    imagen:dibujar().

Como se ve, el módulo que lleva el cotarro se llama imagen y tiene una función que se llama dibujar. Si se le pasa un nombre de fichero, llama a imagen pasándole ese nombre donde debe escribir el resultado. En otras ocasiones, cuando hago un escript, suelo intentar controlar algún posible error, como que falte algún parámetro. Sin embargo, en este caso, como es una herramienta de lanzamiento para pruebas y cualquier fallo es importante, prefiero no manejar error y que me informe de los detalles fallo que ha ocurrido.

Por otro lado, el código que genera la imagen que he mostrado más arriba es el siguiente:

-module(imagen).

-define(ANCHO,256).
-define(ALTO,256).
-define(FICHERO,"imagen.ppm").

-export([
          dibujar/0
        , dibujar/1]).

dibujar() ->
    dibujar(?FICHERO).

dibujar(Fichero) ->
    Cabecera = io_lib:format("P3~n~p ~p~n255~n", [?ANCHO,?ALTO]),
    {ok,F} = file:open(Fichero, [write]),
    B = 63,
    Imagen =
        lists:map(
          fun(G) ->
                  io:format("Faltan ~p filas  \e[36D", [G]),
                  lists:map(
                    fun(R) ->
                            io_lib:format("~p ~p ~p~n", [R,G,B])
                    end,
                    lists:seq(0,255))
          end,
          lists:seq(255,0,-1)),
    file:write(F, Cabecera ++ Imagen),
    file:close(F),
    io:format("\nFin.\n").

Muy sencillo de entender: el módulo imagen está compuesto por dos funciones con el mismo nombre, dibujar, que puede llevar el parámetro del nombre de fichero o no. En caso de no llevar, utiliza la constante ?FICHERO para llamar a la función con parámetro.

Por el nombre del fichero por defecto se puede ver que utilizamos el formato de imagen ppm, que es un formato de imagen definido mediante texto ASCII y si no tienes muy claro cómo funciona, lo voy a explicar más despacio. Veamos la cabecera:

P3
256 256
255

La cabecera que genera el programa se divide en las siguiente líneas7:

  • Número mágico: en la familia de ficheros gráficos denominados como portable pixel format PPM, los dos caracteres P3 indican que es un fichero ASCII que contiene valores RGB.
  • Ancho y alto: en pixeles de la imagen. En este caso la imagen tendrá \(256 \times 256\) es decir un cuadrado de 0 a 255 píxeles de lado.
  • Valor máximo del color: en este caso el valor de cada componente puede llegar hasta 255, el mínimo siempre es 0.

A partir de ahí, en el ejemplo, cada punto será dibujado por una combinación RGB tal que los componentes R y G se toman de dos contadores obtenidos mediante lists:seq y el valor B se mantiene constante en 63. Luego crea una cadena por cada punto, uno por línea, con la instrucción io_lib:format("~p ~p ~p~n", [R,G,B]).

Al final, cuando tiene la cabecera y la imagen preparadas, las escribe en un fichero y después lo cierra guardando el contenido.

Simulando clases con erlang

Lo siguiente que hace el librillo es crearse algunas clases básicas: en realidad una clase vec3 para almacenar y operar valores representados por 3 valores individuales. Lo utiliza tanto para guardar valores de vectores, propiamente dichos, como para guardar colores. En erlang podría utilizar tuplas con el formato {A,B,C} y luego operar la tupla según el concepto que almacene. Sin embargo, creo que me irá mejor no mezclar las churras y las merinas.

No lo he mirado, pero seguramente en el futuro, el autor, separará esos conceptos derivando clases, por lo que voy desobedecer un poco la línea argumental inicial del tutorial y voy crear los dos conceptos separados. Podría, por ejemplo, crear tuplas del estilo {color,{r,g,b}} o {vec3,{x,y,z}}, pero ésto es la definición de un registro o record. Como además supongo que esas definiciones las necesitaré en todos los módulos que se irán creando, las pongo en un fichero aparte que llamo registros.hrl y que presenta el siguiente contenido:

-record(vec3,  {x,y,z}).
-record(color, {r,g,b}).

La clase vec3

Como digo, el autor crea la clase vec3 que en mi caso, he decidido separar en dos: vec3 y color. En su código fuente, crea algunos operadores para operar con ellos y también algunos alias como point3 y color, definiendo después algunas funciones auxiliares más: las típicas de sumar vectores o multiplicarlos, operar también con escalares, etc. Mi módulo lo he dejado así:

-module(vec3).

-include("registros.hrl").

-export([
         nuevo/3
       , suma/2
       , dife/2
       , mul/2
       , divi/2
       , cruz/2
       , punto/2
       , modulo_cuadrado/1
       , modulo/1
       , unidad/1]).

nuevo(X,Y,Z) ->
    #vec3{x=X,y=Y,z=Z}.

suma(V1,V2) ->
    nuevo(V1#vec3.x + V2#vec3.x,
          V1#vec3.y + V2#vec3.y,
          V1#vec3.z + V2#vec3.z).

dife(V1,V2) ->
    nuevo(V1#vec3.x - V2#vec3.x,
          V1#vec3.y - V2#vec3.y,
          V1#vec3.z - V2#vec3.z).

mul(Vector, X) ->
    nuevo(Vector#vec3.x * X,
          Vector#vec3.y * X,
          Vector#vec3.z * X).

divi(Vector, X) ->
    mul(Vector, 1/X).

%% Producto vectorial
cruz(V1,V2) ->
    nuevo(V1#vec3.x * V2#vec3.x,
          V1#vec3.y * V2#vec3.y,
          V1#vec3.z * V2#vec3.z).

%% Producto escalar
punto(V1,V2) ->
    V1#vec3.x * V2#vec3.x +
    V1#vec3.y * V2#vec3.y +
    V1#vec3.z * V2#vec3.z.

modulo_cuadrado(V) ->
    V#vec3.x*V#vec3.x + V#vec3.y*V#vec3.y + V#vec3.z*V#vec3.z.

modulo(V) ->
    math:sqrt(modulo_cuadrado(V)).

unidad(V) ->
    divi(V, modulo(V)).

Tengo que duplicar algo de código, aunque no todo, para el módulo color. Por lo menos lo que el libro define en la cabecera y la que luego utiliza para escribir el color en el fichero. El módulo me ha quedado así:

-module(color).

-export([
          nuevo/3
        , escribe/1
        , mul/2
        , suma/2]).

-include("registros.hrl").

nuevo(R,G,B) ->
    #color{r=R,g=G,b=B}.

escribe(C) ->
    io_lib:format("~p ~p ~p~n", [round(255*C#color.r),round(255*C#color.g),round(255*C#color.b)]).

mul(C,X) ->
    nuevo(C#color.r * X,
          C#color.g * X,
          C#color.b * X).

suma(C1,C2) ->
    nuevo(C1#color.r + C2#color.r,
          C1#color.g + C2#color.g,
          C1#color.b + C2#color.b).

Por último, está el código del módulo imagen, es decir, el código que genera el resultado final utilizando los módulos8 ha quedado así:

-module(imagen).

-define(ANCHO,256).
-define(ALTO,256).
-define(FICHERO,"imagen.ppm").

-include("registros.hrl").

-export([
          dibujar/0
        , dibujar/1]).

dibujar() ->
    dibujar(?FICHERO).

dibujar(Fichero) ->
    Cabecera = io_lib:format("P3~n~p ~p~n255~n", [?ANCHO,?ALTO]),
    {ok,F} = file:open(Fichero, [write]),
    Imagen =
        lists:map(
          fun(J) ->
                  io:format("Faltan ~p filas  \e[36D", [J]),
                  lists:map(
                    fun(I) ->
                            Color = color:nuevo(I/(?ANCHO - 1), J/(?ALTO - 1), 0.25),
                            color:escribe(Color)
                    end,
                    lists:seq(0,255))
          end,
          lists:seq(255,0,-1)),
    file:write(F, Cabecera ++ Imagen),
    file:close(F),
    io:format("\nFin.\n").

El código, de momento, no es nada concurrente, sigue el desarrollo y las explicaciones del libro que están muy dirigidas por un lenguaje POO como C++. El resultado obtenido con éstos últimos cambios es el mismo que mostré al principio, pero se le ha añadido una capa de modularidad con objetos. Desde el punto de vista de erlang seguramente, el proceso estrella podría ser imagen que fuera lanzando procesos color o pixel que calculen el valor para cada punto de la imagen. De momento, no hay cálculos complejos y es todo muy lineal, pero creo que puede ser una línea de desarrollo futuro. Ya veremos por dónde continúa el libro y hacia dónde me obliga a tirar a mí.

Otro problema sobrevenido es el tema de la compilación de los módulos. Los cambios en este último procesamiento de la imagen me funcionaron al tercer intento, no porque tuviera ningún error, sino porque había módulos sin compilar. Necesito un Makefile. Además está todo metido en el mismo directorio, los fuentes y los .beam y todo a mogollón. La verdad es que es un poco farragoso. Para evitarlo he metido todo el código fuente en un directorio src y creado un Makefile que lo compila y lo guarda en un directorio ebin:

.PHONY: clean compilar all

DIR_SRC = ./src
DIR_BIN = ./ebin

LANZADOR = rerl

SOURCES = vec3.erl \
          imagen.erl \
          color.erl \
          $(LANZADOR)

TARGETS = $(DIR_BIN)/vec3.beam \
          $(DIR_BIN)/imagen.beam \
          $(DIR_BIN)/color.beam \
          $(DIR_BIN)/$(LANZADOR)

compilar: $(DIR_BIN) $(TARGETS)

clean:
        rm -rf $(DIR_BIN)

$(DIR_BIN)/%.beam: $(DIR_SRC)/%.erl $(DIR_SRC)/registros.hrl
        erlc -o $(DIR_BIN) $<

$(DIR_BIN)/$(LANZADOR): $(DIR_SRC)/$(LANZADOR)
        cp $(DIR_SRC)/$(LANZADOR) $(DIR_BIN)/$(LANZADOR)

$(DIR_BIN):
        mkdir $(DIR_BIN)

all: compilar

Esto del Makefile y la organización de los directorios, no viene en el libro, pero me parecía necesario para aclararme yo. De esta forma sigo también las estructuras de directorios habituales en erlang: los fuentes en un directorio src y los binarios listos para ejecutarse en un directorio llamado ebin.

Sólo recordaros, si alguien copia el Makefile del código anterior seguramente no os funcione. El pasar de texto a html y de html a texto puede haber hecho desaparecer los tabuladores. Recordad que make utiliza tabuladores y no se pueden sustituir por espacios, editar el fichero, borrad la indentación y hacedla con tabuladores.

Y creo que por hoy ya os he cansinado lo suficiente con mis cosas y proyectos de aprendizaje.

Conclusiones

Primera aproximación a un tutorial que promete explicar, por lo menos a priori, cada paso de forma clara y directa. No puedo afirmar si el código que escribe el autor es correcto y compila en todos los casos, porque estoy utilizando otro lenguaje para realizar los ejercicios que propone y tengo que escribir mi propio código. Recordad: en mi máquina funciona.

El utilizar erlang y no C++ puede representar un problema importante más adelante cuando se meta de lleno en el uso de clases y otras características de la programación orientada a objetos que tendré que adaptar a la programación funcional. Como ejercicio parece un interesante juego intelectual. Al principio me preocupará más entender cómo funciona todo el sistema y no tanto la eficiencia, sin embargo, intentaré también ir preparando el código para optimizarlo.

El formato de salida del fichero se mantiene durante los tres libros, por lo que he podido ver. No es un formato muy eficiente, ocupa bastante en disco, pero es sencillo de escribir y de leer.

De momento es muy pronto para saber qué módulos se convertirán en el futuro en procesos de erlang para ser procesados de forma concurrente. A bote pronto, creo que serán los píxeles de imagen y los rayos que se envían para calcular su color. Pero aún no hemos llegado a trazar ningún rayo... que, espero, lo haré en una próxima entrega de estos apuntes.

Por último, ya disculparéis si utilizo el blog como mi libreta de notas. Alguien puede aburrirse con esto, pero quizá a otros les sirva de inspiración o incluso para resolver alguna duda.

Footnotes:

1

Ahora llamado Yafaray https://www.yafaray.org

2

Cuando empezamos a reunirnos unos pocos aficionados, antes de que se profesionalizara el evento.

3

La fecha no la recuerdo de forma exacta, lo demás sí.

4

No muy largos pero al parecer sí muy prácticos.

5

Es una solución ingeniosa, pero no me parece limpia.

6

Si te lo imaginas con Stallman es inquietante... habría que proponérselo a Joaquín Reyes.

7

Pueden ser cadenas separadas por espacios simplemente, pero he preferido dividirlas en líneas para visualizarlo mejor.

8

En realidad, de momento, sólo utiliza el módulo de color, sin embargo en el código en C++ es relevante, porque utiliza vec3 para colores también.

Categoría: erlang raytrace

Comentarios

Debido a algunos ataques mailintencionados a través de la herramienta de comentarios, he decidido no proporcionar dicha opción en el Blog. Si alguien quiere comentar algo, me puede encontrar en esta cuenta de Mastodon, también en esta otra cuenta de Mastodon y en Diaspora con el nick de Notxor.

Si usas habitualmente XMPP (si no, te recomiendo que lo hagas), puedes encontrar también un pequeño grupo en el siguiente enlace: notxor-tiene-un-blog@salas.suchat.org

Disculpen las molestias.