シェーディング

今回はWebGLというよりは一般的なCGにおけるシェーディングについてのお話
と言っても、最終的に自分の作った独自のシェーディング手法の解説をいたします
何がどう違うのかを説明するためにも、まずは一般的なものから解説します
独自のシェーディングについてはまた次回に分けさせてもらいます
リアルタイム3DCGがどのように色付けされるのか気になる方にも是非どうぞ

このページではなるべくプログラムではなくシェーディングの一般論の話をしたいので
プログラムの解説は最小限に抑えたいと思います
(まあウチのサイトは概要の説明ばかりだが)


シェーディングとは?

シェーディングとはどんなものであるか?CGに興味がある人なら知っているかもしれないが
興味ない人は知らないかもしれない

シェーディングは言葉のとおり、物体にシェードをつける作業です
・・・つまりどういうことよ?
日本語ではシェードと言う言葉にぴったり当てはまるような単語が無いのでちょっと日本人にはピンと来ませんね

物に光が当たると影ができますよね?
影には2種類あります
シェードとシャドウです
3DCGではシェードは簡単に計算できるのですがシャドウは簡単には描けません
その理由は説明を聞けば分かるでしょう

ちなみに今でこそシェーディングと言うとCGの世界ですがその起源となる考え方は絵画の世界で、コンピュータができるずっと前から存在してたりします
なんと機械を使わず手計算で画面に頂点を配置し、面の明るさを計算して描かれた絵があるんですね
ある意味3DCGは写実絵画の延長にあるわけです


シェードとは

何かの物体に光を当てると、当然ですが光が当たる面と当たらない面ができますね
光が当たっていないところは当然暗くなりますし、明るいところにも強く光が当たっているところとやや暗いところなどがあります
シェードとはこの光の当たり方によってできる陰影の事を指します

シャドウとは

シェードが光の当たり具合を示すものなら、シャドウとは光の遮りかたを示すものといえます
何かの物体に光が当たれば、その物体の奥にあるものは光が遮られてしまって暗くなりますね
この光の遮断によってできる影をシャドウといいます
しかし、リアルタイム3DCGでは基本的に他の物体との位置関係を無視して計算を行います
なぜならそれらを計算するにはとても時間が掛かるからです
ですのでシャドウは簡単には表現できないんですね
ゲームなんかでシャドウが描かれていたりするのはちょっと特殊なことをしています
ここではシェーディングの話なのでシャドウについては解説しません



反射モデルについて

シェーディングを行うには物体がどんな風に光を反射するのかを定義する必要があります
それが反射モデルです
一番有名なフォンの反射モデルは次のような形です

 反射光=拡散反射光+鏡面反射光+環境光 ・・・式1

厳密に言うとこんな曖昧な書き方じゃないんですが概念としてみると大体こんな感じ

拡散反射光(Diffuse)

拡散反射光とは物体表面で微細凹凸やら、光の浸透やらなんか色々が起こってありとあらゆる角度へ反射する光です
物理的な現象は複雑なのですが、重要なのは拡散反射は光源と物体の位置関係によって明るさが決まるという点と、反射光が物体の色の影響を受けると言うことですかね

鏡面反射光(Specular)

鏡面反射光は名前のとおり、鏡に当たって跳ね返ったようにまっすぐ反射される光です
表面のツヤに当たる光ですね
拡散反射と違って、鏡面反射では表面の色の影響を受けることはあまりありません(絶対ではないんですが)
入ってきた光がほぼそのまま出て行くので、拡散反射とは別の計算をします

拡散反射光は物体と光源の位置から明るさが決まるのに対して、鏡面反射光は見ている人の位置によっても明るさが変わっていく という特徴を持ちます

環境光(ambient)

環境光とは、そんなもの実在しません(おいおい)
どういうことかというと、この光は物体の周りにある色んな他の物体(壁であったり、テーブルだったり、地面やポットとか何でも)に当たって跳ね返った光のことで、その跳ね返った光を擬似的に表したものです
周囲の環境から来る光、ということで環境光です
昼間部屋のカーテンを開けると電気が無くても部屋が明るいですよね?それは環境光があるからなのです
なぜ擬似的な光なんて使うのか?それは、他の物体と光源との位置関係や遮蔽やら色んな計算を膨大に行わないと計算できないからです
そういった複雑な計算を行った描画はグローバルイルミネーション、GIと呼ばれるのですがここではリアルタイムレンダリングの話なので解説は避けます
このニセモンの環境光は周囲360度ありとあらゆる方向から照らされる光、と定義されています


シルエット

先ずはもっとも簡単な形とも言える最小限のシェーダ
なにも色をつけないシェーディング(というかシェーディングとはいえないけど)
とりあえず物体を真っ白に塗ってみます

サンプルページ

モデルがくるくる回ってますね
でも真っ白です
まだ何も色に関する計算をしていないのでこのようになります

まずはこの真っ白の物体に色を着けてみましょう
マテリアル設定を追加してみます
といってもWebGLにマテリアルというものはありません(多分)
シェーダにuniform変数Mcolorを追加します
この変数を変えることでマテリアルとします

サンプルページ


シェーダのソースコード
描画の本体プログラムのほうは解説するとかなり複雑なので端折りますスミマセン
ドラゴンは緑、ウサギは白、仏像は黄色に塗ってみました

わざわざ一度バーテックスシェーダからフラグメントシェーダに色を渡してますね
これは、現在のプログラムならばuniform変数Mcolorをフラグメントシェーダに書いてもいいんですけど
後々バーテックスシェーダで必要になるのであえてこうなってます

いまさらながらシェーダについての簡単な説明
バーテックスシェーダは頂点シェーダとも呼ばれ、名のとおり頂点位置を出力します
フラグメントシェーダはピクセルシェーダとも呼ばれ、ピクセルに塗る色を決定します
基本的にはそれだけです
gl_Positionが頂点位置、gl_FragColorが色ですね
変数の前にattributeがつくのは頂点ごとに設定が必要なデータ
uniformがつくのは全ての計算に共通するデータだと思っていただいて構いません
varyingはバーテックスシェーダとフラグメントシェーダでデータを受け渡すのに使います
あとは大体C++みたいなものです

ディレクショナルライト(平行光源)

今度はようやくシェーディング、影付けの作業ですね
先ほどは一色で塗っていましたが、今度は影を描いて見ましょう
ちょっとデッサンみたいですね
影を付けて立体感を出します

でもどうやって影を付ければいいのでしょう?鉛筆で描き込むことはできません
でもフラグメントシェーダで色を設定することができます
ならばフラグメントシェーダに渡す色を変えていけば影を付けられますね

どんな色を作ればいいのか?
ここではとりあえず平行光源について説明しますので、その答えは光の当たり具合を計算すればいい訳ですが
実は何もこれにとらわれることなく自由な発想で色を計算していいのです
計算しだいでトゥーンレンダリングだってできるし、たとえば温度を割り当てて描画すればサーモグラフィーみたいな絵もかけます

ちょっと話がそれましたが、それでは平行光源を表現するための計算式を説明しましょう
平行光源とは太陽のようなとっても離れた(無限大に離れた)場所にある光と仮定します
つまり物体の位置関係に因らずに常に同じ方向から当たる光です

拡散反射光の計算

平行光源の向きを単位ベクトルLとすると、物体表面の向き(法線ベクトル)を単位ベクトルNとして、物体のある一点での反射の強さDは次の式で表されます

 D=N・L ・・・式2

内積を計算しただけです
すごく簡単ですね
法線ベクトルとは面に対して垂直な向きのベクトルのことです
単位ベクトルを使った内積ですのでえられる範囲は-1~1の範囲になるのですが
求めるのは反射の強さなのでDは正の数でないと困ります
0以下は切り捨てます

これで終わりではありません
拡散反射光は物体表面の色の影響を受け減衰しますので、反射光の色は次のような形になります

 反射光=D×マテリアルカラー ・・・式3

RGB毎にこれを計算してやれば拡散反射光となるわけです

実装
では平行光源の反射光計算を実装します
uniform変数としてライトの向きvec3 DLight、attribute変数として面の向きvec3 normalを追加します
そして式2のDに当たる変数としてfloat Dffuseを定義します
式3の反射光に当たるのは元々あるフラグメントシェーダに渡す変数vColorを使います
それから、式には出ていませんがモデルの移動やカメラの移動などを打ち消すための行列が必要になります
ライトの向きはワールド座標での定義になりますが、シェーダに渡されるのは頂点のローカル座標であるためです
uniform変数としてmat4 inv_mMatrixを設定します

サンプルページ


シェーダのソースコード
どうでしょう?一気に3DCGらしくなりましたね
ちなみにこの拡散光の計算がランバート反射(ランバートの余弦則に基づいた反射計算)と呼ばれるものです
ランバート(ランベルト)さんが考え出した明るさの計算方法です
1700年代の学者さんです
まさかこんなCGが世にあふれる世界など知る由も無かっただろうなあ


鏡面反射光の計算

拡散反射光だけだとなんだか粘土のようなマットな質感になりますね
世の中のほとんどの物体は鏡面反射も起きます
物によってさまざまですがツヤがあります
金属なんかはほぼ全て鏡面反射ですね

鏡面反射なんだから計算はライトの向きと面の向きが分かれば簡単!
・・・ではないんですね
鏡面反射光を計算するには実は物体表面の微細凹凸が重要なんです
よーく磨かれた金属表面なら理想的に鏡面反射するんですが、大抵の物体はざらつきや傷などで反射方向がブレます
このブレを考慮しないで計算すると皆ツヤツヤになってしまいます

この微細凹凸を表現する方法はいくつかあるんですが、ここではメジャーかつ効果的なハーフベクトルをつかった計算にしましょう
ちなみに、この微細凹凸の表現手法がOpenGLとDirectXでは異なっているため見映えが双方で違ったりする一因になってたりします

ハーフベクトルについて
ハーフベクトルって何のハーフだよ?とお思いですよね
ずばり言うとライトベクトルと視線ベクトルの半分のベクトルです
分かりにくいので図で説明すると

これと法線ベクトルの内積をとります
この計算方法をBlinn-Phong鏡面反射モデルというそうです

つまり、ハーフベクトルを単位ベクトルH、法線ベクトルを単位ベクトルNとして鏡面反射光の強さSは

 S=H・N (ただし0≦S≦1.0)・・・式4

としたいところですが、この計算だけでは質感が考慮されていませんね
質感を表現するためのファクターとしてPという変数を定義して
式4を次のようにします

 S=(H・N)^P ・・・式5

^はべき乗ね
HとEの内積は0~1の範囲にあります
一番明るいところで1ですね
1はいくらべき乗しても1のままですが小数はどんどん小さくなります
つまりPが大きければ大きいほどに1点で反射する=ツヤツヤになります
これでツヤの変化を付けて質感をあらわします
PはOpenGLだとshininessと呼ばれたりしてます

拡散反射光+鏡面反射光
鏡面反射光の計算式は以上ですが、最終的な反射光は拡散反射光(と環境光)を足さなくてはいけませんね
冒頭で述べたとおり、鏡面反射はあまり物体そのものの色の影響を受けることはありません
金属表面などでは変わりますが…
大抵は物体表面にコーティングがあったりして、そこで鏡面反射を起こすためです

つまり、拡散反射光とは別の色設定をする必要があります
マテリアルカラーとして鏡面反射色を付けると良いでしょう
これまでは拡散反射光に対してのカラーだけだったのでマテリアルカラー=拡散反射光の色でしたが
鏡面反射光にも別の色を着けるので、それぞれディフューズカラーとスペキュラーカラーと呼ぶことにします

実装
では実際に計算させましょう
鏡面反射光の計算をする上で新たに必要な要素は視線ベクトル、質感表現用のP、さらにスペキュラーカラーですね
ハーフベクトルはライトベクトルと視線ベクトルから計算できるのでattributeやuniformではないですね
uniformとして視線ベクトルvec3 Eye、マテリアルとしてPをuniform float shininessとでもしておきましょう
それからスペキュラーカラーはuniform vec4 Scolorとしておきましょう
また、例によって視線ベクトルはワールド座標空間におけるものですので、座標変換を打ち消す必要があります
拡散反射の反射光はcDiffuseという変数に格納することにします
鏡面反射の反射光はcSpecularとしましょう
その他色々ありまして結果↓

サンプルページ

シェーダのソースコード

ふむ、なんだかドラゴンは妖しい輝きですね
実はスペキュラーの色を
ドラゴン :赤
ウサギ :白
仏像 :ちょっと明るい黄色
としています
緑の地に赤い反射光で黄色く光っています
実際にそんな材質あるのかは知らないけど、CGならありえないことも起こせてしまうわけです

これじゃ気持ち悪いので白い反射光のバージョンも用意してみよう
サンプルページ


ちなみにスペキュラーのツヤ感は
ドラゴン :10
ウサギ :50
仏像 :100
としています
・・・あれ?ツヤ感10の方がなんだか明るくなって見えるような?
実はこのスペキュラーは結構いい加減に実装されていて
ライトの持つ光のエネルギーを考慮せず、ただ計算した色を乗せる形になってます
本来は鏡面反射光が増えた分拡散反射光の強さが減りますし、鏡面反射光が広がった分鏡面反射の全体の明るさを落とさないといけません
そのどちらも行っていないので面積を広く鏡面反射させたドラゴンが一番輝いて見えてしまいます
これはリアルタイムの3DCGならほぼ全てが抱えている問題です
対処法は簡単で、マテリアル設定で色のほうを暗くします
そうすれば光の増幅が起きていないように見えます

もちろんシェーダ側で光エネルギーを考慮した反射光を計算してもいいのですが
計算量が増えますし、エネルギー保存した反射光の計算式なんて考えるの面倒ですね…
頂点あたりの計算量が増えてしまうので、できればやっぱりマテリアルの色を暗くして誤魔化しましょう


環境光

さて、もうこれまでの計算で充分3DCGらしくなりましたね
でも、暗いところが本当に真っ黒で背景と同化してしまってますね
宇宙空間ならこれでいいんですけど部屋の中とかだとこんなに暗くては困ります
じゃあもう一個反対側からライト当てちゃおう!でも回避できる問題なんですが
昨今はハードウェアが高速でライト増やしてもいいんですけど、昔はそうも行かないんですよね
OpenGLでは最低限8個のライトを使うことができると保障されています
逆に言うと最悪の場合8個までしかライトを使えないのです
そのうちの貴重な1個を照り返しに使うのも自由ですが、もっとリーズナブルな方法を考え出したのです
それが環境光です

これまでの反射光計算は複雑でしたが、環境光はすごく簡単です
環境光を考慮した反射光は、拡散反射光をD、鏡面反射光をSとして(これらは今までの解説によって求められる値ですね)、環境光の強さをAとすると
求める反射光Bは

 B=D+S+A ・・・式6

です
つまり全体にAという明るさを足してしまいます
さらにこの形こそが一番最初に示した式1の事ですね
これでフォンの反射モデルを実現できます

OpenGLやDirectXにはこの環境光に対するマテリアル設定(反射係数)が存在しますが
自分はそんなもの設定しません
それには理由があるのですが、それはまた次回
環境光の色だけ設定しましょう

実装
というわけで実装
全体に影響するのでuniformでvec4 ambientと定義しました
これをさっきのスペキュラー計算まで書いたソースに足すだけ

サンプルページ


シェーダのソースコード

なんとなく少し赤い環境光を付けてみました
さらに先ほど言っていた鏡面反射光や拡散反射光の色を減らす という事をしています
いい加減に減らしましたが、3つのモデルが同じ明かりに照らされてるんだなと思えるような明るさに調整しないといけないので少し面倒ですね


ライトカラー

今までの計算は全てライトが白色であると仮定して作ってきました
でも現実にはいろんな色の光源がありますね
ライトにも色はあるのです
これも実装しましょう

といっても難しくはないです
要は色を乗算するだけです
式6を変形して、ライトの色をCとでもしておいて

 B = D*C + S*C + A ・・・式7

ですかね
最終的なサンプルとソースです
ライトカラーはuniformですね Lcolorとしておきました

サンプルページ


シェーダのソースコード
光の色はマンセル表色系の色相環を360度回るように変化します

拡散反射光については
ウサギは白いので全ての色の影響を受けます
ドラゴンは緑色なのでどの色が当たっても跳ね返るのは緑の成分のみです
仏像は黄色なので赤と緑の成分を跳ね返し青の影響を受けません

鏡面反射光は地の色の影響は受けないのでどのモデルもライトの色をそのまま跳ね返します
環境光は全く関係ない光なので変わりません

ちなみに、細かいことですが3DCGでマテリアルに色を設定するっていうのは、実は色というよりはどの色の光をどれだけ反射するかという係数、反射係数を設定する、と考えた方が良いですよ
つまり真っ黒(R:0.0 G:0.0 B:0.0)のマテリアルは全く色を跳ね返さない、現実でいうならブラックホールになってしまいます
反射係数全てが0の物質は地球上には先ずありえません
これはテクスチャにもいえることで、黒潰れした部分がある画像は使わないほうが良いです
まあ真っ黒に描きたいんだい!って言うならとめませんが


あとがき

さて、今回のお話はここまでです
これがフォンの反射モデルでシェーディングした場合の3DCGの描き方です
(厳密には少しだけ違うけど)
ただの一色に塗られたシルエットに見事に輝きと影がついてリアルになりましたね

次回はこのシェーディングの問題点を自分なりに考察して改良を加えた手法を説明します
本当はこの基本的なシェーディングにはもっと綺麗になる可能性が秘められています
お楽しみに


2013/11/12掲載 2013/12/01更新

FC2カウンター
inserted by FC2 system