Hatena::ブログ(Diary)

土屋つかさのテクノロジーは今か無しか

2018-04-19

UnityのシェーダーでBINORMAL入力セマンティクスが機能しない話と回避する方法の話

 技術書典4合わせで「ユニティちゃんトゥーンシェーダーをひたすら読む本」というコピー本を作りました。時間の関係で調べ切れなかった事を1個書いておきます。ああ、これも書きたかったな……。シェーダー本2に収録しよう……。

 Unityの頂点シェーダーの入力に、頂点の従法線ベクトルである": BINORMAL"セマンティクスを指定すると、Unity上で以下のようにエラーが発生し、シェーダーがピンクになってしまいます。

Shader error in 'xxxx': Vertex program 'vert': unknown input semantics BINORMAL/0  (on d3d11)

 BINORMAL入力セマンティクスはHLSLの仕様上は存在しており(ただしUnityのドキュメントには書いてありません)、コンパイルも通っているので、これは恐らくUnityの内部処理的な事情(例えば、プラットフォーム間の差異吸収の為とか)なのでしょう。
 接線ベクトルと従法線ベクトルについて、Unityドキュメントには以下のように書かれています。公式訳に微妙な訳し漏れがあるので、私訳を掲載しておきます。

https://docs.unity3d.com/Manual/SL-VertexProgramInputs.html
原文:Tangent and binormal vectors are used for normal mapping. In Unity only the tangent vector is stored in vertices, and the binormal is derived from the normal and tangent values.
公式訳:接線および従法線ベクトルは、法線マッピングに使用されます。Unity では、接線ベクトルは、頂点に格納され、従法線ベクトルは、法線および接線から派生します。
私訳:接線ベクトル及び従法線ベクトルは、法線マッピングを行う際に使用されます。Unityでは、頂点には接線ベクトルのみが格納されており、従法線については、法線と接線の値から導出します。

 法線(float3 normal)と接線(float4 tangent)から従法線(binormal ちなみに従接線(bitangent)と言った場合も同じ物を指すそうです)を算出するのは以下の様な式になります。

float3 binormal = cross( normal, tangent.xyz ) * tangent.w;

 crossは二つのベクトルから外積を取ります。法線と接線の外積を取れば従法線を取得できます。tangentのw要素には、プラットフォームの右手系/左手系を示す1or-1が入っているので、これを乗算して反映させます(OpenGLベースなのか、DirectXベースなのかで右手系/左手系が変わります)。
 このtangent.wのことをhandedness(利き手)と呼ぶようです。これは右手系/左手系を両方扱うUnity固有の概念で、ドキュメントでは下記ページのコードのコメントに書いてあります。
https://docs.unity3d.com/Manual/SL-VertexFragmentShaderExamples.html

// in Unity tangents are 4D vectors, with the .w component used to
// indicate direction of the bitangent vector.

私訳:Unityにおいては接線(tangent)は.wを持つ4次元ベクトルで、従接線(bitangent)ベクトルの方向を示すために使用されます。

参考:tangent.wについて海外フォーラムでの議論
https://forum.unity.com/threads/what-is-tangent-w-how-to-know-whether-its-1-or-1-tangent-w-vs-unity_worldtransformparams-w.468395/

2018-04-05

Unity Tips "UV Charting Control"と"Lightmap Settings"のオプションを出す方法

一瞬戸惑ったのでメモ。Mesh Rendererのインスペクターで"UV Charting Control"と"Lightmap Settings"の2項目を表示させるには、以下の設定が必要です。

・"Lightmap Static"のチェックを入れる(対象のオブジェクトをStaticにした場合は自動でオンになる)
・Lightingの設定で、以下のどちらか(あるいは両方)のチェックボックスをオンにする
 ・Realtime Lighting>Realtime Grobal Illumination
 ・Mixed Lighting>Baked Grobal Illumination

要するに、これらのオプションはGI用の物なので、「オブジェクトがGIの対象になる(=staticにする)」「シーン上でGIを有効にする」の二つが実行されないと、表示されないのです。

関連

Mesh Renderer
https://docs.unity3d.com/ja/current/Manual/class-MeshRenderer.html
Maya から UnityUVインポート
https://docs.unity3d.com/ja/current/Manual/LightingGiUvs-ImportingFromMaya.html

2018-03-30

ユニティちゃんシェーダーを読む(トゥーンシェード1)

 準備運動も終わったので本丸のトゥーンシェード部のPass(Name "FORWARD")を見て行きます。今回は頂点シェーダー部

入出力セマンティクスと頂点シェーダ

struct VertexInput {
    float4 vertex : POSITION;
    float3 normal : NORMAL;
    float4 tangent : TANGENT;
    float2 texcoord0 : TEXCOORD0;
};

struct VertexOutput {
    float4 pos : SV_POSITION;
    float2 uv0 : TEXCOORD0;
    float4 posWorld : TEXCOORD1;
    float3 normalDir : TEXCOORD2;
    float3 tangentDir : TEXCOORD3;
    float3 bitangentDir : TEXCOORD4;
    LIGHTING_COORDS(5,6)
    UNITY_FOG_COORDS(7)
};

VertexOutput vert (VertexInput v) {
    VertexOutput o = (VertexOutput)0;

 頂点シェーダーvertはVertexInput構造体を入力、VertexOutput構造体を出力としています。LIGHTING_COORDSマクロはTEXCOORD5/TEXCOORD6にライト周りのテクスチャとして設定し、UNITY_FOG_COORDSマクロはTEXCOORD7にフォグ周りのテクスチャを設定しています(これのマクロ展開についてはまた別の時にやります)。

o.uv0 = v.texcoord0;
o.normalDir = UnityObjectToWorldNormal(v.normal);

 uvはそのまま出力に渡しています。
 出力normalDirは、頂点法線の「ワールド空間」の値になります。これ、覚えておいた方が良いかもです。

o.tangentDir = normalize( 
	mul( unity_ObjectToWorld, 
		 float4( v.tangent.xyz, 0.0 ) ).xyz 
	);

 接線(タンジェントベクトルもワールド空間に変換して出力します。mulに渡す前に同次座標wを削除してるのはなんでなんだろ。wに値が入ってるのかな?(よくわかってない)

                o.bitangentDir = normalize(
                	cross(o.normalDir, o.tangentDir) * v.tangent.w);

 crossはベクトル外積を返す関数で、ここでは法線と接線から従法線(バイタンジェントベクトルを算出しています。さっきサクったwはここで使っているのですね。
※この辺のwの使い方実はいまいち把握できてないので今度ちゃんと調べます。

                o.posWorld = mul(unity_ObjectToWorld, v.vertex);

 頂点座標自体もワールド空間の物を出力しておきます。

                float3 lightColor = _LightColor0.rgb;

 このライトのカラー値はマクロで使用しています。

                o.pos = UnityObjectToClipPos(v.vertex );

 最後に、頂点のクリップ空間座標を出力します。

                UNITY_TRANSFER_FOG(o,o.pos);
                TRANSFER_VERTEX_TO_FRAGMENT(o)
                return o;
            }

 組み込みマクロを2個実行して、構造体を返します。頂点シェーダーはここまで。

余談

 そういえば、Unity2017.3から(なのか?)、"mul(UNITY_MATRIX_MVP,〜)"を含むシェーダーコードを書くと、該当部分が自動的に"UnityObjectToClipPos(〜)"に置換され、ファイルの先頭に以下の注釈が追記されるようになりました。

// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'

 なにかしらの演算を省略する最適化があるのかな?(UnityObjectToClipPosの中身が更新されたのかはまだ見てない)。前者の方が直感的で分かりやすいと思っていたのですが、保存ができないので今後は後者を使っていきます。

2018-03-29

ユニティちゃんシェーダーを読む(輪郭線3)

フラグメントシェーダ関数

 ここからフラグメントシェーダ関数になります。

float4 frag(VertexOutput i, float facing : VFACE) : SV_Target{

 入力は、VertexOutput構造体と、VFACEセマンティクスがバインドされたfacingです。VFACEは浮動小数スカラーで、描画対象のピクセルが負ならカメラに対して裏向き、正なら表向きであることを意味します。数値が割合を示すのかはぱっと調べた限りではわかりませんでした(シェーダー本の記述ミスだったかも)。また、2.0.3コードでは、このfacingは使っていないようです。

//頂点シェーダーアウトプット
struct VertexOutput {
    float4 pos : SV_POSITION; //POSITION
    float2 uv0 : TEXCOORD0;
};

 VertexOutput構造体はなぜか解説を忘れてました。SV_POSITIONはフラグメントシェーダーの入力セマンティクスに含まれないために使えません(値が不定)。TEXCOORD0には、テクスチャuv座標が入っています。

float4 _BaseMap_var = tex2D(
    _BaseMap,
    TRANSFORM_TEX(i.uv0, _BaseMap)
);

 ベースマップ(_BaseMap)からカラー値を取得。ここで取得されるのは、本来そのピクセルが持つべき値と同じです(プリミティブ自体は若干大きくなってますが)。

float3 Set_BaseColor = lerp( 
    _BaseColor.rgb * _BaseMap_var.rgb, 
    _BaseColor.rgb * _BaseMap_var.rgb * _LightColor0.rgb, 
    _Is_LightColor_Base
);

 _BaseColorはインスペクター上でBaseColorとして設定できる値で、BaseMapに乗算されるカラー値です。輪郭線のα値は常に0にするので、これ以降はfloat3で演算します。
 Is_LightColor_Baseはインスペクターでトグルで表示され、基本色に対しライトカラーを有効にするかどうかを表します。型はFloatですがトグルなので0.0か1.0のどちらかとなり、lerp関数では0.0なら前者(ベースマップ×ベースカラーのみ)、1.0なら後者(ライトカラーも乗算する)が選択されます。
 lerp関数はかなり高速に動作するようで(良く知りません)、こんな風に条件分岐の代わりに使われることが多い印象があります。

float3 Set_Outline_Color = lerp( 
    _Outline_Color.rgb, 
    _Outline_Color.rgb * Set_BaseColor * Set_BaseColor, 
    _Is_BlendBaseColor 
);

 _Outline_Colorはインスペクター上でOutline_Colorとして設定できる値で、輪郭線のカラー値です。さっきと同じように、Is_BlendBaseColorトグルのオンオフによってlerpの前者(輪郭線カラー値のみ)と後者(輪郭線カラー値にベースカラーを乗算)のどちらかがカラー値として算出されます。
 Set_BaseColorが2回乗算されてるのはなんでだろう? なにかのイディオムなのかな(ごめんなさいわかりません)。

                return fixed4(Set_Outline_Color,0);
            }

 算出された輪郭線カラー値を戻り値に返します。

Cull Front

 「え? もう終わり? なんでこれで輪郭線が描画されるの? このままだと元のモデルを一回り大きな単色のモデルが覆っちゃうんじゃない?」と正直思ったんですが、確認したら輪郭線のPassでは以下のようにカリングで裏面のみを描画するように設定されていました。

Cull Front

 乱暴に言えば後ろ半分だけ描画され、かつ、それらのプリミティブは常に元のモデルよりも奥側に配置されるために、上手い事輪郭線部分だけがフレームバッファに記録されるのですね。Passの順序を入れ替えれば、早期Zテストが機能して、もっと効率化できるかもしれません(機会があったら計測してみます。)

輪郭線部まとめ

 2.0.3では頂点を外側に動かしてその部分を指定したカラー値で塗りつぶすことで輪郭線を描画する、比較的シンプルな手法(「押し出し法」と言うようです)を使っていました。2.0.4でどうなってるかはわかりませんが、ひとまず輪郭線描画の理解が進みました!><

2018-03-28

ユニティちゃんシェーダーを読む(輪郭線2)

余談:ユニティちゃんシェーダーのバージョンの話

 この記事で参照しているコードのverは2.0.3なのですが、Unityの小林さんから2.0.4ではごっそり修正してるというお話をツイッター経由で教えて頂きました。うっすらそうなのかなとは思っていたのですが、まあコードリーディングの勉強でもあるのでこのまま進めます。2.0.4が正式リリースされたらそれも読みたいと思っています。

ワールド空間でのカメラ方向の取得

float3 viewDirection = normalize(
    _WorldSpaceCameraPos.xyz - o.pos.xyz
);

 カメラ方向を算出します。先にも出てきた_WorldSpaceCameraPosはカメラのワールド空間位置(World space position of the camera. 参照:https://docs.unity3d.com/ja/current/Manual/SL-UnityShaderVariables.html
)。後者はこの時点では初期化直後なのでfloat3(0,0,0)の筈(んーこれバグか? 2.0.4ではなくなってるかも)。なので、ここではワールド原点からライトへの方向を、normalize関数を使って単位ベクトルに変換して取得しています。

プロジェクション空間でのカメラ方向の取得

float4 viewDirectionVP = mul(
    UNITY_MATRIX_VP, float4(viewDirection.xyz, 1)
);

 viewDirectionにVP変換行列を乗算し、プロジェクション空間でのカメラ方向を取得します。

頂点の座標変換

o.pos = UnityObjectToClipPos(
    float4(v.vertex.xyz + v.normal * Set_Outline_Width, 1) 
);

 頂点をクリップ空間座標に変換します。
 まず、頂点法線の単位ベクトル(v.normal)にアウトライン幅係数(Set_Outline_Width)をかけ、それと頂点ベクトル(v.vertex.xyz)を加算します。ベクトルの足し算なので、結果は「頂点から垂直方向に、アウトライン幅係数分だけ移動した座標」になります。
 この演算によって、このPassで描画される頂点は、元の頂点と同じか少し外側に配置されます。結果として、輪郭線として機能するわけです。
 輪郭線用の頂点になった座標を、UnityObjectToClipPosでクリップ空間座標に変換し、出力に話足ます。

 余談ですが、Unityではプロジェクション空間とクリップ空間を区別していない(というか用語が混在している)ようなのですが、恐らく全部クリップ空間(プロジェクション空間座標を射影変換した物)なのだと思います(未確認)

最終的な頂点座標の算出

o.pos.z = o.pos.z + _Offset_Z * -0.1 * viewDirectionVP.z;

 Z座標のみここでオフセットを加算します。_Offset_ZはインスペクターではOffset_Camera_Zと表示されている変数で「通常は0を入れておいてください」となっています。

 このシェーダーでは輪郭線を描画するために頂点を法線方向に押し出しているわけですが、これだとカメラに対して平行あるいはより鋭角(って言うのか?)に配置されたプリミティブは、元のプリミティブが輪郭線プリミティブを覆ってしまい、輪郭線が描画されなくなります(ドキュメントで言うスパイク形状のオブジェクトでは、恐らくこれが頻出するのでしょう多分)。そのため、ここでオフセット値を加算します。

 例えばこの画像では、髪の毛の先に輪郭線が描かれていません(ただし、ヘアはシェーダーが違うので別の理由かも。)。
f:id:t_tutiya:20180329192121p:image

 viewDirectionVPにはカメラ方向のベクトルが入っているので、そのz値に係数をかけて減算(* -0.1)すると、奥行き方向にオフセットがかかる……のかな?(すみません自信無し)

    return o;
}

 演算結果を返して頂点シェーダ関数終了。次回はフラグメントシェーダーを見て行きます。