UE4 – 有关 UTexture2DArray 的 Runtime 支持

成果

引擎版本 4.26.2
实现一个 UTexture2DArray 包裹器。
支持在 Runtime 的纹理数组创建与修改。
支持纹理数组 MipMap (官方实现只有第0层Mip)。

效果图

此纹理数组共5个元素,5个 Mip 层级。
其中,横向为元素,纵向为 Mip 层级。

代码

Texture2DArrayWrapper.h

#pragma once

#include "CoreMinimal.h"
#include "UObject/Object.h"
#include "Texture2DArrayWrapper.generated.h"

class UTexture2D;
class UTexture2DArray;

UCLASS(BlueprintType)
class REDCRAFT_API UTexture2DArrayWrapper : public UObject
{
    GENERATED_BODY()

public:

    UTexture2DArrayWrapper(const FObjectInitializer& ObjectInitializer);

public:

    UPROPERTY(BlueprintReadWrite, EditAnywhere)
    TArray<UTexture2D*> SourceTextures;

    UFUNCTION(BlueprintCallable, BlueprintPure)
    UTexture2DArray* GetTextureArray() const;

    UFUNCTION(BlueprintCallable)
    void UpdateTextureArray(int32 TemplateIndex = 0);

    UFUNCTION(BlueprintCallable, BlueprintPure)
    const TArray<UTexture2D*>& GetTextures() const;

private:

    UPROPERTY()
    UTexture2DArray* Texture2DArray;

    UPROPERTY()
    TArray<UTexture2D*> Textures;

};

Texture2DArrayWrapper.cpp

#include "Texture2DArrayWrapper.h"

#include "Engine/Texture2D.h"
#include "Engine/Texture2DArray.h"

UTexture2DArrayWrapper::UTexture2DArrayWrapper(const FObjectInitializer& ObjectInitializer)
{
    Texture2DArray = CreateDefaultSubobject<UTexture2DArray>(TEXT("Texture2DArray"));
    Texture2DArray->UpdateResource();
}

UTexture2DArray* UTexture2DArrayWrapper::GetTextureArray() const
{
    return Texture2DArray;
}

void UTexture2DArrayWrapper::UpdateTextureArray(int32 TemplateIndex)
{
    // 重置有效纹理表列
    Textures.Reset();

    // 获取纹理数组的平台数据
    FTexturePlatformData*& PlatformData = *Texture2DArray->GetRunningPlatformData();

    // 如果纹理源为空 删除平台数据并更新渲染资源
    if (SourceTextures.Num() == 0)
    {
        if (PlatformData != nullptr)
        {
            delete PlatformData;
            PlatformData = nullptr;
        }

        Texture2DArray->UpdateResource();
        return;
    }

    // 如果模板索引无效 默认使用第0个纹理做模板
    TemplateIndex = SourceTextures.IsValidIndex(TemplateIndex) ? TemplateIndex : 0;

    // 获取模板纹理
    UTexture2D* TemplateTexture = SourceTextures[TemplateIndex];

    // 得到模板纹理的平台数据
    const FTexturePlatformData* TemplatePlatformData = *TemplateTexture->GetRunningPlatformData();

    // 取得模板纹理的长宽
    const int32 SizeX = TemplatePlatformData->SizeX;
    const int32 SizeY = TemplatePlatformData->SizeY;

    // 取得模板纹理的像素格式
    const EPixelFormat PixelFormat = TemplatePlatformData->PixelFormat;

    // 筛选有效的纹理
    for (UTexture2D* Texture : SourceTextures)
    {
        const FTexturePlatformData* SourcePlatformData = *Texture->GetRunningPlatformData();

        bool bIsInvalid = false;

        // 只有长宽与纹理格式都符号模板才有效
        bIsInvalid |= SourcePlatformData->SizeX != SizeX;
        bIsInvalid |= SourcePlatformData->SizeY != SizeY;
        bIsInvalid |= SourcePlatformData->PixelFormat != PixelFormat;

        if (!bIsInvalid)
        {
            Textures.AddUnique(Texture);
        }
    }

    // 获取纹理数组元素数与Mip层数
    const int32 NumSlices = Textures.Num();
    const int32 NumMips = TemplatePlatformData->Mips.Num();

    // 设置一般属性
    Texture2DArray->AddressX = TemplateTexture->AddressX;
    Texture2DArray->AddressY = TemplateTexture->AddressY;
    Texture2DArray->AddressZ = TextureAddress::TA_Wrap;
    Texture2DArray->SRGB = TemplateTexture->SRGB;
    Texture2DArray->Filter = TemplateTexture->Filter;
    Texture2DArray->MipLoadOptions = TemplateTexture->MipLoadOptions;

    // 如果平台数据不存在则创建
    if (PlatformData == nullptr)
    {
        PlatformData = new FTexturePlatformData();
    }

    // 设置纹理长宽元素数与像素格式
    PlatformData->SizeX = SizeX;
    PlatformData->SizeY = SizeY;
    PlatformData->PackedData = NumSlices;
    PlatformData->PixelFormat = PixelFormat;

    // 删除多余的Mip层
    if (PlatformData->Mips.Num() > NumMips)
    {
        PlatformData->Mips.RemoveAt(NumMips, PlatformData->Mips.Num() - NumMips);
    }

    // 遍历每个Mip层
    for (int32 MipIndex = 0; MipIndex < NumMips; ++MipIndex)
    {
        // 获取模板上对应的Mip层
        const FTexture2DMipMap& TemplateMip = TemplatePlatformData->Mips[MipIndex];

        // 获取此Mip层的元素字节数与总字节数
        const int32 ElementBytesCount = TemplateMip.BulkData.GetElementCount();
        const int32 MipBytesCount = ElementBytesCount * NumSlices;

        // 确保当前Mip层对象存在
        if (!PlatformData->Mips.IsValidIndex(MipIndex))
        {
            check(PlatformData->Mips.Num() == MipIndex);

            PlatformData->Mips.Add(new FTexture2DMipMap());
        }

        // 取得纹理数组中的Mip层对象
        FTexture2DMipMap& Mip = PlatformData->Mips[MipIndex];

        // 设置当前Mip层长宽及元素数
        Mip.SizeX = TemplateMip.SizeX;
        Mip.SizeY = TemplateMip.SizeY;
        Mip.SizeZ = NumSlices;

        // 以读写方式锁定当前Mip层
        Mip.BulkData.Lock(LOCK_READ_WRITE);

        // 重置Mip层到所需大小
        void* BulkData = (uint8*)Mip.BulkData.Realloc(MipBytesCount);

        // 遍历每个纹理元素
        for (int32 SliceIndex = 0; SliceIndex < NumSlices; ++SliceIndex)
        {
            // 获取源纹理平台数据
            FTexturePlatformData*& SourcePlatformData = *Textures[SliceIndex]->GetRunningPlatformData();

            // 获取源纹理对应Mip层
            FTexture2DMipMap& SourceMip = SourcePlatformData->Mips[MipIndex];

            // 以只读方式锁定源纹理Mip层
            const void* SourceBulkData = SourceMip.BulkData.Lock(LOCK_READ_ONLY);

            // 从源纹理复制数据到纹理数组中
            FMemory::Memcpy((uint8*)BulkData + ElementBytesCount * SliceIndex, SourceBulkData, ElementBytesCount);

            // 解锁源纹理Mip层
            SourceMip.BulkData.Unlock();
        }

        // 解锁当前Mip层
        Mip.BulkData.Unlock();
    }

    // 更新纹理数组渲染资源
    Texture2DArray->UpdateResource();
}

const TArray<UTexture2D*>& UTexture2DArrayWrapper::GetTextures() const
{
    return Textures;
}

原理分析

UE 中纹理数据使用 FTexturePlatformData 结构储存,可通过 UTexture::GetRunningPlatformData 在纹理对象中得到此结构,所以动态生成 Texture2DArray 的重点就是将 UTexture2D 的 PlatformData 正确复制并组合到 UTexture2DArray 的 PlatformData 中。

FTexturePlatformData [Texture.h]

用于在运行时储存特定平台的纹理数据。

int32 SizeX:储存纹理的宽度。
int32 SizeY:储存纹理的高度。
uint32 PackedData:其中最高位二进制表示是否是立方体贴图,次高位二进制表示是否包含部分平台需要的额外数据,其余位表示切片数量,在 Texture2DArray 中的含义即为数组大小。
EPixelFormat PixelFormat:像素的编码格式。
TIndirectArray\ Mips:Mip层,可类比 Mesh 的 LOD,真正存放像素数据的地方。

其余部分在这里用不到,省略。

FTexture2DMipMap [Texture.h]

表示纹理的一个 Mip 层。

int32 SizeX:当前 Mip 层的高度。
int32 SizeY:当前 Mip 层的宽度。
int32 SizeZ:当前 Mip 层的切片数量,即数组大小。
FByteBulkData BulkData:像素数据的实际存放位置。

SizeX 、 SizeY 和 SizeZ 必须与 BulkData 对应,且为对应像素格式的块 XYZ 的整数倍。
BulkData 其大小必须为对应像素格式的块字节数的整数倍。

GPixelFormats [RHI.h]

一个全局数组,储存了像素格式相关块信息。
可以看到其中 BlockSizeZ 的大小都是 1 所以我们不用担心 Mip 层的 SizeZ 不是其整数倍。

纹理构建基本流程

首先我们拿到一个 UTexture2D 的数组,作为 UTexture2DArray 的元素来源。
因为纹理数组要求其元素必须长宽相同、像素格式相同,所以我们从源纹理中选择一个纹理为模板。
对比模板与每一个 UTexture2D 的长宽像素格式,将与模板匹配的纹理选择出来。
然后使用 NewObject 创建一个 UTexture2DArray 。
根据模板设置纹理数组的 AddressX 、AddressY 等基本设置。
设置纹理数组的 PlatformData 成员 SizeX 、 SizeY 、 PackedData 和 PixelFormat 。
逐层遍历 Mip 从第0层 Mip 开始创建。
每一层 Mip 的 SizeX 与 SizeY 可以直接从模板纹理的对应层得到, SizeZ 与元素数相同即可。
锁定 Mip 层的 BulkData ,重置其大小到所需大小。
这里我们可以认为每个纹理的 BulkData 大小都与模板纹理相同,所以模板纹理的 BulkData 大小就是纹理数组的元素大小,其乘以元素数就是对应 Mip 层的 BulkData 大小。
逐个遍历源纹理,拿到对应 Mip 层的 BulkData ,只读方式锁定,并复制数据到纹理数组,其中复制的目标位置指针可以通过纹理数组的 BulkData 数据头指针加上每个元素的大小乘以元素索引得到。
复制完所有纹理,开始下一层 Mip 的数据复制。
最后不要忘了 UTexture::UpdateResource 将数据提交到渲染系统。

看文字描述不如直接看代码。

发表评论