본문 바로가기

유니티/Catlikecoding

[메시 기초] 메시 변형 (Catlikecoding)

반응형

해당 글은 CatlikeCoding의 튜토리얼을 실습하며 번역해놓은 글입니다. 번역 과정에서 제가 내용을 덧붙이거나 삭제한 부분이 존재합니다. 원본의 링크는 글 하단에 있습니다.

 

 원본 글에 적혀있지 않은 내용을 추가할 때에는 (작성자 추가: ..내용)과 같은 형태로 작성하겠습니다.

 

요약

- 물체에 광선을 투사하고 디버그 라인을 그립니다.

- 힘을 정점의 이동량으로 변환합니다.

- 스프링과 댐핑으로 형태를 유지합니다.

- 오브젝트의 변형을 보정합니다.

 

1. 씬 설정

중앙에 하나의 큐브 구체가 있는 씬부터 시작하겠습니다. 변형을 제대로 테스트 하기 위해서는 구에 적절한 수의 정점이 포함되어야 합니다. 구의 격자 크기를 20으로 설정하고 반지름을 1로 설정했습니다.

 

2. 매시 변형

변형을 처리할 'MeshDeformer' 스크립트를 생성합니다. 큐브 구체 컴포넌트와 마찬가지로 작업할 메시 필터가 필요합니다.

 

using UnityEngine;

[RequireComponent(typeof(MeshFilter))]
public class MeshDeformer : MonoBehaviour
{
}

 

 메시 필터만 필요하다는 점에 유의하세요. 메시를 어떻게 가져오는지는 상관이 없습니다. 지금은 절차적으로 생성한 큐브 구체를 사용하고 있지만, 해당 스크립트는 어떤 메시에서든 사용할 수 있어야 합니다.

 

 변형을 수행하려면 메시에 접근할 수 있어야 합니다. 메시에 접근하면 변형하기 전의 버텍스 위치를 추출할 수 있습니다. 또한 변형하는 동안의 변화된 버텍스를 추적해야 합니다. 'Start'메서드에서 메시와 그 정점을 가져오고 'originalVertices'을 'displacedVertices'으로 복사합니다.

 

    private void Start()
    {
        deformingMesh     = GetComponent<MeshFilter>().mesh;
        originalVertices  = deformingMesh.vertices;
        displacedVertices = new Vector3[originalVertices.Length];
        for (int i = 0; i < originalVertices.Length; i++)
            displacedVertices[i] = originalVertices[i];
    }

 

메시가 변형되면 정점이 움직입니다. 따라서 각 정점의 속도도 저장해야 합니다.

 

    Vector3[] vertexVelocities;

    private void Start()
    {
    	...
        vertexVelocities = new Vector3[originalVertices.Length];
    }

 

 메시가 변형되는 방식을 제어할 방법이 필요합니다. 이를 위해 사용자 입력을 사용할 것입니다. 사용자가 오브젝트를 터치할 때 마다 그 지점에 힘을 가할 것입니다. 'MeshDeformer' 컴포넌트는 메시의 변형을 처리하지만 입력 방식은  책임지지 않습니다. 사용자 입력을 처리하기 위해 별도의 컴포넌트(MeshDeformerInput)를 생성하겠습니다. 이 컴포넌트는 사용자의 시점을 나타내는 카메라에 부착하는 것이 가장 합리적입니다. 씬에 여러개의 MeshDeformer 오브젝트가 있을 수 있으므로 해당 컴포넌트에 함께 부착해서는 안됩니다.

 

 우리는 유저가 마우스 버튼을 누르고 있는 동안 입력 이벤트를 처리할 것입니다. 따라서 클릭이나 드래그가 있을 때 마다 이벤트를 처리해주면 됩니다. 마우스를 클릭했을 때 사용자가 어디를 가리키고 있는지 파악해야 합니다. 카메라에서 씬으로 광선(ray)을 투사하여 이를 수행합니다. 씬의 메인 카메라를 가져와서 커서의 위치를 이용하여 광선을 처리하는데 사용합니다.

 

 물리 엔진을 사용하여 광선을 캐스팅하고 광선이 부딪힌 대상에 대한 정보를 저장합니다. 광선이 무언가와 충돌한 경우, 충돌한 오브젝트에서 MeshDeformer 컴포넌트를 가지고 있는데 검색할 수 있습니다.

 

public class MeshDeformerInput : MonoBehaviour
{
    public float force = 10f;
    void Update () 
    {
        if (Input.GetMouseButton(0)) 
        {
            HandleInput();
        }
    }
    void HandleInput () 
    {
        Ray inputRay = Camera.main.ScreenPointToRay(Input.mousePosition);
        RaycastHit hit;
        if (Physics.Raycast(inputRay, out hit)) 
        {
            MeshDeformer deformer = hit.collider.GetComponent<MeshDeformer>();
        }
    }
}

 

 광선이 무언가와 부딪혔는데 그 무언가에 MeshDeformer 컴포넌트가 있다면, 그 무언가를 변형시킬 수 있습니다. 이제 메시와의 충돌지점에 변형을 가해봅시다. 메시에 변형을 가하기 전에, 광선을 시각화하기 위해 메인 카메라에서 충돌 지점까지 디버그 선을 그려봅시다.

 

// MeshDeformerInput.cs
    void HandleInput () 
    {
        Ray inputRay = Camera.main.ScreenPointToRay(Input.mousePosition);
        RaycastHit hit;
        if (Physics.Raycast(inputRay, out hit)) 
        {
            MeshDeformer deformer = hit.collider.GetComponent<MeshDeformer>();
            if (deformer) 
            {
                Vector3 point = hit.point;
                deformer.AddDeformingForce(point, force);
            }
        }
    }
// MeshDeformer.cs
    public void AddDeformingForce (Vector3 point, float force) 
    {
        Debug.DrawLine(Camera.main.transform.position, point);
    }

 

 메시가 사용자에 의해 찌그러지거나 음푹 패일 수 있는 기능을 제작하려고 합니다. 이를 위해서는 충돌점 근처의 버텍스가 표면의 안으로 밀려나야 합니다. 하지만 변형력에는 고유한 방향이 없습니다. 모든 방향에 동일하게 적용됩니다. 따라서 평평한 표면의 정점은 안쪽으로 밀려나는 것이 아니라 서로 멀어지게 됩니다. 힘이 작용되는 중심점을 표면에서 멀어지게 함으로써 방향을 추가할 수 있습니다. 약간의 오프셋은 버텍스가 힘점으로부터 표면 안으로 밀려나가는 방향에 위치할 수 있도록 보장합니다. 접점의 법선을 오프셋의 방향으로 사용할 수 있습니다.

 

(작성자 추가: 위 예시는 평평한 표면에서 충돌지점을 기준 힘 점으로 삼아 버텍스들을 밀어내는 경우, 버텍스가 표면 안으로 밀려 들어가는 느낌이 아닌 단순히 멀어지는것처럼 보이게 됨, 그래서 충돌 지점의 법선을 기준으로 약간의 오프셋을 더해주면 아래 예시처럼 버텍스들이 표면 안으로 밀려 들어가는것처럼 이동시킬 수 있게됨)

 

    public float forceOffset = 0.1f;
    
    void HandleInput () 
    {
        Ray inputRay = Camera.main.ScreenPointToRay(Input.mousePosition);
        RaycastHit hit;
        if (Physics.Raycast(inputRay, out hit)) 
        {
            MeshDeformer deformer = hit.collider.GetComponent<MeshDeformer>();
            if (deformer) 
            {
                Vector3 point = hit.point;
                point += hit.normal * forceOffset;
                deformer.AddDeformingForce(point, force);
            }
        }
    }

 

4. 기본 변형

 이제 실제로 변형을 가할 차례입니다. MeshDeformer.AddDeformingForce는 현재 변위된 모든 버텍스들을 순회하며 각 버텍스에 개별적으로 변형력을 적용해야 합니다. 메쉬는 각 정점에 힘이 가해지기 때문에 변형됩니다. 정점이 밀려나면서 속도(velocity)를 얻게 됩니다. 시간이 지남에 따라 정점은 모두 위치를 변경합니다. 모든 정점에 똑같은 힘이 가해지면 물체 전체가 모양을 바꾼다기 보다는 움직이는 것처럼 보일 것입니다. 하지만 우리는 그렇게 하진 않을 것입니다.

 

 큰 폭발이 일어났다고 생각해보세요. 그라운드 제로에 있다면 당신은 죽습니다. 근처에 있으면 쓰러집니다. 멀리 떨어져 있다면 문제 없습니다. 힘은 거리에 따라 감소합니다. 이 감쇠는 방향의 차이와 결합하여 물체에 변형을 일으킵니다. 따라서 정점당 변형되는 힘의 방향과 거리를 모두 알아야 합니다. 두 가지 모두 힘이 작용되는 지점에서 정점 위치를 가리키는 벡터에서 도출해낼 수 있습니다. 그리고 감쇠된 힘은 역제곱 법칙을 사용하여 구할 수 있습니다. 원래의 힘을 거리 제곱으로 나누면 됩니다. 실제로는 거리 제곱에 1을 더해주고 있는데, 이렇게 하면 거리가 0일 때 힘이 최대 강도를 갖도록 보장합니다. 그렇지 않으면 힘은 지점에 가까워질수록 무한대를 향해 발산합니다.

 

1/x^2 and a/(1+x^2)

 이제 힘을 얻었으니 이를 속도 velocity delta(속도 변화량)값으로 변환할 수 있습니다. 실제로 힘은 먼저 가속도로 변환됩니다. (a = F/m) 그런 다음 속도 변화는 다음과 같습니다. (Δv = aΔt) 간단하게 하기 위해 질량을 각 버텍스에 하나씩 있는 것처럼 생각하겠습니다. 따라서 다음과 같이 정리할 수 있습니다. (Δv = FΔt)

 

 이 시점에서 속도 변화량 값은 있지만 아직 방향에 대한 정보는 없습니다. 방향은 힘이 시작된 포지션에서 정점을 향하는 벡터를 정규화해주면 됩니다. 그런 다음 이 결과에 속도 변화량을 곱해서 정점의 속도값에 더해주면 됩니다.

 

    void AddForceToVertex (int i, Vector3 point, float force) 
    {
        Vector3 pointToVertex   = displacedVertices[i] - point;
        float   attenuatedForce = force / (1f + pointToVertex.sqrMagnitude);
        float   velocity        = attenuatedForce * Time.deltaTime;
        vertexVelocities[i] += pointToVertex.normalized * velocity;
    }

 

 이제 정점에 속도가 생겼으니 정점을 움직일 수 있습니다. Update 이벤트 메서드를 추가하여 각 정점을 처리합니다. 그런 다음 메쉬가 실제로 변경되도록 변위된 정점을 메쉬에 할당합니다. 메쉬의 모양이 변경되었으므로 법선도 다시 계산해야 합니다.

찌그러진 구

 

    void Update() 
    {
        for (int i = 0; i < displacedVertices.Length; i++) 
        {
            UpdateVertex(i);
        }
        deformingMesh.vertices = displacedVertices;
        deformingMesh.RecalculateNormals();
    }

    void UpdateVertex(int i) 
    {
        Vector3 velocity = vertexVelocities[i];
        displacedVertices[i] += velocity * Time.deltaTime;
    }

 

5. 모양 유지하기

 이제 정점에 힘을 가하자마자 정점이 움직이기 시작합니다. 하지만 한번의 인풋이 생긴 이후 변형이 멈추지 않고 계속됩니다. 정점들이 계속 움직이기 때문에 물체의 원래 모양이 사라지게 됩니다. 이제 물체가 원래 모양으로 돌아올 수 있게 만들어 봅시다.

 

 실제 물체는 고체이며 변형되는 동안 압축되고 늘어납니다. 또한 이러한 변형에 저항합니다. 물체에 가했던 힘이 사라지면 원래 모양으로 돌아갈 수 있습니다. 우리에게는 실제 체적이 없고 표면을 나타내기 위한 정점들의 집합만이 있습니다. 이것으로는 현실적인 물리 시뮬레이션을 수행할 수 없습니다. 하지만 그건 문제가 되지 않습니다. 우리에게 필요한 것은 사실적으로 보이는 것 뿐이기 때문입니다.

 

(작성자 추가: 이 예시에서 다루는 실제 물체는 스펀지 같은 것을 생각하면 좋을듯!)

 

 우리는 각 정점의 원래 위치와 변형된 위치를 모두 추적하고 있습니다. 각 버텍스의 두 버전 사이에 스프링을 연결한다고 상상해보겠습니다. 변형된 정점이 원본에서 멀어질 때마다 스프링이 다시 잡아당깁니다. 변형된 정점이 멀리 떨어져 있을수록 스프링의 당기는 힘이 강해집니다.

 

    public float springForce = 20f;
    void UpdateVertex(int i) 
    {
        Vector3 velocity     = vertexVelocities[i];
        Vector3 displacement = displacedVertices[i] - originalVertices[i];
        velocity             -= displacement * springForce * Time.deltaTime;
        vertexVelocities[i]  =  velocity;
        displacedVertices[i] += velocity * Time.deltaTime;
    }

 

 이제 정점은 변형에 저항하고 원래 위치로 다시 튀어오릅니다. 하지만 오버슈팅이 발생하고 변형이 끈임없이 발생합니다. 이는 정점이 스스로 원래의 위치로 돌아가는 동안 스프링이 계속 당겨져 속도 변화량이 변되기 때문에 발생합니다. 정점을 속도를 지속적으로 느리게 만든다면 이러한 영원한 진동을 방지할 수 있습니다. 이 감쇠(damping)효과는 저항, 항력, 관성 등을 대체할 수 있습니다. 시간이 지남에 따라 속도를 감쇠시키는 간단한 요소입니다. (v_d=v(1−dΔt)) 댐핑 수치가 클수록 물체가 덜 탱탱하고 효과가 더 느리게 나타납니다.

 

    public float damping     = 5f;
    
    void UpdateVertex(int i) 
    {
        Vector3 velocity     = vertexVelocities[i];
        Vector3 displacement = displacedVertices[i] - originalVertices[i];
        velocity             -= displacement * springForce * Time.deltaTime;
        velocity             *= 1f - damping * Time.deltaTime;
        vertexVelocities[i]  =  velocity;
        displacedVertices[i] += velocity * Time.deltaTime;
    }

 

6. 트랜스폼 변환 처리하기

 이제 오브젝트의 트랜스폼을 변형할때를 제외하고는 메시 변형이 완전히 작동합니다. 모든 계산은 로컬 공간에서 수행됩니다. 이제 구를 이동하거나 회전해보세요. 변형력이 잘못 적용되는 것을 확인할 수 있습니다. 물체의 변형을 보정해야 합니다. 변형력의 위치를 월드 스페이스에서 로컬 스페이스로 변환하여 이를 수행합니다.

 

    public void AddDeformingForce (Vector3 point, float force) 
    {
        point = transform.InverseTransformPoint(point);
        for (int i = 0; i < displacedVertices.Length; i++) 
            AddForceToVertex(i, point, force);
    }

 

 이제 힘이 올바른 위치에 적용되었지만 여전히 이상합니다. 구의 크기를 변경시켜서 테스트해보면 메시의 변형이 오브젝트의 스케일에 비례하게 스케일링되는 것을 알 수 있습니다. 이것은 올바르지 않습니다. 작은 물체와 큰 물체는 동일한 물리학의 적용을 받아야 합니다. 물체의 스케일을 보정해야 합니다. 먼저 물체의 스케일을 알아야 합니다. 트랜스폼의 localScale축 중 하나를 확인하면 이를 찾을 수 있습니다. 이 작업을 업데이트할 때마다 수행하면 스케일이 동적으로 변경되는 오브젝트에 대해 어느 정도 대응할 수 있습니다. 'uniformScale' 필드를 추가합니다.

 

    float uniformScale = 1f;

    void Update() 
    {
        uniformScale = transform.localScale.x;
        ...
    }
    
    void UpdateVertex(int i) 
    {
        Vector3 velocity     = vertexVelocities[i];
        Vector3 displacement = displacedVertices[i] - originalVertices[i];
        displacement         *= uniformScale;
        velocity             -= displacement * springForce * Time.deltaTime;
        velocity             *= 1f - damping * Time.deltaTime;
        vertexVelocities[i]  =  velocity;
        displacedVertices[i] += velocity * (Time.deltaTime / uniformScale);
    }

    public void AddDeformingForce (Vector3 point, float force) 
    {
    	...
    } 
    
    void AddForceToVertex (int i, Vector3 point, float force) 
    {
        Vector3 pointToVertex   = displacedVertices[i] - point;		
        pointToVertex *= uniformScale;
        float   attenuatedForce = force / (1f + pointToVertex.sqrMagnitude);
        float   velocity        = attenuatedForce * Time.deltaTime;
        vertexVelocities[i] += pointToVertex.normalized * velocity;
    }

다른 스케일, 같은 힘의 적용

 이제 완성되었습니다. 모든 위치, 회전 및 균일한 스케일에서 작동하는 변형 메시입니다. 이것은 간단하고 비교적 저렴한 시각 효과라는 점을 명심하세요. 소프트 바디 물리 시뮬레이션이 아닙니다. 오브젝트의 콜라이더는 변경되지 않으므로 물리 엔진은 오브젝트의 변형된 모양을 인식하지 못합니다.

 

 

전체 프로젝트는 아래 깃허브를 참고해주세요!

https://github.com/eugene-doobu/catlikecoding-rendering/tree/main/MeshBasics

 

 

---

 

Mesh Deformation, a Unity C# Tutorial

A Unity C# scripting tutorial in which you will deform a mesh, turning it into a stress ball.

catlikecoding.com

 

역제곱법칙

 

Inverse-square law - Wikipedia

From Wikipedia, the free encyclopedia Physical law S represents the light source, while r represents the measured points. The lines represent the flux emanating from the sources and fluxes. The total number of flux lines depends on the strength of the ligh

en.wikipedia.org

 

 

 

 

반응형