模仿NGUI实现SoftClip(一)

用过NGUI的童鞋都知道UIPanel可以设置一个矩形的Clip区域,它下辖的UIWidget都只能在Clip区域内显示。今天我就模仿UIPanel实现类似的Clip功能,让一个3D面片只能在我所指定的矩形区域内显示。相信看完这篇文章,UIPanel的Clip原理也就不再神秘了。


为了让难度循序渐进,我们先实现HardClip(边缘没有Alpha过渡,直接硬切)。

实现原理:用脚本算出显示区域ClipArea的大小和位置,把计算结果通过Material传给Shader,让Shader在显示时做判断,如果该像素在ClipArea外就让其Alpha为0,达到裁切的目的。


  准备1:打开一个Unity项目,自己找一张图片放到项目里用于显示。

  准备2:创建一个Unlit Shader取名为Clip,再创建一个Material也取名Clip并选择“Unlit/Clip”这个Shader,然后把图片拖到Texture属性里。

  准备3:在场景中添加一个叫做ClipPanel的空物体(用于配置Clip),再添加一个叫做ClipDrawer的Quad物体并把Clip.Material拖上去(用于显示)。


准备工作做好后,先创建一个脚本ClipPanel.cs并拖到ClipPanel上,有了该脚本的4个变量,我们就能确定显示区域的大小位置了。脚本内容如下

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class ClipPanel : MonoBehaviour
{
	public float clipWidth = 4f; // 显示区域宽度
	public float clipHeight = 3f; // 显示区域高度

	public float offsetHor = 0; // 显示区域水平偏移
	public float offsetVer = 0; // 显示区域垂直偏移

	// 在Scene窗口中绘制一个白框用来定位显示区域
	private void OnDrawGizmos()
	{
		Vector3 panelPointLB = new Vector3(offsetHor - clipWidth * 0.5f, offsetVer - clipHeight * 0.5f);
		Vector3 panelPointRT = new Vector3(offsetHor + clipWidth * 0.5f, offsetVer + clipHeight * 0.5f);

		Vector3 worldPointLB = transform.TransformPoint(panelPointLB);
		Vector3 worldPointRT = transform.TransformPoint(panelPointRT);
		Vector3 worldPointLT = new Vector3(worldPointLB.x, worldPointRT.y, worldPointRT.z);
		Vector3 worldPointRB = new Vector3(worldPointRT.x, worldPointLB.y, worldPointLB.z);

		Gizmos.DrawLine(worldPointLB, worldPointLT);
		Gizmos.DrawLine(worldPointRB, worldPointRT);
		Gizmos.DrawLine(worldPointLB, worldPointRB);
		Gizmos.DrawLine(worldPointLT, worldPointRT);
	}
}
再创建一个脚本ClipDrawer.cs并拖到ClipDrawer上,然后把ClipPanel物体拖到其panel属性里。这个脚本负责收集ClipPanel的数据然后传递给Shader。脚本内容如下

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class ClipDrawer : MonoBehaviour
{
	public ClipPanel panel;

	private void OnWillRenderObject()
	{
		if (panel != null)
		{
			// 从panel里取得裁切窗口数据,转化为窗口的左下角、右上角两个坐标点(基于panel的本地坐标)
			Vector3 panelPointLB = new Vector3(panel.offsetHor - panel.clipWidth * 0.5f, panel.offsetVer - panel.clipHeight * 0.5f);
			Vector3 panelPointRT = new Vector3(panel.offsetHor + panel.clipWidth * 0.5f, panel.offsetVer + panel.clipHeight * 0.5f);
			// 把这两个点转化为世界坐标点
			Vector3 worldPointLB = panel.transform.TransformPoint(panelPointLB);
			Vector3 worldPointRT = panel.transform.TransformPoint(panelPointRT);
			// 把这两个点转化为ClipDrawer的本地坐标点
			Vector3 localPointLB = transform.InverseTransformPoint(worldPointLB);
			Vector3 localPointRT = transform.InverseTransformPoint(worldPointRT);
			// 恢复为窗口尺寸和偏移数据
			Vector2 localSize = new Vector2(localPointRT.x - localPointLB.x, localPointRT.y - localPointLB.y);
			Vector2 localOffset = (localPointLB + localPointRT) * 0.5f;
			// 合并数据到一个Vector4中并等待发送
			Vector4 clipRange = new Vector4(localSize.x, localSize.y, localOffset.x, localOffset.y);

			Renderer r = GetComponent<Renderer>();
			Material mat = r.materials[0];
			// 把数据发送给Shader,一共4条数据:窗口宽度、窗口高度、窗口水平偏移、窗口垂直偏移(注意这些数据都基于本地坐标)
			mat.SetVector("_ClipRange", clipRange);
		}
	}
}
这个脚本每帧都会把最新的裁切窗口数据发送给自己的Material,供Shader使用。下面到了最关键的Shader部分,如果你对Shader一无所知,我推荐你买本《Unity Shader入门精要》学习一下。这里假定你已经对Shader有一定基础。下面打开Clip.shader并修改为如下内容

Shader "Unlit/Clip"
{
	Properties
	{
		_MainTex ("Texture", 2D) = "white" {}
		_ClipRange("ClipRange", Vector) = (0, 0, 0, 0)
	}
	SubShader
	{
		// 因为需要操控alpha,所以需要设置渲染类型为透明
		Tags { "RenderType"="Transparent" }

		Pass
		{
			// 因为是透明Shader,需要关闭深度写入并开启alpha混合
			ZWrite Off
			Blend SrcAlpha OneMinusSrcAlpha

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

			struct appdata
			{
				float4 vertex : POSITION;
				float2 uv : TEXCOORD0;
			};

			struct v2f
			{
				float2 uv : TEXCOORD0;
				float4 vertex : SV_POSITION;
				// 该二维数组记录该点的xy坐标与显示区域的关系
				float2 relation : TEXCOORD1;
			};

			sampler2D _MainTex;
			float4 _MainTex_ST;
			float4 _ClipRange = float4(1.0, 1.0, 0.0, 0.0);
			
			v2f vert (appdata v)
			{
				v2f o;
				o.vertex = UnityObjectToClipPos(v.vertex);
				o.uv = TRANSFORM_TEX(v.uv, _MainTex);

				// 将顶点的本地坐标值和窗口尺寸相除,如果点在窗口内,结果会落在(-1,1)区间内。这行代码一定要理解
				o.relation = (v.vertex.xy - _ClipRange.zw) / (_ClipRange.xy * 0.5);
				return o;
			}
			
			fixed4 frag (v2f i) : SV_Target
			{
				// sample the texture
				fixed4 col = tex2D(_MainTex, i.uv);
				// 如果像素点的relation值不在[-1, 1]区间内,就把输出颜色的alpha改成0
				if (i.relation.x < -1 || i.relation.x > 1 || i.relation.y < -1 || i.relation.y > 1)
				{
					col.a = 0;
				}
				return col;
			}
			ENDCG
		}
	}
}

Shader里的代码很短,理解了坐标转区间那行代码就可以了。

然后运行场景,调整ClipDrawer和ClipPanel的位置,就能看到图片被裁剪的效果,效果如下图所示

上面的Shader虽然有效但很low,在Shader里分支语句会影响性能,所以if..else..能避免就避免。下面则提供一个高级点的不带if的frag函数

fixed4 frag (v2f i) : SV_Target
{
	// 处理relation,处理后的结果中,大于0表示点在窗口内,否则点在窗口外
	float2 relation1 = float2(1, 1) - abs(i.relation.xy);
	// 不需要分别检查x和y两个坐标,选择其中较小的值做检查即可
	float relation2 = min(relation1.x, relation1.y);
	// 向上取整,大于0的整数表示窗口内的点,小于1的整数表示点在窗口外
	float relation3 = ceil(relation2);
	// 把数据约束到[0, 1]范围内,此时的结果只可能是0或者1
	float relation4 = clamp(relation3, 0, 1);

	// sample the texture
	fixed4 col = tex2D(_MainTex, i.uv);
	// 把输出颜色乘以relation4,得到想要的结果
	col.a *= relation4;
	return col;
}
我们用cg语言自带的各种数学函数来操作数据,最后达到了和if语句相同的结果。

下文我会添加一个带alpha过渡的Soft Clip版本shader,敬请期待。

猜你喜欢

转载自blog.csdn.net/lzdidiv/article/details/78711683