# How to follow object on CatmullRomSplines at constant speed (e.g. train and train carriage)?

I have a CatmullRomSpline, and using the very good example at https://github.com/libgdx/libgdx/wiki/Path-interface-%26-Splines I have my object moving at an even pace over the spline.

Using a simple train and carriage example, I now want to have the carriage follow the train at the same speed as the train (not jolting along as it does with my code below). This leads into my main questions:

1. How can I make the carriage have the same constant speed as the train and make it non jerky (it has something to do with the derivative I think, I don't understand how that part works)?

2. Why do I need to divide by the line length to convert to metres per second, and is that correct? It wasn't done in the linked examples?

I have used the example I linked to above, and modified for my specific example:

    private void process(CatmullRomSpline catmullRomSpline) {
// Render path with precision of 1000 points
renderPath(catmullRomSpline, 1000);
float length = catmullRomSpline.approxLength(catmullRomSpline.spanCount * 1000);

// Render the "train"
Vector2 trainDerivative = new Vector2();
Vector2 trainLocation = new Vector2();
catmullRomSpline.derivativeAt(trainDerivative, current);
// For some reason need to divide by length to convert from pixel speed to metres per second but I do not
// really understand why I need it, it wasn't done in the examples???????
current += (Gdx.graphics.getDeltaTime() * speed / length) / trainDerivative.len();
catmullRomSpline.valueAt(trainLocation, current);
renderCircleAtLocation(trainLocation);
if (current >= 1) {
current -= 1;
}

// Render the "carriage"
Vector2 carriageLocation = new Vector2();
float carriagePercentageCovered = (((current * length) - 1f) / length); // I would like it to follow at 1 metre behind
carriagePercentageCovered = Math.max(carriagePercentageCovered, 0);
catmullRomSpline.valueAt(carriageLocation, carriagePercentageCovered);
renderCircleAtLocation(carriageLocation);
}

private void renderPath(CatmullRomSpline catmullRomSpline, int k) {
// catMulPoints would normally be cached when initialising, but for sake of example...
Vector2[] catMulPoints = new Vector2[k];
for (int i = 0; i < k; ++i) {
catMulPoints[i] = new Vector2();
catmullRomSpline.valueAt(catMulPoints[i], ((float) i) / ((float) k - 1));
}

SHAPE_RENDERER.begin(ShapeRenderer.ShapeType.Line);
SHAPE_RENDERER.setColor(Color.NAVY);
for (int i = 0; i < k - 1; ++i) {
SHAPE_RENDERER.line((Vector2) catMulPoints[i], (Vector2) catMulPoints[i + 1]);
}
SHAPE_RENDERER.end();
}

private void renderCircleAtLocation(Vector2 location) {
SHAPE_RENDERER.begin(ShapeRenderer.ShapeType.Filled);
SHAPE_RENDERER.setColor(Color.YELLOW);
SHAPE_RENDERER.circle(location.x, location.y, .5f);
SHAPE_RENDERER.end();
}


To create a decent sized CatmullRomSpline for testing this out:

Vector2[] controlPoints = makeControlPointsArray();
CatmullRomSpline myCatmull = new CatmullRomSpline(controlPoints, false);

....

private Vector2[] makeControlPointsArray() {
Vector2[] pointsArray = new Vector2;
pointsArray = new Vector2(1.681817f, 10.379999f);
pointsArray = new Vector2(2.045455f, 10.379999f);
pointsArray = new Vector2(2.663636f, 10.479999f);
pointsArray = new Vector2(3.027272f, 10.700000f);
pointsArray = new Vector2(3.663636f, 10.939999f);
pointsArray = new Vector2(4.245455f, 10.899999f);
pointsArray = new Vector2(4.736363f, 10.720000f);
pointsArray = new Vector2(4.754545f, 10.339999f);
pointsArray = new Vector2(4.518181f, 9.860000f);
pointsArray = new Vector2(3.790908f, 9.340000f);
pointsArray = new Vector2(3.172727f, 8.739999f);
pointsArray = new Vector2(3.300000f, 8.340000f);
pointsArray = new Vector2(3.700000f, 8.159999f);
pointsArray = new Vector2(4.227272f, 8.520000f);
pointsArray = new Vector2(4.681818f, 8.819999f);
pointsArray = new Vector2(5.081817f, 9.200000f);
pointsArray = new Vector2(5.463636f, 9.460000f);
pointsArray = new Vector2(5.972727f, 9.300000f);
pointsArray = new Vector2(6.063636f, 8.780000f);
pointsArray = new Vector2(6.027272f, 8.259999f);
pointsArray = new Vector2(5.700000f, 7.739999f);
pointsArray = new Vector2(5.300000f, 7.440000f);
pointsArray = new Vector2(4.645454f, 7.179999f);
pointsArray = new Vector2(4.136363f, 6.940000f);
pointsArray = new Vector2(3.427272f, 6.720000f);
pointsArray = new Vector2(2.572727f, 6.559999f);
pointsArray = new Vector2(1.900000f, 7.100000f);
pointsArray = new Vector2(2.336362f, 7.440000f);
pointsArray = new Vector2(2.590908f, 7.940000f);
pointsArray = new Vector2(2.318181f, 8.500000f);
pointsArray = new Vector2(1.663636f, 8.599999f);
pointsArray = new Vector2(1.209090f, 8.299999f);
pointsArray = new Vector2(1.118181f, 7.700000f);
pointsArray = new Vector2(1.045455f, 6.880000f);
pointsArray = new Vector2(1.154545f, 6.100000f);
pointsArray = new Vector2(1.281817f, 5.580000f);
pointsArray = new Vector2(1.700000f, 5.320000f);
pointsArray = new Vector2(2.190908f, 5.199999f);
pointsArray = new Vector2(2.900000f, 5.100000f);
pointsArray = new Vector2(3.700000f, 5.100000f);
pointsArray = new Vector2(4.372727f, 5.220000f);
pointsArray = new Vector2(4.827272f, 5.220000f);
pointsArray = new Vector2(5.463636f, 5.160000f);
pointsArray = new Vector2(5.554545f, 4.700000f);
pointsArray = new Vector2(5.245453f, 4.340000f);
pointsArray = new Vector2(4.445455f, 4.280000f);
pointsArray = new Vector2(3.609091f, 4.260000f);
pointsArray = new Vector2(2.718181f, 4.160000f);
pointsArray = new Vector2(1.990908f, 4.140000f);
pointsArray = new Vector2(1.427272f, 3.980000f);
pointsArray = new Vector2(1.609090f, 3.580000f);
pointsArray = new Vector2(2.136363f, 3.440000f);
pointsArray = new Vector2(3.227272f, 3.280000f);
pointsArray = new Vector2(3.972727f, 3.340000f);
pointsArray = new Vector2(5.027272f, 3.360000f);
pointsArray = new Vector2(5.718181f, 3.460000f);
pointsArray = new Vector2(6.100000f, 4.240000f);
pointsArray = new Vector2(6.209091f, 4.500000f);
pointsArray = new Vector2(6.118181f, 5.320000f);
pointsArray = new Vector2(5.772727f, 5.920000f);
pointsArray = new Vector2(4.881817f, 6.140000f);
pointsArray = new Vector2(5.318181f, 6.580000f);
pointsArray = new Vector2(6.263636f, 7.020000f);
pointsArray = new Vector2(6.645453f, 7.420000f);
pointsArray = new Vector2(6.681817f, 8.179999f);
pointsArray = new Vector2(6.627272f, 9.080000f);
pointsArray = new Vector2(6.572727f, 9.699999f);
pointsArray = new Vector2(6.263636f, 10.820000f);
pointsArray = new Vector2(5.754546f, 11.479999f);
pointsArray = new Vector2(4.536363f, 11.599998f);
pointsArray = new Vector2(3.572727f, 11.700000f);
pointsArray = new Vector2(2.809090f, 11.660000f);
pointsArray = new Vector2(1.445455f, 11.559999f);
pointsArray = new Vector2(0.936363f, 11.280000f);
pointsArray = new Vector2(0.754545f, 10.879999f);
pointsArray = new Vector2(0.700000f, 9.939999f);
pointsArray = new Vector2(0.918181f, 9.620000f);
pointsArray = new Vector2(1.463636f, 9.600000f);
return pointsArray;
}


Disclaimer: My math is very rusty, so please explain in lay mans terms....

• possible duplicate of Determine arc-length of a Catmull-Rom spline Jun 1, 2014 at 13:58
• BTW railroads and roads mostly use circular arcs instead of catmull-rom or bezier splines. The math is simpler in some ways, especially the math for following the paths and determining lengths of sections. Jun 1, 2014 at 16:35
• Thanks amitp, that is a useful article, but I would really like to understand what is wrong with my above code. It is VERY close to working and the train itself moves at a very consistant speed. I am sure it is just something simple around the "Render the carriage" part. Jun 1, 2014 at 17:06
• msell, it is a very similar problem but this is a specific example. I have the train moving at a constant speed, it is just the carriage - it is related to the derivative for sure but I cannot see how I need to plug it in. Jun 1, 2014 at 17:10

You're using a fixed offset from the train, rather than taking into account the fact that a parameter difference of 1/length is not 1 meter along the whole spline. In places with a large derivative, this offset will be greater than 1 meter. In places with a small derivative, it will be less.

Try something like this instead:

float followOffset = 1f/trainDerivative.len();

float carriagePercentageCovered = (((current * length) - followOffset) / length);


Note that even this is only a first approximation, and you may continue to see some variation in spacing without further iterations. If this proves to be an issue, let me know in a comment and I can show you how to refine the movement further.

• Unfortunately the "carriage" just bounces backwards and forwards quite abrubtly along behind the (smoothly running) train using the suggested approach. I know you are right, and I definately need to take into account the derivative, but eveything I try just makes it look worse! I was wondering if I should be using the carriages derivative somehow? Many thanks. Jun 2, 2014 at 21:11
• DMGregory? Anyone? :S Jun 15, 2014 at 20:03