Ambient Volume

Introducción

Mi intención era la de conseguir una componente de ambiente en cada píxel de la escena en función de la posición de éste y de los objetos que lo rodean. Aparte de esto también quería conseguirlo mediante una matemática sencilla, que aunque no me reportara calidad máxima, me produjera unos resultados aceptables tanto en visualización como en computación.

Descripción

Mi idea de componente ambiente es la de un color en un píxel determinado producido por la luz rebotada en los diferentes objetos de esa misma escena. Se excluye de esta idea la luz directa de la luz al píxel pues esta luz será añadida posteriormente en la ecuación de iluminación.

En el diagrama siguiente podemos observar el concepto de luz rebotada en un píxel.

Diagrama de rebotes
Amb1.jpg

Como hemos comentado anteriormente, podemos observar como la luz rebotada es la que lleva el color de la componente ambiente hasta el píxel, por otro lado, también vemos como la luz directa en estos momentos no nos interesa.

Creación de una Ambient Volume

Llegados a este punto, podemos decir que esta componente ambiente es el resultado de renderizar la escena de forma normal pero colocando nuestra cámara en el píxel designado y con orientación proporcionada por la normal de éste.
Como podemos suponer, este método de renderizado píxel a píxel tardaría mucho y no podría ser llevado a cabo en tiempo real. Debido a eso debemos simplificar.

A partir de ahora, generaremos una estructura intermedia para almacenar dicha componente y que nos permita recuperar una aproximación de esta componente de forma rápida y simple.
Yo elegí la representación de 6 vectores en 3 ejes, los cuales almacenan los colores provenientes de 6 direcciones. Podemos pensar en una esfera en la que en cada uno de sus puntos cardinales contiene un color, y en los puntos donde no hay color, se interpolan los colores más cercanos.

Si miramos el diagrama siguiente podemos hacernos una idea rápida:

Ambient Volume
Amb2.jpg

En el diagrama anterior podemos observar como cada uno de los vectores que forman los ejes contienen un color. El proceso de generación de esta esfera que yo llamo Ambient Volume, pasa por rellenar los seis colores que son percibidos desde los seis vectores.
Antes de explicar cómo conseguir estos seis colores, me gustaría hablar de la relación que tendrá o tendrán estas esferas con los objetos de la escena.

Cada una de estas esferas puede almacenar la componente ambiente en un punto determinado, pero ¿qué puntos vamos a elegir? ¿Qué objetos se beneficiaran de esta componente ambiente?

Concretamente yo utilizo estas esferas de tal forma que los objetos dinámicos de la escena, es decir, los que se pueden mover, puedan recibir esa componente ambiente y así adaptarse mejor al entorno que les rodea. Por tanto, es necesario de hablar de otro sistema que tengo implementado que es el View Manager.

El View Manager es una objeto que me permite partir el espacio de la escena en una matriz tridimensional de celdas y decirme en todo momento en que celda estoy. Cada una de esas celdas contiene una esfera de Ambient Volume generada para el centro de esa celda.

Si miramos a continuación, podremos observar de qué estoy hablando:

View Manager
Amb3.jpg

En la imagen anterior podemos observar una matriz de 1x2x2 celdas, en la que 3 de ellas contienen información de Ambient Volume. La cuarta celda no contiene nada porque he querido representar la habilidad del ViewManager de poder tener celdas con la información ‘descargada’ de memoria para no ocupar tanto. Esta memoria es cargada cuando alguien se acerca a esa celda.

Como comentaba antes, cada una de estas esferas estará relacionada con una celda, gracias a esto podremos saber que Ambient Volume utilizar según nuestra posición en la matriz del View Manager.

Para poder generar los colores de los 6 vectores será necesario obtener un render en la dirección de ese vector. Una vez obtenido el render y este lo tengamos depositado en un Render Target, será necesario hacer la media de los colores de todos los píxeles de ese Render Target.

Para realizar la media del color de los píxeles, será necesario bloquear la superficie para poder leerla y así poder calcular el color final.
A continuación se muestra un código para poder pasar la superficie del Render Target de memoria de video a memoria de sistema y allí poder bloquearla. Una vez bloqueada ya podemos leer y calcular.

//-------------------------------------------------------------------------------------------------------
/// \brief Carga una textura desde un Render Target
/// \param[in] inPtr Puntero al device utilizado
/// \param[in] inTex Puntero a la textura de entrada
//-------------------------------------------------------------------------------------------------------
void CTexture::LoadTextureFromRT(IDirect3DDevice9* inPtr, IDirect3DTexture9* inTex)
{
    HRESULT hr;
    D3DSURFACE_DESC Desc;
    IDirect3DSurface9 *pSurfOut, *pSurfIn;
 
    if(m_tTexture == NULL)
    {
        inTex->GetLevelDesc(0,&Desc);
        m_iWidth = Desc.Width;
        m_iHeight = Desc.Height;
 
        D3DXCreateTexture(inPtr,m_iWidth,m_iHeight,1,0,Desc.Format,D3DPOOL_SYSTEMMEM,&m_tTexture);
 
        hr = inTex->GetSurfaceLevel(0, &pSurfOut);
 
        hr = m_tTexture->GetSurfaceLevel(0, &pSurfIn);
 
        inPtr->GetRenderTargetData(pSurfOut,pSurfIn);
 
        pSurfIn->Release();
        pSurfOut->Release();
 
    }
}
//-------------------------------------------------------------------------------------------------------
/// \brief Hace un Lock() a la textura para poder ser accedida a nivel de pixel
/// \ret bool Cierto si se ha hecho el lock, falso de lo contrario
//-------------------------------------------------------------------------------------------------------
bool CTexture::Lock()
{
    HRESULT hr;
    IDirect3DSurface9 *pSurf;
 
    m_pLockedRect = new D3DLOCKED_RECT;
    m_tTexture->GetSurfaceLevel(0, &pSurf);
    hr = pSurf->LockRect(m_pLockedRect, NULL, D3DLOCK_READONLY);
    pSurf->Release();
 
    return (hr == S_OK);
}
//-------------------------------------------------------------------------------------------------------
/// \brief Devuelve el color de un píxel especificado
/// \ret D3DXCOLOR Color del píxel
//-------------------------------------------------------------------------------------------------------
D3DXCOLOR CTexture::GetTexPixelColor(unsigned int inX, unsigned int inY)
{
    IDirect3DSurface9 *pSurf;
 
    const BYTE* pBits = (const BYTE*)m_pLockedRect->pBits;
 
   unsigned int nBytesPerPixel = 4;
   const unsigned int* pPixel = (unsigned int*)(pBits + (inY * m_pLockedRect->Pitch) + (inX * nBytesPerPixel));
   unsigned int dwPixel = *pPixel;
 
    return (dwPixel);
}
//-------------------------------------------------------------------------------------------------------
/// \brief Hace un Unlock() a la textura
/// \ret bool Cierto si se ha hecho el Unlock, falso de lo contrario
//-------------------------------------------------------------------------------------------------------
bool CTexture::Unlock()
{
    HRESULT hr;
    IDirect3DSurface9 *pSurf;
 
    m_tTexture->GetSurfaceLevel(0, &pSurf);
    hr = pSurf->UnlockRect();
    pSurf->Release();
 
    delete m_pLockedRect;
    m_pLockedRect = NULL;
 
    return (hr == S_OK);
}

En el código anterior, solo será necesario ejecutar la función para cargar la superficie de un Render Target a nuestra nueva textura, luego hacer un Lock para poder bloquear, y ya podemos leer todos los píxeles como si del acceso a una array 2D se tratara.

Con eso podemos obtener el color medio que nos proporcionan todos los píxeles de ese render.

Cierto es de que este proceso lo debemos repetir 6 veces, uno para cada dirección, es decir, uno para cada uno de los vectores de nuestra esfera.

Se me ha dado el caso que en ciertas celdas, y dependiendo de los objetos que haya en ellas y su posición, los colores de la Ambient Volume no son del todo representativos de esa celda. Por ello yo realizo varios Ambients Volumes esparcidos por cada celda y cuando los tengo todos, hago la media de todos los Ambient Volumes para generar uno solo por celda.

Fijémonos en lo siguiente:

Puntos de creación
Amb4.jpg

Inicialmente como he comentado, realizaba solo un Ambient Volume centrado en la celda que lo contenía. Posteriormente hice más Ambient Volumes tal y como se indica en la figura de la derecha, es decir, incrementos de posiciones a lo largo de los ejes X, Y, Z de la propia celda. Una vez obtenidas todas las Ambient Volumes, hago la media de todas ellas.

Interpolación de color

Llegados a este punto en el que ya tenemos una Ambient Volume por cada celda, primero deberíamos abordar el cómo obtener a partir de la Ambient Volume el color que le corresponde a un píxel según su normal, y posteriormente abordar qué Ambient Volume elegir en una posición 3D dada.

Para entender el concepto de obtener la componente ambiente para un píxel, deberemos entender que para cada una de las celdas de nuestra escena, tenemos una Ambient Volume que almacena esa componente, y para recuperar esa componente ambiente para un píxel, deberemos mirar que componente se le asignará a ese píxel según su normal.

A partir de la normal del píxel y de la Ambient Volume podremos calcular que componente ambiente necesita ese píxel:

Diagrama de colores
Amb5.jpg

En el diagrama anterior tenemos una Ambient Volume con sus seis colores definidos, también podemos observar un vector verde, que en nuestro caso, será el vector que representara la normal del píxel de la cual queremos recuperar su componente ambiente.

Para eso, primero de todo será necesario identificar en que cuadrante está situada esa normal. Existen 8 cuadrantes, y concretamente en este ejemplo, la normal esta en el cuadrante (+X,+Y,-Z). Pero en vez de seleccionar un cuadrante y calcular allí el color resultante, es más sencillo traer la normal al cuadrante (+X,+Y,+Z) y utilizar los colores del cuadrante en el que originalmente esta la normal.

Para realizar este proceso, simplemente cogemos el signo de las componentes de la normal y dependiendo de este signo, elegimos los colores adecuados.

Una vez elegidos los colores, solo nos queda hacer el valor absoluto a las componentes de la normal para traerla al primer cuadrante.

Reducción a un cuadrante
Amb6.jpg

Como podemos observar en la imagen anterior, a partir de ahora ya podemos trabajar en el primer cuadrante con los colores adecuados para esa normal.
Una vez llegados a este punto, viene el turno de la interpolación, es decir, a partir de la posición de la normal, determinar qué color obtiene según las influencias de los tres colores que podemos ver en la imagen anterior.
Para realizar esta interpolación, he decidido hacer una interpolación en cuatro fases.

  1. En la primera fase se calculan los colores necesarios para las siguientes fases.
  2. En la segunda fase se interpolan los colores en el eje de las X
  3. En la tercera fase y con la mitad de cálculos, se interpolan los colores de la anterior fase en el eje de las Y
  4. En la cuarta fase y con la mitad anterior de cálculos, se interpolan los colores resultantes de la fase anterior, en el eje de las Z

Para ver estas fases, será mejor mostrarlo mediante unos diagramas:

Fase 1
Amb7.jpg
Fase 2
Amb8.jpg
Fase 3
Amb9.jpg
Fase 4
Amb10.jpg
  • En la primera fase, a partir de los tres colores que ya tenemos, hemos de generar los colores restantes. Para generar estos colores lo haremos de la siguiente forma:

ColorXY: Aplicaremos la función max(…) entre el ColorX y el colorY, esto nos dará el máximo de color por canal de estos dos colores.
ColorZX: Aplicaremos la función max(…) entre el ColorZ y el colorX, esto nos dará el máximo de color por canal de estos dos colores.
ColorXYZ: Para este color aplicaremos la función max(…) entr el ColorXY y el ColorZ.
Se aplica la función max(…) porque si se sumaran los colores saldrían valores demasiado elevados. También se podría realizar una media entre los dos colores, pero hay que tener en cuenta que la media entre 1.0f y 0.0f es 0.5f, es decir, dependiendo de los valores, pueden dar resultados no tan deseados.

  • En la segunda fase, una vez tenemos todos los colores, es necesario interpolar esos colores mediante el eje X. Para realizar esto, emparejaremos los colores según el eje X, así, el ColorCentral se empareja con el ColorX, el ColorZX se empareja con el ColorZ, etc. Todas estas interpolaciones se realizaran mediante el valor X de la normal para la que estamos calculando el color.
  • La tercera fase se realiza lo mismo que en la anterior pero utilizando los resultados de esas interpolaciones. Esta vez interpolaremos en el eje Y y utilizaremos la componente Y de la normal usada.
  • La cuarta fase nos da el resultado final, se cogen los resultados de la fase 3 y se interpola mediante la componente Z de la normal.

En las fases anteriores podemos observar como llevamos a cabo la interpolación de colores. Siempre que se hace una interpolación entre dos valores, debe haber un tercer valor indicando cuanta interpolación debemos aplicar. En nuestro caso, si interpolamos en el eje de la X, será la componente X de la normal el que nos indique cuanto debemos interpolar. Si interpolamos en el eje de la Y será la componente Y de la normal y lo mismo para el eje Z.

Cabe destacar que esta interpolación es una simple aproximación, pues no entramos en cálculos más complejos pero que ofrece unos resultados totalmente viables.

Si repetimos este proceso para cada uno de los píxeles del objeto, obtendremos una componente ambiente para la superficie de este objeto.

Interpolación de Ambient Volumes

Hemos hablado de la interpolación de una Ambient Volume en una celda, pero nunca hemos hablado de qué pasa si el objeto lo estamos moviendo y pasa de una celda a otra, pues dos celdas contiguas, aunque parecidos, pueden tener diferentes colores en sus Ambient Volume.

Esto nos daría como resultado un cambio brusco de color ambiente que nuestro ojo podría notar. Para paliar este efecto, aplicaremos una técnica parecida a la anterior.

Para conseguir la componente ambiente de una Ambient Volume hemos reducido el problema a un cuadrante y hemos interpolado. Para una interpolación a lo largo de las celdas usaremos una metodología parecida, es decir, a partir de la posición 3D de nuestro punto, elegiremos las celdas más cercanas, que precisamente serán las que forman el cuadrante en el que nos situamos dentro de la celda.

Una vez elegidas las 8 celdas que forman ese cuadrante, interpolaremos de la misma forma que hemos hecho con los colores de la Ambient Volume, pero esta vez, en vez de interpolar colores unitarios, interpolaremos entre Ambient Volumes.

De esa manera obtendremos una transición continua viajemos en la dirección que viajemos.

Renders

No existe ninguna luz dinamica en los siguientes renders.

Render 1
Amb11.jpg
Render 2
Amb12.jpg
Render 3
Amb13.jpg

Conclusiones

Como conclusiones preliminares puedo decir que el efecto da bastante buen resultado teniendo en cuenta la sencillez de las matemáticas utilizadas en el algoritmo.

Es un sistema que permite modificar en tiempo real los colores de los Ambient Volumes de forma sencilla e intuitiva, cosa que nos podría permitir en un futuro aplicar migraciones de colores entre Ambient Volumes de diferentes objetos dinámicos para que entre ellos se aplicaran dinámicamente color ambiente.

También el método permite no solo guardar un color por eje sino también poder almacenar mas colores en los ejes compartidos como XY, XZ o YZ. Pero hay que tener en cuenta que esto aumenta la cantidad de registros utilizados de la tarjeta grafica en lo shaders.

Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License