|
|
|
|
/******************************************************************************/
|
|
|
|
|
/*
|
|
|
|
|
Project - MudBun
|
|
|
|
|
Publisher - Long Bunny Labs
|
|
|
|
|
http://LongBunnyLabs.com
|
|
|
|
|
Author - Ming-Lun "Allen" Chou
|
|
|
|
|
http://AllenChou.net
|
|
|
|
|
*/
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
|
|
|
|
|
#if UNITY_EDITOR
|
|
|
|
|
using UnityEditor;
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
using Unity.Collections;
|
|
|
|
|
using UnityEngine;
|
|
|
|
|
|
|
|
|
|
#if MUDBUN_BURST
|
|
|
|
|
using Unity.Burst;
|
|
|
|
|
using Unity.Mathematics;
|
|
|
|
|
using AOT;
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
namespace MudBun
|
|
|
|
|
{
|
|
|
|
|
#if MUDBUN_BURST
|
|
|
|
|
[BurstCompile]
|
|
|
|
|
#endif
|
|
|
|
|
public class MudCurveSimple : MudSolid
|
|
|
|
|
{
|
|
|
|
|
[Header("Shape")]
|
|
|
|
|
|
|
|
|
|
[SerializeField] private float m_elongation = 0.0f;
|
|
|
|
|
public float Elongation { get => m_elongation; set { m_elongation = value; MarkDirty(); } }
|
|
|
|
|
|
|
|
|
|
public Transform PointA;
|
|
|
|
|
public Transform ControlPoint;
|
|
|
|
|
public Transform PointB;
|
|
|
|
|
|
|
|
|
|
[SerializeField] private float m_radiusA = 0.2f;
|
|
|
|
|
public float RadiusA { get => m_radiusA; set { m_radiusA = value; MarkDirty(); } }
|
|
|
|
|
|
|
|
|
|
[SerializeField] private float m_radiusControlPoint = -1.0f;
|
|
|
|
|
public float ControlPointRadius { get => m_radiusControlPoint; set { m_radiusControlPoint = value; MarkDirty(); } }
|
|
|
|
|
|
|
|
|
|
[SerializeField] private float m_radiusB = 0.2f;
|
|
|
|
|
public float RadiusB { get => m_radiusB; set { m_radiusB = value; MarkDirty(); } }
|
|
|
|
|
|
|
|
|
|
[SerializeField] [Range(0.0f, 1.0f)] private float m_smoothStepBlend = 0.0f;
|
|
|
|
|
public float SmoothStepBlend { get =>m_smoothStepBlend; set { m_smoothStepBlend = value; MarkDirty(); } }
|
|
|
|
|
|
|
|
|
|
[Header("Noise")]
|
|
|
|
|
|
|
|
|
|
[SerializeField] private bool m_enableNoise = false;
|
|
|
|
|
[SerializeField] private float m_noiseOffset = 0.0f;
|
|
|
|
|
[SerializeField] private Vector2 m_noiseBaseOctaveSize = 0.5f * Vector2.one;
|
|
|
|
|
[SerializeField] [Range(0.0f, 1.0f)] private float m_noiseThreshold = 0.45f;
|
|
|
|
|
[SerializeField] [Range(0.0f, 1.0f)] private float m_noiseThresholdFade = 0.0f;
|
|
|
|
|
[SerializeField] [Range(-1.0f, 1.0f)] private float m_noiseThresholdCoreBias = 0.0f;
|
|
|
|
|
[SerializeField] [Range(1, 3)] private int m_noiseNumOctaves = 2;
|
|
|
|
|
[SerializeField] private float m_noiseOctaveOffsetFactor = 0.5f;
|
|
|
|
|
[SerializeField] private float m_noiseTwist = 0.0f;
|
|
|
|
|
[SerializeField] private float m_noiseTwistOffset = 0.0f;
|
|
|
|
|
public bool EnableNoise { get => m_enableNoise; set { m_enableNoise = value; MarkDirty(); } }
|
|
|
|
|
public float NoiseOffset { get => m_noiseOffset; set { m_noiseOffset = value; MarkDirty(); } }
|
|
|
|
|
public Vector2 NoiseBaseOctaveSize { get => m_noiseBaseOctaveSize; set { m_noiseBaseOctaveSize = value; MarkDirty(); } }
|
|
|
|
|
public float NoiseThreshold { get => m_noiseThreshold; set { m_noiseThreshold = value; MarkDirty(); } }
|
|
|
|
|
public float NoiseThresholdFade { get => m_noiseThresholdFade; set { m_noiseThresholdFade = value; MarkDirty(); } }
|
|
|
|
|
public float NoiseThresholdCoreBias { get => m_noiseThresholdCoreBias; set { m_noiseThresholdCoreBias = value; MarkDirty(); } }
|
|
|
|
|
public int NoiseNumOctaves { get => m_noiseNumOctaves; set { m_noiseNumOctaves = value; MarkDirty(); } }
|
|
|
|
|
public float NoiseOctaveOffsetFactor { get => m_noiseOctaveOffsetFactor; set { m_noiseOctaveOffsetFactor = value; MarkDirty(); } }
|
|
|
|
|
public float NoiseTwist { get => m_noiseTwist; set { m_noiseTwist = value; MarkDirty(); } }
|
|
|
|
|
public float NoiseTwistOffset { get => m_noiseTwistOffset; set { m_noiseTwistOffset = value; MarkDirty(); } }
|
|
|
|
|
|
|
|
|
|
public override Aabb RawBoundsRs
|
|
|
|
|
{
|
|
|
|
|
get
|
|
|
|
|
{
|
|
|
|
|
if (PointA == null || PointB == null || ControlPoint == null)
|
|
|
|
|
return Aabb.Empty;
|
|
|
|
|
|
|
|
|
|
Vector3 a = PointRs(PointA.position);
|
|
|
|
|
Vector3 b = PointRs(PointB.position);
|
|
|
|
|
Vector3 c = PointRs(ControlPoint.position);
|
|
|
|
|
|
|
|
|
|
Vector3 r = Mathf.Max(m_radiusA, m_radiusB, m_radiusControlPoint) * Vector3.one;
|
|
|
|
|
Aabb bounds = Aabb.Empty;
|
|
|
|
|
bounds.Include(new Aabb(a - r, a + r));
|
|
|
|
|
bounds.Include(new Aabb(b - r, b + r));
|
|
|
|
|
bounds.Include(new Aabb(c - r, c + r));
|
|
|
|
|
|
|
|
|
|
if (m_elongation != 0.0f)
|
|
|
|
|
{
|
|
|
|
|
Vector3 ab = b - a;
|
|
|
|
|
Vector3 ac = c - a;
|
|
|
|
|
Vector3 x = VectorUtil.NormalizeSafe(ab, VectorRs(transform.right));
|
|
|
|
|
Vector3 z = VectorUtil.NormalizeSafe(Vector3.Cross(ab, ac), VectorRs(transform.forward));
|
|
|
|
|
|
|
|
|
|
Vector3 e = m_elongation * VectorUtil.Abs(VectorRs(z));
|
|
|
|
|
bounds.Min -= e;
|
|
|
|
|
bounds.Max += e;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return bounds;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override void SanitizeParameters()
|
|
|
|
|
{
|
|
|
|
|
base.SanitizeParameters();
|
|
|
|
|
|
|
|
|
|
Validate.NonNegative(ref m_elongation);
|
|
|
|
|
|
|
|
|
|
Validate.NonNegative(ref m_radiusA);
|
|
|
|
|
Validate.NonNegative(ref m_radiusB);
|
|
|
|
|
|
|
|
|
|
Validate.NonNegative(ref m_noiseBaseOctaveSize);
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Transform[] m_aPoint = new Transform [] { null, null, null };
|
|
|
|
|
private void Update()
|
|
|
|
|
{
|
|
|
|
|
if (m_aPoint[0] != PointA || m_aPoint[1] != PointB || m_aPoint[2] != ControlPoint)
|
|
|
|
|
MarkDirty();
|
|
|
|
|
|
|
|
|
|
m_aPoint[0] = PointA;
|
|
|
|
|
m_aPoint[1] = PointB;
|
|
|
|
|
m_aPoint[2] = ControlPoint;
|
|
|
|
|
foreach (var p in m_aPoint)
|
|
|
|
|
{
|
|
|
|
|
if (p == null)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
if (!p.hasChanged)
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
MarkDirty();
|
|
|
|
|
p.hasChanged = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Matrix4x4 m_basis;
|
|
|
|
|
|
|
|
|
|
public override int FillComputeData(NativeArray<SdfBrush> aBrush, int iStart, List<Transform> aBone)
|
|
|
|
|
{
|
|
|
|
|
if (PointA == null || PointB == null || ControlPoint == null)
|
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
|
|
Vector3 a = PointA.position;
|
|
|
|
|
Vector3 b = PointB.position;
|
|
|
|
|
Vector3 c = ControlPoint.position;
|
|
|
|
|
Vector3 d = 0.5f * (a + b);
|
|
|
|
|
Vector3 ab = b - a;
|
|
|
|
|
Vector3 ac = c - a;
|
|
|
|
|
Vector3 x = VectorUtil.NormalizeSafe(ab, transform.right);
|
|
|
|
|
Vector3 z = VectorUtil.NormalizeSafe(Vector3.Cross(ab, ac), transform.forward);
|
|
|
|
|
Vector3 y = VectorUtil.NormalizeSafe(Vector3.Cross(z, x), transform.up);
|
|
|
|
|
m_basis = Matrix4x4.TRS(d, RotationRs(Quaternion.LookRotation(z, y)), Vector3.one);
|
|
|
|
|
Matrix4x4 basisInv = m_basis.inverse;
|
|
|
|
|
a = basisInv.MultiplyPoint(a);
|
|
|
|
|
b = basisInv.MultiplyPoint(b);
|
|
|
|
|
c = basisInv.MultiplyPoint(c);
|
|
|
|
|
|
|
|
|
|
int iBrush = iStart;
|
|
|
|
|
SdfBrush brush = SdfBrush.New();
|
|
|
|
|
|
|
|
|
|
bool colinear = Mathf.Abs(Vector3.Dot(VectorUtil.NormalizeSafe(ab, Vector3.forward), VectorUtil.NormalizeSafe(ac, Vector3.forward))) > 0.99999f;
|
|
|
|
|
|
|
|
|
|
brush.Type = (int) SdfBrush.TypeEnum.CurveSimple;
|
|
|
|
|
brush.Data0 = new Vector4(a.x, a.y, a.z, m_radiusA);
|
|
|
|
|
brush.Data1 = new Vector4(b.x, b.y, b.z, m_radiusB);
|
|
|
|
|
brush.Data2 = new Vector4(c.x, c.y, c.z, m_enableNoise ? 1.0f : 0.0f);
|
|
|
|
|
brush.Data3 = new Vector4(m_elongation, m_radiusControlPoint, m_smoothStepBlend, colinear ? 1.0f : 0.0f);
|
|
|
|
|
|
|
|
|
|
if (aBone != null)
|
|
|
|
|
{
|
|
|
|
|
brush.BoneIndex = aBone.Count;
|
|
|
|
|
aBone.Add(PointA);
|
|
|
|
|
aBone.Add(PointB);
|
|
|
|
|
aBone.Add(ControlPoint);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
aBrush[iBrush++] = brush;
|
|
|
|
|
|
|
|
|
|
if (m_enableNoise)
|
|
|
|
|
{
|
|
|
|
|
brush.Type = (int) SdfBrush.TypeEnum.Nop;
|
|
|
|
|
brush.Data0 = new Vector4(m_noiseBaseOctaveSize.x, m_noiseBaseOctaveSize.y, m_noiseBaseOctaveSize.y, m_noiseThreshold);
|
|
|
|
|
brush.Data1 = new Vector4(m_noiseOffset, 0.0f, 0.0f, m_noiseNumOctaves);
|
|
|
|
|
brush.Data2 = new Vector4(m_noiseOctaveOffsetFactor, MathUtil.TwoPi * (m_noiseTwistOffset), MathUtil.TwoPi * (m_noiseTwistOffset + m_noiseTwist), 0.0f);
|
|
|
|
|
brush.Data3 = new Vector4(m_noiseThresholdFade, m_noiseThresholdCoreBias, 0.0f, 0.0f);
|
|
|
|
|
aBrush[iBrush++] = brush;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return iBrush - iStart;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override void FillBrushData(ref SdfBrush brush, int iBrush)
|
|
|
|
|
{
|
|
|
|
|
base.FillBrushData(ref brush, iBrush);
|
|
|
|
|
|
|
|
|
|
brush.Position = PointRs(m_basis.MultiplyPoint(Vector3.zero));
|
|
|
|
|
brush.Rotation = RotationRs(m_basis.rotation);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal override bool IsSelected()
|
|
|
|
|
{
|
|
|
|
|
if (base.IsSelected())
|
|
|
|
|
return true;
|
|
|
|
|
|
|
|
|
|
#if UNITY_EDITOR
|
|
|
|
|
if (PointA?.gameObject != null && Selection.Contains(PointA.gameObject))
|
|
|
|
|
return true;
|
|
|
|
|
|
|
|
|
|
if (PointB?.gameObject != null && Selection.Contains(PointB.gameObject))
|
|
|
|
|
return true;
|
|
|
|
|
|
|
|
|
|
if (ControlPoint?.gameObject != null && Selection.Contains(ControlPoint.gameObject))
|
|
|
|
|
return true;
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override void DrawSelectionGizmosRs()
|
|
|
|
|
{
|
|
|
|
|
base.DrawSelectionGizmosRs();
|
|
|
|
|
|
|
|
|
|
if (PointA == null || PointB == null || ControlPoint == null)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
Vector3 a = PointRs(PointA.position);
|
|
|
|
|
Vector3 b = PointRs(PointB.position);
|
|
|
|
|
Vector3 c = PointRs(ControlPoint.position);
|
|
|
|
|
|
|
|
|
|
int n = 8;
|
|
|
|
|
float t = 0.0f;
|
|
|
|
|
float dt = 1.0f / (n - 1);
|
|
|
|
|
for (int i = 0; i < n; ++i)
|
|
|
|
|
{
|
|
|
|
|
GizmosUtil.DrawInvisibleSphere(VectorUtil.BezierQuad(a, b, c, t), Mathf.Lerp(m_radiusA, m_radiusB, t), Vector3.one, Quaternion.identity);
|
|
|
|
|
t += dt;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#if MUDBUN_BURST
|
|
|
|
|
[BurstCompile]
|
|
|
|
|
[MonoPInvokeCallback(typeof(Sdf.SdfBrushEvalFunc))]
|
|
|
|
|
[RegisterSdfBrushEvalFunc(SdfBrush.TypeEnum.CurveSimple)]
|
|
|
|
|
public static unsafe float EvaluateSdf(float resDummy, ref float3 p, in float3 pRel, SdfBrush* aBrush, int iBrush)
|
|
|
|
|
{
|
|
|
|
|
float res = float.MaxValue;
|
|
|
|
|
var b = aBrush[iBrush];
|
|
|
|
|
float3 pRelAdj = pRel;
|
|
|
|
|
|
|
|
|
|
float3 pA = new float4(b.Data0).xyz;
|
|
|
|
|
float3 pB = new float4(b.Data1).xyz;
|
|
|
|
|
float3 pC = new float4(b.Data2).xyz;
|
|
|
|
|
|
|
|
|
|
float3 pRelRaw = pRelAdj;
|
|
|
|
|
float elongation = b.Data3.x;
|
|
|
|
|
pRelAdj.z -= math.clamp(pRelAdj.z, -elongation, elongation);
|
|
|
|
|
|
|
|
|
|
float controlPointR = b.Data3.y;
|
|
|
|
|
float smoothStepBlend = b.Data3.z;
|
|
|
|
|
float r = 0.0f;
|
|
|
|
|
|
|
|
|
|
bool colinear = b.Data3.w > 0.0f;
|
|
|
|
|
float2 curRes;
|
|
|
|
|
if (b.Data3.w > 0.0f) // colinear?
|
|
|
|
|
Sdf.Segment(pRelAdj, pA, pB, out curRes);
|
|
|
|
|
else
|
|
|
|
|
Sdf.Bezier(pRelAdj, pA, pC, pB, out curRes);
|
|
|
|
|
|
|
|
|
|
if (controlPointR < 0.0f)
|
|
|
|
|
{
|
|
|
|
|
float t = curRes.y;
|
|
|
|
|
r = b.Data0.w + (b.Data1.w - b.Data0.w) * math.lerp(t, math.smoothstep(0.0f, 1.0f, t), smoothStepBlend);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
if (curRes.y < 0.5f)
|
|
|
|
|
{
|
|
|
|
|
float t = 2.0f * curRes.y;
|
|
|
|
|
r = b.Data0.w + (controlPointR - b.Data0.w) * math.lerp(t, math.smoothstep(0.0f, 1.0f, t), smoothStepBlend);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
float t = 2.0f * (curRes.y - 0.5f);
|
|
|
|
|
r = controlPointR + (b.Data1.w - controlPointR) * math.lerp(t, math.smoothstep(0.0f, 1.0f, t), smoothStepBlend);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
res = curRes.x - r;
|
|
|
|
|
|
|
|
|
|
bool useNoise = (b.Data2.w > 0.0f);
|
|
|
|
|
if (useNoise)
|
|
|
|
|
{
|
|
|
|
|
float curveLen = 0.0f;
|
|
|
|
|
int precision = 16;
|
|
|
|
|
float dt = 1.0f / precision;
|
|
|
|
|
float t = dt;
|
|
|
|
|
float3 prevPos = pA;
|
|
|
|
|
for (int i = 1; i < precision; ++i, t += dt)
|
|
|
|
|
{
|
|
|
|
|
float3 currPos;
|
|
|
|
|
MathUtil.BezierQuad(pA, pC, pB, t, out currPos);
|
|
|
|
|
curveLen += math.length(currPos - prevPos);
|
|
|
|
|
prevPos = currPos;
|
|
|
|
|
}
|
|
|
|
|
if (curRes.y < 0.0001f)
|
|
|
|
|
curRes.y = math.min(0.0f, -math.dot(math.normalize(pA - pC), pRelAdj - pA) / curveLen);
|
|
|
|
|
else if (curRes.y > 0.9999f)
|
|
|
|
|
curRes.y = math.max(1.0f, 1.0f + math.dot(math.normalize(pB - pC), pRelAdj - pB) / curveLen);
|
|
|
|
|
|
|
|
|
|
float3 up = math.normalize(new float3(0.0f, 1.0f, 0.0f) + 1e-3f * Sdf.Rand(pA));
|
|
|
|
|
float3 front = math.normalize(math.lerp(pA - pC, pC - pB, curRes.y));
|
|
|
|
|
float3 left = math.normalize(math.cross(up, front));
|
|
|
|
|
up = math.cross(front, left);
|
|
|
|
|
float3 closest;
|
|
|
|
|
MathUtil.BezierQuad(pA, pC, pB, curRes.y, out closest);
|
|
|
|
|
float3 pDelta = pRelRaw - closest;
|
|
|
|
|
float3 s = new float3(curRes.y * curveLen, math.dot(pDelta, up), math.dot(pDelta, left));
|
|
|
|
|
|
|
|
|
|
// advance to additional noise data
|
|
|
|
|
b = aBrush[iBrush + 1];
|
|
|
|
|
|
|
|
|
|
float thresholdFade = b.Data3.x;
|
|
|
|
|
float thresholdCoreBias = b.Data3.y;
|
|
|
|
|
|
|
|
|
|
// twist
|
|
|
|
|
float twistA = b.Data2.y;
|
|
|
|
|
float twistB = b.Data2.z;
|
|
|
|
|
float twistT = math.lerp(twistA, twistB, curRes.y);
|
|
|
|
|
float twistCos = math.cos(twistT);
|
|
|
|
|
float twistSin = math.sin(twistT);
|
|
|
|
|
s.yz = math.mul(new float2x2(twistCos, twistSin, -twistSin, twistCos), s.yz);
|
|
|
|
|
|
|
|
|
|
float3 offset = new float4(b.Data1).xyz;
|
|
|
|
|
float3 size = new float4(b.Data0).xyz;
|
|
|
|
|
float threshold = b.Data0.w;
|
|
|
|
|
float rDelta = math.length(pDelta);
|
|
|
|
|
float coreBiasT = 1.0f - math.saturate(rDelta / math.max(MathUtil.Epsilon, r));
|
|
|
|
|
threshold = math.saturate(threshold + math.sign(thresholdCoreBias) * math.abs(thresholdCoreBias) * coreBiasT);
|
|
|
|
|
threshold += (1.0f - threshold) * thresholdFade * math.saturate(curRes.y);
|
|
|
|
|
int numOctaves = (int) b.Data1.w;
|
|
|
|
|
float octaveOffsetFactor = b.Data2.x;
|
|
|
|
|
|
|
|
|
|
float twistSdfMult = 1.0f / (1.0f + math.saturate(math.abs(twistA - twistB))); // hack: evlauate more surrounding voxels when twisted to avoid holes
|
|
|
|
|
float n = twistSdfMult * Sdf.Noise((int) SdfBrush.NoiseTypeEnum.BakedPerlin, s, float.MinValue, float.MaxValue, offset, size, threshold, numOctaves, octaveOffsetFactor, 8.0f);
|
|
|
|
|
res = Sdf.IntCubic(res, n, 0.5f * r);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return res;
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
public override void DrawOutlineGizmosRs()
|
|
|
|
|
{
|
|
|
|
|
base.DrawOutlineGizmosRs();
|
|
|
|
|
|
|
|
|
|
if (PointA != null)
|
|
|
|
|
{
|
|
|
|
|
GizmosUtil.DrawWireSphere(PointRs(PointA.position), m_radiusA, Vector3.one, RotationRs(PointA.rotation));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (PointB != null)
|
|
|
|
|
{
|
|
|
|
|
GizmosUtil.DrawWireSphere(PointRs(PointB.position), m_radiusB, Vector3.one, RotationRs(PointB.rotation));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (ControlPoint != null)
|
|
|
|
|
{
|
|
|
|
|
float da = (ControlPoint.position - PointA.position).magnitude;
|
|
|
|
|
float db = (ControlPoint.position - PointB.position).magnitude;
|
|
|
|
|
float r = m_radiusControlPoint >= 0.0f ? m_radiusControlPoint : Mathf.Lerp(m_radiusA, m_radiusB, da / (da + db));
|
|
|
|
|
GizmosUtil.DrawWireSphere(PointRs(ControlPoint.position), r, Vector3.one, RotationRs(ControlPoint.rotation));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (PointA != null && PointB != null && ControlPoint != null)
|
|
|
|
|
{
|
|
|
|
|
GizmosUtil.DrawBezierQuad(PointRs(PointA.position), PointRs(PointB.position), PointRs(ControlPoint.position));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|