SceneKitReel/AAPLGameViewController.m

/*
 Copyright (C) 2017 Apple Inc. All Rights Reserved.
 See LICENSE.txt for this sample’s licensing information
 
 Abstract:
 Game View Controller declaration.
*/
 
#import <GLKit/GLKit.h>
#import <SceneKit/SceneKit.h>
#import <SpriteKit/SpriteKit.h>
 
 
#import "AAPLGameViewController.h"
 
#define SLIDE_COUNT 10
 
#define TEXT_SCALE 0.75
#define TEXT_Z_SPACING 200
 
#define MAX_FIRE 25.0
#define MAX_SMOKE 20.0
 
// utility function
static CGFloat randFloat(CGFloat min, CGFloat max)
{
    return min + (max - min) * (CGFloat)rand() / RAND_MAX;
}
 
// SpriteKit overlays
@interface AAPLSpriteKitOverlayScene : SKScene
 
@property (readonly) SKNode *nextButton;
@property (readonly) SKNode *previousButton;
@property (readonly) SKNode *buttonGroup;
 
- (void)showLabel:(NSString *)label;
 
@end
 
@implementation AAPLGameViewController {
@private
    //steps of the demo
    NSUInteger _introductionStep;
    NSUInteger _step;
    
    //scene
    SCNScene *_scene;
 
    // save spot light transform
    SCNMatrix4 _originalSpotTransform;
 
    //references to nodes for manipulation
    SCNNode *_cameraHandle;
    SCNNode *_cameraOrientation;
    SCNNode *_cameraNode;
    SCNNode *_spotLightParentNode;
    SCNNode *_spotLightNode;
    SCNNode *_ambientLightNode;
    SCNNode *_floorNode;
    SCNNode *_sceneKitLogo;
    SCNNode *_mainWall;
    SCNNode *_invisibleWallForPhysicsSlide;
    
    //ship
    SCNNode *_shipNode;
    SCNNode *_shipPivot;
    SCNNode *_shipHandle;
    SCNNode *_introNodeGroup;
    
    //physics slide
    NSMutableArray *_boxes;
    
    //particles slide
    SCNNode *_fireTruck;
    SCNNode *_collider;
    SCNNode *_emitter;
    SCNNode *_fireContainer;
    SCNNode *_handle;
    SCNParticleSystem *_fire;
    SCNParticleSystem *_smoke;
    SCNParticleSystem *_plok;
    BOOL _hitFire;
    
    //physics fields slide
    SCNNode *_fieldEmitter;
    SCNNode *_fieldOwner;
    SCNNode *_interactiveField;
    
    //SpriteKit integration slide
    SCNNode *_torus;
    SCNNode *_splashNode;
    
    //shaders slide
    SCNNode *_shaderGroupNode;
    SCNNode *_shadedNode;
    int      _shaderStage;
    
    // shader modifiers
    NSString *_geomModifier;
    NSString *_surfModifier;
    NSString *_fragModifier;
    NSString *_lightModifier;
    
    //camera manipulation
    SCNVector3 _cameraBaseOrientation;
    CGPoint    _initialOffset, _lastOffset;
    SCNMatrix4 _cameraHandleTransforms[SLIDE_COUNT];
    SCNMatrix4 _cameraOrientationTransforms[SLIDE_COUNT];
    dispatch_source_t _timer;
 
    
    BOOL _preventNext;
}
 
#if TARGET_OS_IPHONE
- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    [self setup];
}
#else
- (void)awakeFromNib
{
    [self setup];
}
#endif
 
#pragma mark - Setup
 
- (void)setup
{
    SCNView *sceneView = (SCNView *)self.view;
    
    //redraw forever
    sceneView.playing = YES;
    sceneView.loops = YES;
    sceneView.showsStatistics = YES;
 
    sceneView.backgroundColor = [SKColor blackColor];
    
    //setup ivars
    _boxes = [NSMutableArray array];
    
    //setup the scene
    [self setupScene];
    
    //present it
    sceneView.scene = _scene;
    
    //tweak physics
    sceneView.scene.physicsWorld.speed = 2.0;
    
    //let's be the delegate of the SCNView
    sceneView.delegate = self;
    
    //initial point of view
    sceneView.pointOfView = _cameraNode;
    
    //setup overlays
    AAPLSpriteKitOverlayScene *overlay = [[AAPLSpriteKitOverlayScene alloc] initWithSize:sceneView.bounds.size];
    sceneView.overlaySKScene = overlay;
    
#if TARGET_OS_IPHONE
    NSMutableArray *gestureRecognizers = [NSMutableArray array];
    [gestureRecognizers addObjectsFromArray:sceneView.gestureRecognizers];
    
    // add a tap gesture recognizer
    UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)];
    
    // add a pan gesture recognizer
    UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePan:)];
    
    // add a double tap gesture recognizer
    UITapGestureRecognizer *doubleTapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleDoubleTap:)];
    doubleTapGesture.numberOfTapsRequired = 2;
    
    [tapGesture requireGestureRecognizerToFail:panGesture];
    
    [gestureRecognizers addObject:doubleTapGesture];
    [gestureRecognizers addObject:tapGesture];
    [gestureRecognizers addObject:panGesture];
    
    //register gesture recognizers
    sceneView.gestureRecognizers = gestureRecognizers;
#endif
    
    if (!_introductionStep)
        [overlay showLabel:@"Go!"];
}
 
- (void)setupScene
{
    _scene = [SCNScene scene];
    
    [self setupEnvironment];
    [self setupSceneElements];
    [self setupIntroEnvironment];
}
 
- (void) setupEnvironment
{
    // |_   cameraHandle
    //   |_   cameraOrientation
    //     |_   cameraNode
    
    //create a main camera
    _cameraNode = [SCNNode node];
    _cameraNode.position = SCNVector3Make(0, 0, 120);
    
    //create a node to manipulate the camera orientation
    _cameraHandle = [SCNNode node];
    _cameraHandle.position = SCNVector3Make(0, 60, 0);
    
    _cameraOrientation = [SCNNode node];
   
    [_scene.rootNode addChildNode:_cameraHandle];
    [_cameraHandle addChildNode:_cameraOrientation];
    [_cameraOrientation addChildNode:_cameraNode];
    
    _cameraNode.camera = [SCNCamera camera];
    _cameraNode.camera.zFar = 800;
#if TARGET_OS_IPHONE
    if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) {
        _cameraNode.camera.yFov = 55;
    }
    else
#endif    
    {
        _cameraNode.camera.xFov = 75;
    }
 
    _cameraHandleTransforms[0] = _cameraNode.transform;
    
    // add an ambient light
    _ambientLightNode = [SCNNode node];
    _ambientLightNode.light = [SCNLight light];
    
    _ambientLightNode.light.type = SCNLightTypeAmbient;
    _ambientLightNode.light.color = [SKColor colorWithWhite:0.3 alpha:1.0];
    
    [_scene.rootNode addChildNode:_ambientLightNode];
    
    
    //add a key light to the scene
    _spotLightParentNode = [SCNNode node];
    _spotLightParentNode.position = SCNVector3Make(0, 90, 20);
    
    _spotLightNode = [SCNNode node];
    _spotLightNode.rotation = SCNVector4Make(1,0,0,-M_PI_4);
    
    _spotLightNode.light = [SCNLight light];
    _spotLightNode.light.type = SCNLightTypeSpot;
    _spotLightNode.light.color = [SKColor colorWithWhite:1.0 alpha:1.0];
    _spotLightNode.light.castsShadow = YES;
    _spotLightNode.light.shadowColor = [SKColor colorWithWhite:0 alpha:0.5];
    _spotLightNode.light.zNear = 30;
    _spotLightNode.light.zFar = 800;
    _spotLightNode.light.shadowRadius = 1.0;
    _spotLightNode.light.spotInnerAngle = 15;
    _spotLightNode.light.spotOuterAngle = 70;
    
    [_cameraNode addChildNode:_spotLightParentNode];
    [_spotLightParentNode addChildNode:_spotLightNode];
    
    //save spotlight transform
    _originalSpotTransform = _spotLightNode.transform;
 
    //floor
    SCNFloor *floor = [SCNFloor floor];
    floor.reflectionFalloffEnd = 0;
    floor.reflectivity = 0;
    
    _floorNode = [SCNNode node];
    _floorNode.geometry = floor;
    _floorNode.geometry.firstMaterial.diffuse.contents = @"wood.png";
    _floorNode.geometry.firstMaterial.locksAmbientWithDiffuse = YES;
    _floorNode.geometry.firstMaterial.diffuse.wrapS = SCNWrapModeRepeat;
    _floorNode.geometry.firstMaterial.diffuse.wrapT = SCNWrapModeRepeat;
    _floorNode.geometry.firstMaterial.diffuse.mipFilter = SCNFilterModeNearest;
    _floorNode.geometry.firstMaterial.doubleSided = NO;
    
    _floorNode.physicsBody = [SCNPhysicsBody staticBody];
    _floorNode.physicsBody.restitution = 1.0;
    
    [_scene.rootNode addChildNode:_floorNode];
}
 
- (void)setupSceneElements
{
    // create the wall geometry
    SCNPlane *wallGeometry = [SCNPlane planeWithWidth:800 height:200];
    wallGeometry.firstMaterial.diffuse.contents = @"wallPaper.png";
    wallGeometry.firstMaterial.diffuse.contentsTransform = SCNMatrix4Mult(SCNMatrix4MakeScale(8, 2, 1), SCNMatrix4MakeRotation(M_PI_4, 0, 0, 1));
    wallGeometry.firstMaterial.diffuse.wrapS = SCNWrapModeRepeat;
    wallGeometry.firstMaterial.diffuse.wrapT = SCNWrapModeRepeat;
    wallGeometry.firstMaterial.doubleSided = NO;
    wallGeometry.firstMaterial.locksAmbientWithDiffuse = YES;
    
    SCNNode *wallWithBaseboardNode = [SCNNode nodeWithGeometry:wallGeometry];
    wallWithBaseboardNode.position = SCNVector3Make(200, 100, -20);
    wallWithBaseboardNode.physicsBody = [SCNPhysicsBody staticBody];
    wallWithBaseboardNode.physicsBody.restitution = 1.0;
    wallWithBaseboardNode.castsShadow = NO;
    
    SCNNode *baseboardNode = [SCNNode nodeWithGeometry:[SCNBox boxWithWidth:800 height:8 length:0.5 chamferRadius:0]];
    baseboardNode.geometry.firstMaterial.diffuse.contents = @"baseboard.jpg";
    baseboardNode.geometry.firstMaterial.diffuse.wrapS = SCNWrapModeRepeat;
    baseboardNode.geometry.firstMaterial.doubleSided = NO;
    baseboardNode.geometry.firstMaterial.locksAmbientWithDiffuse = YES;
    baseboardNode.position = SCNVector3Make(0, -wallWithBaseboardNode.position.y + 4, 0.5);
    baseboardNode.castsShadow = NO;
    baseboardNode.renderingOrder = -3; //render before others
 
    [wallWithBaseboardNode addChildNode:baseboardNode];
    
    //front walls
    _mainWall = wallWithBaseboardNode;
    [_scene.rootNode addChildNode:wallWithBaseboardNode];
    _mainWall.renderingOrder = -3; //render before others
 
    //back
    SCNNode *wallNode = [wallWithBaseboardNode clone];
    wallNode.opacity = 0;
    wallNode.physicsBody = [SCNPhysicsBody staticBody];
    wallNode.physicsBody.restitution = 1.0;
    wallNode.physicsBody.categoryBitMask = 1 << 2;
    wallNode.castsShadow = NO;
    wallNode.physicsBody.contactTestBitMask = ~0;
    
    wallNode.position = SCNVector3Make(0, 100, 0);
    wallNode.rotation = SCNVector4Make(0, 1, 0, M_PI);
    [_scene.rootNode addChildNode:wallNode];
    
    //left
    wallNode = [wallWithBaseboardNode clone];
    wallNode.position = SCNVector3Make(-120, 100, 40);
    wallNode.rotation = SCNVector4Make(0, 1, 0, M_PI_2);
    [_scene.rootNode addChildNode:wallNode];
    
    
    //right (an invisible wall to keep the bodies in the visible area when zooming in the Physics slide)
    wallNode = [wallNode clone];
    wallNode.opacity = 0;
    wallNode.position = SCNVector3Make(120, 100, 40);
    wallNode.rotation = SCNVector4Make(0, 1, 0, -M_PI_2);
    _invisibleWallForPhysicsSlide = wallNode;
    
    //right (the actual wall on the right)
    wallNode = [wallWithBaseboardNode clone];
    wallNode.physicsBody = nil;
    wallNode.position = SCNVector3Make(600, 100, 40);
    wallNode.rotation = SCNVector4Make(0, 1, 0, -M_PI_2);
    [_scene.rootNode addChildNode:wallNode];
    
    //top
    wallNode = [wallWithBaseboardNode copy];
    wallNode.geometry = [wallNode.geometry copy];
    wallNode.geometry.firstMaterial = [SCNMaterial material];
    wallNode.opacity = 1;
    wallNode.position = SCNVector3Make(200, 200, 0);
    wallNode.scale = SCNVector3Make(1, 10, 1);
    wallNode.rotation = SCNVector4Make(1, 0, 0, M_PI_2);
    [_scene.rootNode addChildNode:wallNode];
    
    _mainWall.hidden = YES; //hide at first (save some milliseconds)
}
 
- (void)setupIntroEnvironment
{
    _introductionStep = 1;
    
    // configure the lighting for the introduction (dark lighting)
    _ambientLightNode.light.color = [SKColor blackColor];
    _spotLightNode.light.color = [SKColor blackColor];
    _spotLightNode.position = SCNVector3Make(50, 90, -50);
    _spotLightNode.eulerAngles = SCNVector3Make(-M_PI_2*0.75, M_PI_4*0.5, 0);
    
    //put all texts under this node to remove all at once later
    _introNodeGroup = [SCNNode node];
    
    //Slide 1
#define LOGO_SIZE 70
#define TITLE_SIZE (TEXT_SCALE*0.45)
    SCNNode *sceneKitLogo = [SCNNode nodeWithGeometry:[SCNPlane planeWithWidth:LOGO_SIZE height:LOGO_SIZE]];
    sceneKitLogo.geometry.firstMaterial.doubleSided = YES;
    sceneKitLogo.geometry.firstMaterial.diffuse.contents = @"SceneKit.png";
    sceneKitLogo.geometry.firstMaterial.emission.contents = @"SceneKit.png";
    _sceneKitLogo = sceneKitLogo;
    
    _sceneKitLogo.renderingOrder = -1;
    _floorNode.renderingOrder = -2;
 
    [_introNodeGroup addChildNode:sceneKitLogo];
    sceneKitLogo.position = SCNVector3Make(200, LOGO_SIZE/2, 200);
 
    SCNVector3 position = SCNVector3Make(200, 0, 200);
    
    _cameraNode.position = SCNVector3Make(200, -20, position.z+150);
    _cameraNode.eulerAngles = SCNVector3Make(-M_PI_2*0.06, 0, 0);
    
    /* hierarchy
     shipHandle
     |_ shipXTranslate
     |_ shipPivot
     |_ ship */
    SCNScene *modelScene = [SCNScene sceneNamed:@"ship.dae" inDirectory:@"assets.scnassets/models" options:nil];
    _shipNode = [modelScene.rootNode childNodeWithName:@"Aircraft" recursively:YES];
 
    SCNNode*shipMesh = _shipNode.childNodes[0];
    // shipMesh.geometry.firstMaterial.fresnelExponent = 1.0;
    shipMesh.geometry.firstMaterial.emission.intensity = 0.5;
    shipMesh.renderingOrder = -3;
    
    _shipPivot = [SCNNode node];
    SCNNode *shipXTranslate = [SCNNode node];
    _shipHandle = [SCNNode node];
    
    _shipHandle.position =  SCNVector3Make(200 - 500, 0, position.z + 30);
    _shipNode.position = SCNVector3Make(50, 30, 0);
    
    [_shipPivot addChildNode:_shipNode];
    [shipXTranslate addChildNode:_shipPivot];
    [_shipHandle addChildNode:shipXTranslate];
    [_introNodeGroup addChildNode:_shipHandle];
    
    //animate ship
    [_shipNode removeAllActions];
    _shipNode.rotation = SCNVector4Make(0, 0, 1, M_PI_4*0.5);
    
    //make spotlight relative to the ship
    SCNVector3 newPosition = SCNVector3Make(50, 100, 0);
    SCNMatrix4 oldTransform = [_shipPivot convertTransform:SCNMatrix4Identity fromNode:_spotLightNode];
    
    [_spotLightNode removeFromParentNode];
    _spotLightNode.transform = oldTransform;
    [_shipPivot addChildNode:_spotLightNode];
 
    _spotLightNode.position = newPosition; // will animate implicitly
    _spotLightNode.eulerAngles = SCNVector3Make(-M_PI_2, 0, 0);
    _spotLightNode.light.spotOuterAngle = 120;
    
    _shipPivot.eulerAngles = SCNVector3Make(0, M_PI_2, 0);
    SCNAction *action = [SCNAction sequence:@[[SCNAction repeatActionForever:[SCNAction rotateByX:0 y:M_PI z:0 duration:2]]]];
    [_shipPivot runAction:action];
 
    CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"position.x"];
    animation.fromValue = @(-50);
    animation.toValue =  @(+50);
    animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
    animation.autoreverses = YES;
    animation.duration = 2;
    animation.repeatCount = MAXFLOAT;
    animation.timeOffset = -animation.duration*0.5;
    [shipXTranslate addAnimation:animation forKey:nil];
 
    SCNNode *emitter = [_shipNode childNodeWithName:@"emitter" recursively:YES];
    SCNParticleSystem *ps = [SCNParticleSystem particleSystemNamed:@"reactor.scnp" inDirectory:@"assets.scnassets/particles"];
    [emitter addParticleSystem:ps];
    _shipHandle.position = SCNVector3Make(_shipHandle.position.x, _shipHandle.position.y, _shipHandle.position.z-50);
 
    [_scene.rootNode addChildNode:_introNodeGroup];
    
    //wait, then fade in light
    [SCNTransaction begin];
    [SCNTransaction setAnimationDuration:1.0];
    [SCNTransaction setCompletionBlock:^{
        [SCNTransaction begin];
        [SCNTransaction setAnimationDuration:2.5];
        
        _shipHandle.position = SCNVector3Make(_shipHandle.position.x+500, _shipHandle.position.y, _shipHandle.position.z);
        
        _spotLightNode.light.color = [SKColor colorWithWhite:1 alpha:1];
        sceneKitLogo.geometry.firstMaterial.emission.intensity = 0.80;
 
        [SCNTransaction commit];
    }];
    
    _spotLightNode.light.color = [SKColor colorWithWhite:0.001 alpha:1];
    
    [SCNTransaction commit];
}
 
#pragma mark -
 
// the material to use for text
- (SCNMaterial *)textMaterial {
    static SCNMaterial *material = nil;
    if (!material) {
        material = [SCNMaterial material];
        material.specular.contents   = [SKColor colorWithWhite:0.6 alpha:1];
        material.reflective.contents = @"color_envmap.png";
        material.shininess           = 0.1;
    }
    return material;
}
 
// switch to the next introduction step
- (void) nextIntroductionStep
{
    _introductionStep++;
    
    //show wall
    _mainWall.hidden = NO;
    
    [SCNTransaction begin];
    [SCNTransaction setAnimationDuration:1.0];
    [SCNTransaction setCompletionBlock:^{
        
        if (_introductionStep == 0) {
            //We did finish introduction step
 
            [_shipHandle removeFromParentNode];
            _shipHandle = nil;
            _shipPivot = nil;
            _shipNode = nil;
            
            _floorNode.renderingOrder = 0;
 
            //We did finish the whole introduction
            [_introNodeGroup removeFromParentNode];
            _introNodeGroup = nil;
            [self next];
        }
    }];
    
    if (_introductionStep == 2) {
        _sceneKitLogo.renderingOrder = 0;
 
        //restore spot light config
        _spotLightNode.light.spotOuterAngle = 70;
        SCNMatrix4 oldTransform = [_spotLightParentNode convertTransform:SCNMatrix4Identity fromNode:_spotLightNode];
        [_spotLightNode removeFromParentNode];
        _spotLightNode.transform = oldTransform;
        
        [_spotLightParentNode addChildNode:_spotLightNode];
        
        _cameraNode.position = SCNVector3Make(_cameraNode.position.x, _cameraNode.position.y, _cameraNode.position.z-TEXT_Z_SPACING);
 
        _spotLightNode.transform = _originalSpotTransform;
        _ambientLightNode.light.color = [SKColor colorWithWhite:0.3 alpha:1.0];
        _cameraNode.position = SCNVector3Make(0, 0, 120);
        _cameraNode.eulerAngles = SCNVector3Make(0, 0, 0);
 
        _introductionStep = 0;//introduction is over
    }
    else {
        _cameraNode.position = SCNVector3Make(_cameraNode.position.x, _cameraNode.position.y, _cameraNode.position.z-TEXT_Z_SPACING);
    }
    
    [SCNTransaction commit];
}
 
//restore the default camera orientation and position
- (void)restoreCameraAngle
{
    //reset drag offset
    _initialOffset = CGPointMake(0, 0);
    _lastOffset = _initialOffset;
    
    //restore default camera
    [SCNTransaction begin];
    [SCNTransaction setAnimationDuration:0.5];
    [SCNTransaction setAnimationTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]];
    _cameraHandle.eulerAngles = SCNVector3Make(0, 0, 0);
    [SCNTransaction commit];
}
 
// tilt the camera based on an offset
- (void)tiltCameraWithOffset:(CGPoint) offset
{
    if (_introductionStep != 0)
        return;
    
    offset.x += _initialOffset.x;
    offset.y += _initialOffset.y;
    
    CGPoint tr;
    tr.x = offset.x - _lastOffset.x;
    tr.y = offset.y - _lastOffset.y;
    
    _lastOffset = offset;
    
    offset.x *= 0.1;
    offset.y *= 0.1;
    float rx = offset.y; //offset.y > 0 ? log(1 + offset.y * offset.y) : -log(1 + offset.y * offset.y);
    float ry = offset.x; //offset.x > 0 ? log(1 + offset.x * offset.x) : -log(1 + offset.x * offset.x);
 
    ry *= 0.05;
    rx *= 0.05;
    
#if TARGET_OS_IPHONE
    rx = -rx; //on iOS, invert rotation on the X axis
#endif
    
    if (rx > 0.5) {
        rx = 0.5;
        _initialOffset.y -=tr.y;
        _lastOffset.y -= tr.y;
    }
    if (rx < -M_PI_2) {
        rx = -M_PI_2;
        _initialOffset.y -=tr.y;
        _lastOffset.y -= tr.y;
    }
    
#define MAX_RY (M_PI_4*1.5)
    if (ry > MAX_RY) {
        ry = MAX_RY;
        _initialOffset.x -=tr.x;
        _lastOffset.x -= tr.x;
    }
    if (ry < -MAX_RY) {
        ry = -MAX_RY;
        _initialOffset.x -=tr.x;
        _lastOffset.x -= tr.x;
 
    }
 
    ry = -ry;
 
    _cameraHandle.eulerAngles = SCNVector3Make(rx, ry, 0);
}
 
#pragma mark -
#pragma mark UIKit configuration
 
#if TARGET_OS_IPHONE
- (BOOL)shouldAutorotate
{
    return YES;
}
 
- (UIInterfaceOrientationMask)supportedInterfaceOrientations
{
    if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) {
        return UIInterfaceOrientationMaskAllButUpsideDown;
    } else {
        return UIInterfaceOrientationMaskAll;
    }
}
 
- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Release any cached data, images, etc that aren't in use.
}
#endif
 
#pragma mark -
#pragma mark Physics
 
#define BOX_W 8
 
// return a new physically based box at the specified position
// sometimes generate a ball instead of a box for more variety
- (SCNNode *) boxAtPosition:(SCNVector3) position
{
    static NSMutableArray *boxes;
    static int count = 0;
    
    if (boxes == NULL) {
        boxes = [NSMutableArray arrayWithCapacity:4];
        
        SCNNode *box = [SCNNode node];
        box.geometry = [SCNBox boxWithWidth:BOX_W height:BOX_W length:BOX_W chamferRadius:0.1];
        box.geometry.firstMaterial.diffuse.contents = @"WoodCubeA.jpg";
        box.geometry.firstMaterial.diffuse.mipFilter = SCNFilterModeLinear;
        box.physicsBody = [SCNPhysicsBody dynamicBody];
        
        [boxes addObject:box];
        
        box = box.clone;
        box.geometry = box.geometry.copy;
        box.geometry.firstMaterial = [box.geometry.firstMaterial copy];
        box.geometry.firstMaterial.diffuse.contents = @"WoodCubeB.jpg";
        [boxes addObject:box];
        
        box = box.clone;
        box.geometry = box.geometry.copy;
        box.geometry.firstMaterial = [box.geometry.firstMaterial copy];
        box.geometry.firstMaterial.diffuse.contents = @"WoodCubeC.jpg";
        [boxes addObject:box];
        
        SCNNode *ball = [SCNNode node];
        SCNSphere *sphere = [SCNSphere sphereWithRadius:BOX_W * 0.75];
        ball.geometry = sphere;
        ball.geometry.firstMaterial.diffuse.wrapS = SCNWrapModeRepeat;
        ball.geometry.firstMaterial.diffuse.contents = @"ball.jpg";
        ball.geometry.firstMaterial.reflective.contents = @"envmap.jpg";
        ball.geometry.firstMaterial.fresnelExponent = 1.0;
        ball.physicsBody = [SCNPhysicsBody dynamicBody];
        ball.physicsBody.restitution = 0.9;
        [boxes addObject:ball];
    }
    
    count++;
    
    int index = count % 3;
    if (count == 1 || (count&7) == 7)
        index = 3;
    
    SCNNode *item = [boxes[index] clone];
    item.position = position;
    
    return item;
}
 
#define FACTOR 2.2
 
//apply an explosion force at the specified location to the specified nodes
//remove from the nodes from the scene graph is removeOnCompletion is set to yes
- (void) explosionAt:(SCNVector3) center receivers:(NSArray *)nodes removeOnCompletion:(BOOL) removeOnCompletion
{
    GLKVector3 c = SCNVector3ToGLKVector3(center);
    
    for(SCNNode *node in nodes) {
        GLKVector3 p = SCNVector3ToGLKVector3(node.presentationNode.position);
        
        c.y = removeOnCompletion ? -20 : -90;
        c.z = removeOnCompletion ? 0 : 50;
        GLKVector3 direction = GLKVector3Subtract(p, c);
        
        c.y = 0;
        c.z = 0;
        GLKVector3 dist = GLKVector3Subtract(p, c);
        
        float force = removeOnCompletion ? 2000 : 1000 * (1.0 + fabs(c.x) / 100.0);
        float distance = GLKVector3Length(dist);
        
        if (removeOnCompletion) {
            if (direction.x < 500.0 && direction.x > 0) direction.x += 500;
            if (direction.x > -500.0 && direction.x < 0) direction.x -= 500;
            node.physicsBody.collisionBitMask = 0x0;
        }
        
        //normalise
        direction = GLKVector3Normalize(direction);
        direction = GLKVector3MultiplyScalar(direction, FACTOR * force / MAX(20.0, distance));
        
        [node.physicsBody applyForce:SCNVector3FromGLKVector3(direction) atPosition:removeOnCompletion ? SCNVector3Zero : SCNVector3Make(randFloat(-0.2, 0.2), randFloat(-0.2, 0.2), randFloat(-0.2, 0.2)) impulse:YES];
        
        if (removeOnCompletion) {
            [node runAction:[SCNAction sequence:@[[SCNAction waitForDuration:1.0],[SCNAction fadeOutWithDuration:0.125], [SCNAction removeFromParentNode]]]];
        }
    }
}
 
// present physics slide
- (void) showPhysicsSlide
{
    NSUInteger count = 80;
    float spread = 6;
    
    SCNScene *scene =  ((SCNView*)self.view).scene;
    
    //tweak physics
    scene.physicsWorld.gravity = SCNVector3Make(0, -70, 0);
    
    //add invisible wall
    [scene.rootNode addChildNode:_invisibleWallForPhysicsSlide];
    
    // drop rigid bodies cubes
    uint64_t intervalTime = NSEC_PER_SEC * 10.0 / count;
    
    dispatch_queue_t queue = dispatch_get_main_queue();
    _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    dispatch_source_set_timer(_timer, dispatch_time(DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC), intervalTime, 0); // every ms
    
    __block NSInteger remainingCount = count;
    __block BOOL right = NO;
    
    dispatch_source_set_event_handler(_timer, ^{
        
        if (_step > 1) {
            dispatch_source_cancel(_timer);
            return;
        }
        
        [SCNTransaction begin];
        
        SCNVector3 pos = SCNVector3Make(right ? 100 : -100, 50, 0);
        
        SCNNode *box = [self boxAtPosition:pos];
        
        //add to scene
        [_scene.rootNode addChildNode:box];
        
        
        [box.physicsBody setVelocity:SCNVector3Make(FACTOR * (right ? -50 : 50), FACTOR * (30+randFloat(-spread, spread)), FACTOR * (randFloat(-spread, spread)))];
        [box.physicsBody setAngularVelocity:SCNVector4Make(randFloat(-1, 1),randFloat(-1, 1),randFloat(-1, 1),randFloat(-3, 3))];
        [SCNTransaction commit];
        
        [_boxes addObject:box];
        
        // ensure we stop firing
        if (--remainingCount < 0)
            dispatch_source_cancel(_timer);
        
        right = 1-right;
    });
    
    dispatch_resume(_timer);
}
 
//remove physics slide
- (void)orderOutPhysics
{
    //move physics out
    [self explosionAt:SCNVector3Make(0, 0, 0) receivers:_boxes removeOnCompletion:YES];
    [_boxes removeAllObjects];
    
    //add invisible wall
    SCNScene *scene = ((SCNView*)self.view).scene;
    [scene.rootNode addChildNode:_invisibleWallForPhysicsSlide];
}
 
#pragma mark - Particles
 
//present particle slide
- (void)showParticlesSlide
{
    //restore defaults
    ((SCNView*)self.view).scene.physicsWorld.gravity = SCNVector3Make(0, -9.8, 0);
    
    //add truck
    SCNScene *fireTruckScene = [SCNScene sceneNamed:@"firetruck.dae" inDirectory:@"assets.scnassets/models/" options:nil];
    SCNNode *fireTruck = [fireTruckScene.rootNode childNodeWithName:@"firetruck" recursively:YES];
    SCNNode *emitter = [fireTruck childNodeWithName:@"emitter" recursively:YES];
    _handle = [fireTruck childNodeWithName:@"handle" recursively:YES];
    
    fireTruck.position = SCNVector3Make(120, 10, 0);
    fireTruck.scale = SCNVector3Make(0.2, 0.2, 0.2);
    fireTruck.rotation = SCNVector4Make(0, 1, 0, M_PI_2);
    
    [_scene.rootNode addChildNode:fireTruck];
    
    //add fire container
    SCNScene *fireContainerScene = [SCNScene sceneNamed:@"bac.dae" inDirectory:@"assets.scnassets/models/" options:nil];
    _fireContainer = [fireContainerScene.rootNode childNodeWithName:@"box" recursively:YES];
    _fireContainer.scale = SCNVector3Make(0.5, 0.25, 0.25);
    [_scene.rootNode addChildNode:_fireContainer];
    
    //preload it to avoid frame drop
    [(SCNView*)self.view prepareObject:_scene shouldAbortBlock:nil];
    
    _fireTruck = fireTruck;
    
    //collider
    SCNNode *colliderNode = [SCNNode node];
    colliderNode.geometry = [SCNBox boxWithWidth:50 height:2 length:25 chamferRadius:0];
    colliderNode.geometry.firstMaterial.diffuse.contents = @"assets.scnassets/textures/train_wood.jpg";
    colliderNode.position = SCNVector3Make(60, 260, 5);
    [_scene.rootNode addChildNode:colliderNode];
    
    SCNAction *moveIn = [SCNAction moveByX:0 y:-215 z:0 duration:1.0];
    moveIn.timingMode = SCNActionTimingModeEaseOut;
    [colliderNode runAction:[SCNAction sequence:@[[SCNAction waitForDuration:2],moveIn]]];
    
    CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"eulerAngles"];
    animation.fromValue = [NSValue valueWithSCNVector3:SCNVector3Make(0, 0, 0)];
    animation.toValue = [NSValue valueWithSCNVector3:SCNVector3Make(0, 0, 2*M_PI)];
    animation.beginTime = CACurrentMediaTime() + 0.5;
    animation.duration = 2;
    animation.repeatCount = MAXFLOAT;
    [colliderNode addAnimation:animation forKey:nil];
    _collider = colliderNode;
    
    SCNParticleSystem *ps;
    
    //add fire
    SCNNode *fireHolder = [SCNNode node];
    _emitter = fireHolder;
    fireHolder.position = SCNVector3Make(0,0,0);
    ps = [SCNParticleSystem particleSystemNamed:@"fire.scnp" inDirectory:@"assets.scnassets/particles/"];
    _smoke = [SCNParticleSystem particleSystemNamed:@"smoke.scnp" inDirectory:@"assets.scnassets/particles/"];
    _smoke.birthRate = 0;
    [fireHolder addParticleSystem:ps];
    
    SCNNode *smokeEmitter = [SCNNode node];
    smokeEmitter.position = SCNVector3Make(0, 0, 0.5);
    [smokeEmitter addParticleSystem:_smoke];
    [fireHolder addChildNode:smokeEmitter];
    [_scene.rootNode addChildNode:fireHolder];
    
    _fire = ps;
    
    //add water
    ps = [SCNParticleSystem particleSystemNamed:@"sparks.scnp" inDirectory:@"assets.scnassets/particles/"];
    ps.birthRate = 0;
    ps.speedFactor = 3.0;
    ps.colliderNodes = @[_floorNode, colliderNode];
    [emitter addParticleSystem:ps];
    
    SCNAction *tr = [SCNAction moveBy:SCNVector3Make(60, 0, 0) duration:1];
    tr.timingMode = SCNActionTimingModeEaseInEaseOut;
    
    [_cameraHandle runAction:[SCNAction sequence:@[[SCNAction waitForDuration:2],tr,[SCNAction runBlock:^(SCNNode *node){
        ps.birthRate = 300;
    }]]]];
}
 
//remove particle slide
- (void)orderOutParticles
{
    //remove fire truck
    [_fireTruck removeFromParentNode];
    [_emitter removeFromParentNode];
    [_collider removeFromParentNode];
    [_fireContainer removeFromParentNode];
    _fireContainer = nil;
    _collider = nil;
    _emitter = nil;
    _fireTruck = nil;
}
 
#pragma mark -
#pragma mark PhysicsFields
 
- (void) moveEmitterTo:(CGPoint) p
{
    SCNView *scnView = (SCNView *) self.view;
    SCNVector3 pTmp = [scnView projectPoint:SCNVector3Make(0, 0, 50)];
    SCNVector3 p3d = [scnView unprojectPoint:SCNVector3Make(p.x, p.y, pTmp.z)];
    p3d.z = 50;
    p3d.y = MAX(p3d.y, 5);
    _fieldOwner.position = p3d;
    _fieldOwner.physicsField.strength = 10000.0;
}
 
 
//present physics field slide
- (void)showPhysicsFields
{
    CGFloat dz = 50;
    
    [SCNTransaction begin];
    [SCNTransaction setAnimationDuration:0.75];
    _spotLightNode.light.color = [SKColor colorWithWhite:0.5 alpha:1.0];
    _ambientLightNode.light.color = [SKColor blackColor];
    [SCNTransaction commit];
 
    //remove gravity for this slide
    _scene.physicsWorld.gravity = SCNVector3Zero;
    
    //move camera
    SCNAction *tr = [SCNAction moveBy:SCNVector3Make(0, 0, dz) duration:1];
    tr.timingMode = SCNActionTimingModeEaseInEaseOut;
    [_cameraHandle runAction:tr];
    
    //add particles
    _fieldEmitter = [SCNNode node];
    _fieldEmitter.position = SCNVector3Make(_cameraHandle.position.x, 5, dz);
    
    SCNParticleSystem *ps = [SCNParticleSystem particleSystemNamed:@"bubbles.scnp" inDirectory:@"assets.scnassets/particles/"];
    
    ps.particleColor = [SKColor colorWithRed:0.8 green:0. blue:0. alpha:1.0];
    ps.particleColorVariation = SCNVector4Make(0.3, 0.2, 0.3, 0.);
    ps.sortingMode = SCNParticleSortingModeDistance;
    ps.blendMode = SCNParticleBlendModeAlpha;
    NSArray *cubeMap = @[@"right.jpg", @"left.jpg", @"top.jpg", @"bottom.jpg", @"front.jpg", @"back.jpg"];
    ps.particleImage = cubeMap;
    ps.fresnelExponent = 2;
    ps.colliderNodes = @[_floorNode, _mainWall];
    
    ps.emitterShape = [SCNBox boxWithWidth:200 height:0 length:100 chamferRadius:0];
    
    [_fieldEmitter addParticleSystem:ps];
    [_scene.rootNode addChildNode:_fieldEmitter];
    
    //field
    _fieldOwner = [SCNNode node];
    _fieldOwner.position = SCNVector3Make(_cameraHandle.position.x, 50, dz+5);
    
    SCNPhysicsField *field = [SCNPhysicsField radialGravityField];
    field.halfExtent = SCNVector3Make(100, 100, 100);
    field.minimumDistance = 20.0;
    field.falloffExponent = 0;
    _fieldOwner.physicsField.strength = 0.0;
    _fieldOwner.physicsField = field;
    [_scene.rootNode addChildNode:_fieldOwner];
}
 
//remove physics field slide
- (void) orderOutPhysicsFields
{
    [SCNTransaction begin];
    [SCNTransaction setAnimationDuration:0.75];
    _spotLightNode.light.color = [SKColor colorWithWhite:1.0 alpha:1.0];
    _ambientLightNode.light.color = [SKColor colorWithWhite:0.3 alpha:1.0];
    [SCNTransaction commit];
    
    //move camera
    CGFloat dz = 50;
    [SCNTransaction begin];
    [SCNTransaction setAnimationDuration:0.75];
    _cameraHandle.position = SCNVector3Make(_cameraHandle.position.x, _cameraHandle.position.y, _cameraHandle.position.z - dz);
    [SCNTransaction commit];
    
    [_fieldEmitter removeFromParentNode];
    [_fieldOwner removeFromParentNode];
    _fieldEmitter = nil;
    _fieldOwner = nil;
}
 
#pragma mark -
#pragma mark SpriteKit
 
#define SPRITE_SIZE 256
 
// add a color "splash" at the specified location in the SKScene used as a material
- (void) addPaintAtLocation:(CGPoint) p color:(SKColor *) color
{
    SKScene *skScene = _torus.geometry.firstMaterial.diffuse.contents;
    
    if ([skScene isKindOfClass:[SKScene class]]) {
        //update the contents of skScene by adding a splash of "color" at p (normalized [0, 1])
        p.x *= SPRITE_SIZE;
        p.y *= SPRITE_SIZE;
        
        SKNode *node = [SKSpriteNode node];
        node.position = p;
        node.xScale = 0.33;
        
        SKSpriteNode *subNode = [SKSpriteNode spriteNodeWithImageNamed:@"splash.png"];
        subNode.zRotation = randFloat(0.0, 2.0 * M_PI);
        subNode.color = color;
        subNode.colorBlendFactor = 1;
        
        [node addChild:subNode];
        [skScene addChild:node];
        
        //remove color splash at some point
        [node runAction:[SKAction sequence:@[[SKAction waitForDuration:5], [SKAction removeFromParent]]]];
        
        if (p.x < 16) {
            node = [node copy];
            p.x = SPRITE_SIZE + p.x;
            node.position = p;
            [skScene addChild:node];
        }
        else if (p.x > SPRITE_SIZE-16) {
            node = [node copy];
            p.x = (p.x - SPRITE_SIZE);
            node.position = p;
            [skScene addChild:node];
        }
    }
}
 
// physics contact delegate
- (void)physicsWorld:(SCNPhysicsWorld *)world didBeginContact:(SCNPhysicsContact *)contact
{
    SCNNode *ball = nil;
    SCNNode *other = nil;
 
    if (contact.nodeA.physicsBody.type == SCNPhysicsBodyTypeDynamic) {
        ball = contact.nodeA;
        other = contact.nodeB;
    }
    else if(contact.nodeB.physicsBody.type == SCNPhysicsBodyTypeDynamic){
        ball = contact.nodeB;
        other = contact.nodeA;
    }
    
    if (ball) {
        SCNParticleSystem *plokCopy = [_plok copy];
        plokCopy.particleImage = _plok.particleImage; // to workaround an bug in seed #1
        plokCopy.particleColor = ball.geometry.firstMaterial.diffuse.contents;
        [_scene addParticleSystem:plokCopy withTransform:SCNMatrix4MakeTranslation(contact.contactPoint.x, contact.contactPoint.y, contact.contactPoint.z)];
        
        if (other != _torus) {
            SCNNode *node = [_splashNode clone];
            node.geometry = [node.geometry copy];
            node.geometry.firstMaterial = [node.geometry.firstMaterial copy];
            node.geometry.firstMaterial.diffuse.contents = plokCopy.particleColor;
            node.castsShadow = NO;
            //node.geometry.firstMaterial.readsFromDepthBuffer = NO;
            node.geometry.firstMaterial.writesToDepthBuffer = NO;
            
            static float eps = 1;
            eps += 0.0002;
            node.position = SCNVector3Make(contact.contactPoint.x, contact.contactPoint.y, _mainWall.position.z + eps);
            
            [node runAction:[SCNAction sequence:@[[SCNAction waitForDuration:6.],
                                                  [SCNAction fadeOutWithDuration:1.5],
                                                  [SCNAction removeFromParentNode]
                                                  ]]];
            [_scene.rootNode addChildNode:node];
            
        } else {
            //compute texture coordinate
            SCNView *scnview = (SCNView*)self.view;
            SCNVector3 pointA = SCNVector3Make(contact.contactPoint.x, contact.contactPoint.y, contact.contactPoint.z+20);
            SCNVector3 pointB = SCNVector3Make(contact.contactPoint.x, contact.contactPoint.y, contact.contactPoint.z-20);
            
            NSArray *results = [scnview.scene.rootNode hitTestWithSegmentFromPoint:pointA toPoint:pointB options:@{SCNHitTestRootNodeKey : _torus}];
            
            if ([results count]>0) {
                SCNHitTestResult *hit = results[0];
                [self addPaintAtLocation:[hit textureCoordinatesWithMappingChannel:0] color:plokCopy.particleColor];
                
            }
        }
        
        [ball removeFromParentNode];
    }
}
 
//present spritekit integration slide
- (void)showSpriteKitSlide
{
    //place camera
    [SCNTransaction begin];
    [SCNTransaction setAnimationDuration:2.0];
    _cameraHandle.position = SCNVector3Make(_cameraHandle.position.x+200, 60, 0);
    [SCNTransaction commit];
 
    
    //load plok particles
    _plok = [SCNParticleSystem particleSystemNamed:@"plok.scnp" inDirectory:@"assets.scnassets/particles"];
    
#define W 50
    
    //create a spinning object
    _torus = [SCNNode node];
    _torus.position = SCNVector3Make(_cameraHandle.position.x, 60, 10);
    _torus.geometry = [SCNTorus torusWithRingRadius:W/2 pipeRadius:W/6];
    _torus.physicsBody = [SCNPhysicsBody bodyWithType:SCNPhysicsBodyTypeStatic shape:[SCNPhysicsShape shapeWithGeometry:_torus.geometry options:@{SCNPhysicsShapeTypeKey : SCNPhysicsShapeTypeConcavePolyhedron}]];
    _torus.opacity = 0.0;
    
    // create a splash
    _splashNode = [SCNNode node];
    _splashNode.geometry = [SCNPlane planeWithWidth:10 height:10];
    _splashNode.geometry.firstMaterial.transparent.contents = @"splash.png";
 
    
    SCNMaterial *material = _torus.geometry.firstMaterial;
    material.specular.contents = [SKColor colorWithWhite:0.5 alpha:1];
    material.shininess = 2.0;
    
    material.normal.contents = @"wood-normal.png";
    
    [_scene.rootNode addChildNode:_torus];
    [_torus runAction:[SCNAction repeatActionForever:[SCNAction rotateByAngle:M_PI*2 aroundAxis:SCNVector3Make(0.4, 1, 0) duration:8]]];
    
    //preload it to avoid frame drop
    [(SCNView*)self.view prepareObject:_scene shouldAbortBlock:nil];
    
    _scene.physicsWorld.contactDelegate = self;
    
    //setup material
    SKScene *skScene = [SKScene sceneWithSize:CGSizeMake(SPRITE_SIZE, SPRITE_SIZE)];
    skScene.backgroundColor = [SKColor whiteColor];
    material.diffuse.contents = skScene;
    
    //tweak physics
    ((SCNView*)self.view).scene.physicsWorld.gravity = SCNVector3Make(0, -70, 0);
 
    // show the torus
    [SCNTransaction begin];
    [SCNTransaction setAnimationDuration:1.0];
    _torus.opacity = 1.0;
    [SCNTransaction commit];
}
 
- (void)launchColorBall
{
    SCNNode *ball = [SCNNode node];
    SCNSphere *sphere = [SCNSphere sphereWithRadius:2];
    ball.geometry = sphere;
    ball.geometry.firstMaterial.diffuse.contents = [SKColor colorWithHue:rand()/(float)RAND_MAX saturation:1 brightness:1 alpha:1];
    ball.geometry.firstMaterial.reflective.contents = @"envmap.jpg";
    ball.geometry.firstMaterial.fresnelExponent = 1.0;
    ball.physicsBody = [SCNPhysicsBody dynamicBody];
    ball.physicsBody.restitution = 0.9;
    ball.physicsBody.categoryBitMask = 0x4;
    ball.physicsBody.contactTestBitMask = ~0;
    ball.physicsBody.collisionBitMask = ~(0x4);
    ball.physicsBody.contactTestBitMask = 0xff;
    
    ball.position = SCNVector3Make(_cameraHandle.position.x, 20, 100);
    
    //add to scene
    [_scene.rootNode addChildNode:ball];
    
#define PAINT_FACTOR 2
    
    [ball.physicsBody setVelocity:SCNVector3Make(PAINT_FACTOR * randFloat(-10, 10),
                                                 (75+randFloat(0, 35)),
                                                 PAINT_FACTOR * -30.0)];
}
 
- (void)orderOutSpriteKit
{
    [_torus removeFromParentNode];
    _scene.physicsWorld.contactDelegate = nil;
}
 
#pragma mark - Shaders
 
- (void)showNextShaderStage
{
    _shaderStage++;
 
    //retrieve the node that owns the shader modifiers
    SCNNode *node = _shadedNode;
    
    switch(_shaderStage){
        case 1: // Geometry
            [SCNTransaction begin];
            [SCNTransaction setAnimationDuration:1.];
            node.geometry.shaderModifiers = @{SCNShaderModifierEntryPointGeometry : _geomModifier,
                                              SCNShaderModifierEntryPointLightingModel : _lightModifier };
 
            [node.geometry setValue:@3.0 forKey:@"Amplitude"];
            [node.geometry setValue:@0.25 forKey:@"Frequency"];
            [node.geometry setValue:@0.0 forKey:@"lightIntensity"];
            [SCNTransaction commit];
            break;
        case 2: // Surface
        {
            [SCNTransaction begin];
            [SCNTransaction setAnimationDuration:0.5];
            [node.geometry setValue:@0.0 forKey:@"Amplitude"];
            [SCNTransaction setCompletionBlock:^{
                [SCNTransaction begin];
                [SCNTransaction setAnimationDuration:1.5];
                node.geometry.shaderModifiers = @{SCNShaderModifierEntryPointSurface : _surfModifier,
                                                  SCNShaderModifierEntryPointLightingModel : _lightModifier };
                [node.geometry setValue:@1.0 forKey:@"surfIntensity"];
                [SCNTransaction commit];
            }];
            [SCNTransaction commit];
        } break;
        case 3: // Fragment
        {
            [SCNTransaction begin];
            [SCNTransaction setAnimationDuration:0.5];
 
            [node.geometry setValue:@0.0 forKey:@"surfIntensity"];
            [SCNTransaction setCompletionBlock:^{
                [SCNTransaction begin];
                [SCNTransaction setAnimationDuration:1.5];
                node.geometry.shaderModifiers = @{SCNShaderModifierEntryPointFragment : _fragModifier,
                                                  SCNShaderModifierEntryPointLightingModel : _lightModifier};
                [node.geometry setValue:@1.0 forKey:@"fragIntensity"];
                [node.geometry setValue:@1.0 forKey:@"lightIntensity"];
                [SCNTransaction commit];
            }];
            [SCNTransaction commit];
        }
 
            break;
        case 4: // None
            [SCNTransaction begin];
            [SCNTransaction setAnimationDuration:0.5];
            [node.geometry setValue:@0.0 forKey:@"fragIntensity"];
            [node.geometry setValue:@0.0 forKey:@"lightIntensity"];
            _shaderStage = 0;
            [SCNTransaction setCompletionBlock:^{
                node.geometry.shaderModifiers = nil;
            }];
            [SCNTransaction commit];
            break;
    }
}
 
- (void)showShadersSlide
{
    _shaderStage = 0;
    
    //move the camera back
    //place camera
    [SCNTransaction begin];
    [SCNTransaction setAnimationDuration:1.0];
    _cameraHandle.position = SCNVector3Make(_cameraHandle.position.x+180, 60, 0);
    _cameraHandle.eulerAngles = SCNVector3Make(-M_PI_4*0.3, 0, 0);
    
    _spotLightNode.light.spotOuterAngle = 55;
    [SCNTransaction commit];
    
    _shaderGroupNode = [SCNNode node];
    _shaderGroupNode.position = SCNVector3Make(_cameraHandle.position.x, -5, 20);
    [_scene.rootNode addChildNode:_shaderGroupNode];
    
    //add globe stand
    SCNNode *globe = [[[SCNScene sceneNamed:@"assets.scnassets/models/globe.dae"] rootNode] childNodeWithName:@"globe" recursively:YES];
    
    [_shaderGroupNode addChildNode:globe];
    
    //show shader modifiers
    //add spheres
    SCNSphere *sphere = [SCNSphere sphereWithRadius:28];
    sphere.segmentCount = 48;
    sphere.firstMaterial.diffuse.contents = @"earth-diffuse.jpg";
    sphere.firstMaterial.specular.contents = @"earth-specular.jpg";
    sphere.firstMaterial.specular.intensity = 0.2;
    
    sphere.firstMaterial.shininess = 0.1;
    sphere.firstMaterial.reflective.contents = @"envmap.jpg";
    sphere.firstMaterial.reflective.intensity = 0.5;
    sphere.firstMaterial.fresnelExponent = 2;
    
    //GEOMETRY
    SCNNode *node = [globe childNodeWithName:@"globeAttach" recursively:YES];
    node.geometry = sphere;
    node.scale = SCNVector3Make(3, 3, 3);
    
    [node runAction:[SCNAction repeatActionForever:[SCNAction rotateByX:0 y:M_PI z:0 duration:6.0]]];
    
    _geomModifier = [NSString stringWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"sm_geom" ofType:@"shader"] encoding:NSUTF8StringEncoding error:nil];
    _surfModifier = [NSString stringWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"sm_surf" ofType:@"shader"] encoding:NSUTF8StringEncoding error:nil];
    _fragModifier = [NSString stringWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"sm_frag" ofType:@"shader"] encoding:NSUTF8StringEncoding error:nil];
    _lightModifier= [NSString stringWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"sm_light" ofType:@"shader"] encoding:NSUTF8StringEncoding error:nil];
    
    [node.geometry setValue:@0.0 forKey:@"Amplitude"];
    [node.geometry setValue:@0.0 forKey:@"lightIntensity"];
    [node.geometry setValue:@0.0 forKey:@"surfIntensity"];
    [node.geometry setValue:@0.0 forKey:@"fragIntensity"];
    
    _shadedNode = node;
    
    //redraw forever
    ((SCNView*)self.view).playing = YES;
    ((SCNView*)self.view).loops = YES;
}
 
- (void)orderOutShaders
{
    [_shaderGroupNode runAction:[SCNAction sequence:@[[SCNAction scaleTo:0.01 duration:1.0], [SCNAction removeFromParentNode]]]];
    _shaderGroupNode = nil;
}
 
#pragma mark - Presentation logic
 
- (void)presentStep:(NSUInteger)step
{
    AAPLSpriteKitOverlayScene *overlay = (AAPLSpriteKitOverlayScene *)((SCNView*)self.view).overlaySKScene;
    
    if (_cameraHandleTransforms[step].m11 == 0) {
        _cameraHandleTransforms[step] = _cameraHandle.transform;
        _cameraOrientationTransforms[step] = _cameraOrientation.transform;
    }
    
    switch(step) {
        case 1:
        {
            [overlay showLabel:@"Physics"];
            [overlay runAction:[SKAction sequence:@[[SKAction waitForDuration:2], [SKAction runBlock:^{
                if (_step == 1)
                    [overlay showLabel:nil];
            }]]]];
            
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                [self showPhysicsSlide];
            });
        }
            break;
        case 2:
        {
            [overlay showLabel:@"Particles"];
            [overlay runAction:[SKAction sequence:@[[SKAction waitForDuration:4], [SKAction runBlock:^{
                if (_step == 2)
                    [overlay showLabel:nil];
            }]]]];
            
            [self showParticlesSlide];
            break;
        }
        case 3:
        {
            [overlay showLabel:@"Physics Fields"];
            [overlay runAction:[SKAction sequence:@[[SKAction waitForDuration:2], [SKAction runBlock:^{
                if (_step == 3)
                    [overlay showLabel:nil];
            }]]]];
            
            [self showPhysicsFields];
            break;
        }
        case 4:
        {
            [overlay showLabel:@"SceneKit + SpriteKit"];
            [overlay runAction:[SKAction sequence:@[[SKAction waitForDuration:4], [SKAction runBlock:^{
                if (_step == 4)
                    [overlay showLabel:nil];
            }]]]];
            
            [self showSpriteKitSlide];
            break;
        }
        case 5:
        {
            [overlay showLabel:@"SceneKit + Shaders"];
            [self showShadersSlide];
            break;
        }
    }
}
 
- (void)orderOutStep:(NSInteger)step
{
    switch(step) {
        case 1:
            [self orderOutPhysics];
            break;
        case 2:
            [self orderOutParticles];
            break;
        case 3:
            [self orderOutPhysicsFields];
            break;
        case 4:
            [self orderOutSpriteKit];
            break;
        case 5:
            [self orderOutShaders];
            break;
    }
}
 
- (void)next
{
    if (_step >= 5)
        return;
    
    [self orderOutStep:_step];
    _step++;
    [self presentStep:_step];
}
 
- (void)previous
{
    if (_step <= 1)
        return;
    
    [self orderOutStep:_step];
    _step--;
    
    [SCNTransaction begin];
    [SCNTransaction setAnimationDuration:0.75];
    [SCNTransaction setCompletionBlock:^{
        [self presentStep:_step];
    }];
    
    _cameraHandle.transform = _cameraHandleTransforms[_step];
    _cameraOrientation.transform = _cameraOrientationTransforms[_step];
    
    [SCNTransaction commit];
}
 
#pragma mark - Rendering Loop
 
- (void)renderer:(id <SCNSceneRenderer>)aRenderer updateAtTime:(NSTimeInterval)time
{
    if (_step == 2 && _hitFire) {
        float fire = _fire.birthRate;
        
        if (fire > 0) {
            fire -= 0.1;
            _smoke.birthRate = (1.0-(fire / MAX_FIRE)) * MAX_SMOKE;
            _fire.birthRate = MAX(0,fire);
        }
        else {
            float smoke = _smoke.birthRate ;
            if (smoke>0)
                smoke -= 0.03;
            
            _smoke.birthRate = MAX(0,smoke);
        }
    }
    
    
    if(_step == 4){ //launch color at some interval
        static NSTimeInterval lastTime = 0;
        
        if(time - lastTime > 0.1){
            lastTime = time;
            
            [self launchColorBall];
        }
    }
}
 
#pragma mark - Gestures
 
- (void)gestureDidEnd
{
     if (_step == 3) {
         //bubbles
         _fieldOwner.physicsField.strength = 0.0;
     }
}
 
- (void)gestureDidBegin
{
    _initialOffset = _lastOffset;
}
 
#if TARGET_OS_IPHONE
- (void)handleDoubleTap:(UIGestureRecognizer *)gestureRecognizer
{
    [self restoreCameraAngle];
}
 
- (void)handlePan:(UITapGestureRecognizer *)gestureRecognizer
{
    if (gestureRecognizer.state == UIGestureRecognizerStateEnded) {
        [self gestureDidEnd];
        return;
    }
    
    if (gestureRecognizer.state == UIGestureRecognizerStateBegan) {
        [self gestureDidBegin];
        return;
    }
    
    if (gestureRecognizer.numberOfTouches == 2) {
        [self tiltCameraWithOffset:[(UIPanGestureRecognizer *)gestureRecognizer translationInView:self.view]];
    }
    else {
        CGPoint p = [gestureRecognizer locationInView:self.view];
        [self handlePanAtPoint:p];
    }
}
 
- (void)handleTap:(UIGestureRecognizer *)gestureRecognizer
{
    CGPoint p = [gestureRecognizer locationInView:self.view];
    [self handleTapAtPoint:p];
}
#endif
 
- (void)handlePanAtPoint:(CGPoint) p
{
    SCNView *scnView = (SCNView *) self.view;
    
    if (_step == 2) {
        //particles
        SCNVector3 pTmp = [scnView projectPoint:SCNVector3Make(0, 0, 0)];
        SCNVector3 p3d = [scnView unprojectPoint:SCNVector3Make(p.x, p.y, pTmp.z)];
        SCNMatrix4 handlePos = _handle.worldTransform;
        
        
        float dy = MAX(0, p3d.y - handlePos.m42);
        float dx = handlePos.m41 - p3d.x;
        float angle = atan2f(dy, dx);
        
        
        angle -= 35.*M_PI/180.0; //handle is 35 degree by default
        
        //clamp
#define MIN_ANGLE -M_PI_2*0.1
#define MAX_ANGLE M_PI*0.8
        if (angle < MIN_ANGLE) angle = MIN_ANGLE;
        if (angle > MAX_ANGLE) angle = MAX_ANGLE;
        
        
#define HIT_DELAY 3.0
        
        if (angle <= 0.66 && angle >= 0.48) {
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(HIT_DELAY * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                //hit the fire!
                _hitFire = YES;
            });
        }
        else {
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(HIT_DELAY * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                //hit the fire!
                _hitFire = NO;
            });
        }
        
        _handle.rotation = SCNVector4Make(1, 0, 0, angle);
    }
    
    if (_step == 3) {
        //bubbles
        [self moveEmitterTo:p];
    }
}
 
- (void)handleDoubleTapAtPoint:(CGPoint)p
{
    [self restoreCameraAngle];
}
 
- (void) preventAccidentalNext:(CGFloat) delay
{
    _preventNext = YES;
    
    //disable the next button for "delay" seconds to prevent accidental tap
    AAPLSpriteKitOverlayScene *overlay = (AAPLSpriteKitOverlayScene *)((SCNView*)self.view).overlaySKScene;
    [overlay.nextButton runAction:[SKAction fadeAlphaBy:-0.5 duration:0.5]];
    [overlay.previousButton runAction:[SKAction fadeAlphaBy:-0.5 duration:0.5]];
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        _preventNext = NO;
        [overlay.previousButton runAction:[SKAction fadeAlphaTo:_step > 1 ? 1 : 0 duration:0.75]];
        [overlay.nextButton runAction:[SKAction fadeAlphaTo:_introductionStep == 0 && _step < 5 ? 1 : 0 duration:0.75]];
    });
}
 
- (void)handleTapAtPoint:(CGPoint)p
{
    //test buttons
    SKScene *skScene = ((SCNView*)self.view).overlaySKScene;
    CGPoint p2D = [skScene convertPointFromView:p];
    SKNode *node = [skScene nodeAtPoint:p2D];
    
    // wait X seconds before enabling the next tap to avoid accidental tap
    BOOL ignoreNext = _preventNext;
    
    if (_introductionStep) {
        //next introduction step
        if (!ignoreNext){
            [self preventAccidentalNext:1];
            [self nextIntroductionStep];
        }
        return;
    }
    
    if (ignoreNext == NO) {
        if (_step == 0 || [node.name isEqualToString:@"next"] || [node.name isEqualToString:@"back"]) {
            BOOL shouldGoBack = [node.name isEqualToString:@"back"];
 
            if ([node.name isEqualToString:@"next"]) {
                ((SKSpriteNode*)node).color = [SKColor colorWithRed:1 green:0 blue:0 alpha:1];
                [node runAction:[SKAction customActionWithDuration:0.7 actionBlock:^(SKNode *node, CGFloat elapsedTime) {
                    ((SKSpriteNode*)node).colorBlendFactor = 0.7 - elapsedTime;
                }]];
            }
            
            [self restoreCameraAngle];
            
            [self preventAccidentalNext:_step==1 ? 3 : 1];
            
            if (shouldGoBack)
                [self previous];
            else
                [self next];
            
            return;
        }
    }
    
    if (_step == 1) {
        //bounce physics!
        SCNView *scnView = (SCNView *) self.view;
        SCNVector3 pTmp = [scnView projectPoint:SCNVector3Make(0, 0, -60)];
        SCNVector3 p3d = [scnView unprojectPoint:SCNVector3Make(p.x, p.y, pTmp.z)];
        
        p3d.y = 0;
        p3d.z = 0;
        
        [self explosionAt:p3d receivers:_boxes removeOnCompletion:NO];
    }
    if (_step == 3) {
        //bubbles
        [self moveEmitterTo:p];
    }
    
    if (_step == 5) {
        //shader
        [self showNextShaderStage];
    }
}
 
@end
 
 
 
@implementation AAPLSpriteKitOverlayScene {
@private
    SKNode *_nextButton;
    SKNode *_previousButton;
    CGSize _size;
    SKLabelNode *_label;
}
 
- (instancetype)initWithSize:(CGSize)size
{
    if (self = [super initWithSize:size]) {
        _size = size;
        
        /* Setup your scene here */
        self.anchorPoint = CGPointMake(0.5, 0.5);
        self.scaleMode = SKSceneScaleModeResizeFill;
        
        //buttons
        _nextButton = [SKSpriteNode spriteNodeWithImageNamed:@"next.png"];
        
        float marginY = 60;
        float maringX = -60;
#if TARGET_OS_IPHONE
        if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) {
            marginY = 30;
            marginY = 30;
        }
#endif
        
        _nextButton.position = CGPointMake(size.width * 0.5 + maringX, -size.height * 0.5 + marginY);
        _nextButton.name = @"next";
        _nextButton.alpha = 0.01;
#if TARGET_OS_IPHONE
        if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) {
            _nextButton.xScale = _nextButton.yScale = 0.5;
        }
#endif
        [self addChild:_nextButton];
        
        _previousButton = [SKSpriteNode spriteNodeWithColor:[SKColor clearColor] size:_nextButton.frame.size];
        _previousButton.position = CGPointMake(-(size.width * 0.5 + maringX), -size.height * 0.5 + marginY);
        _previousButton.name = @"back";
        _previousButton.alpha = 0.01;
        [self addChild:_previousButton];
    }
    return self;
}
 
- (void)showLabel:(NSString *)label
{
    if (!_label) {
        _label = [SKLabelNode labelNodeWithFontNamed:@"Myriad Set"];
        if(!_label)
            _label = [SKLabelNode labelNodeWithFontNamed:@"Avenir-Heavy"];
        _label.fontSize = 140;
        _label.position = CGPointMake(0,0);
        
        [self addChild:_label];
    }
    else {
        if (label)
            _label.position = CGPointMake(0, _size.height * 0.25);
    }
    
    if (!label) {
        [_label runAction:[SKAction fadeOutWithDuration:0.5]];
    }
    else {
#if TARGET_OS_IPHONE
        if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) {
            _label.fontSize = [label length] > 10 ? 50 : 80;
        }
        else
#endif
        {
            _label.fontSize = [label length] > 10 ? 100 : 140;
        }
        
        _label.text = label;
        _label.alpha = 0.0;
        [_label runAction:[SKAction sequence:@[[SKAction waitForDuration:0.5], [SKAction fadeInWithDuration:0.5]]]];
    }
}
 
@end