Transmittance

Welcome to the 14th tutorial in the XNA Shader Programming tutorial. Last time, we looked at using alpha maps and the alpha channel to make objects look transparent. Today we are going to dive a bit deeper in transparency, by implementing transmittance.

Transmittance

Things like glass, water, crystal, gass, air++ are things that absorb light as light-rays pass through them. In tutorial 13, we used alpha maps to make things look transparent and could create a transparent glass ball by just creating an alphamap with the color RGB(0.5,0.5,0.5) and we got ourself a transparent glassball. This approach works well in many cases, but it makes the transparency quite flat.

Objects in the real world, say a glass sphere, absorb/scatters light as the light-rays pass trough them. The longer the rays are inside the glass-ball, the more light will be scattered and absorbed before coming out. This is called Transmittance (wikipedia).

To calculate the transmittance (T), we can use Beer-Lamberts law (wikipedia) on the light-rays that pass trough the transmitter. Let's take a look at Beer-Lamberts law, and understand what we need calculate!

Beer-Lamberts law [1]:

where T is the transmittance, a' is the absorption factor, c is the consistency of the absorbing object and d is the thickness of the object.So, in order to use this, we need to find a', c and d.

Let's start with c. c controls how much light is absorbed when traveling through the transmitter. This value can just be set to any user specific number above 0.0.

Next is a'. We see a' in [1], and can use that to find a'[2]:

There T is the darkest transmitting color, and that is reached at distance.

Finally, we got the c-variable. This variable is set to the thickness of the object, at a given point, and probably is the hardest part to get right.

In this tutorial, we are calculating c quite correct for any non-complex objects( those that does not contain any holes or "arms" sticking out, like a sphere, simple glass figures and so on). The object used in this shader is a complex one, because we are going to get it right in a later tutorial, but let's start simple!

Now that we got all variables needed to calculate T at a given point, we can use this to see how much light is absorbed. This is done by multiplying the color of the light-ray (pixel behind the transmitter) with T!

So, how do we calculate the distance each light-ray is traveling through the transmitter? By using the depth buffer (wikipedia)!The Depth buffer (Z-Buffer) can be thought of as a grayscale image containing the scene is black and white, where the grayscale value indicates how far away an object is from the camera. So, if you take a look at the image on top of the article, we see a complex glass object. The scenes depth buffer looks something like this:

The depth buffer needs to have correct values in the Near and Far clipping plane of the projection matrix. Preferably having Near at the closest( to the view/camera) vertex of the transmitter, and Far at the most distance vertex of the transmitter.

So, knowing this, we can find the thickness of the transmitter from any angle of it, by using two depth buffer textures. By using culling, we can render the front faces of the transmitting object in one depth texture, and the backfaces of the transmitter into another depth texture. Taking the difference in these two gives us the distance on every pixel:

Backfaces in one depth texture

Frontfaces in another depth texture

Giving us a texture that can look like this. The grayscale value indicates how far a light-ray have to travel to get through the transmitter. White is far, and black is short/nothing. We are going to look at how to get the depth buffer and render it to a texture in the "Using the shader" section of the tutorial, but first, let's see how to implement the shaders.

Implementing the shader

In this tutorial, we got three techniques. One is just rendering the object with specular light (as seen in tutorial 3), the other one is rendering the scene to a depth texture, and the last shader is the post process shader that will add transmittance to the objects.

We don't want ALL objects rendered in a scene to defined as a transmitter. As this is a post process shader, we can first render the scene without the transmitters into one texture (a background texture), and then render the transmitters alone in a 2nd pass, and then combining these in the post process shader.Given this information, let's start with the specular light shader:

This shader is just a specular light shader, quite similar to the one we made in Tutorial 3 (using the exact same lighting-algoritm) so if this is new, you can see the explanation of this there.

Let's continue with our Depth Texture shader. This shader will only render the scene in grayscale, where the depth of each vertex/pixel is represented with a value between 0.0 and 1.0, where 1.0 is near the camera and 0.0 is at the far-plane of our shader (Pos.w).

So, to get the depth vertex, we simply take the Z-value of the given vertex, and devide it with it's W-value to make it range between the Near and Far clipping plane of our projection matrix.

First, we translate our vertex correctly to our world*view*projection matrix. Then we set the distance value to the correct depth-value. This is done by using the Position.z / Position.w, giving us the depth between the projections Near and Far plane.Now, it's the pixel shaders turn to show her magic! Oh, well, there ain't much magic in there. All we got left is to convert the Distance-value in OUT_DEPTH to a texture, so we can use this later:

Nothing new here, we just have to make sure we got the Z buffer enabled, and have it writable.

And now, the shader this tutorial is really about, the transmittance post process shader!First of all, we need the Background scene texture, the transmitting objects scene (the texture containing all of the objects that will be transmitters) and the two depth buffer textures!

Nothing new here, we are getting the pixels from different textures. depth1 and depth2 contains the r-channel the depth texture shader returned.Let's take a look at the formula for transmittance again and see what variables we need to get:

So far, we only got the c-variable and the distance-variable. Let's go and get the rest, shall we? ;)The d-variable, that will contain the thickness of the transmitter object, can easily be calculated using depth1 and depth2:

float distance = ((depth2-depth1));

Here we take the difference in depth2 and depth1, resulting in the objects thickness!

The T-variable used to find the absorption fact at the max in [2], contains the darkest color. This could be a color hard-coded in, or sent to the shader as a parameter. In this tutorial, we are using the value found in Color (the transmitter objects, rendered to a texture). This gives us the last variable we need to calculate the absorption factor a':

We calculate the transmittance value on each color-channel, using [1]. To avoid the T value of 0 (making the object completely black), we add 0.000001 to each channel.Once this is done, we can take the pixels that exists behind the transmitter (the light-rays traveling through the transmitter) and multiply it with T. As this is what we want with this shader, we return it from the pixel shader:

Phew, what a shader. It's not a very complex shader, but it's probably more advanced than any of the other shaders in my shader tutorial this far. So, in other words, if you don't understand it all, play around with parameters and the math, to see how each variable works. :)

We got two render targets, two stencil buffers and two textures that will contain our depth texture. We could use just one depth stencil buffer it we wanted but for simplicity, I'll do the same thing on both.

Also, in this shader, we are going to set the technique we want to enable in a shader using variables:

EffectTechnique environmentShader;
EffectTechnique depthMapShader;

Also, we need to set the distance factore used to calculate the absorbtion, and the consitency of our trasmitter:

float Du = 1.0f;
float C = 12.0f;

Now we are ready to start making the scene and using the shader. In LoadContent, we need to initate and create our render targets:

This creates two DepthStencilBuffers using our depth render targets and setting the depth format to Depth24Stencil8, witch sets our DepthBuffer-channel to 24bit and our stencil buffer channel to 8-bit. Here is a list of the different values we can set our DepthFormat to:

Depth15Stencil1

Depth16

Depth24

Depth24Stencil4

Depth24Stencil8

Depth24Stencil8Single

Depth32

Unknown

A 16-bit depth-buffer bit depth in which 15 bits are reserved for the depth channel and 1 bit is reserved for the stencil channel.A 16-bit depth-buffer bit depth.

A 32-bit depth-buffer bit depth that uses 24 bits for the depth channel.

A 32-bit depth-buffer bit depth that uses 24 bits for the depth channel and 4 bits for the stencil channel. A non-lockable format that contains 24 bits of depth (in a 24-bit floating-point format − 20E4) and 8 bits of stencil.

A 32-bit depth-buffer bit depth that uses 24 bits for the depth channel and 8 bits for the stencil channel.

A 32-bit depth-buffer bit depth.

Format is unknown.

We use two custom functions to create our depth buffers. CreateDepthStencil(RenderTarget2D target) creates the DepthStencilBuffer using the rendertarget passed in to it:

Now, we got all our textures containing what we want, and ready to go through our post process transmittance shader. In this tutorial, I only use a texture for the background scene, but this could be a rendertexture as well.As you probably noticed, we use a custom function to render our DepthMaps. This function is simply just a function that sets the render target to the one we pass in to it, renders the scene, restores the old render target state and returns the DepthBuffer as a texture:

Not much here, we set the shaders parameters, and render the scene with the shader enabled.

Here are a few other transmitters rendered with this shader:

NOTE:You might have noticed that I have not used effect.commitChanges(); in this code. If you are rendering many objects using this shader, you should add this code in the pass.Begin() part so the changed will get affected in the current pass, and not in the next pass. This should be done if you set any shader parameters inside the pass.

Thats if for now. In the next tutorial we will add reflection to the transmitter.Any feedback is very welcome!