0

For the life of me, I cannot understand why this shader animation is not running. Any help is appreciated. Please see the code below... the shader displays correctly, but does not animate. Thank you.

NOTE: Xcode 9.4.1, Swift 4.1, iOS 11.4

import SpriteKit

class GameScene: SKScene {
    static let marquee: SKShader = {
        let shader = SKShader(
            source: "void main() {" +
                "float np = mod(a_path_phase + v_path_distance / u_path_length, 1.0);" +
                "vec3 hsb = vec3(np, 1.0, 1.0);" +
                "vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);" +
                "vec3 p = abs(fract(hsb.xxx + K.xyz) * 6.0 - K.www);" +
                "vec3 rgb = hsb.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), hsb.y);" +
                "vec4 df = SKDefaultShading();" +
                "gl_FragColor = vec4(rgb, df.z);" +
            "}"
        )
        shader.attributes = [SKAttribute(name: "a_path_phase", type: .float)]
        return shader
    } ()

    private let beat: TimeInterval = 0.05
    private var phase: Float = 0

    override func didMove(to view: SKView) {
        let node = SKShapeNode(rectOf: CGSize(width: 256, height: 256), cornerRadius: 16)
        node.fillColor = .clear
        node.strokeColor = .white
        node.strokeShader = GameScene.marquee
        node.lineWidth = 8
        node.glowWidth = 4
        node.run(
            SKAction.repeatForever(
                SKAction.sequence(
                    [
                        SKAction.wait(forDuration: beat),
                        SKAction.run() { [weak self] in
                            if let weak = self {
                                weak.phase = fmodf(weak.phase + Float(weak.beat), 1)
                                node.setValue(SKAttributeValue(float: weak.phase), forAttribute: "a_path_phase")
                            }
                        }
                    ]
                )
            )
        )
        addChild(node)
    }
}
jmdecombe
  • 762
  • 6
  • 15
  • Hi! Did you ever happen to figure this one out? So far, for me, the attribute value does not seem to arrive at the shader, the shader seems to always get value = 0.0; It's as if the node.setValue(,forAttribute:) command is being ignored. Could it be an iOS bug, or am I missing something? I hope it is not a bug. I hope I am missing something. – SirEnder Sep 01 '18 at 20:02

1 Answers1

1

I was also having trouble with .setValue(forAttribute:). It did not seem to work. Upon investigation, my conclusion is that...

I believe SKShapeNode.setValue(forAttribute:) is not working.

The fact that SKSpriteNode.setValue(forAttribute:) did work correctly under the same set of conditions led me to conclude that it was almost surely some bug in iOS in SKShapeNode.

The fact was that any SKAttributes I might set in SKShapeNode.fillShader or SKShapeNode.strokeShader kept their values set to 0.0 no matter what I did.

However, the same did not happen with SKSpriteNode.shader.

So I had to find a way to circumvent this bug. Some sort of hack. Fortunately, in my case, I did find a way.

And fortunately, it seems to be enough to solve your problem too.


What you can do, in this case, is to pass the value of your phase as one of the components of the strokeColor, for example, the red component.

To do this you will have to normalize the phase to values between 0.0 to 1.0. You can pass a phase multiplier using a shader.uniform to help denormalize it.

Then read the phase value inside the shader from SKDefaultShading().x, (the red component).

Make sure to set the alpha component of the strokeColor to 1.0, for SKDefaultShading() will have its color component values already premultiplied by its alpha.

Now, because your strokeColor is already being used to pass the phase, you can pass the actual color of the stroked line as a vec4 shader.uniform.


  • In my case, I also made use of u_time to help in performing my animation. Stuff like fract(u_time) can be very useful.

Here is how the corrected code might look like:

> NOTE: There is a much simpler solution. Look by the end of this post.

import SpriteKit

class GameScene: SKScene {
    private typealias Me = GameScene
    static let phaseMultiplier: Float = 1000

    static let marquee: SKShader = {
        let shader = SKShader(
            source: "void main() {" +
                "float red = SKDefaultShading().x;" +
                "float phase = red * u_phase_multiplier;" +
                "float np = mod(phase + v_path_distance / u_path_length, 1.0);" +
                "vec3 hsb = vec3(np, 1.0, 1.0);" +
                "vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);" +
                "vec3 p = abs(fract(hsb.xxx + K.xyz) * 6.0 - K.www);" +
                "vec3 rgb = hsb.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), hsb.y);" +
                "vec4 df = u_stroke_color;" +
                "gl_FragColor = vec4(rgb, df.z);" +
            "}"
        )
        let white = vector_float4.init(1, 1, 1, 1)
        shader.uniforms = [
            SKUniform(name: "u_phase_multiplier", float: Me.phaseMultiplier), 
            SKUniform(name: "u_stroke_color", vectorFloat4: white)]
        return shader
    } ()

    private let beat: TimeInterval = 0.05
    private var phase: Float = 0

    override func didMove(to view: SKView) {
        let node = SKShapeNode(rectOf: CGSize(width: 256, height: 256), cornerRadius: 16)
        node.fillColor = .clear
        node.strokeColor = .white
        node.strokeShader = Me.marquee
        node.lineWidth = 8
        node.glowWidth = 4
        node.run(
            SKAction.repeatForever(
                SKAction.sequence(
                    [
                        SKAction.wait(forDuration: beat),
                        SKAction.run() { [weak self] in
                            if let weak = self {
                                weak.phase = fmodf(weak.phase + Float(weak.beat), 1)
                                let phase = weak.phase / Me.phaseMultiplier
                                node.strokeColor = SKColor.init(red: phase, green: 0, blue: 0, alpha: 1)
                            }
                        }
                    ]
                )
            )
        )
        addChild(node)
    }
}

NOTE: I did not compile the code above posted. I should serve merely as a sketch.


The day after this post, I found out that attribute parameters are not supposed to be used in fragment shaders, only in vertex shaders.

Take a look at StackOverflow question: In WebGL what are the differences between an attribute, a uniform, and a varying variable?

The SDK documentation of SKAttribute and SKAttributeValue, however, seem to suggest that there may be no direct relation between "SKAttributes" and actual shader attribute parameters. They just seem to be named the same.


Simpler Solution:

In the end, however, there is never any need to make use of any hack like the one I show here.

All that needs to be done is to set the value of a uniform through SKShader.uniformNamed("u_phase")?.floatValue = phase, instead of using .setValue(forAttribute:) just as jmdecombe, correctly, later pointed out.

I will keep my answer here for reference, as it presents an unconventional solution to a problem.

SirEnder
  • 464
  • 4
  • 13
  • Hi, it's been a while but I give that code a look and indeed the solution was to use uniforms instead of attributes, then it worked. – jmdecombe Sep 01 '18 at 22:25
  • Namely, the line shader.attributes = etc. had to become: shader.uniforms = [SKUniform(name: "a_path_phase", float: 0.0)], then the line node.setValue etc. had to become: node.strokeShader?.uniformNamed("a_path_phase")?.floatValue = weak.phase – jmdecombe Sep 01 '18 at 22:27
  • What you are telling me here is that you decided to write to a Uniform rather than to an Attribute, which I did not realize could be possible. Both of our "alternative" ways to accomplish our needs do seem to confirm one thing though, which is that there is a bug in SKShapeNode.setValue(,forAttribute:). – SirEnder Sep 02 '18 at 05:39