unity3d ugui编辑器中有遮罩,手机上没有
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了unity3d ugui编辑器中有遮罩,手机上没有相关的知识,希望对你有一定的参考价值。
在你所需要的地方建立一个文件夹,并新建一个材质material,你可以用中文为其命名,比如“”文字材质“”。2你需要更改材质的shader,此种shader将被用于UGUI中,不限于Text,Panel也可应用,有一定代替Sprite的效果。言归正传,更改shader,如图中所示的那样。这种设置只包含了一种贴图,请将Detail Strength的值调整成1,这样,将完全显示你所赋予的纹理。3添加一张图片,当你导入一张图片到unity中,默认是Texture形式的,Texture是指的该图片的shader。所以你什么都不需要做,直接添加图片到之前我们所建立的文字材质中去。4新建一个Canvas并添加一个子UI,也就是一个Text。5我们更改一些Text中的属性,以便我们只做与观察。并在Text的Material中将我们的文字材质添加过去。这是很重要的一个步骤,添加一个名叫 position as UV1 。在官方的解释中这个组件是用于Text的显示的,也就是说,你只能这样用,由于他没有任何参数,也没有过多的说明,所以不能解释。如果你不添加这个组件,将不能够正确显示,如果你添加了组件,之后有删除了,你的显示效果可能依然存在,这点我无法解释,总之,只要添加就好。你会看到一下文字。当你的图片过小的时候,请更改你的材质一栏的属性,Detail下的Tiling和offset找寻合适的值。对于一个文字材质,你无法真正找到正确的拉伸,但是你可以对此处进行手动微调,特别是你的图片在看起来不是那种连续的图片的时候。我们试着写一个繁体字,让他看起来很方,方便我们调节材质的相关系数。我们尝试谢谢其他字,并对text面板进行手动的拉动,看看有何种效果。我们会发现材质随着我们的拖动发生了变化,所以请在使用时候注意到这一点。但是请注意这一点,某种意义上并不会影响你的使用,因为你可以勾选Text组件下的红框体中的选项,使文字不受到框体的约束做出如换行等动作。如上面的“”啊喽哈“”几个字,我们可以为Text添加Outline组件,使得发生描边效果。我们再回到“”啊喽哈“”几个字,我们可以为其添加除了秒黑边外的阴影属性,我们可以将他的的Color变得白一些,你会看到变化。这时候你删除或者取消掉之前的outline属性,只保留shadow,你将会看到3D立体感的“”啊喽哈“”。之后在Text再次添加Shadow组件,也就是说Text这时候有两个shadow,我们添加不同的组件,可以达到多种效果,也就是我们如果愿意的话在之前我们创建的名为“”文字材质“”的材质球中,如果我们移除材质的贴图,我们适当更改其他部分的颜色,我们将看到红白配的字,是不是很丑,但这点很实际,只不过他们不般配而已。我们搭配合理一些可以出现还能凑合一看的效果,只是字体稍显难看。当我们下载合适的字体,譬如一些魔法文字,并赋予一些材质之后,我们之前的一些设置,就有了用武之地。但是有些时候我们期望得到一个窄窄高高的魔法字的时候,我们只需调节Text组件的RectTransform中关于缩放的数值。我们再次使用其他字体做做实验,比如“”开始游戏“”,一款感觉有战地情节的有些是什么样子呢。当然了,我们也可以更酷一点吧,字体是大头,兴许你的一次改动整个风格就全变了。 参考技术A 你解决了吗?UGUI源码分析Unity遮罩之Mask详细解读
博客园博文链接 https://www.cnblogs.com/iwiniwin/p/15131528.html
遮罩,顾名思义是一种可以掩盖其它元素的控件。常用于修改其它元素的外观,或限制元素的形状。比如ScrollView或者圆头像效果都有用到遮罩功能。本系列文章希望通过阅读UGUI源码的方式,来探究遮罩的实现原理,以及通过Unity不同遮罩之间实现方式的对比,找到每一种遮罩的最佳使用场合。
Unity UGUI主要提供两种遮罩,分别是Mask和Rect Mask 2D。在2D游戏开发中,可能还会用到Sprite Mask,虽然不是本文的重点,但后面也会提到。原本是希望将对各个遮罩的分析与对比整合在一篇文章中,但在书写过程中发现篇幅过长,因此只好拆分为三个部分。本篇文章是第一部分,专门解读Mask遮罩。另外两篇分别是
本文使用的源码与内置资源均基于Unity2019.4版本
Mask
查阅Unity的官方文档,对Mask有如下定义
遮罩不是可见的 UI 控件,而是一种修改控件子元素外观的方法。遮罩将子元素限制(即“掩盖”)为父元素的形状。因此,如果子项比父项大,则子项仅包含在父项以内的部分才可见。
也有简单提到Mask的实现原理
使用 GPU 的模板缓冲区来实现遮罩。第一个遮罩元素将 1 写入模板缓冲区。遮罩下面的所有元素在渲染时进行检查,仅渲染到模板缓冲区中有 1 的区域。 嵌套的遮罩会将增量位掩码写入缓冲区,这意味着可渲染的子项需要具有要渲染的逻辑和模板值。
是不是有些晦涩难懂?没关系,接下来的分析就是对这个实现原理的展开,每句话都会有对应的解读
模板缓冲?
要搞懂模板缓冲,先要了解模板测试。在渲染流水线的逐片元操作阶段,会有一个模板测试,可以作为一种丢弃片元的辅助方法(这里的片元可以简单理解为对应着一个像素),而要进行模板测试就要用到模板缓冲。每个像素/片段都可以有一个与之对应的模板值,就存储在模板缓冲中。
如果开启了模板测试,GPU会首先读取(使用读取掩码)模板缓冲区中该片元位置的模板值,然后将该值和读取到(使用读取掩码)的参考值进行比较,这个比较函数可以是由开发者指定的,例如小于时舍弃该片元,或者大于等于时舍弃。如果这个片元没有通过这个测试,该片元就会被舍弃。不管一个片元有没有通过模板测试,我们都可以根据模板测试和下面的深度测试结果来修改模板缓冲区,这个修改操作也是由开发者指定的。开发者可以设置不同结果下的修改操作,例如,在失败时模板缓冲区保持不变,通过时将模板缓冲区中对应位置的值加1等。
而Mask就是通过在渲染时,将其对应位置像素的模板值都置为特定值(不一定是1),然后当遮罩下的子元素渲染时,逐像素判断模板值是否为特定值,如果是特定值,就表示在遮罩范围内,可以显示。如果不是,则表示不在遮罩范围内,不显示。借用一张网上的图,很形象的描述了这种方式。
绿色矩形是遮罩区域,模板值都被写入为1,当渲染横着的红色矩形时,只有模板值为1的区域才会显示,非1的会被丢弃不会显示。从而实现了裁剪效果
源码
在了解了Mask的基本实现原理后,再来通过源码看看具体的实现方式
UGUI中所有可显示的图形都有一个基类,Graphic。比如Image和Text就是间接继承于Graphic的。Graphic定义了一个materialForRendering属性。它表示传递给CanvasRenderer,实际被用于渲染的材质。从这个属性的get访问器可以发现,在获取最终被用于渲染的材质时,会先依次调用这个GameObject上所有实现了IMaterialModifier接口组件的GetModifiedMaterial方法来修改最后返回的材质。
public virtual Material materialForRendering
get
var components = ListPool<Component>.Get();
GetComponents(typeof(IMaterialModifier), components);
var currentMat = material;
for (var i = 0; i < components.Count; i++)
currentMat = (components[i] as IMaterialModifier).GetModifiedMaterial(currentMat);
ListPool<Component>.Release(components);
return currentMat;
IMaterialModifier定义如下所示,也就是说其它组件可以通过实现IMaterialModifier接口来达到修改最终渲染所使用的材质的目的
public interface IMaterialModifier
/// <summary>
/// Perform material modification in this function.
/// </summary>
/// <param name="baseMaterial">The material that is to be modified</param>
/// <returns>The modified material.</returns>
Material GetModifiedMaterial(Material baseMaterial);
Mask组件就实现了IMaterialModifier接口,并通过这个接口返回了一个新材质,并通过这个新材质设置修改模板缓冲值
/// Stencil calculation time!
public virtual Material GetModifiedMaterial(Material baseMaterial)
// ...
var rootSortCanvas = MaskUtilities.FindRootSortOverrideCanvas(transform);
var stencilDepth = MaskUtilities.GetStencilDepth(transform, rootSortCanvas);
if (stencilDepth >= 8)
Debug.LogWarning("Attempting to use a stencil mask with depth > 8", gameObject);
return baseMaterial;
int desiredStencilBit = 1 << stencilDepth;
// 第一部分
// if we are at the first level...
// we want to destroy what is there
if (desiredStencilBit == 1)
// CompareFunction.Always,始终通过,执行StencilOp.Replace操作,将模板缓冲中的值替换为(1 & 255)= 1
var maskMaterial = StencilMaterial.Add(baseMaterial, 1, StencilOp.Replace, CompareFunction.Always, m_ShowMaskGraphic ? ColorWriteMask.All : 0);
StencilMaterial.Remove(m_MaskMaterial);
m_MaskMaterial = maskMaterial;
var unmaskMaterial = StencilMaterial.Add(baseMaterial, 1, StencilOp.Zero, CompareFunction.Always, 0);
StencilMaterial.Remove(m_UnmaskMaterial);
m_UnmaskMaterial = unmaskMaterial;
// 设置渲染器可使用的材质数量为1
graphic.canvasRenderer.popMaterialCount = 1;
// 设置渲染器使用的材质
graphic.canvasRenderer.SetPopMaterial(m_UnmaskMaterial, 0);
return m_MaskMaterial;
// 第二部分
// ...
GetModifiedMaterial的实现可以分两部分来看,上面的代码只列出了第一部分。简单起见,我们先只看第一部分,主要是if (desiredStencilBit == 1)语句块内代码,它是用于处理只有自身有Mask的简单情况的
- 代码中的stencilDepth表示自身到Canvas之间Mask的个数,如果每层有多个Mask则只计一个。如果除了自身的Mask,再往上没有Mask了,则stencilDepth为0,如果再往上找到1个,stencilDepth为1,找到2个,stencilDepth为2,以此类推。
- desiredStencilBit表示实际要写入模板缓冲的参考值。desiredStencilBit = 1 << stencilDepth。当stencilDepth >= 8时会打印警告,是因为模板值一般是8位的,desiredStencilBit将超出这个范围无法写入
- 如果只是自身有Mask,再往上没有了。那stencilDepth就是0,desiredStencilBit就是1,此时通过StencilMaterial.Add获得一个新材质,并将这个材质返回,从而达到修改最终渲染使用材质的目的。StencilMaterial.Add方法具体实现如下所示,主要是对材质设置一些传入的参数。
public static Material Add(Material baseMat, int stencilID, StencilOp operation, CompareFunction compareFunction, ColorWriteMask colorWriteMask, int readMask, int writeMask)
// ...
var newEnt = new MatEntry();
newEnt.count = 1;
newEnt.baseMat = baseMat;
newEnt.customMat = new Material(baseMat);
newEnt.customMat.hideFlags = HideFlags.HideAndDontSave;
newEnt.stencilId = stencilID;
newEnt.operation = operation;
newEnt.compareFunction = compareFunction;
newEnt.readMask = readMask;
newEnt.writeMask = writeMask;
newEnt.colorMask = colorWriteMask;
newEnt.useAlphaClip = operation != StencilOp.Keep && writeMask > 0;
newEnt.customMat.name = string.Format("Stencil Id:0, Op:1, Comp:2, WriteMask:3, ReadMask:4, ColorMask:5 AlphaClip:6 (7)", stencilID, operation, compareFunction, writeMask, readMask, colorWriteMask, newEnt.useAlphaClip, baseMat.name);
newEnt.customMat.SetInt("_Stencil", stencilID);
newEnt.customMat.SetInt("_StencilOp", (int)operation);
newEnt.customMat.SetInt("_StencilComp", (int)compareFunction);
newEnt.customMat.SetInt("_StencilReadMask", readMask);
newEnt.customMat.SetInt("_StencilWriteMask", writeMask);
newEnt.customMat.SetInt("_ColorMask", (int)colorWriteMask);
newEnt.customMat.SetInt("_UseUIAlphaClip", newEnt.useAlphaClip ? 1 : 0);
if (newEnt.useAlphaClip)
newEnt.customMat.EnableKeyword("UNITY_UI_ALPHACLIP");
else
newEnt.customMat.DisableKeyword("UNITY_UI_ALPHACLIP");
m_List.Add(newEnt);
return newEnt.customMat;
StencilMaterial本质上只是缓存材质的一个工具类,主要作用就是提供一个新的材质。再结合下面这句代码传入的参数。这个新材质起到的作用是始终通过模板测试(CompareFunction.Always),替换模板缓冲中的模板值(StencilOp.Replace)为1
var maskMaterial = StencilMaterial.Add(baseMaterial, 1, StencilOp.Replace, CompareFunction.Always, m_ShowMaskGraphic ? ColorWriteMask.All : 0);
对材质设置的参数,实际上是设置给Shader的,查看UI默认使用的Shader是UI/Default,这是Unity的内置Shader,源码可以在Unity官网下载,下载时选择"Built in shaders"
UI-Default.shader的部分源码如下所示,可以看到主要是利用Unity ShaderLab的模板语句来实现对模板缓冲区的一些操作,详细介绍可以点击这里查看,就不再赘述了
// Unity built-in shader source. Copyright (c) 2016 Unity Technologies. MIT license (see license.txt)
Shader "UI/Default"
Properties
[PerRendererData] _MainTex ("Sprite Texture", 2D) = "white"
_Color ("Tint", Color) = (1,1,1,1)
_StencilComp ("Stencil Comparison", Float) = 8
_Stencil ("Stencil ID", Float) = 0
_StencilOp ("Stencil Operation", Float) = 0
_StencilWriteMask ("Stencil Write Mask", Float) = 255
_StencilReadMask ("Stencil Read Mask", Float) = 255
_ColorMask ("Color Mask", Float) = 15
[Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip ("Use Alpha Clip", Float) = 0
SubShader
// ...
Stencil
Ref [_Stencil]
Comp [_StencilComp]
Pass [_StencilOp]
ReadMask [_StencilReadMask]
WriteMask [_StencilWriteMask]
// ...
到这里不难发现,Unity文档Mask原理描述中的第一句话就是对上面过程的一个概括
使用 GPU 的模板缓冲区来实现遮罩。第一个遮罩元素将 1 写入模板缓冲区。
接下来我们再来看被遮掩的对象,是怎样利用模板缓冲实现遮罩效果的
UGUI中所有可被遮掩的图形都有一个基类,MaskableGraphic,同样MaskableGraphic是继承于Graphic的。比如Image和Text就是继承于MaskableGraphic的。同理,MaskableGraphic也实现了IMaterialModifier接口来修改最终渲染使用的材质
public virtual Material GetModifiedMaterial(Material baseMaterial)
var toUse = baseMaterial;
if (m_ShouldRecalculateStencil)
var rootCanvas = MaskUtilities.FindRootSortOverrideCanvas(transform);
m_StencilValue = maskable ? MaskUtilities.GetStencilDepth(transform, rootCanvas) : 0;
m_ShouldRecalculateStencil = false;
// if we have a enabled Mask component then it will
// generate the mask material. This is an optimization
// it adds some coupling between components though :(
if (m_StencilValue > 0 && !isMaskingGraphic)
var maskMat = StencilMaterial.Add(toUse, (1 << m_StencilValue) - 1, StencilOp.Keep, CompareFunction.Equal, ColorWriteMask.All, (1 << m_StencilValue) - 1, 0);
StencilMaterial.Remove(m_MaskMaterial);
m_MaskMaterial = maskMat;
toUse = m_MaskMaterial;
return toUse;
- 代码中的m_StencilValue表示在自身层级之上有多少个Mask,如果只有父节点有Mask组件,则m_StencilValue值为1
- 可以看到它返回的新材质主要作用是,比较传入的参考值((1 << m_StencilValue) - 1)与模板缓冲中的值,如果相等就通过(CompareFunction.Equal),即使通过了模板测试也仍保留模板缓冲中的值(StencilOp.Keep)。
var maskMat = StencilMaterial.Add(toUse, (1 << m_StencilValue) - 1, StencilOp.Keep, CompareFunction.Equal, ColorWriteMask.All, (1 << m_StencilValue) - 1, 0);
- 当只有父节点有Mask组件时,(1 << m_StencilValue) - 1值即为1,与前面Mask组件提前设置的模板缓冲区的值相同,所以在Mask范围内的元素将能够通过模板测试,最终显示出来,未通过的将被裁剪无法显示出来
这里就对应了Unity文档Mask原理描述中的中间部分
遮罩下面的所有元素在渲染时进行检查,仅渲染到模板缓冲区中有 1 的区域。
实际上到这里,一个简单的,只有父节点有Mask的图形是怎样实现遮罩效果的,我们已经彻底搞清楚了,接下来,让我们来看看复杂点的情况
如果大家还没忘记的话,让我们回到Mask的GetModifiedMaterial实现(注意是Mask的哦~),查看它的第二部分,即if语句块后面的代码,他们是被用来处理嵌套Mask的
public virtual Material GetModifiedMaterial(Material baseMaterial)
if (!MaskEnabled())
return baseMaterial;
var rootSortCanvas = MaskUtilities.FindRootSortOverrideCanvas(transform);
var stencilDepth = MaskUtilities.GetStencilDepth(transform, rootSortCanvas);
if (stencilDepth >= 8)
Debug.LogWarning("Attempting to use a stencil mask with depth > 8", gameObject);
return baseMaterial;
int desiredStencilBit = 1 << stencilDepth;
// 第一部分
// ...
// 第二部分
//otherwise we need to be a bit smarter and set some read / write masks
var maskMaterial2 = StencilMaterial.Add(baseMaterial, desiredStencilBit | (desiredStencilBit - 1), StencilOp.Replace, CompareFunction.Equal, m_ShowMaskGraphic ? ColorWriteMask.All : 0, desiredStencilBit - 1, desiredStencilBit | (desiredStencilBit - 1));
StencilMaterial.Remove(m_MaskMaterial);
m_MaskMaterial = maskMaterial2;
graphic.canvasRenderer.hasPopInstruction = true;
var unmaskMaterial2 = StencilMaterial.Add(baseMaterial, desiredStencilBit - 1, StencilOp.Replace, CompareFunction.Equal, 0, desiredStencilBit - 1, desiredStencilBit | (desiredStencilBit - 1));
StencilMaterial.Remove(m_UnmaskMaterial);
m_UnmaskMaterial = unmaskMaterial2;
graphic.canvasRenderer.popMaterialCount = 1;
graphic.canvasRenderer.SetPopMaterial(m_UnmaskMaterial, 0);
return m_MaskMaterial;
- 与第一部分不同的是,StencilMaterial.Add传入的参数不同,而这些不同就是处理嵌套Mask的关键。嵌套Mask是指除了自身Mask,层级再往上还有Mask。针对这种情况,传入的参考值是desiredStencilBit | (desiredStencilBit - 1),而不再固定是1了。这个值的实际含义是利用每一位是否是1来表示每一层是否有Mask。举个栗子,如果除了自身,再往上还能找到两个Mask,则stencilDepth为2,desiredStencilBit为8,二进制形式为100,经过计算传入的参考值是111,用每个1来分别表示,自身有Mask,第一层有,第二层有。这个参考值被Unity称之为增量位掩码
- 这个增量位掩码正好可以与MaskableGraphic部分判断模板值是否相等时用到的(1 << m_StencilValue) - 1对应上
// Mask处理嵌套遮罩所用的新材质
var maskMaterial2 = StencilMaterial.Add(baseMaterial, desiredStencilBit | (desiredStencilBit - 1), StencilOp.Replace, CompareFunction.Equal, m_ShowMaskGraphic ? ColorWriteMask.All : 0, desiredStencilBit - 1, desiredStencilBit | (desiredStencilBit - 1));
// MaskableGraphic判断是否在遮罩内所用的新材质
var maskMat = StencilMaterial.Add(toUse, (1 << m_StencilValue) - 1, StencilOp.Keep, CompareFunction.Equal, ColorWriteMask.All, (1 << m_StencilValue) - 1, 0);
实际上这部分就对应了Unity文档Mask原理描述中的后两句话
嵌套的遮罩会将增量位掩码写入缓冲区,这意味着可渲染的子项需要具有要渲染的逻辑和模板值。
补充
最后还有几处地方觉得值得提一下
-
StencilMaterial.Add传入参数的最后两个分别是readMask读取掩码和writeMask写入掩码,读取掩码不仅是在读取模板缓冲中的值时会与其相与,对于要比较的参考值也会相与
-
细心的同学可能会发现,Mask在获取新材质的时候,会多获取一个。这个材质实际是用来清除模板缓冲区的。以避免不要影响后续的渲染
// 第一部分 var unmaskMaterial = StencilMaterial.Add(baseMaterial, 1, StencilOp.Zero, CompareFunction.Always, 0); // 第二部分 var unmaskMaterial2 = StencilMaterial.Add(baseMaterial, desiredStencilBit - 1, StencilOp.Replace, CompareFunction.Equal, 0, desiredStencilBit - 1, desiredStencilBit | (desiredStencilBit - 1));
利用Unity的帧调试器也可以看到这个清除过程
-
为什么Mask可以实现圆形遮罩效果?
众所周知,圆头像效果可以使用Mask实现,具体方式是使用一张只显示圆形,非圆形区域是透明像素的切图实现的。但这张切图实际上还是矩形的,根据上面的原理解读,矩形区域对应的模板值都会被Mask设置为特定值,从而使其下的子元素都能通过模板测试,是无法实现圆形裁剪的
关键代码还是在UI-Default.shader中,它通过clip指令,将透明度低于0.001的片元都裁剪掉了,因此被裁剪的片元也就不会再设置对应的模板值了。UNITY_UI_ALPHACLIP宏定义是通过Shader参数_UseUIAlphaClip控制的,Mask获取的新材质会将该参数设置为true
#ifdef UNITY_UI_ALPHACLIP clip (color.a - 0.001); #endif
-
关于SpriteMask
Sprite Mask不属于UGUI的范围,Unity官方并没有将它开源,不过通过官方论坛我们可以了解到其实现原理也是利用了模版缓冲。
不像Mask,只实现了Visible Inside Mask功能,SpriteMask不仅实现了Visible Inside Mask功能,也实现了Visible Outside Mask功能。在经过对Mask的原理分析以后,我们知道通过修改模板缓冲的比较函数是可以轻易的实现这种效果的,感兴趣的同学赶快动手试一下吧
参考
以上是关于unity3d ugui编辑器中有遮罩,手机上没有的主要内容,如果未能解决你的问题,请参考以下文章