首页 iOS.& Swift Books 金属由教程

7
地图& Materials 由Caroline Begbie撰写

在上一章中,使用模型I / O,您导入并呈现出具有平坦颜色纹理的简单房子。但如果你看一下你周围的物体,你会注意到他们的基本颜色如何根据光线落在它们上的基本颜色。有些物体具有光滑的表面,有些物体具有粗糙的表面。哎呀,有些人甚至可能是闪亮的金属!

在 this chapter, you’LL了解如何使用材料集群来描述表面,以及如何为微细详细设计纹理。 This is also the final chapter on how to render still models.

普通地图

以下示例最能介绍普通地图:

在左边,有一个带有颜色纹理的立方体。在右侧,具有相同的低聚立方体具有相同的颜色纹理和照明,然而,它还具有第二个纹理,适用于它,称为a 正常地图.

使用普通地图,它看起来好像立方体是一个高多方面,其中所有Nooks和Crankies建模到对象中。但这只是一个幻觉!

对于工作的幻觉,它需要一个纹理,如下所示:

所有型号都具有垂直于各脸部的正常。立方体有六个面,每个面部的正常点在不同方向。而且,每张脸都是平的。如果您想创建陷阱的错觉,则需要在片段着色器中更改正常。

在以下图像中,左侧是一个平坦的表面,在片段着色器中的正常。在右边,你看到扰乱了法线。正常地图中的纹理通过RGB通道提供这些法线的方向向量。

看看这个单一的砖块分成一个构成RGB图像的红色,绿色和蓝色频道。

Each channel has a value between 0 and 1, and you generally visualize them in grayscale as it’s easier to read color values. For example, in the red channel, a value of 0 is no red at all, while a value of 1 is full red. When you convert 0 to an RGB color (0, 0, 0), that results in black. On the opposite spectrum, (1, 1, 1) is white, and in the middle you have (0.5, 0.5, 0.5) which is mid-gray. In grayscale, all three RGB values are the same, so you only need refer to a grayscale value by a single float.

Take a closer look at the edges of the red channel’s brick. Look at the left and right edges in the grayscale image. The red channel has the darkest color where the normal values of that fragment should point left (-X, 0, 0), and the lightest color where it should point right (+X, 0, 0).

Now look at the green channel. The left and right edges have equal value but are different for the top and bottom edges of the brick. The green channel in the grayscale image has darkest for pointing down (0, -Y, 0) and lightest for pointing up (0, +Y, 0).

最后,蓝色频道大多是白色的灰度图像,因为砖 - 除了纹理中的一些不规则性 - 向外点。砖的边缘是唯一应该指出的地方。

笔记:正常地图可以是右手或左撇子。您的渲染会期望正面y升级,但有些应用程序将生成正常映射,呈正y下降。要解决此问题,您可以将普通地图带入Photoshop并反转绿色通道。

The base color of a normal map — where all normals are “normal” (orthogonal to the face) — is (0.5, 0.5, 1).

这是一种有吸引力的颜色,但没有任意选择。 RGB颜色具有0和1之间的值,而模型的正常值在-1和1之间。正常映射中的0.5的颜色值转换为0的模型为0。

从普通贴图读取扁平Texel的结果应该是1的Az值,x和y值为1,x和y值为1,x和y值为1和x和颜色空间的颜色空间正常地图的颜色导致颜色(0.5,0.5,1)。这就是为什么大多数普通地图高呼吸的原因。

创建普通地图

要创建成功的普通贴图,您需要专门的应用程序。在上一章中,您了解了纹理化应用程序,例如物质设计师和Mari。这两个应用程序都是程序性的,将生成正常地图以及基本颜色纹理。事实上,在章节开始时图像中的砖块纹理是在物质设计师中创建的。

切线空间

您以与颜色纹理相同的方式将普通贴图发送到片段函数,并且使用相同的UV提取正常值。但是,您无法直接将您的普通贴图值应用于模型的当前正常数。在您的片段着色器中,模型的法线在世界空间,普通地图正常 切线空间.

使用普通地图

开辟本章的入门项目。注意上一章的最终代码有一些更改:

return float4(normalValue, 1);

return float4(normalValue, 1);

1.装载切线和排点

打开 VertexDescriptor.swift. and look at defaultVertexDescriptor. Model I/O is currently reading into the vertex buffer the normal values from the .obj file. And you can see that you’re telling the vertex descriptor that there are normal values in the attribute named MDLVertexAttributeNormal.

let (mdlMeshes, mtkMeshes) = 
     try! MTKMesh.newMeshes(asset: asset,
                            device: Renderer.device)
var mtkMeshes: [MTKMesh] = []
let mdlMeshes = 
     asset.childObjects(of: MDLMesh.self) as! [MDLMesh]
_ = mdlMeshes.map { mdlMesh in
  mdlMesh.addNormals(withAttributeNamed: 
                        MDLVertexAttributeNormal,
                     creaseThreshold: 1.0)
  mtkMeshes.append(try! MTKMesh(mesh: mdlMesh, 
                                device: Renderer.device))
}

mdlMesh.addNormals(withAttributeNamed: MDLVertexAttributeNormal,
                   creaseThreshold: 1.0)
mdlMesh.addTangentBasis(forTextureCoordinateAttributeNamed: 
            MDLVertexAttributeTextureCoordinate,
          tangentAttributeNamed: MDLVertexAttributeTangent,
          bitangentAttributeNamed: MDLVertexAttributeBitangent)
static var vertexDescriptor: MDLVertexDescriptor =
  MDLVertexDescriptor.defaultVertexDescriptor
Model.vertexDescriptor = mdlMesh.vertexDescriptor
let vertexDescriptor = Model.vertexDescriptor

2.向GPU发送切线和批量值

Renderer.swift., in draw(in:), locate // render multiple buffers and these lines of code:

let vertexBuffer = mesh.mtkMesh.vertexBuffers[0].buffer
renderEncoder.setVertexBuffer(vertexBuffer, offset: 0,
                      index: Int(BufferIndexVertices.rawValue))
for (index, vertexBuffer) in 
      mesh.mtkMesh.vertexBuffers.enumerated() {
  renderEncoder.setVertexBuffer(vertexBuffer.buffer,
                                offset: 0, index: index)
}
typedef enum {
  BufferIndexVertices = 0,
  BufferIndexUniforms = 1,
  BufferIndexLights = 2,
  BufferIndexFragmentUniforms = 3
} BufferIndices;
typedef enum {
  BufferIndexVertices = 0, 
  BufferIndexUniforms = 11,
  BufferIndexLights = 12,
  BufferIndexFragmentUniforms = 13
} BufferIndices;

3.将切线和批量值转换为世界空间

就在您将模型的正常转换为世界空间时,您需要将切线和分批转换为顶点函数的世界空间。

Tangent = 3,
Bitangent = 4
float3 tangent [[attribute(Tangent)]];
float3 bitangent [[attribute(Bitangent)]];
float3 worldTangent;
float3 worldBitangent;
.worldTangent = uniforms.normalMatrix * vertexIn.tangent,
.worldBitangent = uniforms.normalMatrix * vertexIn.bitangent,

4.计算新的正常情况

现在你有一切都到位,计算新的正常情况是一个简单的事情。

normalValue = normalValue * 2 - 1;
float3 normalDirection = normalize(in.worldNormal);
float3 normalDirection = float3x3(in.worldTangent, 
                                  in.worldBitangent, 
                                  in.worldNormal) * normalValue;
normalDirection = normalize(normalDirection);

其他纹理地图类型

正常地图不是更改模型表面的唯一方法。还有其他纹理地图:

材料

并非所有型号都有纹理。例如,您在本书中呈现的火车具有不同的材料组,可指定颜色而不是使用纹理。

typedef struct {
  vector_float3 baseColor;
  vector_float3 specularColor;
  float roughness;
  float metallic;
  vector_float3 ambientOcclusion;
  float shininess;
} Material;
let material: Material
private extension Material {
  init(material: MDLMaterial?) {
    self.init()
    if let baseColor = material?.property(with: .baseColor),
      baseColor.type == .float3 {
      self.baseColor = baseColor.float3Value
    }
  }
}
if let specular = material?.property(with: .specular),
  specular.type == .float3 {
  self.specularColor = specular.float3Value
}
if let shininess = material?.property(with: .specularExponent),
  shininess.type == .float {
  self.shininess = shininess.floatValue
}
material = Material(material: mdlSubmesh.material)
BufferIndexMaterials = 14
var material = submesh.material
renderEncoder.setFragmentBytes(&material,
              length: MemoryLayout<Material>.stride,
              index: Int(BufferIndexMaterials.rawValue))
constant Material &material [[buffer(BufferIndexMaterials)]],
float3 baseColor = material.baseColor;
float3 materialSpecularColor = material.specularColor;
float materialShininess = material.shininess;

功能专业化

多年来,有很多关于如何渲染不同材料的讨论。您是否应该为差异创建单独的短片段着色器?或者你应该有一个长的“优步”着色器,有条件列出的所有可能性?功能专业化处理此问题,并允许您创建一个着色器,编译器变成单独的着色器。

static func makeFunctionConstants(textures: Textures) 
                                  -> MTLFunctionConstantValues {
  let functionConstants = MTLFunctionConstantValues()
  var property = textures.baseColor != nil
  functionConstants.setConstantValue(&property, 
                                     type: .bool, index: 0)
  property = textures.normal != nil
  functionConstants.setConstantValue(&property, 
                                     type: .bool, index: 1)
  return functionConstants
}
let functionConstants = 
      makeFunctionConstants(textures: textures)
let fragmentFunction: MTLFunction?
do {
  fragmentFunction = 
         try library?.makeFunction(name: "fragment_main",
                            constantValues: functionConstants)
} catch {
  fatalError("No Metal function exists")
}
constant bool hasColorTexture [[function_constant(0)]];
constant bool hasNormalTexture [[function_constant(1)]];
float3 baseColor = material.baseColor;
float3 baseColor;
if (hasColorTexture) {
  baseColor = baseColorTexture.sample(textureSampler,
                       in.uv * fragmentUniforms.tiling).rgb;
} else {
  baseColor = material.baseColor;
}
float3 normalValue;
if (hasNormalTexture) {
  normalValue = normalTexture.sample(textureSampler,
                       in.uv * fragmentUniforms.tiling).rgb;
  normalValue = normalValue * 2 - 1;
} else {
  normalValue = in.worldNormal;
}
normalValue = normalize(normalValue);
texture2d<float> baseColorTexture [[texture(BaseColorTexture), 
                          function_constant(hasColorTexture)]],
texture2d<float> normalTexture [[texture(NormalTexture), 
                          function_constant(hasNormalTexture)]],
if (hasColorTexture) {
  return float4(1, 0, 0, 1);
}
return float4(0, 1, 0, 1);

物理上基于渲染

为了实现壮观的场景,你需要有良好的纹理,但照明发挥了更大的作用。近年来,物理基于渲染(PBR)的概念变得比简单的Phong着色模型更流行。随着其名称表明,PBR试图用表面的灯光实际互动。现在,增强现实已成为我们生活的一部分,使您的模型符合其健身环境更为重要。

PBR工作流程

首先,更改片段功能以使用PBR计算。里面 auppesh.swift.swift., in makePipelineState(textures:), change the name of the referenced fragment function from "fragment_main" to "fragment_mainPBR".

let roughness: MTLTexture?
roughness = property(with: .roughness)
if let roughness = material?.property(with: .roughness),
  roughness.type == .float3 {
  self.roughness = roughness.floatValue
}
property = textures.roughness != nil
functionConstants.setConstantValue(&property, 
                                   type: .bool, index: 2)
property = false
functionConstants.setConstantValue(&property, 
                                   type: .bool, index: 3)
functionConstants.setConstantValue(&property, 
                                   type: .bool, index: 4)
renderEncoder.setFragmentTexture(submesh.textures.roughness,
                                 index: 2)
camera.distance = 3
camera.target = [0, 0, 0]

渠道包装

稍后,您将再次使用PBR片段函数进行渲染。即使您不了解数学,也会了解函数的布局和所使用的概念。

roughness = roughnessTexture.sample(textureSampler, in.uv).r;

挑战

在里面 资源 本章文件夹是来自Sketchfab.com的Demeter Dzadik的神话般的宝箱模型。您的挑战是呈现此模型!有三种纹理将加载到资产目录中。不要忘记将解释从颜色更改为数据,因此纹理不会加载为SRGB。

然后去哪儿?

天空是极限!现在,您可以在物理上渲染的胃口展示,探索梦幻般的联系 参考.Markdown. 你会发现哪个 资源 文件夹。一些链接是高度数学的,而其他链接则用华丽的照片图像解释。

有一个技术问题?想记者一个错误吗? 您可以向官方书籍论坛中的书籍作者提出问题和报告错误 这里.

有反馈分享在线阅读体验吗? 如果您有关于UI,UX,突出显示或我们在线阅读器的其他功能的反馈,您可以将其发送到设计团队,其中表格如下所示:

© 2021 Razeware LLC

您可以免费读取,本章的部分显示为 混淆了 文本。解锁这本书,以及我们整个书籍和视频目录,带有Raywenderlich.com的专业订阅。

现在解锁

要突出或记笔记,您需要在订阅中拥有这本书或自行购买。