Unity-2018-着色器和特效秘籍-全-
Unity 2018 着色器和特效秘籍(全)
原文:
zh.annas-archive.org/md5/098f0d13cb44fa6ce9a7637097151ffe译者:飞龙
前言
《Unity 2018 着色器和效果食谱》 是您熟悉在 Unity 2018 中创建着色器和后处理效果的指南。您将从起点开始,探索后处理堆栈,看看您可以使用着色器以不编写任何脚本的方式影响您所看到的内容的几种可能方法。之后,我们将学习如何从头开始创建着色器,从创建最基本的着色器开始,了解着色器代码的结构。这些基础知识将为您在每一章中进一步学习提供手段,例如学习体积爆炸和毛发着色等高级技术。我们还探讨了新添加的着色器图编辑器,看看您如何可以通过拖放界面创建着色器!本书的这一版专为 Unity 2018 编写,将帮助您掌握基于物理的渲染和全局照明,尽可能接近照片级真实感。
每章结束时,您将获得新的技能,这将提高您着色的质量,甚至使您的着色器编写过程更加高效。这些章节已经定制,以便您可以跳入每个部分,从初学者到专家学习特定的技能。对于那些刚开始在 Unity 中编写着色器的人来说,您可以逐章学习,一次学习一章,以构建您的知识。无论哪种方式,您都将学习使现代游戏看起来如此之美的技术。
完成本书后,您将拥有一套可以在您的 Unity 3D 游戏中使用,并了解如何添加到它们中,实现新效果,并解决性能需求的着色器。那么,让我们开始吧!
本书面向的对象
《Unity 着色器和效果食谱》 是为想要在 Unity 2018 中创建第一个着色器或通过添加专业的后处理效果将游戏提升到全新水平的开发者编写的。需要具备扎实的 Unity 理解能力。
本书涵盖的内容
第一章,后处理堆栈,向读者介绍了后处理堆栈,这将使用户能够在不编写任何额外脚本的情况下调整游戏的外观。
第二章,创建您的第一个着色器,向您介绍了 Unity 中着色器编码的世界。您将构建一些基本的着色器,并学习如何在您的着色器中引入可调整的属性,使它们更具交互性。
第三章,表面着色器和纹理映射,涵盖了您可以使用表面着色器实现的最常见和有用的技术,包括如何使用纹理和法线图为您模型建模。
第四章, 理解光照模型,深入解释了着色器如何模拟光的行为。本章教你如何创建用于模拟特殊效果的自定义光照模型,例如卡通着色。
第五章, Unity 5 中的基于物理的渲染,展示了基于物理的渲染是 Unity 5 用于将现实感带入游戏的标准技术。本章解释了如何通过掌握透明度、反射表面和全局照明来充分利用它。
第六章, 顶点函数,教你如何使用着色器来改变物体的几何形状。本章介绍了顶点修改器,并使用它们将体积爆炸、雪着色器和其他效果栩栩如生地呈现出来。
第七章, 片段着色器和抓取通道,解释了如何使用抓取通道来制作模拟半透明材料产生的变形的材料。
第八章, 移动着色器调整,帮助你优化着色器,以充分利用任何设备。
第九章, 使用 Unity 渲染纹理的屏幕效果,展示了如何创建特殊效果和视觉,这些效果在其他情况下是无法实现的。
第十章, 游戏玩法和屏幕效果,介绍了如何使用后处理效果来补充你的游戏玩法,例如模拟夜视效果。
第十一章, 高级着色技术,介绍了本书中最先进的技术,如毛发着色和热图渲染。
第十二章, 着色器图,解释了如何设置项目以使用 Unity 新添加的着色器图编辑器。我们涵盖了如何创建简单的着色器图,如何公开属性,以及如何通过代码使用发光高亮系统与着色器图交互。
为了充分利用本书
预期读者具备使用 Unity 的经验以及一些脚本编写经验(C#或 JavaScript 均可)。本书以 Unity 2018.1.0f2 编写,但应可通过一些小调整与未来版本的引擎兼容。
下载示例代码文件
你可以从www.packtpub.com的账户下载本书的示例代码文件。如果你在其他地方购买了这本书,你可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给你。
你可以通过以下步骤下载代码文件:
- 
在www.packtpub.com登录或注册。 
- 
选择 SUPPORT 选项卡。 
- 
点击代码下载与勘误。 
- 
在搜索框中输入书籍名称,并遵循屏幕上的说明。 
文件下载完成后,请确保您使用最新版本解压或提取文件夹:
- 
WinRAR/7-Zip for Windows 
- 
Zipeg/iZip/UnRarX for Mac 
- 
7-Zip/PeaZip for Linux 
书籍的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Unity-2018-Shaders-and-Effects-Cookbook-Third-Edition。如果代码有更新,它将在现有的 GitHub 仓库中更新。
我们还有来自我们丰富的书籍和视频目录中的其他代码包,可在github.com/PacktPublishing/找到。查看它们!
下载彩色图像
我们还提供了一份包含本书中使用的截图/图表的彩色图像的 PDF 文件。您可以从这里下载:www.packtpub.com/sites/default/files/downloads/Unity2018ShadersandEffectsCookbookThirdEdition_ColorImages.pdf。
使用的约定
本书使用了多种文本约定。
CodeInText:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“Unity 包是一个包含各种Assets的单个文件,这些Assets可以在 Unity 中以类似.zip文件的方式使用。”
代码块设置为如下:
Properties 
{
  _MainTex("Texture", 2D) = "white" 
}
当我们希望将您的注意力引向代码块中的特定部分时,相关的行或项目将以粗体显示:
void surf (Input IN, inout SurfaceOutputStandard o) {
  // Use the tint provided as the base color for the material
  o.Albedo = _MainTint;
  // Get the normal data out of the normal map texture 
  // using the UnpackNormal function 
  float3 normalMap = UnpackNormal(tex2D(_NormalTex, 
    IN.uv_NormalTex)); 
 normalMap.x *= _NormalMapIntensity; 
 normalMap.y *= _NormalMapIntensity; 
 // Apply the new normal to the lighting model 
 o.Normal = normalize(normalMap.rgb); 
}
粗体:表示新术语、重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词在文本中显示如下。以下是一个示例:“要最终烘焙灯光,请通过转到 Window | Lighting | Settings 打开 Lighting 窗口。一旦到达那里,选择 Global Maps 选项卡。”
警告或重要注意事项看起来像这样。
小贴士和技巧看起来像这样。
部分
在本书中,您将找到一些频繁出现的标题(准备就绪、如何操作...、如何操作...、还有更多...和相关内容)。
为了清楚地说明如何完成食谱,请按以下方式使用这些部分:
准备就绪
本节将向您介绍在食谱中可以期待的内容,并描述如何设置任何软件或任何为食谱所需的初步设置。
如何操作...
本节包含遵循食谱所需的步骤。
如何操作...
本节通常包含对上一节发生情况的详细说明。
还有更多…
本节包含有关食谱的附加信息,以便您对食谱有更深入的了解。
相关内容
本节提供了链接到其他对食谱有用的信息。
联系我们
我们欢迎读者的反馈。
一般反馈:请发送邮件至 feedback@packtpub.com 并在邮件主题中提及书名。如果您对本书的任何方面有疑问,请通过 questions@packtpub.com 发送邮件给我们。
勘误:尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在本书中发现错误,我们将不胜感激,如果您能向我们报告这一错误。请访问 www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入详细信息。
盗版:如果您在互联网上发现任何形式的我们作品的非法副本,我们将不胜感激,如果您能提供位置地址或网站名称。请通过 copyright@packtpub.com 联系我们,并提供材料的链接。
如果您有兴趣成为作者:如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问 authors.packtpub.com。
评论
留下评论。一旦您阅读并使用过本书,为何不在您购买书籍的网站上留下评论呢?潜在读者可以看到并使用您的客观意见来做出购买决定,Packt 可以了解您对我们产品的看法,我们的作者也可以看到他们对书籍的反馈。谢谢!
如需更多关于 Packt 的信息,请访问 packtpub.com。
第一章:后期处理堆栈
在本章中,你将学习以下食谱:
- 
安装后期处理堆栈 
- 
使用颗粒、渐晕和抗锯齿获得电影般的视觉效果 
- 
使用辉光和抗锯齿模仿现实生活 
- 
使用色彩分级设定氛围 
- 
使用雾气创建恐怖游戏的外观 
简介
编写自己的着色器和效果以精细调整你的项目,使其看起来正是你想要的,这是我们将在本书的大部分内容中探讨的。然而,指出 Unity 已经内置了一些预构建的方式来获取用户喜欢通过后期处理堆栈使用的一些更常见的效果,这也是很好的。
对于只想快速启动的人来说,后期处理堆栈可以是一个极好的方式,让你在不编写任何额外代码的情况下调整游戏的外观。使用后期处理堆栈还可以帮助你了解着色器能做什么以及它们如何改进你的游戏项目,因为幕后,后期处理堆栈本身就是一个应用于屏幕的着色器,恰当地称为屏幕着色器。
安装后期处理堆栈
在我们能够使用后期处理堆栈之前,我们必须首先从新引入的包管理器中获取它。Unity 包是一个包含各种资产的单个文件,这些资产可以在 Unity 中以类似.zip 文件的方式使用。以前,Unity 使用资产商店与用户共享这些文件,但随着时间的推移,包管理器被添加以使用户能够轻松地从 Unity 获取免费内容。我们实际上将在第十二章 Shader Graph 中再次使用包管理器,但现在我们将使用它来安装它所包含的后期处理包。
准备工作
要开始这个食谱,你需要运行 Unity 并创建一个新的项目。本章还要求你有一个工作环境。本书提供的代码文件将包含一个基本的场景和内容,用于创建 Unity 的标准资产场景。
打开项目浏览器中的Asset | Chapter 01 | Scenes文件夹内的Chapter 1  | Starting Point场景。如果一切顺利,你应该在游戏标签页中看到如下内容:

这是一个简单的环境,它将使我们能够轻松地看到在后期处理效果中做出的更改如何修改屏幕上绘制的内容。
如果你对学习如何创建所使用的环境感兴趣,请查看我的上一本书,Unity 5.x 游戏开发蓝图,也由 Packt Publishing 出版。
如何操作...
开始:
- 通过前往窗口 | 包管理器(或按Ctrl + 9)打开包管理器:

- 从列表视图,点击“全部”按钮以显示所有可能的包列表。一旦列表填充了所有选项,选择“后处理”选项:

- 从那里,在菜单的右上角,点击安装 2.0.7-preview 按钮。可能需要等待一段时间,直到它完成内容的下载。一旦完成,你应该会返回到“在项目中选择”界面,现在你会在列表中看到添加了“后处理”:

- 关闭“包”标签页,返回到场景窗口查看级别。然后,从层次窗口,我们需要选择带有我们的相机组件的对象,因为后处理堆栈需要知道我们想要修改哪个屏幕。如果你使用自己的项目,你可以选择默认 Unity 场景中附带的主相机对象,但正在使用的示例项目中,相机位于FPSController对象的子对象中。要选择它,点击名称旁边的箭头以展开对象的子对象,然后选择FirstPersonCharacter对象:

此对象上有相机组件,负责在游戏开始时将所看到的内容绘制到游戏标签页。
你可以在 Hierarchy 标签页中双击一个游戏对象,从 Scene 标签页将其相机缩放到其位置。这使得在大型游戏级别中查找事物变得非常容易。
- 在对象被选中并且我们的相机组件附加到它之后,接下来我们需要通过进入 Component | Rendering | Post-process Layer 来向对象添加后处理行为组件:

- 
添加后,从 Inspector 标签页向下滚动到 Post Process Layer(脚本)组件,并在 Layer 下,将下拉菜单更改为 PostProcessing。
- 
这告诉组件我们想要在屏幕上绘制哪些对象。在设置此属性时,对象必须将其 Layer 属性设置为 PostProcessing才能被看到。
- 
要创建后处理体积,转到菜单并选择 GameObject | 3D Object | Post Process Volume。从那里,转到 Inspector 标签页,将 Layer 属性更改为 PostProcessing。最后,为了便于工作,将 Position 更改为0,0,0,并在 Post Process Volume 组件下,勾选 Is Global 属性:

注意,体积有一个 Profile 属性。此属性将包含有关我们希望如何修改屏幕的信息。通过勾选 Is Global,我们表示这些信息应该始终绘制在对象上。通过取消勾选它,效果将仅在从放置体积的一定距离处可见。根据游戏的不同,这可能允许你极大地改变游戏在某些区域的外观,但我们目前只关心获得视觉效果。
使用颗粒、渐晕和景深获得电影般的视觉效果
现在我们已经安装了后处理堆栈,我们可以创建第一个后处理体积。新的后处理堆栈依赖于使用描述如何绘制的体积,无论是全局还是特定区域。
人们喜欢项目的外观之一是电影风格。这在诸如《未知的》系列和《侠盗猎车手 V》等标题中相当常用。它也在《左 4 死》系列中被非常有效地使用,因为它的创作者试图模仿基于游戏的 B 级僵尸电影。
准备工作
确保你在开始这个之前已经完成了“安装后处理堆栈”配方。
如何操作...
- 我们首先通过在项目窗口中的Assets|第一章文件夹内右键单击并选择创建 | 后处理配置文件来创建一个新的后处理配置文件。一旦选择,它将允许我们重命名项目。继续并将名称设置为FilmicProfile:

如果你没有正确输入名称,你可以通过在项目选项卡中点击名称然后再次点击来重命名项目。
- 
一旦创建,你应该注意到,当选择时,检查器窗口现在将包含一个显示“添加效果...”(如前图所示)的按钮,这将允许我们增强通常绘制在屏幕上的内容。 
- 
从层次结构选项卡中再次选择后处理体积对象,然后从检查器选项卡转到后处理体积组件,并将“配置文件”属性分配给刚刚创建的 FilmicProfile:

注意,一旦设置了配置文件,添加效果...按钮也会在这里显示。我们可以在任何地方使用它,并且更改将被保存在文件中。
- 要开始,点击“添加效果...”按钮并选择 Unity | 粒子选项。默认情况下,你只会看到带有勾选的粒子选项,因此点击箭头以展开其内容:

默认情况下,你会看到所有内容都是灰色的。为了使其影响任何内容,你必须点击左侧的复选框。你可以通过按屏幕上的全部或无按钮快速打开或关闭它们。
- 在我们的情况下,勾选强度选项并将其设置为0.2。然后,勾选大小属性并将其设置为0.3。之后,切换到游戏选项卡以查看我们的调整所产生的影响:

- 你会注意到屏幕比之前模糊得多。将强度降低到0.2,大小降低到0.3,并取消选中彩色选项。
与用户在 Unity 中通常的工作方式不同,由于后处理配置文件已归档,你可以在玩游戏时修改它们,停止游戏后,值仍然被保存。这可以用于调整值以实现你想要的精确外观。
- 我们接下来想要调整的属性是 Vignette 属性。注意屏幕周围的变暗边缘。点击添加效果...并选择 Unity | Vignette。打开属性并将强度设置为0.5,平滑度设置为0.35:

- 接下来,再次选择添加效果...,这次选择 Unity | 景深。勾选景深选项。一开始可能难以看到变化,但将焦点距离设置为6,焦距设置为80,你应该会注意到背景前的草地和远处的山现在变模糊了:

- 现在,如果我们进入游戏本身,你应该能看到我们的影视风格正在发挥作用:

影视风格的最终效果
有了这些,我们现在有一个场景看起来比一开始更像电影了!
它是如何工作的...
每次我们将效果添加到后处理体积中,我们都是在覆盖原本应该显示在屏幕上的内容。
如果你曾经去过仍然使用胶片的电影院,你可能注意到了在电影播放时胶片上会有一些小颗粒。颗粒效果模拟了这种胶片颗粒,随着电影的播放,效果变得更加明显。这通常用于恐怖游戏,以模糊玩家的视野。
关于颗粒效果的信息,请查看:github.com/Unity-Technologies/PostProcessing/wiki/Grain.
在电影界,晕影可能是由于使用了错误的镜头类型来达到你想要的效果,或者是因为你拍摄的画面宽高比不正确。在游戏开发中,我们通常使用晕影来产生戏剧效果,或者通过暗化屏幕边缘并/或降低其饱和度来让玩家专注于屏幕中心。
关于光晕效果的信息,请查看:github.com/Unity-Technologies/PostProcessing/wiki/Vignette.
景深设置基本上决定了什么会模糊,什么不会。想法是让重要的物品保持清晰,而背景中的物品则不清晰。
关于景深效果的信息,请查看:github.com/Unity-Technologies/PostProcessing/wiki/Depth-of-Field.
使用光晕和抗锯齿模拟真实生活
光晕光学效果旨在模仿现实世界中相机的成像效果,在光线区域内的物品会沿着边缘发光,从而压倒相机。光晕效果非常独特,你很可能在游戏中魔法或天堂般的区域看到过它。
准备工作
在开始这一步之前,请确保你已经完成了安装 Post Processing Stack的步骤。
如何实现...
- 
我们首先通过在项目窗口中的 Assets文件夹内右键单击并选择创建 | 后处理配置文件来创建一个新的后处理配置文件。一旦选择,它将允许我们重命名项目。继续并将名称设置为BloomProfile。
- 
选择 后处理体积对象,然后从检查器窗口中转到后处理体积组件,并将配置文件属性分配给刚刚创建的BloomProfile。
- 
之后,选择游戏标签页,如果尚未选择,以查看我们将在以下步骤中进行的更改的结果。 
- 
选择添加效果...按钮,选择 Unity | Bloom。一旦效果被添加到后处理体积组件的覆盖部分,选择箭头打开其属性。检查强度属性并将其设置为 3。之后,检查并设置阈值到0.75,软膝盖到0.1,半径到3:

- 接下来,选择带有后处理图层组件的对象(在示例中,是FPSController|FirstPersonCharacter对象),然后从检查器选项卡向下滚动到后处理图层脚本。从那里,将抗锯齿属性下拉菜单更改为快速近似抗锯齿:

- 之后,保存你的场景并点击播放按钮来检查你的项目:

使用 bloom 和抗锯齿的最终结果
它是如何工作的...
如前所述,bloom 会使明亮的东西更加明亮,同时为较亮区域添加光晕。在这个配方中,你可能注意到路径比之前要亮得多。这可以用来确保玩家会沿着路径到达游戏下一部分。
想了解更多关于 bloom 的信息,请查看:github.com/Unity-Technologies/PostProcessing/wiki/Bloom.
抗锯齿试图减少锯齿的出现,这是屏幕上线条出现锯齿状效果的原因。这通常是因为玩家使用的显示设备分辨率不够高,无法正确显示。抗锯齿将通过与附近的线条组合颜色来消除它们的突出,但代价是游戏看起来会模糊。
想了解更多关于抗锯齿以及每种模式意味着什么的信息,请查看:github.com/Unity-Technologies/PostProcessing/wiki/Anti-aliasing.
使用色彩分级设置氛围
改变场景氛围的最简单方法之一是通过改变场景使用的颜色。最好的例子之一是电影《黑客帝国》系列,其中现实世界总是带有蓝色调,而矩阵的计算机生成世界总是带有绿色调。我们可以通过使用色彩分级在我们的游戏中模仿这一点。
准备工作
在开始此操作之前,确保已经完成了安装后处理堆栈配方。
如何操作...
- 
我们首先通过在项目窗口的 资产文件夹内右键单击并选择创建|后处理配置文件来创建一个新的后处理配置文件。一旦选择,它将允许我们重命名项目。继续并将名称设置为ColorProfile。
- 
选择 后处理体积对象,然后从检查器窗口进入后处理体积组件,将配置文件属性分配给刚刚创建的ColorProfile。
- 
之后,选择 游戏选项卡,如果尚未选择,以查看所做的更改结果。
- 
选择 添加效果...按钮并选择Unity | 色彩分级。
- 
检查 模式属性,将其设置为低定义范围(LDR)。从那里,你会看到许多可以用来调整屏幕上颜色的属性,类似于 Photoshop 的色调/饱和度菜单工作方式。检查温度属性并将其设置为30。之后,将色调偏移属性设置为-20,将饱和度设置为15:

- 在进行更改后,进入游戏查看游戏时的效果:

使用色彩分级后的最终效果
注意之前非常绿色的环境现在变得温暖多了,比之前更黄。
关于色彩分级效果的更多信息,请查看:github.com/Unity-Technologies/PostProcessing/wiki/Color-Grading。
使用雾气创建恐怖游戏外观
最有效地利用后处理堆栈功能的游戏类型之一是恐怖游戏。使用诸如景深来隐藏恐怖物体以及静态来使屏幕更具威胁性,这真的可以帮助你的游戏牢牢地定位在正确的位置,并提供你所追求的氛围。
准备工作
确保在开始此操作之前已经完成了安装后处理堆栈配方。
如何操作...
- 
我们首先通过在项目窗口的 资产文件夹内右键单击并选择创建|后处理配置文件来创建一个新的后处理配置文件。一旦选择,它将允许我们重命名项目。继续并将名称设置为HorrorProfile。
- 
选择 后处理体积对象,然后从检查器窗口进入后处理体积组件,将配置文件属性分配给刚刚创建的HorrorProfile。
- 
与之前的设置不同,尽管如此,雾气设置位于 照明窗口中,可以通过进入窗口|照明来访问。
- 
从那里,滚动到最底部直到到达其他设置选项。一旦到达那里,检查雾气并将其颜色设置为接近天空盒的值。我使用了以下设置: 

如果你从你的图形编辑软件中知道了颜色的十六进制值,你只需在颜色窗口的十六进制颜色属性中输入即可。
- 接下来,将模式改为指数,并将密度改为0.03:

如你所见,它已经比之前更加恐怖了,但仍有更多选项我们可以更改。
- 再次打开“HorrorProfile”,转到“检查器”选项卡。按下“添加效果...”按钮,选择 Unity | 环境遮挡。检查模式选项,并选择“可伸缩环境遮挡”。之后,将强度改为2,并将半径改为20:

- 
最后,光照通常对场景的主题也有很大的影响。如果你正在使用示例地图,请在“层次”选项卡中选择“方向光”对象,然后在“灯光组件”下的“检查器”选项卡中,将强度改为 0.5,然后调整颜色为更暗的色调。(我使用了与步骤 4相同的颜色,十六进制值为5F7684FF。)
- 
保存你的游戏,然后启动它以查看所有更改的效果: 

我们恐怖风格的最终结果
它是如何工作的...
环境遮挡选项将计算应该有额外阴影的区域。由于我们的场景中充满了树木,这将使底部比之前暗得多。
关于环境遮挡效果的更多信息,请查看:github.com/Unity-Technologies/PostProcessing/wiki/Ambient-Occlusion。如果你对查看后处理堆栈的其他选项感兴趣,请查看:github.com/Unity-Technologies/PostProcessing/wiki。
第二章:创建你的第一个着色器
在本章中,你将学习以下配方:
- 
创建基本的标准着色器 
- 
向着色器添加属性 
- 
在表面着色器中使用属性 
简介
本章将介绍当今游戏开发着色器管道中的一些更常见的漫反射技术。让我们想象一个在 3D 环境中被均匀涂成白色的立方体。即使使用的颜色在每个面上都相同,但由于光线来的方向和观察的角度不同,它们都会呈现出不同的白色阴影。通过使用着色器,这种额外的真实感在 3D 图形中得以实现,着色器是主要用于模拟光线工作的特殊程序。一个木制立方体和一个金属立方体可能共享相同的 3D 模型,但使它们看起来不同的正是它们使用的着色器。
本章将向你介绍 Unity 中的着色器编程。如果你对着色器几乎没有或没有先前的经验,那么这一章就是你需要的,以便了解着色器是什么,它们是如何工作的,以及如何自定义它们。到本章结束时,你将学会如何构建执行基本操作的基本着色器。掌握了这些知识,你将能够创建几乎任何表面着色器。
创建基本的标准着色器
在 Unity 中,当我们创建一个游戏对象时,我们通过使用组件来附加额外的功能。实际上,每个游戏对象都需要有一个变换组件;Unity 已经包含了一些组件,当我们编写扩展自MonoBehaviour的脚本时,我们创建自己的组件。
所有属于游戏的对象都包含一些影响其外观和行为的组件。虽然脚本决定了对象应该如何行为,但渲染器决定了它们应该在屏幕上如何显示。Unity 提供了多种渲染器,具体取决于我们试图可视化的对象类型;每个 3D 模型通常都附有一个MeshRenderer组件。一个对象应该只有一个渲染器,但渲染器本身可以包含多个材质。每个材质都是一个单一着色器的包装,是 3D 图形链中的最后一环。这些组件之间的关系可以在以下图中看到:

理解这些组件之间的区别对于理解着色器的工作方式至关重要。
准备工作
要开始这个配方,你需要运行 Unity 并打开一个项目。如前所述,这个食谱中还将包含一个 Unity 项目,因此你也可以使用那个项目,并且只需在逐步执行每个配方时添加你自己的自定义着色器即可。完成这些后,你现在就可以进入实时着色的奇妙世界了!
在我们开始第一个着色器之前,让我们创建一个小的场景供我们使用:
- 
通过导航到文件 | 新建场景来创建一个场景。 
- 
一旦创建了场景,在 Unity 编辑器中通过导航到 GameObject | 3D Objects | Plane 创建一个平面作为地面。接下来,在层次结构标签页中选择对象,然后进入检查器标签页。从那里,右键点击 Transform 组件并选择重置位置选项: 

这将重置对象的定位属性为 0, 0, 0:
- 为了更容易看到我们的着色器应用后的样子,让我们添加一些形状来可视化每个着色器的作用。通过导航到 GameObject | 3D Objects | Sphere 创建一个球体。创建完成后,选择它并进入检查器标签页。接下来,将 Position 改为 0,1,0以使其位于世界原点(在0,0,0)和之前创建的平面上方:

- 一旦创建了球体,再创建两个球体,将它们放置在球体的左侧和右侧,位置分别为 -2,1,0和2,1,0:

- 最后,确认你有一个方向光(应该在层次结构标签页中可见)。如果没有,你可以通过选择 GameObject | Light | Directional Light 来添加一个,以便更容易看到你更改的效果以及你的着色器如何对光线做出反应。
如果你使用的是随烹饪书附带的 Unity 项目,你可以打开“第二章” | “起点”场景,因为它已经设置好了。
如何操作...
在场景生成后,我们可以继续到着色器编写步骤:
- 
在你的 Unity 编辑器的“项目”标签页中,右键点击“第二章”文件夹,然后选择创建 | 文件夹。 
- 
通过右键点击你创建的文件夹并从下拉列表中选择重命名,或者通过选择文件夹并按键盘上的 F2,将其重命名为“着色器”。 
- 
创建另一个文件夹并将其重命名为“材质”。 
- 
右键点击“着色器”文件夹并选择创建 | 着色器 | 标准表面着色器。然后,右键点击“材质”文件夹并选择创建 | 材质。 
- 
将着色器和材质都重命名为 StandardDiffuse。
- 
通过双击文件启动 StandardDiffuse着色器。这将自动为你启动一个脚本编辑器并显示着色器的代码。
你会看到 Unity 已经用一些基本代码填充了我们的着色器。默认情况下,这将为你提供一个接受一个纹理的 Albedo(RGB)属性的着色器。我们将修改这个基础代码,以便你可以学习如何快速开始开发你自己的自定义着色器。
- 现在,让我们为我们的着色器创建一个自定义文件夹,以便从中选择。着色器中的第一行代码是我们必须提供给着色器的自定义描述,这样 Unity 才能在将着色器分配给材质时在着色器下拉列表中使其可用。我们已经将路径重命名为Shader "CookbookShaders/StandardDiffuse",但您可以将其命名为任何您想要的名称,并且可以在任何时候重命名,所以请放心,目前不需要担心任何依赖关系。在脚本编辑器中保存着色器,并返回到 Unity 编辑器。当 Unity 识别到文件已更新时,它将自动编译着色器。此时您的着色器应该看起来是这样的:
Shader "CookbookShaders/StandardDiffuse" {
  Properties {
    _Color ("Color", Color) = (1,1,1,1)
    _MainTex ("Albedo (RGB)", 2D) = "white" {}
    _Glossiness ("Smoothness", Range(0,1)) = 0.5
    _Metallic ("Metallic", Range(0,1)) = 0.0
  }
  SubShader {
    Tags { "RenderType"="Opaque" }
    LOD 200
    CGPROGRAM
    // Physically based Standard lighting model, and enable shadows on all light types
    #pragma surface surf Standard fullforwardshadows
    // Use shader model 3.0 target, to get nicer looking lighting
    #pragma target 3.0
    sampler2D _MainTex;
    struct Input {
      float2 uv_MainTex;
    };
    half _Glossiness;
    half _Metallic;
    fixed4 _Color;
    // Add instancing support for this shader. You need to check 'Enable Instancing' on materials that use the shader.
    // See https://docs.unity3d.com/Manual/GPUInstancing.html for more information about instancing.
    // #pragma instancing_options assumeuniformscaling
    UNITY_INSTANCING_BUFFER_START(Props)
      // put more per-instance properties here
    UNITY_INSTANCING_BUFFER_END(Props)
    void surf (Input IN, inout SurfaceOutputStandard o) {
      // Albedo comes from a texture tinted by color
      fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
      o.Albedo = c.rgb;
      // Metallic and smoothness come from slider variables
      o.Metallic = _Metallic;
      o.Smoothness = _Glossiness;
      o.Alpha = c.a;
    }
    ENDCG
  }
  FallBack "Diffuse"
}
- 
从技术角度讲,这是一个基于基于物理的渲染(PBR)的表面着色器。正如其名所示,这种类型的着色器通过模拟光线击中物体时的物理行为来实现逼真效果。 
- 
在您的着色器创建后,我们需要将其连接到材质。选择我们在步骤 4中创建的名为 StandardDiffuse的材质,并查看“检查器”标签页。从“着色器”下拉列表中选择 CookbookShaders | StandardDiffuse。(如果选择了不同的路径名称,您的着色器路径可能不同。)这将把您的着色器分配给您的材质,并使其准备好分配给对象。
将材质分配给对象,您只需简单地从“项目”标签页点击并拖动您的材质到场景中的对象即可。您还可以将材质拖动到 Unity 编辑器中对象的“检查器”标签页,以便分配它。
以下是一个示例截图:

目前看起来没有什么可看的,但我们的着色器开发环境已经设置好了,我们现在可以开始修改着色器以满足我们的需求。
它是如何工作的...
Unity 已经使您设置着色器环境的工作变得非常简单。这只是一个点击几个按钮的问题,然后您就可以开始了。在表面着色器本身方面,有很多元素在后台工作。Unity 已经对 Cg 着色器语言进行了优化,以便您更高效地编写,通过为您执行大量的重 Cg 代码。表面着色器语言是一种更基于组件的编写着色器的方法。处理自己的纹理坐标和变换矩阵等任务已经为您完成,因此您不必从头开始。在过去,我们不得不创建一个新的着色器并反复重写大量代码。随着您在表面着色器方面经验的增加,您自然会想要探索 Cg 语言的更多底层功能以及 Unity 是如何为您处理所有底层图形 处理单元(GPU)任务的。
Unity 项目中的所有文件都是独立于它们所在的文件夹进行引用的。我们可以在编辑器内部移动着色器和材质,而不用担心破坏任何连接。然而,文件永远不应该从编辑器外部移动,因为 Unity 将无法更新它们的引用。
因此,只需将着色器的路径名称更改为我们选择的名称,我们就在 Unity 环境中实现了基本漫反射着色器,包括灯光和阴影等,只需更改一行代码即可!
还有更多...
内置着色器的源代码通常在 Unity 中是隐藏的。你不能像处理自己的着色器那样从编辑器中打开它。有关查找大量内置 Cg 函数的信息,请转到你的 Unity 安装目录,并导航到Editor | Data | CGIncludes文件夹:

在这个文件夹中,你可以找到 Unity 附带着色器的源代码。随着时间的推移,它们已经发生了很大变化;如果你需要访问不同版本的 Unity 中使用的着色器的源代码,请访问Unity 下载存档(unity3d.com/get-unity/download/archive)。选择正确的版本后,从下拉列表中选择内置着色器,如图下所示:

目前有三个文件值得关注:UnityCG.cginc、Lighting.cginc 和 UnityShaderVariables.cginc。我们当前的着色器正在使用所有这些文件。在第十一章,“高级着色技术”中,我们将深入探讨如何使用 CGInclude 以模块化方式编写着色器代码。
向着色器添加属性
着色器的属性对于着色器管道非常重要,因为它们是艺术家或着色器用户用来分配纹理和调整着色器值的方法。属性允许你在材质的检查器标签页中暴露 GUI 元素,而无需使用单独的编辑器,这提供了调整着色器的视觉方式。在你的 IDE 中打开着色器,查看第二行到第七行的代码块。这被称为脚本的Properties块。目前,它将包含一个名为_MainTex的纹理属性。
如果你查看应用了此着色器的材质,你会在检查器标签页中注意到有一个纹理GUI 元素。我们着色器中的这些代码行正在为我们创建这个 GUI 元素。再次强调,Unity 在编码效率和迭代更改属性所需的时间方面使这个过程非常高效。
准备工作
让我们看看在我们的当前着色器 StandardDiffuse 中它是如何工作的,通过创建我们自己的属性并了解更多关于涉及的语法。对于这个例子,我们将重新调整之前创建的着色器。它将不再使用纹理,而只使用其颜色和一些我们可以直接从检查器选项卡更改的其他属性。首先,复制 StandardDiffuse 着色器。您可以在检查器选项卡中选择它并按 *Ctrl *+ D 来完成此操作。这将创建一个名为 StandardDiffuse 1 的副本。继续将其重命名为 StandardColor。
您可以在着色器的第一行为其提供一个更友好的名称。例如,Shader "CookbookShaders/StandardDiffuse" 告诉 Unity 将此着色器命名为 StandardDiffuse 并将其移动到名为 CookbookShaders 的组中。如果您使用 *Ctrl *+ D 复制一个着色器,您的新文件将具有相同的名称。为了避免混淆,请确保更改每个新着色器的第一行,以便它使用在此和未来的配方中唯一的别名。
如何操作...
一旦 StandardColor 着色器准备就绪,我们就可以开始更改其属性:
- 在脚本的第 一行中,更新名称为以下内容:
Shader "CookbookShaders/Chapter 02/StandardColor" {
下载示例代码
您可以从您在 www.packtpub.com 购买的 Packt 书籍的账户中下载所有示例代码文件。如果您在其他地方购买了这本书,您可以访问 www.packtpub.com/support 并注册以将文件直接通过电子邮件发送给您。
- 在我们的着色器 Properties块中,通过从当前着色器中删除以下代码来移除当前属性:
_MainTex ("Albedo (RGB)", 2D) = "white" {} 
- 由于我们已经移除了一个基本属性,这个着色器将不会编译,直到移除对 _MainTex的其他引用。让我们在SubShader部分中删除这一行:
sampler2D _MainTex; 
- 原始着色器使用 _MainTex为模型着色。让我们通过用以下代码替换surf()函数的第一行来更改这一点:
fixed4 c = _Color; 
正如您在用 C# 和其他编程语言编写代码时可能习惯于使用 float 类型表示浮点值一样,fixed 用于定点值,并且在编写着色器时使用此类型。您也可能看到使用 half 类型,它类似于 float 类型,但占用一半的空间。这有助于节省内存,但在表示上精度较低。我们将在第八章第八部分的使着色器更有效的技术配方中更详细地讨论这一点,移动着色器调整。
关于定点值的更多信息,请查看en.wikipedia.org/wiki/Fixed-point_arithmetic。
fixed4中的4表示颜色是一个包含四个fixed值的单个变量:红色、绿色、蓝色和 alpha。你将在下一章中了解更多关于它是如何工作的以及如何更详细地修改这些值的信息,第三章,表面着色器和纹理映射。
- 当你保存并返回 Unity 时,着色器将进行编译,你会看到现在我们的材质的“检查器”标签中不再有纹理样本。为了完成这个着色器的重新配置,让我们在属性块中添加一个额外的属性并看看会发生什么。输入以下代码:
_AmbientColor ("Ambient Color", Color) = (1,1,1,1) 
- 我们已经在材质的“检查器”标签中添加了另一个颜色样本。现在让我们再添加一个,以便感受我们可以创建的其他类型的属性。将以下代码添加到属性块中:
_MySliderValue ("This is a Slider", Range(0,10)) = 2.5 
- 我们现在创建了一个另一个 GUI 元素,允许我们以视觉方式与我们的着色器交互。这次,我们创建了一个名为 This is a Slider 的滑块,如下面的截图所示:

- 属性允许你创建一种视觉方式来调整着色器,而无需更改着色器代码中的值。下一道菜谱将向你展示这些属性实际上是如何被用来创建一个更有趣的着色器的。
虽然属性属于着色器,但与它们关联的值存储在材质中。相同的着色器可以在许多不同的材质之间安全共享。另一方面,更改材质的属性将影响当前使用它的所有对象的外观。
它是如何工作的...
每个 Unity 着色器都在其代码中寻找一个内置的结构。属性块是 Unity 期望的函数之一。背后的原因是为了给你,着色器程序员,提供一种快速创建与着色器代码直接关联的 GUI 元素的方法。你可以在属性块中声明的这些属性(变量)然后可以在你的着色器代码中使用它们来更改值、颜色和纹理。定义属性的语法如下:

让我们来看看这里底层的运作原理。当你第一次开始编写一个新的属性时,你需要给它一个变量名。变量名将是你的着色器代码将用来从 GUI 元素获取值的名称。这为我们节省了很多时间,因为我们不需要自己设置这个系统。属性的下一个元素是检查器 GUI 名称和属性的类型,这些都在括号内。检查器 GUI 名称是当用户与材质的检查器标签进行交互和调整着色器时将显示的名称。类型是此属性将要控制的数据类型。在 Unity 着色器中,我们可以为属性定义许多类型。以下表格描述了我们可以在我们着色器中拥有的变量类型:
| 表面着色器属性类型 | 描述 | 
|---|---|
| Range(min, max) | 这创建了一个从最小值到最大值的 float属性滑块 | 
| Color | 这将在检查器标签中创建一个颜色样本,并打开一个 color picker = (float,float,float,float) | 
| 2D | 这将创建一个纹理样本,允许用户将纹理拖放到着色器中 | 
| Rect | 这将创建一个非二的幂纹理样本,并且与 2DGUI 元素功能相同 | 
| Cube | 这将在检查器标签中创建一个立方体贴图样本,并允许用户将立方体贴图拖放到着色器中 | 
| float | 这将在检查器标签中创建一个浮点值,但没有滑块 | 
| Vector | 这将创建一个四浮点属性,允许你创建方向或颜色 | 
最后,还有默认值。这简单地将此属性的值设置为你在代码中放置的值。所以,在先前的示例图中,名为_AmbientColor的属性默认值,其类型为Color,被设置为1, 1, 1, 1。由于这是一个期望颜色为RGBA或float4或r, g, b, a = x, y, z, w的Color属性,因此当首次创建时,此Color属性被设置为白色。
参见
这些属性在 Unity 手册中有文档说明,请参阅docs.unity3d.com/Documentation/Components/SL-Properties.html。
在表面着色器中使用属性
现在我们已经创建了一些属性,让我们将它们实际连接到着色器,这样我们就可以将它们用作着色器的调整,并使材质处理更加交互式。我们可以使用材质检查器标签中的属性值,因为我们已经将变量名附加到属性本身,但在着色器代码中,你必须在开始通过变量名调用值之前设置一些事情。
如何做到这一点...
以下步骤显示了如何在表面着色器中使用属性:
- 在上一个示例的基础上继续,让我们创建另一个名为 ParameterExample的着色器。就像之前一样,以与本章中“向着色器添加属性”配方相同的方式移除_MainTex属性:
// Inside the Properties block
_MainTex ("Albedo (RGB)", 2D) = "white" {} 
// Below the CGPROGRAM line
sampler2D _MainTex; 
// Inside of the surf function
fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color; 
- 之后,将 Properties部分更新为以下代码:
Properties {
  _Color ("Color", Color) = (1,1,1,1)
 _AmbientColor ("Ambient Color", Color) = (1,1,1,1) 
 _MySliderValue ("This is a Slider", Range(0,10)) = 2.5 
  _Glossiness ("Smoothness", Range(0,1)) = 0.5
  _Metallic ("Metallic", Range(0,1)) = 0.0
}
- 接下来,在 CGPROGRAM行下面添加以下代码行到着色器中:
float4 _AmbientColor; 
float _MySliderValue; 
- 经过完成步骤 3,我们现在可以使用着色器中的属性值了。让我们通过将 _Color属性的值添加到_AmbientColor属性中,并将这个结果赋给o.Albedo代码行来实现这一点。所以,让我们在surf()函数的着色器中添加以下代码:
void surf (Input IN, inout SurfaceOutputStandard o) {
      // We can then use the properties values in our shader 
      fixed4 c = pow((_Color + _AmbientColor), _MySliderValue); 
      // Albedo comes from property values given from slider and colors
      o.Albedo = c.rgb;
      // Metallic and smoothness come from slider variables
      o.Metallic = _Metallic;
      o.Smoothness = _Glossiness;
      o.Alpha = c.a;
    }
    ENDCG
- 最后,你的着色器应该看起来像以下着色器代码。如果你保存你的着色器并重新进入 Unity,你的着色器将会编译。如果没有错误,你现在将能够通过滑动条值来改变材质的环境光和自发光颜色,以及增加最终颜色的饱和度。非常方便:
Shader "CookbookShaders/Chapter02/ParameterExample" {
  // We define Properties in the properties block 
  Properties {
    _Color ("Color", Color) = (1,1,1,1)
    _Glossiness ("Smoothness", Range(0,1)) = 0.5
    _Metallic ("Metallic", Range(0,1)) = 0.0
  }
  SubShader {
    Tags { "RenderType"="Opaque" }
    LOD 200
    // We need to declare the properties variable type inside of the
    // CGPROGRAM so we can access its value from the properties block.
    CGPROGRAM
    // Physically based Standard lighting model, and enable shadows on all light types
    #pragma surface surf Standard fullforwardshadows
    // Use shader model 3.0 target, to get nicer looking lighting
    #pragma target 3.0
    float4 _AmbientColor; 
    float _MySliderValue; 
    struct Input {
      float2 uv_MainTex;
    };
    half _Glossiness;
    half _Metallic;
    fixed4 _Color;
    // Add instancing support for this shader. You need to check 'Enable Instancing' on materials that use the shader.
    // See https://docs.unity3d.com/Manual/GPUInstancing.html for more information about instancing.
    // #pragma instancing_options assumeuniformscaling
    UNITY_INSTANCING_BUFFER_START(Props)
      // put more per-instance properties here
    UNITY_INSTANCING_BUFFER_END(Props)
    void surf (Input IN, inout SurfaceOutputStandard o) {
      // We can then use the properties values in our shader 
      fixed4 c = pow((_Color + _AmbientColor), _MySliderValue); 
      // Albedo comes from property values given from slider and colors
      o.Albedo = c.rgb;
      // Metallic and smoothness come from slider variables
      o.Metallic = _Metallic;
      o.Smoothness = _Glossiness;
      o.Alpha = c.a;
    }
    ENDCG
  }
  FallBack "Diffuse"
}
pow(arg1, arg2) 函数是一个内置函数,它将执行等价的 math 函数的幂运算。所以,参数 1 是我们想要提升到幂的值,而参数 2 是我们想要提升到的幂。
要了解更多关于 pow() 函数的信息,请查看 Cg 教程。这是一个非常好的免费资源,你可以用它来学习更多关于着色器的知识,并且有一个所有 Cg 着色语言中可用的函数的词汇表,可以在http.developer.nvidia.com/CgTutorial/cg_tutorial_appendix_e.html找到。
以下截图展示了使用我们的属性从材质的“检查器”标签中控制材质颜色和饱和度的结果:

它是如何工作的...
当你在 Properties 块中声明一个新的属性时,你为着色器提供了一种从材质的“检查器”标签中检索调整值的方法。这个值存储在属性的变量名部分。在这种情况下,_AmbientColor、_Color 和 _MySliderValue 是我们存储调整值的变量。
为了让你能够使用 SubShader 块中的值,你需要创建三个具有与属性变量名相同名称的新变量。这自动设置了一个链接,使它们知道它们必须使用相同的数据。此外,它声明了我们想要存储在 SubShader 变量中的数据类型,这在我们在后面的章节中查看优化着色器时将很有用。一旦创建了 SubShader 变量,你就可以在 surf() 函数中使用这些值。在这种情况下,我们想要将 _Color 和 _AmbientColor 变量相加,并将其提升到 _MySliderValue 变量在材质检查器标签中等于的任何幂。绝大多数着色器最初都是标准着色器,并经过修改直到它们符合所需的视觉效果。我们现在为任何需要漫反射组件的表面着色器创建了基础。
材质是资产。这意味着在编辑器中游戏运行时对它们所做的任何更改都是永久的。如果你不小心更改了属性的值,你可以使用 *Ctrl *+ Z 撤销更改。
还有更多...
就像任何其他编程语言一样,Cg 不允许错误。因此,如果你的代码中有拼写错误,你的着色器将无法工作。当这种情况发生时,你的材质将以无阴影的洋红色渲染:

当脚本无法编译时,Unity 会阻止你的游戏导出或执行。相反,着色器中的错误不会阻止你的游戏执行。如果你的某个着色器显示为洋红色,那么是时候调查问题所在了。如果你选择有问题的着色器,你将在其检查器标签中看到一个错误列表:

尽管错误信息显示了引发错误的行,但这很少意味着这就是需要修复的行。前一个屏幕截图中的错误信息是由从“SubShader{”块中删除 sampler2D _MainTex 变量生成的。然而,错误是由尝试访问此类变量的第一行引发的。找到并修复代码中的问题是一个称为 调试 的过程。你应该检查的最常见的错误如下:
- 
缺少的括号。如果你忘记添加花括号来关闭一个部分,编译器可能会在文档的末尾、开头或在新部分中引发错误。 
- 
缺少的分号。这是最常见的错误之一,但幸运的是,这也是最容易发现和修复的。在查看错误定义时,首先检查它上面的行是否有分号。 
- 
在“属性”部分中定义但未与“SubShader{”块中的变量耦合的属性。 
- 
与你在 C# 脚本中可能习惯的相比,Cg 中的浮点值不需要跟一个 f。它是1.0,而不是1.0f。
着色器抛出的错误信息可能会非常误导,尤其是由于它们严格的语法约束。如果你对它们的含义有疑问,最好是上网搜索。Unity 论坛上充满了其他开发者,他们很可能在之前遇到过(并解决了)你的问题。
另请参阅
- 
关于如何掌握表面着色器和它们的属性,更多信息可以在第三章《表面着色器和纹理映射》中找到。 
- 
如果你好奇想看看当着色器发挥其全部潜力时能做什么,可以查看第十一章《高级着色技术》,书中涵盖的一些最先进的技术。 
第三章:表面着色器和纹理映射
在本章中,我们将比上一章更深入地探讨表面着色器。我们将从一个非常简单的哑光材质开始,以全息投影和高级地形混合结束。我们还将看到如何使用纹理来动画化、混合和驱动它们喜欢的任何其他属性。
在本章中,你将学习以下方法:
- 
漫反射着色 
- 
访问和修改打包数组 
- 
向着色器添加纹理 
- 
通过修改 UV 值滚动纹理 
- 
使用法线贴图创建着色器 
- 
创建透明材质 
- 
创建全息着色器 
- 
打包和混合纹理 
- 
在你的地形周围创建一个圆 
简介
表面着色器在第二章“创建你的第一个着色器”中引入,作为 Unity 中使用的着色器的主要类型。本章将详细介绍这些着色器的实际内容和它们的工作原理。一般来说,每个表面着色器有两个基本步骤。首先,你必须指定你想要描述的材料的某些物理属性,例如其漫反射颜色、平滑度和透明度。这些属性在名为surface function的函数中初始化,并存储在名为SurfaceOutput的结构中。其次,SurfaceOutput被传递给一个光照模型。这是一个特殊的函数,它还会接收场景中附近灯光的信息。这两个参数随后被用来计算模型每个像素的最终颜色。光照函数是着色器真正计算的地方,因为它是确定光线接触材料时应如何行为的代码片段。
以下图表大致总结了表面着色器的工作原理。自定义光照模型将在第四章“理解光照模型”中探讨,而第六章“顶点函数”将专注于顶点修改器:

漫反射着色
在我们开始纹理映射之旅之前,了解漫反射材料的工作原理非常重要。某些物体可能具有均匀的颜色和光滑的表面,但不够光滑以在反射光中发光。这些哑光材料最好用漫反射着色器来表示。虽然,在现实世界中纯漫反射材料不存在,但漫反射着色器的实现相对便宜,并且在具有低多边形美学的游戏中大量应用,因此它们值得学习。
你可以通过几种方式创建自己的漫反射着色器。一种快速的方法是从 Unity 的标准表面着色器开始,并编辑它以删除任何额外的纹理信息。
准备工作
在开始此配方之前,您应该已经创建了一个名为SimpleDiffuse的标准表面着色器。有关创建标准表面着色器的说明,请参阅第二章中的创建您的第一个着色器配方,如果您还没有这样做的话。
如何做到这一点...
打开您创建的SimpleDiffuse着色器,并做出以下更改:
- 在属性部分,删除除_Color之外的所有变量:
  Properties 
  {
    _Color ("Color", Color) = (1,1,1,1)
  }
- 
从 SubShader{}部分中删除_MainTex、_Glossiness和_Metallic变量。您不应该删除对uv_MainTex的引用,因为 Cg 不允许Input结构为空。该值将被简单地忽略。
- 
此外,删除 UNITY_INSTANCING_BUFFER_START/END宏及其相关的注释。
- 
删除 surf()函数的内容,并替换为以下内容:
void surf (Input IN, inout SurfaceOutputStandard o) 
{
  o.Albedo = _Color.rgb; 
}
- 您的着色器应该看起来如下:
Shader "CookbookShaders/Chapter03/SimpleDiffuse" {
  Properties 
  {
    _Color ("Color", Color) = (1,1,1,1)
  }
  SubShader 
  {
    Tags { "RenderType"="Opaque" }
    LOD 200
    CGPROGRAM
    // Physically based Standard lighting model, and enable shadows on all light types
    #pragma surface surf Standard fullforwardshadows
    // Use shader model 3.0 target, to get nicer looking lighting
    #pragma target 3.0
    struct Input 
    {
      float2 uv_MainTex;
    };
    fixed4 _Color;
    void surf (Input IN, inout SurfaceOutputStandard o) 
    {
      o.Albedo = _Color.rgb; 
    }
    ENDCG
  }
  FallBack "Diffuse"
}
以下CGPROGRAM的两行实际上是同一行,由于书籍的大小而被截断。
- 由于这个着色器已经适配了 Standard Shader,它将使用基于物理的渲染来模拟光线在模型上的行为。
如果您试图实现非真实感的外观,您可以更改第一个#pragma指令,使其使用Lambert而不是Standard。如果您这样做,还应该将surf函数的SurfaceOutputStandard参数替换为SurfaceOutput。有关此信息和 Unity 支持的其他光照模型的信息,Jordan Stevens 编写了一篇非常好的文章,您可以通过以下链接查看:www.jordanstevenstechart.com/lighting-models
- 
保存着色器,然后返回 Unity。使用与第二章中创建基本标准表面着色器配方相同的指令,创建一个名为 SimpleDiffuseMat的新材质,并将我们新创建的着色器应用到它上。通过在检查器窗口中选择并单击颜色属性旁边的窗口,将颜色更改为不同的颜色,例如红色。
- 
然后,进入本书示例代码的 Models文件夹,通过从项目窗口拖放到层次窗口中,将兔子对象拖放到我们的场景中。从那里,将SimpleDiffuseMat材质分配给对象:

- 您可以在层次窗口中双击一个对象,以便将相机居中到所选对象。
它是如何工作的...
着色器允许你通过它们的 SurfaceOutput 将你的材料渲染属性传达给光照模型的方式。它基本上是围绕当前光照模型所需的所有参数的一个包装。不同光照模型有不同的 SurfaceOutput 结构体,这不会让你感到惊讶。以下表格显示了在 Unity 中使用的三个主要输出结构体以及它们的使用方法:
| 着色器类型 | 标准 | 基于物理的光照模型 | 
|---|---|---|
| 扩散 | 任何表面着色器 SurfaceOutput | 标准 SurfaceOutputStandard | 
| 镜面 | 任何表面着色器 SurfaceOutput | 标准(镜面设置) SurfaceOutputStandardSpecular | 
SurfaceOutput 结构体具有以下属性:
- 
fixed3 Albedo;: 这是材料的扩散颜色
- 
fixed3 Normal;: 这是切线空间中的法线,如果写入的话
- 
fixed3 Emission;: 这是材料发出的光的颜色(在标准着色器中此属性被声明为half3)
- 
fixed Alpha;: 这是材料的透明度
- 
half Specular;: 这是镜面功率,范围从0到1
- 
fixed Gloss;: 这是镜面强度的固定值
SurfaceOutputStandard 结构体具有以下属性:
- 
fixed3 Albedo;: 这是材料的基色(无论是扩散还是镜面)
- 
fixed3 Normal;
- 
half3 Emission;: 这个属性被声明为half3,而在SurfaceOutput中定义为fixed3
- 
fixed Alpha;
- 
half Occlusion;: 这是遮挡(默认1)
- 
half Smoothness;: 这是平滑度(0= 粗糙,1= 平滑)
- 
half Metallic;:0= 非金属,1= 金属
SurfaceOutputStandardSpecular 结构体具有以下属性:
- 
fixed3 Albedo;
- 
fixed3 Normal;
- 
half3 Emission;
- 
fixed Alpha;
- 
half Occlusion;
- 
half Smoothness;
- 
fixed3 Specular;: 这是镜面颜色。这与SurfaceOutput中的Specular属性非常不同,因为它允许你指定一个颜色而不是单个值。
正确初始化 SurfaceOutput 为正确的值是正确使用表面着色器的问题。
关于创建表面着色器的更多信息,请查看以下链接: docs.unity3d.com/Manual/SL-SurfaceShaders.html
访问和修改打包数组
简单来说,着色器内部的代码必须至少执行屏幕上每个像素一次。这就是为什么 GPU 高度优化了并行计算;它们可以同时执行多个进程。这种理念在 Cg 中可用的标准类型变量和运算符中也很明显。理解它们是至关重要的,不仅是为了正确使用着色器,也是为了编写高度优化的着色器。
如何做到这一点...
Cg 中有两种类型的变量:单个值和压缩数组。后者可以通过它们的类型以数字结尾来识别,例如float3或int4。正如它们的名称所暗示的,这些类型的变量类似于结构体,这意味着它们各自包含几个单个值。Cg 称它们为压缩数组,尽管它们在传统意义上并不完全是数组。
压缩数组的元素可以像正常结构体一样访问。它们通常被称为x、y、z和w。然而,Cg 还为你提供了它们的另一个别名,即r、g、b和a。尽管使用x或r之间没有区别,但它对读者来说可能有很大的影响。实际上,着色器编码通常涉及位置和颜色的计算。你可能在标准着色器中见过这种情况:
o.Alpha = _Color.a; 
在这里,o是一个结构体,_Color是一个压缩数组。这也是为什么 Cg 禁止混合使用这两种语法:你不能使用_Color.xgz。
压缩数组还有一个重要的特性,在 C#中没有等效功能:swizzling。Cg 允许在单行内对压缩数组中的元素进行寻址和重新排序。再次强调,这也在标准着色器中体现出来:
o.Albedo = _Color.rgb; 
Albedo是fixed3,这意味着它包含三个fixed类型的值。然而,_Color被定义为fixed4类型。直接赋值会导致编译器错误,因为_Color比Albedo大。在 C#中这样做的方式如下:
o.Albedo.r = _Color.r; 
o.Albedo.g = _Color.g; 
o.Albedo.b = _Color.b; 
然而,在 Cg 中它可以被压缩:
o.Albedo = _Color.rgb; 
Cg 还允许重新排序元素,例如,使用_Color.bgr来交换红色和蓝色通道。
最后,当单个值赋给压缩数组时,它被复制到所有字段中:
o.Albedo = 0; // Black =(0,0,0) 
o.Albedo = 1; // White =(1,1,1) 
这被称为模糊化。
Swizzling 也可以用于表达式的左侧,允许只覆盖压缩数组的某些组件:
o.Albedo.rg = _Color.rg; 
在这种情况下,它被称为遮罩。
更多...
当 swizzling 应用于压缩矩阵时,它真正显示出其全部潜力。Cg 允许使用float4x4这样的类型,它代表一个有四行四列的浮点矩阵。你可以使用_mRC表示法访问矩阵的单个元素,其中R是行,C是列:
float4x4 matrix; 
// ... 
float first = matrix._m00; 
float last = matrix._m33; 
_mRC表示法也可以链式使用:
float4 diagonal = matrix._m00_m11_m22_m33; 
可以使用方括号选择整行:
float4 firstRow = matrix[0]; 
// Equivalent to 
float4 firstRow = matrix._m00_m01_m02_m03; 
参见
- 
除了更容易编写之外,swizzling、smearing 和 masking 属性还有性能优势。 
- 
然而,不当使用 swizzling 可能会使你的代码在第一眼看起来更难以理解,也可能使编译器更难自动优化你的代码。 
- 
压缩数组是 Cg 最吸引人的特性之一。你可以在这里了解更多信息: http.developer.nvidia.com/CgTutorial/cg_tutorial_chapter02.html
向着色器添加纹理
纹理可以迅速使我们的着色器变得生动,以实现非常逼真的效果。为了有效地使用纹理,我们需要了解二维图像是如何映射到三维模型的。这个过程称为纹理映射,并且需要对我们要使用的着色器和 3D 模型进行一些工作。实际上,模型是由三角形组成的,通常被称为多边形;模型上的每个顶点都可以存储着色器可以访问和使用的数据,以确定要绘制的内容。
存储在顶点中最重要的信息之一是 UV 数据。它由两个坐标组成,U 和 V,范围从 0 到 1。它们代表将要映射到顶点的 2D 图像中像素的 XY 位置。UV 数据仅存在于顶点中;当三角形的内部点需要纹理映射时,GPU 会插值最接近的 UV 值,以找到纹理中要使用的正确像素。以下图表显示了如何将 2D 纹理从 3D 模型映射到三角形:

UV 数据存储在 3D 模型中,需要使用建模软件进行编辑。一些模型缺少 UV 组件,因此它们无法支持纹理映射。例如,斯坦福兔子最初并没有提供 UV 组件。
准备工作
对于这个食谱,你需要一个带有 UV 数据和纹理的 3D 模型。在开始之前,它们都需要导入到 Unity 中。你可以通过简单地拖动它们到编辑器中来实现。由于标准着色器默认支持纹理映射,我们将使用它,并详细解释其工作原理。
如何操作...
使用标准着色器为你的模型添加纹理非常简单,如下所示:
- 
在本书提供的本章示例代码中,你可以找到 basicCharacter模型,默认情况下,它已经嵌入 UV 信息,这使得当我们附加材质时,它会使用这些信息来绘制纹理。
- 
通过转到项目标签并选择创建 | 着色器 | 标准表面着色器,创建一个名为 TexturedShader的新标准表面着色器。一旦创建,你可以输入着色器的新名称,然后按 Enter 键。
- 
为了组织起见,打开着色器并将第一行更改为以下内容: 
Shader "CookbookShaders/Chapter03/TexturedShader" {
- 
这将使我们能够找到我们迄今为止为本书使用的组织内部的着色器。 
- 
通过转到项目标签并选择创建 | 材质,创建一个名为 TexturedMaterial的新材质。一旦创建,你可以输入材质的新名称,然后按 Enter 键确认更改。
- 
通过转到检查器标签并点击着色器下拉菜单,然后选择 CookbookShaders/Chapter 03/TexturedShader来将着色器分配给材质:

你也可以通过首先选择材质,然后在项目标签中将其拖动到着色器文件上来实现这一点。
- 选择材质后,将你的纹理拖放到名为 Albedo (RGB)的空白矩形中。如果你缺少某些纹理,本章的示例代码中提供了可以使用的纹理。如果你正确地遵循了所有这些步骤,你的材质检查器标签页应该看起来像这样:

标准着色器知道如何使用其 UV 模型和纹理将 2D 图像映射到 3D 模型,本例中使用的纹理是由 Kenney Vleugels 和 Casper Jorissen 创建的。你可以在Kenney.nl找到这些以及其他许多公共领域游戏资产。
- 要查看 UV 数据在实际中的应用,在示例代码的模型文件夹中,将模型拖放到“层次结构”标签页。一旦到达那里,双击新创建的对象以便放大,以便你可以看到该对象:

- 一旦到达那里,你可以转到“项目”标签页,打开第三章|材料文件夹,并将我们的纹理材质拖放到角色上。请注意,该模型由不同的对象组成,每个对象都提供了在特定位置绘制方向。这意味着你需要将材质应用到模型的每个部分(ArmLeft1、ArmRight1、Body1等);仅尝试将其应用于层次结构的顶层(basicCharacter)将不会起作用:

- 通过更改正在使用的纹理,也可以更改对象的外观。例如,如果我们使用提供的其他纹理(skin_womanAlternative),我们将得到一个看起来非常不同的角色:

这通常在游戏中用于以最小的成本提供不同类型的角色。
它是如何工作的...
当从材质的检查器使用标准着色器时,纹理映射背后的过程对开发者来说是完全透明的。如果我们想了解它是如何工作的,就需要更仔细地查看TexturedShader。从属性部分,我们可以看到Albedo (RGB)纹理实际上在代码中被称为_MainTex:
_MainTex ("Albedo (RGB)", 2D) = "white" {} 
在CGPROGRAM部分,此纹理被定义为sampler2D,这是 2D 纹理的标准类型:
sampler2D _MainTex; 
下面的行显示了一个名为struct的结构。这是表面函数的输入参数,包含一个名为uv_MainTex的打包数组:
struct Input { 
  float2 uv_MainTex; 
}; 
每次调用surf()函数时,Input结构将包含需要渲染的 3D 模型特定点的_MainTex的 UV。标准着色器识别出名称uv_MainTex指的是_MainTex,并自动初始化它。如果你对了解 UV 实际上是如何从 3D 空间映射到 2D 纹理感兴趣,可以查看第五章,理解光照模型。
最后,UV 数据用于在表面函数的第一行采样纹理:
fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color; 
这是通过使用 Cg 的 tex2D() 函数来完成的;它接受一个纹理和 UV,并返回该位置的像素颜色。
U 和 V 坐标从 0 到 1,其中 (0,0) 和 (1,1) 对应于两个相对的角。不同的实现将 UV 与不同的角关联;如果你的纹理恰好出现反转,尝试反转 V 分量。
更多内容...
当您将纹理导入 Unity 时,您正在设置 sampler2D 将使用的一些属性。最重要的是过滤模式,它决定了在采样纹理时颜色如何插值。UV 数据不太可能正好指向像素的中心;在其他所有情况下,您可能想要在最近的像素之间进行插值,以获得更均匀的颜色。以下是一个示例纹理的检查器选项卡的截图:

对于大多数应用来说,双线性提供了既经济又有效的方法来平滑纹理。然而,如果您正在创建 2D 游戏,双线性可能会产生模糊的瓦片。在这种情况下,您可以使用点采样来从纹理采样中移除任何插值。
当从陡峭的角度观察纹理时,纹理采样很可能会产生视觉上不愉快的伪影。您可以通过将 Aniso Level 设置为更高的值来减少它们。这对于地板和天花板纹理特别有用,因为故障可能会破坏连续性的幻觉。
参见
- 
如果您想了解更多关于纹理如何映射到 3D 表面的内部工作原理的信息,您可以阅读在 developer.nvidia.com/CgTutorial/cg_tutorial_chapter03.html可用的信息。
- 
要获取导入 2D 纹理时可用选项的完整列表,您可以参考以下网站: docs.unity3d.com/Manual/class-TextureImporter.html
通过修改 UV 值来滚动纹理
在当今游戏行业中,最常用的纹理技术之一是允许您在对象的表面上滚动纹理的过程。这使您能够创建瀑布、河流和熔岩流动等效果。这同样也是创建动画精灵效果的基础技术,但我们将在这章的后续菜谱中介绍。首先,让我们看看我们如何在 Surface Shader 中创建一个简单的滚动效果。
准备工作
要开始这个菜谱,您需要创建一个新的着色器文件(ScrollingUVs)和材质(ScrollingUVMat)。这将为我们提供一个干净整洁的着色器,我们可以用它单独研究滚动效果。
如何操作...
首先,我们将启动我们刚刚创建的新着色器文件,并按照以下步骤输入代码:
- 着色器需要两个新属性,这将允许我们控制纹理滚动的速度。所以,让我们添加一个用于X方向的滚动速度属性和一个用于Y方向的滚动速度属性,如下面的代码所示:
Properties {
  _Color ("Color", Color) = (1,1,1,1)
  _MainTex ("Albedo (RGB)", 2D) = "white" {}
  _ScrollXSpeed ("X Scroll Speed", Range(0,10)) = 2 
  _ScrollYSpeed ("Y Scroll Speed", Range(0,10)) = 2 
}
- 当在 ShaderLab 中工作时,Properties的语法看起来如下:
Properties
{
    _propertyName("Name in Inspector", Type) = value
}
包含在Properties块中的每个属性首先有一个用于在代码中引用对象的名称,这里指定为_propertyName。下划线不是必需的,但这是一个常见的标准。在括号内,您将看到两个参数。第一个是一个字符串,它定义了在检查器中显示的文本,用于表示此属性。第二个参数是我们希望存储的数据类型。
在我们的案例中,对于X和Y滚动速度,我们创建了一个可能的范围从 0 到 10 的数字。最后,我们可以使用默认值初始化属性,这通常在末尾完成。正如我们之前看到的,如果您选择使用此着色器的材质,这些属性将显示在检查器中。
有关属性及其创建的更多信息,请参阅docs.unity3d.com/Manual/SL-Properties.html。
对于这个例子,我们不需要Smoothness或Metallic属性,因此我们也可以移除它们。
- 在_MainTex定义上面的CGPROGRAM部分中修改 Cg 属性,并创建新变量,以便我们可以从我们的属性中访问值:
fixed _ScrollXSpeed; 
fixed _ScrollYSpeed; 
sampler2D _MainTex; 
- 
由于我们不再使用它们,我们需要移除 _Glossiness和_Metallic的定义。
- 
修改表面函数以改变提供给 tex2D()函数的 UVs。然后,在编辑器中按下播放按钮时,使用内置的_Time变量随时间动画 UVs:
void surf (Input IN, inout SurfaceOutputStandard o) {
    // Create a separate variable to store our UVs 
    // before we pass them to the tex2D() function 
    fixed2 scrolledUV = IN.uv_MainTex; 
    // Create variables that store the individual x and y 
    // components for the UV's scaled by time 
    fixed xScrollValue = _ScrollXSpeed * _Time; 
    fixed yScrollValue = _ScrollYSpeed * _Time; 
    // Apply the final UV offset 
    scrolledUV += fixed2(xScrollValue, yScrollValue); 
    // Apply textures and tint 
    half4 c = tex2D(_MainTex, scrolledUV); 
    o.Albedo = c.rgb * _Color; 
    o.Alpha = c.a; 
}
- 一旦脚本完成,保存它,然后回到 Unity 编辑器。转到Materials文件夹,将ScrollingUVsMat分配给使用ScrollingUVs着色器的材质。完成后,在 Albedo (RGB)属性下,从本书提供的示例代码中拖放水纹理以分配属性:

- 
创建完成后,我们需要创建一个可以使用着色器的对象。从一个新场景开始,选择 GameObject | 3D Object | Plane,并将 ScrollingUVMat材质拖放到它上。
- 
一旦应用,继续玩游戏以查看着色器的作用: 

虽然在这个静态图像中不可见,但您会注意到在 Unity 编辑器中,对象现在将在X和Y轴上移动!您可以自由地将 X 滚动速度和 Y 滚动速度属性拖放到检查器选项卡中,以查看这些更改如何影响对象的移动。如果您想更容易地看到,也可以自由地移动相机。
如果你在游戏过程中修改了材质上的变量,其值将保持更改,这与 Unity 通常的工作方式不同。
非常酷!有了这些知识,我们可以将这个概念进一步发展,以创建有趣的视觉效果。以下截图展示了使用多个材料利用滚动 UV 系统创建简单河流运动环境的结果:

它是如何工作的...
滚动系统从声明几个属性开始,这些属性将允许用户增加或减少滚动效果的滚动速度。在本质上,它们是作为从材料的 Inspector 标签传递到着色器表面函数的浮点值。有关着色器属性的更多信息,请参阅第二章,创建您的第一个着色器。
一旦我们从材料的 Inspector 标签中获取了这些浮点值,我们就可以使用它们在着色器中偏移我们的 UV 值。
要开始这个过程,我们首先将 UVs 存储在一个名为scrolledUV的单独变量中。这个变量必须是float2/fixed2,因为 UV 值是从Input结构传递给我们的:
struct Input 
{ 
  float2 uv_MainTex; 
} 
一旦我们能够访问网格的 UVs,我们可以使用我们的滚动速度变量和内置的_Time变量来偏移它们。这个内置变量返回一个float4类型的变量,这意味着这个变量的每个分量都包含与游戏时间相关的不同时间值。
这些个别时间值的完整描述可以在以下链接中找到:docs.unity3d.com/Manual/SL-UnityShaderVariables.html
这个_Time变量将根据 Unity 的游戏时间时钟给我们一个递增的浮点值。因此,我们可以使用这个值在 UV 方向上移动我们的 UVs,并使用我们的滚动速度变量来缩放这个时间:
// Create variables that store the individual x and y  
// components for the uv's scaled by time 
fixed xScrollValue = _ScrollXSpeed * _Time; 
fixed yScrollValue = _ScrollYSpeed * _Time; 
通过计算正确的时间偏移,我们可以将新的偏移值添加回原始 UV 位置。这就是为什么我们在下一行使用+=运算符的原因。我们想要获取原始 UV 位置,添加新的偏移值,然后将这个值传递给tex2D()函数作为纹理的新 UVs。这会在表面上创建纹理移动的效果。我们真正做的是操作 UVs,因此我们是在模拟纹理移动的效果:
scrolledUV += fixed2(xScrollValue, yScrollValue); 
half4 c = tex2D (_MainTex, scrolledUV); 
创建具有法线贴图的着色器
3D 模型的每个三角形都有一个面向方向,这是它指向的方向。通常用一个箭头表示,放置在三角形的中心,并且与表面垂直。面向方向在光线反射到表面上的方式中起着重要作用。如果相邻的两个三角形面向不同的方向,它们将以不同的角度反射光线,因此它们将以不同的方式着色。对于弯曲物体,这是一个问题:显然,几何形状是由平面三角形组成的。
为了避免这个问题,三角形上光线的反射方式不考虑其朝向方向,而是考虑其法线方向。正如在“向着色器添加纹理”的配方中所述,顶点可以存储数据;法线方向是除 UV 数据之外最常用的信息。这是一个单位长度的向量(这意味着它的长度为 1),它指示顶点所面对的方向。
无论朝向方向如何,三角形内的每个点都有自己的法线方向,它是其顶点中存储的法线的线性插值。这使我们能够在低分辨率模型上模拟高分辨率几何形状的效果。
下面的截图显示了使用不同顶点法线渲染的相同几何形状。在图像的左侧,法线与由其顶点表示的面垂直;这表明每个面之间有明显的分离。在右侧,法线沿着表面进行插值,表明即使表面是粗糙的,光线也应该像在光滑表面上一样反射。很容易看出,即使以下截图中的三个物体具有相同的几何形状,它们反射光线的方式也不同。尽管它们由平面三角形组成,但右侧的物体反射光线就像其表面实际上是弯曲的一样:

带有粗糙边缘的平滑物体是顶点法线插值的一个明显迹象。如果我们绘制每个顶点中存储的法线方向,就像以下截图所示,这一点可以观察到。请注意,每个三角形只有三个法线,但由于多个三角形可以共享同一个顶点,因此可能有多条线从这个顶点发出:

从 3D 模型计算法线是一种迅速被更高级的单法线贴图技术所取代的技术。与纹理贴图类似,法线方向可以通过一个额外的纹理提供,通常称为正常贴图或凹凸贴图。
正常贴图通常是使用图像的红色、绿色和蓝色通道来表示法线方向的X、Y和Z分量。如今创建正常贴图的方法有很多。一些应用程序,例如 CrazyBump(www.crazybump.com/)和 NDO Painter(quixel.se/ndo/),会接收 2D 数据并将其转换为正常数据。其他应用程序,如 Zbrush 4R7(www.pixologic.com/)和 AUTODESK(usa.autodesk.com),则会接收 3D 雕刻数据并为您创建正常贴图。创建正常贴图的实际过程超出了本书的范围,但前文中的链接应该能帮助您开始。
Unity 使用UnpackNormals()函数使在表面着色器领域添加法线到你的着色器变得非常简单。让我们看看这是如何完成的。
准备工作
要开始这个配方,首先通过选择 File | New Scene 创建一个新的场景。然后,通过 GameObject | 3D Objects | Sphere 创建一个球体游戏对象。在 Hierarchy 标签中双击对象,使其在 Scene 标签中聚焦。你还需要创建一个新的标准表面着色器文件(NormalShader)和材质(NormalShaderMat)。创建后,将材质设置为场景中的球体。这将给我们一个干净的工作空间,我们可以查看仅法线贴图技术:

你需要为这个配方提供一个法线贴图,但本书附带的 Unity 项目中也有一个。
本书内容中包含的一个示例法线贴图如下所示:

你可以在Assets | Chapter 03 | Textures文件夹下的normalMapExample中自己查看。
如何操作...
创建法线贴图着色器的以下步骤:
- 让我们设置Properties块,以便有一个颜色Tint和纹理:
Properties 
{ 
  _MainTint ("Diffuse Tint", Color) = (0,1,0,1) 
  _NormalTex ("Normal Map", 2D) = "bump" {} 
} 
在这种情况下,我在绿色和 alpha 通道中设置为1,红色和蓝色通道设置为0,因此默认颜色将是绿色。对于_NormalTex属性,我们使用 2D 类型,这意味着我们可以使用 2D 图像来指定每个像素将使用的内容。通过将纹理初始化为bump,我们告诉 Unity _NormalTex将包含一个法线贴图(有时也称为凹凸贴图,因此命名为 bump),如果未设置纹理,它将被灰色纹理替换。使用的颜色(0.5,0.5,0.5,1)表示没有任何凹凸。
- 在SubShader{}块中,在CGPROGRAM语句下方滚动并删除原始的_MainText,_Glossiness,_Metallic和_Color定义。之后,添加我们的_NormalTex和_MainTint:
    CGPROGRAM
    // Physically based Standard lighting model, and enable shadows 
    // on all light types
    #pragma surface surf Standard fullforwardshadows
    // Use shader model 3.0 target, to get nicer looking lighting
    #pragma target 3.0
    // Link the property to the CG program 
    sampler2D _NormalTex; 
    float4 _MainTint; 
- 我们需要确保更新Input结构体中的正确变量名,以便我们可以使用模型的 UVs 来为法线贴图纹理:
// Make sure you get the UVs for the texture in the struct 
struct Input 
{ 
  float2 uv_NormalTex; 
} 
- 最后,我们使用内置的UnpackNormal()函数从法线贴图纹理中提取法线信息。然后,你只需将这些新的法线应用到表面着色器的输出中:
void surf (Input IN, inout SurfaceOutputStandard o) {
  // Use the tint provided as the base color for the material
  o.Albedo = _MainTint;
  // Get the normal data out of the normal map texture 
  // using the UnpackNormal function 
  float3 normalMap = UnpackNormal(tex2D(_NormalTex, 
    IN.uv_NormalTex)); 
  // Apply the new normal to the lighting model 
  o.Normal = normalMap.rgb; 
}
- 
保存你的脚本并返回到 Unity 编辑器。你应该注意到,如果添加了球体,它现在默认是绿色的。更重要的是,请注意新添加的法线贴图属性。将法线贴图纹理拖放到槽中。 
- 
你可能会注意到一些变化,但可能难以直观地看到正在发生什么。在法线贴图属性中,将平铺设置为( 10,10)。这样,你可以在球体的 X 和 Y 轴上看到法线贴图重复 10 次,而不是只重复一次:

- 以下截图展示了我们的法线贴图着色器的结果:

着色器可以同时具有纹理贴图和法线贴图。使用相同的 UV 数据来处理两者并不罕见。然而,在顶点数据中(UV2)提供一组次级 UV 也是可能的,这些 UV 专门用于法线贴图。
它是如何工作的...
执行正常映射效果的数学计算确实超出了本章的范围,但 Unity 已经为我们完成了所有这些。它为我们创建了函数,这样我们就不必一遍又一遍地重复做同样的事情。这也是为什么表面着色器是编写着色器的一种非常高效的方法的另一个原因。
如果你查看在 Unity 安装目录中的Editor | Data | CGIncludes文件夹中找到的UnityCG.cginc文件,你将找到UnpackNormal()函数的定义。当你在这个表面着色器中声明这个函数时,Unity 会为你处理提供的法线贴图,并给出正确的数据类型,以便你可以在你的每像素光照函数中使用它。这是一个节省大量时间的方法!在采样纹理时,你会从0到1得到 RGB 值;然而,法线向量的方向范围从-1到1。UnpackNormal()将这些分量带入正确的范围。
一旦使用UnpackNormal()函数处理了法线贴图,你就将其发送回你的SurfaceOutput结构体,以便在光照函数中使用。这是通过使用o.Normal = normalMap.rgb;来完成的。我们将在第四章,“理解光照模型”中看到法线是如何实际上用于计算每个像素的最终颜色的。
更多...
你还可以为你的法线贴图着色器添加一些控件,让用户调整法线贴图的强度。这可以通过修改法线贴图变量的x和y分量,然后将它们全部加回来轻松完成。在Properties块中添加另一个属性,并将其命名为_NormalMapIntensity:
_NormalMapIntensity("Normal intensity", Range(0,3)) = 1 
在这种情况下,我们赋予属性在0到3之间的能力,默认值为1。一旦创建,你需要在 SubShader 内部添加变量:
// Link the property to the CG program 
sampler2D _NormalTex; 
float4 _MainTint; 
float _NormalMapIntensity;
在添加属性后,我们可以利用它。将展开的法线贴图的x和y分量相乘,然后将此值以粗体显示的更改重新应用于法线贴图变量:
void surf (Input IN, inout SurfaceOutputStandard o) {
  // Use the tint provided as the base color for the material
  o.Albedo = _MainTint;
  // Get the normal data out of the normal map texture 
  // using the UnpackNormal function 
  float3 normalMap = UnpackNormal(tex2D(_NormalTex, 
    IN.uv_NormalTex)); 
 normalMap.x *= _NormalMapIntensity; 
 normalMap.y *= _NormalMapIntensity; 
 // Apply the new normal to the lighting model 
 o.Normal = normalize(normalMap.rgb); 
}
法线向量应该具有长度等于一的长度。将它们乘以_NormalMapIntensity会改变它们的长度,因此需要进行归一化。归一化函数将调整向量,使其指向正确的方向,但长度为 1,这正是我们所寻找的。
现在,你可以在材质的检查器标签中让用户调整法线贴图的强度,如下所示:

以下截图显示了使用我们的标量值修改法线贴图的结果:

创建透明材质
我们迄今为止看到的着色器都有一个共同点;它们用于固体材质。如果你想改善游戏的外观,透明材质通常是一个很好的起点。它们可以用于从火焰效果到玻璃窗户的任何东西。不幸的是,与它们一起工作稍微复杂一些。在渲染固体模型之前,Unity 会根据从相机到模型的距离对它们进行排序(Z 排序),并跳过所有面向相机的三角形(裁剪)。当渲染透明几何体时,这两个方面有时会导致问题。这个配方将向你展示如何在创建透明表面着色器时解决这些问题。这个主题将在第七章(part0188.html#5J99O0-e8c76c858d514bc3b1668fda96f8fa08)中大量回顾,片段着色器和抓取通道,其中将提供逼真的玻璃和水着色器。
准备工作
这个配方需要一个新着色器,我们将称之为 Transparent,以及一个新的材质(TransparentMat),以便它可以附加到对象上。由于这将是一个透明玻璃窗户,一个四边形或平面是完美的(GameObject | 3D Objects | Quad)。我们还需要几个其他不透明对象来测试效果:

在这个例子中,我们将使用 PNG 图像文件作为玻璃纹理,因为它支持用于确定玻璃透明度的 alpha 通道。创建此类图像的过程取决于你使用的软件。然而,这些是你需要遵循的主要步骤:
- 
找到你想要用于窗户的玻璃图像。 
- 
使用像 GIMP 或 Photoshop 这样的照片编辑软件打开它。 
- 
选择你想要半透明的图像部分。 
- 
在你的图像上创建一个白色(全不透明)的图层蒙版。 
- 
使用之前选择的选项,用较深的颜色填充图层蒙版。白色被视为完全可见,黑色被视为不可见,灰色则介于两者之间。 
- 
保存图像并将其导入 Unity。 
本配方中使用的玩具图像是法国 梅奥大教堂 的彩色玻璃的图片 (en.wikipedia.org/wiki/Stained_glass)。如果你已经遵循了所有这些步骤,你的图像应该看起来像这样(RGB 通道在左侧,A 通道在右侧):

你还可以使用提供的示例代码中的图像文件(Chapter 3 | Textures 文件夹中的 Meaux_Vitrail.psd)。
将此图像附加到材质上会使图像显示出来,但我们无法看到玻璃后面的内容:

由于我们想看到背后的内容,我们可以调整着色器来实现这一点。
如何操作...
如前所述,在使用透明着色器时,有几个方面需要注意:
- 
从代码的 Properties和SubShader部分移除_Glossiness和_Metallic变量,因为在这个示例中它们不是必需的。
- 
在着色器的 SubShader{}部分中,修改Tags部分如下,以便我们可以发出信号,表明着色器是透明的:
Tags 
{ 
  "Queue" = "Transparent" 
  "IgnoreProjector" = "True" 
  "RenderType" = "Transparent" 
} 
标签由SubShader使用,以了解项目和何时渲染。类似于字典类型,标签是键值对,其中左侧是标签名称,右侧是您希望设置的值。
有关 ShaderLab 中标签的更多信息,请参阅:docs.unity3d.com/Manual/SL-SubShaderTags.html
- 由于这个着色器是为 2D 材质设计的,请确保通过在LOD 200行下方添加以下内容来防止模型的背面几何体被绘制:
    LOD 200
    // Do not show back
    Cull Back
    CGPROGRAM
    // Physically based Standard lighting model, and enable shadows on all light types
    #pragma surface surf Standard alpha:fade 
- 告诉着色器,这个材质是透明的,并且需要与屏幕上绘制的内容混合:
#pragma surface surf Standard alpha:fade 
- 使用此表面着色器确定玻璃的最终颜色和透明度:
void surf(Input IN, inout SurfaceOutputStandard o) 
{ 
  float4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color; 
  o.Albedo = c.rgb; 
  o.Alpha = c.a; 
} 
- 之后,保存您的脚本并返回到 Unity 编辑器:

注意,您现在可以看到玻璃后面的立方体。太完美了!
它是如何工作的...
这个着色器引入了几个新概念。首先,Tags用于添加有关对象如何渲染的信息。这里真正有趣的是Queue。默认情况下,Unity 会根据对象与摄像机的距离为您排序对象。因此,当对象靠近摄像机时,它将覆盖所有远离摄像机的对象。对于大多数情况,这对游戏来说效果很好,但您会发现自己在某些情况下想要对场景中对象的排序有更多的控制。Unity 为我们提供了一些默认的渲染队列,每个队列都有一个独特的值,指示 Unity 何时将对象绘制到屏幕上。这些内置的渲染队列被称为Background、Geometry、AlphaTest、Transparent和Overlay。这些队列并非随意创建;它们实际上有助于我们在编写着色器和与实时渲染器交互时简化生活。
参考以下表格,了解每个单独渲染队列的用法描述:
| 渲染队列 | 渲染队列描述 | 渲染队列值 | 
|---|---|---|
| Background | 这个渲染队列首先渲染。它用于天空盒等。 | 1000 | 
| Geometry | 这是默认的渲染队列。它用于大多数对象。不透明几何体使用此队列。 | 2000 | 
| AlphaTest | 使用此队列进行 Alpha 测试的几何体。它与 Geometry队列不同,因为它在绘制所有实体对象之后渲染 Alpha 测试对象更有效率。 | 2450 | 
| Transparent | 此渲染队列在 Geometry和AlphaTest队列之后按从后向前的顺序渲染。任何 alpha 混合(即不写入深度缓冲区的着色器)的内容应放在这里,例如,玻璃和粒子效果。 | 3000 | 
| Overlay | 此渲染队列用于叠加效果。最后渲染的内容应放在这里,例如,镜头光晕。 | 4000 | 
因此,一旦您知道您的对象属于哪个渲染队列,您就可以分配其内置的渲染队列标签。我们的着色器使用了Transparent队列,因此我们编写了Tags{"Queue"="Transparent"}。
Transparent队列在Geometry之后渲染的事实并不意味着我们的玻璃会出现在所有其他实体物体之上。Unity 将最后绘制玻璃,但它不会渲染属于被其他东西遮挡的几何形状的像素。这种控制是通过一种称为ZBuffering的技术完成的。有关模型渲染的更多信息,请参阅以下链接:docs.unity3d.com/Manual/SL-CullAndDepth.html。
IgnoreProjector标签使此对象不受 Unity 投影器的影响。最后,RenderType在着色器替换中发挥作用,这个主题将在第十章游戏玩法和屏幕效果中简要介绍。
最后介绍的概念是alpha:fade。这表示所有来自这种材质的像素都必须根据它们的 alpha 值与屏幕上之前的内容混合。如果没有这个指令,像素将以正确的顺序绘制,但它们将没有任何透明度。
创建全息着色器
每年都有越来越多的以太空为主题的游戏发布。一款好的科幻游戏的一个重要部分是未来科技的表现和融入游戏玩法的方式。没有比全息投影更能体现未来感的了。尽管全息投影以多种形式存在,但它们通常被表示为半透明、薄薄的对象投影。这个配方向您展示了如何创建模拟这种效果的着色器。以此为起点:您可以添加噪声、动画扫描线、振动,以创建真正出色的全息效果。以下截图显示了全息效果的示例:

准备工作
创建一个名为Holographic的着色器。将其附加到材质(HolographicMat)并将它分配到场景中的 3D 模型:

如何做到这一点...
以下更改将把我们的现有着色器转换为全息着色器:
- 
删除以下属性,因为它们将不会使用: - 
_Glossiness
- 
_Metallic
 
- 
- 
将以下属性添加到着色器中: 
_DotProduct("Rim effect", Range(-1,1)) = 0.25 
- 将其相应的变量添加到CGPROGRAM部分:
float _DotProduct; 
- 由于这种材质是透明的,请添加以下标签:
Tags 
{ 
  "Queue" = "Transparent" 
  "IgnoreProjector" = "True" 
  "RenderType" = "Transparent" 
} 
根据你将使用的对象类型,你可能希望其背面看起来。如果是这样,添加Cull Off,这样模型的背面就不会被移除(裁剪)。
- 这个着色器并不试图模拟真实材料,因此不需要使用 PBR 光照模型。相反,使用非常便宜的朗伯反射。此外,我们应该禁用任何带有nolighting的照明,并通知 Cg 这是一个使用alpha:fade的透明着色器:
#pragma surface surf Lambert alpha:fade nolighting 
- 更改Input结构,以便 Unity 将其填充为当前视图方向和世界法线方向:
struct Input 
{ 
  float2 uv_MainTex; 
  float3 worldNormal; 
  float3 viewDir; 
}; 
- 使用以下表面函数。请记住,由于这个着色器使用朗伯反射作为其光照函数,因此SurfaceOutput结构的名称应相应地更改为SurfaceOutput而不是SurfaceOutputStandard:
void surf(Input IN, inout SurfaceOutput o) 
{ 
  float4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color; 
  o.Albedo = c.rgb; 
  float border = 1 - (abs(dot(IN.viewDir, 
     IN.worldNormal))); 
  float alpha = (border * (1 - _DotProduct) + _DotProduct); 
  o.Alpha = c.a * alpha; 
} 
- 保存你的脚本并进入 Unity。从那里,更改 HolographicMat 中的颜色属性,看看你的全息图是如何变得生动起来的:

你现在可以使用边缘效果滑块来选择全息效果的强度。
它是如何工作的...
如前所述,这个着色器通过仅显示对象的轮廓来工作。如果我们从另一个角度观察对象,其轮廓将改变。从几何学角度讲,模型的边缘是所有那些其法线方向与当前视图方向正交(90 度)的三角形。Input结构分别声明了这些参数,worldNormal和viewDir。
使用_DotProduct可以解决理解两个向量是否正交的问题。这是一个操作符,它接受两个向量,如果它们是正交的,则返回零。我们使用_DotProduct来确定_DotProduct需要接近零到什么程度,以便三角形完全消失。
在这个着色器中使用的第二个方面是模型边缘(完全可见)与由_DotProduct确定的角(不可见)之间的柔和渐变。这种线性插值如下所示:
float alpha = (border * (1 - _DotProduct) + _DotProduct); 
最后,将原始纹理中的alpha与新计算的系数相乘,以实现最终的外观。
还有更多...
这种技术非常简单且相对便宜,但可以用于各种效果,如下所示:
- 
科幻游戏中行星的略带色彩的气氛 
- 
已被选中或当前鼠标悬停下的对象边缘 
- 
一个幽灵或鬼魂 
- 
发动机排出的烟雾 
- 
爆炸的冲击波 
- 
正在受到攻击的宇宙飞船的气泡护盾 
参见
_DotProduct在计算反射的方式中起着重要作用。第四章,理解光照模型,将详细解释它是如何工作的以及为什么它在许多着色器中被广泛使用。
纹理打包和混合
纹理不仅用于存储大量数据,不仅仅是像素颜色,正如我们通常所认为的那样,而且还用于在x和y方向以及 RGBA 通道中的多组像素。我们实际上可以将多个图像打包到一个 RGBA 纹理中,并通过从着色器代码中提取每个组件,将每个 R、G、B 和 A 组件用作单独的纹理。
将单个灰度图像打包到单个 RGBA 纹理中的结果可以在以下屏幕截图中看到:

为什么这有帮助呢?好吧,从你的应用程序实际占用的内存量来看,纹理占据了应用程序大小的大部分。我们当然可以减小图像的大小,但这样我们就会失去它在表示方式上的细节。因此,为了开始减小应用程序的大小,我们可以查看我们在着色器中使用的所有图像,看看我们是否可以将这些纹理合并成一个纹理。使用包含多个图像的单个纹理比使用单独的文件需要更少的绘制调用和更少的开销。我们还可以使用这个概念将不规则形状的纹理(即不是正方形的纹理)合并成一个,这样占用的空间比给它们各自的全纹理要少。
任何灰度纹理都可以打包到另一个纹理的 RGBA 通道中。一开始这听起来可能有点奇怪,但这个配方将要演示打包纹理和使用这些打包纹理在着色器中的用途之一。
使用这些打包纹理的一个例子是当你想要将一组纹理混合到单个表面上的情况。你通常在地面类型着色器中看到这种情况,你需要使用某种控制纹理或打包纹理(在这种情况下)很好地融合到另一个纹理中。这个配方涵盖了这项技术,并展示了如何构建一个优秀的四纹理混合地面着色器的开始。
准备工作
让我们在Shaders文件夹中创建一个新的着色器文件(TextureBlending),然后为这个着色器创建一个新的材质(TextureBlendingMat)。着色器和材质文件的命名规范完全取决于你,所以尽量保持它们有组织,便于以后参考。
一旦你的着色器和材质准备好了,创建一个新的场景,我们可以在这个场景中测试我们的着色器。在场景内部,放置来自“第三章”|“模型”文件夹的Terrain_001对象,并将其TextureBlendingMat材质分配给它:

你还需要收集你想要混合的四个纹理。这些可以是任何东西,但为了一个漂亮的地面着色器,你将想要草地、泥土、多石泥土和岩石纹理。你可以在本书的示例代码中找到这些资源,在“第一章”|“标准资源”|“环境”|“地形资源”|“表面纹理”文件夹中。
最后,我们还需要一个包含灰度图像的混合纹理。这将给我们四个混合纹理,我们可以使用它们来指导颜色纹理如何放置在物体表面上。
我们可以使用非常复杂的混合纹理来在地形网格上创建非常逼真的地形纹理分布,如下面的屏幕截图所示:

如何做到...
让我们通过以下步骤中的代码来学习如何使用打包纹理:
- 我们需要在Properties块中添加一些属性。我们需要五个sampler2D对象,或纹理,以及两个Color属性:
Properties 
{ 
 _MainTint ("Diffuse Tint", Color) = (1,1,1,1) 
//Add the properties below so we can input all of our 
   textures 
  _ColorA ("Terrain Color A", Color) = (1,1,1,1) 
  _ColorB ("Terrain Color B", Color) = (1,1,1,1) 
  _RTexture ("Red Channel Texture", 2D) = ""{} 
  _GTexture ("Green Channel Texture", 2D) = ""{} 
  _BTexture ("Blue Channel Texture", 2D) = ""{} 
  _ATexture ("Alpha Channel Texture", 2D) = ""{} 
  _BlendTex ("Blend Texture", 2D) = ""{} 
} 
和往常一样,从我们的代码中移除我们不使用的基着色器属性。
- 然后,我们需要创建SubShader{}部分变量,这将是我们与Properties块中的数据链接:
CGPROGRAM 
#pragma surface surf Lambert 
// Use shader model 3.5 target, to support enough textures
#pragma target 3.5
float4 _MainTint; 
float4 _ColorA; 
float4 _ColorB; 
sampler2D _RTexture; 
sampler2D _GTexture; 
sampler2D _BTexture; 
sampler2D _BlendTex; 
sampler2D _ATexture; 
- 由于我们的着色器中包含的项目数量,我们需要将我们的着色器模型的目标版本更新到3.5:
有关着色器编译目标级别的更多信息,请参阅:docs.unity3d.com/Manual/SL-ShaderCompileTargets.html。
- 因此,现在我们有了纹理属性,并将它们传递给我们的SubShader{}函数。为了允许用户按纹理基础更改平铺率,我们需要修改我们的Input结构。这将允许我们在每个纹理上使用平铺和偏移参数:
struct Input  
{ 
  float2 uv_RTexture; 
  float2 uv_GTexture; 
  float2 uv_BTexture; 
  float2 uv_ATexture; 
  float2 uv_BlendTex; 
}; 
- 在surf()函数中,获取纹理信息并将其存储在其自己的变量中,这样我们就可以以干净、易于理解的方式处理数据:
  void surf (Input IN, inout SurfaceOutput o) {
    //Get the pixel data from the blend texture 
    //we need a float 4 here because the texture 
    //will return R,G,B,and A or X,Y,Z, and W 
    float4 blendData = tex2D(_BlendTex, IN.uv_BlendTex); 
    //Get the data from the textures we want to blend 
    float4 rTexData = tex2D(_RTexture, IN.uv_RTexture); 
    float4 gTexData = tex2D(_GTexture, IN.uv_GTexture); 
    float4 bTexData = tex2D(_BTexture, IN.uv_BTexture); 
    float4 aTexData = tex2D(_ATexture, IN.uv_ATexture); 
记住,由于我们使用了 Lambert,我们将使用SurfaceOutput而不是SurfaceOutputStandard作为surf函数。
- 让我们使用lerp()函数将我们每个纹理混合在一起。它接受三个参数,lerp(value : a, value : b, blend: c)。lerp()函数接收两个纹理,并使用最后一个参数提供的浮点值将它们混合:
//No we need to construct a new RGBA value and add all  
//the different blended texture back together 
float4 finalColor; 
finalColor = lerp(rTexData, gTexData, blendData.g); 
finalColor = lerp(finalColor, bTexData, blendData.b); 
finalColor = lerp(finalColor, aTexData, blendData.a);
finalColor.a = 1.0;
- 最后,我们将混合纹理乘以颜色着色值,并使用红色通道来确定两种不同的地形着色颜色放置的位置:
  //Add on our terrain tinting colors 
  float4 terrainLayers = lerp(_ColorA, _ColorB, blendData.r); 
  finalColor *= terrainLayers; 
  finalColor = saturate(finalColor); 
  o.Albedo = finalColor.rgb * _MainTint.rgb; 
  o.Alpha = finalColor.a;
}
- 保存你的脚本,然后回到 Unity。一旦进入,你就可以将TerrainBlend纹理分配给混合纹理属性。完成此操作后,将不同的纹理放置在不同的通道中,以便看到我们的脚本在起作用:

- 通过使用不同的纹理和地形着色,我们可以进一步实现这一效果,以最小的努力创建出一些看起来很棒的地形。将四个地形纹理混合在一起并创建地形着色技术的结果可以在以下屏幕截图中看到:

它是如何工作的...
这可能看起来像是很多行代码,但混合的概念实际上非常简单。为了使该技术生效,我们必须使用来自 CgFX 标准库的内置 lerp() 函数。这个函数允许我们使用第三个参数作为混合量,在第一个和第二个参数之间选择一个值:
| 函数 | 描述 | 
|---|---|
| lerp(a, b, f) | 这涉及线性插值:(1 - f) a + b * f*在这里, a和b是匹配的向量或标量类型。f参数可以是与a和b相同类型的标量或向量。 | 
例如,如果我们想找到 1 和 2 之间的中间值,我们可以将值 0.5 作为 lerp() 函数的第三个参数输入,它将返回值 1.5。这对于我们的混合需求来说非常完美,因为 RGBA 纹理中单个通道的值是单个浮点值,通常在 0 到 1 的范围内。
在着色器中,我们简单地从我们的混合纹理中取一个通道,并使用它来驱动每个像素中选择的颜色,在 lerp() 函数中。例如,我们取我们的草地纹理和泥土纹理,使用混合纹理的红通道,并将其输入到 lerp() 函数中。这将为我们提供每个像素表面的正确混合颜色结果。
使用 lerp() 函数时发生的情况的更直观表示如下所示:

着色器代码简单地使用混合纹理的四个通道和所有颜色纹理来创建最终的混合纹理。然后,这个最终纹理将成为我们可以与漫反射光照相乘的颜色。
在你的地形周围创建一个圆
许多即时战略游戏通过在选定的单位周围画圆来显示距离(攻击范围、移动距离、视野等)。如果地形是平坦的,这可以通过拉伸一个带有圆形纹理的四边形来完成。如果不是这样,四边形很可能会被山丘或其他几何图形裁剪掉。这个配方将向你展示如何创建一个着色器,允许你在任意复杂性的对象周围画圆。如果你想能够移动或动画化你的圆,我们需要一个着色器和 C# 脚本。
以下截图显示了使用着色器在丘陵地区绘制圆的示例:

准备工作
尽管与每一块几何图形都有关联,但这种技术主要针对地形。因此,第一步是在 Unity 中设置一个地形,但我们将不在模型中使用,而是在 Unity 编辑器内创建一个:
- 
让我们从创建一个新的着色器 RadiusShader和相应的材质RadiusMat开始。
- 
准备好你的对象角色;我们将围绕它画一个圆。 
- 
从菜单中,导航到 GameObject | 3D Object | Terrain 创建一个新的地形。 
- 
为你的地形创建几何形状。你可以导入现有的一个,或者使用可用的工具(提升/降低地形,绘制高度,平滑高度)来绘制自己的地形。 
- 
在 Unity 中,地形是特殊对象,纹理映射的方式与传统 3D 模型不同。你不能从着色器提供 _MainTex,因为它需要直接从地形本身提供。为此,选择绘制纹理,然后点击添加纹理...:

本书没有涵盖地形的创建,但如果你想了解更多,请查看以下链接:docs.unity3d.com/Manual/terrain-UsingTerrains.html
- 现在纹理已经设置好了,你必须更改地形的材质,以便提供一个自定义着色器。从地形设置中,将材质属性更改为Custom,然后将半径材质拖到Custom Material框中:

你现在可以创建你的着色器了。
如何做到这一点...
让我们先编辑RadiusShader文件:
- 在新的着色器中,删除_Glossiness和_Metallic属性,并添加以下四个属性:
_Center("Center", Vector) = (200,0,200,0) 
_Radius("Radius", Float) = 100 
_RadiusColor("Radius Color", Color) = (1,0,0,1) 
_RadiusWidth("Radius Width", Float) = 10
- 将它们各自的变量添加到CGPROGRAM部分,记得删除_Glossiness和_Metallic的声明:
float3 _Center; 
float _Radius; 
fixed4 _RadiusColor; 
float _RadiusWidth; 
- Input到我们的表面函数不仅需要纹理的 UV,还需要地形的每个点的位置(在世界坐标中)。我们可以通过更改- struct Input来检索此参数:
struct Input 
{ 
  float2 uv_MainTex; // The UV of the terrain texture 
  float3 worldPos;   // The in-world position 
}; 
- 最后,我们使用这个表面函数:
void surf(Input IN, inout SurfaceOutputStandard o) 
{
  // Get the distance between the center of the 
  // place we wish to draw from and the input's 
  // world position
  float d = distance(_Center, IN.worldPos);
  // If the distance is larger than the radius and
  // it is less than our radius + width change the color
  if ((d > _Radius) && (d < (_Radius + _RadiusWidth)))
  {
    o.Albedo = _RadiusColor;
  }
  // Otherwise, use the normal color
  else
  {
    o.Albedo = tex2D(_MainTex, IN.uv_MainTex).rgb;
  }
}
这些步骤就是绘制地形上的圆圈所需的所有步骤。你可以使用材质的检查器选项卡来更改圆圈的位置、半径和颜色:

移动圆圈
这很棒,但你可能还希望更改运行时圆圈的位置,我们可以通过代码来实现。如果你想让圆圈跟随你的角色,则需要其他步骤:
- 
创建一个新的 C#脚本,命名为 SetRadiusProperties。
- 
由于你可能希望在游戏和编辑器中都能看到这个变化,我们可以在类顶部添加一个标签,表示在游戏运行时执行此代码,通过添加以下标签: 
[ExecuteInEditMode]
public class SetRadiusProperties : MonoBehaviour
- 将以下属性添加到脚本中:
public Material radiusMaterial; 
public float radius = 1; 
public Color color = Color.white; 
- 在Update()方法中,添加以下代码行:
if(radiusMaterial != null)
{
    radiusMaterial.SetVector("_Center", transform.position);
    radiusMaterial.SetFloat("_Radius", radius);
    radiusMaterial.SetColor("_RadiusColor", color);
}
- 
将脚本附加到你希望绘制圆圈的对象上。 
- 
最后,将 RadiusMat材质拖到脚本的半径材质槽中:

现在,你可以移动你的角色,这将围绕它创建一个漂亮的圆圈。更改Radius脚本的属性将更改半径。
它是如何工作的...
绘制圆的相关参数包括其中心、半径和颜色。这些参数在着色器中均可用,分别以 _Center、_Radius 和 _RadiusColor 命名。通过将 worldPos 变量添加到 Input 结构中,我们请求 Unity 提供我们正在绘制的像素的位置,该位置以世界坐标表示。这是在编辑器中对象的实际位置。
surf() 函数是实际绘制圆的地方。它计算正在绘制的点与半径中心的距离,然后检查它是否位于 _Radius 和 _Radius + _RadiusWidth 之间;如果是这种情况,它使用所选颜色。在其他情况下,它就像我们迄今为止看到的所有其他着色器一样,对纹理图进行采样。
第四章:理解光照模型
在前面的章节中,我们介绍了表面着色器,并解释了如何通过改变物理属性(如漫反射和镜面反射)来模拟不同的材料。这究竟是如何实现的?每个表面着色器的核心是其光照模型。这是一个函数,它接受这些属性并计算每个像素的最终颜色。Unity 通常将这一点隐藏起来,因为要编写光照模型,你必须了解光线如何反射和折射到表面上。本章将最终向你展示光照模型是如何工作的,并为你提供创建自己光照模型的基础。
在本章中,你将学习以下配方:
- 
创建自定义的漫反射光照模型 
- 
创建卡通着色器 
- 
创建 Phong 镜面类型 
- 
创建 BlinnPhong 镜面类型 
- 
创建各向异性镜面类型 
简介
模拟光线的工作方式是一项非常具有挑战性和资源消耗的任务。多年来,视频游戏一直使用非常简单的光照模型,尽管缺乏真实感,但它们非常可信。即使现在大多数 3D 引擎都使用基于物理的渲染器,探索一些更简单的技术也是值得的。本章中介绍的技术在资源有限的设备上(如手机)得到了广泛应用,并且理解这些简单的光照模型对于你想要创建自己的光照模型也是至关重要的。
创建自定义的漫反射光照模型
如果你熟悉 Unity 4,你可能知道它提供的默认着色器是基于一个称为朗伯反射的光照模型。这个配方将向你展示如何创建具有自定义光照模型的着色器,并解释相关的数学和实现。以下图表显示了使用标准着色器(右侧)和漫反射朗伯着色器(左侧)渲染的相同几何体:

基于朗伯反射的着色器被归类为非真实感着色器;在现实世界中,没有任何物体真的看起来像这样。然而,朗伯着色器仍然经常在低多边形游戏中使用,因为它们能够在复杂几何体的表面之间产生清晰的对比。用于计算朗伯反射的光照模型也非常高效,使其非常适合移动游戏。
Unity 已经为我们提供了一个可以用于着色器的光照函数。它被称为朗伯光照模型。这是更基本和高效的反射形式之一,你甚至可以在今天很多游戏中找到它。因为它已经内置在 Unity 表面着色器语言中,我们认为最好从它开始,并在此基础上构建。你还可以在 Unity 参考手册中找到一个示例,但我们将更深入地探讨数据来源以及为什么它以这种方式工作。这将帮助你建立良好的基础,以便我们可以在本章后面的配方中构建这一知识。
准备工作
让我们首先执行以下步骤:
- 
创建一个新的着色器并给它命名( SimpleLambert)。
- 
创建一个新的材质,给它命名( SimpleLambertMat),并将新着色器分配给其shader属性。
- 
然后,创建一个球体对象,并将其大致放置在场景的中心,并将新材质附加到它上面。 
- 
最后,让我们创建一个方向光,如果还没有创建的话,以便照亮我们的物体。 
- 
当你在 Unity 中设置好资产后,你应该有一个类似于以下截图的场景: 

如何做...
通过对着色器进行以下更改可以实现朗伯反射:
- 首先替换着色器的 Properties块,如下所示:
Properties 
{
  _MainTex("Texture", 2D) = "white" 
}
- 
由于我们正在移除所有其他属性,请从 SubShader部分中移除_Glossiness、_Metallic和_Color声明。
- 
更改着色器的 #pragma指令,使其不再使用Standard,而是使用我们自定义的光照模型:
#pragma surface surf SimpleLambert  
如果你现在尝试运行脚本,它将抱怨它不知道 SimpleLambert 光照模型是什么。我们需要创建一个名为 Lighting + 我们在这里给出的名称的函数,其中包含如何照亮物体的说明,我们将在本食谱的后面部分编写。在这种情况下,它将是 LightingSimpleLambert。
- 使用一个非常简单的表面函数,它只是根据其 UV 数据采样纹理:
void surf(Input IN, inout SurfaceOutput o) { 
  o.Albedo = tex2D(_MainTex, IN.uv_MainTex).rgb; 
} 
- 添加一个名为 LightingSimpleLambert()的函数,该函数将包含以下代码以实现朗伯反射:
// Allows us to use the SimpleLambert lighting mode
half4 LightingSimpleLambert (SurfaceOutput s, half3 lightDir, 
                             half atten) 
{ 
  // First calculate the dot product of the light direction and the 
  // surface's normal
  half NdotL = dot(s.Normal, lightDir); 
  // Next, set what color should be returned
  half4 color; 
  color.rgb = s.Albedo * _LightColor0.rgb * (NdotL * atten); 
  color.a = s.Alpha; 
  // Return the calculated color
  return color; 
} 
- 保存你的脚本并返回到 Unity 编辑器。你应该注意到它看起来与之前有些不同:

- 如果我们使用上一章第三章,“表面着色器和纹理映射”中使用的圆柱体,效果甚至更容易看到:

它是如何工作的...
如前所述,在第二章,“创建您的第一个着色器”中,#pragma 指令用于指定要使用哪个表面函数。选择不同的光照模型以类似的方式工作:SimpleLambert 强制 Cg 寻找名为 LightingSimpleLambert() 的函数。注意开头的 Lighting,在指令中省略了它。
Lighting 函数接受三个参数:表面输出(其中包含物理属性,如反射率和透明度)、光线来的方向以及其衰减。
根据朗伯反射定律,一个表面反射的光量取决于入射光与表面法线之间的角度。如果你玩过台球,你一定熟悉这个概念;球的方向取决于其与墙壁的入射角度。如果你以 90 度角击打墙壁,球会反弹回来;如果你以非常低的角度击打它,其方向基本不会改变。朗伯模型做出了相同的假设;如果光线以 90 度角击中三角形,所有光线都会被反射回去。角度越低,反射回你的光线就越少。这一概念在以下图中展示:

这个简单概念必须被转换成数学形式。在向量代数中,两个单位向量之间的角度可以通过一个称为点积的运算符来计算。当点积等于零时,两个向量是正交的,这意味着它们形成一个 90 度的角。当它等于一(或负一)时,它们是相互平行的。Cg 有一个名为dot()的函数,它实现了点积的高效计算。
以下图展示了一个光源(太阳)照射在复杂表面上的情况。L表示光线方向(在着色器中称为lightDir)和N是表面的法线。光线以与击中表面的相同角度被反射:

更多关于法线和它们在数学上的含义的信息,请查看:en.wikipedia.org/wiki/Normal_(geometry)
朗伯反射定律简单地将NdotL点积作为光强度的乘法系数:

当N和L平行时,所有光线都会反射回光源,导致几何体看起来更亮。_LightColor0变量包含计算出的光线颜色。
在 Unity 5 之前,光线的强度是不同的。如果你使用基于朗伯模型的旧漫反射着色器,你可能注意到NdotL被乘以了两个:(NdotL * atten * 2),而不是(NdotL * atten)。如果你从 Unity 4 导入自定义着色器,你需要手动进行修正。然而,遗留的着色器已经考虑到这一方面。
当点积为负时,光线来自三角形的对面。对于不透明几何体来说这不是问题,因为不是正对相机的前面的三角形会被裁剪(丢弃)并且不会被渲染。
当你正在原型化你的着色器时,这个基本朗伯模型是一个很好的起点,因为你可以在不担心基本Lighting函数的情况下,完成很多关于编写着色器核心功能的工作。
Unity 已经为我们提供了一个光照模型,该模型已经为您完成了创建 Lambert 光照的任务。如果您查看位于 Unity 安装目录下Data文件夹中的UnityCG.cginc文件,您会注意到您有 Lambert 和 BlinnPhong 光照模型可供使用。当您使用#pragma surface surf Lambert编译着色器时,您正在告诉着色器使用 Unity 在UnityCG.cginc文件中实现的 Lambert Lighting函数,这样您就无需反复编写该代码。我们将在本章后面探讨 BlinnPhong 模型的工作原理。
创建卡通着色器
在游戏中使用最频繁的效果之一是卡通着色,也称为赛璐珞(CEL)着色。这是一种非真实感渲染技术,可以使 3D 模型看起来很平。许多游戏使用它来营造图形是手工绘制的错觉,而不是 3D 建模。您可以在以下图中看到使用卡通着色器(左)和标准着色器(右)渲染的球体:

仅使用表面函数来实现此效果并非不可能,但这将非常昂贵且耗时。实际上,表面函数仅在材质的属性上工作,而不是其实际的照明条件。由于卡通着色需要我们改变光线反射的方式,因此我们需要创建自己的自定义光照模型。
准备工作
让我们从创建一个着色器及其材质并导入一个特殊纹理开始这个配方,如下所示:
- 首先,创建一个新的着色器;在这个例子中,我们将通过在项目选项卡中选择它并按*Ctrl *+ D来复制上一个配方中创建的着色器。我们将把这个新着色器的名字改为ToonShader。
您可以通过在项目窗口中单击名称来重命名一个对象。
- 
为着色器( ToonShaderMat)创建一个新的材质,并将其附加到一个 3D 模型上。卡通着色在曲面上的效果最佳。
- 
此配方需要一个额外的纹理,称为渐变图,它将用于根据接收到的阴影来决定我们想要使用某些颜色的时间: 

- 本书在第四章|纹理文件夹中提供了一个示例纹理。如果您决定导入自己的纹理,重要的是您要选择您的下一个纹理,并在检查器选项卡中,将渐变图的 Wrap 模式更改为 Clamp。如果您想要颜色之间的边缘清晰,过滤器模式也应设置为 Point:

本书附带的项目示例已经完成了这一步,在Assets | 第四章 | 纹理| ToonRamp文件中,但在继续之前验证这一点是个好主意。
如何操作...
通过对着色器进行以下更改,可以实现卡通美学:
- 为一个名为_RampTex的纹理添加一个新属性:
_RampTex ("Ramp", 2D) = "white" {} 
- 在CGPROGRAM部分添加其相关变量:
sampler2D _RampTex; 
- 修改#pragma指令,使其指向名为LightingToon()的函数:
#pragma surface surf Toon 
- 将LightingSimpleLambert函数替换为以下函数:
fixed4 LightingToon (SurfaceOutput s, fixed3 lightDir, 
            fixed atten) 
{ 
  // First calculate the dot product of the light direction and the 
  // surface's normal
  half NdotL = dot(s.Normal, lightDir); 
  // Remap NdotL to the value on the ramp map
  NdotL = tex2D(_RampTex, fixed2(NdotL, 0.5)); 
  // Next, set what color should be returned
  half4 color; 
  color.rgb = s.Albedo * _LightColor0.rgb * (NdotL * atten ); 
  color.a = s.Alpha; 
  // Return the calculated color
  return color; 
} 
- 保存脚本,打开ToonShaderMat,并将Ramp属性分配给你的渐变图。如果一切顺利,你应该能在场景中看到以下效果:

这种效果可能会受到场景中光照的影响。你可以通过转到窗口 | 光照 | 设置,并更改环境 | 环境光照 | 强度乘数属性为0来改变场景的照明。
它是如何工作的...
卡通着色的主要特征是光照的渲染方式;表面不是均匀着色。为了实现这种效果,我们需要一个渐变图。它的目的是将 Lambertian 光照强度NdotL重新映射到另一个值。使用没有渐变的渐变图,我们可以强制光照以步骤的形式渲染。以下图表显示了如何使用渐变图来校正光照强度:

还有更多...
有许多不同的方法可以实现卡通着色效果。使用不同的渐变图可以显著改变模型的外观,因此你应该进行实验以找到最佳方案。
与渐变纹理的替代方案是固定光照强度NdotL,使其只能假设从0到1等距采样的特定数值:
half4 LightingCustomLambert (SurfaceOutput s, half3 lightDir, 
                half3 viewDir, half atten) 
{ 
  half NdotL = dot (s.Normal, lightDir); 
  // Snap instead
  half cel = floor(NdotL * _CelShadingLevels) / 
             (_CelShadingLevels - 0.5); 
  // Next, set what color should be returned
  half4 color; 
  color.rgb = s.Albedo * _LightColor0.rgb * (cel * atten ); 
  color.a = s.Alpha; 
  // Return the calculated color
  return color; 
} 
为了将数字固定,我们首先将NdotL乘以_CelShadingLevels变量,通过floor函数将结果四舍五入到整数,然后再除以它。这种舍入是通过floor函数完成的,它将有效地从数字中去除小数点。通过这样做,cel数量被迫采用从0到1的_CelShadingLevels等距值之一。这消除了对渐变纹理的需求,并使所有颜色步骤的大小相同。如果你正在寻求这种实现方式,请记住在你的着色器中添加一个名为_CelShadingLevels的属性。你可以在本章的示例代码中找到一个例子。尝试拖动级别属性,看看它如何影响截图的显示:

创建 Phong 高光类型
物体表面的光泽度简单描述了它有多亮。这类效果在着色器世界中通常被称为视依赖效果。这是因为,为了在着色器中实现逼真的镜面反射效果,你需要包含相机或用户面对物体表面的方向。最基本的、性能友好的镜面反射类型是 Phong 镜面反射效果。它是计算光线从表面反射回来的方向与用户的视图方向相比的结果。它是在许多应用中非常常见的镜面反射模型,从游戏到电影。虽然它不是在准确模拟反射镜面方面最逼真的,但它提供了对预期光泽度的良好近似,在大多数情况下表现良好。此外,如果你的物体离相机更远,并且不需要非常精确的镜面反射,这是一种为着色器提供镜面反射效果的好方法。
在这个食谱中,我们将介绍如何实现着色器的逐顶点版本以及使用表面着色器Input结构中的新参数实现的逐像素版本。我们将看到这两种不同实现之间的差异,并讨论在不同情况下何时以及为什么使用这些不同的实现。
准备工作
要开始这个食谱,请执行以下步骤:
- 
创建一个新的着色器( Phong)、材质(PhongMat),以及一个包含球体和其下方的平面(GameObject | 3D Objects | Plane)的新场景。
- 
将着色器附加到材质上,并将材质附加到物体上。为了完成你的新场景,如果还没有,创建一个新的方向光,这样你就可以在编写代码时看到你的镜面反射效果: 

如何操作...
按照以下步骤创建 Phong 光照模型:
- 你可能已经看到了一个模式,但我们总是喜欢从着色器编写过程的最基本部分开始:属性的创建。因此,让我们从SubShader块中移除所有当前属性及其定义,然后添加以下属性到着色器中:
Properties 
{ 
  _MainTint ("Diffuse Tint", Color) = (1,1,1,1) 
  _MainTex ("Base (RGB)", 2D) = "white" {} 
  _SpecularColor ("Specular Color", Color) = (1,1,1,1) 
  _SpecPower ("Specular Power", Range(0,30)) = 1 
} 
- 我们必须确保将相应的变量添加到我们的SubShader{}块中的CGPROGRAM块中:
float4 _SpecularColor; 
sampler2D _MainTex; 
float4 _MainTint; 
float _SpecPower; 
- 现在,我们必须添加我们的自定义光照模型,以便我们可以计算自己的 Phong 镜面反射。如果现在还不理解,请不要担心;我们将在本食谱的如何工作...部分中逐行解释代码。将以下代码添加到着色器的SubShader{}函数中:
fixed4 LightingPhong (SurfaceOutput s, fixed3 lightDir, 
                      half3 viewDir, fixed atten) 
{ 
  // Reflection 
  float NdotL = dot(s.Normal, lightDir); 
  float3 reflectionVector = normalize(2.0 * s.Normal * 
     NdotL - lightDir); 
  // Specular 
  float spec = pow(max(0, dot(reflectionVector, viewDir)), 
     _SpecPower); 
  float3 finalSpec = _SpecularColor.rgb * spec; 
  // Final effect 
  fixed4 c; 
  c.rgb = (s.Albedo * _LightColor0.rgb * max(0,NdotL) * 
     atten) + (_LightColor0.rgb * finalSpec); 
  c.a = s.Alpha; 
  return c; 
} 
- 接下来,我们必须告诉CGPROGRAM块它需要使用我们的自定义光照函数而不是内置函数之一。我们通过将#pragma语句更改为以下内容来实现这一点:
CGPROGRAM 
#pragma surface surf Phong 
- 最后,让我们更新surf函数为以下内容:
void surf (Input IN, inout SurfaceOutput o) 
{
 half4 c = tex2D (_MainTex, IN.uv_MainTex) * _MainTint;
 o.Albedo = c.rgb;
 o.Alpha = c.a;
}
- 以下截图展示了我们使用自定义反射向量的自定义 Phong 光照模型的结果:

尝试更改光滑功率属性并注意你看到的效果。
它是如何工作的...
让我们单独分解照明函数,因为到目前为止,其余的着色器应该对你来说相当熟悉。
在之前的配方中,我们使用了一个只提供光方向的照明函数,lightDir。
Unity 提供了一套你可以使用的照明函数,包括一个提供视方向的函数,viewDir。
要了解如何编写自己的自定义照明模式,请参考以下表格,将 NameYouChoose 替换为你在 #pragma 语句中给出的照明函数名称,或访问 docs.unity3d.com/Documentation/Components/SL-SurfaceShaderLighting.html 获取更多详细信息:
| 非视依赖 | `half4 LightingNameYouChoose` (`SurfaceOutput s`, `half3` `lightDir`, `half atten`); | 
|---|---|
| 视依赖 | half4 LightingNameYouChoose (SurfaceOutput s, half3 lightDir, half3 viewDir, half atten); | 
在我们的情况下,我们使用的是光滑着色器,因此我们需要有视依赖的 Lighting 函数结构。我们必须编写以下内容:
CPROGRAM 
#pragma surface surf Phong 
fixed4 LightingPhong (SurfaceOutput s, fixed3 lightDir, half3 viewDir, fixed atten) 
{ 
  // ... 
} 
这将告诉着色器我们想要创建自己的视依赖着色器。始终确保你的照明函数名称在 Lighting 函数声明和 #pragma 语句中相同,否则 Unity 将无法找到你的照明模型。
在以下图像中描述了在 Phong 模型中起作用的分量。我们有线方向 L(与其完美的反射 R 相关联)和法线方向 N。它们在之前遇到的 Lambertian 模型中都已经出现过,除了 V,它是视方向:

Phong 模型假设反射表面的最终光强度由两个分量给出,其漫反射颜色和光滑值,如下所示:

漫反射分量 D 从 Lambertian 模型保持不变:

光滑分量 S 定义如下:

在这里,p 是在着色器中定义为 _SpecPower 的光滑功率。唯一未知的参数是 R,它是根据 N 对 L 的反射。在向量代数中,这可以计算如下:

这正是以下计算的内容:
float3 reflectionVector = normalize(2.0 * s.Normal * NdotL - 
                                    lightDir);
这会使法线向光源弯曲;作为一个顶点,法线是指向远离光源的,它被迫看向光源。请参考以下图表以获得更直观的表示。产生此调试效果的脚本包含在此书的支持页面上,www.packtpub.com/books/content/support:

以下图表显示了我们在着色器中进行的最终 Phong 光滑计算的最终结果:

创建 BlinnPhong 镜面类型
Blinn 是另一种更高效的计算和估计镜面反射的方法。它是通过获取视图方向和光方向的半向量来实现的。这种方法是由 Jim Blinn 引入到 Cg 领域的。他发现,直接获取半向量比计算我们自己的反射向量要高效得多。这减少了代码和处理的耗时。如果你实际查看 UnityCG.cginc 文件中包含的内置 BlinnPhong 照明模型,你会发现它也在使用半向量,因此得名 BlinnPhong。它只是完整 Phong 计算的一个简化版本。
准备工作
要开始这个配方,请执行以下步骤:
- 这次,我们不是创建一个全新的场景,而是通过使用文件 | 保存场景为...,并创建一个新的着色器(BlinnPhong)和材质(BlinnPhongMat)来使用我们已有的对象和场景:

- 一旦你有了一个新的着色器,双击它以启动你选择的 IDE,这样你就可以开始编辑你的着色器了。
如何实现...
执行以下步骤以创建 BlinnPhong 照明模型:
- 首先,让我们在 SubShader块中删除所有当前属性及其定义。然后我们需要在Properties块中添加我们自己的属性,以便我们可以控制镜面高光的视觉效果:
Properties 
{ 
  _MainTint ("Diffuse Tint", Color) = (1,1,1,1) 
  _MainTex ("Base (RGB)", 2D) = "white" {} 
  _SpecularColor ("Specular Color", Color) = (1,1,1,1) 
  _SpecPower ("Specular Power", Range(0.1,60)) = 3 
} 
- 然后,我们需要确保我们在 CGPROGRAM块中创建了相应的变量,以便我们可以从我们的Properties块中访问数据,在我们的子着色器中:
sampler2D _MainTex; 
float4 _MainTint; 
float4 _SpecularColor; 
float _SpecPower; 
- 现在是时候创建我们自定义的照明模型,该模型将处理我们的漫反射和镜面计算。代码如下:
fixed4 LightingCustomBlinnPhong (SurfaceOutput s, 
                  fixed3 lightDir, 
                  half3 viewDir, 
                  fixed atten) 
{ 
  float NdotL = max(0,dot(s.Normal, lightDir)); 
  float3 halfVector = normalize(lightDir + viewDir); 
  float NdotH = max(0, dot(s.Normal, halfVector)); 
  float spec = pow(NdotH, _SpecPower) * _SpecularColor; 
  float4 color; 
  color.rgb = (s.Albedo * _LightColor0.rgb * NdotL) + 
        (_LightColor0.rgb * _SpecularColor.rgb * spec) * atten; 
  color.a = s.Alpha; 
  return color; 
} 
- 然后更新 surf函数为以下内容:
void surf (Input IN, inout SurfaceOutput o) 
{
 half4 c = tex2D (_MainTex, IN.uv_MainTex) * _MainTint;
 o.Albedo = c.rgb;
 o.Alpha = c.a;
}
- 为了完成我们的着色器,我们需要告诉我们的 CGPROGRAM 块使用我们自定义的照明模型而不是内置的一个,通过修改 #pragma语句并使用以下代码:
CPROGRAM 
#pragma surface surf CustomBlinnPhong 
- 以下截图展示了我们的 BlinnPhong 照明模型的结果:

它是如何工作的...
BlinnPhong 镜面几乎与 Phong 镜面完全相同,只是它更高效,因为它使用更少的代码就能达到几乎相同的效果。在基于物理的渲染引入之前,这种方法是 Unity 4 中镜面反射的默认选择。
计算反射向量 R 通常很昂贵。BlinnPhong 镜面将其替换为视图方向 V 和光方向 L 之间的半向量 H:

我们不是计算我们自己的反射向量,而是简单地获取视图方向和光方向之间的向量,基本上模拟反射向量。实际上已经发现,这种方法比之前的方法在物理上更准确,但我们认为有必要向你展示所有可能性:

根据向量代数,半向量可以按以下方式计算:

这里 |V+L| 是向量 V+L 的长度。在 Cg 中,我们只需将视向和光向相加,然后将结果归一化到单位向量:
float3 halfVector = normalize(lightDir + viewDir); 
然后,我们只需将顶点法线与这个新的半向量点积,以获得我们的主要镜面值。之后,我们只需将其乘以_SpecPower的幂,并乘以镜面颜色变量。这在代码和数学上要简单得多,但仍然给我们一个很好的镜面高光,适用于许多实时场景。
参见
本章中看到的光模型非常简单;没有真实材料是完美哑光或完美镜面的。此外,对于如衣物、木材和皮肤等复杂材料,了解光线在表面下层的散射方式是常见的。使用以下表格来回顾迄今为止遇到的不同光照模型:
| 技术 | 类型 | 光照强度 (I) | 
|---|---|---|
| 拉姆伯特 | 漫反射 |  | 
| Phong | 镜面 |   | 
| BlinnPhong | 镜面 |   | 
还有其他有趣的模型,例如用于粗糙表面的 Oren-Nayar 光照模型:en.wikipedia.org/wiki/Oren%E2%80%93Nayar_reflectance_model.
创建各向异性镜面类型
各向异性是一种模拟表面凹槽方向性的镜面或反射类型,并在垂直方向上修改/拉伸镜面。当你想要模拟刷漆金属时,它非常有用,而不是那种表面清晰、光滑和抛光的金属。想象一下,当你看 CD 或 DVD 的数据面时看到的镜面,或者锅底或平底锅底部镜面的形状。你会注意到,如果你仔细检查表面,凹槽有一个方向,通常是金属刷的方向。当你将镜面应用于这个表面时,你会得到一个在垂直方向上拉伸的镜面。
本食谱将向您介绍增强您的镜面高光以实现不同类型刷漆表面的概念。在未来的食谱中,我们将探讨如何使用本食谱中的概念来实现其他效果,例如拉伸反射和毛发,但在这里,你将首先学习这项技术的根本。我们将使用这个着色器作为我们自定义各向异性着色器的参考:
wiki.unity3d.com/index.php?title=Anisotropic_Highlight_Shader.
以下图表显示了使用 Unity 中的各向异性着色器可以实现的多种不同镜面效果示例:

准备工作
让我们从创建一个着色器、其材质以及场景中的灯光开始这个食谱:
- 
创建一个新的场景,其中包含一些对象和光源,以便我们可以直观地调试我们的着色器。在这种情况下,我们将使用一些胶囊、一个球体和一个圆柱体。 
- 
然后创建一个新的着色器和材质,并将它们连接到您的对象上: 

- 
最后,我们需要一种类型的法线图,以指示我们的各向异性高光的方向性。 
- 
以下截图显示了我们将用于此菜谱的各向异性法线图。它可以从本书的支持页面 www.packtpub.com/books/content/support获取:

如何操作...
要创建各向异性效果,我们需要对之前创建的着色器进行以下更改:
- 我们首先需要删除旧属性,然后添加我们将需要用于着色器的属性。这些将允许对表面最终外观进行大量艺术控制:
Properties 
{ 
  _MainTint ("Diffuse Tint", Color) = (1,1,1,1) 
  _MainTex ("Base (RGB)", 2D) = "white" {} 
  _SpecularColor ("Specular Color", Color) = (1,1,1,1) 
  _Specular ("Specular Amount", Range(0,1)) = 0.5 
  _SpecPower ("Specular Power", Range(0,1)) = 0.5 
  _AnisoDir ("Anisotropic Direction", 2D) = "" {} 
  _AnisoOffset ("Anisotropic Offset", Range(-1,1)) = -0.2 
} 
- 
然后,我们需要在 Properties块和我们的 SubShader{}块,以便我们可以使用Properties块提供的数据:
sampler2D _MainTex; 
sampler2D _AnisoDir; 
float4 _MainTint; 
float4 _SpecularColor; 
float _AnisoOffset; 
float _Specular; 
float _SpecPower; 
- 现在我们可以创建我们的Lighting函数,该函数将在我们的表面上产生正确的各向异性效果。我们将使用以下代码来完成此操作:
fixed4 LightingAnisotropic(SurfaceAnisoOutput s, fixed3 
   lightDir, half3 viewDir, fixed atten) 
{ 
  fixed3 halfVector = normalize(normalize(lightDir) + 
     normalize(viewDir)); 
  float NdotL = saturate(dot(s.Normal, lightDir)); 
  fixed HdotA = dot(normalize(s.Normal + s.AnisoDirection), 
     halfVector);  float aniso = max(0, sin(radians((HdotA + _AnisoOffset) * 
     180)));  float spec = saturate(pow(aniso, s.Gloss * 128) * 
     s.Specular); 
  fixed4 c; 
  c.rgb = ((s.Albedo * _LightColor0.rgb * NdotL) + 
     (_LightColor0.rgb * _SpecularColor.rgb * spec)) * 
     atten; 
  c.a = s.Alpha; 
  return c; 
} 
- 为了使用这个新的Lighting函数,我们需要告诉子着色器的#pragma语句去寻找它,而不是使用内置的Lighting函数之一:
CGPROGRAM 
#pragma surface surf Anisotropic 
- 我们还在struct Input中声明了以下代码,为各向异性法线图赋予了它自己的 UV。这并不是完全必要的,因为我们可以直接使用主纹理的 UV,但这样我们可以独立控制刷漆金属效果的重叠,以便我们可以将其缩放到任何大小:
struct Input  
{ 
  float2 uv_MainTex; 
  float2 uv_AnisoDir; 
}; 
- 我们还需要添加struct SurfaceAnisoOutput:
struct SurfaceAnisoOutput 
{ 
  fixed3 Albedo; 
  fixed3 Normal; 
  fixed3 Emission; 
  fixed3 AnisoDirection; 
  half Specular; 
  fixed Gloss; 
  fixed Alpha; 
}; 
- 最后,我们需要使用surf()函数将正确的数据传递给我们的Lighting函数。因此,我们将从我们的各向异性法线图中获取每像素信息,并将我们的高光参数设置如下:
void surf(Input IN, inout SurfaceAnisoOutput o) 
{ 
  half4 c = tex2D(_MainTex, IN.uv_MainTex) * _MainTint; 
  float3 anisoTex = UnpackNormal(tex2D(_AnisoDir, 
     IN.uv_AnisoDir)); 
  o.AnisoDirection = anisoTex; 
  o.Specular = _Specular; 
  o.Gloss = _SpecPower; 
  o.Albedo = c.rgb; 
  o.Alpha = c.a; 
} 
- 保存您的脚本并返回到 Unity 编辑器。选择AnisotropicMat材质,并将各向异性方向属性分配给我们在本菜谱的“准备”部分中提到的纹理。之后,使用滑块调整各向异性偏移属性,并注意变化。
各向异性法线图使我们能够给表面赋予方向,并帮助我们分散表面上的高光。以下截图展示了我们的各向异性着色器的结果:

它是如何工作的...
让我们将这个着色器分解为其核心组件,并解释我们为什么会得到这种效果。我们在这里主要会涵盖自定义光照函数,因为到目前为止,着色器的其余部分应该相当容易理解。
我们首先声明自己的SurfaceAnisoOutput结构体。我们需要这样做是为了从各向异性法线图中获取每个像素的信息,而在表面着色器中,我们唯一能够做到这一点的方法是在surf()函数中使用tex2D()函数。以下代码展示了我们在着色器中使用的自定义表面输出结构:
struct SurfaceAnisoOutput 
{ 
  fixed3 Albedo; 
  fixed3 Normal; 
  fixed3 Emission; 
  fixed3 AnisoDirection; 
  half Specular; 
  fixed Gloss; 
  fixed Alpha; 
}; 
我们可以使用SurfaceAnisoOutput结构体作为光照函数和表面函数之间交互的一种方式。在我们的情况下,我们在surf()函数中将每个像素的纹理信息存储在名为anisoTex的变量中,然后通过将数据存储在AnisoDirection变量中来将其传递给SurfaceAnisoOutput结构体。一旦我们有了这个,我们就可以使用Lighting函数中的s.AnisoDirection来使用每个像素的信息。
在设置好这种数据连接后,我们可以继续进行实际的光照计算。这首先是通过获取通常的半向量来开始的,这样我们就不必进行完整的反射计算和漫反射光照,即顶点法线与光向量或方向的点乘。这是在 Cg 中使用以下行完成的:
fixed3 halfVector = normalize(normalize(lightDir) + 
                    normalize(viewDir)); 
float NdotL = saturate(dot(s.Normal, lightDir)); 
然后,我们开始对 Specular 进行实际修改以获得正确的视觉效果。我们首先将顶点法线与每个像素向量的归一化总和与上一步计算的halfVector进行点乘。这给我们一个浮点值,当表面法线与halfVector平行时,其值为1,而当它垂直时,其值为0。最后,我们使用sin()函数修改这个值,以便我们基本上可以得到一个较暗的中间高光,并最终基于halfVector得到一个环状效果。所有之前提到的操作都在以下两行 Cg 代码中总结:
fixed HdotA = dot(normalize(s.Normal + s.AnisoDirection), 
                  halfVector); 
float aniso = max(0, sin(radians((HdotA + _AnisoOffset) * 180))); 
最后,我们通过将其提升到s.Gloss的幂来缩放aniso值的效果,然后通过乘以s.Specular来全局降低其强度:
float spec = saturate(pow(aniso, s.Gloss * 128) * s.Specular); 
这种效果非常适合创建更高级的金属类型表面,特别是那些刷过并且看起来有方向性的表面。它也适用于头发或任何有方向性的软表面。以下截图显示了显示最终各向异性光照计算的最终结果:

第五章:基于物理的渲染
Unity 5 中引入的 PBR 是一种着色模型,它试图以与真实世界中光的行为相似的方式渲染图形。前几章反复提到了它,但没有透露太多细节。如果你想了解 PBR 的工作原理,以及如何充分利用它,那么你应该阅读这一章。在这一章中,你将学习以下配方:
- 
理解金属设置 
- 
向 PBR 添加透明度 
- 
创建镜子和反射表面 
- 
在场景中烘焙灯光 
简介
在第四章“理解光照模型”中遇到的所有光照模型,都是对光的行为的非常原始的描述。在制作过程中最重要的方面是效率。实时着色成本高昂,Lambertian 或 BlinnPhong 等技术是在计算成本和真实感之间的一种折衷。
更强大的 GPU 使我们能够编写越来越复杂的照明模型和渲染引擎,目的是模拟光的真实行为。简而言之,这就是 PBR 背后的哲学。正如其名所示,它试图尽可能接近赋予每种材料独特外观的过程背后的物理原理。尽管如此,PBR 这个术语在营销活动中被广泛使用,它更多的是最先进渲染的同义词,而不是一个定义明确的技巧。
Unity 实现 PBR 的两种主要方式:
- 
第一种是一个全新的光照模型(称为标准)。表面着色器允许开发者指定材料的物理属性,但它们不对它们施加实际的物理约束。PBR 通过使用强制执行物理原则(如能量守恒(一个物体不能反射比它接收到的光更多的光)、微表面散射(粗糙表面与光滑表面相比,反射光更不规则)、菲涅耳反射率(在掠射角处出现镜面反射)、和表面遮挡(难以照亮的角落和其他几何形状的变暗)的光照模型来填补这一空白)。所有这些方面以及许多其他方面都被用来计算标准光照模型。 
- 
PBR(物理基础渲染)如此逼真的第二个方面被称为全局照明(GI),它是对基于物理的光线传输的模拟。这意味着场景中的物体不是作为独立的实体被绘制出来。它们都贡献于最终的渲染,因为光线可以在击中其他物体之前反射在其上。这一方面在着色器本身中并未捕捉到,但它是渲染引擎工作方式的一个基本部分。不幸的是,在实时中准确模拟光线如何在表面上实际反弹超出了现代 GPU 的能力。Unity 进行了一些巧妙的优化,使我们能够在不牺牲性能的情况下保持视觉保真度。然而,一些最先进的技术(如反射)则需要用户输入。 
本章将涵盖所有这些方面。重要的是要记住,PBR 和 GI 并不自动保证你的游戏将是照片般的真实。实现照片般的真实是一个极具挑战性的任务,就像每一种艺术一样,它需要极大的专业知识和非凡的技能。
理解金属设置
Unity 提供了三种不同的 PBR 着色器;在材质的检查器标签页的下拉菜单中,它们被称为标准、标准(粗糙度设置)和标准(光泽度设置)。主要区别在于,标准和标准(粗糙度设置)暴露了金属属性,但标准包含一个平滑度属性,而第二个用粗糙度替换了平滑度****。**标准(光泽度设置)包含平滑度,但用光泽度替换了金属属性。平滑度和粗糙度是彼此的相反,所以1平滑度意味着0粗糙度,反之亦然。您通常可以使用任何着色器得到相同的结果,所以这主要取决于个人偏好。
这些设置代表了你可以初始化 PBR 材料的不同方式。推动 PBR 的一个概念是提供有意义的、与物理相关的属性,艺术家和开发者可以调整和玩耍。某些材料的属性更容易表示,表明它们有多金属。对于其他材料,更重要的是指定它们如何通过其光泽度直接反射光线。这个配方将向您展示如何有效地使用金属****设置。重要的是要记住,金属工作流程不仅适用于金属材质;它是一种定义材料将如何根据其表面是金属还是非金属来呈现的方式。尽管作为两种不同类型的着色器呈现,但金属和光泽度设置通常具有同等的表现力。如 Unity 文档docs.unity3d.com/Manual/StandardShaderMetallicVsSpecular.html中所示,以及前面提到的,相同的材料通常可以用这两种设置重新创建(见以下截图):

准备工作
此配方将使用标准着色器,因此无需创建新的着色器。开始配方的步骤如下:
- 
创建一个新的材质( MetallicMat)。
- 
从其检查器中,确保从 Shader 下拉菜单中选择了 Standard。 
- 
你还需要一个纹理化的 3D 模型。我们之前使用的基本角色将完美适用。将其拖放到场景中。之后,将 MetallicMat材质拖放到角色的各个部分上。同时,将材质的纹理分配给 Albedo 属性:

如何做到这一点...
在标准着色器中需要配置两种主要纹理:Albedo 和 Metallic。为了有效地使用金属工作流程,我们需要正确初始化这些图:
- 
Albedo 图应该使用 3D 模型的未光照纹理初始化。 
- 
要创建 Metallic 图,首先复制你的 Albedo 图文件。你可以通过从项目选项卡中选择图并按Ctrl + D来实现。 
- 
使用白色( #ffffff)为对应纯金属材质的区域着色。对于所有其他颜色使用黑色(#000000)。灰色阴影用于灰尘、风化或磨损的金属表面、锈迹、划痕油漆等。事实上,Unity 只使用红色通道来存储金属值;绿色和蓝色通道被忽略。
- 
使用图像的 alpha 通道来提供关于材料平滑度的信息: 

在 Photoshop 中打开的 Metallic 图示例
对于我们的简单角色,腰带和连帽衫的小末端是我们需要金属化的唯一部分。我还将主要角色的不透明度设置为 55%,腰带的不透明度更高,为 80%:
- 将 Metallic 图分配给材质。当这两个属性现在由图控制时,Metallic 滑块将消失。你可以使用 Smoothness 滑块来提供对提供的图的修饰:

它是如何工作的...
金属因其导电性而闻名;光以电磁波的形式存在,这意味着几乎所有金属与非导体(通常称为绝缘体)相比都有相似的行为。导体倾向于反射大多数光子(70-100%),导致高反射率。剩余的光被吸收,而不是扩散,这表明导体具有非常暗的扩散分量。相反,绝缘体的反射率很低(4%);其余的光在表面散射,有助于它们的扩散外观。
在标准着色器中,纯金属材质具有暗淡的漫反射组件,其镜面反射的颜色由 Albedo 图决定。相反,纯非金属材质的漫反射组件由 Albedo 图决定;其镜面高光的颜色由入射光的颜色决定。遵循这些原则允许金属工作流程将 Albedo 和镜面反射合并到 Albedo 图中,强制执行物理准确的特性。这也允许节省更多空间,从而在牺牲对材质外观控制的情况下显著提高速度。
相关内容
关于金属设置更多信息,您可以参考以下链接:
- 
校准图:如何校准金属材质( blogs.unity3d.com/wp-content/uploads/2014/11/UnityMetallicChart.png)
- 
材质图:如何初始化标准着色器参数以用于常见材质( docs.unity3d.com/Manual/StandardShaderMaterialCharts.html)
- 
Quixel MEGASCANS:一个包含纹理和 PBR 参数的庞大材料库( megascans.se/)
- 
PBR 纹理转换:如何将传统着色器转换为 PBR 着色器( www.marmoset.co/toolbag/learn/pbr-conversion)
- 
Substance Designer:一个基于节点的软件,用于处理 PBR( www.allegorithmic.com/products/substance-designer)
- 
基于物理渲染的理论:关于 PBR 的完整指南( www.allegorithmic.com/pbr-guide)
为 PBR 添加透明度
透明度在游戏中是一个非常重要的方面,标准着色器支持三种不同的实现方式。如果您需要具有透明或半透明特性的真实材料,这个配方非常有用。眼镜、瓶子、窗户和晶体是 PBR 透明着色器的良好候选者。这是因为您仍然可以通过添加透明或半透明效果来获得 PBR 引入的所有真实感。如果您需要为 UI 元素或像素艺术等不同事物实现透明度,第三章中“创建透明材料”配方中探讨了更有效的替代方案,表面着色器和纹理映射。
为了拥有一个透明的标准材质,仅更改其 Albedo 颜色属性的 alpha 通道是不够的。除非您正确设置其渲染模式,否则您的材质将不会显示为透明。
准备工作
本配方将使用标准着色器,因此无需创建新的着色器:
- 
创建一个新的材质( TransparencyMat)。
- 
确保从材质的检查器标签页将 Shader 属性设置为标准或标准(镜面设置)。 
- 
将新创建的材质分配给您想要设置为透明的 3D 对象: 

如何实现...
标准着色器提供了三种不同类型的透明度。尽管它们非常相似,但它们有细微的差别,适用于不同的环境。
半透明材料
一些材料,如透明塑料、晶体和玻璃是半透明的。这意味着它们都需要 PBR 的所有真实效果(如镜面高光和菲涅耳折射和反射),但允许看到附着在材质上的物体背后的几何体。如果您需要这样做,请执行以下步骤:
- 
从材质的检查器标签页,将渲染模式设置为透明。 
- 
透明度的大小由 Albedo 颜色的 alpha 通道或 Albedo 贴图(如果有)决定。如果您点击 Albedo 部分右侧的框,会弹出一个颜色菜单。调整 A 通道会使项目更可见或更不可见: 

- 将 A 通道设置为44会产生以下效果:

- 以下截图显示了 Unity 校准场景中的四个不同的高光塑料球体。从左到右,它们的透明度逐渐增加。最后一个球体是完全透明的,但保留了所有添加的 PBR 效果:

Unity 校准场景可以从 Asset Store 免费下载,网址为www.assetstore.unity3d.com/en/#!/content/25422。
透明渲染模式非常适合窗户、瓶子、宝石和耳机。
您应该注意到,许多透明材质通常不会投射阴影。除此之外,材质的金属和光滑度属性可能会干扰透明效果。具有类似镜面的表面可以将 alpha 设置为零,但如果它反射所有入射光,它就不会看起来透明。
消失的物体
有时,您希望物体通过渐变效果完全消失。在这种情况下,镜面反射和菲涅耳折射和反射也应该消失。当一个渐变物体完全透明时,它也应该不可见。为此,请执行以下步骤:
- 
从材质的检查器标签页,将渲染模式设置为渐变。 
- 
如前所述,使用 Albedo 颜色或贴图的 alpha 通道来确定最终的透明度: 

- 以下截图显示了四个渐变球体。从截图可以看出,PBR 效果随着球体的渐变而消失。正如您在以下图像中看到的那样,最右边的一个几乎看不见:

- 这种渲染模式最适合非真实物体,如全息图、激光束、假灯、鬼魂和粒子效果。
带孔的实体几何体
在游戏中遇到的许多材质都是实心的,这意味着它们不允许光线穿透。同时,许多物体具有非常复杂(但平坦)的几何形状。用 3D 对象建模树叶和草地通常是过度设计。一个更有效的方法是使用带有树叶纹理的四边形(矩形)。虽然树叶本身是实心的,但纹理的其余部分应该是完全透明的。如果你想要这样,请执行以下步骤:
- 
从材质的“检查器”选项卡中,将渲染模式设置为“剪裁”。 
- 
使用 Alpha 截止滑块确定截止阈值。所有 Albedo 图中 Alpha 值等于或小于 Alpha 截止的像素将被隐藏。 
以下图像来自 Unity 官方教程中的 PBR(www.youtube.com/watch?v=fD_ho_ofY6A),展示了如何使用剪裁渲染模式的效果在几何形状中创建一个洞:

值得注意的是,剪裁不允许看到几何形状的背面。在先前的例子中,你无法看到球体的内部体积。如果你需要这种效果,你需要创建自己的着色器并确保背面几何形状不被裁剪。
参见
- 
如前所述,本食谱中的一些示例是使用 Unity Shader Calibration Scene 创建的,该场景在 Asset Store 中免费提供,链接为 www.assetstore.unity3d.com/en/#!/content/25422.
- 
更多关于反照率和透明度的信息可以在以下找到 
创建镜子和反射表面
当从特定角度观察物体时,镜面材质会反射光线。不幸的是,即使是 Fresnel 反射,这是最准确模型之一,也无法正确地反射来自附近物体的光线。前几章中检查的光照模型只考虑了光源,但忽略了来自其他表面的反射光线。根据你到目前为止对着色器的了解,制作镜子是根本不可能的。
通过提供关于其周围环境的信息,全局照明使得使用 PBR 着色器成为可能。这使得物体不仅具有镜面高光,还有真实的反射,这些反射取决于周围的物体。实时反射非常昂贵,并且需要手动设置和调整才能工作。当正确设置时,它们可以用来创建类似镜面的表面,如下面的图中所示:

准备工作
本食谱不会介绍任何新的着色器。相反,大部分工作都是在编辑器中直接完成的。请执行以下步骤:
- 
创建一个新的场景。 
- 
创建一个四边形(GameObject | 3D Object | Quad),它将作为镜子。我已经将它绕 Y 轴旋转了-65 度,以便更容易看到。 
- 
创建一个新的材质( MirrorMat)并将其附加到镜子上。
- 
将四边形放置在包含其他物体的场景中。 
- 
从 GameObject | Light | Reflection Probe 创建一个新的反射探针并将其放置在四边形前方: 

如何实现...
如果前面的步骤都正确执行,你应该在场景中间有一个四边形,靠近反射探针。为了使其成为镜子,需要进行一些更改:
- 
将材质的着色器更改为 Standard,并将渲染模式更改为不透明。 
- 
将其金属和光滑度属性更改为一个。你应该看到材质更清晰地反射天空。 
- 
选择反射探针并调整其大小和探针原点,直到它位于四边形前方并包围所有你想要反射的物体。 
- 
为了在 Cubemap 捕获设置下使项目更清晰,将分辨率更改为 2048。
- 
最后,将其类型更改为实时并刷新模式更改为每帧。同时,确保清除遮罩设置为一切。 
- 
你的反射探针应该配置如下所示: 

- 使用这些设置,你应该看到类似以下内容:

你可能会注意到,在反射中兔子看起来比它旁边的物体要大。如果你的探头用于真实镜子,你应该检查“盒投影”标志(在这个例子中,将盒子大小设置为1,1,1可以很好地模拟镜子的效果)。如果它用于其他反射表面,例如闪亮的金属片或玻璃桌面,你可以取消选中它。
它是如何工作的...
当着色器需要关于其周围环境的信息时,它通常在一个称为立方体贴图的结构中提供。它们在第二章,“创建你的第一个着色器”中简要提到,作为着色器属性类型之一,包括Color,2D,Float和Vector。简单来说,立方体贴图是 2D 纹理的 3D 等价物;它们代表从中心点看到的 360 度世界视图。
Unity 使用球形投影预览立方体贴图,如下面的图所示:

当立方体贴图与相机一起使用时,它们被称为天空盒,因为它们用于提供反射天空的方法。它们可以用来反射实际场景中不存在的几何形状,例如星云、云彩和星星。
它们被称为立方体贴图是因为它们的创建方式:立方体贴图由六个不同的纹理组成,每个纹理都附着在立方体的一个面上。你可以手动创建立方体贴图或将任务委托给反射探针。你可以想象反射探针是一组六个相机,创建周围区域的 360 度映射。这也解释了为什么探针如此昂贵。通过在我们的场景中创建一个探针,我们让 Unity 知道哪些对象在镜子周围。如果你需要更多的反射表面,你可以添加多个探针。对于反射探针的工作,你无需采取任何进一步的操作。标准着色器将自动使用它们。
你应该注意到,当它们设置为实时时,它们会在每一帧的开始渲染立方体贴图。有一个技巧可以使这个过程更快;如果你知道你想要反射的几何形状部分不会移动,你可以烘焙反射。这意味着 Unity 可以在游戏开始前计算反射,从而允许更精确(且计算成本更高)的计算。为了做到这一点,你的反射探针必须设置为烘焙,并且仅适用于标记为静态的对象。静态对象不能移动或改变,这使得它们非常适合地形、建筑和道具。每次移动静态对象时,Unity 都会为其烘焙的反射探针重新生成立方体贴图。这可能会花费几分钟到几个小时。
你可以将实时和烘焙探针混合使用,以增加你游戏的现实感。烘焙探针将提供非常高质量的反射和环境反射,而实时探针可以用来移动汽车或镜子等对象。在“场景中烘焙灯光”部分将详细解释光照烘焙是如何工作的。
参见
如果你想要了解更多关于反射探针的信息,你应该查看以下链接:
- 
Unity 关于反射探针的手册: docs.unity3d.com/Manual/class-ReflectionProbe.html
- 
箱体投影和其他高级反射探针设置: docs.unity3d.com/Manual/AdvancedRefProbe.html
场景中烘焙灯光
渲染光照是一个非常昂贵的进程。即使是最先进的 GPU,准确计算光传输(即光如何在表面之间反射)也可能需要数小时。为了使这一过程对游戏可行,实时渲染是必不可少的。现代引擎在真实感和效率之间做出妥协;大部分计算都在一个称为光照烘焙的过程中提前完成。本指南将解释光照烘焙是如何工作的以及如何最大限度地利用它。
准备工作
灯光烘焙需要你有一个准备好的场景。它应该有几何体,显然,还有灯光。对于这个配方,我们将依赖 Unity 的标准功能,因此不需要创建额外的着色器或材质。我们将重新使用之前在第一章,后处理堆栈中使用的地图。为了更好的控制,你可能想要访问照明窗口。如果你看不到它,请从菜单中选择 Window | Lighting | Settings 并将其停靠在更方便的位置。
如何操作...
灯光烘焙需要一些手动配置。你需要采取三个基本且独立的步骤。
配置静态几何体
这些步骤必须遵循配置:
- 
识别场景中所有位置、大小和材质不发生变化的物体。可能的候选物体包括建筑、墙壁、地形、道具、树木等。在我们的例子中,除了 FPSController及其子对象之外的所有物体。
- 
选择这些物体,并在检查器标签中勾选静态框,如图所示。如果选中的任何物体有子对象,Unity 将询问你是否希望它们也被视为静态。如果它们满足要求(固定位置、大小和材质),请选择是,并在弹出框中更改子对象: 

如果一个灯光符合静态物体的资格但照亮了非静态几何体,请确保其 Baking 属性设置为 Mixed。如果它只会影响静态物体,请将其设置为 Baked。
配置光探头
在你的游戏中,有一些物体将会移动,例如主要角色、敌人以及其他非玩家角色(NPC)。如果它们进入一个被照亮的静态区域,你可能想要用光探头来包围它。为此,请按照以下步骤操作:
- 
从菜单中,导航到 GameObject | Light | Light Probe Group。在 Hierarchy 中将出现一个名为 Light Probe Group 的新对象。 
- 
一旦选择,将出现八个相互连接的球体。点击并移动它们,使它们围绕场景,将静态区域包围起来,其中角色可以进入。以下截图显示了如何使用光探头来包围静态办公空间的体积示例: 

对于我们的示例,它将是玩家能够进入的中心区域:

- 
选择将进入光探头区域的移动物体。 
- 
在它们的检查器中,展开它们的渲染器组件(通常是网格渲染器),并确保光探头未设置为关闭(请参见以下截图): 

决定在哪里以及何时使用光探头是一个关键问题;关于此的信息可以在本食谱的 How it works... 部分找到。
烘焙灯光
要烘焙灯光,请按照以下步骤操作:
- 首先,选择你想要烘焙的灯光。从检查器选项卡确认在“灯光”组件中“模式”设置为“烘焙”:

- 
要最终烘焙灯光,请通过转到“窗口”|“光照”|“设置”打开光照窗口。一旦进入,选择“全局贴图”选项卡。 
- 
如果启用了“自动生成”复选框,Unity 将在后台自动执行烘焙过程。如果没有,请点击“生成光照”。 
即使是相对较小的场景,轻度烘焙也可能需要几个小时。如果你一直在移动静态物体或灯光,Unity 将从头开始重新启动这个过程,导致编辑器严重减速。你可以从“光照”|“光照贴图”|“设置”选项卡中取消选中“自动”复选框来防止这种情况,这样你就可以决定何时手动启动过程。
它是如何工作的...
渲染中最复杂的部分是光传输。在这个阶段,GPU 计算光线如何在物体之间反弹。如果一个物体及其灯光不移动,这个计算只需进行一次,因为游戏过程中它不会改变。将物体标记为静态就是告诉 Unity 可以进行这种优化的方式。
简单来说,轻度烘焙是指计算静态对象的全局光照并将其保存到所谓的光照贴图中的过程。一旦烘焙完成,光照贴图可以在光照窗口的全局贴图选项卡中看到:

轻度烘焙代价高昂:内存。实际上,每个静态表面都会重新纹理化,以便它已经包含了其光照条件。让我们想象一下,你有一片森林,所有的树都使用相同的纹理。一旦它们被设置为静态,每棵树都将拥有自己的纹理。轻度烘焙不仅会增加游戏的大小,如果无差别地使用,还会消耗大量的纹理内存。
本配方中引入的第二个方面是光线探针。光线烘焙对于静态几何体可以产生非常高质量的结果,但无法应用于移动物体。如果你的角色进入了一个静态区域,它可能会看起来有些脱离环境。它的着色不会与周围环境匹配,导致视觉效果不愉快。其他物体,例如皮肤网格渲染器,即使设置为静态也不会接收到全局照明。实时烘焙光线是不可能的,尽管光线探针提供了一个有效的替代方案。每个光线探针在空间中的特定点采样全局照明。一个光线探针组可以在空间中的几个点进行采样,允许在特定体积内进行全局照明的插值。这使我们能够在移动物体上投射更好的光线,即使全局照明只计算了几个点。重要的是要记住,光线探针需要包围一个体积才能工作。最好将光线探针放置在光线条件突然变化的区域。与光照贴图类似,探针会消耗内存,应该明智地放置;记住,它们仅存在于非静态几何体中。由于演示场景中没有可见的物体,这纯粹是为了演示目的而进行的。
即使使用光线探针,Unity 的全局照明仍然无法捕捉到一些方面。例如,非静态物体无法在其他物体上反射光线。
参见
你可以在docs.unity3d.com/Manual/LightProbes.html了解更多关于光线探针的信息。
第六章:顶点函数
着色器这个术语来源于 Cg 主要被用来模拟 3D 模型上的真实光照条件(阴影)。尽管如此,着色器现在远不止于此。它们不仅定义了对象的外观,还可以完全重新定义它们的形状。如果您想学习如何通过着色器操纵 3D 对象的几何形状,那么这一章就是为您准备的。
在本章中,您将学习以下菜谱:
- 
在表面着色器中访问顶点颜色 
- 
在表面着色器中动画顶点 
- 
扩展您的模型 
- 
实现雪着色器 
- 
实现体积爆炸 
简介
在第二章,“创建您的第一个着色器”中,我们解释了 3D 模型不仅仅是三角形的集合。每个顶点都可以包含渲染模型本身所必需的数据。本章将探讨如何访问这些信息以便在着色器中使用它。我们还将详细探讨如何仅使用 Cg 代码简单地变形物体的几何形状。
在表面着色器中访问顶点颜色
让我们首先看看如何使用表面着色器中的顶点函数访问模型顶点的信息。这将使我们具备利用模型顶点中包含的元素来创建真正有用和视觉上吸引人的效果的知识。
在顶点函数中,顶点可以返回我们需要了解的信息。实际上,您可以检索顶点的法线方向作为 float3 值和顶点的位置作为 float3,您甚至可以在每个顶点中存储颜色值,并将该颜色作为 float4 返回。这正是我们将要探讨的内容。我们需要了解如何存储颜色信息并在表面着色器的每个顶点中检索存储的颜色信息。
准备工作
为了编写这个着色器,我们需要准备一些资产。
为了查看顶点的颜色,我们需要有一个已经对其顶点应用了颜色的模型。虽然您可以使用 Unity 来应用颜色,但您需要编写一个工具来允许个人应用颜色,或者编写一些脚本来实现颜色应用。
在这个菜谱的情况下,您可以使用 3D 建模工具,如 Maya 或 Blender 来对我们的模型应用颜色。示例代码中提供了一个模型,位于书中第六章 | 模型文件夹中的VertexColorObject.fbx,您可以在书的支持页面www.packtpub.com/books/content/support上获取。
以下步骤将为我们创建这个顶点着色器做好准备:
- 
创建一个新的场景,并将导入的模型( VertexColorObject)放置在场景中。
- 
创建一个新的着色器( SimpleVertexColor)和材质(SimpleVertexColorMat)。
- 
完成后,将着色器分配给材质,然后将材质分配给导入的模型。 
你的场景现在应该类似于以下截图:

如何做到这一点…
当场景、着色器和材质创建并准备就绪后,我们可以开始编写我们的着色器代码。在 Unity 编辑器的项目标签中双击着色器以启动它。执行以下步骤:
- 由于我们正在创建一个非常简单的着色器,我们不需要在我们的Properties块中包含任何属性。我们仍然会包含一个Global Color Tint,以保持与其他着色器的一致性。在你的着色器Properties块中输入以下代码:
Properties
{
  _MainTint("Global Color Tint", Color) = (1,1,1,1)
}
- 下一步操作告诉 Unity,我们将包括一个顶点函数在我们的着色器中:
CGPROGRAM
#pragma surface surf Lambert vertex:vert
- 与往常一样,如果我们已经在Properties块中包含了属性,我们必须确保在CGPROGRAM语句中创建相应的变量。在#pragma语句下方输入以下代码:
float4 _MainTint;
- 我们现在将注意力转向Input struct。我们需要添加一个新的变量,以便我们的surf()函数可以访问由我们的vert()函数提供的数据:
struct Input 
{
  float2 uv_MainTex;
  float4 vertColor;
};
- 现在,我们可以编写我们的简单vert()函数来访问存储在我们网格每个顶点中的颜色:
void vert(inout appdata_full v, out Input o)
{
  UNITY_INITIALIZE_OUTPUT(Input,o);
  o.vertColor = v.color;
}
- 最后,我们可以使用来自我们的Input struct的顶点颜色数据分配给内置的SurfaceOutput结构中的o.Albedo参数:
void surf (Input IN, inout SurfaceOutput o) 
{
  o.Albedo = IN.vertColor.rgb * _MainTint.rgb;
}
- 我们的代码完成后,现在我们可以重新进入 Unity 编辑器并让着色器编译。如果一切顺利,你应该会看到以下截图类似的内容:

它是如何工作的…
Unity 为我们提供了一种访问附加着色器的模型顶点信息的方法。这使我们能够修改诸如顶点位置和颜色等事物。使用这个配方,我们从 Maya(尽管几乎任何 3D 软件应用都可以使用)导入了一个网格,其中顶点颜色被添加到Verts中。你会注意到,通过导入模型,默认材质不会显示顶点颜色。实际上,我们必须编写一个着色器来提取顶点颜色并将其显示在模型的表面上。当使用表面着色器时,Unity 为我们提供了大量的内置功能,这使得提取此顶点信息的过程变得快速高效。
我们的首要任务是告诉 Unity,在创建我们的着色器时我们将使用一个顶点函数。我们通过在CGPROGRAM的#pragma语句中添加vertex:vert参数来实现这一点。这会自动使 Unity 在编译着色器时寻找名为vert()的顶点函数。如果找不到,Unity 将抛出编译错误,并要求你添加一个vert()函数到你的着色器中。
这就带我们到了下一步。我们必须实际编写 vert() 函数,如 步骤 5 中所示。我们首先使用一个内置宏来确保 0 变量在没有其他要求的情况下初始化为 0,如果您针对 DirectX 11 或更高版本。
更多关于宏的信息,以及 ShaderLab 提供的所有其他宏,请查看: docs.unity3d.com/Manual/SL-BuiltinMacros.html。
通过拥有这个函数,我们可以访问名为 appdata_full 的内置数据结构。这个内置结构是存储顶点信息的地方。因此,我们通过添加代码 o.vertColor = v.color 将其传递到我们的 Input struct 中,然后提取顶点颜色信息。
o 变量代表我们的 Input struct,而 v 变量是我们的 appdata_full 顶点数据。在这种情况下,我们只是从 appdata_full 结构中获取颜色信息并将其放入我们的 Input struct 中。一旦顶点颜色在我们的输入结构中,我们就可以在 surf() 函数中使用它。在这个菜谱的情况下,我们只是将颜色应用到内置的 SurfaceOutput 结构的 o.Albedo 参数。
还有更多...
您还可以访问 vert 颜色数据的第四个组件。如果您注意到,我们在 Input struct 中声明的 vertColor 变量是 float4 类型。这意味着我们也在传递顶点颜色的 alpha 值。了解这一点后,您可以使用它来存储第四个顶点颜色,以执行透明度或给自己提供一个额外的遮罩来混合两个纹理等效果。这完全取决于您和您的制作决定是否真的需要使用第四个组件,但在这里提一下是值得的。
在表面着色器中动画顶点
现在我们知道了如何按顶点访问数据,让我们扩展我们的知识库,包括其他类型的数据和顶点的位置。
使用顶点函数,我们可以访问网格中每个顶点的位置。这允许我们在着色器处理时实际修改每个单独的顶点。
在这个菜谱中,我们将创建一个着色器,允许我们使用正弦波修改网格上每个顶点的位置。这种技术可以用来为旗帜或海洋上的波浪等对象创建动画。
准备工作
让我们收集我们的资产,以便我们可以为我们的顶点着色器编写代码:
- 创建一个新的场景,并将平面网格放置在场景中心(GameObject | 3D Objects | Plane)。
创建的 Plane 对象可能看起来是一个单独的四边形,但实际上有 121 个顶点,我们将要移动这些顶点。使用四边形将提供意外的结果。为了自己检查,选择 Plane 对象,然后在平面(网格过滤器)组件下双击网格属性。
- 
创建一个新的着色器( VertexAnimation)和材质(VertexAnimationMat)。
- 
最后,将着色器分配给材质,并将材质分配给平面网格。 
你的场景应该看起来类似于以下截图:

如何操作…
当场景准备就绪后,让我们双击我们新创建的着色器,在代码编辑器中打开它:
- 让我们从着色器开始,并填充Properties块:
Properties 
{
  _MainTex ("Base (RGB)", 2D) = "white" {}
  _tintAmount ("Tint Amount", Range(0,1)) = 0.5
  _ColorA ("Color A", Color) = (1,1,1,1)
  _ColorB ("Color B", Color) = (1,1,1,1)
  _Speed ("Wave Speed", Range(0.1, 80)) = 5
  _Frequency ("Wave Frequency", Range(0, 5)) = 2
  _Amplitude ("Wave Amplitude", Range(-1, 1)) = 1
}
- 现在,我们需要告诉 Unity 我们将要使用顶点函数,通过在#pragma语句中添加以下内容来实现:
CGPROGRAM
#pragma surface surf Lambert vertex:vert
- 为了访问我们属性给出的值,我们需要在CGPROGRAM块中声明一个相应的变量:
sampler2D _MainTex;
float4 _ColorA;
float4 _ColorB;
float _tintAmount;
float _Speed;
float _Frequency;
float _Amplitude;
float _OffsetVal;
- 我们将使用顶点位置修改作为vert颜色,这将允许我们为对象上色:
struct Input 
{
  float2 uv_MainTex;
  float3 vertColor;
}
- 在这一点上,我们可以使用正弦波和顶点函数来执行顶点修改。在Input struct之后输入以下代码:
void vert(inout appdata_full v, out Input o)
{
  UNITY_INITIALIZE_OUTPUT(Input,o);
  float time = _Time * _Speed;
  float waveValueA = sin(time + v.vertex.x * _Frequency) * _Amplitude;
  v.vertex.xyz = float3(v.vertex.x, v.vertex.y + waveValueA, v.vertex.z);
  v.normal = normalize(float3(v.normal.x + waveValueA, v.normal.y, v.normal.z));
  o.vertColor = float3(waveValueA,waveValueA,waveValueA);
}
- 最后,我们通过在两个颜色之间执行一个lerp()函数来完成我们的着色器,这样我们就可以为经过顶点函数修改后的新网格的峰值和谷值上色:
void surf (Input IN, inout SurfaceOutput o)
{
  half4 c = tex2D (_MainTex, IN.uv_MainTex);
  float3 tintColor = lerp(_ColorA, _ColorB, IN.vertColor).rgb; 
  o.Albedo = c.rgb * (tintColor * _tintAmount);
  o.Alpha = c.a;
}
- 
完成你的着色器代码后,切换回 Unity 并让着色器编译。一旦编译完成,选择材质并将基础(RGB)纹理分配给包含在本书示例代码 第六章|纹理文件夹中的UV Checker材质。
- 
从那里,将颜色 A 和颜色 B 分配给不同的颜色。更改后,你应该看到以下截图类似的内容: 

它是如何工作的…
这个特定的着色器使用了上一个配方中的相同概念,但这次我们正在修改网格中顶点的位置。如果你不想为简单的对象,如旗帜,设置骨架并使用骨架结构或变换层次结构来动画它们,这将非常有用。
我们简单地使用 Cg 语言内置的sin()函数创建一个正弦波值。计算这个值后,我们将其添加到每个顶点位置的y值,从而创建出波浪状的效果。
我们还修改了网格上的法线,以便根据正弦波值提供更逼真的着色。
你将看到通过利用表面着色器给我们提供的内置顶点参数,执行更复杂的顶点效果是多么容易。
挤出你的模型
游戏中最大的问题之一是重复。创建新内容是一项耗时的工作,当你必须面对成千上万的敌人时,他们很可能看起来都一样。为了给你的模型添加变化,使用一个改变其基本几何形状的着色器是一种相对便宜的技术。这个配方将向你展示一种称为法线挤出的技术,它可以用来创建一个更胖或更瘦的模型版本,如以下 Unity camp 演示中的士兵截图所示:

为了方便使用,我在本书的示例代码中提供了士兵的预制件,位于第六章|预制件文件夹下。
准备工作
对于这个配方,你需要能够访问你想要修改的模型的着色器。一旦你有了它,我们就复制它,这样我们就可以安全地编辑它。可以按照以下步骤进行:
- 
找到你的模型使用的着色器,一旦选中,通过按*Ctrl *+ D来复制它。如果它只是使用标准着色器,如本例所示,也可以创建一个新的标准材质,如正常材质,并且 Albedo 贴图将自动转换过来。无论如何,将这个新的着色器重命名为 NormalExtrusion。
- 
复制模型的原始材质并将其分配给克隆的着色器。 
- 
将新的材质分配给你的模型( NormalExtrusionMat)并开始编辑它。
为了使这个效果起作用,你的模型应该有法线。
如何做到这一点…
为了创建这个效果,首先修改复制的着色器:
- 让我们从向我们的着色器添加一个属性开始,该属性将用于调节其挤压。这里显示的范围从-0.0001到0.0001,但你可能需要根据你的需求进行调整:
__Amount ("Extrusion Amount", Range(-0.0001, 0.0001)) = 0
- 将属性与其相应的变量配对:
float _Amount;
- 修改#pragma指令,使其现在使用顶点修改器。你可以通过在指令末尾添加vertex:function_name来实现。在我们的例子中,我们调用的是vert:函数:
#pragma surface surf Standard vertex:vert
- 添加以下顶点修改器:
void vert (inout appdata_full v) 
{
  v.vertex.xyz += v.normal * _Amount;
}
- 着色器现在已准备好;你可以通过在材质的检查器标签中调整挤压量滑块来使你的模型更瘦或更胖。此外,你也可以自由地创建材质的克隆,以便为每个角色提供不同的挤压量:

它是如何工作的…
表面着色器分为两个步骤。在前面的所有章节中,我们只探索了最后一个步骤:表面函数。还有一个可以使用的函数:顶点修改器。它接受顶点的数据结构(通常称为appdata_full)并对其应用变换。这给了我们几乎可以对模型的几何形状做任何事的自由。我们通过在表面着色器的#pragma指令中添加vertex:vert来向 GPU 发出存在这样一个函数的信号。你可以参考第七章,片段着色器和抓取通道,来了解如何在顶点和片段着色器中定义顶点修改器。
可以用来改变模型几何形状的最简单且有效的技术之一被称为法线挤压。它是通过沿着顶点的法线方向投影顶点来工作的。这是通过以下代码行实现的:
v.vertex.xyz += v.normal * _Amount;
顶点的位置通过_Amount单位向顶点法线方向偏移。如果_Amount太高,结果可能会相当不愉快。然而,使用较小的值,你可以在你的模型中添加很多变化。
还有更多…
如果你有多名敌人,并且希望每个敌人都有自己的权重,你必须为它们中的每一个创建不同的材质。这是必要的,因为材质通常在模型之间共享,更改一个会改变所有模型。你可以通过几种方式来完成这项工作;最快的方法是创建一个脚本,它会自动为你完成这项工作。以下脚本一旦附加到具有 Renderer 的对象上,就会自动复制其第一个材质并设置 _Amount 属性:
using UnityEngine;
public class NormalExtruder : MonoBehaviour {
  [Range(-0.0001f, 0.0001f)]
 public float amount = 0;
  // Use this for initialization
 void Start () 
  {
    Material material = GetComponent<Renderer>().sharedMaterial;
    Material newMaterial = new Material(material);
    newMaterial.SetFloat("_Amount", amount);
    GetComponent<Renderer>().material = newMaterial;
  }
}
添加挤出图
实际上,这种技术还可以进一步改进。我们可以添加一个额外的纹理(或使用主纹理的 alpha 通道)来指示挤出量。这允许对哪些部分被抬起或降低有更好的控制。以下代码展示了如何实现这种效果(与之前所做的主要区别用粗体表示):
Shader "CookbookShaders/Chapter06/Normal Extrusion Map" 
{
  Properties
  {
    _MainTex("Texture", 2D) = "white" {}
    _ExtrusionTex("Extrusion map", 2D) = "white" {}
  _ Amount("Extrusion Amount", Range(-0.0001, 0.0001)) = 0
  }
  SubShader
  {
    Tags{ "RenderType" = "Opaque" }
    CGPROGRAM
    #pragma surface surf Standard vertex:vert
    struct Input 
    {
      float2 uv_MainTex;
    };
    float _Amount;
    sampler2D _ExtrusionTex;
 void vert(inout appdata_full v) 
 {
 float4 tex = tex2Dlod (_ExtrusionTex, float4(v.texcoord.xy,0,0));
 float extrusion = tex.r * 2 - 1;
 v.vertex.xyz += v.normal * _Amount * extrusion;
 } 
    sampler2D _MainTex;
    void surf(Input IN, inout SurfaceOutputStandard o) 
    {
      float4 tex = tex2D(_ExtrusionTex, IN.uv_MainTex);
      float extrusion = abs(tex.r * 2 - 1);
      o.Albedo = tex2D(_MainTex, IN.uv_MainTex).rgb;
      o.Albedo = lerp(o.Albedo.rgb, float3(0, 0,0), extrusion * _Amount   
                      / 0.0001 * 1.1);
    }
  ENDCG
  }
  Fallback "Diffuse"
}
_ExtrusionTex 的红色通道被用作正常挤出时的乘法系数。值为 0.5 会使模型不受影响;较暗或较亮的色调分别用于向内或向外挤出顶点。你应该注意,为了在顶点修改器内采样纹理,应使用 tex2Dlod 而不是 tex2D。
在着色器中,颜色通道从零到一,尽管有时你需要表示负值(例如向内挤出)。在这种情况下,将 0.5 视为零;较小的值视为负值,较大的值视为正值。这与法线的情况完全相同,法线通常编码在 RGB 纹理中。UnpackNormal() 函数用于将范围 (0,1) 的值映射到范围 (-1,+1)。从数学上讲,这相当于 tex.r * 2 -1。
挤出图非常适合通过缩小皮肤来突出显示下面骨骼的形状,将角色僵尸化。以下屏幕截图展示了如何仅使用着色器和挤出图将一个健康的士兵变成尸体。与前面的例子相比,你可能注意到衣服没有受到影响。以下屏幕截图中所使用的着色器还将挤出区域变暗,使士兵看起来更加消瘦:

实现雪着色器
在游戏中,雪的模拟一直是一个挑战。绝大多数游戏只是直接将雪包含在模型的纹理中,使其顶部看起来是白色的。然而,如果这些物体中的任何一个开始旋转呢?雪不仅仅是表面的一层油漆;它是一种真正的材料积累,应该这样处理。这个配方展示了如何仅使用着色器给你的模型添加雪的外观。
这个效果分为两个步骤。首先,使用白色处理所有面向天空的三角形。其次,它们的顶点被挤出以模拟雪积累的效果。你可以在下面的屏幕截图中看到结果:

请记住,这个配方并不旨在创建逼真的雪效果。它提供了一个良好的起点,但艺术家需要创建正确的纹理并找到合适的参数,以便使其适合你的游戏。
准备工作
这种效果完全基于着色器。我们需要以下内容:
- 
为雪效果创建一个新的着色器( SnowShader)。
- 
为着色器创建一个新的材质( SnowMat)。
- 
将新创建的材质分配给你想使其变雪的对象,并分配一个颜色以便更容易地判断雪的位置: 

如何做到这一点...
要创建雪的效果,打开你的着色器并做出以下更改:
- 将着色器的属性替换为以下属性:
_Color("Main Color", Color) = (1.0,1.0,1.0,1.0)
_MainTex("Base (RGB)", 2D) = "white" {}
_Bump("Bump", 2D) = "bump" {}
_Snow("Level of snow", Range(1, -1)) = 1
_SnowColor("Color of snow", Color) = (1.0,1.0,1.0,1.0)
_SnowDirection("Direction of snow", Vector) = (0,1,0)
_SnowDepth("Depth of snow", Range(0,1)) = 0
- 使用它们的相关变量来完成:
sampler2D _MainTex;
sampler2D _Bump;
float _Snow;
float4 _SnowColor;
float4 _Color;
float4 _SnowDirection;
float _SnowDepth;
- 将Input结构替换为以下结构:
struct Input 
{
  float2 uv_MainTex;
  float2 uv_Bump;
  float3 worldNormal;
  INTERNAL_DATA
};
- 将表面函数替换为以下函数。它将模型的雪部分着色为白色:
void surf(Input IN, inout SurfaceOutputStandard o) 
{
  half4 c = tex2D(_MainTex, IN.uv_MainTex);
  o.Normal = UnpackNormal(tex2D(_Bump, IN.uv_Bump));
  if (dot(WorldNormalVector(IN, o.Normal), _SnowDirection.xyz) 
      >= _Snow)
  {
  o.Albedo = _SnowColor.rgb;
  }
  else
  {
  o.Albedo = c.rgb * _Color;
  }
  o.Alpha = 1;
}
- 配置#pragma指令,使其使用顶点修饰符:
#pragma surface surf Standard vertex:vert
- 添加以下顶点修饰符,这些修饰符将扩展被雪覆盖的顶点:
void vert(inout appdata_full v) 
{
  float4 sn = mul(UNITY_MATRIX_IT_MV, _SnowDirection);
  if (dot(v.normal, sn.xyz) >= _Snow)
  {
    v.vertex.xyz += (sn.xyz + v.normal) * _SnowDepth * _Snow;
  }
}
- 你现在可以使用材质的“检查器”选项卡来选择你的模型将被覆盖的部分以及雪的厚度:

它是如何工作的...
这个着色器分为两个步骤:
- 
表面着色 
- 
修改几何形状。 
表面着色
第一步改变了面向天空的三角形的颜色。它影响所有与_SnowDirection法线方向相似的三角形。如前所述,在第三章“理解光照模型”中,比较单位向量可以使用点积来完成。当两个向量正交时,它们的点积为零;当它们相互平行时,点积为 1(或-1)。_Snow属性用于决定它们应该有多大的对齐度才能被认为是面向天空的。
如果你仔细观察表面函数,你会发现我们并没有直接在法线和雪的方向上打点。这是因为它们通常定义在不同的空间中。雪的方向用世界坐标表示,而物体的法线通常相对于模型本身。如果我们旋转模型,其法线不会改变,这并不是我们想要的。为了解决这个问题,我们需要将法线从物体坐标转换为世界坐标。这可以通过WorldNormalVector()函数来完成,如下面的代码所示:
if (dot(WorldNormalVector(IN, o.Normal), _SnowDirection.xyz) >=
  _Snow)
{
  o.Albedo = _SnowColor.rgb;
}
else {
  o.Albedo = c.rgb * _Color;
}
这个着色器简单地使模型着色为白色;一个更高级的着色器应该使用来自逼真雪材料的纹理和参数初始化SurfaceOutputStandard结构。
修改几何形状
这个着色器的第二个效果通过改变几何体来模拟雪的积累。首先,我们通过测试表面函数中使用的相同条件来识别哪些三角形被涂成了白色。遗憾的是,这次我们无法依赖WorldNormalVector(),因为SurfaceOutputStandard结构在顶点修改器中尚未初始化。我们使用这种方法代替,它将_SnowDirection转换为对象坐标:
float4 sn = mul(UNITY_MATRIX_IT_MV, _SnowDirection);
然后,我们可以通过拉伸几何体来模拟雪的积累:
if (dot(v.normal, sn.xyz) >= _Snow)
{
    v.vertex.xyz += (sn.xyz + v.normal) * _SnowDepth * _Snow;
}
再次强调,这是一个非常基础的效果。你可以使用纹理图来更精确地控制雪的积累,或者给雪一个奇特、不均匀的外观。
参见
如果你需要高质量的游戏雪效果和道具,你还可以在 Unity Asset Store 上查看这些资源:
- 
冬季套件($30):在这个菜谱中展示的雪着色器的更复杂版本可以在 www.assetstore.unity3d.com/en/#!/content/13927找到。
- 
冬季包($60):可以在 www.assetstore.unity3d.com/en/#!/content/13316找到一套非常逼真的雪地环境和道具材质。
实现体积爆炸
游戏开发的艺术是一种巧妙地权衡现实主义和效率的贸易。这在爆炸效果方面尤其如此;它们是许多游戏的核心,但背后的物理原理往往超出现代机器的计算能力。本质上,爆炸不过是非常热的气体球体;因此,正确模拟它们的方法是将流体模拟集成到你的游戏中。正如你可以想象的那样,这对于运行时应用来说是不切实际的,许多游戏只是简单地使用粒子来模拟爆炸。当一个物体爆炸时,通常简单地实例化火焰、烟雾和碎片粒子,以便它们共同产生可信的结果。不幸的是,这种方法并不非常逼真,很容易被发现。有一种中间技术可以用来实现更逼真的效果:体积爆炸。这个概念背后的想法是,爆炸不是像一堆粒子那样被处理的;它们是不断演变的 3D 对象,而不仅仅是平面的 2D 纹理。
准备工作
通过以下步骤完成此菜谱:
- 
为此效果创建一个新的着色器( VolumetricExplosion)。
- 
创建一个新的材质来承载着色器( VolumetricExplosionMat)。
- 
将材质附加到一个球体上。你可以直接从编辑器创建一个,导航到 GameObject | 3D Object | Sphere: 

这个配方与标准的 Unity 球体配合得很好,但如果您需要大爆炸,您可能需要使用高多边形球体。实际上,顶点函数只能修改网格的顶点。所有其他点将使用附近顶点的位置进行插值。顶点越少,您的爆炸分辨率就越低。
- 对于这个配方,您还需要一个渐变中包含所有爆炸将具有的颜色的高斯纹理。您可以使用 GIMP 或 Photoshop 创建如下截图所示的纹理:

您可以在本书提供的示例代码中的第六章|纹理文件夹中找到这张图片(explosionRamp)。
- 
一旦您有了这张图片,将其导入 Unity。然后,从其检查器中,确保将过滤模式设置为双线性,并将包裹模式设置为固定。这两个设置确保渐变纹理被平滑采样。 
- 
最后,您还需要一个噪声纹理。您可以在互联网上搜索免费提供的噪声纹理。最常用的那些是使用 Perlin 噪声生成的。我已经在 第六章|纹理文件夹中包含了一个示例供您使用:

如何做到这一点…
这个效果分为两个步骤:一个顶点函数来改变几何形状,一个表面函数来赋予它正确的颜色。步骤如下:
- 删除当前属性并添加以下属性到着色器中:
Properties 
{
  _RampTex("Color Ramp", 2D) = "white" {}
  _RampOffset("Ramp offset", Range(-0.5,0.5))= 0
  _NoiseTex("Noise Texture", 2D) = "gray" {}
  _Period("Period", Range(0,1)) = 0.5
  _Amount("_Amount", Range(0, 1.0)) = 0.1
  _ClipRange("ClipRange", Range(0,1)) = 1
}
- 添加它们的相对变量,以便着色器的 Cg 代码可以实际访问它们:
sampler2D _RampTex;
half _RampOffset;
sampler2D _NoiseTex;
float _Period;
half _Amount;
half _ClipRange;
- 修改Input结构,使其接收渐变纹理的 UV 数据:
struct Input 
{
 float2 uv_NoiseTex;
};
- 添加以下顶点函数:
void vert(inout appdata_full v) {
 float3 disp = tex2Dlod(_NoiseTex, float4(v.texcoord.xy,0,0));
 float time = sin(_Time[3] *_Period + disp.r*10);
  v.vertex.xyz += v.normal * disp.r * _Amount * time;
}
- 添加以下表面函数:
void surf(Input IN, inout SurfaceOutput o) 
{
  float3 noise = tex2D(_NoiseTex, IN.uv_NoiseTex);
  float n = saturate(noise.r + _RampOffset);
  clip(_ClipRange - n);
  half4 c = tex2D(_RampTex, float2(n,0.5));
  o.Albedo = c.rgb;
  o.Emission = c.rgb*c.a;
}
- 我们在#pragma指令中指定顶点函数,添加nolightmap参数以防止 Unity 为我们添加逼真的光照:
#pragma surface surf Lambert vertex:vert nolightmap
- 最后一步是选择材质,并从其检查器中,将两个纹理附加到相应的插槽中:

- 这是一个动画材质,意味着它会随时间演变。您可以通过在场景窗口中点击动画材质来观看材质在编辑器中的变化:

它是如何工作的…
如果您正在阅读这个配方,您应该已经熟悉了表面着色器和顶点修改器的工作方式。这个效果背后的主要思想是以看似混乱的方式改变球体的几何形状,就像在现实中的爆炸一样。以下截图显示了在编辑器中这样的爆炸将看起来是什么样子。您可以看到原始网格已经被严重变形:

顶点函数是本章“扩展你的模型”配方中引入的称为法线扩展技术的一种变体。这里的区别在于,扩展量由时间和噪声纹理共同决定。
当你在 Unity 中需要一个随机数时,你可以依赖Random.Range()函数。在着色器中没有获取随机数的标准方式,所以最简单的方法是采样噪声纹理。
没有标准的方式来做到这一点,所以这只是一个例子:
float time = sin(_Time[3] *_Period + disp.r*10);
内置的_Time[3]变量用于从着色器内部获取当前时间,disp.r噪声纹理的红通道用于确保每个顶点独立移动。sin()函数使顶点上下移动,模拟爆炸的混沌行为。然后,进行正常的拉伸:
v.vertex.xyz += v.normal * disp.r * _Amount * time;
你应该玩转这些数字和变量,直到你找到一个让你满意的运动模式。
效果的最后部分是通过表面函数实现的。在这里,噪声纹理被用来从渐变纹理中采样一个随机颜色。然而,还有两个方面值得注意。第一个方面是引入了_RampOffset。它的使用强制爆炸从纹理的左侧或右侧采样颜色。使用正值时,爆炸的表面倾向于显示更多灰色调——这正是它正在溶解时发生的情况。你可以使用_RampOffset来确定你的爆炸中应该有多少火焰或烟雾。表面函数中引入的第二个方面是使用clip()。clip()的作用是从渲染管线中裁剪(移除)像素。当使用负值调用时,当前像素不会被绘制。这种效果由_ClipRange控制,它决定了哪些体积爆炸的像素将是透明的。
通过控制_RampOffset和_ClipRange,你拥有完全的控制权,可以确定爆炸的行为和溶解方式。
还有更多…
本配方中展示的着色器使球体看起来像爆炸。如果你真的想使用它,你应该结合一些脚本以便充分利用它。最好的做法是创建一个爆炸对象,并将其制作成预制件,这样你每次需要时都可以重用。你可以通过将球体拖回项目窗口来完成此操作。一旦完成,你可以使用Instantiate()函数创建任意数量的爆炸。
然而,值得注意的是,所有相同材质的物体看起来都一样。如果你同时发生多个爆炸,它们不应该使用相同的材质。当你实例化一个新的爆炸时,你也应该复制其材质。你可以用这段代码轻松做到这一点:
GameObject explosion = Instantiate(explosionPrefab) as GameObject;
Renderer renderer = explosion.GetComponent<Renderer>();
Material material = new Material(renderer.sharedMaterial);
renderer.material = material;
最后,如果你打算以现实的方式使用这个着色器,你应该给它附加一个脚本,根据你想要重现的爆炸类型来改变其大小、_RampOffset和_ClipRange。
参见
- 
可以做更多的事情来使爆炸更逼真。本配方中展示的方法只创建了一个空壳;实际上,爆炸内部是空的。 
- 
提高这一效果的简单技巧是在其中创建粒子。然而,您只能做到这一步。 
- 
Unity Technologies 与 Passion Pictures 和 Nvidia 合作制作的短片蝴蝶效应( unity3d.com/pages/butterfly)是完美的例子。
- 
它基于改变球体几何形状的相同概念,但使用了一种称为体积光线投射的技术来呈现。 
- 
简而言之,它将几何形状呈现得仿佛是实心的。您可以在下面的屏幕截图中看到一个示例: 

- 如果您在寻找高质量的爆炸效果,请查看 Asset Store 中的 Pyro Technix (www.assetstore.unity3d.com/en/#!/content/16925)。它包括体积爆炸,并将它们与逼真的冲击波相结合。
第七章:片段着色器和抓取通道
到目前为止,我们一直依赖于表面着色器。它们被设计用来简化着色器编码的方式,为艺术家提供有意义的工具。如果我们想进一步推进我们对着色器的了解,我们需要进入顶点和片段着色器的领域。
在本章中,你将学习以下配方:
- 
理解顶点和片段着色器 
- 
使用抓取通道在物体后面绘制 
- 
实现玻璃着色器 
- 
实现二维游戏的着色器 
简介
与表面着色器相比,顶点和片段着色器在确定光线如何反射到表面上的物理属性方面提供的信息很少或没有。它们在表现力方面的不足,通过强大的功能来补偿:顶点和片段着色器不受物理约束的限制,非常适合非真实感效果。本章将重点介绍一种称为抓取通道的技术,它允许这些着色器模拟变形。
理解顶点和片段着色器
理解顶点和片段着色器工作原理的最好方式是创建一个自己。这个配方将向你展示如何编写这些着色器之一,它将简单地应用一个纹理到模型上,并将其乘以给定的颜色,如以下截图所示:

注意它的工作方式与 Photoshop 中的乘法滤镜相似。这是因为我们将执行那里所做的相同计算!
这里展示的着色器非常简单,它将被用作所有其他顶点和片段着色器的起始基础。
准备工作
对于这个配方,我们需要一个新的着色器。按照以下步骤操作:
- 
创建一个新的着色器( Multiply)。
- 
创建一个新的材质( MultiplyMat)并将着色器分配给它。
- 
将士兵预制体从 Chapter 06|Prefabs文件夹拖入场景,并将新材质附加到预制体的头部。头部位于Soldier对象的Soldier子对象中。
- 
从那里,在检查器选项卡中,向下滚动到 Skinned Mesh Renderer 组件,在材质下,将元素 0 设置为新材料。最后,在 Albedo (RGB)属性中,拖放Unity_soldier_Head_DIF_01纹理。以下截图应该有助于展示我们正在寻找的内容:

如何做到这一点……
在所有前面的章节中,我们总能重新配置表面着色器。现在不再是这样了,因为表面和片段着色器在结构上是不同的。我们需要实施以下更改:
- 删除着色器的所有属性,用以下内容替换:
Properties 
{
 _Color ("Color", Color) = (1,0,0,1)
 _MainTex ("Albedo (RGB)", 2D) = "white" {}
}
- 删除SubShader块中的所有代码,并用以下内容替换:
SubShader 
{
 Pass 
 {
 CGPROGRAM
 #pragma vertex vert
 #pragma fragment frag
 half4 _Color;
 sampler2D _MainTex;
 struct vertInput 
 {
 float4 pos : POSITION;
 float2 texcoord : TEXCOORD0;
 };
 struct vertOutput 
 {
 float4 pos : SV_POSITION;
 float2 texcoord : TEXCOORD0;
 };
 vertOutput vert(vertInput input) 
 {
 vertOutput o;
 o.pos = mul(UNITY_MATRIX_MVP, input.pos);
 o.texcoord = input.texcoord;
 return o;
 }
 half4 frag(vertOutput output) : COLOR
 {
 half4 mainColour = tex2D(_MainTex, output.texcoord);
 return mainColour * _Color;
 }
 ENDCG
 }
}
FallBack "Diffuse"
- 保存你的着色器脚本并返回到 Unity 编辑器。完成后,修改MultiplyMat材质的 Color 属性,并查看我们是否得到了预期的结果:

这也将成为所有未来顶点和片段着色器的基础。
它是如何工作的……
如其名所示,顶点和片段着色器分为两个步骤工作。首先将模型传递给顶点函数;然后将结果输入到片段函数。这两个函数都使用#pragma指令进行分配:
#pragma vertex vert
#pragma fragment frag
在这种情况下,它们简单地被称为vert和frag。
从概念上讲,片段与像素密切相关;术语片段常用来指代绘制像素所需的数据集合。这也是为什么顶点和片段着色器常被称为像素着色器。
顶点函数接收在着色器中定义为vertInput的结构体中的输入数据:
struct vertInput 
{
 float4 pos : POSITION;
 float2 texcoord : TEXCOORD0;
};
它的名字完全是任意的,但它的内容却不是。struct中的每个字段都必须用绑定语义进行装饰。这是 Cg 的一个特性,允许我们标记变量,以便它们可以初始化为某些数据,例如法线向量和顶点位置。绑定语义POSITION表示当vertInput输入到顶点函数时,pos将包含当前顶点的位置。这与 Surface Shader 中appdata_full结构的顶点字段类似。主要区别在于pos是以模型坐标(相对于 3D 对象)表示的,我们需要手动将其转换为视图坐标(相对于屏幕上的位置)。
在表面着色器中,顶点函数用于仅改变模型的几何形状。而在顶点和片段着色器中,顶点函数是必要的,用于将模型的坐标投影到屏幕上。
这种转换背后的数学超出了本章的范围。然而,可以通过使用UnityObjectToClipPos函数来实现这种转换,该函数将点从对象空间转换为相机的裁剪空间(齐次坐标)。这是通过乘以模型视图投影矩阵来完成的,这对于找到屏幕上顶点的位置是至关重要的:
vertOutput o;
o.pos = UnityObjectToClipPos(input.pos);
关于此以及其他 ShaderLab 内置的辅助函数的更多信息,请查看docs.unity3d.com/Manual/SL-BuiltinFunctions.html。
另一个初始化的信息是textcoord,它使用TEXCOORD0绑定语义来获取第一个纹理的 UV 数据。不需要进一步处理,这个值可以直接传递给片段函数(frag):
o.texcoord = input.texcoord;
虽然 Unity 会为我们初始化vertInput,但我们负责初始化vertOutput。尽管如此,其字段仍然需要用绑定语义进行装饰:
struct vertOutput 
{
  float4 pos : SV_POSITION;
  float2 texcoord : TEXCOORD0;
};
一旦顶点函数初始化了vertOutput,该结构体就传递给片段函数(frag)。这将从模型的主要纹理中采样并乘以提供的颜色。
如你所见,顶点和片段着色器没有关于材料物理属性的知识。这意味着材料在光源下不会产生相同的效果,并且它没有关于如何与表面着色器相比,通过反射光线创建凹凸表面的数据;它更接近图形 GPU 的架构。
更多内容...
顶点和片段着色器中最令人困惑的方面之一是绑定语义。还有许多其他可以使用的语义,它们的含义取决于上下文。
输入语义
下表中的绑定语义可以在vertInput中使用,这是 Unity 提供给顶点函数的结构。带有此语义的字段将自动初始化:
| 绑定语义 | 描述 | 
|---|---|
| POSITION,SV_POSITION | 顶点在世界坐标系(对象空间)中的位置 | 
| NORMAL | 顶点的法线,相对于世界(而不是相机) | 
| COLOR,COLOR0,DIFFUSE,SV_TARGET | 存储在顶点中的颜色信息 | 
| COLOR1,SPECULAR | 存储在顶点中的次颜色信息(通常是高光) | 
| TEXCOORD0,TEXCOORD1, …,TEXCOORDi | 存储在顶点中的第 i 个 UV 数据 | 
输出语义
绑定时,语义在vertOutput中使用;它们并不自动保证字段将被初始化。恰恰相反;这是我们的责任。编译器将尽力确保字段用正确的数据初始化:
| 绑定语义 | 描述 | 
|---|---|
| POSITION,SV_POSITION,HPOS | 顶点在相机坐标系(裁剪空间,每个维度从零到一)中的位置 | 
| COLOR,COLOR0,COL0,COL,SV_TARGET | 前主颜色 | 
| COLOR1,COL1 | 前次颜色 | 
| TEXCOORD0,TEXCOORD1, …,TEXCOORDi,TEXi | 存储在顶点中的第 i 个 UV 数据 | 
| WPOS | 在窗口中的位置,以像素为单位(原点在左下角) | 
如果出于任何原因,你需要一个包含不同类型数据的字段,你可以用可用的许多TEXCOORD数据之一来装饰它。编译器将不允许字段未装饰。
参见
你可以参考 NVIDIA 参考手册来检查 Cg 中可用的其他绑定语义:
developer.download.nvidia.com/cg/Cg_3.1/Cg-3.1_April2012_ReferenceManual.pdf
使用抓取通道在物体后面绘制
在第五章的“为 PBR 添加透明度”配方中,我们看到了如何使材质变得透明。即使透明材质可以在场景中绘制,它也不能改变其下已经绘制的内容。这意味着那些透明着色器不能创建像玻璃或水中通常看到的扭曲。为了模拟这些效果,我们需要引入另一种称为抓取遍历的技术。这允许我们访问到目前为止屏幕上已经绘制的内容,以便着色器可以无限制地使用它(或修改它)。为了学习如何使用抓取遍历,我们将创建一个材质,它抓取它后面的渲染内容并在屏幕上再次绘制。这是一个着色器,它矛盾地使用几个操作来显示没有任何变化。
准备工作
这个配方需要以下操作:
- 
创建一个着色器( GrabShader),我们稍后会初始化它。
- 
创建一个材质( GrabMat)来托管着色器。
- 
将材质附加到一个平面几何体上,例如一个四边形。将其放置在某个其他物体前面,以便无法透过它。一旦着色器完成,四边形将看起来是透明的: 

如何做到这一点…
要使用抓取遍历,你需要遵循以下步骤:
- 
删除 Properties部分和Input部分;这个着色器将不会使用它们。
- 
在 SubShader部分中,删除所有内容,并添加以下内容以确保对象被视为Transparent:
Tags{ "Queue" = "Transparent" }
- 然后,在下面添加一个抓取遍历:
GrabPass{ }
- 在GrabPass之后,我们需要添加这个额外的遍历:
Pass 
{
  CGPROGRAM
  #pragma vertex vert
  #pragma fragment frag
  #include "UnityCG.cginc"
  sampler2D _GrabTexture;
  struct vertInput 
  {
    float4 vertex : POSITION;
  };
  struct vertOutput 
  {
    float4 vertex : POSITION;
    float4 uvgrab : TEXCOORD1;
  };
  // Vertex function
  vertOutput vert(vertInput v) 
  {
    vertOutput o;
    o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
    o.uvgrab = ComputeGrabScreenPos(o.vertex);
    return o;
  }
  // Fragment function
  half4 frag(vertOutput i) : COLOR 
  {
    fixed4 col = tex2Dproj(_GrabTexture, UNITY_PROJ_COORD(i.uvgrab));
    return col + half4(0.5,0,0,0);
  }
  ENDCG
}
- 保存你的脚本并返回 Unity 编辑器。返回后,你应该注意到你的材质现在按你期望的方式工作:

它是如何工作的…
这个配方不仅介绍了抓取遍历,还介绍了顶点和片段着色器;因此,我们必须详细分析着色器。
到目前为止,所有的代码都始终直接放置在SubShader部分中。这是因为我们之前的着色器只需要一个遍历。这次需要两个遍历。第一个是GrabPass{},它简单地定义为GrabPass{}。其余的代码放置在第二个遍历中,它包含在一个Pass块中。
第二次遍历与本章第一个配方中显示的着色器在结构上没有区别;我们使用顶点函数vert来获取顶点的位置,然后在片段函数frag中给它一个颜色。区别在于vert计算了另一个重要的细节:GrabPass{}的 UV 数据。GrabPass{}会自动创建一个可以如下引用的纹理:
sampler2D _GrabTexture;
为了采样这个纹理,我们需要它的 UV 数据。ComputeGrabScreenPos函数返回我们可以用于以后正确采样抓取纹理的数据。这是在片段着色器中使用以下行完成的:
fixed4 col = tex2Dproj(_GrabTexture, UNITY_PROJ_COORD(i.uvgrab));
这是纹理以正确位置抓取并应用于屏幕的标准方式。如果一切操作都正确,这个着色器将简单地克隆几何体后面渲染的内容。我们将在接下来的配方中看到这种技术如何用于创建水、玻璃等材料。
更多内容...
每次你使用带有GrabPass{}的材料时,Unity 都不得不将屏幕渲染到纹理中。这个操作非常昂贵,限制了你在游戏中可以使用的GrabPass实例的数量。Cg 提供了一种略有不同的变体:
GrabPass {"TextureName"}
这一行不仅允许你给纹理命名,而且还与所有具有名为TextureName的GrabPass的材料共享纹理。这意味着如果你有十个材料,Unity 将只执行一个GrabPass并将纹理与它们共享。这种技术的主要问题是它不允许堆叠效果。如果你使用这种技术创建玻璃,你将无法连续放置两个玻璃。
实现玻璃着色器
玻璃是一种非常复杂的材料;在第五章的“在 PBR 中添加透明度”配方中,其他章节已经创建了着色器来模拟它,这并不令人惊讶。我们已经知道如何使我们的玻璃半透明,以完美地显示其后的物体,并且这适用于许多应用。然而,大多数玻璃并不完美。例如,如果你透过彩色玻璃窗看,你可能会注意到当你透过它们看时会有扭曲或变形。这个配方将教会你如何实现这种效果。这个效果背后的想法是使用带有GrabPass的顶点和片段着色器,然后通过对其 UV 数据进行一点改变来采样抓取纹理,以创建扭曲。你可以在下面的屏幕截图中看到这个效果,使用了 Unity 标准资产中的玻璃染色纹理:

准备工作
这个配方与之前在第六章中介绍的设置类似,顶点函数:
- 
创建一个新的顶点和片段着色器。你可以通过选择它并按Ctrl+D来复制之前配方中使用的着色器,作为基础。一旦复制,将其名称更改为 WindowShader。
- 
创建一个将使用着色器的材料( WindowMat)。
- 
将材料分配给一个四边形或其他平面几何体,以模拟你的玻璃。 
- 
放置一些物体在其后面,以便你可以看到扭曲效果: 

如何操作…
让我们先从编辑顶点和片段着色器开始:
- 创建一个包含以下项目的Properties块:
Properties 
{
  _MainTex("Base (RGB) Trans (A)", 2D) = "white" {}
  _Colour("Colour", Color) = (1,1,1,1)
  _BumpMap("Noise text", 2D) = "bump" {}
  _Magnitude("Magnitude", Range(0,1)) = 0.05
}
- 在第二次传递中添加它们的变量:
sampler2D _MainTex;
fixed4 _Colour;
sampler2D _BumpMap;
float _Magnitude;
- 将纹理信息添加到输入和输出结构中:
float2 texcoord : TEXCOORD0;
- 将 UV 数据从输入传输到输出结构:
// Vertex function
vertOutput vert(vertInput v) 
{
  vertOutput o;
  o.vertex = UnityObjectToClipPos(v.vertex);
  o.uvgrab = ComputeGrabScreenPos(o.vertex);
  o.texcoord = v.texcoord;
  return o;
}
- 使用以下片段函数:
half4 frag(vertOutput i) : COLOR 
{
  half4 mainColour = tex2D(_MainTex, i.texcoord);
  half4 bump = tex2D(_BumpMap, i.texcoord);
  half2 distortion = UnpackNormal(bump).rg;
  i.uvgrab.xy += distortion * _Magnitude;
  fixed4 col = tex2Dproj(_GrabTexture, UNITY_PROJ_COORD(i.uvgrab));
  return col * mainColour * _Colour;
}
- 此材质是透明的,因此它在 SubShader块中更改其标记:
Tags{ "Queue" = "Transparent" "IgnoreProjector" = "True" "RenderType" =
  "Opaque" }
- 现在剩下的是设置玻璃的纹理,以及用于偏移抓取纹理的法线图:

它是如何工作的...
此着色器使用的核心是抓取通道,以获取屏幕上已经渲染的内容。扭曲发生的地方在片段函数中。在这里,一个法线图被解包并用于偏移抓取纹理的 UV 数据:
half4 bump = tex2D(_BumpMap, i.texcoord);
half2 distortion = UnpackNormal(bump).rg;
i.uvgrab.xy += distortion * _Magnitude;
_Magnitude 滑块用于确定效果有多强:

还有更多...
此效果非常通用;它抓取屏幕并根据法线图创建扭曲。没有理由它不能用来模拟更有趣的东西。许多游戏使用爆炸或其他科幻设备周围的扭曲。此材质可以应用于球体,并且使用不同的法线图,它可以完美地模拟爆炸的热浪。
为 2D 游戏实现水着色器
在上一个配方中引入的玻璃着色器是静态的;其扭曲从不改变。只需进行一些更改即可将其转换为动画材质,使其非常适合具有水的 2D 游戏。这使用与 第六章 中 Vertex Functions 配方中显示的类似技术:

准备工作
此配方基于 使用抓取通道绘制在物体后面 配方中描述的顶点和片段着色器,因为它将严重依赖于 GrabPass。
- 
创建一个新的顶点着色器和片段着色器。你可以通过选择它并按 Ctrl+D 复制来以“使用抓取通道绘制在物体后面”中使用的着色器作为基础,然后将其名称更改为 WaterShader。
- 
创建一个将使用着色器的材质( WaterMat)。
- 
将材质分配给一个平面几何体,该几何体将代表你的 2D 水。为了使此效果生效,你应该在它后面渲染一些内容,以便你可以看到类似水的位移: 

- 这个配方需要一个噪声纹理,它用于获取伪随机值。选择无缝噪声纹理非常重要,例如由可平铺的 2D Perlin 噪声生成的纹理,如下面的截图所示。这确保了当材质应用于大型物体时,你不会看到任何不连续性。为了使此效果生效,纹理必须以重复模式导入。如果你想为你的水创建平滑且连续的外观,你还应该将其设置为从检查器中的双线性。这些设置确保从着色器正确采样纹理:

你可以在书的示例代码的 第六章 | 纹理 文件夹中找到一个示例噪声纹理。
如何操作...
要创建这个动画效果,你可以从重新调整着色器开始。按照以下步骤操作:
- 添加以下属性:
_NoiseTex("Noise text", 2D) = "white" {}
_Colour ("Colour", Color) = (1,1,1,1)
_Period ("Period", Range(0,50)) = 1
_Magnitude ("Magnitude", Range(0,0.5)) = 0.05
_Scale ("Scale", Range(0,10)) = 1
- 将它们各自的变量添加到着色器的第二次传递中:
sampler2D _NoiseTex;
fixed4 _Colour;
float _Period;
float _Magnitude;
float _Scale;
- 为顶点函数定义以下输入和输出结构:
struct vertInput 
{
  float4 vertex : POSITION;
  fixed4 color : COLOR;
  float2 texcoord : TEXCOORD0;
};
struct vertOutput 
{
  float4 vertex : POSITION;
  fixed4 color : COLOR;
  float2 texcoord : TEXCOORD0;
  float4 worldPos : TEXCOORD1;
  float4 uvgrab : TEXCOORD2;
};
- 这个着色器需要知道每个片段空间的确切位置。为此,更新顶点函数如下:
// Vertex function
vertOutput vert(vertInput v) 
{
  vertOutput o;
  o.vertex = UnityObjectToClipPos(v.vertex);
  o.color = v.color;
  o.texcoord = v.texcoord;
  o.worldPos = mul(unity_ObjectToWorld, v.vertex);
  o.uvgrab = ComputeGrabScreenPos(o.vertex);
  return o;
}
- 使用以下片段函数:
fixed4 frag (vertOutput i) : COLOR 
{
  float sinT = sin(_Time.w / _Period);
  float distX = tex2D(_NoiseTex, i.worldPos.xy / _Scale +                 float2(sinT,
    0) ).r - 0.5;
  float distY = tex2D(_NoiseTex, i.worldPos.xy / _Scale + float2(0,
    sinT) ).r - 0.5;
  float2 distortion = float2(distX, distY);
  i.uvgrab.xy += distortion * _Magnitude;
  fixed4 col = tex2Dproj( _GrabTexture, UNITY_PROJ_COORD(i.uvgrab));
  return col * _Colour;
}
- 保存你的脚本并返回 Unity 编辑器。之后,选择你的水材质(WatMat)并应用噪声纹理。之后,调整水材质中的属性,注意它如何修改其后的内容。

它是如何工作的…
这个着色器与在实现玻璃着色器配方中介绍的那个非常相似。主要区别在于这是一个动画材质;位移不是从法线图中生成的,而是考虑当前时间以创建持续的动画。位移 UV 数据的代码似乎相当复杂;让我们尝试理解它是如何生成的。其背后的想法是使用正弦函数使水振荡。这个效果需要随时间演变;为了实现这个效果,着色器生成的扭曲取决于通过内置变量_Time检索的当前时间。_Period变量决定了正弦波的周期,这意味着波浪出现的速度:
float2 distortion = float2( sin(_Time.w/_Period), 
  sin(_Time.w/_Period) ) – 0.5;
这段代码的问题在于你在 X 轴和 Y 轴上有相同的位移;因此,整个抓取纹理将以圆形运动旋转,看起来根本不像水。显然,我们需要添加一些随机性。
向着色器添加随机行为的最常见方法是包含一个噪声纹理。现在的问题是找到一种在看似随机位置采样纹理的方法。为了避免看到明显的正弦波模式,最好的方法是在_NoiseTex纹理的 UV 数据中使用正弦波作为偏移量:
float sinT = sin(_Time.w / _Period);
float2 distortion = float2( 
    tex2D(_NoiseTex, i.texcoord / _Scale + float2(sinT, 0) ).r - 0.5,
    tex2D(_NoiseTex, i.texcoord / _Scale + float2(0, sinT) ).r - 0.5
);
_Scale变量决定了波浪的大小。这个解决方案更接近最终版本,但有一个严重的问题——如果水四边形移动,UV 数据会跟随它,你会看到水波跟随材质而不是锚定在背景上。为了解决这个问题,我们需要使用当前片段的世界位置作为 UV 数据的初始位置:
float sinT = sin(_Time.w / _Period);
float2 distortion = float2( 
    tex2D(_NoiseTex, i.worldPos.xy / _Scale + float2(sinT, 0) ).r - 0.5,
    tex2D(_NoiseTex, i.worldPos.xy / _Scale + float2(0, sinT) ).r - 0.5
);
i.uvgrab.xy += distortion * _Magnitude;
结果是一种愉快、无缝的扭曲,不会向任何明显的方向移动。
我们还可以通过将扭曲分解成更小的步骤来提高代码的可读性:
float sinT = sin(_Time.w / _Period);
float distX = tex2D(_NoiseTex, i.worldPos.xy / _Scale + 
  float2(sinT, 0) ).r - 0.5;
float distY = tex2D(_NoiseTex, i.worldPos.xy / _Scale + 
  float2(0, sinT) ).r - 0.5;
float2 distortion = float2(distX, distY);
i.uvgrab.xy += distortion * _Magnitude;
这就是最终结果中你应该看到的内容。
就像所有这些特殊效果一样,没有完美的解决方案。这个配方向你展示了一种创建类似水波扭曲的技术,但鼓励你尝试,直到找到适合你游戏美学的效果。
第八章:移动设备着色器调整
在接下来的两个章节中,我们将探讨如何使我们在不同平台上编写的着色器以性能友好的方式运行。我们不会具体讨论任何单一平台,但我们将分解我们可以调整的着色器元素,以便使它们更适合移动设备,并在任何平台上更高效。这些技术包括了解 Unity 提供的内置变量,这些变量可以减少着色器内存的负担,以及了解我们可以如何使自己的着色器代码更高效。本章将涵盖以下配方:
- 
提高着色器效率的技术 
- 
分析你的着色器 
- 
修改我们的着色器以适应移动设备 
简介
学习优化你的着色器的艺术几乎会在你参与的任何游戏项目中出现。在任何制作过程中,总会有一个时刻需要优化着色器,或者可能需要使用更少的纹理但产生相同的效果。作为一个技术艺术家或着色器程序员,你必须理解这些核心基础,以便优化你的着色器,从而在保持相同视觉保真度的同时提高你游戏的表现。拥有这些知识还可以帮助你从开始就设定你编写着色器的方式。例如,通过知道使用你的着色器构建的游戏将在移动设备上运行,我们可以自动将所有的Lighting函数设置为使用半向量作为视图方向,或者将所有浮点变量类型设置为固定或半型,以减少使用的内存量。这些以及其他许多技术,都对你的着色器在目标硬件上高效运行做出了贡献。让我们开始我们的旅程,开始学习如何优化我们的着色器。
提高着色器效率的技术
什么是廉价的着色器?当第一次被问到这个问题时,可能有点难以回答,因为有很多元素会影响着色器的效率。这可能包括你的变量使用的内存量。这可能还包括着色器使用的纹理数量。也可能是因为我们的着色器运行良好,但实际上我们可以通过减少我们使用的代码量或创建的数据量,用一半的数据量产生相同的视觉效果。在这个配方中,我们将探索一些这些技术,并展示如何将它们结合起来,使你的着色器快速高效,同时仍然产生今天游戏玩家所期望的高质量视觉效果,无论是在移动设备还是 PC 上。
准备工作
为了启动这个配方,我们需要收集一些资源。所以,让我们执行以下任务:
- 
创建一个新的场景,并在其中添加一个简单的球体对象和单一方向性光源。 
- 
创建一个新的着色器( OptimizedShader01)和材质(OptimizedShader01Mat),并将着色器分配给材质。
- 
然后,我们需要将我们刚刚创建的材质分配到我们的球体对象中 新场景: 

- 最后,修改着色器,使其使用漫反射纹理和法线贴图,并包含您自己的自定义Lighting函数。
Properties 
{
  _MainTex ("Base (RGB)", 2D) = "white" {}
  _NormalMap ("Normal Map", 2D) = "bump" {}
}
SubShader 
{
  Tags { "RenderType"="Opaque" }
  LOD 200
  CGPROGRAM
  #pragma surface surf SimpleLambert 
  sampler2D _MainTex;
  sampler2D _NormalMap;
  struct Input 
  {
    float2 uv_MainTex;
    float2 uv_NormalMap;
  };
  inline float4 LightingSimpleLambert (SurfaceOutput s, 
                                       float3 lightDir, 
                                       float atten)
  {
   float diff = max (0, dot (s.Normal, lightDir));
   float4 c;
   c.rgb = s.Albedo * _LightColor0.rgb * (diff * atten * 2);
   c.a = s.Alpha;
   return c;
  }
  void surf (Input IN, inout SurfaceOutput o) 
  {
    fixed4 c = tex2D (_MainTex, IN.uv_MainTex);
    o.Albedo = c.rgb;
    o.Alpha = c.a;
    o.Normal = UnpackNormal(tex2D(_NormalMap, IN.uv_NormalMap));
  }
  ENDCG
} 
FallBack "Diffuse"
- 
最后,将基础和法线贴图分配给您的材质(我在第一章,后处理堆栈)中包含的 MudRockey纹理)。现在您应该有一个类似于以下截图的设置。
- 
这种设置将使我们能够查看一些基本概念,这些概念在 Unity 中使用表面着色器优化着色器时是必不可少的: 

如何做到这一点...
我们将构建一个简单的DiffuseShader,以便查看您在一般情况下可以优化着色器的几种方法。
首先,我们将优化我们的变量类型,以便它们在占用更少内存的情况下工作。
处理数据:
- 
让我们从着色器中的 struct Input开始。目前,我们的 UVs 被存储在一个float2类型的变量中。
- 
记住,浮点数提供了最高形式的精度,占用完整的 32 位内存。这对于复杂的三角函数或指数运算来说是必需的,但如果您能处理更低的精度,使用半精度或固定精度会更好。半精度类型使用一半的大小,即 16 位内存,提供高达 3 位的精度。这意味着我们可以有一个 half2,其内存量与单个浮点数相同。我们需要将其更改为使用half2:
struct Input 
{
    half2 uv_MainTex;
    half2 uv_NormalMap;
};
- 然后,我们可以转到我们的Lighting函数,通过将变量的类型更改为以下内容来减少它们的内存占用:
inline fixed4 LightingSimpleLambert (SurfaceOutput s, fixed3 lightDir, fixed atten)
{
  fixed diff = max (0, dot(s.Normal, lightDir));
  fixed4 c;
  c.rgb = s.Albedo * _LightColor0.rgb * (diff * atten * 2);
  c.a = s.Alpha;
  return c;
}
- 在这种情况下,我们使用的是fixed类型的最低精度,它只有 11 位,而float类型有 32 位。这对于简单的计算,如颜色或纹理数据,非常适用,这正是这个特定案例的情况。
如果您想复习固定类型以及我们正在使用的所有其他类型,请查看第二章,创建您的第一个着色器,或者查看docs.unity3d.com/Manual/SL-DataTypesAndPrecision.html。
- 最后,我们可以通过更新我们的surf()函数中的变量来完成这次优化过程。由于我们正在使用纹理数据,因此在这里使用fixed4是完全可以的:
void surf (Input IN, inout SurfaceOutput o) 
{
  fixed4 c = tex2D (_MainTex, IN.uv_MainTex);
  o.Albedo = c.rgb;
  o.Alpha = c.a;
  o.Normal = UnpackNormal(tex2D(_NormalMap, IN.uv_NormalMap));
}
- 现在我们已经优化了变量,我们将利用内置的Lighting函数变量,以便我们可以控制这个着色器如何处理光线。通过这样做,我们可以大大减少着色器处理的灯光数量。使用以下代码修改您的着色器中的#pragma语句:
CGPROGRAM
#pragma surface surf SimpleLambert noforwardadd
- 我们可以通过在法线贴图和漫反射纹理之间共享 UV 来进一步优化。为此,我们只需将我们的UnpackNormal()函数中的 UV 查找更改为使用_MainTexUVs 而不是_NormalMap的 UVs:
void surf (Input IN, inout SurfaceOutput o) 
{
  fixed4 c = tex2D (_MainTex, IN.uv_MainTex);
  o.Albedo = c.rgb;
  o.Alpha = c.a;
  o.Normal = UnpackNormal(tex2D(_NormalMap, IN.uv_MainTex));
}
- 由于我们已经消除了对法线图 UV 的需求,我们需要确保从Inputstruct中删除法线图 UV 代码:
struct Input 
{
  half2 uv_MainTex;
};
- 最后,我们可以通过告诉着色器它只与某些渲染器一起工作来进一步优化这个着色器:
CGPROGRAM
#pragma surface surf SimpleLambert exclude_path:prepass noforwardadd
我们优化过程的结果显示,我们在视觉质量上并没有真正注意到差异,但我们已经减少了这个着色器绘制到屏幕上所需的时间。你将在下一道菜谱中了解到如何找出着色器渲染所需的时间,但这里要关注的思想是,我们用更少的数据实现了相同的结果。所以当你创建着色器时要记住这一点。以下截图显示了我们的着色器的最终结果:

它是如何工作的...
现在我们已经看到了我们可以如何优化我们的着色器,让我们更深入地研究一下,真正理解所有这些技术是如何工作的,为什么我们应该使用它们,并看看一些你可以在自己的着色器中尝试的其他技术。
首先,让我们关注一下当我们声明变量时,每个变量所存储的数据大小。如果你熟悉编程,那么你会明白你可以用不同大小的类型来声明值或变量。这意味着浮点数实际上在内存中有一个最大大小。以下描述将更详细地描述这些变量类型:
- 
Float:float 是一个完整的 32 位精度值,是三种类型中最慢的。 我们在这里看到的不同类型。它也有其对应的 float2、float3和float4的值,这些值允许我们在一个变量中存储多个浮点数。
- 
Half:half 变量类型是一个减少的 16 位浮点值,适合存储 UV 值和颜色值,比使用 float 值快得多。与 float 类型一样,它也有其对应的值,即 half2、half3和half4。
- 
Fixed:fixed 值是三种类型中最小的,但可以用于光照计算和颜色,并且有对应的 fixed2、fixed3和fixed4值。
有关在着色器中使用数组类型的更多信息,请参阅第三章的使用打包数组菜谱,表面着色器和纹理映射。
我们优化简单着色器的第二阶段是向我们的#pragma语句中声明noforwardadd值。这基本上是一个自动告诉 Unity 任何具有这种特定着色器的对象只从单个方向光接收每像素光的开关。任何其他由这个着色器计算的光都将被迫以 Unity 内部产生的球谐函数值作为每顶点光进行处理。当我们放置另一个灯光照亮场景中的球体对象时,这一点尤其明显,因为我们的着色器正在使用法线图进行每像素操作。
这很好,但如果你想在场景中有一堆方向光,并控制这些光中哪一盏用作主每像素光呢?注意,每个灯光都有一个渲染模式下拉菜单。如果你点击这个下拉菜单,你会看到可以设置的一些标志。这些是自动、重要和不重要。通过选择一个灯光,你可以告诉 Unity,通过将其渲染模式设置为重要,一个灯光应该被视为比顶点光更接近每像素光,反之亦然。如果你将灯光设置为自动,那么你将让 Unity 决定最佳行动方案:

在场景中放置另一盏灯,并移除当前用于我们着色器的主纹理中的纹理。你会注意到第二个点光源不会与法线图反应,只有我们最初创建的方向光会反应。这里的理念是通过仅计算所有额外的灯光作为顶点灯光来节省每像素操作,并通过仅计算主方向光作为每像素光来节省性能。以下图表直观地展示了这一概念,因为点光源不会与法线图反应:

最后,我们进行了一些清理工作,简单地将法线贴图纹理的 UV 值设置为使用主纹理的 UV 值,并去掉了专门为法线图提取一组 UV 值的代码行。这是一种简化代码和清理任何不需要数据的不错方法。
我们还在#pragma语句中声明了exclude_pass: prepass,这样
着色器不接受来自延迟渲染器的任何自定义光照。这意味着我们只能在正向渲染器中有效地使用这个着色器,这是在主相机的设置中设置的。
通过花点时间,你会对着色器可以优化到什么程度感到惊讶。你已经看到了我们如何将灰度纹理打包到单个 RGBA 纹理中,以及如何使用查找纹理来模拟光照。着色器可以通过多种方式优化,这也是为什么一开始就问这个问题总是模糊不清的原因,但了解这些不同的优化技术,你可以根据你的游戏和目标平台定制着色器,最终得到非常流畅的着色器和稳定的帧率。
着色器性能分析
现在我们知道了如何减少我们的着色器可能带来的开销,让我们看看如何在有大量着色器或大量对象、着色器和脚本同时运行的场景中找到有问题的着色器。在整款游戏中找到一个单独的对象或着色器可能相当困难,但 Unity 为我们提供了其内置的 Profiler。这允许我们实际上在每一帧的基础上看到游戏中发生了什么,以及 GPU 和 CPU 正在使用的每个项目。
使用 Profiler,我们可以通过其界面创建分析作业的块来隔离如着色器、几何体和一般渲染项。我们可以过滤出项目,直到我们只看到单个对象的表现。这样,我们就可以看到对象在运行时执行其功能时对 CPU 和 GPU 产生的影响。
让我们浏览 Profiler 的不同部分,并学习如何调试我们的场景,最重要的是,我们的着色器。
准备工作
让我们通过准备一些资产并启动 Profiler 窗口来使用我们的 Profiler:
- 
让我们使用上一道菜中的场景,并从窗口 | Profiler 或Ctrl + 7启动 Unity Profiler。请随意拖放或移动它,以便您可以清楚地看到。我个人把它放在 Inspector 标签页的同一位置。 
- 
让我们再复制我们的球体几次,看看这对我们的渲染有什么影响。 
- 
从 Profiler 标签页,点击 Deep Profile 选项以获取有关项目的更多信息,然后玩游戏! 
您应该看到以下类似图像:

如何做到这一点...
要使用 Profiler,我们将查看这个窗口的一些 UI 元素。在我们按 Play 之前,让我们看看如何从 Profiler 中获取所需的信息:
- 首先,点击 Profiler 窗口中称为 GPU Usage、CPU Usage 和 Rendering 的较大块。您可以在窗口的左侧找到这些块:

- 使用这些块,我们可以看到针对我们游戏的主要功能的不同数据。CPU Usage 显示了我们大多数脚本正在做什么,以及物理和整体渲染。GPU Usage 块提供了关于我们照明、阴影和渲染队列的特定元素的确切信息。最后,Rendering 块提供了关于 drawcalls 和在任何一帧中我们场景中的几何体数量的信息。
如果您看不到 GPU Usage 选项,请点击 Add Profiler | GPU。如果您的显卡驱动程序未更新,它可能不会显示。
通过点击这些块中的每一个,我们可以在分析会话期间隔离我们看到的数据类型。
- 
现在,点击这些 Profile 块中的一个彩色小方块,然后按 Play 键,或Ctrl + P,以运行场景。 
- 
这使我们能够进一步深入我们的分析会话,以便我们可以过滤出返回给我们的信息。当场景运行时,除了 GPU Usage 块中的 Opaque 之外,取消选中所有复选框。注意,我们现在可以看到渲染到不透明渲染队列的对象所花费的时间: 

- 
Profiler 窗口的另一个出色功能是在图表视图中点击和拖动。 
- 
这将自动暂停你的游戏,以便你可以进一步分析图表中的某个峰值,以找出确切是哪个项目导致了性能问题。在图表视图中点击并拖动以暂停游戏并查看使用此功能的效果: 

- 将我们的注意力转向 Profiler 窗口的下半部分,你会注意到当我们选择了 GPU Block 时,会出现一个下拉菜单项。我们可以展开这个菜单以获取关于当前活动分析会话的更详细信息,在这个例子中,是关于相机当前渲染的内容及其占用时间的更多信息:

如果你点击显示为“无详细信息”的按钮,并将选项更改为“显示相关对象”,你可以看到在调用的函数中使用哪些对象。
- 
这为我们提供了 Unity 在此特定帧中处理内部工作的完整视图。在这种情况下,我们可以看到我们的三个带有优化着色器的球体绘制到屏幕上大约需要 0.066 毫秒,它们占用了十五次绘制调用,并且这个过程在每个帧中占用了 GPU 的 8.4%时间(这些数字可能因你的电脑硬件而异)。我们可以使用这类信息来诊断和解决与着色器相关的性能问题。让我们进行一个测试,看看向我们的着色器添加一个纹理并使用 lerp函数混合两个漫反射纹理的效果。你将在 Profiler 中清楚地看到这些效果。
- 
使用以下代码修改你的着色器的 Properties块,以给我们另一个纹理使用:
Properties 
{
  _MainTex ("Base (RGB)", 2D) = "white" {}
  _BlendTex("Blend Texture", 2D) = "white" {}
  _NormalMap ("Normal Map", 2D) = "bump" {}
}
- 然后,让我们将我们的纹理传递给CGPROGRAM:
sampler2D _MainTex;
sampler2D _NormalMap;
sampler2D _BlendTex;
- 现在是时候更新我们的surf()函数,以便我们将漫反射纹理混合在一起:
void surf (Input IN, inout SurfaceOutput o) 
{
  fixed4 c = tex2D (_MainTex, IN.uv_MainTex);
  fixed4 blendTex = tex2D(_BlendTex, IN.uv_MainTex);
  c = lerp(c, blendTex, blendTex.r);
  o.Albedo = c.rgb;
  o.Alpha = c.a;
  o.Normal = UnpackNormal(tex2D(_NormalMap, IN.uv_MainTex));
}
保存你的着色器修改并返回 Unity 编辑器后,我们可以运行我们的游戏并查看新着色器带来的毫秒级增加。
- 在你的Blend Texture中附加一个新的纹理:

- 按下播放按钮以再次启动游戏并开启 Profiler。返回 Unity 后按下播放按钮,让我们看看 Profiler 中的结果:

现在,你可以看到在这个场景中渲染我们的不透明着色器所需的时间为 0.069 毫秒,比 0.066 毫秒有所增加。通过添加另一个纹理并使用lerp()函数,我们增加了我们的球体的渲染时间。虽然这是一个小的变化,但想象一下有 20 个着色器以不同的方式在不同的对象上工作。
使用这里提供的信息,你可以更快地定位导致性能下降的区域,并使用前一个菜谱中的技术来解决这些问题。
它是如何工作的...
尽管本书完全不涉及描述这个工具内部如何工作的细节,但我们可以推测 Unity 为我们提供了一种在游戏运行时查看计算机性能的方法。基本上,这个窗口与 CPU 和 GPU 紧密相连,以便为我们提供每个脚本、对象和渲染队列所花费时间的实时反馈。利用这些信息,我们发现我们可以追踪我们的着色器编写的效率,以消除问题区域和代码。
重要的是要注意,使用 Profiler 打开的游戏,以及通常在编辑器中运行的游戏,会比在正常情况下编译和运行时慢一些。你甚至可能会在 CPU 开销列表中看到 Editor。
还有更多...
也可以专门针对移动平台进行性能分析。当在构建设置中将 Android 或 iOS 设置为构建目标时,Unity 为我们提供了一些额外的功能。我们实际上可以在游戏运行时从我们的移动设备获取实时信息。这非常有用,因为你可以直接在设备上而不是在编辑器中直接进行性能分析。要了解更多关于这个过程的详细信息,请参考以下链接的 Unity 文档:
docs.unity3d.com/Documentation/Manual/MobileProfiling.html
为移动设备修改我们的着色器
现在我们已经看到了一系列针对真正优化着色器的技术,让我们来看看如何编写一个针对移动设备的优质、高质量着色器。实际上,对已编写的着色器进行一些调整以使其在移动设备上运行得更快是非常容易的。这包括使用approxview或halfasview Lighting函数变量等元素。我们还可以减少所需的纹理数量,甚至为使用的纹理应用更好的压缩。到这个配方结束时,我们将有一个优化良好的正常贴图、高光着色器,适用于我们的移动游戏。
准备工作
在我们开始之前,让我们创建一个全新的场景,并填充一些对象以应用我们的MobileShader:
- 
创建一个新的场景,并填充一个默认的球体和一个单方向光源。 
- 
创建一个新的材质( MobileMat)和一个着色器(MobileShader),并将着色器分配给材质。
- 
最后,将材质分配到场景中的球体对象上。 
完成后,你应该有一个类似于以下截图的场景:

如何操作...
对于这个配方,我们将从头开始编写一个适合移动设备的着色器,并讨论使其更适合移动设备的元素:
- 让我们先在我们的Properties块中填充所需的纹理。在这种情况下,我们将使用一个带有光泽图在其 alpha 通道中的单个_Diffuse纹理,Normal map和一个用于光泽强度滑块的滑块:
Properties 
{
  _Diffuse ("Base (RGB) Specular Amount (A)", 2D) = "white" {}
  _SpecIntensity ("Specular Width", Range(0.01, 1)) = 0.5
  _NormalMap ("Normal Map", 2D) = "bump"{}
}
- 我们下一个任务是设置我们的#pragma声明。这将简单地打开或关闭表面着色器的某些功能,最终使着色器更便宜或更昂贵:
CGPROGRAM
#pragma surface surf MobileBlinnPhong exclude_path:prepass nolightmap noforwardadd halfasview
突出的行应该放在一行上。
- 
接下来,删除 #pragma target 3.0行,因为我们没有使用它的任何特性。
- 
然后,我们需要在我们的 Properties块和CGPROGRAM之间建立连接。这次,我们将使用固定变量类型来减少光泽强度滑块的内存使用:
sampler2D _Diffuse;
sampler2D _NormalMap;
fixed _SpecIntensity;
- 为了将我们的纹理映射到我们对象的表面,我们需要获取一些 UV 坐标。在这种情况下,我们将只获取一组 UV 坐标,以将我们的着色器中的数据量降到最低:
struct Input 
{
  half2 uv_Diffuse;
};
- 下一步是使用新#pragma声明中可用的几个新输入变量来填写我们的Lighting函数:
inline fixed4 LightingMobileBlinnPhong (SurfaceOutput s, fixed3 lightDir, fixed3 halfDir, fixed atten)
    {
      fixed diff = max (0, dot (s.Normal, lightDir));
      fixed nh = max (0, dot (s.Normal, halfDir));
      fixed spec = pow (nh, s.Specular*128) * s.Gloss;
      fixed4 c;
      c.rgb = (s.Albedo * _LightColor0.rgb * diff + _LightColor0.rgb * spec) * (atten*2);
      c.a = 0.0;
      return c;
    }
- 最后,我们通过创建surf()函数并处理我们表面的最终颜色来完成着色器:
void surf (Input IN, inout SurfaceOutput o) 
{
  fixed4 diffuseTex = tex2D (_Diffuse, IN.uv_Diffuse);
  o.Albedo = diffuseTex.rgb;
  o.Gloss = diffuseTex.a;
  o.Alpha = 0.0;
  o.Specular = _SpecIntensity;
  o.Normal = UnpackNormal(tex2D(_NormalMap, IN.uv_Diffuse));
}
- 当完成这个菜谱的代码部分后,保存你的着色器并返回 Unity 编辑器,让着色器进行编译。如果没有发生错误,为基色和法线贴图属性分配一些属性:

- 添加几个点光源和一些新对象的副本,你应该会看到以下截图类似的结果:

它是如何工作的...
因此,让我们通过解释这个着色器做什么和不做什么来开始对这个着色器的描述。首先,它排除了延迟光照通道。这意味着如果你创建了一个连接到延迟渲染器的prepass的Lighting函数,它将不会使用那个特定的Lighting函数,而是会寻找默认的Lighting函数,就像我们在本书中迄今为止创建的那样。
这个特定的着色器不支持 Unity 内部的光照贴图系统。这仅仅阻止着色器尝试为着色器附加的对象查找光照贴图,这使得着色器更易于性能优化,因为它不需要执行光照贴图检查。
我们包含了noforwardadd声明,这样我们只处理具有单一方向光的单像素纹理。所有其他灯光都将被强制转换为顶点灯光,并且不会包含在surf()函数中你可能进行的任何单像素操作中。
最后,我们使用halfasview声明来告诉 Unity 我们不会使用在常规Lighting函数中找到的viewDir主参数。相反,我们将使用半向量作为视图方向,并以此处理我们的镜面反射。这样做使得着色器处理速度更快,因为它将在每个顶点上完成。虽然在模拟现实世界中的镜面反射时并不完全准确,但在移动设备上视觉上看起来相当不错,且着色器更加优化。
正是这类技术使得着色器在代码上更加高效和简洁。始终确保你只使用所需的数据,同时权衡你的目标硬件和游戏所需的视觉质量。最终,这些技术的混合使用构成了你游戏中着色器的核心。
第九章:使用 Unity 渲染纹理的屏幕效果
在本章中,你将学习以下食谱:
- 
设置屏幕效果脚本系统 
- 
使用亮度、饱和度和对比度与屏幕效果 
- 
使用类似 Photoshop 的基本混合模式与屏幕效果 
- 
使用叠加混合模式与屏幕效果 
简介
学习编写着色器最令人印象深刻的一面是创建自己的屏幕效果的过程,也称为后期效果。有了这些屏幕效果,我们可以通过泛光、运动模糊、HDR 效果等创建令人惊叹的实时图像。如今市场上大多数现代游戏都大量使用这些屏幕效果来实现景深效果、泛光效果,甚至色彩校正效果。
在第一章“后期处理堆栈”中,我们讨论了如何使用 Unity 内置的后期处理堆栈,但在这章中,你将学习如何自己构建脚本系统。这个系统将赋予你创建许多种屏幕效果的控件。我们将涵盖RenderTexture、深度缓冲区是什么,以及如何创建能够让你对游戏最终渲染图像有类似 Photoshop 控制效果的方法。通过为你的游戏利用屏幕效果,你不仅完善了你的着色器编写知识,而且还将拥有从零开始使用 Unity 创建自己令人难以置信的实时渲染的能力。
设置屏幕效果脚本系统
创建屏幕效果的过程是我们抓取全屏图像(或纹理),使用着色器在 GPU 上处理其像素,然后将它发送回 Unity 的渲染器以应用于整个游戏的渲染图像。这使我们能够在实时中对游戏的渲染图像进行逐像素操作,从而给我们提供更全局的艺术控制。
想象一下,如果你必须逐个调整游戏中每个对象的材质,仅仅调整游戏最终外观的对比度。虽然不是不可能,但这需要一些劳动来完成。通过利用屏幕效果,我们可以整体调整屏幕的最终外观,从而让我们对游戏最终外观有更多的类似 Photoshop 的控制。
为了让屏幕效果系统运行起来,我们必须设置一个单独的脚本来作为游戏当前渲染图像的使者,或者 Unity 所说的RenderTexture。通过利用这个脚本将RenderTexture传递给着色器,我们可以创建一个灵活的系统来建立屏幕效果。对于我们的第一个屏幕效果,我们将创建一个非常简单的灰度效果,使我们的游戏看起来是黑白的。让我们看看这是如何实现的。
准备工作
为了让我们的屏幕效果系统运行起来,我们需要为我们的当前 Unity 项目创建一些资产。通过这样做,我们将为以下章节中的步骤做好准备:
- 
在当前项目中创建一个新的场景来工作。 
- 
在场景中创建一个简单的球体,并为其分配一个新的材质(我称之为 RedMat)。这个新材质可以是任何东西,但为了我们的示例,我们将使用 Standard Shader 创建一个简单的红色材质。
- 
最后,创建一个新的方向光并保存场景。 
- 
我们需要创建一个新的 C#脚本并命名为 TestRenderImage.cs。为了组织目的,从项目选项卡创建一个名为Scripts的文件夹来放置它。
在所有资产准备就绪后,你应该有一个简单的场景设置,看起来类似于以下截图:

如何做到这一点...
为了让我们的灰度屏幕效果工作,我们需要一个脚本和着色器。因此,我们将在这里完成这两个新项目,并填充适当的代码以产生我们的第一个屏幕效果。我们的第一个任务是完成 C#脚本。这将使整个系统运行。在此之后,我们将完成着色器并查看屏幕效果的结果。让我们按照以下步骤完成我们的脚本和着色器:
- 打开TestRenderImage.csC#脚本,首先输入一些我们将需要存储重要对象和数据的变量。在TestRenderImage类的顶部输入以下代码:
#region Variables
public Shader curShader;
public float greyscaleAmount = 1.0f;
private Material screenMat;
#endregion
- 为了让我们能够实时编辑屏幕效果,当 Unity 编辑器没有播放时,我们需要在TestRenderImage类的声明上方输入以下代码行:
using UnityEngine;
[ExecuteInEditMode]
public class TestRenderImage : MonoBehaviour {
- 由于我们的屏幕效果正在使用着色器在我们的屏幕图像上执行像素操作,我们必须创建一个材质来运行着色器。没有这个,我们无法访问着色器的属性。为此,我们将创建一个 C#属性来检查材质,并在找不到时创建一个。在步骤 1的变量声明之后输入以下代码:
#region Properties
Material ScreenMat
{
    get
    {
        if (screenMat == null)
        {
            screenMat = new Material(curShader);
            screenMat.hideFlags = HideFlags.HideAndDontSave;
        }
        return screenMat;
    }
}
#endregion
- 现在,我们想在脚本中设置一些检查,以查看我们正在构建 Unity 游戏的当前目标平台是否实际上支持图像效果。如果脚本开始时找不到任何东西,那么脚本将禁用自己:
void Start()
{
    if (!SystemInfo.supportsImageEffects)
    {
        enabled = false;
        return;
    }
    if (!curShader && !curShader.isSupported)
    {
        enabled = false;
    }
}
- 为了从 Unity 渲染器实际获取渲染的图像,我们需要使用 Unity 为我们提供的以下内置函数,称为OnRenderImage()。输入以下代码以便我们可以访问当前的RenderTexture:
void OnRenderImage(RenderTexture sourceTexture, RenderTexture destTexture)
{
    if (curShader != null)
    {
        ScreenMat.SetFloat("_Luminosity", greyscaleAmount);
        Graphics.Blit(sourceTexture, destTexture, ScreenMat);
    }
    else
    {
        Graphics.Blit(sourceTexture, destTexture);
    }
}
- 我们的屏幕效果有一个名为grayScaleAmount的变量,我们可以用它来控制我们想要的最终屏幕效果的灰度程度。因此,在这种情况下,我们需要将值从0变为1,其中0表示没有灰度效果,而1表示全灰度效果。我们将在Update()函数中执行此操作,该函数将在游戏运行时每帧被调用:
void Update ()
{
    greyscaleAmount = Mathf.Clamp(greyscaleAmount, 0.0f, 1.0f);
}
- 最后,我们通过在脚本开始时创建的对象上进行一些清理来完成我们的脚本:
void OnDisable()
{
  if(screenMat)
  {
    DestroyImmediate(screenMat);
  }
}
- 
到目前为止,我们可以在 Unity 中将此脚本应用于相机,如果它没有错误地编译,让我们将 TestRenderImage.cs脚本应用到场景中的主相机上。你应该看到grayScaleAmount值和一个着色器字段,但脚本在控制台窗口中抛出一个错误。它说缺少一个对象实例,因此无法适当地处理。如果你还记得 步骤 4,我们正在做一些检查,看看我们是否有着色器以及当前平台是否支持着色器。因为我们没有给屏幕效果脚本提供一个着色器来工作,所以curShader变量只是 null,这会抛出一个错误。让我们通过完成着色器来继续我们的屏幕效果系统。
- 
创建一个新的着色器,命名为 ScreenGrayscale。为了开始我们的着色器,我们将用一些变量填充我们的Properties,这样我们就可以将数据发送到这个着色器:
Properties 
{
 _MainTex ("Base (RGB)", 2D) = "white" {}
 _Luminosity("Luminosity", Range(0.0, 1)) = 1.0
}
- 我们现在的着色器将利用纯 CG 着色器代码,而不是使用 Unity 内置的 Surface Shader 代码。这将使我们的屏幕效果更加优化,因为我们只需要处理 RenderTexture的像素。因此,我们将删除Pass中之前的所有内容,并在我们的着色器中创建一个新的Pass块,并用一些我们之前未见过的新的#pragma语句填充它:
SubShader 
{
  Pass
  {
    CGPROGRAM
    #pragma vertex vert_img
    #pragma fragment frag
    #pragma fragmentoption ARB_precision_hint_fastest
    #include "UnityCG.cginc"
- 为了访问从 Unity 编辑器发送到着色器的数据,我们需要在我们的 CGPROGRAM中创建相应的变量:
uniform sampler2D _MainTex;
fixed _Luminosity;
- 最后,我们只需要设置我们的像素函数,在这个例子中称为 frag()。这是屏幕效果的核心所在。这个函数将处理RenderTexture的每个像素,并将新的图像返回到TestRenderImage.cs脚本:
fixed4 frag(v2f_img i) : COLOR
{
  //Get the colors from the RenderTexture and the uv's
  //from the v2f_img struct
  fixed4 renderTex = tex2D(_MainTex, i.uv);
  //Apply the Luminosity values to our render texture
  float luminosity = 0.299 * renderTex.r + 0.587 * renderTex.g + 0.114 * renderTex.b;
  fixed4 finalColor = lerp(renderTex, luminosity, _Luminosity);
  renderTex.rgb = finalColor;
  return renderTex;
}
- 最后,将 FallBack行更改为以下内容:
FallBack off
- 最终的着色器应如下所示:
Shader "CookbookShaders/Chapter09/Grayscale" 
{
  Properties 
  {
    _MainTex ("Base (RGB)", 2D) = "white" {}
    _Luminosity("Luminosity", Range(0.0, 1)) = 1.0
  }
  SubShader 
  {
    Pass
    {
      CGPROGRAM
      #pragma vertex vert_img
      #pragma fragment frag
      #pragma fragmentoption ARB_precision_hint_fastest
      #include "UnityCG.cginc"
      uniform sampler2D _MainTex;
      fixed _Luminosity;
      fixed4 frag(v2f_img i) : COLOR
      {
        //Get the colors from the RenderTexture and the uv's
        //from the v2f_img struct
        fixed4 renderTex = tex2D(_MainTex, i.uv);
        //Apply the Luminosity values to our render texture
        float luminosity = 0.299 * renderTex.r + 0.587 * renderTex.g + 0.114 * renderTex.b;
        fixed4 finalColor = lerp(renderTex, luminosity, _Luminosity);
        renderTex.rgb = finalColor;
        return renderTex;
      }
    ENDCG
    }
  }
  FallBack off
}
一旦着色器完成,返回 Unity 并让它编译以查看是否发生了错误。如果没有错误,将新的着色器分配给 TestRenderImage.cs 脚本并更改灰度量变量的值。你应该看到游戏视图从彩色游戏版本变为灰度游戏版本:

以下截图演示了此屏幕效果:

完成后,我们现在有一个简单的方法来测试新的屏幕效果着色器,而无需反复编写整个屏幕效果系统。让我们深入了解,了解 RenderTexture 在其存在过程中是如何处理的。
它是如何工作的...
要在 Unity 中实现屏幕效果,我们需要创建一个脚本和着色器。脚本驱动编辑器中的实时更新,并负责从主相机捕获 RenderTexture 并传递给着色器。一旦 RenderTexture 到达着色器,我们就可以使用着色器执行逐像素操作。
在脚本开始时,我们执行一些检查以确保当前选定的构建平台实际上支持屏幕效果以及着色器本身。有些情况下,当前平台可能不支持屏幕效果或我们使用的着色器。因此,我们在 Start() 函数中进行的检查确保如果平台不支持屏幕系统,我们不会遇到任何错误。
一旦脚本通过这些检查,我们就通过调用内置的 OnRenderImage() 函数来初始化屏幕效果系统。这个函数负责获取 renderTexture,使用 Graphics.Blit() 函数将其传递给着色器,并将处理后的图像返回给 Unity 渲染器。你可以在以下网址找到有关这两个函数的更多信息:
- 
OnRenderImage:docs.unity3d.com/Documentation/ScriptReference/MonoBehaviour.OnRenderImage.html
- 
Graphics.Blit:docs.unity3d.com/Documentation/ScriptReference/Graphics.Blit.html
当当前的 RenderTexture 达到着色器时,着色器将其获取,通过 frag() 函数进行处理,并为每个像素返回最终颜色。
你可以看到这有多么强大,因为它让我们对游戏最终渲染图像有了类似 Photoshop 的控制。这些屏幕效果像 Photoshop 层一样按顺序工作,覆盖在摄像机所看到的内容之上。当你一个接一个地放置这些屏幕效果时,它们将按此顺序进行处理。这些只是让屏幕效果工作起来的基本步骤,但这是屏幕效果系统工作的核心。
还有更多...
现在我们已经有一个简单的屏幕效果系统正在运行,让我们看看我们可以从 Unity 的渲染器中获得的一些其他有用信息:

我们实际上可以通过打开 Unity 的内置深度模式来获取我们当前游戏中所有事物的深度。一旦打开,我们就可以使用深度信息来实现大量不同的效果。让我们看看如何实现这一点:
- 将我们创建的球体复制两次,并在下面创建一个平面:

- 
通过选择 ScreenGreyscale代码并按 *Ctrl *+ D 复制来创建一个新的着色器。一旦复制,将脚本重命名为SceneDepth。然后双击此着色器以在脚本编辑器中打开它。
- 
我们将创建主纹理 ( _MainTex) 属性和一个用于控制场景深度效果强度的属性。在你的着色器中输入以下代码:
Properties 
{
  _MainTex ("Base (RGB)", 2D) = "white" {}
  _DepthPower("Depth Power", Range(0, 1)) = 1
}
- 现在我们需要在我们的 CGPROGRAM中创建相应的变量。我们将添加一个名为_CameraDepthTexture的额外变量。这是一个内置变量,Unity 通过使用UnityCG.cginclude文件提供给我们。它为我们提供了来自摄像机的深度信息:
Pass
{
  CGPROGRAM
  #pragma vertex vert_img
  #pragma fragment frag
  #pragma fragmentoption ARB_precision_hint_fastest
  #include "UnityCG.cginc"
  uniform sampler2D _MainTex;
 fixed _DepthPower;
 sampler2D _CameraDepthTexture;
- 我们将通过利用 Unity 为我们提供的几个内置函数来完成我们的深度着色器,这些函数是UNITY_SAMPLE_DEPTH()和linear01Depth()。第一个函数实际上从我们的_CameraDepthTexture中获取深度信息,并为每个像素生成一个单独的浮点值。然后Linear01Depth()函数确保这些值在0-1范围内,通过将最终的深度值取到我们可以控制的幂,其中0-1范围内的中值基于相机位置位于场景中:
fixed4 frag(v2f_img i) : COLOR
{
  //Get the colors from the RenderTexture and the uv's
  //from the v2f_img struct
  float depth = UNITY_SAMPLE_DEPTH(tex2D(_CameraDepthTexture, i.uv.xy));
  depth = pow(Linear01Depth(depth), _DepthPower);
  return depth;
}
- 
在我们的着色器完成后,让我们将注意力转向 Unity 编辑器,并创建一个新的脚本以与之配合。选择我们的 TestRenderImage脚本并复制它。将这个新脚本命名为RenderDepth并在脚本编辑器中打开它。
- 
将脚本更新为与我们在上一步中重命名的类名相同( RenderDepth):
using UnityEngine;
[ExecuteInEditMode]
public class RenderDepth : MonoBehaviour {
- 我们需要在脚本中添加depthPower变量,以便我们可以在编辑器中让用户更改该值:
#region Variables
public Shader curShader;
public float depthPower = 0.2f;
private Material screenMat;
#endregion
- 然后,我们的OnRenderImage()函数需要更新,以便它向我们的着色器传递正确的值:
void OnRenderImage(RenderTexture sourceTexture, RenderTexture destTexture)
{
    if (curShader != null)
    {
        ScreenMat.SetFloat("_DepthPower", depthPower);
        Graphics.Blit(sourceTexture, destTexture, ScreenMat);
    }
    else
    {
        Graphics.Blit(sourceTexture, destTexture);
    }
}
- 为了完成我们的深度屏幕效果,我们需要告诉 Unity 在当前相机中开启深度渲染。这可以通过简单地设置主相机的depthTextureMode来完成:
void Update ()
{
    Camera.main.depthTextureMode = DepthTextureMode.Depth;
    depthPower = Mathf.Clamp(depthPower, 0, 1);
}
在设置好所有代码后,保存你的脚本和着色器,并返回 Unity 让它们都编译。之后,选择主相机,在 TextRenderImage 组件上右键单击,并选择移除组件。之后,将这个新组件附加到对象上,并将我们新的着色器拖放到里面。如果没有遇到错误,你应该会看到一个类似于以下截图的结果:

如果我们进一步调整这些值,我们可以得到以下示例:

使用亮度、饱和度和对比度与屏幕效果结合
现在我们已经将屏幕效果系统搭建起来并运行,我们可以探索如何创建更复杂的像素操作来执行游戏中今天常见的某些屏幕效果。
使用屏幕效果来调整游戏的整体最终颜色对于让艺术家对游戏最终外观拥有全局控制至关重要。例如,颜色调整滑块等技术允许用户调整最终渲染游戏中的红色、蓝色和绿色的强度。这一概念也用于像棕褐色调效果这样的技术,在整个屏幕上覆盖某种色调的颜色。
对于这个特定的配方,我们将介绍一些我们可以在图像上执行的核心颜色调整操作。这些是亮度、饱和度和对比度。学习如何编写这些颜色调整代码为我们提供了一个很好的基础,我们可以从中学习屏幕效果的艺术。
准备工作
我们需要创建一些新的资产。我们可以利用与我们的测试场景相同的场景,但我们需要一个新的脚本和着色器:
- 
通过访问文件 | 新场景来创建一个新的场景。 
- 
在场景中添加几个新对象,设置一些不同颜色的漫反射材质,并将它们随机分配给场景中的新对象。这将为我们提供良好的颜色范围来测试我们的新屏幕效果: 

如何做到这一点...
现在我们已经完成了场景设置并创建了新的脚本和着色器,我们可以开始填写实现亮度、饱和度和对比度屏幕效果的必要代码。我们将专注于脚本和着色器的像素操作和变量设置,因为在本章的设置屏幕效果脚本系统食谱中描述了如何设置屏幕效果系统:
- 
通过从项目标签下的 第九章|着色器文件夹中选择ScreenGreyscale代码并按Ctrl + D来复制一个新的着色器。一旦复制,将脚本重命名为ScreenBSC。然后双击此着色器以在脚本编辑器中打开它。
- 
首先编辑着色器更有意义,这样我们就会知道我们的 C#脚本需要哪些变量。让我们先输入亮度、饱和度和对比度效果的适当属性。记住,我们需要在着色器中保留 _MainTex属性,因为这是RenderTexture在创建屏幕效果时指向的属性:
Properties 
{
  _MainTex ("Base (RGB)", 2D) = "white" {}
  _Brightness("Brightness", Range(0.0, 1)) = 1.0
  _Saturation("Saturation", Range(0.0, 1)) = 1.0
  _Contrast("Contrast", Range(0.0, 1)) = 1.0
}
- 如同往常一样,为了在我们CGPROGRAM中访问来自属性的数据,我们需要在CGPROGRAM中创建相应的变量,替换之前的变量:
Pass
{
  CGPROGRAM
  #pragma vertex vert_img
  #pragma fragment frag
  #pragma fragmentoption ARB_precision_hint_fastest
  #include "UnityCG.cginc"
 uniform sampler2D _MainTex;
 fixed _Brightness;
 fixed _Saturation;
 fixed _Contrast;
- 现在,我们需要创建执行亮度、饱和度和对比度效果的运算。在我们的着色器中,在frag()函数之上输入以下新函数:
float3 ContrastSaturationBrightness(float3 color, float brt, float sat, float con)
{
  // Increase or decrease these values to 
  //adjust r, g and b color channels separately
  float AvgLumR = 0.5;
  float AvgLumG = 0.5;
  float AvgLumB = 0.5;
//Luminance coefficients for getting lumoinance from the image
  float3 LuminanceCoeff = float3(0.2125, 0.7154, 0.0721);
  //Operation for brightness
  float3 AvgLumin = float3(AvgLumR, AvgLumG, AvgLumB);
  float3 brtColor = color * brt;
  float intensityf = dot(brtColor, LuminanceCoeff);
  float3 intensity = float3(intensityf, intensityf, intensityf);
  //Operation for Saturation
  float3 satColor = lerp(intensity, brtColor, sat);
  //Operation for Contrast
  float3 conColor = lerp(AvgLumin, satColor, con);
  return conColor;
}
如果现在还不明白也没有关系;所有代码将在本食谱的工作原理部分进行解释。
- 最后,我们只需更新我们的frag()函数以实际使用ContrastSaturationBrightness()函数。这将处理我们的RenderTexture的所有像素,并将其传递回我们的脚本:
fixed4 frag(v2f_img i) : COLOR
{
  //Get the colors from the RenderTexture and the uv's
  //from the v2f_img struct
  fixed4 renderTex = tex2D(_MainTex, i.uv);
  //Apply the Brughtness, saturation, contrast operations
  renderTex.rgb = ContrastSaturationBrightness(renderTex.rgb, 
                        _Brightness, 
                        _Saturation, 
                        _Contrast);
  return renderTex;
}
在着色器中输入代码后,返回 Unity 编辑器以让新着色器编译。如果没有错误,我们可以返回代码编辑器来工作。让我们开始创建一些新的代码行,将适当的数据发送到我们的着色器:
- 
现在着色器已经完成,让我们开始编写使效果显示所需的脚本。从项目标签,转到 第九章|脚本文件夹。一旦到达那里,选择TestRenderImage脚本并按Ctrl + D进行复制。将新创建的脚本重命名为RenderBSC。一旦重命名,双击它以进入您选择的 IDE。
- 
要修改我们的脚本,我们需要将类名重命名为与我们的文件名匹配,即 RenderBSC:
[ExecuteInEditMode]
public class RenderBSC : MonoBehaviour {
- 之后,我们需要添加驱动屏幕效果值的正确变量。在这种情况下,我们需要一个亮度滑块、一个饱和度滑块和一个对比度滑块:
#region Variables
public Shader curShader;
public float brightness = 1.0f;
public float saturation = 1.0f;
public float contrast = 1.0f;
private Material screenMat;
#endregion
- 在设置好我们的变量后,我们现在需要告诉脚本将我们创建的变量的值发送到着色器。我们在OnRenderImage()函数中这样做:
void OnRenderImage(RenderTexture sourceTexture, RenderTexture destTexture)
{
    if (curShader != null)
    {
        ScreenMat.SetFloat("_Brightness", brightness);
        ScreenMat.SetFloat("_Saturation", saturation);
        ScreenMat.SetFloat("_Contrast", contrast);
        Graphics.Blit(sourceTexture, destTexture, ScreenMat);
    }
    else
    {
        Graphics.Blit(sourceTexture, destTexture);
    }
}
- 最后,我们只需要将变量的值限制在合理的范围内。这些钳位值完全是主观的,所以你可以使用你认为合适的任何值:
void Update()
{
    brightness = Mathf.Clamp(brightness, 0.0f, 2.0f);
    saturation = Mathf.Clamp(saturation, 0.0f, 2.0f);
    contrast = Mathf.Clamp(contrast, 0.0f, 3.0f);
}
在脚本完成和着色器完成后,我们只需将我们的脚本分配给我们的主相机,将着色器分配给脚本,你应该会看到通过操作属性值实现的亮度、饱和度和对比度效果:

以下截图显示了使用此屏幕效果可以实现的结果:

以下截图显示了通过调整渲染图像的颜色可以实现的另一个示例:

它是如何工作的...
既然我们已经了解了基本屏幕效果系统的工作原理,那么我们就来了解一下在ContrastSaturationBrightness()函数中创建的逐像素操作。
函数首先接受几个参数。第一个也是最重要的参数是当前的RenderTexture。其他参数只是简单地调整屏幕效果的整体效果,并在屏幕效果的“检查器”标签页中以滑块的形式表示。一旦函数接收到RenderTexture和调整值,它就会声明一些常量值,我们使用这些值来修改并与原始的RenderTexture进行比较。
luminanceCoeff变量存储了将给我们提供当前图像整体亮度的值。这些系数基于 CIE 颜色匹配函数,并且在整个行业中相当标准化。我们可以通过获取当前图像与这些亮度系数的点积来找到图像的整体亮度。一旦我们有了亮度,我们只需使用几个lerp函数来混合亮度操作的灰度版本和原始图像,乘以传递给函数的亮度值。
这样的屏幕效果对于实现你游戏中高质量的图形至关重要,因为它们让你可以调整游戏最终的外观,而无需编辑当前游戏场景中的每个材质。
使用基本的 Photoshop-like 混合模式与屏幕效果
屏幕效果不仅限于调整我们游戏中渲染图像的颜色。我们还可以使用它们将其他图像与我们的RenderTexture结合。这种技术与在 Photoshop 中创建一个新图层并选择混合模式来混合两个图像或,在我们的情况下,一个纹理与RenderTexture没有区别。这成为了一种非常强大的技术,因为它为制作环境中的艺术家提供了一种在游戏中模拟他们的混合模式的方法,而不仅仅是 Photoshop 中。
对于这个特定的菜谱,我们将查看一些更常见的混合模式,例如乘法、加法和叠加。您将看到在游戏中拥有 Photoshop 混合模式的力量是多么简单。
准备工作
首先,我们必须准备好我们的资产。所以让我们遵循接下来的几个步骤,为我们的新混合模式屏幕效果启动屏幕效果系统:
- 
我们需要另一个纹理来执行我们的混合模式效果。在这个菜谱中,我们将使用一种磨损类型的纹理。这将使我们在测试时效果非常明显。 
- 
以下截图是制作此效果时使用的磨损纹理。找到一个具有足够细节和良好灰度值范围的纹理将使我们的新效果测试变得很棒: 

前面的纹理可以在本书的示例代码中找到,位于第九章|纹理文件夹中。
如何操作...
我们将实现的第一种混合模式是 Photoshop 中看到的乘法混合模式。让我们首先修改我们的着色器中的代码:
- 
通过在 第九章|着色器文件夹下从项目标签中选择ScreenGreyscale代码并按Ctrl + D来复制它,创建一个新的着色器。一旦复制,将脚本重命名为ScreenBlendMode。然后,双击此着色器以在脚本编辑器中打开它。
- 
我们需要添加一些新属性,以便我们有一个可以混合的纹理和一个滑块来调整我们想要使用的混合模式的最终数量。在你的新着色器中输入以下代码: 
Properties 
{
  _MainTex ("Base (RGB)", 2D) = "white" {}
  _BlendTex ("Blend Texture", 2D) = "white"{}
  _Opacity ("Blend Opacity", Range(0,1)) = 1
}
- 在我们的CGPROGRAM中输入相应的变量,以便我们可以从我们的Properties块访问数据,替换之前创建的变量:
Pass
{
  CGPROGRAM
  #pragma vertex vert_img
  #pragma fragment frag
  #pragma fragmentoption ARB_precision_hint_fastest
  #include "UnityCG.cginc"
 uniform sampler2D _MainTex;
 uniform sampler2D _BlendTex;
 fixed _Opacity;
- 我们修改我们的frag()函数,以便它对两个纹理执行乘法操作:
fixed4 frag(v2f_img i) : COLOR
{
  //Get the colors from the RenderTexture and the uv's
  //from the v2f_img struct
  fixed4 renderTex = tex2D(_MainTex, i.uv);
  fixed4 blendTex = tex2D(_BlendTex, i.uv);
  //Perform a multiply Blend mode
  fixed4 blendedMultiply = renderTex * blendTex;
  //Adjust amount of Blend Mode with a lerp
  renderTex = lerp(renderTex, blendedMultiply, _Opacity);
  return renderTex;
}
- 
保存着色器并返回 Unity 编辑器,让新的着色器代码编译并检查错误。如果没有发生错误,那么我们可以继续创建我们的脚本文件。 
- 
着色器完成后,让我们开始编写使效果显示所需的脚本。从项目标签中,转到 第九章|脚本文件夹。一旦到达那里,选择TestRenderImage脚本并按Ctrl + D复制它。将新创建的脚本重命名为RenderBlendMode。一旦重命名,双击它以进入您选择的 IDE。
- 
修改我们的脚本的第一步是重命名类以匹配我们的文件名, RenderBlendMode:
[ExecuteInEditMode]
public class RenderBlendMode : MonoBehaviour {
- 在我们的脚本文件中,我们需要创建相应的变量。我们需要一个纹理,以便我们可以将其分配给着色器,以及一个滑块来调整我们想要使用的混合模式的最终数量:
#region Variables
public Shader curShader;
public Texture2D blendTexture;
public float blendOpacity = 1.0f;
private Material screenMat;
#endregion
- 然后,我们需要通过OnRenderImage()函数将我们的变量数据发送到着色器中:
void OnRenderImage(RenderTexture sourceTexture, RenderTexture destTexture)
{
    if (curShader != null)
    {
        ScreenMat.SetTexture("_BlendTex", blendTexture);
        ScreenMat.SetFloat("_Opacity", blendOpacity);
        Graphics.Blit(sourceTexture, destTexture, ScreenMat);
    }
    else
    {
        Graphics.Blit(sourceTexture, destTexture);
    }
}
- 要完成脚本,我们只需填写我们的Update()函数,以便我们可以将blendOpacity变量的值限制在0.0和1.0之间:
void Update()
{
    blendOpacity = Mathf.Clamp(blendOpacity, 0.0f, 1.0f);
}
- 完成后,我们将屏幕效果脚本分配给我们的主相机(如果之前附加了Render BSC脚本,请将其移除),并将我们的屏幕效果着色器添加到脚本中,以便它有一个用于每个像素操作的着色器。为了使效果完全功能,脚本和着色器会查找一个纹理。您可以在屏幕效果脚本的检查器中将任何纹理分配给纹理字段。一旦这个纹理到位,您将看到将这个纹理乘以游戏渲染截图的效果:

- 以下截图展示了具有较小Blend Opacity选项的屏幕效果:

在我们设置好第一个混合模式后,我们可以开始添加几个更简单的混合模式,以更好地理解添加更多效果以及如何轻松地在游戏中微调最终结果。然而,首先让我们分析一下这里发生了什么。
它是如何工作的...
现在我们开始在屏幕效果编程中获得大量的功能和灵活性。我相信你现在开始理解你在 Unity 中可以用这个简单的系统做多少事情。我们实际上可以复制 Photoshop 图层混合模式的效果到我们的游戏中,为艺术家提供他们需要的灵活性,以便在短时间内实现高质量的图形。
在这个特定的食谱中,我们看看如何将两个图像相乘,将两个图像相加,并执行屏幕混合模式,只需一点数学知识。当使用混合模式时,你必须从每个像素的角度去思考。例如,当我们使用乘法混合模式时,我们实际上是从原始RenderTexture中取出每个像素,并将其与混合纹理的每个像素相乘。对于加法混合模式也是如此。它只是将源纹理或RenderTexture中的每个像素简单地加到混合纹理上。
屏幕混合模式确实要复杂一些,但它实际上在做的是同样的事情。它对每个图像、RenderTexture和混合纹理进行反转,然后将它们相乘,再次反转以实现最终的外观。就像 Photoshop 使用混合模式混合纹理一样,我们也可以用屏幕效果做到同样的事情。
更多...
让我们通过添加几个更多的混合模式到我们的屏幕效果中继续这个食谱:
- 在屏幕效果着色器中,让我们在我们的frag()函数中添加以下代码,并将我们返回给脚本的值更改。我们还需要注释掉乘法混合模式,这样就不会返回它:
fixed4 frag(v2f_img i) : COLOR
{
  //Get the colors from the RenderTexture and the uv's
  //from the v2f_img struct
  fixed4 renderTex = tex2D(_MainTex, i.uv);
  fixed4 blendTex = tex2D(_BlendTex, i.uv);
  //Perform a multiply Blend mode
 //fixed4 blendedMultiply = renderTex * blendTex;
 //Perform an additive Blend mode
 fixed4 blendedAdd = renderTex + blendTex;
  //Adjust amount of Blend Mode with a lerp
  renderTex = lerp(renderTex, blendedAdd, _Opacity);
  return renderTex;
}
- 将着色器文件保存在你选择的 IDE 中(例如MonoDevelop)并返回 Unity 编辑器,让着色器进行编译。如果没有错误发生,你应该会看到以下截图类似的结果:

- 这是一个简单的加法混合模式,Blend Opacity设置为0.5:

如你所见,这与乘法的效果相反,因为我们是在将两个图像相加。
- 最后,让我们添加一个名为屏幕混合的更多混合模式。这个混合模式在数学上稍微复杂一些,但仍然易于实现。在着色器的 frag()函数中输入以下代码:
    fixed4 frag(v2f_img i) : COLOR
    {
      //Get the colors from the RenderTexture and the uv's
      //from the v2f_img struct
      fixed4 renderTex = tex2D(_MainTex, i.uv);
      fixed4 blendTex = tex2D(_BlendTex, i.uv);
      //Perform a multiply Blend mode
      //fixed4 blendedMultiply = renderTex * blendTex;
      //Perform an additive Blend mode
 //fixed4 blendedAdd = renderTex + blendTex;
 //Perform screen blending mode
 fixed4 blendedScreen = (1.0 - ((1.0 - renderTex) * (1.0 - blendTex)));
      //Adjust amount of Blend Mode with a lerp
      renderTex = lerp(renderTex, blendedScreen, _Opacity);
      return renderTex;
    }
以下截图展示了使用屏幕类型混合模式将两个图像混合在一起在屏幕效果中的结果:

这是一个显示效果的截图:

使用屏幕效果与叠加混合模式
对于我们的最终配方,我们将查看另一种类型的混合模式,即叠加混合模式。这种混合模式实际上使用了一些条件语句来确定每个通道中每个像素的最终颜色。因此,使用这种类型的混合模式需要更多的编码才能工作。让我们看看在接下来的几个配方中是如何实现的。
如何做到...
为了开始我们的叠加屏幕效果,我们需要确保我们的着色器代码没有错误并运行起来。然后我们可以修改我们的脚本文件,以便向着色器提供正确的数据:
- 
通过复制 ScreenGreyscale代码并从Chapter 9 | Shaders文件夹下的项目标签页中选择它,然后按 Ctrl + D 来创建一个新的着色器。一旦复制完成,将脚本重命名为ScreenOverlay。然后,双击此着色器以在脚本编辑器中打开它。
- 
我们首先需要在 Properties块中设置属性。我们将使用本章前面几个配方中相同的属性:
Properties 
{
  _MainTex ("Base (RGB)", 2D) = "white" {}
  _BlendTex ("Blend Texture", 2D) = "white"{}
  _Opacity ("Blend Opacity", Range(0,1)) = 1
}
- 然后,我们需要在 CGPROGRAM中创建相应的变量,删除之前创建的变量:
Pass
{
  CGPROGRAM
  #pragma vertex vert_img
  #pragma fragment frag
  #pragma fragmentoption ARB_precision_hint_fastest
  #include "UnityCG.cginc"
  uniform sampler2D _MainTex;
  uniform sampler2D _BlendTex;
  fixed _Opacity;
- 为了让叠加混合效果工作,我们必须对每个通道的每个像素进行单独处理。要在着色器中这样做,我们必须编写一个自定义函数,该函数将接受单个通道,例如红色通道,并执行叠加操作。在着色器中变量声明下方输入以下代码:
fixed OverlayBlendMode(fixed basePixel, fixed blendPixel)
{
  if(basePixel < 0.5)
  {
    return (2.0 * basePixel * blendPixel);
  }
  else
  {
    return (1.0 - 2.0 * (1.0 - basePixel) * (1.0 - blendPixel));
  }
}
- 我们需要更新我们的 frag()函数,以便处理我们的纹理的每个通道,以执行混合:
fixed4 frag(v2f_img i) : COLOR
{
  //Get the colors from the RenderTexture and the uv's
  //from the v2f_img struct
  fixed4 renderTex = tex2D(_MainTex, i.uv);
  fixed4 blendTex = tex2D(_BlendTex, i.uv);
  fixed4 blendedImage = renderTex;
  blendedImage.r = OverlayBlendMode(renderTex.r, blendTex.r);
  blendedImage.g = OverlayBlendMode(renderTex.g, blendTex.g);
  blendedImage.b = OverlayBlendMode(renderTex.b, blendTex.b);
  //Adjust amount of Blend Mode with a lerp
  renderTex = lerp(renderTex, blendedImage, _Opacity);
  return renderTex;
}
- 在着色器中完成代码后,我们的效果应该已经开始工作了。保存着色器并返回到 Unity 编辑器,让着色器进行编译。我们的脚本已经设置好了;选择主摄像机对象。从项目标签页,将 ScreenOverlay 着色器拖放到检查器标签页中的渲染混合模式组件的 Cur Shader属性上。一旦着色器编译完成,你应该会看到一个类似于以下截图的结果:

这是一个使用 0.5 混合不透明度的截图:

它是如何工作的...
我们的叠加混合模式确实更为复杂,但如果你真正分解其功能,你会发现它实际上只是一个乘法混合模式和屏幕混合模式。在这种情况下,我们进行条件检查,以将一个或另一个混合模式应用于像素。
在这个特定的屏幕效果中,当叠加功能接收到一个像素时,它会检查该像素是否小于0.5。如果是,则对该像素应用修改后的乘法混合模式;如果不是,则对该像素应用修改后的屏幕混合模式。我们对每个通道的每个像素都这样做,从而得到屏幕效果的最终 RGB 像素值。
如你所见,可以使用屏幕效果做很多事情。这实际上完全取决于平台和分配给屏幕效果的内存量。通常,这会在游戏项目的整个过程中确定,所以尽情享受并发挥创意,制作出有趣的屏幕效果。
第十章:游戏玩法和屏幕效果
当涉及到创建逼真和沉浸式的游戏时,材质设计并不是我们需要考虑的唯一方面。整体感觉可以通过屏幕效果来改变。这在电影中非常常见,例如,在后期制作阶段校正颜色。你可以在游戏中使用从第九章,“使用 Unity 渲染纹理的屏幕效果”中获得的知识来实现这些技术。本章中介绍了两个有趣的效果;然而,你可以根据需要调整它们,创建你自己的屏幕效果。
在本章中,你将学习以下内容:
- 
创建老电影屏幕效果 
- 
创建夜视屏幕效果 
简介
如果你正在阅读这本书,你很可能是这样一个在某个时候玩过一两个游戏的人。实时游戏的一个方面是让玩家沉浸在一个世界中,让他们感觉就像他们真的在现实世界中玩游戏一样。更现代的游戏大量使用屏幕效果来实现这种沉浸感。
通过屏幕效果,我们只需改变屏幕的外观,就能将某个环境的情绪从平静转变为恐怖。想象一下走进一个位于关卡内的房间,然后游戏接管并进入一个电影时刻。许多现代游戏会开启不同的屏幕效果来改变当前时刻的氛围。了解如何创建由游戏玩法触发的效果是我们关于着色器编写的旅程中的下一个环节。
在本章中,我们将探讨一些更常见的游戏玩法屏幕效果。你将学习如何将游戏的外观从正常转变为老电影效果,并且我们将探讨许多第一人称射击游戏如何将夜视效果应用到屏幕上。对于这些配方中的每一个,我们将探讨如何将它们连接到游戏事件,以便它们根据游戏当前的表现需求开启和关闭。
创建老电影屏幕效果
许多游戏设定在不同的时代。有些发生在幻想世界或未来科幻世界中,有些甚至发生在老西部,当时电影摄影机刚刚被开发出来,人们观看的电影是黑白或有时被一种称为棕褐色效果的颜色着色。这种外观非常独特,我们将使用 Unity 中的屏幕效果来复制这种外观。
要实现这种外观有几个步骤;仅仅为了让整个屏幕变成黑白或灰度,我们需要将这个效果分解为其组成部分。如果我们分析一些老电影的参考片段,我们就可以开始这样做。让我们看一下以下图像,并分解构成老电影外观的元素:

我们使用网上找到的一些参考图像构建了这张图片。尝试使用 Photoshop 构建这样的图片总是一个好主意,这可以帮助你为新屏幕效果制定计划。执行此过程不仅告诉我们将需要编码的元素,而且还给我们提供了一个快速查看哪些混合模式有效以及我们将如何构建屏幕效果层的方法。
准备工作
现在我们知道了我们要制作什么,让我们看看每一层是如何组合在一起以创建最终效果,并为我们的着色器和屏幕效果脚本收集一些资源:
- 棕褐色调:这是一个相对简单的效果,因为我们只需要将原始渲染纹理的所有像素颜色调整到单个颜色范围内。这可以通过使用原始图像的亮度并添加一个常数颜色轻松实现。我们的第一层将看起来像以下截图:

- 晕影效果:当使用老旧电影放映机放映老电影时,我们总能看到某种类型的软边框。这是由于用于电影放映机的灯泡在电影边缘的亮度比中间的亮度要低。这种效果通常被称为晕影效果,并且是屏幕效果的第二层。我们可以通过在整个屏幕上叠加纹理来实现这一点。以下截图演示了这一层看起来像什么,隔离为纹理:

- 灰尘和划痕:在我们老旧电影屏幕效果中的第三和最后一层是灰尘和划痕。这一层将利用两种不同的平铺纹理,一种用于划痕,一种用于灰尘。原因是我们将想要在时间上以不同的平铺速率动画化这两种纹理。这将产生一种效果,即电影在移动,并且每一帧的老电影上都有小划痕和灰尘。以下截图演示了将此效果隔离到其自身纹理中的样子:

让我们使用前面的纹理准备好我们的屏幕效果系统。执行以下步骤:
- 
收集晕影纹理和灰尘划痕纹理,就像我们刚才看到的那些。 
- 
我们还需要一个场景,我们想要模拟我们试图构建的效果。我创建了一个示例场景,你可以将其用于示例代码中的 第十章文件夹,名为10.1 Starter Scene:

- 
通过复制 ScreenGrayscale代码创建一个新的着色器;从第九章|着色器文件夹下的项目选项卡中选择它,然后按Ctrl + D。一旦复制,将脚本重命名为ScreenOldFilm。然后,将脚本拖放到第十章|着色器文件夹中,如果需要则创建它。
- 
接下来,转到 第九章|脚本文件夹,复制TestRenderImage脚本。将新文件重命名为RenderOldFilm,然后将其拖放到第十章|脚本文件夹中,如果需要则创建它。
最后,当我们的屏幕效果系统运行良好并且我们已经收集了纹理后,我们可以开始重新创建这个老式电影效果的过程。
如何做到这一点...
我们的老式电影屏幕效果的各个单独层相当简单,但结合在一起,我们会得到一些非常视觉上令人惊叹的效果。让我们来了解一下如何构建我们的脚本和着色器的代码,然后我们可以逐行分析代码,了解为什么事情会以这种方式工作。到目前为止,你应该已经启动并运行了屏幕效果系统,因为我们不会在这个配方中介绍如何设置它。
- 我们将从在脚本中输入代码开始。修改我们的脚本的第一步是将类名重命名为与我们的文件名匹配,RenderOldFilm:
[ExecuteInEditMode]
public class RenderOldFilm : MonoBehaviour {
- 我们将要输入的第一个代码块将定义我们想要暴露给 Inspector 的变量,以便用户可以根据需要调整此效果。我们还可以在决定需要暴露给此效果 Inspector 的内容时,将我们的模拟 Photoshop 文件作为参考。在你的效果脚本中输入以下代码:
#region Variables 
public Shader curShader; // old film shader
public float OldFilmEffectAmount = 1.0f;
public Color sepiaColor = Color.white;
public Texture2D vignetteTexture;
public float vignetteAmount = 1.0f;
public Texture2D scratchesTexture;
public float scratchesYSpeed = 10.0f;
public float scratchesXSpeed = 10.0f;
public Texture2D dustTexture;
public float dustYSpeed = 10.0f;
public float dustXSpeed = 10.0f;
private Material screenMat;
private float randomValue;
#endregion
- 接下来,我们需要填充我们的 OnRenderImage()函数的内容。在这里,我们将从我们的变量传递数据到着色器,以便着色器可以使用这些数据来处理渲染纹理:
void OnRenderImage(RenderTexture sourceTexture, RenderTexture destTexture)
{
    if (curShader != null)
    {
        ScreenMat.SetColor("_SepiaColor", sepiaColor);
        ScreenMat.SetFloat("_VignetteAmount", vignetteAmount);
        ScreenMat.SetFloat("_EffectAmount", OldFilmEffectAmount);
        if (vignetteTexture)
        {
            ScreenMat.SetTexture("_VignetteTex", vignetteTexture);
        }
        if (scratchesTexture)
        {
            ScreenMat.SetTexture("_ScratchesTex", scratchesTexture);
            ScreenMat.SetFloat("_ScratchesYSpeed", scratchesYSpeed);
            ScreenMat.SetFloat("_ScratchesXSpeed", scratchesXSpeed);
        }
        if (dustTexture)
        {
            ScreenMat.SetTexture("_DustTex", dustTexture);
            ScreenMat.SetFloat("_dustYSpeed", dustYSpeed);
            ScreenMat.SetFloat("_dustXSpeed", dustXSpeed);
            ScreenMat.SetFloat("_RandomValue", randomValue);
        }
        Graphics.Blit(sourceTexture, destTexture, ScreenMat);
    }
    else
    {
        Graphics.Blit(sourceTexture, destTexture);
    }
}
- 为了完成这个效果的脚本部分,我们只需确保我们将需要限制范围的变量值进行限制,而不是任何值:
void Update()
{
    vignetteAmount = Mathf.Clamp01(vignetteAmount);
    OldFilmEffectAmount = Mathf.Clamp(OldFilmEffectAmount, 0f, 1.5f);
    randomValue = Random.Range(-1f, 1f);
}
- 我们的脚本完成后,让我们将注意力转向我们的着色器文件。我们需要创建在脚本中创建的相应变量,这样脚本和着色器就可以相互通信。在着色器的 Properties块中输入以下代码:
Properties 
{ 
    _MainTex ("Base (RGB)", 2D) = "white" {} 
    _VignetteTex ("Vignette Texture", 2D) = "white"{} 
    _ScratchesTex ("Scratches Texture", 2D) = "white"{} 
    _DustTex ("Dust Texture", 2D) = "white"{} 
    _SepiaColor ("Sepia Color", Color) = (1,1,1,1) 
    _EffectAmount ("Old Film Effect Amount", Range(0,1)) = 1.0 
    _VignetteAmount ("Vignette Opacity", Range(0,1)) = 1.0 
    _ScratchesYSpeed ("Scratches Y Speed", Float) = 10.0 
    _ScratchesXSpeed ("Scratches X Speed", Float) = 10.0 
    _dustXSpeed ("Dust X Speed", Float) = 10.0 
    _dustYSpeed ("Dust Y Speed", Float) = 10.0 
    _RandomValue ("Random Value", Float) = 1.0 
    _Contrast ("Contrast", Float) = 3.0 
} 
- 然后,像往常一样,我们需要将这些相同的变量名添加到我们的 CGPROGRAM块中,以便Properties块可以与CGPROGRAM块通信:
Pass
{
  CGPROGRAM 
  #pragma vertex vert_img 
  #pragma fragment frag 
  #pragma fragmentoption ARB_precision_hint_fastest 
  #include "UnityCG.cginc" 
  uniform sampler2D _MainTex; 
  uniform sampler2D _VignetteTex; 
  uniform sampler2D _ScratchesTex; 
  uniform sampler2D _DustTex; 
  fixed4 _SepiaColor; 
  fixed _VignetteAmount; 
  fixed _ScratchesYSpeed; 
  fixed _ScratchesXSpeed; 
  fixed _dustXSpeed; 
  fixed _dustYSpeed; 
  fixed _EffectAmount; 
  fixed _RandomValue; 
  fixed _Contrast; 
- 现在,我们只需填充我们的 frag()函数的内部逻辑,以便我们可以处理屏幕效果的像素。首先,让我们从脚本中获取传递给我们的渲染纹理和晕影纹理:
fixed4 frag(v2f_img i) : COLOR 
{ 
    //Get the colors from the RenderTexture and the uv's 
    //from the v2f_img struct 
    fixed4 renderTex = tex2D(_MainTex, i.uv); 
    //Get the pixels from the Vignette Texture 
    fixed4 vignetteTex = tex2D(_VignetteTex, i.uv); 
- 然后,我们需要通过输入以下代码来添加灰尘和划痕的处理过程:
//Process the Scratches UV and pixels 
half2 scratchesUV = half2(i.uv.x + (_RandomValue * _SinTime.z * _ScratchesXSpeed), i.uv.y + (_Time.x * _ScratchesYSpeed)); 
fixed4 scratchesTex = tex2D(_ScratchesTex, scratchesUV); 
//Process the Dust UV and pixels 
half2 dustUV = half2(i.uv.x + (_RandomValue * (_SinTime.z * _dustXSpeed)), i.uv.y + (_RandomValue * (_SinTime.z * _dustYSpeed))); 
fixed4 dustTex = tex2D(_DustTex, dustUV); 
- 接下来在我们的列表中是棕褐色调过程:
// get the luminosity values from the render texture using the YIQ values. 
fixed lum = dot (fixed3(0.299, 0.587, 0.114), renderTex.rgb); 
//Add the constant color to the lum values 
fixed4 finalColor = lum + lerp(_SepiaColor, _SepiaColor + 
 fixed4(0.1f,0.1f,0.1f,1.0f), _RandomValue); 
finalColor = pow(finalColor, _Contrast); 
- 最后,我们将所有层和颜色组合在一起,并返回最终的屏幕效果纹理:
  //Create a constant white color we can use to adjust opacity of effects 
  fixed3 constantWhite = fixed3(1,1,1); 
  //Composite together the different layers to create finsl Screen Effect 
  finalColor = lerp(finalColor, finalColor * vignetteTex, _VignetteAmount); 
  finalColor.rgb *= lerp(scratchesTex, constantWhite, (_RandomValue)); 
  finalColor.rgb *= lerp(dustTex.rgb, constantWhite, (_RandomValue * _SinTime.z)); 
  finalColor = lerp(renderTex, finalColor, _EffectAmount); 
  return finalColor; 
}
- 在输入所有代码且没有错误后,返回 Unity 编辑器,并将 RenderOldFilm组件添加到示例场景中的MainCamera上。从那里,将我们的着色器拖放到 Cur Shader 属性中。之后,在棕褐色调下,分配一个类似以下的棕色:

- 然后,将提供的每个纹理分配给相应的属性。你应该会在屏幕上注意到以下类似的内容:

- 此外,确保在 Unity 编辑器中点击播放,以查看灰尘和划痕效果的全貌以及我们给予屏幕效果的轻微图像偏移。
它是如何工作的...
现在,让我们逐一分析这个屏幕效果中的每一层,解释每一行代码为什么能按预期工作,并深入了解我们如何为这个屏幕效果添加更多内容。
现在我们老旧的电影屏幕效果已经生效,让我们逐步分析frag()函数中的代码行,因为在此书此阶段,其他代码应该相当直观易懂。
就像我们的 Photoshop 图层一样,我们的着色器正在处理每一层并将它们组合在一起,所以当我们逐层分析时,试着想象 Photoshop 中的图层是如何工作的。保持这个概念在心中,在开发新的屏幕效果时总是有帮助的。
在这里,我们有frag()函数中的第一组代码行:
fixed4 frag(v2f_img i) : COLOR 
{ 
    //Get the colors from the RenderTexture and the uv's 
    //from the v2f_img struct 
    fixed4 renderTex = tex2D(_MainTex, i.uv); 
    //Get the pixels from the Vignette Texture 
    fixed4 vignetteTex = tex2D(_VignetteTex, i.uv); 
代码的第一行,紧随frag()函数声明之后,定义了 UVs 应该如何为我们的主渲染纹理或游戏实际渲染的帧工作。由于我们想要模拟老旧电影风格的效果,我们希望在每一帧中调整渲染纹理的 UVs,使其闪烁。这种闪烁模拟了电影放映机卷轴略微偏移的情况。这告诉我们需要动画化 UVs,这正是第一行代码所做的事情。
我们使用了 Unity 提供的内置_SinTime变量,以获取介于-1和1之间的值。然后我们将其乘以一个非常小的数字,在这个例子中是0.005,以减少效果强度。最终的值然后再次乘以我们在效果脚本中生成的_RandomValue变量。这个值在-1和1之间来回弹跳,基本上是来回翻转运动的方向。
一旦我们的 UVs 构建并存储在renderTexUV变量中,我们就可以使用tex2D()函数来采样渲染纹理。这个操作然后给我们最终的渲染纹理,我们可以在着色器的其余部分进一步处理它。
接下来看上一张图片中的最后一行,我们只是简单地使用tex2D()函数对晕影纹理进行直接采样。我们不需要使用之前已经创建的动画 UVs,因为晕影纹理将与摄像机的运动本身相关联,而不是与摄像机胶片的闪烁相关联。
frag() function:
//Process the Scratches UV and pixels 
half2 scratchesUV = half2(i.uv.x + (_RandomValue * _SinTime.z * _ScratchesXSpeed), 
        i.uv.y + (_Time.x * _ScratchesYSpeed)); 
fixed4 scratchesTex = tex2D(_ScratchesTex, scratchesUV); 
//Process the Dust UV and pixels 
half2 dustUV = half2(i.uv.x + (_RandomValue * (_SinTime.z * _dustXSpeed)),  
        i.uv.y + (_RandomValue * (_SinTime.z * _dustYSpeed))); 
fixed4 dustTex = tex2D(_DustTex, dustUV); 
这些代码行几乎与之前的代码行完全相同,我们需要生成独特的动画 UV 值来修改屏幕效果层的位置。我们简单地使用内置的_SinTime值来获取介于-1和1之间的值,乘以我们的随机值,然后再乘以另一个乘数来调整动画的整体速度。一旦生成了这些 UV 值,我们就可以使用这些新的动画值来采样灰尘和划痕纹理。
我们下一组代码处理的是为我们的老电影屏幕效果创建着色效果。以下代码片段演示了这些行:
// get the luminosity values from the render texture using the YIQ values 
fixed lum = dot (fixed3(0.299, 0.587, 0.114), renderTex.rgb); 
//Add the constant color to the lum values 
fixed4 finalColor = lum + lerp(_SepiaColor, _SepiaColor +
     fixed4(0.1f,0.1f,0.1f,1.0f), _RandomValue);
使用这组代码,我们正在创建整个渲染纹理的实际颜色着色。为了实现这一点,我们首先需要将渲染纹理转换为它自己的灰度版本。为此,我们可以使用由 YIQ 值提供的亮度值。YIQ 值是 NTSC 彩色电视系统使用的颜色空间。YIQ 中的每个字母实际上存储了电视用来调整可读性的颜色常数。
虽然实际上知道这种颜色尺度的原因并不是必要的,但应该知道,YIQ 中的 Y 值是任何图像的恒定亮度值。因此,我们可以通过将渲染纹理的每个像素与我们的亮度值点积来生成渲染纹理的灰度图像。这就是这组代码中的第一行所做的事情。
一旦我们有了亮度值,我们就可以简单地添加我们想要给图像着色的颜色。这种颜色从我们的脚本传递到我们的着色器,然后到我们的 CGPROGRAM 块,在那里我们可以将其添加到我们的灰度渲染纹理中。一旦完成,我们就会得到一个完美着色的图像。
最后,我们在屏幕效果中创建每一层的混合。以下代码片段显示了我们所查看的代码集:
//Create a constant white color we can use to adjust opacity of effects 
fixed3 constantWhite = fixed3(1,1,1); 
//Composite together the different layers to create finsl Screen Effect 
finalColor = lerp(finalColor, finalColor * vignetteTex, _VignetteAmount); 
finalColor.rgb *= lerp(scratchesTex, constantWhite, (_RandomValue)); 
finalColor.rgb *= lerp(dustTex.rgb, constantWhite, (_RandomValue * _SinTime.z)); 
finalColor = lerp(renderTex, finalColor, _EffectAmount); 
return finalColor 
我们最后一组代码相对简单,实际上并不需要太多解释。简而言之,它只是将所有层相乘以得到最终结果。就像我们在 Photoshop 中将层相乘一样,我们在着色器中将它们相乘。每一层都通过一个 lerp() 函数进行处理,这样我们就可以调整每一层的透明度,从而对最终效果有更多的艺术控制。在屏幕效果方面,提供的调整越多,效果越好。
参见
关于 YIQ 值的更多信息,请参阅以下链接:
创建夜视屏幕效果
我们下一个屏幕效果绝对是一个更受欢迎的效果。夜视屏幕效果在 Call of Duty: Modern Warfare、Halo 以及市场上几乎任何第一人称射击游戏中都可以看到。这是使用非常独特的草绿色来提亮整个图像的效果。
为了实现我们的夜视效果,我们需要使用 Photoshop 将效果分解。这是一个简单的过程,即在网上找到一些参考图像,并组合一个分层图像来查看你需要什么样的混合模式,或者我们需要按什么顺序组合我们的层。以下截图显示了在 Photoshop 中执行此过程的成果:

让我们将粗糙的 Photoshop 合成图像分解成其组成部分,以便我们更好地理解我们需要收集的资产。在下一章中,我们将介绍这一过程。
准备工作
让我们再次将我们的效果分解为其组成部分,以此开始这个屏幕效果。使用 Photoshop,我们可以构建一个分层图像,以更好地说明我们如何捕捉夜视效果:
- 着色绿色:我们屏幕效果中的第一层是标志性的绿色,几乎在每一张夜视图像中都能找到。这将给我们的效果带来独特的夜视外观,如下面的截图所示:

- 
扫描线:为了增加这种新类型显示器对玩家的效果,我们在着色层上方添加了扫描线。为此,我们将使用在 Photoshop 中创建的纹理,并允许用户平铺它,以便扫描线可以更大或更小。 
- 
噪声:我们的下一层是一个简单的噪声纹理,我们将其平铺在着色图像和扫描线上,以打破图像并为我们添加更多细节。这一层仅仅强调了数字读数的外观: 

- 晕影:我们夜视效果中的最后一层是晕影。如果你查看《使命召唤:现代战争》中的夜视效果,你会注意到它使用了一个模仿通过瞄准镜向下看的晕影效果。我们将为这个屏幕效果做同样的事情:

通过收集我们的纹理来创建一个屏幕效果系统。执行以下步骤:
- 
收集晕影纹理、噪声纹理和扫描线纹理,就像我们刚才看到的。就像之前一样,我在书的示例代码中的 Chapter 10|Textures文件夹中提供了这些纹理。
- 
找一个示例场景以便更容易看到着色器的效果。我将使用与上一章相同的场景,所以请随意再次使用 10.1 Sample Scene。
- 
通过复制 ScreenGrayscale代码创建一个新的着色器;从Chapter 9|Shaders文件夹下的Project标签中选择它,然后按Ctrl + D。一旦复制,将脚本重命名为ScreenNightVision。然后将脚本拖放到Chapter 10|Shaders文件夹中,如果需要则创建它。
- 
接下来,前往 Chapter 9|Scripts文件夹,复制TestRenderImage脚本。将新文件重命名为RenderNightVision,然后将其拖放到Chapter 10|Scripts文件夹中,如果需要则创建它。
最后,当我们的屏幕效果系统运行起来并且我们已经收集了纹理后,我们可以开始重新创建这个夜视效果的过程。
如何操作...
在收集了所有资产并且屏幕效果系统运行顺畅后,让我们开始添加必要的代码,包括脚本和着色器。我们将从RenderNightVision.cs脚本开始编码,所以现在请双击此文件,在您选择的代码编辑器中打开它:
- 我们将首先在我们的脚本中输入代码。修改脚本的第一步是将类重命名为与文件名匹配,即RenderNightVision:
[ExecuteInEditMode]
public class RenderNightVision : MonoBehaviour {
- 我们需要创建一些变量,使用户能够通过脚本的检查器调整此效果。在NightVisionEffect.cs脚本中输入以下代码:
#region Variables 
    public Shader curShader; 
    public float contrast = 3.0f; 
    public float brightness = 0.1f; 
    public Color nightVisionColor = Color.green; 
    public Texture2D vignetteTexture; 
    public Texture2D scanLineTexture; 
    public float scanLineTileAmount = 4.0f; 
    public Texture2D nightVisionNoise; 
    public float noiseXSpeed = 100.0f; 
    public float noiseYSpeed = 100.0f; 
    public float distortion = 0.2f; 
    public float scale = 0.8f; 
    private float randomValue = 0.0f; 
    private Material screenMat; 
    #endregion 
- 接下来,我们需要完成我们的OnRenderImage()函数,以便正确地将数据传递给着色器,以便着色器能够正确处理屏幕效果。使用以下代码完成OnRenderImage()函数:
void OnRenderImage(RenderTexture sourceTexture, RenderTexture destTexture)
{
    if (curShader != null)
    {
        ScreenMat.SetFloat("_Contrast", contrast);
        ScreenMat.SetFloat("_Brightness", brightness);
        ScreenMat.SetColor("_NightVisionColor", nightVisionColor);
        ScreenMat.SetFloat("_RandomValue", randomValue);
        ScreenMat.SetFloat("_distortion", distortion);
        ScreenMat.SetFloat("_scale", scale);
        if (vignetteTexture)
        {
            ScreenMat.SetTexture("_VignetteTex", vignetteTexture);
        }
        if (scanLineTexture)
        {
            ScreenMat.SetTexture("_ScanLineTex", scanLineTexture);
            ScreenMat.SetFloat("_ScanLineTileAmount", scanLineTileAmount);
        }
        if (nightVisionNoise)
        {
            ScreenMat.SetTexture("_NoiseTex", nightVisionNoise);
            ScreenMat.SetFloat("_NoiseXSpeed", noiseXSpeed);
            ScreenMat.SetFloat("_NoiseYSpeed", noiseYSpeed);
        }
        Graphics.Blit(sourceTexture, destTexture, ScreenMat);
    }
    else
    {
        Graphics.Blit(sourceTexture, destTexture);
    }
}
- 要完成NightVisionEffect.cs脚本,我们只需确保某些变量被限制在一定的范围内,这样它们才能保持在这个范围内。这些范围是任意的,可以在以后的时间进行更改。这些只是工作得很好的值:
void Update()
{
    contrast = Mathf.Clamp(contrast, 0f, 4f);
    brightness = Mathf.Clamp(brightness, 0f, 2f);
    randomValue = Random.Range(-1f, 1f);
    distortion = Mathf.Clamp(distortion, -1f, 1f);
    scale = Mathf.Clamp(scale, 0f, 3f);
}
- 现在,我们可以将注意力转向这个屏幕效果的着色器部分。如果您还没有打开着色器,请打开它,并首先在Properties块中输入以下属性:
Properties 
{ 
    _MainTex ("Base (RGB)", 2D) = "white" {} 
    _VignetteTex ("Vignette Texture", 2D) = "white"{} 
    _ScanLineTex ("Scan Line Texture", 2D) = "white"{} 
    _NoiseTex ("Noise Texture", 2D) = "white"{} 
    _NoiseXSpeed ("Noise X Speed", Float) = 100.0 
    _NoiseYSpeed ("Noise Y Speed", Float) = 100.0 
    _ScanLineTileAmount ("Scan Line Tile Amount", Float) = 4.0 
    _NightVisionColor ("Night Vision Color", Color) = (1,1,1,1) 
    _Contrast ("Contrast", Range(0,4)) = 2 
    _Brightness ("Brightness", Range(0,2)) = 1 
    _RandomValue ("Random Value", Float) = 0 
    _distortion ("Distortion", Float) = 0.2 
    _scale ("Scale (Zoom)", Float) = 0.8 
} 
- 为了确保我们从Properties块传递数据到CGPROGRAM块,我们需要确保在CGPROGRAM块中使用相同的名称声明它们:
Pass
{
  CGPROGRAM 
  #pragma vertex vert_img 
  #pragma fragment frag 
  #pragma fragmentoption ARB_precision_hint_fastest 
  #include "UnityCG.cginc" 
  uniform sampler2D _MainTex; 
  uniform sampler2D _VignetteTex; 
  uniform sampler2D _ScanLineTex; 
  uniform sampler2D _NoiseTex; 
  fixed4 _NightVisionColor; 
  fixed _Contrast; 
  fixed _ScanLineTileAmount; 
  fixed _Brightness; 
  fixed _RandomValue; 
  fixed _NoiseXSpeed; 
  fixed _NoiseYSpeed; 
  fixed _distortion; 
  fixed _scale; 
- 我们的效果还将包括镜头畸变,以进一步传达我们正在通过镜头看,图像的边缘正被镜头的角度所扭曲。在CGPROGRAM块的变量声明之后输入以下函数:
    float2 barrelDistortion(float2 coord)  
    { 
        // lens distortion algorithm 
        // See http://www.ssontech.com/content/lensalg.htm 
        float2 h = coord.xy - float2(0.5, 0.5); 
        float r2 = h.x * h.x + h.y * h.y; 
        float f = 1.0 + r2 * (_distortion * sqrt(r2)); 
        return f * _scale * h + 0.5; 
    } 
- 现在,我们可以专注于我们的NightVisionEffect着色器的核心部分。让我们首先输入获取渲染纹理和晕影纹理所需的代码。在着色器的frag()函数中输入以下代码:
    fixed4 frag(v2f_img i) : COLOR 
    { 
        //Get the colors from the RenderTexture and the uv's 
        //from the v2f_img struct 
        half2 distortedUV = barrelDistortion(i.uv); 
        fixed4 renderTex = tex2D(_MainTex, distortedUV); 
        fixed4 vignetteTex = tex2D(_VignetteTex, i.uv); 
- 在我们的frag()函数中的下一步是处理扫描线和Noise纹理,并将适当的动画 UV 应用到它们上:
//Process scan lines and noise 
half2 scanLinesUV = half2(i.uv.x * _ScanLineTileAmount, i.uv.y * _ScanLineTileAmount); 
fixed4 scanLineTex = tex2D(_ScanLineTex, scanLinesUV); 
half2 noiseUV = half2(i.uv.x + (_RandomValue * _SinTime.z * _NoiseXSpeed), 
                  i.uv.y + (_Time.x * _NoiseYSpeed)); 
fixed4 noiseTex = tex2D(_NoiseTex, noiseUV); 
- 要完成屏幕效果中的所有层,我们只需处理渲染纹理的亮度值,然后将其应用于夜视颜色,以实现标志性的夜视外观:
// get the luminosity values from the render texture using the     //YIQ values. 
        fixed lum = dot (fixed3(0.299, 0.587, 0.114), renderTex.rgb); 
        lum += _Brightness; 
        fixed4 finalColor = (lum *2) + _NightVisionColor; 
- 最后,我们将所有层合并在一起,并返回我们夜视效果的最终颜色:
  //Final output 
  finalColor = pow(finalColor, _Contrast); 
  finalColor *= vignetteTex; 
  finalColor *= scanLineTex * noiseTex; 
  return finalColor; 
}
- 当你完成代码输入后,返回 Unity 编辑器,让脚本和着色器编译。如果没有错误,选择场景中的MainCamera。如果已经存在,移除Render Old Film组件,并添加RenderNightVision组件。一旦完成,将ScreenNightVision着色器拖放到组件的 Cur Shader 属性中,然后将夜视颜色属性分配为绿色,如下所示:

- 之后,将纹理分配到它们正确的位置:

- 之后,确保在编辑器中运行以查看效果的完整最终版本:

我们夜视屏幕效果的最终结果
它是如何工作的...
夜视效果实际上与老式电影屏幕效果非常相似,这展示了我们可以将这些组件做得多么模块化。只需简单地交换我们用于叠加的纹理,并改变我们的平铺率计算速度,我们就可以使用相同的代码实现非常不同的结果。
与此效果唯一的区别是我们将镜头畸变效果包含到了屏幕效果中。因此,让我们将其分解,以便更好地理解其工作原理。
SynthEyes, and the code is freely available to use in your own effects:
float2 barrelDistortion(float2 coord)  
{ 
    // lens distortion algorithm 
    // See http://www.ssontech.com/content/lensalg.htm 
    float2 h = coord.xy - float2(0.5, 0.5); 
    float r2 = h.x * h.x + h.y * h.y; 
    float f = 1.0 + r2 * (_distortion * sqrt(r2)); 
    return f * _scale * h + 0.5; 
} 
还有更多...
在视频游戏中,需要突出显示某些对象的情况并不少见。例如,热视镜应该只对人和其他热源应用后处理效果。根据本书迄今为止收集的知识,这样做已经可能;实际上,您可以通过代码更改对象的着色器或材质。然而,这通常很费时,并且必须在每个对象上重复。
使用替换着色器的更有效方法。每个着色器都有一个名为 RenderType 的标签,迄今为止从未使用过。此属性可以用来强制相机仅对某些对象应用着色器。您可以通过将以下脚本附加到相机上来实现这一点:
using UnityEngine; 
public class ReplacedShader : MonoBehaviour { 
    public Shader shader; 
    void Start () { 
        GetComponent<Camera>().SetReplacementShader(shader, "Heat"); 
    } 
} 
进入播放模式后,相机将查询所有需要渲染的对象。如果它们没有装饰有 RenderType = "Heat" 的着色器,则不会进行渲染。带有此类标签的对象将使用脚本附加的着色器进行渲染。
第十一章:高级着色技术
在本章中,您将学习以下内容:
- 
使用 Unity 内置的 CgInclude 文件 
- 
使用 CgInclude 使您的着色器世界模块化 
- 
实现毛皮着色器 
- 
使用数组实现热图 
简介
本章介绍了您可以在游戏中使用的某些高级着色器技术。您应该记住,您在游戏中看到的大部分最引人注目的效果都是通过测试着色器能做什么的极限来制作的。本书为您提供修改和创建着色器的技术基础,但强烈鼓励您尽可能多地玩耍和实验。制作一款好游戏并不是追求照片级真实感;您不应该带着复制现实的目的去处理着色器,因为这不太可能发生。相反,您应该尝试将着色器作为工具,使您的游戏真正独特。通过本章的知识,您将能够创建您想要的材质。
使用 Unity 内置的 CgInclude 文件
我们编写自己的 CgInclude 文件的第一个步骤是了解 Unity 已经为我们提供了哪些着色器。在编写表面着色器时,幕后有很多事情发生,这使得编写表面着色器的过程非常高效。我们可以在您安装 Unity 的目录中找到的包含的 CgInclude 文件中看到此代码,在 Editor | Data | CGIncludes。这个文件夹中的所有文件都在屏幕上使用我们的着色器渲染我们的对象。其中一些文件负责阴影和光照,一些文件负责辅助函数,还有一些文件管理平台依赖项。没有它们,我们的着色器编写体验将会更加费力。
您可以在以下链接中找到 Unity 提供给我们的一张信息列表:
让我们开始理解这些内置的 CgInclude 文件的过程,使用来自 UnityCG.cginc 文件的一些内置辅助函数:

准备工作
在我们开始深入编写着色器的核心内容之前,我们需要在我们的场景中设置一些项目。让我们做以下操作,然后打开您选择的 IDE 中的着色器:
- 
创建一个新的场景,并用一个简单的球体模型填充它。 
- 
创建一个新的着色器( Desaturate)和一个材质(DesaturateMat)。
- 
将新的着色器附加到新的材质上,并将材质分配给球体。 
- 
创建一个方向光并将其放置在球体上方。 
- 
最后,从 Unity 的 CgInclude文件夹中打开UnityCG.cginc文件,该文件夹位于 Unity 的安装目录中。这将使我们能够分析一些辅助函数的代码,以便我们可以在使用它们时理解正在发生什么。
- 
您现在应该已经设置了一个简单的场景来编写着色器。参考以下截图,这是一个示例: 

如何操作...
场景准备就绪后,我们现在可以开始尝试使用 UnityCG.cginc 文件中包含的一些内置辅助函数了。双击为这个场景创建的着色器,以便在您选择的 IDE 中打开它,并按照以下步骤插入代码:
- 将以下代码添加到新着色器文件的 Properties块中。我们的示例着色器需要一个纹理和一个滑动条:
Properties 
{ 
    _MainTex ("Base (RGB)", 2D) = "white" {} 
    _DesatValue ("Desaturate", Range(0,1)) = 0.5 
} 
我们接下来需要确保在 Properties 和 CGPROGRAM 块之间创建数据连接。
- 在 CGPROGRAM声明和#pragma指令之后放置以下代码,移除其他默认属性:
sampler2D _MainTex; 
fixed _DesatValue; 
- 接下来,我们只需更新我们的 surf()函数,以包含以下代码。我们引入了一个我们还没有见过的函数,它是 Unity 的UnityCG.cginc文件内置的:
void surf (Input IN, inout SurfaceOutputStandard o) 
{ 
  half4 c = tex2D (_MainTex, IN.uv_MainTex); 
  c.rgb = lerp(c.rgb, Luminance(c.rgb), _DesatValue); 
  o.Albedo = c.rgb; 
  o.Alpha = c.a; 
} 
- 保存您的脚本并返回到 Unity 编辑器。从那里,您应该能够将材质分配给 DesaturateMat(我使用了来自第三章|纹理文件夹的TerrainBlend纹理):

- 修改着色器代码后,您应该会看到类似于前面的截图。我们只是使用了一个内置的辅助函数,这个函数是 Unity 的 CgInclude文件的一部分,以给我们一个去饱和化主纹理的效果。注意,如果我们把值改为1,所有的颜色都会消失,给我们一个灰度效果:

它是如何工作的...
使用名为 Luminance() 的内置辅助函数,我们能够快速在我们的着色器上获得去饱和化或灰度效果。这一切都是因为当我们使用 Surface Shader 时,UnityCG.cginc 文件会自动带到我们的着色器中。
如果您在脚本编辑器中搜索 UnityCG.cginc 文件,您将在第 473 行找到这个函数的实现。以下代码片段来自该文件:
// Converts color to luminance (grayscale)
inline half Luminance(half3 rgb)
{
    return dot(rgb, unity_ColorSpaceLuminance.rgb);
}
由于这个函数包含在文件中,并且 Unity 会自动与这个文件一起编译,因此我们也可以在我们的代码中使用这个函数,从而减少我们需要反复编写的代码量。
注意,还有一个名为 Lighting.cginc 的文件,这是 Unity 自带的。这个文件包含了我们在声明类似 #pragma Surface surf Lambert 这样的内容时使用的所有光照模型。浏览这个文件可以发现,所有内置的光照模型都定义在这里,以便重用和模块化。
还有更多...
您会注意到我们使用的 Luminance 函数将返回传入的颜色和名为 unity_ColorSpaceLuminance 的属性之间的点积。要查看这是什么,您可以使用文本编辑器的 查找 菜单 (*Ctrl *+ F) 并输入它。搜索后,您应该能够在第 28 行看到以下内容:
#ifdef UNITY_COLORSPACE_GAMMA
#define unity_ColorSpaceGrey fixed4(0.5, 0.5, 0.5, 0.5)
#define unity_ColorSpaceDouble fixed4(2.0, 2.0, 2.0, 2.0)
#define unity_ColorSpaceDielectricSpec half4(0.220916301, 0.220916301, 0.220916301, 1.0 - 0.220916301)
#define unity_ColorSpaceLuminance half4(0.22, 0.707, 0.071, 0.0) // Legacy: alpha is set to 0.0 to specify gamma mode
#else // Linear values
#define unity_ColorSpaceGrey fixed4(0.214041144, 0.214041144, 0.214041144, 0.5)
#define unity_ColorSpaceDouble fixed4(4.59479380, 4.59479380, 4.59479380, 2.0)
#define unity_ColorSpaceDielectricSpec half4(0.04, 0.04, 0.04, 1.0 - 0.04) // standard dielectric reflectivity coef at incident angle (= 4%)
#define unity_ColorSpaceLuminance half4(0.0396819152, 0.458021790, 0.00609653955, 1.0) // Legacy: alpha is set to 1.0 to specify linear mode
#endif
这意味着,根据所使用的色彩空间,给定的值将发生变化。默认情况下,Unity 使用伽玛色彩空间,因为只有某些平台支持线性。要检查你的项目中使用的是哪种色彩空间,你可以转到 Edit | Project Settings | Player | Other Settings,并查看色彩空间属性。
想了解更多关于色彩空间的信息,请查看:www.kinematicsoup.com/news/2016/6/15/gamma-and-linear-space-what-they-are-how-they-differ。
使用 CgInclude 以模块化方式构建你的着色器世界
了解内置的 CgInclude 文件很好,但如果我们想构建自己的 CgInclude 文件来存储我们自己的光照模型和辅助函数呢?实际上,我们可以创建自己的 CgInclude 文件,但在我们能够高效地在着色器编写管道中使用它们之前,我们需要学习一些更多的代码语法。让我们看看从头开始创建一个新的 CgInclude 文件的过程。
准备工作
让我们通过这个过程来生成这个食谱的新条目:
- 在项目标签页中,右键单击 Assets文件夹,并选择 Show in Explorer。你应该能看到你的项目文件夹。然后通过右键单击并选择 New | Text Document 来创建一个文本文件:

- 将文件重命名为 MyCGInclude,并将.txt文件扩展名替换为.cginc:

- 
Windows 将会给出一个警告消息,说文件可能会变得不可用,但它仍然可以工作。 
- 
将这个新的 .cginc文件导入到你的 Unity 项目中,并让它编译。如果一切顺利,你将看到 Unity 已经知道将其编译成CgInclude文件。
现在,我们已经准备好开始创建我们自己的自定义 CgInclude 代码。只需双击你创建的 CgInclude 文件,以便在你的首选 IDE 中打开它。
如何操作...
当我们的 CgInclude 文件打开时,我们可以开始输入将使其与我们的表面着色器一起工作的代码。以下步骤将使我们的 CgInclude 文件准备好在表面着色器中使用,并允许我们在开发更多着色器时不断向其中添加更多代码:
- 我们从所谓的预处理器指令开始我们的 CgInclude文件。这些指令包括#pragma和#include等语句。在这种情况下,我们想要定义一组新的代码,如果我们的着色器在编译指令中包含此文件,则将执行这些代码。在你的CgInclude文件顶部输入以下代码:
#ifndef MY_CG_INCLUDE 
#define MY_CG_INCLUDE 
- 我们始终需要确保使用 #endif来关闭#ifndef或#ifdef的定义检查,就像在 C# 中,一个if语句需要用两个括号来关闭一样。在#define指令之后立即输入以下代码:
#endif 
- 在这一点上,我们只需要实现 CgInclude文件的内容。因此,我们在#define之后和#endif之前输入以下代码来完成我们的CgInclude文件:
fixed4 _MyColor; 
inline fixed4 LightingHalfLambert(SurfaceOutput s, fixed3 lightDir, fixed atten) 
{ 
    fixed diff = max(0, dot(s.Normal, lightDir)); 
    diff = (diff + 0.5)*0.5; 
    fixed4 c; 
    c.rgb = s.Albedo * _LightColor0.rgb * ((diff * _MyColor.rgb) * atten); 
    c.a = s.Alpha; 
    return c; 
} 
#endif 
- 完成这些后,你现在就有了你的第一个CgInclude文件。仅用这么一点代码,我们就可以大大减少需要重写的代码量,并且我们可以开始在这里存储我们经常使用的光照模型,这样我们就永远不会丢失它们。你的CgInclude文件应该看起来类似于以下代码:
#ifndef MY_CG_INCLUDE 
#define MY_CG_INCLUDE 
fixed4 _MyColor; 
inline fixed4 LightingHalfLambert(SurfaceOutput s, fixed3 lightDir, fixed atten) 
{ 
    fixed diff = max(0, dot(s.Normal, lightDir)); 
    diff = (diff + 0.5)*0.5; 
    fixed4 c; 
    c.rgb = s.Albedo * _LightColor0.rgb * ((diff * _MyColor.rgb) * atten); 
    c.a = s.Alpha; 
    return c; 
} 
#endif 
在我们可以完全利用这个CgInclude文件之前,我们还需要完成几个步骤。我们只需要告诉当前我们正在处理的着色器使用这个文件及其代码。为了完成创建和使用CgInclude文件的过程,让我们完成以下步骤:
- 我们必须将我们的CgInclude文件放在与我们的着色器相同的目录中,所以从项目标签中将其拖放到第十一章|着色器文件夹中。
如果步骤 1未完成,你将得到一个编译错误。
- 
现在我们在这个文件夹中,选择在先前的菜谱中创建的 Desaturate着色器,并对其进行复制(*Ctrl *+ D)。将副本命名为Colorize,然后双击它以打开它。
- 
从那里,更新着色器名称: 
Shader "CookbookShaders/Chapter11/Colorize" 
- 如果你将注意力转向我们的着色器,你会看到我们需要告诉我们的CGPROGRAM块包含我们的新CgInclude文件,这样我们就可以访问它包含的代码。修改我们的CGPROGRAM块的指令以包含以下代码:
CGPROGRAM
#include "MyCGInclude.cginc" 
// Physically based Standard lighting model, and enable shadows on all light types
#pragma surface surf Standard fullforwardshadows
- 我们当前的着色器目前正在使用内置的标准光照模型,但我们想使用我们在CgInclude中创建的半朗伯光照模型。由于我们已经包含了CgInclude文件中的代码,我们可以使用以下代码使用半朗伯光照模型:
CGPROGRAM 
#include "MyCGInclude.cginc" 
#pragma surface surf HalfLambert 
- 最后,我们还在我们的CgInclude文件中声明了一个自定义变量,以表明我们可以为我们的着色器设置默认变量。要查看此操作,请在着色器的Properties块中输入以下代码:
Properties 
{ 
    _MainTex ("Base (RGB)", 2D) = "white" {} 
    _DesatValue ("Desaturate", Range(0,1)) = 0.5 
    _MyColor ("My Color", Color) = (1,1,1,1) 
} 
- 最后,我们需要更新我们的surf函数头,因为我们使用了LightingHalfLambert函数中的SurfaceOutput:
 void surf (Input IN, inout SurfaceOutput o) 
- 回到 Unity 中,创建一个新的材质,它将使用新创建的Colorize着色器(ColorizeMat),并将其分配给我们在上一个菜谱中创建的球体。像往常一样分配材质,并从检查器中修改 MyColor 值以查看它如何修改对象。以下截图显示了使用我们的CgInclude文件的结果:

它是如何工作的...
当使用着色器时,我们可以使用#include预处理指令包含其他代码集。这告诉 Unity 我们希望当前着色器使用包含文件中的代码;这就是为什么这些文件被称为CgInclude文件。我们使用#include指令包含 Cg 代码片段。
一旦我们声明了#include指令,并且 Unity 能够在项目中找到该文件,Unity 就会开始寻找已经定义的代码片段。这就是我们开始使用#ifndef和#endif指令的地方。当我们声明#ifndef指令时,我们只是在说“如果没有定义,就用一个名字定义一些东西。”在这个菜谱的情况下,我们说我们想要#define MY_CG_INCLUDE。所以,如果 Unity 找不到名为MY_CG_INCLUDE的定义,它会在CgInclude文件编译时创建它,从而让我们能够访问随后的代码。#endif方法只是简单地表示这是这个定义的结束,所以停止寻找更多的代码。
你现在可以看到这是多么强大,我们可以在一个文件中存储所有的光照模型和自定义变量,从而大大减少我们需要编写的代码量。真正的力量在于你可以在CgInclude文件中定义多个函数状态,从而开始给你的着色器提供灵活性。
实现毛皮着色器
材质的外观取决于其物理结构。着色器试图模拟它们,但在这样做的时候,它们过于简化了光的行为。具有复杂宏观结构的材料尤其难以渲染。许多织物和动物毛皮就是这样。这个菜谱将向你展示如何模拟毛皮和其他材料(如草地),这些材料不仅仅是平面。为了做到这一点,相同的材质被多次绘制,每次都增加大小。这创造了毛发的错觉。
这里展示的着色器是基于 Jonathan Czeck 和 Aras Pranckevičius 的工作:

准备工作
为了使这个菜谱工作,你需要一个显示你希望如何显示毛发的纹理:

我在书的第十一章Textures文件夹中提供了两个示例,包括书中的示例代码(Faux Fur和panda)。
和之前所有的着色器一样,你需要创建一个新的标准表面着色器(Fur)和一个材质(FurMat)来承载它,并将其附加到一个球体上进行演示:

如何做到这一点...
在这个菜谱中,我们可以开始修改一个标准表面着色器:
- 双击Fur着色器以在您选择的 IDE 中打开它。一旦打开,添加以下加粗的Properties:
Properties 
{
  _Color ("Color", Color) = (1,1,1,1)
  _MainTex ("Albedo (RGB)", 2D) = "white" {}
  _Glossiness ("Smoothness", Range(0,1)) = 0.5
  _Metallic ("Metallic", Range(0,1)) = 0.0
 _FurLength ("Fur Length", Range (.0002, 1)) = .25
 _Cutoff ("Alpha Cutoff", Range(0,1)) = 0.5 // how "thick"
 _CutoffEnd ("Alpha Cutoff end", Range(0,1)) = 0.5 // how thick they are at the end
 _EdgeFade ("Edge Fade", Range(0,1)) = 0.4
 _Gravity ("Gravity Direction", Vector) = (0,0,1,0)
 _GravityStrength ("Gravity Strength", Range(0,1)) = 0.25
}
- 这个着色器需要你重复执行相同的流程多次。我们将使用在使用 CgIncludes 使你的着色器世界模块化菜谱中介绍的技术,将单个流程中所有必要的代码组合到一个外部文件中。让我们开始创建一个名为FurPass.cginc的新CgInclude文件,并包含以下代码:
#pragma target 3.0
fixed4 _Color;
sampler2D _MainTex;
half _Glossiness;
half _Metallic;
uniform float _FurLength;
uniform float _Cutoff;
uniform float _CutoffEnd;
uniform float _EdgeFade;
uniform fixed3 _Gravity;
uniform fixed _GravityStrength;
void vert (inout appdata_full v)
{
  fixed3 direction = lerp(v.normal, _Gravity * _GravityStrength + v.normal * (1-_GravityStrength), FUR_MULTIPLIER);
  v.vertex.xyz += direction * _FurLength * FUR_MULTIPLIER * v.color.a;
  //v.vertex.xyz += v.normal * _FurLength * FUR_MULTIPLIER * v.color.a;
}
struct Input {
  float2 uv_MainTex;
  float3 viewDir;
};
void surf (Input IN, inout SurfaceOutputStandard o) {
  fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
  o.Albedo = c.rgb;
  o.Metallic = _Metallic;
  o.Smoothness = _Glossiness;
  //o.Alpha = step(_Cutoff, c.a);
  o.Alpha = step(lerp(_Cutoff,_CutoffEnd,FUR_MULTIPLIER), c.a);
  float alpha = 1 - (FUR_MULTIPLIER * FUR_MULTIPLIER);
  alpha += dot(IN.viewDir, o.Normal) - _EdgeFade;
  o.Alpha *= alpha;
}
- 回到你的原始着色器,并在ENDCG部分之后添加这个额外的流程:
void surf (Input IN, inout SurfaceOutputStandard o) {
  // Albedo comes from a texture tinted by color
  fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
  o.Albedo = c.rgb;
  // Metallic and smoothness come from slider variables
  o.Metallic = _Metallic;
  o.Smoothness = _Glossiness;
  o.Alpha = c.a;
}
ENDCG
CGPROGRAM
#pragma surface surf Standard fullforwardshadows alpha:blend vertex:vert
#define FUR_MULTIPLIER 0.05
#include "FurPass.cginc"
ENDCG
- 返回 Unity,并将FauxFur纹理分配到 Albedo(RGB)属性中。你应该会注意到着色器上沿着一些小点:

- 增加更多遍历,逐步增加FUR_MULTIPLIER。使用 20 遍,从0.05到0.95可以得到相当不错的结果:
    CGPROGRAM
    #pragma surface surf Standard fullforwardshadows alpha:blend vertex:vert
    #define FUR_MULTIPLIER 0.05
    #include "FurPass.cginc"
    ENDCG
    CGPROGRAM
    #pragma surface surf Standard fullforwardshadows alpha:blend vertex:vert
    #define FUR_MULTIPLIER 0.1
    #include "FurPass.cginc"
    ENDCG
    CGPROGRAM
    #pragma surface surf Standard fullforwardshadows alpha:blend vertex:vert
    #define FUR_MULTIPLIER 0.15
    #include "FurPass.cginc"
    ENDCG
    // ... 0.2 - 0.85 here
        CGPROGRAM
    #pragma surface surf Standard fullforwardshadows alpha:blend vertex:vert
    #define FUR_MULTIPLIER 0.90
    #include "FurPass.cginc"
    ENDCG
    CGPROGRAM
    #pragma surface surf Standard fullforwardshadows alpha:blend vertex:vert
    #define FUR_MULTIPLIER 0.95
    #include "FurPass.cginc"
    ENDCG
  }
  Fallback "Diffuse"
}
- 一旦着色器被编译并附加到材质上,您就可以在检查器中更改其外观。
Fur Length属性决定了毛发壳之间的空间,这将改变毛发的长度。较长的毛发可能需要更多的遍历来看起来逼真。
Alpha Cutoff和Alpha Cutoff End用于控制毛发的密度以及其如何逐渐变薄。
Edge Fade决定了毛发的最终透明度和其外观的模糊程度。较软的材料应该有较高的Edge Fade。
最后,Gravity Direction和Gravity Strength使毛发壳弯曲,以模拟重力效果:

它是如何工作的...
本食谱中介绍的技术被称为 Lengyel 的同心毛发壳技术,或简单地称为壳技术。它是通过创建需要渲染的几何形状的逐渐增大的副本来工作的。通过适当的透明度,它会产生连续毛发线的错觉:

壳技术非常灵活且相对容易实现。要实现逼真的毛发,不仅需要拉伸模型的几何形状,还需要改变其顶点。这可以通过细分着色器实现,这些着色器更为先进,但本书没有涉及。
在这个Fur着色器中的每一遍都在FurPass.cginc中。顶点函数创建了一个稍微大一点的模型版本,这是基于法线拉伸原理。此外,还考虑了重力效应,因此越远离中心,效果越强烈:
void vert (inout appdata_full v) 
{ 
    fixed3 direction = lerp(v.normal, _Gravity * _GravityStrength + v.normal * (1-_GravityStrength), FUR_MULTIPLIER); 
    v.vertex.xyz += direction * _FurLength * FUR_MULTIPLIER * v.color.a; 
} 
在本例中,alpha 通道用于确定毛发的最终长度,这允许更精确的控制。
最后,表面函数从 alpha 通道读取控制掩码。它使用截止值来确定哪些像素要显示,哪些要隐藏。此值从第一个到最后的毛发壳都会改变,以匹配Alpha Cutoff和Alpha Cutoff End:
o.Alpha = step(lerp(_Cutoff,_CutoffEnd,FUR_MULTIPLIER), c.a); 
float alpha = 1 - (FUR_MULTIPLIER * FUR_MULTIPLIER); 
alpha += dot(IN.viewDir, o.Normal) - _EdgeFade; 
o.Alpha *= alpha; 
毛发的最终 alpha 值还取决于其与摄像机的角度,使其看起来更柔和。
还有更多...
Fur着色器已被用于模拟毛发。然而,它可以用于各种其他材质。它非常适合由多层自然构成的材质,如森林树冠、毛茸茸的云朵、人类头发,甚至草地。
在本书的示例代码中,可以看到仅通过调整参数就使用相同着色器的额外示例:

有许多其他改进可以显著提高其真实感。您可以通过根据当前时间改变重力的方向来添加一个非常简单的风动画。如果校准正确,这可以给人一种毛发因风而移动的印象。
此外,您还可以在角色移动时使您的毛发移动。所有这些小调整都有助于使您的毛发看起来更加逼真,给人一种它不仅仅是绘制在表面上的静态材质的错觉。不幸的是,这个着色器代价很高:20 次遍历计算非常沉重。遍历的次数大致决定了材质的逼真程度。您应该调整毛发长度和遍历次数,以获得最适合您的效果。鉴于这个着色器的性能影响,建议您拥有几个不同遍历次数的材质;您可以在不同的距离处使用它们,从而节省大量的计算。
使用数组实现热图
使着色器难以掌握的一个特点是缺乏适当的文档。大多数开发者通过在代码中摸索来学习着色器,而没有深入了解正在发生的事情。由于 Cg/HLSL 做出了很多假设,其中一些并没有得到适当的宣传,这个问题变得更加严重。Unity3D 允许 C#脚本使用SetFloat、SetInt、SetVector等方法与着色器通信。不幸的是,Unity3D 没有SetArray方法,这使得许多开发者认为Cg/HLSL也不支持数组。这并不正确。本文将向您展示如何将数组传递给着色器。只需记住,GPU 高度优化了并行计算,在着色器中使用循环将大大降低其性能。
对于这个配方,我们将实现一个热图,如下面的截图所示:

准备工作
这个配方中的效果是从一组点创建热图。这个热图可以叠加在另一张图片上,如前面的截图所示。以下步骤是必要的:
- 
创建一个带有您想要用于 Heatmap的纹理的四边形(GameObject|3D Object|Quad)。在这个例子中,使用了伦敦的地图。为了将纹理放在四边形上,使用 Unlit/Texture 着色器创建一个新的材质(Map),并将图像分配给 Base (RGB)属性。创建后,将此对象拖放到四边形上。四边形对象的位置必须设置为(0,0,0)。
- 
创建另一个四边形,并将其放置在之前的四边形之上。我们的 Heatmap将显示在这个四边形上。
- 
将新的着色器( Heatmap)和材质(HeatmapMat)附加到第二个四边形上:

- 为了便于可视化,我还选择了 MainCamera,并将投影改为Orthographic,大小属性设置为0.5。
如何实现...
这个着色器与之前创建的着色器相当不同,但它相对较短。因此,以下步骤提供了整个代码:
- 将此代码复制到新创建的着色器中:
shader " Heatmap" { 
    Properties { 
        _HeatTex ("Texture", 2D) = "white" {} 
    } 
    Subshader { 
        Tags {"Queue"="Transparent"} 
        Blend SrcAlpha OneMinusSrcAlpha // Alpha blend 
        Pass { 
            CGPROGRAM 
            #pragma vertex vert              
            #pragma fragment frag 
            struct vertInput { 
                float4 pos : POSITION; 
            };   
            struct vertOutput { 
                float4 pos : POSITION; 
                fixed3 worldPos : TEXCOORD1; 
            }; 
            vertOutput vert(vertInput input) { 
                vertOutput o; 
                o.pos = mul(UNITY_MATRIX_MVP, input.pos); 
                o.worldPos = mul(_Object2World, input.pos).xyz; 
                return o; 
            } 
            uniform int _Points_Length = 0; 
            uniform float3 _Points [20];        // (x, y, z) = position
            uniform float2 _Properties [20];    // x = radius, y = intensity 
            sampler2D _HeatTex; 
            half4 frag(vertOutput output) : COLOR { 
                // Loops over all the points 
                half h = 0; 
                for (int i = 0; i < _Points_Length; i ++) 
                { 
                    // Calculates the contribution of each point 
                    half di = distance(output.worldPos, _Points[i].xyz); 
                    half ri = _Properties[i].x; 
                    half hi = 1 - saturate(di / ri); 
                    h += hi * _Properties[i].y; 
                } 
                // Converts (0-1) according to the heat texture 
                h = saturate(h); 
                half4 color = tex2D(_HeatTex, fixed2(h, 0.5)); 
                return color; 
            } 
            ENDCG 
        } 
    }  
    Fallback "Diffuse" 
} 
- 一旦将此脚本附加到您的材料上,您应该为热图提供渐变纹理。重要的是要配置它,使其 Wrap Mode 设置为 Clamp:

如果你的热图将要用作叠加层,那么请确保渐变纹理具有 alpha 通道,并且纹理是以“Alpha is Transparency”选项导入的。
- 使用以下代码创建一个名为 HeatmapDrawer的新脚本:
using UnityEngine;
public class HeatmapDrawer : MonoBehaviour
{
    public Vector4[] positions;
    public float[] radiuses;
    public float[] intensities;
    public Material material;
    void Start()
    {
        material.SetInt("_Points_Length", positions.Length);
        material.SetVectorArray("_Points", positions);
        Vector4[] properties = new Vector4[positions.Length];
        for (int i = 0; i < positions.Length; i++)
        {
            properties[i] = new Vector2(radiuses[i], intensities[i]);
        }
        material.SetVectorArray("_Properties", properties);
    }
}
- 
将脚本附加到场景中的对象上,最好是四边形。然后,将为此效果创建的材料拖到脚本的 Material槽中。通过这样做,脚本将能够访问Material并初始化它。
- 
最后,扩展你的脚本中的位置、半径和强度字段,并用你的热图值填充它们。位置表示你的热图在世界坐标中的点,半径表示它们的大小,强度表示它们对周围区域的影响强度: 

- 如果一切顺利,当你玩游戏时,你应该会注意到以下截图类似的内容:

如果你没有看到这个,请确保热图放置在地图四边形之前,并且两个对象都在摄像机之前。
如果你收到一个警告说点的数量已更改,请进入你的着色器,通过添加一个空格修改脚本,然后再次保存。
它是如何工作的...
这个着色器依赖于本书之前未介绍的内容;第一个是数组。Cg 允许使用以下语法创建数组:
uniform float3 _Points [20];     
Cg 不支持未知大小的数组:你必须预先分配你需要的所有空间。前面的代码行创建了一个包含 20 个元素的数组。
Unity 允许我们通过使用多种方法来设置数组,包括 SetVectorArray、SetColorArray、SetFloatArray 和 GetMatrixArray。
SetVectorArray 函数目前只与 Vector4 类一起使用。这不会给我们带来任何问题,因为你可以自动将一个 Vector3 分配给一个 Vector4,Unity 会自动在最后一个元素中包含一个零。此外,你也可以在 Update 循环中使用我们的 Start 代码来能够看到我们修改它们时的值变化,但这将非常耗费计算资源。
在着色器的片段函数中,有一个类似的循环,它对材料的每个像素查询所有点以找到它们对热图的贡献:
half h = 0; 
for (int i = 0; i < _Points_Length; i ++) 
{ 
    // Calculates the contribution of each point 
    half di = distance(output.worldPos, _Points[i].xyz); 
    half ri = _Properties[i].x; 
    half hi = 1 - saturate(di / ri); 
    h += hi * _Properties[i].y; 
} 
h 变量存储了所有点的热量,这些点由它们的半径和强度给出。然后它被用来查找从渐变纹理中使用的颜色。
着色器和数组是一个获胜的组合,尤其是由于非常少有游戏能够充分利用它们。然而,它们引入了一个显著的瓶颈,因为对于每个像素,着色器必须遍历所有点。
第十二章:Shader Graph
在本章中,您将学习以下配方:
- 
创建 Shader Graph 项目 
- 
实现简单的 Shader Graph 
- 
通过 Shader Graph 将属性暴露给检查器 
- 
实现发光高亮系统 
简介
首次发布于 Unity 2018.1,Shader Graph 允许您通过连接节点而不是直接编写代码来使用可视化界面创建着色器。这将使开发者,包括艺术家,能够以类似于 3D 建模程序(如 Autodesk Maya 和 Blender)中的材质编辑器或 Unreal Engine 中的材质编辑器的方式创建着色器。在撰写本文时,Shader Graph 仅支持某些类型的项目,并且不像从头编写着色器那样具有相同的灵活性。
创建 Shader Graph 项目
与我们之前编写的所有着色器不同,Shader Graph 工具要求用户拥有一个使用轻量级渲染管道的项目。轻量级渲染管道旨在用于低端硬件,并专注于单遍绘制,尽可能减少绘制次数。对于这个第一个配方,您将通过使用 Shader Graph 所需的设置来确保您的项目设置正确。
如何操作...
要开始,我们首先需要创建我们的新项目:
- 从 Unity Hub 创建新项目时,将模板设置为轻量级 RP(预览):

Shader Graph 目前仅与轻量级渲染管道兼容,这确保了图将正确工作。
Shader Graph 仅适用于 Unity 2018.1 及以上版本。如果您正在使用更早的版本,请确保在继续本章之前升级。
- 选中后,按创建项目按钮:

如您所见,此项目已包含一些资产在内。
- 
当 Unity 编辑器打开时,Shader Graph 默认不包含在 Unity 编辑器中。要访问它,您需要使用 Unity 包管理器。从顶部菜单,转到窗口 | 包管理器。包管理器允许您安装或卸载 Unity 的不同方面。您会注意到两个按钮,一个用于项目中的包(项目内),另一个用于所有当前可下载的包(全部)。 
- 
从包管理器窗口,点击全部按钮,向下滚动直到您看到 Shader Graph 按钮,然后选择它。从那里,点击安装 1.1.9-preview 按钮,等待它完成下载并导入内容: 

- 
一切下载完成后,转到项目标签页,选择创建 | 着色器,看看您是否能找到以下新选项: - 
PBR 图 
- 
子图 
- 
无光照图 
 
- 
它是如何工作的...
如前所述,着色器图目前仅与轻量级渲染管线兼容。确保项目使用该管道的最简单方法是在创建项目时将其作为模板选择。
创建项目后,我们使用新添加的包管理器,该管理器允许你安装或卸载 Unity 的不同方面。在这种情况下,你已经添加了着色器图功能。
如果一切都已经包含在内,着色器图已经成功安装,你应该能够完成本章其余的食谱!
实现一个简单的着色器图
为了熟悉着色器图的用户界面,让我们通过采样纹理来创建一个简单的着色器,来创建一个类似之前看到的东西。
准备工作
确保你已经创建了一个使用轻量级渲染管线的项目,如 创建着色器图项目 食谱中所述。之后,完成以下步骤:
- 
如果你还没有这样做,通过前往文件 | 新场景来创建一个新的场景。 
- 
之后,我们需要有一个东西来展示我们的着色器,所以让我们通过前往游戏对象 | 3D 对象 | 球体来创建一个新的球体: 

如何做...
我们将从一个简单的着色器图开始。
- 
从项目窗口中,通过前往创建 | 着色器 | PBR 图来创建一个新的着色器,并将其命名为 SimpleGraph。
- 
之后,通过前往创建 | 材质(我将其命名为 SimpleGraphMat)来创建一个新的材质。接下来,通过选择材质,然后在检查器选项卡中,你应该选择顶部的着色器下拉菜单并选择 graphs/SimpleGraph 来分配着色器。
和往常一样,你也可以将着色器拖放到材质的顶部。
- 接下来,将材质拖放到场景中的球形对象上,这样我们就可以看到着色器在实际中的应用:

- 现在设置完成后,我们可以开始创建图表。如果你选择着色器,你应该注意到检查器选项卡中有一个按钮,上面写着打开着色器编辑器。点击该按钮,着色器图编辑器将自动打开:

要在着色器图编辑器内移动,你可以使用鼠标滚轮来缩放,并且你可以按住中间鼠标按钮并拖动来平移图表。或者,你也可以使用 Alt + 左键鼠标按钮。
- 要开始,让我们添加一个纹理。在 PBR Master 的左侧右键单击并选择创建节点。从那里,你会看到一个菜单,允许你输入节点的名称或从菜单中选择。要浏览菜单,选择输入 | 纹理 | 样本纹理 2D。或者,你可以输入 tex,然后使用箭头键选择 Sample Texture 2D 选项,然后按 Enter:
你也可以通过将鼠标移到你想创建节点的地方并按空格键来创建一个新的节点。

随意点击并拖动 Shader Graph 上的任何节点,使其更容易看到。
- 在“Sample Texture 2D”节点的左侧,点击带有点的圆圈以将纹理分配为我们可以使用的东西(我使用了项目包含的 Ground_Albedo 属性):

此后,您应该在节点下看到来自纹理的数据图像。
- 点击并拖动位于“Sample Texture 2D”节点右侧的粉色圆圈到“PBR Master”节点的输入 Albedo 节点:

正如我们在前面的章节中学到的,我们可以将fixed4赋予fixed3,它将忽略第四个参数。
如果您对某个特定节点的作用或属性的含义感兴趣,请随意右键单击它并选择打开文档。它将打开一个窗口,其中将提供节点作用的描述。
- 点击顶部菜单中的“保存资产”按钮,然后返回 Unity 编辑器:

如您所见,着色器现在已更新,包含来自 Shader Graph 编辑器的信息!
它是如何工作的...
从 Shader Graph 中,我们介绍了我们遇到的第一批节点。值得注意的是屏幕上的“PBR Master”部分。这就是所有关于着色器的信息将去的地方。您可能会注意到属性与过去我们使用的常规 Standard Shader 非常相似,现在我们可以以类似的方式修改属性,但我们还可以创建额外的节点并将它们连接起来以创建独特的效果。
“Sample Texture2D”节点允许我们将“Texture”属性作为输入,然后将其数据作为右侧的 RGBA 输出(节点右侧的东西是输出,而左侧的东西是输入,例如“PBR Master”节点上的 Albedo 属性)。
注意,来自“Texture”输入的圆圈颜色为红色(T 代表纹理),RGBA 的输出为粉色(4 代表fixed4),而“PBR Master”节点的 Albedo 输入为黄色(3 代表fixed3)。
通过 Shader Graph 将属性公开给检查器
能够使用图编辑器创建图表并设置其属性真是太好了,但有时使用与之前创建的着色器相同的简单调整来使用相同的着色器也很不错。为此,我们可以使用黑板面板。
准备工作
确保您在先前的配方中创建了 SimpleGraph 着色器。之后,完成以下步骤:
- 
从“项目”选项卡中选择 SimpleGraph 着色器,并按Ctrl + D进行复制。一旦复制,将新创建的着色器命名为 ExposeProperty。
- 
接下来,创建一个新的材质( ExposePropertyMat),并将它使用的着色器设置为 graphs/ExposeProperty。
- 
将材质分配到场景中的球体上: 

由于我们使用的是前一个着色器的副本,所以项目应该看起来与上一个配方中的相同。
如何做到这一点...
如果您从检查器选项卡查看我们的着色器,您可能会注意到我们上一次配方中分配的 Ground_Albedo 图像的 Texture 属性。这样的属性可能是我们想要修改的,但默认情况下,它是灰色的,所以我们不能不进入 Shader Graph 就修改它。为了调整这一点,我们可以使用 Shader Graph 编辑器的 Blackboard 方面来公开属性:
- 双击 ExposeProperty 着色器以打开 Shader Graph:

注意,在左下角,有 graphs/ExposeProperty 黑板菜单。这将包含我们可以通过检查器修改的所有参数的列表。
如您可能已经知道,Shader Graph 是全新的,因此容易出问题,例如黑板默认不可见。遗憾的是,目前没有通过菜单开启或关闭它的方法。如果您看不到黑板,您可以尝试保存您的图并返回。或者,您可以通过进入布局 | 恢复出厂设置来重置您的布局...
- 从黑板面板中,点击+图标并选择 Texture:

- 
从那里,您可以给属性起一个名字(我使用了 TextureProperty)。请注意,在默认情况下,您可以以与之前相同的方式分配一个纹理。
- 
从那里,要将属性连接到我们当前的着色器,请将带有属性名称的按钮拖放到 Shader Graph 中。或者,您可以右键单击并选择创建节点。一旦进入菜单,您可以选择属性 | 属性:TextureProperty。之后,将属性节点中的 TextureProperty 输出连接到 Sample Texture 2D 节点的 Texture 输入: 

- 之后,点击保存资产按钮并返回 Unity 编辑器:

现在,您应该能够通过检查器将 TextureProperty 分配到任何您想要的地方,而且您不需要再次进入图来做出这些更改!
它是如何工作的...
黑板菜单允许您创建可以从检查器访问的变量。这与前几章中的属性块以类似的方式工作。目前,它支持以下类型:
- 
Vector1 
- 
Vector2 
- 
Vector3 
- 
Vector4 
- 
颜色 
- 
纹理 
- 
立方体贴图 
- 
布尔值 
添加到黑板上的属性可以通过拖动它们来重新排序,并且可以通过双击名称来重命名每个属性。
更多关于黑板的信息,请查看以下链接:github.com/Unity-Technologies/ShaderGraph/wiki/Blackboard。
实现发光高亮系统
现在我们已经了解了如何构建着色器的一些背景信息,让我们看看一个我们可以潜在使用的着色器的真实世界示例。当玩某些类型的游戏时,你可能会注意到,当玩家面对可以与之交互的对象时,该对象可能会发光,例如在 Dontnod Entertainment 的Life is Strange,The Fullbright Company 的Gone Home,甚至在最近的移动游戏如 Jam City 的Harry Potter Hogwarts Mystery中。这是我们可以轻松在着色器图中做到的事情,这也会让我们看到 Shader Graph 被使用的非平凡示例。
准备工作
确保你已经使用轻量级渲染管线创建了一个项目,如创建着色器图项目配方中所述。之后,完成以下步骤:
- 
如果尚未创建,请通过转到“文件”|“新建场景”来创建一个新的场景。 
- 
之后,我们需要有一些东西来展示我们的着色器,所以让我们通过转到“游戏对象”|“3D 对象”|“球体”来创建一个新的球体。 
如何做到这一点...
我们将开始创建一个简单的着色器图:
- 
从“项目”窗口中,通过转到“创建着色器 PBR 图”来创建一个新的着色器,并将其命名为 GlowGraph。
- 
之后,通过转到“创建”|“材质”(我将其命名为 GlowGraphMat)来创建一个新的材质。然后,通过选择材质,然后从“检查器”选项卡中选择顶部的 Shader 下拉菜单,并将它设置为“graphs/GlowGraph”来将着色器分配给材质。
- 
然后,将材质拖放到场景中的球体对象上,这样我们就可以看到着色器在实际应用中的效果。 
- 
现在设置完成后,我们可以开始创建图。如果你选择着色器,你应该会看到在“检查器”选项卡中,将有一个按钮显示为“打开着色器图”。点击该按钮,着色器图编辑器将自动打开。 
- 
首先,我们将添加一个名为菲涅耳(发音为 fer-nel)效果的新节点。要添加它,请转到 PBR Master 节点的左侧,右键单击,然后选择创建节点。从那里,键入 Fresnel,一旦选中,按Enter键。
菲涅耳效果通常用于为对象提供边缘照明。有关更多信息,请参阅:github.com/Unity-Technologies/ShaderGraph/wiki/Fresnel-Effect-Node。
- 
创建后,将菲涅耳效果节点的输出连接到 PBR Master 节点的 Emission 属性。 
- 
为了更容易地了解每个节点的作用,点击 Albedo 属性左侧的灰色颜色并将其更改为不同的颜色,例如明亮的粉色: 

注意,由于菲涅耳效果使用该值作为 Emission 属性,因此它被应用于 Albedo 颜色之上。
- 
我们只想让物体的边缘发光,所以将菲涅耳效果节点的 Power属性更改为4。目前,围绕我们的物体的光是白色的,但我们可以通过乘以一个颜色来将其改为不同的颜色。
- 
要做到这一点,请转到黑板并点击 + 图标创建一个新的颜色,然后选择颜色。创建后,给它起一个名字( HoverColor),然后设置默认颜色。
- 
创建完成后,以我们之前在配方中学习的方式,将属性拖放到 Fresnel Effect 节点下方: 

- 现在,我们需要将它们相乘。通过选择 Math | Basic | Multiply 在它们之间创建一个新的节点。将 Fresnel Effect 节点的输出连接到乘法节点的 A。然后,将 HoverColor 属性连接到乘法节点的 B。之后,将乘法节点的输出连接到发射属性:

- 
保存图并返回 Unity 编辑器。你应该会注意到效果确实按预期工作。 
- 
从项目标签页中选择我们创建的 GlowGraph着色器。注意,检查器标签页包含了着色器中使用的属性信息:

尽管我们在着色器图中使用的是 HoverColor 这个名称,但在整个代码中它被称作 Color_AA468061。如果我们想在代码中引用它,就需要使用这个名称。
- 创建一个新的 C# 脚本,命名为 HighlightOnHover。双击它进入你的 IDE,并使用以下代码:
using UnityEngine;
public class HighlightOnHover : MonoBehaviour
{
    public Color highlightColor = Color.red;
    private Material material;
    // Use this for initialization
    void Start()
    {
        material = GetComponent<MeshRenderer>().material;
        // Turn off glow
        OnMouseExit();
    }
    void OnMouseOver()
    {
        material.SetColor("Color_AA468061", highlightColor);
    }
    void OnMouseExit()
    {
        material.SetColor("Color_AA468061", Color.black);
    }
}
- 保存你的脚本并返回 Unity 编辑器。从那里,将组件附加到你的球体上并开始游戏:

现在,当我们用鼠标高亮对象时,我们会看到悬停效果,但除此之外,它将自动关闭!
它是如何工作的...
发射属性反映了物体接收到的光线。如果发射是白色,它将以该颜色完全照亮。如果是黑色,它将表现得好像不存在。我们通过默认使用黑色来利用这一点。然而,如果我们把鼠标放在物体上,OnMouseOver 函数将被触发,导致它使用提到的颜色。
我可以就 Shader Graph 的话题写更多,但遗憾的是,这本书的空间不够。如果你想更深入地探索 Shader Graph,Andy Touch 已经整理了一系列 Shader Graph 的使用示例,这些可以作为很好的研究材料。请查看github.com/UnityTechnologies/ShaderGraph_ExampleLibrary。

 
                    
                     
                    
                 
                    
                
 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号