1059 lines
35 KiB
C#
1059 lines
35 KiB
C#
|
using UdonSharp;
|
||
|
using UnityEngine;
|
||
|
using VRC.SDK3.Components;
|
||
|
using VRC.SDKBase;
|
||
|
using VRC.Udon.Common.Interfaces;
|
||
|
|
||
|
#pragma warning disable IDE0044
|
||
|
#pragma warning disable IDE0090, IDE1006
|
||
|
|
||
|
namespace QvPen.UdonScript
|
||
|
{
|
||
|
[DefaultExecutionOrder(10)]
|
||
|
[UdonBehaviourSyncMode(BehaviourSyncMode.NoVariableSync)]
|
||
|
public class QvPen_Pen : UdonSharpBehaviour
|
||
|
{
|
||
|
public const string _version = "v3.2.5";
|
||
|
|
||
|
#region Field
|
||
|
|
||
|
[Header("Pen")]
|
||
|
[SerializeField]
|
||
|
private TrailRenderer trailRenderer;
|
||
|
[SerializeField]
|
||
|
private LineRenderer inkPrefab;
|
||
|
|
||
|
[SerializeField]
|
||
|
private Transform inkPosition;
|
||
|
private Transform inkPositionChild;
|
||
|
[SerializeField]
|
||
|
private Transform inkPoolRoot;
|
||
|
[System.NonSerialized]
|
||
|
public Transform inkPool;
|
||
|
[System.NonSerialized]
|
||
|
public Transform inkPoolSynced;// { get; private set; }
|
||
|
private Transform inkPoolNotSynced;
|
||
|
|
||
|
private QvPen_LateSync syncer;
|
||
|
|
||
|
[Header("Pointer")]
|
||
|
[SerializeField]
|
||
|
private Transform pointer;
|
||
|
|
||
|
private float _pointerRadius = 0f;
|
||
|
private float pointerRadius
|
||
|
{
|
||
|
get
|
||
|
{
|
||
|
if (_pointerRadius > 0f)
|
||
|
return _pointerRadius;
|
||
|
else
|
||
|
{
|
||
|
var sphereCollider = pointer.GetComponent<SphereCollider>();
|
||
|
sphereCollider.enabled = false;
|
||
|
var s = pointer.lossyScale;
|
||
|
_pointerRadius = Mathf.Min(s.x, s.y, s.z) * sphereCollider.radius;
|
||
|
return _pointerRadius;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
[SerializeField]
|
||
|
private float _pointerRadiusMultiplierForDesktop = 3f;
|
||
|
private float pointerRadiusMultiplierForDesktop => isUserInVR ? 1f : Mathf.Abs(_pointerRadiusMultiplierForDesktop);
|
||
|
[SerializeField]
|
||
|
private Material pointerMaterialNormal;
|
||
|
[SerializeField]
|
||
|
private Material pointerMaterialActive;
|
||
|
|
||
|
[Header("Screen")]
|
||
|
[SerializeField]
|
||
|
private Canvas screenOverlay;
|
||
|
[SerializeField]
|
||
|
private Renderer marker;
|
||
|
|
||
|
[Header("Other")]
|
||
|
[SerializeField]
|
||
|
private bool canBeErasedWithOtherPointers = true;
|
||
|
|
||
|
private bool enabledLateSync = true;
|
||
|
|
||
|
private MeshCollider _inkPrefabCollider;
|
||
|
private MeshCollider inkPrefabCollider
|
||
|
=> _inkPrefabCollider ? _inkPrefabCollider : (_inkPrefabCollider = inkPrefab.GetComponentInChildren<MeshCollider>(true));
|
||
|
private GameObject lineInstance;
|
||
|
|
||
|
private bool isUser;
|
||
|
public bool IsUser => isUser;
|
||
|
|
||
|
// Components
|
||
|
private VRC_Pickup _pickup;
|
||
|
private VRC_Pickup pickup => _pickup ? _pickup : (_pickup = (VRC_Pickup)GetComponent(typeof(VRC_Pickup)));
|
||
|
|
||
|
private VRCObjectSync _objectSync;
|
||
|
private VRCObjectSync objectSync
|
||
|
=> _objectSync ? _objectSync : (_objectSync = (VRCObjectSync)GetComponent(typeof(VRCObjectSync)));
|
||
|
|
||
|
// PenManager
|
||
|
private QvPen_PenManager manager;
|
||
|
|
||
|
// Ink
|
||
|
private int inkMeshLayer;
|
||
|
private int inkColliderLayer;
|
||
|
private const float followSpeed = 30f;
|
||
|
private int inkNo;
|
||
|
|
||
|
// Pointer
|
||
|
private bool isPointerEnabled;
|
||
|
private Renderer pointerRenderer;
|
||
|
|
||
|
// Double click
|
||
|
private bool useDoubleClick = true;
|
||
|
private const float clickTimeInterval = 0.2f;
|
||
|
private float prevClickTime;
|
||
|
private float clickPosInterval = 0.01f; // Default: Quest
|
||
|
private Vector3 prevClickPos;
|
||
|
|
||
|
// State
|
||
|
private const int StatePenIdle = 0;
|
||
|
private const int StatePenUsing = 1;
|
||
|
private const int StateEraserIdle = 2;
|
||
|
private const int StateEraserUsing = 3;
|
||
|
private int currentState = StatePenIdle;
|
||
|
private string nameofCurrentState
|
||
|
{
|
||
|
get
|
||
|
{
|
||
|
switch (currentState)
|
||
|
{
|
||
|
case StatePenIdle: return nameof(StatePenIdle);
|
||
|
case StatePenUsing: return nameof(StatePenUsing);
|
||
|
case StateEraserIdle: return nameof(StateEraserIdle);
|
||
|
case StateEraserUsing: return nameof(StateEraserUsing);
|
||
|
default: return string.Empty;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Sync state
|
||
|
[System.NonSerialized]
|
||
|
public int currentSyncState = SYNC_STATE_Idle;
|
||
|
public const int SYNC_STATE_Idle = 0;
|
||
|
public const int SYNC_STATE_Started = 1;
|
||
|
public const int SYNC_STATE_Finished = 2;
|
||
|
|
||
|
// Ink pool
|
||
|
public const string inkPoolRootName = "QvPen_Objects";
|
||
|
public const string inkPoolName = "InkPool";
|
||
|
private int penId;
|
||
|
public Vector3 penIdVector { get; private set; }
|
||
|
private string penIdString;
|
||
|
|
||
|
private const string inkPrefix = "Ink";
|
||
|
private float inkWidth;
|
||
|
|
||
|
private VRCPlayerApi _localPlayer;
|
||
|
private VRCPlayerApi localPlayer => _localPlayer ?? (_localPlayer = Networking.LocalPlayer);
|
||
|
|
||
|
private int localPlayerId => VRC.SDKBase.Utilities.IsValid(localPlayer) ? localPlayer.playerId : -1;
|
||
|
|
||
|
private bool isCheckedIsUserInVR = false;
|
||
|
private bool/*?*/ _isUserInVR;
|
||
|
private bool/*?*/ isUserInVR => isCheckedIsUserInVR ? _isUserInVR
|
||
|
: (isCheckedIsUserInVR = VRC.SDKBase.Utilities.IsValid(localPlayer)) && (_isUserInVR = localPlayer.IsUserInVR());
|
||
|
|
||
|
#endregion Field
|
||
|
|
||
|
public void _Init(QvPen_PenManager manager)
|
||
|
{
|
||
|
this.manager = manager;
|
||
|
_UpdateInkData();
|
||
|
|
||
|
inkPool = inkPoolRoot.Find(inkPoolName);
|
||
|
|
||
|
var inkPoolRootGO = GameObject.Find($"/{inkPoolRootName}");
|
||
|
if (inkPoolRootGO)
|
||
|
{
|
||
|
inkPoolRoot = inkPoolRootGO.transform;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
inkPoolRoot.name = inkPoolRootName;
|
||
|
SetParentAndResetLocalTransform(inkPoolRoot, null);
|
||
|
inkPoolRoot.SetAsFirstSibling();
|
||
|
#if !UNITY_EDITOR
|
||
|
const string ureishi = nameof(ureishi);
|
||
|
Log($"{nameof(QvPen)} {_version}");
|
||
|
#endif
|
||
|
}
|
||
|
|
||
|
SetParentAndResetLocalTransform(inkPool, inkPoolRoot);
|
||
|
|
||
|
penId = string.IsNullOrEmpty(Networking.GetUniqueName(gameObject))
|
||
|
? 0 : Networking.GetUniqueName(gameObject).GetHashCode();
|
||
|
penIdVector = new Vector3((penId >> 24) & 0x00ff, (penId >> 12) & 0x0fff, penId & 0x0fff);
|
||
|
penIdString = $"0x{(int)penIdVector.x:x2}{(int)penIdVector.y:x3}{(int)penIdVector.z:x3}";
|
||
|
inkPool.name = $"{inkPoolName} ({penIdString})";
|
||
|
|
||
|
syncer = inkPool.GetComponent<QvPen_LateSync>();
|
||
|
if (syncer)
|
||
|
syncer.pen = this;
|
||
|
|
||
|
inkPoolSynced = inkPool.Find("Synced");
|
||
|
inkPoolNotSynced = inkPool.Find("NotSynced");
|
||
|
|
||
|
#if !UNITY_EDITOR
|
||
|
Log($"QvPen ID: {penIdString}");
|
||
|
#endif
|
||
|
|
||
|
inkPositionChild = inkPosition.Find("InkPositionChild");
|
||
|
|
||
|
pickup.InteractionText = nameof(QvPen);
|
||
|
pickup.UseText = "Draw";
|
||
|
|
||
|
pointerRenderer = pointer.GetComponent<Renderer>();
|
||
|
pointer.gameObject.SetActive(false);
|
||
|
pointer.transform.localScale *= pointerRadiusMultiplierForDesktop;
|
||
|
|
||
|
marker.transform.localScale = Vector3.one * inkWidth;
|
||
|
|
||
|
#if !UNITY_ANDROID
|
||
|
if (isUserInVR)
|
||
|
clickPosInterval = 0.005f;
|
||
|
else
|
||
|
clickPosInterval = 0.001f;
|
||
|
#endif
|
||
|
}
|
||
|
|
||
|
public void _UpdateInkData()
|
||
|
{
|
||
|
inkWidth = manager.inkWidth;
|
||
|
inkMeshLayer = manager.inkMeshLayer;
|
||
|
inkColliderLayer = manager.inkColliderLayer;
|
||
|
|
||
|
inkPrefab.gameObject.layer = inkMeshLayer;
|
||
|
trailRenderer.gameObject.layer = inkMeshLayer;
|
||
|
inkPrefabCollider.gameObject.layer = inkColliderLayer;
|
||
|
|
||
|
#if UNITY_ANDROID
|
||
|
var material = manager.questInkMaterial;
|
||
|
inkPrefab.widthMultiplier = inkWidth;
|
||
|
trailRenderer.widthMultiplier = inkWidth;
|
||
|
#else
|
||
|
var material = manager.pcInkMaterial;
|
||
|
if (material && material.shader == manager.roundedTrailShader)
|
||
|
{
|
||
|
inkPrefab.widthMultiplier = 0f;
|
||
|
trailRenderer.widthMultiplier = 0f;
|
||
|
material.SetFloat("_Width", inkWidth);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
inkPrefab.widthMultiplier = inkWidth;
|
||
|
trailRenderer.widthMultiplier = inkWidth;
|
||
|
}
|
||
|
#endif
|
||
|
|
||
|
inkPrefab.material = material;
|
||
|
trailRenderer.material = material;
|
||
|
inkPrefab.colorGradient = manager.colorGradient;
|
||
|
trailRenderer.colorGradient = CreateReverseGradient(manager.colorGradient);
|
||
|
|
||
|
surftraceMask = manager.surftraceMask;
|
||
|
}
|
||
|
|
||
|
public bool _CheckId(Vector3 idVector)
|
||
|
=> idVector == penIdVector;
|
||
|
|
||
|
#region Data protocol
|
||
|
|
||
|
#region Base
|
||
|
|
||
|
// Mode
|
||
|
public const int MODE_UNKNOWN = -1;
|
||
|
public const int MODE_DRAW = 1;
|
||
|
public const int MODE_ERASE = 2;
|
||
|
public const int MODE_DRAW_PLANE = 3;
|
||
|
|
||
|
// Footer element
|
||
|
public const int FOOTER_ELEMENT_DATA_INFO = 0;
|
||
|
public const int FOOTER_ELEMENT_PEN_ID = 1;
|
||
|
|
||
|
public const int FOOTER_ELEMENT_DRAW_INK_INFO = 2;
|
||
|
//public const int FOOTER_ELEMENT_DRAW_INK_WIDTH = ;
|
||
|
//public const int FOOTER_ELEMENT_DRAW_INK_COLOR = ;
|
||
|
public const int FOOTER_ELEMENT_DRAW_LENGTH = 3;
|
||
|
|
||
|
public const int FOOTER_ELEMENT_ERASE_POINTER_POSITION = 2;
|
||
|
public const int FOOTER_ELEMENT_ERASE_POINTER_RADIUS = 3;
|
||
|
public const int FOOTER_ELEMENT_ERASE_LENGTH = 4;
|
||
|
|
||
|
private int GetFooterSize(int mode)
|
||
|
{
|
||
|
switch (mode)
|
||
|
{
|
||
|
case MODE_DRAW: return FOOTER_ELEMENT_DRAW_LENGTH;
|
||
|
case MODE_ERASE: return FOOTER_ELEMENT_ERASE_LENGTH;
|
||
|
case MODE_UNKNOWN: return 0;
|
||
|
default: return 0;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private int currentDrawMode = MODE_DRAW;
|
||
|
|
||
|
#endregion
|
||
|
|
||
|
private Vector3 GetData(Vector3[] data, int index)
|
||
|
=> data.Length > index ? data[data.Length - 1 - index] : Vector3.negativeInfinity;
|
||
|
|
||
|
private void SetData(Vector3[] data, int index, Vector3 element)
|
||
|
{
|
||
|
if (data.Length > index)
|
||
|
data[data.Length - 1 - index] = element;
|
||
|
}
|
||
|
|
||
|
private int GetMode(Vector3[] data)
|
||
|
=> data.Length > 0 ? (int)GetData(data, FOOTER_ELEMENT_DATA_INFO).y : MODE_UNKNOWN;
|
||
|
|
||
|
private int GetFooterLength(Vector3[] data)
|
||
|
=> data.Length > 0 ? (int)GetData(data, FOOTER_ELEMENT_DATA_INFO).z : 0;
|
||
|
|
||
|
#endregion
|
||
|
|
||
|
#region Unity events
|
||
|
|
||
|
#region Screen mode
|
||
|
#if !UNITY_ANDROID
|
||
|
private VRCPlayerApi.TrackingData headTracking;
|
||
|
private Vector3 headPos, center;
|
||
|
private Quaternion headRot;
|
||
|
private Vector2 _wh, wh, clampWH;
|
||
|
// Wait for Udon Vector2.Set() bug fix
|
||
|
private /*readonly*/ Vector2 mouseDelta = new Vector2();
|
||
|
private float ratio, scalar;
|
||
|
|
||
|
private float sensitivity = 0.75f;
|
||
|
private bool isScreenMode = false;
|
||
|
private void Update()
|
||
|
{
|
||
|
if (isUserInVR || !isUser)
|
||
|
return;
|
||
|
|
||
|
if (Input.GetKeyDown(KeyCode.Tab))
|
||
|
{
|
||
|
EnterScreenMode();
|
||
|
}
|
||
|
else if (Input.GetKeyUp(KeyCode.Tab))
|
||
|
{
|
||
|
ExitScreenMode();
|
||
|
}
|
||
|
else if (Input.GetKey(KeyCode.Tab))
|
||
|
{
|
||
|
if (Input.GetKeyDown(KeyCode.Delete))
|
||
|
{
|
||
|
manager.SendCustomNetworkEvent(NetworkEventTarget.All, nameof(manager.Clear));
|
||
|
}
|
||
|
else if (Input.GetKey(KeyCode.UpArrow))
|
||
|
{
|
||
|
sensitivity = Mathf.Min(sensitivity + 0.001f, 1.5f);
|
||
|
Log($"Sensitivity -> {sensitivity:f3}");
|
||
|
}
|
||
|
else if (Input.GetKey(KeyCode.DownArrow))
|
||
|
{
|
||
|
sensitivity = Mathf.Max(sensitivity - 0.001f, 0.5f);
|
||
|
Log($"Sensitivity -> {sensitivity:f3}");
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void EnterScreenMode()
|
||
|
{
|
||
|
isScreenMode = true;
|
||
|
|
||
|
marker.enabled = true;
|
||
|
|
||
|
_wh = Vector2.zero;
|
||
|
screenOverlay.gameObject.SetActive(true);
|
||
|
wh = screenOverlay.GetComponent<RectTransform>().rect.size;
|
||
|
screenOverlay.gameObject.SetActive(false);
|
||
|
clampWH = wh / (2f * 1920f * 0.98f);
|
||
|
ratio = 2f * 1080f / wh.y;
|
||
|
}
|
||
|
|
||
|
private void ExitScreenMode()
|
||
|
{
|
||
|
isScreenMode = false;
|
||
|
|
||
|
if (!isSurftraceMode)
|
||
|
marker.enabled = false;
|
||
|
|
||
|
SendCustomNetworkEvent(NetworkEventTarget.All, nameof(ChangeStateToPenIdle));
|
||
|
|
||
|
inkPositionChild.localPosition = Vector3.zero;
|
||
|
inkPositionChild.localRotation = Quaternion.identity;
|
||
|
trailRenderer.transform.SetPositionAndRotation(inkPositionChild.position, inkPositionChild.rotation);
|
||
|
}
|
||
|
#endif
|
||
|
#endregion Screen mode
|
||
|
|
||
|
private void LateUpdate()
|
||
|
{
|
||
|
if (!isHeld)
|
||
|
return;
|
||
|
|
||
|
#if !UNITY_ANDROID
|
||
|
if (!isUserInVR && isUser && Input.GetKey(KeyCode.Tab))
|
||
|
{
|
||
|
headTracking = localPlayer.GetTrackingData(VRCPlayerApi.TrackingDataType.Head);
|
||
|
headPos = headTracking.position;
|
||
|
headRot = headTracking.rotation;
|
||
|
|
||
|
center = headRot * Vector3.forward * Vector3.Dot(headRot * Vector3.forward, transform.position - headPos);
|
||
|
scalar = ratio * Vector3.Dot(headRot * Vector3.forward, center);
|
||
|
center += headPos;
|
||
|
|
||
|
// Wait for Udon Vector2.Set() bug fix
|
||
|
// mouseDelta.Set(Input.GetAxis("Mouse X"), Input.GetAxis("Mouse Y"));
|
||
|
{
|
||
|
mouseDelta.x = Input.GetAxis("Mouse X");
|
||
|
mouseDelta.y = Input.GetAxis("Mouse Y");
|
||
|
}
|
||
|
_wh += sensitivity * Time.deltaTime * mouseDelta;
|
||
|
_wh = Vector2.Min(Vector2.Max(_wh, -clampWH), clampWH);
|
||
|
|
||
|
inkPositionChild.SetPositionAndRotation(center + headRot * _wh * scalar, headRot);
|
||
|
}
|
||
|
#endif
|
||
|
|
||
|
if (isSurftraceMode)
|
||
|
{
|
||
|
Vector3 inkPositionPosition;
|
||
|
#if !UNITY_ANDROID
|
||
|
if (isScreenMode)
|
||
|
inkPositionPosition = inkPositionChild.position;
|
||
|
else
|
||
|
#endif
|
||
|
inkPositionPosition = inkPosition.position;
|
||
|
|
||
|
var closestPoint = surftraceTarget.ClosestPoint(inkPositionPosition);
|
||
|
var distance = Vector3.Distance(closestPoint, inkPositionPosition);
|
||
|
|
||
|
#if !UNITY_ANDROID
|
||
|
inkPositionChild.position = Vector3.MoveTowards(closestPoint, inkPositionPosition, inkWidth / 1.999f);
|
||
|
#else
|
||
|
inkPositionChild.position = Vector3.MoveTowards(closestPoint, inkPositionPosition, inkWidth / 1.9f);
|
||
|
#endif
|
||
|
|
||
|
if (distance > surftraceMaxDistance)
|
||
|
ExitSurftraceMode();
|
||
|
}
|
||
|
|
||
|
if (!isPointerEnabled)
|
||
|
{
|
||
|
if (isUser)
|
||
|
trailRenderer.transform.SetPositionAndRotation(
|
||
|
Vector3.Lerp(trailRenderer.transform.position, inkPositionChild.position, Time.deltaTime * followSpeed),
|
||
|
Quaternion.Lerp(trailRenderer.transform.rotation, inkPositionChild.rotation, Time.deltaTime * followSpeed));
|
||
|
else
|
||
|
trailRenderer.transform.SetPositionAndRotation(inkPositionChild.position, inkPositionChild.rotation);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private readonly Collider[] results = new Collider[4];
|
||
|
public override void PostLateUpdate()
|
||
|
{
|
||
|
if (!isUser)
|
||
|
return;
|
||
|
|
||
|
if (isPointerEnabled)
|
||
|
{
|
||
|
var count = Physics.OverlapSphereNonAlloc(pointer.position, pointerRadius, results, 1 << inkColliderLayer, QueryTriggerInteraction.Ignore);
|
||
|
for (var i = 0; i < count; i++)
|
||
|
{
|
||
|
var other = results[i];
|
||
|
|
||
|
if (other && other.transform.parent && other.transform.parent.parent)
|
||
|
{
|
||
|
if (canBeErasedWithOtherPointers
|
||
|
? other.transform.parent.parent.parent && other.transform.parent.parent.parent.parent == inkPoolRoot
|
||
|
: other.transform.parent.parent.parent == inkPool
|
||
|
)
|
||
|
{
|
||
|
var lineRenderer = other.GetComponentInParent<LineRenderer>();
|
||
|
if (lineRenderer && lineRenderer.positionCount > 0)
|
||
|
{
|
||
|
var data = new Vector3[GetFooterSize(MODE_ERASE)];
|
||
|
|
||
|
SetData(data, FOOTER_ELEMENT_DATA_INFO, new Vector3(localPlayerId, MODE_ERASE, GetFooterSize(MODE_ERASE)));
|
||
|
SetData(data, FOOTER_ELEMENT_PEN_ID, penIdVector);
|
||
|
SetData(data, FOOTER_ELEMENT_ERASE_POINTER_POSITION, pointer.position);
|
||
|
SetData(data, FOOTER_ELEMENT_ERASE_POINTER_RADIUS, Vector3.right * pointerRadius);
|
||
|
|
||
|
_SendData(data);
|
||
|
}
|
||
|
}
|
||
|
//else if (
|
||
|
// false
|
||
|
//)
|
||
|
//{
|
||
|
//
|
||
|
//}
|
||
|
}
|
||
|
|
||
|
results[i] = null;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Surftrace mode
|
||
|
private bool useSurftraceMode = true;
|
||
|
|
||
|
private const float surftraceMaxDistance = 1f;
|
||
|
private const float surftraceEnterDistance = 0.05f;
|
||
|
|
||
|
private int surftraceMask = ~0;
|
||
|
|
||
|
private Collider surftraceTarget = null;
|
||
|
private bool isSurftraceMode => surftraceTarget;
|
||
|
|
||
|
private void OnTriggerEnter(Collider other)
|
||
|
{
|
||
|
if (isUser && useSurftraceMode && VRC.SDKBase.Utilities.IsValid(other) && !other.isTrigger)
|
||
|
{
|
||
|
if ((1 << other.gameObject.layer & surftraceMask) == 0)
|
||
|
return;
|
||
|
|
||
|
// other.GetType().IsSubclassOf(typeof(MeshCollider))
|
||
|
if (other.GetType() == typeof(MeshCollider) && !((MeshCollider)other).convex)
|
||
|
return;
|
||
|
|
||
|
var distance = Vector3.Distance(other.ClosestPoint(inkPosition.position), inkPosition.position);
|
||
|
if (distance < surftraceEnterDistance)
|
||
|
EnterSurftraceMode(other);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void EnterSurftraceMode(Collider target)
|
||
|
{
|
||
|
surftraceTarget = target;
|
||
|
marker.enabled = true;
|
||
|
}
|
||
|
|
||
|
private void ExitSurftraceMode()
|
||
|
{
|
||
|
surftraceTarget = null;
|
||
|
|
||
|
#if !UNITY_ANDROID
|
||
|
if (!isScreenMode)
|
||
|
#endif
|
||
|
marker.enabled = false;
|
||
|
|
||
|
SendCustomNetworkEvent(NetworkEventTarget.All, nameof(ChangeStateToPenIdle));
|
||
|
|
||
|
inkPositionChild.localPosition = Vector3.zero;
|
||
|
inkPositionChild.localRotation = Quaternion.identity;
|
||
|
trailRenderer.transform.SetPositionAndRotation(inkPositionChild.position, inkPositionChild.rotation);
|
||
|
}
|
||
|
|
||
|
#endregion Unity events
|
||
|
|
||
|
#region VRChat events
|
||
|
|
||
|
public override void OnPickup()
|
||
|
{
|
||
|
isUser = true;
|
||
|
|
||
|
manager._TakeOwnership();
|
||
|
manager.SendCustomNetworkEvent(NetworkEventTarget.All, nameof(QvPen_PenManager.StartUsing));
|
||
|
|
||
|
SendCustomNetworkEvent(NetworkEventTarget.All, nameof(ChangeStateToPenIdle));
|
||
|
}
|
||
|
|
||
|
public override void OnDrop()
|
||
|
{
|
||
|
isUser = false;
|
||
|
|
||
|
manager.SendCustomNetworkEvent(NetworkEventTarget.All, nameof(QvPen_PenManager.EndUsing));
|
||
|
|
||
|
SendCustomNetworkEvent(NetworkEventTarget.All, nameof(ChangeStateToPenIdle));
|
||
|
|
||
|
manager._ClearSyncBuffer();
|
||
|
|
||
|
#if !UNITY_ANDROID
|
||
|
ExitScreenMode();
|
||
|
#endif
|
||
|
ExitSurftraceMode();
|
||
|
}
|
||
|
|
||
|
public override void OnPickupUseDown()
|
||
|
{
|
||
|
if (useDoubleClick
|
||
|
&& Time.time - prevClickTime < clickTimeInterval
|
||
|
&& Vector3.Distance(inkPosition.position, prevClickPos) < clickPosInterval
|
||
|
)
|
||
|
{
|
||
|
prevClickTime = 0f;
|
||
|
switch (currentState)
|
||
|
{
|
||
|
case StatePenIdle:
|
||
|
SendCustomNetworkEvent(NetworkEventTarget.All, nameof(DestroyJustBeforeInk));
|
||
|
SendCustomNetworkEvent(NetworkEventTarget.All, nameof(ChangeStateToEraseIdle));
|
||
|
break;
|
||
|
case StateEraserIdle:
|
||
|
SendCustomNetworkEvent(NetworkEventTarget.All, nameof(ChangeStateToPenIdle));
|
||
|
break;
|
||
|
default:
|
||
|
Error($"Unexpected state : {nameofCurrentState} at {nameof(OnPickupUseDown)} Double Clicked");
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
prevClickTime = Time.time;
|
||
|
prevClickPos = inkPosition.position;
|
||
|
switch (currentState)
|
||
|
{
|
||
|
case StatePenIdle:
|
||
|
SendCustomNetworkEvent(NetworkEventTarget.All, nameof(ChangeStateToPenUsing));
|
||
|
break;
|
||
|
case StateEraserIdle:
|
||
|
SendCustomNetworkEvent(NetworkEventTarget.All, nameof(ChangeStateToEraseUsing));
|
||
|
break;
|
||
|
default:
|
||
|
Error($"Unexpected state : {nameofCurrentState} at {nameof(OnPickupUseDown)}");
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public override void OnPickupUseUp()
|
||
|
{
|
||
|
switch (currentState)
|
||
|
{
|
||
|
case StatePenUsing:
|
||
|
SendCustomNetworkEvent(NetworkEventTarget.All, nameof(ChangeStateToPenIdle));
|
||
|
break;
|
||
|
case StateEraserUsing:
|
||
|
SendCustomNetworkEvent(NetworkEventTarget.All, nameof(ChangeStateToEraseIdle));
|
||
|
break;
|
||
|
case StatePenIdle:
|
||
|
Log($"Change state : {nameof(StateEraserIdle)} to {nameofCurrentState}");
|
||
|
break;
|
||
|
case StateEraserIdle:
|
||
|
Log($"Change state : {nameof(StatePenIdle)} to {nameofCurrentState}");
|
||
|
break;
|
||
|
default:
|
||
|
Error($"Unexpected state : {nameofCurrentState} at {nameof(OnPickupUseUp)}");
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public void _SetUseDoubleClick(bool value)
|
||
|
{
|
||
|
useDoubleClick = value;
|
||
|
|
||
|
if (isUser)
|
||
|
SendCustomNetworkEvent(NetworkEventTarget.All, nameof(ChangeStateToPenIdle));
|
||
|
}
|
||
|
|
||
|
public void _SetEnabledLateSync(bool value)
|
||
|
{
|
||
|
enabledLateSync = value;
|
||
|
}
|
||
|
|
||
|
public void _SetUseSurftraceMode(bool value)
|
||
|
{
|
||
|
useSurftraceMode = value;
|
||
|
|
||
|
if (isUser)
|
||
|
SendCustomNetworkEvent(NetworkEventTarget.All, nameof(ChangeStateToPenIdle));
|
||
|
}
|
||
|
|
||
|
private GameObject justBeforeInk;
|
||
|
public void DestroyJustBeforeInk()
|
||
|
{
|
||
|
if (justBeforeInk)
|
||
|
{
|
||
|
Destroy(justBeforeInk);
|
||
|
justBeforeInk = null;
|
||
|
inkNo--;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void OnEnable()
|
||
|
{
|
||
|
if (inkPool)
|
||
|
inkPool.gameObject.SetActive(true);
|
||
|
}
|
||
|
|
||
|
private void OnDisable()
|
||
|
{
|
||
|
if (inkPool)
|
||
|
inkPool.gameObject.SetActive(false);
|
||
|
}
|
||
|
|
||
|
private void OnDestroy() => Destroy(inkPool);
|
||
|
|
||
|
#endregion
|
||
|
|
||
|
#region ChangeState
|
||
|
|
||
|
public void ChangeStateToPenIdle()
|
||
|
{
|
||
|
switch (currentState)
|
||
|
{
|
||
|
case StatePenUsing:
|
||
|
FinishDrawing();
|
||
|
break;
|
||
|
case StateEraserIdle:
|
||
|
ChangeToPen();
|
||
|
break;
|
||
|
case StateEraserUsing:
|
||
|
DisablePointer();
|
||
|
ChangeToPen();
|
||
|
break;
|
||
|
}
|
||
|
currentState = StatePenIdle;
|
||
|
}
|
||
|
|
||
|
public void ChangeStateToPenUsing()
|
||
|
{
|
||
|
switch (currentState)
|
||
|
{
|
||
|
case StatePenIdle:
|
||
|
StartDrawing();
|
||
|
break;
|
||
|
case StateEraserIdle:
|
||
|
ChangeToPen();
|
||
|
StartDrawing();
|
||
|
break;
|
||
|
case StateEraserUsing:
|
||
|
DisablePointer();
|
||
|
ChangeToPen();
|
||
|
StartDrawing();
|
||
|
break;
|
||
|
}
|
||
|
currentState = StatePenUsing;
|
||
|
}
|
||
|
|
||
|
public void ChangeStateToEraseIdle()
|
||
|
{
|
||
|
switch (currentState)
|
||
|
{
|
||
|
case StatePenIdle:
|
||
|
ChangeToEraser();
|
||
|
break;
|
||
|
case StatePenUsing:
|
||
|
FinishDrawing();
|
||
|
ChangeToEraser();
|
||
|
break;
|
||
|
case StateEraserUsing:
|
||
|
DisablePointer();
|
||
|
break;
|
||
|
}
|
||
|
currentState = StateEraserIdle;
|
||
|
}
|
||
|
|
||
|
public void ChangeStateToEraseUsing()
|
||
|
{
|
||
|
switch (currentState)
|
||
|
{
|
||
|
case StatePenIdle:
|
||
|
ChangeToEraser();
|
||
|
EnablePointer();
|
||
|
break;
|
||
|
case StatePenUsing:
|
||
|
FinishDrawing();
|
||
|
ChangeToEraser();
|
||
|
EnablePointer();
|
||
|
break;
|
||
|
case StateEraserIdle:
|
||
|
EnablePointer();
|
||
|
break;
|
||
|
}
|
||
|
currentState = StateEraserUsing;
|
||
|
}
|
||
|
|
||
|
#endregion
|
||
|
|
||
|
[System.NonSerialized]
|
||
|
public bool pickuped = false; // protected
|
||
|
public bool isHeld => pickuped;
|
||
|
|
||
|
public void _Respawn()
|
||
|
{
|
||
|
pickup.Drop();
|
||
|
|
||
|
if (Networking.IsOwner(gameObject))
|
||
|
objectSync.Respawn();
|
||
|
}
|
||
|
|
||
|
public void _Clear()
|
||
|
{
|
||
|
foreach (Transform ink in inkPoolSynced)
|
||
|
Destroy(ink.gameObject);
|
||
|
|
||
|
foreach (Transform ink in inkPoolNotSynced)
|
||
|
Destroy(ink.gameObject);
|
||
|
|
||
|
inkNo = 0;
|
||
|
}
|
||
|
|
||
|
private void StartDrawing()
|
||
|
{
|
||
|
trailRenderer.gameObject.SetActive(true);
|
||
|
}
|
||
|
|
||
|
private void FinishDrawing()
|
||
|
{
|
||
|
if (isUser)
|
||
|
{
|
||
|
var data = PackData(trailRenderer, currentDrawMode);
|
||
|
|
||
|
_SendData(data);
|
||
|
}
|
||
|
|
||
|
trailRenderer.gameObject.SetActive(false);
|
||
|
trailRenderer.Clear();
|
||
|
}
|
||
|
|
||
|
private Vector3[] PackData(TrailRenderer trailRenderer, int mode)
|
||
|
{
|
||
|
if (!trailRenderer)
|
||
|
return null;
|
||
|
|
||
|
justBeforeInk = null;
|
||
|
|
||
|
var positionCount = trailRenderer.positionCount;
|
||
|
|
||
|
if (positionCount == 0)
|
||
|
return null;
|
||
|
|
||
|
var data = new Vector3[positionCount + GetFooterSize(mode)];
|
||
|
|
||
|
trailRenderer.GetPositions(data);
|
||
|
|
||
|
SetData(data, FOOTER_ELEMENT_DATA_INFO, new Vector3(localPlayerId, mode, GetFooterSize(mode)));
|
||
|
SetData(data, FOOTER_ELEMENT_PEN_ID, penIdVector);
|
||
|
SetData(data, FOOTER_ELEMENT_DRAW_INK_INFO, new Vector3(inkMeshLayer, inkColliderLayer, enabledLateSync ? 1f : 0f));
|
||
|
|
||
|
return data;
|
||
|
}
|
||
|
|
||
|
public Vector3[] _PackData(LineRenderer lineRenderer, int mode)
|
||
|
{
|
||
|
if (!lineRenderer)
|
||
|
return null;
|
||
|
|
||
|
var positionCount = lineRenderer.positionCount;
|
||
|
|
||
|
if (positionCount == 0)
|
||
|
return null;
|
||
|
|
||
|
var data = new Vector3[positionCount + GetFooterSize(mode)];
|
||
|
|
||
|
lineRenderer.GetPositions(data);
|
||
|
|
||
|
var inkMeshLayer = (float)lineRenderer.gameObject.layer;
|
||
|
var inkColliderLayer = (float)lineRenderer.GetComponentInChildren<MeshCollider>(true).gameObject.layer;
|
||
|
|
||
|
SetData(data, FOOTER_ELEMENT_DATA_INFO, new Vector3(localPlayerId, mode, GetFooterSize(mode)));
|
||
|
SetData(data, FOOTER_ELEMENT_PEN_ID, penIdVector);
|
||
|
SetData(data, FOOTER_ELEMENT_DRAW_INK_INFO, new Vector3(inkMeshLayer, inkColliderLayer, enabledLateSync ? 1f : 0f));
|
||
|
|
||
|
return data;
|
||
|
}
|
||
|
|
||
|
public void _SendData(Vector3[] data) => manager._SendData(data);
|
||
|
|
||
|
private void EnablePointer()
|
||
|
{
|
||
|
isPointerEnabled = true;
|
||
|
pointerRenderer.sharedMaterial = pointerMaterialActive;
|
||
|
}
|
||
|
|
||
|
private void DisablePointer()
|
||
|
{
|
||
|
isPointerEnabled = false;
|
||
|
pointerRenderer.sharedMaterial = pointerMaterialNormal;
|
||
|
}
|
||
|
|
||
|
private void ChangeToPen()
|
||
|
{
|
||
|
DisablePointer();
|
||
|
pointer.gameObject.SetActive(false);
|
||
|
}
|
||
|
|
||
|
private void ChangeToEraser()
|
||
|
{
|
||
|
pointer.gameObject.SetActive(true);
|
||
|
}
|
||
|
|
||
|
public void _UnpackData(Vector3[] data)
|
||
|
{
|
||
|
if (data.Length == 0)
|
||
|
return;
|
||
|
|
||
|
switch (GetMode(data))
|
||
|
{
|
||
|
case MODE_DRAW:
|
||
|
CreateInkInstance(data);
|
||
|
break;
|
||
|
case MODE_ERASE:
|
||
|
if (isUser && VRCPlayerApi.GetPlayerCount() > 1)
|
||
|
tmpErasedData = data;
|
||
|
else
|
||
|
EraseInk(data);
|
||
|
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
#region Draw Line
|
||
|
|
||
|
private void CreateInkInstance(Vector3[] data)
|
||
|
{
|
||
|
lineInstance = Instantiate(inkPrefab.gameObject);
|
||
|
lineInstance.name = $"{inkPrefix} ({inkNo++})";
|
||
|
|
||
|
var inkInfo = GetData(data, FOOTER_ELEMENT_DRAW_INK_INFO);
|
||
|
lineInstance.layer = (int)inkInfo.x;
|
||
|
lineInstance.GetComponentInChildren<MeshCollider>(true).gameObject.layer = (int)inkInfo.y;
|
||
|
SetParentAndResetLocalTransform(lineInstance.transform, (int)inkInfo.z == 1 ? inkPoolSynced : inkPoolNotSynced);
|
||
|
|
||
|
var line = lineInstance.GetComponent<LineRenderer>();
|
||
|
line.positionCount = data.Length - GetFooterLength(data);
|
||
|
line.SetPositions(data);
|
||
|
|
||
|
CreateInkCollider(line);
|
||
|
|
||
|
lineInstance.SetActive(true);
|
||
|
|
||
|
justBeforeInk = lineInstance;
|
||
|
}
|
||
|
|
||
|
private void CreateInkCollider(LineRenderer lineRenderer)
|
||
|
{
|
||
|
var inkCollider = lineRenderer.GetComponentInChildren<MeshCollider>(true);
|
||
|
inkCollider.name = "InkCollider";
|
||
|
SetParentAndResetLocalTransform(inkCollider.transform, lineInstance.transform);
|
||
|
|
||
|
var mesh = new Mesh();
|
||
|
|
||
|
{
|
||
|
var tmpWidthMultiplier = lineRenderer.widthMultiplier;
|
||
|
|
||
|
lineRenderer.widthMultiplier = inkWidth;
|
||
|
lineRenderer.BakeMesh(mesh);
|
||
|
lineRenderer.widthMultiplier = tmpWidthMultiplier;
|
||
|
}
|
||
|
|
||
|
inkCollider.GetComponent<MeshCollider>().sharedMesh = mesh;
|
||
|
inkCollider.gameObject.SetActive(true);
|
||
|
}
|
||
|
|
||
|
#endregion
|
||
|
|
||
|
#region Erase Line
|
||
|
|
||
|
private Vector3[] tmpErasedData;
|
||
|
public void ExecuteEraseInk()
|
||
|
{
|
||
|
if (tmpErasedData != null)
|
||
|
EraseInk(tmpErasedData);
|
||
|
|
||
|
tmpErasedData = null;
|
||
|
}
|
||
|
|
||
|
private void EraseInk(Vector3[] data)
|
||
|
{
|
||
|
if (data.Length < GetFooterSize(MODE_ERASE))
|
||
|
return;
|
||
|
|
||
|
var pointerPosition = GetData(data, FOOTER_ELEMENT_ERASE_POINTER_POSITION);
|
||
|
var radius = GetData(data, FOOTER_ELEMENT_ERASE_POINTER_RADIUS).x;
|
||
|
var count = Physics.OverlapSphereNonAlloc(pointerPosition, radius, results, 1 << inkColliderLayer, QueryTriggerInteraction.Ignore);
|
||
|
for (var i = 0; i < count; i++)
|
||
|
{
|
||
|
var other = results[i];
|
||
|
Transform t;
|
||
|
if (other && (t = other.transform.parent) && (t = t.parent) && (t = t.parent) && t.parent == inkPoolRoot)
|
||
|
{
|
||
|
Destroy(other.GetComponent<MeshCollider>().sharedMesh);
|
||
|
Destroy(other.transform.parent.gameObject);
|
||
|
}
|
||
|
|
||
|
results[i] = null;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
#endregion
|
||
|
|
||
|
#region Utility
|
||
|
|
||
|
private Gradient CreateReverseGradient(Gradient sourceGradient)
|
||
|
{
|
||
|
var newGradient = new Gradient();
|
||
|
var newColorKeys = new GradientColorKey[sourceGradient.colorKeys.Length];
|
||
|
var newAlphaKeys = new GradientAlphaKey[sourceGradient.alphaKeys.Length];
|
||
|
|
||
|
{
|
||
|
var length = newColorKeys.Length;
|
||
|
for (var i = 0; i < length; i++)
|
||
|
{
|
||
|
var colorKey = sourceGradient.colorKeys[length - 1 - i];
|
||
|
newColorKeys[i] = new GradientColorKey(colorKey.color, 1f - colorKey.time);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
{
|
||
|
var length = newAlphaKeys.Length;
|
||
|
for (var i = 0; i < length; i++)
|
||
|
{
|
||
|
var alphaKey = sourceGradient.alphaKeys[length - 1 - i];
|
||
|
newAlphaKeys[i] = new GradientAlphaKey(alphaKey.alpha, 1f - alphaKey.time);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
newGradient.SetKeys(newColorKeys, newAlphaKeys);
|
||
|
|
||
|
return newGradient;
|
||
|
}
|
||
|
|
||
|
private void SetParentAndResetLocalTransform(Transform child, Transform parent)
|
||
|
{
|
||
|
if (child)
|
||
|
{
|
||
|
child.SetParent(parent);
|
||
|
child.localPosition = Vector3.zero;
|
||
|
child.localRotation = Quaternion.identity;
|
||
|
child.localScale = Vector3.one;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
#endregion
|
||
|
|
||
|
#region Log
|
||
|
|
||
|
private void Log(object o) => Debug.Log($"{logPrefix}{o}", this);
|
||
|
private void Warning(object o) => Debug.LogWarning($"{logPrefix}{o}", this);
|
||
|
private void Error(object o) => Debug.LogError($"{logPrefix}{o}", this);
|
||
|
|
||
|
private readonly Color logColor = new Color(0xf2, 0x7d, 0x4a, 0xff) / 0xff;
|
||
|
private string ColorBeginTag(Color c) => $"<color=\"#{ToHtmlStringRGB(c)}\">";
|
||
|
private const string ColorEndTag = "</color>";
|
||
|
|
||
|
private string _logPrefix;
|
||
|
private string logPrefix
|
||
|
=> string.IsNullOrEmpty(_logPrefix)
|
||
|
? (_logPrefix = $"[{ColorBeginTag(logColor)}{nameof(QvPen)}.{nameof(QvPen.Udon)}.{nameof(QvPen_Pen)}{ColorEndTag}] ") : _logPrefix;
|
||
|
|
||
|
private string ToHtmlStringRGB(Color c)
|
||
|
{
|
||
|
c *= 0xff;
|
||
|
return $"{Mathf.RoundToInt(c.r):x2}{Mathf.RoundToInt(c.g):x2}{Mathf.RoundToInt(c.b):x2}";
|
||
|
}
|
||
|
|
||
|
#endregion
|
||
|
}
|
||
|
}
|