美文网首页
Unity 动画系列八 Target Matching 和 Re

Unity 动画系列八 Target Matching 和 Re

作者: 合肥黑 | 来源:发表于2021-12-01 09:22 被阅读0次

    参考
    Unity动画系统详解9:Target Matching是什么?
    学习笔记 --- Unity动画系统

    小新:“大智,我有一个需求,感觉可以用IK去实现,但是使用IK会有问题,我就不知道怎么办了。”
    大智:“具体是什么需求呢?”
    小新:“绝地求生里面人物可以双手撑墙跳过一个堵墙或者窗户,我想让角色的双手能恰好放到墙上,这个应该怎么做呢?”

    image.png
    大智:“这种需求其实可以使用Target Matching的技术。在游戏中,经常有这种情况:角色的手或者脚需要在特定时间放在特定的位置。比如角色需要用手撑着跳过一个石头或一堵墙,或者跳起抓住房梁。Target Match就是让动画的特定片段去匹配特定的位置。”

    通过MatchTarget我们可以在人物执行某些与场景互动的特殊动作过程中(翻越,攀爬)将人物身体的某个部分逐渐匹配到某点的位置和朝向上,在进行匹配的过程中人物的身体位置和方位会发生改变,从而实现人物与场景的真实互动效果。

    一、MatchTarget
    1.参数

    这个方法用来自动调节GameObject的位置和旋转

    public void MatchTarget(Vector3 matchPosition, 
      Quaternion matchRotation, 
      AvatarTarget targetBodyPart, 
      MatchTargetWeightMask weightMask, 
      float startNormalizedTime, 
      float targetNormalizedTime = 1);
    
    • matchPosition 匹配的位置
    • matchRotation 匹配的旋转
    • targetBodyPart 身体的部位,从AvatarTarget枚举中选择。
    • weightMask 设置位置和旋转匹配的权重
    • startNormalizedTime 开始匹配的时间,注意是单位化时间(动画开始位置是0,结束位置是1)。如果开始时间已经超过了当前动画的播放时间,会匹配下一次符合的时间。比如:startNormalizedTime传入0.2,但是当前已经播放到0.3,会匹配下一次循环的0.2的位置。
    • targetNormalizedTime 最终匹配到的单位化时间。如果值大于1,可以设置特定循环数后的位置。比如2.3代表第2次循环的30%位置。

    Unity会自动调整GameObject的位置和旋转来保证到特定时间时角色的特定部位能达到指定的位置和旋转。Target matching只能对Base Layer(index 0)生效。同一时间只能有一个match target生效,后续一个的match target需要等待前面的执行完成,再后续的match target会被忽略。如果需要打断,应使用下面这个方法

    animator.InterruptMatchTarget(bool);//默认值为true
    //是否需要被打断的MatchTarget置于完成效果(true)或是仅打断(false)
    
    2.参数详解

    小新:“大概知道这个方法是什么了,那对于跳跃这个动画,如何应用呢?”

    大智:“其实只要对这几个参数多研究一下,就知道怎么用了。首先前两个参数是匹配的位置和旋转,这个不用多说吧?”

    小新:“嗯嗯,比如我要匹配墙上,那就是墙上的位置,旋转就是让手的角度贴合墙,应该可以用调IK的方式来调。”

    大智:“没错。targetBodyPart的类型是一个枚举,可以用来匹配对应的位置,比如说匹配左手,那就应该传入左手。weightMask是一个结构体,可以同时设置位置和旋转的权重,这个权重的概念我们已经接触很多次了,如果为1就是完全匹配到对应的位置和旋转。”

    小新:“嗯,这几个参数没什么问题,就是最后两个参数有些难搞清楚”

    大智:“最后两个参数是开始匹配和完全匹配上的时间。比如你想在特定的时间匹配到对应的位置,那么肯定不能到那个时间点一下子把动画拉过去,是吧?”

    小新:“对,那样太突兀了”

    大智:“是的,我们动画中很多的混合都是为了让动画看起来更平滑,更自然。所以除了这个匹配到的特定时间,也就是targetNormalizedTime,我们还需要一个开始匹配的时间startNormalizedTime。开始时间到了就开始去进行匹配混合,到targetNormalizedTime时正好精确的到达匹配的位置。”

    小新:“那我大概明白了,我得去亲手实践一下”

    大智:“还要注意Target Matching只能对Base Layer生效

    小新:“好我记住了”

    3.一段人物攀爬动作的MatchTarget示例

    通常我们会通过曲线来标识出动画片段中需要进行匹配的起止点,这样编码人员就不需要在意素材的动作表现。


    image.png

    这里使用了两条恒值曲线,从而编码人员只需要获取动画参数中的映射就能确定startNormalizedTime与targetNormalizedTime。

    通常我们不会进行朝向匹配,保持原动画中人物肢体的朝向即可。因此我们会使用Quaternion.identity填充matchRotation,在MatchTargetWeightMask中将方位权重置为0。

    AnimatorStateInfo animatorStateInfo2;
    
    Animator animator;
    
    AnimatorStateInfo animatorStateInfo;
    
    Vector3 matchPos;
    
    int climbHash = Animator.StringToHash("Climb");
    int runStateHash = Animator.StringToHash("RunTree");
    
    int startTime = Animator.StringToHash("MatchStart");
    int endTime = Animator.StringToHash("MatchEnd");
    
    int colliderHash = Animator.StringToHash("Collider");
    
    int dfixSHash = Animator.StringToHash("HFixStart");
    int dfixEHash = Animator.StringToHash("HFixEnd");
    
    RaycastHit raycastHit;
    
    [SerializeField]
    float hFix = 0f;//高度修正量
    [SerializeField]
    float xFix = 0f;//左右偏移修正量
    
    [SerializeField]
    float downFix = 0f;//向下差位修正
    
    [SerializeField]
    float maxAngle = 0f;//方向对其允许的最大偏角
    
    Collider collider;
    
    float checkAngle;
    
    Vector3 dfixStartPos;//记录人物的初始位置
    Vector3 dfixEndPos;//记录人物站起时应达到的位置
    
    float downfixTime;//向下修正时间长度
    
    private void Awake()
    {
        animator = gameObject.GetComponent<Animator>();
        collider = gameObject.GetComponent<Collider>();
    }
    
    //Update中-----------------------------------------------------------------------
            animatorStateInfo = animator.GetCurrentAnimatorStateInfo(0);
    
            //ClimbUp位置匹配-------------------------------------------------------------
    
            //位置搜索
            if (animatorStateInfo.shortNameHash == runStateHash)
            {
                if (Input.GetKeyDown(KeyCode.LeftShift))
                {
                    if(Physics.Raycast(transform.position+Vector3.up,transform.forward,out raycastHit, 0.5f))
                    {
                        if (raycastHit.collider.CompareTag("Wall"))
                        {
                            
                            checkAngle = Vector3.Angle(raycastHit.normal, transform.forward);
    
                            checkAngle = 180 - checkAngle;
    
                            if (checkAngle < maxAngle)//检测对齐偏角
                            {
                                Debug.Log("法向量夹角:" + checkAngle + "通过检测");
    
                                matchPos = new Vector3(raycastHit.point.x, 
                                raycastHit.collider.bounds.size.y + hFix, raycastHit.point.z) + transform.right * xFix;
    
                                animator.SetTrigger(climbHash);
                            }
                        }
                    }
    
                }
            }
    
            //位置匹配
            if (animatorStateInfo.shortNameHash==climbHash&&!animator.IsInTransition(0))
            {
                animator.MatchTarget(matchPos, Quaternion.identity, AvatarTarget.RightHand, 
                new MatchTargetWeightMask(new Vector3(1, 1, 1), 0), animator.GetFloat(startTime), animator.GetFloat(endTime));
                //通过曲线在攀爬过程中保持Collider开启,而在翻越动作时取消Collider,保证人物能够向前位移到
                if (animator.GetFloat(colliderHash) < -0.1f)
                {
                    collider.enabled = false;
                }
                else
                {
                    collider.enabled = true;
                }
    
                //站起时向下修正位置,保证脚触地
    
                if (animatorStateInfo.normalizedTime > animator.GetFloat(dfixSHash))
                {
                    transform.position = Vector3.Lerp(dfixStartPos, dfixEndPos, 
                    (animatorStateInfo.normalizedTime - animator.GetFloat(dfixSHash)) / downfixTime);
                }
                else
                {
                    dfixStartPos = transform.position;
                    dfixEndPos = dfixStartPos + Vector3.up * downFix;
                    downfixTime = animator.GetFloat(dfixEHash) - animator.GetFloat(dfixSHash);
                }
            }
    

    可以看到我们使用了非常多的字段才解决了这个问题,因为需要实现人物特殊动作与场景的真实互动,MatchTarget只是提供了一个差值方法,而实际应用中为了能够正确匹配动作和场景,会有非常多的问题需要我们去解决。下面记录一下我们都解决了什么问题

    Q1:
    首先我们不会像官方那样偷懒,无论人物在哪都把人物往固定球的位置上差。我们希望实现比较真实的,人物攀上了面前的墙的效果,因此我们配置了Collider的Tag标签,并使用射线检测的方式去主动查找人物需要位移到的目标位点。


    image.png
            //位置搜索
            if (animatorStateInfo.shortNameHash == runStateHash)
            {
                if (Input.GetKeyDown(KeyCode.LeftShift))
                {
                    if(Physics.Raycast(transform.position+Vector3.up,transform.forward,out raycastHit, 0.5f))
                    {
                        if (raycastHit.collider.CompareTag("Wall"))
                        {
    

    Q2:
    Q1其实又引出了一个问题,就是人物手部与目标位点匹配的偏移问题,由于射线检测位置是从人物的中心,而高度我们只能获取到BoxCollider的Size高度,因此从中心到人物左手,从Box顶部到手真正需要放的位置,我们使用了两个偏移量保证左手位置的正确


    image.png
                            checkAngle = 180 - checkAngle;
    
                            if (checkAngle < maxAngle)//检测对齐偏角
                            {
                                Debug.Log("法向量夹角:" + checkAngle + "通过检测");
    
                                matchPos = new Vector3(raycastHit.point.x, 
                                raycastHit.collider.bounds.size.y + hFix, raycastHit.point.z) + transform.right * xFix;
    
                                animator.SetTrigger(climbHash);
                            }
    

    Q3:一个关于人物Collider的问题

    我们需要在人物进行位置匹配的时候,保持Colliderde开启,以免人物陷入墙体中。而人物在进行翻越的过程中会有一个向前的位移,此时我们需要关闭Collider,否则人物就会被墙挡住,无法移动到墙顶的内侧。


    如果我们完全不管Collider的开去与否,那么最终人物是站在墙外的,并没成功翻到墙上

    因此我们又使用了一条动画曲线,控制人物Collider的开启与否,从而在进行翻越的过程中关闭Collider,使人物能够翻越进墙内


    image.png
                //通过曲线在攀爬过程中保持Collider开启,而在翻越动作时取消Collider,保证人物能够向前位移到
                if (animator.GetFloat(colliderHash) < -0.1f)
                {
                    collider.enabled = false;
                }
                else
                {
                    collider.enabled = true;
                }
    

    Q4:
    如果人物正面方向与墙法线夹角过大,执行攀爬动作就会显得有些怪异,因此我们引入了一个夹角检测,如果人物forward向量和墙体法向夹角大于阈值,则能不进行攀爬。


    夹角过大执行攀爬动作会有些奇怪
    完成攀爬过渡到站立,人物出现了脚的悬空
                            checkAngle = Vector3.Angle(raycastHit.normal, transform.forward);
    
                            checkAngle = 180 - checkAngle;
    
                            if (checkAngle < maxAngle)//检测对齐偏角
                            {
                                Debug.Log("法向量夹角:" + checkAngle + "通过检测");
    

    Q5:
    一个非常头疼的问题,我们正确匹配了手与墙顶的位置,但人物完成攀爬后站立起身,脚底竟与墙顶有一个差距,使得人物悬空站立,结束动作后人物会从空中掉到了墙顶。并且我们确定不是因为过早的开启Collider,这完全是动画师的一个失误。


    image.png

    因此我们需要一个位置差值的逻辑来修正人物的位置,并且为了保持这个差值不被察觉,我们将它安排在了人物站起的过程中,和之前一样,我们使用曲线标记了差值的起止位置,在代码中获取即可


    image.png
                //站起时向下修正位置,保证脚触地
    
                if (animatorStateInfo.normalizedTime > animator.GetFloat(dfixSHash))
                {
                    transform.position = Vector3.Lerp(dfixStartPos, dfixEndPos, 
                    (animatorStateInfo.normalizedTime - animator.GetFloat(dfixSHash)) / downfixTime);
                }
                else
                {
                    dfixStartPos = transform.position;
                    dfixEndPos = dfixStartPos + Vector3.up * downFix;
                    downfixTime = animator.GetFloat(dfixEHash) - animator.GetFloat(dfixSHash);
                }
    

    ok解决了这些问题我们才达到了一个比较满意的,人物攀爬上面前的墙壁的效果

    二、动画重定向

    小新:“我突然想起来一个也叫什么Retargeting的东西,和这个好像啊,你顺便给我讲讲呗”

    大智:“你说的应该是动画重定向,也就是Retargeting,比如将角色A的动画应用到没有动画的角色B上面,实现动画的重用。”

    小新:“对对,就是这个”

    大智:“其实要想重用动画不一定要用到Retargeting系统。如果两个角色的骨骼结构一样,那么动画其实是可以直接重用的。”

    小新:“哦,那为啥还要这个Retargeting系统呢?”

    大智:“虽说两个角色的骨骼结构一样,动画是可以直接重用。但是要求很严格,包括骨骼的命名、骨骼的数量、骨骼的父子结构等等要求一模一样。这种情形一般只会出现在同一个动画师手里。如果骨骼稍微有些差别,那就没办法直接重用了。”

    小新:“确实,如果我找到一个特别好的人物角色模型,但是却没办法用其他已有的动画,那是多么的折磨啊”

    大智:“是的,这时候就需要用到Retargeting系统了,但是需要注意的是Retargeting系统只能用于人形动画。重定向相当于将不同的骨骼结构都映射到了Unity的Avatar上面。之前我们配置过AvatarMask,和那个有点像。配置的位置就是在模型导入设置的Rig标签里”

    在Mapping页面下,可以将模型的骨骼节点映射到Unity的Avatar上面对应的骨骼节点。在Musule & Settings页面下可以设置每个骨骼节点可以旋转的范围,避免出现不合常理的骨骼旋转。

    大智:“Retargeting系统就相当于将模型A和模型B的骨骼都映射到了一个Unity Avatar上面,这样最终都是用这个同一个Avatar来控制动画,达到重用的目的。”

    小新:“明白了,相当于用了一个中间件,将两个不同的东西匹配了起来。”

    大智:“没错。在设置模型的Rig的时候,需要注意的是,Avatar Definition除了Create From This Model这个选项之外,还有一个选项是Copy From Other Avatar,如果两个模型的骨骼结构是一致的,可以直接使用这个Copy From Other Avatar将Avatar复制过来,就不需要再重新设置一遍了,也能节省内存。”

    Unity动画系统-配置Avatar

    首先我们所有的人形动画的骨骼都大致相似,比如头,胳膊,腿等,唯一不同的可能是骨骼的数量不同。所以Unity为我们建立一套标准的骨骼,我们需要把自己的骨骼映射到标准骨骼中,这样我们就可以实现人形动画的重用(不同人物的动画通用)。

    相关文章

      网友评论

          本文标题:Unity 动画系列八 Target Matching 和 Re

          本文链接:https://www.haomeiwen.com/subject/ltdizltx.html