在SpriteKit中实现路径动画绘制

6
我正在尝试使用SpriteKit来动态绘制路径上的线条。我已经使用SKActions和一个单独的CABasicAnimations实现了一个可行的解决方案。但是SKAction的解决方案并不太优雅;它会在每次SKAction.repeatAction(action:count:)调用的迭代中创建并描绘一条新路径,每个新路径略微比之前更完整。
func start() {
    var i:Double = 1.0
    let spinAction:SKAction = SKAction.repeatAction(SKAction.sequence([
        SKAction.runBlock({
            self.drawCirclePercent(i * 0.01)

            if (++i > 100.0) {
                i = 1.0
            }
        }),
        SKAction.waitForDuration(0.01)
    ]), count: 100)
    runAction(spinAction)
}

func drawCirclePercent(percent:Double) {
    UIGraphicsBeginImageContext(self.size)

    let ctx:CGContextRef = UIGraphicsGetCurrentContext()
    CGContextSaveGState(ctx)
    CGContextSetLineWidth(ctx, lineWidth)
    CGContextSetRGBStrokeColor(ctx, 1.0, 1.0, 1.0, 1.0)
    CGContextAddArc(ctx, CGFloat(self.size.width/2.0), CGFloat(self.size.height/2.0), radius/2.0, CGFloat(M_PI * 1.5), CGFloat(M_PI * (1.5 + 2.0 * percent)), 0)
    CGContextStrokePath(ctx)

    let textureImage:UIImage = UIGraphicsGetImageFromCurrentImageContext()
    texture = SKTexture(image: textureImage)

    UIGraphicsEndImageContext()
}

虽然上面的代码可以工作,但它肯定不够美观或高效,并且可能不是SKActions的预期使用方式。使用CABasicAnimation解决方案更加优雅和高效。

let path:CGMutablePathRef = CGPathCreateMutable()
CGPathAddArc(path, nil, 0, 0, 40.0, CGFloat(M_PI_2 * 3.0), CGFloat(M_PI_2 * 7.0), false)

let pathLayer:CAShapeLayer = CAShapeLayer()
pathLayer.frame = CGRectMake(100, 100, 80.0, 80.0)
pathLayer.path = path
pathLayer.strokeColor = SKColor.whiteColor().CGColor
pathLayer.fillColor = nil
pathLayer.lineWidth = 3.0
self.view.layer.addSublayer(pathLayer)

let pathAnimation:CABasicAnimation = CABasicAnimation(keyPath: "strokeEnd")
pathAnimation.duration = 2.0
pathAnimation.fromValue = 0.0
pathAnimation.toValue = 1.0
pathLayer.addAnimation(pathAnimation, forKey: "strokeEndAnimation")

我的问题是,我希望所有代码都包含在SKSpriteNode的子类中(如果上述两种解决方案是我的唯一选择,我会选择第一种方案)。是否有任何方法可以改进我的SKAction实现,使其更接近CoreAnimation实现,而无需包括CoreAnimation?本质上,我想知道SpriteKit是否具有我不知道的功能,可以用来改进第一种实现方式。

我会在不用手机发布时进一步研究这个问题,但是这里有一个提示可以帮助你开始:如果你为绘制形状节点创建一个自定义 SKShader,那么你可以在你的着色器 GLSL 代码中访问的变量之一是沿路径的弧长。 - rickster
@rickster 哦,有趣。我对GLSL不是很熟悉,但我会去阅读一些资料。 - Scott Mielcarski
有人最终写了用于绘制弧线的自定义SKShader吗? - Ben Morrow
@BenMorrow,我深入研究了它,但最终使用了我在原始问题中发布的方法。我认为SKShader选项会更好,但很抱歉我不能提供更多帮助。 - Scott Mielcarski
@ScottMielcarski 谢谢,我会使用类似的东西。 - Ben Morrow
@BenMorrow 没问题,如果你想到更好的解决方案,请将其发布为答案。我相信其他人也遇到了类似的问题。 - Scott Mielcarski
2个回答

9

您可以通过提供自定义的strokeShader来使SKShapeNode的路径动画化,该shader基于SKShader的几个属性properties v_path_distanceu_path_length输出。这是由上面的rickster提示的,以下是完整的代码。在shader中,我们添加了u_current_percentage,它指的是我们想要描绘的路径中当前点的位置。通过这样做,场景确定了描边动画的速度。还要注意的是,strokeShader是一个片段着色器,在每一步都会输出RGB值,这允许沿路径进行颜色控制,例如实现渐变色。

将该shader作为文件添加到Xcode项目中的"animateStroke.fsh"中:

    void main()  
{  
    if ( u_path_length == 0.0 ) {  
        gl_FragColor = vec4( 0.0, 0.0, 1.0, 1.0 );   
    } else if ( v_path_distance / u_path_length <= u_current_percentage ) {  
        gl_FragColor = vec4( 1.0, 0.0, 0.0, 1.0 );   
    } else {  
        gl_FragColor = vec4( 0.0, 0.0, 0.0, 0.0 );   
    }  
}  

使用它的示例SKScene子类:

    import SpriteKit  
    import GameplayKit  
    func shaderWithFilename( _ filename: String?, fileExtension: String?, uniforms: [SKUniform] ) -> SKShader {  
        let path = Bundle.main.path( forResource: filename, ofType: fileExtension )  
        let source = try! NSString( contentsOfFile: path!, encoding: String.Encoding.utf8.rawValue )  
        let shader = SKShader( source: source as String, uniforms: uniforms )  
        return shader  
}  
class GameScene: SKScene {  
    let strokeSizeFactor = CGFloat( 2.0 )  
    var strokeShader: SKShader!  
    var strokeLengthUniform: SKUniform!  
    var _strokeLengthFloat: Float = 0.0  
    var strokeLengthKey: String!  
    var strokeLengthFloat: Float {  
        get {  
            return _strokeLengthFloat  
        }  
        set( newStrokeLengthFloat ) {  
            _strokeLengthFloat = newStrokeLengthFloat  
            strokeLengthUniform.floatValue = newStrokeLengthFloat  
        }  
    }  

    override func didMove(to view: SKView) {  
        strokeLengthKey = "u_current_percentage"  
        strokeLengthUniform = SKUniform( name: strokeLengthKey, float: 0.0 )  
        let uniforms: [SKUniform] = [strokeLengthUniform]  
        strokeShader = shaderWithFilename( "animateStroke", fileExtension: "fsh", uniforms: uniforms )  
        strokeLengthFloat = 0.0  
        let cameraNode = SKCameraNode()  
        self.camera = cameraNode  
        let strokeHeight = CGFloat( 200 ) * strokeSizeFactor  
        let path1 = CGMutablePath()  
        path1.move( to: CGPoint.zero )  
        path1.addLine( to: CGPoint( x: 0, y: strokeHeight ) )  

        // prior to a fix in iOS 10.2, bug #27989113  "SKShader/SKShapeNode: u_path_length is not set unless shouldUseLocalStrokeBuffers() is true"  
        // add a few points to work around this bug in iOS 10-10.1 ->  
        // for i in 0...15 {  
        //    path1.addLine( to: CGPoint( x: 0, y: strokeHeight + CGFloat( 0.001 ) * CGFloat( i ) ) )  
        // }  

        path1.closeSubpath()  
        let strokeWidth = 17.0 * strokeSizeFactor  
        let path2 = CGMutablePath()  
        path2.move( to: CGPoint.zero )  
        path2.addLine( to: CGPoint( x: 0, y: strokeHeight ) )  
        path2.closeSubpath()  
        let backgroundShapeNode = SKShapeNode( path: path2 )  
        backgroundShapeNode.lineWidth = strokeWidth  
        backgroundShapeNode.zPosition = 5.0  
        backgroundShapeNode.lineCap = .round  
        backgroundShapeNode.strokeColor = SKColor.darkGray  
        addChild( backgroundShapeNode )  

        let shapeNode = SKShapeNode( path: path1 )  
        shapeNode.lineWidth = strokeWidth  
        shapeNode.lineCap = .round  
        backgroundShapeNode.addChild( shapeNode )  
        shapeNode.addChild( cameraNode )  
        shapeNode.strokeShader = strokeShader  
        backgroundShapeNode.calculateAccumulatedFrame()  

        cameraNode.position = CGPoint( x: backgroundShapeNode.frame.size.width/2.0, y: backgroundShapeNode.frame.size.height/2.0 )              
    }  

    override func update(_ currentTime: TimeInterval) {        
        // the increment chosen determines how fast the path is stroked. Note this maps to "u_current_percentage" within animateStroke.fsh  
        strokeLengthFloat += 0.01  
        if strokeLengthFloat > 1.0 {  
            strokeLengthFloat = 0.0  
        }        
    }  
}  

很棒的东西。你有没有关于如何使颜色在“线”本身上移动的见解?现在,我想让线条周围的alpha值从1.0渐变到0.0。也就是说,这不是关于v_path_distance的问题,而应该是指某些着色器变量(如果存在这样的变量,我不确定)在“水平”方向上跨越线条。使用默认着色器的SKShapeNode可以通过glowWidth属性实现这一点。我打算做类似的事情。最终,也希望线条上也有类似的渐变效果。 - Jonny
1
我想我找到了如何利用glowWidth值的方法。我在另一个答案中添加了我的更新着色器。 - Jonny
你能稍微解释一下shader吗?你不能使用它来设置glowWidth或颜色。 - twodayslate
很棒的解决方案!有没有想法如何使用SKAction来动画化自定义strokeShader? - Bogy
实际上,您可以使用SKAction.customAction,传入一个块,SpriteKit将在指定的持续时间内回调该块。 SK将elapsedTime传递到块中,因此您可以使用它来驱动进度,而不是像上面的答案中那样使用更新回调。 - Bobjt
显示剩余3条评论

4

根据Bobjt的答案,我的着色器程序利用SKShapeNode的glowWidth属性...

这样,当我使用它时,我可以像这样使用shapeNode.glowWidth = 5.0;等!

void main()
{
    vec4 def = SKDefaultShading();

    if ( u_path_length == 0.0 ) {
        gl_FragColor = vec4( 0xff / 255.0f, 0 / 255.0f, 0 / 255.0f, 1.0f );
    } else if ( v_path_distance / u_path_length <= u_current_percentage ) {
        gl_FragColor = vec4(
                            //0x1e / 255.0f, 0xb8 / 255.0f, 0xca / 255.0f, <-- original boring color :-P
                            0x1e / 255.0f * def.z, 0xb8 / 255.0f * def.z, 0xca / 255.0f * def.z, def.z
                            );
    } else {
        gl_FragColor = vec4( 0.0f, 0.0f, 0.0f, 0.0f );
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接