Search Unity

[First shader] Distance and alpha calculation - some help would be great :-)

Discussion in 'Shaders' started by Kiupe, Jul 17, 2017.

  1. Kiupe

    Kiupe

    Joined:
    Feb 1, 2013
    Posts:
    528
    Hi guys,

    I'm trying to write my first shader which goal is to set the alpha value according to a target distance. I have 2 files, one for the shader and one for a script that set the new target position inside an Update method. It seems that whatever my target position is the alpha value is never updated. I'm pretty it's all my fault but I need some help to point me in the right direction.

    The shader script :

    Code (CSharp):
    1. // Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
    2.  
    3. Shader "Custom/AlphaDependingDistance"
    4. {
    5.     Properties
    6.     {
    7.         _MainTex ("Texture", 2D) = "white" {}
    8.         _MinAlpha("Alpha min", Float) = 0.25
    9.         _MaxAlpha("Alpha max", Float) = 1.
    10.         _MinDistance("Distance min", Float) = 100.
    11.         _MaxDistance("Distance max", Float) = 200.
    12.     }
    13.     SubShader
    14.     {
    15.         Tags {"Queue"="Transparent" "RenderType"="Transparent" }
    16.  
    17.         Blend SrcAlpha OneMinusSrcAlpha
    18.  
    19.         Pass
    20.         {
    21.             CGPROGRAM
    22.             #pragma vertex vert
    23.             #pragma fragment frag
    24.             #include "UnityCG.cginc"
    25.  
    26.             struct v2f {
    27.                 float4 pos : SV_POSITION;
    28.                 float2 uv : TEXCOORD0;
    29.                 float4 worldPos : TEXCOORD1;
    30.             };
    31.  
    32.             sampler2D _MainTex;
    33.             float4 _MainTex_ST;
    34.  
    35.             v2f vert(appdata_base v) {
    36.                 v2f o;
    37.                 o.pos = UnityObjectToClipPos(v.vertex);
    38.                 o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
    39.                 o.worldPos = mul(unity_ObjectToWorld, v.vertex);
    40.                 return o;
    41.             }
    42.  
    43.             float _MinAlpha;
    44.             float _MaxAlpha;
    45.             float _MinDistance;
    46.             float _MaxDistance;
    47.             float4 _Target;
    48.  
    49.             fixed4 frag(v2f i) : SV_Target {
    50.  
    51.                 fixed4 col = tex2D(_MainTex, i.uv);
    52.  
    53.                 //Compute distance
    54.                 //float dist = distance(i.worldPos, _WorldSpaceCameraPos);
    55.                 float dist = distance(i.worldPos, _Target);
    56.  
    57.                 //Clamp distance
    58.                 dist = clamp(dist,_MinDistance,_MaxDistance);
    59.  
    60.                 //Compute distance ratio
    61.                 dist = (dist - _MinDistance) / (_MaxDistance - _MinDistance);
    62.  
    63.                 //Compute Alpha
    64.                 col.a = _MinAlpha + (_MaxAlpha - _MinAlpha) * dist;
    65.  
    66.                 return col;
    67.             }
    68.  
    69.             ENDCG
    70.         }
    71.     }
    72. }
    The MonoBehavior script :

    Code (CSharp):
    1. public class AlphaDependingScript : MonoBehaviour {
    2.  
    3.     public Transform target;
    4.  
    5.     private Material m;
    6.  
    7.     void Awake(){
    8.  
    9.         m = this.gameObject.GetComponent<SpriteRenderer> ().sharedMaterial;
    10.     }
    11.  
    12.     void Update(){
    13.  
    14.         if (m != null) {
    15.  
    16.             m.EnableKeyword("_Target");
    17.             m.SetVector("_Target", target.position);
    18.         }
    19.     }
    20. }
    I tried to declare _Target as a Vector at first but it changed nothing. I don't know if the issue is about the distance calculation or about updating the target position or ... I'm having trouble to figured what's happening because I have no clue how to set some "debug" in the shader code.

    Any help would be great.

    Thanks
     
  2. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,348
    A couple of things, most of which won't fix the issue but are good to know.

    m.EnableKeyword("_Target");

    Not needed for what you're doing. If you see #pragma shader_feature SOME_TEXT or #pragma multi_compile SOME_OTHER_TEXT_ON SOME_OTHER_TEXT_OFF or similar, along with #if, the EnableKeyword() and DisableKeyword() functions are for enabling and disabling these. Alternatively you can use the [Toggle(SOME_TEXT)] or [KeywordEnum(On, Off)] Material Property Drawers to have these be controlled from the material inspector. Since you're just setting a vector value enabling the keyword is unnecessary.


    m.SetVector("_Target", target.position);

    The SetVector() function takes a Vector4, but Transform.position is a Vector3. In most cases Unity will complain at you for trying to convert from a Vector3 to a Vector4, but in the case of SetVector() it "just works". The thing is you don't really know what the w component of the resulting Vector4 is going to be since you're not setting it. I would guess it'd be 1, or 0, but it could also be a copy of the z component. Honestly I just don't know as I try to avoid this, or at least avoid using the w component.
    edit: fixed typo, Vector4 to Vector3 is fine, the w component is just dropped, other way is bad.


    float4 _Target;

    As I just said SetVector() is a Vector4, but you can define the uniform variable in the shader to be any dimension you want (from float to float4) and Unity will handle this properly. For this case you're passing a Vector3 value that's getting turned into a Vector4 with an unknown (and unneeded) w component, so you're better off changing this to a float3 _Target; or limiting the later shader code to just the .xyz components.


    float4 worldPos : TEXCOORD1;
    ...
    o.worldPos = mul(unity_ObjectToWorld, v.vertex);
    ...
    float dist = distance(i.worldPos, _Target);


    Now we're getting a bit to the possible meat of the problem. A position is a Vector3 / float3 value, the fourth component is not needed, and indeed not wanted, when doing position comparisons. It's only use is with matrix transforms, like mul(unity_ObjectToWorld, v.vertex) needs the v.vertex to be a float4 with the w component == 1 or it'll just be rotated and not moved by the matrix multiplication. However you're passing a float4 worldPos and a float4 _Target position and doing a distance comparison between those 4 component numbers, so you're going to get a four dimensional distance and not the three dimensional distance you actually want. Not knowing what values are passed in the w component of _Target means you have no way to know for sure what that distance is going to be.

    Try changing that last line to:

    float dist = distance(i.worldPos.xyz, _Target.xyz);

    And that might be all that's needed to make everything "just work", but there's probably a few more things that I'll mention.


    _MinDistance("Distance min", Float) = 100.
    _MaxDistance("Distance max", Float) = 200.


    Those are pretty big numbers, but I'm not sure how big your Unity scene is. It might make sense if you're doing this for a 2D game so it's possibly fine? The positions are going to be calculated the same as if they were in your scene's root.


    //Clamp distance
    dist = clamp(dist,_MinDistance,_MaxDistance);

    //Compute distance ratio
    dist = (dist - _MinDistance) / (_MaxDistance - _MinDistance);


    This is a very minor adjustment, but you'd be better off with removing the clamp and using just:

    //Compute distance ratio and clamp
    dist = saturate((dist - _MinDistance) / (_MaxDistance - _MinDistance));


    The saturate() function is basically a free clamp(value, 0.0, 1.0) GPUs have.

    It won't change anything here since you're setting the value from script, but it can be useful just so you can see what the value is, or modify it by hand if needed.

    Technically you can use Visual Studio to debug DX11 shaders. See:
    https://docs.unity3d.com/Manual/SL-DebuggingD3D11ShadersWithVS.html

    I personally don't bother with this and just use the visual equivalent of "Debug.log" debugging, which is adding a return value; at random points in the shader and seeing if what comes out matches my expectation. For something like debugging the distance you could do the following which should get you rings of gradients going towards the target:

    fixed debugDist = frac(dist * 0.1); // repeat 0.0 to 1.0 every 10 units
    return fixed4(debugDist, debugDist, debugDist, 1);
     
    Last edited: Jul 18, 2017
  3. Kiupe

    Kiupe

    Joined:
    Feb 1, 2013
    Posts:
    528
    Thanks for the detailed explanation - it completely makes sense and it's very helpful !
     
  4. Kiupe

    Kiupe

    Joined:
    Feb 1, 2013
    Posts:
    528
    So it works great, at least way better than before. But now I want to do more :)

    All my Sprites have a X rotation, so the bottom part is closer to the camera than the top part. I'd like in my shader to check if the _Target is in front or behind the bottom part of the Sprite. The point would be to force alpha = 1 when the _Target is in front, no matter what the distance is. So my question is, how to compute the more front vertice position and then be able to use it in my Frag function ?

    Thanks
     
  5. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,348
    Depends on how you define "in front"? If you mean closer to the camera by depth, you can transform the world positions into view or camera space and compare the Z depth.

    Code (CSharp):
    1. float3 viewPos = mul(UNITY_MATRIX_V, float4(worldPos.xyz, 1)).xyz;
    2. float3 viewTargetPos = mul(UNITY_MATRIX_V, float4(_Target.xyz, 1)).xyz;
    3. if(viewPos.z > viewTargetPos.z)
    4.     alpha = 1;
    However that'll be testing per-pixel, so there can be a transition where the lowest parts of the sprite is still closer to the camera than the target than the pixels above. To fix that you would need to be comparing either against the sprite normal, or do the test in the vertex shader and pass a "in front" value to the fragment shader that you test, or test against the sprite's pivot position which actually requires you pass that data to the shader yourself due to the way Unity's sprites work. If you're sprites are always at a world z of 0 and you're not rotating the camera you could also do the test in world space, but I suspect that's not the case for you.
     
  6. Kiupe

    Kiupe

    Joined:
    Feb 1, 2013
    Posts:
    528
    Hi,

    I think compute the most "front" value in the vert function and then pass it to the frag function is a good idea. I don't really have the right habits when it comes to shaders, but I will eventually :)

    Thanks for the help !