你的浏览器不支持canvas

让我们携手共创奇迹!

UTexture

Date: Author: LBD

本文章采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可。转载请注明来自WindCrazy

UE4的Texture生成和原理浅分析

Texture的读写生成

读取:UE 实现读取本地系统图片为 UTexture2D

通用方法

本方法在 编辑器 和 GamePlay 模式下都可行,支持的图片文件格式有 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(//该函数使用了GEditor
        UTexture2D::StaticClass(), GetTransientPackage(), *TextureName, RF_Transient,
        NULL, *Extension, BufferPtr, BufferPtr + Buffer.Num(), GWarn));

    return Ret;
}
#endif

实现是使用了 UTextureFactoryFactoryCreateBinary 函数,这个函数能够读取前面提到的额外文件类型。

复制:UE 实现复制 UTexture2D

有的时候需要复制一个 UTexture2D 出来,再对这个复制出来的图片进行修改,复制图片需要使用到引擎自带的函数 FImageCore::CopyImage,只要设置好两个图片的参数,调用这个接口即可

UTexture2D* CopyTexture2D(UTexture2D* InTexture, UObject* Outer, FName Name, EObjectFlags Flags)
{
    // src texture info, src ImageView
    FTextureMipDataLockGuard InTextureGuard(InTexture);//参考D:\UE_5.4\Engine\Source\Runtime\Core\Private\AutoRTFM\LockGuard.h可实现此效果
    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->SizeZ = 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
    {
        // D:\UE_5.4\Engine\Source\Runtime\Core\Private\AutoRTFM\LockGuard.h
        //相当于void* MipData = InImage->GetPlatformData()->Mips[0].BulkData.Lock(LOCK_READ_ONLY);
        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());

        //Save an image. Output format will be chosen automatically based on the Image pixel format eg. EXR for float, PNG for BGRA8 extension will be added to file name
        FImageUtils::SaveImageAutoFormat(*InSavePath, MipImage);
    }
}

保存:UE 实现保存 UTexture2D 到 Asset

保存内存中的 UTexture2D 到 Asset 中,并可以在资源浏览器 (Content Browser) 中查看。

核心函数需要用到上面实现的 CopyTexture2D,我们需要先复制出来一个新的图片,然后再调用 UPackage::SavePackage 把图片所在的 Package 保存成 Asset。

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);
    //和上面相呼应SaveAssetDialogConfig.DefaultPath =  FEditorDirectories::Get().GetLastDirectory(ELastDirectory::NEW_ASSET);
    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的SDK但我还没试过这个方法

Windows 相关函数

我们会用到以下 Windows 操作剪贴板的相关函数:

  • OpenClipboard:打开剪贴板,获取剪贴板的 Handler 。
  • EmptyClipboard:清空剪贴板,并把剪贴板的所有权分配给当前的窗口。
  • SetClipboardData:设置剪贴板的数据,图片的数据是通过这个接口发送给剪贴板。
  • CloseClipboard:设置好数据之后,关闭剪贴板。

剪贴板的图片格式

标准剪贴板格式 里面介绍了可用的剪贴板格式,其中 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;
}

UTexture的简介

Texture的FTextureSouce

FTextureSouce存储了纹理的源数据(Color等等),这所谓纹理源数据,可以理解为一份外部导入,不可破坏的数据。或者说是编辑器数据.

//D:\UE_5.4\Engine\Source\Runtime\Engine\Classes\Engine\Texture.h
/*--------------------------------------------------------------------------
		Editor only properties used to build the runtime texture data.
	--------------------------------------------------------------------------*/

#if WITH_EDITORONLY_DATA
	/* Dynamic textures will have ! Source.IsValid() ;
	Also in UEFN , Textures from the cooked-only texture library.  Always check Source.IsValid before using Source. */
	UPROPERTY()
	FTextureSource Source;
#endif

可以看出纹理源数据只存在编辑时,最终Shipping打包出的项目是没这个东西。而且也可以在上面文章中 (复制:UE 实现复制 UTexture2D) 可以看到使用Source时使用了WITH_EDITORONLY_DATA

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

但是纹理源数据是为了解决什么问题的?从注释上看,是为了创建运行时的纹理数据 (给材质Shader等情况使用的纹理数据). 假设不存在FTextureSource, 并且有个情况,我们需要调节纹理的对比度和亮度度等属性,第一次我们调亮度为了0.9,第二次我调亮度回到0.8. 第一次调节,假设原来为FColor(100, 100, 100), 则第一次调节后我们变为了FColor(90, 90, 90), 第二次变为多少?不知道,因为我们原始值已经变了,但是第二次调节亮度为0.8是相对于原始的,没了原始数据我们调节得到的颜色值根本无法知道,因此FTextureSource的作用是作为不可破坏的原始数据存在,Mip的生成都是遵从原始数据来的。Source不存在Mip或者说存在相当于第0级的Mip.

Texture2D的FTexturePlatformData

FTexturePlatformData确切来说就是运行时数据,后面UE的RenderThreadFTexture(对应于DX的SRV)就是靠FTexturePlatformData的各级Mip颜色数据(TIndirectArray<struct FTexture2DMipMap> Mips;)来填充创建的。

/**
 * Platform-specific data used by the texture resource at runtime.
 */
USTRUCT()
struct FTexturePlatformData

Texture的PrivateResource

PrivateResource就是我们在RenderThread使用的纹理资源,GameThreadUTexture2DRenderThread的代表。 如果我们发现材质、UI等,需要用到SRV的情况下纹理上不正常甚至纹理相关的崩溃, PrivateResource是最优先Debug的。

//Texture.h
private:
	/** The texture's resource, can be NULL */
	class FTextureResource*	PrivateResource;

Texture2D的UpdateResource

上面说到的FTextureSouceFTexturePlatformDataFTextureResource三个概念。Texture2D给来一个接口把三个概念串联出来。

编辑器生成Texture2D

也就是纹理生成,我们只要建立FTextureSource数据,然后UpdateResource就可以得到FTexturePlatformDataFTextureResource,也就是说在编辑器生成纹理资源不需要手动填充FTexturePlatformData,而且哪怕手动填充FTexturePlatformData也会因为调用UpdateResource(手动在编辑器调节亮度,对比度, Mip生成设定等都会触发UpdateResource), 导致旧的FPlatfromData被新生成的FPlatfromData数据覆盖掉。

运行时使用Texture2D

使用的Texture2D是打包的游戏在运行时创建的(不打算保存为编辑器文件),并非DevelopmentEditor或者DebugEditor这些模式下,也就是Texture2D不存在FTextureSource,怎么办? 还是得看上面的UpdateResource, 渲染资源FTextureResource是由FPlatfromData生成的,此时我们直接手动填充FTexturePlatformData的各级Mip数据,然后UpdateResource就行了。顺便说下,UTexture2D已经提供来一个创建给完全运行时使用纹理的接口(只有一个Mip等级),但是手动填充FTexturePlatformData数据后,还得手动UpdateResource

参考链接

1,(UE4 4.27)UE4的Texture生成和原理浅分析

2,UE 实现各种图片 (UTexture2D) 操作 (读取、保存、复制、剪贴板…)


对于本文内容有问题或建议的小伙伴,欢迎提出问题