UIDrawCall
文章说明
在阅读这篇文章的时候,希望你是已经看了 UIDrawCall(一)的,所以第二篇主要来讲一下关于渲染调用阶段的函数,其实这一章和 UIPanel 裁剪有关,最好是先阅读一下 UIPanel 源码再来看会比较清晰。
如果文章中有写的不对的地方,欢迎大家指出,谢谢。
源码分析
UIPanel 裁剪介绍
在进行源码讲解之前,先来点小菜,了解一下 “UIPanel” 组件的裁剪部分。
那么究竟什么是裁剪,就是我们只需要显示我们想要看到的那部分内容,对于不想看到的那些多余的内容,我们就需要将他们裁剪掉,对于 UIDrawcall 来讲,接下来讲到的裁剪都是和 UIPanel 的这个 Clipping 属性相关,关于 UIPanel 更多详细的内容我们在 UIPanel 专用篇中详细介绍,这里不过多阐述。
如图,重点关注一下 UIPanel 的 Clipping 的属性有四个可以选择的选项。除了第一个 None,剩下三个都是与裁剪相关。
我们看到三个裁剪选择:
- Texture Mask 贴图裁剪
- Soft Clip 软裁剪
- Constrain But Dont Clip (这个裁剪我还是不太了解,基本也没用过,这里不多介绍)
渲染调用阶段
1. 主要函数
OnWillRenderObject()
:这个函数是用于做一些渲染之前的一些设置的,一般用于设置材质的参数 还有 裁剪信息。
SetClipping()
: 这是设置裁剪信息的核心代码。
裁剪部分主要分为三部分:
- 贴图裁剪,主要就是 Texture Mask 贴图裁剪
- 其他裁剪,主要就是 Soft Clip 软裁剪
- 遗留功能,这个一般不会走进去,这里不讨论
首先重点介绍一下 第二部分 “其他裁剪”,主要就是 soft Clip 。看懂了第二种裁剪,贴图裁剪也就明白了,先看源码:
2. NGUI 源码:
//这个函数是用于做一些渲染之前的一些设置的,一般用于设置材质的参数 还有 裁剪信息
void OnWillRenderObject ()
{
//渲染前再次检查材质是否需要重建
UpdateMaterials();
if (mBlock != null) mRenderer.SetPropertyBlock(mBlock);
if (onRender != null) onRender(mDynamicMat ?? mMaterial);
if (mDynamicMat == null || mClipCount == 0) return;
//如果有发生裁剪,才会走下来,接下来的部分全部是针对 panel 裁剪的设置部分
if (mTextureClip)
{
//这里设置的是 贴图的 Mask 裁剪,原理是:使用一张 Mask(遮罩) 图片,shader 中在根据裁剪区域和像素 alpha 设置裁剪
Vector4 cr = panel.drawCallClipRange;
//panel 的 clipSoftness,看代码并没有使用到这些
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;
//在材质中设置 _ClipRange0 的数值为 (-cr.x / cr.z, -cr.y / cr.w, 1f / cr.z, 1f / cr.w)
mDynamicMat.SetVector(ClipRange[0], new Vector4(-cr.x / cr.z, -cr.y / cr.w, 1f / cr.z, 1f / cr.w));
//在材质中设置 裁剪贴图信息 _ClipTex
mDynamicMat.SetTexture("_ClipTex", clipTexture);
//最终 _ClipRange0 和 _ClipTex 都会被传递到 shader 中对应的变量
}
else if (!mLegacyShader)
{
//如果有裁剪设置,并且不是 mask 裁剪,一般都会走到这里,用于给 Soft Clip (软裁剪)设置裁剪范围的
UIPanel currentPanel = panel;
//从自己开始遍历所有的父Panel,凡是支持裁剪的,都要设置裁剪范围,直到 currentPanel 为 null
//之前的 只有三种 “Transparent Colored”shader 就与这个 panel 裁剪嵌套相关最多支持3层叠加裁剪
for (int i = 0; currentPanel != null; )
{
if (currentPanel.hasClipping)
{
//这个里面计算裁剪范围和父Panel 与 当前DrawCall所属的Panel的角度
float angle = 0f;
Vector4 cr = currentPanel.drawCallClipRange;
// Clipping regions past the first one need additional math
//从叠加的第2个裁剪区域开始,需要增加一些额外的运算,只会显示叠加裁剪重叠的部分
if (currentPanel != panel)
{
//对于父子节点中多个 使用 Soft Clip 的 UIPanel,他们的显示区域是他们叠加中重叠部分的范围,所以要计算这个角度
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);
// unity 是左手坐标系,所以我们正常看到的平面都是垂直于z轴的
// 所以这里防止界面绕着 x轴 或者 y轴旋转 90度,使界面 完全横向水平于z轴,或者完全纵向水平于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轴的,所以只有绕着z轴旋转是有效角度
angle = diff.z;
}
// Pass the clipping parameters to the shader
///这里就是真正设置裁剪的地方了
// cr就是this.panel的 drawCallClipRange,而这个drawCallClipRange的各个参数意义是这样的:
// ( x:中心点X坐标 y:中心点Y坐标 z:panel width的一半, w:panel height的一半)
// currentPanel.clipSoftness 软裁剪设置的渐变边缘
// angle 是与父Panel的角度
// 如果是单层:angle 是0
// 如果是多层叠加:angle 会被使用
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);
}
}
SetClipping 和 Awake 相关代码如下:
static int[] ClipRange = null;
static int[] ClipArgs = null;
/// <summary>
/// Set the shader clipping parameters.
/// </summary>
//设置裁剪需要的相关信息
void SetClipping (int index, Vector4 cr, Vector2 soft, float angle)
{
angle *= -Mathf.Deg2Rad;
//如果 soft.x 和 soft.y 都小于等于0的话,就拿默认值
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 被写死了,命名分别是:_ClipRange0 _ClipRange1 _ClipRange2 (这些名字都是在 shader 中定义对应的变量名)
//ClipArgs 数组在 Awake 也是被写死了,命名分别是: _ClipArgs0 _ClipArgs1 _ClipArgs2 _ClipArgs3
//所以如果你们项目中对于这些不够用的话,扩展也要按照这个规则走,(如果不拓展的话,超过这个范围的多重叠加裁剪就不会设置信息,所以要拓展就要增加自定义 shader)
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)));
}
}
// Unity 5.4 bug work-around: http://www.tasharen.com/forum/index.php?topic=14839.0
static int dx9BugWorkaround = -1;
/// <summary>
/// Cache the property IDs.
/// </summary>
void Awake ()
{
if (dx9BugWorkaround == -1)
{
var pf = Application.platform;
#if !UNITY_5_5_OR_NEWER
dx9BugWorkaround = ((pf == RuntimePlatform.WindowsPlayer || pf == RuntimePlatform.XBOX360) &&
#else
dx9BugWorkaround = ((pf == RuntimePlatform.WindowsPlayer) &&
#endif
SystemInfo.graphicsShaderLevel < 40 && SystemInfo.graphicsDeviceVersion.Contains("Direct3D")) ? 1 : 0;
}
if (ClipRange == null)
{
ClipRange = new int[]
{
Shader.PropertyToID("_ClipRange0"), //这些名字都是在 shader 中定义的变量
Shader.PropertyToID("_ClipRange1"),
Shader.PropertyToID("_ClipRange2"),
Shader.PropertyToID("_ClipRange4"), //shader 中我没找到这个变量(诶不知道是不是遗留功能还没做,不过也好奇为啥这个不是3是4,哇,我不会搞到盗版源码了吧)
};
}
if (ClipArgs == null)
{
ClipArgs = new int[]
{
Shader.PropertyToID("_ClipArgs0"), //这些名字都是在 shader 中定义的变量
Shader.PropertyToID("_ClipArgs1"),
Shader.PropertyToID("_ClipArgs2"),
Shader.PropertyToID("_ClipArgs3"), //shader 中我没找到这个变量(诶不知道是不是遗留功能还没做)
};
}
}
3. shader 源码分析:
这里我们只分析单层 clip ,多层叠加裁剪感兴趣的可以自己去阅读,懂了原理没什么太大问题哈,我对 shadr 也是刚刚入门,如果解释不对的地方,欢迎大神指出。接下来这张图是整篇文章最重要的部分,核心讲解裁剪的原理:
“Hidden/Unlit/Transparent Colored 1 ” shader 部分源码如下:
sampler2D _MainTex;
float4 _ClipRange0 = float4(0.0, 0.0, 1.0, 1.0); //从 NGUI 中设置的信息传递进来
float2 _ClipArgs0 = float2(1000.0, 1000.0); //从 NGUI 中设置的信息传递进来
struct appdata_t
{
float4 vertex : POSITION;
half4 color : COLOR;
float2 texcoord : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct v2f
{
float4 vertex : SV_POSITION;
half4 color : COLOR;
float2 texcoord : TEXCOORD0;
float2 worldPos : TEXCOORD1;
UNITY_VERTEX_OUTPUT_STEREO
};
v2f o;
v2f vert (appdata_t v)
{
UNITY_SETUP_INSTANCE_ID(v);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
o.vertex = UnityObjectToClipPos(v.vertex);
o.color = v.color;
o.texcoord = v.texcoord;
o.worldPos = v.vertex.xy * _ClipRange0.zw + _ClipRange0.xy;
return o;
}
half4 frag (v2f IN) : SV_Target
{
// Softness factor
float2 factor = (float2(1.0, 1.0) - abs(IN.worldPos)) * _ClipArgs0;
// Sample the texture
half4 col = tex2D(_MainTex, IN.texcoord) * IN.color;
//核心裁剪代码
col.a *= clamp( min(factor.x, factor.y), 0.0, 1.0);
return col;
}
“Hidden/Unlit/Transparent Colored (TextureClip)” shader 部分源码如下:
sampler2D _MainTex;
sampler2D _ClipTex;
float4 _ClipRange0 = float4(0.0, 0.0, 1.0, 1.0);
struct appdata_t
{
float4 vertex : POSITION;
float2 texcoord : TEXCOORD0;
half4 color : COLOR;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct v2f
{
float4 vertex : SV_POSITION;
float2 texcoord : TEXCOORD0;
float2 clipUV : TEXCOORD1;
half4 color : COLOR;
UNITY_VERTEX_OUTPUT_STEREO
};
v2f vert (appdata_t v)
{
v2f o;
UNITY_SETUP_INSTANCE_ID(v);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
o.vertex = UnityObjectToClipPos(v.vertex);
o.color = v.color;
o.texcoord = v.texcoord;
//这个计算方式是为了将 [-1, 0]范围内的点转换成 [0,0.5],将(0, 1]范围的点转化成 (0.5,1],这样就将[-1,1]的点转化成[0,1]
//这个是计算 裁剪 贴图中每个顶点所在的像素的位置,根据我之前的 mesh 文章,已知,uv坐标是只有[0,1]的
//这样计算完成后,方便在片段着色器中去寻找该像素所对应的 裁剪 color 值
o.clipUV = (v.vertex.xy * _ClipRange0.zw + _ClipRange0.xy) * 0.5 + float2(0.5, 0.5);
return o;
}
half4 frag (v2f IN) : SV_Target
{
//tex2D(sampler2D tex, float2 s)函数,找到贴图上 对应的uv点,直接使用颜色信息来进行着色
//这是CG程序中用来在一张贴图中对一个点进行采样的方法,返回值是 float4 类型的颜色值。
//这里对 _MainTex 在输入点上进行了采样,将 贴图中 像素颜色 赋值给 采样点的颜色,包括透明度。
half4 col = tex2D(_MainTex, IN.texcoord) * IN.color;
//这是设置裁剪 的最终代码,如果这个注视调,就没有裁剪效果了
col.a *= tex2D(_ClipTex, IN.clipUV).a;
return col;
}
4. 多重裁剪的例子
这里我使用两个 soft Clip panel 进行裁剪,并且修改了 第二个 panel 的 rotation 的 z 轴坐标,使其有旋转的效果,如图所示,看了这个图后,再去看多重裁剪的代码,就会更加理解代码中的含义,修改 z 轴坐标,就可以明白为什么代码中要计算 angle 。
5. 结果展示
未裁剪
这里我们设置了一个 520 * 520 大小的贴图,如下是没有裁剪的效果:
Soft Clip 裁剪
单层 Soft Clip 裁剪效果:
Texture Mask 裁剪
贴图裁剪效果如下:
修改
修改 “Hidden/Unlit/Transparent Colored 1” shader 中,将 片段着色器 设置 alpha 的代码注释掉,观察裁剪是否还有效果:
half4 frag (v2f IN) : SV_Target
{
// Softness factor
float2 factor = (float2(1.0, 1.0) - abs(IN.worldPos)) * _ClipArgs0;
// Sample the texture
half4 col = tex2D(_MainTex, IN.texcoord) * IN.color;
//注释掉这行代码
/*col.a *= clamp( min(factor.x, factor.y), 0.0, 1.0);*/
return col;
}
结果如下,很明显,失去裁剪效果: