Unity3d基于TDPT插件实现摄像头、视频画面的AI三维姿态估并在Avatar 3D模型动作同步、识别游戏(含源码)

前言

这个需求的核心是在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动捕游戏 免费获取体验程序(底部公众号)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

十幺卜入

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值