找回密码
 立即注册
查看: 203|回复: 0

游戏引擎应用-Unreal Engine内容示例之Control Rig分析(二)

[复制链接]
发表于 2022-9-24 05:59 | 显示全部楼层 |阅读模式
0. 前言(续)

上一篇
6. Sphere Trace

IK又IK,这个例子的强度和我之前分析的ALS没法比,这里简单按照Sequence讲下流程,借此机会看下IK是怎么做的。
这里面将两个Control绑上值,但按理来说foot_r_ctrl是不用绑的因为第二步会赋值。
这里和之前一样,也是根据脚的骨骼点位置,建立起始点和终止点,Trace一遍看有没有Hit,如果Hit到了设定的ObjectType(Example中为WorldStatic一种),那么把影响作用到foot_r_ctrl控制点上,这里很好玩的是,他用Aim Math做的Rotation,也是没谁了,同时Translation只加z轴上的分量即可。如果Hit不到,那么就将foot_r原始的Transform给控制点。
将数据放进IK求解,下面的分析基本上包括了求解IK的所有过程,比较难懂的地方我都写了注释。
//这个函数是后面要用到的,可以等会再看
bool FCachedRigElement::UpdateCache(const FRigElementKey& InKey, const URigHierarchy* InHierarchy)
{
        if(InHierarchy)
        {
                //Valid:是否是一个正常的Key,需要Index Valid并且Key Valid
                //Identical:是否和现在Hierarchy中存的信息时间戳一致
                if(!IsValid() || !IsIdentical(InKey, InHierarchy))
                {
                        Reset();
                        int32 Idx = InHierarchy->GetIndex(InKey);
                        if(Idx != INDEX_NONE)
                        {
                                Key = InKey;
                                Index = (uint16)Idx;
                                Element = InHierarchy->Get(Index);
                        }
                        ContainerVersion = InHierarchy->GetTopologyVersion();//同步时间戳
                }
                return IsValid();
        }
        return false;
}
//Key类型就是我们在蓝图中经常能够看见的,就是Type类型里面有Bone、Control那些,和Name名字,具体可以在蓝图中对照
USTRUCT(BlueprintType)
struct CONTROLRIG_API FRigElementKey
{
...
        UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Hierarchy")
        ERigElementType Type;
        UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Hierarchy", meta = (CustomWidget = "ElementName"))
        FName Name;
...
        FORCEINLINE bool IsValid() const
        {
                return Name != NAME_None && Type != ERigElementType::None;
        }
...
}

//--------------------------------------------主体!----------------------------------------
//USTRUCT(meta=(DisplayName="Basic IK", Category="Hierarchy", Keywords="TwoBone,IK", NodeColor="0 1 1"))
FRigUnit_TwoBoneIKSimplePerItem_Execute()
{
        DECLARE_SCOPE_HIERARCHICAL_COUNTER_RIGUNIT()
        //这个HierarchySystem似乎也值得研究一下,基本上很多数据都是从Hierarchy中获取的
        URigHierarchy* Hierarchy = ExecuteContext.Hierarchy;
        if (Hierarchy == nullptr)
        {
                return;
        }
        //初始Init所有的Cached类型,这个是和Hierarchy一起用的
        if (Context.State == EControlRigState::Init)
        {
                CachedItemAIndex.Reset();
                CachedItemBIndex.Reset();
                CachedEffectorItemIndex.Reset();
                CachedPoleVectorSpaceIndex.Reset();
                return;
        }
        if (!CachedItemAIndex.UpdateCache(ItemA, Hierarchy) ||
                !CachedItemBIndex.UpdateCache(ItemB, Hierarchy) ||
                !CachedEffectorItemIndex.UpdateCache(EffectorItem, Hierarchy))
        {
                return;
        }
        //UpdateCache是将Hierarchy中的数据放到Cache中,通过Cache这个名字也可以知道
        //利用时间戳同步Key的数据,然后更新,等于说Key在Valid的时候都可以直接用Cache而不需要找Hierachy
        CachedPoleVectorSpaceIndex.UpdateCache(PoleVectorSpace, Hierarchy);
        if (Weight <= SMALL_NUMBER)
        {
                return;
        }

        FVector PoleTarget = PoleVector;
        //Example中是没有这一项的,所以Example中不会进行一次变换,设定的PoleTarget就是最后的位置
        if (CachedPoleVectorSpaceIndex.IsValid())
        {
                const FTransform PoleVectorSpaceTransform = Hierarchy->GetGlobalTransform(CachedPoleVectorSpaceIndex);
                if (PoleVectorKind == EControlRigVectorKind::Direction)
                {
                        PoleTarget = PoleVectorSpaceTransform.TransformVectorNoScale(PoleTarget);
                }
                else
                {
                        PoleTarget = PoleVectorSpaceTransform.TransformPositionNoScale(PoleTarget);
                }
        }
        //初始化三个Transform量
        FTransform TransformA = Hierarchy->GetGlobalTransform(CachedItemAIndex);
        FTransform TransformB = TransformA;
        TransformB.SetLocation(Hierarchy->GetGlobalTransform(CachedItemBIndex).GetLocation());
        FTransform TransformC = Effector;
        //这两个量在Example中为0,看看是用来干什么的
        float LengthA = ItemALength;
        float LengthB = ItemBLength;
        //如果LengthA=0也就是没输入,那么这里会利用Initial的Transform计算出第一个骨骼的长度
        if (LengthA < SMALL_NUMBER)
        {
                FTransform InitialTransformA = Hierarchy->GetInitialGlobalTransform(CachedItemAIndex);
                FVector Scale = FVector::OneVector;
                if (InitialTransformA.GetScale3D().SizeSquared() > SMALL_NUMBER)
                {
                        Scale = TransformA.GetScale3D() / InitialTransformA.GetScale3D();
                }
                FVector Diff = InitialTransformA.GetLocation() - Hierarchy->GetInitialGlobalTransform(CachedItemBIndex).GetLocation();
                Diff = Diff * Scale;
                LengthA = Diff.Size();
        }
        //如果LengthB=0也就是没输入,并且下面还有Effector
        //那么这里会利用Initial的Transform计算出第二个骨骼的长度
        if (LengthB < SMALL_NUMBER && CachedEffectorItemIndex != INDEX_NONE)
        {
...                //和上面一模一样
        }
        //经过Check与初始化,还是救不回来,就寄!
        if (LengthA < SMALL_NUMBER || LengthB < SMALL_NUMBER)
        {
                UE_CONTROLRIG_RIGUNIT_REPORT_WARNING(TEXT("Item Lengths are not provided.\nEither set item length(s) or set effector item."));
                return;
        }
        //好长的调用,也就是解IK的关键
        FControlRigMathLibrary::SolveBasicTwoBoneIK(TransformA, TransformB, TransformC,
                PoleTarget, PrimaryAxis, SecondaryAxis, SecondaryAxisWeight,
                LengthA, LengthB, bEnableStretch, StretchStartRatio, StretchMaximumRatio);

        if (Context.DrawInterface != nullptr && DebugSettings.bEnabled)
        {
...                //绘制方法
        }
        //根据Weight进行调整,现在TransformA、TransformB、TransformC已经是正确的了
        //所以Slerp的对象是Cached和这几项
        if (Weight < 1.0f - SMALL_NUMBER)
        {
                FVector PositionB = TransformA.InverseTransformPosition(TransformB.GetLocation());
                FVector PositionC = TransformB.InverseTransformPosition(TransformC.GetLocation());
                TransformA.SetRotation(FQuat::Slerp(Hierarchy->GetGlobalTransform(CachedItemAIndex).GetRotation(), TransformA.GetRotation(), Weight));
                TransformB.SetRotation(FQuat::Slerp(Hierarchy->GetGlobalTransform(CachedItemBIndex).GetRotation(), TransformB.GetRotation(), Weight));
                TransformC.SetRotation(FQuat::Slerp(Hierarchy->GetGlobalTransform(CachedEffectorItemIndex).GetRotation(), TransformC.GetRotation(), Weight));
                TransformB.SetLocation(TransformA.TransformPosition(PositionB));
                TransformC.SetLocation(TransformB.TransformPosition(PositionC));
        }
        //按照Index设置,这个函数也见过很多次了,其有不同的重载
        Hierarchy->SetGlobalTransform(CachedItemAIndex, TransformA, bPropagateToChildren);
        Hierarchy->SetGlobalTransform(CachedItemBIndex, TransformB, bPropagateToChildren);
        Hierarchy->SetGlobalTransform(CachedEffectorItemIndex, TransformC, bPropagateToChildren);
}

//--------------------------------------------SolveBasicTwoBoneIK----------------------------------------
void FControlRigMathLibrary::SolveBasicTwoBoneIK(FTransform& BoneA, FTransform& BoneB, FTransform& Effector,
        const FVector& PoleVector, const FVector& PrimaryAxis, const FVector& SecondaryAxis,
        float SecondaryAxisWeight, float BoneALength, float BoneBLength, bool bEnableStretch,
        float StretchStartRatio, float StretchMaxRatio)
{
        //获取三个Pos,分别是上中下三个骨骼
        FVector RootPos = BoneA.GetLocation();
        FVector ElbowPos = BoneB.GetLocation();
        FVector EffectorPos = Effector.GetLocation();
        //又是一个调用,其实这个就是ALS里面调用的函数,正常接触过动画的已经知道这个是什么了
        //FAnimNode_TwoBoneIK::EvaluateSkeletalControl_AnyThread就是之前AnimGraph里面节点的执行逻辑
        //里面的逻辑和这整个函数非常相似,同样也调用了这个Solve,这个函数里面注释写的非常详细
        //void SolveTwoBoneIK(const FVector& RootPos, const FVector& JointPos, const FVector& EndPos,
        //        const FVector& JointTarget, const FVector& Effector,
        //        FVector& OutJointPos, FVector& OutEndPos,
        //        float UpperLimbLength, float LowerLimbLength, bool bAllowStretching,
        //        float StartStretchRatio, float MaxStretchScale)
        AnimationCore::SolveTwoBoneIK(RootPos, ElbowPos, EffectorPos,
                PoleVector, EffectorPos,
                ElbowPos, EffectorPos, //解算出来的中间节点和末端节点
                BoneALength, BoneBLength, bEnableStretch,
                StretchStartRatio, StretchMaxRatio);

        BoneB.SetLocation(ElbowPos);
        Effector.SetLocation(EffectorPos);
        //PrimaryAxis是(1,0,0),在上一章分析过BoneTransform的X方向指向,现在也就是大腿指向小腿的方向
        //还是要具体看骨骼是怎么设置的,比如左腿右腿就完全不一样的Transform
        FVector Axis = BoneA.TransformVectorNoScale(PrimaryAxis);
        FVector Target1 = BoneB.GetLocation() - BoneA.GetLocation();

        FVector BoneBLocation = BoneB.GetLocation();
        if (!Target1.IsNearlyZero() && !Axis.IsNearlyZero())
        {
                Target1 = Target1.GetSafeNormal();
                //了解IK的底层原理就知道,为了不损失自由度,我们会强制让IK的结果不共线,这里就是对共线的处理
                {
                        FVector BaseDirection = (EffectorPos - RootPos).GetSafeNormal();
                        if (FMath::Abs(FVector::DotProduct(BaseDirection, Target1) - 1.0f) < KINDA_SMALL_NUMBER)
                        {
                                // Offset the bone location to use when calculating the rotations
                                BoneBLocation += (PoleVector - BoneBLocation).GetSafeNormal() * KINDA_SMALL_NUMBER*2.f;
                                Target1 = (BoneBLocation - BoneA.GetLocation()).GetSafeNormal();
                        }
                }
                //Axis到Target1,其实如果Axis真选了异常的,我感觉也很难想象出变成什么样子
                //解算出的Rotation作用到第一节骨骼上
                FQuat Rotation1 = FQuat::FindBetweenNormals(Axis, Target1);
                BoneA.SetRotation((Rotation1 * BoneA.GetRotation()).GetNormalized());
                //这个Axis是指向膝盖侧外面的,特别注意左右脚在这个Axis上存在不同
                Axis = BoneA.TransformVectorNoScale(SecondaryAxis);

                if (SecondaryAxisWeight > SMALL_NUMBER)
                {
                        //中点计算
                        FVector Target2 = BoneBLocation - (Effector.GetLocation() + BoneA.GetLocation()) * 0.5f;
                        if (!Target2.IsNearlyZero() && !Axis.IsNearlyZero())
                        {
                                Target2 = Target2 - FVector::DotProduct(Target2, Target1) * Target1;
                                Target2 = Target2.GetSafeNormal();
                                //Target2是计算出来给翻大腿外侧的
                                //等于说,PrimaryAxis是让我们旋转第一个骨骼点以对准第二个骨骼点的位置
                                //那么SecondaryAxis就是让我们旋转大腿让之前对准膝盖的外侧皮肤旋转至对准膝盖
                                //把大腿想成一个直的圆柱,SecondaryAxis就是旋转圆柱的中轴,这样可以保证正确性
                                //因为虽然PrimaryAxis旋转之后一定能对准骨骼点,但不能保证大腿自身旋转正确
                                FQuat Rotation2 = FQuat::FindBetweenNormals(Axis, Target2);
                                if (!FMath::IsNearlyEqual(SecondaryAxisWeight, 1.f))
                                {
                                        FVector RotationAxis = Rotation2.GetRotationAxis();
                                        float RotationAngle = Rotation2.GetAngle();
                                        Rotation2 = FQuat(RotationAxis, RotationAngle * FMath::Clamp<float>(SecondaryAxisWeight, 0.f, 1.f));
                                }
                                BoneA.SetRotation((Rotation2 * BoneA.GetRotation()).GetNormalized());
                        }
                }
        }
...        //同样的方式处理BoneB,这里就不再赘述
}
至此,IK的逻辑就梳理完毕了,感觉确实对于我这种新手来说,难点还是在骨骼Transform设置的标准上,如果有能力可以自行去TwoBoneIK.cpp查看底层TwoBoneIK的求解过程,难倒是不怎么难,不过依我这么浅显的阅历感觉这个Pole的设计也不是很好。
7.IK w/ Sphere Trace

又是两个IK的例子,还是根据Sequence分层说明吧。
①还是将Control设置好,这次又整了一个新活,Project to new Parent,初始的Control黏到Initial的calf_l,然后再换Parent黏到现在的calf_l,这TM不是脱裤子放屁么,之前用Parent Constraint开启Maintain Offset不就可以保持Initial状态的相对Offset不变么?(流汗黄豆)
②这个蓝图实现了一个函数,IK Foot Trace,其实就是上面那个示例的Trace封装了一下,因为是Example里面连Hit都没判断,是真的懒啊。然后就是这里例子最精巧的地方了,其实平时做过一点点IK的都知道,如果直接用Trace到的位置赋值,那么上下楼梯的时候都会产生非常剧烈的抖动,因为腿在跨过楼梯的一瞬间IK就作用上去了,也就是角色的IK变换不平滑,这里将Trace获得到的IK Offset,过一趟上篇分析过的Spring Interpolate,并且设置了Critical Damping=1,这不就实现在最短时间内回归到平衡状态了?感觉这个方式的平滑过渡可以用在很多地方,说实话这个想法有点惊艳到我。
③后面也就是执行Basic IK的过程,就不细说了,不过我个人觉得,该是Control的就是Control的,设计的时候应该将Control点的Transform给更新掉,而不是在Basic IK前想打补丁一样贴上去一个糊糊。总的来说这个例子我还是挺喜欢的。
既然你不CheckHit,那我就把Basic IK的拉伸打开,Start调到1,Max调到2,再Trace 500的距离。



脚环的Transform更新了,但是PoleVector的就没有更新

第二个例子又给了一个新的思路,对于脚部IK,那应该得是所有时候都有的,但是对于手部IK就不一定了,这里把Hit的Check传递到Basic IK的Weight上,上面仔细看过源码就知道,源码中内置了很多Check,没通过都是直接不执行的,所以这里Weight变成0的时候就不执行了,感觉比牵出来一个Branch要简化很多。他这个例子的设计又符合我之前说的设计思路了,真感觉一个蓝图换一个人做。

8. Procedural Animation States

这个例子展示了程式化控制Animation状态的例子,整体的逻辑是,例子设计了一个蓝图接口,这个接口可以接收Button函数,然后设计一个蓝图类ProceduralAnimStates,实现了接受Button函数的接口,也就是IsAttacking的状态转换,将骨骼网格体放在这个下面,然后骨骼网格体在每个Tick就可以访问ProceduralAnimStates的Attacking状态。
在按钮端,设置了一个碰撞体,并且设置了可以影响的ActorList(将实例化的Actor放进去),在碰撞检测到的时候就调用Actor对Button反应的接口,也就是上面所说的IsAttacking的状态切换,实用价值有待商榷,但感觉用在做Demo的实现的时候,提高交互性非常合适。

9. Layering Rigs

在原有的动画逻辑上,我们还能对单个的物件添加分层动画逻辑。这个速度调节就是刚才学习的Procedural的方式,人物的奔跑是通过混合动画实现的,里面可以根据不同的Speed输出不同的姿态,而我们需要通过Speed在Control Rig的Layer添加额外的动画。虽然这个看起来功能很不起眼,但是这个例子恰好说明了,使用Control Rig,可以直接套用在之前做好的东西上开发而不需要整套系统都迁移到Control Rig才能用!并且对小物件的控制也可以分成很多个模块,这样还能提高复用性。
里面的做法其实就非常简单了,先是经过一个Curve,将速度映射到 \Delta \omega 上(因为要风车转得是不断累积的Rotation),然后经过一个AlphaInterp节点,这个节点对启动和停止都起到了缓冲作用,如果没有这个节点,那么Speed归零的时候马上就不转了,等于说Curve提供了最大值,而AlphaInterp提供了加速状态,加速度就是Interp Speed,增加与减少也能分别控制。然后再经过一个Accumulate Add节点,最后将旋转值给到Rotation。这个插值累积的方案也可以学一下。
//USTRUCT(meta=(DisplayName="Alpha Interpolate", Keywords="Alpha,Lerp,LinearInterpolate", Category = "Simulation|Time", PrototypeName = "AlphaInterp", MenuDescSuffix = "(Vector)"))
FRigUnit_AlphaInterp_Execute()
{
        DECLARE_SCOPE_HIERARCHICAL_COUNTER_RIGUNIT()

        ScaleBiasClamp.bMapRange = bMapRange;
        ScaleBiasClamp.bClampResult = bClampResult;
        ScaleBiasClamp.bInterpResult = bInterpResult;

        if (Context.State == EControlRigState::Init)
        {
                ScaleBiasClamp.Reinitialize();
        }
        else
        {
                ScaleBiasClamp.InRange = InRange;
                ScaleBiasClamp.OutRange = OutRange;
                ScaleBiasClamp.ClampMin = ClampMin;
                ScaleBiasClamp.ClampMax = ClampMax;
                ScaleBiasClamp.Scale = Scale;
                ScaleBiasClamp.Bias = Bias;
                ScaleBiasClamp.InterpSpeedIncreasing = InterpSpeedIncreasing;
                ScaleBiasClamp.InterpSpeedDecreasing = InterpSpeedDecreasing;

                Result = ScaleBiasClamp.ApplyTo(Value, Context.DeltaTime);
        }
}

float FInputScaleBiasClamp::ApplyTo(float Value, float InDeltaTime) const
{
        float Result = Value;
        if (bMapRange)
        {
                Result = FMath::GetMappedRangeValueUnclamped(InRange.ToVector2f(), OutRange.ToVector2f(), Result);
        }
        Result = Result * Scale + Bias;
        if (bClampResult)
        {
                Result = FMath::Clamp<float>(Result, ClampMin, ClampMax);
        }
        if (bInterpResult)
        {
                if (bInitialized)//这个是True的话代表确认已经Initialize过了
                {
                        const float InterpSpeed = (Result >= InterpolatedResult) ? InterpSpeedIncreasing : InterpSpeedDecreasing;
                        Result = FMath::FInterpTo(InterpolatedResult, Result, InDeltaTime, InterpSpeed);
                }

                InterpolatedResult = Result;
        }
        bInitialized = true;
        return Result;
}

10. Additive FK Control Rig

其实我还不太懂Sequencer的逻辑,但是这个例子其实是说明了,在Sequencer中,我们可以在已有动画基础上,再调节骨骼实现更加丰富的效果。对于这个例子,Additive就是spine中的几节,就实现了走路时躯干左右摇晃的效果。



11. Rig Inversion

这个是说明了Control Rig其中一个非常强大的功能,将骨骼动画逆解算到Control控制的动画中。我感觉看了那么多示例应该是有能力看懂这个例子的,其RigFile为CR_Mannequin_Body。一个大的Control Rig分为三个部分,第一个部分是Setup Solve,这个部分主要是利用Initial状态的Rig计算出Initial状态的Control。
第二个部分是Forward Solve,这个部分主要是利用Current状态的Control计算出Current状态的Rig。
第三个部分是Backwards Solve,这个部分主要是利用Current状态的Rig计算出Current状态的Control。
我自己在看的时候还是按身体部位来看的,所以下面的逻辑可能也会这样。
11.1 Spine

Spine作为脊柱,基本上是一个宏观控制的存在,所以先分析这个部分的逻辑。
首先看一下其树状结构



Control结构



Rig结构

给一个先入为主的观念,body_control控制了整个躯干,但是无法控制四肢IK的作用,body_offset_ctrl可以无视IK与FK的区别,而hips_ctrl是专门控制下半身的部分,spine_01_ctrl是专门控制上半身的部分。
11.1.1 setup spine

在Setup的时候,body_ctrl的Translation设置为了spine_01和spine_02中点的值,而hips_ctrl和spine_01_ctrl分别绑定到了骨骼spine_01、spine_02的Translation,这样的设计让body_ctrl对这两个Control的管理体现了出来,后续对spine_0x、neck_0x的设置就是照猫画虎。而最后一个设置却有点意思,也就是head,head这里他分出了一个head_ctrl_space和head_ctrl,这里先记着,下面在计算的时候就可以看来其用处了。
其实很简单,Setup这里就做完了,我们可以看下Setup完毕的Control怎么影响Rig的。
11.1.2 spine_fk_setup

前面给脊柱设置好对应的值,特别注意这里给pelvis骨骼设置了值,因为只有设置这个全身才能转动,同时也需要注意骨骼层次与Transform传递的关系。后面将双腿FK计算的space设置为hips_ctrl(之前说过这个可以单独控制下肢)
11.1.3 neck_setup

前面还是正常的将Rig附上对应Control的相对Transform(也就是Project to new Parent+Set Transform),下一步就要涉及到head_local的使用了。
①neck移动了我们头还是得跟着移动,也就是我们需要先单独将neck的Translation作用到head_ctrl_space上。
②如果将Control——head_local置为了True,那么head_ctrl_space的Rotation就要变成body_ctrl的Rotation,也就是body_ctrl以上,spine_0x_ctrl的旋转(由层次结构带来的),以及neck_0x_ctrl的旋转(由Graph节点计算带来的,因为neck_0x_ctrl和head_ctrl_space无层次结构关系)都与头的旋转无关了;如果head_local为False,那么head的旋转就与连到这个部分的骨骼都有关系。



打开head_local

③最后将head设置为head_ctrl的Transform。
为什么要这么设计呢?其实head的例子体现不了这一点,虽然还没有解释到,但是这里可以体现说明一下,对于手臂来说,如果我们要控制finger组的父亲Control,应该怎么设定呢?


对于Rig来说可能想都不用想,我们直接黏在hand骨骼点上就可以,然而对于Control,同一个骨骼点可能有不同类型的Control,这样我们整个Finger组只放ik、只放fk、两边都放,这样的设计都不合适,最合适的就是将整个Finger组的Parent抽象出来,这就是设计逻辑。
11.1.4 clavicles_setup

左手右手都是同样的逻辑,我们只需要看一边即可,但是为什么两边的函数都会有不同(这真的是一个团队做的么),按理来说还是定式好一点(也就是Project to new Parent+Set Transform),这里面也有local的逻辑,和上面一样就不赘述了。
11.1.5 spine_inversion

除了head以外,都是正常的黏住,不过整体和setup spine差不多,浏览一下就可以了。
基本上Spine的逻辑已经囊括了很多个操作定式,一些不一样的逻辑也基本上是由骨架结构带来的。
11.2 Leg

接下来先研究IK常客Leg。
11.2.1 setup left leg

前面是正常的设置,后面需要一些思考,首先是foot_l_ik/fk_ctrl,这个地方就是ik只需要Translation,同样ball_l也是这么处理的,在处理fk的时候Rotation做了个Y的旋转+90(为什么),然后同样的Translation赋给ik。对于ik还需要一个pole来提供弯曲方向,其实这个部分不明白为什么要用两个完全一样的Transform去构造一个Identity的Transform,其实蓝图里面很多节点都没必要的,要实现这个就构造一个腿部半长Y的Transform,然后用膝盖的Transform作用上去,下图展示了膝盖的Transform,其Y轴是绿的,我们构造的半长Transform就在了蓝色球(leg_l_pv_ik_ctrl)的位置。



加粗的Transform是关节的Transform,此时其Y方向指向弯曲方向

11.2.2 setup right leg (Mirrored)

这个就很有意思了,枚举左脚刚才设置的所有节点,然后用Replace换成右脚对应的节点,然后将左脚的Transform用一个Scale转换之后就可以赋值给右脚了。


11.2.3 left_leg_setup

对于腿来说,比较主要的就是有一个ik的开关,控制IK与FK的切换。
①controls_visibility
里面与ik开关控制ik组与fk组的显隐逻辑。
②Leg_left_ik_fk_switch
开关Switch的Branch
③Controls_and_joints_Collections
一组是Rig包括thigh_l、calf_l、foot_l、ball_l,一组是所有与这条腿有关的Control,包括了IK与FK。
④leg_left_FK_setup
先对Rig进行遍历,利用Concat搞到对应的Control,然后在将相对变换作用上去


所有骨骼遍历完毕之后,需要修改leg_l_pv_ik_ctrl的位置,在设置的时候我们用的膝盖局部空间,但是在这个地方更新的时候是使用的thigh_l的相对变换,可能是thigh_l的相对稳定,这也解释了为什么上面要用腿长一半作为长度,后面就是逐个设置ik control的位置。在最后有一个Branch,这个Branch触发是设置leg_l_fk_ik_switch从True到False的时候leg_l_ik_mode此时为True,等于说刚从IK切换到FK的第一次,会进入这个Branch中的逻辑。暂时还不知道这个Send有什么用处。


⑤leg-left_ik_setup
第一步就是解BasicIK,这步和以前的IK一样,之后花了一个Line可视化PoleVector,根据ball_ik设置ball,这些做完之后,逐个设置fk control的位置。最后就是一个与上面相反的Branch。


从上面可以看出来,整体的逻辑就是ik就先计算ik再用结果设置fk,fk就先计算fk再用结果设置ik,这样能够保证两组节点都可以在非自己状态上,表现正确的结果。右腿同理。
11.2.4 left_leg_inversion

对于逆向解算,这个部分比上面都简单,除了PoleVector也就是leg_l_pv_ik_ctrl用的是thigh_l,其他都是用的同名骨骼,其实可以写的很简单的。


左腿就分析完了,感觉又简单又意犹未尽的感觉。
11.3 Arm

手臂看起来比腿复杂一点。其主要结构都比腿要长,主要骨骼点如下
clavicle(锁骨)->upperarm->lowerarm->hand,其中upperarm到hand是手部的TwoBone。
11.3.1 setup left arm

Setup阶段先直接设置前三个主要骨骼点,然后设置手臂的PoleVector(arm_l_pv_ik_ctrl),设置方法和腿部的一样,也是一样做了很多奇怪的计算。设置hand的ik与fk以及finger的space。还有finger,finger节点多,但是都进Array了,用Concat转化用手指Rig Initial设置Control Offset。
11.3.2 setup right arm (Mirrored)

左手所有Finger都做完了,就会进入右手臂的设置,这个Mirror除了骨骼种类不一样,和腿部的Mirror是差不多一致的。
11.3.3 left_arm_setup

逻辑和腿一致,fk和ik的转换也一样,ik就先计算ik再用结果设置fk,fk就先计算fk再用结果设置ik。


11.3.4 fingers

手臂比腿要多一个环节,就是手指的控制。根据之前对Space的描述,这里将hand骨骼点的Transform作用到finger Control组的Parent上面,也就是fingers_l_space,后面就是for循环赋值很简单。
11.3.5 left_arm_inversion

这个部分就是最后一个部分了,读者就尝试自己分析一下吧。如果要检验一个知识是不是真懂了,我觉得用自己给自己讲课的方式来描述一个技术的过程是最合适的,假如你没有成功的自己给自己完整叙述这一步的整个流程的逻辑,那么还是需要多熟悉一下整个框架,以及整体的设计思想。
所有的结构就分析完了,反过来看,其实整体真的是很多的复制粘贴,逻辑重复的地方太多了,有的地方似乎官方也没有给出一个最简单的表示方式。特别是部分骨骼进Array,部分不进,进不进的评判标准应该就是是否能够用同一种执行方式,而官方显然不是这么想的,其更喜欢ik与fk分开,这样其实不利于找出一些特殊的情况,ik就每个列出来,fk就合成Array执行,不太好。
而且感觉可能这个案例可以实现的效果没有想象中多?毕竟我也不知道怎么测试了。
12. Rig Sharing

这个也是一个特性,等我更加进阶的时候再来分析吧。
13. Splines

这个也感觉好好玩。
Setup阶段会把所有的Spline Control放到一个Array里面,并且开一个等长的Spline Points。
Forwards阶段
老实巴交地把root rig设置为root control
根据Spline Control的Transforms设置Spline Points的Transforms。
利用这一组Points生成样条曲线(生成的时候可以设置生成的参数),然后用Fit Chain on Spline Curve将样条曲线作用到骨骼链上。
将黄色球的Transform作用到蓝色框上。
和上面的体量根本没法比,这个Example提供了打fk框架里面套小ik的方案,还行。
14. 总结

其实示例给了很多基础的实现方法,从这上面可以看出这个系统真的感觉设计的非常前卫,我挺喜欢的,不知道在工程实际应用中会表现如何了,特别是相当于加了一层解释器(这个比喻我觉得应该没错哈哈),在移动端的效率也是一个需要考虑的点。
总的来说,我觉得未来可期,特别是Forwards和Backwards的想法,有点牛逼。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-7-4 10:34 , Processed in 0.105657 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表