목표
1. 플레이어는 시야를 가진다.
2. 해당 시야의 각도를 조절할 수 있다. (사진의 경우 360도로 설정)
3. 장애물에 닿으면 그 뒷쪽은 시야가 닿지 않는다. (사진의 경우 파랑색이 벽)
4. 적들은 내 시야에 들어와 있어야 보이며, 장애물 또한 시야에 닿아야 보인다.
5. 내 시야가 닿지 않는 부분은 검은색으로 칠한다.
구현 방식 간단 요약
1. 플레이어는 지정한 각도로 많은 광선을 쏴서 닿는곳을 체크. (장애물 없을 시 최대거리)
2. 1번에서 얻은 각 광선들의 끝점을 이어 시야 오브젝트를 만듦.
2-1. 추후 처리를 위해 장애물 처리가 안되는 언제나 동그란 시야 오브젝트도 생성.
3. 시야 오브젝트의 스탠실값을 1로 수정.
3-1. 2-1의 오브젝트의 스탠실값을 2로 수정.
4. 적 오브젝트는 해당 부분의 스탠실값이 1일때만 나타나게 함.
5. 장애물 오브젝트는 스탠실값이 2일때만 해당 스탠실을 1로 변경. + 값이 1일때만 표시됨. (시야밖의 장애물은 안나옴)
6. 위에는 검은 판을 덮고, 이 판은 스탠실을 반대로 구현하여 스탠실이 1이 아닌 부분만 나타나게 함.
자세한 설명은 아래 영상/영상을 번역해둔 블로그가 정말 잘 설명해줬으니 거기 가서 보는걸 권장.
나도 그대로 긁어와서 스탠실 쉐이더만 URP용으로 변경하고, 2-1, 3-1의 작업만 추가해줬을 뿐이다.
// 쓰면서 생각났는데 버그있음.
// 저러면 두겹으로 있는 벽들이 시야 범위내에 있으면 뒤쪽 벽도 보이네...
// 내 시야에 처음 닫는 장애물만 보이도록 개선해야할듯.
코드
메인이 되는 FieldOfView 파일.
나도 그대로 긁어와서 스탠실 쉐이더만 URP용으로 변경하고, 2-1, 3-1의 작업만 추가해줬을 뿐이다.
기타 상세한 내용은 아래의 영상/영상을 번역한 블로그 참조.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace Scripts
{
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 class FieldOfView : MonoBehaviour
{
public float viewRadius;
[Range(0, 360)]
public float viewAngle;
[Tooltip("값이 높을수록 계산량이 많아져 더 디테일해짐")]
public float meshResolution;
public LayerMask targetMask, obstacleMask;
// Target mask에 ray hit된 transform을 보관
public List<Transform> visibleTargets = new List<Transform>();
private Mesh _viewMesh;
public MeshFilter viewMeshFilter; //내 시야 범위(장애물 적용)
private Mesh _stencil2Mesh;
private List<Vector3> _stencil2ViewPoints = new List<Vector3>();
public MeshFilter stencil2ViewMeshFilter; //내 시야 범위(장애물 무시)
//중간점 ray의 길이 임계치를 정할 변수 edgeDstThreshold와 이진 탐색 반복 횟수 edgeResolveIterations를 public으로 선언
[Tooltip("값이 높을수록 계산량이 많아져 더 디테일해짐")]
public int edgeResolveIterations;
public float edgeDstThreshold;
private void Start()
{
_viewMesh = new Mesh();
_viewMesh.name = "View Mesh";
viewMeshFilter.mesh = _viewMesh;
_stencil2Mesh = new Mesh();
_stencil2Mesh.name = "Ignoring Obstacles View Mesh";
stencil2ViewMeshFilter.mesh = _stencil2Mesh;
StartCoroutine(FindTargetsWithDelay(0.2f));
}
private void LateUpdate()
{
DrawFieldOfView();
}
private IEnumerator FindTargetsWithDelay(float delay)
{
while (true)
{
yield return new WaitForSeconds(delay);
FindVisibleTargets();
}
}
private 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;
Vector3 dirToTarget = (target.position - transform.position).normalized;
// 플레이어와 forward와 target이 이루는 각이 설정한 각도 내라면
if (Vector3.Angle(transform.forward, dirToTarget) < viewAngle / 2)
{
float dstToTarget = Vector3.Distance(transform.position, target.transform.position);
// 타겟으로 가는 레이캐스트에 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));
}
private 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;
}
createMesh(_viewMesh, viewPoints);
createMesh(_stencil2Mesh, _stencil2ViewPoints);
_stencil2ViewPoints.Clear();
void createMesh(Mesh mesh, List<Vector3> viewPoints)
{
var pair = makeVerticesAndTriangles(viewPoints);
mesh.Clear();
mesh.vertices = pair.Key;
mesh.triangles = pair.Value;
mesh.RecalculateNormals();
}
KeyValuePair<Vector3[], int[]> makeVerticesAndTriangles(List<Vector3> viewPoints)
{
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;
}
}
return new KeyValuePair<Vector3[], int[]>(vertices, triangles);
}
}
private ViewCastInfo ViewCast(float globalAngle, bool stencil2 = true)
{
Vector3 dir = DirFromAngle(globalAngle, true);
RaycastHit hit;
//스탠실2 전용 그냥 원 추가..
if (stencil2)
{
ViewCastInfo info = new ViewCastInfo(false, transform.position + dir * viewRadius, viewRadius, globalAngle);
_stencil2ViewPoints.Add(info.point);
}
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);
}
private 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, false);
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);
}
}
}
쉐이더 코드
Shader "Custom/StencilMask"
{
// 해당 픽셀의 스탠실 레퍼런스를 인스펙터에서 지정한 값으로 변경.
Properties
{
[IntRange] _StencilID("Stencil ID",Range(0,255)) = 0
_MainTex ("Mask Texture", 2D) = "white" {}
_Color ("Mask Color", Color) = (0, 0, 0, 1)
}
SubShader
{
Tags
{
"Queue" = "Geometry-1"
}
Pass
{
Zwrite off
ColorMask 0
Cull Back
Stencil
{
Ref [_StencilID]
Comp always
Pass replace
}
}
}
}
Shader "Custom/StencilReverse"
{
// 해당 픽셀의 스탠실 레퍼런스가 지정한 값과 다를 때만 출력합니다.
Properties
{
[IntRange] _StencilID("Stencil ID",Range(0,255)) = 0
_MainTex ("Mask Texture", 2D) = "white" {}
_Color ("Mask Color", Color) = (0, 0, 0, 1)
}
SubShader
{
Tags
{
"Queue" = "Geometry-1"
}
Pass
{
Stencil
{
Ref [_StencilID]
Comp notequal
Pass keep
}
ColorMask RGBA
//Blend SrcAlpha OneMinusSrcAlpha
//Lighting Off
SetTexture [_MainTex]
{
Combine texture * constant
ConstantColor [_Color]
}
}
}
}
Shader"Custom/StencilTarget"
{
// 해당 픽셀의 스탠실 레퍼런스가 지정한 값과 같을 때만, 출력합니다.
Properties
{
[IntRange] _StencilID("Stencil ID",Range(0,255)) = 0
_MainTex ("Mask Texture", 2D) = "white" {}
_Color ("Mask Color", Color) = (1, 1, 1, 1)
}
SubShader
{
Tags
{
"Queue" = "Geometry-1"
}
Pass
{
Stencil
{
Ref [_StencilID]
Comp Equal
Pass Keep
}
ColorMask RGBA
//Blend SrcAlpha OneMinusSrcAlpha
//Lighting Off
SetTexture [_MainTex]
{
Combine texture * constant
ConstantColor [_Color]
}
}
}
}
Shader"Custom/StencilObstacle"
{
// 해당 픽셀의 스탠실 레퍼런스가 지정한 값보다 더 크다면 덮어씌우고, 출력합니다.
Properties
{
[IntRange] _StencilID("Stencil ID",Range(0,255)) = 0
_MainTex ("Mask Texture", 2D) = "white" {}
_Color ("Mask Color", Color) = (1, 1, 1, 1)
}
SubShader
{
Tags
{
"Queue" = "Geometry-1"
}
Pass
{
Stencil
{
Ref [_StencilID]
Comp LEqual
Pass Replace
}
ColorMask RGBA
//Blend SrcAlpha OneMinusSrcAlpha
//Lighting Off
SetTexture [_MainTex]
{
Combine texture * constant
ConstantColor [_Color]
}
}
}
}
참고 사이트
시야각 구현 참고 (영상/해당 영상 번역+설명 블로그)
https://nicotina04.tistory.com/197
유니티 Field Of View 유닛 시야 구현하기 1
유니티 버전 2020.3.18f1 들어가기 전 참고 이 포스트는 아래의 동영상을 한국어로 재구성한 자료임을 밝힌다. 어느 정도 영어 청취가 가능하다면 동영상을 봐도 좋다. https://youtu.be/rQG9aUWarwE 이 포
nicotina04.tistory.com
URP 스탠실 참고 (블로그/실 적용 프로젝트)
https://giseung.tistory.com/65
[Unity] URP 스텐실(Stencil) 활용하기
스텐실은 GPU의 스텐실 버퍼(Stencil Buffer)를 활용하여특정 픽셀의 렌더링을 제어하는 기능이다. 이번에는 스텐실을 활용한 간단한 예시를 구현해보도록 하겠다.아래의 결과 화면부터 보도록
giseung.tistory.com
https://github.com/BongYunnong/CodingExpress
GitHub - BongYunnong/CodingExpress: [게임코딩급행열차] 게임을 개발하면서 필요한 여러가지 기능들을 구
[게임코딩급행열차] 게임을 개발하면서 필요한 여러가지 기능들을 구현해보는 프로젝트. Contribute to BongYunnong/CodingExpress development by creating an account on GitHub.
github.com
'Unity' 카테고리의 다른 글
[DoTween] Ease 곡선 종류 시각화 & 애니메이션 확인 사이트 (0) | 2024.11.01 |
---|---|
[Unity] LocalizedString에서 테이블 이름과 키로 값 가져오기. (0) | 2024.09.05 |
댓글