Unity 使用Final IK實(shí)現(xiàn)擬真調(diào)整物體IK動(dòng)畫

[toc]
<iframe src="http://player.bilibili.com/player.html?aid=780596152&bvid=BV1V24y1G7Qy&cid=1041802723&page=1" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true"></iframe>

演示視頻

IK部分

需求

NPC拿起物體,指定轉(zhuǎn)向放到指定位置,要求動(dòng)作盡可能自然,貼近真實(shí)。

何為IK

談IK之前先講一下正向運(yùn)動(dòng)學(xué)(forward kinematics),正向動(dòng)力學(xué)通俗來(lái)說(shuō)就是父骨骼帶動(dòng)子骨骼運(yùn)動(dòng),Unity父子對(duì)象其實(shí)已經(jīng)實(shí)現(xiàn)了正向動(dòng)力學(xué)基本功能,當(dāng)你移動(dòng)父物體,子物體保持與父物體的相對(duì)變換跟著運(yùn)動(dòng),想讓攝像機(jī)或者炮塔保持看向物體或保持在炮臺(tái)之上,最簡(jiǎn)單的方法就是設(shè)為子物體。

反向動(dòng)力學(xué)(inverse kinematics),簡(jiǎn)寫為IK,通俗來(lái)說(shuō)就是子骨骼帶動(dòng)父骨骼運(yùn)動(dòng)。反向動(dòng)力學(xué)的實(shí)現(xiàn)方法有很多種,常見的有 CCD(循環(huán)坐標(biāo)下降法),F(xiàn)ABR(前向和后向法)。實(shí)現(xiàn)方法不細(xì)談,實(shí)現(xiàn)的結(jié)構(gòu)大體是由一個(gè)solver管理數(shù)個(gè)需要IK的bone,其中最末端的bone稱為effector,IK算法會(huì)根據(jù)effector的變換,在一定的迭代次數(shù)iterations中,逐步修正父骨骼的變換,達(dá)到子骨骼帶動(dòng)父骨骼運(yùn)動(dòng)的效果。此外還可以通過(guò)添加一些約束,例如旋轉(zhuǎn)方向約束,旋轉(zhuǎn)角度約束,實(shí)現(xiàn)一些具體場(chǎng)景里的IK需求,例如機(jī)械臂IK和鏈條IK。

對(duì)實(shí)現(xiàn)細(xì)節(jié)感興趣,可以查看下列參考資料

參考資料

正向動(dòng)力學(xué)
IK(反向動(dòng)力學(xué))簡(jiǎn)單原理與實(shí)現(xiàn)
CCD IK(循環(huán)坐標(biāo)下降逆動(dòng)態(tài)學(xué))
逆運(yùn)動(dòng)學(xué)(六): FABRIK

Final IK概述

Final IK是一個(gè)拓展IK功能較為強(qiáng)大的插件。在我看來(lái)主要分為5個(gè)部分。

  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. 交互系統(tǒng)
    • Interaction System
  5. 拓展功能
    • Baker
    • Extending Final IK

列出的只是文檔目錄里有的,工程里還有很多其他腳本,例如finger rig實(shí)現(xiàn)了基本手指IK。

這個(gè)項(xiàng)目用到的主要是Full Body Biped IK,Interaction System,Look At IK,finger rig,CCD IK,Rotation Limits,Limb IK。

  • Full Body Biped IK是一個(gè)全身IK組件,可以操控全身多個(gè)effectors,協(xié)調(diào)控制身體動(dòng)作。
  • Look At IK控制頭部以及脊柱朝向,實(shí)現(xiàn)注視效果。
  • CCD IK和Rotation Limits結(jié)合起來(lái)可以靈活配置想要的任何關(guān)節(jié)效果,例如手指IK,不過(guò)配置略微繁瑣
  • finger rig是插件內(nèi)提供的一個(gè)實(shí)現(xiàn)的手指IK效果的組件,雖然配置簡(jiǎn)單但是不好用(估計(jì)這就是為啥不放文檔里),其底層是Limb IK實(shí)現(xiàn),而Limb IK是由Trigonometric IK拓展而來(lái),如果想要使用此組件,建議也得明白其他這兩個(gè)組件。
  • Interaction System是插件提供一個(gè)方便實(shí)現(xiàn)IK交互效果的系統(tǒng),可分為四個(gè)部分
    • Interaction System 交互的主體,放在人身上
    • Interaction Object 可交互物體,放在物體身上。通過(guò)權(quán)重曲線控制各effector權(quán)重,事件和動(dòng)畫實(shí)現(xiàn)復(fù)雜效果
    • Interaction Target 交互目標(biāo),實(shí)現(xiàn)交互的細(xì)節(jié),例如手指該怎么擺,手腕怎么放
    • Interaction Trigger 交互觸發(fā)器,限制觸發(fā)交互的條件,如距離,角度,攝像機(jī)朝向

手部IK效果

此部分意為實(shí)現(xiàn)較為自然的手部拾取效果。

技術(shù)點(diǎn)主要分為三點(diǎn),手指IK實(shí)現(xiàn),手指自適應(yīng)放置,與手腕自適應(yīng)。

  1. 手指IK實(shí)現(xiàn)
    手指IK實(shí)現(xiàn)有兩個(gè)方法,一是用CCDIK與Rotation Limit構(gòu)建,二是直接用finger rig。ccd ik具體使用方法參考官方文檔和demo,finger rig注意事項(xiàng)上文也提過(guò)。
  2. 手指自適應(yīng)放置
    實(shí)現(xiàn)思路大致就是,先記錄手指完全張開時(shí)的五個(gè)位置為手指起點(diǎn),然后規(guī)定一個(gè)共同的抓取中心,從手指起點(diǎn)向抓取中心,發(fā)送射線,打到物體上的點(diǎn)就是手指effector的位置。參考代碼如下
 void fingersAdapt()
    {
        for (int i = 0; i < fingers.Length; i++)
        {
            //抓取方向是掌心浮空固定一點(diǎn)
            Vector3 direction = grabCenter.position - fingerOrigins[i].position;

            //起始點(diǎn)沿著反方向延伸一點(diǎn)效果更好
            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. 手腕自適應(yīng)
    這部分內(nèi)容建議熟悉interaction target的使用再看。
    實(shí)際上final ik的interaction target提供手腕適應(yīng)的功能,但在全角度測(cè)試下,無(wú)法滿足需求。所以選擇了模仿著自己實(shí)現(xiàn)手腕自適應(yīng)朝向。
    實(shí)現(xiàn)方法也比較簡(jiǎn)單,給手部節(jié)點(diǎn)一個(gè)pivot節(jié)點(diǎn)作為軸心,軸心位置處于手腕處,直接控制軸心與物體的相對(duì)變換,就是控制手腕。手腕適應(yīng)分為兩個(gè)方面,一是手腕朝向,二是手腕相對(duì)物體的深度??臻g之中,兩點(diǎn)確定一個(gè)方向,所以只需要兩個(gè)vector3和一個(gè)float就可以較為靈活的控制手腕了。例如,想要在初次拾取物體時(shí),手腕有個(gè)較為合適的角度,我就設(shè)手的同側(cè)肩膀?yàn)槠瘘c(diǎn),物體為終點(diǎn)。
    參考代碼如下
    void InitGrabDepth()
    {
        grabDepth = Vector3.Magnitude(grabableObj.position - transform.position) - grabableObj.GetComponent<MeshRenderer>().bounds.size.z;

    }
    
    void SetHandAim()
    {

        //設(shè)置方向?yàn)橥瑐?cè)肩部朝著物體方向
        Vector3 direction = grabAim.position - origin;

        HandPivot.forward = direction;


        SetHandGrabDepth(direction);
    }

    void SetHandGrabDepth(Vector3 direction)
    {
        //物體相對(duì)肩部方向射出射線,從第一個(gè)接觸點(diǎn)向此方向延伸一定距離的位置
        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;

            //調(diào)試用
            //grabDepth = Vector3.Magnitude(grabCenter.position - this.transform.position) + grabOffset;
            //Debug.LogError("調(diào)使用代碼記得注釋掉");


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

    }

IK交互

這里先提一下Final IK提供的基本的交互過(guò)程。

interaction trigger組件綁定了交互物體和交互身體部分,interaction system組件選擇一個(gè)trigger觸發(fā),此時(shí)interaction system組件中的effector就會(huì)根據(jù)interaction object的權(quán)重曲線運(yùn)動(dòng),整個(gè)過(guò)程可以被暫停,根據(jù)時(shí)間添加事件,添加動(dòng)畫等。而interaction system中未提供的effector,例如手部IK,由interaction target和hand poser實(shí)現(xiàn),這兩個(gè)組件會(huì)將物體下同名同結(jié)構(gòu)的手部節(jié)點(diǎn)的運(yùn)動(dòng),同步到實(shí)際的手上,要想實(shí)現(xiàn)精確的雙手交互,每個(gè)物體都有一雙手(我感覺這設(shè)計(jì)初始應(yīng)該沒考慮可變手勢(shì)的情況,如果是固定手勢(shì),配置起來(lái)比較方便,如果做可變手勢(shì),就顯得很冗余,但由于水平和時(shí)間原因,無(wú)法對(duì)插件進(jìn)行更深層的理解,改造這部分結(jié)構(gòu),所以還是沿用每個(gè)物體一雙手,控制物體上的手的做法)

拿取與雙手調(diào)整效果

整個(gè)交互過(guò)程就是在權(quán)重曲線規(guī)定的時(shí)間內(nèi),開始,暫停或繼續(xù)交互進(jìn)程,控制IK點(diǎn)以此控制身體肢體,控制身體的IK參數(shù)協(xié)調(diào)整體表現(xiàn)。很繁瑣,很細(xì)節(jié),每個(gè)物體都得配置,并且每個(gè)物體還是有兩雙手(下面稱為同步手),所以編碼時(shí),盡量要將手動(dòng)配置的地方壓到最低,代碼結(jié)構(gòu)優(yōu)化好,否則很容易導(dǎo)致效率低并且bug率高。

這部分我處理并不好,所以給不出實(shí)際的建議了。因?yàn)槲覄傞_始做這塊的時(shí)候,對(duì)整個(gè)交互流程很模糊,完全沒有概念,現(xiàn)在稍微做了點(diǎn)東西,大概明白了整個(gè)流程,所以就把我交互部分已實(shí)現(xiàn)的功能的流程的幾個(gè)點(diǎn)大概講講,給交互過(guò)程一個(gè)大體認(rèn)識(shí),如果從來(lái)沒做的話人看的話,應(yīng)該會(huì)稍微點(diǎn)底。

interaction object的position weight權(quán)重曲線控制是interaction trigger綁定的身體effector的position權(quán)重,當(dāng)權(quán)重為1時(shí),effector的位置就會(huì)和交互物體的位置嚴(yán)格一致,拉一個(gè)曲線控制手部effector權(quán)重變化,具體表現(xiàn)就是手逐漸的靠近到交互物體的位置,在拉一個(gè)曲線控制poser weight權(quán)重,手部的動(dòng)作也會(huì)同步物體下那雙手上,這樣一個(gè)用手去拾取的效果就實(shí)現(xiàn)了。還有很多其他參數(shù)參考官方文檔。

  1. 拿取并握住往回收
    • 控制肘部bend goal weight使拾取動(dòng)作更加自然
    • 控制position,rotation,poser 的權(quán)重,使手去接近物體
      • 這個(gè)期間動(dòng)態(tài)改變同步手的姿態(tài)適配物體形狀,身體方向(前面提到的方法)
        • 手部朝向:同側(cè)肩部朝著物體方向
        • 手部相對(duì)物體位置(手部軸心):物體相對(duì)肩部方向射出射線,從第一個(gè)接觸點(diǎn)向此方向延伸一定距離的位置
    • 接觸到物體
      • 暫停交互進(jìn)程
      • 控制物體的位置與方向朝身體內(nèi)部收(此時(shí)控制物體就是控制整條手臂)
  2. 雙手
    • 另一只手觸發(fā)交互來(lái)拿物體(所有身體部分觸發(fā)后的事件相同的,所以要做好判斷,坑爹的是final ik提供的接口并不好用,似乎設(shè)計(jì)時(shí)沒太考慮到多觸發(fā)的需求)
      • 控制手部軸心使兩只手各在物體兩邊
    • 拿起物體開始轉(zhuǎn)動(dòng)
      • 物體自由旋轉(zhuǎn),哪只手控制,哪只手開啟手部自適應(yīng)
        • 整個(gè)旋轉(zhuǎn)過(guò)程為了保證手部自然,理想的情況是將需要旋轉(zhuǎn)的角度,拆分成數(shù)個(gè)左右手能接受的角度,讓左右手依次將物體旋轉(zhuǎn)到最終角度
    • 放下物體還原
      • 物體移動(dòng)到目標(biāo)位置
      • 繼續(xù)交互進(jìn)程
        • 權(quán)重曲線控制position,rotation,poser 的權(quán)重,使手去離開物體

參考代碼

/// <summary>
    /// 放置物品
    /// 拿起物品后,觸發(fā)此方法,將物品位置與目標(biāo)位置做線性插值,到位后松手
    /// </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://第一次觸發(fā)
                if (isLeftHand)
                {
                    firstHand = FullBodyBipedEffector.LeftHand;

                    firstRotateAngle = 90;

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

                    firstRotateAngle = -90;
                }
                break;
            //第二次觸發(fā)
            case FullBodyBipedEffector.LeftHand:
            case FullBodyBipedEffector.RightHand:
                print("第二只手來(lái)了");//不進(jìn)入adjust階段
                return;
            default:
                Debug.LogError("觸發(fā)居然有其他情況");
                break;
        }

        //進(jìn)入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)
            //另一只手也來(lái)拿物體
            .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("另一只手觸發(fā)居然有其他情況");
                        break;
                }
            })
            .InsertCallback(0.6f, () =>
            {
                //重新設(shè)置兩只手錨點(diǎn)
                //設(shè)置兩只手自適應(yīng)
                foreach (var target in targets)
                {
                    HandAdapterIK2 hand = target.GetComponent<HandAdapterIK2>();
                    switch (target.effectorType)
                    {
                        case FullBodyBipedEffector.LeftHand://左手新起點(diǎn)在物體正左方,手離的稍微開一點(diǎn)
                            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("觸發(fā)居然有其他情況");
                            break;
                    }
                }
            })
            .AppendCallback(() =>
            {
                //transform.SetParent(null);
                print(gameObject.name + " placed adjust point");


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

                //關(guān)閉自適應(yīng)
                //還原兩只手腕方向
                //還原兩只手自適應(yīng)
                foreach (var target in targets)
                {
                    switch (target.effectorType)
                    {
                        case FullBodyBipedEffector.LeftHand://左手新起點(diǎn)在物體正左方
                            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("觸發(fā)居然有其他情況");
                            break;
                    }
                }
            })
            .Append(transform.DOMove(new Vector3(0.1f, 1.2f, 0.028f), 1.5f))
            .AppendCallback(() =>
            {

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

                ////設(shè)置物體層級(jí)
                //gameObject.layer = 0;

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




                //自動(dòng)放下物體
                interactionSystem.ResumeAll();
                //手部歸零
                firstHand = FullBodyBipedEffector.Body;

            })
            ;
        
    }


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容