using System.Collections.Generic; using System.Linq; using UnityEngine; namespace RayFire { [SelectionBase] [AddComponentMenu("RayFire/Rayfire Cluster")] [HelpURL("http://rayfirestudios.com/unity-online-help/components/unity-cluster-component/")] public class RayfireCluster : MonoBehaviour { // Cluster Type public enum ClusterType { ByPointCloud = 0, BySharedArea = 1 } [Space(2)] [Header(" Properties")] [Space (2)] public ClusterType type = ClusterType.ByPointCloud; [Range(1, 7)] public int depth = 1; [Range(0, 100)] public int seed = 1; [Range(0, 4)] public int smoothPass = 1; [Header(" By Point Cloud")] [Space (2)] [Range(2, 100)] public int baseAmount = 5; [Range(2, 50)] public int depthAmount = 2; public ConnectivityType connectivity = ConnectivityType.ByBoundingBox; [Header(" By Shared Area")] [Space (2)] [Range(2, 8)] public int minimumAmount = 2; [Range(2, 8)] public int maximumAmount = 5; // Preview [HideInInspector] public bool showGizmo = true; [HideInInspector] public bool colorPreview = false; [HideInInspector] public bool scalePreview = false; [HideInInspector] public Color wireColor = new Color(0.58f, 0.77f, 1f); [HideInInspector] public float previewScale = 0f; [HideInInspector] public List allClusters = new List(); [HideInInspector] public List allShards = new List(); int clusterId = 0; /// ///////////////////////////////////////////////////////// /// Clustering /// ///////////////////////////////////////////////////////// // Extract all children under root public void Extract() { previewScale = 0f; allShards.Clear(); allClusters.Clear(); // Get all child nodes List allTm = GetComponentsInChildren().ToList(); // Set root as parent for (int i = allTm.Count - 1; i >= 0; i--) { // Get all components Component[] allComponents = allTm[i].GetComponents(typeof(Component)); // Destroy if empty object with transform only if (allComponents.Length == 1) { DestroyImmediate(allTm[i].gameObject); allTm.RemoveAt(i); continue; } // Set as parent allTm[i].parent = transform; } } // Clusterize all children public void Clusterize() { // Reset vars // soloNow = 0; // Extract all children first Extract(); // Clear lists allShards.Clear(); allClusters.Clear(); // Clusterize by type ClusterizeVoronoi(); // Clusterize by size and range ClusterizeRange(); } /// ///////////////////////////////////////////////////////// /// Voronoi /// ///////////////////////////////////////////////////////// // Clusterize by Voronoi pc void ClusterizeVoronoi() { if (type == ClusterType.ByPointCloud) { // Create Base cluster RFCluster mainCluster = SetupMainCluster(connectivity); // Base amount of clusters is more than shards amount if (baseAmount >= mainCluster.shards.Count) return; // Set shard neibs RFShard.SetShardNeibs(mainCluster.shards, connectivity); // List with all clusters List clusters = new List {mainCluster}; // Collect base cluster allClusters.Add(mainCluster); // Clusterize while (clusters.Count > 0) { // Get local cluster RFCluster cls = clusters[0]; // Remove current cluster from clustering list clusters.RemoveAt(0); // Low amount of shards if (cls.shards.Count < 4) continue; // Get amount int amount = baseAmount; if (cls.depth > 0) amount = depthAmount; // Get local depth roots cls.childClusters = ClusterizeClusterByAmount(cls, amount); // Collect new clusters allClusters.AddRange(cls.childClusters); // Check if local cluster should be clusterized further and add to list if (cls.childClusters.Count > 0 && depth > cls.depth + 1) clusters.AddRange(cls.childClusters); } // Set name to roots SetClusterNames(); } } // Clusterize shards by amount List ClusterizeClusterByAmount(RFCluster parentCluster, int amount) { // Empty list of all new cluster roots List childClusters = new List(); // Check if root has children more than at least 2 if (parentCluster.tm.childCount <= 2) return childClusters; // Shards are more than cluster amount if (amount >= parentCluster.shards.Count) return childClusters; // Get bounds for random point cloud generation Bounds bound = RFCluster.GetChildrenBound(parentCluster.tm); // Collect cluster points List voronoiPoints = VoronoiPointCloud(bound, amount); // Create cluster for each point foreach (Vector3 point in voronoiPoints) { RFCluster childCluster = new RFCluster(); childCluster.pos = point; childCluster.depth = parentCluster.depth + 1; // Set id clusterId++; childCluster.id = clusterId; childClusters.Add(childCluster); } // Separate shards by closest to cluster distance foreach (RFShard shard in parentCluster.shards) { // Puck first cluster float rootDist = Vector3.Distance(shard.tm.position, childClusters[0].pos); float minDist = rootDist; shard.cluster = childClusters[0]; // Set closest cluster if (childClusters.Count > 1) { for (int i = 1; i < childClusters.Count; i++) { rootDist = Vector3.Distance(shard.tm.position, childClusters[i].pos); if (rootDist < minDist) { minDist = rootDist; shard.cluster = childClusters[i]; } } } // Apply shard to closest cluster and reset shard.cluster.shards.Add(shard); shard.cluster = null; } // Check child clusters and remove empty or solo shard clusters List soloShards = new List(); for (int i = childClusters.Count - 1; i >= 0; i--) { if (childClusters[i].shards.Count < 2) { soloShards.AddRange(childClusters[i].shards); childClusters.RemoveAt(i); } } // First pass Find neib cluster for solo shards SetSoloShardToCluster(soloShards, childClusters); //// Second pass Find neib cluster for solo shards SetSoloShardToCluster(soloShards, childClusters); // Roughness pass. Remove shards from cluster and add to another. if (smoothPass > 0 && connectivity == ConnectivityType.ByTriangles) for (int i = 0; i < smoothPass; i++) RoughnessPassShards(childClusters); // Create clusters by connectivity check if (connectivity == ConnectivityType.ByTriangles) ConnectivityCheck(childClusters); // Check if only one cluster left if (childClusters.Count == 1) { childClusters.Clear(); return childClusters; } // Create root for cluster at shards center and set shards as children foreach (RFCluster childCluster in childClusters) CreateRoot(childCluster, parentCluster.tm); return childClusters; } // Check cluster for connectivity and create new connected clusters void ConnectivityCheck(List childClusters) { // New list for solo shards List soloShards = new List(); List newChildClusters = new List(); // Check every cluster for connectivity foreach (RFCluster childCluster in childClusters) { // Collect solo shards with no neibs for (int i = childCluster.shards.Count - 1; i >= 0; i--) if (childCluster.shards[i].neibShards.Count == 0) soloShards.Add(childCluster.shards[i]); // Get list of all shards to check List allShardsLoc = new List(); foreach (RFShard shard in childCluster.shards) allShardsLoc.Add(shard); // Check all shards and collect new clusters int shardsAmount = allShardsLoc.Count; List newClusters = new List(); while (allShardsLoc.Count > 0) { // List of connected shards List newClusterShards = new List(); // List of check shards List checkShards = new List(); // Start from first shard checkShards.Add(allShardsLoc[0]); newClusterShards.Add(allShardsLoc[0]); // Collect by neibs while (checkShards.Count > 0) { // Add neibs to check foreach (RFShard neibShard in checkShards[0].neibShards) { // If neib among current cluster shards if (allShardsLoc.Contains(neibShard) == true) { // And not already collected if (newClusterShards.Contains(neibShard) == false) { checkShards.Add(neibShard); newClusterShards.Add(neibShard); } } } // Remove checked checkShards.RemoveAt(0); } // Child cluster connected if (shardsAmount == newClusterShards.Count) allShardsLoc.Clear(); // Child cluster not connected else { // Create new cluster and add to parent RFCluster newCluster = new RFCluster(); newCluster.pos = childCluster.pos; newCluster.depth = childCluster.depth; newCluster.shards = newClusterShards; // Set id clusterId++; newCluster.id = clusterId; newClusters.Add(newCluster); // Remove from all shards list for (int i = allShardsLoc.Count - 1; i >= 0; i--) if (newClusterShards.Contains(allShardsLoc[i]) == true) allShardsLoc.RemoveAt(i); } } // Non connectivity. Remove original cluster if (newClusters.Count > 0) { childCluster.shards.Clear(); newChildClusters.AddRange(newClusters); } } // Clear empty clusters for (int i = childClusters.Count - 1; i >= 0; i--) if (childClusters[i].shards.Count == 0) childClusters.RemoveAt(i); // Collect new clusters childClusters.AddRange(newChildClusters); // Set clusters neib info RFCluster.SetClusterNeib(childClusters, true); // Second pass Find neib cluster for solo shards SetSoloShardToCluster(soloShards, childClusters); // Roughness pass. Remove shards from cluster and add to another. if (smoothPass > 0) RoughnessPassShards(childClusters); } /// ///////////////////////////////////////////////////////// /// By range /// ///////////////////////////////////////////////////////// // Second clustering type void ClusterizeRange() { if (type == ClusterType.BySharedArea) { Random.InitState(seed); // Create Base cluster and collect RFCluster mainCluster = SetupMainCluster(ConnectivityType.ByTriangles); allClusters.Add(mainCluster); // Set shard neibs RFShard.SetShardNeibs(mainCluster.shards, ConnectivityType.ByTriangles); // Clusterize base shards to clusters List childClusters = ClusterizeRangeShards(mainCluster); // Create root and set shards and children foreach (RFCluster childCluster in childClusters) CreateRoot(childCluster, transform); // Add to all clusters allClusters.AddRange(childClusters); // Clusterize clusters in depth if (depth > 1) { for (int i = 1; i < depth; i++) { // Set clusters neib info RFCluster.SetClusterNeib(mainCluster.childClusters, true); // Get new depth clusters List newClusters = ClusterizeRangeClusters(mainCluster); if (newClusters.Count > 1) { // Create root for all new clusters and set as parent for them foreach (RFCluster cls in newClusters) { CreateRoot(cls, mainCluster.tm); foreach (RFCluster childCLuster in cls.childClusters) childCLuster.tm.parent = cls.tm; } // Set as child cluster for main cluster to be clusterized at next pass mainCluster.childClusters = newClusters; // Add to all clusters allClusters.AddRange(newClusters); // Get all nested clusters and increment depth foreach (RFCluster cls in allClusters) if (cls.id != 0) cls.depth += 1; } } } // Set name to roots SetClusterNames(); } } // Base clustering pass for shards List ClusterizeRangeShards(RFCluster mainCluster) { // Empty list of all new cluster roots List soloShards = new List(); // List with all clusters List childClusters = new List(); // Sort from smallest to biggest mainCluster.shards.Sort(); // Clusterize starting from biggest while (mainCluster.shards.Count > 0) { // Local amount of shards in cluster int shardsAmount = Random.Range(minimumAmount, maximumAmount); // Start from biggest shard RFShard startShard = mainCluster.shards[0]; // Remove from lists mainCluster.shards.RemoveAt(0); // Starting shard list List shardGroup = new List(); shardGroup.Add(startShard); // Find neibs for (int s = 0; s < shardsAmount - 1; s++) { // Get neib shard among cluster.shards with biggest shared area RFShard biggestShard = GetNeibShardArea(shardGroup, mainCluster.shards); // No neib with shared area if (biggestShard == null) break; // TODO check if area is much smaller than with another neibs. Set as solo // Add in group shardGroup.Add(biggestShard); // Remove from cluster.shards mainCluster.shards.RemoveAll(t => t.id == biggestShard.id); } // Solo shard if (shardGroup.Count == 1) soloShards.Add(startShard); // Group of shards for cluster else if (shardGroup.Count > 1) { // Clusterize with picked shard RFCluster childCluster = new RFCluster(); childCluster.shards.AddRange(shardGroup); childCluster.depth = 1; // Set id clusterId++; childCluster.id = clusterId; // Collect luster childClusters.Add(childCluster); mainCluster.childClusters.Add(childCluster); } } // First pass Find neib cluster for solo shards SetSoloShardToCluster(soloShards, childClusters); // Second pass Find neib cluster for solo shards SetSoloShardToCluster(soloShards, childClusters); // Roughness pass. Remove shards from cluster and add to another. if (smoothPass > 0) for (int i = 0; i < smoothPass; i++) RoughnessPassShards(childClusters); // TODO consider solo amount // Set id int startId = 1; for (int i = 0; i < childClusters.Count; i++) childClusters[i].id = startId + i; // Set main cluster solo shards back to main cluster mainCluster.shards.Clear(); mainCluster.shards.AddRange(soloShards); return childClusters; } // Clustering pass for clusters List ClusterizeRangeClusters(RFCluster parentCluster) { // Empty list of all new solo clusters List soloClusters = new List(); // List with all new clusters List newClusters = new List(); // Sort from smallest to biggest parentCluster.childClusters.Sort(); // Clusterize starting from biggest while (parentCluster.childClusters.Count > 0) { // Local amount of shards in cluster int clustersAmount = Random.Range(minimumAmount, maximumAmount); // Start from biggest cluster RFCluster startCluster = parentCluster.childClusters[0]; // Remove from lists parentCluster.childClusters.RemoveAt(0); // Starting list List clusterGroup = new List(); clusterGroup.Add(startCluster); for (int s = 0; s < clustersAmount - 1; s++) { // Get neib cluster among cluster with biggest shared area RFCluster biggestCluster = RFCluster.GetNeibClusterArea(clusterGroup, parentCluster.childClusters); // No neib with shared area if (biggestCluster == null) break; // Add in group clusterGroup.Add(biggestCluster); // Remove from mainCluster.childClusters parentCluster.childClusters.RemoveAll(t => t.id == biggestCluster.id); } // Solo if (clusterGroup.Count == 1) soloClusters.Add(startCluster); // Group of clusters. Creat parent clusters for them else { // Clusterize with picked clusters RFCluster newCluster = new RFCluster(); newCluster.childClusters.AddRange(clusterGroup); // Set depth newCluster.depth = 0; // Set id clusterId++; newCluster.id = clusterId; // Collect luster newClusters.Add(newCluster); } } // Attach solo clusters to neib clusters SetSoloClusterToCluster(soloClusters, newClusters); // Attach solo clusters to neib clusters SetSoloClusterToCluster(soloClusters, newClusters); // Roughness pass. Remove shards from cluster and add to another. if (smoothPass > 0) for (int i = 0; i < smoothPass; i++) RoughnessPassClusters(newClusters); return newClusters; } // Roughness pass. Remove shards from cluster and add to another. static void RoughnessPassShards(List clusters) { // Set clusters neib info RFCluster.SetClusterNeib(clusters, true); // Check cluster for shard with one neib among cluster shards for (int s = clusters.Count - 1; s >= 0; s--) { RFCluster cluster = clusters[s]; // Skip clusters with 2 shards if (cluster.shards.Count == 2) continue; // Skip clusters without neib clusters if (cluster.neibClusters.Count == 0) continue; // Collect shards to exclude from cluster List excludeShards = new List(); List attachToClusters = new List(); // Check all shards and compare area with own cluster and neib clusters foreach (RFShard shard in cluster.shards) { // Get amount of neibs among cluster shards float areaInCluster = 0f; for (int i = 0; i < shard.neibShards.Count; i++) if (cluster.shards.Contains(shard.neibShards[i]) == true) areaInCluster += shard.nArea[i]; // Compare with amount of shards from neib clusters List neibAreaList = new List(); foreach (RFCluster neibCluster in cluster.neibClusters) { float areaInNeibCluster = 0f; for (int i = 0; i < shard.neibShards.Count; i++) if (neibCluster.shards.Contains(shard.neibShards[i]) == true) areaInNeibCluster += shard.nArea[i]; neibAreaList.Add(areaInNeibCluster); } // Get maximum neibs in neib cluster float maxArea = neibAreaList.Max(); // Skip shard because neib clusters has less neib shards if (areaInCluster >= maxArea) continue; // Collect cluster which has more neibs for shard than own cluster for (int i = 0; i < neibAreaList.Count; i++) { if (maxArea == neibAreaList[i]) { excludeShards.Add(shard); attachToClusters.Add(cluster.neibClusters[i]); } } } // Reorder shards if (excludeShards.Count > 0) { for (int i = 0; i < excludeShards.Count; i++) { // Exclude from own cluster for (int c = cluster.shards.Count - 1; c >= 0; c--) if (cluster.shards[c] == excludeShards[i]) cluster.shards.RemoveAt(c); // Add to neib cluster attachToClusters[i].shards.Add(excludeShards[i]); } } } // Remove empty and solo clusters for (int i = clusters.Count - 1; i >= 0; i--) { // Remove solo shard if (clusters[i].shards.Count == 1) { clusters[i].shards.Clear(); } // Remove empty cluster if (clusters[i].shards.Count == 0) clusters.RemoveAt(i); } } // Roughness pass. Remove shards from cluster and add to another. void RoughnessPassClusters(List clusters) { // Set clusters neib info RFCluster.SetClusterNeib(clusters, true); // Check cluster for shard with one neib among cluster shards foreach (RFCluster bigCluster in clusters) { // Skip clusters with 2 child clusters if (bigCluster.childClusters.Count <= 2) continue; // Skip clusters without neib clusters if (bigCluster.neibClusters.Count == 0) continue; // Collect shards to exclude from cluster List excludeClusters = new List(); List attachToClusters = new List(); foreach (RFCluster childCluster in bigCluster.childClusters) { // Get amount of neibs among cluster child clusters float areaInCluster = 0f; for (int i = 0; i < childCluster.neibClusters.Count; i++) if (bigCluster.childClusters.Contains(childCluster.neibClusters[i]) == true) areaInCluster += childCluster.neibArea[i]; // Compare with amount of shards from neib clusters List neibAreaList = new List(); foreach (RFCluster bigNeibCluster in bigCluster.neibClusters) { float areaInNeibCluster = 0f; for (int i = 0; i < childCluster.neibClusters.Count; i++) if (bigNeibCluster.childClusters.Contains(childCluster.neibClusters[i]) == true) areaInNeibCluster += childCluster.neibArea[i]; neibAreaList.Add(areaInNeibCluster); } // Get maximum neibs in neib cluster float maxArea = neibAreaList.Max(); // Skip shard because neib clusters has less neib shards if (areaInCluster >= maxArea) continue; // Collect cluster which has more neibs for shard than own cluster for (int i = 0; i < neibAreaList.Count; i++) { if (maxArea == neibAreaList[i]) { excludeClusters.Add(childCluster); attachToClusters.Add(bigCluster.neibClusters[i]); } } } // Skip if cluster may loose all shards if (excludeClusters.Count + 1 >= bigCluster.childClusters.Count) continue; // Reorder shards if (excludeClusters.Count > 0) { for (int i = 0; i < excludeClusters.Count; i++) { // Exclude from own cluster for (int s = bigCluster.shards.Count - 1; s >= 0; s--) if (bigCluster.childClusters[s] == excludeClusters[i]) bigCluster.childClusters.RemoveAt(s); // Add to neib cluster attachToClusters[i].childClusters.Add(excludeClusters[i]); } } } } /// ///////////////////////////////////////////////////////// /// Methods /// ///////////////////////////////////////////////////////// // Add solo shards to closest cluster void SetSoloShardToCluster(List soloShards, List childClusters) { // No solo shards if (soloShards.Count == 0) return; // Find neib cluster for solo shards for (int i = soloShards.Count - 1; i >= 0; i--) { int ind = GetNeibIndArea(soloShards[i]); if (ind >= 0) { RFShard neibShard = soloShards[i].neibShards[ind]; for (int c = 0; c < childClusters.Count; c++) { if (childClusters[c].shards.Contains(neibShard) == true) { childClusters[c].shards.Add(soloShards[i]); soloShards.RemoveAt(i); continue; } } } } } // Get neib index with biggest shared area int GetNeibIndArea(RFShard shard, List shardList = null) { // Get neib index with biggest shared area float biggestArea = 0f; int neibInd = 0; for (int i = 0; i < shard.neibShards.Count; i++) { // Skip if check neib shard not in filter list if (shardList != null) if (shardList.Contains(shard.neibShards[i]) == false) continue; // Remember if bigger if (shard.nArea[i] > biggestArea) { biggestArea = shard.nArea[i]; neibInd = i; } } // Return index of neib with biggest shared area if (biggestArea > 0) return neibInd; // No neib return -1; } // Add solo shards to closest cluster void SetSoloClusterToCluster(List soloClusters, List childClusters) { // No solo clusters if (soloClusters.Count == 0) return; // Find neib cluster for solo cluster for (int i = soloClusters.Count - 1; i >= 0; i--) { int ind = soloClusters[i].GetNeibIndArea(); if (ind >= 0) { RFCluster neibCluster = soloClusters[i].neibClusters[ind]; for (int c = 0; c < childClusters.Count; c++) { if (childClusters[c].childClusters.Contains(neibCluster) == true) { childClusters[c].childClusters.Add(soloClusters[i]); soloClusters.RemoveAt(i); } } } } } // Set up main cluster and set shards RFCluster SetupMainCluster (ConnectivityType connect) { // Create Base cluster RFCluster cluster = new RFCluster(); cluster.tm = transform; cluster.depth = 0; cluster.pos = transform.position; // Set cluster id cluster.id = 0; // Set shards for main cluster RFShard.SetShards(cluster, connectivity); clusterId = 0; // Collect all shards allShards.Clear(); allShards.AddRange(cluster.shards); // TODO set bound return cluster; } // Set name to roots void SetClusterNames() { foreach (RFCluster cls in allClusters) if (cls.id > 0) if (cls.tm != null) cls.tm.name = gameObject.name + "_cls_" + cls.id; } // Create root for cluster at shards center and set shards as children void CreateRoot(RFCluster childCluster, Transform parentTm) { // Get cluster bound Bounds childBound = GetShardsBound(childCluster.shards, childCluster.childClusters); // Set cluster bound childCluster.bound = childBound; // Create root for cluster GameObject childRoot = new GameObject(); // Set cluster root position childCluster.tm = childRoot.transform; childCluster.pos = childBound.center; childCluster.tm.position = childBound.center; // Set cluster parent childCluster.tm.parent = parentTm; // Set cluster root as shards parent foreach (RFShard shard in childCluster.shards) shard.tm.parent = childCluster.tm; } /// ///////////////////////////////////////////////////////// /// Bounds /// ///////////////////////////////////////////////////////// // Get bound for list of shards Bounds GetShardsBound(List shards, List clusters = null) { // Get list of bounds List bounds = new List(); // Consider shards bounds foreach (RFShard shard in shards) bounds.Add(shard.bnd); // Consider clusters bounds if (clusters != null) foreach (RFCluster cluster in clusters) bounds.Add(cluster.bound); return RFCluster.GetBoundsBound(bounds.ToArray()); } // Get neib shard from shardList which is neib to one of the shards static RFShard GetNeibShardArea(List shardGroup, List shardList) { // No shards to pick if (shardList.Count == 0) return null; // Get all neibs for shards, exclude neibs not from shardList List allNeibs = new List(); // Biggest area float biggestArea = 0f; RFShard biggestShard = null; // Check shard foreach (RFShard shard in shardGroup) { // Check neibs for (int i = 0; i < shard.neibShards.Count; i++) { // Neib shard has shared area lower than already founded if (biggestArea >= shard.nArea[i]) continue; // Neib already in neib list if (allNeibs.Contains(shard.neibShards[i]) == true) continue; // Neib not among allowed shards if (shardList.Contains(shard.neibShards[i]) == false) continue; // Remember neib allNeibs.Add(shard.neibShards[i]); biggestArea = shard.nArea[i]; biggestShard = shard.neibShards[i]; } } allNeibs = null; // Pick shard with biggest area return biggestShard; } /// ///////////////////////////////////////////////////////// /// Point cloud /// ///////////////////////////////////////////////////////// // Generate random point3 cloud by bound and amount List VoronoiPointCloud(Bounds bound, int am) { Random.InitState(seed + clusterId); List points = new List(); for (int i = 0; i < am; i++) { float randomX = Random.Range(bound.min.x, bound.max.x); float randomY = Random.Range(bound.min.y, bound.max.y); float randomZ = Random.Range(bound.min.z, bound.max.z); Vector3 randomPoint = new Vector3(randomX, randomY, randomZ); points.Add(randomPoint); } return points; } } }