【Unity】MaterialのRenderQueueが2000に戻る罠

2024/12/28に公開

はじめに

Transparentシェーダー("Queue"="Transparent")を使用したマテリアルの
RenderQueueが2000になるという不可思議な現象に遭遇したので
こちらについてまとめてみようと思います。

int renderQueue = material.renderQueue;
material.shader = shader; 
material.renderQueue = renderQueue; // 2000になることがある

結論を先に述べると、以下の条件を満たすときにmaterial.renderQueueが2000になります。

  1. material.shader がエラーを起こしている
  2. マテリアルの CustomRenderQueue が -1 である

マテリアルのRenderQueue

マテリアルのRenderQueueは、マテリアル内部でCustomRenderQueueという値として保存されています。
.matファイルをコードエディタで開いてみると、CustomRenderQueueを確認できます。

  m_CustomRenderQueue: -1

CustomRenderQueue の挙動は以下の通りです

  • CustomRenderQueue が -1 の場合、Shaderで定義されたRenderQueueが使用される
  • -1 以外の値の場合、マテリアルのRenderQueueとして使用される

Shader の RenderQueue

Shader上では、Queueというシェーダータグを指定することで、RenderQueueを定義できます。

Tags { "RenderType"="Opaque" "Queue"="Overlay" } 

参考 : https://docs.unity3d.com/ja/2019.4/Manual/SL-SubShaderTags.html

マテリアルのInspector上でFrom Shaderを選択した場合に、Shader上で定義されたRenderQueueが使用されます。

RenderQueueを直接入力

マテリアルのInspector上で、Render Queueの値を入力した場合、その数値がRenderQueueとして使用されます。

.matファイルをコードエディタで開いてみると、CustomRenderQueueに数値が格納されていることを確認できます。

  m_CustomRenderQueue: 3000

ハマりポイント1 : Shaderを変更するとRenderQueueはリセットされる

C#コード側で、マテリアルのShaderを差し替えた時、CustomRenderQueue は -1 にリセットされます。

By default materials use render queue of the shader it uses. You can override the render queue used using this variable. Note that if a shader on the material is changed, the render queue resets to that of the shader itself.

https://docs.unity3d.com/ja/2022.3/ScriptReference/Material-renderQueue.html

_material.renderQueue = 3500;
Debug.Log(_material.renderQueue); // 3500 (CustomRenderQueue = 3500)
_material.shader = _shader;
Debug.Log(_material.renderQueue); // 3000 (CustomRenderQueue = -1)

シェーダーを差し替えたときに、元のRenderQueueの値を保持させたい場合、
Shader差し替え前のRenderQueueを保持しておいて、Shader差し替え後に復元するような実装をすると良さそうです。

void ReplaceShader(Material material, Shader shader)
{
  // マテリアルに元から保持されているRenderQueueを保持
  int renderQueue = material.renderQueue; 

  // マテリアルのシェーダーを差し替える (CustomRenderQueue = -1 にリセットされる)
  material.shader = shader;

  // RenderQueueを元に戻す
  material.renderQueue = renderQueue; 
}

しかし、この実装を行った場合、特定のケースでRenderQueueが意図しない値になることがあります。その理由は後述します。

ハマりポイント2 : FallbackErrorシェーダーはRenderQueue=2000

マテリアルが参照するシェーダーがエラーになっている場合、
レンダリングに InternalErrorShaderシェーダーが使用されます。
そして、このシェーダーのRenderQueue は 2000 です。

先ほど、RenderQueueがリセットされる現象を回避するための実装を紹介しました。

void ReplaceShader(Material material, Shader shader)
{
  // マテリアルに元から保持されているRenderQueueを保持
  int renderQueue = material.renderQueue; 

  // マテリアルのシェーダーを差し替える (CustomRenderQueue = -1 にリセットされる)
  material.shader = shader;

  // RenderQueueを元に戻す
  material.renderQueue = renderQueue; 
}

この実装は、以下のケースで問題が起きます。

  1. material.shaderがエラーを起こしている
  2. エラーを起こす前のmaterial.shaderのRenderQueueが2000以外
  3. マテリアルのCustomRenderQueue = -1
  4. マテリアルに最初に設定されていたRenderQueueを、shader差し替え後に適用したい場合

C#コードのコメントに補足を加えると、以下のような感じでしょうか。

void ReplaceShader(Material material, Shader shader)
{
   // 2000 (CustomRenderQueue=-1、かつシェーダーがエラーとなっている場合)
   int renderQueue = material.renderQueue;

   // マテリアルのシェーダーを差し替える (CustomRenderQueue = -1 にリセットされる)
   material.shader = shader; 

   // renderQueueが2000になる (元のmaterial.shaderのRenderQueueが2000以外でも問答無用で2000になる)
   material.renderQueue = renderQueue; 
}

マテリアルに最初に適用されていたshaderのRenderQueueが2000以外でも、
上記のメソッドを実行した結果RenderQueueが2000に戻るという現象が発生します。

こちらの問題を回避するためには、material.rawRenderQueueを使用すると良いでしょう。

rawRenderQueue

material.rawRenderQueueを使用すると、マテリアルの CustomRenderQueue の値を直接取り出すことができます。

Unity6000

Unity6000では、rawRenderQueueはpublicなAPIとして公開されているため、簡単に利用できます。
https://docs.unity3d.com/6000.0/Documentation/ScriptReference/Material-rawRenderQueue.html

void ReplaceShader(Material material, Shader shader)
{
    // CustomRenderQueueを保持しておく
    int rawRenderQueue = material.rawRenderQueue; 

    // マテリアルのシェーダーを差し替える (CustomRenderQueue = -1 にリセットされる)
    material.shader = shader;

    // RenderQueueを復元
    material.renderQueue = (rawRenderQueue == -1) 
      ? shader.renderQueue 
      : rawRenderQueue;
}

Unity6000以前

Unity6000以前ではmaterial.rawRenderQueueはinternalなAPIとして定義されているため、
利用するためにはリフレクションを使用する必要があります。

internal extern int rawRenderQueue { [NativeName("GetCustomRenderQueue"), MethodImpl(MethodImplOptions.InternalCall)] get; }
void ReplaceShader(Material material, Shader shader)
{
    // CustomRenderQueueを保持しておく
    int rawRenderQueue = (int)typeof(Material)
        .GetProperty("rawRenderQueue", BindingFlags.Instance | BindingFlags.NonPublic)
        .GetValue(material);

    // マテリアルのシェーダーを差し替える (CustomRenderQueue = -1 にリセットされる)
    material.shader = shader;

    // RenderQueueを復元
    material.renderQueue = (rawRenderQueue == -1) 
      ? shader.renderQueue 
      : rawRenderQueue;
}

関連

https://light11.hatenadiary.com/entry/2018/05/26/185954

Discussion