Article : Realistic Soft Edged Water In XNA
Nice, But It’s Too Smooth
What we have now resembles more of a mirror with soft edges rather than water. What we need are some ripples. To do that we will need to perturb the coordinates used to read the reflection and refraction textures, and also scroll the planes texture coordinates. Some of you may be thinking, “Oh yeh, we can use parallax mapping to shift the coordinates based on height!”, and you would be correct, you could. However, there is a cheaper method, and that is using values directly from a texture called a DUDV map to offset the clip space coordinates. DUDV maps became popular in game programming during the DX8 era, and were used for refractions. This is what we will use it for, aswell as reflection perturbation.
Here is a photoshop tutorial for creating a DUDV map: http://blog.jailbreaksource.com/?p=762
I will provide one for use with this tutorial, download the following image:
Add this offset map to the content project by right clicking the Content project and choosing Add>Existing Item, and picking the downloaded texture.
In the PlanarWater class, add a new field to hold the offset map:
private Texture2D OffsetMap;
Change the PlanarWater constructor to take the texture, and set on the OffsetMap field:
public PlanarWater(GraphicsDevice device, Effect waterEffect, Texture2D offsetMap)
{
this.OffsetMap = offsetMap;
...
Next, in the PlanarWater Draw method, set the offset map on texture index 3;
device.Textures[3] = this.OffsetMap;
And set it back to null with the others further down:
device.Textures[3] = null;
In order to scroll the plane texture coordinates, we will need a time value. Change the PlanarWater Draw signature to:
public void Draw(GraphicsDevice device, Texture2D sceneDepth, Texture2D waterReflection, Texture2D sceneColor, Matrix waterViewProjection, Matrix view, Matrix projection, float time)
Then we can send it to the effect, with the other parameters:
this.WaterEffect.Parameters["time"].SetValue(time);
Now that we change these method parameters, we will need to update their calls in the Game class.
Change the PlanarWater constructor call to:
water = new PlanarWater(graphics.GraphicsDevice, Content.Load("Water"), Content.Load("WaterDUDV"));
And the draw call to:
this.water.Draw(graphics.GraphicsDevice, this.sceneDepth.GetTexture(), this.waterReflection.GetTexture(), this.sceneColor.GetTexture(), reflectionViewProjection, view, projection, (float)gameTime.TotalGameTime.TotalSeconds);
The last thing that needs updating is the water shader itself.
Add a new constant to the top:
float time;
Add new sampler for the offset map:
sampler2D OffsetSampler : register(s3);
We need to modify the texture coordinate according to time, scrolling it in a specific direction.
Change the vertex shader to this:
VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
VertexShaderOutput output;
float4 objectPos = float4(input.Position, 1);
float4 worldPosition = mul(objectPos, World);
float4 viewPosition = mul(worldPosition, View);
output.Position = mul(viewPosition, Projection);
const float2 scrollDirection = float2(0.0f, 1.0f);
float time = time / 1000.0f;
float2 scroll = scrollDirection * time * 10.1f;
output.TexCoord = input.TexCoord + scroll;
output.ClipPos = output.Position;
float4x4 waterWorldViewProjection = mul(World, WaterViewProjection);
output.ReflClipPos = mul(objectPos, waterWorldViewProjection);
output.ViewPos = viewPosition;
return output;
}
Since we are converting clip space to texture coordinate 3 times, add a couple of methods to help slim down the pixel shader:
float2 ClipSpaceToTexCoord(float4 input)
{
return float2(0.5f, -0.5f) * (input.xy / input.w) + 0.5f;
}
float2 ClipSpaceToTexCoordPerturb(float4 input, float2 perturb)
{
return float2(0.5f, -0.5f) * ((input.xy + perturb) / input.w) + 0.5f;
}
Finally, 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;
// Get the water plane depth
float waterViewZ = length(input.ViewPos) / 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;
float3 color = lerp(refraction, reflection, alpha);
return float4(color, 1);
}

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 ‘)’