前言
上一篇文章中我们介绍了如何使用CALayer遮罩实现渐变二维码,没看过的读者如果有兴趣可以去看一下。本文将介绍如何使用Metal(苹果的亲儿子)实现渐变二维码的效果。下面是效果图。
Metal概述
作为一名iOS开发,就算你没用过Metal,也应该听说过。Metal是苹果捣腾出来用来代替OpenGL的一套3D渲染API,他和OpenGL ES一样,可以高效率的利用GPU,编写Vertex Shader和Fragment Shader充分的控制渲染流程。想要顺利的看完本文,你至少要知道Shader是什么。Metal和OpenGL ES在Shader的概念上是互通的,可以前往OpenGL ES相关知识对Shader有一个初步的了解,这对你阅读下文会有很大的帮助。
涉及代码
本文涉及的代码在项目的ColorfulQRCodeMetalView.swift和qrcode.metal中。ViewController.swift里只是添加了ColorfulQRCodeMetalView进行示例显示。
Metal基础代码
首先来介绍使用Metal需要的基础代码,CAMetalLayer是利用Metal进行渲染的容器,我们需要创建并初始化一个CAMetalLayer。device是Metal中用来申请资源的重要对象,比如创建纹理,缓冲区等等。
func initMetal() { device = MTLCreateSystemDefaultDevice() guard device != nil else { print("Metal is not supported on this device") return } metalLayer = CAMetalLayer() metalLayer.device = device metalLayer.pixelFormat = .bgra8Unorm metalLayer.framebufferOnly = true metalLayer.frame = self.bounds self.layer.addSublayer(metalLayer) }
接下来初始化Shader,在Metal中,Shader编译处理完后形成的对象称为Pipline,流水线。还是很符合Shader的实际工作流程的。
func initPipline() { commandQueue = device.makeCommandQueue() commandQueue.label = "main metal command queue" let defaultLibrary = device.makeDefaultLibrary()! let fragmentProgram = defaultLibrary.makeFunction(name: "passThroughFragment")! let vertexProgram = defaultLibrary.makeFunction(name: "passThroughVertex")! self.pipelineStateDescriptor = MTLRenderPipelineDescriptor() pipelineStateDescriptor.vertexFunction = vertexProgram pipelineStateDescriptor.fragmentFunction = fragmentProgram pipelineStateDescriptor.colorAttachments[0].pixelFormat = metalLayer.pixelFormat do { try pipelineState = device.makeRenderPipelineState(descriptor: pipelineStateDescriptor) } catch let error { print("Failed to create pipeline state, error /(error)") } }
创建完成后,得到一个pipelineState,我们会在渲染时使用到它。
Metal渲染基础代码
我们有了显示渲染内容的metalLayer,关联好Shader的pipelineState,接下来,就可以编写基础的渲染代码了。
func render(qrcodeTexture: MTLTexture) { guard let drawable = metalLayer?.nextDrawable() else { return } let renderPassDescriptor = MTLRenderPassDescriptor.init() renderPassDescriptor.colorAttachments[0].texture = drawable.texture renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor.init(red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0); renderPassDescriptor.colorAttachments[0].loadAction = .clear let commandBuffer = commandQueue.makeCommandBuffer()! commandBuffer.label = "Frame command buffer" let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor)! renderEncoder.label = "render encoder" renderEncoder.pushDebugGroup("begin draw") renderEncoder.setRenderPipelineState(pipelineState) self.draw(renderEncoder: renderEncoder, qrcodeTexture: qrcodeTexture) renderEncoder.popDebugGroup() renderEncoder.endEncoding() commandBuffer.present(drawable) commandBuffer.commit() }
这段代码看似很长,实际上并没有做具体的渲染工作。只是做了一些准备工作。使用renderPassDescriptor来描述本次渲染的渲染目标和如何清空缓冲区。然后配合pipelineState获得一个用作绘图的对象renderEncoder。我在draw方法中使用该对象进行具体内容的绘制。最后使用commandBuffer的present和commit把绘制指令递交给GPU。
创建二维码纹理
ColorfulQRCodeMetalView接受UIImage类型的变量作为二维码图片,但是Metal只接受纹理。所以我们需要使用UIImage类型的二维码图片生成Metal纹理。
func createQRCodeTexture(qrcodeImage: UIImage) -> MTLTexture? { let bitsPerComponent = 8 let bytesPerPixel = 4 let width:Int = Int(qrcodeImage.size.width) let height:Int = Int(qrcodeImage.size.height) let imageData = UnsafeMutableRawPointer.allocate(bytes: Int(width * height * bytesPerPixel), alignedTo: 8) let colorSpace = CGColorSpaceCreateDeviceRGB() let imageContext = CGContext.init(data: imageData, width: width, height: height, bitsPerComponent: bitsPerComponent, bytesPerRow: width * bytesPerPixel, space: colorSpace, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue | CGImageByteOrderInfo.order32Big.rawValue ) UIGraphicsPushContext(imageContext!) imageContext?.translateBy(x: 0, y: CGFloat(height)) imageContext?.scaleBy(x: 1, y: -1) qrcodeImage.draw(in: CGRect.init(x: 0, y: 0, width: width, height: height)) UIGraphicsPopContext() let descriptor = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: .rgba8Unorm, width: width, height: height, mipmapped: false) descriptor.usage = .shaderRead let texture = device.makeTexture(descriptor: descriptor) texture?.replace(region: MTLRegionMake2D(0, 0, width, height), mipmapLevel: 0, withBytes: imageData, bytesPerRow: width * bytesPerPixel) return texture }
将图片数据解压为RGBA格式的纯字节流,然后通过Metal的API创建纹理。
绘制
万事具备,接下来就可以进行绘制了。我们要绘制的是一个2x2的矩形,这样可以刚好填满MetalLayer,然后把二维码纹理传递给Shader。渐变色数组和渐变色数目也要传递给Shader,我们会在Shader中计算每个像素应该是什么颜色。
func draw(renderEncoder: MTLRenderCommandEncoder, qrcodeTexture: MTLTexture) { let squareData: [Float] = [ -1, 1, 0.0, 0, 0, -1, -1, 0.0, 0, 1, 1, -1, 0.0, 1, 1, 1, -1, 0.0, 1, 1, 1, 1, 0.0, 1, 0, -1, 1, 0.0, 0, 0 ] let vertexBufferSize = MemoryLayout.size * squareData.count let vertexBuffer = device.makeBuffer(bytes: squareData, length: vertexBufferSize, options: MTLResourceOptions.cpuCacheModeWriteCombined) renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0) renderEncoder.setFragmentTexture(qrcodeTexture, index: 0) let colors: [Float] = [ 0x2a / 255.0, 0x9c / 255.0, 0x1f / 255.0, 0xe6 / 255.0, 0xcd / 255.0, 0x27 / 255.0, 0xe6 / 255.0, 0x27 / 255.0, 0x57 / 255.0 ] let colorsBufferSize = MemoryLayout.size * colors.count let colorsBuffer = device.makeBuffer(bytes: colors, length: colorsBufferSize, options: MTLResourceOptions.cpuCacheModeWriteCombined) renderEncoder.setFragmentBuffer(colorsBuffer, offset: 0, index: 0) let uniform: [Int] = [colors.count / 3] let uniformBufferSize = MemoryLayout.size * uniform.count let uniformBuffer = device.makeBuffer(bytes: uniform, length: uniformBufferSize, options: MTLResourceOptions.cpuCacheModeWriteCombined) renderEncoder.setFragmentBuffer(uniformBuffer, offset: 0, index: 1) renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6) }
代码从上往下,先是创建顶点缓冲对象,绑定到Vertex Shader Index为0的缓冲区,然后创建渐变色数组缓冲对象,绑定到Fragment Shader Index为0的缓冲区。最后创建一个Uniform缓冲对象,里面只有渐变色的个数,绑定到Fragment Shader Index为1的缓冲区。都绑定完后,使用drawPrimitives绘制三角形。
Shader
swift的代码差不多就结束了,接下来我们来看看Shader是怎么写的。Metal的Shader以metal为后缀。本文Shader文件名为qrcode.metal。Vertex Shader和Fragment Shader可以在一个文件中,通过对方法的修饰区分。我们先来看一下Shader的全貌。
#include using namespace metal; struct VertexIn { packed_float3 position; packed_float2 uv; }; struct VertexOut { float4 position [[position]]; float2 uv; }; struct Uniforms { int colorCount; }; vertex VertexOut passThroughVertex(uint vid [[ vertex_id ]], const device VertexIn* vertexIn [[ buffer(0) ]]) { VertexOut outVertex; VertexIn inVertex = vertexIn[vid]; outVertex.position = float4(inVertex.position, 1.0); outVertex.uv = inVertex.uv; return outVertex; }; constexpr sampler s(coord::normalized, address::repeat, filter::linear); fragment float4 passThroughFragment(VertexOut inFrag [[stage_in]], texture2d diffuse [[ texture(0) ]], const device packed_float3* colors [[ buffer(0) ]], const device Uniforms& uniform [[ buffer(1) ]]) { int colorCount = uniform.colorCount; float colorEffectRange = 1.0 / (colorCount - 1.0); float3 gradientColor = float3(0.0); int colorZoneIndex = inFrag.uv.y / colorEffectRange; colorZoneIndex = colorZoneIndex >= colorCount - 1 ? colorCount - 2 : colorZoneIndex; float effectFactor = (inFrag.uv.y - colorZoneIndex * colorEffectRange) / colorEffectRange; gradientColor = colors[colorZoneIndex] * (1.0 - effectFactor) + colors[colorZoneIndex + 1] * effectFactor; float4 qrcodeColor = diffuse.sample(s, inFrag.uv); if (qrcodeColor.r > 0.5) { discard_fragment(); } else { return float4(gradientColor, 1.0); } };
Vertex Shader
Metal中Vertex Shader的入口方法需要以 vertex开头,本文的入口方法如下。它做的事情很简单,接受传递进来的顶点数据,包括位置和uv,然后原封不动的传递给Fragment Shader。我们在主程序中将顶点数据绑定到了缓冲区0,所以顶点数据参数后面使用[[ buffer(0) ]]标记。
vertex VertexOut passThroughVertex(uint vid [[ vertex_id ]], const device VertexIn* vertexIn [[ buffer(0) ]]) { VertexOut outVertex; VertexIn inVertex = vertexIn[vid]; outVertex.position = float4(inVertex.position, 1.0); outVertex.uv = inVertex.uv; return outVertex; };
Fragment Shader
Fragment Shader以fragment开头,接受了3个参数,二维码纹理diffuse,渐变色数组colors,渐变色个数uniform.colorCount。我们还申明了一个sampler s,用来对纹理进行采样。
constexpr sampler s(coord::normalized, address::repeat, filter::linear); fragment float4 passThroughFragment(VertexOut inFrag [[stage_in]], texture2d diffuse [[ texture(0) ]], const device packed_float3* colors [[ buffer(0) ]], const device Uniforms& uniform [[ buffer(1) ]]) { int colorCount = uniform.colorCount; float colorEffectRange = 1.0 / (colorCount - 1.0); float3 gradientColor = float3(0.0); int colorZoneIndex = inFrag.uv.y / colorEffectRange; colorZoneIndex = colorZoneIndex >= colorCount - 1 ? colorCount - 2 : colorZoneIndex; float effectFactor = (inFrag.uv.y - colorZoneIndex * colorEffectRange) / colorEffectRange; gradientColor = colors[colorZoneIndex] * (1.0 - effectFactor) + colors[colorZoneIndex + 1] * effectFactor; float4 qrcodeColor = diffuse.sample(s, inFrag.uv); if (qrcodeColor.r > 0.5) { discard_fragment(); } else { return float4(gradientColor, 1.0); } };
代码主要是在计算当前像素的取色,首先计算出当前像素点在渐变色的哪一段,得到colorZoneIndex,然后根据像素在这一段的位置,使用两端的颜色计算最终的像素颜色。最后取出二维码的颜色,如果二维码是偏白色的就直接忽略这个像素,这样就会显示主程序里配置的清除色clearColor。反之,直接返回计算出来的渐变色。我使用r > 0.5来判断是不是白色像素。这样出来的效果就是,白色的部分被替换为clearColor,黑色部分变成对应的渐变色。
总结
本文我们使用Metal实现了渐变二维码的效果, 相对遮罩技术来说,没有任何技巧性,粗暴的将每个像素处理成我们需要的颜色。当然你也可以使用Fragment Shader中的算法,直接在CPU中处理每个像素。不过很明显这种任务GPU更合适。
作者:handyTOOL
链接:http://www.jianshu.com/p/8c909f515e78
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。