Unity-合并模型贴图

需求

项目需要做个工具,用于合并模型贴图,以此可以省掉贴图尺寸为了补足2的次方而多出来的多余空白像素。

过程

  1. 平铺式编码,相当于一个 Demo,把几个没把握的主要问题解决,得出粗略结果
  2. 再和需求方确认细节,以此做相应调整优化

经验总结

  1. 做性能不敏感的编辑器工具,采用先收集数据,再统一处理的方式,会更加灵活,尽管重复遍历一次(一次收集,一次处理)
  2. 需求还是不能全信。本来确认过所有资源名字都是一一对应的,因此不需要收集引用的对应关系,直接按资源名字,改个后缀名就行。后来有了共用材质的模型,这样必然有一个材质和模型的名字不一样,好在比较好改。其实本来也是应该用引用来做比较通用,属于是“合理”偷懒了

Unity快速入门之四 - Unity模型动画相关_翕翕堂的博客-CSDN博客_unity 模型动画

主要问题

  • fbx 文件无法修改资源引用(贴图、材质、网格)
    把 fbx 实例化,再把这个实例另存为新预制,这时就可以修改模型的资源引用了
  • 如何合并贴图?
    借助 TexturePacker,项目中也一直是用这个工具来处理 UI 图集
  • 每个网格的 UV 要根据合并的图重新设置对应坐标
    TexturePacker 合并完成后会有一份配置,记录了小图在大图中的坐标,我们以此来修改 UV 坐标
  • 当 fbx 重新导出时,不能改变原有的资源引用,否则每次都要全量热更
    替换资源时,不要用 Unity 的函数,它的替换会连同 meta 一起删除再重新创建,这样资源 id 就会发生变化,引用也就变了。所以直接用系统的方法替换文件,不改变 meta

FBX 文件是什么


AutoDesk 提供的模型文件格式,以二进制形式存储。包含各种模型信息,比如模型面数、三角形数、顶点坐标、骨骼动画信息、模型版本号信息等等。

深入理解加载FBX模型文件

Unity 中组成模型的部件有哪些?


MeshFilter + MeshRenderer(不带蒙皮),MeshFilter负责网格绘制(挂载 mesh),MeshRenderer负责材质表现(挂载材质)
SkinnedMeshRenderer(带蒙皮),包含了网格和材质
从图形学认识Unity中的Mesh

分离 FBX


因为需要对网格、贴图和材质进行自定义,所以 fbx 源文件不能直接使用,需要把资源都拷贝一份,再对拷贝出来的资源进行修改。
避免篇幅太长,代码删除了一些检查性判断。

创建 fbx 预制

// 创建单个fbx预制
public static void CreateOneFbxPrefab(Object makingFbxObj)
{
    string makingFbxPath = AssetDatabase.GetAssetPath(makingFbxObj);
    string makingFbxDir = Path.GetDirectoryName(makingFbxPath);
    string newFbxPrefabDir = makingFbxDir.ReplacePath("art/making", "Data");

    // 创建目录
    if (!Directory.Exists(newFbxPrefabDir))
    {
        FileHelper.CreateDirectory(newFbxPrefabDir);
        AssetDatabase.ImportAsset(newFbxPrefabDir);
    }

    // 创建 fbx 预制
    string fbxName = makingFbxObj.name;
    string newPrefabPath = newFbxPrefabDir + "/" + fbxName + ".prefab";
    GameObject newGo = AssetDatabase.LoadAssetAtPath<GameObject>(newPrefabPath);
    if(newGo == null)
    {
        newGo = new GameObject(fbxName);
        // fbx 作为子节点
        GameObject newFbx = Object.Instantiate(makingFbxObj) as GameObject;
        newFbx.name = fbxName;
        newFbx.transform.SetParent(newGo.transform);
        newFbx.transform.localPosition = Vector3.zero;
        newFbx.transform.eulerAngles = new Vector3(0, 0, 0);
        PrefabUtility.SaveAsPrefabAssetAndConnect(newGo, newPrefabPath, InteractionMode.AutomatedAction);
        GameObject.DestroyImmediate(newGo, true);
    }
    else
    {
        // Debug.LogFormat("预制已经存在,不另外创建 {0}", makingFbxPath);
    }
 }

创建网格、贴图和材质

public static void SplitOneFbxRes(Object makingFbxObj)
{
    string makingFbxPath = AssetDatabase.GetAssetPath(makingFbxObj);
    string makingFbxDir = Path.GetDirectoryName(makingFbxPath);
    string newFbxResDir = makingFbxDir.ReplacePath("making/", "");

    // 创建目录
    if (!Directory.Exists(newFbxResDir))
    {
        FileHelper.CreateDirectory(newFbxResDir);
        AssetDatabase.ImportAsset(newFbxResDir);
    }

    // 重新导入一下fbx,防止引用没有更新
    AssetDatabase.ImportAsset(makingFbxPath);

    // 把贴图、材质、网格拷贝到新目录
    GameObject makingFbx = AssetDatabase.LoadAssetAtPath<GameObject>(makingFbxPath);
    MeshRenderer[] makingFbxMrs = makingFbx.GetComponentsInChildren<MeshRenderer>();
    foreach (var makingMr in makingFbxMrs)
    {
        Material makingMat = makingMr.sharedMaterial;
        Texture makingTex = makingMat.mainTexture;
        
        // 拷贝贴图
        string makingTexPath = AssetDatabase.GetAssetPath(makingTex);
        string newTexPath = newFbxResDir + "/" + Path.GetFileName(makingTexPath);
        FileUtil.ReplaceFile(makingTexPath, newTexPath);

        // 拷贝材质
        string newMatPath = newFbxResDir + "/" + makingMat.name + ".mat";
        Material newMat = AssetDatabase.LoadAssetAtPath<Material>(newMatPath);
        if(newMat == null)
        {
            newMat = Object.Instantiate(makingMat) as Material;
            AssetDatabase.CreateAsset(newMat, newMatPath);

            // 只有新材质才需要更新贴图引用
            AssetDatabase.ImportAsset(newTexPath);
            Texture newTex = AssetDatabase.LoadAssetAtPath<Texture>(newTexPath);
            newMat.mainTexture = newTex;
        }
        else
        {
            // 如果已经存在材质,则不处理材质(贴图在上一步直接就替换了)
        }

        MeshFilter makingMf = makingMr.GetComponent<MeshFilter>();
        Mesh makingMesh = makingMf.sharedMesh;
        
        // 拷贝网格
        string makingMeshPath = AssetDatabase.GetAssetPath(makingMesh);
        string newMeshPath = newFbxResDir + "/" + makingMesh.name + ".mesh";
        Mesh newMesh = AssetDatabase.LoadAssetAtPath<Mesh>(newMeshPath);
        if(newMesh == null)
        {
            newMesh = Object.Instantiate(makingMesh) as Mesh;
            newMesh.SetNormals(new List<Vector3>(0));
            newMesh.SetTangents(new List<Vector4>(0));
            // 这个方法会先删除旧资源,包括 meta,再创建,会导致原来的引用丢失
            AssetDatabase.CreateAsset(newMesh, newMeshPath);
            Debug.LogFormat("创建网格:{0}", newMeshPath);
        }
        else
        {
            newMesh.indexFormat = makingMesh.indexFormat;
            newMesh.SetTriangles(makingMesh.triangles, 0);
            newMesh.SetVertices(makingMesh.vertices);
            newMesh.SetUVs(0, makingMesh.uv);
            newMesh.SetUVs(1, makingMesh.uv2);
            newMesh.SetIndices(makingMesh.GetIndices(0), makingMesh.GetTopology(0), 0);
            newMesh.SetColors(makingMesh.colors);
            newMesh.SetNormals(new List<Vector3>(0));
            newMesh.SetTangents(new List<Vector4>(0));
            Debug.LogFormat("更新网格:{0}", newMeshPath);
        }
    }
}

贴图合并


SpriteSheet精灵动画引擎

// 合并纹理
public static void CombineFbxDirRes(Object makingFbxDirObj)
{
    string makingFbxDir = AssetDatabase.GetAssetPath(makingFbxDirObj);
    string newFbxResDir = makingFbxDir.ReplacePath("making/", "");

    // 创建目录
    if (!Directory.Exists(newFbxResDir))
    {
        FileHelper.CreateDirectory(newFbxResDir);
        AssetDatabase.ImportAsset(newFbxResDir);
    }

    // 合并贴图
    DirectoryInfo resDirInfo = new DirectoryInfo(makingFbxDir);
    string resDirName = resDirInfo.Name;
    string tpName = resDirName;
    string tpDir = "Assets/TP/" + tpName;
    
    // 借助 Texturepacker 合并贴图,这里自己需要自己封装方法处理
    AtlasEditorHelper.BuildMakeAtlas(tpName, makingFbxDir, tpDir, AtlasEditorHelper.TrimMode.Trim);
    // 新贴图放到art/scene
    string tpTexPath = string.Format("{0}/{1}.png", tpDir, tpName);
    string newTexPath = string.Format("{0}/{1}.png", newFbxResDir, tpName);
    FileUtil.ReplaceFile(tpTexPath, newTexPath);
    AssetDatabase.ImportAsset(newTexPath);
    
    // 通过 TexturePacker 的输出文件获得新纹理信息
    Texture newTex = AssetDatabase.LoadAssetAtPath<Texture>(newTexPath);
    string tpTxtPath = string.Format("{0}/{1}.txt", tpDir, tpName);
    string tpText = FileHelper.ReadTextFromFile(tpTxtPath);
    List<TexturePacker.PackedFrame> frames = TexturePacker.ProcessToFrames(tpText);
    // 新图大小
    System.Collections.Hashtable hashtable = tpText.hashtableFromJson();
    TexturePacker.MetaData metaData = new TexturePacker.MetaData((System.Collections.Hashtable)hashtable["meta"]);
    Vector2 totalSize = metaData.size;

    // 拷贝所有mesh到art/scene
    string[] makingMeshGuids = AssetDatabase.FindAssets("t:Mesh", new string[] { makingFbxDir });
    Dictionary<string, Mesh> meshName2Mesh = new Dictionary<string, Mesh>();
    foreach (var makingMeshGuid in makingMeshGuids)
    {
        string makingMeshPath = AssetDatabase.GUIDToAssetPath(makingMeshGuid);
        Mesh makingMesh = AssetDatabase.LoadAssetAtPath<Mesh>(makingMeshPath);

        string newMeshPath = newFbxResDir + "/" + makingMesh.name + ".mesh";
        Mesh newMesh = Object.Instantiate(makingMesh) as Mesh;
        newMesh.SetTangents(new List<Vector4>(0));
        newMesh.SetNormals(new List<Vector3>(0));
        AssetDatabase.CreateAsset(newMesh, newMeshPath);

        meshName2Mesh[makingMesh.name] = newMesh;
    }

    // 收集mesh和纹理的对应关系(多个mesh共用一个纹理)
    Dictionary<string, string> meshName2TexName = new Dictionary<string, string>();
    string[] makingFbxGuids = AssetDatabase.FindAssets("t:Model", new string[] { makingFbxDir });
    foreach (var makingFbxGuid in makingFbxGuids)
    {
        string makingFbxPath = AssetDatabase.GUIDToAssetPath(makingFbxGuid);
        GameObject fbx = AssetDatabase.LoadAssetAtPath<GameObject>(makingFbxPath);
        // 取 mesh
        MeshFilter mf = fbx.GetComponent<MeshFilter>();
        MeshRenderer mr = fbx.GetComponent<MeshRenderer>();
        meshName2TexName[mf.sharedMesh.name] = mr.sharedMaterial.mainTexture.name;
    }

    // 需要遍历 mesh,再根据纹理名字找到纹理数据,所以这里映射一下
    Dictionary<string, TexturePacker.PackedFrame> texName2PackFrame = new Dictionary<string, TexturePacker.PackedFrame>();
    foreach (TexturePacker.PackedFrame pm in frames)
    {
        texName2PackFrame[Path.GetFileNameWithoutExtension(pm.name)] = pm;
    }

    // 修改mesh的UV
    foreach (KeyValuePair<string, string> p in meshName2TexName)
    {
        string meshName = p.Key;
        string texName = p.Value;
        TexturePacker.PackedFrame pm = texName2PackFrame[texName];

        // tp左上角为原点,贴图uv左下角为原点
        float beginX = pm.frame.x - pm.spriteSourceSize.x;
        float trimYTop = pm.spriteSourceSize.y;
        // float trimYBottom = fm.sourceSize.y - trimYTop - fm.spriteSourceSize.height;
        // float beginY = totalSize.y - (fm.frame.y + fm.frame.height + trimYBottom);
        float beginY = totalSize.y - (pm.frame.y + pm.sourceSize.y - trimYTop);

        Mesh newMesh;
        if(meshName2Mesh.TryGetValue(meshName, out newMesh))
        {
            Vector2[] oldUVs = newMesh.uv;
            Vector2[] newUVs = new Vector2[oldUVs.Length];
            for (int i = 0; i < oldUVs.Length; i++)
            {
                float oldUvX = oldUVs[i].x;
                float oldUvY = oldUVs[i].y;
                float newX = (beginX + (oldUvX * pm.sourceSize.x)) / totalSize.x;
                float newY = (beginY + (oldUvY * pm.sourceSize.y)) / totalSize.y;
                newUVs[i] = new Vector2(newX, newY);
            }
            newMesh.uv = newUVs;
            EditorUtility.SetDirty(newMesh);
            // Debug.LogFormat("修改mesh的uv:{0}", newMesh.name);
        }
        else
        {
            Debug.LogErrorFormat("图片名没有对应的网格:{0}", pm.name);
            continue;
        }
    }

    // 创建新合并材质
    string newMatPath = newFbxResDir + "/" + tpName + ".mat";
    Material combinedMat = AssetDatabase.LoadAssetAtPath<Material>(newMatPath);
    if(combinedMat == null)
    {
        Shader shader = AssetDatabase.LoadAssetAtPath<Shader>("Assets/Shader/Builtin/Unlit-Normal.shader");
        combinedMat = new Material(shader);
        combinedMat.name = tpName;
        AssetDatabase.CreateAsset(combinedMat, newMatPath);
    }
    combinedMat.mainTexture = newTex;

    AssetDatabase.SaveAssets();
    AssetDatabase.Refresh();
}

替换合并的贴图、材质和网格


把原来拷贝出来的模型贴图、材质和网格,替换成合并后的。

// 替换一个模型预制的材质和网格(已有资源路径一致则不替换)
public static void ReplaceOneMaterialAndMesh(Object fbxPrefabObj, ResCollection resCollection = null)
{
    GameObject fbxPrefab = fbxPrefabObj as GameObject;
    string fbxPrefabPath = AssetDatabase.GetAssetPath(fbxPrefabObj);
    if(resCollection == null)
    {
        // Assets/Data/scene/home/furniture/50128.prefab
        // Assets/Art/scene/home/furniture
        string fbxPrefabDir = Path.GetDirectoryName(fbxPrefabPath);
        string fbxResDir = fbxPrefabDir.ReplacePath("Data", "Art");
        resCollection = new ResCollection();
        resCollection.Collect(new string[]{fbxResDir});
    }

    bool isChanged = false;
    // 检查网格
    MeshFilter[] mfs = fbxPrefab.GetComponentsInChildren<MeshFilter>();
    Dictionary<string, string> meshName2MeshPath = resCollection.meshName2MeshPath;
    foreach (MeshFilter mf in mfs)
    {
        Mesh sharedMesh = mf.sharedMesh;
        string meshName = sharedMesh.name;
        string meshPath = AssetDatabase.GetAssetPath(sharedMesh);
        string collectedMeshPath;
        if(meshName2MeshPath.TryGetValue(meshName, out collectedMeshPath))
        {
            // mesh存在,但资源不是在指定目录,则更换为指定目录的
            if(meshPath.StandardPath() != collectedMeshPath.StandardPath())
            {
                mf.sharedMesh = AssetDatabase.LoadAssetAtPath<Mesh>(collectedMeshPath);
                Debug.LogFormat("更新网格:{0}\n节点:{1}\n旧路径:{2}\n新路径:{3}", fbxPrefab.name, mf.name, meshPath, collectedMeshPath);
                isChanged = true;
            }
        }
        else
        {
            // Debug.LogFormat("没有同名网格:{0}\n节点:{1}\n旧路径:{2}", fbxPrefab.name, mf.name, meshPath);
        }
    }

    // 检查材质
    MeshRenderer[] mrs = fbxPrefab.GetComponentsInChildren<MeshRenderer>();
    Dictionary<string, string> matName2MatPath = resCollection.matName2MatPath;
    foreach (var mr in mrs)
    {
        Material sharedMat = mr.sharedMaterial;
        if(sharedMat == null)
        {
            Debug.LogErrorFormat("MeshRenderer上没有material:{0}\n {1}", fbxPrefab.name, mr.name);
            continue;
        }

        string matName = sharedMat.name;
        string matPath = AssetDatabase.GetAssetPath(sharedMat);
        string collectedMatPath;
        if(matName2MatPath.TryGetValue(matName, out collectedMatPath))
        {
            // material存在,但资源不是在指定目录,则更换为指定目录的
            if(matPath != collectedMatPath)
            {
                mr.sharedMaterial = AssetDatabase.LoadAssetAtPath<Material>(collectedMatPath);
                Debug.LogFormat("替换材质:{0}\n节点:{1}\n旧路径:{2}\n新路径:{3}", fbxPrefab.name, mr.name, matPath, collectedMatPath);
                isChanged = true;
            }
        }
        else
        {
            // 没有同名材质
            // 有合并材质,且当且材质名和预制名一样,则替换为合并材质(第一次创建的时候)
            if(resCollection.combinedMat != null && fbxPrefab.name == matName)
            {
                mr.sharedMaterial = resCollection.combinedMat;
                Debug.LogFormat("替换合并材质:{0}\n节点:{1}\n旧路径:{2}\n新路径:{3}", fbxPrefab.name, mr.name, matPath, collectedMatPath);
                isChanged = true;
            }
            else
            {
                // Debug.LogFormat("没有同名材质:{0}\n节点:{1}\n旧路径:{2}", fbxPrefab.name, mr.name, matPath);
            }
        }
    }

    if(isChanged)
    {
        GameObject fbxPrefabGo = GameObject.Instantiate(fbxPrefab);
        PrefabUtility.SaveAsPrefabAssetAndConnect(fbxPrefabGo, fbxPrefabPath, InteractionMode.AutomatedAction);
        GameObject.DestroyImmediate(fbxPrefabGo, true);
    }
}

其他参考


【Unity3D】 合并mesh那些事 CombineMeshes(二)
骨骼动画程序原理介绍
Skinned Mesh原理解析和一个最简单的实现示例
Unity Shader:Unity网格(1)---顶点,三角形朝向,法线,uv,以及双面渲染三角形

posted @ 2022-10-23 13:00  尼克多摩雄  阅读(891)  评论(0编辑  收藏  举报