Representación volumétrica en WebGL


Figura 1. Un ejemplo de representaciones volumétricas realizadas por el renderizador WebGL descrito en la publicación. Izquierda: simulación de la distribución de probabilidad espacial de electrones en una molécula de proteína de alto potencial. Derecha: tomograma de un árbol bonsai. Ambos conjuntos de datos se toman del repositorio Open SciVis Datasets .

En la visualización científica, la representación volumétrica se usa ampliamente para visualizar campos escalares tridimensionales. Estos campos escalares son a menudo cuadrículas homogéneas de valores que representan, por ejemplo, la densidad de carga alrededor de la molécula, una resonancia magnética o tomografía computarizada, una corriente de aire que envuelve el avión, etc. La representación volumétrica es un método conceptual simple para convertir dichos datos en imágenes: al muestrear los datos a lo largo de los rayos del ojo y asignar color y transparencia a cada muestra, podemos crear imágenes útiles y hermosas de tales campos escalares (ver Figura 1). En el procesador de GPU, estos campos escalares tridimensionales se almacenan como texturas 3D; sin embargo, WebGL1 no admite texturas 3D, por lo que se requieren hacks adicionales para emularlos en la representación de volumen. WebGL2 agregó recientemente soporte para texturas 3D, permitiendo que el navegador implemente una representación de volumen elegante y rápida. En esta publicación, discutiremos los fundamentos matemáticos del renderizado volumétrico y hablaremos sobre cómo implementarlo en WebGL2 para crear un renderizador volumétrico interactivo que funcione completamente en el navegador. Antes de comenzar, puede probar el renderizador volumétrico en línea descrito en esta publicación.

1. Introducción



Figura 2: representación volumétrica física, teniendo en cuenta la absorción y emisión de luz por volumen, así como los efectos de dispersión.

Para crear una imagen físicamente realista a partir de datos volumétricos, necesitamos simular cómo los rayos de luz son absorbidos, emitidos y dispersados ​​por el medio (Figura 2). Aunque modelar la propagación de la luz a través de un medio a este nivel crea resultados hermosos y físicamente correctos, es demasiado costoso para la representación interactiva, que es el objetivo del software de visualización. En las visualizaciones científicas, el objetivo final es permitir a los científicos investigar de forma interactiva sus datos, así como hacer preguntas sobre su tarea de investigación y responderlas. Dado que un modelo de dispersión completamente físico sería demasiado costoso para el renderizado interactivo, las aplicaciones de visualización utilizan un modelo simplificado de absorción de emisiones, ya sea ignorando los costosos efectos de dispersión o aproximándolos de alguna manera. En este artículo, consideramos solo el modelo de absorción de emisiones.

En el modelo de absorción de emisiones, calculamos los efectos de iluminación que aparecen en la Figura 2 solo a lo largo del rayo negro, e ignoramos los que surgen de los rayos grises punteados. Los rayos que pasan a través del volumen y llegan a los ojos acumulan el color emitido por el volumen y se desvanecen gradualmente hasta que el volumen los absorbe por completo. Si rastreamos los rayos del ojo a través del volumen, podemos calcular la luz que ingresa al ojo integrando el haz sobre el volumen para acumular emisión y absorción a lo largo del haz. Tome un rayo que cae en el volumen en un punto s=0y sin volumen en un punto s=L. Podemos calcular la luz que ingresa al ojo usando la siguiente integral:

C(r)= int0LC(s) mu(s)e− int0s mu(t)dtds


A medida que el haz pasa a través del volumen, integramos el color emitido C(s)y absorción  mu(s)en cada punto sa lo largo de la viga. La luz emitida en cada punto se desvanece y devuelve al ojo la absorción de volumen hasta este punto, que se calcula por el término e− int0s mu(t)dt.

En el caso general, esta integral no puede calcularse analíticamente; por lo tanto, debe usarse una aproximación numérica. Realizamos la aproximación de la integral tomando muchas muestras Na lo largo del rayo en el intervalo s=[0,L]cada uno de los cuales se encuentra a una distancia  Deltasseparados uno del otro (Figura 3), y resumiendo todas estas muestras. El término de amortiguación en cada punto de muestreo se convierte en el producto de la absorción acumulada en serie en muestras anteriores.

C(r)= sumi=0NC(i Deltas) mu(i Deltas) Deltas prodj=0i−1e− mu(j Deltas) Deltas


Para simplificar aún más esta suma, aproximamos el término de amortiguación ( e− mu(j Deltas) Deltas) él cerca de Taylor. También por conveniencia presentamos alfa  alpha(i Deltas)= mu(i Deltas) Deltas. Esto nos da la ecuación de composición alfa realizada de adelante hacia atrás:

C(r)= sumi=0NC(i Deltas) alpha(i Deltas) prodj=0i−1(1− alpha(j Deltas))



Figura 3: Cálculo de la integral de la representación de la absorción-emisión en volumen.

La ecuación anterior se reduce a un bucle for, en el que pasamos a través del haz paso a paso a través del volumen y acumulamos iterativamente color y opacidad. Este ciclo continúa hasta que el rayo abandona el volumen o el color acumulado se vuelve opaco (  alpha=1) El cálculo iterativo de la suma anterior se realiza utilizando las conocidas ecuaciones de composición de adelante hacia atrás:

 hatCi= hatCi−1+(1− alphai−1) hatC(i Deltas)


 alphai= alphai−1+(1− alphai−1) alpha(i Deltas)


Estas ecuaciones finales contienen la opacidad previamente multiplicada para una mezcla adecuada,  hatC(i Deltas)=C(i Deltas) alpha(i Deltas).

Para renderizar una imagen de volumen, simplemente trace el rayo desde el ojo hasta cada píxel y luego realice la iteración que se muestra arriba para cada rayo que cruza el volumen. Cada rayo procesado (o píxel) es independiente, por lo que si queremos renderizar la imagen rápidamente, necesitamos una forma de procesar una gran cantidad de píxeles en paralelo. Aquí es donde la GPU es útil. Al implementar el proceso de raymarching en el sombreador de fragmentos, podemos usar el poder de la computación de GPU paralela para implementar un renderizador de volumen muy rápido.


Figura 4: Raymarching sobre una cuadrícula de volumen.

2. Implementación de GPU en WebGL2


Para que se realice el marcado de rayos en el sombreador de fragmentos, es necesario forzar a la GPU a ejecutar el sombreador de fragmentos en los píxeles a lo largo de los cuales queremos rastrear el rayo. Sin embargo, la tubería OpenGL funciona con primitivas geométricas (Figura 5) y no tiene formas directas de ejecutar un sombreador de fragmentos en un área específica de la pantalla. Para solucionar este problema, podemos renderizar algún tipo de geometría intermedia para ejecutar el sombreador de fragmentos en los píxeles que necesitamos renderizar. Nuestro enfoque para renderizar el volumen será similar al de Shader Toy y a los renderizadores de escenas de demostración , que representan dos triángulos de pantalla completa para realizar un sombreador de fragmentos, y luego hace el verdadero trabajo de renderizado.


Figura 5: La canalización de OpenGL en WebGL consta de dos etapas de sombreadores programables: un sombreador de vértices, que se encarga de convertir los vértices de entrada en un espacio de clip, y un sombreador de fragmentos, que se encarga de sombrear los píxeles cubiertos por el triángulo.

Aunque renderizar dos triángulos de pantalla completa a la manera de ShaderToy funcionará, realizará un procesamiento de fragmentos innecesario cuando el volumen no cubra toda la pantalla. Este caso es bastante común: los usuarios alejan la cámara del volumen para ver una gran cantidad de datos en general o para estudiar partes características grandes. Para limitar el procesamiento de fragmentos a solo píxeles afectados por el volumen, podemos rasterizar el paralelogramo delimitador de la cuadrícula de volumen y luego realizar el paso de marcado de rayos en el sombreador de fragmentos. Además, no necesitamos renderizar las caras frontal y posterior del paralelogramo, porque con un cierto orden de representación de triángulos, el sombreador de fragmentos en este caso se puede realizar dos veces. Además, si renderizamos solo las caras frontales, entonces podemos encontrar problemas cuando el usuario hace zoom porque las caras frontales se proyectarán detrás de la cámara, lo que significa que se recortarán, es decir, estos píxeles no se procesarán. Para permitir a los usuarios acercar completamente la cámara al volumen, solo mostraremos las caras inversas del paralelogramo. La tubería de renderizado resultante se muestra en la Figura 6.


Figura 6: Tubería WebGL para el volumen de raymarching. Rasterizaremos las caras inversas del paralelogramo del volumen delimitador para que el sombreador de fragmentos se ejecute para los píxeles que tocan este volumen. Dentro del sombreador de fragmentos, renderizamos los rayos a través del volumen paso a paso para renderizar.

En esta canalización, la mayor parte del renderizado real se realiza en el sombreador de fragmentos; sin embargo, aún podemos usar el sombreador de vértices y el equipo para la interpolación fija de funciones para realizar cálculos útiles. El sombreador de vértices convertirá el volumen en función de la posición de la cámara del usuario, calculará la dirección del haz y la posición del ojo en el espacio de volumen, y luego los transferirá al sombreador de fragmentos. La dirección del haz calculada en cada vértice se interpola en un triángulo mediante un equipo de interpolación de función fija en la GPU, lo que nos permite calcular las direcciones de los rayos para cada fragmento un poco menos costoso, sin embargo, cuando se transfieren al sombreador de fragmentos, estas direcciones pueden no normalizarse, por lo que Todavía tengo que normalizarlos.

Representaremos el paralelogramo delimitador como un solo cubo [0, 1] y lo escalaremos según los valores de los ejes de volumen para proporcionar soporte para volúmenes de volumen desigual. La posición del ojo se convierte en un solo cubo, y en este espacio se calcula la dirección del haz. Raymarching en un solo espacio de cubo nos permitirá simplificar las operaciones de muestreo de textura durante raymarching en un sombreador de fragmentos. porque ya estarán en el espacio de coordenadas de textura [0, 1] del volumen tridimensional.

El sombreador de vértices utilizado por nosotros se muestra arriba, las caras posteriores rasterizadas pintadas en la dirección del haz de visibilidad se muestran en la Figura 7.

#version 300 es layout(location=0) in vec3 pos; uniform mat4 proj_view; uniform vec3 eye_pos; uniform vec3 volume_scale; out vec3 vray_dir; flat out vec3 transformed_eye; void main(void) { // Translate the cube to center it at the origin. vec3 volume_translation = vec3(0.5) - volume_scale * 0.5; gl_Position = proj_view * vec4(pos * volume_scale + volume_translation, 1); // Compute eye position and ray directions in the unit cube space transformed_eye = (eye_pos - volume_translation) / volume_scale; vray_dir = pos - transformed_eye; }; 


Figura 7: Caras inversas del paralelogramo de volumen delimitado pintado en la dirección de la viga.

Ahora que el sombreador de fragmentos procesa los píxeles para los que necesitamos representar el volumen, podemos marcar el volumen y calcular el color de cada píxel. Además de la dirección del haz y la posición del ojo, calculada en el sombreador de vértices, para representar el volumen, necesitamos transferir otros datos de entrada al sombreador de fragmentos. Por supuesto, para empezar, necesitamos una muestra de textura 3D para muestrear el volumen. Sin embargo, el volumen es solo un bloque de valores escalares, y si los usáramos directamente como valores de color ( C(s)) y opacidad (  alpha(s)), una imagen renderizada en escala de grises no sería muy útil para el usuario. Por ejemplo, sería imposible resaltar áreas interesantes con diferentes colores, agregar ruido y hacer que las áreas de fondo sean transparentes para ocultarlas.

Para dar al usuario control sobre el color y la opacidad asignados a cada valor de muestra, se utiliza un mapa de color adicional llamado función de transferencia en los renderizadores de visualizaciones científicas. La función de transferencia establece el color y la opacidad que se asignarán a un valor específico muestreado del volumen. Aunque existen funciones de transferencia más complejas, por lo general, tales funciones usan tablas de búsqueda de color simples, que pueden representarse como una textura unidimensional de color y opacidad (en formato RGBA). Para aplicar la función de transferencia cuando se realiza el marcado por rayos de volumen, podemos muestrear la textura de la función de transferencia en función de un valor escalar muestreado de la textura de volumen. Los valores de color de retorno y la opacidad se utilizan como C(s)y  alpha(s)muestra

Los últimos datos de entrada para el sombreador de fragmentos son las dimensiones de volumen que usamos para calcular el tamaño del paso del haz (  Deltas) para muestrear cada vóxel a lo largo del haz al menos una vez. Dado que la ecuación de haz tradicional tiene la forma r(t)= veco+t vecd, para el cumplimiento, cambiaremos la terminología en el código y denotaremos  Deltascomo  textttdt. Del mismo modo, el intervalo s=[0,L]a lo largo de la viga, cubierta por volumen, denotamos como [ texttttmin, texttttmax].

Para realizar un marcado de volumen en un sombreador de fragmentos, haremos lo siguiente:

  1. Normalizamos la dirección del haz de visibilidad recibido como entrada desde el sombreador de vértices;
  2. Cruce la línea de visión con los límites del volumen para determinar el intervalo. [ texttttmin, texttttmax]realizar raymarching con el objetivo de generar volumen;
  3. Calculamos tal longitud de paso  textttdtpara que cada vóxel se muestree al menos una vez;
  4. Comenzando en el punto de entrada a r( texttttmin), recorramos el haz a través del volumen hasta llegar al punto final en r( texttttmax)
    1. En cada punto, tomamos muestras del volumen y utilizamos la función de transferencia para asignar color y opacidad;
    2. Acumularemos color y opacidad a lo largo del haz utilizando la ecuación de composición de adelante hacia atrás.

Como una optimización adicional, puede agregar la condición para salir prematuramente del ciclo de raymarching para completarlo cuando el color acumulado se vuelva casi opaco. Cuando el color se vuelve casi opaco, cualquier muestra posterior no tendrá casi ningún efecto en el píxel, porque su color será completamente absorbido por el medio ambiente y no llegará al ojo.

El sombreador de fragmentos completo para nuestro procesador de volumen se muestra a continuación. Se le han agregado comentarios, marcando cada etapa del proceso.


Figura 8: Resultado de visualización de bonsai procesado listo desde el mismo punto de vista que en la Figura 7.

Eso es todo!

El renderizador descrito en este artículo podrá crear imágenes similares a las que se muestran en la Figura 8 y la Figura 1. También puede probarlo en línea . En aras de la brevedad, omití el código Javascript necesario para preparar el contexto WebGL, cargar texturas de volumen y funciones de transferencia, configurar sombreadores y renderizar un cubo para renderizar volumen; El código de renderizador completo está disponible para referencia en Github .

Source: https://habr.com/ru/post/437526/


All Articles