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 1

 

Background 2

Background 3

Background 4

Background 5

Background 6

Background 7 (Gigas)

Analysis

Common Effects

Wave patterns

All of the backgrounds use (trigonometric) waves to some effect.

Linear Movement

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.

Mirroring

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.

Common Features and Forms

Shapes

Color Theory

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.

Animation Theory

Techniques

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.

Drawing

Drawing a checker pattern without using a texture

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:

  1. Constrain the pixel position to be between 0 or just under 2-times the size of a square (the “color ramp” is 2 squares in pixels wide - white then black)
  2. Divide the pixel’s position by the size of the square. The position is now between [0, 2)
  3. Floor the pixel position. The position is now either 0 or 1!

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 bool u_Blend;

uniform vec3 u_ColorA;

Uniform vec3 u_ColorB;

void main()

{

        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;

        if (u_Blend)

                val = (x + y) / 2; // Smooth between colors A and B

        else

                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.

Drawing a wave with using a texture

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.

Drawing a wave without using a texture

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:

  1. Translate the pixel position, probably as a function of time
  2. Rotate the pixel position
  3. Map the pixel’s y position so that the y axis goes from 0 to 1 then back to 0 again, causing the wave to repeat along the y axis.
  4. Calculate the wave’s value at the pixel’s x position, and map the result to be between [0, 1] which are the values a fragment shader uses.
  5. Calculate the absolute difference between the wave value and the pixel’s y position to see how close the y value is to the wave’s value. Keep in mind, a low difference means the pixel’s position is close to the wave!
  6. Increase the allowable range of valid values by subtracting some amount from the difference, then clamping the result to be between [0, 1].
  7. Smooth the edges by multiplying the current value by some amount, then clamping the result to be between [0, 1].

Drawing a function of multiple waves

Distortions

Wave Distortion

Mirroring

Translations

Translating

Moving a wave distortion

Demos

Background 6

Background 3

Background 2

Background 1

Background 7

References