Can I use a PNG sequence as an animated material o

2019-05-10 14:43发布

问题:

I'd like to programmatically change the material of an object rendered using SceneKit in iOS. But I would like to use animated image files. PNG sequences work great in iOS for stuff like UIImageViews -- how do I use them as a material in SceneKit?

Something like this doesn't work:

floor.firstMaterial.diffuse.contents = [UIImage animatedImageNamed:@"ANIMATION_" duration:6.0f];

Thanks!

回答1:

there is nothing that you can use out of the box.

I would do this using shader modifiers. If you pack all your images in a single image, you can use a modifier (at the SCNShaderModifierEntryPointGeometry entry point) to update the object's texture coordinates according to u_time so that every t ms another image (or sub-image of the atlas) is used.



回答2:

There is a much simpler solution (or at least I think it's simpler) though I can't attest to whether it's more efficient. I've tested this on an iPad mini (1st gen) and it works fine.

Rather than place a texture that is a spritesheet into the material's contents (and fiddle with the shader), you can place an SKScene object there instead.

That way you can just use SKSpriteNode objects within the SKScene and animate them using SCNAction objects. All of the animation can then be controlled easily and more powerfully without having to touch shader code.

    NSArray<SKTexture*> *cursorTextures =
    @[
      [SKTexture textureWithImageNamed:@"tilecursor1.png"],
      [SKTexture textureWithImageNamed:@"tilecursor2.png"],
      [SKTexture textureWithImageNamed:@"tilecursor3.png"],
      [SKTexture textureWithImageNamed:@"tilecursor4.png"],
      [SKTexture textureWithImageNamed:@"tilecursor5.png"]
      ];

    SKSpriteNode *cursorSprite = [SKSpriteNode spriteNodeWithTexture:cursorTextures[0]];
    cursorSprite.position = CGPointMake(cursorSprite.size.width/2.0, cursorSprite.size.height/2.0);
    [cursorSprite runAction:[SKAction repeatActionForever:[SKAction animateWithTextures:cursorTextures timePerFrame:0.1]]];

    SKScene *cursorScene = [SKScene sceneWithSize:cursorSprite.size];
    cursorScene.backgroundColor = [UIColor clearColor];
    [cursorScene addChild:cursorSprite];

    ...

    self.cursor.material.diffuse.contents = cursorScene;


回答3:

Swift 4.1 / Xcode 9.3 / iOS 11.3

You can resolve this by rolling your own animation with CADisplayLink:

Load your images into an array of UIImage

var images : [UIImage] = [
    UIImage(named: "image1.png"),
    UIImage(named: "image2.png"),
    ...
]

Then, to start the animation:

var animationImageFrameIndex : Double = 0
var displayLink : CADisplayLink?

func startAnimation() {
    animationImageFrameIndex = 0
    displayLink = CADisplayLink(target: self, selector: #selector(animationStep(_:)))
    displayLink!.preferredFramesPerSecond = 60
    displayLink!.add(to: .current, forMode: .defaultRunLoopMode)
}

The preferredFramesPerSecond is the desired rate at which you'd like your animationStep method to be called. However, CADisplayLink does not guaranty that it will be called at the rate you request.

The animationStep function gets called periodically by CADisplayLink, which actually cycles through the images:

@objc func animationStep(_ displayLink: CADisplayLink) {
    let desiredFPS : Double = 24
    let realFPS = 1 / (displayLink.targetTimestamp - displayLink.timestamp)

    material.diffuse.contents = images[Int(animationImageFrameIndex)]
    animationImageFrameIndex += desiredFPS / realFPS
    if Int(animationImageFrameIndex) >= images.count {
        displayLink.remove(from: .current, forMode: .defaultRunLoopMode)
    }
}

In the method above, desiredFPS is the frame rate you actually want to use in the animation.

There is a full sample project on Bitbucket, here.

You SHOULD be able to animate this (a sequence of images) with a simple CAKeyframeAnimation, but after much testing, it's clear that doesn't work, which appears to be a bug on Apple's side. There's an open Radar here from someone else, and I also filed a bug report.