Cet effet a été inspiré par l' 
épisode de Powerpuff Girls . Je voulais créer l'effet de la propagation de la couleur dans un monde en noir et blanc, mais l' 
implémenter dans les coordonnées de l'espace mondial , pour voir comment la 
couleur peint les objets , et pas seulement s'étaler à plat sur l'écran, comme dans un dessin animé.
J'ai créé l'effet dans le nouveau 
pipeline de rendu léger du moteur Unity, un exemple intégré du pipeline de rendu scriptable. Tous les concepts s'appliquent à d'autres pipelines, mais certaines fonctions ou matrices intégrées peuvent avoir des noms différents. J'ai également utilisé la nouvelle pile de post-traitement, mais dans le didacticiel, j'omettre une description détaillée de ses paramètres, car elle est assez bien décrite dans d'autres manuels, par exemple, dans 
cette vidéo .
L'effet du post-traitement en niveaux de gris
Juste pour référence, voici à quoi ressemble une scène sans effets de post-traitement.
Pour cet effet, j'ai utilisé le nouveau package de post-traitement Unity 2018, qui peut être téléchargé depuis le gestionnaire de packages. Si vous ne savez pas comment l'utiliser, alors je recommande 
ce tutoriel .
J'ai écrit mon propre effet en étendant les classes PostProcessingEffectSettings et PostProcessEffectRenderer écrites en C #, dont le code source peut être vu 
ici . En fait, je n'ai rien fait de particulièrement intéressant avec ces effets côté CPU (en code C #) sauf que j'ai ajouté un groupe de propriétés générales à l'inspecteur, donc je ne vais pas expliquer comment faire cela dans le tutoriel. J'espère que mon code parle de lui-même.
Passons au code de shader et commençons par l'effet de niveaux de gris. Dans le tutoriel, nous ne modifierons pas le fichier shaderlab, les structures d'entrée et le vertex shader, vous pouvez donc voir leur code source 
ici . Au lieu de cela, nous nous occuperons du fragment shader.
Pour convertir une couleur en niveaux de gris, nous 
réduisons la valeur de chaque pixel à une valeur de luminance qui décrit sa 
luminosité . Cela peut être fait en prenant le produit scalaire de 
la valeur de couleur de la texture de la caméra et du 
vecteur pondéré , qui décrit la contribution de chaque canal de couleur à la luminosité globale des couleurs.
Pourquoi utilisons-nous un produit scalaire? N'oubliez pas que les produits scalaires sont calculés comme suit:
dot(a, b) = a x * b x + a y * b y + a z * b zDans ce cas, nous multiplions chaque canal de 
la valeur de couleur par le 
poids . Ensuite, nous ajoutons ces produits pour les réduire à une seule valeur scalaire. Lorsque la couleur RVB a les mêmes valeurs dans les canaux R, G et B, la couleur devient grise.
Voici à quoi ressemble le code du shader:
 float4 fullColor = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.screenPos); float3 weight = float3(0.299, 0.587, 0.114); float luminance = dot(fullColor.rgb, weight); float3 greyscale = luminance.xxx; return float4(greyscale, 1.0); 
Si le shader de base est correctement configuré, l'effet de post-traitement doit colorer tout l'écran en niveaux de gris.
Effet de rendu des couleurs dans l'espace mondial
Comme il s'agit d'un effet de post-traitement, 
nous n'avons aucune information sur la géométrie de la scène dans le vertex shader. Au stade du post-traitement, les seules informations dont nous disposons sont l' 
image rendue par la caméra et l' 
espace des coordonnées tronquées pour l'échantillonner. Cependant, nous voulons que l'effet de coloration se répande sur les objets, comme s'il se produisait dans le monde, et pas seulement sur un écran plat.
Pour dessiner cet effet dans la géométrie de la scène, nous avons besoin des 
coordonnées de l'espace monde de chaque pixel. Pour passer des 
coordonnées de l'espace des coordonnées tronquées aux 
coordonnées de l'espace mondial , nous devons effectuer une 
transformation de l'espace des coordonnées .
Habituellement, pour passer d'un espace de coordonnées à un autre, une matrice est nécessaire qui définit la transformation de l'espace de coordonnées A vers l'espace B.Pour passer de A à B, nous multiplions le vecteur dans l'espace de coordonnées A par cette matrice de transformation. Dans notre cas, nous allons effectuer la transition suivante: l' 
espace des coordonnées tronquées (espace clip) -> l' 
espace vue (espace vue) -> l' 
espace monde (espace monde) . Autrement dit, nous avons besoin de la matrice clip-to-view-space et de la matrice view-to-world-space fournies par Unity.
Cependant, les 
coordonnées Unity de l'espace de coordonnées tronqué n'ont pas de valeur z qui détermine la profondeur du pixel ou la distance à la caméra. Nous avons besoin de cette valeur pour passer de l'espace des coordonnées tronquées à l'espace des espèces. Commençons par ça!
Obtention de la valeur du tampon de profondeur
Si le pipeline de rendu est activé, il dessine une texture dans la 
fenêtre qui stocke 
les valeurs z dans une structure appelée 
tampon de profondeur . Nous pouvons échantillonner ce tampon pour obtenir la 
valeur z manquante 
de notre espace de coordonnées de coordonnées tronquées!
Tout d'abord, assurez-vous que le 
tampon de profondeur est réellement rendu en cliquant sur la section «Ajouter des données supplémentaires» de la caméra dans l'inspecteur et en vérifiant que la case «Texture de profondeur requise» est cochée. Assurez-vous également que l'option Autoriser MSAA est activée pour la caméra. Je ne sais pas pourquoi cet effet doit être vérifié, mais ça l'est. Si le tampon de profondeur est dessiné, alors dans le 
débogueur de trame, vous devriez voir l'étape 
«Depth Prepass» .
Créer un échantillonneur _CameraDepthTexture dans le 
fichier hlsl TEXTURE2D_SAMPLER2D(_CameraDepthTexture, sampler_CameraDepthTexture); 
Écrivons maintenant la fonction GetWorldFromViewPosition et pour l'instant nous allons l'utiliser pour vérifier 
le tampon de profondeur . (Plus tard, nous allons l'étendre pour obtenir une position dans le monde.)
 float3 GetWorldFromViewPosition (VertexOutput i) { float z = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_CameraDepthTexture, i.screenPos).r; return z.xxx; } 
Dans le fragment shader, tracez la valeur de l'échantillon de texture de profondeur.
 float3 depth = GetWorldFromViewPosition(i); return float4(depth, 1.0); 
Voici à quoi ressemblent mes résultats quand il n'y a qu'une seule plaine vallonnée dans la scène (j'ai désactivé tous les arbres afin de simplifier davantage le test des valeurs de l'espace mondial). Votre résultat devrait ressembler. Les valeurs en noir et blanc décrivent la distance entre la géométrie et la caméra.
Voici quelques étapes à suivre si vous rencontrez des problèmes:
- Assurez-vous que le rendu en profondeur de la caméra est activé.
- Assurez-vous que la caméra a activé MSAA.
- Essayez de changer le plan proche et éloigné de la caméra.
- Assurez-vous que les objets que vous prévoyez de voir dans le tampon de profondeur utilisent un shader avec une passe de profondeur. Cela garantit que l'objet dessine dans le tampon de profondeur. Tous les shaders standard de LWRP le font.
Obtenir de la valeur dans l'espace mondial
Maintenant que nous avons toutes les informations nécessaires pour l' 
espace des coordonnées tronquées , passons à l' 
espace des espèces , puis à l' 
espace du 
monde .
Notez que les matrices de transformation requises pour ces opérations sont déjà dans la bibliothèque SRP. Cependant, ils sont contenus dans la bibliothèque C # du moteur Unity, je les ai donc insérés dans le shader de la fonction Render du script 
ColorSpreadRenderer :
 sheet.properties.SetMatrix("unity_ViewToWorldMatrix", context.camera.cameraToWorldMatrix); sheet.properties.SetMatrix("unity_InverseProjectionMatrix", projectionMatrix.inverse); 
Étendons maintenant notre fonction GetWorldFromViewPosition.
Tout d'abord, nous devons obtenir la position dans la fenêtre en 
multipliant la position dans l'espace de coordonnées tronqué par InverseProjectionMatrix . Nous devons également faire un peu plus de magie vaudou avec une position à l'écran, qui est liée à la façon dont Unity stocke sa position dans l'espace des coordonnées tronquées.
Enfin, nous pouvons 
multiplier la position dans la fenêtre d'affichage par ViewToWorldMatrix pour obtenir la position dans l' 
espace mondial .
 float3 GetWorldFromViewPosition (VertexOutput i) {  
Faisons une vérification pour nous assurer que les positions dans l'espace global sont correctes. Pour ce faire, j'ai écrit un 
shader qui ne renvoie que la position d'un objet dans l' 
espace mondial ; il s'agit d'un calcul assez simple basé sur un shader régulier, dont l'exactitude peut être fiable. Désactivez l'effet du post-traitement et prenez une capture d'écran de ce shader de test pour l' 
espace mondial . Mon après avoir appliqué le shader à la surface de la terre dans la scène ressemble à ceci:
(Notez que les valeurs dans l'espace mondial sont beaucoup plus grandes que 1,0, alors ne vous inquiétez pas que ces couleurs aient un sens; au lieu de cela, assurez-vous simplement que les résultats sont les mêmes pour les réponses «vraies» et «calculées».) Ensuite, revenons au test l'objet est un matériau ordinaire (et non le matériau de test de l'espace mondial), puis réactivez l'effet de post-traitement. Mes résultats ressemblent à ceci:
Ceci est complètement similaire au shader de test que j'ai écrit, c'est-à-dire que les calculs de l'espace mondial sont probablement corrects!
Dessiner un cercle dans l'espace mondial
Maintenant que nous avons des 
positions dans l'espace mondial , nous pouvons dessiner un cercle de couleur dans la scène! Nous devons définir le 
rayon dans lequel l'effet dessinera la couleur. À l'extérieur, l'effet rendra l'image en niveaux de gris. Pour le définir, vous devez ajuster les valeurs 
du rayon d' effet ( 
_MaxSize ) et du centre du cercle (_Center). J'ai défini ces valeurs dans la classe C # 
ColorSpread afin qu'elles soient visibles dans l'inspecteur. Développons notre fragment shader en le forçant 
à vérifier si le pixel actuel se trouve dans le rayon du cercle :
 float4 Frag(VertexOutput i) : SV_Target { float3 worldPos = GetWorldFromViewPosition(i);  
Enfin, nous pouvons dessiner la couleur selon qu'elle se trouve à l'intérieur d'un 
rayon dans l' 
espace mondial . Voilà à quoi ressemble l'effet de base!
Ajout d'effets spéciaux
J'examinerai quelques autres techniques utilisées pour répartir la couleur sur le sol. Il y a beaucoup plus pour le plein effet, mais le tutoriel est déjà devenu trop volumineux, nous allons donc nous limiter au plus important.
Animation d'agrandissement du cercle
Nous voulons que l'effet se répande dans le monde entier, c'est-à-dire comme s'il grandissait. Pour ce faire, vous devez modifier le 
rayon en fonction de l'heure.
_StartTime indique l'heure à laquelle le cercle devrait commencer à se développer. Dans mon projet, j'ai utilisé un script supplémentaire qui vous permet de cliquer n'importe où sur l'écran pour démarrer la croissance du cercle; dans ce cas, l'heure de début est égale à l'heure à laquelle la souris a été cliquée.
_GrowthSpeed définit la vitesse d'augmentation du cercle.
 
Nous devons également mettre à jour le contrôle de distance pour comparer la distance actuelle avec le 
rayon croissant 
de l'effet , et non avec _MaxSize.
 
Voici à quoi devrait ressembler le résultat:
Ajout au rayon de bruit
Je voulais que l'effet ressemble plus à un flou de peinture, pas seulement à un cercle en pleine croissance. Pour ce faire, 
ajoutons du bruit au rayon de l'effet afin que la distribution soit inégale.
Nous devons d'abord échantillonner la texture dans l' 
espace mondial . Les coordonnées UV de i.screenPos sont situées dans l' 
espace de l' 
écran , et si nous échantillonnons en fonction d'eux, la forme de l'effet se déplacera avec la caméra; utilisons donc les coordonnées dans l' 
espace mondial . J'ai ajouté le paramètre 
_NoiseTexScale pour contrôler l' 
échelle de l'échantillon de texture de bruit , car les coordonnées dans l'espace mondial sont assez grandes.
 
Échantillons maintenant la texture du bruit et ajoutons cette valeur au rayon de l'effet. J'ai utilisé l'échelle _NoiseSize pour plus de contrôle sur la taille du bruit.
 
Voici à quoi ressemblent les résultats après quelques ajustements:
En conclusion
Vous pouvez suivre les mises à jour des tutoriels sur mon 
Twitter , et sur 
Twitch je passe des streams de codage! (De plus, je diffuse des jeux de temps en temps, alors ne soyez pas surpris si vous me voyez assis en pyjama et jouant à Kingdom Hearts 3.)
Remerciements: