株式会社デジタル・フロンティア-Digital Frontier

Header

Main

  • TOP
  • DF TALK
  • 【Unity】 ~Unityでシェーダーを書いてみる~:その2【初心者向け】

【Unity】 ~Unityでシェーダーを書いてみる~:その2【初心者向け】

2016/8/8

Tag: ,

お久しぶりです。開発室の辻です。

前回の記事からだいぶ時間が空いてしまいましたが、今回はUnityでシェーダーを書いてみるその2です。
前回は頂点・フラグメントシェーダーの流れについて説明したので、今回はモデルに陰影をつけるところをやっていきます。

内容的には拡散反射のDiffuse鏡面反射のSpecularを出す所までです。
シェーダーはShading(=陰影付け)を行うものなので、○○反射という名前がつく上の2つの計算は、どちらも光の反射を計算して、どこがどれぐらい明るくなるのかを計算するものです。
最近は物理ベースのシェーダーが映像・ゲーム問わずスタンダードになってきている風潮はありますが、今回は特にそういった要素には触れず、簡単なところからやっていきます。

DiffuseとSpecularを描画する

今回は先に結果を出します。
自分の好きな名前のシェーダーファイルを作成して、以下のソースをコピペしてください。
作成の仕方は前回の記事を参照して頂けたらと思います。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
Shader "Custom/DF_TALK2"
{
    //Unityのinspectorに表示されるアトリビュートを設定
    Properties {
        _DiffuseColor ("DiffuseColor", Color) = (1.0, 1.0, 1.0, 1.0)
        _SpecularColor ("SpecularColor", Color) = (1.0, 1.0, 1.0, 1.0)
        _Shininess ("Shininess", Range(0.0, 500.0)) = 100.0
    }
    SubShader {
        Cull Back ZWrite On ZTest LEqual
        //このshaderのレンダリングされる順序や種類を設定
 
        Pass{
            Tags{ "Queue"="Geometry" "RenderType"="Opaque" "LightMode"="ForwardBase"}
            CGPROGRAM
 
            //vs,fsのエントリー関数名の宣言
            #pragma vertex vert
            #pragma fragment frag
            #pragma target 3.0
 
            #include "UnityCG.cginc"
 
            //Propertiesで宣言した名前をここで再度宣言することで使用できるようになる。
            float4 _DiffuseColor;
            float4 _SpecularColor;
            float _Shininess;
 
            //頂点シェーダーの出力構造体
            struct vertexOutput{
                float4 P   : SV_POSITION; // 座標変換後の位置
                float3 N   : NORMAL; // 法線ベクトル
                float3 L   : TEXCOORD2; // ライトベクトル
                float3 V   : TEXCOORD3; // 視線ベクトル
            };
 
            //頂点に対しての処理
            vertexOutput vert(appdata_base v)
            {
                //出力する情報
                vertexOutput output;
                //座標変換処理
                output.P = mul (UNITY_MATRIX_MVP, v.vertex);
                //法線ベクトル
                output.N = UnityObjectToWorldNormal(v.normal);
                //ライトベクトル
                output.L = WorldSpaceLightDir(v.vertex);
                //視線ベクトル
                output.V = WorldSpaceViewDir(v.vertex);
 
                return output;
            }
            //こっちはピクセル単位の処理
            float4 frag(vertexOutput input) : COLOR
            {
                float3 L = normalize(input.L);
                float3 N = normalize(input.N);
                float3 V = normalize(input.V);
                //ライトの正反射ベクトル
                float3 R = reflect(-L, N);
 
                //Diffuse計算
                float4 diffuseRaw = clamp(dot(N, L), 0.0, 1.0);
                float4 diffuse = diffuseRaw * _DiffuseColor;
 
                //Specular計算
                float specularReflection = clamp(dot(V, R), 0.0, 1.0);
                float4 specular = pow(specularReflection,  _Shininess) * _SpecularColor;
 
                //出力結果計算
                float4 resultColor = diffuse + specular;
                return resultColor;
            }
            ENDCG
        }
    }
    FallBack "Diffuse"
}

コピペしたシェーダーファイルを保存して、任意のマテリアルに設定します。
僕はDF_TALK2という名前のマテリアルにしました。

設定したら自分の好きなオブジェクトに割り当てます。単純なSphereとかで大丈夫です。
後はDirectionalLightを一つ置くとこんな感じの結果になります。

Diffuseについて

Diffuseとは日本語で拡散反射のことで、物体の表面のある一点に入射した光が、物体の表面で吸収され、内部で反射して全方位に散乱する反射のことです。
今回実装したDiffuseの計算は、光がどのように拡散反射するのかをライトの向きオブジェクトの面の向き=法線の内積を使用したもので、Lambert反射と言われています。

計算は以下のようになっています。

62
63
64
//Diffuse計算
float4 diffuseRaw = clamp(dot(N, L), 0.0, 1.0);
float4 diffuse = diffuseRaw * _DiffuseColor * _DiffuseWeight;

ポイントは、clamp(dot(N, L), 0.0, 1.0);の部分で、ここが先ほど述べたライトの向きと法線の内積を計算している部分です。
dot関数に2つのベクトル(ライトの向きと、法線どちらもベクトルです)を入力することで内積を出力してくれます。

正しい結果を得る為に、入力するベクトルは正規化されている必要がありますが、これは58行目から行っているnormalize関数で行っています。
正規化はベクトルの長さを1にする計算処理のことです。
詳しい説明は割愛しますが、これを行っておくことによってdot関数の出力が扱いやすい値になってくれます。

clamp関数は、第一引数に入力した結果を第二引数以下の値は第二引数の値に、第三引数以上の値は第三引数の値にしてくれます。
簡潔に言うと、指定の値より大きく、又は小さくならないようにしてくれる関数です。
上記の例では0.0以下は0.0に、1.0以上は1.0になります。

ライトの向きと法線の情報は、頂点シェーダーで計算しています。

44
45
46
47
//法線ベクトル
output.N = UnityObjectToWorldNormal(v.normal);
//ライトベクトル
output.L = WorldSpaceLightDir(v.vertex);

法線ベクトルはセマンティクスから取得したものをUnityObjectToWorldNormal()でWorld座標に変換しています。
v.normalは、入力であるappdata_baseで、NORMALというセマンティクスが設定されており、
そのオブジェクトの法線情報を持ってきてくれます。

ライトべクトルは、WorldSpaceLightDir()から取得しています。
WorldSpaceLightDir()は、入力した座標からライトへのベクトルを取得してくれます。

両者とも、公式のドキュメントに説明があります
WorldSpaceLightDir()はUnityのビルトイン関数で、UnityCG.cgincをincludeすることによって使えるようになります。

ライト⇒入力座標へのベクトルではなく、入力座標⇒ライトのベクトルであることに注意してください。
これは内積計算の際にこの向きでないと正しく計算できない為です。

Lambert反射モデルでは、ライトを真正面から受け取る=法線と向きが同じに近い面が最も明るく、ライトの向きに対して法線が垂直に近づくほど反射は弱くなっていきます。

内積の値は-1~1の範囲を取りますが、0以下の値は光が当たっていないということになるので、実際に使用しているのは0~1の範囲で、マイナスの値を使用してしまわないように先ほど紹介したclamp関数を使用しています。

diffuseだけを表示すると以下のような結果になります。

この値をカラーに乗算することで、画像のような陰影をつける事が出来ます。
今回はDiffuseColorというアトリビュートを乗算しているので、DiffuseColorの値を変えるとオブジェクトの色が変わるのが確認できます。

Specularについて

Specularは日本語で鏡面反射のことで、物体の表面で反射される光のことを指します。
鏡面、という名前が付いている通り、入ってきた光をそのまま跳ね返すため、反射の向きに指向性があるという点がDiffuseと違っています。

今回実装したものは鏡面反射によって出来るハイライトの部分を計算したものす。
ライトの向きと法線、そして視線の方向を使って計算を行っており、面で反射した光のベクトルが、視線のベクトルに近いほど光が強くなるように計算しています。
これはPhongの反射モデルのSpecularの計算を使用しています。

Specularの計算を行っているのは以下の部分です。

66
67
68
//Specular計算
float specularReflection = clamp(dot(V, R), 0.0, 1.0);
float4 specular = pow(specularReflection,  _Shininess) * _Specular;

Specularでは法線とライトの向きから求めた反射ベクトルと、視線ベクトルの内積を計算しています。
内積を計算しているのはDiffuseと同様ですが、こちらは視線のベクトルを使用しているため、見る方向によって結果が変わってきます。

図にすると以下の様な感じです。ライトの反射したベクトルと、視線のベクトルの向きが近い程結果が明るくなります。

視線ベクトルと反射ベクトルは以下の部分で計算しています。

48
49
//視線ベクトル
output.V = WorldSpaceViewDir(v.vertex);
59
60
//ライトの正反射ベクトル
float3 R = reflect(-L, N);

WorldSpaceViewDir()はWorldSpaceLightDir()と似た関数で、入力座標からカメラまでの座標を取得してくれます。
reflect()は第二引数のベクトル(法線)に対して反射したベクトルを返してくれるビルトイン関数です。
Lにマイナスがついて居るのは、ここでの計算にはWorldSpaceLightDir()で取得出来るベクトルと逆向きの、ライト⇒入力座標のベクトルが必要だからです。

この計算によって求められた結果を単体で表示すると以下のようになります。

今回は計算された結果に対してShininessというアトリビュートでべき乗計算を行っています。
べき乗の計算は1未満の値に行うと結果が小さくなっていくので、べき乗する値を増やすことで、Specularの表示される範囲を絞る事ができます。

適当な大小2つの値をSpecularのみを表示して比較すると、以下のようになります。

まとめ

以上で、今回の実装は終了です。
今回実装したDiffuseとSpecularは、法線や視線ベクトルといった様々な向きの情報を用いて計算しました。
簡単なものでしたが、どのような計算をするのかというざっくりとした印象はつかめたのではないかと思います。

また僕個人の考えですが、シェーダーを学習する際に大事なのは計算の内容と同じぐらい、その計算に使用する情報がどこから持ってこれるかを知ること、だと思います。

シェーダーの処理を計算方法と、計算に使用する情報はどこから持ってくるのかという二点に関して分けて考えておくと、Unity以外の環境に行った際にも、同じことをするためにどの情報を取得できればよいか等の判断ができるようになるはずです。

例えば、WorldSpaceLightDir()関数はUnityでしか使えませんが、ある座標からライトへのベクトルを求められれば、今回のようなシェーディングが行えることは既にわかったと思います。
なので、他の環境で同じことをしようと思った際には、その環境である座標からライトへのベクトルの求め方を調べればいいわけです。

これを意識するだけで、様々な環境のシェーダーコードの見え方が変わると思います。

セマンティクスに関する補足

少し疑問に思われた方も居ると思われますが、ライトと視線のベクトル情報を頂点シェーダーからフラグメントシェーダーに渡す際に、TEXCOORDのセマンティクスを使用しています。
これは、セマンティクスをつけておかないと、頂点シェーダーからフラグメントシェーダーへの値の引き渡しができないからです。
TEXCOORDは通常uvの情報を渡すセマンティクスですが、uvのように2チャンネル(float2)のものだけでなく、float4までの値を格納することができます。
Unity側で使用されるのは基本的にTEXCOORD0で、2つ目のuvを使用する場合はTEXCOORD1が使用されます。ですので今回は、TEXCOORD2と3を使ってライトと視線のベクトル情報をフラグメントシェーダーに投げています。
Unityのセマンティクスについてのページ頂点シェーダー出力とフラグメントシェーダー入力のところにあるように、頂点シェーダーの出力で位置情報等のデータを渡す際にはTEXCOORD[n]を使用するのが一般的なようです。

終わりに

今回作成したシェーダーは、とても簡素な実装なので、複数のライトや影に関する実装はなされていません。
そういった内容に関しては、また次の機会に紹介しようと思います。

ここまで読んで頂きありがとうございます。何か一つでも皆様のプラスになれば幸いです。

※免責事項※
本記事内で公開している全ての手法・コードの有用性、安全性について、当方は一切の保証を与えるものではありません。
これらのコードを使用したことによって引き起こる直接的、間接的な損害に対し、当方は一切責任を負うものではありません。
自己責任でご使用ください。

コメント

コメントはありません

コメントフォーム

*

コメントは承認制ですので、即時に反映されません。ご了承ください。

CAPTCHA