模仿NGUI实现SoftClip(二)

书接上文,这篇文章我们要在上文的基础上实现SoftClip效果。


上图是最终效果,内层白框里面正常显示,外层白框外完全透明,两层白框间透明过渡。


首先我们要在ClipPanel脚本上添加过softWidth和softHeight这两个参数,用来定位内框,修改后的脚本如下:

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; // 显示区域垂直偏移

	public float softWidth = 0; // 显示区域边缘Alpha过渡宽度
	public float softHeight = 0; // 显示区域边缘Alpha过渡高度

	// 在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);

		Vector3 panelSoftPointLB = panelPointLB + new Vector3(softWidth, softHeight);
		Vector3 panelSoftPointRT = panelPointRT - new Vector3(softWidth, softHeight);

		Vector3 worldSoftPointLB = transform.TransformPoint(panelSoftPointLB);
		Vector3 worldSoftPointRT = transform.TransformPoint(panelSoftPointRT);
		Vector3 worldSoftPointLT = new Vector3(worldSoftPointLB.x, worldSoftPointRT.y, worldSoftPointRT.z);
		Vector3 worldSoftPointRB = new Vector3(worldSoftPointRT.x, worldSoftPointLB.y, worldSoftPointLB.z);

		Gizmos.DrawLine(worldSoftPointLB, worldSoftPointLT);
		Gizmos.DrawLine(worldSoftPointRB, worldSoftPointRT);
		Gizmos.DrawLine(worldSoftPointLB, worldSoftPointRB);
		Gizmos.DrawLine(worldSoftPointLT, worldSoftPointRT);

		Gizmos.DrawLine(worldSoftPointLB, worldPointLB);
		Gizmos.DrawLine(worldSoftPointLT, worldPointLT);
		Gizmos.DrawLine(worldSoftPointRB, worldPointRB);
		Gizmos.DrawLine(worldSoftPointRT, worldPointRT);
	}
}
然后在ClipDrawer脚本里,也需要对该新加的数据做一些处理并传递给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);

			// 计算‘显示区域尺寸’和‘Soft区域尺寸’的比值(Soft区域在两侧都存在,所以要除以2个Soft尺寸)
			float softWidthRatio = panel.softWidth < 0.000001f ? 10000 : panel.clipWidth / panel.softWidth * 0.5f;
			float softHeightRatio = panel.softHeight < 0.000001f ? 10000 : panel.clipHeight / panel.softHeight * 0.5f;
			Vector4 clipSoftRatio = new Vector4(softWidthRatio, softHeightRatio, 0, 0);

			Renderer r = GetComponent<Renderer>();
			Material mat = r.materials[0];
			// 把 ClipRange 数据发送给Shader,一共4条数据:窗口宽度、窗口高度、窗口水平偏移、窗口垂直偏移(注意这些数据都基于本地坐标)
			mat.SetVector("_ClipRange", clipRange);
			// 把 ClipSoftRatio 数据发送给Shader,一共2条数据:窗口宽度比Soft宽度、窗口高度比Soft高度
			mat.SetVector("_ClipSoftRatio", clipSoftRatio);
		}
	}
}
传给Shader的是一个叫做_ClipSoftRatio的向量数据,它记录了窗口区域和过渡区域的比值,后面会解释为什么要传递这个值。


Shader内容变化也不大,先把代码贴出来:

Shader "Unlit/SoftClip"
{
	Properties
	{
		_MainTex("Texture", 2D) = "white" {}
		_ClipRange("ClipRange", Vector) = (0, 0, 0, 0)
		_ClipSoftRatio("ClipSoftRatio", 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;
				// 该二维数组记录该点的 显示区域尺寸 和 Soft区域 的比值
				float2 softRatio : TEXCOORD2;
			};

			sampler2D _MainTex;
			float4 _MainTex_ST;
			float4 _ClipRange = float4(0, 0, 0, 0);
			float4 _ClipSoftRatio = float4(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);
				o.softRatio = _ClipSoftRatio.xy;
				return o;
			}

			fixed4 frag(v2f i) : SV_Target
			{
				// 优化relation,结果若是大于0就表示点在窗口内,否则点在窗口外(显示区域中心点的值为1,区域边缘为0,区域外都是负数,线性递减)
				float2 relation1 = float2(1, 1) - abs(i.relation.xy);
				// 乘以softRatio,此时relation等于1的点不再位于区域中心,而是Soft过渡区域的最内边
				float2 relation2 = relation1 * i.softRatio;
				// 不需要分别检查x和y两个坐标,选择其中较小的值做检查即可
				float relation3 = min(relation2.x, relation2.y);
				// 把数据约束到[0, 1]范围内
				float relation4 = clamp(relation3, 0, 1);

				// sample the texture
				fixed4 col = tex2D(_MainTex, i.uv);
				// 把输出颜色乘以relation4,得到想要的结果
				col.a *= relation4;
				return col;
			}
			ENDCG
		}
	}
}
最后解说一下原理:

我们只看x的值,假定Alpha过渡区域宽度是显示区域宽度的1/4,那么vert函数中算出来的relation值的分布应该入下图所示,显示区域内最左侧为-1,最右侧为1,线性增长。

后续计算出的relation1的值如下图所示,从左到右几个关键点的值分别是0、0.25、1、0.25、0,此时已经距离最终结果很近了。如果我们把relation1的值直接赋给输出颜色,就会得到一个透明度从0到1又回到0的显示区域(过渡区域跟显示区域一样大)。


而我们需要的是下图这样的值,只要把上面的值乘以4(显示区域和过渡区域的比值)再把所有大于1的值都压到1 就能得到,是不是很简单?


把一步步处理得到的relation值乘以最终输出颜色的Alpha,就得到了我们的SoftClip效果!


备注:

    本文的实现机制跟NGUI基本上一致,NGUI自带的带数字后缀的Shader都是支持Soft Clip功能的Shader,后缀数字表示可同时被几个Panel裁剪(最大为3)。

    我们传给Shader的Soft参数是“显示区域和过渡区域的比值”,所以如果过渡区域大小为0的时候,为了避免除数为0我们需要特殊处理(也不应该是负数),所以一旦发现过渡区域值小于等于0,就赋值一个很大的固定比值(本文选了10000而NGUI选了1000)。这也导致了一个小问题,就算把过渡区域设为0,也还是有个很小的过渡区域,大家可以在NGUI上测试一下(把使用SoftClip的Panel的size设的巨大而softness设为0,然后看边缘裁切其实是有Alpha过渡的)。





猜你喜欢

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