csdn 这垃圾工具, 一保存就丢内容!!!
https://github.com/SunGuangdong/Field-of-View
https://www.youtube.com/watch?v=rQG9aUWarwE (共3个)
测试场景这样的 , 橙色Cube 几个, 设置他们的 Layer 为 Obstacles。
蓝色 Capsule 几个, 设置他们的 Layer 为 Targets。
白色 Obstacles 作为角色。
先弄第一个脚本 挂在 Character 上: 控制方向和旋转!!!
|
using UnityEngine;
/// <summary>
/// 控制角色移动
/// </summary>
public class Controller : MonoBehaviour
{
/// <summary>
/// 移动速度
/// </summary>
public float moveSpeed = 6;
Rigidbody rb;
Camera viewCamera;
Vector3 velocity;
void Start()
{
rb = GetComponent<Rigidbody>();
viewCamera = Camera.main;
}
void Update()
{
// 鼠标位置控制 角色朝向
Vector3 mousePos = viewCamera.ScreenToWorldPoint(new Vector3(Input.mousePosition.x, Input.mousePosition.y, viewCamera.transform.position.y));
transform.LookAt(mousePos + Vector3.up * transform.position.y);
// 方向键控制移动
velocity = new Vector3(Input.GetAxisRaw("Horizontal"), 0, Input.GetAxisRaw("Vertical")).normalized * moveSpeed;
}
void FixedUpdate()
{
rb.MovePosition(rb.position + velocity * Time.fixedDeltaTime);
}
}
|
然后 开始另外一个脚本组件, 先画一个圆
|
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FieldOfView : MonoBehaviour
{
/// <summary>
/// 圆半径
/// </summary>
public float viewRadius;
/// <summary>
/// 视野 角度
/// </summary>
[Range(0, 360)]
public float viewAngle;/// <summary>
/// 根据角度得到 方向单位向量(极坐标 x=sin(a), z=cos(a))
/// </summary>
/// <param name="angleInDegrees"></param>
/// <param name="angleIsGlobal"></param>
/// <returns></returns>
public Vector3 DirFromAngle(float angleInDegrees, bool angleIsGlobal)
{
// 相对自身的?
if (!angleIsGlobal)
{
angleInDegrees += transform.eulerAngles.y;
}
return new Vector3(Mathf.Sin(angleInDegrees * Mathf.Deg2Rad), 0, Mathf.Cos(angleInDegrees * Mathf.Deg2Rad));
}
}
|
|
using UnityEditor;
using UnityEngine;
[CustomEditor(typeof(FieldOfView))]
public class FieldOfViewEditor : Editor
{
void OnSceneGUI()
{
FieldOfView fow = (FieldOfView)target;
Handles.color = Color.white;
// 画一个圆
Handles.DrawWireArc(fow.transform.position, Vector3.up, Vector3.forward, 360, fow.viewRadius);
// 视野的两条线
Vector3 viewAngleA = fow.DirFromAngle(-fow.viewAngle / 2, false);
Vector3 viewAngleB = fow.DirFromAngle(fow.viewAngle / 2, false);
Handles.DrawLine(fow.transform.position, fow.transform.position + viewAngleA * fow.viewRadius);
Handles.DrawLine(fow.transform.position, fow.transform.position + viewAngleB * fow.viewRadius);
}
}
|
回到场景中, 不要Play,就在编辑器 状态下, 选中 Character 角色。 圆 和直线
然后添加碰撞检测相关的代码(障碍 和 目标):
|
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FieldOfView : MonoBehaviour
{
/// <summary>
/// 圆半径
/// </summary>
public float viewRadius;
/// <summary>
/// 视野 角度
/// </summary>
[Range(0, 360)]
public float viewAngle;
/// <summary>
/// 目标层
/// </summary>
public LayerMask targetMask;
/// <summary>
/// 障碍层
/// </summary>
public LayerMask obstacleMask;
/// <summary>
/// 在视野内能看到的目标
/// </summary>
[HideInInspector]
public List<Transform> visibleTargets = new List<Transform>();
void Start()
{
StartCoroutine(FindTargetsWithDelay(.2f));
}
IEnumerator FindTargetsWithDelay(float delay)
{
while (true)
{
yield return new WaitForSeconds(delay);
FindVisibleTargets();
}
}
void FindVisibleTargets()
{
visibleTargets.Clear();
// 球内的所有碰撞体
Collider[] targetsInViewRadius = Physics.OverlapSphere(transform.position, viewRadius, targetMask);
for (int i = 0; i < targetsInViewRadius.Length; i++)
{
Transform target = targetsInViewRadius[i].transform;
// 目标方向
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))
{
visibleTargets.Add(target);
}
}
}
}
/// <summary>
/// 根据角度得到 方向单位向量(极坐标 x=sin(a), z=cos(a))
/// </summary>
/// <param name="angleInDegrees"></param>
/// <param name="angleIsGlobal"></param>
/// <returns></returns>
public Vector3 DirFromAngle(float angleInDegrees, bool angleIsGlobal)
{
// 相对自身的?
if (!angleIsGlobal)
{
angleInDegrees += transform.eulerAngles.y;
}
return new Vector3(Mathf.Sin(angleInDegrees * Mathf.Deg2Rad), 0, Mathf.Cos(angleInDegrees * Mathf.Deg2Rad));
}
}
|
// 如果视野内有 目标, 做一个红色连线
|
using UnityEditor;
using UnityEngine;
[CustomEditor(typeof(FieldOfView))]
public class FieldOfViewEditor : Editor
{
void OnSceneGUI()
{
FieldOfView fow = (FieldOfView)target;
Handles.color = Color.white;
// 画一个圆
Handles.DrawWireArc(fow.transform.position, Vector3.up, Vector3.forward, 360, fow.viewRadius);
// 视野的两条线
Vector3 viewAngleA = fow.DirFromAngle(-fow.viewAngle / 2, false);
Vector3 viewAngleB = fow.DirFromAngle(fow.viewAngle / 2, false);
Handles.DrawLine(fow.transform.position, fow.transform.position + viewAngleA * fow.viewRadius);
Handles.DrawLine(fow.transform.position, fow.transform.position + viewAngleB * fow.viewRadius);
// 和视野内的目标做一个红色连线
Handles.color = Color.red;
foreach (Transform visibleTarget in fow.visibleTargets)
{
Handles.DrawLine(fow.transform.position, visibleTarget.position);
}
}
}
|
注意,别忘了指定Layer
这个效果必须要 Play状态下看了!
2、 我们创建了一个系统来检测哪些目标在我们单位的视野中。 这对于stealth类型游戏是有用的。
使用Mesh进行视野绘制!
先写一些测试代码,看看
|
/// <summary>
/// 视野被分的密度
/// </summary>
public float meshResolution;
void LateUpdate()
{
//DrawFieldOfView();
// 视野角度分成多少份
int stepCount = Mathf.RoundToInt(viewAngle * meshResolution);
// 每份大小
float stepAngleSize = viewAngle / stepCount;
for (int i = 0; i <= stepCount; i++)
{
float angle = transform.eulerAngles.y - viewAngle / 2 + stepAngleSize * i;
Debug.DrawLine(transform.position, transform.position + DirFromAngle(angle, true) * viewRadius, Color.red);
}
}
|
meshResolution = 0.1 的效果
meshResolution = 1 的效果
Mesh 最主要的数据 顶点 和三角形, 但是三角形也是顶点组合。
顶点通过射线确定位置。
这种扇形的Mesh还是挺有规律。 下面右图中, 顶点5个, 三角形3个。 那么所有三角形需要的数组就是 (v-2) *3 , 每个三角形是3个点。
|
/// <summary>
/// 记录射线 射中的一些信息
/// </summary>
public struct ViewCastInfo
{
// 射线是否射中目标
public bool hit;
// 射中的位置,(没有射中就是圆上的点呗)
public Vector3 point;
// 射中的距离,(没有射中就是半径呗)
public float dst;
// 就是 这个射线的角度
public float angle;
public ViewCastInfo(bool _hit, Vector3 _point, float _dst, float _angle)
{
hit = _hit;
point = _point;
dst = _dst;
angle = _angle;
}
}
/// <summary>
/// 发射线,并记录命中信息
/// </summary>
/// <param name="globalAngle"></param>
/// <returns></returns>
ViewCastInfo ViewCast(float globalAngle)
{
Vector3 dir = DirFromAngle(globalAngle, true);
RaycastHit hit;
if (Physics.Raycast(transform.position, dir, out hit, viewRadius, obstacleMask))
{
return new ViewCastInfo(true, hit.point, hit.distance, globalAngle);
}
else
{
return new ViewCastInfo(false, transform.position + dir * viewRadius, viewRadius, globalAngle);
}
}
void LateUpdate()
{
DrawFieldOfView();
}
// 组件
public MeshFilter viewMeshFilter;
// Mesh 对象
Mesh viewMesh;
void Start()
{
viewMesh = new Mesh();
viewMesh.name = "View Mesh";
viewMeshFilter.mesh = viewMesh;
StartCoroutine(FindTargetsWithDelay(.2f));
}
/// <summary>
/// 绘制视野
/// </summary>
void DrawFieldOfView()
{
// 视野角度分成多少份
int stepCount = Mathf.RoundToInt(viewAngle * meshResolution);
// 每份大小
float stepAngleSize = viewAngle / stepCount;
List<Vector3> viewPoints = new List<Vector3>();
for (int i = 0; i <= stepCount; i++)
{
float angle = transform.eulerAngles.y - viewAngle / 2 + stepAngleSize * i;
ViewCastInfo newViewCast = ViewCast(angle);
viewPoints.Add(newViewCast.point);
}
// 填充 Mesh的 顶点和 三角形数据
int vertexCount = viewPoints.Count + 1; // 射线的数量在加上 圆点
Vector3[] vertices = new Vector3[vertexCount];
int[] triangles = new int[(vertexCount - 2) * 3];
vertices[0] = Vector3.zero; // 圆点
for (int i = 0; i < vertexCount - 1; i++)
{
vertices[i + 1] = transform.InverseTransformPoint(viewPoints[i]);
if (i < vertexCount - 2)
{
triangles[i * 3] = 0;
triangles[i * 3 + 1] = i + 1;
triangles[i * 3 + 2] = i + 2;
}
}
viewMesh.Clear();
viewMesh.vertices = vertices;
viewMesh.triangles = triangles;
viewMesh.RecalculateNormals();
}
|
为 Character 对象创建一个 子对象 用Cube就行(主要用它上面的 MeshFilter, MeshRenderer 组件)。
运行就可以看到Mesh已经出来了!!!, 可以为Cube换一个材质
但是有一个问题, 出于性能考虑 肯定不能将meshResolution 的值设置的特别大。
如果值小了, 就会出现下面精度的问题!
边缘信息, 如果两个相邻节点发生了是否命中的改变。 说明两个点之间有一个碰撞边缘被漏掉了。
那么用二分的方式 在这两个相邻节点之间做射线。 找到碰撞的边缘信息!
|
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FieldOfView : MonoBehaviour
{
/// <summary>
/// 圆半径
/// </summary>
public float viewRadius;
/// <summary>
/// 视野 角度
/// </summary>
[Range(0, 360)]
public float viewAngle;
/// <summary>
/// 目标层
/// </summary>
public LayerMask targetMask;
/// <summary>
/// 障碍层
/// </summary>
public LayerMask obstacleMask;
/// <summary>
/// 在视野内能看到的目标
/// </summary>
[HideInInspector]
public List<Transform> visibleTargets = new List<Transform>();
/// <summary>
/// 视野被分的密度
/// </summary>
public float meshResolution;
/// <summary>
/// 查找边界 需要迭代的次数
/// </summary>
public int edgeResolveIterations;
/// <summary>
/// 当两个射线命中点很近(就是这个近的程度)的时候,认为他俩需要替换用的
/// </summary>
public float edgeDstThreshold;
/// <summary>
/// 顶点做偏移
/// </summary>
public float maskCutawayDst = .1f;
// 组件
public MeshFilter viewMeshFilter;
// Mesh 对象
Mesh viewMesh;
void Start()
{
viewMesh = new Mesh();
viewMesh.name = "View Mesh";
viewMeshFilter.mesh = viewMesh;
StartCoroutine(FindTargetsWithDelay(.2f));
}
IEnumerator FindTargetsWithDelay(float delay)
{
while (true)
{
yield return new WaitForSeconds(delay);
FindVisibleTargets();
}
}
void LateUpdate()
{
DrawFieldOfView();
}
void FindVisibleTargets()
{
visibleTargets.Clear();
// 球内的所有碰撞体
Collider[] targetsInViewRadius = Physics.OverlapSphere(transform.position, viewRadius, targetMask);
for (int i = 0; i < targetsInViewRadius.Length; i++)
{
Transform target = targetsInViewRadius[i].transform;
// 目标方向
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))
{
visibleTargets.Add(target);
}
}
}
}
/// <summary>
/// 绘制视野
/// </summary>
void DrawFieldOfView()
{
// 视野角度分成多少份
int stepCount = Mathf.RoundToInt(viewAngle * meshResolution);
// 每份大小
float stepAngleSize = viewAngle / stepCount;
List<Vector3> viewPoints = new List<Vector3>();
// old, new 是为了 边缘检测做准备
ViewCastInfo oldViewCast = new ViewCastInfo();
for (int i = 0; i <= stepCount; i++)
{
float angle = transform.eulerAngles.y - viewAngle / 2 + stepAngleSize * i;
ViewCastInfo newViewCast = ViewCast(angle);
if (i > 0)
{
// 认为 两个点没在一个圆周上
bool edgeDstThresholdExceeded = Mathf.Abs(oldViewCast.dst - newViewCast.dst) > edgeDstThreshold;
// 两个射线的命中结果不一样 或者 命中了一样的但是碰撞体的这一面太斜了(不插入会露馅)
if (oldViewCast.hit != newViewCast.hit || (oldViewCast.hit && newViewCast.hit && edgeDstThresholdExceeded))
{
// 新旧两个点构成 一条边
EdgeInfo edge = FindEdge(oldViewCast, newViewCast);
if (edge.pointA != Vector3.zero)
{
viewPoints.Add(edge.pointA);
}
if (edge.pointB != Vector3.zero)
{
viewPoints.Add(edge.pointB);
}
}
}
Debug.DrawLine(transform.position, transform.position + DirFromAngle(angle, true) * viewRadius, Color.red);
viewPoints.Add(newViewCast.point);
oldViewCast = newViewCast;
}
// 填充 Mesh的 顶点和 三角形数据
int vertexCount = viewPoints.Count + 1; // 射线的数量在加上 圆点
Vector3[] vertices = new Vector3[vertexCount];
int[] triangles = new int[(vertexCount - 2) * 3];
vertices[0] = Vector3.zero; // 圆点
for (int i = 0; i < vertexCount - 1; i++)
{
vertices[i + 1] = transform.InverseTransformPoint(viewPoints[i]) + Vector3.forward * maskCutawayDst;
if (i < vertexCount - 2)
{
triangles[i * 3] = 0;
triangles[i * 3 + 1] = i + 1;
triangles[i * 3 + 2] = i + 2;
}
}
viewMesh.Clear();
viewMesh.vertices = vertices;
viewMesh.triangles = triangles;
viewMesh.RecalculateNormals();
}
/// <summary>
/// 找到边界 (是从Max 到 Min 做二分找)
/// </summary>
/// <param name="minViewCast"></param>
/// <param name="maxViewCast"></param>
/// <returns></returns>
EdgeInfo FindEdge(ViewCastInfo minViewCast, ViewCastInfo maxViewCast)
{
float minAngle = minViewCast.angle;
float maxAngle = maxViewCast.angle;
Vector3 minPoint = Vector3.zero;
Vector3 maxPoint = Vector3.zero;
float angle = minViewCast.angle;
// 迭代 查找 边界(次数越多越精确)
for (int i = 0; i < edgeResolveIterations; i++)
{
angle = (minAngle + maxAngle) / 2;
ViewCastInfo newViewCast = ViewCast(angle);
// 近似处理 两个碰撞点的距离 是否超出了范围
bool edgeDstThresholdExceeded = Mathf.Abs(minViewCast.dst - newViewCast.dst) > edgeDstThreshold;
// 经过迭代两个射线都 射中同一个碰撞体, 或者都未中。 就是可以认为新的点替代min然后继续做查找 (是从Max 到 Min 做二分找)
if (newViewCast.hit == minViewCast.hit && !edgeDstThresholdExceeded)
{
minAngle = angle;
minPoint = newViewCast.point;
}
// 新的点替代max然后继续做查找
else
{
maxAngle = angle;
maxPoint = newViewCast.point;
}
}
Debug.DrawLine(transform.position, transform.position + DirFromAngle(angle, true) * viewRadius, Color.red);
return new EdgeInfo(minPoint, maxPoint);
}
/// <summary>
/// 发射线,并记录命中信息
/// </summary>
/// <param name="globalAngle"></param>
/// <returns></returns>
ViewCastInfo ViewCast(float globalAngle)
{
Vector3 dir = DirFromAngle(globalAngle, true);
RaycastHit hit;
if (Physics.Raycast(transform.position, dir, out hit, viewRadius, obstacleMask))
{
return new ViewCastInfo(true, hit.point, hit.distance, globalAngle);
}
else
{
return new ViewCastInfo(false, transform.position + dir * viewRadius, viewRadius, globalAngle);
}
}
/// <summary>
/// 根据角度得到 方向单位向量(极坐标 x=sin(a), z=cos(a))
/// </summary>
/// <param name="angleInDegrees"></param>
/// <param name="angleIsGlobal"></param>
/// <returns></returns>
public Vector3 DirFromAngle(float angleInDegrees, bool angleIsGlobal)
{
// 相对自身的?
if (!angleIsGlobal)
{
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 dst;
// 就是 这个射线的角度
public float angle;
public ViewCastInfo(bool _hit, Vector3 _point, float _dst, float _angle)
{
hit = _hit;
point = _point;
dst = _dst;
angle = _angle;
}
}
/// <summary>
/// 边缘信息
/// </summary>
public struct EdgeInfo
{
// 有一个代表要插入的边缘点
public Vector3 pointA;
public Vector3 pointB;
public EdgeInfo(Vector3 _pointA, Vector3 _pointB)
{
pointA = _pointA;
pointB = _pointB;
}
}
}
|
可以看到这里面插入了一条边
3、 我们创建了一个stencil着色器,以实现一个很酷的有限FOV效果。
这个shader 最重要的就是 Stencil 关键字。这个技术可以实现很多功能, 特效,模型的Mask !!!
|
Shader "Custom/Stencil Mask" {
Properties {
_Color ("Color", Color) = (1,1,1,1)
_MainTex ("Albedo (RGB)", 2D) = "white" {}
_Glossiness ("Smoothness", Range(0,1)) = 0.5
_Metallic ("Metallic", Range(0,1)) = 0.0
}
SubShader {
Tags { "RenderType"="Opaque" "Queue"="Geometry-100" }
ColorMask 0
ZWrite off
LOD 200
Stencil {
Ref 1
Pass replace
}
CGPROGRAM
// Physically based Standard lighting model, and enable shadows on all light types
#pragma surface surf Standard fullforwardshadows
// Use shader model 3.0 target, to get nicer looking lighting
#pragma target 3.0
sampler2D _MainTex;
struct Input {
float2 uv_MainTex;
};
half _Glossiness;
half _Metallic;
fixed4 _Color;
void surf (Input IN, inout SurfaceOutputStandard o) {
// Albedo comes from a texture tinted by color
fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
o.Albedo = c.rgb;
// Metallic and smoothness come from slider variables
o.Metallic = _Metallic;
o.Smoothness = _Glossiness;
o.Alpha = c.a;
}
ENDCG
}
FallBack "Diffuse"
}
|
|
Shader "Custom/Stencil Object" {
Properties {
_Color ("Color", Color) = (1,1,1,1)
_MainTex ("Albedo (RGB)", 2D) = "white" {}
_Glossiness ("Smoothness", Range(0,1)) = 0.5
_Metallic ("Metallic", Range(0,1)) = 0.0
}
SubShader {
Tags { "RenderType"="Opaque" }
LOD 200
Stencil {
Ref 1
Comp equal
}
CGPROGRAM
// Physically based Standard lighting model, and enable shadows on all light types
#pragma surface surf Standard fullforwardshadows
// Use shader model 3.0 target, to get nicer looking lighting
#pragma target 3.0
sampler2D _MainTex;
struct Input {
float2 uv_MainTex;
};
half _Glossiness;
half _Metallic;
fixed4 _Color;
void surf (Input IN, inout SurfaceOutputStandard o) {
// Albedo comes from a texture tinted by color
fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
o.Albedo = c.rgb;
// Metallic and smoothness come from slider variables
o.Metallic = _Metallic;
o.Smoothness = _Glossiness;
o.Alpha = c.a;
}
ENDCG
}
FallBack "Diffuse"
}
|
之前 Character 创建的Cube用于 Mesh的显示, 它的Shader要 替换为 Stencil Mask 。
地面,所有障碍物和目标 Ground,Obstacles ,Targets 替换为 Stencil Object 着色器。