(could use some optimization, but I’ve been looking for this for so long)
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Valve.VR;
using Valve.VR.InteractionSystem;
public class Scaleable : MonoBehaviour
{
private GameObject middleMan;
private bool stoppingResize;
private SteamVR_Action_Boolean grabBoolean = SteamVR_Input.GetAction<SteamVR_Action_Boolean>("GrabGrip");
public SteamVR_Skeleton_JointIndexEnum fingerJointHover = SteamVR_Skeleton_JointIndexEnum.indexTip;
protected MeshRenderer[] highlightRenderers;
protected MeshRenderer[] existingRenderers;
protected GameObject highlightHolder;
protected SkinnedMeshRenderer[] highlightSkinnedRenderers;
protected SkinnedMeshRenderer[] existingSkinnedRenderers;
protected static Material highlightMat;
[Tooltip("An array of child gameObjects to not render a highlight for. Things like transparent parts, vfx, etc.")]
public GameObject[] hideHighlight;
private bool isResizing;
public SteamVR_Action_Boolean rightGrab;
public Hand rightHand;
private bool rightGrabbing;
private Collider[] rightOverlappingColliders;
public LayerMask rightHoverLayerMask = -1;
public SteamVR_Action_Boolean leftGrab;
public Hand leftHand;
private bool hovering;
private bool wasHovering;
private bool leftGrabbing;
private Collider[] leftOverlappingColliders;
public LayerMask leftHoverLayerMask = -1;
public int hoverPriority;
private int prevOverlappingColliders = 0;
private bool attachedToHand;
private float initialDistance;
private Vector3 initialScale;
private Quaternion initialRot;
private Vector3 offsetPos;
private Hand currentMain;
private List<MeshRenderer> flashingRenderers = new List<MeshRenderer>();
public Color hintColor;
public GameObject grabHintPrefab;
private GameObject rightTextHint;
private GameObject leftTextHint;
void OnEnable()
{
rightGrab.AddOnChangeListener(SetRightGrab, rightHand.handType);
leftGrab.AddOnChangeListener(SetLeftGrab, leftHand.handType);
}
void Start()
{
highlightMat = (Material)Resources.Load("SteamVR_HoverHighlight", typeof(Material));
if (highlightMat == null)
{
Debug.LogError("<b>[SteamVR Interaction]</b> Hover Highlight Material is missing. Please create a material named 'SteamVR_HoverHighlight' and place it in a Resources folder", this);
}
if (rightHand.gameObject.layer == 0)
Debug.LogWarning("<b>[SteamVR Interaction]</b> Hand is on default layer. This puts unnecessary strain on hover checks as it is always true for hand colliders (which are then ignored).", this);
else
rightHoverLayerMask &= ~(1 << rightHand.gameObject.layer); //ignore self for hovering
if (leftHand.gameObject.layer == 0)
Debug.LogWarning("<b>[SteamVR Interaction]</b> Hand is on default layer. This puts unnecessary strain on hover checks as it is always true for hand colliders (which are then ignored).", this);
else
leftHoverLayerMask &= ~(1 << leftHand.gameObject.layer); //ignore self for hovering
// allocate array for colliders
rightOverlappingColliders = new Collider[32];
leftOverlappingColliders = new Collider[32];
foreach (Hand hand in Player.instance.hands)
{
hand.HideController();
}
}
void SetRightGrab(SteamVR_Action_Boolean fromAction, SteamVR_Input_Sources fromSource, bool newState)
{
float scaledHoverRadius = 0.075f * Mathf.Abs(SteamVR_Utils.GetLossyScale(rightHand.transform));
float closestDistance = float.MaxValue;
Scaleable closestInteractable = null;
if (rightHand.mainRenderModel != null)
CheckHoveringForTransform(rightHand, rightOverlappingColliders, rightHand.mainRenderModel.GetBonePosition((int)rightHand.fingerJointHover), ref closestDistance, ref closestInteractable, Color.blue);
if (this.Equals(closestInteractable))
{
if (newState)
{
wasHovering = hovering;
hovering = false;
GrabHintOff(rightHand);
}
else
{
wasHovering = hovering;
hovering = true;
if (isResizing)
{
stoppingResize = true;
isResizing = false;
EndScale(rightHand, leftGrabbing);
}
}
rightGrabbing = newState;
SetResizing(rightHand);
SetPickup(newState, rightHand);
}
}
void SetLeftGrab(SteamVR_Action_Boolean fromAction, SteamVR_Input_Sources fromSource, bool newState)
{
float scaledHoverRadius = 0.075f * Mathf.Abs(SteamVR_Utils.GetLossyScale(leftHand.transform));
float closestDistance = float.MaxValue;
Scaleable closestInteractable = null;
if (leftHand.mainRenderModel != null)
CheckHoveringForTransform(leftHand, leftOverlappingColliders, leftHand.mainRenderModel.GetBonePosition((int)leftHand.fingerJointHover), ref closestDistance, ref closestInteractable, Color.blue);
if (this.Equals(closestInteractable))
{
if (newState)
{
wasHovering = hovering;
hovering = false;
GrabHintOff(leftHand);
}
else
{
wasHovering = hovering;
hovering = true;
if (isResizing)
{
stoppingResize = true;
isResizing = false;
EndScale(leftHand, rightGrabbing);
}
}
leftGrabbing = newState;
SetResizing(leftHand);
SetPickup(newState, leftHand);
}
}
void EndScale(Hand hand, bool grabbing)
{
transform.SetParent(null);
if (grabbing)
{
transform.SetParent(hand.otherHand.transform);
}
}
// Update is called once per frame
void Update()
{
float rightClosestDistance = float.MaxValue;
Scaleable rightClosestInteractable = null;
if (rightHand.mainRenderModel != null)
CheckHoveringForTransform(rightHand, rightOverlappingColliders, rightHand.mainRenderModel.GetBonePosition((int)rightHand.fingerJointHover), ref rightClosestDistance, ref rightClosestInteractable, Color.blue);
float leftClosestDistance = float.MaxValue;
Scaleable leftClosestInteractable = null;
if (leftHand.mainRenderModel != null)
CheckHoveringForTransform(leftHand, leftOverlappingColliders, leftHand.mainRenderModel.GetBonePosition((int)leftHand.fingerJointHover), ref leftClosestDistance, ref leftClosestInteractable, Color.blue);
if (this.Equals(leftClosestInteractable) || this.Equals(rightClosestInteractable))
{
if (this.Equals(leftClosestInteractable))
{
if (transform.parent == null)
{
GrabHintOn(leftHand, "Grab");
}
else if (transform.parent.Equals(rightHand.transform))
{
GrabHintOn(leftHand, "Scale");
}
}
else
{
GrabHintOff(leftHand);
}
if (this.Equals(rightClosestInteractable))
{
if (transform.parent == null)
{
GrabHintOn(rightHand, "Grab");
}
else if (transform.parent.Equals(leftHand.transform))
{
GrabHintOn(rightHand, "Scale");
}
}
else
{
GrabHintOff(rightHand);
}
wasHovering = hovering;
hovering = true;
}
else
{
wasHovering = hovering;
hovering = false;
GrabHintOff(rightHand);
GrabHintOff(leftHand);
}
if (hovering && !wasHovering)
{
CreateHighlightRenderers();
}
else if (!hovering || !wasHovering)
{
UnHighlight();
}
UpdateHighlightRenderers();
if (isResizing)
{
SetScale();
}
foreach (MeshRenderer r in flashingRenderers)
{
r.material.SetColor("_EmissionColor", Color.Lerp(Color.black, hintColor, Util.RemapNumberClamped(Mathf.Cos((Time.realtimeSinceStartup) * Mathf.PI * 2.0f), -1.0f, 1.0f, 0.0f, 1.0f)));
r.material.SetFloat("_EmissionScaleUI", Mathf.Lerp(0.0f, 10.0f, Util.RemapNumberClamped(Mathf.Cos((Time.realtimeSinceStartup) * Mathf.PI * 2.0f), -1.0f, 1.0f, 0.0f, 1.0f)));
}
if (rightTextHint != null)
{
rightTextHint.transform.LookAt(Camera.main.transform);
}
if (leftTextHint != null)
{
leftTextHint.transform.LookAt(Camera.main.transform);
}
}
private void GrabHintOn(Hand hand, string text)
{
hand.ShowController();
hand.HideSkeleton();
// hand.GetComponent<HandPhysics>().enabled = false;
SteamVR_RenderModel model = hand.GetComponentInChildren<SteamVR_RenderModel>();
if (model != null)
{
string gripName = grabBoolean.GetRenderModelComponentName(hand.handType);
Dictionary<string, Transform> componentTransformMap = new Dictionary<string, Transform>();
for (int childIndex = 0; childIndex < model.transform.childCount; childIndex++)
{
Transform child = model.transform.GetChild(childIndex);
if (!componentTransformMap.ContainsKey(child.name))
{
componentTransformMap.Add(child.name, child);
}
}
Transform buttonTransform = componentTransformMap[gripName];
if (hand.Equals(rightHand))
{
if (rightTextHint == null)
{
rightTextHint = GameObject.Instantiate(grabHintPrefab, buttonTransform.position, buttonTransform.rotation);
rightTextHint.transform.SetParent(buttonTransform);
rightTextHint.transform.localPosition += new Vector3(-0.05349f, 0.01587f, -0.16261f);
}
rightTextHint.GetComponent<HintText>().text.text = text;
}
else
{
if (leftTextHint == null)
{
leftTextHint = GameObject.Instantiate(grabHintPrefab, buttonTransform.position, buttonTransform.rotation);
leftTextHint.transform.SetParent(buttonTransform);
leftTextHint.transform.localPosition += new Vector3(0.05349f, -0.01587f, -0.16261f);
}
leftTextHint.GetComponent<HintText>().text.text = text;
}
foreach (MeshRenderer r in buttonTransform.GetComponentsInChildren<MeshRenderer>())
{
if (!flashingRenderers.Contains(r))
flashingRenderers.Add(r);
r.material.EnableKeyword("_EMISSION");
}
}
}
private void GrabHintOff(Hand hand)
{
if (flashingRenderers.Count > 0)
{
SteamVR_RenderModel model = hand.GetComponentInChildren<SteamVR_RenderModel>();
if (model != null)
{
string gripName = grabBoolean.GetRenderModelComponentName(hand.handType);
Debug.Log($"gripName: {gripName}");
Dictionary<string, Transform> componentTransformMap = new Dictionary<string, Transform>();
for (int childIndex = 0; childIndex < model.transform.childCount; childIndex++)
{
Transform child = model.transform.GetChild(childIndex);
if (!componentTransformMap.ContainsKey(child.name))
{
componentTransformMap.Add(child.name, child);
}
}
Transform buttonTransform = componentTransformMap[gripName];
foreach (MeshRenderer r in buttonTransform.GetComponentsInChildren<MeshRenderer>())
{
flashingRenderers.Remove(r);
r.material.DisableKeyword("_EMISSION");
}
}
if (hand.Equals(rightHand) && rightTextHint != null)
{
Destroy(rightTextHint);
}
else if (hand.Equals(leftHand) && leftTextHint != null)
{
Destroy(leftTextHint);
}
}
if (flashingRenderers.Count == 0)
{
hand.HideController();
hand.ShowSkeleton();
// hand.GetComponent<HandPhysics>().enabled = true;
}
}
private void UnHighlight()
{
Destroy(highlightHolder);
GrabHintOff(rightHand);
GrabHintOff(leftHand);
}
void SetResizing(Hand mainHand)
{
if (leftGrabbing && rightGrabbing)
{
isResizing = true;
attachedToHand = true;
Debug.Log($"attached to {mainHand.handType}");
UnHighlight();
initialDistance = Vector3.Distance(mainHand.transform.position, mainHand.otherHand.transform.position);
middleMan = new GameObject();
Transform midpoint = middleMan.transform;
midpoint.position = (mainHand.otherHand.transform.position + mainHand.transform.position) / 2;
midpoint.rotation = FindRot(mainHand.transform, mainHand.otherHand.transform);
currentMain = mainHand;
transform.SetParent(midpoint);
midpoint.SetParent(null);
offsetPos = transform.localPosition;
}
}
void SetScale()
{
Vector3 mainPos = currentMain.transform.position;
Vector3 otherPos = currentMain.otherHand.transform.position;
float scale = Vector3.Distance(mainPos, otherPos) / initialDistance;
middleMan.transform.localScale = new Vector3(scale, scale, scale);
middleMan.transform.rotation = FindRot(currentMain.transform, currentMain.otherHand.transform);
middleMan.transform.position = (mainPos + otherPos) / 2;
}
public void ScaleAround(GameObject target, Vector3 pivot, Vector3 newScale)
{
Vector3 A = target.transform.localPosition;
Vector3 B = pivot;
Vector3 C = A - B; // diff from object pivot to desired pivot/origin
float RS = newScale.x / target.transform.localScale.x; // relataive scale factor
// calc final position post-scale
Vector3 FP = B + C * RS;
// finally, actually perform the scale/translation
target.transform.localScale = newScale;
target.transform.localPosition = FP;
}
//find rotation between two points to add to initialrot
private Quaternion FindRot(Transform t1, Transform t2)
{
Quaternion rot1 = t1.rotation;
Quaternion rot2 = t2.rotation;
Vector3 pos1 = t1.position;
Vector3 pos2 = t2.position;
Vector3 axis1to2 = (pos2 - pos1);
Vector3 up1 = t1.up;
Vector3 up2 = t2.up;
Vector3 averageUp = (up1 + up2) / 2;
Vector3 forward = Vector3.Cross(averageUp, axis1to2);
Vector3 finalUp = Vector3.Cross(forward, axis1to2);
Quaternion rot = Quaternion.LookRotation(forward, finalUp);
return rot;
}
void SetPickup(bool newState, Hand hand)
{
if (isResizing || stoppingResize)
{
if (stoppingResize)
stoppingResize = false;
return;
}
else if (newState)
{
transform.SetParent(hand.transform);
attachedToHand = true;
UnHighlight();
return;
}
else
{
transform.SetParent(null);
attachedToHand = false;
UnHighlight();
CreateHighlightRenderers();
return;
}
}
protected virtual bool CheckHoveringForTransform(Hand hand, Collider[] overlappingColliders, Vector3 hoverPosition, ref float closestDistance, ref Scaleable closestInteractable, Color debugColor)
{
bool foundCloser = false;
// null out old vals
for (int i = 0; i < overlappingColliders.Length; ++i)
{
overlappingColliders[i] = null;
}
int numColliding = Physics.OverlapSphereNonAlloc(hoverPosition, hand.controllerHoverRadius, overlappingColliders, hand.hoverLayerMask.value);
if (numColliding >= 32)
Debug.LogWarning("<b>[SteamVR Interaction]</b> This hand is overlapping the max number of colliders: " + 32 + ". Some collisions may be missed. Increase 32 on Hand.cs");
// DebugVar
int iActualColliderCount = 0;
// Pick the closest hovering
for (int colliderIndex = 0; colliderIndex < overlappingColliders.Length; colliderIndex++)
{
Collider collider = overlappingColliders[colliderIndex];
if (collider == null)
continue;
Scaleable contacting = collider.GetComponentInParent<Scaleable>();
// Yeah, it's null, skip
if (contacting == null)
continue;
// Ignore this collider for hovering
IgnoreHovering ignore = collider.GetComponent<IgnoreHovering>();
if (ignore != null)
{
if (ignore.onlyIgnoreHand == null || ignore.onlyIgnoreHand == hand)
{
continue;
}
}
// Can't hover over the object if it's attached
bool hoveringOverAttached = false;
for (int attachedIndex = 0; attachedIndex < hand.AttachedObjects.Count; attachedIndex++)
{
if (hand.AttachedObjects[attachedIndex].attachedObject == contacting.gameObject)
{
hoveringOverAttached = true;
break;
}
}
if (hoveringOverAttached)
continue;
// Best candidate so far...
float distance = Vector3.Distance(contacting.transform.position, hoverPosition);
//float distance = Vector3.Distance(collider.bounds.center, hoverPosition);
bool lowerPriority = false;
if (closestInteractable != null)
{ // compare to closest interactable to check priority
lowerPriority = contacting.hoverPriority < closestInteractable.hoverPriority;
}
bool isCloser = (distance < closestDistance);
if (isCloser && !lowerPriority)
{
closestDistance = distance;
closestInteractable = contacting;
foundCloser = true;
}
iActualColliderCount++;
}
if (iActualColliderCount > 0 && iActualColliderCount != prevOverlappingColliders)
{
prevOverlappingColliders = iActualColliderCount;
}
return foundCloser;
}
protected virtual void CreateHighlightRenderers()
{
existingSkinnedRenderers = this.GetComponentsInChildren<SkinnedMeshRenderer>(true);
if (highlightHolder == null)
highlightHolder = new GameObject("Highlighter");
highlightSkinnedRenderers = new SkinnedMeshRenderer[existingSkinnedRenderers.Length];
for (int skinnedIndex = 0; skinnedIndex < existingSkinnedRenderers.Length; skinnedIndex++)
{
SkinnedMeshRenderer existingSkinned = existingSkinnedRenderers[skinnedIndex];
if (ShouldIgnoreHighlight(existingSkinned))
continue;
GameObject newSkinnedHolder = new GameObject("SkinnedHolder");
newSkinnedHolder.transform.parent = highlightHolder.transform;
SkinnedMeshRenderer newSkinned = newSkinnedHolder.AddComponent<SkinnedMeshRenderer>();
Material[] materials = new Material[existingSkinned.sharedMaterials.Length];
for (int materialIndex = 0; materialIndex < materials.Length; materialIndex++)
{
materials[materialIndex] = highlightMat;
}
newSkinned.sharedMaterials = materials;
newSkinned.sharedMesh = existingSkinned.sharedMesh;
newSkinned.rootBone = existingSkinned.rootBone;
newSkinned.updateWhenOffscreen = existingSkinned.updateWhenOffscreen;
newSkinned.bones = existingSkinned.bones;
highlightSkinnedRenderers[skinnedIndex] = newSkinned;
}
MeshFilter[] existingFilters = this.GetComponentsInChildren<MeshFilter>(true);
existingRenderers = new MeshRenderer[existingFilters.Length];
highlightRenderers = new MeshRenderer[existingFilters.Length];
for (int filterIndex = 0; filterIndex < existingFilters.Length; filterIndex++)
{
MeshFilter existingFilter = existingFilters[filterIndex];
MeshRenderer existingRenderer = existingFilter.GetComponent<MeshRenderer>();
if (existingFilter == null || existingRenderer == null || ShouldIgnoreHighlight(existingFilter))
continue;
GameObject newFilterHolder = new GameObject("FilterHolder");
newFilterHolder.transform.parent = highlightHolder.transform;
MeshFilter newFilter = newFilterHolder.AddComponent<MeshFilter>();
newFilter.sharedMesh = existingFilter.sharedMesh;
MeshRenderer newRenderer = newFilterHolder.AddComponent<MeshRenderer>();
Material[] materials = new Material[existingRenderer.sharedMaterials.Length];
for (int materialIndex = 0; materialIndex < materials.Length; materialIndex++)
{
materials[materialIndex] = highlightMat;
}
newRenderer.sharedMaterials = materials;
highlightRenderers[filterIndex] = newRenderer;
existingRenderers[filterIndex] = existingRenderer;
}
}
protected virtual void UpdateHighlightRenderers()
{
if (highlightHolder == null)
return;
for (int skinnedIndex = 0; skinnedIndex < existingSkinnedRenderers.Length; skinnedIndex++)
{
SkinnedMeshRenderer existingSkinned = existingSkinnedRenderers[skinnedIndex];
SkinnedMeshRenderer highlightSkinned = highlightSkinnedRenderers[skinnedIndex];
if (existingSkinned != null && highlightSkinned != null && attachedToHand == false)
{
highlightSkinned.transform.position = existingSkinned.transform.position;
highlightSkinned.transform.rotation = existingSkinned.transform.rotation;
highlightSkinned.transform.localScale = existingSkinned.transform.lossyScale;
highlightSkinned.localBounds = existingSkinned.localBounds;
highlightSkinned.enabled = hovering && existingSkinned.enabled && existingSkinned.gameObject.activeInHierarchy;
int blendShapeCount = existingSkinned.sharedMesh.blendShapeCount;
for (int blendShapeIndex = 0; blendShapeIndex < blendShapeCount; blendShapeIndex++)
{
highlightSkinned.SetBlendShapeWeight(blendShapeIndex, existingSkinned.GetBlendShapeWeight(blendShapeIndex));
}
}
else if (highlightSkinned != null)
highlightSkinned.enabled = false;
}
for (int rendererIndex = 0; rendererIndex < highlightRenderers.Length; rendererIndex++)
{
MeshRenderer existingRenderer = existingRenderers[rendererIndex];
MeshRenderer highlightRenderer = highlightRenderers[rendererIndex];
if (existingRenderer != null && highlightRenderer != null && attachedToHand == false)
{
highlightRenderer.transform.position = existingRenderer.transform.position;
highlightRenderer.transform.rotation = existingRenderer.transform.rotation;
highlightRenderer.transform.localScale = existingRenderer.transform.lossyScale;
highlightRenderer.enabled = hovering && existingRenderer.enabled && existingRenderer.gameObject.activeInHierarchy;
}
else if (highlightRenderer != null)
{
highlightRenderer.enabled = false;
GrabHintOff(rightHand);
GrabHintOff(leftHand);
}
}
}
protected virtual bool ShouldIgnoreHighlight(Component component)
{
return ShouldIgnore(component.gameObject);
}
protected virtual bool ShouldIgnore(GameObject check)
{
for (int ignoreIndex = 0; ignoreIndex < hideHighlight.Length; ignoreIndex++)
{
if (check == hideHighlight[ignoreIndex])
return true;
}
return false;
}
}