Effet de shader Doodle

Dans ce didacticiel, je vais vous expliquer comment recréer l'effet de doodle sprite populaire dans Unity à l'aide de shaders. Si votre style nécessite ce style, alors cet article vous apprendra comment le réaliser sans rendre un tas d'images supplémentaires.

Au cours des dernières années, ce style est devenu de plus en plus populaire et est activement utilisé dans des jeux tels que GoNNER et Baba is You .


Ce didacticiel couvre tout ce dont vous avez besoin, des bases du codage des shaders aux mathématiques utilisées. À la fin de l'article, il y a un lien pour télécharger le package Unity complet.

Le succès de Doodle Studio 95 m'a inspiré pour créer ce tutoriel ! .

Présentation


Dans mon blog, j'explore des sujets assez complexes, des mathématiques de la cinématique inverse à la diffusion de Rayleigh atmosphérique . J'aime vraiment rendre ces sujets difficiles compréhensibles pour un large public. Mais le nombre de personnes intéressées et ayant un niveau technique suffisant n'est pas si grand. Par conséquent, vous ne devriez pas être surpris que les articles les plus populaires soient les plus simples. Cela s'applique également à un récent tweet de Nick Caman dans lequel il a montré comment créer un effet de griffonnage dans Unity.


Après 1000 likes et 4000 retweets, il est devenu clair qu'il existe une forte demande pour des tutoriels plus simples qui peuvent être étudiés même par des personnes qui n'ont pratiquement aucune connaissance de la création de shaders.

Si vous cherchez un moyen professionnel et efficace d'animer des sprites 2D avec un haut niveau de contrôle artistique, alors je recommande fortement Doodle Studio 95! (voir GIF ci-dessous). Ici, vous pouvez regarder quelques jeux qui utilisent cet outil.


Anatomie de l'effet Doodle


Pour recréer l'effet doodle, nous devons d'abord comprendre comment il fonctionne et quelles techniques y sont utilisées.

Effet shader. Premièrement, nous voulons que cet effet soit aussi simple que possible et ne nécessite pas de scripts supplémentaires. Cela est possible grâce à l'utilisation de shaders qui indiquent à Unity comment rendre les modèles 3D (y compris les modèles plats!) À l'écran. Si vous n'êtes pas familier avec le monde du codage des shaders , alors vous devriez étudier mon article A Gentle Introduction to Shaders .

Shader de sprite. Unity est livré avec de nombreux types de shaders. Si vous utilisez les outils Unity 2D fournis, vous travaillez probablement avec des sprites . Si c'est le cas, vous avez besoin de Sprite Shader - un type spécial de shader compatible avec SpriteRenderer Unity. Ou vous pouvez commencer avec le shader Unlit plus traditionnel.

Décalage de sommet. Lorsque vous dessinez des sprites manuellement, aucun cadre ne coïncidera complètement avec les autres. Nous voulons que le sprite "vacille" d'une certaine manière afin de simuler cet effet. Les shaders ont un moyen très efficace de le faire, en utilisant le décalage de sommet . Il s'agit d'une technique qui vous permet de modifier la position des sommets d'un objet 3D. Si nous les déplaçons au hasard, nous obtenons l'effet souhaité.

Temps de trajet. Les animations à main levée ont généralement une faible fréquence d'images. Si nous voulons simuler, disons, cinq images par seconde, nous devons changer la position des sommets des sprites cinq fois par seconde. Cependant, Unity est susceptible d'exécuter le jeu à un taux de rafraîchissement beaucoup plus élevé; peut-être avec 30 ou même 60 images par seconde. Pour que le sprite ne change pas 60 fois par seconde, vous devez travailler sur le composant de synchronisation d'animation.

Étape 1: terminer le shader de sprite


Si vous souhaitez créer un nouveau shader dans Unity, le choix sera assez limité. Le shader le plus proche avec lequel nous pouvons commencer sera Shader non éclairé , bien qu'il ne soit pas nécessairement le mieux adapté à nos besoins.

Si vous souhaitez que le shader doodle soit entièrement compatible avec SpriteRenderer Unity, nous devons compléter le Sprite Shader existant. Malheureusement, nous ne pouvons pas y accéder directement depuis Unity lui-même.

Vous pouvez y accéder en accédant à la page d' archives de téléchargement d'Unity et en téléchargeant le package Build in shaders pour la version d'Unity avec laquelle vous travaillez. Il s'agit d'un fichier zip contenant le code source de tous les shaders fournis avec votre build Unity.


Après le téléchargement, décompressez-le et recherchez le fichier Sprites-Diffuse.shader dans le dossier builtin_shaders-2018.1.6f1\DefaultResourcesExtra . C'est le fichier que nous utiliserons dans le tutoriel.


Sprites-Diffuse n'est pas un shader de sprite standard!
Lors de la création d'un nouveau sprite, son matériau standard utilise un shader appelé Sprites-Default.shader , et non Sprites-Diffuse.shader .

La différence entre les deux est que le premier n'utilise pas d'éclairage et que le second réagit à l'éclairage de la scène. En raison de la nature de l'implémentation Unity, la version diffuse est beaucoup plus facile à éditer que la version sans éclairage.

À la fin de ce tutoriel, il y a un lien pour télécharger les shaders de doodle avec et sans éclairage.

Étape 2: décalage des sommets


À l'intérieur de Sprites-Diffuse.shader il y a une fonction appelée vert , qui est la fonction de sommet dont nous avons parlé ci-dessus. Son nom n'est pas important, l'essentiel est qu'il coïncide avec celui spécifié dans la section vertex: de la #pragma :

 #pragma surface surf Lambert vertex:vert nofog nolightmap nodynlightmap keepalpha noinstancing 

En bref, la fonction de sommet est appelée pour chaque sommet du modèle 3D et décide comment la superposer à l'espace d'écran bidimensionnel. Dans ce didacticiel, nous nous intéressons uniquement à la façon de déplacer un objet.

Le paramètre appdata_full v contient le champ de vertex , qui contient la position 3D de chaque sommet dans l'espace objet . Lorsque la valeur change, le sommet se déplace. C'est-à-dire, par exemple, que le code ci-dessous transfère l'objet avec son shader d'une unité le long de l'axe X.

 void vert (inout appdata_full v, out Input o) { v.vertex = UnityFlipSprite(v.vertex, _Flip); v.vertex.x += 1; #if defined(PIXELSNAP_ON) v.vertex = UnityPixelSnap (v.vertex); #endif UNITY_INITIALIZE_OUTPUT(Input, o); o.color = v.color * _Color * _RendererColor; } 

Par défaut, les jeux 2D créés dans Unity fonctionnent uniquement avec les axes X et Y, nous devons donc changer v.vertex.xy pour déplacer le sprite sur un plan bidimensionnel.

Qu'est-ce que l'espace objet?
Le champ de vertex de la structure appdata_full contient la position du sommet actuel traité par le shader dans l'espace objet. Il s'agit de la position du sommet sous l'hypothèse que l'objet est situé au centre du monde (0,0,0), sans changement d'échelle et sans rotation.

Les pics exprimés dans l'espace mondial reflètent leur position réelle dans la scène Unity.

Pourquoi l’objet ne se déplace-t-il pas à une vitesse d’un mètre par image?
Si nous ajoutons +1 à la composante x de la valeur transform.position dans la méthode Update d'un script C #, nous verrons comment l'objet vole vers la droite à une vitesse de 1 mètre par image, soit environ 216 kilomètres par heure.

En effet, les modifications apportées par C # modifient la position elle-même. Dans la fonction vertex, cela ne se produit pas. Le shader modifie uniquement la représentation visuelle du modèle, mais ne met pas à jour ni ne modifie les sommets stockés du modèle. C'est pourquoi l'ajout de +1 à v.vertex.x décale un objet d'un mètre une seule fois.

N'oubliez pas d'importer le sprite en tant que Tight!
Cet effet déplace le haut du sprite. Traditionnellement, les sprites sont importés dans Unity sous forme de quadrangles (voir la figure à gauche). Cela signifie qu'ils n'ont que quatre pics. Si c'est le cas, seuls ces points peuvent être déplacés, ce qui réduit la force de l'effet de griffonnage.

Pour une distorsion plus forte et plus réaliste, vous devez importer des sprites en choisissant Tight pour le paramètre Mesh Type , qui les transforme en une forme convexe (voir la figure à droite).


Cela augmente le nombre de sommets. Ce n'est pas toujours souhaitable, mais c'est exactement ce dont nous avons besoin maintenant.

Décalage aléatoire


L'effet de griffonnage décale aléatoirement la position de chaque sommet. L'échantillonnage de nombres aléatoires dans un shader a toujours été une tâche difficile. Cela est principalement dû à l'architecture GPU distribuée, qui complique et réduit l'efficacité de la recréation de l'algorithme utilisé dans la plupart des bibliothèques (y compris Mathf.Random ).

Le post de Nick Caman a utilisé une texture de bruit qui, une fois échantillonnée, donne l'illusion du hasard. Dans le cadre de votre projet, ce n'est peut-être pas l'approche la plus efficace, car dans ce cas, le nombre d'opérations de recherche de texture effectuées par le shader double.

Par conséquent, dans la plupart des shaders, des fonctions plutôt confuses et chaotiques sont utilisées, qui, malgré leur déterminisme, nous semblent n'avoir aucune régularité. Et comme ils doivent être distribués, chaque nombre aléatoire doit être généré avec sa propre graine. C'est très bien pour nous, car la position de chaque sommet doit être unique. Nous pouvons l'utiliser pour lier un nombre aléatoire à chaque sommet. Nous discuterons plus tard de l'implémentation de cette fonction aléatoire; appelons-le random3 .

Nous pouvons utiliser random3 pour générer un décalage aléatoire de chaque sommet. Dans l'exemple ci-dessous, les nombres aléatoires sont mis à l'échelle à l'aide de la propriété _NoiseScale , qui vous permet de contrôler la force de déplacement.

 void vert (inout appdata_full v, out Input o) { ... float2 noise = random3(v.vertex.xyz).xy * _NoiseScale; v.vertex.xy += noise; ... } 

Maintenant, nous devons écrire le code random3 .

image

Aléatoire dans le shader


L'une des fonctions pseudo-aléatoires les plus courantes et les plus emblématiques utilisées dans les shaders est tirée d'un article de W. Ray publié en 1998 sous le titre " Sur la génération de nombres aléatoires, à l'aide de y = [(a + x) sin (bx)] mod 1 ".

 float rand(float2 co) { return fract(sin(dot(co.xy ,float2(12.9898,78.233))) * 43758.5453); } 

Cette fonction est déterministe (c'est-à-dire qu'elle n'est pas vraiment aléatoire), mais elle se comporte de manière si aléatoire qu'elle semble complètement aléatoire. Ces fonctions sont appelées pseudo-aléatoires . Pour mon tutoriel, j'ai choisi une fonction plus complexe, inventée par Nikita Miropolsky.

La génération d'un nombre pseudo aléatoire dans un shader est un sujet très complexe. Si vous souhaitez en savoir plus sur elle, alors le Livre des Shaders a un bon chapitre sur elle. De plus, Patricio Gonzales Vivo a construit un grand référentiel de fonctions pseudo-aléatoires appelé bruit GLSL , qui peut être utilisé dans les shaders.

Étape 3: ajouter du temps


Grâce au code que nous avons écrit, chaque point de chaque image est décalé du même montant. Nous obtenons donc un sprite déformé, pas un effet de griffonnage. Pour résoudre ce problème, vous devez trouver un moyen de modifier l'effet au fil du temps. L'une des façons les plus simples de procéder consiste à utiliser la position du sommet et l'heure actuelle pour générer un nombre aléatoire.

Dans notre cas, je viens d'ajouter l'heure actuelle en secondes _Time.y à la position du sommet.

 float time = float3(_Time.y, 0, 0); float2 noise = random3(v.vertex.xyz + time).xy * _NoiseScale; v.vertex.xy += noise; 

Des effets plus complexes peuvent nécessiter des moyens plus sophistiqués pour ajouter du temps à l'équation. Mais comme nous ne nous intéressons qu'à un effet aléatoire intermittent, alors deux valeurs sont plus que suffisantes.


Commutation horaire


Le principal problème avec l'ajout de _Time.y est qu'il provoque l'animation de l'image-objet dans chaque image. Cela n'est pas souhaitable pour nous, car la plupart des animations dessinées à la main ont une faible fréquence d'images. La composante temporelle ne doit pas être continue, mais discrète. Cela signifie que si nous voulons afficher cinq images par seconde, cela ne devrait changer que cinq fois par seconde. Autrement dit, le temps devrait être lié à un cinquième de seconde. Les seules valeurs valides doivent être  frac05=0,  frac15=0,2,  frac25=0,4,  frac35=0,6,  frac45=0,8,  frac55=1avec, et ainsi de suite ...

J'ai déjà couvert Snap sur mon blog dans How To Snap To Grid . Dans cet article, j'ai proposé une solution au problème de la liaison de la position d'un objet sur une grille spatiale. Si nous devons lier le temps à une grille temporelle, les mathématiques, et donc le code, seront les mêmes.

La fonction ci-dessous prend le nombre x et le lie à des valeurs entières qui sont des multiples de snap .

 inline float snap (float x, float snap) { return snap * round(x / snap); } 

Autrement dit, notre code devient comme ceci:

 float time = snap(_Time.y, _NoiseSnap); float2 noise = random3(v.vertex.xyz + float3(time, 0.0, 0.0) ).xy * _NoiseScale; v.vertex.xy += noise; 


Conclusion


Le package Unity pour cet effet peut être téléchargé gratuitement sur Patreon .

Ressources supplémentaires


Au cours des derniers mois, un grand nombre de jeux dans le style de griffonnages sont apparus. Il me semble que la raison en est le succès de Doodle Studio 95! - Un outil pour Unity, développé par Fernando Ramallo . Si ce style convient à votre jeu, je vous recommande d'acheter cet outil incroyable.

Source: https://habr.com/ru/post/fr449540/


All Articles