Help us understand the problem. What is going on with this article?

Unity のシェーダー開発方法のまとめと備忘録・頂点アニメーションテクスチャ(VAT)シェーダー

Unity のみで完結できる頂点アニメーションテクスチャー(VAT・Vertex Animation Texture)のエクスポーターとシェーダーを作りました。

--

こちらの記事はとりあえず動くシェーダーは作れる、だけど… という内容です。

拾い物やネットからのコピペ、アセットストア経由など、プロジェクトは雑多なアセットのチャンプルーが常で、たまーにシェーダーを作ったり、という方や、プロジェクトの規模に合うであろう内容になっています。

もくじ

はじめに

Project ウインドウから Unlit Shader を作り、それをベースに自作シェーダーを作ると、影を落とさない&受けない、Global Illumination の各種プローブ、ライトマップの適用やベイクにも反応しないシェーダーが出来上がります。

加えて、Unity のバージョンアップによって古くなった情報がいまでも検索に引っかかる為、専業ではない人間ではなかなか新しい情報に巡り合えず、開発に無駄な時間がかかってしまうのが Unity シェーダー開発のあるあるかと思います。

この記事は、VAT シェーダーを開発する過程で調べた、Unity の各標準機能に対応したシェーダーを開発する為に必要な情報と ShaderLab の備忘録をまとめたものです。

こんな方におススメ:

  • バージョンは Unity 2018 LTS
  • なんだかんだで Built-in Render Pipeline (Legacy) を使っている
  • グラス型AR・VR やモバイル等で Forward Rendering を使っている

目標

前述の通り、Unity の標準機能に一通り対応させることです。特定のプロジェクトだけではなく汎用的に使うことができ、アセットストアへの出品も可能なものを目指します。

対応させたい Unity 標準機能の一覧

  • GI やライトマップ、各種プローブへの対応。
    • 天球やスカイボックス、Reflection Probe が反射に映り込む。
    • Light Probe がある環境で影響を受ける。
    • ライトマップが適用される。
    • ライトマップのベイク時に周辺環境に影響を与える。
    • Reflection Probe のレンダリングに映る。
  • 影を落とす&受ける。
  • GPU Instancing と関連技術に対応している。
  • グラス型AR・VR でのステレオレンダリングで、Single Pass が使える。
    • Single Pass(Instanced) にも対応している。 ※ 全てのアセットに目が行き届く開発環境でないと使ったことが無い人が多いのでは・・・?
  • Lighting 設定のフォグの影響を受ける。

Universal Render Pipeline(旧LWRP)について

先日ベータ版から抜けて正式リリースとなった Universal Render Pipeline。
ここではシェーダーの基本的なところは抑えていますが、Built-in 向けのシェーダーを URP に対応させるにはどうしたらいいか、については扱っていません。

ステレオレンダリング&フォワードレンダリングやモバイルに関わることが多いという事もありますが、冒頭で述べたように、自身の管理外のアセットの対応状況に左右されることなどを考えると、中小規模のプロジェクトではまだ時期尚早かなと思っています。

以下の表を見て、うっ、となったというのもあります。

Shader Graph について

Maya のような DCC ツールもノードベースで見栄えをいじりますが、最後の最後で結局、欲しい情報を取れるノードが無くて望むものが作り切れない、なんて事も多いので、コーディングせずに Unity Shader Graph で、、、も扱っていません。

個人的には、ノードベースでは作りきれない、となった時に Shader Graph から ShaderLab のコードを書き出して自分で実装する、という選択もあると思うので、Unity でのシェーダー開発の方法は理解しておいて損はないと思います。

Unity の標準機能への対応方法

まずは Unity 標準機能への対応方法の紹介です。

ShaderLab の基本的なところは、備忘録として次の章にまとめています。

Global Illumination への対応

Light Probe(球面調和)の影響を受けるようにする

ライトプローブに対応するには、UnityCG.cgincShadeSH9 を使用します。このマクロは Pass のタグ設定で LightModeForwardBase に設定した状態で使用する必要があります。

#include "UnityCG.cginc"
...
Tags
{
    "LightMode" = "ForwardBase"
}
球面調和は可能な限り頂点シェーダーで処理する

球面調和(Spherical Harmonics)はけして軽い処理ではないので、頂点シェーダーで十分な結果を得られる場合はピクセルシェーダーで処理をしない方が良いです。

以下のサンプル画像を見てわかるように、Light Probe と Lighting > Environment Lighting の影響を計算するだけなので、ノーマルマッピング等を行わない限りにおいてはクッキリはっきりとした変化のある絵は出ないからです。

※ ロボットの下手側の肩のライティング結果に、わずかながら違いが見受けられます。

例(頂点シェーダー):

o.color.rgb *= ShadeSH9( half4(UnityObjectToWorldNormal(v.normal), 1) );

※ 頂点シェーダーで設定したカラーはフラグメントシェーダーに渡される際に、GPU で頂点間を線形補完された状態になります。

例(ピクセルシェーダー):

col.rgb *= ShadeSH9( half4(UnityObjectToWorldNormal(i.normal), 1) );

Reflection Probe に映るようにする

カスタムシェーダー側で何もしなくても Reflection Probe に映り込み、頂点シェーダーの変形結果も反映されます。もしも映らない場合は Reflection Probe Static の設定を忘れていないかを確認します。

※ VAT Kit ではシェーダー内で _Time を使用してアニメーションさせていますが、ベイクする度に映り込む位置が1フレームずつ進んでしまいました。Unity & ShaderLab ではベイク用のパスなどは用意されていないので、必要に応じて対策を施す必要があります。

Reflection Probe や天球・スカイボックスが映り込むようにする

Reflection Probe 及び Lighting > Environment で設定された天球・スカイボックスの情報の取得方法は、こちらの公式ドキュメントに載っています。

ただし、上記の公式ドキュメントの実装だと、なぜか複数の Reflection Probe を跨いだ際のブレンドには対応していません。

Reflection Probe のブレンドに対応する

ブレンディングに対応するにはフラグメントシェーダーで追加の処理が必要になります。

※ Reflection Probe のブレンドに対応した例

リフレクションの計算には、ワールド空間の法線と視線ベクトルが必要になります。

ノーマルマッピング等を行わない場合は、頂点シェーダーでリフレクションの法線を事前に計算しておき、映り込みのサンプリングのみフラグメントシェーダーで行います。

※ 視線ベクトルの計算に必要な頂点のワールド座標は、頂点シェーダーで計算した場合も GPU で線形補完されるので精度は変わらない(ハズ)なので、ノーマルマッピングをする時は頂点の座標のみ頂点シェーダーで計算しておくのも良いかもしれません。

Reflection Probe 対応のサンプルコード

各手法を切り替えられるようにコメントとして残したサンプルです。

struct v2f
{
    ...
    half3 worldRefl : TEXCOORD2;
    half3 worldPos : TEXCOORD3; // not required if worldRefl is calculated in vertex shader
    ...
};

worldRefl を頂点シェーダーで計算した場合は、worldPos をフラグメントシェーダーに渡す必要はなくなります。v2f から消して、頂点シェーダーの o.worldPoshalf3 worldPos に変更します。

頂点シェーダー

v2f vert(appdata v)
{
    ...
    o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
    float3 worldViewDir = normalize( UnityWorldSpaceViewDir(o.worldPos) );
    float3 worldNormal = UnityObjectToWorldNormal(v.normal);
    o.worldRefl = reflect(-worldViewDir, worldNormal);
    ...
}

フラグメントシェーダー
※ 映り込みのボケはミップマップの LOD レベルを調整することで実現します。

fixed4 frag(v2f i) : SV_Target
{
    ...
    half3 worldRefl;
    ///////half3 worldPos;
    half mipmap_lod_level = 0;

    // blurry reflection
    ////////mipmap_lod_level = roughness * UNITY_SPECCUBE_LOD_STEPS;

    // worldspace data from vertex shader
    worldRefl = i.worldRefl;
    ///////worldPos = i.worldPos;

    // calculate worlspace data in fragment shader
    ////////worldPos = mul(unity_ObjectToWorld, i.pos).xyz;
    ////////worldRefl = reflect( -normalize(UnityWorldSpaceViewDir(worldPos)), UnityObjectToWorldNormal(i.normal) );

    // reflection probe blending
    half4 skyData0 = UNITY_SAMPLE_TEXCUBE_LOD(unity_SpecCube0, worldRefl, mipmap_lod_level);
    half4 skyData1 = UNITY_SAMPLE_TEXCUBE_SAMPLER_LOD(unity_SpecCube1, unity_SpecCube0, worldRefl, mipmap_lod_level);
    half3 reflColor0 = DecodeHDR(skyData0, unity_SpecCube0_HDR);
    half3 reflColor1 = DecodeHDR(skyData1, unity_SpecCube1_HDR);

    // resulting reflection color
    half3 reflColor = lerp(reflColor1, reflColor0, unity_SpecCube0_BoxMin.w);
    ...
}

リフレクションプローブのブレンディングはこちらを参考にさせて頂きました。
(Unity 2018 LTS よりバージョンが古い Unity についての記事です)

ライトマップのベイクに対応する

ライトマップのベイク時に周辺環境に影響を与えるようにする方法は、以下の通りです。

自作の自己発光マテリアルを使ってベイクした結果

まずは結果です。中央の黒い球体が、ライトマップのベイクの際に使われる Meta パスのみを実装したカスタムシェーダーで、周辺環境に対して光を放って影響を与えています。

自己発光させて周囲を照らしたい時の注意点

上図のように自己発光しているマテリアルで周囲を照らしたい場合、シェーダーの実装だけでは実現できません。

以下の通り Material.globalIlluminationFlags を適切に設定しておく必要があります。
※ これは自己発光(Emission)が周辺環境に影響を与える時のみに必要になる設定で、Albedo や Specular のカラーブリーディングは設定をしていなくても反映されます。

(これは分かりませんよ)

Yes. The thing is the color used by the baked lighting doesn't come from the usual vertex/fragment passes people are familiar with. Those come from a special "Meta" pass. See the Illumin-VertexLit.shader from the built in shader source for a basic example. However for realtime emission there's one more step that needs to be done. There's a flag that needs to be set on the material from script. Usually this is done with a custom material editor. It can be manually set via material.globalIlluminationFlags or automatically set with MaterialEditor.FixupEmissiveFlag().
https://forum.unity.com/threads/emmisive.492609/#post-3206559

ビルトインのスタンダードシェーダーの CustomEditor として使われている StandardShaderGUI.cs では、以下のような処理をしています。

// A material's GI flag internally keeps track of whether emission is enabled at all, it's enabled but has no effect
// or is enabled and may be modified at runtime. This state depends on the values of the current flag and emissive color.
// The fixup routine makes sure that the material is in the correct state if/when changes are made to the mode or color.
MaterialEditor.FixupEmissiveFlag(material);
bool shouldEmissionBeEnabled = (material.globalIlluminationFlags & MaterialGlobalIlluminationFlags.EmissiveIsBlack) == 0;
SetKeyword(material, "_EMISSION", shouldEmissionBeEnabled);
MaterialGlobalIlluminationFlags を設定するスクリプト

エディターから設定する場合は、メッシュに対して以下のようなコンポーネントを適用します。設定が済めばアセット自体が更新されるので、剥がしても大丈夫(なハズ)です。

using UnityEngine;

public class GlobalIlluminationFlags : MonoBehaviour
{
    public MaterialGlobalIlluminationFlags flag = MaterialGlobalIlluminationFlags.AnyEmissive;

    void OnValidate()
    {
        var rend = GetComponent<Renderer>();
        if (rend && rend.sharedMaterial)
        {
            rend.sharedMaterial.globalIlluminationFlags = flag;
        }
    }

}
ライトマップのベイク時に使用する "LightMode"="Meta" パスの実装

GI のベイクの際に使われる Meta パスは、UnityMetaPass.cginc で定義されている構造体に必要な値を設定して返してあげれば良いだけです。

struct UnityMetaInput
{
    half3 Albedo;
    half3 Emission;
    half3 SpecularColor;
};
Meta パス実装のサンプルコード

Unity built-in Shader の Standard Shader の Meta パスをベースに、フラグメントシェーダーのみカスタマイズして実装するのが最も簡単な手法です。

※ 強い光を設定する事が多い自己発光カラーのプロパティーは、[HDR] を付けておくと Inspector 上でスーパーホワイトの設定が出来るようになるので便利です。

[HDR] _EmissionColor("Emission Color", Color) = (0, 0, 0, 1)

サンプルの Meta パスの実装は以下の通りです。

#pragma fragment frag_meta_custom 等に名前を書き換えて自作シェーダーを実装します。
UnityMetaInputUnityMetaFragment を通して返します。
_EmissionColor など、UnityStandardInput.cginc で定義済みのプロパティー変数の定義は不要です。(再定義するなとエラーが出ます)

Pass
{
    Name "META"
    Tags { "LightMode" = "Meta" }

    Cull Off

    CGPROGRAM
    #include "UnityStandardMeta.cginc"

    #pragma vertex vert_meta // change name and implement if customizing vertex shader
    #pragma fragment frag_meta_custom // changed to customized fragment shader name

    #pragma shader_feature _EMISSION
    #pragma shader_feature _METALLICGLOSSMAP
    #pragma shader_feature _ _SMOOTHNESS_TEXTURE_ALBEDO_CHANNEL_A
    #pragma shader_feature ___ _DETAIL_MULX2
    #pragma shader_feature EDITOR_VISUALIZATION

    // pre-defined in UnityStandardInput.cginc from UnityStandardMeta.cginc
    //half _Glossiness;
    //half4 _Color;
    //half4 _EmissionColor;
    half3 _SpecularColor;

    // customized fragment shader
    float4 frag_meta_custom(v2f_meta i) : SV_Target
    {
        UnityMetaInput o;
        UNITY_INITIALIZE_OUTPUT(UnityMetaInput, o);

        // required but no effect for EnergyConservationBetweenDiffuseAndSpecular
        half oneMinusReflectivity = 0;

        // do it
        half smoothness = _Glossiness;
        half3 albedo = _Color;
        half3 specularColor = _SpecularColor;
        half3 emissionColor = _EmissionColor;

        // assign result to output
        albedo = EnergyConservationBetweenDiffuseAndSpecular(albedo, specularColor, oneMinusReflectivity);
        #if defined(EDITOR_VISUALIZATION)
            o.Albedo = albedo;
        #else
            o.Albedo = UnityLightmappingAlbedo(albedo, specularColor, smoothness);
        #endif
        o.SpecularColor = specularColor;
        o.Emission = emissionColor;

        return UnityMetaFragment(o);
    }

    ENDCG
}
Diffuse と Specular と Smoothness の関係性

Unity の Standard シェーダーの Meta パスでは、Diffuse と Specular と Smoothness に基づいて重み付けを行う Unity のマクロが2つ使用されていて、その状態でベイクを行うと以下のような結果になります。
(当然ですが、カラーブリーディングに合わせて自身の見た目を実装する必要があります)

※ 表面が滑らかだと Diffuse(赤)のみが拡散し、ザラついていると Specular(青)が混ざる。

表面が滑らかだとスペキュラー成分が拡散しないのではなく、本来であれば指向性の強い反射が起こるハズですが、2018 LTS ではライトマッパーがその現象に対応していません。(Enlighten、Progressive CPU / GPU ともに)

※ 表面が滑らかな曲面を使って、指向性の強い反射光を集めたレンダリング例

Standard シェーダーの Meta パスで使用されているマクロ関数

Standard シェーダーの Meta パスで使用されている Unity ビルトインのマクロ関数は、エネルギー保存の法則にしたがって Albedo の調整を行うものと、ライトマッパーに合わせてさらに Albedo の調整を行うものの2つで、以下の通り定義されています。

SpecularStrengthspecColor.rgb の3つのうち最大の値を返します。
UnityStandardCore.cgincUNITY_SETUP_BRDF_INPUT = SpecularSetup 内で一度 diffuse の調整を行い、焼きこむ際には次に示す UnityLightmappingAlbedo マクロ関数でさらにライトマッパー向けの調整を行っているようです。

// Diffuse/Spec Energy conservation
inline half3 EnergyConservationBetweenDiffuseAndSpecular (
    half3 albedo, half3 specColor, out half oneMinusReflectivity
) {
    oneMinusReflectivity = 1 - SpecularStrength(specColor);
    #if !UNITY_CONSERVE_ENERGY
        return albedo;
    #elif UNITY_CONSERVE_ENERGY_MONOCHROME
        return albedo * oneMinusReflectivity;
    #else
        return albedo * (half3(1, 1, 1) - specColor);
    #endif
}

※ Unity はエディタープレビュー時には上記の調整のみを行った Albedo を表示し、焼きこむ時のみ以下のマクロ関数で smoothness に基づいたライトマッパー向けの調整を加え、その結果を Albedo に設定しているようです。
※ プレビューか否かの処理はマクロ内ではなく Meta パス内に直接記述されているので、将来のアップデートに伴ってコード修正が必要になりそうな気がしています。

half3 UnityLightmappingAlbedo (half3 diffuse, half3 specular, half smoothness)
{
    half roughness = SmoothnessToRoughness(smoothness);
    half3 res = diffuse;
    res += specular * roughness * 0.5;
    return res;
}
公式ドキュメントの Global Illumination Meta パスの実装例

公式ドキュメントにも、自己発光に対応していない実装サンプルがあります。

Unity のライトマップをシェーダーに適用する

※ Unity でベイクしたライトマップをカスタムシェーダーに適用した状態。

Unity がベイクしたライトマップをシェーダーに適用する方法は、以下の通りです。

※ モデルインポーターの「Generate Lightmap UVs」の設定に関わらず、TEXCOORD1 にはライトマップ用の UV が入っています。

struct appdata
{
    ...
    float2 texcoord1 : TEXCOORD1;
    ...
};
struct v2f
{
    ...
    float2 texcoord1 : TEXCOORD1;
    ...
};

※ Unity のライトマップは複数のオブジェクトを一つのテクスチャアトラスにまとめた状態になっているので、ライトマップ内の指定の位置に UV をスケール&移動します。

v2f vert(appdata v)
{
    ...
    o.texcoord1 = v.texcoord1.xy * unity_LightmapST.xy + unity_LightmapST.zw;
    ...
}
fixed4 frag(v2f i) : SV_Target
{
    ...
    col.rgb *= DecodeLightmap( tex2D(unity_Lightmap, i.texcoord1.xy) );
    ...
}

こちらの公式ドキュメントにも実装例があります。

影を受ける

影の情報をシェーダーで得る方法は、こちらの記事に非常に分かりやすくまとまっています。

Unity のライティング情報を取得する

影を受ける以外にも、LIGHT_ATTENUATION マクロで Unity が提供するライティングの情報も取得できます。こちらの記事が非常に参考になります。

影を落とす

カスタムシェーダーを割り当てたオブジェクトの影を落とす一番手っ取り早い方法は、後述の FallBack 法です。

頂点シェーダー等で変形させている場合の注意点

頂点シェーダーやジオメトリシェーダーによる変形がある場合は、変形処理を ShadowCaster パスでも同様に適用する必要があります。

その為、以下の FallBack 法や UnityStandardShadow.cginc 法は使用できません。

※ 変形した場所ではなく、オレンジで囲われた本来の位置を元に影が落ちてしまっている。

ShadowCaster パスでも同様の変形を行う必要があるので、VATKit_vertShader.cginc 等のように頂点シェーダーを外部ファイル化して #include する等、重複した処理を書かずに済む方法も開発の早い段階から検討しておかないと、後々の切り分け作業で大変なことになりそうです。

参考:

FallBack 法

上記のシェーダーテンプレートにも入っていますが、

  • FallBack "Mobile/VertexLit"

として処理を任せてしまうのが、将来的な保守も不要で一番です。

ShadowCaster パスが存在するとフォールバックが起きないので、パスが存在しない状態にしておきます。

UnityStandardShadow.cginc 法

こちらは Standard Shader の ShadowCaster パスをそのまま使う方法です。

将来的なアップデートで動かなくなる可能性も無くはないので、FallBack を本来の用途以外で使うのはためらわれる、という場合以外は FallBack "Mobile/VertexLit" を使う方が良いと思います。

参考:

オフィシャルサイトのアーカイブから、「ダウンロード(Win or Mac)」→「ビルトインシェーダー」を選択して最新のシェーダーコードをダウンロードすることもできます。

全自前実装法

頂点シェーダーで変形している場合など、ShadowCaster パスを自身で実装する方法は、こちらが非常に参考になります。

ステレオレンダリングへの対応

マルチパスのステレオレンダリング(単に複数のカメラで2回描画しているだけ)にはデフォルトで対応していますが、左右の目の画像を横に並べてレンダリングする、Single pass モードのステレオレンダリングに対応する為には、頂点シェーダー等を対応させる必要があります。

ざっくり言うとスクリーン座標など、画面が左右横並びで2倍幅になると影響を受ける処理は、Unity謹製のマクロを使おう、というお話です。
(例:mul(UNITY_MATRIX_MVP, v); ではなく UnityObjectToClipPos() を使う)

加えて、Single-pass instanced(2018 LTS ではプレビュー版) モードへの対応もあります。そちらは以下に続きます。

インスタンス及び関連技術への対応(GPU Instancing)

シェーダーをインスタンス対応にすると、マテリアルのインスペクターに「Enable GPU Instancing」というチェックボックスが表示されるようになります。

逆を言えば、Enable GPU Instancing が無いものはインスタンスに未対応となり、インスタンスだけではなく以下の関連技術の全てが無効であるという事になります。

まず最初に、インスタンスへの対応方法はこちらです。

Material Property Block への対応

シェーダーをインスタンスに対応させることで、MaterialPropertyBlock への対応も可能になります。

ただし、あくまでも似通ったマテリアルが大量に必要になる事態を防ぐための機能であって、インスタンスへの対応が必要だからと言って Batch 数や SetPass calls、Draw call が減ったりすることは無いようです。

※ Saved by batching: 0、Batches / SetPass calls ともに 5,404 で、減っている様子はない。

VAT シェーダーでは Time Offset が MaterialPropertyBlock に対応しているので、冒頭で紹介した動画のようにメッシュとマテリアルが一対でも、それぞれのアニメーションのタイミングを変えることが可能になっています。

※ 対応しても GPU Instancing のように Inspector の表示が変わるわけではないので、残念ながら外側からではどのプロパティーが対応しているのかは分かりません。

Material Property Block への対応方法

以下のページにある、「Adding per-instance data」セクションに対応方法の記載があります。

Material Property Block の使用方法

Material Property Block を使ったプロパティーの設定方法は、既存のものを取得 → 無ければ作成&セット → レンダラーへ適用の流れです。

Single pass instanced モードのステレオレンダリング

Single pass Instanced モードへの対応方法の詳細はこちらです。

フォグへの対応

Project ウインドウから作成できる、Unlit Shader のテンプレートで既に対応がなされていますが、

  1. #pragma multi_compile_fog を追加
  2. struct v2fUNITY_FOG_COORDS(1);(1 は TEXCOORD# の # 部分)
  3. vert()UNITY_TRANSFER_FOG(o, o.vertex);
  4. frag()UNITY_APPLY_FOG(i.fogCoord, apply_to_this_fixed4_color);

で対応させることができます。こちらの Unity Answers の記事が詳しいです。

フォグ対応の注意点

フォグのマクロは LightModeForwardAdd に設定されている場合は、なにもしてくれません。ForwardBase もしくは LightMode 無しのパスでのみ動作します。

ForwardAdd を単純に実装して複数のライトで照らした場合は、2つ目以降のライトがフォグを無視することになるので、少しおかしな結果になった気がします(うろ覚えですが…)この辺りはフォワードライティングという手法の限界かもしれませんね。

i.fogCoord の情報が ForwardAdd でも取得できるようなら、それを元にライティング側が配慮すれば対応は出来そうです。

プラットフォームごとの違いを吸収する方法

OpenGL や DirectX など、プラットフォームごとに UV 座標やデプスバッファの扱いなどに違いがあります。

何もしなくても Unity が吸収してくれる場合や、そうではない場合があります。詳細や対応方法は以下のページから確認できます。

※ 抜粋:クリップ後の座標に違いがある。

  • Direct3D-like: The clip space depth goes from +1.0 at the near plane to 0.0 at the far plane. This applies to Direct3D, Metal and consoles.
  • OpenGL-like: The clip space depth goes from –1.0 at the near plane to +1.0 at the far plane. This applies to OpenGL and OpenGL ES.

※ 抜粋:デプスが黒→白ではなく、白→黒のプラットフォームがある。

  • Use SystemInfo.usesReversedZBuffer to find out if you are on a platform using reversed depth (Z).

※ 抜粋:UV 座標系の違いに対応する方法。

// Flip sampling of the Texture: 
// The main Texture
// texel size will have negative Y).

#if UNITY_UV_STARTS_AT_TOP
if (_MainTex_TexelSize.y < 0)
    uv.y = 1 - uv.y;
#endif
v2f vert(appdata v)
{
    ...
    o.uv = v.uv;
    // This example is rendering with upside-down flipped projection,
    // so flip the vertical UV coordinate too
    if (_ProjectionParams.x < 0)
    {
        o.uv.y = 1 - o.uv.y;
    }
    ...
}

UNITY_UV_STARTS_AT_TOP_ProjectionParams については、こちらに詳細があります。

ShaderLab の構造・文法の備忘録

ここからは、シェーダーの中身の構造・文法についてです。

自作シェーダー作成の際の注意点

基本的にサーフェイスシェーダーは使わない方が良いと思います。ある程度まではパッと作れますが、後々制限に悩まされます。

特に、サーフェイスシェーダーの情報を扱っている人が少ないという点がネックです。

※ 以下のようなコードが載っている場合はサーフェイスシェーダーの情報です。

  • #pragma surface surf Lambert
  • void surf(Input IN, inout SurfaceOutput o)
  • SurfaceOutputStandard
  • uv_MainTex

シェーダーのテンプレート

シェーダー開発のテンプレートとして、リファレンスの URL をコメントとして記載した物を作ってみました。

Shader "Path/To/The/Shader"
{
    // https://docs.unity3d.com/Manual/SL-Properties.html
    // https://docs.unity3d.com/ScriptReference/MaterialPropertyDrawer.html
    Properties
    {
    }

    // https://docs.unity3d.com/Manual/SL-SubShader.html
    SubShader
    {
        // https://docs.unity3d.com/Manual/SL-SubShaderTags.html
        Tags
        {
            "Queue" = "Geometry"
            "RenderType" = "Opaque"
        }

        // https://docs.unity3d.com/Manual/SL-Pass.html
        // Cull, ZTest, etc...


        CGINCLUDE
        #include "UnityCG.cginc"
        #include "Lighting.cginc"
        #include "AutoLight.cginc"
        #pragma target 3.0
        ENDCG


        // https://docs.unity3d.com/Manual/SL-Pass.html
        Pass
        {
            // https://docs.unity3d.com/Manual/SL-PassTags.html
            Tags
            {
            }

            // https://docs.unity3d.com/Manual/SL-Pass.html
            // Cull, ZTest, etc...


            CGPROGRAM
            ENDCG

        }

    }
    FallBack "Mobile/VertexLit"
    // https://docs.unity3d.com/Manual/SL-CustomEditor.html
    //CustomEditor "ClassName"
}

構造体やシェーダープログラムのおススメと注意点

印象ベースですが、各要素の名前は下記のものがおススメです。

特に、v2fpos は、テンプレとして使う事も多い Unlit Shader NewUnlitShader.shader では vertex になっていますが、ライティング、影関連のマクロの影響で pos に変更しておいた方が良いです。
TRANSFER_VERTEX_TO_FRAGMENT(o) というマクロで pos がハードコードされています)

struct は最後にセミコロン

struct appdata
{
    ...
};
struct v2f
{
    float4 pos : SV_POSITION;
    ...
};
v2f vert(appdata v)
{
    v2f o;
    ...
    return o;
}
fixed4 frag(v2f i) : SV_Target
{
    ...
}

--

Properties

Properties ブロックでは、シェーダーに必要なテクスチャやその他のプロパティーを設定します。
※ シェーダープログラムからアクセスするには、後述の手続きが必要です。

例:

_MyTexture ("My texture", 2D) = "white" {}
_MyNormalMap ("My normal map", 2D) = "bump" {}  //Grey
_MyInt ("My integer", Int) = 2
_MyFloat ("My float", Float) = 1.5
_MyRange ("My range", Range(0.0, 1.0)) = 0.5

_MyColor ("My colour", Color) = (1, 0, 0, 1)    // (R, G, B, A)
_MyVector ("My Vector4", Vector) = (0, 0, 0, 0)    // (x, y, z, w)

プロパティーの命名に迷った場合

プロパティーの命名に迷ったら、UnityStandardInput.cginc を参考にすると良いかもしれません。

抜粋:

//---------------------------------------
half4       _Color;
half        _Cutoff;

sampler2D   _MainTex;
float4      _MainTex_ST;

sampler2D   _DetailAlbedoMap;
float4      _DetailAlbedoMap_ST;

sampler2D   _BumpMap;
half        _BumpScale;

sampler2D   _DetailMask;
sampler2D   _DetailNormalMap;
half        _DetailNormalMapScale;

sampler2D   _SpecGlossMap;
sampler2D   _MetallicGlossMap;
half        _Metallic;
half        _Glossiness;
half        _GlossMapScale;

sampler2D   _OcclusionMap;
half        _OcclusionStrength;

sampler2D   _ParallaxMap;
half        _Parallax;
half        _UVSec;

half4       _EmissionColor;
sampler2D   _EmissionMap;

//-------------------------------------------------------------------------------------

マテリアルインスペクターのふるまいを調整

チェックボックスやスライダー付きの整数プロパティーなど、Unity のビルトインシェーダーのようなインスペクターを再現するには、以下のページにある Material Property Drawer アトリビュートを追加で設定する必要があります。

抜粋:

  • 整数プロパティーにスライダーが欲しい場合は、[IntRange]
  • 単に Inspector でチェックボックスを表示したいだけなら、[MaterialToggle]
    • チェックボックスに応じて 0 or 1 がプロパティーに設定されます。
  • [Toggle] はキーワードを設定したい時に使用する。
    • #pragma shader_feature#pragma multi_compile で使用します。

CustomEditor を指定して完全にカスタマイズする

上記のシェーダーテンプレートの、FallBack の並びに CustomEditor を追加してクラス名を指定すると、指定のエディター拡張でインスペクターの描画を完全にコントロールできるようになります。

CustomEditor "ShaderInspectorClassName"

参考 ※指定のクラスを含むエディタースクリプトは Editor フォルダ以下に配置します。

Cull や ZTest 等に Inspector で設定したプロパティーの値を使う

Cull 等にインスペクターで設定した値を渡したい場合は、[プロパティ名] と記述します。

例:

  • Cull [_CullPropertyName]
  • ZTest [_ZTestPropertyName]

プロパティーに Enum を設定してドロップダウンメニューから選べるようにしておくと、間違った値を設定をしてしまうことを防ぐ事が出来ます。

例:

[Enum(UnityEngine.Rendering.CullMode)]
_Cull("Cull", Float) = 0

Enum がある場合はプロパティーに設定しておく

大抵のものは UnityEngine.Rendering に Enum が定義されているので、一覧から探して忘れずに設定しておきます。

プロパティーにシェーダーからアクセスする

シェーダーからプロパティーの値を参照するには、CGINCLUDECGPROGRAM 内で変数を定義します。詳細はこちらです。

uniform float4 _MyColor; と書くことも出来ますが、uniform は無くても良いです。

テクスチャ解像度や時間などの特殊なプロパティー

テクスチャとして _MainTex を設定すると、Inspector から UV オフセットとスケールが設定できるようになります。シェーダーからそれらの値にアクセスするには、_ST(Scale & Translation)を末尾に付けた float4 _MainTex_ST という特殊なプロパティーを使います。

同様に float4 _MainTex_TexelSize を定義した場合は、アサインされているテクスチャのサイズなどが取得できるようになります。

その他にも、末尾 _HDR や、VATシェーダーでも使っている _Time 等があります。詳細は以下のページが参考になります。

SubShader と Pass

SubShader はプロパティの並びに必要なブロックで、タグを通して Render Queue の設定などが可能です。

※ レンダーキューについての抜粋

Background (1000): 背景、スカイボックス(天球)の描画に使用。
Geometry (2000): 不透明オブジェクト用のキューで、描画順は手前→奥。
AlphaTest (2450): カットアウト(Cutout)用のキューで不透明オブジェクトとしてソートされる。
↑ ↑ ↑
レンダーキューが 2,500 以下の場合は不透明オブジェクトとして手前→奥にソートされる。
キューが 2,501 以上の場合は透明オブジェクトとして奥→手前にソートされるようになる。
↓ ↓ ↓
Transparent (3000): 透明オブジェクト用のキューで、描画順は奥→手前。
Overlay (4000): レンズフレアや GUI、テキストの描画などに使用。

"Geometry+5" のように相対的に指定することも可能です。
※ フラグメントシェーダーでカットアウト処理を行うには clip() 関数を使用します。

SubShader のその他の詳細はこちらから確認できます。

アルファチャンネルを使用したカットアウト(Cutout)処理

キューを AlphaTest にしただけではカットアウト処理は行われません。Unity 2018 LTS・フラグメントシェーダー対応のカットアウト処理については、こちらの記事が非常に参考になります。

パフォーマンスに応じてシェーダーを切り替える LOD 処理

シェーダーの LOD については、こちらから確認できます。

SubShader の Tags

SubShader の Tags の詳細はこちらから確認できます。

SubShader の Pass

SubShader には複数の Pass ブロックを持つ事ができ、影描画やライトマップのベイク時など、場合によって処理を変える事が出来ます。(パスが無い SubShader のみの実装でも動作します)

Pass と SubShader は記述内容はほぼ同様ですが、Tags の内容が若干違います。

タグの後には SubShader と同様に Cull 等の設定を必要に応じて記述します。

CGPROGRAM ~ ENDCG

SubShader や Pass ブロック内に記述する、シェーダー本体の実装部分です。頂点シェーダーやピクセルシェーダーは CGPROGRAM ~ ENDCG の間に記述します。

Unity エンジンから取得できるデータの一覧など、良く参照する公式ドキュメントはこちらです。

チュートリアル

リファレンス

サンプル集

CGINCLUDE ~ ENDCG

上記の CGPROGRAM とほぼ同様ですが、SubShader 内などで、各 Pass 共通の処理を書くときに使用します。

シェーダーモデルのターゲットレベル

ターゲットレベルは、基本的に Shader Model 3.0 にしておくと良いと思います。

  • #pragma target 3.0

各モデルでサポートされている機能の一覧はこちらで確認できます。

multi_compile vs. shader_feature

設定によって処理内容を変える方法として、#pragma multi_compile...#pragma shader_feature... の2つがあります。

  • multi_compile
    • 使用しているかどうかに関わらず、すべての組み合わせがコンパイルされる。ランタイム中に Material.EnableKeyword で切り替えが可能。
  • shader_feature
    • プロジェクト内で使用している組み合わせだけがコンパイルされる。ランタイム中に切り替えは不可能。

とりあえず multi_compile を使っておこう、と思えますが、multi_compile は組み合わせのすべてを shader variants としてコンパイルするので、数によっては非常に時間のかかる処理になってしまいます。
(例: オン・オフが4つ、2 ^ 4 = 16 種類のバリアント)

シェーダーが初めて表示されるときにメインスレッドでコンパイルが走るわけですが、シェーダーバリアントが多い場合は、特にモバイル環境でコンパイルに時間がかかり、終わるまでアニメーション・パーティクル・UIの入力ほか全てが停まることになります。

そのため、shader_feature との使い分けが必要になります。

参考:

if/else の使用は避ける

シェーダーの条件分岐で if/else を使用することは可能ですが、処理が重いので代わりに step 関数を使用します。詳細は以下の記事が非常に参考になります。

GLSL と HLSL の差異はこちらにまとまっています。

TEXCOORD が不足した場合

UV は基本的には最大 4 枚(TEXCOORD0~3)で、TEXCOORD1 はライトマップ用に使われる想定になっています。モデルのインポート設定で Generate Lightmap UVs をオンにした場合、TEXCOORD1 が更新されるという事ですね。

この TEXCOORD は UV 以外にも Unity 標準のフォグの実装などでも使われていたり、その他様々な用途で消費され、足りなくなってきます。

UVは2次元の座標ですが、TEXCOORDfloat4 or half4 の4要素を持てるようになっています。なので、UV 1つ目を TEXCOORD#.xy に、UV 2つ目を TEXCOORD#.zw に、といったように2つを1つにまとめる事も可能です。
※ VAT シェーダーでも TEXCOORD0.zw を使っています。

Unity 上で UV を設定する場合、Mesh.uv* を使用すると .zw の設定が出来ないので、代わりに Mesh.GetUVs & Mesh.SetUVs を使います。

v2f など、頂点シェーダーからピクセルシェーダーにデータを送る際には、TEXCOORD を効率よく利用することを心がけておきたいです。

頂点シェーダーの注意点

テクスチャの色情報を取得する tex2D() がありますが、これはフラグメントシェーダーの中でしか使えません。

バーテックスシェーダーからテクスチャにアクセスしたい場合には、代わりに tex2Dlod() を使用します。

フラグメントシェーダーの注意点

注意点、では無いですが、fixed4 frag() だと HDR レンダリングに対応できるのか、出来ないのか、調べ切れませんでした。

溢れた色が思った通りにならなければ、half4 frag()float4 frag() にすると良いかもしれません。

おわりに

冒頭で紹介した VAT シェーダーですが、仕事で使う必要が無くなった&他の事で手一杯なので、今後の開発についてはまっっったく予定が立たない状態ではありますが、積んでしまったパーティクル(Shuriken)のメッシュインスタンスへの対応なども、シェーダーの開発に合わせてこちらの備忘録まとめに追加していければ良いなと思っています。

今回は1体のポリゴン数が 4,472、アニメーションが 202 frames @ 30fps のアセットを録画しながら大量に動かす、という結構な重みでのテストでしたが、モバイルでも 100 体程度なら十分動いたのは予想外でした。

ループモーションで動くキャラをつまんで移動することもできるので、リアルタイムストラテジーのように引いた絵に大量のキャラがいるようなゲームであれば、VAT でのアニメーションでも十分なのかもしれませんね。

--

最後までお読み頂きありがとうございます。これは Unity のアドベントカレンダーに向けて書いていたのですが、当初想定した以上に長く&遅くなってしまいました。みなさまの今後のシェーダー開発の助けになれば幸いです。

それでは以上になります。お疲れさまでした。

おまけ

モバイル端末(Android)でのテスト動画です。

sator_imaging
作業用のメモや、読んだ資料のまとめが置いてあります。 アメドラ好き。 最近は ・プリプロ ・演出シーンやアセットの制作 が半々ぐらいです。 ◆ 短編アニメシリーズ ◆ VR進撃の巨人 THE HUMAN RACE ◆ 出資向けVTuber ◆ 版権物スマホゲー
https://twitter.com/sator_imaging
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした