Sunday, 8 July 2012

Basic HLSL Lighting Techniques - Episode 7: Simple Animation

Now this is very simple animation, no bones are used at all here. The point of this post is to show you how you can manipulate vertices in the shader. What I intend to do with this tutorial is go a little further than just lighting and finish it of with some posts around doing a multi pass shader and reusing geometry in those subsequent passes as well as some other HLSL bits and bobs I have found useful. This post may seem a little abstract because of this, but please bare with me...

In the DirectX SDK there is a simple HLSL tutorial and in there there is a technique that covers what I am going to do here, there is also a shader on the NVIDIA site that does a similar thing, I think it's called "Mr Wiggle", also the same idea is applied to the Ocean shaders I have posted before. So what we are going to look at, in it's simplest form, is this kind of animation. The great thing with this IMhO is that it's all done on the GPU, you don't have to re position the vertices on the CPU and pass them over, so making it efficient.

I first started to play about with this sort of thing when I had the idea to do a 3D type of JellyCar (I love that game!) so I started playing with this kind of animation, I started off with a cube mesh I created in Blender 3D and applied the same principles I am going to show here.

From a still you can't get a very good idea of the motion of the cube, but what you can see is how I have altered it's shape, this is done inside the shader, as I said the model is a cube. There is a small amount of motion in this shader, I have added a clip of it to the back of the tutorial clip at the top so you can see it in motion,it's not finished, still a WIP.

Simple Animation for a Models

What we are going to do is alter the existing BlinnPhong shader we did in an earlier tutorial. We are first going to add a new parameter of Time to the shader

float Time : Time;

And a function called CalcVertexAnimation (pretty sure this is the name of the method in the SDK tut too:P)

float4 CalcVertexAnimation(float4 Offset)
return float4((cos(Time+Offset.y)*.5)+Offset.x ,Offset.y,(sin(Time-Offset.y) * .5)+Offset.z,Offset.w);

Now add a call to this function in the vertex shader so that we alter the position of the incoming vertex

input.Position += CalcVertexAnimation(input.Position);

And then alter the call to the shader to pass the current time in (I multiply by 3 to speed it up a bit)

if(effect.Parameters["Time"] != null)
effect.Parameters["Time"].SetValue((float)gameTime.TotalGameTime.TotalSeconds * 3);

And TA DA!! Simple Animation... I know, it's not that awesome, but when I move onto multi pass shader and show how we can reuse the geometry, this sort of thing could be used to animate a force field around a model, well that was the implementation that sprang to my mind. But I am sure you will come up with far better applications for this sort of thing.

At the time of me playing about with this, I was on the XNA IRC and someone on there was asking for some help regarding how to go about doing a flag animation (sorry forget your name now..) So I suggested this method of doing it.

Animated Flags

What I did the first time around did the job, but, I was not happy with the way it was being lit. What is happening when we animate the vertex is that the relating normal for that vertex is not altering. This makes the lighting look unnatural, so in the shader we also have to re calculate the normal based on the animation function we have, the current vertex and it's surrounding vertices.

I am not going to go into the detail of how I created the flag it's self, it's just a plane of vertices created on the XY axis, and it's normals all set to Vecto3.Backward so 0,0,1 facing.

So the flag shader it's self is really just an Ambient light shader, that is passed a texture. As before we add a Time parameter and a CalcVertexAnimation function

float4 CalcVertexAnimation(float4 Offset)
return float4(Offset.x,Offset.y,sin(Time+Offset.z+(Offset.y-Offset.x)) * 1.5,Offset.w);

As you can see the animation is a little different, we are just moving the vertex along the Z axis by the time + the X and Y axis I am multiplying by 1.5 to give it a bit more depth to each of the waves.

The vertex shader has altered a fair bit, first thing I do is store the original position for the vertex, then set up an array of 3 positions, these three positions will give us our new normal value, but first we have to work out where they are. This will only work with this flag class as we know that our vertices are evenly spaced, this may not be the case in a model. So to get the correct normal we need to get the cross product from two sides of this vertex.

The first side is created from the current vertex position - the vertices position below this current vertex.
The second side is created from the current vertex position - the vertices position to the right of it.

We can now use these two vales to give us the current vertices normal by using the intrinsic HLSL method cross and giving it our two sides.

VS_OUT VS_ColorMap(VS_IN input)
VS_OUT output = (VS_OUT)0;
float4 orgPos = input.Position;

// Calc other vert positions to get aniamted normal.
float4 poss[3];
float4 nextPos = orgPos.xyzw;
for(int p=1;p<3;p++)
if(p == 2)
nextPos.x = orgPos.x;
nextPos.y -= .5;
nextPos.x += .5;

poss[p] = nextPos;
poss[p] += CalcVertexAnimation(nextPos);

input.Position += CalcVertexAnimation(input.Position);
poss[0] = input.Position;
output.Position = mul(input.Position ,wvp);
output.TexCoord = input.TexCoord;

// Alter for animation;
float3 side1 = poss[0] - poss[2];
float3 side2 = poss[0] - poss[1];
output.Normal = cross(side1,side2);
//output.Normal = input.Normal;

return output;

The Vertex shader is unchanged to, but here it is anyway

PS_OUT PS_ColorMap(VS_OUT input)
PS_OUT output = (PS_OUT)0;

float3 LightDir = normalize(LightDirection);
float Diffuse = saturate(dot(LightDir,normalize(input.Normal)));
float4 texCol = tex2D(ColorMapSampler,input.TexCoord);
float4 Ambient = AmbientIntensity * AmbientColor;

texCol *= Diffuse;
output.Color = Ambient + texCol;

return output;

And we then get this

I would like to thank my friend Michael Quandt for helping me sanity check the flag shader too, thanks mate :)

So, the solution to this episode can be found here.

No comments:

Post a comment