Metal - 纹理(一)

2019-05-26  本文已影响0人  沙琪玛dd

啥是馒头(Metal)

纹理(一)

UV 坐标

UV 坐标是用于描述纹理贴纸的坐标系。所有的图像文件都是一个二维的平面,水平方向为 U 轴,垂直方向为 V 轴。通过这个二维的 UV 坐标系,可以定位图像上的任意一个像素。纹理的 UV 坐标可以通过映射将模型表面的点和平面图像上的像素对应起来,这样就可以在模型表面上定位纹理贴图了。

在我们用建模工具导出一个带有纹理的模型到 .obj 文件的时候,会将纹理 UV 坐标的数据同时导出在 obj 文件中。我们可以打开 Resource 文件夹下的 monkey.obj 文件一探究竟。在用记事本打开 obj 文件后,我们可以看到紧跟着顶点数据之后,会有 vt 开头的纹理坐标数据,它代表的就是当前顶点映射的纹理贴图中的纹理坐标数据。

如果你想自己动手搭一个简单的 3d 模型,除了用 PhotoShop,我推荐使用免费的 Blender 工具。当然还有其他收费的建模工具,比如 Substance Designer 和 Substance Painter、3DCoat 等等。

如何给模型贴纹理

接下来我们开始实践给模型贴纹理,首先在工程的 Resource 文件夹下,我们有完整的模型文件以及贴图文件,先将它们导入到项目中来。接下来打开本章 Texture 目录下的 Start 文件夹,打开工程文件。没错,工程文件就是上一章 Transform 中的 final 工程,我们接着给这个猴子贴图,让它变成一只更好(chou)看(lou)的猴子。

首先打开 Shaders.metal 文件,看到 fragment_main 的片元处理函数。这里以前的代码是返回 float4(0, 1, 0, 1) 的颜色值,也就是为什么我们看到的是一只绿猴子的原因。接下来我们将改造这个函数,实现给猴子贴纹理的功能。

那么如何在片元处理函数中读取纹理贴图呢,我们将过程主要分为以下几步:

  1. 将纹理的 UV 坐标添加到 model 的顶点描述器(vertex descriptor)中。
  2. 在 shader 中的 VertexIn 结构中添加一个属性来匹配该顶点的 UV 坐标。
  3. 写一个函数来负责加载纹理图片。
  4. 在绘制 model 之前将加载进来的纹理贴图传给 fragment 函数。
  5. 在 fragment 函数中实现从纹理贴图中对像素进行采样

接下来我们会根据这几个主要步骤添加相关的代码实现

1.将纹理的 UV 坐标添加到 model 的顶点描述器(vertex descriptor)中

首先在 Common.h 文件中新增 Attributes 的枚举类型,用于 表示index 值。

typedef enum {
  Position = 0,
  Normal = 1,
  UV = 2
} Attributes;

然后在 Model.swift 文件中,给 defaultDescriptor 添加一个新属性。

 vertexDescriptor.attributes[Int(UV.rawValue)] = MDLVertexAttribute(name: MDLVertexAttributeTextureCoordinate, format: .float2, offset: 12, bufferIndex: 0)
 
         vertexDescriptor.layouts[0] = MDLVertexBufferLayout(stride: 20)
        

这里由于 position 的顶点位置数据已经占有 12 字节,float 4个字节乘以 x,y,z 三个数据。所以 textureCoordinate 的数据将从 offset 为 12 的地方开始读取。最后由于添加了 textureCoordinate 的数据,所以我们需要将 layout 的跨度加上 8 byte (textureCoordinate 数据为 float 4字节 * u、v两个数据)。

2.更新 Shader 中的 VertexIn 结构

在 shader 文件中,顶点处理函数的参数 vertexIn 通过 stage_in 属性与顶点描述器中的 layout 布局绑定。由于上面我们已经给顶点描述器添加了 textureCoordinate 属性,所以现在我们可以通过给 vertexIn 添加一个 UV 属性来获取顶点描述器中传递过来的 texturecoordinate 数据。

struct VertexIn {
    float4 position [[ attribute(Position) ]];
    float2 uv [[ attribute(UV) ]];
};

当然,与之对应的,在传递给片元处理器的数据结构中也要加上 uv 属性,所以我们要在 VertexOut 中添加上 uv 属性。

struct VertexOut {
    float4 position [[ position ]];
    float3 worldPosition;
    float2 uv;
};

然后在顶点处理函数 vertex_main() 中,在函数返回前添加 vertexout 从 vertexin 中赋值 uv 数据的代码。

out.uv = vertexIn.uv;

3.加载纹理图片

首先新建一个 swift 文件叫 Texturable.swift 用于负责加载纹理图片。然后添加一个 Texturable 的协议。

import MetalKit

protocol Texturable {}

extension Texturable {
}

然后在 Texturable 的扩展中,添加 loadTexture 函数。

    static func loadTexture(imageName: String) throws -> MTLTexture? {
        //5
        let textureLoader = MTKTextureLoader(device: Renderer.device)
        
        //6
        let textureLoaderOptions : [MTKTextureLoader.Option : Any] =
            [.origin:
                MTKTextureLoader.Origin.bottomLeft]
        
        //7
        let fileExtension = URL(fileURLWithPath: imageName).pathExtension.isEmpty ? "png" : nil
        
        //8
        guard let url = Bundle.main.url(forResource: imageName, withExtension: fileExtension) else {
            print("Failed to load")
            return nil
        }
        
        let texture = try textureLoader.newTexture(URL: url, options: textureLoaderOptions)
        print("loaded texture")
        return texture;
    }

然后在 SubMesh.swift 文件中,修改 SubMesh 使其遵循 Texturable 的协议,使 SubMesh 可以加载纹理图片。

extension Submesh : Texturable {
    
}

Model I/O 可以方便地将整个模型以及所有的材质都加载进来。点击查看 mtl 文件,可以在最下方看到 map_Kd monkey.png 。map_Kd 表示为漫反射指定颜色纹理文件(.mpc)或程序纹理文件(.cxc),或是一个位图文件。所以 Model I/O 在加载 mtl 文件时可以同时拿到 monkey.png 的纹理贴图文件。

由于每个 Submesh 可能对应不同的纹理,所以在 Submesh 中,我们需要创建一个 Textures 的结构体,并且持有一个 Textures 的属性。

struct Textures {
  let baseColor: MTLTexture?
}
let textures: Textures

Textures 结构体的属性是根据 MDLMaterialSemantic 的属性来定义的。MDLMaterialSemantic 表示对 Material 的语义描述,可以通过 semantic 获取到 material 对应的属性值。

接下来在 Submesh 文件中添加 Textures 的初始化函数

private extension Submesh.Textures {
  init(material: MDLMaterial?) {
    func property(with semantic: MDLMaterialSemantic) -> MTLTexture? {
      guard let property = material?.property(with: semantic),
        property.type == .string,
        let filename = property.stringValue,
        let texture = try? Submesh.loadTexture(imageName: filename)
else {
return nil
}
      return texture
    }
    baseColor = property(with: MDLMaterialSemantic.baseColor)
  }
}

上述初始化函数从 material 中加载了 base color texture 贴图,这里的 base Color 表示的是漫反射光的基础颜色。后面的章节中,会涉及到加载镜像反射的纹理,加载的过程都是类似的。

到这里,我们可以 run 一下看控制台输出,如果有输出 loaded texture 的话说明纹理已经可以成功加载啦。

4.在绘制 model 之前将加载进来的纹理贴图传给 fragment 函数

在之后的章节中,我们会用到不同的纹理类型,不同的纹理类型会用不同的索引来传递给片元处理函数。所以我们现在先在 Common.h 中创建一个新的枚举类型 Textures 用于区分不同的纹理缓冲区索引值。

typedef enum {
  BaseColorTexture = 0
} Textures;

然后在 Renderer.swift 中的 draw 函数,找到//9 的注释处,在处理每个 submesh 的代码中,修改代码如下:

      for modelSubmesh in model.submeshes {
                renderEncoder.setFragmentTexture(modelSubmesh.textures.baseColor, index: Int(BaseColorTexture.rawValue))
                let submesh = modelSubmesh.submesh
                renderEncoder.drawIndexedPrimitives(type: .triangle,
                                                    indexCount: submesh.indexCount,
                                                    indexType: submesh.indexType,
                                                    indexBuffer: submesh.indexBuffer.buffer,
                                                    indexBufferOffset: submesh.indexBuffer.offset)
            }

这样我们就通过 renderEncoder 将纹理传递给了 fragment 函数,并且保存在 texture buffer 0中。

所有的 Buffers,Textures 和 Sampler states 都是被一张参数表所持有的。我们是通过索引值来获取相对应的 buffer、texture 或者 sampler state。你可以将这个索引值理解为一个句柄,我们通过句柄去获取我们需要的东西。在 iOS 中,最多可以存在 31 个 buffer 缓冲区,31个纹理缓冲区以及 16 个采样器状态值。在 MacOS 中,纹理缓冲区的最大数量可以有 128 个。

5. 在 fragment 函数中实现从纹理贴图中对像素进行采样

接下来我们需要修改片元处理函数,去接收并且读取纹理数据。
首先我们在 Shaders.metal 文件的 fragment_main,也就是片元处理函数中添加一个新的参数代表 BaseColor 的纹理。

texture2d<float> baseColorTexture [[ texture(BaseColorTexture) ]],

当我们在对一个纹理进行读取或者采样时,我们不一定刚好对某一个像素能够采样到贴切的值。在纹理中,我们对纹理进行采样的基本单元叫做纹素(Texels)。我们可以通过采样器(Sampler)来决定如何去采样每个纹素。现在我们可以先在片元处理函数中创建一个最简单的默认采样器,修改 fragment_main 函数内容如下:

fragment float4 fragment_main(VertexOut in [[stage_in]],
                              texture2d<float> baseColorTexture [[ texture(BaseColorTexture)]]) {
    constexpr sampler textureSampler;
    float3 baseColor = baseColorTexture.sample(textureSampler, in.uv).rgb;
    return float4(baseColor ,1);
}

最后为了便于观察结果,我们将 metalView 的 clearColor 改为 RGB(202,225,255),然后 run 一下,可以看到结果:

textureResult.jpg

最后

本章节的主要工作是将纹理贴纸渲染出来,接下来的章节中会继续介绍 Samplers 采样、Mipmaps 以及其他相关内容。

Demo地址

点击查看 Whats Metal 第四节 纹理 Demo

上一篇 下一篇

猜你喜欢

热点阅读