【連載】Unity時代の3D入門 – 第8回「 応用編 – 金属やプラスチックを表現する」

こんにちは、クライアントサイドエンジニアの矢野です。

第7回では、キューブマッピングについて書きました。
第8回ではこれまで紹介した技術を組み合わせて、金属や不透明なプラスチックといった物質をできるだけフォトリアルに表現してみます。
なお、今回のアウトプットは今までの連載で学んできた限られた技術のみで表現しているため、一般的にモデル化されているような手法でないことをご理解ください。

[原理] 直接光と金属性

まず直接光の影響について考えます。
直接光とは光源から直に届く光のことであり、第5回の拡散反射ライティングや第6回の鏡面反射ライティングがこの影響を表したものであるといえます。

復習となりますが、透過を考えないとき、物体に入射した光は一部が鏡面反射し、残りは物体の内部に入り拡散反射します。
つまりこのとき射出する光のうちの鏡面反射光の比率をmとすれば、拡散反射光の比率は1-mと表すことができます。
08-01-00.png
この値を計算に組み込めば、鏡面反射が強くなるほど拡散反射の強さが小さくなるような表現ができます。
また、一般的に金属は鏡面反射率が高く、非金属は低いため、このmは金属性の度合いとして捉えることができます。

[シェーダ] 直接光と金属性

これをシェーダで表すと次のようになります。

Shader "Custom/BasicMaterial"
{
    Properties
    {
        _MainTex ("Main Texture (RGB)", 2D) = "white" {}
        _Color ("Diffuse Tint (RGB)", Color) = (1.0, 1.0, 1.0, 1.0)
        _Shininess ("Shininess", float) = 10
        _Metalness ("Metalness", Range(0.0, 1.0)) = 0.0

    }
    SubShader
    {
        Pass
        {
            Tags { "LightMode"="ForwardBase" }

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                half2 uv : TEXCOORD0;
                half3 normal : NORMAL;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                half2 uv : TEXCOORD0;
                half3 normal : TEXCOORD1;
                half3 lightDir : TEXCOORD2;
                half3 halfDir : TEXCOORD3;
            };

            sampler2D _MainTex;
            half4 _MainTex_ST;
              half4 _Color;
            half4 _LightColor0;
            half _Shininess;
              half _Metalness;

            v2f vert (appdata v)
            {
                v2f o = (v2f)0;
                o.pos = UnityObjectToClipPos(v.vertex);
                half3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                o.normal = UnityObjectToWorldNormal(v.normal);
                half3 viewDir = normalize(_WorldSpaceCameraPos - worldPos);
                o.lightDir = normalize(_WorldSpaceLightPos0.xyz);
                o.halfDir = normalize(viewDir + o.lightDir);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                half4 base = tex2D(_MainTex, i.uv) * _Color;
                half3 specColor = lerp(1.0, base.rgb, _Metalness);

                // 拡散反射光
                half3 directDiff = base * (max(0.0, dot(i.normal, i.lightDir)) * _LightColor0.rgb);

                // 鏡面反射光
                half3 directSpec = pow(max(0.0, dot(i.normal, i.halfDir)), _Shininess) * _LightColor0.rgb * specColor;

                fixed4 col;
                // Metalnessに応じてブレンドする
                col.rgb = lerp(directDiff, directSpec, _Metalness);
                return col;
            }
            ENDCG
        }
    }
}

拡散反射光と鏡面反射光の計算については第5回第6回で行ったものと同じものです。
違う部分はプロパティとして _Metalness を定義していることと、最終的な色を求める際に、この値を用いて拡散反射光と鏡面反射光の影響を補間している点です。

col.rgb = lerp(directDiff, directSpec, _Metalness);

このシェーダを適用すると、次のようになります。

08-01-01.png
_Metalness = 0.5 の場合 / 背景は第7回と同様のものを使用

[原理] 間接光による鏡面反射

次に間接光の影響について考えます。
間接光とは光源から出た光が一回以上他の物体に反射してから入射する光のことです。

間接光による鏡面反射を表現するには、第7回のキューブマッピングを使います。
周囲の環境を描き込んだキューブマップを用意すれば、それをそのまま間接光であるとみなすことができます。
08-02-00.png
そのため、もし物体が完全に鏡面反射するものであれば、前回のようにキューブマッピングを行うだけで物質を表現できたこととなります。
前回の描画結果が鏡のように見えていたのはこのためです。

[原理] フレネル反射

しかしながら、鏡面反射率100%の物質はあまり現実的ではありません。
そこで今回は、間接光の計算にフレネル反射を考慮します。

フレネル反射とは、例えば水などの物体において、入射角が大きくなればなるほど反射率が高くなるといった現象です。

08-03-00.png

つまり、物体上の、視線を掠めるような部分が強く反射するようになります。
下記は次節の近似式を用いて、物体のフレネル反射率を可視化した例です。
08-04-00.png
このようにして求めたフレネル反射率を用いて、間接光の鏡面反射を表現します。

[シェーダ] 間接光による鏡面反射、フレネル反射

これらをシェーダで表現します。

Shader "Custom/BasicMaterial"
{
    Properties
    {
        _MainTex ("Main Texture (RGB)", 2D) = "white" {}
        _Color ("Diffuse Tint (RGB)", Color) = (1.0, 1.0, 1.0, 1.0)
        _Shininess ("Shininess", float) = 10
        _Metalness ("Metalness", Range(0.0, 1.0)) = 0.0

    }
    SubShader
    {
        Pass
        {
            Tags { "LightMode"="ForwardBase" }

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                half2 uv : TEXCOORD0;
                half3 normal : NORMAL;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                half2 uv : TEXCOORD0;
                half3 normal : TEXCOORD1;
                half3 lightDir : TEXCOORD2;
                half3 halfDir : TEXCOORD3;
                half3 viewDir : TEXCOORD4;
                half3 reflDir : TEXCOORD5;
            };

            // F0を定義
            #define F0        0.04f

            sampler2D _MainTex;
            half4 _MainTex_ST;
              half4 _Color;
            half4 _LightColor0;
            half _Shininess;
              half _Metalness;

            v2f vert (appdata v)
            {
                v2f o = (v2f)0;
                o.pos = UnityObjectToClipPos(v.vertex);
                half3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                o.normal = UnityObjectToWorldNormal(v.normal);
                o.viewDir = normalize(_WorldSpaceCameraPos - worldPos);
                o.lightDir = normalize(_WorldSpaceLightPos0.xyz);
                o.halfDir = normalize(o.viewDir + o.lightDir);

                // 反射ベクトル
                o.reflDir = reflect(-o.viewDir, o.normal);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                half4 base = tex2D(_MainTex, i.uv) * _Color;
                half3 specColor = lerp(1.0, base.rgb, _Metalness);

                half3 directDiff = base * (max(0.0, dot(i.normal, i.lightDir)) * _LightColor0.rgb);
                half3 directSpec = pow(max(0.0, dot(i.normal, i.halfDir)), _Shininess) * _LightColor0.rgb * specColor;

                // フレネル反射から反射率を求める
                half fresnel = F0 + (1 - F0) * pow(1 - dot(i.viewDir, i.normal), 5);
                half indirectRefl = lerp(fresnel, 1, _Metalness);

                // 間接光の鏡面反射
                half3 indirectSpec = UNITY_SAMPLE_TEXCUBE(unity_SpecCube0, i.reflDir) * specColor;

                fixed4 col;
                // Metalnessを乗算してから加算する
                col.rgb = lerp(directDiff, directSpec, _Metalness) + indirectSpec * indirectRefl;
                return col;
            }
            ENDCG
        }
    }
}

まず、フレネル反射率を求めます。
フレネル反射率は、Schlickの近似式により求めます。

この近似式は、θを入射角、入射角がゼロの時の反射率をF0としたとき、求めるべき反射率Frを次のように表します。
08-04-01.png
Wikipediaより

F0の値は、こちらの SIGGRAPH の資料によると、水が0.02、プラスチックが0.03〜0.05になるようです。
今回はプラスチックを表現するため、0.04に設定しています。

 #define F0        0.04f

cosθ はライト方向の単位ベクトルと法線の内積で求められますが、今回は視点からみたときの反射率を求めたいため、視線方向の単位ベクトルと法線との内積を計算しています。

half fresnel = F0 + (1 - F0) * pow(1 - dot(i.viewDir, i.normal), 5);

また、金属は入射角でそれほど反射率が変わらず、常に高い反射率を示す性質を持っています。
このため、非金属の場合は反射率をフレネル反射で求め、金属の場合は反射率を1とするように _Metalness で補間します。

half indirectRefl = lerp(fresnel, 1, _Metalness);

ここまでで、反射率を求めることができました。

次に、キューブマップをサンプリングして間接光の色を取得します。
反射ベクトルは第7回と同様の方法で求め、unity_SpecCube0 から色を取得します。

half3 indirectSpec = UNITY_SAMPLE_TEXCUBE(unity_SpecCube0, i.reflDir) * specColor;

こうして得られた反射色に先ほど求めた反射率を乗算し、直接光の値に加算することで間接光の影響をレンダリング結果に反映します。

col.rgb = lerp(directDiff, directSpec, _Metalness) + indirectSpec * indirectRefl;

これを適用すると次のようになります。
08-04-01.png
_Metalness = 0

フレネル反射の影響は右のオブジェクトの方がわかりやすいです。
陰になっている部分のうち、視線を掠めるようなところが水色に見えています。

これは、フレネル反射により空の色が間接光として反射されているためです。
さらに、間接光は直接光の作る陰の影響を受けないため、陰になっている部分で特に見えやすくなっています。

[原理] 間接光による拡散反射

次に、間接光による拡散反射について考えます。
直接光による拡散反射の場合と同じく、ある点の拡散反射の色を考えるときには、まずはその点に入射する光の色を求める必要があります。
間接光の場合にはこの点に様々な角度から光が入射するため、それら全ての影響を考える必要があります。
08-05-00.png
この光を計算するためには、様々な方向から入ってくる光に対して、入射角に応じた照度の低下を入射角のコサイン値を用いて計算して、それらを積分するという考え方が必要です。

しかし複雑なので、今回は非常にざっくりとした考え方でごまかしてしまおうと思います。

まず間接光を足し合わせた色について考えます。
様々な方向からの光の影響を考えるとき、垂直に差し込む光の影響がもっとも強くなり、入射角が大きくなるにつれて影響は弱くなります。
08-06-00.png
ということは、垂直方向の光の色を中心として周りの色を少しずつ合成していけば、間接光を足し合わせたような色になるはずです。
この「周りの色を少しずつ合成」する処理はペイントソフトのいわゆる「ぼかし」フィルタの処理と同様です。
そのため、キューブマップをぼかして法線方向でサンプリングすれば近しい効果が得られると考えられます。
08-07-00.png
今回はこのぼかしを、環境マップのミップマップを用いて実現してみます。

[原理] ミップマップ

ミップマップとは、同じ元画像から、解像度がだんだんと小さくなるような複数の画像を生成し、1つのテクスチャとしてまとめたものです。
08-08-00.png
このテクスチャから、必要に応じた解像度のものを取得して使用します。
たとえば、画面内にモデルがとても小さく描画されて、画面の1ピクセル内にテクセル(テクスチャの画素)が4個あるとします。
08-09-00.png
このとき、ピクセルの色には一番ピクセルの中心に近い色を取ったり、周囲4つのテクセルの色をブレンドしたりすることが考えられます。
しかしこれらの方法では、描画結果が正確性に欠けたり、処理負荷が大きかったりします。

そこで、縦横の幅が半分のミップマップを参照することで、あらかじめブレンドされた色を取得します。
以上がミップマップの仕組みです。

この解像度の度合いのことをミップマップレベルと呼び、シェーダではこのミップマップレベルを指定してテクスチャをサンプリングすることができます。
今回はこのミップマップレベルを用いて、前節で述べた色が平滑化されたようなテクスチャを得ます。

[シェーダ] 間接光による拡散反射、ミップマップ

間接光による拡散反射を適用したシェーダは以下のようになります。

Shader "Custom/BasicMaterial"
{
    Properties
    {
        _MainTex ("Main Texture (RGB)", 2D) = "white" {}
        _Color ("Diffuse Tint (RGB)", Color) = (1.0, 1.0, 1.0, 1.0)
        _Shininess ("Shininess", float) = 10
        _Metalness ("Metalness", Range(0.0, 1.0)) = 0.0
        _IndirectDiffRefl ("Indirect Diffuse Reflection", Range(0.0, 1.0)) = 1.0
    }
    SubShader
    {
        Pass
        {
            Tags { "LightMode"="ForwardBase" }

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                half2 uv : TEXCOORD0;
                half3 normal : NORMAL;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                half2 uv : TEXCOORD0;
                half3 normal : TEXCOORD1;
                half3 lightDir : TEXCOORD2;
                half3 halfDir : TEXCOORD3;
                half3 viewDir : TEXCOORD4;
                half3 reflDir : TEXCOORD5;
            };

            #define F0                            0.04f
            #define INDIRECT_DIFF_TEX_MIP        8.5f

            sampler2D _MainTex;
            half4 _MainTex_ST;
              half4 _Color;
            half4 _LightColor0;
            half _Shininess;
              half _Metalness;
              half _IndirectDiffRefl;

            v2f vert (appdata v)
            {
                v2f o = (v2f)0;
                o.pos = UnityObjectToClipPos(v.vertex);
                half3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                o.normal = UnityObjectToWorldNormal(v.normal);
                o.viewDir = normalize(_WorldSpaceCameraPos - worldPos);
                o.lightDir = normalize(_WorldSpaceLightPos0.xyz);
                o.halfDir = normalize(o.viewDir + o.lightDir);
                o.reflDir = reflect(-o.viewDir, o.normal);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                half4 base = tex2D(_MainTex, i.uv) * _Color;
                half3 specColor = lerp(1.0, base.rgb, _Metalness);

                half3 directDiff = base * (max(0.0, dot(i.normal, i.lightDir)) * _LightColor0.rgb);
                half3 directSpec = pow(max(0.0, dot(i.normal, i.halfDir)), _Shininess) * _LightColor0.rgb * specColor;

                half fresnel = F0 + (1 - F0) * pow(1 - dot(i.viewDir, i.normal), 5);
                half indirectRefl = lerp(fresnel, 1, _Metalness);

                // 間接光の拡散反射
                half3 indirectDiff = base * UNITY_SAMPLE_TEXCUBE_LOD(unity_SpecCube0, i.normal, INDIRECT_DIFF_TEX_MIP) * _IndirectDiffRefl;

                half3 indirectSpec = UNITY_SAMPLE_TEXCUBE(unity_SpecCube0, i.reflDir) * specColor;

                fixed4 col;
                col.rgb = lerp(directDiff, directSpec, _Metalness) + lerp(indirectDiff, indirectSpec, indirectRefl);
                return col;
            }
            ENDCG
        }
    }
}

間接光は UNITY_SAMPLE_TEXCUBE_LOD を使って、ミップマップレベルを指定して取得しています。
INDIRECT_DIFF_TEX_MIP はミップマップレベルで、今回はそれらしくぼやける値を定義しています。

UNITY_SAMPLE_TEXCUBE_LOD(unity_SpecCube0, i.normal, INDIRECT_DIFF_TEX_MIP)

また、今回は間接光の明るさを調整するために、_IndirectDiffRefl というプロパティを定義して拡散反射の計算時に乗算しています。

half3 indirectDiff = base * UNITY_SAMPLE_TEXCUBE_LOD(unity_SpecCube0, i.normal, INDIRECT_DIFF_TEX_MIP) * _IndirectDiffRefl;

最後にこれを間接光の鏡面反射の値と indirectRefl で補間して、間接光の影響とします。
これを適用するとレンダリング結果は次のようになります。
08-09-01.png
_Metalness = 0 / _IndirectDiffRefl = 0.66

反映前は黒くなっていた陰の部分に、なんとなく周囲の色が反映されてより背景に馴染んだ結果となりました。

[原理] 物体表面の粗さを表現する

次に、物体表面の粗さを表現します。
この影響については、特に鏡面反射光について視覚的に顕著であるため、今回は直接光と間接光の鏡面反射についてのみ考えます。

直接光の鏡面反射については第5回で触れた通り、表面が滑らかになるほど鏡面反射は局所的になります。
しかしながら、これはエネルギー保存の法則を考慮していません。

エネルギー保存の法則とは、エネルギー量の総和が変化しないという物理学の法則です。
これに従えば、鏡面反射する光の総量が変わらないとき、反射光が局所的になればなるほど明るくなり、反対に広範囲に広がれば暗くなっていきます。
08-10-00.png
そのため今回のシェーダでは、大雑把にではありますが、この現象を考慮して計算しています。

次に間接光の鏡面反射について考えます。
間接光の軌道を視点側から辿ると、物体表面が粗いほど、より広範囲の環境から影響を受けていることがわかります。
08-11-00.png
すなわちこれは、より広範囲のキューブマップ上の色を反映させる必要があるということです。
この光を計算するためには、様々な方向から入ってくる光に対して、視線を反射したベクトルと光源方向のベクトルの内積を計算して、Shininessに応じて累乗し、それらを積分するという考え方が必要です。

しかし拡散反射のときと同様やはり複雑なので、これもキューブマップのミップマップを用いて表現することにします。

[シェーダ] 物体表面の粗さを表現する

例のごとくまずはシェーダの全文です。

Shader "Custom/BasicMaterial-03"
{
    Properties
    {
        _MainTex ("Main Texture (RGB)", 2D) = "white" {}
        _Color ("Diffuse Tint (RGB)", Color) = (1.0, 1.0, 1.0, 1.0)
        _Metalness ("Metalness", Range(0.0, 1.0)) = 0.0
        _IndirectDiffRefl ("Indirect Diffuse Reflection", Range(0.0, 1.0)) = 1.0

        // Roughnessを追加
        // Shininessは削除
        _Roughness ("Roughness", Range(0.0, 1.0)) = 0.0
    }
    SubShader
    {
        Pass
        {
            Tags { "LightMode"="ForwardBase" }

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                half2 uv : TEXCOORD0;
                half3 normal : NORMAL;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                half2 uv : TEXCOORD0;
                half3 normal : TEXCOORD1;
                half3 lightDir : TEXCOORD2;
                half3 halfDir : TEXCOORD3;
                half3 viewDir : TEXCOORD4;
                half3 reflDir : TEXCOORD5;
            };

            #define F0                            0.04f
            #define INDIRECT_DIFF_TEX_MIP        8.5f
            #define MAX_SHININESS                50.0f
            #define SHININESS_POW                0.2f
            #define DIRECT_SPEC_ATTEN_MIN        0.2f
            #define MAX_MIP                        8.0f

            sampler2D _MainTex;
            half4 _MainTex_ST;
              half4 _Color;
            half4 _LightColor0;
              half _Metalness;
              half _IndirectDiffRefl;
              half _Roughness;

            v2f vert (appdata v)
            {
                v2f o = (v2f)0;
                o.pos = UnityObjectToClipPos(v.vertex);
                half3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                o.normal = UnityObjectToWorldNormal(v.normal);
                o.viewDir = normalize(_WorldSpaceCameraPos - worldPos);
                o.lightDir = normalize(_WorldSpaceLightPos0.xyz);
                o.halfDir = normalize(o.viewDir + o.lightDir);
                o.reflDir = reflect(-o.viewDir, o.normal);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                half4 base = tex2D(_MainTex, i.uv) * _Color;
                half3 specColor = lerp(1.0, base.rgb, _Metalness);

                half3 directDiff = base * (max(0.0, dot(i.normal, i.lightDir)) * _LightColor0.rgb);

                // 直接光の鏡面反射の計算を変更
                half shininess = lerp(MAX_SHININESS, 1.0, pow(_Roughness, SHININESS_POW));
                half directSpecAtten = lerp(DIRECT_SPEC_ATTEN_MIN, 1.0, shininess / MAX_SHININESS);
                half3 directSpec = pow(max(0.0, dot(i.normal, i.halfDir)), shininess) * _LightColor0.rgb * specColor * directSpecAtten;

                half fresnel = F0 + (1 - F0) * pow(1 - dot(i.viewDir, i.normal), 5);
                half indirectRefl = lerp(fresnel, 1, _Metalness);
                half3 indirectDiff = base * UNITY_SAMPLE_TEXCUBE_LOD(unity_SpecCube0, i.normal, INDIRECT_DIFF_TEX_MIP) * _IndirectDiffRefl;

                // 間接光の鏡面反射の計算を変更
                half3 indirectSpec = UNITY_SAMPLE_TEXCUBE_LOD(unity_SpecCube0, i.reflDir, _Roughness * MAX_MIP) * specColor;

                fixed4 col;
                col.rgb = lerp(directDiff, directSpec, _Metalness) + lerp(indirectDiff, indirectSpec, indirectRefl);
                return col;
            }
            ENDCG
        }
    }
}

プロパティとしては、Shininess を削除して Roughness を追加しています。

また、直接光の鏡面反射の計算を、Roughness が大きくなればなるほど広範囲に光が広がり、かつ色が薄くなるという影響を表すように変更しています。

half shininess = lerp(MAX_SHININESS, 1.0, pow(_Roughness, SHININESS_POW));
half directSpecAtten = lerp(DIRECT_SPEC_ATTEN_MIN, 1.0, shininess / MAX_SHININESS);
half3 directSpec = pow(max(0.0, dot(i.normal, i.halfDir)), shininess) * _LightColor0.rgb * specColor * directSpecAtten;

この部分は、計算自体に物理的な意味はありません。
Roughness を変数として shininess(鏡面反射光の鋭さ) と directSpecAtten(鏡面反射光が広範囲に広がることによる減衰)を求められるように式を作っています。
MAX_SHININESS や SHININESS_POW、DIRECT_SPEC_ATTEN_MIN といった値はそれらしくなるような値に設定しています。

また、間接光の鏡面反射の計算にも Roughness を適用しています。

half3 indirectSpec = UNITY_SAMPLE_TEXCUBE_LOD(unity_SpecCube0, i.reflDir, _Roughness * MAX_MIP) * specColor;

こちらは単純に _Roughness の値を大きくするほどミップマップレベルも大きくなり、キューブマップがぼやけるような実装にしています。

このシェーダを適用すると以下のようになります。
08-11-01.png
左: Metalness: 0.8 / IndirectDiffRefl: 0.66 / Roughness: 0.2
右: Metalness: 0.8 / IndirectDiffRefl: 0.66 / Roughness: 0.9

色々な物体を表現する

最後に、今回作ったシェーダを使って現実的な物体を表現してみます。
調整するパラメータは Metalness と Roughness に加え、物体の色となります。

また、表面が完全に滑らかなもの(Roughness = 0)や、完全に反射するもの(Metalness = 1)はあまり現実的に見えないので、少しパラメータをずらすとより現実的に見えます。

このようにして調整したものをレンダリングした結果が下図です。
08-12-00.png
左から
マットな銀色の金属(Metalness: 0.8 / IndirectDiffRefl: 0.66 / Roughness: 0.8)
赤いプラスチック(Metalness: 0.12 / IndirectDiffRefl: 0.66 / Roughness: 0.14)
金色の金属(Metalness: 0.805 / IndirectDiffRefl: 0.66 / Roughness: 0.123)
マットなプラスチック(Metalness: 0.06 / IndirectDiffRefl: 0.66 / Roughness: 1)

[発展]キューブマップのコンボリューション

今回のシェーダでは、間接光の光の影響をかなりざっくりと、キューブマップをミップマップによりぼかすことで表現しました。
しかしこの方法だとあまり綺麗にぼかすことができない上に、正確性に欠けます。

より正確に間接光を適用するためには、鏡面反射と拡散反射用にキューブマップを複製し、 Texture Importer から Convolution Type を変更します。
08-13-00.png
Convolution とはコンボリューション行列のことであり、簡単に言えばペイントソフトでいうところの「フィルター」の仕方です。
実際に、例えば Photoshop ではフィルター > その他 > カスタムから、また gimp ではフィルター > 汎用 > コンボリューション行列から行列を指定することができます。

Texture Importer では、Convolution Type に Specular と Diffuse を指定することができます。
Specular は反射先の色を、Diffuse は放射照度を事前計算してテクスチャに焼き付けます。
マニュアルにもう少し詳しく書いてありますので、興味がある方は参照してください。

また、実際に使われているところを確認するためには、適当なシーンで Lighting Window の Environment > Environment Reflections > Source を Skybox にした上で Generate Lighting ボタンを押下します。
この結果、Sceneファイルと同じ階層にフォルダが生成され、その中にリフレクションのキューブマップが生成されますが、このTexuture Importer を見ると Convolution Type が Specular になっていることが確認できます。

まとめ

今回は応用編ということで、技術的に新しい内容は少ないものの、基本的な物質を表現する上での考え方を中心に書きました。

  • 金属性
  • フレネル反射
  • 間接光による拡散反射
  • ミップマップ
  • 間接光による鏡面反射
  • 表面の粗さの表現
  • キューブマップのコンボリューション

レンダリングの目的はあくまで今回のように物質などの表現をすることであるため、区切りとして今回の応用編を挟みました。
連載はもう少し続く予定です。

バックナンバー