Présentation d'Unity 2D Filters
Vous essayez de créer des effets sophistiqués pour votre jeu Unity Sprite 2D mais ne savez pas par où commencer ? Vous avez entendu parler des shaders mais c'est de la magie noire pour vous ? Pourquoi est-ce…
Filters 2D était une bonne expérience dans l'univers des shaders. J'ai rejoint Da Viking Code en avril et l'une de mes missions principales était de mettre à jour ce plugin et d'aller plus loin dans les expérimentations sur les shaders…

Filters 2D était une bonne expérience dans l’univers des shaders. J’ai rejoint Da Viking Code en avril, et l’une de mes missions principales était de mettre à jour ce plugin et d’aller plus loin dans les expérimentations autour des shaders. Nous sommes heureux de proposer une mise à jour du plugin avec de nombreuses améliorations. Passons en revue quelques explications sur les problèmes rencontrés durant ces mises à jour et corrections.

Artefacts sur les filtres outline, pixelate et blur sur un Mali-450
La première chose sur laquelle nous avons travaillé fut l’affichage de certains artefacts sur les filtres pixelate, outline et blur. Ces artefacts étaient visibles sur des appareils mobiles.
Nous avons testé les filtres sur nos appareils mobiles pour déterminer la cause de ces artefacts. Nous avons constaté que les artefacts apparaissaient sur des mobiles d’entrée/milieu de gamme, tandis que le haut de gamme n’avait aucun problème. Cet effet d’artefact ressemble à un autre artefact bien connu en simulation de lumière : l’artefact d’acné (acne artefact). Cet artefact peut être causé par un problème de précision des flottants. Lorsqu’on travaille avec des mesures extrêmement précises et qu’on leur applique des opérations mathématiques, il est possible d’atteindre la limite de précision du flottant. La différence entre les valeurs flottantes devient alors plus grande que la précision requise. Il en résulte un effet d’escalier sur les valeurs calculées, créant cet artefact d’acné.
Dans les shaders, lorsqu’on travaille au niveau du pixel, les positions de pixel sur une texture sont comprises entre 0 et 1 : {0,0} pour l’origine de la texture et {1,1} pour sa fin. Avec une plage aussi fine, la précision des flottants est cruciale. Nous nous sommes donc principalement concentrés sur ce point.
Après quelques recherches, il apparaît que les GPU mobiles ne sont pas égaux en matière de résolution des flottants. Certains GPU comme le Mali-450 n’ont que 16 bit pour encoder un flottant, contre 32 bit pour un GPU desktop. Vous trouverez plus d’informations à ce sujet dans la documentation Unity : https://docs.unity3d.com/Manual/SL-DataTypesAndPrecision.html.
Au cours de nos recherches, nous sommes tombés sur les travaux de Tom Olson et Stuart Russell sur la précision des flottants. https://community.arm.com/graphics/b/blog/posts/benchmarking-floating-point-precision-in-mobile-gpus. Leur travail consiste à déterminer le nombre de bits du flottant qui participent à la précision décimale. Nous avons adapté leurs travaux pour utiliser leur shader sur Unity. Nous avons constaté que le GPU Mali-450 dispose de 10 bit sur les 16 bit du flottant pour la précision décimale. Sur un GPU desktop, ce nombre est de 23.

Shader de Tom Olson sur un Mali-450. On voit 10 lignes grises, ce qui signifie 10 bit de précision décimale sur le flottant

Shader de Tom Olson sur une Geforce 1070. On voit 23 lignes grises, ce qui signifie 23 bit de précision décimale sur le flottant
Avec cette information, nous avons cherché un moyen de réduire la précision nécessaire ou de contourner le problème.
D’abord, nous avons envisagé de calculer la valeur dans le vertex shader, car la résolution des flottants y est souvent plus élevée que dans le fragment shader. Mais lorsqu’on récupère les pixels de la texture dans le fragment shader pour créer l’effet, la valeur était redéfinie avec la résolution basse. Nous avons ensuite essayé de réaliser l’effet directement dans le vertex shader (calcul des valeurs + combinaison des pixels), sans succès. Ce n’est pas très bien supporté par les shaders, seules les versions les plus récentes des API graphiques permettent cette fonctionnalité. Et d’autre part, le workflow des API graphiques n’est pas conçu pour travailler sur les pixels ailleurs que dans le fragment shader.
Donc, puisque le problème ne peut pas être résolu dans les shaders ni contourné, nous avons regardé dans l’autre sens. Les shaders travaillent sur la position des pixels, et c’est avec la précision des flottants que les artefacts apparaissent. Nous avons donc examiné les sprites pour voir ce qui pouvait être fait.
Les sprites de notre démo se trouvaient dans un atlas de 4096x2048 pixels. C’est un atlas assez énorme, donc la taille d’un pixel est de 1/4096 (2.44*10-4). Cela demande une précision très fine, et avec la faible résolution des flottants sur GPU mobile, il n’est pas surprenant que cela pose problème. Pour en être sûrs, nous avons testé avec un sprite dans son fichier source et aucun artefact n’est apparu.
Nous avons créé une scène de test benchmark pour observer les conséquences de la taille de l’atlas sur l’effet des filtres, et décider si réduire la taille de l’atlas était une bonne solution.

Benchmark de la résolution de l’atlas sur le test des filtres sur un Mali-450
Comme vous pouvez le voir, la taille de l’atlas a un impact significatif sur les artefacts lorsque la résolution des flottants est faible.
Nous vous conseillons donc d’utiliser les grands atlas avec précaution, surtout si vous souhaitez utiliser les filtres sur mobile. Du moins, sur les mobiles d’entrée et de milieu de gamme en 2017.
Nos filtres peuvent être appliqués sur des éléments UI, mais nous avons découvert qu’ils ne prenaient en compte aucun masque UI : ni le 2D rect mask, ni le masque standard. Nous avons donc implémenté cette fonctionnalité.
Le 2D rect mask fonctionne uniquement sur l’espace 2D {X, Y}, l’espace Z étant ignoré. Il crée un rectangle (2 vector2 {X,Y} dans un vector4) suivant la forme du GameObject UI auquel il est attaché. Ce rectangle est ensuite transmis aux GameObject enfants pour être transféré à leurs shaders. Ici, les shaders doivent comparer la position monde de leur pixel au rectangle fourni par le masque, c’est UnityGet2DClipping() qui fait ce travail.
Voici le code minimum pour utiliser le 2D rect mask dans vos shaders :
Shader "MyShader"
{
Properties {
[PerRendererData]_MainTex_MainTex("Base (RGB)", 2D) = "white" {} // Main texture
}
SubShader {
Tags { ... }
Cull Off
Lighting Off
ZWrite Off
ZTest [unity_GUIZTestMode]
Blend SrcAlpha OneMinusSrcAlpha
Pass {
CGPROGRAM // Define shader program
#pragma vertex vert // Specify vertex shader function called vert
#pragma fragment frag // Specify fragment shader function called frag
#pragma multi_compile __ UNITY_UI_ALPHACLIP
#include "UnityCG.cginc"
#include "UnityUI.cginc" // Import the UnityGet2DClipping funciton
// Define structure to pass to vertex shader
struct appdata_t {
float4 vertex : POSITION;
float4 color : COLOR;
float2 texcoord : TEXCOORD0;
};
// Define structure to pass from vertex shader to fragment shader
struct v2f {
half2 texcoord : TEXCOORD0; // Texture coords given by Unity
float4 vertex : SV_POSITION; // Position of the vertex after being transformed into projection space given by Unity
fixed4 color : COLOR; // Vertex color given by Unity
float4 worldPosition : TEXCOORD1; // The world pixel position for the 2D Rect Mask
};
// Get properties of shader
sampler2D _MainTex;
float4 _ClipRect; // The 2D rectangle Mask
// Vertex shader
v2f vert(appdata_t IN) {
v2f OUT;
OUT.worldPosition = IN.vertex;
OUT.vertex = UnityObjectToClipPos(OUT.worldPosition);
OUT.texcoord = IN.texcoord;
OUT.color = IN.color;
return OUT;
}
// Fragment shader to give final color of the pixel
fixed4 frag(v2f i) : COLOR {
half4 outputColor = (tex2D(_MainTex, IN.texcoord) * IN.color;
outputColor.a *= UnityGet2DClipping(IN.worldPosition.xy, _ClipRect);
#ifdef UNITY_UI_ALPHACLIP
clip (outputColor.a - 0.001);
#endif
return outputColor;
}
ENDCG
} // Pass
} // SubShader
Fallback "Sprites/Default"
}
En plus du 2D rect mask, on peut utiliser le masque standard. Celui-ci utilise le stencil buffer pour réaliser la fonctionnalité de masquage.

Schéma simplifié du fonctionnement du stencil buffer
Le stencil buffer, dans les shaders, sert à afficher ou non un pixel de texture. C’est plus ou moins une fonction de masque appliquée après le z-buffer.
Il est composé d’un nombre assigné au shader, d’un nombre stocké dans un buffer lisible par tous les shaders, et d’une fonction de comparaison mathématique. Les nombres sont compris entre 0-255 et le buffer stocke un nombre pour chaque pixel écran, comme une image.
Le programme détermine sur quel pixel écran le pixel de texture courant sera rendu, et récupère le nombre du stencil buffer pour ce pixel écran. Le nombre stencil du shader est comparé au nombre stencil récupéré précédemment. Si la comparaison mathématique est exacte (ou vraie), le pixel est affiché, sinon il ne l’est pas.
8 fonctions de comparaison mathématique existent : Always, Never, Greater, GEqual, Less, LEqual, Equal, et NotEqual.
| Nom de la fonction de comparaison | Nombre correspondant dans les shaders | Signification mathématique | Résultats des tests |
|---|---|---|---|
| Always | 8 | Toujours exact ou vrai | 0 Always 1 ⇒ √ 0 Always 0 ⇒ √ 1 Always 0 ⇒ √ |
| Never | 1 | Jamais exact ou vrai | 0 Never 1 ⇒ Χ 0 Never 0 ⇒ Χ 1 Never 0 ⇒ Χ |
| Greater | 5 | > | 0 Greater 1 ⇒ Χ 0 Greater 0 ⇒ Χ 1 Greater 0 ⇒ √ |
| GEqual | 7 | >= | 0 GEqual 1 ⇒ Χ 0 GEqual 0 ⇒ √ 1 GEqual 0 ⇒ √ |
| Less | 3 | < | 0 Less 1 ⇒ √ 0 Less 0 ⇒ Χ 1 Less 0 ⇒ Χ |
| LEqual | 4 | <= | 0 LEqual 1 ⇒ √ 0 LEqual 0 ⇒ √ 1 LEqual 0 ⇒ Χ |
| Equal | 3 | = | 0 Equal 1 ⇒ Χ 0 Equal 0 ⇒ √ 1 Equal 0 ⇒ Χ |
| NotEqual | 6 | ≠ | 0 NotEqual 1 ⇒ √ 0 NotEqual 0 ⇒ Χ 1 NotEqual 0 ⇒ √ |
Après cela, le nombre du stencil buffer peut être modifié et transmis au shader suivant.
Vous trouverez plus d’informations sur le stencil buffer dans la documentation Unity : https://docs.unity3d.com/Manual/SL-Stencil.html.
Dans notre cas, voici le code pour utiliser le stencil buffer :
Shader "MyShader" {
Properties {
[PerRendererData]_MainTex_MainTex("Base (RGB)", 2D) = "white" {} // Main texture
// required for UI.Mask
_StencilComp ("Stencil Comparison", Float) = 8 // The Mathematical compare function
_Stencil ("Stencil ID", Float) = 0 // The stencil shader number
}
SubShader {
Tags{
"Queue" = "Transparent"
"IgnoreProjector" = "true"
"RenderType" = "Transparent"
"PreviewType" = "Plane"
"CanUseSpriteAtlas"="True"
}
// required for UI.Mask
Stencil {
Ref [_Stencil] // The stencil shader number
Comp [_StencilComp] // The Mathematical compare function
}
Cull Off
Lighting Off
ZWrite Off
ZTest [unity_GUIZTestMode]
Blend SrcAlpha OneMinusSrcAlpha
Pass {
CGPROGRAM // Define shader program
#pragma vertex vert // Specify vertex shader function called vert
#pragma fragment frag // Specify fragment shader function called frag
#include "UnityCG.cginc"
// Define structure to pass to vertex shader
struct appdata_t {
float4 vertex : POSITION;
float4 color : COLOR;
float2 texcoord : TEXCOORD0;
};
// Define structure to pass from vertex shader to fragment shader
struct v2f {
half2 texcoord : TEXCOORD0; // Texture coords given by Unity
float4 vertex : SV_POSITION; // Position of the vertex after being transformed into projection space given by Unity
fixed4 color : COLOR; // Vertex color given by Unity
};
// Get properties of shader
sampler2D _MainTex;
// Vertex shader
v2f vert(appdata_t IN) { ... }
// Fragment shader to give final color of the pixel
fixed4 frag(v2f i) : COLOR { ... }
ENDCG
} // Pass
} // SubShader
Fallback "Sprites/Default"
}
Nous n’avons pas mentionné tout le travail réalisé dans cette mise à jour, mais nous espérons que vous apprécierez les corrections et les nouvelles fonctionnalités.
Pour la prochaine mise à jour, quels types d’effets de filtre aimeriez-vous voir dans Filters2D ? Ou peut-être une nouvelle fonctionnalité ?
Nous espérons que cette mise à jour vous plaira !