Unity: UI 图片透明度重叠问题

5
问题:当CanvasGroup修改透明度时,画布中的两个对象会相互混合。
例如图片中的示例:场景有一个黑色背景,画布上有绿色和红色的图像。如果两个对象的不透明度都小于100%,它们将混合在一起。
左侧:不透明度为100%;
中间:不透明度为50%,图像混合(实际结果);
右侧:不透明度为50%,图像不混合(期望结果)。

enter image description here

如何根据它们的顺序防止图像混合?我知道一种基于纹理的解决方案(第二个相机呈现到放置在画布中的纹理),但这似乎太过复杂了。还有其他方法吗?着色器、通过代码进行更改、其他方法?
Unity 2019.3.0f1
1个回答

6

Resolution: a new shader and its material, plus one additional material. Shader creation can be avoided in some situations!

Shaders in Unity has a tool called Stencil. This tool maintains pixel buffer that can be used to discard pixels.

CanvasImageMask shader: it is a clone of default UI image shader, except Stencil adjustments and tiny addition for comfort work. When mask rendering is happening, Stencil will set the pixel buffer's value to 1 in the place where it actually drew something. The second shader reacts to this change and discards new pixels in that location. Keep in mind that objects with such material (that hold CanvasImageMask shader) should come first in Canvas because during rendering they will modify pixel buffers, which can be used later.

All objects that should be masked have to use another material that is based on default UI image shader with Stencil adjustments. This material will discard any pixels with a buffer's value equal or higher than 1.

Note that this work can be avoided. The developer can create 2 materials (clones of default image material), and modify Stencil settings in the editor. But this solution has a problem: how to create such a mask, that shouldn't be drawn? The best solution would be alpha channels set to 1 (out of 255). But if Canvas is using CanvasGroup, pixels can be discarded earlier, and the mask won't work.

CanvasImageMask shader hides Stencil settings (convenience) and adds a flag, which will hide all pixels from the mask yet still do the job.

CanvasImageMask:

Shader "Custom/CanvasImageMask"
{
    Properties
    {
        [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
        _Color ("Tint", Color) = (1,1,1,1)
        [MaterialToggle(DO_NOT_DRAW)]
        _DoNotDraw ("Do not draw", Float) = 0
    }

    SubShader
    {
        Tags
        { 
            "Queue"="Transparent" 
            "IgnoreProjector"="True" 
            "RenderType"="Transparent" 
            "PreviewType"="Plane"
            "CanUseSpriteAtlas"="True"
        }

        Stencil
        {
            Ref 1
            Comp Greater
            Pass Replace
        }

        Cull Off
        Lighting Off
        ZWrite Off
        ZTest [unity_GUIZTestMode]
        Blend SrcAlpha OneMinusSrcAlpha
        ColorMask RGBA

        Pass
        {
        CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma shader_feature DO_NOT_DRAW
            #include "UnityCG.cginc"

            struct appdata_t
            {
                float4 vertex   : POSITION;
                float4 color    : COLOR;
                float2 texcoord : TEXCOORD0;
            };

            struct v2f
            {
                float4 vertex   : SV_POSITION;
                fixed4 color    : COLOR;
                half2 texcoord  : TEXCOORD0;
            };

            fixed4 _Color;

            v2f vert(appdata_t IN)
            {
                v2f OUT;
                OUT.vertex = UnityObjectToClipPos(IN.vertex);
                OUT.texcoord = IN.texcoord;
#ifdef UNITY_HALF_TEXEL_OFFSET
                OUT.vertex.xy += (_ScreenParams.zw-1.0)*float2(-1,1);
#endif
                OUT.color = IN.color * _Color;
                return OUT;
            }

            sampler2D _MainTex;

            fixed4 frag(v2f IN) : SV_Target
            {
                half4 color = tex2D(_MainTex, IN.texcoord) * IN.color;
                clip (color.a - 0.01);
                #ifdef DO_NOT_DRAW
                    color.a = 0;
                #endif
                return color;
            }
        ENDCG
        }
    }
}


If you want to compare, here is the default UI image shader:

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
    }

    SubShader
    {
        Tags
        { 
            "Queue"="Transparent" 
            "IgnoreProjector"="True" 
            "RenderType"="Transparent" 
            "PreviewType"="Plane"
            "CanUseSpriteAtlas"="True"
        }

        Stencil
        {
            Ref [_Stencil]
            Comp [_StencilComp]
            Pass [_StencilOp] 
            ReadMask [_StencilReadMask]
            WriteMask [_StencilWriteMask]
        }

        Cull Off
        Lighting Off
        ZWrite Off
        ZTest [unity_GUIZTestMode]
        Blend SrcAlpha OneMinusSrcAlpha
        ColorMask [_ColorMask]

        Pass
        {
        CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            struct appdata_t
            {
                float4 vertex   : POSITION;
                float4 color    : COLOR;
                float2 texcoord : TEXCOORD0;
            };

            struct v2f
            {
                float4 vertex   : SV_POSITION;
                fixed4 color    : COLOR;
                half2 texcoord  : TEXCOORD0;
            };

            fixed4 _Color;

            v2f vert(appdata_t IN)
            {
                v2f OUT;
                OUT.vertex = UnityObjectToClipPos(IN.vertex);
                OUT.texcoord = IN.texcoord;
#ifdef UNITY_HALF_TEXEL_OFFSET
                OUT.vertex.xy += (_ScreenParams.zw-1.0)*float2(-1,1);
#endif
                OUT.color = IN.color * _Color;
                return OUT;
            }

            sampler2D _MainTex;

            fixed4 frag(v2f IN) : SV_Target
            {
                half4 color = tex2D(_MainTex, IN.texcoord) * IN.color;
                clip (color.a - 0.01);
                return color;
            }
        ENDCG
        }
    }
}

To summarize: create CanvasImageMask, create material based on this shader, and place an image on the canvas. Create another material, based on default shader, set Stencil ID to 1, and Stencil comparison to 5 (aka "Greater"). Assign this material to any other image on the canvas that comes after mask object.

References:


它的效果很好,但是嵌套图片怎么办?我有几张图片在一个容器图像父级内部,应用这些材料后,所有位于下方的内容都会弹出父级。 - Lobsang White
@LobsangWhite,请更具体地说明。您期望的顺序是什么,哪些图像应该被遮罩?例如,这是我所做的:[根]->[根的子元素1=不可见蒙版]->[根的子元素2=要被遮罩的图像]->[根的任何其他子元素]。这有效,因为在画布中顺序很重要。例如,一些嵌套也可以工作:[根]->[根的子元素1=不可见蒙版]->[蒙版的子元素1=要被遮罩的对象]->[根的任何其他子元素]。第二个示例同样有效,因为画布中的顺序相同:根、子元素1(子元素1的所有子元素)、子元素2(子元素2的所有子元素)、子元素3等。 - Anton Kasabutski
我将尝试这个顺序,不过想象一下一个三明治,其中奶酪是进度条,位于进度条容器和顶部引用图标之间。通常情况下,当使用画布组修改alpha时,像这样的一切看起来都很糟糕,就像在这里描述的问题一样。 - Lobsang White
@LobsangWhite的三明治比喻更好地解释了你的情况。因此,在你的例子中,父母不起任何作用,绘制顺序才是关键。容器有图像吗?如果有,那么顺序如下:使用进度条形状的蒙版(模板ID=1),使用图标形状的蒙版(模板ID=1),容器(可蒙版,带有模板ID=1),使用图标形状的蒙版(ID 2),进度条(可蒙版,带有模板ID=2),图标(默认材料/着色器)。 ID = 1的蒙版将在进度条和图标所在的位置剪切容器的图像,而ID = 2的蒙版将在图标后面剪切进度条。 - Anton Kasabutski
@LobsangWhite 我也有一个进度条和一个图标在其上方。但是,我的图标放在了进度条的侧面,而不是中间。因此,我只使用1个遮罩来剪切除图标本身以外的所有内容(包括进度条和背景)。我避免在根目录中使用带有图像的容器,因为这会使开发或维护变得复杂。 - Anton Kasabutski

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接