Earthbound Background Study
Earthbound features unforgettable battle backgrounds that use seemingly simple shader techniques to render them! This seems like a good, approachable goal to learn some cool techniques for creating interesting effects quickly while being beginner friendly.
The document is broken up into 3 parts: Analysis (breaking down the backgrounds), Techniques (how to replicate specific, single effects), and finally demos (reproductions of the full background).
I’m only including 2 gifs on this document to ensure the page loads for folks with lower bandwidth, but the images link to fully animated gifs if you want to see them in action.
These reference backgrounds will be referred to as Background 1 through Background 6 later on in the document. Background 1 is the first one here with black and pulsing red and blue colors. Background 2 is the red/peach colored checkered pattern. And so on.
Gigas is the final boss of this game and features some unique (and disturbing) effects. That battle features numerous backgrounds during the entire fight which is also unique to the game, but I chose to focus on the last iteration which I think is most disturbing (and therefore interesting)!
Background 7 (Gigas)
All of the backgrounds use (trigonometric) waves to some effect.
Only a few of the backgrounds actually involve translating or scrolling the pattern or texture in some regard. When they do move, it’s often at a slower constant speed towards a single direction.
Background 2 moves very slowly to the right. Background 6 moves more rapidly towards the top-left corner.
I’m not quite sure on the correct technical term for this, but some of the backgrounds could be more easily generated using mirroring - when an image is flipped about some axis.
This can be demonstrated by cropping some of the backgrounds at specific points.
If we take Background 1 and crop it at just one quarter of the screen, we can isolate the part being mirrored. If we then mirrored this effect 3 times (along the center of the screen vertically, horizontally, and both) to fill the screen we achieve the full effect!
This can potentially save a lot of trouble, frustrations, and work trying to get the entire effect to fill the screen precisely.
You can also see this effect in Background 3, though it's a little harder to spot because of the wave effects on top of it.
This is also done using a smaller portion of the screen in Background 4, though that might instead be a texture being tiled over the screen.
The designers didn't go too crazy with color ranges, often using an analogous color scheme (nearby colors) or just a single hue with various shades and tints. Whether this was because of hardware limitations or not, it's a good choice for color design.
This section covers how to replicate common effects seen in the backgrounds, but on their own. Ideally we will be able to combine these effects together to recreate a full background! Because the sections are so short, I won’t be writing interactive demos for them and will only showcase relevant shader code and the result.
The checker pattern is made up of squares alternating between 2 different colors. Let’s assume black and white.
The algorithm is to return one of two colors depending on where the pixel is and how wide each square is. Imagine a color ramp with a hard edge right at 50%. Values below 50% return black and values above or equal to 50% return white. We just need a mapping from pixel position to this color ramp.
My first intuition was to use if statements - if the pixel’s position is before halfway of the ramp, then return black, otherwise return white. While this would work, doing dynamic flow control (if statements where the path chosen depends pixel to pixel) is discouraged and inefficient in rendering.
So I turned to the GLSL docs looking for functions that would act like a linear interpolation (the function called mix) but not interpolate at all… I didn’t find anything to fit that need, but I realized I could simply make sure the values being passed to mix were either 0 or 1 beforehand!
Thus my algorithm is:
Though this only results in a bar pattern for one of the directions (x or y) and not the full checker. To fix this, we run the algorithm on both pixel positions, then combine the results together.
To create a true checker pattern, we simply take the difference between the x and y values and absolute the result. When both are 0, 0 is the result. When x is 1 and y is 0, 1 is the result. When both are 1, 0 is the result!
I did stumble upon a neat way to keep the checker pattern, but use a gradient 50% value when either the x or the y value is 1. We add the values together and divide by 2!
Given a fragment shader knows which position on the screen it is, we're now set to create the program!
uniform float u_SquareSize;
uniform vec3 u_ColorA;
Uniform vec3 u_ColorB;
float x = mod(gl_FragCoord.x, u_SquareSize * 2);
x /= u_SquareSize;
x = floor(x);
float y = mod(gl_FragCoord.y, u_SquareSize * 2);
y /= u_SquareSize;
y = floor(y);
float val = 0;
val = (x + y) / 2; // Smooth between colors A and B
val = abs(x - y); // Either color A or color B will be used
vec3 color = mix(u_ColorA, u_ColorB, val);
gl_FragColor = vec4(color, 1.0);
Some examples of the shader. Already can see this being used by Background 2.
First make a wave texture, greyscale with alpha so we can color it easily.
It’s invisible on this document, but to the left of the image above is a white line with the black turned into alpha.
From here it’s quite straightforward, render the texture and have it repeat. Add rotation and scaling so the texture gets tiled.
These were drawn using 2 different textures. The left image uses the thin wave texture just above, and the right image uses a thicker wave texture, both tile seamlessly.
But what if we want to really mess with the waves and change the frequency, width, or amplitude on the fly?
We don't have access to a plotting system like a graphics calculator, so we can't just write an equation for the wave and have it render. Instead I opted to do something similar - calculate the value of the waveform at each pixel position (based on the x value) and see how close the pixel’s y value is to this trigonometric function. If it’s sufficiently close, then return the line color! I have concerns that this may not be terribly efficient - calculating a cosine per pixel, but then again even simple lighting systems use dot products and cosine calculations per pixel on the regular!
An alternative would be to pass in a line mesh to a vertex shader, or have the line mesh be generated in a geometry shader, but I haven’t studied geometry shaders yet nor have I used line rendering. This is something I’d like to explore at a later time.
Additional features I want to support in this function:
My algorithm is: