iosswiftsprite-kitskeffectnode

Warp effect for SKSpriteNodes passing through it?


I'm trying to apply a warp/distortion effect to all SKSpriteNodes that pass through a fixed rectangle sized area on the screen. Shown in the image below, the same SKSpriteNode will start at the top of the screen and work its way down allowing the rectangle distortion filter to warp the node as it passes through.

I've tried using a SKEffectNode shown in the code below. However, I couldn't set a fixed height and width value to the SKEffectNode which later gave me inconsistent warp effects due the SKEffectNode constantly changing its height to accommodate all the SKSpritNode children.

I'm wondering if there is another way to achieve this effect or if I'm missing something with the SKEffectNode. Ideally I'd like the the warp to effect any SKNode that passes under it without the need to add and remove children.

Any information would be much appreciated.

enter image description here

Warp effect I'm trying to achieve above and current SKEffectNode code below.

 func warpToUpEffectNode(effectNode:SKEffectNode, view:SKView){
    effectNode.zPosition = priorityPos.upEffectNodeZ
    let destinationPositions: [vector_float2] = [
        vector_float2(-0.1, 1), vector_float2(0.5, 1.3), vector_float2(1.1, 1),
        vector_float2(0.1, 0.5), vector_float2(0.5, 0.5), vector_float2(0.9, 0.5),
        vector_float2(-0.1, 0), vector_float2(0.5, -0.3), vector_float2(1.1, 0)
    ]
    let warpGeometryGrid = SKWarpGeometryGrid(columns: 10,rows: 1)
    effectNode.warpGeometry = warpGeometryGrid.replacingByDestinationPositions(positions: destinationPositions)
}

Solution

  • you can accomplish this using render-to-texture.

    enter image description here

    first, put all of your scene elements into a single large node, i'm calling it container

    then set up your viewport area, the part you want to warp. it's a bit tricky because you also have to crop it (otherwise you'll see the fringe of the warped shape)

    /*
     create a crop node with
     - mask
     - a visible frame
     - a warpable spritenode
     */
    viewport_warp = SKSpriteNode(color: .white, size: CGSize(width: 150, height: 150))
    let viewport_frame = SKShapeNode(rectOf: viewport_warp.size, cornerRadius: 15)
    viewport_frame.strokeColor = .black
    viewport_frame.zPosition = 3
    viewport_warp.addChild(viewport_frame)
    
    let viewport_mask = SKShapeNode(rectOf: viewport_warp.size, cornerRadius: 15)
    viewport_mask.fillColor = .black
    
    let cropNode = SKCropNode()
    cropNode.zPosition = 2
    cropNode.maskNode = viewport_mask
    cropNode.addChild(viewport_warp)
    addChild(cropNode)
    

    then set up your warp geometry

    //warp the geometry of the spritenode
    let PINCH_OFFSET:Float = 0.1
    let dst = [
        // bottom row: left, center, right
        vector_float2(0.0, 0.0),
        vector_float2(0.5, 0.0 - PINCH_OFFSET),
        vector_float2(1.0, 0.0),
    
        // middle row: left, center, right
        vector_float2(0.0 - PINCH_OFFSET, 0.5),
        vector_float2(0.5, 0.5),
        vector_float2(1.0 + PINCH_OFFSET, 0.5),
    
        // top row: left, center, right
        vector_float2(0.0, 1.0),
        vector_float2(0.5, 1.0 + PINCH_OFFSET),
        vector_float2(1.0, 1.0)
    ]
    let warpGeometryGrid = SKWarpGeometryGrid(columns: 2,rows: 2)
    viewport_warp.warpGeometry = warpGeometryGrid.replacingByDestinationPositions(positions: dst)
    

    and finally, do render-to-texture on the container and update the texture of your spritenode

    override func update(_ currentTime: TimeInterval) {
        let cropped_viewport = viewport_warp.frame.insetBy(dx: 10, dy: 10) //optional: adds magnification effect
        let texture:SKTexture? = self.view?.texture(from:container, crop:cropped_viewport)
        viewport_warp.texture = texture
    }