Unity 使用Final IK实现拟真调整物体IK动画

这篇具有很好参考价值的文章主要介绍了Unity 使用Final IK实现拟真调整物体IK动画。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

演示视频

IK部分

需求

NPC拿起物体,指定转向放到指定位置,要求动作尽可能自然,贴近真实。

何为IK

谈IK之前先讲一下正向运动学(forward kinematics),正向动力学通俗来说就是父骨骼带动子骨骼运动,Unity父子对象其实已经实现了正向动力学基本功能,当你移动父物体,子物体保持与父物体的相对变换跟着运动,想让摄像机或者炮塔保持看向物体或保持在炮台之上,最简单的方法就是设为子物体。

反向动力学(inverse kinematics),简写为IK,通俗来说就是子骨骼带动父骨骼运动。反向动力学的实现方法有很多种,常见的有 CCD(循环坐标下降法),FABR(前向和后向法)。实现方法不细谈,实现的结构大体是由一个solver管理数个需要IK的bone,其中最末端的bone称为effector,IK算法会根据effector的变换,在一定的迭代次数iterations中,逐步修正父骨骼的变换,达到子骨骼带动父骨骼运动的效果。此外还可以通过添加一些约束,例如旋转方向约束,旋转角度约束,实现一些具体场景里的IK需求,例如机械臂IK和链条IK。

对实现细节感兴趣,可以查看下列参考资料

参考资料

正向动力学
IK(反向动力学)简单原理与实现
CCD IK(循环坐标下降逆动态学)
逆运动学(六): FABRIK

Final IK概述

Final IK是一个拓展IK功能较为强大的插件。在我看来主要分为5个部分。

  1. 基本IK
    • CCD IK
    • FABRIK
    • FABRIK Root
    • Rotation Limits
    • Trigonometric IK
  2. 部分身体IK
    • Aim IK
    • Arm IK
    • Grounder
    • Leg IK
    • Limb IK
    • Look At IK
  3. 全身IK
    • Biped IK
    • Full Body Biped IK
    • VRIK
  4. 交互系统
    • Interaction System
  5. 拓展功能
    • Baker
    • Extending Final IK

列出的只是文档目录里有的,工程里还有很多其他脚本,例如finger rig实现了基本手指IK。

这个项目用到的主要是Full Body Biped IK,Interaction System,Look At IK,finger rig,CCD IK,Rotation Limits,Limb IK。

  • Full Body Biped IK是一个全身IK组件,可以操控全身多个effectors,协调控制身体动作。
  • Look At IK控制头部以及脊柱朝向,实现注视效果。
  • CCD IK和Rotation Limits结合起来可以灵活配置想要的任何关节效果,例如手指IK,不过配置略微繁琐
  • finger rig是插件内提供的一个实现的手指IK效果的组件,虽然配置简单但是不好用(估计这就是为啥不放文档里),其底层是Limb IK实现,而Limb IK是由Trigonometric IK拓展而来,如果想要使用此组件,建议也得明白其他这两个组件。
  • Interaction System是插件提供一个方便实现IK交互效果的系统,可分为四个部分
    • Interaction System 交互的主体,放在人身上
    • Interaction Object 可交互物体,放在物体身上。通过权重曲线控制各effector权重,事件和动画实现复杂效果
    • Interaction Target 交互目标,实现交互的细节,例如手指该怎么摆,手腕怎么放
    • Interaction Trigger 交互触发器,限制触发交互的条件,如距离,角度,摄像机朝向

手部IK效果

此部分意为实现较为自然的手部拾取效果。

技术点主要分为三点,手指IK实现,手指自适应放置,与手腕自适应。

  1. 手指IK实现
    手指IK实现有两个方法,一是用CCDIK与Rotation Limit构建,二是直接用finger rig。ccd ik具体使用方法参考官方文档和demo,finger rig注意事项上文也提过。
  2. 手指自适应放置
    实现思路大致就是,先记录手指完全张开时的五个位置为手指起点,然后规定一个共同的抓取中心,从手指起点向抓取中心,发送射线,打到物体上的点就是手指effector的位置。参考代码如下
 void fingersAdapt()
    {
        for (int i = 0; i < fingers.Length; i++)
        {
            //抓取方向是掌心浮空固定一点
            Vector3 direction = grabCenter.position - fingerOrigins[i].position;

            //起始点沿着反方向延伸一点效果更好
            Vector3 realOrigin = fingerOrigins[i].position - direction * ExtendK;

            Debug.DrawRay(realOrigin, grabCenter.position - realOrigin, Color.red);

            Ray ray = new Ray(realOrigin, direction);
            RaycastHit hit;
            if (Physics.Raycast(ray, out hit, 10, 1 << LayerMask.NameToLayer("PickingCube")))
            {
                //grabCenter.position = grabableObj.transform.position;

                touchPoints[i].position = hit.point;

                if (fingers[i].target == null)
                    fingers[i].target = touchPoints[i];
            }
            else
            {
                if (fingers[i].target != null)
                    fingers[i].target = null;
            }
        }
    }
  1. 手腕自适应
    这部分内容建议熟悉interaction target的使用再看。
    实际上final ik的interaction target提供手腕适应的功能,但在全角度测试下,无法满足需求。所以选择了模仿着自己实现手腕自适应朝向。
    实现方法也比较简单,给手部节点一个pivot节点作为轴心,轴心位置处于手腕处,直接控制轴心与物体的相对变换,就是控制手腕。手腕适应分为两个方面,一是手腕朝向,二是手腕相对物体的深度。空间之中,两点确定一个方向,所以只需要两个vector3和一个float就可以较为灵活的控制手腕了。例如,想要在初次拾取物体时,手腕有个较为合适的角度,我就设手的同侧肩膀为起点,物体为终点。
    参考代码如下
    void InitGrabDepth()
    {
        grabDepth = Vector3.Magnitude(grabableObj.position - transform.position) - grabableObj.GetComponent<MeshRenderer>().bounds.size.z;

    }
    
    void SetHandAim()
    {

        //设置方向为同侧肩部朝着物体方向
        Vector3 direction = grabAim.position - origin;

        HandPivot.forward = direction;


        SetHandGrabDepth(direction);
    }

    void SetHandGrabDepth(Vector3 direction)
    {
        //物体相对肩部方向射出射线,从第一个接触点向此方向延伸一定距离的位置
        Ray ray = new Ray(origin, direction);

        Debug.DrawRay(origin, ray.direction, Color.red);

        RaycastHit hit;
        if (Physics.Raycast(ray, out hit, 10, 1 << LayerMask.NameToLayer("PickingCube")))
        {
            Vector3 point = hit.point;

            //调试用
            //grabDepth = Vector3.Magnitude(grabCenter.position - this.transform.position) + grabOffset;
            //Debug.LogError("调使用代码记得注释掉");


            handPivot.position = point - Vector3.Normalize(ray.direction) * (grabDepth + grabOffset * 0.01f);
        }

    }

IK交互

这里先提一下Final IK提供的基本的交互过程。

interaction trigger组件绑定了交互物体和交互身体部分,interaction system组件选择一个trigger触发,此时interaction system组件中的effector就会根据interaction object的权重曲线运动,整个过程可以被暂停,根据时间添加事件,添加动画等。而interaction system中未提供的effector,例如手部IK,由interaction target和hand poser实现,这两个组件会将物体下同名同结构的手部节点的运动,同步到实际的手上,要想实现精确的双手交互,每个物体都有一双手(我感觉这设计初始应该没考虑可变手势的情况,如果是固定手势,配置起来比较方便,如果做可变手势,就显得很冗余,但由于水平和时间原因,无法对插件进行更深层的理解,改造这部分结构,所以还是沿用每个物体一双手,控制物体上的手的做法)

拿取与双手调整效果

整个交互过程就是在权重曲线规定的时间内,开始,暂停或继续交互进程,控制IK点以此控制身体肢体,控制身体的IK参数协调整体表现。很繁琐,很细节,每个物体都得配置,并且每个物体还是有两双手(下面称为同步手),所以编码时,尽量要将手动配置的地方压到最低,代码结构优化好,否则很容易导致效率低并且bug率高。

这部分我处理并不好,所以给不出实际的建议了。因为我刚开始做这块的时候,对整个交互流程很模糊,完全没有概念,现在稍微做了点东西,大概明白了整个流程,所以就把我交互部分已实现的功能的流程的几个点大概讲讲,给交互过程一个大体认识,如果从来没做的话人看的话,应该会稍微点底。

interaction object的position weight权重曲线控制是interaction trigger绑定的身体effector的position权重,当权重为1时,effector的位置就会和交互物体的位置严格一致,拉一个曲线控制手部effector权重变化,具体表现就是手逐渐的靠近到交互物体的位置,在拉一个曲线控制poser weight权重,手部的动作也会同步物体下那双手上,这样一个用手去拾取的效果就实现了。还有很多其他参数参考官方文档。

  1. 拿取并握住往回收
    • 控制肘部bend goal weight使拾取动作更加自然
    • 控制position,rotation,poser 的权重,使手去接近物体
      • 这个期间动态改变同步手的姿态适配物体形状,身体方向(前面提到的方法)
        • 手部朝向:同侧肩部朝着物体方向
        • 手部相对物体位置(手部轴心):物体相对肩部方向射出射线,从第一个接触点向此方向延伸一定距离的位置
    • 接触到物体
      • 暂停交互进程
      • 控制物体的位置与方向朝身体内部收(此时控制物体就是控制整条手臂)
  2. 双手
    • 另一只手触发交互来拿物体(所有身体部分触发后的事件相同的,所以要做好判断,坑爹的是final ik提供的接口并不好用,似乎设计时没太考虑到多触发的需求)
      • 控制手部轴心使两只手各在物体两边
    • 拿起物体开始转动
      • 物体自由旋转,哪只手控制,哪只手开启手部自适应
        • 整个旋转过程为了保证手部自然,理想的情况是将需要旋转的角度,拆分成数个左右手能接受的角度,让左右手依次将物体旋转到最终角度
    • 放下物体还原
      • 物体移动到目标位置
      • 继续交互进程
        • 权重曲线控制position,rotation,poser 的权重,使手去离开物体

参考代码文章来源地址https://www.toymoban.com/news/detail-468135.html

/// <summary>
    /// 放置物品
    /// 拿起物品后,触发此方法,将物品位置与目标位置做线性插值,到位后松手
    /// </summary>
    /// <returns></returns>
    public void PlaceDoTween()
    {
        print("PlaceDoTween");

        float firstRotateAngle = 0;//第一次将物体拿到身前的角度

        bool isLeftHand = interactionSystem.IsInInteraction(FullBodyBipedEffector.LeftHand);
        bool isRightHand = interactionSystem.IsInInteraction(FullBodyBipedEffector.RightHand);
        switch (firstHand)
        {
            case FullBodyBipedEffector.Body://第一次触发
                if (isLeftHand)
                {
                    firstHand = FullBodyBipedEffector.LeftHand;

                    firstRotateAngle = 90;

                }
                else if (isRightHand)
                {
                    firstHand = FullBodyBipedEffector.RightHand;

                    firstRotateAngle = -90;
                }
                break;
            //第二次触发
            case FullBodyBipedEffector.LeftHand:
            case FullBodyBipedEffector.RightHand:
                print("第二只手来了");//不进入adjust阶段
                return;
            default:
                Debug.LogError("触发居然有其他情况");
                break;
        }

        //进入AdjustObj流程
        Sequence adjustAndPlace = DOTween.Sequence();
        adjustAndPlace
            //一只手拿起物体放在身体前方,手在物体两边拿着
            .Append(transform.DOMove(adjustObjPoint.position, 1f).SetEase(Ease.InOutSine))
            .Join(transform.DORotateQuaternion(Quaternion.AngleAxis(firstRotateAngle, Vector3.up) * transform.rotation, 1)).SetEase(Ease.InOutSine)
            //另一只手也来拿物体
            .InsertCallback(0.5f, () =>
            {
                switch (firstHand)
                {
                    case FullBodyBipedEffector.LeftHand:
                        interactionSystem.StartInteraction(FullBodyBipedEffector.RightHand, interactionObject, false);
                        break;
                    case FullBodyBipedEffector.RightHand:
                        interactionSystem.StartInteraction(FullBodyBipedEffector.LeftHand, interactionObject, false);
                        break;
                    default:
                        Debug.LogError("另一只手触发居然有其他情况");
                        break;
                }
            })
            .InsertCallback(0.6f, () =>
            {
                //重新设置两只手锚点
                //设置两只手自适应
                foreach (var target in targets)
                {
                    HandAdapterIK2 hand = target.GetComponent<HandAdapterIK2>();
                    switch (target.effectorType)
                    {
                        case FullBodyBipedEffector.LeftHand://左手新起点在物体正左方,手离的稍微开一点
                            hand.RotateOnce = false;
                            //hand.SetOrigin(transform.position + Vector3.left);
                            DOTween.To(() => hand.NewOrigin, x => hand.NewOrigin = x, transform.position + Vector3.left, 0.5f).From(hand.HandPivot.position).SetEase(Ease.InOutQuad).OnComplete(() =>
                            {
                                hand.RotateOnce = true;
                            });
                            //hand.GrabOffset = 2;
                            DOTween.To(() => hand.GrabOffset, x => hand.GrabOffset = x, 2, 0.5f);
                            break;
                        case FullBodyBipedEffector.RightHand:
                            hand.RotateOnce = false;
                            //hand.SetOrigin(transform.position + Vector3.right);
                            DOTween.To(() => hand.NewOrigin, x => hand.NewOrigin = x, transform.position + Vector3.right, 0.5f).From(hand.HandPivot.position).SetEase(Ease.InOutQuad);
                            //hand.GrabOffset = 3.5f;
                            DOTween.To(() => hand.GrabOffset, x => hand.GrabOffset = x, 3.5f, 0.5f);
                            break;
                        default:
                            Debug.LogError("触发居然有其他情况");
                            break;
                    }
                }
            })
            .AppendCallback(() =>
            {
                //transform.SetParent(null);
                print(gameObject.name + " placed adjust point");


            })
            //物体自由旋转
            .Append(transform.DORotateQuaternion(Quaternion.Euler(0, 0, 0), 2))
            //由右手放下物体
            .AppendCallback(() =>
            {
                //放下左手
                interactionSystem.ResumeInteraction(FullBodyBipedEffector.LeftHand);

                //关闭自适应
                //还原两只手腕方向
                //还原两只手自适应
                foreach (var target in targets)
                {
                    switch (target.effectorType)
                    {
                        case FullBodyBipedEffector.LeftHand://左手新起点在物体正左方
                            target.GetComponent<HandAdapterIK2>().RotateOnce = true;
                            target.GetComponent<HandAdapterIK2>().SetOrigin(Vector3.zero);

                            break;
                        case FullBodyBipedEffector.RightHand:
                            target.GetComponent<HandAdapterIK2>().SetOrigin(Vector3.zero);
                            target.GetComponent<HandAdapterIK2>().RotateOnce = true;


                            break;
                        default:
                            Debug.LogError("触发居然有其他情况");
                            break;
                    }
                }
            })
            .Append(transform.DOMove(new Vector3(0.1f, 1.2f, 0.028f), 1.5f))
            .AppendCallback(() =>
            {

                设置trigger失效
                //InteractionTrigger trigger = transform.GetComponentInChildren<InteractionTrigger>();
                //trigger.gameObject.SetActive(false);

                设置物体层级
                //gameObject.layer = 0;

                背景吸附到正确地方
                //StartCoroutine(SetPosAndRot(transform));




                //自动放下物体
                interactionSystem.ResumeAll();
                //手部归零
                firstHand = FullBodyBipedEffector.Body;

            })
            ;
        
    }


到了这里,关于Unity 使用Final IK实现拟真调整物体IK动画的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 使用Unity里的ShaderGraph实现物体边框呼吸灯效果

    提前声明一下,以下效果需要在hdrp项目中进行。 首先创建好我们的正方体: 用来接下来的边框显示。 这里需要创建两个材质球,一个用于显示方块的材质,另一个用于边框的显示。 (Material用于方块材质,OutlineMat用于边框线) 为了更好地突出边框的视觉效果我这里给它加

    2024年04月22日
    浏览(53)
  • 【Unity】Unity使用动画实现场景转换

    哈喽,大家好,我是FEZ98. 由于今天需要实现一个小项目中的场景转换效果,于是使用了Animator简单的实现了这个需求,同时也与大家分享一下今日的收获,如果哪里有错误,还望朋友们指正。 使用动画实现简单的场景转换的思路是向场景中添加一个专门处理转换效果的 Imag

    2024年02月03日
    浏览(46)
  • Unity 基于URP使用Highlight Plus实现物体高亮

    内容将会持续更新,有错误的地方欢迎指正,谢谢!   Unity 基于URP使用Highlight Plus实现物体高亮       TechX 坚持将创新的科技带给世界! 拥有更好的学习体验 —— 不断努力,不断进步,不断探索 TechX —— 心探索、心进取! 助力快速实现 Highlight Plus 基于URP的物体高亮 为初学

    2024年02月05日
    浏览(38)
  • [Unity] 使用Mathf函数实现平滑移动物体的7种方法

     Unity中要利用Mathf中的函数实现物体的平滑运动,有以下7种方法: Mathf.SmoothDamp、Mathf.Lerp、Mathf.SmoothStep三个方法非常相似,分别使用三个方法移动同一个物体的效果如图所示: 可以看出,三者均是先快后慢,但SmoothDamp方法会有一些卡顿;SmoothStep与Lerp比较起来,Mathf.Smooth

    2024年02月12日
    浏览(52)
  • Unity 使用Shader实现序列帧动画

    序列帧动画图片使用shader逐帧播放 可以直接使用,拿走不谢。 可以挂到材质上放入Image组件的材质中使用。

    2024年02月13日
    浏览(45)
  • 【Unity自动寻路】使用Navigation系统实现物体自动寻路绕开障碍物

    知识点流程图 我们在游戏场景中经常会有一些障碍物、墙壁、树木等等,如果我想要让角色或者怪物去墙的另一边,我直接在墙另一边点击左键,我希望角色自动跑过去,但是他不能直接穿透墙,他需要“智能”的绕开障碍物,自动找到可以走的路,自己过去!这就是Unity

    2024年02月03日
    浏览(52)
  • unity手指、鼠标滑动实现物体360度旋转、点击按钮实现物体旋转

    先创建一个ObjectRotation 脚本 之后将该脚本拖动到需要转动的object上 因为如果每次都真机模拟回很麻烦,所以我加入了让鼠标代替手指滑动的脚本来代替手指输入 同样的也是先创建一个MouseRow 脚本 最后我还需要某笔画沿着坐标旋转,代码如下: 里面有我的其他参数,如果读

    2024年02月11日
    浏览(66)
  • Unity实现鼠标拖拽多物体(拖拽单物体的拓展)

    学习了B站UP主OneCredit【Unity快速教学】鼠标拖曳甩动物件BV1qK4y1d7iZ的教学视频后 拓展了一下功能,实现多个物体也可以拖拽,互不受影响 主要是做了一个检测,在鼠标上物体才能被拖拽 目录 bool Drag 拖拽物体的实现 解决摄像机视角的影响 绑定刚体,写好需要用到的变量 Dr

    2024年03月24日
    浏览(61)
  • Unity实现物体绕指定轴旋转

    在Unity中,我们经常需要控制物体的旋转行为。有时候,我们需要让物体绕指定的轴旋转,而不是默认的旋转轴。在本文章中,我将展示如何使用C#脚本来实现这个功能。 首先,我们需要创建一个空对象,并将它作为父对象,然后将我们想要旋转的物体作为子对象。这样,我

    2024年01月23日
    浏览(46)
  • UE4/5动画系列(4.足部ik制作)

    目录 前期准备 添加虚拟骨骼 ​编辑 腿部函数: 前肢: ​编辑 盆骨函数: 后肢:  进入动画图表: 首先准备一个后期处理动画蓝图 然后【因为笔者之前的大象因为不知明原因崩溃,这里就不展示如何用接口分层制作】 这里我们将直接在post蓝图中直接进行制作。 这是我

    2024年02月11日
    浏览(63)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包