UE4 从网络端下载图片并且缓存到本地
从网络端下载一个图片,并且在UI上显示出来,然后再缓存到本地文件中,方便下次使用。
下载图片
UE4官方提供了一个异步任务类,专门处理下载图片的需求。
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FDownloadImageDelegate, UTexture2DDynamic*, Texture);
UCLASS()
class UMG_API UAsyncTaskDownloadImage : public UBlueprintAsyncActionBase
{
GENERATED_UCLASS_BODY()
public:
UFUNCTION(BlueprintCallable, meta=( BlueprintInternalUseOnly="true" ))
static UAsyncTaskDownloadImage* DownloadImage(FString URL);
public:
UPROPERTY(BlueprintAssignable)
FDownloadImageDelegate OnSuccess;
UPROPERTY(BlueprintAssignable)
FDownloadImageDelegate OnFail;
public:
void Start(FString URL);
private:
/** Handles image requests coming from the web */
void HandleImageRequest(FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded);
};
void UAsyncTaskDownloadImage::HandleImageRequest(FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded)
{
#if !UE_SERVER
RemoveFromRoot();
if ( bSucceeded && HttpResponse.IsValid() && HttpResponse->GetContentLength() > 0 )
{
IImageWrapperModule& ImageWrapperModule = FModuleManager::LoadModuleChecked<IImageWrapperModule>(FName("ImageWrapper"));
TSharedPtr<IImageWrapper> ImageWrappers[3] =
{
ImageWrapperModule.CreateImageWrapper(EImageFormat::PNG),
ImageWrapperModule.CreateImageWrapper(EImageFormat::JPEG),
ImageWrapperModule.CreateImageWrapper(EImageFormat::BMP),
};
for ( auto ImageWrapper : ImageWrappers )
{
if ( ImageWrapper.IsValid() && ImageWrapper->SetCompressed(HttpResponse->GetContent().GetData(), HttpResponse->GetContentLength()) )
{
TArray64<uint8>* RawData = new TArray64<uint8>();
const ERGBFormat InFormat = ERGBFormat::BGRA;
if ( ImageWrapper->GetRaw(InFormat, 8, *RawData) )
{
if ( UTexture2DDynamic* Texture = UTexture2DDynamic::Create(ImageWrapper->GetWidth(), ImageWrapper->GetHeight()) )
{
Texture->SRGB = true;
Texture->UpdateResource();
FTexture2DDynamicResource* TextureResource = static_cast<FTexture2DDynamicResource*>(Texture->Resource);
if (TextureResource)
{
ENQUEUE_RENDER_COMMAND(FWriteRawDataToTexture)(
[TextureResource, RawData](FRHICommandListImmediate& RHICmdList)
{
WriteRawToTexture_RenderThread(TextureResource, RawData);
});
}
else
{
delete RawData;
}
OnSuccess.Broadcast(Texture);
return;
}
}
}
}
}
OnFail.Broadcast(nullptr);
#endif
}
static void WriteRawToTexture_RenderThread(FTexture2DDynamicResource* TextureResource, TArray64<uint8>* RawData, bool bUseSRGB = true)
{
check(IsInRenderingThread());
if (TextureResource)
{
FRHITexture2D* TextureRHI = TextureResource->GetTexture2DRHI();
int32 Width = TextureRHI->GetSizeX();
int32 Height = TextureRHI->GetSizeY();
uint32 DestStride = 0;
uint8* DestData = reinterpret_cast<uint8*>(RHILockTexture2D(TextureRHI, 0, RLM_WriteOnly, DestStride, false, false));
for (int32 y = 0; y < Height; y++)
{
uint8* DestPtr = &DestData[((int64)Height - 1 - y) * DestStride];
const FColor* SrcPtr = &((FColor*)(RawData->GetData()))[((int64)Height - 1 - y) * Width];
for (int32 x = 0; x < Width; x++)
{
*DestPtr++ = SrcPtr->B;
*DestPtr++ = SrcPtr->G;
*DestPtr++ = SrcPtr->R;
*DestPtr++ = SrcPtr->A;
SrcPtr++;
}
}
RHIUnlockTexture2D(TextureRHI, 0, false, false);
}
delete RawData;
}
根据以上内容可以看出,利用Http协议从网络端获取图片的数据后,应对PNG,JPEG,BMP三种图片格式来进行读取,如果读取成功,则把数据写入到RenderThread线程中,渲染为UTexture2DDynamic内部的Resource,实现UTexture2DDynamic对象的生成,用于直接从OnSuccess函数中返回,这里初步实现了下载功能。
保存图片
到保存图片这步时候,我先实现了一个方案,基于UTexture2D的本地存储。
bool ImageManager::SaveTextureToTempDir(const FString& ImageName, UTexture2D* Texture)
{
if (!Texture || ImageName.IsEmpty())
return false;
//FTexture2DMipMap& MipMap = Texture->PlatformData->Mips[0];
FTexturePlatformData** pPlatformData = Texture->GetRunningPlatformData();
if (!pPlatformData)
{
return false;
}
FTexture2DMipMap& MipMap = (*pPlatformData)->Mips[0];
unsigned char* Data = (unsigned char*)MipMap.BulkData.Lock(LOCK_READ_WRITE);
int32 TexSizeX = MipMap.SizeX;
int32 TexSizeY = MipMap.SizeY;
TArray<FColor> nColors;
nColors.SetNum(TexSizeX * TexSizeY);
FMemory::Memcpy(nColors.GetData(), Data, (int32)(sizeof(FColor)) * TexSizeX * TexSizeY);
MipMap.BulkData.Unlock();
TArray<uint8> ImgData;
FImageUtils::CompressImageArray(TexSizeX, TexSizeY, nColors, ImgData);
FString FilePath = FPaths::ProjectSavedDir() + gGameConfig.GetImageSaveDir();
if (!DirectoryExists(*FilePath))
{
MakeDirectory(*FilePath, true);
}
FString SaveFilePathName = FilePath / ImageName;
return FFileHelper::SaveArrayToFile(ImgData, *SaveFilePathName);
}
在我想去保存UTexture2DDynamic类型的图片时候,就发生了很多问题,首先其内部信息存储结构完全不同,在努力了一阵之后发现依然无法实现,这时候我重新去找了官方的方案,查到了另外一个专门用于导出Texture的官方库类UImageWriteBlueprintLibrary。
/**
* Function library containing utility methods for writing images on a global async queue
*/
UCLASS()
class UImageWriteBlueprintLibrary : public UBlueprintFunctionLibrary
{
public:
GENERATED_BODY()
static bool IMAGEWRITEQUEUE_API ResolvePixelData(UTexture* Texture, const FOnPixelsReady& OnPixelsReady);
/**
* Export the specified texture to disk
*
* @param Texture The texture or render target to export
* @param Filename The filename on disk to save as
* @param Options Parameters defining the various export options
*/
UFUNCTION(BlueprintCallable, Category=Texture, meta=(ScriptMethod))
static IMAGEWRITEQUEUE_API void ExportToDisk(UTexture* Texture, const FString& Filename, const FImageWriteOptions& Options);
};
但是该类型,依然是只支持UTextureRenderTarget2D和UTexture2D两种类型的导出,所以经过调试研究,我对ResolvePixelData函数进行了一些改造,使它支持了UTexture2DDynamic类型的导出。
bool UImageWriteBlueprintLibrary::ResolvePixelData(UTexture* InTexture, const FOnPixelsReady& OnPixelsReady)
{
if (!InTexture)
{
FFrame::KismetExecutionMessage(TEXT("Invalid texture supplied."), ELogVerbosity::Error);
return false;
}
EPixelFormat Format = PF_Unknown;
if (UTextureRenderTarget2D* RT2D = Cast<UTextureRenderTarget2D>(InTexture))
{
Format = RT2D->GetFormat();
}
else if (UTexture2D* Texture2D = Cast<UTexture2D>(InTexture))
{
Format = Texture2D->GetPixelFormat();
}
//新增内容 start
else if(UTexture2DDynamic* TextureDynamic2D = Cast<UTexture2DDynamic>(InTexture))
{
Format = InTexture->Resource->TextureRHI->GetTexture2D();
}
//新增内容 end
switch (Format)
{
default:
FFrame::KismetExecutionMessage(TEXT("Unsupported texture format."), ELogVerbosity::Error);
return false;
case PF_FloatRGBA:
case PF_A32B32G32R32F:
case PF_R8G8B8A8:
case PF_B8G8R8A8:
break;
}
...//省略部分内容
}
至此其实已经实现了我需要的全部功能,从网络端下载图片,显示到UI,然后再存储到本地。然而其中存在一个关键性的问题,我们对于引擎的修改尽量能够导出到项目中,这样方便与他人同步,也方便在更新引擎时候不需要重复修改这部分内容,所以我对上边的方案进行了重新设计。
首先因为需要UTexture2D的类型来在内存中做一个缓存,方便不同位置使用该图片时候不用重复下载或者加载,所以我们还是需要将内存中的图片转为UTexture2D类型。所以我对最初下载图片的类进行了改造,并且放在了项目下。
void UMyAsyncTaskDownloadImage::HandleImageRequest(FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded)
{
#if !UE_SERVER
RemoveFromRoot();
if ( bSucceeded && HttpResponse.IsValid() && HttpResponse->GetContentLength() > 0 )
{
IImageWrapperModule& ImageWrapperModule = FModuleManager::LoadModuleChecked<IImageWrapperModule>(FName("ImageWrapper"));
TSharedPtr<IImageWrapper> ImageWrappers[3] =
{
ImageWrapperModule.CreateImageWrapper(EImageFormat::PNG),
ImageWrapperModule.CreateImageWrapper(EImageFormat::JPEG),
ImageWrapperModule.CreateImageWrapper(EImageFormat::BMP),
};
for ( auto ImageWrapper : ImageWrappers )
{
if ( ImageWrapper.IsValid() && ImageWrapper->SetCompressed(HttpResponse->GetContent().GetData(), HttpResponse->GetContentLength()) )
{
TArray64<uint8>* RawData = new TArray64<uint8>();
const ERGBFormat InFormat = ERGBFormat::BGRA;
if ( ImageWrapper->GetRaw(InFormat, 8, *RawData) )
{
UTexture2D* Texture = gImageManager.CreateTexture2D(*RawData, ImageWrapper->GetWidth(), ImageWrapper->GetHeight());
if (Texture)
{
OnSuccess.Broadcast(Texture);
}
delete RawData;
return;
}
}
}
}
OnFail.Broadcast(nullptr);
#endif
}
//将TArray64<uint8> PixelData 的图片信息转化为UTexture2D的对象
UTexture2D * ImageManager::CreateTexture2D(const TArray64<uint8>& PixelData, int32 InSizeX, int32 InSizeY, EPixelFormat InFormat, FName BaseName)
{
UTexture2D* NewTexture = nullptr;
if (InSizeX > 0 && InSizeY > 0 &&
(InSizeX % GPixelFormats[InFormat].BlockSizeX) == 0 &&
(InSizeY % GPixelFormats[InFormat].BlockSizeY) == 0)
{
NewTexture = NewObject<UTexture2D>(
GetTransientPackage(),
BaseName,
RF_Transient
);
NewTexture->PlatformData = new FTexturePlatformData();
NewTexture->PlatformData->SizeX = InSizeX;
NewTexture->PlatformData->SizeY = InSizeY;
NewTexture->PlatformData->PixelFormat = InFormat;
NewTexture->SRGB = true;
//NewTexture->CompressionSettings = TC_HDR;
// Allocate first mipmap.
int32 NumBlocksX = InSizeX / GPixelFormats[InFormat].BlockSizeX;
int32 NumBlocksY = InSizeY / GPixelFormats[InFormat].BlockSizeY;
FTexture2DMipMap* Mip = new FTexture2DMipMap();
NewTexture->PlatformData->Mips.Add(Mip);
Mip->SizeX = InSizeX;
Mip->SizeY = InSizeY;
Mip->BulkData.Lock(LOCK_READ_WRITE);
void* TextureData = Mip->BulkData.Realloc(NumBlocksX * NumBlocksY * GPixelFormats[InFormat].BlockBytes);
FMemory::Memcpy(TextureData, PixelData.GetData(), PixelData.Num());
Mip->BulkData.Unlock();
NewTexture->UpdateResource();
}
else
{
UE_LOG(LogImageManager, Warning, TEXT("Invalid parameters specified for ImageManager::CreateTexture2D()"));
}
return NewTexture;
}
然后在保存的时候就可以很方便了调用我最初写的 ImageManager::SaveTextureToTempDir(UTexture* InTexture, const FOnPixelsReady& OnPixelsReady) 函数,这样就算是完整的实现了所有的需求。
另外官方的
UImageWriteBlueprintLibrary::ExportToDisk(UTexture* Texture, const FString& Filename, const FImageWriteOptions& Options)
函数在我们需要保存不同类型和不同质量图片时候也可以很好的用到,其中参数如下:
USTRUCT(BlueprintType)
struct FImageWriteOptions
{
GENERATED_BODY()
FImageWriteOptions()
: Format(EDesiredImageFormat::EXR)
, bOverwriteFile(true)
, bAsync(true)
{
CompressionQuality = 0;
}
/** The desired output image format to write to disk */
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="Image")
EDesiredImageFormat Format;
/** A callback to invoke when the image has been written, or there was an error */
UPROPERTY(BlueprintReadWrite, Category="Image")
FOnImageWriteComplete OnComplete;
/** An image format specific compression setting. Either 0 (Default) or 1 (Uncompressed) for EXRs, or a value between 0 and 100. */
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="Image")
int32 CompressionQuality;
/** Whether to overwrite the image if it already exists */
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="Image")
bool bOverwriteFile;
/** Whether to perform the writing asynchronously, or to block the game thread until it is complete */
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="Image")
bool bAsync;
/** A native completion callback that will be called in addition to the dynamic one above. */
TFunction<void(bool)> NativeOnComplete;
};
可以很直观的看到在CompressionQuality参数上,明确指出了可以保存从1%-100%质量图片,方便我们存储一些临时的图片作为缓存机制。