尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

UE5/UE4动态获取资源缩略图并导出PNG/Base64完整实践

UE5/UE4动态获取资源缩略图并导出PNG/Base64完整实践 前阵子给项目组写资产管理工具需要在编辑器里遍历几百个StaticMesh把资源缩略图统一导出成PNG同时还要把每张图的Base64和宽高信息返回给Web接口。最后整理出这套用UE动态获取资源缩略图、下载到本地或返回图片信息的完整链路。刚开始想得很简单UE不是自带ThumbnailTools嘛直接调用不就行了。真正上手才发现光是选API就能绕晕——不同接口拿到的缩略图来源不一样有从缓存里读的、有现场生成的、有依赖Slate渲染的转成最终能落盘的PNG还牵扯到BGRA通道顺序和纹理格式。这篇文章不搞虚的直接把能跑通的完整方案写出来适合正在做编辑器工具、资产管理、批量导出、Web接口对接的UE开发者参考。其中会涉及ThumbnailTools、FObjectThumbnail、AssetThumbnail/FAssetThumbnailPool、FImageUtils以及Base64输出这几条核心链路。新手照着抄也能用老手可以直接跳到踩坑那节有几个坑不写下来可惜了。1. 先分清“编辑器缩略图”和“运行时预览图”别选错API1.1 编辑器缩略图与运行时预览图的本质区别使用场景决定API我先说结论UE编辑器里看到的内容浏览器缩略图和游戏运行时真正渲染出来的“预览图”是两套完全不同的东西。编辑器里的缩略图本质上是引擎在资产保存或导入时调用资源对应ThumbnailRenderer生成的一张位图存放在工程Saved/Thumbnails目录下的缩略图缓存里。ThumbnailTools这一族API基本上是围绕着这份缓存和FObjectThumbnail对象做文章。它快、轻量适合批处理但它依赖UnrealEd模块打包出来之后就没有了。运行时预览图则是用SceneCapture2D这类方式把静态网格体、骨骼网格体或者关卡场景重新渲染一遍。它需要真正的渲染线程资源开销大得多但胜在打包后也能用而且可以调节光照、相机角度、背景。这就引出了第一个决策点你这个功能是在编辑器里跑还是在打包后的应用里跑如果你是想做DCC工具、批量导出、给Web服务返回缩略图基本都是编辑器侧ThumbnailTools路线就够了下面的内容可以继续看。如果是在游戏运行时动态展示某个资产的预览那你需要的是运行时Capture方案和本文的ThumbnailTools就不是一路了别混着用。1.2 编辑器侧获取缩略图的四个入口对比实际编辑器工具开发中经常被用到的“获取缩略图”入口大概有下面四种。放个表格一眼能看明白各自的定位。入口所在模块同步/异步输出形态典型场景ThumbnailTools::GenerateThumbnailForObjectToSaveToFileUnrealEd同步FObjectThumbnail*批量生成/落盘/二次压缩ThumbnailTools::GetThumbnailForObjectUnrealEd同步可能返回空FObjectThumbnail*拿内容浏览器已经生成的缓存缩略图AssetThumbnail AssetThumbnailPoolAssetThumbnail异步ViewportTexture/RenderTargetSlate UI中展示资产缩略图SceneCapture2D 等运行时渲染Engine实时TextureRenderTarget2D打包后运行时动态预览简单解释一下ThumbnailTools::GenerateThumbnailForObjectToSaveToFile是同步的你给它一个UObject它立刻调该资产的ThumbnailRenderer生成一张新缩略图返回FObjectThumbnail指针。这个数据结构本身就是为了“保存成图片”而设计的内部直接带未压缩像素数据最适合做导出。ThumbnailTools::GetThumbnailForObject其实是另一个路线它优先去拿已经缓存的缩略图对象。如果资产没有生成过缩略图或者缓存被清掉了返回nullptr需要自己兜底。AssetThumbnail是Slate层面用的封装带缩略图池可以让大量缩略图异步生成、异步更新非常适合内容浏览器那种滚动列表。但它输出的是用于Slate绘制和展示的纹理你想拿到像素做导出还得绕一圈渲染目标。这个我放到第3节详细说。SceneCapture2D那就是另一套运行时方案了不是本文主角但它确实在很多“运行时资源预览”需求里是唯一可行的路子。这句建议重要如果你只是想要“把缩略图存成文件”或“把图片信息返回出去”优先用第一条同步生成路线代码量和心智复杂度都最低。AssetThumbnail虽然听起来高级但很多情况下是自我感动。2. 正统路线用ThumbnailTools生成FObjectThumbnail并导出PNG2.1 同步生成缩略图的核心函数先说头文件别在include上浪费时间#include ThumbnailRendering/ThumbnailTools.h #include ThumbnailRendering/ThumbnailManager.h #include ImageUtils.h #include Misc/FileHelper.h核心接口就一个FObjectThumbnail* Thumbnail ThumbnailTools::GenerateThumbnailForObjectToSaveToFile( AssetObject, 128, 128 );第一个参数是你要生成缩略图的资产UObject第二个和第三个参数是目标宽度和高度。但注意这个函数生成的实际尺寸不一定等于你传进去的尺寸它是一个“最小宽高”概念最终尺寸受资产自带缩略图尺寸的影响。所以后面读取尺寸时一定要用Thumbnail-GetImageWidth()和Thumbnail-GetImageHeight()不要用自己传进去的参数。AssetObject需要是一个已经加载到内存的UObject。如果你手里只有FAssetData记得先调用GetAsset()完成加载。FObjectThumbnail这个对象的生命周期由ThumbnailTools内部管理不需要你手动delete。你只需要在拿到它之后立即处理数据。2.2 解包像素数据的格式陷阱生成完之后取像素数据用这个接口const TArrayuint8 RawData Thumbnail-GetUncompressedImageData();这里有一半的人会踩坑GetUncompressedImageData返回的是BGRA8格式不是RGB也不是RGBA。也就是说数组顺序是 Blue、Green、Red、Alpha而不是常见的 R、G、B、A。你直接把这段数据当RGBA传给OpenCV、传给Python PIL、或者直接塞给Web端的RawImage图片会整体偏蓝偏紫观感像开了假的色调映射。如果就是要往Web返回PNG正确的做法是让FImageUtils去处理PNG编码PNG格式本身带颜色空间标注引擎编码时会正确处理所以下面要讲的CompressImageArray是安全的。只有当你打算拿RawData自己转成RGB数组或者丢给别的库做像素处理时才需要手动交换第0和第2个字节。2.3 压缩成PNG并写文件拿到RawData之后压缩成PNG并保存其实就两行核心代码TArrayuint8 PngBytes; FImageUtils::CompressImageArray(Width, Height, RawData, PngBytes); FFileHelper::SaveArrayToFile(PngBytes, *OutFilePath);FFileHelper::SaveArrayToFile会把PngBytes整个写到磁盘。实测下来几百个静态网格体导出成256x256 PNG就算在大工程里速度也完全能接受。在UE5.1及以上版本FImageUtils还提供了更直接的SaveImageAutoFormat接口配合FImage对象使用。思路是先用RawData初始化一个FImage然后指定路径让引擎根据扩展名自动编码FImage Image; Image.Init(Width, Height, ERawImageFormat::BGRA8, EGammaSpace::sRGB); FMemory::Memcpy(Image.GetData(), RawData.GetData(), RawData.Num()); FImageUtils::SaveImageAutoFormat(*OutFilePath, Image);两种写法都能跑CompressImageArray胜在兼容性老工程切过去不用改头文件SaveImageAutoFormat更符合UE5风格但要求UE5.1以上。我自己的习惯是维护一个统一封装内部走CompressImageArray这样UE4和UE5都能编译。这里单独说一个很多人没注意的点CompressImageArray之后拿到的PngBytes并不是你RawData的简单字节复制它是正经经过zlib压缩的PNG文件流。所以你后面无论走FFileHelper还是走Base64都是基于这段PNG字节流而不是原始像素流。图片在Web端展示、在图片查看器里打开都不需要你额外处理颜色转换。3. 界面路线AssetThumbnail在Slate中的正确打开方式3.1 FAssetThumbnail与FAssetThumbnailPool的分工如果目标只是为了导出成文件我强烈建议跳过这一节用第2节的方案。但如果你要做的是资产管理器里那种网格视图、列表缩略图那就绕不开AssetThumbnail这套Slate组件。FAssetThumbnail和FAssetThumbnailPool是配对使用的。FAssetThumbnailPool相当于一个缩略图异步生成和缓存池负责调度“哪些缩略图该后台生成了”“生成完怎么通知界面刷新”。FAssetThumbnail则代表一个具体资产的缩略图对象内部持有一个对池的引用负责提供这个缩略图对应的Viewport和绘制用纹理。要理解它的设计动机就得知道内容浏览器的缩略图是“按需生成”的几十万个资产不可能在打开编辑器时全部生成完只有当你滚动到某个资产附近时池才会触发该资产的异步生成任务。生成完后通过委托通知关联的Slate控件重新绘制。3.2 等回调后再取ViewportTexture创建FAssetThumbnail本身不难TSharedPtrFAssetThumbnailPool ThumbnailPool MakeSharedFAssetThumbnailPool(32); TSharedPtrFAssetThumbnail AssetThumbnail MakeSharedFAssetThumbnail( AssetObject, 128, 128, ThumbnailPool );难点在于什么时机可以拿到可用的纹理。如果你创建完立刻去取图很大概率拿到一张灰色占位图或者nullptr。因为缩略图还在异步生成队列里根本没有到位。正确的姿势是监听缩略图池的通知等某个缩略图生成完成后再通过FAssetThumbnail暴露的Viewport接口拿纹理ThumbnailPool-OnGetThumbnail().AddLambda([AssetThumbnail](const FAssetThumbnail Thumbnail) { // 确认回调的就是自己关联的资产 if (Thumbnail ! AssetThumbnail.Get()) { return; } UTexture* ViewportTexture AssetThumbnail-GetViewportRenderTargetTexture(); if (ViewportTexture nullptr) { return; } // 这里拿到的是可用于Slate显示/读取的纹理资源 });不同UE版本里FAssetThumbnail获取纹理的接口名略有差异有的叫GetViewportTexture有的叫GetViewportRenderTargetTexture编译报错时切换一下即可本质都是拿那张已经烘好的纹理对象。拿到的UTexture想要导出为图片文件还需要再走一步创建一个TextureRenderTarget2D把纹理拷贝进去然后通过RenderTarget-ReadPixels()把像素读出来。这又多了一层渲染目标拷贝所以对于纯粹的“导出图片”需求绕这一圈纯属自找麻烦。3.3 什么时候该用它我自己对AssetThumbnail路线的定位就一句话只做展示不做导出。如果你的需求是“资产管理器里能看到缩略图”或者“某个UI面板上要预览一批资产”那就用AssetThumbnail它替你解决了异步生成、缓存复用、UI刷新这些痛点。如果你是想把图存下来、发出去应该回到ThumbnailTools那条路上。两条路都是UE官方提供的但不是互相替代的关系而是分工不同。4. 两种输出形态落盘保存与Base64回传图片信息4.1 批量保存到指定目录的完整函数现在把链路串起来给你一个可以直接复制到编辑器模块里的完整函数。它的作用是输入一个FAssetData列表批量把每个资产的缩略图生成好保存为PNG到指定目录同时返回成功和失败的文件路径列表。bool ExportThumbnailsToLocal( const TArrayFAssetData InAssets, const FString OutDir, TArrayTPairFString, FString OutResult) { if (InAssets.Num() 0) { return false; } IFileManager::Get().MakeDirectory(*OutDir, true); for (const FAssetData AssetData : InAssets) { UObject* Asset AssetData.GetAsset(); if (!Asset) { continue; } FObjectThumbnail* Thumbnail ThumbnailTools::GenerateThumbnailForObjectToSaveToFile( Asset, 256, 256 ); if (!Thumbnail) { continue; } const int32 Width Thumbnail-GetImageWidth(); const int32 Height Thumbnail-GetImageHeight(); const TArrayuint8 RawData Thumbnail-GetUncompressedImageData(); TArrayuint8 PngBytes; FImageUtils::CompressImageArray(Width, Height, RawData, PngBytes); FString SafeAssetName AssetData.AssetName.ToString(); SafeAssetName FPaths::MakeValidFileName(SafeAssetName); FString OutFilePath FPaths::Combine(OutDir, SafeAssetName TEXT(.png)); if (FFileHelper::SaveArrayToFile(PngBytes, *OutFilePath)) { OutResult.Emplace(AssetData.GetObjectPathString(), OutFilePath); } } return true; }几个细节说一下这里调用GetAsset()会同步加载资产。如果资产本来就常驻内存开销很小如果是冷资产第一次加载会有点卡在批处理大量资产时建议用第6节的分帧思路。FPaths::MakeValidFileName用来过滤掉文件名里的非法字符否则Windows上可能会出现保存失败。缩略图生成失败的情况是真实存在的。资产没有注册ThumbnailRenderer时Thumbnail可能返回nullptr。所以一定要做空判断不要把生成失败直接当异常。这个函数是同步的几百个资产跑下来编辑器还是会卡。批量场景最好用一个滑动窗口每帧处理几个后面第6节会展开。4.2 返回图片信息给HTTP/脚本调用Base64 宽高 路径“返回图片信息”在我理解里有两种形态一种是返回图片文件的本地路径另一种是直接把图片数据编码成Base64配合宽高、格式等信息打包成JSON文本交给上一级工具。比如UE侧起一个HTTP服务收到外部请求后返回这些JSON或者C工具函数直接输出给Python脚本消费。Base64编码这段代码很简单#include Misc/Base64.h TArrayuint8 PngBytes; FImageUtils::CompressImageArray(Width, Height, RawData, PngBytes); FString Base64String; FBase64::Encode(PngBytes, Base64String);注意编码之后体积会比原PNG膨胀约三分之一。256x256的缩略图PNG一般就几KB到几十KBBase64之后也不会太大完全够用。但如果一次性返回几十张图建议还是把图落盘返回本地路径更稳妥。完整的返回信息可以拼成JSONFString JsonString FString::Printf( TEXT({\objectPath\:\%s\,\width\:%d,\height\:%d,\base64\:\%s\}), *AssetPath, Width, Height, *Base64String );这里没有用UE的FJsonObjectSerializer是为了让你一眼看懂字段结构。真要上生产建议用FJsonObject装配好再序列化避免特殊字符出问题——尤其是资产路径里如果带反斜杠或者引号上面的Printf直接拼会有转义隐患。4.3 扩展Python侧怎么解码如果你正好需要跟Python侧对接解码这段Base64很简单import base64 from PIL import Image from io import BytesIO image_data base64.b64decode(base64_string) img Image.open(BytesIO(image_data)) print(img.size, img.mode)PNG和JPEG在这种场景下都能直接交PIL处理不需要关心BGRA还是RGBA因为PNG解码后PIL会按颜色空间自动解释。这也是为什么我强烈建议“要么存PNG文件要么给PNG的Base64”不要直接把RawData传给外部。RawData的通道顺序、每像素字节数、有无压缩这些信息太容易被忽略一旦出错排查成本极高。5. 实测踩坑记录空指针、偏色、卡线程与路径问题5.1 GetThumbnailForObject为什么会返回nullptr我刚上手时用的是ThumbnailTools::GetThumbnailForObject想着“这不就是拿现有缩略图吗”结果打了一堆空指针。后来查了源码才知道这个函数是去内容浏览器的缩略图缓存里捞已有的FObjectThumbnail对象。如果这个资产从来没被内容浏览器展示过或者缓存被清理了它就返回nullptr。它不是“没有就现场生成”的兜底函数而是一个“只读缓存”的查询入口。如果你的使用场景是“资产已经显示在内容浏览器里现在想额外拿一份缩略图去做点事”可以用它。但如果你想要“不管缓存里有没有我都要一张缩略图”就必须用第2节的GenerateThumbnailForObjectToSaveToFile。这里其实反映了一个通用认知带Generate字样的接口通常意味着现场生成带Get字样的接口多半是查缓存。我后来选型时都会先按这个规律过一遍能少踩很多坑。5.2 通道顺序错了网页上全是蓝紫色这个坑前面已经提到但值得单独说一次。背景是我最初没有走CompressImageArray而是把RawData直接通过TArray 发给了Python侧想让Python直接做成Raw图片。结果Web端预览全是蓝紫色像开了一个奇怪的色调映射。问题就出在GetUncompressedImageData返回的是BGRA8。如果你希望直接拿到RGB数组或者要把数据交给不识别BGRA的库记得做一次通道交换void ConvertBGRAtoRGBA(TArrayuint8 InRawData) { for (int32 i 0; i 3 InRawData.Num(); i 4) { Swap(InRawData[i], InRawData[i 2]); } }交换完第0和第2个字节就变成RGBA。注意别连Alpha一起动Alpha在第3个字节保持不变。5.3 批处理时编辑器卡死第二个实际问题是批处理几百个资产时编辑器UI基本卡死鼠标拖动都不顺畅。原因很简单GenerateThumbnailForObjectToSaveToFile是同步函数而且它在调用ThumbnailRenderer时某些资产的缩略图渲染很重——尤其是高面数StaticMesh或材质复杂的SkeletalMesh一个就可能耗掉几十毫秒甚至几百毫秒。我的做法是分帧处理。用一个滑动窗口每帧只处理几个资产把整个批量过程拆开。这样虽然总耗时变长了但编辑器和引擎的Tick还能继续跑不会让用户以为程序崩了。具体实现可以在编辑器模块里注册一个FTSTicker委托每次Tick从队列里取N个资产处理FTSTicker::GetCoreTicker().AddTicker( FTickerDelegate::CreateLambda([this](float DeltaTime) { int32 ProcessedThisFrame 0; while (PendingAssets.Num() 0 ProcessedThisFrame MaxPerFrame) { FAssetData AssetData PendingAssets.Pop(); // 执行生成与保存 ProcessedThisFrame; } return true; }) );MaxPerFrame建议设在5~10具体看缩略图生成耗时。这个方案不是零成本但至少不会让编辑器看起来像死了一样。5.4 中文路径、未加载资产、延迟缩略图还有几个零散但很常见的坑放在一起说一下。第一中文路径。FFileHelper::SaveArrayToFile在Windows上保存中文路径本身没问题但如果你的最终文件要被Web服务或第三方工具读取还是尽量保证目录和文件名剔除非ASCII字符。否则传出去容易在后续环节产生一串乱码路径。用FPaths::MakeValidFileName还不够我一般会额外做一次Unicode转拼音或者直接用资产GUID作为文件名。第二未加载资产。前文说过GetAsset()会触发同步加载。要在批处理场景里尽量避免大资产的同步加载可以考虑先只在缩略图生成时才加载不要提前把一堆资产引用保留在内存里。用一个FAssetData列表持有资产路径而不是持有UObject强引用这样释放起来干净。第三延迟缩略图。某些资产的缩略图是在编辑器后台线程异步生成的GenerateThumbnailForObjectToSaveToFile虽然名字里带Generate但它并不是对所有资产类型都会立刻返回有效结果。遇到个别资产返回了空FObjectThumbnail不要直接报错做个重试机制效果会更好。6. 性能优化与缓存策略批量导出时不拖垮编辑器6.1 用资产注册表拿列表避免全量遍历Object在编辑器工具里拿资产列表我见过不少人直接去遍历UObject或者用FindObject到处找。工程小的时候没事工程一大人就麻了。规范做法是从资产注册表AssetRegistry拿元数据列表。以UE5.x为例#include AssetRegistry/AssetRegistryModule.h FAssetRegistryModule AssetRegistryModule FModuleManager::LoadModuleCheckedFAssetRegistryModule(AssetRegistry); TArrayFAssetData AssetDataList; AssetRegistryModule.Get().GetAssetsByClass( FTopLevelAssetPath(TEXT(/Script/Engine), TEXT(StaticMesh)), AssetDataList, true );如果是UE4.x把第一个参数换成TEXT(StaticMesh)的FName即可。资产注册表拿到的只是FAssetData轻量描述包含路径、类名、GUID不会把资产本体加载进内存。这对批量处理是个巨大优势。这里加上bSearchSubClasses参数为true是因为很多引擎类有子类比如蓝图生成的资产可能继承自StaticMesh搜索子类能覆盖更多情况。6.2 分帧处理和异步压缩前面讲了分帧生成缩略图。另外一个优化点是压缩PNG的工作也可以丢到异步任务里做。ThumbnailTools生成缩略图依赖编辑器GUI上下文不能随便丢线程但FImageUtils::CompressImageArray和FFileHelper::SaveArrayToFile本身是可以离线程的。所以一个更好的pipeline是主线程生成FObjectThumbnail把RawData拷贝出来塞到异步任务队列里后台线程负责压缩和写盘。这样主线程只干一件事生成缩略图而且生成完就把数据交给后台不占UI时间。用UE的AsyncTask就可以#include Async/Async.h AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [RawData MoveTemp(RawData), Width, Height, OutFilePath]() { TArrayuint8 PngBytes; FImageUtils::CompressImageArray(Width, Height, RawData, PngBytes); FFileHelper::SaveArrayToFile(PngBytes, *OutFilePath); });但要注意RawData是从FObjectThumbnail里拿的const引用拷贝一份再MoveTemp是必要的不能直接把引用叼到lambda里然后让主线程把Thumbnail释放掉否则就是悬垂引用。6.3 直接复用Saved/Thumbnails缓存最后一个关于缓存的想法。UE的内容浏览器本身有一整套缩略图缓存位置在工程的Saved/Thumbnails目录。你在编辑器里浏览过哪些资产这些资产的缩略图就有可能存在那里。如果你只是想把某些“已经在内容浏览器里生成过缩略图”的资产对应图片复制出来不需要每次都现场Generate。不过UE的缩略图缓存文件名是经过GUID或路径哈希处理的不是按资产名命名直接复制文件出来没法对应。要靠代码去读FThumbnailCache又比较麻烦所以我一般只在两个场景用到它一是做增量导出先比对目标目录里有没有同名PNG有就跳过二是设置一个较大尺寸的AssetThumbnailPool让内容浏览器和我的工具共用一套缩略图池减少重复生成。对于大多数“动态获取资源缩略图、下载到本地或返回图片信息”的需求更务实的策略是自己维护一份导出缓存。比如导过一次之后在工程里放一个Cache目录下次直接查Cache命中就不再走ThumbnailTools。这样比每次现场生成快得多也能避免大量的重复IO和渲染开销。我实测过一个500资产规模的工程第一次全量导出大约需要几十秒第二次因为有缓存几乎秒开。这对工具的使用体验影响很大尤其是需要反复调试输出格式或者跟Web联调的时候不用每次等全量重跑。我做这套东西最大的体会是不要被“动态获取缩略图”这句话误导它在UE里的实现路径远比听起来琐碎。先从使用场景倒推确定是编辑器还是运行时再选定是走ThumbnailTools还是AssetThumbnail最后才轮到编码和优化。把这四步理顺剩下的事情其实就是调API加处理边缘情况。回头再看那些卡住我一整天的问题——通道顺序、异步回调时机、空指针——每一个都有明确的规律可循只是文档里不会把这些坑提前告诉你。如果再做一次我会直接跳过AssetThumbnail从第一节就用ThumbnailTools FImageUtils这条组合拳然后立刻把导出缓存加上。这套方案基本能覆盖编辑器工具里绝大部分缩略图导出需求。剩下的那部分等真正遇到再回来补SceneCapture那套。
返回列表