Metal着色器语言介绍并使用其实现三角形绘制

1,163 阅读8分钟

什么是Metal?

早在2014年的WWDC大会上,Apple为游戏开发者推出了新的平台技术 Metal,该技术能够为 3D 图像提高 10 倍的渲染性能,并支持大家熟悉的游戏引擎及公司。

什么是Metal着色器语言?

Metal着色器语言是iOS开发中一个用来编写【3D图形渲染逻辑】和并行【并行计算核心逻辑】的编程语言,编写Metal框架的App需要使用Metal着色器语言程序. Metal着色器语言主与Metal框架配合使用,Metal框架管理Metal着色器语言的运行和可选编译先项。Metal着色器语言使用Clang和LVVM进行编译处理,编译器对于在GPU上的代码执行效率有更好的控制。

Metal语法

Metal数据类型

  • bool布类型,true/false
  • char 有符号8位整数
  • unsigned char/uchar 无符号8bit整数
  • short 有符号16bit整数
  • unsigned short/ushort 无符号16bit整数
  • half 16bit浮点数
  • float 32bit浮点数
  • size_t 64bit无符号整数
  • void 该类型表示一个空的集合

注意:Metal支持后缀表示字面量类型,例如:0.5f,0.5F,0.5h,0.5H

纹理类型

纹理类型是一个句柄,它指向一个一维/二维/三维纹理数据. 在一个函数中描述纹理对象的类型; 纹理类型有三种访问权限:

  • sample:可读可写
  • read:只读
  • write:只写

使用规则:

  • texture1d<T,access, a=access::sample>
  • texture2d<T,access, a=access::sample>
  • texture3d<T,access, a=access::sample>

T:数据类型,设定了从纹理中读取或向纹理中写入时的颜色类型,T可以是half,float,short,int等。

代码示例:

void foo (texture2d<float> imgA [[ texture(0) ]] ,
texture2d<float, access::read> imgB [[ texture(1) ]],
 texture2d<float, access::write> imgC [[ texture(2) ]])
{ 
... 
}
  • void:无返回值
  • foo:函数名
  • texture2d imgA [[ texture(0) ]] 默认使用sample;
  • texture2d<float, access::read> imgB [[ texture(1) ]] 使用read
  • texture2d<float, access::write> imgC [[ texture(2) ]] 使用write

采样器类型

采取器类型决定了如何对一个纹理进行采样操作.在Metal框架中有一个对应着色器语言的采样器的对象

  • enum class coord{normalized,pixel} 纹理中采样时,纹理坐标是否需要归一化
  • enum class filter{nearest,linear} 纹理采样过滤方式,放大/缩小滤方式
  • enum class min_filter{nearest,linear} 设置纹理采样的缩小过滤模式
  • enum class mag_filter{nearest,linear} 设置纹理采样的放大过滤模式

设置纹理s,t,r坐档的寻址模式

  • enum class s_address{clamp_to_zero,clamp_to_edge,repeat,mirrored_repeat};
  • enum class t_address{clamp_to_zero,clamp_to_edge,repeat,mirrored_repeat};
  • enum class r_address{clamp_to_zero,clamp_to_edge,repeat,mirrored_repeat};

设置所有纹理坐标的寻址模式 enum class address{clamp_to_zero,clamp_to_edge,repeat,mirrored_repeat}

设置纹理采样的mipMap过滤模式,如果是none,那么只有一层纹理生效 enum class mip_filter{none,nearest,linear}

注意:在Metal程序中初始化采样器必须使用constexpr修饰符声明

constexpr sampler s(coord::pixel,address::clamp_to_zero,filter::linear)

函数修饰符

  • kernel:表示该函数是一个并行计算着色函数,它可以被分配在一维/二维/三维线程中去执行;
  • vertex: 表示该函数是一个顶点着色函数,它将为顶点数据流中的每一个顶点执行一次然后为每一个顶点生成数据输出到绘制管线;
  • fragment: 表示该函数是一个片元函数,它将为片元数据流中的每一个片元和其关联执行一次然后将每一个片元的颜色数据输出到绘制管线中.

注意:使用kernel修饰的函数,其返回值类型必须是void类型

只有图形着色函数才可以被vertex和fragment修饰。对于图形着色函数,返回值类型可以辨认出它是为顶点做计算还是为像素做计算。图形着色函数的返回值可以为void,但是这也就意味着该函数不产生数据输出到绘制管线,这是一个无意义的操作。

注意:一个被函数修饰符修饰的函数不可以再调用一个被函数修饰符修饰的函数,否则会导致失败。

kernel void hello1(){}
  
kernel void hello2(){
   hello1();//这样调用是不行滴。  
}
  

用于变量或者参数的地址空间修饰符

  • device : 设备地址空间,指向设备内存池分配出来的缓存对象,它是可读也是可写的;一缓存对象可以被声明成一个标量,向量或是用户自定义结构体的指针或是引用。

注意:纹理对象总是在设备地址空间分配内存,device地址空间修饰符不必出现在纹理类型定义中,一个纹理对象的内容无法直接访问。Metal提供读纹理的内建函数

  • threadgroup:线程组地址空间,用于并行计算着色器函数分配内存变量,被其修饰的变量被线程组的所有线程共享,在线程组地址空间分配的变量不能被用于图形绘制着色函数使用.
  • constant:常量地址空间,指向的缓存对象也在设备内存池中,但是是只读的;

注意:常量地址空间的指针或是引用可以作为函数的参数,向声明为常量的变量赋值会产生编译错误,声明常量但是没有赋予初值也会产生编译错误。

  • thread:线程地址空间,修饰的变量只在当前线程可用,其它线程不可用。在图形绘制着色函数或者并行计算着色器函数中声明的变量地址空间分配。

对于图形着色器函数,其指针或是引用类型的参数必须定义为device或是constant地址空间;对于并行计算着色函数,其指针或是引用类型的参数必须定义为device或是threadgroup或是constant地址空间.

函数参数与变量

函数参数传递到函数中,都有一个存的位置,分别有以下四种及修饰方式:

  • device buffer/constant buffer : [[buffer(index)]],buffer固定写法,index是uint类型数据,下同;表示缓存的位置
  • texture : [[texture(index)]] , texture固定写法;表示纹理的位置
  • sampler : [[sampler(index)]] , sampler固定写法;表示采样器的位置
  • threadgroup buffer : [[threadgroup(index)]] ,threadgroup固定写法
kernel void add_vectors(
  const device float4 *inA [[ buffer(0) ]],
const device float4 *inB [[ buffer(1) ]], 
  device float4 *out [[ buffer(2) ]],
uint id [[ thread_position_in_grid ]])
{
 out[id] = inA[id] + inB[id];
}
  

以上例子中展示了一个简单的并行计算着色函数,它把两个设备地址空间中的缓存inA和inB相加,然后把结果写入到out,属性修饰符"[[buffer(index)]]"为着色器函数参数设定的缓存位置;thread_position_in_grid:用于表示当前节点在多线程网格中的位置。

内建变量属性修饰符

  • [[vertex_id]] 顶点id标识符
  • [[position]] 顶点信息
  • [[point_size]] 点的大小
  • [[color(m)]] 颜色
  • [[stage_in]] 片元着色函数使用的片元输入数据是由顶点着色函数输出后经过光栅化生成的,顶点着色函数和片元着色函数都只能有一个参数被“stage_in”修饰,对于被"stage_in"修饰的结构体,其成员必须是整形或浮点形的标量或向量。

使用Metal着色器语言渲染一个三角形

创建OC和Metal语言共用的数据类型文件CCShaderTypes.h

//
//  CCShaderTypes.h
//  MetalTriangle
//
//  Created by iot_user on 2020/8/21.
//  Copyright © 2020 IOT. All rights reserved.
//

#ifndef CCShaderTypes_h
#define CCShaderTypes_h

typedef enum CCVertexInputIndex{
    //顶点
    CCVertexInputIndexVertices     = 0,
    //视图大小
    CCVertexInputIndexViewportSize = 1,
    
} CCVertexInputIndex;


typedef struct {
    //顶点
    vector_float4 position;
    //颜色
    vector_float4 color;
    
} CCVertex;

#endif /* CCShaderTypes_h */

  

创建iOS工程并生成.metal文件CCShaders.metal

//
//  CCShaders.metal
//  MetalTriangle
//
//  Created by iot_user on 2020/8/21.
//  Copyright © 2020 IOT. All rights reserved.
//

#include <metal_stdlib>
using namespace metal;
#import "CCShaderTypes.h"

//顶点着色器的输出,和片元着色器的输入
typedef struct{
    //顶点
    float4 clipSpacePosition [[position]];
    //颜色
    float4 color;
    
} RasterizerData;


//顶点着色器函数
vertex RasterizerData vertexShader(uint vertexID [[vertex_id]] , constant CCVertex *vertices [[buffer(CCVertexInputIndexVertices)]], constant vector_uint2 *viewportSizePointer [[buffer(CCVertexInputIndexViewportSize)]]){
    RasterizerData out;
    
    out.clipSpacePosition = vertices[vertexID].position;
    out.color = vertices[vertexID].color;
    
    return out;
}

//片元着色器函数
fragment float4 fragmentShader(RasterizerData in [[stage_in]]) {
    return in.color;
}

  

创建用于管理Metal着色器加载和渲染的文件CCRender

CCRender.h

 //
//  CCRender.h
//  MetalTriangle
//
//  Created by iot_user on 2020/8/21.
//  Copyright © 2020 IOT. All rights reserved.
//

#import <Foundation/Foundation.h>
@import MetalKit;

NS_ASSUME_NONNULL_BEGIN

@interface CCRender : NSObject<MTKViewDelegate>
-(id)initWithMetalKitView:(MTKView *)mtkView;
@end

NS_ASSUME_NONNULL_END

CCRender.m

//
//  CCRender.m
//  MetalTriangle
//
//  Created by iot_user on 2020/8/21.
//  Copyright © 2020 IOT. All rights reserved.
//

#import "CCRender.h"
#import "CCShaderTypes.h"

@implementation CCRender

{
    id<MTLDevice>               _device;        //GPU设备
    id<MTLCommandQueue>         _commandQueue;  //命令队列
    id<MTLRenderPipelineState>  _pipelineState; //渲染管道
    vector_uint2                _viewportSize;  //视口大小
}

-(id)initWithMetalKitView:(MTKView *)mtkView{
    self = [super init];
    if (self) {
        //获取GPU
        _device = mtkView.device;
        //创建命令队列
        _commandQueue = [_device newCommandQueue];
        //创建着色器渲染管道
        //读取metal文件
        id<MTLLibrary> defaultLibrary = [_device newDefaultLibrary];//读取全部的metal文件
        //获取顶点着色器函数
        id<MTLFunction> vertexFunction = [defaultLibrary newFunctionWithName:@"vertexShader"];
        //获取片元着色器函数
        id<MTLFunction> fragmentFunction = [defaultLibrary newFunctionWithName:@"fragmentShader"];
        //创建渲染管理描述
        MTLRenderPipelineDescriptor *pipelineDescriptor =[[MTLRenderPipelineDescriptor alloc] init];
        pipelineDescriptor.label = @"pipelineDescriptor";
        pipelineDescriptor.vertexFunction = vertexFunction;
        pipelineDescriptor.fragmentFunction = fragmentFunction;
        pipelineDescriptor.colorAttachments[0].pixelFormat = mtkView.colorPixelFormat;
        //创建渲染管道
        NSError *error;
        _pipelineState = [_device newRenderPipelineStateWithDescriptor:pipelineDescriptor error:&error];
        if (!_pipelineState) {
            //如果我们没有正确设置管道描述符,则管道状态创建可能失败
            NSLog(@"Failed to created pipeline state, error %@", error);
            return nil;
        }
        
    }
    return self;
}

-(void)drawInMTKView:(MTKView *)view{
    
    //1.顶点数据和颜色数据
    static const CCVertex triangleVertices[] = {
        //顶点,    RGBA 颜色值
        { {  0.5, -0.25, 0.0, 1.0 }, { 1, 0, 0, 1 } },
        { { -0.5, -0.25, 0.0, 1.0 }, { 0, 1, 0, 1 } },
        { { -0.0f, 0.25, 0.0, 1.0 }, { 0, 0, 1, 1 } },
    };
    //2.清空背景颜色
    view.clearColor = MTLClearColorMake(0, 0, 0, 1.0);
    //3.创建渲染缓冲区
    id<MTLCommandBuffer> commandBuffer = [_commandQueue commandBuffer];
    commandBuffer.label = @"commandBuffer";
    
    //4.创建渲染描述符
    MTLRenderPassDescriptor *renderPassDescriptor = view.currentRenderPassDescriptor;
    if (renderPassDescriptor != nil) {
        //5.创建渲染编码器
        id<MTLRenderCommandEncoder> renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor];
        renderEncoder.label = @"renderEncoder";
        //6.设置渲染区域
        MTLViewport viewport = {0.0,0.0,_viewportSize.x,_viewportSize.y,-1.0,1.0};
        [renderEncoder setViewport:viewport];
        //7.设置渲染管道
        [renderEncoder setRenderPipelineState:_pipelineState];
        //8.将顶点数据和颜色数据传入着色器
        [renderEncoder setVertexBytes:triangleVertices length:sizeof(triangleVertices) atIndex:CCVertexInputIndexVertices];
        //9.将视口传入着色器
        [renderEncoder setVertexBytes:&_viewportSize length:sizeof(_viewportSize) atIndex:CCVertexInputIndexViewportSize];
        //10.画出所有的点
        [renderEncoder drawPrimitives:(MTLPrimitiveTypeTriangle) vertexStart:0 vertexCount:3];
        //11.画完了,关闭编码器
        [renderEncoder endEncoding];
        //12.将命令缓冲区推送到视图的可绘制区域
        [commandBuffer presentDrawable:view.currentDrawable];
    }
    //13.将渲染命令提交到GPU
    [commandBuffer commit];

}

-(void)mtkView:(MTKView *)view drawableSizeWillChange:(CGSize)size {
    _viewportSize.x = size.width;
    _viewportSize.y = size.height;
}

@end
  

通过MTKView加载渲染结果

//
//  ViewController.m
//  MetalTriangle
//
//  Created by iot_user on 2020/8/21.
//  Copyright © 2020 IOT. All rights reserved.
//

#import "ViewController.h"
#import "CCRender.h"

@interface ViewController ()

{
    MTKView *_view;
    CCRender *_render;
}

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    _view = (MTKView *)self.view;
    _view.device = MTLCreateSystemDefaultDevice();
    if (!_view.device) {
        NSLog(@"Metal is not supported on this device");
        return;
    }
    //
    _render = [[CCRender alloc] initWithMetalKitView:_view];
    if (!_render) {
        NSLog(@"Renderer failed initialization");
        return;
    }
    //初始化视口大小
    [_render mtkView:_view drawableSizeWillChange:_view.drawableSize];
    _view.delegate = _render;
    

}


@end

注意: self.view在Main.storyboard已经被强转成了MTKView类型,你也可以自己创建一个MTKView的子view来显示渲染结果.

效果如下