您好,欢迎访问代码之道!登录后台查看权限
  • 欢迎大神光临
  • 有朋自远方来 不亦悦乎

Metal学习笔记:绘制纹理源码分析

Objective-C 老刘 2018-09-12 1951 次浏览 0个评论

本文demo git地址纹理源码

Metal配置

配置Metal代码如下

// 获取设备
    _device = MTLCreateSystemDefaultDevice();

    if(_device == nil) {
        NSLog(@"创建Metal设备失败");
        return;
    }

    // 创建配置MTKView
    _mtkView = [[MTKView alloc]initWithFrame:self.view.frame device:_device];
    [self.view addSubview:_mtkView];
    _mtkView.delegate = self;
    [self mtkView:_mtkView drawableSizeWillChange:_mtkView.drawableSize];

    // _mtkView.colorPixelFormat = MTLPixelFormatRGBA8Unorm_sRGB;
    _mtkView.paused = YES; // 不自动执行渲染函数

    // 加载库 函数
    id<MTLLibrary> library = [_device newDefaultLibrary];
    id<MTLFunction> vertextFunction = [library newFunctionWithName:@"vertextShader"];
    id<MTLFunction> fragmentFunction = [library newFunctionWithName:@"fragmentShader"];

    // 配置 创建渲染管线
    MTLRenderPipelineDescriptor *descriptor = [[MTLRenderPipelineDescriptor alloc]init];
    descriptor.vertexFunction = vertextFunction;
    descriptor.fragmentFunction = fragmentFunction;
    descriptor.colorAttachments[0].pixelFormat = _mtkView.colorPixelFormat;
    descriptor.sampleCount = _mtkView.sampleCount;

    NSError *error;
    _state = [_device newRenderPipelineStateWithDescriptor:descriptor error:&error];
    if(error != nil) {
        NSLog(@"创建渲染管线状态失败:%@", error.localizedDescription);
    }

    // 创建命令队列
    _queue = [_device newCommandQueue];

Metal配置基本是固定的步骤

  • 获取GPU设置:使用MTLCreateSystemDefaultDevice获取GPU
  • 配置MTKView:该例子中设置为不主动渲染,也就是_mtkView.paused = YES,当需要渲染的时候调用_mtkView draw
  • 加载Metal库和函数:[_device newDefaultLibrary],加载当前项目的.metal编译出来的默认库,通过 [library newFunctionWithName:@"函数名"]获取Metal的顶点和片元函数
  • 创建渲染管线:配置一个MTLRenderPipelineDescriptor渲染管线描述,然后通过他创建管线状态对象
  • 创建命令队列:通过[_device newCommandQueue]创建命令队列

纹理加载

加载纹理代码如下

    MTKTextureLoader *loader = [[MTKTextureLoader alloc]initWithDevice:_device];

    UIImage *img = [UIImage imageNamed:@"test.jpg"];
    if (@available(iOS 10.0, *)) {
        [loader newTextureWithCGImage:
            img.CGImage
            options:@{MTKTextureLoaderOptionOrigin : MTKTextureLoaderOriginFlippedVertically}
            completionHandler:
             ^(id<MTLTexture>  _Nullable texture, NSError * _Nullable error) {
                if(error) {
                    NSLog(@"加载纹理失败:%@", error.localizedDescription);
                    return;
                }
                self->_inputTexture = texture;
                // self->_mtkView.paused = NO;
                [self->_mtkView draw];
            }];
    } else {
        // Fallback on earlier versions
    }

例子使用了MetalKit提供的MTKTextureLoader加载纹理,因为在UI(UIImage/CGImage)系统中,图片顶点是左上角,而在Metal中纹理原点是在左下角的, 默认就会看到上下颠倒的效果,所以要使用MTKTextureLoaderOptionOrigin : MTKTextureLoaderOriginFlippedVertically, 翻转图片。

纹理加载完毕后,调用_mtkView draw请求渲染。

渲染

完整的渲染代码如下

- (void)drawInMTKView:(nonnull MTKView *)view {

    id<MTLCommandBuffer> buffer = [_queue commandBuffer];

    id<MTLRenderCommandEncoder> encoder = [buffer renderCommandEncoderWithDescriptor:_mtkView.currentRenderPassDescriptor];

    [encoder setRenderPipelineState:_state];

    // 更新视口大小
    [encoder setViewport:(MTLViewport){0.0, 0.0, _viewportSize.x, _viewportSize.y, -1.0, 1.0 }];
    // -1,1   1, 1
    // -1,-1  1,-1

    static vector_float2 positionCoord[] = {{-1,-1},  {1,-1}, {-1,1}, {1,1}};
    static vector_float2 textureCoord[] = {{0,0},  {1,0}, {0,1}, {1,1}};
    [encoder setVertexBytes:positionCoord length:sizeof(positionCoord) atIndex:0];
    [encoder setVertexBytes:textureCoord length:sizeof(textureCoord) atIndex:1];

    [encoder setFragmentTexture:_inputTexture atIndex: 0];

    [encoder drawPrimitives:MTLPrimitiveTypeTriangleStrip vertexStart:0 vertexCount:4];
    [encoder endEncoding];

    [buffer presentDrawable:_mtkView.currentDrawable];
    [buffer commit];
}

渲染步骤

  • 通过命令队列创建命令缓冲
  • 通过命令缓冲获取命令编码器
  • 编码器绑定渲染管线状态,因为命令队列、命令缓冲都和管线状态无关
  • 编码纹理相关数据
  • 添加绘制图元命令
  • 结束编码
  • 最后,命令缓冲需要执行两部操作,指定输出绘制件,提交命令缓冲(到创建它的队列)

和纹理有关的其实只有下面几行

    static vector_float2 positionCoord[] = {{-1,-1},  {1,-1}, {-1,1}, {1,1}};
    static vector_float2 textureCoord[] = {{0,0},  {1,0}, {0,1}, {1,1}};
    [encoder setVertexBytes:positionCoord length:sizeof(positionCoord) atIndex:0];
    [encoder setVertexBytes:textureCoord length:sizeof(textureCoord) atIndex:1];

    [encoder setFragmentTexture:_inputTexture atIndex: 0];
  • 编码位置坐标
  • 编码纹理坐标
  • 编码纹理数据

Metal Shading

整个Metal着色器代码如下

#include <metal_stdlib>
#include <simd/simd.h>
using namespace metal;

typedef struct {
    float4 position [[position]];
    float2 textureCoord;
}RasterInputOutput;

vertex RasterInputOutput vertextShader (
    uint vid [[vertex_id]],
    constant vector_float2* positionCoord [[buffer(0)]],
    constant vector_float2* textureCoord [[buffer(1)]]
) {
    RasterInputOuput io;
    io.position = float4(positionCoord[vid].xy, 0, 1.0);
    io.textureCoord = textureCoord[vid];
    return io;
}

fragment half4 fragmentShader(
    RasterInputOutput io [[stage_in]],
    texture2d<half> inputTexture [[texture(0)]]
) {
    constexpr sampler textureSampler;
    half4 color = inputTexture.sample(textureSampler, io.textureCoord);
    return color;
}

声明一个结构体

typedef struct {
    float4 position [[position]];
    float2 textureCoord;
}RasterInputOuput;

该结构体声明了光栅化的输入输出数据布局,[[position]]修饰的变量等价于OpenGL的gl_Position,为裁剪空间坐标。 textureCoord没有任何修饰,类似于OpenGL的varying变量,光栅化阶段会插值。

顶点函数

vertex RasterInputOutput vertextShader (
    uint vid [[vertex_id]],
    constant vector_float2* positionCoord [[buffer(0)]],
    constant vector_float2* textureCoord [[buffer(1)]]
) {
    RasterInputOuput io;
    io.position = float4(positionCoord[vid].xy, 0, 1.0);
    io.textureCoord = textureCoord[vid];
    return io;
}

使用vertex关键字修饰,返回值为自定义的RasterInputOutput

参数vid[[vertex_id]]修饰,表示该变量将被赋值为 当前函数正在处理的顶点id(顶点下标)是什么,每个顶点都会执行一次顶点函数处理,他们是并行执行的,只有通过顶点id来区分当前处理的 是哪个顶点了。

positionCoord前用关键字constant修饰,说明positionCoord存放在不可变的地址空间,我们不可以修改positionCoord, 后用[[buffer(0)]]修饰,对应[encoder setVertexBytes:positionCoord length:sizeof(positionCoord) atIndex:0]atIndex:0

textureCoordpositionCoord类似。

片元函数

fragment half4 fragmentShader(
    RasterInputOutput io [[stage_in]],
    texture2d<half> inputTexture [[texture(0)]]
) {
    constexpr sampler textureSampler;
    half4 color = inputTexture.sample(textureSampler, io.textureCoord);
    return color;
}

前用fragment修饰,返回值为half4(颜色),参数io用[[stage_in]] ,告诉GPU请将上个阶段的处理结果赋值给我。

inputTexture类型为texture2d,用泛型明确像素精度,后用[[texture(0)], 对应[encoder setFragmentTexture:_inputTexture atIndex: 0]

采样步骤

  • 声明采样器,其实就是包含一些采样参数,比如 constexpr sampler textureSampler (mag_filter::linear, min_filter::linear);。 也可以默认,不加任何参数。
  • 使用.sample函数取指定位置的像素数据。可以点出方法,说明texture2d是个类呀!

已有 1951 位网友参与,快来吐槽:

发表评论