1998/12/23作成
1999/8/13更新
1999/8/20サンプル追加
以下、AnimeBodyで行っている、リアルタイムにアニメ調の3D表示を実現するための方法を書きます。なお、フラットシェーディング、グーローシェーディング、隠面処理などの3DCGの基本的な知識があることを前提で書きます。
AnimeBodyでポイントとなっているのは以下の4点です。
IK(インバースキネマティクス)の考え方が基本となっており、手足の位置から各関節の位置を逆算します。本気でこれをやる場合、体→手などへつながる各座標系の合成を考えて、その逆変換を解く必要がありますが、これは非常に面倒なのに加えて、リアルタイム処理の場合、これで動かしてみるとなんか動きが不自然です。それは力学を無視して、各関節の制約情報のみで強制的に処理しているからです。
ですので、手足の長さが絶対変わってはいけないとか、関節がちょっとでも外れることは許さないというのであれば別ですが、この程度ならちょっとした力学計算の応用で処理する方が自然です。
AnimeBodyでは具体的には、各制御点(肩、ひじ、手首など)を力点と考え、関節間の骨を、特定の距離で力が0となる強力なバネと考え、そこに働く力を計算して各制御点を動かしてます。そのため、力がかかると各制御点間の距離が変化(手足が伸びる(^^;)してしまいますが、動き自身は結構自然なものになります。
で実際の処理ですが、AnimeBodyではActiveDrawの関節処理と違い物体の回転を考えてません。また、物体の速度も無視してます。単に、
func 関連計算()
{
全制御点をバックアップ();
for (全ての関節間の接続) {
Vector dp = old_p1 - old_p2;
double dis = dp.abs();
Vector f = dp / dis * (dis - r) * k;
p1 += f * DT;
p2 -= f * DT;
}
}
こんな感じです。制御点間の全関連に関して、位置の違いを求め(dp)、その距離を求め(dis)、その情報から単位ベクトルを求め(dp / dis)、正常な制御点間の距離(r)と実際の距離を元に、フックの法則(単純バネは距離に比例した力がかかる)に従い、係数Kで制御点の位置を動かします。
ちゃんと力学に従えば本当は直接位置を変えるのではなく、力点に加わる力を質量で割って加速度を求めて二回積分ですが、んなまじめな計算必要ないなら、こんなんでもOKです。
なお、関節間の接続は一つだけでなく、一つの制御点に複数の接続があるわけですので、今現在の頂点位置を元に、頂点位置を動かしてはいけません。でないと、各関連の処理順番によって結果が変わってしまいます。そのため必ず、一つ前のステップの頂点位置から力をもとめます。上のプログラムで制御点情報をバックアップしてるのとか、制御点間の距離を求めるのに、バックアップした前回の制御点位置を用いているのはこのためです。このあたりは数値計算では基本です。
あと、この方式で関節を表す場合、バネ係数kを非常に大きくしてやる必要がありますが、単にkを大きくしただけだと計算が発散してものすごい値になってしまいます。そのために、計算ステップDTを小さくする必要がありますが、このままだと結果的にkが小さくなり、やっぱり間延びした動きにしかなりません。この解決手法はいろいろありますが(陰解法とかもっと精度の良い計算手法など)、一番簡単なのは実際のDTと内部計算のDTを別のものとして陽解法で解くことです。
単に、
{
for (メインループ) {
// 関節計算
for (i=0;i<LOOP;i++)
関連計算(); // 内部SubDTはMainDTを計算回数(LOOP)で割ったもの
表示();
}
}
こうしてDTを小さくするとともに、時間の進行速度を速めるだけです(^^;
CPUパワーを余計に食いますが、今のマシンは早いし、どうせいんちき計算なのでこんなのでOK(笑)。
適当にモデラで作ってください・・・といきたいとこですが、この手法を用いる場合、形状には少し注意が必要です。
次に述べるエッジ検出アルゴリズムでは各面の接続情報を利用するので、腕とボディのつなぎ目などは別オブジェクトでなく、滑らかにつながってないと綺麗に処理されません。面がつながっていないエッジには、必ずエッジ線が出てしまします。そのため、もし腕などを動かす場合、単に各パーツオブジェクトを移動・回転させるだけではだめで、各パーツ間をきちんとポリゴンで繋げてやる必要があります。
で、AnimeBodyでは(たまに破綻してますが)ひじやひざに関してはこの接続処理をやっています。ただ、あまりに面倒だったので、ボディと各腕間の関節に関してはこの処理をしていません。腕の根元をボディに埋め込ませてごまかしてます(^^;.。そのため、肩関節とか股関節が何か変です。
あと、上で述べた関節処理だと、制御点間の距離が一定でなく、手足が伸びます。そのためそれにあわせて各パーツも伸ばしてやる必要があります。AnimeBodyでは、このあたりが面倒だったのと、どうせ私にはモデラが使いこなせない(というか持ってない(^^;) ため、腕とかボディとかの形状をあらわす関数(各制御点を包み込むような形状となる関数)を作ってしまい、それを変形させ、そこから表面ポリゴンをつくるという手法にしています。メタボールみたいな処理ですが、少しは関節が外れにくくなってるはず。ただ、お尻の形状がうまく関数で表せなかったので、お尻が美しくないです(^^;
これがアニメ調の肝です。実は「アニメ調=エッジ強調」と断言してもOKで、次のアニメ塗りは実はアニメ調に必須ではありません。
このアニメ絵の特徴たるエッジですが、アニメーターが適当に書いているように見えますが、よく観察するとエッジ出現にはきちんと法則があります。それは、
ということです。1は当たり前ですが、わかりにくいのは2です。
たとえば、球体のように完全に滑らかな物体の場合、どの辺が実際の見た目上のエッジとなるかは、表示してみないとわかりません。ですが2の法則から、視点から見てちょうど真横になる面が視点から見て円状に存在し、この面(正確には線)がエッジになります。
さて、問題はこのエッジラインの求め方ですが、もし完全な連続面を仮定すると求めるのが非常に面倒です。ですが、表示される対象が、今現在一般的な3D技法であるポリゴン(多角形)によるサーフェイスモデル(表面のみのモデル)ならば、話が非常に簡単になります。
本来ならば、連続面におけるエッジは滑らかに連続した複雑な線となりますが、ポリゴンで表示している限り、実際に表示されているエッジは滑らかなものでなく、必ず直線で構成されています。すなわち、ポリゴンモデルにおけるポリゴンのエッジのどれかが、実際の物体のエッジになります。
となると話は簡単で、あるポリゴンのエッジが物体のエッジかそうでないのかだけを判断し、物体のエッジと判断されたら、実際にそのエッジを黒で表示すればよいことになります。ですので、具体的な判断基準は上の法則に従い、
となります。やはり2の方はわかり難いですが、ここまでくれば3Dポリゴンをやっている人にとってはお約束の処理の応用で簡単に処理できます。
結局2で必要なのは、視点から見てその面の表が見えているか裏が見えているのかを調べ、その表裏が変わるエッジ部分を黒で表示するという処理です。ここで、この視点から見た面の表裏判定ですが、まともな表示ドライバならこの処理はすでにやっているはずです。裏面は絶対見えませんので表示する必要はないため、面の表裏を判定して、無駄な面ははじめから処理しないようにしているはず。で、この面の表裏判定はご存知のように結構簡単で、面の法線ベクトルと視線ベクトルの内積を求めればOKです。内積が正なら裏面、負なら表です。
ということで、このエッジ処理は実は非常に簡単。辺を共有している隣のポリゴンが裏面クリップに引っかかったら、その共有している辺を強調するだけです。
すでに上で書きましたが、アニメで一般的なべた塗りは実はアニメ調に必須なものではありません。アニメ調にはエッジが強調されていることが重要で、この塗りつぶし(シェーディング)はなんでもよいと思われます。最近の多色塗りされた2Dゲームを見ればわかりますが、綺麗にグーローシェーディングされているものも多いです(妙に色変化が強調されているが・・)。
#法線ベクトルと光源ベクトルを基盤とするライティング計算ではなく、想像でグーロー処理しているのというのが、なんとももの凄い職人芸です(^^;絵描きの皆さん。
てなことで、普通にグーローシェーディングしても良いのでしょうが、どう見てもアニメ調とするためには、あの特有のシェーディング手法を実現する必要があります。
実際のアニメ絵を見ると、結局、諧調が非常に限られたグーローシェーディングであるということがわかりますが、問題はこれをどうやってプログラムするかです。
グーローシェーディングの方は、現在はほとんどハードで高速に処理されているので、特定諧調に制限したグーローシェーディング機能を単にハードウェアの方で持っていてくれればそれがベストです。ですが、実はそんな特殊なハードウェアはそもそも要りません。結局、単色で塗るわけですので、ハードウェア的には単色塗りとエッジのラインが表示できれば良いのです。ということはハードウェア的にはGDIでもOKなのです。
問題になるのは、アニメ塗りに発生する色が変わる線(なんか名前があった気がするが忘れた(^^;)をどうやって求めるかです。エッジと違い、こちらはポリゴンのエッジと一対一対応するわけではありません。
で、これに関しては、ぜんぜん別の分野のテクニックが使えます。それが、可視化手法で良く利用される等高線を表示するアルゴリズムです。
具体的には、もし頂点abcからなる3角形ポリゴンがあって、各頂点の明るさが0.0,1.0,1.0であった場合、特定の明るさの閾値が0.5なら、その境界線は、辺ab間と辺ac間を通ります。実際にどこを通るかは、各点の明るさを再計算してみないとわかりませんが、明るさの変化を線形に仮定すると、明るさ0.5となる等高線は、辺abの真中と辺ac間の真中を通りそうです(下図)。
この様に、ある点の値がわかっている場合、等高線は、その線上に、ある値が存在しているかどうかを調べ、閾値があると判明した時点で閾値をとりそうな予想点を頂点間上に求め、その間をつなぐことで表示することができます。
これで色の変化する線が求まりますので、あとは塗りつぶすだけです。上の処理がきちんと行われていれば、等高線は必ず閉じてますので、一気に塗りつぶしても良いのでしょうが、AnimeBodyでは元のポリゴン単位で塗りつぶし、色の境界線を含む場合、ポリゴンをサブポリゴンに分割することで行っています。実際には、元のポリゴンは、エッジと代表点位置を求めるためだけに必要で、内部ポリゴンに塗りの方もこれに従う必要は本来はありません。バージョン1.0ではこのあたり一番簡単な処理で済ましてますが、より高度な処理をすることで、高速化やより綺麗な表示が行えるはずです。スプラインの制御点とみなして、エッジをスプラインで滑らかに処理するとかの応用も考えられるでしょう。
エッジが判明したからといって、必ず表示すればよいというわけではありません。エッジといえども、他のポリゴンの裏に隠れれば表示されませんので、やはり隠面処理する必要があります。ここでやっかいなのが、何にも考えずにエッジを書くとエッジがかすれやすいということです。下の、AnimeBodyにおける全ポリゴンエッジを表示させたところを見ればわかりますが、エッジになる辺近辺はちょうど視点から見て多くのポリゴンが重なる部分であり、せっかく求めたエッジのラインが手前のポリゴンで消されてしまいがちです。
今のところAnimeBodyでは、単にエッジラインを太さ表示するという手でごまかしてます。よりエッジを綺麗にするには、面の法線ベクトル情報を元に、必ずエッジがかすれないように実際のエッジより少し法線側にエッジを書くなどの工夫が必要になると思います。なお、AnimeBodyではGDIで処理していることからすぐわかると思いますが、隠面処理はZソートで処理してます。これに関しては、Zバッファ処理でも特に問題ないでしょう。
以上、これだけの情報があれば、AnimeBodyのような表示は直ぐに実現できるはずです。より良い方法など思いつきましたら、こちらまでメールお願いします。
99/8/13追加
Ver2.1以降で利用している、剛体向け物理演算手法の解説を追加しておきます。
通常、物理演算というと、NekoFlightのプログラマ向けページで書いたように、
#define DT 0.1
{
double x = 0.0, y = 10.0, vx = 1.0, vy = 0.0;
for (;;) {
x += vx * DT;
y += vy * DT;
vy += -0.98 * DT;
if (y < 0.0) {
y = 0.0;
vy *= -0.5;
}
// 表示
}
}
と、こんな風に速度を数値積分して位置を求めます。上の例だと、跳ね返りありの放物運動となります。
もし、より複雑に、ばね運動とかの力学処理が入った場合、
#define DT 0.1
{
double x = 0.0, v = 1.0, a = 0.0, f, m = 1.0;
for (;;) {
x += v * DT;
v += a * DT;
f = x * -0.2 + v * -0.1;
a = f / m;
// 表示
}
}
のように、各力点にかかる力を求め、それを質量で割って加速度を求め(a = f / m)、加速度を数値積分して速度を求め、速度を数値積分して位置を求めます(上の例は減衰ありのバネ振動)。
さてさて、これで問題になるのは、AnimeBodyのような複雑な関節をもつ剛体の場合です。
旧AnimeBodyの場合、各制御点間の骨に当たる部分を、一定の距離で働く力が0となる強力なバネと考えて処理してますので、各力点に働く力を求めて二階数値積分することになりますが、ただでさえ関節が伸びやすいのに、そんなことしていたら関節が外れてしまいます(ActiveDrawがこれで処理しているので骸骨のサンプルを見てみましょう)。
そもそも、関節が外れないようにするためには、バネを超強力、できればバネ係数∞にしたいですが、従来の方法ですとそれは計算できません。
ということで編み出したのが、ここで紹介する手法です。
基本的な考え方ですが、
位置をP,速度をV,加速度をAとしたときに、dP/dt=V, dV/dt=Aなため、積分してP=∫Vdt,V=∫Adt,と考えるというのが従来の手法ですが、数値計算ならばこれらのオペレータが混在していても計算可能。すなわち、P=∫Vdt, dP/dt=V とも書けます。解析的には意味ないですが、数値計算的には意味があります。制約計算ばかりで、位置の変更は頻繁に行う、しかし、加速度が良く分からないという条件下でも、速度成分を考慮することができるということです。
具体的な計算手順は
となります。
サンプルをあげると、
#define DT 0.1
{
double px1 = 0.0, py1 = 10.0, px2 = 1.0, py2 = 10.01;
double ox1 = 0.0, oy1 = 10.0, ox2 = 1.0, oy2 = 10.01;
double vx1 = 0.0, vy1 = 0.0, vx2 = 0.0, vy2 = 0.0;
for (;;) {
// 速度を位置に加える
px1 += vx1;
py1 += vy1;
px2 += vx2;
py2 += vy2;
// 加速処理
py1 += -0.98 * DT;
py2 += -0.98 * DT;
// 制約処理
// p1とp2間の距離は常に1
double dx = px2 - px1;
double dy = py2 - py1;
double dis = sqrt(dx * dx + dy * dy);
double d = (1 - dis) * 0.5;
px1 -= dx / dis * d;
py1 -= dy / dis * d;
px2 += dx / dis * d;
py2 += dy / dis * d;
// p1とp2は常にy>=0
if (py1 < 0) py1 = -vy1 * 0.5;
if (py2 < 0) py2 = -vy2 * 0.5;
// 速度成分を求める
vx1 = px1 - ox1;
vy1 = py1 - oy1;
vx2 = px2 - ox2;
vy2 = py2 - oy2;
// 位置情報を保存
ox1 = px1;
oy1 = py1;
ox2 = px2;
oy2 = py2;
// 表示
}
}
これで、長さ1の棒が地面の上で跳ね返るところをシミュレーションすることができます。回転などもきちんと処理されます。どこにも角速度など出てきませんが、これでOKです。剛体というのは、結局制約を持った質点群ですので、これでよいのです。ちょっと改造してやればサイコロのシミュレータなんかはすぐできます(HPにある車のプログラムなど参照)。
問題は常に距離を1に保つ部分ですが、強制的に動かしてしまいます(^^;
リンクが一つだけなら上の処理でOKですが、もしリンクが複数ある場合、何回かこの部分を繰り返してやります。
またこの手法の場合、位置の変更=加速発生ですので、強制的に下などに動かしてやると加速となります。
凄い複雑な制約をもっていても、これで非常に簡単、かつ正確に動きが計算できます。
AnimeBody2.1以降では、関節、スカート、髪の毛、全てこの手法で処理しています。
フライトシミュレータなんかには向きませんが、車のシミュレータなど、地面の上にいる系のシミュレータならこれで全部OK。布だろうが多関節だろうが処理できます。おまけでIKもできてしまいます。
オイラー角って制御が難しい、クォータニオンよく分からん、回転行列めんどくさい、力学演算どのぐらいの力が発生するか分からん、という状態でもかまいません。
物理演算(っぽい処理)で困っている方は一度試してみてはどうでしょう?
99/8/20追加
なんか上のプログラム、挙動が怪しい気がする・・・
ということで、実際に動くプログラムにしてみましたので、こちらも参考。
ボールにワイヤーがついた場合です。