// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System.Collections.Generic; using UnityEngine; namespace Microsoft.MixedReality.Toolkit.Utilities { /// /// A Scatter Object Collection is simply a set of child objects randomly laid out within a radius. /// Pressing "update collection" will run the randomization, feel free to run as many times until you get the desired result. /// [AddComponentMenu("Scripts/MRTK/SDK/ScatterObjectCollection")] public class ScatterObjectCollection : GridObjectCollection { /// /// Overriding base function for laying out all the children when UpdateCollection is called. /// protected override void LayoutChildren() { Vector3[] nodeGrid = new Vector3[NodeList.Count]; Vector3 newPos; // Now lets lay out the grid Columns = Mathf.CeilToInt((float)NodeList.Count / Rows); HalfCell = new Vector2(CellWidth * 0.5f, CellHeight * 0.5f); // First start with a grid then project onto surface ResolveGridLayout(nodeGrid, Layout); // Get randomized planar mapping // Calculate radius of each node while we're here // Then use the packer function to shift them into place for (int i = 0; i < NodeList.Count; i++) { ObjectCollectionNode node = NodeList[i]; newPos = VectorExtensions.ScatterMapping(nodeGrid[i], Radius); Collider nodeCollider = NodeList[i].Transform.GetComponentInChildren(); if (nodeCollider != null) { // Make the radius the largest of the object's dimensions to avoid overlap Bounds bounds = nodeCollider.bounds; node.Radius = Mathf.Max(Mathf.Max(bounds.size.x, bounds.size.y), bounds.size.z) * 0.5f; } else { // Make the radius a default value node.Radius = 1f; } node.Transform.localPosition = newPos; UpdateNodeFacing(node); NodeList[i] = node; } // Iterate [x] times for (int i = 0; i < 100; i++) { IterateScatterPacking(NodeList, Radius); } } /// /// Pack randomly spaced nodes so they don't overlap /// Usually requires about 25 iterations for decent packing /// private static void IterateScatterPacking(List nodes, float radiusPadding) { // Sort by closest to center (don't worry about z axis) // Use the position of the collection as the packing center nodes.Sort(ScatterSort); Vector3 difference; Vector2 difference2D; // Move them closer together float radiusPaddingSquared = Mathf.Pow(radiusPadding, 2f); for (int i = 0; i < nodes.Count - 1; i++) { for (int j = i + 1; j < nodes.Count; j++) { if (i != j) { difference = nodes[j].Transform.localPosition - nodes[i].Transform.localPosition; // Ignore Z axis difference2D.x = difference.x; difference2D.y = difference.y; float combinedRadius = nodes[i].Radius + nodes[j].Radius; float distance = difference2D.SqrMagnitude() - radiusPaddingSquared; float minSeparation = Mathf.Min(distance, radiusPaddingSquared); distance -= minSeparation; if (distance < (Mathf.Pow(combinedRadius, 2))) { difference2D.Normalize(); difference *= ((combinedRadius - Mathf.Sqrt(distance)) * 0.5f); nodes[j].Transform.localPosition += difference; nodes[i].Transform.localPosition -= difference; } } } } } private static int ScatterSort(ObjectCollectionNode circle1, ObjectCollectionNode circle2) { float distance1 = (circle1.Transform.localPosition).sqrMagnitude; float distance2 = (circle2.Transform.localPosition).sqrMagnitude; return distance1.CompareTo(distance2); } } }