git.iko.soy/metal-metaballs
on master help sign in
Fixing merge issues.
b3f0035vinivendra
PixelImage/ImageMetaballRenderer.swift+12−4
@@ -4,9 +4,17 @@ import UIKit
4class ImageMetaballRenderer: MetaballRenderer {
4class ImageMetaballRenderer: MetaballRenderer {
5 typealias TargetView = UIImageView
5 typealias TargetView = UIImageView
6
6
7+ let targetView = TargetView()
8+
9+ let dataSource: MetaballDataSource
10+
11+ required init(dataSource: MetaballDataSource) {
12+ self.dataSource = dataSource
13+ }
14+
7− static func updateTargetView(imageView: TargetView, dataSource: MetaballDataSource) {
15+ func updateTargetView() {
8− let width = Int(imageView.width)
16+ let width = Int(targetView.width)
9− let height = Int(imageView.height)
17+ let height = Int(targetView.height)
10 let metaballs = dataSource.metaballs
18 let metaballs = dataSource.metaballs
11
19
12 let bytesPerPixel = 4;
20 let bytesPerPixel = 4;
@@ -57,7 +65,7 @@ class ImageMetaballRenderer: MetaballRenderer {
57
65
58 let cgImage = CGBitmapContextCreateImage(context)
66 let cgImage = CGBitmapContextCreateImage(context)
59 let image = UIImage(CGImage: cgImage!)
67 let image = UIImage(CGImage: cgImage!)
60− imageView.image = image
68+ targetView.image = image
61 }
69 }
62}
70}
63
71
PixelImage/MetaballRenderer.swift+4−6
@@ -8,11 +8,9 @@ protocol MetaballDataSource {
8protocol MetaballRenderer {
8protocol MetaballRenderer {
9 typealias TargetView: UIView
9 typealias TargetView: UIView
10
10
11+ var targetView: TargetView { get }
12+
11− static func updateTargetView(targetView: TargetView, dataSource: MetaballDataSource)
13+ init(dataSource: MetaballDataSource)
12−}
13
14
14−extension MetaballRenderer {
15+ func updateTargetView()
15− static func createTargetView(frame frame: CGRect) -> TargetView {
16− return TargetView(frame: frame)
17− }
18}
16}
PixelImage/MetalMetaballRenderer.swift+71−32
@@ -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)
4
4
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)
9+
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)
6
11
7// TODO: Interpolate metaball colors
12// TODO: Interpolate metaball colors
@@ -15,39 +20,58 @@ class MetalMetaballRenderer: MetaballRenderer {
15
20
16 typealias TargetView = UIImageView
21 typealias TargetView = UIImageView
17
22
23+ let targetView = TargetView()
24+ var previousFrame = CGRect.zero
25+
18− static let context = MTLContext()
26+ let context = MTLContext()
27+ var renderingTexture: MTLTexture!
28+ var imageBuffer: UnsafeMutablePointer<Void>!
29+
30+ let dataSource: MetaballDataSource
31+
32+ required init(dataSource: MetaballDataSource) {
33+ self.dataSource = dataSource
34+ }
19
35
20− static func updateTargetView(imageView: UIImageView, dataSource: MetaballDataSource) {
36+ func updateTargetView() {
21 let metaballs = dataSource.metaballs
37 let metaballs = dataSource.metaballs
22
38
39+ if previousFrame != targetView.frame {
40+ previousFrame = targetView.frame
41+ updateFrame()
42+ }
43+
44+ let width = Int(targetView.width)
45+ let height = Int(targetView.height)
46+
47+ if renderingTexture == nil {
48+ let textureDescriptor =
49+ MTLTextureDescriptor.texture2DDescriptorWithPixelFormat(.BGRA8Unorm, width: width, height: height, mipmapped: false)
50+
51+ renderingTexture = context.device.newTextureWithDescriptor(textureDescriptor)
52+ }
53+
23 let kernelFunction: MTLFunction?
54 let kernelFunction: MTLFunction?
24 let pipeline: MTLComputePipelineState
55 let pipeline: MTLComputePipelineState
25− var internalTexture: MTLTexture! = nil
26−
27 do {
56 do {
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!)
30
60
31− let width = Int(imageView.width)
61+ // Configure info for computing
32− let height = Int(imageView.height)
33−
34− let textureDescriptor =
35− MTLTextureDescriptor.texture2DDescriptorWithPixelFormat(.BGRA8Unorm, width: width, height: height, mipmapped: false)
36−
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)
41
65
42− //
66+ // Create up-to-date metaball buffer
43 let metaballInfoTexture = metaballTexture(metaballs)
67 let metaballInfoTexture = metaballTexture(metaballs)
44
68
45− //
69+ // Send commands to metal and render
46 let commandBuffer = context.commandQueue.commandBuffer()
70 let commandBuffer = context.commandQueue.commandBuffer()
47
71
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!")
61 }
85 }
62
86
87+ // Transform new metal texture into image
63− imageView.image = image(texture: internalTexture)
88+ targetView.image = image(texture: renderingTexture)
64 }
89 }
65
90
66− static func image(texture texture: MTLTexture) -> UIImage {
91+ func image(texture texture: MTLTexture) -> UIImage {
92+ // Get image info
67 let imageSize = CGSizeMake(CGFloat(texture.width),
93 let imageSize = CGSizeMake(CGFloat(texture.width),
68 CGFloat(texture.height))
94 CGFloat(texture.height))
69−
70 let width = Int(imageSize.width)
95 let width = Int(imageSize.width)
71 let height = Int(imageSize.height)
96 let height = Int(imageSize.height)
72−
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)
79
100
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))
84 }
104 }
85
105
106+ // Transfer texture info into image buffer
107+ texture.getBytes(imageBuffer, bytesPerRow: bytesPerRow,
108+ fromRegion: region, mipmapLevel: 0)
109+
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
120+
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
101 }
130 }
102
131
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
108 }
138 }
109− let size = floats.count
110
139
140+ // Create texture
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)
114−
115 let texture = context.device.newTextureWithDescriptor(textureDescriptor)
145 let texture = context.device.newTextureWithDescriptor(textureDescriptor)
116
146
147+ // Copy array info into texture
117 let region = MTLRegionMake2D(0, 0, size, 1)
148 let region = MTLRegionMake2D(0, 0, size, 1)
118−
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)
121
151
122 return texture
152 return texture
123 }
153 }
124−
125−}
126
154
155+ func updateFrame() {
156+ if imageBuffer != nil {
157+ free(imageBuffer)
158+ }
159+ imageBuffer = nil
160+ }
161+
162+ deinit {
163+ free(imageBuffer)
164+ }
165+}
PixelImage/ViewController.swift+7−4
@@ -8,6 +8,7 @@ class ViewController: UIViewController, MetaballDataSource {
8 typealias TargetView = Renderer.TargetView
8 typealias TargetView = Renderer.TargetView
9
9
10 var metaballView: TargetView!
10 var metaballView: TargetView!
11+ var renderer: Renderer!
11
12
12 let width = 350
13 let width = 350
13 let height = 600
14 let height = 600
@@ -20,6 +21,7 @@ class ViewController: UIViewController, MetaballDataSource {
20 override func viewDidLoad() {
21 override func viewDidLoad() {
21 super.viewDidLoad()
22 super.viewDidLoad()
22
23
24+ renderer = Renderer(dataSource: self)
23
25
24 let recognizer = UIPanGestureRecognizer(target: self, action: "handlePan:")
26 let recognizer = UIPanGestureRecognizer(target: self, action: "handlePan:")
25 view.addGestureRecognizer(recognizer)
27 view.addGestureRecognizer(recognizer)
@@ -27,7 +29,8 @@ class ViewController: UIViewController, MetaballDataSource {
27
29
28 let border = 20
30 let border = 20
29 let metaballViewFrame = CGRect(x: border/2, y: border/2, width: width, height: height)
31 let metaballViewFrame = CGRect(x: border/2, y: border/2, width: width, height: height)
32+ metaballView = renderer.targetView
30− metaballView = Renderer.createTargetView(frame: metaballViewFrame)
33+ metaballView.frame = metaballViewFrame
31
34
32 let bigView = UIView(frame: CGRect(x: 10, y: 70, width: width + border, height: height + border))
35 let bigView = UIView(frame: CGRect(x: 10, y: 70, width: width + border, height: height + border))
33 bigView.backgroundColor = UIColor.redColor()
36 bigView.backgroundColor = UIColor.redColor()
@@ -40,7 +43,7 @@ class ViewController: UIViewController, MetaballDataSource {
40 }
43 }
41
44
42
45
43− Renderer.updateTargetView(metaballView, dataSource: self)
46+ renderer.updateTargetView()
44 }
47 }
45
48
46 func handlePan(recognizer: UIPanGestureRecognizer) {
49 func handlePan(recognizer: UIPanGestureRecognizer) {
@@ -57,10 +60,10 @@ class ViewController: UIViewController, MetaballDataSource {
57 selectedMetaball?.middle = location
60 selectedMetaball?.middle = location
58
61
59 if recognizer.state == .Changed && dynamicRendering {
62 if recognizer.state == .Changed && dynamicRendering {
60− Renderer.updateTargetView(metaballView, dataSource: self)
63+ renderer.updateTargetView()
61 }
64 }
62 if recognizer.state == .Ended {
65 if recognizer.state == .Ended {
63− Renderer.updateTargetView(metaballView, dataSource: self)
66+ renderer.updateTargetView()
64 }
67 }
65 }
68 }
66}
69}