Unity Shaders and Effets Cookbook
《着色器和屏幕特效制作攻略》
这本书可以让你学习到如何使用着色器和屏幕特效让你的Unity工程拥有震撼的渲染画面。
—— Kenny Lammers
Unity Shaders and Effets Cookbook
Unity Shaders and Effets Cookbook
《着色器和屏幕特效制作攻略》
I like this book!记录学习过程并分享!
第一章:漫反射着色器
本章覆盖的内容是游戏开发中,在着色器流水线里一些常见的漫反射技术。你将会学到:
介绍
漫反射的内部结构,或者光照模型决定着一个shader质量的高低。所以,在开始写shader时,自定义漫反射计算的过程是非常有意义的。 在早期的计算机图形学中,漫反射着色器是使用固定功能的光照模型来完成的,图形程序员也只能稍微调整一下内置的这个光照模型,并且里面只能使用一组参数和纹理。在目前的行业中,Cg语言让我们对它可以进行更多的控制,并且还具备很大的灵活性,尤其是unity中的表面着色器。
在Shader中,漫反射的组成部分基本上描述了光从表面向各个方向反射的方式。这和反光镜的工作原理很像。但它们是有区别的。反光镜是一个带有反光的表面,它其实反射的是周围的环境图。但漫反射光照是把所有的光从光源的位置,将光线反射回观察者的眼中,比如我们把太阳比作光源的位置。在后边的章节中会说到反射。 目前而言,目的只是为了区分一下两者。
为了实现一个基础漫射光照模型,我们需要创建一个着色器,并且里面需包含自发光颜色、环境光颜色以及所有光源累积的总和。下面会来演示并创建一个完整的漫反射光照模型,同时也展示了一些不同的行业技巧,比如只使用纹理来创建模拟更复杂的漫反射光照模型的方法。
最后,本章内容你将会学习到如何创建基础着色器的基本操作。凭借此知识点,你可以创建几乎任何表面着色器。
第1节. 创建基础表面着色器
当我们进一步深入到本书的内容时,需要对Unity软件的基础使用有所了解,这点很重要。它会大大提高你的制作效率。如果你在Unity中可以非常熟练的创建着色器和材质球,那么就可以跳过这一节。这节是专门为了让新手顺利学习本书接下来的内容而准备的。
1.1、准备工作
打开Unity引擎,开始以下内容。首先创建一个新工程。本书提供了一个Unity的工程文件,所以你可以直接使用这个工程,当完成每一步的着色器(Shader)编写以后,只需要把自己写的Shader添加上去即可。做完这些准备后,我们开始走进实时着色器的奇妙世界。
1.2、如何实现...
在第一个着色器开始之前,为了方便接下来的操作我们先创建一个小场景,创建完成后你可以在GameObject菜单中添加需要的游戏组件。这里我们创建一个平面(plane)作为地面,和几个球体(sphere)用来展示我们写好的shader的渲染效果。场景中再添加一盏平行光。准备完成后,我们开始进入着色器的编写步骤:
- 步骤1:在Unity编辑器的Project面板中,选择Assets文件夹,并右键单击选择Create | folder。如下图1.1所示:
图1.1
【 这里需要注意的是,Project面板的布局设置的是:两栏布局(Two Column Layout)。如下图1.2所示: 】
图1.2
【如果你使用的是本书自带的Unity项目,可以直接跳到第4步。】
- 步骤2:将新创建的文件夹命名为Shaders(这个文件夹用来装我们写的着色器—Shader)。选择文件夹单击右键,在下拉列表中选择Rename进行重命名。或者选择文件夹点击键盘上的F2键,也可以重命名。
- 步骤3:接下来再创建一个文件夹并将其命名为Materials(这个文件夹用来装材质球—Material)。如图1.3创建好了两个件夹并完成了重新命名:
图1.3
- 步骤4:选择Shaders文件夹右键单击,出现下拉菜单后,点击Create | Shader(意思是在Shaders文件夹中创建一个着色器文件)如图1.4所示。 然后选择 Materials文件夹,步骤同上继续右键单击,在下拉菜单中选择Create | Material(意思是在Materials文件夹中我们创建一个材质球)如图1.5所示。
图1.4
图1.5
- 步骤5:将着色器和材质球都重命名为BasicDiffuse。如图1.6所示:
图1.6
【 注意:在步骤6开始之前(即打开Shader文件前),我们需要配置一下代码环境,可以安装Visual Studio Code(简称VSCode)或者Visual Studio。安装好之后在顶部菜单栏中单击Edit菜单选择Preferences | External Tools,在External Script Editor的下拉菜单中选择你所安装的代码配置环境的软件,如下图安装的是Visual Studio Code。图1.7是Unity2018版本里面的设置界面,图1.8是Unity2021版本里面的设置界面,这里就不展示更早期Unity的设置界面了。】
图1.7
图1.8
- 步骤6:这时我们就可以双击BasicDiffuse.shader打开Shader文件了,现在就可以显示Shader的代码了。(在早期老的Unity版本中如果不配置代码环境,双击shader文件时,Unity会使用默认脚本编辑器MonoDevelop打开,在这里我们使用的是外部配置代码环境)。VSCode安装包下载地址:【免费】VSCode安装包(VSCodeUserSetup-x64)资源-CSDN文库
【 打开着色器(Shader)文件后,你会看到Shader中有默认的代码,这是Unity中自带的。 在默认的Shader里会自带一个基础的漫反射光照模型并且还计算了一个纹理采样。我们会基于这个代码来学习,在它的基础上进行修改调整,快速开发成为自己的自定义着色器。】
- 步骤7:下面给我们的着色器赋予一个在材质球中选中的文件路径。我们在Shder的第一行代码里修改它。目的是为了便于在材质球中的Shader下拉列表中快速找到它。 我们将Shader路径重命名为“CookbookShaders/BasicDiffuse”。(这个名字并不是一成不变的,你是可以随意修改它,因此不用担心任何的依赖关系)如下所示:
Shader "CookbookShaders/BasicDiffuse"
- 修改完成之后点击键盘上的Ctrl+S,保存着色器,回到Unity编辑器中。 当Unity识别到文件已经更新时,它会自动编译Shader。 这时你的着色器中的代码应该是如下所示的样子:
Shader "CookbookShaders/BasicDiffuse"
{
Properties
{
_MainTex ("Base (RGB)", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 200
CGPROGRAM
#pragma surface surf Lambert
sampler2D _MainTex;
struct Input
{
float2 uv_MainTex;
};
void surf (Input IN, inout SurfaceOutput o)
{
half4 c = tex2D (_MainTex, IN.uv_MainTex);
o.Albedo = c.rgb;
o.Alpha = c.a;
}
ENDCG
}
FallBack "Diffuse"
}
- 步骤8:选择我们在步骤4中创建的名为BasicDiffuse的材质球,并查看它的Inspector面板。 从Shader下拉列表中,选择CookbookShaders | BasicDiffuse 如图1.9(如果你使用不同的路径名,那么你的Shader的路径就会不同)。把Shader指定到材质球上之后,就可以把材质球赋给场景中的物体了。如图1.10
图1.9
【现在把这个材质球指定到场景中的物体上,。在Project面板中选择材质球,把它直接拖拽到场景中的物体上即可;也可以将材质球拖到对象的Inspector选项卡上。】
图1.10
虽然在效果上没有什么特别之处。但是我们目的主要是为了设置着色器的开发环境,接下来我们就可以按照自己的需求来修改着色器了。
1.3、实现原理...
Unity已经完成了着色器环境的启动及运行的任务,以上的操作是很容易实现的。但事实上关于表面着色器本身有很多的基础原理是在底层实现的。Unity采用了Cg着色器语言,为我们处理了大量繁重的Cg代码,这样大大提高了我们写代码的效率。表面着色器语言的编写方式是一款更偏向基于组件的着色器。比如怎么去处理着色器中的纹理坐标和矩阵变换,类似这样的计算Unity已经为你完成了, 所以你不需要再从头去写 。早期的时候,我们是需要重新的一遍又一遍的去写这些计算,同时也会产生大量的代码。随着我们对表面着色器的认识越来越深入,我们自然会想去探索更多关于Cg底层的一些功能,以及也会想去了解Unity是如何让底层的图形处理单元(GPU)去工作的。
所以,通过简单地修改着色器路径名,也仅仅只改了一行代码,就得到了一个基础漫反射光照着色器,并且在带有灯光和阴影的Unity环境中运行。
1.4、另请参阅
在Unity中的大部分内置Cg功能信息位置,都在你的Unity安装目录Unity4\Editor\Data\CGIncludes中,有三个文件夹需要注意,它们分别是UnityCG.cginc、Lighting.cginc、 UnityShaderVariables.cginc。这些就是我们当前的着色器正在使用的文件。
我们将在第9章中更深入地介绍CgInclude文件,使用CgIncludes让你的着色器世界模块化。
第2节.为表面着色器添加属性
在着色器的流水线中它里面的属性是很重要的,因为它们是用来让艺术家来指派纹理和微调着色器的值的方法。属性允许你把GUI的元素公开显示在材质球的Inspector面板上,并且无需使用单独的编辑器去调整。它提供了可视化的方式来调整着色器。
我们看到打开的Shader里,在第3行到第6行被称为Properties代码块。 目前,_MainTex是里面唯一的一个属性。我们再查看名为BasicDiffuse的材质球的属性面板(Inspector面板),我们会发现上边显示了一个纹理GUI元素。GUI元素就是在Shader中的Properties块中创建的。
Unity再一次让这一过程在编写代码和属性的迭代修改所需时间方面变得非常高效。
2.1、如何实现...
通过学习更多的复杂语法来创建自己的属性块,下面看一下它在BasicDiffuse这个着色器中是如何实现的。
- 步骤1:在Shader的属性快中,删除下边这行代码:
_MainTex ("Base (RGB)", 2D) = "white" {}
- 步骤2:在Shader的属性块中加入下边这行代码,保存Shader,完成后返回到Unity编辑器中
_EmissiveColor ("Emissive Color", Color) = (1,1,1,1)
- 步骤3:返回Unity之后,Shader会自动编译,你会看到材质球的检查器(Inspector)面板中会有一个名为Emissive Color的颜色样本,让我们再添加几个属性样本看一看,返回代码,添加下边这行代码到属性块中:
_AmbientColor ("Ambient Color", Color) = (1,1,1,1)
- 步骤4:下面让我们增加一个其它类型的属性,把下面这行代码写到属性块中:
_MySliderValue ("This is a Slider", Range(0,10)) = 2.5
- 步骤5:这次我们创建了一个滑动条,命名为This is a Slider,如下面的截图2.1所示:
图2.1
2.2、实现原理...
每个Unity Shader都有一个内置的结构,供它在内部的代码去来获取它。这个属性块就是它内置结构中的一个功能。这意味着,Shader程序员可以在Shader中快速的直接去创建这些GUI元素。在属性块中声明这些属性可以在shader代码中使用它们,并且还能用来改变值、颜色、和纹理。
图2.2
让我们来看看接下来发生了什么!如图2.2变量名是属性的第一个元素 。当你开始编写一个新的属性时,你需要给这个属性起一个变量名。 这个变量名是用来在你的Shader代码中调取使用的。这样的操作为我们节省了很多时间,因为我们不需要自己来建立这个系统了。
属性中的下一个元素是Inspector GUI Name和属性的类型,它们都是写在括号里的。检查器GUI的名称是,显示在材质球的属性面板中的名称。Type是指此属性的数据类型。我们可以在Shader中定义许多属性的类型。下面的表格描述了我们在着色器中使用的一些变量的类型:
表面着色器的属性类型 | |
Range(min,max) | 这将会创建一个浮点属性,使用的是一个滑动条来调节值域范围。它的值域范围是有最大值和最小值的。 |
Color | 这将会创建一个颜色样本,它会显示在Inspector属性面板中。并且会赋予一个颜色选择器(float,float,float,float) |
2D | 这将会创建一个纹理样本,允许用户将纹理贴图拖拽到着色器中 |
Rect | 这将会创建一个非2次幂的纹理样本,其功能与2D GUI元素相同 |
Cube | 这将会在Inspector面板中创建一个立方体贴图样本,允许用户将立方体贴图拖拽到着色器中 |
Float | 这将会在Inspector面板中创建一个浮点值,但它没有滑动条。Float与Range的区别就在于是否有滑动条。 |
Vector | 这将会创建一个四维的浮点属性,它允许你去创建方向或颜色。 |
属性中的最后一个元素是默认值,这只是将属性默认值设置为你在代码中写的,这个值是可以在外部改变的。 因此在示例图中属已命名的属性默认值为,_AmbientColor的类型是Color,它的默认值是1,1,1,1。它是属于一个四维向量属性,表现形式为,RGBA或者Float4,或者r,g,b,a, = x,y,z,w。一般创建颜色属性的时候,默认都是给一个白色,即(1,1,1,1)。
2.3、另请参阅
Unity文档手册-属性地址:Unity - Manual: Properties block reference in ShaderLab
第3节. 在表面着色器中使用材质属性
现在我们已经创建了一些属性,本节会将展示它们在着色器中是如何调用和进行计算的。这样可以让材质球上的每个属性值调节起来都会有直观的效果展示。
如果我们想在Shader中使用这些属性的变量并在Shader的代码中进行计算。我们必须在Shader代码中设置一些东西,才能通过变量名在shader中去使用它们。
3.1、如何实现...
下面的步骤演示了如何在表面着色器中使用这些属性:
- 步骤1:首先,让我们删除以下代码行,因为我们在本章创建的表面着色器中删除了名为MainTex的属性:
sampler2D _MainTex;
half4 c = tex2D (_MainTex, IN.uv_MainTex);
- 步骤2:接下来,将以下代码添加到Shader中的CGPROGRAM这行代码下面:
float4 _EmissiveColor;
float4 _AmbientColor;float _MySliderValue;
- 步骤3:第2步完成后,我们现在就可以使用Shader中的属性值了。 接下来让我们把_EmissiveColor属性值和_AmbientColor属性值相加,将相加的结果和_MySliderValue属性值做一个指数幂(Pow)的计算,(意为:_EmissiveColor + _AmbientColor的_MySliderValue次方)。并将它们的结果c 赋给o.Albedo代码,从而将我们计算的结果呈现出来。下面让我们需要把以下代码添加到Shader中的surf函数里:
void surf (Input IN, inout SurfaceOutput o)
{//定义名字为c的四维变量
float4 c;//将得到的计算结果,赋予给c
c = pow((_EmissiveColor + _AmbientColor), _MySliderValue);//通过 o.Albedo,将c的值输出
o.Albedo = c.rgb;
o.Alpha = c.a;
}
- 步骤4:保存Shader代码,回到Unity编辑器中,调节材质球中的Emissive Color、Ambient Color、和This is a Slider这几个属性,就可以看见计算效果(我们表面着色器里的代码是非常的干净整洁的 对不对?)。完整代码如下所示 :
Shader "CookbookShaders/BasicDiffuse"
{
Properties
{
//我们在属性块中定义了属性
_EmissiveColor ("Emissive Color", Color) = (1,1,1,1)
_AmbientColor ("Ambient Color", Color) = (1,1,1,1)
_MySliderValue ("This is a Slider", Range(0,10)) = 2.5
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 200
CGPROGRAM
#pragma surface surf Lambert
//我们需要在CGPROGRAM内部声明属性变量类型,这样我们就可以从属性块中访问到它的值。
float4 _EmissiveColor;
float4 _AmbientColor;
float _MySliderValue;
struct Input
{
float2 uv_MainTex;
};
void surf (Input IN, inout SurfaceOutput o)
{
//然后我们可以在着色器中使用它们的属性值
float4 c;
c = pow((_EmissiveColor + _AmbientColor), _MySliderValue);
o.Albedo = c.rgb;
o.Alpha = c.a;
}
ENDCG
}
FallBack "Diffuse"
}
【pow(arg1, arg2)是一个内置函数,参数1 (arg1)是底数,参数2 (arg2)是我们想要取幂的值也就是指数。 要了解关于 pow() 函数的更多信息,请查看Cg教程。 这是一个很棒的免费资源,你可以使用它来学习更多关于着色的知识,并获得Cg着色语言中所有可用功能的术语表: The Cg Tutorial - Appendix E. Cg Standard Library Functions】
下面的截图演示了,在材质球属性面板中,调节了对应的属性值即颜色和饱和度,就可以得到下图的结果:
3.2、实现原理...
当你在属性块中声明一个新属性时,你就为着色器提供了一种从材质球的Inspector面板中检索调整值的方法。 此值存储在属性的变量名部分中。 在这种情况下,_AmbientColor, _EmissiveColor和_MySliderValue就是我们存储调整值的变量。 为了让你能够使用SubShader{}块中的值,你需要创建三个与属性变量名称相同的新变量。 这会自动在两者之间建立一个链接,这样他们就知道他们必须使用相同的数据。 此外,它声明了我们想要存储在子着色器变量中的数据类型,这将在我们在后面的章节中优化着色器时派上用场。
一旦你创建了subshader变量,你就可以使用surf()函数中的值了。 在这种情况下,我们想要将_EmissiveColor和_AmbientColor变量一起添加,并将其设置为材质检查器选项卡中_MySliderValue变量的幂。
我们现在为下一节的将要讲解的漫反射光照模型奠定了基础。
第4节. 创建自定义漫反射光照模型
Unity内置的所有光照功能都是很不错的,但你会很快发现内置的光照功能不会满足你所有的需求,你会想要创建更多的可自定义的光照模型。从以往经验来看,一般情况下我们从来都不会在项目中只使用Unity的内置光照功能的。我们一般都会针对项目的不同需求去创建定制相对应的光照模型。这样很多操作都会变得很灵活,比如可以产生边缘照明特效、会有更多基于立方体贴图的光照类型、或者甚至可以控制你的Shader对游戏玩法做出回应。比如使用Shader可以去做类似于力学相关的事情,像是控制立场、模拟物体之间的运动。
本小节主要专注于创建自己的自定义漫反射光照模型,我们可以使用它来修改和创建许多不同的效果。
4.1、如何实现...
使用我们在上一节中创建的基本漫射Shader,让我们通过以下步骤再次修改它:
- 步骤1:让我们将#pragma语句修改为以下代码:
#pragma surface surf BasicDiffuse
- 步骤2:将以下代码添加到子着色器Subshader中:
inline float4 LightingBasicDiffuse (SurfaceOutput s, fixed3 lightDir, fixed atten)
{
float difLight = max(0, dot (s.Normal, lightDir));
float4 col;
col.rgb = s.Albedo * _LightColor0.rgb * (difLight * atten * 2);
col.a = s.Alpha;
return col;
}
- 步骤3:保存Shader,回到Unity中。Shader将会自动编译,我们会看到在材质球属性面板上是没有任何变化的。上边的两个步骤主要是删除着色器和内置的Unity漫反射光照模型的联系,然后替换成我们自己创建的自定义光照模型。完整代码如下:
Shader "CookbookShaders/BasicDiffuse"
{
Properties
{
_EmissiveColor ("Emissive Color", Color) = (1,1,1,1)
_AmbientColor ("Ambient Color", Color) = (1,1,1,1)
_MySliderValue ("This is a Slider", Range(0,10)) = 2.5
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 200
CGPROGRAM
//#pragma surface surf Lambert //此行代码删除掉
#pragma surface surf BasicDiffuse
float4 _EmissiveColor;
float4 _AmbientColor;
float _MySliderValue;
//加入下列8行代码
inline float4 LightingBasicDiffuse (SurfaceOutput s, fixed3 lightDir, fixed atten)
{
float difLight = max(0, dot (s.Normal, lightDir));
float4 col;
col.rgb = s.Albedo * _LightColor0.rgb * (difLight * atten * 2);
col.a = s.Alpha;
return col;
}
struct Input
{
float2 uv_MainTex;
};
void surf (Input IN, inout SurfaceOutput o)
{
float4 c;
c = pow((_EmissiveColor + _AmbientColor), _MySliderValue);
o.Albedo = c.rgb;
o.Alpha = c.a;
}
ENDCG
}
FallBack "Diffuse"
}
4.2、实现原理...
为了便于理解,我们把着色器中的关于光照的计算逐个做个拆解,来了解一下它每项在做什么:
- #pragma surface指令告诉Shader使用哪个光照模型进行计算。 当创建一个默认表面着色器时,兰伯特光照(Lambert Lighting)是它里面默认的光照模型,主要是因为在Lighting.cginc 文件中把Lambert定义为了默认光照模型 。 而现在我们告诉了Shader把光照模型改成名为BasicDiffuse的光照模型。
- 创建一个新的光照模型是通过声明一个新的光照模型函数来完成的。 创建完成之后,只需将函数的名称替换成新的函数名称即可。 例如,LightingName变成Lighting<Your selected Name>。 你可以使用以下三种类型的光照模型功能:
half4 LightingName (SurfaceOutput s, half3 lightDir, half atten){}
在前向渲染中,不使用视方向的计算。
half4 LightingName (SurfaceOutput s, half3 lightDir, half3 viewDir, half atten){}
在前向渲染中,使用视方向的计算 。
half4 LightingName_PrePass (SurfaceOutput s, half4 light){}
当项目使用延迟渲染时,将使用此函数。
- 点积函数(Dot Function)是Cg语言中另一个内置的数学函数。 我们可以用它来比较空间中两个向量的方向。 点积可以判断两个向量是否平行或垂直。对于两个向量,通过点积(Dot)你会得到一个范围在-1到1内的浮点值;如果值为-1就是背对着你,值为1就是正面朝向你,0是完全垂直于你。
【我们使用了点积(Dot)或者内积(inner product)来判断了法线向量(N)和光向量(L)这两个向量之间夹角的度数,并将结果进行了归一化。 向量N和向量L之间的夹角越小,点积值就越大,表面接收到的入射光也就越多。 参考:The Cg Tutorial - Chapter 5. Lighting】
- 为了完成漫反射的计算,我们需要将它乘以一些数据,这些数据是由与Unity和SurfaceOutput结构体提供给我们的。为此,我们需要将 s.Albedo(来自我们的surf函数)与传入的_LightColor0相乘(_LightColor0是Unity提供的)。然后再乘以(difLight * attenten)的值。最后将该值作为颜色返回。代码如下所示:
inline float4 LightingBasicDiffuse (SurfaceOutput s, fixed3lightDir, fixed atten){float difLight = max(0, dot (s.Normal, lightDir));float4 col;col.rgb = s.Albedo * _LightColor0.rgb * (difLight * atten * 2);col.a = s.Alpha;return col;}
下面的图4.1展示了我们基本漫射着色器的结果:
图4.1
4.3、更多内容...
通过使用内置的Cg函数max,我们可以从点积的函数值到它的返回的值进行限制。 max函数有两个参数,max(arg1, arg2)。 在我们着色器中使用它目的是,用来确保我们计算的漫射的值需要限制在0和点积的最大值之间。 这样我们就永远不会得到一个低于0的值,尤其是-1,如果是-1这个值,它将会在我们的着色器中绘制一块非常黑的区域,这样后边就无法展示一些其它的数学计算在Shader中的效果。
在Cg函数库中也有saturate函数。 它的作用也是用来限制值域范围的,和max函数很像。 max()和saturate()之间的唯一区别是,您只需将想要限制的一维值放到saturate中即可。但 max函数是需要接受两个参数,并返回两者之间的最大值。
4.4、另请参阅
你可以在这里找到更多关于表面着色器照明模型函数参数的信息 :http://docs.unity3d.com/Documentation/Components/SLSurfaceShaderLighting.html
第5节. 创建半兰伯特照明模型
Half Lambert是Valve创造的一种可获取光照的技术,用于在低光区域显示物体表面。 它基本上照亮了物体的整个材质的表面。其实半兰伯特只是在兰伯特的基础上稍微调整了一下。
【“半兰伯特”照明是最初在半条命(Half-Life - Valve Developer Community)中开发的一种技术。 它的设计是为了防止物体的后部失去形状而看起来过于平坦的技术。 半兰伯特是一种完全非物理的技术,只提供纯粹的视觉效果。 它是一个经验型模型。 参考: Half Lambert - Valve Developer Community】
3.1、如何实现...
复制一份我们在上一节中创建的Shader,让我们按照下面的步骤来更改漫反射的计算:
- 步骤1:将漫反射的结果乘以0.5再加上0.5。 代码如下所示:
inline float4 LightingBasicDiffuse (SurfaceOutput s, fixed3 lightDir, fixed atten)
{
float difLight = max(0, dot (s.Normal, lightDir));
float hlambert = difLight * 0.5 + 0.5;
float4 col;
col.rgb = s.Albedo * _LightColor0.rgb * (hlambert * atten * 2);
col.a = s.Alpha;
return col;
}
下面的截图展示了半兰伯特技术的实现结果如下图5.1 :
图 5.1
3.2、实现原理...
半兰伯特(Half-lambert)技术的工作原理是基于兰伯特光照模型基础上的,也就是上文所说的漫反射(Diffuse)的计算。半兰伯特获取的值其实是漫反射的数值范围,把它分成了两半,然后再加上0.5。意思就是你有一个值是1,然后把1分成了两半,之后再加上0.5。你就又得到了1这个值。如果你有一个值是0,操作同上的方法,你最后就得到了0.5的值。因此半兰伯特的原理就是,把我们上文得到的范围是0到1的Diffuse值,把这个0到1的值重新映射到0.5到1.0的范围内。
下面是漫反射值的函数映射图5.2,表示半兰伯特计算的结果:
图5.2
第6节. 创建一个渐变纹理来控制漫反射的着色
在你的Shader编写工具箱中,另一个很哇塞的工具就是使用渐变纹理来驱动漫射光照模型的颜色。 这个方法允许你重新塑造表面的颜色,用它来模拟更多的反射光或更高级的照明程序的效果。 这种技术在卡通风格的游戏中是很常见的,在类似于卡通的渲染风格游戏中大部分的美术效果是由艺术家调节材质球上的属性设置来实现的。而不是需要使用很多精确的基于物理的光照模型。
在《军团要塞2》中这项技术是很受欢迎的,Valve在其中想出了一种独特的方法来照亮他们的角色。 他们就这个问题发表了一份非常受欢迎的白皮书,你一定要读一读。
关于《军团要塞2》光照和阴影的 —— Valve白皮书地址如下:
地址1:http://www.valvesoftware.com/publications/2007/NPAR07_IllustrativeRenderingInTeamFortress2.pdf
地址2:【免费】Valve白皮书-《军团要塞2》光照和阴影的实现资源-CSDN文库
6.1、准备工作
开始本节内容前,需要在绘图软件中创建一个渐变纹理,这里我们在是Photoshop软件中创建这个渐变纹理如图6.1:
图6.1
.
6.2、如何实现...
让我们通过输入以下代码来实现:
- 步骤1:在Properties属性面板中加入下面这行新代码:
//采样渐变贴图
_RampTex("RampTex", 2D) = "whit"{}
- 步骤2:在Subshader中定义同名的属性变量:
sampler2D _RampTex;
- 步骤3:只需修改光照函数,使其包含以下新代码:
inline float4 LightingBasicDiffuse (SurfaceOutput s, fixed3 lightDir, fixed atten)
{
float difLight = max(0, dot (s.Normal, lightDir));
float hlambert = difLight * 0.5 + 0.5;
float3 ramp = tex2D(_RampTex,float2(hlambert,hlambert)).rgb;
float4 col;
col.rgb = s.Albedo * _LightColor0.rgb * (ramp);// (hlambert * atten * 2);
col.a = s.Alpha;
return col;
}
- 步骤4:保存Shader,返回Unity中。把我们在Photoshop软件中创建的渐变纹理赋予到材质球上。如下图6.2:
图6.2
- 完整代码如下:
Shader "CookbookShaders/BasicDiffuse"
{
Properties
{
_EmissiveColor ("Emissive Color", Color) = (1,1,1,1)
_AmbientColor ("Ambient Color", Color) = (1,1,1,1)
_MySliderValue ("This is a Slider", Range(0,10)) = 2.5
_RampTex("RampTex", 2D) = "whit"{}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 200
CGPROGRAM
#pragma surface surf BasicDiffuse
float4 _EmissiveColor;
float4 _AmbientColor;
float _MySliderValue;
sampler2D _RampTex;
inline float4 LightingBasicDiffuse (SurfaceOutput s, fixed3 lightDir, fixed atten)
{
float difLight = max(0, dot (s.Normal, lightDir));
float hlambert = difLight * 0.5 + 0.5;
float3 ramp = tex2D(_RampTex,float2(hlambert,hlambert)).rgb;
float4 col;
col.rgb = s.Albedo * _LightColor0.rgb * (ramp);// (hlambert * atten * 2);
col.a = s.Alpha;
return col;
}
struct Input
{
float2 uv_MainTex;
};
void surf (Input IN, inout SurfaceOutput o)
{
float4 c;
c = pow((_EmissiveColor + _AmbientColor), _MySliderValue);
o.Albedo = c.rgb;
o.Alpha = c.a;
}
ENDCG
}
FallBack "Diffuse"
}
运行代码后,渲染效果如图6.3所示:
图6.3
3.2、实现原理...
float3 ramp = tex2D(_RampTex,float2(hlambert,hlambert)).rgb;
这行代码返回的是一组颜色,也可以说成是一个三维的向量float3,或者是 r, g, b,它们的性质都是一样的。这些颜色是Cg语言里面名为tex2D函数来产生的。 tex2D()函数里面需要传输两个参数。 传输的第一个参数是我们在Properties代码块中定义的纹理属性。 传输的第二个参数是纹理模型的UV坐标。
在这项技术中,我们不使用顶点着色器中传输的UV坐标,而是使用半兰伯特(Half Lambert)的值来作为UV坐标对渐变贴图来进行采样。这个渐变纹理贴图(或者说这个渐变的颜色)会根据光的方向上的计算,最终会把整个物体都包裹起来。
我们将半兰伯特(Half Lambert)通过重映射的方法,并传递给了一个二维的向量float2(),以此来来创建纹理的Uv坐标。当我们把hLambert的变量值设为0时,tex2D函数就会在UV坐标值(0,0)处查找像素值。在渐变贴图中,坐标(0,0)的位置是淡淡的桃色。当我们把hLambert的变量值设为1时,tex2D函数就会在UV坐标值(1,1)处查找像素。在渐变贴图中,坐标(1,1)的位置是白色。
现在,艺术家就可以通过绘制渐变贴图对物体表面的光线进行一些自定义的控制了。这就是为什么这种技术在“需要更多说明性外观”的项目中更常见的原因,比如卡通渲染的项目。
第7节. 使用2D渐变纹理模拟BRDF
我们通过使用由照明功能提供的视方向(viewDir),以及进一步采用的ramp diffuse(渐变纹理)方法,可以使我们的光照达到更高级的视觉外观。 通过利用视方向,我们将能够模拟边缘照明的效果。
让我们来看一下ramp diffuse这项技术,它只是使用了一个值放到了渐变纹理(ramp texture)的Uv坐标中进行查找。这说明了它是一个标准线性类型的灯光效果。在这个方法中,我们利用了一个额外的参数,即视方向(view direction)改变我们的光照功能。
视方向指的就是观察者本身,或者说是摄像机(摄像机在某种定义上也是属于观察者)。它是一个指向一个方向的向量,并且可以与法线(normal)和光方向(light direction)结合使用。这个观察向量(view vector)将为我们提供创建更高级纹理查找的方法。
在Cg行业中,这项技术通常被称为BRDF效果(BRDF effect)。 BRDF指的是双向反射分布函数(bidirectional reflectance distribution function)。 这个词比较拗口,简单的理解就是光在一个不透明的表面上从视方向(view direction)和光照方向(light direction)这两者的反射方式。为了可以看到这个BRDF着色器的效果,让我们来继续调整场景以及编写我们的着色器。
7.1、准备工作
在开始之前,我们这次将需要一个更加绚丽的渐变纹理。 这个渐变纹理需要具备两个维度的渐变。
- 1. 创建一个大小为512 x 512的新纹理。
- 2. 创建一个渐变,使用对角线的方式,从图片的左下角开始,到图片的右上角。
- 3. 接下来我们从图像的左上角开始到图像的中间再创建个渐变,。
- 4. 最后,从图像的右下角到图像中间再创建一个渐变。 最终会得到如下图7.1 所示的纹理:
图7.1
7.2、如何实现...
让我们按照接下来的几个步骤去实现这项技术。复制上一节完成的Shader,作为本节的起点:
- 步骤1:首先,我们需要改变我们的光照函数,里面需要增加Unity提供的viewDir变量,增加它的目的是用来获取当前场景中相机的观察方向(view direction),相机类似于我们的眼睛,始终保持观察着场景中的物体。下面我们来修改光照函数,代码如下所示:
inline float4 LightingBasicDiffuse (SurfaceOutput s, fixed3 lightDir, half3 viewDir, fixed atten)
{
float difLight = max(0, dot (s.Normal, lightDir));
float hlambert = difLight * 0.5 + 0.5;
float3 ramp = tex2D(_RampTex,float2(hlambert,hlambert)).rgb;
float4 col;
col.rgb = s.Albedo * _LightColor0.rgb * (ramp);// (hlambert * atten * 2);
col.a = s.Alpha;
return col;
}
- 步骤2:然后我们需要计算视方向(viewDir)和表面法线(normal)的点积(如下面的代码所示)。 这会产生一个衰减类型的效果,我们可以用它(rimLight)来驱动我们的BRDF纹理。
inline float4 LightingBasicDiffuse (SurfaceOutput s, fixed3 lightDir, half3 viewDir, fixed atten)
{
float difLight = max(0, dot (s.Normal, lightDir));
float rimLight = dot(s.Normal, viewDir);
float hlambert = difLight * 0.5 + 0.5;
float3 ramp = tex2D(_RampTex,float2(hlambert,hlambert)).rgb;
float4 col;
col.rgb = s.Albedo * _LightColor0.rgb * (ramp);// (hlambert * atten * 2);
col.a = s.Alpha;
return col;
}
- 步骤3:完成这个操作需要将点积结果放入到一个二维向量float2()中,并将这个二维向量作为tex2D()函数的第二个输入参数,即它的Uv坐标。 把你的光照函数修改为以下代码:
inline float4 LightingBasicDiffuse (SurfaceOutput s, fixed3 lightDir, half3 viewDir, fixed atten)
{
float difLight = max(0, dot (s.Normal, lightDir));
float rimLight = dot(s.Normal, viewDir);
float hlambert = difLight * 0.5 + 0.5;
float3 ramp = tex2D(_RampTex,float2(hlambert,rimLight)).rgb;
float4 col;
col.rgb = s.Albedo * _LightColor0.rgb * (ramp);// (hlambert * atten * 2);
col.a = s.Alpha;
return col;
}
- 步骤4:保存Shader返回Unity编辑器中。 在Unity中把新做的BRDF贴图赋予到材质球上。把之前的渐变纹理用新的BRDF纹理替换掉。这时你会看到你的光照现在包含了两个边缘光(rim light)的类型效果:一个是在模型的底部,一个是在顶部。 完整代码如下所示:
Shader "CookbookShaders/FakedBRDF"
{
Properties
{
_EmissiveColor ("Emissive Color", Color) = (1,1,1,1)
_AmbientColor ("Ambient Color", Color) = (1,1,1,1)
_MySliderValue ("This is a Slider", Range(0,10)) = 2.5
_RampTex("RampTex", 2D) = "whit"{}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 200
CGPROGRAM
#pragma surface surf BasicDiffuse
float4 _EmissiveColor;
float4 _AmbientColor;
float _MySliderValue;
sampler2D _RampTex;
inline float4 LightingBasicDiffuse (SurfaceOutput s, fixed3 lightDir, half3 viewDir, fixed atten)
{
float difLight = max(0, dot (s.Normal, lightDir));
float rimLight = dot(s.Normal, viewDir);
float hlambert = difLight * 0.5 + 0.5;
float3 ramp = tex2D(_RampTex,float2(hlambert,rimLight)).rgb;
float4 col;
col.rgb = s.Albedo * _LightColor0.rgb * (ramp);
col.a = s.Alpha;
return col;
}
struct Input
{
float2 uv_MainTex;
};
void surf (Input IN, inout SurfaceOutput o)
{
float4 c;
c = pow((_EmissiveColor + _AmbientColor), _MySliderValue);
o.Albedo = c.rgb;
o.Alpha = c.a;
}
ENDCG
}
FallBack "Diffuse"
}
下图7.2展示了使用BRDF渐变纹理来驱动整体漫反射颜色的结果。 这种技术对于制作团队来说非常有用,因为它可以让美术小伙伴轻松地在Photoshop中制作并随时更新纹理贴图,而不是在游戏中调整灯光。
图7.2
7.3、实现原理...
当使用视方向参数时,我们能够创建一个非常简单的衰减类型效果。 您可以使用此参数去创建很多不同类型的效果。比如:创建一个气泡可以使用它来模拟气泡中间的透明度、边缘光效果、屏蔽效果、甚至是卡通轮廓(描边)效果。如下 图7.3所示:
图7.3
前面的图像展示了视方向(viewDir)与表面法线(normal)的点积。 考虑一下,如果你已经去查看到了视方向和表面法线所产生的点积值。
在这种情况下,我们使用它作为一个组件在BRDF渐变纹理中去进行查找它。 由于漫反射光(diffLight)的计算和边缘光(rimLight)的计算结果都是在0到1的线性范围,我们可以使用这两个范围来选择渐变纹理的不同区域。 如下图7.4所示:
图7.4:图中箭头展示了着色器代码内部发生的事情,以及它是如何拾取的表面上的颜色
所以这里的关键是理解我们从点积函数中得到的值,以及我们是如何巧妙的处理纹理的。在光照函数的内部,将它们包裹在一个表面上,用来模拟更复杂的照明效果。
7.4、另请参阅
请参考Polycount BRDF Map,链接为 :BDRF map - polycount
这本书可以让你学习到如何使用着色器和屏幕特效让你的Unity工程拥有震撼的渲染画面。
作者:Kenny Lammers