Unityのシェーダでプロシージャルに髪のハイライトを作る方法です。
Unity2019.2.18
やりたいこと
この記事では下図のような感じの髪のハイライト表現をシェーダで作ります。
制約として、処理負荷はモバイルでも使えるくらい軽いものを目指すものとします。
binormalを使ってハイライトを作る
ブリンフォン鏡面反射モデルでは、ハーフベクトルと法線の内積を使ってスペキュラを表現します。
ここで、法線の代わりに従法線を使うと以下のような結果が得られます。
この性質を利用して、従法線をハーフベクトルの内積値を上手く加工してハイライト表現を作ってみます。
シェーダは以下のように書きます。
Shader "HairHighlight" { Properties { _Position("Position", float) = 0.3 _Sharpness("Sharpness", float) = 30 _Intensity("Intensity", Range(0.0, 1.0)) = 0.5 } SubShader { Tags{ "RenderType" = "Opaque" } Pass { Tags{ "LightMode" = "ForwardBase" } CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_fwdbase #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; half3 normal : NORMAL; half4 tangent : TANGENT; }; struct v2f { float4 pos : SV_POSITION; float3 worldPos : TEXCOORD1; half3 worldNormal : TEXCOORD2; half3 viewDir : TEXCOORD3; half3 binormal : TEXCOORD4; }; float _Position; float _Sharpness; float _Intensity; v2f vert(appdata v) { v2f o = (v2f)0; o.pos = UnityObjectToClipPos(v.vertex); o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; o.worldNormal = UnityObjectToWorldNormal(v.normal); o.viewDir = UnityWorldSpaceViewDir(o.worldPos); o.binormal = normalize(cross(v.normal.xyz, v.tangent.xyz) * v.tangent.w * unity_WorldTransformParams.w); o.binormal = mul(unity_ObjectToWorld, o.binormal); return o; } half4 frag(v2f i) : SV_Target { i.worldNormal = normalize(i.worldNormal); i.viewDir = normalize(i.viewDir); i.binormal = normalize(i.binormal); float3 lightDir = _WorldSpaceLightPos0.xyz; float3 halfDir = normalize(lightDir + i.viewDir); i.binormal = normalize(i.binormal - i.worldNormal * _Position); float bdoth = dot(i.binormal, halfDir) * 0.5 + 0.5; float highlight = bdoth * (1 - bdoth) * 4; highlight = pow(highlight, _Sharpness) * _Intensity; return highlight; } ENDCG } } }
結果は以下の通りとなります。髪のハイライトらしくなってきました。
jitterテクスチャを使う
さてこのままだとイマイチなので、jitterテクスチャを使ってハイライトの形状を変更してみます。
Shader "HairHighlight" { Properties { _Position("Position", float) = 0.3 _Sharpness("Sharpness", float) = 30 _Intensity("Intensity", Range(0.0, 1.0)) = 0.5 _Jitter("Jitter", 2D) = "black" {} _JitterIntensity("Jitter Intensity", Range(0.0, 1.0)) = 0.5 } SubShader { Tags{ "RenderType" = "Opaque" } Pass { Tags{ "LightMode" = "ForwardBase" } CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_fwdbase #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; half2 texcoord : TEXCOORD0; half3 normal : NORMAL; half4 tangent : TANGENT; }; struct v2f { float4 pos : SV_POSITION; half2 uv : TEXCOORD0; float3 worldPos : TEXCOORD1; half3 worldNormal : TEXCOORD2; half3 viewDir : TEXCOORD3; half3 binormal : TEXCOORD4; }; float _Position; float _Sharpness; float _Intensity; sampler2D _Jitter; float _JitterIntensity; v2f vert(appdata v) { v2f o = (v2f)0; o.pos = UnityObjectToClipPos(v.vertex); o.uv = v.texcoord; o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; o.worldNormal = UnityObjectToWorldNormal(v.normal); o.viewDir = UnityWorldSpaceViewDir(o.worldPos); o.binormal = normalize(cross(v.normal.xyz, v.tangent.xyz) * v.tangent.w * unity_WorldTransformParams.w); o.binormal = mul(unity_ObjectToWorld, o.binormal); return o; } half4 frag(v2f i) : SV_Target { i.worldNormal = normalize(i.worldNormal); i.viewDir = normalize(i.viewDir); i.binormal = normalize(i.binormal); float3 lightDir = _WorldSpaceLightPos0.xyz; float3 halfDir = normalize(lightDir + i.viewDir); half jitter = tex2D(_Jitter, i.uv).r; i.binormal = normalize(i.binormal - i.worldNormal * (_Position + (jitter * 0.5 - 0.5) * _JitterIntensity)); float bdoth = dot(i.binormal, halfDir) * 0.5 + 0.5; float highlight = bdoth * (1 - bdoth) * 4; highlight = pow(highlight, _Sharpness) * _Intensity; return highlight; } ENDCG } } }
Jitterテクスチャには以下のテクスチャを入れます。
ちなみにこのテクスチャは以下の方法で作りました。
これはプロシージャルに作るメリットも特にないので普通にPhotoshopとかで作ってもいいと思います。
結果
前節のシェーダを適用すると以下のような見た目になります。
髪のハイライト表現ができていることが確認できました。
ちなみにトゥーンシェーダと組み合わせると以下のような表現ができます(ちょっと処理変えてます)。
もちろんライトに応じてハイライトもいい感じに動きます。