# Trail Renderer

I could not, for the life of me, find a decent tutorial on how to make a weapon trail. I ended up coding my own trail renderer, which you can find the source files for below.

I used this code to create the trails on Zandra's sword:

This technique's concept is to gradually create a mesh behind something moving, to leave a trail. You need to tell a trail to Move() to a new point every game update, and it will create a segment from it's current front to the new position. Now, for anything moving fast, that would result in a very chunky trail. The player would be able to see all the vertices cleary. So this trail uses an interpolation technique call a Catmull-Rom spline. Much like Quaternions, this is something you don't really need to understand the theory behind, you just need to know how to use it. Almost any game engine will have an implementation of this, like XNA/MonoGame's Vector3.CatmullRom(). If you supply this method 4 control points and an interpolation percent (0.0f - 1.0f), it will return an interpolated point that lies between your middle two points. It's like using a Vector3 lerp, except it's for curves.

This brings up one issue, though: If we need two points on either side of the intepolation, how do we interpolate between the most recent two locations? The answer is that we don't, we interpolate one frame behind the head of the trail. The last two points are a straight line. On the next frame, when you get another point, you go back and interpolate between the last two points. This may sound wonky. But I guarantee, when a game is rendering at 30-60 frames per second, it won't make a difference. Just look at the gif above. It's hard to notice even if you're looking for it. You'd have pause the gif to see it:

You can see the un-curved space in between Zandra's sword and the rest of the trail. Not a big deal.

## Trail.cs

This is the main logic for the technique. The UpdateSegments() and UpdateVertices() are the two methods you need to understand. Extend this class to create a useable trail object, the child class is the one responsible for setting the texture effect with SetEffect() and calling Move() each frame.

```

/*
* Renders a dynamic trail with a texture mapped to it.
* Uses a Catmull-rom spline to smooth the trail out and
* make it look nice. Each frame, Move() needs to be called
* NUM_SMOOTH_POINTS
* is how many points will be added in between those positions
* for smoothing.
*/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace MGB.Trails
{
public abstract class Trail
{
private const float MAX_ENDING_LENGTH = .2f;
private const int NUM_SMOOTH_POINTS = 4;

protected MainGame Game;
public bool Dead { get; private set; }
private bool dying = false;
protected bool Dying { get { return dying; } }

public static BlendState overlapCompensationBlend = new BlendState()
{
AlphaBlendFunction = BlendFunction.Max,
ColorBlendFunction = BlendFunction.Max,
AlphaSourceBlend = Blend.One,
ColorSourceBlend = Blend.One,
ColorDestinationBlend = Blend.One,
};

private bool visible = true;
private bool smoothing = false;
private double lifeCounter;
private bool timed = false;
protected int usedSegments = 0;
//length of the trail, expressed in segments
protected int trailLength;
private TrailSegment[] controlPoints = new TrailSegment[4];
//queue of new segments to add

protected int primitiveCount;
protected VertexPositionTexture[] vertices;
protected Effect effect;
private EffectParameter viewParam;
private EffectParameter projectionParam;
private EffectParameter depthMapParam;
protected BlendState blend = BlendState.AlphaBlend;
protected CameraComponent camera;
public Trail(MainGame game, int length, float radius, bool smooth)
{
this.Game = game;
this.smoothing = smooth;
//This assumes you have an object somewhere called CameraComponent, that at least handles your
//View and Projection matrices.
camera = (CameraComponent)game.Services.GetService(typeof(CameraComponent));

this.trailLength = length;
this.originalTrailLength = trailLength;
Init();
}

protected void SetEffect(string effectName)
{
effect = (Game.Services.GetService(typeof(TrailManager)) as TrailManager).GetEffect(effectName);
viewParam = effect.Parameters["View"];
projectionParam = effect.Parameters["Projection"];
depthMapParam = effect.Parameters["DepthMap"];
}

private void RecycleQueuedSegments()
{
while (curNode != null)
{
curNode = curNode.Next;
}
}
private TrailSegment GetNewQueueSegment()
{
TrailSegment retSeg;
if (recycledSegments.First != null)
{
retSeg = recycledSegments.First.Value;
recycledSegments.RemoveFirst();
}
else
{
}
return retSeg;
}
protected virtual void Init()
{
if (vertices == null)
{
vertices = new VertexPositionTexture[originalTrailLength * 2];
primitiveCount = (originalTrailLength - 1) * 2;
}

RecycleQueuedSegments();

int iIndex = 0;

for (int i = 0; i < originalTrailLength; i++)
{
//newSegment.Position = new Vector3(0, 0, 100);

VertexPositionTexture vVertex = new VertexPositionTexture();

vVertex.Position = newSegment.Position;
vertices[iIndex] = vVertex;

vVertex.Position = newSegment.Position;
vertices[iIndex + 1] = vVertex;

iIndex += 2;
}

for (int i = 0; i < controlPoints.Length; ++i)
{
}
}

{
this.lifeCounter = millis;
timed = true;
}

public virtual void Update(GameTime gameTime)
{
if (timed)
{
lifeCounter -= gameTime.ElapsedGameTime.TotalMilliseconds;
if (lifeCounter <= 0)
{
KillTrail();
timed = false;
}
}

{
UpdateVertices();
}
}

public void Move(Vector3 pos, Vector3 right)
{
TrailSegment seg = GetNewQueueSegment();
seg.UpdateData(pos, right, Vector3.Up);
}

public void Move(Vector3 pos, Vector3 right, Vector3 normal)
{
TrailSegment seg = GetNewQueueSegment();
seg.UpdateData(pos, right, normal);
}

public void HideTrail()
{
visible = false;
}

public void ShowTrail()
{
visible = true;
}

public void KillTrail()
{
dying = true;
}

public void Reset()
{
segments.Clear();
Init();
dying = false;
trailLength = originalTrailLength;
}

{
if (segments.Count == 0)
return;

if (dying && usedSegments >= trailLength)
{
{
{
}
}
segments.Remove(toRemove);
--trailLength;
--usedSegments;
}

if (segments.Count == 0)
{
return;
}

//makes the trail close in on its head when it is not moving anymore
{
if (f == null)
{
f = segments.Last;
}

Move(f.Value.Position, f.Value.Right);
}
/*
* smooths one vertex behind the head. It has to be this way to use a catmull rom spline, because you need
* two control points before and after the interpolated point. This is rendered quickly enough that it isnt noticable.
* (could also look into Bezier curves)
*/
while(curNode != null)
{
if (smoothing)
{
//control points for interpolation.
//move all the control points up one index,
//and add the new control point to the end.
controlPoints[0].Copy(controlPoints[1]);
controlPoints[1].Copy(controlPoints[2]);
controlPoints[2].Copy(controlPoints[3]);
controlPoints[3].UpdateData(curNode.Value.Position, curNode.Value.Right, curNode.Value.Normal);
}
TrailSegment seg;
if (smoothing && usedSegments >= controlPoints.Length && controlPoints[1].Right != controlPoints[2].Right)
{
//the head is currently one behind the newest point. move to the point before that, because we are adding
//vertices between control points 2 and 3.
for (int j = 1; j <= NUM_SMOOTH_POINTS; ++j)
{
float lerpAmt = (float)j / ((float)(NUM_SMOOTH_POINTS + 1.0f));

Vector3 lerpPos = Vector3.CatmullRom(controlPoints[0].Position, controlPoints[1].Position, controlPoints[2].Position, controlPoints[3].Position, lerpAmt);
Vector3 lerpDir = Vector3.CatmullRom(controlPoints[0].Right, controlPoints[1].Right, controlPoints[2].Right, controlPoints[3].Right, lerpAmt);
Vector3 lerpNormal = Vector3.CatmullRom(controlPoints[0].Normal, controlPoints[1].Normal, controlPoints[2].Normal, controlPoints[3].Normal, lerpAmt);
seg.UpdateData(lerpPos, lerpDir, lerpNormal);

if (usedSegments < trailLength)
{
++usedSegments;
}
}

seg.UpdateData(controlPoints[2].Position, controlPoints[2].Right, controlPoints[2].Normal);
}

seg.UpdateData(curNode.Value.Position, curNode.Value.Right, curNode.Value.Normal);

if (usedSegments < trailLength)
{
++usedSegments;
}

curNode = curNode.Next;
}

RecycleQueuedSegments();
}

{
if (next == null)
{
next = segments.First;
}
return next;
}

{
if (cur == null)
{
return null;
}

if (prev == null)
{
prev = segments.Last;
}
return prev;
}

protected override void UpdateVertices()
{
TrailSegment curSegment;
TrailSegment prevSegment;
int vertexIndex = 0;

if (curNode == null)
{
curNode = segments.Last;
}

int segmentIndex = vertexIndex / 2;
do
{
segmentIndex = vertexIndex / 2;
curSegment = curNode.Value;
prevSegment = GetNext(curNode).Value;

vertices[vertexIndex].Position = curSegment.Position - curSegment.Right * curSegment.Radius;
vertices[vertexIndex + 1].Position = curSegment.Position + curSegment.Right * curSegment.Radius;

float fStep = (segmentIndex + 1) * (TextureRepetition / trailLength);
if (fStep == 1)
{
fStep = .99f;
}
vertices[vertexIndex].TextureCoordinate.X = 0;
vertices[vertexIndex].TextureCoordinate.Y = fStep;
vertices[vertexIndex + 1].TextureCoordinate.X = .99f;
vertices[vertexIndex + 1].TextureCoordinate.Y = fStep;

vertexIndex += 2;

{
curNode = null;
break;
}

curNode = GetPrev(curNode);
}
while (curNode != null && segmentIndex < usedSegments - 1);

int indexToCopy = vertexIndex - 2;
//if weve started destroying segments, udpate all discarded vertices to the front position
while (vertexIndex < vertices.Length)
{
vertices[vertexIndex].Position = vertices[indexToCopy].Position;
vertices[vertexIndex + 1].Position = vertices[indexToCopy + 1].Position;

vertices[vertexIndex].TextureCoordinate.X = 0;
vertices[vertexIndex].TextureCoordinate.Y = 1;
vertices[vertexIndex + 1].TextureCoordinate.X = 1;
vertices[vertexIndex + 1].TextureCoordinate.Y = 1;

vertexIndex += 2;
}
}
}

public virtual void Draw(CameraComponent camera)
{
if (visible)
{
Game.GraphicsDevice.DepthStencilState = DepthStencilState.None;
Game.GraphicsDevice.BlendState = blend;
Game.GraphicsDevice.RasterizerState = RasterizerState.CullNone;
Game.GraphicsDevice.SamplerStates[0] = SamplerState.LinearClamp;

//to render this, you need to pass in the View and Projection Matrices, as well as the Depth map if youre doing deferred rendering
viewParam.SetValue(camera.View);
projectionParam.SetValue(camera.Projection);
depthMapParam.SetValue(camera.DepthRT);
effect.CurrentTechnique.Passes[0].Apply();

Game.GraphicsDevice.DrawUserPrimitives<VertexPositionTexture>
(
PrimitiveType.TriangleStrip,
vertices,
0,
primitiveCount
);
}
}
}

public class TrailSegment
{
/*
* Data for a single segment of a trail
*/
public float Radius { get; private set; }
public Vector3 Position { get; private set; }
public Vector3 Right { get; private set; }
public Vector3 Normal { get; private set; }

{
Position = new Vector3(0);
Right = Vector3.Forward;
Normal = Vector3.Up;
}

{
this.Position = position;
this.Normal = normal;
}

public TrailSegment(TrailSegment toCopy)
{
this.Position = toCopy.Position;
this.Right = toCopy.Right;
this.Normal = toCopy.Normal;
}

public void Copy(TrailSegment other)
{
this.Position = other.Position;
this.Right = other.Right;
this.Normal = other.Normal;
}

public void UpdateData(Vector3 position, Vector3 radiusDirection, Vector3 normal)
{
this.Position = position;
this.Normal = normal;
}
}
}
```

## TrailManager.cs

TrailManager is a simple Manager class to update and draw all Trail components. It is also the place I initialize the Effect objects, for binding to the shader code.

```
/*
* A simple manager to update and draw all Trail components. Also initializes and stores
* the Effects for each kind of trail.
*/

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using bepu = BEPUutilities;
using MGB.Trails;

namespace MGB
{
public class TrailManager : GameComponent
{
public const string STR_SWORD = "sword";

private Effect texturedEffect;
private Effect vertexColorEffect;
private Dictionary<string, Effect> effects;
private CameraComponent camera;
public TrailManager(MainGame game)
: base(game)
{
this.camera = (CameraComponent)game.Services.GetService(typeof(CameraComponent));
}

public override void Initialize()
{
effects = new Dictionary<string, Effect>();

texturedEffect.Parameters["alpha"].SetValue(1.0f);

Effect swordTrail = texturedEffect.Clone();
swordTrail.Parameters["colorTint"].SetValue(new Vector3(1, 1, 1));
}

public Effect GetEffect(string name)
{
return effects[name];
}

public override void Update(GameTime gameTime)
{
while (curNode != null)
{
curNode.Value.Update(gameTime);
if (curNode.Value.Remove)
{
curNode = curNode.Next;
nodeToRemove.Value.End();
trails.Remove(nodeToRemove);
}
else
{
curNode = curNode.Next;
}
}

base.Update(gameTime);
}

{
}

public void Draw()
{
Game.GraphicsDevice.RasterizerState = RasterizerState.CullNone;
foreach (TrailComponent t in trails)
{
t.Draw(camera);
}
}
}
}
```

## TrailTextureEffect.fx

This is a basic shader to render a set of primitives. Nothing special. It uses the vertices and UV coordinates created by Trail.cs to draw what it's told.

```/*
* A basic shader to render a set of vertices with a texture mapped to it.
* Specifically for a deferred rendering engine, since we pass in a DepthMap
* and Screen Position.
*/

#include "Macros.fxh"

DECLARE_TEXTURE(Texture, 1);
DECLARE_TEXTURE(DepthMap, 2);

cbuffer Parameters : register(b0)
{
float4x4 View;
float4x4 Projection;
float3 colorTint;
float alpha;
}

struct VSIn
{
float4 Position : SV_Position;
float2 TexCoord : TEXCOORD0;
};

struct VSOut
{
float4 Position  : SV_Position;
float2 TexCoord  : TEXCOORD0;
float4 ScreenPos : TEXCOORD1;
};

VSOut VS(VSIn input)
{
VSOut output;

output.Position = mul(mul(input.Position, View), Projection);

//also output the position as a texcoord, so that it
//is interpolated correctly for figuring out position on the depth map
output.ScreenPos = output.Position;
output.TexCoord = input.TexCoord;

return output;
}

float4 PS(VSOut input) : SV_Target
{
float billboardDepth = input.ScreenPos.z / input.ScreenPos.w;

//divide by homogenous coordinate because math. This is for deferred rendering.
input.ScreenPos.xy /= input.ScreenPos.w;
//transform from [-1, 1] to [0, 1]
float2 texCoord = .5f * (float2(input.ScreenPos.x, -input.ScreenPos.y) + 1);
float sceneDepth = SAMPLE_TEXTURE(DepthMap, texCoord);
if(billboardDepth > sceneDepth)
{