Sgt. Conker We are "absolutely fine"

3Nov/097

Article : Realistic Soft Edged Water In XNA

Body of Water

There are two more things we can do to make this more appealing. The first is specular light reflection from the sun. This will really help show the water oriention, and generally give more depth to the scene. The second is giving the water color. At this point the only color we have is from refraction and reflection, so what we will do is define two colors and blend between them based on the water depth. This allows us to have a dark murky deep color, and a warmer, lighter shoreline color.

First, the specular. We need the light direction in the shader. This means we need a new parameter to our PlanarWater draw method:

public void Draw(GraphicsDevice device, Texture2D sceneDepth, Texture2D waterReflection, Texture2D sceneColor, Matrix waterViewProjection, Matrix view, Matrix projection, float time, Vector3 lightDirection)

And send it to the effect with the other parameters:

this.WaterEffect.Parameters["LightDirection"].SetValue(lightDirection);

Update the call to the draw method:

this.water.Draw(graphics.GraphicsDevice, this.sceneDepth.GetTexture(), this.waterReflection.GetTexture(), this.sceneColor.GetTexture(), reflectionViewProjection, view, projection, (float)gameTime.TotalGameTime.TotalSeconds, lensFlare.LightDirection);

Now we can use it in the shader. Add a new constant to the top:

float3 LightDirection;

Add the following method above the pixel shader:

float PhongSpecular(float3 normal, float3 viewDir, float specularDecay)
{
    float nDotL = dot(normal, LightDirection);

    float3 reflection = 2.0f * normal * nDotL + LightDirection;

    reflection = normalize(reflection);

    float rdotV = saturate(dot(reflection, viewDir));

    return pow(rdotV, specularDecay);
}

In the pixel shader, change the last lines to:

float specular = PhongSpecular(normalWorld, -viewDir, 256);

// lerp back to refraction for the shoreline
color = lerp(refraction, color, alpha)+ (specular * alpha);

That’s better, but it’s still suffering the problem of looking like a thin sheet and not a body of water. This is where our second addition comes in. For this we need two colors added to the shader, add them as constants like so:

float3 SurfaceColor = {0.36, 0.664, 0.608};
float3 DeepColor = {0.09, 0.166, 0.177};

You can of course change these colors, or set them from C#, these are just some defaults I like.

Now we need another constant. This will determine when the deep color ends, and when the surface color begins:

const float DeepDepth = 50.0;

Now replace the pixe shader with this:

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) * FarPlane;

    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 range of depth from waterplane to terrain
    float depthRange = (sceneViewZ - viewLen);

    const float shoreFalloff = 2.0f;
    const float shoreScale = 5.0f;

    // calculate a transparency value using a power function
    float alpha = saturate(max(pow(depthRange / FarPlane, shoreFalloff) * FarPlane * shoreScale, 0));

    float3 refraction = tex2D(RefractionSampler, refractCoords).rgb;

    float3 waterColor = DeepColor * refraction;

    if (depthRange <= DeepDepth)
    {
        float difference = DeepDepth - depthRange;
        float factor = saturate(pow((difference / depthRange), shoreFalloff));

        waterColor = lerp(waterColor, SurfaceColor * refraction, factor);
    }

    float2 reflectTexCoords = ClipSpaceToTexCoordPerturb(input.ReflClipPos, dudv);

    float3 reflection = tex2D(ReflectionSampler, reflectTexCoords) * 0.5f;

    // color based on how much refraction vs reflection
    float3 color = lerp(waterColor, reflection, reflectionFactor);

    float specular = PhongSpecular(normalWorld, -viewDir, 256);

    // lerp back to refraction for the shoreline
    color = lerp(refraction, color, alpha)+ (specular * alpha);

    return float4(color, 1);
}

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.