7
\$\begingroup\$

I have a background of a given size and filled with a given color. I want to change it with an animation effect, starting from the center and spread out until it extends the whole background. The new color should fade / blend smooth into the existing color from the background. Some kind of radial gradient that changes the color and then spreads out over the whole background.

I am working with SpriteKit on iOS and I am really sure that the best way to implement this is to do this with fragment shaders which are new to iOS 8 SpriteKit SDK. I have done some work with shaders and understand how they work but I am asking for help more on the mathematics behind this.

\$\endgroup\$
  • \$\begingroup\$ What did you try ? \$\endgroup\$ – concept3d Sep 23 '14 at 8:01
  • \$\begingroup\$ Nothing yet. Just worked with some Blooming-FX and some "Plasma FX" but I really can't get a get idea of a starting point for this ... \$\endgroup\$ – Simon Kemper Sep 23 '14 at 8:03
  • \$\begingroup\$ You might find helpful my question and its answer on stackoverflow. It covers how to do near-perfect gradient (linear, radial, elliptic, displacement, scaling and rotating depending on input). Can also be expanded to multi-stop gradient. \$\endgroup\$ – wondra Sep 23 '14 at 16:00
  • \$\begingroup\$ Also a good source to get a point to start from! Thanks! \$\endgroup\$ – Simon Kemper Sep 23 '14 at 17:44
11
\$\begingroup\$

Here's how I would do this. First, make sure you have the object's UVs or world coords (which you can pass through from your vertex shader) available to you. If it's just a background, you could also just use fragment coords (gl_FragCoord).

For instance, let's say we're using UV coords. A fragment shader with only: gl_FragColor = vec4(vec3(uv.x),1.0); will look something like:

X UV

And similarly, gl_FragColor = vec4(vec3(uv.y),1.0); will look like:

Y UV

If you're using world coords or fragment coords, you can still scale and translate to normalize to (0,0) -> (1, 1), but we're going to need to scale and translate again anyway so you can skip that step. Make your coords instead range from (-1.0, -1.0) to (1.0, 1.0) to make the next steps easier, like so: uv = 2.0 * uv - 1.0;

Now before we go any further, let's make a super plain radial gradient. Recall that the equation of a circle is x² + y² = r². So using the transformed UVs as a coordinate system, a unit circle (r = 1) can be made with gl_FragColor = vec4(vec3(uv.x * uv.x + uv.y * uv.y),1.0); and will look something like:

R UV

The next step is animation. To do this, you just need to play around with the "r" value and change the radius. You don't need to worry about clamping color values between 0.0 and 1.0 because that happens automatically anyway.

All together it would look something like this:

uniform vec2 screenSize;
uniform float time;

void main(void) {
    // Scale to UV space coords
    vec2 uv = gl_FragCoord.xy / screenSize;

    // Transform to [(-1.0, -1.0), (1.0, 1.0)] range
    uv = 2.0 * uv - 1.0;

    // Have something to vary the radius (can also just be a linear counter (time))
    float wave = sin(time);

    // Calculate how near to the center (0.0) or edge (1.0) this fragment is
    float circle = uv.x * uv.x + uv.y * uv.y;

    // Put it all together
    gl_FragColor = vec4(vec3(circle + wave),1.0);
}

Of course you can simplify a lot of things and play around much more with the variables and constants. Here's a live demo of me doing just that: https://www.shadertoy.com/view/4sjXWh

Edit:

If you want to do this with different colors, fortunately there's an easy solution! Pass in your colors as uniforms (or hardcode them in the actual fragment shader) and change the last line to this:

gl_FragColor = mix(color1, color2, circle + wave);

color1 and color2 are both vec4s. Here's that in action: https://www.shadertoy.com/view/XsjSW1

\$\endgroup\$
  • 1
    \$\begingroup\$ Brilliant! That's a good point for me to get started! But instead of animating the gradient in a loop I need a new gradient every time I change the color and start that animation on top of the old gradient. Like gradient color waves. I think I will use this shader as some kind of masking and apply it on sprites. And every time I change the color I add a new sprite with that color on top ... \$\endgroup\$ – Simon Kemper Sep 23 '14 at 17:20
  • 1
    \$\begingroup\$ Not sure those first two examples can both be gl_FragColor = vec4(vec3(uv.x),1.0); ? (Not being fussy - this answer helped me too!) \$\endgroup\$ – user5196 Sep 23 '14 at 18:26
  • \$\begingroup\$ @Poldie You're absolutely right; that's a typo. Fixed! \$\endgroup\$ – Yousef Amar Sep 23 '14 at 20:19
  • \$\begingroup\$ @SimonKemper You could also pass in the colors you want as uniforms and change them as needed when an animation cycle has ended (by checking the time variable outside the shader). I've added a bit to my answer to show you what I mean. Hope you find that useful! \$\endgroup\$ – Yousef Amar Sep 23 '14 at 20:36
2
\$\begingroup\$

Since I am working with SpriteKit on iOS the shader support in SpriteKit is still missing a few things to implement an animated color gradient shader in SpriteKit.

However for those who come up to the same situation I have found a more performant way to make animated gradients with sprite kit. Instead of working with shaders I have created a simple image with a blurred out circle ... Like this:

gradient sprite

and with a few lines of SpriteKit-Code you get nice and smooth blended gradient color waves starting from the center and extending all over the screen!

// Create a Sprite with Gradient-Texture
SKSpriteNode* gradientSpriteNode = [SKSpriteNode spriteNodeWithImageNamed:@"SSGradientMap.png"];

// Set the color and scale properties
gradientSpriteNode.color = color;
gradientSpriteNode.colorBlendFactor = 1.0;
[gradientSpriteNode setScale:0.1];

// Add it to our parent node
[self addChild:gradientSpriteNode];

// Add it to our container
[self.gradientNodes addObject:gradientSpriteNode];

// Create and configure a scale up action
SKAction* scaleAction = [SKAction scaleTo:3.0 duration:3.0];
scaleAction.timingMode = SKActionTimingEaseInEaseOut;

// Run that action and attach a completion block
[gradientSpriteNode runAction:scaleAction completion:^{

    // Create a random color
    float r = [SSHelper randomWithLowerFloat:0. andUpperFloat:1.];
    float g = [SSHelper randomWithLowerFloat:0. andUpperFloat:1.];
    float b = [SSHelper randomWithLowerFloat:0. andUpperFloat:1.];
    UIColor* randomColor = [UIColor colorWithRed:r green:g blue:b alpha:1.0];

    // Create a new gradient ontop of the last one
    [self addGradientWithColor:randomColor];

    // If more than 2 gradient sprites inside our container the last one can be removed
    if (self.gradientNodes.count > 2) {
        SKSpriteNode* gradientNode_ = [self.gradientNodes firstObject];
        [gradientNode_ removeFromParent];
        [self.gradientNodes removeObject:gradientNode_];
    }
}];

The idea is to add the sprite to the screen but scaled down to zero. An animation scales it up until it extends the whole screen. Then you start again but having the sprite blended with a different color.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.