본문 바로가기

프로그래밍/Unreal

Landscape Heightmap의 데이터를 World Height로 바꾸어보자

 아마 언리얼을 사용하는 대부분의 사용자에게는 전혀 필요없는 정보일 것이다.

 사건의 발단은 언리얼 월드 전체를 Z축 방향으로 일정 Offset 만큼 올려야하는 일이 있어서였다.

 월드 안에 있는 오브젝트가 한 두개 였다면 에디터에서 진행했겠지만, 수십만개의 오브젝트가 있었기 때문에 그건 불가능했다.

 Actor의 경우 정한 Offset 만큼 Z축 방향으로 올려준 뒤, SetActorLocation을 불러주고 저장하면 된다.

 그러나 이렇게 해버리면 Landscape는 올라가지 않는다.

 

 Landscape를 위해서는 Heightmap 데이터를 불러와서, World의 Offset만큼 높여준 뒤 다시 Heightmap의 형태로 저장해주어야 한다.

 

Get landscape heightmap data

 Heightmap 데이터를 얻어오는 코드는 아래와 같다.

ALandscape* LandscapeActor;
{
    // 이 녀석은 PersistentLevel의 Actor에 붙어있을 것이다.
    for (AActor* Actor : GWorld->PersistentLevel->Actors)
    {
        if (Actor->IsA<ALandscape>())
        {
            LandscapeActor = Cast<ALandscape>(Actor);
            break;
        }
    }
}
 
// LandscapeInfo를 얻어오자.
ULandscapeInfo LandscapeInfo = LandscapeActor->GetLandscapeInfo();
 
// 만약 Heightmap을 Export 하고 싶다면, 여기에서 바로 Export 해버리면 된다.
LandscapeInfo->ExportHeightmap(TEXT("MyPath\\Heightmap.png"));
 
// 그렇지 않고 Heightmap의 데이터를 얻어오고 싶다면 우선 Landscape의 Extent를 알아오자.
FIntRect LandscapeExtent;
LandscapeInfo->GetLandscapeExtent(LandscapeExtent.Min.X, LandscapeExtent.Min.Y, LandscapeExtent.Max.X, LandscapeExtent.Max.Y);
 
// LandscapeEdit 객체는 우리가 HeightData에 접근하기위한 인터페이스다.
FLandscapeEditDataInterface LandscapeEdit = FLandscapeEditDataInterface(LandscapeInfo);
 
// HeightData는 uint16타입으로 저장되어있다. uint16은 0~65535의 범위를 갖는 정수형 타입이다.
TArray<uint16> HeightData;
// Heightmap Texture는 항상 1의 바운더리 패딩값을 두고 있다는 것을 기억하자.
HeightData.AddZeroed((LandscapeExtent.Max.X - LandscapeExtent.Min.X + 1) * (LandscapeExtent.Max.Y - LandscapeExtent.Min.Y + 1));
LandscapeEdit.GetHeightDataFast(LandscapeExtent.Min.X, LandscapeExtent.Min.Y, LandscapeExtent.Max.X, LandscapeExtent.Max.Y, HeightData.GetData(), 0);

 

Change heightmap data

 위 코드에서 Heightmap을 얻어오는 것은 성공했다. 이제 이 Heightmap 데이터를 잘 수정한 뒤, 다시 Landscape에 세팅해주기만 하면 된다.

 문제는 Heightmap Data는 World Location의 Z값과는 Translate가 필요하다는 것이다. Heightmap데이터가 어떻게 Z값을 표현하는지 알아보자.

 

 Heightmap에 저장되어있는 값은 float값으로, -256~256까지의 값을 저장하도록 되어있다.

 HeightData에 있는 uint16은 이 값을 0~65535의 값으로 사영한 것으로 uint16:0이 float -256에, uint16:32768 값이 float 0, uint16:65535 값은 float 256에 매핑된다.

 이 값이 다시 World의 Z값으로 변환되기 위해서는 LandscapeActor Transform의 ZScale 값을 이용해주면된다.

 

 Unreal Engine에서는 Landscape Z Scale값이 1일때, Heightmap은 -256uu~256uu까지의 값을 가진다.

 Scale을 늘릴 수록 이 Heightmap이 표현하는 범위가 넓어지게 되고, 그만큼 해상도는 떨어지게 된다.

 만약 Landscape의 Z Scale값은 145라면, Landscape Heightmap은 -371.20~371.20 (m)를 표현할 수 있게 된다. (256 * 145 = 37120)

 

 아래와 같이 변경하면 내가 원하는 Offset 만큼 Heightmap 데이터를 변경하는 것이 가능하다.

constexpr float MidValue = 32768.f;
constexpr float LandscapeZScale = LandscapeActor->GetTransform().GetScale3D().Z;
 
for (int32 i = 0; i < HeightData.Num(); ++i)
{
    uint16 Data = HeightData[i];
     
    // HeightData를 float차원으로 투영해주자
    float FloatValue = ((float)Data - MidValue) * 256.f / MidValue;
 
    // 여기서 Landscape Z Scale을 곱해주면 World Height가 된다.
    float WorldHeight = FloatValue * LandscapeZScale;
 
    // 우리가 원하는 Offset만큼 올려주자.
    float MovedHeight = WorldHeight + Offset;
 
    // 저장할 값을 역으로 다시 계산해주자.
    uint16 MovedData = (uint16)((MovedHeight / (LandscapeZScale * 256.f)) * MidValue - MidValue);
    HeightData[i] = MovedData;
}

 

 이 코드는 UE 4.26기반으로 작성했기 때문에, 현재 Landscape에 대해서도 잘 동작할지는 모르겠다.