Maxscript tutorial matrices de transformación

Hola. Bueno, pues aquí estoy con un tutorial. Algo que tal vez os resulte útil en algún momento. Evidentemente eso dependerá de a que os vayáis a dedicar. Evidentemente una persona que se dedica exclusivamente a modelar difícilmente se va a encontrar con estas cuestiones, pero si uno tira por el camino de la animación/dinámicas, entonces es más que probable que llegue un momento que se tope con este tipo de cosas. Os aseguro que un programador de motores para videojuegos se topa con esto continuamente.

Así que, ¿de qué diablos estoy hablando? Pues se trata de una situación que ya comenté en el hilo que abrí hace unos días, donde explicaba cómo hacer que un objeto mirase a otro haciendo uso de script controllers (sin utilizar el lok-at constraint que viene con 3ds Max, evidentemente).

El tema va de destripar la matriz de transformación de un objeto para así conocerla mejor y de esta forma saber que cosas podemos hacer con ella (no seáis mal pensados. ¿os imagináis montandos un rollito con una matriz de transformación? Qué freaky).

En este caso, como ejemplo, crearé un script controller que hará que un objeto siga un Path, pero sin hacer uso del Path constraint de 3ds Max. Además, de seguir el Path, el objeto variara su orientación para que mire en la dirección del desplazamiento. O sea, como si activarais la opción follow del Path constraint.

Pero antes de pasar a comentar el código del script controller, voy a hacer una breve introducción a las matrices de transformación de un objeto en 3dsmax.
matrices de transformación..

Todo objeto en 3dsmax tiene una matriz de transformación asignada que indica su posición, rotación y escalado. Por ejemplo, cread un cubo en la vista perspective y mantenedlo seleccionado. Entonces abrid el Maxscript Listener (tecla f11) y escribid:

Código:

$.transform


pulsad introducción para ejecutar la línea y el resultado será similar a éste (puede que no os de el mismo resultado si no habéis creado el cubo en el origen de coordenadas):

Código:

(matrix3 [1,0,0] [0,1,0] [0,0,1] [0,0,0])


ahora puede que os estéis preguntando: ¿y que diablos significa esa retahíla de números? Pues bien, parece claro que estamos ante una matriz, concretamente una matriz de tipo matrix3. Eso significa, en 3dsmax, que estamos ante una matriz de dimensiones 4×3, es decir, 4 filas y 3 columnas. Para que visualicéis mejor, voy a reordenar la matriz para mostrarosla en filas y columnas:

Código:

[1,0,0]
[0,1,0]
[0,0,1]
[0,0,0]


a esta matriz se le denomina concretamente matriz identidad. La matriz identidad tiene una característica y es que, además de que su diagonal principal esta rellena con unos, cualquier matriz a que multipliquemos por la matriz identidad, nos dará como resultado la propia matriz a.

En cualquier caso, esto no era lo que os quería explicar. Lo que quería explicar es el significado de cada fila de la matriz que será lo que nos interese para resolver el problema de que un objeto mire hacia una dirección determinada. Así que yendo al grano, la primera fila de la matriz es el eje local x o Vector right que indica la orientación del objeto en el eje X de nuestro mundo 3d. Luego tenemos la segunda fila que es el eje local y o Vector up que indica la orientación del objeto en el eje y de nuestro mundo 3d. Casi para terminar, la tercera fila indica el eje local z o Vector lok o view que indica la orientación del objeto en el eje Z de nuestro mundo 3d. Finalmente, tenemos la cuarta fila que indica la posición de nuestro objeto relativa al origen de coordenadas de nuestro mundo 3d. Rescribo la matriz para que lo veáis mejor:

Código:

[right.x right, y right, z]
[up.x up, y up, z]
[lok.x lok, y lok, z]
[pos.x pos, y pos, z]


desesperados tres vectores forman lo que en matemáticas se conoce como base y además tiene una peculiaridad y es que los tres vectores deben formar 90 grados entre sí, lo que se conoce como base ortonormal. También tienen otra propiedad y es que los tres vectores deben estar normalizados, es decir, deben ser vectores cuya longitud es 1). Esto es importante tenerlo en cuenta porque de lo contrario nuestra matriz de transformación estará mal (sera una matriz degenerada).

Ahora, si seleccionáis el cubo, veréis dibujados los ejes x, y, y z en forma de flechas. Pero estos ejes no son los ejes locales del cubo sino que son los ejes alineados con los ejes de nuestro mundo 3d. Para ver los ejes locales que nos dice nuestra matriz de transformación, pulsad el botón derecho del ratón mientras mantenéis pulsada la tecla alt. Entonces os aparecerá un cuadro desplegable donde tendréis que escoger la opción local. De esta forma sí que estaréis visualizando los eje locales del cubo (right, up, lok) que son los que nos indica la matriz de transformación, como ya he dicho.

Pero un momento, los ejes son iguales que, antes de activar el sistema de coordenadas local. Claro, esto es porque el cubo se crea inicialmente con sus ejes alineados con los ejes de nuestro mundo 3d. Haced una cosa, rotad el cubo 90 grados en el eje x. Ahora haced lo mismo que antes para poner el sistema de coordenadas en modo local. Ahora sí veréis cómo el eje local y apunta hacia arriba (eje global z) y el eje local z apunta hacía fuera de la pantalla, es decir, el eje global -y. Evidentemente el eje X no cambia ya que ha sido el eje de rotación que hemos utilizado.

Ahora, teniendo aún seleccionado el cubo, volved al Maxscript Listener y volved a ejecutar la orden:

Código:

$.transform


vaya, ¿cómo era de esperar la matriz ha cambiado para reflejar la nueva orientación de los ejes locales del cubo:

Código:

(matrix3 [1,0,0] [0,0,1] [0,-1,0] [0,0,0])


o visto de otra forma:

Código:

[1, 0,0]
[0, 0,1]
[0,-1,0]
[0, 0,0]


como podéis ver, el Vector right no ha variado mientras que el Vector up ([0,0,1]) ahora apunta en la dirección del eje global z y el Vector look ([0,-1,0]) ahora apunta en la dirección del eje global -y. Lógico, si acabamos de hacer una rotación de 90 grados en el eje x.

Ni que decir tiene que si movéis el cubo a otra posición y volvéis a mirar la matriz de transformación, observaréis cómo la fila cuarta de la matriz ha variado y refleja la nueva posición del cubo.

Bueno, creo que con esto ya es suficiente para que entendáis el código del script controller.
script controller : follow the Path..

Sin más preámbulos, aquí tenéis el código. Hala, a palo seco.

Código:

epsilon = 0.0001.

Position = lengthinterp $line01 nt.

Tangent = lengthtangent $line01 NT.

Up = [0,0,1].

Cosine = Dot Tangent up.

If (cosine > -epsilon and cosine < epsilon) do up = [0,1,0].

Right = normalize (cross Tangent up).

Up = cross Tangent right.

Matrix3 right up Tangent position


como veis son muy pocas líneas de código. Ahora paso a explicar el código línea por línea:

Código:

epsilon = 0.0001


aquí declaramos y asignamos la variable epsilon. Normalmente, este tipo de variable con este valor, se utiliza para comparar valores en coma flotante. Me explico. Imaginad que queréis comparar una variable num en coma flotante con el valor 0, es decir:
[quote=== 0.0 do ().[/quote]En un mundo perfecto esto sería totalmente correcto, pero por desgracia, en este mundo, esto no es correcto. ¿Qué problema hay? El problema es que Maxscript tiene una precisión simple. Esto quiere decir que si tenemos un número con muchos decimales, debido a esta falta de precisón, el número final que va a utilizar Maxscript, es una versión redondeada. Esto se agraba más cuando concatenamos operación complejas como productos escalares, productos vectoriales, multiplicación de matrices o divisiones ya que el resultado se va degenerando cada vez más. Así que, si bien es posible que después de una operación donde intervienen números en coma flotante esperemos un resultado de 0, es más que probable que el resultado sea próximo a 0 pero no exactamente 0. Por ejemplo, 0.00001. Es evidente que en este caso la comparación (0.0 == 0.00001) devolvería un rotundo false. Así que la forma de solucionar esto es comparando nuestra variable num con un rango, en este caso el rango ]-epsilon, epsilon[:
[quote=> -epsilon and num < epsilon do ().[/quote]Esa comparación sí que devolvería true. Espero haberme explicado.

Pasamos a la siguiente línea:

Código:

position = lengthinterp $line01 nt


bien, nosotros queremos que nuestro objeto siga una Spline. Para ello necesitamos conocer la posición dentro de la Spline en un espacio de tiempo determinado. Por suerte, Maxscript ofrece una función, lengthinterp, que precisamente hace eso (de lo contrario nos hubiera tocado bregar con los diferentes tipos de interpolación existentes para las Splines). A este método se le pasan dos parámetros (pueden ser hasta 4. Mirad la documentación de Maxscript para más información). El primero de ellos es una referencia a la Spline en cuestión y el siguiente es el factor de interpolación. Es muy probable que muchos de vosotros os estéis preguntando que es eso del factor de interpolación. Pues bien, digamos que es un porcentaje (normalizado, es decir, en vez de 0% a 100%, de 0 a 1) que nos indica en que punto nos encontramos en la Spline. Así, un factor de interpolación de 0 nos devolverá el punto inicial de la Spline. Un valor de 1 nos devolverá el punto terminal de la Spline y un valor de 0.5 nos devolverá el punto medio de la Spline. Y así sucesivamente. Una vez explicado esto, os digo que es NT. NT es una variable que viene predefinida en el script controller y nos indica el tiempo actual, pero normalizado, es decir, una vez más, el tiempo en un rango [0,1]. Esto quiere decir que si tenemos una animación que va de 0 a 100 frames, si estamos en el frame 0, NT tendrá un valor de 0 (0/100). En el frame 50 tendrá un valor de 0.5 (50/100) y en el frame 100 un valor de 1 (100/100). Como podéis ver, esto es exactamente lo que queremos utilizar como factor de interpolación ya que en el frame 0 estaremos en el punto inicial de la Spline mientras que en el 50 estaremos en la mitad de la Spline y así sucesivamente.

Pasamos de línea:

Código:

Tangent = lengthtangent $line01 nt


ok, nosotros queremos que la posición del objeto siga la Spline, pero además, éste debe orientarse de forma que mire en la dirección de la trayectoria (de la Spline). Para ello, necesitamos saber la tangente de la curva en un espacio de tiempo determinado. Pues bien, precisamente la función lengthtangent hace eso. El funcionamiento es similar a la función lengthinterp que he comentado anteriormente. Por si no os habéis percatado aún, éste Vector tangente hará de Vector lok local a la hora de calcular la matriz de transformación. De esta forma conseguiremos que el objeto mire en la dirección de la curva. Ahora nos falta calcular los dos vectores restantes (right y up) para poder calcular la matriz de transformación final.

Empezamos con el Vector up:

Código:

up = [0,0,1]


en esta línea escogemos un Vector up temporal. Da igual el que escojamos, pero a mí me pone éste. Siguiente línea:

Código:

cosine = Dot Tangent up


aquí introducimos la operación producto escalar o producto punto (Dot product). Esta operación es esencial en la programación 3d, ya sea en el apartado gráfico, físicas, etc. Esencial. El producto escalar tiene varios comportamiento dependiendo de los valores que se pasen para realizar la operación. En este caso os bastara con saber que el producto escalar de dos vectores normalizados (Vector normalizado es un Vector cuya longitud es 1) es igual al coseno del ángulo que forman ambos vectores. Concretamente nosotros estamos calculando el coseno del ángulo que forman los vectores Tangent y up. En próximas líneas os explicaré el porqué de calcular el coseno.

Código:

if (cosine > -epsilon and cosine < epsilon) do up = [0,1,0]


bueno, aquí entra en juego la variable epsilon y el coseno que acabamos de calcular. Lo único que hace esta línea es compobar que el ángulo que forman el Vector Tangent y up sea distinto de 0 (o próximo a cero, recordad la charla sobre epsilon). Si resulta que no, que realmente el ángulo es próximo a 0, escogemos un nuevo valor para el Vector up (tened en cuenta que éste es un Vector up temporal, el up final lo calcularemos más adelante). Seguramente os estaréis preguntando por que hago esto. Pues bien, la explicación en la siguiente línea.

Código:

right = normalize (cross Tangent up)


oki Doki. Nos disponemos a calcular el Vector right. Si os acordáis, en la explicación sobre las matrices de transformación dije que los vectores right, up y lok debían formar una base ortonormal, es decir, los 3 vectores debían formar ángulos rectos (90 grados) entre cada uno de ellos. Pues precisamente vamos a empezar a hacer eso, es decir, calcular nuestra base ortonormal. Por ahora ya tenemos el Vector lok (la tangente) y tenemos un Vector up temporal. Así que es de lógica que si queremos tener una base ortonormal, el Vector right debe ser perpendicular al plano formado por los vectores Tangent y up (tened en cuenta que dos vectores definen un plano). Pues mirad por dónde aquí entra en juego otra operación tan importante como el producto escalar, el producto vectorial (cross product). Precisamente la función cross de Maxscript lo que hace es calcular el Vector perpendicular a los vectores que se le pasa como parámetros. En este caso estamos hallando el Vector perpendicular a los vectores Tangent y up que es justamente lo que andamos buscando. Además, si os fijáis, el Vector emitido por el producto vectorial es normalizado con la función normalize. Recordad lo que dije en el tema de las matrices de transformación, además de formar una base ortonormal, los vectores deben estar normalizados.

En este momento donde voy a explicar por que hice la comprobación de que el ángulo formado por los vectores Tangent y up no fuera 0. La explicación es bien sencilla. Imaginad que nuestro Vector Tangent hubiera valido [0,0,1] que es precisamente el primer valor que hemos asignado a up. Ahora hacemos el producto vectorial de ambos vectores. Podéis comprobarlo vosotros mismo escribiendo y ejecutando la siguiente línea en el Maxscript Listener:

Código:

cross [0,0,1] [0,0,1]


horror, nos ha devuelto el Vector [0,0,0] lo cual no es lo que queremos para nuestro Vector right. Ahora escribid y ejecutad la línea:

Código:

cross [0,0,1] [0,1,0]


bien, efectivamente el producto vectorial nos ha devuelto [-1,0,0] que es un Vector perpendicular a [0,0,1] y [0,1,0]. Ahora entenderéis por que he hecho la comprobación del ángulo y en el caso de que fuera 0 (o próximo a 0) por que he variado el valor del Vector up.

Pasamos línea:

Código:

up = cross Tangent right


bueno, pues ya estamos llegando al final. Ya tenemos el Vector lok y el Vector right pero sólo tenemos un Vector up temporal que lo más probable es que no sea perpendicular a los vectores lok y right (recordad, imprescindible para obtener una base ortonormal). Por eso tenemos que regenerar este Vector. ¿cómo? Pues fácil, con otro producto vectorial. Sólo que en este caso, el producto vectorial será entre los vectores lok y right. De esta forma obtendremos nuestro Vector up definitivo. Fijaos que en este caso no he normalizado el Vector resultante ya que el producto vectorial de dos vectores normalizados da como resultado otro Vector normalizado.

Y finalmente llegamos al final.

Código:

matrix3 right up Tangent position


esto es muy sencillo. Estamos creando la matriz de transformación final de nuestro objeto. No será ninguna sorpresa. Sólo tenéis que comparar esta matriz con la matriz que os mostré en la explicación sobre matrices de transformación. La vuelvo a poner:

Código:

[right.x right, y right, z]
[up.x up, y up, z]
[lok.x lok, y lok, z]
[pos.x pos, y pos, z]


la única diferencia es que en vez de llamar al Vector lok, lok, lo hemos llamado Tangent ya que es la tangente de la curva la que hemos utilizado como Vector lok para nuestro objeto. Pongo la matriz que hemos calculado para que lo veáis mejor:

Código:

[right.x right, y right, z]
[up.x up, y up, z]
[Tangent.x Tangent, y Tangent, z]
[position.x position, y position, z]


Y esto es todo, con un poco de suerte todo esto tendrá sentido. Si no (y estáis interesados en el tema), os recomiendo que leáis el mensaje varias veces y poco a poco lo iréis entendiendo.

Adjunto una escena de prueba y este mismo mensaje en formato de texto. Para ver el script controller no tenéis más que seleccionar el cono, ir a la pestaña motion, y en la lista que hay, hacer click con el botón derecho del ratón sobre la única entrada que hay (transform: transform script) y seleccionar properties. De esta forma os aparecerá el cuadro de diálogo script controller donde podéis ver el código del controlador.

En fin, espero que esto os haya resultado de utilidad.

Thanks for watching.

Archivos adjuntados

Ver más sobre el tema y los comentarios en el foro