Translate

Saturday, August 23, 2014

How to render Parallax Background and Foreground Layers Completely in the Shader [Tutorial]

EDIT Oct 16 2014: Changed precision to lowp for fragment shader. Consider using this as a default for mobile, unless you really need something better.  I saw a 2x speed increase on Kindle by doing so.


Hi all!

Here's a quick little diddy for anyone curious on a quick way to get a layered effect in your 2D game (note: we are still using 3D vertices):

I did a little more progress on Hoarder Monkey  and I am happy to have parallax backgrounds working all in the shader and in 1 draw call per object type (static VBO, dynamic sprites, Semi-transparent VBO, semi-transparent sprites).  :)

The background scrolling is all handled in the shader, and works surprisingly well.  I also do some color tweaking to make things a little darker in the foreground and blue-ish in the background:


Notice the closest graphics are darker


Notice the background objects are a more blue/white hue.



In case your curious the shader code is pretty simple (as it should be for a shader):



Here's the Vertex shader for parallax.  Notice I just arbitrarily chose < -50 to be the furthest away, scrolling layer (right before the background).  This code will render based on camera movement half the distance that the action layer, will move (action layer == z distance where player is):

new_offset = u_camera_offset.xy / 2.0;

The closest palm trees and stuff  will move twice as fast as the action layer, abritrarily chosen to be distance Z > 50.0 (Keep in mind, in OpenGL -Z is further away from the camera, while +Z is towards the screen!):

new_offset = u_camera_offset.xy * 2.0;

uniform mat4 u_projection_matrix;
uniform float u_cutoff_alpha;
uniform vec2 u_camera_offset;
uniform float u_scale;

attribute vec4 a_Position;
attribute vec2 a_texture_coords;

varying vec2 v_texture_coords;
varying float v_cutoff_alpha;
varying float v_zdepth;

void main()
{

  vec2 new_offset = u_camera_offset;
  v_texture_coords = a_texture_coords;

  if(a_Position.z < -50.0)
  {
    new_offset = u_camera_offset.xy / 2.0;

  }
  else if(a_Position.z > 50.0)
  {
    new_offset = u_camera_offset.xy * 2.0;

  }

  vec4 pos =  u_scale* (vec4(new_offset.x, new_offset.y, 0.0, 0.0) + 
      vec4(a_Position.x, a_Position.y, a_Position.z/2.0, a_Position.a));

  gl_Position = u_projection_matrix * pos;
  v_cutoff_alpha = u_cutoff_alpha;
  v_zdepth = a_Position.z;

}


Ok, now that scrolling business has been taken care of, for that oh so nice cartoon layered depth illusion.  Now what about coloring.  How about we color the background layers a little bit blue-er and white than the closer layers, and maybe the foreground (closest branches and such) can be a little more black?

// The Blue/white far scrolling layer
orig_color.b *= 1.90; 
gl_FragColor = mix(vec4(1.0,1.0,1.0,orig_color.a), orig_color, .9);

// The closest layer (more black color)
gl_FragColor = mix(vec4(0.0,0.0,0.0,orig_color.a), orig_color, .5);

// The normal action layer (where player is etc, no color changes):
gl_FragColor = orig_color;


#ifdef GL_ES
precision lowp float;
#endif

uniform sampler2D u_texture;
varying vec2 v_texture_coords;
varying float v_cutoff_alpha;
varying float v_zdepth;

void main()
{

  vec4 orig_color =  texture2D(u_texture, v_texture_coords);

  if(orig_color.a < v_cutoff_alpha)
  {
    discard;
  }
  
  if(v_zdepth < -50.0)
  {

    orig_color.b *= 1.90;
    gl_FragColor = mix(vec4(1.0,1.0,1.0,orig_color.a), orig_color, .9);

  }
  else if(v_zdepth > 50.0)
  {

    gl_FragColor = mix(vec4(0.0,0.0,0.0,orig_color.a), orig_color, .5);

  }
  else
  {
    gl_FragColor = orig_color;

  }
  
}


So there you have it.  In a nutshell this is how you can do parallax scrolling backgrounds all in the shader.  Of course you will still want to order your draw polygons to draw first:  closest to further away, and for semi-transparents draw first: further away to closest.

Draw ALL your semi-transparents last. Even after your background!  If you don't you'll get fun artifacts and it won't blend correctly.  Also, don't forget to use a TextureAtlas to reduce all your draws to just a few, and VBOs for any non-moving game objects (background decorations, non-moving platforms etc).

Happy GameDev-ing!