Cross-billboards and fading of camera-perpendicular quads

Here’s a screenshot of my game in development, and as you can see I have crossed billboards for representing trees. What I would like to do is be able to fade the billboards as they get perpendicular to the camera or find some other way to make it look less jarring. Any suggestions or ideas are more than welcome! Thank you!

You’ll need a custom shader for this, so the shader forum might be a better location for this. However the basics are you’ll want to calculate the equivalent of fresnel / rim lighting / edge glow and use the value to fade out the alpha instead of using it for a glow.

There’s an example of how to do some of it on the Surface Shader Examples page under “rim lighting”.

There’s also a bit of it hidden in this thread, though it’s for a different look than what you’re after.

It looks like you’re using a cutout shader with point sampled texture. O your probably want to use the “rim light” fresnel with a noise texture so the fade out is dithered. You might be able to build that dither pattern into your existing tree texture’s alpha.

Thank you for the info, I’ll look up those threads. The only problem is that I have no knowledge of shader programming, so in the worst case I’ll learn. :slight_smile:

1 Like

Ok, I’ve checked out those two threads and came to the conclusion that I have absolutely no idea how to implement what you’ve kindly described above. :slight_smile:

I’ve however applied your two-pass shader from the second thread to a tree and I may be able to work out something by modifying the alpha cutoff with the angle

For your visual style, the two pass shader is likely unnecessary. You appear to be going for an explicitly low-fy, hard pixel look which smooth transparency doesn’t usually have a place in, which is why I suggested dithering.

What shader are you currently using?

I am using Mario Lelas’ StandardDoubleSided shader from “Double Sided Standard Mobile Legacy Shaders” (Unity Asset Store - The Best Assets for Game Making).

Hmm… okay, I’m familiar with those shaders, in that they exist and work, but I’m not familiar with exactly how they’re implemented in the shader code, and won’t ask you to post the code here (as it’s a paid asset, and that would be rude). Basically I don’t know if they’re implemented as surface shaders or not (there’s a #pragma surface somewhere in the .shader file vs #pragma vertex and #pragma fragment) as that changes the specifics on how this would be implemented, though the concept is the same.

Also as an aside it looks like there’s already a shader included with that package that uses the two pass method I posted about, but again it’s probably not relevant to your use case.

If his shaders are using surface shaders, and IN.viewDir appears somewhere in the shader already, you can make a copy of it and try adding this near the end of the surf function:

o.Alpha -= 1.0 - saturate(dot (normalize(IN.viewDir), o.Normal));

It won’t be perfect for what you need, but it’ll get you closer.

As I can tell, the shader is implemented as a vertex/fragment shader, and it looks mighty complex to me with all those passes inside. I really need something simple (no metallic map/smoothness/scale/normal map stuff), so perhaps there is an existing doublesided shader I could start with?

I’ve also taken a simple shader and modified it with the “o.Alpha” line you suggested, but I can’t get it to display the tree image from the other side (I’m using a quad). It also shows the tree shadow from one side only.

Shader "Custom/TwoSidedCutoutDiffuse" {
    Properties {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        _Cutoff ("Alpha cutoff", Range(0,1)) = 0.5
    }

    SubShader {
        Tags {"Queue"="AlphaTest" "IgnoreProjector"="True" "RenderType"="TransparentCutout"}
        LOD 200
        Cull Off
     
        CGPROGRAM
        #pragma surface surf Lambert alphatest:_Cutoff

        // Use shader model 3.0 target, to get nicer looking lighting
        #pragma target 3.0

        sampler2D _MainTex;
        fixed4 _Color;

        struct Input {
            float2 uv_MainTex;
            float3 viewDir;
        };

        void surf (Input IN, inout SurfaceOutput o) {
            // Albedo comes from a texture tinted by color
            fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
            o.Albedo = c.rgb;
            o.Alpha = c.a;
            o.Alpha -= 1.0 - saturate(dot (normalize(IN.viewDir), o.Normal));
        }
        ENDCG
    }

    FallBack "Transparent/Cutout/VertexLit"
}

This is because this shader doesn’t do proper two sided lighting. If you point a light at it so that it’s bright, then look at both sides of the planes, both sides will be bright (or dark if you turn the light to hit the other side). This is because the normals, the direction the surface faces, is a single value that’s getting reused for both sides of the polygons.

If you read through some of the two sided / double sided threads on the forum you’ll see a number of solutions for this, with a popular one being a two pass method where you render an object once, then a second time but with the normal direction flipped. The other way is to do a dot product, almost exactly like what is already being done in your shader, and flipping the normal then. But the first option is wasteful and can cause problems for transparent rendering, and the second option quite often flips the normal when it shouldn’t.

The real answer is the VFACE semantic.

This is a 1 or -1 value that the fragment shader receives that tells it if the surface is facing the camera or not. It’s based on the same internal information that does the front / back face culling. The above page shows how to use it in a vertex / fragment shader, but Unity’s surface shaders can use it to like this:

Shader "Custom/TwoSidedCutoutDiffuse" {
    Properties {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        _Cutoff ("Alpha cutoff", Range(0,1)) = 0.5
    }
    SubShader {
        Tags {"Queue"="AlphaTest" "IgnoreProjector"="True" "RenderType"="TransparentCutout"}
        LOD 200
        Cull Off
    
        CGPROGRAM
        #pragma surface surf Lambert alphatest:_Cutoff
        // Use shader model 3.0 target, to get nicer looking lighting
        #pragma target 3.0
        sampler2D _MainTex;
        fixed4 _Color;
        struct Input {
            float2 uv_MainTex;
            float3 viewDir;
            fixed facing : VFACE;
        };
        void surf (Input IN, inout SurfaceOutput o) {
            // Albedo comes from a texture tinted by color
            fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
            o.Albedo = c.rgb;
            o.Alpha = c.a;
            o.Normal = half3(0, 0, IN.facing);
            o.Alpha -= 1.0 - saturate(dot (normalize(IN.viewDir), o.Normal));
        }
        ENDCG
    }
    FallBack "Transparent/Cutout/VertexLit"
}

Now the normal is flipped if you’re looking at the back side of the polygon, and will be lit accordingly. Also, the dot product will work. The VFACE semantic is how I believe the shaders you were previously using already worked.

A problem you might run into is if you’re using real-time directional light shadows or any effects that use the camera depth texture you might see “halos” the areas that are cutout. I couldn’t tell from the screenshot you posted exactly what you’re doing for shadows, but this might not be a problem for you.

Thank you, this has been most insightful. I’ll take my time and read through the tutorials and docs.

I am using one dynamic directional light (the sun) and other point light sources (torchlight for example). I have made two gif animations, one with Alpha cutoff = 0.01 and the other at 0.3.

0.3:

0.01:

At 0.01 it looks better than my old shader, but going significantly above that value, that halo effect you mentioned seems to ruin the scene.

The tree is also not casting shadows when rotated 180 degrees, but it does cast shadows at 360.

The shadow caster pass, what’s used to render the shadows (and the depth texture) is coming from the shader linked to by the FallBack line. By default the shadow caster is single sided, hence why you’re not seeing shadows. You can remove the FallBack line and add addshadow to the end of the #pragma surface line to have the surface shader generate a custom shadow caster pass which will be double sided, but it might also cast some weird shadows because the edge fade stuff will still be happening in the shadow. However it should prevent the haloing as the depth texture will now match the visuals. There’s no a clean way to differentiate between the shadow caster pass being used for the depth texture and the directional shadow map unfortunately, Unity simply doesn’t bother to separate them cleanly, even though point light shadows and spotlight shadows are easy to detect.

This is what the shadow looks like as I move around… I can’t use it like this. Perhaps a second pass or something could solve this?

That’s the problem I mentioned where the shadow caster and depth passes are both using the same shader. That’s the shadow caster being clipped out too, which you don’t want.

For that there’s only really one way that exists to tell the difference between when the shader is being used by the camera depth rendering or the direction shadows rendering; you have to see if unity_LightShadowBias.z == 0.0 or not, and you must have at least some bias on your directional light.

For a surface shader you only want to be testing for that when the surf function is being used for the shadowcaster pass, and luckily there’s a handy define for that. When the surface shader is generating the shader passes it adds a define for each pass type so you can check for it, the shadow caster pass is UNITY_PASS_SHADOWCASTER.

So you need to add this:

#ifdef UNITY_PASS_SHADOWCASTER
if (unity_LightShadowBias.z == 0) { // Test if shadowcaster is for camera depth
#endif
o.Alpha-=1.0- saturate(dot (normalize(IN.viewDir), o.Normal));
#ifdef UNITY_PASS_SHADOWCASTER
}
#endif

Basically, if the surf function is being called during a shadowcaster pass it’ll add an if condition block around the fresnel preventing it from being used during and shadow pass (as long as they have a bias), but will clip for the camera depth texture.

That’s brilliant! I thank you very much for all your help, it is working nicely now. I’m even starting to enjoy shaders. :slight_smile:

Oh and one more thing. In the first post you mentioned using a noise texture to apply dithering to the fade. Care to enlighten me on how I could achieve that?

There’s a few ways to do this, and there’s a few ways to think about how you want it to look.

The simplest implementation is to take your tree texture and add some noise to the alpha channel. However you’ll quickly run into the problem of controlling the fade out edge with just the cutout value. The easiest way to handle this is with a scale / bias on the results of the dot product.

// additional properties
_FadeEdgeA (“Fade Edge A”, Range(0,1) = 0.3
_FadeEdgeB (“Fade Edge B”, Range(0,1) = 0.7

// declare variables
fixed _FadeEdgeA;
fixed _FadeEdgeB;

// bias original ndotv
half ndotv = 1.0 - saturate(dot(normalize(IN.viewDir), o.Normal));
fixed fade = saturate((ndotv - _FadeEdgeA)/(_FadeEdgeB - _FadeEdgeA));
o.Alpha -= fade;

With the hard alpha your texture has, the above code won’t really change the appearance much apart from giving an alternate control for where the cutoff shows up. But if your alpha has some extra noise it’ll look significantly different and let you control that more easily.

Alternatively you could use a separate noise texture instead of adding the noise to the original texture. You can either use your own, or there are a few included with Unity, including some “hidden” ones that Unity already uses for stuff like the Standard shader’s transparent stippled shadows and LOD blending.

sampler3D _DitherMaskLOD;

fixed fade = saturate((ndotv - _FadeEdgeA)/(_FadeEdgeB - _FadeEdgeA));
fade = tex3D(_DitherMaskLOD, float3(IN.uv_MainTex * 8.0, fade * 0.9375).a;
o.Alpha -= fade;

These methods work in the same space as the original textures / polygons which might not be what you want, especially since the dither will stop working when far away as it’ll get too small to work properly, and be really blocky and noticeable up close. That might lead you to thinking about doing it in screen space, which is totally possible … but that’s a huge world of pain due to some missing features / bugs in the surface shaders. If you want to go that route you pretty much have to go with a completely custom shadow caster pass and don’t rely on the surface shader at all.

I’ve tried implementing the dithering, but the end result is always a checkerboard pattern on the tree. I’ve played with the parameters in “fade = tex3D(_DitherMaskLOD, float3(IN.uv_MainTex * 30.0, fade * 0.2)).a;” and got it to look somewhat ok. The problem is that it doesn’t seem to care for the _DitherMaskLOD texture. No matter what noise texture I assign to the shader, it always renders that checkerboard pattern.

Shader "COS/TwoSidedCutoutDiffuseDither" {
    Properties {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        _DitherMaskLOD ("Alpha Noise (RGB)", 2D) = "white" {}
        _Cutoff ("Alpha cutoff", Range(0,1)) = 0.01
        _FadeEdgeA ("Fade Edge A", Range(0,1)) = 0.3
        _FadeEdgeB ("Fade Edge B", Range(0,1)) = 0.7
    }

    SubShader {
        Tags {"Queue"="AlphaTest" "IgnoreProjector"="True" "RenderType"="TransparentCutout"}
        LOD 200
        Cull Off
     
        CGPROGRAM
        #pragma surface surf Lambert alphatest:_Cutoff addshadow

        // Use shader model 3.0 target, to get nicer looking lighting
        #pragma target 3.0

        sampler2D _MainTex;
        sampler3D _DitherMaskLOD;
        fixed4 _Color;
        fixed _FadeEdgeA;
        fixed _FadeEdgeB;

        // Input data for one evaluated pixel
        struct Input {
            float2 uv_MainTex;
            float3 viewDir;
            fixed facing : VFACE; // fixed = 10 bits (from at least -2 to +2)
        };

        void surf (Input IN, inout SurfaceOutput o) {
            // Albedo comes from a texture tinted by color
            // tex2D returns a rgba color from the given texture and uv coordinate
            fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
            o.Albedo = c.rgb;
            o.Alpha = c.a;
            o.Normal = half3(0, 0, IN.facing);

            #ifdef UNITY_PASS_SHADOWCASTER
                // Test if shadowcaster is for camera depth, so shadows are not affected
                if (unity_LightShadowBias.z == 0) {
            #endif
                //o.Alpha -= 1.0 - saturate(dot(normalize(IN.viewDir), o.Normal));
                half ndotv = 1.0 - saturate(dot(normalize(IN.viewDir), o.Normal));
                fixed fade = saturate((ndotv - _FadeEdgeA)/(_FadeEdgeB - _FadeEdgeA));
                fade = tex3D(_DitherMaskLOD, float3(IN.uv_MainTex * 30.0, fade * 0.2)).a;
                o.Alpha -= fade;
            #ifdef UNITY_PASS_SHADOWCASTER
                }
            #endif
        }
        ENDCG
    }

//    FallBack "Transparent/Cutout/VertexLit"
}

That’s because _DitherMaskLOD is a built in 3D texture for doing checkerboard dithering, and I didn’t show a property definition in my example because _DitherMaskLOD gets auto assigned to that texture. If you have it listed in the properties any texture you set in the material will just be ignored. Use a different property name like _NoiseTex, or _DitherTex, or _BobsMyUncle.

If you do what to use your own noise texture, the way you use it will have to be much different than the example I gave you using the dither mask. As mentioned the dither mask is a 3D texture, it’s a “black or white” alpha only texture with each step of the noise pattern on a each step of the z dimension. You can think of it as a stack of several 2D textures. Any noise texture you provide is going to be a 2D texture, and won’t have a z dimension to sample from.

Instead to use a 2D noise texture you’ll need to treat things a little differently. The 3D noise texture only has 0 and 1 values, so doing o.Alpha -= works nicely to get that check board dither (and you may have noticed the alpha cutoff value almost stops doing anything). That means you need to get your greyscale noise texture into a similar binary value based on the fade using shader math. That can be to either to use in the same “o.Alpha -=” way, or it could be using the intrinsic clip() function which is what alpha test / cutout shaders use to tell the GPU to do the cutout transparency.

So…

_NoiseTex (“Alpha Noise”, 2D) = “white” {}

sampler2D _NoiseTex;

half ndotv = … // same as before
fixed fade = … // same as before
fixed n = tex2D(_NoiseTex, IN.uv_MainTex * 30).g; // or .a, depends on if you have your noise in the RGB or Alpha)
clip(n - fade);
// o.Alpha -= fade; no longer necessary, o.Alpha only needs the alpha from the main texture

That clip is exactly what the surface shader is doing normally immediately after the surf function finishes, just with the equivalent of clip(o.Alpha - _Cutoff); The clip() function checks if the value passed to it is less than 0 and skip drawing that pixel if it is.

Also the * 0.9375 is a magic number you shouldn’t touch when using the 3D dither mask. It has to do with the 3D texture being 4x4x16 texels.