【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一样。
最后,我们整合菲涅耳和高光反射的计算,这让我们创建了那些微小纤维也能够反射光的视觉效果(这里的我的理解是倾斜的时候就会看到布料的表面越粗糙,那些纤维的细节就越明显)。
写在最后
感觉这一篇原文作者解释的非常easy,可是看起来还是有点吃力的。尤其是光照模型中最后关于颜色赋值方面的计算,感觉非常多计算实际是靠经验和视觉来进行整合的。
呼。。。今天先写到这里,希望多看看能够有很多其它的理解。