Audio Occlusion with ADX2 and Unity
Introduction
Occlusion refers to the obstruction of a path between two points. This phenomenon can be calculated in games by drawing a line between two points and detecting whether this line gets broken by an object. This information can then be used to inform the engine to do something, like unloading 3D objects from the world to save on processing. In audio, we can use this information to mimic the effect that objects have on sound sources, by making them appear quieter when they are occluded.
Atom Craft
The first step in setting up our occlusion system is creating an AISAC in Atom Craft. I will be using the same project from the second part of our 3D audio series. We can set up a global AISAC Control and apply this to any sound which we would want to be affected by occlusion.
To do this, we can create an AISAC Control named “AISAC_Occlusion” and drawn in a linear graph for both Volume and Bandpass – Cutoff High from 1 to 0. In practical terms, this means that the graph indicates “no occlusion”, to “maximum occlusion”. This can then be applied to the “boxhum” Cue, and the project can subsequently be built.
Unity
Once we have a Cri Atom Source script attached to our sound source and a Cri Atom Listener script attached to the relevant camera, we can add the following script to our audio source to create a simple audio occlusion effect:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Cri3DOcclusion : MonoBehaviour
{
// Gameworld Variables
Transform listenerTransform;
public LayerMask OcclusionLayer = 1;
// Internal Variables
private CriAtomSource atomSource;
public float minOcclusion = 0.0f;
public float maxOcclusion = 0.75f;
public float transitionTime = 0.5f;
private float curOcclusion = 1.0f;
void Start()
{
listenerTransform = FindObjectOfType().transform;
atomSource = gameObject.GetComponent();
atomSource.Play();
}
void Update()
{
Physics.Linecast(transform.position, listenerTransform.position, out RaycastHit hit, OcclusionLayer);
if (hit.collider.tag == "Player")
{
Debug.DrawLine(transform.position, listenerTransform.position, Color.blue);
if (curOcclusion != minOcclusion)
StartCoroutine(MoveTo(minOcclusion));
}
else
{
Debug.DrawLine(transform.position, listenerTransform.position, Color.red);
if (curOcclusion != maxOcclusion)
StartCoroutine(MoveTo(maxOcclusion));
}
}
IEnumerator MoveTo(float targetOcclusion)
{
curOcclusion = Mathf.MoveTowards(curOcclusion, targetOcclusion, Time.deltaTime / transitionTime);
atomSource.SetAisacControl("AISAC_Occlusion", curOcclusion);
yield return null;
}
}
Variables
First, we have our variables. The LayerMask variable allows us to define what counts as obstruction. Setting this to “1” will give us the “Default” value, which will work fine for our purposes. You can set this to public to see what the other native values are if you want to find out more about this type.
Next up, we have float variables which are used to control our AISAC. These are set to public so that they can be tweaked in the Inspector as needed.
Setting the Max Occlusion to less than 1 means that the sound will still be slightly audible behind cover.
Start
In the Start function, we are defining our listener and sound source. We could expose the listenerTransform as a public variable and set the listener in the Inspector, but by using the FindObjectOfType method, our code becomes much more flexible and efficient, as we can simply drag this script onto any object, without needing to define the listener each time manually.
For the sound source, since it is going to be attached to the same object as our occlusion script, we can simply get the relevant CriAtomSource component.
Update
By using descriptive variable names, I’ve tried to make the Update function as legible as possible.
- First, we use the Linecast between the position of the sound source, and the position of the listener, which returns a RaycastHit (hit).
- We then see IF the line from the sound source ‘hits’ the “Player”, and IF the value of our occlusion level is NOT currently equal to the minimum value (the latter IF is an optimisation which prevents the program from updating the AISAC if it does not need to).
- Finally, we move from our current occlusion value to the minimum occlusion value.
The ELSE portion of this statement is just doing the reverse, which is, if the “Player” is NOT hit, then move to a state of maximum occlusion.
Within the IF statements, we can also call the Debug.DrawLine method and change its color to display the current occlusion state.
MoveTo
The MoveTo block is what is known as a Coroutine. This is essentially like an efficient function which works well for updating values over time. Here, we can MoveTowards our target value, from our current value, over some time. Since our transitionTime variable is public, we can tweak this speed to preference in the Inspector. Finally, the AISAC is updated to reflect the change.
Conclusion
In just 52 lines of code, and some minor setup in Atom Craft, we have created an efficient and reactive occlusion script. Of course, this is quite a simple implementation of this idea, but it could be expanded to check different surface types or optimised further to virtualise sounds which are occluded. Even concepts such as these would be relatively straight-forward with ADX2!