1일1알

지형지물에 따라 메쉬 모양 변형시키기 본문

유니티/Project Nammo

지형지물에 따라 메쉬 모양 변형시키기

영춘권의달인 2024. 8. 9. 14:53

 

구 형태의 메쉬가 지형에 따라 변형되는 것을 구현해봤다.

private MeshFilter _meshFilter;
private Mesh _mesh;

Vector3[] _originVertices;
Vector3[] _vertices;
bool[] _deformCheck;

void Start()
{
    _meshFilter = GetComponent<MeshFilter>();
    _mesh = _meshFilter.mesh;

    GetVertices();
}

private void GetVertices()
{
    _originVertices = new Vector3[_mesh.vertices.Length];
    _vertices = new Vector3[_mesh.vertices.Length];
    _deformCheck = new bool[_mesh.vertices.Length];
    for (int i = 0; i < _mesh.vertices.Length; i++)
    {
        _originVertices[i] = _mesh.vertices[i];
        _vertices[i] = _mesh.vertices[i];
    }
}

우선 MeshFilter를 통해 현재 메쉬의 정점 정보들을 가져온다.

originVertices는 원본 메쉬의 정점 정보를 보관하기 위한 정보이고

vertices는 지형에 따라 정점의 정보가 변경될 때 메쉬에 적용할 정보이다.

deformCheck는 메쉬가 부드럽게 변경되도록 하기 위해 사용했고 크게 중요하지는 않다.

 

float[] _dirX = new float[12] { 0, 0.5f, 0.86f, 1, 0.86f, 0.5f, 0, -0.5f, -0.86f, -1, -0.86f, -0.5f };
float[] _dirY = new float[12] { 1, 0.86f, 0.5f, 0, -0.5f, -0.86f, -1, -0.86f, -0.5f, 0, 0.5f, 0.86f };

private void Update()
{
    DeformVertices();
    UpdateVertices();
}

private void DeformVertices()
{
    for (int i = 0; i < _vertices.Length; i++)
    {
        _deformCheck[i] = false;
    }
    for (int i = 0; i < 12; i++) // 12방향 ray 탐색
    {
        RaycastHit2D hit;
        float rayDist = 5f;
        hit = Physics2D.Raycast(transform.position, new Vector2(_dirX[i], _dirY[i]), rayDist, LayerMask.GetMask("Ground")); // Ground Layer만 탐색
        if (hit)
        {
            Debug.DrawLine(transform.position, hit.point, Color.green);

            // Ray에 탐색된 오브젝트 내의 가장 가까운 좌표를 추출
            Vector2 closestPos = hit.collider.ClosestPoint(transform.position);
            // 탐색된 가장 가까운 좌표를 local로 변환
            Vector2 localClosestPos = transform.InverseTransformPoint(closestPos);
            // 충돌 지점을 local 좌표계로 변환
            Vector2 localHitPoint = transform.InverseTransformPoint(hit.point);
            // 위의 두 좌표를 통해 직선의 벡터를 구한다
            Vector2 localHitObjectVector = localClosestPos - localHitPoint;
            if (localHitObjectVector.magnitude > 0.03f) localHitObjectVector = localHitObjectVector.normalized;
            // 내적을 통해 투영 길이를 구한다
            float projection = Vector2.Dot(-localHitPoint, localHitObjectVector);
            // 최단 벡터를 구한다. (원점에서 투영된 점까지의 벡터)
            Vector2 projectedPoint = localHitPoint + localHitObjectVector * projection;
            for (int j = 0; j < _vertices.Length; j++)
            {
                // 충돌 지점까지의 벡터와 모든 정점들까지의 벡터를 검사
                float value = Vector2.Angle(new Vector2(localHitPoint.x, localHitPoint.y), new Vector2(_originVertices[j].x, _originVertices[j].y));
                float maxAngle = 15f;
                // 각도가 15도 이상이면 스킵, 다른 영역에 맡긴다.
                if (value > maxAngle) continue;

                // 최단 벡터와 정점까지의 벡터의 사이각을 구한다.
                float angle = Vector2.Angle(projectedPoint, new Vector2(_originVertices[j].x, _originVertices[j].y));

                // 삼각함수 응용하면 이런 식을 얻을 수 있음
                float vertexLen = (projectedPoint.magnitude) / Mathf.Cos(angle * Mathf.Deg2Rad);
                vertexLen /= rayDist;
                vertexLen = Mathf.Clamp(vertexLen, 0, 1);
                _vertices[j] = Vector3.Slerp(_vertices[j], _originVertices[j] * vertexLen, _deformSpeed);
                _deformCheck[j] = true;
            }
        }
        else
        {
            Debug.DrawLine(transform.position, transform.position + new Vector3(_dirX[i], _dirY[i]) * 5f, Color.red);
        }
    }
    for (int i = 0; i < _vertices.Length; i++)
    {
        if (_deformCheck[i] == false)
        {
            _vertices[i] = Vector3.Slerp(_vertices[i], _originVertices[i], _deformSpeed);
        }
    }
}
private void UpdateVertices()
{
    _mesh.vertices = _vertices;
    _mesh.RecalculateBounds();
}

1. 12방향으로 Ray를 쏘면서 지형지물 Layer에 맞는 오브젝트만 탐지한다.

 

2. ClosestPoint 함수를 이용해 구의 중앙에서 Ray에 탐지된 오브젝트까지의 가장 가까운 좌표를 추출한다.

 

3. 추출된 좌표를 world를 기준으로 한 좌표이고, 이것을 구의 local좌표계로 변환한다.

 

4. Ray에 충돌된 좌표 또한 구의 local좌표계로 변환한다.

 

여기까지의 과정을 그림으로 표현한 것이다. 편의상 Ray는 1개만 쏘는 상태이다.

 

5. hitPoint, closestPos 두 좌표를 통해 직선의 벡터를 구한다.

 

6. 직선의 벡터와 hitPoint 벡터의 내적을 통해 투영되는 길이를 구한다. 여기서 hitPoint의 부호를 바꿔주는 이유는 두 벡터의 시작지점을 맞춰주기 위해서이다.

 

7. 충돌 지점, 오브젝트 표면의 방향, 투영된 길이를 통해 구의 원점에서 투영된 점까지의 벡터를 구할 수 있고, 이것이 구의 원점에서 오브젝트의 표면의 직선까지의 최단 벡터이다.

 

8. 12방향으로 Ray를 쏘기 때문에 하나의 Ray가 담당하는 각도는 360/12 = 30 도이다. 양옆으로 15도씩 담당하면 되기 때문에 충돌 지점까지의 벡터와 모든 정점들 사이의 각도를 구해서 만약 15도 이상이면 스킵한다.

 

 

9. 15도 내에 있는 정점들에 대해 7번 과정에서 구한 최단 벡터와 정점까지의 벡터의 사이각을 구한다.

10. 9번 과정에서 만들어진 삼각형을 옆으로 돌리면 이런 삼각형이 되고, 사이각과 최단거리를 알고있기 때문에 삼각함수를 통해 빗변의 길이를 구할 수 있다.

 

11. 이 빗변의 길이를 통해 정점이 위치해야 할 좌표를 구할 수 있고 UpdateVertices함수를 통해 정점을 재설정하여 메쉬의 모양을 변형시킬 수 있다.

 

'유니티 > Project Nammo' 카테고리의 다른 글

적 쉐이더(2D)  (0) 2024.08.09
유니티로 언리얼의 GAS 시스템 따라하기  (0) 2024.07.31