Pythonstuff GLSL in English Pythonstuff GLSL auf Deutsch Pythonstuff GLSL Pythonstuff
PythonStuff Home


Example 5 - GLSL Parallax Mapping with Pyglet

Based on the previous example we do Parallax Mapping.

What is parallax mapping ?

As Wikipedia says:

Parallax mapping is implemented by displacing the texture coordinates at a point on the rendered polygon by a function of the view angle in tangent space (the angle relative to the surface normal) and the value of the height map at that point. At steeper view-angles, the texture coordinates are displaced more, giving the illusion of depth due to parallax effects as the view changes.

Or - as a picture says more:


The result looks like this:

with_parallax.jpg without_parallax.jpg

In the left picture the red pyramid tops look shifted sideways when they are not oriented towards the viewer. This effect can never be realized by texture mapping alone (even bump mapping just changes the lighting - see the picture to the right for comparison).

Even better do the pyramids look if they are upside down (concave) - so you are not irritated by the missing silhouette:


Parallax mapping looks best in movement, but here is another one:

ziegel_mit_parallaxe.jpg ziegel_ohne_parallaxe.jpg

In the left screenshot you can see the cement between the bricks only in the center of the picture - under steeper angles the bricks hide the cement. In the right screenshot the “parallax height” parameter is set to zero, so you see just the “bump mapped” bricks.

The textures I use in this example are in the Texture Pack 3.

To run this demo, you need Pyglet and Tristam Macdonald's Library You find everything on the installation page.

Program description

To properly view this effect (and its limits), I added a “zoom” keyboard function:

global dist
def on_draw():
    glTranslatef(0.0, 0.0, dist); # instead of fixed "-3.5"
def on_key_press(...):
    elif symbol == key.PLUS:
        dist += 0.5
        print 'Distance now ', dist
    elif symbol == key.MINUS:
        dist -= 0.5
        print 'Distance now ', dist

We also need a value for scaling the effect:

global parallaxheight
def on_draw():
    shader.uniformf('parallaxheight', parallaxheight )
def on_key_press(...):
    elif symbol == key.P:
        parallaxheight += 0.01
        print 'Parallax Height now ', parallaxheight
    elif symbol == key.O:
        parallaxheight -= 0.01
        if parallaxheight <= 0.0:
            parallaxheight = 0.0
            print 'Parallax now OFF'
            print 'Parallax Height now ', parallaxheight

then add another texture, the “height texture”. This is an 8bit-greyscale image, “black” meaning “lowest” and “white” means “highest”:

texturecnt = 3          # Texturemap0.jpg = Colormap Texturemap1.jpg = Bumpmap Texturemap2.jpg = Heightmap

That is all there is in the surrounding Python program, so we can concentrate on the GLSL-Effect in the pixel shader. For a bumpmapped pixel we sampled the Colormap:

vec4 texColor = vec4(texture2D(my_color_texture[0], gl_TexCoord[0].st).rgb, 1.0);

Now we look at the “height” at the position:

vec2 coords1 = gl_TexCoord[0].st;
float height1 = parallaxheight * (texture2D( my_color_texture[2], coords1).r - 0.5);

The value -0.5..+0.5 (after scaling) sampled from the heightmap is multiplied by our value “parallaxheight” - if we set “parallaxheight” to zero by pressing “O” repeatedly, we get a “height1” of zero, meaning “just bumpmap, no parallax effect” (try it !).

Now we do not select the color pixel from gl_TexCoord[0].st as in

vec4 texColor = vec4(texture2D(my_color_texture[0], gl_TexCoord[0].st).rgb, 1.0);

but from gl_TexCoord[0].st + offset1 as in

offset1  = height1 *  vec2( eye.x, eye.y );
vec4 texColor = vec4(texture2D(my_color_texture[0], gl_TexCoord[0].st + offset1).rgb, 1.0);

where the place of sampling is moved away in eye direction (projected to the texture) proportional to the height we just calculated.

This follows the original implementation of Tomomichi Kaneko from 2001. There have been various improvements since then.

  • You could limit the displacement that could get quite large in case of pathological eye angles.
  • You could check for a “higher” part of the height map in the line of sight, giving correct occlusion (“Steep Parallax Mapping”)
  • You could do a light vector calculation in addition to the eye vector and implement very detailed shadows on the surface
  • You could improve the precision of the intersection coordinate by some kind of interpolation

and more.

In my source file I tried to improve the intersection search by doing one interpolation step between height values. So I reduce the artefacts introduced by too steep height changes (a bit):

vec2  coords1 = gl_TexCoord[0].st;
vec2  coords2  = coords1 + offset1;
float height2 = parallaxheight * (texture2D( my_color_texture[2], coords2).r - 0.5);
vec2  newCoords = coords2;
vec2  newCoords = coords1 + (height2/height1) * offset1;
vec4  texColor = vec4(texture2D(my_color_texture[0], newCoords).rgb, 1.0);

Enjoy !

If you have enjoyed this enough, let's get continue with Example 6 - Vertex Offset Shader for deformable Objects.

Deutschsprachige Version, Start
Impressum & Disclaimer