-
-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 8da9058
Showing
10 changed files
with
568 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
*.xcuserdatad |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
The MIT License (MIT) | ||
|
||
Copyright (c) 2016 Jacob Bandes-Storch | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining a copy | ||
of this software and associated documentation files (the "Software"), to deal | ||
in the Software without restriction, including without limitation the rights | ||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
copies of the Software, and to permit persons to whom the Software is | ||
furnished to do so, subject to the following conditions: | ||
|
||
The above copyright notice and this permission notice shall be included in all | ||
copies or substantial portions of the Software. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
SOFTWARE. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,189 @@ | ||
import XCPlayground | ||
import Cocoa | ||
import Metal | ||
/*: | ||
## Drawing Fractals with Minimal Metal | ||
|
||
*[Jacob Bandes-Storch](http://bandes-stor.ch/), Feb 2016* | ||
|
||
This playground provides a small interactive example of how to use Metal to render visualizations of [fractals](https://en.wikipedia.org/wiki/Fractal) (namely, the [Mandelbrot set](https://en.wikipedia.org/wiki/Mandelbrot_set) and [Julia sets](https://en.wikipedia.org/wiki/Julia_set)). This certainly isn’t a comprehensive overview of Metal, but hopefully it’s easy to follow and modify. Enjoy! | ||
|
||
- Experiment: Click and drag on the fractal. Watch it change from the Mandelbrot set to a Julia set, and morph as you move the mouse. What happens if you click in a black area of the Mandelbrot set, as opposed to a colored area? | ||
|
||
- Note: To see the playground output, click the Assistant Editor button in the toolbar, or press ⌥⌘↩, which should display the Timeline. To see the extra functions in `Helpers.swift`, enable the Navigator via the toolbar or by pressing ⌘1. | ||
![navigation](navigation.png) | ||
|
||
- Note: This demo only covers using Metal for **compute functions**. There are a lot more complicated things you can do with the full rendering pipeline. Some further reading material is linked at the end of the playground. | ||
*/ | ||
/*: | ||
---- | ||
### Setup | ||
|
||
To use Metal, we first need access to a connected graphics card (***device***). Since this is a small demo, we’ll prefer a low-power (integrated) graphics card. | ||
|
||
We’ll also need a ***command queue*** to let us send commands to the device, and a [dispatch queue](https://developer.apple.com/library/mac/documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationQueues/OperationQueues.html) on which we’ll send these commands. | ||
*/ | ||
let device = require(MTLCopyAllDevices().firstWhere{ $0.lowPower } ?? MTLCreateSystemDefaultDevice(), | ||
orDie: "need a Metal device") | ||
|
||
let commandQueue = device.newCommandQueue() | ||
|
||
let drawingQueue = dispatch_queue_create("drawingQueue", dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INTERACTIVE, 0)) | ||
|
||
/*: | ||
---- | ||
***Shaders*** are small programs which run on the graphics card. | ||
We can load the shader library from a separate file, `Shaders.metal` (which you can find in the left-hand Project navigator (⌘1) under **Resources**), and compile them on the fly for this device. This example uses two shaders, or ***compute kernels***, named `mandelbrotShader` and `juliaShader`. | ||
*/ | ||
let shaderSource = require(try String(contentsOfURL: [#FileReference(fileReferenceLiteral: "Shaders.metal")#]), | ||
orDie: "unable to read shader source file") | ||
|
||
let library = require(try device.newLibraryWithSource(shaderSource, options: nil), | ||
orDie: "compiling shaders failed") | ||
|
||
//: - Experiment: Open up `Shaders.metal` and glance through it to understand what the shaders are doing. | ||
//: - Important: If your shader has a syntax error, `newLibraryWithSource()` will throw an error here when it tries to compile the program. | ||
|
||
let mandelbrotShader = require(library.newFunctionWithName("mandelbrotShader"), | ||
orDie: "unable to get mandelbrotShader") | ||
|
||
let juliaShader = require(library.newFunctionWithName("juliaShader"), | ||
orDie: "unable to get juliaShader") | ||
|
||
//: The Julia set shader also needs some extra input, an *(x, y)* point, from the CPU. We can pass this via a shared buffer. | ||
let juliaBuffer = device.newBufferWithLength(2 * sizeof(Float32), options: []) | ||
|
||
/*: | ||
---- | ||
Before we can use these shaders, Metal needs to know how to request they be executed on the GPU. This information is precomputed and stored as ***compute pipeline state***, which we can reuse repeatedly. | ||
|
||
When executing the program, we’ll also have to decide how to utilize the GPU’s threads (how many groups of threads to use, and the number of threads per group). This will depend on the size of the view we want to draw into. | ||
*/ | ||
let mandelbrotPipelineState = require(try device.newComputePipelineStateWithFunction(mandelbrotShader), | ||
orDie: "unable to create compute pipeline state") | ||
|
||
let juliaPipelineState = require(try device.newComputePipelineStateWithFunction(juliaShader), | ||
orDie: "unable to create compute pipeline state") | ||
|
||
var threadgroupSizes = ThreadgroupSizes.zeros // To be calculated later | ||
|
||
/*: | ||
---- | ||
### Drawing | ||
|
||
The fundamental way that Metal content gets onscreen is via CAMetalLayer. The layer has a pool of ***textures*** which hold image data. Our shaders will write into these textures, which can then be displayed on the screen. | ||
|
||
We’ll use a custom view class called `MetalView` which is backed by a CAMetalLayer, and automatically resizes its “drawable” (texture) to match the view’s size. (MetalKit provides the MTKView class, but it’s overkill for this demo.) | ||
*/ | ||
let outputSize = CGSize(width: 300, height: 250) | ||
|
||
let metalView = MetalView(frame: CGRect(origin: .zero, size: outputSize), device: device) | ||
let metalLayer = metalView.metalLayer | ||
/*: | ||
- Experiment: Look at the MetalView implementation to see how it interacts with the CAMetalLayer. | ||
|
||
A helper function called `computeAndDraw` in `Helpers.swift` takes care of encoding the commands which execute the shader, and submitting the buffer of encoded commands to the device. All we need to tell it is which pipeline state to use, which texture to draw into, and set up any necessary parameters to the shader functions. | ||
*/ | ||
func drawMandelbrotSet() | ||
{ | ||
dispatch_async(drawingQueue) { | ||
commandQueue.computeAndDraw(into: metalLayer.nextDrawable(), with: threadgroupSizes) { | ||
$0.setComputePipelineState(mandelbrotPipelineState) | ||
} | ||
} | ||
} | ||
|
||
func drawJuliaSet(point: CGPoint) | ||
{ | ||
dispatch_async(drawingQueue) { | ||
commandQueue.computeAndDraw(into: metalLayer.nextDrawable(), with: threadgroupSizes) { | ||
$0.setComputePipelineState(juliaPipelineState) | ||
|
||
// Pass the (x,y) coordinates of the clicked point via the buffer we allocated ahead of time. | ||
$0.setBuffer(juliaBuffer, offset: 0, atIndex: 0) | ||
let buf = UnsafeMutablePointer<Float32>(juliaBuffer.contents()) | ||
buf[0] = Float32(point.x) | ||
buf[1] = Float32(point.y) | ||
} | ||
} | ||
} | ||
/*: | ||
- Experiment: | ||
Go check out the implementation of `computeAndDraw`! Can you understand how it works? | ||
|
||
---- | ||
### The easy part | ||
Now for some user interaction! Our view controller draws fractals when the view is first laid out, and whenever the mouse is dragged (user interaction requires Xcode 7.3). | ||
*/ | ||
class Controller: NSViewController, MetalViewDelegate | ||
{ | ||
override func viewDidLayout() { | ||
metalViewDrawableSizeDidChange(metalView) | ||
} | ||
func metalViewDrawableSizeDidChange(metalView: MetalView) { | ||
// This helper function chooses how to assign the GPU’s threads to portions of the texture. | ||
threadgroupSizes = mandelbrotPipelineState.threadgroupSizesForDrawableSize(metalView.metalLayer.drawableSize) | ||
drawMandelbrotSet() | ||
} | ||
|
||
override func mouseDown(event: NSEvent) { | ||
drawJuliaSetForEvent(event) | ||
} | ||
override func mouseDragged(event: NSEvent) { | ||
drawJuliaSetForEvent(event) | ||
} | ||
override func mouseUp(event: NSEvent) { | ||
drawMandelbrotSet() | ||
} | ||
|
||
func drawJuliaSetForEvent(event: NSEvent) { | ||
var pos = metalView.convertPointToLayer(metalView.convertPoint(event.locationInWindow, fromView: nil)) | ||
let scale = metalLayer.contentsScale | ||
pos.x *= scale | ||
pos.y *= scale | ||
|
||
drawJuliaSet(pos) | ||
} | ||
} | ||
|
||
//: Finally, we can put our view onscreen! | ||
let controller = Controller() | ||
controller.view = metalView | ||
metalView.delegate = controller | ||
|
||
metalView.addSubview(Label(string: "Click me! (requires Xcode 7.3)"), at: CGPoint(x: 5, y: 5)) | ||
|
||
XCPlaygroundPage.currentPage.liveView = metalView | ||
|
||
/*: | ||
---- | ||
## What Next? | ||
|
||
I hope you’ve enjoyed (and learned something from) this demo. If you haven’t already, I encourage you to poke around in `Helpers.swift` and `Shaders.metal`. Try changing the code and see what happens — *take chances, make mistkaes, get messy!* | ||
|
||
If you like reading, there’s lots of reading material about Metal available from Apple, as well as excellent resources from others in the community. Just a few examples: | ||
* Apple’s own [Metal for Developers](https://developer.apple.com/metal/) documentation and resources. | ||
* [Metal By Example](http://metalbyexample.com/), a blog and book by Warren Moore. | ||
* [Blog posts about Metal](http://redqueencoder.com/category/metal/) by The Red Queen Coder (Janie Clayton). | ||
* [Posts and demos](http://flexmonkey.blogspot.co.uk/?view=magazine) by FlexMonkey (Simon Gladman) on topics including Metal and Core Image. | ||
|
||
### Modify this playground! | ||
|
||
This demo barely scratches the surface. Here are a handful of ideas for things to try. (If you come up with something cool, I’d love to [hear about it](https://twitter.com/jtbandes)!) | ||
|
||
- Experiment: Tweak the `maxiters` and `escape` parameters in the shader source file. Do the fractals look different? Can you notice any difference in speed? Try modifying the playground to use a discrete graphics card, if your machine has one. | ||
|
||
- Experiment: Adapt this code to display the same fractals on an iOS device. (Metal isn’t supported in the iOS simulator.) You’ll need to use UIView instead of NSView, but most the Metal-related code can remain the same. | ||
|
||
- Experiment: Choose another fractal or another coloring scheme, and modify `Shaders.metal` to render it. | ||
|
||
- Experiment: Add a label which shows the coordinates that were clicked in the complex plane. | ||
|
||
Bonus: can you share the code which does this x,y-to-complex conversion between Swift and the shader itself? Try moving things into a full Xcode project and setting up a [bridging header](https://developer.apple.com/library/ios/documentation/Swift/Conceptual/BuildingCocoaApps/MixandMatch.html). You might want to use `#ifdef __cplusplus` and/or `extern "C"`. | ||
|
||
- Experiment: Try using [MTKView](https://developer.apple.com/library/ios/documentation/MetalKit/Reference/MTKView_ClassReference/) instead of the simple `MetalView` in this playground. Use the MTKView’s delegate or a subclass to render each frame, and modify the pipeline so that your fractals can change over time. | ||
* Try to make the colors change slowly over time. | ||
* Try to make the visualization zoom in on an [interesting point](https://en.wikipedia.org/wiki/Mandelbrot_set#Image_gallery_of_a_zoom_sequence). | ||
|
||
- Experiment: Use `CIImage.init(MTLTexture:options:)` to render the fractals into an image. Save an animated GIF using [CGImageDestination](http://stackoverflow.com/q/14915138/23649), or a movie using [AVAssetWriter](http://stackoverflow.com/q/3741323/23649). | ||
*/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
#include <metal_stdlib> | ||
using namespace metal; | ||
|
||
#define M_PI 3.141592653589793238462643383 | ||
|
||
/// Basic implementation of complex numbers, with * + - operators, and a function to return the squared magnitude. | ||
template<typename T> | ||
struct complex | ||
{ | ||
T _x, _y; | ||
|
||
complex(T x, T y) : _x(x), _y(y) { } | ||
|
||
T sqmag() const { | ||
return _x*_x + _y*_y; | ||
} | ||
|
||
complex<T> operator*(const thread complex<T>& other) const { | ||
return complex(_x*other._x - _y*other._y, | ||
_x*other._y + _y*other._x); | ||
} | ||
|
||
complex<T> operator+(const thread complex<T>& other) const { | ||
return complex(_x + other._x, _y + other._y); | ||
} | ||
|
||
complex<T> operator-(const thread complex<T>& other) const { | ||
return complex(_x - other._x, _y - other._y); | ||
} | ||
|
||
complex<T> operator*(const thread T& c) const { | ||
return complex(_x * c, _y * c); | ||
} | ||
}; | ||
|
||
|
||
#define complexExtentX 3.5 | ||
#define complexExtentY 3 | ||
#define mandelbrotShiftX 0.2 | ||
|
||
/// Convert a point on the screen to a point in the complex plane. | ||
template<typename T> | ||
complex<T> screenToComplex(T x, T y, T width, T height) | ||
{ | ||
const T scale = max(complexExtentX/width, complexExtentY/height); | ||
|
||
return complex<T>((x-width/2)*scale, (y-height/2)*scale); | ||
} | ||
|
||
|
||
/// Both Mandelbrot and Julia sets use the same iterative algorithm to determine whether | ||
/// a given point is in the set. Each point is colored based on how quickly it escape to infinity. | ||
template<typename T> | ||
float4 colorForIteration(complex<T> z, complex<T> c, int maxiters, float escape) | ||
{ | ||
for (int i = 0; i < maxiters; i++) { | ||
z = z*z + c; | ||
if (z.sqmag() > escape) { | ||
// Smoothing coloring, adapted from: | ||
// <https://en.wikipedia.org/wiki/Mandelbrot_set#Continuous_.28smooth.29_coloring> | ||
float hue = (i+1-log2(log10(z.sqmag())/2))/maxiters*4 * M_PI + 3; | ||
|
||
// Convert to RGB | ||
return float4((cos(hue)+1)/2, | ||
(-cos(hue+M_PI/3)+1)/2, | ||
(-cos(hue-M_PI/3)+1)/2, | ||
1); | ||
} | ||
} | ||
|
||
return float4(0, 0, 0, 1); | ||
} | ||
|
||
|
||
/// Render a visualization of the Mandelbrot set into the `output` texture. | ||
kernel void mandelbrotShader(texture2d<float, access::write> output [[texture(0)]], | ||
uint2 upos [[thread_position_in_grid]]) | ||
{ | ||
int width = output.get_width(); | ||
int height = output.get_height(); | ||
if (upos.x > width || upos.y > height) return; | ||
|
||
complex<float> z(0, 0); | ||
|
||
complex<float> c = screenToComplex<float>(upos.x - mandelbrotShiftX*width, | ||
upos.y, | ||
width, height); | ||
|
||
output.write(float4(colorForIteration(z, c, 100, 100)), upos); | ||
} | ||
|
||
|
||
/// Render a visualization of the Julia set for the point `screenPoint` into the `output` texture. | ||
kernel void juliaShader(texture2d<float, access::write> output [[texture(0)]], | ||
uint2 upos [[thread_position_in_grid]], | ||
const device float2& screenPoint [[buffer(0)]]) | ||
{ | ||
int width = output.get_width(); | ||
int height = output.get_height(); | ||
if (upos.x > width || upos.y > height) return; | ||
|
||
complex<float> z = screenToComplex<float>(upos.x, upos.y, width, height); | ||
|
||
complex<float> c = screenToComplex<float>(screenPoint.x - mandelbrotShiftX*width, | ||
screenPoint.y, | ||
width, | ||
height); | ||
|
||
output.write(float4(colorForIteration(z, c, 100, 50)), upos); | ||
} |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Oops, something went wrong.