UnityShader学习_基本光照模型的实现

最近在学习《Unity Shader入门精要》一书,学到了光照模型,正好自己以前也用过OpenGL实现过标准光照模型(也可以叫Phong光照模型),那么这次就用Unity Shader实现一下标准光照模型吧。


 那么首先做一下准备工作:

  1. 准备使用的软件:这里用的是unity2018以及vs2015,其中vs2015使用了VSTU,HlSL Tools for  VS,ShaderLabVS三个插件。(虽然并不是很好用)
  2. 创建一种材质Material以及一个Unlit Shader,把这个Shader赋予给材质。
  3. 在场景中关闭天空盒(skybox),创建一个3D物体(这里采用的是capsule,能比较好的看出光照效果),把上面的材质赋予给物体,光源就采用默认的平行光,其它光源的计算方式有所不同。在如图所示的地方关闭天空盒。                                                                                        
  4. 接下来复习一下标准光照模型的知识,其中传到相机的光线由四个部分组成:环境光自发光漫反射 ,高光反射(也叫镜面反射)。那么:最终的效果颜色=环境光颜色+自发光颜色+漫反射颜色+高光反射颜色  。  其中环境光可以通过内置变量获取 ,自发光可以自身设置,因此我们所要计算的就只有漫反射和高光反射。

            至此,准备工作已经完成,现在开始计算漫反射。漫反射使用兰伯特(Lambert)光照模型表示,公式为

                                    diffuse = Kd * lightColor * max(dot(N, L), 0)

其中Kd为材质的漫反射颜色,lightColor为光照颜色,N, L分别为表面法线,指向光源的单位矢量,dot为向量点乘,max的作用是保证法线方向和光源方向的点乘不为负值,意义就是防止物体被从后面来的光源照亮。

虽然漫反射公式已经知道,那么是在顶点着色器计算它的颜色还是在片元着色器计算它的颜色呢?答案自然是两者都可以。其中在片元中计算就是逐像素光照,在顶点中计算就是逐顶点光照,对于下面的高光反射的计算也是如此。

逐顶点计算漫反射:

Shader "Unity Shaders Book/DiffuseVertexLevel"
{
	Properties{
	_Diffuse("Diffuse",Color)=(1,1,1,1) //声明一个Color属性用来表示漫反射颜色,可以在Inspector面板显示
	}
	SubShader{
		Pass{
			Tags{"LightMode"="ForwardBase"} //定义该Pass在光照流水线中的角色,只有定义正确才能获得一些内置光照变量
			CGPROGRAM
			#pragma vertex vert 
			#pragma fragment frag 
			#include "Lighting.cginc" 
			//Lighting.cginc为包含一些内置光照变量的头文件
			fixed4 _Diffuse; //定义一个在Properties中声明过的变量,这样才可以使用它

		    struct a2v //顶点着色器输入结构体
			{
                float4 vertex : POSITION;
                float3 normal : NORMAL;
            };

            struct v2f //顶点着色器输出结构体,片元着色器输入结构体
			{
                float4 pos : SV_POSITION;
                fixed3 color : COLOR;
            };

			v2f vert(a2v v)//顶点着色器
			{
				v2f o;
				o.pos=UnityObjectToClipPos(v.vertex);//把顶点坐标转换到裁剪空间
				fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz;//获取自然光
				fixed3 worldNormal=normalize(mul(v.normal,(float3x3)unity_WorldToObject));//把表面法线转换到世界坐标系并归一化
				fixed3 worldLight=normalize(_WorldSpaceLightPos0.xyz);//获取平行光的方向(只适用只有一个平行光源的时候)
				fixed3 diffuse=_LightColor0.rgb*_Diffuse.rgb*saturate(dot(worldNormal,worldLight));//计算漫反射,其中_LightColor0只有正确声明了Tags和包含正确的头文件才可以获取
				o.color=ambient+diffuse;
				return o;
			}

			fixed4 frag(v2f i):SV_Target//表面着色器
			{
				return fixed4(i.color,1.0);
			}
			ENDCG
		}
	}
	FallBack "Diffuse"
}

       逐像素计算漫反射:

Shader "Unlit/DiffusePixelLevel"
{
	Properties{
	_Diffuse("Diffuse",Color)=(1,1,1,1)
	}
	SubShader{
		Pass{
			Tags{"LightMode"="ForwardBase"}
			CGPROGRAM
			#pragma vertex vert 
			#pragma fragment frag 
			#include "Lighting.cginc"
			fixed4 _Diffuse;
		    struct a2v {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
            };

            struct v2f {
                float4 pos : SV_POSITION;
                float3 worldNormal:TEXCOORD0;
            };

			v2f vert(a2v v)
			{
				v2f o;
				o.pos=UnityObjectToClipPos(v.vertex);
				o.worldNormal=mul(v.normal,(float3x3)unity_WorldToObject);
				return o;
			}

			fixed4 frag(v2f i):SV_Target{
				fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz;
				fixed3 worldNormal=normalize(i.worldNormal);
				fixed3 worldLgihtDir=normalize(_WorldSpaceLightPos0.xyz);
				fixed3 diffuse=_LightColor0.rgb*_Diffuse.rgb*saturate(dot(worldNormal,worldLgihtDir));
				fixed3 color=ambient+diffuse;
				return fixed4(color,1.0);
			}
			ENDCG
		}
	}
	FallBack "Diffuse"
}

        对比可见,两种计算方式并没有太大的区别,区别只是在于颜色的计算位置。逐顶点的颜色计算是在顶点着色器中,得到的是顶点光照颜色,然后片元着色器通过线性插值输出像素的颜色,逐片元的是以每个像素基础,得到它的法线(这里是对顶点法线插值得到),然后再计算颜色。

至此,漫反射光照模型已经建立完成,但是由于max函数的作用,物体的背光面会是完全的黑色,这显然与现实不符合,因此就有了半兰伯特模型。它使用一个α倍的缩放和一个β倍的偏移来保证其不会为负值,这两个数绝大部分时候为0.5.

计算公式:

漫反射光照 = (光照颜色与强度 * 漫反射颜色)* (dot(法线方向 ,光照方向) * α + β);

因此使用半兰伯特模型的逐像素漫反射为:

Shader "Unlit/DiffuseHalfLambert"
{
	Properties{
	_Diffuse("Diffuse",Color)=(1,1,1,1)
	}
	SubShader{
		Pass{
			Tags{"LightMode"="ForwardBase"}
			CGPROGRAM
			#pragma vertex vert 
			#pragma fragment frag 
			#include "Lighting.cginc"
			fixed4 _Diffuse;
		    struct a2v {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
            };

            struct v2f {
                float4 pos : SV_POSITION;
                float3 worldNormal:TEXCOORD0;
            };

			v2f vert(a2v v)
			{
				v2f o;
				o.pos=UnityObjectToClipPos(v.vertex);
				o.worldNormal=mul(v.normal,(float3x3)unity_WorldToObject);
				return o;
			}

			fixed4 frag(v2f i):SV_Target{
				fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz;
				fixed3 worldNormal=normalize(i.worldNormal);
				fixed3 worldLgihtDir=normalize(_WorldSpaceLightPos0.xyz);
				fixed halfLambert=dot(worldNormal,worldLgihtDir)*0.5+0.5;
				fixed3 diffuse=_LightColor0.rgb*_Diffuse.rgb*halfLambert;
				fixed3 color=ambient+diffuse;
				return fixed4(color,1.0);
			}
			ENDCG
		}
	}
	FallBack "Diffuse"

     最终三种效果为:

UnityShader学习_基本光照模型的实现_第1张图片

             从里到外分别为逐顶点光照,逐像素光照,半兰伯特逐像素光照,可以看出逐顶点的黑白交界处有着明显的锯齿,逐像素的则更加平滑,半兰伯特的黑白分别没有前面两个的那么明显。


             现在,我们来计算一下高光反射模型,也可以说是Phong光照模型,计算公式为:

specular = Ks * lightColor * pow(max(dot(view, reflect), 0), gloss)         

其中Ks 为高光反射颜色, lightColor为光照颜色,view为相机方向,reflect为入射光反射方向,gloss为材质的光泽度,用来控制高光区域的大小,光泽度越大,区域越小。

同样的,这个光照模型也有两种方式:逐顶点着色,逐像素着色。

Phong模型的逐顶点着色(其中反射方向用reflect函数求得)

Shader "Unlit/SpecularVertexLevel"
{
	Properties
	{
		_Diffuse("Diffuse",Color)=(1,1,1,1)
		_Specular("Specular",Color)=(1,1,1,1)
		_Gloss("Gloss",Range(8.0,256))=20
	}
	SubShader
	{
		Pass
		{
		Tags{"LightMode"="ForwardBase"}
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			#include "Lighting.cginc"
			fixed4 _Diffuse;
			fixed4 _Specular;
			float _Gloss;

			struct a2v{
			float4 vertex:POSITION;
			float3 normal:NORMAL;
			};

			struct v2f{
			float4 pos:SV_POSITION;
			fixed3 color:COLOR;
			};

			v2f vert(a2v v)
			{
			v2f o;
			o.pos=UnityObjectToClipPos(v.vertex);
			fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz;
			fixed3 worldNormal=normalize(mul(v.normal,(float3x3)unity_WorldToObject));
			fixed3 worldLightDir=normalize(_WorldSpaceLightPos0.xyz);
			fixed3 diffuse=_LightColor0.rgb*_Diffuse.rgb*saturate(dot(worldNormal,worldLightDir));

			fixed3 reflectDir=normalize(reflect(-worldLightDir,worldNormal));
			fixed3 viewDir=normalize(_WorldSpaceCameraPos.xyz-mul(unity_ObjectToWorld,v.vertex).xyz);
			fixed3 specular=_LightColor0.rgb*_Specular.rgb*pow(saturate(dot(reflectDir,viewDir)),_Gloss);
			o.color=ambient+diffuse+specular;
			return o;
			}

			fixed4 frag(v2f i):SV_Target{
			return fixed4(i.color,1.0f);
			}
			ENDCG
		}
	}
	FallBack "Specular"
}

    Phong模型的逐像素着色(其中反射方向用reflect函数求得)    

Shader "Unlit/SpecularPixelLevel"
{
	Properties
	{
		_Diffuse("Diffuse",Color)=(1,1,1,1)
		_Specular("Specular",Color)=(1,1,1,1)
		_Gloss("Gloss",Range(8.0,256))=20
	}
	SubShader
	{
		Pass
		{
		Tags{"LightMode"="ForwardBase"}
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			#include "Lighting.cginc"
			fixed4 _Diffuse;
			fixed4 _Specular;
			float _Gloss;

			struct a2v{
			float4 vertex:POSITION;
			float3 normal:NORMAL;
			};

			struct v2f{
			float4 pos:SV_POSITION;
			float3 worldNormal:TEXCOORD0;
			float3 worldPos:TEXCOORD1;
			};

			v2f vert(a2v v)
			{
			v2f o;
			o.pos=UnityObjectToClipPos(v.vertex);
			o.worldNormal=mul(v.normal,(float3x3)unity_WorldToObject);
			o.worldPos=mul(unity_ObjectToWorld,v.vertex).xyz;
			return o;
			}

			fixed4 frag(v2f i):SV_Target
			{
			fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz;
			fixed3 worldNormal=normalize(i.worldNormal);
			fixed3 worldLightDir=normalize(_WorldSpaceLightPos0.xyz);
			fixed3 diffuse=_LightColor0.rgb*_Diffuse.rgb*saturate(dot(worldNormal,worldLightDir));

			fixed3 reflectDir=normalize(reflect(-worldLightDir,worldNormal));
			fixed3 viewDir=normalize(_WorldSpaceCameraPos.xyz-i.worldPos.xyz);
			fixed3 specular=_LightColor0.rgb*_Specular.rgb*pow(saturate(dot(reflectDir,viewDir)),_Gloss);
			return fixed4(ambient+diffuse+specular,1.0);
			}
			ENDCG
		}
	}
	FallBack "Specular"
}

 某些情况下计算更快,更正确的光照模型——Blinn光照模型

这是一种对Phong光照模型进行了修改的模型,并不是Phong模型的改进,它引入了一个新的矢量h ,目的是避免计算反射方向reflect,计算方式为:

                                h=normalize(view+light)   

            其中view为相机方向,light为光源入射方向 。

            那么使用了Blinn光照模型的逐像素着色   

Shader "Unlit/BlinnPhong"
{
	Properties
	{
		_Diffuse("Diffuse",Color)=(1,1,1,1)
		_Specular("Specular",Color)=(1,1,1,1)
		_Gloss("Gloss",Range(8.0,256))=20
	}
	SubShader
	{
		Pass
		{
		Tags{"LightMode"="ForwardBase"}
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			#include "Lighting.cginc"
			#include "UnityCG.cginc"
			fixed4 _Diffuse;
			fixed4 _Specular;
			float _Gloss;

			struct a2v{
			float4 vertex:POSITION;
			float3 normal:NORMAL;
			};

			struct v2f{
			float4 pos:SV_POSITION;
			float3 worldNormal:TEXCOORD0;
			float3 worldPos:TEXCOORD1;
			};

			v2f vert(a2v v)
			{
			v2f o;
			o.pos=UnityObjectToClipPos(v.vertex);
			//o.worldNormal=mul(v.normal,(float3x3)_World2Object);
			o.worldNormal=UnityObjectToWorldNormal(v.normal);
			o.worldPos=mul(unity_ObjectToWorld,v.vertex).xyz;
			return o;
			}

			fixed4 frag(v2f i):SV_Target
			{
			fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz;
			fixed3 worldNormal=normalize(i.worldNormal);
			//fixed3 worldLightDir=normalize(_WorldSpaceLightPos0.xyz);
			fixed3 worldLightDir=normalize(UnityWorldSpaceLightDir(i.worldPos));
			fixed3 diffuse=_LightColor0.rgb*_Diffuse.rgb*saturate(dot(worldNormal,worldLightDir));

			fixed3 reflectDir=normalize(reflect(-worldLightDir,worldNormal));
			//fixed3 viewDir=normalize(_WorldSpaceCameraPos.xyz-i.worldPos.xyz);
			fixed3 viewDir=normalize(UnityWorldSpaceViewDir(i.worldPos));
			fixed3 halfDir=normalize(worldLightDir+viewDir);
			fixed3 specular=_LightColor0.rgb*_Specular.rgb*pow(saturate(dot(worldNormal,halfDir)),_Gloss);
			return fixed4(ambient+diffuse+specular,1.0);
			}
			ENDCG
		}
	}
	FallBack "Specular"
}

           最终效果为:UnityShader学习_基本光照模型的实现_第2张图片                 从里到外分别为逐顶点着色,逐像素着色,Bling模型的逐像素着色,其中漫反射颜色为浅黑色,这是为了更好的看出高光反射效果。由图可以看出逐顶点着色并不是一个圆形,这是对颜色进行线性插值的结果,但是高光反射的计算是非线性的。而使用Bling-Phong光照模型的高光部分更大更亮。                                                                                                                                             至此一个完整的标准光照模型就此完成,尽管标准光照模型并不是很符合现实,但是其简单,易于计算,更容易上手。                                                         

     

你可能感兴趣的:(Shader)