本系列文章是对 http://metalkit.org 上面MetalKit内容的全面翻译和学习.
在第13部分
末尾,我们说过让我们的行星看起来更真实有两种方法:添加纹理,或添加一些噪声到plante
颜色中.我们在第14部分
已经展示了添加噪声.这周我们看看纹理和采样.纹理非常有用,因为比起为每个顶点计算颜色,它可以给表面提供更好的细节.
让我们比第13部分Part 13
开始,因为我们不再需要噪声代码了.首先,在MetalView.swift
中移除mouseDown
函数,我们已经不再需要它了.同时移除mouseBuffer
和pos
变量,同时移除代码中对它们的引用.然后,创建一个新的纹理对象:
var texture: MTLTexture!
下一步,将这行(可能你已经在前面清理中移除过了):
commandEncoder.setBuffer(mouseBuffer, offset: 0, atIndex: 2)
替换为:
commandEncoder.setTexture(texture, atIndex: 1)
同时改变timer
的缓冲器索引,从1改为0:
commandEncoder.setBuffer(timerBuffer, offset: 0, atIndex: 0)
我在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)]]属性拿到了input
纹理,因为这个属性就是我们设置到命令编码器里时的索引.同时,访问权限设置为read.然后我们将其读取到color
变量,然而,它却是上下颠倒的.为了修复这个问题,在下一行我们为每个像素反转Y轴.输出图片看起来应该像这样:
如果你打开图片并与我们的输出比较,你会发现它已经旋转到正确朝向了.下一步,我们要找回我们的行星及周围的黑暗天空.用下面一大块代码替换output
行:
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);
这段代码看起来很熟悉,因为前些章节我们已经讨论过如何创建行星及周围的黑色空间.输出图片看起来应该像这样:
现在很好!下一步我们让行星转动起来.用下面一大块代码替换
output
行:
uv = fmod(float2(gid) + float2(timer * 100, 0), float2(width, height));
color = input.read(uint2(uv));
output.write(distance < 0 ? color : float4(0), gid);
这段代码看起来又很熟悉,因为前些章节我们已经讨论过如何使用timer
让行星动起来.输出图片看起来应该像这样:
这看起来就比较傻了!输出的图像看起来像一个人举着火把紧贴墙壁走在黑暗的洞穴里.用下面的代码替换最后三行:
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来匹配行星对象的尺寸和纹理尺寸.然后,神奇的地方来了.让我们引入sampler采样器.sampler采样器
是一个包含了各种渲染状态的对象,让纹理来配置:坐标,寻址方式(这里设置为repeat
)和过滤方法(设置为linear
).下一步,计算球面上每个点的normal法线
.最后,我们用采样来计算color
而不是像前面一样直接读取.还有一件事要做,在内核参数列表中,让我们也把纹理访问权限由read
改为sample
.将这一行:
texture2d<float, access::read> input [[texture(1)]],
替换为:
texture2d<float, access::sample> input [[texture(1)]],
输出图像看起来应该像这样:
这就是我们所说的真实的行星表面!还要感谢 Chris的帮助.
源代码source code 已发布在Github上.
下次见!