From 5885a67b73c17eb627296bff8be1b26d326cae4d Mon Sep 17 00:00:00 2001 From: Marius Horga Date: Wed, 25 May 2016 18:51:07 +0300 Subject: [PATCH] added chapter 13 --- README.md | 3 +- ch13/chapter13.playground/Contents.swift | 8 ++ .../Resources/Shaders.metal | 30 ++++++++ .../Sources/MetalView.swift | 71 ++++++++++++++++++ .../contents.xcplayground | 4 + .../contents.xcworkspacedata | 7 ++ .../UserInterfaceState.xcuserstate | Bin 0 -> 19608 bytes ch13/chapter13.playground/timeline.xctimeline | 6 ++ 8 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 ch13/chapter13.playground/Contents.swift create mode 100755 ch13/chapter13.playground/Resources/Shaders.metal create mode 100755 ch13/chapter13.playground/Sources/MetalView.swift create mode 100644 ch13/chapter13.playground/contents.xcplayground create mode 100755 ch13/chapter13.playground/playground.xcworkspace/contents.xcworkspacedata create mode 100755 ch13/chapter13.playground/playground.xcworkspace/xcuserdata/marius.xcuserdatad/UserInterfaceState.xcuserstate create mode 100644 ch13/chapter13.playground/timeline.xctimeline diff --git a/README.md b/README.md index c6d03ea..4104d77 100644 --- a/README.md +++ b/README.md @@ -13,4 +13,5 @@ Repository to accompany the following blog posts: - [Using MetalKit part 9](http://mhorga.org/2016/04/18/using-metalkit-part-9.html) - [Using MetalKit part 10](http://mhorga.org/2016/05/02/using-metalkit-part-10.html) - [Using MetalKit part 11](http://mhorga.org/2016/05/10/using-metalkit-part-11.html) -- [Using MetalKit part 12](http://mhorga.org/2016/05/18/using-metalkit-part-12.html) \ No newline at end of file +- [Using MetalKit part 12](http://mhorga.org/2016/05/18/using-metalkit-part-12.html) +- [Using MetalKit part 13](http://mhorga.org/2016/05/25/using-metalkit-part-13.html) \ No newline at end of file diff --git a/ch13/chapter13.playground/Contents.swift b/ch13/chapter13.playground/Contents.swift new file mode 100644 index 0000000..366d424 --- /dev/null +++ b/ch13/chapter13.playground/Contents.swift @@ -0,0 +1,8 @@ + +import Cocoa +import XCPlayground + +let device = MTLCreateSystemDefaultDevice()! +let frame = NSRect(x:0, y:0, width:400, height:400) +let view = MetalView(frame: frame, device: device) +XCPlaygroundPage.currentPage.liveView = view diff --git a/ch13/chapter13.playground/Resources/Shaders.metal b/ch13/chapter13.playground/Resources/Shaders.metal new file mode 100755 index 0000000..f06002f --- /dev/null +++ b/ch13/chapter13.playground/Resources/Shaders.metal @@ -0,0 +1,30 @@ + +#include + +using namespace metal; + +kernel void compute(texture2d output [[texture(0)]], + constant float &timer [[buffer(1)]], + constant float2 &mouse [[buffer(2)]], + uint2 gid [[thread_position_in_grid]]) +{ + int width = output.get_width(); + int height = output.get_height(); + float2 uv = float2(gid) / float2(width, height); + uv = uv * 2.0 - 1.0; + float radius = 0.5; + float distance = length(uv) - radius; +// output.write(distance < 0 ? float4(1) : float4(0), gid); + + float planet = float(sqrt(radius * radius - uv.x * uv.x - uv.y * uv.y)); +// planet /= radius; +// output.write(distance < 0 ? float4(planet) : float4(0), gid); + + float3 normal = normalize(float3(uv.x, uv.y, planet)); +// output.write(distance < 0 ? float4(float3(normal), 1) : float4(0), gid); + + float3 source = normalize(float3(cos(timer), sin(timer), 1)); +// float3 source = normalize(float3(-1, 0, 1)); + float light = dot(normal, source); + output.write(distance < 0 ? float4(float3(light), 1) : float4(0), gid); +} diff --git a/ch13/chapter13.playground/Sources/MetalView.swift b/ch13/chapter13.playground/Sources/MetalView.swift new file mode 100755 index 0000000..37c8100 --- /dev/null +++ b/ch13/chapter13.playground/Sources/MetalView.swift @@ -0,0 +1,71 @@ + +import MetalKit + +public class MetalView: MTKView, NSWindowDelegate { + + var queue: MTLCommandQueue! = nil + var cps: MTLComputePipelineState! = nil + var timer: Float = 0 + var timerBuffer: MTLBuffer! + var mouseBuffer: MTLBuffer! + var pos: NSPoint! + + override public func mouseDown(event: NSEvent) { + pos = convertPointToLayer(convertPoint(event.locationInWindow, fromView: nil)) + let scale = layer!.contentsScale + pos.x *= scale + pos.y *= scale + } + + required public init(coder: NSCoder) { + super.init(coder: coder) + } + + override public init(frame frameRect: CGRect, device: MTLDevice?) { + super.init(frame: frameRect, device: device) + registerShaders() + } + + override public func drawRect(dirtyRect: NSRect) { + super.drawRect(dirtyRect) + if let drawable = currentDrawable { + let commandBuffer = queue.commandBuffer() + let commandEncoder = commandBuffer.computeCommandEncoder() + commandEncoder.setComputePipelineState(cps) + commandEncoder.setTexture(drawable.texture, atIndex: 0) + commandEncoder.setBuffer(mouseBuffer, offset: 0, atIndex: 2) + commandEncoder.setBuffer(timerBuffer, offset: 0, atIndex: 1) + update() + let threadGroupCount = MTLSizeMake(8, 8, 1) + let threadGroups = MTLSizeMake(drawable.texture.width / threadGroupCount.width, drawable.texture.height / threadGroupCount.height, 1) + commandEncoder.dispatchThreadgroups(threadGroups, threadsPerThreadgroup: threadGroupCount) + commandEncoder.endEncoding() + commandBuffer.presentDrawable(drawable) + commandBuffer.commit() + } + + } + + func update() { + timer += 0.01 + var bufferPointer = timerBuffer.contents() + memcpy(bufferPointer, &timer, sizeof(Float)) + bufferPointer = mouseBuffer.contents() + memcpy(bufferPointer, &pos, sizeof(NSPoint)) + } + + func registerShaders() { + queue = device!.newCommandQueue() + let path = NSBundle.mainBundle().pathForResource("Shaders", ofType: "metal") + do { + let input = try String(contentsOfFile: path!, encoding: NSUTF8StringEncoding) + let library = try device!.newLibraryWithSource(input, options: nil) + let kernel = library.newFunctionWithName("compute")! + cps = try device!.newComputePipelineStateWithFunction(kernel) + } catch let e { + Swift.print("\(e)") + } + timerBuffer = device!.newBufferWithLength(sizeof(Float), options: []) + mouseBuffer = device!.newBufferWithLength(sizeof(NSPoint), options: []) + } +} diff --git a/ch13/chapter13.playground/contents.xcplayground b/ch13/chapter13.playground/contents.xcplayground new file mode 100644 index 0000000..06828af --- /dev/null +++ b/ch13/chapter13.playground/contents.xcplayground @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/ch13/chapter13.playground/playground.xcworkspace/contents.xcworkspacedata b/ch13/chapter13.playground/playground.xcworkspace/contents.xcworkspacedata new file mode 100755 index 0000000..919434a --- /dev/null +++ b/ch13/chapter13.playground/playground.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ch13/chapter13.playground/playground.xcworkspace/xcuserdata/marius.xcuserdatad/UserInterfaceState.xcuserstate b/ch13/chapter13.playground/playground.xcworkspace/xcuserdata/marius.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100755 index 0000000000000000000000000000000000000000..ac9ca7b310403cea8b2ab3ba0332c49ce6191ba9 GIT binary patch literal 19608 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 literal 0 HcmV?d00001 diff --git a/ch13/chapter13.playground/timeline.xctimeline b/ch13/chapter13.playground/timeline.xctimeline new file mode 100644 index 0000000..bf468af --- /dev/null +++ b/ch13/chapter13.playground/timeline.xctimeline @@ -0,0 +1,6 @@ + + + + +