Thursday, April 30, 2009

Parallax Occlusion Mapping and Glass HLSL

Ok so I just setup my blog here to post my process on my graphic demo. I doing this to improve my 3D graphic skill as well as work on my portfolio to find an internship.

Here is my current graphic demo. It's written using DirectX 9.0c and HLSL.

1) Overview: So in this demo I implement these graphic techniques:

* Cube mapping to create the sky box.

* Normal mapping for the floor and those columns.

* Reflection and refraction on water. I first build reflection and refraction maps, then project them onto the water plane. Then at each water plane pixel, I blend refraction and reflection associated texels together, along with material and lighting to produce the final pixel colors for the water.

All of these techniques I learned from Frank D. Luna's book called Introduction to 3D Game Programming with DirectX 9.0c A Shader Approach.

So the demo looks good but I still think it doesn't have enough cool techniques. So I implement another popular effect: the glass effect

2) Glass shader:

So voila, a glass teapot ! It has both reflection and refraction properties just like the water.The violet color is due to I try to implement some rainbow color as the result of refracted white light. So here is the process:

- For reflection: we look up the reflection vector and use it to sample from the environment map.

- For refraction: we'll use Snell's refraction law:

n1 * sin (theta1) = n2 * sin (theta2)

with theta1 is the incoming light angle and theta2 is the refracted angle. n1 and n2 are just refractive indices of two media. After compute the refraction vector, we also use it to sample from the environment map.

- Finall we combine reflection and refraction with some weight. Here is the HLSL code:


// Look up the reflection
float3 reflVec = reflect(-toEyeW, normalW);
float4 reflection = texCUBE(EnvMapS, reflVec.xyz);

// We'll use Snell's refraction law
float cosine = dot(toEyeW, normalW);
float sine = sqrt(1 - cosine * cosine);

float sine2 = saturate(gIndexOfRefractionRatio * sine);
float cosine2 = sqrt(1 - sine2 * sine2);

float3 x = -normalW;
float3 y = normalize(cross(cross(toEyeW, normalW), normalW));

// Refraction
float3 refrVec = x * cosine2 + y * sine2;
float4 refraction = texCUBE(EnvMapS, refrVec.xyz);

float4 rainbow = tex1D(RainbowS, pow(cosine, gRainbowSpread));

float4 rain = gRainbowScale * rainbow * gBaseColor;
float4 refl = gReflectionScale * reflection;
float4 refr = gRefractionScale * refraction * gBaseColor;

return sine * refl + (1 - sine2) * refr + sine2 * rain + gLight.ambient/10.0f;


The effect is based on ATI glass shader. The downside of this is the teapot just reflects and refracts the environment map only. So anything that is not of the environment ( water plane, columns, etc. ) will not be visible through the glass. I'm currently searching for a solution for this so if you know any please suggest :).

The latest technique that I implement is Parallax Occlusion Mapping ( or POM ).

3) Parallax Occlusion Mapping (POM):



POM is used to replace normal mapping technique in order to achieve many features such as:

- Display motion parallax

- Calculate occlusion and filters visibility samples for soft self-shadowing.

- Use flexible lighting model

This technique is explained in detail in Practical Parallax Occlusion Mapping for Highly Detailed Surface Rendering by Natalya Tatarchuk of ATI. I personally don't fully grab all the technical details yet so I'm still reading on it. The implementation is learned from the POM sample from DirectX :D. However, this is what I understand so far:

- First we encode surface displacement information into a scalar height map. This information often stored in the alpha channel of the normal map.

- Then effect of motion parallax for a surface can be computed by applying this height map and offsetting each pixel in the height map using geometry normal and the view vector.

So as you can see from the picture above, the view ray from the eye to the pixel on the polygonal surface represent what we would have see if we use normal mapping. However, in the actual geometry, we would have see the pixel correspond to t-off instead. So how do we fix that ?


- We have to compute the intersection point of the view ray and the height-field of the surface. We do this by approximate the height field (seen as the light green curve) as a piecewise linear curve (seen here as dark green curve), intersecting it withe the given ray (the view ray) for each linear section.

- We start by tracing from the input sample coordinates t-0 along the computed parallax offset vector P. We perform linear search for the intersection along the parallax offset vector. Each piecewise segment is of step size sigma. To test each segment, we simply use the height displacement value from each end point to see if they are above the current horizon level.

- Once the segment is found, we compute the intersection between it and the view ray. The intersection of the height field yields the point on the extruded surface that would be visible to the view. From this point, we can trace back to t-off and compute the offset vector for the texture.

Here is the video I made demonstrates POM technique:



Phew that's a lot of technical details. If you want a simpler explanation I suggest you read Jason Zink's A closer look at parallax occlusion mapping at GameDev.net. Here is the link :

3 comments:

  1. This is amazing! I'd love to see the source.

    ReplyDelete
  2. Hi there,

    I've implemented the shader from DX9 SDK example. You can download the SDK and take a look at the code. Let me know if you need more assist. Thanks.

    ReplyDelete
  3. Hum really interested in ...
    But trying to implement it in FX composer from Nvidia ..
    Some errors of compiling it because of the declarations difference i think ..

    Possible for you to help me implemented this example in FX composer ?

    Thanks :)

    ReplyDelete