好长一段时间都没写过博客了,身边一有各种杂七杂八的事就懒散下来了,我要振作(╯‵□′)╯""┻━┻。楼主在写这篇博客的时候室友突然要和几个同班同学去云南玩耍个一星期,搞得楼主差点没心情继续往下写了,我也想和他们一起来场说走就走的旅行ಥ_ಥ,不行,我要振作(╯‵□′)╯""┻━┻!!

      好吧扯远了,自己学shader也有一段时间了(自学这玩意儿真的很苦逼啊,遇到问题得各种尝试,想不通为何Unity官方对这方面的教程这么少),但是从来没在啥项目中真正去编写一些shader,因此就想提取一下在游戏中使用普通逻辑难以实现的相关技术,然后用shader实现它,相信这也是一种很好地学习方法。我们玩游戏时常常能预览角色的技能范围或者点击一个npc或者玩家的时候能看到角色的光环,就比如最近楼主在玩的天涯明月刀ol:




unity光圈_阴影




可以看到这些光环是根据地形来动态地投影形状的,下面我们就来在Unity3D引擎中实现这些效果,楼主使用的是Unity 4.6.4f1版本。






实现方法一:使用阴影来模拟


      人物的阴影是游戏引擎根据人物模型的顶点所在位置与光线形成向量,接收阴影的物体计算出这些向量与自己是否有交点,如果有交点,即将颜色渲染成灰黑色。因此我们只需要计算接收光环的物体与光环的y轴向量或光线方向的交点便可以实现。




新建一个场景,这里楼主选用了一个Capsule胶囊体作为主角:



unity光圈_shader_02




然后往Capsule上挂一个Plane,这个Plane没什么很大的作用,只是为了方便观察光环的半径大小,由于不需要显示出来,我们把Plane上的Mesh Renderer给勾掉,楼主给这个Plane加上了一个“PlayerCircle”,用来待会儿在代码中查找这个Plane的位置:



unity光圈_光环_03




然后我们在Plane下新建一个空物体GameObject,这个物体到Capsule的世界坐标系距离就作为光环的半径



unity光圈_unity光圈_04




接下来我们写一个Projection1.cs脚本,这个脚本向shader中提供主角的位置以及光环的半径信息:


Projection1.cs:


using UnityEngine;
using System.Collections;

public class Projection1 : MonoBehaviour {
	
	Transform mPlayerCircle;
	public Material shaderMaterial;
	
	// Use this for initialization
	void Awake () {
		//找到光环或者是主角
		GameObject circleObj = GameObject.FindGameObjectWithTag ("PlayerCircle");
		if (!circleObj)
		{
			return;
		}
		mPlayerCircle = circleObj.transform;
		
		//计算光环的半径
		float dis = 0;
		foreach(Transform son in mPlayerCircle)
		{
			dis = Vector3.Distance(mPlayerCircle.position,son.position);
		}
		
		//获取物体上的所有材质
		Material[] objMaterials = renderer.materials;
		foreach(Material m in objMaterials)
		{
			//通过材质的名字来查找材质
			//由于实际运行时有可能会有多个角色会使用这个材质,而材质的参数可能会因角色的不同而不同,因此我们不使用sharedMaterial,而使用拷贝的实例
			if("Projection1 (Instance)" == m.name)
			{
				shaderMaterial = m;
				break;
			}
		}
		
		//告诉shader主角光环的半径
		if(shaderMaterial && mPlayerCircle)
		{
			shaderMaterial.SetFloat("_circleRadius",dis);
		}
	}
	
	// Update is called once per frame
	void Update () {
		if(mPlayerCircle)
		{
			if(shaderMaterial)
			{
				Vector3 pos = mPlayerCircle.position;
				//告诉shader主角的光环的世界坐标
				shaderMaterial.SetVector("_circlePos",new Vector4(pos.x,pos.y,pos.z,1));
				
				//告诉shader主角光环的两个矩阵:世界坐标系转模型坐标系/模型坐标系转世界坐标系
				shaderMaterial.SetMatrix("_wTom",mPlayerCircle.renderer.worldToLocalMatrix);
				shaderMaterial.SetMatrix("_mTow",mPlayerCircle.renderer.localToWorldMatrix);
			}
		}
	}
}





对应的,我们还需要写一个Projection1.shader


Projection1.shader:

Shader "Custom/Projection1" {
 Properties {
	_circleRadius("circleRadius",float) = 0
	_circlePos("circlePos",vector) = (0,0,0,1)
	_circleColor("circleColor",color) = (1,1,1,1)
	_width("circleWidth",Range(3,10)) = 0
 }
SubShader {
	Pass{
		Tags {"RenderType" = "Transparent"}

		Blend SrcAlpha OneMinusSrcAlpha

		CGPROGRAM
		#pragma vertex vert
		#pragma fragment frag
		#include "UnityCG.cginc"

		float4 _circlePos;
		float _circleRadius;
		float4 _LightColor0;
		float4 _circleColor;
		float _width;

		struct v2f
		{
			float4 pos:SV_POSITION; 	//投影空间的顶点
			float4 vertexPos:TEXCOORD3; //世界空间的顶点
			float3 litDir:TEXCOORD0; 	//顶点的光照方向或一个y轴垂直分量
			float3 disDir:TEXCOORD1;	//顶点到光环中心位置的距离
			float4 uv:TEXCOORD2; 		//顶点的颜色
		};

		v2f vert(appdata_base v)
		{
			v2f o;
			//将自己的顶点坐标转换成世界坐标
			float4 worldPos = mul(_Object2World,v.vertex);
			o.vertexPos = worldPos;
			//每个点默认的颜色为_LightColor0;
			o.uv = _LightColor0;
			//光线的方向
			//o.litDir = WorldSpaceLightDir(v.vertex);
			o.litDir.y = 1;
			o.litDir.x = 0;
			o.litDir.z = 0;
			//该物体到光环的向量
			o.disDir = (_circlePos-worldPos).xyz;

			o.pos = mul(UNITY_MATRIX_MVP,v.vertex);
			return o;
		}
		float4 frag(v2f o):COLOR
		{
			float3 litDir = normalize(o.litDir);
			float3 disDir = o.disDir;
			float c = length(disDir);
			disDir = normalize(disDir);
			float cosB = dot(disDir,litDir);
			float sinB = sin(acos(max(0,cosB)));
			float b = sinB*c;

			if((b>_circleRadius) || (b<_circleRadius-_circleRadius/_width))
			{
			 return o.uv*-1;
			}
			return _circleColor;
		}
		ENDCG
		}
	}
	FallBack "Diffuse"
}





这个shader的工作就是计算物体当前的顶点是否在光环的半径内,如果在的话就把顶点的颜色更换掉就好了,用一些简单的数学知识就能搞定,我们还是看看图最直接,下图中的描述对应着shader中的内容:



unity光圈_unity3d_05




对于刚接触shader没多久的同学来说,同学们一定会注意到v2f结构体


struct v2f
{
	float4 pos:SV_POSITION; 	//投影空间的顶点
	float4 vertexPos:TEXCOORD3; //世界空间的顶点
	float3 litDir:TEXCOORD0; 	//顶点的光照方向或一个y轴垂直分量
	float3 disDir:TEXCOORD1;	//顶点到光环中心位置的距离
	float4 uv:TEXCOORD2; 		//顶点的颜色
};



为何在顶点信息结构体v2f中vertexPos和disDir等变量的语义都是TEXCOORD而不是POSITION?因为POSITION是物体的顶点信息,TEXCOORD是物体的uv坐标信息,它们的单位不一样,TEXCOORD与物体的面的uv坐标有关,单位为像素,而POSITION的范围与建模时建模者设定的物体中心有关,单位比像素大,我们需要的是改变物体的uv坐标的颜色,如果使用POSITION来作为判断,将会发现判断的误差相当大,出现颜色成片断裂的情况。




o.uv = _LightColor0;
return o.uv*-1;

由于场景中的物体常常包含1个或多个材质,这两句话的作用是让被渲染的物体在没有接近光环的时候保持原来材质的颜色。


 

float cosB = dot(disDir,litDir);


为嘛cosB就等于dot(disDir,litDir)呢?


由于dot函数计算的是两向量的点积,点积的几何意义是衡量两向量的相似程度,dot(disDir,litDir) = |disDir|*|litDir|*cos<disDir,litDir>,又因为disDir和litDir都被标准化为了单位向量,因此模都为1,因此cosB就等于dot(disDir,litDir)啦




回到场景,新建一个Projection1的材质,使用刚刚写好的Projection1.shader, 自己设定好光环的颜色以及光环的粗细


unity光圈_unity光圈_06




将cs文件和材质赋给你想要接收光环的物体上 ,比如我想让马路、蘑菇、岩石都能接收光环:



unity光圈_unity光圈_07




unity光圈_unity光圈_08




运行游戏,就可以看到效果了:


unity光圈_光环_09




把shader的代码改一下,直接根据光环中心到顶点uv的水平距离来判断该顶点是否要被渲染成光环的颜色,让它更简单直观一些:


Shader "Custom/Projection1" {
Properties {
	_circleRadius("circleRadius",float) = 0
	_circlePos("circlePos",vector) = (0,0,0,1)
	_circleColor("circleColor",color) = (1,1,1,1)
	_width("circleWidth",Range(3,10)) = 0
}
SubShader {
	Pass{
		Tags {"RenderType" = "Transparent"}

		Blend SrcAlpha OneMinusSrcAlpha

		CGPROGRAM
		#pragma vertex vert
		#pragma fragment frag
		#include "UnityCG.cginc"

		float4 _circlePos;
		float _circleRadius;
		float4 _LightColor0;
		float4 _circleColor;
		float _width;

		struct v2f
		{
			float4 pos:SV_POSITION; 		//投影空间的顶点
			float4 vertexPos:TEXCOORD3; 	//世界空间的顶点
			float3 litDir:TEXCOORD0; 		//顶点的光照方向或一个y轴垂直分量
			float3 disDir:TEXCOORD1;	    //顶点到光环中心位置的距离
			float4 uv:TEXCOORD2; 			//顶点的颜色
		};

		v2f vert(appdata_base v)
		{
			v2f o;
			//将自己的顶点坐标转换成世界坐标
			float4 worldPos = mul(_Object2World,v.vertex);
			o.vertexPos = worldPos;
			//每个点默认的颜色为_LightColor0;
			o.uv = _LightColor0;
			//光线的方向
			//o.litDir = WorldSpaceLightDir(v.vertex);
			o.litDir.y = 1;
			o.litDir.x = 0;
			o.litDir.z = 0;
			//该物体到光环的向量
			o.disDir = (_circlePos-worldPos).xyz;

			o.pos = mul(UNITY_MATRIX_MVP,v.vertex);
			return o;
		}
		float4 frag(v2f o):COLOR
		{

			float2 circlePos = float2(_circlePos.x,_circlePos.z);
			float2 wPos = float2(o.vertexPos.x,o.vertexPos.z);
			float2 tempVec3 = circlePos-wPos;
			//float dis = distance(circlePos,wPos);
			float dis = length(tempVec3);
			if((dis>_circleRadius) || (dis<_circleRadius-_circleRadius/_width))
			{
			 return o.uv*-1;
			}
			return _circleColor;

		}
		ENDCG
		}
	}
	FallBack "Diffuse"
}




可以看到效果是一样的:


unity光圈_shader_10

unity光圈_unity3d_11




      这种方法实现起来比较简单,但是也存在一些限制,比如要是假定角色的光环是一张图片而不是纯色,那么用这种方法将不能实现,因为我们没办法以Plane的空间坐标系来对光环图片进行采样;而且这种方法要求每一个物体都挂上写好的cs脚本与材质,在大场景中使用将会产生比较大的开销。因此这种方法建议在一些比较小的场景中使用。






实现方法二:投影

       要想实现投影效果,核心是接收光环的物体需要获取uv坐标信息与对光环图片的采样。Unity中有为我们封装好的投影控件Projector,使用这个控件我们可以轻易实现动态的光环效果。




新建一个场景,同样的新建一个Capsule作为我们的主角:



unity光圈_阴影_12




在Capsule下新建一个空物体,给这个空物体绑上一个Projector组件,这个组件使用自定义的材质Projection2,其他参数比如Near Clip Plane(近剪切平面)、Far Clip Plane(远剪切平面)、Field Of View(投影的视角范围)、Aspect Ratio(剪切平面的宽高比)等根据实际场景情况来调节:



unity光圈_阴影_13




接下来我们就来写材质Projection2要用到的Projection2.shader。


Projection2.shader:

Shader "Custom/Projection2" {
Properties {
	_MainTex ("Base (RGB)", 2D) = "white" {}
}
SubShader {
	Pass{
		Tags {"RenderType"="Transparent"}
		LOD 200
		ZWrite off
		Blend SrcAlpha OneMinusSrcAlpha
		CGPROGRAM
		#pragma vertex vert
		#pragma fragment frag
		#include "UnityCG.cginc"

		sampler2D _MainTex;
		float4x4 _Projector;

		struct v2f
		{
			float4 pos:SV_POSITION;
			float4 texc:TEXCOORD0;
		};

		v2f vert(appdata_base v)
		{
			v2f o;
			o.pos = mul(UNITY_MATRIX_MVP,v.vertex);
			//将顶点变换到矩阵空间
			o.texc = mul(_Projector,v.vertex);
			return o;
		}

		float4 frag(v2f o):COLOR
		{
			//对光环图片进行投影采样
			float4 c = tex2Dproj(_MainTex,o.texc);
			//限制投影方向
			c = c*step(0,o.texc.w);
			return c;
		}

		ENDCG
		}
	}
	FallBack "Diffuse"
}




可以看到这个shader的内容很简单,我们来看看比较关键的几句代码:


o.texc = mul(_Projector,v.vertex); 
float4 c = tex2Dproj(_MainTex,o.texc);




我们结合图片来看这两句话的意思,大致的思想便是通过_Projector来判定物体的某一个顶点是否处于投影组件的剪切平面内,通过tex2Dproj来获得对光环图片的采样颜色


unity光圈_unity光圈_14





Blend SrcAlpha OneMinusSrcAlpha


Blend表示颜色的混合 ,SrcAlpha则是混合的标识,这个标识告诉shader可以混合原图颜色通道中的Alpha值,OneMinusSrcAlpha则是混合的具体算法,告诉shader使用(1-source color)的方法来混合。因此我们可以实现材质的透明度的效果,除此之外我们还可以使用Blend DstColor One等混合来实现透明度的效果。



shader写好后,新建一个Projection2材质,使用这个shader,然后使用的光环图片是这张:


unity光圈_光环_15

unity光圈_unity光圈_16




由于光环的图片并不透明,我们还需要设置一下图片的属性,勾选Alpha from Grayscale和Alpha Is Transparency,将纯黑的颜色值给过滤(或者事先就在ps中准备好一张透明的png图片),同时把Wrap Mode改为Clamp:


unity光圈_unity3d_17




然后看看运行效果:


unity光圈_光环_18



可以看到Projector这个组件很好地将光环的图片投影到各个物体上,由于使用的是Unity内部封装好的矩阵与投影采样算法,而且从始至终只需要实例化一个材质,因此这种实现方法适合于大场景中。使用Projector组件的时候需要注意的是这个组件默认会向周围多个方向同时投影,如果不加限制的话投影出来的效果就会比较难以控制,Projector组件的投影是带有穿透效果的,适当地使用Ignore Layers属性能达到更好的效果。








实现方法三:投影(这种方法比较奇葩,楼主摸索了大半天写出来的,感兴趣的同学们看看得了)

unity光圈_阴影_19


      在方法二中,我们通过使用_Projector矩阵就能计算出物体在投影剪切平面下的对应顶点坐标和uv坐标,那么是不是只要我们自己写出一个矩阵来代替_Projector就可以了呢?我尝试着找到_Projector的相关内容,但是找了好久都没有找到,但是注意到Unity的摄像机也是带有投影的各种属性(如进剪切平面),而且摄像机中确实也开放了他的矩阵属性:Camera.projectionMatrix,那么我们就利用这个矩阵来进行探索。




新建一个场景,与方法一中一样,新建一个Capsule、一个Plane、一个用来计算光环半径的空物体GameObject。


unity光圈_shader_20




新建一个摄像机,作为Capsule的子物体,将这个摄像机的视角向下,适当调整Clipping Planes里的属性,让摄像机的远剪切平面大致与地面齐平就可以了(不设置也没问题):


unity光圈_unity光圈_21




接下来,与方法一一样,我们写个Projection1_1.cs脚本:


Projection1_1.cs:

using UnityEngine;
using System.Collections;

public class Projection1_1 : MonoBehaviour {
	
	Transform mPlayerCircle;
	public Material shaderMaterial;
	public Camera circleCamera;
	
	// Use this for initialization
	void Awake () {
		GameObject circleObj = GameObject.FindGameObjectWithTag ("PlayerCircle");
		if (!circleObj) {
			return;
		}
		mPlayerCircle = circleObj.transform;
		float dis = 0;
		foreach(Transform son in mPlayerCircle)
		{
			dis = Vector3.Distance(mPlayerCircle.position,son.position);
		}
		Material[] objMaterials = renderer.materials;
		foreach(Material m in objMaterials)
		{
			if("Projection1_1 (Instance)" == m.name)
			{
				shaderMaterial = m;
				break;
			}
		}
		
		//告诉shader主角光环的半径
		if(shaderMaterial && mPlayerCircle)
		{
			shaderMaterial.SetFloat("_circleRadius",dis);
			
			//获取指定摄像机的投影矩阵
			if(circleCamera)
			{
				Matrix4x4 cameraPro = circleCamera.projectionMatrix;
				shaderMaterial.SetMatrix("_cameraProMatrix",cameraPro);
			}
			
		}
	}
	
	// Update is called once per frame
	void Update () {
		if(mPlayerCircle)
		{
			if(shaderMaterial)
			{
				Vector3 pos = mPlayerCircle.position;
				//告诉shader主角的光环的世界坐标
				shaderMaterial.SetVector("_circlePos",new Vector4(pos.x,pos.y,pos.z,1));
				
			}
		}
	}
}





然后写一个Projection1_1.shader:


Projection1_1.shader:

Shader "Custom/Projection1_1" {
Properties {
	_circleRadius("circleRadius",float) = 0
	_circlePos("circlePos",vector) = (0,0,0,1)
	_MainTex("MainTex",2D) = "white"{}
}
SubShader {
	Pass{
		Tags {"RenderType" = "Transparent"}

		Blend SrcAlpha OneMinusSrcAlpha

		CGPROGRAM

		#pragma vertex vert
		#pragma fragment frag
		#include "UnityCG.cginc"

		float4 _circlePos;
		float _circleRadius;
		float4x4 _cameraProMatrix; //指定摄像机的投影矩阵
		float4 _LightColor0;
		sampler2D _MainTex;

		struct v2f
		{
			float4 pos:SV_POSITION;
			float4 vertexPos:TEXCOORD3;
			float4 uv:TEXCOORD2;
			float4 tex:TEXCOORD4;
		};

		v2f vert(appdata_base v)
		{
			v2f o;
			//将自己的顶点坐标转换成世界坐标
			float4 worldPos = mul(_Object2World,v.vertex);
			o.vertexPos = worldPos;

			float4x4 proj = _cameraProMatrix;
			o.tex = mul(proj,v.vertex);
			o.uv = _LightColor0;

			o.pos = mul(UNITY_MATRIX_MVP,v.vertex);
			return o;
		}
		float4 frag(v2f o):COLOR
		{
			float2 circlePos = float2(_circlePos.x,_circlePos.z);
			float2 wPos = float2(o.vertexPos.x,o.vertexPos.z);
			float2 tempVec3 = circlePos-wPos;
			float dis = length(tempVec3);
			if((dis>_circleRadius))
			{
			 return o.uv*-1;
			}
			return tex2Dproj(_MainTex,o.tex);
		}
		ENDCG
		}
	}
	FallBack "Diffuse"
}





同样的,我们给接收光环的物体绑定上脚本和材质,为了方便观察效果,先使用一些比较显眼的图片作为_MainTex,比如楼主这里使用了无冬ol的一张壁纸:


unity光圈_unity3d_22




运行效果如下:


unity光圈_光环_23




可以发现这完全不是我们想要的效果,这说明仅仅使用Camera.projectionMatrix来计算是不够的,一定还缺少了其他的矩阵。仔细思考一下,可以发现Camera.projectionMatrix是用来将物体投影到相机屏幕空间的矩阵,不像_Projector是投射到每一个物体表面的矩阵,那么我们就可以猜测只要正确处理渲染到相机屏幕的过程,就可以获取正确的投影uv颜色值,而渲染到相机屏幕的过程正是UNITY_MATRIX_MVP。UNITY_MATRIX_MVP即Model空间变换矩阵*View空间变换矩阵*Projection空间变换矩阵,其中的Model对应被渲染物体的_Object2World矩阵;View对应UNITY_MATRIX_V,放在每一个相机中看就是每一个相机自己空间矩阵;Projection对应UNITY_MATRIX_P,放在每一个相机中看就是每一个相机自己的投影矩阵。因此,接下来我们尝试获取摄像机屏幕对应的uv颜色值。




MVP中的M、P我们都可以获取,剩下的V就是Camera.worldToCameraMatrix,于是我们来修改Projection1_1.cs与Projection1_1.shader:


Projection1_1.cs:

using UnityEngine;
using System.Collections;

public class Projection1_1 : MonoBehaviour {
	
	Transform mPlayerCircle;
	public Material shaderMaterial;
	public Camera circleCamera;
	
	// Use this for initialization
	void Awake () {
		GameObject circleObj = GameObject.FindGameObjectWithTag ("PlayerCircle");
		if (!circleObj) {
			return;
		}
		mPlayerCircle = circleObj.transform;
		float dis = 0;
		foreach(Transform son in mPlayerCircle)
		{
			dis = Vector3.Distance(mPlayerCircle.position,son.position);
		}
		Material[] objMaterials = renderer.materials;
		foreach(Material m in objMaterials)
		{
			if("Projection1_1 (Instance)" == m.name)
			{
				shaderMaterial = m;
				break;
			}
		}
		
		//告诉shader主角光环的半径
		if(shaderMaterial && mPlayerCircle)
		{
			shaderMaterial.SetFloat("_circleRadius",dis);
			
			//获取指定摄像机的投影矩阵
			if(circleCamera)
			{
				Matrix4x4 cameraPro = circleCamera.projectionMatrix;
				shaderMaterial.SetMatrix("_cameraProMatrix",cameraPro);
				Matrix4x4 cameraPro1 = circleCamera.worldToCameraMatrix;
				shaderMaterial.SetMatrix("_cameraLocalMatrix",cameraPro1);
			}
			
		}
	}
	
	// Update is called once per frame
	void Update () {
		if(mPlayerCircle)
		{
			if(shaderMaterial)
			{
				Vector3 pos = mPlayerCircle.position;
				//告诉shader主角的光环的世界坐标
				shaderMaterial.SetVector("_circlePos",new Vector4(pos.x,pos.y,pos.z,1));
				
			}
		}
	}
}







Projection1_1.shader:



Shader "Custom/Projection1_1" {
Properties {
	_circleRadius("circleRadius",float) = 0
	_circlePos("circlePos",vector) = (0,0,0,1)
	_MainTex("MainTex",2D) = "white"{}
}
SubShader {
	Pass{
		Tags {"RenderType" = "Transparent"}

		Blend SrcAlpha OneMinusSrcAlpha

		CGPROGRAM

		#pragma vertex vert
		#pragma fragment frag
		#include "UnityCG.cginc"

		float4 _circlePos;
		float _circleRadius;
		float4x4 _cameraProMatrix; //指定摄像机的投影矩阵
		float4x4 _cameraLocalMatrix; //指定摄像机的模型空间矩阵
		float4 _LightColor0;
		sampler2D _MainTex;

		struct v2f
		{
			float4 pos:SV_POSITION;
			float4 vertexPos:TEXCOORD3;
			float4 uv:TEXCOORD2;
			float4 tex:TEXCOORD4;
		};

		v2f vert(appdata_base v)
		{
			v2f o;
			//将自己的顶点坐标转换成世界坐标
			float4 worldPos = mul(_Object2World,v.vertex);
			o.vertexPos = worldPos;

			float4x4 proj = _cameraProMatrix;
			//设置矩阵的级联
			proj = mul(proj,_cameraLocalMatrix);
			proj = mul(proj,_Object2World);
			o.tex = mul(proj,v.vertex);

			o.uv = _LightColor0;

			o.pos = mul(UNITY_MATRIX_MVP,v.vertex);
			return o;
		}
		float4 frag(v2f o):COLOR
		{
			float2 circlePos = float2(_circlePos.x,_circlePos.z);
			float2 wPos = float2(o.vertexPos.x,o.vertexPos.z);
			float2 tempVec3 = circlePos-wPos;
			float dis = length(tempVec3);
			if((dis>_circleRadius))
			{
			 return o.uv*-1;
			}
			return tex2Dproj(_MainTex,o.tex);
		}
		ENDCG
		}
	}
	FallBack "Diffuse"
}





运行效果如下:


unity光圈_unity3d_24




可以看到uv的颜色能正确地按照图片来了,但是还有许多问题。由于只要在光环范围内,渲染过程就会一直重复,无论我们把图片的Warp Mode设置成repeat或是clamp都会有这种情况的发生:


unity光圈_unity光圈_25




上面这图片向我们展示了一个问题就是角色在平移的过程中,uv的像素并不会跟着一起移动;角色旋转的过程中,uv的像素同样不会跟着一起旋转,这个过程就像我们平移一个相机一样,相机的视野在变化,但是相机视野内的物体不会自动跟着相机一起平移。我们再修改一下cs与shader脚本,需要解决三个问题:缩放、平移和旋转。


我们可以设置一个缩放矩阵来控制uv的缩放,再设置一个旋转矩阵来控制uv的旋转,楼主的线性代数不是很好,我们去网上找一下矩阵的公式:D


unity光圈_光环_26






矩阵公式有了以后,我们就可以往cs和shader中添加代码了




在cs文件中,我们主要添加这几句:


//设置缩放矩阵
    Matrix4x4 scaleMatrix = Matrix4x4.identity;
    scaleMatrix.m00 = xScaleValue;
    scaleMatrix.m11 = yScaleValue;
    scaleMatrix.m22 = zScaleValue;
    shaderMaterial.SetMatrix("_scaleMatrix",scaleMatrix);

    rotationAngle = mPlayerCircle.eulerAngles.y;

    //设置旋转矩阵
    Matrix4x4 rotateMatrix = Matrix4x4.identity;

    rotateMatrix.m00 = Mathf.Cos(rotationAngle);
    rotateMatrix.m01 = Mathf.Sin(rotationAngle);
    rotateMatrix.m10 = -Mathf.Sin(rotationAngle);
    rotateMatrix.m11 = Mathf.Cos(rotationAngle);
    shaderMaterial.SetMatrix("_rotateMatrix",rotateMatrix);




在shader中,我们主要添加这几句:


proj = mul(proj,_scaleMatrix);
o.tex = mul(_rotateMatrix,o.tex);
    //平移uv坐标
    o.tex.x += _xOffset;
    o.tex.y += _yOffset;




然后在编辑的时候,又遇到了问题

unity光圈_shader_27


unity光圈_阴影_28



大家可以在这个gif图片上看到缩放没有什么大问题,主要是旋转,我们可以很明显的看到圆心在正前方的不远处,这种情况下只能平移uv去找到圆心,那么当角色旋转的时候误差就会比较大了,楼主尝试了许多方法想通过换算某些比例来自动找到圆心(比如重新计算texture2D的纹理等),但是发现都有误差,于是这个旋转的效果只能将就这样了(看看就好)

unity光圈_unity3d_29

。。



最后我们根据角色的移动来计算uv偏移的比例:


//设置平移变量
     float xOffset = (mNowPos.x - mPrePos.x) / tXScaleValue;
     float yOffset = (mNowPos.z - mPrePos.z) / tYScaleValue;
     tXSumValue += xOffset;
     tYSumValue += yOffset;
     shaderMaterial.SetFloat ("_xOffset", tXSumValue);
     shaderMaterial.SetFloat ("_yOffset", tYSumValue);




最终代码如下:


Projection1_1.cs:



using UnityEngine;
using System.Collections;

public class Projection1_1 : MonoBehaviour {
	
	Transform mPlayerCircle;
	public Material shaderMaterial;
	public Camera circleCamera;
	public float xScaleValue; //缩放uv.x的比例
	public float yScaleValue; //缩放uv.y的比例
	public float zScaleValue; //缩放uv.z的比例
	public float rotationAngle; //旋转的角度
	public float tXScaleValue; //平移uv.x的比例
	public float tYScaleValue; //平移uv.y的比例
	public float tXSumValue; //平移uv.x的总值
	public float tYSumValue; //平移uv.y的总值
	Vector3 mPrePos; //上一帧的位置
	Vector3 mNowPos; //这一帧的位置
	
	// Use this for initialization
	void Awake () {
		GameObject circleObj = GameObject.FindGameObjectWithTag ("PlayerCircle");
		if (!circleObj) {
			return;
		}
		mPlayerCircle = circleObj.transform;
		float dis = 0;
		foreach(Transform son in mPlayerCircle)
		{
			dis = Vector3.Distance(mPlayerCircle.position,son.position);
		}
		Material[] objMaterials = renderer.materials;
		foreach(Material m in objMaterials)
		{
			if("Projection1_1 (Instance)" == m.name)
			{
				shaderMaterial = m;
				break;
			}
		}
		
		//告诉shader主角光环的半径
		if(shaderMaterial && mPlayerCircle)
		{
			shaderMaterial.SetFloat("_circleRadius",dis);
			
			//获取指定摄像机的投影矩阵
			if(circleCamera)
			{
				Matrix4x4 cameraPro = circleCamera.projectionMatrix;
				shaderMaterial.SetMatrix("_cameraProMatrix",cameraPro);
				Matrix4x4 cameraPro1 = circleCamera.worldToCameraMatrix;
				shaderMaterial.SetMatrix("_cameraLocalMatrix",cameraPro1);
			}
			
		}
		tXSumValue = 5.98f;
		tYSumValue = 1.57f;
		mNowPos = mPlayerCircle.position;
		mPrePos = mPlayerCircle.position;
		
	}
	
	// Update is called once per frame
	void Update () {
		//if (0 == Time.frameCount % 10)
		{
			if (mPlayerCircle) {
				mNowPos = mPlayerCircle.position;
				if (shaderMaterial) {
					Vector3 pos = mPlayerCircle.position;
					//告诉shader主角的光环的世界坐标
					shaderMaterial.SetVector ("_circlePos", new Vector4 (pos.x, pos.y, pos.z, 1));
					
					//设置缩放矩阵
					Matrix4x4 scaleMatrix = Matrix4x4.identity;
					scaleMatrix.m00 = xScaleValue;
					scaleMatrix.m11 = yScaleValue;
					scaleMatrix.m22 = zScaleValue;
					shaderMaterial.SetMatrix ("_scaleMatrix", scaleMatrix);
					
					//rotationAngle = mPlayerCircle.eulerAngles.y;
					
					//设置旋转矩阵
					Matrix4x4 rotateMatrix = Matrix4x4.identity;
					
					rotateMatrix.m00 = Mathf.Cos (rotationAngle);
					rotateMatrix.m01 = Mathf.Sin (rotationAngle);
					rotateMatrix.m10 = -Mathf.Sin (rotationAngle);
					rotateMatrix.m11 = Mathf.Cos (rotationAngle);
					shaderMaterial.SetMatrix ("_rotateMatrix", rotateMatrix);
					
					//设置平移变量
					float xOffset = (mNowPos.x - mPrePos.x) / tXScaleValue;
					float yOffset = (mNowPos.z - mPrePos.z) / tYScaleValue;
					tXSumValue += xOffset;
					tYSumValue += yOffset;
					shaderMaterial.SetFloat ("_xOffset", tXSumValue);
					shaderMaterial.SetFloat ("_yOffset", tYSumValue);
					
				}
				mPrePos = mNowPos;
			}
		}
	}
}






Projection1_1.shader:



Shader "Custom/Projection1_1" {
Properties {
	_circleRadius("circleRadius",float) = 0
	_circlePos("circlePos",vector) = (0,0,0,1)
	_MainTex("MainTex",2D) = "white"{}
	_xOffset("xOffset",float) = 0
	_yOffset("yOffset",float) = 0
}
SubShader {
	Pass{
		Tags {"RenderType" = "Transparent"}

		Blend SrcAlpha OneMinusSrcAlpha

		CGPROGRAM

		#pragma vertex vert
		#pragma fragment frag
		#include "UnityCG.cginc"

		float4 _circlePos;
		float _circleRadius;
		float4x4 _cameraProMatrix; //指定摄像机的投影矩阵
		float4x4 _cameraLocalMatrix; //指定摄像机的模型空间矩阵
		float4x4 _scaleMatrix; //缩放图像的矩阵
		float4x4 _rotateMatrix; //旋转图像的矩阵
		float _xOffset;
		float _yOffset;
		float4 _LightColor0;
		sampler2D _MainTex;

		struct v2f
		{
			float4 pos:SV_POSITION;
			float4 vertexPos:TEXCOORD3;
			float4 uv:TEXCOORD2;
			float4 tex:TEXCOORD4;
		};

		v2f vert(appdata_base v)
		{
			v2f o;
			//将自己的顶点坐标转换成世界坐标
			float4 worldPos = mul(_Object2World,v.vertex);
			o.vertexPos = worldPos;

			float4x4 proj = _cameraProMatrix;
			//设置矩阵的级联
			proj = mul(proj,_cameraLocalMatrix);
			proj = mul(proj,_Object2World);
			proj = mul(proj,_scaleMatrix);
			o.tex = mul(proj,v.vertex);
			o.tex = mul(_rotateMatrix,o.tex);

			//平移uv坐标
			o.tex.x += _xOffset;
			o.tex.y += _yOffset;

			o.uv = _LightColor0;

			o.pos = mul(UNITY_MATRIX_MVP,v.vertex);
			return o;
		}
		float4 frag(v2f o):COLOR
		{
			float2 circlePos = float2(_circlePos.x,_circlePos.z);
			float2 wPos = float2(o.vertexPos.x,o.vertexPos.z);
			float2 tempVec3 = circlePos-wPos;
			float dis = length(tempVec3);
			if((dis>_circleRadius))
			{
			 return o.uv*-1;
			}
			return tex2Dproj(_MainTex,o.tex);
		}
		ENDCG
		}
	}
	FallBack "Diffuse"
}





与方法一一样,我们把脚本和材质挂到接收光环的平面上,比如草地、房屋等等:


unity光圈_光环_30





运行效果:


unity光圈_阴影_31


unity光圈_unity3d_32




可以看到这种方法实现起来要处理的问题比较多,要达到很好的效果还得优化许多细节,就当一种另类的思路吧= =。








扩展:



楼主尝试了很久只找到了一个办法,就是在光环周围按照一定的密度设置射线点,这些射线点垂直于xz平面发射射线,获取这些射线返回的顶点信息,但是依然会有许多问题,比如开销大、顶点高度会不平滑等等。。




那我们能不能直接修改这些内置好的阴影信息,让它们变成光环纹理的颜色呢,楼主测试了许多遍,发现这些内置的阴影只允许更改uv的坐标和Alpha值,但是uv的颜色是不允许更改的。。于是这种方法是否可行我也没继续深究下去了,有兴趣的同学可以研究研究,要修改这些内置的阴影,可以使用TRANSFER_SHADOW_CASTER以及SHADOW_CASTER_FRAGMENT等宏。





       这篇文章就到这里啦,楼主正在努力研究其他好玩的东西~ 由于水平有限,本篇文章讲述的观点有误还请大家轻喷,顺便帮忙指正,共同进步,么么哒:D     

unity光圈_阴影_33