9. ライティング パート2:鏡面反射とポイントライト
第4章では、平らな三角形をアンビエントライトとディレクショナルライトで照らしました。
平らな三角形には法線が1つしか無いので、全体が同じ明るさになり、ライティングが行われているのか殆ど分かりませんでした。
ライティングが面白くなるのは曲面の上で、其処では全てのピクセルが少しずつ違う方向を向いています。
其の為、此の章では第5章のプレイグラウンドに戻り、三角形を1枚も使わずに球を描きます。
フラグメントシェーダーが全てのピクセルについて、球の何処にいて、其処で表面がどちらを向いているのかを計算します。
其の上で、照らされた物体を切り抜きではなく「物」に見せる2つの要素を追加します:
- 鏡面反射のハイライト:光沢のある物に見える小さな明るい点です。先ずフォンのモデルで、次に殆ど全ての人が使うブリン・フォンで作ります。
- ポイントライト:電球の様に位置を持つ光です。向きはピクセル毎に変わり、明るさは距離と共に弱くなります。
最後には、光が球の周りを回ったり、マウスを追い掛けたりします。
変更するのは shader/playground.glsl だけです。
テスターを起動したまま、編集する度にRキーを押して下さい。
9.1 三角形を使わない球
9.1.1 表面を見つける
第5章の中心を原点にした座標から始めます。yは−1から1で、xとyの1単位は同じ長さです:
vec2 p = (2.0 * gl_FragCoord.xy - uResolution) / uResolution.y;
原点を中心とする半径の球を想像し、其れを非常に遠くから−z軸に沿って真っ直ぐ見ているとします。
円の中の各ピクセルからは、球の手前半分の点が丁度1つ見えます。
其の点のxとyはピクセルのxとyと同じなので、求めるのはzだけです。
原点を中心とする球の点は全て、中心から距離にあります(線型代数の第3章):
正の平方根を取るのは、其れがあたし達の方を向いている半分だからです(+zは見る人の方を向いています)。
球の法線は、全ての法線の中で一番簡単です。中心から真っ直ぐ外を向いています。
中心から点へのベクトルはで、其の長さはなので、で割れば正規化出来ます:
数字で確かめてみましょう。、ピクセルとします。
なので、です。
法線はで、確かにです。
此のピクセルは、後の計算例でも使います。
平らな物を描きながら、ピクセル毎に表面を計算して3Dの物体の振りをさせる此の技は、インポスターと呼ばれます。
ゲームでは遠くの木やパーティクルに、分子ビューアーでは何千もの完璧な球を平らな四角形として描くのに使われます。
9.1.2 法線を表示する
playground.glsl の main を次の物に置き換えて下さい(上のユニフォームはそのままです):
const float RADIUS = 0.8; // 球の半径
void main() {
// 中心が原点、縦が-1〜1の座標(第5章)
vec2 p = (2.0 * gl_FragCoord.xy - uResolution) / uResolution.y;
float px = 2.0 / uResolution.y; // 1ピクセルの大きさ
// 球面上の点と法線(球の外ではz = 0にしておく)
float z = sqrt(max(RADIUS * RADIUS - dot(p, p), 0.0));
vec3 pos = vec3(p, z);
vec3 n = pos / RADIUS;
vec3 sphere = n * 0.5 + 0.5; // 法線(-1〜1)を色(0〜1)にする
vec3 background = vec3(0.1);
// 球の縁を1ピクセル分滑らかにする(第6章)
float mask = 1.0 - smoothstep(RADIUS - px, RADIUS, length(p));
out_color = vec4(mix(background, sphere, mask), 1.0);
}
dot(p, p) は、つまり長さの2乗で、length が計算する平方根はありません。
円の外ではが負になり、GLSLでは負の数の sqrt は未定義です(GPUによってNaNになったり、0になったり、何にでもなり得ます)。
max(..., 0.0) で全ての場所で値を正しく保ちます。どうせ其れらのピクセルはマスクで捨てられます。
if を使う代わりに、全てのピクセルで球を計算してから第6章のアンチエイリアス付きのマスクで混ぜるので、縁が滑らかになります。
色は第5章のデバッグの技です。法線の成分は−1から1なので、n * 0.5 + 0.5 で0から1にすれば目に見えます。
右端は赤っぽく(x = 1)、上は緑っぽく(y = 1)、中央は法線が(0, 0, 1)なので(0.5, 0.5, 1)の青紫になります。
此れは正に法線マップの見た目で、第12章で又会います。
9.1.3 全てを1つの空間で
ライティングには幾つかのベクトルが必要です。法線、光への向き、目への向きです。
決して破ってはいけない唯一のルールは、其れらが全て同じ座標空間にある事です。
本格的な3Dプログラムでは、普通はワールド空間かビュー空間にあり(線型代数の第11章)、第13章で其れをきちんと設定します。
此処では全てが1つの簡単な空間にあり、其れは実はカメラのビュー空間です。xは右、yは上、zは見る人の方、画面の中央が原点で、カメラは−zの方を見ています。
平行な線に沿って見ているので(正射影のビュー)、表面のどの点からも目への向きは同じです:
透視投影のカメラなら、は normalize(cameraPos - pos) になり、ピクセル毎に変わります。其れは第13章と第14章で行います。
9.2 拡散反射:ランバートの復習
第4章と線型代数の第4章で、つや消しの表面が受け取る光は、法線と光源への向きの間の角度のコサインに比例する事を見ました。
単位ベクトル同士なら、其のコサインは内積です:
向きの約束を思い出して下さい。此のシリーズでは、は常に表面から光源への向きです。
代わりに「光が進む向き」を保存している場合は、第4章と同じ様にマイナスの符号が必要です。
今はピクセル毎に本物の法線があるので、其れを使いましょう:
const float RADIUS = 0.8; // 球の半径
const vec3 ALBEDO = vec3(0.8, 0.2, 0.3); // 表面の色(第4章と同じ)
const vec3 LIGHT_COLOR = vec3(1.0); // 光の色
const float AMBIENT = 0.1; // アンビエントの強さ
void main() {
vec2 p = (2.0 * gl_FragCoord.xy - uResolution) / uResolution.y;
float px = 2.0 / uResolution.y;
float z = sqrt(max(RADIUS * RADIUS - dot(p, p), 0.0));
vec3 pos = vec3(p, z);
vec3 n = pos / RADIUS;
// ディレクショナルライト:表面から光への向き(左上手前)
vec3 l = normalize(vec3(-0.5, 0.6, 0.6));
float diffuse = max(dot(n, l), 0.0);
vec3 sphere = ALBEDO * AMBIENT + ALBEDO * LIGHT_COLOR * diffuse;
vec3 background = vec3(0.1);
float mask = 1.0 - smoothstep(RADIUS - px, RADIUS, length(p));
out_color = vec4(mix(background, sphere, mask), 1.0);
}
アルベドは表面其の物の色、つまり各色チャンネルをどれ位反射するかを表す言葉です。
2つの色を成分毎に掛ける事(ALBEDO * LIGHT_COLOR)が、光と表面の相互作用です。赤い表面は白い光の下では赤を反射しますが、純粋な青い光の下では黒になります。
もう丸く見えます。
でも粘土やチョークの玉の様です。何も光っていません。
9.3 鏡面反射のハイライト:フォンのモデル
9.3.1 考え方
光沢のある表面は、少しだけ鏡の様に振る舞います。
完璧な鏡は光を丁度1つの方向に反射し、目が丁度其の方向にある時だけ光が見えます。
光沢はあるけれど完全には滑らかでない表面(プラスチック、磨いたリンゴ、濡れた肌)は、其の方向の周りの小さな円錐に反射を広げます。
其の結果、鏡の方向が目の近くを向く所に明るい点、ハイライトが出来ます。
1975年にブイ・トゥオン・フォンが、此れの簡単なモデルを発表しました。
光源への向きを法線について鏡映して反射の向きを求め、が目への向きにどれ位近いかを内積で測り、累乗して点を小さくします:
指数は光沢度(シャイニネス)です。
0と1の間の数を大きな数で累乗すると、とても速く小さくなります。ですが、です。
従って、が大きいと鏡の方向に非常に近い方向だけが明るくなり、小さく鋭いハイライトになります。
9.3.2 反射ベクトル
反射は線型代数の第4章の公式です。入って来る向きと単位法線に対して:
GLSLには reflect(I, N) として組み込まれています。
最初の引数は入射の向き、つまり光が進む向きで、表面に向かっています。
あたし達のは表面から離れる向きなので、反転しなければなりません:reflect(-l, n)。
此のマイナスを忘れるのが此処の典型的なバグです。ハイライトが球の反対側に出たり、全く出なかったりします。
書き出すと、です。
9.3.3 計算例
9.1.1のピクセル、光(上の手前)、目を使います。
(確認:。鏡は長さを変えません。)
ならハイライトは、ならです。
同じピクセルの拡散反射は0.928で殆ど全力ですが、光沢度32では此のピクセルはもうハイライトの外です。
9.3.4 シェーダーで
定数を2つと鏡面反射の項を追加します:
const float SHININESS = 32.0; // 光沢度(大きい程ハイライトが小さく鋭い)
const float SPECULAR = 0.5; // 鏡面反射の強さ
vec3 l = normalize(vec3(-0.5, 0.6, 0.6));
float diffuse = max(dot(n, l), 0.0);
// フォンの鏡面反射
vec3 v = vec3(0.0, 0.0, 1.0); // 表面から目への向き
vec3 r = reflect(-l, n); // 光が反射して進む向き
float specular = pow(max(dot(r, v), 0.0), SHININESS);
vec3 sphere = ALBEDO * AMBIENT
+ ALBEDO * LIGHT_COLOR * diffuse
+ LIGHT_COLOR * SPECULAR * specular;
pow(x, y) はを計算します。
GLSLの仕様では、の時、及びでの時は未定義なので、前の max(..., 0.0) は省略出来ません。
鏡面反射の項にはアルベドを掛けていない事に注目して下さい。
金属以外(プラスチック、塗装、肌、木)では、ハイライトは光其の物の反射なので、表面ではなく光の色になります。
金属は違います。反射に色が付き(金は黄色を反射します)、拡散反射は殆どありません(課題3)。
急に光沢のある玉になりました。
光沢度を4、16、64、256にした同じ球です:
9.4 ブリン・フォン:ハーフベクトル
9.4.1 考え方
1977年にジム・ブリンが小さな変更を提案し、其れは殆どあらゆる点でより良い物でした。
光を反射して目と比べる代わりに、彼はこう問いました。光を丁度目の方に反射する法線はどれか?
其の法線はとの丁度真ん中を向いていて、ハーフベクトルと呼ばれます:
2つの単位ベクトルを足すと、其れらが作る菱形の対角線に沿ったベクトルになるので、丁度真ん中を向きます。正規化すれば又単位ベクトルになります。
本当の法線がに近ければ、表面は「光と目の間の殆ど鏡」で、ハイライトは明るくなります。
図の様に4つのベクトルが全て1つの平面上にある時、との間の角度は、との間の角度の丁度2倍です。
(図ではが135°、が70°なので、は45°、は102.5°です。、。)
角度が小さいので、同じ光沢度ならブリン・フォンの方がハイライトが大きくなります。
大体の目安として、光沢度のブリン・フォンは、光沢度のフォンと同じ位に見えます。で、です。
9.4.2 同じ計算例
同じピクセル、同じ光、同じ目で:
なら、ならです。
フォンはで0.048でした。ブリン・フォンで同じ値にするには、指数が約4倍必要です。
(此処での角度はフォンが46.8°、ブリン・フォンが24.4°です。4つのベクトルが1つの平面上に無いので、丁度2対1ではありません。)
9.4.3 シェーダーで
変わるのは2行です:
// ブリン・フォンの鏡面反射
vec3 v = vec3(0.0, 0.0, 1.0); // 表面から目への向き
vec3 h = normalize(l + v); // ハーフベクトル
float specular = pow(max(dot(n, h), 0.0), SHININESS);
違いを直接見る為に、左半分がフォン、右半分がブリン・フォンの球を用意しました。どちらも光沢度32で、光は計算例と同じです(p.x < 0.0 ? phong : blinn でピクセル毎にどちらかを選びます):
9.4.4 何故ブリン・フォンが勝ったのか
- ディレクショナルライトでは安い。 ディレクショナルライトと一定の視線の向きなら、は全てのピクセルで同じで、CPUで一度だけ計算する事も出来ます。
reflectはピクセル毎に実行しなければなりません。 - 途切れない。 との間の角度が90°を超えると、は負になり、フォンのハイライトは0にクランプされ、光沢度が低いと境目が見えます。は其の範囲のずっと広い所で正のままです。
- 現実に近い。 実際の表面のハイライトを測ると、浅い角度で見た時に引き伸ばされます(濡れた道路に映る太陽の長い筋を思い浮かべて下さい)。ブリン・フォンは其の伸びを再現しますが、フォンのハイライトは丸いままです。
- 現代のレンダリングにも生き残っている。 今のエンジンが使う物理ベースのモデルは、表面を沢山の小さな鏡として扱い、其の内どれ位がの方を向いているかを問います。ハーフベクトルは其の全ての中心にあります。
シェーダー以前の古いOpenGLの固定機能パイプラインもブリン・フォンを使っていて、デフォルトでは目の向きを、あたし達と同じと仮定していました。
9.4.5 後ろからの光
ブリン・フォンには罠が一つあります。
光が表面の後ろにある時、で拡散反射は0ですが、特に光が表面を掠める時、はまだ正になり得ます。
すると暗い側にハイライトが出てしまい、そんな事をする現実の物体はありません。
此処では光が上の後ろから来て、問題が見え易い様に光沢度は低い2にしています(左半分がフォン、右半分がブリン・フォン):
直し方は、表面が少しでも光の方を向いている時だけハイライトを計算する事、if (diffuse > 0.0) です。
此の様な低い光沢度では、ハイライトが切られる所に硬い境目が見えます。現実的な光沢度なら、其の境目はハイライトが既にとても弱い所に来ます。
9.5 アンビエント・拡散反射・鏡面反射を合わせる
整理しましょう。
直ぐに光が動き、第10章では光が幾つも出て来るので、光1つ分の仕事を関数にまとめます:
const float RADIUS = 0.8; // 球の半径
const vec3 ALBEDO = vec3(0.8, 0.2, 0.3); // 表面の色(第4章と同じ)
const vec3 LIGHT_COLOR = vec3(1.0); // 光の色
const float AMBIENT = 0.1; // アンビエントの強さ
const float SHININESS = 64.0; // 光沢度(大きい程ハイライトが小さく鋭い)
const float SPECULAR = 0.5; // 鏡面反射の強さ
// 1つの光からの拡散反射と鏡面反射(ブリン・フォン)
// n:法線、v:目への向き、l:光への向き(全て単位ベクトル)
vec3 blinnPhong(vec3 n, vec3 v, vec3 l, vec3 lightColor) {
float diffuse = max(dot(n, l), 0.0);
float specular = 0.0;
if (diffuse > 0.0) {
vec3 h = normalize(l + v);
specular = pow(max(dot(n, h), 0.0), SHININESS);
}
return (ALBEDO * diffuse + SPECULAR * specular) * lightColor;
}
void main() {
vec2 p = (2.0 * gl_FragCoord.xy - uResolution) / uResolution.y;
float px = 2.0 / uResolution.y;
float z = sqrt(max(RADIUS * RADIUS - dot(p, p), 0.0));
vec3 pos = vec3(p, z);
vec3 n = pos / RADIUS;
vec3 v = vec3(0.0, 0.0, 1.0);
vec3 l = normalize(vec3(-0.5, 0.6, 0.6));
vec3 ambient = ALBEDO * AMBIENT;
vec3 sphere = ambient + blinnPhong(n, v, l, LIGHT_COLOR);
vec3 background = vec3(0.1);
float mask = 1.0 - smoothstep(RADIUS - px, RADIUS, length(p));
out_color = vec4(mix(background, sphere, mask), 1.0);
}
アルベド、光の色、アンビエントの強さ、鏡面反射の強さで、モデル全体を1つの式にすると(色同士の積は全て成分毎です):
此の3つの部分が、「ADS」モデル(アンビエント、ディフューズ、スペキュラー)と呼ばれる物です。
3つの項を其々単独で表示した物と、其の合計です:
各項が他の項の弱点を補っています。
アンビエントだけでは形が無く、拡散反射だけでは奥側が真っ黒になり(現実では光が部屋の中で跳ね返って少し照らします)、鏡面反射だけではただの点です。
上の様に各項を単独で表示出来る様にシェーダーを書くのは、ライティングをデバッグする一番良い方法でもあります。
9.6 ポイントライト
9.6.1 ピクセル毎の向き
ディレクショナルライトは太陽の様に無限に遠くにあり、全てのピクセルが同じを受け取ります。
ポイントライトは電球の様に位置を持ち、全ての方向に光を出します。
すると光源への向きは、表面の点が何処にあるかに依存します:
「目標引く始点」は、線型代数の第3章のAからBへの向きです。
どうせ距離が必要なので(下で使います)、normalize と length を呼ぶ(同じ平方根を2回計算する)代わりに、長さを1回計算して其れで割ります。
9.6.2 減衰
電球は部屋の向こうの壁よりも、机をずっと明るく照らします。
物理的な理由は簡単です。電球を出た光は球の表面に広がり、半径の球の面積はです。
2倍遠くでは同じ光が4倍の面積に広がるので、各点が受け取るのは4分の1です。
此れが逆2乗の法則です:
シェーダーで其のまま使うと、問題が2つあります。
で無限大になり、光のすぐ近くではモニターが表示出来る何よりもずっと明るくなります。
そして決して0にならないので、全ての光が世界中の全てのピクセルに影響します。
古典的な解決策は、此れも固定機能のOpenGL(GL_CONSTANT_ATTENUATION、GL_LINEAR_ATTENUATION、GL_QUADRATIC_ATTENUATION)から来た、3つの定数を持つ多項式です:
- (定数項)は普通1で、光源の位置で減衰が1になり、1を超える事はありません。
- (1次の項)は光を早めに弱くします。
- (2次の項)は物理的な部分で、光源から遠い所で支配的になります。
此れらを手で選ぶ人はいません。
光の届く範囲毎の定数の此の表は、Ogre3Dエンジンのwikiから来た物で、インターネットの至る所で使われています(LearnOpenGLが広めました)。
「範囲」は、大体光が無視出来る程になるワールド単位の距離です:
| 範囲 | |||
|---|---|---|---|
| 7 | 1.0 | 0.7 | 1.8 |
| 13 | 1.0 | 0.35 | 0.44 |
| 20 | 1.0 | 0.22 | 0.20 |
| 32 | 1.0 | 0.14 | 0.07 |
| 50 | 1.0 | 0.09 | 0.032 |
| 100 | 1.0 | 0.045 | 0.0075 |
| 200 | 1.0 | 0.022 | 0.0019 |
| 600 | 1.0 | 0.007 | 0.0002 |
あたし達のシーン全体は幅が約2単位なので、一番小さいを使います。
計算例:
3本の曲線は全て光源の位置で丁度1から始まり、遠くでは物理法則のの様に振る舞います。
純粋な(破線)を見ると、定数項が必要な理由が分かります。
9.6.3 材質、壁、ポイントライト
ポイントライトをちゃんと見る為には、球以外にも照らす物が必要です。
球の真後ろに壁を置きます。平面で、法線は此方を向いています。
球の外の全てのピクセルからは、点の壁が見えます。
壁は灰色でつや消し、球は赤くて光沢があるべきなので、材質の値を構造体(struct)にまとめます:
// 表面の材質
struct Material {
vec3 albedo; // 表面の色
float specular; // 鏡面反射の強さ
float shininess; // 光沢度(大きい程ハイライトが小さく鋭い)
};
const float RADIUS = 0.6; // 球の半径
const float WALL_Z = -RADIUS; // 壁の位置(球の真後ろ)
const float AMBIENT = 0.1; // アンビエントの強さ
const vec3 LIGHT_COLOR = vec3(1.0, 0.95, 0.85) * 3.0; // 光の色と明るさ
const vec3 ATTENUATION = vec3(1.0, 0.7, 1.8); // 減衰の定数(定数項、1次、2次)
const Material SPHERE = Material(vec3(0.8, 0.2, 0.3), 0.5, 64.0);
const Material WALL = Material(vec3(0.7, 0.7, 0.75), 0.1, 8.0);
// 1つの光からの拡散反射と鏡面反射(ブリン・フォン)
// n:法線、v:目への向き、l:光への向き(全て単位ベクトル)
vec3 blinnPhong(vec3 n, vec3 v, vec3 l, Material m, vec3 lightColor) {
float diffuse = max(dot(n, l), 0.0);
float specular = 0.0;
if (diffuse > 0.0) {
vec3 h = normalize(l + v);
specular = pow(max(dot(n, h), 0.0), m.shininess);
}
return (m.albedo * diffuse + m.specular * specular) * lightColor;
}
// ポイントライト:光の位置から向きと減衰を求める
vec3 pointLight(vec3 pos, vec3 n, vec3 v, Material m, vec3 lightPos, vec3 lightColor) {
vec3 toLight = lightPos - pos;
float d = length(toLight); // 光までの距離
vec3 l = toLight / d; // normalize(toLight)と同じ
float attenuation = 1.0 / (ATTENUATION.x + ATTENUATION.y * d + ATTENUATION.z * d * d);
return blinnPhong(n, v, l, m, lightColor) * attenuation;
}
// 表面の1点の色
vec3 shade(vec3 pos, vec3 n, Material m, vec3 lightPos) {
vec3 v = vec3(0.0, 0.0, 1.0); // 目への向き
vec3 color = m.albedo * AMBIENT;
color += pointLight(pos, n, v, m, lightPos, LIGHT_COLOR);
return color;
}
void main() {
vec2 p = (2.0 * gl_FragCoord.xy - uResolution) / uResolution.y;
float px = 2.0 / uResolution.y;
vec3 lightPos = vec3(-0.7, 0.5, 0.7); // 光の位置(左上、球の手前)
// 球
float z = sqrt(max(RADIUS * RADIUS - dot(p, p), 0.0));
vec3 spherePos = vec3(p, z);
vec3 sphere = shade(spherePos, spherePos / RADIUS, SPHERE, lightPos);
// 壁(z = WALL_Zの平面、法線は手前を向く)
vec3 wall = shade(vec3(p, WALL_Z), vec3(0.0, 0.0, 1.0), WALL, lightPos);
float mask = 1.0 - smoothstep(RADIUS - px, RADIUS, length(p));
out_color = vec4(mix(wall, sphere, mask), 1.0);
}
幾つか注目して下さい:
- 壁が沢山見える様に、球を小さく(0.6)しました。
- 光の色は
* 3.0です。減衰で全てが暗くなるので、ポイントライトが明るく見えるには1.0より大きい値が必要です。球の一番近い点では光までの距離が約0.51で、減衰は約0.55です。 pointLightとshadeは表面の点の位置posを受け取ります。此れが新しい材料で、此れまでのライティングには法線しか必要ありませんでした。- 定数ベクトル
ATTENUATIONに3つの定数を入れています。.x、.y、.zは3つの数を保存するコンパクトな方法に過ぎません。
ディレクショナルライトと比べてみて下さい。
近くにある光は球の其の光に向いた側にしか届かないので、照らされた部分が小さくなっています。
そして壁は距離と共に消えていく柔らかい光の溜まりになっています。其の円が、目に見える減衰です。
9.7 光を動かす
動く光は、どんな静止画よりもずっと良く形を見せてくれます。
uTime で球の周りを回らせるか、uMouse でマウスを追わせましょう。
2つの内どちらにするかは、プリプロセッサー(第2章)で選びます。上の方に #define FOLLOW_MOUSE 0 を書き、main の中で:
// 1にするとマウスで光を動かす、0にすると時間で光が回る
#define FOLLOW_MOUSE 0
// 光の位置(zは球より手前)
#if FOLLOW_MOUSE
vec2 mouse = (2.0 * uMouse - uResolution) / uResolution.y; // pと同じ座標にする
vec3 lightPos = vec3(mouse, 0.7);
#else
vec3 lightPos = vec3(0.9 * cos(uTime * 1.25), 0.6 * sin(uTime * 1.25), 0.7);
#endif
uMouse は左下が原点のピクセル単位なので(第5章)、p と全く同じ式で変換します。
軌道は楕円で、ウィンドウが縦より横に長いので横に広くしています。uTime * 1.25 で1周に秒掛かります。
zの0.7で光を球の手前に保ちます。の光は位置によっては球の中に入ってしまい、其の時は見えている表面全体が光と反対を向きます。
最後に、ライティングが何をしているか分かる様に、光の位置に小さな白い点を表示します。
main の最後の2行を置き換えて下さい:
float mask = 1.0 - smoothstep(RADIUS - px, RADIUS, length(p));
vec3 color = mix(wall, sphere, mask);
// 光の位置に小さな白い点を描く
float marker = 1.0 - smoothstep(0.03 - px, 0.03, length(p - lightPos.xy));
color = mix(color, vec3(1.0), marker);
out_color = vec4(color, 1.0);
(変数を dot と名付けないで下さい。GLSLではローカル変数が組み込み関数を隠せるので、同じブロックの次の dot(...) の呼び出しがコンパイルエラーになります。)
#define FOLLOW_MOUSE 1 にしてRを押すと、光がカーソルを追います:
ハイライトが決して光の点の真下に無い事に注目して下さい。
ハイライトは球の法線が光と目の真ん中を向く所にあり、目は真正面にあるので、ハイライトは光の点と球の中心の間に来ます。
9.8 此の章の新しいGLSLの機能
| 機能 | 何をするか |
|---|---|
reflect(I, N) |
。Iは入って来る向き、Nは正規化されている事 |
pow(x, y) |
。xが0未満の時、及びx = 0でyが0以下の時は未定義 |
sqrt(x) |
。xが0未満の時は未定義 |
normalize(v) |
。ゼロベクトルでは未定義 |
struct Name { ... }; |
幾つかの値をまとめる型。.name で読む |
Material(a, b, c) |
全ての構造体が持つコンストラクター。値は宣言の順 |
const Material X = ... |
定数の構造体。コンストラクターの引数が定数なので使える |
#define、#if |
プリプロセッサー(第2章)。コンパイルの前にコードを有効・無効にする |
reflectは「光源への向きを反射した物」とよく誤解されます。進む向きなので-lを渡して下さい。- GLSLの構造体は関数(メソッド)やデフォルト値を持てませんが、関数に渡したり、返したり、
==で比べたり、入れ子にしたり出来ます。 - 関数の引数はコピー(第2章の
in引数)なので、関数は呼び出し元に影響を与えずにMaterial mを変更出来ます。第10章で其れを使います。 - 此処の全ては、macOSのOpenGL 4.1でも其のまま動きます。
9.9 実際のゲームやエンジンでの使われ方
- 1990〜2010年の主力。 ブリン・フォンは固定機能のOpenGL(
GL_SHININESSは0〜128に制限されていました)とDirect3Dのライティングであり、PS2、Xbox 360、PS3の時代の殆どのゲームのライティングでした。最初は頂点毎、後にピクセル毎です。安くて調整し易いので、今でも多くのインディーゲームやスタイライズされたゲームが使っています。 - 物理ベースレンダリング。 現代のエンジン(Unreal、Unity、Godot、Frostbite)はGGXの様な「PBR」のモデルを使います。見た目はブリン・フォンと違いますが、入力は此の章と全く同じです。、、、そしてハーフベクトルとです。光沢度は「ラフネス」に、鏡面反射の色は「メタルネス」になります。先ずブリン・フォンを学ぶのが普通の道です。
- 沢山のポイントライト。 ゲームのレベルには何百ものポイントライト(ランプ、マズルフラッシュ、光る魔法)がある事があります。其の為エンジンは各光に半径を与えて其の外では無視し、光が届くピクセルにだけ描きます(ディファードシェーディング又はクラスターシェーディング。OpenGLシリーズで少し触れます)。多項式の減衰は決して0にならないので、エンジンは半径で丁度0になる様に作られた減衰曲線を使います(課題7)。
- インポスター。 分子ビューアー、パーティクルシステム、遠くの木は、あたし達の球と同じ様に、球や他の形をピクセル毎の表面を持つ平らな四角形として描きます。第14章(レイマーチング)では、此の考え方を3Dのシーン全体にまで広げます。
9.10 まとめ
- −z方向に見る球では、ピクセルの下の表面の点はで、法線は其の点をで割った物です。
- ライティングのベクトルは全て1つの空間に無ければなりません。此処では正射影のカメラのビュー空間なので、です。
- 拡散反射:。は光源の方を向きます。
- フォン:
r = reflect(-l, n)で。ブリン・フォン:h = normalize(l + v)で。同じ大きさのハイライトには約4倍の指数が必要です。 - 鏡面反射はの所だけに足します。金属以外のハイライトは表面ではなく光の色です。
- ポイントライト:、減衰はで、定数は範囲の表から選びます。
- ライティングは、1項ずつテストして表示出来る小さな関数(
blinnPhong、pointLight、shade)として書きます。
9.11 課題
- ★ 床の点で、光の向き、目とします。と、及びのフォンの鏡面反射を計算し、次にと、及びのブリン・フォンの鏡面反射を計算して下さい。(フォンは0.64と0.4096、ブリン・フォンは0.9と0.81になる筈です。)
- ★ 定数で、との減衰を計算して下さい。次にを解いて、光が丁度半分になる距離を求めて下さい(約0.58)。
- ★★ 球を金にして下さい。拡散反射のアルベドは殆ど黒にし、ハイライトには普通の
m.specularの代わりに鏡面反射の色vec3(1.0, 0.77, 0.34)を掛けます(Materialにvec3 specularColorを追加します)。暗い玉に金色のハイライトがあり、光が近い所が金色に光って見える筈です。 - ★★ 2つ目のポイントライトを追加して下さい。青い
vec3(0.3, 0.5, 1.0) * 3.0の光で、逆向き(-uTime)に回り、1つ目の光はマウスを追います。shadeでpointLightの呼び出しを1つ足すだけです。ハイライトが2つと、壁に光の溜まりが2つ見え、重なる所は紫になる筈です。 - ★★ 透視投影の目を作って下さい。カメラをに置き、
shadeでv = normalize(C - pos)を使います。壁のハイライトが見え易い様に、壁の鏡面反射の強さを0.5、光沢度を32にします。正射影の目では壁のハイライトは光の点の真下にありますが、透視投影の目では光の点とウィンドウの中心の間(外向きに約0.74の所)に移動します。其処で壁が光をカメラに向けて鏡の様に反射するからです。 - ★★ 鏡面反射の項だけを表示するデバッグビューを書いて下さい。左半分がフォンで光沢度
SHININESS、右半分がブリン・フォンで光沢度4.0 * SHININESSです。マウスで光を動かすと、球の縁の近く以外では2つの半分が殆ど完璧に一致する筈です。 - ★★★ Unreal Engine 4は、半径の光にという減衰を使います。で実装して多項式と比べて下さい。で丁度0になる事を示し、其れが何百もの光があるエンジンにとって何故重要なのか説明して下さい。何故ではなくを使うのでしょうか?
ファイル: a/shader/playground.glsl | b/shader/playground.glsl
| 前 | 新 | ||
|---|---|---|---|
| @@ -7,16 +7,80 @@ uniform float uTime; // 起動してからの秒数 | |||
| 7 | uniform vec2 uResolution; // 画面の大きさ(ピクセル) | 7 | uniform vec2 uResolution; // 画面の大きさ(ピクセル) |
| 8 | uniform vec2 uMouse; // マウスの位置(ピクセル、左下が原点) | 8 | uniform vec2 uMouse; // マウスの位置(ピクセル、左下が原点) |
| 9 | 9 | ||
| 10 | // 1にするとマウスで光を動かす、0にすると時間で光が回る | ||
| 11 | #define FOLLOW_MOUSE 0 | ||
| 12 | |||
| 13 | // 表面の材質 | ||
| 14 | struct Material { | ||
| 15 | vec3 albedo; // 表面の色 | ||
| 16 | float specular; // 鏡面反射の強さ | ||
| 17 | float shininess; // 光沢度(大きい程ハイライトが小さく鋭い) | ||
| 18 | }; | ||
| 19 | |||
| 20 | const float RADIUS = 0.6; // 球の半径 | ||
| 21 | const float WALL_Z = -RADIUS; // 壁の位置(球の真後ろ) | ||
| 22 | const float AMBIENT = 0.1; // アンビエントの強さ | ||
| 23 | const vec3 LIGHT_COLOR = vec3(1.0, 0.95, 0.85) * 3.0; // 光の色と明るさ | ||
| 24 | const vec3 ATTENUATION = vec3(1.0, 0.7, 1.8); // 減衰の定数(定数項、1次、2次) | ||
| 25 | |||
| 26 | const Material SPHERE = Material(vec3(0.8, 0.2, 0.3), 0.5, 64.0); | ||
| 27 | const Material WALL = Material(vec3(0.7, 0.7, 0.75), 0.1, 8.0); | ||
| 28 | |||
| 29 | // 1つの光からの拡散反射と鏡面反射(ブリン・フォン) | ||
| 30 | // n:法線、v:目への向き、l:光への向き(全て単位ベクトル) | ||
| 31 | vec3 blinnPhong(vec3 n, vec3 v, vec3 l, Material m, vec3 lightColor) { | ||
| 32 | float diffuse = max(dot(n, l), 0.0); | ||
| 33 | float specular = 0.0; | ||
| 34 | if (diffuse > 0.0) { | ||
| 35 | vec3 h = normalize(l + v); | ||
| 36 | specular = pow(max(dot(n, h), 0.0), m.shininess); | ||
| 37 | } | ||
| 38 | return (m.albedo * diffuse + m.specular * specular) * lightColor; | ||
| 39 | } | ||
| 40 | |||
| 41 | // ポイントライト:光の位置から向きと減衰を求める | ||
| 42 | vec3 pointLight(vec3 pos, vec3 n, vec3 v, Material m, vec3 lightPos, vec3 lightColor) { | ||
| 43 | vec3 toLight = lightPos - pos; | ||
| 44 | float d = length(toLight); // 光までの距離 | ||
| 45 | vec3 l = toLight / d; // normalize(toLight)と同じ | ||
| 46 | float attenuation = 1.0 / (ATTENUATION.x + ATTENUATION.y * d + ATTENUATION.z * d * d); | ||
| 47 | return blinnPhong(n, v, l, m, lightColor) * attenuation; | ||
| 48 | } | ||
| 49 | |||
| 50 | // 表面の1点の色 | ||
| 51 | vec3 shade(vec3 pos, vec3 n, Material m, vec3 lightPos) { | ||
| 52 | vec3 v = vec3(0.0, 0.0, 1.0); // 目への向き | ||
| 53 | vec3 color = m.albedo * AMBIENT; | ||
| 54 | color += pointLight(pos, n, v, m, lightPos, LIGHT_COLOR); | ||
| 55 | return color; | ||
| 56 | } | ||
| 57 | |||
| 10 | void main() { | 58 | void main() { |
| 11 | // ピクセル座標を0〜1に正規化 | 59 | vec2 p = (2.0 * gl_FragCoord.xy - uResolution) / uResolution.y; |
| 12 | vec2 uv = gl_FragCoord.xy / uResolution; | 60 | float px = 2.0 / uResolution.y; |
| 61 | |||
| 62 | // 光の位置(zは球より手前) | ||
| 63 | #if FOLLOW_MOUSE | ||
| 64 | vec2 mouse = (2.0 * uMouse - uResolution) / uResolution.y; // pと同じ座標にする | ||
| 65 | vec3 lightPos = vec3(mouse, 0.7); | ||
| 66 | #else | ||
| 67 | vec3 lightPos = vec3(0.9 * cos(uTime * 1.25), 0.6 * sin(uTime * 1.25), 0.7); | ||
| 68 | #endif | ||
| 69 | |||
| 70 | // 球 | ||
| 71 | float z = sqrt(max(RADIUS * RADIUS - dot(p, p), 0.0)); | ||
| 72 | vec3 spherePos = vec3(p, z); | ||
| 73 | vec3 sphere = shade(spherePos, spherePos / RADIUS, SPHERE, lightPos); | ||
| 74 | |||
| 75 | // 壁(z = WALL_Zの平面、法線は手前を向く) | ||
| 76 | vec3 wall = shade(vec3(p, WALL_Z), vec3(0.0, 0.0, 1.0), WALL, lightPos); | ||
| 13 | 77 | ||
| 14 | // 時間で動くグラデーション | 78 | float mask = 1.0 - smoothstep(RADIUS - px, RADIUS, length(p)); |
| 15 | vec3 color = 0.5 + 0.5 * cos(uTime + uv.xyx + vec3(0.0, 2.0, 4.0)); | 79 | vec3 color = mix(wall, sphere, mask); |
| 16 | 80 | ||
| 17 | // マウスの周りに白い円 | 81 | // 光の位置に小さな白い点を描く |
| 18 | float d = length(gl_FragCoord.xy - uMouse); | 82 | float marker = 1.0 - smoothstep(0.03 - px, 0.03, length(p - lightPos.xy)); |
| 19 | color = mix(color, vec3(1.0), 1.0 - smoothstep(18.0, 20.0, d)); | 83 | color = mix(color, vec3(1.0), marker); |
| 20 | 84 | ||
| 21 | out_color = vec4(color, 1.0); | 85 | out_color = vec4(color, 1.0); |
| 22 | } | 86 | } |
コメント(0)
まだコメントはありません。
ログインするとコメント出来ます。