Notifications
Article
Writing a double-sided shader the right way
Updated 4 months ago
770
1
Tutorial on making a flag work with Unity's lighting system.
Let’s say you want to make a nice looking flag. So, you open Unity, create a 3D plane, put the texture in, and… It looks ok, but it “doesn’t exist” from behind. You may already know that’s called backface culling, and while it’s really important for games, it sucks if you’re trying to make a flag.
A bad solution would be to add another plane, flipped, in its back. This will surely work for a static object. But the point of making a flag (or any kind of cloth really) is to have it moving and floating around. So what you really want to do is have a nice shader that allows your flag to be seen from any side, correctly lit and shadowed.

STARTING RIGHT

The first thing we're gonna do is create a new “Standard Surface Shader”. Some people may prefer a fixed-function shader for this particular case, as those are more flexible. But flexibility comes at the cost of a LOT of work: with surface shaders, Unity takes care of all (almost) the PBR lighting and shadowing so you just have to worry about the shader itself.
And they can do almost everything a fixed-function shader can do. Also, if all you want is just the shader, the link is at the end of the post.
For the record, this is the shader I’m starting off with:
Shader "Custom/Double_Sided" { Properties { _Color ("Color", Color) = (1,1,1,1) _MainTex ("Albedo (RGB)", 2D) = "white" {} } SubShader { Tags { "RenderType"="Opaque" } LOD 200 CGINCLUDE // Whatever you write inside here // will be included in every pass struct Input { float2 uv_MainTex; }; fixed4 _Color; sampler2D _MainTex; void surf (Input IN, inout SurfaceOutputStandard o) { fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color; o.Albedo = c.rgb; o.Alpha = c.a; } ENDCG CGPROGRAM // This is the actual pass #pragma surface surf Standard fullforwardshadows #pragma target 3.0 ENDCG } FallBack "Diffuse" }
There are 2 things to notice in this piece of code:
  • I removed some things that come with the shader by default, so we can no longer use normal/metallic, etc... This is just for the sake of simplicity.
  • I moved almost everything that was in the CGPROGRAM section (except the #pragma directives) to the new CGINCLUDE section.
Everything that is contained within a CGPROGRAM…ENDCG block is called a “pass”, a rendering pass to be exact. And everything contained within a CGINCLUDE…ENDCG block will be copy-pasted to every pass by the compiler. This will saves us on typing since we are going to write 2 passes.
The theory is this: we're gonna write a pass that renders the flag as usual, and then an extra pass that renders our flag as if all its polygons were flipped inside out. Now you can save the shader, create a material with it and add it to your flag before we start with the fun part.

BACK-FACE PASS

Okay, so far we did nothing to the flag. If anything it looks worse. What we need to do now is add another pass BEFORE the first CGPROGRAM block.
This is what we’re adding:
Cull Front CGPROGRAM #pragma surface surf Standard fullforwardshadows #prgama target 3.0 ENDCG
As simple as that. Now we can finally see both sides of the flag. But as you may have already noticed, lighting is broken on the other side.That's because even though we're telling Unity to render the triangles on the other side, they all share the same normal direction. To correct that we need to invert the normals of the surface in this last pass.
Change the CGPROGRAM block we just added to this:
Cull Front CGPROGRAM #pragma surface surf Standard fullforwardshadows vertex:vert #pragma target 3.0 void vert (inout appdata_full v) { // Here we are making the surface look // the opposite direction v.normal = -v.normal; } ENDCG
Here we add this “vert” function. This is how you manipulate vertex data in a Surface Shader in Unity. Also, notice the “vertex:vert” part at the end of the first #pragma directive, this makes Unity actually use our vert function.
Inside the function, we just invert the normal of the vertex. This way both sides are rendered correctly. You can save the shader and see it for yourself.

FURTHER IMPROVEMENT

From this point, you can add more properties to actually take care of normal mapping, roughness, etc… Or even show different textures on each side of the flag. But that’s beyond the scope of this little post. The biggest downside to this shader is that it only works correctly in deferred rendering; due to the way Forward works, lighting will be okay but if seen from behind depth artifacts may appear.
You can grab on my GitHub the full code of an improved version of this shader.

THANKS FOR READING

Hope you found something useful in the post. Any comments or feedback would be really welcome. Also, if you have any better way of working a two-sided shader just let me know. You can write me here, by mail or at reach me at Twitter.

Victor Navarro
Technical Artist - Programmer
2
Comments
uper
6 months ago
Developer - Programmer
Very Nice!
1