土屋つかさの技術ブログは今か無しか

土屋つかさが主にプログラミングについて語るブログです。

ユニティちゃんシェーダーを読む(輪郭線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を入れておいてください」となっています。

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

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

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

    return o;
}

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