Applying a custom SKShader to SKScene that pixelat

2019-07-02 05:11发布

I'm trying to create a full-screen pixelation effect on SKScene. I've learned that there should be two options to do this:

  • Using a custom SKShader using GLES 2.0.
  • Using Core Image filters.

I've tried to add a custom SKShader that should modify the whole screen by pixelating it. I'm not sure that if it's possible, but documentation from SKScene (which is a subclass of SKEffectNode) suggests it:

An SKEffectNode object renders its children into a buffer and optionally applies a Core Image filter to this rendered output.

It's possible to assign a SKShader to the SKScene, as in GameScene : SKScene:

override func didMoveToView(view: SKView) {
    let shader = SKShader(fileNamed: "pixelation.fsh")
    self.shader = shader
    self.shouldEnableEffects = true
}

... but it seems that the rendered buffer is not passed as the u_texture to the GLES:

void main()
{
    vec2 coord = v_tex_coord;
    coord.x = floor(coord.x * 10.0) / 10.0;
    coord.y = floor(coord.y * 10.0) / 10.0;
    vec4 texture = texture2D(u_texture, coord);
    gl_FragColor = texture;
}

... so the previous shader doesn't work.

If I assign that shader to a texture-based SKSpriteNode, it works.

So is it possible to modify the whole frame buffer (and for example pixelate it) as a post-processing measure after all the nodes have been rendered?

Edit: I found a way to do the pixelation using Core Image filters in OS X (How do you add a CIPixellate Core Image Filter to a Sprite Kit scene?), but copying that implementation doesn't yield any results on iOS. According to the documents CIPixellate should be Available in OS X v10.4 and later and in iOS 6.0 and later..

3条回答
爱情/是我丢掉的垃圾
2楼-- · 2019-07-02 05:56

In order to get your .shader running on SKScene, you need to set shouldEnableEffects to true on the scene (same thing goes for SKEffectNode).

While technically, that "works" (the shader is applied), there's a bug in the rendering of the scene afterwards that gets slightly resized.

So using CoreImage filters is, so far, the best way to go.

查看更多
The star\"
3楼-- · 2019-07-02 05:56

I actually had to do the exact same thing for a recent project as a way to transition between levels, and ended up doing a work around for it. Basically, I took a screenshot of the screen in the code then when I loaded the next level, I called a previously saved screenshot of how the level should look when it was loaded. I added the previous level screenshot as an SKSpriteNode and then ran the shader a number of times until it was incredibly pixelated. Then I did the same to the screenshot for that level and replaced the two and then I un-pixelated the second screenshot so it looked like as soon as the level was beaten everything pixelated itself then un-pixelated itself to reveal a new level.

          UIGraphicsBeginImageContextWithOptions(UIScreen.mainScreen().bounds.size, false, 0);
            self.view!.drawViewHierarchyInRect(view!.bounds, afterScreenUpdates: true)
            let image:UIImage = UIGraphicsGetImageFromCurrentImageContext();
            UIGraphicsEndImageContext();
            //UIGraphicsEndImageContext()

            protoImage = SKSpriteNode(texture: SKTexture(CGImage: image.CGImage!))
            protoImage.size = CGSizeMake(self.frame.size.width, self.frame.size.height)
            node.position = CGPointMake(self.frame.size.width/2, self.frame.size.height/2)
            protoImage.zPosition = 9000

Second Scene

           let shader: SKShader = SKShader(fileNamed: "RWTGradient2.fsh")
           let ratioX: Float = divisor/Float(protoImage.frame.size.width)
           let ratioY: Float = divisor/Float(protoImage.frame.size.height)
           shader.uniforms = [

                SKUniform(name: "ratioX", float: ratioX),
                SKUniform(name: "ratioY", float: ratioY),

           ]
           protoImage.shader = shader;
查看更多
虎瘦雄心在
4楼-- · 2019-07-02 06:00

I managed to make it work using Core Image filter CIPixellate. I used is as a filter to SKEffectNode to produce the pixelation effect. Couple of things to note:

  • SKScene is a subclass of SKEffectNode, but applying the filter to SKScene doesn't work. It'll mess up the background and doesn't do any pixellation.
  • You need to create a SKEffectNode and add the nodes to be pixelated under that.

Here's the solution based on the code generated when you choose a Game type project with Swift:

import SpriteKit

class GameScene: SKScene {
    var effectNode : SKEffectNode = SKEffectNode.node()

    override func didMoveToView(view: SKView) {
        let filter = CIFilter(name: "CIPixellate")
        filter.setDefaults()
        filter.setValue(5.0, forKey: "inputScale")

        self.effectNode.filter = filter
        self.effectNode.shouldEnableEffects = true
        self.addChild(effectNode)
    }

    override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
        for touch: AnyObject in touches {
            let location = touch.locationInNode(self)

            let sprite = SKSpriteNode(imageNamed:"Spaceship")

            sprite.xScale = 0.5
            sprite.yScale = 0.5
            sprite.position = location

            let action = SKAction.rotateByAngle(CGFloat(M_PI), duration:1)

            sprite.runAction(SKAction.repeatActionForever(action))

            self.effectNode.addChild(sprite)
        }
    }

    override func update(currentTime: CFTimeInterval) {
        /* Called before each frame is rendered */
    }
}
查看更多
登录 后发表回答