Unreal – Runtime support for UTexture2DArray

Result

Engine version 4.26.2
Implement a UTexture2DArray wrapper.
Supports texture array creation and modification at Runtime.
Supports texture array MipMap (the official implementation only has layer 0 Mip).

Effect map

This texture array has a total of 5 elements and 5 Mip levels.
Among them, the horizontal direction is the element, and the vertical direction is the Mip level.

代码

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;
}

Principle analysis

Texture data in UE is stored using the FTexturePlatformData structure. This structure can be obtained in the texture object through UTexture::GetRunningPlatformData. Therefore, the focus of dynamically generating Texture2DArray is to correctly copy and combine the PlatformData of UTexture2D into the PlatformData of UTexture2DArray.

FTexturePlatformData [Texture.h]

Used to store platform-specific texture data at runtime.

int32 SizeX: The width of the stored texture.
int32 SizeY: The height of the stored texture.
uint32 PackedData: The highest binary bit indicates whether it is a cube map, the second highest binary bit indicates whether it contains additional data required by some platforms, and the remaining bits indicate the number of slices. The meaning in Texture2DArray is the array size.
EPixelFormat PixelFormat: The encoding format of pixels.
TIndirectArray\ Mips: Mip layer, which can be compared to the LOD of Mesh, where the pixel data is actually stored.

The remaining parts are not used here and are omitted.

FTexture2DMipMap [Texture.h]

Represents a mip layer of texture.

int32 SizeX: The height of the current Mip layer.
int32 SizeY: The width of the current Mip layer.
int32 SizeZ: The number of slices in the current Mip layer, that is, the array size.
FByteBulkData BulkData: The actual storage location of pixel data.

SizeX, SizeY and SizeZ must correspond to BulkData and be an integer multiple of block XYZ in the corresponding pixel format.
BulkData The size must be an integer multiple of the number of bytes in the block of the corresponding pixel format.

GPixelFormats [RHI.h]

A global array that stores pixel format-related block information.
You can see that the size of BlockSizeZ is all 1, so we don't have to worry that the SizeZ of the Mip layer is not an integer multiple of it.

Basic texture construction process

First we get an array of UTexture2D as the element source of UTexture2DArray.
Because the texture array requires that its elements must have the same length, width, and pixel format, we select a texture from the source texture as a template.
Compare the length and width pixel format of the template with each UTexture2D, and select the texture that matches the template.
Then use NewObject to create a UTexture2DArray.
Set the basic settings such as AddressX and AddressY of the texture array according to the template.
Set the PlatformData members SizeX, SizeY, PackedData, and PixelFormat of the texture array.
Traverse Mip layer by layer and start creating Mip from layer 0.
The SizeX and SizeY of each layer of Mip can be obtained directly from the corresponding layer of the template texture, and SizeZ can be the same as the number of elements.
Lock the mip layer's BulkData and reset its size to the desired size.
Here we can think that the BulkData size of each texture is the same as the template texture, so the BulkData size of the template texture is the element size of the texture array, multiplied by the number of elements is the BulkData size of the corresponding Mip layer.
Traverse the source texture one by one, get the BulkData of the corresponding Mip layer, lock it in read-only mode, and copy the data to the texture array. The copied target position pointer can be passed through the BulkData data header pointer of the texture array plus the size of each element multiplied by the element. Index is obtained.
After copying all textures, start copying the data of the next layer of Mip.
Finally don't forget UTexture::UpdateResource to submit data to the rendering system.

It is better to read the code directly than to read the text description.

Post Reply