Unity Shaders and Effets Cookbook
《着色器和屏幕特效制作攻略》
这本书可以让你学习到如何使用着色器和屏幕特效让你的Unity工程拥有震撼的渲染画面。
——Kenny Lammers
I like this book!
第三章:镜面反射让你的游戏闪光
Making Your Game Shine with Specular
我们每个人都喜欢类似于Gears of War 和 Call of Duty 这样的游戏,但是什么让这些游戏在视觉上如此引人注目而且还非常逼真呢?嗯,这确实是需要很多因素一起影响的作用,但这些游戏其实在 Shader管线中采用的最关键的元素之一是:不同类型的高光反射(Specular)。本章将向您介绍 Specular 的基础知识,并演示当今 3A 游戏中的 Shader 管线里使用的一些技巧。
在本章中,将会学习到以下内容:
介绍
在对象表面上的镜面反射度只是描述它的光泽度。这些类型的效果在 Shader 世界中通常称为视方向相关的特效。这是因为,为了在着色器中实现逼真的镜面反射效果,您需要把摄像机或观察者的视方向计算进来。尽管 Specular 需要一个额外的组件来实现物体材质的真实度,这个组件就会是光线方向(Light Direction)。通过组合这两个方向或向量,我们最终会在对象表面得到一个聚光区或高光,位于视图方向和光线方向之间的中间位置。这个中间方向称为半角向量(Half Direction),是我们将在本章中探讨的新内容,同时自定义我们的 高光(Specular) 效果以模拟金属和布料材质的表面。
第1节. 使用Unity3D内置的镜面反射
Unity 已经为我们提供了一个可用于着色器的 Specular 函数。它被称为 BlinnPhong Specular 光照模型。它在众多的高光(Specular)类型中是最基础、最高效的,即使在今天,仍然有很多游戏在使用这项技术。由于它已经内置于 Unity Surface Shader 语言中,因此我们认为最好先从创建内置的开始,然后在此基础上进行构建。在 Unity 参考手册中你也可以找到一个示例,但我们会更深入的来介绍它,并解释数据的来源以及为什么它以这种方式工作。这会有利于对你创建 Speculular 时打下良好的基础,也便于我们在本章的后边几节课程中创建它们。
1.1、准备工作
让我们开始执行以下操作:
- 创建一个新的 Shader 并为其命名。
- 创建一个新材质球,为其命名,并将新 Shader 分配给它的 shader 属性(就是把shader直接托到材质球上)。
- 然后创建一个球体,并将其大致放置在世界中心。
- 最后,让我们创建一盏平行光,这样就会有一些光线会照射到我们物体上。在Unity中设置完成之后,你的会有一个类似于下图1.1的场景:
图1.1
1.2、如何实现...
- 步骤1:首先将以下属性添加到 Shader 的 Properties 块中:
Properties
{
_MainTex("_MainTex", 2D) = "white"{}
_MainTint("Diffuse Tint", color) = (1,1,1,1)
_SpecColor("Specular Color", color) = (1,1,1,1)
_SpecPower("Specular Power", Range(0,1)) = 0.5
}
- 步骤2:然后,我们需要将变量添加到 CGPROGRAM 块中,以便我们可以在 Shader 的 CGPROGRAM 代码块中使用属性中的新数据。请注意,我们不需要将 _SpecColor 属性声明为变量。因为 Unity 已经在内置的 Specular 模型中创建了这个变量。我们只需要在 Properties 代码块中声明它即可,Unity会把数据自动传递给 surf() 函数。
// 将属性链接到CG程序
sampler2D _MainTex;
half4 _MainTint;
float _SpecPower;
- 步骤3:现在需要让Shader去告诉哪个光照模型是我们需要的模型。此前我们已经了解了兰伯特(Lambert)光照模型以及如何来制作自定义的光照模型,但我们还没有看到 BlinnPhong 光照模型(BlinnPhong lighting model)。因此,让我们将 BlinnPhong 添加到我们的 #pragma 语句中,如下代码所示:
CGPROGRAM
#pragma surface surf BlinnPhong
- 步骤4:然后,我们需要修改 surf() 函数,使其如下代码所示:
void surf (Input IN, inout SurfaceOutput o)
{
float4 c = tex2D (_MainTex, IN.uv_MainTex) * _MainTint;
o.Specular = _SpecPower;
o.Gloss = 1.0;
o.Albedo = c.rgb;
o.Alpha = c.a;
}
ENDCG
- 完整代码:
Shader "CookbookShaders/built-in BlinnPhong"
{
Properties
{
_MainTex("_MainTex", 2D) = "white"{}
_MainTint("Diffuse Tint", color) = (1,1,1,1)
_SpecColor("Specular Color", color) = (1,1,1,1)
_SpecPower("Specular Power", Range(0,1)) = 0.5
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 200
CGPROGRAM
//#pragma surface surf Lambert
#pragma surface surf BlinnPhong
// 将属性链接到CG程序
sampler2D _MainTex;
half4 _MainTint;
float _SpecPower;
// 确保在struct中获得纹理的uv
struct Input
{
float2 uv_MainTex;
};
void surf (Input IN, inout SurfaceOutput o)
{
float4 c = tex2D (_MainTex, IN.uv_MainTex) * _MainTint;
o.Specular = _SpecPower;
o.Gloss = 1.0;
o.Albedo = c.rgb;
o.Alpha = c.a;
}
ENDCG
}
FallBack "Diffuse"
}
1.3、实现原理...
当你在对着色器进行原型设计时,这个基础的 Specular 是一个很好的起点,因为它可以让你在编写 Shader 的核心功能方面完成很多的工作,不需要去担心基础光照函数。
Unity 为我们提供了一个光照模型,该模型已经为我们创建好了镜面反射光照的任务。如果去查看一下 Unity 安装目录里的 Data 文件夹下的 UnityCG.cginc 文件,你会看到里面的 Lambert 和 BlinnPhong 光照模型,它们是可以直接拿来使用的。当使用 #pragma surface surf BlinnPhong 编译着色器时,其实就是在告诉着色器去使用 UnityCG.cginc 文件中的 BlinnPhong 光照函数,这样我们就不必一遍又一遍重复的编写该代码了。如果你写的 Shader 没有出现报错,那么就会看到类似于下 图1.2 的结果:
图1.2
第2节. 创建 Phong 镜面反射
其实最基础的且对性能最友好的高光类型是 Phong Specular 效果。它计算的是从表面反射的光线方向与观察者的视方向的一个比较。它是一种非常常见的镜面反射模型,从游戏到电影,在许多应用程序中得到广泛的应用。虽然这个高光反射相比于大多数精准的光照模型来说表现得并不是非常真实,但它提供了一个很好的近似值,在大多数情况下表现良好。此外,如果您的对象离摄像机较远,并且不需要非常精确的镜面反射,这个高光的效果就是一个很好的方法。
在本节中,我们会介绍如何实现逐顶点(per vertex)渲染版本,并了解如何使用表面着色器的 Input 结构中的一些新参数实现逐像素(per pixel)渲染版本。我们会从中看到差异,并且会进行讨论一下何时以及为什么,在不同情况下使用这两种不同的版本来实现。
2.1、准备工作
- 创建一个新的着色器、材质和对象,并为它们指定适当的名称,以便以后可以找到它们。
- 最后,将 Shader 附加到 Material(材质),并将 Material (材质) 附加到对象。要完成新场景,请创建一个新的方向光,以便我们可以在编码时看到 Specular 效果。
2.2、如何实现...
- 步骤1:到这里你可能会看到一个模式,我们总是喜欢从编写 Shader 的最基本部分开始即:创建属性。因此,让我们将以下属性添加到 Shader 中:
Properties
{
_MainTint("Diffuse Tint", color) = (1,1,1,1)
_MainTex("_MainTex", 2D) = "white"{}
_SpecularColor("Specular Color", color) = (1,1,1,1)
_SpecPower("Specular Power", Range(0,30)) = 1
}
- 步骤2:然后,将相应的变量添加到 SubShader 块内的 CGPROGRAM 块中。
// 将属性链接到CG程序
sampler2D _MainTex;
half4 _SpecularColor;
half4 _MainTint;
float _SpecPower;
- 步骤3:现在我们开始添加自定义光照模型,以便我们计算自己的 Phong Specular。将以下代码添加到 Shader 的 SubShader() 函数中。如果不理解它们的含义的话,请不要担心,我们将在下一个知识点中介绍每一行代码:
// 添加以下代码
inline half4 LightingPhong (SurfaceOutput s, half3 lightDir, half3 viewDir, half atten)
{
float diff = dot(s.Normal, lightDir);
float3 reflectionVector = normalize(2.0 * s.Normal * diff - lightDir);
float spec = pow(max(0, dot(reflectionVector, viewDir)), _SpecPower);
float3 finalSpec = _SpecularColor.rgb * spec;
half4 c;
c.rgb = (s.Albedo * _LightColor0.rgb * diff) + (_LightColor0.rgb * finalSpec);
c.a = 1.0;
return c;
}
- 步骤4:最后,我们需要告诉 CGPROGRAM 代码块,它需要使用我们的自定义光照功能,而不是内置功能。为此,我们将 #pragma 语句更改为以下内容:
CGPROGRAM
#pragma surface surf Phong
- 完整代码:
Shader "CookbookShaders/Phong Specular type"
{
Properties
{
// 添加以下属性
_MainTint("Diffuse Tint", color) = (0.5,0.5,0.5,0.5)
_MainTex("_MainTex", 2D) = "white"{}
_SpecularColor("Specular Color", color) = (1,1,1,1)
_SpecPower("Specular Power", Range(0.001,30)) = 1
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 200
CGPROGRAM
#pragma surface surf Phong
// 将属性链接到CG程序
sampler2D _MainTex;
half4 _SpecularColor;
half4 _MainTint;
float _SpecPower;
// 确保在struct中获得纹理的uv
struct Input
{
float2 uv_MainTex;
};
// 添加以下代码
inline half4 LightingPhong (SurfaceOutput s, half3 lightDir, half3 viewDir, half atten)
{
float diff = dot(s.Normal, lightDir);
float3 reflectionVector = normalize(2.0 * s.Normal * diff - lightDir);
float spec = pow(max(0, dot(reflectionVector, viewDir)), _SpecPower);
float3 finalSpec = _SpecularColor.rgb * spec;
half4 c;
c.rgb = (s.Albedo * _LightColor0.rgb * diff) + (_LightColor0.rgb * finalSpec);
c.a = 1.0;
return c;
}
void surf (Input IN, inout SurfaceOutput o)
{
float4 c = tex2D (_MainTex, IN.uv_MainTex) * _MainTint;
o.Albedo = c.rgb;
o.Alpha = c.a;
}
ENDCG
}
FallBack "Diffuse"
}
下图2.1展示了,使用自定义的反射向量实现了我们自定义的Phong光照模型的结果:
图2.1
2.3、实现原理...
让我们来单独分解一下这个光照函数,因为此时您应该非常熟悉 Shader 的其余部分。
我们从提供给我们的视方向(View Direction)光照函数开始。请记住,Unity 已经为我们提供了一组可以使用的光照函数,但为了正确的使用它们,你传输的参数必须和它们提供的参数要保持相同。请参阅下边的表格,或转到此链接查看:http://docs.unity3d.com/Documentation/Components/SLSurfaceShaderLighting.html :
不使用视方向 | half4 Lighting Name You choose (SurfaceOutput s, half3 lightDir, half atten); |
使用视方向 | half4 Lighting Name You choose (SurfaceOutput s, half3 lightDir, half3 viewDir, half atten); |
在我们的例子中,我们所做的正是 Specular Shader,因此我们需要使用具备视方向的光照函数结构。所以,我们需要写成以下形式:
CGPROGRAM
#pragma surface surf Phong
inline half4 LightingPhong (SurfaceOutput s, half3 lightDir, half3 viewDir, half atten)
{
}
这将会告诉 Shader 我们想要创建一个需要使用视方向的 Shader。在声明你的光照函数时要始终保持你的光照函数名字和#pragma 语句中声明的光照函数名称相同。否则 Unity 将无法找到光照模型。
然后,光照函数通常先使用光线方向和顶点法线的点积来声明 Diffuse 组件。当模型上的法线朝向光源时,我们会得到 1 这个值;当它背对光源方向时,那么该值为 -1。
接下来,我们使用顶点法线来计算反射向量,我们使用得到的diff(法线点积光方向的结果)值将其缩放 2.0 ,然后从中减去光线方向。这就产生了法线向光弯曲的效果;因此,当顶点法线的指向远离光源时,它会始终朝向光源。为了更直观的表达以上观点,请参阅以下屏幕截图。生成此调试效果的脚本包含在本书的支持页面:www.packtpub.com/support。
图2.2
然后我们剩下要做的就是创建高光的最终规范的值和颜色。为此,我们将反射向量(Refection Vector)和视方向(View Direction)进行点积操作,并将点积的结果设置为 _SpecPower 的幂。最后,我们只需将 _SpecularColor.rgb 值乘以 spec 值,即可获得最终的 Specular 高光。
以下截图展示了 Phong Specular 计算的最终结果,如下图2.3所示:
图2.3
第3节. 创建 BlinnPhong 镜面反射
Blinn 是另一种计算镜面反射更高效的方法。它是通过从视方向和光线方向获取的半向量来完成的。Jim Blinn 这个人把它带入了Cg 世界。它是由一个名叫 Jim Blinn 的人带入 Cg 世界的。他发现,只获取 half 向量比计算我们自己的反射向量要高效得多。它减少了代码及其处理的时间。如果你去查看 UnityCG.cginc 这个文件里面包含的内置 BlinnPhong 光照模型,它使用的也是半角向量,因此把它叫做 BlinnPhong。它只是完整 Phong 计算的更简单版本。
3.1、准备工作
- 这一次,我们不要创建一个全新的场景,而是使用我们之前的对象和场景即可,我们只需创建一个新的着色器和材质球,并将它们命名为 BlinnPhong。
- 有了新的 Shader 后,双击它,这样我们就可以开始编辑我们的 Shader 了。
3.2、如何实现...
- 步骤1. 首先,我们需要将自己的属性添加到 Properties 块中,以便我们可以控制 Specular 高光的外观。
Properties
{
_MainTint("Diffuse Tint", color) = (1,1,1,1)
_MainTex("Main Map", 2D) = "white"{}
_SpecularColor("Specular Color", color) = (1,1,1,1)
_SpecPower("Specular Power", Range(0.1, 60)) = 3
}
- 步骤2. 然后,需要在 CGPROGRAM 语义块中创建了相应的变量,以便我们可以在子 Subshader 内访问 Properties 块中的数据。
// 将属性链接到CG程序
half4 _MainTint;
sampler2D _MainTex;
half4 _SpecularColor;
float _SpecPower;
- 步骤3. 现在,开始创建自定义光照模型,该模型将处理我们的 Diffuse 和 Specular 计算。
inline half4 LightingCustomBlinnPhong (SurfaceOutput s, half3 lightDir, half3 viewDir, half atten)
{
float3 halfVector = normalize(lightDir + viewDir);
float diff = max(0, dot(s.Normal, lightDir));
float nh = max(0, dot(s.Normal, halfVector));
float spec = pow(nh, _SpecPower) * _SpecularColor;
half4 c;
c.rgb = (s.Albedo * _LightColor0.rgb * diff) + (_LightColor0.rgb * _SpecularColor.rgb * spec) * (atten * 2);
c.a = s.Alpha;
return c;
}
- 步骤4. 想要完成我们的Shader,我们需要修改 #pragma 语句,告诉 CGPROGRAM 语义块来使用我们的自定义光照模型,而不是内置的光照模型:
CGPROGRAM
#pragma surface surf CustomBlinnPhong
- 完整代码:
Shader "CookbookShaders/BlinnPhong Specular"
{
Properties
{
_MainTint("Diffuse Tint", color) = (1,1,1,1)
_MainTex("Main Map", 2D) = "white"{}
_SpecularColor("Specular Color", color) = (1,1,1,1)
_SpecPower("Specular Power", Range(0.1, 60)) = 3
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 200
CGPROGRAM
#pragma surface surf CustomBlinnPhong
// 将属性链接到CG程序
half4 _MainTint;
sampler2D _MainTex;
half4 _SpecularColor;
float _SpecPower;
inline half4 LightingCustomBlinnPhong (SurfaceOutput s, half3 lightDir, half3 viewDir, half atten)
{
float3 halfVector = normalize(lightDir + viewDir);
float diff = max(0, dot(s.Normal, lightDir));
float nh = max(0, dot(s.Normal, halfVector));
float spec = pow(nh, _SpecPower) * _SpecularColor;
half4 c;
c.rgb = (s.Albedo * _LightColor0.rgb * diff) + (_LightColor0.rgb * _SpecularColor.rgb * spec) * (atten * 2);
c.a = s.Alpha;
return c;
}
// 确保在struct中获得纹理的uv
struct Input
{
float2 uv_MainTex;
};
void surf (Input IN, inout SurfaceOutput o)
{
float4 c = tex2D (_MainTex, IN.uv_MainTex) * _MainTint;
o.Albedo = c.rgb;
o.Alpha = c.a;
}
ENDCG
}
FallBack "Diffuse"
}
下 图3.1 演示了 BlinnPhong 光照模型的渲染结果:
图3.1
3.3、实现原理...
BlinnPhong 镜面反射几乎与 Phong 镜面反射完全相同,不同之处在于BlinnPhong 镜面反射的效率更高,因为它使用更少的代码来实现了几乎相同的效果。在现今的 Shader 中,你会发现十个里面有九个在使用BlinnPhong 镜面反射,因为它的代码相比之下更容易,并且对 Shader 性能的优化更好。
我们不需要再去计算反射向量,而是只需要获取视方向和光方向之间的向量即可,基本上是在模拟反射向量。实际上已经发现这种方法比上一种方法在物理上是更准确的,但我认为向大家介绍一下所有的可能性是很有必要的。
因此,想要获取半矢量,我们只需将视方向和光方向相加即可,如以下代码片段所示:
float3 halfVector = normalize (lightDir + viewDir);
然后,我们只需使用新的半角向量 点积 顶点法线,就可以得到 Specular 值。接下来,我们只需将其取 _SpecPower 的幂,然后再乘以镜面反射颜色(Specular color)变量。这个计算在代码量以及数学计算上要轻量很多。虽然它使用了很少的计算量,但是仍然为我们提供了一个非常棒的镜面反射(Specular)效果,并且它适用于很多的实时渲染领域。
第4节. 使用高光贴图来实现镜面反射
现在我们已经了解了如何在着色器中创建镜面反射的效果,接下来让我们开始学习如何给艺术家在镜面反射上提供更多的控制。在后边的知识点中,我们会学习到如何使用纹理来控制镜面反射(Specular)和 Specular power 属性。
镜面反射纹理的技术在现今大多数的游戏开发流程中都是很常见和常使用的一项技术,因为它的最终的效果是由 3D 艺术家通过绘制的镜面反射贴图来实现的。这为我们提供了在一个 Shader 中可以同时拥有粗糙类型的表面和光滑类型的表面的方法;或者,我们可使用另一张纹理来控制高光的宽度和高光的强度,让一个表面既可以具有宽泛的 Specular 高光,也可以具备非常尖锐的小高光。
仅通过将 Shader 的计算与纹理进行混合这一个方法就可以实现许多效果,这样可以让艺术家能够控制其 Shader 的最终渲染效果,同时这也是一个实现高效流程的关键。 让我们看看如何使用纹理来控制 镜面反射(Specular)光照模型。本节的内容会跟你介绍一些新概念,例如创建自己的 Input 结构,并了解数据如何从 output 结构传递到 lighting 函数、Input 结构和 surf() 函数。了解这些核心 Surface Shader 元素之间的数据流是 Shader 管线成功的核心。
4.1、准备工作
- 我们创建一个新的着色器(Shader)、材质球(Material)以及创建一个新的 3D 对象,将Shader赋予给材质球。将材质球赋予给新对象。
- 将 Shader 和 Material 连接并分配给场景中的对象后,双击 Shader 以在 MonoDevelop 中调出它。
- 我们还需要使用一张 Specular 纹理。任何纹理都可以,只要它在颜色和图案上有一些很好的变化即可。以下截图是我们本节所使用的镜面反射纹理,如图4.1:
图4.1
4.2、如何实现...
- 步骤1. 首先,在Properties 块中增加一些新的属性。我们将以下代码添加到 Shader 的 Properties 块中:
Properties
{
// 在编辑器的检查器面板中可以获取到Shader中的这些属性信息
_MainTint("Diffuse Tint", color) = (1,1,1,1)
_MainTex("Base (RGB)", 2D) = "white"{}
_SpecularColor("Specular Tint", color) = (1,1,1,1)
_SpecularMask("Specular Texture", 2D) = "white"{}
_SpecPower("Specular Power", Range(0.1, 120)) = 3
}
- 步骤2. 然后,我们需要将相应的变量添加到 Subshader 中,这样就可以使用 Properties 块中的属性进行后续的计算。我们在 #pragma 语句之后添加以下代码:
// 将属性块中的数据链接到CG程序
sampler2D _MainTex;
sampler2D _SpecularMask;
half4 _MainTint;
half4 _SpecularColor;
float _SpecPower;
- 步骤3. 现在我们需要添加自定义的 Output 结构体,使用这个结构体在surf 函数和 lighting 模型之间进行数据的传输,并且这个Output 结构体里面可以储存大量的我们需要的一些数据。如果清楚它的含义,请不要担心。我们会在下一个知识点中详细介绍 Output 结构体。我们把下边的代码放在 SubShader 块中声明的变量之后:
// 创建自定义的 Output 结构体
struct SurfaceCustomOutput
{
half3 Albedo;
half3 Normal;
half3 Emission;
half3 SpecularColor;
half Specular;
half Gloss;
half Alpha;
};
- 步骤4. 在我们刚刚写好的 Output 结构体之后,来添加我们自定义的光照模型。在本例中,我们把这个自定义的光照模型命名为 LightingCustomPhong 。接下来我们在 Output 结构之后输入以下代码:
inline half4 LightingCustomPhong (SurfaceCustomOutput s, half3 lightDir, half3 viewDir, half atten)
{
// 计算漫反射和反射向量
float diff = dot(s.Normal, lightDir);
float3 reflectionVector = normalize(2.0 * s.Normal * diff - lightDir);
// 计算 Phong Specular
float spec = pow(max(0.0,dot(reflectionVector, viewDir)), _SpecPower) * s.Specular;
float3 finalSpec = s.SpecularColor * spec * _SpecularColor.rgb;
// 计算最终的颜色值
half4 c;
c.rgb = (s.Albedo * _LightColor0.rgb * diff) + (_LightColor0.rgb * finalSpec);
c.a = s.Alpha;
return c;
}
- 步骤5. 接下来我们需要告诉 SubShader 模块来使用我们刚刚自定义的光照模型。在 #pragma 语句中输入以下代码,以便它加载我们的自定义照明模型:
CGPROGRAM
#pragma surface surf CustomPhong
- 步骤6. 由于我们将使用纹理来修改基础高光度计算的值,因此我们需要专门为该纹理存储另一组 UV。这是在 Input 结构体中完成的,方法是把 uv 这个词放在纹理变量名的前面。在自定义的光照模型代码的后边输入以下代码:
struct Input
{
// 从 Input 结构体中获取uv信息
float2 uv_MainTex;
float2 uv_SpecularMask;
};
- 步骤7. 最后我们只需要修改我们的 surf() 函数就可以完成这个 Shader 的效果了,修改方式如下代码所示。这样就会把纹理信息传到我们自定义的光照模型中进行计算,因此我们就可以在光照模型中去使用纹理的像素值来调整镜面反射最终的效果了:
void surf (Input IN, inout SurfaceCustomOutput o)
{
// 从纹理中获取颜色信息
float4 c = tex2D (_MainTex, IN.uv_MainTex) * _MainTint;
float4 specMask = tex2D(_SpecularMask, IN.uv_SpecularMask) * _SpecularColor;
// 在输出结构体中设置参数
o.Albedo = c.rgb;
o.Specular = specMask.r;
o.SpecularColor = specMask.rgb;
o.Alpha = c.a;
}
完整代码:
Shader "CookbookShaders/ Masking Specular with textures "
{
Properties
{
// 在编辑器的检查器面板中可以获取到Shader中的这些属性信息
_MainTint("Diffuse Tint", color) = (1,1,1,1)
_MainTex("Base (RGB)", 2D) = "white"{}
_SpecularColor("Specular Tint", color) = (1,1,1,1)
_SpecularMask("Specular Texture", 2D) = "white"{}
_SpecPower("Specular Power", Range(0.1, 120)) = 3
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 200
CGPROGRAM
#pragma surface surf CustomPhong
// 将属性块中的数据链接到CG程序
sampler2D _MainTex;
sampler2D _SpecularMask;
half4 _MainTint;
half4 _SpecularColor;
float _SpecPower;
// 创建自定义的 Output 结构体
struct SurfaceCustomOutput
{
half3 Albedo;
half3 Normal;
half3 Emission;
half3 SpecularColor;
half Specular;
half Gloss;
half Alpha;
};
inline half4 LightingCustomPhong (SurfaceCustomOutput s, half3 lightDir, half3 viewDir, half atten)
{
// 计算漫反射和反射向量
float diff = dot(s.Normal, lightDir);
float3 reflectionVector = normalize(2.0 * s.Normal * diff - lightDir);
// 计算 Phong Specular
float spec = pow(max(0.0,dot(reflectionVector, viewDir)), _SpecPower) * s.Specular;
float3 finalSpec = s.SpecularColor * spec * _SpecularColor.rgb;
// 计算最终的颜色值
half4 c;
c.rgb = (s.Albedo * _LightColor0.rgb * diff) + (_LightColor0.rgb * finalSpec);
c.a = s.Alpha;
return c;
}
struct Input
{
// 从 Input 结构体中获取uv信息
float2 uv_MainTex;
float2 uv_SpecularMask;
};
void surf (Input IN, inout SurfaceCustomOutput o)
{
// 从纹理中获取颜色信息
float4 c = tex2D (_MainTex, IN.uv_MainTex) * _MainTint;
float4 specMask = tex2D(_SpecularMask, IN.uv_SpecularMask) * _SpecularColor;
// 在输出结构体中设置参数
o.Albedo = c.rgb;
o.Specular = specMask.r;
o.SpecularColor = specMask.rgb;
o.Alpha = c.a;
}
ENDCG
}
FallBack "Diffuse"
}
下 图4.2 展示了我们计算的镜面反射遮罩的最终效果,这个效果使用了镜面反射颜色图及它的通道信息。现在,我们可以看到镜面反射在整个表面上的细节变化是非常丰富的,而不像之前,表面上只有一个高光点:
图4.2
4.3、实现原理...
此 Shader 与 Phong 的计算基本上是相同的,它们的不同之处在于此 Shader 是通过纹理(镜面反射图)像素值来修改镜面反射(Specular)的效果。因此,这会给镜面反射最终呈现的渲染结果带来更丰富且很有深度的感官效果。
为此,我们需要把 Surface 函数的信息传递到 lighting 函数中去。原因是我们无法在 lighting 函数中获取表面的 UV 信息(即采样纹理贴图的UV信息)。你也可以在 lighting 函数中使用程序方式去生成 UV,但是如果想要对纹理进行采样并获取其像素信息,则必须使用 Input 结构体,而从 Input 结构访问数据的唯一方法就是使用 surf() 函数。
因此,要设置数据之间的关系,这样的话我们就需要创建一个自定义的 SurfaceCustomOutput 结构。这个结构体里面装了所有表面着色器最终输出的数据,它就类似于一个数据的容器。幸运的是,lighting 函数和 surf() 函数它们都可以访问到这个结构体中的数据。因此,如果我们要创建自己需要的数据,我们就可以把这些数据都放到这个结构体中。以下代码是我们着色器中的 SurfaceCustomOutput 结构体:
// 创建自定义的 Output 结构体
struct SurfaceCustomOutput
{
half3 Albedo;
half3 Normal;
half3 Emission;
half3 SpecularColor;
half Specular;
half Gloss;
half Alpha;
};
因此,我们需要把它添加到 Shader 中,并且要告诉 surf() 函数和 lighting 函数它们不要使用内置的结构了,而是使用 SurfaceCustomOutput 结构体。代码如下所示:
// 创建自定义的 Output 结构体
struct SurfaceCustomOutput
{
half3 Albedo;
half3 Normal;
half3 Emission;
half3 SpecularColor;
half Specular;
half Gloss;
half Alpha;
};
inline half4 LightingCustomPhong (SurfaceCustomOutput s, half3 lightDir, half3 viewDir, half atten)
{
}
void surf (Input IN, inout SurfaceCustomOutput o)
{
}
ENDCG
需要注意的是如何让 SurfaceCustomOutput 结构体成为surf() 函数和 lighting 函数之间的一个参数。我们还在 SurfaceOutput 结构体中添加了一个名为 SpecularColor 的新条目。这样我们就可以在 Specular 颜色纹理中来存储每个像素的信息了,并且还可以在我们的 Lighting 函数中使用它。这样的话就不仅仅是只使用一个全局颜色去乘以一个 Specular 值这么单调了。
我们只需要使用 tex2D() 函数就可以对纹理信息进行采样,然后把tex2D() 函数的结果指定给 o.SpecularColor ,最后将其传递到我们的 SurfaceCustomOutput 结构体中。完成后,你现在就可以在 lighting 函数中访问到纹理信息了。
void surf (Input IN, inout SurfaceCustomOutput o)
{
// 从纹理中获取颜色信息
float4 c = tex2D (_MainTex, IN.uv_MainTex) * _MainTint;
float4 specMask = tex2D(_SpecularMask, IN.uv_SpecularMask) * _SpecularColor;
// 在输出结构体中设置参数
o.Albedo = c.rgb;
o.Specular = specMask.r;
o.SpecularColor = specMask.rgb;
o.Alpha = c.a;
}
这个技术对于在 Shader 中创建自定义效果是至关重要的。现在你应该知道了如何在 surf() 函数中进行纹理的采样,并且可以在自定义的光照模型中去调用它。这个方法可以让你的 Shader 把每个像素都能高质量的渲染出来,从而你会得到一个非常高质量的i画面效果。
第5节. 金属与软镜面反射的比较
在本节中,我们来探索一种多功能 Shader 的创建方法,就是使用一个 Shader 同时表现软高光(Soft Specular)和硬高光(Hard Speculular)两种效果。在大多的项目中一般都是需要创建一组着色器去执行很多的任务。但这样就会需要我们去管理很多的着色器,也对我们造成了非常大的负担。因此着色器程序员通常会在一个 Shader 中来实现布料和金属这两种材质。在本节中我们的目标是通过 Specular 来实现这种模块化,这样我们的艺术家或者 Shader 的使用者只需使用一个 Shader 就能实现两种材质效果,即柔软、有光泽的材质(Soft Specular),和坚硬的金属材质。
为了实现这种灵活性,我们会创建一个与 Cook Torrance 着色器类似的镜面反射光照模型,为了让使用此着色器的艺术家或最终用户体验感更好一些,我们还在里面增加了一些自己的风格。
5.1、准备工作
- 新建一个 Unity 场景,并在新场景中创建一个球体、平面和平行光。将场景命名并保存。
- 创建一个新的着色器(Shader )和材质球(Material),并为其命名。
- 最后,把 Shader 赋予给材质球,并将材质球赋予给场景中的球体。
- 我们还需要把一些纹理组合到一起,艺术家就可以通过定义高光的模糊程度和锐化程度来调整高光的粗糙度。有关这些纹理的示例,请参阅以下屏幕截图。
下 图5.1 直观的展示了我们本节中使用的不同粗糙度的纹理:
图5.1
5.2、如何实现...
- 步骤1. 首先,设置 Shader 所需的属性。将以下代码输入到 Shader 的 Properties 块中:
Properties
{
_MainTint("Diffuse Tint", color) = (1,1,1,1)
_MainTex("Base (RGB)", 2D) = "white"{}
_RoughnessTex("Roughness texture", 2D) = ""{}
_Roughness("_Roughness", Range(0,1)) = 0.5
_SpecularColor("Specular Color", color) = (1,1,1,1)
_SpecPower("Specular Power", Range(0,30)) = 2
_Fresnel("Fresnel Value", Range(0, 1.0)) = 0.05
}
- 步骤2. 然后,在 SubShader 块中声明以下属性,将属性链接到CG程序。因此在 Shader 中的 #pragma 语句后面输入以下代码:
// 将属性链接到CG程序
sampler2D _MainTex;
sampler2D _RoughnessTex;
float _Roughness;
float _SpecPower;
float _Fresnel;
half4 _SpecularColor;
half4 _MainTint;
- 步骤3. 声明新的照明模型,命名为LightingMetallicSoft 。并告诉 #pragma 语句来执行:
CGPROGRAM
#pragma surface surf MetallicSoft
#pragma target 3.0
inline half4 LightingMetallicSoft (SurfaceOutput s, half3 lightDir, half3 viewDir, half atten)
{
}
- 步骤4. 此时,我们已准备好了光照计算所用到的向量,把它们写到自定义的光照模型函数中即可。在光照计算中首先要把漫反射和视方向相关的向量计算出来,因为光照模型会用到它们。
// 计算漫反射和视方向的值
float3 halfVector = normalize(lightDir + viewDir);
float NdotL = saturate(dot(s.Normal, normalize(lightDir)));
float NdotH_raw = dot(s.Normal, halfVector);
float NdotH = saturate(dot(s.Normal, halfVector));
float NdotV = saturate(dot(s.Normal, normalize(viewDir)));
float VdotH = saturate(dot(halfVector, normalize(viewDir)));
- 步骤5. 以下是计算镜面反射中粗糙度的代码。它是通过使用纹理贴图来控制高光形状的,用程序的方式来模拟表面上微小的凹凸 。代码如下所示:
// 微观方面分布
float geoEnum = 2.0 * NdotH;
float3 G1 = (geoEnum * NdotV) / NdotH;
float3 G2 = (geoEnum * NdotL) / NdotH;
float3 G = min(1.0, min(G1,G2));
// 使用镜面反射去查找BRDF样本
float roughness = tex2D(_RoughnessTex, float2(NdotH_raw * 0.5 + 0.5, _Roughness)).r;
- 步骤6. 高光度计算所需的最后一个元素是菲涅耳项。菲涅尔的主要作用是让镜面反射的强度如何根据你的视角的不同而变化。当我们在非常倾斜的一个角度去观察物体表面时菲涅尔可以屏蔽掉镜面反射。
// 创建自定义菲涅尔值
float fresnel = pow(1.0 - VdotH, 5.0);
fresnel *= (1.0 - _Fresnel);
fresnel += _Fresnel;
- 步骤7. 现在我们已经计算好了高光所需的所有组件,我们只需要把这些组件组合到一起来得到最终的 Specular 值。如下代码所示:
// 计算最终的镜面反射值
float3 spec = float3(fresnel * G * roughness * roughness) * _SpecPower;
- 步骤8. 此光照模型的最后一个 步骤是将漫反射项(Diffuse)和镜面反射项(Specular)相加即可:
float4 c;
c.rgb = (s.Albedo * _LightColor0.rgb * NdotL) + (spec * _SpecularColor.rgb) * (atten * 2.0);
c.a = s.Alpha;
return c;
- 完整代码:
Shader "CookbookShaders/ Metallic versus soft Specular "
{
Properties
{
_MainTint("Diffuse Tint", color) = (1,1,1,1)
_MainTex("Base (RGB)", 2D) = "white"{}
_RoughnessTex("Roughness texture", 2D) = ""{}
_Roughness("_Roughness", Range(0,1)) = 0.5
_SpecularColor("Specular Color", color) = (1,1,1,1)
_SpecPower("Specular Power", Range(0,30)) = 2
_Fresnel("Fresnel Value", Range(0, 1.0)) = 0.05
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 200
CGPROGRAM
#pragma surface surf MetallicSoft
#pragma target 3.0
// 将属性链接到CG程序
sampler2D _MainTex;
sampler2D _RoughnessTex;
float _Roughness;
float _SpecPower;
float _Fresnel;
float4 _SpecularColor;
half4 _MainTint;
inline half4 LightingMetallicSoft (SurfaceOutput s, half3 lightDir, half3 viewDir, half atten)
{
// 计算漫反射和视方向的值
float3 halfVector = normalize(lightDir + viewDir);
float NdotL = saturate(dot(s.Normal, normalize(lightDir)));
float NdotH_raw = dot(s.Normal, halfVector);
float NdotH = saturate(dot(s.Normal, halfVector));
float NdotV = saturate(dot(s.Normal, normalize(viewDir)));
float VdotH = saturate(dot(halfVector, normalize(viewDir)));
// 微观方面分布
float geoEnum = 2.0 * NdotH;
float3 G1 = (geoEnum * NdotV) / NdotH;
float3 G2 = (geoEnum * NdotL) / NdotH;
float3 G = min(1.0, min(G1,G2));
// 使用镜面反射去查找BRDF样本
float roughness = tex2D(_RoughnessTex, float2(NdotH_raw * 0.5 + 0.5, _Roughness)).r;
// 创建自定义菲涅尔值
float fresnel = pow(1.0 - VdotH, 5.0);
fresnel *= (1.0 - _Fresnel);
fresnel += _Fresnel;
// 计算最终的镜面反射值
float3 spec = float3(fresnel * G * roughness * roughness) * _SpecPower;
float4 c;
c.rgb = (s.Albedo * _LightColor0.rgb * NdotL) + (spec * _SpecularColor.rgb) * (atten * 2.0);
c.a = s.Alpha;
return c;
}
// 确保在struct中获得纹理的uv
struct Input
{
float2 uv_MainTex;
};
void surf (Input IN, inout SurfaceOutput o)
{
float4 c = tex2D (_MainTex, IN.uv_MainTex) * _MainTint;
o.Albedo = c.rgb;
o.Alpha = c.a;
}
ENDCG
}
FallBack "Diffuse"
}
完成此 Shader 中所有代码之后,回到 Unity 编辑器中运行此 Shader 。如果你的 Shader 没有报错,就会得到如 图5.2 的渲染效果:
图5.2
5.3、实现原理...
好...我们看似做了很多的计算,但实际上这些都是很容易理解的。你可以把每个步骤进行输出查看效果,将每个步骤的计算结果直接赋值给 c.rgb 即可。完成此操作后,你就可以在 Editor 视窗中查看着色器里输出的计算步骤。记住,这是在调试 Shader 时非常好用的一个方法。
在光照模型函数里,第一组代码计包含了所有有关漫反射和视方向相关的计算,下 图5.3 中展示了里面每项调试后的渲染结果。
图5.3
有了所有这些数据,我们就可以使用它进行后续的光照计算了,它们就类似于 Photoshop 软件中的图层一样。我们通过生成一个程序值来制作光线反射和分散光线的效果。该值模拟了物体表面上微小的凹凸效果。
// 微观方面分布
float geoEnum = 2.0 * NdotH;
float3 G1 = (geoEnum * NdotV) / NdotH;
float3 G2 = (geoEnum * NdotL) / NdotH;
float3 G = min(1.0, min(G1,G2));
在这个光照模型中有一个很关键的地方,就是我们通过使用采样一张纹理贴图的方式来控制高光的宽度或者它的粗糙度。所采样的UV是通过程序的方式生成的,在采样图中拾取一个位置来展示高光点。这个UV是一个 float2() 二维的向量。它的实现方式是在这个二维的向量中,我们使用了 NdotH 或者 半角向量和顶点法线的点积,把它放到了这个二维向量中并作为 foat2() 里的第一个属性参数,它的第二个属性参数是暴漏在检查器(Inspector)属性面板上的,即_Roughness。它是用来控制高光(Specular)缩放的。
// 使用镜面反射去查找BRDF样本
float roughness = tex2D(_RoughnessTex, float2(NdotH_raw * 0.5 + 0.5, _Roughness)).r;
接下来,我们开始创建菲涅耳(Fresnel)效果,这样当我们看向光线指向反方向时,镜面反射的强度就会增加。
// 创建自定义菲涅尔值
float fresnel = pow(1.0 - VdotH, 5.0);
fresnel *= (1.0 - _Fresnel);
fresnel += _Fresnel;
完成以上的计算后,我们只需要将它们相乘就可以获得最终的 Specular 值。在本例中,我们还需要乘以另一个名为 _SpecPower 的属性,它可以更高级的去控制 Specular 的强度。
// 计算最终的镜面反射值
float3 spec = float3(fresnel * G * roughness * roughness) * _SpecPower;
最后一步是将镜面反射(Specular)和漫反射(Diffuse)相加,并将最终颜色值返回给表面着色器。希望你可以看到,通过使用其它类型的向量和纹理贴图对一个简单系统进行修改的程度。
5.4、更多内容
有关 Cook Torrance Specular 模型的更多信息,请参阅以下链接:
http://en.wikipedia.org/wiki/Specular_highlight#Cook.E2.80.93Torrance_modelhttp://en.wikipedia.org/wiki/Specular_highlight#Cook.E2.80.93Torrance_modelhttp://en.wikipedia.org/wiki/Specular_highlight#Cook.E2.80.93Torrance_modelhttp://en.wikipedia.org/wiki/Specular_highlight#Cook.E2.80.93Torrance_modelhttp://en.wikipedia.org/wiki/Specular_highlight#Cook.E2.80.93Torrance_modelhttp://en.wikipedia.org/wiki/Specular_highlight#Cook.E2.80.93Torrance_modelhttp://en.wikipedia.org/wiki/Specular_highlight#Cook.E2.80.93Torrance_modelhttp://en.wikipedia.org/wiki/Specular_highlight#Cook.E2.80.93Torrance_modelhttp://en.wikipedia.org/wiki/Specular_highlight#Cook.E2.80.93Torrance_modelGame Programming Wiki - GPWikihttp://content.gpwiki.org/index.php/D3DBook:%28Lighting%29_Cook-Torrancehttp://content.gpwiki.org/index.php/D3DBook:%28Lighting%29_Cook-Torrancehttp://content.gpwiki.org/index.php/D3DBook:%28Lighting%29_Cook-Torrancehttp://content.gpwiki.org/index.php/D3DBook:%28Lighting%29_Cook-Torrancehttp://content.gpwiki.org/index.php/D3DBook:%28Lighting%29_Cook-Torrancehttp://content.gpwiki.org/index.php/D3DBook:%28Lighting%29_Cook-Torrancehttp://content.gpwiki.org/index.php/D3DBook:%28Lighting%29_Cook-Torrance
http://content.gpwiki.org/index.php/D3DBook:%28Lighting%29_Cook-Torrancehttp://forum.unity3d.com/threads/158589-Cook-Torrancehttp://forum.unity3d.com/threads/158589-Cook-Torrancehttp://forum.unity3d.com/threads/158589-Cook-Torrancehttp://forum.unity3d.com/threads/158589-Cook-Torrancehttp://forum.unity3d.com/threads/158589-Cook-Torrancehttp://forum.unity3d.com/threads/158589-Cook-Torrancehttp://forum.unity3d.com/threads/158589-Cook-Torrancehttp://forum.unity3d.com/threads/158589-Cook-Torrance
http://forum.unity3d.com/threads/158589-Cook-Torrance
第6节. 创建各向异性镜面反射
各向异性(Anisotropic)是一种镜面反射(Specular)类型或反射(reflection)类型,用于模拟表面上凹槽的方向性,并在垂直方向上修改或拉伸镜面反射。常用于模拟拉丝金属效果,在清晰、光滑、抛光的金属效果上是不会使用各向异性的。想象以下当你看到 CD 或 DVD 碟片的一端时的镜面反射,或者平底锅底部形状的镜面反射形状时,如果你仔细观察它们的表面你会发现表面凹槽位置的方向,这就是拉丝金属的效果。当你把各向异性镜面反射应用到一个表面时,你就可以得到一个沿垂直方向拉伸的镜面反射(Specular)效果。
通过此项技术会向大家介绍使用 “增强镜面反射高光” 来实现不同类型的拉丝表面的概念。在未来的讲解中,我们将研究使用此技术的概念来实现其他效果的方法,例如拉伸反射和头发,目前,我们首先学习这项技术的基础知识。下方链接中的着色将作为我们自定义各向异性着色器的参考:
图6.1
6.1、准备工作
- 创建一个带有灯光和3D物体的新场景,方便我们直观的来调试 Shader。
- 然后,创建一个新的着色器(Shader)和材质球(Material),并将它们赋予到场景中的物体上。
- 最后,我们需要一张方向性的法线贴图,这个方向指的是表示各向异性镜面反射高光的方向。
下 图6.2 展示了我们在此方法中使用到的法线贴图。它可以从本书的支持页面获得,网址为:www.packtpub.com/support 。
图6.2
6.2、如何实现...
- 步骤1. 首先在着色器中添加我们所需要的属性,这些属性供艺术家们来控制物体表面的外观:
Properties
{
_MainTint("Diffuse Tint", color) = (1,1,1,1)
_MainTex("Main Map", 2D) = "white"{}
_SpecularColor("Specular Color", color) = (1,1,1,1)
_Specular("Specular Power", 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
}
- 步骤2. 然后,我们需要在 Properties 块和 SubShader 块之间建立连接,以便我们可以使用 Properties 块提供的数据:
// 将属性链接到CG程序
sampler2D _MainTex;
sampler2D _RoughnessTex;
float _Roughness;
float _SpecPower;
float _Fresnel;
float4 _SpecularColor;
half4 _MainTint;
- 步骤3. 现在我们开始创建光照函数,该函数会在我们的表面上展现出正确的各向异性效果:
inline half4 LightingAnisotropic (SurfaceAnisoOutput s, half3 lightDir, half3 viewDir, half atten)
{
half3 halfVector = normalize(normalize(lightDir) + normalize(viewDir));
float NdotL = saturate(dot(s.Normal, lightDir));
half 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);
half4 c;
c.rgb = ((s.Albedo * _LightColor0.rgb * NdotL) + (_LightColor0.rgb * _SpecularColor.rgb * spec)) * (atten * 2);
c.a = 1.0;
return c;
}
- 步骤4. 为了使用这个新的光照函数,我们需要告诉 subshader 中的 #pragma 语句来查找新创建的这个光照函数,不再使用内置的光照函数。我们还告诉 Shader 使用 着色器模型的目标为 3.0版本,以便我们可以在程序中为纹理留出更多空间:
CGPROGRAM
#pragma surface surf Anisotropic
#pragma target 3.0
- 步骤5. 我们还通过在 Input 结构中声明以下代码,为各向异性法线贴图指定了自己的 UV。并不是一定要给法线贴图指定自己的UV,它也可以使用主纹理中的UV。为它指定自己的UV主要是为了能够独立控制拉丝金属效果的平铺,这样我们就可以调节我们所需的任意大小。
struct Input
{
float2 uv_MainTex;
float2 uv_AnisoDir;
};
- 步骤6. 最后,我们需要使用 surf() 函数将正确的数据传递给我们的光照函数。所以,我们就可以从各向异性法线贴图中获取每个像素的信息,并设置高光度参数。
void surf (Input IN, inout SurfaceAnisoOutput o)
{
float4 c = tex2D (_MainTex, IN.uv_MainTex) * _MainTint;
float3 anisoTex = UnpackNormal(tex2D (_MainTex, IN.uv_AnisoDir));
o.AnisoDirection = anisoTex;
o.Specular = _Specular;
o.Gloss = _SpecPower;
o.Albedo = c.rgb;
o.Alpha = c.a;
}
- 完整代码:
Shader "CookbookShaders/Anisotropic Specular"
{
Properties
{
_MainTint("Diffuse Tint", color) = (1,1,1,1)
_MainTex("Main Map", 2D) = "white"{}
_SpecularColor("Specular Color", color) = (1,1,1,1)
_Specular("Specular Power", Range(0,1)) = 0.5
_SpecPower("Specular Power", Range(0,1)) = 0.5
_AnisoDir("Anisotropic Direction", 2D) = "bump"{}
_AnisoOffset("Anisotropic Offset", Range(-1,1)) = -0.2
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 200
CGPROGRAM
#pragma surface surf Anisotropic
#pragma target 3.0
// 将属性链接到CG程序
sampler2D _MainTex;
sampler2D _AnisoDir;
half4 _MainTint;
half4 _SpecularColor;
float _AnisoOffset;
float _Specular;
float _SpecPower;
// 确保在struct中获得纹理的uv
struct Input
{
float2 uv_MainTex;
float2 uv_AnisoDir;
};
struct SurfaceAnisoOutput
{
half3 Albedo;
half3 Normal;
half3 Emission;
half3 AnisoDirection;
half Specular;
half Gloss;
half Alpha;
};
inline half4 LightingAnisotropic (SurfaceAnisoOutput s, half3 lightDir, half3 viewDir, half atten)
{
half3 halfVector = normalize(normalize(lightDir) + normalize(viewDir));
float NdotL = saturate(dot(s.Normal, lightDir));
half 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);
half4 c;
c.rgb = ((s.Albedo * _LightColor0.rgb * NdotL) + (_LightColor0.rgb * _SpecularColor.rgb * spec)) * (atten * 2);
c.a = 1.0;
return c;
}
void surf (Input IN, inout SurfaceAnisoOutput o)
{
float4 c = tex2D (_MainTex, IN.uv_MainTex) * _MainTint;
float3 anisoTex = UnpackNormal(tex2D (_MainTex, IN.uv_AnisoDir));
o.AnisoDirection = anisoTex;
o.Specular = _Specular;
o.Gloss = _SpecPower;
o.Albedo = c.rgb;
o.Alpha = c.a;
}
ENDCG
}
FallBack "Diffuse"
}
下 图6.3 展示了我们的各向异性着色器的渲染结果。各向异性法线贴图可以去控制或影响表面各项异性的方向,并帮助我们分散表面周围的高光:
图6.3
6.3、实现原理...
让我们来分解一下 Shader 的核心组件,并且解释一下为什么我们能够获得我们所拿到的效果。我们在这里主要介绍自定义光照函数,那么 Shader 的其余剩下的部分我们应该就会不言自明了。
我们首先声明我们自己的结构体 SurfaceCustomOutput 。我们这样做的目的是需要从各向异性法线贴图中获取每像素的信息,实现它的唯一方式就是在surf() 函数中使用 tex2D() 函数。
struct SurfaceAnisoOutput
{
half3 Albedo;
half3 Normal;
half3 Emission;
half3 AnisoDirection;
half Specular;
half Gloss;
half Alpha;
};
我们可以使用 SurfaceOutput 结构体作为光照函数和表面函数之间的一种交互方式。在本例中,我们将每个像素纹理信息都存储在 surf() 函数中名为 anisoTex 的变量中,然后通过将数据存储在 AnisoDirection 变量中,再将该数据传递给 SurfaceAnisoOutput 结构体。一旦有了这些信息,我们就可以通过 s.AnisoDirection 在光照函数中去使用每个像素信息了。
设置好这些数据的链接以后,我们就继续进行局部光照计算。首先,我们先从半角向量(half vector)开始,如下代码所示将它们都皈依化。这样我们就不用进行计算完整的反射和漫反射光照,漫反射光照是通过顶点法线点积光向量或光方向来实现的。
half3 halfVector = normalize(normalize(lightDir) + normalize(viewDir));
float NdotL = saturate(dot(s.Normal, lightDir));
接下来,我们把 Specular 修改成我们想要实现的效果。首先,将 顶点法线 和 各向异性法线图相加的结果 与在上一步计算中得到的 halfVector 进行点积,在这里需要将顶点法线 加上 各向异性法线图的结果进行归一化(normalize)。这为我们提供了一个值为 1 的表面法线的浮点值,通过各向异性法线贴图来进行修改,它与 halfVector 平行,与 0 垂直。最后,我们使用 sin() 函数修改此值,之后我们基本上就可以获得较暗的中间高光,并最终获得基于 halfVector 的环形效果。
half 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);
此效果非常适合创建更高级的金属类型表面,尤其是那些被刷过的并具有方向性的表面。它也适用于头发或者任何具有方向性的柔软表面。下 图6.4 显示了各向异性光照计算的最终渲染效果:
图6.4
这本书可以让你学习到如何使用着色器和屏幕特效让你的Unity工程拥有震撼的渲染画面。
作者:Kenny Lammers