Unityで複数ライトによるライティングを行う方法


さてさて、今回もWikiBooks翻訳ライクな記事ですw
(最近はWikiBooksのチュートリアルをやりまくるのが日課なのでw)

ただ、普通に翻訳するのでは意味がないので、読んで自分なりに理解したところをメモとして書いていく感じです。

まず、Unityのシェーダでライトを扱うにはuniform変数としてライトの位置、色などを渡してもらわないとなりません。

ライトを使う場合にはシェーダ内でuniform変数として以下のように宣言します。

//ワールド座標系でのライトの位置
uniform vec4 _WorldSpaceLightPos0;

//ひとつめのライトの色
uniform vec4 _LightColor0;

ライトの数を設定する

どうも、ライトの数が複数ある場合は設定を変更しないとデフォルトの数(多分2?)しか使えないようです。
設定方法は、[Edit] > [Project Settings] > [Quality]から、 Pixel Light Count を必要数に変更する必要があるようです。

Settingの場所

Pixel Light Count

Diffuse Reflection(ディフューズライト)

さて、今回説明するのはディフューズライトの仕組みです。
ディフューズライトは光沢のない、いわゆる「陰」を作るためのライティング手法です。
概念としては、各頂点の法線に対して光がどれくらいの角度から照らしているか、を元に光の強さを計算する方法です。図で表すと、WikiBooksから拝借すると以下のようなものです。

ここでNは頂点の法線ベクトル、Lは光の方向です。
光の強さは角度から求めます。0度のときはまったく光があたっておらず、90度のときが一番強くあたっている状態になります。
角度はベクトルの内積から簡単に求めることができます。
式にすると

a \cdot b = |a||b|\cos\theta

となります。(a、bはそれぞれベクトル、θはaとbの成す角度です)
ちなみに余談ですが、内積は英語で「dot product」と呼び、そのため式は「・(ドット)」を使って表します。
さらに外積は「cross product」と呼び、その場合は「×(クロス)」を使って表します。

そしてこれらの計算を行うときは通常、ベクトルを正規化して(1にして)計算するため、右辺の|a||b|は1 x 1 = 1で、cosθのみが残ります。つまり、内積の計算結果がそのまま角度(ラジアン)として算出できるわけです。

※|a|はベクトルaの長さを意味します(ノルムと呼びます)。計算方法は、各成分の二乗を足したものの平方根を取ります。 $\sqrt{x * x + y * y + z * z}$

シェーダ

さて、ではWikiBooksに掲載されているシェーダソースを解説していきたいと思います。

大まかなフローは、

  1. Unityから渡される情報を使って適切に座標変換する
  2. ライトと法線のベクトル計算を行う
  3. 上で説明した内積を用いたライトのあたり具合を計算する
  4. それを計算結果の色として適用する

という流れになります。
WikiBooksに計算されているソースは以下。

Shader "GLSL per-vertex diffuse lighting" {
   Properties {
      _Color ("Diffuse Material Color", Color) = (1,1,1,1) 
   }
   SubShader {
      Pass {    
         Tags { "LightMode" = "ForwardBase" } 
            // pass for first light source

         GLSLPROGRAM

         uniform vec4 _Color; // shader property specified by users

         // The following built-in uniforms (except _LightColor0) 
         // are also defined in "UnityCG.glslinc", 
         // i.e. one could #include "UnityCG.glslinc" 
         uniform mat4 _Object2World; // model matrix
         uniform mat4 _World2Object; // inverse model matrix
         uniform vec4 _WorldSpaceLightPos0; 
            // direction to or position of light source
         uniform vec4 _LightColor0; 
            // color of light source (from "Lighting.cginc")

         varying vec4 color; 
            // the diffuse lighting computed in the vertex shader

         #ifdef VERTEX

         void main()
         {                              
            mat4 modelMatrix = _Object2World;
            mat4 modelMatrixInverse = _World2Object; // unity_Scale.w 
                // is unnecessary because we normalize vectors

            vec3 normalDirection = normalize(
               vec3(vec4(gl_Normal, 0.0) * modelMatrixInverse));
            vec3 lightDirection = normalize(
               vec3(_WorldSpaceLightPos0));

            vec3 diffuseReflection = vec3(_LightColor0) * vec3(_Color) 
               * max(0.0, dot(normalDirection, lightDirection));

            color = vec4(diffuseReflection, 1.0);
            gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
         }

         #endif

         #ifdef FRAGMENT

         void main()
         {
            gl_FragColor = color;
         }

         #endif

         ENDGLSL
      }

      Pass {    
         Tags { "LightMode" = "ForwardAdd" } 
            // pass for additional light sources
         Blend One One // additive blending 

         GLSLPROGRAM

         uniform vec4 _Color; // shader property specified by users

         // The following built-in uniforms (except _LightColor0) 
         // are also defined in "UnityCG.glslinc", 
         // i.e. one could #include "UnityCG.glslinc" 
         uniform mat4 _Object2World; // model matrix
         uniform mat4 _World2Object; // inverse model matrix
         uniform vec4 _WorldSpaceLightPos0; 
            // direction to or position of light source
         uniform vec4 _LightColor0; 
            // color of light source (from "Lighting.cginc")

         varying vec4 color; 
            // the diffuse lighting computed in the vertex shader

         #ifdef VERTEX

         void main()
         {                              
            mat4 modelMatrix = _Object2World;
            mat4 modelMatrixInverse = _World2Object; // unity_Scale.w 
               // is unnecessary because we normalize vectors

            vec3 normalDirection = normalize(
               vec3(vec4(gl_Normal, 0.0) * modelMatrixInverse));
            vec3 lightDirection = normalize(
               vec3(_WorldSpaceLightPos0));

            vec3 diffuseReflection = vec3(_LightColor0) * vec3(_Color) 
               * max(0.0, dot(normalDirection, lightDirection));

            color = vec4(diffuseReflection, 1.0);
            gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
         }

         #endif

         #ifdef FRAGMENT

         void main()
         {
            gl_FragColor = color;
         }

         #endif

         ENDGLSL
      }
   } 
   // The definition of a fallback shader should be commented out 
   // during development:
   // Fallback "Diffuse"
}

TagsやBlendについては前に書いたUnityのGLSLで半透明を扱うで解説しているので詳しくはそちらを見てください。

Passをふたつ準備する

まず大事なポイントとして、(ソースを見れば分かりますが)Passをふたつ記述しています。
さらに、それぞれの内部のTagsが少し異なります。

1つ目がTags { "LightMode" = "ForwardBase" }、2つ目がTags { "LightMode" = "ForwardAdd" }になっています。
2つ目は「Add」と書かれていることから分かる通り、複数ライトを合成するために「Add」になっているようです。

続いてGLSL部分。よく見ると、実はどちらもまったく同じですね。
あとは、ライティング部分の計算ができれば終わりです。

そして幸いなことに、フラグメントシェーダではgl_FragColor = color;しか書いていません。
つまりライティングと色の計算はすべてバーテックスシェーダで行っている、というわけです。

さて、では問題のバーテックスシェーダはこちら。

vertex-shader.vs
void main()
 {                              
    mat4 modelMatrix = _Object2World;
    mat4 modelMatrixInverse = _World2Object;
    // unity_Scale.w 
    // is unnecessary because we normalize vectors

    vec3 normalDirection = normalize(vec3(vec4(gl_Normal, 0.0) * modelMatrixInverse));
    vec3 lightDirection = normalize(vec3(_WorldSpaceLightPos0));

    vec3 diffuseReflection = vec3(_LightColor0) * vec3(_Color)
    * max(0.0, dot(normalDirection, lightDirection));

    color = vec4(diffuseReflection, 1.0);
    gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
 }

コード自体はそれほど長くありません。
特に最初の数行はただの変数の宣言です。
_Object2World_World2ObjectはどちらもUnityから渡される、モデルの変換行列です。
(2つ目はその逆行列)
それをたんにローカル変数にコピーしているだけですね。

大事な点はnormalDirectionlightDirectiondiffuseReflectionの3つの計算です。

法線方向の算出

最初のnormalDirectionはそのまま法線の方向です。
法線の方向は、Unityから法線のベクトル情報が渡される(gl_Normal)ので、それを元に計算します。
通常、各頂点はモデル変換行列を掛けることでワールド空間座標に変換しますが、諸々の事情により法線はその逆行列を掛ける必要があります。理由についてはこちらの記事が参考になります。法線の変換の話

さて、これでワールド空間上の法線方向が求まりました。
続いて、ワールド空間座標に置けるライトの方向を求めます。

ライト方向の算出

次はlightDirectionです。といっても、実はもうUnityがその情報を渡してくれています。
_WorldSpaceLightPos0です。そのままの名前ですね。
そしてそれを正規化(normalize)することで方向が得られます。

ライトのあたり具合を計算する

最後はdiffuseReflectionです。ここがライトのあたり具合を計算している箇所ですね。
式はvec3(_LightColor0) * vec3(_Color) * max(0.0, dot(normalDirection, lightDirection));です。

これは2つの部分に分けることができ、vec3(_LightColor0) * vec3(_Color)max(0.0, dot(normalDirection, lightDirection))です。
前半部分は単純にライトの色とGUIから設定できるシェーダ自身に設定された色を掛けあわせています。つまり、ただの色の計算です。

後半がライトのあたり具合の計算です。実は色々やってますが、これだけです。
maxはGLSLの組み込み関数で、指定された2つの数字のうち、大きい方を返してくれます。
つまりここでは0以上の値になるようにしているわけですね。

続いてdot、これは内積を計算してくれる、これまたGLSLの組み込み関数です。
内積の意味については冒頭で説明しました。つまり、冒頭の説明の計算はここで行われているわけですね。

内積の結果はただの数値になります。算出される数値の意味は、冒頭の説明通り角度(ラジアン)です。
(内積自体の計算はとても簡単で、ふたつのベクトルがあったとしたらv1x * v2x + v1y * v2y + v1z * v2zと、すべての成分を掛けて足しあわせたものです)

そして2つのベクトルの成す角が90度なら0、0度なら1になります。max関数により、0〜1の範囲にしているわけですね。
そして0度、つまり一番ライトが強くあたっている場合に1を、そこから角度が90度になるにつれて数値が小さくなっていくわけです。(90度で0、つまり色は黒になる)

この式の意味するところは、実はライティングと言いつつも「どれだけライトがあたっていないかを計算すること」だったんですね。

実際にこのシェーダを適用したのが以下です。しっかり陰がついてますね。