UE4中3D角色预览和2D头像动态生成
游戏开发中的捕获需求
在游戏开发过程中,对角色的预览,和某些情况下生成头像是比较常见的需求。有些引擎,可以创建一个额外的场景和相机,然后将需要的内容放置到合适的位置,然后可以获得想要的图像。在UE4中不是这样,没法创建其他的场景,我们需要使用CaptureSceneComponent2D,字面意思看出来这是个场景捕获的组件,它就是用来捕获我们想要的场景内容到2D图像上。
CaptureSceneComponent2D如何使用
这个组件的使用相当简单,首先我们新建一个第三人称的示例工程,使用以下基本步骤:
- 创建RenderTarget2D,捕获组件捕获的内容需要渲染到这个纹理上。选择AddNew->Materials & Textures -> RenderTarget来创建,并命名为 RT_Preview。
- 在场景中放入SceneCapture2D,调整位置和朝向,对着角色
- 选中场景中的SceneCapture2D对象,查看Details面板,找到TextureTarget项,然后选择刚刚创建的RT_Preview,选择之后立刻就会发现其中的小预览图就变成场景中的样子。
对,就是这么简单,这个时候我们已经得到了一张捕获的纹理。那么这个纹理如何使用呢?可以跟普通的贴图一样使用,不过稍微有一点点区别,这个稍后再说。
使用RenderTarget2D
为了使用刚刚捕获的图像,我们可以建立一个界面来展示它。
- 创建一个界面蓝图,并放上一个图像控件,并设置图像控件Brush中的图片为之前的RT_Preview,然后在关卡蓝图里面,设置一个按键响应,把这个界面给展示出来。这里选择F键。
2. 运行游戏之后,按F键,发现界面上啥也没有。原来RenderTarget纹理的透明度比较特别(具体是怎样还没有研究),我们回到场景,再次选中SceneCapture2D对象,在Details面板找到Catpture Source
选项,然后选择如下。再次运行我们就可以看到界面中的图像。
只捕获想要获取的对象
从上面的截图可以看出来,角色背后的蓝天及场景中的其他内容都被捕获了。在实际应用中,这些部分一般来说是不需要的。那么如何设置只捕获特定内容呢?
- 依然是SceneCapture2D对象,它的属性中有个参数叫
Primitive Render Mode
, 这个就是用来控制绘制内容模式的,它有个叫做Use ShowOnly List
的模式,这个模式就可以指定一些对象,然后只捕获这部分内容。
使用这个模式的一个小问题是,如果在编辑器中直接选择这个选项,那么TextureTarget中的预览将会消失。所以我们在编辑器中不修改它的模式,而在蓝图中动态设置。
2. 打开场景蓝图,在F按键之后的代码加上SceneCapture2D的一些设置
结果如下:可以看到场景部分都消失了,不过还有蓝的白的背景在。这部分内容应该是属于透明内容。下一节我们再把它给去除掉。在这里我们使用了Show Only ActorComponents
即只捕获特定的Actor对象,也可以使用Show Only Components
节点来添加指定的组件,可以做到比Actor更细化的控制。后面还会讲到。
去除不透明的背景,创建UI材质
- 前面有讲到将CaptureSource给设置成了
Final Color(LDR) in RGB
,这个设置实际上是将Alpha属性给丢了,所以我们首先将这个属性重新设置为第一项:
2. 创建一个材质, 命名为 MT_Preview,并打开。
- 把MaterialDomain设置为User Interface,因为这个才是是给UI用的
- 把BlendMode设置Translucent因为我们需要透明属性
- 把之前建的RT_Preview拖入图表中,按如图连线
- 1-x节点叫 OneMinus
3. 回到界面蓝图,之前图片的Image选择的是RT_Preview,是直接使用的纹理,现在改成MT_Preview,使用新建的材质。
4. 再次运行游戏,按F键可以看到如下画面,背景中的蓝白色已经去除了。
到了这里,我们捕获的内容基本上已经符合了预期:
- 只捕获特定的角色
- 背景是透明的,便于在界面上显示
然而,它还不能正直使用在游戏中,为什么?
改进
目前我们捕获的对象是场景中的玩家对象,玩家对象会移动,一旦移动我们就捕获不到了。这个问题有两个解决方法,
- 第一个是可以把捕获控件绑在玩家身上,跟着玩家一起动。这个方案也还有问题,那就是玩家会播很多动作,可能并不是你想要捕获的,除了特殊使用目的,不建议使用。
- 创建一个专门的捕获Actor,在其中放入特定的Mesh,然后捕获组件只捕获自己Actor中的指定Mesh或者特定Actor。此方案需要管理一个临时Actor。
接下来就以第二个方案来简述实际操作。
- 创建Preview的Actor:创建一个蓝图,继承Actor,命名为A_Capture_Preview。并添加SceneCaptureComponent2D组件和SkeltalMesh组件。SceneCaptureComponent2D设置TextureTarget为RT_Preview,SkeltalMesh的Mesh设置为SK_Mannequin,并调整位置和朝向如下图所示
2. 去除之前场景中添加的那个SceneCapture2D对象,我们使用动态创建的方法来构建Preview Actor。首先在A_Capture_Preview的BeginPlay中设置只捕获自己的本身
3. 然后我们修改一下Level Blueprint,创建这个捕获Actor,CollisionHandlingOverride可以选择AlwaysSpawn,避免因为某些原因创建失败。
4. 运行游戏,按F键,可以看到界面跟之前展示的一样,不同的是:
- 玩家走动,不会影响界面
- 场景中多了一个模型,这是我们SpawnActor引入的一个模型
我们SpawnActor只是为了用于捕获,不希望主摄像机可以看到,那么这里又有两个方法:
- 把临时Actor放远一点(这个方案有些意外的效果,后面再说)
- 让主摄像机看不到这个临时Actor
我们先来说第二中方案,如何让主摄像机屏蔽或者说过滤掉我们得临时Actor。首先打开ACapturePreview蓝图,选中SkeletalMesh,然后在细节面板上找到Renderring选项卡,点击下面的三角形展开高级选项,如下图:
这里有两个选项,Owner No See(拥有者不可见) Only Owner See(仅拥有者可见) ,利用Owner No See这个选项,再把临时对象的Owner设置成主角Actor,那么主角就看不到这个临时Actor了(对于网络游戏,在客户端创建的临时Actor,其他人本来就看不见,所以也不用担心。)
- 勾选Owner No See
- 在Spawn之后,将Owner设置成主角Character
再次运行游戏,按F之后,已经看不到临时对象了,界面表现则是和之前一样。
这里可能有人要问了,为什么不选择OnlyOwnerSee,然后把Owner设置成自己呢?这里的问题是Owner不能设置成自己,如果要选择OnlyOwnerSee,则必须将模型与SceneCaptureComponent拆成2个Actor来实现,这也是个方法,但是需要管理两个临时对象,具体的实现就大家自行实现吧。
为什么不把临时Actor放远一点来拍摄?
在一定程度上来说,没啥问题,但是如果Actor离主相机太远,引擎会对Actor进行优化,导致捕获的效果非常糟糕,一方面是LOD的原因,一方面未启用LOD的情况下,离的足够用,也会强制优化。
不断的将距离设置的更大之后,可以发现界面上的效果越来越差,直到不能接受。最后一项Actor离主角的距离是40000000。当然你可能不会设置那么远,只要找一个合适的位置让玩家当前看不到他就是可以接受的。如果你发现界面上的效果达不到预期的时候,就要考虑是否因为距离过大导致的。
另外,也是接下来要讲的,使用正交投影来获取2D头像的时候,对距离比透视投影要敏感非常多,距离设置到10000就有明显的区别。
所以推荐的做法是前面说的那种,将临时对象放在离主角比较近的地方,但让主角看不到他。
2D头像的获取
3D预览使用透视投影,2D头像由于离的非常近,如果继续使用透视投影,很容易出现比例不协调的问题,比如鼻子可能非常大,其他部位特别小,不符合要求。
这个时候就要使用正交投影,继续使用刚刚的ACapturePreview对象,选中SceneCaptureComponent2D组件,找到Projection选项卡,ProjectionType选择Orthographic,选择之后OrthoWidth变得可用,这个选项类似Field,越大则范围越大,越小则范围越小。在这个模式下,相机的远近不会改变捕获的范围,需要修改OrthoWidth才有效果。例如下面的选项,设置为50,可以看到TextureTarget部分的预览。
注意红框中的两个选项,PrimitiveRenderMode在前面有提到,为了方便在未运行时预览TextureTarget,这里不做修改,使用蓝图代码在运行时修改模式为ShowOnlyList
。CaptureEveryFrame
是每帧捕获的意思,可想而知这个选项会带来相当的消耗。对于2D头像来说,是静态的,只需要捕获一帧就可以了,所以这里取消勾选。
2D头像在3D的基础上只是两点不同:
- 投影方式修改
- 去除CaptureEveryFrame
接下来,探讨个稍微复杂点的情况。通过上面的介绍大家基本上明白了,捕获的基本流程:
- 创建RenderTarget
- 创建使用RenderTarget的材质
- 界面图像使用指定材质
- 创建临时Actor,捕获图像,更新RenderTarget
这个过程中,我们创建了一个RenderTarget,一个材质,一个临时对象。如果我们在游戏中需要同时显示多个动态捕获的头像怎么办?那就得创建N个RenderTarget,N个材质,N个临时对象?下面就来介绍一种全动态RenderTarget+材质的创建方案,对于捕获Actor,如果各个需要的场景对镜头没有区分要求,那其实可以使用同一个,然后动态的修改SkeletonMesh就可以。
全动态生成2D头像
- 首先我们还是得手动创建一个RenderTarget,这个RenderTarget仅用于在编辑器中预览使用,可以继续使用前面的RT_Preview。材质也可以继续使用MT_Preview,但是需要稍作修改,在材质中的TextureSample节点上右键,并选择
ConvertToParameter
, 这个时候可以在左侧ParameterName出修改参数的名称,这个名称记下来,后面动态设置的时候会用到。
2. 打开WBP_Head蓝图,新建函数UpdateImgMat
,新增Input参数,类型选择为Material Instance Dynamic, 并命名为 ImgMat
并实现函数,此函数可以让我们使用动态材质来填充图像。
3. 打开Level Blueprint,新建一个函数CreateDynamicHead
,这个函数建在哪里不重要,只要有合适的时机即可
这三个节点,首先创建一个RenderTarget2D ,然后根据MT_Preview来创建一个动态材质示例,第三个节点就是把前面的RenderTarget2D传递给材质的MainTex参数。这就是我们第一步把材质中的纹理提升为参数的目的。实际上动态材质的核心就是上面三个节点,接下来需要做两件事:
- 把材质传递给界面控件
- 把RenderTarget传递给捕获Actor
界面已经提供了接口,调用一下即可,ACapturePreview也需要提供一个接口来接受RenderTarget数据。
4. 打开ACapturePreview,新建函数SetRenderTarget,新建参数RenderTarget,类型为TextureRenderTarget2D
。
函数内容也很简单
接下来就是稍微改造一下Level Blueprint中的函数CreateDynamicHead
分成四个步骤:
- 创建界面
- 创建临时Capture对象
- 创建动态的RenderTarget及材质
- 将材质传递给界面对象,将RenderTarget传递给Catpture对象
按键F的响应则只需要直接调用该函数
按F运行游戏,发现头像跟之前没有任何的区别。
没有区别就对了,这里只是展示一种动态构建的方法,效果并没有改变。对于界面上多个头像的展示,跟前面的代码结构类似,不同的界面,使用不同的预览对象,自行设置就好。由于大家各自使用的RenderTarget和材质都是动态的,所以不会相互干扰,这是主要目的。