I am loading a third-party .dae Collada file as a scene into a SceneKit project.
The .dae file has many different animations in it, set at different times/frames. I am trying to figure out how I can split these out and reference each individual animation by reference name. There are no intelligible reference names in the dae file - the animations are all set as one single animation.
I am able to parse out the animations into a CAAnimation object, and verify that I have successfully done so with the following code:
SCNScene *scene = [SCNScene sceneNamed:#"art.scnassets/man.dae"];
SCNNode *man = [scene.rootNode childNodeWithName:#"Bip01" recursively:YES];
CAAnimation *animation = [man animationForKey:#"test_Collada_DAE-1"];
[man removeAllAnimations];
[man addAnimation:animation forKey:#"animation"];
Is there any way to set a start and end frame or time to my CAAnimation object? What is the best way to parse out the various animations? I'm hoping that I do not have to manually split the dae file into many and load each one individually.
3d tools often export multiple animations as a single animation with sub animations. In that case SceneKit will load these animations as a CAAnimationGroup with sub animations. So one option is to "parse" the animation group's sub animations and retrieve the ones you want. Another option is to retrieve (sub)animations by name using SCNSceneSource (but this will work only if your 3d tool exported names when it exported your DAE).
If you need to "crop" animation (i.e extract an animation that starts at t0 with duration D from a longer animation), CoreAnimation has a APIs for that:
create an animation Group to "crop" with duration D.
add the animation you want to crop as a sub-animation and set it's
timeOffset to t0.
As Toyos mentioned in his answer, enumerate the CAAnimationGroup using SCNSceneSource and retrieve the CAAnimation objects as follows:
NSURL *daeURL = [[NSBundle mainBundle] URLForResource:#"exportedFilename" withExtension:#"dae"];
SCNSceneSource *sceneSource = [SCNSceneSource sceneSourceWithURL:daeURL options:nil];
NSMutableArray *myAnimations = [#[] mutableCopy];
for (NSString *singleAnimationName in [sceneSource identifiersOfEntriesWithClass:[CAAnimation class]]) {
CAAnimation *thisAnimation = [sceneSource entryWithIdentifier:singleAnimationName withClass:[CAAnimation class]];
[myAnimations addObject:thisAnimation];
}
Here is code that converts frame numbers to times and then plays only that portion of the animation by using a CAAnimationGroup as #Toyos described. This sample code plays an "idle" animation by repeating frames 10 through 160 of fullAnimation:
func playIdleAnimation() {
let animation = subAnimation(of:fullAnimation, startFrame: 10, endFrame: 160)
animation.repeatCount = .greatestFiniteMagnitude
addAnimation(animation, forKey: "animation")
}
func subAnimation(of fullAnimation:CAAnimation, startFrame:Int, endFrame:Int) -> CAAnimation {
let (startTime, duration) = timeRange(startFrame:startFrame, endFrame:endFrame)
let animation = subAnimation(of: fullAnimation, offset: startTime, duration: duration)
return animation
}
func subAnimation(of fullAnimation:CAAnimation, offset timeOffset:CFTimeInterval, duration:CFTimeInterval) -> CAAnimation {
fullAnimation.timeOffset = timeOffset
let container = CAAnimationGroup()
container.animations = [fullAnimation]
container.duration = duration
return container
}
func timeRange(startFrame:Int, endFrame:Int) -> (startTime:CFTimeInterval, duration:CFTimeInterval) {
let startTime = timeOf(frame:startFrame)
let endTime = timeOf(frame:endFrame)
let duration = endTime - startTime
return (startTime, duration)
}
func timeOf(frame:Int) -> CFTimeInterval {
return CFTimeInterval(frame) / framesPerSecond()
}
func framesPerSecond() -> CFTimeInterval {
// number of frames per second the model was designed with
return 30.0
}
Related
I have a view that creates SCNView dynamically. It's scene is empty, but when I press a button I would like to add a node from separate scn file. This file contains animation, and I would like it to animate in main scene. The problem is that after adding object to the scene it's not animating. When I use this file as SCNView scene it works. isPlaying and loops are enabled. What else do I need to do to import such node with animation? Sample code below:
override func viewDidLoad() {
super.viewDidLoad()
let scene = SCNScene()
let sceneView = SCNView(frame: CGRect(x: 0, y: 0, width: 300, height: 300))
sceneView.scene = scene
sceneView.loops = true
sceneView.isPlaying = true
sceneView.autoenablesDefaultLighting = true
view.addSubview(sceneView)
let subNodeScene = SCNScene(named: "Serah_Animated.scn")!
let serah = subNodeScene.rootNode.childNode(withName: "main", recursively: false)!
scene.rootNode.addChildNode(serah)
}
You need to fetch the animation from your scene Serah_Animated.scn, which will be a CAAnimation object. You then add that animation object to the rootNode of your main scene.
let animScene = SCNSceneSource(url:<<URL to your scene file", options:<<Scene Loading Options>>)
let animation:CAAnimation = animScene.entryWithIdentifier(<<animID>>, withClass:CAAnimation.self)
You can find the animID from the .scn file using scene editor in Xcode, as shown below.
Now you can add the animation object to your root node.
scene.rootNode.addAnimation(animation, forKey:<<animID>>)
Note that we are reusing animID, that will allow you to also remove the animation from the node.
scene.rootNode.removeAnimation(forKey:<<animId>>)
My solution above assumes your animation is a single animation. If you see a bunch of animations, you need to add all the animation nodes. In my workflow, I have files in Blender which I export to Collada format and then use the Automated Collada Converter to ensure I have single animation nodes.
Related SO answer
You can also fetch the animID programmatically using entriesWithIdentifiersOfClass(CAAnimation.self), useful when you have a bunch of animations instead of a single animation as above or if you just want to add the animation without bothering about the animID before hand.
Apple Sample Code for scene kit animations, note the sample code is in ObjC but translation to Swift should be straight forward.
All you need is retrieving animations:
[childNode enumerateChildNodesUsingBlock:^(SCNNode *child, BOOL *stop) {
for(NSString *key in child.animationKeys) { // for every animation key
CAAnimation *animation = [child animationForKey:key]; // get the animation
animation.usesSceneTimeBase = NO; // make it system time based
animation.repeatCount = FLT_MAX; // make it repeat forever
[child addAnimation:animation forKey:key]; // animations are copied upon addition, so we have to replace the previous animation
}
}];
I'm currently animating between 4 images like this:
UIImageView *tom3BeforeImage;
tom3Images = [NSArray arrayWithObjects: [UIImage imageNamed:#"floortom_before1.png"],[UIImage imageNamed:#"floortom_before2.png"],[UIImage imageNamed:#"floortom_before3.png"],[UIImage imageNamed:#"floortom_before4.png"], nil ];
tom3BeforeImage.animationImages = tom3Images;
tom3BeforeImage.animationDuration = 0.75;
[tom3BeforeImage startAnimating];
It works fine, except that the animation is choppy between the images. I need the duration to be exactly .75 seconds, so speeding it up is not an option.
What's the best way to have the animation be smoother between the images, kind of like blending between each image change?
Thanks!
If you're using frame based UIImageView animation, and the animation must be .75 seconds, then the only way I know of to make it smoother is to create more frames. Try 30 frames/second, or about 22 frames. That should give very smooth motion.
If you want some sort of cross-dissolve between frames then you won't be able to use UIView frame animation. you'll have to use UIView block animation (using animateWithDuration:animations: or its cousins.)
You could create a sequence of cross-dissolves between your frames where the total duration of the sequence is .75 seconds. Have each transition trigger the next transition in it's completion block.
Something like this:
You'll need 2 image views, stacked on top of each other. You'll fade one out and the other in at the same time. You'll need to set the opaque flag to NO on both.
Lets call them tom3BeforeImage1 and tom3BeforeImage2
Add an int instance variable imageCount and make your array of images, tom3Images, and instance variable as well:
- (void) animateImages;
{
CGFloat duration = .75 / ([tom3Images count] -1);
//Start with the current image fully visible in tom3BeforeImage1
tom3BeforeImage1.image = tom3Images[imageCount];
tom3BeforeImage1.alpha = 1.0;
//Get the next image ready, at alpha 0, in tom3BeforeImage2
tom3BeforeImage2.image = tom3Images[imageCount+1];
tom3BeforeImage2.alpha = 0;
imageCount++
[UIView animateWithDuration: duration
delay: 0
options: UIViewAnimationOptionCurveLinear
animations:
^{
//Fade out the current image
tom3BeforeImage1.alpha = 0.0;
//Fade in the new image
tom3BeforeImage2.alpha = 1.0;
}
completion:
^{
//When the current animation step completes, trigger the method again.
if (imageCount < [tom3Images count] -1)
[self animateImages];
}
];
}
Note that I banged out the code above in the forum editor without having had enough coffee. It likely contains syntax errors, and may even have logic problems. This is just to get you thinking about how to do it.
Edit #2:
I'm not sure why, but I decided to flesh this out into a full-blown example project. The code above works passably well after debugging, but since it's fading one image out at the same time it's fading another one in, the background behind both image views shows through.
I reworked it to have logic that only fades the top image in and out. It puts the first frame in the top image view and the second frame in the bottom image view, then fades out the top image view.
The project is up on github, called Animate-Img. (link)
Then it installs the third frame in the top image view and fades it IN,
Then it installs the 4th fame in the bottom image view and fades out the top to expose the bottom, etc, etc.
I ended up creating a generalized method
- (void) animateImagesWithDuration: (CGFloat) totalDuration
reverse: (BOOL) reverse
crossfade: (BOOL) doCrossfade
withCompletionBlock: (void (^)(void)) completionBlock;
It will animate a set of images, into a pair of image views, optionally reversing the animation once it's done. It takes a completion block that gets called once the animation is finished.
The animate button actually calls a method that repeats the whole animation sequence. It's currently set to only run it once, but changing a constant will make the program repeat the whole sequence, if desired.
I do had the requirement to have animation with array of images. Initially when i used animationImages property of imageview, I got the desired animation but the transition between the images were not smooth, I then used CAKeyframeAnimation to achieve the smooth transition, the catch is to use timingFunction along with correct calculationMode. I am not sure this is the exact the answer for the question but this is one way to make the animation smoother,
below is the code for that
For more info on calculation mode please see Apple Documentation
- (void) animate
{
NSMutableArray * imageArray = [[NSMutableArray alloc] init];
int imageCount = 8;
for (int i=0; i<=imageCount; i++) {
[imageArray addObject:(id)[UIImage imageNamed:[NSString stringWithFormat:#“image%d”,i]].CGImage];
}
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:#"contents"];
animation.calculationMode = kCAAnimationLinear;// Make sure this is kCAAnimationLinear or kCAAnimationCubic other mode doesnt consider the timing function
animation.duration = 8.0;
animation.values = imageArray;
animation.repeatCount = 1; // Change it for repetition
animation.removedOnCompletion = NO;
animation.fillMode = kCAFillModeForwards; // To keep the last frame when animation ends
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
[imageView.layer addAnimation:animation forKey:#"animation"];
}
UPDATE- Swift 3
func animate() {
var imageArray = [CGImage]()
let imageCount: Int = 3
for i in 0...imageCount {
imageArray.append((UIImage(named: String(format:"image\(i)"))?.cgImage!)!)
}
let animation = CAKeyframeAnimation(keyPath: "contents")
animation.calculationMode = kCAAnimationLinear
// Make sure this is kCAAnimationLinear or kCAAnimationCubic other mode doesnt consider the timing function
animation.duration = CFTimeInterval(floatLiteral: 8.0)
animation.values = imageArray
animation.repeatCount = 1
// Change it for repetition
animation.isRemovedOnCompletion = false
animation.fillMode = kCAFillModeForwards
// To keep the last frame when animation ends
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
animImageView.layer.add(animation, forKey: "animation")
}
I'm trying to make a sequence of animations, I've found in CAAnimationGroup the right class to achieve that object. In practice I'm adding on a view different subviews and I'd like to animate their entry with a bounce effect, the fact is that I want to see their animations happening right after the previous has finished. I know that I can set the delegate, but I thought that the CAAnimationGroup was the right choice.
Later I discovered that the group animation can belong only to one layer, but I need it on different layers on screen. Of course on the hosting layer doesn't work.
Some suggestions?
- (void) didMoveToSuperview {
[super didMoveToSuperview];
float startTime = 0;
NSMutableArray * animArray = #[].mutableCopy;
for (int i = 1; i<=_score; i++) {
NSData *archivedData = [NSKeyedArchiver archivedDataWithRootObject: self.greenLeaf];
UIImageView * greenLeafImageView = [NSKeyedUnarchiver unarchiveObjectWithData: archivedData];
greenLeafImageView.image = [UIImage imageNamed:#"greenLeaf"];
CGPoint leafCenter = calculatePointCoordinateWithRadiusAndRotation(63, -(M_PI/11 * i) - M_PI_2);
greenLeafImageView.center = CGPointApplyAffineTransform(leafCenter, CGAffineTransformMakeTranslation(self.bounds.size.width/2, self.bounds.size.height));
[self addSubview:greenLeafImageView];
//Animation creation
CAKeyframeAnimation *bounceAnimation = [CAKeyframeAnimation animationWithKeyPath:#"transform.scale"];
greenLeafImageView.layer.transform = CATransform3DIdentity;
bounceAnimation.values = #[
[NSNumber numberWithFloat:0.5],
[NSNumber numberWithFloat:1.1],
[NSNumber numberWithFloat:0.8],
[NSNumber numberWithFloat:1.0]
];
bounceAnimation.duration = 2;
bounceAnimation.beginTime = startTime;
startTime += bounceAnimation.duration;
[animArray addObject:bounceAnimation];
//[greenLeafImageView.layer addAnimation:bounceAnimation forKey:nil];
}
// Rotation animation
[UIView animateWithDuration:1 animations:^{
self.arrow.transform = CGAffineTransformMakeRotation(M_PI/11 * _score);
}];
CAAnimationGroup * group = [CAAnimationGroup animation];
group.animations = animArray;
group.duration = [[ animArray valueForKeyPath:#"#sum.duration"] floatValue];
[self.layer addAnimation:group forKey:nil];
}
CAAnimationGroup is meant for having multiple CAAnimation subclasses being stacked together to form an animation, for instance, one animation can perform an scale, the other moves it around, while a third one can rotate it, it's not meant for managing multiple layers, but for having multiple overlaying animations.
That said, I think the easiest way to solve your issue, is to assign each CAAnimation a beginTime equivalent to the sum of the durations of all the previous ones, to illustrate:
for i in 0 ..< 20
{
let view : UIView = // Obtain/create the view...;
let bounce = CAKeyframeAnimation(keyPath: "transform.scale")
bounce.duration = 0.5;
bounce.beginTime = CACurrentMediaTime() + bounce.duration * CFTimeInterval(i);
// ...
view.layer.addAnimation(bounce, forKey:"anim.bounce")
}
Notice that everyone gets duration * i, and the CACurrentMediaTime() is a necessity when using the beginTime property (it's basically a high-precision timestamp for "now", used in animations). The whole line could be interpreted as now + duration * i.
Must be noted, that if a CAAnimations is added to a CAAnimationGroup, then its beginTime becomes relative to the group's begin time, so a value of 5.0 on an animation, would be 5.0 seconds after the whole group starts. In this case, you don't use the CACurrentMediaTime()
If you review the documentation, you will note that CAAnimationGroup inherits from CAAnimation, and that CAAnimation can only be assigned to one CALayer. It's intent is really to make it easy to create and manage multiple animations you wish to apply to a CALayer at the same time, not to manager animations for multiple CALayer objects.
To handle the sequencing of different animations between different CALayer or UIViewobjects, a technique I use is to create an NSOperation for each object/animation, then throw them into a NSOperationQueue to manage the sequencing. This is a bit complicated as you have to use the animation completion callback to tell the NSOperation it is finished, but if you write a good animation management subclass of NSOperation, it can be rather convenient and allow you to create sophisticated sequencing paths. The low-rent way of accomplishing the sequencing goal is to simply set the beginTime property on your CAAnimation object (which comes from it's adoption of the CAMediaTiming protocol) as appropriate to get the timing you want.
With that said, I am going to point you to some code that I wrote and open-sourced to solve the exact same use case you describe. You may find it on github here (same code included). I will add the following notes:
My animation management code allow your to define your animation in a plist by identifying the sequence and timing of image changes, scale changes, position changes, etc. It's actually pretty convenient and cleaner to adjust your animation in a plist file rather than in code (which is why I wrote this).
If the user is not expected to interact with the subviews you creating, it's actually much better (less overhead) to create layer objects that are added as sub-layers to your hosting view's layer.
I'm trying to get CAEmitterLayers and CAEmitterCells to start their animation from somewhere in the middle of their parent's duration. Is this possible at all? I tried playing with the beginTime and timeOffset properties but I can't seem to get this working.
Added some code for posterity: (lets say I want the emitter to start at the 5th second)
CAEmitterLayer *emitter = [CAEmitterLayer new];
// emitter.beginTime = -5.0f; // I tried this
// emitter.timeOffset = 5.0f; // I also tried this, with beginTime = 0.0, and with beginTime = AVCoreAnimationBeginTimeAtZero
/* set some other CAEmitterLayer properties */
CAEmitterCell *cell = [CAEmitterCell new];
// cell.beginTime = -5.0f; // Then I saw that CAEmitterCell implements CAMediaTiming protocol so I tried this
// cell.timeOffset = 5.0f; // and this
/* set some other CAEmitterCell properties */
emitter.emitterCells = #[cell];
[viewLayer addSubLayer:emitter];
But still the animation starts from where the emitter generates the particles.
Edited again to explain what I'm trying to do:
Lets say I have a CAEmitterLayer that animates rain, so I setup the cells to do a "falling" animation that starts from the top of the screen. During the start of rendering, I don't want to start in a state that's "not raining yet". I want to start where the screen is already covered with rain.
The beginTime isn't relative to now. You need to grab the current time relative to the current layer time space, which you can get by using the CACurrentMediaTime() function. So in your case, you'd do something like this:
emitter.beginTime = CACurrentMediaTime() + 5.f;
I am using key frame animation to animate a sequence of images. But when I run it for the first time there is a delay before animation begins. After that it runs smoothly. I tried force loading all images. It reduced delay but it is still visible. How can I further reduce the delay.
Apple is notorious for using "lazy" loading techniques, and its quite possible that putting an image retrieved from "[UIImage imageNamed:]" does not in fact create a cached bitmap, just the receipt to create it.
If all else fails try a brute force approach: force the system to render it by rendering the image in a context you then just throw away.
CGSize bigSize; // MAX width and height of your images
UIGraphicsBeginImageContextWithOptions(bigSize, YES, 0);
CGContextRef context = UIGraphicsGetCurrentContext();
for(UIImage *image in arrayOfImages) {
CGContextDrawImage(context, (CGRect){ {0,0}, image.size }, [image CGImage]);
}
UIGraphicsEndImageContext();
Now you not only have a reference to the images, but they have been forced to render, and so hopefully the system keeps that internal bitmap around. Should be an easy test for you to make.
This code is to swift 2.2!
Try to put a final image of you want before the animations end.
It's more like:
// the animation key is for retrive the informations about the array images
func animation(cicleTime:Int, withDuration: Double, animationKey:String){
var images = [UIImage]()
// get all images for animation
for i in 0...cicleTime {
let fileName = String(format: "image%d",i)
let image = UIImage(named: fileName)
images.append(image!)
}
let animation = CAKeyframeAnimation(keyPath: "contents")
animation.setValue(animationKey, forKey: "animationName")
animation.duration = withDuration
animation.repeatCount = 1 // times you want to repeat the animation
animation.values = images.map{$0.CGImage as! AnyObject}
animation.delegate = self
images.removeAll(keepCapacity: false)
YourViewHere.layer.addAnimation(animation, forKey: "contents")
YourViewHere.image = UIImage(named: "nameOfYourLastImageOfAnimation")
}
Well, this works for me.
I had this problem recently and solved it by 'prerunning' the animation as early as possible with a duration of 0 and not removing it on completion. By the time I actually want to run it, the whole sequence is loaded and smooth
let animation: CAKeyframeAnimation = CAKeyframeAnimation(keyPath: "contents")
animation.calculationMode = kCAAnimationDiscrete
animation.duration = 0
animation.values = spritesArray
animation.repeatCount = 1
animation.removedOnCompletion = false
animation.fillMode = kCAFillModeForwards
layer.addAnimation(animation, forKey: "animateIn")
In my case I actually had split the sequence into 2 separate keyframe animations for intro/outro and the outro was always stalling before starting. Preloading the whole thing first in a 0 duration keyframe animation prevented that.
Consider pre-loading your images in a NSArray.
Your delay is most likely caused by the fact that it first has to load the images.
So , basically , let's say you have img1.png , img2.png , etc up to img10.png:
//do this before your keyframe animation.
NSMutableArray *frames = [NSMutableArray array];
for(int i = 1 ; i <= 10 ; i++)
[frames addObject:[UIImage imageNamed:[NSString stringWithFormat:#"img%d.png" , i]]];
//now use this array for the animation
Hope this helps. Cheers!