【Unity】【シェーダ】プロシージャルな髪の異方性ハイライト(天使の輪)表現を作る方法

Unityのシェーダでプロシージャルに髪のハイライトを作る方法です。

Unity2019.2.18

やりたいこと

この記事では下図のような感じの髪のハイライト表現をシェーダで作ります。

f:id:halya_11:20200120003405p:plain
ハイライト

f:id:halya_11:20210225201948g:plain
適用したもの
制約として、処理負荷はモバイルでも使えるくらい軽いものを目指すものとします。

binormalを使ってハイライトを作る

ブリンフォン鏡面反射モデルでは、ハーフベクトルと法線の内積を使ってスペキュラを表現します。

f:id:halya_11:20200120001733p:plain
スペキュラ

ここで、法線の代わりに従法線を使うと以下のような結果が得られます。

f:id:halya_11:20200120001904p:plain
従法線を使う

この性質を利用して、従法線をハーフベクトルの内積値を上手く加工してハイライト表現を作ってみます。
シェーダは以下のように書きます。

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

結果は以下の通りとなります。髪のハイライトらしくなってきました。

f:id:halya_11:20200120002130p:plain
結果

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テクスチャには以下のテクスチャを入れます。

f:id:halya_11:20200120003247p:plain
jitter

ちなみにこのテクスチャは以下の方法で作りました。

light11.hatenadiary.com

これはプロシージャルに作るメリットも特にないので普通にPhotoshopとかで作ってもいいと思います。

結果

前節のシェーダを適用すると以下のような見た目になります。

f:id:halya_11:20200120003405p:plain
適用

髪のハイライト表現ができていることが確認できました。

ちなみにトゥーンシェーダと組み合わせると以下のような表現ができます(ちょっと処理変えてます)。

f:id:halya_11:20200119225417p:plain
トゥーンシェーダとの組み合わせ

もちろんライトに応じてハイライトもいい感じに動きます。

f:id:halya_11:20210225201948g:plain
動く

関連

light11.hatenadiary.com