Friday Facts #411 - All about asteroids

Posted by Fearghall, Earendel on 2024-05-17

Welcome to Fearghall's Factorio Friday Facts! (FFFF, if you will.)

Over the last few months, you have seen asteroids a few times as a background part of some other FFFs, but for all their understated majesty, they were actually quite a complicated trick to pull off! Come with me as I take you on a 3720 to 1 journey through this asteroid field.

Asteroids do concern me

We first started with placeholder asteroids that were just the same as any other sprite - a 2D pre-rendered image with lights and shadows as part of it. This worked, but it looked pretty weird that the asteroids were all orientated the same direction, and only moved in the XY plane without rotating.

Early placeholder asteroids by v453000

So rotation is something that we were talking about going into the asteroid design process.

Asteroid designsEarendel

When we settled on 3 different asteroid types we could start on the real asteroid designs. Given the setting, it was clear that the asteroids would need to be as visually distinct from each other as possible.

We could have used the same conventional asteroid shape for all the asteroid types and only use colour to differentiate them like with the placeholders. That would require them to be more vibrant than we wanted them to be, but the main problem is that it just wouldn't look as good overall.

Instead we decided to design asteroids to be different in every aspect.

Asteroid shapes

First, the overall shape. This is important because of the lighting on the asteroids. A large part of identifying them is the shape of their left illuminated side. Metallic asteroids would be heavily deformed concave shapes of pitted metal. Carbonic asteroids would be convex masses of gravel and dust, a bit more like merged spheres that had been blended together. Oxide asteroids would be hard faceted shapes with more straight lines and sharp angles.

For the materials, the metallic asteroids would have a metallic sheen. Carbonic asteroids would be very rough with more gravel-like surface imperfections. Oxide asteroids would be more shiny and glass-like, but less like a clean gemstone and more of a battered dusty or frosty look.

With these things we could have some nice looking and distinct asteroids, but it made another problem worse: The asteroids being very distinct, also made it very recognisable when any of them repeated. To avoid noticeable repetitions, we'd need loads of variations, and because the sprites are quite large doing so would be very costly.

Another option is to hide some of the similarity by making the asteroids rotate, but...

7680 reasons to not rotate Fearghall

Usually for sprites that need to rotate, such as the character or vehicles, we render a separate image for every rotation so the lighting matches up. While this approach would likely still have worked for this case, it would have been a performance nightmare - with each asteroid class having 5 sizes, each of which would need some amount of variants, and probably at least 64 frames to convincingly rotate, we would have needed a whopping 7680~ frames of animation.

Clearly, a different solution was needed.

Enter Shaders

It seemed clear that we would need some method of dynamically applying lighting information to an asteroid. This is where the game artist's best friend, the normal map, comes in. For those not in the know, a normal map is an image which essentially stores the slope of what it is representing.

Red Channel Combined Normal

Left/Right slopes are stored in the red channel, Top/Bottom slopes are stored in the green

In order to use this slope information to apply lighting, we need to do some clever maths on it in a shader to reference the slope direction from a fixed point of light.

Lighting updating with rotation

This gives us the "diffuse" lighting, but there is no colour information! As we don't want to create a 50s style space opera, we need our asteroids to not be black and white. So when rendering them, we need to separate out the colour information from the lighting information.

We can blend the generated lighting from the normal map onto our raw colour image, and it gives us a good-ish result

Colour only Lighting + Colour

Diffuse colour | Diffuse Colour with applied lighting

However, you will notice a few things, the lighting is very “flat”, the metal of the metallic asteroids and the shininess of the ice don't come through. This is because the shader isn't accounting for the shininess of the materials yet. But we don't want a uniform increase in shininess, it should be different in different areas! So we render out another pass, this time for the specular lighting.

We apply this simply by using the diffuse colour to handle the reflection colour, and make the asteroids brighter in the right areas.

Specular only Specular combined

Specular only | Specular with previous steps

But the ice still looks wrong! It is too solid, ice allows some light to pass through it and scatter, giving it that beautiful blue glow. This is known as sub-surface scattering, or SSS. So, one more image and render pass for those, blended only in the areas where light is indirectly hitting.

There is also still a bit too much shadow on all the asteroids, however. While this is technically accurate since there is nothing to bounce light around in space, it looks unnatural to our earth-evolved eyes. So we can drop the intensity of the shadow down, and add a secondary light source at a complimentary angle to the first.

No SSS SSS combined

No SSS or bounce light | SSS and bounce light

And we are done! Thank you to Earendel and Posila for helping figure out some issues with the lighting calculations.

Lighting updating with rotation

Space dust shaderFearghall

While Jerzy's starfield shader and the motion of the asteroids went some way towards showing the player the platform was moving, the effect was overall difficult to see, we needed something obvious and everywhere, but not over the top.

One solution would have been the classic warp speed effect of the likes of Star Trek or Dr.Who, but this felt a little cartoony and slightly divorced from the pseudo-realistic style of Factorio. So I settled instead on a compromise of some motion blurred micro-meteorites and interplanetary dust, presumably from all the exploded asteroids.

Space dust going by

One method to create this effect would have been spawning huge amounts of smoke and particles with some motion blur, however particles take quite a lot of resources in large numbers, and since this effect needs to scale with whatever size platform you are viewing, this would result in an exponential increase in cost.

Instead, I opted to create a single texture with all of the "particles" on it. In an effort to further optimise, this texture is then "packed" with several other textures used in the shader, this keeps the sampler count low, while still allowing for lots of detail.

All 4 images combined into 1 RGBA image

With a little maths on the texture coordinates, we can make this image move, and by masking out the Red, green, blue, and alpha channels, we can access the “packed” data that we need.

Simply panning all the textures

However, the dust is all still moving at more or less the same rate, which looks weird. So we will make the specks and dust move at different rates by simply modifying the speed in the panner function. But we also don't want all the specks to move at the same speed, it would look too much like a single cohesive structure. This is where some of the masking textures come in. We can use image #1 to selectively mask out individual particles at different times, which helps sell the illusion of many small things moving independently. This is done with a bit of maths to graph a peak around a value, we then assign the target value to a time variable, and loop it back around.

"Random" selection of which speck to show

Currently the particles are using the trail image at 100% all the time, but they should be less blurred if they are moving slowly. Thanks to some pre planning when creating the image, we can do this by cutting out pixels below a threshold. Since the trail image is a linear gradient, this means we can control the length pretty easily, we just subtract the target value from the image value.

The specks grow a longer trail as the platform gains momentum.

If we repeat the process a couple of times at different speeds and scales, we create a forced parallax effect. I.e Dust closer to the camera is larger and moves more quickly.

Now all we need to do is combine it all together, and tie the intensity to the zoom level so it's not too distracting when building, and we are done!

Space dust over a moving platform

Done with the foreground element, anyway.

While the dust helps tell the story of movement, the space still feels very 2D. Why are all the asteroids perfectly planar with our platform?

So we need some background elements, more asteroids! Again, we could have just created more asteroids that render behind the platform and don't collide, but this seemed suboptimal.

Instead, I used a technique called "texture splatting" to create infinite amounts of asteroids from a single texture atlas containing only a few images. This (F)FFF is already quite long, so I won't go into details, but the function basically splits the screen into a grid, and aims to place 1 asteroid in each, with a random offset.

For those interested in the technique, Daniel Elliot created a fantastically in-depth youtube series on its implementation, which is almost the exact implementation used here.

Texture atlas of all the asteroids
Texture atlas of all the background asteroids

The shader splits this image into single asteroids by modifying the UV-coordinate of the texture sample. If we create a grid of randomized UVs, we can place these asteroids within them. However, if an asteroid is placed across the boundary of 2 cells in the grid, it will be cut off. So we needed to reference neighboring cells to find out if they are overlapping the current cell. Thankfully, this can be managed by simply using the same function +/-1 in any given direction.

Here you can see the grid of cells we generate, as well as overlapping samples (coloured red).

We give each asteroid a random speed of rotation, motion, and scale, and do the whole thing again at a different scale for parallax. We then do the same and use the same calculations for dynamic lighting as before, and we have infinite fields of asteroids!

On and on and on forever

Thank you very much for coming on this long and technical journey with me. It seems our ship has survived intact.

As always, never tell me the odds in the usual places.