Today we continue working on that beautiful fractal, so let’s pick up where we left off in Part 11. Using the same playground we worked on last time, we will next see how to bring it to life, that is, animate it. For that, we will use uniforms
again. We introduced them in Part 5 in case you want to read again why they are useful in cases such as this one.
First, at the top of the MetalView.swift
file, let’s create a global variable named timer and a data buffer to hold it:
var timer: Float = 0
var timerBuffer: MTLBuffer!
Next, inside registerShaders()
we want to initialize the buffer:
timerBuffer = device!.newBufferWithLength(sizeof(Float), options: [])
Then, we need to create an update() function which will increase the timer and send its new value to the buffer:
func update() {
timer += 0.01
var bufferPointer = timerBuffer.contents()
memcpy(bufferPointer, &timer, sizeof(Float))
}
Next, in drawRect()
right below the line where we set the texture to the current drawable, we need to also set the timer buffer at index 1. Then we need to also call the update
function since drawRect()
runs every frame so the timer will have a bigger value as frames go by:
commandEncoder.setBuffer(timerBuffer, offset: 0, atIndex: 1)
update()
Then, in Shaders.metal
we need to update our kernel signature to also include the timer buffer:
kernel void compute(texture2d<float, access::write> output [[texture(0)]],
constant float &timer [[buffer(1)]],
uint2 gid [[thread_position_in_grid]])
Here comes the fun part! Replace the line below:
float2 cc = 1.1*float2( 0.5*cos(0.1) - 0.25*cos(0.2), 0.5*sin(0.1) - 0.25*sin(0.2) );
with this line:
float2 cc = 1.1*float2( 0.5*cos(0.1*timer) - 0.25*cos(0.2*timer), 0.5*sin(0.1*timer) - 0.25*sin(0.2*timer) );
If you run the playground right now, you should see something similar:

Wasn’t that easy and fun? There is another important and useful feature we could have, and that is mouse interaction. Obviously, we can use uniforms
again. Let’s conform our MetalView
class to the NSWindowDelegate
protocol so we can use its mouse
methods.
public class MetalView: MTKView, NSWindowDelegate {
Next, let’s create again, like we did for the timer, a global variable named pos for the mouse position (coordinates) and a data buffer to hold it. We can now override the mouseDown() method and work with the coordinates:
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
}
As you notice, we are scaling down the coordinates, from the entire screen to only our MetalView
size, and then we update the coordinates with this scale factor we got from the view’s layer. Next, inside the registerShaders()
function let’s initialize the mouse buffer:
mouseBuffer = device!.newBufferWithLength(sizeof(NSPoint), options: [])
Now go back to the update()
function and add these lines at the end of it so we can send the current mouse coordinates to the buffer:
bufferPointer = mouseBuffer.contents()
memcpy(bufferPointer, &pos, sizeof(NSPoint))
Next, in drawRect()
we set the mouse buffer at index 2:
commandEncoder.setBuffer(mouseBuffer, offset: 0, atIndex: 2)
Then, in Shaders.metal
we again update the kernel signature to also include the mouse buffer:
kernel void compute(texture2d<float, access::write> output [[texture(0)]],
constant float &timer [[buffer(1)]],
constant float2 &mouse [[buffer(2)]],
uint2 gid [[thread_position_in_grid]])
Finally, we update the line below:
float3 color = float3( dmin.w );
with this line:
float3 color = float3(mouse.x - mouse.y);
What we did here is change the way color is calculated, by passing the mouse coordinates to the color
variable. Run the playground and click in various view areas, then notice the effect. The output image should look like this:

Play with the kernel code to achieve prettier effects by using the mouse coordinates in other parts of the code. There is one other matter I need to take a closer look at, the fact that NSPoint
might not correctly map to the kernel float2
type we used. The source code is posted on Github as usual.
Until next time!