Unreal Skeletal Mesh to Procedural Mesh - 전체 Skeletal Mesh 복사
2024 Unreal Fest를 보고 절단을 구현하고 싶었다. (구현 버전은 Unreal 5.4이다)
https://www.youtube.com/watch?v=IL9j4NchTvA&list=PLBHH5tjTRZxLVLsfs4NCeImcX5XSYhH-G&index=22
Unreal Fest의 동영상은 위와 같다. 절단부분은 25:30부터 참고하면 된다.
Skeletal Mesh에서의 절단의 프로세스는 정말 간결하게 설명하면 Skeletal Mesh를 카피한 Procedrual Mesh를 만들고 해당 Procedrual Mesh를 자르면 된다.
말은 간단하지만 알아야할 지식이 많다.
일단 기본적으로 여기서 설명할 Mesh에 관련해서 간단하게 설명한다.
이 절단을 구현하기 위해서 필요한 정보만 설명한다.
이미 Skeletal Mesh, Static Mesh, Procedural Mesh에 대해 간단하게 알고 있다면 넘어가도 괜찮다. (아래 접은글)
Skeltal Mesh
Skeletal Mesh는 Unreal에서 캐릭터나 움직이는 객체를 표현하는 모델이다.
Skeletal Mesh라는 이름에서 유추할 수 있듯이 뼈대 기반으로 만들어진 Mesh이다.
이 Mesh는 Animation 적용가능하고 절단에서 사용할 Ragdoll 또한 가능하다.
Static Mesh
Static Mesh는 Static이라는 이름에서 알 수 있듯이 멈춰있고 고정된 Mesh를 의미한다.
보통 건물, 배경 Object에 사용되는 Mesh이다.
Animation 처리가 불가능한 만큼 렌더링 성능은 Skeletal Mesh보다 좋다.
Procedural Mesh
Procedural Mesh는 간단하게 코드로 생성되는 Static Mesh라고 생각하면 된다.
Static Mesh는 런타임동안 변경이 불가능하지만 Procedural Mesh는 런타임 동안 동적 생성 및 변경이 가능하다.
또한 Procedural Mesh는 생성할 때 Static과 다르게 Collision, LOD, UV, Color 등 직접 설정해야한다.
Static보단 유연하게 설정할 수 있지만 그로인해 Static보단 최적화가 어렵고 성능을 신경써야한다.
이제 글이 길었으니 바로 Skeletal Mesh를 똑같이 Procedrual Mesh로 만드는 코드를 살펴보자.
void AEnemy::CopySkeletalMeshToProcedural(int32 LODIndex)
{
if (!GetMesh() || !ProcMeshComponent)
{
UE_LOG(LogTemp, Warning, TEXT("CopySkeletalMeshToProcedural: SkeletalMeshComponent or ProcMeshComp is null."));
return;
}
//Skeletal Mesh의 Location과 Rotation을 들고온다.
FVector MeshLocation = GetMesh()->GetComponentLocation();
FRotator MeshRotation = GetMesh()->GetComponentRotation();
//Skeletal Mesh의 Location과 Rotation을 Procedural Mesh에 적용한다.
ProcMeshComponent->SetWorldLocation(MeshLocation);
ProcMeshComponent->SetWorldRotation(MeshRotation);
// SkeletalMesh를 가져온다.
USkeletalMesh* SkeletalMesh = GetMesh()->GetSkeletalMeshAsset();
if (!SkeletalMesh)
{
UE_LOG(LogTemp, Warning, TEXT("CopySkeletalMeshToProcedural: SkeletalMesh is null."));
return;
}
//GetResourceForRendering - Skeletal Mesh의 렌더링 데이터를 가져오는 함수
const FSkeletalMeshRenderData* RenderData = SkeletalMesh->GetResourceForRendering();
if (!RenderData || !RenderData->LODRenderData.IsValidIndex(LODIndex))
{
UE_LOG(LogTemp, Warning, TEXT("CopySkeletalMeshToProcedural: LODRenderData[%d] is not valid."), LODIndex);
return;
}
//SkeletalMesh에서 LODRenderData를 가져온다.LODRenderData는 버텍스 데이터, 인덱스 데이터, 섹션 정보 등이 포함
//FSkeletalMeshLODRenderData란 LOD의 Mesh 데이터를 가지고 있는 구조체이다.
const FSkeletalMeshLODRenderData& LODRenderData = RenderData->LODRenderData[LODIndex];
// //SkinWeightVertexBuffer를 가져온다. -> vertex가 어떤 Bone에 영향을 받는지 저장하는 데이터이며 Animation에서 사용 예정
// const FSkinWeightVertexBuffer& SkinWeights = LODRenderData.SkinWeightVertexBuffer;
//UE_LOG(LogTemp, Display, TEXT("CopySkeletalMeshToProcedural: Processing LODIndex %d."), LODIndex);
TArray<FVector> VerticesArray;
int32 TotalVertices = 0;
for (const FSkelMeshRenderSection& Section : LODRenderData.RenderSections)
{
//NumVertices - 해당 Section의 Vertex 수, BaseVertexIndex - 해당 Section의 시작 Vertex Index
const int32 NumSourceVertices = Section.NumVertices;
const int32 BaseVertexIndex = Section.BaseVertexIndex;
//UE_LOG(LogTemp, Display, TEXT("Processing %d vertices in section... (BaseVertexIndex: %d)"), NumSourceVertices, BaseVertexIndex);
for (int32 i = 0; i < NumSourceVertices; i++)
{
const int32 VertexIndex = i + BaseVertexIndex;
//vertex의 위치를 가져온다. -> LODRenderData.StaticVertexBuffers.PositionVertexBuffer(현재 LOD의 Vertex 위치 데이터를 저장하는 버퍼)
//.VertexPosition(VertexIndex)-> VertexIndex의 위치를 가져온다. 반환 타입이 FVector3f이다.
const FVector3f SkinnedVectorPos = LODRenderData.StaticVertexBuffers.PositionVertexBuffer.VertexPosition(VertexIndex);
//FVector3f -> FVector로 변환 후 VerticesArray에 추가한다.
VerticesArray.Add(FVector(SkinnedVectorPos));
//VertexIndex에 해당하는 버텍스의 노멀(Normal) 벡터를 반환.
const FVector3f Normal = LODRenderData.StaticVertexBuffers.StaticMeshVertexBuffer.VertexTangentZ(VertexIndex);
//VertexIndex에 해당하는 버텍스의 탄젠트(Tangent) 벡터를 반환. - 노멀 벡터와 함께 사용되어 표면의 방향과 비틀림을 결정하는 값
const FVector3f TangentX = LODRenderData.StaticVertexBuffers.StaticMeshVertexBuffer.VertexTangentX(VertexIndex);
//VertexIndex에 해당하는 버텍스의 UV 좌표를 반환. - 메쉬의 특정 버텍스가 텍스처에서 어디에 매핑되는지를 결정하는 2D 좌표
const FVector2f SourceUVs = LODRenderData.StaticVertexBuffers.StaticMeshVertexBuffer.GetVertexUV(VertexIndex, 0);
//이후 Procedural Mesh를 생성할 때 사용하기 위해 Array에 추가.
Normals.Add(FVector(Normal));
Tangents.Add(FProcMeshTangent(FVector(TangentX), false));
UV.Add(FVector2D(SourceUVs));
Colors.Add(FColor(0, 0, 0, 255));
}
TotalVertices += NumSourceVertices;
}
UE_LOG(LogTemp, Display, TEXT("Total vertices processed: %d"), TotalVertices);
//LODRenderData.MultiSizeIndexContainer.GetIndexBuffer()는 원래 Skeletal Mesh의 각 vertex가 어떻게 삼각형으로 구성되어있었는지를 들고온다.
const FRawStaticIndexBuffer16or32Interface* IndexBuffer = LODRenderData.MultiSizeIndexContainer.GetIndexBuffer();
if (!IndexBuffer)
{
UE_LOG(LogTemp, Warning, TEXT("CopySkeletalMeshToProcedural: Index buffer is null."));
return;
}
//현재 LOD의 총 Index 수를 가져온다. 삼각형이 100개면 IndexBuffer->Num()은 300이다.
const int32 NumIndices = IndexBuffer->Num();
//메모리 미리 확보
Indices.SetNumUninitialized(NumIndices);
for (int32 i = 0; i < NumIndices; i++)
{
//IndexBuffer Get(i) - 현재 처리 중인 삼각형을 구성하는 버텍스 인덱스를 가져옴.
//VertexIndex : Get(0) = a, Get(1) = b, Get(2) = c로 abc삼각형, Get(3) = c, Get(4) = d, Get(5) = a로 cda삼각형 (여기서 abcd는 FVector위치라고 취급)
//즉, 첫 BaseVertexIndex에서 그려지는 삼각형부터 순서대로 삼각형이 그려지는 vertex의 Index를 가져온다.
//결과적으로 Indices를 순환하면 3개씩 묶어서 삼각형을 그릴 수 있다.
//uint32가 반환되어 int32로 Casting, 데이터 일관성을 위해 Casting한다.
Indices[i] = static_cast<int32>(IndexBuffer->Get(i));
}
// Create procedural mesh section
//Section Index - 어떤 Section부터 시작하는가?, Vertices - 어떤 vertex를 사용하는가?
//Indices - 어떤 삼각형 구조를 사용하는가?, Normals, UV, Colors, Tangents, bCreateCollision - 충돌 활성화
ProcMeshComponent->CreateMeshSection(0, VerticesArray, Indices, Normals, UV, Colors, Tangents, true);
UE_LOG(LogTemp, Display, TEXT("Procedural mesh creation completed."));
//Convex Collision 추가
if (VerticesArray.Num() > 0)
{
ProcMeshComponent->ClearCollisionConvexMeshes(); // 기존 Collision 삭제
//Convex Collision - 현재 Vertex 기반으로 Convex(볼록한) Collision 생성
ProcMeshComponent->AddCollisionConvexMesh(VerticesArray); // Convex Collision 추가
UE_LOG(LogTemp, Display, TEXT("Convex Collision added with %d vertices."), VerticesArray.Num());
}
// Collision 및 Physics 설정
ProcMeshComponent->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
ProcMeshComponent->SetCollisionObjectType(ECC_WorldDynamic);
ProcMeshComponent->SetSimulatePhysics(true);
ProcMeshComponent->SetEnableGravity(true);
//위에선 LOD Section별로 Vertex를 가져와서 모두 처리했지만 여기서는 GetMaterial(0)로 0번째만 Material을 가져와서 적용함. 즉, 0번째 Material만 적용됨.
//더 적용하기 위해선 수정 필요.
UMaterialInterface* SkeletalMeshMaterial = GetMesh()->GetMaterial(0);
if (SkeletalMeshMaterial)
{
ProcMeshComponent->SetMaterial(0, SkeletalMeshMaterial);
UE_LOG(LogTemp, Display, TEXT("Applied material from SkeletalMesh to ProceduralMesh."));
}
else
{
UE_LOG(LogTemp, Warning, TEXT("SkeletalMesh has no material assigned."));
}
GetMesh()->SetVisibility(false);
}
위 함수를 실행하면 원래 가지고 있던 SkeletalMesh의 위치에 ProceduralMesh를 생성한다.
위 함수를 뜯어보자
이미 달려있는 주석으로 알 수 있는 부분은 넘긴다.
//GetResourceForRendering - Skeletal Mesh의 렌더링 데이터를 가져오는 함수
const FSkeletalMeshRenderData* RenderData = SkeletalMesh->GetResourceForRendering();
if (!RenderData || !RenderData->LODRenderData.IsValidIndex(LODIndex))
{
UE_LOG(LogTemp, Warning, TEXT("CopySkeletalMeshToProcedural: LODRenderData[%d] is not valid."), LODIndex);
return;
}
//FSkeletalMeshLODRenderData란 LOD의 Mesh 데이터를 가지고 있는 구조체이다.
const FSkeletalMeshLODRenderData& LODRenderData = RenderData->LODRenderData[LODIndex];
위 코드는 Skeletal Mesh가 렌더링될 때 사용되는 데이터를 들고오는 코드이다.
여러 개의 FSkeletalMeshLODRenderData를 갖고 각 LOD는 Mesh의 vertex, Triangle, index등의 정보를 들고 있다.
LODRendererData.IsValidIndex(LODIndex)는 LODIndex가 5라면 실제로 5라는 LOD를 가지고 있는지 확인하는 절차이다. 만약 LODRendereData가 3개의 LOD만 가지고 있다면 false를 반환한다.
RenderData->LODRenderData[LODIndex]는 LODIndex에 해당하는 LOD의 Data(위에 적은 것들)를 들고온다.
즉, 위 코드는 LOD의 정보를 Skeletal에서 들고오고 LODIndex에 해당하는 LOD의 데이터를 LODRenderData로 참조한다.
LOD
LOD는 Level of Detail로 멀리 있는 Object는 오브젝트의 Detail을 낮춰서 성능을 최적화하는 기술
숫자가 낮을수록 (0에 가까울 수록) 고해상도, 숫자가 높을 수록 저해상도를 사용한다.
즉, LOD가 높을 수록 메쉬가 더 단순한 형태가 되어 폴리곤 수가 줄어들고 성능적으로 좋다.
TArray<FVector> VerticesArray;
int32 TotalVertices = 0;
for (const FSkelMeshRenderSection& Section : LODRenderData.RenderSections)
{
//NumVertices - 해당 Section의 Vertex 수, BaseVertexIndex - 해당 Section의 시작 Vertex Index
const int32 NumSourceVertices = Section.NumVertices;
const int32 BaseVertexIndex = Section.BaseVertexIndex;
//UE_LOG(LogTemp, Display, TEXT("Processing %d vertices in section... (BaseVertexIndex: %d)"), NumSourceVertices, BaseVertexIndex);
for (int32 i = 0; i < NumSourceVertices; i++)
{
const int32 VertexIndex = i + BaseVertexIndex;
//vertex의 위치를 가져온다. -> LODRenderData.StaticVertexBuffers.PositionVertexBuffer(현재 LOD의 Vertex 위치 데이터를 저장하는 버퍼)
//.VertexPosition(VertexIndex)-> VertexIndex의 위치를 가져온다. 반환 타입이 FVector3f이다.
const FVector3f SkinnedVectorPos = LODRenderData.StaticVertexBuffers.PositionVertexBuffer.VertexPosition(VertexIndex);
//FVector3f -> FVector로 변환 후 VerticesArray에 추가한다.
VerticesArray.Add(FVector(SkinnedVectorPos));
//VertexIndex에 해당하는 버텍스의 노멀(Normal) 벡터를 반환.
const FVector3f Normal = LODRenderData.StaticVertexBuffers.StaticMeshVertexBuffer.VertexTangentZ(VertexIndex);
//VertexIndex에 해당하는 버텍스의 탄젠트(Tangent) 벡터를 반환. - 노멀 벡터와 함께 사용되어 표면의 방향과 비틀림을 결정하는 값
const FVector3f TangentX = LODRenderData.StaticVertexBuffers.StaticMeshVertexBuffer.VertexTangentX(VertexIndex);
//VertexIndex에 해당하는 버텍스의 UV 좌표를 반환. - 메쉬의 특정 버텍스가 텍스처에서 어디에 매핑되는지를 결정하는 2D 좌표
const FVector2f SourceUVs = LODRenderData.StaticVertexBuffers.StaticMeshVertexBuffer.GetVertexUV(VertexIndex, 0);
//이후 Procedural Mesh를 생성할 때 사용하기 위해 Array에 추가.
Normals.Add(FVector(Normal));
Tangents.Add(FProcMeshTangent(FVector(TangentX), false));
UV.Add(FVector2D(SourceUVs));
Colors.Add(FColor(0, 0, 0, 255));
}
TotalVertices += NumSourceVertices;
}
위 코드는 각 Section마다 vertex를 들고와서 VerticesArray라는 array에 저장한다.
Render Section은 특정 Matrial과 vertex,데이터를 포함하는 Mesh부분이다.
for를 사용해서 각 Section의 정점, Normal 등 다양한 데이터를 모으는 작업이다.
여기서 저장한 값들은 Procedural Mesh를 만들 때 사용된다.
const int32 VertexIndex = i + BaseVertexIndex를 사용하는 이유는 시작하는 Index(BaseVertexIndex)에서 하나씩 증가하며 모든 Index를 탐색하기 위함이다.
Normal(법선 벡터)
아무 Matrial에 들어가서 Normal 연결을 끊어보면 어떤 것을 의미하는지 감으로 느낄 수 있다.
정말 간단하게 울퉁불퉁함을 나타낸다고 생각하면 된다.
vertex나 삼각형의 방향을 나타내는 vector. 보통 Z축을 기준으로 Normal을 지정한다. (함수도 TangentZ)
위 코드에서 알 수 있듯 VertexTangentZ(int a)를 통해서 Normal을 가져올 수 있다.
조명계산과 Normal Mapping 기법 등에 활용된다.
조명과 방향이 같으면 밝게 나타내고 조명과 각도가 커질수록 어둡게 표시한다.
Normal Mapping은 들어간 곳과 튀어난 곳에 빛을 왜곡시키는 기법이다. 고해상도 모델처럼 나타낼 수 있는 기법이다.
Tangent(탄젠트 벡터)
Normal 벡터와 함께 표면의 방향을 더 세밀하게 정의하는 벡터이다.
보통 X축을 기준으로 지정한다. 함수도 VertexTangetX이다.
Tanget는 Normal vector와 같이 Normal Map의 방향을 결정한다.
Normal vector와 Tangent vector 둘 다 있어야 Normal Mapping이 가능하다.
Normal Mapping이 뭔지 궁금해질텐데 Matrial에서 자주 볼 수 있다.

푸른 이미지에 색상이 Normal Vector 값을 나타내고 Tangent Vector는 3D Model(ex. Skeletal Mesh)가 가지고 있다.
UV
3D model의 vertex와 2D Texture 사이의 매핑 정보를 저장하는 좌표값.
U : X축, V : Y축으로 이루어져있다.
이 UV를 통해 3D Model의 특정 지점이 2D Texture의 어느 부분에 해당하는지 알 수 있다.
//LODRenderData.MultiSizeIndexContainer.GetIndexBuffer()는 원래 Skeletal Mesh의 각 vertex가 어떻게 삼각형으로 구성되어있었는지를 들고온다.
const FRawStaticIndexBuffer16or32Interface* IndexBuffer = LODRenderData.MultiSizeIndexContainer.GetIndexBuffer();
if (!IndexBuffer)
{
UE_LOG(LogTemp, Warning, TEXT("CopySkeletalMeshToProcedural: Index buffer is null."));
return;
}
위 코드는 LOD의 Index Buffer를 들고온다.
Index Buffer는 Skeletal Mesh의 vertex들이 어떻게 삼각형으로 연결되는지 정의하는 데이터이다.
즉, 각 삼각형을 구성하는 vertex index목록을 의미한다.
Skeletal Mesh가 삼각형을 어떻게 구성하여 만들어졌는지 알고 있어야 똑같이 만들 수 있기 때문에 필요한 데이터이다.
//Convex Collision 추가
if (VerticesArray.Num() > 0)
{
ProcMeshComponent->ClearCollisionConvexMeshes(); // 기존 Collision 삭제
//Convex Collision - 현재 Vertex 기반으로 Convex(볼록한) Collision 생성
ProcMeshComponent->AddCollisionConvexMesh(VerticesArray); // Convex Collision 추가
UE_LOG(LogTemp, Display, TEXT("Convex Collision added with %d vertices."), VerticesArray.Num());
}
여기선 위에서 CreateMeshSection으로 만들어진 Procedural Mesh의 Collision을 추가하는 것이다.
이렇게 Collision을 생성해야 Physic의 영향을 받기 때문에 생성하였다.
다른 코드는 주석으로 충분히 설명이 될 것 같아서 생략한다.
SkeletalMesh와 동일한 Mesh가 생성된 것을 확인할 수 있다.