ベイ・マックス
ベイ・マックス見たわけですよ。すごかったすよ。2014 年の映画としては、男子としては男が添え物と言われる「アナ雪」よりよっぽど面白かったし、技術的にも CG に関わっている身としてはいろいろ悔しいなぁと思うところもありました。また、自分の立場としてもオープニングの「大学」の描かれ方にグッとくるものがあって、もっと頑張らなきゃって無闇にテンション上げられてしまいました。
んで、そのオープニングで主人公のヒロがキャラハン教授と出会った時に発した「キャラハン-キャットムル曲線の!」というセリフで、もう自分の顔がゆるみまくってるのを矯正できずに困ってしまいました。あー、WALL-E の起動音といい、どうしてこういう限定された相手にしか通じない?ような細かいネタを仕込んでくるかなぁ。そう、”キャットムル” は CG の研究者であり PIXAR の創業者であり、現在はウォルト・ディズニー・アニメーション・スタジオの社長も務めてる Edwin Catmull 氏であって (って 1 年生にはこれから授業で話す予定)、氏の数多くの業績の一つに Catmull-Rom 曲線ってのがあります (これは 3 年生と M1 にはもう授業で話した)。
だから、思わずつぶやいちゃったよ…
髪の毛の細分割
さらに言うと、この Catmull-Rom 曲線をこの髪の毛のシリーズで使おうと思ってたところだったので、そのあとドキドキしちゃった。んでね、なんでこれを使おうかと思ったかというと、やっぱり物理シミュレーションは重いわけで、それにゲームなんかだと他にもやらなきゃいけない処理もいろいろあるわけで、やっぱり何とかして処理を軽くしたいって思うわけですよ。それには単純に力の計算の対象となる節点の数を減らすのが一番手っ取り早いんですけど、そうすると角が見えたり陰影が汚くなったりして、見栄えが悪くなってしまいます。実際、前回追加した陰影は、あまり滑らかではありませんでした。

そこで節点を通る曲線を求め、それを細分割することにより、滑らかな髪の毛を表現してみようと思います。この辺の話は (この辺に限らず髪の毛には) もう国内1外2に山のように文献があって、SIGGRAPH 2008 の Course Note3 見ただけで目が回りそうです4。
三次 Hermite 曲線
Catmull-Rom 曲線の説明の前に、三次 Hermite (エルミート) 曲線について説明します。

導き方はエルミート曲線軌道で詳しく解説されています。2 点 $P_0, P_1$ を結び、$P_0$ における接線が $m_0$、$P_1$ における接線が $m_1$ となる曲線
\[P(t), \quad t \in [0, 1]\]を考えます。$P(t)$ が $t$ の 3 次関数なら、
\[P(t) = a t^3 + b t^2 + c t + d\]と書くことができます。したがって、
\[\begin{aligned} P_0 &= P(0) = d \\ P_1 &= P(1) = a + b + c + d \end{aligned}\]となります。次に、$P(t)$ を $t$ で微分すると
\[P'(t) = 3 a t^2 + 2 b t + c\]となりますから、
\[\begin{aligned} m_0 &= P'(0) = c \\ m_1 &= P'(1) = 3 a + 2 b + c \end{aligned}\]となります。これらから $c$ と $d$ を消去すると、次の連立方程式が得られます。
\[\begin{aligned} P_1 - P_0 &= a + b + m_0 \\ m_1 - m_0 &= 3 a + 2 b \end{aligned}\]これを解けば、
\[\begin{aligned} a &= 2(P_0 - P_1) + m_0 + m_1 \\ b &= -3(P_0 - P_1) - 2 m_0 - m_1 \\ c &= m_0 \\ d &= P_0 \end{aligned}\]となります。これを整理して、$P(t)$ は前の図中の式のように表されます。
Catmull-Rom 曲線
Catmull-Rom 曲線は点 $P_i$ における接線 $m_i$ に、$P_i$ のひとつ前の点 $P_{i-1}$ から一つ先の点 $P_{i+1}$ に向かうベクトルを、そのパラメータ $t$ の定義域の大きさで割ったものを用います。$t \in [0, 1]$ であれば、ひとつ前の点と結ぶ曲線 $P_{i-1}(t) = P_i(t - 1)$、一つ次の点と結ぶ曲線 $P_{i+1}(t) = P_i(t + 1)$ と考えて、定義域の大きさは 2 とします。

なお、$m_0, m_1$ のそれぞれに重み $(1 - w)$ をかけたものを Cardinal Spline といい、$w = 1$ で折れ線、$w = 0$ で Catmull-Rom 曲線と同じになります。この $w$ はこの点における張り (Tension) になります。さらに偏り (Bias) や連続性 (Continuity) も制御できるようにしたものは Kochanek-Bartels Spline と呼びます。これは TCB スプラインとして知られています。

ジオメトリシェーダの追加
節点を Catmull-Rom 曲線で補間して線分を細分化する処理は、ジオメトリシェーダで行います。それに伴い、これまでバーテックスシェーダで行っていた処理は細分化により生成した線分に対して行うため、ジオメトリシェーダに移します。バーテックスシェーダでは与えられた頂点属性 (節点の位置) をそのままジオメトリシェーダに送ります。
#version 150 core
#extension GL_ARB_explicit_attrib_location : enable
// 頂点属性
layout (location = 0) in vec4 position; // 節点のローカル座標系での位置
void main()
{
// 視点座標系の座標値
gl_Position = position;
}
ジオメトリシェーダのソースファイル hair.geom を新たに追加します。Catmull-Rom 曲線では、曲線の両端点のほかに、始点のひとつ前の点と終点の一つ先の点が必要になります。したがって、ジオメトリシェーダへの入力となる図形プリミティブに GL_LINES_ADJACENCY を使う必要があります。詳しくは 2009 年 8 月 28 日の記事を参照してください。layout (lines_adjacency) in; はジオメトリシェーダに入力する図形プリミティブに GL_LINES_ADJACENCY を使用します。また、layout (line_strip, max_vertices = 20) out; によりジオメトリシェーダから出力する図形プリミティブとして GL_LINE_STRIP を指定します。max_vertices = 20 はこのジオメトリシェーダ内で生成される頂点属性の最大数です。

#version 150 core
#extension GL_ARB_explicit_attrib_location : enable
layout (lines_adjacency) in;
layout (line_strip, max_vertices = 20) out;
// 定数
const int division = 4; // 曲線の分割数
const vec4 pl = vec4(1.0, 6.0, 4.0, 1.0); // 光源位置
// uniform 変数
uniform mat4 mc; // ビュープロジェクション変換行列
// ラスタライザに送る頂点属性
out vec3 v; // 視線ベクトル
out vec3 l; // 光線ベクトル
out vec3 t; // 接線ベクトル
入力された 4 つの節点から、Catmull-Rom 曲線の係数 $a, b, c, d$ を求めます。
void main()
{
vec4 m0 = (gl_in[2].gl_Position - gl_in[0].gl_Position) * 0.5; // (P1 - P-1) / 2
vec4 m1 = (gl_in[3].gl_Position - gl_in[1].gl_Position) * 0.5; // (P2 - P0) / 2
vec4 dp = gl_in[1].gl_Position - gl_in[2].gl_Position; // P0 - P1
vec4 a = 2.0 * dp + m0 + m1;
vec4 b = -3.0 * dp - 2.0 * m0 - m1;
vec4 c = m0;
vec4 d = gl_in[1].gl_Position;
Catmull-Rom 曲線上の点とその微分を求め、それらから視線ベクトル、光線ベクトル、接線ベクトルを求めてラスタライザに送ります。曲線の分割数は division です。
for (int i = 0; i <= division; ++i)
{
float s = float(i) / float(division);
// Catmull-Rom 曲線
vec4 position = ((a * s + b) * s + c) * s + d;
// スクリーン座標系の座標値
gl_Position = mc * position;
// 視線ベクトルは頂点位置の逆ベクトル
v = -normalize(position.xyz);
// 光線ベクトルは頂点から光源に向かうベクトル
l = normalize((pl - position * pl.w).xyz);
// 接線ベクトルは Catmull-Rom 曲線の微分
t = normalize(((3.0 * a * s + 2.0 * b) * s + c).xyz);
// 頂点の出力
EmitVertex();
}
// 図形の終了
EndPrimitive();
}
シェーダプログラムの読み込み時にジオメトリシェーダ hair.geom を追加します。これに伴い、近傍の節点の位置を保持するテクスチャバッファオブジェクトは必要なくなります。
...
//
// プログラムオブジェクト
//
// 描画用のシェーダプログラムを読み込む
const GLuint hairShader(ggLoadShader("hair.vert", "hair.frag", "hair.geom"));
const GLint hairMcLoc(glGetUniformLocation(hairShader, "mc"));
描画の際には GL_LINES_ADJACENCY を用います。一本の髪の毛を描画する際に指定する頂点数は、節点の数 count[i] より 2 つ多くなります。また、この描画に用いる節点の位置の頂点バッファオブジェクトをマッピングして、最初の節点の手前 (first[i] - 1) と最後の節点の直後 (first[i] + count[i]) にダミーの頂点位置をコピーしておきます。
...
//
// 通常の描画
//
// 頂点配列オブジェクトを選択する
glBindVertexArray(vao[buffer]);
// 描画用のシェーダプログラムを使用する
glUseProgram(hairShader);
// ビュープロジェクション変換行列を設定する
glUniformMatrix4fv(hairMcLoc, 1, GL_FALSE, mc.get());
// 頂点配列を描画する
for (int i = 0; i < hairNumber; ++i)
{
glDrawArrays(GL_LINE_STRIP_ADJACENCY, first[i] - 1, count[i] + 2);
}
// フレームバッファを入れ替える
window.swapBuffers();
以上の変更により、次のように滑らかに表示できるようになります。

-
長谷川 晶二, 諸岡 健一, 宮崎 慎也: “毛髪アニメーションのためのヘアスタイリングモデルに関する研究”, 画像電子学会誌, Vol.42, No.3, pp.353-363 (2013). ↩
-
Tariq, Sarah, and Louis Bavoil: “Large scale hair simulation and real-time generation using simulation levels of detail”, NVIDIA Whitepaper, 2008. ↩
-
Ward, Kelly, et al.: “Hair simulation in computer graphics”, ACM SIGGRAPH 2008 classes. ACM, 2008. ↩
-
というか、この論文の著者の一人の Florence Bertails-Descoubes さんの業績見てたら、自分が情けなくなってきました… ↩