Podemos crear aplicaciones que muestren gráficos en 3D utilizando la API opcional Mobile 3D Graphics for J2ME (JSR-184).
Figura 8. Ejemplos de aplicaciones 3D para dispositivos móviles
Esta API nos permitirá añadir contenido 3D a las aplicaciones de dos modos distintos:
Más adelante estudiaremos con más detalle cómo mostrar gráficos 3D utilizando cada uno de estos modos.
Para renderizar gráficos 3D en el móvil utilizaremos un objeto
de tipo Graphics3D
. Para obtener este objeto utilizaremos el siguiente
método estático:
Graphics3D g3d; ... g3d = Graphics3D.getInstance();
Podremos declarar este objeto como campo global de la clase y obtenerlo una única vez durante la inicialización de la misma.
Para poder utilizar este objeto, deberemos decirle en qué contexto gráfico queremos que vuelque el contenido 3D que genere. Normalmente estableceremos como objetivo donde volcar el contexto gráfico asociado a la pantalla del móvil.
Esto lo haremos en el método paint
, como se muestra a continuación:
protected void paint(Graphics g) {
try {
g3d.bindTarget(g);
... g3d.render(escena);
} finally {
g3d.releaseTarget();
}
}
Con bindTarget
establecemos en qué contexto gráfico
vamos a volver los gráficos 3D. Una vez hecho esto podremos renderizar
la escena que hayamos definido utilizando el método render
del objeto Graphics3D
. Más adelante veremos cómo
definir esta escena utilizando tanto el modo inmediato como el modo retained.
Por último, con releaseTarget
liberamos el contexto gráfico
que estabamos utilizando para volcar los gráficos.
Cuando queramos establecer la posición de un objeto 3D en el mundo,
deberemos transformar sus coordenadas para que éstas estén en
la posición deseada. Para representar estas transformaciones geométricas
en el espacio tenemos la clase Transform
.
Utilizaremos objetos Transform
para indicar la posición
y el tamaño que van a tener los objetos en el mundo. En la transformación
podemos establecer traslaciones del objeto, rotaciones, y cambios de escala.
Normalmente asociaremos a cada objeto que tengamos en el mundo 3D una transformación, donde se indicará la posición de dicho objeto. Además, esto nos servirá para hacer animaciones, ya que simplemente cambiando los valores de la transformación en el tiempo podremos hacer que el objeto cambie de posición o tamaño.
Utilizando el constructor de Transform
podemos crear una transformación
identidad:
Trasform trans = new Transform();
Con esta transformación, el objeto al que se aplique permanecerá en sus coordenadas iniciales. Podremos modificar esta transformación para cambiar la posición o el tamaño del objeto mediante:
trans.postTranslate(tx, ty, tz);
trans.postRotate(angulo, vx, vy, vz);
trans.postScale(sx, sy, sz);
Podemos hacer que la transformación vuelva a ser la identidad para volver a la posición original del objeto:
trans.setIdentity();
Para poder renderizar una escena es imprescindible establecer previamente el
punto de vista desde el que la queremos visualizar. Para hacer esto tendremos
que definir una cámara, encapsulada en el objeto Camera
.
Crearemos la cámara utilizando el constructor vacío de esta clase:
Camera cam; ... cam = new Camera();
Con esto tendremos la cámara, que siempre estará apuntando hacia
la coordenada Z negativa (0, 0, -1). Si queremos mover o rotar la cámara,
deberemos hacerlo mediante un objeto Transform
que aplicaremos
a la cámara:
Transform tCam; ... tCam = new Transform(); tCam.postTranslate(0.0f, 0.0f, 2.0f);
De la cámara deberemos especificar el tipo de proyección que queremos utilizar y una serie de parámetros. Podemos establecer dos tipos de proyecciones:
cam.setParallel(campoDeVision, relacionAspecto,
planoFrontal, planoPosterior);
cam.setPerspective(campoDeVision, relacionAspecto,
planoFrontal, planoPosterior);
En el parámetro campoDeVision
indicaremos el campo visual
de la cámara en grados. Este valor normalmente será 45º o
60º. Valores superiores a 60º distorsionan la imagen. En relacionAspecto
indicaremos la proporción entre el ancho y el alto de la pantalla donde
se van a mostrar los gráficos. Además deberemos establecer dos
planos de recorte: uno cercano (planoFrontal
) y uno lejano (planoPosterior
),
de forma que sólo se verán los objetos que estén entre
estos dos planos. Todo aquello más cercano al plano frontal, y más
lejano al plano posterior será recortado.
Por ejemplo, podemos utilizar una proyección perspectiva como la siguiente:
cam.setPerspective(60.0f, (float)getWidth()/(float)getHeight(),
1.0f, 1000.0f);
Una vez definida la cámara, deberemos establecerla en el objeto Graphics3D
de la siguiente forma:
g3d.setCamera(cam, tCam);
Vemos que al establecer la cámara, además de el objeto Camera
,
deberemos especificar la transformación que se va a aplicar sobra la
misma. Si queremos hacer movimientos de cámara no tendremos más
que modificar la transformación que utilizamos para posicionarla en la
escena.
Si vamos a dejar la cámara fija, podemos establecer la cámara
en el objeto Graphics3D
una única vez durante la inicialización.
Sin embargo, si queremos mover la cámara, deberemos establecerla dentro
del método paint
, para que cada vez que se redibuje se sitúe
la cámara en su posición actual.
Deberemos también definir la iluminación del mundo. Para crear
luces utilizaremos el objeto Light
. Podemos crear una luz utilizando
su constructor vacío:
Light luz; ... luz = new Light();
La luz tendrá un color y una intensidad, que podremos establecer utilizando los siguientes métodos:
luz.setColor(0x0ffffff); luz.setIntensity(1.0f);
Como color podremos especificar cualquier color RGB, y como intensidad deberemos introducir un valor dentro del rango [0.0, 1.0]. Estos atributos son comunes para cualquier tipo de luces.
Además, podemos utilizar diferentes tipos de luces con distintas propiedades. Estos tipos son:
Light.AMBIENT
): Esta luz afectará
a todos los objetos por igual en todas las direcciones. Es la luz mínima
del ambiente, que incide en todas las partes de los objetos. Simula la reflexión
de la luz sobre los objetos que produce dicha luz que está "en
todas partes".Light.DIRECTIONAL
): Esta luz
incide en todos los objetos en una misma dirección. Con está
luz podemos modelar una fuente de luz muy lejana (en el infinito). Por ejemplo,
nos servirá para modelar la iluminación el sol, que incide en
todos los objetos en la misma dirección en cada momento. La luz estará
apuntando en la dirección del eje de las Z negativo (0, 0, -1).Light.OMNI
): Se trata de
una fuente de luz que ilumina en todas las direcciones. Por ejemplo con esta
luz podemos modela una bombilla, que es un punto que emana luz en todas las
direcciones.Light.SPOT
): Este tipo produce un cono
de luz. El foco ilumina en una determinada dirección, produciendo un
cono de luz. El foco estará orientado en la dirección del eje
de las Z negativo (0, 0, -1).Para establecer el tipo de luz que queremos utilizar tenemos el método
setMode
:
luz.setMode(Light.DIRECTIONAL);
Dentro de los tipos de luces anteriores, podemos distinguir dos grupos según
la posición de la fuente. Tenemos luces sin posición (ambiente
y direccional) y luces con posición (omnidireccional y foco). En el caso
de las luces con posición, podemos utilizar el método setAttenuation
para dar una atenuación a la luz en función a la distancia de
la fuente. Con esto haremos que los objetos que estén más lejos
de la fuente de luz estén menos iluminados que los que estén más
cerca. Esto no se puede hacer en el caso de las fuentes de luz sin posición,
ya que en ese caso la distancia de cualquier objeto a la fuente es infinito
o no existe.
Otra clasificación que podemos hacer es entre fuentes de luz sin dirección
(ambiente y omnidireccional) y con dirección (direccional y foco). Hemos
visto que las fuentes con dirección siempre están apuntando en
la dirección del eje de las Z negativo. Si queremos modificar este dirección
podemos aplicar una rotación a la luz mediante un objeto Transform
.
De la misma forma, en las fuentes con posición podremos aplicar una traslación
para modificar la posición de la fuente de luz con este objeto.
Transform tLuz; ... tLuz = new Transform();
Para añadir las luces al mundo utilizaremos el método addLight
sobre el objeto de contexto gráfico 3D, en el que especificaremos la
luz y la transformación que se va a realizar:
g3d.addLight(luz, tLuz);
Si como transformación especificamos null
, se aplicará
la identidad. Por ejemplo en el caso de una luz ambiente no tiene sentido aplicar
ninguna transformación a la luz, ya que no tiene ni posición ni
dirección.
g3d.addLight(luzAmbiente, null);
Al igual que ocurría en el caso de la cámara, si las luces van
a permanecer fijas sólo hace falta que las añadamos una vez durante
la inicialización. Sin embargo, si se van a mover, tendremos que establecer
la posición de la luz cada vez que se renderiza para que se apliquen
los cambios. En este caso si añadimos las luces dentro del método
paint
deberemos llevar cuidado, ya que el método addLight
añade luces sobre las que ya tiene definidas el mundo. Para evitar que
las luces se vayan acumulando, antes de volver a añadirlas tendremos
que eliminar las que teníamos en la iteración anterior con resetLights
.
Una vez hecho esto podremos añadir las demás luces:
g3d.resetLights();
g3d.addLight(luz, tLuz);
g3d.addLight(luzAmbiente, null);
También deberemos establecer un fondo para el visor antes de renderizar
en él los gráficos 3D. Como fondo podemos poner un color sólido
o una imagen. El fondo lo representaremos mediante un objeto de la clase Background
:
Background fondo; ... fondo = new Background();
Con el constructor vacío construiremos el fondo por defecto que consiste
en un fondo negro. Podemos cambiar el color de fondo con el método setColor
,
o establecer una imagen de fondo con setImage
.
Cada vez que vayamos a renderizar los gráficos en paint
es importante que vaciemos todo el área de dibujo con el color (o la
imagen) de fondo. Para ello utilizaremos el método clear
:
g3d.clear(fondo);
Si no hiciésemos esto, es posible que tampoco se visualizasen los gráficos
3D que se rendericen posteriormente. Si no queremos crear ningún fondo
y utilizar el fondo negro por defecto, podremos llamar a este método
proporcionando null
como parámetro:
g3d.clear(null);
Vamos a recapitular todo lo visto hasta ahora, y a ver cómo quedará
el método paint
añadiendo luces, cámara y
fondo:
protected void paint(Graphics g) { try { g3d.bindTarget(g);
g3d.setCamera(cam, tCam);
g3d.resetLights(); g3d.addLight(luz, tLuz); g3d.addLight(luzAmbiente, null);
g3d.clear(fondo);
g3d.render(vb, tsa, ap, tCubo); } finally { g3d.releaseTarget(); } }
Para crear gráficos utilizando este modo deberemos definir la lista de vértices (x, y, z) de nuestro gráfico, y una lista de índices en la que definimos las caras (polígonos) que se van a mostrar. Cada cara se construirá a partir de un conjunto de vértices. Los índices con los que se define cada cara en la lista de caras, harán referencia a los índices de los vértices que la componen en la lista de vértices.
A continuación se muestra un ejemplo de cómo crear un cubo utilizando modo inmediato:
// Lista de vertices
short [] vertexValues = { 0, 0, 0, // 0 0, 0, 1, // 1 0, 1, 0, // 2 0, 1, 1, // 3 1, 0, 0, // 4 1, 0, 1, // 5 1, 1, 0, // 6 1, 1, 1 // 7 };
// Lista de caras
int [] faceIndices = { 0, 1, 2, 3, 7, 5, 6, 4, 4, 5, 0, 1, 3, 7, 2, 6, 0, 2, 4, 6, 1, 5, 3, 7 };
En la lista de vértices podemos ver que tenemos definidas las coordenadas de las 8 esquinas del cubo. En la lista de caras se definen las 6 caras del cubo. Cada cara se define a partir de los 4 vértices que la forman. De estos vértices especificaremos su índice en la lista de vértices.
En M3G representaremos las caras y vértices de nuestros objetos mediante
los objetos VertexBuffer
e IndexBuffer
respectivamente.
Estos objetos los crearemos a partir de los arrays de vértices y caras
definidos anteriormente.
Para crear el objeto VertexBuffer
es necesario que le pasemos
los arrays de datos necesarios en forma de objetos VertexArray
.
Para construir el objeto VertexArray
como parámetros deberemos
proporcionar:
VertexArray va = new VertexArray(numVertices, numComponentes, bytesComponente);
Debemos decir en numVertices
el número de vértices
que hemos definido, en numComponentes
el número de componentes
de cada vértice (al tratarse de coordenadas 3D en este caso será
siempre 3: x, y ,z) y el número de bytes por componente. Es decir, si
se trata de una array de tipo byte
tendrá un byte por componente,
mientras que si es de tipo short
tendrá 2 bytes por componente.
Una vez creado el array, deberemos llenarlo de datos. Para ello utilizaremos el siguiente método:
va.set(verticeInicial, numVertices, listaVertices);
Proporcionaremos un array de vértices (listaVertices
) como
el visto en el ejemplo anterior para insertar dichos vértices en el objeto
VertexArray
. Además diremos, dentro de este array de vértices,
cual es el vértice inicial a partir del cual queremos empezar a insertar
(verticeInicial
) y el número de vértices, a partir
de dicho vértice, que vamos a insertar (numVertices
).
En el caso del array de vértices de nuestro ejemplo, utilizaremos los siguientes datos:
VertexArray va = new VertexArray(8, 3, 2); va.set(0, 8, vertexValues);
Con esto tendremos construido un objeto VertexArray
en el que
tendremos el array de posiciones de los vértices.
Ahora podremos crear un objeto VertexBuffer
y utilizar el objeto
anterior para establecer su lista de coordenadas de los vértices.
VertexBuffer vb; ... vb = new VertexBuffer(); vb.setPositions(va, 1.0f, null);
Al método setPositions
le podemos proporcionar como segundo
y tercer parámetro un factor de escala y una traslación respectivamente,
para transformar las coordenadas de los puntos que hemos proporcionado. Si no
queremos aplicar ninguna transformación geométrica a estos puntos,
simplemente proporcionaremos como escala 1.0f
y como traslación
null
.
Una vez creado este objeto, necesitaremos el objeto de índices en el
que se definen las caras. Este objeto será de tipo IndexBuffer
,
sin embargo esta clase es abstracta, por lo que deberemos utilizar una de sus
subclases. La única subclase disponible por el momento es TriangleStripArray
que representa las caras a partir de tiras (strips) de triángulos.
Para definir cada tira de triángulos podremos especificar 3 o más
vértices. Si indicamos más de 3 vértices se irán
tomando como triángulos cada trio adyacente de vértices. Por ejemplo,
si utilizamos la tira { 1,2,3,4 }
, se crearán los triángulos
{ 1,2,3 }
y { 2,3,4 }
.
Figura 9. Tira de triángulos para una cara del cubo.
El orden en el que indiquemos los vértices de cada polígono será importante. Según el sentido de giro en el que se defina su cara frontal y cara trasera será una u otra. La cara frontal será aquella para la que el sentido de giro de los vértices vaya en el sentido de las agujas del reloj.
Cuando creemos este objeto deberemos proporcionar, a parte de la lista de índices, un array con el tamaño de cada strip.
Por ejemplo, en el caso del ejemplo del cubo, como cada strip se compone de 4 índices, el array de tamaños tendrá el valor 4 para cada uno de los 6 strips que teníamos definidos:
int [] stripSizes = { 4, 4, 4, 4, 4, 4 };
Con esta información ya podremos crear el objeto TriangleStripArray
:
TriangleStripArray tsa; ... tsa = new TriangleStripArray(faceIndices, stripSizes);
Además de la información de vértices y caras, deberemos
darle una apariencia al objeto creado. Para ello podremos utilizar el objeto
Appearance
. Podemos crear un objeto con la apariencia por defecto:
Appaerance ap; ... ap = new Appaerance();
Sobre este objeto podremos cambiar el material o la textura de nuestro objeto 3D.
Para renderizar el gráfico 3D que hemos construido, utilizaremos la
siguiente variante del método render
:
g3d.render(VertexBuffer vertices, IndexBuffer indices,
Appaerance apariencia, Transform transformacion);
En ella especificaremos los distintos componentes de nuestra figura 3D, conocida
como submesh: lista de vértices (VertexBuffer
),
lista de caras (IndexBuffer
) y apariencia (Appaerance
).
Además especificaremos también un objeto Transform
con la transformación geométrica que se le aplicará a nuestro
objeto al añadirlo a la escena 3D. Podemos crear una transformación
identidad utilizando el constructor vacío de esta clase:
Transform tCubo; ... tCubo = new Transform();
Aplicando esta transformación se dibujará el cubo en sus coordenadas originales. Si posteriormente queremos moverlo en el espacio (trasladarlo o rotarlo) o cambiar su tamaño, podremos hacerlo simplemente modificando esta transformación.
Para renderizar nuestro ejemplo del cubo utilizaremos el método render
como se muestra a continuación:
g3d.render(vb, tsa, ap, tCubo);
Con lo que hemos visto hasta ahora, si mostramos el cubo que hemos creado aparecerá con el siguiente aspecto:
Esta es la apariencia (Appaerance
) definida por defecto. En ella
no hay ningún material ni textura definidos para el objeto. Al no haber
ningún material establecido (el material es null
), la iluminación
no afecta al objeto.
Vamos ahora a establecer un material para el objeto. Creamos un material (objeto
Material
):
Material mat = new Material();
Esto nos crea un material blanco por defecto. Podremos modificar en este objeto el color de ambiente, el color difuso, el color especular, y el color emitido del material.
Cuando hayamos establecido un material para el objeto, la iluminación afectará sobre él. En este caso será necesario definir las normales de los vértices del objeto. Si estas normales no estuviesen definidas, se producirá un error al renderizar.
Para asignar las normales a los vértices del objeto utilizaremos un
objeto VertexArray
que añadiremos al VertexBuffer
de nuestro objeto utilizando el método setNormals
.
byte [] normalValues = { -1, -1, -1, -1, -1, 1, -1, 1, -1, -1, 1, 1, 1, -1, -1, 1, -1, 1, 1, 1, -1, 1, 1, 1 };
...
VertexArray na = new VertexArray(8, 3, 1); na.set(0, 8, normalValues);
...
vb.setNormals(na);
Con esto, al visualizar nuestro cubo con una luz direccional apuntando desde nuestro punto de vista hacia el cubo, se mostrará con el siguiente aspecto:
Vamos a ver ahora como añadir textura al objeto. Para ello lo primero
que debemos hacer es añadir coordenadas de textura a cada vértice.
Para añadir estas coordenadas utilizaremos también un objeto VertexArray
que añadiremos al VertexBuffer
mediante el método
setTexCoords
.
short [] tex = { 0,0, 0,0, 0,1, 0,1, 1,0, 1,0, 1,1, 1,1 }; VertexArray ta = new VertexArray(8, 2, 2); ta.set(0, 8, tex); vb.setTexCoords(0, ta, 1.0f, null);
El primer parámetro que toma setTexCoords
es el índice
de la unidad de textura a la que se van a asignar esas coordenadas. Como segundo
parámetro se proporcionan las coordenadas en forma de VertexArray
.
El tercer y cuarto parámetro nos permiten realizar escalados y traslaciones
de estas coordenadas respectivamente.
Una vez hecho esto podemos crear la textura a partir de una imagen y añadirla a la apariencia:
try { Image img = Image.createImage("/texture.png"); Image2D img2d = new Image2D(Image2D.RGB, img); Texture2D tex2d = new Texture2D(img2d); ap.setTexture(0, tex2d); } catch (IOException e) { }
La textura se establece en el objeto Appaerance
mediante el método
setTexture
. A este método le proporcionamos como parámetros
el índice de la unidad de textura, y la textura que vamos a establecer
en dicha unidad de textura. De esta forma se podrán definir varias unidades
de textura, teniendo cada una de ellas unas coordenadas y una imagen.
Con esto el aspecto del cubo será el siguiente:
Podemos añadir animación al objeto modificando su transformación a lo largo del tiempo. Para ello podemos crear un hilo como el siguiente:
public void run() { while(true) { tCubo.postTranslate(0.5f, 0.5f, 0.5f); tCubo.postRotate(1.0f, 1.0f, 1.0f, 1.0f); tCubo.postTranslate(-0.5f, -0.5f, -0.5f); repaint(); try { Thread.sleep(25); } catch (InterruptedException e) { } } }
A continuación mostramos el código completo del canvas de este ejemplo:
package es.ua.j2ee.m3d;
import java.io.IOException;
import javax.microedition.lcdui.*; import javax.microedition.m3g.*;
public class Visor3DInmediate extends Canvas implements Runnable {
MIDlet3D owner; Graphics3D g3d; Transform tCam; Transform tLuz; Transform tCubo; VertexBuffer vb; IndexBuffer tsa; Appearance ap; Camera cam; Light luz; Light luzAmbiente; Background fondo; short [] vertexValues = { 0, 0, 0, // 0 0, 0, 1, // 1 0, 1, 0, // 2 0, 1, 1, // 3 1, 0, 0, // 4 1, 0, 1, // 5 1, 1, 0, // 6 1, 1, 1 // 7 };
byte [] normalValues = { -1, -1, -1, -1, -1, 1, -1, 1, -1, -1, 1, 1, 1, -1, -1, 1, -1, 1, 1, 1, -1, 1, 1, 1 }; int [] faceIndices = { 0, 1, 2, 3, 7, 5, 6, 4, 4, 5, 0, 1, 3, 7, 2, 6, 0, 2, 4, 6, 1, 5, 3, 7 };
int [] stripSizes = { 4, 4, 4, 4, 4, 4 }; short [] tex = { 0,0, 0,0, 0,1, 0,1, 1,0, 1,0, 1,1, 1,1 }; public Visor3DInmediate(MIDlet3D owner) { this.owner = owner; init3D(); }
public void init3D() { g3d = Graphics3D.getInstance(); tCubo = new Transform(); tCam = new Transform(); tLuz = new Transform();
cam = new Camera(); cam.setPerspective(60.0f, (float)getWidth()/(float)getHeight(), 1.0f, 10.0f); tCam.postTranslate(0.0f, 0.0f, 2.0f);
luz = new Light(); luz.setColor(0x0ffffff); luz.setIntensity(1.0f); luz.setMode(Light.DIRECTIONAL); tLuz.postTranslate(0.0f, 0.0f, 5.0f); luzAmbiente = new Light(); luzAmbiente.setColor(0x0ffffff); luzAmbiente.setIntensity(0.5f); luzAmbiente.setMode(Light.AMBIENT); fondo = new Background(); VertexArray va = new VertexArray(8, 3, 2); va.set(0, 8, vertexValues);
VertexArray na = new VertexArray(8, 3, 1); na.set(0, 8, normalValues);
VertexArray ta = new VertexArray(8, 2, 2); ta.set(0, 8, tex); vb = new VertexBuffer(); vb.setPositions(va, 1.0f, null); vb.setNormals(na); vb.setTexCoords(0, ta, 1.0f, null); tsa = new TriangleStripArray(faceIndices, stripSizes); ap = new Appearance(); Material mat = new Material(); ap.setMaterial(mat); try { Image img = Image.createImage("/texture.png"); Image2D img2d = new Image2D(Image2D.RGB, img); Texture2D tex2d = new Texture2D(img2d); ap.setTexture(0, tex2d); } catch (IOException e) { } tCubo.postTranslate(-0.5f, -0.5f, -0.5f); } protected void showNotify() { Thread t = new Thread(this); t.start(); }
public void run() { while(true) { tCubo.postTranslate(0.5f, 0.5f, 0.5f); tCubo.postRotate(1.0f, 1.0f, 1.0f, 1.0f); tCubo.postTranslate(-0.5f, -0.5f, -0.5f); repaint(); try { Thread.sleep(25); } catch (InterruptedException e) { } } } protected void paint(Graphics g) { try { g3d.bindTarget(g); g3d.setCamera(cam, tCam); g3d.resetLights(); g3d.addLight(luz, tLuz); g3d.addLight(luzAmbiente, null); g3d.clear(fondo); g3d.render(vb, tsa, ap, tCubo); } finally { g3d.releaseTarget(); } }
}
Utilizando este modo trabajaremos con un grafo en el que tendremos los distintos
elementos de la escena 3D: objetos 3D, luces, cámaras, etc. Este grafo
estará compuesto por objetos derivados de Node
(nodos).
Tenemos los siguientes tipos de nodos disponibles:
World
: El nodo raíz de este grafo es
del tipo World
, que representa nuestro mundo 3D completo. De
él colgaremos todos los objetos del mundo, las cámaras y las
luces. Además, el nodo World
tendrá asociado el
fondo (Background
) de la escena.Group
: Grupo de nodos. Nos permite crear grupos
de nodos dentro del grafo. De él podremos colgar varios nodos. El tener
los nodos agrupados de esta forma nos permitirá, aplicando transformaciones
geométricas sobre el grupo, mover todos estos nodos como un único
bloque. Podemos crear un nuevo grupo creando un nuevo objeto Group
,
y añadir nodos hijos mediante su método addChild
.Camera
: Define una cámara (punto de
vista) en la escena. Las cámaras definen la posición del espectador
en la escena. Podemos tener varias cámaras en el mundo, pero en un
momento dado sólo puede haber una cámara activa, que será
la que se utilice para renderizar. Se crean como hemos visto en apartados
anteriores. El objeto correspondiente al mundo (World
) tiene
un método setActiveCamera
con el que podremos establecer
cuál será la cámara activa en cada momento.Light
: Define las luces de la escena. Se crean
como hemos visto en apartados anteriores. Mesh
: Define un objeto 3D. Este objeto se
puede crear a partir de sus vértices y caras como vimos en el apartado
anterior.Mesh obj = new Mesh(vb, tsa, ap);
Sprite3D
: Representa una imagen 2D posicionada
dentro de nuestro mundo 3D y alineada con la pantalla.Figura 10. Ejemplo de grafo de la escena.
En este modo normalmente cargaremos estos componentes de la escena 3D de un fichero con formato M3G. Para crear mundo en este formato podremos utilizar herramientas como Swerve Studio. De este modo podremos modelar objetos 3D complejos utilizando una herramienta adecuada, y posteriormente importarlos en nuestra aplicación.
Para cargar objetos 3D de un fichero M3G utilizaremos un objeto Loader
de la siguiente forma:
World mundo;
...
try { Object3D [] objs = Loader.load("/mundo.m3g"); mundo = (World)objs[0]; } catch (IOException e) { System.out.println("Error al cargar modelo 3D: " + e.getMessage()); }
Con load
cargaremos del fichero M3G especificado los objetos 3D
que contenga. La clase Object3D
es la superclase de todos los tipos
de objetos que podemos utilizar para definir nuestra escena. De esta forma permitimos
que esta función nos devuelva cualquier tipo de objeto, según
lo que haya almacenado en el fichero. Estos objetos pueden ser nodos como los
vistos anteriormente, animaciones, imágenes, etc.
Por ejemplo, si tenemos un fichero M3G que contiene un mundo 3D completo (objeto
World
), cogeremos el primer objeto devuelto y haremos una conversión
cast al tipo adecuado (World
).
Como en este objeto se define la escena completa, no hará falta añadir nada más, podemos renderizarlo directamente con:
protected void paint(Graphics g) { try { g3d.bindTarget(g); g3d.render(mundo); } finally { g3d.releaseTarget(); } }
En el mundo 3D del fichero, a parte de los modelos 3D de los objetos, luces
y cámaras, podemos almacenar animaciones predefinidas sobre elementos
del mundo. Para utilizar esta animación llamaremos al método animate
sobre el mundo que queremos animar:
protected void paint(Graphics g) { try { g3d.bindTarget(g);
mundo.animate(tiempo); g3d.render(mundo); } finally { g3d.releaseTarget(); } }
Le deberemos proporcionar un valor de tiempo, que deberemos ir incrementando
cada vez que se pinta. Podemos crear un hilo que cada cierto periodo incremente
este valor y llame a repaint
para volver a renderizar la escena.
A continuación vemos el código completo el ejemplo de mundo que utiliza modo retained:
package es.ua.j2ee.m3d;
import java.io.IOException;
import javax.microedition.lcdui.*; import javax.microedition.m3g.*;
public class Visor3DRetained extends Canvas implements Runnable {
MIDlet3D owner;
Graphics3D g3d; World mundo;
int tiempo = 0; public Visor3DRetained(MIDlet3D owner) { this.owner = owner; init3D(); }
public void init3D() { g3d = Graphics3D.getInstance(); try { Object3D [] objs = Loader.load("/mundo.m3g"); mundo = (World)objs[0]; } catch (IOException e) { System.out.println("Error al cargar modelo 3D: " + e.getMessage()); } } protected void showNotify() { Thread t = new Thread(this); t.start(); }
public void run() { while(true) { tiempo+=10; repaint(); try { Thread.sleep(25); } catch (InterruptedException e) { } } } protected void paint(Graphics g) { try { g3d.bindTarget(g); mundo.animate(tiempo); g3d.render(mundo); } finally { g3d.releaseTarget(); } }
}