前言
这个需求的核心是在Unity3d中实现基于计算机视觉的实时动作捕捉系统,然后进行Unity3d实时Avatar 3D模型动画控制即可。关于计算机视觉姿态估计技术,目前常用的3D姿态识别方案:MediaPipe:Google开源方案,支持3D全身姿态估计。OpenPose:CMU经典姿态识别框架。Kinect Azure:微软深度摄像头硬件方案。iPhone LiDAR:苹果设备的深度传感方案。由于现在AI的飞速发展,使得使用不再需要Kinect 和iPhone LiDAR等硬件,光靠单目摄像头可以实现以上功能,通过对比采用了毕竟直接的TDPT Unity3d插件方案来实现,TDPTSDK(Three-Dimensional Pose Tracking Software Development Kit)是一款专注于实时人体3D姿态追踪的跨平台开发工具包,主要应用于:虚拟现实(VR)动作交互、影视动画实时动捕、体育训练动作分析、医疗康复评估、游戏体感控制。注意关于该插件其价格接近2w RMB,试用版有1分钟体验时间,同时检测到的人数限制为 1 人,同时对于硬件配置也有要求,需要Unity 2019.4.30f1 及更高版本 (C#)。结合如上插件,本工程主要实现摄像头、视频画面的AI三维姿态估,Avatar 3D模型动作同步、识别,基于摄像的实时动捕游戏和人物动作识别的技能释放等功能。
效果
视频同步
游戏技能
关注并私信 U3D动捕游戏 免费获取体验程序(底部公众号)。
关于插件
可以用于各种用途,例如娱乐内容创作、应用程序开发、游戏开发、动画创作以及医疗和护理领域、运动场等。费用为385000日元,折合rmb:
它目前可用于 Windows 10 及更高版本的平台,可同时检测到的人数限制为 1 人。是一种实时动作捕捉系统,当输入来自常规相机的视频或图像数据时,可以通过图像识别 AI 技术(深度学习)以 3-D 坐标输出身体的 24 个检测点。
1右耳;2左耳;3右眼;4左眼;5鼻子;6右肩;7左肩;8右肘;9左肘;10右手腕;11左手腕;12右拇指的根部;13左拇指的根部;14右中指根部;15左中指根部;16胃;17右髋关节;18左髋关节;19右膝;20左膝;21右脚踝;22左脚踝;23右脚趾;24左脚趾。
配置需求:
支持的作系统 Windows 10(64 位)或更高版本
CPU Corei7-7700 或更高 内存 16GB 或更高 GPU GTX
1080 或更高,它在 GTX-20 上以大约 1070fpt 的速度工作。
Unity 2019.4.30f1 及更高版本 (C#)
实现
导入插件后,如果购买了授权填写相关信息即可,如果不购买默认试用版有1分钟体验时间也够测试使用,同时需要接入摄像头设备即可体验设备了,即可在Sample体验插件的基础功能,其余功能主要是游戏逻辑实现。
插件配置
导入插件unitypackage 包,全部导入:
可以打开实例场景,找到ThreeDPoseSDK →BarracudaRunner节点,修改ID和 License Key(如果购买的话):
找到ThreeDPoseSDK → AvatarController节点,修改模型的Avatar Animator:
以上操作绑定了需要同步的模型,后续执行可以看到模型动作将会自动同步。
UI搭建
UI的搭建相对简单,除了几个简单的按钮和技能按钮,触发会提示持续时间和冷却CD等:
输入画面
这里实现了两种输入:视频和摄像头画面,代码如下:
//播放视频
public IEnumerator VideoStart(string path)
{
videoPlayer.url = path;
VideoClip vclip = (VideoClip)Resources.Load(path);
yield return new WaitForSeconds(1);
VidImg.texture = videoTexture;
videoPlayer.Prepare();
while (!videoPlayer.isPrepared) yield return null;
var aspect = new Vector2((float)videoTexture.width / videoTexture.height, 1f);
inputVideoScreen.transform.localScale = new Vector3(aspect.x, aspect.y, 1);
inputVideoScreen.GetComponent<Renderer>().material.mainTexture = videoTexture;
}
//打开摄像头
public void CameraPlayStart()
{
WebCamDevice[] devices = WebCamTexture.devices;
if (devices.Length < 1)
{
return;
}
webCamTexture = new WebCamTexture(devices[CameraIndex].name);
webCamTexture.Play();
VidImg.texture = webCamTexture;
var aspect = new Vector2((float)webCamTexture.width / (float)webCamTexture.height, 1f);
inputVideoScreen.transform.localScale = new Vector3(aspect.x, aspect.y, 1);
inputVideoScreen.GetComponent<Renderer>().material.mainTexture = webCamTexture;
}
inputVideoScreen是作为TDPTSDK插件的输入,两种方式的输入源已经通过修改material.mainTexture即可完成。
骨骼预览
通过TDPTSDK插件的图像识别 AI 技术(深度学习)以 3-D 坐标输出身体的 24 个检测点,通过点(Sphere预制体)将人体检测点创建出来,并通过线段(LineRenderer)将关联的骨骼连接起来,完整代码如下:
using System.Collections;
using System.Collections.Generic;
using ThreeDPoseLibrary;
using UnityEngine;
public class Skeleton : MonoBehaviour
{
public Material lineMat;
public class SkeletonPoint
{
public GameObject LineObject;
public LineRenderer Line;
public Vector3 start = new Vector3();
public Vector3 end = new Vector3();
public void move(Vector3 s, Vector3 e)
{
start = s;
end = e;
Line.SetPosition(0, new Vector3(s.x, s.y, s.z));
Line.SetPosition(1, new Vector3(e.x, e.y, e.z));
}
}
private SkeletonPoint rightUpperArm;
private SkeletonPoint rightLowerArm;
private SkeletonPoint rightHandThum;
private SkeletonPoint rightHandMid;
private SkeletonPoint leftUpperArm;
private SkeletonPoint leftLowerArm;
private SkeletonPoint leftHandThum;
private SkeletonPoint leftHandMid;
private SkeletonPoint bothShoulders;
private SkeletonPoint rightArmpit;
private SkeletonPoint leftArmpit;
private SkeletonPoint leftEye;
private SkeletonPoint leftNose;
private SkeletonPoint rightEye;
private SkeletonPoint rightNose;
private SkeletonPoint rightSpine;
private SkeletonPoint rightUpperLeg;
private SkeletonPoint rightLowerLeg;
private SkeletonPoint rightFoot;
private SkeletonPoint leftSpine;
private SkeletonPoint leftUpperLeg;
private SkeletonPoint leftLowerLeg;
private SkeletonPoint leftFoot;
private SkeletonPoint hip;
private List<GameObject> points = new List<GameObject>();
// Start is called before the first frame update
void Start()
{
var point = (GameObject)Resources.Load("SkeletonPoint");
for (var i = 0; i < Bones.PI_Count; i++)
{
points.Add(Clone(point));
}
rightUpperArm = AddSkeleton("rightUpperArm");
rightLowerArm = AddSkeleton("rightLowerArm");
rightHandThum = AddSkeleton("rightHandThum");
rightHandMid = AddSkeleton("rightHandMid");
leftUpperArm = AddSkeleton("leftUpperArm");
leftLowerArm = AddSkeleton("leftLowerArm");
leftHandThum = AddSkeleton("leftHandThum");
leftHandMid = AddSkeleton("leftHandMid");
bothShoulders = AddSkeleton("bothShoulders");
rightArmpit = AddSkeleton("rightArmpit");
leftArmpit = AddSkeleton("leftArmpit");
leftEye = AddSkeleton("leftEye");
leftNose = AddSkeleton("leftNose");
rightEye = AddSkeleton("rightEye");
rightNose = AddSkeleton("rightNose");
rightSpine = AddSkeleton("rightSpine");
rightUpperLeg = AddSkeleton("rightUpperLeg");
rightLowerLeg = AddSkeleton("rightLowerLeg");
rightFoot = AddSkeleton("rightFoot");
leftSpine = AddSkeleton("leftSpine");
leftUpperLeg = AddSkeleton("leftUpperLeg");
leftLowerLeg = AddSkeleton("leftLowerLeg");
leftFoot = AddSkeleton("leftFoot");
hip = AddSkeleton("hip");
}
// Update is called once per frame
void Update()
{
}
public void Move(KeyPoint[] keyPoints)
{
for (var i = 0;i < keyPoints.Length; i++)
{
points[i].transform.localPosition = keyPoints[i].Pos3D;
}
rightUpperArm.move(points[Bones.PI_rShldrBend].transform.position, points[Bones.PI_rForearmBend].transform.position);
rightLowerArm.move(points[Bones.PI_rForearmBend].transform.position, points[Bones.PI_rHand].transform.position);
rightHandThum.move(points[Bones.PI_rHand].transform.position, points[Bones.PI_rThumb2].transform.position);
rightHandMid.move(points[Bones.PI_rHand].transform.position, points[Bones.PI_rMid1].transform.position);
leftUpperArm.move(points[Bones.PI_lShldrBend].transform.position, points[Bones.PI_lForearmBend].transform.position);
leftLowerArm.move(points[Bones.PI_lForearmBend].transform.position, points[Bones.PI_lHand].transform.position);
leftHandThum.move(points[Bones.PI_lHand].transform.position, points[Bones.PI_lThumb2].transform.position);
leftHandMid.move(points[Bones.PI_lHand].transform.position, points[Bones.PI_lMid1].transform.position);
bothShoulders.move(points[Bones.PI_rShldrBend].transform.position, points[Bones.PI_lShldrBend].transform.position);
rightArmpit.move(points[Bones.PI_rShldrBend].transform.position, points[Bones.PI_abdomenUpper].transform.position);
leftArmpit.move(points[Bones.PI_lShldrBend].transform.position, points[Bones.PI_abdomenUpper].transform.position);
leftEye.move(points[Bones.PI_lEar].transform.position, points[Bones.PI_lEye].transform.position);
leftNose.move(points[Bones.PI_lEye].transform.position, points[Bones.PI_Nose].transform.position);
rightEye.move(points[Bones.PI_rEar].transform.position, points[Bones.PI_rEye].transform.position);
rightNose.move(points[Bones.PI_rEye].transform.position, points[Bones.PI_Nose].transform.position);
rightSpine.move(points[Bones.PI_rThighBend].transform.position, points[Bones.PI_abdomenUpper].transform.position);
rightUpperLeg.move(points[Bones.PI_rThighBend].transform.position, points[Bones.PI_rShin].transform.position);
rightLowerLeg.move(points[Bones.PI_rShin].transform.position, points[Bones.PI_rFoot].transform.position);
rightFoot.move(points[Bones.PI_rFoot].transform.position, points[Bones.PI_rToe].transform.position);
leftSpine.move(points[Bones.PI_lThighBend].transform.position, points[Bones.PI_abdomenUpper].transform.position);
leftUpperLeg.move(points[Bones.PI_lThighBend].transform.position, points[Bones.PI_lShin].transform.position);
leftLowerLeg.move(points[Bones.PI_lShin].transform.position, points[Bones.PI_lFoot].transform.position);
leftFoot.move(points[Bones.PI_lFoot].transform.position, points[Bones.PI_lToe].transform.position);
hip.move(points[Bones.PI_rThighBend].transform.position, points[Bones.PI_lThighBend].transform.position);
}
private GameObject Clone(GameObject go)
{
var clone = GameObject.Instantiate(go) as GameObject;
clone.transform.parent = this.gameObject.transform;
clone.transform.localPosition = go.transform.localPosition;
clone.transform.localScale = go.transform.localScale;
return clone;
}
private SkeletonPoint AddSkeleton(string name)
{
var sk = new SkeletonPoint()
{
LineObject = new GameObject(name + "_Skeleton"),
};
sk.LineObject.transform.parent = this.gameObject.transform;
sk.Line = sk.LineObject.AddComponent<LineRenderer>();
sk.Line.startWidth = 0.01f;
sk.Line.endWidth = 0.01f;
sk.Line.positionCount = 2;
sk.Line.material = lineMat;
sk.Line.GetComponent<Renderer>().material.color = Color.blue;
sk.Line.useWorldSpace = false;
return sk;
}
}
执行后的效果如下:
动作识别
有了人体的 24个检测点,在结合碰撞体的触发器,在对应肢体节点上加上触发器,可以进行动作的识别,从而触发技能,这里要实现如下两个动作技能:
一个动作是双手合十到胸前,这个的实现方式是采用了两个指尖的触发器实现:
触发后调用技能:
void OnTriggerEnter(Collider collider)
{
//Debug.Log("collider.name:" + collider.name);
if (collider.name == "dfdffd_CrossFGCol")
handfire.DoFingerTouchSkill();
}
//先触发器判断, 再进行位置判断;
//判断交叉角度, 手臂弯度判断。
public bool IsTouchFinger()
{
//低于脖子
if (R_HandTran.position.y >= NeckTran.position.y || L_HandTran.position.y >= NeckTran.position.y)
return false;
//位置判断;
if (Mathf.Abs(L_LowArmTran.position.y - R_LowArmTran.position.y) >= HandPosOffset || Mathf.Abs(R_HandTran.position.y - L_HandTran.position.y) >= HandPosOffset)
return false;
if (R_LowArmTran.position.y >= R_HandTran.position.y || L_LowArmTran.position.y >= L_HandTran.position.y)
return false;
//交叉角度判断
Vector3 from = L_UpArmTran.position - L_LowArmTran.position,
to = L_HandTran.position - L_LowArmTran.position;
float angle = Vector3.Angle(from, to);
Vector3 nordir = Vector3.Cross(from, to);
float dot = Vector3.Dot(nordir, Vector3.down);
if (dot < 0)
{
angle *= -1;
angle += 360;
}
if (Mathf.Abs(90 - angle) > CrossAngleFactor)
return false;
//Debug.Log("交叉角度判断完成");
//手臂弯度判断 ?
double RAngle = Angle(R_LowArmTran.position, R_HandTran.position, R_UpArmTran.position);
//Debug.Log("RAngele:" + RAngle);
if (RAngle < ArmMinAg || RAngle > ArmMaxAg)
return false;
double LAngle = Angle(L_LowArmTran.position, L_HandTran.position, L_UpArmTran.position);
//Debug.Log("LAngle:" + LAngle);
if (LAngle < ArmMinAg || LAngle > ArmMaxAg)
return false;
//Debug.Log("手臂弯度判断完成");
return true;
}
如上的姿态判定都是根据手指、脖子的位置,以及他们的角度进行了判定处理,这个过程需要一些时间反复测试。
最后实现效果:
另一个技能是通过双手合十放到头顶,有个蓄能的过程,到达蓄能时间后,释放技能。
判断触发为触发器的OnTriggerEnter,开始蓄能:
if (IsKillAble() && collider.name == "dfdffd_CrossHandCol" && CheckDis(collider.transform.position))
{
IsTouched = true;
StartTime = Time.time;
KillAllSkill.Instance.ShowTouchEf((Other.position + transform.position) / 2, 1.5f * (Time.time - StartTime) / TriggerTime);
//Debug.Log("开始触及");
}
bool CheckDis(Vector3 pos1) {
//高于于脖子
if (transform.position.y <= HeadTop.position.y || Other.position.y <= HeadTop.position.y)
return false;
//Debug.Log("Dis1:" + Vector3.Distance(pos1, HeadTop.position) + " Dis2:" + Vector3.Distance(transform.position, HeadTop.position));
if (Vector3.Distance(pos1, HeadTop.position) <= 0.7f && Vector3.Distance(transform.position, HeadTop.position) <= 0.7f)
return true;
return false;
}
蓄能的过程是特效逐步增大:
if (Time.time - StartTime < TriggerTime)
KillAllSkill.Instance.ShowTouchEf((Other.position + transform.position) / 2, 1.5f * (Time.time - StartTime) / TriggerTime);
else {
KillAllSkill.Instance.DoSkill();
IsTouched = false;
}
蓄能时间到后,开始释放技能,效果如下:
源码
https://download.youkuaiyun.com/download/qq_33789001/90531401
以上源码为基于试用版TDPT插件实现(一分钟体验),请悉知。如需打包或者其他可联系。
关注并私信 U3D动捕游戏 免费获取体验程序(底部公众号)。