Dynamic Hand Collision Audio with FMOD, Unity and Oculus Touch

In this tutorial I will show you how to create a “door knock” interaction in VR using your hands. We will modulate the volume of the knocking sound based on how hard we knock at the door. See an example of this interaction in this video:

Hand audio collision with FMOD and Unity example

Preparation in FMOD Studio

In FMOD Studio we create a new event called door_knock. We insert a pounding/knocking sound into the timeline and create a continuous local parameter called KnockForce with a range from 0 to 1. In the game parameter tab we automate the volume of the track to be very quiet/muted around the zero value and full volume at 1:

Door Knock Event in FMOD Studio
Door Knock Event in FMOD Studio

Alternatively, you could add different samples or even multi instruments inside the game parameter at different values instead of using the timeline. For the minimal scope of this tutorial placing just one instrument in the timeline is completely fine.

We add a Resonance Audio Source effect to the master track of the event:

Door Knock Resonance Audio Source effect
Door Knock Resonance Audio Source effect

Similarly to the volume automation, we can automate the Occlusion value of the Resonance Audio Source to give the sound a more low passed effect when knocking at a lighter speed:

Door Knock Resonance Audio Source occlusion automation
Door Knock Resonance Audio Source occlusion automation

As a final step, we set the cooldown of the event to about 140ms:

FMOD Event Master Cooldown
FMOD Event Master Cooldown

Build your banks and head over to Unity.

Preparation in Unity

I’m using the OVRPlayerController provided by the Oculus Integration. We locate the LeftHandAnchor and RightHandAnchor GameObjects inside that prefab:

OVRPlayerController GameObject
OVRPlayerController GameObject

We add a Capsule Collider with following values to both GameObjects and and check the Is Trigger checkbox:

OVRPlayerController Handanchors Capsule Collider
OVRPlayerController Handanchors Capsule Collider

The colliders will allow us to use OnTriggerStay() in our script that will be attached to the receiving GameObject. In fact, as a last step we need to create an Object that also will contain a collider and a kinematic Rigidbody component that will allow us to receive the collider triggers from our hands. I used a door that was already inside the Oculus Integration package, but you can surely test this with a simple cube if you wish. Create the object and add a Box Collider and a kinematic Rigidbody component to it:

Box Collider and Kinematic Rigidbody components
Box Collider and Kinematic Rigidbody components

This is it for the preparation work in Unity. We can finally write the script for the audio interaction!

C# code for a door knock interaction in Unity

Let’s create a new script called FistAudioCollision.cs and attach it to the door/cube GameObject created before.

We first declare the usual FMOD EventInstance, a string that will contain the FMOD Event and other helpful variables:

private FMOD.Studio.EventInstance instance;

[FMODUnity.EventRef]
public string fmodEvent;

[SerializeField]
private float minCollisionVolume = 0.1f;
[SerializeField]
private float maxCollisionVelocity = 5f;

private GameObject leftHand, rightHand;
private Transform leftHandPosition, rightHandPosition;

We will use the float variable minCollisionVolume to check if the force applied at the collision is below that value, if yes we will not play any sound. The GameObjects leftHand and rightHand will help us to check if we are entering the collider with one hand or another. The Transforms leftHandPosition and rightHandPosition will get us a more precise location of the sound.

After that we use properties to get other information about our hands. We want to only play a sound if the player is currently holding the controller in a “fist” position. Therefore we use OVRInput.Get() and return a bool variable to retrieve this info for each hand. In Oculus’s documentation about OVRInput we can read more about how to get information about button presses and touches for the touch controllers.

 private bool RightControllerFist
    {
        get
        {
            bool fist;
            if (OVRInput.Get(OVRInput.Button.PrimaryHandTrigger, OVRInput.Controller.RTouch)
                && OVRInput.Get(OVRInput.Touch.One, OVRInput.Controller.RTouch)
                && OVRInput.Get(OVRInput.Touch.Two, OVRInput.Controller.RTouch)
                ||
                OVRInput.Get(OVRInput.Button.PrimaryHandTrigger, OVRInput.Controller.RTouch)
                && OVRInput.Get(OVRInput.Touch.Two, OVRInput.Controller.RTouch)
                ||
                OVRInput.Get(OVRInput.Button.PrimaryHandTrigger, OVRInput.Controller.RTouch)
                && OVRInput.Get(OVRInput.Touch.PrimaryThumbstick, OVRInput.Controller.RTouch)
                )
            {
                fist = true;
            }
            else fist = false;
            return fist;
        }
    }

    private bool LeftControllerFist
    {
        get
        {
            bool fist;
            if (OVRInput.Get(OVRInput.Button.PrimaryHandTrigger, OVRInput.Controller.LTouch)
                && OVRInput.Get(OVRInput.Touch.One, OVRInput.Controller.LTouch)
                && OVRInput.Get(OVRInput.Touch.Two, OVRInput.Controller.LTouch)
                ||
                OVRInput.Get(OVRInput.Button.PrimaryHandTrigger, OVRInput.Controller.LTouch)
                && OVRInput.Get(OVRInput.Touch.Two, OVRInput.Controller.LTouch)
                 ||
                OVRInput.Get(OVRInput.Button.PrimaryHandTrigger, OVRInput.Controller.LTouch)
                && OVRInput.Get(OVRInput.Touch.PrimaryThumbstick, OVRInput.Controller.LTouch)
                )
            {
                fist = true;
            }
            else fist = false;
            return fist;
        }
    }

After that we get the local velocity and the magnitude of the velocity of each controller. To be able to access these values easily we also use properties:

 private Vector3 RightControllerVelocity
    {
        get
        {
            return OVRInput.GetLocalControllerVelocity(OVRInput.Controller.RTouch);
        }
    }

    private Vector3 LeftControllerVelocity
    {
        get
        {
            return OVRInput.GetLocalControllerVelocity(OVRInput.Controller.LTouch);
        }
    }

private float RightControllerSpeed
    {
        get
        {
            return OVRInput.GetLocalControllerVelocity(OVRInput.Controller.RTouch).magnitude;
        }
    }

    private float LeftControllerSpeed
    {
        get
        {
            return OVRInput.GetLocalControllerVelocity(OVRInput.Controller.LTouch).magnitude;
        }
    }

We calculate the “impact volume” by using a Cubic Ease-Out method that will return a more consistent float value that ranges between 0f to 1f depending on how hard we knock on the door. We will use that value to drive our KnockForce parameter:

   private float CalculateImpactVolume(float speed)
    {
        float volume;
        volume = Cubiceaseout(speed);
        return volume;
    }

    private float CubicEaseOut(float velocity, float startingValue = 0, float changeInValue = 1)
    {
        return changeInValue * ((velocity = velocity / maxCollisionVelocity - 1) * velocity * velocity + 1) + startingValue;
    }

Before we can finally write code for the OnTriggerStay() callback method. we reference the Hands GameObjects and Transforms in Unity’s Awake() method:

private void Awake() 
{
  leftHand = GameObject.Find("LeftHandAnchor");
  rightHand = GameObject.Find("RightHandAnchor");
  leftHandPosition = leftHand.transform;
  rightHandPosition = rightHand.transform;
}

In the OnTriggerStay() method we check if the GameObject entering the trigger is one of both hands and if the hand is in a fist position by checking if the boolean variable LeftControllerFist or RightControllerFist is true.

If that is true we call CalculateImpactVolume() and pass LeftControllerSpeed or RightControllerSpeed as a parameter. We assign the return value to a local float variable called volumeLeft or volumeRight. We quickly check if that value is less than the minCollisionVolume we declared at the beginning. If it’s not, we exit the method by using the return keyword.

In the final part of the OnTriggerStay() method we calculate the dot product between the touch controller velocity and the door direction:

 private void OnTriggerStay(Collider other)
    {
        if (other.gameObject == leftHand && LeftControllerFist)
        {

            float volumeLeft = CalculateImpactVolume(LeftControllerSpeed);

            if (volumeLeft < minCollisionVolume)
            {
                return;
            }

            float minimumCloseness = 0.5f;
            Vector3 doorDirection = gameObject.transform.position - leftHandPosition.position;
            if (Vector3.Dot(LeftControllerVelocity, doorDirection) > minimumCloseness)
            {
                Knock(leftHandPosition, volumeLeft);
            }
        }

        else if (other.gameObject == rightHand && RightControllerFist)
        {
            float volumeRight = CalculateImpactVolume(RightControllerSpeed);

            if (volumeRight < minCollisionVolume)
            {
                return;
            }

            float minimumCloseness = 0.5f; 
            Vector3 doorDirection = gameObject.transform.position - rightHandPosition.position; 
            if (Vector3.Dot(RightControllerVelocity, doorDirection) > minimumCloseness)
            {
                Knock(rightHandPosition, volumeRight);
            }
        }
    }

Why aren’t we simply firing the sounds in OnTriggerEnter() instead of going to such lenghts? The reason for this choice is that sometimes the colliders placed on the hands are still inside the trigger when we repeat the knock interaction. Especially when knocking at slower speeds, the collider will sometimes slightly remain in the trigger, preventing the play back of the sound. This solution was suggested to me by my friend Ryan Boyer.

We check if the result of the dot product is greater than a threshold we set at a float value of 0.5f. That corresponds to an angle of 60 degrees: If that is true, we call the method Knock() in which we create the FMOD instance, pass the Transforms leftHandPosition or rightHandPosition as parameters for FMOD’s set3dAttributes() method, set the FMOD parameter KnockForce to the volumeLeft or volumeRight variables, start and release the instance:

private void Knock(Transform handPosition, float volume)
    {
        instance = FMODUnity.RuntimeManager.CreateInstance(fmodEvent);
        instance.set3DAttributes(FMODUnity.RuntimeUtils.To3DAttributes(handPosition));
        SetParameter(instance, "KnockForce", volume);
        instance.start();
        instance.release();
    }

That’s it! Select the FMOD Event in Unity’s inspector, start the game and you should be able to hear the sound if you knock against the object you assigned the script to. Make sure your hands are in a fist position and listen to the change in volume when knocking lighter and harder.

↑ To the Top