【转载】Unity Shader 锥形扫描遮挡效果

546 阅读4分钟

原文链接

版权声明:本文为CSDN博主「Unity小林」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

原文链接:blog.csdn.net/weixin_4383…

正文

效果图如下:

20200609230648817.gif

扫描着色器

新建 shader,代码如下:

Shader "Unlit/ConeScan"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
		_Color("Color",Color)=(1,1,1,1)//颜色
		_StrongFloat("_StrongFloat",float)=0.1//增强圆形边缘效果的值
		_AlphaDownFloat("_AlphaDownFloat",float)=0.2//降低锥形区域外的alpha
		_Angle("Angle",float)=25//25*2度角的锥形
		_GradientFloat("_GradientFloat",float)=0.3//渐变半圆弧的颜色
    }
    SubShader
    {
	Blend SrcAlpha OneMinusSrcAlpha
        Tags { "RenderType"="Transparent" "Queue"="Transparent" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

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

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
			fixed4 _Color;
			float _StrongFloat;
			float _AlphaDownFloat;
			float _Angle;
			float _GradientFloat;
			uniform float _FloatArray[256];

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {

                fixed4 col;
                //圆弧
                float2 uv=i.uv;
                uv.x=uv.x-0.5;
                uv.y=uv.y-0.5;
                //i.uv=i.uv-float2(0.5,0.5);
                //根据 UV 来计算出一个变化区域,一个以 UV 中心点为中心,半径为 0.5 的圆形,圆形内到外从1渐变到0
                fixed dis=1.0-sqrt(uv.x*uv.x+uv.y*uv.y)*2;
                //中心点向周围发射的向量(归一化)
                fixed2 fragmentDir=normalize(uv.xy);
                //半圆弧,圆弧中心向两旁的值从 1 逐渐变为 0,cos 正好满足
                fixed rightHalfCircle=clamp(dot(float2(1,0),fragmentDir.xy),0,1);
                //渐变半圆弧颜色(内到外)
                col=lerp(_Color,fixed4(1,1,1,1),dis*_GradientFloat);
                //衰减半圆弧两旁,衰减 _Angle 角后的
                fixed tempAngleCos=cos(radians(_Angle));
                //增强边缘效果
                fixed strongF=pow(dis,_StrongFloat);
                col.a=col.a*dis*rightHalfCircle*strongF;
                //大于 _Angle 角度区域的像素衰减(在视野之外的),_Angle 是视野角度的一半
                if(rightHalfCircle<tempAngleCos)
                {
                        col.a*=_AlphaDownFloat;
                }
                else
                {
                        //扫描遮挡的核心:视野角度内(-_Angle,_Angle)范围内进行一个遮挡处理
                        //计算出 index
                        //fragmentDir.y 是归一化后的向量 y 值
                        //因为 sqrt(fragmentDir.y*fragmentDir.y+fragmentDir.x*fragmentDir.x)=1
                        //sin(fragmentDir) 与 UV 正 x 轴(0.5,0.5)的角度弧度为fragmentDir.y/1, 即 fragmentDir.y,
                        //反过来说 fragmentDir.y 就是 sin(角度)
                        //输入的是正弦值 sin(角度)=对边/斜边=fragmentDir.y/1  斜边是1,因为fragmentDir是归一化向量
                        //反正弦函数 输入 [-1,1] (sin值) 输出 [-π/2, π/2](弧度)
                        //简单来讲就是将偏移后的 uv 坐标点与中心点向量 和 正X轴的夹角角度转成了弧度..
                        //知道什么是反正弦函数就很容易了。。 就是反着来,正弦函数是输入弧度 输出正弦值,反正弦就是输入正弦值输出弧度
                        float curRad=asin(fragmentDir.y);
                        curRad+=radians(_Angle);//偏移到正数(上面的弧度是指(-_Angle,_Angle)角度的当前片元所在的角度弧度)
                        float f=curRad/radians(_Angle*2);//当前弧度/总弧度  得到一个系数
                        float index=f*256;		//系数乘上索引最大值 获取索引
                        //因为c#计算出的当 index 为 0 时,应该是照射区域上方,而此时Shader是不是0时上方,
                        //答案不是,上方 index 为 0 时,curRad 是 0,在没有经过偏移时,它位于照射区域最下。所以应该取反索引
                        index=256-index;
                        float curFloat=_FloatArray[index];
                        //dis 是 1 到 0,(1-dis)就是 0 到 1,curFloat 是锥形尖角的位置到目标障碍物的距离,*5要根据实际情况考虑
                        if(curFloat>0&&(1-dis)*5>curFloat)
                        {
                                col*=0;
                        }
                }
                return col;
            }
            ENDCG
        }
    }
}

注意 Main CameraAllow HDR 要取消勾选。

接下来就是射线检测部分了,通过射线检测将取得的数值传给 shader 中使用。

扫描射线检测脚本

代码如下:

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

public class Scan : MonoBehaviour
{
    //旋转速度
    public float speed;
    //射线检测的角度大小
    public float angle;
    //存储射线检测的结果
    float[] arrayFloat;
    //射线长度
    public float rayLength;
    //扫描材质
    public Material mat;
    void Start()
    {
        arrayFloat = new float[256];
    }

    void Update()
    {
        transform.Rotate(transform.up * Time.deltaTime * speed);
        UpdateRay();
    }

    private void UpdateRay()
    {
        int index = 0;
        //角度转弧度
        float rad = Mathf.Deg2Rad*angle;
        float step = rad * 2 / 256;
        for (int i = 1; i <= 256; i++)
        {
            //step * i 是视角范围内的一个弧度变化+自身角度弧度 进行旋转
            float curRad = step * i + Mathf.Deg2Rad * (transform.eulerAngles.y+180)-rad;
            //根据当前弧度计算出坐标
            float x = rayLength * Mathf.Cos(-curRad);
            float z = rayLength * Mathf.Sin(-curRad);
            Vector3 pos = new Vector3(x, 0, z);
            Ray ray = new Ray(transform.position, pos);
            RaycastHit hit;
            if(Physics.Raycast(ray,out hit))
            {
                arrayFloat[index] = hit.distance;
                //Debug.DrawLine(transform.position, pos, Color.red, arrayFloat[index]);
            }
            else
            {
                arrayFloat[index] = -1;
                //Debug.DrawLine(transform.position, pos,Color.blue,arrayFloat[index]);
            }
            index++;
        }
        mat.SetFloatArray("_FloatArray", arrayFloat);
    }
}

这样就完成效果了。

image.png

这个方式是 GPU 和 CPU 一直在通信,并且射线检测较多次,频率较高,性能可能不太好,下面这个是通过射线检测后绘制网格的,性能更好些。

20200609230648817.gif

玩家视野脚本

image.png

IHideable 接口:

/// <summary>
/// Interface that needs to be implemented by any object that gets affected by the Field of View of the player.
/// </summary>
public interface IHideable {

    void OnFOVEnter();
    void OnFOVLeave();
}

Hideable 代码:

using UnityEngine;

public class Hideable : MonoBehaviour, IHideable {

    private MeshRenderer render;

    private void Awake()
    {
        OnFOVLeave();
    }

    public void OnFOVEnter() {
        if (render == null)
            //TryGetComponent(out render);
            render = GetComponent<MeshRenderer>();
        render.enabled = true;
    }
    public void OnFOVLeave() {
        if (render == null)
            //TryGetComponent(out render);
            render = GetComponent<MeshRenderer>();
        render.enabled = false;
    }
}

FieldOfView 代码:

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

[RequireComponent(typeof(MeshFilter))]
public class FieldOfView : MonoBehaviour {

    [Header("视野设置")]
    [Tooltip("玩家可以看到的半径或最大距离")] public float viewRadius = 50f;
    [Range(0, 360), Tooltip("视野角度")] public float viewAngle = 90f;
    
    [Header("周边视野设置")]
    [Tooltip("玩家是否有周边视野?")] public bool hasPeripheralVision = false;
    [Tooltip("玩家用其周边视觉所能看到的最大半径距离.")] public float viewRadiusPeripheralVision = 10f;
    
    [Header("边缘解析设置")]    [Tooltip("边缘分解算法的迭代(更高=更精确但也更昂贵)")] public int edgeResolveIterations = 1;
    public float edgeDstThreshold;

    [Header("常规设置")]
    [Range(0, 1), Tooltip("视场更新之间的延迟,隔多少秒设置一次扫描物体的隐藏显示")] public float delayBetweenFOVUpdates = 0.2f;

    [Header("层级设置")]
    [Tooltip("进入/离开视野时受到影响的物体。它们必须实现iHidable接口")] public LayerMask targetMask;
    [Tooltip("阻挡视野的对象")] public LayerMask obstacleMask;

    [Header("可视化设置")]
    [Tooltip("视野可视化吗?")] public bool visualizeFieldOfView = true;
    [Tooltip("影响重新计算视场时射出的射线数量。光线投射计数=视角*网格分辨率")] public float meshResolution = 1;
    [Tooltip("影响重新计算玩家周边视野时投射出的射线数量。价值越高,成本就越高!光线投射计数")] public int meshResolutionPeripheralVision = 10;
    private MeshFilter viewMeshFilter;
    private Mesh viewMesh;


    //变量在 DrawFieldOfView 方法中使用(在这里存储效率更高 -GC.collect…)
    private List<Vector3> viewPoints = new List<Vector3>();


    private void Start() {
        //TryGetComponent(out viewMeshFilter);  2019.2以上版本才有的API.....................................
        viewMeshFilter = GetComponent<MeshFilter>();
        viewMesh = new Mesh
        {
            name = "View Mesh"
        };
        viewMeshFilter.mesh = viewMesh;
    }
    void OnEnable()
    {
        StartCoroutine("FindTargetsWithDelay", delayBetweenFOVUpdates);
    }

    private void LateUpdate()
    {
        if (visualizeFieldOfView)
        {
            viewMeshFilter.mesh = viewMesh;
            DrawFieldOfView();
        } else
        {
            viewMeshFilter.mesh = null;
        }
    }

    private readonly List<int> triangles = new List<int>();
    private readonly List<Vector3> vertices = new List<Vector3>();
    
    /// <summary>
    /// 画出视野
    /// </summary>
    void DrawFieldOfView()
    {
        
        viewPoints.Clear();
        ViewCastInfo oldViewCast = new ViewCastInfo();

        /* 计算法向视野 */
        for (int i = 0; i <= Mathf.RoundToInt(viewAngle * meshResolution); i++)
        {
            //存储了射线检测后的结果
            ViewCastInfo newViewCast = ViewCast(transform.eulerAngles.y - viewAngle / 2 + (viewAngle / Mathf.RoundToInt(viewAngle * meshResolution)) * i, viewRadius);
            if (i > 0)
            {
                if (oldViewCast.hit != newViewCast.hit || (oldViewCast.hit && newViewCast.hit && Mathf.Abs(oldViewCast.distance - newViewCast.distance) > edgeDstThreshold))
                {
                    EdgeInfo edge = FindEdge(oldViewCast, newViewCast, viewRadius);
                    if (edge.pointA != Vector3.zero)
                    {
                        viewPoints.Add(edge.pointA);
                    }
                    if (edge.pointB != Vector3.zero)
                    {
                        viewPoints.Add(edge.pointB);
                    }
                }
            }
            viewPoints.Add(newViewCast.point);
            oldViewCast = newViewCast;
        }
        /* 计算周边视野 */
        if (hasPeripheralVision && viewAngle < 360)
        {
            //把较短的光线投射到周围,以确保他总是能从各个方向看一点东西
            for (int i = 0; i < meshResolutionPeripheralVision + 1; i++)
            {
                ViewCastInfo newViewCast = ViewCast(transform.eulerAngles.y + viewAngle / 2 + i * (360 - viewAngle) / meshResolutionPeripheralVision, viewRadiusPeripheralVision);
                //viewPoints.Add(newViewCast.point);
                if (i > 0)
                {
                    if (oldViewCast.hit != newViewCast.hit || (oldViewCast.hit && newViewCast.hit && Mathf.Abs(oldViewCast.distance - newViewCast.distance) > edgeDstThreshold))
                    {
                        EdgeInfo edge = FindEdge(oldViewCast, newViewCast, viewRadiusPeripheralVision);
                        if (edge.pointA != Vector3.zero)
                        {
                            viewPoints.Add(edge.pointA);
                        }
                        if (edge.pointB != Vector3.zero)
                        {
                            viewPoints.Add(edge.pointB);
                        }
                    }
                }
                viewPoints.Add(newViewCast.point);
                oldViewCast = newViewCast;
            }
        }
        /* 画出网格 */
        int vertexCount = viewPoints.Count + 1;
        vertices.Clear();
        triangles.Clear();
        vertices.Add(Vector3.zero);
        for (int i = 0; i < vertexCount - 1; i++)
        {
            vertices.Add(transform.InverseTransformPoint(viewPoints[i]));
            if (i < vertexCount - 2)
            {
                triangles.Add(0);
                triangles.Add(i + 1);
                triangles.Add(i + 2);
            }
        }
        viewMesh.Clear();
        viewMesh.SetVertices(vertices) ;
        //Unity中,可以有submesh。0表示主mesh
        viewMesh.SetTriangles(triangles,0) ;
        viewMesh.RecalculateNormals();
    }

    /// <summary>
    /// 以给定的角度投射光线,结果返回ViewCastInfo结构。
    /// </summary>
    /// <param name="globalAngle">每条射线的角度</param>
    /// <returns></returns>
    ViewCastInfo ViewCast(float globalAngle, float viewRadius)
    {
        Vector3 dir = DirFromAngle(globalAngle, true);
        Physics.autoSyncTransforms = false;
        if (Physics.Raycast(transform.position, dir, out RaycastHit hit, viewRadius, obstacleMask))
        {
            Physics.autoSyncTransforms = true;
            return new ViewCastInfo(true, hit.point, hit.distance, globalAngle);
        } else
        {
            Physics.autoSyncTransforms = true;
            return new ViewCastInfo(false, transform.position + dir * viewRadius, viewRadius, globalAngle);
        }
    }
    
    /// <summary>
    /// 找到碰撞体的边缘
    /// </summary>
    /// <param name="minViewCast"></param>
    /// <param name="maxViewCast"></param>
    /// <returns></returns>
	EdgeInfo FindEdge(ViewCastInfo minViewCast, ViewCastInfo maxViewCast, float viewRadius)
    {
        float minAngle = minViewCast.angle;
        float maxAngle = maxViewCast.angle;
        Vector3 minPoint = Vector3.zero;
        Vector3 maxPoint = Vector3.zero;

        for (int i = 0; i < edgeResolveIterations; i++)
        {
            float angle = (minAngle + maxAngle) / 2;
            ViewCastInfo newViewCast = ViewCast(angle, viewRadius);

            bool edgeDstThresholdExceeded = Mathf.Abs(minViewCast.distance - newViewCast.distance) > edgeDstThreshold;
            if (newViewCast.hit == minViewCast.hit && !edgeDstThresholdExceeded)//xxxxxxxxxxxxxxxxx
            {
                minAngle = angle;
                minPoint = newViewCast.point;
            }
            else
            {
                maxAngle = angle;
                maxPoint = newViewCast.point;
            }
        }
        return new EdgeInfo(minPoint, maxPoint);
    }

    /// <summary>
    /// 每 1 秒运行一次 FindVisibleTargets 方法
    /// </summary>
    /// <param name="delay"></param>
    /// <returns></returns>
    IEnumerator FindTargetsWithDelay(float delay)
    {
        while (true)
        {
            FindVisibleTargets();
            yield return new WaitForSeconds(delay);
        }
    }

    Collider[] targetsInViewRadius = new Collider[10];
    
    /// <summary>
    /// 查找所有可见目标并将其添加到 “可见目标” 列表中.
    /// </summary>
    void FindVisibleTargets()
    {
        int length = Physics.OverlapSphereNonAlloc(transform.position, viewRadius , targetsInViewRadius, targetMask);
        //在执行下一次 FixedUpdate 之前,对碰撞体的改动不会立即同步到物理场景
        Physics.autoSyncTransforms = false;

        /* check normal field of view */
        for (int i = 0; i < length; i++)
        {
            Transform target = targetsInViewRadius[i].transform;
            bool isInFOV = false;
            //检查是否应该隐藏
            Vector3 dirToTarget = (target.position - transform.position).normalized;
            if (Vector3.Angle(transform.forward, dirToTarget) < viewAngle / 2)
            {
                float dstToTarget = Vector3.Distance(transform.position, target.position);
                if (!Physics.Raycast(transform.position, dirToTarget, dstToTarget, obstacleMask))
                {
                    isInFOV = true;
                }
            } else if (hasPeripheralVision)
            {
                float dstToTarget = Vector3.Distance(transform.position, target.position);
                // 这里我们必须检查到目标的距离,因为周围的视野可能有不同于正常视野的半径
                if (dstToTarget < viewRadiusPeripheralVision && !Physics.Raycast(transform.position, dirToTarget, dstToTarget, obstacleMask))
                {
                    isInFOV = true;
                }
            }
            //apply effect to IHideable
            IHideable hideable ;
            //target.TryGetComponent(out hideable);............................................
            hideable = target.GetComponent<IHideable>();
            if (hideable != null)
            {
                if (isInFOV)
                {
                    hideable.OnFOVEnter();
                } else
                {
                    hideable.OnFOVLeave();
                }
            }
        }
        Physics.autoSyncTransforms = true;
    }

    /// <summary>
    /// 将角度转换为方向矢量.
    /// </summary>
    /// <param name="angleInDegrees"></param>
    /// <returns></returns>
    public Vector3 DirFromAngle(float angleInDegrees, bool IsAngleGlobal)
    {
        if (!IsAngleGlobal)
        {
            angleInDegrees += transform.eulerAngles.y;
        }
        return new Vector3(Mathf.Sin(angleInDegrees * Mathf.Deg2Rad), 0, Mathf.Cos(angleInDegrees * Mathf.Deg2Rad));
    }
}


/// <summary>
/// 用于存储有关视图光线投射的信息的结构体
/// </summary>
public struct ViewCastInfo
{
    public bool hit;
    public Vector3 point;
    public float distance;
    public float angle;

    public ViewCastInfo(bool hit, Vector3 point, float distance, float angle)
    {
        this.hit = hit;
        this.point = point;
        this.distance = distance;
        this.angle = angle;
    }
}

/// <summary>
/// 保存边缘信息的结构体
/// </summary>
public struct EdgeInfo
{
    public Vector3 pointA;
    public Vector3 pointB;

    public EdgeInfo(Vector3 pointA, Vector3 pointB)
    {
        this.pointA = pointA;
        this.pointB = pointB;
    }
}