Article : Realistic Soft Edged Water In XNA
by X-Tatic
Still water bodies are a powerful distinguishing factor for identifying an outdoor scene. The nature of this water, such as a lake, makes it challenging to render efficiently while still maintaining a believable sense of realism. There are several factors that need to be considered when rendering such a water body and in this article I will cover each one while presenting the technical details.
There is a sample download link at the end of the article, however I strongly suggest you follow the article in order to understand the progression of the effect.
Introduction
Many games seem to now have a higher occurrence of outdoor scenes, or even elements of outdoor scenes. Many outdoor elements are significantly difficult to implement, such as large terrain, believe vegetation, or atmospheric scattering. Fortunately one such element can be implemented easily with spectacular results. Here I present an implementation to represent calm and slow flowing flat water bodies, such as lakes. The technique will implement varying intensity and color over depth, as well as fading intensity of color, transparency, and ripple at the shoreline. Lighting will consist of phong specular and a Fresnel factor for varying transparency between reflection and refraction.
Rendering a Scene
One of the best ways to contrast the rendering of your water, showing reflection and refraction is with terrain. A terrain implementation is beyond the scope of this article, however you can find multiple articles about this subject throughout the web. For this article and sample, I will start with the XNA Creators Club Sample titled “Lens Flare”. You can download it from here: http://creators.xna.com/downloads/?id=138 (you may need to upgrade the solution to XNA 3.1).
The first thing I am going to do with this sample is to re-factor the terrain drawing into its own method. The reason for this is that we will need to draw it more than once a frame, and do not want a lot of redundant code.
Move the terrain drawing code into a method like this:
protected void DrawTerrain(ref Matrix view, ref Matrix projection)
{
// Draw the terrain.
GraphicsDevice.RenderState.CullMode = CullMode.None;
foreach (ModelMesh mesh in terrain.Meshes)
{
foreach (BasicEffect effect in mesh.Effects)
{
effect.World = Matrix.Identity;
effect.View = view;
effect.Projection = projection;
effect.LightingEnabled = true;
effect.DiffuseColor = new Vector3(1f);
effect.AmbientLightColor = new Vector3(0.5f);
effect.DirectionalLight0.Enabled = true;
effect.DirectionalLight0.DiffuseColor = Vector3.One;
effect.DirectionalLight0.Direction = lensFlare.LightDirection;
effect.FogEnabled = true;
effect.FogStart = 200;
effect.FogEnd = 500;
effect.FogColor = Color.CornflowerBlue.ToVector3();
}
mesh.Draw();
}
}
Then call the draw where it used to be in the Games Draw method:
this.DrawTerrain(ref view, ref projection);
Drawing the Water Plane
To get something representing the water surface, we will draw some geometry covering the same area as the terrain; since we are doing this step by step we will draw this plane geometry white for now. What we will need is: Vertex/Index Buffers for the planar geometry and an Effect to draw it with. For convenience, we will place this in its own class called PlanarWater.
Right click in the project in the solution explorer window and choose Add>New Item, the pick Class, calling it PlanarWater.cs.
Add the following code to the file:
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
namespace LensFlare
{
public class PlanarWater
{
private VertexBuffer PlaneVB;
private VertexDeclaration PlaneVD;
private Effect WaterEffect;
///
/// A tesselation factor to balance the texture read cache.
///
private const int Tesselation = 10;
private const int TesselationN1 = Tesselation - 1;
///
/// Static world matrix that will match our water grid up to terrain
///
private static Matrix World;
static PlanarWater()
{
PlanarWater.World = Matrix.CreateScale(600f, 1f, 600f) * Matrix.CreateTranslation(25f, 23f, 25f);
}
public PlanarWater(GraphicsDevice device, Effect waterEffect)
{
const int vertexCount = 6 * TesselationN1 * TesselationN1;
this.WaterEffect = waterEffect;
this.PlaneVB = new VertexBuffer(device, typeof(VertexPositionTexture), vertexCount, BufferUsage.WriteOnly);
this.PlaneVD = new VertexDeclaration(device, VertexPositionTexture.VertexElements);
VertexPositionTexture[] vertices = new VertexPositionTexture[vertexCount];
// Space between grid cells
const float delta = 1f/Tesselation;
// Offset to position around origin in object space
const float halfLength = 0.5f;
// current index into vertices
int index = 0;
for (int x = 0; x < Tesselation-1; x++)
{
// positions for current and next along X
float column1 = (delta * x) - halfLength;
float column2 = column1 + delta;
// texcoords for current and next along U
float uCoord1 = column1 + 0.5f;
float uCoord2 = column2 + 0.5f;
for (int z = 0; z < Tesselation-1; z++)
{
// positions for current and next along Z
float row1 = (delta * z) - halfLength;
float row2 = row1 + delta;
// texcoords for current and next along V
float vCoord1 = row1 + 0.5f;
float vCoord2 = row2 + 0.5f;
/// Triangle 1
{
vertices[index].TextureCoordinate = new Vector2(uCoord1, vCoord1);
vertices[index++].Position = new Vector3(column1, 0, row1);
vertices[index].TextureCoordinate = new Vector2(uCoord2, vCoord1);
vertices[index++].Position = new Vector3(column2, 0, row1);
vertices[index].TextureCoordinate = new Vector2(uCoord2, vCoord2);
vertices[index++].Position = new Vector3(column2, 0, row2);
}
/// Triangle 2
{
vertices[index].TextureCoordinate = new Vector2(uCoord2, vCoord2);
vertices[index++].Position = new Vector3(column2, 0, row2);
vertices[index].TextureCoordinate = new Vector2(uCoord1, vCoord2);
vertices[index++].Position = new Vector3(column1, 0, row2);
vertices[index].TextureCoordinate = new Vector2(uCoord1, vCoord1);
vertices[index++].Position = new Vector3(column1, 0, row1);
}
}
}
this.PlaneVB.SetData(vertices);
}
public void Draw(GraphicsDevice device, Matrix view, Matrix projection)
{
device.VertexDeclaration = this.PlaneVD;
device.Vertices[0].SetSource(this.PlaneVB, 0, VertexPositionTexture.SizeInBytes);
device.Indices = null;
RenderState renderState = device.RenderState;
renderState.DepthBufferEnable = true;
renderState.AlphaTestEnable = false;
renderState.AlphaBlendEnable = false;
this.WaterEffect.Begin();
this.WaterEffect.Parameters["World"].SetValue(PlanarWater.World);
this.WaterEffect.Parameters["View"].SetValue(view);
this.WaterEffect.Parameters["Projection"].SetValue(projection);
this.WaterEffect.Techniques[0].Passes[0].Begin();
const int primitiveCount = 2 * (TesselationN1*TesselationN1);
device.DrawPrimitives(PrimitiveType.TriangleList, 0, primitiveCount);
this.WaterEffect.Techniques[0].Passes[0].End();
this.WaterEffect.End();
}
}
}
This creates a grid mesh of type triangle list, and draws it in the world so that it intersects the terrain. However, first it needs an effect to draw with.
Right click your content project and choose Add>New Item. Choose Effect File, and call it Water.fx.
Add the following HLSL code to the .fx file:
float4x4 World;
float4x4 View;
float4x4 Projection;
struct VertexShaderInput
{
float3 Position : POSITION0;
float2 TexCoord : TEXCOORD0;
};
struct VertexShaderOutput
{
float4 Position : POSITION0;
float2 TexCoord : TEXCOORD0;
};
VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
VertexShaderOutput output;
float4 worldPosition = mul(float4(input.Position, 1), World);
float4 viewPosition = mul(worldPosition, View);
output.Position = mul(viewPosition, Projection);
output.TexCoord = input.TexCoord;
return output;
}
float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
return float4(input.TexCoord.x, input.TexCoord.y, 0, 1);
}
technique Technique1
{
pass Pass1
{
VertexShader = compile vs_2_0 VertexShaderFunction();
PixelShader = compile ps_2_0 PixelShaderFunction();
}
}
Now we can draw the planar water mesh, showing its texture coordinates. First we need to create an instance of PlanarWater and pass it the effect. Add a new field:
private PlanarWater water;
Inside the LoadContent method, initialize the water field:
water = new PlanarWater(graphics.GraphicsDevice, Content.Load("Water"));
Now finally, call draw on the water right after the terrain is drawn, inside the Games Draw method:
this.water.Draw(graphics.GraphicsDevice, view, projection);
Now you should have something like this:

