跳轉至

UE 實現各種圖片(UTexture2D)操作(讀取、保存、複製、剪貼板...)

以下程式碼都是以 UE5.3 版本為例。

源碼

可以在UE商店裡獲取插件AIChatPlus

讀取:UE 實現讀取本地系統圖片為 UTexture2D

通用方法

這種方法在編輯器和遊戲模式下都可使用,支援的圖片檔案格式包括 PNG、JPEG、BMP、ICO、EXR、ICNS、HDR、TIFF、DDS、TGA,基本上能涵蓋大多數常見的圖片類型。

程式碼也很簡潔:

#include <Engine/Texture2D.h>
#include <ImageUtils.h>

UTexture2D* LoadImage(const FString& InLoadPath)
{
    FImage ImageInfo;
    FImageUtils::LoadImage(*InLoadPath, ImageInfo);
    return FImageUtils::CreateTexture2DFromImage(ImageInfo);
}

返回的即是 UTexture2D。

編輯器專用方法

這個方法能額外支援更多種類的圖片:UDIM 紋理貼圖、IES 檔案、PCX、PSD。

程式碼的實現可能會變得更複雜一些:

#include <Engine/Texture2D.h>
#include <Misc/FileHelper.h>
#include <Misc/Paths.h>
#include <UObject/UObjectGlobals.h>

#if WITH_EDITOR
UTexture2D* LoadImage(const FString& InLoadPath)
{

    TArray64<uint8> Buffer;
    if (!FFileHelper::LoadFileToArray(Buffer, *InLoadPath))
    {
        return nullptr;
    }

    const FString TextureName;
    const FString Extension = FPaths::GetExtension(InLoadPath).ToLower();
    const uint8* BufferPtr = Buffer.GetData();

    auto TextureFact = NewObject<UTextureFactory>();
    UTexture2D* Ret = Cast<UTexture2D>(TextureFact->FactoryCreateBinary(
        UTexture2D::StaticClass(), GetTransientPackage(), *TextureName, RF_Transient,
        NULL, *Extension, BufferPtr, BufferPtr + Buffer.Num(), GWarn));

    return Ret;
}
#endif

透過使用 UTextureFactory 的 FactoryCreateBinary 函數,我們實現了這個方法,該函數可讀取前面提到的其他文件類型。

複製:透過 UE 實現複製 UTexture2D

有時候需要複製一個 UTexture2D 出來,再對這個複製出來的圖片進行修改,複製圖片需要使用到引擎自帶的函數 FImageCore::CopyImage,只要設定好兩個圖片的參數,調用這個接口即可。

UTexture2D* CopyTexture2D(UTexture2D* InTexture, UObject* Outer, FName Name, EObjectFlags Flags)
{
    // src texture info, src ImageView
    FTextureMipDataLockGuard InTextureGuard(InTexture);
    uint8* SrcMipData = InTextureGuard.Lock(LOCK_READ_ONLY);        // Texture->GetPlatformData()->Mips[0].BulkData.Lock(InLockFlag)
    const int32 InSizeX = InTexture->GetSizeX();
    const int32 InSizeY = InTexture->GetSizeY();
    const EPixelFormat InFormat = InTexture->GetPixelFormat();
    const FImageView SrcMipImage(
        SrcMipData, InSizeX, InSizeY, 1, GetRawImageFormat(InFormat), InTexture->GetGammaSpace());

    // create dst texture
    UTexture2D* NewTexture = NewObject<UTexture2D>(Outer, Name, Flags);
    NewTexture->SetPlatformData(new FTexturePlatformData());
    NewTexture->GetPlatformData()->SizeX = InSizeX;
    NewTexture->GetPlatformData()->SizeY = InSizeY;
    NewTexture->GetPlatformData()->SetNumSlices(1);
    NewTexture->GetPlatformData()->PixelFormat = InFormat;

    // Allocate first mipmap.
    int32 NumBlocksX = InSizeX / GPixelFormats[InFormat].BlockSizeX;
    int32 NumBlocksY = InSizeY / GPixelFormats[InFormat].BlockSizeY;
    FTexture2DMipMap* Mip = new FTexture2DMipMap();
    Mip->SizeX = InSizeX;
    Mip->SizeY = InSizeY;
    Mip->SizeX = 1;
    NewTexture->GetPlatformData()->Mips.Add(Mip);
    Mip->BulkData.Lock(LOCK_READ_WRITE);
    Mip->BulkData.Realloc((int64)NumBlocksX * NumBlocksY * GPixelFormats[InFormat].BlockBytes);
    Mip->BulkData.Unlock();

    // dst texture ImageView
    uint8* DstMipData = static_cast<uint8*>(NewTexture->GetPlatformData()->Mips[0].BulkData.Lock(LOCK_READ_WRITE));
    const FImageView DstMipImage(
        DstMipData, InSizeX, InSizeY, 1, GetRawImageFormat(InFormat), InTexture->GetGammaSpace());

    // run CopyImage
    FImageCore::CopyImage(SrcMipImage,DstMipImage);

#if WITH_EDITORONLY_DATA
    NewTexture->Source.Init(
        InSizeX, InSizeY, 1, 1,
        FImageCoreUtils::ConvertToTextureSourceFormat(GetRawImageFormat(InFormat)), DstMipData);
#endif

    // cleanup
    NewTexture->GetPlatformData()->Mips[0].BulkData.Unlock();
    NewTexture->UpdateResource();

    return NewTexture;
}

保留:UE 實現將 UTexture2D 保存到文件

主要是使用引擎函式 FImageUtils::SaveImageAutoFormat,處理起來相對簡單,但需要注意處理失敗重試的情況。

void SaveImage(UTexture2D* InImage, const FString& InSavePath)
{
    if (!InImage) return;
    FImage ImageInfo;
    if (FImageUtils::GetTexture2DSourceImage(InImage, ImageInfo))
    {
        FImageUtils::SaveImageAutoFormat(*InSavePath, ImageInfo);
    }
    else
    {
        // if prev save failed
        // use ConvertTextureToStandard to change InImage to Standard format, and try again
        // then revert InImage's origin format
        // this is what FTextureMipDataLockGuard does
        FTextureMipDataLockGuard InImageGuard(InImage);

        uint8* MipData = InImageGuard.Lock(LOCK_READ_ONLY);
        check( MipData != nullptr );

        const FImageView MipImage(
            MipData, InImage->GetSizeX(), InImage->GetSizeY(), 1,
            GetRawImageFormat(InImage->GetPixelFormat()), InImage->GetGammaSpace());

        FImageUtils::SaveImageAutoFormat(*InSavePath, MipImage);
    }
}

保存:UE 實現將 UTexture2D 保存到資產

將保存在記憶體中的 UTexture2D 存入資產 (Asset) 中,並可在資源瀏覽器 (Content Browser) 中查看。

核心功能需要使用上述實現的 CopyTexture2D,我們需要先複製出一張新圖片,然後再呼叫 UPackage::SavePackage 將圖片所在的 Package 保存為資產。

void SaveTextureToAsset(UTexture2D* InTexture)
{
    if (!InTexture) return;

    // open save asset dialog, choose where/which to save
    FSaveAssetDialogConfig SaveAssetDialogConfig;

    SaveAssetDialogConfig.DefaultPath =  FEditorDirectories::Get().GetLastDirectory(ELastDirectory::NEW_ASSET);
    SaveAssetDialogConfig.AssetClassNames.Add(UTexture2D::StaticClass()->GetClassPathName());
    SaveAssetDialogConfig.ExistingAssetPolicy = ESaveAssetDialogExistingAssetPolicy::AllowButWarn;
    SaveAssetDialogConfig.DialogTitleOverride = FAIChatPlusEditor_Constants::FCText::SaveAsAsset;

    const FContentBrowserModule& ContentBrowserModule = FModuleManager::LoadModuleChecked<FContentBrowserModule>("ContentBrowser");
    const FString SaveObjectPath = ContentBrowserModule.Get().CreateModalSaveAssetDialog(SaveAssetDialogConfig);

    if (SaveObjectPath.IsEmpty()) return;

    // init save info
    const FString PackageName = FPackageName::ObjectPathToPackageName(SaveObjectPath);
    const FString PackageFileName = FPackageName::LongPackageNameToFilename(PackageName, FPackageName::GetAssetPackageExtension());
    const FString PackagePath = FPaths::GetPath(PackageFileName);
    const FString TextureName = FPaths::GetBaseFilename(PackageName);

    // create new UPackage to put the new texture in
    UPackage* const NewPackage = CreatePackage(*PackageName);
    NewPackage->FullyLoad();

    // copy texture
    UTexture2D* NewTexture = UAIChatPlus_Util::CopyTexture2D(
        InTexture, NewPackage, FName(TextureName), RF_Public | RF_Standalone | RF_Transactional);

    // Generate the thumbnail
    // if not doing so, the texture will not have thumbnail in content browser
    FObjectThumbnail NewThumbnail;
    ThumbnailTools::RenderThumbnail(
        NewTexture, NewTexture->GetSizeX(), NewTexture->GetSizeY(),
        ThumbnailTools::EThumbnailTextureFlushMode::NeverFlush, NULL,
        &NewThumbnail);
    ThumbnailTools::CacheThumbnail(NewTexture->GetFullName(), &NewThumbnail, NewPackage);

    // setting up new package and new texture
    NewPackage->MarkPackageDirty();
    FAssetRegistryModule::AssetCreated(NewTexture);
    FEditorDirectories::Get().SetLastDirectory(ELastDirectory::NEW_ASSET, FPaths::GetPath(PackageName));

    // save args
    FSavePackageArgs SaveArgs;
    SaveArgs.TopLevelFlags = RF_Public | RF_Standalone;
    SaveArgs.bForceByteSwapping = true;
    SaveArgs.bWarnOfLongFilename = true;

    // save it
    if (!UPackage::SavePackage(NewPackage, NewTexture, *PackageFileName, SaveArgs))
    {
        UE_LOG(AIChatPlusEditor, Error, TEXT("Failed to save Asset: [%s]\n"), *PackageFileName);
    }
}

剪貼板:UE 實現複製圖片 (UTexture2D) 到 Windows 剪貼板 (Clipboard)

Windows相關函數

我們將使用以下 Windows 操作剪貼簿相關的函式:

  • OpenClipboard打開剪貼板,取得剪貼板的處理器。
  • EmptyClipboard清空剪貼板,並將剪貼板的所有權分配給當前的視窗。
  • SetClipboardData設定剪貼簿的資料,圖片的資料是透過這個介面傳送至剪貼簿。
  • CloseClipboard設置好資料後,關閉剪貼板。

剪貼板的圖片格式

(https://learn.microsoft.com/zh-cn/windows/win32/dataxchg/standard-clipboard-formats)文件中描述了可用的剪貼板格式,其中 CF_DIBV5 可用於設置圖片。

CF_DIBV5 要求的格式具体定義 BITMAPV5HEADER 結構我們選擇了以下配置。

BITMAPV5HEADER Header;
Header.bV5CSType        = LCS_sRGB;
Header.bV5Compression   = BI_BITFIELDS;

UTexture2D 設置

我哋係上面揀咗剪貼板圖片嘅顏色空間係 LCS_sRGB,即 sRGB 顏色空間,咁呢,UTexture2D 都需要先設定到對應嘅格式:

bool ConvertTextureToStandard(UTexture2D* InTexture)
{
    if (InTexture->CompressionSettings != TC_VectorDisplacementmap)
    {
        InTexture->CompressionSettings = TC_VectorDisplacementmap;
        IsChanged = true;
    }
    if (InTexture->SRGB != true)
    {
        InTexture->SRGB = true;
        IsChanged = true;
    }
    if (IsChanged)
    {
        InTexture->UpdateResource();
    }
}

ConvertTextureToStandard是負責將UTexture2D轉換為標準格式:TC_VectorDisplacementmap(RGBA8)和SRGB色彩空間的功能。對齊了UTexture2D和Windows剪貼簿的圖片格式後,我們就可以將圖片數據複製到剪貼簿上。

具體代碼

void CopyTexture2DToClipboard(UTexture2D* InTexture)
{
    if (!InTexture) return;

    FTextureMipDataLockGuard InTextureGuard(InTexture);
    // get InTexture info
    uint8* SrcMipData = InTextureGuard.Lock(LOCK_READ_ONLY);
    const int32 InSizeX = InTexture->GetSizeX();
    const int32 InSizeY = InTexture->GetSizeY();
    const EPixelFormat InFormat = InTexture->GetPixelFormat();
    const FImageView SrcMipImage(
        SrcMipData, InSizeX, InSizeY, 1, GetRawImageFormat(InTexture), InTexture->GetGammaSpace());

    // set clipboard Texture info
    const EPixelFormat OutFormat = PF_B8G8R8A8;
    const int32 NumBlocksX = InSizeX / GPixelFormats[OutFormat].BlockSizeX;
    const int32 NumBlocksY = InSizeY / GPixelFormats[OutFormat].BlockSizeY;
    const int64 BufSize = static_cast<int64>(NumBlocksX) * NumBlocksY * GPixelFormats[InFormat].BlockBytes;

    // set header info
    BITMAPV5HEADER Header;
    Header.bV5Size          = sizeof(BITMAPV5HEADER);
    Header.bV5Width         = InSizeX;
    Header.bV5Height        = -InSizeY;
    Header.bV5Planes        = 1;
    Header.bV5BitCount      = 32;
    Header.bV5Compression   = BI_BITFIELDS;
    Header.bV5SizeImage     = BufSize;
    Header.bV5XPelsPerMeter = 0;
    Header.bV5YPelsPerMeter = 0;
    Header.bV5ClrUsed       = 0;
    Header.bV5ClrImportant  = 0;
    Header.bV5RedMask       = 0x00FF0000;
    Header.bV5GreenMask     = 0x0000FF00;
    Header.bV5BlueMask      = 0x000000FF;
    Header.bV5AlphaMask     = 0xFF000000;
    Header.bV5CSType        = LCS_sRGB;
    // Header.bV5Endpoints;    // ignored
    Header.bV5GammaRed      = 0;
    Header.bV5GammaGreen    = 0;
    Header.bV5GammaBlue     = 0;
    Header.bV5Intent        = 0;
    Header.bV5ProfileData   = 0;
    Header.bV5ProfileSize   = 0;
    Header.bV5Reserved      = 0;

    HGLOBAL WinBuf = GlobalAlloc(GMEM_MOVEABLE, sizeof(BITMAPV5HEADER) + BufSize);
    if (WinBuf == NULL)
        return;

    HWND WinHandler = GetActiveWindow();
    if (!OpenClipboard(WinHandler)) {
        GlobalFree(WinBuf);
        return;
    }
    verify(EmptyClipboard());

    // copy InTexture into BGRA8 sRGB Standard Texture
    FTexture2DMipMap* DstMip = new FTexture2DMipMap();
    DstMip->SizeX = InSizeX;
    DstMip->SizeY = InSizeY;
    DstMip->SizeZ = 1;
    DstMip->BulkData.Lock(LOCK_READ_WRITE);
    uint8* DstMipData = static_cast<uint8*>(DstMip->BulkData.Realloc(BufSize));
    const FImageView DstMipImage(
        DstMipData, InSizeX, InSizeY, 1, ERawImageFormat::BGRA8, EGammaSpace::sRGB);

    FImageCore::CopyImage(SrcMipImage,DstMipImage);
    DstMip->BulkData.Unlock();

    // copy Standard Texture data into Clipboard
    void * WinLockedBuf = GlobalLock(WinBuf);
    if (WinLockedBuf) {
        memcpy(WinLockedBuf, &Header, sizeof(BITMAPV5HEADER));
        memcpy((char*)WinLockedBuf + sizeof(BITMAPV5HEADER), DstMipData, BufSize);
    }
    GlobalUnlock(WinLockedBuf);

    if (!SetClipboardData(CF_DIBV5, WinBuf))
    {
        UE_LOG(AIChatPlus_Internal, Fatal, TEXT("SetClipboardData failed with error code %i"), (uint32)GetLastError() );
    }

    // finish, close clipboard
    verify(CloseClipboard());

    delete DstMip;
}

UTexture2D 與 Base64 之間的轉換

這個實現起來比較簡單,直接上程式碼。

#include <Misc/Base64.h>
#include <ImageUtils.h>

UTexture2D* B64ToImage(const FString& B64)
{
    TArray<uint8> Data;
    FBase64::Decode(B64, Data);
    return FImageUtils::ImportBufferAsTexture2D(Data);
}

FString ImageToB64(UTexture2D* InTexture, const int32 InQuality)
{
    FTextureMipDataLockGuard InTextureGuard(InTexture);

    uint8* MipData = InTextureGuard.Lock(LOCK_READ_ONLY);
    check(MipData != nullptr);

    const FImageView InImage(
        MipData, InTexture->GetSizeX(), InTexture->GetSizeY(), 1,
        GetRawImageFormat(InTexture->GetPixelFormat()), InTexture->GetGammaSpace());

    TArray64<uint8> Buffer;
    FString Ret;
    if (FImageUtils::CompressImage(Buffer, TEXT("png"), InImage, InQuality))
    {
        Ret = FBase64::Encode(Buffer.GetData(), Buffer.Num());
    }
    return Ret;
}

Original: https://wiki.disenone.site/tc

This post is protected by CC BY-NC-SA 4.0 agreement, should be reproduced with attribution.

這篇文章是由ChatGPT翻譯的,請在反饋指出所有被忽略的地方。