本系列文章是对 http://metalkit.org 上面MetalKit内容的全面翻译和学习.
今天我们关注Metal function
中没用过的类型,kernel function内核函数或compute shader计算着色器.你将经常听到它们两个的混合词变形词.内核是用于计算
任务,也就是在GPU
上进行大规模并行计算.例如:图像处理,科学模拟,等等.关于内核有一些重要特点:没有渲染管线,函数总是返回void
,并且名字总是以kernel关键字开头,就像我们以前用过的前面带有vertex
和vertex
关键字的函数一样.
让我们从第8部分Part 8的playground处继续.首先,删除MathUtils.swift
因为我们已经不需要了.然后,在MetalView.swift
中删除createBuffers()
函数及其在初始化中的调用,还有两个缓冲器.将MTLRenderPipelineState
声明替换为MTLComputePipelineState声明.下一步,来到registerShaders()
函数.下面是新旧两个版本的不同:
注意,我们不在使用descriptor
了,而是在内核函数中直接创建MTLComputePipelineState
.下一步,我们看看drawRect()
函数的不同:
注意,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
,你应该能看到类似的视图:
如果你看到了上面的输出,就说明准备好继续下去了.从当前开始,我们将不再关注主代码(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);
你可能已经猜到了,我们拿到纹理的width
和height
,然后根据像素在纹理中的位置来计算red
和green
的值,然后将新颜色写入回纹理中.你将看到类似这样的东西:
接着,让我们在屏幕中间画一个黑色的圆.用下面几行代码替换最后一行:
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);
你会看到类似这样的东西:
我们到底是怎么做到的呢?其实,这是在着色中很常用的技术,叫做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);
你应该看到类似这样的东西:
很漂亮,对吧?现在我们让它变成了日食,让我们将它变得更真实一些.我们需要另一个圆(太阳),并且我们想要让初始的圆向左一点,向下一点,这样它们就都能看到了.用下面几行替换我们定义内部
的那行:
float distToCircle2 = dist(uv, float2(-0.1, 0.1), 0.5);
bool inside = distToCircle2 < 0;
你会看到类似下面的东西:
我们现在只是学会了着色技术的皮毛.在下一章节我们将学习更复杂和动态的计算任务.特别感谢Chris Wood的建议.
源代码source code 已发布在Github上.
下次见!