はじめに
今回はライティングの計算をフラグメントシェーダで行います。
1. 修正前のシェーダ
前後比較をわかりやすくするために、今までのライティングの計算を頂点シェーダで行っているシェーダスクリプトを掲載します。
#version 410
layout (location=0) in vec3 vertex_pos;
layout (location=1) in vec3 vertex_normal;
layout (location=2) in vec4 vertex_color;
uniform vec3 light_pos;
uniform float light_attenuation;
uniform vec3 spot_dir;
uniform float spot_phi;
uniform float spot_theta;
uniform float spot_falloff;
uniform vec3 eye_dir;
uniform mat4 pvm_mat;
uniform mat4 model_mat;
uniform vec4 diffuse_color;
uniform vec4 ambient_color;
uniform vec4 specular_color;
uniform float specular_shininess;
out vec4 color;
void main()
{
gl_Position = vec4(vertex_pos, 1.0) * pvm_mat;
vec3 light_vec_dir = (vec4(vertex_pos, 1.0) * model_mat).xyz - light_pos;
float light_len = length(light_vec_dir);
float attenuation = 1.0 / (light_attenuation * light_len * light_len);
vec3 light_vec_dirN = normalize(light_vec_dir);
vec3 spor_dirN = normalize(spot_dir);
float cos_alpha = dot(light_vec_dirN, spor_dirN);
float cos_half_theta = cos(spot_theta / 2.0);
float cos_half_phi = cos(spot_phi / 2.0);
if (cos_alpha <= cos_half_phi)
{
// out-range
// attenuation * 0.f;
color = ambient_color;
return;
}
else
{
if (cos_alpha > cos_half_theta)
{
// inner corn
// attenuation * 1.f
}
else
{
// outer corn
attenuation *= pow((cos_alpha - cos_half_phi)/(cos_half_theta - cos_half_phi), spot_falloff);
}
vec3 normal = normalize((vec4(vertex_normal, 0.0) * model_mat).xyz);
vec3 light = -light_vec_dirN;
float diffuse_power = clamp(dot(normal, light), 0.0, 1.0);
vec3 eye = -normalize(eye_dir);
vec3 half_vec = normalize(light + eye);
float specular = pow(clamp(dot(normal, half_vec), 0.0, 1.0), specular_shininess);
color = vertex_color * diffuse_color * diffuse_power * attenuation + ambient_color + specular_color * specular;
}
}
#version 410
in vec4 color;
layout (location=0) out vec4 frag_color;
void main()
{
frag_color = color;
}
以下が、前回の自己発光パラメータを追加したスポットライトのレンダリング結果です。
スポットライトがあたっているplaneの左下のあたりに四角いメッシュが少し見えてしまっています。
plabe.objをもっと粗いメッシュのものに差し替えるとより顕著に見えるようになります。
頂点シェーダからフラグメントシェーダには color
という変数を引き渡していて、
フラグメントシェーダは頂点間の color
を線形補間して描画しています。
しかしながら、頂点の間隔が大きかったり、光源と頂点の位置関係によって、線形補間では光の減衰をうまく表現できないことがあります。
色自体を線形補間するではなく、計算に用いる頂点座標(vertex_pos)自体を線形補間して光源までの距離を計算してそこから色を求めれば、より減衰のシミュレーション結果が精細にできそうです。
2. フラグメントシェーダにライティングを移す意味
さきほど確認したように、頂点シェーダでライティングを計算した場合、計算の粒度は頂点単位となってしまいます。メッシュの細かいオブジェクトのライティングの計算には多少は不自然な部分がありましたが場合によっては許容できるでしょう。一方でメッシュの粗いオブジェクトのレンダリングでは到底許容できるような結果ではありませんでした。
このように、頂点シェーダでのライティングには、メッシュの粗さにレンダリング結果が大きく依存してしまうという欠点があります。
もちろん、納得の行くレンダリング結果になるようにオブジェクトのメッシュが細かくなるように作り直すというのも解決策です。
別の解決策として、 ライディングの計算をフラグメントシェーダに移す 方法もあります。
フラグメントシェーダに渡している color
という変数が頂点間で線形補間されていますが、実は線形補間できるのは色情報に限ったものではありません。
頂点や法線など、頂点シェーダからフラグメントシェーダに引き渡している変数が線形補間可能です。
頂点シェーダからフラグメントシェーダに線形線形補間された頂点情報が渡ることで、
ピクセル単位で光源までの距離や位置関係を計算し直すことができ、ピクセル単位で光の減衰具合をシミュレーションすることができます。
3. 修正後のシェーダ
#version 410
layout (location=0) in vec3 vertex_pos;
layout (location=1) in vec3 vertex_normal;
layout (location=2) in vec4 vertex_color;
uniform mat4 pvm_mat;
uniform mat4 model_mat;
out vec3 v_position;
out vec3 v_normal;
out vec4 v_color;
void main()
{
v_position = (vec4(vertex_pos, 1.0) * model_mat).xyz;
v_normal = vertex_normal;
v_color = vertex_color;
gl_Position = vec4(vertex_pos, 1.0) * pvm_mat;
}
#version 410
in vec3 v_position;
in vec3 v_normal;
in vec4 v_color;
uniform vec3 light_pos;
uniform float light_attenuation;
uniform vec3 spot_dir;
uniform float spot_phi;
uniform float spot_theta;
uniform float spot_falloff;
uniform vec3 eye_dir;
uniform mat4 pvm_mat;
uniform mat4 model_mat;
uniform vec4 diffuse_color;
uniform vec4 ambient_color;
uniform vec4 specular_color;
uniform float specular_shininess;
uniform vec4 emissive_color;
layout (location=0) out vec4 frag_color;
void main()
{
vec3 light_vec_dir = v_position - light_pos;
float light_len = length(light_vec_dir);
float attenuation = 1.0 / (light_attenuation * light_len * light_len);
vec3 light_vec_dirN = normalize(light_vec_dir);
vec3 spor_dirN = normalize(spot_dir);
float cos_alpha = dot(light_vec_dirN, spor_dirN);
float cos_half_theta = cos(spot_theta / 2.0);
float cos_half_phi = cos(spot_phi / 2.0);
if (cos_alpha <= cos_half_phi)
{
// out-range
// attenuation * 0.f;
frag_color = ambient_color + emissive_color;
}
else
{
if (cos_alpha > cos_half_theta)
{
// inner corn
// attenuation * 1.f
}
else
{
// outer corn
attenuation *= pow((cos_alpha - cos_half_phi)/(cos_half_theta - cos_half_phi), spot_falloff);
}
vec3 normal = normalize((vec4(v_normal, 0.0) * model_mat).xyz);
vec3 light = -light_vec_dirN;
float diffuse_power = clamp(dot(normal, light), 0.0, 1.0);
vec3 eye = -normalize(eye_dir);
vec3 half_vec = normalize(light + eye);
float specular = pow(clamp(dot(normal, half_vec), 0.0, 1.0), specular_shininess);
frag_color = v_color * diffuse_color * diffuse_power * attenuation + ambient_color + specular_color * specular + emissive_color;
}
}
大部分のライティングの計算がフラグメントシェーダに移動したことがわかります。
頂点の位置・法線・色を v_position, v_normal, v_color という変数名で詰めているだけです。
フラグメントシェーダに引き渡す変数は out
で指定します(古いGLSLのバージョンではvaringという名前で引き渡していました)。
そしてフラグメントシェーダ側では in
で同名の変数を受け取っています。
ライティングの計算式自体は前回から変わっていません。
フラグメントシェーダで描画した結果が以下になります。
planeオブジェクトのスポットライトが当たっていた左下のあたりの四角いジャギがなくなっています。
ちなみにさきほど頂点シェーダの例で用いた粗いメッシュのplane.objに差し替えても、
違和感のないレンダリング結果になっています。
※2018/10/27 修正後の頂点シェーダーのGLSLコードを修正(使用しなくなったuniform変数の定義を削除)