纹理(二)
sRGB 颜色空间
sRGB 是一种标准的颜色格式,它是站在我们人类肉眼的对不同颜色的可分辨度和敏感度去定义的一种颜色格式。如果用 sRGB 去表示灰度颜色数据的话,你会发现 sRGB 的颜色渐变并不是线性的。我们会看到暗色区域变化更快,而大部分的渐变区域都是浅色。因为对于人眼来说,对浅色的敏感度其实是高于深色的,所以我们会用更多的值去表示浅色。相对于 Abobe RGB 来说,sRGB 的色域更窄,但是由于 sRGB 作为标准的 RGB,使得让它可以保证在不同设备上的颜色表现都是一致的。
在 textureLoaderOptions 配置设置的时候,其实是可以选择设置是否 sRGB 的。
let textureLoaderOptions : [MTKTextureLoader.Option : Any] =
[.origin:
MTKTextureLoader.Origin.bottomLeft, .SRGB: false];
设置成非 sRGB 之后,跑出来的效果会比原来亮很多~
GPU frame capture
GPU frame capture 就是 GPU 的 debug 工具。
在调试工具栏中,相机图标的就是它啦!
GPU frame capture 可以帮助我们拿到渲染管线中每一步的操作、信息以及对应的渲染结果,比如 setFragmentBytes setRenderPipelineState 等。
选中 GPU 堆栈中的 drawIndexedPrimitives 指令,我们可以看到如下的界面:
那我们如何来看这个数据信息呢?这张表格都告诉了我们什么?
MDL_OBJ-Indices: 顶点的索引、offset等
Buffer 0x: 顶点缓冲区信息,包含了顶点的位置、法线、纹理坐标数据等,跟 shader 中定义的 struct 结构成员变量相对应。
Vertex Bytes:矩阵信息
Vertex Attributes: 顶点属性信息,表示从顶点处理函数中返回的顶点数据。
Vertex_mainP:顶点处理函数
xxx.png Texture0:指在位置0的纹理贴图
Fragment Bytes:光照信息队列以及光照数量
MTKView Depth:深度缓冲信息,黑色表示更近,白色则表示更远
Samplers 采样器
在之前的工程中,我们已经用到了 Sampler,但是也是最基础的初始化。sampler 的初始化还可以设置 filter 和 address 参数。其中 filter 表示 sampler 将如何从原图中采样,是线性采样还是最近邻采样,前者比较顺滑,后者是偏向像素风。而 address 参数则表示,纹理将以何种方式去填充缓冲区。其中 repeat 表示重复平铺满缓冲区,mirrored_repeat 表示每行用镜像的方式平铺。clamp_to_edge 表示当图像大小不够撑满整个缓冲区时,将其最边缘的像素扩展填充满整个空间。clamp_to_zero 就表示不进行任何处理,不够就不够,放在那儿就行。
Sampler states
在上述的介绍中,我们的采样器 Samplers 相关设置或初始化都是写在片元处理函数 fragment_main 中。那么,也就是说所有用这个片元处理函数的模型在进行纹理采样的时候,都是用这同一种采样器。那如何做到对不同的模型使用不同的采样器呢?
在这里 Sampler State 就发挥作用了。不同的模型使用不同的采样器,最好的方式就是将采样器信息封装在模型中。所以,这里我们在 Model 类中添加一个 samplerState:MTLSamplerState 的属性,然后再添加一个 buildSamplerState 的方法来对属性进行初始化。
let samplerState: MTLSamplerState?
private static func buildSamplerState() -> MTLSamplerState? {
let descriptor = MTLSamplerDescriptor()
descriptor.sAddressMode = .repeat
descriptor.tAddressMode = .repeat
let samplerState =
Renderer.device.makeSamplerState(descriptor: descriptor)
return samplerState
}
然后,在 Model 中添加好 SamplerState 属性之后,我们需要在 Renderer 类中将该信息传递到 GPU。所以,在 in draw(in:) 函数中,加入以下代码:
renderEncoder.setFragmentSamplerState(model.samplerState, index: 0)
最后,需要在 shader 的 fragement_main 中添加对应的 sampler 参数来接受信息:
sampler textureSampler [[sampler(0)]],
然后把原来 fragment_main 中有关 sampler 初始化的代码删掉,就好啦。
Mipmaps
当采样器取到的纹素数量比像素量更多的时候,比如我们观察远距离的物体贴图的时候,会发现物体的表面会产生锯齿。可以想象到的是,这个问题的产生明显带来了两个弊端:第一,产生图像锯齿一定是我们不想看到的。第二,远距离的物体在做纹理贴图的时候,其实并不需要完全将原贴图的数据采样渲染出来,也就是说浪费了性能并且影响到渲染的速度。而 Mipmap 的产生就是为了解决这个问题。
Mipmap 就是为了加快渲染速度和减少图像锯齿,将原来的贴图处理成一系列优化过的图片的文件。Mipmap 中每个层级的小图都是由原图的一个特定比例的缩小细节的复制,当贴图被缩小或者只需要从远距离去观看时,mipmap 就会转换到适当的层级。由于 mipmap 贴图需要被读取的像素远小于普通贴图,所以渲染的速度就得到了提升,并且 mipmap 的图片是已经经过抗锯齿处理的,同时也减少了实时渲染的负担。
Mipmaps 中的图片包含了在原图尺寸基础上,所有小于原图尺寸的,2的n次幂的等级。我们假设原图是一张 64 * 64 尺寸的纹理,那么整个 mipmap 的集合将是 Level 0: 64 x 64,1: 32 x 32, 2: 16 x 16, 3: 8 x 8, 4: 4 x 4, 5: 2 x 2, 6: 1
如图所示就是一个 mipmap 如何存储的例子:
那么在 Metal 中,我们可以简单的通过如下代码在对应纹理第一次被加载的时候来创建纹理的 mipmap。首先在我们加载 Texture 的地方,初始化一个 textureLoaderOption:[MTKTextureLoader.Option: Any].
let textureLoaderOptions: [MTKTextureLoader.Option: Any] =
[.origin: MTKTextureLoader.Origin.bottomLeft,
.SRGB: false,
.generateMipmaps: NSNumber(booleanLiteral: true)]
相对应的,我们需要在 Model 类中,生成 SamplerState 的函数中,也就是上面的 buildSamplerState() 将 descriptor 的 mipFilter 属性改为线性 .linear。因为 mipFilter 属性的默认值是不使用 mipmap 的,也就是 .notMipmapped 。除了 .linear还有另外的 .nearest 枚举,在进行对应设置的时候,GPU 都会根据配置对正确的 mipmap 进行采样。
最后
本章主要介绍纹理相关的理论知识以及调试工具的基本用法,后续如有完善会继续补充~