Masque [Apprentissage du code source Unity] : Apprentissage Mask et RectMask2D

avant-propos

Le découpage d'UGUI est divisé en Mask et Mask2D.

Table des matières

  • Analyse du principe du masque
  • Analyse du principe RectMask2D
  • Différence de performances entre RectMask2D et Mask

1. Analyse du principe du masque

Masque:IMaskable,IMaterialModifier

Regardons d'abord Mask. Il peut spécifier une image découpée pour que Mask découpe des sous-éléments. Nous spécifions une image circulaire pour Mask, puis les éléments sous les nœuds enfants seront coupés dans cette zone circulaire.

Le principe de réalisation de Mask :

1. Le masque donnera à l'image un matériau spécial, qui marquera chaque pixel de l'image et stockera le résultat du marquage dans un cache (ce cache est appelé Stencil Buffer )
2. Lorsque l'interface utilisateur de sous-niveau est rendue, elle vérifiera le marque dans le Stencil Buffer, s'il y a une marque dans la zone actuellement couverte (c'est-à-dire que la zone est dans la couverture de l'image), rendez-la, sinon elle ne sera pas rendue

1.1 StencilBuffer

Cela semble assez simple, alors qui est le héros derrière ça - StencilBuffer ?

En termes simples, le GPU alloue une zone de mémoire de 1 octet appelée StencilBuffer pour chaque pixel , qui peut être utilisée pour enregistrer ou supprimer des pixels.

Prenons un exemple simple pour illustrer la nature de ce tampon.

Comme le montre la figure ci-dessus, il y a 1 image rouge et 1 image verte dans notre scène, et la partie qui se chevauche se trouve dans le cadre noir. Le rendu d'une image commence. Tout d'abord, l'image verte "dessine" la couleur de chaque pixel dans sa zone de couverture sur l'écran, puis l'image rouge dessine également sa propre couleur sur l'écran, ce qui est l'effet sur la figure.

Dans ce cas, le rouge recouvre complètement le vert dans la zone de chevauchement. Ensuite, nous ajoutons un composant Mask à l'image verte. Alors ça devient comme ça :

À ce stade, le rendu d'une image commence. Tout d'abord, l'image verte peint sa zone de couverture en vert, et en même temps, la valeur du tampon de stencil de chaque pixel est définie sur 1. À ce moment, la distribution du tampon de stencil de l'écran est comme suit:

Ensuite, c'est au tour de l'image rouge de "peindre".Avant de peindre en rouge, elle va d'abord retirer la valeur du stencil buffer de ce point pour juger, dans la plage du cadre noir, cette valeur est 1, donc continuez à dessiner en rouge ; en dehors de la plage du cadre noir, cette La valeur est 0, donc le rouge n'est plus dessiné, et l'effet dans l'image est finalement atteint.

Donc, essentiellement, le tampon de gabarit existe pour permettre la communication entre plusieurs "peintres". Étant donné que le GPU est une opération de pipeline, ils ne peuvent pas communiquer directement les uns avec les autres, de sorte que le message est transmis via cette zone de données partagée , afin d'atteindre des objectifs "indicibles".

1.2 Nuanceur d'unité

Après avoir compris le principe du stencil, regardons sa syntaxe. Le format de syntaxe défini dans le shader d'unité est le suivant

(Les valeurs entre crochets sont des valeurs modifiables, et le reste sont des mots clés) :

Stencil
{
    
    
	Ref [_Stencil]//Ref表示要比较的值;0-255
	Comp [_StencilComp]//Comp表示比较方法(等于/不等于/大于/小于等);
	Pass [_StencilOp]// Pass/Fail表示当比较通过/不通过时对stencil buffer做什么操作
			// Keep(保留)
			// Replace(替换)
			// Zero(置0)
			// IncrementSaturate(增加)
			// DecrementSaturate(减少)
	ReadMask [_StencilReadMask]//ReadMask/WriteMask表示取stencil buffer的值时用的mask(即可以忽略某些位);
	WriteMask [_StencilWriteMask]
}

La traduction est : la valeur du tampon stencil est associée à ReadMask, puis Comp est comparée à la valeur Ref. Si le résultat est vrai, l'opération Pass est effectuée, sinon, l'opération Fail est effectuée et la valeur de l'opération est associé à AND avec WriteMask avant d'écrire le tampon stencil.

1.2.1 Interface utilisateur/Par défaut

Enfin, examinons le Shader qu'Unity utilise par défaut lors du rendu des composants de l'interface utilisateur - UI/Default (certains contenus non pertinents sont omis) :

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
	}
···
}

1.3 Implémentation du code source du masque

Après avoir compris le gabarit, regardons l'implémentation du code source du masque

Étant donné que le recadrage doit recadrer l'image et le texte en même temps, Image et Texte sont dérivés de MaskableGraphic.

Si vous souhaitez couper les éléments sous le nœud Mask, il doit occuper un DrawCall, car ces éléments ont besoin d'un nouveau paramètre Shader pour être rendus.

Comme indiqué dans le code suivant, MaskableGraphic implémente l'interface IMaterialModifier, et StencilMaterial.Add() sert à définir les paramètres de coupe dans le Shader.

MaskableGraphic.cs
        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 optimisation
            // it adds some coupling between components though :(
            // 如果我们用了Mask,它会生成一个mask材质,
            Mask maskComponent = GetComponent<Mask>();
            if (m_StencilValue > 0 && (maskComponent == null || !maskComponent.IsActive()))
            {
    
    
                //设置模板缓冲值,并且设置在该区域内的显示,不在的裁切掉
                var maskMat = StencilMaterial.Add(toUse,  // Material baseMat
                    (1 << m_StencilValue) - 1,            // 参考值
                    StencilOp.Keep,                       // 不修改模板缓存
                    CompareFunction.Equal,                // 相等通过测试
                    ColorWriteMask.All,                   // ColorMask
                    (1 << m_StencilValue) - 1,            // Readmask
                    0);                                   //  WriteMas
                StencilMaterial.Remove(m_MaskMaterial);
                //并且更换新的材质
                m_MaskMaterial = maskMat;
                toUse = m_MaskMaterial;
            }
            return toUse;
        }

Lorsque l'objet Image exécute Rebuild(), la méthode UpdateMaterial() obtiendra le matériau à rendre et déterminera si le composant de l'objet actuel hérite de l'interface IMaterialModifier.

Si tel est le cas, il est lié au script Mask, puis appelle la méthode GetModifiedMaterial mentionnée ci-dessus pour modifier les paramètres du Shader sur le matériau.

Image.cs 
    protected virtual void UpdateMaterial()
    {
    
    
        if (!IsActive())
            return;
        //更新刚刚替换的新的模板缓冲的材质
        canvasRenderer.materialCount = 1;
        canvasRenderer.SetMaterial(materialForRendering, 0);
        canvasRenderer.SetTexture(mainTexture);
    }

    public virtual Material materialForRendering
    {
    
    
        get
        {
    
    
            //遍历UI中的每个Mask组件
            var components = ListPool<Component>.Get();
            GetComponents(typeof(IMaterialModifier), components);
            //并且更新每个Mask组件的模板缓冲材质
            var currentMat = material;
            for (var i = 0; i < components.Count; i++)
                currentMat = (components[i] as IMaterialModifier).GetModifiedMaterial(currentMat);
            ListPool<Component>.Release(components);
            //返回新的材质,用于裁切
            return currentMat;
        }
    }

Étant donné que le tampon de gabarit peut fournir la zone du modèle, c'est-à-dire l'image circulaire définie précédemment, l'élément sera éventuellement coupé au centre de l'image.

1.3.1 Mask.GetModifiedMaterial

Mask.cs        
        /// Stencil calculation time!
        public virtual Material GetModifiedMaterial(Material baseMaterial)
        {
    
    
            if (!MaskEnabled())
                return baseMaterial;

            var rootSortCanvas = MaskUtilities.FindRootSortOverrideCanvas(transform);
            var stencilDepth = MaskUtilities.GetStencilDepth(transform, rootSortCanvas);
            // stencil只支持最大深度为8的遮罩
            if (stencilDepth >= 8)
            {
    
    
                Debug.LogError("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)
            {
    
    
                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;
                graphic.canvasRenderer.popMaterialCount = 1;
                graphic.canvasRenderer.SetPopMaterial(m_UnmaskMaterial, 0);

                return m_MaskMaterial;
            }

            //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;
        }

Le composant Mask appelle le shader de modèle pour créer son propre shader, il utilise donc la méthode de modèle dans le rendu en temps réel pour découper les parties qui n'ont pas besoin d'être affichées, et tous les nœuds enfants du composant Mask seront coupés .

On peut dire que Mask est le découpage fait dans le GPU, et la méthode utilisée est la méthode template dans le shader.


2. Analyse de principe de RectMask2D

RectMask2D:IClippable

Le flux de travail de RectMask2D est à peu près le suivant :

①Couche C# : Trouvez l'intersection de toutes les zones de couverture RectMask2D dans l'objet parent ( FindCullAndClipWorldRect )
②Couche C# : Tous les composants d'objet enfant qui héritent de MaskGraphic appellent la méthode pour définir la zone de découpage ( SetClipRect ) et la transmettent à Shader
③Couche Shader : Recevoir la zone rectangulaire _ClipRect, Dans le shader de fragment, il est jugé si le pixel est dans la zone rectangulaire, sinon, la transparence est définie sur 0 ( UnityGet2DClipping )
④ Couche de shader : Ignorer les éléments avec un alpha inférieur à 0,001 ( clip (color. a - 0,001) )

Ensuite, étudions en détail le principe de RectMask2D.Lorsque Canvas.willRenderCanvases() a été introduit plus tôt, ClipperRegistry.instance.Cull() sera appelé dans la méthode PerformUpdate pour traiter toutes les découpes Mask2D dans l'interface.

CanvasUpdateRegistry.cs
        protected CanvasUpdateRegistry()
        {
    
    
            Canvas.willRenderCanvases += PerformUpdate;
        }

        private void PerformUpdate()
        {
    
    
            //...略
            // 开始裁切Mask2D
            ClipperRegistry.instance.Cull();
            //...略
        }

ClipperRegistry.cs
        public void Cull()
        {
    
    
            for (var i = 0; i < m_Clippers.Count; ++i)
            {
    
    
                m_Clippers[i].PerformClipping();
            }
        }

RectMask2D enregistrera le composant actuel avec ClipperRegistry.Register(this) dans la méthode OnEnable() ;

De cette façon, dans la méthode ClipperRegistry.instance.Cull(); ci-dessus, vous pouvez parcourir tous les composants Mask2D et appeler leur méthode PerformClipping().

La méthode PerformClipping() doit trouver tous les éléments de l'interface utilisateur qui doivent être coupés, car Image et Text héritent de l'interface IClippable, et Cull() sera appelé pour couper.

RectMask2D.cs
    protected override void OnEnable()
    {
    
    
        //注册当前RectMask2D裁切对象,保证下次Rebuild时可进行裁切。
        base.OnEnable();
        m_ShouldRecalculateClipRects = true;
        ClipperRegistry.Register(this);
        MaskUtilities.Notify2DMaskStateChanged(this);
    }

        public virtual void PerformClipping()
        {
    
    
            //...略
            bool validRect = true;
            Rect clipRect = Clipping.FindCullAndClipWorldRect(m_Clippers, out validRect);
            bool clipRectChanged = clipRect != m_LastClipRectCanvasSpace;
            if (clipRectChanged || m_ForceClip)
            {
    
    
                foreach (IClippable clipTarget in m_ClipTargets)
                    //把裁切区域传到每个UI元素的Shader中[划重点!!!]
                    clipTarget.SetClipRect(clipRect, validRect);

                m_LastClipRectCanvasSpace = clipRect;
                m_LastValidClipRect = validRect;
            }

            foreach (IClippable clipTarget in m_ClipTargets)
            {
    
    
                var maskable = clipTarget as MaskableGraphic;
                if (maskable != null && !maskable.canvasRenderer.hasMoved && !clipRectChanged)
                    continue;
                // 调用所有继承IClippable的Cull方法
                clipTarget.Cull(m_LastClipRectCanvasSpace, m_LastValidClipRect);
            }
        }

MaskableGraphic.cs
        public virtual void Cull(Rect clipRect, bool validRect)
        {
    
    
            var cull = !validRect || !clipRect.Overlaps(rootCanvasRect, true);
            UpdateCull(cull);
        }

        private void UpdateCull(bool cull)
        {
    
    
            var cullingChanged = canvasRenderer.cull != cull;
            canvasRenderer.cull = cull;

            if (cullingChanged)
            {
    
    
                UISystemProfilerApi.AddMarker("MaskableGraphic.cullingChanged", this);
                m_OnCullStateChanged.Invoke(cull);
                SetVerticesDirty();
            }
        }

RectMask2D passera la zone de RectTransform dans Shader en tant que **_ClipRect**.

Une valeur de 0 pour Stencil Ref signifie qu'il n'utilise pas de comparaison de tampon de stencil

Dans les pixels de traitement Frag de Shader, la zone rognée est comparée à UnityGet2DClipping() selon _ClipRect pour voir si le pixel actuel se trouve dans la zone de rognage, et sinon, Color.a devient transparent.

Shader "UI/Default"
{
    
    
    //...略
    fixed4 frag(v2f IN) : SV_Target
    {
    
    
        half4 color = (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd) * IN.color;
        //根据_ClipRect比较当前像素是否在裁切区域中,如果不在颜色将设置成透明
        #ifdef UNITY_UI_CLIP_RECT
        color.a *= UnityGet2DClipping(IN.worldPosition.xy, _ClipRect);
        #endif
        #ifdef UNITY_UI_ALPHACLIP
        clip (color.a - 0.001);
        #endif
        return color;
    }
}

3. Distinction des performances entre RectMask2D et Mask

3.1 RectMask2D

Mask2D n'a pas besoin de s'appuyer sur un composant Image et sa zone de découpage correspond à la taille rect de son RectTransform.

  • Propriété 1 : Tous les enfants sous le nœud RectMask2D ne peuvent pas être regroupés avec des nœuds d'interface utilisateur externes, et plusieurs RectMask2D ne peuvent pas être regroupés. [Le pro-test ne peut pas être approuvé]
  • Nature 2 : lors du calcul de la profondeur, tous les RectMask2D sont traités comme des nœuds d'interface utilisateur généraux, mais il n'a pas de composant CanvasRenderer, il ne peut donc pas être considéré comme le bottomUI d'un contrôle d'interface utilisateur.

3.2 Masque

Le composant Mask doit s'appuyer sur un composant Image et la zone de découpage correspond à la taille de l'image.

  • Nature 1 : Mask aura deux autres drawcalls au début et à la fin (premier=nœud Mask, tail=enfants sous le nœud Mask après traversée). Si plusieurs Masks remplissent les conditions de batching, ces deux drawcalls peuvent correspondre au batching (mask1's The first combinaison du premier et du masque2 ; la dernière combinaison du masque1 et la dernière du masque2. Le premier et le dernier ne peuvent pas être combinés)
  • Nature 2 : lors du calcul de la profondeur, lors de la traversée jusqu'au début d'un masque, traitez-le comme un nœud d'interface utilisateur qui ne peut pas être regroupé, mais faites attention au bottomUI qui peut être utilisé comme son nœud d'interface utilisateur enfant.
  • Nature 3 : les nœuds d'interface utilisateur dans le masque et les nœuds d'interface utilisateur qui ne sont pas à l'extérieur du masque ne peuvent pas être regroupés, mais les nœuds d'interface utilisateur dans plusieurs masques peuvent être regroupés s'ils remplissent les conditions de regroupement.

D'après la nature du masque 3, on peut voir que ce n'est pas que plus il y a de masques, mieux c'est, car les masques peuvent être regroupés. Conclu comme suit :

  • Lorsqu'une interface n'a qu'un seul masque, alors RectMask2D est meilleur que Mask
  • Quand il y a deux masques, alors les deux sont presque identiques.
  • Lorsqu'il est supérieur à deux masques, alors Mask est meilleur que RectMask2D.
  • S'il ne s'agit que d'un recadrage rectangulaire, RectMask2D n'a pas besoin de recréer le matériau, et chaque image est rendue à nouveau avec un nouveau matériau, de sorte que l'efficacité de RectMask2D sera supérieure à celle de Mask .

référence

[Apprentissage du code source Unity] Mask : Analyse et compréhension de Mask et Mask2D Unity mask

Analyse du code source des similitudes et des différences entre Mask, Rect Mask2D et Sprite Mask

Distinction simple entre les performances de RectMask2D et Mask

Je suppose que tu aimes

Origine blog.csdn.net/m0_58523831/article/details/131171958
conseillé
Classement