IOS开发入门之Metal API随着iOS 10与macOS 10.12新引入的Tessellation特性
白羽 2018-11-23 来源 :网络 阅读 1392 评论 0

摘要:本文将带你了解IOS开发入门Metal API随着iOS 10与macOS 10.12新引入的Tessellation特性,希望本文对大家学IOS有所帮助。

    本文将带你了解IOS开发入门Metal API随着iOS 10与macOS 10.12新引入的Tessellation特性,希望本文对大家学IOS有所帮助。



        

Tessellation——中文一般译作“细分曲面”,一般用于将由少量顶点构成的面生成细节度更高的面。这其中的原理是将一个三角形或四边形,由GPU根据我们编程的控制点生成规则,自动生成更多的顶点,然后将这些顶点根据一定规则生成更多的三角形。这么一来,我们可以在3D游戏中在远处的敌人使用低模也能做出精细度较高的模型出来了,而且也省顶点数据传输带宽。

在Metal API中,通过tessellation绘制出的图形所走的渲染流水线会与通过传统的顶点着色器所走的渲染流水线会有所不同。Metal API为了简化本身Metal Shading Language的语法,将已有的Compute Shader用来计算控制点生成规则,然后将结果送给Tesselator(曲面细分器)生成具体的控制点,最后交给用于细分曲面后处理的Vertex Shader做顶点数据输出,然后后面的就跟典型的渲染流水线一样处理了。下面贴出Apple官方编程指南中的流水线图:点击打开链接

上图中,蓝色圆角矩形部分是可编程着色器;绿色圆角矩形部分是GPU的固定硬件单元;灰色部分是数据缓存对象。我们可以看到,走Tessellation流水线相对于传统的渲染流水线,其实也就多了计算着色器(用于生成每个patch的细分曲面因子)以及曲面细分器单元。所谓patch其实与一般的三角形(triangle)与四边形(quad)差不多,只是在用于细分曲面的处理过程中,一个patch是在一个平面里的,所以它没有z坐标,我们在细分曲面后处理的顶点着色器中可以给转换后的顶点再做视图模型变换以及投影变换,然后输出。在Metal API中,细分曲面只支持三角形与四边形,不支持线段;在OpenGL中还支持线段。

对于四边形而言,patch的顶点排列与普通的矩形会有些差别。我们通常用Metal API绘制一个矩形往往会用triangle strip的绘制模式(由于Metal API中没有triangle fan的绘制模式),因此一个矩形的顶点排列可以是:左上,左下,右上,右下这种排列方式。然而绘制patch则不能用这种方式,我们只能对四个顶点依次以顺时针或逆时针的顺序进行排列。四边形patch的控制点位于四边形的边缘以及四边形的内部,因此用于描述四边形patch控制点的结构体定义如下:


   

typedef struct {

    /* NOTE: edgeTessellationFactor and insideTessellationFactor are interpreted as half (16-bit floats) */

    uint16_t edgeTessellationFactor[4];

    uint16_t insideTessellationFactor[2];

} MTLQuadTessellationFactorsHalf;

   

该结构体中的edgeTessellationFactor成员用于描述四边形四个边缘的控制点生成情况;insideTessellationFactor用于描述内部控制点的生成情况。patch顶点的排列不同,对于edgeTessellationFactor数组对象的各个元素所对应的哪条边也是有所不同的。本文中,我们编排四边形patch的顶点顺序依次为四边形的左下顶点、右下顶点、右上顶点、左上顶点,以逆时针的次序进行编排。这样,edgeTessellationFactor[0]对应的是左边,edgeTessellationFactor[1]对应的是下边,edgeTessellationFactor[2]对应的是右边,edgeTessellationFactor[3]对应的是上边。

edgeTessellationFactor[i]的值表示在该边上生成(edgeTessellationFactor[i] - 1)个控制点,使得这些控制点正好将当前边平均分为edgeTessellationFactor[i]条线段。

insideTessellationFactor[0]表示在四边形内部水平方向上生成(insideTessellationFactor[0] - 1)列个控制点,使得这些控制点列能将整个四边形在垂直方向上平均分为insideTessellationFactor[0]列条带。insideTessellationFactor[1]表示在四边形垂直方向上生成(insideTessellationFactor[1] - 1)行控制点,使得这些控制点行能将整个四边形在水平方向上平均划分为insideTessellationFactor[1]行条带。
在上图中,四边形patch的控制点的坐标是用(u, v)来表示的。u和v的取值都是规格化的,在[0.0, 1.0]的区间内。控制点在左下顶点处的(u, v)坐标为(0.0, 0.0),在右上顶点处的坐标为(1.0, 1.0)。

三角形patch的控制点生成规则稍微复杂一些。与四边形patch类似的是,三角形patch的控制点生成也分为边缘上的控制点与三角形内部控制点。如下结构体定义所示:


   

typedef struct {

    /* NOTE: edgeTessellationFactor and insideTessellationFactor are interpreted as half (16-bit floats) */

    uint16_t edgeTessellationFactor[3];

    uint16_t insideTessellationFactor;

} MTLTriangleTessellationFactorsHalf;

   


为了统一描述,本文将三角形patch的顶点编排次序排列为:右下、中上、左下。这样使得这里的edgeTessellationFactor[0]对应于三角形的左边,edgeTessellationFactor[1]对应于三角形的下边,edgeTessellationFactor[2]对英语三角形的右边。

三角形边缘上控制点的生成与四边形的类似,而内部控制点生成规则就要复杂不少了。对于insideTessellationFactor值,如果它小于3,那么在三角形内就只有一个控制点,坐落于三角形的重心(所谓三角形重心,即三角形的三个顶点到对边上的中线所交汇的点)处。如果是3,那么在三角形内部则正好有3个控制点构成一个小三角形。如果是4,那么在小三角形中再增加一个控制点,坐落于小三角形的重心处,并且小三角形的每个边缘的中点处增加一个控制点。如果是5,内部小三角形的每条边缘则含有两个控制点,均分边缘;并且在小三角形内部再次构成一个小三角形。如果是6,小三角形的边缘含有3个控制点均分边缘;小三角形内部的小三角形的每个边缘上增加1个控制点,并且在其内部再增加一个控制点,以此类推……。

三角形patch的每个控制点的坐标以(u, v, w)进行描述,u、v和w的取值均为[0.0, 1.0]区间内。以本文的三角形patch顶点编排顺序为例,三角形左边的所有控制点的u坐标为0;三角形下边上的所有控制点的v坐标为0,三角形右边上所有控制点的w坐标为0。那么在三角形内部的某一个控制点的坐标如何计算得到呢?这里比较晕乎的是在一个二维平面中用三个坐标值来表示一个点,不过这里有意思的一点是:对于三角形内的任一控制点的(u, v, w)坐标,u + v + w = 1.0。所以它们之间是具有关联性的。我们可以从一个简单的例子进行着手分析——对于一个等边三角形,我们假设其中线(对于等边三角形,三线合一,即中线、垂直线、角平分线,另外中心与重心也是同一个)长度为1.0,那么其重心到三条边中点的距离正好都是1/3,那么我们知道,如果将位于重心的控制点的坐标定义为(1/3, 1/3, 1/3),那么u + v + w的值正好为1.0。所以我们判定三角形patch内部某个控制点的坐标可以这么做:对于判定u值,将三角形左边沿着其中线进行平移,直到与控制点重合,然后看平移后的左边与左边中线的相交点,该点在此中线上的位置即为u值;判断v值也类似,将三角形下边沿着其中线进行平移,然后正好与该控制点重合处,看该下边与其中线相交点在此中线上的位置;w点则是看右边沿着其中线平移的情况。

有了以上对于细分曲面控制点生成的相关概念之后,下面我们就开始描述Metal API中完成细分曲面渲染的整个过程了。

我们首先看如何用计算着色器设置控制点的生成规则。计算着色器的实现如下所示:


   

constant int4 tessOutFactors [[ function_constant(0) ]];

constant int2 tessInnerFactors [[ function_constant(1) ]];

 

 

// 三角形用于生成控制点的计算内核

kernel void triangle_kernel(device struct

                            MTLTriangleTessellationFactorsHalf *factors [[ buffer(0) ]],

                            uint tid [[ thread_position_in_grid ]])

{

    // Simple passthrough operation

    // More sophisticated compute kernels might determine the tessellation factors based on the state of the scene (e.g. camera distance)

    factors[tid].edgeTessellationFactor[0] = tessOutFactors.x;

    factors[tid].edgeTessellationFactor[1] = tessOutFactors.y;

    factors[tid].edgeTessellationFactor[2] = tessOutFactors.z;

    factors[tid].insideTessellationFactor = tessInnerFactors.x;

}

 

// 四边形用于生成控制点的计算内核

kernel void quad_kernel(device struct

                        MTLQuadTessellationFactorsHalf *factors [[ buffer(0) ]],

                        uint tid [[ thread_position_in_grid ]])

{

    // Simple passthrough operation

    // More sophisticated compute kernels might determine the tessellation factors based on the state of the scene (e.g. camera distance)

    factors[tid].edgeTessellationFactor[0] = tessOutFactors.x;

    factors[tid].edgeTessellationFactor[1] = tessOutFactors.y;

    factors[tid].edgeTessellationFactor[2] = tessOutFactors.z;

    factors[tid].edgeTessellationFactor[3] = tessOutFactors.w;

    factors[tid].insideTessellationFactor[0] = tessInnerFactors.x;

    factors[tid].insideTessellationFactor[1] = tessInnerFactors.y;

}

   


上述Metal Shading Language片段中,最上面的两个常量值将由主机端传入,tessOutFactors是一个四元素向量,每个分量值对应edgeTessellationFactor的相应元素值,这一点我们在两个内核函数中也能看得很清楚。而tessInnerFactors则是一个二元素向量,对于四边形,其第一个分量作为insideTessellationFactor的第0个元素值,第二个分量作为insideTessellationFactor的第1个元素值;对于三角形,tessInnerFactors的第一个分量作为insideTessellationFactor的值。内核函数的参数就是我们上文提到过的指向MTLTriangleTessellationFactorsHalf结构体或MTLQuadTessellationFactorsHalf结构体数组的指针。每一个此结构体对象表征了对某一个patch进行控制点生成的规则,如果我们有多个patch需要做曲面细分,那么这里将会有多个元素,如果就对一个patch进行曲面细分,那么这里其实也就一个元素,我们可以在主机端进行控制给计算内核调度多少个线程进行执行。

以下代码片段是主机端对计算着色器的相关设置:


   

    // 0号对应外部边缘的tess。对于四边形,顺序为左、下、右、上;对于三角形,顺序为:左、下、右

    [constantValues setConstantValue:(const int[]){5, 4, 3, 2} type:MTLDataTypeInt4 atIndex:0];

     

    // 1号对应内部的tess。对于四边形,分别为水平方向tess、垂直方向tess;三角形只能用一个值

    [constantValues setConstantValue:(const int[]){4, 3} type:MTLDataTypeInt2 atIndex:1];

     

    computeProgram = [mLibrary newFunctionWithName:tessControlKernelName constantValues:constantValues error:NULL];

    if(computeProgram == nil)

    {

        NSLog(@"计算内核获取失败");

        break;

    }

    [constantValues release];

 

    mComputePipelineState = [device newComputePipelineStateWithFunction:computeProgram error:NULL];

     

    if(mComputePipelineState == nil)

    {

        NSLog(@"计算流水线创建失败");

        break;

    }

 

// 由于细分曲面因子缓存最终由生成控制点的内核程序生成,因此不会被CPU读写,而是直接在流水线内部使用,因此这里使用GPU私有存储模式

mTessFactorsBuffer = [device newBufferWithLength:256 options:MTLResourceStorageModePrivate];

   



上述代码片段设置了上面所提到的Metal Shading Language中的常量向量对象,另外创建了计算着色器程序,并建立了计算流水线。mTessFactorsBuffer这个缓存对象中就将存放着色器中的MTLTriangleTessellationFactorsHalf结构体或MTLQuadTessellationFactorsHalf结构体的数据。由于我们这里仅仅对一个patch进行绘制,所以就假定其长度为256个字节,这对于存放这俩结构体的内容是绰绰有余了。

下面我们看看如何调度执行计算着色器。


   

id <mtlcommandbuffer> commandBuffer = [mCommandQueue commandBuffer];

 

// 设置命令缓存执行完成后的处理

[commandBuffer addCompletedHandler:^void(id<mtlcommandbuffer> cmdBuf){

    // 命令全都执行完之后,将mCurrentDrawable置空,表示可以绘制下面一帧

    mCurrentDrawable = nil;

}];

 

// 从命令缓存对象获取计算命令编码器

id <mtlcomputecommandencoder> computeCommandEncoder = [commandBuffer computeCommandEncoder];

 

// 设置计算流水线

[computeCommandEncoder setComputePipelineState:mComputePipelineState];

 

// 设置内核程序的缓存参数

[computeCommandEncoder setBuffer:mTessFactorsBuffer offset:0 atIndex:0];

 

// 分派线程组

// 由于我们这里仅对一个patch做控制点生成规则,因此只需要一个线程进行处理即可

[computeCommandEncoder dispatchThreadgroups:MTLSizeMake(1, 1, 1) threadsPerThreadgroup:MTLSizeMake(1, 1, 1)];

 

[computeCommandEncoder endEncoding];</mtlcomputecommandencoder></mtlcommandbuffer></mtlcommandbuffer>

   


上述代码片段首先获得了命令缓存对象,然后通过该命令缓存对象获取计算命令编码器,对它设置计算流水线以及相关缓存对象。然后我们用计算命令编码器对计算内核进行分派,这样就激活了计算着色器的计算执行,然后结束命令编码器。在Metal中,一个命令缓存对象可以有多个命令编码器对其进行操作,一般情况下,同一时段只能由一个命令编码器处于活动状态,然后一直到它结束编码为止,另一个命令编码器才能操作。等到所有命令编码器完成操作之后,命令缓存可以做提交操作,让GPU做真正的命令执行。

这里对于计算着色器而言,MTLTriangleTessellationFactorsHalf结构体或MTLQuadTessellationFactorsHalf结构体数组其实是作为输出,并且该结构体类型是固定的,我们不要用其他类型来代替它,不过我们可以再添加其他缓存对象来输入一些必要的参数。

控制点生成规则数据创建完之后,它是怎么被传到细分曲面器tessellator中的呢?首先,计算着色器计算好的MTLTriangleTessellationFactorsHalf结构体或MTLQuadTessellationFactorsHalf结构体的数据都是直接存入主机端所创建的mTessFactorsBuffer缓存对象中的。然后,我们将mTessFactorsBuffer缓存对象传递给渲染命令编码器,最后通过渲染编码器调用drawPatches接口来激活tessellator,并执行后续的渲染流水线。

我们在看细分曲面后处理顶点着色器之前,先看一下主机端的一些设置。首先,我们看一下我们所指定的四边形与三角形patch的顶点排列:


   

// 以下以逆时针方向构成一个四边形,使得四边形的边顺序依次为左、下、右、上。

static const float sRectangleVertices[] = {

    // 左下顶点

    -0.9f, -0.9f,    0.9f, 0.1f, 0.1f, 1.0f,

     

    // 右下顶点

    0.9f, -0.9f,     0.1f, 0.9f, 0.1f, 1.0f,

     

    // 右上顶点

    0.9f, 0.9f,    0.1f, 0.1f, 0.9f, 1.0f,

     

    // 左上顶点

    -0.9f, 0.9f,   0.9f, 0.9f, 0.1f, 1.0f,

};

 

// 以下以逆时针方向构成一个三角形,使得三角形的边顺序依次为左、下、右。

static const float sTriangleVertices[] = {

     

    // 右下顶点

    0.9f, -0.9f,    0.1f, 0.1f, 0.9f, 1

本文由职坐标整理并发布,希望对同学们有所帮助。了解更多详情请关注职坐标移动开发之IOS频道!

   


本文由 @白羽 发布于职坐标。未经许可,禁止转载。
喜欢 | 0 不喜欢 | 0
看完这篇文章有何感觉?已经有0人表态,0%的人喜欢 快给朋友分享吧~
评论(0)
后参与评论

您输入的评论内容中包含违禁敏感词

我知道了

助您圆梦职场 匹配合适岗位
验证码手机号,获得海同独家IT培训资料
选择就业方向:
人工智能物联网
大数据开发/分析
人工智能Python
Java全栈开发
WEB前端+H5

请输入正确的手机号码

请输入正确的验证码

获取验证码

您今天的短信下发次数太多了,明天再试试吧!

提交

我们会在第一时间安排职业规划师联系您!

您也可以联系我们的职业规划师咨询:

小职老师的微信号:z_zhizuobiao
小职老师的微信号:z_zhizuobiao

版权所有 职坐标-一站式AI+学习就业服务平台 沪ICP备13042190号-4
上海海同信息科技有限公司 Copyright ©2015 www.zhizuobiao.com,All Rights Reserved.
 沪公网安备 31011502005948号    

©2015 www.zhizuobiao.com All Rights Reserved