-
Notifications
You must be signed in to change notification settings - Fork 0
/
MetalView.swift
310 lines (264 loc) · 13.2 KB
/
MetalView.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
import MetalKit
import SwiftUI
import Foundation
import MetalPerformanceShaders
import CoreMotion
struct MetalView: UIViewRepresentable {
typealias UIViewType = MTKView
var mtkView: MTKView
let cameraInput = CameraInput()
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: UIViewRepresentableContext<MetalView>) -> MTKView {
mtkView.delegate = context.coordinator
mtkView.preferredFramesPerSecond = 60
mtkView.device = MTLCreateSystemDefaultDevice()
mtkView.framebufferOnly = false
mtkView.clearColor = MTLClearColor(red: 0, green: 0, blue: 0, alpha: 0)
mtkView.drawableSize = mtkView.frame.size
return mtkView
}
func updateUIView(_ uiView: MTKView, context: UIViewRepresentableContext<MetalView>) {
}
class Coordinator : NSObject, MTKViewDelegate {
struct Raindrop {
var position: SIMD2<Float>
var hidden: Bool
}
var parent: MetalView
let motion = CMMotionManager()
var gyroscopeTimer: Timer?
var metalDevice: MTLDevice!
var metalCommandQueue: MTLCommandQueue!
var mapTextureFunction: MTLFunction!
var displayTextureFunction: MTLFunction!
var addMistKernelFunction: MTLFunction!
var blurredTexture: MTLTexture?
var mistRatioTexture: MTLTexture?
var raindrops: [Raindrop] = []
var raindropPositions: [SIMD2<Float>] = []
var touches: [SIMD2<Float>] = []
init(_ parent: MetalView) {
self.parent = parent
self.metalDevice = MTLCreateSystemDefaultDevice()
self.metalCommandQueue = metalDevice.makeCommandQueue()!
let library = metalDevice.makeDefaultLibrary()
self.mapTextureFunction = library!.makeFunction(name: "mapTexture")
self.displayTextureFunction = library!.makeFunction(name: "displayTexture")
self.addMistKernelFunction = library!.makeFunction(name: "addMist")
super.init()
self.raindrops = createRaindrops()
if motion.isDeviceMotionAvailable {
motion.startDeviceMotionUpdates()
} else {
print("Device motion not available. You won't get anything out of rotating your device then.")
}
}
func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
}
func draw(in view: MTKView) {
guard let cameraTexture = parent.cameraInput.texture
else {
print("No camera input yet")
return
}
updateRaindrops()
updateTouches(view: view)
if textureIsMissingOrWrongDimensions(blurredTexture, asTexture: cameraTexture) {
blurredTexture = buildIdenticalTexture(asTexture: cameraTexture)
}
blurTexture(sourceTexture: cameraTexture, destinationTexture: blurredTexture!)
if textureIsMissingOrWrongDimensions(mistRatioTexture, asTexture: cameraTexture) {
mistRatioTexture = buildTexture(withSizeFrom: cameraTexture, pixelFormat: .r32Float)
}
addMist(mistRatioTexture!)
renderFinalImage(in: view, cameraTexture: cameraTexture)
}
func createRaindrops() -> [Raindrop] {
var result: [Raindrop] = []
for _ in 0..<50 {
result.append(Raindrop(
position: SIMD2<Float>(Float.random(in: 0..<1), Float.random(in: 0..<1)),
hidden: false
))
}
return result
}
func updateRaindrops() {
let gravity = currentGravity()
// Very ugly, old-school for loop. Idiomatic Swift for loop doesn't let me change
// the array elements. Maybe there's something better I can do here?
for i in 0..<raindrops.count {
var raindrop = raindrops[i]
// Follow gravity
let downwardSpeed = Float.random(in: 0..<0.004)
raindrop.position.x += downwardSpeed * gravity.x
raindrop.position.y += downwardSpeed * gravity.y
// ...but also go a tiny bit sideways
let sidewaysSpeed = Float.random(in: -0.001..<0.001)
raindrop.position.x += sidewaysSpeed * gravity.y
raindrop.position.y += sidewaysSpeed * gravity.x
if raindrop.position.x < 0 || raindrop.position.x > 1 || raindrop.position.y < 0 || raindrop.position.y > 1 {
if abs(gravity.x) > abs(gravity.y) {
// Place drops to the left or the right
if gravity.x < 0 {
// Place drops to the right
raindrop.position.x = 1
} else {
raindrop.position.x = 0
}
raindrop.position.y = Float.random(in: 0..<1)
} else {
// Place drops at the top or bottom
if gravity.y < 0 {
// Place tops at the bottom
raindrop.position.y = 1
} else {
raindrop.position.y = 0
}
raindrop.position.x = Float.random(in: 0..<1)
}
raindrop.hidden = false
}
raindrops[i] = raindrop
}
self.raindropPositions = raindrops
.filter({!$0.hidden})
.map({$0.position})
if self.raindropPositions.isEmpty {
// Metal cannot handle an empty buffer, so we'll just add a dummy raindrop position
self.raindropPositions = [SIMD2<Float>(-1, -1)]
}
}
func currentGravity() -> SIMD2<Float> {
if let deviceMotion = motion.deviceMotion {
// Weird swapping and negating because our view is sideways...
return SIMD2<Float>(Float(-deviceMotion.gravity.y), Float(-deviceMotion.gravity.x))
}
// Just in case we cannot get the gravity data, we're always letting drops go down
return SIMD2<Float>(0, 1)
}
func updateTouches(view: MTKView) {
let uiTouches = (view as! MTKTouchAwareView).currentTouches
self.touches = uiTouches.map({ touch in
let location = touch.location(in: view)
// Divide both x and y location by width, since we want normalized
// coordinates for our shader
let x = location.x / view.frame.width
let y = location.y / view.frame.width
return SIMD2<Float>(Float(x), Float(y))
})
// Hide wiped-out raindrops
for i in 0..<raindrops.count {
var raindrop = raindrops[i]
touches.forEach({touch in
let distX = touch.x - raindrop.position.x
let distY = touch.y - raindrop.position.y
let distanceSquared = (distX*distX) + (distY*distY)
let fingerRadius = Float(0.05)
if distanceSquared < (fingerRadius*fingerRadius) {
raindrop.hidden = true
}
})
raindrops[i] = raindrop
}
if self.touches.isEmpty {
// Metal cannot handle an empty buffer, so we'll just add a dummy touch
self.touches = [SIMD2<Float>(-1, -1)]
}
}
func blurTexture(sourceTexture: MTLTexture, destinationTexture: MTLTexture) {
let buffer = metalCommandQueue.makeCommandBuffer()!
let blur = MPSImageGaussianBlur(device: metalDevice, sigma: 40)
blur.encode(commandBuffer: buffer, sourceTexture: sourceTexture, destinationTexture: destinationTexture)
buffer.commit()
buffer.waitUntilCompleted()
}
func addMist(_ texture: MTLTexture) {
do {
let pipeline = try metalDevice.makeComputePipelineState(function: addMistKernelFunction)
let threadgroupCounts = MTLSizeMake(8, 8, 1);
let threadgroups = MTLSizeMake(texture.width / threadgroupCounts.width,
texture.height / threadgroupCounts.height,
1);
let buffer = metalCommandQueue.makeCommandBuffer()!
let encoder = buffer.makeComputeCommandEncoder()!
encoder.setComputePipelineState(pipeline)
encoder.setTexture(texture, index: 0)
var raindropPositionsCount: Float = Float(raindropPositions.count)
encoder.setBytes(&raindropPositionsCount, length: MemoryLayout<Float>.stride, index: 0)
encoder.setBytes(&raindropPositions, length: MemoryLayout<SIMD2<Float>>.stride * Int(raindropPositionsCount), index: 1)
var touchesCount: Float = Float(touches.count)
encoder.setBytes(&touchesCount, length: MemoryLayout<Float>.stride, index: 2)
encoder.setBytes(&touches, length: MemoryLayout<SIMD2<Float>>.stride * Int(touchesCount), index: 3)
encoder.dispatchThreadgroups(threadgroups, threadsPerThreadgroup: threadgroupCounts)
encoder.endEncoding()
buffer.commit()
buffer.waitUntilCompleted()
} catch {
print("Unexpected error adding mist: \(error)")
}
}
func renderFinalImage(in view: MTKView, cameraTexture: MTLTexture) {
let pipelineDescriptor = MTLRenderPipelineDescriptor()
pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
pipelineDescriptor.depthAttachmentPixelFormat = .invalid
pipelineDescriptor.vertexFunction = mapTextureFunction
pipelineDescriptor.fragmentFunction = displayTextureFunction
var renderPipelineState: MTLRenderPipelineState?
do {
try renderPipelineState = metalDevice.makeRenderPipelineState(descriptor: pipelineDescriptor)
}
catch {
print("Failed creating a render state pipeline. Can't render the texture without one. \(error)")
return
}
let commandBuffer = metalCommandQueue.makeCommandBuffer()!
guard
let currentRenderPassDescriptor = view.currentRenderPassDescriptor,
let currentDrawable = view.currentDrawable,
renderPipelineState != nil
else {
print("Missing something...")
return
}
let encoder = commandBuffer.makeRenderCommandEncoder(descriptor: currentRenderPassDescriptor)!
encoder.pushDebugGroup("RenderFrame")
encoder.setRenderPipelineState(renderPipelineState!)
encoder.setFragmentTexture(cameraTexture, index: 0)
encoder.setFragmentTexture(blurredTexture, index: 1)
encoder.setFragmentTexture(mistRatioTexture, index: 2)
var raindropPositionsCount: Float = Float(raindropPositions.count)
encoder.setFragmentBytes(&raindropPositionsCount, length: MemoryLayout<Float>.stride, index: 0)
encoder.setFragmentBytes(&raindropPositions, length: MemoryLayout<SIMD2<Float>>.stride * Int(raindropPositionsCount), index: 1)
encoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4, instanceCount: 1)
encoder.popDebugGroup()
encoder.endEncoding()
commandBuffer.present(currentDrawable)
commandBuffer.commit()
}
func textureIsMissingOrWrongDimensions(_ texture: MTLTexture?, asTexture: MTLTexture) -> Bool {
return texture == nil
|| texture!.width != asTexture.width
|| texture!.height != asTexture.height
}
func buildIdenticalTexture(asTexture texture: MTLTexture) -> MTLTexture {
return buildTexture(withSizeFrom: texture, pixelFormat: texture.pixelFormat)
}
func buildTexture(withSizeFrom texture: MTLTexture, pixelFormat: MTLPixelFormat) -> MTLTexture {
let textureDescriptor = MTLTextureDescriptor.texture2DDescriptor(
pixelFormat: pixelFormat,
width: texture.width,
height: texture.height,
mipmapped: false)
textureDescriptor.usage = [.shaderRead, .shaderWrite]
return metalDevice.makeTexture(descriptor: textureDescriptor)!
}
}
}
struct MetalView_Previews: PreviewProvider {
static var previews: some View {
/*@START_MENU_TOKEN@*/Text("Hello, World!")/*@END_MENU_TOKEN@*/
}
}