Scene Kit Session WWDC 2013/Sources/ASCPresentationViewController.m
/* |
File: ASCPresentationViewController.m |
Abstract: ASCPresentationViewController controls the presentation, including ordering the slides in and out, updating the position of the camera, the light intensites and more. |
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 "ASCPresentationViewController.h" |
#import "ASCSlide.h" |
#import "ASCSlideTextManager.h" |
#import "Utils.h" |
typedef NS_ENUM(NSUInteger, ASCLightName) { |
ASCLightMain = 0, |
ASCLightFront, |
ASCLightSpot, |
ASCLightLeft, |
ASCLightRight, |
ASCLightAmbient, |
ASCLightCount |
}; |
@implementation ASCPresentationViewController { |
// Keeping track of the current slide |
NSInteger _currentSlideIndex; |
NSInteger _currentSlideStep; |
// The scene used for this presentation |
SCNScene *_scene; |
// Light nodes |
SCNNode *_lights[ASCLightCount]; |
// Other useful nodes |
SCNNode *_cameraNode; |
SCNNode *_cameraPitch; |
SCNNode *_cameraHandle; |
// Managing the floor |
SCNFloor *_floor; |
NSImage *_floorImage; |
// Presentation settings and slides |
NSDictionary *_settings; |
NSMutableDictionary *_slideCache; |
// Managing the "New" badge |
SCNNode *_newBadgeNode; |
CAAnimation *_newBadgeAnimation; |
} |
#pragma mark - View controller |
- (SCNView *)view { |
return (SCNView *)[super view]; |
} |
- (id)initWithContentsOfFile:(NSString *)path { |
if ((self = [super initWithNibName:nil bundle:nil])) { |
// Load the presentation settings from the plist file |
NSString *settingsPath = [[NSBundle mainBundle] pathForResource:path ofType:@"plist"]; |
_settings = [NSDictionary dictionaryWithContentsOfFile:settingsPath]; |
_slideCache = [[NSMutableDictionary alloc] init]; |
// Create a new empty scene |
_scene = [SCNScene scene]; |
// Create and add a camera to the scene |
// We create three separate nodes to ease the manipulation of the global position, pitch (ie. orientation around the x axis) and relative position |
// - cameraHandle is used to control the global position in world space |
// - cameraPitch is used to rotate the position around the x axis |
// - cameraNode is sometimes manipulated by slides to move the camera relatively to the global position (cameraHandle). But this node is supposed to always be repositioned at (0, 0, 0) in the end of a slide. |
_cameraHandle = [SCNNode node]; |
_cameraHandle.name = @"cameraHandle"; |
[_scene.rootNode addChildNode:_cameraHandle]; |
_cameraPitch = [SCNNode node]; |
_cameraPitch.name = @"cameraPitch"; |
[_cameraHandle addChildNode:_cameraPitch]; |
_cameraNode = [SCNNode node]; |
_cameraNode.name = @"cameraNode"; |
_cameraNode.camera = [SCNCamera camera]; |
// Set the default field of view to 70 degrees (a relatively strong perspective) |
_cameraNode.camera.xFov = 70.0; |
_cameraNode.camera.yFov = 42.0; |
[_cameraPitch addChildNode:_cameraNode]; |
// Setup the different lights |
[self initLighting]; |
// Create and add a reflective floor to the scene |
SCNMaterial *floorMaterial = [SCNMaterial material]; |
floorMaterial.ambient.contents = [NSColor blackColor]; |
floorMaterial.diffuse.contents = @"/Library/Desktop Pictures/Circles.jpg"; |
floorMaterial.diffuse.contentsTransform = CATransform3DScale(CATransform3DMakeRotation(M_PI / 4, 0, 0, 1), 2.0, 2.0, 1.0); |
floorMaterial.specular.wrapS = |
floorMaterial.specular.wrapT = |
floorMaterial.diffuse.wrapS = |
floorMaterial.diffuse.wrapT = SCNWrapModeMirror; |
_floor = [SCNFloor floor]; |
_floor.reflectionFalloffEnd = 3.0; |
_floor.firstMaterial = floorMaterial; |
SCNNode *floorNode = [SCNNode node]; |
floorNode.geometry = _floor; |
[_scene.rootNode addChildNode:floorNode]; |
// Use a shader modifier to support a secondary texture for some slides |
NSString *shaderFile = [[NSBundle mainBundle] pathForResource:@"floor" ofType:@"shader"]; |
NSString *shaderSource = [NSString stringWithContentsOfFile:shaderFile encoding:NSUTF8StringEncoding error:nil]; |
floorMaterial.shaderModifiers = @{ SCNShaderModifierEntryPointSurface : shaderSource }; |
// Set the scene to the view |
self.view = [[SCNView alloc] init]; |
self.view.scene = _scene; |
self.view.backgroundColor = [NSColor blackColor]; |
// Turn on jittering for better anti-aliasing when the scene is still |
self.view.jitteringEnabled = YES; |
// Start the presentation |
[self goToSlideAtIndex:0]; |
} |
return self; |
} |
#pragma mark - Presentation outline |
- (NSInteger)numberOfSlides { |
return [_settings[@"Slides"] count]; |
} |
- (Class)classOfSlideAtIndex:(NSInteger)slideIndex { |
NSDictionary *info = _settings[@"Slides"][slideIndex]; |
NSString *className = info[@"Class"]; |
return NSClassFromString(className); |
} |
#pragma mark - Slide creation and warm up |
// This method creates and initializes the slide at the specified index and returns it. |
// The new slide is cached in the _slides array. |
- (ASCSlide *)slideAtIndex:(NSInteger)slideIndex loadIfNeeded:(BOOL)loadIfNeeded { |
if (slideIndex < 0 || slideIndex >= [_settings[@"Slides"] count]) |
return nil; |
// Look into the cache first |
ASCSlide *slide = _slideCache[@(slideIndex)]; |
if (slide) { |
return slide; |
} |
if (!loadIfNeeded) |
return nil; |
// Create the new slide |
Class slideClass = [self classOfSlideAtIndex:slideIndex]; |
slide = [[slideClass alloc] init]; |
// Update its parameters |
NSDictionary *info = _settings[@"Slides"][slideIndex]; |
NSDictionary *parameters = info[@"Parameters"]; |
[parameters enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { |
[slide setValue:obj forKey:key]; |
}]; |
_slideCache[@(slideIndex)] = slide; |
if (!slide) |
return nil; |
// Setup the slide |
[slide setupSlideWithPresentationViewController:self]; |
return slide; |
} |
// Preload the next slide |
- (void)prepareSlideAtIndex:(NSInteger)slideIndex { |
// Retrieve the slide to preload |
ASCSlide *slide = [self slideAtIndex:slideIndex loadIfNeeded:YES]; |
if (slide) { |
[SCNTransaction flush]; // make sure that all pending transactions are flushed otherwise objects not added yet to the scene graph would not be preloaded |
// Preload the node tree |
[self.view prepareObject:slide.contentNode shouldAbortBlock:nil]; |
// Preload the floor image if any |
if ([slide.floorImageName length]) { |
NSImage *image = [[NSBundle mainBundle] imageForResource:slide.floorImageName]; |
// Create a container for this image to be able to preload it |
SCNMaterial *material = [SCNMaterial material]; |
material.diffuse.contents = image; |
material.diffuse.mipFilter = SCNFilterModeLinear; // we also want to preload mipmaps |
[SCNTransaction flush]; //make this material ready before warming up |
// Preload |
[self.view prepareObject:material shouldAbortBlock:nil]; |
// Don't release the material now, otherwise we will loose what we just preloaded |
slide.floorWarmupMaterial = material; |
} |
} |
} |
#pragma mark - Navigating within a presentation |
- (void)goToNextSlideStep { |
ASCSlide *slide = [self slideAtIndex:_currentSlideIndex loadIfNeeded:NO]; |
if (_currentSlideStep + 1 >= [slide numberOfSteps]) { |
[self goToSlideAtIndex:_currentSlideIndex + 1]; |
} else { |
[self goToSlideStep:_currentSlideStep + 1]; |
} |
} |
- (void)goToPreviousSlide { |
[self goToSlideAtIndex:_currentSlideIndex - 1]; |
} |
- (void)goToSlideAtIndex:(NSInteger)slideIndex { |
NSUInteger oldIndex = _currentSlideIndex; |
// Load the slide at the specified index |
ASCSlide *slide = [self slideAtIndex:slideIndex loadIfNeeded:YES]; |
if (!slide) |
return; |
// Compute the playback direction (did the user select next or previous?) |
float direction = slideIndex >= _currentSlideIndex ? 1 : -1; |
// Update badge |
self.showsNewInSceneKitBadge = [slide isNewIn10_9]; |
// If we are playing backward, we need to use the slide we come from to play the correct transition (backward) |
NSInteger transitionSlideIndex = direction == 1 ? slideIndex : _currentSlideIndex; |
ASCSlide *transitionSlide = [self slideAtIndex:transitionSlideIndex loadIfNeeded:YES]; |
// Make sure that the next operations are synchronized by using a transaction |
[SCNTransaction begin]; |
[SCNTransaction setAnimationDuration:0]; |
{ |
SCNNode *rootNode = slide.contentNode; |
SCNNode *textContainer = slide.textManager.textNode; |
SCNVector3 offset = SCNVector3Make(transitionSlide.transitionOffsetX, 0.0, transitionSlide.transitionOffsetZ); |
offset.x *= direction; |
offset.z *= direction; |
// Rotate offset based on current yaw |
double cosa = cos(-_cameraHandle.rotation.w); |
double sina = sin(-_cameraHandle.rotation.w); |
double tmpX = offset.x * cosa - offset.z * sina; |
offset.z = offset.x * sina + offset.z * cosa; |
offset.x = tmpX; |
// If we don't move, fade in |
if (offset.x == 0 && offset.y == 0 && offset.z == 0 && transitionSlide.transitionRotation == 0) { |
rootNode.opacity = 0; |
} |
// Don't animate the first slide |
BOOL shouldAnimate = !(slideIndex == 0 && _currentSlideIndex == 0); |
// Update current slide index |
_currentSlideIndex = slideIndex; |
// Go to step 0 |
[self goToSlideStep:0]; |
// Add the slide to the scene graph |
[self.view.scene.rootNode addChildNode:rootNode]; |
// Fade in, update paramters and notify on completion |
[SCNTransaction begin]; |
[SCNTransaction setAnimationDuration:shouldAnimate ? slide.transitionDuration : 0]; |
[SCNTransaction setCompletionBlock:^{ |
[self didOrderInSlideAtIndex:slideIndex]; |
}]; |
{ |
rootNode.opacity = 1; |
_cameraHandle.position = SCNVector3Make(_cameraHandle.position.x + offset.x, slide.altitude, _cameraHandle.position.z + offset.z); |
_cameraHandle.rotation = SCNVector4Make(0, 1, 0, _cameraHandle.rotation.w + transitionSlide.transitionRotation * M_PI / 180.0 * direction); |
_cameraPitch.rotation = SCNVector4Make(1, 0, 0, slide.pitch * M_PI / 180.0); |
[self updateLightingForSlideAtIndex:slideIndex]; |
_floor.reflectivity = slide.floorReflectivity; |
_floor.reflectionFalloffEnd = slide.floorFalloff; |
} |
[SCNTransaction commit]; |
// Compute the position of the text (in world space, relative to the camera) |
CATransform3D textWorldTransform = CATransform3DConcat(CATransform3DMakeTranslation(0, -3.3, -28), _cameraNode.worldTransform); |
// Place the rest of the slide |
rootNode.transform = textWorldTransform; |
rootNode.position = SCNVector3Make(rootNode.position.x, 0, rootNode.position.z); // clear altitude |
rootNode.rotation = SCNVector4Make(0, 1, 0, _cameraHandle.rotation.w); // use same rotation as the camera to simplify the placement of the elements in slides |
// Place the text |
CATransform3D textTransform = [textContainer.parentNode convertTransform:textWorldTransform fromNode:nil]; |
textContainer.transform = textTransform; |
// Place the ground node |
SCNVector3 localPosition = SCNVector3Make(0, 0, 0); |
SCNVector3 worldPosition = [slide.groundNode.parentNode convertPosition:localPosition toNode:nil]; |
worldPosition.y = 0; // make it touch the ground |
localPosition = [slide.groundNode.parentNode convertPosition:worldPosition fromNode:nil]; |
slide.groundNode.position = localPosition; |
// Update the floor image if needed |
NSImage *floorImage = [[NSBundle mainBundle] imageForResource:slide.floorImageName]; |
[self updateFloorImage:floorImage forSlide:slide]; |
} |
[SCNTransaction commit]; |
// Preload the next slide after some delay |
double delayInSeconds = 1.5; |
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)); |
dispatch_after(popTime, dispatch_get_main_queue(), ^(void) { |
[self prepareSlideAtIndex:slideIndex + 1]; |
}); |
// Order out previous slide if any |
if (oldIndex != _currentSlideIndex) |
[self willOrderOutSlideAtIndex:oldIndex]; |
} |
- (void)goToSlideStep:(NSInteger)index { |
_currentSlideStep = index; |
ASCSlide *slide = [self slideAtIndex:_currentSlideIndex loadIfNeeded:YES]; |
if (!slide) |
return; |
if ([self.delegate respondsToSelector:@selector(presentationViewController:willPresentSlideAtIndex:step:)]) { |
[self.delegate presentationViewController:self willPresentSlideAtIndex:_currentSlideIndex step:_currentSlideStep]; |
} |
[slide presentStepIndex:_currentSlideStep withPresentionViewController:self]; |
} |
- (void)didOrderInSlideAtIndex:(NSInteger)slideIndex { |
ASCSlide *slide = [self slideAtIndex:slideIndex loadIfNeeded:NO]; |
[slide didOrderInWithPresentionViewController:self]; |
} |
- (void)willOrderOutSlideAtIndex:(NSInteger)slideIndex { |
ASCSlide *slide = [self slideAtIndex:slideIndex loadIfNeeded:NO]; |
if (slide) { |
SCNNode *node = slide.contentNode; |
// Fade out and remove on completion |
[SCNTransaction begin]; |
[SCNTransaction setAnimationDuration:0.75]; |
[SCNTransaction setCompletionBlock:^{ |
[node removeFromParentNode]; |
}]; |
{ |
node.opacity = 0.0; |
} |
[SCNTransaction commit]; |
[slide willOrderOutWithPresentionViewController:self]; |
[_slideCache removeObjectForKey:@(slideIndex)]; |
} |
} |
#pragma mark - Scene decorations |
- (void)setShowsNewInSceneKitBadge:(BOOL)showsBadge { |
_showsNewInSceneKitBadge = showsBadge; |
if (_newBadgeNode && showsBadge) |
return; // already visible |
if (!_newBadgeNode && !showsBadge) |
return; // already invisible |
// Load the model and the animation |
if (!_newBadgeNode) { |
_newBadgeNode = [SCNNode node]; |
SCNNode *badgeNode = [_newBadgeNode asc_addChildNodeNamed:@"newBadge" fromSceneNamed:@"newBadge" withScale:1]; |
_newBadgeNode.scale = SCNVector3Make(0.03, 0.03, 0.03); |
_newBadgeNode.opacity = 0; |
_newBadgeNode.position = SCNVector3Make(50, 20, -10); |
_newBadgeNode.rotation = SCNVector4Make(1, 0, 0, -M_PI_2); |
SCNNode *imageNode = [_newBadgeNode childNodeWithName:@"badgeImage" recursively:YES]; |
imageNode.geometry.firstMaterial.emission.intensity = 0.0; |
[self.cameraPitch addChildNode:_newBadgeNode]; |
_newBadgeAnimation = [badgeNode animationForKey:badgeNode.animationKeys[0]]; |
[badgeNode removeAllAnimations]; |
_newBadgeAnimation.speed = 1.5; |
_newBadgeAnimation.fillMode = kCAFillModeBoth; |
_newBadgeAnimation.usesSceneTimeBase = NO; |
_newBadgeAnimation.removedOnCompletion = NO; |
} |
// Play |
if (showsBadge) { |
[_newBadgeNode addAnimation:_newBadgeAnimation forKey:nil]; |
[SCNTransaction begin]; |
[SCNTransaction setAnimationDuration:2]; |
{ |
_newBadgeNode.position = SCNVector3Make(14, 8, -20); |
[SCNTransaction setCompletionBlock:^{ |
[SCNTransaction begin]; |
[SCNTransaction setAnimationDuration:3]; |
{ |
SCNNode *ropeNode = [_newBadgeNode childNodeWithName:@"rope02" recursively:YES]; |
ropeNode.opacity = 0.0; |
} |
[SCNTransaction commit]; |
}]; |
_newBadgeNode.opacity = 1.0; |
SCNNode *imageNode = [_newBadgeNode childNodeWithName:@"badgeImage" recursively:YES]; |
imageNode.geometry.firstMaterial.emission.intensity = 0.4; |
} |
[SCNTransaction commit]; |
} |
// Or hide |
else { |
[SCNTransaction begin]; |
[SCNTransaction setAnimationDuration:1.5]; |
{ |
[SCNTransaction setCompletionBlock:^{ |
[_newBadgeNode removeFromParentNode]; |
_newBadgeNode = nil; |
}]; |
_newBadgeNode.position = SCNVector3Make(14, 50, -20); |
_newBadgeNode.opacity = 0.0; |
} |
[SCNTransaction commit]; |
} |
} |
#pragma mark - Lighting the scene |
- (void)initLighting { |
// Omni light (main light of the scene) |
_lights[ASCLightMain] = [SCNNode node]; |
_lights[ASCLightMain].name = @"omni"; |
_lights[ASCLightMain].position = SCNVector3Make(0, 3, -13); |
_lights[ASCLightMain].light = [SCNLight light]; |
_lights[ASCLightMain].light.type = SCNLightTypeOmni; |
[_lights[ASCLightMain].light setAttribute:@10 forKey:SCNLightAttenuationStartKey]; |
[_lights[ASCLightMain].light setAttribute:@50 forKey:SCNLightAttenuationEndKey]; |
[_cameraHandle addChildNode:_lights[ASCLightMain]]; //make all lights relative to the camera node |
// Front light |
_lights[ASCLightFront] = [SCNNode node]; |
_lights[ASCLightFront].name = @"front light"; |
_lights[ASCLightFront].position = SCNVector3Make(0, 0, 0); |
_lights[ASCLightFront].light = [SCNLight light]; |
_lights[ASCLightFront].light.type = SCNLightTypeDirectional; |
[_cameraHandle addChildNode:_lights[ASCLightFront]]; |
// Spot light |
_lights[ASCLightSpot] = [SCNNode node]; |
_lights[ASCLightSpot].name = @"spot light"; |
_lights[ASCLightSpot].position = SCNVector3Make(0, 30, -19); |
_lights[ASCLightSpot].rotation = SCNVector4Make(1, 0, 0, -M_PI_2); |
_lights[ASCLightSpot].light = [SCNLight light]; |
_lights[ASCLightSpot].light.type = SCNLightTypeSpot; |
_lights[ASCLightSpot].light.shadowRadius = 10; |
[_lights[ASCLightSpot].light setAttribute:@30 forKey:SCNLightShadowNearClippingKey]; |
[_lights[ASCLightSpot].light setAttribute:@50 forKey:SCNLightShadowFarClippingKey]; |
[_lights[ASCLightSpot].light setAttribute:@10 forKey:SCNLightSpotInnerAngleKey]; |
[_lights[ASCLightSpot].light setAttribute:@45 forKey:SCNLightSpotOuterAngleKey]; |
[_cameraHandle addChildNode:_lights[ASCLightSpot]]; |
// Left light |
_lights[ASCLightLeft] = [SCNNode node]; |
_lights[ASCLightLeft].name = @"left light"; |
_lights[ASCLightLeft].position = SCNVector3Make(-20, 10, -5); |
_lights[ASCLightLeft].light = [SCNLight light]; |
_lights[ASCLightLeft].light.type = SCNLightTypeOmni; |
[_lights[ASCLightLeft].light setAttribute:@30 forKey:SCNLightAttenuationStartKey]; |
[_lights[ASCLightLeft].light setAttribute:@80 forKey:SCNLightAttenuationEndKey]; |
[_cameraHandle addChildNode:_lights[ASCLightLeft]]; |
// Right light |
_lights[ASCLightRight] = [SCNNode node]; |
_lights[ASCLightRight].name = @"right light"; |
_lights[ASCLightRight].position = SCNVector3Make(20, 10, -5); |
_lights[ASCLightRight].light = [SCNLight light]; |
_lights[ASCLightRight].light.type = SCNLightTypeOmni; |
[_lights[ASCLightRight].light setAttribute:@30 forKey:SCNLightAttenuationStartKey]; |
[_lights[ASCLightRight].light setAttribute:@80 forKey:SCNLightAttenuationEndKey]; |
[_cameraHandle addChildNode:_lights[ASCLightRight]]; |
// Ambient light |
_lights[ASCLightAmbient] = [SCNNode node]; |
_lights[ASCLightAmbient].name = @"ambient light"; |
_lights[ASCLightAmbient].light = [SCNLight light]; |
_lights[ASCLightAmbient].light.type = SCNLightTypeAmbient; |
[_scene.rootNode addChildNode:_lights[ASCLightAmbient]]; |
// Switch off all the lights |
for (NSInteger i = 0; i < ASCLightCount; i++) |
_lights[i].light.color = [NSColor blackColor]; |
} |
- (void)updateLightingForSlideAtIndex:(NSInteger)slideIndex { |
ASCSlide *slide = [self slideAtIndex:slideIndex loadIfNeeded:YES]; |
_lights[ASCLightMain].position = slide.mainLightPosition; |
[self updateLightingWithIntensities:slide.lightIntensities]; |
[self enableShadows:slide.enableShadows]; |
} |
- (void)updateLightingWithIntensities:(NSArray *)intensities { |
for (NSInteger i = 0; i < ASCLightCount; i++) { |
CGFloat intensity = [intensities count] > i ? [intensities[i] floatValue] : 0; |
_lights[i].light.color = [NSColor colorWithDeviceHue:_lightHueAtSlideIndex(i) |
saturation:_lightSaturationAtSlideIndex(i) |
brightness:intensity |
alpha:1]; |
} |
} |
- (void)enableShadows:(BOOL)castsShadows { |
_lights[ASCLightSpot].light.shadowColor = [NSColor colorWithDeviceWhite:0 alpha:castsShadows ? 0.75 : 0.0]; |
_lights[ASCLightSpot].light.castsShadow = castsShadows; |
} |
- (void)narrowSpotlight:(BOOL)narrow { |
if (narrow) { |
[_lights[ASCLightSpot].light setAttribute:@20 forKey:SCNLightSpotInnerAngleKey]; |
[_lights[ASCLightSpot].light setAttribute:@30 forKey:SCNLightSpotOuterAngleKey]; |
} else { |
[_lights[ASCLightSpot].light setAttribute:@10 forKey:SCNLightSpotInnerAngleKey]; |
[_lights[ASCLightSpot].light setAttribute:@45 forKey:SCNLightSpotOuterAngleKey]; |
} |
} |
- (void)riseMainLight:(BOOL)rise { |
if (rise) { |
[_lights[ASCLightMain].light setAttribute:@90 forKey:SCNLightAttenuationStartKey]; |
[_lights[ASCLightMain].light setAttribute:@250 forKey:SCNLightAttenuationEndKey]; |
_lights[ASCLightMain].position = SCNVector3Make(0, 10, -10); |
} else { |
[_lights[ASCLightMain].light setAttribute:@10 forKey:SCNLightAttenuationStartKey]; |
[_lights[ASCLightMain].light setAttribute:@50 forKey:SCNLightAttenuationEndKey]; |
_lights[ASCLightMain].position = SCNVector3Make(0, 3, -13); |
} |
} |
- (SCNNode *)spotLight { |
return _lights[ASCLightSpot]; |
} |
- (SCNNode *)mainLight { |
return _lights[ASCLightMain]; |
} |
#pragma mark - Updating the floor |
// Updates the secondary image of the floor if needed |
- (void)updateFloorImage:(NSImage *)image forSlide:(ASCSlide *)slide { |
// We don't want to animate if we replace the secondary image by a new one |
// Otherwise we want to translate the secondary image to the new location |
BOOL disableAction = NO; |
if (_floorImage != image) { |
_floorImage = image; |
disableAction = YES; |
if (image) { |
// Set a new material property with this image to the "floorMap" custom property of the floor |
SCNMaterialProperty *property = [SCNMaterialProperty materialPropertyWithContents:image]; |
property.wrapS = SCNWrapModeRepeat; |
property.wrapT = SCNWrapModeRepeat; |
property.mipFilter = SCNFilterModeLinear; |
[_floor.firstMaterial setValue:property forKey:@"floorMap"]; |
} |
} |
if (image) { |
SCNVector3 slidePosition = [slide.groundNode convertPosition:SCNVector3Make(0, 0, 10) toNode:nil]; |
if (disableAction) { |
[SCNTransaction begin]; |
[SCNTransaction setAnimationDuration:0]; |
{ |
[_floor.firstMaterial setValue:[NSValue valueWithSCNVector3:slidePosition] forKey:@"floorImageNamePosition"]; |
} |
[SCNTransaction commit]; |
} else { |
[_floor.firstMaterial setValue:[NSValue valueWithSCNVector3:slidePosition] forKey:@"floorImageNamePosition"]; |
} |
} |
} |
#pragma mark - Misc |
CGFloat _lightSaturationAtSlideIndex(int index) { |
if (index >= 4) return 0.1; // colored |
return 0; // black and white |
} |
CGFloat _lightHueAtSlideIndex(int index) { |
if (index == 4) return 0; // red |
if (index == 5) return 200/360.0; // blue |
return 0; // black and white |
} |
@end |
Copyright © 2014 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2014-01-07