private static Vector3D[] GetVelocities(HullVoronoiExploder_Shard[] hullShards, HullVoronoiExploder_ShotHit[] hits, double hullRadius, HullVoronoiExploder_Options options)
        {
            //TODO: When the asteroid shards aren't resmoothed, the velocities are very small
            //
            //Maybe add more orth velocity
            //
            //Maybe pull the hit source slightly into the asteroid --- done
            //
            //Maybe add some random velocity (size proportional to the velocity) --- done

            Vector3D[] retVal = CalculateVelocites(hullShards.Select(o => o.Center_ParentCoords).ToArray(), hits, hullRadius, options);

            if (options.RandomVelocityPercent != null)
            {
                retVal = retVal.
                    Select(o => o + Math3D.GetRandomVector_Spherical(o.Length * options.RandomVelocityPercent.Value)).
                    ToArray();
            }

            return retVal;
        }
        private static Vector3D[] CalculateVelocites(Point3D[] centers, HullVoronoiExploder_ShotHit[] hits, double hullRadius, HullVoronoiExploder_Options options)
        {
            Vector3D[] retVal = new Vector3D[centers.Length];

            foreach (var hit in hits)
            {
                Point3D shotStart = hit.Hit.Item1;
                if (options.InteriorVelocityCenterPercent != null)
                {
                    shotStart += ((hit.Hit.Item2 - hit.Hit.Item1) * options.InteriorVelocityCenterPercent.Value);
                }

                //Vector3D[] velocities = DistributeForces(centers, shotStart, hit.Shot.Item2 * (hit.Shot.Item3 * options.ShotMultiplier), hullRadius, options);
                Vector3D[] velocities = DistributeForces(centers, shotStart, hit.Shot.Item2 * hit.Shot.Item3, hullRadius, options);

                for (int cntr = 0; cntr < retVal.Length; cntr++)
                {
                    retVal[cntr] += velocities[cntr];
                }
            }

            return retVal;
        }
        private static Point3D[] GetVoronoiCtrlPoints(HullVoronoiExploder_ShotHit[] hits, ITriangle[] convexHull, int minCount, int maxCount, double aabbLen)
        {
            const int MIN = 5;

            Random rand = StaticRandom.GetRandomForThread();

            #region examine hits

            Tuple<int, double>[] hitsByLength = hits.
                Select((o, i) => Tuple.Create(i, (o.Hit.Item2 - o.Hit.Item1).Length)).
                OrderByDescending(o => o.Item2).
                ToArray();

            double totalLength = hitsByLength.Sum(o => o.Item2);

            Tuple<int, double>[] hitsByPercentLength = hitsByLength.
                Select(o => Tuple.Create(o.Item1, o.Item2 / totalLength)).
                ToArray();

            #endregion

            #region define control point cones

            double entryRadius = aabbLen * .05;
            double exitRadius = aabbLen * .35;
            double maxAxisLength = aabbLen * .75;

            int count = hits.Length * 2;
            count += (totalLength / (aabbLen * .1)).ToInt_Round();

            if (count < minCount) count = minCount;
            else if (count > maxCount) count = maxCount;

            #endregion

            #region randomly pick control points

            // Keep adding rings around shots until the count is exceeded

            var sets = new List<Tuple<int, List<Point3D>>>();
            int runningSum = 0;

            // Make sure each hit line gets some points
            for (int cntr = 0; cntr < hits.Length; cntr++)
            {
                AddToHitline(ref runningSum, sets, cntr, hits[cntr].Hit, entryRadius, exitRadius, maxAxisLength, rand);
            }

            // Now that all hit lines have points, randomly choose lines until count is exceeded
            while (runningSum < count)
            {
                var pointsPerLength = hitsByLength.
                    Select(o =>
                    {
                        var pointsForIndex = sets.FirstOrDefault(p => p.Item1 == o.Item1);
                        int countForIndex = pointsForIndex == null ? 0 : pointsForIndex.Item2.Count;
                        return Tuple.Create(o.Item1, countForIndex / o.Item2);
                    }).
                    OrderBy(o => o.Item2).
                    ToArray();

                double sumRatio = pointsPerLength.Sum(o => o.Item2);

                var pointsPerLengthNormalized = pointsPerLength.
                    Select(o => Tuple.Create(o.Item1, o.Item2 / sumRatio)).
                    ToArray();

                int index = UtilityCore.GetIndexIntoList(rand.NextPow(3), pointsPerLengthNormalized);

                AddToHitline(ref runningSum, sets, index, hits[index].Hit, entryRadius, exitRadius, maxAxisLength, rand);
            }

            #endregion

            #region remove excessive points

            while (runningSum > count)
            {
                Tuple<int, double>[] fractions = sets.
                    Select((o, i) => Tuple.Create(i, o.Item2.Count.ToDouble() / runningSum.ToDouble())).
                    OrderByDescending(o => o.Item2).
                    ToArray();

                int fractionIndex = UtilityCore.GetIndexIntoList(rand.NextPow(1.5), fractions);     //nextPow will favor the front of the list, which is where the rings with the most points are
                int setIndex = fractions[fractionIndex].Item1;

                sets[setIndex].Item2.RemoveAt(UtilityCore.GetIndexIntoList(rand.NextDouble(), sets[setIndex].Item2.Count));

                runningSum--;
            }

            #endregion

            #region ensure enough for voronoi algorithm

            List<Point3D> retVal = new List<Point3D>();
            retVal.AddRange(sets.SelectMany(o => o.Item2));

            // The voronoi algrorithm fails if there aren't at least 5 points (really should fix that).  So add points that are
            // way away from the hull.  This will make the voronoi algorithm happy, and won't affect the local results
            if (count < MIN)
            {
                retVal.AddRange(
                    Enumerable.Range(0, MIN - count).
                    Select(o => Math3D.GetRandomVector_Spherical_Shell(aabbLen * 20).ToPoint())
                    );
            }

            #endregion

            return retVal.ToArray();
        }