Search code examples
iosxcodesprite-kitshader

Understanding coordinates of shader in SKShader?


I have been trying to understand how the coordinates of shaders work with SpriteKit. Below is my test program. I created a sprite and then loaded and run a shader (as a string) over that sprite. The shader should create a blue rectangle on a white background. I need the rectangle close to the sprite size (equal to 0.9 or 90% of its size) and to stay in the center of the sprite regardless of the position of the sprite on the screen. However, I observed:

  • The blue rectangle is significantly smaller than the sprite
  • The blue rectangle seems to be stuck to the screen, not come with the sprite. It means it may "run" out of the sprite when I move the sprite to other positions. I guess that is caused by using gl_FragCoord, but it didn't work eighter when I replaced it by v_tex_coor

Could someone explain to me the coordination of shaders when working with SpriteKit and how to fix those issues? Thanks.

import SpriteKit

class ViewController: UIViewController {
    // Shader as a string
    let shaderSource =
"""
// the center point of the rectangle, not really center vec2(0.0) but just for testing
constant vec2 center = vec2(0.5, 0.5); 
constant vec2 rectSz = vec2(0.9, 0.9); // size of the rectangle

void main()
{
    /// uv from -1 to 1 in Y height
    vec2 uv = (gl_FragCoord.xy - u_sprite_size.xy * 0.5) / u_sprite_size.y;

    vec2 sz = rectSz / 2.0; // half size to the center point

    vec2 d = abs(uv - center);
    bool inside = d.x <= sz.x && d.y <= sz.y;

    // draw blue rectangle, white background
    gl_FragColor = inside ? vec4(0.0, 0.0, 1.0, 1.) : vec4(1.0);
}
"""
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        
        let sceneView = SKView(frame: view.bounds)
        print("view.bounds = \(view.bounds)")
        self.view.addSubview(sceneView)

        let scene = SKScene(size: view.bounds.size)
        sceneView.presentScene(scene)
        
        // sprite in yellow if shader is not working
        let sprite = SKSpriteNode(color: UIColor.yellow, size: CGSize(width: 300, height: 300))
        sprite.position = CGPoint(x: 150, y: view.bounds.height - 150)
        scene.addChild(sprite)
        
        let spriteSize = vector_float2(Float(sprite.frame.size.width),
                                       Float(sprite.frame.size.height))

        let shader = SKShader(source: shaderSource,
                              uniforms: [
                                SKUniform(name: "u_sprite_size", vectorFloat2: spriteSize)
                              ])
        sprite.shader = shader
    }
}

Screenshot with iPhone 14 Pro Max simulator


Solution

  • passing the sprite size as a uniform can be useful in some cases. but here you're actually overthinking it. if you want shader coords in a 0-1 number space flush to the size of the sprite, it's sufficient to grab your coords as vec2 st = v_tex_coord.xy; and simply use them without scaling them. here is an example drawing a circle in two sprites of different sizes.

    let shaderSource = """
    constant vec2 center = vec2(0.5, 0.5);
    
    void main() {
        vec2 st = v_tex_coord.xy;
        float d = distance(st,vec2(0.5)); //distance to center
        vec3 color = (d > center.x/2) ? vec3(1.0) : vec3(0.0); //interpret distance as color value
        gl_FragColor = vec4( color, 1.0 );
    }
    """
    
    class GameScene: SKScene {
        
        override func didMove(to view: SKView) {
            let shader = SKShader(source: shaderSource)
                                     
            let sprite1 = SKSpriteNode(color: UIColor.yellow, size: CGSize(width: 300, height: 300))
            sprite1.position.y = 150
            sprite1.shader = shader
            addChild(sprite1)
    
            let sprite2 = SKSpriteNode(color: UIColor.red, size: CGSize(width: 100, height: 100))
            sprite2.position.y = -150
            sprite2.shader = shader
            addChild(sprite2)
        }
    
    }
    

    enter image description here