了解光照模型
在前一章节我们学习了Surface Shader,知道怎么样去改变物理材质计算不同的纹理,那么他们到底是怎么工作的呢,这个核心的部分就是光照模型。光照模型是一个计算每一个像素最终的颜色的函数。Unity总是去隐藏了光照函数的实现,因为去编写一个光照模型函数你需要了解光是怎么样从物体表面反射。这一章节将介绍光照模型是如何工作的,你可以知道:
- 创建自定义的Diffuse 光照模型
- 创建卡通(TOON)着色模型
- 创建Phong Specular 光照模型
- 创建BinnPhong Specular 光照模型
- 创建一个Anisotropic Specular 光照模型
Creatting a custom diffuse lighting model
如果你熟悉Unity4的话,应该知道默认的shader是基于Lambert关照模型的。Lambert反射模型是十分经典的非拟真关照模型,没有物体在真是世界长这个样子。然而,它依然被众多游戏使用,效率高,并且提供了不错的效果,特别对于移动平台。
Unity已经为我们准备了关照函数,叫:Lambertian lighting model,其中一个更基础更有效率的反射模型,即使在今天,依然可以在很多游戏中看到。
How Lembert lighting model work..
在Sufrface shader 中使用#pragma 来声明使用哪个函数作为光照反射函数。
#pragma surface surf SimpleLambert
half4 LightingSimpleLambert()
依照Lambertian反射模型,多少光将被表面反射取决于关照方向和表面法向量的夹角。
采用数学计算Lambert反射模型。角度值可以通过两个向量(光照方向和法向量)的点乘来计算,当点乘的结果等于0,表示两个向量正交垂直,当两个向量点乘结果等于1,表示两个向量重合平行。CG中提供了专门的API dot()实现了高效的运算过程。
I = N·L
当N 和 L垂直的时候所有的光照都被反射,使得表面高亮。Unity内置变量 _LightColor0 包含了用于计算的光照颜色。当点乘的值是负的时候表示这个面不朝向Camera,这其实很好处理,因为这个变将不被渲染。
这个基本的Lambert反射模型是我们编写自己的着色器的一个很好的起点,同时Unity也为我们编写了一个Lambert光照模型的内置函数,在Unity.cginc文件中可以找到。
Creating a Toon Shader(卡通着色)
在游戏总其中一个非常有用的效果是Toon Shading(卡通着色),这是一个非真实的着色技术,是的3D模型看起来变得扁平。很多游戏使用这个技术,给人一种错觉,这是手绘的而不是3D建模的。与标准着色器效果对比:
要实现这样的效果仅仅使用Surface functions也可以实现,不过这代价可能非常昂贵。实际上,Surface function,只对材质的属性进行计算(应该是说默认的Surface Shader隐藏了光照的实现),我们需要改变光照反射模型,自定义光照函数去实现Toon Shading.
Toon Shader 原理
Toon Shading的光照着色不是按照正常方式,要达到这种效果,需要一张Ramp map(梯度图),这个的目的是把Lambertian light的计算结果映射到一个个的梯度区间内。
The Code
fixed4 LightingToon(SurfaceOutput s, fixed3 lightDir, fixed atten)
{
half NdotL = dot(s.Normal, lightDir);
NdotL = tex2D(_RampTex, fixed2(NdotL, 0.5));
fixed4 c;
c.rgb = s.Albedo * _LightColor0.rgb * NdotL * atten;
c.a = s.Alpha;
return c;
}
事实上有很多方法可以得到Toon Shading的效果。使用不公的Ramp map产生一些意想不到的效果,为了达到最想要的效果,通常需要做比较多的尝试。
Creating a Phong Specular type
物体表面的高光是描述其表面的光滑度,这种类型的效果通常和视角有着密切关系。这是因为为了达到真实的高光效果你需要考虑到摄像机的方向或者说观察者是否面向这个面。其中最为基础的并且表现不错的一种高光模型是Phone Specular。它计算了对比光从表面的反射方向和观察者的视线,这是非常普遍的在很多App中应用的高光模型,游戏呀,电影呀之类的。虽然没有达到真正的真实效果,但她提供了一种很好的近似模拟的算法,并且在很多平台,场景中表现不俗。另外,当你的物体远离摄像机的时候,其实是不需要进行实际上的高光计算,这不失为一种好方法去表现高光效果。
在这一小节,我们将探讨实现逐顶点和逐像素两个版本的Phone Specular Shader的实现细节,使用和前面不一样的Input参数,并且也会讨论在什么样的场景选择哪个版本的Shader.
先了解Phong的实现原理,如图,L是光照方向,R是要求的反射向量,R = 2S - L, 然后|S| / |L| = (N · L) / |N||L| = cosθ, 得到 S = (N · L) * N
最后:R = 2 * (N · L) * N - L
对于光照颜色的计算:I = D + S,diffuse颜色加上高光, 计算最终高光的时候还要考虑观察者和反射向量的角度,下面是shader代码:
Shader "CookbookShaders/self/PhongSpec" {
Properties {
_MainTint ("Diffuse Tint", Color) = (1,1,1,1)
_MainTex ("Albedo (RGB)", 2D) = "white" {}
_SpecularColor ("Specular Color", Color) = (1, 1, 1, 1)
_SpecPower ("Specular Power", Range(0.1,128)) = 1
}
SubShader {
Tags { "RenderType"="Opaque" }
LOD 200
CGPROGRAM
// Physically based Standard lighting model, and enable shadows on all light types
#pragma surface surf Phong
// Use shader model 3.0 target, to get nicer looking lighting
#pragma target 3.0
sampler2D _MainTex;
struct Input {
float2 uv_MainTex;
};
float4 _MainTint;
float4 _SpecularColor;
half _SpecPower;
fixed4 LightingPhong(SurfaceOutput s, fixed3 lightDir, half3 viewDir, fixed atten)
{
//reflection
float NdotL = dot(s.Normal, lightDir);
float3 reflectionVector = normalize(2.0 * s.Normal * NdotL - lightDir);
//specular
float spec = pow(max(0, dot(reflectionVector, viewDir)), _SpecPower);
float3 finalSpec = _SpecularColor.rgb * spec;
//final effect
fixed4 c;
c.rgb = (s.Albedo * _LightColor0.rgb * max(0, NdotL) * atten) + (_LightColor0.rgb * finalSpec);
c.a = s.Alpha;
return c;
}
void surf (Input IN, inout SurfaceOutput o) {
// Albedo comes from a texture tinted by color
fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _MainTint;
o.Albedo = c.rgb;
o.Alpha = c.a;
}
ENDCG
}
FallBack "Diffuse"
}
Creating a BlinnPhong Specular Type(一种简化的Phong光照模型,是个经验模型)
Blinn是另一种计算和模拟高光效果的方法。她通过去观察者向量和光照方向的中间向量替换反射向量的计算,改算法由Jim Blinn带进CG的世界。他发现相比计算光照的反射向量这样显然更有效率,并且效果上十分接近Phong。某种意义上,这是一个简化版本的Phong模型。
这几乎可以看做是Phong模型,Phong中
S = (RV)^p
在BlinnPhong中:
S = (NH)^p
代码实现:
fixed4 LightingBlinnPhong(SurfaceOutput s, fixed3 lightDir, half3 viewDir, fixed atten)
{
//reflection
float NdotL = max(0, dot(s.Normal, lightDir));
float3 halfVector = normalize( lightDir + viewDir);
//specular
float NdotH = max(0, dot(s.Normal, halfVector));
float spec = pow(NdotH, _SpecPower);
float3 finalSpec = _SpecularColor.rgb * spec;
//final effect
fixed4 c;
c.rgb = (s.Albedo * _LightColor0.rgb * NdotL * atten) + (_LightColor0.rgb * finalSpec * atten);
c.a = s.Alpha;
return c;
}
BinnPhong相对于Phong会显得更亮一些,但是她依然在大多数情况下有着良好的表现。
最后来详细对比下这两种高光计算的过程:
Creating an Anisotropic Specular Type(各向异性高光模型)
各向异性是模拟一些有凹槽的表面的高光效果,通过修改或延伸他们的法线方向,她非常有用,当你想要模拟磨砂/抛光金属质感,而不仅仅是光滑的表面的时候。想象一下平时看到的CD, DVD的表面,或者一些金属盘的底部的高光反射。如果你仔细观察,你会发现这些表面上其实是有凹槽的。
这一章节将介绍扩展高光使之达到不同类型的拉丝(Brushed)表面。在更后面的章节,你将看到一些更高级的效果,如头发。
一些例子图片:
The Code Fisrt
Shader "CookbookShaders/self/Anisotropoy" {
Properties {
_MainTint ("Diffuse Tint", Color) = (1,1,1,1)
_MainTex ("Albedo (RGB)", 2D) = "white" {}
_SpecularColor ("Specular Color", Color) = (1, 1, 1, 1)
_Specular ("Specular Amount", Range(0,1)) = 0.5
_SpecPower ("Specular Power", Range(0.1,128)) = 1
_AnisoDir("Anisotropic Direection", 2D) = ""{}
_AnisoOffset ("Anisotropic Offset", Range(-1, 1)) = -0.2
}
SubShader {
Tags { "RenderType"="Opaque" }
LOD 200
CGPROGRAM
// Physically based Standard lighting model, and enable shadows on all light types
#pragma surface surf MyAnisotropic
// Use shader model 3.0 target, to get nicer looking lighting
#pragma target 3.0
struct Input {
float2 uv_MainTex;
float2 uv_AnisoDir;
};
struct SurfaceAnisoOutput
{
fixed3 Albedo;
fixed3 Normal;
fixed3 Emission;
fixed3 AnisoDirection;
half Specular;
fixed Gloss;
fixed Alpha;
};
sampler2D _MainTex;
sampler2D _AnisoDir;
float4 _MainTint;
float4 _SpecularColor;
float _AnisoOffset;
float _Specular;
half _SpecPower;
fixed4 LightingMyAnisotropic(SurfaceAnisoOutput s, fixed3 lightDir, half3 viewDir, fixed atten)
{
fixed3 halfVector = normalize(normalize(lightDir) + normalize(viewDir));
float NdotL = saturate(dot(s.Normal, lightDir));
fixed 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);
fixed4 c;
c.rgb = ((s.Albedo * _LightColor0.rgb * NdotL) + (_LightColor0.rgb * _SpecularColor.rgb * spec)) * atten;
c.a = s.Alpha;
return c;
}
void surf (Input IN, inout SurfaceAnisoOutput o) {
half4 c = tex2D(_MainTex, IN.uv_MainTex) * _MainTint;
float3 anisoTex = UnpackNormal(tex2D(_AnisoDir, IN.uv_AnisoDir));
o.AnisoDirection = anisoTex;
o.Specular = _Specular;
o.Gloss = _SpecPower;
o.Albedo = c.rgb;
o.Alpha = c.a;
}
ENDCG
}
//FallBack "Diffuse"
}
从原理上,该Shader通过更向异性法相贴图,修改模型的真实法线方向,其他方面基本和高光结算一样。
首先自定义了SurfaceOutput结构:SurfaceAnisoOutput,这样做的原因是为了传递各向异性贴图中每一像素的法线信息,这个可以简单的通过tex2D结合UnpackNormal()来获得。
当我们的光照函数有了数据之后,就可以开始光照的计算啦,想前面BlinnPhong一样,我们计算出观察向量和光照向量之间的中间向量,不同的是在计算高光的时候使用的是被修改过的法向量。
fixed HdotA = dot(normalize(s.Normal + s.AnisoDirection), halfVector);
再加上偏移值得到高光系数
float aniso = max(0, sin(radians( (HdotA + _AnisoOffset) * 180) ));
效果: