前言
最近接到一个新需求,美术那边经常需要导出模型的 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
文章里面通过 shader 可以将角色拆成 UV 平铺的形式。 可以利用这个方法做一个简单的材质。
从材质的预览窗口也可以看到 UV 展平的效果很好地实现了。 接下来只需要构建一个相机从上往下 Capture UV 图即可。
接下来需要构建一个用于拍屏的蓝图。
蓝图里面添加三个 component , capture相机
StaticMeshComponent
SkeletalMeshComponent
相机需要设置为正交模式避免透视影响
创建一个 RenderTarget 获取相机 Capture 的信息。
最后需要注意,capture 组件的大小和材质的大小要保持一致,否则渲染出来的图片不会刚好占满了。
接下来我们已经可以在蓝图验证 Capture 的效果了
将模型附着,加上之前做好的展开 UV 的材质,就可以完美渲染出 UV 快照。 下一步可以从 RenderTarget 创建一个 静态的贴图 然后将贴图导出到对应的目录即可~
自动化 Capture
上述的方案在蓝图即可完成,只是蓝图默认打开并不在 Viewport 的标签页下。导致无法触发界面的更新。 于是我想到将蓝图放到场景里面进行渲染即可~
自动化输出需要解决一个问题,需要等待图片输出到 RenderTarget 才能进行到下一步的输出。 因此需要用到 Python 的延时代码触发的功能,用原生的 Python 多线程就有 Timer 类可以用。 不过 Qt 的 QTimer 用起来更加方便简单。
完整的代码在 PyToolkit 仓库里 链接
1 2 3 4 5 6 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 7 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 24 @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) 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() 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 13 @classmethod def on_capture_finish (cls, mesh, capture_actor ): name = os.path.basename(mesh.get_outer().get_name()) texture = render_lib.render_target_create_static_texture2d_editor_only(RT, name) 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 时间不够。