From 048dc00f252a5df71c1b06f8916b39ced2794238 Mon Sep 17 00:00:00 2001 From: Marius Horga Date: Thu, 23 Mar 2017 00:17:31 -0500 Subject: [PATCH] initial commit --- README.md | 3 +- .../ao.playground/Contents.swift | 9 + .../ao.playground/Resources/Shaders.metal | 156 ++++++++++++++++++ .../ao.playground/Sources/MetalView.swift | 56 +++++++ .../ao.playground/contents.xcplayground | 4 + .../contents.xcworkspacedata | 7 + .../UserInterfaceState.xcuserstate | Bin 0 -> 14526 bytes .../UserInterfaceState.xcuserstate | Bin 0 -> 16111 bytes .../ao.playground/timeline.xctimeline | 6 + 9 files changed, 240 insertions(+), 1 deletion(-) create mode 100644 ambient_occlusion/ao.playground/Contents.swift create mode 100644 ambient_occlusion/ao.playground/Resources/Shaders.metal create mode 100644 ambient_occlusion/ao.playground/Sources/MetalView.swift create mode 100644 ambient_occlusion/ao.playground/contents.xcplayground create mode 100644 ambient_occlusion/ao.playground/playground.xcworkspace/contents.xcworkspacedata create mode 100644 ambient_occlusion/ao.playground/playground.xcworkspace/xcuserdata/marius.xcuserdatad/UserInterfaceState.xcuserstate create mode 100644 ambient_occlusion/ao.playground/playground.xcworkspace/xcuserdata/mhorga.xcuserdatad/UserInterfaceState.xcuserstate create mode 100644 ambient_occlusion/ao.playground/timeline.xctimeline diff --git a/README.md b/README.md index 563835a..4662ffb 100644 --- a/README.md +++ b/README.md @@ -22,4 +22,5 @@ Repository to accompany the following blog posts: - [Using MetalKit part 18](http://metalkit.org/2016/10/01/using-metalkit-part-2-3-2.html) - [Raymarching in Metal](http://metalkit.org/2016/12/30/raymarching-in-metal.html) - [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) \ No newline at end of file +- [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) \ No newline at end of file diff --git a/ambient_occlusion/ao.playground/Contents.swift b/ambient_occlusion/ao.playground/Contents.swift new file mode 100644 index 0000000..694d377 --- /dev/null +++ b/ambient_occlusion/ao.playground/Contents.swift @@ -0,0 +1,9 @@ + +import MetalKit +import PlaygroundSupport + +let frame = NSRect(x: 0, y: 0, width: 400, height: 400) +let delegate = MetalView() +let view = MTKView(frame: frame, device: delegate.device) +view.delegate = delegate +PlaygroundPage.current.liveView = view diff --git a/ambient_occlusion/ao.playground/Resources/Shaders.metal b/ambient_occlusion/ao.playground/Resources/Shaders.metal new file mode 100644 index 0000000..9843452 --- /dev/null +++ b/ambient_occlusion/ao.playground/Resources/Shaders.metal @@ -0,0 +1,156 @@ + +#include +using namespace metal; + +struct Ray { + float3 origin; + float3 direction; + Ray(float3 o, float3 d) { + origin = o; + direction = d; + } +}; + +struct Sphere { + float3 center; + float radius; + Sphere(float3 c, float r) { + center = c; + radius = r; + } +}; + +struct Plane { + float yCoord; + Plane(float y) { + yCoord = y; + } +}; + +struct Box { + float3 center; + float size; + Box(float3 c, float s) { + center = c; + size = s; + } +}; + +struct Camera { + float3 position; + Ray ray = Ray(float3(0), float3(0)); + float rayDivergence; + Camera(float3 pos, Ray r, float div) { + position = pos; + ray = r; + rayDivergence = div; + } +}; + +float unionOp(float d0, float d1) { + return min(d0, d1); +} + +float differenceOp(float d0, float d1) { + return max(d0, -d1); +} + +float distToSphere(Ray ray, Sphere s) { + return length(ray.origin - s.center) - s.radius; +} + +float distToPlane(Ray ray, Plane plane) { + return ray.origin.y - plane.yCoord; +} + +float distToBox(Ray r, Box b) { + float3 d = abs(r.origin - b.center) - float3(b.size); + return min(max(d.x, max(d.y, d.z)), 0.0) + length(max(d, 0.0)); +} + +float distToScene(Ray r) { + Plane p = Plane(0.0); + float d2p = distToPlane(r, p); + Sphere s1 = Sphere(float3(0.0, 0.5, 0.0), 8.0); + Sphere s2 = Sphere(float3(0.0, 0.5, 0.0), 6.0); + Sphere s3 = Sphere(float3(10., -5., -10.), 15.0); + Box b = Box(float3(1., 1., -4.), 1.); + float dtb = distToBox(r, b); + float d2s1 = distToSphere(r, s1); + float d2s2 = distToSphere(r, s2); + float d2s3 = distToSphere(r, s3); + float dist = differenceOp(d2s1, d2s2); + dist = differenceOp(dist, d2s3); + dist = unionOp(dist, dtb); + dist = unionOp(d2p, dist); + return dist; +} + +float3 getNormal(Ray ray) { + float2 eps = float2(0.001, 0.0); + float3 n = float3(distToScene(Ray(ray.origin + eps.xyy, ray.direction)) - + distToScene(Ray(ray.origin - eps.xyy, ray.direction)), + distToScene(Ray(ray.origin + eps.yxy, ray.direction)) - + distToScene(Ray(ray.origin - eps.yxy, ray.direction)), + distToScene(Ray(ray.origin + eps.yyx, ray.direction)) - + distToScene(Ray(ray.origin - eps.yyx, ray.direction))); + return normalize(n); +} + +float ao(float3 pos, float3 n) { + float eps = 0.01; + pos += n * eps * 2.0; + float occlusion = 0.0; + for (float i=1.0; i<10.0; i++) { + float d = distToScene(Ray(pos, float3(0))); + float coneWidth = 2.0 * eps; + float occlusionAmount = max(coneWidth - d, 0.); + float occlusionFactor = occlusionAmount / coneWidth; + occlusionFactor *= 1.0 - (i / 10.0); + occlusion = max(occlusion, occlusionFactor); + eps *= 2.0; + pos += n * eps; + } + return max(0.0, 1.0 - occlusion); +} + +Camera setupCam(float3 pos, float3 target, float fov, float2 uv, int x) { + uv *= fov; + float3 cw = normalize(target - pos ); + float3 cp = float3(0.0, 1.0, 0.0); + float3 cu = normalize(cross(cw, cp)); + float3 cv = normalize(cross(cu, cw)); + Ray ray = Ray(pos, normalize(uv.x * cu + uv.y * cv + 0.5 * cw)); + Camera cam = Camera(pos, ray, fov / float(x)); + return cam; +} + +kernel void compute(texture2d output [[texture(0)]], + constant float &time [[buffer(0)]], + 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; + uv.y = -uv.y; + float3 camPos = float3(sin(time) * 10., 3., cos(time) * 10.); + Camera cam = setupCam(camPos, float3(0), 1.25, uv, width); + float3 col = float3(1.0); + bool hit = false; + for (int i=0; i<200; i++) { + float dist = distToScene(cam.ray); + if (dist < 0.001) { + hit = true; + break; + } + cam.ray.origin += cam.ray.direction * dist; + } + if (!hit) { + col = float3(0.5); + } else { + float3 n = getNormal(cam.ray); + float o = ao(cam.ray.origin, n); + col = col * o; + } + output.write(float4(col, 1.0), gid); +} diff --git a/ambient_occlusion/ao.playground/Sources/MetalView.swift b/ambient_occlusion/ao.playground/Sources/MetalView.swift new file mode 100644 index 0000000..bf69602 --- /dev/null +++ b/ambient_occlusion/ao.playground/Sources/MetalView.swift @@ -0,0 +1,56 @@ + +import MetalKit + +public class MetalView: NSObject, MTKViewDelegate { + + public var device: MTLDevice! = nil + var queue: MTLCommandQueue! = nil + var cps: MTLComputePipelineState! = nil + var timerBuffer: MTLBuffer! = nil + var timer: Float = 0 + + override public init() { + super.init() + device = MTLCreateSystemDefaultDevice() + queue = device.makeCommandQueue() + registerShaders() + } + + func registerShaders() { + 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 kernel = library.makeFunction(name: "compute") else { return } + cps = try device.makeComputePipelineState(function: kernel) + } catch let e { + Swift.print("\(e)") + } + timerBuffer = device.makeBuffer(length: MemoryLayout.size, options: []) + } + + func update() { + timer += 0.01 + let bufferPointer = timerBuffer.contents() + memcpy(bufferPointer, &timer, MemoryLayout.size) + } + + 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() + commandEncoder.setComputePipelineState(cps) + commandEncoder.setTexture(drawable.texture, at: 0) + commandEncoder.setBuffer(timerBuffer, offset: 0, at: 0) + 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.present(drawable) + commandBuffer.commit() + } + } +} diff --git a/ambient_occlusion/ao.playground/contents.xcplayground b/ambient_occlusion/ao.playground/contents.xcplayground new file mode 100644 index 0000000..a93d484 --- /dev/null +++ b/ambient_occlusion/ao.playground/contents.xcplayground @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/ambient_occlusion/ao.playground/playground.xcworkspace/contents.xcworkspacedata b/ambient_occlusion/ao.playground/playground.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/ambient_occlusion/ao.playground/playground.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ambient_occlusion/ao.playground/playground.xcworkspace/xcuserdata/marius.xcuserdatad/UserInterfaceState.xcuserstate b/ambient_occlusion/ao.playground/playground.xcworkspace/xcuserdata/marius.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000000000000000000000000000000000000..1ea18a6bee7469c4d298c454064614fccb2035e3 GIT binary patch literal 14526 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 literal 0 HcmV?d00001 diff --git a/ambient_occlusion/ao.playground/timeline.xctimeline b/ambient_occlusion/ao.playground/timeline.xctimeline new file mode 100644 index 0000000..bf468af --- /dev/null +++ b/ambient_occlusion/ao.playground/timeline.xctimeline @@ -0,0 +1,6 @@ + + + + +