#if (UNITY_EDITOR) using System.Collections.Generic; using UnityEngine; using UnityEditor; using System.Linq; namespace ECE { [System.Serializable] public class EasyColliderEditor : ScriptableObject, ISerializationCallbackReceiver { #region VHACD // VHACD section #if (!UNITY_EDITOR_LINUX) /// /// An instance of VHACD for use in each step when using RunVHACDStep /// private EasyColliderVHACD _VHACD; /// /// Array of meshes created from VHACDRunStep /// private Mesh[] _VHACDMeshes; /// /// Maximum number of vhacd recalculations /// private int _VHACDMaxCalculations = 3; /// /// Current vhacd calculation /// private int _VHACDCurrentCalculation = 0; /// /// List of colliders created by vhacd /// public List VHACDCreatedColliders = new List(); public List _VHACDConvertedData = new List(); /// /// Checks if the computation is finished and valid. /// /// If force under 256 triangles is enabled, if the computation is finished but not valid, /// will start a recomputation with a lower vertex limit. /// Only recomputes a certain # of times before logging a warning and finishing. /// /// If it's not enabled, just checks if the computation of convex hull data is complete /// /// Parameters of current vhacd calculation /// True if complete or complete and valid, false otherwise. public bool VHACDCheckCompute(VHACDParameters parameters) { // check to see if we're forcing under 256 triangles and can still recalculate. if (parameters.forceUnder256Triangles && _VHACDCurrentCalculation < _VHACDMaxCalculations) { // if the computation is finished if (_VHACD.IsComputeFinished()) { // and valid if (_VHACD.IsValid()) { // were done. return true; } else { // recompute the colliders (this changes the max number of vertices per hull so we get under 256) _VHACD.RecomputeVHACD(); // increase the current calculation count. _VHACDCurrentCalculation += 1; return false; } } // compute isn't finished. else { return false; } } else { // if it's finished and we reached calculation max, tell user to reduce number of vertices per CH. if (_VHACD.IsComputeFinished() && _VHACDCurrentCalculation == _VHACDMaxCalculations && _VHACD.IsValid() == false) { Debug.LogWarning("EasyColliderEditor: VHACD computation completed, but the final result had a number of triangles > 255. Try decreasing max vertices per hull, or increasing the number of hulls to prevent these errors."); } // return if the compute is finished. return _VHACD.IsComputeFinished(); } } /// /// Checks if VHACD is null /// /// false if null public bool VHACDExists() { if (_VHACD == null) { return false; } return true; } private Dictionary _VHACDPreviewResult = new Dictionary(); public Dictionary VHACDGetPreview() { if (_VHACDPreviewResult != null && _VHACDPreviewResult.Count > 0) { return _VHACDPreviewResult; } return null; } public void VHACDClearPreviewResult() { _VHACDPreviewResult = new Dictionary(); _VHACDConvertedData = new List(); } /// /// Runs VHACD step by step /// /// Current step to run, 0-5 /// Parameters to use /// Save path for meshes /// Gameobject to attach mesh collider's to. /// true if step is valid and completes, false otherwise public bool VHACDRunStep(int step, VHACDParameters parameters, bool saveAsAsset) { switch (step) { case 0: // setup // clean before another run which re-initializes itself. // fix for an issue with the newer version of vhacd (although it's not used yet) if (_VHACD != null) { if (!_VHACD.IsComputeFinished()) { return false; } _VHACD.Clean(); } _VHACD = new EasyColliderVHACD(); _VHACDCurrentCalculation = 0; _VHACD.Init(true); VHACDCreatedColliders = new List(); if (parameters.SeparateChildMeshes && parameters.UseSelectedVertices) { // occasionally this can happen somehow even though it shouldn't, this should treat the issue but not the cause. parameters.SeparateChildMeshes = false; parameters.UseSelectedVertices = true; } return _VHACD.SetParameters(parameters); case 1: // prepare mesh data. // if we're not using just the selected vertices, ie we are using the whole mesh at once. if (!parameters.UseSelectedVertices) { // for seperate child meshes, we attach each result to each mesh. // this allows one to run it on multiple meshes all with the same parameters using a common (or temporary) parent object. if (parameters.SeparateChildMeshes && parameters.MeshFilters[parameters.CurrentMeshFilter] != null) { //TODO: figure out a smart wayt hat communicates that attach to objects are used with separate child meshes // child object as the attach to with separate child meshes? as that functionality is then the same as the normal child object one? // \nChild Object: Attach colliders to a child of Attach To field.\nIndividual Child Objects: Each collider is attached to its own child whos parent is a child of the Attach To field. // so since attach to says it attachs to the thing int he attach to field, we should probably change the behaviour on the IndividualChildObjects thing. // if (parameters.vhacdResultMethod == VHACD_RESULT_METHOD.AttachTo && parameters.SeparateChildMeshes && !parameters.PerMeshAttachOverride) // { // default to using the attach to object, only if it's null so we don't override anything that was already set through previous separated child meshes. if (parameters.AttachTo == null) { parameters.AttachTo = AttachToObject; } // if we're overriding to per mesh, use the mesh filters gameobject as the base attach to object. if (parameters.PerMeshAttachOverride) { parameters.AttachTo = parameters.MeshFilters[parameters.CurrentMeshFilter].gameObject; } // it's a temporary added component. if (AddedInstanceIDs.Contains(parameters.AttachTo.GetInstanceID())) { if (parameters.AttachTo.transform.parent != null) { // update attach to so we dont lose created colliders, and adjust save name. parameters.AttachTo = parameters.AttachTo.transform.parent.gameObject; if (!parameters.IsCalculationForPreview) { parameters.SavePath = parameters.SavePath.Remove(parameters.SavePath.LastIndexOf("/") + 1) + parameters.AttachTo.name; } } } } // if we're including child meshes, we need to prepare them differently, // all the child meshes essentially get merged into 1 mesh and sent into VHACD. if (IncludeChildMeshes && !parameters.SeparateChildMeshes) { // mesh filters need to be passed as the vertices need to be transformed to attach to's local space. return _VHACD.PrepareMeshData(parameters.MeshFilters, parameters.AttachTo.transform); } else { // a single mesh, or each individual seperated child mesh gets prepared. // attach to the attach to object. if (parameters.MeshFilters[parameters.CurrentMeshFilter] != null) { bool prepared = _VHACD.PrepareMeshData(parameters.MeshFilters[parameters.CurrentMeshFilter], parameters.AttachTo.transform, parameters.MeshFilters[parameters.CurrentMeshFilter].sharedMesh); if (!prepared) { Debug.LogWarning("EasyColliderEditor: Unable to run VHACD on: " + parameters.MeshFilters[parameters.CurrentMeshFilter].name + ". Likely due to missing a mesh in the mesh filter.", parameters.MeshFilters[parameters.CurrentMeshFilter].gameObject); } } return true; } } else // use selected verts. { // Use selected verts is enabled, so we need to create a mesh from the selected vertices. Mesh m = CreateVHACDSelectedVerticesPreviewMesh(parameters); // prepare the mesh return _VHACD.PrepareMeshData(parameters.MeshFilters[parameters.CurrentMeshFilter], parameters.AttachTo.transform, m); } case 2: // calculate (run VHACD on the prepared-mesh) if (_VHACDCurrentCalculation == 0) { _VHACD.RunVHACD(); _VHACDCurrentCalculation = 1; return false; } else if (_VHACD.IsComputeFinished()) // check if compute is finished { // we recalculate if necessary, otherwise calculation is done. return VHACDCheckCompute(parameters); } return false; case 3: // save meshes as assets if needed / get the data for each mesh from VHACD and build a mesh. if (!parameters.IsCalculationForPreview) { _VHACDMeshes = _VHACD.CreateConvexHullMeshes(); if (saveAsAsset && parameters.ConvertTo == VHACD_CONVERSION.None) { _VHACDMeshes = EasyColliderSaving.CreateAndSaveMeshAssets(_VHACDMeshes, parameters.SavePath, parameters.SaveSuffix); } } else if (parameters.IsCalculationForPreview) { // for the preview. _VHACDMeshes = _VHACD.CreateConvexHullMeshes(); if (parameters.ConvertTo == VHACD_CONVERSION.None) { if (_VHACDPreviewResult.ContainsKey(parameters.AttachTo.transform)) { _VHACDPreviewResult[parameters.AttachTo.transform] = _VHACDPreviewResult[parameters.AttachTo.transform].Concat(_VHACDMeshes).ToArray(); } else { _VHACDPreviewResult.Add(parameters.AttachTo.transform, _VHACDMeshes); } } else { EasyColliderCreator convert = new EasyColliderCreator(); foreach (Mesh m in _VHACDMeshes) { EasyColliderData ecd = null; if (parameters.ConvertTo == VHACD_CONVERSION.Boxes) { ecd = convert.CalculateBoxLocal(m.vertices.ToList()); } else if (parameters.ConvertTo == VHACD_CONVERSION.Spheres) { ecd = convert.CalculateSphereMinMaxLocal(m.vertices.ToList()); } else if (parameters.ConvertTo == VHACD_CONVERSION.Capsules) { ecd = convert.CalculateCapsuleMinMaxLocal(m.vertices.ToList(), CAPSULE_COLLIDER_METHOD.MinMax); } ecd.Matrix = parameters.AttachTo.transform.localToWorldMatrix; _VHACDConvertedData.Add(ecd); } } } return true; case 4: // use the data from VHACD to generate convex mesh colliders. if (parameters.IsCalculationForPreview) { return true; } // skip step 4 for previews. if (parameters.vhacdResultMethod != VHACD_RESULT_METHOD.AttachTo) { // prevents non-per mesh override duplication of "VHACDColliders" when using separate child meshes // otherwise each mesh would create another "VHACDColliders" as a child of the original base VHACDColliders object. if (parameters.AttachTo == null || (parameters.AttachTo != null && !parameters.AttachTo.name.Contains("VHACDColliders"))) { // if the method isn't the default attach to, we create a parent to hold colliders GameObject parent = new GameObject("VHACDColliders"); // since all verts were coverted to the attach to's location/position/rotation we use it's settings. parent.transform.parent = parameters.AttachTo.transform; parent.transform.position = parameters.AttachTo.transform.position; parent.transform.rotation = parameters.AttachTo.transform.rotation; parent.transform.localScale = Vector3.one; if (ECEPreferences.CopyParentObjectLayer) { parent.layer = parameters.AttachTo.layer; } else { parent.layer = GameObjectLayerOverride; } Undo.RegisterCreatedObjectUndo(parent, "Create VHACD Collider Holder"); parameters.AttachTo = parent; } } // keep the attach to object in case we need to create children. GameObject attachTo = parameters.AttachTo; EasyColliderCreator ecc = new EasyColliderCreator(); for (int i = 0; i < _VHACDMeshes.Length; i++) { // for individual child objects. if (parameters.vhacdResultMethod == VHACD_RESULT_METHOD.IndividualChildObjects) { // we create a child at the same position and rotation as the common parent (the attachto object) GameObject child = new GameObject("VHACDCollider"); child.transform.parent = parameters.AttachTo.transform; child.transform.position = parameters.AttachTo.transform.position; child.transform.rotation = parameters.AttachTo.transform.rotation; child.transform.localScale = Vector3.one; if (ECEPreferences.CopyParentObjectLayer) { child.layer = parameters.AttachTo.layer; } else { child.layer = GameObjectLayerOverride; } attachTo = child; Undo.RegisterCreatedObjectUndo(child, "Create VHACD Collider"); } if (parameters.ConvertTo == VHACD_CONVERSION.None) { // create a convex mesh collider. MeshCollider mc = ecc.CreateConvexMeshCollider(_VHACDMeshes[i], attachTo, GetProperties()); CreatedColliders.Add(mc); VHACDCreatedColliders.Add(mc); AddedColliderIDs.Add(mc.GetInstanceID()); } else if (parameters.ConvertTo == VHACD_CONVERSION.Boxes) { EasyColliderProperties p = GetProperties(); p.AttachTo = attachTo; BoxCollider bc = ecc.CreateBoxCollider(_VHACDMeshes[i].vertices.ToList(), p, true); CreatedColliders.Add(bc); AddedColliderIDs.Add(bc.GetInstanceID()); } else if (parameters.ConvertTo == VHACD_CONVERSION.Spheres) { EasyColliderProperties p = GetProperties(); p.AttachTo = attachTo; SphereCollider sc = ecc.CreateSphereCollider_MinMax(_VHACDMeshes[i].vertices.ToList(), p, true); CreatedColliders.Add(sc); AddedColliderIDs.Add(sc.GetInstanceID()); } else if (parameters.ConvertTo == VHACD_CONVERSION.Capsules) { EasyColliderProperties p = GetProperties(); p.AttachTo = attachTo; CapsuleCollider cc = ecc.CreateCapsuleCollider_MinMax(_VHACDMeshes[i].vertices.ToList(), p, CAPSULE_COLLIDER_METHOD.MinMax, true); CreatedColliders.Add(cc); AddedColliderIDs.Add(cc.GetInstanceID()); } } return true; case 5: // clean up if (parameters.IsCalculationForPreview) { return true; } // skip step 5 for previews. if (parameters.UseSelectedVertices) { Undo.RecordObject(this, "Run VHACD"); ClearSelectedVertices(); } // compute buffer gets all points set to origin when VHACD is run without use only selected vertices. So let's reupdate it. if (Compute != null) { Compute.UpdateSelectedBuffer(GetWorldVertices()); } _VHACD.Clean(); return true; } return false; } /// /// Creates a mesh from the current VHACD preview using the "use selected vertices" method /// /// Current VHACD parameters /// Mesh created from the current VHACD preview using full-triangles, and adding remaining vertices by closest distance private Mesh CreateVHACDSelectedVerticesPreviewMesh(VHACDParameters parameters) { // list of created meshes, 1 for each mesh filter. List createdMeshList = new List(); // arrays to hold vertices of triangles of each mesh filter, and transform for each mesh filter. Vector3[] vertices = new Vector3[0]; int[] triangles = new int[0]; Vector3[] normals = new Vector3[0]; Transform t = null; // variables to hold easy collider vertices for each triangle of the mesh EasyColliderVertex ecv0, ecv1, ecv2 = ecv1 = ecv0 = null; foreach (MeshFilter mf in MeshFilters) { if (mf == null) continue; // hashset of used vertices for use after full triangle pass. HashSet usedVerticesSet = new HashSet(); // dictionary of vertex : vertex index to update. Dictionary ecvVertIndexDictionary = new Dictionary(); // calculated mesh vertices and triangles to create a mesh with. List verticesList = new List(); List trianglesList = new List(); // transform, verts, and tris of current mesh filter. t = mf.transform; vertices = mf.sharedMesh.vertices; triangles = mf.sharedMesh.triangles; normals = mf.sharedMesh.normals; // keep track of how many verts have been added just to make it a little easier. int vertexCount = 0; // go through triangles to see if the whole triangle is selected. for (int i = 0; i < triangles.Length; i += 3) { // vertices of the triangle. ecv0 = new EasyColliderVertex(t, vertices[triangles[i]]); ecv1 = new EasyColliderVertex(t, vertices[triangles[i + 1]]); ecv2 = new EasyColliderVertex(t, vertices[triangles[i + 2]]); // if the full triangle is selected, add it. if (SelectedVerticesSet.Contains(ecv0) && SelectedVerticesSet.Contains(ecv1) && SelectedVerticesSet.Contains(ecv2)) { // Debug.Log("Full triangle."); // add verts in world-space to convert later. Vector3 v0 = vertices[triangles[i]]; Vector3 v1 = vertices[triangles[i + 1]]; Vector3 v2 = vertices[triangles[i + 2]]; // if it's been used before. if (ecvVertIndexDictionary.ContainsKey(ecv0)) { // Debug.Log("Vertex already in dictionary"); // it's in the dictionary, so add the value in the dict to the triangle list trianglesList.Add(ecvVertIndexDictionary[ecv0]); } else { // we haven't used this vertex yet, so add it verticesList.Add(v0); vertexCount++; trianglesList.Add(vertexCount - 1); // remember to add the vertex to the dictionary with the appropriate index value. ecvVertIndexDictionary.Add(ecv0, vertexCount - 1); // and add them to the used vertices set usedVerticesSet.Add(ecv0); } // ecv1 - repeat above. if (ecvVertIndexDictionary.ContainsKey(ecv1)) { trianglesList.Add(ecvVertIndexDictionary[ecv1]); } else { verticesList.Add(v1); vertexCount++; trianglesList.Add(vertexCount - 1); ecvVertIndexDictionary.Add(ecv1, vertexCount - 1); usedVerticesSet.Add(ecv1); } // ecv2 - repeat above if (ecvVertIndexDictionary.ContainsKey(ecv2)) { trianglesList.Add(ecvVertIndexDictionary[ecv2]); } else { verticesList.Add(v2); vertexCount++; trianglesList.Add(vertexCount - 1); ecvVertIndexDictionary.Add(ecv2, vertexCount - 1); usedVerticesSet.Add(ecv2); } } } // hashset of selected vertices. HashSet selectedVerticesSet = new HashSet(SelectedVerticesSet); // int count = selectedVerticesSet.Count; // remove unused vertices that are full triangles. selectedVerticesSet.ExceptWith(usedVerticesSet); // Debug.Log("Remaining vertices count:" + selectedVerticesSet.Count + " Total At Start:" + count); // list of reamining verts (vertices that are selected that arent' a part of at least 1 full triangle.) List remainingVertsLocal = new List(); // transform remaining verts to world-space. foreach (EasyColliderVertex ecv in selectedVerticesSet) { // make sure the transform is the same as the current mesh filter. if (ecv.T == t) { remainingVertsLocal.Add(ecv.LocalPosition); } } // here we are checking to see if there was at least 1 full triangle to build from, if there wasn't then we make one. // need at least 1 triangle to build from, but need at least 3 verts to do so. if (trianglesList.Count < 3 && remainingVertsLocal.Count >= 3) { // find closest 3 in-order points (faster.. less accurate) float totalMinDistance = Mathf.Infinity; int[] vertIndexs = new int[3]; for (int i = 0; i < remainingVertsLocal.Count - 3; i++) { // d0 -> d1 float d01 = Vector3.Distance(remainingVertsLocal[i], remainingVertsLocal[i + 1]); // d1 -> d2 float d12 = Vector3.Distance(remainingVertsLocal[i + 1], remainingVertsLocal[i + 2]); // d2 -> d0 float d20 = Vector3.Distance(remainingVertsLocal[i + 2], remainingVertsLocal[i]); // new "smallest" in-order triangle if (d01 + d12 + d20 < totalMinDistance) { totalMinDistance = d01 + d12 + d20; vertIndexs[0] = i; vertIndexs[1] = i + 1; vertIndexs[2] = i + 2; } } // add the vertices verticesList.Add(remainingVertsLocal[vertIndexs[0]]); verticesList.Add(remainingVertsLocal[vertIndexs[1]]); verticesList.Add(remainingVertsLocal[vertIndexs[2]]); // add the triangle trianglesList.Add(verticesList.Count - 3); trianglesList.Add(verticesList.Count - 2); trianglesList.Add(verticesList.Count - 1); } // add the remaining left-over non-full triangle vertices. float minDistance = Mathf.Infinity; int vertIndex0, vertIndex1 = vertIndex0 = -1; foreach (Vector3 pos in remainingVertsLocal) { // find "closest" triangle edge quickly. minDistance = Mathf.Infinity; for (int i = 0; i < trianglesList.Count; i += 3) { // calc distance from vertex to each triangle point. float d0, d1, d2 = d1 = d0 = 0; d0 = Vector3.Distance(pos, verticesList[trianglesList[i]]); d1 = Vector3.Distance(pos, verticesList[trianglesList[i + 1]]); d2 = Vector3.Distance(pos, verticesList[trianglesList[i + 2]]); // find lowest distance from remaining vert to a triangle vertex if (d0 < minDistance) { // set the min distance minDistance = d0; // update the i0 index vertIndex0 = trianglesList[i]; // update i1 index based on which other vertex in the triangle is closer. vertIndex1 = d1 < d2 ? trianglesList[i + 1] : trianglesList[i + 2]; } // repeat for d1. if (d1 < minDistance) { minDistance = d1; vertIndex0 = trianglesList[i + 1]; vertIndex1 = d0 < d2 ? trianglesList[i] : trianglesList[i + 2]; } // d2 not needed because d0 -> d1, d2 or d1 -> d0, d2 would give the same result if d2 was lowest. } // now we have the "closest" triangle.. // add the vertex if (vertIndex0 >= 0 && vertIndex1 >= 0) { verticesList.Add(pos); // add the the triangle. trianglesList.Add(verticesList.Count - 1); trianglesList.Add(vertIndex0); trianglesList.Add(vertIndex1); } else { // we have a single lonely vertex that needs to be added. // luckily, just adding it 3 times and setting is a triangle works for VHACD.... verticesList.Add(pos); verticesList.Add(pos); verticesList.Add(pos); trianglesList.Add(verticesList.Count - 1); trianglesList.Add(verticesList.Count - 2); trianglesList.Add(verticesList.Count - 3); } } // extrude selected vertices. if (parameters.NormalExtrudeMultiplier > 0.0f) { int vCount = verticesList.Count; Dictionary vertexIndexToTriangleIndexDictionary = new Dictionary(); int count = trianglesList.Count; for (int i = 0; i < count; i++) { if (vertexIndexToTriangleIndexDictionary.ContainsKey(trianglesList[i])) { trianglesList.Add(vertexIndexToTriangleIndexDictionary[trianglesList[i]]); } else { int nCount = 0; Vector3 p = verticesList[trianglesList[i]]; Vector3 n = Vector3.zero; for (int j = 0; j < vertices.Length; j++) { if (vertices[j] == p) { nCount++; n += normals[j]; } } n.Normalize(); trianglesList.Add(verticesList.Count); vertexIndexToTriangleIndexDictionary.Add(trianglesList[i], verticesList.Count); verticesList.Add(p + n * parameters.NormalExtrudeMultiplier); } } } // covert from local mesh space to world then // convert verts from world to attach to local space. Transform attachTo = parameters.AttachTo.transform; for (int i = 0; i < verticesList.Count; i++) { verticesList[i] = t.TransformPoint(verticesList[i]); verticesList[i] = attachTo.InverseTransformPoint(verticesList[i]); } // create and return the mesh for use in vhacd. Mesh m = new Mesh(); #if (UNITY_2017_3_OR_NEWER) m.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32; #endif m.vertices = verticesList.ToArray(); m.triangles = trianglesList.ToArray(); createdMeshList.Add(m); } Mesh result = new Mesh(); // use 32 bit index format for high # of verts. #if (UNITY_2017_3_OR_NEWER) result.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32; #endif List cis = new List(); // create combine instances for each created mesh. for (int i = 0; i < createdMeshList.Count; i++) { if (createdMeshList[i] != null) { CombineInstance ci = new CombineInstance(); ci.mesh = createdMeshList[i]; cis.Add(ci); } } //combine the mesh with the combine instances, NOT using the matrix. result.CombineMeshes(cis.ToArray(), true, false); return result; } #endif // end VHACD section #endregion [SerializeField] private List _AddedColliderIDs; /// /// List of added colliders through GetInstanceID() /// public List AddedColliderIDs { get { if (_AddedColliderIDs == null) { _AddedColliderIDs = new List(); } return _AddedColliderIDs; } set { _AddedColliderIDs = value; } } [SerializeField] private List _AddedInstanceIDs; /// /// List of all object's instance IDs that should be destroyed on cleanup. These are things like /// MeshCollider used for vertex selection. /// MeshFilter for skinned mesh renderers for mesh colliders. /// Compute shader component. /// Gizmo drawing component. /// public List AddedInstanceIDs { get { if (_AddedInstanceIDs == null) { _AddedInstanceIDs = new List(); } return _AddedInstanceIDs; } set { _AddedInstanceIDs = value; } } [SerializeField] private GameObject _AttachToObject; /// /// If different from the selected gameobject, the attach to object is used to convert to local vertices / attach the collider to. /// public GameObject AttachToObject { get { if (_AttachToObject == null) { return SelectedGameObject; } return _AttachToObject; } set { _AttachToObject = value; } } /// /// Are we automatically including child skinned meshes when include child meshes is enabled? /// public bool AutoIncludeChildSkinnedMeshes { get { return ECEPreferences.AutoIncludeChildSkinnedMeshes; } } [SerializeField] private bool _ColliderSelectEnabled; /// /// Is Collider Selection Enabled? Toggles colliders on and off when changed. /// public bool ColliderSelectEnabled { get { return _ColliderSelectEnabled; } set { _ColliderSelectEnabled = value; } } [SerializeField] private EasyColliderCompute _Compute; /// /// Compute shader script /// public EasyColliderCompute Compute { get { if (_Compute == null && SelectedGameObject != null) { _Compute = SelectedGameObject.GetComponent(); } return _Compute; } set { _Compute = value; if (value != null && DisplayMeshVertices) { _Compute.SetDisplayAllBuffer(GetAllWorldMeshVertices()); } } } [SerializeField] private List _CreatedColliders; /// /// List of colliders we created /// public List CreatedColliders { get { if (_CreatedColliders == null) { _CreatedColliders = new List(); } return _CreatedColliders; } set { _CreatedColliders = value; } } [SerializeField] /// /// Volume per vertex density. Used in displaying vertices so less verts in more space displays larger and vice versa. /// public float DensityScale = 1.0f; public bool DisplayMeshVertices { get { return ECEPreferences.DisplayAllVertices; } } private static EasyColliderPreferences _ECEPreferences; public static EasyColliderPreferences ECEPreferences { get { if (_ECEPreferences == null) { _ECEPreferences = EasyColliderPreferences.Preferences; } return _ECEPreferences; } } [SerializeField] /// /// Does the selected gameboject have a skinned mesh renderer as a child somewhere? /// public bool HasSkinnedMeshRenderer = false; [SerializeField] private EasyColliderGizmos _Gizmos; /// /// Gizmos component for drawing vertices and selections. /// public EasyColliderGizmos Gizmos { get { if (_Gizmos == null && SelectedGameObject != null) { _Gizmos = SelectedGameObject.GetComponent(); } return _Gizmos; } set { _Gizmos = value; if (value != null && DisplayMeshVertices) { _Gizmos.DisplayVertexPositions = GetAllWorldMeshVertices(); } } } [SerializeField] private bool _IncludeChildMeshes; /// /// Are we including child meshes for vertex selection? /// public bool IncludeChildMeshes { get { return _IncludeChildMeshes; } set { if (_IncludeChildMeshes != value) { OnlyDeselectableColliders.Clear(); // lets store things. // List selectedVertsBeforeChange = new List(SelectedVertices); //HashSet selectedNonVerts = new HashSet(SelectedNonVerticesSet); HashSet selectedChildColliders = new HashSet(SelectedColliders); if (!value && SelectedGameObject != null) { selectedChildColliders.ExceptWith(SelectedGameObject.GetComponents()); } // why do we do this? GameObject selected = SelectedGameObject; GameObject attach = AttachToObject; SelectedGameObject = null; _IncludeChildMeshes = value; //OnIncludeChildMeshesChanged(value); SelectedGameObject = selected; AttachToObject = attach; /* foreach(var v in selectedVertsBeforeChange) { SelectVertex(v, true); }*/ foreach (var c in selectedChildColliders) { SelectCollider(c); OnlyDeselectableColliders.Add(c); } } // _IncludeChildMeshes = value; // SetupChildObjects(value); // CalculateDensity(); if (value == false) { // CleanChildSelectedVertices(); } } } public HashSet OnlyDeselectableColliders = new HashSet(); /* void OnIncludeChildMeshesChanged(bool value) { HashSet rootMeshFilters = new HashSet(); HashSet childMeshFilters = new HashSet(); rootMeshFilters.UnionWith(SelectedGameObject.GetComponents()); childMeshFilters.UnionWith(SelectedGameObject.GetComponentsInChildren()); childMeshFilters.ExceptWith if (!value) { // CleanUpObject() } }*/ [SerializeField] private bool _IsTrigger; /// /// Created colliders marked as trigger? /// /// public bool IsTrigger { get { return _IsTrigger; } set { _IsTrigger = value; } } [SerializeField] private List _LastSelectedVertices; /// /// List of the vertices that were selected last /// public List LastSelectedVertices { get { if (_LastSelectedVertices == null) { _LastSelectedVertices = new List(); } return _LastSelectedVertices; } set { _LastSelectedVertices = value; } } [SerializeField] private List _MeshFilters; /// /// List of mesh filters on SelectedGameobject + children (if IncludeChildMeshes) /// public List MeshFilters { get { if (_MeshFilters == null) { _MeshFilters = new List(); } return _MeshFilters; } set { _MeshFilters = value; } } [SerializeField] private List _NonKinematicRigidbodies; /// /// Rigidbodies already on the objects that were marked as kinematic during setup. /// public List NonKinematicRigidbodies { get { if (_NonKinematicRigidbodies == null) { _NonKinematicRigidbodies = new List(); } return _NonKinematicRigidbodies; } set { _NonKinematicRigidbodies = value; } } #if (UNITY_6000_0_OR_NEWER) [SerializeField] private PhysicsMaterial _PhysicMaterial; /// /// Physic material to add to colliders upon creation. /// public PhysicsMaterial PhysicMaterial { get { return _PhysicMaterial; } set { _PhysicMaterial = value; } } #else [SerializeField] private PhysicMaterial _PhysicMaterial; /// /// Physic material to add to colliders upon creation. /// public PhysicMaterial PhysicMaterial { get { return _PhysicMaterial; } set { _PhysicMaterial = value; } } #endif [SerializeField] private List _RaycastableColliders; public List RaycastableColliders { get { if (_RaycastableColliders == null) { _RaycastableColliders = new List(); } return _RaycastableColliders; } set { _RaycastableColliders = value; } } /// /// Method we use to render points with. Either using a shader or gizmos. /// private RENDER_POINT_TYPE RenderPointType { get { return ECEPreferences.RenderPointType; } } public void ChangeRenderPointType(RENDER_POINT_TYPE value) { // add or remove one if the value is changing and it's already added if (value != RENDER_POINT_TYPE.SHADER && Compute != null) { TryDestroyComponent(Compute); } if (value != RENDER_POINT_TYPE.GIZMOS && Gizmos != null) { TryDestroyComponent(Gizmos); } // add the new component if needed. if (value == RENDER_POINT_TYPE.GIZMOS && Gizmos == null && SelectedGameObject != null) { Gizmos = Undo.AddComponent(SelectedGameObject); AddedInstanceIDs.Add(Gizmos.GetInstanceID()); } if (value == RENDER_POINT_TYPE.SHADER && Compute == null && SelectedGameObject != null) { Compute = Undo.AddComponent(SelectedGameObject); AddedInstanceIDs.Add(Compute.GetInstanceID()); } } [SerializeField] private int _GameObjectLayerOverride; /// /// Layer to set on rotated collider's gameobject if not using selected gameobject's layer. /// public int GameObjectLayerOverride { get { return _GameObjectLayerOverride; } set { _GameObjectLayerOverride = value; } } [SerializeField] private List _SelectedColliders; /// /// List of currently selected colliders /// public List SelectedColliders { get { if (_SelectedColliders == null) { _SelectedColliders = new List(); } return _SelectedColliders; } set { _SelectedColliders = value; } } [SerializeField] private GameObject _SelectedGameObject; /// /// The currently selected gameobject. Changing this causes a cleanup of the previous selected object, and initialization of the object you are setting. /// public GameObject SelectedGameObject { get { return _SelectedGameObject; } set { if (value == null) { CleanUpObject(_SelectedGameObject, false); _SelectedGameObject = value; AttachToObject = value; } else if (!EditorUtility.IsPersistent(value)) { // new selected object. if (value != _SelectedGameObject) { // Had a selected object, clean it up. if (_SelectedGameObject != null) { CleanUpObject(_SelectedGameObject, false); } // Value is an actual object, so set up everything that's needed. if (value != null) { _SelectedGameObject = value; AttachToObject = value; SelectObject(value); // log a warning if there is a kinematic rigidbody on a parent, we dont track it because it can get lost more easily than child-rigidbodies. // so just alert the user that vertices would not be able to be selected. Rigidbody[] rbs = _SelectedGameObject.GetComponentsInParent(); foreach (Rigidbody rb in rbs) { if (rb.gameObject != _SelectedGameObject && !rb.isKinematic) { Debug.LogWarning("EasyColliderEditor: A parent (" + rb.gameObject.name + ") of the selected object has a non-kinematic rigidbody, you may not be able to select vertices while it is marked as non-kinematic.", rb.gameObject); } } } } _SelectedGameObject = value; AttachToObject = value; } else { Debug.LogError("Easy Collider Editor's Selected GameObject must be located in the scene. Select a gameobject from the scene hierarchy. If you wish to use a prefab, enter prefab isolation mode then select the object. For more information of editing prefabs, see the included documentation pdf."); } } } [SerializeField] private List _SelectedVertices; /// /// Selected Vertices list (Needs to be a list, as hashsets are unordered, and some of the collider methods require specific order selection (like rotated ones)) /// public List SelectedVertices { get { if (_SelectedVertices == null) { _SelectedVertices = new List(); } return _SelectedVertices; } private set { _SelectedVertices = value; } } [SerializeField] private HashSet _SelectedVerticesSet; /// /// HashSet of SelectedVertices. Used to make things a little faster to search through. /// public HashSet SelectedVerticesSet { get { if (_SelectedVerticesSet == null) { _SelectedVerticesSet = new HashSet(); } return _SelectedVerticesSet; } set { _SelectedVerticesSet = value; } } public HashSet SelectedNonVerticesSet = new HashSet(); //Serializing our hashsets. [SerializeField] private List _SerializedSelectedVertexSet; [SerializeField] private List _TransformPositions; /// /// List of mesh filter world positions /// public List TransformPositions { get { if (_TransformPositions == null) { _TransformPositions = new List(); } return _TransformPositions; } set { _TransformPositions = value; } } [SerializeField] private List _TransformRotations; /// /// List of mesh filter rotations /// public List TransformRotations { get { if (_TransformRotations == null) { _TransformRotations = new List(); } return _TransformRotations; } set { _TransformRotations = value; } } [SerializeField] private List _TransformLocalScales; /// /// List of mesh filter local scales /// public List TransformLocalScales { get { if (_TransformLocalScales == null) { _TransformLocalScales = new List(); } return _TransformLocalScales; } set { _TransformLocalScales = value; } } [SerializeField] private List _TransformLossyScales; public List TransformLossyScales { get { if (_TransformLossyScales == null) { _TransformLossyScales = new List(); } return _TransformLossyScales; } set { _TransformLossyScales = value; } } [SerializeField] /// /// Is vertex selection enabled? /// public bool VertexSelectEnabled; HashSet _WorldMeshVertices; /// /// Set of all world space vertices for meshes that are able to have their vertices selected /// /// public HashSet WorldMeshVertices { get { if (_WorldMeshVertices == null) { _WorldMeshVertices = new HashSet(); } return _WorldMeshVertices; } } HashSet _WorldMeshPositions; /// /// Set of mesh filters positions, for update world mesh vertices. /// /// HashSet WorldMeshPositions { get { if (_WorldMeshPositions == null) { _WorldMeshPositions = new HashSet(); } return _WorldMeshPositions; } } HashSet _WorldMeshRotations; /// /// Set of world mesh rotations, for updating world mesh vertices. /// HashSet WorldMeshRotations { get { if (_WorldMeshRotations == null) { _WorldMeshRotations = new HashSet(); } return _WorldMeshRotations; } } HashSet _WorldMeshTransforms; /// /// Set of world mesh transforms, for updating world mesh vertices. /// HashSet WorldMeshTransforms { get { if (_WorldMeshTransforms == null) { _WorldMeshTransforms = new HashSet(); } return _WorldMeshTransforms; } } /// /// Removes all vertices that have index's greater than MeshFilter's count. /// private void CleanChildSelectedVertices() { // SelectedVertices.RemoveAll(vert => vert.MeshFilterIndex >= MeshFilters.Count); SelectedVertices.RemoveAll(vert => vert.T != SelectedGameObject.transform); } /// /// Cleans up the object and children if required. Destroys components based on if going into play mode. Reenables/disables components to pre-selection values. /// /// Parent object to clean up /// Is the editor going into play mode? public void CleanUpObject(GameObject go, bool intoPlayMode) { foreach (int id in AddedInstanceIDs) { #if (UNITY_6000_3_OR_NEWER) Object o = EditorUtility.EntityIdToObject(id); #else Object o = EditorUtility.InstanceIDToObject(id); #endif if (o != null) { if (intoPlayMode) { if (o is Component) { TryDestroyComponent((Component)o); } else { TryDestroyObject((GameObject)o); } } else { if (o is Component) { TryDestroyComponent((Component)o); } else { TryDestroyObject((GameObject)o); } } } } foreach (Rigidbody rb in NonKinematicRigidbodies) { if (rb != null) { rb.isKinematic = false; } } // force cleanup gizmos and compute. if (Gizmos != null) { TryDestroyComponent(Gizmos); } if (Compute != null) { TryDestroyComponent(Compute); } if (go != null) { // lets be safe and search for additional compute and gizmos. EasyColliderGizmos[] extraGizmos = go.GetComponentsInChildren(); for (int i = 0; i < extraGizmos.Length; i++) { TryDestroyComponent(extraGizmos[i]); } EasyColliderCompute[] extraComputes = go.GetComponentsInChildren(); for (int i = 0; i < extraComputes.Length; i++) { TryDestroyComponent(extraComputes[i]); } } if (go != null) { // Enable by added collider instance ids incase they were lost. Collider[] cols = go.GetComponentsInChildren(); foreach (Collider col in cols) { if (AddedColliderIDs.Contains(col.GetInstanceID())) { col.enabled = true; } } } HasSkinnedMeshRenderer = false; ClearListsForNewObject(); } /// /// Creates new lists for all the lists used. /// void ClearListsForNewObject() { AddedInstanceIDs = new List(); CreatedColliders = new List(); LastSelectedVertices = new List(); MeshFilters = new List(); NonKinematicRigidbodies = new List(); OnlyDeselectableColliders = new HashSet(); RaycastableColliders = new List(); SelectedColliders = new List(); SelectedVertices = new List(); SelectedVerticesSet = new HashSet(); SelectedNonVerticesSet = new HashSet(); _SkinnedMeshFilterPairs = new Dictionary(); BoneTransforms = new List(); _selectedBones = new List(); _selectedBoneVertices = new List(); LastSelectedBone = null; #if (!UNITY_EDITOR_LINUX) _VHACDPreviewResult = new Dictionary(); #endif } /// /// Deselects all currently selected vertices. /// public void ClearSelectedVertices() { this.SelectedVertices = new List(); this.SelectedVerticesSet = new HashSet(); this.SelectedNonVerticesSet = new HashSet(); } /// /// Creates a box colider with a given orientation /// /// Orientation of box collider public void CreateBoxCollider(COLLIDER_ORIENTATION orientation = COLLIDER_ORIENTATION.NORMAL) { EasyColliderCreator ecc = new EasyColliderCreator(); Collider createdCollider = ecc.CreateBoxCollider(GetWorldVertices(true), GetProperties(orientation)); if (createdCollider != null) { DisableCreatedCollider(createdCollider); } ClearSelectedVertices(); } /// /// Creates a capsule collider using the method and orientation provided. /// /// Capsule collider algoirthm to use /// Orientation to use public void CreateCapsuleCollider(CAPSULE_COLLIDER_METHOD method, COLLIDER_ORIENTATION orientation = COLLIDER_ORIENTATION.NORMAL) { EasyColliderCreator ecc = new EasyColliderCreator(); Collider createdCollider = null; switch (method) { // use the same method for all min max' but pass the method in. case CAPSULE_COLLIDER_METHOD.MinMax: case CAPSULE_COLLIDER_METHOD.MinMaxPlusDiameter: case CAPSULE_COLLIDER_METHOD.MinMaxPlusRadius: createdCollider = ecc.CreateCapsuleCollider_MinMax(GetWorldVertices(true), GetProperties(orientation), method); break; case CAPSULE_COLLIDER_METHOD.BestFit: createdCollider = ecc.CreateCapsuleCollider_BestFit(GetWorldVertices(true), GetProperties(orientation)); break; } if (createdCollider != null) { DisableCreatedCollider(createdCollider); } ClearSelectedVertices(); } public void CreateCylinderCollider() { EasyColliderCreator ecc = new EasyColliderCreator(); // convert the selected world points to local points that represent a cylinder. MeshColliderData data = ecc.CalculateCylinderCollider(GetWorldVertices(true), AttachToObject.transform); if (ECEPreferences.SaveConvexHullAsAsset) { EasyColliderSaving.CreateAndSaveMeshAsset(data.ConvexMesh, SelectedGameObject); } // generate the hull from the cylinder points. MeshCollider createdCollider = ecc.CreateConvexMeshCollider(data.ConvexMesh, AttachToObject, GetProperties()); if (createdCollider != null) { DisableCreatedCollider(createdCollider); } ClearSelectedVertices(); } /// /// Creates a convex mesh collider from the currently selected points using the given method /// /// public void CreateConvexMeshCollider(MESH_COLLIDER_METHOD method) { Mesh mesh = CreateConvexMesh(method); if (mesh == null) { Debug.LogWarning("EasyColliderEditor: Unable to create a valid convex mesh collider from the selected vertices, likely because all selected vertices are coplanar."); } else { EasyColliderCreator ecc = new EasyColliderCreator(); MeshCollider createdCollider = ecc.CreateConvexMeshCollider(mesh, AttachToObject, GetProperties()); if (createdCollider != null) { DisableCreatedCollider(createdCollider); } ClearSelectedVertices(); } } /// /// Creates a convex mesh from the currently selected points using the given method /// /// /// Convex Mesh private Mesh CreateConvexMesh(MESH_COLLIDER_METHOD method) { if (method == MESH_COLLIDER_METHOD.QuickHull) { return new EasyColliderCreator().CreateMesh_QuickHull(GetWorldVertices(true), AttachToObject, false, SelectedGameObject); } else { return new EasyColliderCreator().CreateMesh_Messy(GetWorldVertices(true), AttachToObject, SelectedGameObject); } } public void CreateRotatedAndDuplicatedColliders(CREATE_COLLIDER_TYPE type) { EasyColliderCreator ecc = new EasyColliderCreator(); List createdColliders = ecc.CreateRotateAndDuplicateColliders(type, GetWorldVertices(true), GetProperties()); foreach (Collider c in createdColliders) { if (c != null) { DisableCreatedCollider(c); } } ClearSelectedVertices(); } /// /// Creates a sphere collider /// /// Algorith to use to create the sphere collider. public void CreateSphereCollider(SPHERE_COLLIDER_METHOD method) { EasyColliderCreator ecc = new EasyColliderCreator(); Collider createdCollider = null; switch (method) { case SPHERE_COLLIDER_METHOD.BestFit: createdCollider = ecc.CreateSphereCollider_BestFit(GetWorldVertices(true), GetProperties()); break; case SPHERE_COLLIDER_METHOD.Distance: createdCollider = ecc.CreateSphereCollider_Distance(GetWorldVertices(true), GetProperties()); break; case SPHERE_COLLIDER_METHOD.MinMax: createdCollider = ecc.CreateSphereCollider_MinMax(GetWorldVertices(true), GetProperties()); break; } if (createdCollider != null) { DisableCreatedCollider(createdCollider); } ClearSelectedVertices(); } /// /// Disables a created collider based on preferences /// /// Collider to disable public void DisableCreatedCollider(Collider col) { // keep track fo the collider that was created. CreatedColliders.Add(col); AddedColliderIDs.Add(col.GetInstanceID()); } /// /// Gets all colliders on parent + children if including children. /// /// Array of all colliders private Collider[] GetAllColliders() { if (SelectedGameObject != null) { HashSet selectedColliders = new HashSet(); if (IncludeChildMeshes) { selectedColliders.UnionWith(SelectedGameObject.GetComponentsInChildren()); selectedColliders.UnionWith(AttachToObject.GetComponentsInChildren()); } else { selectedColliders.UnionWith(SelectedGameObject.GetComponents()); selectedColliders.UnionWith(AttachToObject.GetComponents()); // "child meshes" toggle would ennable this, but collider holders don't have meshes, but generally should be selectable. for (int i = 0; i < SelectedGameObject.transform.childCount; i++) { Transform t = SelectedGameObject.transform.GetChild(i); // special names are Rotated X (x = box, capsule), VHACDCollider, and EasyColliderHolder if ((t.name.Contains("Rotated") && t.name.Contains("Collider")) || t.name.Contains("VHACDCollider") || t.name.Contains("EasyColliderHolder")) { // collider holders can also hold rotated colliders, so get children as well. selectedColliders.UnionWith(t.gameObject.GetComponentsInChildren()); } } } selectedColliders.ExceptWith(RaycastableColliders); // Allow selecting colliders from selected gameobject, and it's children, and attach to / it's children. return selectedColliders.ToArray(); } return new Collider[0]; } /// /// Gets all the locations in world space of all MeshFilters vertices. /// /// World space locations of all mesh filters public HashSet GetAllWorldMeshVertices() { bool hasChanged = false; // if the pos, rot, or transform count is different than the mesh filter count we know we need to update. if (WorldMeshPositions.Count != MeshFilters.Count || WorldMeshRotations.Count != MeshFilters.Count || WorldMeshTransforms.Count != MeshFilters.Count) { hasChanged = true; } if (!hasChanged) { // we need to update if any of the mesh transforms have moved, or translated, // or the transform itself is different (ie. 2 different objects with same pos and rotation still means we need to update) foreach (MeshFilter filter in MeshFilters) { if (!WorldMeshPositions.Contains(filter.transform.position)) { hasChanged = true; break; } if (!WorldMeshRotations.Contains(filter.transform.rotation)) { hasChanged = true; break; } if (!WorldMeshTransforms.Contains(filter.transform)) { hasChanged = true; break; } } } // need to recalculate all the world locations. if (hasChanged) { // Clear our lists to rebuild them. WorldMeshVertices.Clear(); WorldMeshPositions.Clear(); WorldMeshRotations.Clear(); WorldMeshTransforms.Clear(); foreach (MeshFilter filter in MeshFilters) { if (filter != null) { Transform t = filter.transform; WorldMeshPositions.Add(t.position); WorldMeshRotations.Add(t.rotation); WorldMeshTransforms.Add(t); Vector3[] vertices = filter.sharedMesh.vertices; foreach (Vector3 vert in vertices) { WorldMeshVertices.Add(t.TransformPoint(vert)); } } } } // nothings changed? just return our hashset of world points. return WorldMeshVertices; } /// /// Gets all the mesh filters on the object. Gets the child meshes if include children is enabled, and creates mesh filters for any skinned mesh renderers if required. /// /// Parent object to get mesh filters from. /// List of mesh filters for the object, children, and skinned mesh renderers. List GetMeshFilters(GameObject go) { if (go == null) return null; List meshFilters = new List(); if (IncludeChildMeshes) { MeshFilter[] childMeshFilters = go.GetComponentsInChildren(false); foreach (MeshFilter childMeshFilter in childMeshFilters) { if (childMeshFilter != null) { meshFilters.Add(childMeshFilter); } } SkinnedMeshRenderer[] childSkinnedMeshRenderers = go.GetComponentsInChildren(false); if (AutoIncludeChildSkinnedMeshes) { foreach (SkinnedMeshRenderer smr in childSkinnedMeshRenderers) { meshFilters.Add(SetupFilterForSkinnedMesh(smr)); } } if (childSkinnedMeshRenderers.Length > 0) { HasSkinnedMeshRenderer = true; } } else { MeshFilter meshFilter = go.GetComponent(); if (meshFilter != null) { meshFilters.Add(meshFilter); } else { SkinnedMeshRenderer smr = go.GetComponent(); if (smr != null) { meshFilters.Add(SetupFilterForSkinnedMesh(smr)); HasSkinnedMeshRenderer = true; } } } return meshFilters; } #if (UNITY_2022_2_OR_NEWER) public LayerMask IncludeLayers; public LayerMask ExcludeLayers; public int LayerOverridePriority; public bool ProvidesContacts; #endif /// /// Creates an EasyColliderProperties based on the ECEditors values. /// /// Orientation property /// EasyColliderProperties to pass to collider creation methods public EasyColliderProperties GetProperties(COLLIDER_ORIENTATION orientation = COLLIDER_ORIENTATION.NORMAL) { EasyColliderProperties ecp = new EasyColliderProperties(); ecp.IsTrigger = IsTrigger; ecp.PhysicMaterial = PhysicMaterial; if (SelectedGameObject != null) { ecp.Layer = ECEPreferences.CopyParentObjectLayer ? AttachToObject.layer : GameObjectLayerOverride; } else { ecp.Layer = GameObjectLayerOverride; } ecp.AttachTo = AttachToObject; ecp.Orientation = orientation; //overrides added in 2022.2. #if (UNITY_2022_2_OR_NEWER) ecp.LayerOverridePriority = LayerOverridePriority; ecp.IncludeLayers = IncludeLayers; ecp.ExcludeLayers = ExcludeLayers; ecp.ProvidesContacts = ProvidesContacts; #endif return ecp; } /// /// Gets all colliders that can be converted for DOTS (ie anything but the mesh colliders added for selection process) /// /// public Collider[] GetConvertibleColliders() { Collider[] colliders = GetAllColliders(); for (int i = 0; i < colliders.Length; i++) { if (AddedInstanceIDs.Contains(colliders[i].GetInstanceID())) { colliders[i] = null; } } return colliders; } /// /// Gets a list of the selected vertices in world space positions. /// /// List of world space positions. public List GetWorldVertices(bool extrude = false) { if (!extrude) { // just return the world verts. List verts = new List(SelectedVertices.Count); foreach (EasyColliderVertex ecv in SelectedVertices) { if (ecv.T == null) continue; verts.Add(ecv.T.TransformPoint(ecv.LocalPosition)); } return verts; } bool extrudeOut = (extrude && ECEPreferences.VertexNormalOffsetType != NORMAL_OFFSET.In && ECEPreferences.VertexNormalOffset != 0); bool extrudeIn = (extrude && ECEPreferences.VertexNormalOffsetType != NORMAL_OFFSET.Out && ECEPreferences.VertexNormalInset != 0); // create list with enough space for all the vertices int count = ((extrudeIn && extrudeOut) ? SelectedVertices.Count * 3 : SelectedVertices.Count) + (extrudeOut ? SelectedVertices.Count : 0) + (extrudeIn ? SelectedVertices.Count : 0); List worldVertices = new List(count); // order of first 3 can matter for rotated colliders, so need to add the actual selected vertices first if needed. if ((!extrudeOut && !extrudeIn) || ECEPreferences.VertexNormalOffsetType == NORMAL_OFFSET.Both) { // both? add selected vertices, otherwise only offset the foreach (EasyColliderVertex ecv in SelectedVertices) { if (ecv.T == null) continue; worldVertices.Add(ecv.T.TransformPoint(ecv.LocalPosition)); } } foreach (EasyColliderVertex ecv in SelectedVertices) { if (ecv.T == null) continue; if (extrudeOut) { worldVertices.Add(ecv.T.TransformPoint(ecv.LocalPosition + ecv.Normal * ECEPreferences.VertexNormalOffset)); } if (extrudeIn) { worldVertices.Add(ecv.T.TransformPoint(ecv.LocalPosition - ecv.Normal * ECEPreferences.VertexNormalInset)); } } return worldVertices; } public List GetNormals() { List normals = new List(SelectedVertices.Count); foreach (EasyColliderVertex ecv in SelectedVertices) { if (ecv.T == null) continue; normals.Add(ecv.Normal); } return normals; } /// /// Grows selected vertices out from all selected vertices /// public void GrowAllSelectedVertices() { GrowVertices(SelectedVerticesSet); } /// /// Grows selected vertices out from all selected vertices until it can no longer grow. /// public void GrowAllSelectedVerticesMax() { int startCount = 0; int currentCount = 0; do { startCount = SelectedVerticesSet.Count; GrowVertices(SelectedVerticesSet); currentCount = SelectedVerticesSet.Count; } while (startCount != currentCount); } /// /// Grows selected vertices out from the last selected vertex(s) /// public void GrowLastSelectedVertices() { HashSet set = new HashSet(); set.UnionWith(LastSelectedVertices); GrowVertices(set); } /// /// Grows selected vertices from the last selected vertices unttil it can no longer be grown. /// public void GrowLastSelectedVerticesMax() { int startCount = 0; int currentCount = 0; do { startCount = SelectedVerticesSet.Count; GrowLastSelectedVertices(); currentCount = SelectedVerticesSet.Count; } while (startCount != currentCount); } /// /// Grows the list of vertices by shared triangles /// /// The list of vertices to expand out from public void GrowVertices(HashSet verticesToGrow) { HashSet newSelectedVertices = new HashSet(); Transform t; Vector3[] vertices; int[] triangles; // Go through every filter & triangle, seems the fastest way to do it without storing the vertices & triangles of every mesh. foreach (MeshFilter filter in MeshFilters) { if (filter == null || filter.sharedMesh == null) continue; triangles = filter.sharedMesh.triangles; vertices = filter.sharedMesh.vertices; t = filter.transform; for (int i = 0; i < triangles.Length; i += 3) { EasyColliderVertex ecv1 = new EasyColliderVertex(t, vertices[triangles[i]]); EasyColliderVertex ecv2 = new EasyColliderVertex(t, vertices[triangles[i + 1]]); EasyColliderVertex ecv3 = new EasyColliderVertex(t, vertices[triangles[i + 2]]); if (verticesToGrow.Contains(ecv1) || verticesToGrow.Contains(ecv2) || verticesToGrow.Contains(ecv3)) { newSelectedVertices.Add(ecv1); newSelectedVertices.Add(ecv2); newSelectedVertices.Add(ecv3); } } } // newly selected vertices are the ones where they are in the new set, but aren't currently in the selected set. HashSet newVertices = new HashSet(newSelectedVertices.Where(value => !SelectedVerticesSet.Contains(value))); SelectVertices(newVertices); } /// /// Checks if the transforms the mesh filters of the currently selected gameobject have moved or rotated /// /// Should the list of transforms be updated? /// True if any of the valid transform meshes are on have moved public bool HasTransformMoved(bool update = false) { bool hasMoved = false; Transform t = null; foreach (MeshFilter filter in MeshFilters) { if (filter == null) { continue; } t = filter.transform; if (filter != null && t != null && !TransformPositions.Contains(t.position) || !TransformRotations.Contains(t.rotation) || !TransformLocalScales.Contains(t.localScale) || !TransformLossyScales.Contains(t.lossyScale)) { hasMoved = true; break; } } if (hasMoved && update) { TransformPositions.Clear(); TransformRotations.Clear(); TransformLocalScales.Clear(); foreach (MeshFilter filter in MeshFilters) { if (filter == null) { continue; } t = filter.transform; if (filter != null) { TransformRotations.Add(t.rotation); TransformPositions.Add(t.position); TransformLocalScales.Add(t.localScale); TransformLossyScales.Add(t.lossyScale); } } } return hasMoved; } /// /// Inverts the currently selected vertices /// public void InvertSelectedVertices() { // just a hash set to add all the vertices to. HashSet invs = new HashSet(); // Variables to hold values Vector3[] vertices; Transform transform; for (int i = 0; i < MeshFilters.Count; i++) { if (MeshFilters[i] != null && MeshFilters[i].sharedMesh != null) { // we only assign vertices array once per filter. transform = MeshFilters[i].transform; vertices = MeshFilters[i].sharedMesh.vertices; for (int j = 0; j < vertices.Length; j++) { invs.Add(new EasyColliderVertex(transform, vertices[j])); } } } invs.UnionWith(SelectedVertices); // select all the vertices (will deselect selected, and select unselected) SelectVertices(invs); } /// /// Checks to see if a collider is already selected /// /// Collider to check /// True if selected public bool IsColliderSelected(Collider collider) { if (SelectedColliders.Contains(collider)) { return true; } return false; } /// /// Checks to make sure the collider is in the list of colliders that were added, or disabled. /// /// Collider to check /// true if selectable public bool IsSelectableCollider(Collider collider) { if (OnlyDeselectableColliders.Contains(collider)) return true; if (GetAllColliders().Contains(collider)) { // we don't want to allow selecting mesh colliders that we've added for vertex selection. // they wouldn't be actually removed, but it makes selecting colliders more difficult if people can. if (AddedInstanceIDs.Contains(collider.GetInstanceID())) return false; return true; } return false; } [SerializeField] private List _SerializedSelectedNonVertexSet = new List(); [SerializeField] private List _SerializedSkinnedMeshRenderers = new List(); [SerializeField] private List _SerializedSkinnedMeshMeshFilters = new List(); /// /// Called after deserializing, used to deserilize our serializable list of selected points back into the hashset. /// Otherwise saving scripts and stuff that reloads domain will break things. /// public void OnAfterDeserialize() { // Deserialize our hashsets. if (_SerializedSelectedVertexSet.Count > 0) { SelectedVerticesSet = new HashSet(_SerializedSelectedVertexSet); } else { SelectedVerticesSet = new HashSet(); } if (_SerializedSelectedNonVertexSet.Count > 0) { SelectedNonVerticesSet = new HashSet(_SerializedSelectedNonVertexSet); } else { SelectedNonVerticesSet = new HashSet(); } if (_SerializedSkinnedMeshRenderers.Count > 0) { _SkinnedMeshFilterPairs = new Dictionary(); for (int i = 0; i < _SerializedSkinnedMeshRenderers.Count; i++) { _SkinnedMeshFilterPairs.Add(_SerializedSkinnedMeshRenderers[i], _SerializedSkinnedMeshMeshFilters[i]); } } } /// /// Called before serialization, used to store our hashset of selected vertices into a serializable list. /// public void OnBeforeSerialize() { // Serialize ours hashsets. if (_SerializedSelectedVertexSet == null) { _SerializedSelectedVertexSet = new List(); } _SerializedSelectedVertexSet = SelectedVerticesSet.ToList(); if (_SerializedSelectedNonVertexSet == null) { _SerializedSelectedNonVertexSet = new List(); } _SerializedSelectedNonVertexSet = SelectedNonVerticesSet.ToList(); if (_SerializedSkinnedMeshMeshFilters == null) { _SerializedSkinnedMeshMeshFilters = new List(); } if (_SerializedSkinnedMeshRenderers == null) { _SerializedSkinnedMeshRenderers = new List(); } _SerializedSkinnedMeshMeshFilters.Clear(); _SerializedSkinnedMeshRenderers.Clear(); foreach (var kvp in _SkinnedMeshFilterPairs) { _SerializedSkinnedMeshRenderers.Add(kvp.Key); _SerializedSkinnedMeshMeshFilters.Add(kvp.Value); } } /// /// Removes all colliders on the currently selected gameobject + attach to, and it's children. /// public void RemoveAllColliders() { // Get colliders from either selected or selected + children. Collider[] colliders = GetAllColliders(); // set selcted colliders SelectedColliders = colliders.ToList(); // remove them. RemoveSelectedColliders(); // traverse children to remove vhacd colliders List vhacdHolders = AttachToObject.GetComponentsInChildren().Where(x => x.gameObject.name.Contains("VHACDColliders")).Select(a => a.gameObject).ToList(); foreach (GameObject obj in vhacdHolders) { // Undo.DestroyObjectImmediate(obj); TryDestroyObject(obj); } } /// /// Removes the currently selected colliders. /// public void RemoveSelectedColliders() { foreach (Collider col in SelectedColliders) { // skip if null, or it's a collider we've added for functionality. if (col == null || AddedInstanceIDs.Contains(col.GetInstanceID())) continue; CreatedColliders.Remove(col); if (col.transform.childCount == 0 && ((col.gameObject.name.Contains("Rotated") && col.gameObject.name.Contains("Collider")) || col.gameObject.name.Contains("VHACDCollider") || col.gameObject.name.Contains("EasyColliderHolder"))) { // is a rotated collider, or a vhacd collider. Collider[] collidersOnRotatedGameObject = col.GetComponents(); bool removeRotated = true; foreach (Collider collider in collidersOnRotatedGameObject) { if (!SelectedColliders.Contains(collider)) { removeRotated = false; break; } } if (removeRotated) { Transform parent = null; if (col.transform.parent != null && col.transform.parent.name.Contains("EasyColliderHolder")) { parent = col.transform.parent; } Undo.RecordObject(col.gameObject, "Remove collider"); Component[] c = col.GetComponents(); if (c.Length == 2) { // don't destroy the collider holder that a user has attached other components to. TryDestroyObject(col.gameObject); } // can remove components from prefab instances, but not the object itself. TryDestroyComponent(col); if (parent != null && parent.childCount == 0) { c = parent.GetComponents(); if (c.Length == 1) { // don't remove collider holder parent that users have attached their own components to. // Undo.DestroyObjectImmediate(parent.gameObject); TryDestroyObject(parent.gameObject); } } } else { // just remove the selected collider. TryDestroyComponent(col); } } else // has children, not a rotated / collider holder or vhacd collider. { Undo.RecordObject(col, "Remove collider"); TryDestroyComponent(col); } } SelectedColliders = new List(); } /// /// Rings around the last 2 selected vertices, selecting all the vertices in the ring. /// public void RingSelectVertices() { if (SelectedVertices.Count < 2) { Debug.LogWarning("Easy Collider Editor: Ring select requires 2 selected vertices."); return; } // last 2 selected vertices must come from the same transform, otherwise you can't really ring around a mesh.. if (SelectedVertices[SelectedVertices.Count - 1].T != SelectedVertices[SelectedVertices.Count - 2].T) { Debug.LogWarning("Easy Collider Editor: Ring select from different transforms not allowed."); return; } // list of all the vertice's were going to add at the end List newVerticesToAdd = new List(); // add the last 2 vertices initially so we know where to end. newVerticesToAdd.Add(SelectedVertices[SelectedVertices.Count - 1]); newVerticesToAdd.Add(SelectedVertices[SelectedVertices.Count - 2]); // get mesh's vertices & triangles. Vector3[] vertices = new Vector3[0]; int[] triangles = new int[3]; Vector3[] normals = new Vector3[0]; Transform t = SelectedVertices[SelectedVertices.Count - 1].T; foreach (MeshFilter filter in MeshFilters) { if (filter == null) continue; if (filter.transform == t) { vertices = filter.sharedMesh.vertices; triangles = filter.sharedMesh.triangles; normals = filter.sharedMesh.normals; } } // start vertex Vector3 currentVertex = SelectedVertices[SelectedVertices.Count - 1].LocalPosition; // previous vertex. Vector3 prevVertex = SelectedVertices[SelectedVertices.Count - 2].LocalPosition; // directon vector for first 2 points. Vector3 currentDirection = (currentVertex - prevVertex).normalized; // Directions for calculations Vector3 directionA, directionB = directionA = Vector3.zero; // points for calculations Vector3 pointA, pointB = pointA = Vector3.zero; // angle from calculations float angleA, angleB = angleA = 0.0f; // best point found in each iteration Vector3 bestPoint = Vector3.zero; // direction fot he best point (from current point) Vector3 bestDirection = Vector3.zero; // best angle from the best point (angle between current direction and best points direction from current point) float bestAngle = Mathf.Infinity; while (true) { // reset best angle for each iteration. bestAngle = Mathf.Infinity; // go through all the triangles. for (int i = 0; i < triangles.Length; i += 3) { // if the triangle doesn't contain both the current and previous vertex. // (as it's by the position, it allows cross edges that match position but not actual vertices' index) if ((vertices[triangles[i]] == currentVertex || vertices[triangles[i + 1]] == currentVertex || vertices[triangles[i + 2]] == currentVertex) && (vertices[triangles[i]] != prevVertex && vertices[triangles[i + 1]] != prevVertex && vertices[triangles[i + 2]] != prevVertex)) { // if it's the first vertex. if (vertices[triangles[i]] == currentVertex) { // set the values for the pointA, pointB, directionA, and directionB to calculate. pointA = vertices[triangles[i + 1]]; pointB = vertices[triangles[i + 2]]; directionA = pointA - currentVertex; directionB = pointB - currentVertex; } else if (vertices[triangles[i + 1]] == currentVertex) { pointA = vertices[triangles[i]]; pointB = vertices[triangles[i + 2]]; directionA = pointA - currentVertex; directionB = pointB - currentVertex; } else if (vertices[triangles[i + 2]] == currentVertex) { pointA = vertices[triangles[i]]; pointB = vertices[triangles[i + 1]]; directionA = pointA - currentVertex; directionB = pointB - currentVertex; } // calculate angles between current direction and the direction to point A and point B. angleA = Vector3.Angle(currentDirection, directionA); angleB = Vector3.Angle(currentDirection, directionB); // if the angle is less than our current best angle, and less than the other triangles angle if (angleA < bestAngle && angleA < angleB) { // set our new best angle, best point, and best direction. bestAngle = angleA; bestPoint = pointA; bestDirection = directionA; } else if (angleB < bestAngle && angleB < angleA) { bestAngle = angleB; bestPoint = pointB; bestDirection = directionB; } } } currentDirection = bestDirection; prevVertex = currentVertex; currentVertex = bestPoint; EasyColliderVertex ecv = new EasyColliderVertex(t, bestPoint); if (newVerticesToAdd.Contains(ecv)) { // reach some kind of end (newest point is already to be added.) break; } else { newVerticesToAdd.Add(ecv); } } // create a hash set from the verts as we need it for the select vertices method which also handles normal smoothing. HashSet newVerts = new HashSet(newVerticesToAdd); // except with the current selected vertices so they dont get deselected when doing a ring. newVerts.ExceptWith(SelectedVerticesSet); // select the vertices. SelectVertices(newVerts); } /// /// Selects or deselects a collider /// /// collider to select or deselect. public void SelectCollider(Collider collider) { // dont try to select a null collider. if (collider == null) return; if (SelectedColliders.Contains(collider)) { SelectedColliders.Remove(collider); OnlyDeselectableColliders.Remove(collider); } else { SelectedColliders.Add(collider); } } public void DeselectAllColliders() { SelectedColliders = new List(); } public void InvertSelection() { HashSet set = new HashSet(); set.UnionWith(GetAllColliders()); set.ExceptWith(SelectedColliders); set.RemoveWhere(x => x.enabled == false || AddedInstanceIDs.Contains(x.GetInstanceID())); SelectedColliders = set.ToList(); } /// /// Selects the gameobject. Sets up the require components based for the object. /// /// GameObject to select void SelectObject(GameObject obj) { // set up mesh filter list MeshFilters = GetMeshFilters(obj); // add / disable rigidbodies + colliders SetRequiredComponentsFrom(obj, MeshFilters); // add display vertices component. if (RenderPointType == RENDER_POINT_TYPE.GIZMOS) { Gizmos = Undo.AddComponent(obj); AddedInstanceIDs.Add(Gizmos.GetInstanceID()); } else if (RenderPointType == RENDER_POINT_TYPE.SHADER) { Compute = Undo.AddComponent(obj); AddedInstanceIDs.Add(Compute.GetInstanceID()); } } /// /// Selects a bunch of vertices at once. /// Also handles smoothing of the selected normals. /// /// Set of vertices public void SelectVertices(HashSet vertices, bool calculateNormals = true) { SelectedNonVerticesSet.ExceptWith(vertices); // removes selected vertices that are in the vertices hashset. (deselects already selected vertices) List stillSelectedVertices = SelectedVertices.Where((value, index) => !vertices.Contains(value)).ToList(); // adds vertices in the vertices set that aren't already selected (selects unselected vertices) List newlySelectedVertices = vertices.Where((value) => !SelectedVerticesSet.Contains(value)).ToList(); // Debug.Log("Select:" + vertices.Count + " Still selected:" + stillSelectedVertices.Count + " New:" + newlySelectedVertices.Count); if (calculateNormals) { // could calculate smooth normals before removing vertices, as that would be more consistently slow. // but then it's the same speed of inverting from all to none. HashSet newSelectedSet = new HashSet(); newSelectedSet.UnionWith(newlySelectedVertices); // calculates the smoothed normals for the selected vertices. Vector3[] verts = new Vector3[0]; Vector3[] normals = new Vector3[0]; MeshFilter mf = null; // dictionary of vert to list of normals found. Dictionary> modified = new Dictionary>(); for (int i = 0; i < MeshFilters.Count; i++) { mf = MeshFilters[i]; if (mf == null || mf.sharedMesh == null) continue; verts = MeshFilters[i].sharedMesh.vertices; normals = MeshFilters[i].sharedMesh.normals; Transform t = mf.transform; for (int j = 0; j < verts.Length; j++) { EasyColliderVertex v = new EasyColliderVertex(t, verts[j]); if (modified.ContainsKey(v)) { // add the normals. modified[v].Add(normals[j]); } else if (newSelectedSet.Contains(v)) { // new vertex, create a list of normals. modified.Add(v, new List() { normals[j] }); } } } // to re-add all the vertices after calculating the normals. newSelectedSet.Clear(); foreach (var item in modified) { Vector3 v = item.Value.Aggregate(new Vector3(0, 0, 0), (cur, val) => cur + val); v.Normalize(); EasyColliderVertex ecv = new EasyColliderVertex(item.Key); ecv.Normal = v; newSelectedSet.Add(ecv); } newlySelectedVertices = newSelectedSet.ToList(); } // last selected are the newly selected vertices. LastSelectedVertices.Clear(); LastSelectedVertices = newlySelectedVertices; // Combine the lists for the all currently selected vertices. stillSelectedVertices.AddRange(newlySelectedVertices); SelectedVertices = stillSelectedVertices; // clear the selected vertices set SelectedVerticesSet.Clear(); // add all the currently selected vertices to it with a union. SelectedVerticesSet.UnionWith(SelectedVertices); } public void EditColliders(List colliders, bool remove = true) { SelectVerticesFromColliders(colliders); if (remove) { RemoveSelectedColliders(); } ColliderSelectEnabled = false; VertexSelectEnabled = true; } /// /// Converts a list of colliders to EasyColliderVertex's and selects them. /// /// public void SelectVerticesFromColliders(List colliders) { EasyColliderCreator ecc = new EasyColliderCreator(); List worldVertices = ecc.GetWorldVertsForColliders(colliders); HashSet worldVerticesSet = new HashSet(); worldVerticesSet.UnionWith(worldVertices); HashSet newSelected = new HashSet(); Vector3[] verts; foreach (MeshFilter mf in _MeshFilters) { if (mf == null) continue; verts = mf.sharedMesh.vertices; Transform tra = mf.transform; foreach (Vector3 p in verts) { Vector3 wp = tra.TransformPoint(p); if (worldVerticesSet.Contains(wp)) { worldVerticesSet.Remove(wp); newSelected.Add(new EasyColliderVertex(tra, p)); } } } Transform t = SelectedGameObject.transform; foreach (Vector3 v in worldVerticesSet) { EasyColliderVertex ecv = new EasyColliderVertex(t, t.InverseTransformPoint(v)); newSelected.Add(ecv); } SelectVertices(newSelected, false); } /// /// Selects or deselects a vertex. Returns true if selected, false if deselected. /// /// Vertex to select /// True if selected, false if deselected. public bool SelectVertex(EasyColliderVertex ecv, bool isVertex) { Vector3 normal = Vector3.zero; int count = 0; // calculate smoothed normal. foreach (MeshFilter mf in MeshFilters) { if (mf == null || mf.transform != ecv.T || mf.sharedMesh == null) continue; Vector3[] vertices = mf.sharedMesh.vertices; Vector3[] normals = mf.sharedMesh.normals; for (int i = 0; i < vertices.Length; i++) { if (vertices[i] == ecv.LocalPosition) { normal += normals[i]; count++; } } } ecv.Normal = normal.normalized; if (SelectedVerticesSet.Remove(ecv)) { // Debug.Log("Removed."); if (!isVertex) { SelectedNonVerticesSet.Remove(ecv); } SelectedVertices.Remove(ecv); return false; } else { // Debug.Log("Not removed."); LastSelectedVertices = new List(); if (!isVertex) { SelectedNonVerticesSet.Add(ecv); } SelectedVerticesSet.Add(ecv); SelectedVertices.Add(ecv); LastSelectedVertices.Add(ecv); return true; } } /// /// Sets the density values on gizmos and compute if needed. /// /// Should density scaling be used? public void SetDensityOnDisplayers(bool useDensityScale) { if (Compute != null) { Compute.DensityScale = DensityScale; } if (Gizmos != null) { Gizmos.DensityScale = Gizmos.UseFixedGizmoScale ? DensityScale * 7.5f : DensityScale; } } /// /// Sets up the required components from the parent to the children (if children are enabled) /// This includes rigidbodies, colliders, and mesh colliders. /// /// /// void SetRequiredComponentsFrom(GameObject parent, List meshFilters) { if (parent == null) return; Rigidbody[] rigidbodies; // get either parent + children or just parent rigidbodies & colliders. if (IncludeChildMeshes) { rigidbodies = parent.GetComponentsInChildren(); } else { rigidbodies = parent.GetComponents(); } // make sure rigidbodies are set to kinematic for raycasting foreach (Rigidbody rb in rigidbodies) { if (!rb.isKinematic && !NonKinematicRigidbodies.Contains(rb)) { Undo.RegisterCompleteObjectUndo(rb, "change isKinmatic"); rb.isKinematic = true; NonKinematicRigidbodies.Add(rb); } } // Add a mesh collider for every mesh filter. foreach (MeshFilter filter in meshFilters) { if (filter != null) { MeshCollider mc = filter.GetComponent(); if (mc != null && mc.enabled && mc.sharedMesh == filter.sharedMesh) { RaycastableColliders.Add(mc); continue; } // don't add a mesh collider if it exists and is the correct mesh. MeshCollider collider = Undo.AddComponent(filter.gameObject); AddedInstanceIDs.Add(collider.GetInstanceID()); RaycastableColliders.Add(collider); } } } void BakeSkinnedMesh(SkinnedMeshRenderer skinnedMesh, Mesh m) { // neither of these work properly 100% of the time, so need to fix skinned mesh baking manually at some point. // #if (UNITY_2020_2_OR_NEWER) // skinnedMesh.BakeMesh(m, true); // #else skinnedMesh.BakeMesh(m); // #endif } /// /// Creates a mesh filter and bakes a mesh for a skinned mesh renderer. /// /// Skinned mesh renderer to create the mesh filter for. /// The mesh filter that was created annd baked. MeshFilter SetupFilterForSkinnedMesh(SkinnedMeshRenderer smr) { // Add a mesh filter and collider to the skinned mesh renderer while we select vertices. MeshFilter filter = smr.GetComponent(); //prevents duplication of un-needed mesh filters. if (smr.transform.childCount > 0) { for (int i = 0; i < smr.transform.childCount; i++) { if (smr.transform.GetChild(i).name == "Scaled Mesh Filter (Temporary)") { filter = smr.transform.GetChild(i).GetComponent(); if (filter != null) { break; } } } } if (filter == null) { // if (smr.transform.localScale != Vector3.one) // { GameObject filterHolder = new GameObject("Scaled Mesh Filter (Temporary)"); filterHolder.transform.parent = smr.transform; filterHolder.transform.localPosition = Vector3.zero; filterHolder.transform.localRotation = Quaternion.identity; AddedInstanceIDs.Add(filterHolder.GetInstanceID()); Undo.RegisterCreatedObjectUndo(filterHolder, "Create MeshFilter"); filter = Undo.AddComponent(filterHolder); // } // else // { // filter = Undo.AddComponent(smr.gameObject); // AddedInstanceIDs.Add(filter.GetInstanceID()); // } } if (filter != null) { if (!_SkinnedMeshFilterPairs.ContainsKey(smr)) { _SkinnedMeshFilterPairs.Add(smr, filter); AddSkinnedMeshBoneTransforms(smr); } AddedInstanceIDs.Add(filter.GetInstanceID()); // Create a new mesh, so we prevent null refs by setting either the collider or filter's shared mesh. Mesh mesh = new Mesh(); // Bake the skinned mesh to the mesh, otherwise you can have offset colliders/filters which aren't correctly located. // smr.BakeMesh(mesh); BakeSkinnedMesh(smr, mesh); // Set the shared mesh's to that mesh. filter.sharedMesh = mesh; // filter.sharedMesh = smr.sharedMesh; // AddedComponentIDs.Add(filter.GetInstanceID()); // reset scale to what it was } return filter; } private void TryDestroyObject(GameObject go, bool undo = true) { #if (UNITY_2018_3_OR_NEWER) if (!PrefabUtility.IsPartOfAnyPrefab(go)) { if (undo) { Undo.DestroyObjectImmediate(go); } else { DestroyImmediate(go); } } #else if (undo) { Undo.DestroyObjectImmediate(go); } else { DestroyImmediate(go); } #endif } private void TryDestroyComponent(Component component) { if (component != null) { Undo.DestroyObjectImmediate(component); } } /// /// Checks added instance IDs and checks if they are mesh filters, also checks if any meshfilters have been deleted /// Re-adds lost meshfilters from Undo-Redos /// public void VerifyMeshFiltersOnUndoRedo() { // fixes bug on lost mesh filter with skinned mesh renderer foreach (int id in AddedInstanceIDs) { #if (UNITY_6000_3_OR_NEWER) Object o = EditorUtility.EntityIdToObject(id); #else Object o = EditorUtility.InstanceIDToObject(id); #endif if (o == null) { continue; } else { if (o is MeshFilter) { MeshFilters.Add(o as MeshFilter); } } } // fix for losing a mesh filter when child meshes are enabled // one is deleted, an operation is done, and undo-redo is done to the point of pre-deletion. // (making sure the count is the same prevents the mesh filter from being permanently lost) bool hasNullMeshFilter = false; foreach (MeshFilter mf in MeshFilters) { if (mf == null) { hasNullMeshFilter = true; } } if (hasNullMeshFilter) { List mfs = GetMeshFilters(SelectedGameObject); if (mfs != null && mfs.Count == MeshFilters.Count) { MeshFilters = mfs; } } } public void MergeSelectedColliders(CREATE_COLLIDER_TYPE mergeTo, bool removeMergedColliders) { EasyColliderCreator ecc = new EasyColliderCreator(); Collider createdCollider = ecc.MergeColliders(SelectedColliders, mergeTo, GetProperties()); if (removeMergedColliders) { RemoveSelectedColliders(); } if (createdCollider != null) { DisableCreatedCollider(createdCollider); } SelectedColliders.Clear(); } public bool SetAttachToOnBoneChange = true; public bool CleanVerticesOnBoneChange = false; public float SelectedBoneWeight = 0.5f; public Transform LastSelectedBone; public List BoneTransforms = new List(); [System.Serializable] /// /// lets us serialize the vertices selected per bone. /// private class SerializableBoneVertexList { public List SelectedVertices = new List(); } [SerializeField] List _selectedBones = new List(); [SerializeField] List _selectedBoneVertices = new List(); Dictionary _SkinnedMeshFilterPairs = new Dictionary(); void AddSkinnedMeshBoneTransforms(SkinnedMeshRenderer renderer) { // fix issue if renderer does not actually have a mesh assigned. if (renderer.sharedMesh == null) return; HashSet validBoneIndexes = new HashSet(); #if UNITY_2019_1_OR_NEWER Transform transform = renderer.transform; Vector3[] vertices = renderer.sharedMesh.vertices; Unity.Collections.NativeArray weights = renderer.sharedMesh.GetAllBoneWeights(); Unity.Collections.NativeArray bonesPerVertex = renderer.sharedMesh.GetBonesPerVertex(); int boneWeightIndex = 0; for (int vertIndex = 0; vertIndex < vertices.Length; vertIndex++) { if (vertIndex <= bonesPerVertex.Length) { int numBonesForVertex = bonesPerVertex[vertIndex]; for (int i = 0; i < numBonesForVertex; i++) { if (boneWeightIndex <= weights.Length) { BoneWeight1 item = weights[boneWeightIndex]; if (item.boneIndex >= 0 && item.weight >= 0) { validBoneIndexes.Add(item.boneIndex); } boneWeightIndex++; } else { break; } } } else { break; } } #else BoneWeight[] weights = renderer.sharedMesh.boneWeights; for (int i = 0; i < weights.Length; i++) { BoneWeight item = weights[i]; if (item.boneIndex0 >= 0 && item.weight0 >= 0) { validBoneIndexes.Add(item.boneIndex0); } if (item.boneIndex1 >= 0 && item.weight1 >= 0) { validBoneIndexes.Add(item.boneIndex1); } if (item.boneIndex2 >= 0 && item.weight2 >= 0) { validBoneIndexes.Add(item.boneIndex2); } if (item.boneIndex3 >= 0 && item.weight3 >= 0) { validBoneIndexes.Add(item.boneIndex3); } } #endif List bonesWithAtLeastOneWeightedVertex = new List(); // add them in order. List validBoneIndexList = validBoneIndexes.ToList(); validBoneIndexList.Sort(); if (renderer.bones != null && renderer.bones.Length > 0) { foreach (var index in validBoneIndexList) { // occurs if a smr is optimized and bones are hidden. if (index >= 0 && index < renderer.bones.Length) { bonesWithAtLeastOneWeightedVertex.Add(renderer.bones[index]); } } } BoneTransforms.AddRange(bonesWithAtLeastOneWeightedVertex); } public void ClearVerticesForBone(Transform bone) { int boneIndex = _selectedBones.IndexOf(bone); if (boneIndex >= 0) { var selectedVertsForBone = _selectedBoneVertices[boneIndex].SelectedVertices; HashSet selectedVertSet = new HashSet(selectedVertsForBone); // because they can technically be cleared already, so clearing and invert would break things. selectedVertSet.IntersectWith(_SelectedVerticesSet); SelectVertices(selectedVertSet, true); _selectedBones.RemoveAt(boneIndex); _selectedBoneVertices.RemoveAt(boneIndex); } } public void SelectVerticesForBone(Transform bone, float weightCutoff) { LastSelectedBone = bone; SkinnedMeshRenderer[] smrs = SelectedGameObject.GetComponentsInChildren(); SkinnedMeshRenderer smr = null; foreach (var item in smrs) { if (item.bones.Contains(bone)) { smr = item; break; } } if (smr == null) return; Transform[] bones = smr.bones; int boneIndex = -1; for (int i = 0; i < bones.Length; i++) { if (bones[i] == bone) { boneIndex = i; break; } } if (_SkinnedMeshFilterPairs.ContainsKey(smr)) { Transform transform = _SkinnedMeshFilterPairs[smr].transform; Vector3[] vertices = _SkinnedMeshFilterPairs[smr].sharedMesh.vertices; HashSet verts = new HashSet(); #if UNITY_2019_1_OR_NEWER Unity.Collections.NativeArray weights = smr.sharedMesh.GetAllBoneWeights(); Unity.Collections.NativeArray bonesPerVertex = smr.sharedMesh.GetBonesPerVertex(); int boneWeightIndex = 0; for (int vertIndex = 0; vertIndex < vertices.Length; vertIndex++) { int numBonesForVertex = bonesPerVertex[vertIndex]; for (int i = 0; i < numBonesForVertex; i++) { BoneWeight1 item = weights[boneWeightIndex]; if (item.boneIndex == boneIndex && item.weight >= weightCutoff) { verts.Add(new EasyColliderVertex(transform, vertices[vertIndex])); } boneWeightIndex++; } } #else BoneWeight[] weights = smr.sharedMesh.boneWeights; for (int i = 0; i < weights.Length; i++) { BoneWeight item = weights[i]; if (item.boneIndex0 == boneIndex && item.weight0 >= weightCutoff) { verts.Add(new EasyColliderVertex(transform, vertices[i])); } if (item.boneIndex1 == boneIndex && item.weight1 >= weightCutoff) { verts.Add(new EasyColliderVertex(transform, vertices[i])); } if (item.boneIndex2 == boneIndex && item.weight2 >= weightCutoff) { verts.Add(new EasyColliderVertex(transform, vertices[i])); } if (item.boneIndex3 == boneIndex && item.weight3 >= weightCutoff) { verts.Add(new EasyColliderVertex(transform, vertices[i])); } } #endif // deselect any previously selected vertices for that bone, then reselect. int selectedBoneIndex = _selectedBones.IndexOf(bone); HashSet vertsAlreadySelectedForBone = new HashSet(); if (selectedBoneIndex >= 0) { // we've selected this bone already, so we want the vertices we selected for it // and then we need only the ones that are still selected with the intersect // then we select them to deselect the currently selected ones, so that // the new selection only selects the appropriate weight vertsAlreadySelectedForBone.UnionWith(_selectedBoneVertices[selectedBoneIndex].SelectedVertices); _selectedBoneVertices[selectedBoneIndex].SelectedVertices = verts.ToList(); } else { _selectedBones.Add(bone); _selectedBoneVertices.Add(new SerializableBoneVertexList() { SelectedVertices = verts.ToList() }); // we dont want to end up deselecting vertices that are already selected, but we do want to store them as verts for this bone // so we will be selecting the ones already selected to deselect them, so they can then be selected vertsAlreadySelectedForBone.UnionWith(verts); } if (vertsAlreadySelectedForBone.Count > 0) { vertsAlreadySelectedForBone.IntersectWith(_SelectedVerticesSet); SelectVertices(vertsAlreadySelectedForBone, true); } SelectVertices(verts, true); // Debug.Log("Verts count:" + verts.Count + " Selected now:" + SelectedVerticesSet.Count); } } } } #endif