Have a new blog post up:
Ruin of the Reckless #5 – Silhouettes in Game Maker
Hello friends. Today we are going to be doing something a little bit different.
Quite a few people have contacted me asking about the silhouette system that I mentioned in our first blog post. This system allows players to see their character even when they are ‘behind’ a wall or other obstructing tile. Early on, we knew that this effect would be necessary for Ruin of the Reckless. We also knew that it was possible in Game Maker because we had already seen it accomplished by Hyper Light Drifter. For any one interested in reproducing this effect themselves, Julian Adams has graciously provided an example .gmz for you to investigate :
https://www.dropbox.com/s/kc208kxcq4eguuk/RotR%20_%20Juju%20-%20three-quarters%20silhouette.gmz?dl=0(provided by Julian Adams under the MIT License
https://opensource.org/licenses/MIT)
Hyper Light Drifter uses a similar silhouette system. Don’t ask me what their method is though! – Hyper Light Drifte.
We had found several methods to handle this but none of them did every thing we needed. Either they didn’t work in randomly generated environments, or they couldn’t handle different colored silhouettes, or they had some other deficiency that made them unsuitable.
Luckily, we found Julian Adams (also known as ‘Juju’) from the game maker community. This guy is an amazing programmer, and a great person, and his solution to this complicated problem is quite brilliant. Being the creator, we asked him to write a guest article about his implementation, and he obliged… so without further ado… Julian Adams on top down silhouettes.
—-
There are a number of solutions to the silhouette problem that have been tried over the years. The most obvious is simply to prevent objects from ever going behind walls to a position where they are not visible. This is very effective – it’s cheap and requires no additional effort – but it compromises the authenticity of the environment. A Link To The Past uses this method an unusual way; employing a warped perspective to show the the front, the sides, and the back of obstacles in the world; this, unfortunately, locks the graphical style into a cartoony, surreal design.
RTS games pursue a more direct visual solution. Thanks to greater processing power available around the late 90s, they could now show units moving behind obstructing features without compromising the artistic expression of the environment.
Here’s a small example of our system in action. – Ruin of the Reckless
When the developers of Ruin of the Reckless contacted me, I was already interested in solving ‘the silhouette problem.’ We discussed different methods for displaying silhouettes in a orthographic/iosometric perspective.
1. The silhouette system needs to work in very large rooms, on the order of 5,000×5,000 pixels, lined with walls and obstacles.
2. The silhouette system needs to have minimal impact on the frame rate. They’ve spent a lot of time making their levels full of content.
3. The silhouette system should be pixel-perfect and support static tiles and animated objects. A wrought iron gate should only silhouette the specific parts of objects covered by the fence and not the holes.
4. The silhouette system should work with their object-based visual effects. The silhouette system should require as few parallel variables as possible.
5. The silhouette system should not mess with the depth and draw order system (using the ever-popular “depth = -y;” method). Silhouettes should obey this draw ordering.
6. The silhouette system should support different coloured silhouettes for different types of object (orange for enemies, blue for the player, green for items etc).
7. The silhouette system must be implemented in procedural, randomly generated environments.
—-
There are a number of pre-existing systems that have been described in GameMaker. Here is a relatively well known and popular solution:
http://gmc.yoyogames.com/index.php?showtopic=675243This silhouetting approach work as follows: Every frame, “occluders” are drawn to a surface, but only if they are below the player. The player’s sprite (an “actor”) has its pixel data forced to a particular silhouette colour and is then drawn to this surface. This drawing operation is special – it doesn’t change the transparency of the surface data, only the colour data. This means wherever a player pixel and an occluder pixel cross over, a silhouette colour is drawn to the surface.
This has a number of deficiencies – firstly, it requires constantly drawing and redrawing tiles to the screen any time the view or player moves. This is hugely wasteful of resources. Secondly, it only supports one actor at a time – the player. Whilst the example provided with this method only includes support for tiles, adding objects to the mix isn’t hard.
Here is another solution, designed by HeartBeast:
This solution works somewhat differently and is, in some ways, much cruder. Every frame, a surface is cleared black. The player is then drawn to this surface in a pure white colour, maintaining transparency, but not colour information. Occluders are then drawn at alpha=0.5 and pure black to this surface. This means any pixels that share a player and occluder pixel have a colour of #7F7F7F or, to us mere morals, “grey”. The surface is drawn to the screen using a shader that only draws pixels that are grey.
This application is also flawed, though in a different manner. This method requires redrawing to the screen every frame, due to drawing the player to the masking surface. Whilst this creates a somewhat accurate end result, this method fudges a lot of the fine detail through the use of an approximate inequality in the shader. Edge cases can also break – if an occluder has a low alpha (highly transparent) or many occluders overlap in once place. Most of all, built explicitly for a 2D side-on perspective, it cannot express three-quarters/isometric perspective at all. Unlike the previous example, however, it can support multiple actors although only with one colour of silhouette.
A peak at the occluder surface, and how it uses the red channel to resolve silhouette information. Note that each occluder is rgb 0,0, 0,at the bottom and the red channel creeps up to 255 pixel by pixel – Ruin of the Reckless
This red channel information represents the distance from any given pixel to the very bottom of that particular occluder. When we want to draw a silhouette, we sample from this masking surface and compare the y-position of the actor being drawn to the colour of the pixel on the masking surface. If the masking pixel is in front of the actor, we know that particular pixel (for the actor) is being occluded. Since we’re sampling the mask surface per actor rather than in one big lump, we can change what the silhouette colour is per actor. This is all done in a shader. Any object for any reason can be drawn as a silhouette with no more than a few extra lines of code.
The actual mechanics of how this is done uses GM’s native depth order and requires each actor to be drawn twice – once for the normal sprite, once for the silhouette. The shader is constantly being set and reset. If the specification is partially broken, a custom depth ordering can be used (using a priority queue or equivalent) to batch all the silhouetting significantly increasing the rendering speed. As it stands, however, the system used as per Ruin of the Reckless’ specification produces one silhouetted actor at the same cost as three non-silhouetted actors.
Introducing animated objects as occluders requires the masking surface to be redrawn every step when they’re on the screen. This is unavoidable but, thankfully, the vertex buffer submission is so fast that this has a minor impact on the frame rate. An occlusion object’s positional data can be drawn to the masking surface on the green or blue channel with an additive blend and treated similarly to static occluders.
There are some structural limitations, however. With the current system, occluders can only be a maximum of 256 pixels tall before we run out of room in the red channel to describe the necessary data. This limitation manifests itself mostly as a limit on the actor size (256px). This limit can be lifted through extra work in a shader to express height using additional channels to have heights of up to 2^24 (which is an absurdly high number). Using a single vertex buffer to store all the occluder geometry means that all sprites/tiles stored in that vertex buffer must be on the same texture page. For large games with a large number of tiles and sprites, this is unlikely to be the case; in these cases, you’ll need to use more than one vertex buffer.