The lookAt problem

We already did some basic normal calculations in previous chapters, and that mostly worked fine. However, there is a problem when we rely too much on a simple lookAt from THREE.js.

The issue is roll, also known as rotation around the Z axis.

When we use lookAt, we have to provide an up vector. So far, we have always used (0, 1, 0). This works well for gentle slopes, but it starts to break down when the track approaches angles close to 90 degrees.

At that point, the curve direction points almost straight up. The up vector also points up. Now both vectors look into the sky, and the matrix becomes unstable.

When the track points straight up, roll is no longer uniquely defined. Rotating around that direction does not change where the track is pointing, so lookAt has no stable orientation to choose.

Mathematically, this happens because lookAt builds an orientation using cross products. When the forward direction becomes parallel, or nearly parallel, to the up vector, the cross product between them approaches zero. That means the remaining axes of the matrix cannot be constructed reliably anymore.

The result is sudden flips, jitter, or strange twisting along the track, especially around slopes close to 90 degrees.

Solution

To fix this, we need a slightly different approach. Instead of computing each orientation in isolation, we build it incrementally. We start from the matrix of the previous node and apply a small rotation that follows the curve direction. In other words, we still use a lookAt-like idea, but we guide it using the orientation from the previous node.

This way, the up direction is not reset every time. It slowly rotates along with the curve, respecting the existing roll instead of fighting against it.

The exact math and geometry behind this is not trivial, and explaining it properly would easily double the length of this chapter. For now, it is enough to think of it like this: we incrementally add rotation while respecting the orientation of the previous node.

In this chapter, we will not go into mathematical details. Instead, we focus on the implementation and simply compare the two approaches in the following interactive demo by switching the lookAt method between a fixed up direction and an incremental rotation based on the previous node: