找回密码
 立即注册
查看: 269|回复: 0

虚幻引擎自定义缩略图渲染器的心得 Custom Thumbnail ...

[复制链接]
发表于 2022-8-11 16:03 | 显示全部楼层 |阅读模式
Created: August 1, 2022 3:40 PM Type: GameDev, Unreal
为什么要写这篇文章

这是笔者工作中碰到的第一个下发下来的任务,属于是“自定义引擎”部分。作为一个刚刚接触Unreal的菜鸟,对于C++ 也是一个勉强会用的前提下我还是希望可以认真记录下自己解开的过程,记录这个时刻。同时将这个问题和解决思路分享给后来者。在Google上关于类似的问题,如果用关键字 CustomThumbnailRenderer 来去搜索,相关的答案就仅仅4页。我是在不断的尝试和调试中最后算是解决了自己的问题。所以我想将我的思路分享下。 同时在写具体的实现过程中,还夹杂了最为Unreal初体验者,超级菜鸟的一些自我思考,所以全文显得有点冗长,还请谅解。另外这是笔者第一次写文章,如果有不当之处欢迎在评论区指正。

同时这个方法仅仅为多个实现思路之一,如果有其他的实现方式也欢迎在评论区提出。
<hr/>问题的描述和前置知识的指路

问题描述

这个需求是美术组提出的,他们正在使用一种自定义的C++类,基于Spline(样条)的形式开发的建筑部署功能。同时建筑上的装饰品使用的是也是自定义C++类通过识别周围的Spline然后可以直接Snap上去。
这里的问题是这些装饰品(继承自 AActor)内有个Structure 内保存了装饰品的静态网格UStaticMesh , 但是在Unreal Editor里这些装饰品显示的是普通的默认图标。因为保持了命名格式,在编辑器下也很难通过名字了解这些物品分别是代表什么(名字过长而折叠)。所以美术希望可以在Editor里直接通过缩略图看到静态网格的样子从而方便自己的工作。
所以这里的目标是对应继承自特定类的蓝图,自定义它的缩略图渲染并且在Editor里展示出来。如果想要看具体的解决方案的,可以直接跳到文章最后。如果最后的解决方案不能够为你提供解题,不妨看看中间的思考过程,或许可以从蛛丝马迹里窥到些许思路。
前置知识和相关资源指路

笔者建议在深入这个问题之前,先去了解下Unreal的模块和插件系统的基本运作机制,在Unreal里构建C++类的基础知识。
然后关于缩略图的问题,笔者在网上一共搜索到三个相关度很高的资源来推进了我的解决过程
一个是 @SQTaoger 的文章
他的文章非常系统性的帮助了我了解了Thumbnail Tools在Unreal源码里的生命流程。其中我的一部分Thumbnail相关的理解会直接参考他的文章
另一个是 @neil3d 分享的 GIF importer 插件,有UE4和UE5版本
他的源码帮助我进一步理解了Unreal Module 系统的构成, .Build.cs 和 .uplugin 文件的使用和Module 的文件结构等。 同时他的里面也包含了一个完整的自定义缩略图渲染器 CustomThumbnailRenderer 位于 Plugins/AnimatedTexturePlugin/Source/AnimatedTextureEditor/Private 中。
最后一个也是真正意义上具体解决了我渲染缩略图的文章(EN)。 但是之中的思路也是促使我完成了自定义缩略图: 通过重新注册对应类和自定义渲染器从而将数据传入。
<hr/>我的思考过程

初步的尝试,混乱中寻找思路

这一步其实也是最难的一步:找到Unreal中对于这个方法的入口
先写在前头:这里笔者犯下了很多错误,所以也给了我很多启迪: 以下都是一个刚刚接触Unreal的菜鸟对自己的告诫。

  • 首先去寻找现成的网上方法,不要一开始就沉到Unreal的成堆的工具里。一开始在网上寻找实现方案可以很快的帮你缩小范围
  • 缩小范围后最好的实践是直接在引擎内寻找官方的实现方法,他们才是最好的老师,推荐使用直接通过寻找文字的方式寻找想要找的Class,这样加载速度更快,而不是寻找引用 Find Reference。具体的方法是选中后按 Ctrl + Shift + F
  • 当你以为你找到了工具function的时候,千万别急着用,一定要看看源码中具体是怎么使用的。我之前误打误撞的找到了一个我认为完美的缩略图的渲染代码:通过take in 一个 UObject 返回一个 UTexture2D , 一切看起来都是那么的合理(直到我了解了Thumbnail具体怎么渲染的之后…)。我废了九牛二虎之力才把这个API接近来甚至为此修改了引擎源码,但是结果这个并不能渲染缩略图,而是寻找Cache中已经存好的缩略图,这导致我将近一天的功夫全部白费。
  • 最后也是最重要的:耐心。当你进入Unreal的源码引擎的海洋,你会被层层的引用吓跑,你会被上千上万行的代码震惊。但是你同样可以看到的是前人对于对于案例周到的思考,优雅的代码书写格式。所以耐下心,打开Visual Studo的 Auto 和 Call Stack , 使用断点一步一步的调试,寻找Stack中的数据,反复验证自己的猜想
对于这个具体的问题,如果想要深入研究的话,笔者推荐从Unreal自带的自定义缩略图渲染引擎开始,看看实现的方式。比如植被的自定义缩略图渲染器,可以查看 UFoliageType_ISMThumbnailRenderer 是具体怎么实现自定义渲染的。
在这个阶段,我了解到了几个重要的文件和Function:

  • ObjectTool.cpp
  • ThumbnailManager.cpp
  • ThumbnailHelpers.cpp
  • AssetThumbnail.cpp
然后真正等我具体了解到是怎么回事还是等到了最后。
暂时的放弃,通过蓝图直接解决

在笔者忙乎了很久依旧没有得到具体的渲染方法。 而美术那边看我迟迟没有动静,所以尝试和我提出另一个Apporach。 当然这是另一个坑,也是一个到笔者正在写的时候还没有填上的坑。具体的方法是直接在蓝图自继承的Class(Parent Class)里通过代码的方式添加一个 Static Mesh组件。但是因为资产不可修改等特点,这个方法最后是没有完整实现,这个坑可能在未来笔者更加了解Unreal后再填上(2022.08)
回归到起点,小小的突破自我

和我的头头聊过之后,他坚持让我使用自定义渲染器的方式实现,他希望我可以沉下心仔细的看代码调用,研究到底是那里出了问题。所以笔者狠下心仔细的一步步调试,最后在结束时限前完成了这个Feature。 总的来说还是从 ”了解什么是耐心“ 到 ”体会了什么是耐心“ 的一个心态转变 ,然而这一步确实是从学院派出来的程序员需要自我突破的一个坎。
回到程序上:
前文提及的四个重要文件里面包含的这些函数在我看来就是解开这个问题的关键:  这里罗列这些函数只是为了想要深入的人去参考,当然最好的方式是直接通过一个引擎内现有的自定义渲染器,从 Draw() 开始逐步调试

  • ObjectTool.cpp

    • ThumbnailTools::RenderThumbnail()

  • ThumbnailManager.cpp

    • UThumbnailManager::Initialize()
    • UThumbnailManager::InitializeRenderTypeArray()
    • UThumbnailManager::GetRenderingInfo()
    • UThumbnailManager::RegisterCustomRenderer()
    • UThumbnailManager::UnregisterCustomRenderer()

  • ThumbnailHelpers.cpp
  • AssetThumbnail.cpp

    • FAssetThumbnailPool::Tick()

以上这些function都可以从任意的Draw的call stack中看到
具体的Thumbnail的运作机制我建议参考前文提到的文章 他比笔者讲的深入多了。
我所注意到的是在具体的Custom renderer里怎么去写 CanVisualizeAsset() 和 Draw()
这里我的建议是:

  • 首先去找到自己的定义的renderer是否真的被注册了

    • 你可以通过调试 void UThumbnailManager::RegisterCustomRenderer() 来看是否真的注册成功,注意有一个TArray 存储了所有的注册的Class




这是所有注册的缩略图渲染,如果注册成功的话应该可以在这里找到自己注册的class信息


  • 然后你可以看看 UThumbnailManager::GetRenderingInfo() 是否tick到了你的Object,如果tick到了,看是否返回到了你的自定义渲染器。当返回成功后也需要看看CanVisualizeAsset() 和 Draw() 是否正常工作。
<hr/>解决方案

源码

下面的源码是经过修改掩盖的源码,并不能直接使用,请参考源码然后自己调试自己的源码,这里仅供思路的提供
头文件
//MyCustomThumbnailRenderer.h
#pragma once

#include "CoreMinimal.h"
#include "ThumbnailRendering/ThumbnailRenderer.h"
#include "ThumbnailRendering/DefaultSizedThumbnailRenderer.h"
#include "ThumbnailRendering/BlueprintThumbnailRenderer.h"
#include "MyCustomThumbnailRenderer.generated.h"

class FCanvas;
class FRenderTarget;

UCLASS()
class XXX_API MyCustomThumbnailRenderer : public UBlueprintThumbnailRenderer
{
    GENERATED_BODY()

        MyCustomThumbnailRenderer (const FObjectInitializer& ObjectInitializer)
        : Super(ObjectInitializer)
        , ThumbnailScene(nullptr)
    {}

    // UThumbnailRenderer implementation
    virtual void Draw(UObject* Object, int32 X, int32 Y, uint32 Width, uint32 Height, FRenderTarget*, FCanvas* Canvas, bool bAdditionalViewFamily) override;
    virtual bool CanVisualizeAsset(UObject* Object) override;
    // UObject implementation
    virtual void BeginDestroy() override;

private:
    class FStaticMeshThumbnailScene* ThumbnailScene;

};
Cpp文件
//MyCustomThumbnailRenderer.cpp
#include "MyCustomThumbnailRenderer.h"

#include "Misc/App.h"
#include "ShowFlags.h"
#include "SceneView.h"
#include "ThumbnailHelpers.h"

#include "MyModule/TheModuleClass.h"

bool MyCustomThumbnailRenderer::CanVisualizeAsset(UObject* Object)
{
    UBlueprint* Blueprint = Cast<UBlueprint>(Object);

    if (Blueprint && Blueprint->ParentClass.Get() == TheModuleClass::StaticClass()) //这里主要是判断是不是对应注册的Class,因为所有的蓝图其实都会pass到这个地方来
    {
        return true;
    }

    return Super::CanVisualizeAsset(Object);
}

void MyCustomThumbnailRenderer::Draw(UObject* Object, int32 X, int32 Y, uint32 Width, uint32 Height, FRenderTarget* RenderTarget, FCanvas* Canvas, bool bAdditionalViewFamily)
{

    UBlueprint* Blueprint = Cast<UBlueprint>(Object);
    TheModuleClass* TheModuleType= Cast<TheModuleClass>(Blueprint->GeneratedClass.GetDefaultObject()); //这一步是从蓝图里获取原始数据的关键,可能有更好的实现,但是这是我一步步调试出来的

    if (TheModuleType&& TheModuleType->GetStaticMesh())
    {
        if (ThumbnailScene == nullptr)
        {
            ThumbnailScene = new FStaticMeshThumbnailScene();
        }

        ThumbnailScene->SetStaticMesh(TheModuleType->GetStaticMesh());
        //ThumbnailScene->SetOverrideMaterials(FoliageType->OverrideMaterials); //这一步主要是我们的数据里并没有提供相应的贴图
        ThumbnailScene->GetScene()->UpdateSpeedTreeWind(0.0);

        FSceneViewFamilyContext ViewFamily(FSceneViewFamily::ConstructionValues(RenderTarget, ThumbnailScene->GetScene(), FEngineShowFlags(ESFIM_Game))
            .SetTime(UThumbnailRenderer::GetTime())
            .SetAdditionalViewFamily(bAdditionalViewFamily));

        ViewFamily.EngineShowFlags.DisableAdvancedFeatures();
        ViewFamily.EngineShowFlags.MotionBlur = 0;
        ViewFamily.EngineShowFlags.LOD = 0;

        RenderViewFamily(Canvas, &ViewFamily, ThumbnailScene->CreateView(&ViewFamily, X, Y, Width, Height));
        ThumbnailScene->SetStaticMesh(nullptr);
        ThumbnailScene->SetOverrideMaterials(TArray<class UMaterialInterface*>());
    }
}

void MyCustomThumbnailRenderer::BeginDestroy()
{
    if (ThumbnailScene != nullptr)
    {
        delete ThumbnailScene;
        ThumbnailScene = nullptr;
    }

    Super::BeginDestroy();
}
模块定义
// MyModule.cpp
// 多余的看自己的定义,不过这是主要需要注册的步骤

void FAnimatedTextureEditorModule::StartupModule()
{
    //Register thumbnail renderer
    //这里主要是先Unreg了原有的蓝图renderer然后替换了所有蓝图的renderer为自己的custom renderer
    UThumbnailManager::Get().UnregisterCustomRenderer(UBlueprint::StaticClass());
    UThumbnailManager::Get().RegisterCustomRenderer(UBlueprint::StaticClass(), MyCustomThumbnailRenderer::StaticClass());
}

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-4-28 22:44 , Processed in 0.529410 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表