RenderMonkey
アプリケーションプログラムがシェーダプログラムやテクスチャを読み込めるようになっていれば、シェーダプログラムの開発自体は ATI の RenderMonkey1 や nVIDIA の FX Composer のようなシェーダ開発ツールを使ったほうが格段に楽ができます。それでも、ある程度は GLSL を知っていたほうが有利だとは思います。まあ、GLSL を使っていると、プログラミングの知識より、CG そのものの知識のほうが重要だなあとしみじみ思えてくるのですが。
似非異方性反射の実現
異方性反射とは、光の反射光強度が入射方向の接線成分にも依存するような反射のことをいいます。これは主にヘアーライン仕上げされた金属表面などに現れます。人の髪に現れる「天使の輪」なんかもこの現象です。 ところで前々回では、GLSL でバンプマッピングを実装するために、頂点に接線ベクトルの情報を与えました。
物体表面上で接線方向が決まっていれば、これを使って異方性反射も実装できそうです。そこで、現時点で GLSL に対応している RenderMonkey のサンプルにあった Anisotropic.rfx というシェーダを見てみました。しかし、やってることがなんだか私にはよくわからなかったので、代わりに適当に思いついた方法で「似非」異方性反射を実装してみることにしました。
異方性反射を正しく実装するには、物体表面の反射特性を精密にモデル化する必要があります。しかし、これはそれなりに面倒な気がします。安直に考えれば、異方性反射は反射率を接線方向から従法線方向に向かって適当に変化させることで実現できそうです。そこで鏡面反射強度のローブ(lobe, 反射光の方向分布の包絡形状)を、接線方向に引き延ばしてみることにします。鏡面反射光成分のローブは輝き係数 shininess で決まるので、フラグメントシェーダでこれを変化させます。
ここでは第4回のシェーダ bump.vert と bump.frag を、それぞれ anisotropic.vert と anisotropic.frag という名前でコピーしたものを書き換えることにします(アプリケーションプログラムが readShaderSource() で読み込むファイル名も、それに合わせて変更してください)。もとのフラグメントシェーダは次のようになっています。
#version 120
// anisotropic.frag
// ラスタライザから受け取る接空間の光線ベクトルの補間値
varying vec3 tlight;
// ラスタライザから受け取る接空間の視線ベクトルの補間値
varying vec3 tview;
// テクスチャのサンプラ
uniform sampler2D color;
void main ()
{
// テクスチャから画素の色を得る
vec4 fcolor = texture2DProj(color, gl_TexCoord[0]);
// 接空間における法線ベクトル
vec3 normal = fcolor.rgb * 2.0 - 1.0;
// 接空間における光線ベクトル
vec3 light = normalize(tlight);
// 拡散反射率
float diffuse = max(dot(normal, light), 0.0);
// 接空間における視線ベクトル
vec3 view = normalize(tview);
// 接空間における中間ベクトル
vec3 halfway = normalize(light + view);
// 鏡面反射率
float specular = pow(max(dot(normal, halfway), 0.0),
halfway.y * halfway.y * gl_FrontMaterial.shininess);
// フラグメントの色
gl_FragColor = gl_FrontLightProduct[0].ambient
+ gl_FrontLightProduct[0].diffuse * diffuse
+ gl_FrontLightProduct[0].specular * specular;
}
ハイライトが接線方向に広がるようにするには、中間ベクトルが従法線ベクトルの垂直方向に近づくほど、shininess が小さくなるようにします。そこで、中間ベクトルと従法線ベクトルとの内積の絶対値を、shininess に乗じます。ここでは接空間の従法線ベクトルを (0, 1, 0) に固定していますから、中間ベクトル halfway との内積は、halfway.y になります。ここでは絶対値を求める代わりに、これを2乗しています。
// 鏡面反射率
float specular = pow(max(dot(normal, halfway), 0.0),
halfway.y * halfway.y * gl_FrontMaterial.shininess);
これだけで、「天使の輪」っぽいハイライトが現れます。

筋状の高さマップを使う
もともとのバンプだと静止画では効果がよくわからない(動かしてみるとそれらしく見えます)ので、バンプマッピングに使う高さマップのテクスチャを筋状のものに変えてみます。これはノイズの画像をぼかして縦方向に引き伸ばしたものです。

こうすると、髪の毛っぽっくなったと思いません?

線状のハイライトの幅は、shininess で制御できます。ただし、glMaterialf() では GL_SHININESS に 128 を超える値を指定できないので、これを超える値を指定する場合はフラグメントシェーダ内に直接書き込むか、別に uniform 変数を用意する必要があります。
// 鏡面反射率
float specular = pow(max(dot(normal, halfway), 0.0),
halfway.y * halfway.y * 1000.0);
shininess を 1000 くらいにすると、こういう絵になります。

よくよく考えるとおかしなところがいっぱいあるんですけど、「似非」ということで。
Kajiya-Kay モデルを実装する (2026 年 7 月 4 日追記)
このように、接平面の中間ベクトルの y 成分 halfway.y の二乗で輝き係数 gl_FrontMaterial.shininess を偏重して異方性を持たせることで髪の毛っぽいハイライトが発生させることができるのは面白いと思ってたんですけど、これには物理的な根拠がありません。そこで、異方性反射モデルの古典である Kajiya-Kay モデル2を実装してみます3。ただし、これはバンプマッピングを反映していません。
Kajiya-Kay モデルは受光面を円筒に見立てたモデルです。円筒に入射した光 (a) は、入射した位置によってさまざまな方向に反射 (b) します。したがって、円筒面に入射した光の正反射光は、円筒の軸を中心とした円錐形に放射される (c) と考えます。

実際、ヘアライン仕上げの面にレーザーポインタを当ててみたら、本当にそういう反射光が出ていました。

円筒面の真正面からあたった光が円筒をすり抜けて向こう側に到達するというのは直観に反する気がしますが、ここでは円筒が無視できるほど細いと仮定します4。拡散反射光は、ランバートの余弦法則にもとづけば、入射光ベクトルと入射点の法線ベクトルの内積により決定されますが、このような円柱の場合は、法線ベクトル $\mathbf{n}_{diff}$ は円筒の軸 $\mathbf{t}$ と垂直に交わる平面上に分布することになります。したがって Kajiya-Kay モデルにおける入射角 $\theta_i’$ は、この面に対する入射光ベクトルのなす角になります。

したがって、
\[\mathbf{l}\cdot\mathbf{t}=\sin\theta_i'\]より、
\[\overline{\cos}\theta_i'=\sqrt{1-(\mathbf{l}\cdot\mathbf{t})^2}\]となります。
鏡面反射光についても同様に、円筒の軸 $\mathbf{t}$ と垂直な平面に対する視線ベクトル $\mathbf{v}$ のなす角を $\theta_r’$ と置きます。

したがって、
\[\mathbf{v}\cdot\mathbf{t}=\sin\theta_r'\]より
\[\cos\theta_r'=\sqrt{1-(\mathbf{v}\cdot\mathbf{t})^2}\]となります。正反射光ベクトル $\mathbf{r}$ と視線ベクトル $\mathbf{v}$ のなす角を $\alpha_i’$ とし、円筒の軸に垂直な面上での方位角の差を $\phi$ とすると、余弦定理より、
\[\cos\alpha_i'=\cos\theta_i'\cos\theta_r'\cos\phi-\sin\theta_i'\sin\theta_r'\]となります。最も反射光が強くなる $\phi=0$(すなわち $\cos\phi=1$)のとき、
\[\overline{\cos}\alpha_i'=\cos\theta_i'\cos\theta_r'-\sin\theta_i'\sin\theta_r'\]となります。ここに $\sin\theta_i’=\mathbf{l}\cdot\mathbf{t}$、$\cos\theta_i’=\sqrt{1-(\mathbf{l}\cdot\mathbf{t})^2}$、$\sin\theta_r’=\mathbf{v}\cdot\mathbf{t}$、$\cos\theta_r’=\sqrt{1-(\mathbf{v}\cdot\mathbf{t})^2}$ を代入し、負の値を $0$ でクランプすると、
\[\overline{\cos}\alpha_i'=\max(\sqrt{1-(\mathbf{l}\cdot\mathbf{t})^2}\sqrt{1-(\mathbf{v}\cdot\mathbf{t})^2} -(\mathbf{l}\cdot\mathbf{t})(\mathbf{v}\cdot\mathbf{t}), 0)\]となります。
#version 120
// anisotropic.frag (Kajiya-Kay モデル)
// ラスタライザから受け取る接空間の光線ベクトルの補間値
varying vec3 tlight;
// ラスタライザから受け取る接空間の視線ベクトルの補間値
varying vec3 tview;
// テクスチャのサンプラ
uniform sampler2D color;
void main ()
{
// テクスチャから画素の色を得る
vec4 fcolor = texture2DProj(color, gl_TexCoord[0]);
// 接空間における法線ベクトル
vec3 normal = fcolor.rgb * 2.0 - 1.0;
// 接空間における光線ベクトル
vec3 light = normalize(tlight);
// 接空間の y 方向
vec3 t = vec3(0.0, 1.0, 0.0);
// 拡散反射率
float lt = dot(light, t);
float diffuse = sqrt(1.0 - lt * lt);
// 鏡面反射率
float vt = dot(normalize(tview), t);
float specular = pow(max(diffuse * sqrt(1.0 - vt * vt) - lt * vt, 0.0),
gl_FrontMaterial.shininess);
// フラグメントの色
gl_FragColor = gl_FrontLightProduct[0].ambient
+ gl_FrontLightProduct[0].diffuse * diffuse
+ gl_FrontLightProduct[0].specular * specular;
}
-
“RenderMonkey has reached end of life.” だそうです。(2026 年 7 月 2 日追記) ↩
-
Kajiya, James T., and Timothy L. Kay. “Rendering fur with three dimensional textures.” ACM Siggraph Computer Graphics 23.3 (1989): 271-280. (2026 年 7 月 4 日追記) ↩
-
実は、これは授業の宿題にしていたので、これまでブログには書いていませんでした。でも先日、この最後の授業を終えたので (本年度で私が定年退職するので)、もう書いていいんじゃないかと思いました。(2026 年 7 月 4 日追記) ↩
-
CG の場合、例えば面積を持たないのに光を放射する点光源とか、ご都合により大雑把な近似をおこなうことがよくあります。 ↩