Search Unity

  1. Unity 6 Preview is now available. To find out what's new, have a look at our Unity 6 Preview blog post.
    Dismiss Notice
  2. Unity is excited to announce that we will be collaborating with TheXPlace for a summer game jam from June 13 - June 19. Learn more.
    Dismiss Notice

Cross-billboards and fading of camera-perpendicular quads

Discussion in 'General Graphics' started by Damjan-Mozetic, Nov 9, 2016.

  1. Damjan-Mozetic

    Damjan-Mozetic

    Joined:
    Aug 15, 2013
    Posts:
    46
    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!

     
  2. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,381
    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".
    https://docs.unity3d.com/Manual/SL-SurfaceShaderExamples.html

    There's also a bit of it hidden in this thread, though it's for a different look than what you're after.
    https://forum.unity3d.com/threads/foliage-similar-to-the-witness.399585/

    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.
     
  3. Damjan-Mozetic

    Damjan-Mozetic

    Joined:
    Aug 15, 2013
    Posts:
    46
    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. :)
     
  4. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,381
  5. Damjan-Mozetic

    Damjan-Mozetic

    Joined:
    Aug 15, 2013
    Posts:
    46
    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. :)

    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
     
  6. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,381
    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?
     
  7. Damjan-Mozetic

    Damjan-Mozetic

    Joined:
    Aug 15, 2013
    Posts:
    46
  8. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,381
    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.
     
  9. Damjan-Mozetic

    Damjan-Mozetic

    Joined:
    Aug 15, 2013
    Posts:
    46
    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?
     
  10. Damjan-Mozetic

    Damjan-Mozetic

    Joined:
    Aug 15, 2013
    Posts:
    46
    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.

    Code (csharp):
    1. Shader "Custom/TwoSidedCutoutDiffuse" {
    2.     Properties {
    3.         _Color ("Color", Color) = (1,1,1,1)
    4.         _MainTex ("Albedo (RGB)", 2D) = "white" {}
    5.         _Cutoff ("Alpha cutoff", Range(0,1)) = 0.5
    6.     }
    7.  
    8.     SubShader {
    9.         Tags {"Queue"="AlphaTest" "IgnoreProjector"="True" "RenderType"="TransparentCutout"}
    10.         LOD 200
    11.         Cull Off
    12.      
    13.         CGPROGRAM
    14.         #pragma surface surf Lambert alphatest:_Cutoff
    15.  
    16.         // Use shader model 3.0 target, to get nicer looking lighting
    17.         #pragma target 3.0
    18.  
    19.         sampler2D _MainTex;
    20.         fixed4 _Color;
    21.  
    22.         struct Input {
    23.             float2 uv_MainTex;
    24.             float3 viewDir;
    25.         };
    26.  
    27.         void surf (Input IN, inout SurfaceOutput o) {
    28.             // Albedo comes from a texture tinted by color
    29.             fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
    30.             o.Albedo = c.rgb;
    31.             o.Alpha = c.a;
    32.             o.Alpha -= 1.0 - saturate(dot (normalize(IN.viewDir), o.Normal));
    33.         }
    34.         ENDCG
    35.     }
    36.  
    37.     FallBack "Transparent/Cutout/VertexLit"
    38. }
    39.  
     
    Last edited: Nov 10, 2016
  11. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,381
    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.
    https://docs.unity3d.com/Manual/SL-ShaderSemantics.html

    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:

    Code (CSharp):
    1. Shader "Custom/TwoSidedCutoutDiffuse" {
    2.     Properties {
    3.         _Color ("Color", Color) = (1,1,1,1)
    4.         _MainTex ("Albedo (RGB)", 2D) = "white" {}
    5.         _Cutoff ("Alpha cutoff", Range(0,1)) = 0.5
    6.     }
    7.     SubShader {
    8.         Tags {"Queue"="AlphaTest" "IgnoreProjector"="True" "RenderType"="TransparentCutout"}
    9.         LOD 200
    10.         Cull Off
    11.    
    12.         CGPROGRAM
    13.         #pragma surface surf Lambert alphatest:_Cutoff
    14.         // Use shader model 3.0 target, to get nicer looking lighting
    15.         #pragma target 3.0
    16.         sampler2D _MainTex;
    17.         fixed4 _Color;
    18.         struct Input {
    19.             float2 uv_MainTex;
    20.             float3 viewDir;
    21.             fixed facing : VFACE;
    22.         };
    23.         void surf (Input IN, inout SurfaceOutput o) {
    24.             // Albedo comes from a texture tinted by color
    25.             fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
    26.             o.Albedo = c.rgb;
    27.             o.Alpha = c.a;
    28.             o.Normal = half3(0, 0, IN.facing);
    29.             o.Alpha -= 1.0 - saturate(dot (normalize(IN.viewDir), o.Normal));
    30.         }
    31.         ENDCG
    32.     }
    33.     FallBack "Transparent/Cutout/VertexLit"
    34. }
    35.  
    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.
     
  12. Damjan-Mozetic

    Damjan-Mozetic

    Joined:
    Aug 15, 2013
    Posts:
    46
    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:
    http://imgur.com/a/cwcrG

    0.01:
    http://imgur.com/a/vAYi8

    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.
     
  13. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,381
    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.
     
  14. Damjan-Mozetic

    Damjan-Mozetic

    Joined:
    Aug 15, 2013
    Posts:
    46
    http://imgur.com/a/ojPti

    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?
     
  15. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,381
    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.
    https://docs.unity3d.com/Manual/SL-BuiltinMacros.html

    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.
     
  16. Damjan-Mozetic

    Damjan-Mozetic

    Joined:
    Aug 15, 2013
    Posts:
    46
    That's brilliant! I thank you very much for all your help, it is working nicely now. I'm even starting to enjoy shaders. :)
     
  17. Damjan-Mozetic

    Damjan-Mozetic

    Joined:
    Aug 15, 2013
    Posts:
    46
    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?
     
  18. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,381
    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.
     
  19. Damjan-Mozetic

    Damjan-Mozetic

    Joined:
    Aug 15, 2013
    Posts:
    46
    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.

    Code (CSharp):
    1. Shader "COS/TwoSidedCutoutDiffuseDither" {
    2.     Properties {
    3.         _Color ("Color", Color) = (1,1,1,1)
    4.         _MainTex ("Albedo (RGB)", 2D) = "white" {}
    5.         _DitherMaskLOD ("Alpha Noise (RGB)", 2D) = "white" {}
    6.         _Cutoff ("Alpha cutoff", Range(0,1)) = 0.01
    7.         _FadeEdgeA ("Fade Edge A", Range(0,1)) = 0.3
    8.         _FadeEdgeB ("Fade Edge B", Range(0,1)) = 0.7
    9.     }
    10.  
    11.     SubShader {
    12.         Tags {"Queue"="AlphaTest" "IgnoreProjector"="True" "RenderType"="TransparentCutout"}
    13.         LOD 200
    14.         Cull Off
    15.      
    16.         CGPROGRAM
    17.         #pragma surface surf Lambert alphatest:_Cutoff addshadow
    18.  
    19.         // Use shader model 3.0 target, to get nicer looking lighting
    20.         #pragma target 3.0
    21.  
    22.         sampler2D _MainTex;
    23.         sampler3D _DitherMaskLOD;
    24.         fixed4 _Color;
    25.         fixed _FadeEdgeA;
    26.         fixed _FadeEdgeB;
    27.  
    28.         // Input data for one evaluated pixel
    29.         struct Input {
    30.             float2 uv_MainTex;
    31.             float3 viewDir;
    32.             fixed facing : VFACE; // fixed = 10 bits (from at least -2 to +2)
    33.         };
    34.  
    35.         void surf (Input IN, inout SurfaceOutput o) {
    36.             // Albedo comes from a texture tinted by color
    37.             // tex2D returns a rgba color from the given texture and uv coordinate
    38.             fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
    39.             o.Albedo = c.rgb;
    40.             o.Alpha = c.a;
    41.             o.Normal = half3(0, 0, IN.facing);
    42.  
    43.             #ifdef UNITY_PASS_SHADOWCASTER
    44.                 // Test if shadowcaster is for camera depth, so shadows are not affected
    45.                 if (unity_LightShadowBias.z == 0) {
    46.             #endif
    47.                 //o.Alpha -= 1.0 - saturate(dot(normalize(IN.viewDir), o.Normal));
    48.                 half ndotv = 1.0 - saturate(dot(normalize(IN.viewDir), o.Normal));
    49.                 fixed fade = saturate((ndotv - _FadeEdgeA)/(_FadeEdgeB - _FadeEdgeA));
    50.                 fade = tex3D(_DitherMaskLOD, float3(IN.uv_MainTex * 30.0, fade * 0.2)).a;
    51.                 o.Alpha -= fade;
    52.             #ifdef UNITY_PASS_SHADOWCASTER
    53.                 }
    54.             #endif
    55.         }
    56.         ENDCG
    57.     }
    58.  
    59. //    FallBack "Transparent/Cutout/VertexLit"
    60. }
     
  20. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,381
    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.
     
  21. Damjan-Mozetic

    Damjan-Mozetic

    Joined:
    Aug 15, 2013
    Posts:
    46
    Sorry for the late reply. With your last tweaks and a noise texture, the trees look pretty good. Thank you for all your help!
     
  22. MadAboutPandas3

    MadAboutPandas3

    Joined:
    Jul 3, 2017
    Posts:
    29
    I have read the thread. Would you be so nice and post your shader?
     
  23. Damjan-Mozetic

    Damjan-Mozetic

    Joined:
    Aug 15, 2013
    Posts:
    46
    I’ll look it up once I get home if I have it still. I have switched to flat billboards since then, as the cross trees never looked that good under varying lighting conditions.
     
  24. Kennai

    Kennai

    Joined:
    Nov 1, 2018
    Posts:
    27
    Oh I was needed slowly to fade sprite when it become perpendicular to camera direction. I spent two days and finally I made that shader! I copied some parts from "Sprite/Default" shader, so my shader can use SpriteRenderer.Color value. ;)
    Hope it will helps someone!

    result:
    https://imgur.com/a/yHQubzU

    Code (CSharp):
    1. Shader "Sprites/SpriteFade"
    2. {
    3.     Properties
    4.     {
    5.         [PerRendererData]_MainTex("Sprite Texture", 2D) = "white" {}
    6.         _Color("Tint",Color) = (1,1,1,1)
    7.     }
    8.  
    9.     SubShader
    10.     {
    11.         Cull Off
    12.         Lighting Off
    13.         ZWrite Off
    14.         Fog { Mode Off }
    15.         Blend One OneMinusSrcAlpha
    16.  
    17.         Tags
    18.         {
    19.             "Queue" = "Transparent"
    20.             "IgnoreProjector" = "True"
    21.             "RenderType" = "Transparent"
    22.             "PreviewType" = "Plane"
    23.             "CanUseSpriteAtlas" = "True"
    24.         }
    25.  
    26.         Pass
    27.         {
    28.             CGPROGRAM
    29.             #pragma vertex vert
    30.             #pragma fragment frag
    31.             #pragma multi_compile DUMMY PIXELSNAP_ON
    32.             #include "UnityCG.cginc"
    33.  
    34.             fixed4 _Color;
    35.             sampler2D   _MainTex;
    36.  
    37.             struct appdata_t
    38.             {
    39.                 float4 vertex     : POSITION;
    40.                 float4 color      : COLOR;
    41.                 float2 texcoord   : TEXCOORD0;
    42.                 half3  normal     : NORMAL;
    43.              
    44.             };
    45.  
    46.             struct v2f
    47.             {
    48.                 float4 vertex        : SV_POSITION;
    49.                 float4 color         : COLOR;
    50.                 float2 texcoord      : TEXCOORD0;
    51.             };
    52.  
    53.             v2f vert(appdata_t v)
    54.             {
    55.                 v2f o;
    56.                 o.color = v.color;
    57.  
    58.                 o.vertex = UnityObjectToClipPos(v.vertex);
    59.                 o.texcoord = v.texcoord;
    60.                 o.color = v.color * _Color;
    61.  
    62.                 //this code do a trick with alpha, vertex position and camera direction
    63.                 float3 pos = mul(unity_ObjectToWorld, v.vertex);
    64.                 float3 normalDir = normalize(mul(unity_ObjectToWorld, float4(v.normal, 0)).xyz);
    65.                 float3 camVec = _WorldSpaceCameraPos - pos.xyz;
    66.                 o.color.a *= saturate(abs(dot(normalDir, normalize(camVec))));
    67.  
    68.                 return o;
    69.             }
    70.  
    71.  
    72.             fixed4 frag(v2f i) : SV_Target
    73.             {
    74.                 fixed4 col = tex2D(_MainTex, i.texcoord) * i.color;
    75.                 col.rgb *= col.a;
    76.                 return col;
    77.             }
    78.  
    79.             ENDCG
    80.         }
    81.     }
    82.     FallBack Off
    83. }
     
  25. MathiasSeverinTrackman

    MathiasSeverinTrackman

    Joined:
    Mar 1, 2021
    Posts:
    4
    Dude! As a non-programmer I have been looking for a script like this for hours. Thanks!