kavenGw 发表于 2020-11-27 10:50

Unreal PyToolkit 插件

本文章转载自 智伤帝的个人博客 - 原文链接

前言

仓库已经提前搭建好了。github地址
最近一直在忙,没有把文章总结整理好。
这个是基于 C++ 蓝图开发的 Python 插件
有一部分功能通过 C++ API 开发蓝图从而暴露到蓝图里面。
进而可以实现 Python 调用 C++ API 操作 Unreal 底层。
使用官方 Python 插件扩展蓝图比起第三方 UnrealEnginePython 要更加方便。
虽然现在官方的插件还不是很成熟,但是通过蓝图直接暴露 C++ API 的思路的确更加简单。
UnrealEnginePython 里面写扩展还需要处理 Python 的 C++ 部分,对于纯 Unreal C++ 开发来说的确不太灵活。
也难怪官方居然抛弃了相当成熟的第三方 Python 插件。
C++ 蓝图编写

其实蓝图编写我也是参照youtube 上的 unreal Python 教程学习的 youtube地址 B站地址 github地址
我之前编写 FBX 动画导入对比面板的时候也有所提到 链接
还有最近写的一篇文章也有比较详细的描述 Unreal Python 结合 C++ 开发蓝图库插件
看完教程,特别的需求就需要查论坛、查文档和查 Unreal 的 C++ 源码。
在文档方面, Unreal 这的特别不行
不说 C++ 文档各种案例都没有,描述都是参数类型和极其简单的描述。
文档的搜索还经常搜不到 API 信息, Python 的文档都比 C++ 强多了
一般首选查官方的 问答区 和 论坛
关键字用 C++ 或者 blueprint 开头,然后加上具体需求的英文关键字。
但是不要抱太好的期望,虚幻的社区也是不太活跃,有些问题有人问了,也没人回♂
如果上述的方法没有收获,就只能采用比较麻烦的方案了。
用 VScode 查引擎的 C++ 源码 。
VScode 可以直接搜索到文件内容,但是如果匹配很多文件的话就只能一个一个找有用的信息。
通常这种操作都比较麻烦,因为并不是所有的 API 函数都可以暴露到蓝图里面。
有些没有 Unreal 宏设置的内部函数蓝图调用会无法编译通过,后来找来程序帮忙才知道这个原因,我还是太菜了
并没有系统的学习 C++ ,Unreal 的 C++ 编程也还没深入看教程学习,最近一直围绕着 Python 开发解决问题
倒是有查过 大象无形 工具书,但是很多要实现的需求里头并没有,以后找时间要系统地学习一下 大象无形 工具书。
Python Startup 脚本调用

虽然我 C++ 开发很一般,但是抄代码的能力总归还是有的。
我想要实现 C++ 插件自动执行 Content 目录下的 initialize.py 脚本的效果。
官方的 Python 插件提供了 Startup 脚本的方案。
但是配置到官方的插件里并不好,配置拆散不是个事情。
于是我就参照 官方的 Python 插件实现自动加载 initialize.py 的效果。
其实参照了源码真的不复杂。
就是插件启动的时候调用 Unreal 的 Ticker ,触发一次 Tick 之后就停止函数的执行而已。
这样确保界面启动了之后再去执行相关的 Python 脚本,不容易导致错误。
// Copyright Epic Games, Inc. All Rights Reserved.

#include "PyToolkit.h"

#define LOCTEXT_NAMESPACE "FPyToolkitModule"

void FPyToolkitModule::StartupModule()
{
// This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module

// Initialize the tick handler
TickHandle = FTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateLambda((float DeltaTime)
{
Tick(DeltaTime);
return true;
}));

}

void FPyToolkitModule::ShutdownModule()
{
// This function may be called during shutdown to clean up your module.For modules that support dynamic reloading,
// we call this function before unloading the module.

}

void FPyToolkitModule::Tick(const float InDeltaTime) {
// 参考 Python 官方插件 | 引擎初始化完成之后 通过 tick 来初始化 initialize.py 脚本
if (!bHasTicked) {
bHasTicked = true;
FString InitScript = TEXT("py \"") + FPaths::ProjectPluginsDir() / TEXT("PyToolkit/Content/initialize.py") + TEXT("\"");
GEngine->Exec(NULL, InitScript.GetCharArray().GetData());   
}
}

#undef LOCTEXT_NAMESPACE

IMPLEMENT_MODULE(FPyToolkitModule, PyToolkit)
如果不用 Tick 来触发而是直接用 GEngine->Exec 在插件的构造函数执行 Python 代码会直接报错的。
Python 环境配置和初始化

Content 目录下的 Python 目录默认会添加到 Python sys.path 里面
Python 目录里面添加的依赖库如下:
PySideFBX Python SDKdayu_widgetsdayu_pathsingledispatchQt.py
上面是我在 Python 目录下添加的脚本库
initialize.py 脚本初始化
这个部分的代码也在 FBX 动画导入对比面板的文章里面有所提及。
不过需要注意的是当时提到的 __QtAppTick__ 回调函数里面,其实不需要不断执行 QtWidgets.QApplication.processEvent
加上表面上也不会对虚幻有什么实质的影响。
不过这边的程序用了第三方的 imgui 来写虚幻的 UI
结果发现如果加上这行 Qt 代码会导致 imgui 的 gui 组件无法聚焦♂
输入组件的焦点会被抢走。
后面我注释掉那一句也没有对 Qt 的界面产生影响~
json 配置 Unreal 菜单

Unreal 官方论坛的解决方案
在官方论坛可以查到关于 Python 生成菜单的方案。
大佬贴心的贴出了可运行的代码,我的代码也是在这个基础上优化成 json 配置菜单的效果。
section - 配置分组

menu
    section - 设置分组
    label - 配置显示名称
    type - 配置 command 执行的类型 可以填写 python 和 command
    command - 根据 type 配置执行相应的命令
{
    "section":{
      "Model":"建模",
      "Anim":"动画",
      "FX":"特效",
      "Render":"渲染",
      "Help":"帮助"
    },
    "menu":{
      "Model_Tool" : {
            "section": "Model",
            "label": "演示:模型处理工具(打印到屏幕)",
            "type": "PYTHON",
            "command": "unreal.SystemLibrary.print_string(None,'模型处理工具',text_color=)"
      },
      "Anim_Tool" : {
            "section": "Anim",
            "label": "动画导入比较面板",
            "type": "COMMAND",
            "command": "py \"{Content}/Anim/FBXImporter/main.py\""
      },
      "FX_Tool" : {
            "section": "FX",
            "label": "Sequencer 导出选择元素动画为骨骼蒙皮",
            "type": "COMMAND",
            "command": "py \"{Content}/FX/sequencer_export_fbx.py\""
      },
      "SequencerFBX" : {
            "section": "Render",
            "label": "批量渲染 Sequencer 工具",
            "type": "COMMAND",
            "command": "py \"{Content}/Anim/sequencer_batch_render/render_tool.py\""
      },
      "Document" : {
            "section": "Help",
            "label": "帮助文档",
            "type": "PYTHON",
            "command": "import webbrowser;webbrowser.open_new_tab('https://github.com/FXTD-ODYSSEY/Unreal-PyToolkit')"
      }
    }
}
这个是 json 配置部分,自动生成对应分组脚本的脚本。
type_map = {
    "command": unreal.ToolMenuStringCommandType.COMMAND,
    "python": unreal.ToolMenuStringCommandType.PYTHON
}

def read_menu_json(path):

    with open(path, 'r') as f:
      data = json.load(f, object_pairs_hook=OrderedDict, encoding='utf-8')

    menu_section_dict = data['section']
    menu_entry_dict = data['menu']
    for menu, data in menu_entry_dict.items():
      data['type'] = type_map.lower()]
      data['command'] = data['command'].format(Content=DIR)

    return menu_section_dict, menu_entry_dict


def create_menu():
    # NOTE 读取 menu json 配置
    menu_section_dict, menu_entry_dict = read_menu_json("%s/menu.json" % DIR)

    # NOTE https://forums.unrealengine.com/development-discussion/python-scripting/1767113-making-menus-in-py
    menus = unreal.ToolMenus.get()

    # NOTE 获取主界面的主菜单位置
    main_menu = menus.find_menu("LevelEditor.MainMenu")
    if not main_menu:
      raise RuntimeError(
            "Failed to find the 'Main' menu. Something is wrong in the force!")

    # NOTE 添加一个下拉菜单
    script_menu = main_menu.add_sub_menu(
      main_menu.get_name(), "PythonTools", "Tools", "PyToolkit")

    # NOTE 初始化下拉菜单的 Section 分组
    for section, label in menu_section_dict.items():
      script_menu.add_section(section, label)

    # NOTE 根据 json 来配置菜单显示的 Entry
    for menu, data in menu_entry_dict.items():
      entry = unreal.ToolMenuEntry(
            name=menu,
            type=unreal.MultiBlockType.MENU_ENTRY,
            insert_position=unreal.ToolMenuInsert(
                "", unreal.ToolMenuInsertType.FIRST)
      )
      entry.set_label(data.get('label', "untitle"))
      command = data.get('command', '')
      entry.set_string_command(data.get("type", 0), "", string=command)
      script_menu.add_menu_entry(data.get('section', ''), entry)

    # NOTE 刷新组件
    menus.refresh_all_widgets()
c++ 蓝图实现 RenderTargetCube 渲染出 TextureCube

C++ 蓝图实现的功能在上次的 C++ 开发蓝图插件的总结里面有所提及 链接
这里刚好遇到了一个 Python API 没有的需求,于是就再补充讲一讲。
官方 API 提供了 RenderTarget 输出 Texture2D 的方法 链接
但是并没有提供 RenderTargetCube 输出 TextureCube 的方法
但是既然后有 RenderTarget 的操作方法,其实就是输出 RenderTargetCube 基本没有太大区别。
我这里的 C++ 代码就是抄引擎的源码然后稍微修改一下出来的。
UTextureCube* UPyToolkitBPLibrary::RenderTargetCubeCreateStaticTextureCube(UTextureRenderTargetCube* RenderTarget, FString InName)
{
FString Name;
FString PackageName;
IAssetTools& AssetTools = FModuleManager::Get().LoadModuleChecked<FAssetToolsModule>("AssetTools").Get();   

//Use asset name only if directories are specified, otherwise full path
if (!InName.Contains(TEXT("/")))
{
FString AssetName = RenderTarget->GetOutermost()->GetName();   
const FString SanitizedBasePackageName = UPackageTools::SanitizePackageName(AssetName);
const FString PackagePath = FPackageName::GetLongPackagePath(SanitizedBasePackageName) + TEXT("/");
AssetTools.CreateUniqueAssetName(PackagePath, InName, PackageName, Name);
}
else
{
InName.RemoveFromStart(TEXT("/"));
InName.RemoveFromStart(TEXT("Content/"));
InName.StartsWith(TEXT("Game/")) == true ? InName.InsertAt(0, TEXT("/")) : InName.InsertAt(0, TEXT("/Game/"));
AssetTools.CreateUniqueAssetName(InName, TEXT(""), PackageName, Name);
}

UTextureCube* NewTex = nullptr;

// create a static 2d texture
NewTex = RenderTarget->ConstructTextureCube(CreatePackage(NULL, *PackageName), Name, RenderTarget->GetMaskedFlags() | RF_Public | RF_Standalone);   
if (NewTex != nullptr)
{
// package needs saving
NewTex->MarkPackageDirty();   

// Notify the asset registry
FAssetRegistryModule::AssetCreated(NewTex);

}
return NewTex;

}
基本上方法调用为 ConstructTextureCube 即可
总结

作为 TA ,开发 C++ 要牢记,我们只是大自然的搬运工。
不要接引擎压根都没有的功能需求,这种功能开发通常需要交给程序去搞。
我们的工作是将引擎的功能整合自动化。
查源码虽然很麻烦,很绕,但是很多类用法可以参考源代码的
页: [1]
查看完整版本: Unreal PyToolkit 插件