

우리가 게임에서 흔히 사용하는 캐릭터나 환경 오브젝트들은 대부분 Rigid Body이거나, 변형되지 않는 Static Mesh 형태의 정적인 메시들이다.
이러한 메시들은 물리적으로 이동하거나 회전할 수는 있지만, 형태 자체가 실시간으로 변형되지는 않는다.
하지만 부드럽게 감싸는 옷감(천)이나 말랑말랑한 젤리, 슬라임같은 변형이 필요한 메시들을 구현하려면 실시간으로 형태를 바꿀 수 있어야한다.
이러한 과정을 언리얼엔진에서는 일반적으로 Material이나 혹은 Niagara System을 이용하여 실제 메시는 변형되지 않지만 시각적인 눈속임으로 처리하는 것이 일반적이다.
(실시간으로 수백~수만개의 버텍스를 직접 물리 연산을 돌리는 것보다 훨씬 쉽고 비용이 적어 그게 보통이다.)
하지만 이번에는 XPBD(Extended Position-Based Dynamics)를 이용해서 실제 물리적으로 소프트바디를 만들어보고자 한다.
이를 위해서는 먼저 기존의 Static Mesh를 런타임에서 수정 가능한 형태로 변환하는 과정이 필요하다.

우선 Actor를 부모 클래스로 삼는 Slime Actor를 하나 만들었다.
Dynamic Mesh는 런타임에서 실시간으로 오브젝트의 Vertex를 직접 수정 가능한 컴포넌트다.
이를 사용하려면 일단 프로젝트에 Dynamic Mesh Component를 사용하기 위한 모듈들을 추가해주어야 한다.
{Project}.Build.cs에 다음 모듈 추가
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput", "GeometryFramework", "GeometryCore", "DynamicMesh", "MeshDescription", "StaticMeshDescription",});
이제 생성한 Slime Actor로 돌아가 헤더에 DynamicMeshComponent를 추가한다.
// Slime.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Slime.generated.h"
class UDynamicMeshComponent; // 전방 선언
UCLASS()
class XPBD_IMPLEMENT_API ASlime : public AActor
{
GENERATED_BODY()
public:
// Sets default values for this actor's properties
ASlime();
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
public:
// Called every frame
virtual void Tick(float DeltaTime) override;
UPROPERTY(VisibleAnywhere)
UDynamicMeshComponent* DynamicMeshComp; // DynamicMeshComponent 추가
};
// Slime.cpp
#include "Slime.h"
#include "Components/DynamicMeshComponent.h"
// Sets default values
ASlime::ASlime()
{
// 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;
// Dynamic Mesh Component 추가
DynamicMeshComp = CreateDefaultSubobject<UDynamicMeshComponent>(TEXT("DynamicMesh"));
RootComponent = DynamicMeshComp;
DynamicMeshComp->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
DynamicMeshComp->SetCollisionObjectType(ECC_PhysicsBody);
DynamicMeshComp->SetCollisionResponseToAllChannels(ECR_Block);
DynamicMeshComp->SetMobility(EComponentMobility::Movable);
}
...
Static Mesh를 Dynamic Mesh로 변환하기 위해 Source Mesh를 설정하는 과정을 추가한다.
그리고 이를 에디터에서 볼 수 있도록 Constructor도 추가한다.
// Slime.h
...
UCLASS()
class XPBD_IMPLEMENT_API ASlime : public AActor
{
GENERATED_BODY()
protected:
...
virtual void OnConstruction(const FTransform& Transform) override; // OnConstruction
public:
...
UPROPERTY(EditAnywhere)
UStaticMesh* SourceMesh; // StaticMesh 추가
UPROPERTY(EditAnywhere)
UMaterialInterface* SourceMaterial; // Material 추가
};
// Slime.cpp
...
ASlime::ASlime()
{
...
// Static Mesh 세팅
SourceMesh = Cast<UStaticMesh>(StaticLoadObject(
UStaticMesh::StaticClass(),
nullptr,
TEXT("/Game/Assets/Sphere.Sphere")
));
// Material Instance 세팅
SourceMaterial = Cast<UMaterialInterface>(StaticLoadObject(
UMaterialInterface::StaticClass(),
nullptr,
TEXT("/Game/Assets/M_Slime_Inst.M_Slime_Inst")
));
}
void ASlime::OnConstruction(const FTransform& Transform)
{
Super::OnConstruction(Transform);
}
이제 Static Mesh를 Source Mesh로 받아 Dynamic Mesh Component의 Mesh로 변환한다.
이를 위해 ConvertStaticMeshToDynamicMesh 함수를 추가한다.
// Slime.h
#include "DynamicMesh/DynamicMesh3.h"
...
UCLASS()
class XPBD_IMPLEMENT_API ASlime : public AActor
{
...
// StaticMesh를 DynamicMesh로 변환하는 유틸리티 함수
static void ConvertStaticMeshToDynamicMesh(const UStaticMesh* StaticMesh, UE::Geometry::FDynamicMesh3& OutMesh);
// Slime.cpp
void ASlime::ConvertStaticMeshToDynamicMesh(const UStaticMesh* StaticMesh, UE::Geometry::FDynamicMesh3& OutMesh)
{
if (!StaticMesh) return;
// LOD0 사용
const FMeshDescription* MeshDesc = StaticMesh->GetMeshDescription(0);
if (!MeshDesc) return;
// MeshDescription에서 버텍스와 트라이앵글 정보를 읽어와 DynamicMesh에 복사
FStaticMeshAttributes Attributes(const_cast<FMeshDescription&>(*MeshDesc));
auto VertexPositions = Attributes.GetVertexPositions();
// VertexID → DynamicMesh VertexID 매핑
TMap<FVertexID, int> VertexMap;
// 버텍스 복사
for (const FVertexID VertexID : MeshDesc->Vertices().GetElementIDs())
{
FVector3f Pos = VertexPositions[VertexID];
int NewID = OutMesh.AppendVertex(static_cast<FVector3d>(Pos));
VertexMap.Add(VertexID, NewID);
}
// 트라이앵글 복사
for (const FTriangleID TriID : MeshDesc->Triangles().GetElementIDs())
{
TArrayView<const FVertexInstanceID> InstanceIDs =
MeshDesc->GetTriangleVertexInstances(TriID);
int V0 = VertexMap[
MeshDesc->GetVertexInstanceVertex(InstanceIDs[0])];
int V1 = VertexMap[
MeshDesc->GetVertexInstanceVertex(InstanceIDs[1])];
int V2 = VertexMap[
MeshDesc->GetVertexInstanceVertex(InstanceIDs[2])];
OutMesh.AppendTriangle(V0, V1, V2);
}
}
ConvertStaticMeshToDynamicMesh에서는 다음 작업을 진행한다.
이제 OnConstruction에서 Static Mesh를 Dynamic Mesh로 변경한다.
// Slime.cpp
...
void ASlime::OnConstruction(const FTransform& Transform)
{
Super::OnConstruction(Transform);
if (!SourceMesh) return;
UE::Geometry::FDynamicMesh3 DynMesh;
// dynamic mesh로 변환
ConvertStaticMeshToDynamicMesh(SourceMesh, DynMesh);
// DynamicMeshComponent에 적용
DynamicMeshComp->GetDynamicMesh()->SetMesh(MoveTemp(DynMesh));
DynamicMeshComp->SetComplexAsSimpleCollisionEnabled(true, true);
DynamicMeshComp->NotifyMeshUpdated();
// Material 적용
if (SourceMaterial)
{
DynamicMeshComp->SetMaterial(0, SourceMaterial);
}
}
이제 Runtime에서 Dynamic Mesh를 실시간으로 변형해보자.
매 프레임마다 버텍스 위치를 수정하면, 물결치거나 말랑하게 움직이는 효과를 만들 수 있다.
// Slime.h
public:
...
float CurrentTime = 0.f;
};
// Slime.cpp
// Called every frame
void ASlime::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
CurrentTime += DeltaTime;
DynamicMeshComp->GetDynamicMesh()->EditMesh(
[this](UE::Geometry::FDynamicMesh3& Mesh)
{
for (int32 vid : Mesh.VertexIndicesItr())
{
FVector3d Pos = Mesh.GetVertex(vid);
// Y축으로 물결 변형
Pos.Z += FMath::Sin(CurrentTime + Pos.X * 0.01f) * 2.0f;
Mesh.SetVertex(vid, Pos);
}
},
EDynamicMeshChangeType::GeneralEdit,
EDynamicMeshAttributeChangeFlags::VertexPositions
);
DynamicMeshComp->NotifyMeshUpdated();
}
지금은 Tick과 CPU에서 모든 버텍스를 순회하는 방식이기 때문에 성능상 좋지는 않다.
이후, GPU의 Compute Shader를 이용한 방식으로 바꿔볼 수도 있겠다.

다음에는 PBD(Position-Based Dynamics)에 대해 알아보고 이를 Dynamic Mesh에서 실제로 동작하도록 구현하겠다.