Анимация рисования пути в SpriteKit

Я пытаюсь анимировать рисунок штриха на пути, используя 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 функции, о которых я не знаю, которые можно использовать для улучшения первой реализации.


person Scott Mielcarski    schedule 09.08.2014    source источник
comment
Я рассмотрю это подробнее, когда не буду публиковать сообщения с телефона, но вот подсказка для начала: если вы создадите собственный SKShader для рисования узла фигуры одна из переменных, к которым вы можете получить доступ в коде шейдера GLSL, — это длина дуги вдоль пути.   -  person rickster    schedule 09.08.2014
comment
@rickster Ах, интересно. Я не очень хорошо знаком с GLSL, но кое-что почитаю.   -  person Scott Mielcarski    schedule 09.08.2014
comment
Кто-нибудь когда-нибудь писал собственный SKShader для рисования дуги?   -  person Ben Morrow    schedule 16.09.2016
comment
@BenMorrow Я копался в этом, но в итоге использовал метод, указанный в моем первоначальном вопросе. Я думаю, что вариант SKShader будет намного лучше. Извините, я не могу больше помочь.   -  person Scott Mielcarski    schedule 16.09.2016
comment
@ScottMielcarski Спасибо, я использую что-то подобное   -  person Ben Morrow    schedule 16.09.2016
comment
@BenMorrow Нет проблем, если вы найдете лучшее решение, опубликуйте его как ответ. Я уверен, что другие люди сталкивались с подобными проблемами.   -  person Scott Mielcarski    schedule 17.09.2016


Ответы (2)


вы можете анимировать путь SKShapeNode, предоставив пользовательский штрихшейдер, который выводит на основе нескольких свойств SKShader. , v_path_distance и u_path_length. На это намекнул Рикстер выше, полный код для этого следует. В шейдере мы добавляем u_current_percentage и ссылаемся на текущую точку пути, до которого мы хотим перейти. Тем самым сцена определяет темп анимированного поглаживания. Также обратите внимание, что strokeShader, будучи фрагментным шейдером, выводит RGB на каждом шаге, он позволяет управлять цветом вдоль пути, делая возможным, например, градиентный цвет.

Шейдер добавляется в виде файла в проект 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  
        }        
    }  
}  
person Bobjt    schedule 12.01.2017
comment
Отличный материал. Есть ли у вас какие-либо идеи о том, как сделать сдвиг цвета по самой линии. На данный момент я бы хотел, чтобы альфа окраин линии исчезала с 1,0 до 0,0. т.е. это не о v_path_distance, но должно ссылаться на какую-то переменную шейдера (если такая существует, я не уверен) горизонтально поперек линии. SKShapeNode с шейдером по умолчанию делает это с помощью свойства GlowWidth. Я стремлюсь сделать что-то подобное. В конце концов, было бы неплохо также иметь градиенты по всей линии. - person Jonny; 20.01.2017
comment
Я думаю, что узнал, как использовать значения GlowWidth. Я добавил свой обновленный шейдер в другой ответ. - person Jonny; 20.01.2017
comment
Относительно комментария по поводу ошибки 27989113: Похоже, это не работает с iOS 9.3.5 (или любой iOS 9, может быть, и, возможно, до iOS 10.2, у меня нет таких устройств, чтобы проверить атм). Я предполагаю, что есть проблема с u_path_length... Для меня я использую круг ([path1 addArcWithCenter:CGPointZero radius:RADIUS startAngle:M_PI_2 * 5 endAngle:M_PI_2 * 1 clockwise:NO];), и это портит весь вид спрайткита - он становится черным. Я еще не придумал, как это исправить. - person Jonny; 23.01.2017
comment
Не могли бы вы немного объяснить шейдер? Вы не можете установить GlowWidth или цвет с этим. - person twodayslate; 03.03.2017
comment
Отличное решение! Есть идеи, как анимировать пользовательский штрихшейдер с помощью SKAction? - person Bogy; 16.11.2017
comment
Да, на самом деле вы можете использовать SKAction.customAction, передавая блок, который SpriteKit будет вызывать в течение указанного времени. SK передает elapsedTime в блок, поэтому вы можете использовать его для управления своим прогрессом вместо обратного вызова обновления, как в ответе выше. - person Bobjt; 17.11.2017
comment
По этому пути можно переместить SKSpriteNode. На самом деле я пробовал что-то вроде, пусть moveObject = SKAction.move(to: movePoint, duration: 2.0) player.run(SKAction.sequence([ moveObject])) Но объект достигает своей позиции раньше, чем целевая строка. Любая помощь будет оценена. - person Madhavan; 20.11.2018
comment
Я реализовал этот способ с помощью вашего кода.. но точный вывод не идет.. in-spritkit-in-swift" title="рисование линии с использованием фрагментного шейдера вершинного шейдера в spritkit in swift">stackoverflow.com/questions/53405562/ . можно здесь проверить.. - person PRADIP KUMAR; 21.11.2018

Основываясь на ответе Bobjt, мой шейдер, который использует GlowWidth SKShapeNode... Поэтому, когда я использую это, я могу использовать 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 );
    }
}
person Jonny    schedule 20.01.2017