首页 > 代码库 > 【Unity Shaders】Lighting Models —— 衣服着色器

【Unity Shaders】Lighting Models —— 衣服着色器

本系列主要参考《Unity Shaders and Effects Cookbook》一书(感谢原书作者),同时会加上一点个人理解或拓展。

这里是本书所有的插图。这里是本书所需的代码和资源(当然你也可以从官网下载)。

========================================== 分割线 ==========================================




写在前面


布料(Cloth)是另一种非常常见的着色需求,在很多实时游戏中都需要它来实现更真实的交互体验。它涉及到如何让布料的纤维合适地分散整个表面的光照,使它看起来像布料一样。布料的渲染非常依赖视角的变化,因此我们将学习一些新的技巧来模拟光扫射到布料上的效果,并且那些细小的纤维还能产生与众不同的边缘光照效果。


这篇将会介绍两种新的概念:细节法线贴图(Detail normal maps)和细节贴图(Detail textures)。通过把这两种法线贴图结合到一起,我们可以得到一种更高层次的细节表现,并且可以存储在一张2048*2048的贴图中。这种技术可以帮助我们模拟表面那种非常细微层次的凹凸不平的感觉,以此来分散整个表面的高光反射。


下面显示了本节最终得到的布料着色器效果:




准备工作


这个Shader需要结合3种不同类型的贴图来模拟布料效果:

  • 一张细节法线贴图(Detail Normal map)。这张贴图将会平铺在表面上来模拟细小的缝纫痕迹。
  • 一张标准变化贴图(Normal Variation map)。这张贴图将会模拟缝纫的变化,防止所有表面看起来都是一样的,而更像是有岁月磨损的样子。
  • 一张细节漫反射贴图(Detail Diffuse map)。我们使用这张贴图去乘以基本颜色来模拟布料的整体颜色,以此来为整体增加更多的深度细节和真实感,并且还能强调布料的缝纫痕迹。

下面展示了本节中需要的三张贴图。你可以在本书资源(见最上方)中找到它们。




同时,你当然还需要像以前一样,新建一个场景,一个平行光,以及一个物体(本节使用自带的布料模型)。最后,新建一个Shader和Material,并命名为ClothShader。



实现



  • 首先,老样子添加新的properties。这里主要是为了控制所有的贴图和菲涅耳以及高光反射等。
    	Properties
    	{
    		_MainTint ("Global 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
    		_SpecIntesity ("Specular Intensiity", Range(0, 1)) = 0.2
    		_SpecWidth ("Specular Width", Range(0, 1)) = 0.2	
    	}

    解释:菲涅耳反射,简单来讲,就是当你垂直观察平面时,反射很弱;但当视线与平面越小时,反射越明显。举个例子,当你站在水边观察水面时,水是透明的,反射很弱,但是当你离水面越远时,基本就看不到河面以下的部分了,反射很强。(百度百科)

  • 由于我们想要全面控制光照对布料平面的影响,因此我们需要在#pragma语句中声明新的光照模型,并且设置使用Shader model 3.0。
    		CGPROGRAM
    		#pragma surface surf Velvet
    		#pragma target 3.0


  • 现在,我们需要建立Properties块和SubShader块的联系。为了使用Properties中的各种数据,我们需要在SubShader中声明同样名字的变量。
    		sampler2D _BumpMap;
    		sampler2D _DetailBump;
    		sampler2D _DetailTex;
    		float4 _MainTint;
    		float4 _FresnelColor;
    		float _FresnelPower;
    		float _RimPower;
    		float _SpecIntesity;
    		float _SpecWidth;


  • 为了分别控制几种细节贴图的平铺率,我们需要在Input结构中声明它们的UV参数。如果你把uv放在相同的贴图名称的前面,就可以建立UV信息的联系。
    		struct Input 
    		{
    			float2 uv_BumpMap;
    			float2 uv_DetailBump;
    			float2 uv_DetailTex;
    		};


  • 现在我们需要创建我们的光照模型函数。首先需要创建光照函数结构。我们需要viewDir参数得到视角方向,这是因为布料表面是受视角影响的。
    		inline fixed4 LightingVelvet (SurfaceOutput s, fixed3 lightDir, half3 viewDir, fixed atten)
    		{
    		}           


  • 永远在一开始就处理好你所有的光照向量(这里指视角方向和光照方向向量,以及它们的衍生向量)。这样可以让你不需要总是标准化你的向量,或担心光照计算的其他部分。因此,在光照模型函数的开头添加光照向量:
    			//Create lighting vectors here
    			viewDir = normalize(viewDir);
    			lightDir = normalize(lightDir);
    			half3 halfVec = normalize (lightDir + viewDir);
    			fixed NdotL = max (0, dot (s.Normal, lightDir));

    解释:
    自己画一画就知道,halfVec将lightDir和viewDir结合在一起,主要用于和这两个向量相关的计算中。例如这里的高光反射(高光反射和观察视角以及光照角度都有关系)。NdotL是光照在平面法线方向上的分量,一般用于和光照颜色相乘来得到关于场景里实际灯光的颜色强度。

  • 下一步,我们需要计算高光反射(Specular)部分。继续添加下面的代码:
    			//Create Specular 
    			float NdotH = max (0, dot (s.Normal, halfVec));
    			float spec = pow (NdotH, s.Specular*128.0) * s.Gloss;

    布料渲染很大程度上依赖你从什么角度观察这个平面。观察角度越倾斜,就有越多的纤维捕捉到灯光后面的光照,并增强了高光反射。(菲涅耳效应)
    			//Create Fresnel
    			float HdotV = pow(1-max(0, dot(halfVec, viewDir)), _FresnelPower);
    			float NdotE = pow(1-max(0, dot(s.Normal, viewDir)), _RimPower);
    			float finalSpecMask = NdotE * HdotV


  • 当大部分计算完成后,我们仅仅需要输出最后的颜色值。添加下面的代码来完成我们的光照模型:
    			//Output the final color
    			fixed4 c;
    			c.rgb = (s.Albedo * NdotL * _LightColor0.rgb)
    					 + (spec * (finalSpecMask * _FresnelColor)) * (atten * 2);
    			c.a = 1.0;
    			return c;


  • 最后,我们创建surf()函数完成我们的Shader。这里,我们仅仅需要解压法线贴图,并把所有的数据传递给我们SurfaceOutput结构。
    		void surf (Input IN, inout SurfaceOutput o) 
    		{
    			half4 c = tex2D (_DetailTex, IN.uv_DetailTex);
    			fixed3 normals = UnpackNormal(tex2D(_BumpMap, IN.uv_BumpMap)).rgb;
    			fixed3 detailNormals = UnpackNormal(tex2D(_DetailBump, IN.uv_DetailBump)).rgb;
    			fixed3 finalNormals = float3(normals.x + detailNormals.x, 
    										normals.y + detailNormals.y, 
    										normals.z + detailNormals.z);
    			
    			o.Normal = normalize(finalNormals);
    			o.Specular = _SpecWidth;
    			o.Gloss = _SpecIntesity;
    			o.Albedo = c.rgb * _MainTint;
    			o.Alpha = c.a;
    		}

    解释在我们的布料着色器中,我们演示的新技术就是如何使用不同的平铺率整合两个法线贴图。基本的线性代数表明,我们可以将两个向量相加得到一个新的位置。因此,我们可以这样操作我们的法线贴图。我们使用UnpackNormal()函数得到标准变化贴图(Normal Variation map)的法线向量,再将其和细节法线贴图(Detail Normal map)的法线向量相加。这样得到了一个新的法线贴图。然后,我们标准化最后的向量,来让它的范围在0到1之间。如果没有这样做,我们的法线贴图就会看起来就是错的。

整体代码如下:
Shader "Custom/ClothShader" {
	Properties
	{
		_MainTint ("Global 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
		_SpecIntesity ("Specular Intensiity", Range(0, 1)) = 0.2
		_SpecWidth ("Specular Width", Range(0, 1)) = 0.2	
	}
	
	SubShader 
	{
		Tags { "RenderType"="Opaque" }
		LOD 200
		
		CGPROGRAM
		#pragma surface surf Velvet
		#pragma target 3.0

		sampler2D _BumpMap;
		sampler2D _DetailBump;
		sampler2D _DetailTex;
		float4 _MainTint;
		float4 _FresnelColor;
		float _FresnelPower;
		float _RimPower;
		float _SpecIntesity;
		float _SpecWidth;

		struct Input 
		{
			float2 uv_BumpMap;
			float2 uv_DetailBump;
			float2 uv_DetailTex;
		};
		
		inline fixed4 LightingVelvet (SurfaceOutput s, fixed3 lightDir, half3 viewDir, fixed atten)
		{
			//Create lighting vectors here
			viewDir = normalize(viewDir);
			lightDir = normalize(lightDir);
			half3 halfVec = normalize (lightDir + viewDir);
			fixed NdotL = max (0, dot (s.Normal, lightDir));
			
			//Create Specular 
			float NdotH = max (0, dot (s.Normal, halfVec));
			float spec = pow (NdotH, s.Specular*128.0) * s.Gloss;
			
			//Create Fresnel
			float HdotV = pow(1-max(0, dot(halfVec, viewDir)), _FresnelPower);
			float NdotE = pow(1-max(0, dot(s.Normal, viewDir)), _RimPower);
			float finalSpecMask = NdotE * HdotV;
			
			//Output the final color
			fixed4 c;
			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) 
		{
			half4 c = tex2D (_DetailTex, IN.uv_DetailTex);
			fixed3 normals = UnpackNormal(tex2D(_BumpMap, IN.uv_BumpMap)).rgb;
			fixed3 detailNormals = UnpackNormal(tex2D(_DetailBump, IN.uv_DetailBump)).rgb;
			fixed3 finalNormals = float3(normals.x + detailNormals.x, 
										normals.y + detailNormals.y, 
										normals.z + detailNormals.z);
			
			o.Normal = normalize(finalNormals);
			o.Specular = _SpecWidth;
			o.Gloss = _SpecIntesity;
			o.Albedo = c.rgb * _MainTint;
			o.Alpha = c.a;
		}
		ENDCG
	} 
	FallBack "Diffuse"
}




下面显示了我们的布料着色器的效果:



解释


实际上我们的Shader并不复杂,无非进行了一些基本的光照计算,但是有时候这些计算就足够了。在你想要用Shader模拟某种表面时,把它分成几个部分,然后再在某一时刻把它们整合到一起。最关键的部分就是你怎样整合不同部分,这就像在Photoshop中混合不同的layers一样。

最后,我们整合菲涅耳和高光反射的计算,这让我们创建了那些微小纤维也可以反射光的视觉效果(这里的我的理解是倾斜的时候就会看到布料的表面越粗糙,那些纤维的细节就越明显)。


写在最后


感觉这一篇原文作者解释的很简单,但是看起来还是有点吃力的。尤其是光照模型中最后关于颜色赋值方面的计算,感觉很多计算实际是靠经验和视觉来进行整合的。

呼。。。今天先写到这里,希望多看看可以有更多的理解。