SceneKit - Crossfade material property textures
Asked Answered
A

3

11

The documentation for SCNMaterialProperty.contents states that it is an animatable property and indeed I can perform a crossfade between two colors. However I’m unable to crossfade between two images.

So I’m starting to wonder if this is possible at all or if I need to create a custom shader for this?


I’ve tried an implicit animation, in which case it immediately shows the ‘after’ image:

node.geometry.firstMaterial.diffuse.contents = [UIImage imageNamed:@"before"];
[SCNTransaction begin];
[SCNTransaction setAnimationDuration:5];
node.geometry.firstMaterial.diffuse.contents = [UIImage imageNamed:@"after"];
[SCNTransaction commit];

An explicit animation, which does nothing:

CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"contents"];
animation.fromValue = (__bridge id)[UIImage imageNamed:@"before"].CGImage;
animation.toValue = (__bridge id)[UIImage imageNamed:@"after"].CGImage;
animation.duration = 5;
[node.geometry.firstMaterial.diffuse addAnimation:animation forKey:nil];

As well as through a CALayer, which does nothing:

CALayer *textureLayer = [CALayer layer];
textureLayer.frame = CGRectMake(0, 0, 793, 1006);
textureLayer.contents = (__bridge id)[UIImage imageNamed:@"before"].CGImage;
node.geometry.firstMaterial.diffuse.contents = textureLayer;

CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"contents"];
animation.fromValue = (__bridge id)[UIImage imageNamed:@"before"].CGImage;
animation.toValue = (__bridge id)[UIImage imageNamed:@"after"].CGImage;
animation.duration = 5;
[textureLayer addAnimation:animation forKey:nil];
Anchylose answered 20/10, 2015 at 11:9 Comment(1)
Have you tried animating a change between two materials, instead of between two contents values for the same material property?Magistrate
M
14

From my own testing, it doesn't look like this property is actually animatable when texture values are involved (rather than solid color values). Either that's a bug in SceneKit (i.e. it's intended to be animatable but that's not working) or it's a bug in Apple's docs (i.e. it's not intended to be animatable but they say it is). Either way, you should file that bug so you get notified when Apple fixes it.

(It also doesn't look like this is a tvOS-specific issue -- I see it on OS X as well.)

I can understand why animated texture transitions might not be there... from a GL/Metal perspective, that requires binding an extra texture unit and having two texture lookups per pixel (instead of one) during the transition.

I can think of a couple of decent potential workarounds:

  1. Use a shader modifier. Write a GLSL(ish) snippet that looks something like this:

    uniform sampler2D otherTexture;
    uniform float fadeFactor;
    #pragma body
    vec4 otherTexel = texture2D(otherTexture, _surface.diffuseTexcoord);
    _surface.diffuse = mix(_surface.diffuse, otherTexel, fadeFactor);
    

    Set it on the material you want to animate using the SCNShaderModifierEntryPointSurface entry point. Then use setValue:forKey: to associate a SCNMaterialProperty with the otherTexture and a CABasicAnimation to animate the fadeFactor from 0 to 1.

  2. Use something more animated (like a SpriteKit scene) as your material property contents, and animate that to perform the transition. (As a bonus, when you do it this way, you can use other transition styles.)

Magistrate answered 26/10, 2015 at 23:31 Comment(2)
Thanks a lot, that works perfectly! I had tried to bind a texture before through KVC, but I mistakingly assigned the image directly without the use of SCNMaterialProperty, which seems silly in retrospect. I’ll file that Radar right away.Anchylose
Hello @rickster, can you please give an example on how to apply CABasicAnimation for fadeFactor ? I try many things, but that doesn't work !!! Please help. ThanksIssiah
C
1

Your animation isn't happening because "contents" property is animatable only when set to a color not for image. You can read it on the apple's documentation about contents property.

Chromoprotein answered 27/10, 2015 at 8:8 Comment(5)
I just took another look at the docs, but I can’t find anything about an image not being suitable for animating.Anchylose
From the Documentation: property contents: Specifies the receiver's contents. This can be a color (NSColor/UIColor), an image (NSImage/CGImageRef), a layer (CALayer), a path (NSString or NSURL), a SpriteKit scene (SKScene) or a texture (SKTexture, id<MTLTexture> or GLKTextureInfo). Animatable when set to a color.Chromoprotein
Huh, even when I do a search for “Animatable when set to a color” I can’t find it on this page: developer.apple.com/library/prerelease/ios/documentation/…. Where are you seeing this?Anchylose
I see. I don’t use Xcode, so I was referring to the online doc that I linked to, weird that they’d diverge.Anchylose
If the current content is a solid color, you can use explicit or implicit animations (see Animating SceneKit Content) to change to another color, creating an effect that fades between the two colors. Using animations to change from or to other content types results in an instantaneous transition—for an animated transition between textured content types (or types that are themselves animated), create a shader modifier (see SCNShadable).Emblazon
M
0

I found a simpler approach that worked for my particular needs. It's not a true crossfade because the two textures fade sequentially instead of simultaneously. But it's close.

Although the contents property isn't animatable by the usual means, the intensity property is.

  1. Fade out the intensity with a CAKeyframeAnimation.
  2. Set the contents to the new texture when intensity reaches zero using a delay.
  3. Fade the intensity back in.
let animation = CAKeyframeAnimation(keyPath: "multiply.intensity")

animation.values = [1.0, 0.0, 0.0, 1.0]
animation.keyTimes = [0.0, 0.4, 0.6, 1.0]
animation.autoreverses = false
animation.duration = 1.0

material.addAnimation(animation, forKey: nil)

//I'm using DispatchQueue, but you could use SCNAction instead:
DispatchQueue.main.asyncAfter(deadline: .now() + 0.45) {
   //The animation has reached a value of zero. Set the new texture:
   material.multiply.contents = newTexture
}
Microreader answered 21/8 at 18:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.