├── 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 | 
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 |
--------------------------------------------------------------------------------