From 2b3a0dce76112232bca21cc5f89dbe97121b6543 Mon Sep 17 00:00:00 2001 From: Marius Horga Date: Tue, 31 Oct 2017 22:37:06 -0500 Subject: [PATCH] updated to Swift 4 --- particles/particle2.playground/Contents.swift | 10 ++ .../Resources/Shaders.metal | 32 ++++++ .../Sources/MetalViewDelegate.swift | 98 ++++++++++++++++++ .../contents.xcplayground | 4 + .../contents.xcworkspacedata | 7 ++ .../UserInterfaceState.xcuserstate | Bin 0 -> 19412 bytes 6 files changed, 151 insertions(+) create mode 100644 particles/particle2.playground/Contents.swift create mode 100644 particles/particle2.playground/Resources/Shaders.metal create mode 100644 particles/particle2.playground/Sources/MetalViewDelegate.swift create mode 100644 particles/particle2.playground/contents.xcplayground create mode 100644 particles/particle2.playground/playground.xcworkspace/contents.xcworkspacedata create mode 100644 particles/particle2.playground/playground.xcworkspace/xcuserdata/marius.xcuserdatad/UserInterfaceState.xcuserstate diff --git a/particles/particle2.playground/Contents.swift b/particles/particle2.playground/Contents.swift new file mode 100644 index 0000000..abcee84 --- /dev/null +++ b/particles/particle2.playground/Contents.swift @@ -0,0 +1,10 @@ + +import MetalKit +import PlaygroundSupport + +let frame = NSRect(x: 0, y: 0, width: 400, height: 400) +let delegate = MetalViewDelegate() +let view = MTKView(frame: frame, device: delegate.device) +view.clearColor = MTLClearColor(red: 0.9, green: 0.9, blue: 0.9, alpha: 1) +view.delegate = delegate +PlaygroundPage.current.liveView = view diff --git a/particles/particle2.playground/Resources/Shaders.metal b/particles/particle2.playground/Resources/Shaders.metal new file mode 100644 index 0000000..b392e96 --- /dev/null +++ b/particles/particle2.playground/Resources/Shaders.metal @@ -0,0 +1,32 @@ + +#include +using namespace metal; + +struct VertexIn { + float4 position [[attribute(0)]]; +}; + +struct VertexOut { + float4 position [[position]]; + float4 color; +}; + +struct Particle { + float4x4 initial_matrix; + float4x4 matrix; + float4 color; +}; + +vertex VertexOut vertex_main(const VertexIn vertex_in [[stage_in]], + constant Particle *particles [[buffer(1)]], + uint instanceid [[instance_id]]) { + VertexOut vertex_out; + Particle particle = particles[instanceid]; + vertex_out.position = particle.matrix * vertex_in.position ; + vertex_out.color = particle.color; + return vertex_out; +} + +fragment float4 fragment_main(VertexOut vertex_in [[stage_in]]) { + return vertex_in.color; +} diff --git a/particles/particle2.playground/Sources/MetalViewDelegate.swift b/particles/particle2.playground/Sources/MetalViewDelegate.swift new file mode 100644 index 0000000..212a1b3 --- /dev/null +++ b/particles/particle2.playground/Sources/MetalViewDelegate.swift @@ -0,0 +1,98 @@ + +import MetalKit + +public class MetalViewDelegate: NSObject, MTKViewDelegate { + + public var device: MTLDevice! + var queue: MTLCommandQueue! + var pipelineState: MTLRenderPipelineState! + var model: MTKMesh! + var particles: [Particle]! + var particlesBuffer: MTLBuffer! + var timer: Float = 0 + + struct Particle { + var initialMatrix = matrix_identity_float4x4 + var matrix = matrix_identity_float4x4 + var color = float4() + } + + override public init() { + super.init() + initializeMetal() + } + + func initializeBuffers() { + particles = [Particle](repeatElement(Particle(), count: 1000)) + particlesBuffer = device.makeBuffer(length: particles.count * MemoryLayout.stride, options: [])! + var pointer = particlesBuffer.contents().bindMemory(to: Particle.self, capacity: particles.count) + for _ in particles.enumerated() { + pointer.pointee.initialMatrix = translate(by: [Float(drand48()) / 10, Float(drand48()) * 10, 0]) + pointer.pointee.color = float4(0.2, 0.6, 0.9, 1) + pointer = pointer.advanced(by: 1) + } + let allocator = MTKMeshBufferAllocator(device: device) + let sphere = MDLMesh(sphereWithExtent: [0.01, 0.01, 0.01], segments: [8, 8], inwardNormals: false, geometryType: .triangles, allocator: allocator) + do { + model = try MTKMesh(mesh: sphere, device: device) + } catch let e { + Swift.print("\(e)") + } + } + + func initializeMetal() { + device = MTLCreateSystemDefaultDevice() + queue = device.makeCommandQueue() + initializeBuffers() + let library: MTLLibrary + do { + let path = Bundle.main.path(forResource: "Shaders", ofType: "metal") + let source = try String(contentsOfFile: path!, encoding: .utf8) + library = try device.makeLibrary(source: source, options: nil) + let descriptor = MTLRenderPipelineDescriptor() + descriptor.colorAttachments[0].pixelFormat = .bgra8Unorm + descriptor.vertexFunction = library.makeFunction(name: "vertex_main") + descriptor.fragmentFunction = library.makeFunction(name: "fragment_main") + descriptor.vertexDescriptor = MTKMetalVertexDescriptorFromModelIO(model.vertexDescriptor) + pipelineState = try device.makeRenderPipelineState(descriptor: descriptor) + } catch let error as NSError { + fatalError("library error: " + error.description) + } + } + + func translate(by: float3) -> float4x4 { + return float4x4(columns: ( + float4( 1, 0, 0, 0), + float4( 0, 1, 0, 0), + float4( 0, 0, 1, 0), + float4( by.x, by.y, by.z, 1) + )) + } + + func update() { + timer += 0.01 + var pointer = particlesBuffer.contents().bindMemory(to: Particle.self, capacity: particles.count) + for _ in particles { + pointer.pointee.matrix = translate(by: [0, -3 * timer, 0]) * pointer.pointee.initialMatrix + pointer = pointer.advanced(by: 1) + } + } + + public func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) { } + + public func draw(in view: MTKView) { + update() + guard let commandBuffer = queue.makeCommandBuffer(), + let descriptor = view.currentRenderPassDescriptor, + let commandEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: descriptor), + let drawable = view.currentDrawable else { fatalError() } + let submesh = model.submeshes[0] + commandEncoder.setRenderPipelineState(pipelineState) + commandEncoder.setVertexBuffer(model.vertexBuffers[0].buffer, offset: 0, index: 0) + commandEncoder.setVertexBuffer(particlesBuffer, offset: 0, index: 1) + commandEncoder.drawIndexedPrimitives(type: .triangle, indexCount: submesh.indexCount, indexType: submesh.indexType, indexBuffer: submesh.indexBuffer.buffer, indexBufferOffset: 0, instanceCount: particles.count) + commandEncoder.endEncoding() + commandBuffer.present(drawable) + commandBuffer.commit() + } +} diff --git a/particles/particle2.playground/contents.xcplayground b/particles/particle2.playground/contents.xcplayground new file mode 100644 index 0000000..a93d484 --- /dev/null +++ b/particles/particle2.playground/contents.xcplayground @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/particles/particle2.playground/playground.xcworkspace/contents.xcworkspacedata b/particles/particle2.playground/playground.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/particles/particle2.playground/playground.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/particles/particle2.playground/playground.xcworkspace/xcuserdata/marius.xcuserdatad/UserInterfaceState.xcuserstate b/particles/particle2.playground/playground.xcworkspace/xcuserdata/marius.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000000000000000000000000000000000000..d02d1fb3b8a4e299df147a6c3e5afb1dc60aba19 GIT binary patch literal 19412 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 VpPk+;8z3aSGna*T-@ko!{U3Z-iF5z} literal 0 HcmV?d00001