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: , ,

sábado, febrero 09, 2008

XSight RT v1.1, para .NET v3.5

Si se aburre, puede entretenerse un poco experimentando con XSight RT v1.1, para .NET Framework 3.5. La descarga contiene el instalador del ejecutable, la ayuda y un puñado de escenas para pruebas. No he incluido el código fuente.
La aplicación permite editar escenas con un lenguaje sencillo y bien documentado, y generar imágenes a partir de estas descripciones. De momento, funciona como una aplicación SDI, permitiendo trabajar con un único documento por instancia de la misma. No está conectado todavía el soporte de refracción y transparencias, ni la perturbación de normales (es una técnica con un nombre perturbador).

Etiquetas: ,