Code Less, Create More!

Simple but useful code snippets for 3D Graphic Developers

Unreal Engine

두께를 가진 Polyline을 Procedural Mesh로 생성하는 방법 (Unreal Engine, C++)

데브엑스 2023. 4. 1. 07:26
반응형

경로, 테두리 및 기타 선 모양 형태를 만드는 데 사용되는 사용자 지정 두께를 가지는 폴리라인(Polyline)을 생성하는 것은 3D 애플리케이션 개발에서 흔하게 요구되는 기능입니다. 그러나 시각적으로 매력적이고 그럴듯한 결과를 보여주도록 이 기능을 구현하는 것은 다소 어려울 수 있습니다.

Unreal Engine에는 Line(선)을 그리는 데 사용할 수 있는 내장 함수가 없기 때문에, 메시의 지오메트리로 라인 세그먼트들을 만들기 위해서는 직접 계산을 처리해야 합니다. (Unreal Engine은 개발 중 에디터에서 디버깅 용도로 라인 세그먼트를 그리는 DebugDrawLine()이라는 함수를 제공합니다. 그러나 이 함수는 게임의 프레임 속도에 큰 영향을 미칠 수 있기 때문에 성능이 중요한 애플리케이션에 사용하기에는 적합하지 않습니다.)

두께를 가진 폴리라인들

1. Polyline의 정의

Polyline은 연속적인 경로를 형성하는 연결된 라인 세그먼트의들의 연속 객체입니다. 일반적으로 곡선이나 복잡한 모양을 나타내기 위해 2D 및 3D 그래픽에서 사용됩니다.

Polyline의 특징 중 하나는 각 점이 연결된 엣지를 가지고 있다는 것입니다. 이러한 엣지는 Polyline의 각 점을 연결하는 라인 세그먼트입니다. 이러한 라인 세그먼트는 Polyline을 구성하는 구성 요소 중 하나입니다.

또한, Polyline은 두께(Thickness 또는 Width)를 가질 수 있습니다. 이것은 Polyline의 엣지의 두께를 의미합니다. 이 두께는 Polyline의 형태를 강조하고 시각적으로 더 풍부하게 만들 수 있습니다.

2. Polyline구성하기

Procedural Mesh는 다양한 모양과 두께의 라인을 만들 수 있도록 합니다. Polyline을 구현하는 방법은 다양하지만, Unreal Engine의 Procedural Mesh 컴포넌트를 사용하는 것이 가장 효율적인 접근법 중 하나입니다. Procedural Mesh 컴포넌트는 C++을 사용하여 런타임에서 메시를 만들 수 있으며, 사용자 지정 모양을 만드는 강력한 도구입니다.
다음은 두께를 가진 Polyline을 구성하는 간단한 예제 코드입니다.

// AMyPolylineActor.cpp
void AMyPolylineActor::BuildPolyline()
{
    TArray<FVector> Vertices;
    TArray<FLinearColor> Colors;
    TArray<int32> Triangles;
    FVector Point, PrevPoint, NextPoint;
    FVector PrevDir, NextDir, PrevOutDir, NextOutDir;
    FVector OutDir, UpDir = FVector::ZAxisVector; // Outward, Upward Direction
    FVector V0, V1, PrevV0, PrevV1;
    int32 I0, I1, PrevI0 = 0, PrevI1 = 0;
    const float HalfThickness = Thickness * 0.5f;
    float OutDirScale = 1.0f;
    for (int32 i = 0, NumPoly = Points.Num(); i < NumPoly; ++i)
    {
        Point = Points[i];
        NextPoint = Points[(i < NumPoly - 1) ? (i + 1) : 0];
        if (i == 0)
        {
            PrevDir = (NextPoint - Point).GetSafeNormal();
            OutDir = UpDir.Cross(PrevDir);
            OutDirScale = 1.0f;
        }
        else if (i == NumPoly - 1)
        {
            PrevDir = (Point - PrevPoint).GetSafeNormal();
            OutDir = UpDir.Cross(PrevDir);
            OutDirScale = 1.0f;
        }
        else
        {
            PrevDir = (Point - PrevPoint).GetSafeNormal();
            PrevOutDir = UpDir.Cross(PrevDir);
            NextDir = (NextPoint - Point).GetSafeNormal();
            NextOutDir = UpDir.Cross(NextDir);
            OutDir = (PrevOutDir + NextOutDir).GetSafeNormal();    // Average normal
            float CosTheta = NextDir.Dot(-PrevDir);
            float Angle = FMath::Acos(CosTheta);
            float SinValue = FMath::Sin(Angle * 0.5f);
            OutDirScale = FMath::IsNearlyZero(SinValue, 0.01f) ? 1000.0f : (1.0f / SinValue);
        }
        V0 = Point + OutDir * HalfThickness * OutDirScale;
        V1 = Point - OutDir * HalfThickness * OutDirScale;
        I0 = Vertices.Add(V0);
        I1 = Vertices.Add(V1);
        Colors.Add(Color);
        Colors.Add(Color);
        if (i > 0)    // Add rectangle of a edge segment
        {
            Triangles.Append({ I0, I1, PrevI0 });
            Triangles.Append({ PrevI1, PrevI0, I1 });
        }
        PrevV0 = V0;
        PrevV1 = V1;
        PrevI0 = I0;
        PrevI1 = I1;
        PrevPoint = Point;
    }
    ProceduralMesh->CreateMeshSection_LinearColor(0, Vertices, Triangles, {}, {}, Colors, {}, false);
}

for 루프 내에서 코드는 각 폴리라인 세그먼트의 방향과 방향성을 계산하기 위해 PrevPoint, NextPoint, PrevDir, NextDir, PrevOutDir 및 NextOutDir과 같은 여러 변수를 사용하여 폴리라인의 지오메트리를 정의하는 데 필요한 데이터를 계산합니다.

코드는 또한 HalfThickness 변수를 사용하여 Polyline을 구성하는 두 평행선 사이의 거리를 결정합니다. 각 세그먼트의 정점은 세그먼트의 방향과 수직인 PrevOutDir 벡터와 HalfThickness 변수를 사용하여 계산됩니다.

코드는 그런 다음 정점 및 색상 데이터를 배열에 추가하고, 현재 세그먼트가 첫 번째 세그먼트가 아닌 경우, 이전 세그먼트와 현재 세그먼트를 연결하는 직사각형을 만드는 삼각형을 추가합니다.

3. Polyline 메시에 사용되는 벡터 수학

Procedural Mesh를 사용하여 사용자 지정 두께의 Polyline을 만들기 위해서는 먼저 Polyline 경로를 정의하는 일련의 정점을 계산해야 합니다. 그런 다음 인접한 점 사이의 각 세그먼트의 방향을 계산하고 이 정보를 사용하여 메시를 정의하는 정점 및 삼각형 인덱스를 구성합니다.

6개의 정점을 가진 폴리라인

3.1 엣지 세그먼트의 방향 벡터

각 엣지 세그먼트의 방향은 목적지 점에서 출발점을 뺀 결과 벡터로 계산됩니다. 결과 벡터는 방향 벡터를 계산하는 데 사용되기 위해 정규화(Normalize)되어야 합니다.

또한 각 세그먼트의 방향에 대해 직각인 외측 방향 벡터는 세그먼트의 방향 벡터와 위측 방향 벡터의 외적으로 계산됩니다.

PrevDir = (Point - PrevPoint).GetSafeNormal();
PrevOutDir = UpDir.Cross(PrevDir);
NextDir = (NextPoint - Point).GetSafeNormal();
NextOutDir = UpDir.Cross(NextDir);

이러한 계산은 Polyline 배열의 점을 반복하는 for 루프에서 인접한 점 사이의 각 엣지 세그먼트의 방향을 계산하는 방식으로 수행됩니다.

3.2 외부 방향 벡터

코너 정점의 외부 방향 벡터를 얻기 위해서는 이웃 세그먼트의 외부 방향 벡터들을 더해서 평균을 계산하고 정규화하여 코너 정점의 평균 방향을 나타내는 단일 벡터를 얻습니다.

 

PrevOutDir = (PrevOutDir + NextOutDir).GetSafeNormal();    // Average normal

이 단계는 Polyline의 각 코너 정점에서 세그먼트의 방향에 직각으로 정렬되고 세그먼트 엣지의 중앙에서 바깥쪽으로 향하도록 보장합니다.

3.3 코너에서 방향 벡터 사이의 각도

 각 코너 지점에서 방향 벡터 사이의 각도를 계산하여 메시의 외부로 향하는 스케일 비율을 결정합니다.

이 각도를 계산하기 위해 코너 정점에서 이전과 다음 방향 벡터의 내적을 취하고 내적의 역코사인을 계산하여 두 벡터 사이의 각도를 구합니다. 각도가 작은 경우, 메시의 외부 스케일을 계산의 한계값으로 설정하여 0으로 나누는 것을 피합니다. 그렇지 않으면 스케일을 벡터 사이의 각도의 절반 크기의 역사인으로 설정합니다.

float CosTheta = NextDir.Dot(-PrevDir);
float Angle = FMath::Acos(CosTheta);
float SinValue = FMath::Sin(Angle * 0.5f);
OutDirScale = FMath::IsNearlyZero(SinValue, 0.01f) ? 1000.0f : (1.0f / SinValue);

코드에서 NextDir.Dot(-PrevDir)를 사용하여 다음 방향 벡터와 이전 방향 벡터의 음수의 내적을 계산합니다. 이 때 PrevDir가 이전 정점에서 현재 정점을 가리키는 방향이기 때문에 방향 벡터의 음수를 사용하여 벡터의 반대 방향을 얻습니다. 

코너에서 외측 방향 벡터의 계산

3.4 엣지의 두께를 표현하는 직사각형 정점

Polyline의 각 엣지 세그먼트에 대해 두께를 표현하기 위해 세그먼트의 방향에 따라 두께 만큼의 직사각형을 형성합니다. Triangles 변수는 Procedural Mesh의 Line Section을 구성하는 삼각형들의 인덱스 요소로 세그먼트 당 두 개의 삼각형 인덱스를 사용해 직사각형을 구성합니다.

V0 = Point + OutDir * HalfThickness * OutDirScale;
V1 = Point - OutDir * HalfThickness * OutDirScale;
I0 = Vertices.Add(V0);
I1 = Vertices.Add(V1);
Colors.Add(Color);
Colors.Add(Color);
if (i > 0)    // Add rectangle of a edge segment
{
    Triangles.Append({ I0, I1, PrevI0 });
    Triangles.Append({ PrevI1, PrevI0, I1 });
}

두 개의 삼각형은 코너 정점의 외부 방향 벡터와 Polyline의 두께의 절반 값을 사용하여 사각형의 정점을 정의합니다. 그리고 정점의 인덱스는 Triangles 배열에 추가되어 사각형을 구성하는 두 개의 삼각형을 형성합니다.

3.5 한계점

위의 3.3에서 보여진 코드는 Polyline 코너에서 외부 방향 스케일을 계산하기 위해 1.0 / sin() 방정식을 사용하기 때문에 두 세그먼트 간의 각도가 180도에 매우 가까운 경우 sin() 값이 너무 작아서 무한대로 커지는 문제가 발생할 수 있는 사용 한계가 있습니다.

이 한계를 해결하기 위해 해당 코드에서는 sin() 값이 일정 임계값(이 경우 0.01) 아래인 경우 외부 스케일을 제한된 큰 값(이 경우 1000.0f)으로 설정하는 sin() 값 검사를 포함합니다. 이것은 외부 스케일이 무한대로 커지는 것을 방지하지만 메시에서 시각적 오류가 발생할 수 있습니다.

4. 결론

위의 예는 C++을 사용하여 Unreal Engine에서 두꺼운 Polyline 메시를 만들고자 하는 경우 출발점이 될 수 있습니다. 간단한 코드의 예제이지만, 두꺼운 Polyline을 위한 Procedural Mesh를 만드는 데 필요한 핵심적인 단계를 보여줍니다.

제공된 코드는 특정 프로젝트 요구 사항에 맞게 수정 할 수 있는 기반을 제공합니다. 예를 들어, 스플라인 또는 곡선과 같은 다양한 유형의 입력 데이터를 처리하기 위해 알고리즘을 수정해야 할 수도 있습니다. 또는 텍스처 매핑이나 조명과 같은 메시에 추가 기능을 추가할 수 있습니다.

한편, 이 포스트의 사례와 별개로 Unreal Engine의 Procedural Mesh 컴포넌트는 C++을 사용하여 런타임에서 사용자 지정 메시를 만드는 데 필요한 강력한 도구를 제공합니다. 약간의 실험과 창의성을 조금 더하면 이 컴포넌트를 사용하여 게임 및 시뮬레이션부터 건축 시각화 및 제품 디자인까지 다양한 응용 분야에서 사용자 지정 메시를 만들 수 있습니다.

예제 프로젝트 및 소스 코드

Github에서 이 포스트의 예제 소스 코드를 다운로드 할 수 있습니다.

https://github.com/odyssey2010/UE_polyline_procedural_mesh

 

GitHub - odyssey2010/UE_polyline_procedural_mesh: Thick polyline using Procedural Mesh in Unreal Engine with C++

Thick polyline using Procedural Mesh in Unreal Engine with C++ - GitHub - odyssey2010/UE_polyline_procedural_mesh: Thick polyline using Procedural Mesh in Unreal Engine with C++

github.com

https://www.youtube.com/watch?v=JxP7XLXa9og 

 

반응형