Work The Problem: Trail Renderer

The Problem

 

I’m working on a client project that needs a trail effect on a moving game object, but requires rendering through the native JS canvas (no WebGL). So I emulated Unity’s TrailRenderer component in JS and rendered it straight using canvas context calls, and I figured I would walk the internet through the algorithms and math (it’s nothing special, you can do it).

Check out the source and the demo on GitHub.

Disclaimer: I’m not here to give you a copy/paste answer to a problem, just express how I think so that you might garner some insights as a reader.

The TrailRenderer Class

The TrailRenderer is a pretty simple class that has some basic configurations and game engine functions.

var TrailRenderer = function(width, time, minVertexDistance = 10) {

    this.width = width; // Width of the trail at the head.
    this.time = time; // The time in seconds each point on the trail lasts.
    this.minVertexDistance = minVertexDistance; // The minimum distance between vertices.

    this.points = []; // The points on the trail.
    this.times = []; // The time each point was dropped.

    // Update the trail renderer between frames.
    this.update = function(pos, timestamp) {
        ...
    }

    // Render the trail.
    this.render = function(ctx) {
        ...
    }

     // This function returns the width of the path at the specified index.
     this.evalWidth = function(index) {
         ...
     }
}

Updating the TrailRenderer

The trail is made up of a series of points dropped at parallel times, that are rendered each frame as a closed vector shape the extends out from each point. So each frame, we need to check if the head has traveled enough of a distance to lock in a new point, and also check for expiring points. We can then defer to the render function to handle drawing.

// Update the trail renderer between frames.
this.update = function(pos, timestamp) {

    var last = this.points[0]; // The point at the end of the trail.
    var first = this.points[this.points.length - 1]; // The point at the front of the trail.

    // Drop a new point if there isn't one yet or if we've travelled far enough to create a new one.
    if(!last || first.distance(pos) > this.minVertexDistance) {
        this.points.push(new Point(pos.x, pos.y));
        this.times.push(timestamp);
    }

    // Check if the points at the end of the list need to decay.
    if(this.times.length > 0) {
        while(this.times[0] < timestamp - this.time * 1000) {
            this.points.shift();
            this.times.shift();
        }
    }
}

Rendering the TrailRenderer

The render loop will get called every frame, so we need our render function to be able to run on its own given the state of the component independent of the update function. We’ll create a vector shape of sequential vertices, then fill the shape with a color. We’ll recalculate the vertices each frame, and do so by projecting along the normal perpendicular to the line between the points previous to and following a given point. TL;DR: we’ll project outwards on each side of a point to find its render verts.

// Render the trail.
this.render = function(ctx) {

    if(this.points.length > 0) {

        // Each point is made up of two verts that form the width of the trail.
        // We need to turn it into one single shape to fill.
        var vertices = [];
        this.points.forEach(function(p, i, points) {

            var prev = i > 0 ? points[i - 1] : null; // The previous point.
            var next = i < points.length - 1 ? points[i + 1] : null; // The next point.
            var w = this.evalWidth(i) / 2; // The width of the trail at this point.

            // If we're at the start of the trail, just push the point, because the width will be 0.
            if(!prev) {
                vertices.push(p);
            }
            else {

                // If we're at the end of the trail, assume two points in the same spot.
                if(next == null) {
                    next = p;
                }

                // Create a vector that represents the line perpendicular to the
                // line between the previous and next points.
                var v = new Point(next.y - prev.y, next.x - prev.x).normalize();

                // Project two points along the perpendicular vector between and
                // beyond the current point at distance w.
                var a = new Point(p.x - v.x * w, p.y + v.y * w);
                var b = new Point(p.x + v.x * w, p.y - v.y * w);

                // Splice these two points one after the other into the array between
                // the last two points, so we create a closed path.
                vertices.splice(i, 0, a, b);
            }
        }, this);

        // Push the first vertex again to close the shape.
        vertices.push(vertices[0]);

        // Pen the path to the canvas context.
        ctx.beginPath();
        ctx.moveTo(vertices[0].x, vertices[0].y);
        vertices.forEach(function(v) {
            ctx.lineTo(v.x, v.y);
        });
        ctx.fill();
    }
}

 

Problem Solved!

We’ve now successfully rendered a trail on an object. I’ll leave implementation up to you, but below are some auxiliary problems I tackled that make this system even more robust and easy.

Future Stuff

I’d like to add some things in the long run to improve the trail, which can be found in the issues section of the GitHub project (feel free to fork and take a crack at it).

  • Extend the ability to evaluate the width of the curve at a point (easing, curves, etc.).
  • Round edges with the bezier curve function of the canvas context.
  • Filling the trail with textures, patterns, gradients (maybe not possible without access to hardware?).

Anyways, thanks for reading!

Advertisements

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