Metal - 纹理(一)

啥是馒头(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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350