Visualización de Escenas 3D Fotorealistas mediante Hardware

Anuncio
Visualización de escenas 3D
fotorrealistas mediante hardware
gráfico programable (GPU)
Federico Jorge Marino
[email protected]
Tesis de Grado en Ingeniería Informática
Director: Ing. Horacio Abbate
Facultad de Ingeniería,
Universidad de Buenos Aires
Resumen
El trabajo comienza exponiendo los fundamentos de las técnicas de iluminación
global para la síntesis de imágenes partir de un modelo 3D y su base físico-matemática.
Se reseñan los diferentes algoritmos entre los que se encuentran: Ray Tracing, Path
Tracing, Radiosity y Photon Mapping.
Luego se analizan las plataformas disponibles para implementar sistemas de
rendering en tiempo real como ser clusters de PCs o GPUs (Graphics Processing Unit).
Se analiza en detalle la arquitectura de las GPU y las técnicas para computar algoritmos
de propósito general en ellas (GPGPU).
A continuación se propone un caso de estudio que consiste en implementar el
algoritmo de Photon Mapping sobre una GPU utilizando OpenGL y el lenguaje Cg.
Para ello, se diseño una capa de software orientada a objetos basada en los conceptos
del paradigma Streaming Programming Model y la extensión de OpenGL, Framebuffer
Object para implementar kernels y streams utilizando los mecanismos de cómputo del
hardware gráfico.
Sobre esa base se construyo un motor de Photon Mapping adaptando una estructura
de aceleración de trazado de rayos denominada BVH (Bounding Volumen Hierarchy) y
la técnica de Photon Splatting para computar la iluminación indirecta.
Finalmente se evalúa el desempeño, escalabilidad de la implementación en relación
a varios parámetros, y se analizan diferentes imágenes generadas a partir de un modelo
de caja de Cornell (Cornell Box).
I
Agradecimientos
Debo comenzar agradeciendo especialmente a Horacio, por sus consejos, su
dedicación y su paciencia durante el extenso desarrollo de este proyecto.
Siguiendo por los docentes de la Facultad de Ingeniería que me orientaron en mi
búsqueda de un proyecto interesante para investigar.
También tengo que agradecer a mi familia, por su colaboración en la redacción y
revisión de varios puntos que integran este trabajo.
Siguiendo por mi madre, sin cuya asistencia logística/gastronómica no hubiera
podido dedicar el tiempo suficiente a esta tarea.
Por último, a mis amigos y conocidos que creyeron que algún día este proyecto
estaría concluido.
Federico J. Marino
Octubre de 2007
II
Índice General
Resumen
I
Agradecimientos
II
1. Introducción
1
2. Fundamentos de la iluminación global
2
2.1. La física de la luz
2
2.2. Terminología de iluminación
2
2.3. Técnicas
6
2.4. La ecuación del Rendering
6
2.5. Interacción luz-superficie
7
2.5.1. La función BSSRDF
7
2.5.2. La función BRDF
8
2.5.3. Reflectancia
9
2.5.4. Reflectancia difusa
9
2.5.5. Reflexión especular
10
2.5.6. Refracción
11
3. Algoritmos de iluminación global
12
3.1. Ray Tracing clásico
12
3.1.1. Estructuras de aceleración
14
3.1.2. Ventajas
14
3.1.3. Desventajas
14
3.2. Path Tracing
14
3.2.1. Ventajas
16
3.2.2. Desventajas
17
3.3. Bidirectional Path Tracing
17
3.3.1. Ventajas
18
3.3.2. Desventajas
18
3.4. Metropolis Light Transport
18
3.4.1. Ventajas
18
I
3.4.2. Desventajas
19
3.5. Radiosity
19
3.5.1. Etapa 1, determinación de los factores de forma
20
3.5.2. Etapa 2, resolución del sistema de ecuaciones
21
3.5.3. Etapa 3, sntesis de la imagen
22
3.5.4. Ventajas
22
3.5.5. Desventajas
22
3.7. Photon Mapping
22
3.7.1. Motivación y antecedentes
22
3.7.2. El método
23
3.7.3. Fase 1, trazado de fotones
23
3.7.4. Fase 2, rendering
26
3.7.5. Desempeño y mejoras
27
4. Plataformas de Rendering de alto desempeño
29
4.1. Clusters de PCs
29
4.2. Hardware gráfico programable
30
5. GPU (Graphics Processing Unit)
32
5.1. El pipeline gráfico
33
5.2. El pipeline gráfico programable
35
5.3. Modelo de memoria de la GPU
37
5.4. El procesador de vértices
38
5.5. El rasterizador
38
5.6. El procesador de fragmentos programable
39
5.7. Stream Programming Model
39
5.8. GPGPU
40
5.9. Computando con un programa de fragmentos
41
6. Implementación de un caso de estudio
45
6.1. Proyección perspectiva de la escena
45
6.2. Fuente de luz
46
6.3. Geometría de la escena
46
6.4. Modelo de iluminación
47
II
6.5. Marco de trabajo basado en Kernels y Streams
48
6.5.1. Clase FpKernel
48
6.5.2. Clase FpStream
49
6.5.3. Ejemplo de uso
51
6.6. Trazador de fotones y rayos en la GPU
51
6.6.1. La estructura de aceleración BVH
52
6.6.2. Clase Tracer
54
6.6.3. El trazado de fotones
55
6.7. Photon Splatting
56
6.8. Motor de Rendering basado en Photon Mapping
59
6.8.1. La clase SceneLoader
60
6.8.2. La clase PhotonMapper
60
6.8.3. La clase Raytracer
62
7. Evaluación de la implementación
64
7.1. Entorno de hardware y software
64
7.2. Mediciones del desempeño
64
7.3. Imágenes
70
8. Conclusiones
74
8.1. Trabajos Futuros
74
A Publicaciones
76
B Código Fuente
77
Referencias
94
III
Capítulo 1 - Introducción
Los gráficos generados por computadora son cada vez una parte más fundamental de
la vida cotidiana. Desde ámbitos como el cine en donde mediante técnicas sofisticadass
casi cualquier producción con un presupuesto mediano, puede realizan complejas
simulaciones que derivan en la creación de personajes o escenarios virtuales de extremo
realismo. Pasando por la industria de los videojuegos, en donde una audiencia cada vez
más exigente y demandante de efectos y simulaciones más reales, impulsan el desarrollo
de la tecnología de semiconductores, dotando a los dispositivos de juego de un poder de
cómputo extraordinario. Llegando a aplicaciones de CAD y animación 3D para
computadores personales que permiten diseñar y modelar objetos o escenas que pueden
ser navegadas virtualmente en tiempo real.
La creación o síntesis de imágenes fotorrealistas se refiere al proceso de generar
imágenes artificiales capaces de engañar al ojo humano al tal punto que resulten casi
indistinguibles de fotografías tomadas del mundo real.
Hace más de dos décadas surgieron las primeras técnicas, como Ray Tracing (1980)
y Radiosity (1984), que utilizaron por primera vez simulaciones basadas en principios
físicos. Dichos métodos tienen su origen en otros campos anteriores a la computación
gráfica como la óptica y los problemas de transferencia de calor.
Tras años de evolución, se han logrado modelar muchos de los efectos de la
interacción entre la luz y las superficies. Sin embargo para lograr altos niveles de
realismo aún se requieren recursos computacionales importantes. En el caso de la
industria cinematográfica los tiempos que demanda la generación de un cuadro de
animación no son un factor fundamental (aunque puedan demandar horas o días). Si, en
cambio, lo es la calidad del resultado obtenido.
Cuando se trata de emplear estos algoritmos de síntesis de imágenes realistas en
aplicaciones de tiempo real generalmente se llega a una solución de compromiso entre
una menor calidad a cambio de una mayor velocidad de generación.
La rápida evolución en el hardware de procesamiento gráfico disponible para PCs,
ha renovado el interés en investigar nuevas formas de utilizarlo ya sea para implementar
algoritmos de iluminación mas complejos o para el cómputo de otros tipos de
aplicaciones no gráficas que impliquen procesar grandes volúmenes de datos.
En este trabajo se busca conocer el estado de arte en cuanto a las técnicas de
iluminación fotorrealistas, analizar que plataformas están disponibles para construir
aplicaciones de tiempo real que implementen ese tipo de métodos y diseñar una
solución que reuna aspectos de los últimos trabajos en la materia.
1
Capítulo 2 - Fundamentos de la iluminación global
2.1 La física de la luz
La luz tiene una naturaleza dual ya que posee las propiedades de una onda y de una
partícula. La luz visible es radiación electromagnética con longitudes de onda en el
rango de los 380 a los 780 nanómetros.
A lo largo de la historia, la luz intentó ser explicada mediante diversos modelos:
•
•
•
•
Óptica de rayos: explica efectos como reflexión y refracción.
Óptica de ondas: explica todos los fenómenos de la óptica de rayos y además
explica el efecto de interferencia y de difracción.
Óptica electromagnética: incluye a la óptica de ondas y agrega explicaciones a
los fenómenos de dispersión y polarización.
Óptica de partículas: explica la interacción entre luz y materia.
En computación gráfica se usa casi exclusivamente la óptica de rayos, o también
llamada óptica geométrica.
El proceso que determina el color que tendrá la luz que entra al ojo proveniente de
una superficie, es dinámico y complejo. Este proceso está regido por las propiedades
físicas de la luz y de los materiales que componen las superficies.
Si se observa la luz, desde su aspecto corpuscular, se la puede considerar como una
serie de paquetes de energía que viajan a la velocidad de la luz, denominados fotones.
Cuando un fotón interactúa con una superficie pueden suceder tres casos:
•
•
•
Reflexión: si el fotón rebota en una superficie perfectamente suave, el ángulo es
determinado por la ley de reflexión, la cual especifica que el mismo, es igual y
opuesto al ángulo de incidencia respecto de la normal (reflexión especular). En
el caso de superficies irregulares o rugosas, el ángulo de reflexión es mas difícil
de predecir ya que la normal en el punto de impacto solo puede determinarse en
forma probabilística (reflexión difusa).
Refracción (o también conocido como transición): el fotón viaja a través de
una superficie. La dirección se determina según la ley de refracción o ley de
Snell que tiene en cuenta los índices de refracción de los dos medios que
comparten la superficie.
Absorción: el fotón genera la excitación de los átomos de la superficie
convirtiendo la energía lumínica en calor o reemitiéndola.
La intensidad o brillo de la luz es proporcional al número de fotones y su color
depende de la energía contenida en el fotón.
2.2 Terminología de iluminación
La energía de un fotón eλ varía de acuerdo a su longitud de onda λ y está definida
por:
2
eλ =
hc
(2.1)
λ
Donde h ≈ 6,63 ⋅ 10−34 J ⋅ s es la constante de Planck y c es la velocidad de la luz.
La energía radiante espectral Qλ, en nλ fotones con longitud de onda λ se define como:
Qλ = nλ ⋅ eλ =
nλ ⋅ hc
(2.2)
λ
La energía radiante Q es la energía de un conjunto de fotones que se calcula
integrando la energía espectral sobre un espectro de longitudes de onda, dada por:
∞
Q = ∫ Q λ dλ
(2.3)
0
El flujo radiante Ф, es la cantidad de energía irradiada por diferencial de tiempo
definido como:
Φ=
dQ
dt
(2.4)
La densidad de flujo radiante por unidad de área, se define como el diferencial de
flujo dФ por diferencial de área dA , sobre una superficie determinada y su expresión
es:
dΦ
d 2Q
=
dA dt ⋅ dA
(2.5)
La exitancia radiante M o radiosidad B, se refieren a la densidad de flujo radiante
por unidad de área, saliendo de una superficie en un punto x y está definida por:
M ( x) = B( x) =
dΦ
dA
(2.6)
La irradiancia E es la densidad de flujo radiante por unidad de área, llegando o
incidiendo en el punto x de una superficie es:
E ( x) =
dΦ
dA
(2.7)
r
La intensidad radiante, es el flujo radiante por unidad de ángulo sólido dw , definida
por:
r
dΦ
I ( w) = r
dw
(2.8)
3
r
La radiancia, es el flujo radiante por unidad de ángulo sólido dw por unidad de área
proyectada dA (ver figura 2.1):
r
L( x, w) =
∞
d 2Φ
d 4 nλ
hc
dλ
r = ∫0
r
cos θ ⋅ dA ⋅ dw
cos θ ⋅ dw ⋅ dA ⋅ dt ⋅ dλ λ
(2.9)
Representa la radiancia expresada como la integral sobre las longitudes de onda del
flujo de energía en nλ fotones por diferencial de área dA por diferencial de ángulo
r
r
sólido dw por unidad de tiempo, donde θ es el ángulo entre la dirección dw y la
r
normal n de la superficie. Esta cantidad es la que representa de modo más cercano el
color de un objeto. Puede pensarse como el número de fotones llegando por unidad de
tiempo a un área pequeña desde una dirección determinada y puede usarse para
describir la intensidad de luz en un punto dado y una dirección determinada.
Figura 2.1: La radiancia, L,
r definida como el flujo radiante por
unidad de ángulo sólido dw , por unidad de área proyectada dA.
Cuando se trabaja con iluminación que proviene de cierta dirección no tiene sentido
tener en cuenta un solo rayo que sería infinitamente delgado porque virtualmente ningún
fotón viajaría en él. En cambio tiene mas sentido hablar del área que rodea a cierta
dirección. También conocido como ángulo cónico, el ángulo sólido, es la “porción de
cielo” que abarca un objeto visto desde un punto dado. Se puede decir que representa
simultáneamente el tamaño angular y la dirección (expresada en coordenadas esféricas)
de un haz. Su unidad es el estereoradián (sr) y es equivalente a un radián al cuadrado.
r
El tamaño de un diferencial de ángulo sólido, dw (ver figura 2.2) en coordenadas
esféricas es:
r
dw = (dθ ) * (sin θ ⋅ dφ ) = sin θ ⋅ dθ ⋅ dφ
(2.10)
Donde, θ es el ángulo entre la dirección y la normal de la superficie S1 y φ es el
ángulo entre la dirección proyectada sobre el plano tangente a la superficie y el eje x.
El lado derecho de la ecuación (2.10) expresa el área infinitesimal en la esfera
unitaria como un producto del largo del arco de longitud dθ y la longitud del arco de
latitud sin θdφ .
4
Figura 2.2: El ángulo sólido.
r
La dirección w del ángulo sólido se puede calcular como:
x = sin θ * cos φ
y = sin θ * sin φ
(2.11)
z = cos θ
5
2.3 Técnicas
Las técnicas de iluminación pretenden simular o aproximar los fenómenos físicos de
distribución de la luz (reflexión, refracción, absorción, etc.) sobre una escena 3D, con el
objetivo de calcular la intensidad lumínica en cualquier punto del modelo.
Las técnicas de iluminación directa son aquellas que tienen en cuenta solamente la luz
que llega a las superficies en forma directa, desde las fuentes. En cambio, las de
iluminación global, permiten capturar la iluminación indirecta (la que proviene de
reflexiones y refracciones en otras superficies), lo cual aporta una gran cuota de
realismo a las imágenes generadas aunque a un costo de cómputo mayor. Sin ella, las
imágenes obtenidas tienen un aspecto plano y sintético.
Estos algoritmos se alimentan de datos como:
•
•
•
Descripción geométrica de la escena (vértices, caras, normales, etc.).
Descripción de materiales de las superficies (coeficientes de brillo,
transparencia, color, rugosidad, etc.).
Descripción de fuentes de luz (color, dirección, potencia, etc.).
Existen numerosas técnicas de iluminación global, pero sin duda las más famosas
son Ray Tracing y Radiosity. Analizándolas en detalle la mayoría se pueden incluir en
uno de estos dos grupos:
•
•
Métodos de muestreo de puntos: por ejemplo Ray Tracing, donde se toman
gran cantidad de muestras de la iluminación en diferentes puntos de la escena.
Métodos de elementos finitos: por ejemplo Radiosity. La distribución de luz se
calcula resolviendo un sistema de ecuaciones lineales que representa el
intercambio de luz entre parches de las superficies.
Además hay técnicas híbridas que combinan aspectos de ambos métodos.
2.4 La ecuación del Rendering
La ecuación, que fue presentada por Kajiya [1], surgió como una forma abstracción
o generalización de los distintos algoritmos de iluminación global. Provee un contexto
unificado para analizar cada método particular y describe matemáticamente el transporte
de luz en cada punto de una escena.
Los distintos algoritmos de iluminación global producen resultados que definen
flujos de energía mediante la aproximación numérica de esta ecuación, que se basa en la
ley de conservación de energía.
r
Básicamente dice que toda la luz emitida en la dirección w por el punto x, Lo, es la
r
suma de la luz reflejada por el punto, Lr y dispersada en la dirección w mas la luz
emitida por el punto Le (es no nula solo si el punto es emisor de luz).
r
r
r
Lo( x, w) = Le ( x, w) + Lr ( x, w)
(2.12)
Desarrollando el término de la radiancia reflejada (Lr) obtenemos:
6
r
r
Lo ( x, w) = Le( x, w) +
r r
r r r
r
∫ f ( x, w' , w) L ( x, w)(w'⋅n )dw'
r
i
r
w '∈Ω
(2.13)
r
Donde Ω es el conjunto de todas las direcciones posibles, n es la normal de la
superficie, Li es la radiancia incidente para cada dirección y fr es la denominada función
BRDF (Bidirectional Reflectance Distribution Function) que para un punto x, da la
r
fracción de la luz total proveniente de la dirección w' que es reflejada en la dirección
r
saliente w .
El valor de la función BRDF varía entre 0 (no hay reflexión) y 1 (reflexión especular
r
perfecta) y además para cada par x, w' se cumple que:
r r
∫ fr ( x, w' , w) ≤ 1
(Cada punto no refleja más que la luz que recibe) (2.14)
r
w '∈Ω
Algunas características a destacar de la ecuación son:
•
•
•
Es lineal (solo está compuesta por sumas y multiplicaciones).
Homogeneidad espacial (es la misma para todas las posiciones y orientaciones).
Depende de la longitud de onda λ de la luz, por lo tanto la ecuación debería ser
resuelta para cada λ posible. En la práctica se resuelve para un solo valor
(obteniendo la luminancia) o para las tres componentes RGB.
Debido a que, la radiancia incidente Li en un punto, es la radiancia saliente Lo de
r
otro punto x’ en la dirección – w' , la integral se convierte en recursiva; lo cual la hace
imposible de resolver por métodos analíticos, excepto para casos muy simples. Es por
ello, que los métodos disponibles llevan a una solución aproximada.
Existen algunos fenómenos naturales que no pueden ser modelados por esta
ecuación como ser:
•
•
•
•
La fluorescencia (la luz es reflejada con una longitud de onda diferente).
La fosforescencia (la luz es reflejada un instante mas tarde).
La polarización.
La interferencia.
2.5 Interacción luz-superficie
El fenómeno de dispersión es aquel donde algunas formas de radiación como en este
caso la luz se desvían de su trayectoria original ya sea porque encuentran un obstáculo o
cambios en el medio que atraviesan.
2.5.1 La función BSSRDF
Esta función, presentada por Nicodemus et al. [2], según sus siglas en inglés
Bidirectional Scattering Surface Reflectance Distribution Function, describe el proceso
por el cual un haz de luz que ingresa en un material, se dispersa en su interior para luego
abandonar la superficie en una ubicación diferente (ver figura 2.3). Esto es común
especialmente en materiales traslucidos como la piel, el mármol o la cera, donde solo un
bajo porcentaje se refleja directamente desde el punto de impacto en la superficie. Este
comportamiento ocurre en cierta medida, en la mayoría los materiales no metálicos.
7
Figura 2.3: Funciones BRDF y BSSRDF.
La función BSSRDF (ecuación 2.15) [2], S, relaciona un diferencial de radiancia
r
reflejada, dLr, en x en la dirección w , con el diferencial de flujo incidente dΦi , en x’ en
r
la dirección w' . Dado que tiene de ocho dimensiones, es muy costosa su evaluación.
r
r
r
dLr ( x, w)
S ( x, w, x' , w' ) =
r
dΦi ( x' , w' )
(2.15)
2.5.2 La función BRDF
La función BRDF (Bidirectional Reflectance Distribution Function), fue presentada
por Nicodemus et al. [2] y describe las características reflectivas de las superficies o
también se dice que describe un modelo local de iluminación.
Surge de simplificar la BSSRDF, condicionando a que el punto en el que la luz
impacta y en el que se refleja sea el mismo (x=x’), reduciendo así la dimensionalidad de
la función a seis.
r
r
r r
dLr ( x, w)
dLr ( x, w)
fr ( x, w' , w) =
r =
r r r r
dEi ( x, w' ) Li ( x, w' )( w'⋅n )dw'
(2.16)
Esta función fr, define que porcentaje de la irradiancia se convierte en radiancia
r
reflejada y donde n es la normal en x.
Partiendo del conocimiento del campo de radiancia incidente en un punto de la
superficie, es posible calcular la radiancia reflejada (ecuación 2.17) en todas las
direcciones, integrando la radiancia incidente Li:
r
r r
r
r r
r r r r
Lr ( x, w) = ∫ fr ( x, w' , w)dE ( x, w' ) = ∫ fr ( x, w' , w) Li ( x, w' )( w'⋅n )dw'
Ω
(2.17)
Ω
r
Aquí n es la normal en el punto x de la superficie, y Ω es el hemisferio de
r r
direcciones incidentes en x (cabe notar que w ⋅ n = cos θ donde θ es el ángulo entre la
r
r
normal n y la dirección w ).
La función BRDF posee las siguientes propiedades:
8
•
•
•
Invariancia posicional: en general la función es invariante respecto de la
posición si se trata de superficies homogéneas. Por el contrario, no es invariante
en superficies rugosas o heterogéneas.
Reciprocidad: que establece que la función es independiente de la dirección en
la que fluye la luz, por lo tanto es posible trazar los recorridos de la luz en ambas
direcciones.
Conservación de energía: una superficie no puede reflejar más luz de la que
recibe.
Existen dos clases de funciones BRDF:
•
•
Isotrópicas: se refiere a aquellas en donde propiedades de reflectancia no varían
según la dirección. Ocurre en superficies muy suaves.
Anisotrópicas: por el contrario, se refiere a esos casos en donde las propiedades
son diferentes o dependen de la dirección.
La mayoría de los materiales tienen principalmente un comportamiento isotrópico, y
exhiben en un grado menor comportamiento anisotrópico.
2.5.3 Reflectancia
Esta cantidad representa la relación entre el flujo de luz incidente y reflejado en un
punto x. La reflectancia ρ , es un valor entre 0 y 1. La fracción de luz que no es
reflejada es absorbida o refractada.
ρ ( x) =
dΦ r ( x )
dΦ i ( x )
(2.18)
En la práctica, para la mayoría de los materiales, la distribución de la energía
reflejada en las diferentes direcciones, puede modelarse como la suma de dos
componentes:
•
•
Reflexión difusa.
Reflexión especular.
2.5.4 Reflectancia difusa
Típicamente ocurre en superficies rugosas o materiales que dispersan la luz dentro
de sus superficies como por ejemplo tierra, piedra o tela. Las pinturas del tipo “mate”
tienen una alta proporción de reflexión difusa. También se conoce con el nombre de
superficies lambertianas a aquellas que exhiben este comportamiento.
La luz incidente es reflejada en todas las direcciones del hemisferio de manera
uniforme (ver figura 2.4). La dirección de reflexión es perfectamente aleatoria, por lo
r
tanto la función BRDF, fr,d, es constante respecto de la dirección saliente w y se define
como:
r
r
Lr ( x, w) = fr , d ( x) ∫ dEi ( x, w' ) = fr , d ( x) Ei ( x)
(2.19)
Ω
9
Luego término, ρd , conocido como reflectancia difusa, se define como:
ρd ( x ) =
dΦ r ( x )
=
dΦ i ( x )
r
Lr ( x)dA∫ dw
Ω
Ei ( x)dA
r
fr , d ( x) Ei ( x)dA∫ dw
Ω
=
Ei ( x)dA
Dado que
= π ⋅ fr , d ( x )
r
∫ dw = π
(2.20)
(2.21)
Ω
Figura 2.4: Reflexión difusa ideal o lambertiana (izquierda),
reflexión difusa (derecha).
2.5.5 Reflexión especular
Sucede cuando un haz de luz choca con una superficie suave, típicamente metálica o
de un material dieléctrico (vidrio o agua).
En el caso ideal (reflexión especular perfecta o reflexión tipo espejo) de una
superficie perfectamente suave, toda la luz incidente se reflejaría en una dirección que
forma un ángulo igual y opuesto al ángulo de incidencia respecto de la normal de la
superficie (según la ley de reflexión), también conocida como dirección de reflexión del
espejo (ver figura 2.5).
Pero la mayoría de las superficies son imperfectas, por lo tanto en estos casos, la luz
es reflejada alrededor de un pequeño cono centrado en la dirección de reflexión del
espejo. Este tipo de materiales se denominan “glossy” (ver figura 2.5). El grado de
imperfección es un parámetro habitual del material llamado comunmente rugosidad
usado en los modelos de reflexión.
La radiancia debido a la reflexión especular está definida como:
r
r
Lr ( x, ws ) = ρs ( x) Li ( x, w' )
(2.22)
r
r
Donde w , es la dirección incidente, ws es la dirección de reflexión del espejo, ρs , es
la reflectancia especular y Li la radiancia incidente.
r
En el caso de una reflexión especular perfecta, ws es:
10
r
r r r r
w s = 2( w ⋅ n ) n − w
(2.23)
r
r
Donde ws = 1 y w = 1
Figura 2.5. Reflexión especular perfecta o tipo espejo (izquierda),
reflexión especular tipo “glossy” (derecha).
2.5.6 Refracción
Es el efecto de cambio en la dirección de una onda debido a un cambio en su
velocidad. Sucede cuando una onda pasa de un medio a otro. En óptica ocurre cuando
un haz de luz pasa de un medio con un cierto índice de refracción a otro con un índice
diferente.
Figura 2.6: Refracción de un haz de luz.
La ley de Snell o ley de refracción se utiliza para calcular la dirección del rayo
r
refractado wr , en función de los índices de refracción de los dos medios η1 y η2, la
r
r
dirección incidente w y la normal de la superficie n (ver figura 2.6).
La relación entre los ángulos está dada por:
η 1 sin θ 1 = η 2 cos θ 2
(2.24)
Y la dirección del rayo refractado se define como:
r
r r
r
η1 r r r r
η1
wr = − ( w − ( w ⋅ n )n ) − ( 1 − ( ) 2 (1 − ( w ⋅ n ) 2 ) )n
η2
η2
(2.25)
11
Capítulo 3 - Algoritmos de iluminación global
3.1 Ray Tracing clásico
Fue una de las primeras técnicas que propuso una solución de iluminación global, ya
que mediante un algoritmo recursivo incorpora efectos de reflexión, refracción y
sombras. Fue presentada por Whitted en 1980 [3]. Se basa en la observación que los
rayos de luz que importan son solo aquellos que llegan al ojo del observador.
En la naturaleza las fuentes de luz emiten fotones que se dispersan por la escena y
solo una pequeña fracción de ellos alcanza al ojo, pero simular el proceso de esa
manera, no es práctico.
La idea principal es trazar el camino recorrido por la luz en sentido inverso, desde el
ojo hacia las fuentes de luz, utilizando el hecho de que los fotones se mueven en línea
recta a través del espacio vacío y que la dispersión de la luz en las superficies es
simétrica.
Los parámetros de entrada del algoritmo de Ray Tracing son:
•
•
•
•
La posición del observador.
Un plano de vista (dirección y campo de visión).
La descripción de la geometría y los materiales de la escena.
Las características de las fuentes de luz.
Un rayo r se define por un origen x y una distancia d que es la recorrida en la
r
dirección w , y se resume en la siguiente fórmula:
r
r
r ( x, w) = x + d ⋅ w
(3.1)
El algoritmo pretende calcular el color (la radiancia promedio) de cada uno de los
píxeles que conforman el plano de vista. Esto se logra trazando uno o más rayos por
cada píxel (ver figura 3.1) y promediando los valores obtenidos.
Figura 3.1: Trazado inverso del recorrido de un haz de luz.
12
El recorrido completo de un haz de luz se conforma de varios segmentos. Cada
vértice del recorrido representa una interacción con las superficies que hace cambiar su
dirección. En la figura 3.1 se observa que el primer segmento o rayo del recorrido (ojo
del observador a x1) se denomina rayo primario y es aquel que atraviesa el plano de
proyección. Luego en los puntos x1, x2 y x3 se generan rayos secundarios producto de la
reflexión especular y refracción en las respectivas superficies.
Para calcular la radiancia del rayo primario debemos hallar la menor distancia (el
menor valor de d) a la cual el rayo interseca un objeto, dicho de otro modo, hay que
encontrar el objeto visto a través del píxel.
Hallado el punto de intersección x, debemos calcular la radiancia saliente en la
dirección del rayo. Para tal fin, debemos conocer la normal de la superficie y la función
BRDF, fr en x. Con esos datos podemos computar la iluminación aportada por cada
fuente de luz estimando la irradiancia en x.
Por ejemplo, la radiancia reflejada, Lr, debido a una fuente puntual con potencia
Φl en el punto p se puede calcular como:
r r
r
r r
w⋅ n
Φl
Lr ( x, w) = fr ( x, w, w' )
V ( x, p )
2
4π
p−x
(3.2)
r
Donde w = ( p − x) / p − x es el vector unitario en la dirección de la fuente de luz.
La función de visibilidad, V, se evalúa trazando un rayo de sombra desde el punto x
hacia la luz. Si el rayo de sombra interseca un objeto entre x y la luz entonces V=0, de
lo contrario V=1.
Para superficies especulares o traslúcidas el algoritmo debe evaluar la reflexión
especular o refracción trazando rayos secundarios y sumar su aporte a la radiancia total
del recorrido, utilizando el mismo procedimiento usado para el rayo primario. A
continuación se detalla una versión simplificada del algoritmo:
Para cada píxel de la imagen
crear un rayo desde el ojo a través de un píxel
Color de píxel=trazar(rayo)
trazar(rayo)
encontrar intersección mas cercana con la escena
computar punto de intersección y normal
color = sombrear(punto, normal)
return color
sombrear(punto,normal)
color=0
para cada fuente de luz
trazar rayo de sombra hacia la luz
si rayo de sombra interseca luz
color=color + iluminación directa
si es especular
color = color+ trazar(rayo reflejado/refractado)
return color
Dada la naturaleza recursiva de método, la complejidad computacional crece
rápidamente a medida que el árbol de recorridos y rayos aumenta en su profundidad. Es
13
por ello que se han propuesto numerosas estrategias para mejorar el desempeño de Ray
Tracing, como por ejemplo la del control adaptativo del nivel máximo de recursión a
alcanzar en el trazado según las características de la escena. Otras estrategias, buscan
aprovechar la coherencia existente entre rayos paralelos, cercanos en el plano de vista,
que probablemente tendrán interacciones similares con las superficies, para ahorrar u
optimizar las ciclos de trazado.
3.1.1 Estructuras de aceleración
En las primeras implementaciones de Ray Tracing se iteraba sobre todos los objetos
para verificar su intersección con cada rayo. Esto genera un problema de búsqueda
lineal de orden O(N) en una escena con N objetos, lo cual resultaba muy ineficiente.
En cambio, es conveniente subdividir el modelo en regiones más pequeñas y solo
verificar la intersección de cada rayo con los objetos de las regiones que este atraviesa.
De este modo se logran tiempos de búsqueda sub-lineales. Este concepto se materializa
en lo que se conoce como estructura de aceleración, que es una estructura de datos
ordenada y construida mediante un preproceso, a fin de reducir la cantidad de
verificaciones de intersección entre rayos y objetos.
Estás estructuras pueden clasificarse en dos grandes grupos:
•
•
De subdivisión espacial: el espacio de la escena se particiona en regiones.
Luego conociendo que regiones atraviesa el rayo, se prueba la intersección
únicamente con los objetos que estén contenidos en ellas. Algunos ejemplos son
las grillas uniformes (regiones de igual tamaño)[4], grillas jerárquicas [5], los
KD-trees o la BSP (Binary Space partition) [7].
De agrupamiento de objetos: los elementos de geometría forman grupos que a
su vez conforman una jerarquía de volúmenes simples. Entonces, si no hay
intersección entre un volumen y el rayo, se evita probar la intersección con todos
los objetos incluidos en dicho volumen. Un ejemplo de esta estructura es BVH
(Bounding Volumen Hierachy) que se detalla en el punto 6.6.1.
Además, existen numerosas estructuras híbridas que combinan aspectos de las
principales.
3.1.2 Ventajas
•
Simplicidad de implementación.
3.1.3 Desventajas
•
•
•
•
•
No puede calcular la iluminación indirecta en superficies difusas.
No permite computar sombras suaves.
No permite computar efectos de enfoque debido a los lentes de cámara.
Es dependiente del punto de vista de la escena.
No puede simular inter-reflexión difusa entre superficies.
3.2 Path Tracing
Es una técnica que extiende el método de Ray Tracing, como una solución a la
ecuación del rendering y fue presentada por Kajiya en 1986 [1]. Incorporó conceptos
14
propuestos por Cook et al. en 1984 [9] en su algoritmo conocido como Distribution Ray
Tracing. Esté último utiliza el muestreo estocástico para computar efectos como
“motion blur”, profundidad de campo y sombras suaves.
Path Tracing hace posible calcular los efectos que requieren evaluar los problemas
de integración recursiva trazando rayos en forma aleatoria en el dominio de integración
para calcular el valor de la integral, método al cual se lo conoce como Monte Carlo.
El concepto principal de la teoría de Monte Carlo es que la integral i puede ser
aproximada evaluando la función f sobre un número de muestras xi (i=1..N). Obteniendo
un valor estimado para I expresado como I :
i = ∫ f ( x)dx
I =
1
N
N
f ( xi )
∑ p( x )
i =1
(3.3)
i
Donde p(xi) es la probabilidad de elegir xi . Lo general del método lo hace aplicable
a integrales de varias dimensiones o a funciones arbitrarias. Sin embargo, aunque el
estimador de I converge a I cuando N → ∞ , la convergencia es lenta. El error estándar
es proporcional a 1 N por lo tanto para reducir el error a la mitad se necesitan cuatro
veces mas muestras.
Aplicándolo a Path Tracing, la función desconocida es la distribución de luz. Aquí el
método de Monte Carlo busca resolver la ecuación del rendering completa, por medio
de la generación de todos los posibles recorridos de los rayos comenzando en el ojo y
terminando en las fuentes de luz (ver figura 3.2).
Figura 3.2: Trazado de múltiples recorridos de rayos por cada píxel.
Con cada impacto sobre una superficie, se suma la radiancia emitida desde el punto
y se envía un nuevo rayo en una dirección aleatoria seleccionada de un hemisferio
alrededor del punto, de acuerdo con la BRDF de la superficie. Si el rayo impacta en otra
superficie, el algoritmo se hace recursivo, el resultado es multiplicado por la
probabilidad de elegir esa dirección y agregado al estimador actual.
15
Para estimar con precisión la radiancia en un punto, se debe muestrear y promediar
un gran número (varios miles) de recorridos de rayos por cada píxel.
Un detalle importante de Path Tracing es que utiliza un solo rayo
reflejado/refractado para estimar la iluminación indirecta. Si utilizara varios rayos en
cada evento de dispersión de luz, provocaría un crecimiento exponencial en el número
total de rayos requeridos, dada su naturaleza recursiva. Además, el autor señaló que era
conveniente concentrar esfuerzos en los eventos que son resultado de pocas reflexiones.
Al trazar un solo rayo por evento (reflexión, refracción, etc.) se asegura que el mismo
esfuerzo es invertido en todas las superficies que son vistas directamente por el
observador.
Para evitar que el algoritmo genere recorridos de rayos de longitud infinita hay que
fijar un criterio de terminación. Una opción sería trazar todos los recorridos hasta una
longitud máxima fija, lo cual resulta ineficiente y generaría errores considerables en el
resultado de la estimación al truncar recorridos que podrían aportar a la radiancia del
píxel en cuestión.
La solución alternativa y elegante es incorporar la idea del muestreo probabilístico al
problema de determinar la longitud del recorrido, sin agregar un sesgo al resultado de la
estimación. Esta técnica conocida como Ruleta Rusa, introducida al área de
computación grafica por Arvo et al. [26], es una forma de muestreo que tiene en cuenta
la distribución probabilística de la función a estimar para eliminar partes no importantes
del dominio.
En este hace posible trazar un haz a través de un número finito de rebotes y aún así
obtener un resultado comparable al que se hubiera logrado trazándolo un número
infinito de rebotes.
Dado que el proceso de generación de cada recorrido depende de variables aleatorias
(la dirección de reflexión al intersecar una superficie difusa es escogida al azar) y que
en píxeles cercanos el valor de iluminación estimado puede variar considerablemente, es
común que se produzca un efecto de ruido (varianza del estimador) en la imagen final.
Para reducir dicho efecto es necesario trazar muchos recorridos por píxel y promediar su
valor.
Hay métodos para reducir la varianza de los métodos de Monte Carlo. Uno de ellos
es conocido como muestreo por importancia, se fundamenta en tratar de tomar las
muestras en aquellas zonas en donde la función desconocida tiene valores elevados
(usando algún conocimiento a priori de la función) para lograr así que el método
converja más rápido. Por ejemplo, una forma de lograr esto es concentrar las muestras
en zonas más iluminadas, las cuales probablemente estén cerca de las fuentes de luz.
Aplicando esta idea a Path Tracing, se suelen trazar recorridos secundarios desde cada
nodo de un recorrido principal hacia las zonas brillantes.
Otro método que funciona muy bien especialmente en escenas de exteriores que
incluyen luz de cielo (la cual varía suavemente) es el método de muestreo estratificado.
La idea es dividir el espacio de muestreo en celdas, de manera de tratar de cubrir de
manera uniforme dicho espacio.
3.2.1 Ventajas
•
•
•
No requiere etapas de preprocesamiento.
Maneja cualquier tipo de función BRDF.
Maneja cualquier tipo de geometría.
16
•
No requiere demasiado espacio de memoria.
3.2.2 Desventajas
•
•
La varianza del estimador de radiancia (el ruido que resulta de usar pocos
recorridos por píxel y por lo tanto pocas muestras para estimar la iluminación).
Convergencia lenta (sobre todo en escenas complejas en donde la probabilidad
de que un recorrido alcance una zona de alta iluminación, es muy baja).
3.3 Bidirectional Path Tracing
El término bidireccional, se refiere a que los recorridos de los rayos se originan
desde dos puntos: el ojo del observador y la fuente de luz. Este cambio logra captar mas
fácilmente los efectos de refracción (vaso de vidrio o una lupa) o de reflexión que
producen concentraciones de luz sobre superficies difusas. Este efecto tiene una baja
probabilidad de ser captado por el Path Tracing tradicional, ya que implicaría que el
rayo pasara por una serie de reflexiones que terminen en la fuente de luz. La técnica fue
presentada por Lafortune y Willems [10] en 1993 e independientemente por Veach y
Guibas [11] en 1994 como una extensión del algoritmo de Path Tracing.
La idea del método es comenzar trazando dos recorridos parciales iniciados
respectivamente en la fuente de luz y el ojo del observador. Luego se intenta conectarlos
mediante segmentos para los cuales se evalúa una función de visibilidad V. Dicha
función define si los segmentos o caminos aportan o no iluminación según estén o no
obstruidos por alguna superficie de la escena. La figura 3.3 ilustra un ejemplo en donde
cada nodo Ei (vértices del recorrido originado en el ojo) se intenta conectar con todos
los Li (vértices del recorrido originado en la fuente de luz). Del resultado de estas
combinaciones surgirán una serie de recorridos válidos que capturarán de forma mas
eficiente efectos de inter-reflexión o iluminación indirecta.
Figura 3.3: Combinación de recorridos parciales en Bidirectional Path Tracing.
17
3.3.1 Ventajas
•
Se requieren evaluar menos recorridos por píxel.
3.3.2 Desventajas
•
•
La varianza o el ruido de la imagen final es un problema al igual que en el
método tradicional.
Cada recorrido de luz implica mayor trabajo ya que hay que evaluar las
conexiones entre ambos recorridos parciales E y L.
3.4 Metropolis Light Transport
Fue presentado por Veach y Guibas [28] en 1997. En realidad, el método tiene sus
orígenes en la década de 1950 para la resolución de problemas de física computacional.
La estrategia que utiliza es concentrar el trabajo de muestreo en las zonas que más
aportan a la imagen ideal final o en regiones de alta radiancia. En lugar de hacer un
muestreo aleatorio de la función a integrar, a medida que se generan recorridos (se crean
de modo similar que en Bidirectional Path Tracing), estos son clasificados según su
aporte. Luego a partir de los recorridos que más contribuyen, se aplican mutaciones de
segmentos o de pequeños conjuntos de vértices, para generar nuevos recorridos.
Luego del proceso de mutación, se analiza si el nuevo recorrido es aceptado o
descartado dependiendo de si está obstruido por alguna superficie y según una función
de aceptación cuidadosamente definida. Si no se cumplen las condiciones, el recorrido
es descartado como inválido.
La función de aceptación permite que los cambios a un recorrido sean aceptados
siempre en una dirección, mientras que algunas veces son rechazados en la dirección
opuesta.
Para las mutaciones se usan dos diferentes estrategias:
•
•
Permutaciones: la idea es que produciendo cambios pequeños (corrimiento de
los vértices) a un recorrido con alta contribución a la iluminación, se producirá
un nuevo recorrido con alta probabilidad de aceptación. Estas estrategias de
mutación se enfocan a capturar efectos específicos como el efecto “caustics”
(concentración de luz debido a la reflexión o refracción en superficies curvas,
como el que produce una lupa o un vaso de vidrio). Se dividen en permutaciones
de lentes, permutaciones de caustics y permutaciones de cadenas múltiples.
Mutaciones bidireccionales: consisten en reemplazar aleatoriamente tramos del
recorrido por nuevos segmentos, pudiendo modificar su longitud total.
3.4.1 Ventajas
•
•
Muy efectivo en escenas “difíciles”. Esto es en aquellas donde el aporte a la
iluminación viene de un área reducida (como por ejemplo un agujero en la
pared) que conduce a un área muy iluminada. Una vez que logra encontrar un
camino a través del agujero, generará mediante mutaciones nuevos recorridos
que capturarán la iluminación de esa zona.
No es sesgado.
18
3.4.2 Desventajas
•
•
•
No es eficiente en escenas simples (aquellas escenas donde la luz se encuentra
distribuida de manera uniforme).
No es aplicable a fuentes de luz puntuales.
El nivel de ruido generado en la imagen final es afectado por numerosos
parámetros del algoritmo y escoger los valores óptimos a fin de reducirlo no
resulta trivial y es altamente dependiente de la escena particular.
3.5 Radiosity
Fue presentado por Goral et al. [12] en 1984 y luego mejorado por Cohen et al.
[13,14]. Tiene su origen en los principios físicos de transferencia de radiación térmica.
La técnica solo es capaz de computar la iluminación global de escenas con superficies
difusas y que puedan ser subdivididas en pequeñas superficies planas (denominadas
parches, ver figura 3.4) sobre las cuales la radiancia reflejada y emitida se considera
constante.
Típicamente, solo tiene en cuenta aquellos recorridos de luz que salen de la fuente y
se reflejan en forma difusa un cierto número de veces hasta alcanzar el ojo del
observador. Incluyendo estás restricciones se logra simplificar la ecuación del rendering
y la función BRDF ya no depende de la dirección incidente, quedando de la siguiente
forma.
Lo = Le + fr ∫ Li cos θdwi
(3.4)
Ω
Donde Lo la radiancia reflejada es la suma de la radiancia emitida Le más la BRDF
por la integral sobre el hemisferio, de la radiancia incidente Li por el coseno del ángulo
de incidencia θ.
Figura 3.4. Escena subdividida en parches.
19
La radiosidad Bi para cada parche se define como la suma de la luz auto-emitida Ei
(no nulo solo para fuentes de luz) más la luz reflejada, donde ρi es la constante de
reflectancia del parche (es parte de la descripción de la escena) y Fij el denominado
factor de forma.
N
Bi = Ei + ρi ∑ BjFij
ρi ∈ 0..1
(3.5)
j =1
El factor de forma es una cantidad sin unidad, que representa la fracción de la
energía total saliendo del parche i que alcanza al parche j. Su cómputo se realiza a partir
del tamaño, la orientación y posición de ambos parches. Dado que depende solo de las
características geométricas, el factor es válido para cualquier longitud de onda.
Si aplicamos la última ecuación (ecuación 3.5) sobre todos los parches de la escena,
obtenemos un sistema de N ecuaciones con N términos (B1...BN), el cual puede ser
resuelto numéricamente por el método de Gauss-Seidel.
1 − ρ 1F 11 − ρ 1F 12 ... − ρ 1F 1N   B1   E1 
 − ρ 2 F 21 1 − ρ 22 F 22 ... − ρ 2 F 2 N   B 2   E 2 

⋅  =  
 ...
  ...   ... 
...
...

    
− ρNFN 2 ... 1 − ρNFNN   BN   EN 
 − ρ NFN 1
El algoritmo consta de tres etapas: determinación de factores de forma, calculo de
radiosidad (resolución del sistema de ecuaciones) y rendering de la imagen final.
3.5.1 Etapa 1, determinación de los factores de forma
Para que el cálculo de factores de forma sea preciso es necesaria una subdivisión en
parches suficientemente fina como para obtener sombras de buena resolución.
Figura 3.5: Factor de forma.
Su expresión matemática es la siguiente (ver figura 3.5):
Fij =
1
Ai
V (i, j ) cos(θi ) cos(θj )dAidAj
πr 2
Ai Aj
∫∫
(3.6)
Donde Ai es el área del parche i, θi es el ángulo entre su normal y el diferencial de
área del parche j, pur útlimo, la función V(i,j) determina la visibilidad entre ambos
puntos y puede valer 0 o 1.
20
Hay que destacar que el factor de forma Fij tiene carácter recíproco, o sea que
Fij=Fji. Por lo tanto basta con calcular la mitad de los factores y además Fii es
obviamente nulo.
Existen dos maneras de calcularlo: con el método del hemicubo y con el método de
trazado de rayos.
El método del hemicubo fue propuesto por Cohen et al. en 1985 [14]. La idea es que
el área de la proyección del parche sobre un hemisferio que envuelve al parche receptor
dividida por el área total del hemisferio da el factor de forma (ver figura 3.6).
Figura 3.6: cómputo de factor de forma por el método del hemicubo.
En la práctica se utiliza un hemicubo en lugar de un hemisferio, ya que aunque es
posible proyectar los parches analíticamente sobre el hemisferio, esto es mucho más
complejo. El hemicubo y sus 5 caras está subdividido en píxeles, para los cuales se
precalcula un factor de forma delta. Luego, el algoritmo consiste en proyectar todos los
parches de la escena sobre el hemicubo. En caso de encontrar que dos o mas parches se
proyectan sobre el mismo píxel se almacena la información del mas cercano. Para
finalizar, el factor de forma de un parche i se obtiene sumando todos los factores delta
de los píxeles asociados al parche i. Este proceso es fácilmente implementable por
hardware. La precisión del cálculo se puede ajustar modificando la cantidad de píxeles
del hemicubo. El resultado también depende de la distancia entre los parches y el
tamaño de estos.Si el parche es muy pequeño respecto de la distancia puede
directamente desaparecer del cálculo.
Por otra parte, el método de trazado de rayos, consiste en aplicar el método de
Monte Carlo, trazando estocásticamente rayos desde el parche i y contabilizando
cuantos de ellos alcanzan el parche j. Entonces, el factor de forma se aproxima mediante
el cociente entre la cantidad de los rayos que llegaron a j y el número total de rayos
trazados N. Cuando N tiene infinito el valor del conciente converge al de la integral que
se pretende estimar.
3.5.2 Etapa 2, resolución del sistema de ecuaciones
El sistema de ecuaciones puede ser resuelto en forma completa o iterativa.
El primer modo requiere que la matriz principal permanezca en memoria, lo cual no
es práctico en escenas con miles de parches (una escena con N parches genera un
sistema de N2 términos) y además puede llevar mucho tiempo.
21
El otro modo es utlizando el algoritmo denominado Progressive Radiosity, que
consiste en calcular una solución inicial gruesa o aproximada (que puede ser
visualizada) la cual luego será refinada hasta alcanzar un criterio de convergencia
establecido. Este método solo requiere la presencia en memoria de una sola fila o
columna de la matriz principal.
3.5.3 Etapa 3, síntesis de la imagen
La información de iluminación indirecta obtenida en la etapa 2, puede ser utilizada
por un Ray Tracer estándar o un algoritmo de Scan-Line para computar la imagen final.
Típicamente, los valores de radiosidad aplicados a los vértices, pueden ser interpolados
usando sombreado de Gouraud [31] y ser visualizados interactivamente.
3.5.4 Ventajas
•
•
Información de iluminación indirecta puede ser precalculada y reutilizada
mientras no cambie la información geométrica.
Solución de iluminación global independiente de la vista (no depende de la
posición del observador).
3.5.5 Desventajas
•
•
•
•
Solo resuelve iluminación para superficies difusas.
Obliga a subdividir la geometría en pequeños elementos (parches) y la calidad
de la solución depende de esta subdivisión.
Requerimientos de memoria se incrementan con parques mas chicos.
No captura efectos de iluminación de alta frecuencia (de rápida variación sobre
un área reducida), como ser sombras con bordes bien definidos (hard shadows).
3.6 Photon Mapping
3.6.1 Motivación y antecedentes
Existen métodos híbridos de varias pasadas que combinan las técnicas de elementos
finitos con las basadas en Monte Carlo, utilizando una fase inicial de preprocesamiento
donde se computa una solución preliminar gruesa de la iluminación indirecta por el
método de Radiosity y luego una segunda fase que calcula la imagen final usando
métodos de Monte Carlo Ray Tracing como Path Tracing.
Estas combinaciones son más rápidas y de mejor calidad que los dos métodos en su
variante más pura, pero sufren de las limitaciones de los métodos de elementos finitos,
que no pueden manejar geometría compleja.
Otra alternativa son los mapas de iluminación (illumination maps), donde un mapa
de texturas (mapas de bits) se utiliza para representar la irradiancia del modelo. Aquí el
problema es determinar la resolución adecuada de estas texturas y cómo mapearlas en
superficies complejas.
Los únicos métodos que pueden lidiar con la simulación de iluminación global
completa en modelos complejos y funciones BRDF arbitrarias son los métodos basados
en Monte Carlo Ray Tracing. Sus ventajas son que no requieren convertir la geometría
en un mallado de parches, consumen poca memoria, y dan un resultado correcto excepto
por la varianza o ruido, lo cual constituye su mayor problema.
22
Un defecto común de los algoritmos de estimación de densidad es que su
representación de los impactos de las partículas de luz está directamente acoplada a la
descripción geométrica de la escena, condicionando el nivel de detalle de la iluminación
a la definición del modelo. En casos extremos puede ocurrir que algunos elementos
geométricos no reciban ningún impacto y por ende sean visualizados en negro.
Está claro a partir de análisis de ventajas y desventajas de métodos mencionados
previamente, que un buen algoritmo de iluminación global debe realizar el muestreo
desde ambos puntos de vista, las fuentes de luz y desde el observador para captar todos
los efectos. Otra observación importante es que la iluminación en general varía
suavemente en grandes áreas y tiene variaciones grandes en otras áreas más pequeñas.
3.6.2 El método
De todas las consideraciones anteriores surge el método de Photon Mapping
desarrollado por Jensen [15]. El término fotón en este contexto se utiliza para
denominar una partícula que transporta una cantidad discreta de energía lumínica en una
dirección específica. El método se clasifica como un algoritmo de estimación de
densidad (de fotones por unidad de área) de dos fases: distribución de fotones y
recopilación de estadísticas de iluminación (síntesis de la imagen).
La característica principal de la técnica es que la estructura de datos que almacena la
distribución de fotones está desvinculada de la representación geométrica de la escena y
se denomina mapa de fotones (Photon Map).
Este mapa es una versión multidimensional de un árbol de búsqueda binaria llamado
árbol KD, donde todos los nodos son fotones. Es similar a los árboles BSP, donde cada
nodo subdivide el espacio en dos sub-espacios (dos nodos hijos) excepto por las hojas.
Photon Mapping consiste de dos fases:
•
•
Trazado de fotones: construcción del mapa de fotones trazándolos desde las
fuentes de luz.
Rendering: utilizando la información del mapa de fotones se sintetiza la imagen
final.
La estimación de densidad usando el mapa de fotones presenta errores de baja
frecuencia a diferencia del Monte Carlo Ray Tracing tradicional, lo cual es una gran
ventaja desde el punto de vista perceptivo ya que este ruido de baja frecuencia es mucho
menos notorio que el de alta frecuencia. El precio que se paga es que el método es
sesgado (no converge al valor de la integral que se pretende aproximar).
3.6.3 Fase 1, trazado de fotones
Es el proceso que construye el mapa de fotones (Photon Map). Estos se crean en las
fuentes de luz que pueden tener una diversidad de formas o distribuciones particulares
(ver figura 3.7). Cada fuente posee una potencia que es repartida en sus fotones (cada
uno transporta una cantidad fija de energía).
23
Figura. 3.7: Emisión de fotones desde distintas fuentes de luz.
En una fuente de luz puntual, los fotones son emitidos uniformemente en todas las
direcciones. La emisión se puede realizar de dos maneras: por muestreo explícito, que
consiste en mapear variables aleatorias en coordenadas esféricas para cubrir la
superficie de una esfera ó muestreo por rechazo que consiste en generar puntos al azar
en un cubo unitario y descartar aquellos que no estén incluidos en la esfera de diámetro
unitario.
En una fuente esférica, primero se escoge un punto de origen en la superficie de la
esfera y luego se escoge una dirección al azar (siempre que apunte hacia el exterior de la
misma). En el caso de una fuente cuadrada, el proceso es similar al de una fuente
esférica.
En una fuente direccional, que se caracteriza por emitir rayos de luz paralelos, se
debe generar primero un volumen que envuelva a todo el modelo, luego proyectarlo en
la dirección de la fuente de luz y escoger un punto sobre esa superficie para determinar
el origen de cada rayo (ver figura 3.8).
Para las fuentes de luz complejas con formas arbitrarias, se deben generar los
fotones con una densidad acorde al perfil de la fuente.
Cuando hay más de una fuente en una escena, la cantidad de fotones emitidos por
cada fuente debe ser proporcional a la potencia de la misma (todos los fotones
transportan la misma cantidad de energía lumínica).
Fig. 3.8. Fuentes de luz direccionales.
24
Es importante diferenciar un aspecto del proceso de trazar fotones del de trazar
rayos. Mientras que los rayos pretenden capturar radiancia, los fotones propagan flujo
lumínico, es por ello que la interacción de los rayos con las superficies es diferente a la
de los fotones.
Cuando un fotón impacta sobre una superficie, puede ser absorbido, reflejado o
refractado. En el caso de la reflexión, si el fotón impactó una superficie especular, se
reflejará en la dirección de reflexión del espejo. En cambio si impactó una superficie
difusa, se reflejara en una dirección aleatoria escogida de un hemisferio ubicado por
encima el punto de impacto con una probabilidad proporcional a la BRDF. Cabe aclarar
que usualmente los materiales de las superficies se modelan como la suma de una
componentes: difusa o especular y ante el impacto de un fotón, se tiene en cuenta uno
de los dos comportamientos de acuerdo a su respectiva probabilidad
En el caso de la refracción, la nueva dirección será determinada por las leyes de
refracción. Finalmente en el caso de la absorción, si la superficie es no-especular, la
información del fotón es almacenada en el Photon Map (posición, dirección, etc.). Dado
que los fotones almacenados en superficies especulares no aportan información útil
porque la probabilidad de encontrar uno que coincida en forma exacta con cierta
dirección de reflexión especular es muy pequeña.
La decisión sobre cual de los eventos debe suceder, es determinado mediante la
técnica de Ruleta Rusa y el material de la superficie. Utilizando una variable aleatoria
uniforme ξ ∈ [0..1] y comparando su valor contra las probabilidades de que suceda cada
evento:
ξ ∈ [0..ρd ] → Reflexión especular
ξ ∈ [ ρd ..ρd + ρs ] → Reflexión difusa
ξ ∈ [ ρd + ρs..ρd + ρs + ρr ] → Refracción
ξ ∈ [ ρd + ρs + ρr ..1] → Absorción
(3.7)
Donde ρd , ρs , ρr representan la probabilidad de reflexión difusa, especular y de
refracción respectivamente.
El Photon Map es una estructura de datos estática, utilizada para computar
densidades de fotones en puntos del modelo 3D. Concretamente es una estructura de
árbol KD diseñada con el fin de resolver búsquedas de fotones cercanos a determinado
punto del espacio 3D de una manera rápida y a la vez eficiente. Debe ser capaz de
almacenar millones de fotones que se distribuyen en forma no uniforme por la escena.
Cada nodo del árbol particiona una de las dimensiones (X, Y o Z) en dos sub-árboles
que contienen los fotones a un lado y otro del plano divisorio.
La estructura permite encontrar los k fotones mas cercanos de un conjunto de N para
una posición dada del espacio, en un tiempo promedio del orden de O(k + log N).
Del proceso de construcción puede resultar un árbol de fotones desbalanceado. Por
eso usualmente se agrega un paso adicional de balanceo que permite luego representar
el árbol por un arreglo plano evitando así el almacenamiento de dos punteros a los
nodos hijos. El paso de balanceo toma un tiempo adicional del orden de O(N log N)
donde N es el número de fotones del árbol.
Algunos trabajos recientes como el de Wald et al. [16] muestran que un árbol
cuidadosamente desbalanceado puede superar en desempeño a uno balanceado, con un
costo en tiempo de precómputo mayor y con requerimientos de almacenamiento
adicionales.
25
El algoritmo de búsqueda es una directa extensión del de búsqueda en un árbol
binario estándar. Se establece un rango máximo de búsqueda (un radio alrededor de un
punto), de lo contrario el método se haría muy lento obligando a recorrer zonas con
escasos fotones. Se mantiene una lista ordenada de los fotones que van siendo hallados
según su cercanía al centro del volumen de búsqueda, de modo que si hay n fotones
encontrados y se encuentra uno nuevo, se puede eliminar el último de la lista que es el
que estaba más lejos.
3.6.4 Fase 2, rendering
Durante esta fase, se pretende aproximar la ecuación del rendering para computar la
radiancia reflejada en posiciones de las superficies. La imagen final se obtiene
promediando un número de estimaciones por cada píxel, trazando rayos desde el ojo del
observador hacia la escena.
Si consideramos que la BRDF fr es usualmente la combinación de dos componentes:
especular fr,S y difusa fr,D y que la radiancia incidente es en realidad la suma de tres
componentes: iluminación directa Li,l, iluminación indirecta por reflexión especular o
refracción Li,c, e iluminación indirecta por reflexión difusa (al menos una vez) Li,d,
podemos descomponer la radiancia reflejada de la siguiente manera:
r
r r
r r r r
Lr ( x, w) = ∫ fr ( x, w' , w) Li ( x, w' )( w'⋅n )dw'
Ω
r
Lr ( x, w) = D + E + C + I
r r
r r r r
D = ∫ fr ( x, w' , w) Li , l ( x, w' )( w'⋅n )dw'
Ω
r r
r
r
r r r
E = ∫ fr , S ( x, w' , w)( Li , c ( x, w' ) + Li , d ( x, w' ))( w'⋅n )dw'
(3.8)
Ω
r r
r r r r
C = ∫ fr , D ( x, w' , w) Li , c ( x, w' )( w'⋅n )dw'
Ω
r r
r r r r
I = ∫ fr , D( x, w' , w) Li , d ( x, w' )( w'⋅n )dw'
Ω
El término de la iluminación directa D, corresponde al cálculo de la radiancia
reflejada debido a la luz que proviene directamente de la fuente y se calcula de manera
precisa del mismo modo que en el Ray Tracing tradicional, ya que es uno de los
términos más importantes porque genera por ejemplo el efecto de sombra. En el caso de
fuentes de luz no puntuales, se utilizan varios rayos de sombra para estimar
apropiadamente las zonas de penumbra.
Para el término de reflexiones especulares E, no se utiliza el Photon Map, en
cambio, se utiliza la técnica de Monte Carlo Ray Tracing.
El término C se puede calcular de manera aproximada evaluando el estimador de
radiancia a partir del Photon Map, o de manera mas precisa usando uno especial
denominado Caustics Photon Map, que almacena solo los fotones que generan el efecto
de caustics (luces enfocadas por reflexión o refracción). Este último se construye
trazando fotones solo hacia objetos del tipo especular.
Por último el término que representa la luz proveniente de reflexiones sobre
superficies difusas I, se calcula también evaluando el estimador de radiancia pero a
partir del Photon Map principal.
26
El cómputo del denominado estimador de radiancia, se basa en que la radiancia
r
reflejada Lr, de la ecuación del rendering en el punto x y la dirección w , puede ser
estimada con los k fotones más cercanos del Photon Map:
r
1
Lr ( x, w) ≈ 2
πr
k
r r
r
p , w) ∆Φp ( x, wp )
∑ f ( x, w
r
(3.9)
p =1
r
Donde wp es la dirección incidente, y ∆Φp es la potencia del fotón p, r es la
distancia máxima hasta cada fotón y π ⋅ r 2 es el área del círculo conteniendo los fotones.
Dado que la búsqueda de los fotones se realiza en una esfera, es probable que se
introduzcan errores en la estimación, al incluir fotones que llegan a la superficie por
detrás, en una esquina o atravesando un objeto delgado y que no deberían contribuir a la
iluminación del punto x. Estos efectos pueden reducirse, comparando la dirección
incidente y la normal de la superficie y descartando mediante un producto escalar a
aquellos que llegan por detrás.
Aunque los fotones están contenidos en una esfera, la densidad se mide sobre el área
de la esfera proyectada sobre la superficie (ver figura 3.9) la cual que tiene forma de
disco. El uso de una distancia máxima r es para evitar que la búsqueda de fotones
abarque áreas excesivamente grandes.
Fig. 3.9: estimador de radiancia
El ruido en la imagen final se reduce a medida que se aumenta la cantidad de fotones
usados en el estimador de radiancia. Está demostrado que se puede obtener un estimador
arbitrariamente bueno incluyendo una cantidad suficiente de fotones.
3.6.5 Desempeño y mejoras
El algoritmo genera soluciones de iluminación global completas, de mayor calidad y
en menor tiempo que otros métodos como Path-Tracing.
Dada la popularidad de esta técnica, su robustez y su sencilla implementación, se
presentaron muchas mejoras y extensiones. Entre ellas, se presentó una extensión para
computar efectos de dispersión dentro de superficies traslucidas (sub-surface scattering)
que simulan materiales como la piel o el mármol. También se desarrollaron extensiones
para computar efectos atmosféricos tales como niebla o humos. Entre las mejoras
propuestas se encuentra el algoritmo propuesto por Ward [17] denominado Irradiance
Caching, que permite reducir el número de búsquedas de fotones cercanos para
27
computar los estimadores de radiancia interpolando los valores de otros estimadores
calculados para puntos cercanos del espacio.
28
Capítulo 4 - Plataformas de Rendering de alto
desempeño
Estas plataformas comprenden aquellas comúnmente utilizadas para implementar
sistemas de visualización que requieren el procesamiento de grandes volúmenes de
datos en tiempos relativamente breves con el objetivo de sintetizar imágenes a partir de
un modelo o escena. Tal es el caso de sistemas que requieren generar secuencias de
imágenes para componer una animación, en donde es deseable una tasa de generación
de cuadros cercana a los 15 cuadros por segundo para lograr una sensación de fluidez de
movimiento propia de las secuencias cinematográficas.
También es el caso de los sistemas de visualización interactivos (por ejemplo
sistemas de realidad virtual), en donde la intervención del usuario mediante algún
dispositivo de entrada modifica las variables del modelo (por ejemplo el cambio de
posición o dirección observación de la escena), a partir de las cuales el sistema debe
generar una nueva respuesta en forma de imagen.
Las estrategias generalmente aplicadas en estos sistemas, apuntan a subdividir las
tareas de cómputo en bloques independientes que puedan ser resueltos en forma
simultánea o paralela por varias unidades de procesamiento. Los algoritmos de síntesis
de imágenes suelen ser altamente paralelizables, lo cual significa que existe un alto de
grado de independencia entre los datos a procesar y los resultados que estos generan,
permitiendo su cómputo simultáneo en unidades separadas. Esta es una característica
deseable ya que reduce la necesidad de comunicación o sincronización entre nodos y
logra un máximo aprovechamiento del tiempo de procesamiento de cada unidad.
En el caso de los algoritmos de iluminación global mencionados en el capítulo 3, la
gran cantidad de cálculos y la complejidad de la escena a sintetizar, hacen que una CPU
estándar (Intel Pentium IV 3.0 GHz) demore varios minutos y hasta incluso horas en
generar una imagen.
Dos caminos para encarar el problema planteado son:
•
•
Implementación sobre clusters de PCs.
Implementación sobre hardware gráfico programable (GPUs).
A continuación se analizarán más en detalles ambas alternativas.
4.1 Clusters de PCs
Son sistemas conformados por un conjunto de hosts o PCs interconectados mediante
una red local (ver figura 4.1) de alta velocidad (Fast o Giga Ethernet) entre los cuales se
distribuyen las tareas de cómputo para ser procesadas en forma simultanea por todos los
nodos.
Respecto al modo de subdividir las tareas de cómputo, algunos sistemas definen
como unidad básica de trabajo un cuadro o imagen completa. Otros en cambio, explotan
al paralelismo inter-cuadro, subdividiendo la ventana de visualización en porciones o
fragmentos independientes. Estos son alimentados a cada nodo para luego ser
integrados nuevamente en una sola imagen o ser provistos directamente a dispositivos
de visualización conformados por múltiples pantallas en configuración de matriz.
29
Cada nodo de la red posee una memoria propia, debiendo intercambiar información
o sincronizar su estado con el resto de la red mediante el pasaje de mensajes.
Estos sistemas explotan el paralelismo de “grano grueso”, lo cual significa que las
tareas a realizar por cada procesador, deben insumir un tiempo relativamente largo en
comparación a los tiempos de comunicación entre nodos (latencia de la red y overhead
de los mensajes). De otro modo, la eficiencia del sistema se vería reducida, ya que se
desperdiciaría un alto porcentaje de tiempo en la comunicación.
Figura 4.1. Cluster de PCs.
Existen ejemplos como el caso del sistema Chromium [23], que provee un
mecanismo genérico para manipular y procesar secuencias de comandos de una API
grafica estándar. El sistema virtualiza los recursos disponibles en la red, permitiendo
ejecutar por ejemplo una aplicación OpenGL (Open Graphics Library) estándar incluso
sin necesidad de recompilarla.
Otro caso es el de WireGL [24], un sistema de rendering distribuido, escalable, que
permite a una aplicación realizar el proceso de rendering sobre una matriz de múltiples
pantallas. No requiere el uso de una API específica y puede funcionar sobre hardware
estándar de bajo costo.
4.2 Hardware gráfico programable
Esté tipo de hardware también conocido como GPU (Graphics Processing Unit),
comprende unidades dedicadas exclusivamente al procesamiento de gráficos 3D. A
diferencia de las CPU, su diseño especializado permite procesar tareas gráficas en
tiempos más breves.
Aunque, las GPUs más recientes son capaces de procesar cientos de millones de
vértices y miles de millones de fragmentos (estructura de datos que actualiza el color de
cada píxel) por segundo, no están preparadas para computar algoritmos de propósito
general,
Es decir no es sencillo portar código de una aplicación escrito originalmente para
una CPU. A pesar de ello, hoy en día, representan la mejor relación entre poder
computacional por dólar. Además su desempeño crece a ritmos mayores que las CPU,
dado que se duplica aproximadamente cada 12 meses. El crecimiento e innovación en
este campo es impulsado principalmente por la industria de los video-juegos.
A continuación se comparan las características de ambas unidades de
procesamiento:
30
CPU
GPU
•
Utiliza la memoria principal
(RAM) del sistema.
•
Utiliza una memoria dedicada
contenida en la placa gráfica de la
PC (memoria de texturas).
•
Arquitectura SISD (single
instruction single data).
•
Arquitectura SIMD (single
instruction multiple data).
•
Optimizada para alto desempeño
de código genérico secuencial
(caches y predicción de
bifurcación).
•
Optimizada para procesamiento
paralelo de código con alta
proporción de operaciones
aritméticas.
•
Su desempeño se duplicada cada
18 meses.
•
Su desempeño se duplica cada 12
meses
•
Intel Pentium 4 3.0 GHz
Desempeño: 6 Gigaflops (teóricos)
Memoria: 6 GB/seg (pico).
•
Nvidia GeforceFX 5900
Desempeño: 20 Gigaflops
Memoria: 20 GB/seg (pico).
•
Aptos para algoritmos de
propósito general.
•
Diseñadas para implementar
algoritmos gráficos específicos
•
Accesible en forma directa.
•
Accesible mediante una API
(Aplication Programming
Interfase).
•
Paradigmas de programación
convencionales como:
Programación Orientada a Objetos,
Procedural, etc.
•
Requiere el uso de nuevos
paradigmas no convencionales,
como Stream Programming.
31
Capítulo 5 - GPU (Graphics Processing Unit)
El término fue acuñado por NVIDIA a finales de la década de los 90, cuando el
término Video Graphics Array (VGA) Controller ya no definía de forma precisa las
funciones del hardware gráfico en una PC. Se trata de un dispositivo capaz de
manipular, procesar y mostrar gráficos de manera eficiente.
Su arquitectura altamente paralela, conformada por múltiples unidades de ejecución,
es capaz de realizar operaciones de punto flotante con 32 bits de precisión y trabajar
simultáneamente sobre múltiples conjuntos de datos. Hoy en día, es un componente
estándar del subsistema de video en la mayoría de las PC.
En el siguiente cuadro se detalla su evolución:
1998 3DFX Voodoo, NVIDIA TNT y ATI Rage.
Realizan proceso de rasterización (conversión de forma vectorial a
píxeles) de triángulos y mapeo de hasta 2 texturas.
Menos de 10 millones de transistores por GPU.
No son capaces de aplicar transformaciones a los vértices.
Operaciones aritméticas muy limitadas.
Implementan funciones de DirectX 6.0.
2000 NVIDIA GeForce serie 2 y ATI Radeon.
25 millones de transistores por GPU.
Incorporan capacidad de transformar vértices.
Son configurables, pero aún no programables.
Implementan funciones de DirectX 7.0.
2001 NVIDIA GeForce series 3 / 4 y ATI Radeon serie R200.
Incorporan programabilidad en el procesador de vértices.
60 millones de transistores por GPU.
Poseen más opciones de configuración que las series anteriores, para
procesar vértices, pero aún no son programables.
Implementan funciones de DirectX 8.0.
2003 NVIDIA GeForce serie FX y ATI Radeon serie R300.
120 millones de transistores por GPU.
Programabilidad a nivel vértices y fragmentos.
Soporte completo de DirectX 9.0.
2004 NVIDIA GeForce serie 6 y ATI Radeon serie R420.
220 millones de transistores por GPU.
Introducción de sistema SLI (Scalable Link Interface).
Introducción de modo MRT (Múltiple Render Targets).
2005 NVIDIA GeForce serie 7 y ATI Radeon serie R520.
300 millones de transistores por GPU.
Mayor velocidad de reloj y ensanchamiento del pipeline (más unidades
de procesamiento en paralelo).
32
2006 NVIDIA GeForce serie 8 y ATI Radeon serie R600.
Arquitectura de unidades de procesamiento unificada (procesan vértices
y fragmentos).
700 millones de transistores por GPU.
Soporte completo para DirectX 10.0.
En futuras generaciones se espera que los recursos de la GPU se generalicen cada
vez más permitiendo aún mayor grado de programabilidad.
5.1
El pipeline gráfico
Un pipeline consta de una secuencia de etapas que operan en un orden determinado
(similar a una línea de montaje industrial). Cada etapa recibe en su entrada el resultado
del proceso de la etapa anterior y todas las etapas en conjunto pueden operar
simultáneamente sobre distintos elementos.
El pipeline gráfico puede ser pensado como una caja negra por donde fluyen
conjuntos de datos que sufren transformaciones para generar como resultado final una
imagen 2D o un mapa de bits. Aunque en realidad no se trata de una caja negra ya que
uno puede controlar la forma en que los datos son transformados.
Este pipeline recibe en su entrada los vértices correspondientes a la geometría de la
escena 3D. Cada vértice es una estructura de datos que contiene las coordenadas
homogéneas de un punto en el espacio 3D y otros atributos opcionales que pueden ser
escalares o vectoriales (de 2, 3 o 4 componentes) que se definen en función de la
aplicación que se quiera implementar. Los principales son:
•
•
•
•
Posición: coordenadas homogéneas del vértice.
Normal: vector que indica la dirección en que apunta la superficie, en el punto
definido por la posición. Este dato es útil para cálculos de iluminación.
Color: este dato luego será utilizado para sombrear el área de pantalla que cubra
la primitiva geométrica según el tipo de algoritmo de sombreado que se use.
Puede haber más de un color definido por cada vértice.
Coordenadas de textura: un par de coordenadas reales mapean un punto sobre
una textura 2D, que se utilizará luego para el sombreado de la primitiva
geométrica. Pueden utilizarse varios pares de coordenadas de vértice para
indexar diferentes texturas.
Además, el usuario puede definir otros atributos de utilidad para modelar
características de los materiales de las superficies representadas.
Sobre los vértices se aplican una serie de operaciones matemáticas denominadas
transformaciones de vértices que incluyen traslaciones, rotaciones, cambios de escala y
proyecciones. Al utilizar un sistema de coordenadas homogéneas, todas las
transformaciones pueden representarse por matrices (de 4 x 4) que al multiplicarlas por
los vectores de coordenadas de los vértices dan como resultado el vector transformado.
Otra ventaja de las coordenadas homogéneas, es la posibilidad de encadenar varias
transformaciones multiplicando sus matrices asociadas.
El objetivo de las transformaciones es traducir las coordenadas de la escena 3D del
espacio de coordenadas abstracto de cada objeto (modelo o patrón de un objeto 3D) a
las coordenadas 2D en el denominado plano de vista o plano de visualización.
33
Esta conversión de coordenadas se subdivide en varias fases:
•
•
•
Transformación de modelado: implica posicionar, rotar y escalar el objeto
respecto del resto de la escena (traducir coordenadas del modelo a coordenadas
del mundo).
Transformación de vista: implica convertir las coordenadas de toda la escena a
un sistema con origen en el punto de vista y la dirección de la “cámara” o el
“ojo” que observa la escena.
Transformación de proyección: implica convertir coordenadas del espacio
tridimensional de las coordenadas de vista a coordenadas en un plano de
proyección bidimensional. Existen varios tipos de proyección como ser la
perspectiva o la ortográfica. La matriz de proyección se define a partir de un
denominado volumen de vista (view frustum) que establece como mapear una
porción de espacio sobre el plano de proyección.
Las 3 transformaciones se aplican en orden a cada vértice de la escena.
Luego, los vértices transformados son combinados en la etapa de ensamblado, para
formar las que se conocen como primitivas geométricas (ver figura 5.1) que
comprenden: puntos, líneas, triángulos, etc.
Figura 5.1: Primitivas geométricas.
Para continuar, el proceso de rasterización descompone las primitivas en elementos del
tamaño de píxeles, llamados fragmentos que intervienen en la definición del color de los
píxeles de la imagen 2D generada a la salida del pipeline.
Figura 5.2: Representación del pipeline gráfico.
34
Un fragmento es una estructura de datos asociada a la ubicación de un píxel de la
imagen de salida. También se puede pensar al fragmento como un “píxel potencial” ya
que algunos son descartados.
En la figura 5.2, se detalla la evolución de los datos a lo largo de las etapas. En la
industria, OpenGL y Direct3D son los dos modelos de pipelines gráficos ampliamente
aceptados como estándares.
5.2
El pipeline gráfico programable
En la actualidad la mayoría de las etapas del pipeline gráfico son implementadas
directamente en el hardware, exhibiendo un alto grado de programabilidad. Esto
significa que los algoritmos que implantan son modificables y son suministrados por la
aplicación de alto nivel como parte del flujo de datos de entrada del pipeline. Incluso es
posible ingresar secuencias que intercalan datos geométricos y los programas que deban
procesarlos, permitiendo el uso de diferentes algoritmos para distintos objetos de la
escena.
En las primeras generaciones de GPUs dichos programas estaban grabados en el
hardware (no eran modificables) y eran solo configurables en forma limitada.
Los programas suministrados al pipeline programable son de dos tipos: los
programas de vértices (aplican las transformaciones a los vértices) y los programas de
fragmentos (computan los datos que definirán los colores de los píxeles).
Ambos tipos de programas pueden ser escritos en diversos lenguajes como:
•
•
•
Cg (C for Graphics): es un lenguaje [29] de sombreado (shading language) de
alto nivel desarrollado por NVIDIA y Microsoft. Está basado en el lenguaje C y
tiene algunos tipos de datos agregados, adecuados para programar en la GPU. Es
un lenguaje especializado. Un compilador lo traduce al assembler o código de
máquina.
HLSL (High Level Shading Language): desarrollado por Microsoft para
utilizarse con Direct3D, es muy similar a Cg.
GLSL (OpenGL Shading Language): fue creado por OpenGL Architecture
Review Board y tiene características muy similares a los dos anteriores.
La figura 5.3 ilustra la estructura del pipeline gráfico programable. El proceso
comienza cuando la aplicación de alto nivel envía a la GPU un conjunto de vértices.
En la primera etapa el procesador de vértices los trasforma de acuerdo a un
programa de vértices y define el valor del resto de sus atributos.
Las transformaciones tienen como objetivo traducir la posición de cada vértice, del
sistema de coordenadas del objeto modelado (tridimensional) al sistema de coordenadas
de la pantalla (bidimensional) aplicando un conjunto de operaciones aritméticas
representadas por productos de matrices (de traslación, escalado, rotación y
proyección).
35
Figura 5.3: El pipeline gráfico programable.
En la segunda etapa (ensamblado y rasterización) se ensamblan los vértices
transformados (vértices con coordenadas en el espacio de pantalla) para formar la
primitiva geométrica. A la primitiva ensamblada se le aplican los procesos de Clipping
y Culling.
El proceso de Clipping, se encarga de recortar la primitiva geométrica, para remover
las porciones que quedan fuera del área que se pretende visualizar (denominada ventana
del mundo). El proceso de Culling se encarga de descartar aquellas primitivas que no
deben dibujarse ya que se encuentran de espaldas al observador de la escena.
Las primitivas geométricas que no son descartadas, pasan al proceso de
rasterización. Este consiste en determinar qué píxeles del dispositivo de salida son
cubiertos por la primitiva geométrica y generar los correspondientes fragmentos.
Los fragmentos son estructuras de datos transitorias que almacenan atributos como:
•
•
•
•
Posición: ubicación del píxel al que está asociado. Son un par de coordenadas
enteras.
Profundidad: es la distancia en el eje Z (eje perpendicular al plano de vista) al
origen del punto de observación.
Color: surge de la interpolación lineal de los atributos de color de los vértices
que dieron origen al fragmento.
Coordenadas de texturas: del mismo modo que el color, su valor es
interpolado linealmente.
Además un fragmento puede poseer otros atributos especialmente definidos por la
aplicación como por ejemplo varios colores y pares de coordenadas de texturas
adicionales.
La tercera etapa, habitualmente llamada de sombreado (porque tradicionalmente
define el color de los píxeles según algún modelo de iluminación) es implementada por
el procesador de fragmentos según un programa, que da como resultado el color del
píxel asociado. El programa resuelve el color a partir de los atributos del fragmento y
otros datos disponibles en la memoria de la GPU, como texturas y parámetros globales
definidos por la aplicación. La información almacenada en texturas se accede mediante
36
índices computados por el programa a partir, por ejemplo, de las coordenadas de
texturas.
Para finalizar, los fragmentos pasan por una serie de pruebas (Scissor Test, Alpha
Test, Stencil Test y Depth Test) y procesos (Blending y Dithering) que son parte
estándar de OpenGL y Direct3D para evaluar cuales producirán una actualización del
píxel correspondiente en el buffer de salida y cuales deben ser descartados.
5.3 Modelo de memoria de la GPU
Existen tres áreas de almacenamiento de datos en una GPU:
•
•
•
Memoria para datos de vértices: este espacio es esencialmente utilizado para
leer los atributos de los vértices desde el procesador de vértices. Estos son
cargados desde la aplicación ejecutada en la CPU. En las GPUs más recientes es
posible escribir o copiar datos en esta memoria, a partir de un programa de
fragmentos (modo copy-to-vertex-array).
Framebuffers: se utilizan principalmente para escribir los resultados generados
por el procesador de fragmentos (hasta 16 valores de punto flotante de 32 bits,
utilizando el modo MRT) y luego ser visualizados en el dispositivo de salida.
Memoria de texturas: su nombre se debe a que surgió como un espacio para
almacenar información de imágenes 2D que representan muestras de colores y
atributos de superficies. Está optimizada para acceso mediante coordenadas 2D.
Es la que se utiliza como memoria de trabajo en programas de fragmentos que
implican múltiples pasadas permitiendo hacer ciclos de lectura y escritura de
datos. Hoy en día es posible utilizarla para otros datos, que no necesariamente
están relacionados con imágenes o colores. Por ejemplo, es posible almacenar
estructuras de datos más complejas, como listas, arreglos 1D y 3D, etc. Estas
estructuras requieren implementar mecanismos de traducción de direcciones y
conversión de tipos de datos, ya que la forma primitiva de organizar los datos es
en arreglos bidimensionales de valores de punto flotante con tamaño fijo.
La siguiente figura 5.4 ilustra la relación entre las 3 áreas de memoria, los
procesadores de vértices, fragmentos y la CPU.
Figura 5.4: Áreas de memoria en la GPU.
37
Se espera que este modelo de memoria siga evolucionando hacia un modelo más
general que permita una mayor flexibilidad en las operaciones de lectura y escritura.
5.4 El procesador de vértices
Es el encargado de ejecutar los programas de vértices (vertex programs). El proceso
comienza con la carga de los atributos de los vértices en registros de entrada.
A medida que se ejecutan las instrucciones del programa de vértices, los resultados
intermedios son almacenados en registros temporales. Además el programa tiene acceso
a registros de solo lectura en donde se almacenan constantes globales de la aplicación
(existe una sola instancia para todos los programas de vértices). Las instrucciones
disponibles incluyen operaciones matemáticas sobre vectores de punto flotante de hasta
4 componentes (X, Y, Z, W), instrucciones de control de flujo, de salto y lazos.
Las últimas generaciones de GPUs permiten el acceso aleatorio a memoria de
texturas desde el programa de vértices. La salida del programa son las coordenadas
transformadas del vértice y el resto de sus atributos que son almacenados en registros de
sólo-escritura para luego ser provistos al rasterizador.
Existe un límite en la cantidad total de instrucciones, por ejemplo, para las GPUs
NVIDIA de la serie GeForce 6 es de 512 instrucciones estáticas (las del programa
compilado) y 65535 dinámicas (son las que realmente se ejecutan al multiplicar los
bloques que incluyen ciclos por la cantidad total de ciclos especificados).
Es importante destacar que por cada vértice de la escena se ejecuta una instancia
independiente del programa de vértices, pudiéndose así procesar varios vértices en
paralelo dependiendo de la cantidad de unidades de procesamiento de vértices que posea
la GPU.
5.5 El rasterizador
Primero el rasterizador ensambla los vértices transformados por la etapa anterior
para conformar la primitiva geométrica de acuerdo a un comando suministrado por la
aplicación que indica como deben vincularse los vértices, para formar un polígono
determinado.
Figura 5.5: Proceso Clipping, Culling y Rasterización.
Sobre el plano de vista, se define un área rectangular denominada ventana del
mundo, que representa la zona de interés que se pretende visualizar.
38
Luego el proceso de Clipping se encarga de determinar que partes de la primitiva
están dentro de la ventana de visualización o ventana del mundo y generar, de ser
necesario, una versión recortada de las mismas. En la figura 5.5, el triángulo nro. 1 es
recortado para remover las porciones que están fuera del área de interés.
El proceso de Culling determina si las primitivas están de frente o de espaldas al
observador de la escena, evitando así dibujar elementos no visibles (por ejemplo en la
figura 5.5, el triángulo nro. 4 es descartado por este proceso).
Por último, el proceso de rasterización proyecta la primitiva recortada sobre el plano
de visualización, convirtiendo el elemento geométrico vectorial 2D en su representación
en forma de fragmentos (ver figura 5.5). Los fragmentos generados son suministrados a
la etapa siguiente junto con las coordenadas del píxel al que están asociados.
Es importante señalar que la cantidad de fragmentos no tiene relación con la
cantidad de vértices de una primitiva (un polígono de 3 vértices puede generar millones
de fragmentos, si la primitiva cubre toda ventana del mundo).
5.6 El procesador de fragmentos programable
Este procesador ejecuta programas de fragmentos. En una GPU existen más de una
unidad de procesamiento de fragmentos, cada una capaz de procesar un fragmento a la
vez.
Las entradas del programa de fragmentos son los atributos que surgen de hacer una
interpolación lineal de los atributos de los vértices que le dieron origen al fragmento.
Además de las instrucciones típicas disponibles en los programas de vértices, los de
fragmentos agregan instrucciones para acceder a una textura mediante un par de
coordenadas reales. Este mecanismo utiliza lo que se denominan “sampler objects”, que
toman una muestra de la textura en base a la interpolación de los valores de los píxeles
cercanos.
El procesador de fragmentos cuenta también con un conjunto de registros de
lectura/escritura para almacenar valores intermedios.
Como salida, el procesador de fragmentos genera una serie de valores que
representan atributos de un píxel de la imagen final como ser color, transparencia, etc.
En el caso de los programas de fragmentos el límite en la cantidad de instrucciones
es de 65535, tanto dinámicas como estáticas.
Aunque los programas de fragmentos y vértices permiten el uso de instrucciones de
bifurcación (If / Else), es importante saber que a diferencia de un programa ejecutado en
una CPU, ambas ramas de la condición son evaluadas. Y el costo en tiempo
computacional es el de la suma de ambas.
5.7 Stream Programming Model
Este paradigma o modelo de programación nació con el surgimiento de los
procesadores de streams y un cambio de tendencia en el desarrollo de la tecnología de
semiconductores, en donde el costo de cómputo es proporcionalmente cada vez más
barato que el costo del acceso a la memoria. En este tipo de procesadores se trata de
reutilizar al máximo los registros internos del chip para entrada/salida de datos y así
minimizar el acceso al sistema de memoria externa para poder mejorar el desempeño de
los programas.
Se pretende explotar la localidad y el paralelismo existente en los programas
empleando centenares de unidades trabajando simultáneamente sobre distintas partes o
tareas independientes que componen un mismo programa.
39
El modelo impone restricciones sobre la forma en que deben ser escritos los
programas a fin de que el compilador logre optimizar el código y mapearlo de modo
directo al hardware. El paralelismo y la localidad del código están explícitamente
definidos en el modelo.
Uno de los componentes de este modelo son los kernels, que básicamente son
llamadas a funciones que realizan un número considerable de cómputos sobre un
conjunto de datos de entrada y escribe los resultados sobre otro conjunto de datos de
salida. Estos conjuntos de datos sobre los que opera se denominan streams.
Los kernels no pueden poseer un estado, esto significa que sus resultados no deben
depender de los resultados de una invocación anterior.
Figura 5.6: Stream Programming Model
En resumen, la figura 5.6 ilustra los conceptos esenciales del modelo, donde un
kernel (programa) procesa streams (datos de entrada) y genera streams (datos de salida).
Además un kernel puede tener acceso a parámetros globales almacenados en un área de
memoria de sólo-lectura. Los streams son los elementos que conectan los kernels entre
sí.
Relacionando los conceptos del modelo con el modelo de programación en una
GPU, se pueden observar analogías en la forma en que los kernels operan sobre los
streams y el modo en el que los programas de fragmentos operan sobre los datos
almacenados en la memoria de texturas. Por lo tanto, el procesador de fragmentos puede
ser interpretado como un procesador de streams que implementa un subconjunto de
funciones de las que tendría un procesador de streams ideal. Esto se debe a ciertas
limitaciones de las GPUs actuales, entre las que se encuentran la imposibilidad de
escribir un dato en una dirección computada dinámicamente desde un programa de
fragmentos, o el límite en la cantidad de streams de salida por programa (4 streams).
Existen varios trabajos recientes, orientados en este sentido, como los Purcell [6,8]
en donde propone la implementación de un motor de Ray Tracing o un motor de Photon
Mapping sobre hardware gráfico programable, utilizando el paradigma de Stream
Programming.
5.8 GPGPU
La posibilidad de utilizar la GPU como un procesador de streams ha generado todo
una tendencia que se conoce como GPGPU (General Purpose Computation on a GPU)
que implica la utilización de la unidad de procesamiento gráfico para computar
40
algoritmos de propósito general que incluso no relacionados a la computación gráfica
como por ejemplo:
•
•
•
•
Procesamiento de señales de audio y video.
Simulaciones físicas.
Procesamiento de bases de datos.
Simulaciones científicas.
En la práctica sólo se hace uso del procesador de fragmentos, ya que las GPUs
actuales poseen mayor cantidad de unidades de procesamiento de fragmentos que de
vértices.
Entre las dificultades se encuentran las limitaciones inherentes al hardware gráfico
sobre el que se trabaja, la casi nula existencia de herramientas de depuración.
5.9 Computando con un programa de fragmentos
Bajo el esquema GPGPU, los programas de fragmentos escriben sus datos de salida
en la memoria de texturas, en lugar del framebuffer (buffer utilizado para mostrar la
salida por el dispositivo de pantalla), para que luego puedan ser utilizados como entrada
en fases de cómputo subsiguientes. Este modo de trabajo se conoce como rendering a
texturas (Render-to-Texture).
La extensión de OpenGL llamada Framebuffer Object [19] permite gestionar y
vincular las texturas OpenGL con buffers lógicos especiales asociados a las salidas del
procesador de fragmentos. Un programa de fragmentos puede escribir hasta en 4
texturas simultáneamente utilizando el mecanismo MRT (Multiple Render Targets).
Respecto de las entradas de un programa de fragmentos, existen 2 tipos:
•
•
Parámetros uniformes: son suministrados directamente por la aplicación
ejecutada en la CPU. Pueden ser valores escalares, vectores o manejadores de
texturas (un puntero a un arreglo de valores de punto flotante). Su valor es
constante e idéntico para todos los fragmentos asociados al programa.
Parámetros variables: llegan al programa de fragmentos luego de la etapa de
rasterización (color, coordenadas de texturas, normales, etc.) y cada fragmento
recibe un valor diferente que surge de la interpolación lineal de los
correspondientes atributos de los vértices.
Hay casos en que se desea definir valores de entrada específicos para cada
fragmento, que no sean el resultado de la interpolación lineal de atributos de los vértices
de la primitiva geométrica. En estos casos se utiliza un mecanismo de direccionamiento
de memoria en base a coordenadas de textura. Dichas coordenadas se definen para los
vértices de una primitiva geométrica que al ser rasterizada genera los pares de
coordenadas intermedias mediante interpolación lineal.
En el programa de fragmentos, se hace una lectura de la textura 2D que contiene los
valores de entrada, utilizando como índice el par de coordenadas de textura. Este
mecanismo es aplicable a múltiples streams de entrada.
La figura 5.7 ilustra un caso donde el programa de fragmentos utiliza tres
parámetros de entrada in1, in2 e in3. El par de coordenadas coord lo genera el
rasterizador, interpolando el atributo del mismo nombre, definido en los 4 vértices del
rectángulo.
41
Figura. 5.7: Acceso indirecto a la memoria de texturas.
El cómputo de propósito general en la GPU tiene algunas limitaciones que impiden
implementar cierto tipo de algoritmos que requieren operaciones de escritura a memoria
de acceso aleatorio, también conocidas como operaciones de dispersión (scattering
operations). En este esquema, cada programa de fragmentos puede guardar información
persistente solo en el píxel del buffer de salida (utilizando MRT se puede extender esta
cantidad a 4 píxeles, cada uno con 4 componentes de 32 bits, correspondientes al
formato de color RGBA), pero no puede escribir en una dirección de memoria aleatoria.
Por el contrario, las operaciones de reunido (gathering operations) son sencillas de
implementar, como se observó en el ejemplo de la figura 5.7.
Para ejecutar los cómputos especificados en el kernel, bajo la plataforma OpenGL,
se deben efectuar los siguientes pasos (ver figura 5.8):
•
•
•
•
•
•
•
Configurar la matriz de proyección: definir la matriz de proyección
(GL_PROJECTION) para obtener proyección ortográfica de la escena.
Configurar la matriz de transformación: cargar la matriz identidad como
matriz de transformación (GL_MODELVIEW) .
Configurar la ventana en el dispositivo de salida: establecer el área de
pantalla (en píxeles) sobre la cual se computará la salida del pipeline. Está área
debe coincidir con las dimensiones exactas del o los streams de salida (en
definitiva una textura 2D).
Cargar el programa de fragmentos: se carga y compila el programa a partir
del archivo de texto con extensión “.cg”.
Establecer parámetros: Se establecen los valores parámetros uniformes y
variables requeridos por el programa Cg, desde la aplicación de alto nivel.
Configurar de buffers de salida: Se vinculan las texturas de los streams de
salida con los 4 buffers lógicos disponibles mediante la extensión FBO.
Dibujo de la primitiva: se dibuja una primitiva geométrica del tipo rectángulo
(GL_QUADS) que cubra todo el plano de vista y se definen las coordenadas de
textura de sus vértices, de modo que coincidan con las coordenadas de sus
píxeles asociados (ver ejemplo en figura 575).
42
•
Actualizar buffers de salida: mediante el comando glFlush() se inicia el
proceso de rasterización del rectángulo y se inicia el cómputo del programa de
fragmentos.
Figura 5.8: Cómputo de un kernel usando el procesador de
fragmentos en el modo render-to-texture.
Es importante notar que para cada fragmento se ejecuta una instancia independiente
del programa Cg. Estas instancias se ejecutan en paralelo por múltiples unidades (en
GPUs como las NVIDIA GeForce de la serie 6 oscilan entre 16 y 32 unidades). Por lo
tanto no es posible referirse, en una instancia, a los valores de salida generados por otra.
En esta limitación yace la eficiencia del procesamiento paralelo del sistema.
Muchos algoritmos requieren múltiples ejecuciones de un mismo kernel en ciclos
que implican leer y escribir sobre un mismo conjunto de datos (streams de entrada y
salida). En estos casos se maneja la estrategia de “ping-pong”, que consiste en alternar
43
en sucesivas iteraciones el rol de 2 streams, uno de entrada y otro de salida (ver figura
5.9).
Figura 5.9: Estrategia de Ping Pong.
Esta estrategia se utiliza debido a que la especificación de la extensión Framebuffer
Object no garantiza un correcto funcionamiento al leer y escribir una misma textura
dentro de un mismo programa. Sin embargo, en este trabajo, no se observaron
inconvenientes con este modo de utilización, por lo tanto se descartó el uso de pingpong a pesar de la falta de garantías.
Cuando se implementan kernels de múltiples pasadas (múltiples iteraciones de un
mismo kernel sobre un conjunto de streams) es necesario poder detectar cuales
elementos del stream no requieren más procesamiento (porque alcanzaron cierto estado
final definido según el algoritmo implementado). Esto puede ocurrir en diferentes
momentos para diferentes elementos del stream, dependiendo de las condiciones
particulares y los valores de las variables involucradas.
En un programa de fragmentos, es posible indicar esta condición o estado de
finalización mediante una instrucción Cg, denominada “discard”. Esta instrucción evita
que el resultado del programa de fragmentos sea escrito en el buffer de salida, aunque
no ahorra en ciclos de cómputo, activa un bit especial asociado al píxel. La extensión de
OpenGL, OCCLUSION_QUERY, permite saber cuantos fragmentos alcanzaron dicha
condición contabilizando la cantidad de esos bits que están activos. La forma de utilizar
este mecanismo sería la siguiente:
mientras (fragmentos_activos>0){
kernel.ejecutar();
fragmentos_activos=obtenerOcclusionQuery()
}
44
Capítulo 6 - Implementación de un caso de estudio
En este capítulo, se propone el desarrollo de una aplicación que implemente el
algoritmo de Photon Mapping sobre una plataforma basada en hardware gráfico
programable, con el fin de analizar cuales fases de esta técnica son factibles de
computar en la GPU, evaluar el desempeño y las limitaciones existentes.
Se diseño una capa de software de base o un marco de trabajo que provee los
mecanismos básicos del Stream Programming Model, esto es, un conjunto de clases que
encapsulan los conceptos de los kernels y los streams.
Luego a partir de esas clases se construyó un motor de rendering, que combina una
estructura de aceleración de trazado denominada BVH (Bounding Volumes Hierachy) y
la técnica de Photon Splatting para reconstruir la iluminación indirecta. Ambas técnicas
ya fueron implementadas individualmente en una GPU, en el caso de BVH, se
generalizó la implementación original, para trazar tanto rayos como fotones.
A continuación se establecen los parámetros que describirán el observador de la
escena (ubicación, dirección y campo de visión, resolución de imagen de salida, etc.), la
fuente de luz, la geometría 3D y las propiedades de sus materiales en la aplicación.
Además se definen las características del modelo de iluminación implementado.
6.1 Proyección perspectiva de la escena
A continuación se definen los parámetros de la aplicación, que definen las
dimensiones del volumen de visualización (o “view frustum”, ver figura 6.1), que es la
porción de espacio 3D a ser proyectada sobre el plano de proyección para representar la
vista de la escena.
Figura 6.1: Volumen de visualización.
•
•
•
•
•
wsize y hsize: resolución horizontal y vertical (píxeles) de la imagen de salida
(también denominada resolución del viewport).
width y height: ancho y alto del área de proyección de la escena en el plano
cercano.
pos: vector posición de ojo del observador o punto de vista.
normal: vector perpendicular al plano de proyección.
up: vector que señala el lado superior del plano de proyección.
45
•
•
near: distancia al plano de proyección cercano desde el ojo del observador.
far: distancia a plano lejano.
6.2 La fuente de luz
La fuente de luz fue modelada como una fuente tipo spotlight, donde los rayos son
emitidos dentro de la zona delimitada por un cono orientado (ver figura 6.2). Los
parámetros que la definen son los siguientes:
•
•
•
•
•
•
•
pos: vector origen de la fuente lumínica.
normal: vector que indica dirección de emisión.
up: vector que indica lado superior del plano de emisión.
maxAngle: es el ángulo respecto de la normal que indica el punto en el que la
potencia lumínica decae a cero.
minAngleRatio: indica el ángulo mínimo como una fracción del ángulo
máximo, a partir del cual la potencia comienza a decaer linealmente hasta cero.
splatRadius: es el radio del splat, utilizado en la etapa de Photon Splatting.
power: indica la potencia lumínica total de la fuente.
Figura 6.2: Fuente de luz tipo spotlight.
Como se observa en la figura 6.2, la intensidad lumínica de la fuente se mantiene en
su valor máximo entre la dirección normal y el ángulo mínimo. Luego decae
linealmente hasta cero en el ángulo máximo (maxAngle). En la aplicación
implementada sólo está prevista la definición de una única fuente.
6.3 La geometría de la escena
Los modelos geométricos utilizados en este caso de estudio, fueron generados con
3D Studio Max y exportados en el formato 3DS. Luego, en la aplicación, este archivo es
interpretado por una librería que devuelve las coordenadas de los vértices y las normales
de los triángulos junto con el nombre del material y su color.
Utilizando el nombre del material como clave de búsqueda, se accede a una librería
de atributos de materiales definida en el archivo de parámetros general (.INI).
Para cada material se definen:
•
•
color: en formato RGB.
diffuseK: índice de reflexión difusa, es un número real entre 0 y 1.
46
•
•
•
•
specularK: índice de reflexión especular, es un número real entre 0 y 1.
reflectK: índice de reflectividad del material, define la intensidad con que se
refleja el entorno. Es un número real entre 0 y 1. Un valor mayor a 0 implica
trazar un rayo secundario para calcular el color la inter-reflexión especular o
reflexión tipo espejo.
smoothing: indica si las normales deben ser o no interpoladas sobre la
superficie de cada triángulo para lograr un efecto de superficie suave. Admite
dos valores posibles: 0 o 1.
selfIlum: indica si el material emite luz propia. Admite dos valores: 0 ó 1. se
utiliza sólo para destacar claramente un objeto rectangular que representa la
fuente de luz, pero no altera ni genera iluminación sobre su entorno.
Para evaluar el desempeño de la implementación, se generaron variantes del modelo
conocido como Cornell Box (ver figura 6.3), utilizando diferentes tipos de materiales y
objetos ubicados en el interior de la caja.
Figura 6.3: Modelo Cornell Box.
6.4 El modelo de iluminación
Mediante Ray Tracing, luego de trazar los rayos primarios desde el ojo del
observador (ver figura 6.4), se calcula la iluminación en las superficies intersecadas
combinando las siguientes componentes:
•
•
Ambiente: se define como el producto del color del material y el coeficiente de
luz ambiental (ambientK), que es un parámetro global de la aplicación.
Directa: utilizando modelo de Phong [30], se computa en base a los coeficientes
de reflexión difusa y especular asociados al material de la superficie (diffuseK y
specularK respectivamente) y se multiplica por un factor de visibilidad que
puede valer 0 ó 1. Dicho factor, se obtiene trazando un rayo de sombra para
determinar la visibilidad de la fuente de luz. Es la iluminación que proviene
directamente de la fuente.
47
•
•
Indirecta: se computa utilizando la técnica de Photon Splatting (ver punto 6.7
para más detalles). Es la iluminación que llega al punto tras, al menos, una
reflexión difusa.
Inter-reflexión especular perfecta: se obtiene trazando un rayo secundario en
la dirección de reflexión del espejo. Sólo en los casos en que el material de la
superficie en cuestión posea un índice de reflectividad (reflectK) mayor a cero.
Figura 6.4: Componentes de iluminación. A, B y C son tres fotones que aportan
iluminación indirecta al punto P.
6.5 Marco de trabajo basado en Kernels y Streams
En base a los conceptos presentados en el capitulo 5, se construyó una capa de
software de base que provee los elementos básicos del Stream Programming Model
implementado sobre el hardware gráfico, utilizando el mecanismo de rendering a
texturas y el procesador de fragmentos de la GPU. Para ello, se definieron dos clases
C++ denominadas FpKernel y FpStream.
6.5.1 La clase FpKernel
Está asociada a un programa de fragmentos Cg y sirve para implementar kernels de
una o de múltiples pasadas. Utiliza los servicios de clase FBO [22], que presenta una
mínima abstracción de la extensión OpenGL Framebuffer Object, para la gestión y
vinculación de texturas y buffers de salida.
La figura 6.5 muestra mediante un diagrama UML los métodos y atributos
principales de la clase.
El método constructor recibe como parámetros las dimensiones del o los streams de
salida, el nombre archivo Cg y el nombre de la función Cg a invocar dentro de ese
archivo (entryPoint).
El procedimiento de uso de la clase es el siguiente en:
•
•
•
Inicializar la clase: mediante el método init.
Establecer parámetros uniformes: mediante los métodos setParamater.
Vincular streams: mediante el método setStream.
48
•
Ejecutar el kernel: mediante el método run.
Figura 6.5: Clase FpKernel.
El método setParameter1f admite un parámetro real escalar y el método
setParameter3fv admite un vector de 3 componentes reales, además en ambos casos en
necesario suministrar el nombre del parámetro asociado al valor, en el programa Cg.
El método setStream, vincula los streams al kernel, esto es, conecta el identificador
de la textura 2D OpenGL con el parámetro asociado en el programa Cg
Finalmente el método run, inicializa, vincula la instancia del Framebuffer Object y
realiza el cómputo utilizando el modo de render a texturas. El método devuelve la
cantidad de fragmentos activos en la corrida del programa (OCCLUSION_QUERY).
6.5.2 La clase FpStream
Su atributo principal es una textura rectangular OpenGL con un formato de 1,3 o 4
canales float de 32 bits de precisión que representa el stream (de tipo
GL_TEXTURE_RECTANGLE_ARB). Existen tres clases descendientes asociadas a
los modos de uso de los streams (entrada, salida o entrada/salida). A continuación, la
figura 6.6 detalla los métodos y atributos principales de la clase y sus tres clases
descendientes:
49
Figura 6.6: Clase FpStream y sus clases descendientes.
El método constructor de un stream de entrada (FpStreamInputOnly) admite como
parámetro un stream de salida (FpStreamOutputOnly), permitiendo vincular la salida de
un kernel con la entrada de otro. Este vínculo se establece a nivel lógico (ver figura 6.7),
ya que en realidad existe una sola instancia de los datos, representada por un mismo
identificador de textura OpenGL (texture id).
Nivel lógico (Instancias de FpKernel y FpStream)
Vínculo lógico
FpKernel
1
Output
FpStream 1
Input
FpStream 2
FpKernel
2
Nivel físico (Programas Cg y texturas 2D)
Entrada
Salida
Programa de
fragmentos
Cg 1
Textura
OpenGL 2D
Programa de
fragmentos
Cg 2
Figura 6.7. Esquema lógico y físico de Kernels y Streams
50
Es posible también crear múltiples streams de entrada por referencia, esto es, varios
streams que comparten el mismo espacio de almacenamiento bajo diferentes
denominaciones.
6.5.3
Un ejemplo de uso
El siguiente ejemplo ilustra la forma en que se combinan ambas clases para crear un
programa sencillo que implementa un kernel de múltiples pasadas.
int w = 64; h = 64; // Resolución horizontal y vertical
// Declaración de kernels
FpKernel k;
FpStreamInput sIn1,sIn2;
FpStreamInputOutput sInOut;
// instanciación de los kernels
// inN son los identificadores en el programa CG
// *dataN poseen datos para inicializar los streams
k = new FpKernel (w,h,”krn1.cg”);
sIn1=new FpStreamInput(w,h,”in1”,*data1);
sIn2=new FpStreamInput(w,h,”in2”,*data2);
sInOut = new FpStreamInputOutput (w,h,0,”in3”,*data3);
// fija el valor de un parámetro uniforme
k->setParameter1f(“sampleparam”,1.75);
k->setStream(sIn1);
//vincula stream de entrada 1
k->setStream(sIn2);
//vincula stream de entrada 2
k->setStream(sInOut);//vincula stream de entrada/salida
int occlusion_query=0;
do {
occlusion_query=k->run(); // ejecuta el kernel
}
while (occlusion_query>0); // iterar
La figura 6.8 muestra la relación entre los distintos objetos del ejemplo anterior.
Figura 6.8: Diagrama de Kernels y Streams para el ejemplo de uso.
6.6 Trazador de fotones y rayos en la GPU
Dadas las similitudes entre el proceso de trazado de rayos y fotones, se decidió
diseñar una clase C++ capaz de resolver el trazado de ambos casos mediante un
51
mecanismo unificado. Se utilizó la estructura de aceleración BVH (Bounding Volumes
Hierachy) y los ejemplos provistos en el trabajo de Thrane et al. [18] como base para la
implementación del caso de estudio. En dicho trabajo se presenta a esta estructura como
la más eficiente y sencilla desde el punto de vista de la programación, para implementar
en la GPU.
6.6.1 La estructura de aceleración BVH
Es una estructura que subdivide la escena en una jerarquía conformada por dos tipos
de nodos: triángulos y volúmenes envolventes (ver figura 6.9). La estructura tiene la
forma de un árbol binario. Los nodos internos corresponden a los volúmenes
envolventes que contienen a todos los elementos de su rama. Los volúmenes, en este
caso, son cajas alineadas con los ejes coordenados. Las hojas del árbol corresponden a
los elementos geométricos de la escena (en este caso los triángulos).
La idea detrás de esta estructura es ahorrar un gran número de verificaciones de
intersección entre triángulos y rayos. Para ello se envuelve un conjunto de elementos
geométricos cercanos con una caja (alineada a los ejes coordenados) y se evalúa la
intersección del rayo con la caja (lo cual, tiene de hecho, un costo menor que evaluar la
intersección de un rayo y un triángulo). En caso de no existir intersección con la caja,
tampoco existirá con los elementos geométricos contenidos en ella.
El proceso para verificar la intersección de un rayo con toda la escena implica
recorrer el árbol haciendo un descenso recursivo comenzando por la raíz.
Figura 6.9: Ejemplo de la estructura BVH aplicada a un conjunto de 5 triángulos.
No existe hasta el momento un algoritmo óptimo de construcción del árbol, pero
existen diferentes heurísticas. El algoritmo que se utilizó en esta implementación esta
basado en el presentado por Thrane et al. [18].
La idea general es tomar el conjunto de triángulos que conforman la escena, calcular
las dimensiones de una caja alineada a los ejes coordenados que envuelva a todo el
conjunto (Bounding Box) y luego calcular la media aritmética de las coordenadas de los
vértices de todos los triángulos. A continuación se debe dividir el conjunto de triángulos
en 2 mitades, según un plano cuya normal coincida con el lado más largo de la caja, que
pase por la media aritmética del conjunto de vértices (ver figura 6.9). Cada subconjunto
de triángulos pasa a formar parte de los dos nodos hijos según el centro de cada
triángulo se encuentre de uno u otro lado del plano de corte. Luego el proceso se repite
para cada nodo hasta que quede un solo triángulo por nodo.
52
A continuación se delinea el algoritmo de construcción:
BVHNode construirArbol (triángulos)
if (triángulos es solo 1 triángulo){
return hoja del árbol con un triángulo
}else {
Calcular punto de corte (media arit. de coord. de los vértices)
Calcular mejor plano de corte (cuya normal = al lado mas largo)
BVHNode res
res.leftChild = construirArbol (triángulos a la izq. del corte)
res.rightChild = construirArbol (triángulos a la der. del corte)
res.boundingBox = caja que envuelva a todos los triángulos
return res
}
El proceso de construcción lo realiza la CPU como un preproceso junto con la carga
del modelo 3D. El resultado es un conjunto de arreglos (ver fig. 6.10) que indican como
recorrer el árbol para hacer la verificación de intersecciones (no se almacena la
estructura completa, sino sólo los datos necesarios para recorrerlo en forma
descendente).
0
1
2
3
4
5
6
7
8
Traversal Array
type
index
escape
code
0
0
INF
0
1
6
0
2
5
1
0
-1
1
1
-1
1
2
-1
0
3
INF
1
3
-1
1
4
-1
type: 0 = bounding box
1 = triangle
0
1
2
3
0
1
2
3
4
bbox_min
(x,y,z)
…
…
…
V0
(x,y,z)
…
…
…
…
V1
…
…
…
…
…
bbox_max
…
…
…
…
V2
…
…
…
…
…
N0
…
…
…
…
…
N1
…
…
…
…
…
N2
…
…
…
…
…
Figura 6.10: Estructuras de datos del algoritmo BVH.
53
El algoritmo de detección de intersecciones rayo-triángulo usando la estructura
BVH, comienza recorriendo el arreglo Traversal Array en orden secuencial, evaluando
en cada posición si hay intersección entre el rayo y el nodo. Si el valor del campo type
es 0, se debe acceder a los parámetros de la caja envolvente en los arreglos bbox_min y
bbox_max, que definen las coordenadas de sus extremos usando el valor del campo
index. Si el valor del campo type es 1, se debe acceder a los arreglos V0, V1, V2, que
definen los vértices del triángulo.
En caso de no verificarse intersección,con un nodo, esto significa que tampoco
ocurrirá con ningún elemento de la rama. El código de escape (escape code) indica a
qué registro del arreglo hay que saltar para evitar toda la rama.
Los arreglos N0, N1 y N2 son las normales en cada uno de los vértices de los
triángulos.
6.6.2 Clase Tracer
Esta clase se ocupa de trazar rayos o fotones, a partir de dos streams que indican los
orígenes y las direcciones.
La clase utiliza los servicios de la clase SceneLoader (ver punto 6.8.1), encargada de
cargar la geometría de la escena desde un archivo y generar las estructuras de datos para
el algoritmo BVH.
La figura 6.11 detalla los métodos y atributos principales de la clase.
Figura 6.11: Clase Tracer.
El método init, que inicializa los atributos internos, acepta como parámetros: los
streams de orígenes, direcciones y de estado inicial (indica si algún rayo o fotón no debe
ser trazado).
El método trace realiza el trazado utilizando la estructura de aceleración detallada en
el punto 6.6.1. Por último, los métodos getTraverserStates y getIntersectorNormals
devuelven en forma de streams el resultado del trazado que incluye por cada rayo o
fotón:
•
Estado: indica si el rayo o fotón impactó alguna superficie o salió fuera de los
límites de la escena.
54
•
•
•
Coordenadas de impacto: define el punto exacto de impacto sobre el triángulo,
en coordenadas locales del plano del triángulo.
Identificador del triángulo impactado: el índice del triángulo dentro de la
escena.
Distancia Z: la distancia al punto de impacto desde el origen del rayo o fotón
hasta alcanzar el punto de impacto en la superficie.
6.6.3 El trazado de fotones
Para el caso de estudio se estableció un máximo de 4 etapas de trazado de fotones.
En la figura 6.12 se observa un ejemplo en donde se emiten 1000 fotones desde la
fuente de luz. En la primera etapa los fotones son trazados hasta interactuar la primera
superficie S1. Dichos fotones representan iluminación directa, por lo tanto, no es útil
almacenarlos, ya que la iluminación directa se resuelve mediante Ray Tracing. Por lo
tanto Ab1 es 0 y todos los fotones incidentes en S1 son reflejados.
diffuseK = 0,3
specularK = 0,1
absorbK = 0,6
S2
Fuente de Luz
Nf=1000
Ab2=600
Pm2=600
Re1=1000
Rd1=750
Rs1=250
S4
Re3=160
Rd3=120
Rs3=40
Ab4=96
Pm4=96
Re4=64
Re2=400
Rd2=300
Rs2=100
S1
Ab1=0
Pm1=0
S3
Ab3=240
Pm3=240
Nf = número de fotones emitidos
Rei = fotones reflejados por la superficie i
Rsi = fotones reflejados en forma difusa por la superficie i
Rdi = fotones reflejados en forma especular por la superficie i
Abi = fotones absorbidos por la superficie i
Pmi= fotones almacenados en la superficie i
Figura 6.12: Fases de trazado de fotones.
Luego, en las superficies S2, S3 y S4 una fracción de los fotones incidentes son
absorbidos. Aquellos fotones que son reflejados desde la última superficie S4, son
descartados. Rdi y Rsi corresponden a la cantidad de fotones reflejados en forma difusa
o especular de acuerdo a los coeficientes diffuseK y specularK en la superficie i.
55
En el método aplicado, en el caso de estudio, los fotones absorbidos por cada
superficie Si, son los almacenados para luego ser utilizados en la fase de Photon
Splatting (ver detalles en el punto 6.7).
6.7 Photon Splatting
Esta técnica fue presentada por primera vez por Stürzlinger y Bastos [19] en 1997.
Permite hacer una reconstrucción de la iluminación indirecta a partir de la información
generada por el trazado de fotones, usando un método basado en imágenes. Luego
Paulin et al. [21] presento algunas mejoras al método original utilizando hardware
gráfico. El objetivo es reconstruir la irradiancia en cada píxel del plano de vista
estimando el aporte de cada fotón. Esto se puede lograr de 2 maneras:
•
•
Para cada píxel, buscar los fotones que impactan dentro de cierto radio del punto
visible a través del píxel y sumar su contribución al estimador.
Para cada fotón, agregar su aporte al estimador de cada píxel cercano (a una
distancia menor a cierto radio del punto visible este a través del píxel).
Esta segunda forma es la que aplica la técnica de Photon Splatting. Un splat tiene la
forma de un disco, que representa la contribución del fotón a la iluminación indirecta en
las cercanías del punto de impacto. Dicha contribución se computa y acumula sobre
cada píxel del plano de vista, cubierto por la proyección del splat, sólo si dicho píxel
pertenece a la misma superficie en la que impactó el fotón. Esto es importante, ya que la
cercanía en el plano de proyección no implica una cercanía en el espacio tridimensional.
De hecho, para 2 píxeles contiguos, su distancia en la coordenada Z (normal al plano
de proyección) puede ser considerable (mayor al radio del splat).
El algoritmo consiste en iterar sobre la lista de fotones de la escena y generar por
cada uno, un disco centrado en el punto donde impactó el fotón y orientado según la
normal de la superficie, que luego es proyectado sobre el plano de vista. La suma de
todas las proyecciones se acumulará en el buffer de salida, utilizando el modo de
OpenGL de fundido aditivo (additive blending).
Antes de comenzar el proceso, se establece la matriz de vista y proyección según los
parámetros de la escena. Luego se procede a dibujar cada disco o splat ejecutando un
comando OpenGL que dibuja un triángulo (GL_TRIANGLE). Un programa de
fragmentos que resuelve el sombreado del triángulo muestrea la función de distribución
de energía del fotón a partir de una textura 2D. La forma de esta distribución responde a
una función patrón (o kernel function) también conocida como filtro, que depende de la
distancia al centro del splat.
Existen tres tipos de funciones patrón usadas habitualmente (ver figura 6.13):
•
•
•
Constante: La intensidad es constante dentro del disco, y nula fuera de este.
Cónica: La intensidad decae linealmente desde el centro hasta el radio del disco.
Gaussiana: La intensidad decae desde el centro hacia afuera según la forma de
una campana gaussiana.
Luego del proceso de rasterización del splat, se debe analizar para cada fragmento (o
“píxel potencial”) si el aporte definido por el muestreo de la función patrón debe ser
agregado o no al buffer de salida. Para sumarlo deben cumplirse dos condiciones:
56
•
r
r
Las normales n x y n f en los puntos X y F respectivamente, deben ser similares
Esta similitud se define en términos un producto escalar entre ambos vectores
(ver figura 6.14).
r r
n x • n f > umbral
Donde 0 < umbral < 1. Esto evita que en zonas como esquinas, el fotón aporte
iluminación a una superficie cercana, pero que está orientada en una dirección
muy diferente.
•
Los identificadores de superficie en los píxeles X’ y F’ deben ser coincidentes.
Estos identificadores son valores enteros asignados a todos los triángulos de
cada objeto de la escena (un objeto puede estar compuesto por varios
triángulos).
Figura 6.13: Funciones patrón.
Figura 6.14: Condiciones de aceptación/rechazo de fragmentos del splat.
57
En el ejemplo de la figura 6.15 se ilustran diferentes casos. Dado el punto F
correspondiente al punto de impacto del fotón, analizando el punto X1 (asociado al píxel
r
r
X1’) se ve que las normales n x1 y n f son similares y sus identificadores de superficie
también ya que ambos puntos pertenecen al mismo objeto, por lo tanto el aporte del
r
píxel X1’ es agregado al buffer de salida. Para el punto X2, las normales n x 2 y
r
n f difieren considerablemente, por lo tanto, se descarta el aporte del píxel X2’del splat.
Por último para el punto X2, las normales son similares pero difieren en los
identificadores de superficies por ende su aporte tampoco es tenido en cuenta.
Figura 6.15: Distribución de aporte de un fotón sobre los píxeles del plano de vista.
La calidad de la solución obtenida mediante el método de Photon Splatting, es
proporcional al número de fotones utilizados en el proceso. A mayor cantidad de splats
sumados, más suave (con menor ruido) es la iluminación de la imagen obtenida. A su
vez, un radio de splat más grande también contribuye a reducir el ruido, pero elimina
detalles locales de la iluminación, ya que cada splat afecta un área demasiado grande.
En la figura 6.16, se ilustran 4 casos en donde se aplica el método sobre una escena
conformada por una caja y una fuente de luz direccional ubicada en el plano superior
apuntando hacia abajo. En cada caso se utilizó una cantidad diferente de fotones.
La figura 6.16a exhibe una calidad insuficiente, mientras que la figura 6.16d logra
una calidad aceptable con un nivel de ruido casi imperceptible.
Además, la calidad depende del tipo de función patrón que se utiliza. El filtro
gaussiano generalmente genera mejores resultados.
La imagen resultante del proceso de splatting se denomina textura de irradiancia y
sirve de entrada a la fase de rendering. Provee el valor de la irradiancia sobre los puntos
de las superficies vistos desde el ojo del observador (por lo tanto es dependiente del
punto de vista de la escena).
58
Figura 6.16: Photon Splatting con una función patrón constante
(radio del splat=1,5 unidades, las paredes de la escena miden 10x10 unidades).
6.8 Motor de Rendering basado en Photon Mapping
Sobre la base del marco de trabajo presentado en 6.5 se diseñó un motor de
rendering capaz de sintetizar una vista de una escena 3D a partir de dos archivos de
entrada que contienen la descripción de la escena. El motor se compone de 3 clases
principales:
•
•
SceneLoader: carga y conversión a streams de los datos de entrada y generación
de estructura de aceleración de trazado.
PhotonMapper: Resuelve el trazado de fotones.
59
•
RayTracer: integra los resultados generados por la clase PhotonMapper con el
trazado de rayos desde el ojo del observador para sintetizar la imagen de salida.
En la figura 6.17 se detalla la relación entre estas clases y el flujo de datos entre
ellas.
Figura 6.17: Arquitectura del motor de Rendering.
6.8.1 La clase SceneLoader
Se ocupa de cargar en memoria la información de la escena desde archivos, construir
la estructura de aceleración para el trazado de rayos o fotones (detallada en 6.6.1) y
convertir esos datos a streams.
La información de la geometría de la escena, es leída desde un archivo en formato
3DS.
Este formato es un estándar para el intercambio de modelos 3D que nació como
parte del software de edición 3D Studio.
Además carga los parámetros que describen la fuente de luz, el punto de vista, las
propiedades de los materiales y las características de la imagen a sintetizar, desde un
archivo de texto plano denominado “param.ini”.
La clase implementa un singleton que provee los streams de entrada requeridos por
las clases RayTracer y PhotonMapper.
6.8.2 La clase PhotonMapper
Esta clase se encarga de trazar los fotones emitidos por la fuente de luz a través de la
escena. El proceso se realiza por medio de cuatro kernels (ver figura 6.18).
Primero, el kernel Photons Generator, muestrea el espacio de emisión de fotones
de la fuente de luz mediante la generación de pares de coordenadas polares aleatorias
dentro de los límites del cono de luz (ver figura 6.2) definido para la escena. Este kernel
60
genera como salida los streams que representan origen, dirección, estado y color de los
fotones. El proceso de muestreo requiere la generación de números al azar desde el
programa Cg con el que se implementa el kernel. Debido a que en la plataforma de
software utilizada, la instrucción Cg para generar números aleatorios no está
implementada, fue necesario generar dichas secuencias de números en la CPU y
cargarlas en texturas para ser utilizadas por el kernel.
Parámetros
de la fuente
de luz
Photons
Generator
Streams de
geometría 3D +
estructura. BVH
1
Parámetros
de la vista
Tracer
4
2
PhotonMapper
Photons
Reflector
3
Photons
Projector
Streams
1. Fotones (origen,
dirección y estado)
2. Fotones (datos de
intersección)
3. Fotones reflejados
(origen y dirección),
son la entrada de una
nueva fase de trazado.
4. Fotones
absorbidos(origen,
dirección y datos de
intersección)
5. Splats (posición, id de
objeto, color y normal)
5
Datos de
los Splats
Figura 6.18: Diseño interno de la clases PhotonMapper
(flujo de datos entre los kernels)
El proceso continúa con una fase de trazado derivada a una instancia de la clase
Tracer (detallada en el punto 6.6.2). Los fotones trazados pasan luego al kernel
Photons Reflector, que se encarga de simular la interacción con las superficies,
determinando cuales deben ser absorbidos, reflejados de forma difusa o especular. Los
fotones reflejados sirven de entrada para una nueva etapa de trazado. Este proceso se
repite hasta completar un número máximo de fases de trazado definido en el archivo
“param.ini”.
Aquellos fotones que son reflejados luego de la última fase trazado junto con
aquellos que escapan fuera de los límites de la escena, son descartados.
Al concluir las N fases de trazado, se tiene un conjunto de datos que especifica los
fotones absorbidos por las superficies (incluyendo el color que es modificado de
acuerdo al color de las superficies en las que impacto el fotón).
Para finalizar, el kernel Photons Projector traduce los fotones absorbidos en una
lista de atributos que especifican la forma y ubicación de los splats en el sistema de
coordenadas de la escena, que servirán de entrada a la fase de Photon Splatting.
Concretamente:
•
•
Las coordenadas X, Y, Z de vértices del triangulo que representa al splat.
Las coordenadas X, Y, Z de la normal del splat.
61
•
El identificador de la superficie impactada.
6.8.3 La clase Raytracer
Se encarga de trazar los rayos desde el ojo del observador, realizar el proceso de
Photon Splatting usando los datos generados por la clase PhotonMapper, para luego
sintetizar la imagen final. En la figura 6.19 se observan los kernels involucrados en el
proceso.
Parámetros del
punto de vista
Raytracer
Datos de
los Splats
Surface-Data
Generator
Eye Rays
Generator
2
1
Tracer
9
2
10
Shader
(primarios)
Proceso
de
Photon
Splatting
8
2
Specular
Reflections
Generator
9
2
Shadows
Generator
4
3
Integrator
Clase
Tracer
5
7
Clase
Tracer
6
Shader
(secund.)
11
Streams
Imagen
Rayos primarios (origen y dirección).
sintetizada
Rayos primarios (datos de intersecciones).
Rayos de sombra (origen y dirección).
Rayos de sombra (datos de intersecciones).
Rayos secundarios por inter-reflexiones especulares (origen y dirección).
Rayos secundarios por inter-reflexiones especulares (datos de intersección).
Colores en puntos de impacto de rayos secundarios.
Colores en puntos de impacto de rayos primarios.
Normales e identificadores de superficies en los puntos de impacto de los
rayos primarios.
10. Irradiancia en puntos de impacto de los rayos primarios.
11. Píxeles de la imagen sintetizada.
1.
2.
3.
4.
5.
6.
7.
8.
9.
Figura 6.19: Diseño interno de la clase RayTracer
(flujo de datos entre sus kernels).
62
Primero, el kernel Eye Ray Generator muestrea los píxeles sobre el plano de
proyección y genera los rayos primarios, con su origen en el ojo del observador.
Una instancia de la clase Tracer traza los rayos primarios y determina los puntos de
intersección con las superficies. A partir de los resultados del trazado, el kernel
Surface-Data Generator determina los atributos de las superficies impactadas por cada
rayo (normal e identificador de superficie).
El kernel Specular Reflections Generador, genera los rayos secundarios que
corresponden a las inter-reflexiones especulares en los puntos impactados por los rayos
primarios.
El kernel Shadows Generator genera los rayos de sombra que determinarán la
visibilidad de la fuente de luz desde los puntos de impacto de los rayos primarios.
Tanto los rayos de sombra como los rayos secundarios, son trazados utilizando los
servicios de una instancia de la clase Tracer.
El kernel Shader calcula el sombreado local usando el modelo de Phong
incorporando la irradiancia computada en la fase de Photon Splatting y la información
capturada por los rayos de sombra para determinar la intensidad de iluminación directa
que llega al punto.
Para finalizar, el kernel Integrator, agrega el aporte de iluminación por interreflexión especular (color capturado por los rayos secundarios) a la imagen generada
por el kernel Shader.
63
Capítulo 7 - Evaluación de la implementación
En este capítulo se presentan una serie de mediciones realizadas sobre la aplicación
implementada en el caso de estudio definido en el capítulo anterior, con el fin de evaluar
el tiempo requerido para sintetizar una imagen de una escena de prueba, bajo diferentes
parámetros.
Además se describe la plataforma de hardware y software utilizada. Por último se
hace un análisis cualitativo de las imágenes generadas.
7.1 Entorno de hardware y software
La plataforma utilizada para la implementación posee las siguientes características:
Software:
• Sistema operativo: Microsoft Windows XP SP2.
• Lenguaje de programación: Microsoft Visual C++ 7.0.
• OpenGL 2.0. y NVIDIA Cg Toolkit 1.5.
• Controlador de video NVIDIA versión 91.45.
Hardware:
• CPU: AMD Athlon 64 3000+ (1.8 GHz).
• GPU: MSI NVIDIA GeForce 6600 GTS 128 MB DDR.
• RAM: 1024 MBytes DDR400 MHz.
7.2 Mediciones del desempeño
Se diseñó un modelo de una “caja de Cornell” (Cornell Box) para evaluar el
funcionamiento de la implementación. En la figura 7.1 se observan los ángulos que
definen al cono de emisión de la fuente de luz. El modelo está delimitado por las
paredes que forman un cubo de 10x10x10 unidades, excepto la pared cercana al ojo del
observador que fue removida, por lo tanto, los fotones emitidos hacia dicha superficie
saldrán fuera de los límites de la escena y por lo tanto serás descartados.
Todas las superficies del modelo poseen un coeficiente de reflexión difusa de 0,2 y
un coeficiente de reflexión especular de 0,1. Por lo tanto, si 100 fotones intersecan una
de estas superficies, el 70% será absorbido y el 30% restante es reflejado, parte en
forma especular (10%) y el resto en forma difusa (20%).
Se estableció un máximo de 4 fases, para el proceso de trazado de fotones (por lo
tanto un fotón puede ser reflejado como máximo en 3 veces).
Además, para todas las superficies se definió, un coeficiente de inter-reflexión
especular perfecta con valor nulo, por lo tanto no se computaron rayos secundarios en la
etapa de Ray Tracing.
Bajo estas restricciones se evaluaron las 27 combinaciones de los siguientes
parámetros:
•
•
•
Resolución de imagen de salida: 128x128, 256x256 y 512x512 píxeles.
Radio del splat: 0,5; 1,0 y 1,5 unidades.
Número de fotones generados: 16384, 65535 y 262144.
64
Figura 7.1: Modelo “Cornell Box” utilizado en las mediciones.
Estos 3 parámetros fueron seleccionados ya que afectan en forma más directa la
calidad de la solución obtenida. En el caso de los parámetros de resolución de imagen y
la cantidad de fotones un incremento suele producir una mejor solución o estimación de
la iluminación de la escena.
Tabla 1. Medición de tiempos de generación de un cuadro.
Tiempo de generación (seg.)
según la resolución
de imagen (píxeles)
Radio del
splat
0,5
1,0
1,5
Tiempo de fase de splatting
(seg.) según resolución
de imagen (píxeles)
Fotones
generados
128 x
128
256 x
256
512 x
512
128 x
128
256 x
256
512 x
512
16384
4,87
5,09
6,17
0,20
0,22
0,33
65535
5,87
6,56
7,37
0,50
0,52
0,73
262144
10,67
11,43
12,33
1,48
1,51
1,92
16384
5,47
5,11
6,78
0,22
0,25
0,53
65535
5,89
6,25
8,22
0,51
0,64
1,59
262144
10,69
11,22
15,89
1,51
1,72
5,23
16384
4,86
5,19
6,65
0,23
0,33
0,84
65535
6,00
6,53
9,48
0,61
0,95
2,84
262144
10,7
12,34
20,43
1,48
2,84
10,01
65
Sin embargo, en el caso del radio de splat, un valor muy grande genera una solución
incorrecta por afectar la iluminación de puntos demasiado distantes al punto de impacto
del fotón.
Es importante mencionar que debido a limitaciones del controlador de video y el kit
de desarrollo de software Cg utilizado, no fue posible utilizar en las mediciones, streams
de salida con resoluciones de textura mayores a 512x512 píxeles. De ahí surge el valor
máximo de resolución de la imagen de salida y el número de fotones utilizados en las
mediciones.
Cabe aclarar que no todos los fotones generados se traducen en un splat, dado que
algunos se reflejan fuera de la escena y otros se descartan luego de ser reflejados pasado
el número máximo de reflexiones de los fotones.
Empíricamente, en esta escena, la cantidad de splats resultó ser equivalente a 75 ±
3% de los fotones generados. Por lo tanto si se generan 262144 fotones, se producen
aproximadamente 196000 splats. Este valor es aproximado, ya que incluso en corridas
sucesivas bajo parámetros idénticos, el número de splats es variable debido a que los
eventos que determinan el camino recorrido por un fotón dependen de variables
aleatorias.
En la tabla 1 se presentan los tiempos totales de generación de un cuadro (sin incluir
los tiempos de preprocesamiento iniciales requeridos para leer los archivos de la escena
y generar la estructura BVH).
Tabla 2. Incremento de tiempo de generación vs.
Incremento en la cantidad de fotones generados.
Factor de incremento del tiempo de
generación (seg.)
según la resol. de imagen (píxeles)
Radio
del
splat
0,5
1,0
1,5
Definición
del factor
Factor de incremento en la
cantidad de fotones
generados
128 x 128
256 x 256
512 x 512
1 (16384 fotones)
1,00
1,00
1,00
t1/t1
4 veces
1,21
1,29
1,19
t2/t1
16 veces
2,19
2,25
2,00
t3/t1
1 (16384 fotones)
1,00
1,00
1,00
t1/t1
4 veces
1,10
1,28
1,40
t2/t1
16 veces
1,95
2,20
2,34
t3/t1
1 (16384 fotones)
1,00
1,00
1,00
t1/t1
4 veces
1,23
1,26
1,43
t2/t1
16 veces
2,20
2,38
3,07
t3/t1
* t1 = tiempo de generación para 16384 fotones
* t2 = tiempo de generación para 65535 fotones
* t3 = tiempo de generación para 262144 fotones
66
En la tabla 2, se analiza el incremento en el tiempo de generación de un cuadro al
aumentar la cantidad de fotones. Se puede observar en todos los casos que el tiempo
crece sublinealmente respecto de la cantidad de fotones.
En la tabla 3, se comparan los tiempos de generación en relación al incremento en la
resolución de la imagen de salida. Nuevamente se observa que el tiempo crece
sublinealmente respecto de la resolución. En el peor caso (correspondiente a un radio de
splat = 1,5; fotones generados = 262144 y una resolución de 512x512 píxeles) el tiempo
de generación prácticamente se duplicó, para una imagen sintetizada con cuatros veces
más de pixeles.
Tabla 3. Incremento de tiempo de generación vs.
Incremento en la resolución de imagen.
Factor de incremento del tiempo de generación (seg.)
según incremento en la resol. de imagen (píxeles)
Radio del splat
0,5
1,0
1,5
Fotones
generados
1 (128 x 128)
2 veces
4 veces
16384
1,00
1,05
1,27
65535
1,00
1,12
1,26
262144
1,00
1,07
1,16
16384
1,00
0,93
1,12
65535
1,00
1,09
1,58
262144
1,00
1,05
1,49
16384
1,00
1,07
1,37
65535
1,00
1,09
1,58
262144
1,00
1,15
1,91
t1/t1
t1/t2
t1/t3
Definición del factor
* t1 = tiempo de generación para una imagen de 128x128 píxeles
* t2 = tiempo de generación para una imagen de 256x256 píxeles
* t3 = tiempo de generación para una imagen de 512x512 píxeles
En la tabla 4, se analiza el incremento en el tiempo de rendering respecto del
incremento en el radio del splat. Aquí se observa una variación mínima de tiempo
(menor a un 15%) para la resolución de 256x256 respecto del caso patrón (128x128
píxeles), esto se debe a que los splats en estos casos cubren muy pocos píxeles, entonces
el incremento en el radio no hace variar sustancialmente la cantidad de fragmentos que
deben ser computados en la fase de Photon Splatting. Luego, igual que en las tablas 2 y
67
3, se observa que el tiempo de generación de un cuadro crece sublinealmente respecto
del radio del splat.
Tabla 4. Incremento de tiempo de generación vs.
Incremento en la resolución de imagen.
Factor de incremento del tiempo de
generación (seg.)
según la resol. de imagen (píxeles)
Fotones
generados
16384
65535
262144
Definición
del factor
Radio del
splat
128 x 128
256 x 256
512 x 512
1
(0.5 unidades)
1,00
1,00
1,00
t1/t1
2 veces
1,12
1,00
1,10
t2/t1
3 veces
1,00
1,02
1,08
t3/t1
1
(0.5 unidades)
1,00
1,00
1,00
t1/t1
2 veces
1,02
1,00
1,29
t2/t1
3 veces
1,02
1,00
1,29
t3/t1
1
(0.5 unidades)
1,00
1,00
1,00
t1/t1
2 veces
1,00
0,98
1,29
t2/t1
3 veces
1,00
1,08
1,66
t3/t1
* t1 = tiempo de generación para un radio de splat de 0,5 unidades
* t2 = tiempo de generación para un radio de splat de 1,0 unidades
* t3 = tiempo de generación para un radio de splat de 1,5 unidades
Tabla 5. Incidencia del trazado de fotones y
fase de Photon Splatting (radio del splat = 1,25).
Fotones
generados
Tiempo total de
generación
de un cuadro
(seg.)
% de tiempo consumido
por la fase de trazado de
fotones
% del tiempo consumido
por la fase de Photon
Splatting
1024
6.81
40,82
2,90
4096
6.89
40,07
3,92
16384
7.42
40,16
8,33
65535
9.42
39,28
17,72
262144
16.92
42,90
32,56
68
En la tabla 5 se puede observar que las de fases de trazado de fotones y de Photon
Splatting tienen una incidencia importante en el proceso de generación.
Especialmente para el caso correspondiente a 262144 fotones generados (ver figura
7.2) donde la calidad de la imagen obtenida comienza a ser aceptable y ambas fases
consumen casi del 75% del tiempo.
Figura 7.2: Imagen generada con 512x512 píxeles, radio del splat =1,25
y 262144 fotones generados.
Tabla 6. Porcentaje del tiempo utilizado por la GPU.
Fotones generados
Tiempo total de generación
de un cuadro (seg.)
% de tiempo de utilización de la GPU
1024
5,56
29,94
4096
5,62
30,70
16384
6,48
36,72
65535
9,59
48,42
262144
20,32
62,89
69
En la tabla 6 se puede observar cuál es el porcentaje del tiempo utilizado por la GPU
durante el proceso de generación de la imagen en función de la cantidad de fotones
generados. Estas mediciones fueron realizadas sobre la misma escena de las pruebas
anteriores, utilizando una resolución de imagen de salida de 512x512 píxeles y un radio
de splat de 1,5.
Se podría decir que el tiempo de uso de la GPU crece en proporción al tamaño del
problema a resolver ya que es dependiente de las dimensiones de los streams a procesar.
En cambio, el rol de la CPU está mas relacionado a tareas de gestión de los recursos
como: la transferencia de datos entre la memoria principal y la GPU, la compilación de
los programas Cg, la gestión de los buffers, la ejecución de la API OpenGL y la
administración de los recursos del hardware gráfico. Al incrementar la cantidad de
fotones el tiempo de uso de la GPU adquiere mayor protagonismo.
Es por esto que casos de mayor complejidad (más resolución de la imagen de salida,
más cantidad de fotones, o mayor radio de splat) se resuelven de modo mas eficiente al
consumir proporcionalmente menos tiempo en preparación y mas tiempo en la
ejecución de los kernels en el procesador de fragmentos.
7.3 Imágenes
A continuación se presentan diversas imágenes generadas con la aplicación
implementada para evaluar el aspecto cualitativo de los resultados obtenidos.
En la figura 7.3 se presenta dos ejemplos que incorporan materiales reflectivos. En
7.3a se puede notar el efecto de enfoque de fotones producido por la superficie espejada
sobre el piso de la escena. En 7.3b se puede observar el efecto de sombra producido
sobre el rincón trasero derecho, que es la suma las sombras “duras” generadas en la fase
de Ray Tracing (por rayos de sombras) y las sombras suaves producto de la iluminación
indirecta.
(a)
(b)
Figura 7.3: Modelo “Cornell Box” con objetos reflectivos.
70
En la figura 7.4 se presentan imágenes de la misma escena utiliza en el punto 7.2
con sus componentes de iluminación separadas (7.4a y 7.4b). En la figura 7.4c se
observan los puntos sobre las superficies en donde los fotones fueron absorbidos.
Figura 7.4: (a) Componentes de iluminación ambiente + directa + sombras. (b)
Componente de iluminación indirecta. (c) Distribución de los fotones.
(d) Solución completa.
En la figura 7.5a se puede apreciar el modo en que es afectada la iluminación en el
piso de caja debido a los 2 tabiques verticales. Dichos tabiques tienen asociados
materiales puramente difusos que desvían gran parte de los fotones de la fuente hacia el
piso, excepto en la separación central en donde los fotones pasan al otro lado.
En la figura 7.5b se observa un defecto en la iluminación indirecta propio de la
técnica de Photon Mapping. En el borde inferior de la superficie rectangular ubicada en
71
el centro, algunos splats atraviesan la superficie filtrando luz por debajo e iluminando
una zona del piso que debería estar oscura.
(a)
(b)
Figura 7.5: Efectos de la iluminación indirecta.
La figura 7.6 corresponde a una escena que incluye una esfera espejada con un
índice de reflectividad (reflectK) de 0,3. Las tres imágenes corresponden a tres
orientaciones diferentes de la fuente de luz (señalada con el vector en color verde).
En esta figura se observa claramente el efecto conocido como “color bleeding” en
donde los fotones “capturan” el color de las superficies sobre las que se reflejan.
En la figura 7.6a es notorio el cambio de coloración de la escena hacia los rojos
debido a que la mayoría de los fotones impactan en la pared izquierda. Por el contrario
en la figura 7.6c la escena se tiñe de tonos azules.
(a)
(b)
(c)
Figura 7.6: Escena generada con 262144 fotones y un radio de splat = 1,0. (a) Fuente de
luz orientada 45º a izquierda de la normal. (b) Fuente de luz orientada hacia abajo (c)
Fuente de luz o orientada 45º a derecha de la normal
72
En la figura 7.7 se pueden comparar los resultados obtenidos al utilizar una función
patrón gaussiana o una función constante, utilizando 65536 fotones.
(a)
(b)
Figura 7.7: Comparación de tipos de funciones patrón. (a) Imagen generada con un
filtro gaussiano. (b) Imagen generada con un filtro constante.
En la figura 7.8, se observan dos imágenes sintetizadas a partir de la misma escena.
En 7.8a se desactivó el cómputo de rayos secundarios, por lo tanto el material reflectivo
se observa opaco. En 7.8b se ve el cómputo de la solución incluyendo rayos
secundarios.
(a)
(b)
Figura 7.8: Componente de inter-reflexión especular. (a) Imagen sin computar rayos
secundarios. (b) Imagen con rayos secundarios.
73
Capítulo 8 - Conclusiones
En este trabajo de tesis se presentaron diferentes técnicas de iluminación global para
la síntesis de imágenes a partir de una escena 3D. Se definió un caso de estudio en base
al método de Photon Mapping y el uso de los recursos de cómputo de una GPU.
Con la aplicación desarrollada, se han logrado tiempos de rendering aptos para
implementar aplicaciones interactivas. Es esperable que al ejecutar la misma aplicación,
sobre hardware gráfico más moderno, sea capaz de sintetizar imágenes en tiempos aptos
para aplicaciones de tiempo real, o que también sea capaz de obtener soluciones más
precisas (mayor resolución de imagen o mayor cantidad de fotones) en igual tiempo de
cómputo que en la plataforma utilizada en este proyecto.
A partir de las mediciones realizadas en el punto 7.2 se puede concluir que la
aplicación implementada resulta escalable respecto de los tres parámetros evaluados.
La rápida evolución tecnológica de las GPUs, impone cambios en la forma de
programar aplicaciones gráficas, que muchas veces obligan a rediseñarlas en forma
completa para aprovechar al máximo el potencial de las nuevas generaciones.
Al momento de concluir este proyecto (segundo semestre de 2007), NVIDIA
presentó una nueva arquitectura de cómputo denominada CUDA (Compute Unified
Device Architecture) que permitiría reescribir la implementación aquí presentada de
manera mucho mas sencilla. Está nueva arquitectura permite aprovechar el poder de
cómputo de la GPU para aplicaciones de propósito general mediante mecanismos mas
simples. Es posible escribir los programas (los kernels del modelo de Stream
Programming) directamente en lenguaje C estándar sin necesidad de utilizar la API
gráfica como OpenGL o DirectX, evitando así definir explícitamente mecanismos de
traducción de las estructuras de datos en texturas y las dificultades que se presentan a la
hora de depurar el código.
8.1 Trabajos Futuros
Respecto de las líneas de trabajo a seguir, sería recomendable comenzar por
optimizar la fase de Photon Splatting. Ya que de las mediciones presentadas en la tabla
5, surge que dicha etapa tiene una incidencia importante en el costo total de cómputo. A
tal fin, un camino sería evaluar el uso de otras primitivas geométricas para proyectar los
splats sobre la textura de irradiancia. Concretamente, se plantea analizar si el uso de
primitivas como puntos (GL_POINT) o rectángulos (GL_RECTANGLE) producen un
ahorro en la cantidad de fragmentos evaluados en el proceso. Esto se sustenta en el
hecho de que la forma circular de las funciones patrón se adapta de manera mas ajustada
al contorno de la primitiva, generando una menor área donde la función filtro es nula
(los puntos ubicados a una distancia mayor al radio del filtro).
Otro punto a explorar sería la posibilidad de implementar los preprocesos que
generan la estructura de aceleración BVH en la GPU. Dado que en este proyecto se
trabajó sobre escenas estáticas, el costo computacional de esta etapa en la CPU no fue
relevante. Pero ante la necesidad de diseñar un motor mas general, en donde las escenas
se modifican a lo largo del tiempo, sería necesario reconstruir la estructura múltiples
veces y allí es donde la incidencia de esta etapa cobraría mayor relevancia.
Respecto de la API 3D en la que se basa la aplicación desarrollada (en este caso fue
OpenGL), resultaría importante analizar las ventajas de utilizar la API de DirectX, para
mejorar el desempeño. Específicamente, existe una funcionalidad de la GPU que podría
resultar útil para optimizar el proceso de Photon Splatting, denominada “instanciación
74
de geometría” (geometry instancing). Esta funcionalidad, que no está disponible
actualmente en la API de OpenGL, permite clonar elementos geométricos ya cargados a
la memoria de la GPU. Para este proyecto podría ser utilizado para generar las múltiples
instancias de los splats, en lugar de hacerlo desde la aplicación corriendo en la CPU.
Otro aspecto a mejorar, está relacionado al aprovechamiento de los tiempos de
cómputo ociosos de la CPU y la GPU respectivamente. Sería conveniente estudiar si es
posible aprovechar el tiempo de cómputo de la CPU mientras la GPU está computando
kernels. Quizás esto se pueda lograr mediante el uso de múltiples hilos (multi-threads)
de ejecución en la aplicación C++ que se corran en forma asincrónica respecto de los
procesos de la GPU.
Un próximo paso a desarrollar a partir de este proyecto sería el diseño de un motor
de Photon Mapping de tiempo real capaz de generar de secuencias de imágenes que
conformen una animación. En ese caso habría que incorporar las ideas presentadas por
Jozwowski [27], que plantea formas de reutilizar cómputos entre cuadros sucesivos
aprovechando similitudes en las condiciones de iluminación. Concretamente platea que
los fotones trazados pueden ser almacenados y reutilizados en cuadros sucesivos y
asignándoles un atributo que mide su edad, conservarlos hasta que el cambio en las
condiciones de iluminación justifique reemitirlos. De esta manera se reduce la cantidad
total de fotones a trazar en cada cuadro, haciendo factible la generación de cuadros en
fracciones de segundos.
Otro tema central a indagar, es la posibilidad de realizar el proceso completo de
Photon Splatting en la GPU. Como se mencionó en el capítulo 6 debido a la
imposibilidad de direccionar aleatoriamente la memoria de la GPU para escritura desde
un programa de fragmentos, es necesario transferir los datos de los splats a la CPU para
luego proyectarlos sobre el plano de vista. Es esperable que futuras generaciones de
GPUs provean mecanismos mas generales de direccionamiento y así evitar el costo
innecesario de transferencia de datos entre la CPU y la GPU, el cual resulto ser
particularmente alto debido al bajo ancho de banda de bus de datos AGP utilizado en
este proyecto.
Desde el punto de vista de la plataforma, existen actualmente tecnologías que
permiten instalar multiples GPUs en una misma PC , mediante la tecnología de
NVIDIA denominada SLI (Scalable Link Interface), sería interesante analizar que
modificaciones serían necesarias realizar sobre la implementación para aprovechar la
capacidad de este tipo de arquitecturas.
Respecto del método de Photon Mapping aquí implementado existen muchos
aspectos a ser mejorados a fin de implementar tipos de efectos de iluminación más
complejos como los de refracción, caustics o el cómputo de la iluminación indirecta
producto de inter-reflexiones en superficies del tipo “glossy” donde la dirección en la
que impacta el fotón debe ser tenida en cuenta, a diferencia de las superficies difusas
donde la radiancia reflejada se distribuye en forma uniforme en todas las direcciones.
75
A
Publicaciones
•
Federico Marino y Horacio Abbate, “Implementación de Photon Mapping en
hardware gráfico programable”. En AST (Simposio Argentino de Informática),
como parte de las 36º Jornadas Argentinas de Informática. Agosto de 2007.
•
Federico Marino y Horacio Abbate, “Stream Programming Framework for
Global Illumination Techniques Using a GPU”. En CACIC 2007 (Congreso
Argentino de Ciencias de la Computación). Octubre de 2007.
76
B
Código Fuente
A continuación se incluye el código fuente de los kernels principales utilizados en la
implementación, escritos en lenguaje Cg.
Ray.cg
#ifndef RAY_CG
#define RAY_CG
struct ray
{
float3 o;
float3 d;
};
float3 evaluate(in ray r, in float module)
{
return r.o + module*r.d;
}
#endif
Photon.cg
#ifndef PHOTON_CG
#define PHOTON_CG
struct photon
{
float3 o;
float3 d;
float power;
};
#endif
Common.cg
#ifndef COMMON_CG
#define COMMON_CG
#define
#define
#define
#define
#define
STATE_ABSORBED (-3)
STATE_DONT_TRACE (-2)
STATE_TRAV_SETUP (-1)
STATE_SHADE (3)
STATE_BACKGROUND (5)
#define INF (999999999)
const float infinity=9999999.9f;
int index3dto1d(in float3 voxel_idx_3d)
{
int r=(floor(voxel_idx_3d.z)*gridCount*gridCount)+
(floor(voxel_idx_3d.y)*gridCount)+ floor(voxel_idx_3d.x);
return r;
}
int3 index1d3d(in int voxel_idx_1d)
{
77
int3 result;
result.z = voxel_idx_1d/(gridCount*gridCount);
voxel_idx_1d-=result.z * gridCount*gridCount;
result.y = voxel_idx_1d/gridCount;
voxel_idx_1d-=result.y*gridCount;
result.x = voxel_idx_1d;
return result;
}
float2 getAddress( in int idx, in int width)
{
return float2(0.25+fmod(float(idx),float(width)), idx/width);
}
void copyState(in float2 idx : TEXCOORD0,
uniform samplerRECT states,out float4 state : COLOR0)
{
state = texRECT(states,idx);
}
#endif
Random.cg
uniform samplerRECT random;
uniform float randomTex_width;
uniform float global_rnd_seed;
float2 randomgen(float2 seed){
float2 OUT=texRECT(random, seed*randomTex_width).xy;
return OUT;
}
Generator.cg
#include "photon.cg"
#include "common.cg"
#include "random.cg"
// frustum atributes
uniform int frustumSize;
uniform float3 frustumOrigin;
uniform float frustumLeft,frustumRight,frustumTop,
frustumBottom,frustumNear;
uniform float maxAngle;
uniform float3 frustumUpVec,frustumNormalVec; //up and normal vector
void generateEyeRay(in float2 photonIndex:TEXCOORD0,
out float3 origin:COLOR0,
out float3 direction:COLOR1,
out float4 state:COLOR2)
{
const float scale=10;
float3 frustumRightVec = cross(frustumUpVec,frustumNormalVec);
float2 uv0 = float2(scale*frustumLeft,scale*frustumBottom);
float2 uv1 = float2(scale*frustumRight,scale*frustumTop);
float near = scale*frustumNear;
//Vectors along the U and V axis of the cone,
float3 a = (uv1.x - uv0.x)*frustumRightVec;
78
float3 b = (uv1.y - uv0.y)*frustumUpVec;
//Vector from the frustum to the lower left point
float3 c = frustumOrigin+uv0.x*frustumRightVec + uv0.y*frustumUpVec
+ near*frustumNormalVec;
float2 hituv = float2(photonIndex.x/frustumSize,
photonIndex.y/frustumSize);
float3 hit = hituv.x * a + hituv.y * b + c;
origin = frustumOrigin; // Photon origin
direction = normalize(hit - frustumOrigin); // Photon direction
// Photon
state.x =
state.y =
state.z =
state.w =
state vector
STATE_TRAV_SETUP;
infinity;
0;
0;
//state
//last isect value
//current voxel
//current triangle
}
float3 generateRandomDirection(float3 normal,float3 u, float2 random){
float3 OUT;
float3 v=cross(normal,u);
float alfa=acos(sqrt(random.x))*(maxAngle/90.0f);
float beta=random.y*radians(360.0);
OUT=(normal*cos(alfa))+(u*sin(alfa)*sin(beta))+(v*sin(alfa)*cos(beta));
return OUT;
}
void generatePhoton(in float2 photonIndex:TEXCOORD0,
out float3 origin:COLOR0,
out float3 direction:COLOR1,
out float4 state:COLOR2,
out float3 color:COLOR3)
{
origin = frustumOrigin; // Photon origin
float2 seed=float2(photonIndex.x/frustumSize,photonIndex.y/frustumSize);
direction = normalize(generateRandomDirection(frustumNormalVec,
frustumUpVec,randomgen(seed)));
// Photon state vector
state.x = STATE_TRAV_SETUP; //state
state.y = infinity;
//last isect value
state.z = 0;
//current voxel
state.w = 0;
//current triangle
color=float3(1,1,1);
}
Bvh.cg
#define
#define
#define
#define
#define
#define
STATE_ABSORBED (-3)
STATE_DONT_TRACE (-2)
STATE_TRAV_SETUP (-1)
STATE_TRAV (0)
STATE_SHADE (3)
STATE_BACKGROUND (5)
const float infinity=9999999.9f ;
float4 ReadIndex(samplerRECT data, float index, float width)
{
79
float2 index2D=float2(0.25+fmod(float(index),float(width)),index/width);
return texRECT(data,index2D);
}
// Used for ray/triangle intersection testing.
float4 Intersects(float3 a, float3 b, float3 c, float3 o, float3 d,float
minT, float vertexIndex, float4 lasthit)
{
float3 e1 = b - a;
float3 e2 = c - a;
float3 p = cross(d, e2);
float det = dot(p, e1);
bool isHit = det > 0.00001f; //the triangle is nearly edge-on
float invdet = 1.0f / det;
float3 tvec = o - a;
float u = dot(p, tvec) * invdet;
float3 q = cross(tvec, e1);
float v = dot(q, d) * invdet;
float t = dot(q, e2) * invdet;
isHit = (u >= 0.0f) && (v >= 0.0f) && (u + v <= 1.0f) &&
t >= 0.0f)&& (t < lasthit.z) && (t > minT);
float4 retVal;
if (isHit) retVal= float4(u, v, t, vertexIndex);
else retVal=lasthit;
return retVal;
}
// Checks for intersection between a ray and a box.
bool BoxIntersects(float3 box_min, float3 box_max, float3 o,float3 d,
float4 bestHit, float tMin) {
float3 tmin, tmax;
tmin = (box_min - o) / d;
tmax = (box_max - o) / d;
float3 real_min = min(tmin, tmax);
float3 real_max = max(tmin, tmax);
float minmax = min(min(real_max.x, real_max.y), real_max.z);
float maxmin = max(max(real_min.x, real_min.y), real_min.z);
bool res = minmax >= maxmin;
return res && bestHit.z >= maxmin && tMin < minmax;
}
//this is the type for the output of the main program
struct fragment_out
{
half4 bestHit : COLOR0; //best hit so far
half4 renderState : COLOR1; //records how far we are
};
// the main ray/scene intersection/traversal program (kernel).
// returns the best hit so far and the index to the next element
fragment_out multipass_main
(
float2 streampos : TEXCOORD0,//texcoord to ray and best hit
uniform samplerRECT bvhArray,
uniform samplerRECT v0,
uniform samplerRECT v1,
uniform samplerRECT v2,
uniform float vertexListTexWidth,
uniform samplerRECT bboxMin,
uniform samplerRECT bboxMax,
uniform float bboxTexWidth,
uniform samplerRECT origins,
//a list of ray origins
uniform samplerRECT directions, //a list of ray directions
80
uniform
uniform
uniform
uniform
uniform
samplerRECT bestHits,
//the best hits found so far
samplerRECT renderStates, //records how far in the traversal
float bvhArrayTexWidth, //the width of the geometry texture
float maxIndex, //maximum legal index in geometry texture
float looplimit //maximum number of allowed loops)
{
float4 renderState = texRECT(renderStates, streampos);
float datapos = renderState.x;
if (datapos > maxIndex) discard;
//find the ray and the previously best hit
float3 origin = texRECT(origins, streampos).xyz;
float3 direction = texRECT(directions, streampos).xyz;
float4 bestHit = texRECT(bestHits, streampos);
int loopcount = looplimit;
while (loopcount > 0 && renderState.x <= maxIndex)
{
half3 bvhData=ReadIndex(bvhArray,datapos,bvhArrayTexWidth);
if (bvhData.x == 0) // current element is a bbox
{
half3 boxMinData=ReadIndex(bboxMin,bvhData.y,bboxTexWidth);
half3 boxMaxData=ReadIndex(bboxMax,bvhData.y,bboxTexWidth);
if (BoxIntersects(bboxMinData, bboxMaxData, origin,
direction, bestHit, 0.0f)){
// advance to next record in BVH traversal Array
renderState.x += 1;
}
else {
// no bbox intersection use escape_code
renderState.x =bvhData.z;
}
}
else //current element is a triangle
{
half3 v0Data=ReadIndex(v0,bvhData.y,vertexListTexWidth);
half3 v1Data=ReadIndex(v1,bvhData.y,vertexListTexWidth);
half3 v2Data=ReadIndex(v2,bvhData.y,vertexListTexWidth);
bestHit = Intersects(v0Data,v1Data,v2Data,
origin.xyz, direction.xyz, 0.0,bvhData.y, bestHit);
// advance to next record in BVH traversal Array
renderState.x += 1;
}
datapos = renderState.x;
loopcount--;
}
fragment_out result;
result.bestHit = bestHit;
result.renderState = renderState;
return result;
}
void getStatesAndIsectData(in float2 pos : TEXCOORD0,
uniform samplerRECT bestHits,
uniform samplerRECT renderStates,
uniform float vertexListTexWidth,
out float4 outStates:COLOR0,
out float4 outIsectData:COLOR1
)
{
half4 renderState = texRECT(renderStates, pos);
half4 bestHit = texRECT(bestHits, pos);
81
float4 states=float4(0,0,0,0);
if ((renderState.x<0) || (renderState.x==(half)infinity)){
states.x=STATE_DONT_TRACE;// DON'T TRACE
}
else if (bestHit.w<0) {
states.x=STATE_BACKGROUND; // Background
states.y=infinity;
}
else {
states.x=STATE_SHADE; // SHADE
states.y=bestHit.z;
}
float4 isectData=float4(0,0,0,0);
isectData.xy=bestHit.xy;
float2 index2D=float2(0.25+fmod(float(bestHit.w),
float(vertexListTexWidth)),bestHit.w/vertexListTexWidth);
isectData.zw=index2D;
outIsectData=isectData;
outStates=states;
}
// is used as output by the initialization kernel.
struct setup_out
{
half4 bestHit : COLOR0;
half4 renderState : COLOR1;
};
// Initialization kernel. Sets up empty intersection records and
// traversal positions start at index 0.
setup_out setup(in float2 tc : TEXCOORD0,
uniform samplerRECT initialRenderStates)
{
setup_out result;
half initialState = (half)texRECT(initialRenderStates, tc).x;
result.bestHit = half4(0, 0, infinity, -1); //"no hit"
half x=0.0;
if ((initialState!=STATE_TRAV_SETUP)) x=(half)infinity;
result.renderState = half4(x, 1.0f, 1.0f, 1.0f);
return result;
}
PhotonSplatVp.cg
struct appdata
{
float4 position : POSITION;
float4 coord0 : TEXCOORD0;
};
struct vfconn
{
float4 pos : POSITION;
float2 coord0 : TEXCOORD0;
};
82
vfconn photonSplat(appdata IN,
uniform float4x4 ModelViewProjMatrix)
{
vfconn OUT;
OUT.pos = mul(ModelViewProjMatrix, IN.position);
OUT.coord0=IN.coord0;
return OUT;
}
PhotonSplatFp.cg
uniform
uniform
uniform
uniform
uniform
uniform
uniform
float photonObjectId;
float3 photonNormal;
float3 photonIrradiance;
sampler2D splat;
samplerRECT irradiances;
samplerRECT surfNormals;
samplerRECT surfDatas;
void photonSplat(in float2 pos:TEXCOORD0,
in float2 uv0:WPOS,
out half4 color:COLOR0
)
{
color = texRECT(irradiances, uv0.xy);
float4 surfdata = texRECT(surfDatas, uv0.xy);
float3 surfnormal = texRECT(surfNormals, uv0.xy).xyz;
half3 splatColor;
if ((surfdata.x==photonObjectId) &&
(dot(surfnormal,photonNormal)>0.75)){
splatColor=photonIrradiance*tex2D(splat, pos).x;
} else {
discard;
}
color=color+half4(splatColor.r,splatColor.g,splatColor.b,1);
}
PhotonsReflector.cg
uniform samplerRECT
uniform samplerRECT
uniform samplerRECT
uniform samplerRECT
uniform samplerRECT
// geometry data
origins;
directions;
colors;
states;
isectdatas;
//ray origins
//ray directions
uniform samplerRECT v0;
uniform samplerRECT v1;
uniform samplerRECT v2;
uniform samplerRECT n0;
uniform samplerRECT n1;
uniform samplerRECT n2;
uniform samplerRECT matColors;
uniform samplerRECT matAttribs;
83
uniform float bouncenum;
#include "ray.cg"
#include "common.cg"
#include "random.cg"
float3 lert(float3 a, float3 b, float3 c, float u, float v)
{
return a + u*(b - a) + v*(c-a);
}
float3 diffuseReflection(float3 normal,float3 u, float2 random){
float3 OUT;
float3 v=cross(normal,u);
float alfa=acos(sqrt(random.x));
float beta=random.y*radians(360.0);
OUT=(normal*cos(alfa))+(u*sin(alfa)*sin(beta))+(v*sin(alfa)*cos(beta));
return
OUT;
}
void photonsReflector(in float2 rayIndex:TEXCOORD0,
out float3 outrayOrig:COLOR0,
out float3 outrayDirec:COLOR1,
out float4 outrayState:COLOR2,
out float3 outrayColor:COLOR3)
{
float3
float3
float4
float3
float4
inrayOrig= texRECT(origins, rayIndex).xyz;
inrayDirec= texRECT(directions, rayIndex).xyz;
inrayState= texRECT(states, rayIndex).xyzw;
inrayColor= texRECT(colors, rayIndex).xyz;
inrayIsectData= texRECT(isectdatas, rayIndex);
outrayOrig=float3(0,0,0);
outrayDirec=float3(0,0,0);
outrayState=float4(0,0,0,0);
outrayColor=float3(0,0,0);
float2 triangleIndex = inrayIsectData.zw;
float4 matattribs = texRECT(matAttribs, triangleIndex);
float3 matColor = texRECT(matColors, triangleIndex).xyz;
if (inrayState.x == STATE_SHADE)
{
float
float
float
float
float
diffuseK=matattribs.x;
specularK=matattribs.y;
reflectK=matattribs.z;
smoothing=matattribs.w;
rangoRandomMax;
outrayColor=float3(inrayColor.x*(matColor.x/255.0),
inrayColor.y*(matColor.y/255.0),
inrayColor.z*(matColor.z/255.0));
if (bouncenum==0) rangoRandomMax=diffuseK+specularK;
else rangoRandomMax=1;
float event=randomgen(inrayIsectData.yx).y*rangoRandomMax;
if (event<(diffuseK+specularK)){// reflexión difusa o especular
84
// Photon should
float3 vertex0 =
float3 vertex1 =
float3 vertex2 =
be reflected
texRECT(v0, triangleIndex).xyz;
texRECT(v1, triangleIndex).xyz;
texRECT(v2, triangleIndex).xyz;
//Fetch normal
float3 normal0 = texRECT(n0, triangleIndex).xyz;
float3 normal1 = texRECT(n1, triangleIndex).xyz;
float3 normal2 = texRECT(n2, triangleIndex).xyz;
//calculate hit point
ray r;
r.o = inrayOrig;
r.d = inrayDirec;
float3 hitpoint = evaluate(r,inrayState.y);
//Calculate normal
float ucoord=inrayIsectData.x;
float vcoord=inrayIsectData.y;
if (smoothing<1.0) {
ucoord=0.5; vcoord=0.5;
}
float3 normal = normalize(lert(normal0, normal1,
normal2,ucoord,vcoord));// surface normal
float3 u=normalize(vertex0-vertex2);
if (event<specularK)
// Photon is speculary reflected
outrayDirec=reflect(inrayDirec,normal);
else
// Photon is diffusely reflected
outrayDirec=diffuseReflection(normal,u,
randomgen(inrayIsectData.xy));
// sumo para evitar auto intersección
outrayOrig=hitpoint+normalize(outrayDirec)*0.01f;
outrayState.x=STATE_TRAV_SETUP;
outrayState.y=INF;
}
else {// Photon is absorbed
outrayState=inrayState;
outrayState.x=STATE_ABSORBED;
}
}
else if (inrayState.x == STATE_BACKGROUND ||
inrayState.x == STATE_DONT_TRACE ||
inrayState.x == STATE_ABSORBED
) {
outrayState=inrayState;
}
else {
outrayState.x=-99; // error
}
}
85
PhotonsProjector.cg
uniform
uniform
uniform
uniform
samplerRECT
samplerRECT
samplerRECT
samplerRECT
origins;
directions;
states;
colors;
uniform samplerRECT isectdatas;
uniform samplerRECT n0;
uniform samplerRECT n1;
uniform samplerRECT n2;
uniform samplerRECT matColors;
uniform samplerRECT matAttribs;
uniform float3 frustumOrigin;
float photonPower;
uniform float hParam;
uniform float iParam;
uniform float splatArea;
#include "ray.cg"
#include "common.cg"
float3 lert(float3 a, float3 b, float3 c, float u, float v)
{
return a + u*(b - a) + v*(c-a);
}
void projector(in float2 rayIndex:TEXCOORD0,
out float4 data1:COLOR0, out float4 data2:COLOR1,
out float4 data3:COLOR2, out float4 data4:COLOR3)
{
const float3 zAxis=float3(0,0,1);
float4 state = texRECT(states, rayIndex).xyzw;
float3 photonColor = texRECT(colors, rayIndex);
data1=float4(0,0,0,-1); data2=float4(0,0,0,0);
data3=float4(0,0,0,0); data4=float4(0,0,0,0);
if (state.x == STATE_ABSORBED)
{
float4 isectdata = texRECT(isectdatas, rayIndex);
float2 triangleIndex =isectdata.zw;
float4 matAttrib = texRECT(matAttribs, triangleIndex);
float4 matColor = texRECT(matColors, triangleIndex);
// Hit Point POSITION
ray r;
r.o = texRECT(origins, rayIndex).xyz;
r.d = texRECT(directions, rayIndex).xyz;
// get a vector with "r" direction , "o" origin and state.y
float3 hitPosition = evaluate(r,state.y);
// Hit Point SURFACE NORMAL
float3 normal0 = texRECT(n0, triangleIndex).xyz;
float3 normal1 = texRECT(n1, triangleIndex).xyz;
float3 normal2 = texRECT(n2, triangleIndex).xyz;
86
float3 normal = normalize(lert(normal0, normal1,
normal2,isectdata.x,isectdata.y));
// Irradiance
float3 irradiance=photonColor*max(dot(normal,
-normalize(r.d))*(photonPower/splatArea),0);
// Splat Triangle Vertex
float3 u,v;
if (abs(dot(normal,zAxis))<1.0)
u=cross(zAxis,normal);
else
u=float3(1,0,0);
v=normalize(cross(normal,u));
float4 vertexA;
float4 vertexB;
float4 vertexC;
vertexA.xyz=hitPosition-hParam*(u+v);
vertexB.xyz=hitPosition+iParam*u-hParam*v;
vertexC.xyz=hitPosition+iParam*v-hParam*u;
data1.xyz=vertexA.xyz;
data2.xyz=vertexB.xyz;
data3.xyz=vertexC.xyz;
data4.xyz=normal;
data1.w=matColor.w; // OBJECT ID
data2.w=irradiance.r; // irradiance RED
data3.w=irradiance.g;
data4.w=irradiance.b;
//data1.w=-1;
}
else {
data1=float4(0,0,0,0);
data2=float4(0,0,0,0);
data3=float4(0,0,0,0);
data4=float4(0,0,0,0);
}
}
Shader.cg
uniform int viewSize;
uniform float colorscale;
uniform int specularBounce;
uniform
uniform
uniform
uniform
float3 lightOrigin;
float3 lightUpVec,lightNormalVec;
float maxAngle;
float minAngleRatio;
uniform
uniform
uniform
uniform
uniform
samplerRECT
samplerRECT
samplerRECT
samplerRECT
samplerRECT
uniform
uniform
uniform
uniform
samplerRECT v0;
samplerRECT v1;
samplerRECT v2;
int vertexListSize;
origins;
directions;
states;
isectdatas;
photonhits;
uniform samplerRECT n0;
87
uniform
uniform
uniform
uniform
samplerRECT
samplerRECT
samplerRECT
samplerRECT
n1;
n2;
matColors;
matAttribs;
uniform samplerRECT photonmapNormals;
uniform samplerRECT irradiance;
uniform samplerRECT shadows;
uniform
uniform
uniform
uniform
float
float
float
float
addAmbientIlum;
addDirectIlum;
addIndirectIlum;
addShadows;
#include "ray.cg"
#include "common.cg"
float3 lert(float3 a, float3 b, float3 c, float u, float v)
{
return a + u*(b - a) + v*(c-a);
}
const float ambientK=0.15;
const float glossiness=15.0;
void shader(in float2 rayIndex:TEXCOORD0,
out float4 color:COLOR0)
{
const float minLightLevel=0;
float3 origin = texRECT(origins, rayIndex).xyz;
float3 direction = texRECT(directions, rayIndex).xyz;
float4 state = texRECT(states, rayIndex).xyzw;
float4 isectdata = texRECT(isectdatas, rayIndex);
float4 photonhit = texRECT(photonhits, rayIndex);
float4 shadow = texRECT(shadows, rayIndex);
half3 irrad = texRECT(irradiance, rayIndex).xyz;
color = float4(0,0,0,1);
float3 lightLeftVec = cross(lightUpVec,lightNormalVec);
if (state.x == STATE_SHADE)
{
float2 triangleIndex = isectdata.zw;
//Fetch normal
half3 normal0 = texRECT(n0, triangleIndex).xyz;
half3 normal1 = texRECT(n1, triangleIndex).xyz;
half3 normal2 = texRECT(n2, triangleIndex).xyz;
//Fetch material color
half3 matColor = texRECT(matColors, triangleIndex).xyz;
half4 matAttrib = texRECT(matAttribs, triangleIndex);
half
half
half
half
bool
diffuseK=matAttrib.x; // prob. of diffuse reflection
specularK=matAttrib.y; // prob. of specular reflection
reflectK=matAttrib.z;
smoothing=fmod(matAttrib.w,128);
selfilum=(matAttrib.w>=128);
//calculate hit point
ray r;
r.o = texRECT(origins, rayIndex).xyz;
r.d = texRECT(directions, rayIndex).xyz;
float3 hit = evaluate(r,state.y);
float3 lightVec = lightOrigin-hit;
88
float3 lightVecN = normalize(lightVec);
// project light-hitpoint vector into light coordinates system
float lx=dot(lightLeftVec,lightVecN);
float ly=dot(lightUpVec,lightVecN);
float lz=dot(lightNormalVec,lightVecN);
float lightDistance=length(lightVec);
float alfa=degrees(atan2(sqrt(lx*lx+ly*ly),-lz));
float lightPower=minLightLevel;
if ((alfa<maxAngle) && (lz<0))
lightPower=(minLightLevel+smoothstep(maxAngle,
maxAngle*0.8,alfa)*(1-inLightLevel));
float attenuation=1/(1+0.01*lightDistance*lightDistance);
lightPower=lightPower*min(1,attenuation);
//Calculate normal
float ucoord=isectdata.x;
float vcoord=isectdata.y;
if (smoothing<1.0) {ucoord=0.5; vcoord=0.5;}
// surface normal
float3 normal = normalize(lert(normal0, normal1,
normal2,ucoord,vcoord));
float3 halfVec = normalize(lightVecN - direction);
float3 ambient=ambientK*matColor/255; // Ambient component
// Diffuse component
float noshadow=1;
if ((addShadows==1.0) && (shadow.y+0.1<lightDistance))
noshadow=0;
float d=dot(normal,lightVecN);
float3 directDiffuse=noshadow*diffuseK*max(d,0)*(matColor/255)*
lightPower*attenuation;
//Specular component
float3 directSpecular=float3(0,0,0);
float s=dot(normal, halfVec);
if ((s>0.0001) && (d>0))
directSpecular=noshadow*specularK*(max(0,pow(s,glossiness))
* lightPower*attenuation)*float3(1,1,1);
// Indirect component
float3 indirect=(matColor/255)*(irrad*0.1);
if (selfilum){
color.xyz=matColor/255;color.w=1;
}
else{
color.xyz=colorscale*(addAmbientIlum*ambient+
addDirectIlum*(directDiffuse+directSpecular)
+addIndirectIlum*(indirect));
color.w=max(0,1-reflectK);
}
}
else if (state.x == STATE_BACKGROUND)
{
color = float4(0,0,0,1);
}
else if (state.x == STATE_DONT_TRACE)
{
color = float4(0,0,0,0);
}
else
89
{
discard;
}
}
ShadowsGenerator.cg
uniform samplerRECT origins;
//Texture of ray origins
uniform samplerRECT directions; //Texture of ray directions
uniform samplerRECT states;
//State vector
uniform float3 lightOrigin;
#include "ray.cg"
#include "common.cg"
void shadowsGenerator(in float2 rayIndex:TEXCOORD0,
out float3 outrayOrig:COLOR0,
out float3 outrayDirec:COLOR1,
out float4 outrayState:COLOR2)
{
float3 inrayOrig= texRECT(origins, rayIndex).xyz;
float3 inrayDirec= texRECT(directions, rayIndex).xyz;
float4 inrayState= texRECT(states, rayIndex).xyzw;
outrayOrig=float3(0,0,0);
outrayDirec=float3(0,0,0);
outrayState=float4(0,0,0,0);
if (inrayState.x == STATE_SHADE)
{
//calculate hit point
ray r;
r.o = inrayOrig;
r.d = inrayDirec;
float3 hitpoint = evaluate(r,inrayState.y);
outrayDirec=normalize(lightOrigin-hitpoint);
outrayOrig=hitpoint+normalize(outrayDirec)*0.01f;
outrayState.x=STATE_TRAV_SETUP;
outrayState.y=INF;
}
else //if (inrayState.x == STATE_BACKGROUND)
{
outrayState.x=STATE_DONT_TRACE;
}
}
SpecularReflectionsGenerator.cg
uniform
uniform
uniform
uniform
samplerRECT
samplerRECT
samplerRECT
samplerRECT
origins;
directions;
states;
isectdatas;
uniform samplerRECT n0;
uniform samplerRECT n1;
90
uniform samplerRECT n2;
uniform samplerRECT matAttribs;
#include "ray.cg"
#include "common.cg"
float3 lert(float3 a, float3 b, float3 c, float u, float v)
{
return a + u*(b - a) + v*(c-a);
}
void specularReflector(in float2 rayIndex:TEXCOORD0,
out float3 outrayOrig:COLOR0,
out float3 outrayDirec:COLOR1,
out float4 outrayState:COLOR2)
{
float3
float3
float4
float4
inrayOrig= texRECT(origins, rayIndex).xyz;
inrayDirec= texRECT(directions, rayIndex).xyz;
inrayState= texRECT(states, rayIndex).xyzw;
inrayIsectData= texRECT(isectdatas, rayIndex);
outrayOrig=float3(0,0,0);
outrayDirec=float3(0,0,0);
outrayState=float4(0,0,0,0);
if (inrayState.x == STATE_SHADE)
{
float2 triangleIndex = inrayIsectData.zw;
float4 matattribs = texRECT(matAttribs, triangleIndex);
float specularity=matattribs.y;
float reflectivity=matattribs.z;
float smoothing=matattribs.w;
if (reflectivity>0.0){
//Fetch normal
float3 normal0 = texRECT(n0, triangleIndex).xyz;
float3 normal1 = texRECT(n1, triangleIndex).xyz;
float3 normal2 = texRECT(n2, triangleIndex).xyz;
//calculate hit point
ray r;
r.o = inrayOrig;
r.d = inrayDirec;
float3 hitpoint = evaluate(r,inrayState.y);
float u=0.5; float v=0.5;
if (smoothing==1.0) {
u=inrayIsectData.x;
v=inrayIsectData.y;
}
float3 normal = normalize(lert(normal0, normal1,
normal2,u,v));// surface normal
outrayDirec=reflect(inrayDirec,normal); // specular
outrayOrig=hitpoint+normalize(outrayDirec)*0.001f;
outrayState.x=STATE_TRAV_SETUP;
outrayState.y=INF;
outrayState.z=0;
outrayState.w=0;
}
else {
outrayState.x=STATE_DONT_TRACE;
}
91
}
else //if (inrayState.x == STATE_BACKGROUND)
{
outrayState.x=STATE_DONT_TRACE;
}
}
SurfaceDataGenerator.cg
uniform samplerRECT states;
uniform samplerRECT isectdatas;
uniform
uniform
uniform
uniform
uniform
samplerRECT
samplerRECT
samplerRECT
samplerRECT
samplerRECT
n0;
n1;
n2;
matAttribs;
matColors;
#include "common.cg"
float3 lert(float3 a, float3 b, float3 c, float u, float v)
{
return a + u*(b - a) + v*(c-a);
}
void surfData(in float2 rayIndex:TEXCOORD0,
out half3 surfnormal:COLOR0,
out half4 surfdata:COLOR1)
{
float4 state = texRECT(states, rayIndex).xyzw;
float4 isectdata = texRECT(isectdatas, rayIndex);
surfnormal=half3(0,0,0);
surfdata=half4(0,0,0,0);
if (state.x == STATE_SHADE)
{
float2 triangleIndex = isectdata.zw;
//Fetch normal
half3 normal0 = texRECT(n0, triangleIndex).xyz;
half3 normal1 = texRECT(n1, triangleIndex).xyz;
half3 normal2 = texRECT(n2, triangleIndex).xyz;
//Fetch material color
half4 matAttrib = texRECT(matAttribs, triangleIndex);
half4 matColor = texRECT(matColors, triangleIndex);
half smoothing=matAttrib.w;
float u=isectdata.x;
float v=isectdata.y;
if (smoothing<1.0) {
u=0.5; v=0.5;
}
half3 normal = normalize(lert(normal0, normal1, normal2,u,v));
surfnormal=normal;
surfdata.x=matColor.w;// Object ID
}
}
92
Integrator.cg
uniform samplerRECT directIlumination;
uniform samplerRECT indirectIlumination;
uniform samplerRECT specularReflections;
uniform int addReflections;
void integrator(in float2 rayIndex:TEXCOORD0,out float3 color:COLOR0)
{
float4 eyerays = texRECT(directIlumination, rayIndex);
float4 reflections = texRECT(specularReflections, rayIndex);
color=(eyerays.w)*(eyerays.xyz)+(reflections.xyz)*addReflections;
}
93
Bibliografía
[1]
J.T. Kajiya, “The rendering equation”. In Proceedings of the 13th annual
conference on Computer graphics and interactive techniques (1986), ACM Press,
pp. 143–150.
[2]
F. E. Nicodemus, J. C. Richmond, J. J. Hsia, I.W. Ginsberg, and T. Limperis,
“Geometric considerations and nomenclature for reflectance”. Monograpgh 161,
National Bureau of Standards (US), October 1977.
[3]
Tuner Whitted. “An improved illumination for shaded display”. Communications
of the ACM 23(6): 343 - 349 (June 1980).
[4]
John M. Snyder and Alan H. Barr, “Ray Tracing complex models containing
surface tessellations”. Computer Graphics (Proc. Siggraph ’87) 21 (4): 119-128
(July 1987).
[5]
Kryzsztof S. Ckilmansezewski and Thomas W. Sederberg. “Faster Ray Tracing
using adaptive grids”. IEEE Computer Graphics & Applications 17(1): 42-51
(January-February 1997).
[6]
T. J. Purcell. “Ray Tracing on a Stream Processor” PhD thesis, Stanford
University, 2004.
[7]
Frederik W. Jansen. “Data structures for Ray Tracing”. In Data structures for
Raster Graphics, edited by L.R.A. Kessener, F.J. Peters, y m. L. P. van Lierop,
pp. 57-73, Berlin: Springer-Verlag, 1985.
[8]
T. J. Purcell, C. Donner, M. Cammarano, H. W. Jensen, and P. Hanrahan.
“Photon mapping on programmable graphics hardware”. In Proceedings of the
ACM SIGGRAPH/Eurographics Symposium on Graphics Hardware.
[9]
Robert L. Cook, Thomas Porter, and Loren Carpenter. “Distributed Ray Tracing”.
Computer Graphics (Proc. SIGGRAPH ’84) 18(3): 137-45 (July 1984).
[10] Eric. P. Lafortune and Yves D. Willems. “Bidirectional Path Tracing”. In
Compugraphics’93, pp. 95-104, 1993.
[11] Eric Veach and Leonidas Guibas. “Bidirectional estimators for light transport”. In
Fifth Eurographics Workshop on Rendering, pp. 147-162, Eurographics, 1994.
[12] C. M. Goral, K. E. Torrance, D. P. Greenberg, and B. Battaile, “Modeling the
interaction of light between diffuse surfaces”. In Proceedings of the 11th annual
conference on Computer Graphics and Interactive Techniques (1984), pp. 213 –
222.
94
[13]
M. F. Cohen, S. E. Chen, J. R. Wallace, and D. P. Greenberg, “A progressive
refinement approach to fast Radiosity image generation”. In proceedings of the
15th annual conference on Computer graphics and interactive techniques (1988),
ACM Press, pp. 75–84.
[14] M. F. Cohen, and D. P. Greenberg, “The hemi-cube: a Radiosity solution for
complex environments”. In Proceedings of the 12th annual conference on
Computer graphics and interactive techniques (1985), ACM Press, pp. 31 – 40.
[15] H. W. Jensen, “Realistic Image Synthesis using Photon Mapping”. A K Peters.
ISBN 1568811470 (2001)
[16] I. Wald, J. G¨unther, and P. Slusallek, “Balancing Considered Harmful – Faster
Photon Mapping using the Voxel Volume Heuristic”. Computer Graphics Forum
22, 3 (2004), 595–603. (Proceedings of Eurographics).
[17] G. J. Ward, F. M. Rubinstein, and R. D. Clear, “A Ray Tracing solution for
diffuse interreflection”. In SIGGRAPH ’88: Proceedings of the 15th annual
conference on Computer graphics and interactive techniques (New York, NY,
USA, 1988), ACM Press, pp. 85–92.
[18] Niels Thrane, Lars Ole Simonsen. “A Comparison of Acceleration Structures for
GPU Assisted Ray Tracing”. Master’s thesis, University of Aarhus, August,
2005.
[19] Framebuffer Object Extension.
http://oss.sgi.com/projects/ogl-sample/registry/EXT/framebuffer_object.txt
[20] W. Stürzlinger, R. Bastos, “Interactive Rendering of Globally Illuminated Glossy
Scenes”. Eurographics Rendering Workshop ’97. ISBN 3-211-83001-4. (June
1997) 93-102.
[21] F. Lavignotte, M. Paulin, “Scalable Photon Splatting for Global illumination”.
Graphite 2003 (International Conference on Computer Graphics and Interactive
Techniques in Australasia and South East Asia). Melbourne, Australia. ACM
SIGGRAPH. 1-11
[22] Framebuffer Object Class
http://www.gpgpu.org/developer/
[23] G. Humphreys, M. Houston, Y.-R. Ng, R. Frank, S. Ahern, P. Kirchner, and J. T.
Klosowski, "Chromium: A Stream Processing Framework for Interactive
Graphics on Clusters," presented at SIGGRAPH, San Antonio, Texas, 2002
[24] Greg Humphreys, Ian Buck, Matthew Eldridge, and Pat Hanrahan, “Distributed
rendering for scalable displays”. In Supercomputing, 2000.
95
[25] F. Randima and Mark J. Kilgard, “The Cg Tutorial: The Definitive Guide to
Programmable
Real-Time
Graphics”.Addison-Wesley,
1996.
ISBN 0-321-19496-9
[26] James Arvo, David B. Kirk “Particle transport and image synthesis”. Computer
Graphics (Proc. SIGGRAPH ’90) 24(4): 63-66 (August 1990).
[27] T. R. Jozwowski, “Real Time Photon Mapping”. Master's thesis, Michigan
Technological University, 2002.
[28] Eric Veach and Leonidas J. Guibas, “Metropolis Light Transport”. In Proceedings
of SIGGRAPH 97, Computer Graphics Proceedings, Annual Conference Series,
edited by Turner Whitted, pp. 65-76, Reading, Ma: Addison Wesley, August
1997.
[29] The NVIDIA Cg Toolkit.
http://developer.nvidia.com/page/cg_main.html
[30] Bui-Tuong Phong, “Illumination for Computer Generated Pictures”. Comm.
ACM 18(6), 311-317.
[31] H. Gouraud, "Continuous shading of curved surfaces". IEEE Transactions on
Computers, 20(6):623–628, 1971.
96
Descargar