Final IK 文档手册译文
/*全身BipedIK
Final IK 为采用biped骨骼的角色提供了一个极端灵活、强大且高效的全身IK解算器。FBBIK先把Biped角色映射到一个低解析度的多反应器角色IK绑定,解算它,然后重新把结果映射角色。这个过程在LateUpdate进行,在Mecanim/Legacy动画完成之后,所以它跟animator系统完全分离。
链:
内部,每个肢体和躯干都是FBIKChain类的实例。躯干是根链,只包含一个node,躯干是它的子链。这种设置形成一个围绕根链的多反应器IK树。
节点:
node是链的组成部分。比如,手臂链包括3个节点-上臂、前臂、手。每个节点维护一个到骨骼的引用(node.transform)。当解算器运行或者结束后,解算出的骨骼位置会保存在node.solverPosition。
反应器:
FBBIK有3种反应器-【末端反应器】(手、脚),【中间反应器】(肩膀、大腿)和【多反应器】(躯干)。当旋转【中间反应器】和【多反应器】无效的时候可以转动【末端反应器】。旋转【末端反应器】同样会改变躯干的弯曲方向(除非你用弯曲目标覆盖它)。躯干反应器是一个【多反应器】,意味著它拖动2个大腿反应器(简化躯干定位)。反应器有positionOffset属性可以非常简单的操控下层动画。反应器会在每次解算结束之后将postionOffset重置为Vector3.zero。
拉和伸:
每个链都有pull属性。当pull=1,pull权重跟肢体一样。这意味著你触到所有反应器是没有保障的,太远的会接触不到。可以通过修改reach参数,或增加解算迭代次数,或这每帧多次解算,来调整或者改进这个结果。但是,如果你把左臂链的pull权重设置到1然后其他都为0,那么你可以拉角色的左手到无限远而不丢失联系。
映射:
IKSolverFullBodyBiped解算一个非常低分辨率的快速躯壳。虽然你的角色有多得多的脊椎骨,他可能有两倍的骨骼在手臂和肩膀或者臀部骨骼等等。因此,在解算之前,解算器需要映射高分辨率的骨架到低分辨率的解算骨架,在解算完成之后再逆向操作。有3种映射器,1.IKMappingSpine用来映射盆骨和脊柱;2.IKMappingLimb映射躯干;3.IKMappingBone映射头部;你可以通过IKSolverFullBody.spineMappingIKSolverFullBody.limbMappings和IKSolverFullBody.boneMappings访问他们。
限制:
1.FullBodyBipedIK没有头部反应器。因为头部只有一个骨头,你可以再FullBodyIK结束之后随意转动,而且,只有极少的情况你需要抓住头部拉动角色。即使如此,它可以通过拉动肩膀来很好的模拟。这是一个让程序更快、更稳定的优化措施。
2.FullBodyBipedIK没有手指和脚趾反应器。解算手指IK似乎有点过分使用IK了,因为极少有游戏对手指做绑定。使用104段CCD或者FABRIK链控制手指,可能这样浪费宝贵的毫秒数并不是你想要的。你可以参考DrivingRigdemo了解如何快速摆手指到物体上。
3.FullBodyBipedIK采样角色的初始pose(在Start()函数,当每次你重新初始化解算器的时候)找到以什么方式弯曲肢体。从此,限制-角色的肢体需要被弯曲到自然的方向。有些角色可能是处于T-Pose,他们的肢体是伸直的。有些角色的肢体可能还轻微的往反向弯曲(有些Mixamo绑定的角色)。FullBodyBipedIK会警告你这些问题可能发生。你需要手动在SceneView稍微旋转骨骼到正确的弯曲方向。因为,这些旋转会在播放的时候被动画覆盖,所以你不用担心搞坏你的角色。
4.FullBodyBipedIK 没有肘部、膝部反应器。这功能在将来如果确实需要的话可能增加。仍然支持通过弯曲目标改变肘部、膝盖的位置。
5.Optimize Game Objects 需要关闭,或者至少所有解算器需要的骨骼要引入(FullBodyBipedIK.references)。
6.支持只有扭曲动画的额外骨骼。如果额外骨骼有摆动动画,比如翅膀摆动。那么FBBIK将不能正确解算。
7.FullBodyBipedIK 需要角色开启animatePhysics。这是Mecanim系统的一个bug,它不允许运行时改变animatePhysics,那么FullBodyBipedIK需要根据初始animatePhysics进行刷新。
8.FullBodyBipedIK不会在拉动手的时候旋转肩膀。这样可以维护胸部动画的。大多数情况,这没有问题,但有时候,特别是抓取或者有东西高于头部的时候,让肩膀跟著旋转会更真实。这种情况,你不仅需要一个底层的抬起动画,还需要肩膀骨骼在IK解算器读取pose之前,能用脚本旋转它。这里同样有一个脚本包括这个demo,它叫ShoulderRotator。
9.当你移动【末端反应器】并且反应器权重=0,FBBIK将在动画的时候尝试修复肢体的弯曲方向,当肢体旋转接近180度的时候你将体验到肢体的轰响,就是说,解算器无法知道在这个奇点上如何旋转。比如,你有一个行走动画,手下垂,而你想要够到头顶正上方的某个物体,这时候无论你做反应器旋转动画或者使用弯曲目标,要在接近180度生硬旋转中确保手臂不翻转都会很麻烦。这并不是一个bug,如果我们想要保持默认动画弯曲方向,这在逻辑上是必然会发生的。
10.FullBodyBipedIK认定手肘和膝盖关节是铰链关节,就是说前臂不能相对上臂扭动。在最常见的绑定,像3dsMaxbiped上这没问题,因为这些绑定阻止这类动画出现。然而如果绑定允许这种扭曲。这不会导致FBBIK解算器失效,相反,FBBIK解算器会强制手肘和膝盖约束到铰链关节,即使它没有反应器。这不会改变肢体末端,它会轻微的改变肢体弯曲方向,和骨骼的扭曲。
开始教程:
1.添加FullBodyBipedIK组件到角色根节点(与Animator/Animation组件相同)
2.确认自动检测到的biped references正确。
3.确认跟节点正确检测到。它应该是脊柱下端的骨骼。
4.看下SceneView,确认FBBIK骨架在角色上显示。
5.按Play,操控解算器。
*/
//访问反应器:
public FullBodyBipedIK ik;
void LateUpdate () {
ik.solver.leftHandEffector.position = something;// 设置左手反应器位置(世界坐标)。如果把权重设置到0,反应器会没有效果。
ik.solver.leftHandEffector.rotation = something;// 设置左手反应器旋转(世界坐标)。如果把权重设置到0,反应器会没有效果。
ik.solver.leftHandEffector.positionWeight = 1f;// 反应器位置权重,这里设为1,左手位置会被固定到ik.solver.leftHandEffector.position.
// 反应器旋转权重,这里设为1,左手和手臂旋转会被固定到ik.solver.leftHandEffector.rotation.
// 注意:如果你想要旋转手,同时不改变手臂绑定
// 最好在FBBIK更新结束之后,再直接旋转手(使用委托事件OnPostUpdate)
ik.solver.leftHandEffector.rotationWeight = 1f;
// 使手便宜它的动画位置,如果反应器的positionWeight为1, 它没有作用。
// 注意:在反应器每帧更新完之后他会重置positionOffset 到 Vector3.zero所以你必须另外设置它.
// This enables you to easily edit the value by more than one script.
ik.solver.leftHandEffector.positionOffset += something;
// 反应器模式用来改变肢体行为方式,当没有参与的时候。The effector mode is for changing the way the limb behaves when not weighed in.
// Free 表示node完全有solver决定。Free means the node is completely at the mercy of the solver.
// 如果你碰到动作平滑问题,你可以试试把手的模式改为MaintainAnimatedPosition 或 MaintainRelativePosition
// MaintainAnimatedPosition在每次迭代解算的时候,重置node到骨骼动画位置
// 这对脚非常有用,因为一般你需要他们在动画位置上。
// MaintainRelativePositionWeight 维护相关部位的相对位置,胸部相对手臂,臀部相对腿。maintains the limb's position relative to the chest for the arms and hips for the legs.
// 所以,如果你从左手拉动角色,右手臂会随著胸部运动。
// 一般你不需要把这个行为应用到腿部。
ik.solver.leftHandEffector.maintainRelativePositionWeight = 1f;
// 躯体反应器是一个【多反应器】,表示在解算器里它跟其他node一起操控,明确的说是左右大腿。
// 所以,你可以带著大腿骨骼移动躯体反应器。如果我们设置effectChildNodes 为 false,大腿node就不会被躯体反应器改变。
ik.solver.body.effectChildNodes = false;
// 其他反应器:rightHandEffector, leftFootEffector, rightFootEffector, leftShoulderEffector, rightShoulderEffector,leftThighEffector, rightThighEffector, bodyEffector
// 你也可以通过下面的方法找到反应器:
ik.solver.GetEffector(FullBodyBipedEffector effectorType);
ik.solver.GetEffector(FullBodyBipedChain chainType);
ik.solver.GetEndEffector(FullBodyBipedChain chainType);// 值返回手或脚反应器
}
// 访问链:
public FullBodyBipedIK ik;
void LateUpdate () {
ik.solver.leftArmChain.pull = 1f;// 改变左臂pull值
ik.solver.leftArmChain.reach = 0f;// 改变左臂Reach值
// 其他链:rightArmChain, leftLegChain, rightLegChain, chain (根链)
// 你可以用下面的方法找到链:
ik.solver.GetChain(FullBodyBipedChain chainType);
ik.solver.GetChain(FullBodyBipedEffector effectorType);
}
//访问映射:
public FullBodyBipedIK ik;
void LateUpdate () {
ik.solver.spineMapping.iterations = 2;// 改变脊柱映射迭代次数
ik.solver.leftArmMapping.maintainRotationWeight = 1f;// 使左手处理旋转与动画保持一致。Make the left hand maintain it's rotation as animated.
ik.solver.headMapping.maintainRotationWeight = 1f;// 使头部旋转处理与动画一致。Make the head maintain it's rotation as animated.
}
//运行时添加 FullBodyBipedIK (UMA):
using RootMotion;// 需要先包含RootMotion 命名空间,因为BipedReferences类
FullBodyBipedIK ik;
// 任何时候可以调用下面的方法
// 注意,FBBIK初始化的时候要采样角色pose,所以再调用这个方法的时候肢体需要被弯曲到自然方向
void AddFBBIK (GameObject go, BipedReferences references = null) {
if (references == null) { // 还没有定义biped的时候,自动检测它
BipedReferences.AutoDetectReferences(ref references, go.transform, BipedReferences.AutoDetectParams.Default);
}
ik = go.AddComponent<FullBodyBipedIK>();// 添加组件
// 设置FBBIK到references. 第二个参数可以为空(root node) 如果你信任FBBIK自动检测到根节点在正确的脊柱骨骼上
ik.SetReferences(references, null);
}
//优化FullBodyBipedIK:
//如果角色没有显示,可以使用renderer.isVisible来显示
//大部分时间你不需要这么的solver迭代和脊椎映射迭代。注意: 如果只有一次迭代,角色的肩膀和大腿拉动手和脚的时候可能想脱臼
//如果不需要“Reach”值,请保持它为0。它默认=0.05f来提高精度。
//保持Spine Twist 权重=0。如果你不需要他。
//同样设置"Spine Stiffness", "Pull Body Vertical" and/or "Pull Body Horizontal" =0 可以提高性能。
//肢体:
// LimbIK 继承 TrigonometricIK 来定义3段 类似手臂和腿的肢体。
// LimbIK由以下几个修Bend改器组成:
// Animation: 尝试按动画控制弯曲方向
// Target: 根据目标IKRotation旋转弯曲方向
// Parent: 根据父物体旋转弯曲方向(盆骨或锁骨)
// Arm: 保持在生物统计学的松弛位置弯曲手臂(相对上面的,它的消耗更大)
// 如果所有的bend修改器都不适合你的要求,那么你可以添加弯曲目标,简单如下;
using RootMotion.FinalIK;
public LimbIK limbIK;
void LateUpdate () {
limbIK.solver.SetBendGoalPosition(transform.position);
}
// 这会使limb弯曲到 从第一个bone指向goal位置的方向
// IKSolverLimb.maintainRotationWeight 属性允许操控最后一个骨骼,使它保持在肢体解算器前的世界左边旋转值
// 当你需要复位脚的时候这非常有用,但This is most useful when we need to reposition a foot, but maintain it's rotation as it was animated to ensure proper alignment with the ground surface.
// 开始:
// 添加LimbIK 组件到角色跟物体(角色需要朝前)
// 添加骨骼到LimbIK组件 bone1, bone2 and bone3
// 按play
// 用脚本实现:
public LimbIK limbIK;
void LateUpdate () {
// 改变目标位置、旋转、权重
limbIK.solver.IKPosition = something;
limbIK.solver.IKRotation = something;
limbIK.solver.IKPositionWeight = something;
limbIK.solver.IKRotationWeight = something;
// 改变自动bend修改器
limbIK.solver.bendModifier = IKSolverLimb.BendModifier.Animation;// 按动画控制弯曲方向
limbIK.solver.bendModifier = IKSolverLimb.BendModifier.Target;// 按目标旋转控制弯曲
limbIK.solver.bendModifier = IKSolverLimb.BendModifier.Parent;// 根据父骨骼控制弯曲方向(盆骨、肩膀)
// 尝试按生物学松弛状态控制手臂弯曲方向
// 腿部不会受这个影响
limbIK.solver.bendModifier = IKSolverLimb.BendModifier.Arm;
}
//运行时添加LimbIK:
//通过脚本添加
LimbIK.solver.SetChain()
//旋转限制:
//所有旋转约束和其他FinalIK组件都是基于Quaternion(四元数)和Axis-Angle(轴角)以确保一致性,连续性和最小化奇异问题。FinalIK没有包含简单欧拉角选项。
//所有旋转约束基于local rotation 并且像Physics 关节一样使用初始local rotation 作为引用。这使它轴独立,并容易设置。
//所有旋转约束可以在SceneView 可以undo
//所有旋转约束支持与IK解算器联合工作
//角度mode
//简单的角度摆动和扭动限制
//铰链mode
//铰链旋转限制限制限制关节只绕某个轴旋转一定角度。它旋转值可以超过360度。
//多边形mode
// 使用一个球面多边形来限制旋转范围,这是普遍存在的球窝使关节。A reach cone is specified as a spherical polygon on the surface of a a reach sphere that defines all
// positions the longitudinal segment axis beyond the joint can take.
// twist limit 参数定义了围绕主轴最大扭转角度
// 这个类基于论文:
// "Fast and Easy Reach-Cone Joint Limits"
// Jane Wilhelms and Allen Van Gelder. Computer Science Dept., University of California, Santa Cruz, CA 95064. August 2, 2001
// 多边形角度限制模式提供方便快速编辑SceneView工具,来编辑、克隆和修改reachcone点
//样条线mode
// 使用样条线限制普遍的球窝关节的旋转范围
// 通过AnimationCurve正交投影到球面获得光滑、快速的限制范围。
// twist limit 参数定义了围绕主轴最大扭转角度
// 样条线角度限制模式提供方便快速编辑SceneView工具,来编辑、克隆和修改reachcone点
//扩展Final IK
// FinalIK的IK解算器和旋转限制架构基于可扩展思想搭建。
// FinalIK的一些组件比如BipedIK, 本质上仅仅是IK解算器的收集器
// 自定义IK组件:
// 在你发掘出FinalIK全部能力之前,了解一些它的架构非常重要。
// IK组件和IK解算器之间的区别:
// 架构上,IK解算器类包括反向关节功能,而IK组件只是拥有、初始化、更新他的解算器然后提供SceneView操作手柄和自定义inspector。
// 因此,IK解算器完全独立于他的组件,并且可以完全脱离直接引用来使用:
using RootMotion.FinalIK;
public IKSolverCCD spine = new IKSolverCCD();
public IKSolverLimb limb = new IKSolverLimb();
void Start() {
// 基于多种原因,根transform引用在IK解算器初始化的时候引用。
// 启发式解算器(CCD) IKSolverCCD, IKSolverFABRIK and IKSolverAim 只需要用它作为警告log的上下文。
// 角色解算器 IKSolverLimb, IKSolverLookAt, BipedIK and IKSolverFullBodyBiped 用它来定义相对与角色的方向。
// IKSolverFABRIKRoot 使用它作为所有FABRIK链的根
spine.Initiate(transform);
limb.Initiate(transform);
}
void LateUpdate() {
// 按顺序更新IK解算器
// 在多IK解算器拥有骨骼层次,先初始化的它的父物体比较好。
spine.Update();
limb.Update();
}
// 你现在有了一个自己的IK组件
// 如果你想把自己的功能全部放在单个组件里,像 BipedIK,所以你不会管理很多不同的IK组件在你的场景里
// 写自定义旋转限制:
// 所有旋转限制都继承抽象类RotationLimit 构建自己的类同样需要继承这个基类,并覆盖抽象方法。
protected abstract Quaternion LimitRotation(Quaternion rotation);
// 这个方法里你需要应用约束,并返回输入Quaternion
// 注意:比较重要,Quaternion已经转换为物体的默认的local rotation空间,意味著如果你返回Quaternion.identity,物体会总是会被修正到他的初始local rotation
// 下面的代码是一个创建自定义旋转限制的模板:
using RootMotion.FinalIK;
// 声明类并继承RotationLimit.cs
public class RotationLimitCustom: RotationLimit {
// 在实例Transform的local空间限制旋转
protected override Quaternion LimitRotation(Quaternion rotation) {
return MyLimitFunction(rotation);
}
}
// 新旋转限制由所有可约束的IK解算器认可并自动应用。
// 组合IK组件:
// 当创建更复杂的IK系统时,你可能需要完全控制解算器的更新顺序。要这么做,你只要禁用他们的组件,并用外部脚本管理他们的解算器
// 所有IK组件继承类IK,而所有IK解算器继承抽象类IKSolver。这让你很容易的掌控和替代解算器,而不需要知道特别的解算器类型
// 控制多重IK组件的更新顺序
using RootMotion.FinalIK;
// IK组件数组,你可以从inspector赋值
// IK是抽象类,所以你不用使用的是考虑那个特别的IK组件类型 is abstract, so it does not matter which specific IK component types are used.
public IK[] components;
void Start() {
// 禁用所有其他IK组件,这样他们不会更新解算器。使用 Disable() 代替=false 后者不能保证初始化。
foreach (IK component in components) component.Disable();
}
void LateUpdate() {
// 按顺序更新IK解算器
foreach (IK component in components) component.GetIKSolver().Update();
}