admas python二次开发 python做cad二次开发_admas python二次开发


前言

上个月的这个时候我写了一篇文章关于如何嵌入 PySide 调用 Qt 的 GUI 开发。 链接

Python 虽然很好,但是有些功能,并没有从 C++ 里面暴露出来。

这种情况就需要通过 C++ 的蓝图开发来将这部分的功能进行暴露。

这样 Python 基本上可以做任何 Unreal 的事情。

如何开发蓝图库也基本可以参照上篇文章提到的 Unreal Python 教程。 链接

为什么需要开发 C++ 蓝图

上面的视频链接有很详细如何通过 Unreal C++ API 开发一个 Unreal 的蓝图,暴露给 Python 调用。

Unreal 的 Python 插件其实已经将 Unreal 内置的所有蓝图暴露给了 Python。

因此 蓝图 能够做到的事情, Python 是绝对可以做到的。

而且经过一个多月的使用来看, Python 的 API 文档做得比 蓝图 的 文档要好很多。

有时候直接查 Python 的 API 反而更有效率,甚至会发现一些其他插件所引入的蓝图。

那么 Python 相较于 蓝图 的有什么异同?

我目前的使用感受来看,除了失去图形节点编程的可视化之外,基本上碾压蓝图,当然运行效率上没有测试过。

蓝图 和 Python 的定位有很大的不一样。

蓝图可以作为游戏运行逻辑的一部分, Python 只能当做编辑器的自动化工具。(Python 效率太低了,运行脚本宁愿用 lua 调 C++)

蓝图自身有它的优缺点,效率比不过 C++ 链接

但是图形化编程,对于非 coding 人员很友好,而且一些简单的逻辑也比较直观。

但是复杂蓝图的连线还是太让人劝退了。

Python 对于像我这种工具向开发的 TA 来说太友好了,毕竟很多 DCC 都使用 Python 。

Unreal 的 Python 插件大部分是对 蓝图 的分装,基本上蓝图有的功能都可以通过 Python 来调。

同时 Python 还可以实现一些神奇的功能,比如说通过 Python 开发一个蓝图节点 ,调用 Python 的第三方库诸如 PySide 包,或者调用系统的 cmd 或者 shell 命令。

因此从引擎的提升来说, Python 的确在这方面更胜一筹,复杂逻辑通过代码看也比较直观。

当然很明显, 蓝图不能实现的引擎操作,基本上也不用指望 Python 能够调用什么 API 来实现了。

这种情况下就需要 C++ 来扩展蓝图,实现 Python 调用。

C++ 开发蓝图库插件

我们目前的需求并不是开发游戏调用的蓝图,因此我们可以开发一个蓝图库插件。

这样可以轻易将这些蓝图迁移到不同的项目里面去。

Unreal 搭建蓝图库开发其实并不难,按照官方的指引去做即可。

首先需要创建一个 C++ 工程,如果是蓝图工程是无法写 C++ 代码的。

然后打开插件面板,选择 New Plugin


admas python二次开发 python做cad二次开发_python开发cad插件_02


然后引擎就会自动创建一个基础插件的模板出来。

后续就是在这个基础模板上调用 C++ 的 API 实现一些特殊的功能。


admas python二次开发 python做cad二次开发_admas python二次开发_03


unreal C++ 插件注意事项

插件的默认结构是 .uplugin 文件加 Resource 和 Source 文件夹。

uplugin 就是一个 Json 配置,配置了插件在引擎的插件列表的显示,以及加载方式。

Resource 存放插件显示的图标。

Source 存放的是 C++ 源码了。


前面两个不需要太过关注,重点的 Source 文件夹的东西。

里面会有 *.Build.cs 文件以及 Public 和 Private 文件夹。

*.Build.cs是 C# 代码,通过虚幻的 Reflect 机制生成 Intermediate 的中间代码用来编译生成 dll。

Public 默认存放头文件

Private 默认存放cpp源码


引用了引擎内部的一些库,需要在 build.cs 文件里面添加上。

否则编译的时候回报某些类型无法识别的错误。

试过排查这种小错误花了我大半天。


admas python二次开发 python做cad二次开发_python开发cad插件_04


前面两个部分是添加路径的,用来缩短头文件索引的路径长度。

后面的 Private 和 Public Module 则是最重要的索引头文件的,必须要在这里配置才能在 c++ 里面调用。

这里怎么填写可以参考引擎 Source 源码下的文件夹名称。

cs 里面配置就可以找 Source 源码的一些头文件进行引用了。

因为虚幻开源了,所以内部 Private 和 Public 没有什么区别,也可能是我的 C++ 造诣还不够。

配置头文件就可以愉快地使用官方提供的一些 C++ 了。

C++ 实现 Add Component 蓝图功能

这个功能看似非常简单,奈何 Python 就是实现不了。


admas python二次开发 python做cad二次开发_admas python二次开发_05


就是给现有 Actor 添加新的 Component 组件而已。

但是查了 API 文档,即便使用 Attach 相关的方法,也无法新的 Component 添加到 Actor 上。

应该说 Python 的操作没有问题, Component 也加上了,可以通过 Python 获取到,但是 Component 没有注册,无法在 UI 上显示出来。

经过我查阅大量网上的资料之后,只在论坛上找到了一个通过 C++ 实现的方案。 链接

这段代码里面有很关键的 RegisterComponent 的操作。

而这些操作并没有暴露到 Python 或者说 蓝图 里面。

当然这个添加 Component 的方法估计也和 Unreal 的机制有关,我对 Unreal 引擎还不是很熟,就不做无关的揣测了。


admas python二次开发 python做cad二次开发_python开发cad插件_06


Python 的文档在 Actor 的部分有所涉及。

不过这个问题就非常蛋疼,

unreal.EditorLevelLibrary.get_all_level_actors_components 可以获取所有注册的 Component

Actor 也可以删除现有的 Component ,偏偏无法添加新的 Component


C++ 的部分我简化了上面回答的代码。

如果没有传入具体的 Component 类型就返回 None 给 Python 就好了。


UActorComponent* URedArtToolkitBPLibrary::AddComponent(AActor* a, USceneComponent *future_parent, FName name, UClass* NewComponentClass)
{

 UActorComponent* retComponent = nullptr;
 if (NewComponentClass)
 {
  UActorComponent* NewComponent = NewObject<UActorComponent>(a, NewComponentClass, name);
    
  FTransform CmpTransform;// = dup_source->GetComponentToWorld();
    
  //NewComponent->AttachToComponent(sc, FAttachmentTransformRules::KeepWorldTransform);
    
  // Do Scene Attachment if this new Comnponent is a USceneComponent
  if (USceneComponent* NewSceneComponent = Cast<USceneComponent>(NewComponent))
    
  {
   if (future_parent != 0)
    NewSceneComponent->AttachToComponent(future_parent, FAttachmentTransformRules::KeepWorldTransform);
    
   else
    NewSceneComponent->AttachToComponent(a->GetRootComponent(), FAttachmentTransformRules::KeepWorldTransform);
    

   NewSceneComponent->SetComponentToWorld(CmpTransform);
    
  }
  a->AddInstanceComponent(NewComponent);
    
  NewComponent->OnComponentCreated();
    
  NewComponent->RegisterComponent();
    

  a->RerunConstructionScripts();
    
  retComponent = NewComponent;
 }

 return retComponent;
}


头文件怎么去 #include ,我基本就是用 VScode 搜索引擎源码,查找头文件的位置,然后逐个添加。

C++有点麻烦的地方就是 cpp 代码写完之后还要将函数注册到 头文件 里面

不过基本上复制 cpp 的函数第一行就可以了,只需要把 :: 前面的类名删除掉而已。

下面就是点击 VS 上面的 本地 windows 调试,编译插件并启动项目。

我用 VS2017 编译经常遇到 clxx.dll 命令行过长 的错误。

网上了查了要将项目的编译改为 Release 版本,或者升级到 VS2019 才可以解决。(网上查到这个是 VS 的 BUG)

后来我是随便将一些 Intermediate 文件夹删除,然后重新调用 UnrealHeaderTool 生成反射代码就不会有这个编译报错了。

完成到这里基本可以参照老外的教程,使用 Python 可以在 unreal 库下找到刚才蓝图扩展的类的,类下面就由刚才扩展的 函数 了。

行数名称自动将 C++ 的驼峰转为 Python pep8 规范的 sneak 写法。

C++ 蓝图获取当前 Sequencer 选择的元素

上面介绍了 C++ 的编写的流程,就不再追溯,这里着重看蓝图的实现。

我最近有一个需求是要获取当前打开的 Sequencer 里面的元素,然后进行 FBX 导出。

但是查遍了 Unreal 的 Python 文档也没有找到这个方法。

对了这里记录一个天坑,之前被坑惨了的。

Unreal Python 的老外教程里面也记录一些使用 Sequencer 处理的 Python 方案。

但是我发现到我调用这些 API 的时候, Unreal 居然报错找不到这些 API。

然后我就以为是我当前 Unreal 版本出 BUG 了,或者是官方删除了这个功能。

后来折腾了好久之后才发现,我没有开启 Sequencer Scripting 插件,所以那些调用蓝图没有加载_(:з」∠)_

我当时还不知道 Python 调用的就是蓝图, 踩了这个坑才有了深刻的认识。


回到这里要实现的功能,我查了 C++ 相关的问题,总算是找到了一个比较可靠的回复。 链接

于是我就抄了这里的代码。

不过上面的代码有点旧,其中 IAssetEditorInstance* AssetEditor = UAssetEditorSubsystem().FindEditorForAsset(LevelSeq, true); 编译会报错

修改为 IAssetEditorInstance* AssetEditor = GEditor->GetEditorSubsystem()->FindEditorForAsset(LevelSeq, true); 解决问题。

经过修改之后上面的代码可以获取到当前 Sequencer 打开的 LevelSequence

原理也不复杂,就是遍历项目所有的 LevelSequence 然后找到那个开启了 Editor 的 LevelSequence

然后在从这个 LevelSequence 里获取 Editor 再从 Editor 获取 Sequencer。

虽然这个遍历有点不太合理,但是我在测试的项目上还是很奏效的。

但是当我将代码编译放到我们正在开发的项目上之后,出现了大问题。

项目有大量的 LevelSequence ,遍历需要很长的时间,并且遍历之后大量的材质启动了编译,导致电脑很卡。


于是我又查了一下 C++ API 文档,发现有个很有用的函数 GetAllEditedAssets。

这个函数可以获取当前打开在编辑器里面的 Assets ,能打开的 Asset 肯定就那么几个。

这样找 Editor 的速度可就快多了。


ULevelSequence* URedArtToolkitBPLibrary::GetFocusSequence()
{
 UAssetEditorSubsystem* sub = GEditor->GetEditorSubsystem<UAssetEditorSubsystem>();
    
 TArray <UObject*> assets = sub->GetAllEditedAssets();
    

 for (UObject* asset : assets)
 {
  IAssetEditorInstance* AssetEditor = sub->FindEditorForAsset(asset, false);
    
  FLevelSequenceEditorToolkit* LevelSequenceEditor = (FLevelSequenceEditorToolkit*)AssetEditor;

  if (LevelSequenceEditor != nullptr)
  {
   ULevelSequence* LevelSeq = Cast<ULevelSequence>(asset);
    
   return LevelSeq;
  }
 }
 return nullptr;
}


上面只是找 LevelSequence ,还需要找当前 LevelSequence 里面选择的元素。

好在 Sequencer 提供了 GetSelectedObjects 的方法

通过 LevelSequence 可以获取到 Sequencer


TArray<FGuid> URedArtToolkitBPLibrary::GetFocusBindings(ULevelSequence* LevelSeq)
    
{
 IAssetEditorInstance* AssetEditor = GEditor->GetEditorSubsystem<UAssetEditorSubsystem>()->FindEditorForAsset(LevelSeq, false);
    

 FLevelSequenceEditorToolkit* LevelSequenceEditor = (FLevelSequenceEditorToolkit*)AssetEditor;
 TArray<FGuid> SelectedGuid;
    
 if (LevelSequenceEditor != nullptr)
 {
  ISequencer* Sequencer = LevelSequenceEditor->GetSequencer().Get();
    
  Sequencer->GetSelectedObjects(SelectedGuid);
    
  return SelectedGuid;
 }
 return SelectedGuid;
}


这样获取返回的是 Guid , Python 有 Guid 类。

可以通过 LevelSequence 的 get_bindings 方法获取 sequence 相关的 binding

再调用 get_id 方法获取 guid ,然后通过 C++ 的蓝图将获取到的 id 筛选一遍。


# NOTE 获取当前 Sequencer 中的 LevelSequence
sequence = unreal.RedArtToolkitBPLibrary.get_focus_sequence()
# NOTE 获取当前 Sequencer 中选中的 Bindings
id_list = unreal.RedArtToolkitBPLibrary.get_focus_bindings(sequence)
bindings_list = [binding for binding in sequence.get_bindings() if binding.get_id() in id_list]


这样就获取到了当前选择的 SequencerBindingProxy 类。

通过 unreal.SequencerTools.export_fbx 就可以将选择的元素导出 FBX 了。


import unreal
from Qt import QtCore, QtWidgets, QtGui

def alert(msg=u"msg", title=u"警告", button_text=u"确定"):
    # NOTE 生成 Qt 警告窗口
    msg_box = QtWidgets.QMessageBox()
    msg_box.setIcon(QtWidgets.QMessageBox.Warning)
    msg_box.setWindowTitle(title)
    msg_box.setText(msg)
    msg_box.addButton(button_text, QtWidgets.QMessageBox.AcceptRole)
    unreal.parent_external_window_to_slate(msg_box.winId())
    msg_box.exec_()

def unreal_export_fbx(fbx_file):
    # NOTE 获取当前 Sequencer 中的 LevelSequence
    sequence = unreal.RedArtToolkitBPLibrary.get_focus_sequence()
    if not sequence:
        alert(u"请打开定序器")
        return

    # NOTE 获取当前 Sequencer 中选中的 Bindings
    id_list = unreal.RedArtToolkitBPLibrary.get_focus_bindings(sequence)
    bindings_list = [binding for binding in sequence.get_bindings() if binding.get_id() in id_list]

    if bindings_list:
        # NOTE 导出 FBX 文件
        option = unreal.FbxExportOption()
        option.set_editor_property("collision",False)
        world = unreal.EditorLevelLibrary.get_editor_world()
        unreal.SequencerTools.export_fbx(world,sequence,bindings_list,option,fbx_file)
    else:
        alert(u"请选择定序器的元素进行 FBX 导出")
        return


上面就是完整的示例代码。

当然导出的 FBX 是带动画的,还需要将动画处理成带 蒙皮骨骼 的 FBX 。

这个操作我是通过 FBX Python SDK 实现的。

官方的 ExportScene01 包含了蒙皮创建,关键帧处理等等的操作,绝大部分的代码可以照抄。

这里蒙皮转换的需求很简单,因此稍微修改一下就可以用了。

处理完成之后将 FBX 输出到临时目录,然后用 Python 调 windows 命令打开路径。

总结

其实调用 C++ API 并不难,这种程度的操作还没有修改到 Unreal 的底层,很多机制也没有用到,我作为个外行还是可以应付的。

而且 Unreal C++ 本身做了很多工作,比如实现了 垃圾回收,含有智能指针,都降低了开发难度(同时增加了学习的难度)

Unreal 开发比较难受的地方时教程文档各方面都不全, Unity 文档还有代码示例,Unreal 因为开源,基本上就是让你直接看源码_(:з」∠)_

有时候遇到的一些奇奇怪怪的问题还找不到任何网上的提问,就很难受了。

最后引擎编译非常耗时,如果要搞这一块的研究,一定一定要配台好电脑。