SPACEJAMMED: technobabble

My last two jams have been followed up by a couple of ~technical blog posts~, mostly going into how I did various graphical things (shaders and so on), so let's not break the tradition now! c:

Examples of the old posts/games, in case anybody remembers them:

LD38: Blomst 🌻LD37: Lock and reload - | - ezgif-4-0dc7b137a0.gif | ezgif-4-6be4dc66a9.gif


logo-black-kopi-2.png

This time we'll be looking at SPACEJAMMED (made by me and Morten with a little input from Marte who couldn't join in fully this time) which is a slightly less ambitious project gameplaywise due to a bit less intense participation than usual, but has a bunch of graphical trickery to look at nonetheless! 👀

ld39emskjerm/em2-kopi2-2.png

Do have a play first to get acquainted with it! 💙


Painted background and sharp foreground

There are some three major graphical considerations to blog about for this game, so let's do one for each post so as not to make it all too lengthy. Today we'll be talking about the major effect that gives the game its primary graphical style.

effekt1-kopi 2.png

In this image, zooming in on the selected areas, we can see that while the gameplay elements are crisp and clear, the background, especially the farther it gets from the camera, is getting increasingly smudged, so as to give off a slightly "painted" effect, inspired a bit by the style of The Legend of Zelda: Skyward Sword, which has a similar thing going for it, if you compare Link to the background in this screenshot:

10yqpok.png

Mine is a bit of a cheap version which doesn't look quite as pretty. 🐻 A side effect of time constraints and a desire not to press the GPU too hard, since there was little time to optimise the effect properly, meaning I had to water it down instead, but I'm quite happy with it, and the higher the resolution, the more noticeable it is, so I do highly recommend playing the game in fullscreen!


Paint effect

Let us split this post up into the two parts. Before I could consider separating the game elements from each other, I needed to implement the paint effect in the first place, which would originally just cover the entire scene and make the character and the puzzle pieces smudged as well, which I had to fix later. It involved more than one step to get right, and so it would be too much to glomp it all together into one section.

Brush

I cannot take all the credit for myself. The implementation was inspired by the description in this forum post on TIGSource, in a thread discussing precisely the Skyward Sword brushstroke effect. The author described it using two images which I will reüse here, so that you don't have to click away from here. c:

The first is this, showing the texture sample pattern necessary for the effect, essentially representing the shape of the "brush". The middle represents the current fragment, and then a couple of extra samples are taken around it; in this case, six more.

pix1.png

Next, the goal is to turn this into a stroke, so that the final effect can be described like so:

pix2.png

In the case of SPACEJAMMED, I opted to simplify the brush and take fewer samples in order to go a little easier on the GPU and speed up rendering, and also moved things around a bit, so that my final brush looked like this:

pix3.png

Shader

The game was made in Unity, and so Unity shader syntax will be used, but the same methodology of course applies when using something like GLSL. The effect was added as a single pass over the entire rendered scene as a post-effect on the camera. The shader code for the sampling looks something like this, where UV is a texture coördinate with an x and y value:

``` // The o is for original sample. fixed3 o = tex2D(tex, UV);

// These are the offset samples. fixed3 a = tex2D(tex, UV + fixed2(0, y * 0.5)); fixed3 b = tex2D(tex, UV + fixed2(x, -y)); fixed3 c = tex2D(tex, UV + fixed2(0, -y * 0.5)); fixed3 d = tex2D(tex, UV + fixed2(x, y)); ```

The x and y values would depend on the distance intended between the samples, which can of course be played with to achieve different degrees of smudging.

Stroke

Next, following the forum post's instruction to render using the pattern, and using a blend algorithm that only picks the brightest of the source and destination colours, I set out to get it working.

Intensity

The brightest indeed. So first I needed a way to calculate the intensity of each fragment's colour. Just like when I had to convert colours to grayscale like I wrote about in my second blog post on Blomst 🌻—see there for details—it needs to be borne in mind that there are conversion weights to consider for each of the colour components (red, green and blue) in order to get the proper value. Rounding the values a bit, this is the code:

fixed intensity(fixed3 col) { return col.r * 0.3 + col.g * 0.59 + col.b * 0.11; }

Blending

Next, I needed to blend each offset brush sample with the previous values by keeping only the brightest fragment, in order to get that soft halo around things. Unfortunately I couldn't think of a good way to solve this without branching (which is generally a bad thing in shaders), but should you try and implement this effect, that may be worth trying to figure out.

The simplest implementation would thus be to simply check if the current pixel being tested is brighter than the previous one in the same place and if so replacing the old one or otherwise keeping it:

``` fixed3 blended(fixed3 a, fixed3 b) { fixed ai = intensity(a); fixed bi = intensity(b);

return (ai > bi) ? ai : bi;

} ```

However, I wanted to go slightly more fancy and only do partial blending to accent the strokes a bit more, and my algorithm ended up being a slightly more complex version of one of the more common methods of alpha blending (which was also explained in more detail in the just aforementioned blog post), with an alpha value based on the highest intensity.

I could then go through all the samples and blend them together:

o = blended(o, a); o = blended(o, b); o = blended(o, c); o = blended(o, d);

Depending on the distance between samples, the end result could now be something like so:

foer-etter-1-kopi.png

Distance from camera

The next thing I did to make the effect a little less overwhelming was to also sample the depth buffer (which is easily done in Unity by getting the built-in sampler variable _CameraDepthTexture) and multiplied the intensity of the effect by the depth, so that things deeper into the scene would be affected more intensely by the smudging than those nearer the camera. I also modified the camera's near clip plane in order to get a more highly contrasted depth buffer.

Here's the depth buffer before and after a little shader code to blow up the effect a lot more in the distance:

dbuf-kopi.png

Current result

At this point, the "end" result was now something this, with the smudge exaggerated for clarity, where there would be little to no smudging near the camera, and a lot more in the distance:

resultat-1.png

Of course, the foreground/gameplay elements were still smudged too, which was not desirable, neither from a playability perspective nor an æsthetic one. There's also a little bit of a lie going on here; as there were light sources on the sides of the scene, the shadows below the puzzle pieces were not actually straight below them, which made gameplay unnecessarily confusing, and this was fixed with a bit of trickery which will be explained in a future post, as will also the solution for the foreground elements.

Note on textures

To really give the shader something to work with, it was also important to make sure the textures weren't completely flat, but had irregularities that the shader could catch onto and amplify with the brush strokes, so I applied a little extra texture on top of the main texture so to speak, as for example in this subsection of the spaceship interior's texture:

texex.png


Next time

Like I said, at this point the effect had only been applied to the entire image, without making gameplay elements such as the character and the puzzle pieces clear and fully distinct from the background, and we have yet to touch upon how the shadows of these were forced to be cast right below them even tho there were light sources coming in from the sides. We'll get to these things in future posts! c:

Until then, have a good one! 🔥