查看原文
其他

UE4快速开发官方B站视频封面合成工具

大钊 虚幻引擎 2020-09-06

前言

相信关注虚幻引擎官方B站频道的小伙伴们,早已经留意到我们最近上传的视频的封面都加上了一个底部的Banner条。

对比以往的方式:

你肯定也会跟我一样认同加上了不同颜色,不同文字的底部Banner条之后,在虚幻引擎B站主页里找视频更加容易了。当然这个方式也不是我首创,而是我们Unreal Engine的YouTube频道上采用这种方式已经很久了。

所以本篇文章,我就来分享讲解一下我用UE4快速制作一个Banner工具的过程和其中的关键技术点!所以这是一篇技术文章哦!

需求分析

作为一个虚幻社区的社区经理,B站也是我的职责范围。所以重建B站频道,梳理B站视频,让大家更好的在B站里学习技术,也是我义不容辞的责任。一般来说,我们的工作流程是在B站上传视频的时候,会需要同时上传一张封面图。以前的时候,封面图都是用自动生成的,所以都没有Banner条,看起来就很零乱。

问题的需求其实就是给图片底部覆盖上一张Banner图。在调研了一番之后,更准确的说,是生成一张1440*900的封面图。

但现实往往比较复杂:

  1. 不同的视频,属于不同的分类,所以需要Banner上的文字和颜色都得相应的变化。
  2. 视频的量比较多。每周平均都得新上传几部,以前已经上传过的500多个我也想重新给封面套上Banner。所以需要同时支持图片和视频。
  3. 图片和视频的分辨率不同,所以希望能尽量自动化的放缩匹配上。

我身为一个程序员,懒惰是我的美德:

  1. 希望流程尽量高效。输入一个视频或图片,输出一张封面图,中间的步骤应该尽量的高效简化。
  2. 可视化调整。加Banner条的时候,希望能实时的调整背景图样式,或者采用不同的视频帧画面。
  3. 方便扩展。可以随时更改Banner条颜色和文字内容。

技术方案选择

需求想明白了之后,就开始考虑用什么方案来实现它。

一,PS

显而易见的,这种典型的P图的流程,人们第一时间想的也是用PS。实际上我们的美国总部的同事也是用PS来制作YouTube的视频封面。他们也分享给了我PS的模板文件。但我在自己尝试制作了一张封面图后就放弃了,原因有:

  1. PS打开比较慢。每次要制作封面图,我都得等它打开一下。
  2. 每次依然都得手动的用截图工具从视频播放器中截取画面,然后再导入PS。
  3. 每次都得在PS里缩放调整操作一番,才能对好一张封面图位置。
  4. PS里的样式不太理想的话,还得从视频里重新截图再导入PS,流程再来一遍,很繁琐。

二,Python Pillow库

第二种立即想到的,就是写个批处理工具来“智能化”地处理素材。所以就需要用个图像库来读取写入图片。人生苦短,我用Python,有同事建议可以用pillow图像库来做。但想了想也不太适合,最大的问题就在于基于命令行的工具,我很难实时调整演示。而且我连命令行我都懒得敲。

三,把UMG当做PS用!

最后突然灵光一现,我可以用UE4来做啊。UE4本身可以导入素材,显示图片和播放视频,所以里面的工具库肯定都具备。同时UMG和蓝图制作一个简单的图形界面工具都非常的方便。另外,因为我对UE4还算比较熟悉,所以有信心自己能搞定。谁规定UE4只能用来游戏?

工具演示

然后,我零碎花了两天时间,很快的做完了一个工具,并且自己使用起来非常的便利。从一个视频到封面图的过程,只需要几个点击和挪动就可以了。


有一些非常有用的功能点:
  1. 可以快速的切换Banner样式。
  2. 不管拖动进来的图片和视频大小如何,可以自动的匹配背景框大小。也支持一定的放大特写。
  3. 拖动进度条可以选取视频帧。
  4. 非常快速的导出结果封面图,如果是图片,最快的操作只需要一次拖动。

技术点

当然,在制作该工具,编写相应代码过程中,也遇到了一些坎。所以本篇文章其实主要是记录分享遇到的技术点问题,以及解决中的过程思路。给大家一些启发,其实利用UE4本身这个庞大的宝藏代码库,可以用来做很多有意思的事情。

一,拖动文件到运行时游戏窗口中

我第一个想要支持的功能点就是,我想要拖动图片和视频到工具页面内。凭印象中,在UMG里实现拖动是在Widget里重载OnDragEnter和OnDrop一系列事件。但在我兴致冲冲跑起游戏窗口,想把图片拖动到窗口内时,竟然发现窗口没有任何响应,蓝图事件竟然完全没有触发!

没道理啊!我们在编辑器里导入资源都是用拖动的啊。编辑器有这功能,那说明一定有其相应的实现。另一方面,编辑器本身也就是个不同样式的Game而已,所以编辑器能做的,我们一定也能做到!

遇事不决,源码Debug大法开起来。同时开始在UE4源码里顺藤摸瓜。
//首先从UUserWidget::OnDragEnter开始找,看它是被谁调用的
void UUserWidget::NativeOnDragEnter( const FGeometry& InGeometry, const FDragDropEvent& InDragDropEvent, UDragDropOperation* InOperation )
{
    OnDragEnter( InGeometry, InDragDropEvent, InOperation );
}

//它是由UUserWidget::NativeOnDragEnter调用,其上又到了SObjectWidget::OnDragEnter。UMG底层就是Slate,所以这也是挺合理的。
void SObjectWidget::OnDragEnter(const FGeometry& MyGeometry, const FDragDropEvent& DragDropEvent)
{
    TSharedPtr
<FUMGDragDropOp> NativeOp = DragDropEvent.GetOperationAs<FUMGDragDropOp>(); //断点
    if ( NativeOp.IsValid() )
    {
if ( CanRouteEvent() )
{
    WidgetObject
->NativeOnDragEnter( MyGeometry, DragDropEvent, NativeOp->GetOperation() );
}
    }
}
在SObjectWidget::OnDragEnter的第一行下个断点,然后在游戏窗口里随便拖入个文件。触发了!

但竟然是Null,然后进不了里面的WidgetObject->NativeOnDragEnter。再来一次,看看第一行为什么类型获取不到:

难怪GetOperationAs<FUMGDragDropOp>会失败,里面的Content的类型是FExternalDragOperation,而不是FUMGDragDropOp。看类型的名字就知道FUMGDragDropOp是专门为UMG内部的互相拖动实现的,而FExternalDragOperation是用来支持外部文件的拖入的。

首先我不管为啥UMG窗口不支持外部文件拖入。我想要的是怎么搞一下,来让我的UWidget可以把事件给接收过来。当然能不改源码就不改源码,否则在编译版引擎就没法运行了。

简单想了一下,既然FExternalDragOperation只分发到了Slate这一层,那我自己定义个SWidget控件把它也显示在窗口内岂不是就能接收到了?!OnDragEnter是虚函数,我在子类重载掉,在里面我就可以为所欲为了!因为UMG的底层就是Slate,所以SWidget可以把UMG的widget嵌套进来,这样也可以在窗口内显示UMG控件了。

说干就干,定义个SDropWidget:
class BANNER_API SDropWidget : public SCompoundWidget
{
public:
    SLATE_BEGIN_ARGS(SDropWidget) { }
    SLATE_END_ARGS()public:
    virtual void OnDragEnter(const FGeometry& MyGeometry, const FDragDropEvent& DragDropEvent)override;//重载掉
protected:
    TWeakObjectPtr
<UBaseBannerMainWidget> mWidget;
};

void SDropWidget::OnDragEnter(const FGeometry& MyGeometry, const FDragDropEvent& DragDropEvent)
{
    //这里就可以按照我们想要的方式来获取了。
    TSharedPtr
<FExternalDragOperation> op = DragDropEvent.GetOperationAs<FExternalDragOperation>();
    const TArray<FString>& files = op->GetFiles(); //得到拖入的文件列表
    if (files.Num() > 0)
    {
        mWidget
->DropFile(files[0]); //一次只接收一个,这里逻辑就可以随便写了。
    }
}

void SDropWidget::AddWidget(TWeakObjectPtr<UBaseBannerMainWidget> widget)
{
    mWidget
= widget;
    ChildSlot
[mWidget->TakeWidget()]; //添加嵌套UMG控件到Slate控件里
}
代码很简单,重载掉OnDragEnter就可以在子类写自己的逻辑代码了。当然,生成Slate控件并显示到窗口的过程也非常简单,不知道的可以去google一下就出来了:
TSharedRef<SDropWidget> dropWidget = SNew(SDropWidget);//创建Slate控件//添加到游戏窗口
GEngine
->GetWorldFromContextObject(this, EGetWorldErrorMode::LogAndReturnNull)->GetGameViewport()->AddViewportWidgetContent(dropWidget);
APlayerController
* pc = UGameplayStatics::GetPlayerController(this, 0);//创建UMG控件
UBaseBannerMainWidget
* mainWidget = CreateWidget<UBaseBannerMainWidget>(pc, BannerWidgetClass, "BannerWidget");//内嵌到Slate控件内
dropWidget
->AddWidget(mainWidget);
最后运行调试一下,成功触发OnDragEnter,大功告成!

二,加载图片生成Texture

在能成功拖入图片并打印出文件名后,紧接着我就遇到了另一个问题,怎么在UMG里的Image控件里显示这张png或jpg图片?这个问题其实就是怎么在runtime加载一张图片生成一个Texture。这个问题并不难,简单搜索一下就能在FImageUtilis里发现了一个已经写好的方法:
class FImageUtils
{
    ENGINE_API static UTexture2D* ImportFileAsTexture2D(const FString& Filename);
}
顺便浏览了一下FImageUtils,发现真是类如其名,里面有好多好东西。

三,渲染UMG特定控件内容到图片

不得不说,用BP跟UMG写这种工具界面真是太小意思了。在刷刷刷写完整个UI交互逻辑之后。最后一个问题就是怎么把特定的UMG控件内的内容渲染输出成一张图片并保存?这也是能否把UMG当做PS用的关键!

这问题怎么办呢?想到以前看到一个技术视频谈UI优化的,里面有谈到可以用UMG里的一个RetainerBox控件来优化UI性能。其原理就是可以把其子控件渲染到一个RenderTarget,然后直接显示出来跳过中间过程加速性能。RT本质就是一张贴图,所以就像游戏内截图一样,一定有办法可以把RT再转换成图片导出保存。

所以让我们打开RetainerBox的实现文件来看一下它是怎么渲染的:
UCLASS()
class UMG_API URetainerBox : public UContentWidget
{
protected:
    TSharedPtr
<class SRetainerWidget> MyRetainerWidget; //老套路,内部是Slate控件
}

class UMG_API SRetainerWidget : public SCompoundWidget, public ILayoutCache
{
protected:
    FRetainerWidgetRenderingResources
* RenderingResources; //内部发现了这么一个东西
}

bool SRetainerWidget::PaintRetainedContent(const FPaintArgs& Args, const FGeometry& AllottedGeometry)
{
//搜索代码后,发现了在这个函数内,有取得RT,并且还出现了FWidgetRenderer
    UTextureRenderTarget2D
* RenderTarget = RenderingResources->RenderTarget;
    FWidgetRenderer
* WidgetRenderer = RenderingResources->WidgetRenderer;

    WidgetRenderer
->DrawWindow(...);
}
FWidgetRenderer的发现是个惊喜!一看名字这么俊就是我想要的。到它的.h文件里看看都有啥:
class UMG_API FWidgetRenderer : public FDeferredCleanupInterface
{
public://哈哈哈,果然Epic很贴心啊!
    void DrawWidget(UTextureRenderTarget2D* RenderTarget,const TSharedRef<SWidget>& Widget,FVector2D DrawSize,float DeltaTime,bool bDeferRenderTargetUpdate = false);
}
DrawWidget更是命中靶心!接着我们在DrawWidget查找所有调用地来看看:

哈哈哈,又有意外之喜:AFunctionalUIScreenshotTest应该是之前Epic自己写来测试的widget渲染的测试代码。我们抄袭的目标有了!在其cpp代码内简单翻翻就找到了目标代码:
void AFunctionalUIScreenshotTest::PrepareTest()
{
ScreenshotRT
= NewObject<UTextureRenderTarget2D>(this);//创建RT
ScreenshotRT
->ClearColor = FLinearColor::Transparent;
ScreenshotRT
->InitCustomFormat(ScreenshotSize.X, ScreenshotSize.Y, PixelFormat, !bIsSRGB);
}

void AFunctionalUIScreenshotTest::RequestScreenshot()
{
    UGameViewportClient
* GameViewportClient = GEngine->GameViewport;
    TSharedPtr
<SViewport> ViewportWidget = GameViewportClient->GetGameViewportWidget();
    FIntPoint ScreenshotSize
= GameViewportClient->GetGameViewport()->GetSizeXY();
    TArray
<FColor> OutColorData;

    // when rendering to a separate render target
    FWidgetRenderer
* WidgetRenderer = new FWidgetRenderer(true, false); //创建widget渲染器

    //渲染到RT
    WidgetRenderer
->DrawWidget(ScreenshotRT, ViewportWidget.ToSharedRef(), ScreenshotSize, 0.f);
    FlushRenderingCommands();
    BeginCleanup(WidgetRenderer);

//读取RT像素值
    ReadPixelsFromRT(ScreenshotRT, &OutColorData);
}
相应辅助对象的创建流程都可以照搬着来,而最后一步从RT读取像素值保存到图片的操作,我想着这个操作应该也挺频繁的。跟图片相关的操作可能在之前的FImageUtils里面也有,所以到那里面翻一下,果然首先发现了ExportRenderTarget2DAsPNG这个方法,依然顺藤摸瓜一下,在UKismetRenderingLibrary下发现了ExportRenderTarget方法!这个方法厉害了,直接从RT到图片,简直就是为我量身定做的。
class FImageUtils
{
ENGINE_API
static bool ExportRenderTarget2DAsPNG(UTextureRenderTarget2D* TexRT, FArchive& Ar);
}

UCLASS(MinimalAPI, meta=(ScriptName="RenderingLibrary"))
class UKismetRenderingLibrary : public UBlueprintFunctionLibrary
{
    /**
     * Exports a render target as a HDR or PNG image onto the disk (depending on the format of the render target)
     */

    UFUNCTION(BlueprintCallable, Category = "Rendering", meta = (Keywords = "ExportRenderTarget", WorldContext = "WorldContextObject"))
    static ENGINE_API void ExportRenderTarget(UObject* WorldContextObject, UTextureRenderTarget2D* TextureRenderTarget, const FString& FilePath, const FString& FileName);
}
OK,到此整个流程都打通了,我们把他们组织起来:
FStriing RenderWidgetToFile(UUserWidget* widget, FString title, FString channel)
{
    UTextureRenderTarget2D
* widgetRT = NewObject<UTextureRenderTarget2D>(this);
    bool bIsSRGB = false;
    EPixelFormat PixelFormat
= PF_B8G8R8A8;
    FIntPoint
ScreenshotSize(1440, 900); //定死了就这么大

    widgetRT
->ClearColor = FLinearColor::Transparent;
    widgetRT
->RenderTargetFormat = ETextureRenderTargetFormat::RTF_RGBA8;
    widgetRT
->InitCustomFormat(ScreenshotSize.X, ScreenshotSize.Y, PixelFormat, !bIsSRGB);

    FWidgetRenderer
* WidgetRenderer = new FWidgetRenderer(true, false);

    WidgetRenderer
->DrawWidget(widgetRT, widget->TakeWidget(), ScreenshotSize, 0.f);

    FlushRenderingCommands();
    BeginCleanup(WidgetRenderer);

    resultFileName
+= "_" + title + +"_" + channel + ".png"; //拼接一下文件名

    UKismetRenderingLibrary
::ExportRenderTarget(this, widgetRT, resultFilePath, resultFileName);

    return FPaths::Combine(resultFilePath, resultFileName);
}
这样的话,只要我给RenderWidgetToFile这个接口,传递红框这个子UMG控件,就可以把这部分的内容渲染成图片了!最后把图片拖入到B站的后台上传页面就可以了。

剩下的一些周边功能,如打开视频,缩放图片,挪动位置等UI交互逻辑就很简单了。不再赘述了。

开源

秉持Epic的“If you love something, set it free.”的思想,我把Banner工具的源码开源到了GitHub。
https://github.com/UE4Community/Banner
供大家查看。我看需要也可能后续再继续更新扩展。

总结

整个功能完成下来,核心就这三个小小的技术点。分享出来希望给大家一些启发。把思想归纳一下:

  1. 坚定信心,UE4编辑器能做到的,我们也能做到。编辑器目前不支持的,我们改改代码绕一下一般也能做到。最后还有个办法改引擎源码,那能做的东西就更多了。要充分理解到UE4编辑器本质其实也是个Game,只不过有些代码没在Runtime模式下启动,但接口功能一般都已经写好了。
  2. UE4源码是个巨大的宝藏。游戏引擎要支持游戏里的各种功能开发,本身已经写成了个庞然大物,所以一般内部都集成了各种库和算法。所以当你想实现某个功能的时候,如果比较典型,第一个反应应该是去源码里找找看有没有现成写好的接口。
  3. 懂得追根溯源。我在一开始开发Banner工具的时候,完全不知道内部Widget的渲染细节,但通过对问题的分析和技术感觉,多用“Find All References”功能,就可以对代码的调用关系进行分析参照。可以快速的找到可供模仿的代码块。
  4. 用UE4开发小工具真的好方便!身为一个技术人员,应该多一些用工具来优化自身工作流程的意识。

近期焦点

虚幻引擎4.23现已发布!

Epic Games携虚幻引擎Unreal Engine亮相AU China 2019

虚拟制片:人人可用的表演捕捉

[活动预告]虚幻引擎Unreal Circle线下技术沙龙 | 9月21号北京站

从SketchUp到Twinmotion:网络研讨会重播

虚幻商城五周年!精选商品五折特惠!

精选免费商城内容 - 2019年9月

一周综述:虚幻引擎在SIGGRAPH 2019

Twinmotion社区挑战赛公告

UE4助力通用雪佛兰打破历史销售记录

虚幻引擎Epic MegaGrant:教育工作者须知

实时渲染之Twinmotion:又好又快的建筑可视化

Epic Games以120万美元Epic MegaGrant资金支持Blender


如需获得更多虚幻引擎4的授权合作方式和技术支持,请发送邮件至
EGC-Business@epicgames.com咨询;
如果你想来 Epic 工作,扫描下方二维码关注我们后点击菜单栏按钮“更多”并选择“招聘”,即可了解我们的最新招聘信息。Epic Games 欢迎你的加入!
长按屏幕选择“识别二维码”关注虚幻引擎
“虚幻引擎”微信公众账号是 Epic Games 旗下 Unreal Engine 的中文官方微信频道,在这里我们与大家一起分享关于虚幻引擎的开发经验与最新活动。



Modified on

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存