[ITG] Lua講座(第5回) コンボに応じて腕を2重振り子状に動かす -火花散らして-

f:id:kai_yuki:20201219194738p:plain

動画

ダウンロード


Lua

Lua(OpenITG向け)

Lua(SM5向け)

概要

第4回の記事で、無限ループを用いたインタラクティブな処理を解説しました。
lua記事の最終回となる第5回では、第4回の記事の応用編としてStepMania上で運動方程式を解くことにします。

発想

良く知られたフレンシーフォの絵ですが、力を抜いたら自然に下方向にだらんとぶら下がるのではないかと考えました。
f:id:kai_yuki:20201219195238p:plain
その際に、慣性の力で少し逆方向にオーバーランしそうだなと思いました。
f:id:kai_yuki:20201219195805p:plain
そうすると、フレンシーフォの肘から先は肘を固定点とした振り子状の動きをすると考えた方が自然ではないかと考えました。
更に考えると、肘までが固定でその先が振り子状になっているというのも変で、肩から肘までも同様に振り子状になっていると考えた方がより自然ではないかということに気づきました。
そこで、フレンシーフォの右腕を2重振り子であるとモデル化し、どのような動きをするかをStepManiaを用いて確かめたいと思いました。
f:id:kai_yuki:20201219194738p:plain

方程式の導出と離散化

wikipediaにちょうどいい方程式と図があったのでそのまま使います。
二重振り子 - Wikipedia

f:id:kai_yuki:20201219202106p:plain

1行目をθ1の2回微分、2行目をθ2の2回微分に関してまとめ直したのが以下の式です。

powerA = impactA - (0.5 * powerB * math.cos(math.rad(degreeA - degreeB)) + 0.5 * 2 * verocityB * math.sin(math.rad(degreeA - degreeB)) + gravity * math.sin(math.rad(degreeA)) + resist * verocityA);
powerB = - (powerA * math.cos(math.rad(degreeA - degreeB)) - 2 * verocityA * math.sin(math.rad(degreeA - degreeB)) + gravity * math.sin(math.rad(degreeB)) + resist * verocityB);

ただし、

θ1(角度):degreeA
θ1の微分(角速度):verocityA
θ1の2回微分(角運動量):powerA

としています。θ2は同様にdegreeBとします。
また、簡単のためにl1=l2=1、m1=m2=1とします。
impactAは外力の項で、ここがコンボに依存する部分となります。
resist*verocityAは速度に依存する抵抗力(空気抵抗のイメージ)です。

powerBの方程式で2 * verocityAとなっている箇所がありますがここは本来はverocityA* verocityAです。が、正規化をサボったせいでどうしてもうまく動かなかったので2 * verocityAで近似しています。

また、微分の代わりに前進差分を取ることにして以下のように計算することにします。

degreeA(t+Δt)=degreeA(t)+Δt*verocityA(t)
verocityA(t+Δt)=verocityA(t)+Δt*powerA(t)

数値計算的にもっと精度のよい方法はあるんですが(ルンゲクッタ法とか)、今回はそれらしい計算ができればよいので1次の項まででよしとします。

一定速度以上の場合は角運動量の逆方向に摩擦力(friction)が働くこととします。

以上をまとめたのが以下です。

		currentcombo = STATSMAN:GetCurStageStats():GetPlayerStageStats(0):GetCurrentCombo();
		if (currentcombo >= combo + 3) then
			impactA = 3000;
			impactB = 1000;
			combo = currentcombo;
		elseif (combo > currentcombo) then
			impactA = 0;
			impactB = 0;
			combo = currentcombo;
		else
			impactA = 0;
			impactB = 0;
		end

		--外力、重力、空気抵抗
		powerA = impactA - (0.5 * powerB * math.cos(math.rad(degreeA - degreeB)) + 0.5 * 2 * verocityB * math.sin(math.rad(degreeA - degreeB)) + gravity * math.sin(math.rad(degreeA)) + resist * verocityA);
		--摩擦力 速度が小さいときは摩擦を考えない
		if (verocityA > 1.0) then
			powerA = powerA - friction;
		elseif (-1.0 > verocityA) then
			powerA = powerA + friction;
		end

		degreeA = degreeA + 0.02 * verocityA;
		verocityA = verocityA + 0.02 * powerA;

これを第4回のlua講座で解説したupdatecommandを用いた無限ループと組み合わせます。

実は前回の記事では解説していなかったのですが、無限ループをどの頻度で回すかはself:sleep(秒)の行に依存します。
(正確には<<命令の記述>>の実行時間も含まれますが十分小さいとします)
そうすると、上記の前進差分のΔtとself:sleepの時間を合わせる
(今回は両方とも0.02)
ことにより、実経過時間に応じた数値計算結果をOpenITG上で表示することができます。

前回の記事でインタラクティブな処理を実現可能と書きましたが、運動方程式数値計算をOpenITG上で行うことにより、
実時間スケールの運動の計算と表示を簡単に行うことができる、というのが非常に強力だと思っています。

余談ですが、2重振り子の運動方程式はパラメータを適切に決めるとカオス的なふるまいをすることが知られています。
今回のモデルでは摩擦力や空気抵抗を考慮しているため保存系ではないこと、
外力をコンボに応じて与えていることから、カオス的なふるまいと関係なくランダムな動きになるのは当然なのですが、
それらしい動きを運動方程式数値計算だけから表現することができたので満足です。

終わりに

全5回でLuaを用いたbgの表現について解説してきました。
これらの記事をきっかけに、luaを使った表現をやってみようかなという作者が増えればいいなと思っています。

参考URL

Lua API for OpenITG / NotITG(英語)
https://sm.heysora.net/doc/
ITGのLuaを書くのにまず参照するページです。OpenITGで使用できる各種命令が記載されています。

Lua for StepMania 5
https://quietly-turning.github.io/Lua-For-SM5/
StepMania5版のlua解説サイト(英語)です。使用できる関数や3.9系との違いなど、SM5系のluaを作成するのに必要な情報が詰まっています。

paraphrohn’s diary
https://paraphrohn.hateblo.jp/
日本語でLuaの解説が書かれた貴重なページです。Luaの基本から譜面のワープ、ハイスピ変更ギミックなど様々な実装についての解説があります。