domingo, abril 13, 2008

Espectros

La ley de Beer establece que un rayo de luz se atenúa exponencialmente al atravesar un material dieléctrico, como el vidrio. Esta ley es necesaria para reproducir materiales transparentes coloreados, como en la siguiente imagen, que copio del post anterior:
Refracción y ley de Beer
La mayoría de los ray tracers, XSight RT incluido, implementan la ley de Beer asociando un filtro a estos materiales. El código correspondiente, en XSight RT, está ubicado dentro del método Trace que heredan todos los samplers del motor:
if (attenuation)
{
// Exponential attenuation inside transparent media.
filter.blue *= (float)Math.Exp(attFltr.blue * info.Time);
filter.green *= (float)Math.Exp(attFltr.green * info.Time);
filter.red *= (float)Math.Exp(attFltr.red * info.Time);
}
Esta parece ser la extrapolación más "natural" de la ley de Beer para que se pueda aplicar a luz multicromática. Sin embargo, es fácil darse cuenta del problema que crea esta implementación. Para intervalos pequeños, es posible controlar los parámetros para dar con cualquier color deseado para el vidrio. Para intervalos grandes, sin embargo, cualquier componente del filtro de atenuación que sea menor que la unidad, terminará eliminando completamente el correspondiente canal de color de la imagen.
Por ejemplo, si digo que mi material tiene un filtro de atenuación con componentes 1, 0, 0 (para el rojo, verde y azul), estaré consiguiendo un vidrio rojo. Si cambio el filtro a 1, 0.5, 0, lograré que los objetos delgados de este material sean de color naranja... pero para objetos más gruesos, la luz terminará atenuándose hacia el rojo puro.
¿Es esto un comportamiento "natural"? Evidentemente no, sino que se trata de un comportamiento inducido por la implementación de los colores en el ray tracer. Esto me ha llevado a investigar, por simple curiosidad, sobre cómo podría implementarse un ray tracer que trabajase con píxeles de cuatro, cinco o más canales espectrales. Hay, sin embargo, muy poca documentación sobre este asunto, aunque me consta que algunos programas comerciales implementan recursos parecidos. Si usted sabe algo sobre el tema, le agradecería cualquier pista.

... y se me ocurre otra idea: buena parte del código consiste en instrucciones por triplicado que manipulan los tres componentes básicos de color de un píxel. ¿Sería posible idear una representación diferente del color que simplificase estos cálculos? Por ejemplo, ¿tiene algún sentido el ray tracing con colores en el espacio HSV? ¿Simplificaría el código? ¿Lo haría más eficiente? De momento, es una pregunta abierta...

Para terminar, una curiosidad: a primera vista, es lógico pensar que una esfera de cristal tintado debería mostrar un tono mucho más claro en los bordes, en comparación con su centro, ya que en los bordes el camino a recorrer sería menor, y se produciría menos atenuación. Sin embargo, esto no ocurre así, y puede buscar una esfera "real" de cristal para comprobarlo.
¿Por qué? Es sencillo: resulta que los rayos de luz que emergen en la zona cercana al borde viajan proporcionalmente un trayecto más largo de lo que pensamos, por culpa de las leyes de la refracción. De hecho, ocurre algo interesante e insospechado: hay una parte del interior de una esfera de cristal que, al mirarla de frente, no podemos ver, debido también a la refracción. Esto es fácil de comprobar generando imágenes de esferas de vídrio con esferas opacas más pequeñas en su interior. Es muy probable que este comportamiento se aproveche en algún truco de magia. Se lo preguntaré al espíritu de Houdini.

Etiquetas: ,

martes, marzo 04, 2008

Superesferas

Superesferas (XSight RT)
El dado de la imagen está modelado mediante una "superesfera" que se puede describir mediante la ecuación x^4 + y^4 + z^4 = 1. Al tratarse de una ecuación de cuarto grado, se puede resolver algebraicamente, algo que XSight RT hace muy eficientemente. El resultado es la figura del dado: un objeto a mitad de camino entre una esfera (de las de toda la vida) y un cubo. Si en vez de elevar a la cuarta potencia, usásemos exponentes mayores, la figura iría aproximándose más al cubo... pero la ecuación dejaría de tener una solución algebraica, y tendría que resolverse mediante el método de Newton.
Las texturas del tapete y del propio dado están modeladas, naturalmente, mediante el clásico y eficiente ruido de Perlin.

Etiquetas: , ,

sábado, marzo 01, 2008

Grietas

Crackle
Observe el cambio en el "suelo", respecto a la imagen del post anterior: he añadido un patrón que en inglés se conoce como crackle. Lo interesante es que está basado en los diagramas de Voronoi, que se definen como el conjunto de líneas equidistantes respecto a un conjunto de puntos aleatorios en el plano. Si a un robot lo situas en una habitación con objetos calientes, para evitar acercarse demasiado a ellos, tendría que calcular el diagrama de Voronoi asociado, para moverse a lo largo de las líneas del mismo.
En mi imagen, se ha utilizado el crackle para el "pigmento" del plano, pero podría utilizarse también para trucar los vectores normales, y simular que el plano está ligeramente arrugado. Esta técnica, aplicada a una superficie reflectante, puede usarse para simular la superficie del mar.
Para una versión del I King
J.L. Borges

El porvenir es tan irrevocable
Como el rígido ayer. No hay una cosa
Que no sea una letra silenciosa
De la eterna escritura indescifrable
Cuyo libro es el tiempo. Quien se aleja
De su casa ya ha vuelto. Nuestra vida
Es la senda futura y recorrida.
El rigor ha tejido la madeja.
No te arredres. La ergástula es oscura,
La firme trama es de incesante hierro,
Pero en algún recodo de tu encierro
Puede haber una luz, una hendidura.
El camino es fatal como la flecha.
Pero en las grietas está Dios, que acecha.
El patrón crackle, imitando la difracción de la luz entre las hojas de un árbol

Etiquetas: ,

domingo, febrero 24, 2008

Físicamente imposible

No es difícil verlo: ¿qué es lo que "falla" en la siguiente imagen?
Dos esferas de cristal (XSight RT)
No se trata de la distorsión de los mosaicos: la imagen ha sido generada con una lente cilíndrica. Este tipo de lentes permite representar ángulos mayores en una de las dos dimensiones (alto/ancho), al precio de que las líneas rectas no siempre se representan como líneas rectas en la imagen generada.

El problema está, en efecto, en la sombra bajo las esferas. Pero no se trata de que sobren las sombras... sino de que falta algo dentro de las sombras: el área donde se concentran los rayos por el efecto lenticular de las esferas. Es una paradoja: se puede lograr sombra con una esfera transparente.
La imagen ha sido generada, por supuesto, con XSight RT, y se trata de una prueba del nuevo soporte para refracción. La buena noticia: la simulación de materiales es buena. Me gusta más la apariencia del "vidrio" (la mezcla de reflexión y refracción dependiente del ángulo de incidencia) de XSight RT que la de POV Ray, al menos en el vidrio que éste ofrece por omisión. Además, no se pierde velocidad relativa.
Imagen generada con POV Ray¿Cómo habría que resolver lo de la sombra? POV Ray ofrece varias salidas: la primera, que fue probablemente la primera en soportar históricamente, consiste en tener en cuenta la presencia de materiales transparentes al efectuar las pruebas de oclusión. Es decir, se podría permitir que la luz directa atravesase la esfera. XSight RT no seguirá esa vía... porque también es físicamente incorrecta. El problema es que el algoritmo puro de raytracing no permite generar las superficies cáusticas. El raytracing consiste en generar los rayos a la inversa, partiendo de la cámara, y aquí hace falta trazar rayos directamente desde una fuente de luz.
La segunda solución de POV Ray, y que será la implementada por XSight RT, es utilizar una técnica llamada photon mapping. El reto está en hacerlo eficientemente, y que la técnica sea lo más "transparente" posible para el diseñador gráfico. Quiero decir, que no sea necesario indicar tropecientos parámetros para poder generar este tipo de imágenes.

Etiquetas: ,

martes, febrero 19, 2008

Recursividad anónima

... y hablando de raytracing, aquí tiene uno bastante exótico, planteado como una gigantesca consulta LINQ en C# 3.0:
Eso es jiu-jitsu y lo demás, tonterías. Como advierte el propio autor, no es una técnica "práctica", pero es un logro impresionante por su complejidad conceptual... y por la elegancia, no del código en sí quizás, pero sí de las ideas teóricas que lo soportan.
El algoritmo de raytracing tiene una característica importante: es recursivo. ¿Podemos, entonces, definir consultas recursivas en LINQ? Como las consultas LINQ están basadas en expresiones lambda, podemos ser más precisos: ¿se pueden definir expresiones lambdas recursivas en C#?
La respuesta es afirmativa, pero la técnica necesaria es complicada... y resulta difícil de leer y mantener. Mads Torgersen la explica, desde una perspectiva teórica, aquí:
Existe, no obstante, una técnica alternativa y algo más sencilla para resolver el problema. Octavio Hernández la explica en la página 85 de su libro "C# 3.0 y LINQ" (que por cierto, puede adquirir a través de nosotros). Esto que sigue es una explicación ampliada de la técnica.
¿Cuál es la dificultad para definir una función lambda recursiva? De entrada, que no podemos llamar directamente a la función por tratarse de una función anónima. Lo que sí podríamos hacer es pasar un puntero a la función como parámetro de la misma, y ejecutar la función a la que hace referencia dicho puntero... o delegado, en la jerga de .NET. El primer paso, por lo tanto, es definir un tipo delegado para funciones que se ajusten a este prototipo. Para simplificar, nos limitaremos a funciones que calculan un valor entero a partir de otro entero:
delegate int Self(Self f, int n);
Construyamos ahora una función lambda que corresponda a este prototipo, y que simule el funcionamiento del factorial:
Self F = (f, n) => n == 0 ? 1 : n * f(f, n - 1);
Esta función F (en mayúsculas) se parece extraordinariamente al factorial. ¿Qué hace falta para que sea "realmente" el factorial? Simplemente, que al llamarla pasemos, en el primero de sus parámetros, la función factorial... es decir, ella misma. Por lo tanto, la función que necesitamos es:
Func<int, int> factorial = n => F(F, n);
Y ya tenemos función anónima recursiva. ¿Le cuesta todavía verlo claro? Bien, ahí va una pista:
  • Observe que F es realmente una variable local, no una "constante de función".
  • Cuando usamos F para definir factorial, en realidad estamos "capturando" la variable local F dentro del cuerpo de la función anónima.
  • Es esta indirección la que permite, en definitiva, que el truco funcione.
  • Dicho sea de paso, observe que el identificador f, usado al "inicializar" F, es en realidad un parámetro de tipo delegado.
  • ¿Se ha dado cuenta de que he dicho "inicializar F" en vez de "definir F"?
Octavio termina su truco en este punto, y aunque se ha resuelto el problema, nos queda cierta insatisfacción, por haber utilizado el paso intermedio de la inicialización de F. Sin embargo, con muy poco esfuerzo más, se pueden mezclar las dos funciones anónimas y definir el factorial en un único paso. Esta es la expresión lambda que buscábamos:
Func<int, int> factorial = i =>
new Self((f, j) => j == 0 ? 1 : j * f(f, j - 1))(
(f, j) => j == 0 ? 1 : j * f(f, j - 1), i);
Es necesario utilizar explícitamente el constructor de Self, el tipo delegado, para que el compilador reconozca la expresión y pueda realizar la inferencia del resto de los tipos de parámetros. Para entender este monstruo, es mejor dividirlo en zonas:
Func<int, int> factorial = i =>
new Self((f, j) => j == 0 ? 1 : j * f(f, j - 1))
(
(f, j) => j == 0 ? 1 : j * f(f, j - 1),
i
);
  • La expresión amarilla se evalúa y produce un delegado. Observe que la usamos sintácticamente del mismo modo en que usaríamos Math.Sqrt o Console.WriteLine. En cierto sentido perverso y retorcido, Sqrt es una "constante de función", mientras que nuestro monstruo es una expresión que se evalúa y produce una función.
  • ¿Se ha dado cuenta de que la expresión verde es estructuralmente idéntica a la amarilla? No obstante, la expresión verde no se evalúa directamente, sino que la pasamos como parámetro para que se ejecute más adelante.
  • Finalmente, tenemos la simple y vulgar expresión azul. No hay misterio aquí, ¿verdad?
¿Complicado? Es cierto, pero la gracia de todo esto es que existe un truco alternativo, mucho más simple. Observe:
Func<int, int> factorial = null;
factorial = i => i == 0 ? 1 : i * factorial(i - 1);
Primero se declara una variable que apuntará a la función y se inicializa con un nulo. Luego se asigna el delegado definitivo... que hace referencia en su cuerpo a la variable local. Se trata de una captura de variable local, una técnica no sólo permitida por el lenguaje: es, en realidad, lo que hace que todo este rollo de las funciones anónimas tenga sentido. Lo que ahora importa es que, cuando ejecutemos el método asociado a la variable factorial, ésta ya estará debidamente inicializada.

Etiquetas: , ,

lunes, agosto 14, 2006

Blobs

BlobsXSight RT despega en complejidad. La imagen adyacente muestra un tipo de sólido llamado blob (en inglés, claro). La figura mostrada, en particular, es no convexa: una línea puede tener más de dos puntos de intersección con uno de estos sólidos.
¿Qué es un blob y cómo se define? La variante implementada por XSight RT se define ubicando esferas en el espacio. El centro de cada esfera define entonces un campo de fuerza, cuya intensidad decrece a medida que nos alejamos del centro de la esfera. Si los campos de dos esferas se solapan en una zona, sus respectivas intensidades se suman. Entonces se define un valor deseado para la intensidad, y la superficie del blob se define como los puntos en los que la intensidad de campo es igual al valor elegido. El resultado es que dos esferas, inicialmente disjuntas, pueden "soldarse" entre sí mediante una superficie continua y suave, como la del ejemplo.
El modelo usado por XSight RT es un subconjunto de los blobs implementados por POV Ray. POV Ray permite usar también cilindros como elementos dentro de un blob, pero espero poder ampliar mi implementación. Lo interesante de estos objetos es que para calcular las intersecciones sólo necesitamos resolver una ecuación polinomial de cuarto grado... de forma similar a lo que ocurre con los toros (las rosquillas, no los bichos con cuernos). Y lo mejor de todo es que XSight RT tiene un algoritmo de resolución para ecuaciones cuárticas muy eficientes. Una ecuación de cuarto grado se resuelve mediante una ecuación auxiliar de tercer grado. Pero de esta ecuación auxiliar sólo se necesita la primera raíz (de las tres posibles), y los coeficientes de la ecuación auxiliar tienen una forma especial. Mientras otros sistemas resuelven una ecuación auxiliar "completa", calculando las tres raíces, XSight RT utiliza un algoritmo optimizado.

Etiquetas: , ,