この記事で紹介すること
この記事では以下を扱います。
- Raycastを用いた遮蔽判定の基本
- 障害物の厚さを考慮した減衰の実装
- dB変換やスムージングによる自然な聞こえ方の工夫
はじめに
この記事は3Dサウンドの基本的な実装をUnityのみで行ってみるシリーズの第一弾です。
昨今、サウンドはゲームにおいて軽視されがちですが、+αとして非常に重要な価値があると考えています。
3Dゲームでは考慮すべきことが多いため、サウンドミドルウェアが導入されてきました。
今回は基本的な仕組みの理解を目的として、サウンドミドルウェアを使わずにUnityの機能だけで実装しています。
今回のテーマ
今回は遮蔽を実装します。
Unityの標準機能では、距離減衰やリバーブなどが提供されています。
一方で、壁を検知してその障害物によって直接聞こえる音が防がれた時、つまり遮蔽による音の変化は実装されていません。
今回はRaycastを用いて、遮蔽を実装していきます。
今回の完成品
遮蔽の種類
日本語の遮蔽は英語では2つの意味を持っています。
- Obstruction
- 直接音が遮られている時
- Occlusion
- 部屋のような、完全に音が抜ける隙間がない時
実際のゲームではObstructionは柱の陰に回り込むような場面、Occlusionは隣の部屋に移動したような場面を想像すると分かりやすいかもしれません。
今回はObstruction寄りの遮蔽を実装していきます。
遮蔽の基本原理
基本的には音源からリスナーに対して、Raycastを飛ばし別のColliderに当たったら遮蔽されたことにする形です。
音の変化
音の変化には、音量の減衰とローパスフィルターをかけます。
ローパスフィルターとは、指定した周波数より高い音を削るフィルターです。
音波は波長が長い(=低い音)ほど障害物を回り込みやすい性質があります。
そのため、壁を通り抜ける音は周波数帯によって異なります。具体的には音が壁を通り抜ける時、低音の方がより通り抜けやすく、高音はより減衰する傾向があります。
これを再現するためにローパスフィルターを使い、高音を除外していくのです。
また、低音も一定減衰するため、音量も調節することで全体に篭っているエフェクトをかけることができます。
より自然な遮蔽を実現する
さて、ここまで基本的な考え方を見てきました。
ここからはより自然な遮蔽を実現するためいくつかの方法を紹介していきます。
障害物の厚さ推定
音は障害物が厚くなればより音が減衰するはずです。
そのため、遮蔽しているオブジェクトがどのくらいの厚さか計算する方法を考えます。
方法としてはレイキャストを飛ばして、ぶつかった点から逆方向に十分に離れた距離からレイを飛ばす方法が考えられます。しかし、これでは1つのレイあたりに2倍の処理負荷がかかるため、頻度の高い遮蔽の処理で採用するのは現実的ではありません。
今回私が採用したのはコライダーの最小辺の長さを厚さとして扱う方式です。
例えば壁であれば高さ・横幅よりも厚さのほうが短いはずです。これを利用する方式です。
var localSize = box.size; // ローカル空間の寸法 (例: 5, 3, 0.2)
var scale = box.transform.lossyScale; // ワールドスケール (例: 1, 1, 1)
var worldSize = new Vector3(
localSize.x * Mathf.Abs(scale.x), // 5 * 1 = 5m
localSize.y * Mathf.Abs(scale.y), // 3 * 1 = 3m
localSize.z * Mathf.Abs(scale.z)); // 0.2 * 1 = 0.2m
var thickness = Mathf.Min(worldSize.x, Mathf.Min(worldSize.y, worldSize.z)); // これが厚さ
ワールドの構築方法や上下の判定など全てのシーンで綺麗に動作するとは限りませんが、それでもある程度厚さを考慮した実装が行えました。
実装上の工夫
これまではRaycast周りの工夫を紹介してきました。
一方で、様々な工夫を実際に適用するにも工夫が必要です。
dB → Unityの音量
今までの処理はdBベースでの計算でした。しかしUnityは0-1の線形的なVolumeで管理されており、適用には工夫が必要です。
今回は人間の感覚が対数ベースで考えられることをもとに以下の式で計算しました。
volumeScale = Mathf.Pow(10f, -dB / 20f);
突然のブツッに対応するための制限
ゲームでは移動速度が速く突然遮蔽に入ってしまうことがあります。
これでは突然音がなくなるような感覚になってしまい、不自然に聞こえてしまいます。
そのため、音量の急激な変化をなくすような制限を追加しています。
var maxChange = _model.maxDbChangePerSecond * Time.deltaTime;
var diff = Mathf.Clamp(_targetAttenuationDb - _currentAttenuationDb, -maxChange, maxChange);
_currentAttenuationDb += diff;
まとめ
以上で今回実装した遮蔽の解説は終わりです。
簡易的な実装ながら、考えることも多かった印象です。Occlusionにも挑戦してみたいですね。
CRI ADXやWwiseなど、サウンドミドルウェアを用いることでこれらをよりリッチにより簡単に実装することができます。特にWwiseはAk Surface ReflectorなどSpatial Audio周りのコンポーネントが揃っています。
そのため今回紹介した内容はWwiseを採用すればこれらをよりリッチかつ簡単に実現できます
次はもう少しミドルウェア単体では解決できない問題に向き合ってみようと思います。