普通视图

发现新文章,点击刷新页面。
昨天以前智伤帝

Maya 拍屏方案汇总

作者 智伤帝
2022年11月28日 21:30

前言

  最近接到了一个需求,又是熟悉的 拍屏工具。
  其实老早之前我就有写过类似的需求,只是表现形式各不相同。
  这里打算将不同的拍屏方案汇总到一起,这样大家可以挑选一个合适的情景的方式完成这个任务。

拍屏方案汇总

Maya Python Publish 检查功能开发

  最早在华强实习的时候,就写过将 Arnold 渲染的界面合成并打开 RV 进行预览。
  背后主要用 renderWindowEditor 命令导出。


https://github.com/FXTD-ODYSSEY/MayaViewportCapture

  后来进入腾讯前,我写了 Maya Viewport Capture 工具。
  那个时候写的比较粗糙,我通过 UI 可以定义几个相机的位置,然后规定进行拍屏。
  当时研究用 Maya 或者 Qt 的 API 将 Viewport 的画面截取下来。
  背后主要用 Maya API M3dViewreadColorBuffer
  Qt 部分其实也是在拿到 Maya 的 MImage 之后转成 QImage 而已。


Maya Python 模型拍屏合并工具

  后来正式工作之后,发现前辈用的是 ogsRender 命令将 Maya Hardware 2.0 输出来。
  相较于 renderWindowEditor 命令不需要打开渲染窗口。


playblast

  实现拍屏有太多的方案,当然最为基础的方法就是使用 playblast 命令。
  建议安装上 QuickTime 这样可以极大压缩 Maya 拍屏的文件大小,同时提升 Maya 拍屏的质量。
  playblast 命令既可以直接生成视频也可以拍屏序列帧。

拍屏需求汇总

  上面提供四种拍屏方案,最常用的时 playblast 方案,因为可以直接输出视频。
  如果是图片序列还需要借助 ffmpeg 等命令行工具将图片序列合成为视频。

  拍屏的需求千变万化,但是有一些点其实大差不差。

  1. 拍屏信息
  2. 镜头角度

  比较常见的信息有 时间,影片的归属名字(比如动画的某一段),影片负责人 等等。
  添加这些信息可以用 headsUpMessage 将相关信息叠加到 Viewport 上。
  但是 headsUpMessage 非常难用,而且字体大小等各种非常不方便自定义。
  要解决这个问题可以用 插件,它通过 OpenMaya API 扩展了 headsUpMessage 的功能。
  作者是 zurbrigg ,只可惜它之前免费的工具现在变成付费了。
  劲爆羊工具盒 里面有拍屏王,它就是通过 ZShotmask VP2 插件,将各种信息贴到屏幕上。
  具体可以在 劲爆羊工具盒 里面找到脚本 resource\tools\MSTools\MST_DATA\plug-ins\zshotmask.py
  当然它是一个 Maya Python 插件,注册之后提供了一个节点,只要设置节点的属性就可以了。


  这个方式可以结合 playblast 解决大部分拍屏的问题。
  但有些情况并不能很好解决,比如我遇到的问题就是,每一帧都要重新矫正一下镜头的位置。
  而且这个矫正还不能单纯使用约束,需要每一帧单独进行计算。
  所以我只能改用 ogsRender 的方式,在后台进行拍屏。

Maya ogsRender 输出序列帧

  使用 ogsRender 输出序列帧只能输出到默认工程 images 文件夹的路径。
  因此要控制 ogsRender 输出的位置只能通过修改工程位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import contextlib
@contextlib.contextmanager
def change_workspace_images(folder):
"""Change maya project images folder temporarily.

Args:
folder (str): Image folder path.
"""
workspace_settings = pm.workspace(q=1, fr=1)
image_index = workspace_settings.index("images")
original_image_folder = workspace_settings[image_index + 1]
pm.workspace(fr=["images", folder])
yield
pm.workspace(fr=["images", original_image_folder])

  我写了一个函数,可以修改输出位置,在修改回去。
  这样我可以输出到任意路径。

Python ThreadPool 多线程后处理

  上面拍屏生成的图片,可以放到 imagemagick 进行图片后处理。
  imagemagick 是 maya 自带的命令行图形处理库。
  在 Maya 2022 之前叫做 imconvert.exe, 2022 之后叫做 magick.exe

  之前也研究过通过 imagemagick 处理图片,真的是拳打 Pillow 脚踢 QImage

ImageMagick 图像处理介绍

  imagemagick 用 C 和 C++ 编写的,非常小巧,而且运行速度很快~
  这里我没有使用 ZShotmask VP2 直接拍屏输出我要的信息,因为有些信息想要通过 imagemagick 叠加到图片上。
  于是我想到可以利用 Pool 线程池的方式多线程后台调用命令行。

  其中 from multiprocessing.dummy import Pool 可以导入 Python 隐藏的线程池。
  这个用起来比起使用 threading 库要简单方便很多。
注: from multiprocessing import Pool 导入进程池, Maya 不太支持这个。
  下面来个实例演示一下多线程调度后处理函数的好处。

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
from multiprocessing.dummy import Pool
from functools import partial
from functools import wraps
import time

def log_time(func=None, msg="elapsed time:"):
if not func:
return partial(log_time, msg=msg)

@wraps(func)
def wrapper(*args, **kwargs):
curr = time.time()
res = func(*args, **kwargs)
print("[{0}]".format(func.__name__),msg, time.time() - curr)
return res

return wrapper

def post_process(index):
time.sleep(0.1)
print("test", index)


@log_time
def multi_thread():
pool = Pool()

results = []
for index in range(10):
time.sleep(0.1)
results.append(pool.apply_async(partial(post_process, index)))

[result.wait() for result in results]
pool.close()
print("done")

@log_time
def sequence_run():
for index in range(10):
time.sleep(0.1)
post_process(index)
print("done")

if __name__ == "__main__":
multi_thread()
sequence_run()

  执行上面的代码

1
2
[sequence_run] elapsed time: 2.201172351837158
[multi_thread] elapsed time: 1.2311382293701172

  最后会得到用线程池的方式可以比直接执行快1倍。
  而且这个代码是 py2 兼容的。
  通过这个方式可以在 Maya 拍屏的时候用多线程调用 imagemagick 来对生成的图像进行处理。
  这样用户几乎感受不到图像后处理的时间。

总结

  以上就是 Maya 各种拍屏方案汇总,使用序列帧的自由度比较高,但是需要 ffmpeg 和 imagemagick 等依赖进行处理。
  简单的需求可以直接用 playblast 加上 ZShotmask VP2 完成。

Unreal Python 导出 MetaHuman 控制器关键帧

作者 智伤帝
2022年6月24日 16:57

前言

  MetaHuman 已经在数字人领域里面相当成熟的解决方案。
  并且 UE 官方开发了源码工程。
  目前 github 上有不少人演示自己套用 MetaHuman 动画的效果。
  于是我自己也尝试着想将它 UE 里面的控制器动画导出来。
  然而却发现行不通。

image.png

  它的控制器关键帧是在 sequencer 里面。
  最初是尝试将 sequencer 的资源全部导出成 FBX。
  然而控制器的关键帧并没有跟随导入到 FBX 当中。

  于是我想到可以用 unreal python 读取关键帧数据导出 json
   Maya 再读取数据设置关键帧到控制器上。

unreal python 导出关键帧

  有思路之后就好办。
  之前我也写过脚本来获取 sequencer 关键帧的。
  需要注意如果想要使用 unreal python 的 API 需要开启相应的 C++ 插件。

image.png

  否则 python 会获取不到相应的 API 报错。

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
# Import built-in modules
from collections import defaultdict
import json
import os

# Import local modules
import unreal

DIR = os.path.dirname(os.path.abspath(__file__))

def unreal_progress(tasks, label="进度", total=None):
total = total if total else len(tasks)
with unreal.ScopedSlowTask(total, label) as task:
task.make_dialog(True)
for i, item in enumerate(tasks):
if task.should_cancel():
break
task.enter_progress_frame(1, "%s %s/%s" % (label, i, total))
yield item


def main():
# NOTE: 读取 sequence
sequence = unreal.load_asset('/Game/Sequencer/MetaHumanSample_Sequence.MetaHumanSample_Sequence')
# NOTE: 收集 sequence 里面所有的 binding
binding_dict = defaultdict(list)
for binding in sequence.get_bindings():
binding_dict[binding.get_name()].append(binding)

# NOTE: 遍历命名为 Face 的 binding
for binding in unreal_progress(binding_dict.get("Face", []), "导出 Face 数据"):
# NOTE: 获取关键帧 channel 数据
keys_dict = {}
for track in binding.get_tracks():
for section in track.get_sections():
for channel in unreal_progress(section.get_channels(), "导出关键帧"):
if not channel.get_num_keys():
continue
keys = []
for key in channel.get_keys():
frame_time = key.get_time()
frame = frame_time.frame_number.value + frame_time.sub_frame
keys.append({"frame": frame, "value": key.get_value()})

keys_dict[channel.get_name()] = keys

# NOTE: 导出 json
name = binding.get_parent().get_name()
export_path = os.path.join(DIR, "{0}.json".format(name))
with open(export_path, "w") as wf:
json.dump(keys_dict, wf, indent=4)

  上面的脚本会定位 MetaHuman 的 sequence 资源,然后导出关键帧的信息为 json

  导出会在脚本目录输出两个 json 文件。
  Maya 可以解析这个这两个 json 将关键帧设置到 控制器上。

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
# Import built-in modules
import json
import os
import traceback

# Import third-party modules
import pymel.core as pm

DIR = os.path.dirname(os.path.abspath(__file__))


def progress(seq, status="", title=""):
pm.progressWindow(status=status, title=title, progress=0.0, isInterruptable=True)
total = len(seq)
for i, item in enumerate(seq):
try:
if pm.progressWindow(query=True, isCancelled=True):
break
pm.progressWindow(e=True, progress=float(i) / total * 100)
yield item # with body executes here
except:
traceback.print_exc()
pm.progressWindow(ep=1)
pm.progressWindow(ep=1)


def main():

# NOTE: 读取数据
with open(os.path.join(DIR, "BP_metahuman_001.json"), "r") as rf:
data = json.load(rf)

attr_map = {"location": "t", "rotation": "r"}
status = "Import Keyframe to metahuman controller"

# NOTE: undo 支持
pm.undoInfo(ock=1)
for channel, frame_list in progress(data.items(), status=status):
# NOTE: 解析 channel_name
has_attr = channel.count(".")

if not has_attr:
# NOTE: 处理 `CTRL_C_eye_parallelLook_4311` 格式
ctrl_name = channel.rsplit("_", 1)[0]
attr = "ty"
else:
parts = iter(channel.split("."))
ctrl_name = next(parts, "")
param = next(parts, "")
axis = next(parts, "")
if not axis:
# NOTE: 处理 `CTRL_C_teethD.Y_4330` 格式
attr = "t"
axis = param
else:
# NOTE: 处理 `CTRL_L_eyeAim.Rotation.Y_4387` 格式
attr = attr_map.get(param.lower())
attr += axis.split("_")[0].lower()

# NOTE: 解析出控制器属性设置关键帧
attribute = pm.PyNode(".".join([ctrl_name, attr]))
for frame_data in frame_list:
frame = frame_data.get("frame")
value = frame_data.get("value")
attribute.setKey(t=frame, v=value)

pm.undoInfo(cck=1)

  加载 unreal 导出的数据。

总结

  其实整个流程不复杂,有思路就很好处理。

❌
❌