たつぷりの調査報告書

博士後期課程(理学)の学生が趣味でUnityやBlenderで遊ぶブログです。素人が独学で勉強した際の忘備録です。

UnityとMIDIで音ゲー:見た目の改良

こんにちは。たつぷりです。最近趣味に費やす時間があまりなったため、少し時間が空いてしまいましたが、一ヶ月ほど前に書いていたUnityでMIDIを使ってリズムゲームを作る企画の続きです。

前回までで、MIDI音源に合わせてノートのオブジェクトを生成して曲に合わせて動かすことをやりました。

tatsupuri.hatenablog.com

tatsupuri.hatenablog.com

今回は、見た目を変更しようと思います。これ自体は本質的にリズムゲームのシステムに関係するわけではないのですが、いつまでもしょうもない箱でテストしてるとこちらも気分がのらないので...

というわけで、今回はあまり手間をかけずに見た目を少しよくすることをやります。

目標

最初に作ったプロトタイプは下図のようなあまりにも味気ないので、この見た目を改良することを目標にする。

f:id:Tatsupuri:20201129164849p:plain:w500

今回の改良の結果下の図のようなものを作成したので、今回は下図のような見た目にする方法に限ってまとめることにする。

f:id:Tatsupuri:20201129164854p:plain:w500

このように絵を改良するために以下を行った。

  • ノートのオブジェクトの形状を変更

  • Post ProcessingでBloom効果を追加

  • ノートの消滅時(この段階ではまだ実装していないがバーの上でタップした時)にパーティクルを生成

前提として、このプロジェクトはURPで作成している。

オブジェクトの作成

当初Unityで作成される立方体をノートオブジェクトに使っていたが、あまり見栄えが良くないので少し丸みを帯びた形にする。今回は簡単にBlenderで丸みを帯びた立方体を作成し、FBXで出力した上でUnityに取り込んだ。

Blenderで立方体を作成

Bevel Modifier

Blenderで丸みを帯びた立方体を作成するのは非常に簡単である。まずBlenderを起動するとデフォルトで立方体が表示されているので、これを変形していく。丸みを持たせるにはBevel Modifierを利用すればよい。今回の目的においていじるパラメータは

  • Offset:どれほど深く切り落とすか

  • Segments:切り落とした部分をどれほど密に補完するか

の二つである。Bevel Modifierは切基本的には各辺を斜めに切り落として新しい面を作成する。実際Bevelは動詞としては「斜めに切る」という意味であることを考えるとこのModifierの意味するところは分かりやすい。今回用いるパラメーターのOffsetは、どれほど深いところまで切り落とすかを指定する。つまり辺を切り落とすことでどれほど大きな面を作成するかを制御するのである。一方のSegmentsは切り落とされて新しくできた面をどれほど密に補完するかを指定している。つまり切り落としただけではただ平坦な面ができるだけだが、そこをSegmentsの分だけ丸みを持たせるイメージである。

f:id:Tatsupuri:20201129164900p:plain

マテリアルの設定

BlenderのオブジェクトをUnityでインポートするときマテリアル名やマテリアルをどこに割り当てたかなどの情報を引き継ぐことができる。詳細な情報はUnity側でも改めて設定できる。逆に、Blenderで詳細に作りこんでもそのまま出力されない。表面などの詳細な情報を反映させるには、法線マップにベイクしてから出力するなどの方法があるが、今回は触れない。今後別の記事で紹介する。

マテリアル名や、大まかな設定はUnityにも引き継がれるのでBlenderで編集する段階である程度作っておくのが良い。ある程度作れたら、これをFBX形式で出力する。

f:id:Tatsupuri:20201129164915p:plain:w500

出力する際、カメラやライトも出力してしまうとUnityにもそれらが持ち込まれるので、必要なオブジェクトのみ出力するようにオプションを設定する。

個人的に簡単だと思うのは、必要なものだけ選択状態にしておいてSelected Objects にチェックを入れる方法である。 他にもObject Typesから必要なものを選ぶなどの方法がある。

f:id:Tatsupuri:20201129164921p:plain:w500

これでオブジェクトがFBX形式で保存されたのでUnityにインポートする準備ができた。

Unityにインポート

先ほど作成したFBXファイルをUnityにインポートする。インポートすると、以下のようにオブジェクトのメッシュとマテリアルがパックされている。下の図ではマテリアル名やメッシュの名前は、Blenderで設定したものが引き継がれる(ここではデフォルトのまま)。マテリアルを複数設定していれば、ここで複数のマテリアルが格納されている。

f:id:Tatsupuri:20201129164927p:plain:w500

このオブジェクトのインスペクターを見ると以下のようになっている。ここでExtract Materials を選択すると、オブジェクトにパックされているマテリアルを抽出することができる。実際に選択すると、抽出したマテリアルの保存先を聞かれるので、適当に他のマテリアルがあるディレクトリなどに保存する。

f:id:Tatsupuri:20201129164933p:plain:w500

マテリアルを抽出すると以下のように元のオブジェクトとマテリアルが分離する。この時自動的に、元のオブジェクトには抽出したマテリアルが割り当てられる。 複数マテリアルを設定していれば、ここで複数マテリアルが設定される。

f:id:Tatsupuri:20201129164938p:plain:w500

抽出したマテリアルは、当然インスペクターからいつも通り設定することができる。

f:id:Tatsupuri:20201129164943p:plain:w500

Post Processing

今回は、ポストプロセスを用いて見た目を改善する。といっても、今回はBloom効果を使うだけにとどめる。

Hierarchy ViewからシーンにGlobal Volumeを追加する。Global Volumeオブジェクトに対してインスペクターからAdd Override でPost Processing 効果を追加できる。今回はBloom効果を用いる。見栄えが良くなるように適当にパラメータを調節する。

f:id:Tatsupuri:20201129164948p:plain:w500

ここでゲームビューにPost Processing効果を適用するためには、メインカメラの設定で、Rendering>Post Processingをオンにしておく必要がある。

f:id:Tatsupuri:20201129164953p:plain:w500

Visual Effect Graph

グラフの構成

今回は以下のように非常にシンプルなグラフで十分である。 やりたいことは、 OnPlayメッセージを受け取ったら適当な量の粒子を生成し、 その後適当に収束させ、最終的に消滅する というだけのエフェクトである。

f:id:Tatsupuri:20201129164907p:plain:w500

Awake時に自動で再生しないようにする方法

デフォルトのままではVFXオブジェクトは生成時に自動で再生されてしまう。これを避けるために以下のように設定を行う。VFXオブジェクトのインスペクターから、Visual Effect > GeneralのInitial Event Nameチェックボックスをオンにして、かつその右側のフィールドを空白にする。

こうすることでオブジェクト生成時にOnPlayが送られることがなくなる。

f:id:Tatsupuri:20201129164910p:plain:w500

同じことは一応ここにも書いてた。

スクリプトから制御

基本的にVFXグラフで作成したエフェクトは、UnityEngine.Experimental.VFX に含まれるVisualEffect クラス(公式レファレンス)で制御することができる。

VisualEffectクラスにはSendEvent() メソッドが用意されている。 このメソッドの引数としてVFXグラフのEventブロックで定義したEvent名を渡すことで、VFXグラフ内でのそのEventからのフローを呼び出すことができる。

using UnityEngine;
using UnityEngine.VFX;
using UnityEngine.Experimental.VFX;

public class Note : MonoBehaviour
{

    private Transform VFXs;
    private VisualEffect VFX;

    void Start()
    {
        this.gameObject.GetComponent<MeshRenderer>().material = defaultMat;
        player = GameObject.FindGameObjectWithTag("Player");
        VFXs = GameObject.FindGameObjectWithTag("VFX").transform;
        VFX = VFXs.GetChild(lineNumber - 1).GetComponent<VisualEffect>();
    }

    public void Success()
    {
        VFX.SendEvent("OnPlay");
        Destroy(this.gameObject);
    }

}