Metal学习笔记十一:贴图和材质
在上一章中,您设置了一个简单的 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,法线贴图如何知道指向该方向?
以球体为例,每个片段都有一个切线,即在该点接触球体的直线。因此,此切线空间中的法线向量是相对于表面的。您可以看到所有箭头都与切线成直角。因此,如果你把所有的切线都放在一个平坦的表面上,蓝色的箭头会朝上指向同一方向。这就是切线空间!
下图显示了立方体在世界空间中的法线。
为了将立方体的法线变为切线空间(原文:To convert the cube’s normals to tangent space,似乎应该是:为了将立方体切线空间的法线变为世界空间),您可以创建一个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;
此代码将重新计算切线空间的法线方向,以匹配法线纹理的切线空间。(此处原文:This code recalculates the normal direction into tangent space to match the tangent space of the normal texture.似乎有误,应该理解为:此代码将重新计算法线纹理切线空间中的法线值,将其转为世界空间中的法线值。)
➤ 构建并运行应用程序以查看应用于球体的法线贴图。
多么不同啊!旋转场景时,请注意照明如何影响模型上的小空腔 — 这几乎就像您创建了新的几何体,但您没有。这就是法线贴图的魔力:为简单的低精度模型添加惊人的细节。
其他纹理贴图类型
法线贴图和粗糙度贴图并不是更改模型表面的唯一方法。您可以将材质值替换为任何纹理。例如,您可以创建描述表面透明部分的不透明度贴图。或者一个内置反射对象的反射贴图。
事实上,您能想到的用于描述表面的任何值 (厚度、曲率等) 都可以存储在纹理中。您只需使用 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
相关文章:
Metal学习笔记十一:贴图和材质
在上一章中,您设置了一个简单的 Phong 光照模型。近年来,研究人员在基于物理的渲染 (PBR) 方面取得了长足的进步。PBR 尝试准确表示真实世界的着色,真实世界中离开表面的光量小于表面接收的光量。在现实世界中…...
STM32-USART串口数据包
一:HEX数据包发送 1.为了收发数据包,先定义两个缓存区的数组 ,这4个数据只存储发送或者接收的载荷数据,包头和包尾不存 uint8_t Serial_TxPacket[4]; uint8_t Serial_RxPacket[4]; uint8_t Serial_RxFlag;//接收一个数据包就置F…...
【LeetCode 热题 100】438. 找到字符串中所有字母异位词 | python 【中等】
继续学!嗨起来!!!(正确率已经下30%了,我在干什么) 题目: 438. 找到字符串中所有字母异位词 给定两个字符串 s 和 p,找到 s 中所有 p 的子串,返回这些子串的…...
Ollama+Deepseek-R1+AnythingLLM本地个人知识库搭建
一、OllamaDeepseek-R1AnythingLLM本地个人知识库搭建 在搭建强大的本地个人知识库以及提升开发效率的技术体系中,Ollama、DeepSeek-R1 和 AnythingLLM 扮演着举足轻重的角色。Ollama 作为模型运行与管理工具,是整个技术架构中的关键枢纽,负…...
决策树(Decision Tree)基础知识
目录 一、回忆1、*机器学习的三要素:1)*函数族2)*目标函数2.1)*模型的其他复杂度参数 3)*优化算法 2、*前处理/后处理1)前处理:特征工程2)后处理:模型选择和模型评估 3、…...
跨域-告别CORS烦恼
跨域-告别CORS烦恼 文章目录 跨域-告别CORS烦恼[toc]1-参考网址2-思路整理1-核心问题2-个人思考3-脑洞打开4-个人思考-修正版1-个人思考2-脑洞打开 3-知识整理1-什么是跨域一、同源策略简介什么是源什么是同源是否是同源的判断哪些操作不受同源策略限制跨域如何跨域 二、CORS 简…...
浅论数据库聚合:合理使用LambdaQueryWrapper和XML
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 前言一、数据库聚合替代内存计算(关键优化)二、批量处理优化四、区域特殊处理解耦五、防御性编程增强 前言 技术认知点:使用 XM…...
css实现元素垂直居中显示的7种方式
文章目录 * [【一】知道居中元素的宽高](https://blog.csdn.net/weixin_41305441/article/details/89886846#_1) [absolute 负margin](https://blog.csdn.net/weixin_41305441/article/details/89886846#absolute__margin_2) [absolute margin auto](https://blog.csdn.net…...
Nerf流程
一.数据处理: 在输入数据时,并没有给出相机的内参与外参,需要在数据处理得出相机的内外惨数,作者使用COLMAP得到相机参数后,转成NeRF可以读取的格式即可以用于模型训练。 旋转矩阵的第一列到第三列分别表示了相机坐标系…...
Spring Cloud Alibaba学习 5- Seata入门使用
Spring Cloud Alibaba学习 5- Seata入门使用 Seata是Spring Cloud Alibaba中用于分布式事务管理的解决方案 一. Seata的基本概念 1. Seata的三大角色 1> TC (Transaction Coordinator) - 事务协调者 维护全局和分支事务的状态,驱动全局事务提交或回滚。TC作…...
Select 下拉菜单选项分组
使用<select>元素创建下拉菜单,并使用 <optgroup> 元素对选项进行分组。<optgroup> 元素允许你将相关的 <option> 元素分组在一起,并为每个分组添加一个标签。 <form action"#" method"post"><la…...
【无人机与无人车协同避障】
无人机与无人车协同避障的关键在于点云数据的采集、传输、解析及实时应用,以下是技术实现的分步解析: 1. 点云数据采集(无人机端) 传感器选择: LiDAR:通过激光雷达获取高精度3D点云(精度达厘米…...
AI视频领域的DeepSeek—阿里万相2.1图生视频
让我们一同深入探索万相 2.1 ,本文不仅介绍其文生图和文生视频的使用秘籍,还将手把手教你如何利用它实现图生视频。 如下为生成的视频效果(我录制的GIF动图) 如下为输入的图片 目录 1.阿里巴巴全面开源旗下视频生成模型万相2.1模…...
飞机大战lua迷你世界脚本
-- 迷你世界飞机大战 v1.2 -- 星空露珠工作室制作 -- 最后更新:2024年1月 ----------------------------- -- 迷你世界API适配配置 ----------------------------- local UI { BASE_ID 7477478487091949474-22856, -- UI界面ID ELEMENTS { BG 1, -- 背景 BTN_LE…...
Android15请求动态申请存储权限完整示例
效果: 1.修改AndroidManifest.xml增加如下内容: <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" /><uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /><uses-perm...
Java 导出大数据到 Excel 表格
背景 之前的项目一直是用XSSFWorkbook来做 Excel 导出,在遇到大数据导出时,经常会遇到 OOM。在 Apache Poi 3.8 之后的版本提供的 SXSSFWorkbook 可以优雅的解决这个问题。 原理 SXSSFWorkbook 被称为流式 API,主要是因为它采用了流式写入…...
GCC RISCV 后端 -- GCC Passes 注释
在前面文章提到,当GCC 前端完成对C源代码解析完成后,就会使用 处理过程(Passes)机制,通过一系列的处理过程,将 GENERIC IR 表示的C程序 转步转换成 目标机器的汇编语言。过程描述如下图所示: 此…...
稚晖君级硬核:智元公司开源机器人通信框架AimRT入驻GitCode平台
在科技的浪潮中,机器人技术正以前所未有的速度发展。它们不再只是科幻小说中的概念,而是逐渐融入到我们的日常生活中,从工厂的自动化生产线到家庭的智能助手,机器人的身影无处不在。然而,随着机器人应用的日益复杂&…...
STM32L051系列单片机低功耗应用
STM32L051单片机支持多种低功耗模式,包括 Sleep(睡眠)、Stop(停止) 和 Standby(待机) 模式。不同模式的功耗和唤醒方式不同。 一、低功耗相关介绍 1.1 低功耗模式概览 模式功耗唤醒源时钟状态…...
【代码分享】基于IRM和RRT*的无人机路径规划方法详解与Matlab实现
基于IRM和RRT*的无人机路径规划方法详解与Matlab实现 1. IRM与RRT*的概述及优势 IRM(Influence Region Map)通过建模障碍物的影响区域,量化环境中的安全风险,为RRT算法提供启发式引导。RRT(Rapidly-exploring Random…...
【JAVA架构师成长之路】【JVM实战】第1集:生产环境CPU飙高排查实战
课程标题:生产环境CPU飙高排查实战——从现象到根因的15分钟攻防战 目标:掌握CPU飙高问题的系统性排查方法,熟练使用工具定位代码或资源瓶颈 0-1分钟:问题引入与核心影响 线上服务器CPU突然飙升至90%以上,导致服务响应延迟激增,用户投诉激增。CPU飙高可能由死循环、线程…...
android edittext 防止输入多个小数点或负号
有些英文系统的输入法,或者定制输入法。使用xml限制不了输入多个小数点和多个负号。所以代码来控制。 一、通过XML设置限制 <EditTextandroid:id="@+id/editTextNumber"android:layout_width="wrap_content"android:layout_height="wrap_conten…...
Spring MVC 页面重定向返回后通过nginx代理 丢失端口号问题处理
Spring MVC页面重定向通过Nginx代理后出现端口丢失问题,通常由以下原因及解决方案构成: ## 一、Nginx配置问题(核心原因) 1. Host头传递不完整 Nginx默认未将原始请求的端口信息传递给后端,导致应用生成重定向…...
DeepSeek V3 源码:从入门到放弃!
从入门到放弃 花了几天时间,看懂了DeepSeek V3 源码的逻辑。源码的逻辑是不难的,但为什么模型结构需要这样设计,为什么参数需要这样设置呢?知其然,但不知其所以然。除了模型结构以外,模型的训练数据、训练…...
基于国产芯片的AI引擎技术,打造更安全的算力生态 | 京东零售技术实践
近年来,随着国产AI芯片的日益崛起,基于国产AI芯片的模型适配、性能优化以及应用落地是国产AI应用的一道重要关卡。如何在复杂的京东零售业务场景下更好地使用国产AI芯片,并保障算力安全,是目前亟需解决的问题。对此,京…...
LINUX网络基础 [一] - 初识网络,理解网络协议
目录 前言 一. 计算机网络背景 1.1 发展历程 1.1.1 独立模式 1.1.2 网络互联 1.1.3 局域网LAN 1.1.4 广域网WAN 1.2 总结 二. "协议" 2.1 什么是协议 2.2 网络协议的理解 2.3 网络协议的分层结构 三. OSI七层模型(理论标准) …...
Linux 开发工具
linux中,常见的软件安装方式---下载 yum/apt.rpm安装包安装源码安装 yum 查看软件包 通过yumlist命令可以罗列出当前⼀共有哪些软件包.由于包的数⽬可能⾮常之多,这⾥我们需要使⽤ grep 命令只筛选出我们关注的包.例如: # Centos $ yum list | grep lrzsz lr…...
SpringBoot 全局异常处理
文章目录 异常处理全局异常处理(推荐)局部异常处理高级技巧设置返回状态码处理404异常异常处理 全局异常处理(推荐) 创建一个全局异常处理类,使用 @RestControllerAdvice 注解标记。 在方法上使用 @ExceptionHandler 声明当前方法可处理的异常类型。当系统发生异常时,…...
EA - 开源工程的编译
文章目录 EA - 开源工程的编译概述笔记环境备注x86版本EABase_x86EAAssert_x86EAThread_x86修改 eathread_atomic_standalone_msvc.h原始修改后 EAStdC_x86EASTL_x86EAMain_x86EATest_x86备注备注END EA - 开源工程的编译 概述 EA开源了‘命令与征服’的游戏源码 尝试编译. 首…...
springboot3 WebClient
1 介绍 在 Spring 5 之前,如果我们想要调用其他系统提供的 HTTP 服务,通常可以使用 Spring 提供的 RestTemplate 来访问,不过由于 RestTemplate 是 Spring 3 中引入的同步阻塞式 HTTP 客户端,因此存在一定性能瓶颈。根据 Spring 官…...
【Python项目】基于深度学习的车辆特征分析系统
【Python项目】基于深度学习的车辆特征分析系统 技术简介:采用Python技术、MySQL数据库、卷积神经网络(CNN)等实现。 系统简介:该系统基于深度学习技术,特别是卷积神经网络(CNN),用…...
爬虫不“刑”教程
在大数据时代,信息的获取至关重要,而网络爬虫正是帮助我们从互联网上获取海量数据的重要工具。无论是数据分析、人工智能训练数据,还是商业情报收集,爬虫技术都能发挥重要作用。本篇文章将全面解析 Python 爬虫的各个方面…...
深入解析 supervision 库:功能、用法与应用案例
1. 引言 在计算机视觉任务中,数据的后处理和可视化是至关重要的环节,尤其是在目标检测、分割、跟踪等任务中。supervision 是一个专门为这些任务提供高效数据处理和可视化支持的 Python 库。本文将深入介绍 supervision 的功能、使用方法,并…...
【橘子golang】从golang来谈闭包
一、简介 闭包(Closure)是一种编程概念,它允许函数捕获并记住其创建时的上下文环境(包括变量)。闭包通常用于函数式编程语言,但在许多现代编程语言中也有支持,包括 Go ,Js等支持函数…...
盛铂科技PDROUxxxx系列锁相介质振荡器(点频源):高精度信号源
——超低相位噪声、宽频覆盖、灵活集成,赋能下一代射频系统 核心价值:以突破性技术解决行业痛点 在雷达、卫星通信、高速数据采集等高端射频系统中,信号源的相位噪声、频率稳定度及集成灵活性直接决定系统性能上限。盛铂科技PDROUxxxx系列锁…...
Linux | Vim 鼠标不能右键粘贴、跨系统复制粘贴
注:本文为 “ Vim 中鼠标右键粘贴、跨系统复制粘贴问题解决方案” 相关文章合辑。 未整理去重。 Linux 入门:vim 鼠标不能右键粘贴、跨系统复制粘贴 foryouslgme 发布时间 2016 - 09 - 28 10:24:16 Vim 基础 命令模式(command - mode&…...
仿12306项目(4)
基本预定车票功能的开发 对于乘客购票来说,需要有每一个车次的余票信息,展示给乘客,供乘客选择,因此首个功能是余票的初始化,之后是余票查询,这两个都是控台端。对于会员端的购票,需要有余票查询…...
调研:如何实现智能分析助手(Agent)(AutoCoder、FastGPT、AutoGen、DataCopilot)
文章目录 调研:如何实现智能分析助手(Agent)(AutoCoder、FastGPT、AutoGen、DataCopilot)一、交互流程二、数据流程三、架构分类四、开源产品4.1 AutoCoder(知识库变体)4.2 FastGPT(…...
爬虫逆向:脱壳工具Youpk的使用详解
更多内容请见: 爬虫和逆向教程-专栏介绍和目录 文章目录 1. Youpk 简介1.1 Youpk介绍1.2 Youpk支持场景1.3 Youpk基本流程1.4 使用 Youpk 脱壳步骤1.5 常用的脱壳工具对比2. Youpk 的安装与使用2.1 安装 Youpk2.2 使用 Youpk 脱壳3. 脱壳后的 Dex 文件分析3.1 使用 JADX 反编译…...
Java 大视界 -- Java 大数据在智能政务公共服务资源优化配置中的应用(118)
💖亲爱的朋友们,热烈欢迎来到 青云交的博客!能与诸位在此相逢,我倍感荣幸。在这飞速更迭的时代,我们都渴望一方心灵净土,而 我的博客 正是这样温暖的所在。这里为你呈上趣味与实用兼具的知识,也…...
Java停车平台高并发抢锁技术方案设计 - 慧停宝开源停车管理平台
Java停车平台高并发抢锁技术方案设计 一、业务场景特征 瞬时流量峰值 早晚高峰时段(07:30-09:00, 17:30-19:00)请求量激增10倍热门商圈停车场每秒并发请求可达5000 QPS 资源竞争特性 单个车位被多人同时抢占(超卖风险)用户操作链…...
【论文笔记】Attentive Eraser
标题:Attentive Eraser: Unleashing Diffusion Model’s Object Removal Potential via Self-Attention Redirection Guidance Source:https://arxiv.org/pdf/2412.12974 收录:AAAI 25 作者单位:浙工商,字节&#…...
Android Flow操作符分类
Flow操作符分类...
Cursor + IDEA 双开极速交互
相信很多开发者朋友应该和我一样吧,都是Cursor和IDEA双开的开发模式:在Cursor中快速编写和生成代码,然后在IDEA中进行调试和优化 在这个双开模式的开发过程中,我就遇到一个说大不大说小不小的问题: 得在两个编辑器之间来回切换查…...
图像识别-手写数字识别项目
训练模型: 实现神经网络实例 准备数据 导入torchvision.transforms模块,它提供了许多常用的数据预处理操作,如裁剪、旋转、归一化等。 从torch.utils.data模块导入DataLoader类,用于加载数据集并提供批量处理功能。 导入tensorboa…...
推荐几款优秀的PDF转电子画册的软件
当然可以!以下是几款优秀的PDF转电子画册的软件推荐,内容简洁易懂,这些软件都具有易用性和互动性,适合不同需求的用户使用。 ❶ FLBOOK|在线创作平台 支持PDF直接导入生成仿真翻页电子书。提供15主题模板与字体库&a…...
bert模型笔记
1.各预训练模型说明 BERT模型在英文数据集上提供了两种大小的模型,Base和Large。Uncased是意味着输入的词都会转变成小写,cased是意味着输入的词会保存其大写(在命名实体识别等项目上需要)。Multilingual是支持多语言的࿰…...
利用 ArcGIS Pro 快速统计省域各市道路长度的实操指南
在地理信息分析与处理的工作中,ArcGIS Pro 是一款功能强大的 GIS 软件,它能够帮助我们高效地完成各种复杂的空间数据分析任务。 现在,就让我们一起深入学习如何借助 ArcGIS Pro 来统计省下面各市的道路长度,这一技能在城市规划、…...
数据库系统概论(一)详细介绍数据库与基本概念
数据库系统概论(一)介绍数据库与基本概念 前言一、什么数据库1.数据库的基本概念2.数据库的特点 二、数据库的基本概念1. 数据2. 数据库3.数据库管理系统4.数据库系统 三、数据管理技术的产生和发展四、数据库系统的特点1.数据结构化2.数据共享性3.数据冗…...
数字IC后端实现教程| Clock Gating相关clock tree案例解析
今天小编给大家分享几个跟时钟树综合,clock tree相关的典型问题。 数字IC后端设计实现之分段长clock tree经典案例 Q1:星主好,下面的图是通过duplicate icg来解setup违例的示意图。我没看懂这个 duplicate操作在cts阶段是怎么实现的,用什么…...