GLSL Programming/Blender/Transparent Textures

This tutorial covers various common uses of alpha texture maps, i.e. RGBA texture images with an A (alpha) component that specifies the opacity of texels.

Map of the Earth with transparent water, i.e. the alpha component is 0 for water and 1 for land.

It combines the shader code of the tutorial on textured spheres with concepts that were introduced in the tutorial on cutaways and the tutorial on transparency.

If you haven't read these tutorials, this would be a very good opportunity to read them.

Discarding Transparent Fragments

edit

Let's start with discarding fragments as explained in the tutorial on cutaways. Follow the steps described in the tutorial on textured spheres and assign the image to the left to the material of a sphere with the following shader:

import bge
 
cont = bge.logic.getCurrentController()
 
VertexShader = """
   varying vec4 texCoords; // texture coordinates at this vertex
 
   void main()
   {
      texCoords = gl_MultiTexCoord0; // in this case equal to gl_Vertex
      gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
   }
"""
 
FragmentShader = """
   uniform float cutoff;

   varying vec4 texCoords; 
      // interpolated texture coordinates for this fragment
   uniform sampler2D textureUnit; 
      // a small integer identifying a texture image
 
   void main()
   {
      vec2 longitudeLatitude = vec2(
         (atan(texCoords.y, texCoords.x) / 3.1415926 + 1.0) * 0.5, 
         1.0 - acos(texCoords.z) / 3.1415926);
         // processing of the texture coordinates; 
         // this is unnecessary if correct texture coordinates 
         // are specified within Blender
 
      gl_FragColor = texture2D(textureUnit, longitudeLatitude);

      if (gl_FragColor.a < cutoff)
         // alpha value less than user-specified threshold?
      {
         discard; // yes: discard this fragment
      }
   }
"""
 
mesh = cont.owner.meshes[0]
for mat in mesh.materials:
    shader = mat.getShader()
    if shader != None:
        if not shader.isValid():
            shader.setSource(VertexShader, FragmentShader, 1)
            shader.setSampler('textureUnit', 0)
            shader.setUniform1f('cutoff', cont.owner.get('cutoff'))

This script uses a game property called cutoff to control the uniform variable of the same name. If you start the game, Blender should complain in the system console (available via Help > Toggle System Console in the menu of an Info window) about a missing floating-point value in the line

shader.setUniform1f('cutoff', cont.owner.get('cutoff'))

because we haven't defined the game property yet. To do so, go to the Logic Editor and click on Add Game Property (open the Properties by pressing n if they aren't opened already). The name should be cutoff and the type should be float. Set the value to   (anything a bit away from 0 and 1 will do).

If you start the game engine now, the fragment shader should read the RGBA texture and compare the alpha value against the threshold specified in the game property cutoff. If the alpha value is less than the threshold, the fragment is discarded and the surface appears transparent.

Since we can look through the transparent parts, it makes sense to deactivate backface culling as described in the tutorial on cutaways: choose the Blender Game engine in the menu of an Info window and then (in the Material tab of the Properties window of the sphere) uncheck Game Settings > Backface Culling. (Note that the usual depth test makes sure that the triangle occlusions are correct, even without checking Transparency in the Material tab.)

Blending

edit

The tutorial on transparency described how to render semitransparent objects with alpha blending. Combining this with an RGBA texture results in this fragment shader (with the same vertex shader as above):

import bge
 
cont = bge.logic.getCurrentController()
 
VertexShader = """
   varying vec4 texCoords; // texture coordinates at this vertex
 
   void main()
   {
      texCoords = gl_MultiTexCoord0; // in this case equal to gl_Vertex
      gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
   }
"""
 
FragmentShader = """
   uniform float cutoff;

   varying vec4 texCoords; 
      // interpolated texture coordinates for this fragment
   uniform sampler2D textureUnit; 
      // a small integer identifying a texture image
 
   void main()
   {
      vec2 longitudeLatitude = vec2(
         (atan(texCoords.y, texCoords.x) / 3.1415926 + 1.0) * 0.5, 
         1.0 - acos(texCoords.z) / 3.1415926);
 
      gl_FragColor = texture2D(textureUnit, longitudeLatitude);
   }
"""
 
mesh = cont.owner.meshes[0]
for mat in mesh.materials:
    shader = mat.getShader()
    if shader != None:
        if not shader.isValid():
            shader.setSource(VertexShader, FragmentShader, 1)
            shader.setSampler('textureUnit', 0)
            mat.setBlending(bge.logic.BL_SRC_ALPHA, 
                            bge.logic.BL_ONE_MINUS_SRC_ALPHA)

Unfortunately, you will see severe rendering artifacts in Blender 2.63 if you deactivate backface culling, even if Transparency > Z Transparency is activated (in the Material tab of the Properties window). Apparently, Blender 2.63 doesn't always sort all triangles according to their depth, which is a problem in this case. However, changing Game Settings > Alpha Blend to Alpha Sort will tell Blender to sort all of the triangles for that material correcting the problem. (This should only be used when needed as sorting is costly, and is usually not necessary)

Note that all texels with an alpha value of 0 are black in this particular texture image. In fact, the colors in this texture image are “premultiplied” with their alpha value. (Such colors are also called “opacity-weighted.”) Thus, for this particular image, we should actually specify the blend equation for premultiplied colors in order to avoid another multiplication of the colors with their alpha value in the blend equation. Therefore, an improvement of the shader (for this particular texture image) is to employ the following blend specification:

mat.setBlending(bge.logic.BL_ONE, bge.logic.BL_ONE_MINUS_SRC_ALPHA)

 
Semitransparent globes are often used for logos and trailers.

Blending with Customized Colors

edit

We should not end this tutorial without a somewhat more practical application of the presented techniques. To the left is an image of a globe with semitransparent blue oceans, which I found on Wikimedia Commons. There is some lighting (or silhouette enhancement) going on, which I didn't try to reproduce. Instead, I only tried to reproduce the basic idea of semitransparent oceans with the following shader, which ignores the RGB colors of the texture map and replaces them by specific colors based on the alpha value:

import bge
 
cont = bge.logic.getCurrentController()
 
VertexShader = """
   varying vec4 texCoords; // texture coordinates at this vertex
 
   void main()
   {
      texCoords = gl_MultiTexCoord0; // in this case equal to gl_Vertex
      gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
   }
"""
 
FragmentShader = """
   uniform float cutoff;

   varying vec4 texCoords; 
      // interpolated texture coordinates for this fragment
   uniform sampler2D textureUnit; 
      // a small integer identifying a texture image
 
   void main()
   {
      vec2 longitudeLatitude = vec2(
         (atan(texCoords.y, texCoords.x) / 3.1415926 + 1.0) * 0.5, 
         1.0 - acos(texCoords.z) / 3.1415926);
 
      gl_FragColor = texture2D(textureUnit, longitudeLatitude);
      if (gl_FragColor.a > 0.5) // opaque 
      {
         gl_FragColor = vec4(0.0, 1.0, 0.0, 1.0); // opaque green
      }
      else // transparent 
      {
         gl_FragColor = vec4(0.0, 0.0, 0.5, 0.7); 
            // semitransparent dark blue
      }
   }
"""
 
mesh = cont.owner.meshes[0]
for mat in mesh.materials:
    shader = mat.getShader()
    if shader != None:
        if not shader.isValid():
            shader.setSource(VertexShader, FragmentShader, 1)
            shader.setSampler('textureUnit', 0)
            mat.setBlending(bge.logic.BL_SRC_ALPHA, 
                            bge.logic.BL_ONE_MINUS_SRC_ALPHA)

If you deactivate backface culling, you will notice that there are rendering artifacts in Blender 2.63. Hopefully, this issue will be improved in coming versions.

Of course, it would be interesting to add lighting and silhouette enhancement to this shader. One could also change the opaque, green color in order to take the texture color into account, e.g. with:

gl_FragColor = vec4(0.5 * gl_FragColor.r, 2.0 * gl_FragColor.g, 0.5 * gl_FragColor.b, 1.0);

which emphasizes the green component by multiplying it with   and dims the red and blue components by multiplying them with  . However, this results in oversaturated green that is clamped to the maximum intensity. This can be avoided by halvening the difference of the green component to the maximum intensity 1. This difference is 1.0 - gl_FragColor.g; half of it is 0.5 * (1.0 - gl_FragColor.g) and the value corresponding to this reduced distance to the maximum intensity is: 1.0 - 0.5 * (1.0 - gl_FragColor.g). Thus, in order to avoid oversaturation of green, we could use (instead of the opaque green color):

gl_FragColor = vec4(0.5 * gl_FragColor.r, 1.0 - 0.5 * (1.0 - gl_FragColor.g), 0.5 * gl_FragColor.b, 1.0);

In practice, one has to try various possibilities for such color transformations. To this end, the use of numeric shader properties (e.g. for the factors 0.5 in the line above) is particularly useful to interactively explore the possibilities.

Summary

edit

Congratulations! You have reached the end of this rather long tutorial. We have looked at:

  • How discarding fragments can be combined with alpha texture maps.
  • How alpha texture maps can be used for blending.
  • How alpha texture maps can be used to determine colors.

Further Reading

edit

If you still want to know more


< GLSL Programming/Blender

Unless stated otherwise, all example source code on this page is granted to the public domain.