3D模型人物换装系统
- 介绍
- 遇到的问题
- 问题修复
- 具体实现换装
- 1.准备所有模型部位和模型骨骼
- 部位准备
- 材质准备
- 模型根骨骼准备
- 创建文件夹将上述模型拖成预制体
- 创建一个动画状态机给他们附上待机动画
- 2.脚本驱动
- Mesh合并代码 UCombineSkinnedMgr.cs
- 创建Mesh以及实例化对象的代码 UCharacterController.cs
- 测试换装调用 UCharacterManager.cs
- 3.测试
- 总结
介绍
本文使用2018.4.4和2020.3.26进行的测试
人物换装系统
人物换装系统下
人物换装对模型的还是有一定要求的,首先换装是要有多套模型的,通用做法是将人物的身体(包含头),衣服、裤子、鞋子、头发分别作为一个部位,然后将这几个部位动态合批成一个人物模型。有的人身体和头也是分开的部位这个看做模型那边的情况具体定。还有的不要身体直接分成衣服、裤子、双手、双脚、头(包含头发),我这里介绍的也是这种方法这样也能避免很多蒙皮问题。
遇到的问题
之前我们在做换装人物合批的时候也遇到过几个影响比较大的问题这里我拿出来说一下。
- 蒙皮外露穿模情况:这个也比较好理解其实就是人物在做动作的时候可能把人身体的一部分露出来了,比如说肩膀衣服露肉了这种穿模情况。
- 人物合批材质也需要合批那么身上的贴图合批会乱(这种情况我会在下面代码讲一下这里不详细说了)。
问题修复
针对蒙皮穿模的这种情况其实有很多办法
- 协商模型制作去掉能够被衣服挡住的蒙皮(但是这里要考虑所有的衣服,但凡有一个衣服是需要露这块的蒙皮那么就不能使用这个方法)
- 针对不同的衣服使用不同程度的蒙皮,比如A衣服要用露胳膊但是B衣服不露胳膊,那么做一个是删除胳膊的身体蒙皮A1和一个是不删除胳膊的身体蒙皮B1,这样A-A1绑定 B-B1绑定,需要穿A衣服的时候绑定A1身体,需要穿B衣服的时候绑定B1身体。
- 不要身体,身体直接和衣服裤子绑定在一起做,去掉身体部位(这个也是本文的做法)。
具体实现换装
1.准备所有模型部位和模型骨骼
部位准备
如下所示把其中一个赋值好材质的模型预制体拖上来,红框是每个需要合批的部位。
选中每一个部位然后Ctrl + D拷贝一份
将上述拷贝的部位改名如下
全部拷贝出来如下所示,这样的对象是包含根节点信息的
改名如下
眉毛和脸模型都是一样的就不需要合批处理只用一个即可
材质准备
模型材质赋值到上述对象上
模型根骨骼准备
使用刚才上面的那个白膜拖拽到场景上
解除预制体绑定先
删除下面其他的Mesh模型
修改名称如下
这里我只是用了一个模型来讲,换装肯定有多个模型,其他模型也是这样处理,第一个模型名字我后缀都是1,后面的模型依次后缀是2、3、4…
创建文件夹将上述模型拖成预制体
创建文件如下,并且将上面的拷贝的对象分别放到对应文件夹下做预制体
其他所有的需要换装的模型都这样处理放到对应文件夹下
上图中脸和眉毛只有一个是因为都是一样的只保留了一份
创建一个动画状态机给他们附上待机动画
创建一个动画控制器并双击打开
将待机动画拖拽上来
将之前的所有模型根节点中的动画控制器换成刚才我们生成的动画控制器
2.脚本驱动
Mesh合并代码 UCombineSkinnedMgr.cs
using UnityEngine;
using System.Collections.Generic;
public class UCombineSkinnedMgr
{
/// <summary>
/// Combine SkinnedMeshRenderers together and share one skeleton.
/// Merge materials will reduce the drawcalls, but it will increase the size of memory.
/// </summary>
/// <param name="skeleton">combine meshes to this skeleton(a gameobject)</param>
/// <param name="meshes">meshes need to be merged</param>
/// <param name="combine">merge materials or not</param>
public void CombineObject(GameObject skeleton, SkinnedMeshRenderer[] meshes)
{
// Fetch all bones of the skeleton
List<Transform> transforms = new List<Transform>();
transforms.AddRange(skeleton.GetComponentsInChildren<Transform>(true));
List<Material> materials = new List<Material>();//the list of materials
List<CombineInstance> combineInstances = new List<CombineInstance>();//the list of meshes
List<Transform> bones = new List<Transform>();//the list of bones
// Collect information from meshes
//获取所有
for (int i = 0; i < meshes.Length; i++)
{
SkinnedMeshRenderer smr = meshes[i];
materials.AddRange(smr.materials); // Collect materials
// Collect meshes
for (int sub = 0; sub < smr.sharedMesh.subMeshCount; sub++)
{
CombineInstance ci = new CombineInstance();
ci.mesh = smr.sharedMesh;
ci.subMeshIndex = sub;
combineInstances.Add(ci);
}
// Collect bones
for (int j = 0; j < smr.bones.Length; j++)
{
int tBase = 0;
for (tBase = 0; tBase < transforms.Count; tBase++)
{
if (smr.bones[j].name.Equals(transforms[tBase].name))
{
bones.Add(transforms[tBase]);
break;
}
}
}
}
// Create a new SkinnedMeshRenderer
SkinnedMeshRenderer oldSKinned = skeleton.GetComponent<SkinnedMeshRenderer>();
if (oldSKinned != null)
{
GameObject.DestroyImmediate(oldSKinned);
}
SkinnedMeshRenderer r = skeleton.AddComponent<SkinnedMeshRenderer>();
r.sharedMesh = new Mesh();
r.sharedMesh.CombineMeshes(combineInstances.ToArray(), false, false);// Combine meshes
r.bones = bones.ToArray();// Use new bones
r.materials = materials.ToArray();
}
}
创建Mesh以及实例化对象的代码 UCharacterController.cs
using UnityEngine;
public class UCharacterController
{
/// <summary>
/// GameObject reference
/// </summary>
public GameObject Instance = null;
/// <summary>
/// 换装总组装数量
/// </summary>
public int m_MeshCount = 9;
public string Role_Skeleton;
public string Role_Body;
public string Role_Cloak;
public string Role_Face;
public string Role_Hair;
public string Role_Hand;
public string Role_Leg;
public string Role_MainWeapon;
public string Role_Retina;
public string Role_SubWeapon;
/// <summary>
/// 创建对象
/// </summary>
/// <param name="job"></param>
/// <param name="skeleton"></param>
/// <param name="body"></param>
/// <param name="cloak"></param>
/// <param name="face"></param>
/// <param name="hair"></param>
/// <param name="hand"></param>
/// <param name="leg"></param>
/// <param name="mainweapon"></param>
/// <param name="retina"></param>
/// <param name="subweapon"></param>
public UCharacterController(string job, string skeleton, string body, string cloak, string face, string hair, string hand, string leg, string mainweapon, string retina, string subweapon)
{
Object res = Resources.Load("RoleMesh/" + job + "/" + job + "/" + skeleton);
this.Instance = GameObject.Instantiate(res) as GameObject;
this.Role_Skeleton = skeleton;
this.Role_Body = body;
this.Role_Cloak = cloak;
this.Role_Face = face;
this.Role_Hair = hair;
this.Role_Hand = hand;
this.Role_Leg = leg;
this.Role_MainWeapon = mainweapon;
this.Role_Retina = retina;
this.Role_SubWeapon = subweapon;
string[] equipments = new string[m_MeshCount];
equipments[0] = "Body/" + Role_Body;
equipments[1] = "Cloak/" + Role_Cloak;
equipments[2] = "Face/" + Role_Face;
equipments[3] = "Hair/" + Role_Hair;
equipments[4] = "Hand/" + Role_Hand;
equipments[5] = "Leg/" + Role_Leg;
equipments[6] = "Mainweapon/" + Role_MainWeapon;
equipments[7] = "Retina/" + Role_Retina;
equipments[8] = "Subweapon/" + Role_SubWeapon;
SkinnedMeshRenderer[] meshes = new SkinnedMeshRenderer[m_MeshCount];
GameObject[] objects = new GameObject[m_MeshCount];
for (int i = 0; i < equipments.Length; i++)
{
res = Resources.Load("RoleMesh/" + job + "/" + equipments[i]);
objects[i] = GameObject.Instantiate(res) as GameObject;
meshes[i] = objects[i].GetComponentInChildren<SkinnedMeshRenderer>();
}
UCharacterManager.Instance.CombineSkinnedMgr.CombineObject(Instance, meshes);
for (int i = 0; i < objects.Length; i++)
{
GameObject.DestroyImmediate(objects[i].gameObject);
}
}
public void Delete()
{
GameObject.Destroy(Instance);
}
/// <summary>
/// 部位换装
/// </summary>
/// <param name="path">路径</param>
/// <param name="index">切换部位</param>
/// <param name="equipment"></param>
/// <param name="combine"></param>
public void ChangeEquipments(string path, int index, int equipmentId)
{
switch (index)
{
case 0:
Role_Body = "Body" + equipmentId;
break;
case 1:
Role_Cloak = "Cloak" + equipmentId;
break;
case 2:
Delete();
this.Instance = GameObject.Instantiate(Resources.Load("RoleMesh/" + path + "/" + path + "/" + path + equipmentId)) as GameObject;
Role_Hair = "Hair" + equipmentId;
break;
case 3:
Role_Hand = "Hand" + equipmentId;
break;
case 4:
Role_Leg = "Leg" + equipmentId;
break;
case 5:
Role_MainWeapon = "Mainweapon" + equipmentId;
Role_SubWeapon = "Subweapon" + equipmentId;
break;
}
string[] equipments = new string[m_MeshCount];
equipments[0] = "Body/" + Role_Body;
equipments[1] = "Cloak/" + Role_Cloak;
equipments[2] = "Face/" + Role_Face;
equipments[3] = "Hair/" + Role_Hair;
equipments[4] = "Hand/" + Role_Hand;
equipments[5] = "Leg/" + Role_Leg;
equipments[6] = "Mainweapon/" + Role_MainWeapon;
equipments[7] = "Retina/" + Role_Retina;
equipments[8] = "Subweapon/" + Role_SubWeapon;
Object res = null;
SkinnedMeshRenderer[] meshes = new SkinnedMeshRenderer[m_MeshCount];
GameObject[] objects = new GameObject[m_MeshCount];
for (int i = 0; i < equipments.Length; i++)
{
res = Resources.Load("RoleMesh/" + path + "/" + equipments[i]);
objects[i] = GameObject.Instantiate(res) as GameObject;
meshes[i] = objects[i].GetComponentInChildren<SkinnedMeshRenderer>();
}
UCharacterManager.Instance.CombineSkinnedMgr.CombineObject(Instance, meshes);
for (int i = 0; i < objects.Length; i++)
{
GameObject.DestroyImmediate(objects[i].gameObject);
}
}
}
测试换装调用 UCharacterManager.cs
using UnityEngine;
using System.Collections.Generic;
/// <summary>
/// 换装管理器
/// </summary>
public class UCharacterManager : MonoBehaviour
{
public static UCharacterManager Instance;
private UCombineSkinnedMgr skinnedMgr = null;
public UCombineSkinnedMgr CombineSkinnedMgr { get { return skinnedMgr; } }
private int characterIndex = 0;
private Dictionary<int, UCharacterController> characterDic = new Dictionary<int, UCharacterController>();
public UCharacterManager()
{
skinnedMgr = new UCombineSkinnedMgr();
}
private void Awake()
{
Instance = this;
}
public UCharacterController mine;
private void Start()
{
mine = Generatecharacter("Axceler", "Axceler1", "Body5", "Cloak5", "Face", "Hair1", "Hand5", "Leg5", "Mainweapon5", "Retina", "Subweapon5");
}
private void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
ChangeRole();
}
//衣服
if (Input.GetKeyDown(KeyCode.Q))
{
mine.ChangeEquipments("Axceler", 0, Random.Range(1, 8));
}
//飘带
if (Input.GetKeyDown(KeyCode.W))
{
mine.ChangeEquipments("Axceler", 1, Random.Range(1, 7));
}
//头发
if (Input.GetKeyDown(KeyCode.E))
{
mine.ChangeEquipments("Axceler", 2, Random.Range(1, 8));
}
//手套
if (Input.GetKeyDown(KeyCode.A))
{
mine.ChangeEquipments("Axceler", 3, Random.Range(1, 8));
}
//腿饰
if (Input.GetKeyDown(KeyCode.S))
{
mine.ChangeEquipments("Axceler", 4, Random.Range(1, 8));
}
//武器
if (Input.GetKeyDown(KeyCode.D))
{
mine.ChangeEquipments("Axceler", 5, Random.Range(1, 8));
}
}
public void ChangeRole()
{
if (mine != null)
{
mine.Delete();
}
int a = Random.Range(1, 8);
int b = Random.Range(1, 8);
int c = Random.Range(1, 7);
int d = Random.Range(1, 8);
int e = Random.Range(1, 8);
int f = Random.Range(1, 8);
int g = Random.Range(1, 8);
mine = Generatecharacter("Axceler", "Axceler" + a, "Body" + b, "Cloak" + c, "Face", "Hair" + a, "Hand" + d, "Leg" + e, "Mainweapon" + f, "Retina", "Subweapon" + g);
}
#region 创建人物模型骨骼
public UCharacterController Generatecharacter(string job, string skeleton, string body, string cloak, string face, string hair, string hand, string leg, string mainweapon, string retina, string subweapon)
{
UCharacterController instance = new UCharacterController(job, skeleton, body, cloak, face, hair, hand, leg, mainweapon, retina, subweapon);
characterDic.Add(characterIndex, instance);
characterIndex++;
return instance;
}
#endregion
}
3.测试
随便创建一个场景然后在随便一个对象上面挂载UCharacterManager.cs脚本运行测试
总结
这个方案是将所有的Mesh合并成一个Mesh,材质球是叠加的并没有合批材质
注意:如果要想将模型完全合批需要将所有图都设置可读可写,这里放到另一篇文章去讲,这里先不讲合批材质的方法
本文资源
如果文章对你有帮助的话留一个免费的关注和点赞吧