UnityRenderFeature application (simple scene scanning effect)

[Unity] RenderFeature application (simple scene scanning effect)

RenderFeature is a concept used for rendering graphics, usually used in graphics engines or game engines. It is a modular component that handles specific rendering functions such as shadows, lighting, particle effects, etc.

Click on the ground to generate an ever-expanding circular light effect, which is used to achieve some special effects on the screen.

Achieve results

1. Implementation method

1. Requirements and principles

The case is based on the unity urp rendering pipeline and uses RenderFeature post-processing effects.

In one sentence, the basic principle is to reconstruct the world space position of the pixel through the camera depth map, perform distance calculation on the space coordinates and the coordinates of the click point to draw a circle.

2. Implementation steps

1) Create UniversalRenderPipelineAsset

? You need to create a UniversalRenderPipelineAsset before writing RenderFeature

? Create→Randering→URPAsset (with Universal Render)

? UniversalRenderPipelineAsset and UniversalRenderData will be generated at the same time after creation

? Method to dynamically set the current UniversalRenderPipelineAsset (can also be set manually)

 //UniversalRenderPipelineAsset used
    public UniversalRenderPipelineAsset UniversalRenderPipelineAsset;
    void Start()
    {
        //Set the UniversalRenderPipelineAsset used in Graphics and Quality respectively.
        GraphicsSettings.renderPipelineAsset = UniversalRenderPipelineAsset;
        QualitySettings.renderPipeline = UniversalRenderPipelineAsset;
    }

2) Write RenderFeature

? Create RenderFeature. For detailed introduction, please refer to [Unity] RenderFeature Notes

?Create→Randering→RenderFeature

? The following is a general shader post-processing method, the difference is the parameter content and method

using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

public class ScanRenderPassFeature : ScriptableRendererFeature
{
    class CustomRenderPass : ScriptableRenderPass
    {
        public Material _Material;
        public Vector4 _Pos;//Click point
        public Color _Color;//Line color
        public float _Interval;//line spacing
        public float _Strength;//Strength range

        public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
        {
           // Matrix4x4 frustumCorners = GetFrustumCornersRay();
            CommandBuffer cmd = CommandBufferPool.Get("ScanRender");
            _Material.SetVector("_CentorPoint", _Pos);
            _Material.SetColor("_Color", _Color);
            _Material.SetFloat("_Interval", _Interval);
            _Material.SetFloat("_Strength", _Strength);
            cmd.Blit(colorAttachment, RenderTargetHandle.CameraTarget.Identifier(), _Material);
            //Execute CommandBuffer
            context.ExecuteCommandBuffer(cmd);
            //Recycle CommandBuffer
            CommandBufferPool.Release(cmd);
        }
    }

    CustomRenderPass m_ScriptablePass;
    public Shader ScanShader;
    public Vector4 Pos;
    public Color Color;
    public float Interval;//spacing
    public float Strength;//strength
    /// <inheritdoc/>
    public override void Create()
    {
        m_ScriptablePass = new CustomRenderPass();
        m_ScriptablePass._Material = new Material(ScanShader);
        m_ScriptablePass._Pos = Pos;
        m_ScriptablePass._Color = Color;
        m_ScriptablePass._Interval = Interval;
        m_ScriptablePass._Strength = Strength;
        // Configures where the render pass should be injected.
        m_ScriptablePass.renderPassEvent = RenderPassEvent.AfterRendering;
    }
    public void SetParam()
    {
        m_ScriptablePass._Pos = Pos;
        m_ScriptablePass._Strength = Strength;
    }
    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        renderer.EnqueuePass(m_ScriptablePass);
    }
}

3) Write shader

? The shader needs to implement two functions. One is to reconstruct the world space position of the pixel from the depth map, and the other is to draw an expansion circle based on the space position.

? To reconstruct the world space position of the pixel, please refer to the official case

? Reconstruct the world space position of a pixel from a depth texture |URP |7.7.1 (unity3d.com)

? Core methods:

? ComputeWorldSpacePosition: is a function used to calculate the position of an object in world space. It is usually used to calculate the position of objects in the game in the world coordinate system.

? SampleSceneDepth: used to obtain depth images. It is mainly used to implement some basic depth-related functions, such as observing the depth information of objects in the scene, calculating the distance between objects, etc.

? The following is modified in conjunction with the official method.

Shader "Unlit/ScanShaderURP"
{<!-- -->
    Properties
    {<!-- -->
        _CentorPoint("CentrePoint",Vector) = (0, 0, 0, 0)
        _Color("color",Color) = (1,1,1,1) //Color
    }
    SubShader
    {<!-- -->
        Tags {<!-- --> "RenderType"="Opaque" }
        LOD 100

        Pass
        {<!-- -->
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment fragment
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareDepthTexture.hlsl"
          
           struct Attributes
            {<!-- -->
            float4 positionOS : POSITION;
        };

        struct Varyings
        {<!-- -->
            float4 positionHCS : SV_POSITION;
        };

            float4x4 _FrustumCornersRay;
            float _Interval;//spacing
            float _Strength;//Strength
            sampler2D _CameraColorTexture;
            float4 _CentorPoint;
            float4 _Color;
            Varyings vert(Attributes IN)
            {<!-- -->
                Varyings OUT;
                OUT.positionHCS = TransformObjectToHClip(IN.positionOS.xyz);
                return OUT;
            }
            half4 frag(Varyings IN) : SV_Target
            {<!-- -->
                 //uv transformation
                float2x2 muls = float2x2(-1, 0, 0, 1);
                float2 centerUV = float2(1, 0);
                float2 UV =1-( mul(( IN.positionHCS.xy / _ScaledScreenParams.xy), muls) + centerUV);
                //Get the depth map
#if UNITY_REVERSED_Z
                real depth = SampleSceneDepth(UV);
#else
                real depth = lerp(UNITY_NEAR_CLIP_VALUE, 1, SampleSceneDepth(UV));
#endif
                //Get the world coordinate position
                float3 worldPos = ComputeWorldSpacePosition(UV, depth, UNITY_MATRIX_I_VP);
                half4 col2 = tex2D(_CameraColorTexture, UV);
                float lerpValue = 0;
                //Shield the space outside the section
                if (depth < _ProjectionParams.z - 1) {<!-- -->
                    float Mul = distance(_CentorPoint.xyz, worldPos.xyz);
                    //change the distance of the control circle
                    float change = _Strength;
                //The value of Mul must be greater than 0
                //The value of the first smoothstep less than change is clipped to 0, and the value greater than _Interval + change is 1
                //The second smoothstep is 1 if it is greater than _Interval + change, and 0 if it is less than _Interval + change
                //Subtract the two smoothsteps to get the change from 0 to 1 between 0 + change and _Interval + change, and the rest are 0
                    float lerp1 = smoothstep(0 + change, _Interval + change, Mul);
                    float lerp2 = smoothstep(_Interval + change, _Interval + change, Mul);
                    float dis = lerp1 - lerp2;
                    lerpValue = dis;
                }
                half4 myCol = lerp(col2, _Color, lerpValue);
                return myCol;
            }
                ENDHLSL
        }
    }
}

4) Control method

using System.Linq;
using UnityEngine;
using UnityEngine.Rendering.Universal;

public class ScanControl : MonoBehaviour
{
    public UniversalRendererData renderData;
    ScanRenderPassFeature custom;
    private void Start()
    {
      custom = renderData.rendererFeatures.OfType<ScanRenderPassFeature>().FirstOrDefault();
    }
    private void Update()
    {
        if (Input.GetMouseButtonDown(1))
        {
            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            RaycastHit hit;
            if (Physics.Raycast(ray, out hit))
            {
                Vector3 vector = hit.point;
                Vector4 vector4 = new Vector4(vector.x, vector.y, vector.z, 1);
                custom.Pos = vector4;
                custom.Strength = 0;
            }
        }
    }
    private void LateUpdate()
    {
        custom.Strength + = Time.deltaTime*10;
        custom.SetParam();
    }
}

3. Implementation method in built-in

shader

Shader "Unlit/ScanShaderBuiltIn"
{<!-- -->
    Properties
    {<!-- -->

        _MainTex("Base (RGB)", 2D) = "white" {<!-- -->} // Main texture
        _CentorPoint("CentrePoint",Vector) = (0, 0, 0, 0)
        _Color("color",Color) = (1,1,1,1) //Color, usually fixed4
        _InverseZ("InverseZ", Float) = -1
    }
    SubShader
    {<!-- -->
        Tags {<!-- --> "RenderType"="Opaque" }
        LOD 100

        Pass
        {<!-- -->
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment fragment
            #include "UnityCG.cginc"

            struct appdata
            {<!-- -->
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {<!-- -->
                float2 uv : TEXCOORD0;
                float4 pos : SV_POSITION;
                float3 viewVec :TEXCOORD1;
            };
            sampler2D _MainTex; // Main texture
            float _Interval;//spacing
            float _Strength;//Strength
            float _InverseZ;
            sampler2D _CameraColorTexture;
            sampler2D _CameraDepthTexture;
            float4 _CentorPoint;
            fixed4 _Color;
            v2f vert (appdata v)
            {<!-- -->
                v2f o;
                o.uv = v.uv;
                o.pos = UnityObjectToClipPos(v.vertex); //MVP transformation
                float4 screenPos = ComputeScreenPos(o.pos); // Calculate the screen coordinates in "homogeneous space"
               // float4 ndcPos = (screenPos / screenPos.w) * 2 - 1; //Screen coordinates --->ndc coordinate transformation formula
               // float4 ndcPos = o.pos / o.pos.w; // Manual perspective division
                float3 ndcPos = float3(o.uv.xy * 2.0 - 1.0, 1); //Directly map uv to ndc coordinates
                float far = _ProjectionParams.z; //Get the z value of the projection information, which represents the far plane distance
                float3 clipVec = float3(ndcPos.x, ndcPos.y, ndcPos.z * _InverseZ) * far; //View frustum vertex coordinates in clipping space
                o.viewVec = mul(unity_CameraInvProjection, clipVec.xyzz).xyz; //View frustum vector in observation space
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {<!-- -->
                fixed4 col2 = tex2D(_MainTex, i.uv);
           // fixed4 col2 = tex2D(_CameraColorTexture, i.uv);
                float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture,i.uv);//Sampling depth map
                depth = Linear01Depth(depth); //Convert to linear depth
                float3 viewPos = i.viewVec * depth; //Get the actual observation space coordinates (after interpolation)
                float3 worldPos = mul(unity_CameraToWorld, float4(viewPos,1)).xyz; //Observation space-->World space coordinates
                float factor = 0;
                if (depth < _ProjectionParams.z - 1) {<!-- -->
                    float Mul = distance(_CentorPoint.xyz, worldPos.xyz);
                    float change = _Strength;
                    //The value of Mul must be greater than 0
                   //The value of the first smoothstep less than change is clipped to 0, and the value greater than _Interval + change is 1
                   //The second smoothstep is 1 if it is greater than _Interval + change, and 0 if it is less than _Interval + change
                   //Subtract the two smoothsteps to get the change from 0 to 1 between 0 + change and _Interval + change, and the rest are 0
                    float lerp1 = smoothstep(0 + change, _Interval + change, Mul);
                    float lerp2 = smoothstep(_Interval + change, _Interval + change, Mul);
                    float dis = lerp1 - lerp2;
                    float lerpDis = smoothstep(0.99, 1, dis);
                    factor = dis;
                }
                fixed4 myCol = lerp(col2, _Color, factor);
                return myCol;
         
            }
            ENDCG
        }
    }
}

control script

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

public class ScanManager : MonoBehaviour
{
    [SerializeField]
    private bool enableWave = false; // Whether to enable scanning effects
    [SerializeField]
    private Shader scanShader;
    private Material material = null; // material
     Vector4 _Pos=Vector4.zero;
    public Color _Color;
     float _Interval=1;//spacing
     float _Strength=0; //Strength
    // Start is called before the first frame update
    void Start()
    {
        material = new Material(scanShader);
    }

    // Update is called once per frame
    void Update()
    {
        if (Input.GetMouseButtonDown(1))
        {
            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            RaycastHit hit;
            if (Physics.Raycast(ray, out hit))
            {
                Vector3 vector = hit.point;
                Vector4 vector4 = new Vector4(vector.x, vector.y, vector.z, 1);
                Debug.Log(vector4);
                _Pos = vector4;
                _Strength = 0;
                enableWave = true;
            }
        }
    }

    private void OnRenderImage(RenderTexture src, RenderTexture dest)
    {
     
        if(enableWave)
        {
            _Strength + = Time.deltaTime * 10;
            material.SetVector("_CentorPoint", _Pos);
            material.SetColor("_Color", _Color);
            material.SetFloat("_Interval", _Interval);
            material.SetFloat("_Strength", _Strength);
            Graphics.Blit(src, dest, material);
        }
        else
        {
            Graphics.Blit(src, dest);
        }
    }


}