前言
這里是一篇Metal新手教程,先定個小目標:把繪制一張圖片到屏幕上。
Metal系列教程的代碼地址;
OpenGL ES系列教程在這里;
你的star和fork是我的源動力,你的意見能讓我走得更遠。
正文
核心思路
通過MetalKit,盡量簡單地實現把一張圖片繪制到屏幕,核心的內容包括:設置渲染管道、設置頂點和紋理緩存、簡單的shader理解。
效果展示
具體步驟
1、新建MTKView
// 初始化 MTKView
self.mtkView = [[MTKView alloc] initWithFrame:self.view.bounds];
self.mtkView.device = MTLCreateSystemDefaultDevice(); // 獲取默認的device
self.view = self.mtkView;
self.mtkView.delegate = self;
self.viewportSize = (vector_uint2){self.mtkView.drawableSize.width, self.mtkView.drawableSize.height};
MTKView
是MetalKit提供的一個View,用來顯示Metal的繪制;
MTLDevice
代表GPU設備,提供創建緩存、紋理等的接口;
2、設置渲染管道
// 設置渲染管道
-(void)setupPipeline {
id<MTLLibrary> defaultLibrary = [self.mtkView.device newDefaultLibrary]; // .metal
id<MTLFunction> vertexFunction = [defaultLibrary newFunctionWithName:@"vertexShader"]; // 頂點shader,vertexShader是函數名
id<MTLFunction> fragmentFunction = [defaultLibrary newFunctionWithName:@"samplingShader"]; // 片元shader,samplingShader是函數名
MTLRenderPipelineDescriptor *pipelineStateDescriptor = [[MTLRenderPipelineDescriptor alloc] init];
pipelineStateDescriptor.vertexFunction = vertexFunction;
pipelineStateDescriptor.fragmentFunction = fragmentFunction;
pipelineStateDescriptor.colorAttachments[0].pixelFormat = self.mtkView.colorPixelFormat;
self.pipelineState = [self.mtkView.device newRenderPipelineStateWithDescriptor:pipelineStateDescriptor
error:NULL]; // 創建圖形渲染管道,耗性能操作不宜頻繁調用
self.commandQueue = [self.mtkView.device newCommandQueue]; // CommandQueue是渲染指令隊列,保證渲染指令有序地提交到GPU
}
MTLRenderPipelineDescriptor
是渲染管道的描述符,可以設置頂點處理函數、片元處理函數、輸出顏色格式等;
[device newCommandQueue]
創建的是指令隊列,用來存放渲染的指令;
3、設置頂點數據
- (void)setupVertex {
static const LYVertex quadVertices[] =
{ // 頂點坐標,分別是x、y、z、w; 紋理坐標,x、y;
{ { 0.5, -0.5, 0.0, 1.0 }, { 1.f, 1.f } },
{ { -0.5, -0.5, 0.0, 1.0 }, { 0.f, 1.f } },
{ { -0.5, 0.5, 0.0, 1.0 }, { 0.f, 0.f } },
{ { 0.5, -0.5, 0.0, 1.0 }, { 1.f, 1.f } },
{ { -0.5, 0.5, 0.0, 1.0 }, { 0.f, 0.f } },
{ { 0.5, 0.5, 0.0, 1.0 }, { 1.f, 0.f } },
};
self.vertices = [self.mtkView.device newBufferWithBytes:quadVertices
length:sizeof(quadVertices)
options:MTLResourceStorageModeShared]; // 創建頂點緩存
self.numVertices = sizeof(quadVertices) / sizeof(LYVertex); // 頂點個數
}
頂點數據里包括頂點坐標,metal的世界坐標系與OpenGL ES一致,范圍是[-1, 1],故而點(0, 0)是在屏幕的正中間;
頂點數據里還包括紋理坐標,紋理坐標系的取值范圍是[0, 1],原點是在左下角;
[device newBufferWithBytes:quadVertices..]
創建的是頂點緩存,類似OpenGL ES的glGenBuffer創建的緩存。
4、設置紋理數據
- (void)setupTexture {
UIImage *image = [UIImage imageNamed:@"abc"];
// 紋理描述符
MTLTextureDescriptor *textureDescriptor = [[MTLTextureDescriptor alloc] init];
textureDescriptor.pixelFormat = MTLPixelFormatRGBA8Unorm;
textureDescriptor.width = image.size.width;
textureDescriptor.height = image.size.height;
self.texture = [self.mtkView.device newTextureWithDescriptor:textureDescriptor]; // 創建紋理
MTLRegion region = {{ 0, 0, 0 }, {image.size.width, image.size.height, 1}}; // 紋理上傳的范圍
Byte *imageBytes = [self loadImage:image];
if (imageBytes) { // UIImage的數據需要轉成二進制才能上傳,且不用jpg、png的NSData
[self.texture replaceRegion:region
mipmapLevel:0
withBytes:imageBytes
bytesPerRow:4 * image.size.width];
free(imageBytes); // 需要釋放資源
imageBytes = NULL;
}
}
MTLTextureDescriptor
是紋理數據的描述符,可以設置像素顏色格式、圖像寬高等,用于創建紋理;
紋理創建完畢后,需要用-replaceRegion: mipmapLevel:withBytes:bytesPerRow:
接口上傳紋理數據;
MTLRegion
類似UIKit的frame
,用于表明紋理數據的存放區域;
5、具體渲染過程
- (void)drawInMTKView:(MTKView *)view {
// 每次渲染都要單獨創建一個CommandBuffer
id<MTLCommandBuffer> commandBuffer = [self.commandQueue commandBuffer];
MTLRenderPassDescriptor *renderPassDescriptor = view.currentRenderPassDescriptor;
// MTLRenderPassDescriptor描述一系列attachments的值,類似GL的FrameBuffer;同時也用來創建MTLRenderCommandEncoder
if(renderPassDescriptor != nil)
{
renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColorMake(0.0, 0.5, 0.5, 1.0f); // 設置默認顏色
id<MTLRenderCommandEncoder> renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor]; //編碼繪制指令的Encoder
[renderEncoder setViewport:(MTLViewport){0.0, 0.0, self.viewportSize.x, self.viewportSize.y, -1.0, 1.0 }]; // 設置顯示區域
[renderEncoder setRenderPipelineState:self.pipelineState]; // 設置渲染管道,以保證頂點和片元兩個shader會被調用
[renderEncoder setVertexBuffer:self.vertices
offset:0
atIndex:0]; // 設置頂點緩存
[renderEncoder setFragmentTexture:self.texture
atIndex:0]; // 設置紋理
[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle
vertexStart:0
vertexCount:self.numVertices]; // 繪制
[renderEncoder endEncoding]; // 結束
[commandBuffer presentDrawable:view.currentDrawable]; // 顯示
}
[commandBuffer commit]; // 提交;
}
drawInMTKView:
方法是MetalKit每幀的渲染回調,可以在內部做渲染的處理;
繪制的第一步是從commandQueue里面創建commandBuffer,commandQueue是整個app繪制的隊列,而commandBuffer存放每次渲染的指令,commandQueue內部存在著多個commandBuffer。
整個繪制的過程與OpenGL ES一致,先設置窗口大小,然后設置頂點數據和紋理,最后繪制兩個三角形。
CommandQueue、CommandBuffer和CommandEncoder的關系如下:
6、Shader處理
typedef struct
{
float4 clipSpacePosition [[position]]; // position的修飾符表示這個是頂點
float2 textureCoordinate; // 紋理坐標,會做插值處理
} RasterizerData;
vertex RasterizerData // 返回給片元著色器的結構體
vertexShader(uint vertexID [[ vertex_id ]], // vertex_id是頂點shader每次處理的index,用于定位當前的頂點
constant LYVertex *vertexArray [[ buffer(0) ]]) { // buffer表明是緩存數據,0是索引
RasterizerData out;
out.clipSpacePosition = vertexArray[vertexID].position;
out.textureCoordinate = vertexArray[vertexID].textureCoordinate;
return out;
}
fragment float4
samplingShader(RasterizerData input [[stage_in]], // stage_in表示這個數據來自光柵化。(光柵化是頂點處理之后的步驟,業務層無法修改)
texture2d<half> colorTexture [[ texture(0) ]]) // texture表明是紋理數據,0是索引
{
constexpr sampler textureSampler (mag_filter::linear,
min_filter::linear); // sampler是采樣器
half4 colorSample = colorTexture.sample(textureSampler, input.textureCoordinate); // 得到紋理對應位置的顏色
return float4(colorSample);
}
Shader如上。與OpenGL ES的shader相比,最明顯是輸入的參數可以用結構體,返回的參數也可以用結構體;
LYVertex
是shader和Objective-C公用的結構體,RasterizerData
是頂點Shader返回再傳給片元Shader的結構體;
Shader的語法與C++類似,參數名前面的是類型,后面的[[ ]]
是描述符。
總結
Metal和OpenGL一樣,需要有一定的圖形學基礎,才能理解具體的含義。
本文為了降低上手的門檻,簡化掉一些邏輯,增加很多注釋,同時保留最核心的幾個步驟以便理解。
這里可以下載demo代碼。