avatar

Hika

A blog focus on GameDev

  • 首页
  • 分类
  • 标签
  • 归档
  • 关于
首页 浅谈不同骨骼的动画的动画逻辑共用
文章

浅谈不同骨骼的动画的动画逻辑共用

发表于 2023/10/13
作者 lynx 12 分钟阅读

对于不同角色或者是不同职业不同性别,策划和美术都会规划不同体型,不同的骨骼和骨骼结构。不同骨骼的动画管理一直是个比较麻烦的事情。项目一般喜欢把这种动画管理的解决方案叫做动画集AnimationSet。但是实际开发中不同骨骼的情况下又会要求它们要共用动画逻辑,比如说人物的Locomotion逻辑共用,各种攀爬运动逻辑共用等,而UE并没有一个完善的共用机制。本文目的是探讨一些共用动画蓝图的方法。

Linked Graph和Linked Layer

AnimationLayer_09.png

Linked Anim Layer和Linked Anim Graph说白了就是类似函数封装,把部分动画逻辑封装到单独的内联蓝图,或者是独立的蓝图中。Linked Anim Layer是通过实现抽象接口,让被共用部分的动画蓝图实现此接口,并且在共用的动画蓝图指定Instance Class,或者在逻辑蓝图调用Linked Anim Class Layers来实现逻辑的共用;Linked Anim Graph则是直接指定被共用动画蓝图来实现共用。两者在实现共用的方面大差不差,具体使用方法可以参考 官方文档。

在面对多骨骼时这种方案在UE5中是没什么问题的,因为UE5有动画蓝图模板。被共用的动画蓝图使用动画蓝图模板则可以被不同骨骼的动画蓝图使用。而UE4没有这项特性,需要修改引擎源码,把Linked Anim Layer和Linked Anim Graph的Instance Class对骨骼不同的动画蓝图的过滤逻辑去掉。

这方案相对来说比较直接,也比较直观(UE4实现相对比较麻烦)。但是这样结构性不会很好,Linked节点有可能会被其他ABP共同维护者滥用。在面对量产型动画,如武器、跟班等骨骼都各不相同的动画,每次导入新骨骼也需要手动链接节点,繁琐且显得工业化程度不足。

动画蓝图继承

UE的动画蓝图本身是支持继承,通过继承的方法可以把共用逻辑都写在父对象动画蓝图中。然后不同骨骼把此动画蓝图作为父对象,并“替换”掉父对象上动画节点所播放的动画为自身骨骼的动画。这种“替换”有各种方案,比较简单直接的就是使用UE自带的Anim Graph Overrides方案,或者父对象使用AnimationSequence变量作为Play Animation Sequence输入,如图:

image-20231016113234258 上图是使用变量作为动画输入,下图则是继承后子蓝图的Anim Graph Overrides

在面对不同骨骼的时候,UE4和UE5都可以直接通过Reparent的方式重新继承到骨骼不一样的父动画蓝图上,并且通过父动画蓝图实现所有动画逻辑。不过UE4有个问题,Anim Graph Overrides指定的覆盖动画时,编辑器会过滤非本骨骼的动画,导致无法选择覆盖的动画资源。需要修改一下引擎源码,过滤代码如下图,可以直接整个函数return false来解决。

image-20231016164419335

UE5因为有动画蓝图模板,所以父动画蓝图设置成动画蓝图模板就不需要如此处理了。

这种方案也有不少问题,其中一个是调试子动画蓝图时,因为子动画蓝图的Anim Graph是没法编辑预览,只能调试父动画蓝图的Anim Graph。而调试父动画蓝图的Anim Graph时,状态机状态,变量的值,动画的流程线都是错的,这是UE的一个Bug,目前在UE4中必现。

另外因为是子动画蓝图的Anim Graph是无法编辑的,所以动画蓝图无法多层继承,也就是说子动画蓝图无法在自己子对象里实现自己的特殊逻辑,只能把逻辑都堆在父动画蓝图里并通过开关的形式来实现动画逻辑差异化。

image-20231016170716820 子动画蓝图只有EventGraph,没有Anim Graph

Native Anim Graph Node

这方案顾名思义就是使用C++并且通过源码来实现动画节点的链接和逻辑运作,个人比较推荐这种方案,或者以这方案为主,配套上面两个方案使用,因为这样能完全掌控动画的共用和继承逻辑,解决无法多层继承问题或者是骨骼不一样导致的过滤等问题,同时也不用考虑动画没有的Fast Path问题,也就是性能问题。同时也不用忍受动画蓝图的调试的各种问题(甚至动画蓝图也可以打Log了),调试可以自己完全掌控。当然缺点是没有用动画蓝图来做节点连接那么直观,迭代效率也没有蓝图节点高。

要使用此方案,首先要创建一个继承于UAnimInstance的自定义AnimInstance类,再创建一个继承于FAnimInstanceProxy的动画代理类。AnimInstance让动画蓝图继承,AnimInstanceProxy在创建Proxy时替换掉默认的类,如下:

1
2
3
4
FAnimInstanceProxy* UWeaponAnimInstance::CreateAnimInstanceProxy()
{
	return new FSMGWeaponAnimInstanceProxy(this);
}

这里稍稍提及一个事,AnimInstanceProxy框架是支持自己实现动画Evaluate的,也就是说你可以不使用节点,不用动画蓝图来实现自己的动画逻辑,同时也能借助它的多线程框架和Skeleton框架的优势,但这个相当于自己开发动画运动实现了。见源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// "Animation/AnimInstanceProxy.h"
// Evaluate native code if implemented, otherwise evaluate the node graph
void FAnimInstanceProxy::EvaluateAnimation_WithRoot(FPoseContext& Output, FAnimNode_Base* InRootNode)
{
	DECLARE_SCOPE_HIERARCHICAL_COUNTER_FUNC()

	ANIM_MT_SCOPE_CYCLE_COUNTER(EvaluateAnimInstance, !IsInGameThread());

	if(InRootNode == RootNode)
	{
		// Call the correct override point if this is the root node
		CacheBones();
	}
	else
	{
		CacheBones_WithRoot(InRootNode);
	}

	// 此处可见,实现Evaluate_WithRoot并返回true即可自行实现动画Evaluate,否则走动画蓝图逻辑
	if (!Evaluate_WithRoot(Output, InRootNode))
	{
		EvaluateAnimationNode_WithRoot(Output, InRootNode);
	}
}

要实现动画逻辑共用,有点类似于类继承逻辑:先调用父对象逻辑,再调用子对象逻辑;或者先调用子对象逻辑,再调用父对象逻辑。我这里取后者,实现方法是在执行完子动画节点后,插入父对象添加的节点逻辑。下面是一个武器动画的例子:

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
void FSMGWeaponAnimInstanceProxy::InitializeObjects(UAnimInstance* InAnimInstance)
{
	FAnimInstanceProxy::InitializeObjects(InAnimInstance);

	if (!HasInitNode)
	{
		auto Root = CastStruct<FAnimNode_Root>(GetRootNode());
		if (Root != nullptr && Root->Result.GetLinkNode() != &Node)
		{
			HasInitNode = true;

             //在根节点插入节点Node
			auto LastLink = Root->Result.GetLinkNode();
			Root->Result.SetLinkNode(&Node);

			FireSlotNode.SlotName = "Fire";
			FireSlotNode.Source.SetLinkNode(&AdditiveIdentityNode);

			AdditiveIdentityNode.RefPoseType = ERefPoseType::EIT_Additive;

			ApplyAdditiveNode.Base.SetLinkNode(LastLink);
			ApplyAdditiveNode.Additive.SetLinkNode(&FireSlotNode);
			
			Node.BlendTime.Reset();
			Node.BlendPose.Reset();
			Node.BlendTime.Add(0.1f);
			Node.BlendTime.Add(0.1f);
			new (Node.BlendPose) FPoseLink();
			new (Node.BlendPose) FPoseLink();
			
			if (USMGWeaponAnimInstance* AnimInstance = CastChecked<USMGWeaponAnimInstance>(InAnimInstance))
			{
				Node.bActiveValue = AnimInstance->bReloading;
				ReloadPlayerNode.Sequence = AnimInstance->ReloadAnim;
			}
		
			Node.BlendPose[0].SetLinkNode(&ReloadPlayerNode);
			Node.BlendPose[1].SetLinkNode(&ApplyAdditiveNode);

             //所有节点都需要手动初始化
			FAnimationInitializeContext Context(this);
			FireSlotNode.Initialize_AnyThread(Context);
			AdditiveIdentityNode.Initialize_AnyThread(Context);
			ApplyAdditiveNode.Initialize_AnyThread(Context);
			ReloadPlayerNode.Initialize_AnyThread(Context);
			Node.Initialize_AnyThread(Context);
		}
	}
}

这里的代码是实现了武器的Reload和Fire,Fire是叠加动画,而Reload是覆盖独立动画。节点代码我放在InitializeObjects中执行而不是Initialize执行是因为初始化时Root节点的LinkPose还是空的,而要注意InitializeObjects是循环执行,需要添加一个已初始化标志。

为了更好解释例子,我这里做了一个动画蓝图,上面C++代码是跟下面动画蓝图等价的:

image-20231016201836268

不过InputPose的输入是子类的动画蓝图,这样能方便子类动画蓝图实现自己不一样的动画。

另外节点的Expose出来的Pin,像是例子的Reloading,也需要在Proxy的Update函数中进行赋值。

个人是比较推荐在逻辑已经基本定型,结构不会大改的情况下,使用Native Anim Graph Node,或者把已经定型的共用动画节点逻辑,翻译成C++代码。这样既能实现较为灵活的共用,性能也不需要担心。但是如果动画逻辑包含大量的状态机转换逻辑,Native Anim Graph Node不是一个很好的选择,如果共用也是刚性需要,考虑自己开发一个C++的状态机代码框架管理复杂的状态转换逻辑。

游戏, 动画
本文由作者按照 CC BY 4.0 进行授权
分享

热门标签

游戏 动画 渲染 物理 网络
推荐博客
  •  iquilezles
  •  timlly cnblog
  •  catlikecoding
  •  therealmjp
  •  realtime-rendering-blog

文章内容

相关文章

2023/09/10

叠加动画原理细节

叠加动画常用于战斗受击、射击抖动、AO等。其作用是能同时两个动画的表现,如角色在跑步的时候受击,跑动动画是不能停的同时也要有被击中的抽搐表现;又如角色瞄准的时候射击,射击会因为武器的后坐力让角色躯体发生抖动,而角色也会下意识地维持着瞄准动作……叠加动画几乎占据角色动画逻辑的一大部分,不过似乎很少人讨论有关叠加动画的技术细节。本以UE实现方法为例,意在分析叠加动画的原理和一些细节。 基本原理...

2023/09/13

MotionWarping

MotionWarping算是一项老生常谈的动画技术了。技术本身原理并不复杂,不过UE自己却有不少的变种实现方案,对这最基础的实现思想有不断的进化和补充,目的也是为了提高算法的最终表现效果,做3A游戏真是对细节真是孜孜不倦啊。本文主要是解释MotionWarping的基础原理和UE的SkewWarping的实现原理。 DeltaCorrection和AnimationWarping Mo...

2021/04/01

真机适配问题记录(三)

有些问题过去久远了,相关的bug单都找不到了,但是实在让人印象深刻,还是要记录一下滴~~ 在iOS中,雾在场景会闪烁 表现:在教堂场景中,打开雾效会闪烁。场景贴图跟雾有种Z-Fighting一样的表现。 原因:iOS中,PBR_Scene_DetailNormal.shader(实际代码在Scene_PBR.hlsl)的雾效的一段计算(ComputeFogFactor函数),在fr...

MotionWarping

射击游戏的延迟对抗

© 2023 lynx. 保留部分权利。

本站采用 Jekyll 主题 Chirpy

热门标签

游戏 动画 渲染 物理 网络

发现新版本的内容。