original in es Carlos Calzada Grau
Soy licenciado en informática y desde que me regalaron un Spectrum me han gustado los gráficos por ordenador. Soy muy aficionado a Linux, a su filosofía de desarrollo y a todo lo que no tiene que ver con micro$oft. Los bonsáis y los acuarios son mis aficiones no relacionadas con los aparatitos, estos con pantalla y teclado.
En este tercer artículo de la serie sobre Renderman (I , II) trataremos uno de los aspectos más importantes: la posibilidad de modelar y animar una escena utilizando "C" o "C++".
Como hemos podido ver en los dos artículos anteriores, crear una escena o una animación escribiendo directamente en un fichero de texto es una tarea tediosa. ¿Te imaginas hacer un fichero ".rib" con el movimiento de una pelota botando?. Para hacernos la vida un poco más fácil tenemos la posibilidad de escribir en "C" o "C++" el modelo de la escena o animación con una serie de funciones que sacan por la salida estándar los comandos Renderman que queramos. Esta salida la podemos pasar a través de un tubo (pipe) a otro proceso (como rendrib, rendribv o rgl) o redireccionarla directamente hacia un fichero .rib.
Cuando instalamos Blue Moon Rendering Tools, nos aparecen dos directorios llamados lib e include. En éstos encontramos cuatro ficheros de los cuales dos son los que nos interesan: ri.h y libribout.a que son las declaraciones y la librería de funciones respectivamente. Debemos copiar ri.h a /usr/local/include y libribout.a a /usr/local/lib (también se pueden poner en otro sitio pero este es un lugar adecuado) y ya está, estamos listos para nuestro primer programa.
En este primer programa veremos lo básico de la programación con Renderman. Como en cualquier programa C, debemos incluir el correspondiente .h antes de utilizar una librería, en nuestro caso será ri.h. Además deberemos enlazar (link) el programa con la librería, para ello deberemos hacer lo siguiente:
gcc miprograma.c -o miprograma -lribout -lm
Nosotros utilizaremos un Makefile para ahorrarnos algo de trabajo a la hora de teclear:
LIBS = -lm -lribout PROGNAME = primero all: $(PROGNAME) $(PROGNAME).o: $(PROGNAME).c gcc -c $(PROGNAME).c $(PROGNAME): $(PROGNAME).o gcc -o $(PROGNAME) $(PROGNAME).o $(LIBS) |
El primer programa que haremos consistirá en unos ejes y una pelota en el centro, lo llamaremos primero.c (¡que original!) y es el siguiente:
1 #include <stdio.h> 2 #include <math.h> 3 #include <ri.h> 4 5 void main(void) 6 { 7 int i; 8 int x,y,z; 9 int nf; 10 float slopex,slopey,slopez; 11 12 RtColor Rojo={1,0,0}; 13 RtColor Verde={0,1,0}; 14 RtColor Azul={0,0,1}; 15 RtColor Blanco={1,1,1}; 16 17 RtPoint p1={30,0,10}; /* Posicicion inicial de la pelota */ 18 RtPoint p2={0,20,10}; /* Posicion final de la pelota */ 19 20 RtPoint from={0,100,100}; /* Direccion de la luz */ 21 RtPoint to={0,0,0}; 22 23 char name[]="primero.tif"; 24 RtFloat fov=45; 25 RtFloat intensity1=0.1; 26 RtFloat intensity2=1.5; 27 RtInt init=0,end=1; 28 29 RiBegin(RI_NULL); 30 RiFormat(320,240,1); 31 RiPixelSamples(2,2); 32 RiShutter(0,1); 33 RiFrameBegin(1); 34 RiDisplay(name,"file","rgb",RI_NULL); 35 name[7]++; 36 RiProjection("perspective","fov",&fov,RI_NULL); 37 RiTranslate(0,-5,60); 38 RiRotate(-120,1,0,0); 39 RiRotate(25,0,0,1); 40 RiWorldBegin(); 41 RiLightSource("ambientlight","intensity",&intensity1,RI_NULL); 42 RiLightSource("distantlight","intensity",&intensity2,"from",from,"to",to,RI_NULL); 43 RiColor(Azul); 44 RiTransformBegin(); 45 RiCylinder(1,0,20,360,RI_NULL); 46 RiTranslate(0,0,20); 47 RiCone(2,2,360,RI_NULL); 48 RiTransformEnd(); 49 RiColor(Verde); 50 RiTransformBegin(); 51 RiRotate(-90,1,0,0); 52 RiCylinder(1,0,20,360,RI_NULL); 53 RiTranslate(0,0,20); 54 RiCone(2,2,360,RI_NULL); 55 RiTransformEnd(); 56 RiColor(Rojo); 57 RiTransformBegin(); 58 RiRotate(90,0,1,0); 59 RiCylinder(1,0,20,360,RI_NULL); 60 RiTranslate(0,0,20); 61 RiCone(2,2,360,RI_NULL); 62 RiTransformEnd(); 63 RiColor(Blanco); 64 RiSphere(5,-5,5,360,RI_NULL); 65 RiWorldEnd(); 66 RiFrameEnd(); 67 RiEnd(); 68 }; |
En las tres primeras lineas tenemos una serie de #includes entre los que está ri.h que es el que tiene los prototipos de las funciones. Cada llamada de Renderman tiene su función equivalente en ri.h, así pues, TransformBegin se corresponde con la función RiTransformBegin(), etc. Haciendo make generamos el fichero ejecutable primero. Si lo ejecutamos tendremos por la salida estándar una serie de comandos Renderman, éstos los podemos redireccionar a un fichero (primero > primero.rib) o redireccionarlo a otro proceso (primero | rendrib). Si optamos por la última forma, rendrib generará el fichero renderizado primero.tif:
Antes de poder utilizar cualquiera de las llamadas en un programa debemos hacer una llamada a RiBegin(RI_NULL) y al acabar un llamada a RiEnd(). El parámetro que le pasamos a RiBegin es típicamente RI_NULL. Para evitar el envío de RIB's hacia la salida estándar podemos poner en este argumento el nombre de un fichero ("mifichero.rib") e incluso el nombre de un proceso (como rendrib) y de esta forma pasárselo a un programa sin necesidad de crear un fichero RIB intermedio.
En el listado podemos ver, además de las cosas típicas de C, algunos tipos y funciones propios del interface de Renderman: tenemos el tipo RtColor que es un vector de tres reales indicando la cantidad de rojo, verde y azul (con valores entre 0.0 y 1.0), vemos también el tipo RtPoint que indica posiciones en el espacio y RtFloat y RtInt que son el tipo real y entero respectivamente.
En la línea 29 tenemos la llamada RiBegin(RI_NULL) que como hemos dicho antes es la primera que debemos usar. A partir de aquí tenemos más o menos lo que escribiríamos en un fichero RIB. Esto lo podemos comprobar ejecutando el programa y redireccionándolo a un fichero (./primero > primero.rib), obtendremos lo siguiente:
##RenderMan RIB-Structure 1.0 version 3.03 Format 320 240 1 PixelSamples 2 2 Shutter 0 1 FrameBegin 1 Display "camara.tif" "file" "rgb" Projection "perspective" "fov" [45 ] Translate 0 -5 60 Rotate -120 1 0 0 Rotate 25 0 0 1 WorldBegin LightSource "ambientlight" 1 "intensity" [0.1 ] LightSource "distantlight" 2 "intensity" [1.5 ] "from" [0 100 100] "to" [0 0 0] Color [0 0 1] TransformBegin Cylinder 1 0 20 360 Translate 0 0 20 Cone 2 2 360 TransformEnd Color [0 1 0] TransformBegin Rotate -90 1 0 0 Cylinder 1 0 20 360 Translate 0 0 20 Cone 2 2 360 TransformEnd Color [1 0 0] TransformBegin Rotate 90 0 1 0 Cylinder 1 0 20 360 Translate 0 0 20 Cone 2 2 360 TransformEnd Color [1 1 1] Sphere 5 -5 5 360 WorldEnd FrameEnd |
Viendo esto podemos pensar que generar escenas de esta forma es más costoso ya que tenemos que hacer exactamente lo mismo, definir todas las cosas y posicionarlas en la escena, pero la verdadera potencia de la librería estriba en las animaciones. Este ejemplo consta sólo de un frame así que a continuación haremos que la pelota se mueva.
En este segundo programa usaremos exactamente la misma escena pero haremos que la pelota se desplace desde la posición (30,0,10) a la posición (0,20,10) que es más o menos desde la derecha de la pantalla hasta la izquierda. Estas dos posiciones las definiremos como RtPoint (lineas 18 y 19). El número de frames o imágenes de las que constará nuestra animación lo definimos con la variable nf. A partir de este número y de las posiciones inicial y final obtenemos el paso por frame en los tres ejes (slopex, slopey y slopez). Con todo esto, lo único que nos falta es variar la posición de la pelota en función del número de frame en que nos encontremos. Esto lo hacemos en las lineas 75 a 78, donde un TransformBegin/TransformEnd se encarga de definir la posición de la bola. La posición la calculamos de una forma muy simple como podemos ver en la línea 76.
1 #include <stdio.h> 2 #include <math.h> 3 #include <ri.h> 4 #include "filename.h" 5 6 void main(void) 7 { 8 int i; 9 int x,y,z; 10 int nf; 11 float slopex,slopey,slopez; 12 13 RtColor Rojo={1,0,0}; 14 RtColor Verde={0,1,0}; 15 RtColor Azul={0,0,1}; 16 RtColor Blanco={1,1,1}; 17 18 RtPoint p1={30,0,10}; /* Posicicion inicial de la pelota */ 19 RtPoint p2={0,20,10}; /* Posicion final de la pelota */ 20 21 RtPoint from={0,100,100}; /* Direccion de la luz */ 22 RtPoint to={0,0,0}; 23 24 char base[]="camara_"; 25 char ext[]="tif"; 26 char name[50]; 27 RtFloat fov=45; 28 RtFloat intensity1=0.1; 29 RtFloat intensity2=1.5; 30 RtInt init=0,end=1; 31 32 nf=100; /* Numero de frames */ 33 slopex=(p2[0]-p1[0])/nf; 34 slopey=(p2[1]-p1[1])/nf; 35 slopez=(p2[2]-p1[2])/nf; 36 37 RiBegin(RI_NULL); 38 RiFormat(320,240,1); 39 RiPixelSamples(2,2); 40 RiShutter(0,1); 41 for (i=1;i <= nf;i++) 42 { 43 RiFrameBegin(i); 44 filename(base,ext,sizeof(base)+4,i-1,name); 45 RiDisplay(name,"file","rgb",RI_NULL); 46 name[7]++; 47 RiProjection("perspective","fov",&fov,RI_NULL); 48 RiTranslate(0,-5,60); 49 RiRotate(-120,1,0,0); 50 RiRotate(25,0,0,1); 51 RiWorldBegin(); 52 RiLightSource("ambientlight","intensity",&intensity1,RI_NULL); 53 RiLightSource("distantlight","intensity",&intensity2,"from",from,"to",to,RI_NULL); 54 RiColor(Azul); 55 RiTransformBegin(); 56 RiCylinder(1,0,20,360,RI_NULL); 57 RiTranslate(0,0,20); 58 RiCone(2,2,360,RI_NULL); 59 RiTransformEnd(); 60 RiColor(Verde); 61 RiTransformBegin(); 62 RiRotate(-90,1,0,0); 63 RiCylinder(1,0,20,360,RI_NULL); 64 RiTranslate(0,0,20); 65 RiCone(2,2,360,RI_NULL); 66 RiTransformEnd(); 67 RiColor(Rojo); 68 RiTransformBegin(); 69 RiRotate(90,0,1,0); 70 RiCylinder(1,0,20,360,RI_NULL); 71 RiTranslate(0,0,20); 72 RiCone(2,2,360,RI_NULL); 73 RiTransformEnd(); 74 RiColor(Blanco); 75 RiTransformBegin(); 76 RiTranslate(p1[0]+slopex*(i-1),p1[1]+slopey*(i-1),p1[2]+slopez*(i-1)); 77 RiSphere(5,-5,5,360,RI_NULL); 78 RiTransformEnd(); 79 RiWorldEnd(); 80 RiFrameEnd(); 81 } 82 RiEnd(); 83 }; |
Podemos pasar ahora a probar nuestro programa, para ello procederemos de la misma forma que en el programa anterior: lo compilamos y lo ejecutamos redireccionándolo, por ejemplo, a rendribv. Podemos de esta forma ver como queda nuestra animación de forma rápida y a una velocidad aceptable. Para ver el fichero rib que se genera no tienes más que redireccionar la salida hacia un fichero. Podrás comprobar que el fichero es bastante grande (segundo.rib ocupa 70kb) ya que tenemos repetido la definición de la escena 100 veces (una por cada frame).
En las siguientes figuras podemos ver una serie de imágenes de la animación:
Por supuesto podemos animar cualquier cosa que se nos ocurra: la posición de los objetos, su tamaño, la intensidad de la luz, la cámara, hacer que aparezcan o desaparezcan cosas, etc.
En este último ejemplo veremos como hacer que la pelota bote. Para ello definimos una función rebote() a la cual le pasamos tres parámetros: el número de frame actual, el número total de frames por rebote y la máxima altura a la que queremos rebotar. A continuación podemos ver la implementación:
float rebote (int i, int nframes, int max) { float min, z; while (i > nframes) i-=nframes; min=sqrt(max); z=i-((float)nframes/2.0); z=(z*min)/((float)nframes/2.0); z=(float)max - (z*z); return(z); } |
Con unos sencillos cálculos transformamos la típica función de parábola (y=x^2) para que se ajuste al número de frames y a la altura que deseamos. En las siguientes figuras tenemos un parte de las imágenes que se generan para cada rebote en el programa tercero.c:
Para ver las animaciones hemos creado unos GIF's animados de las escenas aunque van un poco lentos (por lo menos con el Netscape) aunque con xanim funcionan a una velocidad aceptable:
Movimiento rectilíneo: segundo_anim.gif
Movimiento parabólico: tercero_anim.gif
Con esto acabamos con los aspectos básicos del interface de Renderman y su programación. La parte más avanzada y también la más espectacular es la programación de shaders. Con esto conseguimos un control total sobre el proceso de rendering y sobre el aspecto final de la escena ya que podemos controlar texturas, iluminación, etc.