「moreover lwjgl」 13 世界をDiffuseな光で照らす(+アンチエイリアシング)
今までのコードでは2Dしかやっていなかったので、1次元上げて3Dの立方体を使います。
また、今までやったこと(テクスチャ、シェーダ)を使って、3Dっぽい感じの物体を表示したいと思います。
そのために今回作るのは、光です。
Diffuseは「拡散光」のことで、物体が光をどれほど吸収し、拡散的に反射するかというものです。つまり、物体に当たっているところが明るいとか、光に垂直なところが一番明るいとかそういう光のことです。「反射光」とは違います。
このDiffuseは、次の式で求められます。
Diffuse Factorは N・S ≧ 0 なら N・Sの値、N・S < 0 なら 0 。
Diffuse Lightがディフューズ処理した後の最終的な色で、Diffuse Intensityは光の強さです。Nは"normal"のことで、法線です。Sは光の向きです。
Diffuse Factro中の"・"はベクトルの内積を表しています。つまり、法線と光の向きの角度が少ないほど大きな値になり、また90度以上になると内積の値はマイナスになってしまうため、0以下は0にしています。(光にマイナスの値はないからです)
ソースコードはどちらも載せますが、Javaの方は今までとさほど変わっていないので、あまり説明しません。ただ、かってにアンチエイリアシング(マルチサンプリング)したので、それは説明します。
解説
Test8_LightUpDiffuseWorld.java
43~94行目 : 描画する物体の頂点座標、テクスチャ座標(UV)、法線です。法線は、ライティング(反射や拡散、屈折など)の時に光の進行方向を決めるベクトルで、基本的に物体の面に垂直な線にします。これを変えれば、物体のライティングの見え方を変えることが出来ます。
130行目 : GLFWのウィンドウ設定に、GLFW_SAMPLESというものがあり、マルチサンプリングの数を決めれます。
今まで表示させてきた物体はよく見るとすべてギザギザしているのがわかると思います。これをジャギーというのですが、ジャギーを取り除いて物体の境界を滑らかにする(正確には目立たなくする)ことをアンチエイリアシングといいます。OpenGLではアンチエイリアシングを、マルチサンプリングという方法で使えるようにできます。
マルチサンプリングは、まず1画素ごとにサンプル数を決めます。そしてそれらを画素中にランダムに散りばめ物体がどの数のサンプルを含んでいるかを計算して、その画素の透明度を変更するというものです。
(1例です)
アンチエイリアシングをすることで、表示される物体がなめらかで自然なものになります。
OpenGLでマルチサンプリングするには、GLFW側でサンプル数を決め、OpenGLでマルチサンプリングを有効にすることで出来ます。デフォルトは0です。つまり、透明度は0か1だけということになります。
176行目 : マルチサンプリングを使用可能にします。
177行目 : 深度テストを使用可能にします。
深度テスト(デプステスト)とは
OpenGLで3Dな物体を描画するとき、glDrawArraysやglDrawElementsなどは頂点座標やその指標の順番に描画していくので、視点の前に描画したい面が、順番により後ろの面が後で重なることで見えなくなったりすることがあります。
そのようなことがなくなるように、深度(物体の視点からの奥行き)を一つ一つに保存して、それらを描画するときには深度に応じて描画する順番を変えてくれるのが「深度テスト」です。
179行目 : マルチサンプリングを使用可能にします。
ここから先はあまり変わっていません。
Attributesに法線(Normal)が足されています。UniformなどはMVP行列、テクスチャサンプラーで同じです。
252~行目 : ライトの情報に関するUniformの値を決めています。GLSL側でvec4と宣言されていれば、OpenGL側では、"glUniform4f"を使えばよいです。
後は、法線のバッファーをVAOに結びつけ、描画する前に法線属性の使用を可能にすること(これ、よく忘れます)で、Javaの方は終わりです。
V3.vert + F3.frag
v2行目 : versionを330に指定します。後で使う"inverse"というメソッドは330でないと使えないっぽいです。
v8行目 : 平行光源の構造体を作ります。
構造体はJavaにはありません(その代わりに、プライベートなクラスを使えます)が、GLSLはCに準じているので、使うことが出来ます。
今回は、光源の座標、強さ、色をまとめています。
v14行目 : 構造体をUniform変数として宣言しています。実際にその値を入れるには、構造体自身ではなく、その中の変数ごとに代入します。
v19行目 : 今まではgl_FragColorなどで決めていた頂点ごとの色を、出力変数 vertColor としてフラグメントシェーダに渡します。
v21行目 : Diffuseシェーディングをする関数を宣言しています。
GLSL内でも、Javaと同じように関数(メソッド)を宣言できます。ただし、引数などを指定することも出来ます。
GLSLの関数における評価戦略は「値渡し(call by value-return)」というものだそうで、関数の引数を値で渡しなさいということです。また、引数に出力変数(out/inout)を指定すれば、関数が返るときに、関数内でのその変数の「値」が代入されて返ってきます。
引数に入出力について記述しなかった場合は、デフォルトでin修飾子が使われます。
v22行目 : 法線行列というものを作成しています。
法線行列(normal matrix, 正規行列が正式名称?)は、もし法線がワールド座標系(つまり、MVP変換した後の座標)にあり、かつ物体に拡大縮小が行われている場合、ModelView変換行列の左上3×3を逆転置した行列です。
これは、次のような考えからだそうです。
まず、法線をローカル座標系(MVP変換する前)からワールド座標系に移したいときは、ふつうの考えでは、
法線ベクトル × ModelView行列 = ワールド座標系
となりますが、法線は向きを表すベクトルで、拡大縮小が反映されてしまいます。また、変換行列中の4列目、平行移動
ベクトルは、法線を動かしてしまうと大きさも向きも変わってしまいます。
そこで、4列目を入れずに、次のように考えを変えます。
法線ベクトルは面に垂直だった場合、頂点と、その隣の頂点を結ぶ線分にも垂直になっています。その線分と同じ向きの正規化されたベクトルを接ベクトルといいます。*1
そう考えると、法線ベクトル(ワールド座標系)と接ベクトル(ワールド座標系)の内積は、間の角が90度なので0になります。
接ベクトルは法線ベクトルと違ってローカル座標系にModelView行列をかければワールド座標系になる(座標系に依存しないということ)ので、先ほどの式をこう変えられます。
法線ベクトル・接ベクトル = 法線ベクトル・(ローカル接ベクトル × ModelView行列)
= (ワールド法線ベクトル × ModelView行列の転置行列)・ローカルの接ベクトル(∵内積と転置行列の関係より 証明 )
つまり、ワールド座標系の法線ベクトルにするには、ModelView行列の転置行列を更に逆行列した物をかければいいということになります。
よくわかりません(;_;)。。。嘘言ってたらすみません
この話は一応、ESライブラリ && ゲームプログラミング バーテックスシェーダー編 - 第5回 ランバート照明モデル に話が載っていました。ありがとうございます。
なんでもいいですが、法線をワールド座標系にしたいときは、ModelView行列の「逆転置3×3」をかければ良いということです。
GLSLでは、"transpose"で転置、"inverse"で逆行列にしてくれます。
v23行目 : 法線の位置をワールド座標系に変換し正規化して、ライティングで使えるものにします。
normalizeはベクトルを正規化してくれるものです。
v24行目 : 頂点座標をワールド座標系にしています。
v26行目 : ライトはもとからワールド座標系なので、ライトの向きをライトの座標から頂点座標を引くことで求めます。
v27行目 : 先ほどのDiffuseを求める式より、ライトの向きと法線の内積からDiffuseの度合いを求めます。
内積はdotで、0以下を0に揃えるのは、maxを使います。(逆に0以上を0に揃えるのはminです)
v32行目 : 宣言したDiffuse計算関数を呼び出して、代入しています。
あんまりコードの量はなかったですが、解説がグダグダになっちゃいました。ダメですね。
あと、LWJGLはあんまり関係なかったです。前からですが。
一応、実行してみると、、、
このように、立方体に光を当てることが出来ました。しかし、これでは内積が0以下のところでは真っ暗になってしまいますし、輝きのようなものもありません。そこで次回はそれらを足したいと思います。
*1:接ベクトルは、実際には法線ベクトルに直角なベクトル一つで、面の接線としてのベクトルのことをいいますが、面が垂直と言っているので一応そうしてます。