オブジェクトに注目させたいときや、何かを選択したときにアウトラインを表示するという演出は非常に効果的です。そうしたアウトラインをシェーダーではどのように表現するのかをまとめました。
Contents
ポイントはパスを二回呼ぶ
アウトラインシェーダーのポイントは、ボディとアウトラインで二回ドローするという点です。1Pass目で、メッシュが少し大きくなるように頂点シェーダーを書き、2Pass目で本来の大きさのメッシュを描画します。すると、1Pass目が少しはみ出して描画され、その部分がアウトラインとなる、というわけです。
共通部分
アウトラインシェーダーでは、Passを二回呼ぶことになりますが、構造体の定義は使いまわすことができます。具体的には、以下のようになります。
Shader "CAGraphicsAcademy/Outline"
{
Properties
{
}
CGINCLUDE
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
};
struct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
};
ENDCG
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
-----本体の描画処理-----
}
Pass
{
-----アウトラインの描画処理-----
}
}
}
このように、appdataやv2fといった共通部分はそれぞれのPass内に書く必要はありません。
本体部分
本体部分の描画に関しては、特にいうことはありません。アウトラインシェーダーならではという注意点もないので、Unlitでカラーを表示したり、テクスチャを張ってください。
テクスチャを張る(準備中)
アウトライン部分
アウトライン部分を描画する2Pass目について、解説していきます。2Pass目では、頂点シェーダーで頂点を外側に膨らませる必要があります。ただし、ただ膨らませるだけだと、本体部分に覆いかぶさってしまいます。ポイントは以下の3点です。
メッシュのFrontをCullingオフにする
通常は、法線が表に向いている方向を面として描画しますが、アウトライン部分のメッシュはあえて表面を描画せず、代わりに裏面を描画します。そのためにはメッシュのカリングを調整する必要があります。
メッシュのカリングをコントロールするには、Passの先頭にCullというキーワードを追加します。ここではさらに、Frontというキーワードによって、表面をカリングするように設定しています。
Pass
{
Cull Front //表面をカリング(描画しない)
CGPROGRAM
---------省略-------
EndCG
}
なお、Cullの後ろに設定できるキーワードについては、以下の記事をご覧ください。
オフセット方向を取り出す
float3 norm = normalize(mul((float3x3)UNITY_MATRIX_IT_MV, v.normal)); //モデル座標系の法線をビュー座標系に変換
float2 offset = TransformViewToProjection(norm.xy); //ビュー座標系に変換した法線を投影座標系に変換
UNITY_MATRIX_IT_MVは、「モデル⇒ビュー」変換の逆行列です。ITはIT = Inverse Transposed、MVはMV=Model Viewをそれぞれ意味しています。TransformViewToProjectionというマクロでは、ビュー座標系を投影座標系に変換します。
こうした座標系の変換は、シェーダーにおける非常に重要な基礎知識となります。もし変換工程が理解しにくいと感じましたら、ぜひこちらの記事をご覧ください。
ディスプレイ上でのアウトライン幅を一定にする
カメラ位置に関係なくアウトライン幅を一定にするためには、カメラからの深度情報を利用します。カメラが遠いほど値を大きくし、近いほど値を小さくする、という感じです。カメラからの深度情報を取得するには、UNITY_Z_0_FAR_FROM_CLIPSPACEというマクロを使用します。
o.pos.xy += offset * UNITY_Z_0_FAR_FROM_CLIPSPACE(o.pos.z); //法線方向に頂点位置を押し出し
逆に、ワールド空間を基準にアウトライン幅を固定したい場合は、こちらの部分を無効化してください。遠くから見た場合は薄く、近くから見た場合は太く、といったケースに有効です。
下のサンプルは、アウトライン幅を一定にしたものです。遠くから見た場合のほうが太く見えますが錯覚です笑。
シェーダー全体
以上をまとめたシェーダー全体は以下の通りです。
Shader "CAGraphicsAcademy/ToonOutline"
{
Properties
{
_MainTex("Base (RGB)", 2D) = "white" { }
_MainColor("Main Color", Color) = (.5,.5,.5,1)
_OutlineColor("Outline Color", Color) = (0,0,0,1)
_OutlineWidth("Outline Width", Range(0, 0.1)) = .005
}
CGINCLUDE
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
};
struct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
};
sampler2D _MainTex;
float4 _MainTex_ST;
uniform float4 _MainColor;
uniform float _OutlineWidth;
uniform float4 _OutlineColor;
ENDCG
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
Name "BASE" //本体部分を描画するパスの名前
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
v2f vert(appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex); //頂点をMVP行列変換
o.uv = TRANSFORM_TEX(v.uv, _MainTex); //テクスチャスケールとオフセットを加味
return o;
}
fixed4 frag(v2f i) : SV_Target
{
half4 c = tex2D(_MainTex, i.uv); //UVをもとにテクスチャカラーをサンプリング
c.rgb *= _MainColor; //ベースカラーを乗算
return c;
}
ENDCG
}
Pass
{
Name "OUTLINE" //アウトライン部分を描画するパスの名前
Cull Front //表面をカリング(描画しない)
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
v2f vert(appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex); //頂点をMVP行列変換
float3 norm = normalize(mul((float3x3)UNITY_MATRIX_IT_MV, v.normal)); //モデル座標系の法線をビュー座標系に変換
float2 offset = TransformViewToProjection(norm.xy); //ビュー座標系に変換した法線を投影座標系に変換
o.pos.xy += offset * UNITY_Z_0_FAR_FROM_CLIPSPACE(o.pos.z) * _OutlineWidth; //法線方向に頂点位置を押し出し
return o;
}
fixed4 frag (v2f i) : SV_Target
{
return _OutlineColor; //プロパティに設定したアウトラインカラーを表示
}
ENDCG
}
}
}
まとめ
アウトラインを描画するにはPassを二回書いて、「二度描きする」という点がポイントです。なお、UnityのURPではレンダリングパイプラインを改造することでもアウトラインを描画することができます。興味がある方はぜひ調べてみてください。