Help us understand the problem. What is going on with this article?

3Dでしっぽがにょろにょろ

More than 5 years have passed since last update.

3Dでしっぽがにょろにょろするインタラクションを作りたいな、と思うことがある。そんなときどうするか。

しっぽの関節を定義

まずは関節から作り始める。このとき、関節の親子関係は考えないほうが、あとの計算が楽になる。

class Joint {
public:
    float3d position;   // 関節の位置
    Quaternion rotation;    // 関節の角度
    float interval; // 前の関節との距離
};

ここでは物理的な正確さは本題ではないので、速度ベクトルや角速度は、持たなくてもわりと問題ない。

関節のリストを作る

続いて、関節の配列をつくって初期化する。
今回は、しっぽの付け根がjoints[0]、末端がjoints.back()と対応付けられるようにした。

const int JOINTS_NUM = 21;  // 関節の数
const float INTERVAL = 0.18f;   // 関節同士の間隔

// 配列を確保
std::vector<Joint> joints;
joints.resize(JOINTS_NUM);

// 各関節の値を設定
for (int i = 0; i < JOINTS_NUM; ++i) {
    joints[i].interval = INTERVAL;
    joints[i].position = float3d(0.0f, 0.0f, INTERVAL * i); // しっぽはZ軸正の方向へ伸びる
}

しっぽの付け根を動かす

しっぽ全体を動かす前に、付け根だけを動かしてみる。
sin関数でY軸に対する回転量を変化させて、しっぽの付け根が周期的に揺れるようにする。

const float ROT_WIDTH = 0.4f;   // 回転の変化の幅(ラジアン)
float theta = 0.0f; // 今の位相を表すグローバル変数

void update(float delta_time) {
    theta += M_PI * 2.0f * delta_time;
    while (theta > M_PI) theta -= M_PI * 2.0f;

    // 軸と回転の大きさから、3次元的な角度を求める
    joints[0].rotation = Quaternion::fromAngleAxis(std::sin(theta) * ROT_WIDTH, float3d(0, 1, 0));
}

他の関節を動かす。

付け根の動きができたら、これをほかの関節にも伝播していくようにしよう。

void updateSingleJoint(float delta_time, Joint& joint, const Joint& prev_joint) {
    // 回転を前の関節に近づける
    Quaternion rotation = Quaternion::slerp(joint.rotation, prev_joint.rotation, delta_time * 30.0f);   // 2つの3次元角度の中間を求める
    rotation.normalize();
    joint.rotation = rotation;

    // 位置を前の関節から推定される位置に近づける
    float3d target = prev_joint.position + joint.rotation.getRotated(float3d(0, 0, joint.interval));
    joint.position += (target - joint.position) * 50.0f * delta_time;
}

ここで30.0fと50.0fという謎定数が出てきたが、これは角度や位置の伝播しやすさを決める係数で、今適当に決めた。
delta_timeが1.0f/50.0fを超えると、位置の補間が予期せぬ値になってしまう可能性があるので、あらかじめうまく丸めてやる必要がある。

void updateJoints(float delta_time) {
    // delta_timeが大きいときは、1/50秒未満の内部フレームに分割する
    float internal_frames_num = std::ceil(delta_time * 50.0f);
    float internal_dt = delta_time / internal_frames_num;

    for (float j = 0.0f; j < internal_frames_num; ++j) {
        for (int i = 1; i < JOINTS_NUM; ++i) {
            updateSingleJoint(internal_dt, joints[i], joints[i-1]);
        }
    }
}

これでしっぽの動きは完成した。
こうしてできた関節に、三角形メッシュをうまいこと結び付けてやれば、それっぽいアニメーションを得ることができるだろう。

本体としっぽを同時に動かす

実際にしっぽをにょろにょろさせる際には、本体としっぽとを同時に描画することになる。
このとき、本体の位置と回転をそのまま利用してしっぽを描画するとよくない。せっかく慣性が残るようにしたしっぽの挙動に、本体の回転が足されてよくわからなくなってしまうからだ。
これを防ぐには、しっぽを描画する際に本体の回転を考慮しないようにする。

OpenGLで書くと、以下のようになる。

void draw() {
    glPushMatrix();
        glTranslatef(本体の位置);
        glPushMatrix();
            glRotatef(本体の回転);
            // 本体の描画
        glPopMatrix();
        glTranslatef(本体からしっぽの付け根へのベクトル);
        // しっぽの描画
    glPopMatrix();
}

本体の回転は、付け根の回転としてしっぽに与えてやればいい。
速度を考えなくても、回転だけ考えていれば意外とそれっぽくなるというのが、今回のポイントだ。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした