We have a series of VG onboarding tasks to show how to tackle different practical use cases using VirtualGrasp in a VR application. In VirtualGrasp SDK, they are packed in VirtualGrasp\Scenes\onboarding.

Task Description

Interaction behaviors wanted

  • We want to close the water bottle with the cap screwed on.
  • Water bottle or cap can be either physical or non-physical, so can not use Unity’s physical joint support if they are not physical objects.

Tips for VR developers

Solution

In VirtualGrasp SDK, we packed the solution of this task in VirtualGrasp/Scenes/onboarding.

VirtualGrasp/Scenes/onboarding/VG_Onboarding.unity
//VirtualGrasp/Scenes/onboarding/Scripts/AssembleVGArticulation.cs:

using UnityEngine;
using VirtualGrasp;

/** 
 * AssembleVGArticulation shows as a tutorial on how to use the VG_Controller.ChangeObjectJoint function for
 * assemble and dissemble non-physical objects (objects without rigid body or articulation body).
 */
[LIBVIRTUALGRASP_UNITY_SCRIPT]
[HelpURL("https://docs.virtualgrasp.com/unity_vgonboarding_task5." + VG_Version.__VG_VERSION__ + ".html")]
public class AssembleVGArticulation : MonoBehaviour
{
    [Tooltip("When assemble, the new parent to assign to this object.")]
    public Transform m_newParent = null;
    [Tooltip("The target pose of the assembled object.")]
    public Transform m_desiredPose = null;
    [Tooltip("Threshold to assemble when object to Desired position is smaller than this value.")]
    public float m_assembleDistance = 0.05f;

    [Tooltip("Threshold to assemble when object to Desired angle is smaller than this value.")]
    public float m_assembleAngle = 45;

    [Tooltip("Threshold to disassemble when sensor hand position to grasped hand position is bigger than this value.")]
    public float m_disassembleDistance = 0.5f;
    [Tooltip("The VG Articulation of constrained (non-FLOATING) joint type to switch to when assemble an object.")]
    public VG_Articulation m_assembleArticulation = null;

    [Tooltip("If provided give disassemble sound effect.")]
    public AudioSource m_disassembleSoundEffect;
    [Tooltip("If provided give assemble sound effect.")]
    public AudioSource m_assembleSoundEffect;

    private float m_timeAtDisassemble = 0.0F;
    private float m_assembleDelay = 1.0F;

    void Start()
    {
        if (m_assembleArticulation == null)
            VG_Debug.LogError("Has to assign an Assemble Articulation on " + this.transform.name);
        if (m_assembleArticulation.m_type == VG_JointType.FLOATING)
            VG_Debug.LogError("Assemble Articulation should be of a constrained joint type, can not be FLOATING on " + this.transform.name);
    }

    void LateUpdate()
    {
        assembleByJointChange();
        dessembleByJointChange();
    }

    void assembleByJointChange()
    {
        VG_JointType jointType;
        Quaternion.FromToRotation(m_desiredPose.up, this.transform.up).ToAngleAxis(out float angle, out _);

        if ((Time.realtimeSinceStartup - m_timeAtDisassemble) > m_assembleDelay
           && (m_desiredPose.position - this.transform.position).magnitude < m_assembleDistance
           && (angle < m_assembleAngle)
           && VG_Controller.GetObjectJointType(this.transform, false, out jointType) == VG_ReturnCode.SUCCESS &&
           jointType == VG_JointType.FLOATING)
        {
            m_desiredPose.gameObject.SetActive(false);

            // Project object rotation axis to align to desired rotation axis.
            Quaternion q_raw = Quaternion.LookRotation(m_desiredPose.up, transform.forward);
            Quaternion q_nat = q_raw * Quaternion.Euler(0, 180, 0) * Quaternion.Euler(-90, 0, 0);
            this.transform.SetPositionAndRotation(m_desiredPose.position, q_nat);

            if (m_newParent != null)
                this.transform.SetParent(m_newParent);

            VG_ReturnCode ret = VG_Controller.ChangeObjectJoint(transform, m_assembleArticulation);
            if(ret != VG_ReturnCode.SUCCESS)
                VG_Debug.LogError("Failed to ChangeObjectJoint() on " + transform.name + " with return code " + ret);

            m_assembleSoundEffect?.Play();
        }
    }

    void dessembleByJointChange()
    {
        foreach (VG_HandStatus hand in VG_Controller.GetHands())
        {
            VG_JointType jointType;
            if (hand.m_selectedObject == transform && hand.IsHolding()
                && VG_Controller.GetObjectJointType(transform, false, out jointType) == VG_ReturnCode.SUCCESS
                && jointType != VG_JointType.FLOATING)
            {
                VG_Controller.GetSensorPose(hand.m_avatarID, hand.m_side, out Vector3 sensor_pos, out Quaternion sensor_rot);
                float jointState = 0.0f;
                if (jointType == VG_JointType.REVOLUTE)
                    VG_Controller.GetObjectJointState(transform, out jointState);
                if (jointState == 0.0f && (sensor_pos - hand.m_hand.position).magnitude > m_disassembleDistance)
                {
                    m_desiredPose.gameObject.SetActive(true);
                    transform.SetParent(m_newParent.parent);
                    VG_ReturnCode ret = VG_Controller.RecoverObjectJoint(transform);
                    if (ret != VG_ReturnCode.SUCCESS)
                        VG_Debug.LogError("Failed to RecoverObjectJoint() on " + transform.name + " with return code " + ret);

                    m_timeAtDisassemble = Time.realtimeSinceStartup;
                    m_disassembleSoundEffect?.Play();
                }
            }
        }
    }
}

is the script showing how to use API function ChangeObjectJoint and RecoverObjectJoint to attach and unattach the cap on to the bottle.