Article : Realistic Soft Edged Water In XNA
Still a Mirror?
Now we have flowing ripples, but it is still very reflective, We are not able to enjoy the refraction effect caused by the ripples. This is where Fresnel comes it. No, it’s not a type of Perfume (although it probably should be), it is an equation that describes bahviour of light through refractive media. Put simply the amount of reflection or refraction depends on the angle we are looking at the surface This is due to the light reflecting off of the water surface into the camera, the angle and fraction indices of the two mediums (air and water in our case) determines if the light reflects or refracts.
We need to add this to our water shader. We will need 3 bits of information, the light direction, the normal of the surface (we will want to take it from the normal map), and a constant based on the refaction index of the water. In this case our light direction is the reflection of our eye direction, we will want this normal in world space, and plug it into the fresnel function. The result is the factor of reflection, 1 – the result is the factor of refraction. First we will need a normal map to indicate the normal of the surface (assuming we are not treating it as flat for fresnel, which you could).
Add this to your content project:
Add a new PlanarWater member for the normal map:
private Texture2D NormalMap;
Change the PlanarWater constructor to accept and store the normal map, the same as the offset map:
public PlanarWater(GraphicsDevice device, Effect waterEffect, Texture2D offsetMap, Texture2D normalMap)
{
this.OffsetMap = offsetMap;
this.NormalMap = normalMap;
Update the call to the constructor:
water = new PlanarWater(graphics.GraphicsDevice, Content.Load("Water"), Content.Load("WaterDUDV"), Content.Load("WaterNormal"));
Now change the PlanarWater draw method. We need the camera position and also need to set the texture, and its sampler states:
public void Draw(GraphicsDevice device, Texture2D sceneDepth, Texture2D waterReflection, Texture2D sceneColor, Matrix waterViewProjection, Matrix view, Matrix projection, float time, Vector3 cameraPosition, Vector3 lightDirection)
{
device.Textures[4] = this.NormalMap;
…
this.WaterEffect.Parameters["CameraPosition"].SetValue(cameraPosition);
…
device.Textures[4] = null;
We can now update the shader to read and use this normal. First we need to add the camera position constant, and a sampler:
float3 CameraPosition; sampler2D NormalSampler : register(s4);
Next we need to add WorldPos to the vertex output struct:
struct VertexShaderOutput
{
float4 Position : POSITION0;
float2 TexCoord : TEXCOORD0;
float4 ClipPos : TEXCOORD1;
float4 ReflClipPos : TEXCOORD2;
float3 ViewPos : TEXCOORD3;
float3 WorldPos : TEXCOORD4;
};
And set it at the end of the vertex shader:
output.WorldPos = worldPosition;
Since a full Fresnel function can be costly, add this approximation function. We can modify this later to take depth into account:
float FresnelApproximation(float3 lightDir, float3 normal, float offset)
{
float3 reflectedViewDir = -reflect(lightDir, normal);
float viewDotNorm = abs(dot(lightDir, normal));
float fresnelFactor = 1 - pow(viewDotNorm, 0.5);
return saturate(fresnelFactor + offset);
}
Now we can read the normal map and use it with the above function to get a reflection factor for our lerp.
Change the pixel shader to the following:
float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
float2 screenTexCoords = ClipSpaceToTexCoord(input.ClipPos);
float2 dudv = 2 * tex2D(OffsetSampler, input.TexCoord * 8).rg - 1;
float2 refractCoords = ClipSpaceToTexCoordPerturb(input.ClipPos, dudv);
// Get the terrain depth
float sceneViewZ = tex2D(DepthSampler, screenTexCoords).r - 0.001f;
float viewLen = length(input.ViewPos);
float3 normal = 2 * tex2D(NormalSampler, input.TexCoord * 8).rgb - 1;
float3 normalWorld = -normalize((float3(0, 1, 0) * normal.z) + (normal.x * float3(0, 0, 1) + normal.y * float3(-1, 0, 0)));
// Get camera direction from view matrix
float3 viewDir = normalize(input.WorldPos - CameraPosition);
// Get the reflection factor so we can use it in our lerp
float reflectionFactor = FresnelApproximation(viewDir, normalWorld, 0.3f);
// Get the water plane depth
float waterViewZ = viewLen / FarPlane;
// Get the range of depth from waterplane to terrain
float depthRange = (sceneViewZ - waterViewZ);
const float shoreFalloff = 2.0f;
const float shoreScale = 5.0f;
// calculate a transparency value using a power function
float alpha = saturate(max(pow(depthRange, shoreFalloff) * FarPlane * shoreScale, 0));
float3 refraction = tex2D(RefractionSampler, refractCoords).rgb;
float2 reflectTexCoords = ClipSpaceToTexCoordPerturb(input.ReflClipPos, dudv);
float3 reflection = tex2D(ReflectionSampler, reflectTexCoords) * 0.5f;
// color based on how much refraction vs reflection
float3 color = lerp(refraction, reflection, reflectionFactor);
// lerp back to refraction for the shoreline
color = lerp(refraction, color, alpha);
return float4(color, 1);
}
It’s finally looking like water, but we are not done. After all, the title says realistic water.

May 6th, 2010 - 20:22
Finally, a subject on just water and not necessarily ‘adding’ water for Xna.
I am going to try this out as my goal is to just do water itself in Xna, for now.
May 20th, 2010 - 17:52
This is very similar to the technique I use to render water as a post processing pass. In my implementation I render geometry into the stencil buffer and output linear view space depth. Then run a full screen quad over the results of that to produce the water in the back-buffer with stencil rejection enabled.
July 13th, 2010 - 00:52
Hey,
is there a way you could re-upload the sample and images for this tutorial? Would be really nice.
BTW this is just the tutorial i’ve been looking for for months … thank you soooo much
July 14th, 2010 - 17:26
Existence – Fixed the images and sample download.
July 21st, 2010 - 17:32
Hey Sarge,
The pixel shader broke when I ran this under Xna 3.0. Band-aid solution: saved the pixel shader to a static variable before executing begin command with the Effect, then set it back once it went to null. Is there a quick fix to get around the ravages of Microsoft progress?
Best,
Dave
July 22nd, 2010 - 13:06
Nota bene: my previous fix didn’t really work. If you receive the debugging message about not having a valid vertex or pixel shader, then you will probably have to set the pixel shader device version to 2 in the .fx file, instead of 3.
July 22nd, 2010 - 14:47
This sample doesnt seem to work for me it causes problems with the for loops:
for (int x = 0; x < Tesselation-1; x++)
it underlines the ‘;’ after -1 and the ‘)’