书接上文,这篇文章我们要在上文的基础上实现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" "Queue" = "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过渡的)。