From f9cb66751d722c3bf010a4cf21e45de93d4afd71 Mon Sep 17 00:00:00 2001 From: Marius Horga Date: Thu, 30 Nov 2017 23:25:37 -0600 Subject: [PATCH] added particles part 3 --- README.md | 7 +- particles/particle3.playground/Contents.swift | 9 +++ .../Resources/Shaders.metal | 35 +++++++++ .../Sources/MetalView.swift | 74 ++++++++++++++++++ .../contents.xcplayground | 4 + .../contents.xcworkspacedata | 7 ++ .../UserInterfaceState.xcuserstate | Bin 0 -> 31326 bytes .../particle3.playground/timeline.xctimeline | 6 ++ 8 files changed, 139 insertions(+), 3 deletions(-) create mode 100644 particles/particle3.playground/Contents.swift create mode 100644 particles/particle3.playground/Resources/Shaders.metal create mode 100644 particles/particle3.playground/Sources/MetalView.swift create mode 100644 particles/particle3.playground/contents.xcplayground create mode 100644 particles/particle3.playground/playground.xcworkspace/contents.xcworkspacedata create mode 100644 particles/particle3.playground/playground.xcworkspace/xcuserdata/marius.xcuserdatad/UserInterfaceState.xcuserstate create mode 100644 particles/particle3.playground/timeline.xctimeline diff --git a/README.md b/README.md index b71a78e..5f4115d 100644 --- a/README.md +++ b/README.md @@ -25,10 +25,11 @@ Repository to accompany the following blog posts: - [Shadows in Metal part 1](http://metalkit.org/2017/01/31/shadows-in-metal-part-1.html) - [Shadows in Metal part 2](http://metalkit.org/2017/02/28/shadows-in-metal-part-2.html) - [Ambient Occlusion in Metal](http://metalkit.org/2017/03/22/ambient-occlusion-in-metal.html) -- [Working with memory in Metal](http://metalkit.org/2017/04/30/working-with-memory-in-metal.html) +- [Working with memory in Metal part 1](http://metalkit.org/2017/04/30/working-with-memory-in-metal.html) - [Working with memory in Metal part 2](http://metalkit.org/2017/05/26/working-with-memory-in-metal-part-2.html) - [Introducing Metal 2](http://metalkit.org/2017/06/30/introducing-metal-2.html) -- [Using ARKit with Metal](http://metalkit.org/2017/07/29/using-arkit-with-metal.html) +- [Using ARKit with Metal part 1](http://metalkit.org/2017/07/29/using-arkit-with-metal.html) - [Using ARKit with Metal part 2](http://metalkit.org/2017/08/31/using-arkit-with-metal-part-2.html) -- [Working with Particles in Metal](http://metalkit.org/2017/09/30/working-with-particles-in-metal.html) +- [Working with Particles in Metal part 1](http://metalkit.org/2017/09/30/working-with-particles-in-metal.html) - [Working with Particles in Metal part 2](http://metalkit.org/2017/10/31/working-with-particles-in-metal-part-2.html) +- [Working with Particles in Metal part 3](http://metalkit.org/2017/11/30/working-with-particles-in-metal-part-3.html) \ No newline at end of file diff --git a/particles/particle3.playground/Contents.swift b/particles/particle3.playground/Contents.swift new file mode 100644 index 0000000..fcee78a --- /dev/null +++ b/particles/particle3.playground/Contents.swift @@ -0,0 +1,9 @@ + +import MetalKit +import PlaygroundSupport + +let frame = NSRect(x: 0, y: 0, width: 600, height: 600) +let mView = MetalView() +let view = MTKView(frame: frame, device: mView.device) +view.delegate = mView +PlaygroundPage.current.liveView = view diff --git a/particles/particle3.playground/Resources/Shaders.metal b/particles/particle3.playground/Resources/Shaders.metal new file mode 100644 index 0000000..8a6345c --- /dev/null +++ b/particles/particle3.playground/Resources/Shaders.metal @@ -0,0 +1,35 @@ + +#include +using namespace metal; + +struct Particle { + float2 position; + float2 velocity; +}; + +kernel void firstPass(texture2d output [[texture(0)]], + uint2 id [[thread_position_in_grid]]) { + output.write(half4(0., 0., 0., 1.), id); +} + +kernel void secondPass(texture2d output [[texture(0)]], + device Particle *particles [[buffer(0)]], + uint id [[thread_position_in_grid]]) { + Particle particle = particles[id]; + float2 position = particle.position; + float2 velocity = particle.velocity; + int width = output.get_width(); + int height = output.get_height(); + if (position.x < 0 || position.x > width) { velocity.x *= -1; } + if (position.y < 0 || position.y > height) { velocity.y *= -1; } + position += velocity; + particle.position = position; + particle.velocity = velocity; + particles[id] = particle; + uint2 pos = uint2(position.x, position.y); + output.write(half4(1.), pos); + output.write(half4(1.), pos + uint2( 1, 0)); + output.write(half4(1.), pos + uint2( 0, 1)); + output.write(half4(1.), pos - uint2( 1, 0)); + output.write(half4(1.), pos - uint2( 0, 1)); +} diff --git a/particles/particle3.playground/Sources/MetalView.swift b/particles/particle3.playground/Sources/MetalView.swift new file mode 100644 index 0000000..fc1f7b6 --- /dev/null +++ b/particles/particle3.playground/Sources/MetalView.swift @@ -0,0 +1,74 @@ + +import MetalKit + +struct Particle { + var position: float2 + var velocity: float2 +} + +public class MetalView: NSObject, MTKViewDelegate { + + public var device: MTLDevice! + var queue: MTLCommandQueue! + var firstState: MTLComputePipelineState! + var secondState: MTLComputePipelineState! + var particleBuffer: MTLBuffer! + let particleCount = 10000 + var particles = [Particle]() + let side = 1200 + + override public init() { + super.init() + initializeMetal() + initializeBuffers() + } + + func initializeBuffers() { + for _ in 0 ..< particleCount { + let particle = Particle(position: float2(Float(arc4random() % UInt32(side)), Float(arc4random() % UInt32(side))), velocity: float2((Float(arc4random() % 10) - 5) / 10, (Float(arc4random() % 10) - 5) / 10)) + particles.append(particle) + } + let size = particles.count * MemoryLayout.size + particleBuffer = device.makeBuffer(bytes: &particles, length: size, options: []) + } + + func initializeMetal() { + device = MTLCreateSystemDefaultDevice() + queue = device.makeCommandQueue() + guard let path = Bundle.main.path(forResource: "Shaders", ofType: "metal") else { return } + do { + let input = try String(contentsOfFile: path, encoding: String.Encoding.utf8) + let library = try device.makeLibrary(source: input, options: nil) + guard let firstPass = library.makeFunction(name: "firstPass") else { return } + firstState = try device.makeComputePipelineState(function: firstPass) + guard let secondPass = library.makeFunction(name: "secondPass") else { return } + secondState = try device.makeComputePipelineState(function: secondPass) + } catch let e { print(e) } + } + + public func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {} + + public func draw(in view: MTKView) { + if let drawable = view.currentDrawable, + let commandBuffer = queue.makeCommandBuffer(), + let commandEncoder = commandBuffer.makeComputeCommandEncoder() { + // first pass + commandEncoder.setComputePipelineState(firstState) + commandEncoder.setTexture(drawable.texture, index: 0) + let w = firstState.threadExecutionWidth + let h = firstState.maxTotalThreadsPerThreadgroup / w + let threadsPerGroup = MTLSizeMake(w, h, 1) + var threadsPerGrid = MTLSizeMake(side, side, 1) + commandEncoder.dispatchThreads(threadsPerGrid, threadsPerThreadgroup: threadsPerGroup) + // second pass + commandEncoder.setComputePipelineState(secondState) + commandEncoder.setTexture(drawable.texture, index: 0) + commandEncoder.setBuffer(particleBuffer, offset: 0, index: 0) + threadsPerGrid = MTLSizeMake(particleCount, 1, 1) + commandEncoder.dispatchThreads(threadsPerGrid, threadsPerThreadgroup: threadsPerGroup) + commandEncoder.endEncoding() + commandBuffer.present(drawable) + commandBuffer.commit() + } + } +} diff --git a/particles/particle3.playground/contents.xcplayground b/particles/particle3.playground/contents.xcplayground new file mode 100644 index 0000000..a93d484 --- /dev/null +++ b/particles/particle3.playground/contents.xcplayground @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/particles/particle3.playground/playground.xcworkspace/contents.xcworkspacedata b/particles/particle3.playground/playground.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/particles/particle3.playground/playground.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/particles/particle3.playground/playground.xcworkspace/xcuserdata/marius.xcuserdatad/UserInterfaceState.xcuserstate b/particles/particle3.playground/playground.xcworkspace/xcuserdata/marius.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000000000000000000000000000000000000..29c04114e9da7f035e98cf25825eb672d6ec4793 GIT binary patch literal 31326 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%0D^P-Fl!evn|)W$ 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 V;e@zb525&1vR(XZ{%tt<{{VDTmW}`b literal 0 HcmV?d00001 diff --git a/particles/particle3.playground/timeline.xctimeline b/particles/particle3.playground/timeline.xctimeline new file mode 100644 index 0000000..bf468af --- /dev/null +++ b/particles/particle3.playground/timeline.xctimeline @@ -0,0 +1,6 @@ + + + + +