SpriteKit game scene transitions with shaders
Deon Botha• Jul 18, 2015The casual game side project I’m working on has recently entered the last 20% phase of development (no doubt where 80% of my time will be invested). Game scene transitions are something I’ve been putting off for a while that I now have the pleasure of grinding through. I wanted to achieve a very “retro” feel with the transitions – the current result is looking like this:
The video shows two transitions (picked at random from a list of many similarly styled transitions) triggered when moving between the in game experience to the world map and vice versa. The transitions are implemented using OpenGL Shaders.
I thought I’d share my approach to SpriteKit scene transition using shaders as it wasn’t as straight forward as I expected.
Transitions using Shaders
SpriteKit scene transition is typically done using one of two  SKView’s presentScene 
methods, one of which takes an SKTransition argument
func presentScene(scene: SKScene?);
func presentScene(scene: SKScene?, transition: SKTransition?);Unfortunately, out the box SKTransition only offers a limited set of transition styles and 
no real support to extend these styles using a shader. So the only option available to us if 
we want to transition between two scenes using a shader is:
- Attach and run the shader transition for the current scene
- Transition “immediately” to the next scene using presentScene
- Optionally attach and run a second shader or animation on the new scene to “smooth” the transition
This approach assumes the hiding of the current scene and display of the new scene happens as two discrete steps i.e. both scenes aren’t partially visible at the same time. If your transition calls for the two scenes to be partially visible in parallel at any point you’ll have some additional work.
I really wanted to maintain SpriteKit’s simplicity for switching scenes by calling
just a single function, unfortunately this isn’t possible due to needing to update the 
elapsed time for the transition shader on each SKScene update(currentTime:). 
 In the end I settled on the following three functions to perform a transition between scenes:
func presentScene(scene: SKScene, 
             shaderName: String, 
     transitionDuration: NSTimeInterval);
func updateShaderTransition(currentTime: CFTimeInterval);
func completeShaderTransition();These functions are implemented within a Swift class extension on SKScene so that you can easily
call them in place of the two SKView’s presentScene methods shown above. We’ll take a look
at the implementation of each function in turn in the sections that follow.
Presenting the new scene
To initiate a scene transition, presentScene(scene:shaderName:transitionDuration:) 
is called passing the name of a shader file & the transition animation duration. The
implementation of the function is as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/* SKScene+ShaderTransition.swift */
func presentScene(scene: SKScene, 
             shaderName: String, 
     transitionDuration: NSTimeInterval) {
    
    // Create shader and add it to the scene
    var shaderContainer = SKSpriteNode(imageNamed: "dummy")
    shaderContainer.name = kNodeNameTransitionShaderNode
    shaderContainer.position = 
        CGPointMake(size.width / 2, size.height / 2)
    shaderContainer.size = 
        CGSizeMake(size.width, size.height)
    shaderContainer.shader = 
        createShader(shaderName, 
            transitionDuration:transitionDuration)
    shaderContainer.zPosition = 9999 
    self.addChild(shaderContainer)
    
    // remove the shader from the scene after the 
    // transition animation has completed:
    let delayTime = dispatch_time(DISPATCH_TIME_NOW, 
        Int64(transitionDuration * Double(NSEC_PER_SEC)))
    
    dispatch_after(delayTime, dispatch_get_main_queue(), 
        { () -> Void in
            var fadeOverlay = 
                SKShapeNode(rect: 
                    CGRectMake(0, 0, 
                        self.size.width, 
                        self.size.height))
                        
            fadeOverlay.name = kNodeNameFadeColourOverlay
            fadeOverlay.zPosition = shaderContainer.zPosition
            fadeOverlay.fillColor = 
                SKColor(red: 131.0 / 255.0, 
                      green: 149.0 / 255.0, 
                       blue: 255.0 / 255.0, 
                      alpha: 1.0)
                     
            scene!.addChild(fadeOverlay)
            self.view!.presentScene(scene)
        }
    )
    
    // Reset the time presentScene was called so that the 
    // elapsed time from now can be calculated in 
    // updateShaderTransitions(currentTime:)
    presentationStartTime = -1
}
Lines 8-18 create an SKShader, attach
it to a dummy SKSpriteNode and then add the node to the view. The dummy image is just a transparent 1x1 pixel png. We give 
the SKSpriteNode a name so that we can get at it’s shader from within other methods in the
extension. The zPosition is set to something suitably large so that it’s guaranteed to 
be in the foreground.
Lines 25-44 dispatch an asynchronous block to be run when the transition animation 
completes. This block is responsible for adding an SKShapeNode that covers the new 
SKScene with an opaque overlay and then immediately presenting it. 
The overlay covers the entirety of the screen and has the same fill colour that 
the SKShader transition animation ends up filling the screen with. The new scene can now call 
completeShaderTransition() when it wants to complete the transition and gracefully 
fade in.
Updating the shader transition
The shader itself has several uniform variables one of which is u_elapsed_time – 
this variable indicates the total time that has elapsed since the start of the transition. 
Updating this uniform variable is the responsibility of the SKScene extension’s 
updateShaderTransition(currentTime:) method:
/* SKScene+ShaderTransition.swift */
func updateShaderTransition(currentTime: CFTimeInterval) {
    if let shader = self.transitionShader {
        let elapsedTime = shader.uniformNamed("u_elapsed_time")!
        if (presentationStartTime < 0) {
            presentationStartTime = currentTime
        }
        elapsedTime.floatValue = 
            Float(currentTime - presentationStartTime)
    }
}presentationStartTime is set to -1 whenever presentScene is called, it then gets
 set to the currentTime next time updateShaderTransition is called and from then on
 we have a reference point as to when the transition animation began.
The above needs to be called from every SKScene update(currentTime:) method
that wants to support shader transitions:
override func update(currentTime: NSTimeInterval) {
    updateShaderTransitions(currentTime)
    // do your usual other updates
}Completing the transition
Once the presented scene is ready to be revealed (perhaps immediately, or perhaps after loading all required 
assets, etc) it calls completeShaderTransition(). This function retrieves the SKShapeNode
overlay and fades it out to gracefully reveal the new scene and complete the transition:
/* SKScene+ShaderTransition.swift */
func completeShaderTransition() {
    if let fadeOverlay = 
            self.childNodeWithName(kNodeNameFadeColourOverlay) {
        fadeOverlay.runAction(
            SKAction.sequence(
                [SKAction.fadeAlphaTo(0, duration: 0.3), 
                 SKAction.removeFromParent()]
            )
        )
    }
}Example retro transition shader
The shader itself has several uniform variables (in addition to those that SpriteKit provides)
- u_total_animation_duration- The total duration of the transition animation
- u_elapsed_time- The time that has elapsed since calling- presentScene. It’s updated every frame in the- updateShaderTransition(currentTime:)method
- u_fill_colour- The individual diamond tile fill colour
- u_border_colour- The individual diamond tile stroke colour
/* retro_transition_fade_from_top.fsh */
int NUM_COLUMNS = 15;
void main( void ) {
    float tileSize = u_size.x / float(NUM_COLUMNS);
    int NUM_ROWS = int(ceil(u_size.y / tileSize));
    
    int column = int(floor(gl_FragCoord.x / tileSize));
    int row = int(floor(gl_FragCoord.y / tileSize));
    
    vec2 pos = mod(gl_FragCoord.xy, 
        vec2(tileSize)) - vec2(tileSize / 2.0);
    float singleTileAnimDuration = 
        u_total_animation_duration / 3.0;
    float animStartOffset = 
        (float(NUM_ROWS - row) / float(NUM_ROWS)) * 
        (u_total_animation_duration - singleTileAnimDuration);
    float elapsedTileAnimTime = 
        min(
            max(0.0, u_elapsed_time - animStartOffset), 
            singleTileAnimDuration);
    float strokeSize = 3.0;
    float tileRadius = 
        (elapsedTileAnimTime / singleTileAnimDuration) * 
        (tileSize + strokeSize);
  
    if (abs(pos.x) + abs(pos.y) < tileRadius - strokeSize) {
        gl_FragColor = u_fill_colour;
    } else if (abs(pos.x) + abs(pos.y) < tileRadius) {
        gl_FragColor = u_border_colour;
    } else {
        gl_FragColor = SKDefaultShading();
    }
}This shader creates the fade down from the top of the screen effect that can be seen as 
the first transition in the video at the top of the page. The shader itself divides the screen 
into NUM_COLUMNS columns and a dynamic number of rows. Within each tileSizextileSize space 
in the grid a diamond shape is drawn. The diamond starts small (it’s initial scale is 0) and 
gradually fills it’s tile space over singleTileAnimDuration seconds. 
Each row of tiles has it’s individual tile animation start time delayed/offset 
by animStartOffset based on the rows distance from from the top – the top row of 
tiles has animStartOffset = 0, the bottom row 
animStartOffset = u_total_animation_duration - singleTileAnimDuration and everything else
has something in between the two.
Source code
Full source code in the form of an example app can be found on GitHub