csdn 这垃圾工具, 一保存就丢内容!!!


https://github.com/SunGuangdong/Field-of-View   

https://www.youtube.com/watch?v=rQG9aUWarwE    (共3个)


测试场景这样的 , 橙色Cube 几个, 设置他们的 Layer 为 Obstacles

蓝色 Capsule 几个, 设置他们的 Layer 为 Targets
Unity的Mesh 应用---视野可视化Field of view
白色 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
Unity的Mesh 应用---视野可视化Field of view
这个效果必须要 Play状态下看了!
Unity的Mesh 应用---视野可视化Field of view


2、 我们创建了一个系统来检测哪些目标在我们单位的视野中。 这对于stealth类型游戏是有用的。
使用Mesh进行视野绘制!
Unity的Mesh 应用---视野可视化Field of view

先写一些测试代码,看看
/// <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 的效果
Unity的Mesh 应用---视野可视化Field of view
meshResolution = 1 的效果
Unity的Mesh 应用---视野可视化Field of view

Mesh 最主要的数据 顶点 和三角形, 但是三角形也是顶点组合。
顶点通过射线确定位置。
这种扇形的Mesh还是挺有规律。 下面右图中, 顶点5个, 三角形3个。 那么所有三角形需要的数组就是 (v-2) *3 , 每个三角形是3个点。

Unity的Mesh 应用---视野可视化Field of view


/// <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 组件)。
Unity的Mesh 应用---视野可视化Field of view
运行就可以看到Mesh已经出来了!!!, 可以为Cube换一个材质
Unity的Mesh 应用---视野可视化Field of view




但是有一个问题, 出于性能考虑 肯定不能将meshResolution 的值设置的特别大。
如果值小了, 就会出现下面精度的问题!
Unity的Mesh 应用---视野可视化Field of view
边缘信息, 如果两个相邻节点发生了是否命中的改变。 说明两个点之间有一个碰撞边缘被漏掉了。
那么用二分的方式 在这两个相邻节点之间做射线。 找到碰撞的边缘信息!
Unity的Mesh 应用---视野可视化Field of view

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;
        }
    }

}

可以看到这里面插入了一条边
Unity的Mesh 应用---视野可视化Field of view

3、 我们创建了一个stencil着色器,以实现一个很酷的有限FOV效果。
Unity的Mesh 应用---视野可视化Field of view
这个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 着色器。
Unity的Mesh 应用---视野可视化Field of view


相关文章: