Steps In Snow

        In this chapter, we won’t be using any new techniques, but we will see how to use the techniques described in the previous chapters to achieve the effect of someone walking over snow. The snow will compress as the character steps on it, and it will rise slowly with time (assuming there’s falling snow). When drawing the snow, we will use a displacement map, similar to how the terrain was rendered. To achieve the deformation effect, we will render the scene to a texture, that is used as a deformation map. During this rendering, we use an camera that will “look” from below the scene, upwards along the Y axis. We will use a special shader so that points that are closer to the ground appear white, and points further away appear darker. This means that white areas in the deformation map represent full deformation, and dark areas represent no deformation. This deformation map is then subtracted from the displacement map that we use to render the snow. Further, to simulate falling snow, we add small random values to the displacement map that slowly accumulate over time and the snow rises again.

Because of the way the deformation map is built, we don’t care what objects are in the scene, and we don’t need to have special code for each object. All we need to do is render these objects with the special shader, and all deformations appear, whatever shape the objects have. For this tutorial, I chose to use the dude from the Skinned Model sample, because it is animated, and doesn’t have a regular form, so the result is nicer.

Before we begin writing code, we have to prepare a new project. This will be more complex than the others, so you can either follow the next steps, or directly download the project prepared for the tutorial here: Chapter4startup.zip.

Create a new windows project (we use SkinnedModel.dll and SkinnedModelPipeline.dll, from the Skinned Model Sample for windows. For Xbox, use the corresponding files)

  • Add the classes Camera.cs, Grid.cs and DudeEntity.cs to the project. DudeEntity.cs is a helper class, and it will be used to make the model move around between random points.
  • Add the files inside DudeModel.zip to the project folder, inside a folder named Content (the folder HAS to be named Content, because the SkinnedModelProcessor is hardcoded to look in that folder).
  • Copy SkinnedModel.dll and SkinnedModelPipeline.dll in the project folder.
  • Add a reference to SkinnedModel.dll (Project->Add Reference->Browse)
  • Add a content pipeline reference to the SkinnedModelPipeline.dll inside the Content Pipeline tab of the project’s properties.
  • Add dude.fbx to the project, using the SkinnedModelProcessor Content Processor.
  • Copy the file noise.png in the Content folder and add it to the project

At this point, we can begin to write some code. First, we will add the camera, and the skinned model, and make it walk around the scene. We need to add three members to the Game1 class. The camera will be instantiated in the constructor of the class. Then, in LoadGraphicsContent, we load the model, and initialize dudeEntity.

 

 

[...]
using SkinnedModel;
using VTFTutorial;
[...]
Camera camera;
Model dudeModel;
DudeEntity dudeEntity;
public Game1()
{
        [...]
        camera = new Camera(this);
        Components.Add(camera);
}
[...]
protected override void LoadGraphicsContent(bool loadAllContent)
{
    if (loadAllContent)
    {
        dudeModel = content.Load<Model>("Content\\dude");
        dudeEntity = new DudeEntity();
        dudeEntity.Initialize(dudeModel.Tag as SkinningData);
    }
}

In the Update function, we update the entity, and for drawing, we use the standard code for drawing a model, taking the bones from the dudeEntity.

 

 

protected override void Update(GameTime gameTime)
        {
            [...]
            dudeEntity.Update(gameTime);
            base.Update(gameTime);
        }

protected override void Draw(GameTime gameTime)
        {
            graphics.GraphicsDevice.Clear(Color.CornflowerBlue);
            graphics.GraphicsDevice.RenderState.CullMode = CullMode.None;
            Matrix[] bones = dudeEntity.AnimationPlayer.GetSkinTransforms();
            foreach (ModelMesh mesh in dudeModel.Meshes)
            {
                foreach (Effect effect in mesh.Effects)
                {
                    effect.Parameters["Bones"].SetValue(bones);
                    effect.Parameters["View"].SetValue(camera.View);
                    effect.Parameters["Projection"].SetValue(camera.Projection);
                }

                mesh.Draw();
            }

            base.Draw(gameTime);
        }

If you run the program now, you should see the dude walking around randomly.

The shader used for rendering the snow should be self explanatory now. We read the height from a displacement map in the vertex shader. The pixel shader colors the ground according to the height. Grey for low snow, white for high snow. As an exercise, you can try and add normals and lighting to the snow, just like we did in the terrain tutorial. The shader will be written in VTFSnow.fx, in the Shaders directory.

 

 

float4x4 view;
 float4x4 proj;
 float4x4 world;

 texture displacementMap;
 sampler displacementSampler = sampler_state
 {
         Texture   = <displacementMap>;
         MipFilter = None;
         MinFilter = Point;
         MagFilter = Point;
         AddressU  = Clamp;
         AddressV  = Clamp;
 };

 struct VS_INPUT
 {
         float4 position : POSITION;
         float4 uv : TEXCOORD0;
 };

 struct VS_OUTPUT
 {
         float4 position  : POSITION;
         float4 pos : TEXCOORD1;
 };

 float heightModifier = 32;
 VS_OUTPUT Transform(VS_INPUT In)
 {
         VS_OUTPUT Out = (VS_OUTPUT)0;
         float4x4 viewProj = mul(view, proj);
         float4x4 worldViewProj= mul(world, viewProj);
         //read height from displacement map
         float height = tex2Dlod ( displacementSampler, In.uv );
         In.position.y = height;
         Out.position = mul( In.position , worldViewProj);
         Out.pos = In.position;
         return Out;
 }

 float4 PixelShader(in float4 pos:TEXCOORD1) : COLOR
 {
         return 0.45f + pos.y / heightModifier;
 }

 technique GridDraw
 {
         pass P0
         {
                 vertexShader = compile vs_3_0 Transform();
                 pixelShader  = compile ps_3_0 PixelShader();
         }
 }

This being done, it’s time to go in Game1.cs. We will need a Grid member, and an Effect for rendering the grid. This code is very much like the one used in the first tutorial. (I know, all this code, identical to what we’ve done before is boring, but bear with me… the nice part comes soon).

 

 

Grid grid;
 Effect gridEffect;

 public Game1()
 {
         [...]
         grid = new Grid(this);
         grid.CellSize = 4;
         grid.Dimension = 256;
 }
 protected override void LoadGraphicsContent(bool loadAllContent)
 {
         if (loadAllContent)
         {
         [...]
         gridEffect = content.Load<Effect>("Shaders\\VTFSnow");
         grid.LoadGraphicsContent();
         }
 }
 protected override void Draw(GameTime gameTime)
 {
         graphics.GraphicsDevice.Clear(Color.CornflowerBlue);
         graphics.GraphicsDevice.RenderState.CullMode = CullMode.None;
         graphics.GraphicsDevice.RenderState.DepthBufferEnable = true;
         gridEffect.Parameters["world"].SetValue(Matrix.Identity);
         gridEffect.Parameters["view"].SetValue(camera.View);
         gridEffect.Parameters["proj"].SetValue(camera.Projection);
         gridEffect.Begin();
         foreach (EffectPass pass in gridEffect.CurrentTechnique.Passes)
         {
                 pass.Begin();
                 grid.Draw();
                 pass.End();
         }
         gridEffect.End();
         [...]
 }

We didn’t set the displacementMap parameter, because we don’t have anything to put there, yet. And now, the fun begins. We’re going to add some members to Game1.cs, and then initialize them in LoadGraphicsContent. The effects will be added to this function after we write them.

 

 

Effect snowEffect;                                //effect file that holds all the needed techniques
 Effect skinnedModelSnowEffect;          //effect that draws a skinned model to the deformation map
 RenderTarget2D temporaryRT;             // temporary render target
 RenderTarget2D snowRT;                    //render target that holds the displacementMap for the snow
 RenderTarget2D deformationRT;          //render target that holds the deformation map in the current frame
 DepthStencilBuffer snowDepth;            //depth buffer used when updating the deformation and displacement maps
 Boolean isSnowReset = false;              // boolean that notifies us when we need to reset the snow
 SpriteBatch spriteBatch;
 Random random = new Random();      //this, together with the noise texture will be used for generating the falling snow
 Texture2D noiseTexture;
ed override void LoadGraphicsContent(bool loadAllContent)
 {
     if (loadAllContent)
     {
         [...]
         spriteBatch = new SpriteBatch(graphics.GraphicsDevice);
         noiseTexture = content.Load<Texture2D>("Content\\noise");
     }
     snowDepth = new DepthStencilBuffer(graphics.GraphicsDevice, 256, 256, graphics.GraphicsDevice.DepthStencilBuffer.Format);
     snowRT = new RenderTarget2D(graphics.GraphicsDevice, 256, 256, 1, SurfaceFormat.Single);
     temporaryRT = new RenderTarget2D(graphics.GraphicsDevice, 256, 256, 1, SurfaceFormat.Single);
     deformationRT = new RenderTarget2D(graphics.GraphicsDevice, 256, 256, 1, SurfaceFormat.Single);
     isSnowReset = false;
 }

        Deformation Map

Now we will render the deformation map. As said earlier, this is done by rendering the objects in the scene as viewed from below, with an orthographic matrix. Since we want the deformation to correspond to the animation of the dude, we have to make an effect for this. The effect will be very similar to the SkinnedModel.fx effect, but we will modify the pixel shader to output information that we want. Create a new file in the Shaders folder, and name it SkinnedModelSnow.fx. I will post the whole effect, and comment on the parts that were changed.

 

 

// Maximum number of bone matrices we can render using shader 2.0 in a single pass.
// If you change this, update SkinnedModelProcessor.cs to match.
#define MaxBones 59
// Input parameters.
float4x4 View;
float4x4 Projection;
float4x4 Bones[MaxBones];
// Vertex shader input structure.
struct VS_INPUT
{
        float4 Position : POSITION0;
        float4 BoneIndices : BLENDINDICES0;
        float4 BoneWeights : BLENDWEIGHT0;
};
// Vertex shader output structure.
struct VS_OUTPUT
{
        float4 Position : POSITION0;
        float4 worldPos : TEXCOORD1;
};
// Vertex shader program.
VS_OUTPUT VertexShader(VS_INPUT input)
{
        VS_OUTPUT output;
        // Blend between the weighted bone matrices.
        float4x4 skinTransform = 0;
        skinTransform += Bones[input.BoneIndices.x] * input.BoneWeights.x;
        skinTransform += Bones[input.BoneIndices.y] * input.BoneWeights.y;
        skinTransform += Bones[input.BoneIndices.z] * input.BoneWeights.z;
        skinTransform += Bones[input.BoneIndices.w] * input.BoneWeights.w;
         // Skin the vertex position.
        float4 position = mul(input.Position, skinTransform);
        output.Position = mul(mul(position, View), Projection);
        output.worldPos = output.Position;
        return output;
}
// Pixel shader program.
float4 PixelShader(float4 worldPos : TEXCOORD1) : COLOR0
{
        float height = worldPos.y;
        return 1.0f - saturate(height / 32.0);
}
technique SkinnedModelTechnique
{
        pass SkinnedModelPass
        {
                VertexShader = compile vs_2_0 VertexShader();
                PixelShader = compile ps_2_0 PixelShader();
        }
}

The important part of this effect file is the pixel shader. In this sample, we assume that the maximum height of the snow is 32 units. So anything above 32 units has no effect on the snow. By saturate(height / 32) we obtain a value between 0 and 1, for the points with heights between 0 and 32. Everything that’s above that is set to 1. We then subtract this value from 1, so in the end, lower points will have a high value, which decreases as the height grows, and stays at zero for everything above 32. This constitutes the deformation map. All items in the scene that affect the snow should be rendered with this effect, or a similar one, for static models. Now, we need to load this effect in the LoadGraphicsContent function:

 

 

skinnedModelSnowEffect = content.Load<Effect>("Shaders\\SkinnedModelSnow");

We need to write a function that will create our deformation map. We create a new function in Game1.cs, and name it RenderDeformationMap(). Inside, we add code needed to preserve the render target, set our new render target and set the render states we desire, in care some exterior force changed them.

 

 

void RenderDeformationMap()
 {

         RenderTarget2D oldRT = graphics.GraphicsDevice.GetRenderTarget(0) as RenderTarget2D;
         DepthStencilBuffer oldDS = graphics.GraphicsDevice.DepthStencilBuffer;
         graphics.GraphicsDevice.SetRenderTarget(0, deformationRT);
         graphics.GraphicsDevice.DepthStencilBuffer = snowDepth;
         graphics.GraphicsDevice.RenderState.CullMode = CullMode.None;
         graphics.GraphicsDevice.RenderState.FillMode = FillMode.Solid;
         graphics.GraphicsDevice.RenderState.DepthBufferEnable = true;
         graphics.GraphicsDevice.Clear(Color.Black);

For the projection matrix, we will use and orthographic projection matrix. Since out grid has Dimension 256 and cellSize 4, in order to cover all the terrain, the view volume has to be of 1024 * 1024 units. For the view matrix, we use a matrix that “looks” along the positive Y axis.

 

 

Matrix orthoMatrix = Matrix.CreateOrthographic(1024, 1024, 1, 500);
Matrix viewMatrix = Matrix.Identity;
viewMatrix.Forward = Vector3.Up;
viewMatrix.Right = Vector3.Right;
viewMatrix.Up = Vector3.Forward;
viewMatrix.Translation = new Vector3(0, 0, 0);

We set the effect parameters, and then we draw the animated model by looping through all meshes. Since we want to use a custom shader, we need to draw the model manually, and cannot use mesh.Draw(). After this, we resolve the render target

 

 

Matrix[] bones = dudeEntity.AnimationPlayer.GetSkinTransforms();
skinnedModelSnowEffect.Parameters["Bones"].SetValue(bones);
skinnedModelSnowEffect.Parameters["View"].SetValue(viewMatrix);
skinnedModelSnowEffect.Parameters["Projection"].SetValue(orthoMatrix);

skinnedModelSnowEffect.Begin();
foreach (EffectPass pass in skinnedModelSnowEffect.CurrentTechnique.Passes)
{
    pass.Begin();
    foreach (ModelMesh mesh in dudeModel.Meshes)
    {
        foreach (ModelMeshPart meshpart in mesh.MeshParts)
        {
            graphics.GraphicsDevice.VertexDeclaration = meshpart.VertexDeclaration;
            graphics.GraphicsDevice.Vertices[0].SetSource(mesh.VertexBuffer, meshpart.StreamOffset, meshpart.VertexStride);
            graphics.GraphicsDevice.Indices = mesh.IndexBuffer;
            graphics.GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, meshpart.BaseVertex, 0, meshpart.NumVertices, meshpart.StartIndex, meshpart.PrimitiveCount);
        }
    }
    pass.End();
}
skinnedModelSnowEffect.End();
graphics.GraphicsDevice.ResolveRenderTarget(0);
graphics.GraphicsDevice.SetRenderTarget(0, oldRT);
graphics.GraphicsDevice.DepthStencilBuffer = oldDS;

}

Normally, in a real application, inside this function we would loop through all the models in the scene, but in this case, we only have one model.

        Creating the final Displacement Map

For this final step of the tutorial, we need another effect file. This effect file will contain several techniques that are used to obtain the displacement map. As usual, create an effect file in the Shaders directory, and call it Snow.fx. The textures needed by these techniques are the inputMap, randomMap, snowMap and deformationMap. The inputMap is used when copying a texture from one rendertarget to another, randomMap contains random values, snowMap is the current displacement map, and deformation map is self-explanatory.

 

 

texture inputMap;
 sampler inputSampler : register(s0)  = sampler_state
 {
         Texture   = <inputMap>;
         MipFilter = Linear;
         MinFilter = Linear;
         MagFilter = Linear;
         AddressU  = Clamp;
         AddressV  = Clamp;
 };
 texture randomMap;
 sampler randomSampler : register(s0)= sampler_state
 {
         Texture   = <randomMap>;
         MipFilter = Linear;
         MinFilter = Linear;
         MagFilter = Linear;
         AddressU  = mirror;
         AddressV  = mirror;
 };
 texture snowMap;
 sampler snowSampler = sampler_state
 {
         Texture   = <snowMap>;
         MipFilter = Linear;
         MinFilter = Linear;
         MagFilter = Linear;
         AddressU  = Clamp;
         AddressV  = Clamp;
 };

 texture deformationMap;
 sampler deformationSampler = sampler_state
 {
         Texture   = <deformationMap>;
         MipFilter = Linear;
         MinFilter = Linear;
         MagFilter = Linear;
         AddressU  = Clamp;
         AddressV  = Clamp;
 };

 When initializing, we set the height of the snow to 32.

 float4 InitSnowPS(in float2 uv : TEXCOORD0) : COLOR
 {
         return 32;
 }

When we deform the snow field, we subtract the deformation from the current height.

 

 

float4 AddDeformationPS(in float2 uv : TEXCOORD0) : COLOR
{
        float height = tex2D(snowSampler,uv);
        float deformation = tex2D(deformationSampler,uv);
        return clamp(height - deformation,0,32);
}

For the snow accumulation, we add random values read from the noise texture. The randomNumbers are varied with every frame, to distribute the noise around the terrain in different amounts.

 

 

float2 randomNumbers;
float4 AccumulateSnowPS(in float2 uv:TEXCOORD0) : COLOR
{
        float accumulatedSnow = 0.5 * tex2D(randomSampler,uv + randomNumbers);
        float height = tex2D(snowSampler,uv);
        return clamp(accumulatedSnow + height, 0, 32);
}

Before combining the deformation map with the displacement map, we will first blur the deformation map, to smooth it out. This shader will be used

 

 

static float2 blurOffset[8] =
{      float2( 1/256.0f ,  1/256.0f),
        float2(-1/256.0f , -1/256.0f),
        float2(-1/256.0f ,  1/256.0f),
        float2( 1/256.0f , -1/256.0f),
        float2( 1/256.0f ,  0.0f),
        float2(-1/256.0f , 0.0f),
        float2(0.0f ,  1/256.0f),
        float2(0.0f , -1/256.0f)
 };
float4 BlurPS(in float2 uv : TEXCOORD0) : COLOR
{
        float4 color = tex2D(deformationSampler, uv);
        for( int i=0; i< 8; i++)
        {
                color += tex2D(deformationSampler, uv + blurOffset[i]);
        }
        return color/ 9.0f;
}

Next, the pixel shader that copies the texture, and the techniques for each of the shaders.

 

 

float4 CopyTexturePS(in float2 uv : TEXCOORD0) : COLOR
{
        return tex2D(inputSampler,uv);
}
technique AddDeformation
{
        pass P0
        {
                pixelShader  = compile ps_3_0 AddDeformationPS();
        }
}
technique AccumulateSnow
{
        pass P0
        {
                pixelShader  = compile ps_3_0 AccumulateSnowPS();
        }
}
technique BlurDeformation
{
        pass P0
        {
                pixelShader  = compile ps_3_0 BlurPS();
        }
}
technique InitSnow
{
        pass P0
        {
                pixelShader  = compile ps_3_0 InitSnowPS();
        }
}
technique CopyTexture
{
        pass P0
        {
                pixelShader  = compile ps_3_0 CopyTexturePS();
        }
}

Back to Game1.cs, we have to load the effect in LoadGraphicsContent

 

 

protected override void LoadGraphicsContent(bool loadAllContent)
{
        if (loadAllContent)
        {
                [...]
                snowEffect = content.Load<Effect>("Shaders\\Snow");
        }
        [...]
}

Now, similarly to what we did in the particle sample, we need a helper function that applies one of the above techniques. We add a function that takes as parameters a string that specifies the technique, and the rendertarget into which to put the result. We save the old render target, assign the temporary one, apply the desired technique, and after that, copy the result from the temporary render target into the destination render target.

 

 

private void DoSnowTechnique(string technique, RenderTarget2D resultTarget)
{
        RenderTarget2D oldRT = graphics.GraphicsDevice.GetRenderTarget(0) as RenderTarget2D;
        DepthStencilBuffer oldDS = graphics.GraphicsDevice.DepthStencilBuffer;
        graphics.GraphicsDevice.DepthStencilBuffer = snowDepth;
        graphics.GraphicsDevice.SetRenderTarget(0, temporaryRT);
        graphics.GraphicsDevice.Clear(Color.White);
        spriteBatch.Begin(SpriteBlendMode.None,
                      SpriteSortMode.Immediate,
                      SaveStateMode.None);
        snowEffect.CurrentTechnique = snowEffect.Techniques[technique];
        snowEffect.Begin();
        if (isSnowReset)
        {
                snowEffect.Parameters["snowMap"].SetValue(snowRT.GetTexture());
                snowEffect.Parameters["deformationMap"].SetValue(deformationRT.GetTexture());
        }
        snowEffect.Parameters["randomNumbers"].SetValue(new Vector2((float)random.NextDouble(), (float)random.NextDouble()));
        snowEffect.CurrentTechnique.Passes[0].Begin();
        spriteBatch.Draw(noiseTexture, new Rectangle(0, 0, 256, 256), Color.White);
        snowEffect.CurrentTechnique.Passes[0].End();
        snowEffect.End();
        spriteBatch.End();
        graphics.GraphicsDevice.ResolveRenderTarget(0);
        graphics.GraphicsDevice.SetRenderTarget(0, resultTarget);
        spriteBatch.Begin(SpriteBlendMode.None,
                          SpriteSortMode.Immediate,
                          SaveStateMode.None);
        snowEffect.CurrentTechnique = snowEffect.Techniques["CopyTexture"];
        snowEffect.Begin();
        snowEffect.CurrentTechnique.Passes[0].Begin();
        spriteBatch.Draw(temporaryRT.GetTexture(), new Rectangle(0, 0, 256, 256), Color.White);
        snowEffect.CurrentTechnique.Passes[0].End();
        snowEffect.End();
        spriteBatch.End();
        graphics.GraphicsDevice.ResolveRenderTarget(0);
        graphics.GraphicsDevice.SetRenderTarget(0, oldRT);
        graphics.GraphicsDevice.DepthStencilBuffer = oldDS;
}

This function will be called with different parameters. First, if the snow has to be reset, we will call the InitSnow technique. After this, we first render the deformation map, blur it, add the deformations to the displacement map, and finally accumulate random snow. All these calls will be put in a function, which will then be called at the beginning of the Draw() function. Finally, we can set the displacementMap parameter of the VTFSnow effect.

 

 

void UpdateSnow()
 {
         if (!isSnowReset)
         {
                 DoSnowTechnique("InitSnow", snowRT);
                 isSnowReset = true;
         }
         RenderDeformationMap();
         DoSnowTechnique("BlurDeformation", deformationRT);
         DoSnowTechnique("AddDeformation", snowRT);
         DoSnowTechnique("AccumulateSnow", snowRT);
 }
 protected override void Draw(GameTime gameTime)
 {

         UpdateSnow();

         graphics.GraphicsDevice.Clear(Color.CornflowerBlue);
         graphics.GraphicsDevice.RenderState.CullMode = CullMode.None;
         graphics.GraphicsDevice.RenderState.DepthBufferEnable = true;
         gridEffect.Parameters["displacementMap"].SetValue(snowRT.GetTexture());
         [...]
 }

If you run the code now, you should see a tough looking dude that doesn’t care about being stuck in a square of snow 🙂

tough3[1]

trail[1]

In this chapter we saw how we can generate deformation map by rendering the scene with special matrices, how to combine this with a displacement texture, and how to finally achieve the effect of someone walking on snow. This effect can be combined with the normal generation algorithm for better lighting, and with the particle systems for better visuals. As an interesting idea, if you decide to add the particle system, you could make the accumulation of snow based on the texture holding the particle positions, and make the particles “die” when they hit the snow, based on the displacement map.

The full code for this sample can be found in Chapter4.zip.

  • http://www.wilez.it WILEz

    FANTASTIC!

  • Dirkie

    This is such a great article! How scalable is this for large terrains Catalin?

  • Jung

    Totally awesome! it’s good stuff which i’d been looking for.

    Thank you.

  • http://Website Rosa

    Good work!!!

    Thanks for this tutorial!!