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

Unreal PyToolkit 插件

[复制链接]
发表于 2020-11-27 10:50 | 显示全部楼层 |阅读模式
本文章转载自 智伤帝的个人博客 - 原文链接

前言

  仓库已经提前搭建好了。  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([this](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=[255,255,255,255])"
        },
        "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[data['type'].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++ 要牢记,我们只是大自然的搬运工。
  不要接引擎压根都没有的功能需求,这种功能开发通常需要交给程序去搞。
  我们的工作是将引擎的功能整合自动化。
  查源码虽然很麻烦,很绕,但是很多类用法可以参考源代码的

本帖子中包含更多资源

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

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

本版积分规则

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

GMT+8, 2024-12-22 15:26 , Processed in 0.101665 second(s), 27 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

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