Sgt. Conker We are "absolutely fine"

3Nov/097

Article : Realistic Soft Edged Water In XNA

Introducing the Soft Edge

One of the first things you may notice at this point is the hard edge where the terrain model intersects the water plane. Since one of our main goals is to eliminate this, we shall tackle it first, before doing any reflection, refraction, or water lighting.

In order to determine where the geometry intersects, we are going to need some measure of distance between the two geometries. We need this at the point of drawing our water plane, meaning we need some measure of distance of the terrain. To do this we shall render the linear view space Z coordinate into a render target, then when rendering the water geometry we can obtain the view space Z coordinate and compare to obtain a difference.

The first thing we will need is a render target to hold the depth of the scene. We will use a format of Single (R32F) as its supported on the 360, and most modern GPU’s.

Add a new field for the render target to Game:

private RenderTarget2D sceneDepth;

Intialize the render target in the Games LoadContent method, after we initialize the water:

int width = GraphicsDevice.PresentationParameters.BackBufferWidth;
int height = GraphicsDevice.PresentationParameters.BackBufferHeight;
sceneDepth = new RenderTarget2D(GraphicsDevice, width, height, 1, SurfaceFormat.Single, RenderTargetUsage.DiscardContents);

Now we can render the terrain to this render target as view space Z, but first we will need an effect for it. Add new Effect to Content and call it SceneDepth.fx, with the following HLSL:

float4x4 View;
float4x4 Projection;

float FarPlane;

struct VertexShaderInput
{
    float3 Position : POSITION0;
};

struct VertexShaderOutput
{
    float4 Position : POSITION0;
    float3 ViewPos : TEXCOORD0;
};

VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
    VertexShaderOutput output;

    float4 worldPosition = float4(input.Position.xyz, 1);

    float4 viewPosition = mul(worldPosition, View);

    output.Position = mul(viewPosition, Projection);

    output.ViewPos = viewPosition.xyz;

    return output;
}

float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
    return float4(length(input.ViewPos) / FarPlane, 0, 0, 1);
}

technique Technique1
{
    pass Pass1
    {
        VertexShader = compile vs_2_0 VertexShaderFunction();
        PixelShader = compile ps_2_0 PixelShaderFunction();
    }
}

Now create a new field to hold this effect:

private Effect RenderDepth;

And load it in LoadContent:

this.RenderDepth = Content.Load("SceneDepth");

Now we hit a little snag. The DrawTerrain method draws a terrain model, which uses BasicEffect. We need to draw it with our RenderDepth effect. Replace your DrawTerrain method with the follow code, it sets a custom effect on the mesh parts, but restores the BasicEffect, so it can draw with both:

protected void DrawTerrain(Effect customEffect, ref Matrix view, ref Matrix projection)
{
    Queue meshPartEffects = null;

    if (customEffect != null)
    {
        meshPartEffects = new Queue();
        customEffect.Parameters["View"].SetValue(view);
        customEffect.Parameters["Projection"].SetValue(projection);
    }

    foreach (ModelMesh mesh in terrain.Meshes)
    {
        if (customEffect == null)
        {
            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 = 300;
                effect.FogEnd = 1000;
                effect.FogColor = Color.CornflowerBlue.ToVector3();
            }
        }
        else
        {
            foreach (ModelMeshPart meshPart in mesh.MeshParts)
            {
                meshPartEffects.Enqueue(meshPart.Effect as BasicEffect);

                meshPart.Effect = this.RenderDepth;
            }
        }

        mesh.Draw();
    }

    if (meshPartEffects == null)
    return;

    foreach (ModelMesh mesh in terrain.Meshes)
        foreach (ModelMeshPart meshPart in mesh.MeshParts)
            meshPart.Effect = meshPartEffects.Dequeue();
}

Now we can test this, change the terrain draw call to use this.RenderDepth as the first parameter, and set the FarPlane distance on the effect:

this.RenderDepth.Parameters["FarPlane"].SetValue(1000f - 0.1f);
this.DrawTerrain(this.RenderDepth, ref view, ref projection);

At this point we are ready to render the depth into a render target, update your Games Draw method so it reads as follows:

protected override void Draw(GameTime gameTime)
{
    // Compute camera matrices.
    Matrix view = Matrix.CreateLookAt(cameraPosition, cameraPosition + cameraFront, Vector3.Up);

    float aspectRatio = GraphicsDevice.Viewport.AspectRatio;
    Matrix projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, aspectRatio, 0.1f, 1000);

    GraphicsDevice.SetRenderTarget(0, this.sceneDepth);
    GraphicsDevice.Clear(ClearOptions.DepthBuffer | ClearOptions.Target, Color.White, 1f, 0);

    this.RenderDepth.Parameters["FarPlane"].SetValue(1000f - 0.1f);
    this.DrawTerrain(this.RenderDepth, ref view, ref projection);

    GraphicsDevice.SetRenderTarget(0, null);
    GraphicsDevice.Clear(Color.CornflowerBlue);

    this.DrawTerrain(null, ref view, ref projection);
    this.water.Draw(graphics.GraphicsDevice, view, projection);

    // Tell the lensflare component where our camera is positioned.
    lensFlare.View = view;
    lensFlare.Projection = projection;

    base.Draw(gameTime);
}

With the terrain depth now rendered into a render target, we can use it to determine the difference of depth between the terrain and the water plane. We need to send this depth texture to the water effect, so we will pass it as a parmaeter in the planar water Draw method:

public void Draw(GraphicsDevice device, Texture2D sceneDepth, Matrix view, Matrix projection)

And update the draw call:

this.water.Draw(graphics.GraphicsDevice, this.sceneDepth.GetTexture(), view, projection);

Inside the PlanarWater Draw method, we will set the texture at the start, so that the effect can use it:

device.Textures[0] = sceneDepth;
device.SamplerStates[0].AddressU = TextureAddressMode.Clamp;
device.SamplerStates[0].AddressV = TextureAddressMode.Clamp;
device.SamplerStates[0].MinFilter = TextureFilter.Point;
device.SamplerStates[0].MagFilter = TextureFilter.Point;
device.SamplerStates[0].MipFilter = TextureFilter.Point;

And also reset them at the end:

device.Textures[0] = null;
device.SamplerStates[0].AddressU = TextureAddressMode.Wrap;
device.SamplerStates[0].AddressV = TextureAddressMode.Wrap;
device.SamplerStates[0].MinFilter = TextureFilter.Linear;
device.SamplerStates[0].MagFilter = TextureFilter.Linear;
device.SamplerStates[0].MipFilter = TextureFilter.Linear;

Now all that left to do is update the Water.fx to this:

float4x4 World;
float4x4 View;
float4x4 Projection;

float FarPlane = 1000.0f - 0.1f;

struct VertexShaderInput
{
    float3 Position : POSITION0;
    float2 TexCoord : TEXCOORD0;
};

struct VertexShaderOutput
{
    float4 Position : POSITION0;
    float2 TexCoord : TEXCOORD0;
    float4 ClipPos : TEXCOORD1;
    float3 ViewPos : TEXCOORD2;
};

sampler2D DepthSampler : register(s0);

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;
    output.ClipPos = output.Position;

    output.ViewPos = viewPosition;

    return output;
}

float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
    float2 screenTexCoords = 0.5 * (float2(input.ClipPos.x, input.ClipPos.y) / input.ClipPos.w) + 0.5;
    screenTexCoords.y = 1 - screenTexCoords.y;

    float sceneViewZ = tex2D(DepthSampler, screenTexCoords).r;
    float waterViewZ = length(input.ViewPos) / FarPlane;

    float depthRange = max(pow((sceneViewZ - waterViewZ), 2)* 10000, 0);

    float4 col = float4(0, 0, 1, 1);
    float4 col2 = float4(1, 0, 0, 1);

    return lerp(col2, col, saturate(depthRange));
}

technique Technique1
{
    pass Pass1
    {
        VertexShader = compile vs_2_0 VertexShaderFunction();
        PixelShader = compile ps_2_0 PixelShaderFunction();
    }
}

Now you will be able to visualize a potential shoreline.

At this point I could just present a final water shader and explain each bit, but instead I will go over each part as we write it.

About CorporalX

Professional XNA/.NET developer.
Comments (7) Trackbacks (0)
  1. 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.

  2. 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.

  3. 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 :D

  4. Existence – Fixed the images and sample download.

  5. 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

  6. 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.

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


Leave a comment


*

No trackbacks yet.