写在前面:
对Metal技术感兴趣的同学,可以关注我的专题:Metal专辑
也可以关注我个人的简书账号:张芳涛
所有的代码存储的Github地址是:Metal
正文
在第14部分的最后,我们得出结论,我们可以通过两种方式使我们的星球看起来更逼真:要么对其应用纹理,要么为行星颜色添加一些噪点。 我们展示了如何在第15部分中添加噪点。接下来我们将重点介绍纹理和采样器。 纹理是有用的,因为它们可以为表面提供比每个顶点的颜色计算更高水平的细节。
让我们从第14部分中的位置开始,因为这次我们不需要噪声代码。 首先,在MetalView.swift
中,让我们删除mouseDown
函数,因为我们不再需要它了。 同时删除mouseBuffer
和pos
变量,以及代码中对它们的任何引用。 然后,创建一个新的纹理对象:
var texture: MTLTexture!
接下来,替换下面的代码:
commandEncoder.setBuffer(mouseBuffer, offset: 0, atIndex: 2)
用如下的代码:
commandEncoder.setTexture(texture, atIndex: 1)
并将我们的计时器的缓冲区索引从1
更改为0
:
commandEncoder.setBuffer(timerBuffer, offset: 0, atIndex: 0)
我在Playground
的Resources
文件夹中添加了一个名为texture.jpg
的图像,但如果需要,可以添加你的图像。 让我们创建一个使用此图像设置或纹理的函数:
func setUpTexture() {
let path = NSBundle.mainBundle().pathForResource("texture", ofType: "jpg")
let textureLoader = MTKTextureLoader(device: device!)
texture = try! textureLoader.newTextureWithContentsOfURL(NSURL(fileURLWithPath: path!), options: nil)
}
接下来,在init
函数中调用此函数:
override public init(frame frameRect: CGRect, device: MTLDevice?) {
super.init(frame: frameRect, device: device)
registerShaders()
setUpTexture()
}
现在,让我们在Shaders.metal
中清理我们的内核,只包括这些行:
kernel void compute(texture2d<float, access::write> output [[texture(0)]],
texture2d<float, access::read> input [[texture(1)]],
constant float &timer [[buffer(1)]],
uint2 gid [[thread_position_in_grid]])
{
float4 color = input.read(gid);
gid.y = input.get_height() - gid.y;
output.write(color, gid);
}
您将首先注意到我们通过[[texture(1)]]
属性获取输入纹理,因为这是我们在命令编码器中设置它的索引。 此外,还会读取我们请求的访问权限。 然后我们将它读入颜色变量,然而,它翻转倒置。 为了解决这个问题,在下一行我们只需翻转每个像素的Y
坐标。 输出图像应如下所示:
如果打开图像并将其与输出进行比较,您会发现它现在已正确定位。 接下来,我们想要带回我们的星球和它周围的黑暗天空。 用这个代码块替换输出行:
int width = input.get_width();
int height = input.get_height();
float2 uv = float2(gid) / float2(width, height);
uv = uv * 2.0 - 1.0;
float radius = 0.5;
float distance = length(uv) - radius;
output.write(distance < 0 ? color : float4(0), gid);
这段代码看起来很熟悉,因为我们已经在前面的章节中讨论了如何创建行星及其周围的黑色空间。 输出图像应如下所示:
到现在为止还挺好! 我们接下来想让我们的星球再次旋转。 用这个代码块替换输出行:
uv = fmod(float2(gid) + float2(timer * 100, 0), float2(width, height));
color = input.read(uint2(uv));
output.write(distance < 0 ? color : float4(0), gid);
这段代码在前几章中再次看起来很熟悉,我们讨论了如何使用计时器为地球制作动画。 输出图像应如下所示:
![效果图](https://upload-images.jianshu.io/upload_images/661949-a2dc0936c21c9d78.gif?imageMogr2/auto-orient/strip)
这有点尴尬! 输出看起来像聚光灯。 用这段代码替换我们添加的最后三行:
uv = uv * 2;
radius = 1;
constexpr sampler textureSampler(coord::normalized,
address::repeat,
min_filter::linear,
mag_filter::linear,
mip_filter::linear );
float3 norm = float3(uv, sqrt(1.0 - dot(uv, uv)));
float pi = 3.14;
float s = atan2( norm.z, norm.x ) / (2 * pi);
float t = asin( norm.y ) / (2 * pi);
t += 0.5;
color = input.sample(textureSampler, float2(s + timer * 0.1, t));
output.write(distance < 0 ? color : float4(0), gid);
首先,我们缩小到纹理大小的一半并将半径设置为1
,这样我们就可以将行星对象大小与纹理大小相匹配。 然后是神奇的。 让我介绍一下采样器。 采样器是包含纹理需要配置的各种渲染状态的对象:其坐标,寻址模式(此处设置为重复)和滤波方法(此处设置为线性)。 接下来,我们计算球体上每个点的法线,然后使用法线计算球体周围的角度。 最后,我们通过采样来计算颜色,而不是像之前那样读取颜色。 还有一件事要做。 在内核参数列表中,让我们重新配置对sample
的纹理访问而不是read
。 替换下面的代码:
texture2d<float, access::read> input [[texture(1)]],
用如下代码:
texture2d<float, access::sample> input [[texture(1)]],
输出图像应如下所示: