写在前面的话
我个人并不是iOS或者macOS的开发工程师,只是之前用GLES2.0做过一些项目,前段时间知道苹果公司已经决定弃用OpenGL和OpenCL。以前做图形相关的项目,只需要维护一套GLES的代码就可以支持Apple、Linux、Android以及市面几乎所有的嵌入式厂商。虽然GLES还没有完全被苹果彻底弃用,但是还是想学习一下这个新的图形底层API---Metal
学习前的准备
Apple目前推荐的官方开发语言是Swift,而且Metal也没有提供C++的API,不过网上有大神用C++封装了一层,可以参考一下。链接地址:https://github.com/naleksiev/mtlpp。不过还是决定试试使用Swift,代码中的各种?和!都是系统提示我的,所以肯定有不完善的地方。 另外手边没有iPhone,所以demo程序是运行在Mac上的。
正题
工程创建
用Xcode创建一个macOS的Cocoa App项目,项目名字可以随便填一个,我这面填的是MyCode。语言选择Swift。Team那个地方选择自己的账号。其他的根据需要填写。
PS:需要提前注册一个Apple ID,去Apple的开发者网站激活一下,不需要付费就可以在mac上调试自己的程序了。具体方法就不详细说明了。
想做什么
因为在GLES中FBO这个东西被用到的次数非常多,所以想看看这个东西在Metal中怎么实现。我是按照GLES的思路先渲染到一个中间Texture上,再渲染到屏幕。不过貌似Metal推荐的做法是使用Compute(就是替代原来OpenCL的东西)。
开始写代码
先实现Texture直接渲染到屏幕上
基本代码结构
Metal貌似支持渲染在MTKView和CAMetalLayer上,我采用MTKView,并且添加一个渲染类来实现真正的渲染工作。
在系统为我们创建好的ViewController.swift中的viewDidLoad函数中添加MTKView的初始化工作。这部分的实现非常简单就一个init函数就搞定了,感觉这部分相当于原来EGL的各种操作,只不过系统帮你实现了。然后我想添加一个渲染类来处理描画。
- 创建TextureRender 类
在构造函数将MTKView作为参数传进来,并加一个render函数用来描画。代码大概是下面的样子:
import Foundation
import MetalKit
class TextureRender {
//MARK: Properties
var _mtkView: MTKView!
//MARK: Initialization
init?( mtkView:MTKView ) {
self._mtkView = mtkView
}
// MARK : render method
func render() -> Void {
}
- ViewController中的实现
- 在ViewDidLoad函数中实例MTKView和TextureRender,并修改ViewController的View为MTKView
override func viewDidLoad() {
super.viewDidLoad()
let mtkView = MTKView.init(frame: self.view.bounds)
mtkView.delegate = self
self.view = mtkView
self._render = TextureRender(mtkView: mtkView)
// Do any additional setup after loading the view.
}
因为要设置mtkView的代理实例,所以要继承MTKViewDelegate以及实现代理方法。最后ViewController的代码如下:
(到目前为止,ViewController中所有工作以及完成了)
//
// ViewController.swift
// MyCode
//
// Created by larry-kof on 2018/11/26.
// Copyright © 2018 larry-kof. All rights reserved.
//
import Cocoa
import MetalKit
class ViewController: NSViewController, MTKViewDelegate {
var _render:TextureRender!
override func viewDidLoad() {
super.viewDidLoad()
let mtkView = MTKView.init(frame: self.view.bounds)
mtkView.delegate = self
self.view = mtkView
self._render = TextureRender(mtkView: mtkView)
// Do any additional setup after loading the view.
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
// MARK: delegate
func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
}
func draw(in view: MTKView) {
self._render.render()
}
}
TextureRender实现
根据GLES中的经验,下一步就要编译vertex shader和fragment shader生成glProgram。不过在metal中需要一个device就是选择一个硬件设备(GPU或者CPU),和OpenCL中的clGetDeviceIDs一样。所以在构造函数中获取device,同时记住mtkView的大小以后来设置描画Viewport的大小。
init?( mtkView:MTKView ) {
self._mtkView = mtkView
self._mtkView.device = MTLCreateSystemDefaultDevice()
self._portViewSize = self._mtkView.drawableSize
}
创建MTLRenderPipelineState
这一步相当于GLES中glCreateProgram + glLinkProgram
和GLES类似,需要些vertex shader和fragment shader
在工程里添加新文件选择Metal File
- shader
Meta shader代码如下:
//
// shader.metal
// MyCode
//
// Created by larry-kof on 2018/11/26.
// Copyright © 2018 larry-kof. All rights reserved.
//
#include
#include "ShaderTypes.h"
using namespace metal;
struct RasterizerData{
float4 position [[position]];
float2 texCoord;
};
vertex RasterizerData
texVertexShader(uint vid [[vertex_id]],
constant Vertex* vertexArray [[ buffer(0) ]])
{
RasterizerData out;
out.position = vertexArray[vid].position;
out.texCoord = vertexArray[vid].texCoord;
return out;
}
fragment float4
texFragmentShader(RasterizerData input [[stage_in]],
texture2d inputTexture [[ texture(0) ]])
{
constexpr sampler textureFilter (mag_filter::linear,
min_filter::linear);
float4 colorSample = inputTexture.sample(textureFilter, input.texCoord);
return colorSample;
}
ShaderTypes.h的代码如下:
//
// ShaderTypes.h
// MyCode
//
// Created by larry-kof on 2018/11/26.
// Copyright © 2018 larry-kof. All rights reserved.
//
#ifndef ShaderTypes_h
#define ShaderTypes_h
#include
struct Vertex
{
vector_float4 position;
vector_float2 texCoord;
};
#endif /* ShaderTypes_h */
- 说明
ShadeTypes.h中的position和texCoord:我们要从swift程序传过来的值
float4 position [[position]]; :告诉shader去哪找vertex坐标,相当于gl_Position的作用
uint vid [[vertex_id]] :因为传过来的vertex坐标最少三个,vid是index
constant Vertex* vertexArray [[ buffer(0) ]]:
(1) constant: 当变量是pointer或者ref,metal规定前面需要加constant或者device 来修饰
(2) [[ buffer(0) ]]:相当于glVertexAttribPointer中的index
RasterizerData input [[stage_in]] :stage_in表示从vertex shader传到fragment shader的数据,类似于GLES2.0 shader的varying关键字或者GLES3.0 的 in/out 关键字
另外关于float4,half4等等变量从代码角度代替了vec4。区别应该是精度。
- Swift
追加一个setupPipe的private方法,代码如下:
private func setupPipe() {
let defaultLibrary = self._mtkView.device?.makeDefaultLibrary()
let vextexFunction = defaultLibrary?.makeFunction(name: "texVertexShader")
let fragFunction = defaultLibrary?.makeFunction(name: "texFragmentShader")
let pipelineStateDesc = MTLRenderPipelineDescriptor.init()
pipelineStateDesc.vertexFunction = vextexFunction
pipelineStateDesc.fragmentFunction = fragFunction
pipelineStateDesc.colorAttachments[0].pixelFormat = self._mtkView.colorPixelFormat
do {
try self._pipelineState = self._mtkView.device?.makeRenderPipelineState(descriptor: pipelineStateDesc)
} catch {
print(error)
}
}
- 说明
let defaultLibrary = self._mtkView.device?.makeDefaultLibrary()
Metal会去寻找工程下所有的metal文件并加载进来。
makeFunction中的name就是对应shader的函数名,这一步相当于glCreateshader+ glShaderSource+ glCompileShader
makeRenderPipelineState 相当于glCreaterProgram+ glAttachShader+ glLinkProgram
创建顶点Buffer
创建setupVertex的private方法,因为想直接使用ShaderTypes.h中的Vertex结构体,Swift不支持直接引用头文件,只能通过bridge的方式,也是好麻烦,突然觉得这个语言不是很好用了。。。
构建XXX-Bridge-Header.h
右键工程文件夹(是黄色的那个) -> New File -> macOS -> Header File ,文件名一般是XXX-Bridge-Header.h,
其中XXX是你的工程名,我的话就是MyCode-Bridge-Header.h配置 Bridge Header
点击工程(蓝色的) -> Build Settings -> 找到Objective-C Bridging Header -> 输入
$(PRODUCT_NAME)/$(PRODUCT_NAME)-Bridging-Header.h
XXX-Bridge-Header.h代码
#import "ShaderTypes.h"
setupVertex代码
其中坐标的范围和GLES相同 vertex是 [-1,1 ], texCoord是[0,1]
这一步相当于glGenBuffers + glBindBuffer + glBufferData
private func setupVertex() {
let qVertices = [Vertex(position: vector_float4([-1.0, -1.0, 0.0, 1.0]), texCoord: vector_float2([0.0, 0.0])),
Vertex(position: vector_float4([ -1.0, 1.0, 0.0, 1.0]), texCoord: vector_float2([0.0, 1.0])),
Vertex(position: vector_float4([1.0, -1.0, 0.0, 1.0]), texCoord: vector_float2([1.0, 0.0])),
Vertex(position: vector_float4([1.0, 1.0, 0.0, 1.0]), texCoord: vector_float2([1.0, 1.0])),
]
self._vertice = self._mtkView.device?.makeBuffer(bytes: qVertices, length: qVertices.count * MemoryLayout.size , options: .storageModeShared)
self._numVertice = qVertices.count
}
创建InputTexture
- 资源
这里面采用TAG文件,因为想用官方Hello Compute提供的AAPLImage来解码。
- 找到一个TGA文件
好像没法插入TGA文件格式的图片,大家去网络找一个,或者我最后分享的整个工程代码中找到,或者使用其他的解码方式吧。。。
导入AAPLImage.m 和 AAPLImage.h文件
生成texture
整体来说就是glTexImage2D
其中Swift要想获得数据的指针真的有点麻烦,要使用withUnsafeBytes 这个方法,可能是为了指针使用的安全吧
private func setupTexture() {
let url = Bundle.main.url(forResource: "miami_beach", withExtension: "tga")
let image = AAPLImage.init(tgaFileAtLocation: url!)
let textureDesc = MTLTextureDescriptor.init()
textureDesc.width = (image?.width)!
textureDesc.height = (image?.height)!
textureDesc.pixelFormat = .bgra8Unorm
textureDesc.usage = .shaderRead
textureDesc.textureType = .type2D
self._inputTexture = self._mtkView.device?.makeTexture(descriptor: textureDesc)
let region = MTLRegionMake2D(0, 0, textureDesc.width, textureDesc.height)
image?.data.withUnsafeBytes {
( bytes:UnsafePointer ) in
let rawPtr = UnsafeRawPointer(bytes)
self._inputTexture.replace(region: region, mipmapLevel: 0, withBytes: rawPtr, bytesPerRow: 4 * textureDesc.width)
}
}
- 在构造函数添加setup方法
//MARK: Initialization
init?( mtkView:MTKView ) {
self._mtkView = mtkView
self._mtkView.device = MTLCreateSystemDefaultDevice()
self._portViewSize = self._mtkView.drawableSize
self.customInit()
}
// MARK: private function
private func customInit() {
self.setupPipe()
self.setupVertex()
self.setupTexture()
self._commandQueue = self._mtkView.device?.makeCommandQueue()
}
render 方法的实现
首先追加一个draw方法
func draw(commandBuffer:MTLCommandBuffer, texture: MTLTexture, desDrawble:CAMetalDrawable) {
}
根据GLES的描画步骤是
- Clear Color, Depth Stencil等 -> commandBuffer.makeRenderCommandEncoder
- glSetViewport -> renderEncoder?.setViewport
- glUseProgram -> renderEncoder?.setRenderPipelineState
- glVertexAttribPointer -> renderEncoder?.setVertexBuffer
- 设置其他 -> renderEncoder?.setFragmentTexture
- glDrawArray -> renderEncoder?.drawPrimitives
所以有以下的代码
func draw(commandBuffer:MTLCommandBuffer, texture: MTLTexture, desDrawble:CAMetalDrawable) {
let renderPassDesc = self._mtkView.currentRenderPassDescriptor
if renderPassDesc != nil {
renderPassDesc?.colorAttachments[0].clearColor = MTLClearColorMake(0.0, 0.0, 0.0, 0.0)
renderPassDesc?.colorAttachments[0].loadAction = .clear
let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDesc!)
renderEncoder?.setViewport(MTLViewport.init(originX: 0.0, originY: 0.0, width: Double(self._portViewSize!.width), height: Double(self._portViewSize!.height), znear: -1.0, zfar: 1.0))
renderEncoder?.setRenderPipelineState(self._pipelineState)
renderEncoder?.setFragmentTexture(texture, index: 0)
renderEncoder?.setVertexBuffer(self._vertice, offset: 0, index: 0)
renderEncoder?.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: self._numVertice)
renderEncoder?.endEncoding()
commandBuffer.present(desDrawble)
}
}
最后完成render函数
// MARK : render method
func render() -> Void {
let commandBuffer = self._commandQueue?.makeCommandBuffer()
draw(commandBuffer: commandBuffer!, texture: self._inputTexture, desDrawble: self._mtkView.currentDrawable!)
commandBuffer!.commit()
}
最后的commit,类似glFinish + eglSwapBuffers
最后的执行画面是
FBO
创建FBORender类,继承TextureRender
创建新的shader将原图改变成灰度图
constant float3 kRec709Luma = float3(0.2126, 0.7152, 0.0722);
fragment float4
fboFragmentShader(RasterizerData input [[stage_in]],
texture2d inputTexture [[ texture(0) ]])
{
constexpr sampler textureFilter (mag_filter::linear,
min_filter::linear);
float4 colorSample = inputTexture.sample(textureFilter, input.texCoord);
float gray = dot(colorSample.rgb, kRec709Luma);
return float4( gray, gray, gray, 1.0 );
}
在FBORender中,和之前创建新的PipelineState和middleTexture用来接收中间处理结果,并作为渲染到屏幕上的源。不同的是生成middleTexture的时候,需要添加renderTarget属性。
textureDesc.usage = [.renderTarget, .shaderRead]
添加drawToTexture函数,不同的是需要设置
renderPassDesc.colorAttachments[0].texture = self._middleTexture
感觉相当于glBindFramebuffer, 最后FBORender.swift的代码如下
//
// FBORender.swift
// MyCode
//
// Created by larry-kof on 2018/11/29.
// Copyright © 2018 larry-kof. All rights reserved.
//
import Foundation
import MetalKit
class FBORender:TextureRender {
private var _fboPipelineState: MTLRenderPipelineState!
private var _middleTexture: MTLTexture!
override init?(mtkView: MTKView) {
super.init(mtkView: mtkView)
self.setupFBOPipe()
self.setupMiddleTexture()
}
// MARK : private func
private func setupFBOPipe() {
let defaultLibrary = self._mtkView.device?.makeDefaultLibrary()
let vextexFunction = defaultLibrary?.makeFunction(name: "texVertexShader")
let fragFunction = defaultLibrary?.makeFunction(name: "fboFragmentShader")
let pipelineStateDesc = MTLRenderPipelineDescriptor.init()
pipelineStateDesc.vertexFunction = vextexFunction
pipelineStateDesc.fragmentFunction = fragFunction
pipelineStateDesc.colorAttachments[0].pixelFormat = self._inputTexture.pixelFormat
do {
try self._fboPipelineState = self._mtkView.device?.makeRenderPipelineState(descriptor: pipelineStateDesc)
} catch {
print(error)
}
}
private func setupMiddleTexture() {
let textureDesc = MTLTextureDescriptor.init()
textureDesc.width = self._inputTexture.width
textureDesc.height = self._inputTexture.height
textureDesc.pixelFormat = self._inputTexture.pixelFormat
textureDesc.usage = [.renderTarget, .shaderRead]
textureDesc.textureType = .type2D
self._middleTexture = self._mtkView.device?.makeTexture(descriptor: textureDesc)
}
private func drawToMiddleTexture(command: MTLCommandBuffer, texture: MTLTexture) {
let renderPassDesc = MTLRenderPassDescriptor.init()
renderPassDesc.colorAttachments[0].clearColor = MTLClearColorMake(0.0, 0.0, 0.0, 0.0)
renderPassDesc.colorAttachments[0].loadAction = .clear
renderPassDesc.colorAttachments[0].texture = self._middleTexture
let renderEncoder = command.makeRenderCommandEncoder(descriptor: renderPassDesc)
renderEncoder?.setViewport(MTLViewport.init(originX: 0.0, originY: 0.0, width: Double(self._middleTexture.width), height: Double(self._middleTexture.height), znear: -1.0, zfar: 1.0))
renderEncoder?.setRenderPipelineState(self._fboPipelineState)
// renderEncoder?.setVertexBuffer(self._vertice, offset: 0, index: 0)
renderEncoder?.setFragmentTexture(texture, index: 0)
renderEncoder?.setVertexBuffer(self._vertice, offset: 0, index: 0)
renderEncoder?.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: self._numVertice)
renderEncoder?.endEncoding()
}
// MARK override method
override func render() -> Void {
let commanBuffer = self._commandQueue?.makeCommandBuffer()
self.drawToMiddleTexture(command: commanBuffer!, texture: self._inputTexture)
super.draw(commandBuffer: commanBuffer!, texture: self._middleTexture, desDrawble: self._mtkView.currentDrawable!)
commanBuffer!.commit()
}
}
最后的最后将ViewController.swift中的
self._render = TextureRender(mtkView: mtkView)
变成
self._render = FBORender.init(mtkView: mtkView)
执行结果:(不知道为什么上下反了,直接渲染的话是正常的,坐标我看了几遍感觉没问题,不知道哪里错了。还请大神指教。也许以后应该使用Compute做中间渲染吧)
工程整体代码
https://github.com/larry-kof/Metal_Study_Demo