これは【unityプロ技】 Advent Calendar 2019の10日目の記事です。
はじめに
ひとつのシェーダーで複数の描画モード(例:ブレンドモード)に対応したいと思ったことはありませんか?
ブレンドモードには、不透明、加算合成、アルファブレンドなど様々な種類がありますが、
シェーダーを複製してブレンドモードだけを修正すると、似たようなシェーダーが乱立してしまい、実装コストも管理コストも増えてしまいます
本記事では、シェーダーのカスタムインスペクタを実装し、単一のシェーダーで様々な描画モードに切り替え可能にする方法をご紹介します。
完成したもの
描画モード切替に対応して、似たような #Unity3D シェーダーを1つにまとめる方法をQiitaに投稿しました!
— がむ (@gam0022) December 9, 2019
カスタムインスペクタを用いて、ブレンドモード、カリングモード、ステンシル操作をマテリアルのプロパティとして選択可能にしています。https://t.co/OPxNSyzuvP pic.twitter.com/7qsSAAvyRp
概要
基本的なしくみは非常にシンプルです。
- シェーダーに、BlendModeやZWriteのプロパティを用意する
- シェーダーのカスタムインスペクタで、BlendModeやZWriteをまとめて切り替えできるようにする
ちなみにUnityビルドインのStandardShaderでも同じような手法でブレンドモードの切り替えを実現しています。
StandardShaderは少々複雑ですので、今回はブレンドモードの切り替えに必要な部分だけ抽出して紹介します。
環境
- 2018.4.13f1 (LTS)
- Build-in Rendering Pipeline および LWRP(Light Weight Render Pipeline)
ソースコード全文
ソースコード全文とプロジェクトファイルはGitHubに公開しています。
GitHubおよび本記事に登場するソースコードはMIT Licenseです。
ソースコード解説
ここからは各処理について詳しく解説していきます。
シェーダー: General.shader
ブレンドモードのプロパティ化
まずはブレンドモード( Blend
)と深度値の書き込みの有無( ZWrite
)をプロパティ化します。
これらのプロパティはユーザには直接触らせず、カスタムインスペクタから自動設定させたいので、 [HideInInspector]
アトリビュートを指定をしてインスペクタ上からは非表示にしています。
_BlendMode
はカスタムインスペクタ用にブレンドモードの選択状態を保存するためのプロパティなので、Passの中では使いません。
Shader "Unlit/General"
{
Properties
{
+ // Blending state
+ [HideInInspector] _BlendMode ("__mode", Float) = 0.0
+ [HideInInspector] _SrcBlend ("__src", Float) = 1.0
+ [HideInInspector] _DstBlend ("__dst", Float) = 0.0
+ [HideInInspector] _ZWrite ("__zw", Float) = 1.0
// Cutoutの閾値
_Cutoff("Alpha Cutoff", Range(0.0, 1.0)) = 0.5
// メインテクスチャ
_MainTex ("Texture", 2D) = "white" {}
// 乗算カラー
[HDR] _TintColor("Tint Color", Color) = (1.0, 1.0, 1.0, 1.0)
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
+ Blend[_SrcBlend][_DstBlend]
+ ZWrite [_ZWrite]
Cull Back
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// make fog work
#pragma multi_compile_fog
カスタムインスペクタの指定
CustomEditor
でシェーダーのカスタムインスペクタを指定します。
}
Fallback "Unlit/Texture"
+ CustomEditor "GeneralShaderGUI"
}
カスタムインスペクタ: GeneralShaderGUI.cs
ブレンドモードの定義
まず BlendMode
というブレンドモードを定義するenumを定義します。
/// <summary>
/// ブレンドモード
/// </summary>
enum BlendMode
{
Opaque,
Cutout,
Transparent,
Additive,
Multiply,
}
ブレンドモードのポップアップの描画
DrawBlendMode
メソッドでブレンドモードのポップアップの描画します。
シェーダーで定義した _BlendMode
プロパティを BlendMode
の enum に変換して、
EditorGUILayout.Popup
でポップアップから選択できるようにします。
また、 BlendMode.Cutout
のときだけに _Cutoff
用のインスペクタを表示します。
/// <summary>
/// BlendModeのインスペクタを描画します
/// </summary>
/// <param name="materialEditor">MaterialEditor</param>
/// <param name="properties">Properties</param>
void DrawBlendMode(MaterialEditor materialEditor, MaterialProperty[] properties)
{
var blendMode = FindProperty("_BlendMode", properties);
var cutoff = FindProperty("_Cutoff", properties);
var mode = (BlendMode) blendMode.floatValue;
using (var scope = new EditorGUI.ChangeCheckScope())
{
mode = (BlendMode) EditorGUILayout.Popup("Blend Mode", (int) mode, Enum.GetNames(typeof(BlendMode)));
if (scope.changed)
{
blendMode.floatValue = (float) mode;
foreach (UnityEngine.Object obj in blendMode.targets)
{
ApplyBlendMode(obj as Material, mode);
}
}
}
if (mode == BlendMode.Cutout)
{
materialEditor.ShaderProperty(cutoff, cutoff.displayName);
}
}
ブレンドモードに応じて各種プロパティを設定
ApplyBlendMode
メソッドが今回のカスタムインスペクタの 一番重要な部分 です。
選択されたブレンドモードに応じて各種プロパティやRenderType、renderQueueを適切に設定します。
/// <summary>
/// マテリアルのブレンドモードを変更します
/// </summary>
/// <param name="material">マテリアル</param>
/// <param name="blendMode">ブレンドモード</param>
static void ApplyBlendMode(Material material, BlendMode blendMode)
{
switch (blendMode)
{
case BlendMode.Opaque:
material.SetOverrideTag("RenderType", "");
material.SetInt("_SrcBlend", (int) UnityEngine.Rendering.BlendMode.One);
material.SetInt("_DstBlend", (int) UnityEngine.Rendering.BlendMode.Zero);
material.SetInt("_ZWrite", 1);
material.DisableKeyword("_ALPHATEST_ON");
material.renderQueue = -1;
break;
case BlendMode.Cutout:
material.SetOverrideTag("RenderType", "TransparentCutout");
material.SetInt("_SrcBlend", (int) UnityEngine.Rendering.BlendMode.One);
material.SetInt("_DstBlend", (int) UnityEngine.Rendering.BlendMode.Zero);
material.SetInt("_ZWrite", 1);
material.EnableKeyword("_ALPHATEST_ON");
material.renderQueue = (int) UnityEngine.Rendering.RenderQueue.AlphaTest;
break;
case BlendMode.Transparent:
material.SetOverrideTag("RenderType", "Transparent");
material.SetInt("_SrcBlend", (int) UnityEngine.Rendering.BlendMode.SrcAlpha);
material.SetInt("_DstBlend", (int) UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha);
material.SetInt("_ZWrite", 0);
material.DisableKeyword("_ALPHATEST_ON");
material.renderQueue = (int) UnityEngine.Rendering.RenderQueue.Transparent;
break;
case BlendMode.Additive:
material.SetOverrideTag("RenderType", "Transparent");
material.SetInt("_SrcBlend", (int) UnityEngine.Rendering.BlendMode.SrcAlpha);
material.SetInt("_DstBlend", (int) UnityEngine.Rendering.BlendMode.One);
material.SetInt("_ZWrite", 0);
material.DisableKeyword("_ALPHATEST_ON");
material.renderQueue = (int) UnityEngine.Rendering.RenderQueue.Transparent;
break;
case BlendMode.Multiply:
material.SetOverrideTag("RenderType", "Transparent");
material.SetInt("_SrcBlend", (int) UnityEngine.Rendering.BlendMode.DstColor);
material.SetInt("_DstBlend", (int) UnityEngine.Rendering.BlendMode.Zero);
material.SetInt("_ZWrite", 0);
material.DisableKeyword("_ALPHATEST_ON");
material.renderQueue = (int) UnityEngine.Rendering.RenderQueue.Transparent;
break;
default:
throw new ArgumentOutOfRangeException("blendMode", blendMode, null);
}
}
シェーダー切替時にApplyBlendModeを呼ぶ
最後に AssignNewShaderToMaterial
をオーバーライドをしてシェーダーのアサインの変更時に ApplyBlendMode を強制的に呼び出しています。
各種プロパティが常に正しい組み合わせになるようにするためのいわば安全装置です。
直前のシェーダーで各種プロパティが別の意味として使われていた場合にも、ApplyBlendMode を呼び出すことで正しい組み合わせにリセットできます。
/// <summary>
/// シェーダー切り替え時の処理をします
/// </summary>
/// <param name="material">マテリアル</param>
/// <param name="oldShader">切り替え前のシェーダー</param>
/// <param name="newShader">切り替え後のシェーダー</param>
public override void AssignNewShaderToMaterial(Material material, Shader oldShader, Shader newShader)
{
base.AssignNewShaderToMaterial(material, oldShader, newShader);
// MaterialのShader切り替え時にBlend指定が変更されてしまうので再設定します。
ApplyBlendMode(material, (BlendMode) material.GetFloat("_BlendMode"));
}
発展編
ここまでで描画モードの代表例としてブレンドモードの切り替え方法を紹介しましたが、カリングモードやステンシル操作といったその他の描画系の機能もプロパティ化するとより便利になります。
いずれもShaderLabの標準機能としてプロパティ化は想定されているので、実装は簡単です。
また、シェーダーバリアントのようにシェーダーを複数回コンパイルすることもないので、メモリやストレージ容量の増加などのデメリットもありません。
カリングモードの切替
以下のようなコードでカリングモードもプロパティ化ができます(GitHubのコミット)。
カリングモード専用の Rendering.CullMode
というEnumがUnityビルドインで定義されています。
--- a/Assets/MultiBlendModeShader/Shaders/General.shader
+++ b/Assets/MultiBlendModeShader/Shaders/General.shader
@@ -11,6 +11,9 @@
// Cutoutの閾値
_Cutoff("Alpha Cutoff", Range(0.0, 1.0)) = 0.5
+ // カリングモード
+ [Enum(UnityEngine.Rendering.CullMode)] _CullMode("Cull Mode", Float) = 2// Back
+
// メインテクスチャ
_MainTex ("Texture", 2D) = "white" {}
@@ -26,7 +29,7 @@
{
Blend[_SrcBlend][_DstBlend]
ZWrite [_ZWrite]
- Cull Back
+ Cull [_CullMode]
CGPROGRAM
#pragma vertex vert
--- a/Assets/MultiBlendModeShader/Editor/GeneralShaderGUI.cs
+++ b/Assets/MultiBlendModeShader/Editor/GeneralShaderGUI.cs
@@ -34,6 +34,9 @@ public class GeneralShaderGUI : ShaderGUI
var tintColor = FindProperty("_TintColor", properties);
materialEditor.ShaderProperty(tintColor, tintColor.displayName);
+ var cullMode = FindProperty("_CullMode", properties);
+ materialEditor.ShaderProperty(cullMode, cullMode.displayName);
+
materialEditor.RenderQueueField();
}
ステンシル操作
同様にしてステンシル操作をプロパティ化できます(GitHubのコミット)。
--- a/Assets/MultiBlendModeShader/Shaders/General.shader
+++ b/Assets/MultiBlendModeShader/Shaders/General.shader
@@ -14,6 +14,13 @@
// カリングモード
[Enum(UnityEngine.Rendering.CullMode)] _CullMode("Cull Mode", Float) = 2// Back
+ // ステンシル
+ [Header(Stencil)]
+ _StencilRef("Stencil Ref", Range(0, 255)) = 0
+ [Enum(UnityEngine.Rendering.CompareFunction)] _StencilComp("Stencil Comp", Float) = 8// ALways
+ [Enum(UnityEngine.Rendering.StencilOp)] _StencilPassOp("Stencil Pass Op", Float) = 0// Keep
+ [Enum(UnityEngine.Rendering.StencilOp)] _StencilZFailOp("Stencil ZFail Op", Float) = 0// Keep
+
// メインテクスチャ
_MainTex ("Texture", 2D) = "white" {}
@@ -31,6 +38,14 @@
ZWrite [_ZWrite]
Cull [_CullMode]
+ Stencil
+ {
+ Ref [_StencilRef]
+ Comp [_StencilComp]
+ Pass [_StencilPassOp]
+ ZFail [_StencilZFailOp]
+ }
+
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
--- a/Assets/MultiBlendModeShader/Editor/GeneralShaderGUI.cs
+++ b/Assets/MultiBlendModeShader/Editor/GeneralShaderGUI.cs
@@ -37,6 +37,18 @@ public class GeneralShaderGUI : ShaderGUI
var cullMode = FindProperty("_CullMode", properties);
materialEditor.ShaderProperty(cullMode, cullMode.displayName);
+ var stencilRef = FindProperty("_StencilRef", properties);
+ materialEditor.ShaderProperty(stencilRef, stencilRef.displayName);
+
+ var stencilComp = FindProperty("_StencilComp", properties);
+ materialEditor.ShaderProperty(stencilComp, stencilComp.displayName);
+
+ var stencilPassOp = FindProperty("_StencilPassOp", properties);
+ materialEditor.ShaderProperty(stencilPassOp, stencilPassOp.displayName);
+
+ var stencilZFailOp = FindProperty("_StencilZFailOp", properties);
+ materialEditor.ShaderProperty(stencilZFailOp, stencilZFailOp.displayName);
+
materialEditor.RenderQueueField();
}
まとめ
シェーダーのカスタムインペクタを実装することで、自作シェーダーでもStandardShaderのようにブレンドモードを切替可能にする方法をご紹介しました。
さらに、発展編として、カリングモードとステンシル操作もプロパティ化もしました。
ブレンドモードやカリングモード、ステンシル操作といった各種描画モードごとにシェーダーを実装しなくて済むため、工数削減のメリットがあります。
実際のゲームのプロジェクト開発においても
- 描画設定をなるべくプロパティ化した汎用シェーダー(いわゆる Uber Shader)
- 特定の演出やモデルに特化した専用シェーダー
というようにシェーダーの役割を2つに分けることで、似たようなシェーダーの乱立を防ぎ、管理工数や実装工数を大きく削減できます。
また、汎用シェーダーを定義することで、プロパティ設定だけで幅広い描画設定に対応できるので、プログラマーのシェーダー修正なしにデザイナーだけで作業を進められるというメリットもあります。
みなさんも参考にしてみてください!