0021--OpenGL Metal 案例二 渲染三角形

[toc]

前言

MEtal选题框架,它可以渲染高级3D图形,并使用GPU执行数据并行计算.

图形渲染管道

The Metal Graphics Rendering Pipeline - Metal图形渲染管道

33f14295a126db7f2bee074ab452d32c

此示例主要关注管道的三个主要阶段:顶点函数,光栅化阶段和片段函数。 顶点函数和片段函数是可编程阶段。 光栅化阶段是固定的。

准备工作

创建一个头文件

头文件包含了 Metal shadersC/OBJC 源之间共享的类型和枚举常数

  • 缓存区索引值共享与 shaderC 代码
typedef enum CCVertexInputIndex
{
  //顶点
  CCVertexInputIndexVertices     = 0,
  //视图大小
  CCVertexInputIndexViewportSize = 1,
} CCVertexInputIndex;

为了确保Metal Shader缓存区索引能够匹配 Metal API Buffer 设置的集合调用

  • 结构体: 顶点/颜色值
typedef struct
{
    // 像素空间的位置
    // 像素中心点(100,100)
    vector_float4 position;

    // RGBA颜色
    vector_float4 color;
} CCVertex;

Position是必需的顶点属性,而color是可选的。 对于此示例,管道使用两个顶点属性将彩色三角形渲染到drawable的特定区域
对应数据:

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 } },
    };

创建Metal文件

command + N --> Metal File 创建metal着色器文件


文件中内容

1. Declare Vertex Function Return Values - 声明顶点函数返回值

typedef struct
{
    // The [[position]] attribute of this member indicates that this value is the clip space
    // position of the vertex when this structure is returned from the vertex function
    float4 clipSpacePosition [[position]];

    // Since this member does not have a special attribute, the rasterizer interpolates
    // its value with the values of the other triangle vertices and then passes
    // the interpolated value to the fragment shader for each fragment in the triangle
    float4 color;

} RasterizerData;

顶点函数必须通过[[position]]属性限定符为clipSpacePosition成员使用返回每个顶点的剪辑空间位置值。 声明此属性后,管道的下一个阶段(栅格化rasterization)使用clipSpacePosition值来标识三角形角的位置,并确定要渲染的像素。

Write a Vertex Function - 写一个顶点函数

顶点函数(也称为顶点着色器vertex shader)的主要任务是处理传入的顶点数据并将每个顶点映射到视口中的位置。 这样,管道中的后续阶段可以引用此视口位置并将像素渲染到drawable中的精确位置。 顶点函数通过将任意顶点坐标转换为标准化设备坐标(也称为剪辑空间坐标clip-space coordinates)来完成此任务。

剪辑空间Clip space是一个2D坐标系,它将视口区域沿x轴和y轴映射到[-1.0,1.0]范围。 视口的左下角映射到(-1.0,-1.0,右上角映射到(1.0,1.0),中心映射到(0.0,0.0)

6dc1f280434a5051b90370a82ee8f112

顶点函数对于绘制的每个顶点执行一次。 在此示例中,对于每个帧,绘制三个顶点以构成三角形。 因此,顶点函数每帧执行三次。

一帧就是一幅静止的画面

Metal着色语言代码专门在GPU上执行。 GPU提供了更大的处理带宽,并且可以在大量顶点和片段上并行工作。 但是,它具有比CPU少的内存,不能有效地处理控制流操作,并且通常具有更高的延迟。

vertexShader

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;
 
 }

第一个参数vertexID使用[[vertex_id]]属性限定符并保存当前正在执行的顶点的索引。当绘制调用使用此顶点函数时,此值从0开始,并在每次调用vertexShader函数时递增。使用[[vertex_id]]属性限定符的参数通常用于索引包含顶点的数组。

第二个参数vertices是包含顶点的数组,每个顶点定义为CCVertex数据类型。指向此结构的指针定义了这些顶点的数组。

第三个也是最后一个参数viewportSizePointer包含视口的大小,并具有vector_uint2数据类型。

vertices和viewportSizePointer参数都使用SIMD数据类型,这些类型是C和Metal着色语言代码都能理解的类型。因此,示例可以在共享CCShaderTypes.h标头中定义CCVertex结构,该结构包含在CCRenderer.mCCShaders.metal代码中。因此,共享头确保三角形顶点的数据类型在Objective-C声明(triangleVertices)中与在Metal着色语言声明(vertices)中相同。在Metal应用程序中使用SIMD数据类型可确保内存布局在CPU / GPU声明中完全匹配,并有助于将顶点数据从CPU发送到GPU`.

注意:对CCVertex结构的任何更改都会同等地影响CCRenderer.m和`CCShaders.metal代码。

verticesviewportSizePointer参数都使用[[buffer(index)]]属性限定符。

CCVertexInputIndexVerticesCCVertexInputIndexViewportSize的值是用于在CCRenderer.mCCShaders.metal代码中标识和设置顶点函数输入的索引。

Process Vertex Data - 处理顶点数据

  1. 处理顶点数据:
    • 执行坐标系转换,将生成的顶点剪辑空间写入到返回值中.
    • 将顶点颜色值传递给返回值

  1. 获取输入顶点,vertexID参数用于索引顶点数组。
out.clipSpacePosition = vertices[vertexID].position;

每个顶点着色器的输出位置在剪辑空间,(也称为归一化设备坐标空间,NDC),剪辑空间中的(-1,-1)表示视口的左下角,而(1,1)表示视口的右上角.

计算和写入XY值到我们的剪辑空间的位置为了从像素空间中的位置转换到剪辑空间的位置,我们将像素坐标除以视口的大小的一半.
float2 pixelSpacePosition = vertices[vertexID].position.xy;
out.clipSpacePosition.xy = pixelSpacePosition / (viewportSize / 2.0); 本次案例没有这样做,也可行

  1. 最后,顶点函数访问每个vertices元素的color成员并将其传递给out.color返回值,而不执行任何修改。
 out.color = vertices[vertexID].color;

Rasterization - 光栅化

顶点函数执行三次后,对于每个三角形的顶点执行一次,管道中的下一个阶段,即栅格化开始。

栅化是管道光栅化器单元产生碎片的阶段。 片段包含原始预像素数据,用于生成渲染到drawable的像素。 对于由顶点函数生成的每个完整三角形,光栅化器确定目标可绘制的哪些像素被三角形覆盖。 它通过测试drawable中每个像素的中心是否在三角形内部来实现。 在下图中,仅生成像素中心位于三角形内部的片段。 这些片段显示为灰色方块。

453d0654a2394e64c88c6b47a2186d26

栅格化还确定发送到管道中下一个阶段的值:片段函数。在管道的早期,顶点函数输出RasterizerData结构的值,该结构包含剪辑空间位置(clipSpacePosition)和颜色(color)。 clipSpacePosition成员使用所需的[[position]]属性限定符,指示这些值直接用于确定三角形的片段覆盖区域。color成员没有属性限定符,表示应该在三角形的片段中插入这些值。

在将每个顶点值转换为每个片段值之后,光栅化器将color值传递给片段函数。此转换使用固定插值函数,该函数计算从三角形的三个顶点的color值派生的单个加权颜色。插值函数的权重(也称为重心坐标barycentric coordinates)是每个顶点位置与片段中心的相对距离。例如:

  1. 如果片段正好位于三角形的中间,与每个三角形的三个顶点等距,则每个顶点的颜色加权1/3。 在下图中,这显示为三角形中心的灰色片段(0.33,0.33,0.33)

  2. 如果一个片段非常靠近一个顶点并且距离另外两个非常远,则将近顶点的颜色加权为1,将远点的颜色加权为0。在下图中,这显示为偏红色 片段(0.5,0.25,0.25)靠近三角形的右下角。

  3. 如果片段位于三角形的边缘,在三个顶点中的两个顶点的中间,则每个边缘定义顶点的颜色加权1/2,非边缘顶点的颜色加权0。在下图中, 这显示为三角形左边缘的青色片段(0.0,0.5,0.5)

    fecc9c6df3592388086069f42e0dd500

由于光栅化是固定的管道阶段,因此无法通过自定义Metal着色语言代码修改其行为。 在光栅化器创建片段及其关联值之后,结果将传递到管道中的下一个阶段。

Write a Fragment Function - 写一个片段函数

片段函数(也称为片段着色器fragment shader)的主要任务是处理传入的片段数据并计算可绘制像素的颜色值。

fragment float4 fragmentShader(RasterizerData in [[stage_in]])
{
    
    //返回输入的片元颜色
    return in.color;
}

[[stage_in]],片元着色函数使用的单个片元输入数据是由顶点着色函数输出.然后经过光栅化生成的.单个片元输入函数数据可以使用"[[stage_in]]"属性修饰符.

一个顶点着色函数可以读取单个顶点的输入数据,这些输入数据存储于参数传递的缓存中,使用顶点和实例ID在这些缓存中寻址.读取到单个顶点的数据.另外,单个顶点输入数据也可以通过使用"[[stage_in]]"属性修饰符的产生传递给顶点着色函数.

stage_in 修饰的结构体的成员不能是如下这些.Packed vectors 紧密填充类型向量,matrices 矩阵,structs 结构体,references or pointers to type 某类型的引用或指针. arrays,vectors,matrices 标量,向量,矩阵数组.

循环渲染类

创建GPU,渲染管线,命令队列和视口实例


    //我们用来渲染的设备(又名GPU)
    id _device;

    // 我们的渲染管道有顶点着色器和片元着色器 它们存储在.metal shader 文件中
    id _pipelineState;

    //命令队列,从命令缓存区获取
    id _commandQueue;

    //当前视图大小,这样我们才可以在渲染通道使用这个视图
    vector_uint2 _viewportSize;

在项目中加载所有的(.metal)着色器文件

  • Obtain Function Libraries and Create a Pipeline - 获取函数库并创建管道

在初始化initWithMetalKitView加载

Metal着色语言代码分两个阶段编译:

  1. 前端编译在构建时在Xcode中发生.metal文件从高级源代码编译为中间表示(IR)文件。
  2. 后端编译在运行时在物理设备中进行。然后将IR文件编译为低级机器代码。
id defaultLibrary = [_device newDefaultLibrary];
        //从库中加载顶点函数
        id vertexFunction = [defaultLibrary newFunctionWithName:@"vertexShader"];
        //从库中加载片元函数
        id fragmentFunction = [defaultLibrary newFunctionWithName:@"fragmentShader"];

这些MTLFunction对象用于创建表示图形渲染管道的MTLRenderPipelineState对象。调用MTLDevice对象的newRenderPipelineStateWithDescriptor:error:方法开始后端编译过程,该过程链接vertexShaderfragmentShader函数,从而产生完全编译的管道。

MTLRenderPipelineState对象包含由MTLRenderPipelineDescriptor对象配置的其他管道设置。除顶点和片段函数外,此示例还配置colorAttachments数组中第一个条目的pixelFormat值。此示例仅渲染到单个目标,即视图的drawable(colorAttachments [0]),其像素格式由视图本身(colorPixelFormat)配置。视图的像素格式定义了每个像素的内存布局;在创建管道时,Metal必须能够引用此布局,以便它可以正确呈现fragment函数生成的颜色值。

 //3.配置用于创建管道状态的管道
        MTLRenderPipelineDescriptor *pipelineStateDescriptor = [[MTLRenderPipelineDescriptor alloc] init];
        //管道名称
        pipelineStateDescriptor.label = @"Simple Pipeline";
        //可编程函数,用于处理渲染过程中的各个顶点
        pipelineStateDescriptor.vertexFunction = vertexFunction;
        //可编程函数,用于处理渲染过程中各个片段/片元
        pipelineStateDescriptor.fragmentFunction = fragmentFunction;
        //一组存储颜色数据的组件
        pipelineStateDescriptor.colorAttachments[0].pixelFormat = mtkView.colorPixelFormat;
        
        //4.同步创建并返回渲染管线状态对象
        _pipelineState = [_device newRenderPipelineStateWithDescriptor:pipelineStateDescriptor error:&error];
        //判断是否返回了管线状态对象
        if (!_pipelineState)
        {
           
            //如果我们没有正确设置管道描述符,则管道状态创建可能失败
            NSLog(@"Failed to created pipeline state, error %@", error);
            return nil;
        }

Send Vertex Data to a Vertex Function - 将顶点数据发送到顶点函数

设置我们绘制的可绘制区域

视口指定Metal渲染内容的drawable区域。 视口是具有x和y偏移,宽度和高度以及近和远平面的3D区域
为管道分配自定义视口需要通过调用setViewport:方法将MTLViewport结构编码为渲染命令编码器。 如果未指定视口,Metal会设置一个默认视口,其大小与用于创建渲染命令编码器的drawable相同。

 MTLViewport viewPort = {
            0.0,0.0,_viewportSize.x,_viewportSize.y,-1.0,1.0
        };
        [renderEncoder setViewport:viewPort];

创建管道后,可以将其分配给渲染命令编码器。 此操作将由该特定管道处理所有后续渲染命令。

 [renderEncoder setRenderPipelineState:_pipelineState];

从应用程序OC 代码 中发送数据给Metal 顶点着色器 函数

  1. 顶点数据+颜色数据
  • bytes:指向要传递给着色器的内存的指针
  • length:我们想要传递的数据的内存大小
  • index:一个整数索引,它对应于我们的“vertexShader”函数中的缓冲区属性限定符的索引。
[renderEncoder setVertexBytes:triangleVertices
                               length:sizeof(triangleVertices)
                              atIndex:CCVertexInputIndexVertices];
  • viewPortSize 数据
  • 指向要传递给着色器的内存的指针
  • 我们想要传递的数据的内存大小
  • 一个整数索引,它对应于我们的“vertexShader”函数中的缓冲区属性限定符的索引。
[renderEncoder setVertexBytes:&_viewportSize
                               length:sizeof(_viewportSize)
                              atIndex:CCVertexInputIndexViewportSize];
                              

Draw the Triangle - 绘制三角形

在不使用索引列表的情况下,绘制图元
primitiveType:绘制图形组装的基元类型
vertexStart:从哪个位置数据开始绘制,一般为0
vertexCount:每个图元的顶点个数,绘制的图型顶点数量

[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle
                          vertexStart:0
                          vertexCount:3];

MTLPrimitiveTypePoint = 0, 点
MTLPrimitiveTypeLine = 1, 线段
MTLPrimitiveTypeLineStrip = 2, 线环
MTLPrimitiveTypeTriangle = 3, 三角形
MTLPrimitiveTypeTriangleStrip = 4, 三角型扇

效果图

56098a4674e7b7c074b28988699a9593

参考:
Metal框架详细解析

你可能感兴趣的:(0021--OpenGL Metal 案例二 渲染三角形)