WebGLセミナー第3回復習、基本的なライティングまとめ

WebGLセミナーに参加しています。第4回ももう終わっているのですが、なかなかできてなかった第3回の整理。
第3回のテーマは「ライティングとテクスチャ」。ぶっちゃけテクスチャについてはセミナー中よくわからないまま終わり、復習もできていないので、まずはライティングについてまとめる。

ライティング

光がないとものが見えないのは当たり前ですが、WebGLにはどうやら「光」という概念は無いようです(自分理解)。3DCGのように光源を置けば勝手に計算してくれる、というものではないみたい。
ではどうするかというと、「色」はあるので、色の変化で光を表現すると言えばよさそう。
光が当たる部分は「物体のもともとの色がよりはっきり出る」もしくは「白くなる(白い光の場合)」などの変化があるので、仮想の光が当たっている部分の色をそのように変化してあげれば、「光っているように見える」という感じ。

ライティングで表現される光は大きく分けて3種類。拡散光、反射光、環境光。それぞれ実装の方法が異なる。

拡散光(diffuse)

拡散光はもっとも基本的な光で、物体に当たってあちこちに散らばる光のこと。目線がどこにあるかは関係なく、単に光源と物体の位置関係で決まる。

こんなの(スマホではぐりぐりできない、ごめん)

拡散光に関係するのは光の向きと、物体表面の法線の向き。法線(normal)は物体の各頂点ごとにもっているベクトルで、頂点が向いている向きを表す。球の各頂点は球の外側を向いているだろうし、ドーナツ型の内側部分は内側向きで、外側部分は外側向き。この法線の向きも、各頂点のきちんと自分で設定する必要がある。

要は頂点の向きと光の向きが向かい合っていれば拡散光はマックスで明るい。逆に頂点の向きと光の向きが直角、もしくは少しでも同じ方向を向いている場合、その頂点には「光が当たっていないとみなすことができる」ので、拡散光は0。

これを表現するためにベクトルの内積を用いる。単位ベクトル同士の内積は「同じ方向を向いている時が1、逆方向を向いている時が-1」なので、光の方向の反対方向を向いた単位ベクトルと各頂点の法線単位ベクトルの内積を取ると、「光が一番当たるところは内積1、光が当たらないところは内積0以下」となる。これを0.0〜1.0にclampしてあげて、それをフラグメントシェーダ上のFragColorに乗算すると、光の当たり具合によってFragColorがはっきり見えてくる、という形になる。該当部分のシェーダのコードは以下。

					
void main(){
    vec3 invLight = normalize(invMatrix * vec4(lightDirection, 1.0)).xyz;
    vec3 invEye   = normalize(invMatrix * vec4(eyePosition - centerPoint, 1.0)).xyz;
    vec3 halfVec  = normalize(invLight + invEye);
    float diff = clamp(dot(invLight, vNormal), 0.0, 1.0);
    float spec = clamp(dot(halfVec, vNormal), 0.0, 1.0);
    spec = pow(spec, 10.0);
    gl_FragColor = vec4(vec3(diff), 1.0) * vColor + vec4(vec3(spec), 0.0);
}
					
				

反射光(specular)

いわゆる鏡面反射。磨かれた物体の表面がキラッと光るやつ。

こんなの

反射光は拡散光と違い、物体と光源、さらに視点の位置関係で決まる。「光源の方から物体を見ると眩しい(反射光が強い)」という感じなので、「視線ベクトルと光ベクトルを足して、それを頂点の法線ベクトルと内積取る」という方法で表現可能。さらに反射光はキラっとした感じが強いため、内積の値をべき乗しておく。該当部分のシェーダコードは以下。

					
void main(){
	vec3 invLight = normalize(invMatrix * vec4(lightDirection, 1.0)).xyz;
	vec3 invEye = normalize(invMatrix * vec4(eyePosition - centerPoint, 1.0)).xyz;
	vec3 halfVec = normalize(invLight + invEye);
	float diff = clamp(dot(invLight, vNormal), 0.0, 1.0);
	float spec = clamp(dot(halfVec, vNormal), 0.0, 1.0);
	spec = pow(spec, 9.0);
	gl_FragColor = vec4(vec3(diff), 1.0) * vColor + vec4(vec3(spec), 0.0);
}
					
				

なんでvec4(eyePosition - centerPoint, 1.0)逆行列invMatrixをかけなければならないのか、忘れた。なんでだっけ。

環境光(ambient)

現実世界では、光が当たっていないところはまったく何も見えない、というわけではない。物体の陰は暗くはあるが、見えはする。これは他の物体から反射した光が回り込んでいるからであり、そうした光が、じつは当たり一面を埋め尽くしている。そうした「環境に満ち溢れている光」を「環境光」と呼ぶが、これを正確に計算するのはすごい大変。昔は「間接光」とか「グローバルイルミネーション」とか言われていた気がするけど、じつはそれが3DCGのレンダリングにかかる時間を大幅に大きくしている原因のひとつである。

WebGLではそんな計算やってられないので、「光が当たってるところも当たってないところも等価に明るくする」という方法をとる。

こんなの
分かりにくいが、光が直接当たっていない裏側も微妙に赤みを帯びている。これがいちおう環境光の効果だが、この方法だと、直接光が当たっているところはめっちゃ明るくなってしまうので、調整がめんどくさそう。

該当部分のシェーダコードは以下。specularの時のコードのgl_FragColorにambientという定数を足しているだけ。

					
void main(){
	vec3 invLight = normalize(invMatrix * vec4(lightDirection, 1.0)).xyz;
	vec3 invEye = normalize(invMatrix * vec4(eyePosition - centerPoint, 1.0)).xyz;
	vec3 halfVec = normalize(invLight + invEye);
	float diff = clamp(dot(invLight, vNormal), 0.0, 1.0);
	float spec = clamp(dot(halfVec, vNormal), 0.0, 1.0);
	spec = pow(spec, 9.0);
	vec4 ambient = vec4(0.4, 0.3, 0.3, 1.0);
	gl_FragColor = vec4(vec3(diff), 1.0) * vColor + vec4(vec3(spec), 0.0) + ambient;
}
					
				

まとめ

以上が基本的なライティング3種類。あとはlightDirectionを各頂点ごとにかえたりするだけで点光源を表現したりできる(減衰がめんどくさいけど)。線光源や面光源は謎だけど、とりあえずそこまで必要になることもいまは無いので一旦置いておく。
ライティングをやった以上、「影(shadow)」をつけたくなるけど、結構めんどくさいらしいので、おいおい勉強していく。

あとは鬼門になってるドラッグによるカメラ回転を頑張る。