이전 글에서 Mesh 클래스로 원하는 대로 도형을 생성할 수 있었다.
https://giseung.tistory.com/54
[Unity] Mesh를 생성하여 도형 만들기
3D 게임을 만들기 위해서는 다양한 모델링을 활용한다. 이 모델링들은 Mesh 데이터를 통해 형상화하게 되는데, 유니티에서는 Mesh 클래스를 생성해서 원하는 도형을 만들 수 있다. 이번 글에서는 Me
giseung.tistory.com
이를 활용하여, 포물선을 그리는 방법을 소개하겠다.
세부 내용은 뒤로 미루고 결과부터 보여주겠다.
결과.gif





스크립트
using UnityEngine;
using System.Collections.Generic;
public class MeshParabola : MonoBehaviour
{
[Header("Component")]
[Tooltip("시작 위치")]
public Transform from;
[Tooltip("종료 위치")]
public Transform to;
[Header("Draw")]
[Tooltip("면의 개수. 클수록 부드러운 선이 된다.")]
[Min(1)] public int face = 20;
[Tooltip("선의 높이")]
public float height = 0.15f;
[Tooltip("거리에 따라 높이를 조정한다.")]
public bool heightByDistance = true;
[Tooltip("선의 두께")]
public float thickness = 0.05f;
[Tooltip("Off : 양면 렌더링, Front : 뒷면 렌더링, Rear : 앞면 렌더링, Both : 렌더링 안함.")]
public Cull cull = Cull.Off;
[Tooltip("사용할 쉐이더")]
public string shader = "Unlit/Transparent";
[Header("Texture")]
[Tooltip("머티리얼에 적용할 텍스쳐")]
public Texture texture;
[Tooltip("텍스쳐 반복 횟수")]
public float tiling = 1f;
[Tooltip("흐르는 속도")]
public float flowSpeed = 4f;
// Component
private MeshRenderer _meshRenderer;
private MeshFilter _meshFilter;
// Mesh
private Mesh _mesh;
private List<Vector3> _vertices;
private List<Vector2> _uvs;
private List<int> _triangles;
// Texture
private float _flow;
public enum Cull
{
Off = 0, //Cull 하지 않는 상태. 양면 렌더링.
Front = 1, //Front를 Cull하는 상태. 뒷면 렌더링.
Back = 2, //Back을 Cull하는 상태. 앞면 렌더링.
Both = 3 //모두 Cull하는 상태. 렌더링 안함.
}
//__________________________________________________________________________ Initialize
private void Awake()
{
initializeMesh();
initializeComponent();
}
private void initializeMesh()
{
_mesh = new Mesh(); //새로운 Mesh 생성
_vertices = new List<Vector3>(); //정점 데이터를 저장할 List
_uvs = new List<Vector2>(); //UV 데이터를 저장할 List
_triangles = new List<int>(); //Triangle 데이터를 저장할 List
}
[ContextMenu("Initialize Component")]
private void initializeComponent()
{
_meshRenderer = GetComponentInChildren<MeshRenderer>(true); //MeshRenderer 찾아보고,
if (_meshRenderer == null) _meshRenderer = gameObject.AddComponent<MeshRenderer>(); //없으면 추가
_meshRenderer.material = new Material(Shader.Find(shader)); //쉐이더 적용한 머티리얼 생성하고,
_meshRenderer.material.mainTexture = texture; //텍스쳐도 지정
_meshFilter = GetComponentInChildren<MeshFilter>(true); //MeshFilter 찾아보고,
if (_meshFilter == null) _meshFilter = gameObject.AddComponent<MeshFilter>(); //없으면 추가
_meshFilter.mesh = _mesh; //위에서 생성한 Mesh 지정
}
//__________________________________________________________________________ Draw
private void Update()
{
draw();
}
private void draw()
{
if (from != null && to != null) //From과 To 모두 있어야한다.
{
float division = 1f / face; //각 면의 t(보간비율)를 구하기 위한 값
float currV = 0f; //현재까지의 V값. UV중 V값이다.
float distance = Vector3.Distance(from.position, to.position); //From과 To의 거리
for (int i = 0; i < face; ++i) //면의 개수만큼 반복
{
float startT = division * i; //시작 t
float endT = startT + division; //끝 t
Vector3 adjFromPos = from.position - transform.position; //현재 오브젝트가 원점이 아닐 수 있으므로 조정
Vector3 adjToPos = to.position - transform.position; //현재 오브젝트가 원점이 아닐 수 있으므로 조정
GetPoints(adjFromPos, adjToPos, height, startT, thickness, out Vector3 startL, out Vector3 startR); //시작 지점의 양쪽 포인트를 구한다.
GetPoints(adjFromPos, adjToPos, height, endT, thickness, out Vector3 endL, out Vector3 endR); //끝 지점의 양쪽 포인트를 구한다.
float startV = currV + _flow; //시작 지점의 V. Flow 값을 더해준다.
float endV = startV + distance * tiling / face / thickness; //끝 지점의 V. V값에 영향을 주는 요소들을 계산한다.
stackMeshSquare(
new Vector3[] { startL, endL, endR, startR },
new Vector2[] { new Vector2(0f, startV), new Vector2(0f, endV), new Vector2(1f, endV), new Vector2(1f, startV) }); //면(사각형) 하나를 쌓는다.
currV = endV - _flow; //다음 V값을 넘겨준다.
}
}
calculateFlow();
applyMesh();
}
private void calculateFlow()
{
_flow -= flowSpeed * Time.deltaTime; //Flow 속도를 적용한다.
_flow = _flow > 1f ? _flow % 1f : _flow < 0f ? 1f - _flow + Mathf.Ceil(_flow) : _flow; //항상 0과 1사이의 값을 가지도록 한다.
}
//__________________________________________________________________________ Mesh
private void stackMeshSquare(Vector3[] vertices, Vector2[] uvs)
{
int vertLen = _vertices.Count; //이전 정점의 개수를 가져온다.
if (Cull.Front != (cull & Cull.Front)) //비트 연산으로 Front가 포함되지 않으면 시계방향으로 사각형을 쌓는다.
{
_triangles.Add(vertLen);
_triangles.Add(vertLen + 1);
_triangles.Add(vertLen + 2);
_triangles.Add(vertLen + 2);
_triangles.Add(vertLen + 3);
_triangles.Add(vertLen);
}
if (Cull.Back != (cull & Cull.Back)) //비트 연산으로 Back이 포함되지 않으면 반시계방향으로 사각형을 쌓는다.
{
_triangles.Add(vertLen);
_triangles.Add(vertLen + 3);
_triangles.Add(vertLen + 2);
_triangles.Add(vertLen + 2);
_triangles.Add(vertLen + 1);
_triangles.Add(vertLen);
}
for (int i = 0, l = vertices.Length; i < l; ++i) //정점을 추가한다.
_vertices.Add(vertices[i]);
for (int i = 0, l = uvs.Length; i < l; ++i) //UV를 추가한다.
_uvs.Add(uvs[i]);
}
private void applyMesh()
{
if (_mesh == null) return;
// 지금까지 쌓은 정점, UV, 사각형 데이터를 적용한다.
if(_mesh.vertices.Length > _vertices.Count) //새로운 정점의 개수가 더 적으면
{
_mesh.triangles = _triangles.ToArray(); //Triangle 먼저 적용한다.
_mesh.vertices = _vertices.ToArray();
_mesh.uv = _uvs.ToArray();
}
else //새로운 정점의 개수가 더 많거나 같으면
{
_mesh.vertices = _vertices.ToArray(); //정점 & UV 먼저 적용한다.
_mesh.uv = _uvs.ToArray();
_mesh.triangles = _triangles.ToArray();
}
_mesh.RecalculateNormals(); //노멀 재계산
_mesh.bounds = new Bounds(Vector3.zero, Vector3.one * float.MaxValue); //Bound를 최대로 지정하여 카메라에 컬링되지 않도록 한다.
_vertices.Clear(); //정점 데이터 초기화
_uvs.Clear(); //UV 데이터 초기화
_triangles.Clear(); //삼각형 데이터 초기화
}
//__________________________________________________________________________ Formula
public void GetPoints(Vector3 from, Vector3 to, float height, float t, float thickness, out Vector3 left, out Vector3 right) //포물선 공식으로 양쪽 포인트를 구하는 함수
{
Vector3 mid = GetPoint(from, to, height, t); //포물선 위치 저장
Vector3 dir = Vector3.Scale(to - from, new Vector3(1f, 0f, 1f)); //From에서 To로 가는 방향벡터를 높이를 무시하고 구한다.
Quaternion lookRot = Quaternion.LookRotation(dir); //방향 벡터를 바라보는 각도 저장
left = mid + lookRot * Vector3.left * thickness * 0.5f; //왼쪽 포인트 반환
right = mid + lookRot * Vector3.right * thickness * 0.5f; //오른쪽 포인트 반환
}
public Vector3 GetPoint(Vector3 from, Vector3 to, float height, float t) //포물선 공식 함수
{
Vector3 midPos = Vector3.Lerp(from, to, t); //중간 위치
if(heightByDistance) height *= Vector3.Distance(from, to); //거리에 따라 높이를 조절
float form = -4 * height * t * t + 4 * height * t; //포물선 공식
return new Vector3(midPos.x, Mathf.Lerp(from.y, to.y, t) + form, midPos.z); //결과 위치 반환
}
}
※ 대부분의 코드에 주석이 달려있어 분석하는데 유용할 것이다.
※ 이전 글에서 Mesh가 생성되는 원리를 이해하는 것을 권장한다.
포물선을 만드는 원리는 사각형을 여러개 이어주는 것이다.
필요한 사각형들을 stackMeshSquare 함수로 쌓고,
마지막에 applyMesh 함수로 반영해주는 것이다.
※ applyMesh 함수에서 새로운 정점의 개수를 판단하는 것은
Mesh 클래스 자체의 오류 때문이다.
만약, 새로운 정점의 개수가 더 적은 경우에 Vertices에 할당하면,
Triangles가 인덱스를 초과했다는 오류문구가 뜬다.
중요한 것은 매 프레임마다 UV를 변경해주는 것이다.
UV에 V값에 비례하는 것은 타일링, 두 지점의 거리이고,
반비례하는 것은 면의 개수, 선의 두께이다.
비례하는 것은 곱해주고, 반비례하는 것은 나눠주면,
항상 일정한 텍스쳐 모양이 나온다.
여기에 Flow 변수를 더해주면 흘러가는 형태를 표현할 수 있다.
stackMeshSquare 함수에서 사용한 비트 연산을 간단히 설명하겠다.
2진수로 Cull(Enum)을 표현하면 아래와 같다.
Off : 00(0)
Front : 01(1)
Back : 10(2)
Both : 11(3)
만약, Front이거나 Both이면 Front(01) And 연산후 01이므로 앞면을 그리지 않고,
Back이거나 Both이면 Back(10) And 연산후 10이므로 뒷면을 그리지 않는다.
Off이면 어떤 And 연산이든 00이므로 양면을 모두 그린다.
사용 방법
위 스크립트를 Inspector 창에서 보면 아래와 같다.
각 옵션에 대해 알아보자.

From, To : 시작 지점과 끝 지점을 나타낸다. 하나라도 없으면 아무것도 나타나지 않는다.

Face : 면의 개수이다. 클수록 부드러운 선이 된다.

Height : 선의 높이다.

Height By Distance : 거리에 따라 높이를 조절하는 옵션이다. 해제하면 높이가 유지된다.

Thickness : 선의 두께이다.

Cull : 컬링할 면을 선택할 수 있다.
Shader : 사용할 쉐이더이다. 쉐이더 경로를 적어주면 된다.

Texture : 사용할 텍스쳐이다.

Tiling : 텍스쳐 반복 정도이다. 클수록 촘촘해진다.

FlowSpeed : 흐르는 속도이다.
어찌보면 이 기능이 목표 지점을 가리키는 가이드라인으로 사용하기에도 좋을 것 같다.
추가로 이 글에서는 보여주지 못했지만 텍스쳐가 반투명이어도 된다.
설명이 잘못되었거나 질문이 있다면 언제든지 댓글을 달아주길 바란다.
'[Unity]' 카테고리의 다른 글
[Unity] 커스텀 단축키 만들기 (37) | 2024.01.11 |
---|---|
[Unity] 자유시점 카메라 구현하기 (26) | 2024.01.02 |
[Unity] Mesh를 생성하여 도형 만들기 (20) | 2023.12.18 |
[Unity] GL로 카메라 페이드 효과 구현하기 (51) | 2023.10.05 |
[Unity] WebGL 빌드 시 웹 페이지에서 키보드 입력받기 (37) | 2023.10.03 |
댓글