第3回.(中級)オフセットテクスチャで陰影を調整する

はじめに

 今回はオフセットテクスチャを使って陰影を調整してみます。

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

  • SurfaceShaderの入出力構造体をカスタマイズする

オフセットテクスチャとは

 Ramp処理を施す前(陰影をクッキリさせる前)に、シェーディング結果をオフセットさせ、最終的な陰影を調整するためのものです。例えば美少女キャラクターの顔にはなるべく陰が出てほしくない、しかし顎から首にかけては常に陰になっていて欲しい、という要望に応えるために用います。

ソースコード解説

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

Shader "Custom/ToonOffset" {

 名前をToonからToonOffsetにしました。


_MainTex("Albedo (RGB)", 2D) = "white" {}
_OffsetTex("Offset (RGB)", 2D) = "black" {}

 プロパティの_MainTexの下に_OffsetTexを追加しました。デフォルトが"black"になっているのは、真っ黒のテクスチャを標準とするという意味です。今回はR(赤チャンネル)とG(緑チャンネル)にオフセット量を格納するため、黒なら全く影響を与えないテクスチャということになります。


sampler2D _OffsetTex;

 サンプラーを追加することも忘れずに。


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;
};

 SurfaceOutputStandardUVという構造体を宣言しています。
 カスタムライティング関数ではUV値を参照できないため、この構造体にuv項目を追加しています。元の構造体はUnity - マニュアル: サーフェスシェーダーの記述に記載されています。


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);
}

 ここが今回のハイライトになります。
 diffuseに対し、Rは減算(暗くする)、Gは加算(明るくする)計算を行っています。また0-1の範囲を超えないように、saturateでクランプしています。(2行に分けましたが、1行でも処理できますね。また0.5=128をバイアス値としておけば、1チャンネルでも実現できるはずです)


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);
}

 LightingToon関数では構造体の型の違いが問題になりませんでしたが、LightingToon_GIではビルトインのLightingStandard_GIという関数を用いているため、値をコピーしてもともとの構造体(SurfaceOutputStandard)に戻してあげる必要がありました。少し面倒な部分です。


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;
}

Surfaceシェーダではuv値をコピーする処理を追加しています。


ソース

 ソースコード全文です。

Shader "Custom/ToonOffset" {
	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

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

			Cull off
			CGPROGRAM
			#pragma surface surf Toon fullforwardshadows
			#pragma target 3.0

			sampler2D _MainTex;
			sampler2D _RampTex;
			sampler2D _OffsetTex;
			float _GIPower;

			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);
				// 陰オフセット
				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 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

			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:20170415142520p:plain f:id:IARIKE:20170415142529p:plain

 どうでしょうか。少し彫りの深い顔立ちのスザンヌちゃんが、明るくなって柔らかい印象になったかと思います。また、鼻の下や口周りに陰オフセット(R)を入れてメリハリを出しています。(ライトの方向や強さは変えていません)

UVガイド入りですが、オフセットテクスチャは以下のようなものです(UVは手抜きで、正面方向からの投影で作っています)
f:id:IARIKE:20170415143427p:plain

おわりに

 トゥーンシェーダではこういった細かな調整が数多く必要になります。これら一つ一つの調整を、筆で絵を描いているのと同じ感覚で追加していき、最終的な絵を完成させることになります。
 これは配布されている(市販の)トゥーンシェーダを使うだけでは絶対に実現不可能です。いくらモデリングが素晴らしくても、テクスチャが緻密でも、最終的に絵を描画するのはあくまでシェーダだということを忘れてはいけません。せっかくそこまで頑張ったのに、どうしてシェーダだけ人任せなのでしょうか?それで本当に表現し切ったと言えるのでしょうか?
 あなたの思い描く理想のキャラクター表現を実現するためにも、ぜひシェーダを使いこなせるようになりましょう!