Unity Shaders and Effets Cookbook
《着色器和屏幕特效制作攻略》
这本书可以让你学习到如何使用着色器和屏幕特效让你的Unity工程拥有震撼的渲染画面。
—— Kenny Lammers
I like this book!
第五章:光照模型
在本章中,你将学习如何创建以下光照模型:
介绍
在过去的几个章节中,我们一直在研究使用 Surface Shading 语言的不同组件,来构建着色器和光照模型的方法。在本章,我们将利用我们新发现的知识来为不同的效果创建我们自己的完整着色器。
我们将介绍业内许多游戏所需的一些常见类型的着色器。这将会为我们提供一些生成自己的着色器的工作流程,并且了解有关在游戏制作中需要自定义材质时尝试处理新着色器的更多信息。我们还会去讨论在团队中工作的时候来提高效率的一些方法,以及该团队中的美术工作人员如何使用着色器。
第1节. Lit Sphere光照模型
Lit Sphere 光照模型是非常有趣的,它的实现方式使用的是基于图像照明的方法。实际上我们可以使用 2D 纹理去烘焙整个的光照信息。它的效果跟你在 Zbrush 中看到的效果相同。如果您熟悉 Zbrush 的 MatCaps,那么 Lit Sphere 的工作原理方式跟它是相同的。我们可以创建一个纹理,该纹理以漫反射、镜面反射、反射和边缘照明的外观进行烘焙,并使用它来照亮我们的着色器。此着色器的唯一问题是,由于我们已经完全烘焙了光照,因此光照永远不会改变,除非你在整个环境中切换到不同的纹理,就像我们在第 4 章 “Reflecting Your World” 文章中的 Simple Cubemap reflection 这小节里看到的那样。因此,此 Shader 不会对环境中的灯光产生交互的反应,也不会随着你在模型中移动视角而改变。以下 图1.1 屏幕截图显示了 Lit Sphere 纹理查找的示例,通常称为 Sphere Map:
图1.1
这意味着此着色器非常适合创建漂亮的立体模型场景,甚至可以用于游戏的过场动画,其中摄像机被锁定,并且您需要为角色和环境提供非常复杂的照明。
那么,让我们来看看如何使用表面着色器创建这种类型的照明模型,以及如何在 Unity 中使用。
1.1、准备工作
开始这个 Shader 之前,我们需要学习如何创建一个纹理(此纹理就是用来在shader中照明使用的)。我们可以使用 Photoshop来创建这张纹理,但更推荐使用 MaCrea 这款免费的小型工具软件来创建,它要容易得多。该工具可以在 Web 上找到,网址为:http://www.taron.de/macrea;这里提供了一个免费且很出色的程序,可帮助你创建这些 Lit Sphere 贴图。我建议你观看 Vimeo 上的视频,以熟悉 MaCrea 界面和工作流程。
在网址 http://vimeo.com/14030320 上提供了 MaCrea 简介。
一旦你熟悉了如何去创建 Sphere 贴图之后,我们就可以继续执下一步的制作部分。下 图1.2 屏幕截图显示了 MaCrea 界面,并且使用了该工具创建完成的 Lit Sphere 效果:
图1.2
- 1. 创建一个新场景,里面包含几个对象、一个平面和一个灯光。
- 2. 创建一个新的 Shader 和 材质球。然后将着色器赋予给材质球。
1.2、如何实现...
使用我们已经创建好的场景资产并在编辑器中打开已经准备好的Shader,下面就开始创建我们的lit sphere光照模型。
- 步骤1. 像之前一样,我们需要为 Surface Shader 设置属性,以便我们可以让此 Shader 的用户输入不同的纹理并且可以调整数值。因此,让我们将以下代码添加到 Properties 块中:
Properties
{
_MainTint("Diffuse Tint", color) = (1,1,1,1)
_MainTex("Main Map", 2D) = "white"{}
_NormalMap("Normal Map", 2D) = "bump"{}
}
- 步骤2. 由于此 Shader 仅使用球形贴图来照亮我们的模型,因此我们不再需要 Lambert 光照函数,但是我们需要声明自己的 Unlit 光照函数。接下来还需要编写一些顶点函数,以便此 Shader 正常工作:
CGPROGRAM
#pragma surface surf Unlit vertex:vert
- 步骤3. 然后,与之前一样,我们需要确保在 SubShader 块中声明我们的属性,以便我们在 Unity 编辑器中可以使用 Inspector 窗口中用户提供的数据。
// 将属性链接到CG程序
sampler2D _MainTex;
sampler2D _NormalMap;
half4 _MainTint;
- 步骤4. 此时,我们可以创建新的光照函数,该函数将为我们生成一个 Unlit 光照模型。我们必须这样做的原因是,因为在这种情况下,我们不希望我们的 Shader 受到光源的影响。我们只希望物体对象投射阴影。因此,我们需要将以下 Lighting 函数添加到着色器中:
inline half4 LightingUnlit(SurfaceOutput s, half3 lightDir, half atten)
{
half4 c = half4(1,1,1,1);
c.rgb = c * s.Albedo;
c.a = s.Alpha;
return c;
}
- 步骤5. 我们现在需要用一些额外的属性填充我们的 Input 结构体,以便我们可以将 vertex() 函数中的信息传递给我们的 surf() 函数:
// 确保在struct中获得纹理的uv
struct Input
{
float2 uv_MainTex;
float2 uv_NormalMap;
float3 tan1;
float3 tan2;
};
- 步骤6. 为了准确的查找到 Sphere 贴图,我们需要将切线旋转矩阵与当前模型的逆转置模型视方向相乘。这会为我们提供正确的向量,以便我们可以应用 Sphere 贴图纹理。如果您不了解顶点着色器中发生的情况,请不要担心,我们将在后边的知识点中详细解释。
void vert(inout appdata_full v, out Input o)
{
UNITY_INITIALIZE_OUTPUT(Input, o);
TANGENT_SPACE_ROTATION;
o.tan1 = mul(rotation, UNITY_MATRIX_IT_MV[0].xyz);
o.tan2 = mul(rotation, UNITY_MATRIX_IT_MV[1].xyz);
}
- 步骤7. 最后,我们可以用适当的计算填充我们的 surf() 函数,以便为我们的 Sphere 贴图纹理生成正确的查找值,并将这些值传输到我们的 SurfaceOutput 结构体中。同样,此函数的核心将在下一个知识点中解释:
void surf (Input IN, inout SurfaceOutput o)
{
float3 normals = UnpackNormal(tex2D(_NormalMap, IN.uv_NormalMap));
o.Normal = normals;
float2 litSphereUV;
litSphereUV.x = dot(IN.tan1, o.Normal);
litSphereUV.y = dot(IN.tan2, o.Normal);
float4 c = tex2D (_MainTex, litSphereUV * 0.5 + 0.5);
o.Albedo = c.rgb * _MainTint;
o.Alpha = c.a;
}
- 完整代码:
Shader "CookbookShaders/5-1 Lit Sphere Lighting Model"
{
Properties
{
_MainTint("Diffuse Tint", color) = (1,1,1,1)
_MainTex("Main Map", 2D) = "white"{}
_NormalMap("Normal Map", 2D) = "bump"{}
}
SubShader
{
Tags { "RenderType"="Opaque" }
CGPROGRAM
#pragma surface surf Unlit vertex:vert
// 将属性链接到CG程序
sampler2D _MainTex;
sampler2D _NormalMap;
half4 _MainTint;
inline half4 LightingUnlit(SurfaceOutput s, half3 lightDir, half atten)
{
half4 c = half4(1,1,1,1);
c.rgb = c * s.Albedo;
c.a = s.Alpha;
return c;
}
// 确保在struct中获得纹理的uv
struct Input
{
float2 uv_MainTex;
float2 uv_NormalMap;
float3 tan1;
float3 tan2;
};
void vert(inout appdata_full v, out Input o)
{
UNITY_INITIALIZE_OUTPUT(Input, o);
TANGENT_SPACE_ROTATION;
o.tan1 = mul(rotation, UNITY_MATRIX_IT_MV[0].xyz);
o.tan2 = mul(rotation, UNITY_MATRIX_IT_MV[1].xyz);
}
void surf (Input IN, inout SurfaceOutput o)
{
float3 normals = UnpackNormal(tex2D(_NormalMap, IN.uv_NormalMap));
o.Normal = normals;
float2 litSphereUV;
litSphereUV.x = dot(IN.tan1, o.Normal);
litSphereUV.y = dot(IN.tan2, o.Normal);
float4 c = tex2D (_MainTex, litSphereUV * 0.5 + 0.5);
o.Albedo = c.rgb * _MainTint;
o.Alpha = c.a;
}
ENDCG
}
FallBack "Diffuse"
}
下图1.3 屏幕截图是我们的着色器使用球体贴图(或 Zbrush 中称为的 MatCaps)展示的结果:
图1.3
1.3、实现原理...
这个光照模型的真正魔力实际上是发生在 vert() 函数中,当我们通过将 旋转的切线向量与逆转置模型视方向矩阵 相乘来为 o.tan1 和 o.tan2 分配一个新向量时。此计算实际上是弯曲矢量,它们可以以正确的方式来查找球体贴图。那么逆转置模型视方向矩阵从何而来呢?这是 Unity 为我们提供的另一个内置值,因此我们不必自己进行计算。
Unity 实际上为我们提供了通常在标准 CGFX 着色器中看到的最常见的转换矩阵。这是使用 Surface Shaders 的好处之一,我们不必自己编写这些位置转换。我们只需调用 built-in 参数。
但是为什么我们需要使用这种特定的顶点变换呢?了解这些矩阵的工作原理绝对超出了本书的范围,因为这并不是一本关于为什么对象在实时引擎中渲染到屏幕上的繁重数学的书,但简单的解释是我们需要获取对象空间中的顶点并将它们转换为世界空间。 因此,我们可以相应地将 Sphere 贴图映射到我们的 Surface 上。尝试将其视为更改与模型的空间关系。
由逆转置模式视方向和旋转切线法线相乘生成的向量,如以下图1.4 屏幕截图所示。我们使用这些向量在 Sphere 贴图纹理中进行值的查找:
图1.4
最后,我们只需使用值 IN.tan1 和 IN.tan2 作为球体贴图纹理查找的 UV 值即可完成我们的着色器。我们可以使用 Input 结构体中的这些值,主要是因为我们用 vert() 函数中的数据填充了它们。
这是实现复杂光照情况的一种简单但视觉上吸引人的方法。使用这种技术的唯一缺点是光照不会根据真实光照进行更新。照明始终锁定在摄像机的正向视方向,差不多就像纹理被投影到视图中的对象上一样。
1.4、另请参阅
与往常一样,Internet 是查找有关许多主题的更多信息的绝佳资源。我们提供了一些链接,这些链接将为您提供有关球体贴图和 Lit Sphere 着色模型的更多信息和培训。
在 Cg 教程在线书籍中对此有很好的解释:
The Cg Tutorial - Chapter 4. Transformationshttp://http.developer.nvidia.com/CgTutorial/cg_tutorial_chapter04.html 你可以在这里找到关于所有内置变换矩阵的更多信息:
http://docs.unity3d.com/Documentation/Components/SLBuiltinStateInPrograms.htmlhttp://docs.unity3d.com/Documentation/Components/SLBuiltinStateInPrograms.html 有关 MaCrea 中反射的信息,请访问:
http://vimeo.com/14189456http://vimeo.com/14189456 有关 MaCrea 中细胞着色的信息,请访问:
http://vimeo.com/14033777http://vimeo.com/14033777
第2节. 漫反射卷积光照模型
漫反射卷积是对立方体贴图进行模糊处理的过程,这样可以保留立方体贴图中照明的整体强度,但会模糊掉细节。当您想要获得更全局照明的表面时,这种类型的技术非常有用。你可以通过捕获场景的立方体贴图并通过漫反射卷积算法运行它,然后使用卷积立方体贴图照亮模型,来伪造全局照明的效果。
我们将了解如何通过 Surface Shader 在 Unity 中使用这项技术。我们还将利用 CubeMapGen 来生成漫反射卷积立方体贴图。
2.1、准备工作
为了实现这项技术,我们需要能够创建一个已经卷积的 Cubemap。有几种方法可以做到这一点,但我们将重点介绍使用 ATI 的 CubeMapGen。您可以使用以下链接从他们的网站下载此工具:
下图2.1 显示了 CubeMapGen 用户界面以及加载到程序中的 Cubemap:
图2.1
让我们来看看制作卷积立方体贴图的过程:
- 1. 启动 CubeMapGen 并加载应用程序附带的其中一个立方体贴图。它们将位于 CubeMapGen 的安装目录中。
- 2. 将 Cubemap 加载到工具中后,我们需要对其进行过滤,也就是说对它进行卷积或模糊处理。因此,我们需要进入用户界面的蓝色部分,将 Filter Type 设置为 Gaussian,将 Base Filter Angle 值改为 72.00,将 Mip Initial Filter Angle 值改为 7.60,将 Mip Filter Angle Scale 值改为 2.02,并将 Edge fix 设置为 4。然后点击界面蓝色部分底部的过滤立方体贴图按钮。这会需要一点时间,最终你应该会得到类似于以下图2.2 屏幕截图的效果:
图2.2
- 3. 一旦 CubeMapGen 完成了过滤过程,您就可以通过点击用户界面绿色部分中的 Save Cubemap to Images 按钮,将 Cubemap 保存到每一个单独的面中。这会为你创建 Cubemap 的每个面。然后,我们可以拿到这些图像并在 Unity 中构建一个新的立方体贴图。
- 4. 现在我们已经完成了立方体贴图的创建,我们需要创建一个场景来实现我们的着色器。因此,创建一个新场景,并在场景中放置一些游戏对象一盏平行光。我们还需要创建一个新的材质球和一个新的 Shader。
2.2、如何实现...
生成所有资产后,我们现在可以利用卷积立方体贴图来完成着色器每一步的创建。
- 步骤1. 像以往一样,让我们创建一些属性,让艺术家能够与我们的 Shader 交互,以便他们能够按照自己认为合适的方式对其进行调整。
Properties
{
_MainTint("Diffuse Tint", color) = (1,1,1,1)
_NormalMap("Normal Map", 2D) = "bump"{}
_AOMap("Ambient Occlusion Map", 2D) = "white"{}
_CubeMap("Diffuse Convolution CubeMap", cube) = ""{}
_SpecIntensity("Specular Intensity", Range(0,1)) = 0.4
_SpecWidth("Specular Width", Range(0,1)) = 0.2
}
- 步骤2. 然后我们需要声明我们的 #pragma 语句。在本例中,由于我们希望使用 Cubemap 来照亮场景中的物体,而不是使用平行光来照亮物体。因此我们需要创建一个新的光照模型。除此之外,我们还需要声明 3.0 目标着色模型,这样我们就不会遇到纹理插值错误的问题。
CGPROGRAM
#pragma surface surf DiffuseConvolution
#pragma target 3.0
- 步骤3. 为了能够访问来自属性的数据,我们需要为每个属性声明相应的变量,在 Properties 块和 SubShader 块之间创建链接。输入以下代码以创建此链接:
// 将属性链接到CG程序
samplerCUBE _CubeMap;
sampler2D _NormalMap;
sampler2D _AOMap;
half4 _MainTint;
float _SpecIntensity;
float _SpecWidth;
- 步骤4. 这次我们的 Input 结构将非常简单,因为我们只需要模型中的世界法线。我们会用到 INTERNAL_DATA 语句,因为我们会在 Shader 中包含法线贴图,它会为我们提供修改后的法线。
// 确保在struct中获得纹理的uv
struct Input
{
float2 uv_AOMap;
float3 worldNormal;
INTERNAL_DATA
};
- 步骤5. 我们的下一个任务就开始为光照模型创建结构。由于我们还需要为 Shader 创建一个简单的镜面反射,因此我们需要包含视方向。
inline half4 LightingDiffuseConvolution(SurfaceOutput s, half3 lightDir, half3 viewDir, half atten)
{
}
- 步骤6. 如果我们在实际中没有使用计算光照的东西来填充光照需要的向量,那么我们的光照功能就不会很好。因此,我们就按顺序排列所有的光照向量:
// 为光照获取所有的向量
viewDir = normalize(viewDir);
lightDir = normalize(lightDir);
s.Normal = normalize(s.Normal);
float NdotL = dot(s.Normal, lightDir);
float3 halfVec = normalize(lightDir + viewDir);
- 步骤7. 然后我们需要处理我们的 Specular 组件。
// 计算高光
float spec = pow(dot(s.Normal, halfVec), s.Specular * 120.0) * s.Gloss;
- 步骤8. 最后,我们将所有计算组合在一起,形成我们的光照模型:
half4 c;
c.rgb = (s.Albedo * atten) + spec;
c.a = 1.0f;
return c;
- 步骤9. 完成光照模型后,我们现在可以简单地处理纹理,使用模型的世界法线对卷积立方体贴图进行采样,并将结果传递给 SurfaceOutput 结构:
void surf (Input IN, inout SurfaceOutput o)
{
half4 c = tex2D(_AOMap, IN.uv_AOMap);
float3 normals = UnpackNormal(tex2D(_NormalMap, IN.uv_AOMap)).rgb;
o.Normal = normals;
float3 diffuseVal = texCUBE(_CubeMap, WorldNormalVector(IN, o.Normal)).rgb;
o.Albedo = (c.rgb * diffuseVal) * _MainTint;
o.Specular = _SpecWidth;
o.Gloss = _SpecIntensity * c.rgb;
o.Alpha = c.a;
}
- 完整代码:
Shader "CookbookShaders/5-2 The Diffuse Convolution Lighting Model"
{
Properties
{
_MainTint("Diffuse Tint", color) = (1,1,1,1)
_NormalMap("Normal Map", 2D) = "bump"{}
_AOMap("Ambient Occlusion Map", 2D) = "white"{}
_CubeMap("Diffuse Convolution CubeMap", cube) = ""{}
_SpecIntensity("Specular Intensity", Range(0,1)) = 0.4
_SpecWidth("Specular Width", Range(0,1)) = 0.2
}
SubShader
{
Tags { "RenderType"="Opaque" }
CGPROGRAM
#pragma surface surf DiffuseConvolution
#pragma target 3.0
// 将属性链接到CG程序
samplerCUBE _CubeMap;
sampler2D _NormalMap;
sampler2D _AOMap;
half4 _MainTint;
float _SpecIntensity;
float _SpecWidth;
// 确保在struct中获得纹理的uv
struct Input
{
float2 uv_AOMap;
float3 worldNormal;
INTERNAL_DATA
};
inline half4 LightingDiffuseConvolution(SurfaceOutput s, half3 lightDir, half3 viewDir, half atten)
{
// 为光照获取所有的向量
viewDir = normalize(viewDir);
lightDir = normalize(lightDir);
s.Normal = normalize(s.Normal);
float NdotL = dot(s.Normal, lightDir);
float3 halfVec = normalize(lightDir + viewDir);
// 计算高光
float spec = pow(dot(s.Normal, halfVec), s.Specular * 120.0) * s.Gloss;
half4 c;
c.rgb = (s.Albedo * atten) + spec;
c.a = 1.0f;
return c;
}
void surf (Input IN, inout SurfaceOutput o)
{
half4 c = tex2D(_AOMap, IN.uv_AOMap);
float3 normals = UnpackNormal(tex2D(_NormalMap, IN.uv_AOMap)).rgb;
o.Normal = normals;
float3 diffuseVal = texCUBE(_CubeMap, WorldNormalVector(IN, o.Normal)).rgb;
o.Albedo = (c.rgb * diffuseVal) * _MainTint;
o.Specular = _SpecWidth;
o.Gloss = _SpecIntensity * c.rgb;
o.Alpha = c.a;
}
ENDCG
}
FallBack "Diffuse"
}
使用世界法线,我们可以在卷积立方体贴图中查找颜色,从而为模型提供非常逼真的外观。
我们的漫反射卷积着色器的渲染结果可以在以下图2.3 屏幕截图中看到:
图2.3
2.3、实现原理...
另一种简单但视觉上令人惊叹的技术是漫反射卷积技术。虽然它比球体贴图方法更具交互性,但光照仍然是被锁定在单个立方体贴图上。你可以实时更新 Cubemap 来对您周围的环境进行采样,但卷积 Cubemap 的过程计算成本太高,无法实时来完成。不过不用担心,这就是 Unity 为我们提供光照探针技术的原因。它允许我们在环境中放置点,并且对进入每个点的卷积环境光来进行采样。这通常称为环境立方体着色。
但是,此 Shader 非常适合放在没有太多运动或与灯光交互的小场景中使用。这通常被称为基于图像的照明,因为我们使用的是 Cubemap 图像来照亮场景中的物体,而不是光源本身。此技术非常适合游戏中的过场动画,甚至于还可以应用在车辆自定义的屏幕上。
这只是通过简单的获取模型的法线信息之后再去修改法线贴图;并使用该数据在立方体贴图中查找位置信息,以检索其像素颜色来实现的。这就是为什么我们必须在 Input 结构体中声明 float3、worldNormal 和 INTERNAL_DATA 参数的原因。然后,我们必须使用 Unity 提供给我们的 WorldNormalVector() 函数来获取 texCUBE() 查找最终的法线向量。到目前为止,Shader 的其余部分对我们来说已经非常熟悉了。
在下 图2.4 的屏幕截图中,我们可以看到世界法线是如何从周围的立方体贴图中查找到它应该是什么颜色的:
图2.4
2.4、更多内容
有关在 Unity 中使用光照探针获取环境立方体贴图的更多信息。请参阅如下链接:
Unity - Manual: Light Probeshttp://docs.unity3d.com/Documentation/Manual/LightProbes.html
2.5、另请参阅
如果需要复习,请记得参考第 4 章 Reflecting Your World 中的在 Unity3D 中创建立方体贴图配方。链接如下:
第3节. 车漆光照模型
Car 着色器或者称为 vehicle 着色器是我们最常使用的 Shader 效果之一。这里涵盖了我们在前几章中到目前为止介绍的许多技术,但现在我们会先来复盘一下我们学习的所有新知识。这会为我们的 Car 着色器奠定了坚实基础,它可以适用于你的任何车型。这绝对是我们编写的更高级的着色器之一,也是最长的着色器之一,但我们会将一步一个脚印的来完成它,并解释每个基本原理。
3.1、准备工作
让我们准备一个新场景和一些我们需要用到的游戏物体,以便创建我们的车漆光照模型。
- 1. 我们首先需要一些东西进行着色,因此我们需要在新场景中创建一个新对象。最好是有一个平面作为地平面用来投射场景中物体的阴影。所以建议在我们的场景中也放置一个平面。
- 2. 为了编写 Shader,我们需要一个新的 Shader 和一个新的材质球。所以,现在让我们创建它们,并将它们赋予到我们的主对象上,在本例中主对象是一个球体。
- 3. 为了实现这个特定的车辆着色器,我们还需要创建一个 BRDF 纹理。如果你还记得 BRDF 的部分,我们只需要创建一个颜色有一些变化的纹理,它表示模型上的不同查看方向。简单地说,我们需要漫反射光的颜色和视方向灯光。以下图2.5 屏幕截图是此车辆着色器中使用的纹理示例:
图2.5
- 4. 准备着色器的最后一步是创建立方体贴图。请记住第 4 章 第四章:Reflecting Your World ,我们可以使用 generate Cubemap 脚本从 Unity 中的场景生成 Cubemap。我们现在就开始吧。
3.2、如何实现...
准备好所有资产后,我们可以开始构建我们的 Shader。我们会先运行这个 Shader 里的代码,然后再分解并且描述 Shader 中每个组件。那么接下来就让我们开始创建着色器吧!
- 步骤1. 第一步是创建我们需要的属性。在这个 Shader 中有很多的属性,但我们会在下一小节中解释它们的作用。虽然有一些属性我们现在已经很熟悉了,但在后边我们仍然会去进行解释。
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.01, 30)) = 3
_ReflCube("Reflection Cube", CUBE) = ""{}
_BRDFTex("BRDF Texture", 2D) = "white"{}
_DiffusePower("Diffuse Power", Range(0.01, 10)) = 0.5
_FalloffPower("Falloff Power", Range(0.01, 10)) = 3
_ReflAmount("Reflection Amount", Range(0.01, 1.0)) = 0.5
_ReflPower("Reflection Power", Range(0.01, 3.0)) = 2.0
}
- 步骤2. 对于此着色器,我们会创建自己的光照模型,并把它命名为 CarPaint ,因此首先我们需要在 Shader 的 #pragma 语句中声明:
CGPROGRAM
#pragma surface surf CarPaint
- 步骤3. 为了能够访问属性中的所有数据,我们还需要在 SubShader 块中声明它们。请参阅以下代码片段:
// 将属性链接到CG程序
samplerCUBE _ReflCube;
sampler2D _MainTex;
sampler2D _BRDFTex;
half4 _MainTint;
half4 _SpecularColor;
float _SpecPower;
float _DiffusePower;
float _FalloffPower;
float _ReflAmount;
float _ReflPower;
- 步骤4. 此时,我们可以开始处理我们的照明模型。对于此 Shader,我们需要创建大量数据,因此在实际将以下代码写入你的 Shader 之前,请通读以下代码片段几次。以便更好的吸收它们:
inline half4 LightingCarPaint(SurfaceOutput s, half3 lightDir, half3 viewDir, half atten)
{
half3 h = normalize(lightDir + viewDir);
half diff = max(0, dot(s.Normal, lightDir));
float ahdn = 1 - dot(h, normalize(s.Normal));
ahdn = pow(clamp(ahdn, 0.0, 1.0), _DiffusePower);
half4 brdf = tex2D(_BRDFTex, float2(diff, 1-ahdn));
float nh = max(0, dot(s.Normal, h));
float spec = pow(nh, s.Specular * _SpecPower) * s.Gloss;
half4 c;
c.rgb = (s.Albedo * _LightColor0.rgb * brdf.rgb + _LightColor0.rgb * _SpecularColor.rgb * spec) * (atten * 2);
c.a = s.Alpha + _LightColor0.a * _SpecularColor.a * spec * atten;
return c;
}
- 步骤5. 让我们将注意力转向 Shader 中的 Input 结构体并添加以下代码。这会让我们制作出在上一章中所学到的菲涅耳效果。
// 确保在struct中获得纹理的uv
struct Input
{
float2 uv_MainTex;
float3 worldRefl;
float3 viewDir;
};
- 步骤6. 最后,我们的 surf() 函数,它可以执行大部分的逐像素计算。在这里,我们将为我们的 car paint 着色器创建最终效果:
void surf (Input IN, inout SurfaceOutput o)
{
half4 c = tex2D(_MainTex, IN.uv_MainTex);
float falloff = saturate(1-dot(normalize(IN.viewDir), o.Normal));
falloff = pow(falloff, _FalloffPower);
o.Albedo = c.rgb * _MainTint;
o.Emission = pow((texCUBE(_ReflCube, IN.worldRefl).rgb * falloff), _ReflPower) * _ReflAmount;
o.Specular = c.r;
o.Gloss = 1.0;
o.Alpha = c.a;
}
- 完整代码:
Shader "CookbookShaders/5-3 Creating a Vehicle Paint Lighting Model"
{
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.01, 30)) = 3
_ReflCube("Reflection Cube", CUBE) = ""{}
_BRDFTex("BRDF Texture", 2D) = "white"{}
_DiffusePower("Diffuse Power", Range(0.01, 10)) = 0.5
_FalloffPower("Falloff Power", Range(0.01, 10)) = 3
_ReflAmount("Reflection Amount", Range(0.01, 1.0)) = 0.5
_ReflPower("Reflection Power", Range(0.01, 3.0)) = 2.0
}
SubShader
{
Tags { "RenderType"="Opaque" }
CGPROGRAM
#pragma surface surf CarPaint
// 将属性链接到CG程序
samplerCUBE _ReflCube;
sampler2D _MainTex;
sampler2D _BRDFTex;
half4 _MainTint;
half4 _SpecularColor;
float _SpecPower;
float _DiffusePower;
float _FalloffPower;
float _ReflAmount;
float _ReflPower;
inline half4 LightingCarPaint(SurfaceOutput s, half3 lightDir, half3 viewDir, half atten)
{
half3 h = normalize(lightDir + viewDir);
half diff = max(0, dot(s.Normal, lightDir));
float ahdn = 1 - dot(h, normalize(s.Normal));
ahdn = pow(clamp(ahdn, 0.0, 1.0), _DiffusePower);
half4 brdf = tex2D(_BRDFTex, float2(diff, 1-ahdn));
float nh = max(0, dot(s.Normal, h));
float spec = pow(nh, s.Specular * _SpecPower) * s.Gloss;
half4 c;
c.rgb = (s.Albedo * _LightColor0.rgb * brdf.rgb + _LightColor0.rgb * _SpecularColor.rgb * spec) * (atten * 2);
c.a = s.Alpha + _LightColor0.a * _SpecularColor.a * spec * atten;
return c;
}
// 确保在struct中获得纹理的uv
struct Input
{
float2 uv_MainTex;
float3 worldRefl;
float3 viewDir;
};
void surf (Input IN, inout SurfaceOutput o)
{
half4 c = tex2D(_MainTex, IN.uv_MainTex);
float falloff = saturate(1-dot(normalize(IN.viewDir), o.Normal));
falloff = pow(falloff, _FalloffPower);
o.Albedo = c.rgb * _MainTint;
o.Emission = pow((texCUBE(_ReflCube, IN.worldRefl).rgb * falloff), _ReflPower) * _ReflAmount;
o.Specular = c.r;
o.Gloss = 1.0;
o.Alpha = c.a;
}
ENDCG
}
FallBack "Diffuse"
}
下图2.6 屏幕截图显示了在 Unity3D 中使用球体测试汽车油漆着色器的结果:
图2.6
3.3、实现原理...
当我们把车漆着色器分解成多个组件时,实际上它是非常简单的。我们已经在 第一章:漫反射着色器-Diffuse Shading 和 第三章:镜面反射让你的游戏闪光 - Making Your Game Shine with Specular 中介绍了每个组件。希望大家现在都明白了,但让我们来回顾一下广义的概念。
我们利用 第一章:漫反射着色器-Diffuse Shading 中的 BRDF 技术来创建车漆的双色调外观。现在,并非每辆车都有这个功能,所以这取决于你。因此这会由着色器程序员需要根据项目需求来决定使用 BRDF 纹理还是其他形式的漫反射组件。
最后,我们简单地计算一个菲涅耳项和一个衰减分量,它们达成了车表面上看到的反射率。所有这些光照组件都由 Properties 块中的属性驱动;因此,艺术家通过调节这些属性可以控制车辆 Shader 的最终外观。
3.4、更多内容
Unity Asset Store 中也有汽车油漆着色器出售。这是其中一个的链接:
第4节. 皮肤着色器
在游戏制作过程中,皮肤着色器的需求度始终很高,也就是说,如果你的游戏制作包含具有某种有机皮肤的角色。本节会介绍一个可用于游戏项目中皮肤着色器的方法,它绝不是最准确的,但它确实起到了作用,并且可以产生一些非常好的效果。
不过,在我们开始之前,我们需要了解在我们的皮肤表面必须做什么。这些信息会为我们提供一些知识点,它可以帮助我们把 Shader 分解成组件,以便我们可以对不同的效果进行编程。
我们可以把皮肤分成四个不同的部分。这不是硬性规定,但您可以通过专注于这四个知识点来获得非常好的皮肤效果。它们如下:
- Sub-surface scattering: 这个效果的皮肤是会变得非常薄或非常清晰的,以至于把光线放到它的后边会产生颜色效果。对于皮肤而言,这通常是微红色调,来模拟暴露在外的血管。在这里,我们将会学习如何使用它的法线贴图来计算表面曲率。
- Diffuse: 正如你想象的那样,当漫反射的效果作为皮肤的时候它不仅仅是简单的灰色缩放值。我们仍然会使用法线向量点乘光向量的技术,但是我们需要利用 BRDF 技术给艺术家提供更多的控制,即当 BRDF 分布在表面上时光是如何受到影响的。
- Specular: 皮肤的镜面反射是非常棘手的,因为它受表面油性程度的控制。我们仍然可以利用到目前为止学到的镜面反射技巧,但我们希望添加菲涅耳和边缘光照明技术来控制镜面反射的放置位置。这会以更逼真的方式分布高光。我们还可以使用查找纹理来控制高光的形状,但在本小节中我们会来实现一个基本的高光,因为我们在之前的章节中介绍了如何进行高光查找。
- Blurred normals: 许多游戏中的皮肤着色器看起来不是那么逼真或者塑料感太强的原因,是因为法线贴图中的法线信息读取的细节级别非常的高,但这对于 Specular (镜面反射) 组件来说是非常好的,因为我们想要捕获所有的细节。但是当我们谈及到皮肤的漫反射成分时,我们需要颜色的过度非常柔和。
4.1、准备工作
所以,现在让我们准备场景并收集一些资源,方便我们在实现 Shader 的不同组件时使用。
- 1. 创建一个新的场景、着色器和材质球。确保将 Shader 赋予到 材质球上,并将材质球赋予给您的游戏物体上。如果能准备一个头部的模型是最好的,但是如果没有,也没关系,你仍然可以继续使用球体,就像我们在前几章中一直使用的那样。
- 2. 我们还需要一张 BRDF 纹理来计算漫反射颜色。本书附带了一个 BRDF 纹理,位于本书的支持页面上,网址为 http:// www.packtpub.com/support。BRDF 纹理需要模拟不同肤色的皮肤展示出的颜色。在这里,我们会以模拟白种人的皮肤为例,使的 BRDF 纹理如下 图4.1 所示:
图4.1
4.2、如何实现...
现在,我们来看看如何构建 Shader。我们将逐步介绍每个代码块,然后在下一节中解释关键概念。
- 步骤1. 首先,我们需要填充 Properties 块以设置不同的可调整属性,并为我们提供一种将纹理传递给 Shader 的方法。我们现在开始在 Shader 中创建一些属性。关于属性的数量也可能会是你与艺术家们沟通的重点,如果纹理可以打包的话,你就不用创建大量的滑块了,但就我们的目前要做的来讲,现有的就足够了。
Properties
{
_MainTint("Diffuse Tint", color) = (1,1,1,1)
_MainTex("Main Map", 2D) = "white"{}
_NormalMap("Normal Map", 2D) = "bump"{}
_CurveScale("Curvature Scale", Range(0.001, 0.09)) = 0.01
_CurveAmount("Curvature Amount", Range(0, 1)) = 0.5
_NormalBias("Normal Map Blur", Range(0, 5)) = 2.0
_BRDF("BRDF Ramp", 2D) = "white"{}
_FresnelVal("Fresnel Amount", Range(0.01, 0.3)) = 0.05
_RimPower("Rim Falloff", Range(0, 5)) = 2
_RimColor("Rim Color", color) = (1,1,1,1)
_SpecIntensity("Specular Intensity", Range(0,1)) = 0.4
_SpecWidth("Specular Width", Range(0, 1)) = 0.2
}
- 步骤2. 接着我们需要声明一些 #pragma 语句,因为这个 Shader 需要相当多的运算和 CGFX 的特定功能。为了清除任何不需要的编译错误,因此,我们需要在 SubShader 块中输入以下内容。这些将在下一节中解释:
CGPROGRAM
#pragma surface surf SkinShader
#pragma target 3.0
#pragma only_renderers d3d9
- 步骤3. 我们的 Shader 需要访问传递到属性中的数据,以便可以在 Shader 中使用属性中的数值。因此,我们需要在 SubShader 块中声明相应的属性名称。
// 将属性链接到CG程序
sampler2D _MainTex;
sampler2D _NormalMap;
sampler2D _BRDF;
half4 _MainTint;
half4 _RimColor;
float _CurveScale;
float _NormalBias;
float _CurveAmount;
float _FresnelVal;
float _RimPower;
float _SpecIntensity;
float _SpecWidth;
- 步骤4. 为了让我们完全利用表面着色器的强大功能,我们需要声明自己的 SurfaceOutput 结构体。这允许我们在自定义光照函数和 Surface 函数之间来回传递数据。如果我们只使用内置的 SurfaceOutput 结构,我们将无法将模糊的法线以及曲率值传递给我们的光照函数,曲率值是按像素级别计算的。
struct SurfaceOutputSkin
{
half3 Albedo;
half3 Normal;
half3 Emission;
half3 Specular;
half Gloss;
half Alpha;
float Curvature;
half3 BlurredNormals;
};
- 步骤5. 要完成 Shader 的基本结构,我们需要声明 Input 结构体,并用一些有用的内置数据填充它。在这种情况下,我们需要模型每个顶点的世界位置以及世界法线,由于我们为此着色器使用了法线贴图,因此我们必须声明 INTERNAL_DATA 这行代码,以便在将法线贴图应用于表面后来获取法线。
// 确保在struct中获得纹理的uv
struct Input
{
float2 uv_MainTex;
float3 worldPos;
float3 worldNormal;
INTERNAL_DATA
};
- 步骤6. 设置完所有数据后,我们现在可以开始编写自定义光照函数了。我们首先声明名为 LightingSkinShader() 的光照模型函数。
inline half4 LightingSkinShader(SurfaceOutputSkin s, half3 lightDir, half3 viewDir, half atten)
{
}
- 步骤7. 现在,我们可以用适当的计算来填充光照模型,来生成皮肤的光照。首先,我们将按顺序获取所有向量并归一化,以便我们处理单位向量。确保下边代码位于 lighting model 函数中。
// 为光照获取所有的向量
viewDir = normalize(viewDir);
lightDir = normalize(lightDir);
s.Normal = normalize(s.Normal);
float NdotL = dot(s.Normal, lightDir);
float3 halfVec = normalize(lightDir + viewDir);
- 步骤8. 向量计算准备完成之后,我们可以生成 BRDF 纹理查找的值。
// 计算BRDF和伪SSS
float3 brdf = tex2D(_BRDF, float2((NdotL *0.5+0.5) * atten, s.Curvature)).rgb;
- 步骤9. 接下来是我们的菲涅耳和边缘照明组件。
// 创建菲涅尔和边缘光
float fresnel = saturate(pow(1 - dot(viewDir, halfVec), 5.0));
fresnel += _FresnelVal *(1-fresnel);
float rim = saturate(pow(1- dot(viewDir, s.BlurredNormals), _RimPower)) * fresnel;
- 步骤10. 然后,我们创建 Specular 组件,就像我们在 第3章:Making Your Game Shine with Specular 中所做的那样。
// 创建高光
float specBase = max(0, dot(s.Normal, halfVec));
float spec = pow(specBase, s.Specular * 128.0) * s.Gloss;
- 步骤11. 完成光照模型的所有计算后,我们现在可以将它们组合起来,并将结果传递给表面函数。
// 计算最终颜色
half4 c;
c.rgb = (s.Albedo * brdf * _LightColor0.rgb * atten) + (spec + (rim * _RimColor));
c.a = 1.0;
return c;
- 步骤12. 最后,我们进入 surf() 函数,在这里我们获取所有的纹理信息,计算模糊的法线,并根据法线贴图为我们的模型生成曲率值。
void surf (Input IN, inout SurfaceOutputSkin o)
{
// 得到我们的贴图信息
half4 c = tex2D(_MainTex, IN.uv_MainTex);
half3 normals = UnpackNormal(tex2D(_NormalMap, IN.uv_MainTex));
float3 normalBlur = UnpackNormal(tex2Dbias(_NormalMap, float4(IN.uv_MainTex, 0.0, _NormalBias)));
// 计算曲率
float curvature = length(fwidth(WorldNormalVector(IN, normalBlur)) / (length(fwidth(IN.worldPos)) * _CurveScale));
// 把我们的所有信息应用到 SurfaceOutput
o.Normal = normals;
o.BlurredNormals = normalBlur;
o.Albedo = c.rgb * _MainTint;
o.Curvature = curvature;
o.Specular = _SpecWidth;
o.Gloss = _SpecIntensity;
o.Alpha = c.a;
}
- 完整代码:
Shader "CookbookShaders/5-4 Skin Shader"
{
Properties
{
_MainTint("Diffuse Tint", color) = (1,1,1,1)
_MainTex("Main Map", 2D) = "white"{}
_NormalMap("Normal Map", 2D) = "bump"{}
_CurveScale("Curvature Scale", Range(0.001, 0.09)) = 0.01
_CurveAmount("Curvature Amount", Range(0, 1)) = 0.5
_NormalBias("Normal Map Blur", Range(0, 5)) = 2.0
_BRDF("BRDF Ramp", 2D) = "white"{}
_FresnelVal("Fresnel Amount", Range(0.01, 0.3)) = 0.05
_RimPower("Rim Falloff", Range(0, 5)) = 2
_RimColor("Rim Color", color) = (1,1,1,1)
_SpecIntensity("Specular Intensity", Range(0,1)) = 0.4
_SpecWidth("Specular Width", Range(0, 1)) = 0.2
}
SubShader
{
Tags { "RenderType"="Opaque" }
CGPROGRAM
#pragma surface surf SkinShader
#pragma target 3.0
#pragma only_renderers d3d9
// 将属性链接到CG程序
sampler2D _MainTex;
sampler2D _NormalMap;
sampler2D _BRDF;
half4 _MainTint;
half4 _RimColor;
float _CurveScale;
float _NormalBias;
float _CurveAmount;
float _FresnelVal;
float _RimPower;
float _SpecIntensity;
float _SpecWidth;
struct SurfaceOutputSkin
{
half3 Albedo;
half3 Normal;
half3 Emission;
half3 Specular;
half Gloss;
half Alpha;
float Curvature;
half3 BlurredNormals;
};
// 确保在struct中获得纹理的uv
struct Input
{
float2 uv_MainTex;
float3 worldPos;
float3 worldNormal;
INTERNAL_DATA
};
inline half4 LightingSkinShader(SurfaceOutputSkin s, half3 lightDir, half3 viewDir, half atten)
{
// 为光照获取所有的向量
viewDir = normalize(viewDir);
lightDir = normalize(lightDir);
s.Normal = normalize(s.Normal);
float NdotL = dot(s.Normal, lightDir);
float3 halfVec = normalize(lightDir + viewDir);
// 计算BRDF和伪SSS
float3 brdf = tex2D(_BRDF, float2((NdotL *0.5+0.5) * atten, s.Curvature)).rgb;
// 创建菲涅尔和边缘光
float fresnel = saturate(pow(1 - dot(viewDir, halfVec), 5.0));
fresnel += _FresnelVal *(1-fresnel);
float rim = saturate(pow(1- dot(viewDir, s.BlurredNormals), _RimPower)) * fresnel;
// 创建高光
float specBase = max(0, dot(s.Normal, halfVec));
float spec = pow(specBase, s.Specular * 128.0) * s.Gloss;
// 计算最终颜色
half4 c;
c.rgb = (s.Albedo * brdf * _LightColor0.rgb * atten) + (spec + (rim * _RimColor));
c.a = 1.0;
return c;
}
void surf (Input IN, inout SurfaceOutputSkin o)
{
// 得到我们的贴图信息
half4 c = tex2D(_MainTex, IN.uv_MainTex);
half3 normals = UnpackNormal(tex2D(_NormalMap, IN.uv_MainTex));
float3 normalBlur = UnpackNormal(tex2Dbias(_NormalMap, float4(IN.uv_MainTex, 0.0, _NormalBias)));
// 计算曲率
float curvature = length(fwidth(WorldNormalVector(IN, normalBlur)) / (length(fwidth(IN.worldPos)) * _CurveScale));
// 把我们的所有信息应用到 SurfaceOutput
o.Normal = normals;
o.BlurredNormals = normalBlur;
o.Albedo = c.rgb * _MainTint;
o.Curvature = curvature;
o.Specular = _SpecWidth;
o.Gloss = _SpecIntensity;
o.Alpha = c.a;
}
ENDCG
}
FallBack "Diffuse"
}
将所有组件组合在 Surface Shader 中后,我们的皮肤着色器应如以下图4.2 屏幕截图所示:
图4.2
4.3、实现原理...
对于大多数 Shader,我们已经看到了如何实现其组件,但有一些组件对我们来说是新的。首先,我们声明了一个名为 SurfaceOutputSkin 的新型结构体,虽然我们之前已经看到过,但我们将在这里再次介绍它。
SurfaceOutputSkin 结构体是我们自己自定义的结构体,lighting 函数和 surf() 函数都可以使用它把数据从 surf() 函数传递到 lighting 函数中。把它想象成一种运输。当我们给 surf() 函数的结构体赋值时,它们会存储在该结构体里的变量中。然后,我们可以在 lighting 函数中使用该数据来执行更多的逐像素光照。
下一个对我们来说是一个新的组件,即曲率计算。我们基本上是在测量表面法线之间的变化量。因此,随着表面曲率的变化,该曲面上法线之间的角度也会发生变化。我们可以使用这些数据来查找曲率最高的区域,并从计算中获得黑白值。
这个计算介绍了两个新的内置 CGFX 函数,它们将为我们返回必要的数据,以找到表面曲率的这种变化。第一个是 fwidth() 函数。在我们的 Shader 中,你会注意到我们发送了一个向量作为 fwidth() 函数的参数。这将为我们返回向量在对象表面上的变化速度。因此,我们最终得到了一个表示表面曲率的向量。以下是 Cg 标准库描述的链接:http://http.developer.nvidia.com/ Cg/fwidth.html。
使用 Cg 标准函数中的 fwidth() 函数,我们可以获取有关模型表面曲率的信息。
图4.3
然后我们不需要完整的向量;我们只想在每个像素的基础上找到它的大小。因此,我们可以使用 length() 函数,因为这将为我们返回向量的长度作为浮点值。以下是 Cg 标准库中 length 函数的描述:http://http.developer.nvidia.com/Cg/length.html。
使用 length() 函数,我们可以找出每像素曲率向量的大小,并获得一个浮点值,该值将决定我们如何查找 BRDF 纹理。
图4.4
处理完这些数据后,我们只需将两个浮点数相除,然后将结果乘以另一个 float 值(从 CurveScale 属性传递给我们),这样就可以控制曲率效果的强度。
我们在皮肤着色器中完成曲率计算的最终结果如以下 图4.5 屏幕截图所示:
图4.5
最终,我们使用的最后一个新函数是 tex2Dbias() 函数,它可以让我们在皮肤上获得漂亮柔和的漫反射光照。这允许我们使用一个属性将当前 mip 级别偏移或移动到较低或较高的 mip 级别,以允许控制纹理的模糊程度。这并不是说我们逐像素地模糊纹理,实际上我们只是从纹理中选择较低的 mip 级别。有关 mip 映射和生成它们的更多信息,请参阅 Unity 参考的此链接:http://docs.unity3d.com/Documentation/Manual/Textures.html。
4.4、更多内容
皮肤着色器的这种特殊实现的灵感来自 Web 上找到的几个着色器。所以说,在此处提及它们是比较合适的:
- Unity forums:
- Skin Shader 3:
第5节. 布料着色器
布料是另一个非常常见的着色任务,需要在制作游戏或对实时交互式体验进行着色的过程中完成。它涉及了解布料的纤维如何将光照分散到表面上以产生布料外观的。它也非常依赖于视方向,因此我们会研究一些可以采用的新技巧来伪造光线掠过布料表面的效果,以及产生非常独特的边缘照明效果的微小纤维。
此着色器将向我们介绍了细节法线贴图和细节纹理的概念。通过将两个法线贴图组合在一起,我们实际上就可以实现来储存更高级别的细节,即把它储在 2048 x 2048 纹理中。该技术会帮助我们模拟表面中微小的凹凸,从而使我们能够将镜面反射组件分散到较宽的表面上。
在这里,我们可以看到如下 图5.1 所示,在本小节中制作的最终布料着色器的截图效果:
图5.1
5.1、准备工作
这个 Shader 需要我们去收集三种不同类型的纹理,用来模拟类似布料的表面。
- 我们需要准备一张 Detail Normal (细节法线) 贴图。此贴图将平铺在表面上,以模拟布料中的微小间线(车缝)。
- 我们需要一张 Normal Variation 贴图,它将在拼接缝中提供一个很好的变化,使其感觉不那么均匀,更像是多年来的磨损。
- 最后,我们需要一个 Detail Diffuse (细节漫反射) 贴图,我们可以将其乘以基础颜色,以便使漫反射更具深度和真实感,并强调布料中的缝合。
以下屏幕截图 图5.2 显示了我们将用于此小节中的三种纹理。它们也包含在本书的支持页面上,网址为 www.packtpub.com/support:
图5.2
要完成设置,我们需要创建一个包含对象和平行光的场景。然后,创建一个新的 Shader 和材质球并赋予到我们的主对象上。
5.2、如何实现...
让我们通过填写 Properties 块来启动着色器吧。
- 步骤1. 我们的着色器在这里会采用一些属性来控制所使用的纹理以及菲涅耳和高光组件的衰减效果。
Properties
{
_MainTint("Diffuse Tint", Color) = (1,1,1,1)
_BumpMap("Normal Map", 2D) = "bump"{}
_DetailBump("Detail Normal Map", 2D) = "bump"{}
_DetailTex("Fabric Weave", 2D) = "white"{}
_FresnelColor("Fresnel Color", Color) = (1,1,1,1)
_FresnelPower("Fresnel Power", Range(0, 12)) = 3
_RimPower("Rim Falloff", Range(0, 12)) = 3
_SpecIntensity("Specular Intensity", Range(0, 1)) = 0.2
_SpecWidth("Specular Width", Range(0, 1)) = 0.2
}
- 步骤2. 我们希望完全控制光照对布料表面的影响,因此需要在 #pragma 语句中声明一个新的光照模型,并将此 Shader 设置为使用 Shader model 3.0。为了创建自己的自定义光照模型,我们必须在 #pragma 语句中声明光照模型的名称。
CGPROGRAM
#pragma surface surf Velvet
#pragma target 3.0
- 步骤3. 现在,我们需要 在 Properties 块和 SubShader 块之间建立连接。为了使用 Properties 块中的数据,我们必须在 SubShader 块中声明相同的命名变量。
// 将属性链接到CG程序
sampler2D _BumpMap;
sampler2D _DetailBump;
sampler2D _DetailTex;
half4 _MainTint;
half4 _FresnelColor;
float _FresnelPower;
float _RimPower;
float _SpecIntensity;
float _SpecWidth;
- 步骤4. 要独立控制细节纹理的平铺比率,我们需要在 Input 结构体中声明 UV 参数。如果你把 uv 放在纹理的名称前面,则 UV 信息就会被连接,就会在 Input 结构中连接纹理的平铺比率:
// 确保在struct中获得纹理的uv
struct Input
{
float2 uv_BumpMap;
float2 uv_DetailBump;
float2 uv_DetailTex;
};
- 步骤5. 现在,我们来创建光照模型的函数。在创建自定义光照模型的过程之前,我们需要先创建光照函数的结构体。在此结构中我们需要为此shader输入 viewDir光照函数结构。因为布料的表面依赖 view 组件。
inline half4 LightingVelvet(SurfaceOutput s, half3 lightDir, half3 viewDir, half atten)
{
}
在光照函数开始时处理好所有光照向量始终是一个好主意。这样,你就可以放心地去考虑光照计算的的其它部分,而不必总是对向量做归一化处理。让我们在光照模型的开始位置来添加我们的光照向量吧:
// 为光照获取所有的向量
viewDir = normalize(viewDir);
lightDir = normalize(lightDir);
float3 halfVec = normalize(lightDir + viewDir);
float NdotL = max(0, dot(s.Normal, lightDir));
- 步骤6. 我们的下一个任务是计算我们的 Specular 组件。在光照向量之后添加以下代码:
// 创建高光
float NdotH = max(0, dot(s.Normal, halfVec));
float spec = pow(NdotH, s.Specular * 128.0) * s.Gloss;
布料的着色在很大程度上取决于你怎么样去查看它的表面。你的视线越瞥过(斜擦)它的表面,布料表面的反光就越多,纤维就越能捕捉到背后的光线,从而放大 Specular (镜面反射)组件的强度。
// 创建菲涅尔效果
float HdotV = pow(1 - max(0, dot(halfVec, viewDir)), _FresnelPower);
float HdotE = pow(1 - max(0, dot(s.Normal, viewDir)), _RimPower);
float finalSpecMask = HdotE * HdotV;
- 步骤7. 完成所有主要光照计算之后,我们只需要输出最终颜色即可。接下来我们在 Fresnel 计算的下方输入以下代码来完成光照模型:
// 输出最终的颜色
half4 c = half4(1,1,1,1);
c.rgb = (s.Albedo * NdotL * _LightColor0.rgb) + (spec *(finalSpecMask * _FresnelColor)) *(atten *2);
c.a = 1.0;
return c;
- 步骤8. 最后,我们通过提交 surf() 函数来完成我们的 Shader。在这里,我们只需要解压缩法线贴图,并将所有数据发送给 SurfaceOuput 结构体。接下来让我们通过对纹理进行采样来完成我们的 Shader:
void surf (Input IN, inout SurfaceOutput o)
{
float4 c = tex2D (_DetailTex, IN.uv_DetailTex);
float3 normals = UnpackNormal(tex2D(_BumpMap, IN.uv_BumpMap)).rgb;
half3 detailNormals = UnpackNormal(tex2D(_DetailBump, IN.uv_DetailBump)).rgb;
half3 finalNormals = float3(normals.x + detailNormals.x, normals.y + detailNormals.y, normals.z + detailNormals.z);
o.Normal = normalize(finalNormals);
o.Specular = _SpecWidth;
o.Gloss = _SpecIntensity;
o.Albedo = c.rgb * _MainTint;
o.Alpha = c.a;
}
- 完整代码
Shader "CookbookShaders/5-5 Cloth Shading"
{
Properties
{
_MainTint("Diffuse Tint", Color) = (1,1,1,1)
_BumpMap("Normal Map", 2D) = "bump"{}
_DetailBump("Detail Normal Map", 2D) = "bump"{}
_DetailTex("Fabric Weave", 2D) = "white"{}
_FresnelColor("Fresnel Color", Color) = (1,1,1,1)
_FresnelPower("Fresnel Power", Range(0, 12)) = 3
_RimPower("Rim Falloff", Range(0, 12)) = 3
_SpecIntensity("Specular Intensity", Range(0, 1)) = 0.2
_SpecWidth("Specular Width", Range(0, 1)) = 0.2
}
SubShader
{
Tags { "RenderType"="Opaque" }
CGPROGRAM
#pragma surface surf Velvet
#pragma target 3.0
// 将属性链接到CG程序
sampler2D _BumpMap;
sampler2D _DetailBump;
sampler2D _DetailTex;
half4 _MainTint;
half4 _FresnelColor;
float _FresnelPower;
float _RimPower;
float _SpecIntensity;
float _SpecWidth;
// 确保在struct中获得纹理的uv
struct Input
{
float2 uv_BumpMap;
float2 uv_DetailBump;
float2 uv_DetailTex;
};
inline half4 LightingVelvet(SurfaceOutput s, half3 lightDir, half3 viewDir, half atten)
{
// 为光照获取所有的向量计算
viewDir = normalize(viewDir);
lightDir = normalize(lightDir);
float3 halfVec = normalize(lightDir + viewDir);
float NdotL = max(0, dot(s.Normal, lightDir));
// 创建高光
float NdotH = max(0, dot(s.Normal, halfVec));
float spec = pow(NdotH, s.Specular * 128.0) * s.Gloss;
// 创建菲涅尔
float HdotV = pow(1 - max(0, dot(halfVec, viewDir)), _FresnelPower);
float HdotE = pow(1 - max(0, dot(s.Normal, viewDir)), _RimPower);
float finalSpecMask = HdotE * HdotV;
// 输出最终的颜色
half4 c = half4(1,1,1,1);
c.rgb = (s.Albedo * NdotL * _LightColor0.rgb) + (spec *(finalSpecMask * _FresnelColor)) *(atten *2);
c.a = 1.0;
return c;
}
void surf (Input IN, inout SurfaceOutput o)
{
float4 c = tex2D (_DetailTex, IN.uv_DetailTex);
float3 normals = UnpackNormal(tex2D(_BumpMap, IN.uv_BumpMap)).rgb;
half3 detailNormals = UnpackNormal(tex2D(_DetailBump, IN.uv_DetailBump)).rgb;
half3 finalNormals = float3(normals.x + detailNormals.x, normals.y + detailNormals.y, normals.z + detailNormals.z);
o.Normal = normalize(finalNormals);
o.Specular = _SpecWidth;
o.Gloss = _SpecIntensity;
o.Albedo = c.rgb * _MainTint;
o.Alpha = c.a;
}
ENDCG
}
FallBack "Diffuse"
}
以下屏幕截图5.3 展示了使用 Cloth 表面着色器在类似于布料的模型上渲染的结果:
图5.3
5.3、实现原理...
总的来说,我们的布料着色器并没有那么复杂。我们执行了一些非常基本的光照操作,但有时这就是着色器所需的全部内容了。当编写着色器时,你想要真正地观察表面,试图模拟并将其分解为组件,然后一次一个地对它们进行编程。真正的魔力在于你最终如何组合不同的计算,就像在 Photoshop 中混合图层一样。
我们 Cloth 着色器中演示的新技术是将两个具有不同平铺比率的法线贴图组合在一起的过程。基本线性代数表明了我们实际上可以将两个向量相加得到一个新的位置。因此,我们可以用法线贴图来做到这一点。我们采用 Variation Normal 贴图(它使用 UnpackNormal() 函数为我们提供的向量),并从 Detail Normal 贴图添加法线向量。这差不多会生成一个新的法线贴图。然后,我们需要对最终向量进行归一化,使其回到 0 到 1 的范围内。如果我们不这样做,我们的法线贴图看起来会变得不堪入目,而且在视觉上也是错误的。
最后,菲涅尔和高光计算的组合以掠射角度捕捉到对象表面的光线,并可以让我们能够创建细小的布料纤维效果。
这本书可以让你学习到如何使用着色器和屏幕特效让你的Unity工程拥有震撼的渲染画面。
作者:Kenny Lammers