Geometry trails / Tire Tracks Tutorial

Lot’s of T’s in the title :O

trail3

Hi guys and girls,

today I’d like to talk a little bit about geometry trails (is this even the right name?), which can replace particles and be used for example for trails left by space ship engines or tire tracks in a racing game.

Since it’s a fairly easy thing to implement but for some reason not many tutorials can be found on the topic I decided to write up a little bit about it. As you might have learned from past blog entries I really enjoy writing about that stuff, even if the write-up takes longer than the sloppy implementation.

The code bits and pieces are in c# and were implemented in MonoGame.

tracks

In my case I use this stuff for tire tracks – see how the cars leave tracks in the sand!

Idea

So the basic idea is: Let’s create a strip of triangles that “follow” a certain path (for example the cursor, or a ship, a car etc.)

We do not want to animate the triangles – only the last segment stretches a bit until it reaches our defined maximum length. Then we remove the oldest segment and create a new one at the front.

trail_simple

In this example I limit the trail to 3 segments.

In-engine this would look like this:

trail

It’s easy to notice that the curves are not really smooth yet. We have to change the direction of the segment before the one we currently draw to face the half-vector between the direction to the second-last segment and the new segmenttrail_simple2

Now we get stuff like this

trail2
Finally we want the trail to fade out smoothly in the end.

The idea is pretty simple.

Let’s say we want to fade out over 2 segments (we can also use a world unit length and convert it to segment amount).

Our trail has a visibility term (alpha) which goes from 0 (transparent) to 1 (fully visible).

If we all our segments have full length then it’s pretty easy:
Our 3 first segments have visibility:

0 – – – 0,5 – – – 1 – – – 1 – – – 1 ….
^            ^             ^          ^          ^
# 0            1             2            3          4   …

Makes sense right?

But what we really want is a smooth fade depending on the length of the newest (not full-length) segment.
Let’s say that it has reached half the length of a full segment … where does our ramp start and end?

Well obviously half way between segment 0 and 1 and it finished half way between segment 2 and 3.

So thing is basically just some simple linear math.

graph

To get the visibility at our current segment i we can use this formula:

y = 1/fadeOutSegments * x – percentOfFinalSegment

If we want the ramp to start somewhere in decimal numbers we have to use the range {-1, 2} for our visibility term and then clamp to {0, 1} in the pixel shader.

Because our graphics card only accepts floats between 0 and 1 we “encode” our y value like this

visibilty = (visibility + 1)/3.0f

to map from {-1,2} to {0,1}. Later we can decode the other way around.

trail3[6]

Looks pretty smooth, right?

Final Modification

So that’s basically it.

Now we need to bring it to the screen and there are just a few things left to say.

First of all – your trails don’t have to have equidistant segment points. It makes sense to make more, smaller segments when processing curves and use larger ones when having a long straight line.

Another thing – if you want to have floating trails, for example lines following some projectile, it would be a good idea to modify the position of both vertices (per segment) in our vertex shader so they always face the camera (like billboards, stuff like lens flares, distant trees etc.)

If we use them as tire tracks it would be a good idea to project them onto our geometry.
Here is a great blog article about decal projection (by David Rosen from Wolfire games)

http://blog.wolfire.com/2009/06/how-to-project-decals/

This is not trivial and, depending on geometry density, not cheap either – but it is the proper way!

If you happen to work with a deferred engine making decals can be easier, there are tons of good resources if you search for “deferred decals” :)

In my case I went a different route.

Since I know I only want to have tire tracks on terrain I simply draw the terrain and then draw the lines on top without any depth check. Since the terrain is rather low frequency it’s a pretty plausible looking solution.

Afterwards I draw all the other models. The obvious downside to this method is that I have a little bit of additional overdraw since I draw the terrain before drawing the models that obstruct/hide parts of it.

However, the effect on frame time is really minimal and the effort of implementing the thing is really low, so I take that.
With the visibility term I can also ensure that cars that currently do not touch ground do not contribute a visible tire track, which is pretty useful.

tracks_final

Code

Let’s initialize our class

public class Trail
{

        //our buffers
        private DynamicVertexBuffer _vBuffer;
private IndexBuffer _iBuffer;

        private TrailVertex[] vertices;

        //How many segments are already initialized?
private int _segmentsUsed = 0;

        //How many segments does our strip have?
   private int _segments;

        //How long is each segment in world units?
private float _segmentLength;

        //The world coordinates of our last static segment end
private Vector3 _lastSegmentPosition;

       //If we fade out – over how many segments?
        private int fadeOutSegments = 4;

        private float _width = 1;

        private float _minLength = 0.01f;

[…]

public Trail(Vector3 startPosition, float segmentLength, int segments, float width, GraphicsDevice graphicsDevice)
{
_lastSegmentPosition = startPosition;
_segmentLength = segmentLength;
_segments = segments;
_width = width;

            _vBuffer = new DynamicVertexBuffer(graphicsDevice, TrailVertex.VertexDeclaration, _segments*2, BufferUsage.None);
_iBuffer = new IndexBuffer(graphicsDevice, IndexElementSize.SixteenBits, (_segments-1)*6, BufferUsage.WriteOnly);

            vertices = new TrailVertex[_segments*2];

            FillIndexBuffer();
}

        private void FillIndexBuffer()
{
short[] bufferArray = new short[(_segments-1)*6];
for (var i = 0; i < _segments-1; i++)
{
bufferArray[0 + i*6] = (short) (0 + i*2);
bufferArray[1 + i * 6] = (short)(1 + i * 2);
bufferArray[2 + i * 6] = (short)(2 + i * 2);
bufferArray[3 + i * 6] = (short)(1 + i * 2);
bufferArray[4 + i * 6] = (short)(3 + i * 2);
bufferArray[5 + i * 6] = (short)(2 + i * 2);
}

            _iBuffer.SetData(bufferArray);
}

Pretty simple so far right?

We use a dynamic vertex buffer where we store the vertex information. A dynamic vertex buffer plays nicely with our goal of changing the geometry constantly.
On the other hand we do not need a dynamic index buffer since the relationship of the vertices always stays the same, so we can initialize it from the start. (Actually we don’t have to do that for each instance of our trail, we can make the index buffer static if we use the same amount of segments/vertices for all our trails/tracks).

Now let’s move to the other 2 parts that are pretty trivial – the draw() function and a dispose() function (since graphics recourses are not handled by our garbage collector we need to delete them manually)

public void Draw(GraphicsDevice graphics, Effect effect)
{
_vBuffer.SetData(vertices);
effect.CurrentTechnique = effect.Techniques[“TexturedTrail”];
graphics.SetVertexBuffer(_vBuffer);
graphics.Indices = _iBuffer;

effect.CurrentTechnique.Passes[0].Apply();
graphics.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, _segmentsUsed*2);
}
public void Dispose()
{
_vBuffer.Dispose();
_iBuffer.Dispose();
}

I think these 2 should make sense, right?

Now comes the main part – the Update() class, which we call from our target with the new position.

public void Update(Vector3 newPosition, float visibility)
{
if (!GameSettings.DrawTrails) return;

            //Initialize the first segment, we have no indication for the direction, so just displace the 2 vertices to the left/right
            if (_segmentsUsed == 0)
{
vertices[0].Position = _lastSegmentPosition + Vector3.Left;
vertices[0].TextureCoordinate = new Vector2(0,0);

                vertices[1].Position = _lastSegmentPosition + Vector3.Right;
vertices[1].TextureCoordinate = new Vector2(0, 1);

                _segmentsUsed = 1;
}

            Vector3 directionVector = newPosition – _lastSegmentPosition;
float directionLength = directionVector.Length();

            //If the distance between our newPosition and our last segment is greater than our assigned
// _segmentLength we have to delete the oldest segment and make a new one at the other end
            if (directionLength > _segmentLength)
{
Vector3 normalizedVector = directionVector / directionLength;

                //normal to the direction. In our case the trail always faces the sky so we can use the cross product
//with (0,0,1)
                Vector3 normalVector = Vector3.Cross(Vector3.UnitZ, normalizedVector);

               //how many segments are we in?
                int currentSegment = _segmentsUsed;

                //if we are already at max #segments we need to delete the last one
                if (currentSegment >= _segments – 1)
{
ShiftDownSegments();
}
else
{
_segmentsUsed++;
}

                //Update our latest segment with the new position
                vertices[currentSegment*2].Position = newPosition + normalVector*_width;
vertices[currentSegment * 2].TextureCoordinate = new Vector2(1, 0);
vertices[currentSegment * 2+1].Position = newPosition – normalVector*_width;
vertices[currentSegment * 2+1].TextureCoordinate = new Vector2(1, 1);

                //Fade out
//We can’t have more fadeout segments than initialized segments!
                int max_fade_out_segments = Math.Min(fadeOutSegments, currentSegment);

                for (var i = 0; i < max_fade_out_segments; i++)
{
//Linear function y = 1/max * x – percent. Need to check with prior visibility, might be lower (if car jumps for example)
                    float visibilityTerm = Math.Min(1.0f / max_fade_out_segments * i, DecodeVisibility(vertices[i * 2].Visibility));
visibilityTerm = EncodeVisibility(visibilityTerm);

                    vertices[i * 2].Visibility = visibilityTerm;
vertices[i * 2 + 1].Visibility = visibilityTerm;
}

                //Our last segment’s position is the current position now. Go on from there
_lastSegmentPosition = newPosition;


}
//If we are not further than a segment’s length but further than the minimum distance to change something
//(We don’t wantto recalculate everything when our target didn’t move from the last segment)
//Alternatively we can save the last position where we calculated stuff and have a minimum distance from that, too.
            else if (directionLength > _minLength)
{
Vector3 normalizedVector = directionVector/directionLength;

                Vector3 normalVector = Vector3.Cross(Vector3.UnitZ, normalizedVector);

                int currentSegment = _segmentsUsed;

                vertices[currentSegment * 2].Position = newPosition + normalVector*_width;
vertices[currentSegment * 2].TextureCoordinate = new Vector2(1, 0);
vertices[currentSegment * 2].Visibility = EncodeVisibility(visibility);
vertices[currentSegment * 2 + 1].Position = newPosition – normalVector*_width;
vertices[currentSegment * 2 + 1].TextureCoordinate = new Vector2(1, 1);
vertices[currentSegment * 2 + 1].Visibility = EncodeVisibility(visibility);

                //We have to adjust the orientation of the last vertices too, so we can have smooth curves!
                if (currentSegment >= 2)
{
Vector3 directionVectorOld = vertices[(currentSegment – 1) * 2].Position –
vertices[(currentSegment – 2) * 2].Position;

                    Vector3 normalVectorOld = Vector3.Cross(Vector3.UnitZ, directionVectorOld.NormalizeLocal());

                    normalVectorOld = normalVectorOld + (1 – Vector3.Dot(normalVectorOld, normalVector).Saturate())*normalVector;

                    normalVectorOld.Normalize();

                    vertices[(currentSegment – 1) * 2].Position = _lastSegmentPosition + normalVectorOld * _width;
vertices[(currentSegment – 1) * 2 + 1].Position = _lastSegmentPosition – normalVectorOld * _width;
}

               // Visibility

                //Fade out the trail to the back
                int max_fade_out_segments = Math.Min(fadeOutSegments, currentSegment);

                //Get the percentage of advance towards the next _segmentLength when we need to change vertices again
                float percent =  directionLength/_segmentLength / max_fade_out_segments;

                for (var i = 0; i < max_fade_out_segments; i++)
{
//Linear function y = 1/max * x – percent. Need to check with prior visibility, might be lower (if car jumps for example)
                    float visibilityTerm = Math.Min(1.0f/max_fade_out_segments*i – percent, DecodeVisibility(vertices[i*2].Visibility));
visibilityTerm = EncodeVisibility(visibilityTerm);

                    vertices[i*2].Visibility = visibilityTerm;
vertices[i * 2 + 1].Visibility = visibilityTerm;
}

            }

        }

I hope that is relatively clear. The helper functions used are here:

private float EncodeVisibility(float visibility)
{
return (visibility + 1)/3.0f;
}

private float DecodeVisibility(float visibility)
{
return (visibility * 3) – 1.0f;
}

private void ShiftDownSegments()
{
for (var i = 0; i < _segments-1; i++)
{
vertices[i*2] = vertices[i*2 + 2];
vertices[i*2 + 1] = vertices[i*2 + 3];
}

  }

Our Vertex Declaration looks like this

public struct TrailVertex
{
// Stores the starting position of the particle.
public Vector3 Position;

        // Stores TexCoords
public Vector2 TextureCoordinate;

        // Visibility term
public float Visibility;

        public static readonly VertexDeclaration VertexDeclaration = new VertexDeclaration
(
new VertexElement(0, VertexElementFormat.Vector3,
VertexElementUsage.Position, 0),
new VertexElement(12, VertexElementFormat.Vector2,
VertexElementUsage.TextureCoordinate, 0),
new VertexElement(20, VertexElementFormat.Single,
VertexElementUsage.TextureCoordinate, 0)
);

    }

The final remaining part is the HLSL code. Here you go

float4x4 WorldViewProjection

float4 GlobalColor;

struct VertexShaderTexturedOutput
{
float4 Position : SV_POSITION;
float2 TexCoord : TEXCOORD0;
float4 Color : COLOR0;
};

Texture2D texMapLine;
sampler LinearSampler = sampler_state
{
MinFilter = linear;
MagFilter = Point;
AddressU = Wrap;
AddressV = Wrap;
};
// Simple trails

VertexShaderTexturedOutput VertexShaderTrailFunction(float4 Position : SV_POSITION, float2 TexCoord : TEXCOORD0, float Visibility : TExCOORD1)
{
VertexShaderTexturedOutput output;

float4 worldPosition = mul(Position, WorldViewProjection);

float vis = saturate(Visibility * 3 – 1);
output.Color = GlobalColor * vis * float4(0.65f,0.65f,0.65f,0.5f);
output.TexCoord = TexCoord;
return output;
}

float4 PixelShaderTrailFunction(VertexShaderTexturedOutput input) : SV_TARGET0
{
float4 textureColor = 1-texMapLine.Sample(LinearSampler, input.TexCoord);
return input.Color * textureColor;
}

technique AmbientTexturedTrail
{
pass Pass1
{
VertexShader = compile vs_5_0 VertexShaderTrailFunction();
PixelShader = compile ps_5_0 PixelShaderTrailFunction();
}
}

Advertisements

One thought on “Geometry trails / Tire Tracks Tutorial

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s