using System.Collections.Generic; using System.IO; using System.Linq; using Art.temp.Editor.PrefabTool; using artcfg.ArtDesign; using Framework; using Sirenix.Utilities; using UnityEditor; using UnityEngine; namespace Art.temp.Editor.AnimationTools { public static class AnimHelper { public static GameObject FindDisplayPrefab(string nameId) { switch (nameId[0]) { case 'A': { var player = CharacterFactory.CreatePlayer(nameId); return player.DisplayPrefab; } case 'E': { var enemy = CharacterFactory.CreateEmeny(nameId); return enemy.DisplayPrefab; } case 'N': { var npc = CharacterFactory.CreateNpc(nameId); return npc.DisplayPrefab; } default: return null; } } // filter = "name t:xx l:xx" Name Labels Types public static IEnumerable FindAssetsByFolders(string filter, string[] folders) where T : Object { // FindAssets 能找子文件夹 string[] guids = AssetDatabase.FindAssets(filter, folders); IEnumerable paths = guids.Select(AssetDatabase.GUIDToAssetPath); return paths.Select(AssetDatabase.LoadAssetAtPath); } /// /// 合并 prefab 中的 mesh, 要求只有一个材质,并且合并后只有一个 material /// /// cloned prefab /// shader 贴图字段 [_MainTex, _MaskTex] private static void CombineSkinnedMesh(GameObject go, string[] texNames) { var parts = go.GetComponentsInChildren(true); if (parts.Length < 2) return; // 获取蒙皮骨骼 var bones = parts.SelectMany(p => p.bones).ToArray(); // 合并贴图 var texGroups = texNames.Select(t => parts.Select(p => p.sharedMaterial.GetTexture(t) as Texture2D)) .ToArray(); var combineTextures = texGroups.Select(g => { var tex = new Texture2D(4, 4, TextureFormat.ARGB4444, true); tex.PackTextures(g.ToArray(), 0, 2048); return tex; }); // 合并 mesh uv var uvGroups = parts.Select(p => p.sharedMesh.uv).ToArray(); var rects = new Texture2D(4, 4, TextureFormat.ARGB4444, true).PackTextures(texGroups.ElementAt(0).ToArray(), 0, 2048); var newUvs = uvGroups.Zip(rects, (uv, rect) => { for (int i = 0; i < uv.Length; i++) { uv[i].x = Mathf.Lerp(rect.xMin, rect.xMax, uv[i].x); uv[i].y = Mathf.Lerp(rect.yMin, rect.yMax, uv[i].y); } return uv; }).SelectMany(uv => uv).ToArray(); // copy and combine mesh var combineInstances = parts.Select(p => new CombineInstance() { mesh = DeepCopyMesh(p.sharedMesh) }); Mesh newMesh = new(); newMesh.CombineMeshes(combineInstances.ToArray(), true, false); // set texture var mat = Object.Instantiate(parts[0].sharedMaterial); var list = texNames.Zip(combineTextures, (name, tex) => { mat.SetTexture(name, tex); return 0; }).ToArray(); // create combine GameObject combinedMesh = new GameObject("combined_mesh"); combinedMesh.transform.SetParent(go.transform.GetChild(0)); var smr = combinedMesh.AddComponent(); smr.sharedMesh = newMesh; smr.sharedMesh.uv = newUvs.ToArray(); smr.bones = bones; smr.rootBone = go.transform.GetChild(0).Find("Root"); smr.sharedMaterial = mat; parts.ForEach(p => Object.DestroyImmediate(p.gameObject)); } public static void CombineSkinSaveAssets(GameObject go, string[] texNames, string parentPath) { string nameId = go.transform.GetChild(0).name; AnimHelper.CombineSkinnedMesh(go, texNames); var smr = go.transform.GetChild(0).GetComponentInChildren(); var mat = smr.sharedMaterial; // texture foreach (var name in texNames) { var tex = mat.GetTexture(name); var texPath = $"{parentPath}/{name}.png"; var newTex = SaveAndReloadTexture2D(tex as Texture2D, texPath); mat.SetTexture(name, newTex); } // material var matSavePath = $"{parentPath}/mat_{nameId}.mat"; AssetDatabase.CreateAsset(mat, matSavePath); // mesh var mesh = smr.sharedMesh; var meshSavePath = $"{parentPath}/mesh_{nameId}.asset"; AssetDatabase.CreateAsset(mesh, meshSavePath); } private static Texture2D SaveAndReloadTexture2D(Texture2D t2d, string savePath) { var bytes = t2d.EncodeToPNG(); File.WriteAllBytes(savePath, bytes); AssetDatabase.Refresh(); var newT2d = AssetDatabase.LoadAssetAtPath(savePath); return newT2d; } public static string[] PreSetAll(GameObject prefab) { var allSkin = prefab.GetComponentsInChildren(); string[] allTexNames = new string[] { "_MainTex", "_MaskTex" }; foreach (SkinnedMeshRenderer skin in allSkin) { var material = skin.sharedMaterial; if (material.shader.name != "NLD_URP/NLD_Charactor") { // 修改为NLD_URP/NLD_Charactor material.shader = Shader.Find("NLD_URP/NLD_Charactor"); } // 判断所有的贴图是否已开启read/write foreach (var texName in allTexNames) { var tex = material.GetTexture(texName); if (!tex) continue; var texPath = AssetDatabase.GetAssetPath(tex); var importer = AssetImporter.GetAtPath(texPath) as TextureImporter; if (!importer) continue; importer.isReadable = true; importer.SaveAndReimport(); } } return allTexNames; } private static Mesh DeepCopyMesh(Mesh originalMesh) { Mesh copiedMesh = new Mesh { vertices = (Vector3[])originalMesh.vertices.Clone(), triangles = (int[])originalMesh.triangles.Clone(), uv = (Vector2[])originalMesh.uv.Clone(), normals = (Vector3[])originalMesh.normals.Clone(), tangents = (Vector4[])originalMesh.tangents.Clone(), colors = (Color[])originalMesh.colors.Clone(), boneWeights = (BoneWeight[])originalMesh.boneWeights.Clone(), bindposes = (Matrix4x4[])originalMesh.bindposes.Clone() }; return copiedMesh; } public static void PreSetModel(GameObject model) { var objPath = AssetDatabase.GetAssetPath(model); var modelImporter = AssetImporter.GetAtPath(objPath) as ModelImporter; if (!modelImporter) return; modelImporter.avatarSetup = ModelImporterAvatarSetup.CreateFromThisModel; AssetDatabase.Refresh(); modelImporter.SaveAndReimport(); } public static void SetFaceMaterial(GameObject model) { const string browTexPath = "Assets/Art_Out/Manual/Emoji/Tex/T_emoji_eyebows_{0}_01.png"; const string browMaskPath = "Assets/Art_Out/Manual/Emoji/Tex/T_emoji_eyebows_{0}_01_mask.png"; const string eyeTexPath = "Assets/Art_Out/Manual/Emoji/Tex/T_emoji_eyes_0{0}.png"; const string eyeMaskPath = "Assets/Art_Out/Manual/Emoji/Tex/T_emoji_eyes_0{0}_mask.png"; const string mouthPath = "Assets/Art_Out/Manual/Emoji/Tex/T_emoji_mouth_0{0}.png"; string nameId = model.name; DataPerson dataPerson = ArtTableManager.instance.tables.PersonConfig.DataList.FirstOrDefault(p => p.NameId == nameId); if (dataPerson == null) { Debug.LogWarning($"{nameId} not found face config"); return; } GameObject emoji = model.transform .Find("Root/Bip001/Bip001 Spine/Bip001 Spine1/Bip001 Neck/Bip001 Head/G_emoji").gameObject; Material[] mats = emoji.GetComponent().sharedMaterials; if (!string.IsNullOrEmpty(dataPerson.BowsColor)) { ColorUtility.TryParseHtmlString(dataPerson.BowsColor, out Color browColor); mats[0].SetColor("_MainColor", browColor); } if (!string.IsNullOrEmpty(dataPerson.EyesColor)) { ColorUtility.TryParseHtmlString(dataPerson.EyesColor, out Color eyeColor); mats[1].SetColor("_MainColor", eyeColor); } Texture2D browTex = AssetDatabase.LoadAssetAtPath(string.Format(browTexPath, dataPerson.BowsType)); Texture2D browMask = AssetDatabase.LoadAssetAtPath(string.Format(browMaskPath, dataPerson.BowsType)); mats[0].SetTexture("_MainTex", browTex); mats[0].SetTexture("_MaskTex", browMask); Texture2D eyeTex = AssetDatabase.LoadAssetAtPath(string.Format(eyeTexPath, dataPerson.EyesType)); Texture2D eyeMask = AssetDatabase.LoadAssetAtPath(string.Format(eyeMaskPath, dataPerson.EyesType)); mats[1].SetTexture("_MainTex", eyeTex); mats[1].SetTexture("_MaskTex", eyeMask); Texture2D mouthTex = AssetDatabase.LoadAssetAtPath(string.Format(mouthPath, dataPerson.MouseType)); mats[2].SetTexture("_MainTex", mouthTex); } public static void RenameAnim(GameObject go) { string goPath = AssetDatabase.GetAssetPath(go); ModelImporter importer = AssetImporter.GetAtPath(goPath) as ModelImporter; importer.avatarSetup = ModelImporterAvatarSetup.CreateFromThisModel; ModelImporterClipAnimation clip = importer.defaultClipAnimations[0]; clip.name = go.name; if (clip.name.EndsWith("idle") || clip.name.EndsWith("move")) clip.loopTime = true; importer.clipAnimations = new[] { clip }; importer.SaveAndReimport(); } public static void CopyAnimToCompressed(GameObject obj) { const string tempFolder = "Assets/Art/temp/Animation"; const string destFilePath = "Assets/Art/Animations/compressed/{0}"; string[] fileNames = CopyAnim(obj, tempFolder, true); foreach (string f in fileNames) { string animFile = string.Format(destFilePath, Path.GetFileName(f)); File.Copy(f, animFile, true); Debug.Log(animFile); } AssetDatabase.Refresh(); } private static string[] CopyAnim(GameObject fbx, string destFolder, bool toCompressed = false) { string fbxPath = AssetDatabase.GetAssetPath(fbx); var assets = AssetDatabase.LoadAllAssetRepresentationsAtPath(fbxPath); var animPaths = new List(); foreach (Object asset in assets) { if (asset is not AnimationClip clip) continue; AnimOptimize(clip); AnimationClip newClip = new(); EditorUtility.CopySerialized(clip, newClip); string str = clip.name + ".anim"; string compressed = toCompressed ? "compressed_" + str : str; string newPath = Path.Combine(destFolder, compressed); AssetDatabase.CreateAsset(newClip, newPath); animPaths.Add(newPath); } return animPaths.ToArray(); } public static void AnimOptimize(AnimationClip clip) { var curveBindings = AnimationUtility.GetCurveBindings(clip); for (int ii = 0; ii < curveBindings.Length; ++ii) { AnimationCurve curve = AnimationUtility.GetEditorCurve(clip, curveBindings[ii]); // 删除移动帧(除了Bip001)和缩放帧 string propName = curveBindings[ii].propertyName.ToLower(); string nodeName = Path.GetFileName(curveBindings[ii].path); /*if (propName.Contains("position") && nodeName.StartsWith("Bip001 ")) { curve = null; //有空格,排除了“Bip001” }*/ if (propName.Contains("scale") && !nodeName.Contains("Grip_point01")) { curve = null; } if (nodeName.StartsWith("Fire")) { curve = null; } // 压缩浮点数 if (curve is { keys: not null }) { var keyFrames = curve.keys; for (int i = 0; i < keyFrames.Length; i++) { Keyframe key = keyFrames[i]; key.value = float.Parse(key.value.ToString("f4")); key.inTangent = float.Parse(key.inTangent.ToString("f4")); key.outTangent = float.Parse(key.outTangent.ToString("f4")); keyFrames[i] = key; } curve.keys = keyFrames; } // curve 为 null, 则删除曲线 AnimationUtility.SetEditorCurve(clip, curveBindings[ii], curve); } } } }