Search code examples
openglparallax2d-gamesopengl-3uv-mapping

openGL 2D Parallax Scrolling Texture Tears / Seams


I am implementing 2d parallax scrolling in openGL using a single quad and a texture atlas.

I use the following (offset is 1.0 / number of layers, so 0.2):

atlas

It works almost perfectly, but at certain offsets seams at the loop points are visible. (See the tear in the middle wall that reveals the castle and ocean behind it.)

seam

My algorithm is: At each fragment, calculate the position in the texture at offset * i; If the color is transparency, move to offset * (i + 1), etc. Stop when a solid color is hit. Again, the offset is 1.0 / (number of layers), or 0.2 here.

In the image above, it looks like my algorithm is sampling the space either to the left or right of the correct texture position, so it hits a transparent color, which is wrong.

I have tried many things to do error corrections including multiplying by 1000.0 before performing mods and then dividing, clamping to specific ranges, and so on, but I haven't found a consistent solution. Also, the issue might not necessarily be a result of floating point inaccuracy alone.

Is there a way of performing the calculations to guarantee that the texture sections loop perfectly? --perhaps using fract and floor in some unusual way? (I found this discussion but couldn't understand it fully because neither the OP nor the accepted answer explained the meaning of many of the variables: parallax offsets with texture atlas? It makes use of fract and floor, but this method might be incompatible. It's difficult to say.)

In case, the following are the relevant excerpts from my vertex and fragment shader code (with irrelevant parts removed -- using precision highp float):

// vertex shader
void main(void) 
{
   gl_Position = u_matrix * vec4(a_position, 1.0);
   v_uv = vec2(a_uv.x * u_offset_layers_x, a_uv.y);
   v_position_cam = u_position_cam; // set the camera position to an out variable
}

// fragment shader
void main(void)
{
   vec4 texel;
   // each successive layer scrolls more slowly
   float rate = 0.65;
   // iterate through each layer and sample until the alpha is not 0
   // since I get tears every so often, I might have rounding errors that end up sampling part of a previous or successive texture by mistake
   for (int i = 0; i < u_count_layers; ++i) {
      // I use multiplications and divisions by 1000.0 in an attempt to reduce the error, but I don't think it works at all

      // the camera position offsets the parallax layer
      float sample_coord = v_uv.x + ((v_position_cam.x * 1000.0 * rate) / 1000.0);

      // the y offset seems fine
      float off_y = (-v_position_cam.y * rate * rate * rate);
      off_y = max(off_y, 0.0);

      // offset by i layers, add that offset to (the sampled coordinate * offset per layer) % offset per layer 
      texel = vec4(texture(tex0, vec2(
         (float(i) * u_offset_layers_x) + mod(sample_coord * 1000.0, u_offset_layers_x * 1000.0) / 1000.0,
         v_uv.y + off_y))
      );

      // if the alpha is not 0, then don't sample the layers behind it (a texel has an alpha of 0.0 or 1.0)
      if (texel.a != 0.0) {
         break;
      }

      // reduce the scroll speed for the next layer
      rate *= rate;
   }

   // output the color
   color = vec4(texel.rgb, texel.a);
}

** I will add that I could easily use GL_REPEAT on 5 separate textures mapped to 5 quads (or rebind uniforms and re-render the same quad), but I want the shader to work on one texture and one quad to create shader effects and texture transitions most easily achievable when rendering to a single quad. Also, reducing the number of uniforms set is good. In addition, I interested in the engineering behind this problem and am using this method for the challenge. I get the sense that I am close, but I cannot find documentation / solutions to the seaming / offset issue. Disabling GL_REPEAT and using GL_CLAMP_TO_EDGE didn't seem to work, but admittedly GL_REPEAT is unnecessary if I use an atlas like this.

Thank you for your help in advance.

EDIT: If the single-texture approach doesn't work, would it be terrible to bind 5-10 repeating textures at once and use a similar algorithm, but rely on GL_REPEAT instead of mod? I think this would probably yield poorer performance, but I could be wrong. In the future I would like to transition between two multi-layer backgrounds using smooth-step. That is why I prefer the single quad approach. Also, if I get this working, then I can have more layers.

EDIT 2: I noticed that one vertical black line on the "brickwork" is missing in this screenshot, so I think that what is happening here is that the sampling / interpolation fails at a pixel-perfect level sometimes, which makes sense. Is there a way to circumvent this?


Solution

  • I would say that you're being too clever by trying to do this in the fragment shader. It'd be much easier to just compute the proper positions for each parallax layer based on the position of the "camera". If you need to wrap around, you render a second quad for that layer as well. That way, you can prevent cracks by ensuring that the vertex positions for the shared vertices are identical.

    Since you're using a texture atlas, you won't even have to change textures between the different layers.