第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" }
結果
オフセットなし | オフセットあり |
どうでしょうか。少し彫りの深い顔立ちのスザンヌちゃんが、明るくなって柔らかい印象になったかと思います。また、鼻の下や口周りに陰オフセット(R)を入れてメリハリを出しています。(ライトの方向や強さは変えていません)
UVガイド入りですが、オフセットテクスチャは以下のようなものです(UVは手抜きで、正面方向からの投影で作っています)
おわりに
トゥーンシェーダではこういった細かな調整が数多く必要になります。これら一つ一つの調整を、筆で絵を描いているのと同じ感覚で追加していき、最終的な絵を完成させることになります。
これは配布されている(市販の)トゥーンシェーダを使うだけでは絶対に実現不可能です。いくらモデリングが素晴らしくても、テクスチャが緻密でも、最終的に絵を描画するのはあくまでシェーダだということを忘れてはいけません。せっかくそこまで頑張ったのに、どうしてシェーダだけ人任せなのでしょうか?それで本当に表現し切ったと言えるのでしょうか?
あなたの思い描く理想のキャラクター表現を実現するためにも、ぜひシェーダを使いこなせるようになりましょう!