Unity5のStandardシェーダのパラメタをスクリプトからいじろうとして丸一日潰れた話

Unity遊びの記録を書かずにまごまごしていたらUnity5になってしまって大変申し訳ない気分でいっぱいです。

お詫びの印じゃないんですが今やっている別ネタで発生した小ネタについて書き残しておきます。誰か既にやってると思ったら誰も書いてない、という……

Unity5を触った方ならご存じかと思いますが、Unity5からはだいたいの材質は物理ベースの統合シェーダ一つで表現されるようになりました。

docs.unity3d.com

このパラメータの中には自己発光を表す「Emission」が含まれています。
Emissionの値をいじることでモデルが自ら光るような表現ができるわけですが、これを応用してSTGでの与ダメージ表現をやってみようと思ったわけです。

ざっくり実装手順を。

スクリプトからマテリアルのパラメータをGet/Setするために、まずパラメータの内部名称を調べます。調べたいシェーダーを指定しているマテリアルをインスペクタで開き、インスペクタ右上隅の歯車マークをクリック。「Select Shader」を選びます。

f:id:dnasoftwares:20150319032129p:plain

そうするとシェーダーの情報が出ます。

f:id:dnasoftwares:20150319032134p:plain

左の赤枠、「_」から始まっている文字列がパラメータの名前です。
同じ行に型と、インスペクタでの表示名が出ています。表示名を参考に、いじりたいパラメタはどの名称かを調べます。

ここではエミッションをいじりたいので_Emissionなんたらというのを探しますが、
どうやら_EmissionColorが探していたもののようです。

じゃあこの_EmissionColorなるパラメータをいじればよさそう、ということで下記のようなコードを試したのですが……

  //適当なMonoBehaviorの内側……
  Renderer r = GetComponent<Renderer>; //Rendererコンポーネントを取得(Material取得のため)
  r.material.SetColor("_EmissionColor", new Color(1,0,0); //とりあえずEmissionで真っ赤にしてみる

ところがこれだとうまくいきません。最初からEmissionが指定されているマテリアル以外では色がつかないのです。

これの理由がわからなくてオロオロしてたのですが、マニュアルやらシェーダーのソースやらを掘り返してようやく理由がわかりました。

docs.unity3d.com

キモの部分だけ一部引用

Often it is convenient to keep most of a piece of shader code fixed but also allow slightly different shader “variants” to be produced. This is commonly called “mega shaders” or “uber shaders”, and is achieved by compiling the shader code multiple times with different preprocessor directives for each case.

In Unity this can be achieved by adding a #pragma multi_compile or #pragma shader_feature directive to a shader snippet. At runtime, the appropriate shader variant is picked up from the Material keywords (Material.EnableKeyword and DisableKeyword) or global shader keywords (Shader.EnableKeyword and DisableKeyword).

全訳はしませんが、"#pragma multi_compile"あるいは"#pragma shader_feature"がC#でいういわゆる#defineみたいなもので、キーワードの有効・無効を切り替えることでシェーダーのコンパイルを分岐できるということのようです。んで、スクリプトからキーワードを有効、無効にする手段があってそれがMaterial.EnableKeywordMaterial.DisableKeywordだと。

シェーダーのソースはUnityのダウンロードページで「ADDITIONAL DOWNLOADS」から「Built in shaders」を選べば入手できます。

Standard.shaderの一部引用。

#pragma shader_feature _NORMALMAP
#pragma shader_feature _ _ALPHATEST_ON _ALPHABLEND_ON _ALPHAPREMULTIPLY_ON
#pragma shader_feature _EMISSION
#pragma shader_feature _METALLICGLOSSMAP 
#pragma shader_feature ___ _DETAIL_MULX2
#pragma shader_feature _PARALLAXMAP

「#pragma shader_feature _EMISSION」どうやらこのあたりらしい、ということで下記のように書き直してみたところ、期待通りに赤色に光ってくれました。

  //適当なMonoBehaviorの内側……
  Renderer r = GetComponent<Renderer>; //Rendererコンポーネントを取得(Material取得のため)
  r.material.EnableKeyword("_EMISSION");
  r.material.SetColor("_EmissionColor", new Color(1,0,0); //とりあえずEmissionで真っ赤にしてみる

例として1秒おきにEmissionを切り替える→元にもどす、を繰り返すコードを用意しました。
コルーチン使ってますけど別にいいよね?(1秒おきに云々とかUpdateで書くのめんどくさいし)

動く方のコード
using UnityEngine;
using System.Collections;

[RequireComponent(typeof(Renderer))]
public class MaterialChangeTester : MonoBehaviour
{
    private Renderer _renderer;

    // Use this for initialization
    void Start ()
    {
        _renderer = GetComponent<Renderer>();
        StartCoroutine(BlinkerCoroutine());
    }

    IEnumerator BlinkerCoroutine()
    {
        //こちらは動く例
        //変更前のマテリアルのコピーを保存
        var originalMaterial = new Material(_renderer.material);
        for (;;)
        {
            _renderer.material.EnableKeyword("_EMISSION"); //キーワードの有効化を忘れずに
            _renderer.material.SetColor("_EmissionColor", new Color(1, 0, 0)); //赤色に光らせる
            yield return new WaitForSeconds(1.0f); //1秒待って
            _renderer.material = originalMaterial; //元に戻す
            yield return new WaitForSeconds(1.0f); //また1秒待ってくりかえし
        }
    }
}
動かない方のコード
using UnityEngine;
using System.Collections;

[RequireComponent(typeof(Renderer))]
public class MaterialChangeTesterBadExample : MonoBehaviour
{
    private Renderer _renderer;

    // Use this for initialization
    void Start ()
    {
        _renderer = GetComponent<Renderer>();
        StartCoroutine(BlinkerCoroutine());
    }

    IEnumerator BlinkerCoroutine()
    {
        //こちらは悪い例(変化しない)
        //変更前のマテリアルのコピーを保存
        var originalMaterial = new Material(_renderer.material);
        for (;;)
        {
            _renderer.material.SetColor("_EmissionColor", new Color(1, 0, 0)); //赤色に光らせたい
            yield return new WaitForSeconds(1.0f); //1秒待って
            _renderer.material = originalMaterial; //元に戻す
            yield return new WaitForSeconds(1.0f); //また1秒待ってくりかえし
        }
    }
}

それぞれSphereに割り付けて、適当なStandardシェーダーをマテリアルにして実行したら下のような感じになります。
f:id:dnasoftwares:20150319095404g:plain
応用すればこんな表現に。(というかこれがやりたかった)

というわけで参考になれば幸いです。