前一阵使用NGUI开发时遇到一个实现圆形小地图的需求。小地图上除了地图背景外还有一大堆的零零碎碎的角色提示信息啥的,因此创建了一个panel进行绘制,剩下的就是如何让这个panel只在一个圆形的区域内进行显示。NGUI本身对于panel提供了alpha clip以及soft clip两种剪裁模式,因此自然而然就是想到给他再添加一个mask clip模式,为该面板提供一张遮罩贴图来对其进行更为灵活的自定义切割。
NGUI的渲染系统及其panel实现clip的方式
NGUI使用3D渲染方式来进行2D/3D UI的绘制,每一个UI元素就是一个简单的面片,通过一套坐标系统,将你在2D坐标系中设置的值转化为3D世界坐标系中的一系列顶点,设置好正确的uv及material通过摄像机渲染到屏幕上。其中UIPanel是NGUI的一个绘制管理单元,负责管理其所有子UI元素的顶点生成、三角形索引列表、更新、深度排序等等。可以将它理解为MeshFilter,构建并维护着一个Mesh,这个Mesh包含了很多submesh,每个submesh使用不同材质渲染,并包含了一个或多个面片。UIPanel还管理一个UIDrawcall列表,UIDrawcall则是NGUI的基本绘制单元如同MeshRender,只不过一个UIDrawcall只对一个submesh进行绘制。
3D渲染除了顶点索引以及uv buffer的设置外还有一个重要的元素就是material,包括了贴图等一系列参数的设置。在NGUI中,渲染使用的material由UIDrawcall根据其绘制的元素动态生成。下面是UIDrawcalll中的一段代码,可以看到其创建material的方式:
void CreateMaterial () { const string alpha = " (AlphaClip)"; const string soft = " (SoftClip)"; const string mask = " (MaskClip)"; 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"); shaderName = shaderName.Replace(alpha, ""); shaderName = shaderName.Replace(soft, ""); shaderName = shaderName.Replace(mask, ""); // Try to find the new shader Shader shader; if (mClipping == Clipping.SoftClip) { shader = Shader.Find(shaderName + soft); } else if (mClipping == Clipping.AlphaClip) { shader = Shader.Find(shaderName + alpha); } ....... }也就是说UIDrawcall会根据当前其绘制元素所使用的材质或shader以及当前的clipping类型使用由对应shader所自动创建的材质。同时UIDrawcall中缓存了材质的其他参数,用以给新材质赋值,更为具体的操作逻辑大家就自己看下源码吧。下面简述一下NGUI实现clipping的过程:首先改变clipping的状态会触发UIPanel的重绘,UIPanel的重绘过程中会更新每个UIDrawcall的绘制内容及设置包括clipping(UIPanel.UpdateUIDrawcall),当UIDrawcall的clipping发生改变时会触发其材质的更新(UIDrawcall.UpdateMaterials),UIDrawcall使用更新好的材质对UI元素进行绘制,而最终剪裁clipping的操作则完全由shader来完成。
这样,实现图片遮罩Panel只需要3步,1.为每个NGUI基础shader编写一个实现texture mask遮罩功能的扩展shader;2.将该shader的设置集成的NGUI工具中;3.将该材质的遮罩图片的设置集成到NGUI工具中。
编写图片遮罩shader
shader的编写其实非常简单,只要参考一下内置的shader就好,NGUI的内置shader如下:
第一次看到这些shader的时候我还记得我是眼花缭乱,现在看过UIDrawcall的代码明白了其实NGUI渲染使用的shader也就是Dynamic Font, Text, Premultiplied Colored, Transparent Colored这几种,其他都是在其基础上加入剪裁功能的。我们需要基于这四种基础shader编写添加遮罩剪裁功能的新shader,当然如果你(wo)懒的话,只写Transparent Colored这个就可以(因为我们的小地图里只有texture和sprite没有文字)。另外如果以后你想在UI中使用一些特效shader,别忘了为这些shader添加clip版本,保证那些UI元素可以正确的被剪裁。
咋写呢,首先在xxxx/Resources/Shaders/目录下创建一个shader,名字随便起,为了与NGUI统一我起的是Unlit - Transparent Colored (MaskClip)。当然文件的名字统不统一无所谓,重要的是shader代码里给这个shader起的真正的名字要合乎规范。直接把SoftClip的shader拷贝到新的shader中,把后缀改成一个自定义的名字就成了。下面就是为这个shader添加一个遮罩的纹理属性并在fragment shader中基于这个纹理对最终像素颜色或alpha值进行过滤就好了。这里比较重要的就是遮罩纹理的uv坐标,不过感觉要说明白比较麻烦,就先放着以后有(bu)空(hui)再写吧。
Shader "Unlit/Transparent Colored (MaskClip)" { Properties { _MainTex ("Base (RGB), Alpha (A)", 2D) = "black" {} _PanelMaskTex ("Mask (A)", 2D) = "white" {} } SubShader { LOD 200 Tags { "Queue" = "Transparent" "IgnoreProjector" = "True" "RenderType" = "Transparent" } Pass { Cull Off Lighting Off ZWrite Off Offset -1, -1 Fog { Mode Off } ColorMask RGB Blend SrcAlpha OneMinusSrcAlpha CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" sampler2D _MainTex; sampler2D _PanelMaskTex; float4 _MainTex_ST; struct appdata_t { float4 vertex : POSITION; half4 color : COLOR; float2 texcoord : TEXCOORD0; }; struct v2f { float4 vertex : POSITION; half4 color : COLOR; float2 texcoord : TEXCOORD0; float2 worldPos : TEXCOORD1; }; v2f vert (appdata_t v) { v2f o; o.vertex = mul(UNITY_MATRIX_MVP, v.vertex); o.color = v.color; o.texcoord = v.texcoord; o.worldPos = TRANSFORM_TEX(v.vertex.xy, _MainTex); return o; } half4 frag (v2f IN) : COLOR { // calculate uv of mask texture in panel coordination float2 panelUV = (1 - IN.worldPos) * 0.5f; // Sample the texture half4 col = tex2D(_MainTex, IN.texcoord) * IN.color; col.a *= tex2D(_PanelMaskTex, panelUV).a; return col; } ENDCG } } }
比较关键的一步是添加UIDrawCall.Clipping枚举项,并修改上文给出的UIDrawcall.CreateMaterial()的代码,将新的shader后缀配合新的clipping枚举加进去,另外为UIDrawcall及UIPanel添加一个Texture2D成员变量用以存储遮罩纹理的设置,添加的方式及调用位置可以完全参照UIPanel.mClipSoftness以及UIDrawcall.mTexture。另外还要修改相应的UIPanelInspector,使你在Panel的Inspector中可以设置遮罩纹理。这些修改都比较零碎,就只展示下Inspector的修改内容吧。