Avec Clément Rivaille (aka Itooh), nous avons participé à la dernière game jam organisée par GMTK et avons proposé un jeu inspiré "bullet hell" où il faut alterner entre des phases de collecte et d'esquive : https://itooh.itch.io/powered-by-geometry.
À cette occasion, on en a profité pour tester Unity 6 et j'ai monté un petit effet de "Ripple", c'est-à-dire d'ondulations déformantes pour obtenir des effets grossissant ou rétrécissant de l'image.
Pour le jeu, nous avons monté deux effets : l'effet de réduction ci-dessus, mais aussi un effet de vague d'explosion.
Le principe :
- On ajoute un effet de post effect full screen à la render pipeline.
- L'effet de post effect s'appuie sur une deuxième image qui contient les informations de déformation (l'équivalent d'une normalmap mais qui indique un décalage pour chaque pixel plutôt qu'une inclinaison). Ce shader fera un sampling de l'image d'origine en appliquant le décalage indiqué.
- La deuxième image avec informations de déformation est générée via une deuxième caméra sur un layer dédié qui rend dans une RenderTexture.
Le Post Effect
Dans Unity 6 avec URP, l'effet Post Effect en plein écran peut s'avérer a priori plus complexe que précédemment. En rendu built-in, on ferait typiquement un script qui surcharge OnRenderImage
et qui utilise Graphics.Blit à partir des textures source et de destination fournies par l'API. Malheureusement cette méthode n'est plus disponible en URP, et la nouvelle fonctionnalité pour faire cette opération de post effet s'appelle une Render Feature.
À ce moment c'est la panique dans la jam : quoi ? Implémenter une ScriptableRendererFeature
qui va lui-même nécessiter d'implémenter une ScriptableRenderPass
?
Mais heureusement, on peut trouver directement dans l'interface un petit raccourci, voilà comment ça fonctionne :
Configurer le render pipeline
Avec URP, la pipeline de rendu est configurée via deux objets de configuration qui sont par défaut dans le dossier Assets/Settings
:
- Un Universal Render Pipeline Asset
- Un Renderer2DData (pour un projet 2D mais il y a l'équivalent en 3D aussi)
Dans ce deuxième fichier, on ajoute un FullScreenPassRendererFeature en appuyant sur le bouton dédié "Add Renderer Feature" en bas de l'inspecteur.
On utilise la configuration suivante :
- Name : comme vous voulez (ici j'ai gardé la valeur par défaut)
- InjectionPoint : par défaut "After Rendering Post Processing", mais pour notre jeu ça ne fonctionnait pas car on rajoute un bloom en post effect, et le bloom déformé rend très mal ! Donc pour cet effet je recommande une injection "Before rendering Post Processing" pour que les autres post effects soient appliqués sur l'image déformée.
- Fetch Color Buffer : coché ! Ça rajoute une passe de copie mais on a besoin des couleurs initiales pour faire notre effet de déformation.
- Bind Depth-Stencil : décoché. Le depth stencil contrôle la profondeur et le masquage des pixels lors du rendu 3D. Pour notre rendu 2D, on ne l'utilisera pas ici.
- Pass Material : l'instance du material du Shader de Post Effect.
Voyons voir comment créer ce shader et son material !
Le Shader de post effect
Initialisation du shader
Pour créer le shader, dans la vue projet : clic droit > Create > Shader Graph > URP > Fullscreen Shader Graph. Ça permettra de créer un shader préconfiguré pour un effet de post effect full screen.
Une fois dans le shader graph, on a le point de sortie (Le fragment Base Color) mais le point d'entrée n'est pas évident. On le trouve dans la doc officielle : https://docs.unity3d.com/Packages/com.unity.render-pipelines.universal@17.0/manual/post-processing/post-processing-custom-effect-low-code.html
Comme indiqué, on crée un nœud URP Sample Buffer
configuré avec la source BlitSource
.
Cette configuration nous donne un Post Effect qui ne fait que copier les pixels d'origine. Sur le nœud URP Sample Buffer
la source d'information du pixel est obtenue à partir d'une valeur UV qui par défaut est un UV screenspace ((0,0) en bas à gauche de l'écran et (1,1) en haut à droite).
Effet de décalage
Modifier cet UV nous permettra de choisir pour chaque pixel d'aller piocher le pixel source en décalé pour créer l'effet voulu. Pour les projectiles, une texture "rapetissant" va concentrer la lecture des pixels vers le milieu et au contraire pour l'explosion on repoussera la lecture vers l'extérieur.
On partira des conventions suivantes pour la texture d'information :
- Les canaux R et G contiendront les informations d'offset sur les axes X et Y respectivement.
- Comme les couleurs négatives n'existent pas, on normalisera comme pour un normal map :
0.5
est la valeur neutre, en dessous on décale dans le négatif, au-dessus on décale dans le positif.
Les images ci-dessous représentent les textures de déformation du projectile et de l'effet d'explosion. Pour le projectile par exemple, si on se concentre uniquement sur la valeur rouge, on voit qu'en partant du bord gauche la couleur est neutre (rouge 0.5) puis transitionne de plus en plus fort vers le noir lorsqu'on approche du centre (où le vert qui reste seul puisqu'il n'y a plus de rouge), et de l'autre côté du cercle l'inverse se produit on passe rapidement d'un rouge intense proche du centre (1) à un rouge neutre (0.5) en allant vers le bord droit.
L'intensité de noir/rouge au niveau du centre indique que l'effet sera le plus fort au près du projectile, et donc plus léger près du bord. Plus l'intensité est forte (noire ou rouge) et plus on ira chercher loin le pixel à lire dans la texture source vers la gauche ou vers la droite. Cela va ici créer un effet rétrécissant autour des particules en allant chercher les pixels plus loin autour.
Pour l'effet grossissant au contraire, on ira chercher à afficher des pixels plus près du centre ce qui repoussera et étirera les pixels de la texture source du centre vers l'extérieur.
Programmation du shader
On monte le shader ainsi :
- Un paramètre _Secondary de type texture qui sera notre source d'information. À terme cette texture contiendra uniquement des infos rouge et vertes comme montré ci-dessus.
- Un nœud SampleTexture2D pour lire les informations de cette texture
- On soustrait 0.5 à chaque canal source pour obtenir une valeur entre -0.5 et 0.5
- On ajoute un nœud UV (channel UV0) comme source d'UV
- On additionne le résultat de la soustraction à l'UV pour obtenir l'UV décalé
- On utilise l'UV décalé (résultat du Add) comme source d'UV de l'URP sample buffer
- On pourra introduire un paramètre float DistortionStrength pour configurer l'amplitude du décalage, on le multiplie avec le résultat de la soustraction avant de l'additionner à l'UV
Ci-dessous le graph complet qui comporte une opération supplémentaire : si l'image source est très proche du noir (Add(R+G) → Step(seuil de 0.1)
) on considère qu'on n'a pas d'information de décalage et on annule l'offset (multiplication par 0 qui annule tout offset).
Configuration du material
On peut maintenant configurer le material du shader dans le Renderer2DData :
- Pour créer le material, dans la vue Project, clic droit sur le shader > Create > Material. Cela crée un matériau attaché au shader automatiquement.
- On attache le material dans l'inspecteur du Renderer2DData
- On configure le material
Ici je teste avec une texture orange (rouge=1, vert=0.5), toute l'image est décalée vers la gauche lorsqu'on augmente la distorsion strength.
- La valeur du canal rouge est proche de 1 ce qui décale la lecture plus à droite. La coordonnée UV est décalée plus à droite, le pixel échantillonné par le nœud
Sample Buffer
est donc plus à droite dans la source ce qui décale l'image rendue vers la gauche et laisse une bande noire sur la droite (il n'y a plus de pixel à lire lorsqu'on sort de la zone de rendu de la texture source ! → rendu noir). - La valeur du canal vert est proche de 0.5, pas de changement vertical puisqu'on n'applique aucun décalage à l'UV d'origine
Il reste à générer cette texture de déformation pour obtenir quelque chose de plus intéressant !
Générer la texture de déformation
Voici les étapes du montage :
- Créer une asset RenderTexture (ici nommée "Camera Output") qui respecte dans ses dimensions le ratio cible en R8G8_UNORM
- Créer un layer dédié (ici nommé "Secondary")
- Créer une caméra secondaire qui filme uniquement secondary et ignore le reste (culling mask)
- Il faudra pour cette caméra créer un Renderer2DData (nommé par ex. Renderer2DSecondary) inspiré du premier Renderer2DData qu'on avait précédemment mais qui désactive tous les posts effects
- Ajouter ce renderer2DData à la renderer list du Universal Render Pipeline Asset
- Utiliser ce renderer au niveau de la caméra secondaire
- Configurer l'output pour utiliser la RenderTexture créée précédemment
- Configurer la caméra principale pour ignorer secondary (culling mask)
- Ajouter un sprite de déformation aux projectiles sur le layer "Secondary"
- Projectile est un gameobject vide qui sert de parent aux éléments
- SpriteRenderer est un Sprite Renderer, le rendu normal du sprite sur le layer Default
- ProjectileMask est un Sprite Renderer du même sprite mais complètement noir sur le layer Secondary. En s'affichant par-dessus Shrink, il permet d'éviter que l'effet de déformation ne soit appliqué au projectile lui-même (sinon il apparaîtrait tout rapetissé).
- Shrink est un Sprite Renderer de l'effet qui est rendu sur le layer Secondary. C'est l'effet de déformation. Il est environ 3 fois plus large que le projectile lui-même pour que la zone d'effet soit autour du projectile.
Dans le cadre de notre jeu Powered By Geometry, j'ai également programmé un shader additionnel pour générer la texture d'UV déformé qu'on peut observer ci-dessus mais il est également possible d'utiliser un sprite pré-calculé.
Pour l'effet d'explosion, j'utilise un émetteur de particules configuré sur le layer Secondary et qui anime l'agrandissement puis le fadeout d'une unique particule de grande taille, similaire à celle des projectiles sinon que les couleurs sont inversées, pour donner un effet qui repousse plutôt qu'un effet convergeant.
Finalisation
Tout est prêt ! La dernière étape consiste à renseigner cette RenderTexture (Camera Output) comme valeur _Secondary du material créé précédemment.
Lorsqu'il y a des projectiles à l'écran ça peut ressembler à ça :
Et pour un aperçu du rendu final lorsqu'il y a plein de particules partout à l'écran, pourquoi ne pas tester le jeu ? Il est disponible gratuitement et devrait tourner de manière performante dans votre navigateur grâce à l'export WebGPU :
https://itooh.itch.io/powered-by-geometry
Vous lisez peut-être cet article depuis un mobile, voici donc un petit extrait de l'effet ondulatoire qu'on devine malgré les artefacts de compression, et qui donne un petit effet aquatique à la scène !
Merci d'avoir lu jusqu'au bout ! Si ce contenu vous a plu, pensez à me suivre sur vos réseaux sociaux préférés :