using System; using System.Collections; using System.Collections.Generic; using System.Globalization; using System.Linq; using MVR.FileManagementSecure; using SimpleJSON; using UnityEngine; /// /// Snug /// By Acidbubbles /// Adjust hand alignement and position to match your actual body /// Source: https://github.com/acidbubbles/vam-wrist /// Note: See FixedUpdate for the implementation of the algorithm /// public class Snug : MVRScript { private const float _baseCueSize = 0.35f; private const string _saveExt = "snugprofile"; private const string _saveFolder = "Saves\\snugprofiles"; private readonly List _cues = new List(); private readonly List _anchorPoints = new List(); private bool _ready, _loaded; private bool _leftHandActive, _rightHandActive; private GameObject _leftHandTarget, _rightHandTarget; private JSONStorableBool _possessHandsJSON; private JSONStorableFloat _falloffJSON; private JSONStorableFloat _handOffsetXJSON, _handOffsetYJSON, _handOffsetZJSON; private JSONStorableFloat _handRotateXJSON, _handRotateYJSON, _handRotateZJSON; private FreeControllerV3 _personLHandController, _personRHandController; private Transform _leftAutoSnapPoint, _rightAutoSnapPoint; private Vector3 _palmToWristOffset; private Vector3 _handRotateOffset; private JSONStorableBool _showVisualCuesJSON; private JSONStorableStringChooser _selectedAnchorsJSON; private JSONStorableFloat _anchorVirtScaleXJSON, _anchorVirtScaleZJSON; private JSONStorableFloat _anchorVirtOffsetXJSON, _anchorVirtOffsetYJSON, _anchorVirtOffsetZJSON; private JSONStorableFloat _anchorPhysOffsetXJSON, _anchorPhysOffsetYJSON, _anchorPhysOffsetZJSON; private JSONStorableFloat _anchorPhysScaleXJSON, _anchorPhysScaleZJSON; private JSONStorableBool _anchorActiveJSON; private readonly Vector3[] _lHandVisualCueLinePoints = new Vector3[VisualCueLineIndices.Count]; private readonly Vector3[] _rHandVisualCueLinePoints = new Vector3[VisualCueLineIndices.Count]; private LineRenderer _lHandVisualCueLine, _rHandVisualCueLine; private readonly List _lHandVisualCueLinePointIndicators = new List(); private readonly List _rHandVisualCueLinePointIndicators = new List(); private static class VisualCueLineIndices { public static int Anchor = 0; public static int Hand = 1; public static int Controller = 2; public static int Count = 3; } public override void Init() { try { if (containingAtom.type != "Person") { SuperController.LogError("Snug can only be applied on Person atoms."); return; } InitPossessHandsUI(); InitVisualCuesUI(); InitArmForRecordUI(); InitDisableSelectionUI(); CreateSpacer().height = 10f; InitPresetUI(); CreateSpacer().height = 10f; InitHandsSettingsUI(); InitAnchors(); InitAnchorsUI(); } catch (Exception exc) { SuperController.LogError($"{nameof(Snug)}.{nameof(Init)}: {exc}"); } StartCoroutine(DeferredInit()); } private void InitPossessHandsUI() { var s = SuperController.singleton; Transform touchObjectLeft = null; if (s.isOVR) touchObjectLeft = s.touchObjectLeft; else if (s.isOpenVR) touchObjectLeft = s.viveObjectLeft; if (touchObjectLeft != null) _leftAutoSnapPoint = touchObjectLeft.GetComponent().autoSnapPoint; _personLHandController = containingAtom.freeControllers.FirstOrDefault(fc => fc.name == "lHandControl"); if (_personLHandController == null) throw new NullReferenceException($"Could not find the lHandControl controller. Controllers: {string.Join(", ", containingAtom.freeControllers.Select(fc => fc.name).ToArray())}"); _leftHandTarget = new GameObject($"{containingAtom.gameObject.name}.lHandController.snugTarget"); var leftHandRigidBody = _leftHandTarget.AddComponent(); leftHandRigidBody.isKinematic = true; leftHandRigidBody.detectCollisions = false; Transform touchObjectRight = null; if (s.isOVR) touchObjectRight = s.touchObjectRight; else if (s.isOpenVR) touchObjectRight = s.viveObjectRight; if (touchObjectRight != null) _rightAutoSnapPoint = touchObjectRight.GetComponent().autoSnapPoint; _personRHandController = containingAtom.freeControllers.FirstOrDefault(fc => fc.name == "rHandControl"); if (_personRHandController == null) throw new NullReferenceException($"Could not find the rHandControl controller. Controllers: {string.Join(", ", containingAtom.freeControllers.Select(fc => fc.name).ToArray())}"); _rightHandTarget = new GameObject($"{containingAtom.gameObject.name}.rHandController.snugTarget"); var rightHandRigidBody = _rightHandTarget.AddComponent(); rightHandRigidBody.isKinematic = true; rightHandRigidBody.detectCollisions = false; _possessHandsJSON = new JSONStorableBool("Possess Hands", false, (bool val) => { if (!_ready) return; if (val) { if (_leftAutoSnapPoint != null) _leftHandActive = true; if (_rightAutoSnapPoint != null) _rightHandActive = true; StartCoroutine(DeferredActivateHands()); } else { if (_leftHandActive) { CustomReleaseHand(_personLHandController); _leftHandActive = false; } if (_rightHandActive) { CustomReleaseHand(_personRHandController); _rightHandActive = false; } } }) { isStorable = false }; RegisterBool(_possessHandsJSON); CreateToggle(_possessHandsJSON); } private void InitVisualCuesUI() { _showVisualCuesJSON = new JSONStorableBool("Show Visual Cues", true, (bool val) => { if (!_ready) return; if (val) CreateVisualCues(); else DestroyVisualCues(); }); RegisterBool(_showVisualCuesJSON); CreateToggle(_showVisualCuesJSON); } private void InitArmForRecordUI() { CreateButton("Arm hands for record").button.onClick.AddListener(() => { SuperController.singleton.ArmAllControlledControllersForRecord(); _personLHandController.GetComponent().armedForRecord = true; _personRHandController.GetComponent().armedForRecord = true; }); } private struct FreeControllerV3Snapshot { public bool canGrabPosition; public bool canGrabRotation; } private readonly Dictionary _previousState = new Dictionary(); private void InitDisableSelectionUI() { var disableSelectionJSON = new JSONStorableBool("Make controllers unselectable", false, (bool val) => { if (val) { foreach (var fc in containingAtom.freeControllers) { if (fc == _personLHandController || fc == _personRHandController) continue; if (!fc.canGrabPosition && !fc.canGrabRotation) continue; if (!fc.name.EndsWith("Control")) continue; var state = new FreeControllerV3Snapshot { canGrabPosition = fc.canGrabPosition, canGrabRotation = fc.canGrabRotation }; _previousState[fc] = state; fc.canGrabPosition = false; fc.canGrabRotation = false; } } else { foreach (var kvp in _previousState) { kvp.Key.canGrabPosition = kvp.Value.canGrabPosition; kvp.Key.canGrabRotation = kvp.Value.canGrabRotation; } _previousState.Clear(); } }); CreateToggle(disableSelectionJSON); } private void InitPresetUI() { var loadPresetUI = CreateButton("Load Preset", false); loadPresetUI.button.onClick.AddListener(() => { FileManagerSecure.CreateDirectory(_saveFolder); var shortcuts = FileManagerSecure.GetShortCutsForDirectory(_saveFolder); SuperController.singleton.GetMediaPathDialog((string path) => { if (string.IsNullOrEmpty(path)) return; JSONClass jc = (JSONClass)LoadJSON(path); RestoreFromJSON(jc); SyncSelectedAnchorJSON(""); SyncHandsOffset(); }, _saveExt, _saveFolder, false, true, false, null, false, shortcuts); }); var savePresetUI = CreateButton("Save Preset", false); savePresetUI.button.onClick.AddListener(() => { FileManagerSecure.CreateDirectory(_saveFolder); var fileBrowserUI = SuperController.singleton.fileBrowserUI; fileBrowserUI.SetTitle("Save colliders preset"); fileBrowserUI.fileRemovePrefix = null; fileBrowserUI.hideExtension = false; fileBrowserUI.keepOpen = false; fileBrowserUI.fileFormat = _saveExt; fileBrowserUI.defaultPath = _saveFolder; fileBrowserUI.showDirs = true; fileBrowserUI.shortCuts = null; fileBrowserUI.browseVarFilesAsDirectories = false; fileBrowserUI.SetTextEntry(true); fileBrowserUI.Show((string path) => { fileBrowserUI.fileFormat = null; if (string.IsNullOrEmpty(path)) return; if (!path.ToLower().EndsWith($".{_saveExt}")) path += $".{_saveExt}"; var jc = GetJSON(); jc.Remove("id"); SaveJSON(jc, path); }); fileBrowserUI.ActivateFileNameField(); }); } private void InitHandsSettingsUI() { _falloffJSON = new JSONStorableFloat("Effect Falloff", 0.3f, UpdateHandsOffset, 0f, 5f, false) { isStorable = false }; CreateSlider(_falloffJSON, false); _handOffsetXJSON = new JSONStorableFloat("Hand Offset X", 0f, UpdateHandsOffset, -0.2f, 0.2f, true) { isStorable = false }; CreateSlider(_handOffsetXJSON, false); _handOffsetYJSON = new JSONStorableFloat("Hand Offset Y", 0f, UpdateHandsOffset, -0.2f, 0.2f, true) { isStorable = false }; CreateSlider(_handOffsetYJSON, false); _handOffsetZJSON = new JSONStorableFloat("Hand Offset Z", 0f, UpdateHandsOffset, -0.2f, 0.2f, true) { isStorable = false }; CreateSlider(_handOffsetZJSON, false); _handRotateXJSON = new JSONStorableFloat("Hand Rotate X", 0f, UpdateHandsOffset, -25f, 25f, false) { isStorable = false }; CreateSlider(_handRotateXJSON, false); _handRotateYJSON = new JSONStorableFloat("Hand Rotate Y", 0f, UpdateHandsOffset, -25f, 25f, false) { isStorable = false }; CreateSlider(_handRotateYJSON, false); _handRotateZJSON = new JSONStorableFloat("Hand Rotate Z", 0f, UpdateHandsOffset, -25f, 25f, false) { isStorable = false }; CreateSlider(_handRotateZJSON, false); } private void InitAnchors() { _anchorPoints.Add(new ControllerAnchorPoint { Label = "Head", RigidBody = containingAtom.rigidbodies.First(rb => rb.name == "head"), VirtualOffset = new Vector3(0, 0.2f, 0), VirtualScale = new Vector3(1, 1, 1), PhysicalOffset = new Vector3(0, 0, 0), PhysicalScale = new Vector3(1, 1, 1), Active = true, Locked = true }); _anchorPoints.Add(new ControllerAnchorPoint { Label = "Lips", RigidBody = containingAtom.rigidbodies.First(rb => rb.name == "LipTrigger"), VirtualOffset = new Vector3(0, 0, -0.07113313f), VirtualScale = new Vector3(0.3830788f, 1f, 0.5201911f), PhysicalOffset = new Vector3(0, 0, 0), PhysicalScale = new Vector3(1, 1, 1), Active = true }); _anchorPoints.Add(new ControllerAnchorPoint { Label = "Chest", RigidBody = containingAtom.rigidbodies.First(rb => rb.name == "chest"), VirtualOffset = new Vector3(0, 0.0682705f, 0.04585214f), VirtualScale = new Vector3(0.8217609f, 1, 0.8080147f), PhysicalOffset = new Vector3(0, 0, 0), PhysicalScale = new Vector3(1, 1, 1), Active = true }); _anchorPoints.Add(new ControllerAnchorPoint { Label = "Abdomen", RigidBody = containingAtom.rigidbodies.First(rb => rb.name == "abdomen"), VirtualOffset = new Vector3(0, 0.0770329f, 0.04218798f), VirtualScale = new Vector3(0.7670161f, 1, 0.5064289f), PhysicalOffset = new Vector3(0, 0, 0), PhysicalScale = new Vector3(1, 1, 1), Active = true }); _anchorPoints.Add(new ControllerAnchorPoint { Label = "Hips", RigidBody = containingAtom.rigidbodies.First(rb => rb.name == "hip"), VirtualOffset = new Vector3(0, -0.08762675f, -0.009161186f), VirtualScale = new Vector3(1.041181f, 1, 0.8080372f), PhysicalOffset = new Vector3(0, 0, 0), PhysicalScale = new Vector3(1, 1, 1), Active = true }); _anchorPoints.Add(new ControllerAnchorPoint { Label = "Ground (Control)", RigidBody = containingAtom.rigidbodies.First(rb => rb.name == "object"), VirtualOffset = new Vector3(0, 0, 0), VirtualScale = new Vector3(1, 1, 1), PhysicalOffset = new Vector3(0, 0, 0), PhysicalScale = new Vector3(1, 1, 1), Active = true, Locked = true }); } private void InitAnchorsUI() { _selectedAnchorsJSON = new JSONStorableStringChooser("Selected Anchor", _anchorPoints.Select(a => a.Label).ToList(), "Chest", "Anchor", SyncSelectedAnchorJSON) { isStorable = false }; CreateScrollablePopup(_selectedAnchorsJSON, true); _anchorVirtScaleXJSON = new JSONStorableFloat("Virtual Scale X", 1f, UpdateAnchor, 0.01f, 3f, true) { isStorable = false }; CreateSlider(_anchorVirtScaleXJSON, true); _anchorVirtScaleZJSON = new JSONStorableFloat("Virtual Scale Z", 1f, UpdateAnchor, 0.01f, 3f, true) { isStorable = false }; CreateSlider(_anchorVirtScaleZJSON, true); _anchorVirtOffsetXJSON = new JSONStorableFloat("Virtual Offset X", 0f, UpdateAnchor, -0.2f, 0.2f, true) { isStorable = false }; CreateSlider(_anchorVirtOffsetXJSON, true); _anchorVirtOffsetYJSON = new JSONStorableFloat("Virtual Offset Y", 0f, UpdateAnchor, -0.2f, 0.2f, true) { isStorable = false }; CreateSlider(_anchorVirtOffsetYJSON, true); _anchorVirtOffsetZJSON = new JSONStorableFloat("Virtual Offset Z", 0f, UpdateAnchor, -0.2f, 0.2f, true) { isStorable = false }; CreateSlider(_anchorVirtOffsetZJSON, true); _anchorPhysScaleXJSON = new JSONStorableFloat("Physical Scale X", 1f, UpdateAnchor, 0.01f, 3f, true) { isStorable = false }; CreateSlider(_anchorPhysScaleXJSON, true); _anchorPhysScaleZJSON = new JSONStorableFloat("Physical Scale Z", 1f, UpdateAnchor, 0.01f, 3f, true) { isStorable = false }; CreateSlider(_anchorPhysScaleZJSON, true); _anchorPhysOffsetXJSON = new JSONStorableFloat("Physical Offset X", 0f, UpdateAnchor, -0.2f, 0.2f, true) { isStorable = false }; CreateSlider(_anchorPhysOffsetXJSON, true); _anchorPhysOffsetYJSON = new JSONStorableFloat("Physical Offset Y", 0f, UpdateAnchor, -0.2f, 0.2f, true) { isStorable = false }; CreateSlider(_anchorPhysOffsetYJSON, true); _anchorPhysOffsetZJSON = new JSONStorableFloat("Physical Offset Z", 0f, UpdateAnchor, -0.2f, 0.2f, true) { isStorable = false }; CreateSlider(_anchorPhysOffsetZJSON, true); _anchorActiveJSON = new JSONStorableBool("Anchor Active", true, (bool _) => UpdateAnchor(0)) { isStorable = false }; CreateToggle(_anchorActiveJSON, true); } private IEnumerator DeferredActivateHands() { yield return new WaitForSeconds(0f); if (_leftHandActive) CustomPossessHand(_personLHandController, _leftHandTarget); if (_rightHandActive) CustomPossessHand(_personRHandController, _rightHandTarget); } private static void CustomPossessHand(FreeControllerV3 controllerV3, GameObject target) { controllerV3.PossessMoveAndAlignTo(target.transform); controllerV3.SelectLinkToRigidbody(target.GetComponent()); controllerV3.canGrabPosition = false; controllerV3.canGrabRotation = false; controllerV3.possessed = true; controllerV3.possessable = false; (controllerV3.GetComponent() ?? controllerV3.GetComponent().handControl).possessed = true; } private static void CustomReleaseHand(FreeControllerV3 controllerV3) { controllerV3.RestorePreLinkState(); // TODO: This should return to the previous state controllerV3.canGrabPosition = true; controllerV3.canGrabRotation = true; controllerV3.possessed = false; controllerV3.possessable = true; (controllerV3.GetComponent() ?? controllerV3.GetComponent().handControl).possessed = false; } private IEnumerator DeferredInit() { yield return new WaitForEndOfFrame(); try { if (!_loaded) containingAtom.RestoreFromLast(this); OnEnable(); SyncSelectedAnchorJSON(""); SyncHandsOffset(); if (_showVisualCuesJSON.val) CreateVisualCues(); } catch (Exception exc) { SuperController.LogError($"{nameof(Snug)}.{nameof(DeferredInit)}: {exc}"); } } #region Load / Save public override JSONClass GetJSON(bool includePhysical = true, bool includeAppearance = true, bool forceStore = false) { var json = base.GetJSON(includePhysical, includeAppearance, forceStore); try { json["hands"] = new JSONClass{ {"offset", SerializeVector3(_palmToWristOffset)}, {"rotation", SerializeVector3(_handRotateOffset)} }; var anchors = new JSONClass(); foreach (var anchor in _anchorPoints) { anchors[anchor.Label] = new JSONClass { {"virOffset", SerializeVector3(anchor.VirtualOffset)}, {"virScale", SerializeVector2(new Vector3(anchor.VirtualScale.x, anchor.VirtualScale.z))}, {"phyOffset", SerializeVector3(anchor.PhysicalOffset)}, {"phyScale", SerializeVector2(new Vector2(anchor.PhysicalScale.x, anchor.PhysicalScale.z))}, {"active", anchor.Active ? "true" : "false"}, }; } json["anchors"] = anchors; needsStore = true; } catch (Exception exc) { SuperController.LogError($"{nameof(Snug)}.{nameof(GetJSON)}: {exc}"); } return json; } private static string SerializeVector3(Vector3 v) { return $"{v.x.ToString(CultureInfo.InvariantCulture)},{v.y.ToString(CultureInfo.InvariantCulture)},{v.z.ToString(CultureInfo.InvariantCulture)}"; } private static string SerializeVector2(Vector2 v) { return $"{v.x.ToString(CultureInfo.InvariantCulture)},{v.y.ToString(CultureInfo.InvariantCulture)}"; } public override void RestoreFromJSON(JSONClass jc, bool restorePhysical = true, bool restoreAppearance = true, JSONArray presetAtoms = null, bool setMissingToDefault = true) { base.RestoreFromJSON(jc, restorePhysical, restoreAppearance, presetAtoms, setMissingToDefault); try { var handsJSON = jc["hands"]; if (handsJSON != null) { _palmToWristOffset = DeserializeVector3(handsJSON["offset"], _palmToWristOffset); _handRotateOffset = DeserializeVector3(handsJSON["rotation"], _handRotateOffset); } var anchorsJSON = jc["anchors"]; if (anchorsJSON != null) { foreach (var anchor in _anchorPoints) { var anchorJSON = anchorsJSON[anchor.Label]; if (anchorJSON == null) continue; anchor.VirtualOffset = DeserializeVector3(anchorJSON["virOffset"], anchor.VirtualOffset); anchor.VirtualScale = DeserializeVector2AsFlatScale(anchorJSON["virScale"], anchor.VirtualScale); anchor.PhysicalOffset = DeserializeVector3(anchorJSON["phyOffset"], anchor.PhysicalOffset); anchor.PhysicalScale = DeserializeVector2AsFlatScale(anchorJSON["phyScale"], anchor.PhysicalScale); anchor.Active = anchorJSON["active"]?.Value != "false"; anchor.Update(); } } _loaded = true; } catch (Exception exc) { SuperController.LogError($"{nameof(Snug)}.{nameof(RestoreFromJSON)}: {exc}"); } } private static Vector3 DeserializeVector3(string v, Vector3 defaultValue) { if (string.IsNullOrEmpty(v)) return defaultValue; var s = v.Split(','); return new Vector3(float.Parse(s[0], CultureInfo.InvariantCulture), float.Parse(s[1], CultureInfo.InvariantCulture), float.Parse(s[2], CultureInfo.InvariantCulture)); } private static Vector3 DeserializeVector2AsFlatScale(string v, Vector3 defaultValue) { if (string.IsNullOrEmpty(v)) return defaultValue; var s = v.Split(','); return new Vector3(float.Parse(s[0], CultureInfo.InvariantCulture), 1f, float.Parse(s[1], CultureInfo.InvariantCulture)); } #endregion private void SyncSelectedAnchorJSON(string _) { if (!_ready) return; var anchor = _anchorPoints.FirstOrDefault(a => a.Label == _selectedAnchorsJSON.val); if (anchor == null) throw new NullReferenceException($"Could not find the selected anchor {_selectedAnchorsJSON.val}"); _anchorVirtScaleXJSON.valNoCallback = anchor.VirtualScale.x; _anchorVirtScaleZJSON.valNoCallback = anchor.VirtualScale.z; _anchorVirtOffsetXJSON.valNoCallback = anchor.VirtualOffset.x; _anchorVirtOffsetYJSON.valNoCallback = anchor.VirtualOffset.y; _anchorVirtOffsetZJSON.valNoCallback = anchor.VirtualOffset.z; _anchorPhysScaleXJSON.valNoCallback = anchor.PhysicalScale.x; _anchorPhysScaleZJSON.valNoCallback = anchor.PhysicalScale.z; _anchorPhysOffsetXJSON.valNoCallback = anchor.PhysicalOffset.x; _anchorPhysOffsetYJSON.valNoCallback = anchor.PhysicalOffset.y; _anchorPhysOffsetZJSON.valNoCallback = anchor.PhysicalOffset.z; _anchorActiveJSON.valNoCallback = anchor.Active; } private void UpdateAnchor(float _) { if (!_ready) return; var anchor = _anchorPoints.FirstOrDefault(a => a.Label == _selectedAnchorsJSON.val); if (anchor == null) throw new NullReferenceException($"Could not find the selected anchor {_selectedAnchorsJSON.val}"); anchor.VirtualScale = new Vector3(_anchorVirtScaleXJSON.val, 1f, _anchorVirtScaleZJSON.val); anchor.VirtualOffset = new Vector3(_anchorVirtOffsetXJSON.val, _anchorVirtOffsetYJSON.val, _anchorVirtOffsetZJSON.val); anchor.PhysicalScale = new Vector3(_anchorPhysScaleXJSON.val, 1f, _anchorPhysScaleZJSON.val); anchor.PhysicalOffset = new Vector3(_anchorPhysOffsetXJSON.val, _anchorPhysOffsetYJSON.val, _anchorPhysOffsetZJSON.val); if (anchor.Locked && !_anchorActiveJSON.val) { _anchorActiveJSON.valNoCallback = true; } else if (anchor.Active != _anchorActiveJSON.val) { anchor.Active = _anchorActiveJSON.val; anchor.Update(); } anchor.Update(); } private void SyncHandsOffset() { if (!_ready) return; _handOffsetXJSON.valNoCallback = _palmToWristOffset.x; _handOffsetYJSON.valNoCallback = _palmToWristOffset.y; _handOffsetZJSON.valNoCallback = _palmToWristOffset.z; _handRotateXJSON.valNoCallback = _handRotateOffset.x; _handRotateYJSON.valNoCallback = _handRotateOffset.y; _handRotateZJSON.valNoCallback = _handRotateOffset.z; } private void UpdateHandsOffset(float _) { if (!_ready) return; _palmToWristOffset = new Vector3(_handOffsetXJSON.val, _handOffsetYJSON.val, _handOffsetZJSON.val); _handRotateOffset = new Vector3(_handRotateXJSON.val, _handRotateYJSON.val, _handRotateZJSON.val); } #region Update public void Update() { if (!_ready) return; try { UpdateCueLine(_lHandVisualCueLine, _lHandVisualCueLinePoints, _lHandVisualCueLinePointIndicators); UpdateCueLine(_rHandVisualCueLine, _rHandVisualCueLinePoints, _rHandVisualCueLinePointIndicators); } catch (Exception exc) { SuperController.LogError($"{nameof(Snug)}.{nameof(Update)}: {exc}"); OnDisable(); } } private static void UpdateCueLine(LineRenderer line, Vector3[] points, IList indicators) { if (line != null) { line.SetPositions(points); for (var i = 0; i < points.Length; i++) indicators[i].transform.position = points[i]; } } public void FixedUpdate() { if (!_ready) return; try { if (_leftHandActive || _showVisualCuesJSON.val && _leftAutoSnapPoint != null) ProcessHand(_leftHandTarget, SuperController.singleton.leftHand, _leftAutoSnapPoint, new Vector3(_palmToWristOffset.x * -1f, _palmToWristOffset.y, _palmToWristOffset.z), new Vector3(_handRotateOffset.x, -_handRotateOffset.y, -_handRotateOffset.z), _lHandVisualCueLinePoints); if (_rightHandActive || _showVisualCuesJSON.val && _rightAutoSnapPoint != null) ProcessHand(_rightHandTarget, SuperController.singleton.rightHand, _rightAutoSnapPoint, _palmToWristOffset, _handRotateOffset, _rHandVisualCueLinePoints); } catch (Exception exc) { SuperController.LogError($"{nameof(Snug)}.{nameof(FixedUpdate)}: {exc}"); OnDisable(); } } private void ProcessHand(GameObject handTarget, Transform physicalHand, Transform autoSnapPoint, Vector3 palmToWristOffset, Vector3 handRotateOffset, Vector3[] visualCueLinePoints) { // Base position var position = physicalHand.position; var snapOffset = autoSnapPoint.localPosition; // Find the anchor over and under the controller ControllerAnchorPoint lower = null; ControllerAnchorPoint upper = null; foreach (var anchorPoint in _anchorPoints) { if (!anchorPoint.Active) continue; var anchorPointPos = anchorPoint.GetWorldPosition(); if (position.y > anchorPointPos.y && (lower == null || anchorPointPos.y > lower.GetWorldPosition().y)) { lower = anchorPoint; } else if (position.y < anchorPointPos.y && (upper == null || anchorPointPos.y < upper.GetWorldPosition().y)) { upper = anchorPoint; } } if (lower == null) lower = upper; else if (upper == null) upper = lower; // Find the weight of both anchors (closest = strongest effect) var upperPosition = upper.GetWorldPosition(); var lowerPosition = lower.GetWorldPosition(); var yUpperDelta = upperPosition.y - position.y; var yLowerDelta = position.y - lowerPosition.y; var totalDelta = yLowerDelta + yUpperDelta; var upperWeight = totalDelta == 0 ? 1f : yLowerDelta / totalDelta; var lowerWeight = 1f - upperWeight; var lowerRotation = lower.RigidBody.transform.rotation; var upperRotation = upper.RigidBody.transform.rotation; var anchorPosition = Vector3.Lerp(upperPosition, lowerPosition, lowerWeight); var anchorRotation = Quaternion.Lerp(upperRotation, lowerRotation, lowerWeight); // TODO: Is this useful? anchorPosition.y = position.y; visualCueLinePoints[VisualCueLineIndices.Anchor] = anchorPosition; // Determine the falloff (closer = stronger, fades out with distance) // TODO: Even better to use closest point on ellipse, but not necessary. var distance = Mathf.Abs(Vector3.Distance(anchorPosition, position)); var physicalCueSize = Vector3.Lerp(Vector3.Scale(upper.VirtualScale, upper.PhysicalScale), Vector3.Scale(lower.VirtualScale, lower.PhysicalScale), lowerWeight) * (_baseCueSize / 2f); var physicalCueDistanceFromCenter = Mathf.Max(physicalCueSize.x, physicalCueSize.z); // TODO: Check both x and z to determine a falloff relative to both distances var falloff = _falloffJSON.val > 0 ? 1f - (Mathf.Clamp(distance - physicalCueDistanceFromCenter, 0, _falloffJSON.val) / _falloffJSON.val) : 1f; // Calculate the controller offset based on the physical scale/offset of anchors var physicalScale = Vector3.Lerp(upper.PhysicalScale, lower.PhysicalScale, lowerWeight); var physicalOffset = Vector3.Lerp(upper.PhysicalOffset, lower.PhysicalOffset, lowerWeight); var baseOffset = position - anchorPosition; var resultOffset = Quaternion.Inverse(anchorRotation) * baseOffset; resultOffset = new Vector3(resultOffset.x / physicalScale.x, resultOffset.y / physicalScale.y, resultOffset.z / physicalScale.z) - physicalOffset; resultOffset = anchorRotation * resultOffset; var resultPosition = anchorPosition + Vector3.Lerp(baseOffset, resultOffset, falloff); visualCueLinePoints[VisualCueLineIndices.Hand] = resultPosition; // Apply the hands adjustments var resultRotation = autoSnapPoint.rotation * Quaternion.Euler(handRotateOffset); resultPosition += resultRotation * (snapOffset + palmToWristOffset); // Do the displacement var rb = handTarget.GetComponent(); rb.MovePosition(resultPosition); rb.MoveRotation(resultRotation); visualCueLinePoints[VisualCueLineIndices.Controller] = physicalHand.transform.position; // SuperController.singleton.ClearMessages(); // SuperController.LogMessage($"y {position.y:0.00} btwn {lower.RigidBody.name} y {yLowerDelta:0.00} w {lowerWeight:0.00} and {upper.RigidBody.name} y {yUpperDelta:0.00} w {upperWeight: 0.00}"); // SuperController.LogMessage($"dist {distance:0.00}/{physicalCueDistanceFromCenter:0.00} falloff {falloff:0.00}"); // SuperController.LogMessage($"rot {upperRotation.eulerAngles} psca {physicalScale} poff {physicalOffset} base {baseOffset} res {resultOffset}"); } #endregion #region Lifecycle public void OnEnable() { try { _ready = true; } catch (Exception exc) { SuperController.LogError($"{nameof(Snug)}.{nameof(OnEnable)}: {exc}"); } } public void OnDisable() { try { _ready = false; DestroyVisualCues(); Destroy(_rightHandTarget); } catch (Exception exc) { SuperController.LogError($"{nameof(Snug)}.{nameof(OnDisable)}: {exc}"); } } public void OnDestroy() { OnDisable(); } #endregion #region Debuggers private void CreateVisualCues() { DestroyVisualCues(); _lHandVisualCueLine = CreateHandVisualCue(_lHandVisualCueLinePointIndicators); _rHandVisualCueLine = CreateHandVisualCue(_rHandVisualCueLinePointIndicators); foreach (var anchorPoint in _anchorPoints) { anchorPoint.VirtualCue = new ControllerAnchorPointVisualCue(anchorPoint.RigidBody.transform, Color.gray); anchorPoint.Update(); anchorPoint.PhysicalCue = new ControllerAnchorPointVisualCue(anchorPoint.RigidBody.transform, Color.white); anchorPoint.Update(); } } private LineRenderer CreateHandVisualCue(List visualCueLinePointIndicators) { var lineGo = new GameObject(); var line = VisualCuesHelper.CreateLine(lineGo, Color.yellow, 0.002f, VisualCueLineIndices.Count, true); _cues.Add(lineGo); for (var i = 0; i < VisualCueLineIndices.Count; i++) { var p = VisualCuesHelper.CreatePrimitive(null, PrimitiveType.Cube, Color.yellow); p.transform.localScale = new Vector3(0.02f, 0.02f, 0.02f); visualCueLinePointIndicators.Add(p); } return line; } private void DestroyVisualCues() { _lHandVisualCueLine = null; _rHandVisualCueLine = null; foreach (var p in _lHandVisualCueLinePointIndicators) Destroy(p); _lHandVisualCueLinePointIndicators.Clear(); foreach (var p in _rHandVisualCueLinePointIndicators) Destroy(p); _rHandVisualCueLinePointIndicators.Clear(); foreach (var debugger in _cues) { Destroy(debugger); } _cues.Clear(); foreach (var anchor in _anchorPoints) { Destroy(anchor.VirtualCue?.gameObject); anchor.VirtualCue = null; Destroy(anchor.PhysicalCue?.gameObject); anchor.PhysicalCue = null; } } #endregion private class ControllerAnchorPoint { public string Label { get; set; } public Rigidbody RigidBody { get; set; } public Vector3 PhysicalOffset { get; set; } public Vector3 PhysicalScale { get; set; } public Vector3 VirtualOffset { get; set; } public Vector3 VirtualScale { get; set; } public ControllerAnchorPointVisualCue VirtualCue { get; set; } public ControllerAnchorPointVisualCue PhysicalCue { get; set; } public bool Active { get; set; } public bool Locked { get; set; } public Vector3 GetWorldPosition() { return RigidBody.transform.position + RigidBody.transform.rotation * (VirtualOffset + PhysicalOffset); } public void Update() { if (VirtualCue != null) { VirtualCue.gameObject.SetActive(Active); VirtualCue.Update(VirtualOffset, VirtualScale); } if (PhysicalCue != null) { PhysicalCue.gameObject.SetActive(Active); PhysicalCue.Update(VirtualOffset + PhysicalOffset, Vector3.Scale(VirtualScale, PhysicalScale)); } } } private class ControllerAnchorPointVisualCue : IDisposable { private const float _width = 0.005f; public readonly GameObject gameObject; private readonly Transform _xAxis; private readonly Transform _zAxis; private readonly Transform _frontHandle; private readonly Transform _leftHandle; private readonly Transform _rightHandle; private readonly LineRenderer _ellipse; public ControllerAnchorPointVisualCue(Transform parent, Color color) { var go = new GameObject(); _xAxis = VisualCuesHelper.CreatePrimitive(go.transform, PrimitiveType.Cube, Color.red).transform; _zAxis = VisualCuesHelper.CreatePrimitive(go.transform, PrimitiveType.Cube, Color.blue).transform; _frontHandle = VisualCuesHelper.CreatePrimitive(go.transform, PrimitiveType.Cube, color).transform; _leftHandle = VisualCuesHelper.CreatePrimitive(go.transform, PrimitiveType.Cube, color).transform; _rightHandle = VisualCuesHelper.CreatePrimitive(go.transform, PrimitiveType.Cube, color).transform; _ellipse = VisualCuesHelper.CreateEllipse(go, color, _width); if (_ellipse == null) throw new NullReferenceException("Boom"); gameObject = go; gameObject.transform.parent = parent; gameObject.transform.localPosition = Vector3.zero; gameObject.transform.localRotation = Quaternion.identity; } public void Update(Vector3 offset, Vector3 scale) { gameObject.transform.localPosition = offset; var size = new Vector2(_baseCueSize * scale.x, _baseCueSize * scale.z); _xAxis.localScale = new Vector3(size.x - _width * 2, _width * 0.25f, _width * 0.25f); _zAxis.localScale = new Vector3(_width * 0.25f, _width * 0.25f, size.y - _width * 2); _frontHandle.localScale = new Vector3(_width * 3, _width * 3, _width * 3); _frontHandle.transform.localPosition = Vector3.forward * size.y / 2; _leftHandle.localScale = new Vector3(_width * 3, _width * 3, _width * 3); _leftHandle.transform.localPosition = Vector3.left * size.x / 2; _rightHandle.localScale = new Vector3(_width * 3, _width * 3, _width * 3); _rightHandle.transform.localPosition = Vector3.right * size.x / 2; if (_ellipse == null) throw new NullReferenceException("Bam"); VisualCuesHelper.DrawEllipse(_ellipse, new Vector2(size.x / 2f, size.y / 2f)); } public void Dispose() { Destroy(gameObject); } } private static class VisualCuesHelper { public static GameObject Cross(Color color) { var go = new GameObject(); var size = 0.2f; var width = 0.005f; CreatePrimitive(go.transform, PrimitiveType.Cube, Color.red).transform.localScale = new Vector3(size, width, width); CreatePrimitive(go.transform, PrimitiveType.Cube, Color.green).transform.localScale = new Vector3(width, size, width); CreatePrimitive(go.transform, PrimitiveType.Cube, Color.blue).transform.localScale = new Vector3(width, width, size); CreatePrimitive(go.transform, PrimitiveType.Sphere, color).transform.localScale = new Vector3(size / 8f, size / 8f, size / 8f); foreach (var c in go.GetComponentsInChildren()) Destroy(c); return go; } public static GameObject CreatePrimitive(Transform parent, PrimitiveType type, Color color) { var go = GameObject.CreatePrimitive(type); go.GetComponent().material = new Material(Shader.Find("Sprites/Default")) { color = color, renderQueue = 4000 }; foreach (var c in go.GetComponents()) { c.enabled = false; Destroy(c); } go.transform.parent = parent; return go; } public static LineRenderer CreateLine(GameObject go, Color color, float width, int points, bool useWorldSpace) { var line = go.AddComponent(); line.useWorldSpace = useWorldSpace; line.material = new Material(Shader.Find("Sprites/Default")) { renderQueue = 4000 }; line.widthMultiplier = width; line.colorGradient = new Gradient { colorKeys = new[] { new GradientColorKey(color, 0f), new GradientColorKey(color, 1f) } }; line.positionCount = points; return line; } public static LineRenderer CreateEllipse(GameObject go, Color color, float width, int resolution = 32) { var line = CreateLine(go, color, width, resolution, false); return line; } public static void DrawEllipse(LineRenderer line, Vector2 radius) { for (int i = 0; i <= line.positionCount; i++) { var angle = i / (float)line.positionCount * 2.0f * Mathf.PI; Quaternion pointQuaternion = Quaternion.AngleAxis(90, Vector3.right); Vector3 pointPosition; pointPosition = new Vector3(radius.x * Mathf.Cos(angle), radius.y * Mathf.Sin(angle), 0.0f); pointPosition = pointQuaternion * pointPosition; line.SetPosition(i, pointPosition); } line.loop = true; } } }