top of page
Writer's picturesantosh nalla

Environment Scanner Shader Effect

Updated: Feb 7, 2022

I have been playing Returnal lately.

Area Scan is a device and action of Selena in Returnal. It is used in combination with the map to locate Selene's path through the levels and to follow the main objective. So this is a pretty handy device we use in game to scan our environment.


I reconstructed the same effect in Unity.

This effect can be easily achieved in Post Processing using Depth Texture.

So what is Post Processing? Have a look at this blog!!


What is depth Texture?

Depth is a term used in computer graphics to refer to how far a fragment (a potential pixel) is from the camera.

For every frame, camera generated a Depth Texture, which stores the depth information of every pixel rendered at that particular frame.

Pixel values in the depth texture range between 0 and 1, with a non-linear distribution.


We can access the depth texture like any other texture in our shader. The name has to be _CameraDepthNormalsTexture in order for Unity to find it.


Lets say you are playing a game and at one particular frame, this is an image snippet rendered by the camera.

Since the pixel values in the depth texture range between 0 and 1, with a non-linear distribution, let me show this to you using an image as depth texture.


You see a street light and a building on the image. Now since you see the building behind the street light, every pixel rendering street light will have depth value less than pixels rendering the building(this is because the building depth is greater than the depth of street light)


This is how the pixel depth values are stored every frame.


Lets come back to shader code!!












In order to work with the depth texture, we need a C# script in which we set the camera’s depthTextureMode to DepthTextureMode.DepthNormals.

Depth textures can come directly from the actual depth buffer, or be rendered in a separate pass, depending on the rendering path used and the hardware.


Typically when using Deferred rendering path, the depth textures come “for free” since they are a product of the G-buffer rendering anyway.

But, when you are using a Forward Rendering Path, make sure to attach a C# script to Main Camera, in which we set the camera's depthTextureMode to DepthTextureMode.DepthNormals.


For my example I am using Deferred rendering path(We can change this in camera's inspector tab)
















If you are using forward, include this in Awake()/Start() function.

GetComponent<Camera>.depthTextureMode = DepthTextureMode.Depth;

Now that you have access for the Depth Texture, you can access it in your shader code like any other texture using _CameraDepthNormalsTexture .

sampler2D _MainTex;
sampler2D _CameraDepthTexture;

_MainText is main diffuse shader.

_CameraDepthTexture stores the depth value of each pixel rendered,

whereas _MainText stores the image itself you see on the screen for every frame. This texture is refreshed every frame by our main camera.


The depth value in the depth texture are stored in r channel. So when you sample the _CameraDepthTexture, you take the value in the r channel for depth values of each pixel.

float depth = tex2D(_CameraDepthTexture, i.uv).r;

Pixel values in the Depth Texture range between 0 and 1, with a non-linear distribution. Precision is usually 32 or 16 bits, depending on configuration and platform used.

Having things in the [0,1] spectrum is amazingly useful. In order to get the camera’s depth in a [0,1] spectrum Unity gives us the “Linear01Depth” method.

depth = Linear01Depth(depth); 

Now since depth, is in the range of 0 to 1, lets multiply this with camera far clip value.

So when depth = 0, it mean the object is located at camera position and

when depth = Camera Far clip Value, object is located at farthest distance from the camera.

depth = depth * _ProjectionParams.z;

all this into one single statement, can be written as

float depth = Linear01Depth(tex2D(_CameraDepthTexture, i.uv).r) * _ProjectionParams.z;

Until now, our fragment shader is looing like this,

fixed4 frag(vertexOutput i) : SV_TARGET
	{
     float depth = tex2D(_CameraDepthTexture, i.uv).r;
	depth = Linear01Depth(depth) * _ProjectionParams.z;
 	return depth;
	}

This code does nothing, apart from giving black and white pixel outputs, based on depth values of pixels rendered.


We want a scan wave. How do we generate that?

If you observe the output video, the wave generated is radiating outward. Stating from the center, radius of the wave keeps increasing. And depending on the radius value, all the pixels which have depth value of that particular radius are colored blue. The radius keeps increasing with time and this generates a wave outward.


Now we need to declare 3 more variables called,

_ScanDistance ("Scan Distance from the Player", Range(0,200)) = 0
_ScanLength ("Length of the Scan", Range(-100,100)) = 1
_ScanColor ("Color", Color) = (1,0,0,1)

_ScanDistance acts as radius as mentioned. We increase this value over time via a C# script.


_ScanLenght is thickness of the wave generated


_ScanColor is color of the wave.


Now lets generate a scan wave:


This is probably straightforward code,

if(_ScanDistance > depth && depth > _ScanDistance - _ScanLength)
	{
	//Scan Part!!
		_CurrentDepth = depth;
		return _ScanColor;
	}
	
	//Non Scan Part!!
else
		return tex2D(_MainTex, i.uv);

Increase _ScanDistance from the inspector, you get a nice scan wave of given _ScanLenght thickness.



That doesn't look good, right? Giving a solid scan color for a wave is a bad idea.

Lets multiply the pixel color value with our scan color.


if(_ScanDistance > depth && depth > _ScanDistance - _ScanLength)
	{
		_CurrentDepth = depth;
		return tex2D(_MainTex, i.uv) * _ScanColor;
	}
else
		return tex2D(_MainTex, i.uv);

Lets also give a texture to this scan effect.













if(_ScanDistance > depth && depth > _ScanDistance - _ScanLength)
	{
_CurrentDepth = depth;
return tex2D(_MainTex, i.uv) * _ScanColor  * tex2D(_ScanTexture, i.uv);
	}
else
return tex2D(_MainTex, i.uv);

FINAL OUTPUT:


Complete Code:

Shader "Unlit/WorldScan"
{
    Properties
	{
		[HideinInspector]
		_MainTex("Basic Texture", 2D) = "white" {}
		_ScanDistance ("Scan Distance from the Player", Range(0,200)) = 0
        _ScanLength ("Length of the Scan", Range(-100,100)) = 1
        _ScanColor ("Color", Color) = (1,0,0,1)
		_ScanTexture("Basic Texture", 2D) = "white" {}
	}


		Subshader
		{
			  Pass
				{
				CGPROGRAM

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

				   //In built Texture info, Where every frame rendered on screen is store.
				   sampler2D _MainTex;
				   //In built Texture info, where depth values are stored!!
				   sampler2D _CameraDepthTexture;

				   uniform sampler2D _ScanTexture;
				   float _ScanDistance;
				   float _ScanLength;
				   float4 _ScanColor;

					struct vertexInput
					{
						float4 vertex: POSITION;
						float2 uv: TEXCOORD0;
					};

					struct vertexOutput
					{
						float4 pos : SV_POSITION;
						float2 uv: TEXCOORD0;
					};

	vertexOutput vert(vertexInput v)
	{
		vertexOutput o;
		o.pos = UnityObjectToClipPos(v.vertex);
		o.uv = v.uv;
		return o;
	}


	fixed4 frag(vertexOutput i) : SV_TARGET
	{
		float depth = tex2D(_CameraDepthTexture, i.uv).r;
		depth = Linear01Depth(depth) * _ProjectionParams.z;

		if(_ScanDistance > depth && depth > _ScanDistance - _ScanLength)
						{
		   return tex2D(_MainTex, i.uv) * _ScanColor * tex2D(_ScanTexture, i.uv);
						}
		else
		   return tex2D(_MainTex, i.uv);
					}

				   ENDCG
	 }

		}
}


I am actually scanning on holding down "L" key!!

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

public class Scan : MonoBehaviour
{
    [SerializeField] private Material PP;
    [SerializeField] private float scanSpeed;
    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        if(Input.GetKey(KeyCode.L))
        {
            PP.SetFloat("_ScanDistance", Mathf.Lerp(PP.GetFloat("_ScanDistance"), 200f, Time.deltaTime * scanSpeed));
        }   
        else
        {
            PP.SetFloat("_ScanDistance", 0);
        }
    }
}


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

public class MainCameraScriptPP : MonoBehaviour
{
    public Material PostProcessingMaterial;
    private void OnRenderImage(RenderTexture source, RenderTexture destination)
    {
        Graphics.Blit(source, destination, PostProcessingMaterial);
    }
}


147 views0 comments

Recent Posts

See All

Comments


bottom of page