【連載】Unity時代の3D入門 – 第7回「キューブマッピング」

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

第6回では、鏡面反射ライティングについて書きました。
第7回では映り込みを表現するキューブマッピングについて説明します。

映り込み

前回、表面が滑らかな物体では光は法線を挟んで反対側に鏡面反射することを説明しました。
07-01-00.png
この光の軌跡を視点側から辿ると、視点からある点に向かうベクトルを反射した先にある色を反映したものが物体表面の鏡面反射の色であるといえます。
07-02-00.png
例えば鏡のように、表面が十分に滑らかで反射率の高い物体は、反射先にある物体の色を反映するため、結果的にそれが映り込みになります。
07-03-00.png
前回行った鏡面反射ライティングは、反射先にあるライトの色のみ考慮していたのでいわゆるハイライトのみを表現する結果となりました。
しかし、鏡面反射光をより正確に捉えるならば、このような周囲の環境の映り込みを考慮する必要があります。

今回は、このような映り込み表現を考えていきます。

[原理]キューブマッピング

キューブマッピングは、このような映り込みをシミュレートする技法です。
概念としては、まずはあるモデル上の点の法線と視線ベクトルから、反射の方向を表すベクトルを求めます。
07-04-00.png
次に、空など、周囲の環境が壁に描かれた立方体の部屋を考えます。

この立方体は実際に作るわけではなく、立方体の展開図のようなテクスチャから計算します(後述)。
以下では便宜的に、実際に環境が描かれた立方体の部屋を作って説明します。
07-04-01.png
リソースはこちら を使わせていただいています。
まず、この部屋の中央に先ほどのモデルを、色を求めたい点が部屋の中央に来るように配置します。
07-05-00.png
次に、最初に求めた反射の方向にある壁の色を取得し、物体に反映します。
これにより、例えば反射先に空があれば物体に空が映り込むことになります。
07-06-00.png
これをシェーダで表現するには、上記の、環境が描かれた立方体の部屋を作る必要があります。
これは、下図ように立方体の展開図のようなテクスチャを作ることで実現します。
07-07-00.png
これをキューブマップといいます。
次節で説明しますが、キューブマップと、反射方向のベクトルをシェーダの関数に渡せば、その先にある色を取得することができます。

またマニュアルによると、キューブマップは様々なレイアウトで作ることができます。
07-08-00.png
このように展開図のようなレイアウトに加え、360度カメラでとったようなレイアウトにも対応しています。
07-09-00.png

[シェーダ]キューブマッピング

上記のキューブマッピングをシェーダで実現するには以下のように記述します。

Shader "Custom/Cubemapping"
{
    Properties {
        // キューブマップテクスチャのプロパティ
        _Cube ("Cube", CUBE) = "" {}
    }
    SubShader
    {
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

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

            struct v2f {
                float4 pos : SV_POSITION;
                half2 uv : TEXCOORD0;
                float3 pos2 : TEXCOORD1;
                half3 normal : TEXCOORD2;
            };

            // UNITY_SAMPLE_TEXCUBEで使用する変数を定義する
              UNITY_DECLARE_TEXCUBE(_Cube);

            v2f vert (appdata v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                o.pos2 = mul(unity_ObjectToWorld, v.vertex).xyz;
                o.normal = UnityObjectToWorldNormal(v.normal);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                i.normal = normalize(i.normal);
                half3 viewDir = normalize(_WorldSpaceCameraPos - i.pos2);
                // 視点からのベクトルと法線から反射方向のベクトルを計算する
                half3 reflDir = reflect(-viewDir, i.normal);

                // キューブマップと反射方向のベクトルから反射先の色を取得する
                half4 refColor = UNITY_SAMPLE_TEXCUBE(_Cube, reflDir);

                return half4(refColor.rgb, 1);
            }
            ENDCG
        }
    }
}

上記ではまず、キューブマップをインスペクタから設定できるようにプロパティを記述しています。

_Cube ("Cube", CUBE) = "" {}

また、UNITY_DECLARE_TEXCUBE マクロを用いて、キューブマップを保持する変数を定義しています。
これはこちらからダウンロードできるビルトインシェーダの HLSLSupport.cginc に定義されており、プラットフォームに応じて定義する変数を変えています。

UNITY_DECLARE_TEXCUBE(_Cube);

フラグメントシェーダでは視点からのベクトルを求め、reflect関数を用いて反射ベクトルを取得しています。
reflect関数はあるベクトルと法線を渡すことで、法線を挟んで反対側に反射するベクトルを取得できます。

half3 viewDir = normalize(_WorldSpaceCameraPos - i.pos2);
half3 reflDir = reflect(-viewDir, i.normal);

また、UNITY_SAMPLE_TEXCUBE を用いて、キューブマップから色を取得しています。
UNITY_SAMPLE_TEXCUBE は HLSLSupport.cginc に定義されており、キューブマップとベクトルを渡すことでベクトルの方向の色を取得することができます。

half4 refColor = UNITY_SAMPLE_TEXCUBE(_Cube, reflDir);

シェーダは以上です。
次に、キューブマップを作ります。
今回はこちらからテクスチャをお借りしました。
ダウンロードしたフォルダ内にあるjpgファイルを使用します。
exr という拡張子のファイルもありますが、こちらはHDR用のものとなりますので今回は使用しません。

これをUnityにインポートし、Import Settings の Texture Shape を Cube に変更します。
07-10-00.png
これでキューブマップは完成です。

それでは、先ほど作ったシェーダを適当なモデルに適用し、インスペクタからキューブマップを適用してみましょう。
07-11-00.png
このように、キューブマッピングされていることがわかります。

ライティング設定からキューブマップを取得する

次にこのキューブマップを Skybox にも適用してみます。
まず新しくマテリアルを作成して Skybox/Cubemap シェーダを選択し、インスペクタからキューブマップを設定します。
07-12-00.png
さらに Window > Lighting > Settings の Scene タブを選択し、Environment > Skybox Material にこのマテリアルを設定します。
07-13-00.png
これで Skybox が設定されました。
07-14-00.png
しかしこれでは、Lightingウィンドウにも各オブジェクトのマテリアルにもキューブマップを設定しなければならず、使い勝手が悪いです。
そのため、 Lightingウィンドウに設定されたキューブマップをシェーダ内で取得できるように修正します。

まず、Lightingウィンドウの Scene タブ、 Environment > Environment Reflections > Source を Custom に設定し、Skyboxと同じキューブマップをセットします。
07-15-00.png
次にシェーダを以下のように変更します。

Shader "Custom/CubemappingSkybox"
{
    SubShader
    {
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

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

            struct v2f {
                float4 pos : SV_POSITION;
                half2 uv : TEXCOORD0;
                float3 pos2 : TEXCOORD1;
                half3 normal : TEXCOORD2;
            };

            v2f vert (appdata v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                o.pos2 = mul(unity_ObjectToWorld, v.vertex).xyz;
                o.normal = UnityObjectToWorldNormal(v.normal);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                i.normal = normalize(i.normal);
                half3 viewDir = normalize(_WorldSpaceCameraPos - i.pos2);
                half3 reflDir = reflect(-viewDir, i.normal);

                // キューブマップと反射方向のベクトルから反射先の色を取得する
                half4 refColor = UNITY_SAMPLE_TEXCUBE(unity_SpecCube0, reflDir);

                return half4(refColor.rgb, 1);
            }
            ENDCG
        }
    }
}

変更点としては、プロパティと変数の定義を削除し、キューブマップの指定を unity_SpecCube0 にしています。
unity_SpecCube0 は UnityShaderVariables.cginc に定義された変数であり、環境の映り込みを表すキューブマップの情報が入ってきます。

これで Skybox からキューブマップを取得できるようになりました。

[補足]物体に環境が映り込む条件

今回作ったシェーダのように、物体に環境がはっきりと映り込むためには、物体の滑らかさと反射率が重要な要素となります。

滑らかさに関しては、物体の表面に細かい凹凸がないほどはっきりと環境が映り込みます。
また、物体表面が滑らかではなく、細かい凹凸が存在する場合には乱反射が起こります。
07-16-00.png
その結果、マット加工を施したアクセサリーのような見た目となり、ぼやけた感じの映り込みになります。
また、反射率に関しては、高い物体ほど当然映り込みがしやすくなります。

しかし反射率がそこまで高くない物体であっても、透過先が暗くて反射先が明るければ、映り込みが見えやすくなります。

これはハーフミラー(マジックミラー)を考えると理解しやすいです。
ハーフミラーはある面からみると鏡のように反射して見えて、他方の面からみるとガラスのように透けて見えます。
07-17-00.png
これは、ハーフミラーの反射率と部屋の暗さによるものです。

ハーフミラーは反射率と透過率がほぼ同じであるという性質を持ちます。
また、ハーフミラーを使用する際にはこれを明るい部屋と暗い部屋の仕切り板として使います。
このとき、明るい部屋から見ると、透過光は暗いため目立たず、反射光は明るいのでよく見えます。
07-18-00.png
また暗い部屋から見ると、透過光は明るいため目立ち、反射光はよく見えません。
07-19-00.png
これらの結果、片側から見ると反射して他方から見ると透過するハーフミラーができあがります。

このように、反射率が極端に高くない物体であっても、透過先が暗くて反射先が明るければ、映り込みが見えやすくなります。

より身近な物を例とするならば、このブログをスマートフォンでみている人は、電源ボタンを押して画面を暗くすると、自分の顔が写っている様子が見えやすくなることが確認できると思います。

まとめ

第7回では以下のことを説明しました。

  • 映り込みの仕組み
  • キューブマッピング
  • ライティング設定からキューブマップを取得する
  • 映り込みの条件

次回は応用として、これまで学んだ技術を用いて金属やプラスチックといった物質を表現します。

バックナンバー