Trailing transparent snapshots are 3D model clones of the jet at variable intervals.
Use SCNView allowsCameraControl to zoom and rotate the entire scene, including models, for different points of view.
* The 3D Jet ship mesh is part of the default Xcode Scene Kit project.
@import SceneKit; @import GLKit; @interface ESJet: NSObject @property (nonatomic) SCNNode * node; @property (nonatomic, assign) CGFloat velocity; @property (nonatomic, assign) BOOL roll; @property (nonatomic, assign) CGFloat rollRate; -(void)update:(NSTimeInterval)aTime isVisible:(BOOL)aVisible; -(instancetype)init; -(instancetype)initWithNode:(SCNNode *)aNode zPosition:(int)aZPosition; @end @implementation ESJet -(void)update:(NSTimeInterval)aTime isVisible:(BOOL)aVisible; { CGFloat nextZPosition = self.velocity; // NSLog(@"jet position: %@", NSStringFromGLKVector3(SCNVector3ToGLKVector3(self.node.position))); if (aVisible) { NSLog(@"jet visible"); } else { //when jet flys off right-side of screen, reposition on left NSLog(@"..."); nextZPosition = -(self.node.position.x * 1.95); } GLKVector3 nextFramePosition = GLKVector3Make(0, 0, nextZPosition); GLKMatrix4 transform = SCNMatrix4ToGLKMatrix4(self.node.transform); self.node.transform = SCNMatrix4FromGLKMatrix4(GLKMatrix4TranslateWithVector3(transform, nextFramePosition)); if (self.roll) { //rotate jet ~ 1 degree per frame on X axis GLKQuaternion rot = GLKQuaternionMakeWithAngleAndAxis((M_1_PI * self.rollRate), 1, 0, 0); GLKQuaternion orientation = GLKQuaternionMultiply(rot, GLKQuaternionMake(self.node.orientation.x, self.node.orientation.y, self.node.orientation.z, self.node.orientation.w)); self.node.orientation = SCNVector4Make(orientation.x, orientation.y, orientation.z, orientation.w); } } -(instancetype)init { if (self = [super init]) { self.velocity = 0.33f; self.rollRate = (self.velocity * 0.364); } return self; } -(instancetype)initWithNode:(SCNNode *)aNode zPosition:(int)aZPosition { if ([self init]) { self.node = aNode; [self.node removeAllActions]; //rotate jet 90 degrees on Y axis, facing right edge of screen GLKQuaternion rot = GLKQuaternionMakeWithAngleAndAxis(M_PI_2, 0, 1, 0); self.node.orientation = SCNVector4Make(rot.x, rot.y, rot.z, rot.w); self.node.position = SCNVector3Make(0, 0, aZPosition); } return self; } @end @interface GameViewController: UIViewController<SCNSceneRendererDelegate> @end @implementation GameViewController { SCNScene * skScene; SCNView * skView; SCNNode * cameraNode; NSMutableArray * nodeSnapshots; NSDate * frameTimer; ESJet * jet; } -(void)viewDidLoad { //default project code here... skScene = scene; skView = scnView; skView.delegate = self; skView.playing = YES; nodeSnapshots = [NSMutableArray array]; jet = [[ESJet alloc] initWithNode:ship zPosition:-20]; jet.roll = YES; } //SCNSceneRendererDelegate -(void)renderer:(id<SCNSceneRenderer>)aRenderer updateAtTime:(NSTimeInterval)time { void (^cloneNode)() = ^ { dispatch_queue_t q = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0); dispatch_async(q, ^ { SCNNode * clone = [jet.node clone]; clone.opacity = 0.20f; [nodeSnapshots addObject:clone]; [skScene.rootNode addChildNode:clone]; }); }; void (^cloneNodeOnTimer)(float) = ^(float interval) { if ((frameTimer == nil) || (frameTimer.timeIntervalSinceNow <= -interval)) { frameTimer = [NSDate date]; cloneNode(); } }; //SCNSceneRenderer protocol BOOL nodeIsVisible = [aRenderer isNodeInsideFrustum:jet.node withPointOfView:cameraNode]; if (nodeIsVisible) { cloneNodeOnTimer(0.6f); } else { skView.playing = NO; jet.node.hidden = YES; if (nodeSnapshots.count > 0) { //remove transparency from last cloned node ((SCNNode *)[nodeSnapshots lastObject]).opacity = 1.0; } } [jet update:time isVisible:nodeIsVisible]; } //tap any node to render new scene -(void)handleTap:(UIGestureRecognizer*)gestureRecognize { CGPoint tp = [gestureRecognize locationInView:skView]; NSArray * hitTestResults = [skView hitTest:tp options:nil]; if (hitTestResults.count > 0) { [nodeSnapshots enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL * stop) { [(SCNNode *)obj removeFromParentNode]; }]; [nodeSnapshots removeAllObjects]; jet.node.hidden = NO; skView.playing = YES; } }