Unity 3D 手部动作捕捉中出现的问题和解决方案
基本实现
手部的位置获取和Unity中的红球绿线的实现是跟着该视频完成的。将手部节点坐标映射到手上还需要一些额外的操作。其中最重要的父子节点相对旋转的代码:
private void UpdateBone(AvatarTree tree, float lerp)
{
var dir1 = tree.GetDir();
var dir2 = pose_joint[tree.idx].transform.position - pose_joint[tree.parent.idx].transform.position;
//dir2.y = -dir2.y;
Quaternion rot = Quaternion.FromToRotation(dir1, dir2);
Quaternion rot1 = tree.parent.transf.rotation;
var lerpToAngle = rot * rot1;
tree.parent.transf.rotation = Quaternion.Lerp(rot1, lerpToAngle, lerp);
}
参考了该网站中描述的方法。
问题
在完成上面的步骤后,手部动作的映射依然会遇到比较麻烦的问题。在重复若干次握拳的动作后,手指发生如下图中的扭曲。
估计原因
由于该方法是根据两个手部节点的坐标(就是场景中相邻的两个小球)来计算出旋转的Quaternion的。这样得出的Rotation在x和z轴的旋转都是没有问题的,但是在y轴的旋转就有很多问题了。我们的代码无法判断出节点(红色小球)的本地y轴(object.transf.localEulerAngles.y)的角度是多少(它可以是任何值,因为都符合相邻两个小球的旋转)。解决该问题的原则是让节点的本地y坐标不转,全局y轴会跟随父节点转动。我尝试过很多方法来解决这个问题,但是都会出现新的问题。比如当我想通过强制手部节点的transfrom.right和手腕的transfrom.right相同时,手模型会无法控制,因为在Unity中改变transfrom.right时,所有轴上的旋转都会被改变,这个问题在该up的文章中也有所描述;当我想通过限定lerpToAngle.y = 0来固定指定节点(主要是除了大拇指的指见关节)不会沿着y轴旋转时,手部一面是正常的,但转到另外一个面时会完全旋转180°;当我想限制节点本地的y坐标总是为0时,tree.parent.transf.localEulerAngles = new Vector3(angle.x, 0, angle.z) 当手部握拳超过90°,手指会进行一个很鬼畜地内面转外面(这个效果相对还不错了),原因类似于第一个问题。握拳90°的状态如下,继续握拳为大于90°,展开手掌为小于90°。
我的解决方案
我最终的处理方法其实就是在上文中最后一个尝试的基础上加一个限定if(Vector3.Dot(tree.parent.transf.up, Wrist.transform.up) > 0) 只有当手指节点的transform.up与手腕节点的transform.up之间的夹角小于90度时才限制local坐标y-axis为0。这样就不会在手部握拳超过90°时出现鬼畜旋转了。这个方法不是最完美的,因为在手指握拳超过90°后还是会发生微小的不规则的本地y轴旋转,但是这种旋转不会像之前一样累加了,因为每当握拳幅度小于90°后代码会强制让本地y轴归零。虽然这个方法感觉上不太优雅,逻辑也不太完美,但是至少视觉上可以看了,不会出现太鬼畜的扭曲[笑哭.jpg]。
如果大佬们能看懂我写的文章,并且能探索出更好的方法欢迎分享出来。手部捕捉节点骨骼绑定和更新的完整代码如下:
using System.Collections.Generic;
using UnityEngine;
public class AvatarJoint : JointBase
{
private int[] fixedRotationY = {5,6,7,9,10,11,13,14,15,17,18,19};
public class AvatarTree
{
public Transform transf;
public AvatarTree[] childs;
public AvatarTree parent;
public int idx; // pose_joint's index
public AvatarTree(Transform tf, int count, int idx, AvatarTree parent = null)
{
this.transf = tf;
this.parent = parent;
this.idx = idx;
if (count > 0)
{
childs = new AvatarTree[count];
}
}
public Vector3 GetDir()
{
if (parent != null)
{
return transf.position - parent.transf.position;
}
return Vector3.up;
}
}
private AvatarTree wrist, thumb_cmc, thumb_mcp, thumb_ip, thumb_tip, index_finger_mcp, index_finger_pip, index_finger_dip, index_finger_tip, middle_finger_mcp, middle_finger_pip;
private AvatarTree middle_finger_dip, middle_finger_tip, ring_finger_mcp, ring_finger_pip, ring_finger_dip, ring_finger_tip, pinky_mcp, pinky_pip, pinky_dip, pinky_tip;
private int[] L = {3, 4, 7, 8, 11, 12, 15, 16, 19, 20};
private List<int> limitYRotationList;
protected override float speed { get { return 10f; } }
void Start()
{
limitYRotationList = new List<int>(L);
InitData();
BuildTree();
}
void BuildTree()
{
wrist = new AvatarTree(Wrist.transform, 5, 0);
thumb_cmc = wrist.childs[0] = new AvatarTree(Thumb_CMC.transform, 1, 1, wrist);
index_finger_mcp = wrist.childs[1] = new AvatarTree(Index_Finger_MCP.transform, 1, 5, wrist);
middle_finger_mcp = wrist.childs[2] = new AvatarTree(Middle_Finger_MCP.transform, 1, 9, wrist);
ring_finger_mcp = wrist.childs[3] = new AvatarTree(Ring_Finger_MCP.transform, 1, 13, wrist);
pinky_mcp = wrist.childs[4] = new AvatarTree(Pinky_MCP.transform, 1, 17, wrist);
thumb_mcp = thumb_cmc.childs[0] = new AvatarTree(Thumb_MCP.transform, 1, 2, thumb_cmc);
thumb_ip = thumb_mcp.childs[0] = new AvatarTree(Thumb_IP.transform, 1, 3, thumb_mcp);
thumb_tip = thumb_ip.childs[0] = new AvatarTree(Thumb_TIP.transform, 0, 4, thumb_ip);
index_finger_pip = index_finger_mcp.childs[0] = new AvatarTree(Index_Finger_PIP.transform, 1, 6, index_finger_mcp);
index_finger_dip = index_finger_pip.childs[0] = new AvatarTree(Index_Finger_DIP.transform, 1, 7, index_finger_pip);
index_finger_tip = index_finger_dip.childs[0] = new AvatarTree(Index_Finger_TIP.transform, 0, 8, index_finger_dip);
middle_finger_pip = middle_finger_mcp.childs[0] = new AvatarTree(Middle_Finger_PIP.transform, 1, 10, middle_finger_mcp);
middle_finger_dip = middle_finger_pip.childs[0] = new AvatarTree(Middle_Finger_DIP.transform, 1, 11, middle_finger_pip);
middle_finger_tip = middle_finger_dip.childs[0] = new AvatarTree(Middle_Finger_TIP.transform, 0, 12, middle_finger_dip);
pinky_pip = pinky_mcp.childs[0] = new AvatarTree(Pinky_PIP.transform, 1, 18, pinky_mcp);
pinky_dip = pinky_pip.childs[0] = new AvatarTree(Pinky_DIP.transform, 1, 19, pinky_pip);
pinky_tip = pinky_dip.childs[0] = new AvatarTree(Pinky_TIP.transform, 0, 20, pinky_dip);
ring_finger_pip = ring_finger_mcp.childs[0] = new AvatarTree(Ring_Finger_PIP.transform, 1, 14, ring_finger_mcp);
ring_finger_dip = ring_finger_pip.childs[0] = new AvatarTree(Ring_Finger_DIP.transform, 1, 15, ring_finger_pip);
ring_finger_tip = ring_finger_dip.childs[0] = new AvatarTree(Ring_Finger_TIP.transform, 0, 16, ring_finger_dip);
}
protected override void LerpUpdate(float lerp)
{
Wrist.transform.position = pose_joint[0].transform.position - new Vector3(5,0,0);
//Wrist.transform.rotation = pose_joint[0].transform.rotation;
//UpdateBone(thumb_cmc, lerp);
UpdateBone(index_finger_mcp, lerp);
//UpdateBone(pinky_mcp, lerp);
//UpdateBone(thumb_mcp, lerp);
UpdateBone(thumb_ip, lerp);
UpdateBone(thumb_tip, lerp);
UpdateBone(index_finger_pip, lerp);
UpdateBone(index_finger_dip, lerp);
UpdateBone(index_finger_tip, lerp);
UpdateBone(middle_finger_mcp, lerp);
UpdateBone(middle_finger_pip, lerp);
UpdateBone(middle_finger_dip, lerp);
UpdateBone(middle_finger_tip, lerp);
UpdateBone(pinky_pip, lerp);
UpdateBone(pinky_dip, lerp);
UpdateBone(pinky_tip, lerp);
UpdateBone(ring_finger_mcp, lerp);
UpdateBone(ring_finger_pip, lerp);
UpdateBone(ring_finger_dip, lerp);
UpdateBone(ring_finger_tip, lerp);
}
private void UpdateTree(AvatarTree tree, float lerp)
{
if (tree.parent != null)
{
UpdateBone(tree, lerp);
}
if (tree.childs != null)
{
for (int i = 0; i < tree.childs.Length; i++)
UpdateTree(tree.childs[i], lerp);
}
}
private void UpdateBone(AvatarTree tree, float lerp)
{
var dir1 = tree.GetDir();
var dir2 = pose_joint[tree.idx].transform.position - pose_joint[tree.parent.idx].transform.position;
//dir2.y = -dir2.y;
Quaternion rot = Quaternion.FromToRotation(dir1, dir2);
Quaternion rot1 = tree.parent.transf.rotation;
#region Recover rotation
var lerpToAngle = rot * rot1;
//local y坐标不转,globally y轴会跟随父节点转动
tree.parent.transf.rotation = Quaternion.Lerp(rot1, lerpToAngle, lerp);
foreach (int n in fixedRotationY)
{
if (tree.parent.idx == n)
{
var angle = tree.parent.transf.localEulerAngles;
//当手指节点的transform.up与手腕节点的transform.up之间的的夹角小于90度时限制local坐标y-axis为0
if(Vector3.Dot(tree.parent.transf.up, Wrist.transform.up) > 0)
tree.parent.transf.localEulerAngles = new Vector3(angle.x, 0, angle.z);
}
}
#endregion
}
}