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 z
Dans 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: