# 前言

在 Unity 中,我喜欢使用 Unlit Shader (Unity 这样叫了,我也这样称呼吧)。

不同于被 Unity 封装了大量东西的 Surface Shader,Unlit Shader 中,写什么就是什么。因此相比前者,它的性能自然会高不少。再继续谈论这个话题之前,我们可以做一个小小的 “测验”,以明白下 Surface Shader 与之相比,究竟被封装了多少东西。

测验非常简单,在项目中随便找个文件夹,同时新建两个 Shader,一个 Surface Shader,一个 Unlit Shader。然后单击 Shader 文件,在一边的 Inspector 面板中,这两个文件得属性中,应当都具有 “Compile and ashow cod” 这个按钮,分别进行点击。然后 Unity 就会这两个 Shader 编译,然后显示在编辑器中。

这时就可以非常明显地看出差别:
(基于 Unity5.2.2 f1 版本)
*Unlit Shader:5.6KB 共 161 行

// Compiled shader for PC, Mac & Linux Standalone, uncompressed size: 5.6KB

*Surface Shader: 259.2KB 共 6522 行

// Compiled shader for PC, Mac & Linux Standalone, uncompressed size: 259.2KB

Unlit Shader 编译出来还及不上 Surface Shader 的零头.... 要知道,这些代码是针对每个顶点、像素运行的,自然是越精简越好,如果能用更少的代码实现同样的功能,性能及各方面都会更加优秀。另外还有一点就是,Surface Shader 对于自定义效果来说,也没有 Unlit Shader 那样自由。

或许大家还发现 Surface Shader 属性面板中,还有一个 “Show generated code” 的按钮。点这个按钮的话,可以得到一份经过 Unity 链接、拼合之后的 Shader,这个 Shader 的格式基本上就与 Unlit Shader 差不多了,不过其中包括了相当多的宏定义及判断。虽然注释也有一些,不过同样达到了近八百行!而点开我们新建的 Unlit Shader 呢?不过区区几十行代码。因为 Surface Shader 就是为了兼容性而生的,难免对性能上产生一些牺牲。可是如果我们知道自己需要的功能及面对的问题,那么最好的方式就是自己按照需求 “添加” 才是。

简单来说,Unity 中 Unlit Shader 才是 “正统” 的 Shader (还包括新建那儿的 Image Effect Shader,只是 Unity 中这样叫了,所以我也这样叫了),格式与传统 Shader Vertex->Fragment 流程一致。所以,一般我就喜欢用 “Unlit Shader”,虽然 Unity 叫它 “无光照 Shader”,不过要实现光照之类的,也是相当简单的。

# 实现

# 场景配置

Unlit Shader 精简、自定义功能更强,一般来说性能也更优。不过同样也因此,如果使用 Unlit Shader,默认情况下是不带阴影、光照的。倘若只是需要无光照 —— 即如其所名的其它功能的话,那倒无妨。不过若是需要阴影之类的东西,那就得自己加了。

我这儿针对的是 Forward 渲染路径,不过测试也是可以在 Deffered 渲染路径下正常工作的。

将新建的 Unlit Shader 稍微改个名字,比如我这儿就叫 “Shadow” 了,然后新建一个材质,将其赋给它。回到场景,新建一个 Plane,在上边放置两个球体:一个放置在上方一点,使用 Unity 默认的材质,另一个放置在平面上,把刚才的 “Shadow” 材质赋给它。保证使用 Unity 默认材质的球体投出的阴影可以覆盖在使用 Shadow 材质的球体上 —— 当然现在对于 Shadow 球体是没什么影响的,我们需要完善的就是这点。

如图:

场景配置

# 代码实现

好了,场景配置很简单。接着回到代码,打开 Shader。首先可以将 Unity 自动生成的贴图、UV、雾化之类的代码都删掉 —— 毕竟我们的重点不是这个。
删掉多余代码之后如下:

//Author:CWHISME
//Date:2016.5.31
//Decription:
Shader "CWH/Shadow"
{
	SubShader
	{
		Tags { "RenderType"="Opaque" }
		LOD 100

		Pass
		{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag

			struct appdata
			{
				float4 vertex : POSITION;
			};

			struct v2f
			{
				float4 vertex : SV_POSITION;
			};

			v2f vert (appdata v)
			{
				v2f o;
				o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
				return o;
			}
			
			fixed4 frag (v2f i) : SV_Target
			{
				return float4(1,1,1,1);
			}
			ENDCG
		}
	}
}

现在片段程序默认只是返回纯白色,所以在场景中依然只能看见一片白。所以在我们的 Shader 中,接下来就是稍微添加一点光照效果,不然也不好看啊。

首先将我们的 Pass 设置为 ForwardBase:

Tags{"LightMode"="ForwardBase"}

接着在 appdata 结构体中添加对 normal 变量的接收语义:

float3 normal : NORMAL;

以及传入片段程序的 v2f 结构体 normal 变量:

float3 normal : NORMAL;

然后在顶点片段中计算法线:

o.N = mul(float4(v.normal,0),_World2Object);

最后,在片段程序中计算漫反射:

			fixed4 frag (v2f i) : SV_Target
			{
				float3	L = normalize(_WorldSpaceLightPos0);
				float3 N = normalize(i.N);
				return float4(1,1,1,1)*max(0,dot(L,N));
			}

效果如下:

光照效果

如果对光照不是很明白的话,可以参考我上一篇 Blog。


现在对于我们的球体来说,基本光照效果有了,不过依然不能接收,也不能投射阴影。
如果在 Unlit Shader 中需要使用阴影的话,Unity 提供了几个宏定义使用。基本上都包括在 “AutoLight.cginc” 头文件中。所以首先,我们必须包含这个头文件,并声明 “multi_compile_fwdbase”(似乎是由于 AutoLight 中得一些计算用到了,所以如果不声明这个的话,那么阴影还是无法成功使用的):

			#include "AutoLight.cginc"
			#pragma multi_compile_fwdbase

然后在 v2f 结构体中添加宏:

LIGHTING_COORDS(3,4)

它的功能主要是声明了两个四维变量_LightCoord 及_ShadowCoord,并将其存放在语义 TEXCOORD3、TEXCOORD4 当中。
然后是顶点程序中,添加宏:

TRANSFER_VERTEX_TO_FRAGMENT(o);

其作用就是承接上边定义的两个语义,通过_LightMatrix0 矩阵将顶点转至光照贴图空间,获得相应坐标 (当然除平行光源外,对于平行光源,Unity 就没用_LightCoord 了),以及获取 Shadow 实际贴图坐标放置于上边定义的_ShadowCoord 中。

这时如果回到场景,就发现报错了?这是因为在 AutoLight 的阴影_ShadowCoord 坐标计算中,使用到了 UnityCG 头文件中的一个函数 “ComputeScreenPos”,刚才咱清理代码的时候,不小心也给清出去了.... 所以还是继续加上吧。

最后,来到片段程序中,使用光照衰减乘上之前计算出来的光照颜色,获取最终颜色:

return float4(1,1,1,1)*max(0,dot(L,N))*LIGHT_ATTENUATION(i);

“LIGHT_ATTENUATION” 使用以上获取的信息计算了最终衰减 —— 实现阴影。

如果现在回到场景,依然不会有什么阴影,反而只能发现我们的球体 “似乎” 变得有些透明了?
当然只是错觉而已,因为还差了最后一步:在最后加上 “FallBack "Diffuse"”。

OK,现在算是完了。

回到场景,可以看见球体可以正常投影,也接受了来自上方球体的阴影。效果如下:

最终效果

就是这样。

有兴趣的也可以继续扒一扒 AutoLight。

# 完整源码

//Author:CWHISME
//Date:2016.5.31
//Decription:
Shader "CWH/Shadow"
{
	SubShader
	{
		Tags { "RenderType"="Opaque" }
		LOD 100

		Pass
		{
			Tags{"LightMode"="ForwardBase"}
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag

			#include "AutoLight.cginc"
			#include "UnityCG.cginc"
			#pragma multi_compile_fwdbase

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

			struct v2f
			{
				float4 pos : SV_POSITION;
				float3 N : TEXCOORD1;
				LIGHTING_COORDS(3,4)
			};

			v2f vert (appdata v)
			{
				v2f o;
				o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
				o.N = mul(float4(v.normal,0),_World2Object);
				TRANSFER_VERTEX_TO_FRAGMENT(o);
				return o;
			}
			
			fixed4 frag (v2f i) : SV_Target
			{
				float3	L = normalize(_WorldSpaceLightPos0);
				float3 N = normalize(i.N);
				return float4(1,1,1,1)*max(0,dot(L,N))*LIGHT_ATTENUATION(i);
			}
			ENDCG
		}
	}
	FallBack "Diffuse"
}

# 结尾

之前就已经决定每周至少要写一篇 Blog 了,结果上周因为各种琐事、以及回家后时间太少而缺掉了。只能说:时间不够用啊!这会儿虽然时间不早了,想想就算熬点夜,也得还是补上吧。

以上。