428 lines
12 KiB
C#
428 lines
12 KiB
C#
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using ToriaCommon.Datas.Types;
|
|
using UnityEngine;
|
|
using Color = UnityEngine.Color;
|
|
using Material = UnityEngine.Material;
|
|
|
|
namespace Wing.Gizmos
|
|
{
|
|
/*
|
|
* NOTE:
|
|
* This code comes from an old Gizmos package, it has been modified a long time ago (legacy code).
|
|
* For improve the system and clean the code I have to modified it again.
|
|
* Now it's possible to specify around which axis rotate.
|
|
*
|
|
* With all these changes, this script has become dependent on ToriaCommon. It is no longer a package script.
|
|
*/
|
|
public class GizmosRotate : MonoBehaviour
|
|
{
|
|
public delegate void RotateEventHandler(float dx, float dy, float dz);
|
|
|
|
public event RotateEventHandler OnRotate;
|
|
public event Action OnRotateEnd;
|
|
|
|
const float interactionAngle = 20; //angle on each side of an arc where it is interactible to track it
|
|
|
|
public Camera RenderCamera;
|
|
public float RotateSpeed = 1;
|
|
[SerializeField] float radius = .1f; //scale of the sphere
|
|
public bool EnableFreeRotate = false;
|
|
|
|
[Header("Colors")]
|
|
[SerializeField] Color xColor = new(1, 0, 0, 0.5f);
|
|
[SerializeField] Color yColor = new(0, 1, 0, 0.5f);
|
|
[SerializeField] Color zColor = new(0, 0, 1, 0.5f);
|
|
[SerializeField] Color selectColor = new(1, 0.92f, 0.016f, 0.5f);
|
|
|
|
private Vector3 mTotal = Vector3.zero;
|
|
readonly private List<int> verticesCount = new();
|
|
|
|
private float m_fTheta = 0.1f; //arc precision (mesh generation)
|
|
private GameObject m_pSphere = null;
|
|
private GameObject m_pMeshGo = null;
|
|
private Mesh m_pMesh;
|
|
|
|
private int m_iLayer = 0;
|
|
|
|
private Matrix4x4[] m_arrMats = null;
|
|
|
|
private AxisXYZ trackedAxis = AxisXYZ.None;
|
|
private AxisXYZ allowedAxis = AxisXYZ.All;
|
|
private bool rotationChanged = false;
|
|
|
|
private Vector3 lastMousePosition = Vector3.zero;
|
|
|
|
void Awake()
|
|
{
|
|
if (RenderCamera == null)
|
|
{
|
|
RenderCamera = Camera.main;
|
|
}
|
|
|
|
m_iLayer = LayerMask.NameToLayer(Settings.GizmosLayerName);
|
|
|
|
m_arrMats = new Matrix4x4[]
|
|
{
|
|
Matrix4x4.TRS(Vector3.zero, Quaternion.Euler(0, 0, 90), Vector3.one),
|
|
Matrix4x4.identity,
|
|
Matrix4x4.TRS(Vector3.zero, Quaternion.Euler(90, 0, 0), Vector3.one)
|
|
};
|
|
|
|
CreateSphere();
|
|
CreateXYZCircles();
|
|
}
|
|
|
|
#region Init
|
|
public void Initialize(float radius, Quaternion rotation, AxisXYZ allowedAxis, bool enableFreeRotation = false)
|
|
{
|
|
//Destroy previous XYZ circles
|
|
Destroy(m_pSphere.transform.GetChild(0).gameObject);
|
|
|
|
m_pSphere.transform.rotation = rotation;
|
|
EnableFreeRotate = enableFreeRotation;
|
|
this.radius = radius;
|
|
this.allowedAxis = allowedAxis;
|
|
|
|
CreateXYZCircles();
|
|
}
|
|
|
|
private void CreateSphere()
|
|
{
|
|
m_pSphere = GameObject.CreatePrimitive(PrimitiveType.Sphere);
|
|
m_pSphere.transform.SetParent(transform);
|
|
m_pSphere.transform.localPosition = Vector3.zero;
|
|
RefreshScale();
|
|
|
|
m_pSphere.GetComponent<SphereCollider>().isTrigger = true;
|
|
m_pSphere.layer = m_iLayer;
|
|
|
|
MeshRenderer render = m_pSphere.GetComponent<MeshRenderer>();
|
|
render.material = new Material(Shader.Find("TransparentShader"));
|
|
render.material.color = new Color(0.5f, 0.5f, 0.5f, 0.2f);
|
|
|
|
//m_pSphere.GetComponent<MeshRenderer>().enabled = false;
|
|
}
|
|
|
|
private void CreateXYZCircles()
|
|
{
|
|
m_pMesh = new Mesh();
|
|
m_pMesh.MarkDynamic();
|
|
int verticesCount;
|
|
|
|
//x
|
|
if (IsAxisAllowed(AxisXYZ.X))
|
|
{
|
|
verticesCount = CreateArc(xColor, m_pMesh, m_arrMats[0]);
|
|
this.verticesCount.Add(verticesCount);
|
|
}
|
|
|
|
//y
|
|
if (IsAxisAllowed(AxisXYZ.Y))
|
|
{
|
|
verticesCount = CreateArc(yColor, m_pMesh, m_arrMats[1]);
|
|
this.verticesCount.Add(verticesCount);
|
|
}
|
|
|
|
//z
|
|
if (IsAxisAllowed(AxisXYZ.Z))
|
|
{
|
|
verticesCount = CreateArc(zColor, m_pMesh, m_arrMats[2]);
|
|
this.verticesCount.Add(verticesCount);
|
|
}
|
|
|
|
//Mesh Generation
|
|
m_pMesh.subMeshCount = 1;
|
|
m_pMesh.SetIndices(SequentialTriangles(m_pMesh.vertices.Length), MeshTopology.Lines, 0);
|
|
|
|
m_pMeshGo = new GameObject("Rotate Axis");
|
|
m_pMeshGo.transform.SetParent(m_pSphere.transform, false);
|
|
m_pMeshGo.layer = m_iLayer;
|
|
m_pMeshGo.AddComponent<MeshFilter>().mesh = m_pMesh;
|
|
m_pMeshGo.AddComponent<MeshRenderer>().material = new Material(Shader.Find("RotateShader"));
|
|
|
|
//reset circles mesh transform
|
|
m_pMeshGo.transform.localPosition = Vector3.zero;
|
|
m_pMeshGo.transform.localRotation = Quaternion.identity;
|
|
m_pMeshGo.transform.localScale = Vector3.one / 2;
|
|
}
|
|
|
|
private int CreateArc(Color color, Mesh mesh, Matrix4x4 mat, float angle = 2 * Mathf.PI, bool close = false)
|
|
{
|
|
m_fTheta = Mathf.Max(0.0001f, m_fTheta);
|
|
|
|
int count = mesh.vertexCount;
|
|
List<Vector3> vertices = new List<Vector3>(mesh.vertices);
|
|
List<Color> colors = new List<Color>(mesh.colors);
|
|
|
|
// draw circle
|
|
Vector3 beginPoint = mat * Vector3.zero;
|
|
Vector3 firstPoint = mat * Vector3.zero;
|
|
for (float theta = 0; theta < angle; theta += m_fTheta)
|
|
{
|
|
float x = Mathf.Cos(theta);
|
|
float z = Mathf.Sin(theta);
|
|
Vector3 endPoint = mat * new Vector3(x, 0, z);
|
|
if (theta == 0 && !close)
|
|
{
|
|
firstPoint = endPoint;
|
|
}
|
|
else
|
|
{
|
|
vertices.Add(beginPoint);
|
|
vertices.Add(endPoint);
|
|
|
|
colors.Add(color);
|
|
colors.Add(color);
|
|
}
|
|
beginPoint = endPoint;
|
|
}
|
|
|
|
// draw last line
|
|
vertices.Add(firstPoint);
|
|
vertices.Add(beginPoint);
|
|
|
|
colors.Add(color);
|
|
colors.Add(color);
|
|
|
|
mesh.SetVertices(vertices);
|
|
mesh.SetColors(colors);
|
|
|
|
return vertices.Count - count;
|
|
}
|
|
|
|
private static int[] SequentialTriangles(int len)
|
|
{
|
|
int[] array = new int[len];
|
|
for (int i = 0; i < len; i++)
|
|
{
|
|
array[i] = i;
|
|
}
|
|
return array;
|
|
}
|
|
#endregion
|
|
|
|
public Quaternion GetRotation()
|
|
{
|
|
return m_pSphere.transform.rotation;
|
|
}
|
|
|
|
private void Update()
|
|
{
|
|
if (Input.GetMouseButtonDown(0))
|
|
StartTracking();
|
|
|
|
if (Input.GetMouseButtonUp(0))
|
|
StopTracking();
|
|
|
|
RotateAroundTrackedAxis();
|
|
RefreshScale();
|
|
}
|
|
|
|
private void StartTracking()
|
|
{
|
|
lastMousePosition = Input.mousePosition;
|
|
Ray ray = RenderCamera.ScreenPointToRay(lastMousePosition);
|
|
|
|
//Try select closest axis and start tracking it
|
|
if (Physics.Raycast(ray, out RaycastHit hit, 10000, 1 << m_iLayer, QueryTriggerInteraction.Collide))
|
|
{
|
|
AxisXYZ axis = FindClosetAxis(hit.point);
|
|
|
|
SelectAxis(axis);
|
|
rotationChanged = false;
|
|
}
|
|
}
|
|
|
|
private void StopTracking()
|
|
{
|
|
SelectAxis(AxisXYZ.None);
|
|
|
|
if (rotationChanged)
|
|
{
|
|
StartCoroutine(DelayRelease());
|
|
rotationChanged = false;
|
|
}
|
|
}
|
|
|
|
private void RefreshScale()
|
|
{
|
|
m_pSphere.transform.localScale = (Vector3.Distance(RenderCamera.transform.position, m_pSphere.transform.position) / 4) * radius * Vector3.one;
|
|
}
|
|
|
|
private void RotateAroundTrackedAxis()
|
|
{
|
|
if (!IsAxisAllowed(trackedAxis))
|
|
return;
|
|
|
|
Vector3 delta = TrackBall(Input.mousePosition);
|
|
|
|
//Snap rotation
|
|
//NOTE: the snap isn't perfect at the moment, so let it disable.
|
|
//It snap axis by axis so when you touch all the axis, the final result isn't correctly snapped.
|
|
//To test the snap, you must activate it beforehand with this line: SnapFactory.Instance.Get(ESnapType.Rotate).Enable = true;
|
|
Vector3 total = Vector3.zero;
|
|
ISnap snap = SnapFactory.Instance.Get(ESnapType.Rotate);
|
|
if (snap.Snap(mTotal + delta, ref total))
|
|
{
|
|
delta = total - mTotal;
|
|
}
|
|
|
|
mTotal += delta;
|
|
mTotal.x %= 360;
|
|
mTotal.y %= 360;
|
|
mTotal.z %= 360;
|
|
//////////////
|
|
|
|
if (delta == Vector3.zero)
|
|
return;
|
|
|
|
m_pSphere.transform.Rotate(delta);
|
|
rotationChanged = true;
|
|
OnRotate?.Invoke(delta.x, delta.y, delta.z);
|
|
}
|
|
|
|
private IEnumerator DelayRelease()
|
|
{
|
|
yield return new WaitForEndOfFrame();
|
|
|
|
// OnRotateEnd(m_eCurrentAxis, GetRotate());
|
|
OnRotateEnd?.Invoke();
|
|
}
|
|
|
|
private Vector3 ComputeRotateDir(Vector3 offset, AxisXYZ type)
|
|
{
|
|
if (offset.x + offset.y == 0)
|
|
{
|
|
return Vector3.zero;
|
|
}
|
|
Vector3 startPos = m_pSphere.transform.position;
|
|
Vector3 start = Camera.main.WorldToScreenPoint(startPos);
|
|
Vector3 axis;
|
|
Vector3 end;
|
|
|
|
if (type == AxisXYZ.X)
|
|
{
|
|
end = Camera.main.WorldToScreenPoint(startPos + m_pSphere.transform.right * 2);
|
|
axis = Vector3.right;
|
|
}
|
|
else if (type == AxisXYZ.Y)
|
|
{
|
|
end = Camera.main.WorldToScreenPoint(startPos + m_pSphere.transform.up * 2);
|
|
axis = Vector3.up;
|
|
}
|
|
else
|
|
{
|
|
end = Camera.main.WorldToScreenPoint(startPos + m_pSphere.transform.forward * 2);
|
|
axis = Vector3.forward;
|
|
}
|
|
|
|
Vector3 axisDir = end - start;
|
|
axisDir.Normalize();
|
|
|
|
axisDir = Vector3.Cross(Vector3.forward, axisDir);
|
|
|
|
float moveLen = axisDir.x * offset.x + axisDir.y * offset.y;
|
|
|
|
float mx = moveLen * axis.x;
|
|
float my = moveLen * axis.y;
|
|
float mz = moveLen * axis.z;
|
|
return new Vector3(mx, my, mz);
|
|
}
|
|
|
|
private Vector3 TrackBall(Vector3 screenPosition)
|
|
{
|
|
Vector3 offset = (screenPosition - lastMousePosition) * RotateSpeed;
|
|
lastMousePosition = screenPosition;
|
|
|
|
Vector3 angle = Vector3.zero;
|
|
if (trackedAxis == AxisXYZ.All)
|
|
{
|
|
Quaternion q = Quaternion.AngleAxis(offset.y, m_pSphere.transform.InverseTransformVector(RenderCamera.transform.right));
|
|
q *= Quaternion.AngleAxis(-offset.x, m_pSphere.transform.InverseTransformVector(RenderCamera.transform.up));
|
|
|
|
angle += q.eulerAngles;
|
|
}
|
|
else
|
|
{
|
|
angle += ComputeRotateDir(offset, trackedAxis);
|
|
}
|
|
|
|
return angle;
|
|
}
|
|
|
|
private AxisXYZ FindClosetAxis(Vector3 hitPoint)
|
|
{
|
|
GetDeltaAngles(hitPoint, out float xAngle, out float yAngle, out float zAngle);
|
|
(AxisXYZ axis, float angle) closest = (AxisXYZ.None, 0);
|
|
|
|
TrySetClosest(AxisXYZ.X, xAngle);
|
|
TrySetClosest(AxisXYZ.Y, yAngle);
|
|
TrySetClosest(AxisXYZ.Z, zAngle);
|
|
|
|
if (EnableFreeRotate && closest.axis == AxisXYZ.None && IsAxisAllowed(AxisXYZ.All))
|
|
closest.axis = AxisXYZ.All;
|
|
|
|
return closest.axis;
|
|
|
|
void TrySetClosest(AxisXYZ axis, float angle)
|
|
{
|
|
if (IsAxisAllowed(axis) && angle < interactionAngle && (closest.axis == AxisXYZ.None || angle < closest.angle))
|
|
closest = (axis, angle);
|
|
}
|
|
}
|
|
|
|
private void GetDeltaAngles(Vector3 position, out float xAngle, out float yAngle, out float zAngle)
|
|
{
|
|
Vector3 dir = m_pSphere.transform.InverseTransformPoint(position).normalized;
|
|
|
|
xAngle = Mathf.Acos(dir.x) * Mathf.Rad2Deg;
|
|
yAngle = Mathf.Asin(dir.y) * Mathf.Rad2Deg;
|
|
zAngle = Mathf.Acos(dir.z) * Mathf.Rad2Deg;
|
|
|
|
xAngle = GetDelta(xAngle, true);
|
|
yAngle = GetDelta(yAngle, false);
|
|
zAngle = GetDelta(zAngle, true);
|
|
}
|
|
|
|
private float GetDelta(float angle, bool verticalAxis)
|
|
{
|
|
//0° is the angle for a horizontal axis
|
|
//90° is the angle for a vertical axis
|
|
//See trigonometry circle
|
|
return Mathf.Abs(angle - (verticalAxis ? 90 : 0));
|
|
}
|
|
|
|
private void SelectAxis(AxisXYZ toSelect)
|
|
{
|
|
if (trackedAxis == toSelect)
|
|
return;
|
|
|
|
trackedAxis = toSelect;
|
|
List<Color> colors = new();
|
|
int index = 0;
|
|
|
|
SetArcColor(AxisXYZ.X, xColor);
|
|
SetArcColor(AxisXYZ.Y, yColor);
|
|
SetArcColor(AxisXYZ.Z, zColor);
|
|
|
|
m_pMesh.SetColors(colors);
|
|
|
|
void SetArcColor(AxisXYZ axis, Color axisColor)
|
|
{
|
|
if (!IsAxisAllowed(axis))
|
|
return;
|
|
|
|
int count = verticesCount[index++];
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
colors.Add(trackedAxis == axis || trackedAxis == AxisXYZ.All ? selectColor : axisColor);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool IsAxisAllowed(AxisXYZ axis) => axis != AxisXYZ.None && (allowedAxis == AxisXYZ.All || allowedAxis == axis);
|
|
}
|
|
}
|