├── MetalParticles ├── hamburger.png ├── MarkerWidget.swift ├── Base.lproj │ ├── LaunchScreen.xib │ └── Main.storyboard ├── Images.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Info.plist ├── AppDelegate.swift ├── ViewController.swift ├── Particles.metal └── ParticleLab.swift ├── MetalParticles.xcodeproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── project.pbxproj ├── MetalParticlesTests ├── Info.plist └── MetalParticlesTests.swift └── README.md /MetalParticles/hamburger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlexMonkey/ParticleLab/HEAD/MetalParticles/hamburger.png -------------------------------------------------------------------------------- /MetalParticles.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /MetalParticles/MarkerWidget.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MarkerWidget.swift 3 | // MetalParticles 4 | // 5 | // Created by Simon Gladman on 17/01/2015. 6 | // Copyright (c) 2015 Simon Gladman. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | class MarkerWidget: UIView 13 | { 14 | override func didMoveToSuperview() 15 | { 16 | let cirlce = Circle() 17 | cirlce.draw() 18 | 19 | layer.addSublayer(cirlce) 20 | } 21 | } 22 | 23 | class Circle: CAShapeLayer 24 | { 25 | func draw() 26 | { 27 | fillColor = UIColor.lightGrayColor().CGColor 28 | 29 | let ballRect = CGRect(x: -10, y: -10, width: 20, height: 20) 30 | let ballPath = UIBezierPath(ovalInRect: ballRect) 31 | 32 | path = ballPath.CGPath 33 | } 34 | } -------------------------------------------------------------------------------- /MetalParticlesTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /MetalParticlesTests/MetalParticlesTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MetalParticlesTests.swift 3 | // MetalParticlesTests 4 | // 5 | // Created by Simon Gladman on 17/01/2015. 6 | // Copyright (c) 2015 Simon Gladman. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import XCTest 11 | 12 | class MetalParticlesTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testExample() { 25 | // This is an example of a functional test case. 26 | XCTAssert(true, "Pass") 27 | } 28 | 29 | func testPerformanceExample() { 30 | // This is an example of a performance test case. 31 | self.measureBlock() { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /MetalParticles/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /MetalParticles/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "ipad", 35 | "size" : "29x29", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "ipad", 40 | "size" : "29x29", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "40x40", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "40x40", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "76x76", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "76x76", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /MetalParticles/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /MetalParticles/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UIRequiresFullScreen 34 | 35 | UIStatusBarHidden 36 | 37 | UISupportedInterfaceOrientations 38 | 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UISupportedInterfaceOrientations~ipad 43 | 44 | UIInterfaceOrientationPortrait 45 | UIInterfaceOrientationPortraitUpsideDown 46 | UIInterfaceOrientationLandscapeLeft 47 | UIInterfaceOrientationLandscapeRight 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /MetalParticles/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // MetalParticles 4 | // 5 | // Created by Simon Gladman on 17/01/2015. 6 | // Copyright (c) 2015 Simon Gladman. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(application: UIApplication) { 33 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ParticleLab - High Performance Particles in Swift and Metal 2 | Particle system that's both calculated and rendered on the GPU using the Metal framework 3 | 4 | ![http://flexmonkey.co.uk/swift/IMG_0699.PNG](http://flexmonkey.co.uk/swift/IMG_0699.PNG) 5 | 6 | This is the most highly optimised version of my Swift and Metal particles system; managing over 40 fps with four million particles and four gravity wells. It manages this by rendering to a MetalKit MTKView rather than converting a texture to a UIImage and by passing in four particle definitions per step with a float4x4 rather than a particle struct. 7 | 8 | You can read about these recent changes at my blog: 9 | 10 | * A First Look at Metal for OS X: http://flexmonkey.blogspot.co.uk/2015/06/a-first-look-at-metal-for-os-x-el.html 11 | * CAMetalLayer work: http://flexmonkey.blogspot.co.uk/2015/03/swift-metal-four-million-particles-on.html 12 | * Use of float4x4: http://flexmonkey.blogspot.co.uk/2015/03/mind-blowing-metal-four-million.html 13 | 14 | This branch wraps up all the Metal code into one class so that it's easily implemented in other projects. To create a new particle system object, instantiate an instance of _ParticleLab_ specifying the dimensions and total number of particles (half, one, two or four million): 15 | 16 | ``` 17 | particleLab = ParticleLab(width: 1024, height: 768, numParticles: ParticleCount.TwoMillion) 18 | ``` 19 | 20 | ...and when ready, add it as a sublayer to your view: 21 | 22 | ``` 23 | view.addView(particleLab) 24 | ``` 25 | 26 | The class has four gravity wells with propeties such as position, mass and spin. These are set with the _setGravityWellProperties_ method: 27 | 28 | ``` 29 | particleLab.setGravityWellProperties(gravityWell: .One, normalisedPositionX: 0.3, normalisedPositionY: 0.3, mass: 11, spin: -4) 30 | 31 | particleLab.setGravityWellProperties(gravityWell: .Two, normalisedPositionX: 0.7, normalisedPositionY: 0.3, mass: 7, spin: 3) 32 | 33 | particleLab.setGravityWellProperties(gravityWell: .Three, normalisedPositionX: 0.3, normalisedPositionY: 0.7, mass: 7, spin: 3) 34 | 35 | particleLab.setGravityWellProperties(gravityWell: .Four, normalisedPositionX: 0.7, normalisedPositionY: 0.7, mass: 11, spin: -4) 36 | ``` 37 | 38 | Classes can implement ```ParticleLabDelegate``` interface which includes ```particleLabDidUpdate```. This method is invoked with each particle step and can be used, for example, for updating the position of gravity wells. 39 | 40 | # ParticleLab Features in Detail 41 | 42 | ## Setting Gravity Well Properties 43 | 44 | ParticleLab supports up to four gravity wells that have properties for position, mass and spin. These properties are set through the ```setGravityWellProperties()``` method that either accepts a ```GravityWell``` enum or an index (0 through 3): 45 | 46 | ``` 47 | particleLab.setGravityWellProperties(gravityWell: .One, normalisedPositionX: 0.3, normalisedPositionY: 0.3, mass: 11, spin: -4) 48 | 49 | particleLab.setGravityWellProperties(gravityWellIndex: 0, normalisedPositionX: 0.3, normalisedPositionY: 0.3, mass: 11, spin: -4) 50 | ``` 51 | 52 | Gravity wells can be cleared so that their mass and spin are set to zero and they have no effect on the particle field: 53 | 54 | ``` 55 | resetGravityWells() 56 | ``` 57 | 58 | ParticleLab can also return the normalised position of any gravity well: 59 | 60 | ``` 61 | getGravityWellNormalisedPosition(#gravityWell: GravityWell) -> (x: Float, y: Float) 62 | ``` 63 | 64 | The positions of each gravity well can be displayed by setting the value of ```showGravityWellPositions``` to true 65 | 66 | ## Setting Particle Properties and Behaviours 67 | 68 | Particles are distributed across three classes which have slightly different masses. The ```particleColor``` property sets the base color and the other two particles colors use variations of it. For example, if ```particleColor``` is set to 0xFFAA00, the other two classes are colored 0x00FFAA and 0xAA00FF. 69 | 70 | The ```dragFactor``` property defines how paricles decelerate. A value of one implies no deceleratation while a value of zero stops particles immediately. Typical values are between 0.8 and 1.0. 71 | 72 | ```respawnOutOfBoundsParticles``` respawns particles to the centre of the screen once they escape the bounds of the simulation. Respawned particles radiate outwards from the centre. 73 | 74 | The particle field can be reset by ```resetParticles()```. This accepts a Boolean argument indicating whether the particles should appear at the edges of the simulation (default, true) or throughout the entire simualtion (false). 75 | 76 | ## ParticleLabDelegate 77 | 78 | The ```ParticleLabDelegate``` protocol contains two methods. 79 | 80 | * ```particleLabDidUpdate()``` is fired with each update 81 | * ```particleLabMetalUnavailable``` is invoked if the target device doesn't support Metal 82 | -------------------------------------------------------------------------------- /MetalParticles/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // MetalParticles 4 | // 5 | // Created by Simon Gladman on 17/01/2015. 6 | // Copyright (c) 2015 Simon Gladman. All rights reserved. 7 | // 8 | // Reengineered based on technique from http://memkite.com/blog/2014/12/30/example-of-sharing-memory-between-gpu-and-cpu-with-swift-and-metal-for-ios8/ 9 | // 10 | // Thanks to https://twitter.com/atveit for tips - espewcially using float4x4!!! 11 | // Thanks to https://twitter.com/warrenm for examples, especially implemnting matrix 4x4 in Swift 12 | // 13 | // This program is free software: you can redistribute it and/or modify 14 | // it under the terms of the GNU General Public License as published by 15 | // the Free Software Foundation, either version 3 of the License, or 16 | // (at your option) any later version. 17 | // 18 | // This program is distributed in the hope that it will be useful, 19 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 20 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 21 | // GNU General Public License for more details. 22 | 23 | // You should have received a copy of the GNU General Public License 24 | // along with this program. If not, see 25 | 26 | import UIKit 27 | 28 | class ViewController: UIViewController, ParticleLabDelegate 29 | { 30 | let menuButton = UIButton() 31 | let statusLabel = UILabel() 32 | 33 | let floatPi = Float(M_PI) 34 | 35 | let hiDPI = false 36 | 37 | var particleLab: ParticleLab! 38 | 39 | var gravityWellAngle: Float = 0 40 | 41 | var demoMode = DemoModes.cloudChamber 42 | 43 | var currentTouches = Set() 44 | 45 | override func viewDidLoad() 46 | { 47 | super.viewDidLoad() 48 | 49 | view.backgroundColor = UIColor.blackColor() 50 | 51 | print(UIScreen.mainScreen().scale) 52 | 53 | let numParticles = ParticleCount.EightMillion 54 | 55 | if hiDPI 56 | { 57 | particleLab = ParticleLab(width: UInt(view.frame.width * UIScreen.mainScreen().scale), 58 | height: UInt(view.frame.height * UIScreen.mainScreen().scale), 59 | numParticles: numParticles, 60 | hiDPI: true) 61 | } 62 | else 63 | { 64 | particleLab = ParticleLab(width: UInt(view.frame.width), 65 | height: UInt(view.frame.height), 66 | numParticles: numParticles, 67 | hiDPI: false) 68 | } 69 | 70 | particleLab.frame = CGRect(x: 0, 71 | y: 0, 72 | width: view.frame.width, 73 | height: view.frame.height) 74 | 75 | particleLab.particleLabDelegate = self 76 | particleLab.dragFactor = 0.5 77 | particleLab.clearOnStep = true 78 | particleLab.respawnOutOfBoundsParticles = false 79 | 80 | view.addSubview(particleLab) 81 | 82 | menuButton.layer.borderColor = UIColor.lightGrayColor().CGColor 83 | menuButton.layer.borderWidth = 1 84 | menuButton.layer.cornerRadius = 5 85 | menuButton.layer.backgroundColor = UIColor.darkGrayColor().CGColor 86 | menuButton.showsTouchWhenHighlighted = true 87 | menuButton.imageView?.contentMode = UIViewContentMode.ScaleAspectFit 88 | menuButton.setImage(UIImage(named: "hamburger.png"), forState: UIControlState.Normal) 89 | menuButton.addTarget(self, action: #selector(ViewController.displayCallout), forControlEvents: UIControlEvents.TouchDown) 90 | 91 | view.addSubview(menuButton) 92 | 93 | statusLabel.text = "http://flexmonkey.blogspot.co.uk" 94 | statusLabel.textColor = UIColor.darkGrayColor() 95 | 96 | view.addSubview(statusLabel) 97 | } 98 | 99 | override func viewDidLayoutSubviews() 100 | { 101 | statusLabel.frame = CGRect(x: 5, 102 | y: view.frame.height - statusLabel.intrinsicContentSize().height, 103 | width: view.frame.width, 104 | height: statusLabel.intrinsicContentSize().height) 105 | 106 | menuButton.frame = CGRect(x: view.frame.width - 35, 107 | y: view.frame.height - 35, 108 | width: 30, 109 | height: 30) 110 | } 111 | 112 | func particleLabMetalUnavailable() 113 | { 114 | // handle metal unavailable here 115 | } 116 | 117 | override func touchesBegan(touches: Set, withEvent event: UIEvent?) 118 | { 119 | currentTouches = currentTouches.union(touches) 120 | } 121 | 122 | override func touchesEnded(touches: Set, withEvent event: UIEvent?) 123 | { 124 | currentTouches = currentTouches.subtract(touches) 125 | } 126 | 127 | func displayCallout() 128 | { 129 | let alertController = UIAlertController(title: nil, message: nil, preferredStyle: UIAlertControllerStyle.ActionSheet) 130 | 131 | let cloudChamberAction = UIAlertAction(title: DemoModes.cloudChamber.rawValue, style: UIAlertActionStyle.Default, handler: calloutActionHandler) 132 | let orbitsAction = UIAlertAction(title: DemoModes.orbits.rawValue, style: UIAlertActionStyle.Default, handler: calloutActionHandler) 133 | let multiTouchAction = UIAlertAction(title: DemoModes.multiTouch.rawValue, style: UIAlertActionStyle.Default, handler: calloutActionHandler) 134 | let respawnAction = UIAlertAction(title: DemoModes.respawn.rawValue, style: UIAlertActionStyle.Default, handler: calloutActionHandler) 135 | let iPadProAction = UIAlertAction(title: DemoModes.iPadProDemo.rawValue, style: UIAlertActionStyle.Default, handler: calloutActionHandler) 136 | 137 | alertController.addAction(cloudChamberAction) 138 | alertController.addAction(orbitsAction) 139 | alertController.addAction(multiTouchAction) 140 | alertController.addAction(respawnAction) 141 | alertController.addAction(iPadProAction) 142 | 143 | if let popoverPresentationController = alertController.popoverPresentationController 144 | { 145 | let xx = menuButton.frame.origin.x 146 | let yy = menuButton.frame.origin.y 147 | 148 | popoverPresentationController.sourceRect = CGRect(x: xx, y: yy, width: menuButton.frame.width, height: menuButton.frame.height) 149 | popoverPresentationController.sourceView = view 150 | } 151 | 152 | particleLab.paused = true 153 | 154 | presentViewController(alertController, animated: true, completion: {self.particleLab.paused = false}) 155 | } 156 | 157 | func calloutActionHandler(value: UIAlertAction!) -> Void 158 | { 159 | demoMode = DemoModes(rawValue: value.title!) ?? DemoModes.iPadProDemo 160 | 161 | switch demoMode 162 | { 163 | case .orbits: 164 | particleLab.dragFactor = 0.82 165 | particleLab.respawnOutOfBoundsParticles = true 166 | particleLab.clearOnStep = true 167 | particleLab.resetParticles(false) 168 | 169 | case .cloudChamber: 170 | particleLab.dragFactor = 0.8 171 | particleLab.respawnOutOfBoundsParticles = false 172 | particleLab.clearOnStep = true 173 | particleLab.resetParticles(true) 174 | 175 | case .multiTouch: 176 | particleLab.dragFactor = 0.95 177 | particleLab.respawnOutOfBoundsParticles = false 178 | particleLab.clearOnStep = true 179 | particleLab.resetParticles(false) 180 | 181 | case .respawn: 182 | particleLab.dragFactor = 0.98 183 | particleLab.respawnOutOfBoundsParticles = true 184 | particleLab.clearOnStep = true 185 | particleLab.resetParticles(true) 186 | 187 | case .iPadProDemo: 188 | particleLab.dragFactor = 0.5 189 | particleLab.respawnOutOfBoundsParticles = true 190 | particleLab.clearOnStep = false 191 | particleLab.resetParticles(true) 192 | } 193 | } 194 | 195 | func particleLabDidUpdate(status: String) 196 | { 197 | statusLabel.text = "http://flexmonkey.blogspot.co.uk | " + status 198 | 199 | particleLab.resetGravityWells() 200 | 201 | switch demoMode 202 | { 203 | case .orbits: 204 | orbitsStep() 205 | 206 | case .cloudChamber: 207 | cloudChamberStep() 208 | 209 | case .multiTouch: 210 | multiTouchStep() 211 | 212 | case .respawn: 213 | respawnStep() 214 | 215 | case .iPadProDemo: 216 | ipadProDemoStep() 217 | } 218 | } 219 | 220 | func respawnStep() 221 | { 222 | gravityWellAngle = gravityWellAngle + 0.02 223 | 224 | particleLab.setGravityWellProperties(gravityWell: .One, 225 | normalisedPositionX: 0.5 + 0.45 * sin(gravityWellAngle), 226 | normalisedPositionY: 0.5 + 0.15 * cos(gravityWellAngle), 227 | mass: 14, 228 | spin: 16) 229 | 230 | particleLab.setGravityWellProperties(gravityWell: .Two, 231 | normalisedPositionX: 0.5 + 0.25 * cos(gravityWellAngle * 1.3), 232 | normalisedPositionY: 0.5 + 0.6 * sin(gravityWellAngle * 1.3), 233 | mass: 8, 234 | spin: 10) 235 | 236 | } 237 | 238 | func multiTouchStep() 239 | { 240 | let currentTouchesArray = Array(currentTouches) 241 | 242 | for (i, currentTouch) in currentTouchesArray.enumerate() where i < 4 243 | { 244 | let touchMultiplier = currentTouch.force == 0 && currentTouch.maximumPossibleForce == 0 245 | ? 1 246 | : Float(currentTouch.force / currentTouch.maximumPossibleForce) 247 | 248 | particleLab.setGravityWellProperties(gravityWellIndex: i, 249 | normalisedPositionX: Float(currentTouch.locationInView(view).x / view.frame.width) , 250 | normalisedPositionY: Float(currentTouch.locationInView(view).y / view.frame.height), 251 | mass: 40 * touchMultiplier, 252 | spin: 20 * touchMultiplier) 253 | } 254 | 255 | for i in currentTouchesArray.count ..< 4 256 | { 257 | particleLab.setGravityWellProperties(gravityWellIndex: i, 258 | normalisedPositionX: 0.5, 259 | normalisedPositionY: 0.5, 260 | mass: 0, 261 | spin: 0) 262 | } 263 | 264 | } 265 | 266 | func ipadProDemoStep() 267 | { 268 | gravityWellAngle = gravityWellAngle + 0.004 269 | 270 | particleLab.setGravityWellProperties(gravityWell: .One, 271 | normalisedPositionX: 0.5 + 0.1 * sin(gravityWellAngle + floatPi * 0.5), 272 | normalisedPositionY: 0.5 + 0.1 * cos(gravityWellAngle + floatPi * 0.5), 273 | mass: 11 * sin(gravityWellAngle / 1.8), 274 | spin: 23 * cos(gravityWellAngle / 2.1)) 275 | 276 | particleLab.setGravityWellProperties(gravityWell: .Two, 277 | normalisedPositionX: 0.5 + 0.1 * sin(gravityWellAngle + floatPi * 1.5), 278 | normalisedPositionY: 0.5 + 0.1 * cos(gravityWellAngle + floatPi * 1.5), 279 | mass: 11 * sin(gravityWellAngle / 0.9), 280 | spin: 23 * cos(gravityWellAngle / 1.05)) 281 | 282 | particleLab.setGravityWellProperties(gravityWell: .Three, 283 | normalisedPositionX: 0.5 + (0.35 + sin(gravityWellAngle * 2.7)) * cos(gravityWellAngle / 1.3), 284 | normalisedPositionY: 0.5 + (0.35 + sin(gravityWellAngle * 2.7)) * sin(gravityWellAngle / 1.3), 285 | mass: 13, spin: 19 * sin(gravityWellAngle * 1.75)) 286 | 287 | let particleOnePosition = particleLab.getGravityWellNormalisedPosition(gravityWell: .One) 288 | let particleTwoPosition = particleLab.getGravityWellNormalisedPosition(gravityWell: .Two) 289 | let particleThreePosition = particleLab.getGravityWellNormalisedPosition(gravityWell: .Three) 290 | 291 | particleLab.setGravityWellProperties(gravityWell: .Four, 292 | normalisedPositionX: (particleOnePosition.x + particleTwoPosition.x + particleThreePosition.x) / 3 + 0.03 * sin(gravityWellAngle), 293 | normalisedPositionY: (particleOnePosition.y + particleTwoPosition.y + particleThreePosition.y) / 3 + 0.03 * cos(gravityWellAngle), 294 | mass: 8 , 295 | spin: 25 * sin(gravityWellAngle / 3 )) 296 | } 297 | 298 | func orbitsStep() 299 | { 300 | gravityWellAngle = gravityWellAngle + 0.0015 301 | 302 | particleLab.setGravityWellProperties(gravityWell: .One, 303 | normalisedPositionX: 0.5 + 0.006 * cos(gravityWellAngle * 43), 304 | normalisedPositionY: 0.5 + 0.006 * sin(gravityWellAngle * 43), 305 | mass: 10, 306 | spin: 24) 307 | 308 | let particleOnePosition = particleLab.getGravityWellNormalisedPosition(gravityWell: .One) 309 | 310 | particleLab.setGravityWellProperties(gravityWell: .Two, 311 | normalisedPositionX: particleOnePosition.x + 0.3 * sin(gravityWellAngle * 5), 312 | normalisedPositionY: particleOnePosition.y + 0.3 * cos(gravityWellAngle * 5), 313 | mass: 4, 314 | spin: 18) 315 | 316 | let particleTwoPosition = particleLab.getGravityWellNormalisedPosition(gravityWell: .Two) 317 | 318 | particleLab.setGravityWellProperties(gravityWell: .Three, 319 | normalisedPositionX: particleTwoPosition.x + 0.1 * cos(gravityWellAngle * 23), 320 | normalisedPositionY: particleTwoPosition.y + 0.1 * sin(gravityWellAngle * 23), 321 | mass: 6, 322 | spin: 17) 323 | 324 | let particleThreePosition = particleLab.getGravityWellNormalisedPosition(gravityWell: .Three) 325 | 326 | particleLab.setGravityWellProperties(gravityWell: .Four, 327 | normalisedPositionX: particleThreePosition.x + 0.03 * sin(gravityWellAngle * 37), 328 | normalisedPositionY: particleThreePosition.y + 0.03 * cos(gravityWellAngle * 37), 329 | mass: 8, 330 | spin: 25) 331 | } 332 | 333 | func cloudChamberStep() 334 | { 335 | gravityWellAngle = gravityWellAngle + 0.02 336 | 337 | particleLab.setGravityWellProperties(gravityWell: .One, 338 | normalisedPositionX: 0.5 + 0.1 * sin(gravityWellAngle + floatPi * 0.5), 339 | normalisedPositionY: 0.5 + 0.1 * cos(gravityWellAngle + floatPi * 0.5), 340 | mass: 11 * sin(gravityWellAngle / 1.9), 341 | spin: 23 * cos(gravityWellAngle / 2.1)) 342 | 343 | particleLab.setGravityWellProperties(gravityWell: .Four, 344 | normalisedPositionX: 0.5 + 0.1 * sin(gravityWellAngle + floatPi * 1.5), 345 | normalisedPositionY: 0.5 + 0.1 * cos(gravityWellAngle + floatPi * 1.5), 346 | mass: 11 * sin(gravityWellAngle / 1.9), 347 | spin: 23 * cos(gravityWellAngle / 2.1)) 348 | 349 | particleLab.setGravityWellProperties(gravityWell: .Two, 350 | normalisedPositionX: 0.5 + (0.35 + sin(gravityWellAngle * 2.7)) * cos(gravityWellAngle / 1.3), 351 | normalisedPositionY: 0.5 + (0.35 + sin(gravityWellAngle * 2.7)) * sin(gravityWellAngle / 1.3), 352 | mass: 26, spin: -19 * sin(gravityWellAngle * 1.5)) 353 | 354 | particleLab.setGravityWellProperties(gravityWell: .Three, 355 | normalisedPositionX: 0.5 + (0.35 + sin(gravityWellAngle * 2.7)) * cos(gravityWellAngle / 1.3 + floatPi), 356 | normalisedPositionY: 0.5 + (0.35 + sin(gravityWellAngle * 2.7)) * sin(gravityWellAngle / 1.3 + floatPi), 357 | mass: 26, spin: -19 * sin(gravityWellAngle * 1.5)) 358 | } 359 | 360 | 361 | override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask 362 | { 363 | return UIInterfaceOrientationMask.Landscape 364 | } 365 | 366 | 367 | override func preferredStatusBarStyle() -> UIStatusBarStyle 368 | { 369 | return UIStatusBarStyle.LightContent 370 | } 371 | 372 | override func didReceiveMemoryWarning() 373 | { 374 | super.didReceiveMemoryWarning() 375 | // Dispose of any resources that can be recreated. 376 | } 377 | 378 | override func prefersStatusBarHidden() -> Bool 379 | { 380 | return true 381 | } 382 | } 383 | 384 | 385 | enum DemoModes: String 386 | { 387 | case iPadProDemo = "iPad Pro Demo" 388 | case cloudChamber = "Cloud Chamber" 389 | case orbits = "Orbits" 390 | case multiTouch = "Multiple Touch" 391 | case respawn = "Respawning" 392 | } 393 | 394 | 395 | 396 | 397 | -------------------------------------------------------------------------------- /MetalParticles/Particles.metal: -------------------------------------------------------------------------------- 1 | // 2 | // Particles.metal 3 | // MetalParticles 4 | // 5 | // Created by Simon Gladman on 17/01/2015. 6 | // Copyright (c) 2015 Simon Gladman. All rights reserved. 7 | // 8 | // Thanks to: http://memkite.com/blog/2014/12/15/data-parallel-programming-with-metal-and-swift-for-iphoneipad-gpu/ 9 | // 10 | // This program is free software: you can redistribute it and/or modify 11 | // it under the terms of the GNU General Public License as published by 12 | // the Free Software Foundation, either version 3 of the License, or 13 | // (at your option) any later version. 14 | // 15 | // This program is distributed in the hope that it will be useful, 16 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | // GNU General Public License for more details. 19 | 20 | // You should have received a copy of the GNU General Public License 21 | // along with this program. If not, see 22 | 23 | #include 24 | using namespace metal; 25 | 26 | kernel void particleRendererShader(texture2d outTexture [[texture(0)]], 27 | // texture2d inTexture [[texture(1)]], 28 | 29 | const device float4x4 *inParticles [[ buffer(0) ]], 30 | device float4x4 *outParticles [[ buffer(1) ]], 31 | 32 | constant float4x4 &inGravityWell [[ buffer(2) ]], 33 | 34 | constant float3 &particleColor [[ buffer(3) ]], 35 | 36 | constant float &imageWidth [[ buffer(4) ]], 37 | constant float &imageHeight [[ buffer(5) ]], 38 | 39 | constant float &dragFactor [[ buffer(6) ]], 40 | 41 | constant bool &respawnOutOfBoundsParticles [[ buffer(7) ]], 42 | 43 | uint id [[thread_position_in_grid]]) 44 | { 45 | const float4x4 inParticle = inParticles[id]; 46 | 47 | const float spawnSpeedMultipler = 2.0; 48 | 49 | const uint type = id % 3; 50 | const float typeTweak = 1 + type; 51 | 52 | const float4 outColor = float4(type == 0 ? particleColor.r : type == 1 ? particleColor.g : particleColor.b, 53 | type == 0 ? particleColor.b : type == 1 ? particleColor.r : particleColor.g, 54 | type == 0 ? particleColor.g : type == 1 ? particleColor.b : particleColor.r, 1); 55 | 56 | // --- 57 | 58 | const float2 gravityWellZeroPosition = float2(inGravityWell[0].x, inGravityWell[0].y); 59 | const float2 gravityWellOnePosition = float2(inGravityWell[1].x, inGravityWell[1].y); 60 | const float2 gravityWellTwoPosition = float2(inGravityWell[2].x, inGravityWell[2].y); 61 | const float2 gravityWellThreePosition = float2(inGravityWell[3].x, inGravityWell[3].y); 62 | 63 | const float gravityWellZeroMass = inGravityWell[0].z * typeTweak; 64 | const float gravityWellOneMass = inGravityWell[1].z * typeTweak; 65 | const float gravityWellTwoMass = inGravityWell[2].z * typeTweak; 66 | const float gravityWellThreeMass = inGravityWell[3].z * typeTweak; 67 | 68 | const float gravityWellZeroSpin = inGravityWell[0].w * typeTweak; 69 | const float gravityWellOneSpin = inGravityWell[1].w * typeTweak; 70 | const float gravityWellTwoSpin = inGravityWell[2].w * typeTweak; 71 | const float gravityWellThreeSpin = inGravityWell[3].w * typeTweak; 72 | 73 | // --- 74 | 75 | const uint2 particlePositionA(inParticle[0].x, inParticle[0].y); 76 | 77 | if (particlePositionA.x > 0 && particlePositionA.y > 0 && particlePositionA.x < imageWidth && particlePositionA.y < imageHeight) 78 | { 79 | outTexture.write(outColor, particlePositionA); 80 | } 81 | else if (respawnOutOfBoundsParticles) 82 | { 83 | inParticle[0].z = spawnSpeedMultipler * fast::sin(inParticle[0].x + inParticle[0].y); 84 | inParticle[0].w = spawnSpeedMultipler * fast::cos(inParticle[0].x + inParticle[0].y); 85 | 86 | inParticle[0].x = imageWidth / 2; 87 | inParticle[0].y = imageHeight / 2; 88 | } 89 | 90 | const float2 particlePositionAFloat(inParticle[0].x, inParticle[0].y); 91 | 92 | const float distanceZeroA = fast::max(distance_squared(particlePositionAFloat, gravityWellZeroPosition), 0.01); 93 | const float distanceOneA = fast::max(distance_squared(particlePositionAFloat, gravityWellOnePosition), 0.01); 94 | const float distanceTwoA = fast::max(distance_squared(particlePositionAFloat, gravityWellTwoPosition), 0.01); 95 | const float distanceThreeA = fast::max(distance_squared(particlePositionAFloat, gravityWellThreePosition), 0.01); 96 | 97 | const float factorAZero = (gravityWellZeroMass / distanceZeroA); 98 | const float factorAOne = (gravityWellOneMass / distanceOneA); 99 | const float factorATwo = (gravityWellTwoMass / distanceTwoA); 100 | const float factorAThree = (gravityWellThreeMass / distanceThreeA); 101 | 102 | const float spinAZero = (gravityWellZeroSpin / distanceZeroA); 103 | const float spinAOne = (gravityWellOneSpin / distanceOneA); 104 | const float spinATwo = (gravityWellTwoSpin / distanceTwoA); 105 | const float spinAThree = (gravityWellThreeSpin / distanceThreeA); 106 | 107 | // --- 108 | 109 | const uint2 particlePositionB(inParticle[1].x, inParticle[1].y); 110 | 111 | if (particlePositionB.x > 0 && particlePositionB.y > 0 && particlePositionB.x < imageWidth && particlePositionB.y < imageHeight) 112 | { 113 | outTexture.write(outColor, particlePositionB); 114 | } 115 | else if (respawnOutOfBoundsParticles) 116 | { 117 | inParticle[1].z = spawnSpeedMultipler * fast::sin(inParticle[1].x + inParticle[1].y); 118 | inParticle[1].w = spawnSpeedMultipler * fast::cos(inParticle[1].x + inParticle[1].y); 119 | 120 | inParticle[1].x = imageWidth / 2; 121 | inParticle[1].y = imageHeight / 2; 122 | } 123 | 124 | const float2 particlePositionBFloat(inParticle[1].x, inParticle[1].y); 125 | 126 | const float distanceZeroB = fast::max(distance_squared(particlePositionBFloat, gravityWellZeroPosition), 0.01); 127 | const float distanceOneB = fast::max(distance_squared(particlePositionBFloat, gravityWellOnePosition), 0.01); 128 | const float distanceTwoB = fast::max(distance_squared(particlePositionBFloat, gravityWellTwoPosition), 0.01); 129 | const float distanceThreeB = fast::max(distance_squared(particlePositionBFloat, gravityWellThreePosition), 0.01); 130 | 131 | const float factorBZero = (gravityWellZeroMass / distanceZeroB); 132 | const float factorBOne = (gravityWellOneMass / distanceOneB); 133 | const float factorBTwo = (gravityWellTwoMass / distanceTwoB); 134 | const float factorBThree = (gravityWellThreeMass / distanceThreeB); 135 | 136 | const float spinBZero = (gravityWellZeroSpin / distanceZeroB); 137 | const float spinBOne = (gravityWellOneSpin / distanceOneB); 138 | const float spinBTwo = (gravityWellTwoSpin / distanceTwoB); 139 | const float spinBThree = (gravityWellThreeSpin / distanceThreeB); 140 | 141 | // --- 142 | 143 | 144 | const uint2 particlePositionC(inParticle[2].x, inParticle[2].y); 145 | 146 | if (particlePositionC.x > 0 && particlePositionC.y > 0 && particlePositionC.x < imageWidth && particlePositionC.y < imageHeight) 147 | { 148 | outTexture.write(outColor, particlePositionC); 149 | } 150 | else if (respawnOutOfBoundsParticles) 151 | { 152 | inParticle[2].z = spawnSpeedMultipler * fast::sin(inParticle[2].x + inParticle[2].y); 153 | inParticle[2].w = spawnSpeedMultipler * fast::cos(inParticle[2].x + inParticle[2].y); 154 | 155 | inParticle[2].x = imageWidth / 2; 156 | inParticle[2].y = imageHeight / 2; 157 | } 158 | 159 | const float2 particlePositionCFloat(inParticle[2].x, inParticle[2].y); 160 | 161 | const float distanceZeroC = fast::max(distance_squared(particlePositionCFloat, gravityWellZeroPosition), 0.01); 162 | const float distanceOneC = fast::max(distance_squared(particlePositionCFloat, gravityWellOnePosition), 0.01); 163 | const float distanceTwoC = fast::max(distance_squared(particlePositionCFloat, gravityWellTwoPosition), 0.01); 164 | const float distanceThreeC = fast::max(distance_squared(particlePositionCFloat, gravityWellThreePosition), 0.01); 165 | 166 | const float factorCZero = (gravityWellZeroMass / distanceZeroC); 167 | const float factorCOne = (gravityWellOneMass / distanceOneC); 168 | const float factorCTwo = (gravityWellTwoMass / distanceTwoC); 169 | const float factorCThree = (gravityWellThreeMass / distanceThreeC); 170 | 171 | const float spinCZero = (gravityWellZeroSpin / distanceZeroC); 172 | const float spinCOne = (gravityWellOneSpin / distanceOneC); 173 | const float spinCTwo = (gravityWellTwoSpin / distanceTwoC); 174 | const float spinCThree = (gravityWellThreeSpin / distanceThreeC); 175 | 176 | // --- 177 | 178 | 179 | const uint2 particlePositionD(inParticle[3].x, inParticle[3].y); 180 | 181 | if (particlePositionD.x > 0 && particlePositionD.y > 0 && particlePositionD.x < imageWidth && particlePositionD.y < imageHeight) 182 | { 183 | outTexture.write(outColor, particlePositionD); 184 | } 185 | else if (respawnOutOfBoundsParticles) 186 | { 187 | inParticle[3].z = spawnSpeedMultipler * fast::sin(inParticle[3].x + inParticle[3].y); 188 | inParticle[3].w = spawnSpeedMultipler * fast::cos(inParticle[3].x + inParticle[3].y); 189 | 190 | inParticle[3].x = imageWidth / 2; 191 | inParticle[3].y = imageHeight / 2; 192 | } 193 | 194 | const float2 particlePositionDFloat(inParticle[3].x, inParticle[3].y); 195 | 196 | const float distanceZeroD = fast::max(distance_squared(particlePositionDFloat, gravityWellZeroPosition), 0.01); 197 | const float distanceOneD = fast::max(distance_squared(particlePositionDFloat, gravityWellOnePosition), 0.01); 198 | const float distanceTwoD = fast::max(distance_squared(particlePositionDFloat, gravityWellTwoPosition), 0.01); 199 | const float distanceThreeD = fast::max(distance_squared(particlePositionDFloat, gravityWellThreePosition), 0.01); 200 | 201 | const float factorDZero = (gravityWellZeroMass / distanceZeroD); 202 | const float factorDOne = (gravityWellOneMass / distanceOneD); 203 | const float factorDTwo = (gravityWellTwoMass / distanceTwoD); 204 | const float factorDThree = (gravityWellThreeMass / distanceThreeD); 205 | 206 | const float spinDZero = (gravityWellZeroSpin / distanceZeroD); 207 | const float spinDOne = (gravityWellOneSpin / distanceOneD); 208 | const float spinDTwo = (gravityWellTwoSpin / distanceTwoD); 209 | const float spinDThree = (gravityWellThreeSpin / distanceThreeD); 210 | // --- 211 | 212 | float4x4 outParticle; 213 | 214 | outParticle[0] = { 215 | inParticle[0].x + inParticle[0].z, 216 | inParticle[0].y + inParticle[0].w, 217 | 218 | (inParticle[0].z * dragFactor) + 219 | ((inGravityWell[0].x - inParticle[0].x) * factorAZero) + 220 | ((inGravityWell[1].x - inParticle[0].x) * factorAOne) + 221 | ((inGravityWell[2].x - inParticle[0].x) * factorATwo) + 222 | ((inGravityWell[3].x - inParticle[0].x) * factorAThree) + 223 | 224 | ((inGravityWell[0].y - inParticle[0].y) * spinAZero) + 225 | ((inGravityWell[1].y - inParticle[0].y) * spinAOne) + 226 | ((inGravityWell[2].y - inParticle[0].y) * spinATwo) + 227 | ((inGravityWell[3].y - inParticle[0].y) * spinAThree), 228 | 229 | (inParticle[0].w * dragFactor) + 230 | ((inGravityWell[0].y - inParticle[0].y) * factorAZero) + 231 | ((inGravityWell[1].y - inParticle[0].y) * factorAOne) + 232 | ((inGravityWell[2].y - inParticle[0].y) * factorATwo) + 233 | ((inGravityWell[3].y - inParticle[0].y) * factorAThree)+ 234 | 235 | ((inGravityWell[0].x - inParticle[0].x) * -spinAZero) + 236 | ((inGravityWell[1].x - inParticle[0].x) * -spinAOne) + 237 | ((inGravityWell[2].x - inParticle[0].x) * -spinATwo) + 238 | ((inGravityWell[3].x - inParticle[0].x) * -spinAThree), 239 | }; 240 | 241 | 242 | outParticle[1] = { 243 | inParticle[1].x + inParticle[1].z, 244 | inParticle[1].y + inParticle[1].w, 245 | 246 | (inParticle[1].z * dragFactor) + 247 | ((inGravityWell[0].x - inParticle[1].x) * factorBZero) + 248 | ((inGravityWell[1].x - inParticle[1].x) * factorBOne) + 249 | ((inGravityWell[2].x - inParticle[1].x) * factorBTwo) + 250 | ((inGravityWell[3].x - inParticle[1].x) * factorBThree) + 251 | 252 | ((inGravityWell[0].y - inParticle[1].y) * spinBZero) + 253 | ((inGravityWell[1].y - inParticle[1].y) * spinBOne) + 254 | ((inGravityWell[2].y - inParticle[1].y) * spinBTwo) + 255 | ((inGravityWell[3].y - inParticle[1].y) * spinBThree), 256 | 257 | (inParticle[1].w * dragFactor) + 258 | ((inGravityWell[0].y - inParticle[1].y) * factorBZero) + 259 | ((inGravityWell[1].y - inParticle[1].y) * factorBOne) + 260 | ((inGravityWell[2].y - inParticle[1].y) * factorBTwo) + 261 | ((inGravityWell[3].y - inParticle[1].y) * factorBThree) + 262 | 263 | ((inGravityWell[0].x - inParticle[1].x) * -spinBZero) + 264 | ((inGravityWell[1].x - inParticle[1].x) * -spinBOne) + 265 | ((inGravityWell[2].x - inParticle[1].x) * -spinBTwo) + 266 | ((inGravityWell[3].x - inParticle[1].x) * -spinBThree), 267 | }; 268 | 269 | 270 | outParticle[2] = { 271 | inParticle[2].x + inParticle[2].z, 272 | inParticle[2].y + inParticle[2].w, 273 | 274 | (inParticle[2].z * dragFactor) + 275 | ((inGravityWell[0].x - inParticle[2].x) * factorCZero) + 276 | ((inGravityWell[1].x - inParticle[2].x) * factorCOne) + 277 | ((inGravityWell[2].x - inParticle[2].x) * factorCTwo) + 278 | ((inGravityWell[3].x - inParticle[2].x) * factorCThree) + 279 | 280 | ((inGravityWell[0].y - inParticle[2].y) * spinCZero) + 281 | ((inGravityWell[1].y - inParticle[2].y) * spinCOne) + 282 | ((inGravityWell[2].y - inParticle[2].y) * spinCTwo) + 283 | ((inGravityWell[3].y - inParticle[2].y) * spinCThree), 284 | 285 | (inParticle[2].w * dragFactor) + 286 | ((inGravityWell[0].y - inParticle[2].y) * factorCZero) + 287 | ((inGravityWell[1].y - inParticle[2].y) * factorCOne) + 288 | ((inGravityWell[2].y - inParticle[2].y) * factorCTwo) + 289 | ((inGravityWell[3].y - inParticle[2].y) * factorCThree) + 290 | 291 | ((inGravityWell[0].x - inParticle[2].x) * -spinCZero) + 292 | ((inGravityWell[1].x - inParticle[2].x) * -spinCOne) + 293 | ((inGravityWell[2].x - inParticle[2].x) * -spinCTwo) + 294 | ((inGravityWell[3].x - inParticle[2].x) * -spinCThree), 295 | }; 296 | 297 | 298 | outParticle[3] = { 299 | inParticle[3].x + inParticle[3].z, 300 | inParticle[3].y + inParticle[3].w, 301 | 302 | (inParticle[3].z * dragFactor) + 303 | ((inGravityWell[0].x - inParticle[3].x) * factorDZero) + 304 | ((inGravityWell[1].x - inParticle[3].x) * factorDOne) + 305 | ((inGravityWell[2].x - inParticle[3].x) * factorDTwo) + 306 | ((inGravityWell[3].x - inParticle[3].x) * factorDThree) + 307 | 308 | ((inGravityWell[0].y - inParticle[3].y) * spinDZero) + 309 | ((inGravityWell[1].y - inParticle[3].y) * spinDOne) + 310 | ((inGravityWell[2].y - inParticle[3].y) * spinDTwo) + 311 | ((inGravityWell[3].y - inParticle[3].y) * spinDThree), 312 | 313 | (inParticle[3].w * dragFactor) + 314 | ((inGravityWell[0].y - inParticle[3].y) * factorDZero) + 315 | ((inGravityWell[1].y - inParticle[3].y) * factorDOne) + 316 | ((inGravityWell[2].y - inParticle[3].y) * factorDTwo) + 317 | ((inGravityWell[3].y - inParticle[3].y) * factorDThree) + 318 | 319 | ((inGravityWell[0].x - inParticle[3].x) * -spinDZero) + 320 | ((inGravityWell[1].x - inParticle[3].x) * -spinDOne) + 321 | ((inGravityWell[2].x - inParticle[3].x) * -spinDTwo) + 322 | ((inGravityWell[3].x - inParticle[3].x) * -spinDThree), 323 | }; 324 | 325 | outParticles[id] = outParticle; 326 | 327 | 328 | // ---- 329 | /* 330 | uint2 textureCoordinate(fast::floor(id / imageWidth),id % int(imageWidth)); 331 | 332 | if (textureCoordinate.x < imageWidth && textureCoordinate.y < imageWidth) 333 | { 334 | float4 accumColor = inTexture.read(textureCoordinate); 335 | 336 | accumColor.rgb = (accumColor.rgb * 0.9f); 337 | accumColor.a = 1.0f; 338 | 339 | outTexture.write(accumColor, textureCoordinate); 340 | } 341 | */ 342 | 343 | } -------------------------------------------------------------------------------- /MetalParticles/ParticleLab.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ParticleLab.swift 3 | // MetalParticles 4 | // 5 | // Created by Simon Gladman on 04/04/2015. 6 | // Copyright (c) 2015 Simon Gladman. All rights reserved. 7 | // 8 | // This program is free software: you can redistribute it and/or modify 9 | // it under the terms of the GNU General Public License as published by 10 | // the Free Software Foundation, either version 3 of the License, or 11 | // (at your option) any later version. 12 | // 13 | // This program is distributed in the hope that it will be useful, 14 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | // GNU General Public License for more details. 17 | 18 | // You should have received a copy of the GNU General Public License 19 | // along with this program. If not, see 20 | 21 | import Metal 22 | import UIKit 23 | import MetalPerformanceShaders 24 | import MetalKit 25 | 26 | class ParticleLab: MTKView 27 | { 28 | let imageWidth: UInt 29 | let imageHeight: UInt 30 | 31 | private var imageWidthFloatBuffer: MTLBuffer! 32 | private var imageHeightFloatBuffer: MTLBuffer! 33 | 34 | let bytesPerRow: UInt 35 | let region: MTLRegion 36 | let blankBitmapRawData : [UInt8] 37 | 38 | private var kernelFunction: MTLFunction! 39 | private var pipelineState: MTLComputePipelineState! 40 | private var defaultLibrary: MTLLibrary! = nil 41 | private var commandQueue: MTLCommandQueue! = nil 42 | 43 | private var threadsPerThreadgroup:MTLSize! 44 | private var threadgroupsPerGrid:MTLSize! 45 | 46 | let particleCount: Int 47 | let alignment:Int = 0x4000 48 | let particlesMemoryByteSize:Int 49 | 50 | private var particlesMemory:UnsafeMutablePointer = nil 51 | private var particlesVoidPtr: COpaquePointer! 52 | private var particlesParticlePtr: UnsafeMutablePointer! 53 | private var particlesParticleBufferPtr: UnsafeMutableBufferPointer! 54 | 55 | private var gravityWellParticle = Particle(A: Vector4(x: 0, y: 0, z: 0, w: 0), 56 | B: Vector4(x: 0, y: 0, z: 0, w: 0), 57 | C: Vector4(x: 0, y: 0, z: 0, w: 0), 58 | D: Vector4(x: 0, y: 0, z: 0, w: 0)) 59 | 60 | private var frameStartTime: CFAbsoluteTime! 61 | private var frameNumber = 0 62 | let particleSize = sizeof(Particle) 63 | 64 | weak var particleLabDelegate: ParticleLabDelegate? 65 | 66 | var particleColor = ParticleColor(R: 1, G: 0.8, B: 0.4, A: 1) 67 | var dragFactor: Float = 0.97 68 | var respawnOutOfBoundsParticles = false 69 | 70 | lazy var blur: MPSImageGaussianBlur = 71 | { 72 | [unowned self] in 73 | return MPSImageGaussianBlur(device: self.device!, sigma: 3) 74 | }() 75 | 76 | lazy var erode: MPSImageAreaMin = 77 | { 78 | [unowned self] in 79 | return MPSImageAreaMin(device: self.device!, kernelWidth: 5, kernelHeight: 5) 80 | }() 81 | 82 | var clearOnStep = true 83 | 84 | let statusPrefix: String 85 | var statusPostix: String = "" 86 | 87 | init(width: UInt, height: UInt, numParticles: ParticleCount, hiDPI: Bool) 88 | { 89 | particleCount = numParticles.rawValue 90 | 91 | imageWidth = width 92 | imageHeight = height 93 | 94 | bytesPerRow = 4 * imageWidth 95 | 96 | region = MTLRegionMake2D(0, 0, Int(imageWidth), Int(imageHeight)) 97 | blankBitmapRawData = [UInt8](count: Int(imageWidth * imageHeight * 4), repeatedValue: 0) 98 | particlesMemoryByteSize = particleCount * sizeof(Particle) 99 | 100 | let formatter = NSNumberFormatter() 101 | formatter.usesGroupingSeparator = true 102 | formatter.numberStyle = NSNumberFormatterStyle.DecimalStyle 103 | 104 | statusPrefix = formatter.stringFromNumber(numParticles.rawValue * 4)! + " Particles" 105 | 106 | let frameWidth = hiDPI ? width / UInt(UIScreen.mainScreen().scale) : width 107 | let frameHeight = hiDPI ? height / UInt(UIScreen.mainScreen().scale) : height 108 | 109 | super.init(frame: CGRect(x: 0, y: 0, width: Int(frameWidth), height: Int(frameHeight)), device: MTLCreateSystemDefaultDevice()) 110 | 111 | framebufferOnly = false 112 | drawableSize = CGSize(width: CGFloat(imageWidth), height: CGFloat(imageHeight)); 113 | 114 | setUpParticles() 115 | 116 | setUpMetal() 117 | 118 | multipleTouchEnabled = true 119 | } 120 | 121 | required init(coder: NSCoder) 122 | { 123 | fatalError("init(coder:) has not been implemented") 124 | } 125 | 126 | 127 | deinit 128 | { 129 | free(particlesMemory) 130 | } 131 | 132 | private func setUpParticles() 133 | { 134 | posix_memalign(&particlesMemory, alignment, particlesMemoryByteSize) 135 | 136 | particlesVoidPtr = COpaquePointer(particlesMemory) 137 | particlesParticlePtr = UnsafeMutablePointer(particlesVoidPtr) 138 | particlesParticleBufferPtr = UnsafeMutableBufferPointer(start: particlesParticlePtr, count: particleCount) 139 | 140 | resetParticles(true) 141 | } 142 | 143 | func resetGravityWells() 144 | { 145 | setGravityWellProperties(gravityWell: .One, normalisedPositionX: 0.5, normalisedPositionY: 0.5, mass: 0, spin: 0) 146 | setGravityWellProperties(gravityWell: .Two, normalisedPositionX: 0.5, normalisedPositionY: 0.5, mass: 0, spin: 0) 147 | setGravityWellProperties(gravityWell: .Three, normalisedPositionX: 0.5, normalisedPositionY: 0.5, mass: 0, spin: 0) 148 | setGravityWellProperties(gravityWell: .Four, normalisedPositionX: 0.5, normalisedPositionY: 0.5, mass: 0, spin: 0) 149 | } 150 | 151 | func resetParticles(edgesOnly: Bool = false) 152 | { 153 | func rand() -> Float32 154 | { 155 | return Float(drand48() - 0.5) * 0.005 156 | } 157 | 158 | let imageWidthDouble = Double(imageWidth) 159 | let imageHeightDouble = Double(imageHeight) 160 | 161 | for index in particlesParticleBufferPtr.startIndex ..< particlesParticleBufferPtr.endIndex 162 | { 163 | var positionAX = Float(drand48() * imageWidthDouble) 164 | var positionAY = Float(drand48() * imageHeightDouble) 165 | 166 | var positionBX = Float(drand48() * imageWidthDouble) 167 | var positionBY = Float(drand48() * imageHeightDouble) 168 | 169 | var positionCX = Float(drand48() * imageWidthDouble) 170 | var positionCY = Float(drand48() * imageHeightDouble) 171 | 172 | var positionDX = Float(drand48() * imageWidthDouble) 173 | var positionDY = Float(drand48() * imageHeightDouble) 174 | 175 | if edgesOnly 176 | { 177 | let positionRule = Int(arc4random() % 4) 178 | 179 | if positionRule == 0 180 | { 181 | positionAX = 0 182 | positionBX = 0 183 | positionCX = 0 184 | positionDX = 0 185 | } 186 | else if positionRule == 1 187 | { 188 | positionAX = Float(imageWidth) 189 | positionBX = Float(imageWidth) 190 | positionCX = Float(imageWidth) 191 | positionDX = Float(imageWidth) 192 | } 193 | else if positionRule == 2 194 | { 195 | positionAY = 0 196 | positionBY = 0 197 | positionCY = 0 198 | positionDY = 0 199 | } 200 | else 201 | { 202 | positionAY = Float(imageHeight) 203 | positionBY = Float(imageHeight) 204 | positionCY = Float(imageHeight) 205 | positionDY = Float(imageHeight) 206 | } 207 | } 208 | 209 | let particle = Particle(A: Vector4(x: positionAX, y: positionAY, z: rand(), w: rand()), 210 | B: Vector4(x: positionBX, y: positionBY, z: rand(), w: rand()), 211 | C: Vector4(x: positionCX, y: positionCY, z: rand(), w: rand()), 212 | D: Vector4(x: positionDX, y: positionDY, z: rand(), w: rand())) 213 | 214 | particlesParticleBufferPtr[index] = particle 215 | } 216 | } 217 | 218 | private func setUpMetal() 219 | { 220 | device = MTLCreateSystemDefaultDevice() 221 | 222 | guard let device = device else 223 | { 224 | particleLabDelegate?.particleLabMetalUnavailable() 225 | 226 | return 227 | } 228 | 229 | defaultLibrary = device.newDefaultLibrary() 230 | commandQueue = device.newCommandQueue() 231 | 232 | kernelFunction = defaultLibrary.newFunctionWithName("particleRendererShader") 233 | 234 | do 235 | { 236 | try pipelineState = device.newComputePipelineStateWithFunction(kernelFunction!) 237 | } 238 | catch 239 | { 240 | fatalError("newComputePipelineStateWithFunction failed ") 241 | } 242 | 243 | let threadExecutionWidth = pipelineState.threadExecutionWidth 244 | 245 | threadsPerThreadgroup = MTLSize(width:threadExecutionWidth,height:1,depth:1) 246 | threadgroupsPerGrid = MTLSize(width:particleCount / threadExecutionWidth, height:1, depth:1) 247 | 248 | frameStartTime = CFAbsoluteTimeGetCurrent() 249 | 250 | var imageWidthFloat = Float(imageWidth) 251 | var imageHeightFloat = Float(imageHeight) 252 | 253 | imageWidthFloatBuffer = device.newBufferWithBytes(&imageWidthFloat, length: sizeof(Float), options: MTLResourceOptions.CPUCacheModeDefaultCache) 254 | 255 | imageHeightFloatBuffer = device.newBufferWithBytes(&imageHeightFloat, length: sizeof(Float), options: MTLResourceOptions.CPUCacheModeDefaultCache) 256 | } 257 | 258 | override func drawRect(dirtyRect: CGRect) 259 | { 260 | guard let device = device else 261 | { 262 | particleLabDelegate?.particleLabMetalUnavailable() 263 | 264 | return 265 | } 266 | 267 | frameNumber += 1 268 | 269 | if frameNumber == 100 270 | { 271 | let frametime = (CFAbsoluteTimeGetCurrent() - frameStartTime) / 100 272 | 273 | statusPostix = String(format: " at %.1f fps", 1 / frametime) 274 | 275 | frameStartTime = CFAbsoluteTimeGetCurrent() 276 | 277 | frameNumber = 0 278 | } 279 | 280 | let commandBuffer = commandQueue.commandBuffer() 281 | let commandEncoder = commandBuffer.computeCommandEncoder() 282 | 283 | commandEncoder.setComputePipelineState(pipelineState) 284 | 285 | let particlesBufferNoCopy = device.newBufferWithBytesNoCopy(particlesMemory, length: Int(particlesMemoryByteSize), 286 | options: MTLResourceOptions.CPUCacheModeDefaultCache, deallocator: nil) 287 | 288 | commandEncoder.setBuffer(particlesBufferNoCopy, offset: 0, atIndex: 0) 289 | commandEncoder.setBuffer(particlesBufferNoCopy, offset: 0, atIndex: 1) 290 | 291 | let inGravityWell = device.newBufferWithBytes(&gravityWellParticle, length: particleSize, options: MTLResourceOptions.CPUCacheModeDefaultCache) 292 | commandEncoder.setBuffer(inGravityWell, offset: 0, atIndex: 2) 293 | 294 | let colorBuffer = device.newBufferWithBytes(&particleColor, length: sizeof(ParticleColor), options: MTLResourceOptions.CPUCacheModeDefaultCache) 295 | commandEncoder.setBuffer(colorBuffer, offset: 0, atIndex: 3) 296 | 297 | commandEncoder.setBuffer(imageWidthFloatBuffer, offset: 0, atIndex: 4) 298 | commandEncoder.setBuffer(imageHeightFloatBuffer, offset: 0, atIndex: 5) 299 | 300 | let dragFactorBuffer = device.newBufferWithBytes(&dragFactor, length: sizeof(Float), options: MTLResourceOptions.CPUCacheModeDefaultCache) 301 | commandEncoder.setBuffer(dragFactorBuffer, offset: 0, atIndex: 6) 302 | 303 | let respawnOutOfBoundsParticlesBuffer = device.newBufferWithBytes(&respawnOutOfBoundsParticles, length: sizeof(Bool), options: MTLResourceOptions.CPUCacheModeDefaultCache) 304 | commandEncoder.setBuffer(respawnOutOfBoundsParticlesBuffer, offset: 0, atIndex: 7) 305 | 306 | guard let drawable = currentDrawable else 307 | { 308 | commandEncoder.endEncoding() 309 | 310 | print("metalLayer.nextDrawable() returned nil") 311 | 312 | return 313 | } 314 | 315 | if clearOnStep 316 | { 317 | drawable.texture.replaceRegion(self.region, 318 | mipmapLevel: 0, 319 | withBytes: blankBitmapRawData, 320 | bytesPerRow: Int(bytesPerRow)) 321 | } 322 | 323 | 324 | commandEncoder.setTexture(drawable.texture, atIndex: 0) 325 | 326 | commandEncoder.dispatchThreadgroups(threadgroupsPerGrid, threadsPerThreadgroup: threadsPerThreadgroup) 327 | 328 | commandEncoder.endEncoding() 329 | 330 | if !clearOnStep 331 | { 332 | let inPlaceTexture = UnsafeMutablePointer.alloc(1) 333 | inPlaceTexture.initialize(drawable.texture) 334 | 335 | blur.encodeToCommandBuffer(commandBuffer, 336 | inPlaceTexture: inPlaceTexture, 337 | fallbackCopyAllocator: nil) 338 | 339 | erode.encodeToCommandBuffer(commandBuffer, 340 | inPlaceTexture: inPlaceTexture, 341 | fallbackCopyAllocator: nil) 342 | } 343 | 344 | commandBuffer.commit() 345 | 346 | drawable.present() 347 | 348 | particleLabDelegate?.particleLabDidUpdate(statusPrefix + statusPostix) 349 | } 350 | 351 | final func getGravityWellNormalisedPosition(gravityWell gravityWell: GravityWell) -> (x: Float, y: Float) 352 | { 353 | let returnPoint: (x: Float, y: Float) 354 | 355 | let imageWidthFloat = Float(imageWidth) 356 | let imageHeightFloat = Float(imageHeight) 357 | 358 | switch gravityWell 359 | { 360 | case .One: 361 | returnPoint = (x: gravityWellParticle.A.x / imageWidthFloat, y: gravityWellParticle.A.y / imageHeightFloat) 362 | 363 | case .Two: 364 | returnPoint = (x: gravityWellParticle.B.x / imageWidthFloat, y: gravityWellParticle.B.y / imageHeightFloat) 365 | 366 | case .Three: 367 | returnPoint = (x: gravityWellParticle.C.x / imageWidthFloat, y: gravityWellParticle.C.y / imageHeightFloat) 368 | 369 | case .Four: 370 | returnPoint = (x: gravityWellParticle.D.x / imageWidthFloat, y: gravityWellParticle.D.y / imageHeightFloat) 371 | } 372 | 373 | return returnPoint 374 | } 375 | 376 | final func setGravityWellProperties(gravityWellIndex gravityWellIndex: Int, normalisedPositionX: Float, normalisedPositionY: Float, mass: Float, spin: Float) 377 | { 378 | switch gravityWellIndex 379 | { 380 | case 1: 381 | setGravityWellProperties(gravityWell: .Two, normalisedPositionX: normalisedPositionX, normalisedPositionY: normalisedPositionY, mass: mass, spin: spin) 382 | 383 | case 2: 384 | setGravityWellProperties(gravityWell: .Three, normalisedPositionX: normalisedPositionX, normalisedPositionY: normalisedPositionY, mass: mass, spin: spin) 385 | 386 | case 3: 387 | setGravityWellProperties(gravityWell: .Four, normalisedPositionX: normalisedPositionX, normalisedPositionY: normalisedPositionY, mass: mass, spin: spin) 388 | 389 | default: 390 | setGravityWellProperties(gravityWell: .One, normalisedPositionX: normalisedPositionX, normalisedPositionY: normalisedPositionY, mass: mass, spin: spin) 391 | } 392 | } 393 | 394 | final func setGravityWellProperties(gravityWell gravityWell: GravityWell, normalisedPositionX: Float, normalisedPositionY: Float, mass: Float, spin: Float) 395 | { 396 | let imageWidthFloat = Float(imageWidth) 397 | let imageHeightFloat = Float(imageHeight) 398 | 399 | switch gravityWell 400 | { 401 | case .One: 402 | gravityWellParticle.A.x = imageWidthFloat * normalisedPositionX 403 | gravityWellParticle.A.y = imageHeightFloat * normalisedPositionY 404 | gravityWellParticle.A.z = mass 405 | gravityWellParticle.A.w = spin 406 | 407 | case .Two: 408 | gravityWellParticle.B.x = imageWidthFloat * normalisedPositionX 409 | gravityWellParticle.B.y = imageHeightFloat * normalisedPositionY 410 | gravityWellParticle.B.z = mass 411 | gravityWellParticle.B.w = spin 412 | 413 | case .Three: 414 | gravityWellParticle.C.x = imageWidthFloat * normalisedPositionX 415 | gravityWellParticle.C.y = imageHeightFloat * normalisedPositionY 416 | gravityWellParticle.C.z = mass 417 | gravityWellParticle.C.w = spin 418 | 419 | case .Four: 420 | gravityWellParticle.D.x = imageWidthFloat * normalisedPositionX 421 | gravityWellParticle.D.y = imageHeightFloat * normalisedPositionY 422 | gravityWellParticle.D.z = mass 423 | gravityWellParticle.D.w = spin 424 | } 425 | } 426 | } 427 | 428 | protocol ParticleLabDelegate: NSObjectProtocol 429 | { 430 | func particleLabDidUpdate(status: String) 431 | func particleLabMetalUnavailable() 432 | } 433 | 434 | enum GravityWell 435 | { 436 | case One 437 | case Two 438 | case Three 439 | case Four 440 | } 441 | 442 | // Since each Particle instance defines four particles, the visible particle count 443 | // in the API is four times the number we need to create. 444 | enum ParticleCount: Int 445 | { 446 | case QtrMillion = 65_536 447 | case HalfMillion = 131_072 448 | case OneMillion = 262_144 449 | case TwoMillion = 524_288 450 | case FourMillion = 1_048_576 451 | case EightMillion = 2_097_152 452 | case SixteenMillion = 4_194_304 453 | } 454 | 455 | // Paticles are split into three classes. The supplied particle color defines one 456 | // third of the rendererd particles, the other two thirds use the supplied particle 457 | // color components but shifted to BRG and GBR 458 | struct ParticleColor 459 | { 460 | var R: Float32 = 0 461 | var G: Float32 = 0 462 | var B: Float32 = 0 463 | var A: Float32 = 1 464 | } 465 | 466 | struct Particle // Matrix4x4 467 | { 468 | var A: Vector4 = Vector4(x: 0, y: 0, z: 0, w: 0) 469 | var B: Vector4 = Vector4(x: 0, y: 0, z: 0, w: 0) 470 | var C: Vector4 = Vector4(x: 0, y: 0, z: 0, w: 0) 471 | var D: Vector4 = Vector4(x: 0, y: 0, z: 0, w: 0) 472 | } 473 | 474 | // Regular particles use x and y for position and z and w for velocity 475 | // gravity wells use x and y for position and z for mass and w for spin 476 | struct Vector4 477 | { 478 | var x: Float32 = 0 479 | var y: Float32 = 0 480 | var z: Float32 = 0 481 | var w: Float32 = 0 482 | } 483 | 484 | -------------------------------------------------------------------------------- /MetalParticles.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | BE75EE921A6A2CC000B20D49 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE75EE911A6A2CC000B20D49 /* AppDelegate.swift */; }; 11 | BE75EE941A6A2CC000B20D49 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE75EE931A6A2CC000B20D49 /* ViewController.swift */; }; 12 | BE75EE971A6A2CC000B20D49 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BE75EE951A6A2CC000B20D49 /* Main.storyboard */; }; 13 | BE75EE991A6A2CC000B20D49 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BE75EE981A6A2CC000B20D49 /* Images.xcassets */; }; 14 | BE75EE9C1A6A2CC000B20D49 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = BE75EE9A1A6A2CC000B20D49 /* LaunchScreen.xib */; }; 15 | BE75EEA81A6A2CC000B20D49 /* MetalParticlesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE75EEA71A6A2CC000B20D49 /* MetalParticlesTests.swift */; }; 16 | BE75EEB31A6A327F00B20D49 /* Particles.metal in Sources */ = {isa = PBXBuildFile; fileRef = BE75EEB21A6A327F00B20D49 /* Particles.metal */; }; 17 | BE75EEB91A6A82C800B20D49 /* MarkerWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE75EEB81A6A82C800B20D49 /* MarkerWidget.swift */; }; 18 | BE947C681ADA382C00614CD9 /* hamburger.png in Resources */ = {isa = PBXBuildFile; fileRef = BE947C671ADA382C00614CD9 /* hamburger.png */; }; 19 | BEEBCABB1ACFAB41002865B3 /* ParticleLab.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEEBCABA1ACFAB41002865B3 /* ParticleLab.swift */; }; 20 | /* End PBXBuildFile section */ 21 | 22 | /* Begin PBXContainerItemProxy section */ 23 | BE75EEA21A6A2CC000B20D49 /* PBXContainerItemProxy */ = { 24 | isa = PBXContainerItemProxy; 25 | containerPortal = BE75EE841A6A2CC000B20D49 /* Project object */; 26 | proxyType = 1; 27 | remoteGlobalIDString = BE75EE8B1A6A2CC000B20D49; 28 | remoteInfo = MetalParticles; 29 | }; 30 | /* End PBXContainerItemProxy section */ 31 | 32 | /* Begin PBXFileReference section */ 33 | BE75EE8C1A6A2CC000B20D49 /* MetalParticles.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MetalParticles.app; sourceTree = BUILT_PRODUCTS_DIR; }; 34 | BE75EE901A6A2CC000B20D49 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 35 | BE75EE911A6A2CC000B20D49 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 36 | BE75EE931A6A2CC000B20D49 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 37 | BE75EE961A6A2CC000B20D49 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 38 | BE75EE981A6A2CC000B20D49 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 39 | BE75EE9B1A6A2CC000B20D49 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; 40 | BE75EEA11A6A2CC000B20D49 /* MetalParticlesTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MetalParticlesTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 41 | BE75EEA61A6A2CC000B20D49 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 42 | BE75EEA71A6A2CC000B20D49 /* MetalParticlesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetalParticlesTests.swift; sourceTree = ""; }; 43 | BE75EEB21A6A327F00B20D49 /* Particles.metal */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.metal; path = Particles.metal; sourceTree = ""; }; 44 | BE75EEB81A6A82C800B20D49 /* MarkerWidget.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarkerWidget.swift; sourceTree = ""; }; 45 | BE947C671ADA382C00614CD9 /* hamburger.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = hamburger.png; sourceTree = ""; }; 46 | BEEBCABA1ACFAB41002865B3 /* ParticleLab.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParticleLab.swift; sourceTree = ""; }; 47 | /* End PBXFileReference section */ 48 | 49 | /* Begin PBXFrameworksBuildPhase section */ 50 | BE75EE891A6A2CC000B20D49 /* Frameworks */ = { 51 | isa = PBXFrameworksBuildPhase; 52 | buildActionMask = 2147483647; 53 | files = ( 54 | ); 55 | runOnlyForDeploymentPostprocessing = 0; 56 | }; 57 | BE75EE9E1A6A2CC000B20D49 /* Frameworks */ = { 58 | isa = PBXFrameworksBuildPhase; 59 | buildActionMask = 2147483647; 60 | files = ( 61 | ); 62 | runOnlyForDeploymentPostprocessing = 0; 63 | }; 64 | /* End PBXFrameworksBuildPhase section */ 65 | 66 | /* Begin PBXGroup section */ 67 | BE75EE831A6A2CC000B20D49 = { 68 | isa = PBXGroup; 69 | children = ( 70 | BE75EE8E1A6A2CC000B20D49 /* MetalParticles */, 71 | BE75EEA41A6A2CC000B20D49 /* MetalParticlesTests */, 72 | BE75EE8D1A6A2CC000B20D49 /* Products */, 73 | ); 74 | sourceTree = ""; 75 | }; 76 | BE75EE8D1A6A2CC000B20D49 /* Products */ = { 77 | isa = PBXGroup; 78 | children = ( 79 | BE75EE8C1A6A2CC000B20D49 /* MetalParticles.app */, 80 | BE75EEA11A6A2CC000B20D49 /* MetalParticlesTests.xctest */, 81 | ); 82 | name = Products; 83 | sourceTree = ""; 84 | }; 85 | BE75EE8E1A6A2CC000B20D49 /* MetalParticles */ = { 86 | isa = PBXGroup; 87 | children = ( 88 | BE947C661ADA380A00614CD9 /* assets */, 89 | BEEBCAB91ACFAB1A002865B3 /* particleLab */, 90 | BE75EEB71A6A82A400B20D49 /* marker */, 91 | BE75EEB11A6A326400B20D49 /* shaders */, 92 | BE75EE911A6A2CC000B20D49 /* AppDelegate.swift */, 93 | BE75EE931A6A2CC000B20D49 /* ViewController.swift */, 94 | BE75EE951A6A2CC000B20D49 /* Main.storyboard */, 95 | BE75EE981A6A2CC000B20D49 /* Images.xcassets */, 96 | BE75EE9A1A6A2CC000B20D49 /* LaunchScreen.xib */, 97 | BE75EE8F1A6A2CC000B20D49 /* Supporting Files */, 98 | ); 99 | path = MetalParticles; 100 | sourceTree = ""; 101 | }; 102 | BE75EE8F1A6A2CC000B20D49 /* Supporting Files */ = { 103 | isa = PBXGroup; 104 | children = ( 105 | BE75EE901A6A2CC000B20D49 /* Info.plist */, 106 | ); 107 | name = "Supporting Files"; 108 | sourceTree = ""; 109 | }; 110 | BE75EEA41A6A2CC000B20D49 /* MetalParticlesTests */ = { 111 | isa = PBXGroup; 112 | children = ( 113 | BE75EEA71A6A2CC000B20D49 /* MetalParticlesTests.swift */, 114 | BE75EEA51A6A2CC000B20D49 /* Supporting Files */, 115 | ); 116 | path = MetalParticlesTests; 117 | sourceTree = ""; 118 | }; 119 | BE75EEA51A6A2CC000B20D49 /* Supporting Files */ = { 120 | isa = PBXGroup; 121 | children = ( 122 | BE75EEA61A6A2CC000B20D49 /* Info.plist */, 123 | ); 124 | name = "Supporting Files"; 125 | sourceTree = ""; 126 | }; 127 | BE75EEB11A6A326400B20D49 /* shaders */ = { 128 | isa = PBXGroup; 129 | children = ( 130 | BE75EEB21A6A327F00B20D49 /* Particles.metal */, 131 | ); 132 | name = shaders; 133 | sourceTree = ""; 134 | }; 135 | BE75EEB71A6A82A400B20D49 /* marker */ = { 136 | isa = PBXGroup; 137 | children = ( 138 | BE75EEB81A6A82C800B20D49 /* MarkerWidget.swift */, 139 | ); 140 | name = marker; 141 | sourceTree = ""; 142 | }; 143 | BE947C661ADA380A00614CD9 /* assets */ = { 144 | isa = PBXGroup; 145 | children = ( 146 | BE947C671ADA382C00614CD9 /* hamburger.png */, 147 | ); 148 | name = assets; 149 | sourceTree = ""; 150 | }; 151 | BEEBCAB91ACFAB1A002865B3 /* particleLab */ = { 152 | isa = PBXGroup; 153 | children = ( 154 | BEEBCABA1ACFAB41002865B3 /* ParticleLab.swift */, 155 | ); 156 | name = particleLab; 157 | sourceTree = ""; 158 | }; 159 | /* End PBXGroup section */ 160 | 161 | /* Begin PBXNativeTarget section */ 162 | BE75EE8B1A6A2CC000B20D49 /* MetalParticles */ = { 163 | isa = PBXNativeTarget; 164 | buildConfigurationList = BE75EEAB1A6A2CC000B20D49 /* Build configuration list for PBXNativeTarget "MetalParticles" */; 165 | buildPhases = ( 166 | BE75EE881A6A2CC000B20D49 /* Sources */, 167 | BE75EE891A6A2CC000B20D49 /* Frameworks */, 168 | BE75EE8A1A6A2CC000B20D49 /* Resources */, 169 | ); 170 | buildRules = ( 171 | ); 172 | dependencies = ( 173 | ); 174 | name = MetalParticles; 175 | productName = MetalParticles; 176 | productReference = BE75EE8C1A6A2CC000B20D49 /* MetalParticles.app */; 177 | productType = "com.apple.product-type.application"; 178 | }; 179 | BE75EEA01A6A2CC000B20D49 /* MetalParticlesTests */ = { 180 | isa = PBXNativeTarget; 181 | buildConfigurationList = BE75EEAE1A6A2CC000B20D49 /* Build configuration list for PBXNativeTarget "MetalParticlesTests" */; 182 | buildPhases = ( 183 | BE75EE9D1A6A2CC000B20D49 /* Sources */, 184 | BE75EE9E1A6A2CC000B20D49 /* Frameworks */, 185 | BE75EE9F1A6A2CC000B20D49 /* Resources */, 186 | ); 187 | buildRules = ( 188 | ); 189 | dependencies = ( 190 | BE75EEA31A6A2CC000B20D49 /* PBXTargetDependency */, 191 | ); 192 | name = MetalParticlesTests; 193 | productName = MetalParticlesTests; 194 | productReference = BE75EEA11A6A2CC000B20D49 /* MetalParticlesTests.xctest */; 195 | productType = "com.apple.product-type.bundle.unit-test"; 196 | }; 197 | /* End PBXNativeTarget section */ 198 | 199 | /* Begin PBXProject section */ 200 | BE75EE841A6A2CC000B20D49 /* Project object */ = { 201 | isa = PBXProject; 202 | attributes = { 203 | LastSwiftUpdateCheck = 0700; 204 | LastUpgradeCheck = 0700; 205 | ORGANIZATIONNAME = "Simon Gladman"; 206 | TargetAttributes = { 207 | BE75EE8B1A6A2CC000B20D49 = { 208 | CreatedOnToolsVersion = 6.1.1; 209 | DevelopmentTeam = ZBFYF9JG5V; 210 | }; 211 | BE75EEA01A6A2CC000B20D49 = { 212 | CreatedOnToolsVersion = 6.1.1; 213 | TestTargetID = BE75EE8B1A6A2CC000B20D49; 214 | }; 215 | }; 216 | }; 217 | buildConfigurationList = BE75EE871A6A2CC000B20D49 /* Build configuration list for PBXProject "MetalParticles" */; 218 | compatibilityVersion = "Xcode 3.2"; 219 | developmentRegion = English; 220 | hasScannedForEncodings = 0; 221 | knownRegions = ( 222 | en, 223 | Base, 224 | ); 225 | mainGroup = BE75EE831A6A2CC000B20D49; 226 | productRefGroup = BE75EE8D1A6A2CC000B20D49 /* Products */; 227 | projectDirPath = ""; 228 | projectRoot = ""; 229 | targets = ( 230 | BE75EE8B1A6A2CC000B20D49 /* MetalParticles */, 231 | BE75EEA01A6A2CC000B20D49 /* MetalParticlesTests */, 232 | ); 233 | }; 234 | /* End PBXProject section */ 235 | 236 | /* Begin PBXResourcesBuildPhase section */ 237 | BE75EE8A1A6A2CC000B20D49 /* Resources */ = { 238 | isa = PBXResourcesBuildPhase; 239 | buildActionMask = 2147483647; 240 | files = ( 241 | BE75EE971A6A2CC000B20D49 /* Main.storyboard in Resources */, 242 | BE947C681ADA382C00614CD9 /* hamburger.png in Resources */, 243 | BE75EE9C1A6A2CC000B20D49 /* LaunchScreen.xib in Resources */, 244 | BE75EE991A6A2CC000B20D49 /* Images.xcassets in Resources */, 245 | ); 246 | runOnlyForDeploymentPostprocessing = 0; 247 | }; 248 | BE75EE9F1A6A2CC000B20D49 /* Resources */ = { 249 | isa = PBXResourcesBuildPhase; 250 | buildActionMask = 2147483647; 251 | files = ( 252 | ); 253 | runOnlyForDeploymentPostprocessing = 0; 254 | }; 255 | /* End PBXResourcesBuildPhase section */ 256 | 257 | /* Begin PBXSourcesBuildPhase section */ 258 | BE75EE881A6A2CC000B20D49 /* Sources */ = { 259 | isa = PBXSourcesBuildPhase; 260 | buildActionMask = 2147483647; 261 | files = ( 262 | BEEBCABB1ACFAB41002865B3 /* ParticleLab.swift in Sources */, 263 | BE75EEB31A6A327F00B20D49 /* Particles.metal in Sources */, 264 | BE75EE941A6A2CC000B20D49 /* ViewController.swift in Sources */, 265 | BE75EEB91A6A82C800B20D49 /* MarkerWidget.swift in Sources */, 266 | BE75EE921A6A2CC000B20D49 /* AppDelegate.swift in Sources */, 267 | ); 268 | runOnlyForDeploymentPostprocessing = 0; 269 | }; 270 | BE75EE9D1A6A2CC000B20D49 /* Sources */ = { 271 | isa = PBXSourcesBuildPhase; 272 | buildActionMask = 2147483647; 273 | files = ( 274 | BE75EEA81A6A2CC000B20D49 /* MetalParticlesTests.swift in Sources */, 275 | ); 276 | runOnlyForDeploymentPostprocessing = 0; 277 | }; 278 | /* End PBXSourcesBuildPhase section */ 279 | 280 | /* Begin PBXTargetDependency section */ 281 | BE75EEA31A6A2CC000B20D49 /* PBXTargetDependency */ = { 282 | isa = PBXTargetDependency; 283 | target = BE75EE8B1A6A2CC000B20D49 /* MetalParticles */; 284 | targetProxy = BE75EEA21A6A2CC000B20D49 /* PBXContainerItemProxy */; 285 | }; 286 | /* End PBXTargetDependency section */ 287 | 288 | /* Begin PBXVariantGroup section */ 289 | BE75EE951A6A2CC000B20D49 /* Main.storyboard */ = { 290 | isa = PBXVariantGroup; 291 | children = ( 292 | BE75EE961A6A2CC000B20D49 /* Base */, 293 | ); 294 | name = Main.storyboard; 295 | sourceTree = ""; 296 | }; 297 | BE75EE9A1A6A2CC000B20D49 /* LaunchScreen.xib */ = { 298 | isa = PBXVariantGroup; 299 | children = ( 300 | BE75EE9B1A6A2CC000B20D49 /* Base */, 301 | ); 302 | name = LaunchScreen.xib; 303 | sourceTree = ""; 304 | }; 305 | /* End PBXVariantGroup section */ 306 | 307 | /* Begin XCBuildConfiguration section */ 308 | BE75EEA91A6A2CC000B20D49 /* Debug */ = { 309 | isa = XCBuildConfiguration; 310 | buildSettings = { 311 | ALWAYS_SEARCH_USER_PATHS = NO; 312 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 313 | CLANG_CXX_LIBRARY = "libc++"; 314 | CLANG_ENABLE_MODULES = YES; 315 | CLANG_ENABLE_OBJC_ARC = YES; 316 | CLANG_WARN_BOOL_CONVERSION = YES; 317 | CLANG_WARN_CONSTANT_CONVERSION = YES; 318 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 319 | CLANG_WARN_EMPTY_BODY = YES; 320 | CLANG_WARN_ENUM_CONVERSION = YES; 321 | CLANG_WARN_INT_CONVERSION = YES; 322 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 323 | CLANG_WARN_UNREACHABLE_CODE = YES; 324 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 325 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 326 | COPY_PHASE_STRIP = NO; 327 | ENABLE_STRICT_OBJC_MSGSEND = YES; 328 | ENABLE_TESTABILITY = YES; 329 | GCC_C_LANGUAGE_STANDARD = gnu99; 330 | GCC_DYNAMIC_NO_PIC = NO; 331 | GCC_OPTIMIZATION_LEVEL = 0; 332 | GCC_PREPROCESSOR_DEFINITIONS = ( 333 | "DEBUG=1", 334 | "$(inherited)", 335 | ); 336 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 337 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 338 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 339 | GCC_WARN_UNDECLARED_SELECTOR = YES; 340 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 341 | GCC_WARN_UNUSED_FUNCTION = YES; 342 | GCC_WARN_UNUSED_VARIABLE = YES; 343 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 344 | MTL_ENABLE_DEBUG_INFO = YES; 345 | MTL_FAST_MATH = YES; 346 | MTL_OPTIMIZATION_LEVEL = s; 347 | ONLY_ACTIVE_ARCH = YES; 348 | SDKROOT = iphoneos; 349 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 350 | TARGETED_DEVICE_FAMILY = "1,2"; 351 | }; 352 | name = Debug; 353 | }; 354 | BE75EEAA1A6A2CC000B20D49 /* Release */ = { 355 | isa = XCBuildConfiguration; 356 | buildSettings = { 357 | ALWAYS_SEARCH_USER_PATHS = NO; 358 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 359 | CLANG_CXX_LIBRARY = "libc++"; 360 | CLANG_ENABLE_MODULES = YES; 361 | CLANG_ENABLE_OBJC_ARC = YES; 362 | CLANG_WARN_BOOL_CONVERSION = YES; 363 | CLANG_WARN_CONSTANT_CONVERSION = YES; 364 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 365 | CLANG_WARN_EMPTY_BODY = YES; 366 | CLANG_WARN_ENUM_CONVERSION = YES; 367 | CLANG_WARN_INT_CONVERSION = YES; 368 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 369 | CLANG_WARN_UNREACHABLE_CODE = YES; 370 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 371 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 372 | COPY_PHASE_STRIP = YES; 373 | ENABLE_NS_ASSERTIONS = NO; 374 | ENABLE_STRICT_OBJC_MSGSEND = YES; 375 | GCC_C_LANGUAGE_STANDARD = gnu99; 376 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 377 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 378 | GCC_WARN_UNDECLARED_SELECTOR = YES; 379 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 380 | GCC_WARN_UNUSED_FUNCTION = YES; 381 | GCC_WARN_UNUSED_VARIABLE = YES; 382 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 383 | MTL_ENABLE_DEBUG_INFO = NO; 384 | MTL_FAST_MATH = YES; 385 | MTL_OPTIMIZATION_LEVEL = s; 386 | SDKROOT = iphoneos; 387 | SWIFT_DISABLE_SAFETY_CHECKS = YES; 388 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 389 | TARGETED_DEVICE_FAMILY = "1,2"; 390 | VALIDATE_PRODUCT = YES; 391 | }; 392 | name = Release; 393 | }; 394 | BE75EEAC1A6A2CC000B20D49 /* Debug */ = { 395 | isa = XCBuildConfiguration; 396 | buildSettings = { 397 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 398 | CODE_SIGN_IDENTITY = "iPhone Developer"; 399 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 400 | INFOPLIST_FILE = MetalParticles/Info.plist; 401 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 402 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 403 | MTL_FAST_MATH = YES; 404 | MTL_IGNORE_WARNINGS = YES; 405 | MTL_OPTIMIZATION_LEVEL = s; 406 | PRODUCT_BUNDLE_IDENTIFIER = "uk.co.flexmonkey.$(PRODUCT_NAME:rfc1034identifier)"; 407 | PRODUCT_NAME = "$(TARGET_NAME)"; 408 | PROVISIONING_PROFILE = ""; 409 | }; 410 | name = Debug; 411 | }; 412 | BE75EEAD1A6A2CC000B20D49 /* Release */ = { 413 | isa = XCBuildConfiguration; 414 | buildSettings = { 415 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 416 | CODE_SIGN_IDENTITY = "iPhone Developer"; 417 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 418 | INFOPLIST_FILE = MetalParticles/Info.plist; 419 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 420 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 421 | MTL_FAST_MATH = YES; 422 | MTL_IGNORE_WARNINGS = YES; 423 | MTL_OPTIMIZATION_LEVEL = s; 424 | PRODUCT_BUNDLE_IDENTIFIER = "uk.co.flexmonkey.$(PRODUCT_NAME:rfc1034identifier)"; 425 | PRODUCT_NAME = "$(TARGET_NAME)"; 426 | PROVISIONING_PROFILE = ""; 427 | }; 428 | name = Release; 429 | }; 430 | BE75EEAF1A6A2CC000B20D49 /* Debug */ = { 431 | isa = XCBuildConfiguration; 432 | buildSettings = { 433 | BUNDLE_LOADER = "$(TEST_HOST)"; 434 | FRAMEWORK_SEARCH_PATHS = ( 435 | "$(SDKROOT)/Developer/Library/Frameworks", 436 | "$(inherited)", 437 | ); 438 | GCC_PREPROCESSOR_DEFINITIONS = ( 439 | "DEBUG=1", 440 | "$(inherited)", 441 | ); 442 | INFOPLIST_FILE = MetalParticlesTests/Info.plist; 443 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 444 | PRODUCT_BUNDLE_IDENTIFIER = "uk.co.flexmonkey.$(PRODUCT_NAME:rfc1034identifier)"; 445 | PRODUCT_NAME = "$(TARGET_NAME)"; 446 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MetalParticles.app/MetalParticles"; 447 | }; 448 | name = Debug; 449 | }; 450 | BE75EEB01A6A2CC000B20D49 /* Release */ = { 451 | isa = XCBuildConfiguration; 452 | buildSettings = { 453 | BUNDLE_LOADER = "$(TEST_HOST)"; 454 | FRAMEWORK_SEARCH_PATHS = ( 455 | "$(SDKROOT)/Developer/Library/Frameworks", 456 | "$(inherited)", 457 | ); 458 | INFOPLIST_FILE = MetalParticlesTests/Info.plist; 459 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 460 | PRODUCT_BUNDLE_IDENTIFIER = "uk.co.flexmonkey.$(PRODUCT_NAME:rfc1034identifier)"; 461 | PRODUCT_NAME = "$(TARGET_NAME)"; 462 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MetalParticles.app/MetalParticles"; 463 | }; 464 | name = Release; 465 | }; 466 | /* End XCBuildConfiguration section */ 467 | 468 | /* Begin XCConfigurationList section */ 469 | BE75EE871A6A2CC000B20D49 /* Build configuration list for PBXProject "MetalParticles" */ = { 470 | isa = XCConfigurationList; 471 | buildConfigurations = ( 472 | BE75EEA91A6A2CC000B20D49 /* Debug */, 473 | BE75EEAA1A6A2CC000B20D49 /* Release */, 474 | ); 475 | defaultConfigurationIsVisible = 0; 476 | defaultConfigurationName = Release; 477 | }; 478 | BE75EEAB1A6A2CC000B20D49 /* Build configuration list for PBXNativeTarget "MetalParticles" */ = { 479 | isa = XCConfigurationList; 480 | buildConfigurations = ( 481 | BE75EEAC1A6A2CC000B20D49 /* Debug */, 482 | BE75EEAD1A6A2CC000B20D49 /* Release */, 483 | ); 484 | defaultConfigurationIsVisible = 0; 485 | defaultConfigurationName = Release; 486 | }; 487 | BE75EEAE1A6A2CC000B20D49 /* Build configuration list for PBXNativeTarget "MetalParticlesTests" */ = { 488 | isa = XCConfigurationList; 489 | buildConfigurations = ( 490 | BE75EEAF1A6A2CC000B20D49 /* Debug */, 491 | BE75EEB01A6A2CC000B20D49 /* Release */, 492 | ); 493 | defaultConfigurationIsVisible = 0; 494 | defaultConfigurationName = Release; 495 | }; 496 | /* End XCConfigurationList section */ 497 | }; 498 | rootObject = BE75EE841A6A2CC000B20D49 /* Project object */; 499 | } 500 | --------------------------------------------------------------------------------