top of page
Search
  • alexlpc2015

[Shader Notes] #01 - Force Field

Hi and welcome to my Shader Notes Series!

This is a comprehensive project with notes about shaders of nearly every common effect included (implemented in Unity). I will explain the key points and theories behind them for you to understand them thoroughly and then enjoy creating your own shaders with proficiency.

Let's start learning together!


#01 - Force Field

Difficulty: 6/10

Points covered:

  • Alpha Blending

  • Depth Detection

  • Rim Fresnel Effect

  • Texture Animation

  • Collision Interaction

This is the very first effect of the series. It is a simple force field or transparent shield shader. It covers some of the basic yet important points in writing shaders. Without further ado, let's begin!


Final effect



Setup:

Create a new material and attach it to a sphere.

Prepare the properties and declare them in CGPROGRAM. These will be explained later.

    //Properties
    _MainTex ("Texture", 2D) = "white" {}
    _Noise ("Noise", 2D) = "white" {}
    _Color ("Color", Color) = (1,1,1,1)
    _NoiseColor ("NoiseColor", Color) = (1,1,1,0.2)
    _RimColor ("RimColor", Color) = (0,0,0,1)
    _Threshold ("Threshold", float) =2
    _Power ("Power", float) =5
    _Fresnel ("Fresnel", float) =1
    _NoiseStrength("Noise Strength", float) = 1
    _NoiseSpeed("Noise Speed", float) = 1

In vertex shader, calculate some of the needed data.

    v2f o;
    o.vertex = UnityObjectToClipPos(v.vertex);
    o.uv = TRANSFORM_TEX(v.uv, _MainTex);
    o.nuv = TRANSFORM_TEX(v.uv, _Noise);
    
    o.worldNormal = mul(unity_ObjectToWorld, v.normal);
    o.worldPos = mul(unity_ObjectToWorld, v.vertex);
    o.worldView =float4(normalize( _WorldSpaceCameraPos.xyz - 
    o.worldPos.xyz), 0);

In fragment shader, sample the main texture and multiply it by the tint color.

    fixed4 col = tex2D(_MainTex, i.uv) * _Color;

Point #1: Alpha Blending

Recall: in order to enable transparent rendering, we have to set the "Queue", "RenderMode", and "Blend" options.

    Tags {"Queue"="Transparent" "RenderType"="Transparent"}
    Blend SrcAlpha OneMinusSrcAlpha

These settings are standard for transparent shaders. Because the force field should be seen from both sides, we should also turn culling and depth writing off.

    ZWrite Off
    Cull Off

Point #2: Depth Detection

To achieve the following effect of having "glowing light" at the borders between the effect and other objects, we can obtain the depth information of both of the objects for comparison.

This technique is widely used in various scenarios, such as water surfaces, zones, etc.


To obtain depth information, we need to first get the screen position in the vertex shader and then get the eye depth from that screen position.

  o.scrPos = ComputeScreenPos(o.vertex);
  COMPUTE_EYEDEPTH(o.scrPos.z);

  //#define COMPUTE_EYEDEPTH(o) o =-mul( UNITY_MATRIX_MV, v.vertex )
  //computes eye space depth of the vertex and outputs to o


The result, which is the depth of the scene objects the force field touches, will be stored inside o.scrPos.z, because screenPos only needs two components.

Now look at the fragment shader.

We need to get both the scene depth and the self depth in order to calculate their difference. For the scene depth, we need to first sample the depth buffer value (0,1 for near, far planes) from the camera depth texture and convert it to world space values (x for x units along camera's z axis). This conversion is done by the LinearEyeDepth function.

sampler2D _CameraDepthTexture;
//...
float sceneZ = LinearEyeDepth(
							 SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, i.scrPos)
							 );

//SAMPLE_DEPTH_TEXTURE_PROJ divides the first two components of i.scrPos 
//by its third component.

Then, we can get the self depth simply by i.scrPos.w.

float selfZ = i.scrPos.w;

Now that we have both of the depth values in our hands, we can calculate their difference and blend colors based on that value. Note that the power function is responsible for the gradient effect.

float diff =min(pow((1-saturate(sceneZ-selfZ))/_Threshold,_Power), 1.0);
col = lerp(col, _RimColor, diff)

Point #3: Rim Fresnel

Using depth detection, we added glowing effect on the borders with other scene objects. We now ne

ed to apply the same effect to the outer bounds to create a full visual effect.


We can use the dot product of normal vector and view direction to achieve this goal. As the angle between these two vectors approach 90 degrees, the fresnel value becomes larger. Then use the fresnel value to blend colors.

float fresnel = pow(1 - abs(dot(normalize(i.worldNormal),normalize(i.worldView))) , _Fresnel);
col = lerp(col, _RimColor, fresnel);

Point #4: Texture Animation

Now it's time to give our force field a texture and make it move!

Basically, sample the noise texture and give it a repeating pattern using the _Time variable.

col *= lerp(col, fixed4(1,1,1,1), saturate(sin(_Time.y * 3)));
col *= saturate(tex2D(_Noise, i.nuv + frac(_Time.x * _NoiseSpeed)) / (1/_NoiseStrength)) * _NoiseColor;

You can try all sorts of movement patterns here :)

Point #5: Collision Interaction

With the main body of the force field created, we can now add some interactive features to it. When a collision is detected on its surface, a hole with glowing borders will appear on the force field.



First, we need to declare some variables for the position and radius of the hole.

float4 _HitPoint;
float _HitRadius = 1.0;

Then go to the editor and attach a collider and a rigidbody component to our force field. Then give it a simple C# script for passing in the variables when a collision happens.

public class ForceField : MonoBehaviour
{
    Material mat;
    Shader shader;

    public float hitRadius = 1.0f;

    private void OnEnable()
    {
        mat = GetComponent<MeshRenderer>().sharedMaterial;
        shader = mat.shader;
    }

    private void OnCollisionEnter(Collision collision)
    {
        mat.SetVector("_HitPoint", collision.contacts[0].point);
        mat.SetFloat("_HitRadius", hitRadius);
    }
}

Inside the fragment shader, calculate the distance from each pixel to the contact point and use that distance to blend colors. Note that the power function is responsible for the gradient effect.

float dist = Distance(i.worldPos, _HitPoint) / _HitRadius;
if(dist <= 1)
    return fixed4(0,0,0,0);
col = lerp(col, _RimColor,  pow(1- saturate((dist - 1)/_Threshold * 2), _Power) );

Now drop a ball with collider and a hole will appear on the force field!


Full Shader Code:

Shader "AlexLiu/Field_0"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _Noise ("Noise", 2D) = "white" {}
        _Color ("Color", Color) = (1,1,1,1)
        _NoiseColor ("NoiseColor", Color) = (1,1,1,0.2)
        _RimColor ("RimColor", Color) = (0,0,0,1)
        _Threshold ("Threshold", float) =2
        _Power ("Power", float) =5
        _Fresnel ("Fresnel", float) =1
        _NoiseStrength("Noise Strength", float) = 1
        _NoiseSpeed("Noise Speed", float) = 1
}
    SubShader
    {
       Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "DisableBatching"="True"}
        Blend SrcAlpha OneMinusSrcAlpha
        ZWrite Off
	    Cull Off
        LOD 100
    
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
                float3 normal : NORMAL;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float2 nuv : TEXCOORD1;
                float4 vertex : SV_POSITION;
                float4 scrPos : POSITION1;
                float4 worldNormal : POSITION2;
                float4 worldView : POSITION3;
                float4 worldPos : POSITION4;
            };

            sampler2D _MainTex;
            sampler2D _Noise;
            sampler2D _CameraDepthTexture;
            float4 _MainTex_ST;
            float4 _Noise_ST;
            fixed4 _Color;
            fixed4 _RimColor;
            fixed4 _NoiseColor;
            float _Threshold;
            float _Fresnel;
            float _Power;
            float _NoiseStrength;
            float _NoiseSpeed;

            float4 _HitPoint;
            float _HitRadius = 1;

            float Distance (float4 v1, float4 v2){
                return sqrt(pow(v1.x-v2.x, 2)+pow(v1.y-v2.y, 2)+pow(v1.z-v2.z, 2));
            }            

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                o.nuv = TRANSFORM_TEX(v.uv, _Noise);
                
                o.worldNormal = mul(unity_ObjectToWorld, v.normal);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex);
                o.worldView =float4(normalize( _WorldSpaceCameraPos.xyz - o.worldPos.xyz), 0);

                o.scrPos = ComputeScreenPos(o.vertex);
                COMPUTE_EYEDEPTH(o.scrPos.z);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = tex2D(_MainTex, i.uv) * _Color;
                
                col *= lerp(col, fixed4(1,1,1,1), saturate(sin(_Time.y * 3)));
                col *= saturate(tex2D(_Noise, i.nuv + frac(_Time.x * _NoiseSpeed)) / (1/_NoiseStrength)) * _NoiseColor;

                //Contact
                float dist = Distance(i.worldPos, _HitPoint) / _HitRadius;
                if(dist <= 1)
                    return fixed4(0,0,0,0);
                col = lerp(col, _RimColor,  pow(1- saturate((dist - 1)/_Threshold * 2), _Power) );                

                //Rim
                float fresnel = pow(1 - abs(dot(normalize(i.worldNormal),normalize(i.worldView))) , _Fresnel);
                col = lerp(col, _RimColor, fresnel);

                // Scene contact
                float sceneZ = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, i.scrPos));
                float selfZ = i.scrPos.w;
                float diff =min(pow((1-saturate(sceneZ-selfZ))/_Threshold,_Power), 1.0);
                col = lerp(col,_RimColor, diff);

                return col;
            }
            ENDCG
        }
    }
}


Thank you for reading!



30 views0 comments

Recent Posts

See All
Post: Blog2_Post
bottom of page