using System.Collections.Generic; using UnityEngine; using UnityEditor; using System.Linq; using System.Collections; #if UNITY_2019_1_OR_NEWER using Unity.Collections; #endif using System; namespace ECE { // TODO: // Convex mesh colliders can fail silently on primarily badly scaled skinned meshes. (Once in local space, the distances are too small for quickhull.) // have tried slowly reducing calculated epsilon, but that does not work either. // best option is to add a faq to documentation, and explain that scaling from some models (particularily blender it seems) // exports with 100,100,100 scaling on the root // NOTE: limit by child bone distance exprimented with, does not function well compared to depenetration methods. // would get vertex distance and closest axis and check distance and toss vertices over the distance to prevent overlap. This does not work well. [System.Serializable] public class EasyColliderAutoSkinned : ScriptableObject, ISerializationCallbackReceiver { // All these proprties added to work without preferences file // values can be set from your own script. // default values should be the same as in preferences. SKINNED_MESH_DEPENETRATE_ORDER _autoSkinnedDepenetrateOrder; public SKINNED_MESH_DEPENETRATE_ORDER AutoSkinnedDepenetrateOrder { get { #if (UNITY_EDITOR) return Application.isPlaying ? _autoSkinnedDepenetrateOrder : Preferences.AutoSkinnedDepenetrateOrder; #else return _autoSkinnedDepenetrateOrder; #endif } set { _autoSkinnedDepenetrateOrder = value; } } bool _autoSkinnedForce256Triangles = true; public bool AutoSkinnedForce256Triangles { get { #if (UNITY_EDITOR) return Application.isPlaying ? _autoSkinnedForce256Triangles : Preferences.AutoSkinnedForce256Triangles; #else return _autoSkinnedForce256Triangles; #endif } set { _autoSkinnedForce256Triangles = value; } } float _autoSkinnedMinBoneWeight = 0.5f; public float AutoSkinnedMinBoneWeight { get { #if (UNITY_EDITOR) return Application.isPlaying ? _autoSkinnedMinBoneWeight : Preferences.AutoSkinnedMinBoneWeight; #else return _autoSkinnedMinBoneWeight; #endif } set { _autoSkinnedMinBoneWeight = value; } } float _autoSkinnedMinRealignAngle; public float AutoSkinnedMinRealignAngle { get { #if (UNITY_EDITOR) return Application.isPlaying ? _autoSkinnedMinRealignAngle : Preferences.AutoSkinnedMinRealignAngle; #else return _autoSkinnedMinRealignAngle; #endif } set { _autoSkinnedMinRealignAngle = value; } } bool _autoSkinnedAllowRealign; public bool AutoSkinnedAllowRealign { get { #if (UNITY_EDITOR) return Application.isPlaying ? _autoSkinnedAllowRealign : Preferences.AutoSkinnedAllowRealign; #else return _autoSkinnedAllowRealign; #endif } set { _autoSkinnedAllowRealign = value; } } SKINNED_MESH_COLLIDER_TYPE _autoSkinnedColliderType = SKINNED_MESH_COLLIDER_TYPE.Box; public SKINNED_MESH_COLLIDER_TYPE AutoSkinnedColliderType { get { #if (UNITY_EDITOR) return Application.isPlaying ? _autoSkinnedColliderType : Preferences.AutoSkinnedColliderType; #else return _autoSkinnedColliderType; #endif } set { _autoSkinnedColliderType = value; } } bool _autoSkinnedDepenetrate; public bool AutoSkinnedDepenetrate { get { #if (UNITY_EDITOR) return Application.isPlaying ? _autoSkinnedDepenetrate : Preferences.AutoSkinnedDepenetrate; #else return _autoSkinnedDepenetrate; #endif } set { _autoSkinnedDepenetrate = value; } } bool _autoSkinnedIndents = true; public bool AutoSkinnedIndents { get { #if (UNITY_EDITOR) return Application.isPlaying ? _autoSkinnedIndents : Preferences.AutoSkinnedIndents; #else return _autoSkinnedIndents; #endif } set { _autoSkinnedIndents = value; } } int _autoSkinnedIterativeDepenetrationCount = 15; public int AutoSkinnedIterativeDepenetrationCount { get { #if (UNITY_EDITOR) return Application.isPlaying ? _autoSkinnedIterativeDepenetrationCount : Preferences.AutoSkinnedIterativeDepenetrationCount; #else return _autoSkinnedIterativeDepenetrationCount; #endif } set { _autoSkinnedIterativeDepenetrationCount = value; } } bool _autoSkinnedPairing = true; public bool AutoSkinnedPairing { get { #if (UNITY_EDITOR) return Application.isPlaying ? _autoSkinnedPairing : Preferences.AutoSkinnedPairing; #else return _autoSkinnedPairing; #endif } set { _autoSkinnedPairing = value; } } bool _autoSkinnedPerBoneSettings; public bool AutoSkinnedPerBoneSettings { get { #if (UNITY_EDITOR) return Application.isPlaying ? _autoSkinnedPerBoneSettings : Preferences.AutoSkinnedPerBoneSettings; #else return _autoSkinnedPerBoneSettings; #endif } set { _autoSkinnedPerBoneSettings = value; } } float _autoSkinnedShrinkAmount = 0.5f; public float AutoSkinnedShrinkAmount { get { #if (UNITY_EDITOR) return Application.isPlaying ? _autoSkinnedShrinkAmount : Preferences.AutoSkinnedShrinkAmount; #else return _autoSkinnedShrinkAmount; #endif } set { _autoSkinnedShrinkAmount = value; } } bool _autoSkinnedUseDistanceDeltaPairing = true; public bool AutoSkinnedUseDistanceDeltaPairing { get { #if (UNITY_EDITOR) return Application.isPlaying ? _autoSkinnedUseDistanceDeltaPairing : Preferences.AutoSkinnedUseDistanceDeltaPairing; #else return _autoSkinnedUseDistanceDeltaPairing; #endif } set { _autoSkinnedUseDistanceDeltaPairing = value; } } float _autoSkinnedPairedDistanceDelta = 0.01f; public float AutoSkinnedPairedDistanceDelta { get { #if (UNITY_EDITOR) return Application.isPlaying ? _autoSkinnedPairedDistanceDelta : Preferences.AutoSkinnedPairedDistanceDelta; #else return _autoSkinnedPairedDistanceDelta; #endif } set { _autoSkinnedPairedDistanceDelta = value; } } #if (UNITY_EDITOR) private EasyColliderPreferences _Preferences; private EasyColliderPreferences Preferences { get { if (_Preferences == null) { _Preferences = EasyColliderPreferences.Preferences; } return _Preferences; } } #endif /// /// Cleans up the data. /// public void Clean() { BoneList = new List(); SortedBoneList = new List(); renderer = null; transformHashCode = -1; _selectedBone = null; _initialScannedObject = null; } EasyColliderAutoSkinnedBone _selectedBone; public void SetSelectedBone(EasyColliderAutoSkinnedBone bone) { _selectedBone = bone; } public EasyColliderAutoSkinnedBone GetSelectedBone() { return _selectedBone; } GameObject _initialScannedObject; public GameObject GetInitialScannedObject() { return _initialScannedObject; } [SerializeField] /// /// List of bones on the current skinned mesh. In the same order as the skinnedmesh's skinnedMesh.bones transform array. /// /// /// public List BoneList = new List(); [SerializeField] /// /// hierarchical sorted bones of the current skinned mesh. /// /// /// public List SortedBoneList = new List(); /// /// Hashcode of the position, rotation, and scale of the transform to more easily detect movement and update preview /// public int transformHashCode; /// /// current skinned mesh renderer /// public SkinnedMeshRenderer renderer; /// /// Checks if the preview needs updating because the root of the skinned mesh has moved/rotated/scaled. /// /// public bool HasSkinnedMeshRendererTransformed() { if (renderer == null) return false; int newHash = renderer.transform.position.GetHashCode() + renderer.transform.rotation.GetHashCode() + renderer.transform.lossyScale.GetHashCode(); if (transformHashCode != newHash) { transformHashCode = newHash; return true; } return false; } /// /// Goes through all the bones in the bone list and makes sure at least one bone weight on the skinned mesh is above it's threshold. If so, marks that bone as valid. /// /// /// private void SetBoneValidity(SkinnedMeshRenderer smr, List boneList) { BoneWeight[] boneWeights = smr.sharedMesh.boneWeights; foreach (var b in boneWeights) { if (b.boneIndex0 >= 0 && b.weight0 > 0) boneList[b.boneIndex0].IsValid = true; if (b.boneIndex1 >= 0 && b.weight1 > 0) boneList[b.boneIndex1].IsValid = true; if (b.boneIndex2 >= 0 && b.weight2 > 0) boneList[b.boneIndex2].IsValid = true; if (b.boneIndex3 >= 0 && b.weight3 > 0) boneList[b.boneIndex3].IsValid = true; } } /// /// Does an initial scan of bones to create the bone list, sorted bone list, pairs them, and sets initial weights /// /// /// public void InitialScanBones(GameObject selectedObject, float weight = 0.5f) { // Debug.Log("InitialScan." + Preferences.SkinnedMeshColliderType); SkinnedMeshRenderer smr = selectedObject.GetComponentInChildren(); _initialScannedObject = selectedObject; if (smr == null) return; EasyColliderAutoSkinnedBone[] boneArray = GetSkinnedMeshBones(smr); if (boneArray == null) return; BoneList = boneArray.ToList(); if (BoneList.Count == 0) return; SetBoneValidity(smr, BoneList); // set initial bone weights. foreach (var smbs in BoneList) { smbs.BoneWeight = weight; } List boneTransforms = smr.bones.ToList(); // pair bones up PairBones(BoneList, boneTransforms); // smr has a root property, so we're good. if (smr.rootBone != null) { Transform root = smr.rootBone; // calculate indent levels if (root.parent != null) { SetIndentRecursive(root.parent, boneTransforms, 0); } else { SetIndentRecursive(root, boneTransforms, 0); } // calculate the sorted bone list. (use root's parent if possible, as there can be multiple "root" bones, so without it some wouldn't get detected.) if (root.parent != null) { SortedBoneList = SortBonesRecursive(root.parent, boneTransforms, BoneList, new List()); } else { SortedBoneList = SortBonesRecursive(root, boneTransforms, BoneList, new List()); } } else { // rootBone property of the skinned mesh is null, which would normally cause issues // so we need to identify the top-level bones, then indent them all, and sort them. List rootBones = IdentifyRootBones(BoneList); foreach (var b in rootBones) { SetIndentRecursive(b.Transform, boneTransforms, 0); } SortedBoneList = NoRootBoneSort(rootBones, boneTransforms); } } /// /// Identifies the "root bones" (the top-level bones) in a bone list. Used in cases where the skinned mesh renderers rootBone property is null. /// /// /// List IdentifyRootBones(List boneList) { HashSet examinedBones = new HashSet(); // without a root bone, find the "toplevel" bones. List rootBones = new List(); foreach (var b in boneList) { if (b.Transform == null) continue; if (examinedBones.Contains(b)) continue; examinedBones.Add(b); EasyColliderAutoSkinnedBone bone = b; bool hasParent = true; while (hasParent) { hasParent = false; Transform parent = bone.Transform.parent; if (parent != null) { foreach (var a in boneList) { if (a.Transform == parent) { if (rootBones.Contains(a) || examinedBones.Contains(a)) { // already added it's parent as a root, so stop looking bone = null; hasParent = false; break; } else { // is a new parent, keep goin. bone = a; hasParent = true; break; } } } } } examinedBones.Add(bone); if (bone != null) { rootBones.Add(bone); } } return rootBones; } /// /// Sorts the bones based on a list of identified rootbones in cases where the skinned mesh renderer's root bone has been remvoed. /// /// identified top level bones /// all bone transforms /// list of bones sorted similar to the unity hierarchy. List NoRootBoneSort(List rootBones, List boneTransforms) { var sortedBones = new List(); foreach (var b in rootBones) { // SetIndentRecursive(b.Transform, boneTransforms, 0); List sortedBonesForBone = new List(); SortBonesRecursive(b.Transform, boneTransforms, BoneList, sortedBonesForBone); sortedBones.AddRange(sortedBonesForBone); } return sortedBones; } /// /// Goes through the boneList and pairs bones togehter by looking at the number of children to determine if theres multiple offshoots (arms/legs) /// then pairing offshoots based on bone-transform chain length. /// /// /// private void PairBones(List boneList, List boneTransforms) { foreach (var smb in boneList) { // skip already paired bones, NOT invalid ones because invalid ones can still have children that are valid and need to be paired. if (smb.IsPaired) continue; if (smb.Transform == null) continue; int childCount = smb.Transform.childCount; // count the number of valid (is a bone) direct children that this bone has. List validChildren = new List(); for (int i = 0; i < childCount; i++) { if (boneTransforms.Contains(smb.Transform.GetChild(i))) { validChildren.Add(i); } } // more than one child on a bone, means it has multiple offshoots (like arms/legs off the back / hips) if (validChildren.Count > 1) { // calculate the number of valid bones each offshoot has. // we are identifying which bone offshoots match with other offshoots by the total valid bone length. List perChainTransformCount = new List(new int[validChildren.Count]); List perChainDistances = new List(new float[validChildren.Count]); for (int i = 0; i < validChildren.Count; i++) { Transform child = smb.Transform.GetChild(validChildren[i]); // skip already paired bones. // get all children and count which ones are actually bones. List childTransforms = child.GetComponentsInChildren().ToList(); int childsValidBoneCount = 0; foreach (var t in childTransforms) { if (boneTransforms.Contains(t)) { childsValidBoneCount++; } perChainDistances[i] = perChainDistances[i] + Vector3.Distance(child.position, t.position); } perChainTransformCount[i] = childsValidBoneCount; } // go through each child and see if the offshoots can be paired, and pair them. for (int i = 0; i < validChildren.Count; i++) { // get the index of the current bone we are looking at. int index = boneTransforms.IndexOf(smb.Transform.GetChild(i)); // if the bone is already paired, skip it. if (index < 0 || boneList[index].IsPaired) continue; // identify bones that have similar count in their bone chain using the perChainTransformCount. List pairedBones = new List(); for (int j = 0; j < validChildren.Count; j++) { if (j == i) continue; // same number of transforms? likely to be a pair. bool isPaired = perChainTransformCount[i] == perChainTransformCount[j]; if (AutoSkinnedUseDistanceDeltaPairing) { // works, and simplifying to only using the actual transform works less well than using full chain distance. if (isPaired && (perChainDistances[i] == perChainDistances[j] || Mathf.Abs(perChainDistances[i] - perChainDistances[j]) < AutoSkinnedPairedDistanceDelta)) { isPaired = true; } else if (isPaired) { isPaired = false; } } if (isPaired) { // bone pair found! Transform c1 = smb.Transform.GetChild(j); int i1 = boneTransforms.IndexOf(c1); // make sure index > 0 if (i1 >= 0) { pairedBones.Add(i1); } } } // if we have a paired bone, pair it up (and their children!) if (pairedBones.Count > 0) { // mark the current bone as paired. boneList[index].PairedBones = pairedBones; boneList[index].IsPaired = true; // mark each bone as paired. foreach (var pIndex in pairedBones) { boneList[pIndex].IsPaired = true; } // we need to pair the children as well, create a list of transform's including itself and it's children. List bChildren = boneList[index].Transform.GetComponentsInChildren().ToList(); // list to contain each paired-bone's list of transforms. List> pairedChildren = new List>(); // create the paired children list, doing the same thing as for the original. foreach (var pIndex in pairedBones) { pairedChildren.Add(boneTransforms[pIndex].GetComponentsInChildren().ToList()); } for (int j = 0; j < bChildren.Count; j++) { // get the bone transform index of the child we're looking at. int bIndex = boneTransforms.IndexOf(bChildren[j]); // skip non-bone transforms. if (bIndex < 0) continue; // pair the bones. List cPairedBones = new List(); foreach (var tList in pairedChildren) { // pair only the matching indexs in each transform list. int pIndex = boneTransforms.IndexOf(tList[j]); if (pIndex < 0) continue; // add them as a paired bonelist index, and mark the current one as paired. cPairedBones.Add(pIndex); boneList[pIndex].IsPaired = true; } // if we paired this child transform with another transform. if (cPairedBones.Count > 0) { // mark it as paired, set it's paired list, and set it as the display bone. boneList[bIndex].IsPaired = true; boneList[bIndex].PairedBones = cPairedBones; boneList[bIndex].IsPairDisplayBone = true; } } } } } } } /// /// Recursively sorts bones in the boneList into the sortedList to match with how they would be displayed in the scene hierarchy. /// /// initially the skinned mesh's root bone. /// the skinned mesh renderers bones /// /// /// Sorted list of bones. List SortBonesRecursive(Transform current, List boneTransforms, List boneList, List sortedList) { int index = boneTransforms.IndexOf(current); if (index >= 0) { EasyColliderAutoSkinnedBone smb = BoneList[index]; smb.BoneName = current.name; sortedList.Add(smb); for (int i = 0; i < current.childCount; i++) { Transform child = current.GetChild(i); SortBonesRecursive(child, boneTransforms, boneList, sortedList); } } else { for (int i = 0; i < current.childCount; i++) { Transform child = current.GetChild(i); SortBonesRecursive(child, boneTransforms, boneList, sortedList); } } return sortedList; } /// /// Recursively calculates the index level of each bone. /// /// initially the skinned mesh's root bone. /// /// void SetIndentRecursive(Transform current, List boneTransforms, int currentIndent) { int index = boneTransforms.IndexOf(current); if (index >= 0) { // only set the indent and increase the indent if the bone is valid (has at least 1 skinned vertex); if (BoneList[index].IsValid) { BoneList[index].IndentLevel = currentIndent; currentIndent += 1; } for (int i = 0; i < current.childCount; i++) { SetIndentRecursive(current.GetChild(i), boneTransforms, currentIndent); } } else { for (int i = 0; i < current.childCount; i++) { SetIndentRecursive(current.GetChild(i), boneTransforms, currentIndent); } } } /// /// (Undoable) Creates a gameobject as a child of parent, with it's forward axis pointed towards the child, and its up axis at the cross of the new forward and the parent's up direction. /// /// parent transform /// child transform /// Gameobject at parent's location with it's forward axis pointing towards child. public GameObject CreateRealignedObject(Transform parent, Transform child, bool forPreview = false) { // realign with a rotated gameboject. GameObject obj = new GameObject(parent.transform.name + "_RotatedCollider"); Vector3 childDir = child.position - parent.position; // use either the bone's right or the the bone's forward, whichever isn't the same as the child's direction in the calculation. Vector3 right = parent.transform.right; if (childDir == right) { right = parent.transform.forward; } Vector3 up = Vector3.Cross(childDir, right); obj.transform.rotation = Quaternion.LookRotation(childDir, up); obj.transform.SetParent(parent.transform); obj.layer = obj.transform.parent.gameObject.layer; obj.transform.localPosition = Vector3.zero; if (!forPreview) { #if (UNITY_EDITOR) Undo.RegisterCreatedObjectUndo(obj, "Create realign bone object"); #endif } return obj; } /// /// gets the TRS that represents the transform from the parent to the child as the forward direction. /// /// /// /// public Matrix4x4 GetMatrixForObject(Transform parent, Transform child) { Vector3 childDir = child.position - parent.position; Vector3 right = parent.transform.right; if (childDir == right) { right = parent.transform.forward; } Vector3 up = Vector3.Cross(childDir, right); Quaternion q = Quaternion.LookRotation(childDir, up); return Matrix4x4.TRS(parent.position, q, Vector3.one); } private List ToLocalVerts(Transform transform, List worldVertices) { List localVerts = new List(worldVertices.Count); foreach (Vector3 v in worldVertices) { localVerts.Add(transform.InverseTransformPoint(v)); } return localVerts; } /// /// Creates and attaches a collider of given type /// /// type of collider /// properties of collider /// data to create collider with /// save path for mesh colliders private Collider GenerateCollider(SKINNED_MESH_COLLIDER_TYPE colliderType, EasyColliderProperties properties, EasyColliderAutoSkinnedBone s, string savePath, bool forPreview = false) { EasyColliderCreator ecc = new EasyColliderCreator(); #if(UNITY_EDITOR) if (forPreview) { ecc.UndoEnabled = false; } #endif switch (colliderType) { case SKINNED_MESH_COLLIDER_TYPE.Box: return ecc.CreateBoxCollider(s.WorldSpaceVertices, properties); case SKINNED_MESH_COLLIDER_TYPE.Capsule: return ecc.CreateCapsuleCollider_MinMax(s.WorldSpaceVertices, properties, CAPSULE_COLLIDER_METHOD.MinMax); case SKINNED_MESH_COLLIDER_TYPE.Sphere: return ecc.CreateSphereCollider_MinMax(s.WorldSpaceVertices, properties); case SKINNED_MESH_COLLIDER_TYPE.Convex_Mesh: s.WorldSpaceVertices = ToLocalVerts(s.Transform, s.WorldSpaceVertices); EasyColliderQuickHull qh = EasyColliderQuickHull.CalculateHull(s.WorldSpaceVertices); Mesh m = qh.Result; if (AutoSkinnedForce256Triangles && m != null) { if (m.triangles.Length / 3 > 255) { // weld vertices to an estimated target, then recalculate the hull, and repeat until // we get a value, or we reach n, which is a failsafe. int n = 0; float weldDistance = 0.0f; while (m.triangles.Length / 3 > 255 && n < 25) { weldDistance = ReduceMeshVerticesOnBone(s, m, weldDistance); qh = EasyColliderQuickHull.CalculateHull(s.WorldSpaceVertices); m = qh.Result; n++; } } } if (m != null) { #if (UNITY_EDITOR) if (Preferences.SaveConvexHullAsAsset && !forPreview) { EasyColliderSaving.CreateAndSaveMeshAsset(qh.Result, s.Transform.gameObject); } #endif // ecc.CreateMesh_QuickHull(s.WorldSpaceVertices, s.Transform.gameObject); return ecc.CreateConvexMeshCollider(qh.Result, s.Transform.gameObject, properties); } break; } return null; } public void ChangeBoneEnabled(EasyColliderAutoSkinnedBone bone, bool enabled, bool includeChildren) { bone.Enabled = enabled; if (includeChildren) { foreach (var b in BoneList) { if (b.Transform.IsChildOf(bone.Transform)) { b.Enabled = enabled; } } } if (AutoSkinnedPairing) { foreach (var index in bone.PairedBones) { ChangeBoneEnabled(BoneList[index], enabled, includeChildren); } } } public void ChangeBoneWeight(EasyColliderAutoSkinnedBone bone, float weight, bool includeChildren) { bone.BoneWeight = weight; if (includeChildren) { foreach (var b in BoneList) { if (b.Transform.IsChildOf(bone.Transform)) { b.BoneWeight = weight; } } } if (AutoSkinnedPairing) { foreach (var index in bone.PairedBones) { ChangeBoneWeight(BoneList[index], weight, includeChildren); } } } public void ChangeBoneColliderType(EasyColliderAutoSkinnedBone bone, SKINNED_MESH_COLLIDER_TYPE colliderType, bool includeChildren) { bone.ColliderType = colliderType; if (includeChildren) { foreach (var b in BoneList) { if (b.Transform.IsChildOf(bone.Transform)) { b.ColliderType = colliderType; } } } if (AutoSkinnedPairing) { foreach (var index in bone.PairedBones) { ChangeBoneColliderType(BoneList[index], colliderType, includeChildren); } } } /// /// Estimates a target number of vertices to reach under 256 triangles and merges vertices together until that target is reached. /// returns the final weld distance used to reach below the estimated target. /// /// bone /// quickhull calculated mesh /// previous weld distance from this method, or 0 /// weld distance used to reach target vertices. private float ReduceMeshVerticesOnBone(EasyColliderAutoSkinnedBone bone, Mesh m, float prevWeldDist) { // Tends to merge vertices at the start over those at the end, as we detect when we've reached enough and then add the rest. List vertices = new List(); HashSet weldedVerts = new HashSet(); int targetVerts = (int)(256.0f * ((float)bone.WorldSpaceVertices.Count / ((float)m.triangles.Length / 3))); if (targetVerts >= bone.WorldSpaceVertices.Count || (targetVerts > bone.WorldSpaceVertices.Count * 0.9f)) { targetVerts = (int)(bone.WorldSpaceVertices.Count * 0.9f); } int currentVerts = bone.WorldSpaceVertices.Count; float sizeMagnitude = m.bounds.size.magnitude; float weldDistance = (sizeMagnitude) / 50f; if (weldDistance < prevWeldDist) { weldDistance = prevWeldDist + (weldDistance * 0.25f); } // weld close vertices. (n as a failsafe.) int n = 0; while (targetVerts < bone.WorldSpaceVertices.Count && n < 25) { int iterCount = bone.WorldSpaceVertices.Count; for (int i = 0; i < bone.WorldSpaceVertices.Count; i++) { // if we break when we've reached the target, // since the method is also called iterative from an outer loop, we tend to only weld the initial vertices. // so instead we'll try to weld the whole thing int weldCount = 1; Vector3 v1 = bone.WorldSpaceVertices[i]; if (weldedVerts.Contains(v1)) continue; for (int j = i; j < bone.WorldSpaceVertices.Count; j++) { Vector3 v2 = bone.WorldSpaceVertices[j]; if (weldedVerts.Contains(v2)) continue; if (Vector3.Distance(v1, v2) < weldDistance) { v1 = ((v1 * weldCount) + v2) / (weldCount + 1); weldCount++; weldedVerts.Add(v2); weldedVerts.Add(v1); } } vertices.Add(v1); } weldDistance *= 1.25f; weldedVerts.Clear(); bone.WorldSpaceVertices = vertices; vertices = new List(); n++; } return weldDistance; } private List ToLocalVerts(List worldVertices, Matrix4x4 m) { List localVerts = new List(worldVertices.Count); Matrix4x4 inverse = m.inverse; foreach (Vector3 v in worldVertices) { localVerts.Add(inverse.MultiplyPoint3x4(v)); } return localVerts; } private EasyColliderData CalculateColliderData(SKINNED_MESH_COLLIDER_TYPE colliderType, EasyColliderAutoSkinnedBone s, EasyColliderProperties properties) { EasyColliderCreator ecc = new EasyColliderCreator(); List localVertices = ToLocalVerts(s.WorldSpaceVertices, s.Matrix); switch (colliderType) { case SKINNED_MESH_COLLIDER_TYPE.Box: BoxColliderData bd = ecc.CalculateBoxLocal(localVertices); bd.Matrix = s.Matrix; return bd; case SKINNED_MESH_COLLIDER_TYPE.Capsule: CapsuleColliderData cd = ecc.CalculateCapsuleMinMaxLocal(localVertices, CAPSULE_COLLIDER_METHOD.MinMax); cd.Matrix = s.Matrix; return cd; case SKINNED_MESH_COLLIDER_TYPE.Sphere: SphereColliderData scd = ecc.CalculateSphereMinMaxLocal(localVertices); scd.Matrix = s.Matrix; return scd; case SKINNED_MESH_COLLIDER_TYPE.Convex_Mesh: MeshColliderData mcd = ecc.CalculateMeshColliderQuickHullLocal(localVertices); mcd.Matrix = s.Matrix; return mcd; } return new EasyColliderData(); } public void SetColliderTypeOnAllBones(SKINNED_MESH_COLLIDER_TYPE colliderType) { foreach (EasyColliderAutoSkinnedBone b in BoneList) { b.ColliderType = colliderType; } } /// /// Sets the collider type and weight on all bones. /// /// /// public void SetColliderTypeAndWeightOnAllBones(SKINNED_MESH_COLLIDER_TYPE colliderType, float weight) { foreach (EasyColliderAutoSkinnedBone b in BoneList) { b.BoneWeight = weight; b.ColliderType = colliderType; } } void BakeSkinnedMesh(SkinnedMeshRenderer skinnedMesh, Mesh m) { Vector3 lossyScale = skinnedMesh.transform.lossyScale; Vector3 local = skinnedMesh.transform.localScale; // this works well enough for most cases. if (lossyScale != Vector3.one) { local.x /= lossyScale.x; local.y /= lossyScale.y; local.z /= lossyScale.z; } skinnedMesh.transform.localScale = local; skinnedMesh.BakeMesh(m); } /// /// Calculates the preview data for the auto-skinned secttion. /// /// /// /// /// /// /// /// /// public List CalculateSkinnedMeshPreview(SkinnedMeshRenderer skinnedMesh, SKINNED_MESH_COLLIDER_TYPE colliderType, EasyColliderProperties properties, float minBoneWeight, bool realignBones = false, float minRealignAngle = 20f, string savePath = "Assets/") { if (skinnedMesh == null) return new List(); List TemporaryGameObjects = new List(); renderer = skinnedMesh; List data = new List(); // bake and scale. Mesh m = new Mesh(); Vector3 prevScale = skinnedMesh.transform.localScale; BakeSkinnedMesh(skinnedMesh, m); Vector3[] vertices = m.vertices; for (int i = 0; i < vertices.Length; i++) { //transform to world relative by root object vertices[i] = skinnedMesh.transform.TransformPoint(vertices[i]); } skinnedMesh.transform.localScale = prevScale; // get skinned mesh bones EasyColliderAutoSkinnedBone[] smbs = BoneList.ToArray(); if (smbs == null || BoneList.Count == 0) { return null; } foreach (EasyColliderAutoSkinnedBone smb in smbs) { smb.WorldSpaceVertices.Clear(); } List generatedColliders = new List(); // set the world vertex for each bone. #if UNITY_2019_1_OR_NEWER SetWorldVertices(smbs, skinnedMesh.sharedMesh.GetAllBoneWeights(), skinnedMesh.sharedMesh.GetBonesPerVertex(), vertices, minBoneWeight); #else SetWorldVertices(smbs, skinnedMesh.sharedMesh.boneWeights, vertices); #endif Transform[] bones = skinnedMesh.bones; foreach (EasyColliderAutoSkinnedBone s in smbs) { if (!s.Enabled || !s.IsValid || s.renderer != skinnedMesh) continue; // ignore excluded, null, and bones with no vertices. if (s == null || s.WorldSpaceVertices.Count == 0) continue; // the attach to is the skinned bones transform's gameobject. properties.AttachTo = s.Transform.gameObject; s.Matrix = s.Transform.localToWorldMatrix; // when the mesh isn't optimized, the bones transform is filled if (bones.Length > 0) { // if realigning is enabled, and its not convex meshes if (realignBones && colliderType != SKINNED_MESH_COLLIDER_TYPE.Convex_Mesh && minRealignAngle > 0.0f) { // try realigning by finding the single child bone, comparing the angles, and creating a properly aligned transform Transform child = GetChildBone(s.Transform, bones); if (child != null) { float minAngle = GetMinimumChildAngle(s.Transform, child); if (minAngle >= minRealignAngle) { // TODO: need to readjust how everything is done to use a matrix instead of a transform. // properties.AttachTo = CreateRealignedObject(s.Transform, child); s.Matrix = GetMatrixForObject(s.Transform, child); if (AutoSkinnedDepenetrate) { GameObject newObject = CreateRealignedObject(s.Transform, child); TemporaryGameObjects.Add(newObject); properties.AttachTo = newObject; } } } } } // finally, calculate the collider EasyColliderData d = CalculateColliderData(s.ColliderType, s, properties); // if its not a mesh collider & we're depenetrating. if (AutoSkinnedDepenetrate) { Collider createdCollider = GenerateCollider(s.ColliderType, properties, s, savePath, true); if (createdCollider != null) { generatedColliders.Add(createdCollider); data.Add(d); s.Collider = createdCollider; } } else if (!AutoSkinnedDepenetrate) { data.Add(d); } } if (generatedColliders.Count == data.Count && AutoSkinnedDepenetrate) { CheckDoDepenetration(generatedColliders); for (int i = 0; i < generatedColliders.Count; i++) { Collider c = generatedColliders[i]; EasyColliderData d = data[i]; if (c is BoxCollider) { BoxColliderData bd = d as BoxColliderData; if (bd == null) continue; BoxCollider bc = c as BoxCollider; bd.Center = bc.center; bd.Size = bc.size; } else if (c is CapsuleCollider) { CapsuleColliderData ccd = d as CapsuleColliderData; if (ccd == null) continue; CapsuleCollider cc = c as CapsuleCollider; ccd.Center = cc.center; ccd.Radius = cc.radius; ccd.Height = cc.height; } else if (c is SphereCollider) { SphereColliderData scd = d as SphereColliderData; if (scd == null) continue; SphereCollider sc = c as SphereCollider; scd.Center = sc.center; scd.Radius = sc.radius; } // would work okay for preview, but not generation. // else if (c is MeshCollider) // { // MeshColliderData mcd = d as MeshColliderData; // if (mcd == null) continue; // MeshCollider mc = c as MeshCollider; // mcd.ConvexMesh = mc.sharedMesh; // } } } // creating and destroying the objects is really the slow part. // creating and destroying the colliders with an UNDO registered was the slow part // since they are only temporary, we can safely just destroy em all without undos. for (int i = 0; i < generatedColliders.Count; i++) { DestroyImmediate(generatedColliders[i]); } for (int i = 0; i < TemporaryGameObjects.Count; i++) { DestroyImmediate(TemporaryGameObjects[i]); } return data; } /// /// Generates colliders along a bone chain of a skinned mesh renderer. /// /// Skinned mesh renderer /// Type of colliders to use /// Parameters to set on created colliders /// Minimum bone weight to include a vertex in a bones collider /// Should realigning colliders be performed? /// When the minimum angle of all of a bone's axis (up, down, left, right, forward, back) and the vector to the next bone in the chain is >= minRealignAngle, realigning is performed if enabled. /// Full path to save mesh's when colliderType is a Convex Mesh. Ie: C:/UnityProjects/ProjectName/Assets/ConvexHulls/BaseHullName public List GenerateSkinnedMeshColliders(SkinnedMeshRenderer skinnedMesh, SKINNED_MESH_COLLIDER_TYPE colliderType, EasyColliderProperties properties, float minBoneWeight, bool realignBones = false, float minRealignAngle = 20f, string savePath = "Assets/") { if (skinnedMesh == null) return new List(); renderer = skinnedMesh; // bake the mesh to get world space vertices as this works in all cases (bone transforms are valid, bind poses are valid, or the allow deoptimization is used) // also works for incorrectly rotated roots / mesh renderers etc. // bake and scale. Mesh m = new Mesh(); Vector3 prevScale = skinnedMesh.transform.localScale; BakeSkinnedMesh(skinnedMesh, m); Vector3[] vertices = m.vertices; for (int i = 0; i < vertices.Length; i++) { //transform to world relative by root object vertices[i] = skinnedMesh.transform.TransformPoint(vertices[i]); } skinnedMesh.transform.localScale = prevScale; // get skinned mesh bones EasyColliderAutoSkinnedBone[] smbs = BoneList.ToArray(); if (smbs == null || BoneList.Count == 0) { return null; } foreach (EasyColliderAutoSkinnedBone smb in smbs) { smb.WorldSpaceVertices.Clear(); } List generatedColliders = new List(); // set the world vertex for each bone. #if UNITY_2019_1_OR_NEWER SetWorldVertices(smbs, skinnedMesh.sharedMesh.GetAllBoneWeights(), skinnedMesh.sharedMesh.GetBonesPerVertex(), vertices, minBoneWeight); #else SetWorldVertices(smbs, skinnedMesh.sharedMesh.boneWeights, vertices); #endif Transform[] bones = skinnedMesh.bones; foreach (EasyColliderAutoSkinnedBone s in smbs) { if (!s.Enabled || !s.IsValid || s.renderer != skinnedMesh) continue; if (s == null || s.WorldSpaceVertices.Count == 0) continue; // Debug.Log("Generate on valid bone."); // the attach to is the skinned bones transform's gameobject. properties.AttachTo = s.Transform.gameObject; colliderType = s.ColliderType; // when the mesh isn't optimized, the bones transform is filled if (bones.Length > 0) { // if realigning is enabled, and its not convex meshes if (realignBones && colliderType != SKINNED_MESH_COLLIDER_TYPE.Convex_Mesh) { // try realigning by finding the single child bone, comparing the angles, and creating a properly aligned transform Transform child = GetChildBone(s.Transform, bones); if (child != null) { float minAngle = GetMinimumChildAngle(s.Transform, child); if (minAngle >= minRealignAngle) { properties.AttachTo = CreateRealignedObject(s.Transform, child); } } } } // finally, create the collider. Collider createdCollider = GenerateCollider(colliderType, properties, s, savePath, false); if (createdCollider != null) { generatedColliders.Add(createdCollider); s.Collider = createdCollider; } } // do depenetration. CheckDoDepenetration(generatedColliders); return generatedColliders; } public bool reverse; /// /// Runs the depenetration method(s) if needed. /// /// private void CheckDoDepenetration(List generatedColliders) { if (AutoSkinnedDepenetrate) { List colliders = null; if (AutoSkinnedDepenetrateOrder == SKINNED_MESH_DEPENETRATE_ORDER.OutsideIn || AutoSkinnedDepenetrateOrder == SKINNED_MESH_DEPENETRATE_ORDER.InsideOut) { colliders = new List(generatedColliders.Count); // order by descending indent level. IEnumerable query = BoneList.OrderByDescending(bone => bone.IndentLevel); foreach (var b in query) { if (b.Collider == null || !b.Enabled || !b.IsValid) continue; colliders.Add(b.Collider); } // reverse if needed if (AutoSkinnedDepenetrateOrder == SKINNED_MESH_DEPENETRATE_ORDER.InsideOut) { colliders.Reverse(); } } else { // just use generated colliders, reverse if needed. colliders = new List(generatedColliders); if (AutoSkinnedDepenetrateOrder == SKINNED_MESH_DEPENETRATE_ORDER.Reverse) { colliders.Reverse(); } } // iteratively shrink and shift! IterativeShrinkAndShift(colliders); } } /// /// Number of colliders shrunk during the current shrink iteration. /// private int ShrinkCount; /// /// Total shrink amount of all colliders during the current shrink iteration. /// private Vector3 ShrinkAmount; /// /// Number of colliders shifted during the current shift iteration. /// private int ShiftCount; /// /// Total shift amount of all colliders during the current shift iteration. /// private Vector3 ShiftAmount; /// /// Iteratively shrinks then shifts the generated colliders a number of times equal to the value set in preferences. /// Shrinks with a multiplier of 0.5, shifts, then repeats. /// /// private void IterativeShrinkAndShift(List generatedColliders) { for (int i = 0; i < AutoSkinnedIterativeDepenetrationCount; i++) { // keep track of shrink & shift count so we can exit once an iteration detects no shifts or shrinks. ShrinkCount = 0; ShiftCount = 0; ShrinkAmount = Vector3.zero; ShiftAmount = Vector3.zero; // if we can can shrink colliders, do so first. if (AutoSkinnedShrinkAmount > 0) { ShrinkDepenetrate(generatedColliders, AutoSkinnedShrinkAmount); } // then shift the colliders. ShiftDepenetrate(generatedColliders); // no more shrinking or shifting? we're done! if (ShrinkCount == 0 && ShiftCount == 0) { return; } } } /// /// Gets a vector3 as a string with more precision by manually building the string with x, y, and z values. /// /// /// public string LogVector(Vector3 vector3) { return "(" + vector3.x + "," + vector3.y + "," + vector3.z + ")"; } /// /// Simply a helpful method that logs each colliders name and overlap count, and their max shift vectors. /// At the end it also logs how many colliders are overlapped with at least 1 other, and the total of the max shift vectors. /// /// private void LogRemainingOverlaps(List generatedColliders) { List> datas = new List>(); foreach (Collider c in generatedColliders) { List shifts = new List(); Transform cTransform = c.transform; foreach (Collider other in generatedColliders) { if (c != other) { ShiftData data = new ShiftData(); if (GetShiftWorld(c, cTransform, other, out data)) { if (data.Direction == Vector3.zero) continue; if (data.Size == 0.0f) continue; shifts.Add(data); } } } datas.Add(shifts); } Vector3 totalShiftRemaining = Vector3.zero; int totalOverlapped = 0; for (int i = 0; i < generatedColliders.Count; i++) { if (datas[i].Count > 0) { Vector3 maxs = Vector3.zero; foreach (var s in datas[i]) { Vector3 dir = generatedColliders[i].transform.InverseTransformVector(s.Direction * s.Size); dir.x = Mathf.Abs(dir.x); dir.y = Mathf.Abs(dir.y); dir.z = Mathf.Abs(dir.z); maxs.x = Mathf.Max(maxs.x, dir.x); maxs.y = Mathf.Max(maxs.y, dir.y); maxs.z = Mathf.Max(maxs.z, dir.z); } if (maxs != Vector3.zero) { Debug.Log("Collider:" + generatedColliders[i].transform.name + " Overlaps with:" + datas[i].Count + " Max Shift Vector:" + LogVector(maxs)); totalShiftRemaining += maxs; totalOverlapped++; } } } Debug.Log("Total Overlapped Colliders:" + totalOverlapped + " Total Shift Vector:" + LogVector(totalShiftRemaining)); } /// /// Attempts to depenetrate colliders by calculating all the penetration shift vectors, then getting the maximum in each local axis, and shrinking the size, height, radius (depeneding on collider type) approriately. /// Mult is used to multiply the value(s) used to shrink (for iterations) /// /// /// Amount to multiple the shrink values with. private void ShrinkDepenetrate(List generatedColliders, float mult = 1.0f) { foreach (Collider c in generatedColliders) { List shiftDatas = new List(generatedColliders.Count - 1); Transform cTransform = c.transform; // get all the shift's needed to depenetrate this collider from all other colliders. foreach (Collider other in generatedColliders) { if (c == other) continue; ShiftData data = new ShiftData(); if (GetShiftWorld(c, cTransform, other, out data)) { shiftDatas.Add(data); } } // no shifts? skip. if (shiftDatas.Count == 0) continue; // calculate the maximum amount we would need to shift on each axis to depenetrate from all colliders. Vector3 maxShifts = Vector3.zero; foreach (ShiftData data in shiftDatas) { Vector3 direction = cTransform.InverseTransformVector(data.Direction * data.Size); // since we're shrinking (and not shifting), we want the absolute value as sign doesn't matter. direction.x = Mathf.Abs(direction.x); direction.y = Mathf.Abs(direction.y); direction.z = Mathf.Abs(direction.z); if (direction.x > maxShifts.x) { maxShifts.x = direction.x; } if (direction.y > maxShifts.y) { maxShifts.y = direction.y; } if (direction.z > maxShifts.z) { maxShifts.z = direction.z; } } // do we have a value to shrink? if (maxShifts != Vector3.zero) { // keep track of how many shrinks we've done this iteration so we can exit early if complete. ShrinkCount++; ShrinkAmount += maxShifts; if (c is BoxCollider) { // shrink a box collider BoxCollider bc = c as BoxCollider; Vector3 size = bc.size; size -= maxShifts * mult; // if we go into a negative size from all the shifts, this collider's size should be zero. if (size.x < 0 || size.y < 0 || size.z < 0) { size = Vector3.zero; } bc.size = size; } else if (c is CapsuleCollider) { // shrink a capsule collider. CapsuleCollider cc = c as CapsuleCollider; float hChange = 0.0f; float rChange = 0.0f; if (cc.direction == 0) //x { hChange = maxShifts.x; // radius change is the max-shift of the non-height axis. rChange = maxShifts.y > maxShifts.z ? maxShifts.y : maxShifts.z; } else if (cc.direction == 1) //y { hChange = maxShifts.y; rChange = maxShifts.x > maxShifts.z ? maxShifts.x : maxShifts.z; } else //z { hChange = maxShifts.z; rChange = maxShifts.y > maxShifts.x ? maxShifts.y : maxShifts.x; } hChange *= mult; rChange *= mult; cc.height -= hChange; cc.radius -= rChange; if (cc.height < 0 || cc.radius < 0) { cc.height = 0; cc.radius = 0; } } else if (c is SphereCollider) { // simply shrink a sphere by reducing the radius by the maximum shift on any axis. SphereCollider sc = c as SphereCollider; sc.radius -= (Mathf.Max(maxShifts.x, Mathf.Max(maxShifts.y, maxShifts.z))) * mult; if (sc.radius < 0) { sc.radius = 0; } } // works for preview, would need to adjust actual source mesh for generation. // else if (c is MeshCollider) // { // MeshCollider mc = c as MeshCollider; // Vector3[] vertices = mc.sharedMesh.vertices; // for (int i = 0; i < vertices.Length; i++) // { // vertices[i] -= maxShifts * mult; // } // mc.sharedMesh.vertices = vertices; // } } } } /// /// Shifts a collider by an amount in local space. Essentially just converts the collider to a box, capsule, or sphere, and shifts the center by amount. /// /// /// private void ShiftCollider(Collider c, Vector3 localAmount) { if (c is BoxCollider) { BoxCollider bc = c as BoxCollider; bc.center += localAmount; } else if (c is CapsuleCollider) { CapsuleCollider cc = c as CapsuleCollider; cc.center += localAmount; } else if (c is SphereCollider) { SphereCollider sc = c as SphereCollider; sc.center += localAmount; } // works for preview, would need to adjust source mesh for generation. // else if (c is MeshCollider) // { // MeshCollider mc = c as MeshCollider; // Vector3[] vertices = mc.sharedMesh.vertices; // for (int i = 0; i < vertices.Length; i++) // { // vertices[i] += localAmount; // } // mc.sharedMesh.vertices = vertices; // } } /// /// Attempts to depenetrate colliders by calculating the average of all ComputeDepenetration Vectors and apply it to the collider's center. /// /// private void ShiftDepenetrate(List generatedColliders) { // TODO: use overlap to get direct array to use instead of n^2? foreach (Collider c in generatedColliders) { // create a list of shifts for each collider. List datas = new List(generatedColliders.Count - 1); // cache transform Transform cTransform = c.transform; foreach (Collider other in generatedColliders) { if (c == other) continue; ShiftData data = new ShiftData(); // if there's a shift, add it to that colliders shift data list. if (GetShiftWorld(c, cTransform, other, out data)) { datas.Add(data); } } // no shifts? skip. if (datas.Count == 0) continue; Vector3 shiftVector = Vector3.zero; // calculate the total shift vector. // since we're doing this on all colliders, colliders's that are "stuck" between child colliders will likely not shift much // and instead colliders that are free to shift in one direction will shift out first, which makes sense to do. foreach (ShiftData data in datas) { shiftVector += cTransform.InverseTransformVector(data.Direction * data.Size); } // if we have a shift vector, shift! if (shiftVector != Vector3.zero) { ShiftCount++; ShiftAmount += shiftVector; shiftVector /= datas.Count; ShiftCollider(c, shiftVector); } } } /// /// Data from a Physics.ComputePenetration result. /// private struct ShiftData { public Vector3 Direction; public float Size; public ShiftData(Vector3 direction, float size) { this.Direction = direction; this.Size = size; } } /// /// Sets set shift data with the penetrations direction and size if there is one. /// /// /// /// /// True if there is a penetration, false otherwise private bool GetShiftWorld(Collider c, Transform ct, Collider other, out ShiftData data) { Vector3 direction = Vector3.zero; float distance = 0.0f; Transform ot = other.transform; if (Physics.ComputePenetration(c, ct.position, ct.rotation, other, ot.position, ot.rotation, out direction, out distance)) { data.Direction = direction; data.Size = distance; return true; } data.Direction = Vector3.zero; data.Size = -1f; return false; } /// /// Gets the child bone of a transform if it has a single valid child bone. /// /// Bone to get child of /// Array of bones /// Transform of child bone if found, otherwise null private Transform GetChildBone(Transform bone, Transform[] bones) { int boneChildCount = bone.childCount; Transform childBone = null; Transform currentChildTransform = null; int totalValidChildBones = 0; for (int j = 0; j < boneChildCount; j++) { currentChildTransform = bone.GetChild(j); int index = Array.IndexOf(bones, currentChildTransform); if (index >= 0) { totalValidChildBones += 1; childBone = currentChildTransform; } } if (totalValidChildBones == 1) { return childBone; } return null; } /// /// Gets the minimum angle between all of transform's axis and the direction from transform to child. /// /// transform to use axis from /// child to get minimum angle to /// Minimum angle from all of transform's axis and the direction from transform to child. private float GetMinimumChildAngle(Transform transform, Transform child) { Vector3 childDir = child.position - transform.position; float minAngle = Mathf.Infinity; float angle = Vector3.Angle(transform.right, childDir); minAngle = minAngle > angle ? angle : minAngle; angle = Vector3.Angle(-transform.right, childDir); minAngle = minAngle > angle ? angle : minAngle; angle = Vector3.Angle(transform.forward, childDir); minAngle = minAngle > angle ? angle : minAngle; angle = Vector3.Angle(-transform.forward, childDir); minAngle = minAngle > angle ? angle : minAngle; angle = Vector3.Angle(transform.up, childDir); minAngle = minAngle > angle ? angle : minAngle; angle = Vector3.Angle(-transform.up, childDir); minAngle = minAngle > angle ? angle : minAngle; return minAngle; } /// /// Gets all the skinned mesh bones for the current skinned mesh renderer. /// /// /// Array of skinned mesh bones. private EasyColliderAutoSkinnedBone[] GetSkinnedMeshBones(SkinnedMeshRenderer skinnedMesh) { int validBonesFound = 0; EasyColliderAutoSkinnedBone[] smbs = null; // first, if there are transform in the bones array, we use that to get everything if (skinnedMesh.bones.Length > 0) { smbs = new EasyColliderAutoSkinnedBone[skinnedMesh.bones.Length]; // try to match based on bones Transform[] bones = skinnedMesh.bones; for (int i = 0; i < bones.Length; i++) { // a bone was deleted.... if (bones[i] == null) { smbs[i] = new EasyColliderAutoSkinnedBone(new Matrix4x4(), i, bones[i]); } else { smbs[i] = new EasyColliderAutoSkinnedBone(bones[i].localToWorldMatrix, i, bones[i]); smbs[i].BoneName = bones[i].name; smbs[i].renderer = skinnedMesh; validBonesFound++; } } } return smbs; } // native array and boneweight1 functionality are 2019.1+ #if UNITY_2019_1_OR_NEWER /// /// Sets the skinned mesh bone's world vertices list. /// /// Skinned mesh we are trying to set world vertices for /// Array of all skinned mesh bones /// Array of all vertices in world space /// Minimum bone weight to include a vertex in a bone's vertex list. private void SetWorldVertices(EasyColliderAutoSkinnedBone[] skinnedMeshBones, NativeArray boneWeights, NativeArray bonesPerVertex, Vector3[] worldVertices, float minBoneWeight) { int boneIndex = 0; for (int i = 0; i < worldVertices.Length; i++) { int numBonesForVertex = bonesPerVertex[i]; for (int j = 0; j < numBonesForVertex; j++) { BoneWeight1 boneWeight = boneWeights[boneIndex]; if (boneWeight.boneIndex < skinnedMeshBones.Length && boneWeight.weight >= skinnedMeshBones[boneWeight.boneIndex].BoneWeight) { skinnedMeshBones[boneWeight.boneIndex].WorldSpaceVertices.Add(worldVertices[i]); } boneIndex++; } } } #endif /// /// Sets the skinned mesh bone's world vertices list. /// /// Array of all skinned mesh bones /// Array of all bone weights /// Array of all vertices in world space /// Minimum bone weight to include a vertex in a bone's vertex list. private void SetWorldVertices(EasyColliderAutoSkinnedBone[] skinnedMeshBones, BoneWeight[] boneWeights, Vector3[] worldVertices) { for (int i = 0; i < boneWeights.Length; i++) { // make sure the weight is above the minimum weight. if (skinnedMeshBones[boneWeights[i].boneIndex0] != null && boneWeights[i].weight0 >= skinnedMeshBones[boneWeights[i].boneIndex0].BoneWeight) { // add the vertex to that bone's vertex list skinnedMeshBones[boneWeights[i].boneIndex0].WorldSpaceVertices.Add(worldVertices[i]); } if (skinnedMeshBones[boneWeights[i].boneIndex1] != null && boneWeights[i].weight1 >= skinnedMeshBones[boneWeights[i].boneIndex1].BoneWeight) { // add the vertex to that bone's vertex list skinnedMeshBones[boneWeights[i].boneIndex1].WorldSpaceVertices.Add(worldVertices[i]); } if (skinnedMeshBones[boneWeights[i].boneIndex2] != null && boneWeights[i].weight2 >= skinnedMeshBones[boneWeights[i].boneIndex2].BoneWeight) { // add the vertex to that bone's vertex list skinnedMeshBones[boneWeights[i].boneIndex2].WorldSpaceVertices.Add(worldVertices[i]); } if (skinnedMeshBones[boneWeights[i].boneIndex3] != null && boneWeights[i].weight3 >= skinnedMeshBones[boneWeights[i].boneIndex3].BoneWeight) { // add the vertex to that bone's vertex list skinnedMeshBones[boneWeights[i].boneIndex3].WorldSpaceVertices.Add(worldVertices[i]); } } } public void OnBeforeSerialize() { //nothing needed } public void OnAfterDeserialize() { // need to relink the classes as otherwise the instances become seperate instances during serialization. for (int i = 0; i < SortedBoneList.Count; i++) { SortedBoneList[i] = BoneList[SortedBoneList[i].BoneIndex]; } } } }