3D模型人物换装系统二(优化材质球合批降低DrawCall)

3D模型人物换装系统

  • 介绍
  • 原理
  • 合批材质对比没有合批材质
  • 核心代码
  • 完整代码修改
  • 总结

介绍

本文使用2018.4.4和2020.3.26进行的测试
本文没有考虑法线贴图合并的问题,因为生成法线贴图有点问题,放在下一篇文章解决在进行优化
如果这里不太明白换装的流程可以参考我之前3D模型人物换装系统
请添加图片描述

原理

原理其实很简单,其实就是将原来没有合批的材质进行了一个合批我下面截图给大家演示一下
下面的图你可以看到只有一个合并贴图的材质球,而且DrawCall也很低,仔细看下面的贴图你能看到其实是好几张贴图合并成了一个
在这里插入图片描述
下面这个是没有合批材质的模型,能看出来Draw Call其实很高,已经到了37,而且材质球也是很多个但是贴图是一个对应一个
在这里插入图片描述

合批材质对比没有合批材质

  1. 合批材质的DrawCall会降低很多有多少个材质就降低多少倍

  2. 合批材质需要贴图有要求,这里我所有用到的贴图大小都是一致的256当然大小多少都可以,前提是需要贴图大小一致
    在这里插入图片描述

  3. 合批贴图需要设置成可读可写
    在这里插入图片描述

  4. 缺点是你合并材质的时候生成图片是需要消耗一定的CPU的,最好做预加载不然会卡顿

  5. 注意:需要身上所有的贴图是相同的shader才能使用,并且每个材质球贴图也要数量相同

综合上面考虑其实合并材质是必然要用的,只要做好预加载其实都不是问题

核心代码

这里面的核心主要就是重新生成新的拼接的贴图,对模型的纹理做一下调整
uv调整代码如下

// 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;
}

完整代码修改

这里我就不放资源了(上一篇文章有之前的资源,但是之前资源头发和眉毛跟人物身上材质不同,无法合批仅供参考,本文的模型是公司未上线游戏模型不能发布公开出来希望理解),但是注意我上面写的需要人物身上模型的材质shader都必须相同的才能完成合批

UCombineSkinnedMgr.cs

using UnityEngine;
using System.Collections.Generic;
using System.IO;

public class UCombineSkinnedMgr
{

    /// <summary>
    /// Only for merge materials.
    /// </summary>
	private const int COMBINE_TEXTURE_MAX = 256;
    private const string COMBINE_ALBEDOMAP_TEXTURE = "_AlbedoMap";
    //private const string COMBINE_NORMALMAP_TEXTURE = "_NormalMap";
    private const string COMBINE_MASKMAP_TEXTURE = "_MaskMap";

    /// <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, bool combine = false)
    {

        // 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

        // Below informations only are used for merge materilas(bool combine = true)
        //老UV坐标
        List<Vector2[]> oldUV = null;

        //新材质球
        Material newMaterial = null;

        //创建新Albedo贴图
        Texture2D newAlbedoMapTex = null;
        //创建新Normal贴图
        //Texture2D newNormalMapTex = null;
        //创建新Mask贴图
        Texture2D newMaskMapTex = null;

        // 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;
                    }
                }
            }
        }

        // merge materials
        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);

            newMaterial.SetFloat("_SideLightScale", 0);

            //#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, false);// Combine meshes
        r.bones = bones.ToArray();// Use new bones
        if (combine)
        {
            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(string job, string skeleton, string body, string clothes, string hair, string head, string pants, string shoes, string socks, bool combine = false)
    {
        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(Instance, meshes, combine);

        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("MaTa", "MaTa", "Body1", "Clothes1", "Hair1", "Head1", "Pants1", "Shoes1", "Socks1", true);
        //mine = Generatecharacter("MaTa", "MaTa", "Body2", "Clothes2", "Hair2", "Head2", "Pants2", "Shoes2", "Socks2", true);
        //mine = Generatecharacter("MaTa", "MaTa", "Body3", "Clothes3", "Hair3", "Head3", "Pants3", "Shoes3", "Socks3", true);
    }

    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            ChangeRole();
        }
    }
    int Index = 2;
    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(string job, string skeleton, string body, string clothes, string hair, string hand, string pants, string shoes, string socks, bool combine = false)
    {

        UCharacterController instance = new UCharacterController(job, skeleton, body, clothes, hair, hand, pants, shoes, socks, combine);
        characterDic.Add(characterIndex, instance);
        characterIndex++;

        return instance;
    }

    #endregion
}

总结

法线贴图的合并博主在研究一下在下一篇文章在解决一下,希望这篇文章对大家有帮助,感谢大家的支持关注和点赞。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/137482.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

SpringBoot之手写starter

SpringBoot之手写starter 在开始之前呢&#xff0c;我们需要了解一些概念 1、starter介绍 spring boot 在配置上相比spring要简单许多, 其核心在于spring-boot-starter, 在使用spring boot来搭建一个项目时, 只需要引入官方提供的starter, 就可以直接使用, 免去了各种配置。…

2390 高校实验室预约系统JSP【程序源码+文档+调试运行】

摘要 本文介绍了一个高校实验室预约系统的设计和实现。该系统包括管理员、教师和学生三种用户&#xff0c;具有基础数据管理、学生管理、教师管理、系统公告管理、实验室管理、实验室预约管理和系统管理等模块。通过数据库设计和界面设计&#xff0c;实现了用户友好的操作体验…

【Linux】虚拟机连不上外网 (ping www.baidu.com不通)

进入linux系统&#xff0c;打开终端&#xff0c;ping www.baidu.com 发现ping不通 首先我连接的是nat模式 查看是否连接上自己本机的网 切换root用户 使用 ifconfig 命令查看是eth0 还是 ens33 vi /etc/sysconfig/network-scripts/ifcfg-ens33 BOOTPROTOstatic ONBOOTyes …

模板——“C++”

各位CSDN的uu们你们好呀&#xff0c;今天&#xff0c;小雅兰的内容是C中的模板初阶的内容&#xff0c;下面&#xff0c;让我们进入C模板的世界吧&#xff01;&#xff01;&#xff01; 1. 泛型编程 2. 函数模板 3. 类模板 泛型编程 如何实现一个通用的交换函数呢&#xff1f;…

如何解决Windows电脑 Create folder error,Access is denied.

如何解决 Create folder error, Error: mkdir C:\Program Files\nodejs\21.1.0/: Access is denied. Waring: Name : http://npm.taobao.org/mirrors/node/v21.1.0/win-x64/node.exe Code : -2 Error : Create folder error, Error: mkdir C:\Program Files\nodejs\\21.1.0/…

MySQL主从环境搭建

MySQL主从环境搭建 主机MySQL配置 修改主机配置文件 vim /etc/my.cnf主要是在my.cnf配置文件中增加以下内容&#xff1a; #主服务器唯一ID server-id1 #启用二进制日志 log-binmysql-bin # 设置不要复制的数据库(可设置多个) binlog-ignore-dbmysql binlog-ignore-dbinform…

解决删除QT后Qt VS Tools中Qt Options中未删除的错误

在Qt VS Tools的Qt Options已经配置好Qt Versions后如果删除QT程序之后会出现Default Qt/Win version任然存在&#xff0c;这是如果再添加一个话就不能出现重名了&#xff0c;如果新建一个其他名字的话其实在vs中还是不能正常运行qt&#xff0c;会出现点击ui文件vs会无故重启或…

【Liunx】服务器解决 跨域问题

首先打开后端的站点 在站点设置内打开 "配置文件" 然后在 “server_name 本机ip ”下方添加跨域配置,添加成功后重启nginx即可 add_header Access-Control-Allow-Origin *; add_header Access-Control-Allow-Methods GET, POST, OPTIONS, PUT, DELETE; add_header A…

ZYNQ_project:ram_dual_port

伪双端口ram&#xff1a;写端口&#xff1a;clk_w,en_A,we_A,addr_A,din_A;读端口:clk_r,en_B,addr_B;dout_B. 设计读写模块&#xff0c;写入256个数据&#xff0c;再读出256个数据。 输入时钟100Mhz&#xff0c;输出时钟50Mhz。 多bit数据&#xff0c;高速时钟域到低速时钟…

05-Spring中Bean的生命周期

Bean的生命周期 生命周期就是对象从创建开始到最终销毁的整个过程 , Spring其实就是一个管理Bean对象的工厂,它负责对象的创建和销毁等 Bean生命周期的管理可以参考Spring的源码&#xff1a;AbstractAutowireCapableBeanFactory类的doCreateBean()方法 研究生命周期的意义&am…

消息中心常见解决方案分享

解决方案 1、问题2、设计3、流程 看了大部分的消息中心解决方案&#xff0c;发现大家的中心思想都大差不差&#xff0c;区别基本都是在符合自身业务场景的做了一些定制化处理。本文为我对消息中心基本骨架的知识梳理&#xff0c;亦在帮助大家对消息中心设计有一个基本的理解。 …

Centos, RockyLinux 常用软件安装汇总

一、基本指令&#xff1a; 命令作用clear清屏pwd显示当前路径cat / more显示文本文档uname -a查看当前版本hostnamectl查看当前版本cat /etc/redhat-release查看当前版本free查看剩余内存df -h[查看磁盘剩余空间]du -sh 查看文件夹名"dir"占用的空间lsof -i:8080查看…

进亦忧,退亦忧,Github Copilot 集成进入 Visual Studio 带来的思考

开篇想到《岳阳楼记》的结尾&#xff1a; 不以物喜&#xff0c;不以己悲&#xff1b;居庙堂之高则忧其民&#xff1b;处江湖之远则忧其君。是进亦忧&#xff0c;退亦忧。然则何时而乐耶&#xff1f;其必曰&#xff1a;“先天下之忧而忧&#xff0c;后天下之乐而乐”乎。未来30…

mini-vue 的设计

mini-vue 的设计 mini-vue 使用流程与结果预览&#xff1a; <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><meta http-equiv"X-UA-Compatible" content"IEedge" /><meta name&qu…

std::any

一、简介 std::any 可以储存任何可拷贝构造和可销毁的类型的对象。 struct test {test(int a,int b){} };int main(int argc, char *argv[]) {std::any a 1;qDebug() << a.type().name();a 3.14;qDebug() << a.type().name();a true;qDebug() << a.type…

LeetCode 189.轮转数组(三种方法解决)

文章目录 题目暴力求解空间换时间三段逆置总结 题目 LeetCode 189.轮转数组 给定一个整数数组 nums&#xff0c;将数组中的元素向右轮转 k 个位置&#xff0c;其中 k 是非负数。 输入: nums [1,2,3,4,5,6,7], k 3 输出: [5,6,7,1,2,3,4] 解释: 向右轮转 1 步: [7,1,2,3,4,5…

2023 年最新企业微信官方会话机器人开发详细教程(更新中)

目标是开发一个简易机器人&#xff0c;能接收消息并作出回复。 获取企业 ID 企业信息页面链接地址&#xff1a;https://work.weixin.qq.com/wework_admin/frame#profile 自建企业微信机器人 配置机器人应用详情 功能配置 接收消息服务器配置 配置消息服务器配置 配置环境变量…

数据结构与算法【数组】Java实现

数组是一组元素组成的数据结构&#xff0c;元素类型必须相同&#xff0c;其次&#xff0c;数组内元素是连续存储的&#xff0c;因此数组中元素地址可以通过索引计算出来。 空间占用 在Java中&#xff0c;数组本质上也是一个对象&#xff0c;因此也存在对象头信息。那么数组的…

04-详解SpringBoot自动装配的原理,依赖属性配置的实现,源码分析

自动装配原理 依赖属性配置 提供Bean用来封装配置文件中对应属性的值 Data public class Cat {private String name;private Integer age; }Data public class Mouse {private String name;private Integer age; }cartoon:cat:name: "图多盖洛"age: 5mouse:name: …

若依Linux与Docker集群部署

若依Linux集群部署 1. 若依2.MYSQL Linux环境安装2.1 MYSQL数据库部署和安装2.2 解压MYSQL安装包2.3 创建MYSQL⽤户和⽤户组2.4 修改MYSQL⽬录的归属⽤户2.5 准备MYSQL的配置⽂件2.6 正式开始安装MYSQL2.7 复制启动脚本到资源⽬录2.8 设置MYSQL系统服务并开启⾃启2.9 启动MYSQL…