标签:c++ 游戏开发者 游戏编程 游戏开发 visual c++
本系列文章由@浅墨_毛星云 出品,转载请注明出处。文章链接:http://hpw123.net/plus/view.php?aid=183
作者:毛星云(浅墨) 微博:http://weibo.com/u/1723155442
QQ交流群:330595914
更多文章尽在:http://www.hpw123.net
本文主要讲解了Unity中SurfaceShader的自定义光照模式的写法。
上篇文章中我们已经说到,表面着色器将分为两次讲解,上一篇文章中介绍了表面着色器的基本概念和一些写法,用内置的兰伯特光照模式来进行Surface Shader的书写,而本文将介绍Surface Shader+自定义的光照模式的写法。
OK,言归正传,依然是先来看看本文配套的游戏场景截图。
运行游戏,音乐响起,金黄色的丰收之秋映入眼帘:
远方:
池塘:
参天大树:
小型村落:
风车:
池塘边:
OK,图先就上这么多。文章末尾有更多的运行截图,并提供了源工程的下载。可运行的exe下载在这里:
好的,我们正式开始。
我们知道,光照模型是真实感图形渲染的基础,从 1967 年 Wylie 等人第一次在显示物体的时候加进光照效果后,该领域迅速的发展。而这一节,我们主要看看最常见的漫反射、Lambert和镜面反射、Phong、Blinn-Phong这五种光照模型。
环境光是对光照现像的最简单抽象,因而局限性很大。它仅能描述光线在空间中无方向并均匀散布时的状态。很多情况下,入射光是带有方向的,比如典型的阳光。
如果光照射到比较粗糙的物体表面,如粉笔,由于这些表面从各个方向等强度地反射光,因而从各个视角出发,物体表面呈现相同的亮度,所看到的物体表面某点的明暗程度不随观测者的位置变化的,这种等同地向各个方向散射的现象称为光的漫反射(diffuse reflection)。
简单光照模型模拟物体表面对光的反射作用。光源被假定为点光源,其几何形状为一个点,向周围所有方向上辐射等强度的光,在物体表面产生反射作用。
如图:
漫反射光的强度近似地服从于Lambert定律,即漫反射光的光强仅与入射光的方向和反射点处表面法向夹角的余弦成正比。
由此可以构造出Lambert漫反射模型:
Idiffuse =Id Kd cosθ
Idiffuse表示物体表面某点的漫反射光强
Id为点光源,Kd(0<Kd<1)表示物体表面该点对漫反射光的反射属性
θ是入射光线的方向与物体表面该点处法线N的夹角,或称为入射角(0≤θ≤90°)
入射角为零时,说明光线垂直于物体表面,漫反射光强最大;
90°时光线与物体表面平行,物体接收不到任何光线。
如图:
把环境光模型添加进来,最后,Lambert光照模型可写为:
I= IaKa + Id Kdcosθ= IaKa + Id Kd(L·N)
该模型包含环境光和漫反射光。
Lambert模型较好地表现了粗糙表面上的光照现象,如石灰粉刷的墙壁、纸张等
但在用于诸如金属材质制成的物体时,则会显得呆板,表现不出光泽,主要原因是该模型没有考虑这些表面的镜面反射效果。
如果光照射到相当光滑的表面,就产生镜面反射(specular reflection),镜面反射的特点是在光滑表面会产生一块称之为高光(high light)的特亮区域 。
镜面反射遵循光的反射定律:反射光与入射光位于表面法向两侧,对理想反射面(如镜面),入射角等于反射角,观察者只能在表面法向的反射方向一侧才能看到反射。
Lambert模型能很好的表示粗糙表面的光照,但不能表现出镜面反射高光。1975年Phong Bui Tong发明的Phong模型,提出了计算镜面高光的经验模型,镜面反射光强与反射光线和视线的夹角a相关:
Ispecular = Ks*Is*(cos a) n
其中Ks为物体表面的高光系数,Is为光强,a是反射光与视线的夹角,n为高光指数,n越大,则表面越光滑,反射光越集中,高光范围越小。如果V表示顶点到视点的单位向量,R表示反射光反向,则cos a可表示为V和R的点积。模型可表示为:
Ispecular = Ks*Is*(V●R) n
反射光放向R可由入射光放向L(顶点指向光源)和物体法向量N求出。
R = (2N●L)N – L
我们重新来看Phong光照的另一种表现形式:
Ispec = IsKscosns α(α∈(0,90o))
Ks为物体表面某点的高亮光系数
ns为物体表面的镜面反射指数,反映了物体表面的光泽程度, ns越大,表示物体越接近于镜面。
只有视线与光源在物体表面的反射光线非常接近时才能看到镜面反射光,此时,镜面反射光将会在反射方向附近形成很亮的光斑,称为高光现象。ns越小,表示物体越粗糙;当ns为零时,镜面反射光便退化为与视点、光源均无关的环境光。
另外,将镜面反射光与环境光、漫反射光叠加起来形成单一光源照射下更为真实的Phong光照模型:
I = Ia Ka+IdKdcosθ+IsKscosns α
θ :入射角
α :视线与镜面反射方向的夹角
图形学界大牛Jim Blinn对Phong模型进行了改进,提出了Blinn-Phong模型。Blinn-Phong模型与Phong模型的区别是,把dot(V,R)换成了dot(N,H),其中H为半角向量,位于法线N和光线L的角平分线方向。Blinn-Phong模型可表示为:
Ispecular = Ks*Is* pow(( dot(N,H), n )
其中H = (L + V) / | L+V |,计算H比计算反射向量R更快速。
Unity中,Phong实际上指的就是Blinn-Phong,两者指的同一种内置光照模型。
PS:光照模型部分的内容主要参考为:http://cg.sjtu.edu.cn/
在编写表面着色器的时候,我们通常要描述一个表面的属性(反射率颜色,法线,…)、并通过光照模式来计算灯光的相互作用。
通过上篇文章的学习,我们已经知道,Unity中的内置的光照模式有两种, 分别是Lambert (漫反射光diffuse lighting) 和 Blinn-Phong (也就是镜面反射光(高光),specular lighting)模式。
然而,内置的光照模式自然有其局限性。想要自己做主的话,我们可以自定义光照模式。
也就是使用自定义光照模式( custom lighting model)。
其实说白了,光照模式(lighting model)无外乎是几个Cg/HLSL函数的组合。
内置的 Lambert 和 Blinn-Phong定义在 Lighting.cginc文件中。我们不妨先来人肉一下他们的实现源代码。
windows系统下位于:
…Unity\Editor\Data\CGIncludes\
Mac系统下位于:
/Applications/Unity/Unity.app/Contents/CGIncludes/Lighting.cginc)
Unity内置的 Lambert 和 Blinn-Phong模型的Shader源代码在这里贴出来:
OK,下面让我们一起来看看光照模式应该怎样声明和定义。
在Unity Shaderlab和CG语言中,光照模式是一个以Lighting开头+自定义文字组合在一起的函数。
即,函数名为:
Lighting+ [自定义部分]
比如,一个可行的函数名为:LightingQianMoLightingMode
我们可以在着色器文件(shader file)或导入文件(included files)中的任何一个地方声明此函数。一般情况下,此函数有五种原型可供选择,具体如下。
一般情况下,于以下五种函数原型中选一种,进行实现就行了。
【形式一】
half4 LightingName (SurfaceOutput s, half3lightDir, half atten);
此种形式的函数可以表示在正向渲染路径(forward rendering path)中的光照模式,且此函数不取决于视图方向(view direction)。例如:漫反射(diffuse)。
【形式二】
half4 LightingName (SurfaceOutput s, half3lightDir, half3 viewDir, half atten);
此种形式的函数可以表示在正向渲染路径(forward rendering path)中使用的光照模式,且此函数包含了视图方向(view direction)。
【形式三】
half4 LightingName_PrePass (SurfaceOutputs, half4 light);
此种形式的函数可以在延时光照路径(deferred lighting path)中使用的。
【形式四】
half4 LightingName_DirLightmap(SurfaceOutput s, fixed4 color, fixed4 scale, bool surfFuncWritesNormal);
这种形式也是不依赖于视图方向(view direction)的光照模式。例如:漫反射(diffuse)。
【形式五】
half4 LightingName_DirLightmap(SurfaceOutput s, fixed4 color, fixed4 scale, half3 viewDir, bool surfFuncWritesNormal,out half3 specColor);这是使用的依赖于视图方向(view direction)的光照模式(light model)。
比如,一个光照模式(lighting model)要么使用视图方向(view direction)要么不使用。同样的,如果光照模式(lightingmodel)在延时光照(deferred lighting)中不工作,只要不声明成 _PrePass(第三种形式),就是行的。
另外,对于形式四和形式五的选择,主要取决于我们的光照模式(light model)是否依赖视图方向(view direction)。需要注意的是,这两个函数将自动处理正向和延时光照路径(forwardand deferred lighting rendering paths)。
PS: Unity在移动平台中暂时不支持延迟光照渲染。
做个总结,在自定义自己的光照模型函数时,根据需要在五种函数原型中选择一种,且:
光照模式的函数名为:Lighting+ [自定义函数名]
pragma声明为: #pragmasurface surf [自定义函数名]
然后就是需要,仿照着其他的光照模式来填充函数体了。
我们举个例子:
1 |
1.#pragma surface surf QianMoLigtingMode
|
2 |
2.half4 LightingQianMoLigtingMode (SurfaceOutputs, half3 lightDir, half3 viewDir, half atten);
|
OK,光照模式的声明就是这样。光照模式的函数体是其最核心的部分,需要根据具体的光照模式数学公式进行书写,我们将在接下来的写Shader实战中进行学习。
PS:估计这节的概念有些难懂,大家肯定在第一时间不能完全理解,没事,让我们依旧在Shader实战中把状态找回来。
上文已经提到过了,: Unity在移动平台中暂时不支持延迟光照(Deferred lighting)渲染。因为延时光照不能与一些自定义per-material 光照模式很好的共同运行,所以在下面的例子中我们只在着色器正向渲染(ForwardRendering)中进行实现。
首先,我们根据上一节所学,写一个依靠内置的兰伯特光照模式的漫反射光的Surface Shader:
01 |
Shader "浅墨Shader编程/Volume7/33.内置的漫反射" |
02 |
{ |
03 |
//--------------------------------【属性】----------------------------------
|
04 |
Properties
|
05 |
{ |
06 |
_MainTex ( "【主纹理】Texture" , 2D) =
"white" {} |
07 |
} |
08 |
//--------------------------------【子着色器】----------------------------------
|
09 |
SubShader
|
10 |
{ |
11 |
//-----------子着色器标签----------
|
12 |
Tags {
"RenderType" =
"Opaque" } |
13 |
14 |
//-------------------开始CG着色器编程语言段-----------------
|
15 |
CGPROGRAM |
16 |
17 |
//【1】光照模式声明:使用兰伯特光照模式 |
18 |
#pragma surface surf Lambert |
19 |
20 |
//【2】输入结构
|
21 |
struct
Input |
22 |
{ |
23 |
float2 uv_MainTex; |
24 |
}; |
25 |
26 |
//变量声明
|
27 |
sampler2D _MainTex; |
28 |
29 |
//【3】表面着色函数的编写
|
30 |
void
surf (Input IN, inout SurfaceOutput o) |
31 |
{ |
32 |
//从主纹理获取rgb颜色值
|
33 |
o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb; |
34 |
} |
35 |
36 |
//-------------------结束CG着色器编程语言段------------------ |
37 |
ENDCG |
38 |
} |
39 |
40 |
Fallback
"Diffuse" |
41 |
} |
实现效果:
下面是一个简单的高光光照模式(specular lighting model)Shader。实际上他就是Unity内置的Blinn-Phong光照模型,实际上做起来并不困难。这边我们将它单独拿出来实现一下:
实现效果:
对应于Unity内建的Lambert光照,我们可以自定义原理类似的光照模式,实现自己Lambert光照:
实现效果如下:
接下来,让我们自制一个半Lambert光照。
Lambert定律认为,在平面某点漫反射光的光强与该反射点的法向量和入射光角度的余弦值成正比(即我们之前使用dot函数得到的结果)。Half Lambert最初是由Valve(大V社)提出来的,用于提高物体在一些光线无法照射到的区域的亮度的。
简单说来,半Lambert光照提高了漫反射光照的亮度,使得漫反射光线可以看起来照射到一个物体的各个表面。
而半Lambert最初也是被用于《半条命2》的画面渲染,为了防止某个物体的背光面丢失形状并且显得太过平面化。这个技术是完全没有基于任何物理原理的,而仅仅是一种感性的视觉增强。
遮蔽的漫反射-漫反射光照的一种改进。照明"环绕(wraps around)"在物体的边缘。它对于假冒子表面(subsurface)散射效果(scattering effect)非常有用。
半Lambert光照Shader的代码如下:
01 |
Shader "浅墨Shader编程/Volume7/36.自制半Lambert光照" |
02 |
{ |
03 |
//--------------------------------【属性】----------------------------------------
|
04 |
Properties
|
05 |
{ |
06 |
_MainTex ( "【主纹理】Texture" , 2D) =
"white" {} |
07 |
} |
08 |
09 |
//--------------------------------【子着色器】----------------------------------
|
10 |
SubShader
|
11 |
{ |
12 |
//-----------子着色器标签----------
|
13 |
Tags {
"RenderType" =
"Opaque" } |
14 |
//-------------------开始CG着色器编程语言段-----------------
|
15 |
CGPROGRAM |
16 |
17 |
//【1】光照模式声明:使用自制的半兰伯特光照模式 |
18 |
#pragma surface surf QianMoHalfLambert |
19 |
20 |
//【2】实现自定义的半兰伯特光照模式 |
21 |
half4 LightingQianMoHalfLambert (SurfaceOutput s, half3 lightDir, half atten)
|
22 |
{ |
23 |
half NdotL =max(0, dot (s.Normal, lightDir)); |
24 |
25 |
//在兰伯特光照的基础上加上这句,增加光强 |
26 |
float
hLambert = NdotL * 0.5 + 0.5; |
27 |
half4 color; |
28 |
29 |
//修改这句中的相关参数 |
30 |
color.rgb = s.Albedo * _LightColor0.rgb * (hLambert * atten * 2); |
31 |
color.a = s.Alpha; |
32 |
return
color; |
33 |
} |
34 |
35 |
//【3】输入结构
|
36 |
struct
Input |
37 |
{ |
38 |
float2 uv_MainTex; |
39 |
}; |
40 |
41 |
//变量声明 |
42 |
sampler2D _MainTex; |
43 |
44 |
//【4】表面着色函数的编写 |
45 |
void
surf (Input IN, inout SurfaceOutput o) |
46 |
{ |
47 |
//从主纹理获取rgb颜色值
|
48 |
o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb; |
49 |
} |
50 |
51 |
//-------------------结束CG着色器编程语言段------------------ |
52 |
ENDCG |
53 |
} |
54 |
55 |
Fallback
"Diffuse" |
56 |
} |
实现效果如下:
下面,我们一起实现一个自定义卡通渐变光照,通过一个不同的渐变纹理(渐变纹理可由PS制作),实现各种不同的渐变效果。
自定义卡通渐变光照Shader代码如下:
01 |
Shader "浅墨Shader编程/Volume7/37.自定义卡通渐变光照" |
02 |
{ |
03 |
//--------------------------------【属性】----------------------------------------
|
04 |
Properties
|
05 |
{ |
06 |
_MainTex ( "【主纹理】Texture" , 2D) =
"white" {} |
07 |
_Ramp ( "【渐变纹理】Shading Ramp" , 2D) =
"gray" {} |
08 |
} |
09 |
10 |
//--------------------------------【子着色器】---------------------------------- |
11 |
SubShader
|
12 |
{ |
13 |
//-----------子着色器标签----------
|
14 |
Tags {
"RenderType" =
"Opaque" } |
15 |
//-------------------开始CG着色器编程语言段-----------------
|
16 |
CGPROGRAM |
17 |
18 |
//【1】光照模式声明:使用自制的卡通渐变光照模式 |
19 |
#pragma surface surf Ramp |
20 |
21 |
//变量声明 |
22 |
sampler2D _Ramp; |
23 |
24 |
//【2】实现自制的卡通渐变光照模式 |
25 |
half4 LightingRamp (SurfaceOutput s, half3 lightDir, half atten) |
26 |
{ |
27 |
//点乘反射光线法线和光线方向 |
28 |
<span style= "white-space: pre;" > </span>half NdotL = dot (s.Normal, lightDir);
|
29 |
//增强光强 |
30 |
<span style= "white-space: pre;" > </span>half diff = NdotL * 0.5 + 0.5; |
31 |
//从纹理中定义渐变效果 |
32 |
half3 ramp = tex2D (_Ramp, float2(diff,diff)).rgb; |
33 |
//计算出最终结果 |
34 |
<span style= "white-space: pre;" > </span>half4 color; |
35 |
color.rgb = s.Albedo * _LightColor0.rgb * ramp * (atten * 2); |
36 |
color.a = s.Alpha; |
37 |
38 |
return
color; |
39 |
} |
40 |
41 |
//【3】输入结构
|
42 |
struct
Input |
43 |
{ |
44 |
float2 uv_MainTex; |
45 |
}; |
46 |
47 |
//变量声明 |
48 |
sampler2D _MainTex; |
49 |
50 |
//【4】表面着色函数的编写 |
51 |
void
surf (Input IN, inout SurfaceOutput o) |
52 |
{ |
53 |
//从主纹理获取rgb颜色值
|
54 |
o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb; |
55 |
} |
56 |
57 |
//-------------------结束CG着色器编程语言段------------------ |
58 |
ENDCG |
59 |
60 |
} |
61 |
Fallback
"Diffuse" |
62 |
} |
我们取不同的渐变纹理,可得到不同的效果。以下是五种不同渐变纹理和对应的效果图。
第一组:
第二组:
第三组:
第四组:
第五组:
让我们在上面这个Shader的基础上,加入更多可选的属性,成为一个功能完备的渐变光照Shader:
01 |
Shader "浅墨Shader编程/Volume7/38.自定义卡通渐变光照v2" |
02 |
{ |
03 |
//--------------------------------【属性】----------------------------------------
|
04 |
Properties
|
05 |
{
|
06 |
_MainTex ( "【主纹理】Texture" , 2D) =
"white" {}
|
07 |
_Ramp ( "【渐变纹理】Ramp Texture" , 2D) =
"white" {}
|
08 |
_BumpMap ( "【凹凸纹理】Bumpmap" , 2D) =
"bump" {}
|
09 |
_Detail ( "【细节纹理】Detail" , 2D) =
"gray" {}
|
10 |
_RimColor ( "【边缘颜色】Rim Color" , Color) = (0.26,0.19,0.16,0.0)
|
11 |
_RimPower ( "【边缘颜色强度】Rim Power" , Range(0.5,8.0)) = 3.0
|
12 |
}
|
13 |
14 |
//--------------------------------【子着色器】---------------------------------- |
15 |
SubShader
|
16 |
{
|
17 |
//-----------子着色器标签----------
|
18 |
Tags {
"RenderType" = "Opaque"
} |
19 |
LOD 200
|
20 |
21 |
//-------------------开始CG着色器编程语言段-----------------
|
22 |
CGPROGRAM
|
23 |
24 |
//【1】光照模式声明:使用自制的卡通渐变光照模式 |
25 |
#pragma surface surf QianMoCartoonShader
|
26 |
|
27 |
|
28 |
//变量声明
|
29 |
sampler2D _MainTex;
|
30 |
sampler2D _Ramp;
|
31 |
sampler2D _BumpMap;
|
32 |
sampler2D _Detail;
|
33 |
float4 _RimColor;
|
34 |
float
_RimPower; |
35 |
36 |
//【2】实现自制的卡通渐变光照模式 |
37 |
inline
float4 LightingQianMoCartoonShader(SurfaceOutput s, fixed3 lightDir, fixed atten)
|
38 |
{
|
39 |
//点乘反射光线法线和光线方向 |
40 |
half NdotL = dot (s.Normal, lightDir);
|
41 |
//增强光强 |
42 |
half diff = NdotL * 0.5 + 0.5; |
43 |
//从纹理中定义渐变效果 |
44 |
half3 ramp = tex2D (_Ramp, float2(diff,diff)).rgb; |
45 |
//计算出最终结果 |
46 |
half4 color; |
47 |
color.rgb = s.Albedo * _LightColor0.rgb * ramp * (atten * 2); |
48 |
color.a = s.Alpha; |
49 |
50 |
return
color; |
51 |
}
|
52 |
53 |
//【3】输入结构
|
54 |
struct
Input |
55 |
{
|
56 |
//主纹理的uv值
|
57 |
float2 uv_MainTex;
|
58 |
//凹凸纹理的uv值
|
59 |
float2 uv_BumpMap;
|
60 |
//细节纹理的uv值
|
61 |
float2 uv_Detail;
|
62 |
//当前坐标的视角方向
|
63 |
float3 viewDir;
|
64 |
};
|
65 |
66 |
|
67 |
//【4】表面着色函数的编写 |
68 |
void
surf (Input IN, inout SurfaceOutput o) |
69 |
{
|
70 |
//先从主纹理获取rgb颜色值
|
71 |
o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb;
|
72 |
//设置细节纹理
|
73 |
o.Albedo *= tex2D (_Detail, IN.uv_Detail).rgb * 2;
|
74 |
//从凹凸纹理获取法线值
|
75 |
o.Normal = UnpackNormal (tex2D (_BumpMap, IN.uv_BumpMap));
|
76 |
//从_RimColor参数获取自发光颜色
|
77 |
half rim = 1.0 - saturate(dot (normalize(IN.viewDir), o.Normal));
|
78 |
o.Emission = _RimColor.rgb *
pow (rim, _RimPower);
|
79 |
80 |
}
|
81 |
82 |
//-------------------结束CG着色器编程语言段------------------ |
83 |
ENDCG
|
84 |
}
|
85 |
FallBack
"Diffuse" |
86 |
} |
我们将此Shader编译后赋给材质,得到如下效果:
可供调节的属性非常多,稍微放几张效果图,剩下的大家可以下载工程源代码,或者拷贝Shader代码,自己回去调着玩吧~
布料细节纹理+灰白渐变纹理+红色边缘光:
布料细节纹理+灰白渐变纹理+浅绿色边缘光:
布料细节纹理+灰白渐变纹理+白色边缘光:
布料细节纹理+灰白渐变纹理+无边缘光(黑色):
以大师级美工鬼斧神工的场景作品为基础,浅墨调整了场景布局,加入了音乐,并加入了更多高级特效,于是便得到了如此这次非常炫酷的暗黑城堡场景。
运行游戏,树影摇曳,我们来到金黄色的丰收之秋。
最后,放一张本篇文章中实现的Shader全家福:
OK,美图就放这么多。游戏场景可运行的exe可以在文章开头中提供的链接下载。而以下是源工程的下载链接。
本篇文章的示例程序源工程请点击此处下载:
【浅墨Unity3D Shader编程】之七 静谧之秋篇配套Unity工程下载
Unity Shader系列文章到目前已经更新了7篇,一共实现了38个详细注释、循序渐进、功能各异的Shader,对Unity中的固定功能Shader、Surface Shader都已经有了比较详细、系统的讲解和实现。而可编程Shader按学习计划来说是以后的计划,目前还是未涉及,有机会在以后的文章中一起和大家一起探讨。
而大家如果仔细参考和阅读这七篇文章,会发现Unity中Shader的书写其实是非常容易和有章可循的。这大概就是浅墨写这个系列文章的初衷吧。
天下没有不散的宴席。
浅墨接下来的一段时间有一些大的游戏项目要接触,所以,Unity Shader系列文章每周周一的固定更新只能改为不定期的更新了。以后浅墨有了空余时间,会继续写博文来与大家交流分享。
OK,于未来某天更新的下篇文章中,我们后会有期。:)
【浅墨Unity3D Shader编程】之七 静谧之秋篇: 表面着色器的写法(二)——自定义光照模式
标签:c++ 游戏开发者 游戏编程 游戏开发 visual c++
原文地址:http://blog.csdn.net/u010283694/article/details/42615017