前言

  这个东西本来是 Minigame 的时候用来方便搭建场景的工具,
  因为当时的情况,时间紧迫,所以最后没弄成,留下了些许遗憾。

  后来也只是更新了个办成品到 PyToolkit 里面
  随后的开发就一直落下了。

  最近刚好天时地利人和,就把这个遗憾给补上了。

alt

获取 Unreal 资源缩略图

  Placer 我想要实现将 Unreal 内置的资产缩略图整合到 Qt 的 ListWidget 里面。
  如此可以让 Qt 界面的功能性更强。

  但是如何利用 C++ 获取到资源的缩略图呢?

  经过网上搜查,我在 CSDN 找到一篇不错的文章 链接

  Unreal 里面有 ThumbnailTools::RenderThumbnail 可以生成缩略图
  文章里面还用了 GetUncompressedImageData 可以获取到图片的源数据。

1
2
3
4
5
6
7
TArray<uint8> UPyToolkitBPLibrary::GetThumbnail(UObject* MeshObject,int32 _imageRes)
{
// NOTE https://blog.csdn.net/zhangxiaofan666/article/details/97643308
FObjectThumbnail _objectThumbnail;
ThumbnailTools::RenderThumbnail(MeshObject, _imageRes, _imageRes, ThumbnailTools::EThumbnailTextureFlushMode::AlwaysFlush, NULL, &_objectThumbnail);
return _objectThumbnail.GetUncompressedImageData();
}

  通过上面的调用可以获取到一串数字的数组,这个整数的数组让我啪的一下就反应过来,和我以前研究的 Maya 视窗截取有相似之处。 链接 代码链接

1
2
3
4
5
6
7
8
9
10

# NOTE https://gist.github.com/hmasato/b72a95fbadf1c63b56ec
pixel = image.pixels().__long__()
ptr = ctypes.cast(pixel, ctypes.POINTER(ctypes.c_char))
size = width * height * 4
# NOTE ord将字符转换为 0 - 255 ASCII码区间
if number:
return [ord(char) for char in ctypes.string_at(ptr, size)]
else:
return ctypes.string_at(ptr, size)

  当时是从 Maya 的 API 中获取到了字符数据,然后通过 Python 将数据转换为 0-255 的数字
  这次反过来,我是获取到了一系列代表 0-255 的数字了。
  随后我研究了一下 QImage 的类,发现它其实可以直接处理 字符 数据,不需要像之前那样进行处理。 链接

alt

  之前为了将数据传到 Maya API 的 Image 才需要如此大费周折。
  因而这一次换一种思路管理获取的数据。

1
2
3
data = "".join([chr(v) for v in py_lib.get_thumbnail(asset)])
image = QtGui.QImage(data, size, size, QtGui.QImage.Format_RGB32)
widget.setPixmap(QtGui.QPixmap.fromImage(image).scaled(size / 2, size / 2))

  所以这里只要反其道而行之,将获取的数字数组转换为 字符串 即可。

alt

2021-3-30 Python3 更新

  升级 Py3 之后,使用上述的方法将 List 数据转成字符串传到 PySide2 之后会出错。
  首先的问题是 Python3 默认使用 unicode 编码,直接会导致 overflow 错误
  经过我的研究,需要将 unicode 转为 bytes 类型才不会出错,但是直接转 bytes 之后图片就对不上了。

alt

  后来实在没有办法了,请教了吴真大佬,最后得出是利用 struct 模块构建出合适类型的数据从而在长度上匹配,传入的数据才是正确的。

1
2
3
4
5
import struct
data = py_lib.get_thumbnail(asset)
data = struct.pack('='+'B'*len(data), *data)
image = QtGui.QImage(data, size, size, QtGui.QImage.Format_RGB32)
widget.setPixmap(QtGui.QPixmap.fromImage(image).scaled(size / 2, size / 2))

Unreal Python Undo 操作实现

  之前做 Minigame 的时候,遇到了个棘手的问题,当时我通过 Python 的 set_actor_location_and_rotation 的方法来实现物体的吸附。

1
2
3
4
5
6
7
8
# NOTE 获取用户的两个物体选择
origin, target = self.get_selected_actors()
location = origin.get_actor_location()
rotation = origin.get_actor_rotation()
target.set_actor_location_and_rotation(
location, rotation, False, False)
# NOTE 选择 actors
level_lib.set_selected_level_actors([target])

  当时上面的做法无法实现 Unreal 的撤销操作。
  如果用户不想要撤销我就彻底没辙。

  我想起之前看 虚幻官方直播有提及 撤销需要接入 begin_transactionend_transaction 才可以。 Youtube链接 Github链接

  有些时候直接用 上面方法是好使的
  但是上面的移动物体的方法并不管用。
  后来还是在老牌的 UnrealEnginePython 的仓库里面找到了很有用的说明文档 Transactions_API.md

alt

  非常凑巧,说明的例子也是移动物体。
  里面提到需要加上 modify 方法才可以正常调用。
  于是我查了一下官方 Python 文档也有这个方法,而且是配合 transaction 来使用的 链接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

origin, targets = self.get_selected_actors()

# NOTE 配置 undo
# NOTE https://github.com/20tab/UnrealEnginePython/blob/master/docs/Transactions_API.md
sys_lib.begin_transaction("ALIGN", "align selected", None)
for target in targets:
location = origin.get_actor_location()
rotation = origin.get_actor_rotation()
# NOTE 设置 undo
target.modify(True)
target.set_actor_location_and_rotation(location, rotation, False, False)
# NOTE 选择 actors
level_lib.set_selected_level_actors([target])
target.modify(False)
sys_lib.end_transaction()

  结合上面提到的就可以通过 modify 的设置来配置 Actor 的 Undo 了

C++ 直接调用右键菜单的功能

  虚幻有些功能功能藏得很深,用起来也很不方便。

alt

  如上图所示吸附、选择等等功能,调用需要在大段的菜单里面找功能。
  我希望将这些功能整合到一个面板上,方便使用。
  当然这个调用靠 Python 做不到的,我希望通过简单的 C++ 就可以调用大量的这种内置的功能。

  以前要解决这个问题,我通常多需要将引擎内部的源码拷贝到我的蓝图里面进行调用。
  如果说这个需要我这边做一些源码的修改还算合理。
  但是如果只是如上述所说的简单功能,那就很费劲了。
  那么有没有更加简单的方式来调用菜单里面注册好的功能呢?
  当然还是有的,答案也在 Unreal 的 C++ 源码里面。


  之前我有研究过怎么用 C++ 添加菜单或者注册快捷键。
  需要用到 UI_COMMAND 来生成 FUICommandInfo

  然后通过 FUICommandList 的 MapAction 分配调用的方法。
  既然是用 FUICommandList 映射了 FUICommandInfo 执行的方法。
  那么能否只通过上述的两个类就能直接触发函数。
  这样我实现脸触发函数的参数都省了,更加简单。
  后来我翻阅 FUICommandList 的 API 文档发现他有 ExecuteAction 的方法。
  理论上应该可以直接触发注册的 FUICommandInfo 的。
  但是直接搜索 ExecuteAction 会找到很多无关的调用。
  后来我灵机一动,搜索 CanExecuteAction 这个方法应该可以极大缩小范围。

alt

  [^F]CanExecuteAction\(通过正则表达式过滤可以找到我想要的实现效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
bool UPyToolkitBPLibrary::ExecLevelEditorAction(FString Action)
{
FLevelEditorModule &LevelEditorModule = FModuleManager::GetModuleChecked<FLevelEditorModule>(TEXT("LevelEditor"));
auto Actions = LevelEditorModule.GetGlobalLevelEditorActions();
auto Commands = LevelEditorModule.GetLevelEditorCommands();

TMap<FString, TSharedPtr<FUICommandInfo>> ActionMap;

ActionMap.Add("SaveAs", Commands.SaveAs);

// Ctrl + E
ActionMap.Add("EditAsset", Commands.EditAsset);
// Ctrl + shift + E
ActionMap.Add("EditAssetNoConfirmMultiple", Commands.EditAssetNoConfirmMultiple);

// Ctrl + End
ActionMap.Add("SnapOriginToGrid", Commands.SnapOriginToGrid);
ActionMap.Add("SnapOriginToGridPerActor", Commands.SnapOriginToGridPerActor);
ActionMap.Add("AlignOriginToGrid", Commands.AlignOriginToGrid);
// Ctrl + Space
ActionMap.Add("SnapTo2DLayer", Commands.SnapTo2DLayer);
// End
ActionMap.Add("SnapToFloor", Commands.SnapToFloor);
ActionMap.Add("AlignToFloor", Commands.AlignToFloor);
// Alt + End
ActionMap.Add("SnapPivotToFloor", Commands.SnapPivotToFloor);
ActionMap.Add("AlignPivotToFloor", Commands.AlignPivotToFloor);
// Shift + End
ActionMap.Add("SnapBottomCenterBoundsToFloor", Commands.SnapBottomCenterBoundsToFloor);
ActionMap.Add("AlignBottomCenterBoundsToFloor", Commands.AlignBottomCenterBoundsToFloor);

ActionMap.Add("DeltaTransformToActors", Commands.DeltaTransformToActors);
ActionMap.Add("MirrorActorX", Commands.MirrorActorX);
ActionMap.Add("MirrorActorY", Commands.MirrorActorY);
ActionMap.Add("MirrorActorZ", Commands.MirrorActorZ);
ActionMap.Add("LockActorMovement", Commands.LockActorMovement);
// alt + B
ActionMap.Add("AttachSelectedActors", Commands.AttachSelectedActors);

ActionMap.Add("SavePivotToPrePivot", Commands.SavePivotToPrePivot);
ActionMap.Add("ResetPrePivot", Commands.ResetPrePivot);
ActionMap.Add("ResetPivot", Commands.ResetPivot);
ActionMap.Add("MovePivotHereSnapped", Commands.MovePivotHereSnapped);
ActionMap.Add("MovePivotToCenter", Commands.MovePivotToCenter);

ActionMap.Add("AlignToActor", Commands.AlignToActor);
ActionMap.Add("AlignPivotToActor", Commands.AlignPivotToActor);

ActionMap.Add("SelectAll", FGenericCommands::Get().SelectAll);
// Escape
ActionMap.Add("SelectNone", Commands.SelectNone);
ActionMap.Add("InvertSelection", Commands.InvertSelection);
// Ctrl + Alt + D
ActionMap.Add("SelectImmediateChildren", Commands.SelectImmediateChildren);
// Ctrl + Shift + D
ActionMap.Add("SelectAllDescendants", Commands.SelectAllDescendants);

ActionMap.Add("SelectRelevantLights", Commands.SelectRelevantLights);
ActionMap.Add("SelectAllWithSameMaterial", Commands.SelectAllWithSameMaterial);

// SLayersView.h
bool Available = ActionMap.Contains(Action);
if (Available)
{
auto ActionPtr = ActionMap[Action].ToSharedRef();
Available = Actions->CanExecuteAction(ActionPtr);
if (Available)
Actions->ExecuteAction(ActionPtr);
}
return Available;
}

  做了个字典用字符串映射内部的 FUICommandInfo
  这样 Python 传入相应的字符串就可以调用内部的方法了。

1
2
3
import unreal
py_lib = unreal.PyToolkitBPLibrary()
py_lib.exec_level_editor_action("SaveAs")

总结

  以上就是 Unreal Python 的一些调用上的增强。
  其实最近还有一些别的 Unreal Python 探索。
  回头抽空在把 菜单 ToolMenuEntryScript 和 蓝图内添加 Component 给加上吧。