C++でソフトボディ(ゼリーやスライムみたいなプルプルの動き)を作成する
はじめに
初めまして。そこら辺の専門学生「mozu」と申します。
今回は簡単な物理演算で「ソフトボディ(軟体)」をC++で実装します。
こういった記事を書くのは初めてなので説明不足な点があるかもしれませんがご容赦ください。
ソフトボディの作成に必要なもの
質量をもつ頂点(質点)
頂点と頂点を繋げるバネ
今回は2Dでソフトボディの基本となるバネ計算のみで実装します。
それぞれの開発環境によって必要なライブラリや詳しい関数定義、アルゴリズムなど細かいところは割愛します。
ソフトボディの作成
まず、頂点を作成するためのMassPointクラスを宣言します。
class MassPoint
{
public:
// コンストラクタ
MassPoint(Vector2 initPosition);
private:
const float MASS = 0.18f; // 質量
const Vector2 GRAVITY = {0.0f, 0.1f}; // 重力
float deltaTime; // 経過時間
Vector2 position; // 位置
Vector2 gravityAcceleration; // 重力加速度
Vector2 springForce; // ばねの力
Vector2 totalForce; // 合計の力
Vector2 velocity; // 速度
}
質量と重力はゲームに合わせたものを設定してください。ここでは私の環境に合わせた値を設定しています。
今回は4つの頂点を使用してソフトボディの作成を行います。ソフトボディを作成するクラス(SoftBodyクラス)で頂点のインスタンスを生成します。
// 可変長配列に格納した方が楽ですが、省きます
// 回転やスケールの計算も割愛します
MassPoint point1({-1.0f, 1.0f }); // 左上
MassPoint point2({ 1.0f, 1.0f }); // 右上
MassPoint point3({-1.0f, -1.0f }); // 左下
MassPoint point4({ 1.0f, -1.0f }); // 右下
頂点を四角形の形になるように作成します。現在の状況はこんな感じです。
頂点が作成できたので、次はバネのSpringクラスを宣言します。
class Spring
{
public:
// コンストラクタ
Spring(MassPoint* point1, MassPoint* point2);
private:
const float RESISTANCE = 0.5f; // 抵抗率
const float ELASTICITY = 2.0f; // 弾性率
MassPoint* p1; // 繋げる頂点1
MassPoint* p2; // 繋げる頂点2
float distance; // 元の距離
float currentDistance; // 現在の距離
Vector2 fluctuation; // 力の増減
}
「RESISTANCE」と「ELASTICITY」はバネの計算で使います。バネ計算の実装時に詳しく説明します。次に、SoftBodyクラスですべての頂点とバネが繋がるようにインスタンスを生成します。
// 実際はループと可変長配列を使ってください
Spring spring1(&point1, &point2);
Spring spring2(&point1, &point3);
Spring spring2(&point1, &point4);
このように繋げたい頂点のアドレスをSpringクラスに渡します。
頂点とバネの生成が終わったので、頂点で必要な計算とバネの計算について解説していきます。
頂点の計算
まず、ニュートンの運動の法則$${F = ma}$$より、重力加速度を計算します。今回は質量と重力の情報を頂点が所持しているので、頂点のインスタンス生成時に呼ばれるコンストラクタで計算を行います。
// 重力加速度
gravityAcceleration = MASS * GRAVITY;
次に、頂点の位置に加算する力(velocity)の計算を毎フレーム行います。
まず、頂点に加えられる力の合計を計算します。
これは単純にバネから与えられる力と重力加速度から計算します。
// 力の合計
totalForce = springForce + gravityAcceleration;
力の合計をもとに、velocityをニュートンの運動の法則より求めます。
$$
velocity = \frac{F*Δt}{m}
$$
velocity = totalForce * deltaTime / MASS;
最後にこのvelocityをpositionに加算すれば頂点での計算は終わりです。
springForceは毎フレーム0で初期化してください。
バネの計算
バネの力は
$${F=-kx}$$
を使用して求めます。
フックの法則では $${F=kx}$$ですが、
大きさと向きを含めたベクトルで弾性力を表したいので「-」が付きます。
$${x}$$はばねの自然長からの伸び(変位)を求めます。
$${x>0}$$ のとき、$${F}$$ は負の向き。$${x<0}$$ のとき、$${F}$$ は正の向きになります。それでは計算の手順を説明します。
計算の手順
まず、バネの計算で必要な「自然長からのバネの伸び」を求めるために、元の距離をバネのインスタンス生成時に計算します。
// 元の距離
distance = Distance(p1->GetPosition(), p2->GetPosition());
次に毎フレーム行うバネの計算です。
現在の距離を計算。
元の距離-現在の距離でバネの伸びを求める。
$${p2}$$の位置から$${p1}$$の位置への向きを計算。
方程式をもとに、p1への向きを$${-k}$$とし、$${x}$$はバネの伸び、そこに弾性率を掛けてバネの力を計算。
抵抗力は、前フレームのバネの力の逆ベクトルに抵抗率を掛けて計算。
バネの力と抵抗力を足したベクトルを$${p1}$$, それの逆ベクトルを$${p2}$$に渡します。
バネの力を前フレームのバネの力として記憶。
弾性率と抵抗率について
・弾性率(ELASTICITY)は抵抗率が「0.5」の時、値を増やすと「バネの跳ね返
り」が固くなり、0に近づけると柔らかくなります。
・抵抗率(RESISTANCE)は弾性率が「2.0」の時、値を増やすと「バネが元に
戻ろうとする力」が大きくなり、減らすと小さくなります。
※どちらも必ず「0より大きい値」を設定してください。弾性率と抵抗率に設定した値はあくまで自分の環境でうまく動作したものであるため、値によってはバネの計算が上手くいきません。調整と検証を繰り返してください。
最適化
バネの力をクランプ(範囲制限)することでバネの力を多少制御できます。また、自然長からのバネの伸びが0に近い場合、計算処理を行わないことで処理が軽くなります。
バネの計算で作成した関数
実際のバネの計算のコードは参考にした記事に基づいて実装しているため載せませんが、距離の計算などで必要な計算を載せておきます。
// 長さを計算
float Length(Vector2 vector)
{
// 平方根の計算は重いのでなるべく避けたいですが
// 今回の場合、計算結果が変わってしまうのが
// 気になったので使用しています(ほぼ誤差です)。
return std::sqrtf(vector.x * vector.x + vector.y * vector.y);
}
// 距離を計算
float Distance(Vector2 lhs, Vector2 rhs)
{
Vector2 distance = lhs - rhs;
return Length(distance);
}
// 正規化
void Normalize(Vector2* vector)
{
float sq = Length(vector);
// 0除算は行わない
if (sq == 0) return;
vector->x /= sq;
vector->y /= sq;
vector->z /= sq;
}
バネの計算は手順に沿って行えば問題ないですが、この記事の最後の方に実装で参考にしたもののURLを載せておきますので、バネの計算はそちらを参照してください。
ソフトボディ完成
すべての頂点の更新後、バネの計算を実行すればソフトボディの基本部分の実装は完了です。
実装した頂点にuv座標を持たせて、テクスチャを設定してあげれば、
このようなプルプルのスライムを物理演算のみで実装することができます。
おわり
長々と書きましたが、今回はバネのみを使用した基本のソフトボディの実装を紹介しました。より形が安定したソフトボディにするためには、元の形に戻るための補間、ガスの圧力による体積保存などの計算が必要ですが、とても複雑になるので紹介しませんでした。
また、ここで解説したものは、私が参考にしたものを自分なりに解釈したものであるため、中途半端な解説になっている部分があるかもしれません。計算手順も私が調べた中で理解でき、簡単だったものをそのまま紹介した「ただの一例」であることをご理解いただけますと幸いです。
最後に、ここまで読んでいただいた読者の皆様、ありがとうございます。
今後の皆様のゲーム開発に役立つことができれば幸いです。