效果图如下:
新建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 Camera的Allow 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);
}
}
这样就完成效果了。
这个方式是GPU和CPU一直在通信,并且射线检测较多次,频率较高,性能可能不太好,下面这个是通过射线检测后绘制网格的,性能更好些。
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;
}
}