SKShader to create parallax background

753 views Asked by At

A parallax background with a fixed camera is easy to do, but since i'm making a topdown view 2D space exploration game, I figured that having a single SKSpriteNode filling the screen and being a child of my SKCameraNode and using a SKShader to draw a parallax starfield would be easier.

I went on shadertoy and found this simple looking shader. I adapted it successfully on shadertoy to accept a vec2() for the velocity of the movement that I want to pass as an SKAttribute so it can follow the movement of my ship.

Here is the original source: https://www.shadertoy.com/view/XtjSDh

I managed to make the conversion of the original code so it compiles without any error, but nothing shows up on the screen. I tried the individual functions and they do work to generate a fixed image.

Any pointers to make it work?

Thanks!

3

There are 3 answers

3
BadgerBadger On BEST ANSWER

EDIT : Code is clean and working now. I've setup a GitHub repo for this.

I guess I didnt explain what I wanted properly. I needed a starfield background that follows the camera like you could find in Subspace (back in the days)

The result is pretty cool and convincing! I'll probably come back to this later when the node quantity becomes a bottleneck. I'm still convinced that the proper way to do that is with shaders!

Here is a link to my code on GitHub. I hope it can be useful to someone. It's still a work in progress but it works well. Included in the repo is the source from SKTUtils (a library by Ray Wenderlich that is already freely available on github) and from my own extension to Ray's tools that I called nuts-n-bolts. these are just extensions for common types that everyone should find useful. You, of course, have the source for the StarfieldNode and the InteractiveCameraNode along with a small demo project.


Starfield in action in my project

12
Confused On

This isn't really an answer, but it's a lot more info than a comment, and highlights some of the oddness and appropriateness of how SK does particles:

There's a couple of weird things about particles in SceneKit, that might apply to SpriteKit.

  1. when you move the particle system, you can have the particles move with them. This is the default behaviour:

From the docs:

When the emitter creates particles, they are rendered as children of the emitter node. This means that they inherit the characteristics of the emitter node, just like nodes do. For example, if you rotate the emitter node, the positions of all of the spawned particles are rotated also. Depending on what effect you are simulating with the emitter, this may not be the correct behavior.

For most applications, this is the wrong behaviour, in fact. But for what you're wanting to do, this is ideal. You can position new SKNodeEmitters offscreen where the ship is heading, and fix them to "space" so they rotate in conjunction with the directional changes of the player's ship, and the particles will do exactly as you want/need to create the feeling of moving throughout space.

  1. SpriteKit has a prebuild, or populate ability in the form of advancing the simulation: https://developer.apple.com/reference/spritekit/skemitternode/1398027-advancesimulationtime

This means you can have stars ready to show wherever the ship is heading to, through space, as the SKEmittors come on screen. There's no need for a loading delay to build stars, this does it immediately.


As near as I can figure, you'd need a 3 particle emitters to pull this off, each the size of the screen of the device. Burst the particles out, then release each layer you want for parallax to a target node at the right "depth" from the camera, and carry on by moving these targets as per the screen movement.

Bit messy, but probably quicker, easier, and much more powerfully full of potential for playful effects than creating your own system.

Maybe... I could be wrong.

0
levigroker On

The short answer is, in SpriteKit you use the fragment coordinates directly without needing to scale against the viewport resolution (iResoultion in shadertoy land), so the line:

vec2 samplePosition = (fragCoord.xy / maxResolution) + vec2(0.0, iTime * 0.01);

can be changed to omit the scaling:

vec2 samplePosition = fragCoord.xy + vec2(0.0, iTime * 0.01);

this is likely the root issue (hard to know for sure without your rendition of the shader code) of why you're only seeing black from the shader.

For a full answer for an implementation of a SpriteKit shader making a star field, let's take the original shader and simplify it so there's only one star field, no "fog" (just to keep things simple), and add a variable to control the velocity vector of the movement of the stars:

(this is still in shadertoy code)

float Hash(in vec2 p)
{
    float h = dot(p, vec2(12.9898, 78.233));
    return -1.0 + 2.0 * fract(sin(h) * 43758.5453);
}

vec2 Hash2D(in vec2 p)
{
    float h = dot(p, vec2(12.9898, 78.233));
    float h2 = dot(p, vec2(37.271, 377.632));
    return -1.0 + 2.0 * vec2(fract(sin(h) * 43758.5453), fract(sin(h2) * 43758.5453));
}

float Noise(in vec2 p)
{
    vec2 n = floor(p);
    vec2 f = fract(p);
    vec2 u = f * f * (3.0 - 2.0 * f);

    return mix(mix(Hash(n), Hash(n + vec2(1.0, 0.0)), u.x),
               mix(Hash(n + vec2(0.0, 1.0)), Hash(n + vec2(1.0)), u.x), u.y);
}

vec3 Voronoi(in vec2 p)
{
    vec2 n = floor(p);
    vec2 f = fract(p);

    vec2 mg, mr;

    float md = 8.0;
    for(int j = -1; j <= 1; ++j)
    {
        for(int i = -1; i <= 1; ++i)
        {
            vec2 g = vec2(float(i), float(j));
            vec2 o = Hash2D(n + g);

            vec2 r = g + o - f;
            float d = dot(r, r);

            if(d < md)
            {
                md = d;
                mr = r;
                mg = g;
            }
        }
    }
    return vec3(md, mr);
}

vec3 AddStarField(vec2 samplePosition, float threshold)
{
    vec3 starValue = Voronoi(samplePosition);
    if(starValue.x < threshold)
    {
        float power = 1.0 - (starValue.x / threshold);
        return vec3(power * power * power);
    }
    return vec3(0.0);
}


void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    float maxResolution = max(iResolution.x, iResolution.y);

    vec2 velocity = vec2(0.01, 0.01);
    vec2 samplePosition = (fragCoord.xy / maxResolution) + vec2(iTime * velocity.x, iTime * velocity.y);
    vec3 finalColor = AddStarField(samplePosition * 16.0, 0.00125);

    fragColor = vec4(finalColor, 1.0);
}

If you paste that into a new shadertoy window and run it you should see a monochrome star field moving towards the bottom left.

To adjust it for SpriteKit is fairly simple. We need to remove the "in"s from the function variables, change the name of some constants (there's a decent blog post about the shadertoy to SpriteKit changes which are needed), and use an Attribute for the velocity vector so we can change the direction of the stars for each SKSpriteNode this is applied to, and over time, as needed.

Here's the full SpriteKit shader source, with a_velocity as a needed attribute defining the star field movement:

float Hash(vec2 p)
{
    float h = dot(p, vec2(12.9898, 78.233));
    return -1.0 + 2.0 * fract(sin(h) * 43758.5453);
}

vec2 Hash2D(vec2 p)
{
    float h = dot(p, vec2(12.9898, 78.233));
    float h2 = dot(p, vec2(37.271, 377.632));
    return -1.0 + 2.0 * vec2(fract(sin(h) * 43758.5453), fract(sin(h2) * 43758.5453));
}

float Noise(vec2 p)
{
    vec2 n = floor(p);
    vec2 f = fract(p);
    vec2 u = f * f * (3.0 - 2.0 * f);

    return mix(mix(Hash(n), Hash(n + vec2(1.0, 0.0)), u.x),
               mix(Hash(n + vec2(0.0, 1.0)), Hash(n + vec2(1.0)), u.x), u.y);
}

vec3 Voronoi(vec2 p)
{
    vec2 n = floor(p);
    vec2 f = fract(p);

    vec2 mg, mr;

    float md = 8.0;
    for(int j = -1; j <= 1; ++j)
    {
        for(int i = -1; i <= 1; ++i)
        {
            vec2 g = vec2(float(i), float(j));
            vec2 o = Hash2D(n + g);

            vec2 r = g + o - f;
            float d = dot(r, r);

            if(d < md)
            {
                md = d;
                mr = r;
                mg = g;
            }
        }
    }
    return vec3(md, mr);
}

vec3 AddStarField(vec2 samplePosition, float threshold)
{
    vec3 starValue = Voronoi(samplePosition);
    if (starValue.x < threshold)
    {
        float power = 1.0 - (starValue.x / threshold);
        return vec3(power * power * power);
    }
    return vec3(0.0);
}


void main()
{
    vec2 samplePosition = v_tex_coord.xy + vec2(u_time * a_velocity.x, u_time * a_velocity.y);
    vec3 finalColor = AddStarField(samplePosition * 20.0, 0.00125);

    gl_FragColor = vec4(finalColor, 1.0);
}

(worth noting, that is is simply a modified version of the original )