NGUI Panel Soft Clip原理分析

发表于2018-10-23
评论0 5.6k浏览
在给大家介绍NGUI Panel Soft Clip之前,需要先给大家分析下Ngui Panel的软裁剪是怎么实现的。之所以大家不了解NGUI中的Panel Soft Clip,是NGUI对这块做了大量的优化,单独看一行代码,或者一个公式是看不出什么的,单独看shader也会云里雾里,所以这里我们要结合shader和代码一起看,才能真正了解ngui的设计精巧之处。

首先,我们知道,所有的NGUI渲染都是通过UIDrawCall这个类实现的,所以我们主要分析这个类。

NGUI考虑到了嵌套Soft Clip的情况,设计了一套支持多重嵌套的Soft Clip架构,目前最多支持到三重嵌套。不同嵌套对应的Shader是不一样的,具体设置函数在这(分析也贴在上面了)
    void CreateMaterial ()
	{
		mTextureClip = false;
		mLegacyShader = false;
		mClipCount = panel.clipCount;
		string shaderName = (mShader != null) ? mShader.name :
			((mMaterial != null) ? mMaterial.shader.name : "Unlit/Transparent Colored");
		// Figure out the normal shader's name
		shaderName = shaderName.Replace("GUI/Text Shader", "Unlit/Text");
		if (shaderName.Length > 2)
		{
			if (shaderName[shaderName.Length - 2] == ' ')
			{
				int index = shaderName[shaderName.Length - 1];
				if (index > '0' && index <= '9') shaderName = shaderName.Substring(0, shaderName.Length - 2);
			}
		}
		shaderName = shaderName.Replace(TEXTURE_CLIP, "");
        var newShader = shader;
        //如果是Mask裁剪,就直接寻找 Unlit/Transparent Colored(TextureClip)这样 或者 Unlit/Text(TextureClip)这样的shader
        if (panel.clipping == Clipping.TextureMask)
		{
			mTextureClip = true;
            newShader = Shader.Find(shaderName + TEXTURE_CLIP);
		}
		else if (mClipCount != 0)       //如果父节点有多个裁剪panel 就设置成Soft Clip的形式
		{
            //寻找的Shader格式为  Unlit/Transparent Colored 1 或者  Unlit/Transparent Colored 2 这样的 ,后缀就是有多少层,最大ngui写到了3,一般够用了
            var shaderPath = shaderName + " " + mClipCount;
            newShader = Shader.Find(shaderPath);
		}
        else newShader = Shader.Find(shaderName);
		// Always fallback to the default shader 如果找不到 就用默认的shader 一般设置成这个shader,就是达不到你需要的效果了
        if (newShader == null) newShader = Shader.Find("Unlit/Transparent Colored");
		if (mMaterial != null)
		{
			mDynamicMat = new Material(mMaterial);
			mDynamicMat.name = "[NGUI] " + mMaterial.name;
			mDynamicMat.hideFlags = HideFlags.DontSave | HideFlags.NotEditable;
			mDynamicMat.CopyPropertiesFromMaterial(mMaterial);      //这里会把之前的材质的所有属性全部复制到新的材质上(贴图啊什么的 很方便啊)
#if !UNITY_FLASH
			string[] keywords = mMaterial.shaderKeywords;
			for (int i = 0; i < keywords.Length; ++i)
				mDynamicMat.EnableKeyword(keywords[i]);
#endif
			// If there is a valid shader, assign it to the custom material
            if (newShader != null)
			{
                mDynamicMat.shader = newShader;
			}
			else if (mClipCount != 0)
			{
				Debug.LogError(shaderName + " shader doesn't have a clipped shader version for " + mClipCount + " clip regions");
			}
		}
		else
		{
            mDynamicMat = new Material(newShader);
            mDynamicMat.name = "[NGUI] " + newShader.name;
			mDynamicMat.hideFlags = HideFlags.DontSave | HideFlags.NotEditable;
		}
	}
所以到时候我们只需要最终去找Unlit/Transparent Colored 1这样的shader就好了

这个类中与裁剪相关的代码在OnWillRenderObject这个函数中,这个函数是用于设置渲染之前的一些设置的,一般用于设置材质的参数。我将函数贴在下面
	void OnWillRenderObject ()
	{
		UpdateMaterials();
		if (onRender != null) onRender(mDynamicMat ?? mMaterial);
		if (mDynamicMat == null || mClipCount == 0) return;
		if (mTextureClip)
		{
			Vector4 cr = panel.drawCallClipRange;
			Vector2 soft = panel.clipSoftness;
			Vector2 sharpness = new Vector2(1000.0f, 1000.0f);
			if (soft.x > 0f) sharpness.x = cr.z / soft.x;
			if (soft.y > 0f) sharpness.y = cr.w / soft.y;
			mDynamicMat.SetVector(ClipRange[0], new Vector4(-cr.x / cr.z, -cr.y / cr.w, 1f / cr.z, 1f / cr.w));
			mDynamicMat.SetTexture("_ClipTex", clipTexture);
		}
		else if (!mLegacyShader)
		{
			UIPanel currentPanel = panel;
			for (int i = 0; currentPanel != null; )
			{
				if (currentPanel.hasClipping)
				{
					float angle = 0f;
					Vector4 cr = currentPanel.drawCallClipRange;
					// Clipping regions past the first one need additional math
					if (currentPanel != panel)
					{
						Vector3 pos = currentPanel.cachedTransform.InverseTransformPoint(panel.cachedTransform.position);
						cr.x -= pos.x;
						cr.y -= pos.y;
						Vector3 v0 = panel.cachedTransform.rotation.eulerAngles;
						Vector3 v1 = currentPanel.cachedTransform.rotation.eulerAngles;
						Vector3 diff = v1 - v0;
						diff.x = NGUIMath.WrapAngle(diff.x);
						diff.y = NGUIMath.WrapAngle(diff.y);
						diff.z = NGUIMath.WrapAngle(diff.z);
						if (Mathf.Abs(diff.x) > 0.001f || Mathf.Abs(diff.y) > 0.001f)
							Debug.LogWarning("Panel can only be clipped properly if X and Y rotation is left at 0", panel);
						angle = diff.z;
					}
					// Pass the clipping parameters to the shader
					SetClipping(i++, cr, currentPanel.clipSoftness, angle);
				}
				currentPanel = currentPanel.parentPanel;
			}
		}
		else // Legacy functionality
		{
			Vector2 soft = panel.clipSoftness;
			Vector4 cr = panel.drawCallClipRange;
			Vector2 v0 = new Vector2(-cr.x / cr.z, -cr.y / cr.w);
			Vector2 v1 = new Vector2(1f / cr.z, 1f / cr.w);
			Vector2 sharpness = new Vector2(1000.0f, 1000.0f);
			if (soft.x > 0f) sharpness.x = cr.z / soft.x;
			if (soft.y > 0f) sharpness.y = cr.w / soft.y;
			mDynamicMat.mainTextureOffset = v0;
			mDynamicMat.mainTextureScale = v1;
			mDynamicMat.SetVector("_ClipSharpness", sharpness);
		}
	}

可以看到,这里有3个分支,
if (mTextureClip)
{
...
}
对应的是图片裁剪,作用是设置一张Mask图片,然后Panel会根据图片的Alpha值来设置显示区域。

第二个分支就是比较重要了(一般不会走到第三个分支,因为mLegacyShader似乎被写死成为false了)
else if (!mLegacyShader)
{
...
}

这一个分支有什么用呢?就是用于给软裁剪设置裁剪范围的。下面的代码段是我对下面这个分支的理解
		else if (!mLegacyShader)
		{
			UIPanel currentPanel = panel;
            //遍历所有的父Panel(所以支持嵌套的Soft Clip)
			for (int i = 0; currentPanel != null; )
			{
                //如果父Panel节点有裁剪
				if (currentPanel.hasClipping)
				{
                    //这个里面计算裁剪范围和父Panel和当前DrawCall所属的Panel的角度
					float angle = 0f;
					Vector4 cr = currentPanel.drawCallClipRange;
                    //如果有多重Soft Clip的话 就会走到这里
					// Clipping regions past the first one need additional math
					if (currentPanel != panel)
					{
						Vector3 pos = currentPanel.cachedTransform.InverseTransformPoint(panel.cachedTransform.position);
						cr.x -= pos.x;
						cr.y -= pos.y;
						Vector3 v0 = panel.cachedTransform.rotation.eulerAngles;
						Vector3 v1 = currentPanel.cachedTransform.rotation.eulerAngles;
						Vector3 diff = v1 - v0;
                        //其实这个函数就是 把角度限制在-180和180之间
						diff.x = NGUIMath.WrapAngle(diff.x);
						diff.y = NGUIMath.WrapAngle(diff.y);
						diff.z = NGUIMath.WrapAngle(diff.z);
						if (Mathf.Abs(diff.x) > 0.001f || Mathf.Abs(diff.y) > 0.001f)
							Debug.LogWarning("Panel can only be clipped properly if X and Y rotation is left at 0", panel);
                        //因为是界面 所以角度是平面的 只有Z是有效角度
						angle = diff.z;
					}
                    //这里就是真正设置裁剪的地方了,我们看看这里的各个参数的意义吧。
                    //如果只是单层Soft Clip的话
                    // i = 0,
                    // cr就是this.panel的 drawCallClipRange,而这个drawCallClipRange的各个参数意义是这样的: x:中心点X坐标 y:中心点Y坐标 z:panel width的一半, w:panel height的一半
                    // currentPanel.clipSoftness 软裁剪设置的渐变边缘
                    // angle 与父Panel的角度 如果 是单层shader的话,这个角度是0(而且shader里面也不会用到)
                    // Pass the clipping parameters to the shader
                    SetClipping(i++, cr, currentPanel.clipSoftness, angle);
				}
				currentPanel = currentPanel.parentPanel;
			}
		}

可以看到最重要的函数就是SetClipping了 我们走进去瞧瞧。
	void SetClipping (int index, Vector4 cr, Vector2 soft, float angle)
	{
		angle *= -Mathf.Deg2Rad;
		Vector2 sharpness = new Vector2(1000.0f, 1000.0f);
		if (soft.x > 0f) sharpness.x = cr.z / soft.x;
		if (soft.y > 0f) sharpness.y = cr.w / soft.y;
        //这个ClipRange在awake被写死成数量为4的数组,命名规则为:_ClipRange0 _ClipRange1 _ClipRange2 _ClipRange3这样
        //ClipArgs同理 规则为 _ClipArgs0 _ClipArgs1 _ClipArgs2 _ClipArgs3
        //所以我们自己扩展的话,也要跟着他的命名规则走。
        //如果是单层soft clip的话,index = 0,所以设置的参数名字为_ClipRange0和_ClipArgs0
        if (index < ClipRange.Length)
		{
			mDynamicMat.SetVector(ClipRange[index], new Vector4(-cr.x / cr.z, -cr.y / cr.w, 1f / cr.z, 1f / cr.w));
			mDynamicMat.SetVector(ClipArgs[index], new Vector4(sharpness.x, sharpness.y, Mathf.Sin(angle), Mathf.Cos(angle)));
		}
	}

在这里,有一堆看不懂的公式,我们先不要管,我们先走进shader里面看看,这些参数是怎么用的,因为单独看这些公式是没有用的。我们就只分析单层的soft clip,所以去Unlit - Transparent Colored 1.shader这个shader里面看看,我们先分析顶点着色器
			v2f vert (appdata_t v)
			{
				o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
				o.color = v.color;
				o.texcoord = v.texcoord;
				//在顶点着色器里面计算了一个所谓的world pos,这个world pos是一个什么东西呢?结合之前在SetCliping函数中的参数,可以得知这个完整的公式如下
				// o.worldPos = float2(v.vertex.x / cr.z - cr.x / cr.z, v.vertex.y / cr.w - cr.y / cr.w);
				//最终实际的得到的是一个标准化值,-1~1范围内的值表示为在裁剪区域范围内,超过这个值的像素点就被裁剪掉了
				o.worldPos = v.vertex.xy * _ClipRange0.zw + _ClipRange0.xy;
				return o;
			}

我们通过一个图来分析这个公式的意义:

图片随便画的 比较low,最终实际的得到的是一个标准化值,-1~1范围内的值表示为在裁剪区域范围内,超过这个值的像素点就被裁剪掉了

接下来,我们到像素着色器里面看看究竟是怎么裁剪的:
			half4 frag (v2f IN) : COLOR
			{
				//这个公式可以看成两个部分 第一个部分是(float2(1.0, 1.0) - abs(IN.worldPos))
				//意义是 把 worldPos转换成另外一种表现形式,就是大于0的就是在像素范围内,小于0的就是在像素范围外
				//同时 * _ClipArgs0实际上是在做渐变边缘计算,这里的_ClipArgs0实际上是cr.z / soft.x和 cr.z / soft.x;
				//所以这个 * _ClipArgs0的意义把factor最终设置成显示范围以内并且在不在渐变边缘的 设置成大于1的,在渐变边缘的,设置成0-1的 裁剪的 设置成小于0的
				// Softness factor
				float2 factor = (float2(1.0, 1.0) - abs(IN.worldPos)) * _ClipArgs0;
				// Sample the texture
				half4 col = tex2D(_MainTex, IN.texcoord) * IN.color;
				//这里的alpha就非常简单了,先取得xy中最小的那个,然后大于1的设置为1 小于0的设置为0,完事
				col.a *= clamp( min(factor.x, factor.y), 0.0, 1.0);
				return col;
			}

我们通过一张图片看看factor的计算结果

总结一下,NGUI在通过这样比较隐晦的方式去实现这个shader,避免了在shader中出现if和除法,使得这种裁剪方式非常高效而且精巧,值得学习。
来自:https://blog.csdn.net/nxshow/article/details/72864419

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引

标签: