Blender:シンプル変形モディファイアでカード開閉アニメーション

カードの「開くアニメーション」と「閉じるアニメーション」をそれぞれ独立したアニメーションとして作成し、GLTF形式でエクスポートします
この形式を使用することでThree.jsで、必要なアニメーションを指定して個別に再生できるようにします

目次
  1. 画像の準備
  2. モデルの準備
  3. UV展開とマテリアル設定
  4. 曲げの準備
  5. シンプル変形モディファイアで折りたたみ
  6. アニメーションの作成
  7. カードの開閉アニメーションで直面した問題

画像の準備

カードの表面と裏面が一枚にまとめられた画像を用意
*UV展開で使用します

余談
画像の作成には Canva を使用しました
無料枠でもカスタムサイズを指定できるので、カードの表面と裏面のデザインを一つにまとめた画像を簡単に作成でき、豊富なデザインテンプレートが用意されています

モデルの準備

表面(Front): カードを開いたときに文字やデザインが見える正面の面
例:名前やメッセージを書く側
裏面(Back): 表面の裏側にあたり、通常は模様のみが表示される面

  • 平面を2つ追加
    Shift + Aで「メッシュ」→「平面」を追加。
    平面を複製する(Shift + D)ことで、2つの平面を準備します
  • スケールの調整
    それぞれの平面をカードの縦横比に合わせてスケールします
    例:sy1.5 X方向 = 1、Y方向 = 1.5(横長カード)
  • 平面の回転
    *1つの平面は非表示にして作業を進めやすい
    表面(Front)を rx 90 (X軸に90度回転)します
    裏面(Back)を rx -90 (X軸に-90度回転)します

正面ビュー(テンキー1)で見たとき

UV展開とマテリアル設定

  • 画面を分割して、UVエディタを開きます
  • UVエディタ内で画像を開くボタンをクリック
    用意したカードの表面と裏面がまとめられた画像ファイルを選択して読み込みます

表面・裏面それぞれのUV展開を行う

  • 3DビューでUV展開したい平面を選択
    編集モードに切り替え、平面全体を選択(Aキー)
  • UVエディタ内に平面のUVマップが表示されます
    *通常は全体が画像全域に対応しているので
    Sキーで(この場合はX軸に対して)スケールを調整
    Gキーで(この場合はX軸)を移動して正確に合わせます

オブジェクトモードに戻り、平面にマテリアルを設定

シェーディングを「マテリアルプレビュー」に切り替えます

  • プロパティエディタの「マテリアル」タブをクリック
    「新規」を押して新しいマテリアルを作成します
  • マテリアル設定内の「ベースカラー」の右側にある丸いアイコン(点の部分)をクリック
  • 表示されたメニューから「画像テクスチャ」を選択
    テクスチャとして準備した画像を指定
  • 「裏面非表示」をチェックします

曲げの準備

サブディビジョンサーフェイスモディファイアを適用
*平面が中央を軸として自然に曲がるように中央に頂点が必要
ループカットでもOK

  • 平面を選択し、プロパティエディタの「モディファイア」タブを開き
    「モディファイアを追加」から「サブディビジョンサーフェイス」を選択
  • タイプを「シンプル」に設定。
    分割数を「1」に設定します
  • モディファイアを適応します

トランスフォームを適用して平面を統合

  • 平面を選択した状態で、Ctrl + A を押して「全トランスフォーム」を適用します
  • 2つ平面を選択(Shiftキーを押しながらクリック)
    Ctrl + J で統合します

シンプル変形モディファイアで折りたたみ

備考
シンプル変形モディファイアとは
シンプル変形モディファイアはオブジェクトを手軽に変形させるためのモディファイアです
以下の4つの変形タイプを利用して、オブジェクトの形状を柔軟に変更できます

4つの変形タイプ

  • 曲げ(Bend)
    オブジェクトを指定した軸に沿って円弧状に変形します
    例: 紙を曲げたりする動作を再現
  • ねじり(Twist)
    オブジェクトを軸に沿ってねじります
    例: リボンやボルトのような形状の再現
  • 拡大縮小(Taper)
    オブジェクトの一方を狭めたり広げたりします
    例: コーンのような形状を作成
  • ストレッチ(Stretch)
    オブジェクトを軸方向に伸縮させます
    例: 細長い形状を作る際に便利

統合した平面に「シンプル変形モディファイア」を追加します

変形タイプを「曲げ」に設定
曲げる軸を「X軸」に設定
値を調整(例: -355~-360)して、カードが自然に折りたたまれるようにします
*テンキー7で上から見たビュー

*縦方向(Y軸)が若干縮んだように見えので
あとでキーフレームを使ってスケール(例: Y軸方向)を調整し、自然な形状を保つようにします

シェイプキーの作成

  • 折りたたんだ状態のモディファイアを「シェイプキーとして保存」します
  • 保存したシェイプキーは「オブジェクトデータプロパティ」で確認できます
    オブジェクトを選択した状態でこのタブを開きます

アニメーションの作成

準備

  • タイムラインで、終了フレームを設定します
    終了フレームは、アニメーションの速度に応じて自由に設定可能
    タイムライン右上の「終了フレーム」を直接入力して変更します
    *今回は100にします
  • シンプル変形モディファイアの角度を「0」に戻します

閉じるアニメーションを作成

  • 正面ビュー(テンキー1)
    • 「1」のフレームでシェイプキーの値「0」のキーフレーム
    • 「80」のフレームでシェイプキーの値「1」のキーフレーム
  • 上から見たビュー(テンキー7)
    • 「1」のフレーム:RX-90(回転)して「iキー」→「回転・スケール」でキーフレーム作成
    • 「80」のフレーム:RX90(回転)・SY1.5(スケール)して「iキー」→「回転・スケール」でキーフレーム作成

NLAエディタ(ノンリニアアニメーション)を使って「閉じるアニメーション」をストリップ化し、エクスポート可能にする
*タイムラインから直接NLAエディタには移動できないため、ドープシートエディタを使用してアクションを管理します

ドープシートエディタを開く

  • ドープシートの上部メニューにある「アクション」を選択
    アクション名を「close」に変更
    「ストリップ化」をクリック
  • ドープシートの上部メニューにある「シェイプキーエディタ」を選択
    アクション名を「close」に変更
    *必ず同じ名前にする
    「ストリップ化」をクリック

NLAエディタを開く

  • NLAエディタで「close」という名前のストリップを確認
  • 一旦「close」をミュートに設定します
    右側プロパティのチェックを外す
    *次に作成する「開くアニメーション」を干渉なく作業するため

開くアニメーションを作成

  • 正面ビュー(テンキー1)
    • 「1」のフレームでシェイプキーの値「1」のキーフレーム
    • 「80」のフレームでシェイプキーの値「0」のキーフレーム
  • 上から見たビュー(テンキー7)
    • 「1」のフレーム:SY1.5(スケール)して「iキー」→「回転・スケール」でキーフレーム作成
    • 「80」のフレーム:RX-90(回転)して「iキー」→「回転・スケール」でキーフレーム作成

「閉じる」の時と同じ要領で、ドープシートエディタを使用して「開くアニメーション」をストリップ化します

  • ドープシートの上部メニューにある「アクション」を選択
    アクション名を「open」に変更「ストリップ化」をクリック
  • ドープシートの上部メニューにある「シェイプキーエディタ」を選択
    アクション名を「open」に変更「ストリップ化」をクリック

エクスポート時の注意点

  • ミュートは解除する(ミュート設定はエクスポート時にも反映されます)
  • エクスポートのオプション
    トランスフォームは「Yが上」を有効にする
  • エクスポートのオプション
    マテリアルをエクスポートする設定にする
    データ>マテリアル
    *マテリアルの「ベースカラー」に画像テクスチャが設定されていれば、GLTF形式でエクスポートする際に画像がそのまま含まれます
  • エクスポートのオプション
    「NLAトラックでグループ化」を有効にする
    アニメーション>アニメーション

エクスポート後のファイルが正しく動作するか確認できます

カードの開閉アニメーションで直面した問題

Blenderで作成したカードの開閉アニメーションをThree.jsで利用する際に直面した問題と、その解決方法
*問題:「open」と「close」を両方利用すると、後の方が180度回転ではなく90度回転になる

問題を解決するために、今回はアニメーションクリップを再生する前にリセットします
GSAPのタイムラインを使用して、アニメーションの順序やタイミングを管理

// 指定された名前のアニメーションクリップを再生
function playAnimationByName(mixer, clips, name) {
    const clip = THREE.AnimationClip.findByName(clips, name);
    if (clip) {
        const action = mixer.clipAction(clip);
        action.setLoop(THREE.LoopOnce); // 1回だけ再生する
        action.clampWhenFinished = true; // 終了時に最終フレームで停止
        action.play();
    } else {
        console.warn(`Animation clip "${name}" not found.`);
    }
}

// 指定された名前のアニメーションクリップをリセット
function resetAnimationByName(mixer, clips, name) {
    const clip = THREE.AnimationClip.findByName(clips, name);
    if (clip) {
        const action = mixer.clipAction(clip);
        action.stop();  // アニメーションを停止
        action.reset(); // アニメーションの時間を0に戻す
    } else {
        console.warn(`Animation clip "${name}" not found.`);
    }
}


// GSAPタイムラインを作成
const timeline = gsap.timeline();

// "open"アニメーションを再生
timeline.call(() => {
    resetAnimationByName(mixer, clips, "close");
    playAnimationByName(mixer, clips, "open");
}, null, "+=2"); // 2秒後に実行

// "close"アニメーションを再生
timeline.call(() => {
    resetAnimationByName(mixer, clips, "open");
    playAnimationByName(mixer, clips, "close");
}, null, "+=2"); // さらに2秒後に実行