[MetalKit]16-Using-MetalKit-part-10使用MetalKit10

730 阅读4分钟

本系列文章是对 metalkit.org 上面MetalKit内容的全面翻译和学习.

MetalKit系统文章目录


今天我们关注Metal function中没用过的类型,kernel function内核函数compute shader计算着色器.你将经常听到它们两个的混合词变形词.内核是用于计算任务,也就是在GPU上进行大规模并行计算.例如:图像处理,科学模拟,等等.关于内核有一些重要特点:没有渲染管线,函数总是返回void,并且名字总是以kernel关键字开头,就像我们以前用过的前面带有vertexvertex关键字的函数一样.

让我们从第8部分Part 8的playground处继续.首先,删除MathUtils.swift因为我们已经不需要了.然后,在MetalView.swift中删除createBuffers()函数及其在初始化中的调用,还有两个缓冲器.将MTLRenderPipelineState声明替换为MTLComputePipelineState声明.下一步,来到registerShaders()函数.下面是新旧两个版本的不同:

chapter10_1.png

注意,我们不在使用descriptor了,而是在内核函数中直接创建MTLComputePipelineState.下一步,我们看看drawRect()函数的不同:

chapter10_2.png

注意,currentRenderPassDescriptor也不用了.命令编码器则用computeCommandEncoder()函数来创建.显然,我们也不再需要设置顶点缓冲器和绘制基本体了.作为替代,使用用一个设置了纹理的内核函数,创建线程组并指派它们干活.我们用MTLSize来设置线程组的维数,及每次计算调用中要执行的线程组的数量.

最后,我们到Shaders.metal文件中,用下面代码替换所有内容:

#include <metal_stdlib>

using namespace metal;

kernel void compute(texture2d<float, access::write> output [[texture(0)]],
                    uint2 gid [[thread_position_in_grid]])
{
    output.write(float4(0, 0.5, 0.5, 1), gid);
}

我们只简单地给纹理中的每个像素/位置设置了相同的颜色.现在如果你到playground的主页面,并显示Assistant editor中的Timeline,你应该能看到类似的视图:

chapter10_3.png

如果你看到了上面的输出,就说明准备好继续下去了.从当前开始,我们将不再关注主代码(MetalView.swift)了,因为我们所有的工作都将在内核着色器中完成.

好了,让我们先从简单的开始.用下面的代码替换内核函数中的代码:

int width = output.get_width();
int height = output.get_height();
float red = float(gid.x) / float(width);
float green = float(gid.y) / float(height);
output.write(float4(red, green, 0, 1), gid);

你可能已经猜到了,我们拿到纹理的widthheight,然后根据像素在纹理中的位置来计算redgreen的值,然后将新颜色写入回纹理中.你将看到类似这样的东西:

chapter10_4.png

接着,让我们在屏幕中间画一个黑色的圆.用下面几行代码替换最后一行:

float2 uv = float2(gid) / float2(width, height);
uv = uv * 2.0 - 1.0;
bool inside = length(uv) < 0.5;
output.write(inside ? float4(0) : float4(red, green, 0, 1), gid); 

你会看到类似这样的东西:

chapter10_5.png

我们到底是怎么做到的呢?其实,这是在着色中很常用的技术,叫做distance function.我们使用length函数来确定像素是否在屏幕中心也就是我们圆的中心的0.5倍之内.注意,我们归一化了uv向量来匹配窗口坐标范围 [-1,1].最后,我们判断像素如果在内部就是黑色,否则就像原来一样,给它一个渐变色.

让我们抽出这个圆内部/外部计算到一个距离函数中:

float dist(float2 point, float2 center, float radius)
{
    return length(point - center) - radius;
} 

然后,用下面几行替换我们定义内部的那行:

float distToCircle = dist(uv, float2(0), 0.5);
bool inside = distToCircle < 0;

看不到任何改变,但我们现在有了一个可以轻易重用的函数.下一步,让我们看看如何根据到圆的距离改变背景颜色,而不是仅根据像素的绝对位置.我们通过计算像素到圆的距离来改变透明通道的值.用下面这行替换最后一行:

output.write(inside ? float4(0) : float4(1, 0.7, 0, 1) * (1 - distToCircle), gid);

你应该看到类似这样的东西:

chapter10_6.png

很漂亮,对吧?现在我们让它变成了日食,让我们将它变得更真实一些.我们需要另一个圆(太阳),并且我们想要让初始的圆向左一点,向下一点,这样它们就都能看到了.用下面几行替换我们定义内部的那行:

float distToCircle2 = dist(uv, float2(-0.1, 0.1), 0.5);
bool inside = distToCircle2 < 0;

你会看到类似下面的东西:

chapter10_7.png

我们现在只是学会了着色技术的皮毛.在下一章节我们将学习更复杂和动态的计算任务.特别感谢Chris Wood的建议.

源代码source code 已发布在Github上.

下次见!