WebGLスクール第6回の課題はフラグメントシェーダーで陰影をつけてみようというものでした。
コード量が多く自前の環境向けに書き換えるのがしんどかったので今回はシェーダーのコードと静止画だけを用意する形にしました。
素人が書いたコードで正解とは限らないので割り引いて読んでください。
フラグメントシェーダーでLambert反射
ベースとなるコードでは頂点シェーダー側でLambert反射が実装されていて、それをフラグメントシェーダーで書き直すという課題でした。
各ピクセルの法線方向と光の入射角がわかれば陰影を計算できるので、頂点シェーダー側でvaryingを使い法線情報をフラグメントシェーダーに渡しています。
頂点シェーダー
attribute vec3 position;
attribute vec3 normal;
attribute vec4 color;
uniform mat4 mvpMatrix;
uniform mat4 normalMatrix; // 法線変換行列
varying vec4 vColor;
varying vec3 vNormal;
void main() {
// 法線をまず行列で変換する
vNormal = (normalMatrix * vec4(normal, 0.0)).xyz;
vColor = vec4(color.rgb, color.a);
// MVP 行列と頂点座標を乗算してから出力する
gl_Position = mvpMatrix * vec4(position, 1.0);
}
フラグメントシェーダー
precision mediump float;
varying vec4 vColor;
varying vec3 vNormal; // ワールド座標系での法線情報
const vec3 light = vec3(1.0, 1.0, 1.0);
void main() {
float d = dot(normalize(vNormal), normalize(light));
vec4 c = vec4(vColor.xyz * d, vColor.a);
gl_FragColor = c;
}

フラグメントシェーダーで点光源 + Lambert拡散
+αの課題として点光源の実装も出題されていました。
平行光源の場合にはすべての点で光のベクトルは同じでしたが、点光源はすべての点で異なるので計算してあげる必要があります。
各ピクセル毎のワールド座標系での位置がわかれば良いので頂点シェーダー側でvaryingを使い位置情報をフラグメントシェーダーに渡しています。
あとは基本的に平行光源の場合と同じ要領で計算できますが、点光源の場合には光源からの距離が離れるほど単位面積あたりの放射強度が小さくなります。
そのため点光源からの距離の二乗の分だけ割り引いてあげる必要があります。
頂点シェーダー
attribute vec3 position;
attribute vec3 normal;
attribute vec4 color;
uniform mat4 mMatrix; // 追加: モデル変換行列
uniform mat4 mvpMatrix;
uniform mat4 normalMatrix; // 法線変換行列
varying vec4 vColor;
varying vec3 vNormal; // ワールド座標系における法線ベクトル
varying vec3 vPosition; // ワールド座標系における位置ベクトル
void main() {
// 位置情報の計算
vPosition = (mMatrix * vec4(position, 1.0)).xyz;
vNormal = (normalMatrix * vec4(normal, 0.0)).xyz;
vColor = color.rgba;
// MVP 行列と頂点座標を乗算してから出力する
gl_Position = mvpMatrix * vec4(position, 1.0);
}
フラグメントシェーダー
// 点光源
precision mediump float;
varying vec4 vColor;
varying vec3 vNormal; // ワールド座標系での法線情報
varying vec3 vPosition; // ワールド座標系での位置情報
const float intensity = 4.0;
const float reflectance = 0.3;
const vec3 light = vec3(1.0, 1.0, 0.0); // 点光源の位置. わかりやすさのために中心に配置.
void main() {
vec3 l = light - vPosition; // 点からライトに向かうベクトル
float r2 = pow(length(l), 2.0); // 点光源からの距離の二乗
// Lambert拡散光の計算
float d = dot(normalize(vNormal), normalize(l));
vec4 c = vec4(intensity * reflectance * vColor.xyz * d / r2, vColor[3]);
gl_FragColor = c;
}

自主課題: Phong反射
Lambert反射まで実装したらPhong反射も実装したいと思って書いたコードがこちらです。
これまでに追加した情報に加えてカメラの位置が必要になるので、uniformで渡してあげます。
Phong反射の式は割愛しますが直感的な理解としては、表面で鏡面のように反射した際のベクトルをR、表面と視点を繋いだベクトルをVとしたときに、RとVが近いほど視点に入ってくる光を多くします。
反射した光が視点にそのまま入ってくれば当然明るく、視点以外の方向に反射光はみえないので関係ないというお気持ちだと理解してます。
なお、以下のコードではLambert拡散光や環境光は加算せず、鏡面反射成分がどう見えるかがわかりやすいようにあえて鏡面成分だけを出力しています。
頂点シェーダー
attribute vec3 position;
attribute vec3 normal;
attribute vec4 color;
uniform mat4 mMatrix;
uniform mat4 mvpMatrix;
uniform mat4 normalMatrix; // 法線変換行列
varying vec4 vColor;
varying vec3 vNormal; // ワールド座標系における法線ベクトル
varying vec3 vPosition; // ワールド座標系における位置ベクトル
void main() {
// 法線をまず行列で変換する
vec4 p = mMatrix * vec4(position, 1.0);
vPosition = p.xyz;
vNormal = (normalMatrix * vec4(normal, 0.0)).xyz;
vColor = vec4(color.rgb, color.a);
// MVP 行列と頂点座標を乗算してから出力する
gl_Position = mvpMatrix * vec4(position, 1.0);
}
フラグメントシェーダー
precision mediump float;
uniform vec3 viewpoint; //ワールド座標系でのカメラ位置
varying vec3 vNormal; // ワールド座標系での法線情報
varying vec3 vPosition; // ワールド座標系での位置情報
varying vec4 vColor; // 点の色情報
const float intensity = 5.0;
const float reflectance = 0.3;
const vec3 light = vec3(1.0, 1.0, 0.0); // わかりやすさのために中心に配置
void main() {
vec3 v = viewpoint - vPosition; // 点からカメラに向かうベクトル
vec3 l = light - vPosition; // 点からライトに向かうベクトル
vec3 r = reflect(-l, normalize(vNormal)); // 正反射ベクトルの計算
float g = pow(dot(normalize(r), normalize(v)), 3.0); // Phong反射の計算式
vec4 c = vec4(intensity * reflectance * vColor.xyz * g, vColor[3]);
gl_FragColor = c;
}

まとめ
第6回の課題はglslを使ったクイズという感じでした。
ここまで丁寧にコードがあると自分で色々試してみたくなります。
最近ガウシアンスプラッティングのサーベイ論文を読んだので、時間があればWebGLで書いてみようと思います。
参考文献
- コンピューターグラフィックス: Chapter 4