Objetos 3D en Processing

Este tutorial no pretende ser un tratado en profundidad de como trabajar con objetos en 3D con Processing, se trata de más bien de todos los puntos que voy aclarando en la tarea de hacer un programa en el que se muestren unas piezas diseñadas con FreeCAD.

Antes de empezar hay que hablar de las versiones de Processing, la última versión emplea características avanzadas de OpenGL y esto puede ser un problema si tu equipo no es de última generación, a mi me pasa, mi portátil tiene 5 años y su GPU emplea una versión de OpenGL algo antigua así que no puedo trabajar con gráficos 3D con Processing 3.x. La solución es volver a una versión anterior de Processing como la 2.2.1

Lo primero es el tamaño del canvas, lo definiremos igual que cuando trabajamos en 2D sólo que ahora indicaremos con un tercer parámetro que vamos a utilizar un renderizado en 3 dimensiones.

Size(width, height, P3D);

P3D es un renderizador de gráficos en 3D, es con el que se hacen todos los ejemplos pero no es el único que incorpora Processing. No entraré en este tema, tenéis más información en la documentación.

Lo segundo es la visualización del escenario que compongamos. Vamos a tener que definir un punto de vista, es decir, la cámara y además decir hacia donde está mirando esta cámara e incluso podremos invertir los ejes para su visualización. También tendremos que iluminar la escena si queremos ver los objetos además de poner un fondo de un color contra el que destaquen. Lo hacemos así:

1
2
3
4
5
size(500, 500, P3D); // tamaño de la ventana y renderizador 3D
background(0);  // fondo negro
lights();  // iluminación mas sencilla, luz difusa
//camera(posX, posY, posZ, lookAtX, lookAtY, lookAtZ, upX, upY, upZ)
camera(500, 500, 500, 0, 0, 0, 0, -1, 0);

La función camera() requiere una explicación, los 3 primeros parámetros son su ubicación en el espacio tridimensional, luego vienen otros 3 parámetros que son las coordenadas del punto hacia el que mira la cámara y, por último, 3 parámetros que definen si invertimos los ejes o no, en el ejemplo los ejes X y Z no se tocan pero el eje Y se invierte para hacer la visualización más “normal”

Si ya estamos acostumbrados a Processing en 2D tendremos claro que el origen de coordenadas está en la esquina superior izquierda de la ventana que definimos con la función size(), pero al añadir una tercera dimensión la cosa cambia por completo. Este punto puede parecer básico pero a mi me ha costado un poco hacerme con el, no lo he encontrado nada intuitivo. Empiezo por decir que al definir en la cámara que esta mira hacia el origen de coordenadas este aparecerá en el centro de nuestro canvas. Como he definido un canvas de 500 por 500 píxeles y la cámara la he desplazado a la posición (500, 500, 500) y mira hacia el origen tendremos una perspectiva isométrica, más o menos como se lograba la sensación de 3 dimensiones en los juegos antiguos. Así resulta más intuitivo para empezar pero tened claro que podéis poner la cámara en cualquier posición y mirando a cualquier punto, con lo que se pueden lograr todo tipo de vistas interesantes.

Yo aquí me atasqué un poco, no conseguía ver bien como iban los ejes así que pinté 4 cubos: uno en el origen, uno rojo desplazado sobre el eje X, uno verde desplazado sobre el eje Y y otro azul desplazado sobre el eje Z. Esto me sirvió para tener una vista muy clara de como funciona el sistema de coordenadas desde el punto de vista de la cámara que he definido. Pongo el código:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// pintamos un cubo sin color en el origen de coordenadas
pushMatrix();
box(100);
popMatrix();

// pintamos cubo verde en eje Y
pushMatrix();
translate(0, 300, 0);
fill(0, 255, 0);
box(100);
popMatrix();

// pintamos cubo rojo en eje x
pushMatrix();
translate(300, 0, 0);
fill(255, 0, 0);
box(100);
popMatrix();

// pintamos cubo azul en eje z
pushMatrix();
translate(0, 0, 300);
fill(0, 0, 255);
box(100);
popMatrix();

Si lo ponemos todo junto en el editor y le damos al botón de play obtenemos una vista como la de la imágen y creo que os quedará tan claro como a mi. Este código no contiene las funciones setup() ni draw() porque para mostrar una imagen simple no son necesarias.

Ahora que sabemos como funciona la cámara para definir la vista y como van los ejes de coordenadas ya podemos pasar a cosas con más chicha, vamos a cargar objetos 3D.

Cada uno utilizará el programa de CAD con el que se sentirá más cómodo, yo personalmente utilizo FreeCAD por ser sencillo y libre, además es fácil de aprender gracias a los micro tutoriales del genial Juan González (Obijuan).

Normalmente utilizo FreeCAD para diseñar piezas que luego exporto a .stl e imprimo en 3D. Pues partimos de cualquier pieza diseñada y la exportamos como .obj. Si leeis la documentación de Processing veréis que también puede cargar archivos .svg, pero en ese caso lo que carga es una vista plana del objeto, lo que puede ser de utilidad para otros usos pero a nosotros lo que nos interesa es trabajar con objetos con volumen.

La carga de un objeto se hace definiendo un objeto PShape en la cabecera de nuestro scketch:

1
PShape s;

Luego, dentro del setup(), cargamos el objeto 3D dentro del objeto s definido antes…. al escribir mezclo objetos 3D con objetos de programación, espero que me sigáis sin liaros.

1
2
3
void setup() {
    s = loadShape("brazo.obj")
}

En el ejemplo de este tutorial no utilizo setup() ni draw() porque sólo voy a hacer vistas del modelo.

La forma de hacer que el archivo brazo.obj sea utilizable por Processing es tan sencilla como pincharlo en el explorador de archivos y arrastrarlo hasta el IDE, de esa forma Processing hace una copia de ese archivo y lo guarda en la carpeta data de nuestro proyecto. A partir de ahí lo podemos llamar sin necesidad de especificar una ruta y si exportamos nuestra aplicación estará dentro de los archivos exportados.

Al igual que en el ejemplo de los cubos definiremos una luz sencilla, una vista de cámara y, por último, utilizaremos la función shape() para mostrar el objeto. A esta función hay que pasarle como parámetros el objeto y las coordenadas en las que aparecerá…. OJO, coordenadas en el plano XY, cosas de Processing. Para no liarnos lo pondremos siempre en las coordenadas (0, 0) y luego lo moveremos con la función translate().

Código:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
PShape s;

  size(300, 300, P3D);

  s = loadShape("brazo.obj");

  background(204);

  lights();
  //camera(eyeX, eyeY, eyeZ, centerX, centerY, centerZ, upX, upY, upZ)
  camera(width, height, width, width/2, height/2, 0, 0, -1, 0);
 
  translate(width/2, height/2);
   
  shape(s, 0, 0);


Y ya tenemos nuestro modelo 3D dentro de nuestro programa y saliendo por pantalla…. eh, espera un momento…. es muy pequeño…. no tiene color… en FreeCAD estaba orientado de forma diferente… y, no me fastidies, lo que debería estar a un lado está en el otro, es una imagen especular!!. Bueno, vayamos por partes.

Lo del tamaño: Processing asimila directamente el modelo hecho con FreeCAD a un ratio de píxel por milímetro, por lo que si la pieza es pequeña se ve a ver más pequeña todavía.

Aquí unas funciones interesantes que he encontrado no en la referencia, sino en el core de la clase PShape (si amigos, he tenido que bucear un poco):

1
2
3
  println(s.getDepth());
  println(s.getHeight());
  println(s.getWidth());

Esto os dará el tamaño de la forma en píxeles (por si no lo sabíais en mm) y lo mostrará en el área de mendajes del IDE.

1
s.scale(3);

Este último método escalará el modelo, en este caso lo hará 3 veces más grande.

Vamos ahora con lo del color, aquí me he encontrado con un problema de FreeCAD y es que el archivo .obj que genera no contiene color ni textura, datos que se encuentran en un archivo .mtl que otros programas de modelado generan automáticamente pero que FreeCAD no saca. Si el color es plano lo podemos solucionar con:

1
 s.setFill(color(255, 0, 0));

Es decir, le ponemos un relleno de color. setFill() funciona igual que fill() para 2 dimensiones y la función color() supongo que la conocéis, si no a buscar en la referencia.

Tambien se puede emplear un archivo .jpg como si fuera una piel con la que se viste el modelo 3D con:

1
 s.setTexture(image);

Donde image sería el objeto PImage que contendría la imagen .jpg. Esto es fácil si nuestro modelo 3D es una forma simple como un cubo o una esfera, de hecho, con la esfera es como se muestran planetas, pero con una forma compleja como mi pieza el archivo de imagen es complejo y debe ser generado por el programa de modelado…. como es un tema que no me interesa no lo continuo, vosotros pues ya sabéis, Google es vuestro amigo.

El tema de la orientación de la pieza… pues no he coseguido solucionarlo pese a haber buscado mucho en mi amigo Google, la única solución que he encontrado es mostrar primero la pieza, ver que transformaciones necesita y aplicarlas luego. Las transformaciones las veremos un poco más adelante pero os las pongo ya:

1
2
3
  rotateY(radians(angle));
  rotateX(radians(angle));
  rotateZ(radians(angle));

La pieza representada en Processing es especular. Esto es lo que más me ha costado de solucionar y, aunque solucionado, no estoy satisfecho del todo. Me he consultado el JavaDoc del core de PShape hasta casi quedarme ciego y no encontré nada, así que al final opté por una solución “manual”: volví a FreeCAD y realicé una copia especular de la pieza sobre el plano ZX, la exporté como .obj y volví a cargarla…. y tema solucionado. Ahora es un punto que debemos tener en cuenta a la hora de diseñar la pieza, una vez tengamos la pieza lista hay que hacer una copia de espejo sobre el eje ZX antes de exportarla como .obj antes de poder utilizarla con Processing.

Bueno, ahora ya tenemos la pieza en Processing lista para utilizarla, o casi, recordemos de cuando trabajabamos en 2D podíamos rotar y transladar las formas y es eso mismo lo que haremos a partir de ahora para manipular el modelo 3D. Encerraremos las transformacipones entre dos funciones pushMatrix() y popMatrix(), luego la transladaremos con translate(x, y, z) y por último aplicaremos rotaciones, pero ojo, aplicaremos una rotación por eje cada vez con las funciones rotateX(), rotateY() y rotateZ().

Una vez más incluyo un ejemplo de código en el que sólo pintaré una representación de mi pieza 3D, por lo que no utilizaré ni setup() ni draw().

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
PShape s;

  size(300, 300, P3D);

  s = loadShape("brazo.obj");

  background(204);
 
  s.setFill(color(255, 0, 0));

  lights();
  //camera(eyeX, eyeY, eyeZ, centerX, centerY, centerZ, upX, upY, upZ)
  camera(width, height, width, width/2, height/2, 0, 0, -1, 0);
 
  pushMatrix();
    translate(width/2, height/2);
    rotateX(radians(-90));
    shape(s, 0, 0);
  popMatrix();


A partir de aquí que cada uno manipule los modelos 3D como quiera, creo que con esto tenéis lo necesario para importarlos y comenzar a hacer con ellos cosas interesantes.

Un saludo.


Si este tutorial te ha sido de utilidad puedes considerar hacerme un donativo, por pequeño que sea estarás contribuyendo a que siga con esta labor.