Does SpriteKit support a dense tessellation of a sprite / texture / shape so that it can be freely deformed?

advertisements

For example if you have an image of a trampoline, and a character jumping on it. Then you want to animate how the trampoline bends down in the center.

To do this I would have to take a bitmap and apply it to a densely tessellated OpenGL ES mesh. Then apply texture to it. Then deform the mesh.

Does SpriteKit support this or can it only display textures as-is?


With iOS 10, SKWarpGeometry has been added that allows you to deform sprites. Warps are defined using a source and destination grid containing eight control points, these points define the warp transform; Sprite Kit will take care of tessellation and other low level details.

You can set the wrap geometry for a sprite directly, or animate warping with SKActions.

SKAction.animate(withWarps: [SKWarpGeometry], times: [NSNumber]) ->     SKAction?
SKAction.animate(withWarps: [SKWarpGeometry], times: [NSNumber], restore: Bool) -> SKAction?
SKAction.warp(to: SKWarpGeometry, duration: TimeInterval) -> SKAction?


UPDATE Nov 2016

On Xcode 8+, the following code snippet can be used in a playground: Drag the control points and see the resulting SKSpriteNode get deformed accordingly.

// SpriteKit warp geometry example
// Copy the following in an Xcode 8+ playground
// and make sure your Assistant View is open
// 

import UIKit
import PlaygroundSupport
import SpriteKit

let view = SKView(frame: CGRect(x: 0, y: 0, width: 480, height: 320))

let scene = SKScene(size: view.frame.size)

view.showsFPS = true
view.presentScene(scene)

PlaygroundSupport.PlaygroundPage.current.liveView = view

func drawCanvas1(frame: CGRect = CGRect(x: 0, y: 0, width: 200, height: 200)) -> UIImage {

    UIGraphicsBeginImageContext(frame.size)

    //// Star Drawing
    let starPath = UIBezierPath()
    starPath.move(to: CGPoint(x: frame.minX + 100.5, y: frame.minY + 24))
    starPath.addLine(to: CGPoint(x: frame.minX + 130.48, y: frame.minY + 67.74))
    starPath.addLine(to: CGPoint(x: frame.minX + 181.34, y: frame.minY + 82.73))
    starPath.addLine(to: CGPoint(x: frame.minX + 149, y: frame.minY + 124.76))
    starPath.addLine(to: CGPoint(x: frame.minX + 150.46, y: frame.minY + 177.77))
    starPath.addLine(to: CGPoint(x: frame.minX + 100.5, y: frame.minY + 160))
    starPath.addLine(to: CGPoint(x: frame.minX + 50.54, y: frame.minY + 177.77))
    starPath.addLine(to: CGPoint(x: frame.minX + 52, y: frame.minY + 124.76))
    starPath.addLine(to: CGPoint(x: frame.minX + 19.66, y: frame.minY + 82.73))
    starPath.addLine(to: CGPoint(x: frame.minX + 70.52, y: frame.minY + 67.74))
    starPath.close()
    UIColor.red.setFill()
    starPath.fill()
    UIColor.green.setStroke()
    starPath.lineWidth = 10
    starPath.lineCapStyle = .round
    starPath.lineJoinStyle = .round
    starPath.stroke()

    let image = UIGraphicsGetImageFromCurrentImageContext()!
    UIGraphicsEndImageContext()
    return image
}

class ControlNode : SKShapeNode {

    override init() {
        super.init()
        self.path = CGPath(ellipseIn: CGRect.init(x: 0, y: 0, width: 10, height: 10), transform: nil)
        self.fillColor = UIColor.red
        self.isUserInteractionEnabled = true
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    typealias UpdateHandler = (ControlNode) -> ()
    var positionUpdated: UpdateHandler? = nil

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        self.fillColor = UIColor.yellow
    }

    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        if let touch = touches.first, let parent = self.parent {
            let pos = touch.location(in: parent).applying(CGAffineTransform(translationX: -5, y: -5))
            self.position = pos
            positionUpdated?(self)
        }
    }

    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        self.fillColor = UIColor.red
    }

    var warpPosition: vector_float2 {
        if let parent = self.parent {
            let size = parent.frame.size
            let pos = self.position.applying(CGAffineTransform(translationX: -size.width/2 + 5, y: -size.height/2 + 5))
            return vector_float2(
                x: Float(1 + pos.x / size.width),
                y: Float(1 + pos.y / size.height)
            )
        }
        return vector_float2(x: 0, y: 0)
    }
}

extension SKSpriteNode {
    func addControlNode(x: CGFloat, y: CGFloat) -> ControlNode {
        let node = ControlNode()
        node.position = CGPoint(
            x: x * frame.width - frame.width / 2 - 5,
            y: y * frame.height - frame.height / 2 - 5
        )
        self.addChild(node)
        return node
    }
}

let texture = SKTexture(image: drawCanvas1())
let image = SKSpriteNode(texture: texture)
image.position = CGPoint(x: 200, y: 160)
scene.addChild(image)

let controlPoints: [(x:CGFloat, y:CGFloat)] = [
    (0, 0),
    (0.5, 0),
    (1.0, 0),
    (0, 0.5),
    (0.5, 0.5),
    (1.0, 0.5),
    (0, 1.0),
    (0.5, 1.0),
    (1.0, 1.0),
]

let sourcePositions = controlPoints.map {
    vector_float2.init(Float($0.x), Float($0.y))
}

var controlNodes: [ControlNode] = []

controlPoints.forEach { controlNodes.append(image.addControlNode(x: $0.x, y: $0.y)) }

for node in controlNodes {
    node.positionUpdated = {
        [weak image]
        _ in
        let destinationPoints = controlNodes.map {
            $0.warpPosition
        }
        image?.warpGeometry = SKWarpGeometryGrid(columns: 2, rows: 2, sourcePositions: sourcePositions, destinationPositions: destinationPoints)
    }
}

PS: I referred to the SKWarpGeometry documentation.