Unity插件 - MeshEditor(五) 网格顶点动画(变形动画)

发表于2017-04-18
评论1 3.3k浏览

网格顶点动画(变形动画)是针对于物体的形状可以随意变换并记录为关键帧的动画,虽然模型的顶点数据还是应该交给GPU绘制才是正道,CPU刷新模型顶点始终是个吃力不讨好的事(不过我好像至始至终就是在干吃力不讨好的事来着),所以变形动画还是别用到过于复杂的模型之上,毕竟到头来吃力的只会是你的CPU,不过一些简单的模型倒不用担心,像什么旗帜飘扬什么的,不用打开3DMAX(前提是得会用这东西K动画),不用局限于Unity的animator系统(毕竟给你一个做得像旗帜的cube,你能用animator调出一个飘动的动画?),只需简单的几步拖拽便可以K出一个动画,并且可以将动画信息保存为本地文件,实现多项目间复用,同时,顶点数相同的模型也可以复用动画。

 

 传送门变形动画状态机变形动画骨骼搭建


变形动画完全不同于Unity Animator系统的机制,事实上跟它半毛钱关系都没有,所以这两种动画在同一物体上是可以共同存在的,事实上,众所周知,Animator的关键帧只会记录物体的transform组件的position、rotation以及scale的数值变化(当然其他组件的部分属性它也是可以记录的,比如Image的Color),其余的很多属性改变都不会被它视为有另一关键帧产生,而变形动画只会记录模型的顶点数据作为关键帧,完全不会改动transform组件的属性,所以这两种动画完全可以共存。


好了,进入正题,我以给一个cube调节一个变形动画为例子讲解一下整个流程及实现的思路。

 


第一步:


为cube添加我们的变形动画编辑器组件(MeshAnimation)

添加动画帧:以Scene场景中当前物体的状态信息保存为一个新的关键帧,这里的代码主要是记录每个顶点的位置

[csharp] view plain copy
 
  1. ///   
  2.     /// 添加动画帧  
  3.     ///   
  4.     public void AddFrame()  
  5.     {  
  6.         Vector3[] vertices = new Vector3[_Vertices.Length];  
  7.         for (int i = 0; i < _Vertices.Length; i++)  
  8.         {  
  9.             vertices[i] = _Vertices[i].transform.position;  
  10.         }  
  11.         _VerticesAnimationArray.Add(vertices);  
  12.     }  


我们最好先在cube的初始状态就添加一个动画帧,以便于播放动画时它会从初始状态开始

 



第二步:


现在我们多添加几个关键帧,目前每帧的状态都是保持在初始形态

 



第三步:


我们的第一帧就让他保持初始状态,现在选中第二帧,同时在场景中调节cube的形态,当你觉得满意的时候,点击apply应用就可以将物体的状态应用到当前的第二帧数据,当然如果这一关键帧不想要了,点击delete删除即可

 


[csharp] view plain copy
 
  1. ///   
  2.     /// 应用动画帧  
  3.     ///   
  4.     public void ApplyFrame()  
  5.     {  
  6.         //如果当前动画帧数据存在,则应用当前物体的各顶点数据至当前动画帧  
  7.         if (_NowSelectFrame >= 0 && _NowSelectFrame < _VerticesAnimationArray.Count)  
  8.         {  
  9.             for (int i = 0; i < _Vertices.Length; i++)  
  10.             {  
  11.                 _VerticesAnimationArray[_NowSelectFrame][i] = _Vertices[i].transform.position;  
  12.             }  
  13.         }  
  14.     }  


 

[csharp] view plain copy
 
  1. ///   
  2.     /// 删除动画帧  
  3.     ///   
  4.     public void DeleteFrame()  
  5.     {  
  6.         //如果当前动画帧数据存在,则删除当前动画帧数据  
  7.         if (_NowSelectFrame >= 0 && _NowSelectFrame < _VerticesAnimationArray.Count)  
  8.         {  
  9.             _VerticesAnimationArray.RemoveAt(_NowSelectFrame);  
  10.             _NowSelectFrame = -1;  
  11.         }  
  12.     }  


 

我们将cube调节成这个样子,然后点击apply应用关键帧

 



第四步:


选中第三个关键帧,再调到自己满意的形态,并再点击apply应用

 



第五步:


选中第四个关键帧,这里我们要让他有个缓冲的效果,也就是说跟第三帧的差距小一点

然后我们的第四帧就调成了这个怂样~

 



第六步:


第五帧我们就要让他发射出去(前几帧是收缩,蓄势,然后第五帧猛地弹出~~有没有一种发射炮弹的感觉~~),当然如果你想复制某一帧的话,只需选中这一帧,点击添加关键帧,最后面就会多出来与此帧相同的一帧,然后在此基础上调节下一帧更方便

 



第七步:


之后就是给他K几个反弹回来的缓冲关键帧,注意这里选中任意一帧场景中的cube就会变化到那一帧的形态(这种方式是仿Animator的),随意修改之后点击应用可以保存,不点击应用默认改动无效,所以修改之后,如果觉得满意,一定要点击apply应用,否则待你切换到其他帧时,这一帧改动的数据就将丢失


[csharp] view plain copy
 
  1. ///   
  2.    /// 选定指定帧  
  3.    ///   
  4.    public void SelectFrame(int frameIndex)  
  5.    {  
  6.        //如果当前动画帧数据存在,则选定当前动画帧,所有顶点应用当前动画帧数据  
  7.        if (frameIndex >= 0 && frameIndex < _VerticesAnimationArray.Count)  
  8.        {  
  9.            _NowSelectFrame = frameIndex;  
  10.            for (int i = 0; i < _Vertices.Length; i++)  
  11.            {  
  12.                _Vertices[i].transform.position = _VerticesAnimationArray[frameIndex][i];  
  13.            }  
  14.        }  
  15.    }  


 



第八步:


完成之后点击预览按钮就可以马上在Scene界面看到cube的动画效果,这里没截图,后面用动画播放器播放的时候再截图


 因为脚本就算添加了编辑器执行的标识,它的update函数依然不会逐帧执行,而是在场景物体发生变化的时候才执行,所以这里的动画预览函数不能放在update里,那么只有将之加入到Unity编辑器逐帧刷新周期了

[csharp] view plain copy
 
  1. ///   
  2.     /// 预览动画  
  3.     ///   
  4.     public void PlayAnimation()  
  5.     {  
  6.         //没有动画可以预览  
  7.         if (_VerticesAnimationArray.Count <= 0)  
  8.         {  
  9.             return;  
  10.         }  
  11.         //预览从第一帧开始(顶点动画数组下标0)  
  12.         _AnimationIndex = 0;  
  13.         //重置记录动画播放上一序列的变量  
  14.         _AnimationLastIndex = -1;  
  15.         //重建新的动画片段  
  16.         _AnimationFragment = new Vector3[_Vertices.Length];  
  17.         //重置动画播放控制器  
  18.         _AnimationPlayControl = 0;  
  19.         //动画进入到第一帧  
  20.         for (int i = 0; i < _Vertices.Length; i++)  
  21.         {  
  22.             _Vertices[i].transform.position = _VerticesAnimationArray[0][i];  
  23.         }  
  24.         _IsPlay = true;  
  25.         //将刷新动画函数注册到Unity编辑器帧执行模块  
  26.         EditorApplication.update += PlayingAnimation;  
  27.     }  


动画刷新函数采用将每个关键帧切分为动画片段的方式,将片段循环累加给cube的网格顶点

[csharp] view plain copy
 
  1. ///   
  2.     /// 动画预览中  
  3.     ///   
  4.     void PlayingAnimation()  
  5.     {  
  6.         if (_IsPlay)  
  7.         {  
  8.             //动画播放至最后一帧,动画播放完毕  
  9.             if (_AnimationIndex + 1 >= _VerticesAnimationArray.Count)  
  10.             {  
  11.                 //动画播放完毕  
  12.                 _IsPlay = false;  
  13.                 //清除刷新动画函数的注册  
  14.                 EditorApplication.update -= PlayingAnimation;  
  15.                 //动画回归到第一帧  
  16.                 for (int i = 0; i < _Vertices.Length; i++)  
  17.                 {  
  18.                     _Vertices[i].transform.position = _VerticesAnimationArray[0][i];  
  19.                 }  
  20.                 return;  
  21.             }  
  22.             //当前动画播放序列不等于上一帧序列,则进入下一帧  
  23.             if (_AnimationIndex != _AnimationLastIndex)  
  24.             {  
  25.                 _AnimationLastIndex = _AnimationIndex;  
  26.                 //分割动画片段  
  27.                 for (int i = 0; i < _AnimationFragment.Length; i++)  
  28.                 {  
  29.                     _AnimationFragment[i] = (_VerticesAnimationArray[_AnimationIndex + 1][i] - _VerticesAnimationArray[_AnimationIndex][i])/ _AnimationPlaySpeed;  
  30.                 }  
  31.             }  
  32.             //动画进行中  
  33.             for (int i = 0; i < _Vertices.Length; i++)  
  34.             {  
  35.                 _Vertices[i].transform.position += _AnimationFragment[i];  
  36.             }  
  37.             //动画控制器计数  
  38.             _AnimationPlayControl += 1;  
  39.             //动画控制器记录的一个动画帧播放完毕  
  40.             if (_AnimationPlayControl >= _AnimationPlaySpeed)  
  41.             {  
  42.                 _AnimationPlayControl = 0;  
  43.                 _AnimationIndex += 1;  
  44.             }  
  45.             RefishMesh();  
  46.         }  
  47.     }  


 



第九步:


这里是重点了,记得点击导出动画,如果你直接点击编辑完成或是突然有了什么好想法跑去VS里随意改了下脚本导致Unity编辑器重新编译的话,很遗憾你的动画数据都会丢失,记得导出完毕了之后再点击编辑完成


使用scriptableobject序列化动画数据至asset文件中,这里的坑是真坑,路径必须还得是Asset开头,后缀必须还得是asset,刚开始坑了我不少无辜的时间

[csharp] view plain copy
 
  1. ///   
  2.     /// 导出动画  
  3.     ///   
  4.     public void ExportAnimation()  
  5.     {  
  6.         //动画帧数小于等于1不允许导出  
  7.         if (_VerticesAnimationArray.Count <= 1)  
  8.             return;  
  9.         //创建动画数据文件  
  10.         MeshAnimationAsset meshAnimationAsset = ScriptableObject.CreateInstance();  
  11.         //记录动画顶点数  
  12.         meshAnimationAsset._VertexNumber = _RecordAllVerticesList.Count;  
  13.         //记录动画帧数  
  14.         meshAnimationAsset._FrameNumber = _VerticesAnimationArray.Count;  
  15.         //记录动画帧数据  
  16.         meshAnimationAsset._VerticesAnimationArray = new Vector3[_VerticesAnimationArray.Count * _RecordAllVerticesList.Count];  
  17.         for (int n = 0; n < _VerticesAnimationArray.Count; n++)  
  18.         {  
  19.             for (int i = 0; i < _VerticesAnimationArray[n].Length; i++)  
  20.             {  
  21.                 for (int j = 0; j < _AllVerticesGroupList[i].Count; j++)  
  22.                 {  
  23.                     int number = n * _RecordAllVerticesList.Count + _AllVerticesGroupList[i][j];  
  24.                     EditorUtility.DisplayProgressBar("导出动画""正在导出顶点数据(" + number + "/" + meshAnimationAsset._VerticesAnimationArray.Length + ")......", 1.0f / meshAnimationAsset._VerticesAnimationArray.Length * number);  
  25.                     meshAnimationAsset._VerticesAnimationArray[number] = transform.worldToLocalMatrix.MultiplyPoint3x4(_VerticesAnimationArray[n][i]);  
  26.                 }  
  27.             }  
  28.         }  
  29.         //创建本地文件  
  30.         string path = "Assets/" + GetComponent().sharedMesh.name + "AnimationData.asset";  
  31.         AssetDatabase.CreateAsset(meshAnimationAsset, path);  
  32.   
  33.         EditorUtility.ClearProgressBar();  
  34.     }  


 

如下就是我们导出来的动画数据,可以看到里面包含了10个关键帧,适用于一切有24个网格顶点的模型(网格顶点是可操控顶点的3倍),当然他的原主是cube

 



第十步:


然后,为cube添加变形动画播放器组件(MeshAnimationPlayer)并将我们的CubeAnimationData拖到其MeshAnimationAsset属性上,每一个MeshAnimationPlayer对应一个AnimationData文件,暂不支持代码中动态变更

MeshAnimationAsset:动画播放器的目标asset文件,顶点数量需与当前挂载物体一致

AnimationPlaySpeed:动画播放速度,注意,这里是值越小播放越快

另外两个参数是开启循环播放和启动时即播放,我们勾选启动播放,然后运行程序,下面是动态效果图




其他效果:


一个看起来有点丑又有点僵硬的机甲变形(用最新的骨架调节方式,虽然这样还是显得一团糟)


原形:


编辑状态:


变形动画:


MeshAnimationPlayer的播放有外部可控开关

[csharp] view plain copy
 
  1. ///   
  2.     /// 播放动画  
  3.     ///   
  4.     public void Play()  
  5.     {  
  6.         //从第一帧开始播放(顶点动画数组下标0)  
  7.         _AnimationIndex = 0;  
  8.         //重置记录动画播放上一序列的变量  
  9.         _AnimationLastIndex = -1;  
  10.         //重置动画播放控制器  
  11.         _AnimationPlayControl = 0;  
  12.         //动画跳转到第一帧  
  13.         SelectFrame(_AnimationIndex);  
  14.         _IsPlaying = true;  
  15.     }  
  16.     ///   
  17.     /// 停止播放  
  18.     ///   
  19.     public void Stop()  
  20.     {  
  21.         _IsPlaying = false;  
  22.         //动画回归到第一帧  
  23.         SelectFrame(0);  
  24.     }  


以及要获取当前动画是否播放中,可以直接读取_IsPlaying属性。


DLL版插件链接:http://download.csdn.net/detail/qq992817263/9659011


github源码链接:https://github.com/SaiTingHu/MeshAnimation

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引