La representación volumétrica es una técnica que permite la representación de campos escalares tridimensionales. Sus aplicaciones son múltiples: representación de imágenes del cuerpo humano procedentes de diversos instrumentos sensores con fines médicos, representación realista de nubes u otros fenómenos gaseosos en simulaciones visuales en entornos virtuales, o representación de diversas estructuras y constructos en la ciencia y la ingeniería.

En este artículo se describirá una variante de la técnica basada en texturas conocida como ‘volume ray casting’. Conceptualmente dicha técnica consiste en el lanzamiento de rayos con origen en la cámara de tal forma que atraviesen el volumen que contiene el campo escalar. El color resultante que se ‘pintará’ en la superficie del volumen en el punto de entrada de cada rayo será una función de los puntos que el rayo atravesó al pasar por el volumen.

Para su implementación se utilizará la plataforma XNA y se hará uso de la aceleración grafica que nos proporcionan la GPU. Será necesaria una tarjeta que soporte al menos la versión 3.0 del modelo de sombreadores (Shader Model 3.0).

El proceso comienza con la representación de un volumen que actuará como frontera del campo escalar, es decir, el campo escalar se encontrará completamente en el interior de ese volumen. Utilizaremos un cubo.

En primer lugar representaremos las posiciones de entrada del rayo y de salida en dos texturas que posteriormente utilizaremos para determinar la dirección. Al Vertex Shader en la GPU se le harán llegar las posiciones sin transformar de los vértices del cubo. Dichas coordenadas se copiaran a la variable InterpolatedPosition a la salida del Vertex Shader. Dicha variable está marcada como coordenadas de textura por lo que a la entrada del pixel shader se recibiran las coordenadas interpoladas. Lo mismo se hará para la variable InterpolatedTransformedPosition solo que en este caso almacenaremos las coordenadas transformadas por la combinación de las matrices de posicionamiento en el mundo (world), vista (view) y proyección (projection) que serán suministradas a la entrada del programa de la GPU como parámetros. Esta última variable será utilizada posteriormente para localizar la posición exacta donde se deben muestrear las texturas para obtener la dirección del rayo.

    1 struct VertexShaderInput

    2 {

    3     float4 Position : POSITION0;

    4 };

    5

    6 struct VertexShaderOutput

    7 {

    8     float4 Position : POSITION0;

    9     float4 InterpolatedPosition : TEXCOORD0;

   10     float4 InterpolatedTransformedPosition : TEXCOORD1;

   11 };

   12

   13 VertexShaderOutput VertexShaderFunction(VertexShaderInput input)

   14 {

   15     VertexShaderOutput output;

   16

   17     float4 worldPosition = mul(input.Position, World);

   18     float4 viewPosition = mul(worldPosition, View);

   19     output.Position = mul(viewPosition, Projection);

   20     output.InterpolatedPosition = input.Position;

   21     output.InterpolatedTransformedPosition = output.Position;

   22     return output;

   23 }

   24

   25 float4 RenderPositionsPixelShaderFunction(VertexShaderOutput input) : COLOR0

   26 {

   27     float4 color = input.InterpolatedPosition;

   28     return color;

   29 }

Se representarán las caras frontales del cubo en una textura utilizando el estado de representación CullMode.CounterClockWise. El color asignado a la textura en realidad contiene las coordenadas tridimensionades del cubo en el espacio de coordenadas local del modelo en el formato (x,y,z,1).

A continuación se repite la operación pero esta vez representando las caras interiores del cubo.

El siguiente y ultimo paso es la representación del cubo, solo que en esta ocasión el color resultante será función del campo escalar, muestreado en base a las direcciones grabadas en las texturas anteriores. Al muestrear cada una de las texturas anteriores podemos obtener para cada punto (x,y) de la textura, las coordenadas (x,y,z) del cubo por las cuales entró el rayo y por las cuales salió. Con estas dos coordenadas se puede calcular la dirección del rayo y al conocer el punto inicial y la dirección del rayo que atraviesa el campo escalar se puede recorrer cada uno de los puntos de ese campo para calcular su contribución al color de la superficie del cubo. Este proceso se realiza en el Pixel Shader que representará el resultado final en pantalla.

    1 float4 RenderVolumePixelShaderFunction(VertexShaderOutput input) : COLOR0

    2 {

    3     //Calculamos que puntos de las texturas debemos muestrear

    4     float2 texC = input.InterpolatedTransformedPosition.xy /= input.InterpolatedTransformedPosition.w;

    5     //lo llevamos al rango [0,1] espacio de coordenadas de textura desde espacio de proyeccion 2D

    6     texC.x =  0.5f*texC.x + 0.5f;

    7     texC.y = -0.5f*texC.y + 0.5f;

    8     //Muestreamos las texturas de posiciones iniciales y finales

    9     float3 frontPos = tex2D(FrontTextureSampler, texC);

   10     float3 backPos = tex2D(BackTextureSampler, texC);

   11     //Calculamos el punto inicial y la direccion

   12     float4 currentPosition = float4(frontPos,0);

   13     float3 direction = normalize(backPos - frontPos);

   14     //Inicializamos las variables del color

   15     float4 color = float4(0, 0, 0, 0);

   16     float4 src = 0;

   17     float value = 0;

   18     //Recorremos el campo escalar en la dirección calculada

   19     //acumulando opacidad en función del campo

   20     float3 Step = direction * (1.0f/256.0f);

   21     for(int i = 0; i < 256; i++)

   22     {

   23         //muestreamos la textura       

   24         value = tex3Dlod(VolumeTextureSampler, currentPosition).r;

   25         src = (float4)value;

   26         //Front to back blending

   27         src.rgb *= src.a;

   28         color = (1.0f - color.a)*src + color;

   29         //advance the current position

   30         currentPosition.xyz += Step;

   31     }

   32     return color;

   33 }

El rendimiento de esta técnica al ser completamente acelerada por GPU es muy bueno como se puede constatar en los siguientes videos obtenidos con una tarjeta gráfica de gama media/baja con un consumo de CPU mínimo.

Basado en un artículo original de Graphic Runner.
Campos escalares obtenidos de vorbis.
Bibliografía: GPU - Based Interactive Visualization Techniques - D. Weiskopf