using System.Collections.Generic; using System.IO; using Sirenix.OdinInspector; using UnityEngine; #if UNITY_EDITOR using UnityEditor; using UnityEditor.SceneManagement; #endif namespace DanieleMarotta.RiversongCodeShowcase { public class MeshBaker : MonoBehaviour { [SerializeField] private string _layer = "Default"; [SerializeField] private List _doNotBake = new(); #if UNITY_EDITOR [Button(ButtonSizes.Large)] [GUIColor("cyan")] private void BakeMesh() { var excluded = GetExcluded(); var combineInstancesByMaterial = CollectCombineInstances(excluded); if (combineInstancesByMaterial.ValueCount <= 0) return; var bakedName = $"{gameObject.name.Replace("_Authoring", string.Empty)}_Baked"; var bakedMesh = CreateBakedMesh(bakedName, combineInstancesByMaterial); var parentFolder = GetBakeOutputFolder(out var isPrefab); AssetDatabase.CreateAsset(bakedMesh, $"{parentFolder}/{bakedName}.asset"); if (isPrefab) SaveBakedPrefab(parentFolder, bakedName, bakedMesh, combineInstancesByMaterial); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); } private ListMultiDictionary CollectCombineInstances(HashSet excluded) { var meshRenderers = GetComponentsInChildren(); var combineInstancesByMaterial = new ListMultiDictionary(); foreach (var childRenderer in meshRenderers) { if (excluded.Contains(childRenderer.transform)) continue; AddRendererCombineInstances(childRenderer, combineInstancesByMaterial); } return combineInstancesByMaterial; } private void AddRendererCombineInstances(MeshRenderer childRenderer, ListMultiDictionary combineInstancesByMaterial) { var meshFilter = childRenderer.GetComponent(); if (!meshFilter || !meshFilter.sharedMesh) return; var mesh = meshFilter.sharedMesh; var materials = childRenderer.sharedMaterials; var subMeshCount = Mathf.Min(mesh.subMeshCount, materials.Length); for (var i = 0; i < subMeshCount; i++) { var material = materials[i]; if (!material) continue; combineInstancesByMaterial.Add( material, new CombineInstance { mesh = mesh, subMeshIndex = i, transform = meshFilter.transform.localToWorldMatrix }); } } private Mesh CreateBakedMesh(string bakedName, ListMultiDictionary combineInstancesByMaterial) { var bakedMesh = new Mesh { name = bakedName }; if (combineInstancesByMaterial.KeyCount == 1) { CombineSingleMaterialMesh(bakedMesh, combineInstancesByMaterial); return bakedMesh; } CombineMultiMaterialMesh(bakedMesh, combineInstancesByMaterial); return bakedMesh; } private void CombineSingleMaterialMesh(Mesh bakedMesh, ListMultiDictionary combineInstancesByMaterial) { foreach (var material in combineInstancesByMaterial.Keys) { var combineInstances = combineInstancesByMaterial[material]; bakedMesh.CombineMeshes(combineInstances.ToArray(), true, true); } } private void CombineMultiMaterialMesh(Mesh bakedMesh, ListMultiDictionary combineInstancesByMaterial) { var finalCombineInstances = new List(); var tempMeshes = new List(); foreach (var material in combineInstancesByMaterial.Keys) { var tempMesh = new Mesh(); tempMesh.CombineMeshes(combineInstancesByMaterial[material].ToArray(), true, true); finalCombineInstances.Add( new CombineInstance { mesh = tempMesh, transform = Matrix4x4.identity }); tempMeshes.Add(tempMesh); } bakedMesh.CombineMeshes(finalCombineInstances.ToArray(), false, false); foreach (var mesh in tempMeshes) DestroyImmediate(mesh); } private string GetBakeOutputFolder(out bool isPrefab) { var selectedPrefab = PrefabUtility.GetPrefabAssetPathOfNearestInstanceRoot(gameObject); if (string.IsNullOrEmpty(selectedPrefab)) { var prefabStage = PrefabStageUtility.GetCurrentPrefabStage(); if (prefabStage && prefabStage.prefabContentsRoot == gameObject) selectedPrefab = prefabStage.assetPath; } isPrefab = !string.IsNullOrEmpty(selectedPrefab); return isPrefab ? Path.GetDirectoryName(selectedPrefab) : "Assets"; } private void SaveBakedPrefab(string parentFolder, string bakedName, Mesh bakedMesh, ListMultiDictionary combineInstancesByMaterial) { var baked = new GameObject(bakedName) { layer = LayerMask.NameToLayer(_layer) }; baked.AddComponent().sharedMesh = bakedMesh; var bakedRenderer = baked.AddComponent(); bakedRenderer.sharedMaterials = new List(combineInstancesByMaterial.Keys).ToArray(); AddExcluded(baked.transform); PrefabUtility.SaveAsPrefabAsset(baked, $"{parentFolder}/{bakedName}.prefab"); DestroyImmediate(baked); } private HashSet GetExcluded() { var excluded = new HashSet(); foreach (var obj in _doNotBake) { if (!obj) continue; ExcludeHierarchy(obj.transform, excluded); } return excluded; } private void ExcludeHierarchy(Transform root, HashSet excluded) { excluded.Add(root); for (var i = 0; i < root.childCount; i++) ExcludeHierarchy(root.GetChild(i), excluded); } private void AddExcluded(Transform root) { foreach (var obj in _doNotBake) { if (!obj) continue; var clone = Instantiate(obj, root); clone.name = obj.name; ApplyRelativeTransform(obj.transform, clone.transform); } } private void ApplyRelativeTransform(Transform source, Transform target) { var relativeMatrix = transform.worldToLocalMatrix * source.localToWorldMatrix; var localPosition = relativeMatrix.MultiplyPoint3x4(Vector3.zero); var localForward = relativeMatrix.MultiplyVector(Vector3.forward); var localUp = relativeMatrix.MultiplyVector(Vector3.up); var scaleX = relativeMatrix.GetColumn(0).magnitude; var scaleY = relativeMatrix.GetColumn(1).magnitude; var scaleZ = relativeMatrix.GetColumn(2).magnitude; target.localPosition = localPosition; target.localRotation = Quaternion.LookRotation(localForward, localUp); target.localScale = new Vector3(scaleX, scaleY, scaleZ); } #endif } }