转载请标明出处http://www.cnblogs.com/zblade/

  最近研究了一下游戏中模型的骨骼动画的原理,做一个学习笔记,便于大家共同学习探讨。

  ps:最近改bug改的要死要活,博客写的吭哧吭哧的~

首先列出学习参考的前人的文章,本文较多的参考了其中的表述:

  1、骨骼动画详解 :http://blog.csdn.net/ccx1234/article/details/6641944,不过这篇文章的原文已经被csdn封了:D,可以看看对应的转载的文章也行

2、OpenGL10-骨骼动画原理篇:http://www.cnblogs.com/zhanglitong/p/3196752.html

一、骨骼动画、关节动画、关键帧动画

  在实际的游戏中,用的最多的是这三种基本的动画。

  在关键帧动画中,模型在每个关键帧中都是一个固定的姿势,相当于一个“快照”,通过在不同的关键帧中进行插值平滑计算,可以得到一个较为流畅的动画表现。关键帧动画的一个优势是只需要做插值计算,相对于其他的动画计算量很小,但是劣势也比较明显,基于固定的“快照”进行插值计算,表现大大被限制,同时插值如果不够平滑容易出现尖刺等现象, 同时一个无法忽略的问题就是,对于每帧动画都需要进行内存占用,在帧数较多和顶点数较多的时候,这会带来极大的内存占用。当然帧动画也有其实际的应用场景,目前在一些模型的表情动画中会较多的应用到帧动画,因为其动画较少,同时可以用插值实现较为平滑的效果,不需要骨骼动画。

  关节动画是早期出现的一种动画,在这种动画中,模型整体不是一个Mesh, 而是分为多个Mesh,通过父子的关系进行组织,这样父节点的Mesh就会带动子节点的Mesh进行变换,这样层层的变换关系,就可以得到各个子Mesh在不同关键帧中的位置。关节动画相比于关键帧动画,依赖于各个关键帧的动画数据,可以实时的计算出各个Mesh的位置,不再受限于固定的位置,但是由于是分散的各个Mesh,这样在不同Mesh的结合处容易出现裂缝。

  骨骼动画是进一步的动画类型,原理构成极其简单,但是解决问题极其有优势。将模型分为骨骼Bone和蒙皮Mesh两个部分,其基本的原理可以阐述为:模型的骨骼可分为基本多层父子骨骼,在动画关键帧数据的驱动下,计算出各个父子骨骼的位置,基于骨骼的控制通过顶点混合动态计算出蒙皮网格的顶点。在骨骼动画中,通常包含的是骨骼层次数据,网格Mesh数据, 网格蒙皮数据Skin Info和骨骼的动画关键帧数据。本文重点讲解一下骨骼动画。

二、骨骼动画原理分析

  对于Skinned Mesh的理解关键是蒙皮的计算过程,这里的皮,并不是我们常见的在shader中用到的贴图texture,而是模型的mesh, 所以蒙皮其实就是计算mesh相对于骨骼的位置变换过程。如果我们的骨骼没有变化,那么我们的mesh就没有变化,此时的mesh就相当于一个静态的mesh。所以对于skinned的理解,就是指具有蒙皮数据skin info的mesh :D。 在实际的实例中,skin info主要包含当前mesh上的顶点受到哪些骨骼影响,各个骨骼的影响权重。借用文章1中的表述,整个mesh顶点的变换过程可以用矩阵的方式表述:

  Vworld = Vmesh * BoneOffsetMatrix1 * CombineMatrix1 * W1 +Vmesh * BoneOffsetMatrix2 * CombineMatrix2 * W2 +...+ Vmesh * BoneOffsetMatrixn * CombineMatrixn * Wn

  其中BoneOffsetMatrix 和 CombineMatrix在下面的讲解中会详细讲解求解过程。

 1、骨骼动画中的骨骼

  在我们常见的模型建模中,美术通常将模型设置成双手水平,双腿分立的类“大”字型。 为什么要用这样的方式? 这与我们的模型的骨骼有一定的关系。通常模型骨骼数量都有一个限制(通常为30,下面会解释为什么受到限制),模型不可能为一根骨骼,如果为一根骨骼,那么模型的表现就会显得比较单一,想要表现出类似于人体的动画,一般都会有多个骨骼。对于每个骨骼,如何建立其组织关系,在建模的时候就进行了基本的设定。在完成建模后,在进行骨骼绑定的时候,通常美术会选择模型的盆骨做为模型的根骨骼。那么基于根骨骼,可以递推出各个骨骼相对于根骨的父子关系。通过骨骼所在的空间,其原点我们会选择两个脚之间的中点作为原点,这时候就会发现根骨骼并没有和原点重合,这时美术会构建一个Scene_Root做为额外的骨骼,其位置就为世界原点,而真正的根骨骼Bip01会作为Scene_Root的唯一子骨骼。

                                      请忽略我的灵魂画法:D

  基于根骨骼,我们可以递推出各个骨骼相对于根骨骼的父子转换矩阵,通常这样的矩阵转换为旋转矩阵,不存在平移和缩放,当然比如你的一个模型动画是拉长模型的身体(比如海贼王这种动画:D),那么你可以添加平移和缩放到变换矩阵中。此时对于整个模型就有了一个基本的直观的感受了,模型的具体动画,取决于模型的根骨骼,牵一发而动全身(这儿的牵一发是指模型的动画数据驱动),整个模型的运动会带动外面的mesh一起运动,就可以表现出整个模型的骨骼动画,这样,我们对于骨骼动画就有一个基本的理解。

  借用参考文1的说话,骨骼的本质,其实就是一个坐标空间,我们在做骨骼动画的时候,关键帧中包含的对骨骼的变换主要为旋转矩阵,所以对骨骼的变换就是对骨骼空间的旋转变换。说简单点,一个骨骼动画,带来的变换,首先作用在根骨骼上,影响根骨骼的坐标空间,然后递归的影响根骨的子骨骼,这样层层的递归影响,最后带来的就是整体骨骼变换。基于此,我们就可以理解关节的作用,关节就是骨骼自身空间的原点。而关节的位置由当前骨骼在其父节点骨骼空间中的位置来描述,围绕关节的旋转就是骨骼空间自身的旋转,这样变换就通过关节层层递归传递。用c++的表述,可以定义一个基本的骨骼类,主要包含自己的世界坐标中的位置,在父节点中的位置,其第一个子节点,其兄弟骨骼的指针,代码依据于文章1:

class Bone
{
Bone* m_pFirstChild;
Bone* m_pSibling;
float m_x, m_y, m_z; // pos in parents' space
float m_wx, m_wy, m_wz; // pos in world space
//
public:
Bone(float x, float y, float z): m_pSibling(NULL),m_pFirstChild(NULL),m_pFather(NULL),m_x(x), m_y(y), m_z(z){} //
void SetFirstChild(Bone* pChild)
{
m_pFirstChild = pChild;
m_pFirstChild->m_pFather = this;
}
//
void SetSibling(Bone* pSibling)
{
m_pSibling = pSibling;
m_pSibling->m_pFather = m_pFather;
}
}

这样,当父节点骨骼发生变换的时候,子节点的骨骼就会做相应的变换,这样的操作可以称为 UpdateBoneMatrix(更新骨骼矩阵),这样的操作可以用一个方法ComputeWorldPos来表示,这样可以用递归的方式在Bone中实现,代码依据于文章1:

class Bone
{
void ComputeWorldPos(float fatherX, float fatherY, float fatherZ)
{
m_wx = fatherX + m_x;
m_wy = fatherY + m_y;
m_wz = fatherZ + m_z;
// 兄弟节点用父节点传递的参数
if(m_pSibling !=NULL)
m_pSibling ->ComputeWorldPos(fatherX, fatherY, fatherZ)
     // 递归到子节点计算其位置
if(m_pFirstChild!=NULL)
m_pFirstChild ->ComputeWorldPos(m_wx, m_wy, m_wz)
}
}

  这样,当父节点骨骼发生变换的时候,子节点的骨骼都会做出相应的变换,从而得到最新的位置、朝向等信息,骨骼发生变化,从而会带动外在的mesh发生变化,所以整体的模型就表现出运动起来。基于此,可以理解为什么骨骼是骨骼动画的核心。

  

2、骨骼动画中的蒙皮

  1)关于蒙皮的一种理解

  在说完骨骼后,对于整体模型在动画中骨骼的变换,可以有一个大致的理解,当然模型只是内在的,动画的外在表现就是模型的蒙皮变化,所以骨骼动画中的第二部分就是蒙皮的计算。这里的皮,就是前面说过的Mesh。

  首先,需要明确的是Mesh所在的空间。在建模的时候,模型的Mesh是和骨骼一样处于同样的空间中的,Mesh中的各个顶点是基于Mesh的原点来进行定位的。但是模型在运动表现的时候,是根据骨骼的变换来做相应的动作的,对应的Mesh上的顶点就需要做出对应的转换,所以Mesh的顶点需要转换到对应的骨骼所在的坐标空间中,进行相应的位置变换,因此对应的需要添加蒙皮信息,也就是skin info,主要是当前顶点受到哪些骨骼的影响,影响的权重等,借用文章1的表述,可以用C++表示一个顶点类,代码依据于文章1:

#define MAX_BONE_VERTEX 4
class Vertex
{
float m_x, m_y, m_z; // local pos in mesh space
float m_wx, m_wy, m_wz; // pos in world space
//skin info
int m_boneNum;
Bone* m_bones[MAX_BONE_VERTEX];
float m_boneWeights[MAX_BONE_VERTEX];
}

  当然,这儿只是一个简单的表述,具体的在引擎中会有规范的设计。那么我们的顶点在跟随骨骼做运动的时候,是如何计算自己的位置的?我们就需要引入BoneOffsetMatrix 和 Transform Matrix的概念。

在前面,我们已经提到,顶点需要依附于骨骼进行位置计算,但是建模的时候,顶点的位置是基于Mesh原点进行建模的,通常情况下,Mesh的原点是和模型的骨骼的根骨骼处于同一个坐标空间中,那么 BoneOffsetMatrix就是用来将Mesh中顶点从Mesh空间转换到骨骼所在空间中。这就是很多文章中提及的Binding Pos的设置。

  在建模的时候,对于每个骨骼,我们是可以得到其对应的Transform Matrix(用来层层计算到父节点所在空间中),其中根骨骼的Transform Matrix是基于世界空间的转换,所以对于每一个下面的子骨骼,要计算其Transform Matrix,需要进行一个矩阵的连乘操作。最后得到的最终矩阵连乘结果矩阵就是Combined Transform Matrix,基于这个矩阵,就可以将顶点从骨骼所在的空间转换到世界空间中。反过来,这个矩阵的逆矩阵(一般只考虑可以取逆的操作),就是从世界空间中转换到该骨骼的空间中,由于Mesh的定义基于Mesh原点,Mesh原点就在世界空间中,所以这个逆矩阵就是需要计算的的 Offset Matrix,也被称为Inverse Matrix,这个逆矩阵一般实在初始位置中求得,通过取逆即可获得。

  在实际的计算中,每个骨骼可能会对应多个顶点,如果每个顶点都保存其对应的骨骼的变换矩阵,那么大量的顶点就会包含非常多的变换矩阵,带来内存数据的扩大。所以我们只需要保存当前该骨骼在初始位置,对应的从世界空间到其骨骼空间的变换矩阵,那么其对应的每个顶点在每次变换操作的时候,只需要对应的用offset Matrix来操作即可。

对于上面的Transform Matrix和offset Matrix,是纳入了旋转、平移和缩放的。其实offset Matrix取决于骨骼的初始位置,此时一般只包含了平移(此时还没有动画,所以没有旋转和缩放),在动画中,一般也以旋转为主(所以大部分的动画的关键帧用四元数表示)。在矩阵中都包含三种矩阵,是处于兼容性考虑。

  这儿就基于平移,做一个基本的蒙皮的计算过程,代码主要依据于文章1:

class BoneOffset
{
public:
float m_offx, m_offy, m_offz; //暂时只考虑平移
} class Bone
{
public :
BoneOffset* m_boneOffset;
//
void ComputeBoneOffset()
{
m_boneOffset.m_offx -= m_wx;
m_boneOffset.m_offy -= m_wy;
m_boneOffset.m_offz -= m_wz; if(m_pSibling != NULL)
m_pSibling->ComputeBoneOffset();
if(m_pFirstChild !=NULL)
m_pFirstChild->ComputeBoneOffset();
}
} //顶点类的计算
class Vertex
{
public:
void ComputeWorldPosByBone(Bone* pBone, float &outX, float& outy, float& outz)
{
//从mesh空间转换到bone空间
outx = m_x + pBone->m_boneOffset.m_offx;
outy = m_y + pBone->m_boneOffset.m_offy;
outz = m_z + pBone->m_boneOffset.m_offz;
//从bone空间转换到世界空间
outx += pBone->m_wx;
outy += pBone->m_wy;
outz += pBone->m_wz;
}
//GPU中计算顶点的位置
void BlendVertex()

float m_wx = ;
float m_wy = ;
float m_wz = ; for(int i=; i < m_boneNum; i++)
{
float tx, ty,tz;
ComputeWorldPosByBone(m_bones[i], tx, ty,tz); tx *= m_boneWeights[i];
ty *= m_boneWeights[i];
tz *= m_boneWeights[i]; m_wx += tx;
m_wy += ty;
m_wz += tz;
}
} }

仔细捋一捋上面的代码,就可以理解整体的蒙皮变换的过程,当然,这儿只用了矩阵变换中的平移变换,如果考虑加上旋转和缩放,则回到最初的计算公式中了。至此,对于基本的骨骼动画中的骨骼变换和蒙皮变换,有了一个详细的解释。下面说说Unity中是如何处理骨骼变换的。

2)关于蒙皮的新的理解

    在我写完这篇文章后,给同事查看,同事觉得较为绕,有点迷糊,后来我想了一下,可以采用一种较为清晰的方式重新讲解一下蒙皮的过程:

    首先给出蒙皮计算的过程: 顶点在模型坐标系中位置 -> 骨骼空间中位置 -> 变换后的骨骼空间中的位置->模型坐标系中的位置 -> 世界空间中的位置

    关于蒙皮的定义,其实质就是骨骼在动画数据的驱动下,变换位置,依附于该骨骼的mesh顶点会对应的变换到新的位置(在世界空间中)的整个过程。

    确定最终结果: mesh上的顶点在世界空间中的位置变换到最新位置

    那么怎么得到这样的一个结果?在前文中,我们已经知道,骨骼是和mesh分离的,骨骼依赖于根骨骼,层层递进得到各自的转换矩阵。而mesh是在模型建模中,基于模型空间得到的位置设定。在这儿,根骨骼的空间原点和mesh的空间原点重合,也就是mesh的坐标系和根骨骼的坐标系为同一个坐标系,也就是模型坐标系。

    我们已经知道,骨骼本身就是一个坐标系空间,其原点就是关节点。那么在建模的时候,建模软件就会有一步计算操作,称为BindingPos操作。这步操作可以这样理解,mesh上分布了众多的顶点,对应于单独的一个骨骼,我们可以得到其骨骼空间的转换矩阵,以及那些顶点依附于其上。对于依附其上的顶点,建模的时候,以及基于模型空间给出了一个模型空间中的位置,那么其相对于当前该骨骼空间的位置,可以基于模型空间位置与骨骼的转换矩阵得到,这个位置就是其BindingPos, 这个变换矩阵,就是BindingPosMatrix,基于这个矩阵,可以将顶点位置从模型空间转换到骨骼空间。

到这儿,我们完成了:  顶点在模型坐标系中位置 -> 骨骼空间中位置

    接下来我们继续下面的计算,在骨骼动画中,每个动画数据中,会带来当前骨骼的新的变化(旋转为主),那么在当前动画数据的驱动下,当前骨骼变换到新的位置,得到新的骨骼矩阵,那么依附其上的顶点的位置也会变换,首先是将顶点位置从模型空间中转换到当前该骨骼的空间中,然后基于骨骼的当前这次变换矩阵(比如旋转90度对应的矩阵),得到其在该骨骼空间中的新位置,这个新的位置再和当前骨骼在和根骨骼的变换矩阵相作用,变换回模型空间中。

    到这儿,我们完成了:  骨骼空间中位置 -> 变换后的骨骼空间中的位置->模型坐标系中的位置

接下来,就是模型坐标系到世界坐标系的变换,这个就不多说,就是一个MVP的转换矩阵。

这样,我们就完成了一次骨骼变换,依附其上的顶点的位置变换的整个过程,由点及面,整个蒙皮的过程就是一个大量该类变换的过程。当然,这儿还没有说一个顶点受多个骨骼影响的情况,其实质也很简单,就是一个顶点受到多个骨骼的影响,那么就各自分配一个对应的权重,那么在计算的时候,各自单独计算,然后乘上权重系数,进行累加,就可以得到mesh上各个顶点的最新位置信息。

三、Unity3D骨骼动画处理

  前面讲解的对于骨骼动画中的骨骼变换,蒙皮的计算,都是在CPU中进行的。在实际的游戏引擎中,这些都是分开处理的,较为通用的处理是将骨骼的动画数据驱动放在CPU中,计算出骨骼的变换矩阵,然后传递给GPU中进行蒙皮计算。在DX10的时候,一般的shader给出的寄存器的大小在128的大小,一个变换矩阵为4x4,如果去除最后一行(0,0,0,1)就可以用3个float表示,那么最多可以表示,嗯,42个左右,如果考虑进行性能优化,不完全占用寄存器的大小,那么一般会限制在30根骨骼的大小上。将这些骨骼的变换矩阵在CPU进行计算后,就可以封装成skin info传递到GPU中。

在GPU的计算中,就会取出这些mesh上的顶点进行对应的位置计算,基于骨骼的转换矩阵和骨骼的权重,得到最新的位置,从而进行一次顶点计算和描绘。之所以将骨骼动画的两个部分分开处理,一个原因就是CPU的处理能力相对而言没有GPU快捷,一般一个模型的骨骼数量是较小的,但是mesh上的顶点数量较大,利用GPU的并行处理能力优势,可以分担CPU的计算压力。

在DX11还是DX12之后(记不太清楚),骨骼变换矩阵的计算结果不再存储在寄存器中,而是存储在一个buffer中,这样的buffer大小基于骨骼数量的大小在第一次计算的时候设定,之后每次骨骼动画数据驱动得到新的变换矩阵,就依次更改对应的buffer中存储的变换矩阵,这样就不再受到寄存器的大小而限制骨骼的根数的大小。但是实际的优化中,都会尽量优化模型的骨骼的数量,毕竟数量越多,特别是影响顶点的骨骼数量越多,那么计算量就会越大,正常的思维是优化骨骼数量而不是去扩展buffer的大小:D

在文章2中,对于GPU的蒙皮计算做了较大的性能优化,主要的思维也是这样,在CPU中进行骨骼变换,将变换的结果传递到GPU中,从而进行蒙皮计算。基本的思维和前面说的变换思维一致,其基本的优化重点也是想利用一个buffer来缓存变换矩阵,从而优化性能。这儿我就重点分析一下shader部分的代码,其在cpu部分的代码处理基本和前面的代码思想一致:

如果采用CPU的计算骨骼变换,那么GPU的shader:

uniform float4x4 _Matrices[]; //设置的骨骼数量最大为24

struct appdata
{
float4 vertex:POSITION;
float2 uv:TEXCOORD0;
//存储的就是骨骼的变换矩阵,x/y为第一个骨骼的索引和权重,z/w为第二个的索引和权重
float4 tangent:TANGENT;
}; v2f vert(appdata v)
{
v2f o;
//蒙皮计算位置,注意看,其实就是矩阵变化加权重的表示
float4 pos =
mul(_Matrices[v.tangent.x], v.vertex)* v.tangent.y +
mul(_Matrices[v.tangent.z], v.vertex)* v.tangent.w
//通用的mvp计算
o.vertex = mul(UNITY_MATRIX_MVP, pos);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
} //怎么计算index和权重,此处一个蒙皮顶点受到2根骨骼的影响
Vector4[] tangents = new Vector4[mesh.vertexCount];
for(int i=; i < mesh.vertexCount;++i)
{
BoneWeight boneWeight = mesh.boneWeights[i];
tangents[i].x = boneWeight.boneIndex0;
tangents[i].y = boneWeight.weight0;
tangents[i].z = boneWeight.boneIndex1;
tangents[i].w = boneWeight.weight1;
} newMesh.tangents = tangents;

  其优化的策略,就是用贴图的方式来存储这个变换矩阵,参看一下代码吧:

inline float4 indexToUV(int index)
{
int row = (int) (index /_MatricesTexSize.x);
int col = (index - row * _MatricesTexsize.x;
return float4(col/_MatricesTexSize.x, row/_MatricesTexSize.y, , );
}
//算出当前的变换矩阵
inline float4x4 getMatrix(int frameStartIndex, float boneIndex)
{
int matStartIndex = frameStartIndex + boneIndex*;
float4 row0 = tex2Dlod(_MatricesTex, indexToUV(matStartIndx));
float4 row1 = tex2Dlod(_MatricesTex, indexToUV(matStartIndx + ));
float4 row2 = tex2Dlod(_MatricesTex, indexToUV(matStartIndx + ));
float4 row3 = float4(,,,);
float4x4 mat = float4x4(row0, row1, row2, row3);
return mat;
} v2f vert(appdata v)
{
v2f o;
float time = _Time.y;
//算出当前时间对应的index
int framIndex = (int)(((_Time.y + v.uv2.x)*_AnimFPS)%(_AnimLength * _AnimFPS));
int frameStartIndex = frameIndex * _MatricesTexFrameTexls;
//去除对应的变换矩阵
float4 mat0 = getMatrix(frameStartIndex, v.tangent.x);
float4 mat1 = getMatrix(frameStartIndex, v.tangent.z); float4 pos =
mul(mat0, v.vertex) * v.tangent,y +
mul(mat1, v.vertex) * v.tangent.w; o.vertex = mul(UNITY_MATRIX_MVP, pos);
o.uv = TRANSFOR_TEX(v.uv, _MainTex);
return o;

  总结:综合整篇文章,对于骨骼动画有了一个较为详细的讲解,主要是参考两篇文章,我只是做了一个知识的搬运工,今儿就写到这儿,后面有更多的更新,会继续回来更改。

Unity3D 骨骼动画原理学习笔记的更多相关文章

  1. 浅谈Unity3D 骨骼动画

    转载请标明出处http://www.cnblogs.com/zblade/ 最近研究了一下游戏中模型的骨骼动画的原理,做一个学习笔记,便于大家共同学习探讨. ps:最近改bug改的要死要活,博客写的吭 ...

  2. Unity3D之Mecanim动画系统学习笔记(七):IK(反向动力学)动画

    什么是IK? IK(Inverse Kinematics)即反向动力学,即可以使用场景中的各种物体来控制和影响角色身体部位的运动,一般来说骨骼动画都是传统的从父节点到子节点的带动方式(即正向动力学), ...

  3. Unity3D骨骼动画的分解(CleanData.Ani详解)

    CleanData是什么 CleanData以前没有特定的名字,(在easydown这个开源项目中,作为一个GameObjParser模块存在).在某三国项目中,我们使用GameObjParser将N ...

  4. Unity3d之Hash&Slash学习笔记之(二)--角色基础类的构建

    Hash&Slash学习笔记之(二)--角色基础类的构建 BaseStat类的构建 基本成员变量: _baseValue //基础属性值 _buffValue //增加的buff值 _expT ...

  5. Unity3D之Legacy动画系统学习笔记

    Unity3D的Mecanim动画系统是非常强大的,而且作为Unity推荐的动画系统,其未来会完全代替老的一套动画系统,即Legacy动画系统.目前的情况是Mecanim与Legacy两套动画系统同时 ...

  6. Unity3D之Mecanim动画系统学习笔记(十):Mecanim动画的资源加载相关

    资源加载是必备的知识点,这里就说说Mecanim动画的资源如何打包及加载. 注意,Unity4.x和Unity5.x的AssetBundle打包策略不一样,本笔记是基于Unity4.x的AssetBu ...

  7. Unity3D之Mecanim动画系统学习笔记(一):认识Mecanim动画系统

    Mecanim简介 Mecanim动画系统是Unity3D4.0开始引入的一套全新的动画系统,主要提供了下面4个方面的功能: 针对人形角色提供一套特殊的工作流. 动画重定向的能力,可以非常方便的把动画 ...

  8. Unity3D之Mecanim动画系统学习笔记(十一):高级功能应用

    动作游戏 还记得读书的时候熬夜打<波斯王子>的时光,我们的王子通过跳跃穿过墙壁的小洞.在高层建筑上进行攀爬和跳跃,还有在操作失误掉下高楼和触发必死机关后使用时之沙的时光倒流功能回归死亡之前 ...

  9. Unity3D之Mecanim动画系统学习笔记(五):Animator Controller

    简介 Animator Controller在Unity中是作为一种单独的配置文件存在的文件类型,其后缀为controller,Animator Controller包含了以下几种功能: 可以对多个动 ...

随机推荐

  1. Atom打造 c/c++编译环境(忙了一个上午)

    众所周知 Atom是一款非常酷炫的编辑器.因为它就像上古卷轴一样,玩家可以开发各种dlc补丁,实现自己想要的效果.所以Atom 可以被你改造成自己想要的东西,可以用来写算法竞赛题目,可以开发网页,可以 ...

  2. 篇4 安卓app自动化测试-Appium API进阶

    篇4                 安卓app自动化测试-Appium API进阶 --lamecho辣么丑 1.1概要 大家好! 我是lamecho(辣么丑),今天是<安卓app自动化测试& ...

  3. NodeJs之fs的读写删移监

    NodeJs版本:4.4.4 fs 文件系统模块是一个封装了标准的 POSIX 文件 I/O 操作的集合.Node.js 文件系统(fs 模块)模块中的方法均有异步和同步版本. 图片的复制与粘贴 创建 ...

  4. 比较Java中几个常用集合添加元素的效率

    初始化需要进行比较的集合,统一增加10万个元素,获取整个过程的执行时间. 1.List集合增加元素 private static void testList() { List<Integer&g ...

  5. C++随机数rand(), srand()

    c++产生随机数会用到rand(), srand()函数,下面总结两个函数特性和使用. 1. rand() #include <iostream> #include <cstdlib ...

  6. yum 安装vim编辑器

    在我们使用的vi编辑器中没有配色不是很好用,我们安装一个vim富文本编辑器方便我们的文本编辑. [root@localhost ~]# yum install vim -y已加载插件:fastestm ...

  7. Android自定义评分控件:RatingStarView

    RatingStarView Android自定义的评分控件,类似ProgressBar那样的,使用星星图标(full.half.empty)作为progress标识的评分/打分控件. 效果图 图1: ...

  8. 微信小程序之购物车功能

    前言 以往的购物车,基本都是通过大量的 DOM 操作来实现.微信小程序其实跟 vue.js 的用法非常像,接下来就看看小程序可以怎样实现购物车功能. 需求 先来弄清楚购物车的需求. 单选.全选和取消, ...

  9. [刷题]算法竞赛入门经典(第2版) 5-4/UVa10763 - Foreign Exchange

    题意:有若干交换生.若干学校,有人希望从A校到B校,有的想从B到C.C到A等等等等.如果有人想从A到B也刚好有人想从B到A,那么可以交换(不允许一对多.多对一).看作后如果有人找不到人交换,那么整个交 ...

  10. ZooKeeper实践:(1)配置管理

    一. 前言     配置是每个程序不可或缺的一部分,配置有多重方式:xml.ini.property.database等等,从最初的单机环境到现在的分布式环境. 1. 以文件的格式存储配置,修改任何都 ...