ved_Rony
article thumbnail

유니티로 시야각 구현

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

public class FovTest : MonoBehaviour
{
    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 Edge
    {
        public Vector3 PointA, PointB;
        public Edge(Vector3 _PointA, Vector3 _PointB)
        {
            PointA = _PointA;
            PointB = _PointB;
        }
    }
    
    // 시야 영역의 반지름과 시야 각도
    public float viewRadius;
    [Range(0, 360)]
    public float viewAngle;
    public float meshResolution = 0.1f;
    
    public int edgeResolveIterations;
    public float edgeDstThreshold;
    
    // 마스크 2종
    public LayerMask targetMask, obstacleMask;
    
    // Target mask에 ray hit된 transform을 보관하는 리스트
    public List<Transform> visibleTargets = new List<Transform>();
    private Mesh viewMesh;
    public MeshFilter viewMeshFilter;
    public bool IsTargetExist => visibleTargets.Count != 0;
    public bool IsTargetClose
    {
        get
        {
            if (visibleTargets.Count == 0) return false;

            return IsTargetExist && Vector3.Distance(transform.position, visibleTargets[0].position) < 3.3f;
        }
    }

    // Start is called before the first frame update
    void Start()
    {
        viewMesh = new ();
        viewMesh.name = "View Mesh";
        viewMeshFilter.mesh = viewMesh;
        // 0.2초 간격으로 코루틴 호출
        StartCoroutine(FindTargetsWithDelay(0.2f));
    }

    IEnumerator FindTargetsWithDelay(float delay)
    {
        while (true)
        {
            yield return new WaitForSeconds(delay);
            FindVisibleTargets();
        }
    }

    void LateUpdate()
    {
        DrawFieldOfView();
    }
    
    void FindVisibleTargets()
    {
        visibleTargets.Clear();
        // viewRadius를 반지름으로 한 원 영역 내 targetMask 레이어인 콜라이더를 모두 가져옴
        Collider[] targetsInViewRadius = Physics.OverlapSphere(transform.position, viewRadius, targetMask);
    
        for (int i = 0; i < targetsInViewRadius.Length; i++)
        {
            Transform target = targetsInViewRadius[i].transform;
            var targetPos = target.position;
            targetPos.y = transform.position.y;
            Vector3 dirToTarget = (targetPos - transform.position).normalized;
         
            // 플레이어와 forward와 target이 이루는 각이 설정한 각도 내라면
            if (Vector3.Angle(transform.forward, dirToTarget) < viewAngle / 2)
            {
                float dstToTarget = Vector3.Distance(transform.position, targetPos);
               
                // 타겟으로 가는 레이캐스트에 obstacleMask가 걸리지 않으면 visibleTargets에 Add
                if (!Physics.Raycast(transform.position, dirToTarget, dstToTarget, obstacleMask))
                {
                       visibleTargets.Add(target);
                }
            }
        }
    }
    
    // y축 오일러 각을 3차원 방향 벡터로 변환한다.
    // 원본과 구현이 살짝 다름에 주의. 결과는 같다.
    public Vector3 DirFromAngle(float angleDegrees, bool angleIsGlobal)
    {
        if (!angleIsGlobal)
        {
            angleDegrees += transform.eulerAngles.y;
        }

        return new Vector3(Mathf.Cos((-angleDegrees + 90) * Mathf.Deg2Rad), 0, Mathf.Sin((-angleDegrees + 90) * Mathf.Deg2Rad));
    }

    void DrawFieldOfView()
    {
        int stepCount = Mathf.RoundToInt(viewAngle * meshResolution);
        float stepAngleSize = viewAngle / stepCount;
        List<Vector3> viewPoints = new List<Vector3>();
        ViewCastInfo prevViewCast = new ViewCastInfo();

        for (int i = 0; i <= stepCount; i++)
        {
            float angle = transform.eulerAngles.y - viewAngle / 2 + stepAngleSize * i;
            ViewCastInfo newViewCast = ViewCast(angle);          
        
            // i가 0이면 prevViewCast에 아무 값이 없어 정점 보간을 할 수 없으므로 건너뛴다.
            if (i != 0)
            {
                bool edgeDstThresholdExceed = Mathf.Abs(prevViewCast.dst - newViewCast.dst) > edgeDstThreshold;
            
                // 둘 중 한 raycast가 장애물을 만나지 않았거나 두 raycast가 서로 다른 장애물에 hit 된 것이라면(edgeDstThresholdExceed 여부로 계산)
                if (prevViewCast.hit != newViewCast.hit || (prevViewCast.hit && newViewCast.hit && edgeDstThresholdExceed))
                {
                    Edge e = FindEdge(prevViewCast, newViewCast);
                
                    // zero가 아닌 정점을 추가함
                    if (e.PointA != Vector3.zero)
                    {
                        viewPoints.Add(e.PointA);
                    }

                    if (e.PointB != Vector3.zero)
                    {
                        viewPoints.Add(e.PointB);
                    }
                }
            }

            viewPoints.Add(newViewCast.point);
            prevViewCast = newViewCast;
        }

        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();
    }
    
    Edge FindEdge(ViewCastInfo minViewCast, ViewCastInfo maxViewCast)
    {
        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 - minAngle) / 2;
            ViewCastInfo newViewCast = ViewCast(angle);
            bool edgeDstThresholdExceed = Mathf.Abs(minViewCast.dst - newViewCast.dst) > edgeDstThreshold;
            if (newViewCast.hit == minViewCast.hit && !edgeDstThresholdExceed)
            {
                minAngle = angle;
                minPoint = newViewCast.point;
            }
            else
            {
                maxAngle = angle;
                maxPoint = newViewCast.point;
            }
        }

        return new Edge(minPoint, maxPoint);
    }
    
    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);
        }
    }
}
[CustomEditor (typeof (FovTest))]
public class FovEditor : Editor
{
    void OnSceneGUI()
    {
        FovTest fow = (FovTest)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 visible in fow.visibleTargets)
        {
            Handles.DrawLine(fow.transform.position, visible.transform.position);
        }
    }
}

 

다음글을 참고 하고 있다. 

https://nicotina04.tistory.com/198

 

유니티 Field Of View 유닛 시야 구현하기 2

유니티 버전 2020.3.18f1 들어가기 전 참고 이 포스트는 아래의 동영상을 한국어로 재구성한 자료임을 밝힌다. 따라서 구현 순서가 원본과 어느 정도 차이가 있을 수 있다. 어느 정도 영어를 청취할

nicotina04.tistory.com

간단하게 설명을 하자면, 기능과 시야각의 실체를 그리는 영역 두개로 나뉜다.
Findvisibletarge으로 sphere 안에 있는 콜라이더들을 내가 설정한 각도와 거리, ray가 장애물에 닿았는 지에 대한 진위 판단을 하여 타겟 리스트안에 넣어준다. 기능은 이 함수로 종결된다.
관건은 시야를 그리는 데에 있다. 원리는 아래의 그림을 참고하면 이해하기 편하다.
보고자하는 각도, radius(길이), meshresolution(삼각형을 얼마나 많이 그리는 지) 이 세가지의 정보를 토대로 보고자 하는 부채꼴 모양을 삼각형으로 나누어서 그리게 된다.

이렇게만 그리게 되면, 다음과 같은 모습을 그리게 되는 데, 장애물에 ray가 닿게되면 해당 정점으로 삼각형을 그리게 되어 시야가 손실이 온다. 이를 개선하기 위한 방법이 있다.

정점을 더 만드는 것이다. 

 

1. minPoint와 maxPoint 중간점 mid 방향으로 raycast를 쏜다. min과 max를 결정하는 기준은 y축 오일러 각이며 중간점은 두 각의 평균으로 얻어낸다.

2. raycast와 minCast의 hit 상태가 같고 ray의 길이와 오브젝트 중심과 minPoint 사이의 길이 차가 설정한 임계치 이하면 minPoint를 mid로 바꾼다.

3. 그렇지 않으면 maxPoint를 mid로 바꾼다.
그림으로 이해하자면, 없어진 시야각을 점점더 좁혀 주고 있다. 

완성된 원본 모습이다.

profile

ved_Rony

@Rony_chan

검색 태그