[TIL]24-01-05:Unreal Engine Static Mesh 원본 데이터 수정: RenderData에서 FMeshDescription으로 전환

김슬아·2025년 1월 4일

🛠️ 문제 상황

Unreal Engine에서 Static Mesh를 수정하는 플러그인 개발 도중, RenderData를 기준으로 작업한 코드는 런타임에서만 작동하고 에디터에는 반영되지 않는 문제를 발견했습니다.

FStaticMeshRenderData* RenderData = StaticMesh->GetRenderData();

RenderData런타임 데이터를 기반으로 하며, Static Mesh 에디터의 원본 데이터에 영향을 주지 않습니다. 이로 인해, UV 조정이나 재질 병합 같은 작업이 런타임에서는 정상 동작하더라도, 에디터에서는 반영되지 않는 문제가 있었습니다.


🔍 해결 방안

Unreal Engine에서 Static Mesh의 원본 데이터를 수정하려면 FMeshDescription을 사용해야 합니다. FMeshDescription은 Static Mesh의 기본 정보를 담고 있어 에디터에서도 수정된 내용을 반영할 수 있습니다.


✨ 수정 전후 비교

1. 재질 병합 (ConsolidateMaterials)

수정 전 코드 (RenderData 기반):

void UMyBlueprintFunctions::ConsolidateMaterials(UStaticMesh* StaticMesh)
{
    FStaticMeshRenderData* RenderData = StaticMesh->GetRenderData();
    FStaticMeshLODResources& LODResources = RenderData->LODResources[0];

    if (LODResources.Sections.Num() > 1)
    {
        LODResources.Sections.SetNum(1);
        LODResources.Sections[0].MaterialIndex = 0;
    }

    TArray<FStaticMaterial> SingleMaterial;
    SingleMaterial.Add(StaticMesh->GetStaticMaterials()[0]);
    StaticMesh->SetStaticMaterials(SingleMaterial);

    UE_LOG(LogTemp, Log, TEXT("All materials consolidated into a single slot."));
}

수정 후 코드 (FMeshDescription 기반):

void UMyBlueprintFunctions::ConsolidateMaterials(UStaticMesh* StaticMesh)
{
    FMeshDescription* MeshDescription = StaticMesh->GetMeshDescription(0);

    TMap<FPolygonGroupID, FPolygonGroupID> PolygonGroupRemap;
    const FPolygonGroupID FirstPolygonGroup = MeshDescription->PolygonGroups().GetFirstValidID();

    for (const FPolygonGroupID& PolygonGroupID : MeshDescription->PolygonGroups().GetElementIDs())
    {
        if (PolygonGroupID != FirstPolygonGroup)
        {
            PolygonGroupRemap.Add(PolygonGroupID, FirstPolygonGroup);
        }
    }

    MeshDescription->RemapPolygonGroups(PolygonGroupRemap);
    StaticMesh->CommitMeshDescription(0);
    UE_LOG(LogTemp, Log, TEXT("All materials consolidated into a single slot."));
}

변경점:

  • LODResources.Sections 대신 FPolygonGroupID를 사용해 원본 데이터를 수정.
  • MeshDescription->RemapPolygonGroups()로 재질 병합 처리.

2. UV 조정 (OffsetUVs)

수정 전 코드 (RenderData 기반):

FStaticMeshLODResources& LODResources = RenderData->LODResources[0];
FStaticMeshVertexBuffers& VertexBuffers = LODResources.VertexBuffers;

FVector2f UV0 = VertexBuffers.StaticMeshVertexBuffer.GetVertexUV(Index0, UVChannel);
UV0.X += MaterialOffsetX;
VertexBuffers.StaticMeshVertexBuffer.SetVertexUV(Index0, UVChannel, UV0);

수정 후 코드 (FMeshDescription 기반):

TVertexInstanceAttributesRef<FVector2f> UVs = MeshDescription->VertexInstanceAttributes().GetAttributesRef<FVector2f>(MeshAttribute::VertexInstance::TextureCoordinate);

for (const FVertexInstanceID VertexInstanceID : MeshDescription->GetPolygonVertexInstances(PolygonID))
{
    FVector2f UV = UVs.Get(VertexInstanceID, UVChannel);
    UV.X += MaterialOffsetX;
    UVs.Set(VertexInstanceID, UVChannel, UV);
}

변경점:

  • LODResources.VertexBuffers를 사용하는 대신, VertexInstanceAttributes에서 UV를 직접 수정.
  • MeshDescription->GetPolygonVertexInstances()를 사용해 다각형 데이터를 조작.

3. 플러그인 반영

수정 후 데이터를 에디터와 동기화하려면 CommitMeshDescription()MarkPackageDirty()를 호출해야 합니다.

StaticMesh->CommitMeshDescription(0);
StaticMesh->Modify();
StaticMesh->MarkPackageDirty();

🧠 느낀 점

Unreal Engine에서 원본 데이터를 수정하지 않으면, 에디터에서 작업한 결과가 반영되지 않는다는 점을 배웠습니다. 특히, FMeshDescription을 사용하면 런타임 데이터와 원본 데이터를 모두 처리할 수 있어 더욱 일관된 결과를 얻을 수 있었습니다.


🚀 최종 코드 정리

// Fill out your copyright notice in the Description page of Project Settings.

#include "MyBlueprintFunctions.h"
#include "StaticMeshAttributes.h"

void UMyBlueprintFunctions::ConsolidateMaterials(UStaticMesh* StaticMesh)
{
    if (!StaticMesh || StaticMesh->GetNumSourceModels() == 0)
    {
        UE_LOG(LogTemp, Warning, TEXT("Invalid Static Mesh for material consolidation."));
        return;
    }

    FMeshDescription* MeshDescription = StaticMesh->GetMeshDescription(0);
    if (!MeshDescription)
    {
        UE_LOG(LogTemp, Warning, TEXT("No Mesh Description available."));
        return;
    }

    // Consolidate materials into a single slot
    TMap<FPolygonGroupID, FPolygonGroupID> PolygonGroupRemap;
    const TArray<FStaticMaterial>& StaticMaterials = StaticMesh->GetStaticMaterials();
    if (StaticMaterials.Num() > 1)
    {
        const FPolygonGroupID FirstPolygonGroup = MeshDescription->PolygonGroups().GetFirstValidID();
        for (const FPolygonGroupID& PolygonGroupID : MeshDescription->PolygonGroups().GetElementIDs())
        {
            if (PolygonGroupID != FirstPolygonGroup)
            {
                PolygonGroupRemap.Add(PolygonGroupID, FirstPolygonGroup);
            }
        }
        MeshDescription->RemapPolygonGroups(PolygonGroupRemap);

        // Update material slots
        TArray<FStaticMaterial> SingleMaterial;
        SingleMaterial.Add(StaticMaterials[0]);
        StaticMesh->SetStaticMaterials(SingleMaterial);
    }

    StaticMesh->CommitMeshDescription(0);
    StaticMesh->Modify();
    StaticMesh->MarkPackageDirty();

    UE_LOG(LogTemp, Log, TEXT("All materials consolidated into a single slot."));
}

UStaticMesh* UMyBlueprintFunctions::ApplyPlanarMappingAndSeparate(UStaticMesh* StaticMesh, int32 UVChannel)
{
    if (!StaticMesh || StaticMesh->GetNumSourceModels() == 0)
    {
        UE_LOG(LogTemp, Warning, TEXT("Invalid Static Mesh for processing."));
        return nullptr;
    }

    FMeshDescription* MeshDescription = StaticMesh->GetMeshDescription(0);
    if (!MeshDescription)
    {
        UE_LOG(LogTemp, Warning, TEXT("No Mesh Description available."));
        return nullptr;
    }

    FBox BoundingBox = MeshDescription->ComputeBoundingBox();
    FVector3f Min = FVector3f(BoundingBox.Min);
    FVector3f Max = FVector3f(BoundingBox.Max);
    FVector3f Size = Max - Min;
    float ScaleU = 1.0f / Size.X;
    float ScaleV = 1.0f / Size.Z;

    TVertexInstanceAttributesRef<FVector2f> UVs = MeshDescription->VertexInstanceAttributes().GetAttributesRef<FVector2f>(MeshAttribute::VertexInstance::TextureCoordinate);

    for (const FVertexInstanceID VertexInstanceID : MeshDescription->VertexInstances().GetElementIDs())
    {
        FVector3f Position = MeshDescription->GetVertexPosition(MeshDescription->GetVertexInstanceVertex(VertexInstanceID));
        FVector2f UV;
        UV.X = (Position.X - Min.X) * ScaleU;
        UV.Y = (Position.Z - Min.Z) * ScaleV;

        UVs.Set(VertexInstanceID, UVChannel, UV);
    }

    StaticMesh->CommitMeshDescription(0);
    StaticMesh->Modify();
    StaticMesh->MarkPackageDirty();

    UE_LOG(LogTemp, Log, TEXT("Planar mapping applied and polygons separated by Material ID."));

    return StaticMesh;
}

void UMyBlueprintFunctions::OffsetUVs(UStaticMesh* StaticMesh, int32 TargetMaterialID, float UVRangeOffset, int32 UVChannel)
{
    if (!StaticMesh || StaticMesh->GetNumSourceModels() == 0)
    {
        UE_LOG(LogTemp, Warning, TEXT("Invalid Static Mesh or no source data available."));
        return;
    }

    FMeshDescription* MeshDescription = StaticMesh->GetMeshDescription(0);
    if (!MeshDescription)
    {
        UE_LOG(LogTemp, Warning, TEXT("No Mesh Description available."));
        return;
    }

    TVertexInstanceAttributesRef<FVector2f> UVs = MeshDescription->VertexInstanceAttributes().GetAttributesRef<FVector2f>(MeshAttribute::VertexInstance::TextureCoordinate);
    TPolygonGroupAttributesConstRef<FName> PolygonGroupNames = MeshDescription->PolygonGroupAttributes().GetAttributesRef<FName>(MeshAttribute::PolygonGroup::ImportedMaterialSlotName);

    if (TargetMaterialID <= 0 || TargetMaterialID > PolygonGroupNames.GetNumElements())
    {
        UE_LOG(LogTemp, Warning, TEXT("Invalid Material ID"));
        return;
    }

    FPolygonGroupID TargetPolygonGroup = FPolygonGroupID(TargetMaterialID - 1);
    float MaterialOffsetX = (TargetMaterialID - 1) * UVRangeOffset;

    for (const FPolygonID PolygonID : MeshDescription->Polygons().GetElementIDs())
    {
        if (MeshDescription->GetPolygonPolygonGroup(PolygonID) == TargetPolygonGroup)
        {
            for (const FVertexInstanceID VertexInstanceID : MeshDescription->GetPolygonVertexInstances(PolygonID))
            {
                FVector2f UV = UVs.Get(VertexInstanceID, UVChannel);
                UV.X += MaterialOffsetX;
                UVs.Set(VertexInstanceID, UVChannel, UV);
            }
        }
    }

    StaticMesh->CommitMeshDescription(0);
    StaticMesh->Modify();
    StaticMesh->MarkPackageDirty();

    UE_LOG(LogTemp, Log, TEXT("UVs have been adjusted for Material ID %d in UVChannel %d."), TargetMaterialID, UVChannel);
}

📚 참고 자료

profile
개발자/디자이너 둘다 잘하고싶은 코린이

0개의 댓글