Pour ce tutoriel, j'utilise Unity 6. Notez que tout ce qu'on va faire ici devrait fonctionner sur d'anciennes versions, car nous n'allons pas utiliser de fonctionalité spécifique à Unity 6. Cependant, certaines choses sont peut-être nommées différemment, notamment ce qui concerne les RendererFeatures.
Car oui, j'utilise l'URP, pour avoir accès aux RendererFeatures, qui vont être utiles pour l'effet papier granuleux. Ce tutoriel est basé sur la création de shaders en HLSL. Donc si vous n'êtes pas à l'aise avec ça, je vous encourage à suivre attentivement ce tutoriel pour bien comprendre la logique derrière ! Je ne vais pas tout expliquer à propos des shaders ici, mais je vais essayer d'être le plus clair possible sur les étapes pour atteindre cette effet aquarelle.
Commençons par mettre en place une scene dans Unity:
- ajoutez un plan (GameObject > 3D Object > Plane)
- créez un shader nommé "watercolor" (Assets > Create > Shader > Unlit shader)
- créez un material (right-click on the watercolor shader > Create > Material)
- appliquez le material au plan.
Ouvrons maintenant le shader, et remplacez son contenu par ceci:
Shader "Watercolor" { Properties // every public shader properties shown in the Inspector { _Color("Color", Color) = (1,1,1,1) } SubShader { Tags { "Queue" = "Transparent" "RenderType" = "Transparent" } Pass // Define the shader pass { Blend SrcAlpha OneMinusSrcAlpha // Use common transparency HLSLPROGRAM // the HLSL code starts here #pragma vertex vert // define vertex function (called per vertex) #pragma fragment frag // define fragment function (called per fragment/pixel) // include Unity URP core functions #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" // data received from the GPU workflow struct appdata { float3 posOS : POSITION; // vertex position in object space UNITY_VERTEX_INPUT_INSTANCE_ID }; // data we send from the vertex shader to the fragment shader struct v2f { float4 posCS : SV_POSITION; // fragment position in clip space UNITY_VERTEX_INPUT_INSTANCE_ID UNITY_VERTEX_OUTPUT_STEREO }; CBUFFER_START(UnityPerMaterial) half4 _Color; CBUFFER_END v2f vert(appdata _input) { v2f output; UNITY_SETUP_INSTANCE_ID(_input); UNITY_TRANSFER_INSTANCE_ID(_input, output); UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output); // use matrices to transform the vertex position from object space to clip space output.posCS = TransformObjectToHClip(_input.posOS); return output; } half4 frag(v2f _input) : SV_Target { UNITY_SETUP_INSTANCE_ID(_input); UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(_input); return _Color; // just returning the defined color } ENDHLSL // end of the HLSL code } } }
Ce shader est très basique et permet seulement d'appliquer une couleur au material. Une fois de plus, je ne vais pas expliquer pleinement son fonctionnement, mais j'ai ajouté des commentaires pour aider.
Maintenant, nous pouvons créer notre shader. Je me suis inspiré de l'incroyable tutoriel de Kai sur Youtube.
Voici les étapes que nous allons traverser :
- nous allons implémenter le pattern Voronoi pour avoir des variations de couleur à la surface,
- on appliquera un lissage (smoothness) au résultat du pattern Voronoi pour adoucir les variations de couleur,
- on utilisera un rayon pour créer une tâche sur notre surface.
Commençons par la première étape. Pour obtenir l'algorithme du Voronoi pattern, j'ai simplement vérifié le code du node de Voronoi de Unity. Si vous souhaitez plus d'informations sur le Voronoi, n'hésitez pas à consulter la page Wikipedia.
Quoi qu'il en soit, voici le résultat que l'on peut intégrer au shader, au-dessus de la fonction frag.
inline float2 GetVoronoiRandomVector(float2 _uv, float _offset) { float2x2 m = float2x2(15.27, 47.63, 99.41, 89.98); _uv = frac(sin(mul(_uv, m)) * 46839.32); return float2(sin(_uv.y * _offset) * 0.5 + 0.5, cos(_uv.x * _offset) * 0.5 + 0.5); } void Voronoi_float(float2 _uv, float _angleOffset, float _cellDensity, out float _noise, out float _cells) { float2 g = floor(_uv * _cellDensity); float2 f = frac(_uv * _cellDensity); float3 res = float3(8.0, 0.0, 0.0); for (int y = -1; y <= 1; y++) { for (int x = -1; x <= 1; x++) { float2 lattice = float2(x, y); float2 offset = GetVoronoiRandomVector(lattice + g, _angleOffset); float d = distance(lattice + offset, f); if (d < res.x) { res = float3(d, offset.x, offset.y); _noise = res.x; _cells = res.y; } } } }
Comme vous pouvez le voir, il y a deux fonctions : 'GetVoronoiRandomVector' et 'Voronoi_float'. GetVoronoiRandomVector est appelé par Voronoi_float, et c'est bien Voronoi_float que nous allons appeler dans notre fonction frag pour appliquer le pattern Voronoi.
La fonction Voronoi_float a plusieurs paramètres :
- un float2 '_uv', un float '_angleOffset' et un float '_cellDensity' que nous devons fournir,
- un float '_noise' et un float '_cells' qui contiennent le résultat.
Pour commencer, nous allons fournir les UV de l'objet. Pour ce faire, nous avons besoin de récupérer les UV à partir de la structure appdata. Et nous pourrons les rendre utilisables dans le fragment shader (notre fonction frag) à travers la structure v2f.
struct appdata { float3 posOS : POSITION; float2 uv : TEXCOORD0; // TEXCOORD0 contains the object UV UNITY_VERTEX_INPUT_INSTANCE_ID };
struct v2f { float4 posCS : SV_POSITION; float2 uv : TEXCOORD0; // so we'll use the UV during fragment shader UNITY_VERTEX_INPUT_INSTANCE_ID UNITY_VERTEX_OUTPUT_STEREO };
Dans la fonction vert, nous devons envoyer la valeur d'UV dans l'instance v2f "ouput".
v2f vert(appdata _input) { v2f output; UNITY_SETUP_INSTANCE_ID(_input); UNITY_TRANSFER_INSTANCE_ID(_input, output); UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output); output.posCS = TransformObjectToHClip(_input.posOS); output.uv = _input.uv; // as simple as that! return output; }
Ensuite, nous allons fournir un "angle offset" et la "cell density" à travers deux paramètres. Nous les déclarons dans la section Properties, pour être en mesure de pouvoir définir leur valeur dans l'Inspector de Unity. Et nous les déclarons également dans la code HLSL pour les utiliser dans la fonction frag.
Properties { _Color("Color", Color) = (1,1,1,1) _AngleOffset("Angle offset", Float) = 1.0 _CellDensity("Cell density", Float) = 1.0 }
CBUFFER_START(UnityPerMaterial) half4 _Color; half _AngleOffset; half _CellDensity; CBUFFER_END
Maintenant, nous avons tout à notre disposition pour la fonction de Voronoi. Utilisons-là !
half4 frag(v2f _input) : SV_Target { UNITY_SETUP_INSTANCE_ID(_input); UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(_input); float noise = 0.0, cells = 0.0; // we declare values to store voronoi results Voronoi_float(_input.uv, _AngleOffset, _CellDensity, noise, cells); // Voronoi's magic float4 color = _Color; color *= noise; // we multiply the color with the voronoi noise return color; }
Retour sur Unity. Vous pouvez selectionner le material "Watercolor", appliquer - par exemple - une Cell density de 5, et jouer avec la valeur d'Angle offset pour voir le résultat.
Pour le moment, nous ne souhaitons pas avoir de transparence. Nous en avons car nous multiplions l'alpha de la couleur par le resultat du Voronoi pattern. Pour empêcher ça, nous pouvons explicitement demander de ne multiplier que les valeurs rgb de la couleur.
color.rgb *= noise; // we multiply the color with the voronoi noise
Egalement, pour notre effet, nous n'avons pas besoin du noise, mais des cells du Voronoi pattern. La valeur de cells est comprise entre 0 et 1, nous pouvons donc ajouter 0.5, pour obtenir une valeur entre 0.5 et 1.5, pour obtenir une variation de couleur proportionnellement autour de la valeur donnée dans le material, plutôt que des couleurs uniquement plus sombres.
color.rgb *= cells + 0.5;
Maintenant, nouvelle étape : nous devons lisser les cells du Voronoi pattern, pour simuler la variation de couleur correctement. Dans son tutoriel, Kai active simplement l'option "smooth" sur le node Voronoi de Blender, ce que nous n'avons évidemment pas sous la main. J'ai donc vérifié directement le code source sur le git de Blender pour implementer la bonne logique dans notre fonction Voronoi_float. Voici le résultat:
void Voronoi_float(float2 _uv, float _angleOffset, float _cellDensity, float _smoothness, out float _noise, out float _cells) { float2 g = floor(_uv * _cellDensity); float2 f = frac(_uv * _cellDensity); //float3 res = float3(8.0, 0.0, 0.0); float smoothDistance = 0.0; _cells = 0.0; _noise = 0.0; float h = -1.0; for (int y = -1; y <= 1; y++) { for (int x = -1; x <= 1; x++) { float2 lattice = float2(x, y); float2 offset = GetVoronoiRandomVector(lattice + g, _angleOffset); float d = distance(lattice + offset, f); // if (d < res.x) // { // res = float3(d, offset.x, offset.y); // _noise = res.x; // _cells = res.y; // } h = h == -1.0f ? 1.0 : smoothstep(0.0, 1.0, 0.5 + 0.5 * (smoothDistance - d) / _smoothness); float correctionFactor = _smoothness * h * (1.0 - h); smoothDistance = lerp(smoothDistance, d, h) - correctionFactor; correctionFactor /= 1.0 + 3.0 * _smoothness; float cell = offset.x; _cells = lerp(_cells, cell, h) - correctionFactor; float pos = d; _noise = lerp(_noise, pos, h) - correctionFactor; } } }
Comme vous pouvez le voir, nous avons un paramètre "_smoothness" à founir. Ajoutons donc un paramètre _Smoothness à notre shader.
Properties { _Color("Color", Color) = (1,1,1,1) _AngleOffset("Angle offset", Float) = 1.0 _CellDensity("Cell density", Float) = 1.0 _Smoothness("Smoothness", Range(0,1)) = 0.5 }
On le déclare également dans le code HLSL.
CBUFFER_START(UnityPerMaterial) half4 _Color; half _AngleOffset; half _CellDensity; half _Smoothness; CBUFFER_END
Et enfin, on le fournit à la fonction Voronoi_float, dans la fonction frag.
Voronoi_float(_input.uv, _AngleOffset, _CellDensity, _Smoothness, noise, cells); // Voronoi's magic
Nos cellules sont maintenant lissées. On peut également ajouter une variable _Power pour contrôler la puissance de variation des couleurs. Le rituel reste le même : ajouter une propriété _Power, la déclarer dans le code HLSL, puis l'utiliser dans la fonction frag.
Properties { _Color("Color", Color) = (1,1,1,1) _AngleOffset("Angle offset", Float) = 1.0 _CellDensity("Cell density", Float) = 1.0 _Smoothness("Smoothness", Range(0,1)) = 0.5 _Power("Power", Range(0,5)) = 1.0 }
CBUFFER_START(UnityPerMaterial) half4 _Color; half _AngleOffset; half _CellDensity; half _Smoothness; half _Power; CBUFFER_END
float4 color = _Color; cells = pow(cells + 0.5, _Power); // apply power color.rgb *= cells; // we multiply the color with the voronoi noise return color;
Grâce à cela, nous pouvons jouer avec Smoothness et Power du material pour obtenir la variation de couleur que l'on recherche (et n'oubliez pas que vous pouvez également jouer avec la Cell density pour changer la taille des cellules, ou avec l'Angle offset pour changer la forme des cellules).
Pour l'effet de tâche, nous avons besoin d'un centre et d'un rayon.
Properties { _Color("Color", Color) = (1,1,1,1) _AngleOffset("Angle offset", Float) = 1.0 _CellDensity("Cell density", Float) = 1.0 _Smoothness("Smoothness", Range(0,1)) = 0.5 _Power("Power", Range(0, 5)) = 1.0 _Center("Stain center", Vector) = (0,0,0,0) // ready for _Radius("Stain radius", Float) = 1.0 // action! }
Comme à chaque fois avec les shaders, il y a plusieurs manières de faire les choses. Les choix dépendent de nos besoins. Ici, nous avons un plan avec l'axe Y pour normale. Donc, plutôt que d'utiliser les UV de l'objet, nous pouvons utiliser la position en X et en Z de l'objet dans le monde (world space).
Pour ce faire, nous devons enregistrer la position dans le monde dans la structure v2f, pour l'utiliser dans le fragment shader (= dans notre fonction frag). D'abord, on ajoute posWS dans la structure v2f.
struct v2f { float4 posCS : SV_POSITION; float2 uv : TEXCOORD0; float3 posWS : VAR_POS; UNITY_VERTEX_INPUT_INSTANCE_ID UNITY_VERTEX_OUTPUT_STEREO };
Ensuite, ne devons affecter la variable pendant le vertex shader (= notre fonction vert).
Au lieu d'utiliser la fonction TransformObjectToHClip, qui nous donne directement la position sur l'écran (clip space) à partir de la position du vertex dans l'objet (object space), nous utiliserons :
- la fonction TransformObjectToWorld pour obtenir la position du vertex dans le monde (world space),
- puis la fonction TransformWorldToHClip pour obtenir la position du vertex sur l'écran.
Notez que nous pourrions simplement utiliser la fonction TransformObjectToWorld à côté de la fonction TransformObjectToHClip. Mais comme TransformObjectToHClip est grosso-modo un mix de TransformObjectToWorld + TransformWorldToHClip, il est meilleur pour les performances de procéder ainsi :
// output.posCS = TransformObjectToHClip(_input.posOS); output.posWS = TransformObjectToWorld(_input.posOS); output.posCS = TransformWorldToHClip(output.posWS); output.uv = _input.uv; return output;
Nous pouvons à présent utiliser posWS dans la fonction frag. Nous devons vérifier la position du fragment/pixel dessiné dans le monde, et obtenir sa distance par rapport au _Center. Si la distance est supérieure à la valeur de _Radius, alors nous sommes en dehors de la tâche, et nous mettons l'alpha à 0.
D'abord, nous devons déclarer _Center et _Radius dans le code HLSL.
CBUFFER_START(UnityPerMaterial) half4 _Color; half _AngleOffset; half _CellDensity; half _Smoothness; half _Power; half2 _Center; // only half2, because we are on a plane here half _Radius; CBUFFER_END
Ajoutons la logique pour obtenir une tâche ! (consultez les commentaires pour quelques explications supplémentaires)
float4 color = _Color; cells = pow(cells + 0.5, _Power); // apply power color.rgb *= cells; // we multiply the color with the voronoi noise float2 worldPos = _input.posWS.xz; // we only use X,Z axes // we get the length of the vector from pos to center = distance from center float distFromCenter = length(_input.posWS.xz - _Center); // step(x,y) returns 1 if y > x, so inStain = 1 if _Radius > distFromCenter // and inStain = 0 if the distance from center is bigger than the wanted radius half inStain = step(distFromCenter, _Radius); color.a = inStain; return color;
Faites bien attention à ce que votre plan soit bien placé en (0;0;0) et vous obtiendrez ceci en jouant avec la valeur de Stain radius.
Nous avons presque terminé. Nous avons juste besoin d'ajouter du bruit pour que ça ressemble plus à une vraie tâche ! Nous pouvons utiliser cette texture libre et seemless que j'ai éditée.
Téléchargez-là, importez-là dans Unity, et configurez-là comme une texture "single channel".
Utilisons-là dans notre shader. Encore, même rituel : on ajoute une propriété publique, on déclare la variable dans le code HLSL, et on l'utilise dans notre fonction frag. Ce tutoriel est déjà bien assez long, on va donc aussi ajouter deux valeurs : _NoiseScale et _NoiseStrength pour gérer notre effet de bruit.
Properties { _Color("Color", Color) = (1,1,1,1) _AngleOffset("Angle offset", Float) = 1.0 _CellDensity("Cell density", Float) = 1.0 _Smoothness("Smoothness", Range(0,1)) = 0.5 _Power("Power", Range(0, 5)) = 1.0 _Center("Stain center", Vector) = (0,0,0,0) _Radius("Stain radius", Float) = 1.0 _NoiseTex("Noise texture", 2D) = "white" {} // for the texture _NoiseScale("Noise scale", Float) = 1.0 // to manage the texture's scale _NoiseStrength("Noise strength", Float) = 1.0 // strength of the effect }
CBUFFER_START(UnityPerMaterial) half4 _Color; half _AngleOffset; half _CellDensity; half _Smoothness; half _Power; half2 _Center; half _Radius; TEXTURE2D(_NoiseTex); // contains the texture SAMPLER(sampler_NoiseTex); // allows us to sample the texture at given UV half _NoiseScale; half _NoiseStrength; CBUFFER_END
float2 worldPos = _input.posWS.xz; // we only use X,Z axes // sample noise texture // we could use the object UV, but here again, we are using the (X;Z) world pos instead float2 noiseUV = worldPos * _NoiseScale; float stainNoise = SAMPLE_TEXTURE2D(_NoiseTex, sampler_NoiseTex, noiseUV).r; // stainNoise value is in range [0;1], we want it in range [-1;1] stainNoise = stainNoise * 2 - 1; stainNoise *= _NoiseStrength; // finally, apply strength // we get the length of the vector from pos to center = distance from center float distFromCenter = length(_input.posWS.xz - _Center) + stainNoise; // we add the noise here! // step(x,y) returns 1 if y > x, so inStain = 1 if _Radius > distFromCenter // and inStain = 0 if the distance from center is bigger than the wanted radius half inStain = step(distFromCenter, _Radius); color.a = inStain; return color;
Voici la version finale du shader d'aquarelle :
Shader "Watercolor" { Properties { _Color("Color", Color) = (1,1,1,1) _AngleOffset("Angle offset", Float) = 1.0 _CellDensity("Cell density", Float) = 1.0 _Smoothness("Smoothness", Range(0,1)) = 0.5 _Power("Power", Range(0, 5)) = 1.0 _Center("Stain center", Vector) = (0,0,0,0) _Radius("Stain radius", Float) = 1.0 _NoiseTex("Noise texture", 2D) = "white" {} // for the texture _NoiseScale("Noise scale", Float) = 1.0 // to manage the texture's scale _NoiseStrength("Noise strength", Float) = 1.0 // strength of the effect } SubShader { Tags { "Queue" = "Transparent" "RenderType" = "Transparent" } LOD 100 Pass // Color { Blend SrcAlpha OneMinusSrcAlpha HLSLPROGRAM #pragma vertex vert #pragma fragment frag #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" struct appdata { float3 posOS : POSITION; float2 uv : TEXCOORD0; UNITY_VERTEX_INPUT_INSTANCE_ID }; struct v2f { float4 posCS : SV_POSITION; float2 uv : TEXCOORD0; float3 posWS : VAR_POS; UNITY_VERTEX_INPUT_INSTANCE_ID UNITY_VERTEX_OUTPUT_STEREO }; CBUFFER_START(UnityPerMaterial) half4 _Color; half _AngleOffset; half _CellDensity; half _Smoothness; half _Power; half2 _Center; half _Radius; TEXTURE2D(_NoiseTex); // contains the texture SAMPLER(sampler_NoiseTex); // allows us to sample the texture at given UV half _NoiseScale; half _NoiseStrength; CBUFFER_END v2f vert(appdata _input) { v2f output; UNITY_SETUP_INSTANCE_ID(_input); UNITY_TRANSFER_INSTANCE_ID(_input, output); UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output); // output.posCS = TransformObjectToHClip(_input.posOS); output.posWS = TransformObjectToWorld(_input.posOS); output.posCS = TransformWorldToHClip(output.posWS); output.uv = _input.uv; return output; } inline float2 GetVoronoiRandomVector(float2 _uv, float _offset) { float2x2 m = float2x2(15.27, 47.63, 99.41, 89.98); _uv = frac(sin(mul(_uv, m)) * 46839.32); return float2(sin(_uv.y * _offset) * 0.5 + 0.5, cos(_uv.x * _offset) * 0.5 + 0.5); } void Voronoi_float(float2 _uv, float _angleOffset, float _cellDensity, float _smoothness, out float _noise, out float _cells) { float2 g = floor(_uv * _cellDensity); float2 f = frac(_uv * _cellDensity); //float3 res = float3(8.0, 0.0, 0.0); float smoothDistance = 0.0; _cells = 0.0; _noise = 0.0; float h = -1.0; for (int y = -1; y <= 1; y++) { for (int x = -1; x <= 1; x++) { float2 lattice = float2(x, y); float2 offset = GetVoronoiRandomVector(lattice + g, _angleOffset); float d = distance(lattice + offset, f); // if (d < res.x) // { // res = float3(d, offset.x, offset.y); // _noise = res.x; // _cells = res.y; // } h = h == -1.0f ? 1.0 : smoothstep(0.0, 1.0, 0.5 + 0.5 * (smoothDistance - d) / _smoothness); float correctionFactor = _smoothness * h * (1.0 - h); smoothDistance = lerp(smoothDistance, d, h) - correctionFactor; correctionFactor /= 1.0 + 3.0 * _smoothness; float cell = offset.x; _cells = lerp(_cells, cell, h) - correctionFactor; float pos = d; _noise = lerp(_noise, pos, h) - correctionFactor; } } } half4 frag(v2f _input) : SV_Target { UNITY_SETUP_INSTANCE_ID(_input); UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(_input); float noise = 0.0, cells = 0.0; // we declare values to store voronoi results Voronoi_float(_input.uv, _AngleOffset, _CellDensity, _Smoothness, noise, cells); // Voronoi's magic float4 color = _Color; cells = pow(cells + 0.5, _Power); // apply power color.rgb *= cells; // we multiply the color with the voronoi noise float2 worldPos = _input.posWS.xz; // we only use X,Z axes // sample noise texture // we could use the object UV, but here again, we are using the (X;Z) world pos instead float2 noiseUV = worldPos * _NoiseScale; float stainNoise = SAMPLE_TEXTURE2D(_NoiseTex, sampler_NoiseTex, noiseUV).r; // stainNoise value is in range [0;1], we want it in range [-1;1] stainNoise = stainNoise * 2 - 1; stainNoise *= _NoiseStrength; // finally, apply strength // we get the length of the vector from pos to center = distance from center float distFromCenter = length(_input.posWS.xz - _Center) + stainNoise; // we add the noise here! // step(x,y) returns 1 if y > x, so inStain = 1 if _Radius > distFromCenter // and inStain = 0 if the distance from center is bigger than the wanted radius half inStain = step(distFromCenter, _Radius); color.a = inStain; return color; } ENDHLSL } } }
Le dernier mais pas des moindres, nous pouvons simuler un effet papier, en utilisant une RendererFeature fourni par Unity : la FullScreenPassRendererFeature. Selectionnez votre Renderer Data actuellement utilisé par votre Render Pipeline Asset, et ajoutez lui un FullScreenPassRendererFeature.
Cette renderer feature utilise un material qu'il applique sur l'entièreté de l'écran. Par défaut, il utilise un material qui inverse les couleurs.
A la place, on va mettre un material qui ajoute l'effet papier granuleux. Pour ça, nous créons un autre shader, que l'on va appliquer sur un nouveau material.
- créez un shader nommé 'paper' (Assets > Create > Shader > Unlit shader),
- créez un material (right-click on the paper shader > Create > Material)
Ouvrez le shader et remplacez son contenu par ceci :
Shader "Paper" { Properties { _PaperTexture("Texture", 2D) = "white" {} _Power("Power", Range(0.001, 3)) = 1.0 } SubShader { Tags { "RenderType"="Opaque" } Pass { HLSLPROGRAM #pragma vertex vert #pragma fragment frag #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" struct appdata { uint vertexID : SV_VertexID; }; struct v2f { float4 posCS : SV_POSITION; float2 uv : TEXCOORD0; float3 posWS : VAR_POS; }; TEXTURE2D(_BlitTexture); // Screen texture SAMPLER(sampler_BlitTexture); TEXTURE2D(_PaperTexture); // Paper texture SAMPLER(sampler_PaperTexture); half4 _PaperTexture_ST; half _Power; // Effect power v2f vert(appdata _input) { v2f output; output.posCS = GetFullScreenTriangleVertexPosition(_input.vertexID); output.uv = GetFullScreenTriangleTexCoord(_input.vertexID); return output; } half4 frag(v2f _input) : SV_Target { // We get the screen render at this pixel half4 render = SAMPLE_TEXTURE2D(_BlitTexture, sampler_BlitTexture, _input.uv); // We get the paper effect at this pixel half3 paper = SAMPLE_TEXTURE2D(_PaperTexture, sampler_PaperTexture, _input.uv.xy * _PaperTexture_ST.xy + _PaperTexture_ST.zw).rgb; paper = pow(paper, _Power); // We apply the paper effect on the screen render render.rgb *= paper; return render; } ENDHLSL } } }
Ensuite, appliquez ce nouveau material à la renderer feature.
Rien ne se passe, et c'est normal : nous n'avons pas appliqué de texture au material. Ici, j'ai pris une texture libre de droit sur TextureLab. Vous pouvez regarder si vous trouvez quelque chose à votre goût. Pour ma part, j'ai choisi celle-ci.
Téléchargez l'image, importez-là dans Unity, and appliquez-là au champ Texture de votre material paper. Et voilà ! L'effet papier est appliqué !
Ce tutoriel est juste une base solide pour obtenir un chouette effet d'aquarelle. Le résultat peut être amélioré de plein de manières !
- les bordures de la tâche sont un peu abruptes, et peuvent être lissée
- on pourrait utiliser des UV triplanar pour le Voronoi pattern, pour l'appliquer sur autre chose que des surfaces planes
- on pourrait mixer différentes couleurs
- on peut ajouter un effet d'outline
- on peut contrôler les paramètres du material via des scripts, pour faire apparaître ou disparaître la tâche, la changer de couleur...
J'espère que vous avez apprécié ce tutoriel et qu'il vous aura appris de nouvelles choses !
Si vous avez des questions, n'hésitez pas. Vous pouvez me les poser (et me suivre) sur Bluesky !