using UnityEngine; using System; using System.IO; using System.Linq; using UnityEditor.SettingsManagement; using UnityEngine.ProBuilder; using Math = UnityEngine.ProBuilder.Math; namespace UnityEditor.ProBuilder { /// /// Mesh editing helper functions that are only available in the Editor. /// public static class EditorMeshUtility { const string k_MeshCacheDirectoryName = "ProBuilderMeshCache"; static string k_MeshCacheDirectory = "Assets/ProBuilder Data/ProBuilderMeshCache"; [UserSetting("Mesh Editing", "Auto Resize Colliders", "Automatically resize colliders with mesh bounds as you edit.")] static Pref s_AutoResizeCollisions = new Pref("editor.autoRecalculateCollisions", false, SettingsScope.Project); /// /// This callback is raised after a ProBuilderMesh has been successfully optimized. /// /// public static event Action meshOptimized = null; /// /// Optmizes the mesh geometry, and generates a UV2 channel (if object is marked as LightmapStatic, or generateLightmapUVs is true). /// /// This is only applicable to meshes with triangle topology. Quad meshes are not affected by this function. /// The ProBuilder mesh component to be optimized. /// If the Auto UV2 preference is disabled this parameter can be used to force UV2s to be built. public static void Optimize(this ProBuilderMesh mesh, bool generateLightmapUVs = false) { if (mesh == null) throw new ArgumentNullException("mesh"); Mesh umesh = mesh.mesh; if (umesh == null || umesh.vertexCount < 1) return; bool skipMeshProcessing = false; // @todo Support mesh compression for topologies other than Triangles. for (int i = 0; !skipMeshProcessing && i < umesh.subMeshCount; i++) if (umesh.GetTopology(i) != MeshTopology.Triangles) skipMeshProcessing = true; if (!skipMeshProcessing) { bool autoLightmap = Lightmapping.autoUnwrapLightmapUV; #if UNITY_2019_2_OR_NEWER bool lightmapUVs = generateLightmapUVs || (autoLightmap && mesh.gameObject.HasStaticFlag(StaticEditorFlags.ContributeGI)); #else bool lightmapUVs = generateLightmapUVs || (autoLightmap && mesh.gameObject.HasStaticFlag(StaticEditorFlags.LightmapStatic)); #endif // if generating UV2, the process is to manually split the mesh into individual triangles, // generate uv2, then re-assemble with vertex collapsing where possible. // if not generating uv2, just collapse vertices. if (lightmapUVs) { Vertex[] vertices = UnityEngine.ProBuilder.MeshUtility.GeneratePerTriangleMesh(umesh); float time = Time.realtimeSinceStartup; UnwrapParam unwrap = Lightmapping.GetUnwrapParam(mesh.unwrapParameters); Vector2[] uv2 = Unwrapping.GeneratePerTriangleUV(umesh, unwrap); // If GenerateUV2() takes longer than 3 seconds (!), show a warning prompting user to disable auto-uv2 generation. if ((Time.realtimeSinceStartup - time) > 3f) Log.Warning(string.Format("Generate UV2 for \"{0}\" took {1} seconds! You may want to consider disabling Auto-UV2 generation in the `Preferences > ProBuilder` tab.", mesh.name, (Time.realtimeSinceStartup - time).ToString("F2"))); if (uv2.Length == vertices.Length) { for (int i = 0; i < uv2.Length; i++) vertices[i].uv2 = uv2[i]; } else { Log.Warning("Generate UV2 failed. The returned size of UV2 array != mesh.vertexCount"); } UnityEngine.ProBuilder.MeshUtility.CollapseSharedVertices(umesh, vertices); } else { UnityEngine.ProBuilder.MeshUtility.CollapseSharedVertices(umesh); } } if (s_AutoResizeCollisions) RebuildColliders(mesh); if (meshOptimized != null) meshOptimized(mesh, umesh); if (Experimental.meshesAreAssets) TryCacheMesh(mesh); UnityEditor.EditorUtility.SetDirty(mesh); } internal static void TryCacheMesh(ProBuilderMesh pb) { Mesh mesh = pb.mesh; // check for an existing mesh in the mesh cache and update or create a new one so // as not to clutter the scene yaml. string meshAssetPath = AssetDatabase.GetAssetPath(mesh); // if mesh is already an asset any changes will already have been applied since // pb_Object is directly modifying the mesh asset if (string.IsNullOrEmpty(meshAssetPath)) { // at the moment the asset_guid is only used to name the mesh something unique string guid = pb.assetGuid; if (string.IsNullOrEmpty(guid)) { guid = Guid.NewGuid().ToString("N"); pb.assetGuid = guid; } string meshCacheDirectory = GetMeshCacheDirectory(true); string path = string.Format("{0}/{1}.asset", meshCacheDirectory, guid); Mesh m = AssetDatabase.LoadAssetAtPath(path); // a mesh already exists in the cache for this pb_Object if (m != null) { if (mesh != m) { // prefab instances should always point to the same mesh if (EditorUtility.IsPrefabInstance(pb.gameObject) || EditorUtility.IsPrefabAsset(pb.gameObject)) { // Debug.Log("reconnect prefab to mesh"); // use the most recent mesh iteration (when undoing for example) UnityEngine.ProBuilder.MeshUtility.CopyTo(mesh, m); UnityEngine.Object.DestroyImmediate(mesh); pb.gameObject.GetComponent().sharedMesh = m; // also set the MeshCollider if it exists MeshCollider mc = pb.gameObject.GetComponent(); if (mc != null) mc.sharedMesh = m; return; } else { // duplicate mesh // Debug.Log("create new mesh in cache from disconnect"); pb.assetGuid = Guid.NewGuid().ToString("N"); path = string.Format("{0}/{1}.asset", meshCacheDirectory, pb.assetGuid); } } else { Debug.LogWarning("Mesh found in cache and scene mesh references match, but pb.asset_guid doesn't point to asset. Please report the circumstances leading to this event to Karl."); } } AssetDatabase.CreateAsset(mesh, path); } } internal static bool GetCachedMesh(ProBuilderMesh pb, out string path, out Mesh mesh) { if (pb.mesh != null) { string meshPath = AssetDatabase.GetAssetPath(pb.mesh); if (!string.IsNullOrEmpty(meshPath)) { path = meshPath; mesh = pb.mesh; return true; } } string meshCacheDirectory = GetMeshCacheDirectory(false); string guid = pb.assetGuid; path = string.Format("{0}/{1}.asset", meshCacheDirectory, guid); mesh = AssetDatabase.LoadAssetAtPath(path); return mesh != null; } static string GetMeshCacheDirectory(bool initializeIfMissing = false) { if (Directory.Exists(k_MeshCacheDirectory)) return k_MeshCacheDirectory; string[] results = Directory.GetDirectories("Assets", k_MeshCacheDirectoryName, SearchOption.AllDirectories); if (results.Length < 1) { if (initializeIfMissing) { k_MeshCacheDirectory = FileUtility.GetLocalDataDirectory() + "/" + k_MeshCacheDirectoryName; Directory.CreateDirectory(k_MeshCacheDirectory); } else { k_MeshCacheDirectory = null; } } else { k_MeshCacheDirectory = results.First(); } return k_MeshCacheDirectory; } /// /// Resize any collider components on this mesh to match the size of the mesh bounds. /// /// The mesh target to rebuild collider volumes for. public static void RebuildColliders(this ProBuilderMesh mesh) { mesh.mesh.RecalculateBounds(); var bounds = mesh.mesh.bounds; foreach (var collider in mesh.GetComponents()) { Type t = collider.GetType(); if (t == typeof(BoxCollider)) { ((BoxCollider)collider).center = bounds.center; ((BoxCollider)collider).size = bounds.size; } else if (t == typeof(SphereCollider)) { ((SphereCollider)collider).center = bounds.center; ((SphereCollider)collider).radius = Math.LargestValue(bounds.extents); } else if (t == typeof(CapsuleCollider)) { ((CapsuleCollider)collider).center = bounds.center; Vector2 xy = new Vector2(bounds.extents.x, bounds.extents.z); ((CapsuleCollider)collider).radius = Math.LargestValue(xy); ((CapsuleCollider)collider).height = bounds.size.y; } else if (t == typeof(WheelCollider)) { ((WheelCollider)collider).center = bounds.center; ((WheelCollider)collider).radius = Math.LargestValue(bounds.extents); } else if (t == typeof(MeshCollider)) { ((MeshCollider)collider).sharedMesh = null; ((MeshCollider)collider).sharedMesh = mesh.mesh; } } } } }