在上一章中,您设置了一个简单的 Phong 光照模型。近年来,研究人员在基于物理的渲染 (PBR) 方面取得了长足的进步。PBR 尝试准确表示真实世界的着色,真实世界中离开表面的光量小于表面接收的光量。在现实世界中,对象的表面并不像到目前为止那样完全平坦。如果你观察周围的物体,你会注意到它们的基本颜色是如何根据光线照射到它们身上的方式而变化的。有些对象具有光滑的表面,而有些对象具有粗糙的表面。哎呀,有些甚至可能是闪亮的金属!
以这个带有砖块纹理的球体为例。左侧的渲染显示一个简单的颜色纹理,阳光直接照射在其上。右侧基于物理的渲染是本章结束时将实现的目标。
3D 艺术家通过为他们的模型创建材质来实现真实世界的着色。根据表面的复杂程度,此材质可能是纹理,也可能是表示特定特性强度的数值。您将创建材质,并在必要时添加纹理,以改善渲染。
基于物理的渲染PBR
顾名思义,PBR尝试重现现实物理世界中光和表面的的相互作用。既然Apple Vision Pro是基于现实的,那么渲染模型以匹配其物理环境就变得更加重要。
注意:仅仅因为您可以制作逼真的渲染,这并不意味着您总是应该这么做。迪士尼使用风格化的PBR,您可以更改片元着色器以产生所需的结果。没有“标准” PBR着色器代码,您可以选择以任何的方式解释所提供的资源材质。
PBR的一般原则是:
•表面不能反射比它接受到的更多的光。
•可以用已知的,可测量的物理特性来描述表面。
双向反射分布函数(BRDF)定义了表面如何响应灯光。目前已经有很多针对散射光和镜面光的高级数学BRDF模型,不过最常用的是朗伯特散射(Lambertian diffuse),对于镜面高光则是Cook-Torrance模型(在SIGGRAPH 1981提出)。这考虑到:
•微平面法线分布:上一章简要覆盖了微平面,以及光线如何在表面从许多方向上射出。
•菲涅尔:如果您直视着一个清澈的湖泊,你可以看到湖底,但是,如果您看湖泊的远处,您只会看到像镜子一样的反射。这是菲涅尔效应,其中表面的反射率取决于视角。
•几何衰减:微平面的自阴影。
这些组件中的每一个都有不同的近似值或许多聪明人编写的模型。这是一个庞大而复杂的话题。在本章的资源文件夹中,references.markdown包含了许多PBR的扩展读物,您可以在其中更多地了解基于物理的渲染和所涉及的计算。您还将在第21章“基于图像的光照”中了解更多有关BRDF和菲涅耳的信息。
常见PBR材质属性
Poly Haven (https://polyhaven.com) 有一些很棒的 3D 资源和纹理。例如,此闹钟型号:
对于此模型,艺术家为 PBR 光照模型中使用的五种最常见的材质属性创建了纹理:
这些属性可以是 纹理 或 值。
• 漫反射颜色 (Albedo反照率):反照率最初是一个天文学术语,用于描述和衡量太阳辐射漫反射,但在计算机图形学中,它表示没有应用任何阴影的表面颜色。漫反射颜色可能在纹理中内置了一些着色。您已经遇到过基础颜色贴图形式的漫反射颜色。
• 法线(Normal):纹理可以覆盖顶点法线值并扭曲照亮的片段以获得额外的表面细节。
• 粗糙度(Roughness):表示表面光泽度的灰度值。白色是粗糙的,黑色是光滑的。如果您的表面有划痕、有光泽,则粗糙度纹理可能主要由黑色或深灰色和浅灰色划痕组成。
•金属度(Metalic):表面要么是电流的导体,在这种情况下是金属;或者它不是导体 - 在这种情况下,它是绝缘体。大多数金属纹理仅由0(黑色)和1(白色)值组成:绝缘体为0,金属为1。
•环境光遮蔽(AO):AO定义了多少光到达表面。例如,较少的光到达角落和缝隙。您将漫反射颜色乘以AO值,因此AO默认值为1,AO纹理是灰度化的,可以增强阴影的外观。
当您为每个片段编写PBR光照着色时,您需要得到漫反射和镜面反射颜色。您从漫反射颜色,金属度和AO中得到颜色。您在前面混合体的基础上引入粗糙度值,以获得镜面反射颜色。
复杂的渲染可以使用许多其他材质属性,但是目前,您只会在材质中包含这些属性。
起始项目
➤ 在 Xcode 中,打开本章的入门项目。
➤ 构建并运行应用程序,您将看到使用 Phong着色渲染的 final-sphere.usdz。球体具有颜色纹理,但结果是平坦且无趣的。
场景中有两个灯光 - 一个太阳光和一个来自后面的柔和定向补光灯。该代码与上一章末尾的代码几乎相同, GameScene 和 SceneLighting 除外,它们只是设置不同的模型和光照。
Lighting.metal 减少镜面高光的数量。按下 “1” 和 “2” 键将分别带您到前视图和默认视图。
由于 PBR 着色在数学上非常具有挑战性,因此 Lighting.h 和 PBR.metal 包含两个函数,当前已注释掉,用于计算漫反射和镜面反射颜色。有许多不同的 PBR 着色模型,但这种镜面反射光照是经过修改的 Cook-Torrance 模型。(R.L. Cook 和 K.E. Torrance 在 1982 年提出了这个计划。)
在 Reality Composer Pro 中检查 USD 文件
您可能想知道如何更改材质并向 USD 模型添加纹理。
Reality Composer Pro 是 Apple 的一款新应用程序,包含在 Xcode 中。您可以加载 USD 文件并创建一个场景,然后将该场景导出到新的 USD 文件。该应用程序旨在创建 RealityKit 和 visionOS 项目,但是,您也可以为自己的项目创建场景和更改材质。在 Blender 中创建模型时,您可以在 Reality Composer Pro 中组装包含这些模型的场景。如果您想了解有关 Reality Composer Pro 的更多信息,Apple 已经记录了完整的演练(https://developer.apple.com/documentation/ visionos/designing-realitykit-content-with-reality-composer-pro)。
➤ 在 Xcode 中,选择菜单选项 Xcode > Open Developer Tool > Reality Composer Pro。
注意:Reality Composer Pro 专为使用 visionOS 进行开发而设计,在撰写本文时,visionOS 还非常新。如果您没有看到 Reality Composer Pro 的菜单选项,则可能需要下载包含 visionOS SDK 的 Xcode 测试版 (https:// developer.apple.com/download/applications/)。
➤ 创建一个名为 Sphere 的新项目,并保存文件以便再次找到它。
➤ 从您的 Xcode 项目中,将 starter-sphere.usdz 拖到 scene assembly 面板中。您可能只能看到 3D 小工具,直到您缩小以查看整个场景。
以下是用于导航场景的控件:
• 单击并拖动背景以环顾场景。
• 按住 Command 键单击并拖动背景以进行平移。
• 按住 Option 键单击并拖动背景以放大和缩小(或使用触控板缩放手势或鼠标滚轮)。
• 您也可以使用主视图底部的图标。
球体相当大,因此您应该缩小以查看所有内容。导航场景时,单击背景。如果单击并拖动球体,则会在场景中移动它。
➤ 单击球体以选择它,然后在检查器中,将变换位置设置为零。
➤ 在场景层次结构中,按住 Option 键并单击Root以查看模型的所有元素。
➤ 在 Scene Hierarchy 中,选择 brick 以在 Inspector 中查看球体的材质。
Reality Composer Pro 向您显示可用于 RealityKit 渲染的所有材质属性。球体具有深灰色漫反射颜色,该颜色被砖纹理覆盖。每个材质属性值旁边都有一个下载图标,您可以在其中向模型添加纹理。
此模型只有一种材质,但是,正如您稍后将看到的,模型可以针对模型几何的不同部分使用多种材质。
➤ 找到本章的资源文件夹。您将找到三种纹理:
• brick-color.png:漫反射颜色纹理。
• brick-roughness.png:这描述了球体表面的光泽度。
• brick-normal.png:法向贴图更复杂,您将在本章后面了解有关扭曲法线的更多信息。
➤ 将brick-roughness.png拖动到 Reality Composer Pro 中的Roughness下载图标,然后拖动brick-normal.png到Normal下载图标。对于每个纹理,请检查场景中模型外观的差异。
应用这些纹理后,Reality 渲染的球体看起来完全不同。现在了解如何在你自己的渲染中执行此操作。
您应该能够从Reality Composer Pro的文件菜单中导出 USD 球体,但在撰写本文时,这并不总是有效。您可以尝试使用导出的球体,但您的入门项目包含拥有这些纹理的 final-sphere.usdz。
注: 您可以使用 Reality Converter (https://developer.apple.com/augmented-reality/tools/)转换 OBJ 和 glTF 模型,也可以添加纹理。
材质
在向render中添加更多纹理之前,您需要设置基本的默认材质值,以便 PBR 着色器可以开始工作。
➤ 在 Xcode 中,打开 Common.h,并添加一个新的结构体来保存材质值:
typedef struct {vector_float3 baseColor;float roughness;float metallic;float ambientOcclusion;
} Material;
如您所见,基于物理的着色器有更多的材质属性可用,但这些是最常见的。法线值由顶点缓冲区提供。
➤ 打开 Submesh.swift,并在Submesh中的textures属性下创建一个新属性来保存 Submesh 的材质:
var material: Material
在初始化 material 之前,您的项目不会编译。
➤ 在 Submesh.swift 的底部,使用初始化器创建一个新的Material扩展:
private extension Material {init(material: MDLMaterial?) {self.init()if let baseColor = material?.property(with: .baseColor),baseColor.type == .float3 {self.baseColor = baseColor.float3Value}ambientOcclusion = 1}
}
在Submesh.Textures中,您可以从子网格的材质属性中,读入纹理的文件名。如果没有特定属性的纹理可用,则可以使用材料基本颜色。例如,如果对象是纯红色,则不必经过制作纹理的麻烦,您只需使用Float3(1,0,0)的材料基本颜色来描述颜色。
当前,您尚未加载或使用环境遮蔽,但是默认值应为1.0(白色)。
➤在Init中,在Init(mdlsubmesh:mtksubmesh :)初始化纹理后,初始化材料:
material = Material(material: mdlSubmesh.material)
现在,您将把此材料发送给着色器。到目前为止,您应该熟悉这一系列代码。
➤打开Common.h,并向BufferIndices添加另一个索引:
MaterialBuffer = 14
➤打开Rendering.swift。在render(encoder:uniforms:params:),在for submesh in mesh.submeshes中,您调用setFragmentTexture的位置,添加以下内容:
var material = submesh.material
encoder.setFragmentBytes(&material,length: MemoryLayout<Material>.stride,index: MaterialBuffer.index)
该代码将材料结构体发送到片元着色器。只要您的材料结构体步长小于4K字节,就无需创建和持有专门的缓冲区。
➤打开Lighting.h并删除围绕两个PBR函数computeSpecular和computeDiffuse的注释。
➤打开PBR.metal并从文件的起始和结尾处删除注释。这些函数之所以被注释,是因为它们是引用您刚刚才定义的Material。
➤打开Fragment.metal,并将以下添加为fragment_main的参数:
constant Material &_material [[buffer(MaterialBuffer)]],
你将模型的材质属性传递给片段着色器。您可以在名称前使用 _,因为 _material 是恒定的,很快您将需要使用纹理的底色(如果有)更新结构体。
➤ 在 fragment_main 的顶部,添加以下内容,以便您可以在着色器中覆盖材质值:
Material material = _material;
➤ 在 fragment_main 中,替换:
float3 baseColor = baseColorTexture.sample(textureSampler,in.uv * params.tiling).rgb;
为:
if (!is_null_texture(baseColorTexture)) {material.baseColor = baseColorTexture.sample(textureSampler,in.uv * params.tiling).rgb;
}
如果纹理存在,将材质基础颜色替换为从纹理中提取的颜色。否则,您已经在 material 中加载了基础颜色。
➤ 仍然在 fragment_main 中,删除使用 Phong 着色器的法线和颜色定义:
float3 normalDirection = normalize(in.worldNormal);
float3 color = phongLighting(normal,in.worldPosition,params,lights,baseColor
);
return float4(color, 1);
➤ 在其位置添加以下代码以使用 PBR 着色器:
// 1
float3 normal = normalize(in.worldNormal);
float3 diffuseColor =computeDiffuse(lights, params, material, normal);
// 2
float3 specularColor = computeSpecular(lights, params, material, normal);
// 3
return float4(diffuseColor + specularColor, 1);
浏览代码:
1. 首先使用灯光、材质和表面法线计算漫反射颜色。
2. 然后计算镜面反射颜色,其中包括使用参数中提供的相机位置。将 PBR.metal 中的代码与 Lighting.metal 中的 Phong 着色器进行比较。PBR 代码还有一些元素需要计算。
3. 将漫反射和镜面反射颜色组合在一起以获得最终结果。
➤ 构建并运行应用程序。到目前为止,除了中心左侧的一个小白点外,您应该不会看到太大差异。这个小白点是镜面高光。
表面粗糙度
表面越光滑,它应该越闪亮。到目前为止,您尚未在 Material 中设置粗糙度值,因此粗糙度为零。表面无限闪亮。
➤ 在 Fragment.metal 中,在 fragment_main 顶部,初始化材质后,添加以下内容:
material.roughness = 0.4;
➤ 构建并运行应用程序。
镜面高光现在更加明显。粗糙度值越高,镜面反射就越宽。
将一个粗糙度值应用于整个表面不是很现实。设置粗糙度纹理贴图将允许片段着色器以不同的方式对每个片段进行着色。模型的粗糙度纹理将使砖块发亮,就像被雨水打过一样,而插入的水泥填充物则完全没有光泽。
这是球体的粗糙度纹理:
当您从灰度纹理中读取时,砖块的粗糙度值将接近于零,而水泥的粗糙度值为 1.0,因此不会反射光线。
➤ 移除 material.roughness = 0.4。
➤ 打开 Submesh.swift,并在Submesh.Textures 的
var roughness: MTLTexture?
➤ 在 Submesh.Textures 扩展中,将以下代码添加到 init(material:) 的末尾:
roughness = material?.texture(type: .roughness)
这将以与加载颜色纹理相同的方式加载粗糙度纹理。如果没有纹理,您还需要读取材质值。
➤ 在 Material 的 init(material:) 底部,添加:
if let roughness = material?.property(with: .roughness),roughness.type == .float {self.roughness = roughness.floatValue
}
打开 Common.h 并将新的纹理索引定义添加到 TextureIndices:
NormalTexture = 1,
RoughnessTexture = 2,
MetallicTexture = 3,
AOTexture = 4
您还没有准备好添加其他纹理,但您可以设置索引以供以后使用。
➤ 打开 Rendering.swift,然后在render(encoder:uniforms:params:)中找到将基础颜色纹理发送到 fragment 函数的位置,然后添加以下代码:
encoder.setFragmentTexture(submesh.textures.roughness,index: RoughnessTexture.index)
您可以创建命令以将粗糙度纹理发送到 GPU。
➤打开Fragment.metal,在fragment_main中,将粗糙度纹理添加到参数列表:
texture2d<float> roughnessTexture [[texture(RoughnessTexture)]]
➤在fragment_main的主体中,在初始化法线之前,请添加以下内容:
if (!is_null_texture(roughnessTexture)) {material.roughness = roughnessTexture.sample(textureSampler,in.uv * params.tiling).r;
}
如果纹理存在的话,您会从粗糙度纹理中读取粗糙度值。如果没有纹理,PBR着色器将使用材料的粗糙度值。与baseColor不同,roughness是一个浮点值,因此您可以从纹理的红色通道中读取值。
➤构建并运行应用程序。
当您通过拖动旋转场景时,您会注意到砖块泛起亮点,但水泥砂浆却没有。
球体看起来更加生动,但仍然缺少一些细节。那就是法向贴图起作用的地方。
法向贴图
这是您想要的最终渲染效果:
与当前渲染的不同之处在于,这个球体是在应用了法向贴图的情况下渲染的。这种法向贴图使得球体看起来就像是具有许多角落和缝隙的高精度模型。事实上,这些高精度细节只是一种错觉。
法向贴图纹理看起来像这样:
所有模型都有垂直于每个面朝外的法线。例如,立方体有六个面,每个面的法线指向不同的方向。此外,每个面都是平坦的。如果您想要创建凹凸不平的错觉,则需要在片段着色器中更改法线。
查看以下图像。左侧是片段着色器中有法线的平坦表面。在右边,您会看到扰动的法线。法向贴图中的纹素通过RGB通道提供了这些法线的方向向量。
现在,看看这个单块砖块分为构成RGB图像的红色,绿色和蓝色通道。
每个通道的值在0到1之间,并且您通常会在灰度图上可视化它们,因为更容易读取颜色值。例如,在红色通道那里,值为0的时候是完全没有红色分量,值为1时是完全红色。当我们把0转换为RGB颜色值为(0, 0, 0),即黑色。处于这个范围另一端,(1, 1, 1)是白色,中间值(0.5, 0.5, 0.5)是中度灰色。在灰度中,RGB值三个分量值都是相同的,因此您只需要通过单个浮点值来引用灰度值。
仔细观察红色通道砖块的边缘。查看灰度图像中的左右边缘。红色通道中法线指向左侧(-x,0,0)的片段具有最深的颜色,而法线指向右侧(+x,0,0)的片段具有最浅的颜色。
现在看绿色通道。左右边缘具有相等的值,但对于砖块的顶部和底部边缘的值有所不同。灰度图像中的绿色通道中法线指向(0,-y,0)的片段具有最深的颜色,而法线指向(0, +y,0)的片段具有最浅的颜色。
最后,蓝色通道在灰度图像中主要是白色的,因为砖块(除纹理中一些不规则的地方)指向外侧。砖的边缘应该是法线唯一不指向外侧的地方(注:即z分量为0)。
注: 法向贴图可以是右手或左手系。渲染器将期望正 y 值向上,但某些应用程序会生成正 y 值向下的法向贴图。要解决此问题,您可以将法向贴图导入 Photoshop 并反转绿色通道。
一个法向贴图的基础颜色为 (0.5, 0.5, 1),贴图中所有法线都是“正交”(与平面垂直)的。
这是一种吸引人的颜色,但不是随意选择的。RGB 颜色的值介于 0 和 1 之间,而模型的法线值介于 -1 和 1 之间。法向贴图中的颜色值 0.5 转换为模型法线中的0。从法向贴图中读取平面纹素的结果应为 z值 为 1,x、y值 为 0。将这些值 (0, 0, 1) 转换为法向贴图的色彩空间会得到颜色 (0.5, 0.5, 1)。这就是大多数法向贴图显示为蓝色的原因。
在照片编辑器中查看法向贴图纹理,您可能会认为它们是颜色,但诀窍是将 RGB 值视为数字数据而不是颜色数据。
注: 大多数 3D 模型都包含法线值,但您可能会遇到必须生成法线的奇怪文件。模型 I/O 可以使用 MDLMesh.addNormals(withAttributeNamed:creaseThreshold:)创建法线。creaseThreshold决定了您希望平滑每个多边形的边缘的程度。
创建法向贴图
要创建成功的法向贴图,您需要一个专门的应用程序。您已经在第 8 章 “纹理” 中了解了纹理应用程序,例如 Adobe Substance Designer 和 Mari。这两个应用程序都是程序化的,将生成法向贴图和j基础颜色纹理。事实上,本章开头图像中的砖块纹理是在 Adobe Substance Designer 中创建的。
雕刻程序,例如 ZBrush、3D-Coat 和 Blender 也将从您的雕刻中生成法向贴图。首先,您雕刻一个详细的高精度网格。然后,该应用程序会查看雕刻的空腔和曲率,并烘焙法向贴图。由于具有大量顶点的高精度网格在游戏中不节省资源,因此您应该创建一个低精度网格,然后将法向贴图应用于此网格。
Photoshop 和 Adobe Substance 3D Sampler 可以从照片或漫反射纹理生成法向贴图。因为这些应用程序会查看阴影并计算值,所以它们不如雕刻或程序化应用程序好,但是拍摄现实生活中的个人对象的照片,通过这些应用程序之一运行它,并渲染出着色模型,可能会非常神奇。
下面是使用 Adobe 的 Bitmap2Material 创建的法向贴图:
在右侧,法向贴图渲染到具有最小几何体和白色基色的简单立方体模型上。
切线空间
要使用法向贴图纹理进行渲染,可以采用与颜色纹理相同的方式将其发送到片段函数,并使用相同的 UV 提取法线值。但是,您不能直接将法向贴图值应用于模型的当前法线。在片段着色器中,模型的法线位于世界空间中,法向贴图法线位于切线空间中。切线空间有点难以理解。想象一个立方体,它的六个面都指向不同的方向。现在想象将法向贴图应用于砖块,所有砖块在所有六个面上都具有相同的颜色。
如果立方体一个面指向负 x,法向贴图如何知道指向该方向?
以球体为例,每个片段都有一个切线,即在该点接触球体的直线。因此,此切线空间中的法线向量是相对于表面的。您可以看到所有箭头都与切线成直角。因此,如果你把所有的切线都放在一个平坦的表面上,蓝色的箭头会指向同一方向。这就是切线空间!
下图显示了立方体在世界空间中的法线。
为了将立方体的法线变为切线空间,您可以创建一个TBN矩阵 - 这是一个切线副切线法线矩阵,它是根据每个顶点的切线,副切线和法线值计算得出的。
在TBN矩阵中,法线是正交向量。切线是沿水平表面指向的矢量。 副切线是由叉乘计算得到的矢量,它垂直于切线和法线。
注: 叉积是一种运算,它为您提供一个垂直于其他两个向量的向量。
切线可以在任何方向上与法线成直角。但是,要在模型的不同部分(甚至完全不同的模型)上共享法向贴图,有两个标准:
1.在模型空间中定义的切线和副切线将各自表示u和v指向的方向。
2.红色通道表示沿u的曲率,绿色通道表示沿v的曲率。
加载模型时可以计算这些值。但是,使用Model I/O,只要您拥有位置和纹理坐标属性的数据,Model I/O就可以在每个顶点为您计算和存储这些切线和副切线值。
使用法向贴图
➤在Geometry组中,打开Submesh.swift,并在Submesh.Textures中添加新属性:
var normal: MTLTexture?
➤在SubMesh.Textures.init(material:)的结尾处,读取以下纹理:
normal = material?.texture(type: .tangentSpaceNormal)
您可以在子网格指向的法向贴图纹理中读取。
➤打开Rendering.swift,在render(encoder:uniforms:params:)中,定位到for submesh in mesh.submeshes中设置基本颜色纹理的地方。
➤添加此:
encoder.setFragmentTexture(submesh.textures.normal,index: NormalTexture.index)
在这里,您将法线纹理发送到GPU。
➤打开Fragment.metal,在fragment_main中,将法线纹理添加到参数列表中:
texture2d<float> normalTexture [[texture(NormalTexture)]]
现在您要传输法线纹理贴图,第一步是将其应用于球体,就好像颜色纹理那样。
➤在fragment_main中,替换float3 normal = normalize(in.worldNormal);为:
float3 normal;
if (is_null_texture(normalTexture)) {normal = in.worldNormal;
} else {normal = normalTexture.sample(textureSampler,in.uv * params.tiling).rgb;
}
normal = normalize(normal);
return float4(normal, 1);
由于并非所有模型都带有纹理,因此请检查是否存在纹理。从纹理中读取法线值(如果有),否则设置默认法线值。返回只是临时的,以确保应用程序正确加载法向贴图,并且法向贴图和 UV 匹配。
➤ 构建并运行以验证法向贴图是否提供片段颜色。
您可以看到法向贴图提供的所有表面细节。
➤ 优秀!您测试了法向贴图的加载情况,因此请从fragment_main移除:
return float4(normal, 1);
现在先别庆祝。您还有几项任务摆在面前。如果你现在运行这个应用程序,你会得到一些奇怪的光照,而且没有颜色。您仍然需要:
1. 使用Model I/O 加载切线和副切线值。
2. 指示渲染命令编码器将新创建的包含值的 MTLBuffers 发送到 GPU。
3. 在顶点着色器中,将切线和副切线值更改为世界空间(就像您执行法线一样),并将新值传递给片段着色器。
4. 根据这些值计算新法线。
1.加载切线和副切线
Model I/O将在新的MTLBuffer中为您创建切线和副切线属性。首先,定义这些新的缓冲属性和缓冲区索引。
➤打开common.h并将其添加到属性:
Tangent = 3,
Bitangent = 4
➤将索引添加到BufferIndices:
TangentBuffer = 2,
BitangentBuffer = 3,
➤打开VertexDescriptor.swift,查看MDLVertexDescriptor的defaultLayout。
在这里,您告诉顶点描述器,有位置,法线和UV属性。Model I/O将在缓冲区中创建切线和副切线属性值,但是您必须告诉GPU在这些缓冲区中读取。
当您在渲染器中创建管道状态时,管道描述符将defaultLayout用作顶点描述符,现在将通知GPU,它需要为这两个额外的缓冲区创建空间。重要的是要记住,模型的顶点描述符布局必须与渲染编码器的管道状态中的相匹配。
➤在返回之前,将其添加到MDLVertexDescriptor的defaultLayout中:
vertexDescriptor.attributes[Tangent.index] =MDLVertexAttribute(name: MDLVertexAttributeTangent,format: .float3,offset: 0,bufferIndex: TangentBuffer.index)
vertexDescriptor.layouts[TangentBuffer.index]= MDLVertexBufferLayout(stride: MemoryLayout<float3>.stride)
vertexDescriptor.attributes[Bitangent.index] =MDLVertexAttribute(name: MDLVertexAttributeBitangent,format: .float3,offset: 0,bufferIndex: BitangentBuffer.index)
vertexDescriptor.layouts[BitangentBuffer.index]= MDLVertexBufferLayout(stride: MemoryLayout<float3>.stride)
您可以为两个缓冲区设置定义和索引,一个用于切线,一个用于副切线。
注意:到目前为止,您只为所有模型创建了一个管道描述符。但模型通常需要不同的顶点布局。或者,如果您的某些模型不包含法线、颜色和切线,您可能希望节省为它们创建缓冲区的时间。您可以为不同的顶点描述符布局创建多个管道状态,并在绘制每个模型之前替换渲染编码器的管道状态。
➤ 打开 Geometry 组中的 Model.swift,然后在init(name:)中,替换:
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 inmtkMeshes.append(try! MTKMesh(mesh: mdlMesh,device: Renderer.device))
}
由于您需要使用切线和副切线更改网格,因此您需要从资产中提取所有 MDLMeshe。您可以从这些 MDLMesh 创建一个 MTKMesh 数组。
➤ 在 mtkMeshes.append 之前,添加以下代码:
mdlMesh.addTangentBasis(forTextureCoordinateAttributeNamed:MDLVertexAttributeTextureCoordinate,tangentAttributeNamed: MDLVertexAttributeTangent,bitangentAttributeNamed: MDLVertexAttributeBitangent)
对于每个 MDLMesh,添加 tangent 和 bitangent 值。
2.发送切线和副切线值到GPU
➤ 打开 Rendering.swift,然后在render(encoder:uniforms:params:)中,找到 for mesh in meshes。
对于每个网格,您当前正在将所有顶点缓冲区发送到 GPU:
for (index, vertexBuffer) in mesh.vertexBuffers.enumerated() {encoder.setVertexBuffer(vertexBuffer,offset: 0,index: index)
}
此代码包括发送 tangent 和 bitangent 缓冲区。您应该知道发送到 GPU 的缓冲区数量。在 Common.h 中,您已将 UniformsBuffer 设置为索引 11,但如果将其定义为索引 3,则现在将与副切线缓冲区发生冲突。
➤构建并运行该应用程序,以确保您的球体仍然呈现。拖动球周围检查镜面反射。
令人失望。所有这些工作,您似乎已经向后退了一步。但是不用担心!在计算机图形学中,当您在着色器中应用正确的计算时,黑屏通常可以解析为光荣的技术彩色。
3.转换切线和副切线为世界坐标
正如您将模型的法线转换为世界空间一样,您需要将切线和副切线转换为顶点函数中的世界空间。
➤在着色器组中,打开ShaderDefs.h,然后将这些新属性添加到顶点:
float3 tangent [[attribute(Tangent)]];
float3 bitangent [[attribute(Bitangent)]];
➤将新属性添加到VertexOut,以便您可以将值发送到片段函数:
float3 worldTangent;
float3 worldBitangent;
➤打开Vertex.metal,在vertex_main中,计算out.worldNormal后添加:
.worldTangent = uniforms.normalMatrix * in.tangent,
.worldBitangent = uniforms.normalMatrix * in.bitangent
该代码将切线和副切线的值变换为世界空间。
4.计算新的法线
现在您已经准备好了一切,计算新法线将是一件简单的事情。
在进行法线计算之前,请考虑您正在读取的法线颜色值。颜色值介于 0 和 1 之间,但法线值的范围介于 -1 到 1 之间。
➤ 打开 Fragment.metal,然后在 fragment_main 中找到您对 normalTexture 进行采样的位置。在条件语句的 else 部分中,从纹理读取法线后,添加:
normal = normal * 2 - 1;
此代码将法线值重新分配为 -1 到 1 范围内。
➤ 在前面的代码之后,还是在条件语句的 else 部分里面,添加这个:
normal = float3x3(in.worldTangent,in.worldBitangent,in.worldNormal) * normal;
此代码将重新计算切线空间的法线方向,以匹配法线纹理的切线空间。
➤ 构建并运行应用程序以查看应用于球体的法向贴图。
多么不同啊!旋转场景时,请注意照明如何影响模型上的小空腔 — 这几乎就像您创建了新的几何体,但您没有。这就是法向贴图的魔力:为简单的低精度模型添加惊人的细节。
其他纹理贴图类型
法向贴图和粗糙度贴图并不是更改模型表面的唯一方法。您可以将材质值替换为任何纹理。例如,您可以创建描述表面透明部分的不透明度贴图。或者一个内置反射对象的反射贴图。
事实上,您能想到的用于描述表面的任何值 (厚度、曲率等) 都可以存储在纹理中。您只需使用 UV 坐标在纹理中查找相关片段,并使用恢复后的值。这是编写自己的渲染器的好处之一。您可以选择要使用的贴图以及如何应用它们。
您可以在片段着色器中使用所有这些纹理,并且几何形状不会更改。
注: 位移贴图或高度贴图可以更改几何形状。您将在第19章“镶嵌与地形”中阅读有关位移的内容。
挑战
您的挑战是从 Apple 的 AR Quick Look 库 (https://developer.apple.com/augmented-reality/quick-look/) 下载并渲染玩具鼓手模型。
在 GameScene.swift 中,您需要将鼓手的 scale 更改为 0.5,并将 rotation.y 更改为 Float.pi 以匹配您的场景比例。检查挑战文件夹中的工程,了解摄像机目标和鼓手居中的距离。更改 SceneLighting 以匹配挑战赛项目的光照。还有额外的灯光可以充分展示鼓手。
您的第一次渲染应如下所示:
在第一次渲染后,就像您对粗糙度纹理所做的那样,在 Submesh、Rendering.swift 和 fragment_main 中将金属和环境光遮蔽纹理添加到您的代码中。然后,欣赏您最终的基于物理的渲染。注意鼓手下巴下的阴影。这来自 Ambient Occlusion 贴图。
参考
https://zhuanlan.zhihu.com/p/394423022
https://zhuanlan.zhihu.com/p/398061293