Shader graph: Scan effect

Using Unity 2019.2.1f1, LWRP 6.9.1 and Shader Graph 6.9.1. You can get the article’s code and shaders here.

Scan effect shader uses depth intersection to determine where to render the scan object’s mesh.

Depth intersection

When the camera renders a scene, it creates Depth texture, writing in it all opaque objects. Using this texture, you can get distance from scene geometry to camera. Scene Depth Node provides access to the Depth Texture in several different sampling modes. (For LWRP to access Depth texture, it must be enabled in the Pipeline Asset)

When you render your object, you can get the fragment’s distance from camera and compare it to the distance to scene geometry. The main trick is to compare Scene Depth node output and Position/Screen Position node output in the same space.(comparing value A in range 0..1 with value B in range near… far plane won’t make any sense.)

Unity’s built-in shaders for particles with enabled Soft Particles use view space depth intersection to fade out particles near scene geometry.

Particle Blend.shader does the Depth intersection in the following lines:

   v2f vert (appdata_t v)
            {
                ...
                #ifdef SOFTPARTICLES_ON
                o.projPos = ComputeScreenPos (o.vertex);
                COMPUTE_EYEDEPTH(o.projPos.z);
                #endif
                ...
            }

            UNITY_DECLARE_DEPTH_TEXTURE(_CameraDepthTexture);
            float _InvFade;

            fixed4 frag (v2f i) : SV_Target
            {
                #ifdef SOFTPARTICLES_ON
                float sceneZ = LinearEyeDepth (SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, UNITY_PROJ_COORD(i.projPos)));
                float partZ = i.projPos.z;
                float fade = saturate (_InvFade * (sceneZ-partZ));
                i.color *= fade;
                #endif
                ...
            }

ComputeScreenPos and COMPUTE_EYEDEPTH are declared in UnityCG.cginc. ComputeScreenPos doesn’t change Z and it still the same as in o.vertex. COMPUTE_EYEDEPTH transforms o.vertex from object space to view/eye space and returns the fragment distance in eye units.

#define COMPUTE_EYEDEPTH(o) o = -UnityObjectToViewPos( v.vertex ).z

LinearEyeDepth decodes sampled _CameraDepthTexture and returns distance to scene geometry in the same eye space.

In frag function, the line below makes the comparison of distances from camera to the fragment and scene. Then multiplies the fragment colour by fade, to make the fragment smoothly fade when it’s near scene geometry and entirely disappear when partZ equals sceneZ.

float fade = saturate (_InvFade * (sceneZ-partZ));
i.color *= fade;

Shader graph depth intersection

Below is view space depth intersection recreated in Shader graph. Also, there is used One Minus Node to swap 0 and 1, so the output is 1 where your object intersects with scene geometry. Power Node is used to change fall off of intersection from linear to make it look better.

Click to view full graph

Expanding scan

To make scan effect use the output of the previous graph as alpha and multiply the Intersection colour with the output.

Click to view full graph

I use generated in the previous tutorial sphere mesh and change its scale over time using the next script.

  void UpdateScale()
        {
            transform.localScale = Vector3.one * _maxScale * Mathf.InverseLerp(_startTime, _startTime + _lifeTime, Time.time);
        }

Scan from camera

By using almost the same shader graph, you can create scans moving away from the camera like Destiny 2.

Use a quad mesh that over time moves from camera position along camera forward. To make the quad cover full screen, regardless of distance from camera change its scale based on camera’s Frustum size at the given distance.

    void UpdatePositionAndScale()
        {
            float distance = _maxDistance * Mathf.InverseLerp(_startTime, _startTime + _lifeTime, Time.time);
            transform.position = _camera.transform.position + _camera.transform.forward * distance;
            transform.forward = _camera.transform.forward;

            float frustumHeight = 2.0f * distance * Mathf.Tan(_camera.fieldOfView * 0.5f * Mathf.Deg2Rad);
            float frustumWidth = frustumHeight * _camera.aspect;
            transform.localScale = new Vector3(frustumWidth, frustumHeight, 1);
        }

In the shader graph, the only difference is that you should decrease InversFade based on the distance of the quad from camera, to keep the same thickness of the scan.

Click to view full graph

You can get the article’s code and shaders here.

Leave a Reply