Sobrecarga de #macros en Pov-Ray

En este tutorial o artículo, según se precie, aprenderemos a simular la sobrecarga de métodos, tan habitual en programación, con las #macros de Pov-Ray.

Requisitos

Para seguir este tutorial es necesario tener claros los conceptos de #macro y array en Pov, o al menos una pequeña base sobre ellos. Si se sabe algo de programación en algún lenguaje como C o Java, pues mejor, aunque se tratará de explicar lo mejor posible el concepto de sobrecarga de métodos, que es lo que vamos a intentar simular aquí.

Ya que este no es un tutorial sobre programación, sino sobre Pov-Ray, espero no ofender a ningún programador con lo que exprese aquí en relación a dicho tema. De todas formas intentaré hacerlo lo mejor posible.

Conceptos básicos

¿Qué es la sobrecarga de métodos?

Pues a grandes rasgos se podría decir que es una característica de algunos lenguajes de programación, como C o Java por citar algunos, que consiste en poder definir varias funciones con el mismo nombre y distintos argumentos. Supongamos que tenemos un programa en un lenguaje parecido a C que dibuja cajas. Pues podriamos tener una función para dibujar una caja tal que así:

void draw_box(int x, int y, boolean centered){
...
}

Esta función acepta tres parámetros (o argumentos, como más guste), que son la anchura, la altura de la caja y un booleano (tipo que admite sólo dos valores: false o true) indicando si la caja debería estar centrada en el eje de coordenadas. Podríamos desear que cada vez que dibujaramos una caja la dibujara centrada, y no tener que poner siempre un true en el tercer parámetro. Pues para eso podriamos definir otra función como esta:

void draw_box(int x, int y){
  draw_box(x, y, true);
}

Esta función tan sólo tiene dos argumentos y dibujaría la caja con las dimensiones pasadas como parámetros y centrada por defecto (o sin centrar como quisieramos). Como se puede ver, la segunda función lo único que hace es llamar a la primera con los parámetros correspondientes.

Esto es totalmente correcto en algunos lenguajes de programación y da mucha comodidad, a la hora de programar, ya que no hay que pasar todos los parámetros a la función cada vez que se usa. Suelen haber funciones con muchos parámetros y sería tedioso tener que pasarselos todos cada vez que vamos a usarla, cuando sólo queremos pasarle unos pocos. También enriquece mucho la interfaz, es decir, nos proporciona muchos métodos con el mismo nombre, que suelen hacer lo mismo, con algunas variantes como hemos visto en el ejemplo anterior, y cada uno de ellos recibe sólo los parámetros necesarios para llevar a cabo su acción.

Por desgracia el SDL (Scene Description Language) de Pov-Ray no nos permite esto pero se puede simular en cierta medida, como ya veremos más adelante.

¿Qué es la signatura de un método?

Aviso: esto es un tostón, pero bastante interesante como culturilla general.

A grandes rasgos también, se podría decir que es la definición del método, o su interfaz, aunque esto es a grandes rasgos. En el primer ejemplo sería esta:

void draw_box(int, int, boolean)

La signatura está compuesta por cuatro partes:

  • El nombre de la función: draw_box
  • El número de parámetros: 3
  • El tipo de los parámetros: int, int y boolean
  • El orden de dichos tipos.

Otro elemento que también forma parte de la interfaz de un método es el tipo devuelto, aunque no forma parte de la signatura. En este caso el tipo devuelto es void, es decir, no devuelve nada, pero por ejemplo podría devolver algún valor, como podría ser el area de la caja que queremos dibujar.

El motivo por el que el tipo devuelto no forma parte de la signatura no es objeto de este tutorial. Se remite al lector a algún libro de programación básica.

Pues una función sobrecargada no es más que una función con el mismo nombre, pero distinta signatura. Por ejemplo:

void draw_box(int, int)
void draw_box(boolean)
void draw_box(boolean, int, int)

Todos estos ejemplos son funciones sobrecargadas de la primera. Como vemos conservan el mismo nombre, pero se diferencia en el número de parámetros y en el orden de sus tipos.

Aunque aquí no se ha hecho una explicación exhaustiva sobre el tema espero que el concepto haya quedado claro.

Simulando esto en Pov-Ray

Un ejemplo ilustrativo

Lo más parecido a una función de un lenguaje de programación en Pov, es una #macro. Una macro se podría definir como un trozo de código parametrizable. Veamos un ejemplo.

En Pov existe la palabra reservada function, que tiene reservada otra función, valga la redundancia.

¿Hay algo más tedioso en Pov que crear una caja?. Realmente crear cajas acaba conmigo.

box{0 <3,2,4>}

Con esto creamos una caja de 3 unidades de ancho, 2 de alto y 4 de largo. Si quisieramos centrarla dejándola sobre el plano XZ tendríamos que añadir a mano un par de translate.

box{0 <3,2,4> translate x*-3/2 translate z*-4/2}

¿Y si queremos cambiar las dimensiones de la caja?. Pues, se cambian a mano y a tirar el render. ¡¡¡ Vaya, no está bien centrada !!!. ¿Qué ... está pasando aquí?. ¡ Ah !, se me olvidaron los translates. Lo que he dicho, un auténtico ... Para resolver esto podriamos crearnos una #macro a la que le pasáramos las dimensiones de la caja y la centrara automáticamente.

// w width (X axis)
// h height (Y axis)
// l length (Z axis)
#macro the_box(w,h,l)
  box{0 <w,h,l> translate x*-w/2 translate z*-l/2}
#end

No podemos llamar a nuestra macro box ya que es una palabra reservada.

Con esto ya tenemos la macro siempre centrada, pero, ¿y si alguna vez queremos que la caja no esté centrada?. Podríamos intentar una sobrecarga como en C, añadiendo un cuarto parámetro booleano, pero Pov-Ray no nos dejaría.

#macro the_box(w,h,l,centered)
  box{0 <w,h,l> #if(centered) translate x*-w/2 translate z*-l/2 #end}
#end

Pov-Ray redefiniría la primera macro y nos quedaríamos tan sólo con la segunda, por lo que tenemos el problema, o la incomodidad de tener que poner siempre el último parámetro. Otro enfoque sería crear otra macro con otro nombre the_box2, pero esto no es una buena solución ya que tendríamos muchas macros con un nombre distinto o más bien parecido, y muchos numeros, pero que vendrían a hacer lo mismo. Además el nombre de la macro no sería nada significativo, por lo que se pierde la verdadera esencia de agrupar código en una macro. Quizá usando una nomenclatura estaría mejor, pero estaríamos en las mismas.

En resumen en Pov tan sólo podemos tener una #macro con el mismo nombre.

Arrays como parámetros

Este es el apartado más importante, por lo que conviene leerlo despacito y entenderlo.

El enfoque aquí presentado es el de pasar un sólo parámetro a nuestra #macro. Dicho parámetro sería un array con los valores que realmente le queremos pasar a la #macro. Como los arrays pueden tener un número variable de valores y diferentes tipos ya hemos resuelto el problema de la signatura. Quedaría tal que así:

#macro the_box(parameters)
  #local n = dimension_size(parameters,1); //Number of parameters
  #local w = parameters[0]; // Widht  (X axis)
  #local h = parameters[1]; // Height (Y axis)
  #local l = parameters[2]; // Length (Z axis)
  #local c = true; // Centered ? Default is true
  #if(n=4) #local c = parameters[3]; #end
  object{
    box{0 <w,h,l>}
    #if(c) translate x*-w/2 translate z*-l/2> #end
  }
#end

En esta #macro el último parámetro se podría decir que es opcional. Como vemos dentro de la #macro se recogen los tres parámetros principales (w, h, l) y se mira si viene un cuarto parámetro en la línea #if (n=4) ... #end. Anteriormente a esto se le da un valor por defecto al parámetro que nos indica si la caja deberá estar o no centrada.

Con esto ya podemos usar nuestra #macro de cualquiera de estas dos formas:

object{the_box(array[3]{3,2,4})}

o bien

object{the_box(array[4]{3,2,4,false})}

Y como un tutorial de Pov sin imágenes es un tostón, aquí va el resultado de las dos líneas anteriores, y aquí teneis el archivito zip con el entorno, colorines y demás cosas listo para renderizar.

Se puede criticar. Hasta más leer.

object{the_box(array[3]{3,2,4}) pigment{Red}}
object{the_box(array[3]{3,2,4}) pigment{Red}}
object{the_box(array[4]{3,2,4,false}) pigment{Red}}
object{the_box(array[4]{3,2,4,false}) pigment{Red}}