top of page
Writer's picturesantosh nalla

Heat Wave Distortion Effect in Unity(Using CG)

Imagine sitting in front of a campfire. When you observe the person sitting opposite

to you on the other side of the campfire through the heat rising from the fire, you see

a distortion effect. Today, let us replicate that effect in Unity using shader CG

programming.


Final Effect:



Firstly, before going into it, let me answer a question- What is grab pass?


Grab Pass is a special pass of a shader that is used to get the contents of the screen in place where the object would be rendered. Hence, it projects whatever is present behind the object, in the form of a texture. It is like all the content behind that object is stored in the form of a texture


Grab pass is just an other pass, which is called first in our shader.

Now the question is – How do you implement it?


Before your default pass in the shader, include grab pass


SubShader
 {
 Tags { "Queue" = "Transparent" }
 
 GrabPass
 {
 "_GrabTexture"
 }
 
 Pass
 {
 CGPROGRAM
 .
 .
 .
 .
 sampler2D _GrabTexture;
 .
 .
 .
 .
 

As mentioned earlier, this grab pass is called for every pass, and the texture behind the object is stored in "_GrabTexture" property (Of course you have to declare _GrabTexture property).


Allow me to explain grab pass in simple terms. Consider a plane and apply the grab pass shader material to it. In that shader, return the grab pass texture (_GrabTexture) in fragment shader as output. As previously mentioned, grab pass returns the scene present behind the object, so the plane now becomes transparent.


For the above effect of making the plane transparent using grab pass, a code is written as follows:


Shader "ShaderLearning/GrabPassIntroduction"
{
 SubShader
 {
 Tags { "Queue" = "Transparent" }
 
 GrabPass
 {
 "_GrabTexture"
 }
 
 Pass
 {
 CGPROGRAM
 #pragma vertex vert
 #pragma fragment frag
 
 #include "UnityCG.cginc"
 
 
 sampler2D _GrabTexture;
 
 struct appdata
 {
 float4 vertex : POSITION;
 float2 uv : TEXCOORD0;
 };
 
 struct v2f
 {
 float4 uv : TEXCOORD0;
 float4 vertex : SV_POSITION;
 };
 
 v2f vert (appdata v)
 {
    v2f o;
    o.vertex = UnityObjectToClipPos(v.vertex);
    
    o.uv = ComputeGrabScreenPos(o.vertex);
    
    return o;
 }
 

 
 fixed4 frag (v2f i) : SV_Target
 {
     return(tex2Dproj(_GrabTexture, i.uv));
 }
 ENDCG
 }
 }
} 

What is happening in the above code? (Remember we are applying this shader material to a plane inorder to make that plane transparent)


As mentioned earlier, as grab pass is getting executed first, we are getting the texture/Scene Image behind the object. The same Scene Image/Texture is getting stored in Grab Pass texture that is "_GrabTexture" for every pass (a pass is executed multiple times every frame).


To return the grab pass texture co-ordinates in fragment shader, we need to sample the texture first. To sample any texture, we need 2 variables - the actual Texture and uv coordinates. We are already getting the actual texture in _GrabTexture property through our grab pass. Then, what about the uv coordinates? We get these uv coordinates in vertex fragment using o.uv = ComputeGrabScreenPos(o.vertex), where o.vertex is in the projection space (after multiplying with MVP matrix).

Finally, ComputeGrabPass() computes texture coordinate for sampling a GrabPass texture. Input is clip space position.


Now that you have understood what a grab pass does, let us try to think about how to implement that fire distortion effect.


The procedure I follow is,

  1. Take a plane, and attach a lookAt script to the plane, so that the plane always looks at our main camera.


2. Now to this plane, attach fire distortion effect shader material.


Now the next question is how is that effect achieved. This is pretty simple now as you have understood what grab pass is. Apply the same grab pass code, get grab pass texture behind the object, and dynamically move those vertices in sin/cos wave with _Time.y and get that nice distortion effect when a player looks at any object through that plane near that camp.


Full Code:

To test it, create a simple unlit shader, copy the below code on to it. Create a Material and apply this shader to that material. Lastly create a Plane, place it in front of the fire and attach a lookAt script to that plane.


Shader "ShaderLearning/HeatDistortionEffect"
{
    Properties
    {
        _Noise("Noise Texture", 2D) = "white" {}

        _DistotionStrength("Distort Strength", float) = 1.0

        _DistortionSpeed("Distort Speed", float) = 1.0
    }
    SubShader
    {
       

        // Grab the screen behind the object into _BackgroundTexture
        GrabPass
        {
            "_BackgroundTexture"
        }


        Pass
        {
            ZTest Always

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
              
            #include "UnityCG.cginc"
      

             sampler2D _BackgroundTexture;
             sampler2D _Noise;

             float _DistotionStrength;
             float _DistortionSpeed;

             struct vertexInput
            {
                float4 vertex : POSITION;
                float3 texCoord : TEXCOORD0;
            };

            struct vertexOutput
            {
                float4 pos : SV_POSITION;
                float4 grabPos : TEXCOORD0;
            };



            vertexOutput vert(vertexInput input)
            {
                vertexOutput output;

                output.pos = UnityObjectToClipPos(input.vertex);

                output.grabPos = ComputeGrabScreenPos(output.pos);

                //Applying actual distortion effect to the _grabTexture Vertices so they move dynamically and give that effect
                float noise = tex2Dlod(_Noise, float4(input.texCoord, 0)).rgb;
                output.grabPos.x += cos(noise * _Time.x * _DistortionSpeed) * _DistotionStrength;
                output.grabPos.y += sin(noise * _Time.x * _DistortionSpeed) * _DistotionStrength;
                
                return output;
            }

            float4 frag(vertexOutput input) : COLOR
            {
                //Sampling _GrabTexture
                return tex2Dproj(_BackgroundTexture, input.grabPos);
            }
            ENDCG
        }
    }
}


LookAtScript(Attached To Plane)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class LookAtScript : MonoBehaviour
{
    [SerializeField] GameObject _mainCam;  //Drag and drop you player main camera here
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        transform.LookAt(_mainCam.transform);
    }
}




222 views0 comments

Recent Posts

See All

Comments


bottom of page