前言

  最近接到一个新需求,美术那边经常需要导出模型的 UV 快照进行流光等效果的制作
  但hexo是 Unreal 可以查看 UV 却无法把 UV 图导出来。
  所以他们只好将引擎的资源导出到 DCC 里面使用 UV 快照输出 UV 图信息。

  经过我的研究,可以利用 shader 展开 uV 然后用 SceneCapture 获取展平 UV 的效果。
  最后用 Unreal 的 API 输出 UV 图即可~

实现思路

  首先需要实现一个 UV 平铺的材质,这是实现效果的关键。
  最初我也没有什么想法,后来咨询了 胡洋 之后,有个教程网站有相应的实现。

https://www.raywenderlich.com/6817-dynamic-mesh-painting-in-unreal-engine-4

alt

  文章里面通过 shader 可以将角色拆成 UV 平铺的形式。
  可以利用这个方法做一个简单的材质。

alt

  从材质的预览窗口也可以看到 UV 展平的效果很好地实现了。
  接下来只需要构建一个相机从上往下 Capture UV 图即可。


  接下来需要构建一个用于拍屏的蓝图。

alt

  蓝图里面添加三个 component , capture相机 StaticMeshComponent SkeletalMeshComponent

alt

  相机需要设置为正交模式避免透视影响

alt

  创建一个 RenderTarget 获取相机 Capture 的信息。

alt

  最后需要注意,capture 组件的大小和材质的大小要保持一致,否则渲染出来的图片不会刚好占满了。

  接下来我们已经可以在蓝图验证 Capture 的效果了

alt

  将模型附着,加上之前做好的展开 UV 的材质,就可以完美渲染出 UV 快照。
  下一步可以从 RenderTarget 创建一个 静态的贴图 然后将贴图导出到对应的目录即可~

自动化 Capture

  上述的方案在蓝图即可完成,只是蓝图默认打开并不在 Viewport 的标签页下。导致无法触发界面的更新。
  于是我想到将蓝图放到场景里面进行渲染即可~

  自动化输出需要解决一个问题,需要等待图片输出到 RenderTarget 才能进行到下一步的输出。
  因此需要用到 Python 的延时代码触发的功能,用原生的 Python 多线程就有 Timer 类可以用。
  不过 Qt 的 QTimer 用起来更加方便简单。

完整的代码在 PyToolkit 仓库里 链接


1
2
3
4
5
6
# NOTE 记录和隐藏所有 Actor 的显示
vis_dict = {}
for actor in level_lib.get_all_level_actors():
vis = actor.is_temporarily_hidden_in_editor()
vis_dict[actor] = vis
actor.set_is_temporarily_hidden_in_editor(True)

  Capture 之前需要将场景里所有的 Actor 隐藏起来,并且记录隐藏的状态, Capture 结束之后恢复显示。

1
2
3
4
5
6
defer = QtCore.QTimer.singleShot
delay_time = 1000

for i, mesh in enumerate(meshes):
defer(delay_time * i, partial(cls.capture, mesh))
defer(delay_time * (i + 1), partial(cls.on_finished, vis_dict))

  接着利用 singleShot 延时调用 capture ,等待 1000 毫秒基本就足够了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@classmethod
def capture(cls, mesh):
capture_actor = level_lib.spawn_actor_from_object(BP, unreal.Vector())
capture_comp = capture_actor.get_editor_property("capture")

if isinstance(mesh, unreal.StaticMesh):
static_comp = capture_actor.get_editor_property("static")
static_comp.set_editor_property("static_mesh", mesh)
materials = get_static_materials(mesh)
comp = capture_actor.get_editor_property("static")
elif isinstance(mesh, unreal.SkeletalMesh):
skeletal_comp = capture_actor.get_editor_property("skeletal")
skeletal_comp.set_editor_property("skeletal_mesh", mesh)
materials = get_skeletal_materials(mesh)
# NOTE 重新获取才可以设置 override_materials
comp = capture_actor.get_editor_property("skeletal")

override_materials = [UV_MAT] * len(materials)
comp.set_editor_property("override_materials", override_materials)

capture_comp.capture_scene()
# NOTE 等待资源更新
defer(delay_time / 2, partial(cls.on_capture_finish, mesh, capture_actor))

  capture 函数会将内置的蓝图生成到 场景里面,并且将模型和材质配置到蓝图相应的 component 上
  然后调用 capture 组件的 capture_scene 来更新 RenderTarget
  最后还是要等待 半 秒,将获取的图输出到对应目录上。

1
2
3
4
5
6
7
8
9
10
11
12
@classmethod
def on_capture_finish(cls, mesh, capture_actor):
# NOTE 生成 2D 贴图
name = os.path.basename(mesh.get_outer().get_name())
texture = render_lib.render_target_create_static_texture2d_editor_only(RT, name)

# NOTE 导出图片
exporter = unreal.TextureExporterTGA()
output_path = os.path.join(cls.export_dir, "%s.tga" % name)
export_asset(texture, output_path, exporter)
capture_actor.destroy_actor()
asset_lib.delete_asset(texture.get_path_name())

  capture 完成的输出从 RenderTarget 生成静态图
  然后将贴图导出到桌面上,最后删除生成的 Actor 和 静态图 资源。

总结

  这个方案有几个问题。
  一个是需要用到 Unreal 的资产,高版本的资产无法兼容老版本。
  第二个是,第一次拍屏的时候,shader可能需要进行编译,导致拍屏效果为空。
  最后就是延时 1 秒的方案,还是有些许不确定性,如果需要渲染一个复杂的模型可能 capture 时间不够。