@@ -1,7 +1,12 @@
1−// TODO: Render metaballs on layer/view instead of image
1+// TODO: Optimize resources (don't allocate textures and images every frame)
2// TODO: Remove references to view size from shader
2// TODO: Remove references to view size from shader
3// TODO: Make shader ignore metaballs that are outside a certain area of tolerance (eg ±100 from borders)
3// TODO: Make shader ignore metaballs that are outside a certain area of tolerance (eg ±100 from borders)
5+// TODO: Metaballs from texture to buffer
6+// TODO: Use async dispatch to free up main queue
7+// TODO: Refactor update into 2 calls: metalRendering() and textureToImage()
8+// TODO: Use two textures/images to alternate the 2 calls (like OpenGL double buffers)
5// TODO: Make graphics have edges only according to graph (instead of being a complete graph)
10// TODO: Make graphics have edges only according to graph (instead of being a complete graph)
7// TODO: Interpolate metaball colors
12// TODO: Interpolate metaball colors
@@ -15,39 +20,58 @@ class MetalMetaballRenderer: MetaballRenderer {
16 typealias TargetView = UIImageView
21 typealias TargetView = UIImageView
23+ let targetView = TargetView()
24+ var previousFrame = CGRect.zero
18− static let context = MTLContext()
26+ let context = MTLContext()
27+ var renderingTexture: MTLTexture!
28+ var imageBuffer: UnsafeMutablePointer<Void>!
30+ let dataSource: MetaballDataSource
32+ required init(dataSource: MetaballDataSource) {
33+ self.dataSource = dataSource
20− static func updateTargetView(imageView: UIImageView, dataSource: MetaballDataSource) {
36+ func updateTargetView() {
21 let metaballs = dataSource.metaballs
37 let metaballs = dataSource.metaballs
39+ if previousFrame != targetView.frame {
40+ previousFrame = targetView.frame
44+ let width = Int(targetView.width)
45+ let height = Int(targetView.height)
47+ if renderingTexture == nil {
48+ let textureDescriptor =
49+ MTLTextureDescriptor.texture2DDescriptorWithPixelFormat(.BGRA8Unorm, width: width, height: height, mipmapped: false)
51+ renderingTexture = context.device.newTextureWithDescriptor(textureDescriptor)
23 let kernelFunction: MTLFunction?
54 let kernelFunction: MTLFunction?
24 let pipeline: MTLComputePipelineState
55 let pipeline: MTLComputePipelineState
25− var internalTexture: MTLTexture! = nil
57+ // Get long-lived objects
28 kernelFunction = context.library.newFunctionWithName("drawMetaballs")
58 kernelFunction = context.library.newFunctionWithName("drawMetaballs")
29 pipeline = try context.device.newComputePipelineStateWithFunction(kernelFunction!)
59 pipeline = try context.device.newComputePipelineStateWithFunction(kernelFunction!)
31− let width = Int(imageView.width)
61+ // Configure info for computing
32− let height = Int(imageView.height)
34− let textureDescriptor =
35− MTLTextureDescriptor.texture2DDescriptorWithPixelFormat(.BGRA8Unorm, width: width, height: height, mipmapped: false)
37− internalTexture = context.device.newTextureWithDescriptor(textureDescriptor)
38 let threadGroupCounts = MTLSizeMake(8, 8, 1)
62 let threadGroupCounts = MTLSizeMake(8, 8, 1)
39 let threadGroups = MTLSizeMake(width / threadGroupCounts.width,
63 let threadGroups = MTLSizeMake(width / threadGroupCounts.width,
40 height / threadGroupCounts.height, 1)
64 height / threadGroupCounts.height, 1)
42− //
66+ // Create up-to-date metaball buffer
43 let metaballInfoTexture = metaballTexture(metaballs)
67 let metaballInfoTexture = metaballTexture(metaballs)
45− //
69+ // Send commands to metal and render
46 let commandBuffer = context.commandQueue.commandBuffer()
70 let commandBuffer = context.commandQueue.commandBuffer()
48 let commandEncoder = commandBuffer.computeCommandEncoder()
72 let commandEncoder = commandBuffer.computeCommandEncoder()
49 commandEncoder.setComputePipelineState(pipeline)
73 commandEncoder.setComputePipelineState(pipeline)
50− commandEncoder.setTexture(internalTexture, atIndex: 0)
74+ commandEncoder.setTexture(renderingTexture, atIndex: 0)
51 commandEncoder.setTexture(metaballInfoTexture, atIndex: 1)
75 commandEncoder.setTexture(metaballInfoTexture, atIndex: 1)
52 commandEncoder.dispatchThreadgroups(threadGroups,
76 commandEncoder.dispatchThreadgroups(threadGroups,
53 threadsPerThreadgroup: threadGroupCounts)
77 threadsPerThreadgroup: threadGroupCounts)
@@ -60,29 +84,32 @@ class MetalMetaballRenderer: MetaballRenderer {
60 print("Error!")
84 print("Error!")
87+ // Transform new metal texture into image
63− imageView.image = image(texture: internalTexture)
88+ targetView.image = image(texture: renderingTexture)
66− static func image(texture texture: MTLTexture) -> UIImage {
91+ func image(texture texture: MTLTexture) -> UIImage {
67 let imageSize = CGSizeMake(CGFloat(texture.width),
93 let imageSize = CGSizeMake(CGFloat(texture.width),
68 CGFloat(texture.height))
94 CGFloat(texture.height))
70 let width = Int(imageSize.width)
95 let width = Int(imageSize.width)
71 let height = Int(imageSize.height)
96 let height = Int(imageSize.height)
73 let imageByteCount = width * height * 4
97 let imageByteCount = width * height * 4
74− let imageBytes = malloc(imageByteCount)
75 let bytesPerRow = width * 4
98 let bytesPerRow = width * 4
76 let region = MTLRegionMake2D(0, 0, width, height)
99 let region = MTLRegionMake2D(0, 0, width, height)
77− texture.getBytes(imageBytes, bytesPerRow: bytesPerRow,
78− fromRegion: region, mipmapLevel: 0)
80− let provider = CGDataProviderCreateWithData(nil, imageBytes,
101+ // Allocate the buffer if needed
102+ if imageBuffer == nil {
81− imageByteCount) {
103+ imageBuffer = malloc(imageByteCount)
82− (_, data, _) -> Void in
83− free(UnsafeMutablePointer<Void>(data))
106+ // Transfer texture info into image buffer
107+ texture.getBytes(imageBuffer, bytesPerRow: bytesPerRow,
108+ fromRegion: region, mipmapLevel: 0)
110+ // Get info for UIImage
111+ let provider = CGDataProviderCreateWithData(nil, imageBuffer,
112+ imageByteCount, nil)
86 let bitsPerComponent = 8
113 let bitsPerComponent = 8
87 let bitsperPixel = 32
114 let bitsperPixel = 32
88 let colorSpaceRef = CGColorSpaceCreateDeviceRGB()
115 let colorSpaceRef = CGColorSpaceCreateDeviceRGB()
@@ -90,6 +117,8 @@ class MetalMetaballRenderer: MetaballRenderer {
90 CGImageAlphaInfo.PremultipliedLast.rawValue |
117 CGImageAlphaInfo.PremultipliedLast.rawValue |
91 CGBitmapInfo.ByteOrder32Big.rawValue)
118 CGBitmapInfo.ByteOrder32Big.rawValue)
92 let renderingIntent = CGColorRenderingIntent.RenderingIntentDefault
119 let renderingIntent = CGColorRenderingIntent.RenderingIntentDefault
121+ // Create UIImage from image buffer
93 let imageRef = CGImageCreate(width, height,
122 let imageRef = CGImageCreate(width, height,
94 bitsPerComponent, bitsperPixel, bytesPerRow,
123 bitsPerComponent, bitsperPixel, bytesPerRow,
95 colorSpaceRef, bitmapInfo, provider,
124 colorSpaceRef, bitmapInfo, provider,
@@ -100,27 +129,37 @@ class MetalMetaballRenderer: MetaballRenderer {
100 return image
129 return image
103− static func metaballTexture(metaballs: [Metaball]) -> MTLTexture {
132+ func metaballTexture(metaballs: [Metaball]) -> MTLTexture {
133+ // Create Float array for buffer
104 let floats: [Float] = metaballs.reduce([Float(metaballs.count)]) { (var array, metaball) -> [Float] in
134 let floats: [Float] = metaballs.reduce([Float(metaballs.count)]) { (var array, metaball) -> [Float] in
105 array.append(Float(metaball.midX))
135 array.append(Float(metaball.midX))
106 array.append(Float(metaball.midY))
136 array.append(Float(metaball.midY))
107 return array
137 return array
109− let size = floats.count
141+ let size = floats.count
111 let textureDescriptor =
142 let textureDescriptor =
112 MTLTextureDescriptor.texture2DDescriptorWithPixelFormat(
143 MTLTextureDescriptor.texture2DDescriptorWithPixelFormat(
113 .R32Float, width: size, height: 1, mipmapped: false)
144 .R32Float, width: size, height: 1, mipmapped: false)
115 let texture = context.device.newTextureWithDescriptor(textureDescriptor)
145 let texture = context.device.newTextureWithDescriptor(textureDescriptor)
147+ // Copy array info into texture
117 let region = MTLRegionMake2D(0, 0, size, 1)
148 let region = MTLRegionMake2D(0, 0, size, 1)
119 texture.replaceRegion(region, mipmapLevel: 0, withBytes: floats,
149 texture.replaceRegion(region, mipmapLevel: 0, withBytes: floats,
120 bytesPerRow: sizeof(Float) * size)
150 bytesPerRow: sizeof(Float) * size)
122 return texture
152 return texture
155+ func updateFrame() {
156+ if imageBuffer != nil {