床井研究室

一度に一つのことしかできない

やらなければならない仕事が二つ以上あると、とてもストレスを感じます。だいたい落ち込んでいるときは物事が決められなくなっているので、仕事が複数あっても優先順位を付けることができません。それに、タスク切り替えにすごく時間がかかる(数時間から1日)ので、効率もよくありません。それで余計に落ち込んでしまいます。周りの人はみんなうまくやってるなぁと思えるので、それでまた落ち込みます。どうしたもんでしょうか。

GLSL の変数

GLSL は C や C++ 言語によく似ていますが、レンダリングのプロセスを反映した仕組みを備えています。特に変数については、アプリケーションプログラムとシェーダプログラムとの間や、シェーダプログラム同士でのデータのやり取りを行うために、以下に示す独特の型修飾子を備えています。また GLSL は、これらにより修飾された多くの組み込み変数1を用意しています。

attribute
アプリケーションプログラムからバーテックスシェーダに渡す、頂点に関係するデータを格納した変数を表します。位置 (gl_Vertex)、色 (gl_Color)、法線ベクトル (gl_Normal)、テクスチャ座標 (gl_MultiTexCoord0 ほか) など、glVertex*() およびそれと一緒に設定するデータがこれに相当します。この変数はバーテックスシェーダにおいて読み出しのみ可能です。
uniform
アプリケーションプログラムからバーテックスシェーダおよびフラグメントシェーダに渡す、光源情報などのあまり変化しないデータを格納した変数を表します。モデルビュー変換行列 (gl_ModelViewMatrix)、透視変換行列 (gl_ProjectionMatrix)、モデルビュー変換行列と透視変換行列の積 (gl_ModelViewProjectionMatrix)、法線ベクトルの変換行列 (gl_NormalMatrix)、テクスチャ変換行列 (gl_TextureMatrix[0] ほか)、光源情報 (gl_LightSource[0] ほか)、材質 (gl_FrontMaterialgl_BackMaterial) など、図形の描画の際にあらかじめ設定しておくデータがこれに相当します。これはバーテックスシェーダとフラグメントシェーダのどちらからでもアクセス可能なグローバルな変数で、いずれも読み出しのみ可能です。
varying
OpenGL の陰影付けに用いられる Gouraud シェーディングでは、頂点ごとに陰影計算を行って頂点における色を求め、ポリゴンの内部の画素の色は頂点の色を補間して決定します。このような処理を実現するために、バーテックスシェーダで varying 変数に値を格納すると、フラグメントシェーダでは格納された値そのままではなく、その値の補間値を取り出すことができるようになっています。バーテックスシェーダで値を格納する varying 変数には、表面色 (gl_FrontColor)、背面色 (gl_BackColor)、テクスチャ座標 (gl_TexCoord[0] ほか) などがあります。またフラグメントシェーダで補間値を取り出す varying 変数には、フラグメントの色 (gl_Color)、テクスチャ座標 (gl_TexCoord[0] ほか) などがあります。gl_BackColor はポリゴンの両面に異なる色を設定する場合(glEnable( GL_VERTEX_PROGRAM_TWO_SIDE ) 実行時)に使用し、この場合 gl_Color からは、視点に対するポリゴンの向きによって、gl_FrontColor あるいは gl_BackColor のどちらかの補間値を取り出すことができます。
const
値が変化しない定数を表します。使用できる光源の数 (gl_MaxLights) やテクスチャユニットの数 (gl_MaxTextureUnits) のように、システムに依存した定数を取り出すことができます。

組み込み変数にはシェーダプログラムの計算結果の出力先として用いられるものがあります。このような組み込み変数には型修飾子が与えられていません。

出力変数
バーテックスシェーダやフラグメントシェーダの計算結果は、それぞれ頂点の座標値と画素の色として出力します。バーテックスシェーダでは変数 gl_Position に頂点の位置を出力します。フラグメントシェーダでは変数 gl_FragColor に画素の色を出力します。フラグメントシェーダで値を出力しない(フラグメントを書き込まない)ときは、この変数に書き込む代わりに discard 命令を呼び出します。このほか、バーテックスシェーダでは点の大きさ (gl_PointSize) やクリッピング座標 (gl_ClipVertex)、フラグメントシェーダではフラグメントの奥行き値 (gl_FragDepth) を出力することもできます。
入力変数
フラグメントシェーダでは、そのフラグメントの画素位置を gl_FragCoord で調べることができます。またそのフラグメントがポリゴンの表なのか裏なのかを gl_FrontFacing で調べることができます。

なお、GLSL の組み込み変数は、上記の限りではありません。詳しくは GLSL 1.2 の仕様書を参照してください2。また、このような変数をユーザが自分で定義して使用することもできます。シェーダプログラム側で定義した attribute 変数や uniform 変数にアプリケーションソフトウェア側から値を設定したり、バーテックスシェーダで独自の varying 変数を設定してフラグメントシェーダで使用したりすることができます。

拡散反射を実装してみる

前回のプログラムではポリゴンの陰影が失われてしまっていますから、バーテックスシェーダで陰影を計算するようにしてみましょう。

まず、フラグメントシェーダのソースプログラム simple.frag を修正します。現在はこのフラグメントシェーダで色を設定していますから、これにバーテックスシェーダで設定した頂点色の補間値を、GLSL の組み込み varying 変数の gl_Color から得ます。

#version 120

// simple.frag

void main ()
{
  gl_FragColor = gl_Color;
}

この状態でプログラムを実行すると、黒い四角が表示されます。

黒い四角形

次にバーテックスシェーダのソースプログラム simple.vert を修正します。バーテックスシェーダにおいて GLSL の組み込み varying 変数 gl_FrontColor に何も設定していないと、gl_Color には黒が入っています。そこで gl_FrontColor に色を設定してみます。gl_FrontColor は表面のポリゴンの頂点色です。また、vec4() は () 内の値を4要素の実数からなるベクトルに変換(キャスト)します。

#version 120

// simple.vert

void main()
{
  // 頂点のクリッピング座標値
  gl_Position = ftransform();

  // 頂点の色
  gl_FrontColor = vec4(1.0, 0.0, 0.0, 1.0);
}

これを実行すると、先ほどと同じ陰影の付かない赤い四角形が表示されるはずです。これにより、色のデータが varying 変数を介してバーテックスシェーダからフラグメントシェーダに送られていることがわかります。

陰影の付かない赤い四角形

それではここで、拡散反射光の算出を行ってみましょう。まず、陰影を求める頂点の、視点座標系における位置を求めます。これは図形のローカル座標系の頂点位置 gl_Vertex にモデルビュー変換行列 gl_ModelViewMatrix を乗じれば得られます。視点座標系というのは、カメラの位置を原点とし、その先にスクリーンを置いた座標系です。

#version 120

// simple.vert

void main()
{
  // 頂点のクリッピング座標値
  gl_Position = ftransform();

  // 頂点のワールド座標値
  vec4 position = gl_ModelViewMatrix * gl_Vertex;

陰影を計算するには、その位置における法線ベクトルも必要になります。これはローカル座標系における頂点の法線ベクトル gl_Normal を視点座標系に移せばよいのですが、これにモデルビュー変換行列 gl_ModelViewMatrix を使うと、正しく変換されません。法線ベクトルの変換には、gl_NormalMatrix を使います。

  // 法線ベクトル
  vec3 normal = normalize(gl_NormalMatrix * gl_Normal);

0番目の光源位置は uniform 変数 gl_LightSource[0].position で得られます。また物体表面上の点の視点座標系における位置は、gl_ModelViewMatrix * gl_Vertex で求めることができます。したがって光線ベクトルは、これらの差から求めることができます。

  // 光線ベクトル
  vec3 light = normalize((gl_LightSource[0].position * position.w
    - gl_LightSource[0].position.w * position).xyz);

vec3, vec4 はそれぞれ3要素、4要素の実数型のベクトルを表します。gl_LightSource[0].position.xyz.xyz は、ベクトル gl_LightSource[0].position の4つの要素のうち、xyz の3つの成分を(この順で)使用することを示します。normalize() はベクトルを正規化する GLSL の組み込み関数です。

拡散反射率 diffuse は光線ベクトル light と法線ベクトル normal の内積により求めます。これに光源強度の拡散反射光成分 gl_LightSource[0].diffuse と拡散反射係数 gl_FrontMaterial.diffuse を乗じて、拡散反射光強度を求めます。dot() は内積を求める GLSL の組み込み関数で、その結果が負の時は 0 になるよう GLSL の組み込み関数 max() を用いて dot() と 0 の大きい方を求めます。

  // 拡散反射率
  float diffuse = max(dot(light, normal), 0.0);

  // 頂点の色
  gl_FrontColor = gl_LightSource[0].diffuse * gl_FrontMaterial.diffuse * diffuse;
}

これでポリゴンに暗めの赤の陰影をつけることができます。

陰影をつけた赤い四角形

鏡面反射を実装してみる

それでは、これにさらに鏡面反射成分を追加してみましょう。鏡面反射光強度の計算には、視線ベクトル $\mathbf{v}$ と光線ベクトル $\mathbf{l}$ の中間ベクトル $\mathbf{h}$ と、法線ベクトル$\mathbf{n}$ との内積を用いることにします。

\[\begin{align} \mathbf{h} &= \frac{\mathbf{l} - \mathbf{v}}{\left|\mathbf{l} - \mathbf{v}\right|} \\ I_s &= k_{spec}I_l\left(\mathbf{n}\cdot\mathbf{h}\right)^{k_{shi}} \end{align}\]

視点座標系では視点の位置は原点にあるので、視線ベクトル view ($\mathbf{v}$) は物体表面上の点の位置ベクトルの逆ベクトルを正規化したものになります。次に、正規化した光線ベクトル light ($\mathbf{l}$) と正規化した視線ベクトル view の逆ベクトルとの和から中間ベクトル halfway ($\mathbf{h}$) を求めます。

#version 120

// simple.vert

void main()
{
  // 頂点のクリッピング座標値
  gl_Position = ftransform();

  // 頂点のワールド座標値
  vec4 position = gl_ModelViewMatrix * gl_Vertex;

  // 法線ベクトル
  vec3 normal = normalize(gl_NormalMatrix * gl_Normal);

  // 光線ベクトル
  vec3 light = normalize((gl_LightSource[0].position * position.w
    - gl_LightSource[0].position.w * position).xyz);

  // 拡散反射率
  float diffuse = max(dot(light, normal), 0.0);

  // 視線ベクトル
  vec3 view = -normalize(position.xyz);

  // 中間ベクトル
  vec3 halfway = normalize(light + view);

鏡面反射率 specular には、この中間ベクトル halfway と法線ベクトル normal の内積を求め、max() 関数を使って負の値が 0 になるようにした後、指数関数 pow() を使って輝き係数 gl_FrontMaterial.shininess によるべき乗したものを用います。

  // 鏡面反射率
  float specular = pow(max(dot(normal, halfway), 0.0), gl_FrontMaterial.shininess);

そして光源強度の鏡面反射光成分 gl_LightSource[0].specular と鏡面反射係数 gl_FrontMaterial.specular の積にこの specular を乗じて鏡面反射光強度を求め、これと環境光の反射光強度を先ほど求めた拡散反射光強度に加えて gl_FrontColor に代入します。

  // 頂点の色
  gl_FrontColor = gl_LightSource[0].diffuse * gl_FrontMaterial.diffuse * diffuse
                + gl_LightSource[0].specular * gl_FrontMaterial.specular * specular
                + gl_LightSource[0].ambient * gl_FrontMaterial.ambient;

  // 頂点位置
  gl_Position = ftransform();
}

なお、光源強度と反射係数の積は、あらかじめ gl_FrontLightProduct という uniform 変数に格納されています。この部分を置き換えると、最終的なプログラムは次のようになります。

  // 頂点の色
  gl_FrontColor = gl_FrontLightProduct[0].ambient
                + gl_FrontLightProduct[0].diffuse * diffuse
                + gl_FrontLightProduct[0].specular * specular;
}

これで通常の OpenGL による陰影付けと同じ、Gouraud シェーディングが実装できました。

Gouraud シェーディングによる赤い四角形 Gouraud シェーディングによる赤い四角形を斜めから見たところ

ティーポットのような曲面では、ポリゴンの境界がわずかながら見えてしまいます。

Gouraud シェーディングによる赤いティーポット

Phong シェーディングを実装してみる

Phong シェーディングは、前述の Gouraud シェーディングで実装したバーテックスシェーダの中の陰影計算を、フラグメントシェーダに移すことにより実現できます。その際、陰影計算を画素単位に行うために、オブジェクト表面上の点の視点座標系での位置と、その点における法線ベクトルが必要になります。

そこでローカル変数 positionnormal を、ともに varying 変数として宣言し直すことにします。そしてバーテックスシェーダでこれらの値を計算し、フラグメントシェーダでこれらの値の補間値を参照します。positionnormalvec3 型の varying として宣言するので、main の中の positionnormal の型宣言の vec3 は削除してください。また、その次の light の計算から gl_Position の計算の前までを削除してください。

#version 120

// simple.vert

// ラスタライザに送る頂点の位置
varying vec4 position;

// ラスタライザに送る頂点の法線ベクトル
varying vec3 normal;

void main()
{
  // 頂点位置
  position = gl_ModelViewMatrix * gl_Vertex;

  // 法線ベクトル
  normal = normalize(gl_NormalMatrix * gl_Normal);

  // 途中削除

  // 頂点位置
  gl_Position = ftransform();
}

バーテックスシェーダで計算した positionnormal の補間値を得るために、フラグメントシェーダでもこれらを varying 変数として宣言します。ただし normal は線形補間により単位ベクトルではなくなっているので、正規化したものを fnormal に得ています。

#version 120

// simple.frag

// ラスタライザから受け取る頂点の位置の補間値
varying vec4 position;

// ラスタライザから受け取る頂点の法線ベクトルの補間値
varying vec3 normal;

void main ()
{
  // 法線ベクトル
  vec3 fnormal = normalize(normal);

バーテックスシェーダから削除した部分は、そっくりそのままフラグメントシェーダの、この後の部分に移します。その際、normalfnormal に変更します。

  // 光線ベクトル
  vec3 light = normalize((gl_LightSource[0].position * position.w
    - gl_LightSource[0].position.w * position).xyz);

  // 視線ベクトル
  vec3 view = -normalize(position.xyz);

  // 中間ベクトル
  vec3 halfway = normalize(light + view);

  // 拡散反射率
  float diffuse = max(dot(fnormal, light), 0.0);

  // 鏡面反射率
  float specular = pow(max(dot(fnormal, halfway), 0.0), gl_FrontMaterial.shininess);

さらに、この計算結果の格納先を、gl_FrontColor(頂点色)から gl_FragColor(画素色)に書き換えます。

  // フラグメントの色
  gl_FragColor = gl_FrontLightProduct[0].ambient
               + gl_FrontLightProduct[0].diffuse * diffuse
               + gl_FrontLightProduct[0].specular * specular;
}

これで Phong シェーディングが実装できました。正面から光を当てたときに、ハイライトが消失していないことを確認してください。

Phong シェーディングによる赤い四角形 Phong ーシェーディングによる赤い四角形を斜めから見たところ

またティーポットにおいても、ポリゴンの境界が現れるようなことはありません。

Phong シェーディングによる赤いティーポット

  1. これらの組み込み変数は、この記事の執筆時点の GLSL バージョン 1.2 で使われていたものです。固定シェーダの設定値を引き継ぐものなので、今は使われていません。(2026 年 5 月 21 日追記) 

  2. 今だったら GLSL 4.6 の仕様書を見ましょう。(2026 年 5 月 21 日追記)