3D模型人物换装系统三(优化合批处理,提取合批配置可,封装)

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);

规划以及封装

这里我大概讲一下我之前脚本设计的缺陷,以及我想修改成的样子,在文章最后面我会把我修改完成的脚本和资源都放过来,如果大家在后面使用脚本的过程中遇到问题可以在询问我。

缺陷

  1. 原来合并设计的是太过于死板,材质球的属性是直接提取出来的并没有动态去做,也就是说我这里只能手动修改,如果换一个四个贴图的材质还需要修改下面的代码,这样太不方便,需要封装一下这里
    private const string COMBINE_ALBEDOMAP_TEXTURE = "_AlbedoMap";
    private const string COMBINE_NORMALMAP_TEXTURE = "_NormalMap";
    private const string COMBINE_MASKMAP_TEXTURE = "_MaskMap";
  1. 创建贴图的大小也是固定死的现在,这里也是有缺陷,没法动态的调整
   /// <summary>
    /// Only for merge materials.
    /// </summary>
	private const int COMBINE_TEXTURE_MAX = 256;
  1. 合并这里或多或少有点缺陷,但是不算是有问题,这里传入的如果是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];
        	}
   		}
  1. Shader也是写死的,不是动态的
 Shader tmpShader = Shader.Find("E3D/Actor/PBR-MaskRG-Normal");

修改整理

  1. 创建一个配置类,当创建模型的时候根据自己需要传入定制化配置文件,这样可以动态调整创建贴图的大小、动态调整合并贴图的数量、动态修改想要合并成的材质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; }
}
  1. 获取需要合并的模型的所有的所有Shader,如果是只有一个Shader则你combine为true时会进行合批处理,如果Shader数量多了哪怕你combine为true则也不进行合批。
  2. 加入了一个材质回调,这样在调用的时候可以对材质球进行设置

整合

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
}

完整资源

总结

人物换装的优化这篇文章后面应该不会在出了,预计得优化大概就是这样,如果有别的好点子可以私信我交流一下。希望这篇文章能给大家带来帮助,感谢大家的支持和关注,感谢点赞。

猜你喜欢

转载自blog.csdn.net/qq_42194657/article/details/134392776
今日推荐