James Stanley


Nightdrive

Fri 16 September 2022
Tagged: software

I've made a JavaScript simulation of driving at night time on the motorway. It's hard to classify what it is. It's not a video, because it's generated dynamically. It's not a game, because you just watch. It's not a screensaver, because it's not the 90s. Maybe it's a "demo"?

This is something I've been planning to make for years, but kept forgetting about. I would only remember it when in the passenger seat of a car on the motorway at night time, fascinated by the patterns of moving lights.

Here's a small embedded version, if your browser runs JavaScript:

For the full version (with music!) click here: Nightdrive.

Gina said it's a great way to find out how filthy your monitor is.

The entire scene is created purely by drawing circles on a HTML5 canvas. It actually works a lot better than I expected. The biggest flaw is that the cars are totally transparent, so you can see lights from distant cars even where they should be occluded by closer cars.

Motorway simulation

The core simulation is entirely 2-dimensional. The coordinate space looks like this:

The motorway is always parallel to the y-axis.

Each car has a 2d position vector and a y-axis velocity (cars on our side having positive velocity, and cars in the oncoming carriageway having negative velocity).

400 cars are positioned at 25 metre intervals, in random lanes, at initialisation time. They also have some slight variation in the size, position, and colour of the lights. Cars in the left-most lane travel at a constant simulated speed of about 40mph, in the middle lane at 70mph, and in the fast lane at 100mph.

When any car is "behind" the viewer (lower y coordinate), and with a lower velocity (will get further behind) it is wrapped forwards by 10km. Similarly, when any car is 10km "in front of" the viewer (higher y coordinate), and with a higher velocity (will get further ahead), it is wrapped backwards by 10km.

That's enough to give the illusion of an infinite road with infinitely-many cars, travelling in both directions, and at different speeds in different lanes.

Projection

A top-down view of a 2d world isn't the effect we're going for, so we need to first make our world coordinates 3-dimensional, and then project them on to the 2-dimensional screen.

To make the coordinates 3-dimensional, I just add a "height" to every point in the 2d simulation. This works because all of the elements I need are at constant heights above the ground: car headlights, cat's eyes, and street lights.

My projection into screen space is not necessarily mathematically sound. I just fiddled with it until it looked how I wanted.

To turn a point from world coordinates into screen coordinates we first subtract the observer's position, to get a position (x,y,z) relative to the observer.

If the relative position vector has a negative y coordinate, then we know the object is behind the viewer and therefore not visible, so we don't draw anything.

Otherwise, we find the distance to the object by taking the magnitude of the relative position vector:

dist = sqrt(x2 + y2 + z2)

Then our coordinates in screen space are:

xscreen = xcentre + k * x / dist
yscreen = ycentre - k * z / dist

Where k is a scale factor that sets the size on the screen, and (xcentre, ycentre) is the centre of the screen.

(We need to subtract for yscreen because in screen space positive y is down, but in our world space positive z is up).

I found it actually worked better if I omitted the x offset from the distance calculation (so just sqrt(y2 + z2)), otherwise objects near the edge of the field of view were weirdly distorted. But I wouldn't suggest doing that in the general case.

You can look at the actual projection function I used if you like.

Terrain

To make the road a bit more interesting we can add some hills. I wrote a function adding up some arbitrary sine waves to make something that seemed reasonable:

function terrain(y) {
    return 10*Math.sin(y/1000) + 5*Math.cos(y/527) + 2*Math.sin(y/219);
}

This takes a y coordinate in road space, and returns the height of the road at that point. This is then added to all z coordinates by the projection function.

Looking at the road from the side, the terrain it generates looks something like this:

(x on the graph is y in world-space, y on the graph is z in world-space, both in metres).

Road occlusion

At this point we have quite a glaring issue: we can see through the ground!

Everything the other side of the crest of the hill should be occluded by the hill, but since all we're doing is projecting positions of lights, nothing can ever get occluded by anything.

A point P is occluded by the road if there is any position, between the viewer and P, that has a screen-projected road height "higher" (smaller value) than the screen-projected y coordinate of P. Because that means if we were to actually draw the road, it would get drawn over the top of P.

I sorted all rendered lights by distance from the viewer (closest first), and walked along the list, keeping track of the highest screen-space road height so far encountered, and occluding any points that fall "below" that height. That means we'll occlude any points that fall behind the road position at any closer position that has a light on it. This is good enough, because we always have frequent-enough sampling of the terrain function due to the density of cars and street lights.

You can look at the implementation if you like.

Come to think of it, I expect there might be an analytical solution to determining whether any given point should be occluded by the road. Maybe that would be better! We just need to find out whether the straight line passing through the camera and P intersects the road at any point between the camera and P: if it does then P is occluded by the road.

Music

The background music is from this video which describes it as "Royalty Free".

I've been told that the audio quality on Nightdrive is bad. I did this on purpose to reduce the size of the mp3, and it doesn't sound bad to me. If you think it sounds bad then you should mute Nightdrive and play that YouTube video in a different tab.

More

There are a few more things that I think would be fun to do:

Bends: Instead of having a perfectly-straight road, it would be good to add some bends, so the patterns of lights would curve off into the distance, something like this:


(From jonnywalker on flickr)

Street light occlusion: Street lights aren't just magical floating orbs, they're held up by metal poles. When something passes behind the metal pole, it is momentarily invisible. This would not be too hard to do in Nightdrive. We'd just use a similar process to the "road occlusion" to hide lights that are hidden by the poles.

Car occlusion: Cars also are not just magical floating orbs, they have volume and are opaque. This would be slightly harder than street light occlusion. Probably a first pass would be to render a black cuboid behind each car's lights, so that the cuboid blocks out anything that would be blocked by the car. This is inconvenient because currently we can only render circles.

Speed variation: Not every car in the same lane drives at exactly the same speed. Sometimes you catch up to the car in front, or the car behind catches up to you. Adding speed variation is easy, but comes with the problem that cars end up driving through each other, which for some reason feels unrealistic. This brings us on to...

Lane changes: When a car catches up to the car in front, it should indicate right and move over to overtake. When it has passed and there's a space on the left it should indicate left and move back over. We could give each car a sort of "personality" which tells it how close it can get to other cars, how long it should indicate for before changing lanes, etc.

Rain shader: A rain effect might be too much trouble to do with just a canvas. Maybe it would need WebGL. But I would like to apply some sort of rain shader to the rendered image to give the effect of driving at night time. We can imagine having a collection of "droplets" on the "windscreen" which refract the light passing through them (dots that distort the pixels underneath). Periodically we could sweep a windscreen wiper across the field of view to clear the dots, and we can randomly add dots at some rate based on how heavily it's raining.

Game: Finally, I wonder how we could turn this into some sort of game? I'm especially not looking to make a driving game. I still want the car to basically drive itself. What game can we make where the premise is that you're a passenger on the motorway at night time? It shouldn't be a particularly taxing game, I think the main experience should still be that you're just enjoying watching the lights, but it would be cool if there was some interactivity and some sort of goal.



If you like my blog, please consider subscribing to the RSS feed or the mailing list: