Code Less, Create More!

Simple but useful code snippets for 3D Graphic Developers

Unreal Engine

정다각형(N-gon)을 Procedural Mesh로 생성하는 방법 (Unreal Engine, C++)

데브엑스 2023. 3. 14. 22:22
반응형

다각형은 기본적인 삼각함수와 벡터 수학을 이용해 만들 수 있는 3D 그래픽스의 기본 도형이며 정점과 변으로 정의되는 평면 도형입니다. 특히 모든 측면의 내각이 동일한 다각형을 정다각형(Regular Polygon)으로 부르며 이 포스트에서는 간단히 다각형으로 부르겠습니다.

컴퓨터 그래픽에서는 다각형을 이용해 3D 모델, 게임 환경 및 시뮬레이션 등을 만듭니다. 이들은 삼각형, 사각형 및 직사각형과 같은 단순한 도형을 만드는 데 자주 사용됩니다. 하지만 구와 원통 등과 같은 보다 복잡한 도형을 만드는 데도 사용됩니다.

다각형은 세 개 이상의 변을 가진 기하 도형입니다. 변의 수에 따라 다각형을 N-gon 이라고 부르며, "N"은 변의 수를 나타냅니다. 예를 들어, 삼각형은 3-gon, 사각형은 4-gon, 오각형은 5-gon입니다. 아래 이미지를 참조하세요.

여러가지 다각형의 예

원 그리기 

3D 그래픽에서 N-gon을 만들거나 원을 그리는 방법에는 몇 가지 유사점이 있습니다. 두 경우 모두 기본적인 삼각함수와 벡터 수학을 사용하여 만들 수 있으며, 도형을 구성하는 정점의 좌표를 계산하는 수식이 필요합니다.

 

원을 그리기 위해서는 아래 그림과 같이 중심점과 반지름을 정의한 후, 중심점을 기준으로 정점을 생성합니다. 이렇게 생성된 정점들을 선분으로 연결하여 원의 둘레를 형성합니다.

원을 이루는 점들의 좌표를 계산하는 수식

다각형 그리기 

한편, 다각형 (N-gon)을 만들기 위해서는 측면(분할) 갯수 만큼의 정점을 계산한 후, 정점들을 연결하여 도형의 경계를 형성합니다. 예를 들어, 삼각형을 만들기 위해서는 세 개의 정점을 정의하고, 이 정점들을 선분으로 연결하여 삼각형의 변을 만들어야 합니다.

원과 N-gon을 만드는 데는 몇 가지 차이점이 있지만, 기본적으로는 동일한 기술을 사용합니다. 원 그리기와 같은 수식을 사용하되, 분할된 측면 갯수만큼 정점을 계산하고, 정점을 연결하여 도형을 만듭니다.

측면 갯수에 따른 여러 다각형 (https://mathworld.wolfram.com/RegularPolygon.html)

컴퓨터 프로그램으로 그려진 원은 근사화된 모양입니다. 원을 근사화하기 위해 사용하는 측면(또는 "분할")의 수는 그림의 정확성과 성능에 영향을 미칩니다. 이는 N-gon(변이 N개인 다각형)과 원의 유사점입니다.

원의 경우, 원주 상에 위치한 일련의 점을 정의해야 합니다. 예를 들어, 주어진 측면 갯수로 원을 그리기 위해서는 다음과 같이 점을 계산할 수 있습니다:

int main() 
{
   const double radius = 100.0;
   const int num_sides = 32;
   const double side_size = 360.0 / num_sides; // size of each side in degrees
   
   for (int i = 0; i < num_sides; ++i) 
   {
      double angle = i * side_size * M_PI / 180.0; // convert angle to radians
      double x = cos(angle) * radius; 
      double y = sin(angle) * radius; 
      cout << "Point " << i << ": (" << x << ", " << y << ")" << endl; // print coordinates
   }
 
   return 0;
}

여기서 num_sides는 분할할 측면의 수이고, M_PI는 원주율이며, radius는 원의 반지름입니다. 이 코드는 원주 상의 일련의 점을 계산하여, 이를 리스트에 추가하거나 원을 그리는 데 사용할 수 있습니다.

이와 같이, N-gon을 만들기 위해서도 정점을 계산하는 방법이 필요합니다. 정점의 수가 많을 수록 다각형은 원과 더욱 거의 동일하게 보이지만, 더 많은 정점이 사용될수록 계산량이 많아지고 성능이 저하됩니다. 따라서 정확성과 성능 사이에서 적절한 균형을 유지하는 것이 중요합니다.

채워진 다각형 만들기 

속이 채워진 다각형을 만들기 위해서는 정점과 변을 계산해야 합니다. 정점은 다각형을 이루는 점들이고, 변은 이들을 연결하는 선입니다. 다각형을 그리기 위해서는 벡터를 사용하여 각 정점의 위치를 3D 공간에서 계산해야 합니다.

다음은 언리얼 엔진에서 Procedural Mesh를 사용하여 N개의 변을 가진 다각형을 만드는 C++ 코드의 예시입니다.

void AMyPolygonActor::BuildPolygonFilled()
{
    TArray<FVector> Vertices;
    TArray<FLinearColor> Colors;
    TArray<int32> Triangles;
 
    FVector Point, RotationDir{0}; // Rotation Vector
    FVector V0, LastV0;
    int32 I0, LastI0 = 0;
    float Theta;
 
    // Center point
    I0 = Vertices.Add(FVector::ZeroVector);
    Colors.Add(Color);
 
    for (uint32 i = 0; i <= Side; ++i)
    {
        Theta = FMath::DegreesToRadians(i * 360.0f / Side);
        FMath::SinCos<double>(&RotationDir.Y, &RotationDir.X, Theta); 
        Point = RotationDir * Radius;
 
        I0 = Vertices.Add(Point);
 
        Colors.Add(Color);
 
        if (i > 0)    // Add triangle indices after first index
        {
            Triangles.Append({ 0, LastI0, I0 });
        }
 
        LastV0 = V0;        
        LastI0 = I0;
    }
 
    ProceduralMesh->CreateMeshSection_LinearColor(0, Vertices, Triangles, TArray<FVector>(), TArray<FVector2D>(), Colors, TArray<FProcMeshTangent>(), false);
}

 

이 코드에서 FMath::SinCos() 함수는 사인과 코사인을 동시에 계산하는 빠른 함수입니다. (참고: https://lazydevkit.blogspot.com/2023/03/why-sincos-is-faster-than-sin-and-cos.html)

 

회전 방향을 나타내는 벡터(RotationDir)를 사용하면 수식을 조작 가능한 객체(화살표 막대기)로 취급할 수 있기 때문에 코드를 직관적으로 이해하기 쉬워집니다. 이 접근 방식은 직접 cos()와 sin() 방정식을 사용하여 x, y 값을 계산하고, 반지름을 곱하는 방식보다 훨씬 쉽습니다. 벡터를 사용하면 회전 방향을 어떻게 나타내고 조작할지에 대한 유연성이 높아집니다.

이 함수는 다각형의 측면 갯수 만큼 반복하며, 다각형 둘레의 각 정점의 위치를 계산합니다. 현재 각도를 라디안으로 변환하고, SinCos() 함수를 사용하여 회전 방향(RotationDir) 화살표(벡터)를 만들고 radius 만큼 늘여서(곱하기) 정점의 x, y 좌표를 계산합니다.

 

CreateMeshSection_LinearColor는 계산된 Vertics와 삼각형 인덱스 및 추가적인 정점 데이터를 전달해 Mesh를 만들어 내는 메서드입니다. 사용하지 않는 Normals, TexCoords(UV), Tangents는 빈 Array를 매개변수로 넘겨주면 됩니다.

 

두께를 가진 다각형 만들기

두께(Thickness or Width)가 있는 다각형을 만드는 것은 일반적인 다각형을 만드는 것보다 조금 더 복잡합니다. 이유는 외곽과 내곽의 두 개의 별도의 다각형을 만들어야 하고, 두께를 만들기 위해 정점들을 직사각형으로 연결해야하기 때문입니다.


바깥쪽 다각형을 만드는 방법은 일반적인 다각형을 만드는 방법과 동일합니다. 안쪽 다각형의 정점은 외곽 다각형의 정점에서 내부로 이동한 위치입니다. 이를 위해 우리는 회전 방향을 나타내는 벡터를 사용하여 각 정점의 위치를 계산할 수 있습니다.

바깥쪽 다각형과 안쪽 다각형을 만든 후, 우리는 이들을 연결하기 위해 사각형을 인덱스로 추가합니다. 사각형은 두 개의 삼각형으로 구성되며, 바깥쪽과 안쪽 다각형의 대응하는 정점을 연결하는 사각형을 추가함으로써 다각형의 두께를 만들 수 있습니다.

채워진 다각형과 두께를 가지는 다각형들

이 포스트에서는 원형 다각형을 위한 정점을 먼저 계산합니다. 이를 위해서는 일반적인 다각형을 만들 때와 같은 방식으로 원형 패턴으로 정점을 정의할 수 있습니다. 원형 다각형의 정점을 정의한 후, 우리는 원하는 두께만큼 내부와 외부로 이동하여 바깥 다각형과 안쪽 다각형의 정점을 만들 수 있습니다.

다음으로, 바깥족 다각형은 원형 다각형의 정점을 기준으로 외부로 이동한 위치입니다. 안쪽 다각형의 정점은 원형 다각형의 정점에서 안쪽으로 이동한 위치입니다. 이를 위해 다각형의 정점과 회전 방향을 나타내는 벡터를 사용하여 각 정점의 위치를 계산할 수 있습니다.

void AMyPolygonActor::BuildPolygonEmpty()
{
    TArray<FVector> Vertices;
    TArray<FVector> Normals;
    TArray<FLinearColor> Colors;
    TArray<int32> Triangles;
 
    FVector Point, RotationDir{ 0 }; // Rotation Vector
    FVector V0, V1;
    FVector LastV0, LastV1;
    int32 I0, I1;
    int32 LastI0, LastI1 = 0;
    float Theta;
    float HalfThickness = Thickness * 0.5f;    // Half length of thickness
 
    for (uint32 i = 0; i <= Side; ++i)
    {
        Theta = FMath::DegreesToRadians(i * 360.0f / Side);
        FMath::SinCos<double>(&RotationDir.Y, &RotationDir.X, Theta);
        Point = RotationDir * Radius;
 
	V0 = Point + RotationDir * HalfThickness;	// outward point from center
	V1 = Point - RotationDir * HalfThickness;	// inward point from center

        I0 = Vertices.Add(V0);
        I1 = Vertices.Add(V1);
 
        Normals.Add(RotationDir);
        Normals.Add(-RotationDir);
 
        Colors.Add(Color);
        Colors.Add(Color);
 
        if (i > 0)    // Add rectangle (two triangles) indices after first index
        {
            Triangles.Append({ LastI0, I0, I1, I1, LastI1, LastI0 });
        }
 
        LastV0 = V0;
        LastV1 = V1;
 
        LastI0 = I0;
        LastI1 = I1;
    }
 
    ProceduralMesh->CreateMeshSection_LinearColor(0, Vertices, Triangles, Normals, TArray<FVector2D>(), Colors, TArray<FProcMeshTangent>(), false);
}

여기서 RotationDir을 다시 한 번 사용하여 다각형 테두리의 두께를 나타내는 내부와 외부의 점을 계산합니다. 이 함수는 우선 회전 벡터, 반지름, 각도를 사용하여 다각형 둘레 상의 점의 좌표를 계산합니다. 그런 다음 RotationDir 벡터를 사용하여 추가로 두 개의 점을 계산합니다. 하나는 두께의 반만큼 바깥쪽으로 이동한 위치이고, 다른 하나는 같은 양 만큼 안쪽으로 이동한 위치입니다. 이 두 점은 다각형 테두리의 정점을 정의하는 데 사용됩니다.

 

RotationDir을 사용하면 최소한의 계산으로 방향을 계산하고 경계 정점을 생성할 수 있습니다. 외부와 내부의 정점을 계산하기 위해 각각의 sin, cos, radius를 곱하는 대신에, 해당 방향(RotationDir)과 +/- 반 두께 길이를 곱하여 내부와 외부의 정점을 계산할 수 있습니다. (화살표 막대기를 바깥 방향과 안쪽 방향으로 늘이는 것으로 이해하면 쉽습니다)

다각형 정점에서 회전 방향 벡터의 화살표 늘이기



내부와 외부의 다각형 정점을 정의한 후, 이들을 직사각형으로 연결하여 두께를 형성합니다. 이는 외부와 내부 다각형의 대응하는 정점을 짝지어 연결하면 됩니다.

Normals 배열은 이 구현에 필요하지 않지만, 이 알고리즘의 추가 확장을 위해 준비로 사용됩니다.

정점 색상 배열을 사용하는 간단한 재질

Unreal Engine의 Mesh는 재질이 지정되어야 하기 때문에 다각형에 대한 아주 간단한 버텍스 컬러 재질 예제를 아래에 제공합니다. 예제 코드와 함께 사용할 수 있습니다. 

정점 색상을 사용하는 간단한 Material

이 재질은 Mesh에서 버텍스 컬러를 가져와 재질의 기본 색상으로 사용합니다. 이 재질을 더욱 자세히 사용하려면, 버텍스 컬러가 기본 색상으로 사용되기 전에 추가적인 재질 표현 노드를 추가하여 버텍스 컬러를 수정할 수 있습니다. 

예를 들어, Material Expression Scalar Parameter 노드를 추가하여 전체 색상 강도를 조절하는 매개 변수를 만들거나,  Material Expression Multiply 노드를 추가하여 색상을 어둡게 하거나 밝게 할 수 있습니다.

다각형의 Mesh에 이 재질을 적용하려면, Procedural Mesh의 SetMaterial 메서드로 방금 만든 재질을 설정하면 됩니다. 이제 다각형 메시의 버텍스 컬러가 메시를 색상으로 표현합니다.

AMyPolygonActor::AMyPolygonActor()
{
     // Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
    PrimaryActorTick.bCanEverTick = true;
 
    ProceduralMesh = CreateDefaultSubobject<UProceduralMeshComponent>(TEXT("ProceduralMeshComponent"));
    ProceduralMesh->SetupAttachment(RootComponent);
 
    static FString Path = TEXT("/Game/M_PolygonMaterial");
    static ConstructorHelpers::FObjectFinder<UMaterial> MaterialLoader(*Path);
 
    if (MaterialLoader.Succeeded())
    {
        Material = MaterialLoader.Object;
    }
    else
    {
        UE_LOG(LogTemp, Error, TEXT("Failed to load material at path: %s"), *Path);
    }
}
 
void AMyPolygonActor::BuildMesh()
{
    if (Thickness > 0.0f)
    {
        BuildPolygonEmpty();
    }
    else
    {
        BuildPolygonFilled();
    }
 
    if (MaterialInstanceDynamic == nullptr)
    {
        MaterialInstanceDynamic = UMaterialInstanceDynamic::Create(Material, this);
        ProceduralMesh->SetMaterial(0, MaterialInstanceDynamic);
    }
}

결론

Procedural Mesh를 사용하여 언리얼 엔진에서 다각형을 생성하는 것은 간단한 작업처럼 보일 수 있지만, 기본적인 수학적 개념의 활용에 도움을 줍니다. 간단한 벡터 수학과 기본적인 C++ 코드를 사용하여 다양한 모양, 크기 및 두께를 가진 다양한 다각형을 만들 수 있습니다. 여기서 다룬 예제는 간단하지만, 보다 복잡한 다각형 및 도형으로 확장할 수 있는 유용한 기본 개념을 보여줍니다.

추가로, 언리얼 엔진에서 Procedural Mesh를 사용하는 것은 3D 그래픽 프로그래밍에서 기본적인 수학적 개념을 배우고 적용하는 좋은 방법입니다. 특히 벡터를 화살표처럼 조작 가능한 도구로 이해하는 것은 3D 개발자로서 매우 유용한 기술입니다. 여기에 창의성과 상상력을 발휘하면 이러한 기술을 사용하여 프로젝트에서 복잡한 3D 도형과 객체를 쉽게 만들 수 있습니다.

참고 자료

이 포스트의 예제 프로젝트는 아래 링크에서 확인하실 수 있습니다.
https://github.com/odyssey2010/UE_polygon_drawing

 

GitHub - odyssey2010/UE_polygon_drawing: Drawing Polygon (N-gon) using Procedural Mesh in Unreal Engine using C++

Drawing Polygon (N-gon) using Procedural Mesh in Unreal Engine using C++ - GitHub - odyssey2010/UE_polygon_drawing: Drawing Polygon (N-gon) using Procedural Mesh in Unreal Engine using C++

github.com

 

반응형