Scene Kit Session WWDC 2014/Sources/Slides/AAPLSlideParticles.m
#import "AAPLPresentationViewController.h" |
#import "AAPLSlideTextManager.h" |
#import "AAPLSlide.h" |
#import "Utils.h" |
typedef enum { |
Step0, |
StepFire, |
StepFireScreen, |
// StepFireSubtract, |
StepLocal, |
StepGravity, |
StepCollider, |
StepFields, |
StepFieldsVortex, |
StepSubSystems, |
StepConfetti, |
StepEmitterCube, |
StepEmitterSphere, |
StepEmitterTorus, |
StepCount |
} ParticleSteps; |
@interface AAPLSlideParticles : AAPLSlide |
{ |
SCNNode *_hole; |
SCNNode *_hole2; |
SCNNode *_floorNode; |
SCNNode *_boxNode; |
SCNNode *_particleHolder; |
SCNNode *_fieldOwner; |
SCNNode *_vortexFieldOwner; |
SCNParticleSystem *_snow; |
SCNParticleSystem *_bokeh; |
} |
@end |
SCNVector3 SCNVector3CrossProduct(const SCNVector3 a, const SCNVector3 b){ |
// return a.yzx * b.zxy - a.zxy * b.yzx; |
SCNVector3 out; |
out.x = a.y*b.z - a.z*b.y; |
out.y = a.z*b.x - a.x*b.z; |
out.z = a.x*b.y - a.y*b.x; |
return out; |
} |
CGFloat SCNVector3Length(const SCNVector3 a){ |
return (CGFloat) (sqrt (a.x * a.x + a.y * a.y + a.z * a.z)); |
} |
SCNVector3 SCNVector3Normalize(const SCNVector3 a){ |
CGFloat invlen = 1.0 / SCNVector3Length(a); |
SCNVector3 out; |
out.x = a.x * invlen; |
out.y = a.y * invlen; |
out.z = a.z * invlen; |
return out; |
} |
@implementation AAPLSlideParticles |
- (void)setupSlideWithPresentationViewController:(AAPLPresentationViewController *)presentationViewController { |
// Set the slide's title and add some text |
self.textManager.title = @"Particles"; |
self.textManager.subtitle = @"SCNParticleSystem"; |
[self.textManager addBullet:@"Achieve a large number of effects" atLevel:0]; |
[self.textManager addBullet:@"3D particle editor built into Xcode" atLevel:0]; |
} |
/* |
-> emitter shape |
fire/reactor -> color ramp |
smoke -> local/global |
etincelles -> gravity + colliders |
rain -> subsystem |
snow -> fields |
bokeh (multiple images) |
xcode editor |
// explosions |
*/ |
- (NSUInteger) numberOfSteps |
{ |
return StepCount; |
} |
static inline SCNVector3 __cross(SCNVector3 a, SCNVector3 b) |
{ |
SCNVector3 c; |
c.x = a.y*b.z - a.z*b.y; |
c.y = a.z*b.x - a.x*b.z; |
c.z = a.x*b.y - a.y*b.x; |
return c; |
} |
- (void)presentStepIndex:(NSUInteger)index withPresentationViewController:(AAPLPresentationViewController *)presentationViewController |
{ |
#define HOLE_Z 10 |
switch(index) |
{ |
case StepFire: |
{ |
[self.textManager flipOutTextOfType:AAPLTextTypeBullet]; |
[self.textManager addEmptyLine]; |
[self.textManager addBullet:@"Particle image" atLevel:0]; |
[self.textManager addBullet:@"Color over life duration" atLevel:0]; |
[self.textManager addBullet:@"Size over life duration" atLevel:0]; |
[self.textManager addBullet:@"Several blend modes" atLevel:0]; |
[self.textManager flipInTextOfType:AAPLTextTypeBullet]; |
SCNNode *hole = [SCNNode node]; |
hole.geometry = [SCNTube tubeWithInnerRadius:1.7 outerRadius:1.9 height:1.5]; |
hole.position = SCNVector3Make(0, 0, HOLE_Z); |
hole.scale = SCNVector3Make(1, 0, 1); |
[self.groundNode addChildNode:hole]; |
[SCNTransaction begin]; |
[SCNTransaction setAnimationDuration:0.5]; |
hole.scale = SCNVector3Make(1,1,1); |
[SCNTransaction commit]; |
SCNParticleSystem *ps = [SCNParticleSystem particleSystemNamed:@"fire" inDirectory:nil]; |
[hole addParticleSystem:ps]; |
_hole = hole; |
} |
break; |
case StepFireScreen: |
{ |
SCNParticleSystem *ps = [[_hole particleSystems] firstObject]; |
ps.blendMode = SCNParticleBlendModeScreen; |
} break; |
/* |
case StepFireSubtract: |
{ |
NSColor *col = [NSColor colorWithCalibratedWhite:0.7 alpha:1.0]; |
[CATransaction begin]; |
[CATransaction setAnimationDuration:5.5]; |
presentationViewController.presentationView.backgroundColor = col; |
[CATransaction commit]; |
[SCNTransaction begin]; |
[SCNTransaction setAnimationDuration:5.5]; |
presentationViewController.presentationView.scene.fogColor = col; |
[SCNTransaction commit]; |
SCNParticleSystem *ps = [[_hole particleSystems] firstObject]; |
ps.blackPassEnabled = NO; |
ps.blendMode = SCNParticleBlendModeSubtract; |
} break; |
*/ |
case StepLocal: |
{ |
[self.textManager flipOutTextOfType:AAPLTextTypeBullet]; |
[self.textManager addBullet:@"Local or global" atLevel:0]; |
[self.textManager flipInTextOfType:AAPLTextTypeBullet]; |
[_hole removeAllParticleSystems]; |
_hole2 = [_hole clone]; |
_hole2.geometry = [_hole.geometry copy]; |
_hole2.position = SCNVector3Make(0, -2, HOLE_Z-4); |
[self.groundNode addChildNode:_hole2]; |
[SCNTransaction begin]; |
[SCNTransaction setAnimationDuration:0.5]; |
_hole2.position = SCNVector3Make(0, 0, HOLE_Z-4); |
[SCNTransaction commit]; |
SCNParticleSystem *ps = [SCNParticleSystem particleSystemNamed:@"smoke" inDirectory:nil]; |
ps.particleColorVariation = SCNVector4Make(0, 0, 0.5, 0); |
[_hole addParticleSystem:ps]; |
SCNParticleSystem *localPs = [ps copy]; |
localPs.particleImage = ps.particleImage; // FIXME: remove when <rdar://problem/16957114> ParticleSystems does not copy its image |
localPs.local = YES; |
[_hole2 addParticleSystem:localPs]; |
{ |
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"position"]; |
animation.fromValue = [NSValue valueWithSCNVector3:SCNVector3Make(7, 0, HOLE_Z)]; |
animation.toValue = [NSValue valueWithSCNVector3:SCNVector3Make(-7, 0, HOLE_Z)]; |
animation.beginTime = CACurrentMediaTime() + 0.75; |
animation.duration = 8; |
animation.autoreverses = YES; |
animation.repeatCount = MAXFLOAT; |
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; |
animation.timeOffset = animation.duration/2; |
[_hole addAnimation:animation forKey:@"animateHole"]; |
} |
{ |
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"position"]; |
animation.fromValue = [NSValue valueWithSCNVector3:SCNVector3Make(-7, 0, HOLE_Z-4)]; |
animation.toValue = [NSValue valueWithSCNVector3:SCNVector3Make(7, 0, HOLE_Z-4)]; |
animation.beginTime = CACurrentMediaTime() + 0.75; |
animation.duration = 8; |
animation.autoreverses = YES; |
animation.repeatCount = MAXFLOAT; |
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; |
animation.timeOffset = animation.duration/2; |
[_hole2 addAnimation:animation forKey:@"animateHole"]; |
} |
} |
break; |
case StepGravity: |
{ |
[self.textManager flipOutTextOfType:AAPLTextTypeBullet]; |
[self.textManager addBullet:@"Affected by gravity" atLevel:0]; |
[self.textManager flipInTextOfType:AAPLTextTypeBullet]; |
[_hole2 removeAllParticleSystems]; |
[_hole2 runAction:[SCNAction sequence:@[[SCNAction scaleTo:0 duration:0.5], [SCNAction removeFromParentNode]]]]; |
[_hole removeAllParticleSystems]; |
[_hole removeAnimationForKey:@"animateHole" fadeOutDuration:0.5]; |
[SCNTransaction begin]; |
[SCNTransaction setAnimationDuration:0.5]; |
SCNTube *tube = (SCNTube*) _hole.geometry; |
tube.innerRadius = 0.3; |
tube.outerRadius = 0.4; |
tube.height = 1.0; |
[SCNTransaction commit]; |
SCNParticleSystem *ps = [SCNParticleSystem particleSystemNamed:@"sparks" inDirectory:nil]; |
[_hole removeAllParticleSystems]; |
[_hole addParticleSystem:ps]; |
_floorNode = [presentationViewController.presentationView.scene.rootNode childNodesPassingTest:^BOOL(SCNNode *child, BOOL *stop) { |
return [child.geometry isKindOfClass:[SCNFloor class]]; |
}][0]; |
ps.colliderNodes = @[_floorNode]; |
break; |
} |
case StepCollider: |
{ |
[self.textManager flipOutTextOfType:AAPLTextTypeBullet]; |
[self.textManager addBullet:@"Affected by colliders" atLevel:0]; |
[self.textManager flipInTextOfType:AAPLTextTypeBullet]; |
SCNNode *boxNode = [SCNNode node]; |
boxNode.geometry = [SCNBox boxWithWidth:5 height:0.2 length:5 chamferRadius:0]; |
boxNode.position = SCNVector3Make(0, 7, HOLE_Z); |
boxNode.geometry.firstMaterial.emission.contents = [NSColor darkGrayColor]; |
[self.groundNode addChildNode:boxNode]; |
SCNParticleSystem*ps = [_hole particleSystems][0]; |
ps.colliderNodes = @[_floorNode, boxNode]; |
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"eulerAngles"]; |
animation.fromValue = [NSValue valueWithSCNVector3:SCNVector3Make(0, 0, M_PI_4*1.7)]; |
animation.toValue = [NSValue valueWithSCNVector3:SCNVector3Make(0, 0, -M_PI_4*1.7)]; |
animation.beginTime = CACurrentMediaTime() + 0.5; |
animation.duration = 2; |
animation.autoreverses = YES; |
animation.repeatCount = MAXFLOAT; |
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; |
animation.timeOffset = animation.duration/2; |
[boxNode addAnimation:animation forKey:@"animateHole"]; |
_boxNode = boxNode; |
} |
break; |
case StepFields: |
{ |
[_hole removeAllParticleSystems]; |
[_hole runAction:[SCNAction sequence:@[[SCNAction scaleTo:0 duration:0.75], [SCNAction removeFromParentNode]]]]; |
[_boxNode runAction:[SCNAction sequence:@[[SCNAction moveByX:0 y:15 z:0 duration:1.0], [SCNAction removeFromParentNode]]]]; |
SCNNode *particleHolder = [SCNNode node]; |
particleHolder.position = SCNVector3Make(0, 20, HOLE_Z); |
[self.groundNode addChildNode:particleHolder]; |
_particleHolder = particleHolder; |
[self.textManager flipOutTextOfType:AAPLTextTypeBullet]; |
[self.textManager addBullet:@"Affected by physics fields" atLevel:0]; |
[self.textManager flipInTextOfType:AAPLTextTypeBullet]; |
SCNParticleSystem *ps = [SCNParticleSystem particleSystemNamed:@"snow" inDirectory:nil]; |
ps.affectedByPhysicsFields = YES; |
[_particleHolder addParticleSystem:ps]; |
_snow = ps; |
//physics field |
SCNPhysicsField *field = [SCNPhysicsField turbulenceFieldWithSmoothness:50 animationSpeed:1]; |
field.halfExtent = SCNVector3Make(20, 20, 20); |
field.strength = 4.0; |
SCNNode *fieldOwner = [SCNNode node]; |
fieldOwner.position = SCNVector3Make(0, 5, HOLE_Z); |
[self.groundNode addChildNode:fieldOwner]; |
fieldOwner.physicsField = field; |
_fieldOwner = fieldOwner; |
ps.colliderNodes = @[_floorNode]; |
} |
break; |
case StepFieldsVortex: |
{ |
_vortexFieldOwner = [SCNNode node]; |
_vortexFieldOwner.position = SCNVector3Make(0, 5, HOLE_Z); |
[self.groundNode addChildNode:_vortexFieldOwner]; |
//tornado |
__block SCNVector3 _worldOrigin = SCNVector3Make(_fieldOwner.worldTransform.m41,_fieldOwner.worldTransform.m42,_fieldOwner.worldTransform.m43); |
__block SCNVector3 _worldAxis = (SCNVector3) {0,1,0}; |
#define VS 20.0 |
#define VW 10.0 |
SCNPhysicsField *vortex = [SCNPhysicsField customFieldWithEvaluationBlock:^SCNVector3(SCNVector3 position, SCNVector3 velocity, float mass, float charge, NSTimeInterval time) { |
SCNVector3 l; |
l.x = _worldOrigin.x - position.x; |
l.z = _worldOrigin.z - position.z; |
SCNVector3 t = __cross(_worldAxis, l); |
float d2 = (l.x*l.x + l.z*l.z); |
float vs = VS / sqrt(d2); |
float fy = 1.0 - (MIN(1.0,(position.y/ 15.0))); |
return SCNVector3Make(t.x * vs + l.x * VW * fy, 0, t.z * vs + l.z * VW * fy); |
}]; |
vortex.halfExtent = SCNVector3Make(100, 100, 100); |
_vortexFieldOwner.physicsField = vortex; |
} |
break; |
case StepSubSystems: |
{ |
[_fieldOwner removeFromParentNode]; |
[_particleHolder removeAllParticleSystems]; |
_snow.dampingFactor = -1; |
[self.textManager flipOutTextOfType:AAPLTextTypeBullet]; |
[self.textManager addBullet:@"Sub-particle system on collision" atLevel:0]; |
[self.textManager flipInTextOfType:AAPLTextTypeBullet]; |
SCNParticleSystem *ps = [SCNParticleSystem particleSystemNamed:@"rain" inDirectory:nil]; |
SCNParticleSystem *pss = [SCNParticleSystem particleSystemNamed:@"plok" inDirectory:nil]; |
pss.idleDuration = 0; |
pss.loops = NO; |
[ps setSystemSpawnedOnCollision:pss]; |
[_particleHolder addParticleSystem:ps]; |
ps.colliderNodes = @[_floorNode]; |
} |
break; |
case StepConfetti: |
{ |
[_particleHolder removeAllParticleSystems]; |
[self.textManager flipOutTextOfType:AAPLTextTypeBullet]; |
[self.textManager addBullet:@"Custom blocks" atLevel:0]; |
[self.textManager flipInTextOfType:AAPLTextTypeBullet]; |
{ |
SCNParticleSystem *ps = [SCNParticleSystem particleSystem]; |
ps.emitterShape = [SCNBox boxWithWidth:20 height:0 length:5 chamferRadius:0]; |
ps.birthRate = 100; |
ps.particleLifeSpan = 5; |
ps.particleLifeSpanVariation = 0; |
ps.spreadingAngle = 20; |
ps.particleSize = 0.25; |
ps.particleVelocity = 5; |
ps.particleVelocityVariation = 2; |
ps.birthDirection = SCNParticleBirthDirectionConstant; |
ps.emittingDirection = SCNVector3Make(0, -1, 0); |
ps.birthLocation = SCNParticleBirthLocationVolume; |
ps.particleImage = @"confetti.png"; |
ps.lightingEnabled = YES; |
ps.orientationMode = SCNParticleOrientationModeFree; |
ps.sortingMode = SCNParticleSortingModeDistance; |
ps.particleAngleVariation = 180; |
ps.particleAngularVelocity = 200; |
ps.particleAngularVelocityVariation = 400; |
ps.particleColor = [NSColor greenColor]; |
ps.particleColorVariation = SCNVector4Make(0.2, 0.1, 0.1, 0); |
ps.particleBounce = 0; |
ps.particleFriction = 0.6; |
ps.colliderNodes = @[_floorNode]; |
ps.blendMode = SCNParticleBlendModeAlpha; |
CAKeyframeAnimation *floatAnimation = [CAKeyframeAnimation animationWithKeyPath:nil]; |
floatAnimation.values = @[@1, @1, @0]; |
floatAnimation.keyTimes = @[@0, @0.9, @1]; |
floatAnimation.duration = 1.0; |
floatAnimation.additive = NO; |
ps.propertyControllers = @{ SCNParticlePropertyOpacity: [SCNParticlePropertyController controllerWithAnimation:floatAnimation] }; |
[ps handleEvent:SCNParticleEventBirth forProperties:@[SCNParticlePropertyColor] withBlock:^(void **data, size_t *dataStride, uint32_t *indices , NSInteger count) { |
for (NSInteger i = 0; i < count; ++i) { |
float *col = (float *)((char *)data[0] + dataStride[0] * i); |
if (rand() & 0x1) { // swith green for red |
col[0] = col[1]; |
col[1] = 0; |
} |
} |
}]; |
[ps handleEvent:SCNParticleEventCollision forProperties:@[SCNParticlePropertyAngle, SCNParticlePropertyRotationAxis, SCNParticlePropertyAngularVelocity, SCNParticlePropertyVelocity, SCNParticlePropertyContactNormal] withBlock:^(void **data, size_t *dataStride, uint32_t *indices , NSInteger count) { |
for (NSInteger i = 0; i < count; ++i) { |
// fix orientation |
float *angle = (float *)((char *)data[0] + dataStride[0] * indices[i]); |
float *axis = (float *)((char *)data[1] + dataStride[1] * indices[i]); |
float *colNrm = (float *)((char *)data[4] + dataStride[4] * indices[i]); |
SCNVector3 collisionNormal = {colNrm[0], colNrm[1], colNrm[2]}; |
SCNVector3 cp = SCNVector3CrossProduct(collisionNormal, SCNVector3Make(0, 0, 1)); |
CGFloat cpLen = SCNVector3Length(cp); |
angle[0] = asin(cpLen); |
axis[0] = cp.x / cpLen; |
axis[1] = cp.y / cpLen; |
axis[2] = cp.z / cpLen; |
// kill angular rotation |
float *angVel = (float *)((char *)data[2] + dataStride[2] * indices[i]); |
angVel[0] = 0; |
if (colNrm[1] > 0.4) { |
float *vel = (float *)((char *)data[3] + dataStride[3] * indices[i]); |
vel[0] = 0; |
vel[1] = 0; |
vel[2] = 0; |
} |
} |
}]; |
[_particleHolder addParticleSystem:ps]; |
_particleHolder.position = SCNVector3Make(0, 15, HOLE_Z); |
} |
} |
break; |
case StepEmitterCube: |
{ |
[_particleHolder removeAllParticleSystems]; |
[self.textManager flipOutTextOfType:AAPLTextTypeBullet]; |
[self.textManager addBullet:@"Emitter shape" atLevel:0]; |
[self.textManager flipInTextOfType:AAPLTextTypeBullet]; |
[_particleHolder removeFromParentNode]; |
SCNParticleSystem *ps = [SCNParticleSystem particleSystemNamed:@"emitters" inDirectory:nil]; |
ps.local = YES; |
[_particleHolder addParticleSystem:ps]; |
SCNNode *node = [SCNNode node]; |
node.position = SCNVector3Make(3, 6, HOLE_Z); |
[node runAction:[SCNAction repeatActionForever:[SCNAction rotateByAngle:M_PI * 2 aroundAxis:SCNVector3Make(0.3, 1, 0) duration:8]]]; |
[self.groundNode addChildNode:node]; |
_bokeh = ps; |
[node addParticleSystem:ps]; |
} |
break; |
case StepEmitterSphere: |
{ |
_bokeh.emitterShape = [SCNSphere sphereWithRadius:5]; |
} |
break; |
case StepEmitterTorus: |
{ |
_bokeh.emitterShape = [SCNTorus torusWithRingRadius:5 pipeRadius:1]; |
} |
break; |
} |
} |
- (void)willOrderOutWithPresentationViewController:(AAPLPresentationViewController *)presentationViewController { |
[presentationViewController.presentationView.scene removeAllParticleSystems]; |
} |
@end |
Copyright © 2014 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2014-10-16