5

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!

BadgerBadger
  • 696
  • 3
  • 12
  • 1
    I think there's probably 3 people in the world using shaders in SpriteKit. Despite the fact that Apple seems to have done a lot of work structuring for and then creating shader support and facilities in SpriteKit, they've done a horrid job of releasing, marketing, supporting and promoting SpriteKit, and an even worse job of informing everyone about shaders in it. – Confused Dec 08 '16 at 18:40
  • @Confused, I do agree with you and it must be why almost nobody uses shaders besides for simple tasks such as turning something gray scale.. hope someone can help! – BadgerBadger Dec 08 '16 at 20:22
  • Yeah, unfortunate that SpriteKit has been left out in the sun to bake, or parked in shade. There's some woeful attempts at puns! – Confused Dec 08 '16 at 20:48
  • on another note... have a look at how the Apple Xcode SK Template uses .copy() - it is VERY efficient. If you do this with SKSpriteNodes that have your star textures, you can make hundreds of them without any impact on performance. Make a few "master" nodes to be layer holders for your parallax and call it a day. Head to the pub. Or, use a particle emitter per layer, and set their speeds differently, and position them at the top of the screen and ensure the life of particles is just long enough for each to reach past the bottom of the screen. Also very performant. More than anything, I think. – Confused Dec 08 '16 at 21:26
  • I thought about using particle emitters, but the problem is that I want the stars to stop moving when the ship stops. Since you can't do anyhing with the particles once they are emitted. So having a particle emitter at the top of the screen is perfect when you are always going up, but if you go in any other direction, you are screwed. I guess Ill make my own "SpriteEmitter" node that will manage a bunch of copied SpriteNodes with different star textures and move them around manually. When they get outside of the screen bounds, I'll just kill them and create a new one. – BadgerBadger Dec 08 '16 at 21:56
  • I think it's best to re-use nodes when they go off screen vs making new ones. so for example a game that has endless bullets flying at you, but you only see one at a time, you can just re-position the first bullet after it disappears and it is recycled. saves resources IMO. – Fluidity Jan 05 '17 at 12:21
  • You are totally right! What I'm doing is simply keep track of the last position of every star and if the new position moves them away from the starfield CGRect when they are outside of it, I move them to the opposite side of the screen. So I generate stars only once. – BadgerBadger Jan 05 '17 at 16:10
  • @Fluidity Let me know when you have time to play with it, I'd like some feedback! It's only something I've thrown together quickly (and as you can see on github, I added a bunch of edits because I realized afterwards that some stuff was depending on hardcoded stuff in the original project. So if you tried it before I did the edit, you should try again and download the other repos too (the SKTUtils is a big time saver, a must have actually). I'll also post a tutorial on using it on my wordpress page soon, along with a demo project. – BadgerBadger Jan 07 '17 at 21:37

3 Answers3

1

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.

Confused
  • 6,048
  • 6
  • 34
  • 75
  • I was aware of the advancesimulation and of the target node for the particles. Ill experiment a bit and keep you posted. – BadgerBadger Dec 10 '16 at 14:42
  • What happens with the particles once they are off screen? If they are removed and recreated i's use that because I don't want twinkling lights, I want stars! So I can't rely on the particle's life span because when the ship stops the particles will die and fade and that's not what I want. – BadgerBadger Dec 10 '16 at 14:45
  • I might be leading you astray, here. I was thinking SKNode's `isPaused` would freeze a particle system. But it may only pause actions, not a particle system. If it's possible to freeze a particle system, or give its particles an infinite lifespan, then what I'm suggesting becomes akin to using particle systems as "textures" of your star fields. In this way, if they're too far from the player's view you could delete them, or just leave them there in case the player turns around and heads back to where they were, offscreen. – Confused Dec 10 '16 at 17:22
  • It's possible to stop a particle system without pausing the container node, it's also possible to give them an infinite lifespan (which should be used with a particle number limit for an obvious reason). The thing is that you can't access the particles themselves, so you can't check their positions or kill them manually. I might be wrong there, but it feels like it. – BadgerBadger Dec 11 '16 at 18:44
  • That's right, you cannot find individual particles to kill them off. Or any other form of interaction with them. If you need individual control, SpriteKit particles aren't it. – Confused Dec 11 '16 at 19:32
  • That's why i wanted to use a Shader to do it. I found many options on ShaderToy, but I can't seem to find a way to make them work properly. Having a single SpriteNode following the camera would be much simpler and then just passing it a vector attribute to modify the animation would make it very slick. I managed to modify a few shaders to change their animation depending on a vector and it looks great... on shadertoy. – BadgerBadger Dec 11 '16 at 21:22
  • Sorry, I don't understand. I'm confused ;) You want animations on the stars, and the ability to control them independently and based on events, too? – Confused Dec 11 '16 at 21:26
  • There is a link in my original post, have a look. I just want to apply that shader to a sprite that follows the camera and control the velocity (speed and direction) of the animation with a SKAttribute. I know how to apply a shader and pass values to it. My problem is not "HOW TO MAKE THE STARS HAPPEN" just how to make the shader happen. It compiles properly and nothing shows up. Ah and I know you are confused, your name says so ;) – BadgerBadger Dec 11 '16 at 22:00
  • I think you're going to have to post your shader code attempts and hope that one of the people that actually know what a shader is can help you. I read "shader" and see "very loose term for self contained program to impact either a) pixels, b) vertices, c) geometry" and know little or nothing after that. It seems like a good concept that's never been fully realised, explained or demonstrated on anything but DirectX hardware... wherein the demoscene did all the work for them -> not Microsoft being kind, caring and perceptive. – Confused Dec 11 '16 at 22:59
  • Yeah... I guess you are right. So far I'm able to use shaders to do basic stuff at the pixel level and with my developement plan, they seem like a very efficient way to do it since there is no real 'pixel support' in spritekit. Used to code with DelphiX, back in 2000 ;). That being said I'm also working on an alternative using CoreImage. They say it's suited for realtime operation, so why not give it a try! It's already part of SpriteKit through the SKEffectsNode class. – BadgerBadger Dec 12 '16 at 02:28
  • If only Apple could spare some of their $200+ Billion USD cash pile to hire articulate, compassionate and considerate writers to create explanations, demonstrations and documentation instead of just bone dry, incomplete, obsolete and desolate 'references'. – Confused Dec 12 '16 at 02:47
  • 1
    I know how you feel. Especially now that Swift 3 is out. Most of the documentation is still only for objective-c – BadgerBadger Dec 12 '16 at 07:00
1

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

BadgerBadger
  • 696
  • 3
  • 12
  • @Fluidity Will do ASAP! – BadgerBadger Jan 05 '17 at 16:13
  • 1
    @Fluidity: I just updated my answer with a link to a GitHub gist. Enjoy! it's very simple to use and pretty much fully automated. just need to create and assign a few things, but there is actually not much to it :) – BadgerBadger Jan 05 '17 at 20:29
  • For those looking, this repo contains an implementation using SpriteKit nodes, NOT with a fragment shader as asked. – levigroker Jan 06 '20 at 21:55
0

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 )

levigroker
  • 2,087
  • 1
  • 21
  • 30