[Unity] シェーダをグローバル変数でまとめて制御、プレイモード終了時に元に戻す [ゲーム開発ログ 2020-11-11]

2020-11-15

描画範囲に現れるオブジェクトの違和感を減らす

遠くを見渡せる 3D のゲームを作るとき、全てのオブジェクトを律儀に描画すると重いので、 普通は遠くのオブジェクトを簡素な描画に置き換えるか、もしくはそもそも描画しないという選択がとられる。

今自分が作っているゲームは iOS / Android がターゲットなので、描画負荷には余計に気を遣う。 自分はシンプルに 「近くのオブジェクトしか描画しない」 という戦略を選んだ。

単に描画しないだけであれば Unity のカメラの Clipping Planes の Far の値に小さめの数字を入れればそれで終わりだが、 実際にやるとオブジェクトの出現と消失が不自然な見た目になる:


単にカメラの描画距離を短く設定した場合

そこでカメラ距離が描画範囲に近づいた時にディザリング疑似透過でフッと現れる感じのシェーダを書いた。 これがあると大分見た目がマシになる:


シェーダでフッと現れるようにした場合

ディザリングという技法自体はそんなに見栄えのよいものではないが、 実際にはもっと描画範囲を広くして、この効果がかかるのは遠くのオブジェクトだけになるので、 そんなに違和感のない見た目にできる。

ちなみに上記の動画で使っているシェーダのソースコードはこちら:

消えるカメラ距離のパラメータをまとめて指定したい

ここで、 「どの距離から消え始めて、どの距離で完全に消えるか」 というのをシェーダのパラメータにするわけだが、 そのパラメータの値は Scene 上のオブジェクト群の各種 Material に対して、まとめて同じ値を指定したくなる。

これをやるのに、各 Material のパラメータに手作業で同じ値を指定するのは面倒だ。 また、プログラムで動的に値を変えたくなった場合にも、変更処理のオーバーヘッドが大きくなってしまう。 どこか一箇所で指定して、全 Material にまとめて反映されてほしい。

実は Unity のシェーダには、そういうことをするための グローバル変数 を設定できる機構があった。

変数名を決めておいて、シェーダ上では普通のプロパティのように扱い、 実行時に C# スクリプトから以下のように値を指定してやればよい:

Shader.SetGlobalFloat("_Alto_Global_DitherCullFrom", 65f);
Shader.SetGlobalFloat("_Alto_Global_DitherCullTo", 70f);

めでたしめでたし。

グローバル変数の変更はプレイモード終了時も残る

が、実際にやってみると、グローバル変数の値の変更はエディタのプレイモードを終了しても残ることがわかった。 ちなみに、現在のグローバル変数の値の内容を確認する方法は今のところ僕にはわかっていない。 (そういうエディタ拡張を書くしかない?)

別に変更が残っていても良さそうだが、「一定の距離以降は透明にする」というシェーダの挙動が Scene View でも適用されるのは面倒だった。 Scene View ではカメラを引いてマップ全体を眺めるような作業を行うが、その時に透明になってしまうのだ。

理想的には 「ゲーム実行中は一定距離で描画を消すけど、実行が終了したらエディタ上ではそれを無効にする」 ということがしたい。プレイモード終了時に自動で先述のグローバル変数を書き換えることができればそれが実現できそうだ。

Unity エディタで、プレイモード終了時に処理を行う

Unity ならそういうこともできるだろうと思って調べてみたら、やはりできた。 EditorApplication クラスにそういう event が用意されていた。 以下のようなコードで、「Edit Mode に入るタイミングでシェーダのグローバル変数を更新」という処理ができた:

using UnityEditor;
using UnityEngine;

namespace Altotascal
{
    [InitializeOnLoad]
    public class EditorPlayModeEventHandler
    {
        static EditorPlayModeEventHandler()
        {
            EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
        }

        static void OnPlayModeStateChanged(PlayModeStateChange state)
        {
            switch (state)
            {
                case PlayModeStateChange.EnteredEditMode: OnEnteredEditMode(); break;
                // 他のイベントもハンドリングしたい場合はここに追記
            }
        }

        static void OnEnteredEditMode()
        {
            Debug.Log("Reset Shader Global Variables");
            Shader.SetGlobalFloat("_Alto_Global_DitherCullFrom", 0f);  // 0 なら処理しないようになってる
            Shader.SetGlobalFloat("_Alto_Global_DitherCullTo", 0f);
        }
    }
}

これで「実行時は一定のカメラ距離でフッと消えるけど、プレイモードを終了した時は Scene View で全部描画されている」 という状態にすることができた。

〜 Happy End 〜

おまけ:霧の世界

描画距離を極端に小さくすると、近くしか見えない霧の中の世界、といった表現ができる:

描画範囲が狭くなって稼いだぶんの負荷でポストエフェクトの Bloom をかけてやれば、 ディザリングの粗さも軽減できそうだ。