最近在看Unity3D的人物模型和动画。所以今天先说下人物的换装吧。相信大家都玩过网游吧,没有玩过的也相信见过,就是网游或者单机游戏里的人物会有更换服装,更换武器的功能。如果外形(mesh)是一样的,那么把贴图换下就好,但是如果外形不一样甚至骨骼都变了的话,就需要我们今天讨论的技术了。
一.Unity3D里的3D模型
Unity3D里的基础模型是从3D工具中导出来的fbx文件。把fbx放在Assets目录下会自动生成一个Materials文件夹,里面会生成一个材质球。在Unity3D项目里的显示是这样
注意:Textrue需要另外添加进去。
模型的主要文件就是那个蓝色的方块,前面两个(WGKS_wugang,WGKS_yaodai)这两个文件是模型的Mesh信息,一个是人物本身的,一个是腰带的,因为比较简单的模型,所以只有两个。下面的一坨是动作信息,动作是可以自己设置的,这个以后文章会讲。而紧接着的
“Bip001”这个是模型的骨骼信息,你拖到Scene里就能层层打开看到了。而WGKS_yaodai则是蒙皮信息的一些玩意。选择到它会在Inspector属性栏里看到SkinnedMeshRenderer组件,这里会看到它牵扯到的mesh,material,bone信息。
二.换装方案1组合模型的显示和隐藏
这个题目可能说的不合适,但是想不到更合适的了。我们的第一种换装的方法很简单,就是让模型或者Mesh进行显示和隐藏。就比如让人物穿两套衣服,显示第一套的时候隐藏第二套,显示第二套的时候显示第一套。
例如这个模型:
这里有TBJ_SR_01和TBJ_SR_03两个SkinnedMeshRenderer.那么我们可以分别设置Active来设置是否可见。代码如下:
public GameObject clothA = null;
public GameObject clothB = null;
if (GUI.Button(new Rect(5, 5, 100, 50), "ClothA"))
{
clothA.SetActive(true);
clothB.SetActive(false);
}
if (GUI.Button(new Rect(5, 60, 100, 50), "ClothB"))
{
clothA.SetActive(false);
clothB.SetActive(true);
}
运行起来的效果如下:
ClothA
ClothB
如果角色是换武器而不是换装备的话,原理是一样的,只不过我们要找到附属的武器GameObject然后设置让它显示和隐藏就可以了。
三.换装方案2 替换Skin信息
方案1的方法用起来比较简单,比较适合简单的模型替换,比如一个模型来来回回就两把武器的话,就可以用。但是如果一个模型有十来套衣服或者十几件装备的话,这么做会比较悲剧的,因为太占资源了,创建一个模型,牵扯的不用的东西也整进内存了,暴殄天物。
所以我想能不能游戏运行的时候替换模型的Skin信息达到我们的目的。一开始我觉得,其实我们只需要把sharedmesh,material替换掉就行了。实验证明不行的。大家可以自己尝试下,Unity3D会报错,即使不报错模型也会变形的。模型中还有一块是骨骼没有动,所以我想尝试下如果把骨骼去掉能不能拿到mesh信息等,然后再替换。结果是从3DMAX里去掉骨骼导出的话,蒙皮信息也没有了。所以我确定了一点是,被换装的模型必须有骨架,而源Mesh信息中必须包含骨架信息,按照我的理解就是Mesh中保存着这个点依附的是哪个骨头,权重是多少等。
这里我准备了两个模型:
带有完整的信息(骨头,动作,蒙皮信息等,只不过模型只有一个mesh而已)。
放在Scene的样子:
我的目的是想让左边的那个模型读取右边模型的SkinnedMeshRenderer信息,然后赋值给自己。首先我把右边的模型除了骨架的所以资源赋值给一个prefab,然后加载prefab,读取信息给左边的模型。
代码如下:
// 声明两个SkinnedMeshRenderer
public SkinnedMeshRenderer MyskinMeshRender = null;
public SkinnedMeshRenderer SrcSkinMeshRender = null;
//加载资源
Object Src = Resources.Load("Res/Materials2");
objSrc = Instantiate(Src) as GameObject;
//找到目标模型的SkinnedMeshRenderer
MyskinMeshRender = GameObject.Find("TBJ_SR_01").GetComponent();
//获取加载的SkinnedMeshRenderer
SrcSkinMeshRender = objSrc.GetComponent();
//获取目标模型的骨骼
Transform[] bonesMy = gameObject.GetComponentsInChildren();
Debug.Log(bonesMy.Length);
//Transform[] bonesSrc = GameObject.Find("TBJ_SR_03").GetComponentsInChildren();
//获得源SkinMesh中依附的骨骼信息
Transform[] bonesSrc = SrcSkinMeshRender.bones;
Debug.Log(bonesSrc.Length);
//根据源SkinMesh依附的骨骼信息重新排列目标骨骼
List bones = new List();
foreach (Transform boneS in bonesSrc)
{
foreach (Transform boneM in bonesMy)
{
if (boneS != null && boneM != null)
{
if (boneS.name != boneM.name)
{
continue;
}
bones.Add(boneM);
}
}
}
MyskinMeshRender.bones = bones.ToArray();
MyskinMeshRender.sharedMesh = SrcSkinMeshRender.sharedMesh;
MyskinMeshRender.sharedMaterials =SrcSkinMeshRender.sharedMaterials;
运行之后发现皮肤换了,而且动画信息没有丢失掉,也就是说还能动,但是有个奇葩的现象,人物是倒着的,如图:
这个我分析后觉得是因为骨骼的问题。两个模型的骨骼有点不一样。因为我是读取了右边的模型的蒙皮信息对应的骨骼信息而重组了左边的骨骼,但是左边的动画信息还是按照原来的。虽然不是很清楚动画数据里面数据结构,但是我觉得就是记录的骨骼或者mesh或者当前模型的世界坐标的某个点对应的Mesh或者骨骼移动到哪个位置。
接着我换了一个思路,用一个人物模型,一套骨骼,一套动作,不过蒙皮信息有两个,导出fbx。
然后在3DMax里面导出一个没有蒙皮信息只有骨骼和动作的fbx,如图:
然后添加脚本,代码如下:
//声明两个SkinMeshRender 声明为public是为了方便观察 真正需要private
public SkinnedMeshRenderer MyskinMeshRender = null;
public SkinnedMeshRenderer SrcSkinMeshRender = null;
//添加SkinnedMeshRenderer组件
MyskinMeshRender = gameObject.AddComponent();
if (Input.GetKeyDown(KeyCode.Z))
{
GameObject obj = GameObject.Find("TBJ_SR_01") as GameObject;
SrcSkinMeshRender = GameObject.Find("TBJ_SR_01").GetComponent();
Transform[] bonesMy = gameObject.GetComponentsInChildren();
Transform[] bonesSrc = SrcSkinMeshRender.bones;
List bones = new List();
foreach (Transform boneM in bonesSrc)
{
foreach (Transform boneS in bonesMy)
{
if (boneM != null && boneS != null)
{
if (boneM.name != boneS.name)
{
continue;
}
bones.Add(boneS);
}
}
}
MyskinMeshRender.bones = bones.ToArray();
MyskinMeshRender.sharedMesh = SrcSkinMeshRender.sharedMesh;
MyskinMeshRender.sharedMaterials = SrcSkinMeshRender.sharedMaterials;
}
运行,结果没有问题了。如图:
按下Z之后:
而且动作保留。
总结:
虽然这个例子不是很经典,也存在些许问题,希望大家指出问题,共同进步。在添加skinnedMeshRenderer组件的时候有个问题,如果模型选择Generic的时候,Unity3D会挂掉。
这个例子可以运用到骨骼和动作基本一样,但是外形不同的模型中,减少游戏中的资源重复占用。网上还有个更经典的例子,运用的是Character Customization 包,运用的原理是一样的,这里给大家看下运行结果:
这个模型是由各个部分的mesh组成,而我的例子是用的一个mesh。在复杂的模型中,我们可以查找子组件的SkinnedMeshRenderer,然后进行操作就可以了。想换哪里就替换哪里的SkinnedMeshRenderer的信息,更新对应的骨骼列表和组合一下网格就行了。
List combineInstances = new List();
CombineInstance ci = new CombineInstance();
ci.mesh = SrcSkinMeshRender.sharedMesh;
combineInstances.Add(ci);
还有一个想法是这样的,源资源中是否需要骨头。我发现重组骨头结构的时候其实只是读取了骨头的列表信息,所以我觉得可以写成一个配置信息,直接读取配置信息来重组。这个想法还没有得到验证,等有时间的吧。