An iOS project I’m working on at the OTHER media required that a 3D object in OpenGL is given a temporary texture while the much larger desired texture is loaded. Once the desired texture was loaded it should be switched with the temporary one. But instead of just dropping the texture in I wanted it to fade between the two textures to give a much more subtle transition. One approach would have been two have two identical 3D objects in the same space (one with the temporary texture, the other with the desired texture) and fade the second object in once the desired texture was loaded, however this requires that OpenGL draws a second object and could also cause rendering issues with the depth buffer if both objects are in the exact same space.
This post describes the better approach I came up. You can download the code from my GitHub repo, it’s contained in the CrossFadingTextures directory.
This approach uses a single OpenGL object that is given two textures using two separate texture units and shader samplers. The fragment shader then fades between the two textures by adding the colours together.

Most of the magic happens in ASDrawable, an NSObject subclass that represents the drawable OpenGL object. It contains several boilerplate methods for loading and compiling the shader and linking the program. The setUp, setUpVertexBuffer and teardown methods prepare the drawable, load some geometry into a vertex buffer and unload the drawable, respectively. Most of the actual rendering code lives in the update method, which sets the state of the drawable, and the draw method which handles actually rendering it. The last important method is setTexture:animationDuration:, which I’ll come to later.
The project also contains a single view controller (ASViewController) which is a subclass of GLKViewController. It is responsible for creating an instance of ASDrawable, presenting a button for switching the texture and providing the drawable with a new texture.
Before we start, let’s look at the instance variables on ASDrawable.
@interface ASDrawable () {
GLint uniforms[NUM_UNIFORMS];
GLuint vertexBuffer;
GLuint shaderProgram;
GLKMatrix4 baseModelViewMatrix;
GLKMatrix4 modelViewProjectionMatrix;
GLKMatrix3 normalMatrix;
GLuint currentTexture;
GLuint animatingTexture;
BOOL animating;
float animationProgress;
NSTimeInterval animationDuration;
NSTimeInterval timestamp;
}
Most of these are fairly boilerplate OpenGL variables for storing uniforms, a vertex buffer, a shader program and various matrices. currentTexture holds the texture identifier for the texture we are currently rendering. animatingTexture holds the texture identifier for the texture we are animating to. When the animation is complete the value of animatingTexture will be moved to currentTexture and animatingTexture will be set back to 0. animating is a boolean flag to show when an animation is taking place. animationProgress stores a value between 0 and 1 that represents the progress through the animation with 0 being not yet started and 1 being complete.
The transition starts when the Fade button is tapped. It calls setTexture:animationDuration: on the drawable passing a new UIImage texture and a number of seconds for the duration of the animation.
Let’s have a look at the implementation of setTexture:animationDuration:
- (void)setTexture:(UIImage *)texture animationDuration:(NSTimeInterval)duration {
if (animatingTexture != 0) {
glDeleteTextures(1, &animatingTexture);
animatingTexture = 0;
}
animatingTexture = [GLKTextureLoader textureWithCGImage:texture.CGImage options:nil error:nil].name;
animating = YES;
animationProgress = 0.0f;
animationDuration = duration;
}
We start by deleting the existing animatingTexture if there is one. Then we load the new texture using GLKTextureLoader and store the identifier in animatingTexture. This could be optimised by loading the texture asynchronously but as the texture is quite small this method doesn’t create any noticeable lag. Finally we set the flag to say we are animating, reset the animation progress to zero and store the duration of the animation.
The next stage of the transition happens in the update method.
- (void)update {
NSTimeInterval now = [NSDate timeIntervalSinceReferenceDate];
NSTimeInterval timeSinceLastUpdate = now - timestamp;
GLKMatrix4 modelViewMatrix = GLKMatrix4Rotate(baseModelViewMatrix, GLKMathDegreesToRadians(_rotation), 0.0f, 0.0f, 1.0f);
normalMatrix = GLKMatrix3InvertAndTranspose(GLKMatrix4GetMatrix3(modelViewMatrix), NULL);
modelViewProjectionMatrix = GLKMatrix4Multiply(_projectionMatrix, modelViewMatrix);
if (animating) {
animationProgress += timeSinceLastUpdate / animationDuration;
if (animationProgress > 1.0f) {
animationProgress = 0.0;
animating = NO;
if (animatingTexture != 0) {
glDeleteTextures(1, ¤tTexture);
currentTexture = animatingTexture;
animatingTexture = 0;
}
}
}
timestamp = now;
}
First we create a timestamp to keep track of time between updates and update the model view, normal and model view projection matrices. Then we check animating to see if an animation is in progress. If it is we increment animationProgress and check if the animation is complete. If the animation is complete we reset the progress and flag, delete the old texture stored in currentTexture and move the new texture stored in animatingTexture into currentTexture before reseting animatingTexture to 0.
Finally we need to look at the draw method to see how these textures are getting rendered:
- (void)draw {
glEnable(GL_DEPTH_TEST);
glDepthMask(GL_TRUE);
glDisable(GL_BLEND);
glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer);
GLuint offset = 0;
glEnableVertexAttribArray(ATTRIB_VERTEX);
glEnableVertexAttribArray(ATTRIB_NORMAL);
glEnableVertexAttribArray(ATTRIB_TEXCOORD);
glVertexAttribPointer(ATTRIB_VERTEX, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * GLTriangleStride, (const void*)offset);
offset += 3 * sizeof(GLfloat);
glVertexAttribPointer(ATTRIB_NORMAL, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * GLTriangleStride, (const void*)offset);
offset += 3 * sizeof(GLfloat);
glVertexAttribPointer(ATTRIB_TEXCOORD, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * GLTriangleStride, (const void*)offset);
glUseProgram(shaderProgram);
glUniformMatrix4fv(uniforms[UNIFORM_MODELVIEWPROJECTION_MATRIX], 1, 0, modelViewProjectionMatrix.m);
glUniformMatrix3fv(uniforms[UNIFORM_NORMAL_MATRIX], 1, 0, normalMatrix.m);
glUniform1i(uniforms[UNIFORM_SAMPLER], 0);
glUniform1i(uniforms[UNIFORM_SAMPLER_TWO], 1);
if (animating) glUniform1f(uniforms[UNIFORM_TEXALPHA], animationProgress);
else glUniform1f(uniforms[UNIFORM_TEXALPHA], 0.0f);
glActiveTexture(GL_TEXTURE0); // Sampler 1
glBindTexture(GL_TEXTURE_2D, currentTexture);
if (animatingTexture != 0) {
glActiveTexture(GL_TEXTURE1); // Sampler 2
glBindTexture(GL_TEXTURE_2D, animatingTexture);
}
glDrawArrays(GL_TRIANGLES, 0, 6);
}
Most of this is boilerplate OpenGL rendering code. The important part is:
if (animating) glUniform1f(uniforms[UNIFORM_TEXALPHA], animationProgress);
else glUniform1f(uniforms[UNIFORM_TEXALPHA], 0.0f);
This sets a float uniform in the shaders that will be used to crossfade the two textures. If an animation is in progress we are using animationProgress, otherwise we just pass 0. The last part of the method binds currentTexture to first texture unit (GL_TEXTURE0) and animatingTexture to the second texture unit (GL_TEXTURE1), if there is one, before drawing the geometry.
The last important part to look at is the fragment shader (Shader.fsh) which handles actually combining the textures.
varying lowp vec4 colorVarying;
varying lowp vec2 texCoordVarying;
uniform sampler2D texture;
uniform sampler2D textureTwo;
uniform lowp float texAlpha;
void main() {
mediump vec4 texColor = texture2D(texture, texCoordVarying);
mediump vec4 texTwoColor = texture2D(textureTwo, texCoordVarying);
gl_FragColor = ((texColor * (1.0 - texAlpha)) + (texTwoColor * texAlpha)) * colorVarying;
}
texture is bound to the first texture unit and therefore contains the current texture. textureTwo is bound to the second texture unit and therefore contains the animating texture. The shader loads the two colours from the two textures and combines them using texAlpha (the animation progress). For example, if the animation is 25% complete, the shader will add together 75% of the current texture and 25% of the animating texture.
And that’s it! We have one OpenGL object that animates between two textures. Have a play with the source code and let me know if you have any thoughts.







