언리얼 auto uv offset move 기능 개발

김슬아·2025년 1월 1일

언리얼에서 아트 작업, 블프 작업만 해봤지 코드를 작성하는 건 처음이라 코드 컴파일할 수 있는 환경 만드는데 애로사항이 많아 힘들었다..ㅠㅠ
일단 한글경로는 절.대. 쓰지 말아야 한다...!
https://velog.io/@singery00/UE5-C-%EC%96%B8%EB%A6%AC%EC%96%BC%EA%B3%BC-Visual-Studio-2022-%EC%97%B0%EB%8F%99%ED%95%98
이분의 벨로그를 믿고 따라했더니 잘 되서 다행.. 감사드립니다🙈

어쨌든 본론으로 들어가서

언리얼 에디터 유틸리티 블루프린트와 c++코드를 블루프린트 노드로 export하여 기능 개발중이다.

void UMeshProcessor::OffsetUVs(UStaticMesh* StaticMesh, int32 MaterialID, FVector2f UVOffset)
{
    if (!StaticMesh || !StaticMesh->GetRenderData())
    {
        UE_LOG(LogTemp, Warning, TEXT("Invalid Static Mesh or no Render Data available"));
        return;
    }

    FStaticMeshRenderData* RenderData = StaticMesh->GetRenderData();
    FStaticMeshLODResources& LODResources = RenderData->LODResources[0];

    const FStaticMeshSectionArray& Sections = LODResources.Sections;
    if (MaterialID >= Sections.Num())
    {
        UE_LOG(LogTemp, Warning, TEXT("Invalid Material ID"));
        return;
    }

    const FStaticMeshSection& TargetSection = Sections[MaterialID];
    FStaticMeshVertexBuffers& VertexBuffers = LODResources.VertexBuffers;

    for (uint32 TriangleIndex = 0; TriangleIndex < TargetSection.NumTriangles; ++TriangleIndex)
    {
        uint32 BaseIndex = TargetSection.FirstIndex + TriangleIndex * 3;

        // 삼각형의 꼭짓점 인덱스
        uint32 Index0 = LODResources.IndexBuffer.GetIndex(BaseIndex);
        uint32 Index1 = LODResources.IndexBuffer.GetIndex(BaseIndex + 1);
        uint32 Index2 = LODResources.IndexBuffer.GetIndex(BaseIndex + 2);

        // 삼각형 꼭짓점의 UV 좌표
        FVector2f UV0 = VertexBuffers.StaticMeshVertexBuffer.GetVertexUV(Index0, 0);
        FVector2f UV1 = VertexBuffers.StaticMeshVertexBuffer.GetVertexUV(Index1, 0);
        FVector2f UV2 = VertexBuffers.StaticMeshVertexBuffer.GetVertexUV(Index2, 0);

        // 폴리곤 중심 UV 좌표 계산
        FVector2f UVCenter = (UV0 + UV1 + UV2) / 3.0f;

        // 중심을 기준으로 UV 좌표 이동
        UV0 = UV0 - UVCenter + UVOffset;
        UV1 = UV1 - UVCenter + UVOffset;
        UV2 = UV2 - UVCenter + UVOffset;

        // UV 좌표 설정
        VertexBuffers.StaticMeshVertexBuffer.SetVertexUV(Index0, 0, UV0);
        VertexBuffers.StaticMeshVertexBuffer.SetVertexUV(Index1, 0, UV1);
        VertexBuffers.StaticMeshVertexBuffer.SetVertexUV(Index2, 0, UV2);
    }

    StaticMesh->Modify();
    StaticMesh->MarkPackageDirty();
    UE_LOG(LogTemp, Log, TEXT("UVs for Material ID %d have been offset by X: %f, Y: %f"), MaterialID, UVOffset.X, UVOffset.Y);
}
void UMeshProcessor::OffsetUVs(UStaticMesh* StaticMesh, int32 MaterialID, FVector2f UVOffset)
{
    if (!StaticMesh || !StaticMesh->GetRenderData())
    {
        UE_LOG(LogTemp, Warning, TEXT("Invalid Static Mesh or no Render Data available"));
        return;
    }

    FStaticMeshRenderData* RenderData = StaticMesh->GetRenderData();
    FStaticMeshLODResources& LODResources = RenderData->LODResources[0];

    const FStaticMeshSectionArray& Sections = LODResources.Sections;
    if (MaterialID >= Sections.Num())
    {
        UE_LOG(LogTemp, Warning, TEXT("Invalid Material ID"));
        return;
    }

    const FStaticMeshSection& TargetSection = Sections[MaterialID];
    FStaticMeshVertexBuffers& VertexBuffers = LODResources.VertexBuffers;

    TSet<int32> ModifiedVertices;

    for (uint32 TriangleIndex = 0; TriangleIndex < TargetSection.NumTriangles; ++TriangleIndex)
    {
        uint32 BaseIndex = TargetSection.FirstIndex + TriangleIndex * 3;

        uint32 Index0 = LODResources.IndexBuffer.GetIndex(BaseIndex);
        uint32 Index1 = LODResources.IndexBuffer.GetIndex(BaseIndex + 1);
        uint32 Index2 = LODResources.IndexBuffer.GetIndex(BaseIndex + 2);

        // Index 0
        if (!ModifiedVertices.Contains(Index0))
        {
            FVector2f UV0 = VertexBuffers.StaticMeshVertexBuffer.GetVertexUV(Index0, 0);
            UV0.X += UVOffset.X;
            UV0.Y += UVOffset.Y;
            VertexBuffers.StaticMeshVertexBuffer.SetVertexUV(Index0, 0, UV0);
            ModifiedVertices.Add(Index0);
        }

        // Index 1
        if (!ModifiedVertices.Contains(Index1))
        {
            FVector2f UV1 = VertexBuffers.StaticMeshVertexBuffer.GetVertexUV(Index1, 0);
            UV1.X += UVOffset.X;
            UV1.Y += UVOffset.Y;
            VertexBuffers.StaticMeshVertexBuffer.SetVertexUV(Index1, 0, UV1);
            ModifiedVertices.Add(Index1);
        }

        // Index 2
        if (!ModifiedVertices.Contains(Index2))
        {
            FVector2f UV2 = VertexBuffers.StaticMeshVertexBuffer.GetVertexUV(Index2, 0);
            UV2.X += UVOffset.X;
            UV2.Y += UVOffset.Y;
            VertexBuffers.StaticMeshVertexBuffer.SetVertexUV(Index2, 0, UV2);
            ModifiedVertices.Add(Index2);
        }
    }

    StaticMesh->Modify();
    StaticMesh->MarkPackageDirty();
    UE_LOG(LogTemp, Log, TEXT("UVs for Material ID %d have been offset by X: %f, Y: %f"), MaterialID, UVOffset.X, UVOffset.Y);
}

지피티가 짜준 최종 코드

#include "MeshProcessor.h"
#include "RenderResource.h"
#include "StaticMeshResources.h"

void UMeshProcessor::OffsetUVs(UStaticMesh* StaticMesh, int32 MaterialID, float UVRangeOffset)
{
    if (!StaticMesh || !StaticMesh->GetRenderData())
    {
        UE_LOG(LogTemp, Warning, TEXT("Invalid Static Mesh or no Render Data available"));
        return;
    }

    FStaticMeshRenderData* RenderData = StaticMesh->GetRenderData();
    FStaticMeshLODResources& LODResources = RenderData->LODResources[0];

    const FStaticMeshSectionArray& Sections = LODResources.Sections;
    if (MaterialID >= Sections.Num())
    {
        UE_LOG(LogTemp, Warning, TEXT("Invalid Material ID"));
        return;
    }

    const FStaticMeshSection& TargetSection = Sections[MaterialID];
    FStaticMeshVertexBuffers& VertexBuffers = LODResources.VertexBuffers;

    // UV 영역 분리: Material ID를 기준으로 UV.X를 고유 영역으로 이동
    float MaterialOffsetX = MaterialID * UVRangeOffset;

    // UV 좌표 캐싱
    TSet<int32> ModifiedVertices;

    for (uint32 TriangleIndex = 0; TriangleIndex < TargetSection.NumTriangles; ++TriangleIndex)
    {
        uint32 BaseIndex = TargetSection.FirstIndex + TriangleIndex * 3;

        uint32 Index0 = LODResources.IndexBuffer.GetIndex(BaseIndex);
        uint32 Index1 = LODResources.IndexBuffer.GetIndex(BaseIndex + 1);
        uint32 Index2 = LODResources.IndexBuffer.GetIndex(BaseIndex + 2);

        // Index 0
        if (!ModifiedVertices.Contains(Index0))
        {
            FVector2f UV0 = VertexBuffers.StaticMeshVertexBuffer.GetVertexUV(Index0, 0);
            UV0.X += MaterialOffsetX; // Material ID 기준 UV 분리
            VertexBuffers.StaticMeshVertexBuffer.SetVertexUV(Index0, 0, UV0);
            ModifiedVertices.Add(Index0);
        }

        // Index 1
        if (!ModifiedVertices.Contains(Index1))
        {
            FVector2f UV1 = VertexBuffers.StaticMeshVertexBuffer.GetVertexUV(Index1, 0);
            UV1.X += MaterialOffsetX; // Material ID 기준 UV 분리
            VertexBuffers.StaticMeshVertexBuffer.SetVertexUV(Index1, 0, UV1);
            ModifiedVertices.Add(Index1);
        }

        // Index 2
        if (!ModifiedVertices.Contains(Index2))
        {
            FVector2f UV2 = VertexBuffers.StaticMeshVertexBuffer.GetVertexUV(Index2, 0);
            UV2.X += MaterialOffsetX; // Material ID 기준 UV 분리
            VertexBuffers.StaticMeshVertexBuffer.SetVertexUV(Index2, 0, UV2);
            ModifiedVertices.Add(Index2);
        }
    }

    // Material Slot 및 Section 통합
    UMeshProcessor::ConsolidateMaterials(StaticMesh);

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

    UE_LOG(LogTemp, Log, TEXT("UVs have been adjusted, and materials have been consolidated to 1 slot."));
}

void UMeshProcessor::ConsolidateMaterials(UStaticMesh* StaticMesh)
{
    if (!StaticMesh)
    {
        UE_LOG(LogTemp, Warning, TEXT("Invalid Static Mesh for material consolidation."));
        return;
    }

    FStaticMeshRenderData* RenderData = StaticMesh->GetRenderData();
    if (!RenderData)
    {
        UE_LOG(LogTemp, Warning, TEXT("No Render Data available for the static mesh."));
        return;
    }

    FStaticMeshLODResources& LODResources = RenderData->LODResources[0];

    // Section 통합
    if (LODResources.Sections.Num() > 1)
    {
        LODResources.Sections.SetNum(1);
        LODResources.Sections[0].FirstIndex = 0;
        LODResources.Sections[0].NumTriangles = LODResources.IndexBuffer.GetNumIndices() / 3;
        LODResources.Sections[0].MaterialIndex = 0;
    }

    // Material Slot 통합
    if (StaticMesh->GetStaticMaterials().Num() > 1)
    {
        TArray<FStaticMaterial> SingleMaterial;
        SingleMaterial.Add(StaticMesh->GetStaticMaterials()[0]); // 첫 번째 Material만 유지
        StaticMesh->SetStaticMaterials(SingleMaterial);
    }

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

구현된 것
1.material ID * offset 값으로 material ID 별 uv 좌표를 옮기는 기능.
2. 옮긴 후 material ID를 모두 1로 통일 한후 머터리얼 슬롯도 1개만 남긴다,

개선해야할 것
1. 좌표가 정확하게 겹쳐있으면 uv가 그냥 붙어버리는 경우가 있다..
0.0001 정도로 미세하게 띄워서 해도 붙어버리는 경우가 있다.
지금 material ID 별로 따로 uv 좌표를 옮기는데도 붙는다..
이건 추후에 디벨롭할 부분으로 남겨놓고 나머지 기능 구현 먼저 해야할 것 같다.

남은 기능 구현
1. material ID * offset 값이 아니라 material slot name 의 prefix 값을 보고 그 값을 곱하여 구현

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

0개의 댓글