Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Shader Tutorial, Interactable Energy Shield

Discussion in 'Community Learning & Teaching' started by 670ok, Apr 4, 2023.

  1. 670ok

    670ok

    Joined:
    Jun 6, 2017
    Posts:
    2

    Demonstration



    Setup URP Project

    Create a new project and switch to Universal RP. I am using Unity2022.2, other versions are also OK


    Remember to toggle DepthTexture and OpaqueTexture, we will use them later


    Create a new shader



    Output UV, normal and other informations from vertex shader for later use

    Code (CSharp):
    1. struct appdata
    2. {
    3.    float4 vertex : POSITION;
    4.    float2 uv : TEXCOORD0;
    5.    float3 normal : NORMAL;
    6. };
    7.  
    8. struct v2f
    9. {
    10.    float4 vertex : SV_POSITION;
    11.    float2 uv : TEXCOORD1;
    12.    float3 normal : TEXCOORD2;
    13.    float3 worldPos : TEXCOORD3;
    14.    float4 screenPos : TEXCOORD4;
    15.    float4 localPos : TEXCOORD5;
    16. };
    17.  
    18. v2f vert (appdata v)
    19. {
    20.    v2f o;
    21.    o.vertex = TransformObjectToHClip(v.vertex.xyz);
    22.    o.uv = v.uv;
    23.    o.normal = TransformObjectToWorldNormal(v.normal);
    24.    o.worldPos = TransformObjectToWorld(v.vertex.xyz);
    25.    o.localPos = v.vertex;
    26.    o.screenPos = o.vertex;
    27.    #if UNITY_UV_STARTS_AT_TOP
    28.     o.screenPos.y *= -1;
    29.    #endif
    30.     return o;
    31. }

    Rim Light


    First, add rim light, use dot product of model normal and view direction

    Code (CSharp):
    1. //Properties
    2. _RimPower ("RimPower", Float) = 1
    3. [HDR] _RimColor ("RimColor", Color) = (1, 1, 1, 1)
    4.  
    5. float _RimPower;
    6. float4 _RimColor;
    7.  
    8. //frag
    9. float3 normal = normalize(i.normal);
    10. float3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos);
    11. float ndv = dot(normal, viewDir);
    12. if(ndv < 0) {
    13.    ndv = abs(ndv);
    14. }
    15. ndv = 1 - ndv;
    16.  
    17. float rimIntensity = pow(ndv, _RimPower);
    18. finalColor += _RimColor * rimIntensity;
    19. finalColor.a = saturate(finalColor.a);

    Seems to be missing something, oh, forgot to add Bloom...


    Intersection Highlight

    When energy shield contacts with other objects, it needs a bright edge. Sample scene depth texture with pixel screen coordinates, and then compare scene depth with pixel depth. When two value are close enough, a bright edge should be displayed

    Code (CSharp):
    1. //Properties
    2. _IntersectionWidth ("IntersectionWidth", Float) = 1
    3. [HDR] _IntersectionColor ("IntersectionColor", Color) = (1, 1, 1, 1)
    4.  
    5. float _IntersectionWidth;
    6. float4 _IntersectionColor;
    7.  
    8. sampler2D _CameraDepthTexture;
    9.  
    10. //frag
    11. i.screenPos.xyz /= i.screenPos.w;
    12. float2 screenUV = i.screenPos.xy;
    13. screenUV = (screenUV + 1) / 2;
    14.  
    15. float selfZ = i.screenPos.z;
    16. float sceneZ = tex2D(_CameraDepthTexture, screenUV).r;
    17. float linearSelfZ = LinearEyeDepth(selfZ, _ZBufferParams);
    18. float linearSceneZ = LinearEyeDepth(sceneZ, _ZBufferParams);
    19. float zDifference = linearSceneZ - linearSelfZ;
    20. if(zDifference < _IntersectionWidth) {
    21.    float intersectionIntensity = (1 - zDifference / _IntersectionWidth);
    22.    intersectionIntensity = saturate(intersectionIntensity);
    23.    intersectionIntensity = pow(intersectionIntensity, 4);
    24.    finalColor += _IntersectionColor * intersectionIntensity;
    25.    finalColor.a = saturate(finalColor.a);
    26. }

    Texture

    Next, add texture to energy shield, sphere's UV is uneven, if sample texture directly with UV, texture will be compressed at the top area and stretched in the middle area.

    Here I transfer 2d texture into a cubemap, sample it with normal vector. And check if pixel is on backface, if it is, no need to show texture


    Code (CSharp):
    1. //Properties
    2. _PatternTex ("PatternTex", Cube) = "white" {}
    3. _PatternPower ("PatternPower", Float) = 1
    4. [HDR] _PatternColor ("PatternColor", Color) = (1, 1, 1, 1)
    5.  
    6. samplerCUBE _PatternTex;
    7. float _PatternPower;
    8. float4 _PatternColor;
    9.  
    10. //frag
    11. int isFrontFace = 1;
    12. //......
    13. if(ndv < 0) {
    14.    isFrontFace = 0;
    15. }
    16.  
    17. float patternIntensity = texCUBE(_PatternTex, normal).a * isFrontFace;
    18. patternIntensity *= pow(ndv, _PatternPower);
    19. finalColor += patternIntensity * _PatternColor;
    20. finalColor.a = saturate(finalColor.a);

    Next, add flow effect to texture, using a grid mask


    Code (CSharp):
    1. //Properties
    2. _Mask ("Mask", 2D) = "black" {}
    3. [HDR] _MaskColor ("MaskColor", Color) = (1, 1, 1, 1)
    4.  
    5. sampler2D _Mask;
    6. float4 _Mask_ST;
    7. float4 _MaskColor;
    8.  
    9. //frag
    10. float mask = 0;
    11. mask += tex2D(_Mask, i.uv * _Mask_ST.xx + _Mask_ST.zz * _Time.y).a;
    12. mask += tex2D(_Mask, i.uv * _Mask_ST.yy + _Mask_ST.ww * _Time.y).a;
    13. mask = saturate(mask);
    14. finalColor += patternIntensity * mask * _MaskColor;
    15. finalColor.a = saturate(finalColor.a);

    Dissolve
    Next, make dissolve effect of starting energy shield, use pixel's y coordinate to control dissolve, and then add a noise texture to make irregular contour

    Code (CSharp):
    1. //Properties
    2. _Noise ("Noise", 2D) = "white" {}
    3.  
    4. _DissolveThreshold ("DissolveThreshold", Float) = 1
    5. _DissolveWidth ("DissolveWidth", Float) = 0.1
    6. [HDR] _DissolveColor ("DissolveColor", Color) = (1, 1, 1, 1)
    7.  
    8. sampler2D _Noise;
    9. float4 _Noise_ST;
    10.  
    11. float _DissolveThreshold;
    12. float _DissolveWidth;
    13. float4 _DissolveColor;
    14.  
    15. //frag
    16. if(i.localPos.y > _DissolveThreshold) {
    17.    discard;
    18. }
    19. else if(i.localPos.y > _DissolveThreshold - _DissolveWidth) {
    20.    float t = (i.localPos.y - _DissolveThreshold + _DissolveWidth) / _DissolveWidth;
    21.    float noise = tex2D(_Noise, i.uv * _Noise_ST.xy + _Noise_ST.zw * _Time.y);
    22.    noise = lerp(1, noise * (1 - t), pow(t, 0.5));
    23.    if(noise > 0.5) {
    24.        finalColor = _DissolveColor;
    25.    }else {
    26.     discard;
    27.    }
    28. }

    Interaction
    Next, make the most complex interaction function. The idea is to use C# script to pass interaction point, interaction radius and interaction color to material. Then in shader, calculate the distance between pixel and interaction point, display interaction color if pixel is within interaction radius.

    Create a new script called Shield.cs, which contains APIs that passes interaction information

    Code (CSharp):
    1. public class Shield : MonoBehaviour {
    2.    private class InteractionData {
    3.        public Color color;
    4.        public Vector3 interactionStartPos;
    5.        public float timer;
    6.    }
    7.  
    8.    public List<Material> materials;
    9.  
    10.    private List<InteractionData> interactionDatas = new List<InteractionData>();
    11.  
    12.    private void Update() {
    13.        //......
    14.         for (int i = 0; i < materials.Count; i++) {
    15.            materials.SetInt("_InteractionNumber", interactionDatas.Count);
    16.  
    17.            if (interactionDatas.Count > 0) {
    18.                materials.SetVectorArray("_InteractionStartPosArray", interactionStartPosArray);
    19.                materials.SetFloatArray("_InteractionInnerRadiusArray", interactionInnerRadiusArray);
    20.                materials.SetFloatArray("_InteractionOuterRadiusArray", interactionOuterRadiusArray);
    21.                materials.SetFloatArray("_InteractionAlphaArray", interactionAlphaArray);
    22.                materials.SetColorArray("_InteractionColorArray", interactionColorArray);
    23.                materials.SetFloatArray("_DistortAlphaArray", distortAlphaArray);
    24.            }
    25.        }
    26.    }
    27.  
    28.    public void AddInteractionData(Vector3 pos, Color color) {
    29.        if (interactionDatas.Count >= 100) {
    30.            return;
    31.        }
    32.  
    33.        InteractionData interactionData = new InteractionData();
    34.        interactionData.color = color;
    35.        interactionData.interactionStartPos = pos;
    36.        interactionDatas.Add(interactionData);
    37.    }
    38. }
    Create a new script called ShootManager.cs that performs ray detection when clicking mouse button and call interaction interface if it collides with energy shield

    Code (CSharp):
    1. public class ShootManager : MonoBehaviour {
    2.     [ColorUsage(true, true)] public Color interactionColor;
    3.  
    4.    private void Update() {
    5.        if (Input.GetMouseButtonDown(0)) {
    6.            RaycastHit hitInfo;
    7.            bool hited = Physics.Raycast(Camera.main.ScreenPointToRay(Input.mousePosition), out hitInfo, Mathf.Infinity);
    8.            if (hited) {
    9.                Shield.instance.AddInteractionData(hitInfo.point, interactionColor);
    10.            }
    11.        }
    12.    }
    13. }
    Then calculate it in shader

    Code (CSharp):
    1. //Properties
    2. int _InteractionNumber;
    3. float3 _InteractionStartPosArray[100];
    4. float _InteractionInnerRadiusArray[100];
    5. float _InteractionOuterRadiusArray[100];
    6. float _InteractionAlphaArray[100];
    7. float4 _InteractionColorArray[100];
    8.      
    9. float _DistortAlphaArray[100];
    10.  
    11. float GetInteractionIntensity(v2f i, float3 startPos, float innerRadius, float outerRadius) {
    12.    float dist = distance(i.worldPos, startPos);
    13.    if(dist > outerRadius || dist < innerRadius) {
    14.        return 0;
    15.    }
    16.    else {
    17.      float intensity = (dist - innerRadius) / (outerRadius - innerRadius);
    18.      return intensity;
    19.    }
    20. }
    21.  
    22. //frag
    23. float interactionIntensity = 0;
    24. float4 interactionColor = 0;
    25. for(int iii = 0; iii < _InteractionNumber; iii++) {
    26.   float tempInteractionIntensity = GetInteractionIntensity(i, _InteractionStartPosArray[iii], _InteractionInnerRadiusArray[iii], _InteractionOuterRadiusArray[iii]) * _InteractionAlphaArray[iii];
    27.    interactionIntensity += tempInteractionIntensity;
    28.  
    29.    interactionColor += _InteractionColorArray[iii] * tempInteractionIntensity;
    30. }
    31.  
    32. interactionIntensity = saturate(interactionIntensity);
    33.  
    34. finalColor += interactionColor;
    35. finalColor.a = saturate(finalColor.a);


    In interactive area, lighten and distort the main texture

    Code (CSharp):
    1. //Properties
    2. _DistortNormal ("DistortNormal", 2D) = "bump" {}
    3. _DistortIntensity ("DistortIntensity", Float) = 1
    4.  
    5. sampler2D _DistortNormal;
    6. float4 _DistortNormal_ST;
    7. float _DistortIntensity;
    8.  
    9. float GetDistortIntensity(v2f i, float3 startPos, float innerRadius, float outerRadius) {
    10.    float dist = distance(i.worldPos, startPos);
    11.    if(dist > outerRadius) {
    12.        return 0;
    13.    }
    14.    else {
    15.        float intensity = dist / outerRadius;
    16.        return intensity;
    17.    }
    18. }
    19.  
    20. //frag
    21. float3 distortNormal = UnpackNormal(tex2D(_DistortNormal, i.uv * _DistortNormal_ST.xy + _DistortNormal_ST.zw * _Time.y));
    22. distortNormal *= _DistortIntensity * distortIntensity;
    23.  
    24. float distortIntensity = 0;
    25. for(int iii = 0; iii < _InteractionNumber; iii++) {
    26.    //......
    27.     distortIntensity += GetDistortIntensity(i, _InteractionStartPosArray[iii], _InteractionInnerRadiusArray[iii], _InteractionOuterRadiusArray[iii]) * _DistortAlphaArray[iii];
    28.    distortIntensity = saturate(distortIntensity);
    29. }
    30.  
    31. float patternIntensity = texCUBE(_PatternTex, normal + distortNormal).a * isFrontFace;
    32. patternIntensity *= pow(ndv + interactionIntensity, _PatternPower);

    Interactive function is one step away!

    Next, add screen distortion, the idea is to change the screen UV with a normal map, and then sample _CameraOpaqueTexture

    Create a new shader called Shield_Distort.shader, copy the code from shield shader and add

    Code (CSharp):
    1. sampler2D _CameraOpaqueTexture;
    2. float4 _CameraOpaqueTexture_TexelSize;
    Modify fragment function

    Code (CSharp):
    1. float4 frag (v2f i) : SV_Target
    2. {
    3.    float4 finalColor = 0;
    4.  
    5.    float distortIntensity = 0;
    6.    for(int iii = 0; iii < _InteractionNumber; iii++) {
    7.       distortIntensity += GetDistortIntensity(i, _InteractionStartPosArray[iii], _InteractionInnerRadiusArray[iii], _InteractionOuterRadiusArray[iii]) * _DistortAlphaArray[iii];
    8.        distortIntensity = saturate(distortIntensity);
    9.    }
    10.  
    11.    float3 distortNormal = UnpackNormal(tex2D(_DistortNormal, i.uv * _DistortNormal_ST.xy + _DistortNormal_ST.zw * _Time.y));
    12.    distortNormal *= _DistortIntensity * distortIntensity;
    13.  
    14.    i.screenPos.xyz /= i.screenPos.w;
    15.    float2 screenUV = i.screenPos.xy;
    16.    screenUV = (screenUV + 1) / 2;
    17.  
    18.    finalColor = tex2D(_CameraOpaqueTexture, screenUV + distortNormal.xy * _CameraOpaqueTexture_TexelSize.xy);
    19.  
    20.    return finalColor;
    21. }
    Make a copy of the energy shield with new shader




    At this point, our energy shield is complete!


    Source Project

    https://github.com/MagicStones23/Unity-Interactable-Energy-Shield

    Note: Some resources in this project are taken from the Internet, do not use them in your commercial project


    My YouTube Channel

    https://www.youtube.com/channel/UCBUXiYqkFy0g6V0mVH1kESw


    Follow me on Twitter

    https://twitter.com/MagicStone23