第5回.(上級)法線転写による滑らかなシェーディング

はじめに

 今回は、頂点の法線を球体(Sphere)の法線に変形させることで、細部の陰影を弱め、顔に不要な陰が出ないようにするテクニックを紹介します。

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

  • VertexModifier関数でオブジェクト空間での法線を修正する

法線を編集する意味

 法線を編集して陰影を弱めるというのは、一体どういうことでしょうか。以下のコードを見てください。

float diffuse = saturate(dot(s.Normal, gi.light.dir));

 これは拡散反射光であるdiffuseを求めている式です。計算式についてはランバート反射 - Wikipediaでも見てもらうとして、一言でいえば"法線(面の向き)とライトの向きの間の角度で、面の明るさが決まる"ということです。
 法線とライトベクトルのなす角が0度の場合、つまり光が面に真正面から当たっている場合はCos(0度)=1なので100%で、光を90度傾けて真横から当てる場合はCos(90度)=0なので0%の明るさになります。これは普段生活していて、周りを観察してみればすぐに気づくかと思います。(裏側から当てた場合もちろん0%ですが、内積ではマイナスの値になるので、saturateで0-1に制限しています)
 つまり面の明るさを決定する要因は、"法線の向き"と"ライトの向き"の2要素しかありません。今回は法線の向きを修正して、明るさを調整します。

 ※これはトゥーンレンダリングにおいてのみ当てはまる話で、写実的レンダリングに関してはより多くの要素が出てきます。
 ※いきなりCosが出てきましたが、長さが1の二つのベクトルの内積は、ベクトル間のなす角をaとしたときのCos(a)に一致するからです。詳しくはググってください。

法線転写

 法線転写はもともとBlenderやMayaなどのモデリングソフト上で法線を編集する機能のようです。複雑な形状の法線を転写する場合シェーダーでは難しいですが、球体やカプセルなどのプリミティブの法線であれば簡単に計算可能です。
 モデリングで法線を転写するのに対し、シェーダーではリアルタイムに転写量を調整可能です。例えばギャグシーンなどでよくある、いきなり顔が劇画チックになる演出などに使えるかと思います。

ソースコード解説

 前回からの変更点を解説します。

Shader "Custom/Toon8" {
	Properties{
		_Color("Color", Color) = (1,1,1,1)
		_MainTex("Albedo (RGB)", 2D) = "white" {}
		_OffsetTex("Offset (RGB)", 2D) = "black" {}
		_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
		
		_EdgeColor("Edge Color", Color) = (0,0,0,0)
		_EdgeWidth("Edge Width",Range(0,0.03)) = 0.001

		_Sphericalize("Sphericalize(X,Y,Z,Blend)",Vector) = (0,0,0,0)
	}

 _Sphericalize(球体化)というベクトルのプロパティを追加しました。"Sphericalize(X,Y,Z,Blend)"のように、どの要素が何を表しているのかをラベルに書いておくと、マテリアル編集時に分かりやすくなります。この場合、球体の原点(XYZ)と転写量(0~1)を表しています。


SubShader
{
	Tags { "RenderType" = "Opaque" }
	LOD 200

	Cull off
	CGPROGRAM
	#pragma surface surf Toon fullforwardshadows vertex:vert
	#pragma target 3.0

 法線はvertという名前のVertexModifier関数で変更するので、vertex:vertを加えています。


sampler2D _MainTex;
sampler2D _RampTex;
sampler2D _OffsetTex;
float _GIPower;
float4 _Sphericalize;

 _Sphericalize変数も忘れずに定義しておきます。


void vert(inout appdata_full v)
{
	float3 sphereNormal = normalize(v.vertex - _Sphericalize.xyz);
	v.normal = normalize(lerp(v.normal,sphereNormal,_Sphericalize.w));
}

 VertexModifier関数です。まず最初に球体の原点(_Sphericalize.xyz)から頂点(v.vertex)へ向かうベクトルを求め、normalize関数で長さ1に正規化します。これでオブジェクト(スザンヌ)を球体に投影したときの法線sphereNormalが得られます。
 次にデフォルト(モデリング時)の法線v.normalとsphereNormalをlerp関数で線形補間(ブレンド)します。ブレンド量は_Sphericalize.wで指定しています。補間後のベクトルは長さが1でない可能性があるので、再びnormalize関数で長さ1のベクトルに正規化し、新しい法線の完成です。


ソース全文

Shader "Custom/Toon8" {
	Properties{
		_Color("Color", Color) = (1,1,1,1)
		_MainTex("Albedo (RGB)", 2D) = "white" {}
		_OffsetTex("Offset (RGB)", 2D) = "black" {}
		_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
		
		_EdgeColor("Edge Color", Color) = (0,0,0,0)
		_EdgeWidth("Edge Width",Range(0,0.03)) = 0.001

		_Sphericalize("Sphericalize(X,Y,Z,Blend)",Vector) = (0,0,0,0)
	}
		SubShader
		{
			Tags { "RenderType" = "Opaque" }
			LOD 200

			Cull off
			CGPROGRAM
			#pragma surface surf Toon fullforwardshadows vertex:vert
			#pragma target 3.0

			sampler2D _MainTex;
			sampler2D _RampTex;
			sampler2D _OffsetTex;
			float _GIPower;
			float4 _Sphericalize;

			struct Input {
				float2 uv_MainTex;
			};

			half _Glossiness;
			half _Metallic;
			fixed4 _Color;
			
			struct SurfaceOutputStandardUV
			{
				fixed3 Albedo;      // base (diffuse or specular) color
				fixed3 Normal;      // tangent space normal, if written
				half3 Emission;
				half Metallic;      // 0=non-metal, 1=metal
				half Smoothness;    // 0=rough, 1=smooth
				half Occlusion;     // occlusion (default 1)
				fixed Alpha;        // alpha for transparencies
				fixed2 uv;
			};

			#include "UnityPBSLighting.cginc"
			inline half4 LightingToon(SurfaceOutputStandardUV s, half3 viewDir, UnityGI gi)
			{
				float diffuse = saturate(dot(s.Normal, gi.light.dir));
				
				float3 offset = tex2D(_OffsetTex,s.uv).rgb;
				// 陰オフセット
				diffuse = saturate(diffuse - offset.r);
				// 明オフセット
				diffuse = saturate(diffuse + offset.g);

				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(SurfaceOutputStandardUV s, UnityGIInput data, inout UnityGI gi)
			{
				SurfaceOutputStandard s2;
				s2.Albedo = s.Albedo;
				s2.Normal = s.Normal;
				s2.Emission = s.Emission;
				s2.Metallic = s.Metallic;
				s2.Smoothness = s.Smoothness;
				s2.Occlusion = s.Occlusion;
				s2.Alpha = s.Alpha;

				LightingStandard_GI(s2, data, gi);
			}
			
			void vert(inout appdata_full v)
			{
				float3 sphereNormal = normalize(v.vertex - _Sphericalize.xyz);
				v.normal = normalize(lerp(v.normal,sphereNormal,_Sphericalize.w));
			}

			void surf(Input IN, inout SurfaceOutputStandardUV 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;
				o.uv = IN.uv_MainTex;
			}
			ENDCG

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

			float4 _EdgeColor;
			float _EdgeWidth;

			struct Input 
			{
				float2 uv_MainTex;
			};

			void vertexmod(inout appdata_full v)
			{
				float dist = length(mul(unity_WorldToObject,float4(_WorldSpaceCameraPos,1)).xyz);
				v.vertex += float4(v.normal * _EdgeWidth * v.color.r * dist, 0);
			}

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

結果

転写0% 転写20% 転写40% 転写60% 転写80%
f:id:IARIKE:20170419010731p:plain f:id:IARIKE:20170419010743p:plain f:id:IARIKE:20170419010753p:plain f:id:IARIKE:20170419010801p:plain f:id:IARIKE:20170419010808p:plain

 転写率を0~80%まで変更したものです。80%になると目の形がわからなくなってしまいますが、実際のキャラクターの場合目のテクスチャが貼ってあったり、まつ毛や眉毛があるのでちゃんと顔になります。美少女キャラクターの場合、陰は顎付近に入っていればいい(無くてもいい)ので、80%くらいの結果がいい感じなのです。

転写0% 転写100%
f:id:IARIKE:20170419011459p:plain f:id:IARIKE:20170419011511p:plain

 真横からライトを当てた場合の結果です。転写0%の場合、美少女キャラには不要な陰影のディティールが出てしまいますが、100%のときは真ん中から陰影が左右に分かれるため、それほど不気味にはなりません。(鼻の下の影は陰影オフセットによるものです)
 また体や服は形が複雑で、球体の法線は転写出来ないので、今回作成したシェーダーは顔専用になるかと思います。(あとキャラクターの場合、普通オブジェクトの原点は足元に来るというのもあって、このシェーダーは実用的ではありません)

おわりに

 今回は法線の調整を行いました。少しずつ数学的な要素が出てきましたが、使い方さえ知っていればとりあえずはオッケーです。難しいところは必要になったら勉強しましょう。