3D模型人物换装系统三(优化合批处理,提取合批配置可,封装)
- 介绍
- 法线贴图问题
- 规划以及封装
- 缺陷
- 修改整理
- 整合
- 总结
介绍
本文使用2018.4.4和2020.3.26进行的测试
这里先说一下我上一篇没有解决的问题,法线贴图不正确,如果按照我之前的文章创建其他贴图的方式去创建法线贴图,你会发现法线贴图不正确。
如果这里不太明白换装的流程可以参考我之前
3D模型人物换装系统一
3D模型人物换装系统二
法线贴图问题
之前创建贴图的方式如下所示
这里能看到我创建所有图片的时候的格式都是使用的RGBA32,但是后来我在网上查了以后才发现,其实法线贴图是不带A通道的图片,所以这里应该使用的是RGB24而不是使用的RGBA32,前后对比图我就不放了,因为我对这里的方法进行了封装,已经不是原来的方式创建和合并贴图了。
newAlbedoMapTex = new Texture2D(COMBINE_TEXTURE_MAX, COMBINE_TEXTURE_MAX, TextureFormat.RGBA32, true);
newNormalMapTex = new Texture2D(COMBINE_TEXTURE_MAX, COMBINE_TEXTURE_MAX, TextureFormat.RGBA32, true);
newMaskMapTex = new Texture2D(COMBINE_TEXTURE_MAX, COMBINE_TEXTURE_MAX, TextureFormat.RGBA32, true);
规划以及封装
这里我大概讲一下我之前脚本设计的缺陷,以及我想修改成的样子,在文章最后面我会把我修改完成的脚本和资源都放过来,如果大家在后面使用脚本的过程中遇到问题可以在询问我。
缺陷
- 原来合并设计的是太过于死板,材质球的属性是直接提取出来的并没有动态去做,也就是说我这里只能手动修改,如果换一个四个贴图的材质还需要修改下面的代码,这样太不方便,需要封装一下这里
private const string COMBINE_ALBEDOMAP_TEXTURE = "_AlbedoMap";
private const string COMBINE_NORMALMAP_TEXTURE = "_NormalMap";
private const string COMBINE_MASKMAP_TEXTURE = "_MaskMap";
- 创建贴图的大小也是固定死的现在,这里也是有缺陷,没法动态的调整
/// <summary>
/// Only for merge materials.
/// </summary>
private const int COMBINE_TEXTURE_MAX = 256;
- 合并这里或多或少有点缺陷,但是不算是有问题,这里传入的如果是true则就是需要合并,但是如果合并不成功的话就直接报错
if (combine)
{
Shader tmpShader = Shader.Find("E3D/Actor/PBR-MaskRG-Normal");
newMaterial = new Material(tmpShader);
oldUV = new List<Vector2[]>();
// merge the texture
List<Texture2D> AlbedoTextures = new List<Texture2D>();
List<Texture2D> NormalTextures = new List<Texture2D>();
List<Texture2D> MaskTextures = new List<Texture2D>();
for (int i = 0; i < materials.Count; i++)
{
AlbedoTextures.Add(materials[i].GetTexture(COMBINE_ALBEDOMAP_TEXTURE) as Texture2D);
NormalTextures.Add(materials[i].GetTexture(COMBINE_NORMALMAP_TEXTURE) as Texture2D);
MaskTextures.Add(materials[i].GetTexture(COMBINE_MASKMAP_TEXTURE) as Texture2D);
}
newAlbedoMapTex = new Texture2D(COMBINE_TEXTURE_MAX, COMBINE_TEXTURE_MAX, TextureFormat.RGBA32, true);
newNormalMapTex = new Texture2D(COMBINE_TEXTURE_MAX, COMBINE_TEXTURE_MAX, TextureFormat.RGBA32, true);
newMaskMapTex = new Texture2D(COMBINE_TEXTURE_MAX, COMBINE_TEXTURE_MAX, TextureFormat.RGBA32, true);
Rect[] uvs = newAlbedoMapTex.PackTextures(AlbedoTextures.ToArray(), 0);
//newNormalMapTex.PackTextures(NormalTextures.ToArray(), 0);
newMaskMapTex.PackTextures(MaskTextures.ToArray(), 0);
newMaterial.SetTexture(COMBINE_ALBEDOMAP_TEXTURE, newAlbedoMapTex);
newMaterial.SetTexture(COMBINE_NORMALMAP_TEXTURE, newNormalMapTex);
newMaterial.SetTexture(COMBINE_MASKMAP_TEXTURE, newMaskMapTex);
// reset uv
Vector2[] uva, uvb;
for (int i = 0; i < combineInstances.Count; i++)
{
uva = combineInstances[i].mesh.uv;
uvb = new Vector2[uva.Length];
for (int k = 0; k < uva.Length; k++)
{
uvb[k] = new Vector2((uva[k].x * uvs[i].width) + uvs[i].x, (uva[k].y * uvs[i].height) + uvs[i].y);
}
oldUV.Add(uva);
combineInstances[i].mesh.uv = uvb;
}
}
if (combine)
{
r.material = newMaterial;
for (int i = 0; i < combineInstances.Count; i++)
{
combineInstances[i].mesh.uv = oldUV[i];
}
}
- Shader也是写死的,不是动态的
Shader tmpShader = Shader.Find("E3D/Actor/PBR-MaskRG-Normal");
修改整理
- 创建一个配置类,当创建模型的时候根据自己需要传入定制化配置文件,这样可以动态调整创建贴图的大小、动态调整合并贴图的数量、动态修改想要合并成的材质Shader。
/// <summary>
/// 贴图属性
/// </summary>
public class CombineClass
{
/// <summary>
/// 新Shader属性名
/// </summary>
public string NewShaderPropertiesName = "_AlbedoMap";
/// <summary>
/// 原始Shader属性名
/// </summary>
public string OriginalPropertiesName = "_AlbedoMap";
/// <summary>
/// 图片格式
/// </summary>
public TextureFormat Format = TextureFormat.RGBA32;
/// <summary>
///
/// </summary>
public bool MipChain = true;
/// <summary>
/// 实例化
/// </summary>
/// <param name="propertiesName"></param>
/// <param name="format"></param>
/// <param name="mipChain"></param>
public CombineClass(string newPropertiesName, string orinalPropertiesName, TextureFormat format, bool mipChain)
{
this.NewShaderPropertiesName = newPropertiesName;
this.OriginalPropertiesName = orinalPropertiesName;
this.Format = format;
this.MipChain = mipChain;
}
}
public class CombineConfig
{
/// <summary>
/// Only for merge materials.
/// 仅适用于合并材质.
/// </summary>
private int COMBINE_TEXTURE_MAX = 256;
/// <summary>
/// Shader名称
/// </summary>
private string ShaderName = "";
/// <summary>
/// 贴图设置集合
/// </summary>
private List<CombineClass> combineClasses = new List<CombineClass>();
/// <summary>
/// 实例化
/// </summary>
/// <param name="COMBINE_TEXTURE_MAX"></param>
/// <param name="combineClasses"></param>
public CombineConfig(int COMBINE_TEXTURE_MAX, string ShaderName, List<CombineClass> combineClasses)
{
this.COMBINE_TEXTURE_MAX = COMBINE_TEXTURE_MAX;
this.ShaderName = ShaderName;
this.combineClasses = combineClasses;
}
/// <summary>
/// 获取合并贴图大小
/// </summary>
/// <returns></returns>
public int CombineTextureMax() { return this.COMBINE_TEXTURE_MAX; }
/// <summary>
/// 获取Shader名
/// </summary>
/// <returns></returns>
public string GetShaderName() { return this.ShaderName; }
/// <summary>
/// 获取贴图设置集合数量
/// </summary>
/// <returns></returns>
public int GetCombineCount() { return this.combineClasses.Count; }
/// <summary>
/// 获取贴图设置集合
/// </summary>
/// <returns></returns>
public List<CombineClass> GetCombineList() { return combineClasses; }
}
- 获取需要合并的模型的所有的所有Shader,如果是只有一个Shader则你combine为true时会进行合批处理,如果Shader数量多了哪怕你combine为true则也不进行合批。
- 加入了一个材质回调,这样在调用的时候可以对材质球进行设置
整合
CombineClass.cs
/// <summary>
/// 贴图属性
/// </summary>
public class CombineClass
{
/// <summary>
/// 新Shader属性名
/// </summary>
public string NewShaderPropertiesName = "_AlbedoMap";
/// <summary>
/// 原始Shader属性名
/// </summary>
public string OriginalPropertiesName = "_AlbedoMap";
/// <summary>
/// 图片格式
/// </summary>
public TextureFormat Format = TextureFormat.RGBA32;
/// <summary>
///
/// </summary>
public bool MipChain = true;
/// <summary>
/// 实例化
/// </summary>
/// <param name="propertiesName"></param>
/// <param name="format"></param>
/// <param name="mipChain"></param>
public CombineClass(string newPropertiesName, string orinalPropertiesName, TextureFormat format, bool mipChain)
{
this.NewShaderPropertiesName = newPropertiesName;
this.OriginalPropertiesName = orinalPropertiesName;
this.Format = format;
this.MipChain = mipChain;
}
}
CombineConfig.cs
public class CombineConfig
{
/// <summary>
/// Only for merge materials.
/// 仅适用于合并材质.
/// </summary>
private int COMBINE_TEXTURE_MAX = 256;
/// <summary>
/// Shader名称
/// </summary>
private string ShaderName = "";
/// <summary>
/// 贴图设置集合
/// </summary>
private List<CombineClass> combineClasses = new List<CombineClass>();
/// <summary>
/// 实例化
/// </summary>
/// <param name="COMBINE_TEXTURE_MAX"></param>
/// <param name="combineClasses"></param>
public CombineConfig(int COMBINE_TEXTURE_MAX, string ShaderName, List<CombineClass> combineClasses)
{
this.COMBINE_TEXTURE_MAX = COMBINE_TEXTURE_MAX;
this.ShaderName = ShaderName;
this.combineClasses = combineClasses;
}
/// <summary>
/// 获取合并贴图大小
/// </summary>
/// <returns></returns>
public int CombineTextureMax() { return this.COMBINE_TEXTURE_MAX; }
/// <summary>
/// 获取Shader名
/// </summary>
/// <returns></returns>
public string GetShaderName() { return this.ShaderName; }
/// <summary>
/// 获取贴图设置集合数量
/// </summary>
/// <returns></returns>
public int GetCombineCount() { return this.combineClasses.Count; }
/// <summary>
/// 获取贴图设置集合
/// </summary>
/// <returns></returns>
public List<CombineClass> GetCombineList() { return combineClasses; }
}
UCombineSkinnedMgr.cs
using UnityEngine;
using System.Collections.Generic;
using System.IO;
using System;
public class UCombineSkinnedMgr
{
/// <summary>
/// Combine SkinnedMeshRenderers together and share one skeleton.
/// 将蒙皮网格渲染器组合在一起并共享一个骨架.(优化合批报错,如果无法合批但是combine为true,则进行不分批处理)
/// Merge materials will reduce the drawcalls, but it will increase the size of memory.
/// 合并材质会减少drawcalls,但会增加内存大小.
/// </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(CombineConfig config, GameObject skeleton, SkinnedMeshRenderer[] meshes, bool combine = false, Action<Material> action = null)
{
//Fetch all bones of the skeleton
//获取所有的骨骼
List<Transform> transforms = new List<Transform>();
transforms.AddRange(skeleton.GetComponentsInChildren<Transform>(true));
//the list of materials
List<Shader> shaders = new List<Shader>();
//the list of materials
//所有材质球
List<Material> materials = new List<Material>();
//the list of meshes
//所有网格
List<CombineInstance> combineInstances = new List<CombineInstance>();
//the list of bones
//所有骨骼节点
List<Transform> bones = new List<Transform>();
#region 合批使用
// Below informations only are used for merge materilas(bool combine = true)
//以下信息仅用于合并材料(bool-combine=true)原始UV坐标
List<Vector2[]> oldUV = null;
//合并之后得新材质球
Material newMaterial = null;
//创建新贴图集合
List<Texture2D> MapTex = new List<Texture2D>();
for (int i = 0; i < config.GetCombineCount(); i++)
{
MapTex.Add(null);
}
#endregion
// Collect information from meshes and shader
// 获取网格和shader信息
for (int i = 0; i < meshes.Length; i++)
{
SkinnedMeshRenderer smr = meshes[i];
materials.AddRange(smr.materials);
for (int j = 0; j < smr.materials.Length; j++)
{
if (!shaders.Contains(smr.materials[j].shader))
{
shaders.Add(smr.materials[j].shader);
}
}
// 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;
}
}
}
}
// merge materials
//合并材质
if (combine && shaders.Count == 1)
{
if (config.GetShaderName() == "") newMaterial = new Material(shaders[0]);
else newMaterial = new Material(Shader.Find(config.GetShaderName()));
oldUV = new List<Vector2[]>();
// merge the texture
//合并贴图
List<List<Texture2D>> texture2Ds = new List<List<Texture2D>>();
for (int i = 0; i < config.GetCombineCount(); i++)
{
texture2Ds.Add(new List<Texture2D>());
}
for (int i = 0; i < materials.Count; i++)
{
Material mat = materials[i];
for (int j = 0; j < config.GetCombineCount(); j++)
{
texture2Ds[j].Add(mat.GetTexture(config.GetCombineList()[j].OriginalPropertiesName) as Texture2D);
}
//int ind = 0;
//texture2Ds.ForEach((txtLst) =>
//{
// txtLst.Add(mat.GetTexture(config.GetCombineList()[ind].OriginalPropertiesName) as Texture2D);
// ind++;
//});
}
Rect[] uvs = new Rect[config.GetCombineCount()];
for (int i = 0; i < config.GetCombineCount(); i++)
{
MapTex[i] = new Texture2D(config.CombineTextureMax(), config.CombineTextureMax(), config.GetCombineList()[i].Format, config.GetCombineList()[i].MipChain);
uvs = MapTex[i].PackTextures(texture2Ds[i].ToArray(), 0);
newMaterial.SetTexture(config.GetCombineList()[i].NewShaderPropertiesName, MapTex[i]);
}
action?.Invoke(newMaterial);
#region 导出图片
//WriteIntoPic(TextureToTexture2D(newMaterial.GetTexture(COMBINE_ALBEDOMAP_TEXTURE)), "albedo");
//WriteIntoPic(TextureToTexture2D(newMaterial.GetTexture(COMBINE_NORMALMAP_TEXTURE)), "normal");
//WriteIntoPic(TextureToTexture2D(newMaterial.GetTexture(COMBINE_MASKMAP_TEXTURE)), "mask");
#endregion
// reset uv
Vector2[] uva, uvb;
for (int i = 0; i < combineInstances.Count; i++)
{
uva = combineInstances[i].mesh.uv;
uvb = new Vector2[uva.Length];
for (int k = 0; k < uva.Length; k++)
{
uvb[k] = new Vector2((uva[k].x * uvs[i].width) + uvs[i].x, (uva[k].y * uvs[i].height) + uvs[i].y);
}
oldUV.Add(uva);
combineInstances[i].mesh.uv = uvb;
}
}
// 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(), combine && shaders.Count == 1, false);// Combine meshes
r.bones = bones.ToArray();// Use new bones
if (combine && shaders.Count == 1)
{
r.material = newMaterial;
for (int i = 0; i < combineInstances.Count; i++)
{
combineInstances[i].mesh.uv = oldUV[i];
}
}
else
{
r.materials = materials.ToArray();
}
}
#region 导出图片
private Texture2D TextureToTexture2D(Texture texture)
{
Texture2D texture2D = new Texture2D(texture.width, texture.height, TextureFormat.RGBA32, false);
RenderTexture currentRT = RenderTexture.active;
RenderTexture renderTexture = RenderTexture.GetTemporary(texture.width, texture.height, 32);
Graphics.Blit(texture, renderTexture);
RenderTexture.active = renderTexture;
texture2D.ReadPixels(new Rect(0, 0, renderTexture.width, renderTexture.height), 0, 0);
texture2D.Apply();
RenderTexture.active = currentRT;
RenderTexture.ReleaseTemporary(renderTexture);
return texture2D;
}
public void WriteIntoPic(Texture2D tex, string name)
{
//编码纹理为PNG格式
var bytes = tex.EncodeToPNG();
File.WriteAllBytes(Application.dataPath + "/" + name + ".png", bytes);
}
#endregion
}
UCharacterController.cs
using UnityEngine;
public class UCharacterController
{
/// <summary>
/// GameObject reference
/// </summary>
public GameObject Instance = null;
/// <summary>
/// 换装总组装数量
/// </summary>
public int m_MeshCount = 7;
public string Role_Skeleton;
public string Role_Body;
public string Role_Clothes;
public string Role_Hair;
public string Role_Head;
public string Role_Pants;
public string Role_Shoes;
public string Role_Socks;
/// <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>
/// <param name="combine"></param>
public UCharacterController(CombineConfig config, string job, string skeleton, string body, string clothes, string hair, string head, string pants, string shoes, string socks, bool combine = false, System.Action<Material> action = null)
{
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_Clothes = clothes;
this.Role_Hair = hair;
this.Role_Head = head;
this.Role_Pants = pants;
this.Role_Shoes = shoes;
this.Role_Socks = socks;
string[] equipments = new string[m_MeshCount];
equipments[0] = "Body/" + Role_Body;
equipments[1] = "Clothes/" + Role_Clothes;
equipments[2] = "Hair/" + Role_Hair;
equipments[3] = "Head/" + Role_Head;
equipments[4] = "Pants/" + Role_Pants;
equipments[5] = "Shoes/" + Role_Shoes;
equipments[6] = "Socks/" + Role_Socks;
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(config, Instance, meshes, combine, action);
for (int i = 0; i < objects.Length; i++)
{
GameObject.DestroyImmediate(objects[i].gameObject);
}
}
public void Delete()
{
GameObject.Destroy(Instance);
}
}
测试代码
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(new CombineConfig(256, "", new List<CombineClass> {
// new CombineClass("_AlbedoMap","_AlbedoMap",TextureFormat.RGBA32,true),
// new CombineClass("_NormalMap","_NormalMap",TextureFormat.RGB24,true),
// new CombineClass("_MaskMap","_MaskMap", TextureFormat.RGBA32,true),
//}), "MaTa", "MaTa", "Body1", "Clothes1", "Hair1", "Head1", "Pants1", "Shoes1", "Socks1", true, (mat) =>
//{
// mat.SetFloat("_SideLightScale", 0);
//});
mine = Generatecharacter(new CombineConfig(256, "Standard", new List<CombineClass> {
new CombineClass("_MainTex","_AlbedoMap",TextureFormat.RGBA32,true),
new CombineClass("_BumpMap","_NormalMap",TextureFormat.RGB24,true),
new CombineClass("_DetailMask","_MaskMap", TextureFormat.RGBA32,true),
}), "MaTa", "MaTa", "Body1", "Clothes1", "Hair1", "Head1", "Pants1", "Shoes1", "Socks1", true, (mat) =>
{
mat.SetFloat("_SideLightScale", 0);
});
}
private void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
ChangeRole();
}
}
public void ChangeRole()
{
//if (mine != null)
//{
// mine.Delete();
//}
//int a = Random.Range(1, 4);
//mine = Generatecharacter("MaTa", "MaTa", "Body" + a, "Clothes" + a, "Hair" + a, "Head" + a, "Pants" + a, "Shoes" + a, "Socks" + a, true);
}
#region 创建人物模型骨骼
public UCharacterController Generatecharacter(CombineConfig config, string job, string skeleton, string body, string clothes, string hair, string hand, string pants, string shoes, string socks, bool combine = false, System.Action<Material> action = null)
{
UCharacterController instance = new UCharacterController(config, job, skeleton, body, clothes, hair, hand, pants, shoes, socks, combine, action);
characterDic.Add(characterIndex, instance);
characterIndex++;
return instance;
}
#endregion
}
完整资源
总结
人物换装的优化这篇文章后面应该不会在出了,预计得优化大概就是这样,如果有别的好点子可以私信我交流一下。希望这篇文章能给大家带来帮助,感谢大家的支持和关注,感谢点赞。