From f59ed061df107d5174fd30a510fb68f419d5c7b2 Mon Sep 17 00:00:00 2001 From: Connor Bell Date: Fri, 15 Jan 2021 13:46:30 -0500 Subject: [PATCH] added post fx and particles add over several frames in a spiral --- .../UserInterfaceState.xcuserstate | Bin 60669 -> 62665 bytes Physarum/MTLTexture+Z.swift | 57 +++-- Physarum/Particle.h | 1 + Physarum/Shaders.metal | 232 +++++++++++++++--- Physarum/Simulation.swift | 90 ++++++- 5 files changed, 309 insertions(+), 71 deletions(-) diff --git a/Physarum.xcodeproj/project.xcworkspace/xcuserdata/connorbell.xcuserdatad/UserInterfaceState.xcuserstate b/Physarum.xcodeproj/project.xcworkspace/xcuserdata/connorbell.xcuserdatad/UserInterfaceState.xcuserstate index e8788ffe1fc0a003595fdacd4844fb45eeb8b38b..a786c0e8db8be83a8b9bb61776afc68382586e0b 100644 GIT binary patch delta 28249 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 delta 26769 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Ð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 diff --git a/Physarum/MTLTexture+Z.swift b/Physarum/MTLTexture+Z.swift index f9e75a7..1bdb857 100644 --- a/Physarum/MTLTexture+Z.swift +++ b/Physarum/MTLTexture+Z.swift @@ -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 } diff --git a/Physarum/Particle.h b/Physarum/Particle.h index e9ef1f8..e24a3c9 100644 --- a/Physarum/Particle.h +++ b/Physarum/Particle.h @@ -6,6 +6,7 @@ typedef struct { float sensorHeading; vector_float2 position; + int active; } Particle; #endif diff --git a/Physarum/Shaders.metal b/Physarum/Shaders.metal index 3032262..52409b9 100644 --- a/Physarum/Shaders.metal +++ b/Physarum/Shaders.metal @@ -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 inTexture [[texture(0)] texture2d inTrailTexture [[texture(2)]], texture2d 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 inTexture [[texture(0)]], + texture2d outTexture [[texture(1)]], + uint2 gid [[ thread_position_in_grid ]]) { + + float4 col = inTexture.read(gid); + col.rgb = 1.0 - col.rgb; + outTexture.write(col, gid); +} diff --git a/Physarum/Simulation.swift b/Physarum/Simulation.swift index c92f33e..dfe59f6 100644 --- a/Physarum/Simulation.swift +++ b/Physarum/Simulation.swift @@ -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.size, index: 1) + commandEncoder.setBytes(&height, length: MemoryLayout.size, index: 2) + commandEncoder.setBytes(&startupProgress, length: MemoryLayout.size, index: 3) + commandEncoder.setBytes(&offset, length: MemoryLayout.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.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.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.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 -- 2.45.2