If you enjoyed my recent experiment implementing Hiroki Sayama's Swarm Chemistry in Swift and Metal, you may be interested to know that I've added a very basic user interface to the app so that users can select from the three genomes and update the parameters of that genome.
The simulation in this demonstration contains 4,096 swarm members and hovers around 33fps on my iPad Air 2. Because all the particle calculations are done on the GPU and GPU to CPU image creation is done in a background thread, the user interface remains totally smooth and responsive.
Adding the user interface components didn't take long.
First, I created a SwarmGenome struct in the view controller:
struct SwarmGenome
{
var radius: Float = 0
var c1_cohesion: Float = 0
var c2_alignment: Float = 0
var c3_seperation: Float = 0
var c4_steering: Float = 0
var c5_paceKeeping: Float = 0
}
...and created three instances of it for the three species:
var redGenome = SwarmGenome(radius: 0.4, c1_cohesion: 0.25, c2_alignment: 0.35, c3_seperation: 0.05, c4_steering: 0.35, c5_paceKeeping: 0.75)
var greenGenome = SwarmGenome(radius: 0.5, c1_cohesion: 0.165, c2_alignment: 0.5, c3_seperation: 0.2, c4_steering: 0.25, c5_paceKeeping: 0.5)
var blueGenome = SwarmGenome(radius: 0.2, c1_cohesion: 0.45, c2_alignment: 0.8, c3_seperation: 0.075, c4_steering: 0.9, c5_paceKeeping: 0.15)
An array holds these three structs and populates a UISegmentedControl.
I reused my ParameterWidget class from my reaction diffusion app. It's basically a horizontal slider with a label which I create and add to the display in a loop:
let fieldNames = ["Radius", "Cohesion", "Alignment", "Seperation", "Steering", "Pace Keeping"]
for i in0 ..< fieldNames.count
{
let parameterWidget = ParameterWidget(frame: CGRectZero)
parameterWidget.fieldName = fieldNames[i]
parameterWidget.addTarget(self, action: "parameterChangeHandler", forControlEvents: UIControlEvents.ValueChanged)
numericDials.append(parameterWidget)
view.addSubview(parameterWidget)
}
When the user changes any of the sliders, parameterChangeHandler() is invoked which updates both the genome in the array and the struct. Because, unlike classes, structs are passed around by value and not reference, I explicitly set both:
func parameterChangeHandler()
{
genomes[speciesSegmentedControl.selectedSegmentIndex].radius = numericDials[0].value
genomes[speciesSegmentedControl.selectedSegmentIndex].c1_cohesion = numericDials[1].value
genomes[speciesSegmentedControl.selectedSegmentIndex].c2_alignment = numericDials[2].value
genomes[speciesSegmentedControl.selectedSegmentIndex].c3_seperation = numericDials[3].value
genomes[speciesSegmentedControl.selectedSegmentIndex].c4_steering = numericDials[4].value
genomes[speciesSegmentedControl.selectedSegmentIndex].c5_paceKeeping = numericDials[5].value
redGenome = genomes[0]
greenGenome = genomes[1]
blueGenome = genomes[2]
}
The three genomes are then passed to the Metal shader via the setBuffer() method:
let redBuffer: MTLBuffer = device.newBufferWithBytes(&redGenome, length: sizeof(SwarmGenome), options: nil)
commandEncoder.setBuffer(redBuffer, offset: 0, atIndex: 2)
let greenBuffer: MTLBuffer = device.newBufferWithBytes(&greenGenome, length: sizeof(SwarmGenome), options: nil)
commandEncoder.setBuffer(greenBuffer, offset: 0, atIndex: 3)
let blueBuffer: MTLBuffer = device.newBufferWithBytes(&blueGenome, length: sizeof(SwarmGenome), options: nil)
commandEncoder.setBuffer(blueBuffer, offset: 0, atIndex: 4)
...which are now accessible within the shader as arguments to the kernel function:
constant SwarmGenome &genomeOne [[buffer(2)]],
constant SwarmGenome &genomeTwo [[buffer(3)]],
constant SwarmGenome &genomeThree [[buffer(4)]],
The source code is available in my GitHub repository here.