Shader Showcase 2 ~ WGD Lightning Talk 2

Warwick Game Design seems to be making it tradition to hold Lightning Talks not just once a year, but once a term. This time round, we had great talks from one speaker about the mistakes she made during the development of her game, and one from the rarest of beasts in the society – a musician. Well, he wears many hats and one is music, anyway. Both were very informative, varied and interesting. Then there was me, caterwauling about shaders once more.

The Palette Swap shader

I shamelessly took inspiration from a talk from the previous Lightning Talks session and my first shader was my bespoke implementation of a palette swap. The general technique describes the process of mapping from some domain of colour values to another.

PaletteMap

Treat this map in a programming sense; a set of keys and the values each maps to – they’re called dictionaries or associative arrays in some languages. Alright, this is too abstract so far. My talk was about a specific problem I was having: I want a fully customisable colour scheme for my player sprite, which features eight different shades of grey.

PlayerSprites
He’s going grey after doing this job so long.

There’s at least a couple of ways of providing this functionality. One: create a different set of sprites for each colour combination. Now, I plan on having presets for skin colour, hair colour, shirt colour and trouser colour, each with ten different combinations. Dunno about you, but I think I’d get repetitive strain injury from drawing this spritesheet 10,000 times. Not to mention the havoc it would wreak on the build size of the game and the memory needed to hold them all!

The second way (pause this article now to have a guess. Got it? Good) is to implement the colour changes with a palette swap shader. I know, it was so hard to guess, I’m unpredictable like that. To understand what we’ll be doing, I shall introduce a few of the concepts of shading languages.

First off, I’m using a shading language called Nvidia Cg. Unity is really weird in that it uses a wrapper language, ShaderLab, to give Unity-friendly semantics around Cg programs embedded in a bunch of ShaderLab code. It’s pretty flexible and simplifies the whole pipeline to minimise writing boilerplate code. Then Cg is cross-compiled to either GLSL (OpenGL Shading Language) or HLSL (High-Level Shading Language) for OpenGL and DirectX platforms respectively. Cg is useful, in that it only supports features found in both those languages, but it’s limited because… it only supports features common to both those languages. Not to mention, the entire language has been deprecated since 2012. Apparently, it’s still helpful, though.

In Cg, and also in GLSL, a colour is a 4-element vector of floating-point (decimal) numbers between 0 and 1. Those represent the red, blue and green colour channels, plus the alpha channel – transparency. Colours are like a successful relationship, you have to have good transparency.

GreyscaleRange2
Greyscale range. Not to be confused with scaling a range; that’s mountain-climbing.
FloatingPoint
Floating-point numbers keep your shaders ship-shape.

In my implementation of palette swap, I don’t need much information for the keys of my map. In fact, I’m going to do something a bit hacky and use my keys as a sort-of array index (you’ll see shortly what I mean), so I only need to use one of the colour channels of the input pixels; since the input is greyscale, I arbitrarily pick the red channel.

It’s easier to appreciate this visually. Imagine the range of greyscale colours split into eight equally-sized sections. Oh, look, there’s an image above for that! Now, if we were computing things on the CPU side, we’d just go “ahh yeah, stick a bunch of if-statements in there and just set the new colour to, I dunno, pink if it’s between 0 and 32, blue between 32 and 64 and so on”. But STOP! We’re writing a shader! Shaders don’t like if-statements. I asked why, and they said if-statements once called shaders smelly.

Shaders work best if we can re-write our problem as a vector or matrix operation, since they’re vector processors – this is their bread and butter. Control flow in a shader is slower than a snail riding a tortoise because of the way GPUs implement branching. Luckily, we can send over our choice of 8 target colours over to our shader as an 8×4 matrix. Except, no. Cg only supports arbitrary-sized matrices up to size 4×4, so we need to do something a bit different and send over a two-element array of 4×4 matrices.

// Sample the matrix at a point determined by the texture.
fixed4 x = tex2D(_MainTex, i.uv);
float y = x.r * 2;
fixed4 result = fixed4((_ColorMatrix[y])[(y % 1) * 4].rgb, x.a) * i.color;
GreyscaleMap
Shaders: bringing excitement to your life, one pixel at a time.

We can then, as mentioned, use the red channel’s value as an array index. We multiply it by two to make sure we cover the range [0, 2] to access the correct matrix (_ColorMatrix[y]), and inside the matrix we want, we multiply the post-decimal-point part by four to act as a second index; this gets the row of the matrix we’re looking for. All you mathematicians out there are probably screaming at (y modulo 1), but in Cg, this’ll work on a floating-point by preserving only the part of the number after the decimal point. It’s a glorious exploit.

Now, we’ve completed the effect. When this is run in-game, you can switch out the greyscale sections of the sprite with some fancy new colours on the fly. I used this for a neat character customisation screen!

 

 

 

The Mesh Explosion shader

So far, all of the shaders I’ve shown off at the society have operated in the fragment shader. That is, they’ve been messing with the colour values of pixels. But this isn’t the only fancy thing one can achieve with shaders, oh no no no. You can mess with the actual geometry of a mesh in other shaders, such as the vertex shader, beforehand. I’m aiming for an effect that takes a mesh and pushes the triangles along their normals outwards – now, you actually can’t use a vertex shader for this; to calculate the triangle’s normal vector, you need access to all three of the vertices of the triangle. We need a geometry shader.

MeshExplosion
Directed by Michael Bay.

A geometry shader can operate on more than one vertex during each iteration; we’ll consume three at once to blast through triangles one at a time. For each triangle, we calculate the normal vector – this is a vector perpendicular to the face of the triangle, and it faces away from the front face of the triangle. The convention in computer graphics is that if, from your viewpoint, you can label the vertices of a face anticlockwise, then you are looking at the front face.

 

 

Finding the normal vector is a vector cross product away. Once we have that, we can add a multiple of that vector to each vertex of the triangle, so that the triangle packs its bags and departs the rest of the mesh. Furthermore, we can pass in a shader uniform variable to control how far the triangles move – I bound it to the mouse wheel on the CPU side. Here’s the pseudocode for calculating the normal vector:

// Calculate the normal vector in the normal way.
e0 = v0 - v1;
e1 = v2 - v1;
n = normalise(cross(e1, e0));
MeshNormal
Just a normal day for a triangle.

The movement code is exactly as you’d imagine – add a fraction of n to each vertex. The rest is some icky convoluted stuff to do with how geometry shaders need to output vertices (after all, a geometry shader can also be used to add geometry at runtime), and I don’t want to blow your minds any further or your screens will get covered in grey matter, so let’s not go into too much detail about that.

I don’t have the source code to hand for this project yet; I thought it would be more productive to get this post written up and release the source code later, when it’s been cleaned up. I’ll update this post when the time comes, but in the meantime I hope you gleaned some succulent shader suggestions. Happy shading everyone!

Advertisements

Shader Showcase ~ WGD Lightning Talk

Every now and then, and definitely not when we could find any other speakers to come in, WGD holds Lightning Talks. Any member is free to come along and opine for a few minutes about whatever game-related topic they please. In the past, we’ve had talks on using OpenGL in fancy ways, using fancy DAWs (Digital Audio Workstations) to create funky tunes and “games for people”, for those fancy-pants people who love games involving no equipment except the bodies of you and your friends.

This time round, there were three talks – one was on palette-swapping and different techniques used in the past, and another was about making your own art (it was sorta like the Shia LaBeouf Just Do It talk, but with less shouting and more creepy drawings). Finally (or rather, in the middle of those two), was a talk from someone not at all built for public speech – me. I did an analysis of some of the cool shaders used in Super Mario Odyssey‘s Snapshot Mode, and I’m gonna do it again, right here. It was somewhat refreshing having a whole session of talks about arty subjects, even if they often swayed into technical artistry, given that the society is somewhat programmer-heavy. Never mind that the Lightning Talks took place last term – let’s pretend I’m prompt at blogging.

Anyway, it’s time to jump up in the air, jump up don’t be scared, jump up and make shaders every day!

00_demo_none.png

I’ve set up this demo scene with a few coloured cuboids, one directional light and a boring standard Unity skybox. It’ll do the job for demonstration purposes. In the executable version, you can freely move the camera around.

01_ex_greyscale

I took the most saturated environment I could find, just to desaturate it.

First up is a simple colour transformation. To convert an image to greyscale, all we do is take the luminance of each pixel in the scene and use that value for the red, green and blue values of that pixel’s colour.

float lum = tex.r * 0.3 + tex.g * 0.59 + tex.b * 0.11;

The luminance value takes into account the sensitivity of the human eye to each component of colour; your eyes are more sensitive to green, so it is heavily weighted in this calculation. That’s basically all there is to the greyscale shader.

02_demo_greyscale.png

This is pretty much what you’d expect; the effect is very similar to Odyssey’s.

03_ex_sepia.png

The city environment felt the most appropriate for a sepia-tone shot.

The sepia-tone shader is very similar to the greyscale one, although the transformation is a little more involved. Before, we used the same value for each of the resulting red, blue and green channels to get greyscale values, but this time, each of the resulting red, blue and green are separate functions of the original RGB values.

half3x3 magicNumbers = half3x3 
( 
    0.393 * 1.25, 0.349 * 1.25, 0.272, // Red. 
    0.769 * 1.25, 0.686 * 1.25, 0.534, // Green. 
    0.189 * 1.25, 0.168 * 1.25, 0.131 // Blue. 
);

half3 sepia = mul(tex.rgb, magicNumbers);

We have to resort to some matrix multiplication to achieve this. The 3×3 matrix represents a bunch of numbers magically pulled out of Microsoft’s rear end – the mapping from the original colours to the new ones. The last line here essentially represents a system of three equations, where, for example:

sepia.r = tex.r * 0.393 * 1.25 + tex.g * 0.349 * 1.25 + tex.b * 0.272

If you’ve never done linear algebra or matrix multiplication before, this might look a bit exotic, but trust me – it works!

04_demo_sepia.png

We end up with another unsurprising result. You might find this a bit too yellow, in which case just remove all the multiplications by 1.25 I added.

05_ex_depth.png

This one is where things get a bit more interesting. Upon seeing this effect, I immediately thought to myself, “Aha! They’re just using the depth buffer values! I’m a goddamn genius”! So, what is the depth buffer I hear you ask?

In computer graphics, you need to render things in the correct order. If you have a crate fully obscured by another crate, you wouldn’t want to render the obscured crate over the other one – it’d look really strange! Whenever a pixel is drawn, its depth – the distance from the camera plane to the object being drawn into this pixel in the z-direction – is recorded in the depth buffer (z-buffer); this is just a big ‘ol 2D array the same size as the program’s on-screen resolution. When you try and draw something, you first check the new object’s z-buffer value against whatever is already recorded at that position, and if it’s a higher value (the distance from the camera is further), that means it’s occluded and we discard that pixel.

06_demo_depth

This is the same scene from above, but from a different angle. Each pixel is just showing the greyscale z-buffer value of the final rendered image, which means it’s representing how far away the closest thing at that pixel position is. But why does this not look as good as the Odyssey example? Well, first of all, I’m not Nintendo EPD and they have a few more resources than I do. Second, I’ve not coloured the image myself, although as we saw earlier, this would be a fairly trivial change. The main reason is actually because the scene isn’t very large and we need to understand how the z-buffer does things to understand why.

There needs to be a bound on how small or large the z-buffer values are, else the values would be pretty meaningless. A camera has a near clip distance and a far clip distance, which are the minimum and maximum distances from the camera plane an object can be to be rendered. If you’ve ever clipped the camera through part of a wall in a game, that’s because the near clip plane is further away than the wall is, so that section of wall is culled (not rendered). Values in the z-buffer are typically floating point values between 0 and 1, so a small scene (i.e. one where the clip planes are fairly close together) would result in two objects having more different z-buffer values than the same objects in a large scene.

In the scene above, the near plane is very close to the camera and the far plane is just far enough to contain the whole scene. By default in Unity, the far clip plane is 1000 units away, which would result in the entire scene here being black – all z-buffer values would be close to 0. But having a small scene means you don’t really get much variation in colours, because things are really close and there’s nothing in the background – in the Odyssey screenshots above, there are things clearly in the foreground (Mario and the rocks he’s stood on) and the background (the section of island behind the waterfall). One detail that’s kind of hard to make out is that the furthest island actually shows up in very faint green in the snapshot, so it’s real geometry rather than part of a skybox. Also, the water particles don’t show up in this mode, so they’re likely reading from the depth buffer but not writing to it, and are rendered after all the opaque level geometry.

07_ex_blur

Seaside Kingdom Doggo raised this game’s review average by 19 points.

Next up is a blur effect. I won’t be talking about the radial blur starting from the edge that Super Mario Odyssey uses though; I’ll talk about a simpler blur that I had time to write – a Gaussian blur that operates on the whole image.

This type of blue works by throwing a ‘kernel’ over each pixel in the original image and looking at the neighbouring pixels. The larger the kernel, the more pixels are considered. Then, a weighted average of the pixel values, with the central pixel having most weight, is calculated for the new image. It’s a pretty simple concept, and probably how you’d expect, but is one step more complicated than a box blur which doesn’t bother with weighting the pixels and just does an unweighted average. Commonly, you would do two or three passes with different sized kernels, so the algorithm is run more than once and the resulting blur has more fidelity.

08_demo_blur.png

This image is doing three blurring passes, each with a larger and larger kernel. Each blurring pass actually requires two shader passes, one in the x-direction and another in the y-direction. You could reduce the amount of blurring by making the kernels smaller or reducing the number of passes.

09_ex_edge

TFW you turn up to a formal occasion in casual clothing.

The final algorithm I implemented is an image-space Sobel filter to attempt to detect edges in the image. This also acts in separate x-and y-direction passes; in each direction, a gradient is calculated, where a higher gradient means a more ‘different’ colour. By definition, you can’t do a gradient in both directions simultaneously, hence the need for two passes. This effect wouldn’t be used in a game if you wanted objects to stand out against objects of similar colour, but it’s pretty simple to understand conceptually.

10_demo_edge.png

Running the algorithm on our sample scene results in pillars that are pretty cleanly defined, but this is mostly due to their block-colouring. You can also see a couple of spots where pillars seem to ‘blend’ into one another, and all shadows are also edge-detected because they are dark areas right next to light flooring.

For a bit of fun, I ‘converted’ this effect into a cel-shading effect. Again, it’s not exactly Borderlands quality, but it was a fun exercise for me. This effect is also not present in Odyssey, so there is no reference image comparison.

11_demo_cel.png

The Sobel edge detection filter from above is inverted and multiplied by the source image, such that ‘edge’ areas become black and all other areas retain their original colouring. The areas where colours are similar clearly lack cel-shading here, such as the red pillars right-of-centre. You can also see on the yellow pillars where the skybox turns lighter that the black lines ‘break’ a little. Of course, because it’s based on the Sobel edge detection filter, you also get cel shading on the shadows, but the effect is less pronounced here because the shadows are dark.

All of the source code and a built executable for Windows can be found over on my Github. It’s really fun to pick apart effects you’ve seen in video games and I wholeheartedly recommend you try the same! If I have time over the holidays, I may try to pick apart some of the other effects, such as the NES/SNES filters which are probably just pixelation filters with a palette swap applied on top.

ShaderLab Programming #1 – Sepia Blend Shader

Now that exams are over, I’ve had time to actually breathe and, more importantly, do game dev stuff. I’ve rarely touched computer graphics in the past, but it’s an area I’ve always wanted to explore in detail. This summer, I plan to mostly spend my time learning the intricacies of computer graphics, and that’s partially where Unity’s ShaderLab steps in.

ShaderLab is a graphics language in its own right, but it also encompasses Cg, a high-level shader language by Nvidia, and HLSL, a Microsoft shader language for use with DirectX, both of which were developed together. We don’t need to know too much about the structure of ShaderLab programs for what we’re doing, so I’m largely going to ignore it for now. I needed to create a post-processing effect similar to Unity’s built-in Sepia Tone, which is an on/off effect, with a tweak that allows it to fade in and out instead. It’s for a game I’m working on in which you can turn back time – more on that in the coming days when I’ve put together something a bit more concrete hopefully! Let’s deconstruct this super-simple program and explore what a sepia effect actually is. Unfortunately, Unity’s Sepia Tone shader seems to use some sort of maths wizardry for its fragment shader that I don’t understand, so I’ll also be writing mine mostly from scratch. Here’s the number bullshit right here:

fixed4 frag (v2f_img i) : SV_Target
{ 
    fixed4 original = tex2D(_MainTex, i.uv);
 
    // get intensity value (Y part of YIQ color space)
    fixed Y = dot (fixed3(0.299, 0.587, 0.114), original.rgb);

    // Convert to Sepia Tone by adding constant
    fixed4 sepiaConvert = float4 (0.191, -0.054, -0.221, 0.0);
    fixed4 output = sepiaConvert + Y;
    output.a = original.a;
 
    return output;
}

The infinite wisdom of the Internet tells us that to convert a source image to a sepia-tone version, we just define a function of the input colours as follows:

new red = min(in red * 0.393 + in green * 0.769 + in blue * 0.189, 255)
new green = min(in red * 0.349 + in green * 0.686 + in blue * 0.168, 255)
new blue = min(in red * 0.272 + in green * 0.534 + in blue * 0.131, 255)

This formula represents 100% sepia; that is, the source image completely turned into a sepia tone. But we don’t always want a completely sepia image – we need to incorporate some sort of progress measure. Using this formula, we can just interpolate between the input colour and the full sepia tone and use a separate script to control the ‘progress’ of the interpolation. I’ve talked about interpolation before, so if this really big word is confusing, it just means we’re picking a colour on an imaginary line between ‘original colour’ and ‘sepia colour’, with a parameter between 0 and 1. We can now create another function to get our final ‘blended’ sepia colour:

out red = parameter * new red + (1 - parameter) * in red
out green = parameter * new green + (1 - parameter) * in green
out blue = parameter * new blue + (1 - parameter) * blue

With the theory out of the way, we can now create the shader and the script that will control it. We’ll be utilising the post-processing effects provided by Unity, so make sure you import them from the Standard Assets using Assets->Import Package->Effects from the menu bar. We’ll start with the controller script.

There are two main ways to keep track of the progress, so I’ll cover both. The first way is to define a method such that another script can just pass in a float to act as the parameter.

using UnityEngine;

namespace UnityStandardAssets.ImageEffects
{
    [ExecuteInEditMode]
    [AddComponentMenu("Image Effects/Color Adjustments/Sepia Blend")]
    public class SepiaBlend : ImageEffectBase
    {
        private float progress = 0f;
        private bool active = false;

        private void SetProgress(float progress)
        {
            this.progress = progress;
            active = (progress == 0f);
        }

        // Called by camera to apply image effect.
        void OnRenderImage(RenderTexture source, RenderTexture destination)
        {
            if(active)
            {
                material.SetFloat("_Progress", progress);
                Graphics.Blit(source, destination, material);
            }
        }
    }
}

OnRenderImage() is the method called when we want to render a post-processing image. We set the ‘progress’ parameter of the material attached to this project – we’ll see how this works with that material’s shader shortly – and use Graphics.Blit() to apply the texture created by the shader to the screen. The second way to control the progress parameter is to allow external scripts to set the ‘active’ boolean and use a time-based interpolation to decide what the progress should be. The guts of the program will look more like this:

private float progress = 0f;
private bool active = false;

private void SetActive(bool active)
{
    this.active = active;
}

private void Update()
{
    progress = Mathf.Lerp(progress, active ? 1f : 0f, Time.deltaTime
        * 5f);
}

// Called by camera to apply image effect.
void OnRenderImage(RenderTexture source, RenderTexture destination)
{
    material.SetFloat("_Progress", progress);
    Graphics.Blit(source, destination, material);
}

Now for the actual shader! I’ll explain very quickly what the fragment shader is doing and ignore the vertex shader since it’s doing nothing in this example; I may do another post in the future explaining the details of how the shader is constructed, but I’m glossing over a lot for now. Don’t even worry about it yet.

Shader "Hidden/SepiaBlendEffect"
{
    Properties
    {
        _MainTex ("Base (RGB)", 2D) = "white" {}
        _Progress ("Progress", Float) = 0
    }
    SubShader
    {
        Pass
        {
            ZTest Always Cull Off ZWrite Off

            CGPROGRAM
            #pragma vertex vert_img
            #pragma fragment frag
 
            #include "UnityCG.cginc"

            uniform sampler2D _MainTex;
            uniform float _Progress;

            fixed4 frag(v2f_img i) : SV_Target
            {
                fixed4 original = tex2D(_MainTex, i.uv);
 
                half3x3 vals = half3x3
                (
                    0.393, 0.349, 0.272, // Values for input red.
                    0.769, 0.686, 0.534, // Values for input green.
                    0.189, 0.168, 0.131  // Values for input blue.
                );

                half3 input = half3(original.rgb);

                half progress = half(_Progress);

                half3 intermed = (progress * mul(input, vals)) + 
                    ((1 - progress) * input);

                fixed4 output = half4(intermed, original.a);

                return output;
            }
            ENDCG
        }
    }
    Fallback Off
}

We’re interested in the stuff between ‘CGPROGRAM’ and ‘ENDCG’ – this is written in Nvidia’s Cg. All we need to know about the structure of shaders for now is that the fragment shader is run on every pixel of a processed image. I’ll also ignore all the types of variables for now and cover those in another post at a later date. Jumping straight to the frag() method, which constitutes the fragment shader, we’ll see a 3×3 table of numbers, vals, that looks strikingly like the one defined for our first function; the table is the transpose of a matrix formed by those functions’s numbers. Our shader performs a matrix multiplication on a vector made of the original rgb values, input, and our magical sepia number matrix vals (which is why it had to be transposed), then we do our interpolation to get intermed. Afterwards, we put together the final output by adding the original colour’s alpha onto intermed, and boom! We have our Sepia Blend shader. Now all that remains is to attach the SepiaBlend script to the main camera and assign the SepiaBlendEffect shader to the script, then we’re good to go.

I hope this has been a helpful introduction to the world of computer graphics. I’ve uploaded the three scripts to avoid you having to copy everything out (both variants of SepiaBlend.cs are included; pick your preferred version). Let me know if everything turns into a bugfest when you attempt to run any of the examples and I’ll try to diagnose the problem.