Librería de códigos C# (Compartan aquí sus códigos)

Iniciado por DarK_FirefoX, 19 Mayo 2015, 18:36 PM

0 Miembros y 1 Visitante están viendo este tema.

z3nth10n

#20
ThreadedDebug: Una utilidad que funciona junto NamedThread & ThreadMarker mostrándote información del Thread actual al debuggear en Unity3D

Código (csharp) [Seleccionar]
using GTAMapper.Extensions.Threading;
using System;
using UnityEngine;

namespace GTAMapper.Utils.Debugging
{
   public static class ThreadedDebug
   {
       private static double TimeRunning
       {
           get
           {
               return (DateTime.Now - System.Diagnostics.Process.GetCurrentProcess().StartTime).TotalSeconds;
           }
       }

       public static void Log(object obj, bool jumpBack = true)
       {
           Debug.Log($"[{ThreadMarker.Name} | {TimeRunning.ToString("F2")}] " + obj.ToString());
       }

       public static void LogFormat(string str, params object[] objs)
       {
           LogFormat($"[{ThreadMarker.Name} | {TimeRunning.ToString("F2")}] " + str, true, objs);
       }

       public static void LogFormat(string str, bool jumpBack = true, params object[] objs)
       {
           Debug.LogFormat($"[{ThreadMarker.Name} | {TimeRunning.ToString("F2")}] " + str, objs);
       }

       public static void LogWarning(object obj, bool jumpBack = true)
       {
           Debug.LogWarning($"[{ThreadMarker.Name} | {TimeRunning.ToString("F2")}] " + obj.ToString());
       }

       public static void LogWarningFormat(string str, params object[] objs)
       {
           LogWarningFormat($"[{ThreadMarker.Name} | {TimeRunning.ToString("F2")}] " + str, true, objs);
       }

       public static void LogWarningFormat(string str, bool jumpBack = true, params object[] objs)
       {
           Debug.LogWarningFormat($"[{ThreadMarker.Name} | {TimeRunning.ToString("F2")}] " + str, objs);
       }

       public static void LogError(object obj, bool jumpBack = true)
       {
           Debug.LogError($"[{ThreadMarker.Name} | {TimeRunning.ToString("F2")}] " + obj.ToString());
       }

       public static void LogErrorFormat(string str, params object[] objs)
       {
           LogErrorFormat($"[{ThreadMarker.Name} | {TimeRunning.ToString("F2")}] " + str, true, objs);
       }

       public static void LogErrorFormat(string str, bool jumpBack = true, params object[] objs)
       {
           Debug.LogErrorFormat($"[{ThreadMarker.Name} | {TimeRunning.ToString("F2")}] " + str, objs);
       }

       public static void LogException(System.Exception ex, bool jumpBack = true)
       {
           Debug.LogException(ex);
       }
   }
}


Básicamente, se usa igual que Debug.LogXX de Unity (https://docs.unity3d.com/ScriptReference/Debug.html) pero este a diferencia, te muesta el momento en el que se ha llamado, y desde el Thread que lo ha hecho.

Ejemplo:



PD: En el ejemplo, vemos dos Count, simplemente, estaba probando que el ConcurrentQueuedCoroutines funcionaba bien, viendo como ConcurrentQueued almacena los datos en todos los contextos usando la instrucción lock(...) internamente.

Interesados hablad por Discord.

z3nth10n

#21
SendMessageContext: Una implementación Thread-Safe de SendMessage

Básicamente, el puñetero de Unity no te deja llamar la función SendMessage desde otro hilo, así que gracias a esta implementación que estuve probando, resolví el problema, para luego darme cuenta de que usaba (blocking) instrucciones en el main thread (dentro de una coroutina gracias a esta utilidad (https://assetstore.unity.com/packages/tools/thread-ninja-multithread-coroutine-15717) es facil/visual cambiar de contexto) :xD (Sí, así somos los programadores cuando tenemos sueño, implementamos cosas que luego ni usaremos)

La utilidad en cuestión: https://forum.unity.com/threads/sendmessage-argumentexception-error.73134/

Mi implementación:

Código (csharp) [Seleccionar]
using System.Collections.Concurrent;
using UnityEngine;

namespace GTAMapper.Extensions.Threading
{
   public class SendMessageHelper : MonoBehaviour
   {
       private static ConcurrentQueue<SendMessageContext> QueuedMessages = new ConcurrentQueue<SendMessageContext>();

       public static void RegisterSendMessage(SendMessageContext context)
       {
           QueuedMessages.Enqueue(context);
       }

       private void Update()
       {
           if (QueuedMessages.Count > 0)
           {
               SendMessageContext context = null;

               if (!QueuedMessages.TryDequeue(out context))
                   return;

               context.Target.SendMessage(context.MethodName, context.Value, context.Options);
           }
       }
   }
}


Nota: No olvidéis añadir esto al inspector de cualquier gameobject donde se quiera usar (con añadir uno solo bastaría)

Código (csharp) [Seleccionar]
using UnityEngine;

namespace GTAMapper.Extensions.Threading
{
   public class SendMessageContext
   {
       public MonoBehaviour Target;
       public string MethodName;
       public object Value;
       public SendMessageOptions Options = SendMessageOptions.RequireReceiver;

       public SendMessageContext(MonoBehaviour target, string methodName, object value, SendMessageOptions options)
       {
           this.Target = target;
           this.MethodName = methodName;
           this.Value = value;
           this.Options = options;
       }
   }
}


Algo que en el topic de arriba no está ;D

Código (csharp) [Seleccionar]
using UnityEngine;

namespace GTAMapper.Extensions.Threading
{
   public static class SendMessageExtensions
   {
       public static SendMessageContext SendSafeMessage(this MonoBehaviour monoBehaviour, string methodName, object value = null, SendMessageOptions sendMessageOptions = default(SendMessageOptions))
       {
           return new SendMessageContext(monoBehaviour, methodName, value, sendMessageOptions);
       }
   }
}


Con esta extensión lo que se consigue es simplificar su uso, básicamente, desde un MonoBehaviour cualquiera dentro del metodo Start() (https://docs.unity3d.com/ScriptReference/MonoBehaviour.html) haciendo esto:

Código (csharp) [Seleccionar]
this.SendSafeMessage("pepito")

Y luego (desde el mismo MonoBehaviour):

Código (csharp) [Seleccionar]
public void pepito() {
   // Moar code que seguirá ejecutándose en el mismo hilo desde el que se llamo "SendSafeMessage"
}


Un saludo.
PD: Aquí termina el flood de snippets, staff no preocuparse, llevo muchos días picando código, y he querido compartir post a post mis utilidades :xD
PD2: Ale ya puedo borrar todo lo que no uso. Que no es poco. ;-) ;-)

Interesados hablad por Discord.

z3nth10n

#22
Simplificar una lista de puntos para obtener las esquinas (dicho de otra forma, obtener la minima cantidad de puntos que puedan definir dicha forma)

Esto ya lo respondí en Stackoverflow: https://stackoverflow.com/a/52952874/3286975 (Pero lo traduciré)

CitarDespués de horas de investigación he encontrado una librería Simplify.NET que internamente ejecuta el algoritmo de Ramer–Douglas–Peucker.

También, os puede interesar el algoritmo de Bresenham, con este algoritmo podéis dibujar una linea a partir de dos puntos.

Con este algoritmo, se podría comprobar si la tolerancia del primer algoritmo es muy alta (comprobando los puntos generados y los que ya tenías antes de simplificar, haciendo una función que te devuelva un porcentaje de similitud). Por suerte, la implementación de Simplify.NET es rápida (unos 25.000 ticks de Stopwatch), llamandose unas 10 veces solo llevaría unos ~30 ms.

Finalmente, es interesante mencionar el algoritmo Concave Hull y Convex Hull (y sus respectivas implementaciones en Unity3D).

Lo que he obtenido con esta implementación:





Nota:

  • Los puntos azules son los puntos iterados.
  • Los puntos verdes son los puntos simplificados.

Y mi implementación: https://dotnetfiddle.net/aPOhPi

Es muy importante decir que los puntos deben de estar ordenados de tal forma que estén conectados. Si la forma es concava (como se puede ver en la segunda foto) quizás necesites una implementación que itere pixel a pixel dentro de la pared de la figura.

Aquí podéis ver una implementación gracias a Bunny83.

Un saludo.

Interesados hablad por Discord.

z3nth10n

#23
Dibujar curvas a partir de Perlin Noise (de forma acotada)

Por acotada me refiero, a que puedes establecer el centro del ruido y su amplitud.

Enlace: https://github.com/z3nth10n/Unity-Curve-Drawer

Imagen:



Un saludo.

Interesados hablad por Discord.

z3nth10n

#24
Rasterizar un triangulo cualquiera

Hoy estoy felis, ya que he conseguido rasterizar un triangulo. Y me diréis pues muy bien no? Vale, os explico.

Rasterizar es esto. Es decir, pintar por pantalla los pixeles que conforman un poligono cualquiera. Bueno sí... Pero lo que no os he dicho que un poligono cualquiera se puede triangular, es decir, como en el post anterior al igual que con Ramer-Douglas-Pecker puedes conseguir el minimo de puntos que definen un poligono cualquiera, con triangulación consigues algo parecido, ya que si lo pensamos, un triangulo es la minima expresión de poligono, es decir, el poligono con menos vertices y que de sumas de ellos podemos definir los demas.

Si queréis conseguir una buena triangulación os recomiendo LibTessDotNet. Funciona muy bien.

Esto es muy potente ya que puedes mostrar por pantalla (aunque para esto ya está DirectX y OpenGL, internamente, esto que comento es un trabajo que la GPU hace muy bien ya que se trata de repetir), hacer procesos más complejos (que es por lo que fundamental lo necesito yo) o bien hacer librerías de pago de rasterización para Unity :xD.

Así que aquí os dejo mi buena *****:

Código (csharp) [Seleccionar]
using System;
using UnityEngine;

namespace UnityEngine.Utilities.Drawing
{
   // Uncomment this if you don't a an own implementation
   /*public struct Point
   {
       public int x;
       public int y;

       public Point(int x, int y)
       {
           this.x = 0;
           this.y = 0;
       }
   }*/

   public static class TriangleUtils
   {
       public static void Rasterize(Point pt1, Point pt2, Point pt3, Action<int, int> action, bool debug = false)
       {
           /*
                // https://www.geeksforgeeks.org/check-whether-triangle-valid-not-sides-given/
                a + b > c
                a + c > b
                b + c > a
            */

           if (!CheckIfValidTriangle(pt1, pt2, pt3, debug))
               return;

           if (TriangleArea(pt1, pt2, pt3) <= 1)
           {
               Point center = GetTriangleCenter(pt1, pt2, pt3);
               action?.Invoke(center.x, center.y);

               return;
           }

           Point tmp;

           if (pt2.x < pt1.x)
           {
               tmp = pt1;
               pt1 = pt2;
               pt2 = tmp;
           }

           if (pt3.x < pt2.x)
           {
               tmp = pt2;
               pt2 = pt3;
               pt3 = tmp;

               if (pt2.x < pt1.x)
               {
                   tmp = pt1;
                   pt1 = pt2;
                   pt2 = tmp;
               }
           }

           var baseFunc = CreateFunc(pt1, pt3);
           var line1Func = pt1.x == pt2.x ? (x => pt2.y) : CreateFunc(pt1, pt2);

           for (var x = pt1.x; x < pt2.x; ++x)
           {
               int maxY;
               int minY = GetRange(line1Func(x), baseFunc(x), out maxY);

               for (int y = minY; y <= maxY; ++y)
                   action?.Invoke(x, y);
           }

           var line2Func = pt2.x == pt3.x ? (x => pt2.y) : CreateFunc(pt2, pt3);

           for (var x = pt2.x; x <= pt3.x; ++x)
           {
               int maxY;
               int minY = GetRange(line2Func(x), baseFunc(x), out maxY);

               for (int y = minY; y <= maxY; ++y)
                   action?.Invoke(x, y);
           }
       }

       private static int GetRange(float y1, float y2, out int maxY)
       {
           if (y1 < y2)
           {
               maxY = Mathf.FloorToInt(y2);
               return Mathf.CeilToInt(y1);
           }

           maxY = Mathf.FloorToInt(y1);
           return Mathf.CeilToInt(y2);
       }

       private static Func<int, float> CreateFunc(Point pt1, Point pt2)
       {
           var y0 = pt1.y;

           if (y0 == pt2.y)
               return x => y0;

           float m = (float)(pt2.y - y0) / (pt2.x - pt1.x);

           return x => m * (x - pt1.x) + y0;
       }

       public static void RasterizeStandard(Point p1, Point p2, Point p3, Action<int, int> action, bool debug = false)
       {
           // Thanks to: https://www.davrous.com/2013/06/21/tutorial-part-4-learning-how-to-write-a-3d-software-engine-in-c-ts-or-js-rasterization-z-buffering/

           if (!CheckIfValidTriangle(p1, p2, p3, debug))
               return;

           // Sorting the points in order to always have this order on screen p1, p2 & p3
           // with p1 always up (thus having the y the lowest possible to be near the top screen)
           // then p2 between p1 & p3
           if (p1.y > p2.y)
           {
               var temp = p2;
               p2 = p1;
               p1 = temp;
           }

           if (p2.y > p3.y)
           {
               var temp = p2;
               p2 = p3;
               p3 = temp;
           }

           if (p1.y > p2.y)
           {
               var temp = p2;
               p2 = p1;
               p1 = temp;
           }

           // inverse slopes
           float dP1P2, dP1P3;

           // http://en.wikipedia.org/wiki/Slope
           // Computing inverse slopes
           if (p2.y - p1.y > 0)
               dP1P2 = (p2.x - p1.x) / (p2.y - p1.y);
           else
               dP1P2 = 0;

           if (p3.y - p1.y > 0)
               dP1P3 = (p3.x - p1.x) / (p3.y - p1.y);
           else
               dP1P3 = 0;

           // First case where triangles are like that:
           // P1
           // -
           // --
           // - -
           // -  -
           // -   - P2
           // -  -
           // - -
           // -
           // P3
           if (dP1P2 > dP1P3)
           {
               for (var y = p1.y; y <= p3.y; y++)
               {
                   if (y < p2.y)
                       ProcessScanLine(y, p1, p3, p1, p2, action);
                   else
                       ProcessScanLine(y, p1, p3, p2, p3, action);
               }
           }
           // First case where triangles are like that:
           //       P1
           //        -
           //       --
           //      - -
           //     -  -
           // P2 -   -
           //     -  -
           //      - -
           //        -
           //       P3
           else
           {
               for (var y = p1.y; y <= p3.y; y++)
               {
                   if (y < p2.y)
                       ProcessScanLine(y, p1, p2, p1, p3, action);
                   else
                       ProcessScanLine(y, p2, p3, p1, p3, action);
               }
           }
       }

       // drawing line between 2 points from left to right
       // papb -> pcpd
       // pa, pb, pc, pd must then be sorted before
       private static void ProcessScanLine(int y, Point pa, Point pb, Point pc, Point pd, Action<int, int> action)
       {
           // Thanks to current y, we can compute the gradient to compute others values like
           // the starting x (sx) and ending x (ex) to draw between
           // if pa.y == pb.y or pc.y == pd.y, gradient is forced to 1
           var gradient1 = pa.y != pb.y ? (y - pa.y) / (pb.y - pa.y) : 1;
           var gradient2 = pc.y != pd.y ? (y - pc.y) / (pd.y - pc.y) : 1;

           int sx = (int)Mathf.Lerp(pa.x, pb.x, gradient1);
           int ex = (int)Mathf.Lerp(pc.x, pd.x, gradient2);

           // drawing a line from left (sx) to right (ex)
           for (var x = sx; x < ex; x++)
               action?.Invoke(x, y);
       }

       public static void RasterizeBarycentric(Point v1, Point v2, Point v3, Action<int, int> action, bool debug = false)
       {
           // Thanks to: http://www.sunshine2k.de/coding/java/TriangleRasterization/TriangleRasterization.html#algo3 && https://www.cs.unm.edu/~joel/cs491_VR/src/DrawUtilities.cs

           /* checks for a valid triangle */

           if (!CheckIfValidTriangle(v1, v2, v3, debug))
               return;

           /* get the bounding box of the triangle */
           int maxX = Mathf.Max(v1.x, Mathf.Max(v2.x, v3.x));
           int minX = Mathf.Min(v1.x, Mathf.Min(v2.x, v3.x));
           int maxY = Mathf.Max(v1.y, Mathf.Max(v2.y, v3.y));
           int minY = Mathf.Min(v1.y, Mathf.Min(v2.y, v3.y));

           //if (debug)
           //    Debug.Log($"({minX}, {minY}, {maxX}, {maxY})");

           /* spanning vectors of edge (v1,v2) and (v1,v3) */
           Point vs1 = new Point(v2.x - v1.x, v2.y - v1.y);
           Point vs2 = new Point(v3.x - v1.x, v3.y - v1.y);

           for (int x = minX; x <= maxX; x++)
           {
               for (int y = minY; y <= maxY; y++)
               {
                   Point q = new Point(x - v1.x, y - v1.y);

                   float s = Vector2.Dot(q, vs2) / Vector2.Dot(vs1, vs2);
                   float t = Vector2.Dot(vs1, q) / Vector2.Dot(vs1, vs2);

                   if ((s >= 0) && (t >= 0) && (s + t <= 1))
                   { /* inside triangle */
                       action?.Invoke(x, y);
                   }
               }
           }
       }

       public static void RasterizeBresenham(Point vt1, Point vt2, Point vt3, Action<int, int> action, bool debugException = false)
       {
           // Thanks to: http://www.sunshine2k.de/coding/java/TriangleRasterization/TriangleRasterization.html#algo3 && https://www.cs.unm.edu/~joel/cs491_VR/src/DrawUtilities.cs

           /* checks for a valid triangle */

           if (!CheckIfValidTriangle(vt1, vt2, vt3, debugException))
               return;

           string invalidTriangle = $"The given points must form a triangle. {{{vt1}, {vt2}, {vt3}}}";

           /* at first sort the three vertices by y-coordinate ascending so v1 is the topmost vertice */
           sortVerticesAscendingByY(ref vt1, ref vt2, ref vt3);

           /* here we know that v1.y <= v2.y <= v3.y */
           /* check for trivial case of bottom-flat triangle */
           if (vt2.y == vt3.y)
           {
               if (!fillBottomFlatTriangle(vt1, vt2, vt3, action, debugException))
               {
                   if (debugException)
                       Debug.LogWarning(invalidTriangle);

                   return;
               }
           }
           /* check for trivial case of top-flat triangle */
           else if (vt1.y == vt2.y)
           {
               if (!fillTopFlatTriangle(vt1, vt2, vt3, action, debugException))
               {
                   if (debugException)
                       Debug.LogWarning(invalidTriangle);

                   return;
               }
           }
           else
           {
               /* general case - split the triangle in a topflat and bottom-flat one */
               Point v4 = new Point((int)(vt1.x + (vt2.y - vt1.y) / (float)(vt3.y - vt1.y) * (vt3.x - vt1.x)), vt2.y);

               if (!fillBottomFlatTriangle(vt1, vt2, v4, action, debugException) || !fillTopFlatTriangle(vt2, v4, vt3, action, debugException))
               {
                   if (debugException)
                       Debug.LogWarning(invalidTriangle);

                   return;
               }
           }
       }

       public static bool CheckIfValidTriangle(Point v1, Point v2, Point v3, bool debug = false)
       {
           /*
               // https://www.geeksforgeeks.org/check-whether-triangle-valid-not-sides-given/
               a + b > c
               a + c > b
               b + c > a
           */

           float a = Vector2.Distance(new Vector2(v1.x, v1.y), new Vector2(v2.x, v2.y)),
                 b = Vector2.Distance(new Vector2(v2.x, v2.y), new Vector2(v3.x, v3.y)),
                 c = Vector2.Distance(new Vector2(v3.x, v3.y), new Vector2(v1.x, v1.y));

           if (a + b <= c || a + c <= b || b + c <= a)
           {
               if (debug)
                   Debug.LogWarning($"The given points must form a triangle. {{{v1}, {v2}, {v3}}}");

               return false;
           }

           return true;
       }

       public static bool CheckIfValidTriangle(Point v1, Point v2, Point v3, out float a, out float b, out float c)
       {
           a = Vector2.Distance(new Vector2(v1.x, v1.y), new Vector2(v2.x, v2.y));
           b = Vector2.Distance(new Vector2(v2.x, v2.y), new Vector2(v3.x, v3.y));
           c = Vector2.Distance(new Vector2(v3.x, v3.y), new Vector2(v1.x, v1.y));

           if (a + b <= c || a + c <= b || b + c <= a)
           {
               // Debug.LogWarning($"The given points must form a triangle. {{{v1}, {v2}, {v3}}}");
               return false;
           }

           return true;
       }

       private static bool fillBottomFlatTriangle(Point v1, Point v2, Point v3, Action<int, int> action, bool debugException = false)
       {
           try
           {
               float invslope1 = (v2.x - v1.x) / (v2.y - v1.y);
               float invslope2 = (v3.x - v1.x) / (v3.y - v1.y);

               float curx1 = v1.x;
               float curx2 = v1.x;

               for (int scanlineY = v1.y; scanlineY <= v2.y; scanlineY++)
               {
                   DrawLine((int)curx1, scanlineY, (int)curx2, scanlineY, action);

                   curx1 += invslope1;
                   curx2 += invslope2;
               }
           }
           catch (Exception ex)
           {
               if (debugException)
                   Debug.LogException(ex);

               return false;
           }

           return true;
       }

       private static bool fillTopFlatTriangle(Point v1, Point v2, Point v3, Action<int, int> action, bool debugException = false)
       {
           try
           {
               float invslope1 = (v3.x - v1.x) / (v3.y - v1.y);
               float invslope2 = (v3.x - v2.x) / (v3.y - v2.y);

               float curx1 = v3.x;
               float curx2 = v3.x;

               for (int scanlineY = v3.y; scanlineY > v1.y; scanlineY--)
               {
                   DrawLine((int)curx1, scanlineY, (int)curx2, scanlineY, action);
                   curx1 -= invslope1;
                   curx2 -= invslope2;
               }
           }
           catch (Exception ex)
           {
               if (debugException)
                   Debug.LogException(ex);

               return false;
           }

           return true;
       }

       private static void sortVerticesAscendingByY(ref Point v0, ref Point v1, ref Point v2)
       {
           if (v2.y < v1.y)
               Swap(ref v1, ref v2);

           if (v1.y < v0.y)
               Swap(ref v0, ref v1);

           if (v2.y < v1.y)
               Swap(ref v1, ref v2);
       }

       private static void Swap<T>(ref T lhs, ref T rhs)
       {
           (lhs, rhs) = (rhs, lhs);

           // Impl for versions lower than C# 7

           //T temp;
           //temp = lhs;
           //lhs = rhs;
           //rhs = temp;
       }

       public static float TriangleArea(Point p1, Point p2, Point p3)
       {
           float a, b, c;

           if (!CheckIfValidTriangle(p1, p2, p3, out a, out b, out c))
               return 0;

           return TriangleArea(a, b, c);
       }

       public static float TriangleArea(float a, float b, float c)
       {
           // Thanks to: http://james-ramsden.com/area-of-a-triangle-in-3d-c-code/

           float s = (a + b + c) / 2.0f;
           return Mathf.Sqrt(s * (s - a) * (s - b) * (s - c));
       }

       // Bresenham impl: https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm

       public static void DrawLine(Vector2 p1, Vector2 p2, Action<int, int> action)
       {
           DrawLine((int)p1.x, (int)p1.y, (int)p2.x, (int)p2.y, action);
       }

       public static void DrawLine(int x0, int y0, int x1, int y1, Action<int, int> action)
       {
           int sx = 0,
               sy = 0;

           int dx = Mathf.Abs(x1 - x0),
               dy = Mathf.Abs(y1 - y0);

           if (x0 < x1) { sx = 1; } else { sx = -1; }
           if (y0 < y1) { sy = 1; } else { sy = -1; }

           int err = dx - dy,
               e2 = 0;

           while (true)
           {
               //colors[P(x0 % width, y0 % height, width, height)] = x0 / width >= 1 || y0 / height >= 1 ? UnityEngine.Color.yellow : c;
               action?.Invoke(x0, y0);

               if ((x0 == x1) && (y0 == y1))
                   break;

               e2 = 2 * err;

               if (e2 > -dy)
               {
                   err = err - dy;
                   x0 = x0 + sx;
               }
               if (e2 < dx)
               {
                   err = err + dx;
                   y0 = y0 + sy;
               }
           }
       }

       public static Point GetTriangleCenter(Point p0, Point p1, Point p2)
       {
           // Thanks to: https://stackoverflow.com/questions/524755/finding-center-of-2d-triangle

           return new Point(p0.x + p1.x + p2.x / 3, p0.y + p1.y + p2.y / 3);
       }
   }
}


Gist: https://gist.github.com/z3nth10n/7d60f22c7e906f645d53c9622507c23b (dadle una estrellica, no me seais gitanos :laugh: :laugh:)

Vídeo de ejemplo:

[youtube=640,360]https://youtu.be/7yY3MIyRtPw[/youtube]

Un saludo.

Interesados hablad por Discord.