Around The World, Part 21: Visibility

In a game focused on exploration, we need to have a way to decide what the player can and cannot see. With a first-person or third-person 3D view, visibility comes for free: anything below the horizon, and anything behind something else, is not visible. With a top-down perspective, we need to do some more work.

Visibility

Here’s the test scene I’ll be using:

A test scene without any visibility limitations

Please ignore the black areas in the corners, which are due to a bug in my camera calculations that I’ll fix later. The player’s ship is in the centre, but it’s so small it’s barely visible. I’ll have to look into that later too.

The simplest way to restrict visibility is to limit the camera zoom, so you simply cannot zoom out farther to see more. Since the view is always centered on the player’s ship, this effectively limits how far they can see. However, because the screen is typically not square, this restricts visibility more in the vertical direction than the horizontal; also, the amount of restriction would depend on the screen aspect ratio. Rather, we want some kind of circular visibility.

Of course, this is not a new problem, and games have been solving it at least since the early real-time strategy games like Dune II:

A screenshot of Dune II, showing the initial map

The areas not visible to the player are simply rendered in black here. More precisely, in RTS games, areas unexplored by the player are rendered in black, but that’s not what I want for my game. I’ll just make everything black that isn’t currently visible.

Thanks to my newly gained experience with Godot’s compositor effects, it’s relatively simple to implement this:

A screenshot with circular visibility

However, this restricts visibility to a circle at the horizon. Wouldn’t it be fun if, just like in a first-person view, things farther away than the horizon would also be visible, if they were tall enough, such as mountains and volcanic islands? So let’s add that! Using some vector algebra and trigonometry, it’s not too hard to compute whether a given point is above or below the horizon:

A screenshot with visibility depending on height

And… that’s cool, but there is a problem. The calculations are based on a typical carrack of the time, with a lookout standing in the crow’s nest 25 meters above the water level. The equation to compute the distance to the horizon is easy to derive using Pythagoras’s theorem:

d = sqrt(2*R*h + h²)

where R is the radius of the planet and h is the height above sea level. (If h is small relative to R, we can omit the term.) With our planet being 1% the size of Earth, this puts the horizon at 1.8 km away. That is, we can see the surface of the ocean up to 1.8 km away, but may be able to see taller things beyond that.

Let’s say that a mountain can be at most 10 km tall. (The tallest mountain on Earth, Mount Everest, stands at 8.8 km.) The tip of such a mountain would then be visible from almost 39 km away! That means it would be very far outside our little horizon circle; you’d have to zoom out until the horizon circle occupies only a fraction of the screen, before you can spot that mountain in the distance. And the game would have to generate and render terrain at huge distances to make this work.

Fortunately, when scaling down the planet to 1% the size of Earth, I also scaled terrain heights down to 10% size. (This should of course also have been 1%, but that would reduce mountains to mere hills in comparison to the ship.) So actually, our mountain is at most 1000 meters tall. This makes it visible from about 13 km away, which is already better, but still a lot compared to our 1.8 km horizon radius.

So it’s time to cheat, and scale down terrain height only in the visibilty calculations. But by what factor? Let’s take it from the other side: from how far away do we want a 1 km tall mountain to be visible? We can make this value configurable directly for the game developer (me), and have the game code figure out the right scale factor. If we set the distance to a reasonable 5 km, that works out to a maximum mountain height of 81 meters, i.e. a scale factor of 0.081 on top of the 10% we have already:

Same, but with heights scaled down

That seems a bit too tame, but maybe these mountains aren’t actually very tall; I haven’t checked. At least now I have a meaningful value to adjust. (And in the remainder of this post, you’ll see that I did adjust it.)

Blue is the new black

Even though you can’t go far wrong with black, it looks rather dull, especially if a large portion of the screen is filled with it. In reality, you would see the sky above the horizon. So how about using the sky colour instead of black?

As it happens, during my excursion down the third-person rabbit hole in the last post, I wrote some shader code to compute sky colours and atmospheric scattering. So I can reuse that here – it hasn’t been in vain after all!

First, let’s compute the sky colour just above the horizon, and use that instead of black. This is a good opportunity to show off the day-night cycle as well, which has been in the game forever:

I think this looks fairly good, but I also added some debanding noise after capturing this video.

(You’ll notice that it’s not completely dark before sunrise and after sunset. That’s because there is also a moon, which is currently always full, and whose position is not overridden by the debug controls.)

Aerial perspective

When looking at a distant object, such as a mountain, you’ll notice that it appears blueish and washed out. This phenomenon is called, somewhat confusingly, aerial perspective. The washing out happens because the light from the mountain is partially absorbed and scattered before it reaches your eye. The blueish tint is caused by light from the sun being scattered into the line of sight; light towards the blue end of the spectrum is more prone to scattering.

Godot has fake aerial perspective built in, but the problem is that it works from the point of view of the camera, where we want it to work from the point of view of the ship. There’s no way to override that, so we have to build our own. Fortunately, with the sky shader, all the pieces are already there, so it’s just a few lines of code:

Screenshot with aerial perspective applied

It adds a nice sense of distance. Because of the 1% planet scale, I had to strengthen the effect by a factor of 30 to make it look like this. Realistically that should have been a factor of 100, but that turns out to be too strong.

Shadows

Godot offers shadow rendering by default, but I had to tweak the values quite a bit to make them work well at these scales. They make a big difference:

Screenshot with added shadows

To wrap things up, here’s a video of exploring a bay in the early morning:

I do see some problems already, such as that the aerial perspective effect is also applied at full strength in shadows, where inscattering should be less. I’m not yet sure how to fix that, so I’ll leave it at this for now. The game still looks better now than it did this morning!