3Dテキストを個別の文字ごとに落下させる動きを実装したいと考えました
しかし、各文字を独立したメッシュとして扱うと、GLTFファイルの読み込み時にメッシュの数が増え、パフォーマンスが悪くなります
また、リジッドボディはメッシュ単位で動作するため、文字を統合すると正しく機能しません
そこで、文字ごとの動きを維持しながら、全体を1つのメッシュとしてまとめる方法を模索しました
その解決策として、アーマチュアを追加し、各文字(頂点グループ)に対応するボーンを設定しました
準備
Align Toolsの有効化(後で利用)
選択したオブジェクトやメッシュの要素(頂点、エッジ、面)を指定した軸や面に整列させるためのツール
編集メニュー > Preferences(ユーザー設定) を開き「3D View: Align Tools」アドオンをチェックして有効化
基本的なシーンの設定とテキストの分割
- 床(平面)とテキストの追加
Shift + A から平面を追加→サイズを10程度で(s10)
Shift + A から Text を追加 - テキストの編集
編集モードにして文字を変更
オブジェクトモードに戻り、テキストプロパティ(オブジェクトデータプロパティ)で配置・押し出し・ベベルなどを調整
テキストの文字の下端が床(平面)に触れるようにZ位置を移動 - テキストのメッシュ化と分割
テキストを選択して オブジェクト > 変換 > メッシュ に変換
編集モード→Aキー(すべての頂点を選択)
M→距離で(隣接する頂点を統合)
P→構造的に分離したパーツで(文字が独立したオブジェクトとして分離) - 文字の原点を設定
オブジェクトモードで、すべての文字オブジェクトを選択
メニューから オブジェクト > 原点を設定 > 原点を重心に移動(ボリューム)
各文字の中心に原点が移動(リジッドボディの結果に影響します)
重要ポイント
1:アーマチュアの追加と親子関係の設定する
2:アーマチュアをスケールする場合は適用後に細分化
- 親がない場合、キーフレームにはグローバル座標系の値が記録されます
一方、親がある場合、キーフレームには親のローカル座標系での値が記録されます
そのため、先にアーマチュアと親子関係を設定することで、リジッドボディの動きがアーマチュアのローカル座標系に基づいて記録され、動きが崩れるのを防ぐことができます - ボーンがスケールされると、それに紐づけられた頂点グループも同様にスケールされます
そのため、アーマチュアを最初にスケールしてCtrl + Aで適用した後に分割することで、頂点グループへの影響を回避
- アーマチュアを追加
Shift + A からアーマチュア
*オブジェクトデータプロパティのビューポートの表示からボーンの名前を表示しておく
rx-90(アーマチュアの座標合わせ)
s3(任意でアーマチュアを3ユニット程度スケール)
オブジェクトモードでアーマチュアを選択し、Ctrl + A → 「全トランスフォームを適用」を実行 - 文字数分のボーンを作成
編集モードから右クリック→細分化
Alt + P→親子関係のクリア(ボーン間の親子関係を解除して文字の動きを個別に制御) - テキストとアーマチュアの親子関係
Shift を押しながらテキストオブジェクトを選択して最後にアーマチュアを選択
Ctrl + P → 「空のグループで」
*この時点ではウェイトは設定しません(最後に手動で割り当てるため)
*ここでの親子関係はあくまでもリジッドボディの動きやアニメーションがアーマチュアのローカル座標に基づいて記録するためです
備考
ボーンの分割数がデフォルトで最大10なので、それ以上必要な場合
Pythonスクリプトを使用すると簡単
import bpy
armature = bpy.context.object.data
bpy.ops.object.mode_set(mode='EDIT')
# 分割したいボーンを取得
target_bone = armature.edit_bones.get("Bone") # "Bone" を対象のボーン名に変更
# 分割数
segments = 20
length = target_bone.length / segments
# ボーンを削除し、分割
head = target_bone.head.copy()
tail = target_bone.tail.copy()
armature.edit_bones.remove(target_bone)
for i in range(segments):
new_bone = armature.edit_bones.new(f"Bone.{i+1}")
new_bone.head = head + (tail - head) * (i / segments)
new_bone.tail = head + (tail - head) * ((i + 1) / segments)
リジッドボディ物理演算を使って落下アニメーションを作成
(参考)リジッドボディ物理演算について
リジッドボディを適用すると、オブジェクトが重力、衝突、摩擦などの影響を受けて自然な動きをシミュレーションできます。
例えば、積み重ねたオブジェクトが崩れる様子や、坂道を転がる動きなど、リアルなシミュレーションを簡単に作成できます
リジッドボディの基本設定
- タイプ
- アクティブ: 動的に動くオブジェクトの設定。
- パッシブ: 静止したオブジェ
- 表面の反応(オブジェクト表面の物理特性を調整)
- 摩擦
低い摩擦(0.1~0.3):氷のように滑りやすい表面
高い摩擦(0.7~1.0):ゴムやカーペットのような滑りにくい表面 - 弾性
低い弾性(0.1~0.3):金属やレンガのように、衝突時にほとんど跳ね返らない表面
高い弾性(0.7~1.0):ゴムボールやバネのように、衝突時に大きく跳ね返る表面
- 摩擦
- コリジョン(オブジェクトの衝突判定に使用する形状を設定します)
- シェイプ
*シンプルな形状(ボックス、球): 計算が軽く、パフォーマンスが向上
*凸型ホール(Convex Hull): 中間的な選択肢で、複雑な形状に対応可能
*メッシュ: 最も正確ですが計算が重いため、静的なオブジェクト(壁や床)に推奨 - ソース(メッシュを選択した場合にのみ適用される設定)
*ベース:元の形状のみで計算、パフォーマンス重視
*変形:変形を反映した形状で計算、動的オブジェクトに適している
*最終結果:最終的な見た目通りの形状で計算、精度は高いが計算が重い
- シェイプ
- 非アクティブ化(オブジェクトのアクティブ化を遅らせる方法)
アクティブなオブジェクトに「非アクティブ化」と「開始時に非アクティブ化」の両方にチェックを入れると、そのオブジェクトはシミュレーションの開始時には非アクティブ状態になりますが、別のオブジェクトが衝突した時点でアクティブ化される
リジッドボディとアニメーションの切り替え
*シミュレーション中でオブジェクトの動きをキーフレームによるアニメーションで制御するか、物理演算で制御するかを切り替えることができる
設定の「アニメ」のチェックをオンにすると
シミュレーションの中でオブジェクトの動きをキーフレームで設定できるようになります
*アニメーションする間、そのオブジェクトは重力や衝突の影響を受けず、設定したキーフレームの動きに従います
アニメーションの後に設定の「アニメ」のチェックをオフにすると、その時点でリジッドボディの物理シミュレーションに戻ります
物理シミュレーションのキーフレーム化
リジッドボディをキーフレームにベイクする(キーフレームアニメーションとして再生)
- 結果をキーフレームに変換したいオブジェクトを選択(または複数選択)
- 上部メニューの「オブジェクト」→「リジッドボディ」→「キーフレームにベイク」を選択
- ベイクの範囲(開始フレームと終了フレーム)を設定するウィンドウが表示されるので、ベイクしたいフレーム範囲を指定して「OK」を押す
ベイク後は物理シミュレーションが無効化され、アニメーションとして再生可能
補足(リジッドボディコンストレイント・リジッドボディワールド)
- 特殊な動きの制御(特定の動きに制約を加えられます)
物理プロパティの「リジッドボディコンストレイント」
ドアのヒンジや機械のアームのような動きを実現 - シミュレーション全体の調整
シーンプロパティの「リジッドボディワールド」
重力やシミュレーションの精度(サブステップと反復数)を調整
転がった文字を自然に元の位置に戻す
リジッドボディを使った物理演算では、文字は床の上にランダムに転がります
そこで、Emptyオブジェクトを活用して元の位置に自然に戻る動きを実装します
*Emptyを親にしてその座標を利用する
平面(床)のリジッドボディ設定
- 平面(床)のリジッドボディ設定
床(平面オブジェクト)を選択
リジッドボディの追加:
「物理演算プロパティ」でリジッドボディを追加
タイプを「パッシブ」に設定
文字のリジッドボディの設定
- 文字の複製と移動
Shift + Dで文字を複製(後で元の位置の座標が必要なので)
gz8程度(約8ユニット程度上に移動)
rx0.2程度(約0.2程度傾ける) - 移動した文字のリジッドボディを設定
「物理演算プロパティ」でリジッドボディを追加
タイプを「アクティブ」に設定
衝突形状を「ボックス」に設定 - タイムラインの終了フレームを約80に設定
シミュレーションを再生し、文字が転がる動きを確認
*転がった文字が表を向くように調整 - リジッドボディをキーフレームにベイク
上部メニューの「オブジェクト」→「リジッドボディ」→「キーフレームにベイク」を選択
ベイクの範囲(開始フレームと終了フレーム)を設定するウィンドウが表示されるので「OK」を押す
注意点
リジッドボディの初期位置(例えば gz8 で8ユニット分上げた高さ)が、最終的にアーマチュア(ボーン)に含まれます
GLTFエクスポート時には、ボーンの可動域がモデル全体の基準位置として記録されるため、Three.jsではその高さがモデル全体の高さとして解釈されます
その結果、以下のような問題が発生する可能性があります
カメラがモデル全体の高さよりも低い位置にあると、モデルが視錐台の外に出て表示されません
カメラの位置はモデル全体の高さを大きく超える必要があり、そうするとモデルが小さくみえます
これを防ぐためには、エクスポート前にBlenderでスケール調整をして
モデル全体を大きくして「Ctrl + A」でスケールを適用します
Emptyの追加と設定
- Shift + A でEmpty(矢印)を追加
- Emptyを文字の最終位置に移動(Align Toolsを使うと便利)
タイムラインのフレームを文字が床に接触したフレーム以降に移動
Empty・転がった文字の順番に選択
Align Toolsの「XYZボタン」をクリックして位置を合わせる - Emptyを親に設定
転がった文字・Emptyの順番に選択、Ctrl + P → 「オブジェクト」 - Emptyを元の位置に移動
Empty・元の位置の文字の順番に選択
Align Toolsの「XYZボタン」をクリックして位置をあわせる - Emptyがアーマチュアの階層からはずれるので戻す
Emptyを選択
アーマチュアをShiftキーで追加選択
Ctrl + P を押し、「オブジェクト」を選択
これでEmptyが再びアーマチュアの子として階層に含まれるようにする - スペースキーを押してタイムラインを再生し、文字が自然に元の位置に戻ることを確認
- 元の位置の文字は不要なので削除
すべての文字に「文字のリジッドボディの設定」と「Emptyの追加と設定」をする
Emptyがアーマチュアの子になるように階層を戻すことが重要
頂点グループを利用して文字をボーンで制御する
リジッドボディはメッシュ単位で動作するため、文字を統合するとうまく動作しなくなります
そこで各文字(頂点グループ)をボーンで動かせるようにします
文字ごとに対応するボーンを配置する
- タイムラインのフレームを、文字が床に接触したフレーム以降に移動して作業
- オブジェクトモードで文字を選択
Shift + S を押して、「カーソル→選択物」(文字の原点にカーソルを移動) - アーマチュアを選択し、編集モードに切り替え
移動させたいボーンを選択
Shift + S を押して、「選択物→カーソル」(ボーンをカーソルに移動) - すべてのボーンの配置が完了したら、はShift + Sを押して「カーソル→ワールド原点」で戻しておく
各ボーンにコンストレインを設定して文字と動きをリンクさせる
*文字ごとに対応するボーンを設定し、文字と同じ動きをさせるために、ボーンに「トランスフォームコピー」のコンストレインを設定します
- ポーズモードに切り替えアーマチュアを選択し、ポーズモードに切り替る
- 対応するボーンを選択
画面右側の「ボーンコンストレインプロパティ」タブを開き
「追加」メニューから「トランスフォームコピー」を選択
「ターゲット」に対応する文字オブジェクトを選択 - タイムラインを再生して、文字の動きにボーンが追従していることを確認
ボーンの動きをキーフレームにベイクする
ボーンに設定されたコンストレインの動きをキーフレームにベイクし、独立したアニメーションとして記録
- アーマチュアを選択しポーズモードに切り替える
メニューから ポーズ > アニメーション > アクションをベイク を選択 - 設定ウィンドウで以下のオプションをチェック
ビジュアルキーイング(コンストレインによる見た目の動きをキーフレームとして記録)
コンストレインクリア(ベイク後にコンストレインを自動で解除)
設定が完了したら「OK」をクリック
リジッドボディのベイクを削除し、各文字(頂点グループ)を対応するボーンで制御するように設定する
- リジッドボディのベイクを削除
タイムラインでA(全選択)→ X →キーフレームを削除 - 頂点グループでボーンの紐付け
編集モードに切り替え
Aで全選択
「オブジェクトデータプロパティ」タブ「頂点グループ」から該当グループを選択
ウエイト1を確認して「割り当て」をクリック - タイムラインを再生してリジッドボディのベイクが削除されていてもボーンの動作に追従してることを確認する
テキストを統合して1つのメッシュにする
- 統合したい文字メッシュをすべて選択
- Ctrl + Jで結合
- 結合により子オブジェクトを持たなくなったEmptyは削除
備考
GLTFの仕様でSkinned Meshはルートノードに配置する
Skinned Meshはアーマチュア(スケルトン)によって制御されるため、通常の親ノードとは違い、親の位置や回転、スケールの影響を受けません
そのため、Skinned Meshはルートノードに配置する
エクスポート前に必要な手順
- Skinned Meshとアーマチュアの親子関係を解除する
「Alt + P」を押して「トランスフォームを維持してクリア」を選択
*親子関係を解除しても、アーマチュアモディファイアが正しく設定されていれば、Skinned Meshはボーンに従って動作する - トランスフォームを適用する
親子関係を解除した後、「Ctrl + A」を押して「全トランスフォームを適用」を実行
*位置、回転、スケールが初期化され、エクスポート時に正確なトランスフォーム情報が記録される
「親子関係をクリア」するときの3つのオプションの違い
- 親子関係をクリア
結果の見た目:大きく変わる
子オブジェクトのトランスフォームの状態:ローカル座標に戻る
親の影響を完全に排除したい場合 - トランスフォームを維持してクリア:
結果の見た目:変わらない
子オブジェクトのトランスフォームの状態:ワールド空間で固定
現在の見た目を維持しつつ親子関係を解除したい場合 - 親との逆行列をクリア
結果の見た目:変わらない
子オブジェクトのトランスフォームの状態:ローカル座標系を再計算し、新しいローカル空間に変わる
見た目を維持しつつ新しいローカル空間を作りたい場合