らくとあいすの備忘録

twitter : lactoice251

Amebientの環境音

こんにちは。Amebient Advent Calendar 20日目の記事です。

もし、Amebientが何であるかがわからないという方は、先にVRChatで公開されているワールド と、phiさんの1日目の記事 を見て頂ければと思います。

今回は、Amebientの環境音ということで、環境音の音量制御について書いていこうと思います。

音量制御

雨や風の音といった環境音は、通常どこか特定の方向から鳴っていると感じられることはありません。 なので、楽器などに用いる3D-AudioSourceではなく、2D-AudioSourceで表現することが素直な実装と言えると思います。

しかし、方向性を持たない音だからといってどこででも一定の音量というわけではありません。 例えば、外では防風が吹き荒れていても、室内に入ればかなり音量は小さくなることは明らかです。 ドアなどで環境が隔てられている場合、このような音量の変化は単純な環境音の入れ替えで事が済む場合も多いかと思いますが、Amebientのマップは明確に区切られた領域というものが無く、全体が連続的に繋がっています。なので、環境音も連続的に変化させて周囲の環境と一致させていく必要があります。

Unityの2D設定のAudioSourceでは、位置に応じて音量を変化させるプロパティは備わっていません。 そこで、AmebientではUdonを介してプレイヤーの頭の位置を取得し、頭の位置の関数として各AudioSourceのVolumeを制御するという方式を取っています。 これによって、建物の形状による音量変化や、水中・水上による音の変化、水深による音量の変化などを一括で扱うことが出来ます。

各環境音について

各環境音について、音量を制御する関数の詳細について書いていきます。 以下では、次図のような座標変数を用いて説明をします。 f:id:Raku_Phys:20201220213951p:plain

雨音は、室内では小さく屋外に近づくほど大きくなると考えられます。 また、1Fから4Fにわたってフロアに占める屋根のある領域の面積が小さくなっていくため、より上のフロアでは室内から屋外に (z方向に) 進んでいったより急に雨音が大きくなると想定出来ます。 これらの条件を満たす、音量の関数として次のようなものを用いました。

private float RainVolume(float y, float z)
    {
        float volume = 0.0f;
        float u = -1.0f * (y - waterY);
        volume = (Sigmoid(z, 0.004f * Mathf.Pow((y - deathY), 2.0f), -2.0f - y * 0.5f) + 0.08f) / 1.08f;
        if (y < waterY)
        {
            volume *= 0.5f * Mathf.Exp(-1.5f * u) + 0.5f * Mathf.Exp(-5.0f * u);
        }
        return volume * rainAmp;
    }

音量のz方向依存性ですが、主に次のように表されるSigmoidと呼ばれる関数で決定されています。

private float Sigmoid(float x, float f, float o)
    {
        return 1.0f / (1.0f + Mathf.Exp(-f * (x - o)));
    }

Sigmoid関数は、次のような見た目をした0から1までの値を取る関数で、fは変化の「急さ」、oは「変化地点」を表す量です。

f:id:Raku_Phys:20201219020952p:plain
(f,o) = (5,2) の場合のSigmoid関数
これを踏まえて、音量のz方向依存性の式を見ると、yが大きくなるにつれてSigmoidの「急さ」が大きくなり「変化地点」が建物側に寄っていきます。これによって、1Fでは外側に向かってなだらかに雨音が大きくなるのに対して、4Fに行くと建物を出た途端に雨音が大きくなるようになっています。

if文内は水中内の処理です。水中では雨音が急速に指数減衰するようになっています。 ただし、急に減衰する成分と、比較的なだらかに減衰する成分の重ね合わせになっています。これは、水中に入る時の急激な変化と水中でもかすかに聴こえている雨音を共存させるためです。特に物理的な根拠のある式ではなく、そういう風に聴こえたかったという感覚的実装です。

風音は、基本的に雨音と同じ音量変化を持ちます。 雨音の方にはないstateによる分岐は、雷が落ちた後に風が強くなる演出のためのものです。

private float WindVolume(float y, float z)
    {
        float volume = 0.0f;
        float u = -1.0f * (y - waterY);
        volume = (Sigmoid(z, 0.004f * Mathf.Pow((y - deathY), 2.0f), -2.0f - y * 0.5f) + 0.1f) / 1.1f;
        if (y < waterY)
        {
            volume *= Mathf.Exp(-4.0f * u);
        }        
        if (state == 2)
        {
            volume *= windAmp2;
        }
        else
        {
            volume *= windAmp1;
        }
        return volume;
    }

室内

室内では、減衰によって高音成分が失われた雨風の音と、室内に反響する水が垂れる音などから出来た環境音が鳴っているということにしました。音量は、雨風の音量の逆として実装されていて、volume = 1.0 - [雨風の音量] となっています。

private float BuildingVolume(float y, float z)
    {
        float volume = 0.0f;
        float u = -1.0f * (y - waterY);
        volume = 1.0f - Sigmoid(z, 0.004f * Mathf.Pow((y - deathY), 2.0f), -2.0f - y * 0.5f);
        if (y < waterY)
        {
            volume *= Mathf.Exp(-0.5f * u);
        }
        return volume * buildingAmp;
    }

海中 (泡音)

海中は、泡のような低音で満たされていて、次のような関数で音量が制御されています。

private float BubbleVolume(float y)
    {
        float volume = 0.0f;
        float u = -1.0f * (y - waterY);
        float u2 = -1.0f * (y - deathY);

        if (y < waterY)
        {
            volume = 0.7f + 0.3f * Mathf.Exp(-2.0f * u);
            if (y < deathY)
            {
                volume *= Mathf.Exp(-1.0f * u2);
            }
        }
        else
        {
            volume *= Mathf.Exp(-5.0f * outWaterTime);
        }

        return volume * bubbleAmp;
    }

泡音は、水中に入った瞬間不連続に大きくなり、その後少し小さな音量まで指数減衰します: volume = 0.7f + 0.3f * Mathf.Exp(-2.0f * u)。 これによって、水中に潜った瞬間のみ少し音が大きくなることで、その瞬間が音的に強調されます。 if (y<deathY)の分岐は、水中に落ちていってリスポーンする直前に音量を急激に減衰させる処理です。 最後のelse 内の処理は、水を出た直後に音量を急激に減衰させる処理で、outWaterTimeは水を出てからの時刻を数えています。 ここで、volume = 0.0fとしても効果としてはおおよそ同じになるのですが、不連続に音を切ってしまうとノイズが乗ってしまうためこのように実装しています。

海中 (Ambient)

海中に深く潜っていくと段々とAmbient的な音楽が聴こえてきます。 この音量はSigmoidで主に制御されていて、穏やかに音量が大きくなっていきます。 これによって、海の底の方から音が聴こえてきているような効果を意図しています。

private float AmbientVolume(float y)
    {
        float volume = 0.0f;
        float u = -1.0f * (y - waterY);
        float u2 = -1.0f * (y - deathY);

        if (y < waterY)
        {
            volume = Sigmoid(0.3f * u, 1.0f, 2.0f) - 0.12f;
        }
        if (y < deathY)
        {
            volume *= Mathf.Exp(-1.0f * u2);
        }
        return volume * ambientAmp;
    }

リズム同期する環境音

Amebientでは、上記のような定常的な環境音の他に周期的に鳴るような環境音が合わせて用いられています。 一度目の雷が落ちた後、64拍に一度すきま風が吹き、その後小さ目の雷が落ちます。 いわゆる「一般的な楽曲」では、ドラムによるクラッシュシンバルなど展開の切れ目で仕切り直しとなるような音が演奏されます。 Amebientでは、プレイヤーが自由に音を作っているだけなので明確な切れ目となるようなものが存在しませんが、定期的にやってくる雷とその予兆となるすきま風によって、適度な切り替え地点が生まれるようになっています。

おわりに

めずらしくコードが多めの記事になりましたね...。 とはいえ、書きたいことはコードの内容というよりはどんな気持ちで音量の線を引いたかという部分かなと思います。 VRCSDK2だとちょっと難しかったですが、Udonによって2D音源でも領域ごとの詳細な音量等々のパラメータ操作が出来るようになったので、色々な世界づくりや演出に活用していけそうですね。 次回は、最後になりますが、Amebientに使用した自作サンプルについてそのレシピ的なものを書いていきたいと思います。