UnityShader后处理实现扫描效果+世界中任意位置向外扩散波纹

  • Post author:
  • Post category:其他




概述

实现环境:

  • Unity2020.3.26
  • 使用的管线是Built-In渲染管线
  • 需要一定Unity基础以及Shader基础

其实这个效果比较容易实现,比较难的点在于规定任意位置然后向外扩散波纹,这就涉及到怎么

把相机中Fragment的相机坐标转化成世界坐标

然后在Shader中使用。



基础原理

在这里插入图片描述

因为是后处理效果,所以关键的一步是

拿到fragment的世界坐标

,想要拿到这个坐标可以通过如上图过程拿到:

  • 首先拿到

    相机的世界坐标,也就是世界中心到相机的向量

    (图中黄色那条)
  • 每个fragment向远裁剪面发射线,拿到

    fragment射线方向
  • 通过深度图拿到每个

    fragment深度信息

  • fragment射线方向 * fragment深度信息 = 相机到fragment世界位置的向量

    (图中绿色那条)
  • 然后简单的向量相加,就可以直接拿到

    世界中心指向fragment世界坐标的向量

    (图中紫色那条),也就是

    fragment的世界坐标



实现



相机脚本

先上脚本具体代码:

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

public class ScannerEffect : MonoBehaviour
{
    //扫描的起点,扫描线材质(预处理材质),已扫描距离
    public Transform ScannerOrigin;
    public Material EffectMaterial;
    public float ScanDistance;
    public float ScanSpeed = 50;

    private Camera mCamera;
    private bool scanning;
    // Update is called once per frame
    void Update()
    {
        if (scanning)
        {
            ScanDistance += Time.deltaTime * ScanSpeed;
        }

        //按下R键重置距离
        if (Input.GetKeyDown(KeyCode.R))
        {
            scanning = true;
            ScanDistance = 0f;
        }

        //鼠标左键单击,射线检测获得世界坐标然后设置起点
        if (Input.GetMouseButtonDown(0))
        {
            Ray ray = mCamera.ScreenPointToRay(Input.mousePosition);
            RaycastHit hit;
            if (Physics.Raycast(ray, out hit))
            {
                scanning = true;
                ScanDistance = 4;
                ScannerOrigin.position = hit.point;
            }
        }
    }

    private void OnEnable()
    {
        mCamera = GetComponent<Camera>();
    }

    [ImageEffectOpaque]
    private void OnRenderImage(RenderTexture source, RenderTexture destination)
    {
        EffectMaterial.SetVector("_WorldSpaceScannerPos", ScannerOrigin.position);
        EffectMaterial.SetFloat("_ScanDistance", ScanDistance);
        RaycastCornerBlit(source, destination, EffectMaterial);
    }

    private void RaycastCornerBlit(RenderTexture source, RenderTexture destination, Material material)
    {
        // Compute Frustum Corners
        float camFar = mCamera.farClipPlane;
        float camFov = mCamera.fieldOfView;
        float camAspect = mCamera.aspect;

        float fovWHalf = camFov * 0.5f;

        Vector3 toRight = mCamera.transform.right * Mathf.Tan(fovWHalf * Mathf.Deg2Rad) * camAspect;
        Vector3 toTop = mCamera.transform.up * Mathf.Tan(fovWHalf * Mathf.Deg2Rad);

        Vector3 topLeft = (mCamera.transform.forward - toRight + toTop);
        float camScale = topLeft.magnitude * camFar;

        topLeft.Normalize();
        topLeft *= camScale;

        Vector3 topRight = (mCamera.transform.forward + toRight + toTop);
        topRight.Normalize();
        topRight *= camScale;

        Vector3 bottomRight = (mCamera.transform.forward + toRight - toTop);
        bottomRight.Normalize();
        bottomRight *= camScale;

        Vector3 bottomLeft = (mCamera.transform.forward - toRight - toTop);
        bottomLeft.Normalize();
        bottomLeft *= camScale;

        // Custom Blit, encoding Frustum Corners as additional Texture Coordinates
        RenderTexture.active = destination;

        material.SetTexture("_MainTex", source);

        GL.PushMatrix();
        GL.LoadOrtho();

        material.SetPass(0);

        GL.Begin(GL.QUADS);

        GL.MultiTexCoord2(0, 0.0f, 0.0f);
        GL.MultiTexCoord(1, bottomLeft);
        GL.Vertex3(0.0f, 0.0f, 0.0f);

        GL.MultiTexCoord2(0, 1.0f, 0.0f);
        GL.MultiTexCoord(1, bottomRight);
        GL.Vertex3(1.0f, 0.0f, 0.0f);

        GL.MultiTexCoord2(0, 1.0f, 1.0f);
        GL.MultiTexCoord(1, topRight);
        GL.Vertex3(1.0f, 1.0f, 0.0f);

        GL.MultiTexCoord2(0, 0.0f, 1.0f);
        GL.MultiTexCoord(1, topLeft);
        GL.Vertex3(0.0f, 1.0f, 0.0f);

        GL.End();
        GL.PopMatrix();
    }
}

首先这是一个

后处理

效果,所以是把当前屏幕设为MainTex进行采样处理,写一个脚本拿到单击屏幕的世界位置,并且设为扫描的扩散中心,并且把扫描边缘位置不断扩散:

在这里插入图片描述
在这里插入图片描述

进入

后处理

,传入Shader中需要脚本提供的参数(当前的扫描距离和扫描中心的

世界位置

):

在这里插入图片描述

脚本中最后一个函数的作用是把屏幕进行裁剪之后传入Shader进行处理:

在这里插入图片描述



具体Shader

Shader的具体代码:

Shader "OtherShader/ScannerShader"
{
    Properties
    {   
        _MainTex("Texture", 2D) = "white" {}
        _DetailTex("Texture", 2D) = "white" {}
        _ScanDistance("Scan Distance", float) = 0
        _ScanWidth("Scan Width", float) = 10
        _LeadSharp("Leading Edge Sharpness", float) = 10
        _LeadColor("Leading Edge Color", Color) = (1, 1, 1, 0)
        _MidColor("Mid Color", Color) = (1, 1, 1, 0)
        _TrailColor("Trail Color", Color) = (1, 1, 1, 0)
        _HBarColor("Horizontal Bar Color", Color) = (0.5, 0.5, 0.5, 0)
    }
    SubShader
    {
        Cull off
        ZWrite off
        ZTest Always

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
                float4 ray : TEXCOORD1;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
                float2 uv : TEXCOORD0;
                float2 uv_depth : TEXCOORD1;
                float4 interpolatedRay : TEXCOORD2;
            };

            float4 _MainTex_TexelSize;
            float4 _CameraWS;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                //o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                o.uv = v.uv.xy;
                o.uv_depth = v.uv.xy;

                //注意平台适配,如果跟dx一样从上开始的话就要把后处理的uv上下颠倒一下,即1minus
                #if UNITY_UV_STARTS_AT_TOP
                if (_MainTex_TexelSize.y < 0)
                    o.uv.y = 1 - o.uv.y;
                #endif				    

                o.interpolatedRay = v.ray;
                return o;
            }

            sampler2D _MainTex;
            sampler2D _DetailTex;
            sampler2D_float _CameraDepthTexture;
            float4 _WorldSpaceScannerPos;

            //视觉效果相关参数
            float _ScanDistance;
            float _ScanWidth;
            float _LeadSharp;
            float4 _LeadColor;
            float4 _MidColor;
            float4 _TrailColor;
            float4 _HBarColor;

            //用一个函数写横向纹理
            float4 horizBars(float2 p)
            {
                return 1 - saturate(round(abs(frac(p.y * 100) * 2)));
            }
            //用一个函数写横纵向纹理
            float4 horizTex(float2 p)
            {
                return tex2D(_DetailTex, float2(p.x * 30, p.y * 40));
            }

            float4 frag (v2f i) : SV_Target
            {
                float4 col = tex2D(_MainTex, i.uv);

                float rawDepth = DecodeFloatRG(tex2D(_CameraDepthTexture, i.uv_depth));
                float linearDepth = Linear01Depth(rawDepth);

                //linearDepth存储了每个fragment对于相机的深度信息
                //i.interpolatedRay是相机射到远裁剪面的射线,乘上深度信息就是wsDir,即相机到fragment的向量
                //_WorldSpaceCameraPos是世界原点到相机的向量,加上wsDir之后就是fragment的世界坐标

                float4 wsDir = linearDepth * i.interpolatedRay;
                float3 wsPos = _WorldSpaceCameraPos + wsDir;
                float4 scannerCol = float4(0, 0, 0, 0);

                float dist = distance(wsPos, _WorldSpaceScannerPos);

                //对扫描边界线进行渲染,值得注意的是linearDepth < 1,如果不加这一条的话会一路扫描到天空盒
                if (dist < _ScanDistance && dist > _ScanDistance - _ScanWidth && linearDepth < 1)
                {
                    float diff = 1 - (_ScanDistance - dist) / (_ScanWidth);
                    half4 edge = lerp(_MidColor, _LeadColor, pow(diff, _LeadSharp));
                    scannerCol = lerp(_TrailColor, edge, diff) + horizBars(i.uv) * _HBarColor;
                    scannerCol *= diff;
  
                }

                return col + scannerCol;
            }
            ENDCG
        }
    }
}


拿到fragment世界坐标

的代码:

在这里插入图片描述


深度

可以直接拿到,

相机的世界位置

也可以直接拿到,

相机射到远裁剪面的射线

也可以直接拿到,简单相加就可以拿到

fragment的世界位置

(wsPos)了。

然后就是根据需要写各种效果了:

在这里插入图片描述

这里我跟据坐标修改了一下,外圈位置不变,然后对于距离小于外圈位置的fragment进行一个关于波的函数的计算,就能拿到一个向外扩散的效果,

if中的参数

都是可以修改的,可以修改然后拿到自己满意的扩散效果:

在这里插入图片描述

在这里插入图片描述


总之拿到了fragment的世界坐标就可以在后处理做各种神奇的操作了!


还有如下根据位置实现一部分正常处理,一部分边缘检测效果的后处理效果(两个Pass就可以了):

在这里插入图片描述



具体代码

具体代码在个人github的shader合集中:


Raler’s Unity Shader Book



扫描效果Scan Effect



参考


油管大神的《无人深空》扫描效果实现



Shader202实现扫描



版权声明:本文为qq_51773145原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。