【Unity】【シェーダ】ノーマルマップのZ値を利用した発光表現テクニック

UnityのシェーダでノーマルマップのZ値を使って情報量を増やした発光表現を紹介します。

Unity2019.2.10

やりたいこと

いま、下図のようなモデルが描画されているとします。

f:id:halya_11:20191206004253p:plain

ゲームでは、武器やキャラクターの演出で輪郭部分を発光させることがよくあります。
この場合よく行われる実装が視線ベクトルと法線の内積を使って云々する方法です。
シェーダで表現するとこんな感じになります。

Shader "Example"
{
    Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        _MetallicMap ("Metallic", 2D) = "white" {}
        _BumpMap("Normal Map"  , 2D) = "bump" {}
        _BumpScale("Normal Scale", Range(0, 1)) = 1.0
        [HDR]_EmissionColor("Emission Color", Color) = (1,1,1,1)
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }

        CGPROGRAM
        #pragma surface surf Standard

        struct Input
        {
            float2 uv_MainTex;
            float3 viewDir;
        };

        fixed4 _Color;
        sampler2D _MainTex;
        sampler2D _MetallicMap;
        sampler2D _BumpMap;
        half _BumpScale;
        float4 _EmissionColor;

        void surf (Input IN, inout SurfaceOutputStandard o)
        {
            fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
            fixed4 m = tex2D(_MetallicMap, IN.uv_MainTex);
            fixed4 n = tex2D(_BumpMap, IN.uv_MainTex);

            o.Albedo = c.rgb;
            o.Metallic = m.r;
            o.Smoothness = m.a;
            o.Alpha = c.a;
            o.Normal = UnpackScaleNormal(n, _BumpScale);

            // 視線ベクトルと法線の内積を使って輪郭を取っている
            half factor = pow(1.0 - max(0, dot(o.Normal, IN.viewDir)), 4);
            o.Emission = factor * _EmissionColor;
        }
        ENDCG
    }
}

手抜きしてPBRしたかったのでSurface Shaderを使っていますが、記事の本筋には関係ないです。
コメントが書いてある部分にだけ注目してください。

レンダリング結果は以下のようになります。

f:id:halya_11:20191206004823p:plain

この方法はシンプルでいいですが、輪郭表現もシンプルで情報量が少ない印象です。

ノーマルマップのZ値を使って情報量を増やす

そこで、ノーマルマップのZ値を利用することで情報量を増やします。

ノーマルマップの性質を考えると、Z値が1の部分は「平ら」な部分であると考えられます。
逆に言うと(Unpack後の)Z値が0に近い部分は「溝」やら「エッジ」やらを表しているといえます。
そこで、Z値が小さい部分を発光させる処理を追加してみます。

Shader "Example"
{
    Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        _MetallicMap ("Metallic", 2D) = "white" {}
        _BumpMap("Normal Map"  , 2D) = "bump" {}
        _BumpScale("Normal Scale", Range(0, 1)) = 1.0
        [HDR]_EmissionColor("Emission Color", Color) = (1,1,1,1)
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }

        CGPROGRAM
        #pragma surface surf Standard

        struct Input
        {
            float2 uv_MainTex;
            float3 viewDir;
        };

        fixed4 _Color;
        sampler2D _MainTex;
        sampler2D _MetallicMap;
        sampler2D _BumpMap;
        half _BumpScale;
        float4 _EmissionColor;

        void surf (Input IN, inout SurfaceOutputStandard o)
        {
            fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
            fixed4 m = tex2D(_MetallicMap, IN.uv_MainTex);
            fixed4 n = tex2D(_BumpMap, IN.uv_MainTex);

            o.Albedo = c.rgb;
            o.Metallic = m.r;
            o.Smoothness = m.a;
            o.Alpha = c.a;
            o.Normal = UnpackScaleNormal(n, _BumpScale);

            half factor1 = pow(1.0 - max(0, dot(o.Normal, IN.viewDir)), 4);
            // ノーマルマップのZ値が小さいところほど光るように程よく調整
            half factor2 = 1.0 - pow(o.Normal.z, 10);
            half factor = max(factor1, factor2);
            o.Emission = factor * _EmissionColor;
        }
        ENDCG
    }
}

powを掛けて調整していますが、要はノーマルマップのZ値が小さいところほど光るようにしているだけです。

レンダリング結果

この処理を適用するとレンダリング結果は以下のように変わります。

f:id:halya_11:20191206005941p:plain

情報量が増えて細かい溝の部分まで発光するようになりました。

関連

light11.hatenadiary.com

light11.hatenadiary.com