GLSL Programming/Unity/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.
It combines the shader code of Section “Textured Spheres” with concepts that were introduced in Section “Cutaways” and Section “Transparency”.
If you haven't read these tutorials, this would be a very good opportunity to read them.
Discarding Transparent Fragments
editLet's start with discarding fragments as explained in Section “Cutaways”. Follow the steps described in Section “Textured Spheres” and assign the image to the left to the material of a sphere with the following shader:
Shader "GLSL texturing with alpha discard" {
Properties {
_MainTex ("RGBA Texture Image", 2D) = "white" {}
_Cutoff ("Alpha Cutoff", Float) = 0.5
}
SubShader {
Pass {
Cull Off // since the front is partially transparent,
// we shouldn't cull the back
GLSLPROGRAM
uniform sampler2D _MainTex;
uniform float _Cutoff;
varying vec4 textureCoordinates;
#ifdef VERTEX
void main()
{
textureCoordinates = gl_MultiTexCoord0;
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}
#endif
#ifdef FRAGMENT
void main()
{
gl_FragColor =
texture2D(_MainTex, vec2(textureCoordinates));
if (gl_FragColor.a < _Cutoff)
// alpha value less than user-specified threshold?
{
discard; // yes: discard this fragment
}
}
#endif
ENDGLSL
}
}
// The definition of a fallback shader should be commented out
// during development:
// Fallback "Unlit/Transparent Cutout"
}
The fragment shader reads the RGBA texture and compares the alpha value against a user-specified threshold. If the alpha value is less than the threshold, the fragment is discarded and the surface appears transparent.
Alpha Testing
editThe same effect as described above can be implemented with an alpha test. The advantage of the alpha test is that it runs also on older hardware that doesn't support GLSL. Here is the code, which results in more or less the same result as the shader above:
Shader "GLSL texturing with alpha test" {
Properties {
_MainTex ("RGBA Texture Image", 2D) = "white" {}
_Cutoff ("Alpha Cutoff", Float) = 0.5
}
SubShader {
Pass {
Cull Off // since the front is partially transparent,
// we shouldn't cull the back
AlphaTest Greater [_Cutoff] // specify alpha test:
// fragment passes if alpha is greater than _Cutoff
GLSLPROGRAM
uniform sampler2D _MainTex;
uniform float _Cutoff;
varying vec4 textureCoordinates;
#ifdef VERTEX
void main()
{
textureCoordinates = gl_MultiTexCoord0;
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}
#endif
#ifdef FRAGMENT
void main()
{
gl_FragColor =
texture2D(_MainTex, vec2(textureCoordinates));
}
#endif
ENDGLSL
}
}
// The definition of a fallback shader should be commented out
// during development:
// Fallback "Unlit/Transparent Cutout"
}
Here, no explicit discard
instruction is necessary but the alpha test has to be configured to pass only those fragments with an alpha value of more than the _Cutoff
property; otherwise they are discarded. More details about the alpha test are available in Unity's ShaderLab documentation.
Note that the alpha test and the discard
instruction are rather slow on some platforms, in particular on mobile devices. Thus, blending is often a more efficient alternative.
Blending
editThe Section “Transparency” described how to render semitransparent objects with alpha blending. Combining this with an RGBA texture results in this code:
Shader "GLSL texturing with alpha blending" {
Properties {
_MainTex ("RGBA Texture Image", 2D) = "white" {}
}
SubShader {
Tags {"Queue" = "Transparent"}
Pass {
Cull Front // first render the back faces
ZWrite Off // don't write to depth buffer
// in order not to occlude other objects
Blend SrcAlpha OneMinusSrcAlpha
// blend based on the fragment's alpha value
GLSLPROGRAM
uniform sampler2D _MainTex;
varying vec4 textureCoordinates;
#ifdef VERTEX
void main()
{
textureCoordinates = gl_MultiTexCoord0;
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}
#endif
#ifdef FRAGMENT
void main()
{
gl_FragColor =
texture2D(_MainTex, vec2(textureCoordinates));
}
#endif
ENDGLSL
}
Pass {
Cull Back // now render the front faces
ZWrite Off // don't write to depth buffer
// in order not to occlude other objects
Blend SrcAlpha OneMinusSrcAlpha
// blend based on the fragment's alpha value
GLSLPROGRAM
uniform sampler2D _MainTex;
varying vec4 textureCoordinates;
#ifdef VERTEX
void main()
{
textureCoordinates = gl_MultiTexCoord0;
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}
#endif
#ifdef FRAGMENT
void main()
{
gl_FragColor =
texture2D(_MainTex, vec2(textureCoordinates));
}
#endif
ENDGLSL
}
}
// The definition of a fallback shader should be commented out
// during development:
// Fallback "Unlit/Transparent"
}
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 in both passes:
Blend One OneMinusSrcAlpha
Blending with Customized Colors
editWe 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:
Shader "GLSL semitransparent colors based on alpha" {
Properties {
_MainTex ("RGBA Texture Image", 2D) = "white" {}
}
SubShader {
Tags {"Queue" = "Transparent"}
Pass {
Cull Front // first render the back faces
ZWrite Off // don't write to depth buffer
// in order not to occlude other objects
Blend SrcAlpha OneMinusSrcAlpha
// blend based on the fragment's alpha value
GLSLPROGRAM
uniform sampler2D _MainTex;
varying vec4 textureCoordinates;
#ifdef VERTEX
void main()
{
textureCoordinates = gl_MultiTexCoord0;
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}
#endif
#ifdef FRAGMENT
void main()
{
gl_FragColor =
texture2D(_MainTex, vec2(textureCoordinates));
if (gl_FragColor.a > 0.5) // opaque back face?
{
gl_FragColor = vec4(0.0, 0.0, 0.2, 1.0);
// opaque dark blue
}
else // transparent back face?
{
gl_FragColor = vec4(0.0, 0.0, 1.0, 0.3);
// semitransparent dark blue
}
}
#endif
ENDGLSL
}
Pass {
Cull Back // now render the front faces
ZWrite Off // don't write to depth buffer
// in order not to occlude other objects
Blend SrcAlpha OneMinusSrcAlpha
// blend based on the fragment's alpha value
GLSLPROGRAM
uniform sampler2D _MainTex;
varying vec4 textureCoordinates;
#ifdef VERTEX
void main()
{
textureCoordinates = gl_MultiTexCoord0;
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}
#endif
#ifdef FRAGMENT
void main()
{
gl_FragColor =
texture2D(_MainTex, vec2(textureCoordinates));
if (gl_FragColor.a > 0.5) // opaque front face?
{
gl_FragColor = vec4(0.0, 1.0, 0.0, 1.0);
// opaque green
}
else // transparent front face
{
gl_FragColor = vec4(0.0, 0.0, 1.0, 0.3);
// semitransparent dark blue
}
}
#endif
ENDGLSL
}
}
// The definition of a fallback shader should be commented out
// during development:
// Fallback "Unlit/Transparent"
}
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 2 and dims the red and blue components by multiplying them with 0.5. However, this results in oversaturated green that is clamped to the maximum intensity. This can be avoided by halving 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
editCongratulations! 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 the alpha test can be used to achieve the same effect.
- How alpha texture maps can be used for blending.
- How alpha texture maps can be used to determine colors.
Further Reading
editIf you still want to know more
- about texturing, you should read Section “Textured Spheres”.
- about discarding fragments, you should read Section “Cutaways”.
- about the alpha test, you should read Unity's ShaderLab documentation: Alpha testing.
- about blending, you should read Section “Transparency”.