https://github.com/SunGuangdong/Field-of-View
https://www.youtube.com/watch?v=rQG9aUWarwE (共3个)
测试场景这样的 , 橙色Cube 几个, 设置他们的 Layer 为 Obstacles。
using
UnityEngine;
///
///
控制角色移动
///
public
class
Controller
:
MonoBehaviour
{
///
///
移动速度
///
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
{
///
///
圆半径
///
public
float
viewRadius;
///
///
视野 角度
///
[
Range
(0, 360)]
public
float
viewAngle;
///
///
根据角度得到 方向单位向量(极坐标 x=sin(a), z=cos(a))
///
///
angleInDegrees
">
///
angleIsGlobal
">
///
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);
}
}
|
using
System.Collections;
using
System.Collections.Generic;
using
UnityEngine;
public
class
FieldOfView
:
MonoBehaviour
{
///
///
圆半径
///
public
float
viewRadius;
///
///
视野 角度
///
[
Range
(0, 360)]
public
float
viewAngle;
///
///
目标层
///
public
LayerMask
targetMask;
///
///
障碍层
///
public
LayerMask
obstacleMask;
///
///
在视野内能看到的目标
///
[
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);
}
}
}
}
///
///
根据角度得到 方向单位向量(极坐标 x=sin(a), z=cos(a))
///
///
angleInDegrees
">
///
angleIsGlobal
">
///
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);
}
}
}
|
///
///
视野被分的密度
///
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);
}
}
|
///
///
记录射线 射中的一些信息
///
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;
}
}
///
///
发射线,并记录命中信息
///
///
globalAngle
">
///
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));
}
///
///
绘制视野
///
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();
}
|
using
System.Collections;
using
System.Collections.Generic;
using
UnityEngine;
public
class
FieldOfView
:
MonoBehaviour
{
///
///
圆半径
///
public
float
viewRadius;
///
///
视野 角度
///
[
Range
(0, 360)]
public
float
viewAngle;
///
///
目标层
///
public
LayerMask
targetMask;
///
///
障碍层
///
public
LayerMask
obstacleMask;
///
///
在视野内能看到的目标
///
[
HideInInspector
]
public
List
<
Transform
> visibleTargets =
new
List
<
Transform
>();
///
///
视野被分的密度
///
public
float
meshResolution;
///
///
查找边界 需要迭代的次数
///
public
int
edgeResolveIterations;
///
///
当两个射线命中点很近(就是这个近的程度)的时候,认为他俩需要替换用的
///
public
float
edgeDstThreshold;
///
///
顶点做偏移
///
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);
}
}
}
}
///
///
绘制视野
///
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();
}
///
///
找到边界 (是从Max 到 Min 做二分找)
///
///
minViewCast
">
///
maxViewCast
">
///
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);
}
///
///
发射线,并记录命中信息
///
///
globalAngle
">
///
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);
}
}
///
///
根据角度得到 方向单位向量(极坐标 x=sin(a), z=cos(a))
///
///
angleInDegrees
">
///
angleIsGlobal
">
///
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));
}
///
///
记录射线 射中的一些信息
///
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;
}
}
///
///
边缘信息
///
public
struct
EdgeInfo
{
// 有一个代表要插入的边缘点
public
Vector3
pointA;
public
Vector3
pointB;
public
EdgeInfo(
Vector3
_pointA,
Vector3
_pointB)
{
pointA = _pointA;
pointB = _pointB;
}
}
}
|
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"
}
|