体感Kinect结合Unity3D引擎开发虚拟现实AR
2015年的第一场源创会,由 @爱吃鱼的猫大哥 做了一篇虚拟现实—我们能做的其实很多的猪蹄,主要介绍了增强现实框架-MetaioSDK开发包,我想大家对于这个主题肯定吃的还不够过瘾,那么今天就在来点猛料,满足大家欲望!由于公司业务需要,我也用这个sdk做过Android上一个虚拟现实app小例子,毕竟收费,有一些功能不付费体验不到,在加上后面跟上需要做一个大型户外体验的虚拟项目,综合考虑选择微软体感Kinect结合Unity3D引擎开发虚拟现实AR! @红薯 是不是后面考虑让我参加一期嘉宾演讲了,哈哈!废话不多扯,直接上干货!
首先上好猪蹄和各种佐料:
大概的流程:Kinect打开之后运行,unity通过绑定的脚本会首先获取RGB流和Depth流,以及骨骼数据(主要为识别手势和人多位置做准),其次通过脚本获取各种手势,以及位置,比如SwipeRight没右手挥动一次,就会切换一个例子场景直接与实时拍摄的场景完美融合(效果图稍后呈上),包括RaiseRightHand会出现流星撞地面,感觉像世界末日的感觉。最后就是同时利用unity的GuiTexture实时显示RGB流!说了那么多先上一张效果图感受下激情摧毁办公室:
好了,材料准备好了,该看的都看了,接下来重点来了,就是放干货一步一步看看怎么做出来:
第一步,也是最重要的一步,就是实现3D模型显示在平面2D图片上面,这个用unity真的很方便,首先需要添加一个摄像机Camera,并在摄像机Depth属性设置为-1,在MainCamera设置为0,说明背景层是最深,其他model所在的相机的Depth是0,这样确保3D模型在背景的前面!这里面有一点需要注意的就是一定要保证MainCamera和所有的模型的layer都在Defaule层!(在这里不得不说unity真的很方便)!
在第一步基础上,第二步实现把Kinect获得RGB流显示出来,这个如果大家不会封装可以利用Kinect结合Unity3D引擎开发体感游戏(一) 里面的卡耐基梅隆的kinectWrapper.unitypackage,我这个就是在这个基础上修改的拓展了不少新功能,目前可以支持KinectSdk1.8版本!如果你比较熟悉unity,可以这样做:
注:kinect封装脚本代码过多,这里接贴出一些关键点,不是很清楚,可以看卡耐基梅隆包,里面实现方法都差不多
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | void Update () { KinectManager manager = KinectManager.Instance;//RGB流封装在这个脚本里面 if(manager && manager.IsInitialized()) { //利用GetUsersClrTex()函数获得Texture2D的RGB流! this.renderer.material.mainTexture = manager.GetUsersClrTex(); /*******************在KinectManager脚本里面函数实现*********************/ public Texture2D GetUsersClrTex() { return usersClrTex; } /*******************************************/ |
到了这一步,就可以把这个脚本直接绑定到Plane对象!这个时候RGB流就显示出来了!如果你和我一样用plane对象,有一个地方需要注意,就是它的属性的Tiling的x设置为-1(这里在扯一句废话,不清楚的可以先了解下unity怎么用,在来看可能更快)。
第三步,在第二步基础上,绑定Tornado(龙卷风)模型,绑定这个模型是为了让Tornado跟着人一起平移运动,人道哪里,Tornado就跟随着!直接看代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | void Update () { KinectManager manager = KinectManager.Instance; if(manager && manager.IsInitialized()) { this.renderer.material.mainTexture = manager.GetUsersClrTex(); int iJointIndex = (int)TrackedJoint; if(manager.IsUserDetected()) { uint userId = manager.GetPlayer1ID(); if(manager.IsJointTracked(userId, iJointIndex)) { //获得需要绑定骨骼点的X,Y,Z坐标,绑定Tornado Vector3 posJoint = manager.GetRawSkeletonJointPos(userId, iJointIndex); Vector2 posDepth = manager.GetDepthMapPosForJointPos(posJoint); Vector2 posColor = manager.GetColorMapPosForDepthPos(posDepth); float scaleX = (float)posColor.x / KinectWrapper.Constants.ImageWidth; float scaleY = 1.0f - (float)posColor.y / KinectWrapper.Constants.ImageHeight; Vector3 localPos = new Vector3(scaleX * 10f - 5f, 0f, scaleY * 10f - 5f); Vector3 vPosOverlay = transform.TransformPoint(localPos); //保证Tornado一直是水平移动 if(OverlayObject) { double num_y = -11.9; double num_z = -4.52; vPosOverlay.y = (float)num_y; vPosOverlay.z = (float)num_z; OverlayObject.transform.position = vPosOverlay; } } } } } |
这里在扯一句废话,这个代码编辑,有很大的提高空间!
第四步,这个让Tornado已经随人一起移动,接下来实现RaiseRightHand和SwipeRight切换粒子场景,直接上代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | //这个就是手势判定产生动作切换场景代码 if(slideChangeWithGestures && gestureListener) { if(gestureListener.IsSwipeLeft()) { m_CurrentElementIndex++; m_CurrentParticleIndex = 0; ShowParticle(); RotateToNext(); } else if(gestureListener.IsSwipeRight()) { m_CurrentElementIndex++; m_CurrentParticleIndex = 0; ShowParticle(); RotateToPrevious(); } else if(gestureListener.IsRaiseRightHand()) { m_CurrentElementIndex = 6;; m_CurrentParticleIndex = 0; ShowParticle(); RotateToPrevious(); } else if(gestureListener.IsRaiseLeftHand()) { m_CurrentElementIndex = 7; m_CurrentParticleIndex = 0; ShowParticle(); RotateToPrevious(); } } |
那么里面的ShowParticle() 函数和 RotateToPrevious()函数的作用分别是:
ShowParticle() :主要切换显示不同场景粒子效果,比如Rain、Snow、Lighting等效果。上实现代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 | #region Functions void ShowParticle() { if(m_CurrentElementIndex == 6 && m_CurrentElementIndex == 7) { ; } else if(m_CurrentElementIndex<0) { m_CurrentElementIndex = 5; } if(m_CurrentElementIndex==0) { music.Stop(); m_CurrentElementList = m_PrefabListFire; m_ElementName = "FIRE"; music.PlayOneShot(theSound); //music.Play(); } else if(m_CurrentElementIndex==1) { music.Stop(); m_CurrentElementList = m_PrefabListWater; m_ElementName = "WATER"; music.PlayOneShot(theSound); //music.Play(); } else if(m_CurrentElementIndex==2) { music.Stop(); m_CurrentElementList = m_PrefabListWind; m_ElementName = "WIND"; music.PlayOneShot(theSound); //music.Play(); } else if(m_CurrentElementIndex==6) { music.Stop(); m_CurrentElementList = m_PrefabListEarth; m_ElementName = "EARTH"; music.PlayOneShot(theSound); //music.Play(); } else if(m_CurrentElementIndex==3) { music.Stop(); m_CurrentElementList = m_PrefabListThunder; m_ElementName = "THUNDER"; //music.PlayOneShot(theSound); music.Play(); } else if(m_CurrentElementIndex==4) { music.Stop(); m_CurrentElementList = m_PrefabListIce; m_ElementName = "ICE"; music.PlayOneShot(theSound); //music.Play(); } else if(m_CurrentElementIndex==5) { music.Stop(); m_CurrentElementList = m_PrefabListLight; m_ElementName = "LIGHT"; music.PlayOneShot(theSound); //music.Play(); } else if(m_CurrentElementIndex==7) { music.Stop(); m_CurrentElementList = m_PrefabListDarkness; m_ElementName = "DARKNESS"; music.PlayOneShot(theSound); //music.Play(); } if(m_CurrentElementIndex == 5 || m_CurrentElementIndex == 6 || m_CurrentElementIndex == 7) { m_CurrentElementIndex = 0; } if(m_CurrentParticleIndex>=m_CurrentElementList.Length) { m_CurrentParticleIndex = 0; } else if(m_CurrentParticleIndex<0) { m_CurrentParticleIndex = m_CurrentElementList.Length-1; } m_ParticleName = m_CurrentElementList[m_CurrentParticleIndex].name; if(m_CurrentParticle!=null) { DestroyObject(m_CurrentParticle); } m_CurrentParticle = (GameObject) Instantiate(m_CurrentElementList[m_CurrentParticleIndex]); music.volume = musicVolume; } #endregion {Functions} |
RotateToPrevious():主要也是切换场景,不过不是粒子,是为了切换一个立方体,以提示切换场景成功信号(主要是考虑用户体验性)!还是上代码吧!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | private void RotateToNext() { tex = (tex + 1) % maxTextures; if(!isBehindUser) { side = (side + 1) % maxSides; } else { if(side <= 0) side = maxSides - 1; else side -= 1; } if(horizontalSides[side] && horizontalSides[side].renderer) { horizontalSides[side].renderer.material.mainTexture = slideTextures[tex]; } float yawRotation = !isBehindUser ? 360f / maxSides : -360f / maxSides; Vector3 rotateDegrees = new Vector3(0f, yawRotation, 0f); targetRotation *= Quaternion.Euler(rotateDegrees); isSpinning = true; } |
然后将脚本绑定到camera或者自己需要的对象物体上,这样就可以控制切换场景了,至于手势判定,这个只需要你获得了骨骼数据了,是很好写出来手势判定的,这里就不赘述了!
第五步,我们就看看效果吧,直接上图:
下图为:流星撞地球,激情毁灭办公室
下图为闪电效果:
下图为龙卷风效果:
下图为下雨效果:
真正的效果,可能截图感觉不是逼真,但是在程序跑的时候,感觉会好很多,图像不是很清晰主要一是kinect的分辨率只有640*480,二来室内还没开灯,有一张闪电图片就是开灯了,就会清楚多!
后期计划:加入高清摄像头,使2D画面更加清晰;
添加更多音效;
加入更多手势动作判断,更多互动使其更好玩。
这篇文章主要比较简单介绍了下虚拟现实,希望可以起到一个抛砖引玉的作用,欢迎指正,谢谢!