Sgt. Conker We are "absolutely fine"

21Oct/092

Article: Fur Rendering

Improvements

Improvement #1: Fake Inter-Fur Occlusion

One issue that appears is visible if we were to render the whole fur with the same color. The result (seen below) does not make individual hair strands stand out, and is not visually pleasing.

Behold, pure gold

We can easily fix this using some fake shadows. In a patch of fur/hair, the lower part of the hair will usually have much lesser light reaching it that the upper layers. This is why we can use the information about the layer currently being drawn (obtainable through the CurrentLayer parameter) to add some fake shadows. The code that handles this should go in the pixel shader of the fur effect.

 //based on layer depth, choose the amount of shading.
 //we lerp between two values to avoid having the base of the fur pure black.
 float shadow = lerp(0.4,1,CurrentLayer);
 furColor *= shadow;

Shaded grass

As you can see, the hair stands out and much better, and the overall effect is nicer. The look of textured fur is similary improved.


Improvement #2: Height Variation

We talked earlier about using the RGB channels of the fur texture to add some parameters for more special rendering. We then mentioned rendering fur strands of different height. This is what we will do now.

First, we need to modify the FillFurTexture() function. We will assign to each strand of hair a number which will represent the maximum layer on which that particular strand will be visible. For our example, we will choose to divide the strands by the number of layers. So assuming we have 1000 strands and 10 layers, the first 100 strands (1000/10) will only be one layer high, the second 100 strands wil go up to the second layer, and so un, until the last hundred strands will reach the top. This gives a linear distribution of heights. Let's see how this looks in the code. We will first compute how many strands reach each layer. Then, when computing the position of each strand, we check to see which group it belongs to, i.e. which is the maximum layer reachable by that strand. Then we normalize this value by dividing it to the total number of layers, to get in into the [0..1] range. Then we set this as a value to the red channel of the pixel.

private void FillFurTexture(Texture2D furTexture, float density)
{
 [...] //other computations
 //compute the number of opaque pixels = nr of hair strands
 int nrStrands = (int)(density * totalPixels);

 //compute the number of strands that stop at each layer
 int strandsPerLayer = nrStrands / nrOfLayers;

 //fill texture with opaque pixels
 for (int i = 0; i < nrStrands; i++)
 {
 int x, y;
 //random position on the texture
 x = rand.Next(height);
 y = rand.Next(width);

 //compute max layer
 int max_layer = i / strandsPerLayer;
 //normalize into [0..1] range
 float max_layer_n = (float)max_layer / (float)nrOfLayers;

 //put color (which has an alpha value of 255, i.e. opaque)
 //max_layer_n needs to be multiplied by 255 to achieve a color in [0..255] range
 colors[x * width + y] = new Color((byte)(max_layer_n * 255), 0, 0, 255);
 }
 //set the pixels on the texture.
 furTexture.SetData<Color>(colors);
}

To use this data, we need to also modify the pixel shader of the fur effect. Before drawing a pixel, we verify if the maximum layer stored in the red channel of the pixel is lower that the current layer. If it is, we don't draw the current pixel. If it isn't (we haven't reached the maximum layer of this pixel yet), we draw it. We also need to combine this with the test we wrote previously about making the lower layer fully opaque. The two lines that achieve this can be seen below.

float furVisibility =(CurrentLayer > furData.r) ? 0 : furData.a;
furColor.a = (CurrentLayer == 0) ? 1 : furVisibility;

You can compare the results below. The first image uses fixed height for all strands, the second one has linear variations.

Fixed Height

Variable Height

One more improvement that can be done related to the height of the hairs stands is to make the height distribution non linear. To do this, just add a line that modifies the value of max_layer_n, right after first computing it. There are several ways to modify this, yielding different results.

 max_layer_n = (float)Math.Sin(max_layer_n);

Sin

 max_layer_n = (float)Math.Pow(max_layer_n,5);

Po 5

 max_layer_n = (float)Math.Sqrt(max_layer_n);

sqrt

if ((max_layer_n > 0.05f) && (max_layer_n < 0.95f))
 max_layer_n = 0.5f;

For some interesting effects, you can also use the X and Y position to compute the height.

max_layer_n = 0.2f + 0.8f * ((float)Math.Sin((double)x / height * 20) / 2.0f + 0.5f);

Vector2 dist = new Vector2((float)x / height - 0.5f, (float)y / width - 0.5f);
max_layer_n = 0.4f + 0.6f *( (float)Math.Cos(dist.Length() * 50) / 2.0f + 0.5f);

You can experiment with different ways of modifying the value of max_layer_n and see what comes up.

Improvement #3: Deformation

To add one lat bit of realism to the fur simulation, we can make it move. Since our strands are composed of a number of layers, we can't move each strand individually, but we can move them all at once. To realize this movement, it is enough to slightly displace each layer (with lower values for lower layers, and larger values for higher layers) in order to give the appearance of deformation. If we alter the displacement value each frame, the fur will look like it's moving. For a quick exemplification, we will take into consideration the gravity, and some arbitrary force. Let's declare the needed variables as members of type Vector3. The gravity will point downwards, for now the force vector will be zero, and we also need a final displacement vector. These are declared in the Game class.

//movement vectors
Vector3 gravity = new Vector3(0, -1.0f, 0);
Vector3 forceDirection = Vector3.Zero;
//final displacement for hair
Vector3 displacement;

We also need a parameter for displacement in the shader. Go ahead and at it to the top of the effect file.

float3 Displacement;

Inside the vertex shader this time, we will use this parameter to alter the position of the fur vertices in world space. We don't want to apply the same displacement for all layers, because the whole fur would move. We will apply multiply the displacement with a factor ranging from 0 to 1, based on the layer being processed. If we would just consider that factor to be CurrentLayer, the displacement would look unrealistic. In real life, a strand of hair is curbed when blown by the wind or affected by gravity, because different parts of the strand have different elasticity and string tension. To simulate this, we will use the third power of the CurrentLayer, to obtain a non-linear displacement. By raising it to a power, the way the displacement grown seems more natural and gives better results. The code can be seen below.

VertexShaderOutput FurVertexShader(VertexShaderInput input)
{
 VertexShaderOutput output;
 float3 pos;
 pos = input.Position + input.Normal * MaxHairLength * CurrentLayer;

 float4 worldPosition = mul(float4(pos,1), World);

 //make the displacement non linear, to make it look more like fur
 float displacementFactor = pow(CurrentLayer, 3);
 //apply the displacement
 worldPosition.xyz +=Displacement*displacementFactor ;

 [...] //rest of the shader
 return output;
}

Back to the drawing code, add the following two lines before anything else in the Draw() function. They compute the final value of the displacement, by summing the gravity and the other force (which for now remains zero). After that, the value is set on the effect parameter with the same name.

displacement = gravity + forceDirection;
furEffect.Parameters["Displacement"].SetValue(displacement);

The result is hair affected by gravity.

Now add a new line at the top of the Draw() function, to modify the forceDirection based on time, and look at the animating fur.

forceDirection.X = (float)Math.Sin(gameTime.TotalGameTime.TotalSeconds) * 0.5f;

You can experiment more by modifying the forceDirection or gravity vectors, or my applying some rotations to the model (furEffect.Parameters["World"]) and see how the fur behaves.

The complete code for the sample (including textures, fake inter-fur shadows, height variations and animations) can be downloaded here.

Applying Fur on a Model

We will apply the fur shader on a 3D model, and use the model's own textures for coloring the fur. Note: the methods used from now on are not the recommended ones. The best approach would be to crate a new Content Processor, and maybe even a custom Model class, and use those on the model that you wish to have fur. However, these operations are beyond the scope of this tutorial.

Add the model of the dinosaur to your Content folder, and make sure the associated textures (dino.png and eyes.png) are inside the same folder as the model. The files can be found in this archive:dino.zip.

Now we need to add a variable to hold the model, and initialize it inside LoadContent().

//dino model
Model dino;

protected override void LoadContent()
{
 dino = Content.Load<Model>("dino");
 [...]
}

We will need two functions. One of them simply draws the geometry of the model. It takes as parameters the model that should be drawn, the collection of bones that will be used, and the effect that will be on the device. It sets the values of the World matrix on the effect, and that of the Texture parameter, so the texture used to color the model's fur will be the same as the texture set on the model.

private void DrawModelGeometry(Model model, Matrix[] bones, Effect effect)
{
 foreach (ModelMesh mesh in model.Meshes)
 {
 effect.Parameters["World"].SetValue(bones[mesh.ParentBone.Index]); //set World Matrix
 foreach (ModelMeshPart meshpart in mesh.MeshParts)
 {
 effect.Parameters["Texture"].SetValue(((BasicEffect)meshpart.Effect).Texture); //set Texture
 effect.CommitChanges(); //commit changes
 graphics.GraphicsDevice.VertexDeclaration = meshpart.VertexDeclaration;
 graphics.GraphicsDevice.Vertices[0].SetSource(mesh.VertexBuffer, meshpart.StreamOffset, meshpart.VertexStride);
 graphics.GraphicsDevice.Indices = mesh.IndexBuffer;
 //draw the geometry
 graphics.GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, meshpart.BaseVertex, 0,
 meshpart.NumVertices, meshpart.StartIndex, meshpart.PrimitiveCount);
 }
 }
}

The seconds function applied the fur rendering algorithm on the model, using code similar to what we used previously to apply fur to a single polygon.

private void DrawFurModel(Model model)
{
 Matrix[] bones = new Matrix[model.Bones.Count];
 model.CopyAbsoluteBoneTransformsTo(bones);

 furEffect.Parameters["Displacement"].SetValue(displacement);
 furEffect.Parameters["MaxHairLength"].SetValue(maxHairLength);
 furEffect.Parameters["FurTexture"].SetValue(furTexture);

 furEffect.Parameters["View"].SetValue(camera.View);
 furEffect.Parameters["Projection"].SetValue(camera.Projection);
 furEffect.Parameters["MaxHairLength"].SetValue(maxHairLength);
 furEffect.Parameters["FurTexture"].SetValue(furTexture);

 furEffect.Begin();
 for (int i = 0; i < nrOfLayers; i++)
 {
 furEffect.Parameters["CurrentLayer"].SetValue((float)i / nrOfLayers);
 furEffect.CommitChanges();
 furEffect.CurrentTechnique.Passes[0].Begin();
 //draw geometry of current layer
 DrawModelGeometry(model, bones, furEffect);
 furEffect.CurrentTechnique.Passes[0].End();
 }
 furEffect.End();
}

After tweaking some values, like the dimension of the fur texture and the force and gravity vectors, a simple call to DrawFurModel(dino); inside the Draw() function will result in the image seen below.

By adding simple N*L lighting in the fur shader (illustrated in the accompanying source code), the quality of the scene is further improved.

Dinofur

The source code for the fur rendering applied to a model can be found here.

Conclusions

This tutorial showed how to render fur, and apply several effects on it, and finally apply it on a model. I hope you find it useful and entertaining.

The final source code archives are:

About Captain ZSquare

Microsoft XNA MVP
Comments (2) Trackbacks (1)
  1. AMAZING !
    This is a VERY useful tutorial, it removes the mystery from the cool looking effects we see in games :)

  2. Catalin, your tutorial is nicely done.

    Have you tried adding the Banks/Kay-Kajiya hair lighting model? I found that to be the key to getting good looking fur, especially as the light moves.

    See http://jedwork.com/jed/papers/LPFH-fur-2001.pdf for more details.


Leave a comment