初めに
Editorにインポートしたテクスチャには基本的に自動でMipmapというものが生成されます。このMipmapというものはテクスチャ画像を元に1/2倍ずつサイズを小さくした画像をMipmap Levelごとに生成し、画面との距離に応じて表示するテクスチャ画像を切り替えるものです(例えばMipmap Level 0で2048px、Level 1で1024px、Level 2で512px...)。
Mipmapがあると何が嬉しいかというと細かい画像によって引き起こされるチラつき(モアレ)をなくしてくれます。
以下のような画像な場合、近くで見る分には問題ないですが、遠くに離れた際に同じ画像サイズで見ようとするとチラつきが発生します。
冒頭でEdiotrにインポートした画像は自動でMipmapを作成してくれると話しましたが、DatasmithRuntimeを使って実行中に読み込んだモデルに適用されているテクスチャ画像に対してはMipmapが自動で生成されません。
今回はプラグイン改造で読み込み時に生成されるようにします。
環境情報
Windows 11
UE 5.3.2
UE 5.5.4
実装
DatasmithRuntimeプラグインのTextureImporter.cppを改造していきます。
まず下記コードを追加します
TMap<EPixelFormat, int32> BytesPerPixels = {
{PF_A32B32G32R32F, 16},
{PF_B8G8R8A8, 4},
{PF_G8, 1},
{PF_G16, 2},
{PF_FloatRGB, 6},
{PF_FloatRGBA, 8},
{PF_R16F, 2},
{PF_R32_FLOAT, 4},
{PF_R32G32B32F, 12},
{PF_R16G16B16A16_UNORM, 8},
{PF_R8G8B8A8, 4},
{PF_A8, 1},
{PF_R8, 1},
};
void GenerateMipmap(UTexture2D* Texture2D, FTextureData& TextureData)
{
if (!TextureData.ImageData || TextureData.Width <= 0 || TextureData.Height <= 0)
{
UE_LOG(LogTemp, Error, TEXT("Invalid TextureData: Null ImageData or zero dimensions."));
return;
}
if (!Texture2D->PlatformData)
{
UE_LOG(LogTemp, Error, TEXT("Invalid Texture2D: Null PlatformData."));
return;
}
int32* FindValue = BytesPerPixels.Find(TextureData.PixelFormat);
if (!FindValue)
{
UE_LOG(LogTemp, Error, TEXT("Unsupported PixelFormat: %d"), (int32)TextureData.PixelFormat);
return;
}
int32 BytesPerPixel = *FindValue;
UE_LOG(LogTemp, Error, TEXT("BytesPerPixel: %d"), BytesPerPixel);
// PlatformData再構築
Texture2D->GetPlatformData()->Mips.Empty();
// TextureDataからMip0を作成して追加
FTexture2DMipMap* Mip0 = new FTexture2DMipMap();
Mip0->SizeX = TextureData.Width;
Mip0->SizeY = TextureData.Height;
Mip0->BulkData.Lock(LOCK_READ_WRITE);
void* MipData = Mip0->BulkData.Realloc(TextureData.Width * TextureData.Height * BytesPerPixel);
FMemory::Memcpy(MipData, TextureData.ImageData, TextureData.Width * TextureData.Height * BytesPerPixel);
Mip0->BulkData.Unlock();
Texture2D->GetPlatformData()->Mips.Add(Mip0);
// Mip生成(最大10レベル)
uint8* SrcData = (uint8*)FMemory::Malloc(TextureData.Width * TextureData.Height * BytesPerPixel); // メモリの先頭位置
FMemory::Memcpy(SrcData, TextureData.ImageData, TextureData.Width * TextureData.Height * BytesPerPixel);
int32 SrcWidth = TextureData.Width;
int32 SrcHeight = TextureData.Height;
// サイズを1/2にしていく
for (int32 MipIndex = 1; MipIndex < 10; ++MipIndex)
{
int32 DstWidth = FMath::Max(1, SrcWidth / 2);
int32 DstHeight = FMath::Max(1, SrcHeight / 2);
uint8* DstData = (uint8*)FMemory::Malloc(DstWidth * DstHeight * BytesPerPixel);
// 4つのピクセルの平均を作成
for (int32 y = 0; y < DstHeight; ++y)
{
for (int32 x = 0; x < DstWidth; ++x)
{
int32 SrcX = x * 2;
int32 SrcY = y * 2;
if (BytesPerPixel == 1)
{
// グレースケールの場合
uint8 Pixel00 = *(SrcData + ((SrcY * SrcWidth + SrcX) * BytesPerPixel));
uint8 Pixel01 = *(SrcData + ((SrcY * SrcWidth + FMath::Min(SrcX + 1, SrcWidth - 1)) * BytesPerPixel));
uint8 Pixel10 = *(SrcData + ((FMath::Min(SrcY + 1, SrcHeight - 1) * SrcWidth + SrcX) * BytesPerPixel));
uint8 Pixel11 = *(SrcData + ((FMath::Min(SrcY + 1, SrcHeight - 1) * SrcWidth + FMath::Min(SrcX + 1, SrcWidth - 1)) * BytesPerPixel));
uint8 OutGray = (Pixel00 + Pixel01 + Pixel10 + Pixel11) / 4;
FMemory::Memcpy(DstData + ((y * DstWidth + x) * BytesPerPixel), &OutGray, BytesPerPixel);
}
else if (BytesPerPixel == 2)
{
// 16ビットグレースケールの場合
uint16* Pixel00 = (uint16*)(SrcData + ((SrcY * SrcWidth + SrcX) * BytesPerPixel));
uint16* Pixel01 = (uint16*)(SrcData + ((SrcY * SrcWidth + FMath::Min(SrcX + 1, SrcWidth - 1)) * BytesPerPixel));
uint16* Pixel10 = (uint16*)(SrcData + ((FMath::Min(SrcY + 1, SrcHeight - 1) * SrcWidth + SrcX) * BytesPerPixel));
uint16* Pixel11 = (uint16*)(SrcData + ((FMath::Min(SrcY + 1, SrcHeight - 1) * SrcWidth + FMath::Min(SrcX + 1, SrcWidth - 1)) * BytesPerPixel));
uint16 OutGray = (*Pixel00 + *Pixel01 + *Pixel10 + *Pixel11) / 4;
FMemory::Memcpy(DstData + ((y * DstWidth + x) * BytesPerPixel), &OutGray, BytesPerPixel);
}
else if (BytesPerPixel == 4)
{
// RGBAの場合
FColor Pixel00 = *((FColor*)(SrcData + ((SrcY * SrcWidth + SrcX) * BytesPerPixel)));
FColor Pixel01 = *((FColor*)(SrcData + ((SrcY * SrcWidth + FMath::Min(SrcX + 1, SrcWidth - 1)) * BytesPerPixel)));
FColor Pixel10 = *((FColor*)(SrcData + ((FMath::Min(SrcY + 1, SrcHeight - 1) * SrcWidth + SrcX) * BytesPerPixel)));
FColor Pixel11 = *((FColor*)(SrcData + ((FMath::Min(SrcY + 1, SrcHeight - 1) * SrcWidth + FMath::Min(SrcX + 1, SrcWidth - 1)) * BytesPerPixel)));
FColor OutColor;
OutColor.R = (Pixel00.R + Pixel01.R + Pixel10.R + Pixel11.R) / 4;
OutColor.G = (Pixel00.G + Pixel01.G + Pixel10.G + Pixel11.G) / 4;
OutColor.B = (Pixel00.B + Pixel01.B + Pixel10.B + Pixel11.B) / 4;
OutColor.A = (Pixel00.A + Pixel01.A + Pixel10.A + Pixel11.A) / 4;
FMemory::Memcpy(DstData + ((y * DstWidth + x) * BytesPerPixel), &OutColor, BytesPerPixel);
}
else if (BytesPerPixel == 6)
{
// 48ビットRGB
struct FFloatRGB
{
float R, G, B;
};
FFloatRGB* Pixel00 = (FFloatRGB*)(SrcData + ((SrcY * SrcWidth + SrcX) * BytesPerPixel));
FFloatRGB* Pixel01 = (FFloatRGB*)(SrcData + ((SrcY * SrcWidth + FMath::Min(SrcX + 1, SrcWidth - 1)) * BytesPerPixel));
FFloatRGB* Pixel10 = (FFloatRGB*)(SrcData + ((FMath::Min(SrcY + 1, SrcHeight - 1) * SrcWidth + SrcX) * BytesPerPixel));
FFloatRGB* Pixel11 = (FFloatRGB*)(SrcData + ((FMath::Min(SrcY + 1, SrcHeight - 1) * SrcWidth + FMath::Min(SrcX + 1, SrcWidth - 1)) * BytesPerPixel));
FFloatRGB OutColor;
OutColor.R = (Pixel00->R + Pixel01->R + Pixel10->R + Pixel11->R) / 4.0f;
OutColor.G = (Pixel00->G + Pixel01->G + Pixel10->G + Pixel11->G) / 4.0f;
OutColor.B = (Pixel00->B + Pixel01->B + Pixel10->B + Pixel11->B) / 4.0f;
FMemory::Memcpy(DstData + ((y * DstWidth + x) * BytesPerPixel), &OutColor, BytesPerPixel);
}
else if (BytesPerPixel == 8)
{
// 64ビットRGBA
struct FFloatRGBA
{
float R, G, B, A;
};
FFloatRGBA* Pixel00 = (FFloatRGBA*)(SrcData + ((SrcY * SrcWidth + SrcX) * BytesPerPixel));
FFloatRGBA* Pixel01 = (FFloatRGBA*)(SrcData + ((SrcY * SrcWidth + FMath::Min(SrcX + 1, SrcWidth - 1)) * BytesPerPixel));
FFloatRGBA* Pixel10 = (FFloatRGBA*)(SrcData + ((FMath::Min(SrcY + 1, SrcHeight - 1) * SrcWidth + SrcX) * BytesPerPixel));
FFloatRGBA* Pixel11 = (FFloatRGBA*)(SrcData + ((FMath::Min(SrcY + 1, SrcHeight - 1) * SrcWidth + FMath::Min(SrcX + 1, SrcWidth - 1)) * BytesPerPixel));
FFloatRGBA OutColor;
OutColor.R = (Pixel00->R + Pixel01->R + Pixel10->R + Pixel11->R) / 4.0f;
OutColor.G = (Pixel00->G + Pixel01->G + Pixel10->G + Pixel11->G) / 4.0f;
OutColor.B = (Pixel00->B + Pixel01->B + Pixel10->B + Pixel11->B) / 4.0f;
OutColor.A = (Pixel00->A + Pixel01->A + Pixel10->A + Pixel11->A) / 4.0f;
FMemory::Memcpy(DstData + ((y * DstWidth + x) * BytesPerPixel), &OutColor, BytesPerPixel);
}
else if (BytesPerPixel == 16)
{
// 128ビットRGBA
struct FFloatRGBA16
{
float R, G, B, A;
};
FFloatRGBA16* Pixel00 = (FFloatRGBA16*)(SrcData + ((SrcY * SrcWidth + SrcX) * BytesPerPixel));
FFloatRGBA16* Pixel01 = (FFloatRGBA16*)(SrcData + ((SrcY * SrcWidth + FMath::Min(SrcX + 1, SrcWidth - 1)) * BytesPerPixel));
FFloatRGBA16* Pixel10 = (FFloatRGBA16*)(SrcData + ((FMath::Min(SrcY + 1, SrcHeight - 1) * SrcWidth + SrcX) * BytesPerPixel));
FFloatRGBA16* Pixel11 = (FFloatRGBA16*)(SrcData + ((FMath::Min(SrcY + 1, SrcHeight - 1) * SrcWidth + FMath::Min(SrcX + 1, SrcWidth - 1)) * BytesPerPixel));
FFloatRGBA16 OutColor;
OutColor.R = (Pixel00->R + Pixel01->R + Pixel10->R + Pixel11->R) / 4.0f;
OutColor.G = (Pixel00->G + Pixel01->G + Pixel10->G + Pixel11->G) / 4.0f;
OutColor.B = (Pixel00->B + Pixel01->B + Pixel10->B + Pixel11->B) / 4.0f;
OutColor.A = (Pixel00->A + Pixel01->A + Pixel10->A + Pixel11->A) / 4.0f;
FMemory::Memcpy(DstData + ((y * DstWidth + x) * BytesPerPixel), &OutColor, BytesPerPixel);
}
}
}
FTexture2DMipMap* NewMip = new FTexture2DMipMap();
NewMip->SizeX = DstWidth;
NewMip->SizeY = DstHeight;
NewMip->BulkData.Lock(LOCK_READ_WRITE);
void* MipDataPtr = NewMip->BulkData.Realloc(DstWidth * DstHeight * BytesPerPixel);
FMemory::Memcpy(MipDataPtr, DstData, DstWidth * DstHeight * BytesPerPixel);
NewMip->BulkData.Unlock();
Texture2D->GetPlatformData()->Mips.Add(NewMip);
// 前のバッファを解放し、Dst → Src として使う
FMemory::Free(SrcData);
SrcData = DstData;
SrcWidth = DstWidth;
SrcHeight = DstHeight;
}
FMemory::Free(SrcData); // 最後のバッファも解放
}
大まかな処理の流れとしては以下の流れです。
①DatasmithRuntimeではロードしたテクスチャデータをFTextureDataで管理しているのでMipmap Level 0として扱う
②元画像データのサイズを1/2倍ずつして画像サイズを縮小していく(10個分になるように生成)
③DatasmithRuntimeが持っているTexture2DのMipmapに登録していく
画像サイズの縮小方法に関しては左上のピクセルから2×2ピクセルを取り出し各チャネル(RGBAチャネルなど)に対して平均をとっていく方式で縮小していきます。
画像は縦横の2次元でピクセルデータを扱っていますが、データのコピーなどはピクセルデータを1次元データとして扱って処理していきます。
その際に1ピクセル当たり何バイトで扱うかはPixcelFormatごとに異なるので注意です。
for文の中でPixel01とPixel11でMinを使って計算しているのは画像の横幅よりも大きい場所を指定しないように調整するためです。
作成したGenerateMipmap関数はCreateImageTexture関数の中でTexture2D->UpdateResource();をする前で呼びます。
UTexture2D* CreateImageTexture(UTexture2D* Texture2D, FTextureData& TextureData, IDatasmithTextureElement* TextureElement, FDataCleanupFunc& DataCleanupFunc)
{
TRACE_CPUPROFILER_EVENT_SCOPE(FSceneImporter::CreateImageTexture);
if (Texture2D == nullptr)
{
Texture2D = UTexture2D::CreateTransient(TextureData.Width, TextureData.Height, TextureData.PixelFormat);
if (!Texture2D)
{
return nullptr;
}
FString TextureName = FString::Printf(TEXT("T_%s_%d"), TextureElement->GetName(), TextureElement->GetNodeId());
#ifdef ASSET_DEBUG
UPackage* Package = CreatePackage(*FPaths::Combine( TEXT("/Game/Runtime/Textures"), TextureName));
RenameObject(Texture2D, *TextureName, Package);
Texture2D->SetFlags(RF_Public);
#else
RenameObject(Texture2D, *TextureName);
#endif
}
#if WITH_EDITORONLY_DATA
FAssetImportInfo Info;
Info.Insert(FAssetImportInfo::FSourceFile(TextureElement->GetFile()));
Texture2D->AssetImportData->SourceData = MoveTemp(Info);
const float RGBCurve = TextureElement->GetRGBCurve();
if (FMath::IsNearlyEqual(RGBCurve, 1.0f) == false && RGBCurve > 0.f)
{
Texture2D->AdjustRGBCurve = RGBCurve;
}
#endif
Texture2D->SRGB = TextureElement->GetSRGB() == EDatasmithColorSpace::sRGB;
// Ensure there's no compression (we're editing pixel-by-pixel)
Texture2D->CompressionSettings = TextureCompressionSettings::TC_VectorDisplacementmap;
// Mipmap生成
GenerateMipmap(Texture2D, TextureData);
// Update the texture with these new settings
Texture2D->UpdateResource();
// The content of the texture has changed, update it
if (TextureData.ImageData != nullptr)
{
TextureData.Region = FUpdateTextureRegion2D(0, 0, 0, 0, TextureData.Width, TextureData.Height);
Texture2D->UpdateTextureRegions(0, 1, &TextureData.Region, TextureData.Pitch, TextureData.BytesPerPixel, TextureData.ImageData, DataCleanupFunc );
}
return Texture2D;
}
こちらの変更を行うことでDatasmithRuntimeを使って実行中にロードした3Dデータに適用されているテクスチャに対してもMipmapを動的に生成することができるので画像のチラつきを抑えることができます。
あとがき
今回こちらの実装を行う際に当初はプラグイン改造ではなく、独自の関数を作成してDatasmithRuntimeで読み込みが完了した後にTexture2Dに対してMipmapを生成する方式を試してみました。
結果としてMipmap自体の生成はできたのですが画像が乱れてしまう現象が発生していました。
UE5.3.2では読み込んだ画像を開くだけで画像の乱れが発生しており(Mipmap生成処理を行わなくても)、UE5.5.4では画像を開くだけでは乱れなかったのですが、同様に読み込み後にMipmap生成処理を行うと画像が乱れていました。
原因は詳しくわかっていませんがDatasmithRuntimeでの読み込み処理の過程で画像の圧縮なども行われているようなのでTexture2Dのデータではなく、ロードした画像データ(FTextureData)を使わないとうまくいかないのかもしれないです。