VC++を使ってペンデュラムウェーブ(Pendulum Waves)の制作
2021/02/14


ずっと電子工作を怠けていたのである。

時々思い出したように若干やってはいたのだが、どうもノリが悪く、すぐにユーチューブなどを見て無駄に一日を過ごすことが多くなった。
やはり、先進的なシステムはうまく出来ていて、私の好みそうな画像を次々に送ってくるのである。
そんなのを見ていると、あっという間に一日が終わる。
もしかすると、ユーチューブというものはテレビより害毒が強いのかもしれないのである。

そのユーチューブの画像の中にペンデュラムウェーブ(Pendulum Waves)という実験装置があった。
これは周期の異なる複数の振り子を同時に振らせて、数分後に同期するまでの変化を楽しむというもので、なかなか興味深い物理実験なのである。
https://www.youtube.com/watch?v=yVkdfJ9PkRQ

一つ一つの振り子は、ただただ普通に揺れているだけなのに、何故か多数の振り子を並べると不思議な波が見えてくるという、興味深い装置なのである。
単振動と位相差と波の性質というものが組み合わさった、良く出来た実験装置なのである。

この装置自体はそう複雑なものではないのだが、きちんと動作する振り子を作るのはけっこう難しそうなのである。
しかし、コンピューター上なら簡単に作れそうである。

アニメ表示のようなプログラムになるはずなので、うまく出来たら私も試しにユーチューブに出してみようかとも思うのである。

そこで、パソコンに入っているVC++、マイクロソフトの(Visual Studio 2017)を使って作ってみることにした。
しかしながら、怠けていたせいでパソコンのプログラミングは全て私の頭の中から消去されているのである。
何時もながら、何年経っても未だに「Hello world」から開始しなければならないのが情けないのであるが、私の頭脳ではいかんともしがたいものなのである。



プログラミングの前に、まず、パソコンの画面上で動画を見られるようにするにはどうすれば良いのか?

ネットで検索すると、アニメの動画はだいたい1秒間に24〜30コマ(フレームと言うらしい)で表示して動かしているらしいのである。
つまり、だいたい33〜42ミリ秒毎に少しずつ変えた画像を書き直せば良いということになる。

具体的には、タイマーを作って定期的にイベントを発生させ、そのイベント毎に画像を再描画するという構造になる。
当然ながら、短い間隔で多くの画面を表示すれば(フレーム数を多くする)、画像は滑らかに綺麗に動くのであるが、当然ながら画面を沢山作るのにはパソコンの計算能力に負担がかかってしまい、それはそれでトラブルの元になる。
動画の計算しやすさと、パソコンの能力と、どの程度の滑らかさが必要かのバランスの問題になってくるのである。


動画を作るのは上記のような方法で行うとして、そもそも振り子の状態jをどう作り出して、どう表示するのか?

test_pgm100
一番簡単な方法は、左のように画面に小さな黒い丸を描き、それをサインカーブで定期的に左右に振らせれば良いかと思うのである。

しかし、どのくらいの周期で振らせるのが一番自然に見えるのであろうか?
このへんはどうもよく解らないので、とりあえずは1秒で一往復という設定で計算を行ってみる。

次に、複数の振り子を、ある周期で同期させるには、どんな計算が必要か考える。
test_pgm101
スタート時には一直線に並んでおり。
test_pgm102
開始すると、周期が異なるので、だんだんとずれていく。

ある時間が経つと、ずれがちょうど一周分となり、また一直線に同期するという動作になる。


問題は、ある時間が経つと、全部の振り子がちょうど一直線に並ぶということはどういう状況になるのかということなのである。?

それは、全ての振り子の角度が一致するということなのである。
つまり、ある時間には全ての振り子が、スタートしてからの角度が2Πラジアンの整数倍になるというわけである。

最小でその条件になるには、振り子が一つ増える毎に2Πラジアンずつ増加(減少)するような角速度というわけである。

たとえば、振り子が2つで、基準となる振り子の周期が1秒として、1分後(60回目)に同期する為には、もう一つの振り子は、1分間にちょうど61回の往復運動をすることになる。
つまり61/60だけ早い周期で振り子が動作することになる。
※本物の振り子では、若干短い糸で吊った振り子になる。

振り子が3つになれば、3つ目の振り子は62/60だけ早い周期で動作するという条件になるのである。



ということで、早速プログラムを作っていく。

まず、動作の情報を変数で定義する。
新しいpendulum01というプロジェクトを作り、pendulum01View.hに以下のような変数を定義する。

public:
	int test_count;
	double PI2 = 6.283185307179586;  // 円周率×2 かなりいいかげんな桁数
	
	int step_count = 20;		// 振り子の数
	double step_Synchro = 60.0;	// 同期する秒数
	int step_val = 25;		// フレーム数、n回で一秒
	double step_rad[20];		// フレーム毎の角度変化
	double step_rad_all[20];	// 現在の角度

	CString str01;
	SYSTEMTIME systime_s;  // 処理の開始時刻
	SYSTEMTIME systime_e;  // 処理の終了時刻

次に、その初期設定を行う処理を、pendulum01View.cppのコンストラクターに入れる。

Cpendulum01View::Cpendulum01View()
{
	// TODO: 構築コードをここに追加します。
	int ix;
	double step_def;

	step_def = (PI2 / step_Synchro) / step_val;
	for (ix = 0; ix < step_count; ix++) {
		step_rad[ix] = (PI2 / step_val) + (ix * step_def);
		step_rad_all[ix] = 0.0;
	}
}
やっていることは、step_defには1周分の角度を同期する時間で割り、それをまた1周のフレームで割る。
つまり、1フレーム毎に移動する角度を算出している。

ループの中では、1番最初の振り子ではixが0なので値はPI2 / step_valのみになり、2×Πをフレーム数で割っただけになる。

次のループでは、その値に、振り子のn番目分の値を加算し、同期する秒数後にちょうど2×Πだけずれるようにする。
以下、同様にN個分の振り子を算出する。

step_rad_all[ix] = 0.0;は、全部の振り子が最初は角度0度からのスタートということになる。
これを全部Π/2からスタートにすれば、コサインカーブの振り子になるはずである。


次に表示のハンドラーを作成する。

void Cpendulum01View::OnDraw(CDC* pDC)
{
	Cpendulum01Doc* pDoc = GetDocument();
	ASSERT_VALID(pDoc);
	if (!pDoc)		return;

	int nx;
	double x;
	double y;
	double r;
	int ix;
	int iy;

	// TODO: この場所にネイティブ データ用の描画コードを追加します。
	pDC->SelectStockObject(BLACK_BRUSH);
	//pDC->Ellipse(300, 200, 310, 210);

	// 振り子の表示
	iy = 50;
	for (nx = 0; nx < step_count; nx++) {

		x = sin(step_rad_all[nx])*90.0 + 120.0;
		ix = x;
		pDC->Ellipse(ix, iy, ix + 10, iy+10);
		iy = iy + 10;

	}

	// 円運動の表示
	r = 90.0;
	for (nx = 0; nx < step_count; nx++) {

		x = sin(step_rad_all[nx])*(r - nx * 3) + 120.0;
		y = -cos(step_rad_all[nx])*(r - nx * 3) + 400.0;
		ix = x;
		iy = y;
		pDC->Ellipse(ix, iy, ix + 10, iy + 10);

	}

}
やっていることは単純である。

pDC->SelectStockObject(BLACK_BRUSH);で黒丸を描くことを宣言している。

上のforループは、振り子の数だけループを行い、xに出てきたサインカーブの位置にpDC->Ellipse(ix, iy, ix + 10, iy+10);で黒丸を描くだけである。

下のforループは、同じように振り子の数だけループを行い、xに出てきたサインカーブとyに出てきたコサインカーブの位置にpDC->Ellipse(ix, iy, ix + 10, iy+10);で黒丸を円のループで描くだけである。
全部を同じ半径で描くと、若干見にくいので、振り子毎に半径を変えている。
左右の振り子だけではなく、円のループを描くと、またちょっと違う状況が見えて、なかなか興味深いものになるのである。
やっていることは上下のループでほぼ同じなので、一つのループで処理は可能なのだが、判りやすくするために二つのままにしてある。


test_pgm103
次のvoid Cpendulum01View::On32771()というコマンドハンドラーが、タイマー一回毎の処理になる。
実際はステップ実行というコマンドをメニュー上に作り、そのコマンドハンドラーである。

一回毎の表示動作試験やらデバックで使用したものをそのまま使いまわす。

InvalidateRect(NULL, TRUE);が重要な命令で、trueをfalseにすると、描画のときにクリアされずに重ねた描画になる。


void Cpendulum01View::On32771()
{
	// TODO: ここにコマンド ハンドラー コードを追加します。
	// ステップ実行

	int nx;

	for (nx = 0; nx < step_count; nx++) {
		step_rad_all[nx] = step_rad_all[nx] + step_rad[nx];
	}

//	InvalidateRect(NULL, FALSE);
	InvalidateRect(NULL, TRUE);
}


次がタイマー割り込みのイベントハンドラーで、今回は1秒に25回くらい呼び出されることになる。

void Cpendulum01View::OnTimer(UINT_PTR nIDEvent)
{
	// TODO: ここにメッセージ ハンドラー コードを追加するか、既定の処理を呼び出します。
	CView::OnTimer(nIDEvent);

	//test_count++;
	if (test_count >= 1500) {
		KillTimer(1);
		GetLocalTime(&systime_e);		// 終了時刻を取得
		str01.Format(_T("タイマー終了   開始 %u:%u:%u.%u 終了 %u:%u:%u.%u"),
			systime_s.wHour, systime_s.wMinute, systime_s.wSecond,
			systime_s.wMilliseconds,
			systime_e.wHour, systime_e.wMinute, systime_e.wSecond,
			systime_e.wMilliseconds);
		AfxMessageBox(str01, MB_ICONEXCLAMATION);
	}

	On32771();
}
呼び出されるごとにtest_countをカウントアップしていき、1500回でタイマーを停止している。
このカウントアップで1分を計測してみると、10秒くらいも余計に時間がかかっているのが解る。
タイマーはミリ秒単位のはずなのだが、どうも原因は今のところ不明である。

実際の表示処理はOn32771();という前記したステップ処理のコマンドハンドラーを呼び出しているだけである。


次がタイマーを作り出すコマンドハンドラーである。

タイマーの関数の最初のパラメータの1はタイマーのハンドルで、2番目の(1000/ step_val)はミリ秒、つまり25フレーム/秒となる。
フレームは多い方が滑らかな動画になるのだが、私のオンボロパソコンの能力ではけっこう負担が大きいかもしれないのである。
といって、少ないフレームだと動画がチラつくし、動画がギクシャクする。
バランスの問題なのである。

ただ、どうもちゃんとした秒数では動かずに、1分動かすと10秒くらい余計に動いている。
GetLocalTime(&systime_s);関数は、その時刻を計測しているものである。
まぁ、実時間で何かをするわけではないので、遅れようが進もうが、プログラムの動作には関係が無いのではあるが・・・・


void Cpendulum01View::On32773()
{
	// TODO: ここにコマンド ハンドラー コードを追加します。
	// タイマースタート

	//AfxMessageBox(_T("タイマースタート"), MB_ICONEXCLAMATION);

	if (!SetTimer(1, (1000/ step_val), NULL)) {
		AfxMessageBox(_T("タイマーエラー"), MB_ICONEXCLAMATION);
		return ;
	}
	test_count = 0;
	GetLocalTime(&systime_s);		// 開始時刻を取得

}


動作中のタイマーを停止させるコマンドハンドラー。


void Cpendulum01View::On32773()
{
	// TODO: ここにコマンド ハンドラー コードを追加します。
	// タイマー終了

	KillTimer(1);
}



プログラムを作ってみると、割と単純なサインカーブ、コサインカーブのプログラムなので、授業でも使えそうな感じである。

テレビのニュースでは、これからは義務教育でもプログラミングの授業があるそうなのである。

そして、どこぞの会社が作った小型のルンバみたいなロボットに対して、「前進」、「後退」、「回転」、などと指令を組み合わせて(プログラムして)動かせていたのである。
しかし私の感覚では、そんなことをやらせるより、今回私が作ったようなプログラムを作らせる方がよっぽど数学、物理、プログラムが総合的に理解出来ると思うのである。
もっとも、思い出せば私は中学生の頃には完全な落ちこぼれだったわけで、教育に関して意見を言えるような人間ではないな〜という感じである。



そして、プログラムは出来たのだが、今回のプログラムは動画を作るプログラムなのである。
その動画をどうするかと思うのである。

このHPのサーバーは、あまり大きくないので、動画を入れるのはもったいないのである。
せっかくなのでユーチューブにアップしてみようかとも思っているのである。

動画のアップは、gmailのアカウントから行うらしい。
さて、うまくアップロード出来るのであろうか?

試しにアップロードしたのが以下の画像である。
https://youtu.be/iE5HctFN5Tw


HOME