Scene Kit Session WWDC 2013/Sources/Slides/ASCSlideCoreImage.m
/* |
File: ASCSlideCoreImage.m |
Abstract: Shows an example of how Core Image filters can be used to achieve screen-space effects. |
Version: 1.1 |
Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple |
Inc. ("Apple") in consideration of your agreement to the following |
terms, and your use, installation, modification or redistribution of |
this Apple software constitutes acceptance of these terms. If you do |
not agree with these terms, please do not use, install, modify or |
redistribute this Apple software. |
In consideration of your agreement to abide by the following terms, and |
subject to these terms, Apple grants you a personal, non-exclusive |
license, under Apple's copyrights in this original Apple software (the |
"Apple Software"), to use, reproduce, modify and redistribute the Apple |
Software, with or without modifications, in source and/or binary forms; |
provided that if you redistribute the Apple Software in its entirety and |
without modifications, you must retain this notice and the following |
text and disclaimers in all such redistributions of the Apple Software. |
Neither the name, trademarks, service marks or logos of Apple Inc. may |
be used to endorse or promote products derived from the Apple Software |
without specific prior written permission from Apple. Except as |
expressly stated in this notice, no other rights or licenses, express or |
implied, are granted by Apple herein, including but not limited to any |
patent rights that may be infringed by your derivative works or by other |
works in which the Apple Software may be incorporated. |
The Apple Software is provided by Apple on an "AS IS" basis. APPLE |
MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION |
THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS |
FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND |
OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. |
IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL |
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, |
MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED |
AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), |
STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE |
POSSIBILITY OF SUCH DAMAGE. |
Copyright (C) 2014 Apple Inc. All Rights Reserved. |
*/ |
#import <GLKit/GLKMath.h> |
#import "ASCPresentationViewController.h" |
#import "ASCSlideTextManager.h" |
#import "ASCSlide.h" |
#import "Utils.h" |
static NSUInteger const kContactImageCount = 44; |
static NSUInteger const kRowCount = 4; |
static NSUInteger const kColumnCount = 11; |
// Custom Core Image filter that will be used to make the selection glow |
@interface ASCGlowFilter : CIFilter |
@property (strong, nonatomic) CIImage *inputImage; |
@property (strong, nonatomic) NSNumber *inputRadius; |
@property (strong, nonatomic) NSNumber *centerX; |
@property (strong, nonatomic) NSNumber *centerY; |
@end |
#pragma mark - Core Image slide |
@interface ASCSlideCoreImage : ASCSlide |
@end |
@implementation ASCSlideCoreImage { |
SCNNode *_groupNode; |
SCNNode *_heroNode; |
CGSize _viewportSize; |
} |
- (void)setupSlideWithPresentationViewController:(ASCPresentationViewController *)presentationViewController { |
// Setup the image grid here to benefit from the preloading mechanism |
[self buildImageGrid]; |
_viewportSize = [presentationViewController.view convertSizeToBacking:presentationViewController.view.frame.size]; |
} |
- (NSUInteger)numberOfSteps { |
return 7; |
} |
- (void)presentStepIndex:(NSUInteger)index withPresentionViewController:(ASCPresentationViewController *)presentationViewController { |
switch (index) { |
case 0: |
// Set the slide's title and subtitle and add some text |
self.textManager.title = @"Core Image"; |
self.textManager.subtitle = @"CI Filters"; |
[self.textManager addBullet:@"Screen-space effects" atLevel:0]; |
[self.textManager addBullet:@"Applies to a node hierarchy" atLevel:0]; |
[self.textManager addBullet:@"Filter parameters are animatable" atLevel:0]; |
[self.textManager addCode:@"aNode.#filters# = @[filter1, filter2];"]; |
break; |
case 1: |
{ |
[SCNTransaction begin]; |
[SCNTransaction setAnimationDuration:1.0]; |
{ |
// Dim the text and move back a little |
self.textManager.textNode.opacity = 0.0; |
presentationViewController.cameraHandle.position = [presentationViewController.cameraNode convertPosition:SCNVector3Make(0, 0, 5.0) toNode:presentationViewController.cameraHandle.parentNode]; |
} |
[SCNTransaction commit]; |
// Reveal the grid |
_groupNode.opacity = 1; |
break; |
} |
case 2: |
{ |
[SCNTransaction begin]; |
[SCNTransaction setAnimationDuration:1.0]; |
{ |
// Highlight an item |
[self highlightContactAtIndex:13 withController:presentationViewController]; |
} |
[SCNTransaction commit]; |
break; |
} |
case 3: |
{ |
__block NSInteger index = 13; |
__block NSInteger subStep = 0; |
dispatch_block_t block = ^{ |
[SCNTransaction begin]; |
[SCNTransaction setAnimationDuration:0.2]; |
{ |
[self unhighlightContactAtIndex:index]; |
if (subStep++ == 3) index += kColumnCount; |
else index++; |
[self highlightContactAtIndex:index withController:presentationViewController]; |
} |
[SCNTransaction commit]; |
}; |
// Successively select items |
for (NSInteger i = 0; i < 5; ++i) { |
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(i * 0.2 * NSEC_PER_SEC)); |
dispatch_after(popTime, dispatch_get_main_queue(), block); |
} |
break; |
} |
case 4: |
{ |
// BLUR+DESATURATE in the background, GLOW in the foreground |
// Here we will change the node hierarchy in order to group all the nodes in the background under a single node. |
// This way we can use a single Core Image filter and apply it on the whole grid, and have another CI filter for the node in the foreground. |
SCNNode *selectionParent = _heroNode.parentNode; |
[SCNTransaction begin]; |
[SCNTransaction setAnimationDuration:0.0]; |
{ |
// Stop the animations of the selected node |
_heroNode.transform = _heroNode.presentationNode.transform; // set the current rotation to the current presentation value |
[_heroNode removeAllAnimations]; |
// Re-parent the node by preserving its world tranform |
CATransform3D wantedWorldTransform = selectionParent.worldTransform; |
[_groupNode.parentNode addChildNode:selectionParent]; |
selectionParent.transform = [selectionParent.parentNode convertTransform:wantedWorldTransform fromNode:nil]; |
} |
[SCNTransaction commit]; |
// Add CIFilters |
[SCNTransaction begin]; |
[SCNTransaction setAnimationDuration:1.0]; |
{ |
// A negative 'centerX' value means no scaling. |
[_heroNode.filters[0] setValue:@-1 forKey:@"centerX"]; |
// Move the selection to the foreground |
selectionParent.rotation = SCNVector4Make(0, 1, 0, 0); |
_heroNode.transform = [self.contentNode convertTransform:CATransform3DMakeTranslation(0, self.altitude, 29) toNode:selectionParent]; |
_heroNode.scale = SCNVector3Make(1, 1, 1); |
_heroNode.rotation = SCNVector4Make(1, 0, 0, -M_PI_4 * 0.25); |
// Upon completion, rotate the selection forever |
[SCNTransaction setCompletionBlock:^{ |
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"rotation"]; |
animation.duration = 4.0; |
animation.fromValue = [NSValue valueWithSCNVector4:SCNVector4Make(0, 1, 0, 0)]; |
animation.toValue = [NSValue valueWithSCNVector4:SCNVector4Make(0, 1, 0, M_PI * 2)]; |
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; |
animation.repeatCount = FLT_MAX; |
[_heroNode.childNodes[0] addAnimation:animation forKey:nil]; |
}]; |
// Add the filters |
CIFilter *blurFilter = [CIFilter filterWithName:@"CIGaussianBlur"]; |
[blurFilter setDefaults]; |
blurFilter.name = @"blur"; |
[blurFilter setValue:@0 forKey:kCIInputRadiusKey]; |
CIFilter *desaturateFilter = [CIFilter filterWithName:@"CIColorControls"]; |
[desaturateFilter setDefaults]; |
desaturateFilter.name = @"desaturate"; |
_groupNode.filters = @[blurFilter, desaturateFilter]; |
} |
[SCNTransaction commit]; |
// Increate the blur radius and desaturate progressively |
[SCNTransaction begin]; |
[SCNTransaction setAnimationDuration:2.0]; |
{ |
[_groupNode setValue:@10 forKeyPath:@"filters.blur.inputRadius"]; |
[_groupNode setValue:@0.1 forKeyPath:@"filters.desaturate.inputSaturation"]; |
} |
[SCNTransaction commit]; |
break; |
} |
case 5: |
{ |
// BLUR+CIHatchedScreen in the background, ZOOM+BLUR in the foreground |
[SCNTransaction begin]; |
[SCNTransaction setAnimationDuration:0.0]; |
{ |
CIFilter *blurFilter = [CIFilter filterWithName:@"CIGaussianBlur"]; |
[blurFilter setDefaults]; |
blurFilter.name = @"blur"; |
[blurFilter setValue:@10 forKey:kCIInputRadiusKey]; |
CIFilter *zoomBlurFilter = [CIFilter filterWithName:@"CIZoomBlur"]; |
[zoomBlurFilter setDefaults]; |
[zoomBlurFilter setValue:[CIVector vectorWithX:0.5 * _viewportSize.width Y:0.5 * _viewportSize.height] forKey:kCIInputCenterKey]; |
zoomBlurFilter.name = @"fx1"; |
CIFilter *hatchedScreenFilter = [CIFilter filterWithName:@"CIHatchedScreen"]; |
[hatchedScreenFilter setDefaults]; |
hatchedScreenFilter.name = @"fx2"; |
// Add filters |
_groupNode.filters = @[blurFilter, hatchedScreenFilter]; |
_heroNode.filters = @[zoomBlurFilter]; |
// Animate them |
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"filters.fx2.inputAngle"]; |
animation.toValue = @(0.1); |
animation.fromValue = 0; |
animation.autoreverses = YES; |
animation.repeatCount = FLT_MAX; |
animation.duration = 10.0; |
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; |
[_groupNode addAnimation:animation forKey:nil]; |
animation = [CABasicAnimation animationWithKeyPath:@"filters.fx1.inputAmount"]; |
animation.toValue = @40; |
animation.fromValue = @0; |
animation.autoreverses = YES; |
animation.repeatCount = FLT_MAX; |
animation.duration = 1.0; |
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn]; |
[_heroNode addAnimation:animation forKey:nil]; |
} |
[SCNTransaction commit]; |
break; |
} |
case 6: |
{ |
// BLUR+PIXELATTE in the background, TWIRL in the foreground |
[SCNTransaction begin]; |
[SCNTransaction setAnimationDuration:0.0]; |
{ |
CIFilter *gaussianBlurFilter = [CIFilter filterWithName:@"CIGaussianBlur"]; |
[gaussianBlurFilter setDefaults]; |
gaussianBlurFilter.name = @"blur"; |
[gaussianBlurFilter setValue:@10 forKey:kCIInputRadiusKey]; |
CIFilter *twirlDistortionFilter = [CIFilter filterWithName:@"CITwirlDistortion"]; |
[twirlDistortionFilter setDefaults]; |
twirlDistortionFilter.name = @"fx1"; |
[twirlDistortionFilter setValue:[CIVector vectorWithX:0.5 * _viewportSize.width Y:0.5 * _viewportSize.height] forKey:kCIInputCenterKey]; |
[twirlDistortionFilter setValue:@1000.0 forKey:kCIInputRadiusKey]; |
CIFilter *pixellateFilter = [CIFilter filterWithName:@"CIPixellate"]; |
[pixellateFilter setDefaults]; |
pixellateFilter.name = @"fx2"; |
// Add filters |
_groupNode.filters = @[pixellateFilter, gaussianBlurFilter]; |
_heroNode.filters = @[twirlDistortionFilter]; |
// Animate them |
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"filters.fx1.inputAngle"]; |
animation.toValue = @M_PI; |
animation.fromValue = @-M_PI; |
animation.autoreverses = YES; |
animation.repeatCount = FLT_MAX; |
animation.duration = 1.5; |
animation.timeOffset = -0.75; |
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; |
[_heroNode addAnimation:animation forKey:nil]; |
animation = [CABasicAnimation animationWithKeyPath:@"filters.fx2.inputScale"]; |
animation.toValue = @50; |
animation.fromValue = @0; |
animation.autoreverses = YES; |
animation.repeatCount = FLT_MAX; |
animation.duration = 2.0; |
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; |
[_groupNode addAnimation:animation forKey:nil]; |
} |
[SCNTransaction commit]; |
break; |
} |
} |
} |
- (SCNVector3) mainLightPosition { |
// For this slide we want the main light to be a little upper |
return SCNVector3Make(0, 3, 0); |
} |
#pragma mark - Grid |
- (void)buildImageGrid { |
// Create a root node for the grid |
_groupNode = [SCNNode node]; |
// Retrieve the template node to replicate |
SCNScene *scene = [SCNScene sceneNamed:@"contact.dae"]; |
SCNNode *templateNode = [scene.rootNode childNodeWithName:@"people" recursively:YES]; |
for (NSUInteger k = 0, j = 0; j < kRowCount; j++) { |
for (NSUInteger i = 0; i < kColumnCount; i++, k++) { |
// Hierarchy : _groupNode > container > node |
SCNNode *container = [SCNNode node]; |
SCNNode *node = [templateNode clone]; |
node.name = [NSString stringWithFormat:@"contact%lu", k]; |
[_groupNode addChildNode:container]; |
[container addChildNode:node]; |
if (k == 28) |
_heroNode = node; |
// Curved layout |
CGFloat angle = 0.12 * ((kColumnCount - 1) / 2.0 - i); |
CGFloat x = cos(angle + M_PI_2) * 500.0; |
CGFloat z = sin(angle + M_PI_2) * 500.0; |
container.position = SCNVector3Make(x, j * 60, -z + 400); |
container.rotation = SCNVector4Make(0, 1, 0, angle); |
// We want a different image on each elemement and to do that we need to |
// unshare the geometry first and then unshare the material |
SCNNode *geometryNode = node.childNodes[0]; |
geometryNode.geometry = [geometryNode.geometry copy]; |
SCNMaterial *materialCopy = [geometryNode.geometry.materials[1] copy]; |
materialCopy.diffuse.contents = [NSImage imageNamed:[NSString stringWithFormat:@"contact%lu", k % kContactImageCount]]; |
[geometryNode.geometry replaceMaterialAtIndex:1 withMaterial:materialCopy]; |
// Animate (rotate forever) |
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"rotation"]; |
animation.duration = 4.0; |
animation.keyTimes = @[@0.0, @0.3, @1.0]; |
animation.values = @[[NSValue valueWithSCNVector4:SCNVector4Make(0, 1, 0, 0)], |
[NSValue valueWithSCNVector4:SCNVector4Make(0, 1, 0, M_PI * 2)], |
[NSValue valueWithSCNVector4:SCNVector4Make(0, 1, 0, M_PI * 2)]]; |
CAMediaTimingFunction *tf = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; |
animation.timingFunctions = @[tf, tf, tf]; |
animation.repeatCount = FLT_MAX; |
animation.beginTime = CACurrentMediaTime() + 1.0 + j * 0.1 + i * 0.05; // desynchronize the animations |
[node addAnimation:animation forKey:nil]; |
} |
} |
// Add the group to the scene |
_groupNode.scale = SCNVector3Make(0.03, 0.03, 0.03); |
_groupNode.position = SCNVector3Make(0, self.altitude - 2.8, 18); |
_groupNode.opacity = 0.0; |
[self.groundNode addChildNode:_groupNode]; |
} |
// Unhighlight the node at index 'index' by removing its CI filter |
- (void)unhighlightContactAtIndex:(NSUInteger)index { |
SCNNode *contactNode = [self.groundNode childNodeWithName:[NSString stringWithFormat:@"contact%d", (int)index] recursively:YES]; |
contactNode.filters = nil; |
// Restore the original position and scale |
contactNode.scale = SCNVector3Make(1, 1, 1); |
contactNode.position = SCNVector3Make(contactNode.position.x, contactNode.position.y, contactNode.position.z - 50); |
} |
// Highlight the node at index 'index' by setting a CI filter |
- (void)highlightContactAtIndex:(NSUInteger)index withController:(ASCPresentationViewController *)presentationViewController { |
// Create a filter |
ASCGlowFilter *glowFilter = [[ASCGlowFilter alloc] init]; |
glowFilter.name = @"aGlow"; |
[glowFilter setDefaults]; |
// Retrieve the node to highlight |
// Scale up and move to front a little |
SCNNode *contactNode = [self.groundNode childNodeWithName:[NSString stringWithFormat:@"contact%d", (int)index] recursively:YES]; |
contactNode.scale = SCNVector3Make(1.2, 1.2, 1.2); |
contactNode.position = SCNVector3Make(contactNode.position.x, contactNode.position.y, contactNode.position.z + 50); |
// Compute the screenspace position of this node because the glow filter needs this info |
SCNVector3 worldPosition = [contactNode convertPosition:SCNVector3Make(0, 0, 0) toNode:nil]; |
SCNVector3 screenPosition = [presentationViewController.view projectPoint:worldPosition]; |
CGPoint screenPositionInPixels = [presentationViewController.view convertPointToBacking:CGPointMake(screenPosition.x, screenPosition.y)]; |
[glowFilter setValue:@(screenPositionInPixels.x) forKey:@"centerX"]; |
[glowFilter setValue:@(screenPositionInPixels.y) forKey:@"centerY"]; |
[glowFilter setValue:@10 forKey:@"inputRadius"]; |
// Set the filter |
contactNode.filters = @[glowFilter]; |
// Animate the radius parameter of the glow filter |
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"filters.aGlow.inputRadius"]; |
animation.toValue = @20; |
animation.fromValue = @10; |
animation.autoreverses = YES; |
animation.repeatCount = FLT_MAX; |
animation.duration = 1.0; |
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; |
[contactNode addAnimation:animation forKey:@"filterAnimation"]; |
} |
@end |
#pragma mark - Core Image filter |
@implementation ASCGlowFilter |
@synthesize inputRadius; |
- (NSArray *)attributeKeys { |
return @[@"inputRadius"]; |
} |
- (CIImage *)outputImage { |
CIImage *inputImage = [self valueForKey:@"inputImage"]; |
if (!inputImage) |
return nil; |
// Monochrome |
CIFilter *monochromeFilter = [CIFilter filterWithName:@"CIColorMatrix"]; |
[monochromeFilter setDefaults]; |
[monochromeFilter setValue:[CIVector vectorWithX:0 Y:0 Z:0 W:0] forKey:@"inputRVector"]; |
[monochromeFilter setValue:[CIVector vectorWithX:0 Y:0 Z:0 W:0.4] forKey:@"inputGVector"]; |
[monochromeFilter setValue:[CIVector vectorWithX:0 Y:0 Z:0 W:1] forKey:@"inputBVector"]; |
[monochromeFilter setValue:[CIVector vectorWithX:0 Y:0 Z:0 W:1] forKey:@"inputAVector"]; |
[monochromeFilter setValue:inputImage forKey:@"inputImage"]; |
CIImage *glowImage = [monochromeFilter valueForKey:@"outputImage"]; |
// Scale |
float centerX = [self.centerX floatValue]; |
float centerY = [self.centerY floatValue]; |
if (centerX > 0) { |
NSAffineTransform *transform = [NSAffineTransform transform]; |
[transform translateXBy:centerX yBy:centerY]; |
[transform scaleBy:1.2]; |
[transform translateXBy:-centerX yBy:-centerY]; |
CIFilter *affineTransformFilter = [CIFilter filterWithName:@"CIAffineTransform"]; |
[affineTransformFilter setDefaults]; |
[affineTransformFilter setValue:transform forKey:@"inputTransform"]; |
[affineTransformFilter setValue:glowImage forKey:@"inputImage"]; |
glowImage = [affineTransformFilter valueForKey:@"outputImage"]; |
} |
// Blur |
CIFilter *gaussianBlurFilter = [CIFilter filterWithName:@"CIGaussianBlur"]; |
[gaussianBlurFilter setDefaults]; |
[gaussianBlurFilter setValue:glowImage forKey:@"inputImage"]; |
[gaussianBlurFilter setValue:self.inputRadius ?: @10.0 forKey:@"inputRadius"]; |
glowImage = [gaussianBlurFilter valueForKey:@"outputImage"]; |
// Blend |
CIFilter *blendFilter = [CIFilter filterWithName:@"CISourceOverCompositing"]; |
[blendFilter setDefaults]; |
[blendFilter setValue:glowImage forKey:@"inputBackgroundImage"]; |
[blendFilter setValue:inputImage forKey:@"inputImage"]; |
glowImage = [blendFilter valueForKey:@"outputImage"]; |
return glowImage; |
} |
@end |
Copyright © 2014 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2014-01-07