~connorbell/Physarum-Metal

f59ed061df107d5174fd30a510fb68f419d5c7b2 — Connor Bell 3 years ago 93f1963
added post fx and particles add over several frames in a spiral
M Physarum.xcodeproj/project.xcworkspace/xcuserdata/connorbell.xcuserdatad/UserInterfaceState.xcuserstate => Physarum.xcodeproj/project.xcworkspace/xcuserdata/connorbell.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
M Physarum/MTLTexture+Z.swift => Physarum/MTLTexture+Z.swift +30 -27
@@ 42,36 42,39 @@ extension MTLTexture {
	typealias XImage = NSImage
	#endif

	var cgImage: CGImage? {
    var cgImage: CGImage? {

		assert(self.pixelFormat == .bgra8Unorm)
	
		// read texture as byte array
		let rowBytes = self.width * 4
		let length = rowBytes * self.height
		let bgraBytes = [UInt8](repeating: 0, count: length)
		let region = MTLRegionMake2D(0, 0, self.width, self.height)
		self.getBytes(UnsafeMutableRawPointer(mutating: bgraBytes), bytesPerRow: rowBytes, from: region, mipmapLevel: 0)
        assert(self.pixelFormat == .bgra8Unorm)
    
        // read texture as byte array
        let rowBytes = self.width * 4
        let length = rowBytes * self.height
        let bgraBytes = [UInt8](repeating: 0, count: length)
        let region = MTLRegionMake2D(0, 0, self.width, self.height)
        self.getBytes(UnsafeMutableRawPointer(mutating: bgraBytes), bytesPerRow: rowBytes, from: region, mipmapLevel: 0)

		// use Accelerate framework to convert from BGRA to RGBA
		var bgraBuffer = vImage_Buffer(data: UnsafeMutableRawPointer(mutating: bgraBytes),
					height: vImagePixelCount(self.height), width: vImagePixelCount(self.width), rowBytes: rowBytes)
		let rgbaBytes = [UInt8](repeating: 0, count: length)
		var rgbaBuffer = vImage_Buffer(data: UnsafeMutableRawPointer(mutating: rgbaBytes),
					height: vImagePixelCount(self.height), width: vImagePixelCount(self.width), rowBytes: rowBytes)
		let map: [UInt8] = [2, 1, 0, 3]
		vImagePermuteChannels_ARGB8888(&bgraBuffer, &rgbaBuffer, map, 0)
        // use Accelerate framework to convert from BGRA to RGBA
        var bgraBuffer = vImage_Buffer(data: UnsafeMutableRawPointer(mutating: bgraBytes),
                    height: vImagePixelCount(self.height), width: vImagePixelCount(self.width), rowBytes: rowBytes)
        let rgbaBytes = [UInt8](repeating: 0, count: length)
        var rgbaBuffer = vImage_Buffer(data: UnsafeMutableRawPointer(mutating: rgbaBytes),
                    height: vImagePixelCount(self.height), width: vImagePixelCount(self.width), rowBytes: rowBytes)
        let map: [UInt8] = [2, 1, 0, 3]
        vImagePermuteChannels_ARGB8888(&bgraBuffer, &rgbaBuffer, map, 0)

		// create CGImage with RGBA
		let colorScape = CGColorSpaceCreateDeviceRGB()
		let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue)
		guard let data = CFDataCreate(nil, bgraBytes, length) else { return nil }
		guard let dataProvider = CGDataProvider(data: data) else { return nil }
		let cgImage = CGImage(width: self.width, height: self.height, bitsPerComponent: 8, bitsPerPixel: 32, bytesPerRow: rowBytes,
					space: colorScape, bitmapInfo: bitmapInfo, provider: dataProvider,
					decode: nil, shouldInterpolate: true, intent: .defaultIntent)
		return cgImage
	}
        // flipping image virtically
        let flippedBytes = rgbaBytes // share the buffer

        // create CGImage with RGBA
        let colorScape = CGColorSpaceCreateDeviceRGB()
        let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue)
        guard let data = CFDataCreate(nil, flippedBytes, length) else { return nil }
        guard let dataProvider = CGDataProvider(data: data) else { return nil }
        let cgImage = CGImage(width: self.width, height: self.height, bitsPerComponent: 8, bitsPerPixel: 32, bytesPerRow: rowBytes,
                    space: colorScape, bitmapInfo: bitmapInfo, provider: dataProvider,
                    decode: nil, shouldInterpolate: true, intent: .defaultIntent)
        return cgImage
    }

	var image: XImage? {
		guard let cgImage = self.cgImage else { return nil }

M Physarum/Particle.h => Physarum/Particle.h +1 -0
@@ 6,6 6,7 @@
typedef struct {
    float sensorHeading;    
    vector_float2 position;
    int active;
} Particle;

#endif

M Physarum/Shaders.metal => Physarum/Shaders.metal +201 -31
@@ 16,19 16,142 @@ float random(float n) {
    return fract(sin(n) * 43758.5453123);
}

float3 hsv2rgb(float3 c)
// glsl mod <3
float2 mod(float2 x, float2 y)
{
    const float4 K = float4(1.0, 0.66667, 0.33333, 3.0);
    float3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
    return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
    return x - y * floor(x/y);
}

// glsl mod <3
float2 mod(float2 x, float2 y)
float mod(float x, float y)
{
    return x - y * floor(x/y);
}

float3 mod(float3 x, float3 y)
{
    return x - y * floor(x/y);
}

float4 mod(float4 x, float4 y)
{
    return x - y * floor(x/y);
}

float3 hueShift(float3 color, float hue) {
    const float3 k = float3(0.57735, 0.57735, 0.57735);
    float cosAngle = cos(hue);
    return float3(color * cosAngle + cross(k, color) * sin(hue) + k * dot(k, color) * (1.0 - cosAngle));
}

float4 permute(float4 x){return mod(((x*34.0)+1.0)*x, 289.0);}
float4 taylorInvSqrt(float4 r){return 1.79284291400159 - 0.85373472095314 * r;}

float snoise(float3 v){
  const float2  C = float2(1.0/6.0, 1.0/3.0) ;
  const float4  D = float4(0.0, 0.5, 1.0, 2.0);

// First corner
  float3 i  = floor(v + dot(v, C.yyy) );
  float3 x0 =   v - i + dot(i, C.xxx) ;

// Other corners
  float3 g = step(x0.yzx, x0.xyz);
  float3 l = 1.0 - g;
  float3 i1 = min( g.xyz, l.zxy );
  float3 i2 = max( g.xyz, l.zxy );

  //  x0 = x0 - 0. + 0.0 * C
  float3 x1 = x0 - i1 + 1.0 * C.xxx;
  float3 x2 = x0 - i2 + 2.0 * C.xxx;
  float3 x3 = x0 - 1. + 3.0 * C.xxx;

// Permutations
  i = mod(i, 289.0 );
  float4 p = permute( permute( permute(
             i.z + float4(0.0, i1.z, i2.z, 1.0 ))
           + i.y + float4(0.0, i1.y, i2.y, 1.0 ))
           + i.x + float4(0.0, i1.x, i2.x, 1.0 ));

// Gradients
// ( N*N points uniformly over a square, mapped onto an octahedron.)
  float n_ = 1.0/7.0; // N=7
  float3  ns = n_ * D.wyz - D.xzx;

  float4 j = p - 49.0 * floor(p * ns.z *ns.z);  //  mod(p,N*N)

  float4 x_ = floor(j * ns.z);
  float4 y_ = floor(j - 7.0 * x_ );    // mod(j,N)

  float4 x = x_ *ns.x + ns.yyyy;
  float4 y = y_ *ns.x + ns.yyyy;
  float4 h = 1.0 - abs(x) - abs(y);

  float4 b0 = float4( x.xy, y.xy );
  float4 b1 = float4( x.zw, y.zw );

  float4 s0 = floor(b0)*2.0 + 1.0;
  float4 s1 = floor(b1)*2.0 + 1.0;
  float4 sh = -step(h, float4(0.0));

  float4 a0 = b0.xzyw + s0.xzyw*sh.xxyy ;
  float4 a1 = b1.xzyw + s1.xzyw*sh.zzww ;

  float3 p0 = float3(a0.xy,h.x);
  float3 p1 = float3(a0.zw,h.y);
  float3 p2 = float3(a1.xy,h.z);
  float3 p3 = float3(a1.zw,h.w);

//Normalise gradients
  float4 norm = taylorInvSqrt(float4(dot(p0,p0), dot(p1,p1), dot(p2, p2), dot(p3,p3)));
  p0 *= norm.x;
  p1 *= norm.y;
  p2 *= norm.z;
  p3 *= norm.w;

// Mix final noise value
  float4 m = max(0.6 - float4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0);
  m = m * m;
  return 42.0 * dot( m*m, float4( dot(p0,x0), dot(p1,x1),
                                dot(p2,x2), dot(p3,x3) ) );
}

float3 curl(float3 pos) {
    float3 eps = float3(1., 0., 0.);
    float3 res = float3(0.);

    float yxy1 = snoise(pos + eps.yxy);
    float yxy2 = snoise(pos - eps.yxy);
    float a = (yxy1 - yxy2) / (2. * eps.x);

    float yyx1 = snoise(pos + eps.yyx);
    float yyx2 = snoise(pos - eps.yyx);
    float b = (yyx1 - yyx2) / (2. * eps.x);

    res.x = a - b;

    a = (yyx1 - yyx2) / (2. * eps.x);

    float xyy1 = snoise(pos + eps.xyy);
    float xyy2 = snoise(pos - eps.xyy);
    b = (xyy1 - xyy2) / (2. * eps.x);

    res.y = a - b;

    a = (xyy1 - xyy2) / (2. * eps.x);
    b = (yxy1 - yxy2) / (2. * eps.x);

    res.z = a - b;

    return res;
}

float3 hsv2rgb(float3 c)
{
    const float4 K = float4(1.0, 0.66667, 0.33333, 3.0);
    float3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
    return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}

kernel void setupParticles(device Particle *particles [[buffer(0)]],
                           device const int &width [[ buffer(1) ]],
                           device const int &height [[ buffer(2) ]],


@@ 38,11 161,37 @@ kernel void setupParticles(device Particle *particles [[buffer(0)]],
{
    uint id = tgPos * tPerTg + tPos;
    Particle p = particles[id];
    p.sensorHeading = random(id/803.134234)*26.28318;
    p.position = float2(width/2.0,height/2.0) + float2(cos(p.sensorHeading), sin(p.sensorHeading)) * (1.+random(id/150.52342309)*500);
    p.active = 0;
    p.position = float2(width/2.0,height/2.0) + (float2(cos(id*0.00235), sin(id*0.0059022))*(250+100));

    p.sensorHeading = random(id*0.034) * 6.28318;

    particles[id] = p;
}

kernel void addParticles(device Particle *particles [[buffer(0)]],
                         device const int &width [[ buffer(1) ]],
                         device const int &height [[ buffer(2) ]],
                         device const float &startupProgress [[ buffer(3) ]],
                         device const int &offset [[ buffer(4) ]],
                         const uint tgPos [[ threadgroup_position_in_grid ]],
                         const uint tPerTg [[ threads_per_threadgroup ]],
                         const uint tPos [[ thread_position_in_threadgroup ]])
{
    uint id = tgPos * tPerTg + tPos + uint(offset);
    Particle particle = particles[id];
    particle.active = 1;
    float halfWidth = width / 2.0;
    float startX = width/4.0;
    float2 pos = float2(halfWidth + cos(-.5+startupProgress * 3.14159 * 3.5) * halfWidth * (1.-startupProgress) * 0.75,
                        height/2.0 + sin(-.5+startupProgress * 3.14159 * 3.5) * halfWidth* (1.-startupProgress)  * 0.75);
    
    particle.position = pos;
    particle.sensorHeading = -.5 + 3.14159 + startupProgress*3.14159*3.5 + random(id*0.104) ;
    particles[id] = particle;
}


kernel void updateParticles(device Particle *particles [[buffer(0)]],
                            device const float &time [[ buffer(1) ]],
                            device SimParameters *parameters [[buffer(2)]],


@@ 58,8 207,13 @@ kernel void updateParticles(device Particle *particles [[buffer(0)]],
    
    // Create a copy of the current particle
    uint id = tgPos * tPerTg + tPos;
    
    if (id >= 131072*2) return;
    Particle p = particles[id];
    uint2 texCoord = uint2(floor(p.position));
    
    if (p.active == 0) return;
    
    uint2 texCoord = uint2(p.position);
    
    float sensorAngle = parameters->sensorAngle;
    // Create coordinates for the sensors to sample the trail map


@@ 69,10 223,10 @@ kernel void updateParticles(device Particle *particles [[buffer(0)]],
    float ccA = p.sensorHeading - sensorAngle;
    float2 coordCounterClockwise = mod(p.position.xy + float2(cos(ccA), sin(ccA))*parameters->sensorDistance, viewSize);
    
    float trailMapSampleForward = inTrailMapTexture.read((uint2)floor(coordForward)).r;
    float trailMapSampleClockwise = inTrailMapTexture.read((uint2)floor(coordClockwise)).r;
    float trailMapSampleCounterClockwise = inTrailMapTexture.read((uint2)floor(coordCounterClockwise)).r;
    float4 color = mix(float4(0., 0., 0., 1.0), float4(1., 1., 1., 1.0), saturate(time*0.025));
    float trailMapSampleForward = inTrailMapTexture.read((uint2)round(coordForward)).r;
    float trailMapSampleClockwise = inTrailMapTexture.read((uint2)round(coordClockwise)).r;
    float trailMapSampleCounterClockwise = inTrailMapTexture.read((uint2)round(coordCounterClockwise)).r;
    float4 color = mix(float4(0., 0., 0., 1.0), float4(1., 1., 1., 1.0), saturate(time*0.1));

    // Do nothing and move forward if most concentrated forward
    if (trailMapSampleForward > trailMapSampleClockwise && trailMapSampleForward > trailMapSampleCounterClockwise) {


@@ 80,8 234,7 @@ kernel void updateParticles(device Particle *particles [[buffer(0)]],
    }
    // Rotate Randomly if not conentrated in center
    else if (trailMapSampleClockwise > trailMapSampleForward && trailMapSampleCounterClockwise > trailMapSampleForward) {
        p.sensorHeading += random(p.position.x*1.333 + p.position.y*0.552) > 0.5 ? parameters->rotationAngle : -parameters->rotationAngle;

        p.sensorHeading += random(id + time) > 0.5 ? parameters->rotationAngle : -parameters->rotationAngle;
    }
    // Rotate clockwise
    else if (trailMapSampleCounterClockwise < trailMapSampleClockwise) {


@@ 92,15 245,16 @@ kernel void updateParticles(device Particle *particles [[buffer(0)]],
        p.sensorHeading -= parameters->rotationAngle;
    }
    
    // Write particle color
    outTexture.write(color, texCoord);
    outTexture.write(color, clamp(texCoord + uint2(0,1), uint2(0.), uint2(viewSize)));
    outTexture.write(color, clamp(texCoord + uint2(1,1), uint2(0.), uint2(viewSize)));
    outTexture.write(color, clamp(texCoord + uint2(1,0), uint2(0.), uint2(viewSize)));
    outTexture.write(color, clamp(texCoord + uint2(1,-1), uint2(0.), uint2(viewSize)));
    outTexture.write(color, clamp(texCoord + uint2(0,-1), uint2(0.), uint2(viewSize)));
    outTexture.write(color, clamp(texCoord + uint2(-1,-1), uint2(0.), uint2(viewSize)));
    outTexture.write(color, clamp(texCoord + uint2(-1,0), uint2(0.), uint2(viewSize)));
    outTexture.write(color, clamp(texCoord + uint2(-1,1), uint2(0.), uint2(viewSize)));
    outTexture.write(color, clamp((uint2)(int2(texCoord) + int2(0,1)), uint2(0.), uint2(viewSize)));
    outTexture.write(color, clamp((uint2)(int2(texCoord) + int2(1,1)), uint2(0.), uint2(viewSize)));
    outTexture.write(color, clamp((uint2)(int2(texCoord) + int2(1,0)), uint2(0.), uint2(viewSize)));
    outTexture.write(color, clamp((uint2)(int2(texCoord) + int2(1,-1)), uint2(0.), uint2(viewSize)));
    outTexture.write(color, clamp((uint2)(int2(texCoord) + int2(0,-1)), uint2(0.), uint2(viewSize)));
    outTexture.write(color, clamp((uint2)(int2(texCoord) + int2(-1,-1)), uint2(0.), uint2(viewSize)));
    outTexture.write(color, clamp((uint2)(int2(texCoord) + int2(-1,0)), uint2(0.), uint2(viewSize)));
    outTexture.write(color, clamp((uint2)(int2(texCoord) + int2(-1,1)), uint2(0.), uint2(viewSize)));
    
    // Deposit
    float trailMapSample = inTrailMapTexture.read(texCoord).r;


@@ 108,7 262,7 @@ kernel void updateParticles(device Particle *particles [[buffer(0)]],
    outTrailMapTexture.write(float4(trailMapSample, 1., 0., 1.0), texCoord);
    
    // Move
    p.position.xy = mod(p.position.xy + float2(cos(p.sensorHeading), sin(p.sensorHeading))*(parameters->movementOffset +  random(id/520.421235) * parameters->movementOffsetRandomness), viewSize);
    p.position.xy = mod(p.position.xy + float2(cos(p.sensorHeading), sin(p.sensorHeading))*(parameters->movementOffset +  random(id*520.421235) * parameters->movementOffsetRandomness), viewSize);

    particles[id] = p;
}


@@ 119,39 273,55 @@ kernel void texturePass(texture2d<float, access::sample> inTexture [[texture(0)]
                        texture2d<float, access::sample> inTrailTexture [[texture(2)]],
                        texture2d<float, access::write> outTrailTexture [[texture(3)]],
                        device TexturePassParameters *parameters [[buffer(0)]],
                        device const float &time [[buffer(1)]],
                        uint2 gid [[ thread_position_in_grid ]]) {

    constexpr sampler colorSampler(coord::normalized,
                                   address::repeat,
                                   filter::linear);
    // Compute weighted sum of box of surrounding pixels
    
    // Compute weighted sum of surrounding box
    float3 col = float3(0.);
    float3 colTrail = float3(0.);
    float2 size = float2(inTexture.get_width(), inTexture.get_height());
    float2 texel = 1. / size;
    float2 uv = (float2)gid / size;
    float3 weight = mix(float3(0.11111111), float3(0.1111111111, 0.1, 0.075), saturate(length(uv*2-1)));
    
    // Non uniform weights to created colored feedback.
    float3 weight = mix(float3(0.11111111), float3(0.1111111111, 0.111111, 0.075).grb, saturate(2*length(uv*2-1)));

    for (float i = -1; i <= 1; i++ ){
        for (float j = -1; j <= 1; j++ ){
            float3 val = inTexture.sample(colorSampler, uv + float2(i,j) * texel).rgb;
    for (int i = -1; i <= 1; i++) {
        for (int j = -1; j <= 1; j++) {
            float3 val = inTexture.read((uint2)(int2(gid) + int2(i,j))).rgb;
            col += val * weight;
            
            val = inTrailTexture.sample(colorSampler, uv + float2(i,j) * texel).rgb;
            val = inTrailTexture.read((uint2)(int2(gid) + int2(i,j))).rgb;
            colTrail += val * weight;
        }
    }

    // Add trail for mouse position
    if (length(uv - parameters->mousePosition) < 0.025) {
    if (length(uv - parameters->mousePosition) < 0.05) {
        colTrail = float3(1.0, 0.0, 0.0);
    }
    
    // Dampen
    col.rgb *= parameters->additiveBlendFrameFactor;
    colTrail *= parameters->additiveBlendTrailFactor;
    
    col.rgb = hueShift(col.rgb, 0.175);
    colTrail = clamp(colTrail, float3(0.01), float3(1.0));

    outTexture.write(float4(col, 1.0), gid);
    outTrailTexture.write(float4(colTrail, 1.0), gid);
}


kernel void postFx(texture2d<float, access::sample> inTexture [[texture(0)]],
                   texture2d<float, access::write> outTexture [[texture(1)]],
                   uint2 gid [[ thread_position_in_grid ]]) {

    float4 col = inTexture.read(gid);
    col.rgb = 1.0 - col.rgb;
    outTexture.write(col, gid);
}

M Physarum/Simulation.swift => Physarum/Simulation.swift +77 -13
@@ 12,11 12,14 @@ import MetalPerformanceShaders

class Simulation: MTKView {
    
    // Parameters 
    var simulationParameters: SimParameters
    var texturePassParameters: TexturePassParameters
    var particleCount = 1024*64*2
    var particleCount = 1024*64
    var activeParticles = 0
    var particlesPerFrame = 1024/4
    
    // Metal Devices
    // Metal related variabbles
    private var metalDevice: MTLDevice
    private var commandQueue: MTLCommandQueue
    private var metalLibrary: MTLLibrary


@@ 25,11 28,15 @@ class Simulation: MTKView {
    private var stepFunction: MTLFunction
    private var textureFunction: MTLFunction
    private var initFunction: MTLFunction
    
    private var addParticlesFunction: MTLFunction
    private var postFxFunction: MTLFunction

    // Compute Pipeline States
    private var stepFunctionPipelineState: MTLComputePipelineState
    private var initFunctionPipelineState: MTLComputePipelineState
    private var textureFunctionPipelineState: MTLComputePipelineState
    private var addParticlesFunctionPipelineState: MTLComputePipelineState
    private var postFxPipelineState: MTLComputePipelineState

    // Buffers and Textures
    private var particleBuffer: MTLBuffer


@@ 39,6 46,8 @@ class Simulation: MTKView {
    private var renderTextureSwap: MTLTexture?

    private var frameNumber = 0
    private var frameskip = 2
    private var renderedFrameNumber = 0
    private var mouseDown = false
    
    // Render PNG properties


@@ 57,12 66,16 @@ class Simulation: MTKView {
        self.stepFunction = metalLibrary.makeFunction(name: "updateParticles")!
        self.textureFunction = metalLibrary.makeFunction(name: "texturePass")!
        self.initFunction = metalLibrary.makeFunction(name: "setupParticles")!
        self.addParticlesFunction = metalLibrary.makeFunction(name: "addParticles")!
        self.postFxFunction = metalLibrary.makeFunction(name: "postFx")!
        
        do
        {
            try self.stepFunctionPipelineState = self.metalDevice.makeComputePipelineState(function: self.stepFunction)
            try self.initFunctionPipelineState = self.metalDevice.makeComputePipelineState(function: self.initFunction)
            try self.textureFunctionPipelineState = self.metalDevice.makeComputePipelineState(function: self.textureFunction)
            try self.addParticlesFunctionPipelineState = self.metalDevice.makeComputePipelineState(function: self.addParticlesFunction)
            try self.postFxPipelineState = self.metalDevice.makeComputePipelineState(function: self.postFxFunction)
        }
        catch
        {


@@ 70,8 83,8 @@ class Simulation: MTKView {
        }
        let rect = CGRect(x: 0, y: 0, width: width, height: height)
        
        self.simulationParameters = SimParameters(sensorAngle: 0.125,
                                                  sensorDistance: 5.0,
        self.simulationParameters = SimParameters(sensorAngle: 0.15,
                                                  sensorDistance: 8.0,
                                                  movementOffset: 1.1,
                                                  movementOffsetRandomness: 1.0,
                                                  rotationAngle: 0.5)


@@ 148,6 161,14 @@ extension Simulation {
              let trailMapTexture = self.trailMapTexture,
              let trailMapTextureSwap = self.trailMapTexture else { return }
        
        if activeParticles < particleCount {
            activeParticles += addParticles(commandEncoder: commandEncoder)
            if activeParticles >= particleCount {
                self.simulationParameters.movementOffset += 0.5;
                self.simulationParameters.sensorAngle -= 0.035;
            }
        }
        
        simulate(commandEncoder: commandEncoder)

        dampenAndDecayTextures(drawableIn: renderTextureSwap,


@@ 155,13 176,11 @@ extension Simulation {
                               trailMapIn: trailMapTextureSwap,
                               trailMapOut: trailMapTexture,
                               commandEncoder: commandEncoder)
                
        postFx(commandEncoder: commandEncoder, sourceTexture: renderTexture, targetTexture: currentDrawable.texture)
        
        commandEncoder.endEncoding()
        
        blit(commandBuffer: commandBuffer,
             sourceTexture: renderTexture,
             targetTexture: currentDrawable.texture)
        
        // Swap the textures
        self.renderTexture = self.renderTextureSwap
        self.trailMapTexture = self.trailMapTextureSwap


@@ 172,20 191,49 @@ extension Simulation {
        commandBuffer.commit()
        commandBuffer.waitUntilCompleted()
    
        if renderFrames && self.currentRenderFrame < self.numRenderFrames {
            saveFrame(texture: renderTexture, frame: self.currentRenderFrame, totalFrames: self.numRenderFrames)
        if renderFrames && self.frameNumber % self.frameskip == 0 && self.currentRenderFrame < self.numRenderFrames {
            saveFrame(texture: currentDrawable.texture, frame: self.currentRenderFrame, totalFrames: self.numRenderFrames)
            self.currentRenderFrame += 1
        }
        
        self.frameNumber += 1
    }
    
    // Returns the amount of particles added
    func addParticles(commandEncoder: MTLComputeCommandEncoder) -> Int {
        var width = Int(self.drawableSize.width), height = Int(self.drawableSize.height)
        var startupProgress = Float(activeParticles) / Float(particleCount)
        var offset = activeParticles
        var particlesPerFrame = self.particlesPerFrame
        
        // Make sure we don't go over the amount of allocated particles
        if activeParticles + particlesPerFrame > particleCount {
            particlesPerFrame = particleCount - activeParticles
        }
        
        if particlesPerFrame > 0 {
            commandEncoder.setComputePipelineState(self.addParticlesFunctionPipelineState)
            commandEncoder.setBuffer(self.particleBuffer, offset: 0, index: 0)
            commandEncoder.setBytes(&width, length: MemoryLayout<Int>.size, index: 1)
            commandEncoder.setBytes(&height, length: MemoryLayout<Int>.size, index: 2)
            commandEncoder.setBytes(&startupProgress, length: MemoryLayout<Float>.size, index: 3)
            commandEncoder.setBytes(&offset, length: MemoryLayout<Int>.size, index: 4)

            let threadgroupsPerGrid = MTLSize(width: (particlesPerFrame + self.addParticlesFunctionPipelineState.threadExecutionWidth - 1) / self.addParticlesFunctionPipelineState.threadExecutionWidth, height: 1, depth: 1)

            let threadsPerThreadGroup = MTLSize(width: self.addParticlesFunctionPipelineState.threadExecutionWidth, height: 1, depth: 1)
            
            commandEncoder.dispatchThreadgroups(threadgroupsPerGrid, threadsPerThreadgroup: threadsPerThreadGroup)
        }
        return particlesPerFrame
    }
    
    func simulate(commandEncoder: MTLComputeCommandEncoder) {
        
        let simulationParametersBuffer = metalDevice.makeBuffer(bytes: &simulationParameters, length:MemoryLayout<SimParameters>.stride, options:[])
        
        var frame = Float(self.frameNumber)
        let threadgroupsPerGrid = MTLSize(width: (particleCount + self.stepFunctionPipelineState.threadExecutionWidth - 1) / self.stepFunctionPipelineState.threadExecutionWidth, height: 1, depth: 1)
        let threadgroupsPerGrid = MTLSize(width: (activeParticles + self.stepFunctionPipelineState.threadExecutionWidth - 1) / self.stepFunctionPipelineState.threadExecutionWidth, height: 1, depth: 1)

        let threadsPerThreadgroup = MTLSize(width: self.initFunctionPipelineState.threadExecutionWidth, height: 1, depth: 1)
        


@@ 225,7 273,8 @@ extension Simulation {
        }
        
        let textureParametersBuffer = metalDevice.makeBuffer(bytes: &texturePassParameters, length:MemoryLayout<TexturePassParameters>.stride, options:[])
        
        var frame = Float(self.frameNumber)

        commandEncoder.setComputePipelineState(self.textureFunctionPipelineState)
        
        commandEncoder.setTexture(drawableIn, index: 0)


@@ 233,6 282,7 @@ extension Simulation {
        commandEncoder.setTexture(trailMapIn, index: 2)
        commandEncoder.setTexture(trailMapOut, index: 3)
        commandEncoder.setBuffer(textureParametersBuffer, offset: 0, index: 0)
        commandEncoder.setBytes(&frame, length: MemoryLayout<Float>.size, index: 1)

        let w = textureFunctionPipelineState.threadExecutionWidth
        let h = textureFunctionPipelineState.maxTotalThreadsPerThreadgroup / w


@@ 250,6 300,20 @@ extension Simulation {
        
        blitEncoder?.endEncoding()
    }
    
    func postFx(commandEncoder: MTLComputeCommandEncoder, sourceTexture: MTLTexture, targetTexture: MTLTexture) {
        commandEncoder.setComputePipelineState(self.postFxPipelineState)
        
        commandEncoder.setTexture(sourceTexture, index: 0)
        commandEncoder.setTexture(targetTexture, index: 1)

        let w = postFxPipelineState.threadExecutionWidth
        let h = postFxPipelineState.maxTotalThreadsPerThreadgroup / w
        
        let threadsPerThreadGroup = MTLSize(width: w, height: h, depth: 1)
        let threadsPerGrid = MTLSize(width: sourceTexture.width, height: sourceTexture.height, depth: 1)
        commandEncoder.dispatchThreads(threadsPerGrid, threadsPerThreadgroup: threadsPerThreadGroup)
    }
}

// MARK: - Updating Parameters