Trail rendering for car skid marks

Hey there! I’m trying to create a racing game, and one effect I’d like to achieve is skid marks appearing on the ground when drifting. I’ve tried a few things with various levels of success. The first thing I tried was the built-in TrailRenderer. This had two problems for this application. The first issue is the “billboard” effect, which as far as I know can’t be turned off, and the second was that there appeared to be no way to stop the trail emitting without removing the entire trail, which is an absolute necessity.

I then found this code, which did a slightly better job, in that it didn’t render the trail as a billboard, and, with a bit of tweaking, could be made to fade the trail out. My problem with this approach, though, was that it was built to only support one trail. This caused weird artefacts to occur if you drifted more than once in quick succession, as it tried to join the trails together. I attempted to build a two-class system, one of which was a trail emitter, and one of which was a trail itself, the emitter keeping track of the trails it has spawned, but this failed spectacularly. I’m just trying to see if anyone knows of a way I could achieve this? Essentially all I need is something that emits trails, and will allow me to stop emitting them, and let them fade out, but also let me start creating a new trail without affecting the old one. Does anyone have any tips on how to go about this?

I solved this one myself. I thought I should post the code here. It uses two classes, Trail and TrailEmitter. To use the code, you attach the TrailEmitter to some object, and when you call “NewTrail()” on it, it will start emitting a trail. Calling “EndTrail()” will stop the trail, and allow you to start a new one. The Trail class is used by the TrailEmitter to actually render the trail.

Here is Trail.cs:

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

namespace Trails
{
	// Created by Edward Kay-Coles a.k.a Hoeloe
	public class Trail {
	
		//Properties of the trail
		private float width;
		private float decay;
		private Material m;
		private int rough;
		private int maxRough;
		private bool softSource;
		
		//Parent object
		private Transform par;
		
		//Pieces for the mesh generation
		private GameObject trail;
		private MeshFilter filter;
		private MeshRenderer render;
		private Mesh mesh;
		
		//Lists storing the mesh data
		private LinkedList<Vector3> verts = new LinkedList<Vector3>();
		private LinkedList<Vector2> uvs = new LinkedList<Vector2>();
		private LinkedList<int> tris = new LinkedList<int>();
		private LinkedList<Color> cols = new LinkedList<Color>();
		
		//Check if the trail is still being generated, and if it has completely faded
		private bool finished = false;
		private bool dead = false;
		
		//For registering if the object has been removed from the game (so you don't have to store it any more)
		public bool Dead
		{
			get { return dead; } 
			private set
			{
				dead = true;
				GameObject.Destroy(trail);
			}
		}
		
		//Set up the trail object, and parameters
		public Trail(Transform parent, Material material, float decayTime, int roughness, bool softSourceEdges, float wid = 0.1f)
		{
			softSource = softSourceEdges;
			maxRough = roughness;
			rough = 0;
			decay = decayTime;
			par = parent;
			width = wid;
			m = material;
			trail = new GameObject("Trail");
			filter = trail.AddComponent(typeof(MeshFilter)) as MeshFilter;
			render = trail.AddComponent(typeof(MeshRenderer)) as MeshRenderer;
			mesh = new Mesh();
			render.material = m;
			filter.mesh = mesh;
		}
		
		//Call this when the trail should stop emitting
		public void Finish()
		{
			finished = true;
		}
		
		//Tells you if the trail is emitting or not
		public bool Finished
		{
			get { return finished; }
		}
		
		// Updates the state of the trail - Note: this must be called manually
		public void Update () 
		{
			if(!finished) //Only add new segments if the trail is not being emitted
			{
				//Decides how often to generate new segments. Smaller roughness values are smoother, but more expensive
				if(rough > 0)
					rough --;
				else
				{
					rough = maxRough;
					
					//Checks in which order we should add vertices (to keep a consistent shape)
					bool odd = !(verts.Count%4 == 0);
					
					//Add new vertices as the current position
					verts.AddLast(par.position + (odd?-1:1)*par.up*width/2f);
					verts.AddLast(par.position + (odd?1:-1)*par.up*width/2f);
					
					//Fades out the newest vertices if soft source edges is set to true
					if(softSource)
					{
						if(cols.Count >= 4)
						{
							cols.Last.Value = Color.white;
							cols.Last.Previous.Value = Color.white;
						}
						cols.AddLast(Color.clear);
						cols.AddLast(Color.clear);
					}
					else //Sets the first vertices to fade out, but leaves the rest solid
					{
						if(cols.Count >= 2)
						{
							cols.AddLast(Color.white);
							cols.AddLast(Color.white);
						}
						else
						{
							cols.AddLast(Color.clear);
							cols.AddLast(Color.clear);
						}
					}
					
					if(!odd) //Set up uv mapping
					{
						uvs.AddLast(new Vector2(1,0));
						uvs.AddLast(new Vector2(0,0));
					}
					else
					{
						uvs.AddLast(new Vector2(0,1));
						uvs.AddLast(new Vector2(1,1));
					}
					
					//Don't try to draw the trail unless we have at least a rectangle
					if(verts.Count < 4) return;
					
					//Add new triangles to the mesh
					int c = verts.Count;
					tris.AddLast(c-4);
					tris.AddLast(c-3);
					tris.AddLast(c-2);
					tris.AddLast(c-4);
					tris.AddLast(c-2);
					tris.AddLast(c-1);
					
					//Copy lists to arrays, ready to rebuild the mesh
					Vector3[] v = new Vector3
;
    					Vector2[] uv = new Vector2[c];
    					int[] t = new int[tris.Count];
    					verts.CopyTo(v,0);
    					uvs.CopyTo(uv,0);
    					tris.CopyTo(t,0);
    					
    					//Build the mesh
    					mesh.vertices = v;
    					mesh.triangles = t;
    					mesh.uv = uv;
    				}
    			}
    			//The next section updates the colours in the mesh
    			int i = cols.Count;
    			
    			//If we have no vertices, don't bother trying to update
    			if(i == 0)
    				return;
    			
    			//This is for checking if the trail has completely faded or not
    			bool alive = false;
    			
    			//Essentially a foreach loop over the colours, but allowing editing to each node as it goes
    			LinkedListNode<Color> d = cols.First;
    			do
    			{
    				if(d.Value.a > 0)
    				{
    					Color t = d.Value;
    					alive = true;
    					//Decrease the alpha value, to 0 if it would be decreased to negative
    					t.a -= Mathf.Min(Time.deltaTime/decay,t.a);
    					d.Value = t;
    				}
    				d = d.Next;
    			}while(d != null);
    			
    			//Trail should be removed if it is not emitting and has faded out
    			if(!alive && finished)
    				Dead = true;
    			else
    			{
    				//Doesn't set the colours if the number of vertices doesn't match up for whatever reason
    				if(i != mesh.vertices.Length)
    					return;
    				//Copy the colours to an array and build the mesh colours
    				Color[] cs = new Color*;
    				cols.CopyTo(cs,0);
    				mesh.colors = cs;
    			}
    		}
    	}
    }

And this is TrailEmitter.cs:

    using UnityEngine;
    using System.Collections;
    using System.Collections.Generic;
    using Trails;
    
    namespace Trails
    {
    	// Created by Edward Kay-Coles a.k.a Hoeloe
    	public class TrailEmitter : MonoBehaviour {
    	
    		//Stores all live trails
    		private LinkedList<Trail> trails = new LinkedList<Trail>();
    		
    		//Parameters
    		public float width = 0.1f;
    		public float decayTime = 1f;
    		public Material material;
    		public int roughness = 0;
    		public bool softSourceEnd = false;
    		
    		//Checks if the most recent trail is active or not
    		public bool Active
    		{
    			get { return (trails.Count == 0?false:(!trails.Last.Value.Finished)); }
    		}
    		
    		// Update is called once per frame
    		void Update () 
    		{
    			//Don't update if there are no trails
    			if(trails.Count == 0) return;
    			
    			//Essentially a foreach loop, allowing trails to be removed from the list if they are finished
    			LinkedListNode<Trail> t = trails.First;
    			LinkedListNode<Trail> n;
    			do
    			{
    				n = t.Next;
    				t.Value.Update();
    				if(t.Value.Dead)
    					trails.Remove(t);
    				t = n;
    			}while(n != null);
    		}
    		
    		/// <summary>
    		/// Creates a new trail.
    		/// </summary>
    		public void NewTrail()
    		{
    			//Stops emitting the last trail and passes the parameters onto a new one
    			EndTrail();
    			trails.AddLast(new Trail(transform, material, decayTime, roughness, softSourceEnd, width));
    		}
    		
    		/// <summary>
    		/// Deactivate the last trail if it was already active.
    		/// </summary>
    		public void EndTrail()
    		{
    			if(!Active) return;
    			trails.Last.Value.Finish();
    		}
    	}
    }

I thought I'd post this here because I had such trouble finding the solution, so I thought it would be a good idea to let others see the one I found.

Hi @Hoeloe, thanks for the code, it works great.

I had to tweak it a bit because it wasn’t rendering continuously (probably winding of the triangles). I just changed it to only add new vertices after the parent moved a minimum distance and dropped the odd variable.

...         
          Vector3 currentPosition = par.transform.position + (Vector3)(par.transform.localToWorldMatrix * positionOffset);
    if (Vector3.Distance(previousPosition, currentPosition) > minimumSegmentLength) {
      previousPosition = currentPosition;
          //Add new vertices as the current position
          Vector3 offset = par.right * width / 2f;
          verts.AddLast(currentPosition - offset);
          verts.AddLast(currentPosition + offset);

...

          //Add new triangles to the mesh
          int c = verts.Count;
          tris.AddLast(c - 4);
          tris.AddLast(c - 2);
          tris.AddLast(c - 3);
          tris.AddLast(c - 3);
          tris.AddLast(c - 2);
          tris.AddLast(c - 1);