Article : Realistic Soft Edged Water In XNA
It Doesn’t Look Like Water To Me
So we have a value with which we can blend to the shoreline, but it is looking nothing like water. Next we will add the two major features that make the water look realistic, reflection and refraction. Reflection is the more complex of the two, and involves rendering the terrain from a virtual camera position, which is a mirrored position of the real camera position around the water plane. The refraction is rather simple and is just a plain color render of the terrain from the regular camera position.
Lets start with reflection. Add a new render target to your game called waterReflection.
private RenderTarget2D waterReflection;
Also initialize it in the LoadContent method, just like the sceneDepth target, but with SurfaceFormat.Color instead.
waterReflection = new RenderTarget2D(GraphicsDevice, width, height, 1, SurfaceFormat.Color, RenderTargetUsage.DiscardContents);
Now we are ready to draw the terrain to this reflection render target. What we need to do is generate a view and projection matrix for our virtual camera position, which is mirrored around the water plane. We also need to render the terrain with a hardware clip plane enabled so that we do not draw anything beneath the water plane. Add the following method to your Game class:
private void DrawReflection(float aspectRatio, out Matrix reflectionViewProjection)
{
// Set target to reflection and clear it
GraphicsDevice.SetRenderTarget(0, this.waterReflection);
GraphicsDevice.Clear(ClearOptions.DepthBuffer | ClearOptions.Target, Color.CornflowerBlue, 1f, 0);
const float waterHeight = 24f;
// Get a virtual camera position
Vector3 reflectionCamPosition = new Vector3
{
X = cameraPosition.X,
Y = waterHeight - (cameraPosition.Y - waterHeight),
Z = cameraPosition.Z
};
// Reflect the current cameraDirection around the water plane
Vector3 reflectedDir = Vector3.Reflect(Vector3.Normalize(cameraFront), Vector3.Up);
Vector3 reflectionCamTarget = default(Vector3);
reflectionCamTarget.X = reflectionCamPosition.X + reflectedDir.X;
reflectionCamTarget.Y = reflectionCamPosition.Y + reflectedDir.Y;
reflectionCamTarget.Z = reflectionCamPosition.Z + reflectedDir.Z;
// Generate view and projection matrices using virtual position, target and a field of view of 90 degrees
Matrix reflectionView = Matrix.CreateLookAt(reflectionCamPosition, reflectionCamTarget, Vector3.Up);
Matrix reflectionProjection = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver2, aspectRatio, 0.1f, 1000f);
// Output combioned reflection view and projection matrices
reflectionViewProjection = reflectionView * reflectionProjection;
// Create a normalized world space clip plane representing the water
Plane reflectionClipPlane = new Plane(Vector3.Up, -waterHeight);
reflectionClipPlane.Normalize();
// Transform the world space clip plane into clip space
Plane reflectionClipPlaneWaterSpace;
Plane.Transform(ref reflectionClipPlane, ref reflectionViewProjection, out reflectionClipPlaneWaterSpace);
// Set the clip plane on the hardware device
GraphicsDevice.ClipPlanes[0].Plane = reflectionClipPlaneWaterSpace;
GraphicsDevice.ClipPlanes[0].IsEnabled = true;
// Draw the terrain with our reflection view and projection
this.DrawTerrain(null, ref reflectionView, ref reflectionProjection);
// Make sure to unset to the clip plane, else further drawing will be spoilt
GraphicsDevice.ClipPlanes[0].IsEnabled = false;
}
Now we can call this method in Draw(), just before we set the render target to sceneDepth:
Matrix reflectionViewProjection; this.DrawReflection(aspectRatio, out reflectionViewProjection);
This result of this is that we have reflection data in our render target and viewProjection matrix to project it onto our water plane. Before we can do that we need to pass both of them to our PlanarWater Draw method. Change that draw method to:
public void Draw(GraphicsDevice device, Texture2D sceneDepth, Texture2D waterReflection, Matrix waterViewProjection, Matrix view, Matrix projection)
{
device.VertexDeclaration = this.PlaneVD;
device.Vertices[0].SetSource(this.PlaneVB, 0, VertexPositionTexture.SizeInBytes);
device.Indices = null;
device.Textures[0] = sceneDepth;
device.Textures[1] = waterReflection;
for (int i = 0; i < 2; i++)
{
device.SamplerStates[i].AddressU = TextureAddressMode.Clamp;
device.SamplerStates[i].AddressV = TextureAddressMode.Clamp;
}
device.SamplerStates[0].MinFilter = TextureFilter.Point;
device.SamplerStates[0].MagFilter = TextureFilter.Point;
device.SamplerStates[0].MipFilter = TextureFilter.Point;
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.Parameters["WaterViewProjection"].SetValue(waterViewProjection);
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();
device.Textures[0] = null;
device.Textures[1] = null;
for (int i = 0; i < 2; i++)
{
device.SamplerStates[i].AddressU = TextureAddressMode.Wrap;
device.SamplerStates[i].AddressV = TextureAddressMode.Wrap;
}
device.SamplerStates[0].MinFilter = TextureFilter.Linear;
device.SamplerStates[0].MagFilter = TextureFilter.Linear;
device.SamplerStates[0].MipFilter = TextureFilter.Linear;
}
Now we need to update the shader to use reflection and refraction, since we don’t have the refraction texture just yet, well use blue again. First we need to add a sampler for the reflection:
sampler2D ReflectionSampler : register(s1);
Next we need to pass a new value from the vertex shader to the pixel shader for the reflection coordinates. Change VertexShaderOutput to the following:
struct VertexShaderOutput
{
float4 Position : POSITION0;
float2 TexCoord : TEXCOORD0;
float4 ClipPos : TEXCOORD1;
float4 ReflClipPos : TEXCOORD2;
float3 ViewPos : TEXCOORD3;
};
Then change the vertex shader to:
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);
output.TexCoord = input.TexCoord;
output.ClipPos = output.Position;
float4x4 waterWorldViewProjection = mul(World, WaterViewProjection);
output.ReflClipPos = mul(objectPos, waterWorldViewProjection);
output.ViewPos = viewPosition;
return output;
}
Now we can use that ReflClipPos in the pixel shader to get a texture coordinate, and read the reflection texture. Change the pixel shader to the following:
float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
float2 screenTexCoords = float2(0.5, -0.5) * (float2(input.ClipPos.x, input.ClipPos.y) / input.ClipPos.w) + 0.5;
// 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 = max(pow(depthRange, shoreFalloff) * FarPlane * shoreScale, 0);
float3 refraction = float3(0, 0, 1
float2 reflectTexCoords = float2(0.5, -0.5) * (float2(input.ReflClipPos.x, input.ReflClipPos.y) / input.ReflClipPos.w) + 0.5;
float3 reflection = tex2D(ReflectionSampler, reflectTexCoords) * 0.5f;
float3 color = lerp(refraction, reflection, saturate(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 ‘)’