第2回.(初級)反転ポリゴン押し出し法で輪郭線を出す

はじめに

 今回は反転ポリゴン押し出し法で輪郭線(エッジ)を出してみようと思います。
前回のシェーダーに追加のパスとして、輪郭線パスを加えます。

本記事で解説される新要素

パスとは

 パスとは、メッシュ(ポリゴン)を1回描画すること(またそれに付随した設定のこと)です。メッシュを2回描画する必要がある処理なら2パス、3回描画する必要があるなら3パスということになります。
 今回はトゥーンシェーディングされたメッシュに加えて、輪郭線を別メッシュとして描画するので「2パス」ということになります。前回のシェーダは1パスだったので、2つ目のパス(輪郭線)を追加します。

ソースコード

 まずソースコードから見ていきましょう。

Shader "Custom/Toon" {
	Properties{
		_Color("Color", Color) = (1,1,1,1)
		_MainTex("Albedo (RGB)", 2D) = "white" {}
		_Glossiness("Smoothness", Range(0,1)) = 0.5
		_Metallic("Metallic", Range(0,1)) = 0.0
		_RampTex("Ramp (RGB)", 2D) = "white" {}
		_GIPower("GI Power",Range(0,1)) = 0.1

		_EdgeWidth("Edge Width",Range(0,0.01)) = 0.001
	}
		SubShader
		{
			Tags { "RenderType" = "Opaque" }
			LOD 200

			CGPROGRAM
			#pragma surface surf Toon fullforwardshadows
			#pragma target 3.0

			sampler2D _MainTex;
			sampler2D _RampTex;
			float _GIPower;

			struct Input {
				float2 uv_MainTex;
			};

			half _Glossiness;
			half _Metallic;
			fixed4 _Color;

			#include "UnityPBSLighting.cginc"
			inline half4 LightingToon(SurfaceOutputStandard s, half3 viewDir, UnityGI gi)
			{
				float diffuse = saturate(dot(s.Normal, gi.light.dir));
				float3 ramp = tex2D(_RampTex, float2(1 - diffuse, 0.5)).rgb;

				return float4(s.Albedo * ramp + s.Albedo * gi.indirect.diffuse * _GIPower, 0);
			}
			inline void LightingToon_GI(SurfaceOutputStandard s, UnityGIInput data, inout UnityGI gi)
			{
				LightingStandard_GI(s, data, gi);
			}
			void surf(Input IN, inout SurfaceOutputStandard o) {
				// Albedo comes from a texture tinted by color
				fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
				o.Albedo = c.rgb;
				// Metallic and smoothness come from slider variables
				o.Alpha = c.a;
			}
			ENDCG

			// ここから輪郭線パス
			Cull front
			CGPROGRAM
			#include "UnityCG.cginc"
			#pragma surface surf Standard fullforwardshadows vertex:vertexmod
			#pragma target 3.0

			float _EdgeWidth;

			struct Input 
			{
				float2 uv_MainTex;
			};

			void vertexmod(inout appdata_base v)
			{
				v.vertex += float4(v.normal * _EdgeWidth, 0);
			}

			void surf(Input IN, inout SurfaceOutputStandard o) 
			{
				o.Albedo = 0;
				o.Alpha = 0;
			}
			ENDCG
		}
		FallBack "Diffuse"
}

 このシェーダでは、以下のようなレンダリング結果が得られます。
f:id:IARIKE:20170408100032p:plain

ソースコード解説

 長いので、前回からの変更点を小分けにして見ていきます。

_EdgeWidth("Edge Width",Range(0,0.01)) = 0.001

 _EdgeWidthというプロパティを追加しました。これは輪郭線の太さをコントロールするための数値になります。

// ここから輪郭線パス
Cull front

 Cull Frontという行が新しく出てきました。これはカリング(裏側のポリゴンを描画しないことで最適化する)を設定する行です。何も書かない場合のデフォルトはCull Backで、これはカメラに対して法線が裏側を向いている面は描画しない、という設定です。
 例えばキャラクターのスカートを描画することを考えたとき、通常スカートのポリゴンは外側を向いていると考えられます(つまり法線が外側を向いています)。キャラクターを挟んで後ろ側のスカートのポリゴンを描画するとき、カメラからはスカートの裏地が見えていることになりますが、そのポリゴンの法線は外側を向いているため、Cull backの設定では描画されません。スカートの裏地も描画されてほしい場合は、Cull Offでカリングをオフにします。
 今回のCull Frontという設定にした場合、スカートの表側は表示されず、裏地のみ表示される状態になります。

Cull Back Cull Front Cull Off
f:id:IARIKE:20170408104312p:plain f:id:IARIKE:20170408104330p:plain f:id:IARIKE:20170408104342p:plain


 ポリゴンが完全に閉じている場合(Solid:ソリッド,WaterTight:防水、などと言われるモデル)は、Cull OffでもCull Backでも見た目上の変化はありません。たとえ裏面ポリゴンが描画されても、表側のポリゴンがカメラに対して必ず手前側に表示され、上書きされるためです。



CGPROGRAM
#include "UnityCG.cginc"
#pragma surface surf Standard fullforwardshadows vertex:vertexmod
#pragma target 3.0

 次に新しく出てきたのは、vertex:vertexmodという部分です。
これは頂点モディファイア関数と呼ばれているもので、頂点の位置や法線の向きなどをモディファイ(Modify:修正、変更する)するために用意された仕組みです。
 この中で、"vertexmod"は私が勝手に名前を付けた関数名で、vertex:の後に指定します。

float _EdgeWidth;

 プロパティで追加した変数をここで実際に定義しています。これを書き忘れてコンパイルエラーになることが多いので、注意しましょう。

struct Input 
{
	float2 uv_MainTex;
};

 この部分は特に変わりありません。

void vertexmod(inout appdata_base v)
{
	v.vertex += float4(v.normal * _EdgeWidth, 0);
}

 これが頂点モディファイア関数の中身になります。頂点の座標を法線方向に_EdgeWidth分だけ膨らませていることがわかると思います。このv.vertexは座標変換前であり、オブジェクト空間(モデル空間)になります。v.normalも同様です。(座標変換に関しては別に解説を作ります)
 何をやっているのか一言でいえば「モデルを太らせている」です。

 またappdata_baseという構造体は、UnityCG.cgincの中に宣言されており、先ほど解説していなかった
#include "UnityCG.cginc"
の行でインクルードされています。これはUnity - Download Archiveのダウンロード->ビルトインシェーダでダウンロードできるzipファイルの中に含まれており、中身を見ることができます(インクルードするだけならDLする必要はありません)

void surf(Input IN, inout SurfaceOutputStandard o) 
{
	o.Albedo = 0;
	o.Alpha = 0;
}

 これは輪郭線のSurfaceシェーダーです。ライティング方法はデフォルトのStandardのままなので、色やラフネス、メタリックを設定すればその通り、物理ベースシェーダとしてライティングされます。
 とりあえず輪郭線は黒でいいので、アルベドに0を入れて黒くなるようにしています。

反転ポリゴン押し出し法

 コードの解説は終わったので、どういう仕組みでこの結果がレンダリングされるのかを考えてみましょう。

  • まず1パス目で普通にレンダリングします。当然ですが輪郭線はありません。

f:id:IARIKE:20170408101651p:plain

  • 2パス目の最初に頂点モディファイアで「モデルを太らせ」ます。つまり2パス目は1パス目よりもシルエットが大きくなります。
  • カリング設定をCull Back,Cull Frontにした場合、ただ太ったモデルが真っ黒に表示されるだけです

f:id:IARIKE:20170408101709p:plain

  • しかしCull Front設定をしているため、太ったモデルの表側は表示されず、ちょうどシルエットが増えた部分の裏側だけ表示されます。(シルエット部分以外は1パス目のピクセルが手前にあるので、輪郭線メッシュの裏側はZバッファテストにより表示されなくなる)

f:id:IARIKE:20170408101831p:plain

おわりに

 今回は輪郭線描画でよく使われる手法を解説しました。SurfaceShaderのライティングが物理ベースであったり、色が黒だけであったり、太さを部分ごとに調整できなかったりと、このままでは色々と未完成です。そのあたりは今後応用編などで解説したいと思います。